学习率调度:从“瞎蒙”到“有章法”
学习率,说白了就是模型每次更新参数时迈的步子有多大。步子太大,容易扯着蛋——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 做精细调优。这个组合拳我用了很多次,效果很稳。
知识体系总览
下面这张图把学习率调度的核心脉络梳理清楚了。你可以把它当作本章的“地图”。
怎么选?我的实战建议
说了这么多,到底该用哪个?我根据项目经验给你几个参考:
- 新手入门:直接用 Adam,lr=0.001。省心,大部分任务都能收敛。
- 追求精度:先用 Adam 跑 50 个 epoch,然后换成 SGD + Cosine Decay 再跑 50 个 epoch。
- 训练大模型:Cosine Decay + warmup。warmup 是啥?下章会细讲。
- 资源有限:Step Decay,简单可控,调参成本低。
重要提醒:没有万能的学习率策略。同一个任务,换了个数据集,最优策略可能就变了。我的习惯是:先跑一个小规模的实验,对比 3-4 种策略的 loss 曲线,再决定用哪个。