简介:这是一份基于线性回归实现波士顿房价预测的Python源码大作业,面向机器学习初学者与期末课程设计场景,适合直接提交或二次学习。项目原创完成且已获导师指导并通过,评分为97分,包含梯度下降法(BGD)与小批量梯度下降算法(MBGD)两种实现,覆盖数据导入、训练集测试集划分、归一化、参数初始化、损失函数迭代、测试预测与可视化等完整流程。压缩包共5个文件,主要为2个Python脚本(核心训练与MBGD对比实现)、1张拟合效果图、1份说明文档及1个gitignore配置文件,整体仅122KB,结构精简、运行便捷。已有1455人学习下载,说明该选题具备较高的参考价值。读者可拿到可直接运行的完整代码、损失变化曲线与预测效果可视化,既能理解线性回归的梯度推导,也能借鉴大作业的项目组织方式,适用于课程设计、期末作业或机器学习入门实践。
1. 波士顿房价预测:为什么线性回归是入门大作业的首选
如果你正在找一份能啃得动、又能在答辩时讲明白的Python大作业,基于线性回归实现波士顿房价预测基本是机器学习入门里最稳妥的选题之一。它不需要GPU、不需要海量数据,一个自带的数据集、几十行Python代码就能完成从数据清洗到模型评估的完整闭环,哪怕你只是刚看完Python语法,也能在两天内跑通。这个题目的价值在于:它把线性回归模型、特征工程、训练集测试集划分、回归评估指标这些机器学习的核心概念全部串在了一起,而且结果可解释、可视化好做、答辩不容易被问倒。本文会从原理到代码、从调参到踩坑,把这个大作业完整拆给你,可直接照着写进你的实验报告。
2. 线性回归与波士顿房价:最小二乘原理与3个必调的基础假设
2.1 线性回归不是简单画一条线:最小二乘损失函数的由来
很多教程会把线性回归说成“找一条直线去拟合数据点”,这句话在只有一个特征的二维平面上成立,但在波士顿房价这种13个特征的多维场景里,你拟合的是一个13维空间里的超平面。
模型写出来是这个样子:
price = w0 + w1*CRIM + w2*ZN + w3*INDUS + ... + w12*LSTAT其中w0是偏置项,w1到w12是每个特征的权重。训练过程就是找到一组w,让模型预测的房价和真实房价之间的误差最小。这个误差的度量方式就是损失函数,线性回归最常用的是均方误差(MSE):
MSE = (1/n) * Σ (y_i - y_pred_i)²最小二乘这个名字的由来,就是因为我们要让误差的平方和最小。注意这里用了平方而不是绝对值,不是说平方更“公平”,而是平方函数是凸的、处处可导,可以直接用偏导数为0求出解析解,也可以用梯度下降一步步逼近。对大作业来说,你不需要会手推整个矩阵求导,但至少要能在报告里写出上面这个公式,并说明为什么用平方而不是绝对值。
2.2 波士顿数据集的数据设计逻辑与特征含义
波士顿房价数据集由StatLib库提供,采集的是1970年代波士顿地区506个街区的房价中位数,每个样本有13个特征加一个目标值。这份数据在大学机器学习课程里用了二十多年,虽然因为时代久远存在一些争议,但它作为教学数据最大的优点是:特征类型丰富、样本量适中、分布有规律,适合做线性回归演示。
写作业时,你在报告里至少要讲清楚这几个关键特征:
| 特征名 | 含义 | 对模型的影响倾向 |
|---|---|---|
| CRIM | 城镇人均犯罪率 | 越高房价越低 |
| RM | 平均房间数 | 越高房价越高 |
| AGE | 自住单位比例 | 老房子多房价偏低 |
| LSTAT | 低收入人口比例 | 越高房价越低 |
| MEDV | 房价中位数(目标值) | 以千美元为单位 |
在这13个特征里,RM和LSTAT与房价的相关性最明显,很多大作业会先画出这两个特征与房价的散点图来“证明”线性关系存在。这是个加分的做法,因为答辩老师看到你在建模前做了探索性数据分析(EDA),印象分会完全不同。
2.3 线性回归的3个前提假设与作业里的直观判断方法
在项目报告里,一定要留一小节写线性回归的假设检验。线性回归模型有几个统计前提,虽然不是每一次大作业都会被严格检查,但如果老师问起来,你答不上来就会比较被动。
第一个假设是特征与目标存在线性关系。判断方法很简单:把每个特征和房价的散点图画出来,看是否存在明显的线性趋势;也可以用相关系数矩阵做整体检查。
第二个假设是残差独立且同分布。直观判断是画残差图,如果残差随机分布在零线附近没有明显趋势,就算通过;如果出现喇叭形或曲线形,说明可能存在异方差或非线性关系。
第三个假设是特征之间不存在严重多重共线性。检查方法是计算特征间的相关系数矩阵,或者计算方差膨胀因子(VIF)。波士顿数据里TAX和RAD两个特征相关性很高,这在作业里是一个可以拿出来讨论的优化点。
提示:你不需要用统计学软件去严格验证这三个假设,散点图加残差图的组合已经足够撑起一份本科大作业的理论部分。
3. 载入与预处理:波士顿房价数据从 load_boston 到 DataFrame 的完整流程
3.1 从 load_boston 到 DataFrame:数据载入的两种姿势
波士顿房价数据的载入方式比较特殊,早期常用sklearn.datasets.load_boston,但新版scikit-learn已经因为数据伦理原因移除了这个接口,所以现在项目里更推荐直接读取csv文件。下面这段代码是两种方式都兼容的写法,可以直接放进你的源码:
import pandas as pd import numpy as np from sklearn.datasets import fetch_openml # 方式一:通过sklearn的fetch_openml获取 try: boston = fetch_openml(name='boston', version=1, as_frame=True) df = boston.frame print("fetch_openml 获取成功,共 {} 条样本".format(df.shape[0])) except Exception as e: print(f"fetch_openml 失败: {e},尝试本地CSV路径") # 方式二:本地csv读取 df = pd.read_csv('boston_housing.csv')代码说明:fetch_openml(name='boston', version=1, as_frame=True)会返回一个DataFrame格式的数据,列名是原本的大写形式。这里先尝试在线获取,失败时落到本地csv,保证在无网络的机房也能运行。如果你本地已经有csv文件,直接注释掉fetch_openml那段,用read_csv就够了。
as_frame=True这个参数很关键,它会返回pandas DataFrame而不是numpy数组,方便你用列名操作。拿到数据后第一件事是查看前几行和缺失值:
# 查看数据基本结构 print(df.head()) print(df.info()) print(df.isnull().sum()) # 拆分特征矩阵X和目标向量y # MEDV列是目标值,其余13列是特征 X = df.drop(columns=['MEDV']) y = df['MEDV'] print(X.shape, y.shape)逻辑说明:df.isnull().sum()是检查每个特征列是否有缺失值,波士顿数据本身没有缺失,但这一步是规范流程、必须保留,因为换任何数据集都得先查这一步。drop(columns=['MEDV'])是把目标列从特征中移除,y单独保存目标值。做完这一步你得确认X是 (506, 13) 的形状,如果你打印出来不是这个尺寸,说明数据源或切分有问题,要回头检查列名差异。
3.2 数据切分与样本顺序:训练集测试集划分的4个关键参数
划分训练集和测试集是实现环节里最容易被马虎处理的一步,很多大作业因为乱切数据导致评估结果偏高,答辩时一问就露馅。标准做法是用train_test_split:
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, shuffle=True ) print(f"训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}")参数说明:test_size=0.2表示留出20%的数据做测试,一般作业在0.15到0.3之间选,不要太小,否则评估结果方差大。random_state=42是随机种子,锁死它才能保证每次运行得到相同的划分,这是作业可复现的基础。shuffle=True表示切分前先打乱数据,如果原始数据按地区有序排列、不打乱,训练集和测试集的地域分布会严重失衡。
这里有个细节要特别提醒:切分的时机必须在任何特征缩放或预处理之前,也就是先切分、后标准化。很多人习惯先标准化全量数据再切分,这是数据泄露的典型错误,后面避坑章节会专门展开。
4. 从零实现到调用sklearn:线性回归训练的两条路线与5个可调参数
4.1 手写梯度下降版线性回归:30行代码看清参数的迭代更新
要体现大作业的“含金量”,建议在项目里同时提供两个版本:一个手写梯度下降实现,一个调用sklearn的LinearRegression。手写版本能让你在报告里写清楚“模型是怎么学出来的”,这在答辩中是很大的加分项。
class LinearRegressionGD: def __init__(self, lr=0.01, epochs=1000): self.lr = lr # 学习率 self.epochs = epochs # 迭代次数 self.weights = None self.bias = 0 self.loss_history = [] # 记录每次迭代的损失,用于画收敛曲线 def fit(self, X, y): n_samples, n_features = X.shape self.weights = np.zeros(n_features) for epoch in range(self.epochs): # 前向传播:计算预测值 y_pred = np.dot(X, self.weights) + self.bias # 计算误差 error = y_pred - y # 梯度计算 dw = (1 / n_samples) * np.dot(X.T, error) db = (1 / n_samples) * np.sum(error) # 参数更新 self.weights -= self.lr * dw self.bias -= self.lr * db # 记录损失 loss = np.mean(error ** 2) self.loss_history.append(loss) return self def predict(self, X): return np.dot(X, self.weights) + self.bias逻辑说明:每轮迭代做的就是四件事——前向传播算预测值、计算预测和真实值的误差、由误差反推梯度和偏置的导数、沿负梯度方向更新参数。dw = (1/n_samples) * np.dot(X.T, error)这一步是梯度的矩阵写法,等价于对每个特征分别求偏导,建议你在报告里把对应的数学推导写一遍,手推公式和代码形成对照。
参数说明:lr=0.01是学习率,控制每一步参数更新的步幅,太大会发散,太小收敛慢。epochs=1000是迭代轮数,作业里通常500到2000之间。self.loss_history这个列表很有用,训练结束后画一条损失下降曲线,如果曲线单调下降并趋于平缓,就说明训练过程收敛正常。这个手写版本没有做特征缩放,直接跑波士顿原始数据大概率不收敛或收敛极慢。
4.2 用 sklearn 一行完成拟合:LinearRegression 的5个可调参数
sklearn的线性回归实现用的是最小二乘解析解,即直接通过矩阵运算求出最优权重,不需要迭代。做实验对比时要清楚这一点:sklearn版本的结果可以当作“标准答案”,手写梯度下降版会逼近这个结果,两者差异能体现实现是否正确。
from sklearn.linear_model import LinearRegression model = LinearRegression( fit_intercept=True, copy_X=True, n_jobs=None ) model.fit(X_train, y_train) # 查看模型的系数和截距 print("截距:", model.intercept_) print("各特征系数:", model.coef_)参数说明:fit_intercept=True表示模型需要学习偏置项,也就是公式里的w0,如果你的数据已经做过中心化处理,可以设False,但作业里保持默认即可。copy_X=True表示拟合前复制一份输入数据,避免原地修改,这是安全选项。n_jobs=None在线性回归里实际上不参与并行计算,但写上它可以应对个别平台版本的要求,不代表有用。
在作业报告里,你应该把model.coef_的结果做成一个表,列出每个特征对应的权重,特别是正的权重和负的权重要分别解读。比如RM的房间数系数是正数,表示房间越多房价越高,而LSTAT系数是负数,表示低收入人口比例越高房价越低,这种解读能让老师看到你真的理解了线性回归模型的含义。
4.3 用三个指标证明你的模型靠谱:RMSE、R²与MAE的组合用法
模型训练完不是跑个分数就结束,大作业里至少要包含三个回归指标的计算,并在结果分析里说明它们各自的视角。
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score y_pred = model.predict(X_test) # 计算评估指标 mse = mean_squared_error(y_test, y_pred) rmse = np.sqrt(mse) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"MSE: {mse:.2f}") print(f"RMSE: {rmse:.2f}") print(f"MAE: {mae:.2f}") print(f"R²: {r2:.4f}")指标解读:RMSE是均方根误差,单位直接是房价的单位(千美元),波士顿数据上合理的结果大约在4到6之间,如果这个值超过10,说明模型效果很差。MAE是平均绝对误差,它比RMSE对异常值更稳健,两者一起看能判断预测误差是否被少数极端样本拉高。R²是决定系数,表示模型解释了目标变量多少比例的方差,对于波士顿数据,R²在0.7以上算合格,0.8以上算优秀。
提示:作业最忌讳只放R²不放其他指标,R²是相对指标,RMSE和MAE是绝对指标。答辩时老师问“你的模型误差到底是多少美元”,你如果答不上RMSE就会减分。
5. 波士顿房价大作业的5个常见坑与排查:从数据泄露到随机种子
5.1 标准化翻车:对全量数据先 fit 再切分会造成数据泄露
现象:测试集上的R²异常高,但换一个数据集或重新采样后效果骤降,老师质疑你“分数是不是有问题”。
原因:你在切分之前就调用了StandardScaler对全部数据做了拟合并标准化。fit过程会使用测试集的数据计算均值和方法差,这些统计量相当于“偷看”了测试集的信息,模型评估变得虚高。严格来说这属于数据泄露,是机器学习实验里的原则性错误。
解决:先train_test_split切分,再对训练集fit,然后只对训练集和测试集分别做transform。标准写法是这样:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)注意测试集只有transform没有fit,用的统计量来自训练集。这行代码应该写进你的源码并在报告里专门说明,很多大作业就坏在这一个顺序上。
5.2 随机种子不固定:每次运行结果不一样导致评分不可复现
现象:同一个脚本连续运行三次,每次打印的RMSE和R²都不一样,浮动范围还挺大。
原因:train_test_split在进行随机划分时,如果没有指定random_state,每次运行都会产生一个新的随机排列,训练集和测试集的组成不同,评估结果自然不同。此外,如果你在项目里用了任何涉及随机初始化的模块,同样需要固定种子。
解决:在所有涉及随机性的位置显式设定随机种子。除了train_test_split(random_state=42)之外,可以在脚本开头补一段通用固定:
import numpy as np np.random.seed(42)作业报告里写明“本文所有实验随机种子固定为42”,这句话能直接体现你的实验素养。
5.3 梯度下降不收敛:学习率和特征量纲对迭代的叠加影响
现象:手写梯度下降版在波士顿数据上训练后,loss_history里的数值不降反升,或者降到一定程度就停滞在一个很大的值上。
原因:波士顿房价的13个特征量纲差异巨大,比如CRIM是每千人犯罪率,数值在0到100之间波动,而TAX是房产税率,数值在200到700之间。梯度下降沿着误差曲面更新时,大数值特征对应的梯度非常大,学习率稍大就会震荡发散;学习率调小后,小数值特征又收敛极慢。这就是为什么手写版本必须配合特征标准化。
解决:对特征做标准化后再喂给梯度下降。直接在fit方法里加一步标准化处理,或者在外面用StandardScaler处理后再传入:
scaler = StandardScaler() X_train_std = scaler.fit_transform(X_train) X_test_std = scaler.transform(X_test) model_gd = LinearRegressionGD(lr=0.01, epochs=1000) model_gd.fit(X_train_std, y_train)另一个排查点是学习率,先把学习率降到0.001,观察loss曲线是否变成平滑下降,如果下降仍然发散,再降到0.0001。判断收敛不能只看最终loss,要看整条曲线的形态。
5.4 只看R²不看残差图:正负残差不平衡时模型等于白做
现象:R²在0.75以上,看起来效果不错,但你画出预测值和真实值的散点图后发现,预测值在低价区间普遍偏高、在30千美元以上的高价区间普遍偏低。
原因:线性回归模型对极端值敏感,波士顿房价里存在几个被设限的样本(实际房价被截断在50千美元),这些样本会让模型产生较大的系统性偏差。R²只表示整体方差解释程度,无法反映误差在哪个区间聚集,必须配合残差分析来发现这种结构性问题。
解决:画出残差图并叠加一条零线查看是否均匀分布:
import matplotlib.pyplot as plt residuals = y_test - y_pred plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(y=0, color='red', linestyle='--') plt.xlabel('预测房价') plt.ylabel('残差') plt.title('残差图') plt.show()如果残差点在零线上方和下方数量接近、且随机分布,说明模型假设基本成立。如果出现明显的喇叭形分布,说明预测误差随房价升高而增大,模型对高房价区域的拟合不充分,这需要在报告里专门讨论。
5.5 特征共线性干扰系数解释:TAX和RAD的高相关性问题
现象:模型训练后,某个特征的系数正负方向与常识矛盾,例如犯罪率CRIM的系数变成正数,或者特征重要性不稳定,删掉一个特征后其他特征系数变化很大。
原因:波士顿数据中TAX和RAD两个特征相关系数超过0.9,存在明显多重共线性。当特征之间高度相关时,最小二乘解在特征空间里会被扭曲,单个特征的系数不再具有清晰的因果含义,数值的正负和大小都可能偏离直觉。
解决:在建模前增加一步相关性检查,打印相关系数矩阵,并在报告中说明如何处理:
corr_matrix = pd.DataFrame(X_train).corr() high_corr_pairs = np.where(np.abs(corr_matrix) > 0.8) # 输出相关系数大于0.8的特征对 print(list(zip(high_corr_pairs[0], high_corr_pairs[1])))处理策略有三种:直接删除其中一个特征、用PCA降维代替原始特征、或者保留但报告里注明无法单独解释系数。对本科大作业来说,选择思路为“保留两个特征但说明它们的共线性由原始数据采集方式决定”通常就能过关,关键是让老师看到你知道这个问题存在。
6. 让大作业超出预期:模型诊断与可视化验证的4个收尾动作
第一个收尾动作是画出预测值与真实值的对比散点图,把对角线加上。如果点都紧密分布在对角线附近,说明模型预测和真实值高度一致;如果明显偏离或在某个区间出现弯曲,说明还有非线性成分没被捕捉。这张图比任何指标都直观,答辩加分效果最好。
第二个动作是画出特征的系数条形图。把13个特征的系数从大到小排序后画条形图,用不同颜色标出正负。这张图能直观展示哪些变量拉高房价、哪些拖低房价,配合报告里的系数表格一起看,老师会觉得你的分析有可视化支撑。
第三个动作是做一个训练曲线,横轴是训练样本数量,纵轴是训练集和测试集的R²。如果训练集分数高、测试集分数低,说明模型过拟合;如果两者都很低,说明模型欠拟合。波士顿数据上线性回归通常是轻度欠拟合,这是线性模型的正常表现,你要在报告里主动说明这一点,并给出尝试多项式特征作为改进方向的思路。
第四个动作是计算一次交叉验证的完整得分,不是单独跑一次train_test_split就结束。用5折交叉验证跑5次,输出每次的R²和平均值,这个平均分比单次划分得出的分数可信得多。如果5次分数标准差很大,说明样本量不足或数据分布不均,这也是值得写进报告的分析点。
我做完这个项目最大的教训是:写代码只用了半天,写报告却花了三天,其中大部分时间在补画图和文字解释。最后跟朋友交流时也发现,大家的模型分数其实都差不多,拉开差距的地方全在数据可视化和对误差的分析深度。只要把第5章的5个坑全部避开,再把本章的4个收尾动作做完整,这份基于线性回归的波士顿房价预测大作业就是一个能拿去答辩的好作品。希望帮到你。
本文还有配套的精品资源,点击获取