简介:一份基于Python的BP神经网络多输入多输出回归模型搭建资源,面向机器学习初学者及需要解决非线性回归问题的开发者。资源聚焦BP反向传播原理,涵盖网络构建、权重初始化、正向传播、反向传播、迭代训练与预测评估等完整流程。压缩包共3个文件,包含bp.py代码文件以及两个xlsx数据文件(输入数据x.xlsx、输出数据y.xlsx),整体大小仅20KB,轻量易读。代码中演示了numpy、pandas、matplotlib的典型用法,并绘制损失变化曲线和真实值-预测值对比图,便于直观验证模型效果。该资源已有960人学习使用,适合课程设计、毕业设计或自学实践;在此基础上,还可通过调整隐藏层节点、学习率等超参数,或引入归一化、缺失值处理等手段进一步提升模型泛化能力。
1. BP神经网络做多输入多输出回归:这套方案到底解决什么问题
BP神经网络(反向传播神经网络)在回归任务里最常见的用法是单输出,也就是用若干特征去预测一个连续值。但实际工程里,很多场景天生就是多输入多输出的:比如根据设备的温度、振动、电流、转速去同时预测剩余寿命和能耗,或者根据原料的配比和工艺参数同时预测产品的多个质量指标。这类问题如果拆成多个单输出模型分别训练,不仅训练成本翻倍,还会丢失输出变量之间本来存在的相关性信息。BP神经网络实现多输入多输出回归模型,本质上就是调整输出层的神经元数量,让网络最后一层同时输出多个值,配合合适的损失函数和评价指标去完成训练和预测。本文用Python和PyTorch把这套流程完整搭建起来,附带一个可直接替换的示例数据集,从数据预处理、网络结构设计到训练调参和排错,一步步讲清楚。
这套方案适合手里有结构化数据、想做多目标预测的工程师和学生。不需要分布式训练,不需要GPU,一台普通笔记本就能跑通。你只需要有Python基础,装好numpy、pandas、torch和scikit-learn这几个常见库就行。下文所有代码都是完整可运行的,数据集用的是我自己构造的一个模拟工况数据,和真实场景的区别只在数据格式上,换成你自己的Excel或CSV文件后,要改的代码不超过三行。
2. 数据准备与归一化:多输出回归的起点不是网络结构,而是数据格式
2.1 输入数据和输出数据的组织方式
先明确一个关键认知:多输入多输出回归的数据组织方式和单输出回归没有任何区别,输入是一个二维矩阵,每行是一个样本,每列是一个特征;输出也是一个二维矩阵,每行对应一个样本,每列是一个输出变量。唯一要注意的是,输出变量有几个,输出矩阵就有几列。
我先用numpy构造一个示例数据集。假设我们有6个输入特征,2个输出变量,一共2000个样本。特征之间加了点相关性,输出则用特征的非线性组合生成,这样可以模拟真实场景里那种“说不出具体公式”的映射关系:
import numpy as np import pandas as pd np.random.seed(42) n_samples = 2000 n_features = 6 # 生成输入特征,每个特征都是0到1之间的随机数 X = np.random.rand(n_samples, n_features) # 给特征加一点相关性,避免各特征完全独立 X[:, 2] = 0.6 * X[:, 0] + 0.4 * X[:, 2] X[:, 4] = 0.5 * X[:, 1] + 0.5 * X[:, 4] # 构造两个输出变量,输出1主要依赖特征0、1、2,输出2主要依赖特征3、4、5 y1 = 3.0 * X[:, 0] ** 2 + 2.0 * np.sin(2 * np.pi * X[:, 1]) + 1.5 * X[:, 2] + 0.5 y2 = 2.5 * X[:, 3] * X[:, 4] + 1.0 * np.cos(2 * np.pi * X[:, 2]) + 0.8 * X[:, 5] + 0.3 y = np.column_stack([y1, y2]) # 保存成CSV,方便你替换自己的数据 df = pd.DataFrame(X, columns=[f'feature_{i}' for i in range(n_features)]) df['output_1'] = y1 df['output_2'] = y2 df.to_csv('mimo_dataset.csv', index=False) print(f"数据集大小: {X.shape}, 输出大小: {y.shape}")这段代码做了三件事:生成输入特征矩阵X(2000行6列),构造两个输出变量y1和y2并合并成y(2000行2列),最后保存成CSV文件。如果你想用真实数据集,把pd.read_csv读进来的数据拆成X和y就行,核心要求是X和y都是二维矩阵,行数一致,列数分别对应特征数和输出数。
2.2 训练集验证集划分与归一化的正确顺序
多输出回归一个很容易踩坑的地方是归一化的顺序和范围。很多教程直接对全部数据做归一化再划分训练集和验证集,这在实际项目里是错的,因为验证集是模拟未知新数据的,它的统计信息(最小值、最大值、均值、标准差)不应该参与归一化参数的拟合。
常见做法是先用train_test_split把数据分成训练集、验证集和测试集三份,然后在训练集上fit归一化器,再用同一个归一化器去transform验证集和测试集。对输出变量y也要做同样的归一化,原因有两个:一是多个输出变量的量纲可能差别很大,一个在0.1量级,一个在1000量级,如果不归一化,损失函数会被大数值的输出变量主导,网络会把几乎所有学习能力都花在大量纲的输出上;二是很多激活函数在0附近梯度更陡,归一化后训练收敛更快。这里我用sklearn的MinMaxScaler,因为它可以把数据压缩到0到1之间,对回归任务很够用:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler # 划分训练集(60%)、验证集(20%)、测试集(20%) X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.4, random_state=42) X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42) # 在训练集上拟合归一化器 scaler_X = MinMaxScaler() scaler_y = MinMaxScaler() X_train_scaled = scaler_X.fit_transform(X_train) y_train_scaled = scaler_y.fit_transform(y_train) # 用训练集上拟合好的归一化器去转换验证集和测试集 X_val_scaled = scaler_X.transform(X_val) y_val_scaled = scaler_y.transform(y_val) X_test_scaled = scaler_X.transform(X_test) y_test_scaled = scaler_y.transform(y_test) print(f"训练集: {X_train_scaled.shape}, 验证集: {X_val_scaled.shape}, 测试集: {X_test_scaled.shape}")这里有一个重要细节:fit_transform只对训练集用,验证集和测试集只能用transform。如果在验证集上重新fit归一化器,归一化的基准就不统一了,模型在验证集上的表现会虚高,因为验证集的分布信息泄漏进了归一化参数。另外,train_test_split里面我连续调了两次,第一次拆出40%的临时数据,第二次把40%对半拆成验证集和测试集,这样得到了60%训练、20%验证、20%测试的划分,random_state=42保证每次运行划分结果一致。
2.3 归一化后的逆变换
模型预测出来的结果也是在0到1范围内的,最后要还原成真实量纲的值,这里必须使用训练时保存下来的scaler_y做inverse_transform,不能新建一个归一化器去还原,否则还原结果就是错的。逆变换操作要在评价指标计算之前完成,因为RMSE、MAE这些指标应该基于真实量纲计算,这样报告给业务方的时候才有实际含义。下文的模型训练和评估部分会反复用到这个逆变换。
3. 搭建多输入多输出BP神经网络:结构设计、损失函数与评估指标
3.1 网络结构怎么定:输入层、隐藏层、输出层的神经元数量
确定网络结构之前先明确一件事:BP神经网络做多输出回归的理论依据是万能逼近定理,即一个包含足够多隐藏神经元的单隐藏层前馈网络,可以以任意精度逼近定义在紧集上的连续函数。所以不需要把网络做得特别深,隐藏层数量2到3层对这个规模的数据集完全够用。
输入层神经元数量等于特征数量,这里X有6个特征,所以输入层是6个神经元。输出层神经元数量等于输出变量数量,这里y有2列,所以输出层是2个神经元。隐藏层的单元数量没有统一公式,行业内有个经验起点:取输入维度的2到4倍,或者取输入维度和输出维度的平均值再乘一个系数。我一般从较大的值开始,比如128或64,然后用验证集损失来确定要不要往小的调。
网络结构层面还有一个重要决定:隐藏层激活函数。回归任务里最常用的是ReLU,因为Sigmoid和Tanh在深层网络中容易梯度消失,而且Sigmoid输出有均值偏移问题。输出层不用激活函数,保持线性输出,因为回归任务的输出范围是任意实数空间,如果输出层加了Sigmoid,输出被限制在0到1之间,等于人为给模型加了一层约束。下面用PyTorch实现一个两层隐藏层的BP网络:
import torch import torch.nn as nn class MIMO_MLP(nn.Module): def __init__(self, n_features, n_outputs, n_hidden_1=64, n_hidden_2=32): super(MIMO_MLP, self).__init__() self.fc1 = nn.Linear(n_features, n_hidden_1) self.fc2 = nn.Linear(n_hidden_1, n_hidden_2) self.fc3 = nn.Linear(n_hidden_2, n_outputs) self.relu = nn.ReLU() def forward(self, x): x = self.relu(self.fc1(x)) x = self.relu(self.fc2(x)) x = self.fc3(x) # 输出层不加激活函数 return x这段代码定义了3个全连接层,前两个层后面接ReLU激活,第三层直接输出。nn.Linear的第一个参数是输入维度,第二个参数是输出维度。最后forward里第三层不加self.relu,这是多输出回归网络结构设计里的一个重要细节。如果输出层加了激活函数,训练时会发现损失降不下去,预测值总是被压缩在一个范围内。
3.2 损失函数的选择:MSE是默认选项,但要看输出量纲是否统一
多输出回归的损失函数最常用的是均方误差(MSE,Mean Squared Error),它的计算方式是对所有输出神经元的误差求平方和再取平均。PyTorch里对应的是nn.MSELoss。MSE对大的误差非常敏感,所以如果某个输出变量的数值范围天然就比另一个大很多,即使归一化后这个问题被削弱了,仍然建议检查一下归一化后各输出列的数值范围是否一致。如果差一个数量级以上,可以按输出维度分别计算损失再加权求和,或者直接用nn.SmoothL1Loss(Huber损失)来降低异常值的干扰。
这里我选择MSE并给出理由:MSE的梯度在误差较大时也较大,收敛速度快;对于模拟数据这种噪声可控的场景,MSE足够稳定;多输出回归的常用评价指标RMSE就是从MSE开根号来的,训练指标和评价指标一致的话,后续复盘也更直观。
criterion = nn.MSELoss()这一段没什么可调的,唯一要记的是criterion接收两个参数:预测值和真实值,两者的形状都必须是(batch_size, n_outputs),不能是(n_outputs,)或者(batch_size, 1, n_outputs),否则维度对不上会报错或隐式广播出问题。
3.3 优化器与学习率:Adam是起点,SGD是调优方向
优化器选择上,我一般先用Adam,因为它自带自适应学习率,对学习率的初始选择不那么敏感,适合快速跑通基线。Adam之后如果想让模型收敛得更稳,可以换成SGD加动量,SGD虽然收敛慢,但泛化能力往往比Adam好一点。学习率初始值通常取1e-3,如果损失在训练初期震荡剧烈,降低到1e-4;如果收敛太慢,提高到3e-3,注意不要超过1e-2。
import torch.optim as optim optimizer = optim.Adam(model.parameters(), lr=1e-3)代码逻辑上没有什么多输出特有的调整,但有一个和batch size配合的细节:如果数据量少,把batch size设小一点,比如16或32,相当于每个step用更少的样本估计梯度,梯度噪声大一点但对小数据集来说反而有正则化效果。batch size如果太大,比如256以上,梯度估计平滑了,收敛过程也会变慢,因为每个epoch更新次数变少了。
3.4 评估指标:R2、RMSE、MAE要分输出算
多输出回归不能只看一个总体指标就下结论,因为不同输出的可预测难度不同。我一般分两步评估:第一步看整体MSE,训练过程中用它监控收敛;第二步在预测完成后,对每个输出维度分别计算R2(决定系数)、RMSE和MAE。R2是回归模型最常用的指标,数值越接近1说明模型对这部分输出的解释力越强。多输出场景下,如果一个输出维度的R2明显低于其他维度,说明这个输出和其他特征的相关性没有学到,需要检查特征重要性和数据质量。
这些指标在代码实现上我会放在下一章的训练循环和预测评估部分一起落地,这里先明确原则:多输出回归的评估永远是“分输出看”,不要把所有输出混在一起算一个R2。
4. 模型训练与参数调节:一个完整的训练脚本,跑通后再谈优化
4.1 构建DataLoader与训练循环
数据准备好、网络定义好之后,接下来是训练管线。先把numpy数组转成PyTorch的Tensor,然后用TensorDataset和DataLoader打包成批数据。DataLoader的作用是自动把数据切分成batch,每个batch随机抽取部分样本送入网络,这样训练时每个step看到的都是一小批样本而不是全量数据。
from torch.utils.data import DataLoader, TensorDataset # 把numpy数组转成torch.Tensor并打包成数据集 train_dataset = TensorDataset( torch.tensor(X_train_scaled, dtype=torch.float32), torch.tensor(y_train_scaled, dtype=torch.float32) ) val_dataset = TensorDataset( torch.tensor(X_val_scaled, dtype=torch.float32), torch.tensor(y_val_scaled, dtype=torch.float32) ) # 定义batch size并创建DataLoader batch_size = 64 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)这里batch_size=64的含义是每个step拿64个样本计算一次梯度。shuffle=True只在训练集打开,它让每个epoch里样本被分成不同的batch组合,避免模型学到固定顺序。验证集不shuffle,因为验证只是前向计算,不需要随机性。
训练循环的核心逻辑是:前向计算得到预测值,计算损失,反向传播计算梯度,优化器更新权重。每跑完一个epoch,在验证集上算一次损失,如果验证集损失连续多个epoch不降甚至上升,就要考虑early stopping或者降低学习率。
model = MIMO_MLP(n_features=6, n_outputs=2) model.train() # 切换到训练模式,影响Dropout和BatchNorm的行为(本例中没有这两个层,但养成习惯) epochs = 500 train_loss_list = [] val_loss_list = [] for epoch in range(epochs): epoch_train_loss = 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() # 梯度清零,否则PyTorch会累加梯度 outputs = model(batch_X) loss = criterion(outputs, batch_y) loss.backward() # 反向传播计算梯度 optimizer.step() # 更新权重 epoch_train_loss += loss.item() * batch_X.size(0) # 计算当前epoch的平均训练损失 epoch_train_loss /= len(train_loader.dataset) # 在验证集上做一次前向计算记录损失 model.eval() epoch_val_loss = 0.0 with torch.no_grad(): for batch_X, batch_y in val_loader: outputs = model(batch_X) loss = criterion(outputs, batch_y) epoch_val_loss += loss.item() * batch_X.size(0) epoch_val_loss /= len(val_loader.dataset) model.train() train_loss_list.append(epoch_train_loss) val_loss_list.append(epoch_val_loss) if (epoch + 1) % 50 == 0: print(f"Epoch {epoch+1}/{epochs}, Train Loss: {epoch_train_loss:.6f}, Val Loss: {epoch_val_loss:.6f}")这个训练循环有几个值得展开的细节。optimizer.zero_grad()是必须的,因为PyTorch默认会累加梯度,如果不清零,下一轮的梯度会和上一轮叠加,训练必然发散。loss.item()是把损失值从Tensor中取出为Python数值,方便记录和打印,不会影响计算图。with torch.no_grad()告诉PyTorch在验证阶段不需要构建计算图,省内存也提速,因为验证集只需要前向结果,不需要反向传播。
model.eval()和model.train()的切换这里没有直接作用,因为当前网络结构里没有Dropout或BatchNorm层,但如果你后续在网络里加了Dropout层做正则化,忘了切eval()模式会导致验证集结果带随机性,每次验证的损失都在变,不好判断模型到底收敛没有。这是个细微但常见的坑。
4.2 损失曲线的解读:三个典型形态
训练完成后,把loss_list画成曲线是判断模型状态的第一步。我用matplotlib把训练损失和验证损失画在同一张图里:
import matplotlib.pyplot as plt plt.plot(range(1, epochs + 1), train_loss_list, label='Train Loss') plt.plot(range(1, epochs + 1), val_loss_list, label='Val Loss') plt.xlabel('Epoch') plt.ylabel('MSE Loss') plt.legend() plt.title('BP MIMO Regression Training Curve') plt.show()曲线有三种典型形态。第一种,训练损失和验证损失一起稳步下降并趋于平稳,这是正常状态,说明网络容量和数据复杂度匹配,可以停止训练。第二种,训练损失持续下降但验证损失先降后升,这是过拟合的信号,说明网络把训练集的特征细节记住了,但对新数据的泛化能力在变差。解决办法是提前停止训练,或者给网络加Dropout层、调低隐藏层神经元数量。第三种,训练损失从一开始就在震荡,或者下降非常缓慢,说明学习率设置不合理,优先把学习率降低到1e-4试试,或者换SGD优化器。
如果一个epoch内训练损失就在震荡,那还要检查batch size和数据是否归一化到位。一般经验是,归一化做好、学习率初始1e-3的情况下,MSE损失会在头50个epoch里有明显下降,如果完全没有变化,直接怀疑代码逻辑问题而不是模型问题。
4.3 早停与模型保存:训练不是等到500个epoch跑完才结束
实际训练里“设置一个固定的epoch数量”并不是好习惯,因为不同数据集收敛的速度差别很大。我的做法是加一个early stopping机制:如果验证集损失在指定的patience个epoch内没有降到新低,就停止训练并恢复最佳模型权重。这样可以避免两个问题:一是过拟合时验证损失升高之后白跑了很多epoch;二是训练时间浪费在没有意义的收敛上。
best_val_loss = float('inf') best_model_state = None patience = 50 epochs_no_improve = 0 for epoch in range(epochs): # 上面train_loader和val_loader的循环省略,直接写验证损失下降判断逻辑 if epoch_val_loss < best_val_loss: best_val_loss = epoch_val_loss best_model_state = model.state_dict() epochs_no_improve = 0 else: epochs_no_improve += 1 if epochs_no_improve >= patience: print(f"Early stopping at epoch {epoch+1}") break # 训练结束后恢复最佳权重并保存 model.load_state_dict(best_model_state) torch.save(model.state_dict(), 'bp_mimo_model.pth')patience=50的含义是允许验证集损失连续50个epoch不下降,如果50个epoch内都没有创下新低,就认为模型已经到极限了。这个值不能设太小,比如设10个epoch,损失只是暂时平台期就被停了,模型还没收敛完。也不能设太大,否则早停就失去了意义。我习惯先跑一轮完整训练看曲线大概在哪个epoch附近收敛,再回头设置合适的patience。
model.state_dict()保存的是网络中所有的权重和偏置参数,torch.load之后要重新构建一个相同结构的模型实例才能加载。这里不要用torch.save(model, ...)保存整个模型对象,因为保存的pickle文件里包含当前Python环境的信息,换环境加载容易出问题,只保存state_dict是最稳妥的。
4.4 超参数选择的优先级排序
当模型能正常跑通之后,你一定会面临调参的问题。多输出回归BP神经网络的超参数不算多,但调整优先级是有讲究的。我最先调的是学习率,因为学习率对训练稳定性的影响最直接;其次是隐藏层神经元数量和隐藏层层数,决定网络容量;然后是batch size,影响梯度估计的平滑程度和训练速度;最后才是epoch数量和patience这些“止损”参数。
一个常见误用是用过大的隐藏层追求训练集上的完美拟合。隐藏层神经元数量从64加到256,训练损失确实可以降到很低,但验证集损失往往差不太多甚至更差。对2000条数据这种规模的数据集,64个隐藏神经元的网络已经足够表达绝大多数函数关系,不需要更大。扩容之前先确认数据量是否足够,而不是盲目加参数。
5. 避坑指南:多输出回归BP神经网络最容易翻车的5个细节
5.1 输出层误加激活函数导致预测值被压缩
现象:模型训练损失降到0.01以下,但用测试集预测出来的结果范围总是集中在0.2到0.8之间,真实值在0到1内的分布算正常,但真实值如果大于1,预测值永远追不上。
原因:输出层加了Sigmoid或Tanh激活函数。Sigmoid的数学性质决定它的输出范围严格限制在(0,1)开区间,即使网络内部特征已经把信息提取好了,最后加上的这个非线性变换等于把输出空间压缩了,表达能力被人为砍掉一大截。归一化后真实值在0到1之间,所以用归一化的数据测不出这个问题,一旦逆变换回真实量纲,预测值就顶在天花板上了。
解决:检查forward方法里最后一层是否有激活函数,输出层必须是线性层且不做任何激活。如果你的情况是输出值天然就在0到1之间(比如概率预测),那输出层加Sigmoid是合理的,但要做x = torch.sigmoid(self.fc3(x))而不是在__init__里混在一起。
5.2 对验证集和测试集重复fit归一化器
现象:训练集域上效果很好,测试集误差也还行,但模型部署上线之后对新数据的预测完全跑偏,误差是训练时的10倍以上。
原因:数据预处理阶段对验证集和测试集单独调用了fit_transform,相当于用测试集的分布信息重新计算了归一化参数,这属于数据泄漏。模型在训练时见到的输入分布范围是训练集的min和max,但测试时输入分布范围已经被重新变换过了,两头对不上。这个问题在训练阶段很难发现,因为测试集误差是在泄漏的归一化条件下计算的,不代表真实泛化能力。
解决:严格遵循scaler.fit_transform(X_train) -> scaler.transform(X_val) -> scaler.transform(X_test)的顺序,绝对不要在验证集或测试集上调用fit或fit_transform。我习惯把三个scaler(X训练、y训练、以及如果需要的话X测试预检)写在一个配置区域,防止忘记。
5.3 多输出量纲差异过大导致训练偏向某一输出
现象:两个输出变量,一个值域在1000到2000,一个在0到1之间,归一化和不归一化都试了,模型总是一个输出预测得很好,另一个完全没学到规律。
原因:即使做了MinMax归一化,如果两个输出之间还存在非线性关系导致的分布差异,MSE损失会把梯度向数值误差大的输出方向偏移。具体来说,MSE是平方损失,大误差项在反向传播中贡献的梯度占主导。归一化能解决线性量纲差异,但解决不了分布形态差异。
解决:最直接的方法是检查归一化后两个输出列的最小值和最大值是否都落在0到1之间。如果差别明显,可以按输出维度单独计算损失,然后给每个输出加一个权重,例如loss = 0.5 * mse(output[:, 0], y[:, 0]) + 0.5 * mse(output[:, 1], y[:, 1]),权重可以根据每个输出的方差倒数的比例来设置。另一个方法是用StandardScaler替代MinMaxScaler,它把数据变成零均值单位方差,对不同量纲的鲁棒性更强。
5.4 训练损失下降但验证损失始终不降
现象:训练损失从0.5降到0.01,验证损失停在0.3附近不动,训练曲线和验证曲线从一开始就分叉,过拟合在早期就出现了。
原因:相对于数据集规模,网络的隐藏层神经元数量太多了。2000个样本撑不起一个128神经元两隐藏层的网络对6个输入和2个输出的映射。网络有足够能力把训练集的每一个样本背下来,但无法提炼出泛化规则。
解决:先把第一隐藏层降到32,第二隐藏层降到16,看看验证损失是否改善。如果网络容量降下来之后验证损失还是高,那问题可能出在数据本身,比如特征和输出的关系太弱或者样本里有异常值。加Dropout也是替代方案,但优先去减小网络容量,因为Dropout适合网络已经训练好的情况下做正则化,容量本身过大时Dropout的效果有限。
5.5 PyTorch版本差异和MPS设备问题导致训练失败
现象:代码是在Windows上写的用CPU跑的,拿到Mac上跑,训练每一个batch时报Device-side assert triggered,或者RuntimeError: MPS framework out of memory。
原因:PyTorch在Apple Silicon芯片上的MPS后端对某些操作的支持没有CUDA和CPU那么完整,一些在CPU上没问题的操作在MPS上会报错。这不是数据或模型的逻辑问题。
解决:第一,代码里所有torch.tensor的创建明确指定dtype=torch.float32,防止默认的float64在MPS上不受支持;第二,在训练代码开头加个设备判断,优先用MPS或CUDA,都不可用就用CPU。为了保险,可以直接指定CPU跑,2000条样本的数据量CPU训练也就几十秒,完全不需要GPU加速。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 如果想在Mac上跑MPS,可以改成下面这个判断 # device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') model = MIMO_MLP(...).to(device)nn.Linear在初始化时会把权重张量创建在CPU上,.to(device)会把模型的所有参数移动到指定设备上。训练数据也要做同样的.to(device)操作,否则Tensor在CPU而模型在MPS上运行时,PyTorch会报设备不匹配错误。如果设备切换玩不明白,老老实实用CPU跑是最稳的,这个项目的数据量根本不需要GPU。
6. 模型验证与结果解读:用R2和误差分布判断模型是否值得部署
训练完和加载模型之后,最后一步是在测试集上做完整评估。测试集从训练开始就被隔离出来,到这一步才使用。先把测试集数据送去预测,然后逆归一化,再逐个计算评价指标。
# 重新构建模型并加载保存的权重 model = MIMO_MLP(n_features=6, n_outputs=2) model.load_state_dict(torch.load('bp_mimo_model.pth')) model.eval() # 测试集预测 with torch.no_grad(): X_test_tensor = torch.tensor(X_test_scaled, dtype=torch.float32) y_pred_scaled = model(X_test_tensor).numpy() # 逆归一化回真实量纲 y_pred = scaler_y.inverse_transform(y_pred_scaled)inverse_transform用的必须是训练时的scaler_y,它的min_和scale_属性储存了训练集每个输出列的最小值和极差,计算公式是y_real = y_scaled * (max - min) + min。手动实现这个公式时经常搞错属性名,直接用sklearn的inverse_transform最稳妥。
接下来是分输出计算评价指标:
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score for i in range(2): rmse = np.sqrt(mean_squared_error(y_test[:, i], y_pred[:, i])) mae = mean_absolute_error(y_test[:, i], y_pred[:, i]) r2 = r2_score(y_test[:, i], y_pred[:, i]) print(f"输出 {i+1}: RMSE={rmse:.4f}, MAE={mae:.4f}, R2={r2:.4f}")R2的取值逻辑在这里需要特别说明:R2越接近1越好,但并不是只有大于0.9才说明模型有用。工业场景里如果数据本身噪声很大,R2在0.7以上就算可用;如果R2小于0.3,说明模型在这个输出上的预测能力基本等于用均值去猜,这个输出要么和输入特征的相关性太弱,要么数据质量有问题。RMSE的单位和输出变量一致,报告给非技术同事时直接说“平均误差是多少”就行,R2则适合用来对比不同输出之间的可预测性。
再进一步,把预测值和真实值画成散点图,横轴真实值,纵轴预测值,理想情况是所有点落在y=x这条直线上。如果散点图呈现明显的弧线弯曲,说明还有非线性关系没学到,考虑增加一个隐藏层或者扩大隐藏层宽度;如果散点围绕直线但离散程度大,说明数据噪声占主导,再加深网络也不会有明显改善。
最后补充一个真实项目里高频用到的小技巧:在验证阶段对每个输出计算置信区间或者误差带。具体做法是把测试集的预测误差按真实值大小分箱,比如把真实值分成10个区间,每个区间内计算预测误差的标准差,这样你在向业务方汇报时能明确说“当输出1在50到60之间的时候,预测误差在±2.5以内,可以放心用;超过80之后误差会增大到±6”。这个分箱做法比笼统的RMSE更能指导实际使用决策。
我自己的习惯是把每次训练的关键信息都记下来,包括隐藏层神经元数、学习率、batch size、最终的验证损失和每个输出的R2。这个项目的网络结构不复杂,超参数空间也不大,但还是值得做实验记录。因为BP神经网络对随机初始化比较敏感,同一个超参数组合换一个随机种子,R2波动0.05甚至0.1都是正常的。如果你复现出来的结果和文章里对不上,先检查随机种子是否一致。训练前固定torch.manual_seed(42)和np.random.seed(42),能少踩很多玄学的坑。希望这篇文章能帮你在多输入多输出回归这条路上少走一些弯路。
本文还有配套的精品资源,点击获取