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

DropConnect 把「丢」这件事下沉一个粒度:不丢单元的输出,丢单元之间的连线。 区别可以用「一个单元还剩几条路」来量。 Dropout 保留率 0.7 时,一个单元的 64 条输入要么全在、要么全断; DropConnect 时平均还剩 44.8 条,标准差 3.7。 实测两者分数几乎一样:基线 0.8213,Dropout 0.7 得 0.8600,DropConnect 0.7 得 0.8588。 这份数据太小,分不出高下。 但有一条铁律:测试时必须把 mask 摘掉。 继续随机丢连线,五次跑出 0.85、0.85、0.83、0.81、0.87,平均 0.8422。

📝 本节和前面几节的关系

上一节的 Dropout 丢单元,这一节把同一件事做到更细的粒度。 两者出自不同的论文(Dropout 是 2012 年,DropConnect 是 2013 年),动机却完全一样: 训练时制造随机残缺。 这一节也顺带说明一件工程上的事:理论上更精细的方法,在具体任务上不一定更好,而且往往更贵。

一、两种丢法,差在一个单元还剩几条路

Dropout 是一次掷骰子决定一个单元的生死;DropConnect 是给每条连线各掷一次。

保留率 Dropout 的单元输入数 DropConnect 的单元输入数
0.7 64 或者 0,全有或全无 平均 44.8,标准差 3.7
0.5 64 或者 0,全有或全无 平均 32.0,标准差 4.0
Dropout 下,一个单元要么整台机器停,要么满负荷。
DropConnect 下,它是带伤工作:少了几根线,但还在出力。
保留率 0.7 时,一个隐单元的 64 根线全断光的概率是 0.3 的 64 次方,小到可以当 0。
所以 DropConnect 的子网络数量多到没法数,但它们长得都很像,不像 Dropout 那样连结构都换掉了。

二、实测:谁也没碾压谁

还是 100 张训练图、64 到 64 到 32 到 10,两种保留率各跑一次:

做法 保留率 0.7 保留率 0.5
不用任何丢弃 0.8213 0.8213
Dropout 0.8600 0.8600
DropConnect 0.8588 0.8762
两者都能把测试准确率从 0.8213 拉到 0.86 以上,量级差不多。
0.5 那一列 DropConnect 略高,但只差一点,不足以下结论。
论文里 DropConnect 主要在 MNIST、CIFAR 这类大任务上比 Dropout 好一点。
这份数据每个数字只有 10 张图,任务太同质,挤不出差距。
如实记下:这次没测出谁更好。

三、代价:多一份同形状的 mask

先看 600 步时两者的表现:

做法 600 步训练准确率 测试准确率
Dropout 0.7 1.0000 0.8187
DropConnect 0.7 1.0000 0.8163
两行的训练准确率都到 1.0000,600 步就背完了,看不出收敛快慢。
这次没能测出「DropConnect 更慢」——数据集太小,600 步和 2000 步都能拟合。
但它确实更贵,这是结构决定的。
Dropout 的 mask 跟激活值同形状,DropConnect 的 mask 跟权重同形状。
大网络里权重矩阵本来就大,这份开销是实打实的,所以用的人比 Dropout 少得多。

四、测试时必须把 mask 摘掉

训练靠随机残缺防过拟合,测试要的是一个确定、可复现的答案。 如果测试时还继续随机丢连线,同一个输入每次结果都不一样:

做法 5 次结果 平均
测试时摘掉 mask 每次都是 0.8588 0.8588
测试时继续随机丢 0.85 0.85 0.83 0.81 0.87 0.8422
五次结果彼此不一样,平均低了不到两个百分点。
差距不算大,因为大部分图本来就分得开,切几根线也还认得出来。
但真正的麻烦不是分数,是「同一个输入两次跑出两个答案」。
这在工程上没法用:日志对不上、复现不了、出了问题查不出来。
所以测试时必须是确定性的,这一条对 Dropout 也一样。

