引言

如果说 Optimizer 是登山者的步伐,那么 Learning Rate (LR) 就是步幅的大小。Momentum 就是:方向的惯性

Learning Rate:步幅的大小

  • 训练初期:我们需要大步流星(大 LR),快速接近山谷(最优解区域)。
  • 训练后期:我们需要小步挪动(小 LR),精细调整,防止在最低点附近来回震荡,最终收敛到精确的最优解。
    LR Scheduler 就是那个负责在训练过程中动态调整步幅的“变速箱”。

Momentum:方向的惯性

如果只有 Learning Rate,登山者每一步都会只看脚下的坡度(当前梯度):
坡度一变小,脚步立刻变慢,一遇到小坑(局部极小值 / 鞍点),就容易原地打转甚至停住。

Momentum 引入了“惯性”这一概念。

想象一个有重量的球沿着山坡滚动:
每一时刻的“推力”来自当前坡度(当前梯度),但球并不会立刻改变方向,而是会保留之前滚动的趋势。

  • 在平坦区域,它能靠惯性继续前进
  • 在震荡区域,它能过滤掉来回摆动的噪声
  • 在正确方向上,它会越滚越快

公式
v t = m × v t − 1 + g t v_{t} = m \times v_{t-1} + g_{t} vt=m×vt1+gt

w t = w t − 1 − l r × v t w_{t} = w_{t-1} - lr \times v_{t} wt=wt1lr×vt
当前的更新方向,并不是只由“这一刻的梯度”决定,而是由历史惯性 + 当前梯度共同决定。
g t g_t gt:当前时刻的梯度(脚下的坡)
v t − 1 v_{t-1} vt1:过去积累的运动趋势(滚动惯性)
m m m:动量系数(惯性有多强)
m m m (momentum系数) 通常设为 0.9,意味着 90% 依赖惯性,10% 依赖当前梯度。

一、为什么需要 Scheduler?

在训练模型时,我们常遇到这种情况:Loss 下降到一定程度后就不再下降了,或者准确率在某个值附近剧烈震荡。这通常意味着当前的学习率对于精细调优来说太大了
此时,我们需要一个策略:随着训练 Epoch 的增加,逐渐减小学习率。 在 PyTorch 中,这个机制由 torch.optim.lr_scheduler 模块提供。
⚠️ 注意 (版本 1.1.0+) 在 PyTorch 代码中,scheduler.step() 必须放在 optimizer.step() 之后!

  • 正确顺序Forward -> Backward -> Optimizer.step() -> Scheduler.step()
  • 错误后果:如果顺序反了,第一个 Epoch 的学习率会被跳过,导致与预期不符。

二、基类_LRScheduler详解

在Pytorch中,这些scheduler都是继承自_LRScheduler类,官方API文档:torch.optim.lr_scheduler

1. 核心属性

  • optimizer:该调度器关联的优化器(Scheduler 是寄生在 Optimizer 上的)。
  • base_lrs:记录初始学习率(即 Optimizer 中设置的 lr)。
  • last_epoch:记录当前训练到了第几个 Epoch。

2. 核心方法

  • step()执行更新。通常在每个 Epoch 结束时调用,计算并更新下一个 Epoch 的 LR。
  • get_lr()计算逻辑。根据具体的策略(如线性衰减、余弦退火)计算新的 LR。

Scheduler 本质上只做一件事:在合适的时机,修改 Optimizer 中 param_groups 的 lr 值

三、学习率调整策略

PyTorch 提供了十几种策略,但常用的主要分为三类:有序衰减周期性变化自适应调整

1. 有序衰减

随着训练进行,学习率呈阶梯状或线性下降。

  • StepLR (阶梯衰减)
    • 原理:每隔固定的 step_size 个 Epoch,LR 乘以 gamma
    • 场景:最经典的策略,适合大部分基准模型。
  • MultiStepLR (指定步长衰减)
    • 原理:在指定的里程碑(如第 30, 80 Epoch)时,LR 乘以 gamma
    • 场景:当你明确知道模型在哪些 Epoch 需要“变挡”时使用。
  • ExponentialLR (指数衰减)
    • 原理:每个 Epoch 都乘以 gamma,当gamma较小时衰减得非常快。

2. 周期/平滑调整 (Cyclic / Smooth)

