资讯详情

PyTorch StepLR学习率调度:原理、实战与避坑

📅 2026/10/17 13:57:51 | 华诺云谱 👁 阅读
PyTorch StepLR学习率调度:原理、实战与避坑
几乎每个拿 PyTorch 训练过模型的人都撞过这堵墙loss 掉得飞快但掉到某个平台之后就开始原地抖动你以为模型通道数配错了或者数据集预处理出了 bug结果折腾半天发现——把学习率调小一个量级loss 又降了一截。这个现象熟悉吧问题不是模型凉了而是学习率没有按节奏降下来。torch.optim.lr_scheduler.StepLR就是 PyTorch 里专门处理这件事的一个调度器核心逻辑一句话就能说清每隔固定轮数把学习率乘上一个衰减系数。适合谁用如果你在跑固定轮数的实验、做迁移学习微调或者只是想给训练曲线加一层“自动降速”的控制它都很好上手。这篇文章不会只贴 API 文档。我会从“为什么需要它”讲起把参数逐一拆开给你可以直接抄的完整训练循环代码再把我这些年踩过的坑和排查思路一并倒出来最后聊聊它在各种调度器里到底该排第几位。1. 训练卡在平台期先别怪模型看看学习率该不该降1.1 固定学习率为什么走不到底先想一个问题把学习率设成 0.1从头训到尾会发生什么训练初期参数离最优点很远0.1 的步长能让你迅速从“荒原”跑到“丘陵地带”。但越靠近谷底地形越复杂0.1 这个步长就太大了——每一步都可能跨过最优点在谷底两侧来回震荡。你看到的就是 loss 曲线在高位横盘像一条被拉直的波浪线。反过来如果一开始就把学习率设成 0.001前期学习速度会慢到让你怀疑显卡是不是坏了。这就是固定学习率的核心矛盾前期需要大步探索后期需要小步精修。手动调可以但你要么守在终端旁边盯着曲线要么每隔几十轮去改一次配置既不现实也不可复现。这个逻辑其实跟“模拟退火”很像先高温让系统乱跑再慢慢降温让系统落到低能态。深度学习训练里的“温度”就是学习率。StepLR做的就是最简单的降温策略——每隔固定步数降一次温降幅固定节奏固定。1.2 什么时候你会特别需要它我自己的经验里下面几类场景最常用到 StepLR第一类是固定轮数的标准训练流程。比如你要训 90 个 epoch习惯上在 30 和 60 的位置降两次学习率每次乘 0.1。这种“里程碑式”的降法在图像分类、目标检测的常规实验里非常常见因为你早就在同类任务上试出了合适的衰减节奏。第二类是迁移学习和微调。拿预训练权重往下游任务调的时候初始学习率通常不敢给太大StepLR 的小步快跑策略很适合这种“低起点、慢衰减”的节奏。第三类是需要实验高度可复现的情况。StepLR 的衰减行为完全由 step_size 和 gamma 两个数决定不存在随机性同配置跑两次结果几乎一致。这对写论文、对比实验、回归测试特别友好。相比之下后面要提到的 ReduceLROnPlateau 依赖验证集指标的变化复现性就差一些。2. StepLR 的机制与参数三个核心设置讲清楚2.1 数学上的行为是什么样的StepLR 的更新规则非常朴素。假设初始学习率是 lr_0每经过 step_size 个 epoch当前学习率就乘以一个 gamma。写成公式lr lr_0 * gamma^(floor(epoch / step_size))这里的epoch从 0 开始计数。举个例子就明白了。初始学习率 0.1step_size30gamma0.1第 0~29 个 epoch学习率保持 0.1第 30~59 个 epoch学习率变成 0.01第 60~89 个 epoch学习率变成 0.001也就是说学习率的变化是一级一级往下跳的不是连续下降。这个“阶梯”形状就是它名字里 Step 的由来。它不关心你当前 loss 是多少也不管验证集指标动没动纯粹按轮数触发。有人会问为什么用乘法衰减而不是直接减一个固定值因为学习率是跨量级的东西。0.1 减 0.05 还能看0.01 减 0.05 直接成负的了。乘法则天然适配这种指数缩小的需求乘 0.1 就是降一个数量级干净利落。2.2 参数逐个拆解看 PyTorch 的构造函数torch.optim.lr_scheduler.StepLR( optimizer, step_size, gamma0.1, last_epoch-1, verboseFalse )optimizer不用多说是你已经创建好的优化器实例。上面这个学习率调度器模式的关键点在于你必须先把优化器创建出来再创建调度器。调度器内部会读取优化器param_groups里的初始学习率作为自己的基准学习率。step_size是衰减周期单位是“调用 scheduler.step() 的次数”。常规用法是每个 epoch 调一次所以一般也就理解为 epoch 间隔。数值必须大于 0。设成 30 就是每 30 轮降一次。这个值的设置直接影响整个训练的节奏后面我专门讲怎么定。gamma是衰减因子默认 0.1。乘 0.1 就是降一个数量级乘 0.5 就是温和减半。这两个值是最常用的0.1 用于标准流程0.5 用于微调或者你想让衰减曲线更平滑的场景。last_epoch表示当前训练走到哪一步了。默认 -1 表示从零开始。断点续训时从 checkpoint 里恢复 scheduler 的 state_dict这个值会自动对上。手动改它很容易把衰减节奏搞乱不推荐。verbose这个参数在新版 PyTorch 里已经被标记为废弃旧版本它会在每次更新时打印一行提示。我现在一般直接用get_last_lr()手动看学习率不用它。2.3 源码逻辑其实只有几行StepLR 的实现非常简洁如果你有心打开源码看一眼核心逻辑大概等价于下面这段伪代码def _get_closed_form_lr(): return [base_lr * gamma ** (last_epoch // step_size) for base_lr in self.base_lrs]它不依赖上一次的学习率值而是直接用“初始学习率、当前轮数、step_size、gamma”算出当前学习率。这种“闭式解”的好处是数值稳定不会因为中间状态错乱导致学习率漂移。相比那种“上一次学习率乘 gamma”的递归写法闭式解在长训练中更可靠。3. 实操把 StepLR 接入你的训练循环3.1 最标准的接法每个 epoch 末尾调用下面这段代码是我个人最常用的模板你可以直接照抄然后替换成自己的模型和数据集import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import StepLR # 1. 定义模型和损失函数 model nn.Linear(128, 10) criterion nn.CrossEntropyLoss() # 2. 创建优化器设好初始学习率 optimizer optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) # 3. 创建 StepLR 调度器 scheduler StepLR(optimizer, step_size30, gamma0.1) # 4. 训练主循环 for epoch in range(90): model.train() for batch_idx, (inputs, targets) in enumerate(train_loader): optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() # 每个 epoch 结束后更新学习率 scheduler.step() # 打印当前学习率写入日志很有用 current_lr scheduler.get_last_lr()[0] print(fEpoch {epoch1}, Loss: {loss.item():.4f}, LR: {current_lr:.6f})注意scheduler.step()的位置它必须在整个 epoch 的 batch 循环结束之后调用而不是在optimizer.step()后面直接接一行。原因很简单——StepLR 默认以“一轮”为更新单位你在一个 epoch 里调用多次就相当于把 step_size 按 batch 数切碎了行为会完全变形。这个问题我见太多人踩过后面专门有一节讲。3.2 为什么先建 optimizer 再建 scheduler这个顺序不是随便定的。调度器在初始化时会调用optimizer.defaults里的lr把它保存为_base_lrs作为所有后续计算的基准。如果你在创建优化器之后再修改param_groups[0][lr]调度器的基准学习率不会跟着变结果就是训练过程中的实际学习率跟你预期完全对不上。有同学为了自定义学习率先建了 scheduler再改 optimizer 的 lr然后发现调度器算出来的学习率很诡异。解决方法所有学习率相关的初始值都在创建 optimizer 之前定好调度器创建之后就不要再手动改 optimizer 里的 lr 了。3.3 打印和监控学习率的正确姿势我想强调一下打印学习率的习惯。训练日志里除了 loss、acc、时间一定要记录当前学习率。否则你排查问题时少了一条关键线索。获取当前学习率有两种方式# 方式一从调度器拿推荐 current_lr scheduler.get_last_lr()[0] # 方式二从优化器的 param_groups 拿 current_lr optimizer.param_groups[0][lr]如果你对不同的参数组设置了不同的学习率get_last_lr()会返回一个列表按 param_groups 的顺序对应。这时候打印fLRs: {scheduler.get_last_lr()}最方便。3.4 断点续训时千万别丢掉调度器状态很多训练任务一跑就是几十个小时中途断电、OOM、人为中断都难免。恢复训练时如果你只恢复了 model 和 optimizer没恢复 scheduler会出现一个特别迷惑的现象模型继续训练但学习率重新从初始值开始。一个大学习率砸在一个已经收敛的模型上loss 瞬间能飙到天上整个训练直接废掉。正确的保存方式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]) # 当前 epoch 数要恢复 start_epoch checkpoint[epoch]这里的核心在scheduler.load_state_dict()。它会恢复内部的last_epoch和base_lrs让衰减节奏无缝衔接。你千万不要在恢复后再手动构造一个全新的StepLR(optimizer, step_size, gamma, last_epochstart_epoch)试图“续上”因为手动指定 last_epoch 很容易跟 state_dict 里的状态打架行为难以预测。4. StepLR 和其他调度器怎么选一张表看明白4.1 常见调度器的行为对比PyTorch 的torch.optim.lr_scheduler里住着一大家子调度器新手最容易挑花眼。我先列一张表把最常用的几个放在一起对比调度器衰减方式需要设置的超参适用场景StepLR每隔固定轮数乘一次 gammastep_size, gamma训练阶段明确、喜欢固定节奏MultiStepLR在指定的轮数里程碑处衰减milestones, gamma对“什么时候该降”有清晰规划ExponentialLR每个 epoch 都乘一次 gammagamma训练轮数长希望平滑指数下降CosineAnnealingLR按余弦曲线从高到低平滑衰减T_max长训练、需要平滑曲线收敛ReduceLROnPlateau验证集指标停滞时自动降patience, factor衰减时机不确定依赖实时指标从实现逻辑看StepLR 和 ExponentialLR 都属于“按固定公式算”只是衰减周期不同。MultiStepLR 是 StepLR 的灵活版——它允许你指定任意几个 epoch 作为衰减点而不是等间距。CosineAnnealingLR 就比较特别了它不乘因子而是按一个余弦周期把学习率从初始值平滑降到接近 0然后再重置。这在某些任务上收敛效果很好但终态学习率会非常小后期几乎不动。你要是喜欢“阶梯式”的节奏感它反而不合适。ReduceLROnPlateau 是最“智能”的一个。它不按 epoch 触发而是盯着你传给它的指标一般是 val_loss连续 patience 轮没变好就乘 factor。好处是不用预先猜衰减时机坏处是训练过程变得不确定复现性打折。4.2 什么时候坚定选 StepLR我自己选调度器的原则很简单能简单就不复杂。你的实验是固定 90 epoch 或 200 epoch跑完就完事中间没有太多幺蛾子用 StepLR。你是做迁移学习、从预训练权重继续微调衰减节奏想拿捏得稳一点用 StepLR。你需要给团队或同学一个“一句话能讲清楚”的训练配置StepLR 是最好的选择。你刚接触深度学习想先理解“学习率调度”到底怎么回事也从 StepLR 入手最合适。什么时候不选它如果你的训练轮数不固定比如依赖 early stopping提前结束是常态那么 StepLR 的固定周期可能就是浪费——可能还没触发衰减训练就停了。这时候 ReduceLROnPlateau 更合适因为它跟着指标走。另一个场景是你想追求极致的收敛曲线平滑度CosineAnnealingLR 可能更优。4.3 一次实验里的组合套路在真实工程里我不止一次用过“StepLR warmup”的组合。warmup 是让学习率从很小的值线性升到初始值目的是稳定训练初期。具体做法能用 LambdaLR 实现也可以手写学习率更新逻辑。我常用的一个套路是这样前 5 个 epoch 做 warmup学习率从 0.02 线性爬到 0.1之后从第 30 轮开始走 StepLR 的 0.1 衰减节奏。实现上可以直接用 LambdaLR 写一个分段函数或者干脆在循环里手动控制。StepLR 本身不带 warmup 功能但它足够简单方便你在此基础上叠加自定义逻辑。5. 使用 StepLR 最容易翻车的 5 个地方含排查思路5.1 在 batch 循环里调用 scheduler.step()这是我见过最频繁的错误。有人把scheduler.step()写在了optimizer.step()旁边导致每个 batch 都会触发一次学习率更新。假设一个 epoch 有 400 个 batchstep_size30那么一个 epoch 内学习率就被更新了 400 次。实际效果相当于 ExponentialLR 甚至更激进的衰减学习率飞一般地掉到接近 0模型后面基本学不动。排查方法很简单每个 epoch 打印一次学习率。如果发现一个 epoch 前后学习率变了很多次或者学习率曲线不是规则的阶梯状大概率就是调用位置错了。修复方法把scheduler.step()挪到for epoch循环的末尾确保它只被调用一次。5.2 加载 checkpoint 后学习率异常跳变前面说过断点续训时如果不恢复 scheduler 的 state_dict学习率会回到初始值。另一种情况是你恢复了 state_dict但恢复时又手动构造了一个新的调度器造成状态冲突。我之前在某个项目里遇到一个诡异现象训练中断在 45 轮恢复后第 30 轮的位置又出现了一次衰减。排查到最后发现我加载了 scheduler 的 state_dict但是把它 apply 到了一个新建的 StepLR 上而这个新调度器的 step_size 被我改成了 15。state_dict 里的 last_epoch45 跟新 step_size15 一结合立刻触发了一次衰减基准还全乱了。所以这个坑的教训是恢复训练时配置参数必须和保存时完全一致包括 step_size 和 gamma。否则 state_dict 加载进去也会产生错位。5.3 verbose 参数在新版 PyTorch 里已经废弃旧代码里常见这样写scheduler StepLR(optimizer, step_size30, gamma0.1, verboseTrue)在 PyTorch 2.x 里这个verbose参数已经被移除了运行时会直接报类型错误或者未识别参数。我有一次在旧服务器上跑通的项目迁移到新环境后莫名其妙报错排查半天才发现是这个参数被废弃。正确的监控方式是get_last_lr()。如果你确实需要每次学习率变化时留日志建议new_lr scheduler.get_last_lr()[0] if new_lr ! old_lr: logger.info(fLR changed to {new_lr})这样既拿到了学习率变化事件又不依赖废弃参数。5.4 step_size 跟总 epoch 数严重不匹配一步到位的误区是step_size 设成总 epoch 数的一半甚至更大导致整个训练只衰减了一次后半程几乎在固定学习率下硬跑。另一种极端是 step_size 设成 1每轮都乘 0.1学习率很快就萎缩得没法看了。经验值上我一般从“总 epoch 数的三分之一到四分之一”开始试。比如 90 轮的训练step_size30 是常见选择。150 轮的训练可以设 50 或者 40。然后盯着验证集 loss 看如果在某个 epoch 附近进入了平台期且学习率还没触发衰减说明 step_size 偏大了下次调小一点如果模型还没收敛好学习率就已经降到很低说明 step_size 太小。这个调参过程没有银弹但有规律。StepLR 的好处是参数少、语义清晰你很容易把“平台期”和“衰减点”对应起来。5.5 打印学习率老是显示 0 或不变有人用optimizer.param_groups[0][lr]打印学习率遇到两种情况一是显示 0二是从头到尾不变。显示 0 通常是因为训练中手动改过 param_groups 里的 lr且不小心覆盖成了 0或者某个优化器实现里 lr 字段被特殊处理过。不变则可能是调度器创建时压根没有接管优化器或者你创建调度器之后又手动改了 optimizer 的 lr导致调度器每次计算完学习率后并没有真正写回优化器。遇到这种情况先用scheduler.get_last_lr()看调度器认为的学习率是多少。如果调度器输出的值是正常的但optimizer.param_groups[0][lr]不正常那就是写回环节出了问题。StepLR 在执行 step 时会把计算出的学习率写回 param_groups正常情况下两者应该一致。如果出现偏差检查你是不是用了别的机制在同时改动学习率。6. 进阶经验让 StepLR 在你项目里真正好用6.1 把调度器配置写成一个可复现的字典我后来养成了一个习惯把训练相关的所有超参包括调度器配置写在一个配置字典或者 yaml 文件里。比如train_config { optimizer: SGD, lr: 0.1, momentum: 0.9, scheduler: StepLR, step_size: 30, gamma: 0.1, total_epochs: 90, }好处是显而易见的。实验记录里能直接看到每组实验用的衰减节奏几周后回看时不需要翻代码 log。更关键的是这种做法天然避免了“恢复训练时配置不一致”的坑——你保存的配置和恢复时读取的配置来自同一个源一致性有保证。6.2 结合早停策略时怎么权衡如果你的流程里有 early stopping比如验证集指标连续 20 轮没提升就终止训练那么给 StepLR 一个很长的 step_size 可能不明智。很可能训练在第 40 轮就被早停了而 step_size 定为 60一次衰减都没发生相当于整个训练都在固定学习率下跑调度器形同虚设。这种情况下我通常的做法是要么把 step_size 调小到总训练轮数的一半甚至三分之一保证早停之前至少能触发一两次衰减要么改用 ReduceLROnPlateau因为它本身会根据指标停滞来降学习率跟早停逻辑天然契合。如果你就是想让早停来兜底、平时还是用固定节奏那也没有问题——但要意识到调度器可能起不到预期作用。6.3 观察 loss 曲线来反推 step_size 是否合理我这里提供一个个人比较依赖的调参法训练时画出 loss 曲线把每个衰减点用竖线标出来。观察衰减点之后 loss 是否有明显的下降。如果某个衰减点之后 loss 完全没变化说明当前 gamma 可能太小了或者衰减时机太晚——模型早就收敛了降学习率没有产生新的优化空间。如果衰减点之后 loss 掉了一大截说明这个时间点降得很及时你会希望上一个衰减点来得更早一点那就可以把 step_size 调小。如果衰减点之后 loss 瞬间上翘那可能是 gamma 太小学习率降太猛导致优化器在“急剧缩小步长”的瞬间扰动太大。这种情况下改用 0.5 的 gamma 会更温和。这种“以曲线反推参数”的调法比盲目按经验公式设参数要可靠得多。6.4 多参数组时的学习率设置如果你的模型里有几组参数需要不同的学习率比如骨干网络用 0.01、新增分类头用 0.1StepLR 依然适用但你要知道它的行为optimizer optim.SGD([ {params: backbone.parameters(), lr: 0.01}, {params: head.parameters(), lr: 0.1}, ]) scheduler StepLR(optimizer, step_size30, gamma0.1)此时 StepLR 会对每个 param_group 分别按各自的初始学习率做同步衰减。骨干网络从 0.01 变 0.001分类头从 0.1 变 0.01相对比例保持不变。这个特性在迁移学习里很好用——你不想破坏预训练权重的结构希望它的学习率始终比新初始化的层小一个量级StepLR 天然就帮你维持住了这个比例。6.5 一个小技巧用学习率日志辅助判断训练状态最后分享一个我自己觉得性价比极高的习惯每次跑到衰减点时手动记录一下当时验证集指标和该 epoch 的 loss。如果你积累了几轮实验你会发现规律——衰减点前后指标变化的幅度能直接告诉你当前“衰减步伐”是快了还是慢了。比如衰减后验证集 acc 提升了 2 个点说明这个时间点降学习率是有效的下次实验可以把这个衰减点提前。如果衰减后 acc 涨了不到 0.5 个点那说明模型早就收敛了衰减点偏晚。这种做法几乎不增加成本但对训练策略的调整特别有帮助。一些个人体会用 StepLR 这些年我最明显的感觉是它不像 CosineAnnealingLR 那样能在 benchmark 上刷出更好看的曲线也不像 ReduceLROnPlateau 那样“聪明”但它有一种难得的确定性——什么轮数降、降多少倍写死了就是写死了。对于需要对比实验、回看 log、复现结果的工作流来说这种确定性本身就是很大的优势。如果你刚开始接触学习率调度我的建议很直接先用 StepLR 把一个固定流程跑通把打印学习率的习惯养好把 checkpoint 里恢复 scheduler 的肌肉记忆建立起来。等这些都顺了再去碰更花哨的调度器你会发现它们解决的那些“痛点”到底长什么样。学习率调度这件事简单的东西往往才是最可靠的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