简介这份资源面向机器学习入门者与需要完成回归预测实战的开发者围绕经典波士顿房价数据集用Python从零实现BP神经网络完成房价预测。压缩包共2个文件含1个py脚本与1个csv数据文件整体约8KB其中csv提供14维房屋特征与中位房价目标变量py脚本承载网络搭建与训练全流程。代码覆盖数据读取与预处理、输入层到输出层的结构定义、前向传播计算、以方差为损失函数的反向传播权重更新以及迭代训练与测试集评估等环节并预留新样本预测接口便于读者理解神经网络回归的完整链路。目前已有21735人学习下载适合希望脱离框架、亲手推导梯度与调参的读者对照实践也可作为课程作业与项目原型的参考实现。1. 房价预测的BP神经网络实现从一份 Excel 到能跑的回归模型很多人第一次接触房价预测的BP神经网络实现都是被一份 Excel 表格逼出来的几十列特征、几百到几千行样本领导或导师丢过来一句“用 Python 跑个预测”。真上手才发现难点根本不在“BP 神经网络”这四个字而在数据怎么清洗、特征怎么对齐、输出层怎么设计、归一化放在哪一步。房价预测本质是一个连续值回归问题不是分类所以最后一层不能用 softmax损失函数也不能用交叉熵这是新手最容易翻车的地方。这篇笔记面向两类人一类是刚学完反向传播公式、想找个真实场景练手的同学另一类是手头有房价数据、想快速搭一个能出结果的基线模型的工程师。我会用一份模拟的房价数据集把 BP 神经网络从数据预处理、网络搭建、训练调参到结果验证整条链路走一遍代码可以直接抄。中间会重点讲清楚几个参数为什么这么设以及我踩过的坑。整套方案不依赖任何特定平台本地 Python 环境就能复现。2. 房价预测的数据准备与特征工程先让数据能进网络BP 神经网络对输入数据的“脾气”很挑量纲不统一、缺失值不处理、类别特征不编码训练出来的模型要么不收敛要么收敛了但预测值离谱。所以在写网络之前得先把数据整理成网络能吃的格式。2.1 房价数据的典型字段与清洗思路一份常见的房价数据字段大致分三类数值型面积、房龄、楼层、房间数、类别型朝向、装修程度、是否有电梯、区域、以及目标值成交价或单价。数值型字段里面积和房龄的量纲差了几十倍直接喂进网络会让梯度更新被大数值特征主导小数值特征几乎不起作用。清洗顺序我一般这样排先去重再处理缺失值然后处理异常值最后做编码和归一化。缺失值处理要看缺失比例缺失超过 40% 的字段直接丢掉缺失较少的数值字段用中位数填充类别字段用众数填充。异常值用四分位距法IQR识别超出上下界的样本先标记不要急着删因为有些高价房是真实存在的删了反而让模型学不到高价区间。import pandas as pd import numpy as np # 读取模拟房价数据 df pd.read_csv(house_price_sim.csv) # 去重 df df.drop_duplicates() # 数值字段缺失用中位数填充 num_cols [area, age, floor, room_num, hall_num] for col in num_cols: df[col] df[col].fillna(df[col].median()) # 类别字段缺失用众数填充 cat_cols [orientation, decoration, has_elevator, region] for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0]) # IQR 标记异常值先不删只打印数量 for col in [area, price]: q1 df[col].quantile(0.25) q3 df[col].quantile(0.75) iqr q3 - q1 low q1 - 1.5 * iqr high q3 1.5 * iqr outlier_count ((df[col] low) | (df[col] high)).sum() print(f{col} 异常值数量: {outlier_count})这段代码的逻辑是先保证没有重复样本再分类型填充缺失最后用 IQR 只做识别不做删除。参数上中位数比均值抗异常值众数适合类别字段。IQR 的 1.5 倍是常规阈值如果数据本身分布很偏可以放宽到 3 倍但要在日志里记下来。2.2 类别特征编码与数值归一化的先后顺序类别特征不能直接变成数字因为“朝向南”和“朝向北”之间没有大小关系。常见做法是独热编码One-Hot把每个类别变成一个 0/1 列。如果类别取值很多比如区域有几十个独热后维度会爆炸这时候可以用目标编码或嵌入但基线模型先用独热就够了。归一化必须放在训练集和测试集划分之后用训练集的均值和标准差去变换测试集否则测试集的信息会泄漏到训练过程里评估结果会虚高。这是很多人容易忽略的一步。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 独热编码类别字段 df pd.get_dummies(df, columnscat_cols, drop_firstTrue) # 分离特征和目标 X df.drop(columns[price]) y df[price].values.reshape(-1, 1) # 先划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 归一化用训练集拟合测试集只做变换 scaler_X StandardScaler() X_train scaler_X.fit_transform(X_train) X_test scaler_X.transform(X_test) scaler_y StandardScaler() y_train scaler_y.fit_transform(y_train) y_test scaler_y.transform(y_test)这里对特征和目标都做了标准化。目标值标准化是为了让损失函数数值稳定训练完预测时再用scaler_y.inverse_transform还原成真实房价。drop_firstTrue是为了避免独热编码的共线性少一列不影响信息量。random_state42保证每次划分一致方便复现。提示归一化只对数值型特征做独热后的 0/1 列不需要再标准化否则会破坏稀疏性。3. 用 PyTorch 搭一个能收敛的 BP 回归网络数据准备好之后网络结构本身其实不复杂。房价预测的 BP 网络通常是全连接结构输入维度等于特征数中间若干隐藏层输出层一个神经元对应房价。真正决定能不能收敛的是隐藏层宽度、激活函数、学习率和损失函数这几个选择。3.1 网络层数、隐藏单元与激活函数的选择输入维度由特征数决定这个不用手动设。隐藏层我一般从两层起步每层 64 或 128 个神经元。层数不是越多越好房价数据样本量通常不大层数太深容易过拟合而且梯度在反向传播中会衰减。激活函数隐藏层用 ReLU它计算简单、不容易饱和输出层不能用 ReLU因为房价可能是任意实数输出层直接线性输出。损失函数用均方误差MSELoss这是回归任务的标准选择。优化器用 Adam学习率从 0.001 开始试。如果训练损失震荡降到 0.0005如果下降太慢升到 0.005但不要超过 0.01否则容易跳过最优解。import torch import torch.nn as nn class HousePriceBPNet(nn.Module): def __init__(self, input_dim): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 1) # 输出层线性不加激活 ) def forward(self, x): return self.net(x) input_dim X_train.shape[1] model HousePriceBPNet(input_dim) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001)网络定义里nn.Linear(64, 1)是输出层输出一个标量。nn.Sequential把层按顺序串起来前向传播自动执行。参数上128 和 64 是隐藏单元数如果特征数超过 200第一层可以加到 256如果样本少于 500第一层降到 64 更稳。3.2 训练循环、批次大小与早停策略训练循环要处理三件事把 numpy 数据转成 tensor、按批次喂数据、每个 epoch 记录训练和验证损失。批次大小一般设 32 或 64样本少就用 16。早停策略是防止过拟合的关键如果验证损失连续 20 个 epoch 不下降就停止训练并保留验证损失最低的模型参数。from torch.utils.data import TensorDataset, DataLoader # 转成 tensor X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.float32) train_ds TensorDataset(X_train_t, y_train_t) train_loader DataLoader(train_ds, batch_size32, shuffleTrue) best_val_loss float(inf) patience 20 wait 0 best_state None for epoch in range(500): model.train() for xb, yb in train_loader: pred model(xb) loss criterion(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): val_pred model(X_test_t) val_loss criterion(val_pred, y_test_t).item() if val_loss best_val_loss: best_val_loss val_loss best_state model.state_dict() wait 0 else: wait 1 if wait patience: print(f早停于 epoch {epoch}) break model.load_state_dict(best_state)训练循环里optimizer.zero_grad()必须在loss.backward()之前调用否则梯度会累加。model.train()和model.eval()切换训练和评估模式虽然这个网络没有 Dropout 和 BatchNorm但养成习惯没坏处。早停的patience20是经验值数据噪声大可以调到 30数据干净可以降到 10。3.3 预测结果反归一化与评估指标计算训练完的模型输出是标准化后的房价必须用scaler_y.inverse_transform还原。评估指标用 MAE、RMSE 和 R²。MAE 反映平均绝对误差RMSE 对大误差更敏感R² 反映模型解释了多少方差。三个指标一起看才能判断模型是整体偏差还是个别样本预测离谱。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model.eval() with torch.no_grad(): pred_scaled model(X_test_t).numpy() # 反归一化 pred_price scaler_y.inverse_transform(pred_scaled) true_price scaler_y.inverse_transform(y_test_t.numpy()) mae mean_absolute_error(true_price, pred_price) rmse np.sqrt(mean_squared_error(true_price, pred_price)) r2 r2_score(true_price, pred_price) print(fMAE: {mae:.2f}) print(fRMSE: {rmse:.2f}) print(fR2: {r2:.4f})反归一化这一步不能省否则算出来的 MAE 是标准化空间的没有业务意义。R² 在 0.8 以上说明模型对房价的解释力不错低于 0.6 就要回头检查特征工程或网络结构。如果 RMSE 远大于 MAE说明存在个别预测严重偏离的样本需要单独看这些样本的特征。4. 房价预测 BP 网络的避坑与排查清单这一章是我自己踩过的坑按“现象 → 原因 → 解决”整理。每一条都对应一个具体的失败场景照着排查能省不少时间。4.1 损失不下降或变成 NaN现象训练几个 epoch 后损失变成 NaN或者一直停在某个值不动。原因最常见的是学习率太大导致梯度爆炸其次是输入数据没有归一化数值大的特征把梯度带飞还有一种情况是目标值没有标准化MSELoss 算出来的数值过大。解决先把学习率降到 0.0001 试一轮如果还 NaN检查输入数据是否有 inf 或 nan。用np.isfinite(X_train).all()确认。目标值标准化一定要做这一步省不得。4.2 训练损失低但测试损失高现象训练集 MAE 很小测试集 MAE 是训练集的两三倍。原因过拟合。房价数据样本少、特征多的时候特别容易发生。网络把训练样本背下来了但没有学到泛化规律。解决先加早停再考虑减小网络宽度比如把 128 降到 64。还可以加 Dropout 层nn.Dropout(0.2)放在两个隐藏层之间。如果数据量允许做 K 折交叉验证比单次划分更可靠。4.3 预测值全部集中在一个窄区间现象不管输入什么特征模型输出的房价都差不多方差很小。原因网络欠拟合或者输出层被错误地加了激活函数。如果输出层用了 ReLU负值会被截断成 0如果用了 Sigmoid输出会被压缩到 0 到 1 之间。解决确认输出层是nn.Linear且后面没有激活函数。检查隐藏层宽度是否太小输入特征是否被归一化过度导致信息丢失。可以先把网络调宽一层看预测方差是否变大。4.4 类别特征独热后维度爆炸现象独热编码后特征数从几十变成几百训练变慢内存吃紧。原因某个类别字段取值太多比如区域字段有上百个不同值。解决对高基数类别字段改用目标编码用目标均值替换类别或者把低频类别合并成“其他”。目标编码要在训练集上计算再应用到测试集避免泄漏。合并阈值一般设 5%出现次数少于 5% 的类别归为一类。4.5 反归一化后房价出现负数现象预测出来的房价有负数明显不合理。原因目标值标准化后模型输出的是标准分数反归一化时如果scaler_y的均值太小或标准差太大极端预测值可能变成负数。解决在反归一化后加一个下限截断pred_price np.maximum(pred_price, 0)。但更根本的是检查模型是否欠拟合负数通常出现在模型没学好、预测值偏离均值很远的时候。5. 让房价预测 BP 网络更稳的几个进阶技巧基线模型跑通之后如果想把 R² 从 0.8 推到 0.9或者想让模型在不同数据集上更稳可以试几个进阶做法。这些不是必须的但在我自己的项目里确实有效。第一个技巧是特征交叉。房价和“面积 × 区域”这种组合特征关系很大单纯把面积和区域分开喂给网络网络需要自己学交互项样本少的时候学不好。手动构造几个交叉特征比如“面积/房间数”“房龄 × 装修程度”能明显提升效果。构造完记得重新做归一化。第二个技巧是学习率调度。固定学习率在后期容易在最优解附近震荡用torch.optim.lr_scheduler.ReduceLROnPlateau在验证损失不下降时自动降学习率比手动调更省心。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience10 ) # 在验证损失计算后调用 scheduler.step(val_loss)factor0.5表示每次降一半patience10表示验证损失 10 个 epoch 不降才触发。这个调度器要和早停配合早停负责停调度器负责在停之前再抢救一下。第三个技巧是集成。训练 3 到 5 个不同随机种子的网络预测结果取平均。单个网络的方差会被平均掉MAE 通常能降 5% 到 10%。代价是训练时间翻倍但在房价这种样本量不大的场景里完全可接受。技巧预期收益代价特征交叉R² 提升 0.02 到 0.05需要领域知识构造学习率调度训练更稳后期不震荡多一个超参数多模型集成MAE 降 5% 到 10%训练时间成倍增加最后说一个验证方法不要只看一次划分的测试集结果。用 K 折交叉验证把数据分成 5 份轮流做测试集看 R² 的均值和标准差。如果标准差很大说明模型对数据划分敏感这时候集成和早停就更重要。我自己做房价预测时最大的教训是太早去调网络结构忽略了数据本身。后来发现把缺失值处理干净、把异常值单独分析、把类别编码做对比换激活函数、加层数有用得多。现在我的习惯是先把数据管线和评估指标搭好再动网络。希望帮到你。本文还有配套的精品资源点击获取