资讯详情 BP神经网络房价预测实战:从反向传播到Python实现
📅 2026/10/12 17:31:16
简介这是一份基于BP神经网络实现房价预测的Python代码资源面向机器学习初学者以及需要回归预测实战的开发者。压缩包共2个文件包含一个Python脚本和一份CSV格式的波士顿房价数据集数据涵盖犯罪率、房间数等14个特征目标变量为中位房价整体仅8KB轻量易取。代码完整实现了神经网络的前向传播、反向传播以及基于方差的损失函数并涵盖了数据读取、缺失值处理、标准化、网络权重初始化、迭代训练与测试集评估等关键步骤网络结构包含输入层、隐藏层和输出层权重随机初始化方便调参实验训练完成后还会计算均方误差、决定系数等指标方便直观判断模型效果。可直接运行注释简洁便于逐行理解也可作为课程设计或论文实验的参考基线。目前已有21728人学习下载适合用最短时间理解BP回归模型的完整Python实现。1. 房价预测的BP神经网络为什么线性回归不够用在房价预测这类表格回归任务上很多人上来先把 LinearRegression 跑一遍验证集 R2 卡在 0.6 左右就上不去了。真实房价和区里收入中位数、房龄、房间数的关系不是一条直线线性模型只能抓主趋势抓不住里头的非线性交互换成 BP 神经网络用同一套特征通常能把 R2 推到 0.75 以上。这篇笔记就来写怎么用 Python 把 BP 神经网络实现出来用 scikit-learn 自带的加州房价数据集从数据预处理到模型训练和评估每一步都给可运行的代码。适合两类人刚学完 Python 基础、想拿真实回归任务练手的初学者以及被业务方问“神经网络到底能不能预测房价”的工程师。跑完这份代码你会明白网络结构怎么搭、超参数改哪里以及哪些坑会让训练直接翻车。2. 拆解BP神经网络前向传播、反向传播与结构选型2.1 前向传播与反向传播模型学习的两个方向BP 神经网络的“BP”全称是 Back Propagation反向传播。整条训练路径分两段前向传播用当前权重把输入特征一点点算到输出值反向传播用预测值和真实值的差距去更新每一层权重。你见过的那种 bp 神经网络结构图信息流都是从左往右输入层的神经元数等于特征数经过一个或几个隐藏层最后压到输出层。对房价预测来说输入是区收入中位数、房龄、房间数这些特征输出就是预测的中位房价。前向传播做两件事加权求和再加激活。每个神经元把上一层传来的数值乘以自己的权重、求和、加偏置然后过一个激活函数。ReLU 这类激活函数负责引入非线性没有它多层网络叠在一起等价于一层线性回归。反向传播则是把损失函数对输出层的误差求梯度再用链式法则回传到每一层的权重上让每个权重沿着梯度相反的方向更新一小步。更新步长就是学习率 learning_rate后面代码里的 Adam 优化器就是在干这件事。给你一个具体画面假设某个街区收入中位数是 3.5万美元、房龄 20 年、平均房间数 5这些数字进入网络后每一层都会算出一个加权和再经过 ReLU 变成非负数最后在第 2 层隐藏层末尾被压缩成一个预测价格。第一次预测大概率不准比如真实价格是 2.3模型给出 1.8这个差值就是损失。反向传播从差值出发逐层倒推每个权重“该付多大责任”然后按责任大小去修改它。很多开发者也把 BP 当成黑匣子用直接调库不碰公式。我建议至少想明白两件事输出层激活函数必须是线性回归任务损失函数必须跟业务指标对得上。想明白这两点排查问题时你至少有方向不会瞎改超参。2.2 隐藏层数量和神经元数房价预测该搭几层网络结构有确定的只有两端输入层 8 个神经元加州房价数据集 8 个特征输出层 1 个神经元而且不加 sigmoid、不加 ReLU直接用线性激活。中间隐藏层才是要试的部分。我的常见做法是从两个隐藏层起步神经元数 64 接 32从大到小递减。之所以递减是因为前一层在抓特征组合后一层在压缩抽象结果参数总量少、训练更快从下往上反着加也不是不行但收益不明显还容易过拟合。隐藏层神经元数没有绝对公式网上能搜到一些经验法则比如取输入层和输出层神经元数乘积的平方根实际上多少带点玄学。落地时我更倾向用手头的验证集去试先固定 6432看 loss 曲线还能不能继续下降如果训练集 R2 高而验证集差说明容量太大改成 3216 并且把 Dropout 调高如果两边都欠拟合就往 12864 走。对于只有两万条样本的表格数据三层以上的隐藏层通常没必要深层网络需要更多样本喂小数据上不会比浅层好。如果你的业务特征不是 8 个而是几十个神经元数量从 128 起步也不奇怪特征多的时候我会先用 PCA 做一次降维把有效维度压到 20 以内再喂 BP常见做法能省一半训练时间。位置特征要提醒一句很多人觉得经纬度没有业务含义想删掉但对房价来说位置几乎是定价核心BP 能从经纬度组合里学到区域溢价别急着砍。2.3 激活函数、损失函数与优化器的选型激活函数我默认 ReLU因为它在正区间的导数是常数 1梯度消失问题比 sigmoid 轻得多收敛快。如果你发现一部分神经元训着训着输出一直为 0也就是“dead ReLU”可以把隐藏层换成 LeakyReLU给负区间留一点小斜率让神经元有机会自己“复活”。损失函数有三种常见选择。MSE均方误差最常用它对大误差惩罚更狠会让模型更努力去拟合那些贵得离谱的豪宅MAE平均绝对误差对所有样本一视同仁业务解释最直接——平均差几万美元Huber 是两者的折中误差小的时候像 MSE误差大的时候像 MAE对离群值更稳。我喜欢在房价预测这类数据上先选 MAE 训练模型的评估值直接就按万美元解释业务方也听得懂。优化器直接用 Adam它能根据梯度的一阶矩和二阶矩自动调整每个参数的学习率表格数据上比纯 SGD 少操心。剩下要手动管的只有初始学习率一般从 1e-3 开始loss 震荡就降到 1e-4loss 降得慢就试 2e-3。这套选型不是唯一答案但够稳定适合作为第一个能跑的基线。你可能会问表格数据为什么不直接上 XGBoost后面第 6 章会做对比但 BP 的价值在于它的特征非线性组合是学习出来的加早停、Dropout、残差分析这些工程手段时更顺手这是两类模型的核心差异。3. 数据准备从加州房价数据集到归一化与划分3.1 数据集选型为什么用加州房价数据集动手前先选数据。早期教学都喜欢用波士顿房价数据集但 scikit-learn 较新版本出于授权与合规原因移除了它所以现在复现用得最多的是 fetch_california_housing。这个数据集包含加利福尼亚州约 20640 个街区的聚合统计8 个特征目标值是街区中位房价单位是十万美元。它不需要下载任何文件scikit-learn 自带运行环境稳定非常适合复现。环境上我建议用 conda 建独立环境避免和现有项目互相污染conda create -n bp python3.10 -y conda activate bp pip install numpy pandas scikit-learn matplotlib tensorflow代码加载数据的部分长这样import numpy as np from sklearn.datasets import fetch_california_housing housing fetch_california_housing() X housing.data y housing.target print(特征名:, housing.feature_names) print(样本量:, X.shape) print(目标范围:, round(float(min(y)), 3), -, round(float(max(y)), 3))特征依次是收入中位数、房龄、平均房间数、平均卧室数、区块人口、平均入住人数、纬度、经度。目标值范围大概是 0.15 到 5.0对应 15 万到 500 万美元。这里有个容易忽略的细节这是“街区级中位房价”不是一套一套的挂牌数据所以模型学的是区块特征到区块价格的关系。如果环境里还没有 scikit-learn直接执行pip install numpy scikit-learn就能补上这也是很多 Python 入门教程里反复提到的装库步骤。VSCode 里建好环境后左下角选择这个解释器就能开始写.py 或 .ipynb 都行。3.2 归一化为什么不能跳过BP 神经网络的训练依赖梯度下降特征量纲不一致会直接影响收敛。拿这份数据处理收入中位数大概在 2 到 8 之间人口却能到几千上万没归一化之前大数值特征算出来的加权和会被人口项主导梯度更新也被它带着跑其它特征学得很慢。这跟线性回归里系数缩放是同一个问题但在多层 BP 里被放得更大。归一化我基本用 StandardScaler把每个特征转成均值 0、方差 1。为什么不用 MinMaxScaler因为 MinMax 受最大值影响很大这数据里人口、房间数都有长尾极值MinMax 会把大部分样本压到零点几的区间里反而丢失区分度。StandardScaler 用均值和标准差做中心化对离群点稳健得多。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 先划分再做归一化顺序不能反 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) # 训练集用 fit_transform X_test_scaled scaler.transform(X_test) # 测试集只用 transform目标值 y 在这份代码里不做归一化。因为 y 的范围只有 0.15 到 5量级和特征归一化后的区间接近而且不做标定最后输出的 MAE、RMSE 能直接按万美元换算业务上好解释。如果换一个跨度特别大的数据集比如二手房标价从几百万到几亿我会对 y 做 log1p 变换训练完再用 expm1 换回原尺度不然模型会被极少数天价房带偏。注意先划分再归一化顺序不能反。先对全量数据 fit_transform测试集信息就泄露给 scaler 了评估结果会虚高。3.3 数据划分边界随机划分与特征相关性检查上面代码里有两个点值得展开。第一个点是 train_test_split 的 random_state42随机划分的随机性来自这个种子固定它之后每次跑的划分方式一样复现才有意义。第二个点是只对训练集 fit_transform测试集只 transform绝不能对全量数据先归一化再划分。如果先对全数据做 scaler.fit_transform测试集的均值方差就已经泄露给 scaler 了模型评估结果会虚高这个叫数据泄露是在“先归一化还是先划分”这个顺序上最容易翻车的坑。回归任务的划分我一般直接随机划分不用分层抽样因为 y 是连续值但如果你的数据是按时间采集的比如按月份汇总的房价记录那必须改成按时间切分用前半段时间训练、后半段测试否则模型偷偷看到“未来数据”上线后直接失灵。特征相关性检查有两个作用一个是快速了解哪些特征对目标有区分度另一个是提前发现冗余特征。简单做import pandas as pd df pd.DataFrame(X, columnshousing.feature_names) df[MedHouseVal] y print(df.corr()[MedHouseVal].sort_values(ascendingFalse))这份数据上收入中位数和房价相关性最高其它特征相关性不高。但注意相关性只捕捉线性关系BP 能学非线性交互所以相关性低不等于特征没用。我一般用这份结果做排序参考而不是直接砍特征。尤其经纬度两个特征单看相关性可能不起眼但组合起来能表达“靠海”“市中心”这类强位置信号放进网络让 BP 自己学就好。4. Python代码实现一套能直接运行的BP网络训练脚本4.1 主脚本从数据到模型的完整链路把前面几步串起来再加模型和训练就是一个能直接运行的脚本。保存为 bp_housing.py命令行执行python bp_housing.py即可。核心代码如下import numpy as np import matplotlib.pyplot as plt import tensorflow as tf from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping # 固定随机种子 tf.random.set_seed(42) np.random.seed(42) # 数据加载与划分 housing fetch_california_housing() X_train, X_test, y_train, y_test train_test_split( housing.data, housing.target, test_size0.2, random_state42 ) # 标准化 scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) # 模型结构 model Sequential([ Dense(64, activationrelu, input_shape(X_train.shape[1],)), Dropout(0.2), Dense(32, activationrelu), Dense(1, activationlinear), # 回归任务线性输出 ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossmae, metrics[mse, mae], ) # 早停 early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) # 训练 history model.fit( X_train_s, y_train, validation_split0.2, batch_size32, epochs200, callbacks[early_stop], verbose1, ) # 训练曲线 plt.plot(history.history[loss], labeltrain) plt.plot(history.history[val_loss], labelval) plt.xlabel(epoch) plt.ylabel(MAE) plt.locator_params(axisx, nbins10) # 横坐标刻度太密时用 plt.legend() plt.show() # 评估 y_pred model.predict(X_test_s).flatten() print(MAE: %.3f % mean_absolute_error(y_test, y_pred)) print(RMSE: %.3f % np.sqrt(mean_squared_error(y_test, y_pred))) print(R2: %.3f % r2_score(y_test, y_pred))从代码顺序说几个关键点。seed 要放在所有随机操作之前包括 TensorFlow 内部的权重初始化和训练时的 shuffle不固定的话你每次跑的损失曲线都不一样也就没法判断是改动有效还是随机波动。train_test_split 之后做 StandardScaler就是第 3 章强调的顺序防数据泄露。模型结构就三层64 个 ReLU 神经元加 Dropout再接 32 个 ReLU 神经元最后线性输出 1 个值。Dropout 层在训练时随机丢弃 20% 的神经元让网络不过分依赖某几个权重相当于轻量正则。输出层用 linear 激活而不是 sigmoid逻辑很简单标定目标范围是 0.15 到 5sigmoid 把输出压到 0 到 1 之间模型连逼近目标都做不到。损失函数选了 MAE 而不是 MSE训练过程里打印的 loss 就是“平均差几万美元”。validation_split0.2 不是再动 X_test而是从训练集里切 20% 做验证早停时就盯这个 val_loss。patience10 的意思是连续 10 轮验证 loss 不下降就停。restore_best_weightsTrue 是后悔药自动把权重回滚到验证 loss 最低那一轮而不是训练结束时可能已经过拟合的最后状态。在这份数据上一个没怎么调参的 BP 网络跑下来R2 通常在 0.7 到 0.8 之间MAE 在 0.5 附近也就是预测中位房价平均偏差约 5 万美元。如果你的结果差很多优先检查是不是漏了标准化或者 learning_rate 不在合理区间。4.2 超参数怎么调learning_rate、batch_size 与 epochs 的配合网上很多 BP 代码把超参写死在 fit 里换数据就失效。我习惯把常改的几个参数收拢成一张表每次只动一个变量记录它对验证集的影响。参数默认值调整方向与影响learning_rate1e-3太大 loss 震荡甚至 NaN太小收敛极慢调整建议按 ×0.1 或 ×2 走batch_size32小 batch 梯度噪声大但泛化稳大 batch 训练快但可能跑进尖锐极小值16-128 之间试epochs200配早停使用不必手动纠结具体数值Dropout0.2验证集比训练集差很多时上调到 0.3-0.5两边都差时下调隐藏层6432欠拟合试 12864过拟合试 3216learning_rate 是第一个要调的。从 1e-3 开始如果损失曲线来回震荡、降不下去直接降一半到 5e-4如果曲线平得让人睡着可以提一点到 2e-3。新手阶段不建议自己写学习率调度器先用早停兜底跑稳以后再考虑 ExponentialDecay 或 ReduceLROnPlateau。epochs 和 early stopping 是配合关系固定 epochs200实际跑多少轮由早停决定。你会在输出里看到Epoch 0012: early stopping而不是非得等 200 轮跑完这是省时间的关键。训练曲线上的 val_loss 如果先降后升就是过拟合的典型信号此时先调 Dropout 和隐藏层容量别急着加数据。画曲线时遇到横坐标太密集、刻度标签挤成一团是因为 epochs 多时 matplotlib 默认把每个刻度都标出来几十个数字挤在一起谁也看不清。用plt.locator_params(axisx, nbins10)让它只显示大约 10 个刻度这是我在项目里常被问到的小技巧。记住最后一轮的 loss 数值意义不大early stopping 已经帮你把权重回滚到了最佳位置看回滚前的曲线段更有参考价值。5. BP神经网络训练避坑五个高频问题排查5.1 训练loss不降甚至出现NaN现象训练开始几轮后 loss 停在某个值不动或者直接变成 nan损失曲线图上出现断崖。原因最常见是 learning_rate 太大Adam 也救不回来梯度更新一步跨过最优点导致震荡其次是特征没归一化大尺度特征让某些梯度特别大最后是 ReLU 死亡负区间梯度恒为 0神经元永远不更新。解决先把 learning_rate 降到 1e-4 重跑一遍能恢复就说明是学习率问题。再确认 scaler 有没有对全部特征做漏掉一列都会带偏。ReLU 死亡的话把激活函数换成 LeakyReLU它给负区间留了 0.01 左右的斜率神经元能自己复活。看训练日志nan 一般出现在前 20 轮越早出现越倾向学习率问题。5.2 训练集R2接近0.9测试集直接腰斩现象训练集上模型几乎完美拟合验证集或测试集指标差很多典型过拟合。原因BP 网络参数量远大于样本量能支撑的量。以 6432 两层隐藏层为例参数量超过 2500训练集只有 1.6 万条网络完全有能力把训练集每个点背下来。解决先开 Dropout从 0.3 试起再砍隐藏层容量6432 改 3216最后检查早停的 patience 是否太短、restore_best_weights 是否没开。L2 正则也叫权重衰减效果和 Dropout 互补但先别两个一起上一次只加一个你能分清是谁起的作用。还有一个隐蔽原因如果之前对 y 做了归一化但预测后忘了 inverse_transform评估时数字全部缩小MAE 算出来只有 0.05 但业务完全对不上先检查这个再动模型。5.3 预测出负房价和明显不合理的值现象预测结果里有负的“房价”或预测值全部挤在一个很窄的区间里。原因输出层的激活函数选错了。回归任务如果习惯性复用分类网络里的 sigmoid输出被压在 0 到 1 之间用 ReLU 输出又只能得到非负值但范围受限。另一个原因是测试集里出现训练集没见过的极端组合模型只能外推外推出负值不奇怪。解决输出层固定用 linear 激活。业务上如果确实不想看到负值在预测后做一次兜底y_pred_clipped np.clip(y_pred, 0, None)这属于业务规则不是模型结构修改。“预测挤在窄区间”通常是隐藏层容量太小或 loss 曲线没收敛加大容量并让早停多跑一会再看看。5.4 同一份代码每次跑结果不一样复现不了现象固定了 random_state42但重复执行两次脚本R2 和 loss 曲线还是有差别。原因scikit-learn 的 train_test_split 只固定了数据划分TensorFlow 内部初始化、shuffle 用的随机数没有被完全控制。有些人只固定 np.random.seed以为够了其实 tf 有自己的随机状态。解决在代码最开头同时固定两个地方np.random.seed(42)和tf.random.set_seed(42)并且必须在 import 之后、任何模型构造之前执行。如果还不行把tf.keras.utils.set_random_seed(42)也调一遍。最后跑出来还有细微波动是正常的量级应该在 0.01 以内超过这个范围说明环境或依赖版本有差异。seed 这种代码建议留在脚本顶部别写在函数里避免变成全局状态污染别的地方。5.5 Windows下代码没跑起来提示找不到msvcp140.dll现象import tensorflow 那一行直接报错提示“由于找不到 msvcp140.dll 无法继续执行代码”代码一行都没跑。原因TensorFlow 在 Windows 上依赖 Microsoft Visual C Redistributable 运行库缺了就会在导入阶段报 dll 错误。这不是代码逻辑问题而是环境缺件。第一次接触 TensorFlow 的人特别容易在这里卡住报错信息又长又吓人。解决去微软官网下载 vc_redist.x64.exe 安装后重开命令行。另一个更省事的选择是用 conda 创建环境conda 会连带把常用运行库装好出错概率小很多。如果你用的是 Apple Silicon Mac装 tensorflow-macos 而不是通用版环境差异是这类 dll 错误的另一面。6. 评估进阶残差分析、指标取舍与下一步R2 能说明模型解释了多少方差但它对异常值敏感单看 R2 容易被漂亮数字骗。我评估回归模型的三件套是 MAE、RMSE 和残差图。MAE 衡量平均误差单位直接RMSE 对大误差更敏感帮你看有没有极端糟糕的预测残差图才是发现问题的地方。fig, axes plt.subplots(1, 2, figsize(10, 4)) axes[0].scatter(y_test, y_pred, s8, alpha0.5) axes[0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--) axes[0].set_xlabel(真实房价万美元) axes[0].set_ylabel(预测房价万美元) residual y_test - y_pred axes[1].hist(residual, bins30, alpha0.7) axes[1].set_xlabel(残差真实-预测) plt.show()左图如果点云明显偏离 yx 对角线说明存在系统性高估或低估右图残差直方图如果偏态明显比如右边拖长尾巴说明高价房被大量低估。这个判断直接影响要不要对 y 做 log 变换或者换 Huber 损失比一味加深网络管用。如果你要冲刺更高精度常见做法是拿 XGBoost 和 BP 对比一遍。在表格类回归里树模型往往把 BP 打得没脾气但你大概率会搜到一堆 xgboost 代码它们跑得快、调参风格完全不同。我不会说 BP 一定输但会把 BP 当基线把 XGBoost 当对手两者都跑再做结果集成。最后说一条我的习惯先改一个超参数跑一遍看曲线和残差再决定下一步一次动两个以上的参数等于没动。之前我为刷 R2 把网络加深、Dropout 调小训练集做到 0.97验证集 0.88一测残差发现高价房全部低估才知道是长尾没处理好。BP 网络解决房价预测不是唯一答案但在 Python 里足够稳定、好复现值得作为你的第一个回归基线。希望帮到你。本文还有配套的精品资源点击获取