ML · 章节 2
机器学习 2 —— 回归
课程章节 · 中央理工里尔学院 · Pascal Yim · 2026 年
上一章我们搭建了 Python 数据科学的工具链,从这一章开始,我们正式踏入监督学习的世界。回归 (regression) 是最古老、也最具直觉性的预测任务:给定一些输入变量,我们希望预测一个连续的数值——房屋的价格、鲍鱼的年龄、汽车的二氧化碳排放量。本章将以最小二乘法为起点,逐步走向多元回归、模型评估、非线性扩展、最近邻方法,并最终汇聚到 scikit-learn 的
Pipeline这一工程化抽象。
1. 最小二乘线性回归
1.1 寻找最佳直线
设我们手头有一组观测点 ,。最简单的预测假设是: 与 之间存在线性关系。我们用一条直线
去逼近数据。其中 是斜率, 是截距。如何挑选这两个参数,才能让这条直线"最贴合"散点云呢?
最自然的回答来自 19 世纪初的勒让德 (Legendre) 与高斯 (Gauss):最小化预测值与真实值之间误差的平方和。对每一个观测点,残差为
我们希望找到使代价函数
达到最小值的 。这就是 最小二乘法 (Ordinary Least Squares, OLS)。
关键点:为什么是平方而不是绝对值?平方让代价函数处处可导,从而可以用微积分求显式解;同时它对大误差的"惩罚"更重,使模型尽量避免出现严重偏离的预测。
1.2 解析解的推导
由于 是关于 的二次函数,它的最小值在两个偏导数同时为零的点取得。先对 求导:
整理后两边除以 ,即得
其中 、 分别是 与 的样本均值。这条结果具有很优美的几何意义:回归直线必然经过样本的重心 。
接着对 求导,并把上面的 代回去,经过简单整理得到
关键点:斜率 等于 与 的协方差除以 的方差。当 的波动越能"带动" 一起变化,斜率就越大。
1.3 用 NumPy 手工实现
理解了公式,我们就可以脱离任何机器学习框架,自己实现一个最小二乘回归器:
class MyLinearRegression: def __init__(self): self.a = None self.b = None def fit(self, x, y): x = np.array(x) y = np.array(y) x_mean = np.mean(x) y_mean = np.mean(y) numerator = np.sum((x - x_mean) * (y - y_mean)) denominator = np.sum((x - x_mean) ** 2) self.a = numerator / denominator self.b = y_mean - self.a * x_mean def predict(self, x): x = np.array(x) return self.a * x + self.b
这段代码刻意模仿了 scikit-learn 的接口规范——fit 用于学习参数,predict 用于产生预测。这样的接口约定在 Python 机器学习生态中已被普遍接受,后续我们调用任何 sklearn 的回归器,都会发现这一对方法。
1.4 残差分析
模型拟合完毕后,绝不能止步于一个数字。残差
蕴含了模型在每个样本点上的预测误差,把它画出来往往能揭示出比指标本身更深刻的问题。一个良好的线性模型,残差应当:
- 在零附近随机分布,看不出明显的趋势线;
- 方差大致保持恒定,不随 增大而扩张;
- 没有局部聚集的"团簇"。
如果残差呈现弯曲的弧形,这往往意味着真实关系是非线性的;如果残差在 较大时方差陡增,则提示存在异方差性 (heteroscedasticity)。在汽车排放数据集 co2_mini 上,残差通常会显示一条非常轻微的弧度,这与发动机效率随排量增加而递减的物理现象一致——线性模型已经做得不错,但还有改进的空间。
绘制残差图的代码相当简洁:
residus = y - y_hat fig = px.scatter( x=x, y=residus, labels={'x': '消耗量', 'y': '残差'}, title='线性回归残差图', ) fig.add_hline(y=0) fig.show()
观察残差应当成为每一次训练完模型后的条件反射。一个仅靠 数字看起来不错的模型,可能在残差图上暴露出系统性偏差;反之,一个 平庸的模型,如果残差均匀干净,也许只是因为数据本身噪声大,模型已经做到极限了。
2. 评价回归质量的指标
2.1 误差类指标
有了预测值 ,我们希望用一个标量数字来概括"模型整体偏差有多大"。常见的回归指标如下:
MAE — 平均绝对误差
它的单位与 一致,对大偏差的反应较为温和。
MSE — 均方误差
平方使得大误差被严重放大,因此 MSE 会"偏爱"那些尽可能避免离谱预测的模型。
RMSE — 均方根误差
开根号后单位回到 的量纲,便于解释。
选择哪个指标,取决于业务语境。预测发电厂的输出功率时,我们最怕的是偶尔出现的大失误,因此应该用 RMSE;预测一个城市每个街区的房价中位数时,我们更关心典型误差有多大,MAE 更合适;若我们要把误差作为百分比汇报给非技术用户,MAPE 则一目了然。
MAPE — 平均绝对百分比误差
它给出相对误差,对量级跨度大的目标(例如房价)很有意义,但当 接近零时会数值不稳。
关键点:MAE 与 RMSE 可能讲述不同的故事。如果它们相差甚远,通常意味着数据中存在少数严重的异常预测——RMSE 的平方放大了它们的影响。
2.2 决定系数
衡量模型相对于"始终预测均值"这个朴素基线的改进幅度:
它的取值含义直观:
- :模型完美解释了所有方差;
- :模型与"永远预测均值"等效;
- :模型表现比朴素基线还差——这种情况完全有可能,通常是过拟合或评估代码出错的信号。
关键点:一个高的 并不等同于良好的模型。它可能掩盖过拟合,也可能因数据本身固有的方差很小而虚高。务必联合多个指标和残差分析来做判断。
3. 用 sklearn 进行多元线性回归
3.1 从直线到超平面
当我们有两个解释变量 ,模型变为
在三维空间里它对应一个平面。推广到 个特征:
将所有样本组织成矩阵 、目标向量 ,可以用矩阵形式紧凑地写出最小二乘解(若 可逆):
实践中,scikit-learn 并不真的去显式求逆,而是调用数值更稳定的最小二乘求解器(基于 SVD 或 QR 分解),这样即使 病态也能给出可信的系数。
3.2 sklearn 接口实战
我们以经典的 abalone_mini 数据集为例。鲍鱼是一种海贝,贝壳上的环数 (Rings) 与年龄成正比。任务是用物理测量(Length, Diameter, Height, Weight)预测 Rings。
from sklearn.linear_model import LinearRegression X = df[['Length', 'Weight']] y = df['Rings'] model = LinearRegression() model.fit(X, y) y_hat = model.predict(X) print(model.coef_, model.intercept_)
注意几个工程细节:
X必须是二维的——即便只有一个特征,也要写df[['Length']]而不是df['Length'];model.coef_是系数向量 ,model.intercept_是 ;predict返回的形状与y一致;- 若希望 sklearn 不自动加截距,可在构造时传
fit_intercept=False; - sklearn 内部使用的求解器是
scipy.linalg.lstsq,它对秩亏 (rank-deficient) 的设计矩阵也能给出最小范数解。
关键点:多元回归的系数 表示在固定其他变量的前提下, 每变化一个单位时 的变化。这正是它相对于一系列单变量回归的核心价值——分离出每个因子的"净效应"。
4. 训练集与测试集
4.1 为什么必须留出测试集?
把模型在训练数据上的表现视为它的"真实水准",几乎总是会得到一个乐观且具有误导性的估计。这就像让学生把考试题就拿来当复习题:成绩好不代表他真的理解了知识。
机器学习中,我们关心的是泛化能力 (generalization):模型在未见数据上的表现。为此,我们将原始数据划分为不相交的两部分:
- 训练集 (
X_train,y_train):用于拟合参数; - 测试集 (
X_test,y_test):仅用于最后的评估,不能参与任何训练决策。
4.2 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, )
参数解释:
test_size=0.2:保留 20% 作为测试集;random_state=42:固定随机种子,保证可复现。
随后训练与评估流程一气呵成:
model = LinearRegression() model.fit(X_train, y_train) y_hat_test = model.predict(X_test)
关键点:永远在测试集上报告最终指标。如果你同时在训练集与测试集上算 MAE,差距过大就提示过拟合;两者都很差则提示欠拟合。
5. 交叉验证
5.1 单次划分的局限
如果不固定 random_state,每次执行 train_test_split 都会得到不同的训练/测试切分。运行三次,可能得到三个差异显著的 值。这种波动说明:单次划分得到的指标对随机抽签高度敏感,不能算作模型性能的可靠估计。
5.2 折交叉验证
折交叉验证 (k-fold cross-validation) 给出了更稳健的方案:
- 将数据集均匀切成 份(称作 "fold");
- 每次轮流让其中 1 份作为测试集,其余 份作为训练集;
- 重复 次,得到 个性能分数;
- 报告它们的均值(总体水平)与标准差(稳定性)。
当 时,每一个样本都恰好被用于评估一次,被用于训练四次。这种"全员参与"的特性,使交叉验证给出的指标比单次训练/测试划分更具有代表性。
的取值通常在 5 到 10 之间。 越大,每折训练集越大,因此每一次训练出的模型越接近用全部数据训练的"真实"模型,但计算成本随之线性增长。极端情况是 ,称作留一交叉验证 (Leave-One-Out, LOO):每次只留出一个样本测试。LOO 的偏差最低,但方差大,且在 很大时计算上不可承受。
from sklearn.model_selection import cross_val_score scores = cross_val_score(model, X, y, cv=5, scoring='r2') print(scores.mean(), scores.std())
scoring 参数指定评价指标。回归中常用的有 'r2'、'neg_mean_absolute_error'、'neg_mean_squared_error'、'neg_root_mean_squared_error'、'neg_mean_absolute_percentage_error'。
关键点:误差类指标在 sklearn 中以负号形式返回。这是因为
cross_val_score内部约定"分数越大越好",而误差是越小越好。要看真正的 MAE,只需mae = -scores。
6. 多项式回归
6.1 当线性假设失效
线性模型 只能捕捉直线关系。但许多物理量并非线性——例如鲍鱼的重量与长度之间近似服从立方关系(因为重量正比于体积)。
引入非线性的一个朴素却高效的办法,是手工构造多项式特征:
注意,虽然 关于 是非线性的,但关于参数 仍然是线性的。因此我们仍可直接套用线性回归来求解!
6.2 实现
X = df[['Length']].copy() X['Length2'] = X['Length'] ** 2 X['Length3'] = X['Length'] ** 3 model = LinearRegression() model.fit(X, y)
或者使用 PolynomialFeatures 自动生成。多项式阶数越高,拟合训练集越好,但过拟合的风险也越大——这正是后续我们要谈正则化和模型选择的伏笔。
关键点:多项式回归是一个绝佳的"特征工程"案例。我们没有改变模型的本质(仍是线性回归),只是把领域知识(重量与长度的立方关系)以新特征的形式注入到了 中。机器学习实践中,优秀的特征往往比复杂的模型更重要——这条原则贯穿整个工程师的职业生涯。
7. k 近邻回归
7.1 思想
k 近邻 (k-Nearest Neighbors, k-NN) 是一种与线性模型截然不同的范式。它不学习任何全局公式,而是直接保存训练集。要预测新样本 时,它做三件事:
- 计算 与训练集中每个样本的距离;
- 挑出距离最小的 个邻居 ;
- 取这些邻居目标值的均值作为预测:
7.2 距离的选择
默认采用 闵可夫斯基距离:
即欧几里得距离, 即曼哈顿距离。
from sklearn.neighbors import KNeighborsRegressor model = KNeighborsRegressor(n_neighbors=5) model.fit(X_train, y_train) y_hat = model.predict(X_test)
参数 n_neighbors 是核心超参数:
- 太小(如 ):模型对噪声极度敏感,方差大;
- 太大:预测被远处样本"稀释",变成全局均值,偏差大。
关键点:k-NN 没有真正的"训练阶段"——所有计算都推迟到预测时。这种惰性学习 (lazy learning) 在小数据上效果惊人,但在大数据上预测代价很高。
8. 标准化:k-NN 的生死线
k-NN 的全部行为都基于距离。如果两个特征的量纲悬殊——比如房屋面积 sqft_living 在 300 到 4000 之间,而 bathrooms 仅在 1 到 5 之间——那么面积上的差异会完全压垮浴室数的差异。算法实际上变成了只用面积来判断邻居,其他变量形同虚设。
8.1 三种常用缩放器
StandardScaler:中心化并按标准差缩放,
MinMaxScaler:线性映射到 。
RobustScaler:使用中位数与四分位距,对离群值鲁棒。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)
关键点:
fit_transform只在训练集上调用,transform用同一个缩放器作用于测试集。任何在测试集上重新拟合缩放器的做法都会导致数据泄漏 (data leakage),让你高估模型性能。
9. Pipeline:一条龙的工程抽象
把"标准化 + 模型"两个步骤手工拼接,代码繁琐且容易出错——尤其在交叉验证中,你需要在每一个 fold 内部重新拟合缩放器。scikit-learn 的 Pipeline 优雅地解决了这个问题:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsRegressor pipe = Pipeline([ ('scaler', StandardScaler()), ('model', KNeighborsRegressor(n_neighbors=5)), ]) pipe.fit(X_train, y_train) y_hat = pipe.predict(X_test)
调用 pipe.fit(X_train, y_train) 时,内部按顺序执行:
StandardScaler在X_train上学习 并对其变换;KNeighborsRegressor在变换后的数据上训练。
调用 pipe.predict(X_test) 时,自动用训练时学到的 来变换 X_test,然后送给 k-NN。
Pipeline 还能与 cross_val_score 完美组合——每个 fold 内部缩放器都会被重新拟合,从根本上杜绝数据泄漏。
关键点:写机器学习代码时,只要预处理步骤不平凡,就用 Pipeline。它是把"研究脚本"升级为"生产代码"的关键一步。
练习
-
手工实现:在
co2_mini数据集(燃料消耗 vs CO₂ 排放)上,用本章的MyLinearRegression类拟合一条直线,绘制散点图与回归线,并画出残差。残差是否表现出非随机的模式? -
多元回归:在
abalone_mini数据集上,用Length和Weight两个变量预测Rings。报告coef_与intercept_,并解释每个系数的物理含义。 -
训练/测试:对练习 2 重新做
train_test_split划分(test_size=0.2)。比较训练集与测试集上的 MAE 和 ,讨论模型是否过拟合。 -
交叉验证:用 5 折交叉验证(
scoring='neg_mean_absolute_percentage_error')在abalone_mini上评估线性回归。报告均值与标准差,并解释负号的含义。 -
多项式拟合:用
Length、Length^2、Length^3预测Weight。多项式回归相对于简单线性回归的提升有多大?把阶数加到 10 阶,会出现什么现象? -
k-NN:在
house_mini数据集上,不做标准化和做StandardScaler标准化,分别用 k-NN(n_neighbors=5)预测price。两者 RMSE 相差多少?为什么? -
Pipeline 综合:把练习 6 重写为一个
Pipeline,并用交叉验证比较LinearRegression与 k-NN 在house_mini上的表现。哪种模型更适合这个任务?
拓展阅读
- scikit-learn 官方文档 — Linear Regression
- scikit-learn 官方文档 — Cross-validation: evaluating estimator performance
- scikit-learn 官方文档 — Polynomial regression: extending linear models with basis functions
- scikit-learn 官方文档 — Nearest Neighbors regression
- scikit-learn 官方文档 — Preprocessing data — StandardScaler, MinMaxScaler, RobustScaler
- scikit-learn 官方文档 — Pipelines and composite estimators
- scikit-learn 官方文档 — Metrics and scoring: quantifying the quality of predictions
- Hastie, Tibshirani, Friedman — The Elements of Statistical Learning (Chapters 2 & 3),自由获取于 https://hastie.su.domains/ElemStatLearn/
- James, Witten, Hastie, Tibshirani — An Introduction to Statistical Learning with Applications in Python (Chapters 3 & 5),自由获取于 https://www.statlearning.com/