news 2026/10/10 1:44:40

波士顿房价预测实战:线性回归从数据预处理到模型评估全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
波士顿房价预测实战:线性回归从数据预处理到模型评估全流程

简介:一份以波士顿房价预测为主线、线性回归从原理到实战的代码合集,适合机器学习初学者与需要快速搭建回归预测流程的开发者。资源共20个文件,含11个Python脚本与9个CSV数据文件:脚本承担数据加载、特征工程、模型训练、评估与可视化等任务,CSV主要提供训练集、测试集及中间曲线数据;压缩包整体228KB,结构紧凑。目前已有346人学习使用。通过学习可掌握数据标准化、多项式特征扩展与组合特征等处理技巧,理解梯度下降实现,还能对比线性回归、岭回归、Lasso与Elastic Net的效果和调参思路,为真实房价预测或其他回归问题提供可直接复用的代码模板与排查思路。

1. 波士顿预测实战:为什么一份40年前的数据集还是学线性回归的第一课

同一份波士顿房价数据,有人把 R² 刷到 0.85,有人换个随机种子直接掉到 0.7,还有人连load_boston()都跑不通——这不是模型玄学,是数据切分、标准化顺序和版本差异在作怪。这个经典回归任务数据量小、特征含义明确、线性关系强,正好用来把「线性回归基础代码」从头到尾捋一遍:损失函数怎么来的、闭式解和梯度下降各自的适用边界、调库前为什么要先手写一遍、以及预测结果好得离谱时该先怀疑哪一步。这篇笔记面向准备用回归模型入门机器学习的实践者,目标是让你拿到一份可复现的完整流程,而不是停留在sklearn的一行调用上。

2. 线性回归基础:损失函数、闭式解与梯度下降怎么选

2.1 从最小二乘说起:损失函数为什么选平方误差

线性回归做的是这样一件事:给定特征矩阵 X 和真实标签 y,找到一组权重 θ,让预测值 ŷ = Xθ 尽量接近 y。这里的「尽量接近」需要一个可微的量化标准,最常用的是均方误差。加上 1/2 的系数是为了后面求导方便:

J(θ) = (1/2m) Σ(yᵢ - θᵀxᵢ)²

为什么选平方而不是绝对值?绝对值损失在零点不可导,梯度下降在误差接近零时会出现锯齿状的震荡;平方损失放大了大误差的惩罚,让模型优先修正偏差大的样本,这和大多数业务场景里「预测偏得太离谱比偏一点更不可接受」的直觉一致。缺点是它对离群点敏感,个别异常样本会把回归线拉歪,这个特性在后面波士顿数据里会直接体现出来。

最小二乘这个名字来自几何视角:我们找的是一条直线/超平面,使样本点到它的竖直距离平方和最小。对这个目标函数求偏导并置零,就能得到闭式解。闭式解没有迭代过程,一次性算出最优权重,这是它最大的优势;但后面会看到,它在特征维度过高或矩阵接近奇异时会失效。

2.2 闭式解与梯度下降:两条收敛路径的取舍

对 J(θ) 关于 θ 求导并令其为零,得到正规方程:

θ = (XᵀX)⁻¹Xᵀy

这个形式非常干净,numpy 里几行就能算完。但它的代价藏在(XᵀX)⁻¹里:矩阵求逆的时间复杂度约是 O(n³),n 是特征维度。波士顿数据集只有 13 个特征,完全无所谓;但特征数上万甚至几十万时,求逆要么慢到无法接受,要么因为特征高度相关导致 XᵀX 不可逆,直接报错。

梯度下降走的是另一条路:不求逆,而是沿着损失函数下降最快的方向迭代。参数更新规则写成向量化形式是:

θ := θ - α · (1/m) · Xᵀ(Xθ - y)

其中 α 是学习率,控制每一步迈多大。这个小批量/批量梯度下降的写法在整个训练循环里只需要矩阵乘法,特征多时也能跑。它不会因为矩阵奇异而失败,但引入了一个新的麻烦:学习率调不好,要么发散要么慢得像蜗牛。

