数学中国公众号文章

岭回归:给系数套上缰绳

上一期结尾留了个尾巴:多项式阶数一高,系数就冲到天文数字——训练集 R² 完美,测试集一塌糊涂。

数学中国

公众号 · 9 篇文章


上一期结尾留了个尾巴:多项式阶数一高,系数就冲到天文数字——训练集 R² 完美,测试集一塌糊涂。当时说"解法是正则化,给 (XᵀX) 加一个惩罚项 λI",今天把这个尾巴收掉。

本期主角是岭回归(Ridge Regression)。它要治两类病:一类是第14期见过的共线性(XᵀX 快解不出来了),一类是上一期的高阶过拟合(系数爆炸)。一个 λ,两个都治。

01

病根:系数开始发疯

先回顾第14期那批学生数据——8 个学生,两个自变量:每周学习时间 x₁、每天睡眠时间 x₂,因变量是考试成绩 y:

学生
学习时间 x₁
睡眠时间 x₂
成绩 y
S1
2.0
5.0
55.0
S2
4.0
6.0
60.0
S3
5.0
6.5
65.0
S4
7.0
7.0
72.0
S5
8.0
7.0
78.0
S6
10.0
7.5
85.0
S7
11.0
8.0
88.0
S8
13.0
8.5
95.0

上期算出来:两个自变量相关性 corr(x₁, x₂) = 0.9836,VIF = 30.65,用正规方程解出的系数是:

β = [62.42, 4.78, −3.39]

注意 β₂ = −3.39。翻译成人话:睡眠时间越长,成绩越低——每天多睡 1 小时,成绩掉 3.39 分。这显然不合理,学习时间和睡眠时间本来就都跟成绩正相关。

为什么冒出个负号?因为 x₁ 和 x₂ 几乎是同一条线:学习时间长的学生,睡眠时间也长(corr = 0.98)。两个几乎一样的变量并排放进模型,它不知道该把功劳算给谁,只好一个给很大的正系数,另一个给负系数去抵消。结果就是系数又大、符号又乱、换一批数据就翻天覆地。

💡 共线性的三大症状:系数符号反常、数值巨大、轻微扰动就剧变。根因是 XᵀX 接近奇异——正规方程里的 (XᵀX)-1把一个很小的扰动放大成了巨大的误差。

02

给系数画一个圈

上一期我们说过,RSS(残差平方和)在参数空间里是一张"地形图"。把 β₁、β₂ 当坐标轴,画出 RSS 的等高线:

图片

左图是关键:等高线不是一个个同心圆,而是一条又窄又长的沟(像个峡谷)。OLS 解落在沟底(红星)。问题在于——这条沟太窄太长,谷底虽然明确,但只要数据抖一下,解就会沿着沟壁滑出去很远。这就是系数不稳定的几何画面。

怎么把解"拴住"?给它加一条约束:不许系数长得太大。

min RSS  subject to  β₁² + β₂² ≤ t²

意思是:在"系数不超出半径 t 的圆"这个范围内,找 RSS 最小的点。几何上就是——把 RSS 等高线和这个圆画在一起,解落在圆与等高线的切点上(右图)。

右图里:红五星是 OLS 解(在沟底,离原点很远,‖β‖ = 5.86);绿圈是约束边界;紫点是岭回归解(落在圆上,‖β‖ = 3.44)。把 λ = 6 代进去,解从 [62.42, 4.78, −3.39] 变成 [44.18, 3.35, +0.79]——睡眠时间的系数从负变正了,符合常识。

这个"带约束"的问题,用拉格朗日乘子一转,就等价于在 RSS 后面加一项惩罚:

min  RSS + λ·‖β‖²

约束半径 t 和惩罚强度 λ 一一对应:t 越小(圈越小),λ 越大(罚得越狠)。所以"画个圈"和"加个 λ"是同一件事的两种说法。

一句话:岭回归 = 在原目标上加一条"系数别太大"的约束,把解从窄沟底拽回原点附近。系数变小了,却换来了稳定和正确的符号。

03

换个角度:λ 把小特征值抬起来

还有一个更本质的视角。上期的正规方程是 β = (XᵀX)-1Xᵀy。岭回归把公式改了一个字:

β = (XᵀX + λI)-1Xᵀy

就是在 XᵀX 的对角线上加一个 λ。别小看这一下——它直接改变了 XᵀX 的特征值。

特征值可以理解成"数据在某个方向上的铺开程度"。这批学生数据的相关矩阵特征值是:

大方向 1.9836,小方向只有 0.0164——两者差了两个数量级。条件数(最大特征值 ÷ 最小特征值)高达 120.6。

