Dropout 的推理时关闭:train() 和 eval() 模式切换时最容易犯的 bug

有一次我训练了一个文本分类模型,准确率不错。部署到服务后,同一个输入每次返回的概率都不一样。我把模型、数据、代码检查了个遍,最后发现——只是忘了调 model.eval()。这个bug浪费了我一下午,也让我第一次认真思考:train() 和 eval() 切换到底发生了什么?为什么一个小小的方法会影响推理结果?

AI technology illustration

我最早以为 dropout 只是训练时的正则化技巧,推理时关掉是理所当然的——框架应该自动处理才对。后来才发现,这个“理所当然”并不自动。它在 PyTorch 里靠着一个 training 布尔标志,而这个标志是否真的让 dropout 失效,取决于你的 forward 怎么写的。

Dropout 到底在做什么?

Dropout 最早由 Srivastava 等人在 2014 年提出,简单说:训练时,每个神经元以概率 p 被随机“开除”。当前这次前向传播里,被开除的神经元不参与计算,反向传播也不更新它的权重。

说人话:模型本来有一堆神经元,它们会合谋解决某个模式。Dropout 强行拆散这些“小团体”,每次都随机换一批人在场。这样一来,模型被迫学会不依赖任何单一神经元,从而减少过拟合。

那么推理时为什么必须关闭?想象你训练完模型,它已经学会了每个特征的加权组合。推理时如果还开着 dropout,模型每跑一次,就有随机一批神经元被“关掉”,输出自然忽高忽低。这不叫随机性,这叫抽风。你当然不希望生产环境里的预测像抽奖一样。

train() 和 eval() 切换的内幕

在 PyTorch 里,model.train()model.eval() 会递归地把每个子模块的 training 标志设为 True 或 False。官方文档说得很清楚:这个操作会影响 Dropout 和 BatchNorm 等层的行为。

但这里有个关键点:切换的是“标志”,不是“行为”。每个模块在自己的 forward 里决定是否使用这个标志。比如 nn.Dropout 的 forward 会检查 self.training,如果是 True 就随机丢弃,False 就原样返回。BatchNorm 也一样,train 模式用当前 batch 的均值方差,eval 模式用训练时积累的 running stats。

这意味着,只要你的模型里用的是标准层,调用 model.eval() 就能正确切换。但如果你在自定义 forward 里直接用了 F.dropout(x),那就出大事了——它的默认参数是 training=True,跟模型的训练状态无关。也就是说,即使你调了 model.eval(),这个 dropout 依然在随机丢弃。

最容易翻车的几个场景

我把常见的模式切换 bug 总结成一张表,后面对着表说细节:

场景 表现 原因
忘了 model.eval() 预测结果每次不一样 dropout 还在随机丢弃
在 forward 里用了 F.dropout(x) eval 模式下仍然随机 F.dropout 默认 training=True
多个模型串行推理,只切了一个 某个模型输出不稳定 另一个模型仍是训练模式
保存模型时处于 train 模式 加载后直接推理结果差 BN 和 dropout 都还开着

先说最经典的:忘了调 eval。很多人在训练完直接 torch.save(model.state_dict()),然后在新脚本里 load 权重就预测。这时候模型默认是训练模式,dropout 随机丢弃,BN 也用当前 batch 的统计量。你每次预测都像是在“训练”中,但没有任何反向传播,所以输出不断抖动。

再说 F.dropout 这个坑。它有时很诱人,因为不用显式创建一个子模块。但它的行为不像 nn.Dropout 那样自动感知模型的训练状态。你必须手动传入 training 参数:

def forward(self, x):
    x = F.dropout(x, p=0.5, training=self.training)
    return x

如果不写 training=self.training,这个 dropout 就永远是开着的。我见过不少开源代码在这里栽跟头——训练时一切正常,一推理就随机。

还有一个隐蔽的场景:你有两个模型 model_Amodel_B,先调 model_A.eval(),然后去跑 model_B。如果忘了调 model_B.eval(),B 的 dropout 还在随机。更糟糕的是,如果你的模型是嵌套的,eval 只作用于已注册的子模块,如果你动态创建了一些 layer 但没注册,它们也“逃过一劫”。

为什么我的模型在 eval 模式下输出还有随机性?

先查你的 forward 里有没有用 F.dropout 或其它自带随机性的操作(比如 torch.multinomial)。如果没有,再确认你有没有正确调用 model.eval(),以及调用的是不是同一个模型对象。

model.eval() 和 torch.no_grad() 是一回事吗?

完全不是。eval 影响模型内部模块的行为(Dropout 和 BN),no_grad 只是不构建计算图,省内存、加速推理,但不改变模型行为。推理时最好两者同时用:model.eval() + torch.no_grad()

保存模型前一定要调 eval 吗?

建议调。虽然加载模型后你可以再手动调,但很多人忘了。保存前调一下 eval,至少保证 checkpoint 对应的模型状态是“可推理”的。如果你在训练中保存,那就得在代码里显式记录当前是训练模式,加载后根据需求切换。

一套让我不再翻车的习惯

  1. 把推理逻辑封装成一个函数,函数开头统一调用 model.eval(),并配合 with torch.no_grad(),这样无论从哪里调都不会忘。
  2. 自定义 forward 中,所有 dropout 都要用 self.dropoutF.dropout(..., training=self.training),不要写死。
  3. 多个模型串行时,写一个帮助函数:def set_eval(*models): 循环调用所有模型的 eval。
  4. 保存 checkpoint 时,除了 state_dict,也保存一个 training 字段,加载时显式恢复。
  5. 如果发现预测结果有随机性,第一反应就是检查模型处在什么模式,而不是怀疑数据或权重。

做了这些,我几乎再没被模式切换坑过。但理论上的理解更重要:dropout 的开关不是一个“自动魔法”,而是依赖框架约定和开发者自觉。你写的每一行自定义 forward,都有可能破坏这个约定。

说实话,这个 bug 很烦人,但它也逼我搞清楚了训练和推理的根本差异:训练需要随机性来泛化,推理需要确定性来复现。Dropout 只是把这种差异放大到你能看见的程度。下次你的模型在推理时“抽风”,不妨先看看是不是又在训练模式下偷偷做随机取舍。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/675.html

(0)
上一篇 10小时前
下一篇 10小时前

相关推荐