3. Batch Size与学习率的联动:线性缩放法则与学习率预热策略
说实话,Batch Size 和学习率之间的关系,是我在调参路上踩过最深的坑之一。刚开始做分布式训练那会儿,我天真地以为把 Batch Size 翻个倍,学习率保持不变就行。结果模型直接炸了——Loss 飞到了天上。后来我才明白,这两个参数是「连体婴儿」,必须一起调。
3.1 线性缩放法则(Linear Scaling Rule)
这个法则说白了就一句话:当你把 Batch Size 放大 k 倍时,学习率也放大 k 倍。为什么?我试着用直觉解释一下。
你想想看,梯度下降的本质是沿着 Loss 曲面的负方向走一步。这一步的大小由学习率控制。当 Batch Size 变大时,每个 step 看到的样本更多,梯度估计更准。既然梯度更靠谱了,步子当然可以迈大一点。
核心公式:
lr_new = lr_base × (batch_size_new / batch_size_base)
举个例子:基线是 batch_size=256, lr=0.1。如果改成 batch_size=1024,那么 lr 应该设为 0.4。
我在项目中遇到过这样一个场景:用 ResNet-50 训练 ImageNet,基线 Batch Size 是 256,学习率 0.1。后来为了加速,我把 Batch Size 提到 1024,学习率直接设成 0.4。嗯,效果还不错,Top-1 准确率只掉了 0.2%。但要注意,这个法则不是万能的。
3.2 线性缩放法则的局限性
为什么说它不是万能的?我总结了几点实战经验:
- Batch Size 不能太大:当 Batch Size 超过某个阈值(比如 8192),梯度估计已经足够准了,再放大学习率反而有害。我记得有篇论文说,超过这个点,模型收敛会变慢。
- 学习率有上限:线性缩放不是无限放大。比如基线 lr=0.1,你 Batch Size 放大 100 倍,lr 设成 10?那肯定炸。我试过,Loss 直接 NaN。
- 不同层要区别对待:Batch Normalization 层的统计量对 Batch Size 很敏感。大 Batch Size 下,BN 的均值和方差估计更准,但学习率放大后,BN 层的参数更新可能不稳定。
避坑指南:
我曾经在训练一个 6 层的 Transformer 时,把 Batch Size 从 64 提到 512,学习率按线性缩放从 0.001 提到 0.008。结果模型训练到第 10 个 epoch 时 Loss 突然飙升。后来发现是学习率太大,导致注意力层的梯度爆炸了。解决办法是给学习率设一个上限,比如 0.004。
3.3 学习率预热(Warm-up)策略
预热策略,说白了就是让学习率「软启动」。为什么需要这个?我解释一下。
训练刚开始时,模型参数是随机初始化的。这时候梯度方向很不稳定,如果直接用大学习率,模型很容易「跑偏」。预热就是让学习率从 0 或很小的值,慢慢升到目标值。等模型稳定了,再正常训练。
我个人习惯用两种预热方式:
| 预热方式 | 公式 | 适用场景 |
|---|---|---|
| 线性预热 | lr = lr_target × (step / warmup_steps) | 大多数 CV 任务,简单直接 |
| 指数预热 | lr = lr_target × (1 - exp(-step / warmup_steps)) | NLP 任务,特别是 Transformer |
这里有个细节:预热步数怎么设?我一般设成总步数的 5%-10%。比如训练 100 个 epoch,预热 5-10 个 epoch。但如果你用大 Batch Size,预热步数要适当增加。
实战技巧:
我在训练 BERT 时,Batch Size 从 256 提到 2048,预热步数从 1000 步提到 4000 步。效果很明显,模型收敛速度提升了 3 倍,而且最终精度没掉。说白了,预热就是给模型一个「适应期」。
3.4 联动调参的实战流程
好了,理论讲完了。我分享一下实际调参的步骤:
- 确定基线:先用小 Batch Size(比如 256)跑一个基线,找到合适的学习率。我习惯用学习率范围测试(LR Range Test)来找基线 lr。
- 放大 Batch Size:按 2 倍或 4 倍放大,同时按线性缩放调整学习率。注意不要一次放大太多,我建议每次翻倍。
- 加入预热:预热步数设为总步数的 5%-10%。如果 Batch Size 很大(比如 8192),预热步数可以提到 15%。
- 监控 Loss 曲线:如果 Loss 在训练初期就震荡或发散,说明学习率太大或预热不够。我一般会先降低学习率上限,再增加预热步数。
- 微调:最后用验证集做一次网格搜索,微调学习率和预热步数。这一步很关键,我见过很多人跳过这步,结果模型精度差 1-2%。
代码示例:PyTorch 实现线性预热 + 余弦退火
import torch.optim as optim
from torch.optim.lr_scheduler import LambdaLR, CosineAnnealingLR
def get_warmup_scheduler(optimizer, warmup_steps, total_steps):
def lr_lambda(step):
if step < warmup_steps:
return step / warmup_steps
else:
return 0.5 * (1 + cos((step - warmup_steps) / (total_steps - warmup_steps) * pi))
return LambdaLR(optimizer, lr_lambda)
# 使用示例
optimizer = optim.SGD(model.parameters(), lr=0.4, momentum=0.9)
scheduler = get_warmup_scheduler(optimizer, warmup_steps=4000, total_steps=40000)
3.5 知识体系总览
为了让你更直观地理解 Batch Size 和学习率的联动关系,我画了一张图:
这张图展示了我平时调参的完整流程。说白了,就是先定基线,再放大,然后加预热,最后微调。每一步都有坑,但按这个流程走,基本不会出大问题。
3.6 总结与避坑清单
最后,我整理了一份避坑清单,都是我用真金白银换来的经验:
- 不要盲目放大 Batch Size:超过 8192 后,收益递减。我试过 16384,训练快了但精度掉了 1.5%。
- 预热不是万能的:如果学习率本身设得太大,预热也救不了。我建议先用小 Batch Size 跑一个 LR Range Test,找到合理的学习率范围。
- 注意 BN 层:大 Batch Size 下,BN 的统计量更准,但学习率放大后,BN 层的 gamma 和 beta 参数容易震荡。我习惯给 BN 层单独设一个较小的学习率。
- 分布式训练要小心:多卡训练时,Batch Size 是每张卡的 Batch Size 乘以卡数。我见过有人把单卡 Batch Size 也放大,结果总 Batch Size 翻了好几倍,模型直接崩了。
我的个人习惯:
我现在做新项目时,会先固定 Batch Size=512,用 LR Range Test 找到最佳学习率。然后根据显存大小,把 Batch Size 放大到 1024 或 2048,学习率按线性缩放调整。预热步数设成总步数的 8%。这个组合在大部分 CV 和 NLP 任务上都表现不错。当然,具体问题还得具体分析。
嗯,Batch Size 和学习率的联动,说白了就是「步子大了容易扯着蛋」。线性缩放法则给了我们一个起点,预热策略给了我们一个缓冲。两者配合好,训练速度和精度都能兼顾。希望这些经验能帮你少走弯路。