我的建议是:特征数小于 1000 且数据量不大时,优先用闭式解,快且没有调参负担;特征维度高或数据量大时转梯度下降,但必须配合标准化和损失曲线监控。

2.3 标准化与学习率:梯度下降能收敛的前提

如果不做标准化,像波士顿数据里的TAX(税率,数百量级)和CHAS(是否临河,0/1 取值)会同时出现在特征矩阵里。梯度下降在更新参数时,梯度大小和特征尺度成正比,量级大的特征对应的权重会被大步长带着乱跳,量级小的特征对应的权重又更新得极慢,整个收敛过程会非常不稳定。

标准化通常用零均值单位方差:z = (x - μ) / σ。做完之后所有特征落到相近的尺度,损失函数的等高线从狭长椭圆变成接近正圆,梯度下降的路径就不再走锯齿形,学习率也更容易设置。需要特别注意的是,μ 和 σ 只能从训练集计算,测试集用同一组参数转换,这一点在第三章会详细展开,因为它是个高频踩坑点。

库实现里,sklearn.linear_model.LinearRegression默认不标准化也能直接算,因为内部走的是最小二乘的 SVD 分解;但一旦切到SGDRegressor或自己写梯度下降,标准化就是刚需。理解这层关系,后面调参时才知道什么时候该动学习率、什么时候该动数据。

3. 取数、改表、切分:做实验前先把波士顿数据搞干净

3.1load_boston弃用后的正确取数方式

很多人卡在第一步:from sklearn.datasets import load_boston在新版 scikit-learn 里直接报AttributeError。这个数据集因为包含一些涉及敏感属性的字段,存在数据伦理争议,官方从较新版本开始将其移除。常见做法是直接从公开数据仓库下载boston.csv一类的现成文件,或使用老版本环境。我一般建议走 CSV 路线,因为后续用 pandas 处理更顺手,不用和Bunch对象打交道:

import pandas as pd df = pd.read_csv("boston.csv") print(df.shape) print(df.columns.tolist())

这里df.shape应该输出(506, 14),对应 506 条样本、13 个特征加 1 个目标列。columns.tolist()可以快速确认列名是否符合预期,防止下载的文件带索引列或命名异常。如果你手里的文件第一列是无名索引,读取时可以加参数index_col=0把它丢掉。老版本环境里的load_boston()返回的data和target也可以直接拼成 DataFrame,但我不推荐为了一个数据集去锁环境版本,后面装别的库容易冲突。

3.2 特征含义与初步相关性观察

拿到数据第一件事不是跑模型,是看每列在讲什么。波士顿房价数据集的 13 个特征里,RM是平均房间数,LSTAT是低收入人口占比,DIS是到就业中心的加权距离,TAX是房产税率,CHAS是是否临河的 0/1 变量。目标列MEDV是自住房屋价格中位数,单位是千美元。

特征含义与 MEDV 的直觉关系
CRIM城镇犯罪率越高房价越低
ZN大户型住宅用地比例一般正相关
INDUS非零售商业用地比例一般负相关
CHAS是否临河临河通常更贵
NOX氮氧化物浓度越高房价越低
RM平均房间数强正相关
AGE老旧自住房比例一般负相关
DIS到就业中心距离越远越低
RAD交通可达性指数与 TAX 强相关
TAX房产税率越高越低
PTRATIO师生比越高教育资源越差
B黑人比例相关统计量敏感字段,仅作特征存在
LSTAT低收入人口占比强负相关
MEDV房价中位数目标列

先做一轮缺失值和描述性统计,用df.describe()看每列的量级和分布,重点观察RM、LSTAT和MEDV的极值。再跑一个相关性矩阵,把热力图打出来,你会看到LSTAT和MEDV的相关系数接近 -0.7,RM和MEDV接近 0.7,这两个特征对房价的解释力明显强于其他列。同时注意TAX和RAD的相关系数超过 0.9,这是一对典型的强共线性特征,到第五章会牵扯出系数解释翻车的问题。

缺失值处理在这个数据集里通常很轻松:df.isnull().sum()大概率全为 0。如果有少量缺失,常见做法是看列分布用中位数填充,而不是均值,因为房价相关的特征多为偏态分布,均值容易被极端值带偏。

