简介:这份资源面向机器学习入门者与需要完成回归预测实战的开发者,围绕经典波士顿房价数据集,用Python从零实现BP神经网络完成房价预测。压缩包共2个文件,包含1个py脚本与1个csv数据文件,整体约8KB,其中csv提供14维房屋特征与中位房价目标变量,py脚本承载网络搭建与训练全流程。代码覆盖数据读取与预处理、输入层到隐藏层再到输出层的前向传播、以方差为损失函数的误差计算,以及基于梯度下降的反向传播权重更新,并设置迭代训练与测试集评估环节,可输出均方误差、决定系数等指标,也支持输入新特征进行价格预测。已有21735人学习下载,适合希望理解神经网络底层原理、亲手推导前向与后向传播细节的读者,可作为课程作业或入门项目的参考实现。
1. 房价预测的BP神经网络实现:从一份Excel到能跑的Python代码
手里有一份带标签的房价数据表,想用BP神经网络跑出预测值,这是很多做数据分析的工程师都会碰到的场景。标题里的“房价预测的BP神经网络实现_python代码”,核心就三件事:把房价这个连续值回归问题建模成神经网络、用反向传播把误差压下去、用Python把整条链路跑通。它适合两类人:一类是刚学完神经网络理论、想找一个完整回归案例练手的开发者;另一类是在业务侧需要快速搭一个房价估价基线、不追求SOTA但要求可解释可复现的工程师。我见过太多人卡在第一步——数据里混着“120万”“3室2厅”这种字符串,直接喂给模型就是一场翻车。这篇笔记按“数据怎么洗、网络怎么搭、参数怎么调、坑在哪”的顺序讲,代码可以直接抄,参数可以按你的数据规模改。
2. 房价数据预处理:把脏表格变成网络能吃的浮点矩阵
BP神经网络只认数值,房价数据里最常见的字段是面积、房间数、楼层、地段评分、成交价。原始表里往往有缺失值、量纲差异巨大(面积100多,房间数3),还有“满五唯一”这种文本标签。不做处理直接训练,结果就是损失不收敛或者收敛到一个荒谬的值。
2.1 缺失值与异常值的处理顺序
先处理异常值再补缺失,顺序反了会把异常值当成正常分布去填充。我一般用IQR(四分位距)法标记异常,再决定是截断还是删除。房价数据里“面积=0”或“单价=99999”这种明显是录入错误,直接删比补更安全。
import pandas as pd import numpy as np # 读取原始数据,假设列名为中文 df = pd.read_excel("house_raw.xlsx") # 1. 删除面积或单价为0或负数的记录 df = df[(df["面积"] > 0) & (df["单价"] > 0)] # 2. 用IQR标记面积异常值 Q1 = df["面积"].quantile(0.25) Q3 = df["面积"].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR # 超出边界的先打印看看数量,再决定删还是截断 outliers = df[(df["面积"] < lower) | (df["面积"] > upper)] print(f"面积异常值数量: {len(outliers)}") df = df[(df["面积"] >= lower) & (df["面积"] <= upper)] # 3. 数值列缺失用中位数填充,类别列缺失用众数填充 num_cols = ["面积", "房间数", "楼层", "地段评分"] cat_cols = ["装修情况", "是否有电梯"] for col in num_cols: df[col] = df[col].fillna(df[col].median()) for col in cat_cols: df[col] = df[col].fillna(df[col].mode()[0])这段代码的逻辑是先保证数值合理性,再处理缺失。IQR的1.5倍是经典阈值,如果你的数据分布偏态严重,可以放宽到3倍。中位数填充比均值填充更抗异常值,这是血泪经验——曾经用均值填充面积,结果一套5000平的别墅把整体均值拉高,模型对普通住宅的预测全部偏高。
2.2 类别特征编码与量纲统一
“装修情况”这种文本列,用独热编码(One-Hot)比标签编码更安全,因为装修等级之间没有大小关系。量纲统一用StandardScaler,把每列变成均值0方差1。注意:目标值(房价)也要缩放,否则梯度爆炸。
from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer # 独热编码类别列,标准化数值列 preprocessor = ColumnTransformer( transformers=[ ("num", StandardScaler(), num_cols), ("cat", OneHotEncoder(sparse_output=False), cat_cols) ]) # 分离特征和目标 X = df[num_cols + cat_cols] y = df["成交价"].values.reshape(-1, 1) # 先划分训练测试集再fit,防止数据泄露 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) X_train_processed = preprocessor.fit_transform(X_train) X_test_processed = preprocessor.transform(X_test) # 目标值单独标准化 y_scaler = StandardScaler() y_train_scaled = y_scaler.fit_transform(y_train) y_test_scaled = y_scaler.transform(y_test)这里的关键参数是sparse_output=False,因为后续要转成PyTorch张量,稀疏矩阵不支持。random_state=42是为了复现,你换成别的数字结果会不同但趋势一致。目标值标准化后,预测时要用y_scaler.inverse_transform还原回真实房价,这一步漏了的话输出就是标准分数,完全没法解释。
3. 用PyTorch搭一个能收敛的BP网络:层数、激活、损失函数怎么选
BP神经网络做回归,结构不需要太深。房价预测的特征维度通常几十到几百,两层隐藏层足够。我见过有人堆到10层,结果梯度消失,损失卡在0.5下不去。这一章把网络定义、训练循环、早停策略拆开讲。
3.1 网络结构定义与初始化
输入维度等于预处理后的特征数,输出维度是1(房价)。隐藏层神经元数量按“输入维度×2/3”起步,再微调。激活函数用ReLU,输出层不加激活(回归任务)。权重初始化用Kaiming,比默认的均匀分布收敛快。
import torch 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) # 回归输出,不加激活 ) # 手动Kaiming初始化 for m in self.net: if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, nonlinearity="relu") nn.init.zeros_(m.bias) def forward(self, x): return self.net(x) input_dim = X_train_processed.shape[1] model = HousePriceNet(input_dim) print(f"输入维度: {input_dim}, 参数量: {sum(p.numel() for p in model.parameters())}")kaiming_normal_的nonlinearity="relu"要和实际激活函数一致,用错初始化方式会让前几轮损失震荡。参数量打印出来心里有数,如果超过训练样本数的十分之一,就要考虑减层或加正则。
3.2 训练循环与早停策略
损失函数用MSELoss,优化器用Adam,学习率1e-3起步。早停的patience设10到20,监控验证集损失。房价数据噪声大,训练集损失一直降但验证集反弹是常态,早停能省下大量无效训练时间。
from torch.utils.data import TensorDataset, DataLoader # 转张量 X_train_t = torch.tensor(X_train_processed, dtype=torch.float32) y_train_t = torch.tensor(y_train_scaled, dtype=torch.float32) X_test_t = torch.tensor(X_test_processed, dtype=torch.float32) y_test_t = torch.tensor(y_test_scaled, dtype=torch.float32) train_ds = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) best_val_loss = float("inf") patience = 15 counter = 0 for epoch in range(500): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) 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 torch.save(model.state_dict(), "best_model.pth") counter = 0 else: counter += 1 if counter >= patience: print(f"早停于第{epoch}轮, 最佳验证损失: {best_val_loss:.4f}") break if epoch % 50 == 0: print(f"Epoch {epoch}, Val Loss: {val_loss:.4f}")weight_decay=1e-5是L2正则,防止过拟合。batch_size=32适合几千条样本,样本量过万可以调到64或128。早停保存的是最佳模型,不是最后一轮模型,这个细节很多人忽略,导致最终用的是过拟合版本。
3.3 预测结果还原与评估指标
训练完加载最佳权重,预测测试集,用inverse_transform还原成真实房价,再算MAE和R²。MAE比RMSE更直观,单位是元;R²看解释方差比例。
model.load_state_dict(torch.load("best_model.pth")) model.eval() with torch.no_grad(): pred_scaled = model(X_test_t).numpy() # 还原真实尺度 pred_real = y_scaler.inverse_transform(pred_scaled) y_test_real = y_scaler.inverse_transform(y_test_scaled) from sklearn.metrics import mean_absolute_error, r2_score mae = mean_absolute_error(y_test_real, pred_real) r2 = r2_score(y_test_real, pred_real) print(f"MAE: {mae:.0f} 元, R²: {r2:.4f}")如果R²低于0.6,先别调网络,回头查数据里有没有泄露特征(比如“成交总价”混进了输入),或者类别编码是否合理。我一般会画一张预测值vs真实值的散点图,看是否集中在45度线附近。
4. 房价预测BP网络的避坑与排查:5个真实翻车现场
这一章记录我在不同数据集上踩过的坑,每条按现象、原因、解决写。你如果训练时遇到类似情况,直接对号入座。
4.1 损失降到某个值就不动了
现象:训练损失卡在0.3左右,验证损失更高,加层加神经元都没用。原因:目标值没有标准化,房价数值在百万级别,MSE梯度太大导致权重更新步长失控。解决:对y做StandardScaler,训练完再逆变换。这个坑我踩过两次,第二次是因为换了数据集忘了改。
4.2 验证损失比训练损失低很多
现象:训练损失0.5,验证损失0.2,看起来模型“泛化很好”。原因:训练集和验证集分布不一致,常见于按时间划分数据时,训练集是去年低价房,验证集是今年高价房。解决:改用随机划分,或者对时间特征做归一化。如果业务要求按时间外推,那就要接受验证损失更高的事实,别被数字骗了。
4.3 预测值全部偏向均值
现象:不管输入什么特征,输出都在均价附近。原因:网络欠拟合,可能是学习率太小或训练轮数不够。解决:先把学习率调到1e-2试几轮,看损失是否下降;如果下降后震荡,再降到1e-3。另外检查ReLU是否把太多神经元置零,可以换成LeakyReLU试试。
4.4 独热编码后维度爆炸
现象:地段评分有200个类别,独热后特征维度从20变成220,训练极慢且过拟合。原因:高基数类别特征不适合独热。解决:改用目标编码(Target Encoding)或嵌入层(Embedding)。目标编码要注意在训练集上计算均值再映射到验证集,防止泄露。
4.5 早停太早导致欠拟合
现象:第20轮就早停,验证损失还在下降趋势中。原因:patience设太小,或者验证集太小导致损失波动大。解决:patience调到30,验证集至少占总样本15%。如果数据量实在少,用K折交叉验证取平均验证损失,而不是单次划分。
5. 让房价预测BP网络更稳的进阶技巧:从能跑到好用
基础版跑通后,想让模型在业务里真正可用,还有几个技巧值得试。第一个是特征工程:把“房间数/面积”做成一个新特征“户均面积”,比单独喂两个特征效果更好。第二个是学习率调度:用ReduceLROnPlateau,验证损失不降时自动减半,比固定学习率稳。第三个是集成:训练5个不同初始化的网络,预测值取平均,MAE通常能降3%到5%。
# 学习率调度示例 scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5) # 在验证后调用 scheduler.step(val_loss)验证方法上,除了MAE和R²,我习惯按房价区间分段看误差。比如低价房(低于中位数)和高价房分别算MAE,如果高价房误差明显更大,说明模型对高价样本欠拟合,可以在损失函数里给高价样本更高权重。具体做法是自定义加权MSE,权重与房价成正比。
# 加权MSE,让高价样本贡献更大梯度 def weighted_mse(pred, target, weight_factor=0.5): weights = 1.0 + weight_factor * torch.abs(target) return torch.mean(weights * (pred - target) ** 2)weight_factor从0.5开始试,太大反而会让低价房预测崩掉。这个技巧在房价数据上特别有用,因为高价房样本少但业务关注度高。
最后说一个我自己的习惯:每次调完参数,把验证集预测结果和真实值按行导出成CSV,人工看前20条。模型指标再好,如果预测出一套“面积50平、房间数5”的房子价格是500万,那肯定是特征编码出了问题。指标是黑匣子,肉眼抽查是后悔药。希望帮到你。
本文还有配套的精品资源,点击获取