资讯动态

PyTorch房价预测实战:从数据清洗到模型训练全流程解析

发布时间:2026/10/19 7:59:31 来源:尧图企业网站定制
做机器学习的人几乎都绕不过房价预测这个经典练手赛。用PyTorch把Kaggle上的房价预测比赛完整跑一遍对我来说收获特别大倒不是因为这个比赛本身有多难而是它把整个机器学习流程从头到尾都串起来了数据清洗、特征工程、模型设计、训练调优、结果提交每一步都有讲究。这篇文章就把我完整的实战过程拆开来讲包括怎么理解赛题、怎么处理那堆缺失值和偏态分布的特征、怎么用PyTorch写数据管道和训练循环以及我在实际过程中踩过的坑和总结的实用技巧。不管你是刚接触PyTorch的初学者还是已经跑过几个项目想系统走一遍完整流程的进阶选手这篇文章都可以作为一份能直接照做的参考。1. 赛题速览房价预测到底在考什么1.1 先搞清楚任务本质很多人一上来就急着写模型把赛题说明扔到一边这是最亏的。房价预测比赛给的是某地住宅销售的历史记录包含八十多个描述房屋属性的特征从地块面积、建筑年份、地下室质量到车库容量五花八门目标变量是销售价格。任务非常明确用这些特征预测房价评估指标是预测值与真实值的误差程度。这个赛题在Kaggle上被归为初级入门级别但别被入门两个字骗了。真正做完你会发现它考察的是机器学习流程中最核心的基本功数据是否干净、特征是否有效、模型是否过拟合、验证方式是否可靠。这四个点任何一个出问题分数都会很难看。所以我建议所有想系统入门机器学习的朋友第一个完整项目就做它性价比极高。需要特别注意的是目标变量的分布形态。房价数据几乎所有情况下都是右偏的也就是少数豪宅把分布往右边拉扯得很长。直接用原始价格做回归模型会拼命去拟合那些极端值结果就是中低端房屋的预测一塌糊涂。正确的做法是对价格取对数让分布变得接近正态训练稳定性和最终排名都能明显改善。这一点在后面的数据管道里我会详细讲。1.2 方案选型为什么用PyTorch自己做既然是Kaggle比赛最常见的做法是直接上XGBoost或者LightGBM这类梯度提升树模型因为它们对表格数据非常友好特征工程也省心。但我在这篇实战里坚持用PyTorch自己搭神经网络有两个原因。第一对于想深入理解深度学习的人来说房价预测是一个绝佳的教学场景。表格数据维度不算高、样本量不大单卡训练很快可以反复试验不同的网络结构、损失函数、正则化手段试错成本极低。第二神经网络在表格数据上虽然默认打不过精心调参的树模型但通过合理的特征工程和结构设计差距并没有想象中那么大而且整个过程中你能学到的东西远比拿个名次多得多。我采用的方案是全连接网络MLP配合标准化、Dropout、学习率调度和K折交叉验证。这个组合是一个被反复验证过的基线方案既能保证不错的效果又不会让代码复杂度失控。后面的所有讨论都基于这个技术路线展开如果你之后想对比树模型也可以拿这份代码作为基准。2. 数据摸底与特征工程2.1 读数据前的第一件事拿到数据文件之后第一步绝对不是直接pd.read_csv()然后开干而是先回答三个问题数据规模多大、缺失情况如何、每个特征是什么类型。我习惯先写一段快速探查代码把数据集的形状、特征类型分布、缺失值统计一次性打出来。import pandas as pd import numpy as np train pd.read_csv(train.csv) test pd.read_csv(test.csv) print(训练集形状:, train.shape) print(测试集形状:, test.shape) print(特征总数:, train.shape[1]) missing train.isnull().sum() missing missing[missing 0].sort_values(ascendingFalse) print(含缺失值的特征数量:, len(missing)) print(missing.head(20))这段代码跑完后你会看到一些非常典型的现象某些特征缺失率超过99%例如地块上的泳池相关字段因为绝大多数房子根本没有泳池还有一批特征缺失率在30%到50%之间比如巷道类型、砌面类型等这些缺失往往不是随机发生的而是和房屋档次相关。这个观察结果直接决定了后续缺失值填充的策略而不是无脑填中位数。还有一个很容易被忽略的点训练集和测试集的形状不一致很常见但如果某列只在训练集出现、而测试集没有说明数据本身有问题一般是特征对齐出错。我在写代码时会把训练集和测试集先纵向拼接再统一做特征工程既能保证两边处理逻辑一致又能避免少处理一列导致的低级报错。2.2 缺失值处理的三个层次缺失值处理是房价预测赛题最大的得分点之一因为几十个特征里几乎一半都有缺失。我把它分成三个层次来对待而不是用一种方法通吃。第一层次是缺失本身有含义的特征。比如地下室总面积相关的特征如果缺失往往表示这栋房子根本没有地下室。这时候填0不仅合理而且是在给模型传递一条真实信息。第二层次是缺失与等级相关的特征比如外部装修质量评级缺失通常代表无此项或较差需要结合业务含义填一个低位类别或固定的等级值。第三层次才是真正需要统计方法介入的数值特征例如地块面积、街道宽度这些用中位数填充即可一般不需要复杂的插值法因为树模型和神经网络对填充方式的敏感度不同神经网络对错误填充值的容忍度更低所以处理要更谨慎。在PyTorch项目里我倾向于把所有缺失值处理都放到特征工程阶段完成输出一个干净的DataFrame这样数据管道里就不再处理缺失了。这比把缺失值逻辑留在Dataset类里要清晰得多也方便调试。切记不要在训练集和测试集上分别做填充统计一定要先拼接再统一计算填充值否则会造成信息泄漏。2.3 类别特征与数值特征的处理路线房价预测数据里有大量类别特征比如道路类型、街区类型、屋面材料、房屋整体形状种类很杂很多类别特征的取值数量不少。我把处理策略分成三类取值数量少的有序类别比如质量评级从低到高分档直接映射成整数因为等级本身有顺序意义取值数量少的无序类别比如巷道类型用独热编码取值特别多的类别特征比如邻居区域传统做法也是独热但会导致维度爆炸我实测下来把它当作数值编码反而更稳比如用该类别下的价格均值作为编码值。这里有一个神经网络特有的注意事项独热编码后的稀疏特征矩阵会显著增加网络输入维度如果全连接网络第一层太大参数量会暴涨既慢又容易过拟合。所以在特征工程阶段我会有意识地控制类别特征的编码方式尽量压缩维度。数值特征方面除了直接使用原始值我还会额外构造几个交叉特征。最有用的一个公式是房屋总面积按地面以上所有楼层面积加总计算再除以地块面积得到建筑密度。这个比值对房价的解释力非常强因为它同时刻画了土地价值和房屋体量。类似的还有总卫生间数量等于各层卫生间数量之和、房龄等于当前年份减去建造年份这类衍生特征。这些特征不是越多越好挑三到五个有明确业务逻辑的加上去就够了。3. 用PyTorch搭建数据管道3.1 Dataset类设计的核心逻辑PyTorch的数据管道核心是torch.utils.data.Dataset和DataLoader。很多初学者一上来就写一个__getitem__返回原始数值结果训练时各种数据类型不匹配卡住半天。我的做法是把所有预处理都提前做完Dataset类只干两件事负责索引样本和把数据转成张量。import torch from torch.utils.data import Dataset class HousePriceDataset(Dataset): def __init__(self, features, labelsNone): self.features torch.tensor(features, dtypetorch.float32) self.labels torch.tensor(labels, dtypetorch.float32) if labels is not None else None def __len__(self): return len(self.features) def __getitem__(self, idx): x self.features[idx] if self.labels is not None: y self.labels[idx] return x, y return x这个类简单到几乎不需要解释但恰恰是这种简单才不容易出错。labels为None时用于推理阶段也就是对测试集做预测时不需要标签。有的同学会在同一条数据管道里既当训练用又当预测用用一堆if分支来切换状态我觉得完全没必要分开两个数据集实例更干净。一个容易忽略的细节是数据类型的对齐。原始数据里可能混入整数、浮点、字符串在特征工程阶段必须统一转成float32否则转成张量时要么报错要么精度爆炸。另外特征矩阵在进入Dataset之前就要保证是连续的NumPy数组且没有NaN。我写过一个检查函数在初始化时直接断言not np.isnan(features).any()一旦触发就立刻报错能省下大量排查时间。3.2 DataLoader的关键参数与坑DataLoader是训练循环的入口参数不多但每个都有讲究。最重要的几个是batch_size、shuffle、num_workers和drop_last。batch_size对于这个数据集我推荐64或128。这个赛题样本量不到两千如果batch太小梯度的噪声会很大训练曲线震荡得厉害如果batch太大每个epoch更新次数太少收敛变慢。实测128配合适当的学习率效果最稳。shuffle在训练集上必须为True在验证集和测试集上必须为False。这个看起来像常识但我确实见过有人在验证集上也开shuffle导致每次验证的样本顺序都不一样虽然不影响最终结果但会干扰你对训练过程的观察。num_workers在多核机器上可以开到4或8加速数据读取。但要注意如果特征工程做得好、数据都在内存里这里其实没什么加速空间开太多反而可能引起多进程的开销。Windows系统上如果跑在Jupyter里num_workers超过0有时候会卡死遇到这种情况直接设成0就行。train_loader DataLoader( train_dataset, batch_size128, shuffleTrue, num_workers4, drop_lastTrue ) val_loader DataLoader( val_dataset, batch_size256, shuffleFalse, num_workers4, drop_lastFalse )drop_last这个参数容易被忽视。当训练集样本数不能被batch_size整除时最后一个batch会偏小。如果开了drop_last这个不完整的batch会被丢弃如果不开模型会用到它但可能导致最后的训练步数和别的epoch不一致。对于小数据集我倾向于drop_last设为True让每个epoch的步数完全一致观察loss曲线更清晰。3.3 特征标准化与目标变量处理神经网络对输入特征的尺度极其敏感。如果某些特征是房价级别的大数值某些特征是0到1的小数值网络初始化的梯度会被大数值特征主导训练非常不稳定。所以标准化不是可选项而是必选项。我使用sklearn.preprocessing.StandardScaler在训练集上fit然后同时transform训练集和测试集。这里有一个关键纪律标准化参数只能用训练集来算不能把测试集数据混进去fit否则验证集的分布就不纯粹了属于泄漏的一种。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val) X_test_scaled scaler.transform(X_test)目标变量同样需要处理。前面提到过房价的右偏分布问题我的做法是对价格取log训练时用log后的值作为回归目标提交前再用指数还原成实际房价。注意还原时的操作顺序先对预测值取指数再计算指标否则误差会被对数变换扭曲。还有一个小细节如果做了K折交叉验证每一折都必须独立fit标准化器不能在整个训练集上fit一次再套用到每一折。这是新手最容易犯的交叉验证泄漏错误我会在后面的验证策略里单独说。4. 模型构建与训练循环4.1 网络结构怎么定房价预测的特征数量经过特征工程后一般在几十到上百维之间。这个维度下全连接网络是最合理的起点。我的网络结构设计原则很简单宽度从输入维度开始逐层减半深度控制在3到4层每层后面接ReLU激活、BatchNorm和Dropout。import torch.nn as nn class MLPRegressor(nn.Module): def __init__(self, input_dim, hidden_dims[256, 128, 64], dropout0.3): super().__init__() layers [] prev_dim input_dim for hdim in hidden_dims: layers.append(nn.Linear(prev_dim, hdim)) layers.append(nn.BatchNorm1d(hdim)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout)) prev_dim hdim layers.append(nn.Linear(prev_dim, 1)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x)为什么用逐层减半而不是别的结构因为表格数据的复杂度通常不会需要很宽的网络过宽的第一层容易把大量参数浪费在无关特征组合上。256、128、64这样的递减结构每一层都在提炼更高阶的信息参数量适中配合Dropout可以很好地控制过拟合。BatchNorm放在哪里是有讲究的。我习惯放在Linear和ReLU之间因为先归一化再激活可以让激活函数的输入保持稳定。Dropout放在激活之后记住它在训练和推理时行为不同这一点PyTorch已经处理好了不用手动干预但你要清楚推理时Dropout是自动关闭的。输出层只有一个神经元因为这是回归任务。激活函数是线性的也就是不加任何激活。如果输出层加了sigmoid或者tanh输出范围会被限制在某个区间里房价预测根本没法用。4.2 损失函数与优化器选择回归任务最常用的损失函数是均方误差损失nn.MSELoss()。由于我们已经对目标变量取过log所以这里的MSE实际上是在对数空间计算的。如果直接对原始价格计算MSE豪宅样本的误差会主导梯度模型会偏向预测高价位房屋中低价位惨不忍睹。优化器我选择Adam学习率初始值设为1e-3配合ReduceLROnPlateau学习率调度器。这个调度器会在验证loss连续若干个epoch不下降时自动把学习率乘以一个衰减因子例如0.5。这个机制非常省心比手动调学习率高效得多。import torch.optim as optim criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience10, verboseTrue )weight_decay这个参数很多人忽略。它在Adam里等效于L2正则化能有效抑制过拟合。对于房价预测这种样本量不大的任务我建议设一个较小的值比如1e-5既有正则化效果又不会因为太大而让模型欠拟合。这里有个我早年踩过的坑MSE的数值在log空间下非常小一般只有零点零几。有人看到loss这么小以为模型效果很好其实还要还原到原始价格空间去计算误差才真实。log空间loss是0.02对应原始价格大概有几万美元的误差这在新手看来很难直观理解。所以我会在训练时同时打印log空间MSE和还原后的均方根误差方便判断模型的真实水平。4.3 训练验证流程与模型保存训练循环本身写起来比较机械但有一些细节决定了训练过程是否可控。我把训练和验证分成两个函数每轮epoch先训练、后验证验证结束后更新学习率调度器并打印该epoch的关键指标。def train_epoch(model, loader, criterion, optimizer): model.train() total_loss 0.0 for x, y in loader: optimizer.zero_grad() pred model(x) loss criterion(pred.view(-1), y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) return total_loss / len(loader.dataset) def validate_epoch(model, loader, criterion): model.eval() total_loss 0.0 with torch.no_grad(): for x, y in loader: pred model(x) loss criterion(pred.view(-1), y) total_loss loss.item() * x.size(0) return total_loss / len(loader.dataset)模型保存这块我只保存验证loss最优的那一版而不是最后一轮。很多人在epoch全部跑完后直接拿最终模型去预测但如果训练后期过拟合了最后一轮往往不是最优解。正确做法是在验证函数里记录当前最佳loss每当loss下降就把model.state_dict()保存到文件中训练结束后再加载这个最优模型。另外一个实用技巧是梯度裁剪。虽然Adam对梯度爆炸有一定的鲁棒性但表格数据偶尔会出现极端特征组合导致梯度异常。在optimizer.step()之前加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)能有效防止这种偶发情况把训练过程毁掉。这个操作不会对正常训练产生明显影响我默认都会加上。K折交叉验证是我在这个赛题上的核心验证策略。因为训练集样本量不大单次划分训练集和验证集结果受划分方式影响太大。我采用5折交叉验证每一折训一个模型最终用5个模型对测试集预测取平均成绩比单模型稳定得多。每折的流程一致用该折的训练部分fit标准化器、训练模型用验证部分评估并保存最优模型。5. 常见问题与排查实录5.1 过拟合信号怎么早发现房价预测样本量小、特征多过拟合是头号问题。我在实战中总结了一套观察信号的方法能在训练早期就发现苗头。最直接的信号是训练loss持续下降但验证loss在某一个epoch后开始反弹。这时候出现过拟合了。第二个信号是训练loss降到非常低比如log空间MSE小于0.005但验证loss还停在0.03左右这种差距就是典型的过拟合表现。第三个信号是随着训练进行模型对某个折的预测方差增大也就是同样一组超参数不同的随机种子跑出来的验证指标差异很大这通常说明模型容量过大或者正则化不足。解决过拟合的措施要按照优先级来试。先加大Dropout概率从0.3调到0.5这一步最简单有效。其次减小网络宽度把256、128、64改成128、64、32参数量直接砍掉一半多。再次增加weight_decay强度从1e-5提高到1e-4。最后也是最有效但最费事的方法增加数据量这一点在比赛中做不到但可以通过更强的数据增强来缓解比如对特征加微小噪声或者对训练样本做随机扰动。表格数据的数据增强确实可行但扰动幅度要非常小心太大了会破坏特征含义。5.2 最容易踩的数据泄漏坑数据泄漏是这种表格竞赛里最隐蔽的错误它不会让程序报错但会让验证指标虚高提交后成绩大跳水。我在实践过程中总结出三个高发场景每个都亲手踩过。第一个是标准化参数泄漏。前面说过如果在每一折内没有重新fit标准化器而是用全量训练集fit好的scaler去处理验证集验证集的信息就有一部分来自训练过程导致验证指标偏乐观。第二个是缺失值填充统计泄漏。填充中位数时必须先fit在训练部分再transform验证部分直接用全量统计量填充也一样是泄漏。第三个是特征选择泄漏。如果我先看了全量训练集和验证集的标签分布再决定保留哪些特征这本质上就把验证集信息用在了模型选择上。一个非常有效的自查方法是观察训练集和验证集的指标差距。正常情况验证loss会略高于训练loss但如果验证loss比训练loss还低几乎可以断定有泄漏。我遇到这种情况的第一反应就是检查标准化器、填充统计量和特征选择逻辑。# 错误示范用全量训练集fit的scaler处理验证集 scaler StandardScaler().fit(X_train_full) X_val_scaled scaler.transform(X_val) # 正确示范每一折内独立fit scaler StandardScaler().fit(X_train_fold) X_val_scaled scaler.transform(X_val_fold)泄漏问题在最终提交前很难被完全识别所以我养成了一个习惯每次改动特征工程或数据管道之后先跑一个快速的小规模交叉验证如果指标上升幅度异常大就先怀疑泄漏而不是先高兴。5.3 提升成绩的几条实战路径当基线模型跑通之后想进一步提升排名我建议按以下顺序尝试而不是东一榔头西一棒子。第一步是攻特征工程。这个赛题中特征工程的收益远大于模型结构改动。我实测效果较好的几个特征操作包括把年份类特征做差得到房龄、把多个质量等级特征取最大值或最小值得到整体品质指标、把地上面积与地下室面积相加得到总使用面积、把车库容量和车库面积合并成一个车库能力综合特征。每加一个特征就跑一次5折验证看均值有没有提升有就保留没有就撤销绝不做无脑叠加。第二步是改进验证策略。5折交叉验证换成10折虽然训练时间翻倍但对于小数据集更稳定。还可以尝试分层抽样按价格区间分层让每一折的标签分布更接近全量。第三步是模型集成。最实用的集成方案是用多个模型分别预测再做几何平均或加权平均。我在这个赛题上常用的组合是不同随机种子训练的三个同一结构的神经网络加上一个用相同特征训练的XGBoost加权平均后通常比单模型提升3到5个百分点。集成的核心前提是各个模型之间的相关性要低如果全是同一个结构、同一个种子集成效果等于零。最后分享一个教训不要过度追求排名而牺牲代码的简洁性。我见过有人为了排名叠加了十几个模型、几百个特征最后根本说不清楚每个部分贡献了多少代码也没法复用。更好的状态是保持一套清晰的基线流程每次改动都记录在案这样的实践经验才真正属于你自己。我个人在实际操作中最深的体会是房价预测这个赛题的价值不在于分数本身而在于它把从数据到模型的每个环节都暴露在你面前让你建立起一套完整的、可复用的排查方法论。后来我再做其他表格类项目依然沿用这套流程逻辑只是换数据和网络结构而已。

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

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

免费获取报价 →
↑