news 2026/9/11 3:47:21

线性回归实战:波士顿房价预测Python源码与原理详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线性回归实战:波士顿房价预测Python源码与原理详解

简介:这是一份基于线性回归实现波士顿房价预测的Python源码大作业项目,适合机器学习初学者、课程设计及期末大作业场景。项目采用梯度下降法(含批量梯度下降BGD与小批量梯度下降MBGD)优化线性回归模型,完整覆盖数据导入、训练集与测试集划分、归一化、参数初始化、损失计算、梯度更新、损失曲线绘制、测试集预测评估及结果可视化等环节,代码结构清晰,可直接运行使用。

资源包共5个文件,包含2个Python源文件、1张效果图、1份Markdown说明文档及配置文件,压缩包总大小122KB,体积小巧便于下载与二次修改。该作业已获导师指导并通过,是获得97分的高分项目,目前已有1453人学习,可作为课程设计或期末大作业的可靠参考,帮助深入理解线性回归与梯度下降的核心实现流程。

1. 波士顿房价预测大作业:线性回归考的不是调包,是把原理说清楚

看到"基于线性回归实现波士顿房价预测的 python 源码大作业"这个题目,先别急着解压。波士顿房价数据集是机器学习入门阶段出镜率最高的回归数据,线性回归又是第一个被要求从原理到代码都讲得出来的算法。真正拉开差距的,不是谁能跑出一个数字,而是谁能把数据集含义、训练流程、评估方式和参数选择依据讲清楚。很多人把 load_boston 一调、model.fit 一敲就交,最后分数并不理想——在线性回归这个粒度上,核心代码只有十几行,剩下的差距全在数据理解、实验设计和复现性上。下面按一份典型大作业的路径走:线性回归原理、波士顿房价数据加载、NumPy 手写实现、评估优化,以及交作业前的自检。熟悉 sklearn 的直接看参数表和排错点,新手从第 2 章跟着走。

2. 线性回归原理与波士顿房价数据集的加载细节

线性回归是被讲得最熟、却最容易被讲错的一个算法。大作业源码里的 fit 函数往往只有几行,真正决定分数的是对模型假设、数据集来源和预处理顺序的理解。这一章先把原理收拢成"假设函数、损失函数、求解方式"三个点,再落到波士顿房价数据集的字段含义和不同环境下的加载代码。

2.1 线性回归的假设函数与损失函数:为什么房价适合用它

线性回归假设目标值是特征的加权和:

y_pred = w1*x1 + w2*x2 + ... + w13*x13 + b

w1 到 w13 是每个特征对应的权重,b 是偏置。波士顿房价数据集的 13 个特征大多是连续数值型,目标 MEDV 的单位是千美元。这个数据集适合线性回归的根本原因,是大部分特征与房价的关系近似单调:RM(平均房间数)越高房价越高,LSTAT(低收入人口比例)越高房价越低,CRIM(犯罪率)越高房价越低。这样的关系能被一组权重大致描述,不需要依赖复杂的非线性结构。

损失函数衡量预测偏离真实值的程度,线性回归默认选均方误差:

J(w, b) = (1 / m) * Σ (y_pred_i - y_i)^2

m 是样本数,计算梯度时对 w 求偏导,得到:

grad_w = (2 / m) * X^T * (Xw + b - y)

梯度下降就是反复执行w = w - lr * grad_w,lr 是学习率。把这段推导写进实验报告的价值,比几行 sklearn 代码大得多,因为助教追问的往往是"为什么梯度方向是这个表达式"。另一种求解方式是正规方程w = (X^T * X)^(-1) * X^T * y,它能一次算出解析解。我一般建议大作业里两条路都写:报告里用正规方程验证手写梯度下降的结果是否一致。

2.2 波士顿房价数据集 13 个特征的含义与相关性直觉

加载数据之前,先要把字段含义和它跟房价的关系方向装进脑子,否则后面做特征工程就是瞎试。波士顿房价数据集一共 506 条样本,14 列,前 13 列是特征,最后一列 MEDV 是目标值。

