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

我最早以为 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_A 和 model_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 对应的模型状态是“可推理”的。如果你在训练中保存,那就得在代码里显式记录当前是训练模式,加载后根据需求切换。
一套让我不再翻车的习惯
- 把推理逻辑封装成一个函数,函数开头统一调用
model.eval(),并配合with torch.no_grad(),这样无论从哪里调都不会忘。 - 自定义 forward 中,所有 dropout 都要用
self.dropout或F.dropout(..., training=self.training),不要写死。 - 多个模型串行时,写一个帮助函数:
def set_eval(*models):循环调用所有模型的 eval。 - 保存 checkpoint 时,除了 state_dict,也保存一个
training字段,加载时显式恢复。 - 如果发现预测结果有随机性,第一反应就是检查模型处在什么模式,而不是怀疑数据或权重。
做了这些,我几乎再没被模式切换坑过。但理论上的理解更重要:dropout 的开关不是一个“自动魔法”,而是依赖框架约定和开发者自觉。你写的每一行自定义 forward,都有可能破坏这个约定。
说实话,这个 bug 很烦人,但它也逼我搞清楚了训练和推理的根本差异:训练需要随机性来泛化,推理需要确定性来复现。Dropout 只是把这种差异放大到你能看见的程度。下次你的模型在推理时“抽风”,不妨先看看是不是又在训练模式下偷偷做随机取舍。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/675.html