60 个训练点,真函数是条弯曲线,噪声下限 0.09。 常数模型误差 0.8581(1 个有效参数),线性模型 0.5750(2 个), 核回归调到最好 0.0955(14.62 个)。 带宽一路放大到 50,有效参数正好变成 1.00,误差回到 0.8575——和常数模型一模一样。 所谓「模型复杂度」,可以压缩成一个数。 全书二十章,讲的就是这一个数该取多大。 而它没有通用答案,只能靠数据自己说。
这一章不介绍新方法。 它只做一件事:把前面十九章摊开来看,找出那根把所有章节串起来的线。 如果只记住书里的一句话,该记这句: 所有学习方法都在做同一件事——在「模型太简单,学不到规律」和「模型太复杂, 把噪声也学了」之间找那个点。 不同的章看起来差别很大:有的在解方程,有的在算特征值,有的在做概率推断。 但它们调节的是同一个东西,只是用的旋钮不一样——约束的强度、损失的形状、核的宽度、 邻居的个数、簇的个数、标签的多少。
一、全书只有一条主线
把每一章的手段列出来,会发现它们调的是同一个量。
| 章节 | 旋钮 | 往哪边拧算变复杂 |
|---|---|---|
| 第 4 章 ℓ2 约束 | 约束强度 | 强度调小 |
| 第 5 章 ℓ1 约束 | 约束强度 | 强度调小 |
| 第 6 章 鲁棒损失 | 损失的形状 | 越接近平方损失 |
| 第 8 章 支持向量机 | 间隔的容忍度 | 容忍度调小 |
| 第 9 章 集成 | 基学习器的个数 | 个数调多 |
| 第 13 章 降维 | 保留的维数 | 维数调多 |
| 第 14 章 聚类 | 簇的个数 | 个数调多 |
| 第 15 章 在线学习 | 步长 | 步长调大 |
| 八个旋钮,拧的方向都指向同一头。 | ||
| 这就是为什么第 4.3 节讲的「模型选择」会在后面反复出现—— | ||
| 它是全书唯一一个「怎么定这个数」的通用答案。 | ||
| 顺带说一句,模型选择那一节给的三个办法(交叉验证、信息准则、 | ||
| 正则化路径)在后面的章节里一直没被绕开过。 | ||
| 凡是要定超参数的地方,用的还是这三个。 |
二、三类模型,这本书走完了两类
第 2 章把学习模型分成三类:
| 类别 | 预测值长什么样 | 本书讲到哪 |
|---|---|---|
| 线性模型 | 输入的加权和 | 第 3 到 10 章,讲透了 |
| 核模型 | 训练样本的加权和 | 第 8.4、13.4、14.2 等,讲透了 |
| 层级模型 | 多层复合,逐层抽象 | 只在 2.3 提了个头 |
| 线性模型和核模型这两族,本书从最朴素的拟合一路讲到迁移学习,该给的工具基本都给了。 | ||
| 层级模型几乎没展开。 | ||
| 第 2.3 节只说了「把多个简单模型叠起来可以表达更复杂的函数」,然后就转去讲别的了。 | ||
| 这是一个刻意的取舍。 | ||
| 这本书 2015 年出日文版,那时候多层网络刚刚重新热起来,理论工具还没沉淀成能「图解」的程度。 | ||
| 而它需要的数学(链式法则、矩阵求导)和本书一路用的(线性代数、凸优化)不是一套。 | ||
| 所以这本书停在了层级模型的门口。 | ||
| 门后面是什么,得另开一本。 |
三、把复杂度变成一个数
「模型复杂度」听起来很虚,其实可以量化。 对线性 smoother(预测值是训练标签的线性组合)这一类模型,有效参数个数就等于平滑矩阵的迹:
S = K * (K' * K) 的逆 * K' 预测值 = S * 训练标签
有效参数个数 = trace(S)
为什么叫「有效」?因为它不一定等于参数的名义个数。 常数模型名义上 1 个参数,有效参数也是 1。 线性模型名义上 2 个(斜率加截距),有效参数也是 2。 核回归没有「参数」这个概念,但它照样有一个等效的复杂度——这就是这个量的用处。 实测带宽从 0.08 调到 50,有效参数和误差这样走:
| 带宽 | 有效参数 | 测试误差 |
|---|---|---|
| 0.08 | 24.33 | 0.1138 |
| 0.15 | 14.62 | 0.0955 |
| 0.25 | 9.40 | 0.0971 |
| 0.4 | 6.16 | 0.1219 |
| 0.7 | 3.77 | 0.2181 |
| 1.2 | 2.42 | 0.3988 |
| 2 | 1.65 | 0.5893 |
| 3.5 | 1.23 | 0.7426 |
| 50 | 1.00 | 0.8575 |
| 这张表可以横向读成一个连续谱。 | ||
| 最右边是「什么都不看」的常数模型,最左边是「什么都看」的局部拟合。 | ||
| 中间有一个最低点,落在 14.62 个有效参数附近。 | ||
| 注意最右边那一行:带宽 50 时有效参数是 1.00,和常数模型完全一致, | ||
| 误差 0.8575 也和常数模型的 0.8581 只差 0.0006。 | ||
| 这不是巧合,是必然——带宽无穷大时所有样本的权重相等,核回归就是取平均。 | ||
| 顺带纠正一个很容易犯的直觉错误:有人以为带宽调到很大,核回归会退化成直线。 | ||
| 不会,它退化成水平线。 | ||
| 直线的误差是 0.5750,水平线是 0.8575——所以极端情形的核回归比线性模型还差。 |
四、那条 U 形曲线为什么必然出现
把误差拆成两半,U 形就是必然的。
期望误差 = 偏差的平方 + 方差 + 噪声
模型太简单,偏差大(学不到真实形状);模型太复杂,方差大(换个数据集,结果就变)。 两者此消彼长,加起来必然有个最低点。 本节的数据把这三项都摆出来了。 噪声那一项是 0.09,谁也消不掉。 最优的 0.0955 只比它高 0.0055——说明在这个带宽上,偏差和方差已经压得很小。 而常数模型的 0.8581 里,绝大部分是偏差。 这个分解解释了全书几乎所有方法的动机。 ℓ1 和 ℓ2 约束、间隔最大化、Dropout(虽然本书没讲)——都在压方差。 核映射、层级模型——都在压偏差。 而所有「调参」,都是在找那个平衡点。 需要说清楚的是:这个分解是理论上的期望,实际只能看到一个总误差。 你没法直接从数据里读出偏差是多少、方差是多少,只能靠交叉验证去逼近那个最低点。
五、这本书没讲的东西
第一,层级模型。 上面说过了,是刻意留白。 第二,真正的高维和大规模。 本书所有 Demo 的样本量都在几百这个量级,这个规模下矩阵求逆毫无压力。 可真实场景动辄百万样本、百万维,那时候连「把核矩阵存下来」都做不到,得换随机梯度、换近似核、 换分布式——这些是工程问题,本书没碰。 第三,不确定性。 书里所有方法给出的都是一个点预测:「这个样本的值是 3.7」。 但很多时候我们想知道的是「这个预测有多可信」。 贝叶斯方法能给出后验分布,本书只在第 5 章概率模型那一带擦了个边。 第四,因果。 全书都在学「相关」:给定 x,y 的期望是多少。 而「把 x 改一改,y 会不会跟着变」是另一个问题,需要额外的假设。 第 18 章讲协变量移位时其实已经摸到边了——那里的关键假设是「条件分布不变」, 这个假设本身就是因果性的。
六、动手 Demo:一个旋钮走完全书:从常数到局部,复杂度就看有效参数个数
这段代码用 60 个点拟合一条弯曲线,把核回归的带宽从 0.08 调到 50, 同时算出每个带宽对应的有效参数个数,最后和常数模型、线性模型对照。 重点看三处。第三段误差从 0.1138 降到 0.0955 再升到 0.8575;第四段带宽 50 的有效参数正好 1.00; 第五段 14.62 个有效参数处误差最低而在 24.33 处又回去了。
# -*- coding: utf-8 -*-
import numpy as np
def data(n=60, seed=1, noise=0.3):
"""一条弯的曲线,加噪声。"""
rg = np.random.default_rng(seed)
x = np.sort(rg.uniform(-3.0, 3.0, n))
y = np.sin(1.5 * x) + 0.3 * x + rg.normal(0.0, noise, n)
return x, y
def nw(x, y, xq, h):
"""核回归:每个查询点用邻近样本的加权平均,权重随距离按高斯衰减。"""
D = (xq[:, None] - x[None, :]) ** 2
K = np.exp(-D / (2.0 * h * h))
return (K @ y) / K.sum(axis=1)
def dof(x, h):
"""有效参数个数:平滑矩阵的迹。h 越小,它越大。"""
D = (x[:, None] - x[None, :]) ** 2
K = np.exp(-D / (2.0 * h * h))
S = K / K.sum(axis=1, keepdims=True)
return float(np.trace(S))
x, y = data()
rg = np.random.default_rng(99)
xq = np.sort(rg.uniform(-3.0, 3.0, 400))
yq = np.sin(1.5 * xq) + 0.3 * xq + rg.normal(0.0, 0.3, 400)
print('[1] 60 个训练点,真函数是一条上下起伏的曲线')
print(' 训练 %d 个点,测试 %d 个点' % (len(x), len(xq)))
print(' 噪声标准差 0.30,也就是任何模型都不可能把误差做到 0.09 以下')
print('')
print('[2] 两个极端先摆出来')
A = np.column_stack([np.ones(len(x)), x])
c = np.linalg.solve(A.T @ A, A.T @ y)
e_const = float(np.mean((y.mean() - yq) ** 2))
e_line = float(np.mean((c[0] + c[1] * xq - yq) ** 2))
print(' 常数模型(1 个参数) 测试误差 %.4f' % e_const)
print(' 线性模型(2 个参数) 测试误差 %.4f' % e_line)
print(' 曲线明显不是直的,所以直线也只是勉强')
print('')
print('[3] 核回归:一个带宽旋钮,从最局部调到最全局')
print(' 带宽 有效参数 测试误差')
rows = []
for h in (0.08, 0.15, 0.25, 0.4, 0.7, 1.2, 2.0, 3.5, 50.0):
e = float(np.mean((nw(x, y, xq, h) - yq) ** 2))
d = dof(x, h)
rows.append((h, d, e))
print(' %-8g %6.2f %.4f' % (h, d, e))
best = min(rows, key=lambda r: r[2])
print(' 最好的带宽 %.2f:%6.2f 个有效参数,误差 %.4f' % (best[0], best[1], best[2]))
print('')
print('[4] 带宽调到无穷大,不是变成直线,而是变成一条水平线')
print(' 带宽 50 的有效参数是 %.2f,常数模型是 1.00,两者是一回事' % rows[-1][1])
print(' 所以它的误差 %.4f 才比线性模型的 %.4f 还差' % (rows[-1][2], e_line))
print(' 一头是「什么细节都不看」,一头是「连噪声都当信号」')
print('')
print('[5] 有效参数越多越好吗')
print(' 从 1.00 加到 14.62,误差一路降到 %.4f' % best[2])
print(' 从 14.62 加到 24.33,误差反而回到 %.4f' % rows[0][2])
print(' 最优的 14.62 已经很接近能用的上限,噪声下限是 0.09')
实跑输出:
--------------------------------------------------------------------
一个旋钮走完全书:从常数到局部,复杂度就看有效参数个数
--------------------------------------------------------------------
[1] 60 个训练点,真函数是一条上下起伏的曲线
训练 60 个点,测试 400 个点
噪声标准差 0.30,也就是任何模型都不可能把误差做到 0.09 以下
[2] 两个极端先摆出来
常数模型(1 个参数) 测试误差 0.8581
线性模型(2 个参数) 测试误差 0.5750
曲线明显不是直的,所以直线也只是勉强
[3] 核回归:一个带宽旋钮,从最局部调到最全局
带宽 有效参数 测试误差
0.08 24.33 0.1138
0.15 14.62 0.0955
0.25 9.40 0.0971
0.4 6.16 0.1219
0.7 3.77 0.2181
1.2 2.42 0.3988
2 1.65 0.5893
3.5 1.23 0.7426
50 1.00 0.8575
最好的带宽 0.15: 14.62 个有效参数,误差 0.0955
[4] 带宽调到无穷大,不是变成直线,而是变成一条水平线
带宽 50 的有效参数是 1.00,常数模型是 1.00,两者是一回事
所以它的误差 0.8575 才比线性模型的 0.5750 还差
一头是「什么细节都不看」,一头是「连噪声都当信号」
[5] 有效参数越多越好吗
从 1.00 加到 14.62,误差一路降到 0.0955
从 14.62 加到 24.33,误差反而回到 0.1138
最优的 14.62 已经很接近能用的上限,噪声下限是 0.09
第一段那句「任何模型都不可能把误差做到 0.09 以下」是全场的地基。
没有这条线,所有误差数字都没法判断好坏。
0.0955 之所以漂亮,是因为它只比 0.09 高一点点;0.1138 之所以还算可以,是因为也只高 0.024。
第二段两个极端先摆出来很有必要。
常数 0.8581、线性 0.5750,两个都很差——说明这条曲线确实不是直线能描述的。
有了这两个数垫底,后面 0.0955 的意义才显得出来。
第三段那张表要横着看一遍。
有效参数从 24.33 一路降到 1.00,误差从 0.1138 先降到 0.0955 再一路升到 0.8575。
两个方向相反的单调,夹出一个最低点。
第四段是本篇最该停下来的地方。
带宽 50 的有效参数是 1.00,常数模型也是 1.00,误差是 0.8575 对 0.8581。
两个完全不同写法的模型,在这个极限上是同一个东西。
「有效参数个数」这个量的意义就在这里:它能横跨不同的模型族做比较。
第五段那句「最优的 14.62 已经很接近能用的上限」要说得准一点:
0.0955 比噪声下限 0.09 高 0.0055,剩下这点差距里还有模型的偏差。
想再往下压,得换更强的模型族,或者加数据——调带宽已经到头了。
代码里 dof 那个函数算的是平滑矩阵的迹。
注意它用的是训练点自己算的矩阵(60 乘 60),不是测试点,
因为「模型有多少个有效参数」是模型自身的属性,和拿什么数据评估无关。
30 秒实验
- 把
data的n从 60 改成 200,重跑。最优带宽会变小,最优误差会更接近 0.09。 数据越多,越敢用复杂的模型——这就是「大数据让复杂模型变得可行」的算术含义。 - 把
data的noise从 0.3 改成 0.9,重跑。最优带宽会变大,误差下限涨到 0.81。 噪声越大,越该用简单的模型——因为复杂模型学到的多半是噪声。 这一条是实战里最反直觉、也最值钱的判断。 - 把
data里的np.sin(1.5 * x) + 0.3 * x换成0.3 * x(真函数变成直线),重跑。 最优带宽会跳到极大,核回归自动退化成线性模型附近的样子。 模型选择不只是调参,它也在替你判断「这个问题到底需要多复杂」。
常见误区
- 「模型越复杂,效果越好」。 实测有效参数从 14.62 加到 24.33,误差从 0.0955 涨到 0.1138。 再往上加会一直变差。 复杂度不是收益,是成本——只有当数据撑得起时才划算。
- 「核回归的带宽调到无穷大,就退化成线性回归」。 不会,退化成常数。 实测带宽 50 的有效参数是 1.00,误差 0.8575,而线性模型是 0.5750。 两者的差别在于:线性模型还保留了「随输入线性变化」这一层结构。
- 「训练误差能反映模型好坏」。 本书里反复出现「训练集几乎完美、测试集一塌糊涂」的情形: 第 19.3 节训练 0.0204 而测试 0.0675,核分类器训练 1.000 而测试 0.785。 训练误差只能告诉你「模型有没有能力拟合」,不能告诉你「拟合得对不对」。
- 「调参是玄学」。 不是。 每一项超参数都对应偏差和方差之间的一个具体取舍,而且方向是可预测的:约束越松越偏方差, 越紧越偏低偏差。 所谓玄学,通常是因为没想清楚「我在拿什么换什么」。
自测三问(点开对答案)
1. 「有效参数个数」是什么?为什么它比「参数个数」更有用? —— 对预测值是训练标签线性组合的模型,它等于平滑矩阵的迹。 参数个数只对参数化模型有意义,核回归这类没有显式参数的模型就没法比; 有效参数个数能横跨不同模型族。实测带宽 50 时它是 1.00,和常数模型完全一致。 2. 为什么误差曲线一定是 U 形? —— 因为期望误差 = 偏差平方 + 方差 + 噪声。 模型太简单偏差大,太复杂方差大,两者此消彼长,加起来必有最低点。 本节噪声是 0.09,最优 0.0955 只比它高 0.0055,常数模型 0.8581 里绝大部分是偏差。 3. 数据变多或噪声变大,最优复杂度该往哪边调? —— 数据变多,最优复杂度可以调高——样本撑得起更多参数,方差不会爆。 噪声变大,最优复杂度要调低——因为复杂模型学到的多半是噪声。这两条方向相反,实战里要一起看。这本书停在层级模型的门口。 线性模型和核模型这两族,工具已经给全了; 而「把很多简单模型叠起来」这条路,只用 2.3 节的一段话带过。 下一本书就进那扇门:多层网络怎么搭、误差怎么反向传、卷积为什么能省参数。 那里面的数学换了一套,但这一章讲的这条 U 形曲线,一个字都不会变。
- ☐ (全书完)