资讯动态

波士顿房价预测线性回归源码解析:BGD梯度下降实现与避坑指南

发布时间:2026/10/19 22:55:36 来源:尧图企业网站定制
简介面向机器学习初学者的波士顿房价预测线性回归完整项目采用梯度下降法BGD与小批量梯度下降算法MBGD实现适合作为课程设计、期末大作业或入门实践参考。项目从boston.csv导入数据完成训练集与测试集划分、数据归一化后通过初始化参数、计算预测值、损失函数与梯度并迭代更新权重最终绘制损失函数随迭代次数的变化曲线并在测试集上评估模型且可视化拟合效果。源码包共5个文件含两个Python脚本、一张结果图、一份说明文档和配置文件整体仅122KB轻量易用。其中包含批量梯度下降算法和小批量梯度下降算法的独立脚本两个版本可对比学习便于理解不同优化策略的收敛过程与精度差异代码注释清晰修改迭代轮次或批量大小即可观察损失变化方便完成实验报告。目前已有1455人学习该大作业结构完整、下载即可运行尤其适合需要提交高质量课程项目的学生参考。1. 线性回归预测波士顿房价这份源码的价值不在精度在梯度下降的每行代码期末季或者课程设计节点经常看到有人在找「数据量不大、流程完整、答辩时能把公式讲清楚」的项目。这个基于线性回归实现波士顿房价预测的 Python 源码恰好是这类需求的典型答案。代码不大但把数据集导入、训练测试划分、归一化、BGD 梯度下降五步、损失曲线、测试评估、可视化全串了起来还附了一版 MBGD 小批量梯度下降实现。适合机器学习课设、期末大作业也适合刚学完线性代数、想知道「梯度到底是怎么更新参数」的人。我不建议把它当纯预测工具看它的正确用法是当教材每一行代码都能对应到损失函数和梯度公式上。2. 项目文件与数据准备先从 boston.csv 读懂特征和标签拿到压缩包先别急着跑python BGD_4.py我建议先花十分钟把目录结构和数据格式确认一遍。这步看起来简单却能避免后面调参时「数据到底对不对」的怀疑。2.1 文件清单BGD_4.py、LinearRegression_1.py 和 boston.csv 的分工解压后是五个文件核心就两个脚本。我整理成表格文件作用BGD_4.py完整 BGD 训练流程从导入数据到可视化全链路LinearRegression_1.py对照实现适合和手写 BGD 的结果做交叉验证boston.csv数据集506 条样本13 个特征 1 个房价标签README.md运行说明和结构介绍.gitignore版本控制忽略文件本地跑不影响两份脚本等价于是「手写派」和「工具派」的对照一份从零手写梯度下降另一份用封装好的方式做结果校准。我一般会先跑BGD_4.py看效果再用LinearRegression_1.py的结果和它对拍。如果两份脚本输出的 R² 差得离谱那问题多半出在数据预处理而不是梯度公式。2.2 数据导入与划分为什么随机种子必须固定boston.csv 是带表头的纯文本格式用 pandas 读最方便。我的习惯是先打印 shape 确认读进来多少行、多少列再检查有没有空值这一步能省掉后面一大堆定位问题的精力。import numpy as np import pandas as pd data pd.read_csv(boston.csv) # 前 13 列是特征最后一列 MEDV 是房价标签 X data.iloc[:, :-1].values y data.iloc[:, -1].values print(X.shape, y.shape) # 预期输出 (506, 13) 和 (506,) print(np.isnan(X).sum(), np.isnan(y).sum()) # 统计缺失值逻辑说明iloc[:, :-1]取全部行、去掉最后一列iloc[:, -1]只取最后一列。506 是样本数13 是输入特征维度y 是一维数组后面矩阵运算全都依赖这个形状正确。缺失值统计是为了防止 csv 里出现空单元格导致后续X w直接算出nan。数据划分这一步random_state必须写死。没有固定随机种子每次运行划分结果都不同损失曲线和测试指标也每次都变答辩时候老师一问「为什么这次 R² 和上次不一样」就很难解释。from sklearn.model_selection import train_test_split # 8:2 划分固定随机种子保证结果可复现 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) print(f训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]})参数说明test_size0.2表示 20% 数据留作测试是课程设计里最常见的比例random_state42是常用的固定种子值换其他整数也行关键是固定。好处是别人下载同一份代码跑出来曲线和指标和你完全一致省去很多交流成本。2.3 数据归一化为什么梯度下降对量纲这么敏感boston.csv 里的特征量纲差异非常大——比如 RM平均房间数是 3 到 9 的量级而 CRIM犯罪率可能只有 0.01 到 100TAX 更是几百。如果不做归一化梯度下降时大数值特征对应的权重更新会被放大损失曲面像一口狭长的碗迭代路径会来回震荡收敛极慢甚至发散。这是梯度下降类算法的老毛病正规方程解法没这个问题所以凡是手写梯度下降第一步都要先归一化。from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 训练集在训练数据上计算均值方差并完成转换 X_train scaler.fit_transform(X_train) # 测试集直接用训练集的均值方差做转换 X_test scaler.transform(X_test)逻辑说明fit_transform是先计算训练集的均值和标准差再去做标准化测试集只调transform用的是训练集统计出来的参数不重新计算。如果测试集单独fit_transform等于把测试数据的分布信息提前泄露给了模型测试指标会虚高这个坑在后文避障章还会展开讲。标准化之后的特征近似满足均值为 0、方差为 1 的分布量纲差异被抹平梯度下降收敛路径就正常了。3. 手写 BGD 训练流程初始化、前向、损失、梯度、更新五件套这个项目的核心价值就在这章。摘要里列的那七个步骤本质上是梯度下降公式的代码翻译。我在给学生讲这个项目时会要求他们先把公式写在纸上再对照着看代码——每个函数、每个变量都能找到公式出处讲解时讲得清答辩时也扛得住问。3.1 参数初始化把截距拼进权重向量少写一个更新分支线性回归的预测函数是f(x) x·w b多一个截距 b意味着梯度和更新都要额外写一个分支。这个项目采用的做法是用np.concatenate把一列 1 拼到特征矩阵最后让截距并进权重向量这样更新逻辑统一成一个公式f(x) X w代码干净很多。# 将截距项并入特征矩阵作为最后一维 X_train np.concatenate([X_train, np.ones((X_train.shape[0], 1))], axis1) X_test np.concatenate([X_test, np.ones((X_test.shape[0], 1))], axis1) # 权重向量维度 特征维度 1最后一维对应截距 w np.zeros(X_train.shape[1])逻辑说明np.ones((样本数, 1))构造一列 1np.concatenate(..., axis1)沿列方向拼接把特征矩阵从(405, 13)扩成(405, 14)。w初始化为全 0长度也是 14最后一个位置对应偏置。初始权重全 0 是线性回归的通用起点因为损失函数是凸函数从任何起点开始梯度下降都能收敛到全局最优选 0 只是图省事。之后所有更新都只操作 w 一个向量不用单独维护 b 的更新逻辑。3.2 f(x) 与 J(w)向量化写法避开 for 循环有了拼接后的 X前向计算直接X w就是 405 个样本的预测值损失函数用均方误差求平均这就是摘要里的「求 f(x)」和「求 J(w)」。向量化的好处是底层走矩阵乘法比 for 循环逐样本算快一个量级。def bgd(X, y, epochs1000, lr0.01): 批量梯度下降训练线性回归模型 X: 已拼接截距列的特征矩阵 y: 房价标签 epochs: 迭代轮数 lr: 学习率 m, n X.shape w np.zeros(n) loss_history [] for epoch in range(epochs): # (a) 前向计算线性模型的预测值 f_x X w # (b) 计算均方误差损失 J(w) loss np.mean((f_x - y) ** 2) loss_history.append(loss) # (c) 计算梯度2/m * X^T (预测值 - 真实值) grad (2 / m) * X.T (f_x - y) # (d) 参数更新沿负梯度方向走一步 w w - lr * grad # 每 100 轮打印一次便于观察收敛过程 if epoch % 100 0: print(fepoch {epoch}, loss {loss:.4f}) return w, loss_history逻辑说明f_x X w一次算出所有预测值(f_x - y)得到残差向量.mean()对残差平方求平均得到当前轮次的 J(w)。梯度X.T (f_x - y)的本质是每个特征与残差的内积乘2/m是因为损失函数是均方误差、求导后系数为 2/m。w w - lr * grad就是上文的 (e)学习率控制每一步走多远。参数说明epochs1000是迭代轮数的默认值这个值在数据集 405 个训练样本上足够收敛lr0.01是学习率默认值下文避坑章会讲到改成 0.1 就会发散。loss_history记录每轮的损失值第 4 章画曲线就靠它。3.3 训练主流程epochs 循环里到底发生了什么把上一节的函数串起来BGD 的训练流程只需要几行代码。图表数据准备完毕调用训练函数拿到训练好的参数 w 和损失曲线数据。# 训练模型返回最优权重和损失历史 w_trained, loss_history bgd(X_train, y_train, epochs1000, lr0.01) # 打印最终训练损失 print(f训练结束最终 loss: {loss_history[-1]:.4f}) print(f权重向量维度: {w_trained.shape}截距项: {w_trained[-1]:.4f})逻辑说明bgd函数内部跑 1000 轮每轮做一次前向、算损失、求梯度、更新权重。loss_history记录了 1000 个损失值loss_history[-1]是最后一轮的损失。一个值得关注的细节最后一轮权重和倒数第二轮并不完全一样因为梯度下降只要学习率不为 0参数就一直在微调只是损失变化幅度越来越小。对于凸优化问题1000 轮时已经逼近最优解再增加轮数收益有限。4. 可视化验证与调参损失曲线、测试集评估和拟合效果训练完不能只盯着控制台打印的 loss 数字可视化才是判断「模型真的学到东西了」的标准。这个项目自带的 image.png 展示的就是损失曲线和拟合效果图读懂这两张图调参就有方向了。4.1 损失曲线能看出什么发散、震荡与收敛三种形态损失曲线是最直接的训练监控工具。把训练过程中每轮的 loss 画出来曲线的形状直接反映学习率和迭代次数的设置是否合理。import matplotlib.pyplot as plt plt.plot(range(len(loss_history)), loss_history) plt.xlabel(Epochs) plt.ylabel(MSE Loss) plt.title(BGD Loss Curve) plt.grid(True) plt.show()逻辑说明range(len(loss_history))作为横轴表示轮数纵轴是均方误差。1000 轮的曲线在训练初期下降非常快大约 100 轮之后趋于平缓这是正常且健康的收敛形态。怎么解读曲线第一种是「陡降后平稳」说明学习率合适模型正常收敛第二种是「上下震荡但整体下行」说明学习率偏大参数在最优点附近来回摆动可以调低学习率解决第三种是「一路上扬甚至出现 nan」说明学习率过大导致参数更新越过最优点数值直接爆掉。画完曲线先判断属于哪一类再决定是调学习率还是增迭代次数。4.2 测试集评估MSE 和 R² 才是说服老师的指标训练损失再低也可能只是拟合了训练集真正反映泛化能力的是测试集上的表现。项目流程接下来就是拿训练好的权重对测试集做预测再计算评估指标。# 测试集预测此时 X_test 已拼接过截距列 y_pred X_test w_trained # 均方误差 test_mse np.mean((y_pred - y_test) ** 2) # R² 决定系数 ss_res np.sum((y_test - y_pred) ** 2) ss_tot np.sum((y_test - np.mean(y_test)) ** 2) r2 1 - ss_res / ss_tot print(f测试集 MSE: {test_mse:.4f}) print(f测试集 R²: {r2:.4f})逻辑说明y_pred X_test w_trained是在测试集上的前向计算注意 X_test 已经在第 3 章拼接了截距列所以这个矩阵乘法自动包含了偏置项。MSE 有量纲单位是房价的平方数值大小不好直观判断好坏R² 是决定系数区间大致在 0 到 1 之间越接近 1 说明模型解释了越多的数据方差。对于这个数据集和线性回归模型的组合常规结果 R² 大约在 0.7 左右上下浮动具体值受random_state影响通常不是重点重点是训练集和测试集指标差距不要太大——如果训练集 R² 0.95、测试集只有 0.4那就是过拟合或数据划分出了问题。4.3 可视化拟合效果散点对比模型与真实值除了损失曲线项目还会把测试集的真实房价和预测房价画在一起。我常用的做法是画真实值 vs 预测值的散点图再画一条 45 度参考线点越贴近参考线说明预测越准。plt.figure(figsize(8, 6)) plt.scatter(y_test, y_pred, alpha0.6) # 45 度参考线xy 的理想情况 plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, linewidth2) plt.xlabel(True Price) plt.ylabel(Predicted Price) plt.title(True vs Predicted Price (Test Set)) plt.show()逻辑说明plt.scatter以真实房价为横轴、预测房价为纵轴每个点代表一个测试样本。红色虚线是理想参考线点落在虚线上说明预测完全等于真实值。如果看到点在参考线两侧均匀分布说明模型没有系统性偏差如果明显偏在线的上方或下方说明可能存在欠拟合或特征没处理好。4.4 参数组合参考学习率、迭代次数、归一化的搭配调参经验我整理成一张表方便快速定位问题组合学习率epochs归一化观察结果A0.1500有loss 前几轮飙升后续出现 nanB0.01500有曲线陡降后平稳收敛正常C0.0011000有曲线持续缓慢下降最终 loss 偏高D0.011000无曲线震荡明显收敛速度慢组合 A 说明学习率过大梯度更新越过了最优解组合 C 说明学习率过小1000 轮还没完全收敛可以加大 epochs 或调大学习率组合 D 说明归一化缺失导致损失曲面不规整震荡是量纲差异的外在表现。项目默认给的是组合 B 的参数直接运行就能得到可复现的结果。5. 避坑与排查归一化泄漏、学习率爆炸、MBGD 分母三类高频翻车点这个项目我在多个课程设计里见过学生反复踩相同的坑有些问题短时间内根本发现不了。下面这几条是我认为最有代表性的排查记录按「现象 → 原因 → 解决」写照着检查能省下半天调试时间。5.1 测试集归一化泄漏指标虚高却不自知现象测试集 R² 高得惊人接近 0.9但换一份新数据跑效果明显变差损失翻了好几倍。原因代码里对 X_test 也调了fit_transform而不是transform。这导致测试集的均值和标准差是从测试数据自己算出来的相当于把测试集的分布信息提前塞进了模型。模型在做预测时「见过」测试集的整体统计特征评估结果虚高。解决严格区分fit_transform和transform的使用对象。训练集用fit_transform计算并应用均值和方差测试集只调用scaler.transform(X_test)使用训练集保存下来的统计参数。运行完打印scaler.mean_和scaler.scale_确认测试集用的是同一套参数。5.2 学习率 0.1 直接爆炸损失曲线一路向上现象把学习率从 0.01 改成 0.1第一轮 loss 就几千跑了 50 轮后变成nan损失曲线直接断掉。原因学习率过大时w w - lr * grad这一步的更新幅度超过了损失函数下降所需的范围。参数跳到了损失更大的位置下一轮梯度更大再一跳更远形成正反馈最终数值溢出变成nan。解决学习率从小的开始。常见做法是先用 0.001 跑一轮看 loss 曲线的下降趋势再逐步放大到 0.01、0.05。当发现某轮loss突然变大立刻把学习率除以 10 重新跑。如果项目要求展示「调参过程」保留一组发散曲线和一组收敛曲线反而能体现对算法的理解。5.3 MBGD 梯度分母混用收敛「稳但慢」的隐蔽问题现象换成小批量梯度下降后loss 曲线下降很稳但 500 轮后的损失明显高于 BGD看起来像是模型还没收敛。原因梯度公式里分母写的是总样本数 m而实际计算只用了batch_size个样本。梯度本身是 batch 的梯度分母却用 m相当于把每次更新的步长缩小到了原来的batch_size / m。学习率实际被严重缩小收敛速度自然变慢。解决梯度分母与样本数保持一致。用当前 batch 的样本数作分母同时把学习率适当调大。改完后用同一份数据对比 MBGD 和 BGD 的最终 loss差距应该在可接受范围内。这条是手写小批量梯度下降最常见的混淆点项目里 MBGD 的实现就值得重点检查和推敲。提示判断梯度公式是否写对可以打印第一次更新前和更新后的权重差值手算一遍确认数值量级符合预期。5.4 旧版数据集加载方式失效换了环境直接报错现象在新环境里运行时from sklearn.datasets import load_boston直接报错提示该数据集已被移除或者提示 future warning。原因新版 scikit-learn 移除了内置的波士顿房价数据集因为这个数据集存在伦理争议和合规问题官方不再维护。许多旧教程里的代码在新版库上跑不通。解决项目使用boston.csv文件自行导入数据绕开了库内置数据集的兼容性问题这也是它带 csv 文件的原因。本地运行时确认boston.csv和脚本在同一目录避免相对路径读取失败。如果你自己写新代码建议直接改读其他公开数据集。5.5 损失曲线「贴地」看不到细节初始 loss 过大惹的祸现象画出来的损失曲线是一条贴着横轴的线前面几轮数值巨大后面几百轮的收敛过程被压缩成几乎看不见的波浪线完全没法判断收敛情况。原因plt.plot默认自动缩放坐标轴第一轮 loss 如果是几千后面 100 轮的 10 以下的变化在图中就变成一条直线。解决画图时从第 10 轮开始截取或者对 loss 取对数再画。plt.plot(range(10, len(loss_history)), loss_history[10:])是简洁做法。另一招是把 loss 数组整体除以第一个值做归一化横轴纵轴都变成相对变化曲线形状更直观。6. MBGD 扩展验证从全量梯度改为小批量参数更新逻辑还成立吗BGD 的局限在于每轮迭代都用全部训练样本计算梯度405 个样本还好但数据量上万时每轮的计算开销会明显变大。MBGD 的思路是每次随机取一小批样本来估计梯度计算量小、收敛速度快。这份源码里保留了 MBGD 的实现正好可以用来验证「换一种梯度估计方式线性回归的更新逻辑是否依然成立」。def mbgd(X, y, epochs300, lr0.01, batch_size16): m, n X.shape w np.zeros(n) loss_history [] for epoch in range(epochs): # 每轮开始前随机打乱样本顺序 idx np.random.permutation(m) X_shuffled X[idx] y_shuffled y[idx] for i in range(0, m, batch_size): X_batch X_shuffled[i:i batch_size] y_batch y_shuffled[i:i batch_size] # 用当前 batch 计算预测值和梯度 f_x X_batch w # 分母用 batch_size与参与计算的样本数保持一致 grad (2 / batch_size) * X_batch.T (f_x - y_batch) w w - lr * grad # 每个 epoch 结束后算一次全量损失用于观察 loss np.mean((X w - y) ** 2) loss_history.append(loss) return w, loss_history逻辑说明np.random.permutation(m)生成乱序索引避免每个 batch 的顺序固定导致梯度方向有偏。内层循环按batch_size切分数据X_batch.T (f_x - y_batch)计算该子集的梯度然后更新权重。注意梯度分母用的是batch_size而损失计算仍用全量数据这样每个 epoch 的 loss 可比性更强。我习惯对比验证两件事一是 MBGD 收敛后的 loss 和 BGD 相差多少二是同样 300 轮下谁跑得快。通常 MBGD 前几十轮的 loss 下降速度明显快于 BGD因为单轮计算量小可迭代次数更多但 batch_size 太小会导致梯度噪声大loss 曲线会周期性抖动。项目默认的batch_size16在 405 个训练样本上是一个比较平衡的值改小到 4 抖动会变明显改大到 128 则更接近 BGD 的收敛曲线。验证方法是把两种算法的最终权重打印出来对比BGD 的w和 MBGD 的w应该非常接近差异主要来自 batch 采样的随机性和学习率的不同。如果两条曲线形态差异过大优先检查分母是batch_size还是m。从那以后我每次跑这类课程设计都会把 BGD 和 MBGD 的损失曲线叠加到同一张图里先看 50 轮内是否都在下降再谈调参这个习惯帮我省掉了大量无意义的参数穷举。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