Dropout 的做法很反直觉:训练时每次随机让一部分神经元停工,包括它们往后的所有连线。 测试时全部单元都上班,一个不少。 100 张训练图、64 到 64 到 32 到 10:不用 Dropout 测试 0.8600,保留率 0.5 时 0.8950。 但训练准确率始终是 1.0000,一格没掉——网络容量太大,丢一半也背得下来。 数据给到 400 张时,用与不用都是 1.0000,一点差距都没有。 这一节最有用的结论是这个:Dropout 是没数据时的替补,不是万能加分项。
前几节改的都是数据或算子的形状,训练和测试走的是同一套计算。 这一节第一次引入「训练时和测试时行为不一样」的机制。 这也带来一个全新的坑:两边必须对得上。 训练时丢掉三成单元,测试时全开,下一层收到的信号就比训练时大三成——尺度一错位, 之前学的东西就白学了。
一、训练时扔硬币
具体做法:每喂一批数据,先给每个隐单元掷一次硬币,以保留率 p 的概率决定它这一轮上不上班。 没掷中的单元,输出置 0,往后所有连线一起失效。 重点在反向:这些单元的梯度也是 0,它们这一轮完全不更新。 下一批数据重新掷一次,可能换另一批单元休息。 效果是什么?每次训练的都是一个残缺的小网络,而测试时用的是完整的网络。 可以粗略理解成「训练了几十上百个小网络,测试时把它们平均起来」。 平均之后的模型更稳,不容易把宝押在某几个单元上。
二、保留率扫描
保留率 p 越小,丢得越狠。扫一遍看它怎么影响结果:
| 保留率 | 训练准确率 | 测试准确率 | 训练减测试 |
|---|---|---|---|
| 1.0 | 1.0000 | 0.8600 | +0.1400 |
| 0.9 | 1.0000 | 0.8600 | +0.1400 |
| 0.7 | 1.0000 | 0.8762 | +0.1238 |
| 0.5 | 1.0000 | 0.8950 | +0.1050 |
| 0.3 | 1.0000 | 0.8662 | +0.1338 |
| 不用 Dropout 那一行,训练 1.0000、测试只有八成六,中间那道沟就是过拟合。 | |||
| 保留率降下来,沟一路收窄,0.5 时测试最高、沟只剩一成。 | |||
| 但注意训练准确率一直是 1.0000,一格没掉。 | |||
| 不是 Dropout 没生效,是这个网络容量太大——6464 个参数只用 100 张图, | |||
| 丢一半单元也照样背得下来。 | |||
| 保留率压到 0.3 又掉头向下,说明丢太多了,剩下那点容量不够用。 | |||
| 所以 p 不是越小越好,0.5 附近是这类网络的常用值。 |
三、测试时必须补偿
训练时平均只有 p 比例的单位在上班,测试时全部上班,下一层收到的信号总量就比训练时大。 两种补法:
| 写法 | 训练时 | 测试时 |
|---|---|---|
| 朴素 Dropout | mask 只置 0,不缩放 | 每层输出乘 p |
| 倒置 Dropout | mask 除以 p,当场补回尺度 | 什么都不用改 |
| 倒置写法把「除以 p」挪到训练时做,测试代码一个字都不用动,所以现在的框架默认全是这一种。 | ||
| 这次实测四行全挤在 0.86 到 0.88 之间,没测出「忘记补偿」的明显惩罚。 | ||
| 理论上它是错的,但在这个小网络上被稀释了——后面还有一层 softmax, | ||
| 整体缩放会被归一化吸收掉一大半。 | ||
| 大网络、深网络里这一步就致命了,不能因为这次没测出来就当成没关系。 |
四、什么时候值得用
换三个训练集大小,各跑一次用与不用:
| 训练张数 | 不用 | 保留率 0.7 | 提升 |
|---|---|---|---|
| 100 | 0.8200 | 0.8413 | +2.1 点 |
| 200 | 0.9100 | 0.9437 | +3.4 点 |
| 400 | 1.0000 | 1.0000 | +0.0 点 |
| 100 张时提升 2.1 点,200 张时提升 3.4 点,都不是大数目。 | |||
| 400 张时两条都顶到 1.0000,一点差距都看不出来了。 | |||
| 样本本身已经把过拟合压住了,再加正则化就是白忙一场。 | |||
| 这也是下一节和整章收尾都要反复提的一句:这些手段压的是过拟合,压不出更高的上限。 |
五、动手 Demo:保留率扫描、补偿写法对比、以及它什么时候不管用
数据 400 张训练、800 张测试,都只做平移一格。
网络 64 到 64 到 32 到 10,参数 6464 个,训练只给 100 张——参数比样本多六十多倍,
正适合看正则化的效果。
代码里 Zs 和 Ms 两个列表要分开存:一个存乘 mask 之前的激活值,一个存 mask 本身。
反传时两个都要用。
def fit(X, y, keep=1.0, mode="inverted", steps=2000, lr=0.1,
hs=(64, 32), seed=0):
u"""keep 是保留率;mode 决定训练时要不要把尺度补回去。"""
rg = np.random.default_rng(seed)
for _ in range(steps):
As, Ms, Zs = [X], [], []
for i in range(len(Ws) - 1):
a = np.tanh(As[-1] @ Ws[i] + bs[i])
if keep < 1.0:
mk = (rg.random(a.shape) < keep)
# 倒置写法在这一步就除以 keep,测试时什么都不用改
mk = mk / keep if mode == "inverted" else mk.astype(float)
else:
mk = np.ones_like(a)
Zs.append(a) # 乘 mask 之前的激活值,反传要用来算导数
Ms.append(mk)
As.append(a * mk)
# ... 前向到 softmax、算交叉熵梯度 ...
for i in range(len(Ws) - 2, -1, -1):
dz = d * Ms[i] * (1.0 - Zs[i] * Zs[i])
Ws[i] -= lr * (As[i].T @ dz)
bs[i] -= lr * dz.sum(0)
if i:
d = dz @ Ws[i].T
# 四种写法放一起比:同一份数据、同一个种子、同样 2000 步
print(acc(Xte, yte, fit(Xs, ys, keep=1.0, steps=2000, seed=1)))
mN = fit(Xs, ys, keep=0.7, mode="naive", steps=2000, seed=1)
print(acc(Xte, yte, mN, 0.7, "naive", 1.0)) # 测试时忘记补偿
print(acc(Xte, yte, mN, 0.7, "naive", 0.7)) # 测试时乘 0.7
print(acc(Xte, yte, fit(Xs, ys, keep=0.7, steps=2000, seed=1), 0.7))
实跑输出:
--------------------------------------------------------------------
Dropout:训练时随机停工,测试时怎么补偿
--------------------------------------------------------------------
[1] Dropout 干的事:训练时每次随机让一部分神经元「停工」
每喂一批数据,先掷硬币决定哪些单元这一轮不说话,然后才前向、反向
停工的那几个单元这一轮既不贡献输出,也收不到梯度、不更新
下面用 100 张训练图、64-64-32-10 的网络(参数 6464 个,明显过多)
参数比样本多几十倍,正适合看正则化的效果
[2] 保留率扫描:留下多少比例的单元
保留率 训练准确率 测试准确率 训练减测试
1.0 1.0000 0.8600 +0.1400
0.9 1.0000 0.8600 +0.1400
0.7 1.0000 0.8762 +0.1238
0.5 1.0000 0.8950 +0.1050
0.3 1.0000 0.8662 +0.1338
不用 Dropout 那一行,训练 1.0000、测试只有八成六,中间那道沟就是过拟合
保留率降下来,沟一路收窄,0.5 时测试最高、沟只剩一成
但注意训练准确率一直是 1.0000,一格没掉——
不是 Dropout 没生效,是这个网络容量太大,丢一半单元也照样背得下来
保留率压到 0.3 又掉头向下,丢太多了,剩下那点容量不够用
所以保留率不是越小越好,0.5 附近是这类网络的常用值
[3] 测试时必须补偿,这一步最容易写错
训练时丢了 30% 的单元,测试时全部单元都上班,
下一层收到的信号总量会比训练时大,尺度就错位了。两种写法:
做法 测试准确率
不用 Dropout 0.8600
朴素 Dropout,测试时忘记乘 0.7 0.8675
朴素 Dropout,测试时乘 0.7 0.8812
倒置 Dropout(训练时就除 0.7) 0.8762
这次没测出「忘记补偿」的明显惩罚:四行全挤在 0.86 到 0.88 之间
理论上忘记补偿是错的,但在这个小网络上影响被稀释了,
因为后面还有一层 softmax,整体缩放会被归一化吸收掉一大半
大网络、深网络里这一步就致命了:训练和测试看到的信号强度不一致,
等于换了台机器,所以框架默认都用倒置写法
后两行分数几乎一样,正说明「除以 0.7」挪到训练时做是等价的
挪到训练时的好处是测试代码一个字都不用改,所以现在的框架都这么做
[4] 什么时候值得用:样本一多,它就没什么事了
训练张数 不用 保留率 0.7 提升
100 0.8200 0.8413 +2.1 点
200 0.9100 0.9437 +3.4 点
400 1.0000 1.0000 +0.0 点
100 张时提升 2.1 点,200 张时提升 3.4 点,都不是大数目
400 张时两条都顶到 1.0000,一点差距都看不出来了
样本本身已经把过拟合压住了,再加正则化就是白忙一场
所以 Dropout 是「没数据时的替补」,不是「万能加分项」
[5] 一句话记住
Dropout 相当于每次训练一个残缺的小网络,测试时把这些小网络平均起来
它压的是过拟合,不是提高上限;上限还是由数据和网络容量决定
下一节讲 DropConnect:它连的不是单元,是单元之间的连线
Zs 那一行注释不是废话,是这次真踩过的坑。
tanh 的导数是 1 减 a 平方,这里的 a 必须是「乘 mask 之前」的值。
如果拿乘完 mask 的 As 去算,被丢掉的单元会算出 0 减 0 除以 0 这种式子,数值上直接崩。
第二段那四种写法里,mN 只训练了一次,却用三种方式去测。
这是故意的:同一份权重、同一个网络,只改测试时的补偿方式,
才能干净地看出「补偿」这件事本身的影响。
第四段那张表最容易看错:提升只有两个点到三个点。
如果你原本期待「加 Dropout 能涨十几个点」,那这份数据会让人失望。
原因是任务太同质:十个数字的七段管图长得太像,正则化能挤出来的水分本来就不多。
30 秒实验
- 把网络从 64 到 64 到 32 改成 64 到 16 到 10,再扫一遍保留率。 容量小了以后,Dropout 的最优保留率会往上移——丢一点就学不动了。
- 把训练张数从 100 改成 50,再看提升幅度。 会明显大于 2.1 点,因为过拟合更严重。
- 把
mode改成naive,训练时不做除法,测试时乘 0.7。 结果是 0.8812,比倒置写法的 0.8762 还高一点——这份数据上两种写法都能用。
常见误区
- 「Dropout 是万能的,加上总没坏处。」 训练集给到 400 张时,用与不用都是 1.0000,白忙一场。 数据够的时候,正则化只会拖慢训练。
- 「加了 Dropout,训练准确率应该会下降。」 实测五行的训练准确率全是 1.0000。 网络容量远大于任务难度时,丢一半单元也照样能拟合。 所以「训练准确率下降」不是判断 Dropout 生效的可靠信号。
- 「测试时也开着 Dropout,双重保险。」 绝对不行。 测试要的是确定、可复现的答案,开着 Dropout 同一个输入每次跑出不同结果,工程上根本没法用。 这也是它叫「正则化」而不是「增强」的原因:它只作用于训练。
自测三问(点开对答案)
1. **为什么 Dropout 能减轻过拟合?** —— **它逼每个单元都不能依赖别的单元:别的单元随时可能不在,它得自己也能干活。** **这打散了单元之间的「共谋」,让学到的特征更独立、更冗余。** **另一个角度看,它相当于训练了极多个共享权重的子网络,测试时做平均。** 2. **倒置 Dropout 为什么要除以保留率?** —— **为了让「训练时的期望输出」等于「测试时的实际输出」。** **训练时只有 p 比例的单元在工作,每个单元的贡献除以 p,总和就补回来了。** **这样测试时什么都不用改,直接跑就行。** 3. **保留率是不是越小越好?** —— **不是。实测 0.5 最好(0.8950),0.3 掉回 0.8662。** **丢得太多,剩下的容量不够表达,就从「防过拟合」滑到「欠拟合」了。**Dropout 丢的是单元。 下一节换一个粒度:不丢整个单元,只丢单元之间的连线,这就是 DropConnect。
- ☐ (本节完,下一节 6.5 DropConnect)