学习率调度:从“瞎蒙”到“有章法”

学习率,说白了就是模型每次更新参数时迈的步子有多大。步子太大,容易扯着蛋——loss 震荡不收敛;步子太小,走到天黑也到不了终点。我刚开始调模型时,就吃过这个亏。

固定学习率是最原始的做法。你设个 0.01,从头到尾都不变。简单是简单,但问题也很明显:

  • 前期太慢:如果学习率设小了,模型在初始阶段走得跟蜗牛一样,半天 loss 降不下去。
  • 后期震荡:如果学习率设大了,模型快接近最优解时,会在最小值附近来回跳,就是落不了地。
  • 一锤子买卖:你没法同时满足“前期大步快跑”和“后期精细微调”这两个需求。

核心矛盾:固定学习率就像用一把扳手拧所有螺丝——总有几个地方拧不紧或者拧花了。

那怎么办?让学习率在训练过程中动态变化。这就是学习率调度的核心思想。

学习率衰减策略:三种主流玩法

我个人习惯把衰减策略分成三类:阶梯式、指数式、余弦式。每种都有自己的脾气。

1. Step Decay(阶梯衰减)

每隔固定的轮数,学习率直接砍掉一截。比如每 30 个 epoch,学习率乘以 0.1。

# PyTorch 实现
scheduler = torch.optim.lr_scheduler.StepLR(
    optimizer, 
    step_size=30,      # 每 30 个 epoch
    gamma=0.1          # 学习率乘以 0.1
)

我在项目中遇到过这种情况:用 Step Decay 训练图像分类模型,前 30 个 epoch loss 降得挺快,第 31 个 epoch 学习率突然掉到原来的十分之一,loss 曲线直接断崖式下跌——嗯,说明之前一直在最优解附近晃悠,就是进不去。

我的经验:Step Decay 适合训练轮数固定的场景。你提前知道要跑 100 个 epoch,那就设 step_size=30,gamma=0.1,简单粗暴有效。

2. Exponential Decay(指数衰减)

学习率按指数函数连续下降。每一步都在衰减,不像 Step 那样突然跳变。

# PyTorch 实现
scheduler = torch.optim.lr_scheduler.ExponentialLR(
    optimizer, 
    gamma=0.95         # 每个 epoch 乘以 0.95
)

指数衰减更平滑,但有个坑:衰减太快的话,后期学习率几乎为零,模型基本不动了。我曾经在训练一个 NLP 模型时用了 gamma=0.9,结果 50 个 epoch 后学习率只剩 0.01 的 0.9^50 ≈ 0.0005,模型直接“冻住”了。

避坑指南:指数衰减的 gamma 不要设得太小。0.95 以上比较安全。如果你不确定,先跑 10 个 epoch 看看学习率曲线。

3. Cosine Decay(余弦衰减)

学习率按照余弦函数从初始值逐渐下降到 0。一开始下降慢,中间加速,最后又变慢。

# PyTorch 实现
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer, 
    T_max=100,         # 半个周期长度
    eta_min=0          # 最小学习率
)

余弦衰减是我现在用得最多的策略。为什么?因为它前期下降慢,让模型有足够时间探索;后期下降也慢,让模型能精细收敛。中间那段加速下降,正好帮模型快速跳过平坦区域。

你想想看,这就像开车——起步时慢慢给油,中间路段加速超车,快到目的地了再减速找车位。是不是很合理?

策略 下降方式 适用场景 我的推荐
Step Decay 阶梯式跳变 固定轮数、分类任务 ⭐⭐⭐
Exponential Decay 连续指数下降 需要平滑衰减 ⭐⭐
Cosine Decay 余弦曲线下降 通用、尤其是大模型 ⭐⭐⭐⭐⭐

自适应学习率方法:让算法自己调

手动调学习率衰减策略,说白了还是靠经验。那有没有办法让优化器自己判断什么时候该大步、什么时候该小步?有——自适应学习率方法。

AdaGrad

每个参数有自己的学习率。频繁更新的参数,学习率自动变小;不常更新的参数,学习率保持较大。

optimizer = torch.optim.Adagrad(model.parameters(), lr=0.01)

AdaGrad 有个硬伤:学习率会一直衰减,到后期几乎为零。我在训练一个稀疏特征模型时用过,前期效果不错,但 200 个 epoch 后模型基本不更新了。

RMSProp

改进了 AdaGrad 的问题。它用指数移动平均来累积梯度信息,不会让学习率单调递减到零。

optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001)

RMSProp 在非凸优化上表现很好。我习惯用它来训练 RNN 和 LSTM,收敛速度比 SGD 快不少。

Adam

把 Momentum(动量)和 RMSProp 结合起来。既考虑了梯度方向,又考虑了梯度大小。是目前最流行的自适应优化器。

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

Adam 几乎成了默认选项。但我得提醒你:它有时候会收敛到泛化性能较差的局部最优。我遇到过几次,用 Adam 训练时 loss 降得很漂亮,但测试集上表现反而不如 SGD。

我的建议:先用 Adam 快速找到一个不错的解,然后切换到 SGD + Cosine Decay 做精细调优。这个组合拳我用了很多次,效果很稳。

知识体系总览

下面这张图把学习率调度的核心脉络梳理清楚了。你可以把它当作本章的“地图”。

学习率调度知识体系 学习率调度 固定学习率 动态学习率 三大问题 ① 前期太慢 ② 后期震荡 ③ 无法兼顾探索与精调 衰减策略 自适应方法 组合策略 Step Decay Exponential Decay Cosine Decay AdaGrad RMSProp Adam Adam → SGD + Cosine 目标:加速收敛 + 提升泛化

怎么选?我的实战建议

说了这么多,到底该用哪个?我根据项目经验给你几个参考:

  1. 新手入门:直接用 Adam,lr=0.001。省心,大部分任务都能收敛。
  2. 追求精度:先用 Adam 跑 50 个 epoch,然后换成 SGD + Cosine Decay 再跑 50 个 epoch。
  3. 训练大模型:Cosine Decay + warmup。warmup 是啥?下章会细讲。
  4. 资源有限:Step Decay,简单可控,调参成本低。

重要提醒:没有万能的学习率策略。同一个任务,换了个数据集,最优策略可能就变了。我的习惯是:先跑一个小规模的实验,对比 3-4 种策略的 loss 曲线,再决定用哪个。