字段含义与 MEDV 的大致关系
CRIM城镇人均犯罪率负向
ZN超过 25000 平方英尺住宅用地比例弱正向
INDUS非零售商业用地比例负向
CHAS是否临查尔斯河(0/1)正向
NOX氮氧化物浓度负向
RM平均房间数强正向
AGE1940 年前老房比例负向
DIS到就业中心的加权距离弱正向,与传统直觉相反
RAD径向高速公路可达性指数负向
TAX每 1 万美元的房产税率负向
PTRATIO师生比负向
B社区结构相关比例指标有争议,不做因果解读
LSTAT低收入人口比例强负向
MEDV房价中位数(千美元)目标值

这里面最容易出问题的有两点。一是 DIS 是正向,说明波士顿就业中心周围并不是房价最高区,这种反直觉结论写进报告反而是加分项。二是 B 字段是这份数据集被争议的源头,scikit-learn 因此从 1.2 版本开始移除了它,报告里写一句"保留该字段但不做因果解读"就够了。

2.3 sklearn 1.2 之后 load_boston 没了:源码里该怎么加载

以前写大作业最常见的开头是from sklearn.datasets import load_boston,但 scikit-learn 1.2 版本起这个接口被移除,新装的 Python 环境会直接 ImportError。大作业源码如果只有这一种加载方式,在助教的新环境里跑不起来,这是非常冤的扣分点。

常见做法是写一个兼容加载函数:优先用 load_boston,失败就改读本地 CSV。原始数据集在 UCI 仓库里的文件名是 housing.data,无表头、按空格分隔;很多课程会预先转成带表头的 housing.csv,两种都要能读。

import numpy as np import pandas as pd COLUMNS = ["CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT", "MEDV"] def load_boston_df(): try: from sklearn.datasets import load_boston data = load_boston() df = pd.DataFrame(data.data, columns=data.feature_names) df["MEDV"] = data.target return df except ImportError: pass try: return pd.read_csv("housing.csv") except FileNotFoundError: return pd.read_csv("housing.data", delim_whitespace=True, header=None, names=COLUMNS)

COLUMNS 的 14 个名字必须和数据文件列顺序一一对应,这是手写数据加载时最常见的错位来源。data.feature_names返回的 13 个字段名不带 MEDV,所以要单独把目标值塞进 DataFrame。delim_whitespace=True是 pandas 读空格分隔文件的写法,老版本用sep=r"\s+",两种等价。如果课程环境是 sklearn 1.1 及以下,load_boston 仍旧可用,但不建议源码里只留这一条路,兼容分支加上不会错。

3. 用 NumPy 手写线性回归,跑通波士顿房价训练与预测

这一章进入源码实现。大作业如果没有强制禁止 sklearn,我仍然建议手写一个梯度下降版本,因为这是整份作业里唯一能真正体现你理解算法的地方。训练、预测、评估三段各司其职,下面按数据预处理的正确顺序来。

3.1 先划分再归一化:防止数据泄漏的正确顺序

波士顿房价特征量纲差得非常大:CRIM 是零点几的小数,TAX 是几百,RM 在 3 到 9 之间。梯度下降对量纲敏感,所以必须先标准化。但标准化有顺序讲究——只能把 scaler 在训练集上 fit,再用它 transform 测试集。如果先把全量数据标准化再划分,测试集的信息已经渗入训练过程,这叫数据泄漏,得到的 R² 会虚高。

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df.drop(columns=["MEDV"]).values y = df["MEDV"].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)

test_size=0.2 意味着 506 条样本里有约 101 条用于测试,训练集 405 条,样本量小,一次随机划分即可。random_state=42 不是魔法数字,它的作用是让 train_test_split 的随机划分可复现,两次运行得到同一份训练集。这个参数不写,每次运行结果都不同,后面的实验对比就没法做了。fit_transformtransform的区别要能讲出来:前者计算均值方差并应用,后者只应用已学到的均值方差,测试集永远不参与计算。

提示:把 scaler 的 fit 写在数据划分之后,是这份作业里最容易被忽略却最致命的顺序错误。

3.2 手写梯度下降版线性回归的最小源码

标准化完成后,写一个只依赖 NumPy 的线性回归类。类的接口跟 sklearn 对齐,后续换 Ridge、Lasso 时评估代码不用改。

