学习率(Learning Rate)是深度学习中最重要的超参数之一,而学习率调度(Learning Rate Scheduling)则是在训练过程中动态调整学习率的策略。合理的学习率调度方案能显著加速收敛、提升模型性能,并帮助模型跳出局部最优。
在梯度下降过程中,学习率 决定了每一步参数更新的步长:
学习率的选择面临一个基本的矛盾:
假设我们优化一个简单的二次函数 ,其梯度为 。从 开始,不同学习率的效果:
| 学习率 | 第1步后 | 第10步后 | 第100步后 | 效果 |
|---|---|---|---|---|
| 0.01 | 9.8 | 8.19 | 1.35 | 收敛太慢 |
| 0.1 | 8.0 | 1.07 | 良好 | |
| 0.5 | 0.0 | 0.0 | 0.0 | 一步到位但仅适合简单问题 |
| 1.0 | -10 | 10 | -10 | 完全震荡,永不收敛 |
可见固定的单一学习率要么偏慢要么偏快。学习率调度的核心思想:让学习率在训练中动态变化,早期用大学习率快速探索,后期用小学习率精细收敛。
学习率调度通常与动量(Momentum)、Adam 等自适应优化器配合使用。例如,Adam 虽然为每个参数自适应调整步长,但基学习率(base learning rate)仍然是关键超参数,且对调度同样敏感。
| 优化器 | 对学习率敏感度 | 推荐调度策略 |
|---|---|---|
| SGD | 极高 | 必须配合调度(Cosine/Step) |
| SGD + Momentum | 高 | 强烈推荐调度 |
| Adam/AdamW | 中等 | Warmup + Cosine Decay |
| RMSprop | 中等 | Step Decay 或 Plateau 策略 |
Step Decay 是最经典的方法:每经过固定数量的 epoch,将学习率乘以一个衰减因子 :
其中 是初始学习率, 是衰减率(通常 0.1 或 0.5), 是衰减步长(epoch 数)。
训练 ResNet-50 在 ImageNet 上,使用 SGD(momentum=0.9),batch size=256:
| Epoch | Step Decay(, ) | Cosine Decay | OneCycle |
|---|---|---|---|
| 1-30 | 0.1 | 从 0.1 逐渐降至 0.0 | 从 0.0 升至 0.1 |
| 31-60 | 0.01 | 从 0.0 降至 0.0 | 从 0.1 降至 0.001 |
| 61-90 | 0.001 | 从 0.0 降至 0.0 | 从 0.001 降至 0.0 |
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(90):
train_one_epoch(model, dataloader, optimizer)
scheduler.step()
变体 - MultiStepLR:在指定 epoch 衰减,而非固定间隔。常设为 epoch 总数的 1/3 和 2/3 处。
scheduler = lr_scheduler.MultiStepLR(
optimizer, milestones=[30, 60, 80], gamma=0.1
)
| 优点 | 缺点 |
|---|---|
| 实现简单,直观易懂 | 衰减位置不连续,可能导致 loss 突跳 |
| 训练效果稳定可预测 | 需要手动设定衰减点和衰减率 |
| 适合训练数据充足的大规模模型 | 缺乏灵活性,无法针对训练状态自适应调整 |
学习率按指数函数连续衰减:
其中 是衰减常数,控制衰减速度。也可以表示为:
此时 为衰减率(如 0.95),每个 step 后学习率乘以 。
初始学习率 ,, 对比:
| Step | ||
|---|---|---|
| 0 | 0.1000 | 0.1000 |
| 50 | 0.0077 | 0.0605 |
| 100 | 0.0006 | 0.0366 |
| 200 | 0.0000 | 0.0134 |
| 500 | 0.0000 | 0.0007 |
关键观察: 的微小变化会显著影响学习率衰减速度。 时,仅 100 步后学习率就降至接近零;而 时,500 步仍保持有效学习率。
scheduler = lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
指数衰减在以下场景表现良好:
余弦退火由 Loshchilov & Hutter (2017) 在 SGDR 论文中提出,学习率按半个余弦周期从初始值平滑降到最小值:
其中 是总 epoch 数, 是当前 epoch。
假设 ,,:
| Epoch | 余弦值 | 学习率 |
|---|---|---|
| 0 | 1.000 | 0.1000 |
| 10 | 0.951 | 0.0976 |
| 25 | 0.707 | 0.0854 |
| 50 | 0.000 | 0.0500 |
| 75 | -0.707 | 0.0146 |
| 90 | -0.951 | 0.0024 |
| 100 | -1.000 | 0.0000 |
scheduler = lr_scheduler.CosineAnnealingLR(
optimizer, T_max=100, eta_min=0
)
余弦退火的平滑衰减比阶梯衰减更自然。直观理解:
这种「先慢后快再慢」的模式,被实践验证能在多种视觉和 NLP 任务上超越 Step Decay。
在 ResNet-50 训练 100 epoch 的对比实验(数据来自 PyTorch 官方 benchmark):
| 调度策略 | Top-1 准确率 | 训练时间 | 超参数个数 |
|---|---|---|---|
| Step Decay (30,60,90) | 76.15% | 基准 | 3 |
| Cosine Annealing | 76.45% | 相同 | 2(无衰减点) |
| Cosine w/ Warmup | 76.62% | 相同 | 3 |
| OneCycle | 76.38% | 相同 | 2 |
SGDR(Stochastic Gradient Descent with Restarts)在余弦退火基础上引入周期性重启:
每次重启将学习率重置为最大值,允许模型跳出当前局部最优。 可设置为递增数列(如 ),即每次重启的周期越来越长。
scheduler = lr_scheduler.CosineAnnealingWarmRestarts(
optimizer, T_0=10, T_mult=2, eta_min=0
)
训练初期,模型权重是随机初始化的,梯度方向不可靠。直接使用高学习率可能导致训练不稳定甚至梯度爆炸。预热策略在开始阶段使用很小的学习率,逐步增加到目标值。
这被称为 warmup,常见的有两种:
线性预热(Linear Warmup):
常数预热(Constant Warmup):
预热在大 Batch Size 训练中尤为关键。一个具体的例子:
| Batch Size | 无预热(Loss 情况) | 5 epoch 预热(Loss 情况) |
|---|---|---|
| 256 | 稳定下降 | 稳定下降(预热=不必要) |
| 1024 | 初始 Loss 跳升 10-20% | 稳定下降 |
| 8192 | Loss 发散 | 稳定下降 |
原因分析:当 batch size 增大时,梯度估计方差降低,但参数更新的信噪比反而变差——因为梯度方向高度一致,一个错误的步长就会把参数推到很远。预热相当于给模型一个"热身期",让梯度方向先变得可靠。
初始学习率 ,预热 5 epoch:
| Epoch | Warmup | 无 Warmup |
|---|---|---|
| 0 | 0.02() | 0.1 |
| 1 | 0.04 | 0.1 |
| 2 | 0.06 | 0.1 |
| 3 | 0.08 | 0.1 |
| 4 | 0.10 | 0.1 |
| 5+ | 0.10(进入正常调度) | 0.1 |
这是目前训练 ViT、BERT、GPT 等大模型的标配方案:
# 自定义 LambdaLR 实现 Warmup + Cosine
import math
def warmup_cosine_lr(epoch, warmup_epochs=5, total_epochs=100, max_lr=0.1, min_lr=0):
if epoch < warmup_epochs:
return (epoch + 1) / warmup_epochs # 线性升温
# 余弦衰减
progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs)
return 0.5 * (1.0 + math.cos(math.pi * progress))
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=warmup_cosine_lr)
GPT-3 论文中采用了类似策略:前 375M tokens(约 0.1% 的总数据)线性从 0 增加到 ,之后使用余弦衰减到 。这一策略在极大规模模型中被广泛采用。
OneCycle 由 Leslie Smith 在 2018 年提出,是 循环学习率(Cyclical LR) 的一个变体。核心思想是一个完整的周期内,学习率先从低到高(升温阶段),再从高到低(降温阶段),最后在极小值收敛。
完整的 OneCycle 策略包含三个参数调整:
假设总训练 100 epoch,升温比例 30%:
| 阶段 | Epoch 范围 | 学习率变化 | 动量变化 |
|---|---|---|---|
| 升温(Warmup Phase) | 1-30 | (线性) | |
| 降温(Annealing Phase) | 31-100 | (余弦) |
直观理解:
scheduler = lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.1,
total_steps=len(train_loader) * 100, # 总步数
pct_start=0.3, # 升温阶段占总比例的30%
anneal_strategy='cos', # 余弦衰减
cycle_momentum=True, # 同时调整动量
base_momentum=0.85,
max_momentum=0.95,
)
在 CIFAR-10 上使用 ResNet-56 训练 200 epoch 的对比:
| 策略 | Test Accuracy | 训练时间 | 说明 |
|---|---|---|---|
| Step Decay | 92.3% | 基准 | milestones=[80, 120] |
| Cosine Annealing | 93.1% | 相同 | T_max=200 |
| OneCycle | 93.8% | 相同 | pct_start=0.3 |
| OneCycle + Label Smoothing | 94.2% | 相同 |
关键发现:OneCycle 可以用更少的 epoch 达到相同的准确率。Smith 原论文中,使用 OneCycle 可以在 50 epoch 达到标准训练 100 epoch 的准确率,训练时间减半。
在训练中监控验证集指标(如 loss 或 accuracy),当指标在 个 epoch 内不再改善时,学习率乘以衰减因子 。
scheduler = lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 监控 loss 降低
factor=0.1, # 衰减因子
patience=10, # 等待 10 个 epoch
threshold=1e-4, # 改善阈值
min_lr=1e-7 # 最低学习率
)
# 每个 epoch 结束时,传入验证 loss
for epoch in range(epochs):
train_one_epoch(model, dataloader, optimizer)
val_loss = validate(model, val_loader)
scheduler.step(val_loss)
| 参数 | 推荐值 | 说明 |
|---|---|---|
| factor | 0.1 ~ 0.5 | 衰减幅度,太大可能跳过低谷,太小无效 |
| patience | 5 ~ 20 | 等待 epoch 数,取决于总训练长度 |
| threshold | 1e-4 ~ 1e-2 | 过小会导致过早衰减,过大会延迟 |
| min_lr | 1e-6 ~ 1e-8 | 避免学习率降到无效区域 |
优点:无需预设衰减时刻,自动适应训练进度
缺点:额外验证开销,在 loss 震荡时可能过早衰减
学习率在区间 之间周期性变化:
scheduler = lr_scheduler.CyclicLR(
optimizer,
base_lr=0.001, # 最小值
max_lr=0.1, # 最大值
step_size_up=2000, # 升温步数
mode='triangular', # 三角波模式(还有 'triangular2', 'exp_range')
cycle_momentum=True
)
三种模式对比:
| 模式 | 波形 | 特点 |
|---|---|---|
triangular |
固定振幅的三角波 | 每个周期完全一样 |
triangular2 |
振幅减半的三角波 | 学习率范围逐周期缩小 |
exp_range |
指数衰减振幅 | 控制衰减 |
| 策略 | 超参数数量 | 是否需要验证集 | 训练稳定性 | 典型准确率提升 | 适用场景 |
|---|---|---|---|---|---|
| Constant LR | 1 | 否 | 低 | 基准 | 简单任务 baseline |
| Step Decay | 3 | 否 | 中 | +0-1% | 经典视觉模型 |
| MultiStep Decay | N+1 | 否 | 中 | +0-1% | 自定义衰减时机 |
| Exponential Decay | 1 | 否 | 低(后期过早衰减) | - | RNN/在线学习 |
| Cosine Annealing | 2 | 否 | 高 | +0.3-0.5% | 推荐默认 |
| Cosine + Warmup | 3 | 否 | 高 | +0.5-1% | 大模型标配 |
| OneCycle | 2 | 否 | 高 | +0.5-1.5% | 快速收敛 |
| ReduceLROnPlateau | 4 | 是 | 高 | +0-1% | 小数据集/精细调参 |
| CyclicLR | 3 | 否 | 中 | +0-0.5% | 模型调试/消融实验 |
学习率调度策略通常依赖一个好的初始学习率。推荐使用学习率范围测试(LR Range Test):
Loss
|\__
| \___
| \__
| \_______
| \______
+-----------------------→ Learning Rate (log scale)
1e-7 1e-5 1e-3 1e-1
↑
选择此处(loss 下降最陡的点)
经验法则:
| 训练总 Epoch 数 | Step Decay 建议衰减点 | Cosine T_max | Warmup epoch |
|---|---|---|---|
| 50 | [20, 40] | 50 | 5 |
| 100 | [30, 60, 90] | 100 | 5-10 |
| 200 | [60, 120, 160] | 200 | 10-20 |
| 300 | [100, 200, 250] | 300 | 10-20 |
| 框架/库 | 默认调度策略 | 说明 |
|---|---|---|
| PyTorch | 无(需要手动设置) | 提供了丰富的 scheduler |
| TensorFlow | 无(需要手动设置) | Keras 的 LearningRateScheduler |
| Hugging Face Transformers | 线性 Warmup + Linear Decay | 默认 6% warmup ratio |
| fastai | OneCycle | 默认 25% 升温比例 |
| Detectron2 | MultiStep Decay | 默认在 [60000, 80000] 处衰减 |
| MMCV | Cosine Annealing | 支持多种变体 |
import torch
import torch.optim as optim
from torch.optim import lr_scheduler
import matplotlib.pyplot as plt
import numpy as np
def create_scheduler(optimizer, strategy='cosine', total_epochs=100, warmup_epochs=0, **kwargs):
"""创建通用的学习率调度器"""
if strategy == 'step':
step_size = kwargs.get('step_size', 30)
gamma = kwargs.get('gamma', 0.1)
scheduler = lr_scheduler.StepLR(optimizer, step_size=step_size, gamma=gamma)
elif strategy == 'multistep':
milestones = kwargs.get('milestones', [30, 60, 90])
scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.1)
elif strategy == 'cosine':
scheduler = lr_scheduler.CosineAnnealingLR(
optimizer, T_max=total_epochs,
eta_min=kwargs.get('min_lr', 0)
)
elif strategy == 'plateau':
scheduler = lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=kwargs.get('factor', 0.1),
patience=kwargs.get('patience', 10), min_lr=kwargs.get('min_lr', 1e-7)
)
elif strategy == 'onecycle':
max_lr = kwargs.get('max_lr', 0.1)
total_steps = kwargs.get('total_steps', 1000)
scheduler = lr_scheduler.OneCycleLR(
optimizer, max_lr=max_lr, total_steps=total_steps,
pct_start=kwargs.get('pct_start', 0.3)
)
elif strategy == 'cosine_warmup':
# 使用 LambdaLR 实现 Warmup + Cosine
def lambda_func(epoch):
if epoch < warmup_epochs:
return (epoch + 1) / warmup_epochs
progress = (epoch - warmup_epochs) / max(total_epochs - warmup_epochs, 1)
cosine_decay = 0.5 * (1 + np.cos(np.pi * progress))
return cosine_decay * (1 - kwargs.get('min_lr_ratio', 0)) + kwargs.get('min_lr_ratio', 0)
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda_func)
else:
raise ValueError(f"Unknown strategy: {strategy}")
return scheduler
# 使用示例
model = get_model()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# 选择任意策略
scheduler = create_scheduler(
optimizer, strategy='cosine_warmup',
total_epochs=100, warmup_epochs=5,
min_lr_ratio=0.01
)
for epoch in range(100):
train_one_epoch(...)
if strategy != 'plateau':
scheduler.step()
学习率调度本质上是一种模拟退火过程:
Loss Surface
\ Local Minima
\ /\
\_/ \___ Global Minima
^
|
高学习率 → 可以跳过浅小的局部最优
一个重要的洞察:平坦最小值(Flat Minima)通常比尖锐最小值(Sharp Minima)泛化更好。
学习率调度策略(特别是余弦退火和 OneCycle)倾向于引导模型找到平坦最小值,原因:
Test Accuracy
| Flat Minima: Sharp Minima:
| /_______\ /\
| 训练误差=2% 训练误差=1%
| 测试误差=3% 测试误差=8%
+------------
本文档对 ai/ml/learning-rate-scheduling 进行了完整扩展,从骨架页面升级为深度技术文档。