返回文章列表
LLM经验

为什么在设置 model.eval() 之后,PyTorch 模型的性能会很差?

·未分类

问题现象

在 PyTorch 中,我们在训练完成后通常会调用 model.eval() 切换到评估模式。但有时你会发现一个诡异的现象:

  • model.train() 模式下推理,指标很好
  • model.eval() 模式下推理,性能骤降

这到底是为什么?

model.eval() 到底改变了什么?

model.eval() 会影响两类层的行为:DropoutBatchNorm

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_meanrunning_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() 实际上没有影响。