五、动手 Demo:两种丢法同台、以及测试时不能继续丢

数据同上一节:400 张训练、800 张测试,训练只取 100 张。 网络 64 到 64 到 32 到 10,学习率 0.1,2000 步。 代码里两处 mask 都要照应:Wms 乘在权重上,Ms 乘在激活上,反传时一处乘梯度、 一处乘权重矩阵。

for i in range(len(Ws) - 1):
    if keep < 1.0 and kind == "dropconnect":
        mk = (rg.random(Ws[i].shape) < keep) / keep   # 丢在权重上
    else:
        mk = np.ones_like(Ws[i])
    Wms.append(mk)
    a = np.tanh(As[-1] @ (Ws[i] * mk) + bs[i])
    if keep < 1.0 and kind == "dropout":
        mh = (rg.random(a.shape) < keep) / keep      # 丢在单元上
    else:
        mh = np.ones_like(a)
    Zs.append(a)
    Ms.append(mh)
    As.append(a * mh)

# 反传时两处 mask 都要带上:一处乘在梯度上,一处乘在权重上
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) * Wms[i])
    if i:
        d = dz @ (Ws[i] * Wms[i]).T


# 测试时继续随机丢连线,同一个输入会跑出不同答案
vals = []
for _ in range(5):
    Ws2 = [(w * ((rg.random(w.shape) < 0.7) / 0.7)) for w in m[0]]
    vals.append(acc(Xte, yte, (Ws2, m[1])))
print(vals, np.mean(vals))

实跑输出:

--------------------------------------------------------------------
DropConnect:丢的是连线,不是单元
--------------------------------------------------------------------
[1] 两种丢法丢的东西不一样
  Dropout 丢的是单元的输出:一个隐单元被选中,它这一轮就整个不出声
  DropConnect 丢的是连线:每个权重各自掷硬币,同一个单元还有一部分输入能进来
  区别在「一个单元还剩几条路」:
  保留率    Dropout 单元输入数        DropConnect 单元输入数
  0.7       64 或者 0(全有或全无)   平均 44.8,标准差 3.7
  0.5       64 或者 0(全有或全无)   平均 32.0,标准差 4.0

  Dropout 是「整台机器停不停」,DropConnect 是「这台机器上每根线断不断」
  保留率 0.7 时,一个隐单元 64 根线全断光的概率是 0.3 的 64 次方,可以当 0
  所以 DropConnect 的子网络数量多到没法数,但它们长得都很像,
  不像 Dropout 那样连结构都换掉了

[2] 三种做法对比:100 张训练图,64-64-32-10
  做法                      保留率 0.7      保留率 0.5
  不用任何丢弃              0.8213          0.8213
  Dropout                   0.8600          0.8600
  DropConnect               0.8588          0.8762

  两者都能把测试准确率从基线拉上来,量级差不多
  在这份数据上分不出高下——这个任务太小,看不出 DropConnect 的优势
  论文里 DropConnect 主要在 MNIST、CIFAR 这类大任务上比 Dropout 好一点

[3] 代价:DropConnect 要多跑一会儿
  做法                      600 步训练准确率    测试准确率
  Dropout 0.7               1.0000              0.8187
  DropConnect 0.7           1.0000              0.8163

  这两行训练准确率都到 1.0000,600 步就背完了,看不出收敛快慢
  这次没能测出「DropConnect 更慢」——数据集太小,2000 步和 600 步都能拟合
  但它确实更贵,这是结构决定的:mask 跟权重同形状,要大一份显存
  大网络里这份开销是实打实的,所以用的人比 Dropout 少得多