3.3 先切分再标准化:防止数据泄漏的做法

数据预处理的顺序是有讲究的。最稳妥的流程是:先分离 X 和 y,再做训练测试切分,最后只基于训练集做标准化。这里的关键点是StandardScaler的fit只能作用在训练数据上,测试数据用训练集算出的均值和方差来transform。

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

test_size=0.2表示留出 20% 的样本做验证,506 条数据里训练集约 404 条、测试集约 102 条。random_state=42固定切分结果,保证你每次跑出来的训练集和测试集完全一致,实验可复现。fit_transform在训练集上先算均值和方差再转换,transform在测试集上只用已算好的参数做转换,不再重新计算。

如果先在整个 X 上做标准化再切分,测试集的分布信息已经混进了训练流程,被称为数据泄漏。泄漏的后果是验证结果虚高,模型上线后真实表现远不如实验,这类问题在小数据集上尤其隐蔽。这一步做对了,后面模型做得好才是真的好。

4. 代码实践:从 NumPy 手写闭式解到 sklearn 调参对比

4.1 用 NumPy 手写闭式解:验证你对公式的理解

在调库之前,我强烈建议先手写一遍线性回归。不是为了重复造轮子,而是为了确认你对正规方程的理解:加一列全 1 作为截距项、矩阵乘法顺序、转置和求逆的配合。代码做到位了,后面看 sklearn 的coef_和intercept_时心里才有底。

import numpy as np def linear_regression_closed_form(X, y): # 在特征矩阵左侧加一列 1,用来学习截距项 X_with_bias = np.c_[np.ones(X.shape[0]), X] # 正规方程:θ = (XᵀX)⁻¹Xᵀy theta = np.linalg.inv(X_with_bias.T @ X_with_bias) @ X_with_bias.T @ y return theta theta = linear_regression_closed_form(X_train_scaled, y_train.values) print("截距项:", theta[0]) print("特征权重:", theta[1:])

X_train_scaled在上一章已经标准化过,所以这里直接传入数值矩阵。np.c_是拼接辅助方法,把一列 1 拼到特征矩阵最左边,这样线性回归的偏置就被吸收进权重向量里,不需要单独处理。np.linalg.inv(...)计算逆矩阵,当特征维度不高时完全够用。这段代码跑出来的是一个 14 维的向量,第一位是截距,后 13 位是特征权重。

两个容易出错的地方:一是y要转成 numpy 数组,pandas Series 直接参与矩阵运算有时会报维度不匹配;二是X.T @ X的结果如果接近奇异,inv会给出数值不稳定的结果,此时可以试试np.linalg.pinv伪逆来兜底。这个细节在你以后处理高维共线性数据时会非常有用。

4.2 梯度下降版训练循环:学会监控损失曲线

闭式解是一次性计算,梯度下降则是一步步逼近。理解梯度下降的关键不是记公式,而是观察损失值随迭代次数的变化。下面这段是批量梯度下降的完整实现,训练过程中每 100 轮打印一次损失:

def linear_regression_gradient_descent(X, y, lr=0.01, n_iter=1000): m, n = X.shape theta = np.zeros(n + 1) # 含截距项 X_with_bias = np.c_[np.ones(m), X] loss_history = [] for i in range(n_iter): gradient = (1 / m) * X_with_bias.T @ (X_with_bias @ theta - y) theta = theta - lr * gradient loss = np.mean((X_with_bias @ theta - y) ** 2) loss_history.append(loss) if i % 100 == 0: print(f"iter {i}, loss {loss:.4f}") return theta, loss_history theta_gd, loss_hist = linear_regression_gradient_descent( X_train_scaled, y_train.values, lr=0.05, n_iter=500 )

lr=0.05在标准化后的特征上是个安全的起点。n_iter=500对 400 多条样本足够,因为批量梯度下降每一步都用全量数据计算梯度,收敛速度通常很快。gradient那一行是核心,X_with_bias.T @ (X_with_bias @ theta - y)算的是误差向量与特征矩阵的内积,再除以样本数得到平均梯度。

