学习率与批次大小的缩放规律
说到学习率和批次大小的关系,我早期在这上面栽过跟头。本来小批次调得好好的模型,把批次放大四倍,直接炸了——损失不降反升。今天咱就把这个缩放规律彻底捋清楚。
为什么学习率要跟着批次走?
先想一个问题:批次大小翻倍,意味着什么?梯度计算的样本数量多了,所以梯度本身的方差会降低。梯度更稳定,步长就可以迈大点。这就是缩放的核心动机。
从数学上看,随机梯度下降的更新公式是:
θ_{t+1} = θ_t - η · (1/B) Σ ∇L(x_i, θ)
B是批次大小,η是学习率。当你把B变成2B,梯度的方差大约减半,噪声变小了。如果学习率保持不变,步长相对保守,训练速度就浪费了。
我个人习惯用一个简单规则:批次大小翻倍时,学习率也翻倍。也就是线性缩放法则(Linear Scaling Rule)。
核心公式:
η_{new} = η_{base} × (B_{new} / B_{base})
条件是B_{new}不能太大,通常不超过4096。
但这个线性缩放不是万能的。有一次我在项目里把批次从256推到8192,直接用了线性缩放——学习率从1e-4调到3.2e-3。结果是训练直接发散。为什么呢?因为太大的批次让梯度变得过于平滑,模型困在局部最优里出不来了。
实际落地的三个关键点
1. 梯度噪声与学习率的平衡
你想想看,批次小的时候,梯度噪声大。噪声大的好处是有一定“随机探索”能力,能帮助跳出局部最优点。坏处是收敛慢、不稳定。
批次大,梯度接近真实梯度,更新方向很准。但太准了就没有探索性,容易陷入尖锐的最优解。这时候如果学习率还跟着线性放大,步长太大直接翻车。
我在BERT微调项目里遇到过这个典型问题。当时将批次从32升到128,学习率按线性从2e-5调到8e-5。刚开始loss崩了。后来我用了平方根缩放,发现稳定多了。
平方根缩放法则:
η_{new} = η_{base} × sqrt(B_{new} / B_{base})
这个法则更保守,适合模型比较大、批次变化范围大的情况。
2. 预热策略不能省
不管用线性缩放还是平方根缩放,直接起步就上大学习率很危险。我最开始犯过这个错,现在我都会加一个预热(Warm-up)阶段。
预热说白了就是:刚开始时用小学习率,慢慢增加到目标值。让模型先稳定下来,再把学习率提上去。
推荐的预热方式:
# linear warm-up
warmup_steps = int(total_steps * 0.1)
for step in range(warmup_steps):
lr = base_lr * (step / warmup_steps)
前10%步数做线性预热,之后可以用cosine衰减。
我记得有一次,把预热从1000步加到3000步后,同样的学习率缩放方案就从发散变成了稳定收敛。所以别小看预热。
3. 最大学习率的搜索
实际项目中怎么确定一个批次对应的最佳学习率?我建议用学习率范围测试(LR Range Test)。基本方法:
- 从很小的学习率开始(比如1e-7)
- 每个batch后学习率线性增加
- 记录loss的变化曲线
- 找到loss下降最快且不震荡的那个学习率区间
def lr_range_test(model, data, start_lr=1e-7, end_lr=1, steps=100):
lrs = []
losses = []
for step, batch in enumerate(data):
if step > steps: break
lr = start_lr + (end_lr - start_lr) * (step / steps)
optimizer.param_groups[0]['lr'] = lr
loss = train_step(model, batch)
lrs.append(lr)
losses.append(loss)
return lrs, losses
画出来之后,一般选loss下降最陡那一段的中部学习率,然后按批次缩放比例调整。
注意:LR Range Test得到的学习率是针对特定批次大小的。换了批次后必须重新按缩放规则调整,不能直接用。
不同场景下的缩放策略对比
我结合几次实践经验,整理了一个简表:
| 场景 | 批次变化范围 | 推荐缩放方式 | 备注 |
|---|---|---|---|
| 小模型、单GPU | 16 → 64 | 线性缩放 | 梯度噪声适中,直接翻倍没问题 |
| BERT/GPT系列 | 32 → 256 | 线性缩放+预热 | 必须加预热,不然前期梯度更新太猛 |
| 大规模分布式训练 | 256 → 4096 | 平方根缩放 | 线性缩放已经不适用,更保守策略安全 |
| Vision Transformer | 64 → 1024 | 线性缩放+热身 | 与NLP略有不同,建议先做LR Range Test |
这个表反映的是我自己的经验。不同任务、不同优化器下会有浮动,总体趋势是对的。
SVG:学习率与批次的缩放关系图谱
下面这张图展示从小批量实验到大批量训练的决策路径,以及缩放失败时的回滚策略。
注意图上一条虚线箭头来自放大批次到平方根缩放,代表批次放大倍数较大时的推荐路径。实线箭头走线性缩放,适合中小范围调整。如果预热后检测到loss异常(震荡或发散),就进入红色回滚节点——降低学习率或者换更保守的缩放方式。
优化器对缩放的影响
不同优化器的容忍度不一样。先说说Adam:它本身有自适应学习率机制,对批次变化的鲁棒性更强。我见过很多人用Adam时根本不调整学习率,批次翻倍后照样跑。
但SGD+momentum就不行了。它依赖手动设定的学习率,批次变化时必须跟着调。我用过最稳妥的方案就是平方根缩放+预热。
再说一点:如果用了Layer Normalization或者Batch Normalization,批次大小变化会影响BN的统计量估计。特别是BN的running mean和running var,大批次下更稳定,小心小批次推大时的统计漂移。
实操建议:
- Adam + 小批次范围调整:用线性缩放,没有预热也行
- SGD + 大批次调整:必须用平方根缩放+预热,同时监控梯度范数
- 分布式训练时:尽量把预热步数加到总步数的10%-20%
一个真实的失败案例
说个我踩过的坑。训练一个12层Transformer做翻译任务,本地批次32,学习率1e-4。后来上分布式训练,批次放大到1024,我没多想直接用32/1024的比例把学习率升到3.2e-3。结果跑了100步loss从4.5暴涨到8.2。
为什么?三层原因:
- 批次太大后梯度极度平滑,学习率放太大导致一步跨出可行域
- 没有预热,前几步直接爆炸
- 我的学习率根本不是从最优曲线上取的,取值太随意
最终解决方案:把学习率降到3.2e-4(平方根缩放后的值),预热3000步,从1e-6线性涨到目标值。跑了5000步后loss曲线和原来32批次时基本重合。
这个教训让我明白了一个道理——线性缩放是好的起点,但不是终点。监控损失曲线的形状才是关键。
检查清单:
- □ 确定批次变化倍数
- □ 选择缩放策略(线性/平方根)
- □ 设置预热步数(至少总步数的10%)
- □ 初始预热学习率设为目标学习率的1/100
- □ 前50步内持续监控loss趋势
搞清楚了学习率和批次的缩放规律,下一步才能放心调其他的超参数。这步做扎实了,后面的工作才会顺利。