资讯动态

PyTorch房价预测实战:从数据预处理到MLP回归模型全流程

发布时间:2026/10/17 13:06:34 来源:尧图企业网站定制
直接说结论房价预测这类回归任务是初学者从“会跑分类Demo”跨进“能独立做完一个项目”最短的一条路。它数据好拿、逻辑直观、评价指标明确而且不会像图像分类那样动不动就得熬显卡。我这次把完整的流程用 PyTorch 从头到尾撸了一遍包含数据处理、模型搭建、训练评估、踩坑修复整套做完你对神经网络的回归任务基本就有手感了。这篇文章是给那些已经会写简单的 Python、知道张量是什么、但还没系统做过一个回归项目的人准备的。我会边做边讲“为什么这么写”而不是只给你一段能跑通的代码。1. 项目从哪开始先搞懂要预测什么再碰代码1.1 需求拆解与目标定义很多人拿到一个房价数据集第一反应就是赶紧导入 PyTorch、搭个网络开训。这是个非常危险的直觉。机器学习项目里最贵的错误都发生在你还没想清楚目标的时候。这次的目标非常明确根据房子的一系列属性预测房价。这里有个关键细节——预测的是“具体价格”还是“价格的对数”很多人忽略这一点直接对原始房价做回归结果训练出来的模型在价格高的样本上误差巨大但在日志里看 mse 又还过得去。原因很简单房价分布极度右偏少数豪宅会把损失函数的主导权完全拉走。我这次的处理方式是先对目标做log1p变换。log1p(x)等于log(x1)好处有两个一是让分布更接近正态模型更容易学二是预测出来之后做一次expm1就能还回原价格不会出现负数预测。这一步我在后面的代码里会再次强调。1.2 数据现状决定方案选型数据集的构成大致是数十个特征其中有连续数值面积、房龄、房间数也有离散类别朝向、装修状态、所属区域编号。这种结构非常典型可以说是“混合型表格数据”。在这个场景下第一版方案根本不需要上复杂的网络结构。我的建议顺序是先用简单的线性回归或决策树做一个基线得到一个大致的误差水平再用多层感知机MLP去试着超越这个基线如果 MLP 有效再考虑特征工程上的优化。这里涉及一个工程原则永远先跑通最简单模型再逐步加复杂度。没有基线你会连模型有没有学到东西都判断不出来。我见过有人调了三天网络结构最后发现数据本身有个字段是强泄漏线性回归都能跑出 0.9 的 R²那后边所有工作都白做了。1.3 特征工程与数据清洗房价预测的特征工程有几个绕不开的活儿缺失值处理。表格数据几乎一定有缺失。数值型我用中位数填充类别型用众数。你也可以用模型预测填充但对入门项目没必要性价比太低。类别特征编码。分类变量不是数字不能直接塞进网络。常见手段是独热编码One-Hot或者目标编码。我的建议类别不多用独热类别超过几十个时再用目标编码或嵌入。数值特征标准化。神经网络对特征的尺度非常敏感。面积是 100 量级房龄是 10 量级如果不做标准化梯度更新会被大尺度特征主导。这里我用StandardScaler把每个特征变成均值为 0、方差为 1 的分布。这里有一个极其关键的细节我必须单独拿出来说标准化只能用训练集去 fit然后对训练集和测试集分别做 transform。如果你拿着全量数据一起 fit测试集的信息就通过均值、方差间接进入了训练过程这就是一种温柔的数据泄漏模型的评估结果会比真实表现更好。后面我会详细演示这一点。2. 模型选型和网络结构设计为什么是 MLP2.1 从线性回归到多层感知机的逻辑跳跃线性回归可以看成没有隐藏层的“神经网络”它只能捕捉特征的线性加权和。但房价和特征之间的关系显然不是纯线性的面积增大带来的房价增幅是边际递减的区域位置和房龄之间也有交互作用。这些都需要网络有非线性拟合能力。多层感知机做的事就是在中间插入一层或多层神经元并在每一层之后接一个非线性激活函数让网络有能力拟合曲线关系。理论上已经证明一个足够宽的单隐藏层网络可以拟合任意连续函数这也叫万能逼近定理但实践中多几层、控制宽度效果更好、更稳。2.2 隐藏层结构与激活函数我这次采用的网络结构是import torch.nn as nn class HousePriceNet(nn.Module): def __init__(self, input_dim): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): return self.net(x)两个隐藏层64 和 32 个神经元。为什么要这个规模因为输入特征只有几十个数据集样本量也就几千到几万之间网络太宽容易过拟合太窄表达力不够。64-32 是一个对表格型数据相当通用的起点。激活函数选了 ReLU原因只有一个字稳。ReLU 计算快梯度消失问题比 Sigmoid 轻得多。这里提醒一下不要在一开始就用 Mish、Swish 这些高级激活函数它们确实在某些任务上更好但对入门项目ReLU 足够让你把所有精力花在数据上。最后一层不加激活函数因为我们要输出连续值不需要把它压缩到 0 到 1 之间。2.3 损失函数、优化器和评估指标回归任务的损失函数最常见的是均方误差MSEcriterion nn.MSELoss()MSE 对异常值敏感如果数据里有极端房价会让模型拼命去拟合那一个点。这也是我前面做 log1p 变换的原因之一。如果不想变换目标也可以换成 HuberLoss它在误差接近零时像 MSE误差大时像 MAE梯度更稳。入门项目用 MSE 配合 log 目标完全够。优化器我选了 Adam学习率设为 0.001。这是 PyTorch 里默认配置中最稳的组合之一。Adam 的自适应学习率机制让它在不同尺度的参数上都能保持比较合理的更新幅度。如果你只想改一个东西去提点不要一上来换优化器先调学习率。评估指标上我会同时看三个RMSE均方根误差和原始价格同量纲直观。但注意如果预测的是 log 价格计算 RMSE 时要在还原后的价格上算。MAE平均绝对误差不容易被极端值带偏。R²决定系数表示模型解释了百分之多少的方差。越接近 1 越好。我自己的习惯是多看 RMSE 和 R²MAE 只作为参考。3. 实战流程DataLoader、训练循环、预测评估3.1 数据加载与标准化先上完整的数据准备代码import pandas as pd 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 pd.read_csv(housing_data.csv) X data.drop(columns[price]) y data[price].values # 目标变量做 log 变换 y np.log1p(y) # 先切分再做标准化避免数据泄漏 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 对数值特征做标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 转成 PyTorch Tensor X_train_t torch.tensor(X_train_scaled, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32).view(-1, 1) X_test_t torch.tensor(X_test_scaled, 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) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse)这里有三个点值得仔细琢磨一是train_test_split的random_state42。不设固定种子每次跑出来的划分都不一样模型结果就没法对比。工程里这叫可复现性没有它你的调参过程就是玄学。二是shuffleTrue与shuffleFalse。训练集必须打乱顺序否则模型会学到样本顺序的伪规律测试集不需要打乱因为只是批量送给模型做预测顺序不影响任何结果。三是view(-1, 1)。把y的形状从(batch_size,)变成(batch_size, 1)这样和模型输出(batch_size, 1)才能直接算损失。3.2 训练循环PyTorch 的训练模式非常固定四步走前向传播、算损失、清空梯度、反向传播更新。from torch.utils.data import DataLoader model HousePriceNet(X_train_t.shape[1]) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) def train_one_epoch(model, dataloader, criterion, optimizer): model.train() total_loss 0.0 for X_batch, y_batch in dataloader: optimizer.zero_grad() preds model(X_batch) loss criterion(preds, y_batch) loss.backward() optimizer.step() total_loss loss.item() * X_batch.size(0) return total_loss / len(dataloader.dataset) num_epochs 200 for epoch in range(num_epochs): avg_loss train_one_epoch(model, train_loader, criterion, optimizer) if (epoch 1) % 20 0: print(fEpoch {epoch 1}/{num_epochs}, Loss: {avg_loss:.6f})一个常见疑问optimizer.zero_grad()为什么要每个 batch 都调用因为 PyTorch 默认会累加梯度如果不清理上一个 batch 的梯度会跟当前 batch 的梯度叠加导致更新方向完全错乱。另一个疑问为什么 epoch 定成 200如果 loss 在 80 轮后基本不降200 轮够你观察到平台期。你也可以用早停机制在验证集 loss 连续 N 轮不下降时提前终止训练这也是防止过拟合的手段之一。3.3 模型评估与预测模型训练完之后用测试集做评估model.eval() with torch.no_grad(): preds_test model(X_test_t) test_loss criterion(preds_test, y_test_t) preds_test_exp np.expm1(preds_test.numpy()) y_test_exp np.expm1(y_test_t.numpy()) from sklearn.metrics import mean_absolute_error, r2_score mae mean_absolute_error(y_test_exp, preds_test_exp) rmse np.sqrt(((y_test_exp - preds_test_exp) ** 2).mean()) r2 r2_score(y_test_exp, preds_test_exp) print(fRMSE: {rmse:.2f}, MAE: {mae:.2f}, R2: {r2:.4f})这一步里有个容易犯的错如果你训练时用的是log1p变换后的 y那么criterion(preds, y_test_t)算出来的 loss 是在 log 空间的它不能直接拿来评价模型在真实价格上准不准。必须在还原成原价格之后再算 RMSE 和 MAE。我这次跑出来的结果折算成原房价大概是这样测试集 RMSE 在几万块钱的水平R² 在 0.88 左右。对入门项目来说这个结果说明模型已经学到了数据里的主要规律。如果 R² 只有 0.5 或者更低通常不是网络不够深而是特征没做透或者数据泄漏导致验证集不可信。3.4 可视化分析预测结果评估不是只看数字画图能看出更多东西。至少画这两张图第一张是真实价格和预测价格的散点图把y_test_exp当横轴preds_test_exp当纵轴。如果点大致落在yx直线上说明模型表现良好。如果出现低端向上偏、高端向下偏的弧线说明存在系统性误差通常是模型在极值处拟合能力不足。第二张是残差直方图。残差等于真实值减预测值理论上应该围绕 0 呈对称分布。如果残差有明显偏斜或者方差随预测值增大而增大说明模型对高房价样本的预测不确定性更大这和房子的异质性直接相关——同样是多 10 平米在市中心和郊区带来的价格增量完全不同。画图代码很简单import matplotlib.pyplot as plt plt.figure(figsize(6, 6)) plt.scatter(y_test_exp, preds_test_exp, alpha0.5) plt.plot([y_test_exp.min(), y_test_exp.max()], [y_test_exp.min(), y_test_exp.max()], r--) plt.xlabel(True Price) plt.ylabel(Predicted Price) plt.show() residuals y_test_exp - preds_test_exp plt.hist(residuals, bins50) plt.show()4. 训练过程中的常见问题与排查思路4.1 损失不下降怎么办这是被问得最多的一个问题。我自己的排查顺序是先看学习率。学习率太大loss 会在一个高位震荡学习率太小loss 几乎不动。建议从 0.001 开始如果 20 轮内 loss 一点不降试试0.01或0.0001。再看数据。把 X 和 y 打印出来人工检查有没有大量 NaN有没有填错的中位数目标值是否被错误地取了对数最后看网络。隐藏层神经元过多或过少都可能导致收敛缓慢。可以先拿一个样本做 sanity check只喂一条数据看模型能不能把它的 loss 降到接近 0。这一步能快速定位到是代码 bug 还是模型问题。4.2 训练集 loss 一直降测试集指标却很糟这是典型的过拟合。常见诱因和应对方案模型容量太大减少隐藏层数量或神经元数训练轮次太多加早停机制或者减少 epoch数据量太小用交叉验证或者加 Dropout正则化缺失在 PyTorch 里给优化器加 weight decay等价于 L2 正则。Adam 优化器本身对 weight decay 的处理和其他优化器有差异但直接给weight_decay1e-4多数情况下是有效的。optimizer torch.optim.Adam(model.parameters(), lr0.001, weight_decay1e-4)还有一招是 Dropoutnn.Dropout(0.2)加在激活函数之后每个隐藏层输出前。它的原理是在训练时随机让一部分神经元的输出置零迫使网络学到更冗余、更鲁棒的特征组合。但注意 Dropout 只应在训练时生效PyTorch 的nn.Dropout在model.eval()模式下会自动关闭所以上面评估代码里我特意加了model.eval()这句不能省。4.3 数据泄漏的隐蔽坑数据泄漏是回归项目里最阴险的问题。除了标准化要在切分后做还要注意时序问题。如果房价数据是带时间属性的比如按月份收集用随机切分会导致未来数据混进训练集。正确做法是按时间顺序切分前 80% 时间的样本做训练后 20% 做测试。还有一个细节是重复样本。有些数据集里同一套房子会出现多次随机划分后同一条数据可能同时出现在训练集和测试集导致评估虚高。处理方式是在建模前drop_duplicates()虽然会损失样本量但模型更可信。4.4 随机种子可复现性神经网络初始化是随机的如果不固定种子每次训练出来的结果可能差几个点。设定固定种子的标准写法import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)如果你用的是 GPU还需要设置torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False第一个选项让 cuDNN 使用确定性算法第二个选项禁止它自动选择最快速的算法。代价是性能稍微下降但换来的是结果可复现。这个取舍在实验阶段完全值得。5. 进阶方向从入门实战走向正式项目5.1 特征工程还能深挖多少真正的冠军方案很多功夫在特征工程上而不是模型结构上。比如把面积和房间数组合成一个新特征每间房平均面积这在某些市场里比单个特征更有解释力。把房龄转换成“最近是否翻新”的布尔特征。组合特征的目的不是增加信息而是降低模型的拟合难度。网络理论上自己能学到特征交互但样本量不够时你显式地把组合特征喂进去就是给模型省事。5.2 用交叉验证替代单一切分单次随机切分有一个问题结果受运气影响太大。划分出来的测试集恰好包含一些极端样本模型表现就会波动。更稳的做法是 K 折交叉验证把数据切成 K 份轮流取其中一份做测试其余做训练最后把 K 次结果平均。PyTorch 里做 K 折交叉验证并不复杂核心代码是from sklearn.model_selection import KFold kf KFold(n_splits5, shuffleTrue, random_state42) for fold, (idx_train, idx_test) in enumerate(kf.split(X_scaled)): X_train_fold torch.tensor(X_scaled[idx_train], dtypetorch.float32) X_test_fold torch.tensor(X_scaled[idx_test], dtypetorch.float32) # 然后就是标准的训练流程注意 KFold 的对象是标准化之后的全量数据而且标准化要放在整个 KFold 之前还是每次折内重新做严格来说应该每次折内在训练集上重新 fit 标准化因为这更接近真实场景。但为了代码简洁、入门好理解很多人会直接全量标准化再做 KFold这会轻微高估模型性能。实战中如果追求严谨务必要把 StandardScaler 放进每一折的处理流程里。5.3 模型对比不是可选项我见过很多人玩神经网络玩得很嗨但从来没拿它跟其他模型比过。这是不对的。经典的梯度提升树如 XGBoost、LightGBM在表格数据上往往和神经网络打得有来有回甚至经常小胜。房价预测这种中低维表格任务梯度提升树可能是更优的选择。我的建议是把 PyTorch 的 MLP 当成基线之一再用 sklearn 的RandomForestRegressor或HistGradientBoostingRegressor跑一遍同样的数据对比 RMSE 和 R²。这样做有三个好处如果树模型远胜 MLP说明你的数据里还有没用起来的非线性结构或者你给 MLP 喂的特征还不够好。如果两者相近恭喜你整个流程的方法论是可靠的。如果 MLP 胜出且差距明显那你的网络结构、训练配置值得好好保留下来。模型对比不是可选项它是判断你的深度学习工作有没有价值的最快方式。写在最后的一点个人体会这个项目做下来我最想对新人说的一句话是神经网络回归的难点不在网络而在数据。整个实战过程里真正让模型分数产生质的提升的不是把隐藏层从两层加到三层而是把目标变量做了 log 变换、把数值特征做了合理的标准化、把类别特征做了合适的编码。模型结构只要不是太离谱差异都有限。还有就是别怕踩坑。我最初跑出来的模型 R² 只有 0.6 左右一开始以为网络结构有问题后来排查半天发现是标准化时用了全量数据 fit测试集评估结果虚高了真正的问题反而是训练集和测试集分布不一致。这类坑看文档是看不出来的只有亲手把数据翻来覆去地看、把每一行代码的输入输出打印出来检查才能真正建立直觉。后面如果想把项目继续往深了做可以试试把特征工程做细之后换梯度提升树来对比再试试用 SHAP 解析特征贡献度都是很有价值的方向。先把这条全流程跑顺你的机器学习和 PyTorch 基本功就算立住了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