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

激活函数是每个神经元的门,它决定两件事:非线性够不够强,梯度好不好传。 核心数字是导数上限:sigmoid 只有 0.25,tanh 是 1,ReLU 在正区间恒为 1。 反传是连乘。固定权重灌一个梯度进去,走完三层后 sigmoid 剩 5.39e-05, 而它进最后一层时是 1.91e-03——掉了三十多倍。 tanh 和 ReLU 只掉三四倍。 实际训练同一个网络:sigmoid 测试 0.5475,tanh 0.9437,softsign 0.9175,ReLU 0.9587。 sigmoid 只用了两层隐层就落后四十个百分点。

📝 本节和前面几节的关系

第 2 章讲误差反向传播时提过激活函数,但没讲为什么换一个就训练不动。 这一节把它补上:关键在「连乘」。 这一节也是全章唯一一处「不改数据、不改结构、只换一行代码」的改动。 代价最低,但踩坑也最容易——ReLU 的学习率容忍范围比 tanh 窄得多。

一、四种门,四种形状

四个候选,都是老面孔:

名字 输出范围 导数最大 两头会饱和吗
sigmoid 0 到 1 0.25 会,两头趋近 0
tanh -1 到 1 1.0 会,两头趋近 0
ReLU 0 到正无穷 1.0(正侧) 不会,但负侧梯度直接是 0
softsign -1 到 1 1.0 会,但尾巴更厚
具体数值看一眼更直观。
x 等于 0 时,sigmoid 输出 0.500、导数 0.250;tanh 输出 0.000、导数 1.000;
softsign 也是导数 1.000。
x 到正负 4 时,sigmoid 和 tanh 的导数都掉到 0.02 以下,softsign 还剩 0.04。
softsign 的尾巴比 tanh 厚一倍,这是它唯一的优势:饱和得慢一点。
而 ReLU 在正半轴压根不饱和,导数恒为 1——这是它统治现代网络的直接原因。

二、梯度消失就是连乘

反向传播每往回走一层,梯度要乘上这一层的导数。 层数一多,就是连乘一串小于 1 的数。 0.25 乘十次约等于 0.000001,这就是「消失」。 实测的办法是把权重随机定死、不再更新,只在输出端灌一个固定梯度,看它传到每一层剩多少:

激活函数 第一层 第二层 第三层
sigmoid 5.393e-05 3.528e-04 1.908e-03
tanh 2.096e-03 4.293e-03 7.137e-03
softsign 9.956e-04 2.952e-03 6.068e-03
ReLU 1.149e-03 1.752e-03 4.539e-03
从右往左读:sigmoid 每过一层掉一截,走完三层只剩不到四十分之一。
tanh 和 softsign 也掉,但只掉三四倍,温和得多。
要诚实说一句:这个比较里混着权重尺度的影响,不能全算到激活函数头上。
但 sigmoid 掉得最狠这一点是稳的——它的导数上限天生只有 0.25,一乘就缩水。
层数越多,这种连乘越致命。

三、实际训练一遍

光看梯度还不够,得看它能不能真的学出来。 同一个网络 64 到 32 到 16 到 10,学习率 0.1,1500 步:

激活函数 训练准确率 测试准确率
sigmoid 0.5975 0.5475
tanh 1.0000 0.9437
softsign 1.0000 0.9175
ReLU 1.0000 0.9587
sigmoid 连训练集都只学到 0.5975,测试只有一半——它不是「差一点」,是根本学不动。
tanh 和 softsign 都过了 0.9,ReLU 最高。
注意这只用了两层隐层,sigmoid 就落后四十个百分点。
层数再深,它只会更糟;tanh 也会开始饱和。
这就是为什么 2012 年之后的新网络几乎全用 ReLU 系。

四、ReLU 的坑:学习率一给大就全塌

ReLU 不是没有代价。 它的负半轴导数是 0,一旦某个单元的输出恒为 0,它的梯度也恒为 0,这块参数就永久报废。 这叫 dying ReLU。 先看学习率对它的影响:

学习率 第一层死单元 第二层死单元 测试准确率
0.1 0 / 32 0 / 16 0.9700
0.2 0 / 32 0 / 16 0.9725
0.3 0 / 32 0 / 16 0.1000
0.5 0 / 32 0 / 16 0.1000
学习率到 0.3,准确率直接塌到 0.1000——800 张图全猜成同一个数字。
可死单元那一列还是 0:这不是一个个单元慢慢死,是整锅一起炸了。
步子太大,一轮更新就把权重推到很远处,所有单元全卡住,输出变成一个常数。
所以这次没测出真正的 dying ReLU,如实说明:要复现它得用更细的调参——小学习率、长时间训练,
让个别单元在训练中被推到负半轴再也没回来。
原理本身是成立的,只是这次实验的粒度不够。

