3. Dropout 基本原理:训练与推理模式差异

讲Dropout,很多教程一上来就扔公式。我个人觉得,先搞懂它为什么能工作,比背公式重要得多。

Dropout的本质,说白了就是随机失活。训练时,我们随机掐掉一部分神经元,让网络不能依赖某几个“明星节点”。这迫使网络学会更鲁棒的特征表达。

3.1 训练模式下:随机丢弃与集成学习效应

训练时,每个神经元都有概率 p 被保留,1-p 的概率被置零。这个 p,就是我们常说的 keep_prob(保留概率),而不是丢弃率。别搞反了,我见过不少新手在这上面栽跟头。

我的习惯:代码里我只关注 keep_prob,这样更直观。比如 keep_prob=0.8,意味着平均有20%的神经元会“罢工”。

每次前向传播,丢弃的神经元都不一样。你可以这样理解:每张输入图,都走了一个不同的子网络。

  • 一个完整的网络,可以看作 N 个子网络的集成。
  • Dropout 相当于同时训练这 N 个网络,但共享权重。
  • 推理时,所有子网络“投票”决定最终输出。

我在项目里遇到过一种情况:某个神经元总是被激活得很高,其他神经元几乎不干活。加上 Dropout 后,这种情况明显改善。你想想看,如果主力神经元突然“请假”了,其他神经元就得顶上,整个系统就变得健壮了。

3.2 推理模式下:期望补偿与权重缩放

推理时,我们不丢弃任何神经元。但这会带来一个问题:训练时的“集成”效应消失了。

如果推理时直接使用完整网络,输出值会比训练时大很多。为什么呢?

  • 训练时,每个神经元只有 p 的概率被激活。
  • 推理时,所有神经元都激活了。
  • 输出期望值会放大 1/p 倍。

为了解决这个问题,推理时需要做一个期望补偿。常见的做法是:将权重乘以 p。

具体来说,推理时每个神经元的输出,都要乘上训练时的保留概率 p。这样,输出的期望值就与训练时一致了。

# 伪代码:推理时的权重缩放
# 训练时 keep_prob = 0.8
# 推理时每个权重乘以 0.8
output = input * weights * keep_prob

嗯,这里要注意一个细节。在实践中,权重缩放可以在训练时做,也可以在推理时做。TensorFlow 和 PyTorch 的默认实现不太一样。

关键差异:
  • TensorFlow 的 Dropout:训练时放大输出(除以 keep_prob),推理时不做任何操作。
  • PyTorch 的 Dropout:训练时不做缩放,推理时缩放权重(乘以 keep_prob)。

两种方式数学等价,但代码实现不同。切换框架时一定要搞清楚。

我曾经因为这个问题,在迁移模型时踩过坑。模型在 PyTorch 上训练得好好的,转到 TensorFlow Serving 推理时,结果全乱了。后来发现是 Dropout 缩放策略不同导致的。

3.3 为什么不能直接套用标准 Dropout?

很多人以为 Dropout 是通用的,拿来就能用。但在蛋白质结构预测这种高维稀疏数据场景下,标准 Dropout 往往效果不佳。

AlphaFold 的输入是 MSA(多序列比对)和结构特征。这些特征非常稀疏,很多位置是占位符或空值。标准 Dropout 随机丢弃神经元,可能会把关键的特征维度给“误杀”了。

举个例子:

  • 某个残基位置,只有少数序列有有效信息。
  • 标准 Dropout 以相同概率丢弃所有特征。
  • 结果:本来就很稀疏的关键特征,被丢弃得更严重了。

所以,AlphaFold 用了 Spatial Dropout 和 Channel-wise Dropout 的变体。这些变体不再是随机丢弃单个神经元,而是按通道或按空间位置进行丢弃,更好地保留了结构信息的完整性。

Dropout 类型 丢弃单位 适用场景
标准 Dropout 单个神经元 全连接层,通用场景
Spatial Dropout 整个通道 卷积层,空间特征
Channel-wise Dropout 整个特征图 MSA特征,通道独立

我个人建议,在 AlphaFold 这种结构化预测任务中,至少用 Spatial Dropout 起步。

3.4 训练与推理模式切换:代码层面的正确实现

理论上讲清楚了,来看看代码怎么写。PyTorch 里,Dropout 层的训练/推理模式由 .train() 和 .eval() 控制。

import torch
import torch.nn as nn

class AlphaFoldBlock(nn.Module):
    def __init__(self, d_model=256, dropout=0.2):
        super().__init__()
        # 标准Dropout
        self.dropout = nn.Dropout(p=dropout)
        # 空间Dropout(按通道丢弃)
        self.spatial_dropout = nn.Dropout2d(p=dropout)

    def forward(self, x):
        # 训练时:随机丢弃
        x = self.dropout(x)
        # 推理时:自动缩放权重
        return x

这里有个常见的坑:必须显式调用 .eval()。很多人写完模型就忘了,导致推理时 Dropout 还在工作,结果每次推理结果都不一样。

我曾经踩过的坑: 训练后做推理测试,发现每次预测结果都不一样。排查了半天,发现是忘了调 .eval()。模型在推理模式下,一直处于“随机丢弃”状态。

正确的推理流程应该这样:

# 推理模式
model.eval()
with torch.no_grad():
    for batch in test_loader:
        output = model(batch)
        # 此时Dropout不生效

# 回到训练模式
model.train()

对于 AlphaFold 这种大型模型,我建议在 验证循环 和 测试循环 的开头,都加上 model.eval()。别嫌麻烦,这是防止“推理随机性”的最保险方式。

下面这张图可以帮你直观理解训练和推理模式的差异:

训练模式 vs. 推理模式:Dropout 行为差异 训练模式 输入层 随机丢弃 keep_prob=0.8 子网络 A 子网络 B 子网络 ... 集成学习效应 推理模式 输入层 不丢弃神经元 所有神经元激活 完整网络 权重 * keep_prob 期望值对齐 训练时随机丢弃产生集成效应;推理时不丢弃,但缩放权重以匹配期望

看完图再总结一下:训练时我们用 Dropout 做随机丢弃,相当于训练了一个“集成模型群”;推理时,我们恢复完整网络,但通过权重缩放来保持一致。两者的配合,才是 Dropout 能通用的关键。