PyTorch 学习率调度器
学习率(Learning Rate)是神经网络训练中最重要的超参数之一。学习率过大,训练震荡甚至发散;学习率过小,收敛极慢,容易陷入局部最优。
学习率调度器(LR Scheduler) 通过在训练过程中动态调整学习率,兼顾训练初期的快速收敛与后期的精细调优。
1. 基础概念与使用模式
标准使用流程
实例
import torch.nn as nn
import torch.optim as optim
model = nn.Linear(10, 1)
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 1. 创建调度器,传入 optimizer
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
# 2. 训练
train(model, optimizer)
# 3. 每个 epoch 结束后调用 scheduler.step()
scheduler.step()
# 4. 查看当前学习率
current_lr = scheduler.get_last_lr()[0]
print(f"Epoch {epoch+1}, LR: {current_lr:.6f}")
step() 的调用时机
实例
optimizer.step()
scheduler.step()
# 错误:scheduler.step() 在 optimizer.step() 之前
# PyTorch 1.1.0 之后会产生警告,部分调度器行为异常
scheduler.step()
optimizer.step()
注意:
optimizer.step()必须在scheduler.step()之前调用。
查看和保存学习率状态
实例
current_lr = optimizer.param_groups[0]['lr']
current_lr = scheduler.get_last_lr()[0] # 上一次 step() 后的 LR
# 保存检查点(必须同时保存 scheduler 状态)
torch.save({
'epoch': epoch,
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'scheduler': scheduler.state_dict(), # 别漏掉这个
}, 'checkpoint.pth')
# 恢复检查点
ckpt = torch.load('checkpoint.pth')
model.load_state_dict(ckpt['model'])
optimizer.load_state_dict(ckpt['optimizer'])
scheduler.load_state_dict(ckpt['scheduler'])
2. 固定衰减调度器
2.1 StepLR 按步衰减
每隔固定 step_size 个 epoch,将学习率乘以 gamma。是最简单、最常用的调度器之一。
公式: lr = lr_base * gamma^(floor(step / step_size))
实例
scheduler = optim.lr_scheduler.StepLR(
optimizer,
step_size=30, # 每 30 个 epoch 衰减一次
gamma=0.1 # 每次乘以 0.1(即缩小为原来的 1/10)
)
# LR 变化:
# Epoch 0-29: 0.1
# Epoch 30-59: 0.01
# Epoch 60-89: 0.001
# Epoch 90+: 0.0001
适用场景:训练节奏固定、阶段清晰的任务,如 ResNet 在 ImageNet 上的训练(90 epoch,在第 30、60 epoch 衰减)。
2.2 MultiStepLR 多里程碑衰减
在指定的若干个 epoch(里程碑)处衰减学习率,比 StepLR 更灵活。
实例
scheduler = optim.lr_scheduler.MultiStepLR(
optimizer,
milestones=[30, 60, 80], # 在第 30、60、80 epoch 衰减
gamma=0.1
)
# LR 变化:
# Epoch 0-29: 0.1
# Epoch 30-59: 0.01
# Epoch 60-79: 0.001
# Epoch 80+: 0.0001
适用场景:已知在哪些 epoch 模型需要精细调整,如分类模型中后期精细收敛阶段。
2.3 ExponentialLR 指数衰减
每个 epoch 都衰减,学习率以指数形式持续下降,衰减更平滑。
公式: lr = lr_base * gamma^epoch
实例
scheduler = optim.lr_scheduler.ExponentialLR(
optimizer,
gamma=0.95 # 每个 epoch 乘以 0.95
)
# LR 变化(前 5 个 epoch):
# Epoch 1: 0.0100
# Epoch 2: 0.0095
# Epoch 3: 0.0090
# Epoch 4: 0.0086
# Epoch 5: 0.0081
gamma设置得太小(如 0.5)会导致学习率迅速趋近于零,通常设在 0.9~0.99 之间。
3. 自适应调度器
3.1 ReduceLROnPlateau 监控指标衰减
最智能的调度器之一:监控某个指标(如验证集 loss),当该指标停止改善时自动降低学习率。不需要提前知道在哪个 epoch 衰减。
实例
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 'min': 监控值越小越好(loss);'max': 越大越好(accuracy)
factor=0.1, # 触发时 lr = lr × factor
patience=10, # 允许指标停滞的 epoch 数,超过则衰减
threshold=1e-4, # 改善幅度小于该值视为未改善
min_lr=1e-6, # 学习率下限,不会低于此值
verbose=True # 打印衰减信息
)
for epoch in range(100):
train_loss = train(model, optimizer)
val_loss = evaluate(model)
# 与其他调度器不同,这里传入监控指标
scheduler.step(val_loss)
监控 Accuracy 的写法:
实例
optimizer,
mode='max', # accuracy 越大越好
factor=0.5,
patience=5,
)
scheduler.step(val_accuracy)
适用场景:几乎所有任务的默认首选,尤其是不确定训练多少 epoch、或训练不稳定时。
3.2 CosineAnnealingLR 余弦退火
学习率按余弦曲线从初始值平滑下降到最小值(eta_min),避免了阶梯式衰减的突变。
公式: lr_t = eta_min + 0.5 * (eta_max - eta_min) * (1 + cos(t * pi / T_max)
实例
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # 半个周期的长度(通常设为总 epoch 数)
eta_min=1e-6 # 学习率最小值(默认 0)
)
# LR 变化轨迹(T_max=10 时的示意):
# 0.1 -> 0.095 -> 0.079 -> 0.055 -> 0.026 -> 0.001
# (余弦曲线平滑下降)
适用场景:训练 epoch 数固定的场景,在 Vision Transformer、ResNet 等论文中广泛使用,收敛质量好。
3.3 CosineAnnealingWarmRestarts
余弦退火的升级版,支持周期性重启(Warm Restarts):每个周期结束后学习率重置回初始值,开始新一轮余弦衰减。允许模型跳出局部最优。
实例
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer,
T_0=10, # 第一个周期的长度(epoch 数)
T_mult=2, # 每次重启后周期长度的倍数(1=等长,2=逐渐加长)
eta_min=1e-6
)
# T_mult=2 时的周期长度:10 -> 20 -> 40 -> 80 ...
# LR 变化(T_0=10, T_mult=1):
# 0.1 -> ... -> 0 -> 0.1 -> ... -> 0 -> 0.1(每 10 个 epoch 重启一次)
适用场景:训练大模型、或希望模型从多个收敛点中选最优时,配合 Snapshot Ensemble 使用效果显著。
4. 预热调度器
Warmup(预热)是指训练开始的若干步内,学习率从一个极小值逐渐升高到目标值。大 batch size 训练、Transformer 类模型几乎都需要预热,否则初期梯度更新过猛,模型难以稳定。
4.1 LinearLR 线性调度
在指定 epoch 内线性改变学习率(可用于线性预热或线性衰减)。
实例
# 线性预热:前 5 个 epoch 内,lr 从 0.01×0.1=0.001 线性增长到 0.01
warmup_scheduler = optim.lr_scheduler.LinearLR(
optimizer,
start_factor=0.1, # 初始 lr = base_lr × start_factor
end_factor=1.0, # 结束 lr = base_lr × end_factor
total_iters=5 # 经过 5 个 epoch 完成
)
# LR 变化:0.001 -> 0.003 -> 0.005 -> 0.007 -> 0.009 -> 0.01
4.2 ConstantLR 常数阶段
在指定 epoch 数内,将学习率固定为 base_lr × factor,之后恢复原值。
实例
scheduler = optim.lr_scheduler.ConstantLR(
optimizer,
factor=0.5,
total_iters=5
)
4.3 SequentialLR 组合调度
将多个调度器按顺序串联,是实现"预热 + 衰减"组合策略的标准做法。
实例
# 阶段一:预热(前 5 个 epoch,LR 从 0.001 线性增长到 0.01)
warmup = optim.lr_scheduler.LinearLR(
optimizer, start_factor=0.1, end_factor=1.0, total_iters=5
)
# 阶段二:余弦退火(剩余 95 个 epoch)
cosine = optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=95, eta_min=1e-6
)
# 组合:先执行 warmup,第 5 个 epoch 后切换到 cosine
scheduler = optim.lr_scheduler.SequentialLR(
optimizer,
schedulers=[warmup, cosine],
milestones=[5] # 在第 5 个 epoch 切换
)
# 使用方式与普通调度器完全一致
for epoch in range(100):
train(...)
scheduler.step()
适用场景:Transformer 训练的标配(预热 + 余弦退火),BERT、GPT、ViT 的预训练均采用此策略。
5. 循环调度器
5.1 CyclicLR 循环学习率
学习率在 base_lr 和 max_lr 之间周期性循环,可帮助模型探索更广的参数空间、跳出鞍点。
实例
scheduler = optim.lr_scheduler.CyclicLR(
optimizer,
base_lr=0.001, # 学习率下界
max_lr=0.01, # 学习率上界
step_size_up=2000, # 从 base_lr 上升到 max_lr 的迭代步数
step_size_down=2000, # 从 max_lr 下降到 base_lr 的迭代步数(默认等于 step_size_up)
mode='triangular', # 三角形循环(等幅)
# mode='triangular2' # 每个周期振幅减半
# mode='exp_range' # 振幅指数衰减
)
# CyclicLR 按 step(batch)调用,不是按 epoch
for epoch in range(num_epochs):
for inputs, labels in train_loader:
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
scheduler.step() # 每个 batch 后调用
三种模式对比:
| mode | 振幅变化 | 特点 |
|---|---|---|
triangular | 不变 | 稳定探索,适合初期 |
triangular2 | 每周期减半 | 先探索后收敛 |
exp_range | 指数衰减 | 最终平稳收敛 |
5.2 OneCycleLR 单周期策略
性能最优的调度器之一,由 fastai 提出的 1-Cycle Policy。整个训练只有一个周期:学习率先升后降,动量(momentum)反向变化。
训练速度更快,通常只需传统训练的 1/5~1/10 的 epoch 数就能达到相同精度。
实例
steps_per_epoch = len(train_loader)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.1, # 学习率最高点
steps_per_epoch=steps_per_epoch, # 每个 epoch 的 step 数
epochs=10, # 总 epoch 数
pct_start=0.3, # 前 30% 用于预热上升
anneal_strategy='cos', # 衰减策略('cos' 或 'linear')
div_factor=25, # 初始 lr = max_lr / div_factor
final_div_factor=1e4 # 最终 lr = max_lr / final_div_factor
)
# 初始 lr = 0.1 / 25 = 0.004
# 峰值 lr = 0.1(在 30% 处)
# 最终 lr = 0.1 / 10000 = 0.00001
# 同样按 batch 调用
for epoch in range(10):
for inputs, labels in train_loader:
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
scheduler.step() # 每个 batch 后调用
适用场景:在计算资源有限、需要快速验证想法时首选;
max_lr建议通过 LR Finder 工具确定最优值。
6. 自定义调度器
6.1 LambdaLR 函数式自定义
通过传入一个 Lambda 函数(接受 epoch 数,返回学习率乘数)来完全自定义调度策略。
实例
# 示例一:Transformer 经典预热策略
# lr ∝ min(step^-0.5, step × warmup_steps^-1.5)
def transformer_lr(epoch, warmup_epochs=10, d_model=512):
if epoch == 0:
return 1e-7 / 0.01 # 避免除零
step = epoch + 1
warmup = warmup_epochs
return (d_model ** -0.5) * min(step ** -0.5, step * warmup ** -1.5) / 0.01
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=transformer_lr)
# 示例二:多项式衰减(lr 从初始值线性/多项式降到 0)
def polynomial_decay(epoch, total_epochs=100, power=1.0):
return max((1 - epoch / total_epochs) ** power, 0.0)
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=polynomial_decay)
# 示例三:分组学习率(不同参数组不同策略)
optimizer = optim.Adam([
{'params': model.backbone.parameters(), 'lr': 1e-4},
{'params': model.head.parameters(), 'lr': 1e-3},
])
# 每个参数组一个 lambda 函数
scheduler = optim.lr_scheduler.LambdaLR(
optimizer,
lr_lambda=[
lambda epoch: 0.95 ** epoch, # backbone:慢衰减
lambda epoch: 0.85 ** epoch, # head:快衰减
]
)
6.2 继承 LRScheduler
需要更复杂的逻辑时,继承 LRScheduler(PyTorch >= 2.0,旧版为 _LRScheduler)实现完全自定义:
实例
import math
class WarmupCosineScheduler(LRScheduler):
"""
预热 + 余弦退火组合调度器(手动实现版)
- 前 warmup_epochs 个 epoch 线性预热
- 之后余弦退火到 min_lr
"""
def __init__(self, optimizer, warmup_epochs, total_epochs,
min_lr=1e-6, last_epoch=-1):
self.warmup_epochs = warmup_epochs
self.total_epochs = total_epochs
self.min_lr = min_lr
super().__init__(optimizer, last_epoch)
def get_lr(self):
epoch = self.last_epoch
# 预热阶段:线性增大
if epoch < self.warmup_epochs:
warmup_factor = (epoch + 1) / self.warmup_epochs
return [base_lr * warmup_factor for base_lr in self.base_lrs]
# 余弦退火阶段
progress = (epoch - self.warmup_epochs) / (
self.total_epochs - self.warmup_epochs
)
cosine_factor = 0.5 * (1 + math.cos(math.pi * progress))
return [
self.min_lr + (base_lr - self.min_lr) * cosine_factor
for base_lr in self.base_lrs
]
optimizer = optim.Adam(model.parameters(), lr=0.01)
scheduler = WarmupCosineScheduler(
optimizer,
warmup_epochs=10,
total_epochs=100,
min_lr=1e-6
)
7. 调度器可视化对比
以下代码可以绘制各调度器的 LR 变化曲线,方便直观对比:
实例
import torch.optim as optim
import matplotlib.pyplot as plt
def simulate_lr(scheduler_fn, epochs=100, steps_per_epoch=None):
"""模拟并记录调度器的学习率变化"""
model = torch.nn.Linear(1, 1)
optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = scheduler_fn(optimizer)
lrs = []
if steps_per_epoch:
# 按 step 调用的调度器
for _ in range(epochs):
for _ in range(steps_per_epoch):
optimizer.step()
scheduler.step()
lrs.append(optimizer.param_groups[0]['lr'])
else:
# 按 epoch 调用的调度器
for _ in range(epochs):
optimizer.step()
scheduler.step()
lrs.append(optimizer.param_groups[0]['lr'])
return lrs
schedulers = {
'StepLR(step=30, gamma=0.1)':
lambda opt: optim.lr_scheduler.StepLR(opt, 30, 0.1),
'CosineAnnealingLR':
lambda opt: optim.lr_scheduler.CosineAnnealingLR(opt, T_max=100),
'ExponentialLR(gamma=0.95)':
lambda opt: optim.lr_scheduler.ExponentialLR(opt, 0.95),
'CosineWarmRestarts(T0=25)':
lambda opt: optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=25),
}
plt.figure(figsize=(12, 5))
for name, fn in schedulers.items():
plt.plot(simulate_lr(fn), label=name)
plt.xlabel('Epoch')
plt.ylabel('Learning Rate')
plt.title('PyTorch LR Scheduler Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('lr_schedulers.png', dpi=150)
plt.show()
8. 完整训练模板
集成了预热 + 余弦退火、模型保存与恢复的生产级训练模板:
实例
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR
# 超参数
EPOCHS = 100
WARMUP_EPOCHS = 5
BASE_LR = 1e-3
MIN_LR = 1e-6
SAVE_PATH = 'best_model.pth'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 模型 / 优化器 / 调度器
model = MyModel().to(device)
optimizer = optim.AdamW(model.parameters(), lr=BASE_LR, weight_decay=1e-4)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# 预热 5 epoch + 余弦退火 95 epoch
warmup_sched = LinearLR(optimizer, start_factor=0.1, total_iters=WARMUP_EPOCHS)
cosine_sched = CosineAnnealingLR(optimizer, T_max=EPOCHS - WARMUP_EPOCHS, eta_min=MIN_LR)
scheduler = SequentialLR(optimizer, [warmup_sched, cosine_sched], milestones=[WARMUP_EPOCHS])
# 恢复检查点
start_epoch = 0
best_acc = 0.0
try:
ckpt = torch.load(SAVE_PATH, map_location=device)
model.load_state_dict(ckpt['model'])
optimizer.load_state_dict(ckpt['optimizer'])
scheduler.load_state_dict(ckpt['scheduler'])
start_epoch = ckpt['epoch'] + 1
best_acc = ckpt['best_acc']
print(f"恢复自 Epoch {start_epoch},最佳准确率 {best_acc:.4f}")
except FileNotFoundError:
print("从头开始训练")
# 训练循环
history = {'train_loss': [], 'val_acc': [], 'lr': []}
for epoch in range(start_epoch, EPOCHS):
# 训练
model.train()
total_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 梯度裁剪
optimizer.step()
total_loss += loss.item()
# 验证
model.eval()
correct = 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
correct += (model(inputs).argmax(1) == labels).sum().item()
avg_loss = total_loss / len(train_loader)
val_acc = correct / len(val_loader.dataset)
cur_lr = scheduler.get_last_lr()[0]
# 记录 & 打印
history['train_loss'].append(avg_loss)
history['val_acc'].append(val_acc)
history['lr'].append(cur_lr)
print(f"Epoch {epoch+1:3d}/{EPOCHS} | "
f"Loss: {avg_loss:.4f} | Acc: {val_acc:.4f} | LR: {cur_lr:.2e}")
# 调度器更新
scheduler.step()
# 保存最优模型
if val_acc > best_acc:
best_acc = val_acc
torch.save({
'epoch': epoch,
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'scheduler': scheduler.state_dict(),
'best_acc': best_acc,
}, SAVE_PATH)
print(f" 保存最优模型,Acc: {best_acc:.4f}")
print(f"\n训练完成,最佳验证准确率: {best_acc:.4f}")
9. 调度器选择指南
按任务类型推荐
| 场景 | 推荐调度器 | 理由 |
|---|---|---|
| 快速实验 / 原型验证 | ReduceLROnPlateau | 无需调超参,自适应衰减 |
| 图像分类(固定 epoch) | CosineAnnealingLR | 平滑衰减,收敛质量好 |
| Transformer / BERT | LinearLR + CosineAnnealingLR | 预热必不可少 |
| 资源有限,快速训练 | OneCycleLR | 1/5 时间达同等精度 |
| 大模型精调(fine-tune) | LinearLR(预热) + ConstantLR | 低学习率稳定微调 |
| 不确定 epoch 数 | ReduceLROnPlateau | 自动响应,不依赖固定节奏 |
| 目标检测(YOLO/Faster-RCNN) | MultiStepLR / OneCycleLR | 有明确衰减点或追求速度 |
| 希望跳出局部最优 | CosineAnnealingWarmRestarts | 周期重启探索参数空间 |
常见配置组合
实例
SequentialLR([LinearLR(5 epochs), CosineAnnealingLR(95 epochs)])
# 组合二:监控验证集 + 自动衰减(通用首选)
ReduceLROnPlateau(mode='min', patience=5, factor=0.5)
# 组合三:OneCycleLR(快速训练首选)
OneCycleLR(max_lr=0.1, total_steps=total_steps, pct_start=0.3)
# 组合四:传统阶梯衰减(CV 经典)
MultiStepLR(milestones=[30, 60, 90], gamma=0.1)
注意事项速查
| 问题 | 解决方法 |
|---|---|
| 调度器没有生效 | 检查 scheduler.step() 是否在 optimizer.step() 之后 调用 |
| CyclicLR / OneCycleLR 没变化 | 这两个按 batch 调用,不是按 epoch |
| ReduceLROnPlateau 不触发 | 检查 mode 是否设对(loss 用 'min',accuracy 用 'max') |
| 恢复训练后 LR 不对 | 检查是否保存并加载了 scheduler.state_dict() |
| 多参数组 LR 设置 | 使用 LambdaLR 传入列表,每个参数组一个 lambda |