运行后你会看到 loss 从最初的几百量级快速下降,到后面每轮只降零点几。如果 loss 曲线出现大幅震荡甚至上升,先检查学习率是不是太大,降到 0.01 再试;如果下降得极慢,说明学习率太小或特征没标准化。这个动手调参的过程,比直接调用SGDRegressor更能建立对超参数的直觉。

4.3 与 sklearn 对比并用指标评估

手写版本的意义在于和标准库结果互相验证。sklearn 的LinearRegression底层默认用最小二乘的 SVD 分解,数值稳定性比直接求逆更好,但数学本质和我们的闭式解一致,两者的系数应该几乎相同。

from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score model = LinearRegression() model.fit(X_train_scaled, y_train) print("手写闭式解权重:", theta[1:]) print("sklearn 权重:", model.coef_) print("截距对比:", theta[0], model.intercept_) y_pred = model.predict(X_test_scaled) 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:.4f}, RMSE: {rmse:.4f}, MAE: {mae:.4f}, R²: {r2:.4f}")

跑完你会发现手写权重和 sklearn 的coef_在小数点后三四位内有细微差别,这是数值计算精度问题,不影响结论。评估指标里,RMSE 和房价的单位一致(千美元),波士顿测试集上通常落在 4 到 6 之间;R² 在 0.7 到 0.85 之间都算正常——如果你想继续提升,后面还能加正则化和特征变换。需要注意的是,R² 超过 0.9 时先别高兴,回去检查是不是把测试集信息泄漏到训练里了。

5. 波士顿预测的 5 个坑:从加载失败到只看 R²

5.1 数据集加载失败:版本差异是第一道坎

现象:from sklearn.datasets import load_boston报错AttributeError,或者程序在 import 阶段就崩了。

原因:scikit-learn 较新版本已经移除了这个数据集,老代码直接迁移过来必然翻车。很多教程没有注明版本约束,导致照着抄的人卡在环境搭建这一步。

解决:先检查 sklearn 版本,再决定走哪条路。我一般直接用 CSV 读取,绕开版本问题:

import sklearn print(sklearn.__version__) # 方案一:直接读 CSV,适用于任何版本 df = pd.read_csv("boston.csv") # 方案二:老版本环境下的兼容写法 try: from sklearn.datasets import load_boston data = load_boston() except ImportError: print("当前 sklearn 版本已移除 load_boston,请改用 CSV 方式读取")

这个坑本身不难,但它提醒了一件事:跑别人的代码前先看环境版本,尤其是涉及到已弃用 API 的机器学习项目。

5.2 标准化顺序颠倒:结果好得可疑时先查这里

现象:模型在训练集上 R² 高达 0.95,测试集却只有 0.5;或者反过来,测试集结果异常地好,好到你不敢相信。

原因:先对整个 X 做标准化,再切分训练测试集,测试集的均值和方差已经参与过训练数据的转换。模型在测试时「见过」数据分布,评估结果失去意义。这是典型的泄漏,表现往往是验证集指标虚高。

解决:严格执行「先切分、后标准化」的流程。scaler.fit(X_train)算参数,scaler.transform(X_test)用同一套参数转换。检查代码时不只要看有没有调用fit_transform,还要看它作用在哪个数据上。小数据集上这一步的判断失误,后面所有调参都是白费。

5.3 随机种子不固定:R² 波动大不是模型问题

现象:代码完全一样,只是没设随机种子,重跑几次 R² 在 0.7 到 0.82 之间来回跳。

原因:train_test_split默认每次随机切分,波士顿数据只有 506 条,测试集 100 条左右的样本分布差异足以让评估指标产生明显波动。这不是模型不稳定,是采样随机性。

解决:固定random_state=42,让切分结果可复现。更进一步的做法是放弃单次切分,改用 K 折交叉验证,取多折的平均分和标准差来判断模型真实水平。对小数据集而言,交叉验证比单次 holdout 更能反映泛化能力。

5.4 多重共线性:系数方向不对时的排查思路

现象:跑出来的TAX系数是正数,直觉上税率越高房价应该越低,为什么模型给出正相关?或者某个特征单独和房价负相关,放进模型后系数变成正的。