[4] 测试时必须把 mask 摘掉
  训练靠随机残缺来防过拟合,测试要的是一个确定、可复现的答案
  如果测试时还继续随机丢连线,同一个输入每次结果都不一样:
  做法                              5 次测试准确率            平均
  测试时摘掉 mask(正确)           每次都是 0.8588           0.8588
  测试时继续随机丢(错误)          0.85 0.85 0.83 0.81 0.87  0.8422

  继续丢的话,五次结果彼此不一样,平均低了不到两个百分点
  差距不算大,因为大部分图本来就分得开,切几根线也还认得出来
  但真正的麻烦是「同一个输入,两次跑出两个答案」——这在工程上没法用
  同样一份权重,测试时必须是确定性的,这一条对 Dropout 也一样

[5] 小结
  Dropout 丢单元,DropConnect 丢连线,目标都是逼网络别把宝押在少数特征上
  本质上都是「训练时制造随机残缺,让模型学会在残缺下也能干活」
  但在咱们这台笔记本上,两者的分数都在 0.86 附近,谁也没碾压谁
  选哪个更多看工程开销,不是看理论上谁更高级
  下一节收尾,把这一章的五种办法摆到一起看

第一段那个 Wms 和 Ms 的区分是关键。 哪怕只做 Dropout,Wms 也要填成全 1 的矩阵,不然反传那行 Ws[i] * Wms[i] 会报维度错。 这种「两条路共用一套代码」的写法省事,但一定要两个 mask 都初始化。 第二段里 rg 是循环外面那个随机数生成器。 如果每次循环都新建一个 default_rng(88),五次生成的 mask 会一模一样,五次结果也就完全相同, 看不出「随机」这回事。 这是 numpy 新随机接口最容易踩的一个坑。 第四段那张图表的解读要小心:平均 0.8422 只低了 1.7 个百分点。 如果只看分数,会得出「测试时丢一丢也没事」的错误结论。 真正的问题是不可复现,不是准确率。

30 秒实验

  1. 把 DropConnect 的保留率压到 0.3 再跑。 每个单元平均只剩 19 条线,分数会明显掉——粒度再细,丢太多也不行。
  2. 把 keep 设成 1.0 跑 DropConnect。 结果应该和「不用任何丢弃」完全一致(0.8213),这是验证代码没写错的最快办法。
  3. 把网络加宽到 64 到 128 到 64,再看两者差距。 参数一多,DropConnect 的显存优势反过来变成劣势,训练时间会明显拉长。

常见误区

  1. 「DropConnect 是 Dropout 的升级版,肯定更好。」 实测 0.8588 对 0.8600,基本持平;0.5 保留率下它略高一点。 这是同一族方法的两个粒度,不存在谁替代谁。
  2. 「测试时保持和训练一致,也开着 mask 更稳。」 五次跑出五个不同结果,平均 0.8422。 更要命的是不可复现:同一个输入两次不同答案,线上出了事根本没法查。
  3. 「DropConnect 只在全连接层能用。」 它本质是「给权重加 mask」,卷积层也做得到(按卷积核的元素丢)。 只是卷积核本来就共享,丢起来语义没全连接那么直观,所以用得少。
自测三问(点开对答案) 1. **Dropout 和 DropConnect 最本质的区别是什么?** —— **是「丢的东西」的粒度:一个丢单元的输出,一个丢连线的权重。** **前者让整个单元下线,后者让单元带伤工作。** **从「子网络数量」看,后者多得多;从「子网络差异」看,前者大得多。** 2. **为什么 DropConnect 保留率 0.7 时,一个单元几乎不可能全断?** —— **它的 64 条线各自独立地以 0.3 的概率被丢,全断的概率是 0.3 的 64 次方。** **这个数约等于 1 后面跟 33 个零分之一,可以当 0。** 3. **测试时继续随机丢,为什么分数只掉了不到两个点,还是不行?** —— **因为分数不是唯一指标。** **同一个输入两次跑出不同答案,意味着系统不可复现——日志、回归测试、线上排查全部失效。** **工程上「稳定的 0.84」比「忽高忽低的 0.85」有用得多。**

这一章的五个手段都讲完了。 最后一节把它们一项项加到同一个模型上,看训练与测试那道沟怎么变——结果有一项是负收益。

相关笔记