☰
PyTorch学习率调整策略全解析:从StepLR到CosineAnnealingLR实战指南
2026/10/17 17:31:34 网站建设 项目流程

1. 学习率调整的底层逻辑与整体设计思路

做深度学习训练的人,迟早都会撞上同一个问题:模型训练到一半,loss 不降了,准确率卡在一个尴尬的位置上不去。很多人第一反应是换模型、加数据、调 batch size,但折腾一圈之后发现,真正卡住训练效果的,往往是那个最不起眼的超参数——学习率。

学习率决定了每次参数更新的步长。步子太小,训练慢得像蜗牛爬,loss 曲线几乎是一条平线;步子太大,loss 直接震荡甚至发散,模型根本收敛不了。更关键的是,一个固定的学习率几乎不可能从头到尾都好用:训练初期需要较大的步长快速下降,训练后期则需要较小的步长精细收敛。这就是学习率调整策略存在的意义。

PyTorch 在torch.optim.lr_scheduler模块里提供了一整套学习率调整工具,从最经典的 StepLR、MultiStepLR,到平滑衰减的 ExponentialLR、CosineAnnealingLR,再到基于指标动态调整的 ReduceLROnPlateau,基本覆盖了主流训练场景的需求。但工具多不代表好用,很多人用错了 scheduler 的调用时机,或者搞混了 epoch 级和 iteration 级的调整节奏,结果训练效果反而更差。

这篇文章面向的是已经能跑通基本训练流程、但在学习率调优上还比较模糊的读者。我会从 scheduler 的设计逻辑讲起,把每种策略的适用场景、参数含义、调用方式拆开说清楚,然后给出可以直接复现的完整代码,最后把我自己在实际训练中踩过的坑和排查经验整理出来。读完你应该能做到:拿到一个训练任务,能判断该用哪种 scheduler,参数怎么设,代码怎么写,出问题怎么查。

1.1 为什么固定学习率不够用

先把这个核心问题讲透。假设你有一个损失函数曲面,训练的过程就是在这个曲面上找最低点。学习率就是你每一步迈多大。

训练刚开始的时候,参数离最优点很远,梯度方向比较明确,这时候大步走效率高。如果一开始就用很小的学习率,可能几百个 epoch 都走不到有效区域。反过来,训练后期参数已经接近最优点了,曲面局部比较平坦,这时候还迈大步,就会在最低点附近来回跳,永远落不到那个坑里。

我用一个实际场景说明。之前做一个图像分类任务,数据集大概五万张图,用 ResNet 结构训练。固定学习率设 0.1,前 30 个 epoch loss 降得很快,从 2.3 降到 0.8,但之后就一直卡在 0.75 到 0.85 之间震荡,验证集准确率停在 72% 左右上不去。后来换成每 30 个 epoch 把学习率乘以 0.1,同样的网络和数据,最终验证准确率到了 79%。差别就是这么直接。

所以学习率调整不是锦上添花,而是训练策略里必须认真对待的一环。它的本质是:让优化过程在不同阶段有不同的“步长预算”,前期敢走,后期敢收。

1.2 PyTorch scheduler 的设计哲学

PyTorch 把学习率调整抽象成了一个独立的 scheduler 对象,和 optimizer 解耦。这个设计有好有坏。

好处是灵活。你可以自由组合 optimizer 和 scheduler,换策略不用改优化器代码。scheduler 内部维护一个last_epoch计数,每次调用step()就更新一次,然后根据策略计算新的学习率写回 optimizer 的 param_group。

需要注意的是,scheduler 并不直接修改 optimizer 的学习率,而是通过optimizer.param_groups里的lr字段来间接生效。也就是说,如果你手动改过 optimizer 的 lr,scheduler 下一次 step 时会覆盖掉你的修改。这一点后面讲坑的时候还会细说。

另一个设计点是:scheduler 的step()调用频率决定了调整的粒度。有的策略是按 epoch 调,有的是按 iteration 调,还有的是根据验证指标调。调用频率搞错,策略效果会完全走样。

2. 主流学习率调整策略逐一拆解

PyTorch 提供的 scheduler 大概可以分成几类:按固定步长衰减的、按平滑曲线衰减的、按指标动态调整的、以及自定义的。下面逐个说清楚每种的特点和适用场景。

2.1 StepLR 与 MultiStepLR:最经典的阶梯衰减

StepLR 的逻辑最简单:每隔固定的 epoch 数,学习率乘以一个衰减系数。

