📗 第6章 提高泛化能力的方法
📄 一句话概括

这一节把五种手段一项项加到同一个模型上,看训练与测试那道沟怎么变。 起点是 100 张图、原始像素、tanh:训练 1.0000、测试 0.8562,沟 0.1438。 加数据增强后测试跳到 0.9475,沟缩到 0.0225——这是整章最划算的一步。 再标准化反而略降到 0.9337,换 ReLU 升到 0.9700。 最后加 Dropout 却掉到 0.8850。 这就是这一章最该记住的一句:手段不是叠得越多越好。

📝 本节和前面几节的关系

前面五节各自讲一种手段,各自验证各自的效果。 这一节做一件前面没做的事:把它们放进同一个模型,一项项累加。 单独有效的两件事放在一起,可能互相抵消——这是所有「技巧清单」类文章最不愿意提的一点。

一、一项项加上去

起点刻意选最裸的一版:100 张图、原始 0/1 像素、tanh、不做任何正则化。 然后按「改数据、改预处理、改激活函数、改正则」的顺序一项项加。

累加到哪一步 训练准确率 测试准确率 沟
裸跑 1.0000 0.8562 0.1438
加数据增强 8 倍 0.9700 0.9475 0.0225
再加标准化 0.9700 0.9337 0.0363
再换 ReLU 0.9600 0.9700 -0.0100
再加 Dropout 0.7 0.9200 0.8850 0.0350
第一行沟有十四个百分点,这是过拟合的原始状态。
加上数据增强,沟一下缩到两个多点——这一步贡献了整章七成以上的改善。
标准化那一步反而略降。
原因在第 2 节量过:这份数据的条件数只有 357.7,本来就不歪,除标准差白忙。
「有一步没加分」这件事本身就值得记:不是每个技巧在每份数据上都有效。

二、换 ReLU 那条要单独说

换 ReLU 那一步测试到了 0.9700,是五行里最高的,沟甚至是负的。 但这一步有个隐藏成本:学习率必须从 0.1 压到 0.02。 如果照抄 0.1,训练准确率会塌到 0.09——整个网络一轮就炸,输出退化成常数。 这一点第 3 节已经量化过:ReLU 的学习率到 0.3 就整锅崩。 所以「换激活函数几乎免费」这句话有前提:你得顺手把学习率一起调。 清单类文章通常只写「换 ReLU 效果更好」,不写「顺便把学习率降五倍」。

三、最后一步为什么是负收益

最后加 Dropout,测试从 0.9700 掉到 0.8850,掉了八点五。 这不是 Dropout 坏了,是它在这个位置不合适。 数据增强已经把 100 张堆到了 800 张,过拟合基本被压住了。 这时候再加一层正则化,等于在已经不缺约束的地方继续加约束——有效容量被压得不够用, 直接从「刚好」滑到「欠拟合」。 回看第 4 节那张表就明白了:训练张数 400 时,用与不用 Dropout 都是 1.0000。 数据够了,它就没活干了。

四、五项办法的账

把收益和代价放在一张表里:

办法 主要作用 代价
加训练样本 直接缩小沟 标注最贵,要人一条条标
数据增强 把已知规律写成数据 几乎免费,但要懂任务
清洗标签 去噪、去矛盾 要人复核,量大时很磨
预处理 让学习率不敏感 算一次,白化要存矩阵
换激活函数 梯度传得下去 改一行,但要重调学习率
Dropout 压过拟合 训练变慢,容量变小
DropConnect 同上,粒度更细 另存一份同形 mask,更贵
性价比最高的两项是「换激活函数」和「数据增强」,几乎不花钱。
最贵的两项是「加样本」和「清标签」,可它们的收益最扎实。
中间三项看情况——数据歪就做预处理,过拟合重就上 Dropout。

五、三条不能忘的前提

一、这些手段都在压过拟合,压不出更高的上限。 数据本身信息不够,再加正则化也只是让模型少背错题,不会变聪明。 二、过拟合不严重时,硬加正则化会拖后腿。 上面最后一行就是活例子:数据已经够了,Dropout 反而扣掉八点五。 三、先看沟,再动手。 训练和测试差得远,才轮到这一章的办法。 两边都低,那是欠拟合——该加容量、加步数、换模型,加 Dropout 只会更糟。

