PyTorch前馈神经网络实战:波士顿房价回归预测与避坑指南
发布时间:2026/9/26 15:39:46来源:尧图网络
简介基于PyTorch的前馈神经网络回归项目专注于波士顿房价预测面向高校人工智能、数据科学等专业学生和机器学习进阶者可作为课程设计、毕业设计模板或科研模型调优的基准。项目采用机器学习经典波士顿房价基准数据附带完整副本与训练权重代码结构清晰、注释齐全易于扩展修改网络层数、激活函数及正则化方式。压缩包共16个文件、约2.04MB涵盖Python源码、PyTorch权重文件、训练数据集、依赖配置文件、说明文档、备份文件以及多张网络结构、预测对比、特征对比和损失曲线图便于直观理解模型运作。已有48人学习下载。使用者既可沿源码逐行理解回归预测全流程又能借助图示验证模型精度项目曾作为学位论文课题通过答辩获评96分兼具教学示范与科研参考意义。资源仅限教育科研使用禁止商用。1. 波士顿房价预测用 PyTorch 前馈神经网络跑通第一个回归闭环很多刚入门 PyTorch 的人不是被张量、autograd 卡住而是卡在“我学完基础框架下一步到底该拿什么练手”。分类任务遍地都是回归任务却总找不到一个数据量适中、特征维度不吓人、跑一遍不至于等半小时的案例。波士顿房价预测正好卡在这个位置13 维特征、506 条样本一个两层隐藏层的前馈神经网络就能把 R² 做到 0.75 以上整条链路从 CSV 读取到训练结束不超过两分钟。这套资源把数据准备、模型搭建、训练循环、评估验证四个阶段拆成了完整可复现的工程代码适合刚装完 PyTorch、准备做第一个实战项目的人也适合想回看回归任务里标准化、学习率、过拟合这些老坑怎么处理的熟手。2. 数据准备与标准化先把 CSV 变成张量再做特征缩放2.1 数据集来源与读取别再去找 load_boston() 了波士顿房价数据集最早是 sklearn 里load_boston()一行代码就能加载的但 sklearn 1.2 版本开始因为数据伦理问题把它移除了。你在网上搜到的大部分老教程还在用load_boston()照抄必然报AttributeError。现在主流的做法是直接用 UCI 或个人维护的 CSV 副本字段名和原来完全一致只是少了 sklearn 包装那层皮。import pandas as pd df pd.read_csv(housing.csv) print(df.shape) # (506, 14) print(df.isnull().sum()) # 确认没有缺失值 print(df.head()) X df.drop(columns[MEDV]).values # 13 个特征numpy 数组 y df[MEDV].values.reshape(-1, 1) # 目标房价中位数单位千美元这里有两个地方值得说明。第一X是 506×13 的二维数组特征包括犯罪率、房间数、一氧化氮浓度、到就业中心距离等连续值和少数离散值量纲差距非常大——比如房间数和犯罪率不在一个数量级这正是后面必须做标准化的原因。第二y我特意reshape(-1, 1)成列向量因为后续 PyTorch 的MSELoss期望预测值和目标值形状一致都是[batch_size, 1]不 reshape 会在 loss 计算时报形状不匹配的错。这套工程里我一般直接把X和y都转成torch.float32因为 PyTorch 默认浮点类型是 float32如果 CSV 里读出来是 float64模型权重和输入类型不统一训练时会有隐式转换警告某些机器上甚至会直接报错。2.2 切分顺序与缩放尺度scaler 必须在 split 之后 fit数据切分和特征缩放是波士顿这个项目里最容易埋雷的一步。常见的错误写法是先对整个X做StandardScaler再train_test_split看起来没什么问题但验证集的信息已经透过 scaler 的均值和方差泄漏到了训练过程里。正确顺序是先切分再让 scaler 只学习训练集的统计量然后用同一套统计量去变换测试集。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() X_train scaler.fit_transform(X_train) # 只 fit 训练集 X_test scaler.transform(X_test) # transform 沿用训练集的均值和方差参数说明test_size0.2对应 506 条样本里留 101 条做测试训练集 405 条对这个数据规模来说是合理比例random_state42保证每次跑出来的切分结果一致方便对比实验。StandardScaler的默认行为是每个特征减去均值、除以标准差变换后每个特征均值约 0、方差约 1。为什么回归任务几乎必做这一步因为神经网络对输入尺度敏感如果房间数3~8和犯罪率0~89混在一起喂进去梯度更新会被大数值特征主导模型收敛极慢甚至不收敛。这套资源里有一个我自己常用的检查习惯transform 完打印X_train.mean(axis0)和X_train.std(axis0)看到均值接近 0、标准差接近 1说明缩放没被切分顺序带偏。2.3 特征张量化的细节什么时候用 TensorDataset什么时候手写 Dataset数据量才 400 多条不需要上torch.utils.data.Dataset子类那套完整写法TensorDataset加DataLoader就够用这也是这套工程里推荐的中间路线。import torch from torch.utils.data import TensorDataset, DataLoader X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.float32) train_dataset TensorDataset(X_train_t, y_train_t) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue)TensorDataset的核心作用是自动按索引对齐特征和目标DataLoader每次迭代返回一个 batch 的(x_batch, y_batch)shuffleTrue让每个 epoch 的样本顺序都打乱一次避免模型学到样本排列顺序。两点注意一是dtypetorch.float32必须在创建张量时指定不要等训练时报类型不匹配再回头补二是这个规模的数据集用batch_size16比较稳太大比如 128会让每个 batch 的梯度方向太平均太小比如 1则训练震荡明显。数据准备到这里就结束了下一步是建模。3. 前馈神经网络建模从 nn.Module 到 forward() 的完整实现3.1 为什么是前馈神经网络表格数据的回归任务没必要上 CNN 或 LSTM波士顿房价是结构化表格数据特征之间没有空间位置关系也没有时序依赖。CNN 的卷积核假设相邻位置有局部相关性LSTM 假设序列有前后依赖这两者对 13 维拼起来的特征向量都是无效归纳偏置。前馈神经网络也叫多层感知机 MLP只做一件事每一层对输入做线性变换加非线性激活把特征逐层映射到目标值。正因为结构简单它在这个任务里反而最可靠——参数少、训练快、调参路径清晰。这也是这套资源选择前馈网络而不是其他结构的原因。3.2 模型定义输入 13 维、两层隐藏层、输出 1 维的 MLP模型结构我用的是 13→64→32→1两个隐藏层分别 64 和 32 个神经元输出层不加激活函数。注意输出层不加激活是回归任务的关键因为预测目标是连续房价不需要压缩到 [0,1] 或 [-1,1] 区间。代码里nn.Sequential可以写但我更推荐显式定义forward()调试时能清楚看到每一层的数据流。import torch.nn as nn class BostonMLP(nn.Module): def __init__(self, input_dim13, hidden164, hidden232): super().__init__() self.fc1 nn.Linear(input_dim, hidden1) self.fc2 nn.Linear(hidden1, hidden2) self.fc3 nn.Linear(hidden2, 1) 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 xnn.Linear的参数值得展开说第一个参数是输入特征数第二个是输出神经元数。fc1把 13 维映射到 64 维fc2把 64 维降到 32 维fc3把 32 维压到 1 维输出。隐藏层用 ReLU 是因为它对梯度消失不敏感、计算快在中小型网络上比 tanh 表现更稳。如果你的输入维度变了比如后续做了特征工程加了列input_dim必须同步改否则forward()第一次跑就会在fc1报维度不匹配这个错在 PyTorch 里信息很明确看报错里mat1和mat2的维度就能定位。另一个容易忽略的点是__init__里的super().__init__()不能省PyTorch 的nn.Module靠它注册子模块和参数。漏掉这行model.parameters()会是空的优化器里什么都拿不到训练时 loss 不动。这种现象非常隐蔽因为它不报错只是 loss 恒为同一个值。3.3 初始化策略默认初始化能用但 He 初始化更稳PyTorch 的nn.Linear默认用均匀分布初始化权重范围是±1/sqrt(fan_in)对小网络够用但如果你把网络加深到三层以上或神经元数过百建议换成 He 初始化。它的核心思想是根据 ReLU 的线性修正特性把权重方差放大到2/fan_in保证信号在前向传播时既不被放大到爆炸也不被压缩到消失。def init_weights(m): if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, modefan_in, nonlinearityrelu) nn.init.zeros_(m.bias) model BostonMLP() model.apply(init_weights)modefan_in表示按输入方向计算方差nonlinearityrelu告诉初始化器当前层后面跟的是 ReLU这两者必须配对如果你把激活换成 tanh 或 sigmoid这里要改成xavier_normal_。bias初始化为 0 是回归任务的常见做法因为偏置作为可学习参数初始为 0 不影响训练初期的梯度稳定性。model.apply(init_weights)会把init_weights递归应用到所有子模块遇到nn.Linear才执行初始化这是 PyTorch 的官方推荐写法。4. 训练循环与超参调试loss、优化器和验证逻辑怎么配合4.1 训练循环的标准结构前向传播、loss、反向传播、step、zero_grad训练循环是这套工程里信息密度最高的一段代码。很多人第一版跑不通不是因为模型写错而是optimizer.zero_grad()、loss.backward()、optimizer.step()三兄弟的顺序或位置搞错。标准顺序是每个 batch 开始前清零梯度然后前向计算 loss再反向传播得到梯度最后优化器更新权重。import torch.optim as optim model BostonMLP() criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) num_epochs 200 for epoch in range(num_epochs): model.train() epoch_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() # 清空上一 batch 的梯度 pred model(x_batch) # 前向传播 loss criterion(pred, y_batch) # 计算 MSE loss.backward() # 反向传播计算梯度 optimizer.step() # 更新权重 epoch_loss loss.item() * x_batch.size(0) if (epoch 1) % 20 0: print(fEpoch {epoch1:3d}, Loss: {epoch_loss / len(train_dataset):.4f})几个关键点说透。optimizer.zero_grad()必须在backward()之前因为 PyTorch 的梯度是累加机制不清零的话每个 batch 的梯度会叠加相当于梯度被放大 batch 次训练必然发散。loss.item()取出的是 Python 浮点数用loss.item() * x_batch.size(0)把 batch loss 还原成样本级损失这样累加后除以训练集大小才是真正的平均 MSE直接累加loss.item()会导致每个 epoch 报告的数值随 batch 大小变化无法横向对比。model.train()在这里虽然不改变什么没有 Dropout 和 BatchNorm但建议保留它是 PyTorch 的显式意图声明以后往模型里加 Dropout 时少了这行测试阶段会出诡异结果。4.2 超参表lr、batch_size、epoch 怎么组合才算合理波士顿房价数据量小超参组合踩过一遍后我整理了一张常用参数表这套资源里默认值就是按下面这组来跑的超参数默认值作用与调整方向lr0.001Adam 的默认学习率通常不需要加调度器batch_size16样本量 40516 能让梯度估计有足够噪声跳出局部极小num_epochs200训练集 MSE 在 150 epoch 后基本不再下降隐藏层64→32增加神经元会提升拟合能力但过拟合风险同步上升优化器Adam比 SGD 收敛快不需手动调 momentum关于学习率最常见的翻车是把lr设成 0.1。对 MNIST 分类这种任务 0.1 可能没事但回归任务的 loss 量级往往是几千甚至上万学习率 0.1 意味着权重一步就要跨出很远loss 会在前几个 epoch 直接冲上 NaN。如果你看到 loss 打印出来是nan第一个要查的就是学习率。num_epochs不是越大越好这个数据集 200 epoch 足够跑 500 个 epoch 也不会带来 R² 的提升只会让验证集 loss 悄悄反弹——那是过拟合在作祟。4.3 验证集怎么用不要等训练结束才看验证 loss我见过不少人在整个训练跑完后才在测试集上算一次 loss中间过程完全不看验证集。对小数据集来说这等于闭着眼开车。正确做法是每个 epoch 结束时在验证集上算一次 loss观察两条曲线的走势训练 loss 降、验证 loss 也降说明训练健康训练 loss 降、验证 loss 不再降甚至上升说明模型开始记忆训练集应该早停。model.eval() with torch.no_grad(): val_pred model(X_test_t) val_loss criterion(val_pred, y_test_t) print(fEpoch {epoch1}, Val Loss: {val_loss.item():.4f})model.eval()切换评估模式配合with torch.no_grad()关闭梯度追踪。这一步在无 Dropout 的模型里看似多余但它能显著省内存和计算时间更重要的是它明确告诉读者推理阶段不需要计算图。no_grad()的作用是让 PyTorch 不构建 autograd 图验证集 forward 的内存占用从全图变成单层临时张量。记住一条铁律验证集和测试集的前向传播必须放在no_grad()里否则计算图会越积越大跑几十个 epoch 后内存报警。5. 避坑笔记波士顿房价项目里最常见的六个翻车现场5.1 现象load_boston()报 AttributeErrorAttributeError: module sklearn.datasets has no attribute load_boston。原因是 sklearn 1.2 起移除了该接口网上老教程没跟上版本更新。解决方式是改用 CSV 版本从 UCI 仓库或维护者个人主页下载housing.csv字段完全一致。从那以后我的习惯是在代码里写清楚数据来源路径避免团队其他人到处找数据集。5.2 现象验证集 R² 虚高换一组数据立刻打回原形原因是 scaler 在切分前对整个数据集做了fit_transform验证集的信息已经泄漏进训练流程。具体表现是测试集上 R² 能到 0.85但换数据或重新采样后跌到 0.7 以下。解决方式是严格按「先 split、后 fit」的顺序scaler 只用训练集的均值和方差。判断是否存在这个问题的快速方法是打印X_test标准化后的均值如果它不是接近 0 而是接近整体均值说明 leakage 大概率发生了。5.3 现象训练 loss 前几个 epoch 直接变成 nan原因通常是学习率过大或输入有 NaN。波士顿房价特征里没有缺失值所以最大嫌疑是学习率。我之前在调试时把 lr 设成 0.05loss 从第 3 个 epoch 开始变 nan。解决方式是把 lr 降到 0.001如果还 nan再检查X_train里有没有 inf以及y_train有没有极端离群值。这个数据集里 MEDV 有上限 50但个别副本数据可能混入异常值先df.describe()看一眼最大值。5.4 现象模型没有报错但 loss 一直不下降R² 是负数原因是优化器拿不到梯度常见的两个来源一是super().__init__()漏写导致参数没注册二是输入特征没有缩放模型在原始量纲下训练梯度方向被大数值特征主导。解决方式是先打印list(model.parameters())确认参数非空再说一遍标准化。R² 为负说明模型比「直接预测均值」还差在波士顿这个数据上如果你发现这种情况优先排查特征缩放而不是网络结构。5.5 现象CPU 训练很慢换 GPU 反而更慢波士顿房价只有 506 条样本网络只有 3 个线性层数据从 CPU 拷贝到 GPU、再从 GPU 取回 loss 的开销远大于计算本身。解决方式是别在这个项目上纠结cudaCPU 跑完 200 个 epoch 只要几十秒。但代码里保留设备判断是好习惯我一般写device torch.device(cuda if torch.cuda.is_available() else cpu)在模型和 tensor 上统一.to(device)这样以后换大项目不需要改结构。5.6 现象测试集 loss 比训练集低让人以为模型很完美原因是切分随机性造成测试集恰好比训练集更「简单」。506 条样本里 405 条训练、101 条测试测试集样本少导致方差大。解决方式是不要只跑一次切分用交叉验证看稳定水平。这套工程里后续版本我加了 5 折交叉验证观察 R² 的标准差如果能稳定在 0.02 以内说明结果可信。6. 收尾验证与进阶R²、交叉验证和模型导出别只停在训练6.1 评估指标用 R² 和 MAE 代替裸的 MSEMSE 的量纲是「千美元的平方」数字看起来很大非专业观众很难直观判断好坏。R² 表示模型解释了目标变量多少比例的方差0.8 就是解释了 80%越接近 1 越好。MAE 是预测值和真实值的平均绝对偏差单位是千美元波士顿房价中位数约 21 千美元MAE 在 2~3 之间说明平均偏差在 2000~3000 美元这个数字对业务方更有感知。from sklearn.metrics import r2_score, mean_absolute_error pred model(X_test_t).detach().numpy() print(R2:, r2_score(y_test, pred)) print(MAE:, mean_absolute_error(y_test, pred)).detach().numpy()是把预测张量从计算图里摘出来转成 numpy不 detach 直接.numpy()会报错因为带梯度的张量无法显式转 numpy。这一步和训练阶段的no_grad()是同一个逻辑评估阶段不需要梯度。6.2 模型保存与 ONNX 导出torch.save 之外的另一条路训练结束后除了torch.save(model.state_dict(), boston_mlp.pth)我强烈建议顺手导出一份 ONNX。ONNX 是跨框架的模型格式可以脱离 PyTorch 环境推理后续接服务端或者嵌入其他工具链都不用再装深度学习框架。dummy_input torch.randn(1, 13) torch.onnx.export(model, dummy_input, boston_mlp.onnx, input_names[features], output_names[price], dynamic_axes{features: {0: batch_size}})dummy_input的维度必须是(1, 13)第一维是 batch 大小第二维是特征数形状要和训练时完全一致。dynamic_axes声明 batch 维度是动态的这样导出后可以一次预测任意数量的样本而不只限制在 1 条。导出后可以用onnxruntime加载验证输出和 PyTorch 结果是否一致偏差在 1e-4 以内说明转换无损。这套工程走到这里算真正闭环了CSV 数据进来训练好的模型和 ONNX 权重出去。说来也讽刺我最早跑这个项目时就是在标准化顺序上翻的车验证集 R² 虚高到 0.88自己高兴了一晚上第二天换了个 random_state 直接掉到 0.71才发现是 scaler 泄漏。从那以后我每次跑回归项目都强制先打印特征缩放后的均值标准差再开始训练。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网