PyTorch 学习率衰减

PyTorch 学习率衰减

训练神经网络时,学习率是最需要花心思的超参数之一。设大了,损失在最优解附近来回震荡,模型不收敛。设小了,训练慢得让人失去耐心。更麻烦的是,一个合适的学习率在训练后期往往不再合适,前期要大步前进,后期要小步精调。学习率调度器(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() 画出来,确认曲线形状符合预期,再开始正式训练。