批次大小:训练节奏的节拍器

说起 Batch Size,我觉得它就像训练模型的节拍器。节拍太快,步子容易乱;节拍太慢,半天跑不出多远。我跟不少同行聊过,大家普遍觉得 Batch Size 是最好调也是最容易踩坑的参数之一。

我个人习惯先把 Batch Size 定下来,再去动学习率。因为这两个东西绑得太紧了,你调一个,另一个必须跟着变。说白了,它就是每轮更新参数时,你喂给模型多少个样本。

吃得多≠长得壮

讲个真实经历。我刚开始做 Transformer 训练时,总觉得 GPU 显存有多大就塞多大 Batch Size。128、256、512,一路往上加。结果呢?模型收敛倒是挺快,可最终精度总差那么一截。

那背后的原理是什么?说白了就是梯度估计的方差。

小 Batch 每个批次样本少,梯度方向噪声大,但噪声大也意味着有机会跳出局部最优。大 Batch 梯度估计更准,可一旦掉进尖锐局部极小点,就很难爬出来。

我给你看张表,是我之前在某个翻译任务上跑出的对比:

Batch Size 收敛到相同Loss所需步数 最终BLEU 显存占用 (GB)
16 120k 27.3 4.2
32 75k 27.5 6.8
64 45k 27.6 11.5
128 28k 27.2 20.1
256 17k 26.8 38.4

看到了吗?64 的 Batch Size 是甜点区间。128 开始,步数虽然少了,但 BLEU 反而掉了 0.4。这就是典型的收敛快但不精。

线性缩放法则

我在项目中遇到过最头疼的问题:为了省时间,把 Batch Size 从 64 翻到 256,结果模型直接炸了。后来才知道,学习率必须跟 Batch Size 走。

业界有个经验法则——线性缩放。具体来说,当你把 Batch Size 放大 N 倍时,学习率也应大致放大 N 倍。但这招在你项目里未必总好用,为什么?

你想想看,梯度是多个样本梯度的平均。Batch Size 越大,平均梯度越接近全样本梯度,可信度更高。所以你可以大胆些,用更大的学习率去冲。

但问题来了,大学习率 + 大 Batch 在训练初期容易崩。我曾经吃过这个亏,后来就养成了预热(warm-up)的习惯。

我的经验:不管 Batch Size 设多少,前 5%~10% 的训练步数都让学习率从 0 线性爬到目标值。等模型站稳了,再切换到常规调度策略。

下面是我常用的预热配置,用 PyTorch 实现:

def get_linear_warmup_scheduler(optimizer, warmup_steps, total_steps):
    def lr_lambda(current_step):
        if current_step < warmup_steps:
            return float(current_step) / float(max(1, warmup_steps))
        return max(
            0.0,
            float(total_steps - current_step) /
            float(max(1, total_steps - warmup_steps))
        )
    return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)

# 使用示例
warmup_steps = 4000
total_steps = 100000
scheduler = get_linear_warmup_scheduler(
    optimizer,
    warmup_steps,
    total_steps
)

批次一致性的隐藏坑

嗯,这里要注意。很多人觉得 Batch Size 只在训练时重要,验证时随便塞就行。其实不对。

我曾经碰到过一个很坑的问题:训练用 Batch Size 64,验证时改成 1,结果每个样本长度不一,验证集 loss 曲线跟心电图似的,完全看不出模型有没有收敛。后来我意识到,验证集也应该尽量保持跟训练集一致的批次大小规格。

如果有显存限制,至少要做到两点:

  • 验证时做动态 padding,把每个 batch 内的样本长度对齐到该 batch 的最大长度
  • 保持每轮 validation 的 batch 数量稳定,不要忽多忽少

这其实属于批次一致性的核心:让训练和评估所处的计算环境尽量一致,你观察到的指标才有可比性。

实践中的选择策略

我个人推荐一个简单粗暴的方法——先从 32 或 64 起步,观察梯度范数的变化。如果梯度范数一直在 1~10 之间跳动,说明这个 Batch Size 还行。如果梯度范数稳得像一条直线(比如一直在 0.01 附近),那可能是 Batch Size 太大,梯度几乎没差异了,模型的泛化能力不会好。

下面这个示意图,是我自己对 Batch Size 选择逻辑的梳理:

Batch Size 选择决策流程 起始 Batch Size (32 或 64) 训练 500 步,观察梯度范数 是否在 1~10 区间内正常波动? 是 保持当前 Batch Size 可以尝试微调学习率 否 调整 Batch Size 数值偏大则减小,偏小则增大 全训练验证最终性能 评估收敛步数和模型效果 确定最优通配批次大小 每次更换 Batch Size 都需同步调整 LR

批次归一化的连锁反应

如果你用 Layer Normalization 的话,Batch Size 影响会小一些。但如果你用的是 Batch Normalization(比如在某些混合架构里),那 Batch Size 一变小,BN 的统计量就跟着剧烈波动。

说实话,Transformer 主要靠 Layer Norm,所以这块还好。不过,我记得在某个多模态项目里,我在图像编码器里用了 BN,Batch Size 从 64 降到 16,图像侧的 loss 直接失控。最后我把 BN 换成了 Group Norm,才算稳住。

千万注意:在 Transformer 的变体架构中,如果混用了 BN 或 SyncBN,Batch Size 突然变小会让 running_mean 和 running_var 完全不准。训练时 loss 看起来挺低,一上验证集马上崩。我建议要么固定 Batch Size 不变,要么用 Layer Norm 或 Group Norm 替代。

我的实操习惯

说来说去,我总结一下自己的流程:

  • 先用 64 跑一个短周期(比如 2000 步),看 loss 下降曲线是否平滑。
  • 如果 loss 下降曲线毛刺多,把 Batch Size 加一倍,同时把学习率也加一倍。
  • 如果 loss 下降太慢但曲线光滑,把 Batch Size 减半,学习率也减半。
  • 每次调整后,跑 500 步看梯度范数是否在合理范围内。

你可能会问,为什么都是翻倍或减半?因为这样好配合线性缩放法则,计算也简单。

最后,别忘了记录每次实验的 Batch Size 和学习率组合。我见过有人调了半天,最后忘了自己用的是哪个组合。真正的批次一致性,就是从这些细节的闭环记录开始的。