第四章:批量大小与梯度累积
说到 Batch Size,我脑子里第一个蹦出来的画面,是几年前调一个图像分类模型。当时显存只有 8G,我硬是把 Batch Size 从 64 一路降到 8,结果模型死活不收敛。后来我才明白,这玩意儿不是越大越好,也不是越小越灵。
今天咱们就聊聊 Batch Size 对收敛的影响、梯度累积的原理,以及显存和速度怎么权衡。
一、Batch Size 对收敛的影响
先问个问题:Batch Size 到底在控制什么?
说白了,它控制的是每次参数更新时,你用多少样本去估计梯度。
- Batch Size 太大:梯度估计更准,收敛更稳定。但容易陷入尖锐的局部极小值,泛化能力反而下降。我见过有人把 Batch Size 设到 1024,训练 loss 降得飞快,测试集上直接崩了。
- Batch Size 太小:梯度噪声大,收敛路径曲折。但好处是能跳出一些不好的局部点,泛化性往往更好。不过训练时间会拉长,显存利用率也低。
我个人习惯的做法是:先试一个中等值,比如 32 或 64。如果 loss 震荡太厉害,就往上加;如果收敛太慢,就往下减。你想想看,这其实是个经验活。
核心结论:Batch Size 和学习率需要联动调整。增大 Batch Size 时,通常也要适当增大学习率,否则收敛速度会变慢。
二、梯度累积原理
显存不够怎么办?梯度累积就是来救场的。
它的原理很简单:把一个大 Batch 拆成几个小 Batch,分别计算梯度,然后累加起来,最后统一更新参数。
举个例子:你想用 Batch Size = 64,但显存只够跑 16。那就跑 4 个 step,每个 step 用 16 个样本算梯度,累加 4 次后再更新参数。效果上,等价于一次用 64 个样本更新。
# 梯度累积伪代码
accumulation_steps = 4
optimizer.zero_grad()
for i, (inputs, labels) in enumerate(dataloader):
outputs = model(inputs)
loss = criterion(outputs, labels)
loss = loss / accumulation_steps # 注意这里要除
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
注意:loss 要除以 accumulation_steps,否则梯度会放大 accumulation_steps 倍,导致训练不稳定。我曾经在这上面吃过亏,调了半天才发现是忘了除。
三、显存与速度的权衡
这里有个常见的误区:梯度累积能省显存,但省不了时间。
为什么?因为计算量没变。你原来一次算 64 个样本,现在分 4 次算 16 个,总计算量是一样的。而且多了梯度累加和参数更新的开销,实际耗时反而可能增加。
那梯度累积的价值在哪?
- 突破显存瓶颈:让你能在有限硬件上跑更大的有效 Batch Size
- 稳定训练:当单卡 Batch Size 太小导致梯度噪声过大时,累积可以平滑梯度
我一般这样权衡:
| 场景 | 推荐做法 |
|---|---|
| 显存够用,追求速度 | 直接用大 Batch Size,不用累积 |
| 显存不够,但需要大 Batch | 梯度累积,accumulation_steps 尽量小 |
| 单卡 Batch Size 太小(如 2、4) | 梯度累积到 16 或 32,减少梯度噪声 |
小技巧:如果你的模型用了 Batch Normalization,梯度累积时要小心。BN 的统计量是基于每个 micro-batch 计算的,累积不会改变这一点。所以有效 Batch Size 还是 micro-batch 的大小,只是梯度更新频率降低了。
四、知识体系图
下面这张图帮你理清 Batch Size 和梯度累积的核心逻辑:
五、实战建议
最后给几条实在的建议:
- 先定 Batch Size,再调学习率。我习惯用学习率预热 + 余弦退火的组合,对 Batch Size 变化比较鲁棒。
- 梯度累积步数不要太大。超过 8 步的话,参数更新间隔太长,训练效率会下降。我一般控制在 2-4 步。
- 混合精度训练 + 梯度累积是绝配。显存能省一半,速度还能提升。不过要注意 loss scaling 的调整。
- 如果条件允许,优先增大单卡 Batch Size,而不是堆累积步数。因为 BN 的统计量会更准确。
一句话总结:Batch Size 控制梯度质量,梯度累积解决显存瓶颈。两者配合好,才能在有限硬件上跑出好模型。
嗯,今天就聊到这儿。这些经验都是真金白银换来的,你试试看就知道了。