
简介这份Python代码包围绕BP神经网络房价预测展开面向具备Python基础、希望动手理解多层前馈网络与回归任务的机器学习初学者和深度学习实践者。包内共2个文件由1个Python脚本和1个CSV数据集组成压缩后约8KB轻量却完整覆盖数据读取、网络结构定义、前向传播、方差损失计算、反向传播权重更新以及测试评估等关键环节便于读者直接运行并对照理解回归预测流程。该案例以经典波士顿房价数据为背景涉及特征预处理、隐藏层设计与模型性能衡量也可作为课程实验或项目练手素材。目前已有21749人学习下载说明其在入门实践中具有较高参考热度。通过脚本与数据的配套读者可以观察从输入特征到预测价格的完整链路并在此基础上调整网络参数、损失函数与评估指标积累神经网络回归任务的调试经验。1. 一份房价预测 BP 神经网络 Python 代码先判断它能不能替你省掉返工一份房价预测的 BP 神经网络 Python 代码最容易让人误判的地方是把“跑通”当成“可用”。很多人拿到代码后先装依赖、改路径、点运行看到 loss 从 0.8 降到 0.2就以为回归预测的链路已经完成真正拿一批新房源做推理时才发现预测值挤在同一条水平线上。这份代码解决的是从结构化特征表到连续房价输出的完整闭环特征清洗、标准化、BP 网络训练、验证集早停、模型保存和批量推理。它适合手里有面积、房龄、楼层、装修等字段想用 Python 快速搭一个回归预测基线的人也适合课程设计或模拟项目里需要讲清前向传播、反向传播和损失收敛的场景。它不替代业务规则和特征工程但能帮你把“网络怎么搭、参数怎么调、坑在哪”先走一遍。2. 网络骨架怎么搭输入维度、隐藏层宽度与回归损失函数的取舍2.1 房价回归为什么适合用 BP 神经网络做基线房价预测的输出是一个连续值面积、房龄、楼层、地段等级这些字段和价格之间很少是纯线性关系。面积翻倍不代表总价翻倍房龄增加五年在不同总价段上的折价幅度也不一样这类交互项用线性回归硬拟合会欠表达。BP 神经网络的价值在于隐藏层能把原始特征组合成非线性映射用反向传播把预测误差逐层传回去适合先做一个比线性回归更有弹性的基线。但 BP 网络不是越深越好。房价数据常见样本量在几千到几万条特征列几十个隐藏层堆到四五层以后验证集往往先变差。我一般会把两到三层全连接层当作起点输入维度等于特征列数量输出层只保留一个神经元并且不加 Sigmoid 或 Tanh。回归预测的输出范围不应该被压到 0 到 1否则反标准化后会出现系统性偏移这是很多人第一次翻车的地方。损失函数也要跟着任务走。MSE 对异常房价非常敏感一套高价房源就能把梯度拉偏HuberLoss 在误差较小时接近 MSE误差较大时转为线性训练更稳。代码里如果标签已经标准化HuberLoss 的 delta 可以设在 1.0 附近如果标签保留原始房价delta 要按房价量级放大否则大部分样本都会落在线性区。2.2 网络结构的四个参数怎么定拿到这份 Python 代码后先不要急着改层数先把输入维度、隐藏层宽度、输出层形式和损失函数对一遍。下面这张表是我在房价回归任务里常用的起点不是固定答案但能避免一上来就把网络搭得过大或过小。参数常用起点调整方向注意点输入维度等于特征列数量one-hot 后列数会增加训练和推理必须用同一份列名隐藏层64 → 32样本少改 32 → 16特征多改 128 → 64不要只看训练 loss输出层1 个神经元Linear不要加激活函数回归输出保持无界损失函数HuberLoss 或 MSE异常值多用 Huber标签标准化后 delta 要同步优化器Adamlr1e-3损失震荡降到 1e-4配合 weight_decaybatch_size64 或 128样本少用 32太小会让梯度噪声大epoch200 起步配合早停 patience20不要固定跑满正则weight_decay1e-4过拟合时加到 1e-3和 Dropout 二选一先试输入维度不是拍脑袋写的它来自X_train.shape[1]。类别字段做 one-hot 后列数会比原始字段多尤其是朝向、装修、地段等级这类枚举值。隐藏层宽度和样本量要匹配几千条样本用 64/32 足够几万条样本可以试 128/64但每加宽一层都要看验证集是否真的下降。学习率 1e-3 是 Adam 的常用起点损失曲线如果在前几十个 epoch 剧烈震荡先降到 1e-4不要急着换优化器。2.3 用 PyTorch 搭一个可训练的最小骨架这份代码的网络部分可以保持很轻关键是输出层和损失函数别写错。下面这个类定义了一个两层隐藏层的 BP 网络输入维度由外部传入前向传播只做线性变换和 ReLU最后一层输出一个标量。import torch from torch import nn class HousePriceBP(nn.Module): def __init__(self, in_dim): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1) # 回归输出保持线性不加 Sigmoid ) def forward(self, x): # squeeze(-1) 把 [batch, 1] 压成 [batch]和标签维度对齐 return self.net(x).squeeze(-1)in_dim来自训练集特征列数量不要手写死。两层隐藏层分别 64 和 32适合作为房价回归的基线squeeze(-1)是为了让模型输出和y的[batch]维度一致否则损失函数会广播出错误形状。最后一层没有激活函数这是回归任务和分类任务最明显的区别之一。模型实例化之后损失函数和优化器要一起定下来。下面的写法把 HuberLoss 和 Adam 放在一起weight_decay作为轻量正则学习率先固定 1e-3。model HousePriceBP(in_dimX_train_s.shape[1]) criterion nn.HuberLoss(delta1.0) # 标签标准化后 delta 可取 1.0 optimizer torch.optim.Adam( model.parameters(), lr1e-3, weight_decay1e-4 )HuberLoss的delta控制 MSE 和线性损失的切换点标签标准化后 1.0 是合理起点如果标签保留原始房价delta要按房价量级调整。weight_decay1e-4是轻量正则验证集反弹时可以加到 1e-3。优化器只接收model.parameters()不要把 scaler 或其他对象传进去。这个骨架不复杂但输出层、损失函数、学习率这三处一旦写错后面调参基本是白费。3. 数据进网络之前特征清洗、标准化和 DataLoader 的三个动作3.1 特征列筛选与缺失值处理房价数据最常见的字段包括面积、房龄、楼层、总楼层、卧室数、卫生间数、朝向、装修、地段等级。数值列里经常混进“暂无”“约 89 平”这类脏字符串直接转 float 会报错类别列缺失时不要直接删行房价样本本来就贵删多了验证集会不稳。我一般先把数值列强转失败的置为 NaN再用训练集的中位数填充类别列缺失单独归到“未知”一档。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(house_price.csv) target_col price num_cols [area, age, floor, total_floor, bedrooms, bathrooms] cat_cols [orientation, decoration, district_level] for c in num_cols: df[c] pd.to_numeric(df[c], errorscoerce) df[c] df[c].fillna(df[c].median()) for c in cat_cols: df[c] df[c].fillna(未知).astype(str) df pd.get_dummies(df, columnscat_cols, drop_firstTrue) X df.drop(columns[target_col]) y df[target_col].astype(float)pd.to_numeric(..., errorscoerce)把无法解析的字符串转成 NaN避免整列转换失败中位数填充比均值更稳房价数据里极端面积不少。类别列先填“未知”再 one-hot等于给缺失值单独留一档。drop_firstTrue去掉每个类别组的第一列减少共线性。X必须把目标列price丢掉这一步写错会让模型直接看到答案训练 loss 会低得不正常。注意one-hot 之后的列顺序要保存下来。推理时如果新数据的类别列顺序和训练不一致模型会把特征对错位置预测值会整体偏移。3.2 标准化只在训练集 fit标准化最常见的错误是先对整个数据集fit_transform再切分训练集和验证集。这样验证集的均值和方差已经进入训练过程验证 loss 会虚低线上效果却对不上。正确顺序是先切分再只用训练集 fit验证集和测试集都只做 transform。标签也要标准化否则房价量级太大HuberLoss 的 delta 和学习率都不好设。from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) X_train, X_val, y_train, y_val train_test_split( X_train, y_train, test_size0.2, random_state42 ) x_scaler StandardScaler() X_train_s x_scaler.fit_transform(X_train) X_val_s x_scaler.transform(X_val) X_test_s x_scaler.transform(X_test) y_scaler StandardScaler() y_train_s y_scaler.fit_transform(y_train.values.reshape(-1, 1)).ravel() y_val_s y_scaler.transform(y_val.values.reshape(-1, 1)).ravel() y_test_s y_scaler.transform(y_test.values.reshape(-1, 1)).ravel()这里先按 8:2 切出测试集再从训练集里切 8:2 做验证集最终比例接近 64:16:20。fit_transform只出现在X_train和y_train上验证集和测试集只调用transform。标签用reshape(-1, 1)是因为StandardScaler要求二维输入转完再ravel()变回一维和模型输出对齐。random_state42固定切分方便复现换数据时可以改但同一轮实验里不要反复改。3.3 小批量训练循环与损失记录数据准备好之后训练循环要解决三件事小批量喂数据、记录训练和验证损失、在验证集不再下降时保存最优权重。DataLoader的训练集要shuffleTrue验证集要shuffleFalse否则损失记录会带随机性。早停的patience不要设得太小房价数据噪声大验证 loss 抖动几个 epoch 很正常。import numpy as np import torch from torch.utils.data import TensorDataset, DataLoader train_ds TensorDataset( torch.tensor(X_train_s, dtypetorch.float32), torch.tensor(y_train_s, dtypetorch.float32) ) val_ds TensorDataset( torch.tensor(X_val_s, dtypetorch.float32), torch.tensor(y_val_s, dtypetorch.float32) ) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size256, shuffleFalse) model HousePriceBP(X_train_s.shape[1]) criterion nn.HuberLoss(delta1.0) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) history {train: [], val: []} best_val float(inf) best_state None patience, wait 20, 0 for epoch in range(200): model.train() train_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() train_loss loss.item() * len(xb) train_loss / len(train_ds) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss criterion(pred, yb).item() * len(xb) val_loss / len(val_ds) history[train].append(train_loss) history[val].append(val_loss) if val_loss best_val - 1e-5: best_val val_loss best_state {k: v.clone() for k, v in model.state_dict().items()} wait 0 else: wait 1 if wait patience: break model.load_state_dict(best_state)训练集batch_size64验证集可以放到 256因为验证阶段不需要反向传播。loss.item() * len(xb)是按样本数加权最后除以数据集大小避免最后一个 batch 偏小导致均值偏差。早停的1e-5是防止验证 loss 微小抖动触发重置best_state保存的是最优轮次的权重不是最后一轮。patience20表示验证 loss 连续 20 轮没有实质下降就停实际训练中经常在 60 到 120 轮之间结束。4. 训练翻车排查损失不降、验证集反弹和梯度异常的定位顺序4.1 损失不降时先查数据管线和学习率训练 loss 一开始就不降先别怀疑网络结构。房价回归里更常见的原因是标签没标准化、特征里有 NaN、目标列混进了特征或者学习率大到梯度直接跳过谷底。我的顺序是先打印一个 batch 的输入均值、标准差、标签范围和 NaN 计数再确认X里没有price列最后把学习率从 1e-3 降到 1e-4 试一轮。xb, yb next(iter(train_loader)) print(x mean/std:, xb.mean().item(), xb.std().item()) print(y mean/std:, yb.mean().item(), yb.std().item()) print(x nan:, torch.isnan(xb).any().item()) print(y nan:, torch.isnan(yb).any().item())标准化后的输入均值应该接近 0标准差接近 1标签标准化后也应该在这个范围附近。如果x mean是几百说明标准化没生效如果x nan是 True回去查缺失值填充。标签范围如果还是几百万说明y_scaler没作用到训练集。学习率方面Adam 的 1e-3 在标准化数据上通常能用但 HuberLoss 的 delta 太小或标签未标准化时1e-3 也可能让 loss 震荡。4.2 验证集反弹时用早停和权重衰减训练 loss 持续下降、验证 loss 先降后升是典型的过拟合。房价数据特征维度不高时先加weight_decay和早停不要一上来就堆 Dropout。Dropout 对全连接层有效但小样本上会把有效信号打散p0.2已经不算低。下面的模型把 Dropout 放在两个隐藏层之后作为过拟合时的备选。class HousePriceBPDropout(nn.Module): def __init__(self, in_dim, p0.2): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Dropout(p), nn.Linear(64, 32), nn.ReLU(), nn.Dropout(p), nn.Linear(32, 1) ) def forward(self, x): return self.net(x).squeeze(-1)p0.2表示每个神经元有 20% 概率被置零训练阶段生效推理阶段自动关闭。小样本房价数据先试weight_decay1e-4到1e-3再考虑 Dropout如果加了 Dropout 后训练 loss 降不下去说明正则过强把p降到 0.1 或去掉。早停的patience可以保持 20因为验证 loss 的抖动周期通常比分类任务长。4.3 梯度消失或爆炸的检查代码BP 网络的反向传播依赖梯度梯度范数太小会让参数几乎不更新太大则会让 loss 变成 NaN。训练循环里每隔几十个 batch 打印一次总梯度范数能快速判断是学习率问题还是数据尺度问题。total_norm 0.0 for p in model.parameters(): if p.grad is not None: param_norm p.grad.data.norm(2) total_norm param_norm.item() ** 2 total_norm total_norm ** 0.5 print(grad norm:, total_norm) # 梯度爆炸时可以在 loss.backward() 后加裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)总梯度范数持续低于1e-6通常是学习率太小、特征未标准化或网络太深高于1e3则要查标签尺度、学习率和异常房价。clip_grad_norm_的max_norm5.0是常用起点它不会改变梯度方向只把范数压到阈值以内。裁剪放在loss.backward()之后、optimizer.step()之前顺序写反了不会生效。4.4 四条踩坑记录现象、原因、解决下面这四条是房价回归 BP 网络里高频出现的翻车记录现象和原因往往对不上按顺序排查能少走弯路。现象原因解决预测值几乎全一样输出层加了 Sigmoid或标签未标准化输出层改 Linear标签标准化后推理反标准化验证 loss 比训练 loss 低很多切分前做了标准化验证集信息泄漏先切分再只用训练集 fit scalerloss 突然变 NaN学习率过大、房价极端值或输入含 inf降学习率换 HuberLoss检查 inf 和缺失值训练集误差低新数据误差大one-hot 列顺序不一致或缺少列保存特征列名推理时 reindex 对齐第一条最容易被忽略因为模型能训练、loss 也能降但输出被压到 0 到 1反标准化后所有预测都挤在均值附近。第二条会让验证指标虚好线上效果对不上血泪经验是标准化器必须和切分顺序一起检查。第三条先看输入有没有inf再看学习率最后看标签是否有极端值。第四条发生在推理阶段训练时列顺序固定新数据 one-hot 后列顺序可能变必须用保存的列名重新索引。5. 推理链路收口模型保存、批量预测和反标准化的误差复盘5.1 模型保存与加载的版本兼容训练完成后不要只保存state_dict还要把输入维度、特征列名、特征标准化器和标签标准化器一起打包。只保存权重推理时忘了标准化参数预测值会整体偏移只保存整个模型对象跨 Python 或 PyTorch 版本加载容易失败。下面的写法把模型状态和预处理对象放进一个字典加载时先重建网络结构再灌权重。import torch torch.save({ model_state: model.state_dict(), in_dim: X_train_s.shape[1], feature_columns: list(X.columns), x_scaler: x_scaler, y_scaler: y_scaler, }, house_bp_bundle.pt) ckpt torch.load(house_bp_bundle.pt, map_locationcpu) model HousePriceBP(ckpt[in_dim]) model.load_state_dict(ckpt[model_state]) model.eval()feature_columns保存的是 one-hot 之后的完整列名推理时用它对齐列顺序。x_scaler和y_scaler是 sklearn 对象跟着字典一起序列化避免推理时重新 fit。map_locationcpu让模型在无 GPU 环境也能加载部署时更稳。model.eval()不能漏Dropout 和 BatchNorm 在推理阶段的行为和训练阶段不同。5.2 批量预测与反标准化推理函数要处理三件事类别列缺失填充、one-hot 后列对齐、反标准化还原房价。新数据可能缺少某个类别reindex时用 0 填充表示该类别不出现。数值列的缺失值填充要和训练一致训练用中位数推理也要用同一份中位数不能重新算。def predict_price(df_new, ckpt_pathhouse_bp_bundle.pt): ckpt torch.load(ckpt_path, map_locationcpu) model HousePriceBP(ckpt[in_dim]) model.load_state_dict(ckpt[model_state]) model.eval() df_new df_new.copy() for c in cat_cols: df_new[c] df_new[c].fillna(未知).astype(str) df_new pd.get_dummies(df_new, columnscat_cols, drop_firstTrue) df_new df_new.reindex(columnsckpt[feature_columns], fill_value0) x ckpt[x_scaler].transform(df_new) with torch.no_grad(): pred_s model(torch.tensor(x, dtypetorch.float32)).numpy().reshape(-1, 1) pred ckpt[y_scaler].inverse_transform(pred_s).ravel() return predreindex(columns..., fill_value0)是推理链路里最关键的一步它保证新数据 one-hot 后的列和训练时完全一致。torch.no_grad()关闭梯度计算减少内存占用。inverse_transform把标准化后的预测值还原成原始房价顺序不能反。数值列的中位数填充建议在调用函数前完成或者把训练集的中位数也存进字典避免推理时重新计算。5.3 误差指标与残差复盘只看 R2 不够房价回归还要看 MAE、RMSE 和残差分布。MAE 直观RMSE 对大误差更敏感残差均值接近 0 说明没有系统性高估或低估残差绝对值的 P90 能看出尾部风险。下面的代码在测试集上做一次完整复盘。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import numpy as np pred_test predict_price(X_test_raw) # 原始特征表未标准化 mae mean_absolute_error(y_test, pred_test) rmse np.sqrt(mean_squared_error(y_test, pred_test)) r2 r2_score(y_test, pred_test) print({MAE: mae, RMSE: rmse, R2: r2}) residual y_test.values - pred_test print(残差均值:, residual.mean()) print(残差绝对值 P90:, np.percentile(np.abs(residual), 90))X_test_raw是未标准化的原始特征表predict_price内部会做标准化和列对齐。rmse用np.sqrt(mean_squared_error(...))计算避免旧版 sklearn 的squaredFalse兼容问题。残差均值接近 0 是基本要求如果明显偏正或偏负回去查标签标准化和反标准化是否配对。P90 过大说明模型对高价房源或特殊户型不稳需要补特征或分客群建模。6. 把预测做稳的进阶手法K 折验证、超参数搜索和一次固定复现6.1 K 折验证与超参数搜索的最小闭环单次切分的验证集受随机性影响大房价数据样本少时更明显。我一般会用 5 折交叉验证看模型稳定性再在小范围里搜隐藏层宽度和学习率。下面这张表是常用的搜索范围不要一次铺太大否则训练成本会失控。超参数候选值观察点隐藏层32/16、64/32、128/64验证 MAE 是否稳定下降学习率1e-4、5e-4、1e-3loss 是否震荡或停滞weight_decay1e-5、1e-4、1e-3验证集反弹是否缓解batch_size32、64、128小样本用 32 更稳交叉验证时每一折都要重新 fit 标准化器不能把全量数据的均值方差带进去。训练函数可以封装成train_bp(X_tr, y_tr, X_va, y_va)返回验证集 MAE 和最优权重。5 折的 MAE 均值和标准差一起看标准差大说明模型对切分敏感先把特征和正则稳住再谈加层。6.2 固定随机种子与一次完整复现BP 网络的初始化、DataLoader 的 shuffle、切分顺序都会影响结果。想让一次实验可复现至少固定 Python、NumPy、PyTorch 的随机种子。下面的代码放在训练脚本最前面每次跑之前执行一次。import random import numpy as np import torch seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 关掉 cudnn 的自动寻优减少非确定性 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsetorch.cuda.manual_seed_all只在有 GPU 时生效没有 GPU 也不会报错。cudnn.deterministicTrue会让部分卷积算法变慢但全连接网络影响不大benchmarkFalse减少自动算法选择带来的非确定性。切分、标准化、模型初始化都在固定种子之后做顺序不要乱。从那以后我每次接房价回归的 BP 神经网络代码都先把特征列、标准化器和随机种子固定成可保存本文还有配套的精品资源点击获取