🤖 第20章 总结与展望
📄 一句话概括

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 秒实验

  1. 把 data 的 n 从 60 改成 200,重跑。最优带宽会变小,最优误差会更接近 0.09。 数据越多,越敢用复杂的模型——这就是「大数据让复杂模型变得可行」的算术含义。
  2. 把 data 的 noise 从 0.3 改成 0.9,重跑。最优带宽会变大,误差下限涨到 0.81。 噪声越大,越该用简单的模型——因为复杂模型学到的多半是噪声。 这一条是实战里最反直觉、也最值钱的判断。
  3. 把 data 里的 np.sin(1.5 * x) + 0.3 * x 换成 0.3 * x(真函数变成直线),重跑。 最优带宽会跳到极大,核回归自动退化成线性模型附近的样子。 模型选择不只是调参,它也在替你判断「这个问题到底需要多复杂」。

常见误区

  1. 「模型越复杂,效果越好」。 实测有效参数从 14.62 加到 24.33,误差从 0.0955 涨到 0.1138。 再往上加会一直变差。 复杂度不是收益,是成本——只有当数据撑得起时才划算。
  2. 「核回归的带宽调到无穷大,就退化成线性回归」。 不会,退化成常数。 实测带宽 50 的有效参数是 1.00,误差 0.8575,而线性模型是 0.5750。 两者的差别在于:线性模型还保留了「随输入线性变化」这一层结构。
  3. 「训练误差能反映模型好坏」。 本书里反复出现「训练集几乎完美、测试集一塌糊涂」的情形: 第 19.3 节训练 0.0204 而测试 0.0675,核分类器训练 1.000 而测试 0.785。 训练误差只能告诉你「模型有没有能力拟合」,不能告诉你「拟合得对不对」。
  4. 「调参是玄学」。 不是。 每一项超参数都对应偏差和方差之间的一个具体取舍,而且方向是可预测的:约束越松越偏方差, 越紧越偏低偏差。 所谓玄学,通常是因为没想清楚「我在拿什么换什么」。
自测三问(点开对答案) 1. 「有效参数个数」是什么?为什么它比「参数个数」更有用? —— 对预测值是训练标签线性组合的模型,它等于平滑矩阵的迹。 参数个数只对参数化模型有意义,核回归这类没有显式参数的模型就没法比; 有效参数个数能横跨不同模型族。实测带宽 50 时它是 1.00,和常数模型完全一致。 2. 为什么误差曲线一定是 U 形? —— 因为期望误差 = 偏差平方 + 方差 + 噪声。 模型太简单偏差大,太复杂方差大,两者此消彼长,加起来必有最低点。 本节噪声是 0.09,最优 0.0955 只比它高 0.0055,常数模型 0.8581 里绝大部分是偏差。 3. 数据变多或噪声变大,最优复杂度该往哪边调? —— 数据变多,最优复杂度可以调高——样本撑得起更多参数,方差不会爆。 噪声变大,最优复杂度要调低——因为复杂模型学到的多半是噪声。这两条方向相反,实战里要一起看。

这本书停在层级模型的门口。 线性模型和核模型这两族,工具已经给全了; 而「把很多简单模型叠起来」这条路,只用 2.3 节的一段话带过。 下一本书就进那扇门:多层网络怎么搭、误差怎么反向传、卷积为什么能省参数。 那里面的数学换了一套,但这一章讲的这条 U 形曲线,一个字都不会变。

相关笔记