首页
/ Transformers 学习率调度完全指南:.optimization 模块中的优化器、调度器与 get_scheduler 统一 API

Transformers 学习率调度完全指南:.optimization 模块中的优化器、调度器与 get_scheduler 统一 API

2026-09-06 17:50:50作者:魏献源Searcher

本篇指南基于 Transformers 文档 optimizer_schedules 展开,系统讲解 .optimization 模块提供的三大类能力:支持权重衰减的 Adafactor 优化器、十余种学习率调度器(从常数、线性、余弦到 WSD 与自适应 GreedyLR),以及统一入口 get_scheduler。读完后,你可以独立为微调任务选择并配置合适的学习率策略,并理解 Trainer 内部如何根据 lr_scheduler_type 完成调度器的自动构建。

模块概览:.optimization 提供什么

根据官方文档描述,.optimization 模块提供三类组件:

  • 一个固定权重衰减(weight decay fixed)的优化器,可直接用于模型微调;
  • 一组继承自 PyTorch LambdaLR / LRScheduler 体系的调度器工厂函数get_*_schedule);
  • 支持多批次梯度累积的梯度累积类

源码实现集中在单文件 src/transformers/optimization.py(约 1300 行),调度器名称枚举则定义在 src/transformers/trainer_utils.py。该模块只依赖 PyTorch 的 torch.optim,因此所有调度器都是标准的 torch.optim.lr_scheduler 对象,可与任意训练循环(不限于 Trainer)配合使用。

调度器类型枚举:SchedulerType

SchedulerType 是一个 ExplicitEnum,它定义了 TrainingArgumentslr_scheduler_type 参数的全部合法取值。从 trainer_utils.py 的源码可以看到完整映射关系:

枚举值 字符串取值 对应的调度器函数
LINEAR "linear" get_linear_schedule_with_warmup
COSINE "cosine" get_cosine_schedule_with_warmup
COSINE_WITH_RESTARTS "cosine_with_restarts" get_cosine_with_hard_restarts_schedule_with_warmup
POLYNOMIAL "polynomial" get_polynomial_decay_schedule_with_warmup
CONSTANT "constant" get_constant_schedule
CONSTANT_WITH_WARMUP "constant_with_warmup" get_constant_schedule_with_warmup
INVERSE_SQRT "inverse_sqrt" get_inverse_sqrt_schedule
REDUCE_ON_PLATEAU "reduce_lr_on_plateau" get_reduce_on_plateau_schedule
COSINE_WITH_MIN_LR "cosine_with_min_lr" get_cosine_with_min_lr_schedule_with_warmup
COSINE_WARMUP_WITH_MIN_LR "cosine_warmup_with_min_lr" get_cosine_with_min_lr_schedule_with_warmup_lr_rate
WARMUP_STABLE_DECAY "warmup_stable_decay" get_wsd_schedule
GREEDY "greedy" get_greedy_schedule

文档同时说明:lr_scheduler_type 默认为 "linear"Trainer 内部会据此取用 get_linear_schedule_with_warmup

统一入口:get_scheduler

对于手动编写训练循环的场景,推荐使用统一 API get_scheduler 按名称获取任意调度器:

from transformers import get_scheduler
import torch

optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
scheduler = get_scheduler(
    "cosine",                          # 调度器名称,对应 SchedulerType 字符串
    optimizer=optimizer,
    num_warmup_steps=100,              # warmup 步数
    num_training_steps=2000,           # 总训练步数
    scheduler_specific_kwargs={},      # 调度器专属参数,如 num_cycles、min_lr 等
)

四个参数的含义(依据源码 docstring):

  • namestrSchedulerType):调度器名称;
  • optimizer:训练中使用的 torch.optim.Optimizer
  • num_warmup_steps(可选):warmup 步数。并非所有调度器都需要——constantreduce_lr_on_plateaugreedy 不需要;需要而未提供时函数会抛出 ValueError
  • num_training_steps(可选):总训练步数。除 constantconstant_with_warmupinverse_sqrtreduce_lr_on_plateaugreedywarmup_stable_decay 外,其余调度器均必需;
  • scheduler_specific_kwargs(可选字典):透传给具体调度器的专属参数,例如 cosine_with_restartsnum_cyclescosine_with_min_lrmin_lr。参数与调度器类型不匹配时,底层函数会抛出 TypeError