六、动手 Demo:五种手段逐项累加,看沟怎么变

数据 400 张训练、800 张测试,训练只取 100 张。 每一行都重新训练一遍,2000 步,同一个随机种子。 训练准确率一律在最初那 100 张上算。 这一点很重要:增强之后训练集有 800 张,如果拿 800 张算训练准确率,各行之间就不可比了。

def augment(X, y, mult=8, jit=1, seed=7):
    u"""把每张图平移一格,重复 mult 份。"""
    rg = np.random.default_rng(seed)
    XX, yy = [], []
    for _ in range(mult):
        for xa, ya in zip(X, y):
            img = xa.reshape(8, 8).copy()
            dr, dc = rg.integers(-jit, jit + 1, 2)
            XX.append(np.roll(np.roll(img, dr, 0), dc, 1).ravel().copy())
            yy.append(ya)
    return np.array(XX), np.array(yy)


def run(tag, X, y, xev, xts, act="tanh", keep=1.0, lr=0.1):
    m = fit(X, y, act=act, keep=keep, steps=2000, lr=lr, seed=11)
    # 训练准确率一律在最初那 100 张上算,这样每一步都可比
    print(tag, acc(xev, ys, m, act), acc(xts, yte, m, act))


Xa, ya = augment(Xs, ys, 8, 1)
run(u"裸跑", Xs, ys, Xs, Xte)
run(u"+ 增强", Xa, ya, Xs, Xte)
run(u"+ 标准化", (Xa - mu) / sd, ya, Xs_n, Xte_n)
run(u"+ 换 ReLU", (Xa - mu) / sd, ya, Xs_n, Xte_n, act="ReLU", lr=0.02)
run(u"+ Dropout 0.7", (Xa - mu) / sd, ya, Xs_n, Xte_n,
    act="ReLU", keep=0.7, lr=0.02)

实跑输出:

--------------------------------------------------------------------
小结:五种手段一项项加上去,看沟怎么变窄
--------------------------------------------------------------------
[1] 这一章讲了五种办法,都是冲同一件事去的:训练集上太顺,新数据上就露怯
  下面把五种办法一项一项加上去,看「训练与测试的沟」怎么变
  起点是最裸的一版:100 张图、原始 0/1 像素、tanh、不做任何正则化
  训练准确率一律在最初那 100 张上算,这样每一步都可比
  累加到哪一步                  训练准确率    测试准确率    沟
  裸跑                          1.0000        0.8562        0.1438
  + 数据增强 8 倍               0.9700        0.9475        0.0225
  + 数据增强 8 倍、标准化       0.9700        0.9337        0.0363
  + 上面两项、换 ReLU           0.9600        0.9700        -0.0100
  + 上面三项、Dropout 0.7       0.9200        0.8850        0.0350

  第一行训练 1.0000、测试八成六,沟有十四个百分点
  加上数据增强,沟一下缩到两个多点——这一步是整章最划算的
  有趣的是标准化那一步反而略降:这份数据本来就不歪,除标准差白忙
  换 ReLU 时学习率必须从 0.1 压到 0.02,否则一轮就炸(训练只剩 0.09)
  「换激活函数几乎免费」这句话有前提:你得顺手把学习率一起调
  到这一行为止,测试 0.9700,比裸跑高出十一个百分点,沟基本没了
  可最后加 Dropout 反而掉了八点——增强已经把样本堆到 800 张,
  再叠一层正则化,有效容量就不够用了
  这就是这一章最该记住的一句:手段不是叠得越多越好

[2] 五项办法的收益与代价
  办法                  主要作用                  代价
  加训练样本            直接缩小沟                标注最贵,要人一条条标
  数据增强              把已知规律写成数据        几乎免费,但要懂任务
  清洗标签              去噪,去矛盾              要人复核,量大时很磨
  预处理                让学习率不敏感            算一次,白化要存矩阵
  换激活函数            梯度传得下去              换一行代码,最便宜
  Dropout               压过拟合                  训练变慢,容量变小
  DropConnect           同上,粒度更细            另存一份同形 mask,更贵

  性价比最高的两项是「换激活函数」和「数据增强」,几乎不花钱
  最贵的两项是「加样本」和「清标签」,可它们的收益最扎实

