简介:基于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_size=0.2, random_state=42 ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) # 只 fit 训练集 X_test = scaler.transform(X_test) # transform 沿用训练集的均值和方差参数说明:test_size=0.2对应 506 条样本里留 101 条做测试,训练集 405 条,对这个数据规模来说是合理比例;random_state=42保证每次跑出来的切分结果一致,方便对比实验。StandardScaler的默认行为是每个特征减去均值、除以标准差,变换后每个特征均值约 0、方差约 1。为什么回归任务几乎必做这一步?因为神经网络对输入尺度敏感,如果房间数(3~8)和犯罪率(0~89)混在一起喂进去,梯度更新会被大数值特征主导,模型收敛极慢,甚至不收敛。这套资源里有一个我自己常用的检查习惯:transform 完打印X_train.mean(axis=0)和X_train.std(axis=0),看到均值接近 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, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) X_test_t = torch.tensor(X_test, dtype=torch.float32) y_test_t = torch.tensor(y_test, dtype=torch.float32) train_dataset = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)TensorDataset的核心作用是自动按索引对齐特征和目标,DataLoader每次迭代返回一个 batch 的(x_batch, y_batch),shuffle=True让每个 epoch 的样本顺序都打乱一次,避免模型学到样本排列顺序。两点注意:一是dtype=torch.float32必须在创建张量时指定,不要等训练时报类型不匹配再回头补;二是这个规模的数据集用batch_size=16比较稳,太大(比如 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_dim=13, hidden1=64, hidden2=32): 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, mode="fan_in", nonlinearity="relu") nn.init.zeros_(m.bias) model = BostonMLP() model.apply(init_weights)mode="fan_in"表示按输入方向计算方差,nonlinearity="relu"告诉初始化器当前层后面跟的是 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(), lr=0.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(f"Epoch {epoch+1: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 怎么组合才算合理
波士顿房价数据量小,超参组合踩过一遍后我整理了一张常用参数表,这套资源里默认值就是按下面这组来跑的:
| 超参数 | 默认值 | 作用与调整方向 |
|---|---|---|
lr | 0.001 | Adam 的默认学习率,通常不需要加调度器 |
batch_size | 16 | 样本量 405,16 能让梯度估计有足够噪声跳出局部极小 |
num_epochs | 200 | 训练集 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(f"Epoch {epoch+1}, 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()报 AttributeError
AttributeError: 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.05,loss 从第 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 的开销远大于计算本身。解决方式是别在这个项目上纠结cuda,CPU 跑完 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 代替裸的 MSE
MSE 的量纲是「千美元的平方」,数字看起来很大,非专业观众很难直观判断好坏。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 泄漏。从那以后我每次跑回归项目,都强制先打印特征缩放后的均值标准差,再开始训练。希望帮到你。
本文还有配套的精品资源,点击获取