小的那个 0.0164 说明:有个方向上数据几乎没铺开(两个变量几乎重合),那个方向的系数就极其不稳定——这正是共线性的病根。

图片

现在在对角线上加 λI,特征值整体平移:变成 [0.0164+λ, 1.9836+λ]。取 λ = 0.5 看效果:

方向
原特征值
加 λ=0.5 后
变化
方向1(大)
1.9836
2.4836
+25%
方向2(小)
0.0164
0.5164
涨 31 倍
条件数
120.6
4.8
良态

看关键:λ 对大特征值几乎没影响(涨 25%),却把小特征值抬高了 31 倍。大方向该怎样还怎样,病态的小方向被补平了——条件数从 120.6 降到 4.8,矩阵从"快解不出来"变成"良态"。

顺带解决"不可逆"。只要 λ > 0,XᵀX + λI 的特征值全部 ≥ λ > 0,一定满秩、一定可逆——哪怕原来的 XᵀX 是奇异的。这也是第13期 SVD 里"伪逆"干的事:给奇异值兜个底。

04

L2 的圆 vs L1 的菱形

上一章的约束是 β₁² + β₂² ≤ t²,这叫 L2 惩罚(系数平方和)。还有一种常见选择:L1 惩罚,用 |β₁| + |β₂| ≤ t。两者的约束区域形状完全不同:

图片

左图(L2):约束是一个圆,圆滑、没有尖角。等高线切在圆弧上的某个点,解被收缩,但一般不会正好落到坐标轴上——所以系数变小,却很少变成 0。

右图(L1):约束是一个菱形,四个角是尖的。等高线很容易切在尖角上,而尖角正好落在坐标轴上——于是某些系数直接变成 0。这就是 Lasso 能"自动选特征"的几何原因。

惩罚
约束形状
系数行为
L2(岭回归)
圆(无尖角)
收缩但不归零,保留所有特征
L1(Lasso)
菱形(有尖角)
尖角切点使系数归零,自动选特征

💡 岭回归用 L2,好处是解稳定、处处可导、有闭式解;代价是不会把系数压成 0——它保留所有特征,只是让它们"变小"。真要挑特征,得请 Lasso 或弹性网出场(下一期讲)。

05

岭迹图:λ 越大,系数越往 0 缩

把 λ 从极小调到极大,观察三个系数各自怎么变,画成图就是岭迹图:

图片

横轴是 log10(λ),纵轴是系数值。三条线读几个关键位置:

λ
β = [截距, 学习, 睡眠]
状态
0.01
[61.65, 4.73, −3.23]
几乎等于 OLS
6
[44.18, 3.35, +0.79]
系数拉回,符号修正
10000
[74.40, 0.04, 0.01]
斜率几乎压到 0

看 λ 极大那一端(最右边):β₁、β₂ 几乎贴着 0,只剩截距 β₀ 还站着(74.40)。这时模型退化成"不管投入多少,都用平均值预测"。换句话说,λ 是一个"收缩力度"旋钮:λ→0 完全不收缩(等于 OLS),λ→∞ 把系数全压平(等于平均值)。

注意截距 β₀ 从头到尾没被压到 0(61.65 → 74.40)。因为岭回归只惩罚斜率系数,不惩罚截距——否则模型连数据的整体水平都对不上了。所以计算前要先把每列特征中心化(减去各自均值),截距才能被单独拎出来、不参与惩罚。

三条系数曲线起起伏伏、汇聚成一条"山岭"——这就是"岭回归"名字的由来。这张图本身也是选 λ 的工具:看哪条曲线在哪个 λ 之后开始趋于平稳,就在那里附近取值。

06

救活过拟合:20 家门店,15 次多项式

再看另一类病——上一期的高阶过拟合。这次换一批更大的数据:20 家门店的广告投入 x 与销售额 y(真实关系大约是 y ≈ 25 + 10√x,边际回报递减):

店
x
y
店
x
y
1
1.27
37.7
11
6.50
49.5
2
1.73
38.5
12
6.51
44.8
3
1.79
39.1
13
6.92
53.9
4
1.84
34.7
14
8.47
53.1
5
3.35
47.4
15
8.96
53.1
6
3.95
45.3
16
9.58
58.6
7
4.17
44.5
17
9.84
52.2
8
5.19
52.9
18
11.01
59.5
9
5.82
49.0
19
11.24
53.4
10
5.97
45.8
20
11.76
57.6

用 15 次多项式拟合:X = [1, x, x², …, x¹⁵],16 个参数,却只有 20 个样本。训练集和测试集对比:

图片

左图(OLS):曲线在训练点上完美穿行(train R² = 0.982),但一到测试点就疯了——测试集 R² = −0.423,比"直接用平均值猜"还差。右图(岭回归,λ≈0.37):曲线平顺地贴着大趋势,train R² = 0.827(略降),但测试集 R² 回到 +0.882。