import torch import torch.optim as optim from torch.optim.lr_scheduler import StepLR optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9) scheduler = StepLR(optimizer, step_size=30, gamma=0.1)

这段代码的意思是:每训练 30 个 epoch,学习率变成原来的 0.1 倍。初始 0.1,第 30 个 epoch 后变 0.01,第 60 个 epoch 后变 0.001,以此类推。

step_size和gamma是两个核心参数。step_size设太小,学习率掉得太快,模型还没学好就没步长可用了;设太大,衰减效果不明显。gamma一般取 0.1 或 0.5,取 0.1 是断崖式下降,取 0.5 相对温和。

MultiStepLR 是 StepLR 的升级版,可以指定在哪些 epoch 衰减,而不是等间隔。

from torch.optim.lr_scheduler import MultiStepLR scheduler = MultiStepLR(optimizer, milestones=[30, 60, 90], gamma=0.1)

milestones列表里的数字就是衰减点。这种策略适合你对训练节奏有明确预期的情况,比如你知道模型大概在 30 epoch 左右收敛变慢,就在那里降一次。

注意:milestones必须递增,且不能有重复值,否则行为未定义。

2.2 ExponentialLR 与 CosineAnnealingLR:平滑衰减路线

阶梯衰减的问题是学习率变化不平滑,每次衰减都会给训练带来一个小的“冲击”。如果你的任务对训练稳定性要求高,可以考虑平滑衰减策略。

ExponentialLR 每个 epoch 都把学习率乘以gamma:

from torch.optim.lr_scheduler import ExponentialLR scheduler = ExponentialLR(optimizer, gamma=0.95)

gamma=0.95意味着每个 epoch 学习率降 5%,100 个 epoch 后大概降到初始值的 0.6%。这种衰减很温和,适合训练周期长、不想让学习率掉太快的场景。

CosineAnnealingLR 走的是余弦曲线:

from torch.optim.lr_scheduler import CosineAnnealingLR scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-5)

T_max是半个余弦周期的长度,eta_min是学习率的下限。学习率会从初始值按余弦曲线降到eta_min,然后再升回去(如果训练超过T_max的话)。这种策略在近几年的论文里很常见,尤其是配合 warm restarts 使用时效果不错。

2.3 ReduceLROnPlateau:看指标说话的动态策略

前面几种策略都是“按计划”衰减,不管模型实际学得怎么样。ReduceLROnPlateau 不一样,它盯着一个指标(通常是验证集 loss),指标不降了就降学习率。

from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, verbose=True)

参数含义:

  • mode='min':指标越小越好(loss);如果是准确率就设'max'
  • factor=0.1:指标停滞时学习率乘以 0.1
  • patience=10:连续 10 个 epoch 指标没改善才触发衰减
  • verbose=True:衰减时打印日志

这个策略的好处是自适应,不需要你猜衰减点。坏处是它依赖验证指标的质量,如果验证集太小或噪声大,可能会误判。

注意:ReduceLROnPlateau 的step()必须传入指标值,即scheduler.step(val_loss),不能空调用。

2.4 自定义 LambdaLR:想怎么调就怎么调

如果内置策略都不满足需求,可以用 LambdaLR 自定义学习率曲线:

from torch.optim.lr_scheduler import LambdaLR lambda_fn = lambda epoch: 0.5 ** (epoch // 20) scheduler = LambdaLR(optimizer, lr_lambda=lambda_fn)

lr_lambda接收 epoch 数,返回一个乘数因子,实际学习率 = 初始学习率 × 因子。上面这个例子等价于每 20 个 epoch 学习率减半。

LambdaLR 的灵活性最高,但需要你自己设计曲线,适合对训练过程有深入理解的情况。

3. 实操流程与完整代码实现

光看参数说明不够,得把 scheduler 放进完整的训练循环里才能看出门道。这一节给出一个可以直接跑的模板,然后逐段解释关键点。

3.1 标准训练循环中的 scheduler 调用

import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR # 假设 model, train_loader, val_loader 已经定义好 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4) scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-5) num_epochs = 100 for epoch in range(num_epochs): # 训练阶段 model.train() train_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() # 验证阶段 model.eval() val_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() val_loss /= len(val_loader) val_acc = 100.0 * correct / total # 关键:scheduler.step() 的位置 scheduler.step() current_lr = optimizer.param_groups[0]['lr'] print(f'Epoch [{epoch+1}/{num_epochs}] ' f'Train Loss: {train_loss/len(train_loader):.4f} ' f'Val Loss: {val_loss:.4f} Val Acc: {val_acc:.2f}% ' f'LR: {current_lr:.6f}')

这段代码里最关键的一行是scheduler.step()的位置。它放在 epoch 循环的末尾,验证阶段之后。对于 CosineAnnealingLR、StepLR 这类按 epoch 调整的策略,这个位置是对的。

但如果你用的是 ReduceLROnPlateau,就得改成:

scheduler.step(val_loss)

而且必须放在验证阶段之后,因为你需要 val_loss 这个值。

3.2 参数选择的具体计算过程

很多人设 scheduler 参数是拍脑袋,其实可以算。

以 CosineAnnealingLR 为例,学习率的变化公式是:

lr = eta_min + (base_lr - eta_min) * (1 + cos(pi * epoch / T_max)) / 2

假设base_lr=0.1,eta_min=1e-5,T_max=100。在第 50 个 epoch 时:

lr = 1e-5 + (0.1 - 1e-5) * (1 + cos(pi * 0.5)) / 2 = 1e-5 + 0.09999 * (1 + 0) / 2 = 1e-5 + 0.049995 ≈ 0.05

也就是说,训练到一半时学习率降到初始值的一半左右。到第 100 个 epoch 时,cos(pi) = -1,学习率降到eta_min。

这个曲线的好处是前期降得慢、中期降得快、后期又变慢,整体比较符合训练的实际需求。

对于 StepLR,step_size可以这样估:假设你计划训练 100 个 epoch,希望学习率衰减 3 次,那step_size大概取 30 左右。gamma取 0.1 的话,最终学习率是0.1 * 0.1^3 = 1e-4,这个量级对大多数任务来说是合理的收敛步长。

3.3 学习率预热(Warmup)的配合使用

有些训练任务在初期需要 warmup,就是让学习率从很小的值慢慢升上去,避免一开始的大梯度把参数带偏。PyTorch 没有内置 warmup,但可以用 LambdaLR 组合实现:

from torch.optim.lr_scheduler import LambdaLR def warmup_cosine(epoch): warmup_epochs = 5 total_epochs = 100 if epoch < warmup_epochs: return (epoch + 1) / warmup_epochs else: progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs) return 0.5 * (1 + math.cos(math.pi * progress)) scheduler = LambdaLR(optimizer, lr_lambda=warmup_cosine)

前 5 个 epoch 学习率从 0 线性升到初始值,之后按余弦曲线衰减。这种组合在 Transformer 类模型训练里几乎是标配。

4. 常见问题与排查技巧实录

scheduler 用起来不难,但坑不少。这一节把我遇到过和自己踩过的问题整理出来,附上排查思路。

4.1 scheduler.step() 调用位置错误

这是最常见的问题。症状是学习率变化不符合预期,或者训练效果比不用 scheduler 还差。

排查方法很简单:在每个 epoch 结束后打印optimizer.param_groups[0]['lr'],看学习率序列对不对。

常见错误包括:

  • 把scheduler.step()放在了 batch 循环里,导致每个 iteration 都调整一次,学习率掉得飞快
  • 把scheduler.step()放在了optimizer.step()之前,导致第一次更新用的是错误的学习率
  • 用 ReduceLROnPlateau 时忘了传指标值

提示:PyTorch 1.1 之后,scheduler.step()必须在optimizer.step()之后调用。虽然旧版本允许在之前调用,但新版本会给出警告。

4.2 恢复训练时学习率不对

如果你用 checkpoint 保存和恢复训练,scheduler 的状态也需要保存:

# 保存 torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': scheduler.state_dict(), }, 'checkpoint.pth') # 恢复 checkpoint = torch.load('checkpoint.pth') model.load_state_dict(checkpoint['model_state_dict']) optimizer.load_state_dict(checkpoint['optimizer_state_dict']) scheduler.load_state_dict(checkpoint['scheduler_state_dict'])

只保存 model 和 optimizer 是不够的。scheduler 内部有last_epoch计数,不恢复的话它会从 0 重新开始,学习率会跳回初始值。

4.3 学习率衰减太快导致欠拟合

有时候你会发现训练后期 loss 几乎不动了,验证准确率也上不去。打印学习率一看,已经降到 1e-7 这种量级了。

这种情况通常是衰减策略太激进。解决办法:

  • 增大step_size或T_max
  • 增大gamma(比如从 0.1 改成 0.5)
  • 设置eta_min或学习率下限,不让它降到太低

