线性回归与波士顿房价预测:原理、实现与避坑指南 简介这是一份基于线性回归实现波士顿房价预测的Python源码大作业面向机器学习初学者、高校学生及需要完成课程设计或期末大作业的开发者。项目已获导师指导并取得97分高分下载后无需修改即可运行可直接用于课程设计与期末答辩。资源包共5个文件含两个Python脚本、一张结果展示图片、一份说明文档及配置文件。Python脚本分别实现批量梯度下降与小批量梯度下降算法涵盖数据导入、训练集与测试集划分、归一化、参数初始化、损失计算、梯度更新、迭代训练、测试集预测及可视化拟合效果等完整流程说明文档对不同模块代码逻辑作了解释便于学习与扩展。压缩包仅122KB轻量易用。目前已有1455人学习下载。若希望快速掌握线性回归与梯度下降的实际编码实现这份高分项目可直接参考复用。1. 线性回归 波士顿房价预测为什么大作业都选它基于线性回归实现波士顿房价预测这一题几乎出现在每个入门机器学习课程的大作业清单里。它样本量不大、特征明确看起来闭着眼睛都能写完但真正从零开始自己跑一遍的人十个里有三四个会卡在数据加载、归一化和梯度爆炸这些环节。这篇笔记把完整源码的思路拆开讲从特征与房价之间的线性假设到数据预处理再到手写梯度下降并与工具库结果做对比验证最后集中踩一遍最常见的几个坑。如果你想要一份能跑、能讲、还能经得起追问的大作业按这条路径走基本不会出错。2. 先把原理拆明白线性回归凭什么能预测房价2.1 十三个特征和一个目标预测问题的输入输出怎么对齐波士顿房价数据集里每一条样本对应城市某个区域的一组统计信息目标值是该区域房屋价格中位数。特征一共 13 个覆盖了犯罪率、住宅用地比例、一氧化氮浓度、每栋住宅平均房间数、到就业中心距离等维度。下面这张表挑了五个影响最直观的你能快速建立起「特征长什么样」的直觉。特征缩写含义RM每栋住宅的平均房间数LSTAT该区域低收入人口占比PTRATIO该区域师生比DIS到波士顿就业中心的加权距离TAX每万美元房产的税率这个任务的本质是找到一个从 13 维特征向量到房价的映射关系。线性回归做了一个很强的假设房价和每一个特征之间是线性组合的关系也就是 y w₁x₁ w₂x₂ … w₁₃x₁₃ b。w 是特征权重b 是截距。所谓「训练模型」就是确定一组 w 和 b让预测值和真实房价的平均差距尽可能小。动手之前先验证一下线性假设是否成立。我习惯把强相关的特征和房价画成散点图肉眼看一眼分布import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(housing.csv) # 加载本地数据 target df.iloc[:, -1] # 最后一列是房价中位数 fig, axes plt.subplots(1, 3, figsize(12, 3)) for ax, col in zip(axes, [RM, LSTAT, PTRATIO]): ax.scatter(df[col], target, alpha0.5) ax.set_xlabel(col) ax.set_ylabel(Price) ax.set_title(f{col} vs Price) plt.tight_layout() plt.show()这段代码用前三个最典型的特征画散点图。alpha0.5 让重叠的点显示为深色避免样本点过多时看不出分布趋势。跑完之后你会看到 RM 与房价明显正相关、LSTAT 与房价明显负相关说明线性模型的起点是站得住的。再看一遍量化的相关系数作为肉眼观察的补充corr df.corr().iloc[:-1, -1].sort_values() print(corr)输出结果里RM 的相关系数通常接近 0.7LSTAT 接近 -0.7属于强相关。有了这一步后面的训练才不是盲目的。2.2 最小二乘与梯度下降这套源码选哪条路更合理线性回归的参数求解有两条经典路线。一条是最小二乘的闭式解直接令损失函数对 w 的导数为零得到 w (XᵀX)⁻¹Xᵀy。另一条是梯度下降从一组初始参数出发沿着损失函数下降最快的方向迭代更新。从工程效率看闭式解一行代码就能出结果但大作业我并不推荐只写闭式解。原因很简单闭式解拿不出「训练过程」答辩时导师问你模型怎么学的你只能答「算了个矩阵」而不是展示一段损失逐渐下降的曲线。梯度下降才是更适合这份源码的路线。它把「学习」变成了可视化的迭代过程每次算一次损失求一次梯度更新一次参数。损失函数用均方误差 MSE公式写出来就是所有样本预测误差平方的平均值。梯度是损失函数对各参数的偏导数在向量化实现里就是一步矩阵乘法。梯度下降有一个关键前提特征必须做归一化。如果 RM 的取值范围是 3 到 9TAX 的取值范围是 200 到 700这两个维度上梯度的尺度差出两个数量级更新步长会被大数值特征主导小数值特征几乎学不动。把每个特征减去均值、除以标准差之后所有维度都在同一个尺度上梯度下降才能稳定收敛。2.3 为什么是线性回归而不是决策树或神经网络拿到这份大作业有人会犹豫直接上随机森林或者三层神经网络分数会不会更高从结果看树模型在非线性特征交互上确实可能略好但大作业考察的重点不是刷分而是「你是否理解了一个模型从假设到求解的完整链路」。线性回归的可解释性是最强的每个特征的权重就是它对房价的边际影响你可以指着coef_说出「房间数每增加一个标准差房价大约上升多少」这是树模型和神经网络很难做到的。另一个现实原因是它是最合适的基线模型。先用线性回归跑出一个 RMSE 和 R²再尝试岭回归、多项式回归每一步提升都能验证自己的理解。直接上黑匣子模型万一效果不理想你甚至不知道是数据问题还是模型问题。线性回归把这条调试路径铺得最平。3. 数据准备这步做不好后面全白费3.1 加载数据集并校验先确认数据量、缺失值和特征列训练模型之前先花两分钟搞清楚数据长什么样。不同来源的 CSV 列名大小写不一致与其硬编码列名不如打印出来动态确认。我用 pandas 读入之后连续输出三样东西形状、列名、每列缺失值数量。import pandas as pd df pd.read_csv(housing.csv) print(数据形状:, df.shape) print(列名:, df.columns.tolist()) print(缺失值:\n, df.isnull().sum())这一步的输出结果会直接影响后面的处理shape 确认样本量和特征数是否符合预期列名确认最后一列是不是目标值缺失值统计则决定要不要做填充。波士顿房价数据集本身没有缺失值但换一个来源的 CSV 就未必了。如果发现某几列缺失比例超过 5%最常见的处理是用中位数填充因为中位数对异常值不敏感比均值更稳。我用df.iloc[:, :-1]取特征矩阵、df.iloc[:, -1]取目标值而不是写df[MEDV]的硬编码。原因是不同渠道下载的 CSV目标列可能叫medv、MEDV、PRICE位置索引可以绕开命名差异。X df.iloc[:, :-1].values y df.iloc[:, -1].values print(X.shape, y.shape).values把 DataFrame 转成 NumPy 数组后面做矩阵乘法直接可用。这一步转换经常被省略结果手写梯度下降时报数据类型错误回头检查才发现传进去的是 DataFrame。3.2 特征归一化与训练/测试划分三个必写步骤模型训练前有三步是固定动作特征缩放、划分训练集和测试集、把特征转成浮点类型。Python 里整数特征参与梯度计算时经常出现类型溢出先统一转 float 更省事。import numpy as np 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 scaler.fit_transform(X_train) X_test scaler.transform(X_test)test_size0.2意味着 506 条样本里约 101 条留作测试。random_state42固定随机种子让每次运行划分结果一致这是保证作业可复现的关键。不固定的话每跑一次 RMSE 都不一样你没法判断是模型改好了还是数据划分变了。StandardScaler 会把每个特征变成均值 0、标准差 1。注意第三行用的是fit_transform第四行用的是transform训练集负责拟合均值和标准差测试集直接复用同一组参数转换。这一步是很多人的翻车点后文避坑部分会展开讲。3.3 归一化时最容易犯的错测试集数据泄露有一种错误写法是先对整个数据集做归一化再划分训练集和测试集。看着结果差不多实际上已经造成了数据泄露。测试集的均值、标准差被混进了训练过程模型在评估时「偷看」了测试集的分布信息测试指标会比真实水平乐观一点。正确的顺序是先划分、后缩放。我在上面的代码里已经按这个顺序写了。另一个隐藏问题出在数据集本身的排列顺序上如果原始数据按区域排列直接取前 80% 做训练、后 20% 做测试测试集会集中在特定地理片区评估结果失去代表性。train_test_split默认会随机打乱数据这一步不要关闭。X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleTrue )把shuffleTrue显式写出来既是给自己看也是给答辩老师看你考虑过样本顺序可能带来的偏差并且主动规避了。4. 手写线性回归类再和工具库结果对比验证4.1 基于 NumPy 实现梯度下降的线性回归类手写线性回归类是大作业源码的核心。这一版我用了向量化实现把循环交给 NumPy训练 5000 轮只需要一瞬间。代码不长但每一行的作用都要能讲出来。import numpy as np class LinearRegressionGD: 基于梯度下降的线性回归 参数: lr: 学习率 n_iter: 最大迭代次数 def __init__(self, lr0.01, n_iter2000): self.lr lr self.n_iter n_iter self.theta None # 参数向量第一位是截距 self.loss_history [] # 每轮的损失用于画曲线 def fit(self, X, y): m, n X.shape X_b np.c_[np.ones((m, 1)), X] # 在X前面加一列1对应截距 self.theta np.zeros(n 1) # 全零初始化 for _ in range(self.n_iter): y_pred X_b self.theta error y_pred - y grad (1 / m) * (X_b.T error) self.theta self.theta - self.lr * grad loss (1 / m) * np.sum(error ** 2) self.loss_history.append(loss) return self def predict(self, X): X_b np.c_[np.ones((X.shape[0], 1)), X] return X_b self.thetanp.c_[np.ones((m, 1)), X]是给特征矩阵左边拼接一列全 1这样截距 b 可以被统一收进 theta 向量里矩阵运算不用单独处理。梯度表达式(1/m) * X_b.T error来自 MSE 对参数的求导向量化之后一步算完所有维度的梯度。lr决定每一步走多远归一化后的数据取 0.01 比较稳取 0.1 可能震荡。所有参数全零初始化的做法在线性回归里是安全的因为损失函数是凸函数只有一个全局最小值从零开始和从随机数开始最终落到同一处。这一点和神经网络截然不同答辩时常被问到提前想清楚。4.2 使用工具库线性回归验证手写结果手写代码的意义在于理解原理但结果对不对需要对照。scikit-learn 的LinearRegression用了最小二乘闭式解精度更高、没有收敛问题是标准答案。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score # 手写模型 gd_model LinearRegressionGD(lr0.01, n_iter2000) gd_model.fit(X_train, y_train) manual_pred gd_model.predict(X_test) # 工具库模型 lr LinearRegression() lr.fit(X_train, y_train) sk_pred lr.predict(X_test) # 统一用 RMSE 和 R2 衡量 manual_rmse np.sqrt(mean_squared_error(y_test, manual_pred)) sk_rmse np.sqrt(mean_squared_error(y_test, sk_pred)) print(f手写模型 RMSE: {manual_rmse:.4f}) print(f工具库 RMSE: {sk_rmse:.4f}) print(f工具库 R2: {r2_score(y_test, sk_pred):.4f})运行结果里手写模型和工具库的 RMSE 应该非常接近差距在 0.1 以内才正常。差距过大说明某一环节有问题最常见的是学习率太大没收敛或者迭代次数不够。我一般会把差距小于 0.05 当作手写实现正确的判据。这里有个细节值得说目标值 y 没有参与标准化所以 RMSE 的单位是千美元大约 4 到 5 意味着平均预测误差在 4000 到 5000 美元相对房价中位数 20 多千美元来说已经是不错的结果。R² 一般在 0.7 以上意味着模型能解释七成以上的房价波动。4.3 三张图说清楚模型效果损失曲线、预测对比与残差图代码跑通只是第一步大作业的分数差距往往体现在你是否理解模型的训练过程。三张图足够讲清楚全部故事。import matplotlib.pyplot as plt # 第一张训练损失曲线 plt.figure(figsize(6, 4)) plt.plot(gd_model.loss_history) plt.xlabel(Iteration) plt.ylabel(MSE) plt.title(Training Loss Curve) plt.show() # 第二张真实房价 vs 预测房价 plt.figure(figsize(6, 4)) plt.scatter(y_test, sk_pred, alpha0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--) plt.xlabel(True Price) plt.ylabel(Predicted Price) plt.title(True vs Predicted) plt.show() # 第三张残差图 residuals y_test - sk_pred plt.figure(figsize(6, 4)) plt.scatter(sk_pred, residuals, alpha0.6) plt.axhline(0, colorr, linestyle--) plt.xlabel(Predicted Price) plt.ylabel(Residual) plt.title(Residual Plot) plt.show()损失曲线应该在前几百轮快速下降然后趋于平缓。如果曲线一直在震荡学习率偏大如果还是单调下降但斜率很缓说明迭代次数不够。真实对预测图里点越贴近对角线越好但房价超过 35 千美元后会出现系统性的低估这是线性模型的边界不是 bug。残差图如果出现明显的喇叭形说明预测值越大、误差波动越大这是后续引入岭回归或对数变换的动机。这三张图直接截图进大作业报告比几百字描述有说服力得多。5. 五个避坑指南从数据加载到画图的中文乱码5.1 数据集在本地加载失败现象pd.read_csv(housing.csv)报 FileNotFoundError或者从某个远程地址拉取数据时连接超时。原因数据文件的路径不在当前工作目录或者引用的远程数据源不再提供服务。大作业提交时数据文件是随代码一起打包的路径写死相对路径后换机器就会失效。解决把 CSV 和源码放在同一级目录用os.path.join(os.path.dirname(__file__), housing.csv)这种相对当前文件定位的方式。提交 zip 前先换一个目录结构完整跑一遍确认路径没写死。5.2 梯度爆炸损失直接变成 NaN现象运行不到几轮loss_history里出现 inf再往后全是 nan。原因学习率太大参数更新一步跨过了最优点误差越来越大最终数值溢出另一种可能是特征没有归一化梯度在某些维度上尺度巨大。解决先把lr从 0.01 降到 0.001 重跑一次如果还是 NaN回第 3 章检查StandardScaler是否作用到了训练数据。我调试时习惯在 fit 里打印前三个loss_history值如果第一轮就是超大数字几乎可以断定是学习率或数据尺度问题。5.3 手写结果与工具库结果相差很大现象manual_rmse和sk_rmse相差超过 1甚至手动实现完全收敛不了。原因最常见的是训练集和测试集划分不一致手写模型用了标准化前的数据、工具库用了标准化后的数据其次是梯度公式写错梯度符号方向反了参数在朝损失增大的方向更新。解决把两组模型喂给完全相同的X_train、y_train、X_test先检查数据是否同一份。再看损失曲线是否下降到平台期如果已经平了手写结果必然接近工具库。我一般会在手写模型里加一个断言训练结束后和工具库的coef_直接相减差值小于 0.01 才继续往下走。5.4 matplotlib 画图中文全是方框现象图标题、坐标轴标签里的中文全部显示成空心方块。原因matplotlib 默认字体不含中文字符系统在字体库里找不到对应的字形。解决在画图代码前加上两行配置指定一个系统中文字体。plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus设置为 False 是保证负号显示正常不然坐标轴上的负号也会变方框。这个坑在最关键的残差图里出现率最高图都画好了才发现中文没显示重新渲染一遍浪费时间。5.5 不固定随机种子导致每次跑分不一样现象同一个 notebook 连续跑两次RMSE 和 R² 都不一样浮动有时超过 0.1。原因train_test_split默认随机划分数据没有固定随机种子时每次执行都会重新洗牌。解决random_state42写进划分函数并在代码注释里说明这是刻意为之。另一个隐藏点在于如果用了任何带随机性的库比如特征选择里的随机逻辑也需要固定其random_state。保持结果完全可复现是工程习惯也是答辩时不给自己挖坑的保障。6. 从「能跑」到「像样」源码文件组织与三个加分项6.1 源码文件怎么组织更利于答辩大作业交付的不只是一段能跑的代码还有「别人能不能快速看懂」。我习惯把代码拆成三个文件而不是一个脚本写到底。. ├── housing.csv ├── model.py └── train_and_evaluate.pymodel.py放线性回归类的定义属于可复用代码train_and_evaluate.py负责加载数据、训练、评估和画图。housing.csv和数据文件放在同一目录保证相对路径可移植。这样做最大的好处是答辩时导师问「模型在哪」你能直接指出类定义问「结果怎么来的」你指向评估脚本不会在一个 300 行的文件里翻半天。6.2 三个低成本加分扩展第一个扩展是岭回归对比。在线性回归基础上加 L2 正则化模型会主动压缩不重要的特征权重和普通线性回归的系数放一起对比能讲出「正则化如何抑制过拟合」这一层。代码只有三行。from sklearn.linear_model import Ridge ridge Ridge(alpha1.0) ridge.fit(X_train, y_train)第二个扩展是特征相关性热力图。用df.corr()配合 seaborn 的heatmap输出全部 14 个维度的相关性矩阵报告里放一张图能直观展示哪些特征彼此高度相关为讨论「多重共线性是否影响权重解释」提供素材。第三个扩展是把训练过程跑 5 次每次用不同随机种子统计 RMSE 的均值和标准差。结果稳定说明模型可靠结果波动大则说明数据划分敏感讨论空间更大。最后说个我自己的习惯每次把代码压缩成 zip 交出去之前先在另一台干净的机器上从零跑一遍。大部分大作业翻车不是代码逻辑问题而是环境问题——你本机跑得飞快换一台机器就缺包。把数据和依赖整理好这份源码才算真正交付。希望帮到你。本文还有配套的精品资源点击获取