[3] 三条不能忘的前提
  一、这些手段都在压过拟合,压不出更高的上限
      数据本身信息不够,再加正则化也只是让模型少背错题,不会变聪明
  二、过拟合不严重时,硬加正则化会拖后腿
      上面最后一行就是活例子:数据已经够了,Dropout 反而扣掉八点
      第 4 节那个实验也一样,训练集给到 400 张,它就不划算了
  三、先看沟,再动手
      训练和测试差得远,才轮到这一章的办法;两边都低,那是欠拟合,
      该加容量、加步数、换模型,而不是加 Dropout

[4] 一句话收这一章
  泛化能力不是「训出来的」,是「用数据和对数据的理解约束出来的」
  下一章换工具:这一章所有实验都是手写 numpy,第七章讲现成的框架

run 里多传一个 xev 参数,就是为了这件事。 X 和 y 是真正拿去训练的数据(可能已经增强过),xev 是用来算训练准确率的那 100 张。 两者分开,前面几行才有可比性。 最后一行 keep=0.7 配上 lr=0.02,两个超参数是绑在一起的。 如果沿用 0.1,这一行会因为 ReLU 崩塌而变成 0.09,看起来像「Dropout 把模型搞坏了」, 其实是学习率的问题。 第四行那个「沟是负的」不要当成异常。 训练准确率是在 100 张原始图上算的,测试是 800 张;测试集更大、更平滑,偶尔高过训练集很正常。

30 秒实验

  1. 把累加顺序倒过来:先加 Dropout,再加增强。 最后一行仍然会掉,但掉的位置变了——说明「第几步加」也有影响。
  2. 把最后一行 Dropout 换成 DropConnect 0.7。 结果差不多,也在 0.88 附近,同样不如不加。
  3. 把起点从 100 张改成 400 张再逐项累加。 第一行的沟会小很多,后面几步的收益也会跟着缩水——因为一开始就没什么过拟合可压。

常见误区

  1. 「技巧都叠上,效果一定最好。」 实测最后一行 0.8850,比不加 Dropout 的 0.9700 低八点五。 每一项都有它的适用前提,前提不成立时,加上去就是负收益。
  2. 「标准化是标配,必须做。」 这一节里它让测试从 0.9475 掉到 0.9337。 标准化的作用是让学习率不敏感,数据本来不歪的时候,它换不来分数。
  3. 「训练准确率掉了,说明正则化起作用了。」 第 4 节那张表里,保留率从 1.0 扫到 0.3,训练准确率一直是 1.0000。 容量远大于任务难度时,丢一半单元也照样拟合。 判断正则化有没有用,要看测试准确率,不是看训练准确率掉了没有。
自测三问(点开对答案) 1. **为什么数据增强这一步的收益最大?** —— **因为它在增加「约束」而不损失「容量」。** **它把「数字挪一格还是同一个数字」这条你已经知道的规律,变成了 700 张新样本。** **其他手段要么在减容量(Dropout),要么在改尺度(预处理),都不增加信息。** 2. **训练和测试都低的时候,该做什么?** —— **那是欠拟合,不该用这一章的任何手段。** **该做的是加容量、加训练步数、换更强的结构或激活函数。** **加 Dropout 只会让它更糟——你在给一个本来就学不动的模型继续上枷锁。** 3. **为什么「沟」比「测试准确率」更适合当诊断指标?** —— **因为测试准确率混了两件事:模型好不好、任务难不难。** **沟只反映一件事:模型有没有在训练集上占便宜。** **沟大就是过拟合,两边都低就是欠拟合,对症才能下药。**

这一章所有实验都是手写 numpy:自己写前向、自己推反传、自己实现 dropout。 下一章换工具——Theano、Pylearn2、Caffe、Chainer、TensorFlow,这些框架把这些活全包了。

相关笔记