按时间表走的调度策略,到点就降学习率,不看训练状态。可实际训练里,你未必知道该在哪个 epoch 降。指标一旦停滞,学习率还按原计划走,就是在浪费时间。有没有一种策略,盯着训练指标,停滞就自动降学习率?
还有一种更激进的思路:让学习率在整个训练里大幅波动,或者只跑一个先升后降的周期,用比平时大得多的学习率,在很少的 epoch 里就收敛。这篇文章讲这两类策略:自适应调度(ReduceLROnPlateau)和循环单周期(CyclicLR、OneCycleLR),最后给一份选型表。
1. 自适应调度
按时间表走的策略,都算开环,到点就降,不看训练状态。ReduceLROnPlateau 是少见的闭环策略:它盯着一个监控指标(通常是验证集 loss),发现指标长时间不改善,就主动把学习率降一档。你不需要事先猜什么时候该降,指标停滞就是信号。
它的构造参数比较多,我们逐个看关键的。mode 选 min 表示指标越低越好(loss),选 max 表示越高越好(准确率)。factor 是衰减乘数,触发时 new_lr = old_lr × factor。patience 是最多容忍多少个 step 指标没有改善,超过才降学习率,patience 太小会被正常波动误伤。threshold 和 threshold_mode 一起定义什么叫改善:rel 模式下,指标比历史最好值降低超过 threshold 才算改善,否则记一次未改善。cooldown 是降过一次学习率后的冷却期,冷却期内不监控不衰减,给模型适应时间。min_lr 是最低学习率下限,eps 是小于该值的衰减直接忽略。
ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10,
threshold=1e-4, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-8)
它的 step() 用法和其他调度器不同,必须传入监控指标:scheduler.step(loss)。下面的例子用随机数模拟 loss 变化,演示它在 loss 停滞时自动降学习率。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import ReduceLROnPlateau
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.SGD(model_parameters, lr=0.1)
# patience=2:连续 2 个 step 指标无改善就衰减,cooldown=2:衰减后 2 步不监控
scheduler = ReduceLROnPlateau(optimizer, factor=0.2, patience=2, cooldown=2)
torch.manual_seed(0)
losses = torch.randint(1, 100, [100,]) / 100 # 模拟 100 个 epoch 的 loss
learning_rates = [scheduler.optimizer.param_groups[0]['lr']]
for loss in losses:
optimizer.step()
scheduler.step(loss) # 注意:这里要传监控指标
learning_rates.append(scheduler.optimizer.param_groups[0]['lr'])
plt.plot(range(len(learning_rates)), learning_rates)
plt.title('ReduceLROnPlateau')
plt.grid()
plt.show()
运行时会打印类似下面的日志,告诉我们学习率在哪些 epoch 发生了衰减(PyTorch 2.0 之后 verbose 参数弃用,旧版本加 verbose=True 才有这些输出):
Epoch 00005: reducing learning rate of group 0 to 2.0000e-02. Epoch 00010: reducing learning rate of group 0 to 4.0000e-03. Epoch 00015: reducing learning rate of group 0 to 8.0000e-04. Epoch 00020: reducing learning rate of group 0 to 1.6000e-04. Epoch 00026: reducing learning rate of group 0 to 3.2000e-05. Epoch 00031: reducing learning rate of group 0 to 6.4000e-06.
适用场景:不熟悉任务该在哪个 epoch 降学习率、或数据集变化频繁(如持续训练、微调)时,ReduceLROnPlateau 是省心的选择,NLP 的 fine-tuning 里很常用。
2. 循环与单周期
这一类的共同点,是学习率幅度很大地波动:不再是单调下降,而是在区间内来回变化。背后的动机来自两个著名研究结论。循环学习率论文(Cyclical Learning Rates for Training Neural Networks)发现,让学习率在区间内循环能更快收敛。超收敛(super-convergence)研究进一步发现,只跑一个先升后降的周期,就能用大得多的最大学习率在很少的 epoch 里收敛。
2.1 循环往返
循环往返的代表是 CyclicLR。
CyclicLR 在 base_lr 和 max_lr 之间周期性往返:step_size_up 步从 base 升到 max,step_size_down 步从 max 降回 base,如此循环。mode 参数决定波形的形状,triangular 是三角波(线性往返),triangular2 是振幅逐周期减半,exp_range 是指数收缩。
CyclicLR(optimizer, base_lr, max_lr, step_size_up=2000, step_size_down=None,
mode='triangular', gamma=1.0, scale_fn=None, scale_mode='cycle',
cycle_momentum=True, base_momentum=0.8, max_momentum=0.9)
base_lr 是循环的下界,通常取 max_lr 的 1/3 到 1/4。max_lr 是上界。step_size_up、step_size_down 决定每个半周期的长度,经验值是样本数除以 batch_size 的 2 到 10 倍,保证每个半周期至少覆盖几个 epoch。cycle_momentum 还会让动量(SGD 的 momentum)反向波动,和学习率错峰配合。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import CyclicLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.SGD(model_parameters, lr=0.1)
scheduler = CyclicLR(optimizer, base_lr=0.1, max_lr=1, step_size_up=10, step_size_down=10)
learning_rates = [scheduler.get_last_lr()[0]]
for _ in range(100):
optimizer.step()
scheduler.step()
learning_rates.append(scheduler.get_last_lr()[0])
plt.plot(range(len(learning_rates)), learning_rates)
plt.title('CyclicLR')
plt.grid()
plt.show()
适用场景:做循环学习率的实验探索、或者不确定最优学习率在什么范围时,用它扫描区间。
2.2 单周期法
单周期的代表是 OneCycleLR。
OneCycleLR 是整个训练只跑一个周期的策略:从很小的初始学习率升到 max_lr,再从 max_lr 一路降到一个很低的终值。因为只有一个周期,需要提前知道训练总步数 total_steps。
OneCycleLR(optimizer, max_lr, total_steps=None, epochs=None, steps_per_epoch=None,
pct_start=0.3, anneal_strategy='cos', cycle_momentum=True,
base_momentum=0.85, max_momentum=0.95, div_factor=25.0,
final_div_factor=10000.0, three_phase=False)
几个关键数值之间的关系要记牢。初始学习率 = max_lr / div_factor,最终学习率 = 初始学习率 / final_div_factor。pct_start 是升温段占整个训练的百分比,默认 0.3,即前 30% 的训练从初始升到 max_lr,后 70% 降到底。anneal_strategy 选 cos 用余弦下降,选 linear 用线性下降。三个参数 total_steps、epochs、steps_per_epoch 提供任意两个即可算出总步数。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import OneCycleLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.SGD(model_parameters, lr=0.1)
# 初始 lr = 0.1/4 = 0.025,前 80 步从 0.025 升到 0.1,后 20 步降到底
scheduler = OneCycleLR(optimizer, max_lr=0.1, pct_start=0.8, total_steps=100,
div_factor=4, final_div_factor=1e-1)
learning_rates = [scheduler.get_last_lr()[0]]
for _ in range(100):
optimizer.step()
scheduler.step()
learning_rates.append(scheduler.get_last_lr()[0])
plt.plot(range(len(learning_rates)), learning_rates)
plt.title('OneCycleLR')
plt.grid()
plt.show()
适用场景:追求超收敛,用比平时大得多的最大学习率、在少量 epoch 内达到接近最优的效果。FastAI 的训练流程大量使用这个策略。注意它必须知道 total_steps,训练步数中途变化(如早停)会失效。
3. 策略选型
策略这么多,选哪个?不用记全部细节,按你的处境对号入座:
| 你的处境 | 推荐策略 |
|---|---|
| 不知道什么时候该降学习率 | ReduceLROnPlateau(看指标自适应) |
| 经典任务,想按部就班降 | StepLR / MultiStepLR |
| 想要平滑单调衰减 | ExponentialLR |
| 训练开头想热身 | LinearLR 或 ConstantLR |
| 想帮模型跳出局部最优 | CosineAnnealingLR / CosineAnnealingWarmRestarts |
| 不同阶段用不同策略 | SequentialLR(如 warmup 后接余弦衰减) |
| 想组合多条规则 | ChainedScheduler |
| 想在少量 epoch 内快速收敛 | OneCycleLR |
| 想探索学习率区间 | CyclicLR |
| 完全自定义曲线 | LambdaLR / MultiplicativeLR |
几条实战建议:
1. scheduler.step() 永远在 optimizer.step() 之后。顺序反了,学习率会提前一步生效,虽然多数时候影响不大,但调试时非常容易踩。
2. ReduceLROnPlateau 的 step 要传指标。其他调度器 step() 不带参数,只有它要 scheduler.step(loss),而且通常每个 epoch 传一次验证集指标,而不是每个 batch。
3. 用 get_last_lr() 而不是猜。记录学习率曲线、写日志、做可视化,都用 scheduler.get_last_lr()[0],别去手动维护一个变量。
4. 多个调度器会叠加。在同一优化器上挂多个调度器,规则是叠加生效的,不要挂一个 StepLR 又挂一个 ExponentialLR 却只想要其中一个的效果。
5. warmup 加衰减是 Transformer 的标准套餐。BERT 这类预训练模型的官方实现基本都是线性预热加线性或余弦衰减,用 SequentialLR 把 LinearLR 和 CosineAnnealingLR 串起来即可。
6. 先看学习率曲线再谈收敛。任何策略接上训练循环后,先跑几十个 step,把 get_last_lr 画出来确认曲线形状符合预期,再开始正式训练,能省掉很多排错时间。
上面三类策略,用起来就三件事。不知道什么时候该降学习率,交给 ReduceLROnPlateau,它盯着验证集指标,停滞就降。想在很少的 epoch 里快速收敛,用 OneCycleLR,先升后降跑一个周期。选型时记住两条主线:按时机选,按幅度选。不管哪个策略,先画学习率曲线,再谈收敛。学习率调度不是玄学,把每个策略的曲线画出来、想清楚时机和幅度,你就掌握了它的全部秘密。

冀公网安备13050302001966号