为什么在设置 model.eval() 之后,PyTorch 模型的性能会很差?
问题现象
在 PyTorch 中,我们在训练完成后通常会调用 model.eval() 切换到评估模式。但有时你会发现一个诡异的现象:
model.train()模式下推理,指标很好model.eval()模式下推理,性能骤降
这到底是为什么?
model.eval() 到底改变了什么?
model.eval() 会影响两类层的行为:Dropout 和 BatchNorm。
Dropout 在 train 和 eval 下的区别
Dropout 在训练时以概率 p 将神经元输出随机置零以减缓过拟合。测试时为了保证推理的一致性,不能再随机失活。但如果简单地让所有神经元原样输出,会导致输出尺度不一致:训练时每个神经元输出的绝对值均值为 (1-p)x,而测试时直接输出则为 x。
为了解决这个尺度问题,有两种策略:
- 测试时缩放:测试时所有神经元输出,但将输出乘以
(1-p) - 训练时缩放(Inverted Dropout):训练时将未被置零的输出除以
(1-p),使训练时输出均值保持为x,测试时直接做恒等映射即可
PyTorch 官方采用的是第二种 Inverted Dropout 实现,推理速度更快。
Dropout 的 eval 行为通常不会导致性能下降,反而可能略有提升。真正的问题出在 BatchNorm 上。
BatchNorm 在 train 和 eval 下的区别
# train 模式:使用当前 batch 的均值和方差
mean = x.mean(dim=0)
var = x.var(dim=0)
# eval 模式:使用训练期间累积的 running_mean 和 running_var
mean = self.running_mean
var = self.running_var
训练时,BN 使用同 batch 内同一特征维度的数据统计均值和方差。测试时,为了保证每个样本的预测不受 batch 内其他数据影响,必须使用固定的全局统计量。
这个全局统计量怎么来的? 直观的做法是测试前再过一遍整个训练集来估计,但数据量大时代价太高。因此 BN 采用了类似 SGD momentum 的思路——训练过程中通过**指数滑动平均(EMA)**在线估计全局统计量:
# momentum 默认 0.1
running_mean = (1 - momentum) * running_mean + momentum * batch_mean
running_var = (1 - momentum) * running_var + momentum * batch_var
这样既能反映整个训练集的分布,又自然地偏向训练后期(权重趋于收敛时)的统计量,与测试时的模型权重更一致。
什么情况下 BatchNorm 会出问题?
场景一:微调预训练模型时 running statistics 被污染
这是最常见的情况。微调预训练模型(如 ResNet)时:
model = torchvision.models.resnet50(pretrained=True)
model.fc = nn.Linear(2048, num_classes)
model.train()
预训练模型的 BN 层保存了在 ImageNet 上累积的 running_mean 和 running_var。微调时如果不冻结 BN 层,这些统计量会被小数据集逐步覆盖。小数据集的 batch 统计量方差大、不稳定,EMA 累积后的 running statistics 无法准确反映数据分布。train() 模式下用的是当前 batch 统计量还能凑合,eval() 模式下切换到这些不靠谱的 running statistics 就暴跌了。
场景二:训练过程中忘记切回 train 模式
for epoch in range(num_epochs):
model.train()
train_one_epoch()
model.eval()
validate()
# ❌ 忘记切回 train(),后续 epoch BN running stats 停止更新!
场景三:Batch Size 太小
Batch size 为 1 或 2 时,每个 batch 的统计量噪声极大,EMA 累积的 running statistics 质量很差。
场景四:数据分布不一致
训练集和测试集存在较大的 domain shift,训练阶段累积的 running statistics 不适用于新数据。
解决方案
方案一:微调时冻结 BN 层
def freeze_bn(model):
for module in model.modules():
if isinstance(module, (nn.BatchNorm1d, nn.BatchNorm2d)):
module.eval()
for param in module.parameters():
param.requires_grad = False
model.train()
freeze_bn(model)
方案二:训练结束后重新校准 running statistics
model.train()
with torch.no_grad():
for batch in train_loader:
model(batch)
model.eval()
方案三:使用不依赖 batch 统计量的归一化
nn.LayerNorm(normalized_shape)
nn.GroupNorm(num_groups=32, num_channels=256)
LayerNorm 和 GroupNorm 不区分 train/eval 模式,不存在 running statistics 问题。
方案四:推理时强制使用 train 模式
model.train()
with torch.no_grad():
output = model(input)
如果性能恢复正常,就确认了问题出在 BN 的 running statistics 上。
model.eval() 影响大模型吗?
搞清楚了 model.eval() 对 Dropout 和 BatchNorm 的影响后,这个问题就等价于:大模型用 BatchNorm 和 Dropout 吗?
首先,大模型(LLM)普遍使用 LayerNorm / RMSNorm,不使用 BatchNorm,因此 BN 的 running statistics 问题不存在。
其次,大多数 Transformer 实现中 Dropout 主要出现在 self-attention 的 softmax 概率之后,对应超参数 attention_dropout。而在 LLaMA、Mistral 等主流开源大模型中,attention_dropout 都被设为 0,即实际上不使用 Dropout。
因此,model.eval() 对这些不开 Dropout 的大模型实际上没有影响。
总结
| 原因 | 解决方案 |
|---|---|
| 微调时 BN running stats 被小数据集污染 | 冻结 BN 层 |
| 训练中忘记切回 train() | 检查训练循环逻辑 |
| Batch size 过小导致统计量不准 | 增大 batch size 或使用 GroupNorm |
| 数据分布不一致 | 重新校准 running stats |
| 根本性解决 | 使用 LayerNorm/RMSNorm 替代 BN |
一句话总结: model.eval() 后性能变差,核心原因是 BatchNorm 的 running statistics 不准确。而对于使用 LayerNorm 且不开 Dropout 的大模型来说,model.eval() 实际上没有影响。