学习率不再是单调递减,而是平滑变化,目前 SOTA 模型常用。

  • CosineAnnealingLR (余弦退火) 🔥 (推荐)
    • 原理:LR 按照余弦函数(Cosine)的形状,从最大值平滑下降到最小值。
    • 优势:下降过程平滑,相比阶梯下降,更容易找到更优解。
    • 场景:训练 CNN、Transformer 等现代模型的首选。

3. 自适应调整 (Adaptive)

只有当模型“学不动”了,才降低学习率。

  • ReduceLROnPlateau (基于指标调整) 🔥 (实战常用)
    • 原理:监控某个指标(如 val_loss)。如果该指标在 patience 个 Epoch 内没有优化,就将 LR 乘以 factor
    • 区别:它的 step() 需要传入监控的指标值,如 scheduler.step(val_loss)

官方API

学习率调整策略 简要说明
lr_scheduler.LRScheduler 在优化过程中调整学习率。
lr_scheduler.LambdaLR 设置初始学习率。
lr_scheduler.MultiplicativeLR 通过指定函数中的因子来乘以每个参数组的学习率。
lr_scheduler.StepLR 每 step_size 个 epoch,将每个参数组的学习率按 gamma 衰减。
lr_scheduler.MultiStepLR 当 epoch 数量达到 milestones 之一时,将每个参数组的学习率按 gamma 衰减一次。
lr_scheduler.ConstantLR 将每个参数组的学习率乘以一个小的常数因子。
lr_scheduler.LinearLR 通过线性改变小的乘法因子来衰减每个参数组的学习率。
lr_scheduler.ExponentialLR 每 epoch,将每个参数组的学习率按 gamma 衰减。
lr_scheduler.PolynomialLR 使用给定 total_iters 中的多项式函数来衰减每个参数组的学习率。
lr_scheduler.CosineAnnealingLR 使用余弦退火调度设置每个参数组的学习率。
lr_scheduler.ChainedScheduler 链接一系列学习率调度器。
lr_scheduler.SequentialLR 包含一系列调度器,这些调度器预计在优化过程中按顺序调用。
lr_scheduler.ReduceLROnPlateau 当某个度量停止改进时,降低学习率。
lr_scheduler.CyclicLR 根据循环学习率策略(CLR)设置每个参数组的学习率。
lr_scheduler.OneCycleLR 根据 1cycle 学习率策略设置每个参数组的学习率。
lr_scheduler.CosineAnnealingWarmRestarts 使用余弦退火调度设置每个参数组的学习率。

四、自定义scheduler

虽然官方 API 很全,但有时我们需要特殊的策略(比如前 5 个 Epoch 线性增长做 Warmup,后面再衰减)。我们可以通过自定义函数来实现。

1.简单实现

实现原理:手动修改 optimizer.param_groups 中的 lr 属性。

def adjust_learning_rate(optimizer, epoch, init_lr):
    """
    自定义策略:每 30 个 Epoch,学习率变为原来的 0.1
    """
    lr = init_lr * (0.1 ** (epoch // 30))
    
    # 核心:修改 optimizer 中的 param_groups
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr
        
    return lr

2.继承 _LRScheduler

继承 _LRScheduler 的方式,适用于:

  • 想和 PyTorch 原生 Scheduler 完全一致的行为
  • 需要 state_dict() / last_epoch / resume 训练
  • 想写成一个“可复用组件”

_LRScheduler 本质上帮我们做了三件事:

  • 记录初始学习率 base_lrs
  • 维护当前 epoch / step 计数 last_epoch
  • 统一管理 step() 与 state_dict()
    我们只需要关心一件事:“在第 n 个 epoch,我希望 lr 变成多少?
import torch
from torch.optim.lr_scheduler import _LRScheduler

class CustomStepLR(_LRScheduler):
    def __init__(self, optimizer, step_size, gamma=0.1, last_epoch=-1):
        self.step_size = step_size
        self.gamma = gamma
        super().__init__(optimizer, last_epoch)

    def get_lr(self):
        # 当前 epoch 不需要衰减
        if self.last_epoch < self.step_size:
            return self.base_lrs

        # 计算衰减次数
        factor = self.gamma ** (self.last_epoch // self.step_size)
        return [base_lr * factor for base_lr in self.base_lrs]

# 使用方式
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
scheduler = CustomStepLR(optimizer, step_size=30, gamma=0.1)

for epoch in range(num_epochs):
    train(...)
    optimizer.step()
    scheduler.step()

代码实战部分在上一篇优化器笔记的末尾代码实战部分有涉及!

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