get_scheduler 的分发逻辑

optimization.py 源码可以看到,模块内维护了一个 TYPE_TO_SCHEDULER_FUNCTION 字典(第 944-957 行),将 12 个 SchedulerType 枚举逐一映射到工厂函数。分发规则为:

  1. 若传入的是 LayerWiseDummyOptimizer(逐层优化器占位对象),则对其中每个子优化器递归调用 get_scheduler,并给参数注册 post_accumulate_grad_hook,使各参数组在梯度累积后自动 step(),最终返回 LayerWiseDummyScheduler——这为后续逐层学习率(layer-wise LR)等高级用法打下基础;
  2. CONSTANT 直接返回 get_constant_schedule(optimizer)
  3. REDUCE_ON_PLATEAUGREEDY 仅透传 scheduler_specific_kwargs(因为二者基于验证指标而非步数工作);
  4. 其余调度器要求 num_warmup_steps 非空,其中 CONSTANT_WITH_WARMUPINVERSE_SQRT 不需要 num_training_stepsWARMUP_STABLE_DECAY 额外要求 num_training_stepsnum_stable_steps 二者之一(通过 scheduler_specific_kwargs 传入);
  5. 其余调度器要求 num_training_steps 非空后,以 num_warmup_steps + num_training_steps + **kwargs 的形式调用工厂函数。

Trainer 内部如何调用 get_scheduler

在使用 Trainer 时,上述入口会被自动封装。Trainer.create_scheduler 的关键实现是:

self.lr_scheduler = get_scheduler(
    self.args.lr_scheduler_type,
    optimizer=optimizer,
    num_warmup_steps=self.args.get_warmup_steps(num_training_steps),
    num_training_steps=num_training_steps,
    scheduler_specific_kwargs=self.args.lr_scheduler_kwargs,
)

也就是说,TrainingArguments 中的 --lr_scheduler_type--warmup_steps(或 warmup_ratio)、--lr_scheduler_kwargs 会在此处被消费。例如想让余弦调度保留最小学习率,可以写:

python train.py ... \
  --lr_scheduler_type cosine_with_min_lr \
  --lr_scheduler_kwargs '{"min_lr": 1e-6}'

从源码结构看,lr_scheduler_kwargs 是一个字符串形式的 JSON 字典,会被解析后原样展开为工厂函数的关键字参数,因此不同调度器可以复用同一个命令行参数位。

各调度器详解与参数说明

以下逐一说明文档列出的调度器,参数与默认值均来自 optimization.py 中的函数签名与 docstring。所有带 warmup 的调度器行为一致:warmup 阶段学习率从 0 线性上升到优化器初始学习率。

get_constant_schedule

get_constant_schedule(optimizer, last_epoch=-1)

学习率保持优化器设定值不变,返回 LambdaLRlast_epoch 用于断点续训时指定恢复到的 epoch 索引。适合与 ReduceLROnPlateau 思想互补的简单基线,或验证训练管线时使用。

get_constant_schedule_with_warmup

get_constant_schedule_with_warmup(optimizer, num_warmup_steps, last_epoch=-1)

warmup 阶段后保持恒定学习率。其核心 lambda 实现为:

def _get_constant_schedule_with_warmup_lr_lambda(current_step, *, num_warmup_steps):
    if current_step < num_warmup_steps:
        return float(current_step) / float(max(1.0, num_warmup_steps))
    return 1.0

注意分母用 max(1.0, num_warmup_steps) 保护,避免 warmup 步数为 0 时除零。

get_linear_schedule_with_warmup

get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps, last_epoch=-1)

Trainer默认调度器:warmup 后学习率从初始值线性衰减到 0。核心公式为:

max(0.0, float(num_training_steps - current_step)
     / float(max(1, num_training_steps - num_warmup_steps)))

即衰减斜率由「总步数 − warmup 步数」归一化决定,并在末尾钳制到 0。

get_cosine_schedule_with_warmup

get_cosine_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps,
                                num_cycles=0.5, last_epoch=-1)