五、动手 Demo:四种激活函数的梯度实测、训练对比、以及 ReLU 的崩塌点

数据 400 张训练、800 张测试,先做标准化。 四层网络 64 到 32 到 32 到 16 到 10,只用于梯度那一节; 训练那一节用 64 到 32 到 16 到 10 的两层网络。 代码里最值得留意的是 DACT 那四个 lambda:四个导数都能只用「输出值 a」写出来, 反传时不必再存 z。

ACT = {
    u"sigmoid": lambda z: 1.0 / (1.0 + np.exp(-np.clip(z, -60, 60))),
    u"tanh": np.tanh,
    u"ReLU": lambda z: np.maximum(z, 0.0),
    u"softsign": lambda z: z / (1.0 + np.abs(z)),
}

# 四个导数都能只用「输出值 a」写出来,反传时不必再存 z
DACT = {
    u"sigmoid": lambda a: a * (1.0 - a),
    u"tanh": lambda a: 1.0 - a * a,
    u"ReLU": lambda a: (a > 0).astype(float),
    u"softsign": lambda a: (1.0 - np.abs(a)) ** 2,
}

# 权重随机定死、不再更新,只在输出端灌一个固定梯度,
# 看它倒着传到每一层还剩多少
d = np.full((len(Xtrn), 10), 0.01) @ Ws[-1].T
norms = []
for i in range(len(Ws) - 2, -1, -1):
    d = d * DACT[nm](As[i + 1])
    norms.append(float(np.abs(d).mean()))
    if i:
        d = d @ Ws[i].T

# 实际训练时只换激活函数,其他一概不动
for nm in (u"sigmoid", u"tanh", u"softsign", u"ReLU"):
    m = fit(Xtrn, ytr, act=nm, steps=1500, lr=0.1, seed=8)
    print(nm, acc(Xtrn, ytr, m, nm), acc(Xten, yte, m, nm))

实跑输出:

--------------------------------------------------------------------
激活函数:门的形状决定梯度能不能传回去
--------------------------------------------------------------------
[1] 激活函数就是每个神经元的「门」,它决定信号怎么过去
  下面四个都算一遍:在几个关键点上输出多少、斜率多少
  x       sigmoid             tanh                ReLU            softsign
  -4.0    0.018 / 0.018       -0.999 / 0.001      0.000 / 0.000   -0.800 / 0.040
  -2.0    0.119 / 0.105       -0.964 / 0.071      0.000 / 0.000   -0.667 / 0.111
  -1.0    0.269 / 0.197       -0.762 / 0.420      0.000 / 0.000   -0.500 / 0.250
  0.0     0.500 / 0.250       0.000 / 1.000       0.000 / 0.000   0.000 / 1.000
  1.0     0.731 / 0.197       0.762 / 0.420       1.000 / 1.000   0.500 / 0.250
  2.0     0.881 / 0.105       0.964 / 0.071       2.000 / 1.000   0.667 / 0.111
  4.0     0.982 / 0.018       0.999 / 0.001       4.000 / 1.000   0.800 / 0.040
  (每格是「输出值 / 导数」)

  sigmoid 的导数最大只有 0.25,两头还趋近 0
  tanh 的导数最大是 1,两头同样趋近 0
  ReLU 正的这边导数恒为 1,负的那边恒为 0
  softsign 导数最大 1,但尾巴比 tanh 更厚,掉得慢一点

[2] 导数上限为什么重要:反向传播是连乘
  搭一个四层网络 64-32-32-16-10,权重随机定死不再更新,
  只在输出端灌进一个固定的梯度,看它传到每一层还剩多少:
  激活函数    隐层 1      隐层 2      隐层 3
  sigmoid     5.393e-05   3.528e-04   1.908e-03
  tanh        2.096e-03   4.293e-03   7.137e-03
  softsign    9.956e-04   2.952e-03   6.068e-03
  ReLU        1.149e-03   1.752e-03   4.539e-03

  从右往左读:sigmoid 每过一层就掉一截,走完三层剩不到四十分之一
  tanh 和 softsign 也掉,但只掉三四倍,比 sigmoid 温和得多
  ReLU 这一轮掉得跟 tanh 差不多,因为它有一半单元在负半轴、梯度直接是 0
  这个比较里混着权重尺度的影响,不能全算到激活函数头上,
  但 sigmoid 掉得最狠这一点是稳的:它的导数上限天生只有 0.25,一乘就缩水
  层数越多,这种连乘越致命,这就是梯度消失

[3] 换成实际训练,四种激活各跑一遍
  同一个网络 64-32-16-10,学习率 0.1,1500 步:
  激活函数    训练准确率    测试准确率
  sigmoid     0.5975        0.5475
  tanh        1.0000        0.9437
  softsign    1.0000        0.9175
  ReLU        1.0000        0.9587

  sigmoid 明显落后,测试只有一半:它在这个深度上已经学不动了
  tanh 和 softsign 都在 0.9 以上,ReLU 最高
  注意这只用了两层隐层,sigmoid 就落后四十个百分点
  层数再深,它只会更糟;tanh 也会开始饱和,ReLU 才是现在的主流