我一般会在训练脚本里加一个学习率下限检查,如果低于 1e-6 就手动停掉或调整策略。

4.4 常见问题速查表

问题现象可能原因排查方法解决方案
学习率不变化scheduler.step() 没调用打印 lr 序列检查调用位置
学习率掉太快step 调用频率过高确认在 epoch 级调用移到 epoch 循环末尾
恢复训练后 lr 跳变scheduler 状态未保存检查 checkpoint 内容保存/加载 scheduler state_dict
ReduceLROnPlateau 不触发patience 太大或指标没改善打印指标序列减小 patience 或检查验证集
训练后期 loss 不动学习率过低打印当前 lr调整衰减参数或设下限
warmup 阶段 loss 爆炸warmup 太短检查 warmup epoch 数增加 warmup 长度

4.5 几个实战中总结的小技巧

第一,训练初期先跑几个 epoch 不调学习率,观察 loss 下降情况,再决定用哪种 scheduler。不要一上来就套模板。

第二,如果拿不准参数,先用 ReduceLROnPlateau 跑一遍,它会自动帮你找到合适的衰减点。然后根据它的衰减时机,反推 StepLR 或 CosineAnnealingLR 的参数。

第三,学习率曲线要配合 batch size 看。batch size 增大时,梯度估计更准,可以用更大的学习率。有个经验公式是 lr 和 batch size 的平方根成正比,但不是绝对的,还是要实验验证。

第四,别忘了在 TensorBoard 或 WandB 里记录学习率曲线。训练出问题时,学习率曲线往往能直接告诉你哪里不对。

5. 不同任务场景下的策略选择建议

说了这么多策略,实际用的时候到底怎么选?我按任务类型给一些建议。

图像分类任务,数据量中等(几万到几十万张),训练周期 50 到 200 epoch。这种场景我一般用 CosineAnnealingLR 配合 5 个 epoch 的 warmup,T_max设为总 epoch 数。如果训练不太稳定,换成 StepLR,step_size设为总 epoch 数的三分之一,gamma=0.1。

目标检测和分割任务,训练周期通常更长,loss 波动也更大。这种场景 ReduceLROnPlateau 更合适,patience设大一点(15 到 20),让它有足够的时间观察。factor取 0.1 或 0.5。

自然语言处理任务,尤其是 Transformer 类模型,warmup 几乎是必须的。warmup 长度一般是总步数的 5% 到 10%,之后用线性衰减或余弦衰减。LambdaLR 是最灵活的选择。

小数据集微调任务,训练 epoch 少(10 到 30),学习率本身就应该设小一点(1e-4 到 1e-5)。这种场景用简单的 StepLR 就够了,甚至可以不调,固定学习率也能跑。

提示:不管用什么策略,都建议先跑一个短周期(比如 10 个 epoch)的试验,确认学习率曲线和 loss 曲线都正常,再开完整训练。

5.1 策略对比与选型参考

策略调整粒度适用场景参数敏感度推荐指数
StepLRepoch通用,训练周期明确中四星
MultiStepLRepoch已知衰减点中四星
ExponentialLRepoch长周期平滑衰减低三星
CosineAnnealingLRepoch中短周期,配合 warmup中五星
ReduceLROnPlateauepoch不确定衰减点,指标驱动高四星
LambdaLRepoch自定义曲线,warmup高四星

这个表是我个人经验的总结,不是绝对标准。实际选型还是要结合具体任务和数据特点。

5.2 学习率与 batch size 的配合关系

最后说一个容易被忽略的点:学习率和 batch size 是联动的。

batch size 增大,每次更新用的样本更多,梯度估计的方差更小,理论上可以用更大的学习率。有个经验法则是线性缩放:batch size 翻倍,学习率也翻倍。但这个法则在 batch size 很大时会失效,这时候用平方根缩放更稳。

实际操作中,我一般先固定 batch size(根据显存能开多大开多大),然后在这个 batch size 下调学习率。调好之后再考虑要不要改 batch size,改了就重新调学习率。不要两个一起动,否则出了问题很难定位。

另外,如果你用了梯度累积来模拟大 batch,学习率也要相应调整。梯度累积 N 次相当于 batch size 扩大了 N 倍,学习率可以适当放大,但不用严格按线性缩放,因为梯度累积的方差特性和真实大 batch 不完全一样。

这些细节看起来琐碎,但训练效果往往就藏在这些地方。学习率调整没有万能公式,多试、多记录、多对比,慢慢就能找到适合自己任务的那套参数。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询