warmup 后按余弦函数从初始学习率衰减到 0。num_cycles(默认 0.5)表示余弦波数,默认即「半个余弦」——从峰值平滑降到 0:

progress = (current_step - num_warmup_steps) / (num_training_steps - num_warmup_steps)
factor = max(0.0, 0.5 * (1.0 + math.cos(math.pi * 2.0 * num_cycles * progress)))

微调大模型时最常见的选择之一,因其后期学习率趋近于 0,收敛更平稳。

get_cosine_with_hard_restarts_schedule_with_warmup

get_cosine_with_hard_restarts_schedule_with_warmup(optimizer, num_warmup_steps,
                                                   num_training_steps,
                                                   num_cycles=1, last_epoch=-1)

与上一节的区别在于学习率会多次回到峰值(warmup 后执行 num_cycles 次「硬重启」)。从源码看,其 lambda 对进度做了取模:

progress = (current_step - num_warmup_steps) / (num_training_steps - num_warmup_steps)
if progress >= 1.0:
    return 0.0
return max(0.0, 0.5 * (1.0 + math.cos(math.pi * ((num_cycles * progress) % 1.0))))

即把整个衰减区间切成 num_cycles 段,每段内部独立走一次半余弦。注意此处 num_cyclesint(硬重启次数),而普通余弦的 num_cyclesfloat。进度超过 1.0 时返回 0,保证训练末尾学习率归零。

get_cosine_with_min_lr_schedule_with_warmup

get_cosine_with_min_lr_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps,
                                            num_cycles=0.5, last_epoch=-1,
                                            min_lr=None, min_lr_rate=None)

普通余弦的改进版:终点不是 0 而是 min_lr。约束条件(源码第 372-377 行):

  • min_lrmin_lr_rate 只能设置其一,同时设置抛 ValueError
  • 二者都不设置同样抛 ValueError(错误信息明确提示应通过 lr_scheduler_kwargs 传入);
  • 若只给 min_lr,内部会换算为 min_lr_rate = min_lr / optimizer.defaults["lr"]

实现上,衰减因子被重新标定:factor = factor * (1 - min_lr_rate) + min_lr_rate,保证余弦最低点恰好落在 min_lr

get_cosine_with_min_lr_schedule_with_warmup_lr_rate

get_cosine_with_min_lr_schedule_with_warmup_lr_rate(optimizer, num_warmup_steps, num_training_steps,
                                                    num_cycles=0.5, last_epoch=-1,
                                                    min_lr=None, min_lr_rate=None,
                                                    warmup_lr_rate=None)

在上一个基础上新增 warmup_lr_rate 参数:warmup 起点不再是 0,而是从 warmup_lr_rate 比例线性升到 1。未设置时按 1/num_warmup_steps 处理(源码第 403-404 行)。其进度计算还做了「半步修正」(current_step - num_warmup_steps + 1.0),避免第一步与 warmup 末点的重复采样。min_lr / min_lr_rate 的互斥规则与上一节完全一致。

get_polynomial_decay_schedule_with_warmup

get_polynomial_decay_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps,
                                          lr_end=1e-7, power=1.0, last_epoch=-1)

warmup 后以多项式衰减到 lr_end。约束与公式(源码第 273-275 行):

  • 要求 lr_end < lr_init,否则抛 ValueError
  • lr_init 取自 optimizer.defaults["lr"]
  • 衰减项:decay = lr_range * pct_remaining**power + lr_end,其中 pct_remaining = 1 - (current - warmup) / decay_steps
  • power=1.0 时退化为线性衰减(与 fairseq/原始 BERT 实现保持一致);
  • current_step > num_training_steps(例如训练意外超长),学习率锁定在 lr_end/lr_init 比例,不会继续下降。

get_inverse_sqrt_schedule

get_inverse_sqrt_schedule(optimizer, num_warmup_steps, timescale=None, last_epoch=-1)

warmup 后按 1/sqrt(step) 衰减,是 Transformer 原始论文中的经典调度。源码注释表明该实现改编自 Google big_vision 工具库。timescale 未提供时默认取 num_warmup_steps(再退化为 10000):