class LinearRegressionGD: def __init__(self, lr=0.1, epochs=1000): self.lr = lr self.epochs = epochs self.w = None self.b = 0.0 self.loss_history = [] def fit(self, X, y): m, n = X.shape self.w = np.zeros(n) self.b = 0.0 self.loss_history = [] for _ in range(self.epochs): y_pred = X @ self.w + self.b error = y_pred - y grad_w = (2 / m) * (X.T @ error) grad_b = (2 / m) * error.sum() self.w -= self.lr * grad_w self.b -= self.lr * grad_b self.loss_history.append(np.mean(error ** 2)) return self def predict(self, X): return X @ self.w + self.b model = LinearRegressionGD(lr=0.1, epochs=2000) model.fit(X_train, y_train)

X @ self.w是向量化矩阵乘法,一次算完所有样本的预测值,比 for 循环快两个量级。error 是预测减真实值,grad_w 的表达式正是 2.1 节推导的偏导结果,(2 / m)是求平均后再乘 2。loss_history 每一轮记录当前 MSE,后面画损失曲线全靠它,建议一定保留。权重用 np.zeros 初始化而不是随机数,在线性回归的凸损失下,零初始化不会带来对称性问题。

如果想验证梯度下降没有写错,用正规方程做一次交叉验证:

Xb = np.column_stack([np.ones(len(X_train)), X_train]) w_solve = np.linalg.pinv(Xb.T @ Xb) @ Xb.T @ y_train w_compare = np.hstack([model.b, model.w]) print(np.max(np.abs(w_solve - w_compare)))

np.linalg.pinv是伪逆,当 X^T*X 接近奇异时比 inv 更稳健。正规方程对 13 维特征几乎是瞬时的,它存在的意义不是更快,而是给手写梯度下降一个基准答案。两组结果如果差异在 1e-4 以内,说明梯度实现正确。注意正规方程里也带偏置项,所以要在特征矩阵前拼一列 1,这一列拼在训练集上,而不是拼到原始 X 上。

3.3 学习率、迭代次数、随机种子三个参数的调法

手写版本里有三个参数影响最终结果,交作业前最好都做一组小实验并写进报告。

参数常见取值现象与处理
学习率 lr0.01 / 0.1 / 1.0loss 发散或震荡时除以 10,收敛太慢时乘以 10
迭代次数 epochs500 / 1000 / 2000看 loss 曲线是否进入平台,进入后继续迭代无收益
随机种子 random_state42 固定不固定则每次划分不同,结果不可复现

学习率是最先调的参数。标准化之后特征都在 0 附近,lr=0.1 通常能正常收敛;如果 loss_history 里出现数值越来越大,一定是学习率太大,先降到 0.01 再跑一次。迭代次数不用贪,跑完后打印 loss_history[-1] 和前几十轮的差,差值小于 1e-3 就说明收敛。有人会把 epochs 设到 100000 追求极小 loss,在 506 条样本上不会过拟合到这个程度,但会让作业显得不懂早停。随机种子影响的是划分而不是模型本身,确定后必须固定,报告里写"所有实验统一使用 random_state=42"这一句就够。

4. 评估与优化:让波士顿房价预测的 R² 从 0.7 提到 0.85

纯线性回归用全特征在波士顿数据集上的 R² 通常在 0.7 上下,这个成绩不算差,但可以明确提升。提分的关键不是换模型,而是三点:指标选对、用正则化处理共线性、做两个特征层面的小改动。

4.1 用 MSE、RMSE、MAE、R² 四个指标给模型打分

评估代码统一封装,训练集和测试集都算一遍,测试集指标才是报告里的主数据。

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"RMSE={rmse:.2f} MAE={mae:.2f} R2={r2:.4f}")

四个指标各有各的读法,报告里要写清楚为什么用它们,而不是只贴数字。

指标单位说明适用场景
MSE千美元²平方后的误差,大误差被放大梯度下降的损失函数本身
RMSE千美元和房价同量纲,能直读主指标,报告首推
MAE千美元对异常值不敏感看典型误差水平
模型解释的方差比例横向对比模型好坏

