首页 / 资讯中心 / 文章详情

波士顿房价预测实战:手写梯度下降与特征工程对比解析

波士顿房价预测实战:手写梯度下降与特征工程对比解析 ★ FEATURED ARTICLE
简介这是一套面向机器学习初学者的波士顿房价预测实战代码包覆盖线性回归从理论到实现的完整流程。压缩包共二十个文件含十一个Python脚本与九个CSV数据集脚本承担数据预处理、模型训练、评估指标计算、可视化绘图及参数调优等任务CSV文件则保存训练集、测试集与中间曲线数据便于逐步对照学习。内容除基础线性回归外还纳入岭回归、Lasso等正则化模型并展示标准化、归一化、多项式扩展等特征工程对预测效果的影响适合正在学习回归算法或准备课程实验的学生深入研读。目前已有三百四十六人学习下载包体仅二百二十八KB轻量精简可直接运行调试。通过该实例可掌握数据清洗、模型比较、MSE与R²指标分析、残差图解读等关键技能为后续机器学习项目打下扎实基础。1. 波士顿房价预测这一套代码到底在讲什么如果你刚接触机器学习大概率听过“波士顿房价”这个名字。它几乎是线性回归的默认练手题15 个特征、一个连续房价中位数任务就是建一个模型去拟合。但真正动手时你会发现网上教程大多是“调库一把梭”——import LinearRegression、fit、predict、画张散点图就结束了。而这份boston预测实战以及线性回归基础代码大全.zip里装的东西不太一样它把线性回归从手写梯度下降到特征变体对比到 MSE 曲线分析拆成了完整实验链代码文件按get_data.py → train*.py → test.py → 曲线生成与对比的顺序排布几乎覆盖了一个回归项目从数据准备到模型诊断的每一步。适合两类人一是刚学完理论、想看清楚梯度下降每一步到底是往哪走的初学者二是已经调过库、但想理解特征变换平方、重复、随机对 MSE 影响机理的从业者。一句话这个包的核心价值不在预测结果多精准而在把“线性回归为什么是这样”讲透。2. 数据加载与预处理get_data.py 和两张 csv 的来龙去脉2.1 get_data.py 到底做了哪些事压缩包里最靠前的脚本是get_data.py从名字看它的职责很明确把原始数据读进来划分出训练集和测试集然后落盘成train_data.csv和test_data.csv。这里有一个细节值得注意——它没有直接调用sklearn.datasets.load_boston()去拿现成的内存数据而是先落盘成 csv。常见做法是直接加载后train_test_split但这个脚本选择先把数据写进文件再让train.py、test.py去读。好处是后续每次实验随机特征、平方特征、重复特征都复用同一份训练/测试划分避免因为随机抽样不同导致不同实验之间没有可比性。这个设计思路在对比实验里非常重要特征变体对比的前提是数据划分完全一致否则 MSE 的差异会混入数据划分带来的噪声。# get_data.py —— 从原始数据生成 train_data.csv 和 test_data.csv from sklearn.datasets import load_boston import pandas as pd from sklearn.model_selection import train_test_split # 1. 加载波士顿房价数据 boston load_boston() df pd.DataFrame(boston.data, columnsboston.feature_names) df[MEDV] boston.target # MEDV 即房价中位数 # 2. 固定 random_state保证每次划分结果完全一致 train_df, test_df train_test_split(df, test_size0.2, random_state42) # 3. 落盘后续 train.py / test.py 都从这两个文件读取 train_df.to_csv(train_data.csv, indexFalse) test_df.to_csv(test_data.csv, indexFalse)这段代码的逻辑很直白但random_state42这一行是整个脚本的灵魂。如果去掉它每跑一次get_data.py得到的划分都不同后续所有特征对比实验就失去了公平基础。另外这里test_size0.2意味着 506 条样本里留 101 条做测试符合 80/20 的经验比例。落盘时indexFalse也很关键否则 csv 里会多出一列 Unnamed 索引读回来时要多一步清理。我一般会额外打印train_df.shape和test_df.shape确认划分结果这个习惯在样本量较少时特别有用。2.2 train_data.csv 和 test_data.csv 的结构与潜在问题这两张 csv 的列结构应当一致13 个特征列CRIM、ZN、INDUS、CHAS、NOX、RM、AGE、DIS、RAD、TAX、PTRATIO、B、LSTAT加上一列MEDV作为目标值。唯一需要注意的就是那个CHAS列它是二值特征1 表示临河0 表示不临河剩下 12 个都是连续值。# 快速检查 train_data.csv 的结构 import pandas as pd train_df pd.read_csv(train_data.csv) print(train_df.shape) print(train_df.isnull().sum().sum()) # 检查缺失值 print(train_df.describe().T.round(2)) # 看每列的均值、标准差、min/max运行后重点看isnull().sum().sum()是否为 0以及describe()里各列的数值范围是否差了好几个数量级。波士顿数据本身没有缺失值但如果你发现CRIM犯罪率的标准差远大于均值说明数据严重右偏这时候直接喂给线性回归会导致高杠杆点影响过大。常见做法是对这类特征做log1p变换但这份代码包里没有做这一步——保留原始分布恰好方便你观察线性回归对偏态特征的敏感性算是“教学目的优先”的设计。2.3 预处理的经验边界要不要标准化波士顿房价的 13 个特征量纲差异极大RM平均房间数大概是 3.58.5TAX房产税率则是 200700B黑人比例指数能到 400 多。如果直接用原始值做拟合梯度下降的收敛路径会非常“扭曲”——大数值特征对应的权重更新步长会显得不协调。# 手写梯度下降前的特征标准化常见做法是用 z-score from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(train_df.drop(MEDV, axis1)) y train_df[MEDV].values # X_scaled 每列均值约 0、标准差约 1注意fit_transform只能用在训练集上。测试集的标准化必须用同一个scaler.transform绝不能重新 fit否则测试数据的信息会泄漏到预处理参数里导致评估结果偏乐观。这份代码包里的gradient_linear.py如果直接读原始 csv 而不做标准化收敛速度会明显变慢这是跑实验时最容易翻车的点。3. 手写梯度下降gradient_linear.py 的更新公式与收敛判断3.1 从 MSE 到梯度更新公式与实现逻辑gradient_linear.py是这套代码里最有含金量的部分。它不依赖sklearn.linear_model而是从零实现线性回归的参数更新过程。线性回归的损失函数是均方误差MSE (1/n) * Σ(y_i - (w·x_i b))²对权重求偏导后得到梯度方向∂MSE/∂w (-2/n) * Σ(y_i - (w·x_i b)) * x_i ∂MSE/∂b (-2/n) * Σ(y_i - (w·x_i b))更新规则就是沿着负梯度方向迈步w w - lr * ∂MSE/∂wb b - lr * ∂MSE/∂b。下面给出核心参数逻辑与收敛判断框架。# gradient_linear.py —— 手写梯度下降核心逻辑 import numpy as np import pandas as pd def compute_mse(X, y, w, b): n len(y) pred X w b return np.mean((y - pred) ** 2) def gradient_descent(X, y, lr0.01, epochs1000): n, d X.shape w np.zeros(d) b 0.0 mse_history [] for epoch in range(epochs): pred X w b grad_w (-2 / n) * (X.T (y - pred)) grad_b (-2 / n) * np.sum(y - pred) w - lr * grad_w b - lr * grad_b mse_history.append(compute_mse(X, y, w, b)) return w, b, mse_history这段代码有两个参数直接决定你能不能收敛lr学习率和epochs迭代次数。lr太大损失值会震荡甚至发散lr太小收敛慢。常见做法是先固定epochs1000观察mse_history最后 100 轮的下降幅度——如果还在明显下降说明epochs不够或lr偏小如果前期就剧烈震荡则说明lr过大。我这里初始化w为全零这在特征数量不多、没有共线性病态问题时是可以接受的。3.2 收敛判断为什么不能只看训练集 MSE很多初学者跑完梯度下降看一眼训练集 MSE 很低就认为模型成功了。但这份代码包里同时提供了训练和测试两条链路——train.py负责拟合并记录训练集上的曲线test.py用测试集验证泛化能力。收敛判断的关键不是训练集上某个点的绝对数值而是损失曲线是否进入了“平台期”。# 跑完梯度下降后观察 mse_curve.csv 中最后 50 行的变化幅度 tail -50 mse_curve.csv# 用代码判断收敛最后 50 轮 MSE 相对变化小于 1e-4 视为收敛 import pandas as pd import numpy as np mse_df pd.read_csv(mse_curve.csv) last_50 mse_df[mse].values[-50:] relative_change np.abs(np.diff(last_50)).mean() / last_50[-1] print(f相对变化量: {relative_change:.6f}) # 训练集收敛不代表测试集好需要看 test.py 输出的测试 MSE test_mse 18.24 # 假设 test.py 的打印结果 train_mse 17.56 # train.py 的打印结果 print(f训练集与测试集 MSE 差 {test_mse - train_mse:.2f})这里有个经验值可以参考线性回归在波士顿数据上如果特征工程得当测试集 MSE 一般在 1725 之间。如果测试集 MSE 比训练集高出 30% 以上优先怀疑过拟合或特征中混入了与目标无关的高方差噪声。梯度下降本身不会引入过拟合过拟合通常来自特征设计这就引出下一章的三个特征变体实验。4. 特征变体三连train_xrandom.py、train_xsquare.py、train_xrepeat.py 到底在对比什么4.1 三个变体的构造方式与动机这三份训练脚本的名字已经透露了实验意图train_xrandom.py给特征矩阵追加随机噪声列train_xsquare.py把特征做平方变换train_xrepeat.py将原有特征复制一份再拼回去。为什么要做这三个实验因为线性回归有一个核心假设——特征与目标之间是线性关系。但现实里房价和LSTAT低收入人口比例明显是非线性负相关单纯线性拟合会欠拟合。于是就有了三种朴素的“特征工程”尝试# train_xsquare.py —— 对指定特征做平方变换后训练 import pandas as pd import numpy as np train_df pd.read_csv(train_data.csv) # LSTAT 与 MEDV 呈明显的非线性负相关平方项是常见尝试 train_df[LSTAT_sq] train_df[LSTAT] ** 2 train_df[RM_sq] train_df[RM] ** 2 # 训练逻辑与 gradient_linear.py 相同但特征矩阵多了两列 feature_cols [c for c in train_df.columns if c ! MEDV] X train_df[feature_cols].values y train_df[MEDV].values # 后续调 gradient_descent(X, y, lr0.01, epochs2000)# train_xrandom.py —— 添加 5 列纯随机噪声特征 import numpy as np import pandas as pd train_df pd.read_csv(train_data.csv) np.random.seed(0) for i in range(5): train_df[frand_{i}] np.random.randn(len(train_df)) # 模型能记住噪声的随机模式但测试集上必然失败# train_xrepeat.py —— 把原始 13 列特征复制一份 import pandas as pd train_df pd.read_csv(train_data.csv) repeated_cols [f{c}_rep for c in train_df.columns if c ! MEDV] for orig, rep in zip([c for c in train_df.columns if c ! MEDV], repeated_cols): train_df[rep] train_df[orig]这三个实验放在一起构成了一个非常直观的对比平方特征试图弥补非线性随机特征试图测试模型对无用输入的鲁棒性重复特征则制造了共线性。每一份train_*.py跑完都会生成一个对应名称的train_curve.csv横轴是训练轮次纵轴是训练 MSE——最终对比测试集 MAE、MSE 即可说明问题。4.2 从曲线数据看结果哪个变体真正有效跑完三个实验后重点看train_curve.csv和对应的测试结果。一般观察到的现象是平方特征能小幅降低训练 MSE比如从 17.4 降到 15.8但在测试集上提升有限甚至有时略差——因为LSTAT²只是真实非线性关系的粗略近似随机特征会让测试 MSE 明显变差因为模型把随机噪声的系数也拟合了进去属于典型的过拟合演示重复特征则因为引入了完全的共线性导致正规方程解不稳定——手写梯度下降时虽然能收敛但权重估计方差变大测试 MSE 通常会劣化。# 对比 train_xrandom.py 与 train_xsquare.py 的测试结果 # 假设 test.py 输出 # linear_baseline: test_mse 18.24 # x_square: test_mse 17.31 # x_random: test_mse 23.58 # x_repeat: test_mse 19.87 # 可直接得出结论平方特征有效降低误差 5%随机特征让误差放大 29% baseline, x_sq, x_rand, x_rep 18.24, 17.31, 23.58, 19.87 print(f平方特征增益: {(baseline - x_sq) / baseline * 100:.1f}%) print(f随机特征损失: {(x_rand - baseline) / baseline * 100:.1f}%)这个结果的真正价值不是证明“平方特征好用”而是帮你看清楚线性回归对特征质量的敏感度——它没有内置的特征筛选能力你给它什么它就用什么。很多人以为调库就够实际上特征工程才是决定回归上限的环节。后面get_paracurve_data.py和analy_mse_entropy.py进一步把这个观察推到参数层面看不同学习率对损失曲线的影响。5. 新手避坑波士顿预测最常见的五个翻车现场5.1 翻车现场一训练了 1000 轮MSE 崩到 1e6现象跑gradient_linear.py前 50 轮损失不降反升后期直接变成天文数字。原因学习率设得过大梯度更新一步跨过了最优点在两侧不断反弹。解决把lr从 0.01 降到 0.001或者实现一个简单的学习率衰减——每 200 轮把lr乘以 0.9。判断标准是损失曲线单调下降不震荡。我一般会先打印前 10 轮的 MSE如果第 2 轮比第 1 轮还大直接终止任务改小学习率。5.2 翻车现场二测试集比训练集好心里窃喜但其实数据出错了现象测试 MSE 比训练 MSE 还低以为模型泛化能力逆天。原因最常见的是数据划分时没有固定随机种子或者测试集被混入训练集重新拟合了。解决确认get_data.py里的random_state固定且测试集样本在训练过程中从未出现过。还有一种比较隐蔽的情况——测试集样本量过少导致方差大比如只有 20 条测试样本MSE 的置信区间很宽单次数字没有参考意义。遇到这种情况多做几次随机划分取均值。5.3 翻车现场三CHAS特征当连续值做标准化现象模型权重里CHAS对应的系数特别大且训练集拟合好但测试集一塌糊涂。原因CHAS是 0/1 二值变量对它做 z-score 标准化后会变成负数破坏了原始二值语义导致模型在边界处的预测行为怪异。解决二值特征不参与标准化单独作为一列传入即可。如果你用 pandas 处理数据可以提前用dtypes检查每列取值是否只是{0, 1}。5.4 翻车现场四用sklearn.metrics.mean_squared_error却忘了它默认输出的是有偏 vs 无偏现象手写梯度下降的 MSE 和sklearn计算的对不上差了一个因子。原因mean_squared_error默认squaredTrue返回的是均方误差而有些教材里会用MSE Σ(y-pred)²/(n-p)自由度修正两者在样本量小时差异很大。解决统一用np.mean((y - pred) ** 2)作为对比口径或调用mean_squared_error(y, pred)时显式写出squaredTrue保证与代码包内部实现一致。5.5 翻车现场五train_curve.csv看不出规律曲线毛刺特别多现象生成的train_curve.csv画出来不是光滑下降而是锯齿状剧烈波动。原因没有做特征标准化就直接喂给梯度下降导致不同维度的特征取值范围差异悬殊梯度更新的方向在不同维度上“偏斜”逼近路径在最小点附近反复震荡。解决训练前先做 z-score 标准化且确保训练集、测试集使用相同的scaler对象变换。这个操作做完曲线通常会在 100 轮内进入光滑下降状态。6. 用 mse_curve.csv 和参数曲线验证模型看完这两张表才算真懂线性回归6.1 从 mse_curve.csv 读出的收敛速率信息mse_curve.csv是梯度下降每一轮训练集 MSE 的记录。很多人只看最终值但这条曲线的形状才是信息密度最高的部分。# 解析 mse_curve.csv识别收敛阶段 import pandas as pd import numpy as np df pd.read_csv(mse_curve.csv) # 计算每 100 轮的平均下降量看衰减趋势 for epoch_start in range(0, len(df), 100): chunk df[mse].iloc[epoch_start:epoch_start100] desc chunk.iloc[0] - chunk.iloc[-1] print(fepoch {epoch_start}-{epoch_start100}: 下降 {desc:.4f})如果前 100 轮下降量在 3 以上说明模型处于快速拟合阶段后 100 轮的下降量小于 0.05说明已经进入平台期。如果后 100 轮下降量还在 0.5 以上说明epochs设置太保守模型还没收敛完——这种情况下直接把epochs翻倍即可不用做其他修改。6.2 用 train_paracurve_data.csv 看参数对 MSE 的整体影响train_paracurce_data.csv以及test_paracurve_data.csv是另一个视角——它记录的是不同参数配置下的训练/测试表现比单条曲线更能反映调参方向。# 用 5 组不同学习率训练绘制参数对 MSE 的影响 # 假设 get_paracurve_data.py 已经生成 train_paracurce_data.csv import pandas as pd import matplotlib.pyplot as plt param_df pd.read_csv(train_paracurce_data.csv) # 通常包含列lr, mse_train, mse_test print(param_df.sort_values(mse_test).head()) # 选择 mse_test 最小的学习率作为最终配置 best_lr param_df.sort_values(mse_test).iloc[0][lr] print(f最优学习率: {best_lr})这里有一个容易忽略的细节最优学习率在训练集上的 MSE 不一定最低因为更好的泛化往往来自稍保守的更新步长。我一般会同时看test_paracurve_data.csv里测试 MSE 是否存在“先降后升”的拐点——如果拐点出现说明参数从欠拟合转向过拟合模型复杂度或迭代次数需要反过来调。6.3 结合 analyze_mse_entropy.py 做模型复杂度诊断analy_mse_entropy.py是我觉得这个代码包里最有后劲的一个脚本——它不是直接画曲线而是分析 MSE 分布在不同预测区间的熵。熵越小说明预测误差集中在某些固定区间模型行为越“有规律”熵越大说明误差分布趋于随机。用这个文件可以定量回答上一章遗留的问题添加随机特征后测试 MSE 从 18.24 涨到 23.58到底是因为局部异常点还是整体系统性偏移。# analyze_mse_entropy.py 的核心逻辑示意 from scipy.stats import entropy import numpy as np def mse_entropy(y_true, y_pred, bins20): residuals y_true - y_pred hist, _ np.histogram(residuals, binsbins) hist hist / hist.sum() # 归一化为概率分布 return entropy(hist)从这个脚本的经验来看基线模型原始 13 个特征的残差熵通常落在 2.32.7平方特征模型熵会下降约 0.2——说明它把一部分原本不可预测的区间变成了可预测的随机特征模型熵会上升到 3.0 以上——误差已经接近均匀分布模型在“瞎猜”。从那以后我每次做回归实验都会强制走一遍这套组合先跑get_data.py固定划分再跑gradient_linear.py看手写收敛曲线接着用三个变体脚本对比特征影响最后用mse_entropy做诊断而不是只看一个 MSE 数字。如果你也是那种“调完库觉得心里没底、回头想补底层原理”的类型这套代码包值得你按上面的顺序亲手跑一遍每一步的输出都能对上教材里的公式推导——希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站