if timescale is None:
    timescale = num_warmup_steps or 10_000
decay = 1.0 / math.sqrt((current_step + (timescale - num_warmup_steps)) / timescale)

该调度不需要 num_training_steps,因此在 get_scheduler 的分发分支中它只被要求提供 num_warmup_steps——对总步数不可预知的训练(如流式数据)特别合适。

get_reduce_on_plateau_schedule

get_reduce_on_plateau_schedule(optimizer, **kwargs)

直接包装 PyTorch 的 ReduceLROnPlateau(源码第 71 行仅一行:return ReduceLROnPlateau(optimizer, **kwargs)),学习率恒定、在监控指标停止改善时按 factor 下调。其所有参数(modefactorpatiencethreshold 等)经 **kwargs 透传给 PyTorch。由于它按 epoch/验证轮而非按训练步触发,get_scheduler 对其不要求 num_warmup_stepsnum_training_steps

get_wsd_schedule(Warmup-Stable-Decay)

get_wsd_schedule(optimizer, num_warmup_steps, num_decay_steps,
                 num_training_steps=None, num_stable_steps=None,
                 warmup_type="linear", decay_type="cosine",
                 min_lr_ratio=0, num_cycles=0.5, last_epoch=-1)

三阶段调度:warmup → 恒定平台期 → 衰减期。关键约束(源码第 553-566 行):

  • num_training_stepsnum_stable_steps 必须至少提供一个,否则 ValueError;同时提供时优先使用 num_stable_steps 并发出 warning;
  • warmup_type / decay_type 各自取值 "linear""cosine""1-sqrt",越界抛 ValueError
  • 三段步数之和应等于 num_training_steps,否则超出部分的学习率直接落到 min_lr_ratio 对应的最小值。

三阶段 lambda 的核心逻辑:warmup 阶段按所选类型从 min_lr_ratio 升到 1.0;平台期恒为 1.0;衰减阶段按 decay_type 从 1.0 降到 min_lr_ratio,并且每个阶段结束前都做 factor * (1 - min_lr_ratio) + min_lr_ratio 的重标定,保证衰减终点精确落在最小学习率比例上。

GreedyLR 与 get_greedy_schedule:基于指标的双向自适应调度器

scheduler = get_greedy_schedule(optimizer, **kwargs)  # 等价于 GreedyLR(optimizer, **kwargs)

GreedyLRoptimization.py 第 621 行起)是本模块中最独特的调度器:它不按步数走预定曲线,而是根据验证指标双向调整学习率——指标持续改善时除以 factor 提升学习率,指标进入平台期时乘以 factor 降低学习率。与 ReduceLROnPlateau 只有「降」不同,GreedyLR 还能「升」,并在长期触底后支持整表重置。

构造参数(默认值来自源码签名):

参数 默认值 说明
mode "min" "min":指标停止下降时降 LR;"max":停止上升时降 LR
factor 0.95 平台期乘该值降 LR,改善期除该值升 LR,必须小于 1.0
patience 10 连续多少轮无改善后调整;连续多少轮持续改善后升 LR
threshold / threshold_mode 1e-06 / "abs" 判定「有改善」的阈值,"abs""rel"
cooldown 0 降 LR 后暂停调整的轮数
warmup 0 升 LR 后暂停调整的轮数
min_lr 1e-3 学习率下限,支持按参数组传列表
max_lr 1.0 学习率上限,支持列表
smooth / window_size False / 50 是否对指标做滑动窗口平均再决策
reset_start 500 所有参数组都降到 min_lr 后再持续多少步触发 _reset() 整表重置

源码还配套了 StreamingAverage 类(第 581-618 行)实现滑窗均值,且 GreedyLR 自带 state_dict / load_state_dict,可随检查点保存与恢复。典型用法(docstring 示例):

optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
scheduler = GreedyLR(optimizer, mode="min", patience=10)
for epoch in range(100):
    train(...)
    val_loss = validate(...)
    scheduler.step(val_loss)   # 注意:传入的是指标值,而不是空 step()

