ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

BP神经网络多输入多输出回归实战:PyTorch实现与数据归一化

BP神经网络多输入多输出回归实战:PyTorch实现与数据归一化 简介一份基于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[ffeature_{i} for i in range(n_features)]) df[output_1] y1 df[output_2] y2 df.to_csv(mimo_dataset.csv, indexFalse) print(f数据集大小: {X.shape}, 输出大小: {y.shape})这段代码做了三件事生成输入特征矩阵X2000行6列构造两个输出变量y1和y2并合并成y2000行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_size0.4, random_state42) X_val, X_test, y_val, y_test train_test_split(X_temp, y_temp, test_size0.5, random_state42) # 在训练集上拟合归一化器 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_state42保证每次运行划分结果一致。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_164, n_hidden_232): 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是默认选项但要看输出量纲是否统一多输出回归的损失函数最常用的是均方误差MSEMean Squared Error它的计算方式是对所有输出神经元的误差求平方和再取平均。PyTorch里对应的是nn.MSELoss。MSE对大的误差非常敏感所以如果某个输出变量的数值范围天然就比另一个大很多即使归一化后这个问题被削弱了仍然建议检查一下归一化后各输出列的数值范围是否一致。如果差一个数量级以上可以按输出维度分别计算损失再加权求和或者直接用nn.SmoothL1LossHuber损失来降低异常值的干扰。这里我选择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(), lr1e-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, dtypetorch.float32), torch.tensor(y_train_scaled, dtypetorch.float32) ) val_dataset TensorDataset( torch.tensor(X_val_scaled, dtypetorch.float32), torch.tensor(y_val_scaled, dtypetorch.float32) ) # 定义batch size并创建DataLoader batch_size 64 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse)这里batch_size64的含义是每个step拿64个样本计算一次梯度。shuffleTrue只在训练集打开它让每个epoch里样本被分成不同的batch组合避免模型学到固定顺序。验证集不shuffle因为验证只是前向计算不需要随机性。训练循环的核心逻辑是前向计算得到预测值计算损失反向传播计算梯度优化器更新权重。每跑完一个epoch在验证集上算一次损失如果验证集损失连续多个epoch不降甚至上升就要考虑early stopping或者降低学习率。model MIMO_MLP(n_features6, n_outputs2) 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(fEpoch {epoch1}/{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, labelTrain Loss) plt.plot(range(1, epochs 1), val_loss_list, labelVal 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(fEarly stopping at epoch {epoch1}) break # 训练结束后恢复最佳权重并保存 model.load_state_dict(best_model_state) torch.save(model.state_dict(), bp_mimo_model.pth)patience50的含义是允许验证集损失连续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的数学性质决定它的输出范围严格限制在01开区间即使网络内部特征已经把信息提取好了最后加上的这个非线性变换等于把输出空间压缩了表达能力被人为砍掉一大截。归一化后真实值在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。我习惯把三个scalerX训练、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的创建明确指定dtypetorch.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_features6, n_outputs2) model.load_state_dict(torch.load(bp_mimo_model.pth)) model.eval() # 测试集预测 with torch.no_grad(): X_test_tensor torch.tensor(X_test_scaled, dtypetorch.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输出 {i1}: RMSE{rmse:.4f}, MAE{mae:.4f}, R2{r2:.4f})R2的取值逻辑在这里需要特别说明R2越接近1越好但并不是只有大于0.9才说明模型有用。工业场景里如果数据本身噪声很大R2在0.7以上就算可用如果R2小于0.3说明模型在这个输出上的预测能力基本等于用均值去猜这个输出要么和输入特征的相关性太弱要么数据质量有问题。RMSE的单位和输出变量一致报告给非技术同事时直接说“平均误差是多少”就行R2则适合用来对比不同输出之间的可预测性。再进一步把预测值和真实值画成散点图横轴真实值纵轴预测值理想情况是所有点落在yx这条直线上。如果散点图呈现明显的弧线弯曲说明还有非线性关系没学到考虑增加一个隐藏层或者扩大隐藏层宽度如果散点围绕直线但离散程度大说明数据噪声占主导再加深网络也不会有明显改善。最后补充一个真实项目里高频用到的小技巧在验证阶段对每个输出计算置信区间或者误差带。具体做法是把测试集的预测误差按真实值大小分箱比如把真实值分成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)能少踩很多玄学的坑。希望这篇文章能帮你在多输入多输出回归这条路上少走一些弯路。本文还有配套的精品资源点击获取
返回列表