RMSE 是这份作业里最适合当主指标的:它的单位和 MEDV 一致,比如 RMSE=4.5 可以直译成"平均预测偏差约 4500 美元"。MAE 通常比 RMSE 小,因为它不被极端样本放大,两个指标一起报,才能说明误差分布是否均匀。R² 在 0.7 左右意味着模型解释了约七成方差;如果出现负数,说明模型比直接预测均值还差,优先查数据泄漏而不是换模型。测试集指标比训练集差是正常的,两者 R² 差值超过 0.2 就要警惕过拟合。

4.2 加正则化处理特征冗余:Ridge 与 Lasso 怎么选

波士顿数据的特征之间有多重共线性:TAX 和 RAD 高度相关,INDUS 和 NOX 也明显正相关。共线性会让普通最小二乘的系数估计方差变大,表现为训练集上漂亮、测试集上抖动。正则化通过对权重加惩罚来压缩系数,是应对这个问题的标准手段。

from sklearn.linear_model import RidgeCV, LassoCV ridge = RidgeCV(alphas=np.logspace(-3, 3, 50)) ridge.fit(X_train, y_train) print(f"Ridge best alpha={ridge.alpha_:.2f}, " f"R2={r2_score(y_test, ridge.predict(X_test)):.4f}") lasso = LassoCV(alphas=np.logspace(-3, 2, 50)) lasso.fit(X_train, y_train) print(f"Lasso best alpha={lasso.alpha_:.2f}, " f"R2={r2_score(y_test, lasso.predict(X_test)):.4f}")

RidgeCV 和 LassoCV 都会把 alpha 的候选网格交给交叉验证选择,不用手工试参。np.logspace(-3, 3, 50)表示从 0.001 到 1000 按对数均匀取 50 个值,覆盖三个数量级,比手写几个固定 alpha 更省事也更客观。Ridge 的惩罚是 L2,会把系数整体压缩但不会变成 0;Lasso 的惩罚是 L1,会把不重要的特征系数压成精确的 0,适合同时做特征选择。大作业里两者都跑一遍,报告写一行结论:"Ridge 在测试集上的 RMSE 略优于 Lasso,说明数据更倾向保留全部特征而非稀疏化"。alpha 越大惩罚越重,模型越接近水平线;alpha 趋近 0 时退回普通最小二乘,这个趋势可以画进报告做对比图。

4.3 两个低成本提分操作:LSTAT 对数化与 CHAS 哑变量处理

在"仍然叫线性回归"的前提下,通过变换特征让模型与数据更吻合,是这份作业里性价比最高的提分操作。第一个是 LSTAT 的分布问题:原始 LSTAT 右偏严重,和 MEDV 的关系呈明显曲线,直接进线性模型会损失精度,取对数后关系更接近直线。第二个是 CHAS 本身已经是 0/1 哑变量,不需要再 one-hot,某些写法会把它拆成两列,反而造成冗余共线性。

def add_features(df): df = df.copy() df["LSTAT_log"] = np.log1p(df["LSTAT"]) df["RM_sq"] = df["RM"] ** 2 return df feature_names = COLUMNS[:-1] df_train = add_features(pd.DataFrame(X_train, columns=feature_names)) df_test = add_features(pd.DataFrame(X_test, columns=feature_names)) model2 = LinearRegressionGD(lr=0.1, epochs=2000) model2.fit(df_train.values, y_train)

np.log1p是 log(x+1),x 为 0 时结果仍是 0,不会出现负无穷。RM_sq 是给提分最明显的特征加一个平方项,模型对 RM 的响应从直线变成抛物线,能抓住"房间数从 3 加到 5 的边际收益,远大于从 7 加到 9"这一真实规律。严格说这已经是特征工程,但参数仍然是线性的,所以还叫线性回归,报告里如实写"加入二次项"即可。这里直接在标准化后的 X 上做变换,数值含义和原始尺度不同,但不影响模型能力;如果报告想做严谨的特征工程版本,应该把 add_features 放在标准化之前,并和 scaler 一起封装成 pipeline。注意 add_features 要同时作用于训练集和测试集,新增列的位置要一致,否则 predict 时特征顺序错位,会得到完全离谱的结果。做完这三步,R² 从 0.7 提到 0.85 在波士顿数据上是正常水平,前提是测试集本身没被动过。

5. 交作业前自检:复现实验、残差图与实验报告的论证写法

