恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
PyTorch实战波士顿房价预测:回归任务从数据预处理到模型训练全解析
首页
资讯中心
/
PyTorch实战波士顿房价预测:回归任务从数据预处理到模型训练全解析
PyTorch实战波士顿房价预测:回归任务从数据预处理到模型训练全解析
发布时间:2026/10/18 2:31:10
波士顿房价预测大概是每一个接触机器学习和深度学习的开发者都绕不过去的入门题我在不同阶段拿它练过好几次手。一开始用 Scikit-learn 跑线性回归后来用 Keras 搭多层感知机再用 PyTorch 重写一遍时才发现这个看似简单的案例里其实藏着不少值得琢磨的细节。这篇文章不打算讲那些教科书里已有的大道理而是把我用 PyTorch 重新实现波士顿房价预测时的完整思路、关键代码、训练陷阱和排查经验一次性说清楚。无论你是刚转过来学 PyTorch 的新手还是已经有一段时间没碰过回归任务的开发者这篇文章应该都能让你少走一些弯路。1. 为什么是波士顿房价被低估的回归入门任务很多初学者容易陷入一个误区觉得回归任务比图像分类简单不就是输入特征输出一个数值。实际上回归问题在工程落地中远比分类更常见从销量预测、风控评分到工业参数估计本质上都是回归。波士顿房价这个数据集最难得的地方在于它的尺寸刚好合适——506个样本、13个特征、1个连续目标值。这个规模意味着训练很快CPU上几十秒就能跑完一个完整的实验周期非常适合用来理解框架的核心机制。我见过不少同学一上来就拿 ImageNet 级别的大模型练手结果连数据加载的 API 都没搞清楚就被 GPU 显存和训练时长劝退了。波士顿房价这种小而美、五脏俱全的数据集才是真正适合拆开揉碎去研究 PyTorch 工作机制的好素材。PyTorch 里跑通这个案例你至少能掌握以下几项核心能力Dataset 和 DataLoader 的基本用法、数据标准化的标准流程、自定义模型网络结构、训练循环的手写方法、MSE 损失函数和优化器的搭配逻辑。这些能力迁移到其他任何项目都是通用的框架可以换但这一整套思维方式不会变。还有个很容易忽视的问题波士顿房价数据集的下载其实已经不那么“官方”了。原始数据集因为涉及一些敏感信息已经从一个常用的公开数据集中被移除了。好在 UCI 机器学习库上仍然保留了原始版本也可以通过一些第三方镜像获取。这也是我建议你在动手前先解决的一个前置问题后面会详细展开。2. 数据准备决定模型上限的隐形环节2.1 拿不到数据怎么办数据集获取的几种可行方案我先说一个实际操作中很多人会卡住的问题波士顿房价数据集在常用的sklearn.datasets.load_boston接口里已经不可用了。如果你沿用老教程里的代码程序会直接抛异常。网上能找到的解决方案五花八门但真正可靠的无非这几种。最稳妥的方法是从 UCI 机器学习的公开仓库下载原始文件文件格式是 data本质上就是一行一个样本的纯文本数据。第 6 列是目标值也就是房价的中位数。也可以用一些第三方维护的镜像仓库但这类镜像的更新频率和维护状态不一建议下载后先用后续提到的检查方法验证一下数据的完整性。我的建议是你把它当成新项目的第一步来对待先确认数据来源再写加载代码。很多项目的失败看起来很随机其实从数据环节就已经埋下了隐患。2.2 只做标准化够吗数据预处理的标准流程拿到数据以后下一步就是预处理。波士顿房价数据集的 13 个特征量纲差异非常大CRIM 这个特征表示犯罪率数值可能只有个位数而 TAX 表示房产税率数值可以达到几百甚至上千。如果直接把原始数据丢进模型数值大的特征会在梯度计算中占据主导地位模型的收敛速度和最终精度都会受影响。我这里的做法是标准化也就是把每个特征减去均值再除以标准差让它们都落在零均值、单位方差附近。这个操作本质上是对特征做了一次线性变换并不会改变数据内部的分布关系但对于基于梯度的优化算法来说这一步几乎可以说是必需品。具体到 PyTorch 的实现很多人会犯一个错误在 Dataset 里直接对全量数据调用均值方差计算。标准化参数应该在划分训练集和测试集之前只基于训练集部分计算然后再应用到训练集和测试集上。这里面的逻辑是测试集是用来模拟未来未知数据的如果你把测试集的信息也透露给了模型那评估结果就会偏乐观失去了真实的参考价值。2.3 划分数据集时的四个注意事项训练集和测试集的划分看似简单往往也有讲究。我习惯用 82 的比例也就是 80% 的数据用于训练20% 用于测试。在这个数据集上大致对应 404 个训练样本和 102 个测试样本对一个单隐层的小网络来说完全够用。划分时需要格外注意几点一是必须设置随机种子否则每次运行代码得到的数据划分都不一样实验结果就无法复现二是要打乱数据顺序原始数据可能存在某种隐含的顺序结构不打乱的话会把这种结构泄漏给模型三是对验证集的使用要慎重如果你想调参最好从训练集里再切出一部分当验证集不要拿测试集反复尝试四是如果你的数据存在明显的不平衡分布应该考虑按目标值分层抽样。之前我在一个实际项目中遇到过这种问题因为数据划分没有固定随机种子前后两次训练虽然模型结构一样但测试分数跳来跳去折腾了大半天才发现是数据划分的问题。3. 模型设计回归任务的网络结构到底该怎么搭3.1 为什么要用多层感知机而不是别的主流模型波士顿房价数据集是典型的表格数据特征之间没有像图像那样的空间结构也不存在像文本那样的时序依赖所以卷积神经网络、循环神经网络这些结构在这里并不适用。表格数据用多层感知机是完全合理的选择用更复杂的结构纯属给自己找麻烦。那层数和隐藏层神经元个数该怎么定呢。我的经验是从简单开始不要一上来就堆复杂结构。对 13 个输入特征这样的规模单隐藏层网络就已经足够拟合。一个常被人误解的原则是深度并不天然比宽度好尤其是在数据量有限的情况下过深的网络反而更容易过拟合。我最终设计的结构是输入层 13 个特征映射到隐藏层 32 个神经元激活函数用 ReLU然后从 32 个神经元映射到输出层 1 个数值。这个结构只有两层线性变换加一次激活函数参数量非常小在 CPU 上训练也就是几秒钟的事效果却相当稳定。3.2 激活函数的选择回归场景下的默认选项隐藏层激活函数我用的 ReLU这个选择基本没有任何悬念。ReLU 计算成本极低没有指数或除法运算而且在正区间梯度恒为 1能有效避免梯度消失问题。市面上常见的替代品比如 tanh 和 sigmoid虽然在某些场景下也有用但在隐藏层使用 tanh 会导致输出范围被限制在 -1 到 1 之间对于潜在范围更大的数值输出场景需要对网络结构和参数初始化做更多的配套调整初学者不建议碰。这里特别提醒一点输出层的激活函数绝对不要加 ReLU也绝对不要加 sigmoid。回归任务的输出层通常就是一个普通的线性层。加了额外约束之后你的模型输出范围就被框死了遇到超出范围的房价标签就只能眼睁睁看着模型学不出来。我见过有些教程为了让“网络更复杂”而在输出层强行加激活函数那本来能轻松收敛到不错精度的模型最后的损失曲线直接变成一条水平线。这是个非常基础但是经常被忽略的细节。3.3 损失函数和优化器回归任务的黄金组合回归任务的默认损失函数是均方误差PyTorch 里对应nn.MSELoss()。MSE 的主体思想很直白算预测值和真实值之差的平方再取平均。平方操作会让误差大的样本在损失里占比更高相当于对离群值更敏感这也促使模型优先去拟合那些偏差大的样本。有的开发者会问为什么不直接用绝对误差 MAE。MAE 对离群值更鲁棒但它的梯度在零点不可导优化过程不如 MSE 平滑。在波士顿房价这个数据集上目标值范围大概在 5 到 50 之间并没有极端离谱的离群值用 MSE 是最稳妥的选择。优化器我用的是随机梯度下降。考虑到数据集只有 506 个样本简单配置一个固定的学习率和冲量就完全足够。Adam 这类自适应学习率的优化器当然也能用而且通常收敛更快但如果你正在学习 PyTorch我建议先用 SGD 把优化器和梯度更新的直觉建立起来再用 Adam 对比效果。4. 核心实现从数据构建到模型训练4.1 加载和解析数据的完整代码结合前面提到的数据源实际数据文件路径可以根据自己的目录调整。这里我直接对一个本地文件进行读取并对数据做了解析。import numpy as np import torch from torch.utils.data import TensorDataset, DataLoader from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler data_path housing.data raw_data [] with open(data_path, r) as f: for line in f: if line.strip() : continue elements line.split() raw_data.append([float(item) for item in elements]) data np.array(raw_data) X data[:, :13] y data[:, 13] 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) X_test scaler.transform(X_test)这里有个小坑需要说明原文件的末尾可能有空行逐行读取时如果不做空行过滤后面转 float 就会报错。习惯上我会在解析的每一步都加验证数据量小并不会影响效率。这里面数据读取部分因为原始文件的格式原因很多人第一次接触时容易一头雾水。我从实际解析逻辑来看每一行包含 13 个特征和紧跟着的 1 个目标值所以按空白字符切分并全部转成 float 就是一个干净的处理方式。4.2 封装 Dataset 和 DataLoaderPyTorch 的数据加载流程和纯 NumPy 流程最大的差别在于你需要把数据组织成框架认识的格式。对于这种直接读入内存的小数据集用 TensorDataset 封装就够了。你需要注意一个关键的细节特征张量和标签张量的数据类型必须是torch.float32而默认的 NumPy 浮点数可能是 float64。PyTorch 的模型参数默认是 float32如果输入是 float64 而参数是 float32类型不匹配会导致程序直接报错或者在某些版本里静默地做出类型转换造成意料之外的行为。标签的形状也需要处理。如果你只传入一个一维数组有些损失函数在某些版本的 PyTorch 里会报维度不匹配的错误。稳妥的做法是直接把它变成和输出形状一致的二维张量也就是(样本数, 1)。X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32).view(-1, 1) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.float32).view(-1, 1) train_dataset TensorDataset(X_train_t, y_train_t) test_dataset TensorDataset(X_test_t, y_test_t) batch_size 32 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse)shuffle 参数只对训练集打开测试集保持顺序不变就足够。因为测试集不参与梯度计算不需要打乱只需要一遍遍前向传播算指标就行。一个小经验batch_size 的选择不要太大也不要太小。在这个数据量上我用 32 比较合适。batch_size 太大会导致每个批次梯度的噪声很小但迭代次数也变少模型容易陷入局部最优。batch_size 太小则每个批次代表性不足训练过程震荡会比较剧烈。4.3 模型定义与初始化的细节PyTorch 中定义网络有两种风格一种是写一个继承nn.Module的类另一种是直接用nn.Sequential组件堆叠。对于这种 13→32→1 的线性网络我用nn.Sequential就够了简洁直观。model torch.nn.Sequential( torch.nn.Linear(13, 32), torch.nn.ReLU(), torch.nn.Linear(32, 1), )有一点值得注意PyTorch 的nn.Linear默认会自动初始化权重和偏置权重初始化方式是均匀分布。对这个问题来说默认初始化完全够用。自己手动做初始化容易把分布范围弄错反而拖慢收敛速度。如果想对模型的工作机制有更多好奇可以在定义完模型后打印一下模型的结构和参数print(model)输出会清楚展示每一层的名称和输出形状这对新手理解数据的流动非常有帮助。4.4 训练循环从零手写的关键环节训练循环是整个 PyTorch 入门中最需要理解的部分它也是与高层封装框架差异最大的地方之一。很多从 Keras 转过来的开发者初接触 PyTorch 时最大的不适应点就在这里一切都要自己控制。一个训练周期大概有下面几个步骤把优化器梯度清零、前向传播算预测值、用损失函数算误差、反向传播计算梯度、优化器执行一步更新参数。五个步骤一步都不能少。epochs 100 learning_rate 0.05 momentum 0.9 criterion torch.nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lrlearning_rate, momentummomentum) train_losses [] for epoch in range(epochs): model.train() running_loss 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() outputs model(batch_X) loss criterion(outputs, batch_y) loss.backward() optimizer.step() running_loss loss.item() * batch_X.size(0) epoch_loss running_loss / len(train_loader.dataset) train_losses.append(epoch_loss) if (epoch 1) % 20 0: print(fEpoch [{epoch 1}/{epochs}], Loss: {epoch_loss:.4f})这里有一个非常容易犯的错忘记调用optimizer.zero_grad()。PyTorch 的梯度是累积的如果不做清零下一次反向传播会把这次的梯度加到上次的梯度上。累积到一定程度以后梯度值会变得很大训练过程直接发散。你可以试验一下把zero_grad注释掉观察损失曲线的变化这会让你对“梯度从哪里来、到哪里去”有更直观的理解。学习率的选择也值得多说一句。我建议从 0.05 起步观察损失下降速度。如果损失震荡剧烈就降一个量级比如到 0.005。如果损失下降太慢就适当加大。这个调参过程是深度学习训练中不可回避的基本功一次一次试出来的经验最可靠。4.5 验证与评估看 MAE 比 MSE 更直观模型训练完以后我们要在测试集上评估它的表现。很多人直接打印测试集上的 MSE 就完事但我更推荐同时计算平均绝对误差 MAE。MAE 的单位和房价本身的单位一致可以直接理解为“平均预测偏差了 X 千美元”。model.eval() with torch.no_grad(): y_pred model(X_test_t) test_loss_mse criterion(y_pred, y_test_t).item() test_mae torch.mean(torch.abs(y_pred - y_test_t)).item() print(fTest MSE: {test_loss_mse:.4f}) print(fTest MAE: {test_mae:.4f}) # 随机挑几个样本对比一下 for i in range(5): true_val y_test_t[i].item() pred_val y_pred[i].item() print(f真实值: {true_val:.2f}, 预测值: {pred_val:.2f})评估模式model.eval()和torch.no_grad()这两个操作缺一不可。前者会影响 BatchNorm 或 Dropout 等层的行为虽然当前模型里没有这些层但要养成习惯。后者是告诉 PyTorch 不需要记录梯度可以显著减少内存占用、加速前向传播。在这个小数据集上你可能感觉不明显放到大模型上差异就非常明显。我在这套配置下跑出来的测试 MSE 大概在 12 到 16 之间测试 MAE 大概在 2.2 到 2.8 之间也就是模型预测房价平均偏差两千多美元。对一个用 CPU 跑几十秒的小模型来说这个精度已经相当可用了。顺带说一句也不要过分追求把训练误差压到极低。这是个 506 样本的小数据集拟合到测试 MAE 低于 2 往往伴随着过拟合。当训练损失持续下降而验证或测试损失开始上升就是过拟合的信号后面会专门展开。5. 训练过程可视化用损失曲线说话损失曲线是我在所有深度学习入门实验里最推荐画的图它能让你一眼判断出模型是欠拟合、过拟合还是刚刚好。初学者最容易犯的错误是只盯着最终打印出来的数字却忽略训练过程中发生了什么。import matplotlib.pyplot as plt plt.plot(range(1, epochs 1), train_losses) plt.xlabel(Epoch) plt.ylabel(Training Loss) plt.title(Training Loss Curve) plt.grid(True) plt.show()一个健康的损失曲线应该是前期快速下降然后逐渐变平。如果你看到损失曲线在某个点之后开始反弹那大概率是学习率设置过大模型在最优解附近来回震荡无法彻底收敛。这时候降低学习率往往立竿见影。把训练过程和可视化结合起来是我反复强调的一次完整建模闭环。没有可视化的训练过程就像闭着眼睛开车最后撞没撞墙全靠运气这不应该是工程中该有的状态。如果你希望更方便地利用验证指标来调整学习率可以考虑用 PyTorch 的学习率调度器。例如torch.optim.lr_scheduler.StepLR每多少轮把学习率乘以一个衰减系数。在实际项目中这种“训练到后期自动降低学习率”的做法对提升最终精度很有效。6. 常见问题与排查技巧实录6.1 验证损失比训练损失还低存在什么问题模型在测试集上的表现比训练集更好看起来是好事但我要提醒你这往往意味着训练过程有泄漏。最常见的泄漏渠道就是把标准化参数放在了整个数据集上计算让模型在训练时就看到了测试集的信息。另一个可能性是原始数据集内部有重复样本或高度相似的样本导致测试集里的一些数据点与训练集中的样本几乎相同。遇到这种泄漏模型的真实泛化能力会比测试结果更差。一旦投入真实环境性能会明显下滑。6.2 损失不下降或变成 NaN学习率带来的灾难损失值变成nan是训练过程中最恐怖的信号之一。它通常意味着数值溢出这背后往往有两个原因要么特征没有标准化输入量级太大导致梯度爆炸要么学习率过高参数一步跳出了正常的数值范围。排查思路一般是这样先确认特征已标准化再检查标签中是否有异常大的离群值再手动调低学习率尝试。如果你用的 SGD 学习率默认就是 0.01把它降到 0.001 试试往往立刻就好转了。6.3 模型对训练数据过拟合怎么判断和处理如果训练损失一路下降而测试损失开始反弹就说明网络开始把训练数据里的噪声也背下来了。在波士顿房价这个数据集上样本数太少过拟合是很容易出现的问题。处理方式有几个选择增加训练数据量但这个数据集已经固定了不现实降低模型复杂度比如把隐藏层神经元从 32 降到 16或者干脆去掉隐藏层加入 L2 正则化也就是在损失函数后面加一个对权重平方的惩罚项或者用 Dropout训练时随机丢弃一部分神经元。我推荐先走降低复杂度这条路对小型数据集来说最直接有效。6.4 固定随机种子别让结果来回飘你可能会遇到这种情况代码明明一模一样跑两次结果却略有差异。原因在于模型参数的随机初始化、数据集的随机划分都依赖于随机种子未固定时每次运行都会产生不同的状态。解决方案是在代码开头加一行torch.manual_seed(42) np.random.seed(42)这样每次实验从头到尾都接收同一组随机序列结果可以复现。深度学习的实验本质上和科学实验没有区别不可复现的结果在工程协作中没有任何价值。6.5 标签形状报错一个容易被忽略的细节我在第一次用 MSELoss 时遇到过形状不匹配的报错。如果你的标签张量是一维的预测输出是二维的它们之间的广播逻辑在某些 PyTorch 版本里并不会自动进行导致损失计算时报错。解决方案就是前面提到的在构造标签张量时加一个.view(-1, 1)把形状统一成(样本数, 1)。这一类问题往往是新手学 PyTorch 时最容易遇到的摩擦点我特意列出来希望你可以提前规避。7. 从波士顿房价到真实项目的三个迁移心得跑通波士顿房价预测只是第一步。我在几个实际项目中遇到过类似的回归任务比如某公司希望根据历史数据预测未来业绩某实验室想通过传感器数据预测设备寿命。这些项目表面上和波士顿房价毫无关系但底层的建模逻辑几乎完全相同。第一个迁移心得是数据预处理永远比模型结构更值得花时间。波士顿房价这个案例中标准化前后的训练速度差异非常明显。在真实项目中缺失值和异常值处理、特征构造、业务规则融合每一项都比选模型框架更影响最终效果。第二个迁移心得是先从最简单的模型起步再慢慢增加复杂度。每次加一个环节就要确认一次它对结果的影响是否值得。我在一个销售预测项目中一开始就用 XGBoost 取得不错的效果后来换成多层感知机反而过拟合严重。后来回过头用简单模型认真处理特征效果才真正稳定下来。第三个迁移心得是一定要养成可视化和记录实验的习惯。每次改动参数都要记下来改了哪一项、结果是什么变化。你在波士顿房价这种小数据集上建立的这套实验方法论可以平移到任何一个深度学习项目里。技术圈里常说“把一件事做透比做多件事更重要”波士顿房价是这句话的绝佳载体。当你把一个最朴素的回归任务从头到尾跑通、跑透你对 PyTorch 的理解将会有质的飞跃再回头看网上那些复杂的项目代码时也会觉得自己看到了骨架上的很多细节。