step(metrics) 的决策流程(源码第 749-793 行):先做可选平滑 → 冷却期/升 LR 保护期内跳过 → 与历史最优比较,累计 num_good_epochs / num_bad_epochs → 任一计数超过 patience 即调用 _increase_lr_reduce_lr。降 LR 时每个参数组钳制在 min_lrs[i] 之上,若全部触底则 reset_start 递减,归零后调用 _reset() 恢复初始学习率并清空所有计数与平滑器状态。

Adafactor 优化器

Adafactor 是 fairseq 原版 Adafactor 的 PyTorch 移植,主打亚线性显存开销:对矩阵参数用行/列向量的分解近似来估计二阶矩,而非维护完整同形状缓存。主要参数与默认值:

参数 默认值 说明
lr None 外部学习率;与 relative_step=True 不可同时使用(源码第 1155 行会抛 ValueError
eps (1e-30, 1e-3) 平方梯度与参数尺度的正则化常数
clip_threshold 1.0 最终梯度更新 RMS 的裁剪阈值
decay_rate -0.8 二阶矩移动平均的衰减系数
beta1 None 一阶矩系数;None 表示不启用一阶矩(纯二阶矩模式)
weight_decay 0.0 L2 惩罚
scale_parameter True 是否按参数 RMS 缩放学习率
relative_step True 是否使用内置的相对步长(min(1e-2, 1/sqrt(step)),warmup_init 时更早)而非外部 lr
warmup_init False 仅当 relative_step=True 时可用(第 1157 行校验)

源码中的 step() 实现值得注意两点:

  1. 低精度支持:参数或梯度为 float16/bfloat16 时,先转 fp32 计算再写回(第 1220-1292 行);不支持稀疏梯度;
  2. 分解近似:对形状维度 ≥ 2 的参数启用 factored 模式,用 exp_avg_sq_rowexp_avg_sq_col 两个低维向量加 _approx_sq_grad(行列因子相乘)近似完整的平方梯度平均——这正是显存开销亚线性的来源。

docstring 给出的 T5 微调推荐配置:

# 关闭内置相对步长,使用外部调度器
Adafactor(model.parameters(), scale_parameter=False, relative_step=False,
          warmup_init=False, lr=1e-3)

若使用 lr=None 让 Adafactor 内部自调度,则需配合代理调度器 AdafactorSchedule(第 1297-1324 行),它实现为空操作:get_lr() 直接从优化器的 _get_lr 读取当前真实学习率,供训练循环记录日志。完整示例:

from transformers.optimization import Adafactor, AdafactorSchedule

optimizer = Adafactor(model.parameters(),
                      scale_parameter=True, relative_step=True,
                      warmup_init=True, lr=None)
lr_scheduler = AdafactorSchedule(optimizer)
trainer = Trainer(..., optimizers=(optimizer, lr_scheduler))

此外,get_adafactor_schedule(optimizer, initial_lr=0.0) 工厂函数(第 1327 行起)可单独获取该代理调度器。

实践建议速查

结合上文源码证据,给出常见场景的选择依据:

场景 推荐 lr_scheduler_type 理由(源码依据)
BERT 类模型微调(Trainer 默认路径) linear 默认值,线性归零衰减实现最简单可预期
大模型 / 视觉模型微调 cosine 半余弦平滑降到 0,后期收敛稳定
希望保留最低学习率 cosine_with_min_lr 必须经 lr_scheduler_kwargsmin_lrmin_lr_rate,二者互斥
数据量未知 / 流式训练 inverse_sqrt 不需要 num_training_stepsget_scheduler 分支只强制 num_warmup_steps
总步数可分三段规划 warmup_stable_decay 可分别指定 warmup / 平台 / 衰减的曲线类型(linear / cosine / 1-sqrt)
按验证集表现动态调参 greedyreduce_lr_on_plateau 前者双向调整并可整表重置,后者仅下调;均不需要步数参数
降低显存的大模型训练 Adafactor 优化器(可与上述调度器组合) 分解二阶矩近似,状态量亚线性

延伸阅读与相关路径

以上路径均可在仓库中直接查看源码,用于核对参数默认值、约束条件与错误分支,便于在自定义训练循环中精确复现 Trainer 的调度行为。

登录后查看全文
热门项目推荐
相关项目推荐