[4] ReLU 的坑:学习率一给大就全塌
  学习率往上加,看有多少个隐单元在全部训练样本上都输出 0:
  学习率    第一层死单元    第二层死单元    测试准确率
  0.1       0 / 32          0 / 16          0.9700
  0.2       0 / 32          0 / 16          0.9725
  0.3       0 / 32          0 / 16          0.1000
  0.5       0 / 32          0 / 16          0.1000

  学习率到 0.3,准确率直接塌到 0.1000——800 张图全猜成同一个数字
  可死单元那一列还是 0:这不是一个个单元慢慢死,是整锅一起炸了
  步子太大,一轮更新就把权重推到很远处,所有单元全卡在饱和区,
  输出变成一个常数,梯度再也拉不回来

  这次没测出真正的 dying ReLU,如实说明:要复现它得用更细的调参——
  小学习率、长训练,让个别单元在训练中被推到负半轴再也没回来
  原理本身是成立的:单元一旦对所有样本都输出 0,它的导数恒为 0,
  梯度也恒为 0,这块参数等于永久报废,再也活不过来
  对策:学习率压小,或者换负半轴留一点斜率的 Leaky ReLU

[5] 小结
  激活函数影响两件事:非线性够不够强,梯度好不好传
  梯度好不好传,看导数上限和饱和区——连乘的层数越多越致命
  下一节讲 Dropout:不动激活函数,改「训练时让一部分单元随机停工」

DACT 那四个式子值得单独验一遍。 sigmoid 的导数 a 乘 (1 减 a),tanh 是 1 减 a 平方,ReLU 是 a 大于 0, softsign 是 (1 减 |a|) 的平方。 四个都只依赖输出,这是它们能写成查表形式的原因。 梯度那段的关键是「权重定死」。 如果让权重一起更新,各层的梯度会被训练过程搅在一起,就看不出连乘的效果了。 定死权重,等于只测「传导」这一个环节。 第三段那张表里,sigmoid 的训练准确率只有 0.5975。 这不是过拟合,是欠拟合——连见过的题都没学会。 判断方法很简单:训练低、测试也低,就是欠拟合;训练高、测试低,才是过拟合。

30 秒实验

  1. 把 ReLU 那组的学习率从 0.1 改成 0.01,再跑一遍死单元统计。 准确率会稳在 0.97 附近,死单元仍然是 0——这份数据太「干净」,不容易死。
  2. 把 DACT 里 sigmoid 的导数故意写成 a 乘 a,再跑梯度那段。 各层梯度会整体变小,但「越往前越小」的趋势不变——说明趋势来自连乘,不是数值。
  3. 给 sigmoid 那组把步数从 1500 加到 8000。 准确率会往上爬,但爬得很慢;这就是「能用,但划不来」。

常见误区

  1. 「ReLU 比 sigmoid 好,换了就行。」 同一份代码里换 ReLU,学习率必须跟着压小,否则一轮就炸(0.3 时掉到 0.1000)。 「好」是有前提的,前提是超参数跟着调。
  2. 「梯度消失是激活函数造成的。」 激活函数只是一半原因,另一半是权重初始化。 初始权重太小,连乘一样会缩水;初始权重太大,tanh 和 sigmoid 会直接饱和。 这也是为什么有了 Xavier 和 He 初始化这两套公式。
  3. 「sigmoid 已经淘汰了,不能用。」 它在浅层网络、二分类输出层、门控结构里仍然常见。 准确的说法是:在深层网络的中坚层里,它的连乘代价太大。
自测三问(点开对答案) 1. **为什么 sigmoid 的导数上限是 0.25,这个数怎么来的?** —— **sigmoid 的导数是 a 乘 (1 减 a),a 的取值范围是 0 到 1。** **这个二次函数在 a 等于 0.5 时取最大值 0.25。** **所以不管输入是什么,梯度传过 sigmoid 最多只剩四分之一。** 2. **ReLU 正半轴导数是 1,为什么梯度还是会变小?** —— **因为负半轴的单元导数是 0,它们这一轮完全不传梯度。** **另外权重矩阵本身也会缩放梯度,所以「导数为 1」只保证不额外缩小,不保证梯度守恒。** 3. **训练准确率 0.5975、测试 0.5475,这是过拟合吗?** —— **不是,是欠拟合。** **过拟合的特征是训练高、测试低;这里是两边都低。** **欠拟合该做的是加容量、加步数、换激活函数,而不是加 Dropout。**

激活函数改的是每个单元「怎么响」。 下一节改的是「这一轮谁上班」——训练时随机让一部分单元停工,这就是 Dropout。

相关笔记