学习率与批次大小的缩放规律

说到学习率和批次大小的关系,我早期在这上面栽过跟头。本来小批次调得好好的模型,把批次放大四倍,直接炸了——损失不降反升。今天咱就把这个缩放规律彻底捋清楚。

为什么学习率要跟着批次走?

先想一个问题:批次大小翻倍,意味着什么?梯度计算的样本数量多了,所以梯度本身的方差会降低。梯度更稳定,步长就可以迈大点。这就是缩放的核心动机。

从数学上看,随机梯度下降的更新公式是:

θ_{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)。基本方法:

  1. 从很小的学习率开始(比如1e-7)
  2. 每个batch后学习率线性增加
  3. 记录loss的变化曲线
  4. 找到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:学习率与批次的缩放关系图谱

下面这张图展示从小批量实验到大批量训练的决策路径,以及缩放失败时的回滚策略。

学习率与批次缩放决策路径 小批次实验 批次=32, 学习率=1e-4 放大批次 2x / 4x / 8x 线性缩放 η × (B_new / B_base) 平方根缩放 η × sqrt(B_new/B_base) 预热 + 检查 loss 稳定?梯度爆炸? 回滚策略 降学习率 / 换缩放法则 选择逻辑:批次放大≤2x推荐线性,≥4x推荐平方根或线性+预热;失败时降低学习率或换保守缩放法则

注意图上一条虚线箭头来自放大批次到平方根缩放,代表批次放大倍数较大时的推荐路径。实线箭头走线性缩放,适合中小范围调整。如果预热后检测到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。

为什么?三层原因:

  1. 批次太大后梯度极度平滑,学习率放太大导致一步跨出可行域
  2. 没有预热,前几步直接爆炸
  3. 我的学习率根本不是从最优曲线上取的,取值太随意

最终解决方案:把学习率降到3.2e-4(平方根缩放后的值),预热3000步,从1e-6线性涨到目标值。跑了5000步后loss曲线和原来32批次时基本重合。

这个教训让我明白了一个道理——线性缩放是好的起点,但不是终点。监控损失曲线的形状才是关键。

检查清单:

  • □ 确定批次变化倍数
  • □ 选择缩放策略(线性/平方根)
  • □ 设置预热步数(至少总步数的10%)
  • □ 初始预热学习率设为目标学习率的1/100
  • □ 前50步内持续监控loss趋势

搞清楚了学习率和批次的缩放规律,下一步才能放心调其他的超参数。这步做扎实了,后面的工作才会顺利。