训练神经网络时,学习率是最需要花心思的超参数之一。设大了,损失在最优解附近来回震荡,模型不收敛。设小了,训练慢得让人失去耐心。更麻烦的是,一个合适的学习率在训练后期往往不再合适,前期要大步前进,后期要小步精调。学习率调度器(Learning Rate Scheduler)就是解决这个问题的工具,它按照预设规则,在训练过程中自动调整学习率。
这篇文章先讲清楚三件事:学习率是什么、为什么需要动态调整、调度器怎么接进训练循环,然后介绍最常用的一类策略——固定规则衰减,一共 5 种:StepLR、MultiStepLR、ExponentialLR、LambdaLR、MultiplicativeLR。每种都给出构造参数、变化规律、可运行的示例代码和适用场景。
1. 学习率调度器
学习率(learning rate)决定每次参数更新时,沿着梯度方向走多远:w_new = w – lr * gradient。它本质上就是更新步长。步长太大,参数在损失函数的低谷附近反复横跳。步长太小,收敛要花大量 epoch。
理想的学习率策略是动态的。训练初期梯度方向还不稳定,参数离最优解远,用较大的学习率快速前进。训练后期离最优解近了,用小学习率精细打磨,避免震荡。
调度器做的事情,就是每隔固定的间隔(通常是一个 epoch 或一个 step),按照某个规则把当前学习率更新为新的值。它不参与梯度计算,只负责改学习率。PyTorch 中所有调度器都挂在优化器上工作:创建时传入优化器,训练循环里调用 scheduler.step() 触发一次更新。
这一篇只讲固定规则衰减这一类,5 种策略的整体面貌先记在心里:
| 策略 | 核心思想 | 一句话适用场景 |
|---|---|---|
| StepLR | 每固定步数衰减一次 | 经典训练,阶梯式下降 |
| MultiStepLR | 在指定 epoch 点衰减 | 已知训练瓶颈出现在哪几个 epoch |
| ExponentialLR | 每步按固定比例衰减 | 平滑指数衰减 |
| LambdaLR | 用自定义函数缩放学习率 | 想要任意自定义曲线(如 warmup) |
| MultiplicativeLR | 按自定义函数逐级缩放 | 每一步都相对当前值缩放 |
需要说明一点:下面所有构造签名都省略了 verbose 参数,它在 PyTorch 2.0 起已经弃用,传入会打印弃用警告,不影响功能。
2. 使用方法
调度器的使用套路非常固定,就三句话:创建优化器后用调度器包住它。每个 epoch(或 step)结束时调用 scheduler.step()。需要记录当前学习率时用 scheduler.get_last_lr()。最关键的一条铁律:scheduler.step() 必须放在 optimizer.step() 之后,先更新参数,再调整学习率,顺序反了学习率会提前一步变化。
PyTorch 还允许在同一个优化器上挂多个调度器,每个都独立调用 step(),多个规则会叠加作用。示意代码如下(dataset 和 loss_fn 需要按你的任务定义):
import torch
model = [torch.nn.Parameter(torch.randn(2, 2, requires_grad=True))]
optimizer = torch.optim.SGD(model, 0.1)
scheduler1 = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
scheduler2 = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[30, 80], gamma=0.1)
for epoch in range(20):
for input, target in dataset:
optimizer.zero_grad()
output = model(input)
loss = loss_fn(output, target)
loss.backward()
optimizer.step() # 1. 先更新参数
scheduler1.step() # 2. 再调整学习率(每个 epoch 一次)
scheduler2.step()
print(scheduler1.get_last_lr()) # 查看当前学习率
如果按 step 而不是按 epoch 调整,就把 scheduler.step() 放进内层循环。后面每个策略的示例代码都是这个骨架,只是换掉了调度器本身。
3. 固定规则衰减
这一类策略的思路最简单:学习率按确定的规则随时间变小,不需要看任何训练指标。理解它们的关键,是搞清楚乘在谁上面。StepLR、MultiStepLR、ExponentialLR 是乘在初始学习率(或上一步学习率)上做等比衰减,LambdaLR 和 MultiplicativeLR 则把乘数交给一个自定义函数。下面我们按这两组逐个讲。
3.1 等比衰减
这一族有三个成员:StepLR、MultiStepLR、ExponentialLR,我们逐个看。
StepLR(optimizer, step_size, gamma=0.1)
step_size 是触发衰减的间隔,gamma 是每次衰减的乘数。适用场景:训练前期希望快速收敛、后期慢慢精调的经典任务,ResNet 等经典 CNN 的官方训练脚本常用这种阶梯式衰减。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import StepLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.SGD(model_parameters, lr=0.1)
scheduler = StepLR(optimizer, step_size=5, gamma=0.5)
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('StepLR')
plt.grid()
plt.show()
MultiStepLR 和 StepLR 唯一的区别,是衰减点不再均匀分布,而是由你自己指定。milestones 是一个列表,训练走到这些 epoch 时各衰减一次。比如总共训练 100 个 epoch,设 milestones=[10, 40, 50, 80]、gamma=0.5,学习率会在第 10、40、50、80 个 epoch 各减半一次。
MultiStepLR(optimizer, milestones, gamma=0.1)
适用场景:你对任务比较有把握,知道瓶颈大致出现在哪几个 epoch。迁移学习里很常见:先全量训练 N 个 epoch,再在第 N 个 epoch 降一次学习率做微调。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import MultiStepLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.SGD(model_parameters, lr=0.1)
scheduler = MultiStepLR(optimizer, gamma=0.5, milestones=[10, 40, 50, 80])
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('MultiStepLR')
plt.grid()
plt.show()
ExponentialLR 每走一个 step,学习率都乘以 gamma(比如 0.9)。它和 StepLR 的区别在于,它是连续平滑地衰减,没有台阶。设初始学习率 0.1、gamma=0.9,前几步是这样的:
0.09000 # 0.1000 * 0.9 0.08100 # 0.0900 * 0.9 0.07290 # 0.0810 * 0.9 0.06561 # 0.0729 * 0.9 ...
ExponentialLR(optimizer, gamma)
适用场景:想要平滑单调衰减、又不想手工设计里程碑点的时候。需要留意的是,衰减是每 step 一次,训练步数多时学习率会迅速趋近于 0。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import ExponentialLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.SGD(model_parameters, lr=0.1)
scheduler = ExponentialLR(optimizer, gamma=0.9)
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('ExponentialLR')
plt.grid()
plt.show()
3.2 自定义乘数
这一族有两个成员:LambdaLR、MultiplicativeLR。
LambdaLR(optimizer, lr_lambda)
这个策略的意义在于任意曲线:想线性衰减、多项式衰减、余弦式衰减,都可以用 lambda 函数写出来。线性预热(warmup)也常基于它实现,比如前 10 步从 0 线性升到初始学习率。适用场景:需要自定义学习率曲线的任何情况。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import LambdaLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.Adam(model_parameters, lr=0.1)
# lr_lambda 作用在初始学习率上:lr = 0.1 * (0.9 ** step)
scheduler = LambdaLR(optimizer, lr_lambda=lambda step: 0.9 ** step)
learning_rates = [scheduler.get_last_lr()[0]]
for _ in range(10):
optimizer.step()
scheduler.step()
learning_rates.append(scheduler.get_last_lr()[0])
print('%.5f' % scheduler.get_last_lr()[0])
plt.plot(range(len(learning_rates)), learning_rates, 'ro-')
plt.title('LambdaLR')
plt.grid()
plt.show()
MultiplicativeLR 和 LambdaLR 看起来很像,但有一个关键区别:LambdaLR 的乘数乘在初始学习率上(lr = 初始值 × λ(step)),而 MultiplicativeLR 乘在上一步的学习率上(lr = 上一步值 × λ(step))。也就是说,MultiplicativeLR 是逐步等比缩放,每一步都在上一步的基础上乘。
MultiplicativeLR(optimizer, lr_lambda)
比如 lr_lambda=lambda step: 0.9,每一步都乘 0.9,效果与 ExponentialLR(gamma=0.9) 一致。如果 lambda 函数返回的是随 step 变化的值,就能造出前慢后快这类自定义缩放曲线。适用场景:需要每一步都相对当前值缩放的定制规则。
import torch
import torch.optim as optim
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim.lr_scheduler import MultiplicativeLR
model_parameters = [nn.Parameter(torch.tensor([1, 2, 3], dtype=torch.float32))]
optimizer = optim.Adam(model_parameters, lr=0.1)
# 每一步都乘 0.9,等价于 ExponentialLR(gamma=0.9)
scheduler = MultiplicativeLR(optimizer, lr_lambda=lambda step: 0.9)
learning_rates = [scheduler.get_last_lr()[0]]
for _ in range(10):
optimizer.step()
scheduler.step()
learning_rates.append(scheduler.get_last_lr()[0])
print('%.5f' % scheduler.get_last_lr()[0])
plt.plot(range(len(learning_rates)), learning_rates, 'ro-')
plt.title('MultiplicativeLR')
plt.grid()
plt.show()
固定规则衰减的五个策略,差别就两个地方:什么时候降,乘数从哪来。StepLR 固定间隔降,MultiStepLR 指定点降,ExponentialLR 每步降,LambdaLR 和 MultiplicativeLR 把规则交给自定义函数。它们都不看训练状态,按部就班地降。初学者建议从 StepLR 起步,跑通整个训练循环,想平滑衰减就换 ExponentialLR,要自定义曲线就用 LambdaLR。任何策略接上训练后,先跑几十个 step,把 get_last_lr() 画出来,确认曲线形状符合预期,再开始正式训练。

冀公网安备13050302001966号