简介:面向机器学习课程设计场景的Python大作业,基于线性回归实现波士顿房价预测,完整覆盖数据导入、训练集与测试集划分、数据归一化、模型训练、损失曲线绘制、测试集预测评估及可视化等环节,并额外实现了小批量梯度下降算法,适合初学回归任务的学生参考与复用。资源包共5个文件,包含两个Python脚本、一张数据拟合效果图、一份说明文档和gitignore配置,压缩包仅122KB,结构精简清晰。代码采用梯度下降法(BGD)优化线性回归模型,通过np.concatenate合并截距与权重参数,经epochs次迭代更新参数,并画出损失函数随迭代次数的变化曲线以观察梯度下降执行情况。项目已获导师指导并通过,期末大作业评分为97分,目前已有1453人学习下载,完整确保可运行,可作为课程设计和期末大作业直接使用,下载即用无需修改。
1. 线性回归与波士顿房价:为什么这份97分大作业值得拆开看
从boston.csv里读506条样本,用13个特征去预测房价中位数MEDV,是机器学习入门最有名的基准题之一。这份python源码大作业没有调sklearn.linear_model,而是用批量梯度下降(BGD)从零实现线性回归,覆盖读数据、切分训练集、归一化、手写梯度更新、画损失曲线、测试集评估和拟合可视化,是一个完整的训练闭环。项目拿到了97分,能直接作为课程设计和期末大作业提交,但比"能运行"更值钱的是它把梯度下降的每一步都摊开在代码里。适合两类人:一是要交作业、需要快速跑通改参数的学生;二是想弄懂w = w - lr * grad这行代码背后矩阵维度怎么对齐的入门者。
2. BGD梯度下降与归一化:训练前必须做对的两件事
2.1 线性回归的矩阵形式与损失函数
线性回归假设目标值y是特征x的线性组合。对波士顿房价这份数据来说,特征有13维,模型要学的是一组权重加上一个截距。把截距和权重合并,用np.concatenate在特征矩阵第一列拼上全1,样本矩阵从(m, 13)变成(m, 14),预测公式就统一成:
f(x) = X @ w也就是一次矩阵乘法完成所有样本的预测。损失函数用均方误差的改良版本:
J(w) = 1 / (2m) * ||Xw - y||²前面多出的1/2不是多余的,求导后平方项的指数会乘到系数上,正好和1/2约掉,梯度表达式写出来干净很多。LinearRegression_1.py的整个训练循环都是围绕这个公式展开的,只要在纸上把X的形状画成(506, 14)、w画成(14,),后面所有维度问题都能推出来。
2.2 特征归一化代码:Min-Max还是Z-Score
波士顿数据里TAX特征取值从一百多到七百多,RM只有几到十几,量纲差距接近两个数量级。如果不做归一化,梯度在各特征方向上的步长差别巨大,损失曲线会呈锯齿状来回跳动,收敛极慢。常见的做法是Min-Max归一化,把每列特征缩放到0到1之间:
def normalize(x): x_min = x.min(axis=0) x_max = x.max(axis=0) return (x - x_min) / (x_max - x_min), x_min, x_maxaxis=0表示按每一列特征分别计算最小值与最大值,而不是在整个矩阵上取一个标量。返回的x_min和x_max必须保留下来,因为测试集进入模型前要用同一组参数做变换,而不是重新算一遍。这份源码的逻辑是先归一化特征矩阵X,目标值MEDV保持原始房价数值,这样后续评估R2、RMSE时数字直观。如果目标值也被压缩到0到1,计算出的误差就没有物理含义了。Z-Score归一化在类似作业里也很常见,每列减去均值除以标准差,对存在异常值的列更稳,但波士顿数据分布相对规整,Min-Max够用。
注意:归一化参数只能从训练集上计算。如果先对全量数据归一化再切分,测试集信息会渗入训练过程,这就是所谓的数据泄漏,答辩时老师经常在这里追问。
2.3 BGD、SGD与MBGD的选型对比
项目主推BGD,原因是这个数据集只有506行样本,全量计算一次梯度的矩阵乘法开销完全可以接受。三种梯度下降策略各自的特点整理如下:
| 策略 | 每次更新使用样本 | 收敛稳定性 | 计算开销 | 适用场景 |
|---|---|---|---|---|
| BGD | 全部样本 | 最稳定,损失单调下降 | 每轮最高 | 小数据集、课程作业 |
| SGD | 1个样本 | 震荡大,但可能跳出局部极小 | 每轮最低 | 在线学习、大数据集 |
| MBGD | batch_size个样本 | 折中 | 折中 | 大多数实际训练任务 |
BGD的缺点是每轮遍历全部样本,更新方向最准,但迭代速度慢;SGD用单样本算梯度,噪声偏大,损失曲线会上下抖动。这份作业末尾补了MBGD实现,本质上就是在这张表里取中间档,后面第5章会专门讲改造方法。选型没有绝对好坏,样本量、算力、收敛要求共同决定选哪一档。
3. LinearRegression_1.py:从csv读取到BGD训练闭环的完整拆解
3.1 boston.csv的特征字段与读取方式
数据文件是boston.csv,506行、14列。前13列依次是CRIM(犯罪率)、ZN(住宅占地比)、INDUS(非零售商业用地比)、CHAS(是否临河)、NOX(氮氧化物浓度)、RM(平均房间数)、AGE(老房占比)、DIS(到就业中心距离)、RAD(高速可达指数)、TAX(房产税率)、PTRATIO(师生比)、B(非裔人口比例换算值)、LSTAT(低收入人群占比),最后一列MEDV是房价中位数,单位是千美元。
sklearn在新版本已经移除了load_boston()接口,所以课程大作业普遍改用本地csv文件落地读取逻辑。读入数据后先做一次基础探查,用data.describe()看每列的值域分布,重点确认CRIM和TAX这类量纲特别大的列,它们直接影响归一化方式和训练收敛速度。
import pandas as pd data = pd.read_csv('boston.csv') X = data.iloc[:, :-1].values # 前13列特征 y = data.iloc[:, -1].values # 最后一列MEDV房价iloc[:, :-1]选中除最后一列外的所有列组成特征矩阵,iloc[:, -1]单独取目标值。这里直接用values转成numpy数组,是为了后续矩阵乘法和np.concatenate操作更顺手,省去pandas索引的开销。
3.2 train_test_split与归一化的执行顺序
先切分后归一化是这个项目最容易被抄错的顺序,正确代码如下:
from sklearn.model_selection import train_test_split train_x, test_x, train_y, test_y = train_test_split( X, y, test_size=0.2, random_state=42 ) train_x, x_min, x_max = normalize(train_x) test_x = (test_x - x_min) / (x_max - x_min)test_size=0.2表示留出约20%的样本做最终预测验证,random_state=42固定随机种子,保证作业每次跑出来的划分结果一致。如果去掉这个参数,每次运行切分不同,模型的评估分数会有几个百分点的随机波动,报告中写出的R2就不可复现了。
测试集归一化必须复用训练集算出的x_min和x_max,这一步就是前面提到的防泄漏关键。有些实现图省事,把测试集单独调用一次normalize(),模型输入的分布被人为改变了,评估结果会失真,这是代码评审里能看到的最典型扣分项。
3.3 训练函数:np.concatenate合并截距与权重
BGD_4.py里最核心的是训练函数,骨架如下:
def model(train_x, train_y, lr=0.01, epochs=1000): m, n = train_x.shape ones = np.ones((m, 1)) X = np.concatenate((ones, train_x), axis=1) # 第一列全1对应截距 w = np.zeros(n + 1) J_history = [] for epoch in range(epochs): f_x = X @ w # 前向计算,维度(m,) loss = 0.5 * np.mean((f_x - train_y) ** 2) grad = (1 / m) * X.T @ (f_x - train_y) # 梯度,维度(n+1,) w = w - lr * grad J_history.append(loss) return w, J_historynp.concatenate((ones, train_x), axis=1)在列方向拼接,把截距和13个特征的权重合并成14维向量w。循环内每个epoch做四个操作:求f_x、求损失J(w)、求梯度、更新参数。X.T @ (f_x - train_y)是X^T(Xw-y)的矩阵形式,得到维度(14,)的梯度向量,和w逐元素对齐。
关键参数有三个。lr=0.01是相对保守的起步学习率,epochs=1000比实际需求略多,用来观察损失曲线走完后是否还有下降趋势。w = np.zeros(n+1)从全零初始化,对线性回归这种凸问题没有局部极小点风险,全零起步和随机初始化最终收敛结果基本一致。如果损失出现nan,优先检查lr是不是超过0.1,或归一化步骤是否被跳过。
| 参数 | 作用 | 推荐值 | 调参方向 |
|---|---|---|---|
| lr | 控制每次更新的步长 | 0.01 | 损失震荡时调小 |
| epochs | 训练轮数 | 1000 | 曲线未平缓时调大 |
| test_size | 测试集比例 | 0.2 | 数据量大时可降至0.3 |
3.4 预测时别忘了补全1列
测试集预测是另一个高频踩坑点。训练时X已经拼接成14列,预测时直接拿归一化后的test_x和w相乘会报维度不匹配,需要再补一次全1列:
test_X = np.concatenate((np.ones((test_x.shape[0], 1)), test_x), axis=1) y_pred = test_X @ w常见错误是调试sklearn模型习惯了,忘了手写模型的特征维度和权重维度必须人工对齐。排查这类问题,先把w.shape和test_X.shape打出来,确认两者第一维一致。y_pred得到的是测试集预测房价,单位是千美元,画图和计算误差时都直接用它。
4. 损失曲线与R2评估:测试集预测效果怎么看
4.1 损失函数曲线的收敛迹象
训练循环里每次迭代都把loss追加进J_history,画图只需两行:
plt.plot(range(len(J_history)), J_history) plt.xlabel('epochs') plt.ylabel('loss')观察点集中在三处:曲线是否平滑下降;后期是否趋于水平;是否出现周期性震荡。lr过大时损失先降后升或来回反弹,lr过小时下降像直线一样平缓,1000轮结束还没走到谷底。BGD全量更新,理论上损失每轮都在下降,一旦看到反弹就可以怀疑数值溢出、学习率太大或归一化没有执行。还可以对纵轴做对数坐标,把前几轮的下降细节放大,更直观地判断是否进入平缓区。
4.2 评估指标:R2、RMSE、MAE
测试集评估集中在代码后半段,核心逻辑如下:
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error r2 = r2_score(test_y, y_pred) rmse = np.sqrt(mean_squared_error(test_y, y_pred)) mae = mean_absolute_error(test_y, y_pred) print(f"R2={r2:.3f}, RMSE={rmse:.3f}, MAE={mae:.3f}")R2是最常被单独汇报的数字,表示模型解释目标方差的比例。这个数据集上,纯手写BGD加Min-Max归一化,R2落在0.68到0.80之间都属于正常水平,不必迷信网上那些0.85以上的截图,很多是特征工程或交叉验证做得更深的成果。RMSE和房价保持相同单位,能直观说明平均预测错几千美元,MAE则对极端值更不敏感。三个指标一起报更能说明模型状态:
| 指标 | 含义 | 建议 |
|---|---|---|
| R2 | 解释方差比例 | 0.7以上即及格 |
| RMSE | 均方根误差 | 与房价单位一致 |
| MAE | 平均绝对误差 | 比RMSE更稳健 |
4.3 拟合效果可视化
作业要求里的"展示数据拟合效果",最常见的是预测值与真实值的散点图加对角线:
plt.scatter(test_y, y_pred, alpha=0.6) plt.plot([test_y.min(), test_y.max()], [test_y.min(), test_y.max()], 'r--')对角线是理想状态,点越贴近红线预测越准。波士顿数据在两端,即房价低于10千美元和高于40千美元的区域,通常会明显偏离对角线,因为线性模型天然会压缩极值。散点图整体偏移时,先检查归一化参数是否在测试集上被错误地重新计算;出现水平带状分布时,检查y_pred是否被意外截断或四舍五入过。用plt.colorbar给点按RAD或LSTAT染色,还能看出模型在哪些特征区间预测偏差大,这一手在答辩展示里很加分。
5. 从BGD到MBGD:减少震荡与稳定收敛的进阶实现
5.1 小批量梯度下降的代码改造
BGD每轮用全部样本算一次梯度,稳定但循环次数多。MBGD把整批样本切成小份,每份算一次梯度,改造后代码如下:
def mbgd(train_x, train_y, lr=0.01, epochs=200, batch_size=32): m, n = train_x.shape X = np.concatenate((np.ones((m, 1)), train_x), axis=1) w = np.zeros(n + 1) for epoch in range(epochs): idx = np.random.permutation(m) # 每轮打乱样本顺序 for i in range(0, m, batch_size): batch_idx = idx[i:i + batch_size] X_b, y_b = X[batch_idx], train_y[batch_idx] grad = (1 / len(batch_idx)) * X_b.T @ (X_b @ w - y_b) w = w - lr * grad return wnp.random.permutation(m)把样本索引打乱,防止样本固定顺序带来的梯度偏差,是MBGD和SGD共用的关键操作。内层循环每次取batch_size个样本计算梯度,506条样本、batch_size为32时,一个epoch内有约16次参数更新。直接用BGD的epochs=1000跑MBGD会导致更新总次数显著膨胀,所以epochs要同步调小到200附近。
5.2 学习率与batch_size的搭配
MBGD调参时,我一般按下面这组组合起步:
| 组合 | 学习率 | batch_size | 表现 |
|---|---|---|---|
| 稳妥 | 0.01 | 32 | 收敛稳定,震荡小 |
| 激进 | 0.05 | 16 | 收敛快,后期轻微震荡 |
| 保守 | 0.005 | 64 | 训练慢,接近BGD行为 |
batch_size越小梯度噪声越大,学习率要相应调小;batch_size越大越接近BGD,学习率可以适当放大。最快的调参路径是先固定lr=0.01,依次试16、32、64三档batch_size,观察损失曲线选震荡最小的档位,再微调lr。绘制损失曲线时,MBGD的单轮loss会有波动,可以把每50轮的均值额外画一条平滑线,看总趋势比盯单次值有效。
5.3 答辩和报告中能直接用的两个验证技巧
第一个技巧:训练时把归一化参数存到npz文件,预测还原时直接用。作业环境里模型训练和预测往往在同一个进程,但如果要演示加载已有模型,归一化参数必须持久化:
np.savez('norm_params.npz', x_min=x_min, x_max=x_max)测试时读取后先还原y_pred,再和test_y做差,得到的误差单位是千美元,比归一化空间里的抽象数字更容易向老师解释。
第二个技巧:MBGD受随机打乱影响,每次跑出的损失曲线不完全重合。报告中说明这一特性,并贴出同一组参数跑三次的曲线叠加图,证明收敛趋势稳定,比只贴一次结果更能体现对梯度下降原理的理解。这两处改动都很小,却是评分里"工程完整度"最常见的加分点来源。
本文还有配套的精品资源,点击获取