进行推理时若忘记调用 model.eval(),预测结果往往飘忽不定几乎必然——在训练模式下,Dropout 和 BatchNorm 的行为是动态的,只有切换到评估模式才能启用确定性逻辑。当然,这只是第一步,还需要同步锁定 cuDNN、DataLoader 以及所有随机种子。

使用 model.train() 进行推理?那你每次获得的结果都可能不同——这并非 bug,而是 PyTorch 的固有设计。
Dropout 和 BatchNorm 在 train 模式下根本不会“固定输出”
这两个层在 model.train() 下的行为是动态的:
- 例如
nn.Dropout(p=0.5),每次前向传播都会随机丢弃一半的神经元,并对输出进行自动缩放(乘以1/(1-p))。这意味着即便输入完全一致,两次推理的输出也可能存在差异。 - 而
nn.BatchNorm2d在训练模式下会使用当前 batch 的均值和方差进行归一化——即使输入完全相同,只要 batch size 或数据顺序发生变化,running_mean 和 running_var 就可能被意外更新,尤其当此前未调用过eval()时。 - 更隐蔽的是,那些依赖
self.training进行分支判断的自定义模块(例如条件 Dropout 或动态路由),也会无意间走训练路径。
model.eval() 并非“关闭随机”,而是切换到确定性模式
model.eval() 的真正意图并非一刀切地消除随机性,而是让特定层按照确定性逻辑运行:
- Dropout 层会跳过掩码步骤,直接返回原始输入,不进行任何缩放。
- BatchNorm 层则不再计算批次的统计量,而是直接使用已冻结的
running_mean和running_var。 - 此外,
model.eval()会递归生效,无需你对每个子模块手动调用.eval()。
需要特别注意的是,torch.no_grad() 仅负责关闭梯度计算,并不会改变层的运行模式——它与 model.eval() 是相互正交的。推理时,两者缺一不可。
部署服务中漏掉 model.eval() 是常见事故
本地 notebook 里跑测试,通常因为反复执行 cell 而无意中触发了 model.eval(),所以往往不易出现问题。但到了真实服务中,漏掉 model.eval() 的情况比比皆是:
- 例如在 Flask 或 FastAPI 的路由函数中忘记添加
model.eval(),每次请求进入都默认走训练模式。 - 或者模型加载后只进行了一次初始化,后续重复使用同一个实例,却从未切换过模式。
- 更严重的是,在 ONNX 导出前未调用
model.eval(),导致 BatchNorm 的running_mean被固化为零向量——在 TensorRT 部署时会直接崩溃。
即使加了 model.eval() 仍不一致?检查这三个随机源
即便已加上 model.eval(),如果结果仍不一致,那么问题很可能出在其他环节:
- cuDNN 默认启用非确定性卷积算法以追求性能,需要手动设置
torch.backends.cudnn.deterministic = True和torch.backends.cudnn.benchmark = False。 - DataLoader 的
shuffle参数应设为False,num_workers最好设为0,或者通过worker_init_fn固定每个 worker 的随机种子。 - 最后,随机种子需全面设置——
torch.manual_seed、numpy.random.seed和random.seed,一个都不能少。
要实现真正稳定的推理,必须同时锁定模型模式、计算后端、数据流和随机源这四个方面。缺少任何一个,model.eval() 都只是开了个好头,远未达到终点。