最后一步不是写代码,是验代码。下面三个自检在交 zip 之前各跑一遍,能挡住大部分"本地能跑、助教环境崩"和"结果对不上"的翻车。

5.1 数值自检与复现性验证:手动重算一条预测

取测试集第一条样本,手工计算预测值,和 predict 接口的结果做差,误差应为 0。

i = 0 manual = X_test[i] @ model.w + model.b pred = model.predict(X_test[i:i + 1])[0] np.testing.assert_allclose(manual, pred, rtol=1e-6)

assert_allclose 会直接抛异常,比 print 肉眼对比更可靠,也说明 w 和 b 确实参与了预测,而不是模型内部另有逻辑。复现性验证同理:固定 random_state 后重新跑一遍完整训练流程,把两次得到的 w 数组做 assert_allclose,精度 1e-4 以内即可放行。这个小断言放进源码的 main 分支里,本身就是作业完整性的证明。

5.2 图形自检:损失曲线和残差图必须能解释

作业报告里放两张图就够:损失曲线体现训练过程,残差图体现模型假设是否成立。

import matplotlib.pyplot as plt fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) ax1.plot(model.loss_history) ax1.set_xlabel("epoch") ax1.set_ylabel("MSE") y_pred = model.predict(X_test) residuals = y_test - y_pred ax2.scatter(y_pred, residuals, alpha=0.6) ax2.axhline(0, color="red", lw=0.8) ax2.set_xlabel("predicted") ax2.set_ylabel("residual") plt.tight_layout() plt.savefig("result.png", dpi=150)

plt.savefig 比 plt.show 更适合大作业,生成的 result.png 可以直接插进报告。损失曲线应该单调下降后进入平台,如果出现锯齿,通常是学习率偏大。残差图的正确形态是围绕 0 随机散布;如果呈喇叭状,预测值越大残差越散,说明误差方差不恒定,即异方差,这时把 y 取对数或引入 LSTAT_log 往往能缓解。残差和预测值的相关性接近 0,是线性假设成立的必要条件,报告里写一句"残差无系统性模式"比贴一堆数字更有说服力。

5.3 实验报告里的论证写法:每个参数都要有一句理由

大作业的评分通常一半看结果、一半看报告。报告不需要长篇大论,但要给每个关键选择配一句话理由。标准化的理由是"消除量纲差异,使梯度下降收敛更快";test_size=0.2 的理由是"样本量仅 506,测试集保留约 100 条,足以可靠估计指标";alpha 交给交叉验证的理由是"避免手工试参带来的主观偏差";主指标选 RMSE 的理由是"与房价同量纲,可解释性强"。把这些句子放在对应图表旁边,整体论证就闭环了。R² 明显高于全特征线性回归的正常区间、比如超过 0.9 时,不要急着高兴,先确认 StandardScaler 是不是在数据划分之前 fit 的——这是这份大作业里最隐蔽、也最容易被追问的扣分点。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 3:46:08

香港可编辑地图技术解析与应用实践

1. 项目背景与核心价值 香港作为国际大都市,其城市空间结构复杂多变。传统静态地图难以满足城市规划、商业选址、交通管理等动态需求。"香港地图可编辑版"正是为解决这一痛点而生。这类地图工具允许用户根据实际需求修改地图元素,比如添加临时…

作者头像 李华
网站建设 2026/9/11 3:40:47

云MySQL vs自建MySQL:瑶池RDS与自主部署的决策逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 3:39:39

智慧农业执行器开关控制实战:风机、卷帘、水肥机、电磁阀从接线到排障

先聊点实在的。搞智慧农业的人,很多都卡在执行器控制这一关。传感器、网关、云平台都搭得差不多了,结果一到真正控制风机、卷帘、水肥机、电磁阀的时候,各种问题就冒出来了——继电器噼里啪啦响但设备不动、卷帘机正反转打架烧了接触器、电磁…

作者头像 李华
网站建设 2026/9/11 3:38:57

HMI进化论:从“傻白甜”显示终端到边缘AI智慧大脑的三级跳

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 3:35:56

Python enumerate函数用法与性能优化全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 3:35:25

RT-Thread工业质检AI:低代码+RTOS原生支持的MCU端落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华