为什么 OLS 会崩?看系数就明白了:

图片

左图是 OLS 的 15 个系数(对数刻度):最大的冲到 7.78×10¹⁰——几百亿。右图是岭回归的系数:全部被压到 10⁰ 附近(最大 6.88)。系数大到几百亿,只要 x 稍微一动,预测值就剧烈摆动——这正是过拟合的样子。

训练 R² 从 0.982 掉到 0.827,看着像"变差了",但测试 R² 从 −0.423 升到 0.882——这才是真本事。宁可训练时"差一点",也要在没见过的新数据上"稳一点"。这就是正则化的核心交换:牺牲一点训练精度,换一大截泛化能力。

07

λ 怎么选:留一交叉验证

λ 是人为设定的旋钮,选大了欠拟合、选小了没用。怎么定?——交叉验证。

最省数据的一种叫留一交叉验证(LOO-CV):每次拿掉 1 个样本当"临时测试",用剩下 19 个训练,去预测那个被拿掉的样本,记下误差;对 20 个样本各做一遍,平均误差就是当前 λ 的得分。把 λ 从 10⁻⁴ 扫到 10⁴,每个 λ 算一次,画出来是一条 U 形曲线:

图片

蓝线是 LOO-CV 误差,读三个位置:

λ
CV 误差
诊断
→ 0(最左)
35.1
过拟合(系数爆炸)
≈ 0.37
12.8
最优(甜点)
→ 大(最右)
53.2
欠拟合(系数被压太平)

曲线两头高、中间低,最低点就是最优 λ ≈ 0.37。绿线(测试集 R²)也印证了这一点——它在同一个位置附近达到最高。把选出的 λ 代回去,就是第 06 章那条平顺的岭回归曲线。

实现起来几行 numpy 就够(注意中心化和 solve 的写法):

import numpy as np

# 岭回归:中心化 -> 截距不参与惩罚
def ridge_fit(Z, y, lam):
    Zc = Z - Z.mean(axis=0)
    yc = y - y.mean()
    p = Z.shape[1]
    # solve 比 inv 更稳更快
    coef = np.linalg.solve(Zc.T @ Zc + lam * np.eye(p), Zc.T @ yc)
    return y.mean() - Z.mean(axis=0) @ coef, coef

# 留一交叉验证误差
def loo_cv_mse(Z, y, lam):
    n = len(y)
    errs = []
    for i in range(n):
        mask = np.ones(n, dtype=bool); mask[i] = False
        itc, cf = ridge_fit(Z[mask], y[mask], lam)
        errs.append((y[i] - (itc + Z[i] @ cf)) ** 2)
    return np.mean(errs)

# 扫描 λ,取 CV 误差最低点
lams = np.logspace(-4, 4, 120)
mse = [loo_cv_mse(Z, y, l) for l in lams]
lam_best = lams[int(np.argmin(mse))]  # ≈ 0.37

💡 为什么不用训练 R² 选 λ?因为训练 R² 会骗你——λ 越小、模型越复杂,训练 R² 越高,但它衡量的是"背题能力",不是"考试能力"。CV 误差衡量的是泛化,所以它能找到真正的甜点。这和上一期"看 test R² 不只看 train R²"是同一条原则。

08

总结与边界

把这一期串起来:

① 共线性 → XᵀX 接近奇异 → 系数爆炸、符号错乱,根因是小特征值太小。
② 岭回归 = (XᵀX + λI)-1Xᵀy,等价于给系数加一个 ‖β‖² ≤ t 的圆约束。
③ 从特征值看:加 λI 把小特征值抬起来,条件数从 120.6 降到 4.8。
④ L2 圆 → 收缩不归零;L1 菱形 → 尖角切点使系数归零(Lasso 自动选特征)。
⑤ λ 用交叉验证选:太小过拟合、太大欠拟合,最低点就是甜点。

岭回归也有边界:

① 不产生稀疏解。它保留所有特征(只是变小),要做特征选择得用 Lasso / 弹性网。
② 用偏差换方差。系数被"压小"意味着引入了偏差(bias)——这是偏差-方差权衡的另一面:牺牲无偏,换来更小的方差。
③ 必须先标准化。若各特征量纲差很多(一个以万元计、一个以个数计),惩罚对量纲大的特征不公平,结果会失真。
④ λ 依赖数据。不同问题的最优 λ 天差地别,没有万能值,必须现场交叉验证。

最后回顾数学建模系列这条主线

前往微信阅读全文

内容来自公众号,可前往微信查看原文。

查看作者的更多文章 →订阅更新、RSS 与阅读指南 →