原因:波士顿数据里TAX和RAD相关系数超过 0.9,高度共线。当两个特征携带相似信息时,模型会把权重在它们之间任意分配,单个系数不再代表该特征的独立影响。这个现象在线性回归里非常常见,也是新手最容易误解的地方。

解决:先看相关系数矩阵或方差膨胀因子 VIF,确认哪些特征强相关。常见的处理是二选一删掉其中一个,或者改用岭回归,用 L2 正则化把系数压缩、缓解共线性带来的方差膨胀。如果目标是预测而非解释,共线性问题可以适当放宽;但如果你想跟别人解释每个特征的影响方向,就必须处理它。

5.5 只看 R²:这个指标在小样本上会骗人

现象:模型 R² 看起来不错,但你把预测值和真实值摆在一起看,发现价格在 20 万以上的房子全都预测偏低。R² 没能暴露这个问题。

原因:R² 是整体拟合优度,对高值区的系统性偏差不敏感。波士顿数据里房价中位数在 20 以上时样本明显变少,模型倾向用均值回归来降低整体损失,于是高房价区域的预测被压低。

解决:把y_pred和y_test画成散点图,配合 MAE 和 RMSE 一起看。RMSE 对偏离大的样本更敏感,MAE 则反映平均误差水平。如果高值区预测总是偏低,考虑对目标列做log1p变换,把偏态分布拉正,再跑回归并比较变换前后的指标。记住 R² 是结论之一,不是结论本身。

6. 进阶:用残差图和学习曲线给模型做体检

6.1 残差图该长什么样

残差是真实值与预测值的差:residual = y_test - y_pred。把它画在预测值旁边,理想情况是残差随机分布在 0 水平线上下,没有明显形状。如果出现漏斗形——预测值越大残差越分散,说明存在异方差,模型在高值区的不确定性更大,这时候单纯调参已经不够,得考虑对目标做变换。

import matplotlib.pyplot as plt residuals = y_test - y_pred plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(0, color="red", linestyle="--") plt.xlabel("Predicted MEDV") plt.ylabel("Residuals") plt.title("Residual Plot") plt.show()

这张图不需要高级技巧,但它能在你被指标迷惑时给出直观判断。我自己的习惯是评估完模型先画残差图,再回头看数字,因为数字会掩盖局部模式。

6.2 用学习曲线判断偏差方差

学习曲线是判断模型处于欠拟合还是过拟合的最直接工具:不断增大训练集规模,分别记录模型在训练集和验证集上的分数,画成两条曲线。训练分高、验证分低且两线差距大,是高方差,模型记忆了训练数据;两线都低且贴在一起,是高偏差,模型本身表达力不够。

from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores = learning_curve( LinearRegression(), X_train_scaled, y_train, cv=5, train_sizes=np.linspace(0.1, 1.0, 5) ) train_mean = train_scores.mean(axis=1) val_mean = val_scores.mean(axis=1) for size, tm, vm in zip(train_sizes, train_mean, val_mean): print(f"训练样本数 {size:.0f}:训练 R² {tm:.3f},验证 R² {vm:.3f}")

波士顿数据上你会看到验证分数随训练样本增多缓慢上升,训练分数始终在验证分数上方一些,这是小数据集的常态。真正厉害的做法是把这个过程用在特征工程之后——比如加了多项式特征再画一次,看曲线是否改善。这套「先指标、再残差图、最后学习曲线」的诊断顺序,也是我处理其他回归问题时沿用的流程。有一次在模拟项目X上,我靠着残差图发现预测误差在目标值两端都偏大,回去查才发现是目标变量没做对数变换,指标虚高骗了我两天。从那以后,我拿到任何回归模型都会先画这两张图再谈结论,希望你也能少走这一步弯路。

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

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

JavaWeb开发环境配置全攻略:JDK、Tomcat、MySQL与IDEA避坑指南

最近隔三差五就有人来问我 JAVAWeb 的配置和安装问题,尤其是刚接触 JavaWeb 的同学,项目代码还没写几行,先被环境折腾得怀疑人生。环境变量、JDK、Tomcat、MySQL、IDEA,这几样东西单独拿出来都不难,但凑到一起就会冒出…

作者头像 李华