1. 训练卡死的第一个背锅侠:为什么固定学习率总会出问题
先说个现象,你八成也遇到过。模型结构没毛病,数据预处理也对,loss 却在一个高位震荡就是下不去;或者前几百步还挺正常,突然 loss 变成 NaN,把一整个周末的训练计划全毁了。排查到最后,十次里有七次问题出在学习率上。
我见过不少基础扎实的同学,一开始就拍一个lr=0.001跑到底,美其名曰"经典默认值"。这个数值在很多任务上确实是个不错的起点,但一年四季固定不变和根据训练动态不断调整,最终收敛效果能差出几个点。一个简单的类比:训练模型就像下山回家,学习率决定你一步迈多大。天还亮、路还宽的时候自然可以大步流星;等你快到家门口、路变窄了还按原来的步幅迈,一步踏空直接掉沟里。
为什么固定学习率必然难受?因为损失曲面是凹凸不平的。训练刚开始,你离最优点远,曲面斜率大,需要大学习率快速接近谷底;训练后期,你已经在谷底附近晃荡,曲面变平缓,如果还保持大学习率,参数就会在最小值两侧反复横跳,loss 降不下来。更麻烦的是,损失曲面里还有大量的平坦区域、马鞍点和局部极小值,固定学习率的模型一旦滑进马鞍点,几乎很难靠自己走出来。
所以学习率自动调整的核心逻辑很简单:根据训练进程、损失变化或者参数状态,动态改变每次参数更新的步幅。这篇内容不做与具体框架绑定太深的操作手册,而是把学习率自动调整这件事从原理到实践捋透,理清每种策略背后的动机、适用场景和坑。你不需要全部用上,选两三种组合成自己的训练流程就够了。
先说明白一个容易被忽略的前提:学习率调整策略解决的是"步长怎么变",它不负责"方向怎么选"。方向由优化算法(SGD、Adam 这类)决定,步长由学习率决定。这两种东西经常被混为一谈,后面到了 Adam 部分你就知道,它们本质上是在两个维度上做文章。
2. 自适应优化器内建的学习率机制:看似自动,实际是分参数的缩放器
很多人以为"用了 Adam 就不需要调学习率了",这是流传最广的误解之一。Adam 确实会根据每个参数的梯度历史自动调整更新幅度,但它的工作方式和传统调度器完全不同,理解错了会让你在诊断问题时走弯路。
2.1 Adam 整套机制到底自动了什么
Adam 维护两个状态:一阶动量(梯度平均方向)和二阶动量(梯度平方的历史平均)。每次更新时,它用当前梯度除以梯度历史累积量的平方根,相当于给每个参数单独做一个归一化:历史梯度大的参数,单次更新的有效步长会被压缩;历史梯度小且平稳的参数,更新幅度会相对放大。
你注意看,这其实是一个分参数的缩放器,它解决的是"不同参数对同一个全局学习率敏感度不一样"的问题。比如某个参数梯度始终在千分之一量级,另一些参数梯度在个位数量级,全局学习率对前者来说太小、对后者来说太大,Adam 通过除以各自的梯度历史量级把这个差异抹平了。这也是它在稀疏特征场景下表现特别好的原因。
但 Adam 没有解决的问题是:全局基准步长怎么随时间变化。它内部的缩放是针对梯度的,不关心你当前处于训练的哪个阶段。训练后期即使已经接近最优解,Adam 也不会主动把整体步长降下来,因为它感知不到"我们已经快到谷底了"。所以做 NLP 预训练或者大模型微调时,业界普遍的做法是 Adam 家族 + 外接一个学习率调度器。调度器管全局步长曲线,Adam 管参数间差异,两者是互补关系,不冲突。
2.2 AdaGrad 和 RMSProp 的遗产
AdaGrad 是自适应类算法的鼻祖,思路是让学习率除以历史累积梯度平方和的平方根。它的坑在于分母单调递增,越训步长越小,到后期基本学不动了,所以实际用的不多。RMSProp 修掉了这个毛病,用一个滑动平均窗口替换全量累积,使得步长不会永久性收缩。
这几代改进的核心脉络其实就一条:怎么把全局固定步长变成跟随梯度历史而变化的动态步长。RNN、Transformer 这类结构对尺度变化敏感,这类自适应机制几乎是必需品,因为它们的梯度天然有长短期分布不均的特征。这不是什么玄学,是梯度量级的统计规律决定的。
2.3 偏置校正与学习率调参的关系
Adam 里有个细节值得单独提一下:前几步因为动量状态从零开始,估计是有偏的,所以论文作者加了偏置校正。读代码的时候你会看到bias_correction1和bias_correction2这两个量,分别校正一阶、二阶动量的冷启动偏差。很多人误以为这和"预热"是一回事,其实不完全一样。
预热是主动在训练最初几百步用很小的学习率,让动量状态先累积起来、避免前期方向太激进;偏置校正是对动量统计量本身做一个数学修正。说通俗点,一个是先慢速起跑再加速,一个是把起跑时的不准确速度计清零。两者可以同时存在,很多成熟配置里也确实同时存在。实际效果上,没有偏置校正的 Adam 在头几百步会有更新的抖动,反映在 loss 曲线上就是一小段过山车。
2.4 AdamW 带来的修正
AdamW 看起来只改了一个小地方:权重衰减不再混进梯度里,而是直接在参数更新后做一次衰减。但这带来的连锁反应是,学习率和权重衰减之间的耦合解耦了。传统 Adam 里,权重衰减会被二阶动量缩放,导致衰减力度随梯度历史变化,非常不可控。AdamW 把权重衰减挪到外面,让它保持恒定的缩放比例。
我用 AdamW 之后最明显的感觉是:同一个小批量样本数量下,同样的学习率数值,AdamW 能容忍的上限比 Adam 略高一些,训练稳定性和迁移性更可预期。现在主流预训练基线基本默认 AdamW,不是因为潮流,而是因为它的超参解耦让调参更省心。但要注意,它依然不会自己调整全局步长,该配调度器还是得配。
3. 框架自带调度器的分工:Step、Cosine 与 ReduceLROnPlateau 各有各的脾气
明确了自适应优化器不管全局步长之后,就轮到调度器登场了。这类策略是"人为预设学习率变化轨迹",可以再拆成两派:按迭代步数硬性管理和按指标反馈动态管理。两派在实际项目里的角色差异很大。
3.1 阶梯式衰减:硬编码的关键时刻
最朴素的是每隔固定轮数把学习率减半或乘以 0.1,PyTorch 里对应StepLR和MultiStepLR。它背后有个朴素假设:训练过程可以预先划分成几个阶段,每个阶段先快速探索,然后收缩步长精修。
这个策略在 CV 分类任务上的表现一直很稳。我有一次跑一个图像分类任务,用 Cosine 始终差MultiStepLR一点,后来发现是因为这个任务的损失曲面变化非常规整,每到一个 epoch 边界就进入新的局部区域,阶梯式衰减恰好匹配这种节奏。推荐的做法是先用训练集跑一个短周期的实验,看 loss 曲线在哪个 epoch 附近开始平台期,把衰减点设计在平台期前一两轮,而不是机械地每 30 轮降一次。
3.2 Cosine 退火:为什么它是默认选项
Cosine 的曲线是平滑地从初始学习率衰减到接近零,不像阶梯那样跳变。PyTorch 里的CosineAnnealingLR需要指定T_max,代表退火周期长度。从数学上看,它按余弦函数的半周期走一段平滑的下降,训练后期学习率已经很低,参数更新变得非常精细。
为什么工程上默认选它?两个原因:平滑和省心。平滑意味着训练中后期不会有阶梯式跳变带来的 loss 突变风险;省心意味着只需要设置好初始学习率和周期长度两个参数,剩下的全部交给公式。相比阶梯式,它的崩溃风险更低,尤其配合较长的训练周期,收敛稳定性好很多。
它唯一明显的坑是T_max设短了导致学习率过早降到接近零,此时 loss 可能还没有充分收敛,后期学习率太低几乎学不动。实际使用时我建议T_max等于总训练步数的 1.2 到 1.5 倍,留出一点余量。宁可使学习率在结束时还没完全降到零,也别让它过早枯竭。
3.3 动态反馈型:指标不动我就降
ReduceLROnPlateau走的是另一条路线。它不关心训练步数,而是盯一个验证指标(通常是验证集 loss 或准确率),连续若干个 epoch 指标不变好,就把学习率乘一个衰减因子。
这个策略对有明确验证指标、且验证集规模合理的任务特别合适。它等于自己判断"模型是不是学不动了",不用你提前猜训练节奏。但也有两个典型的坑:第一,验证集本身的噪声会导致误判,这时候要设置足够的patience和合理的threshold;第二,如果指标一直在缓慢下降但幅度很小,会被判定为 plateau 触发早退,导致学习率被提前调低,后面反而困在局部解里。我的经验是,把factor设成 0.5 到 0.8 之间,不要一降就是一折,太狠。
3.4 Lambda 方式的灵活变体
还有一些场景需要完全自定义学习率变化,LambdaLR就是留给这种需求的。你可以写任意函数来定义每个 epoch 的学习率倍数,例如恢复学习、做阶段式切换,或者实现一个非常规的曲线。它本质是一个"白板",理论上可以做前面所有策略的任意组合。
但说实话,除非你的任务非常规到前面的调度器都覆盖不了,否则不建议自己发明曲线。一个自创的奇怪曲线如果没有充分的实验依据,很容易在训练后期引入不预期的波动,排查起来还特别费劲。常规任务,用余弦或阶梯就足够了。
4. 循环学习率与热重启:让学习率周期性回升,逃离平坦陷阱
上一节的调度器都是单方向衰减,要么降,要么余弦滑到零。训练逻辑是"近处精修,不再回去探索"。但损失曲面里的局部极小值很多,一旦进去了,单方向衰减的方法很难再跳出来。这就引出另一套思路:周期性把学习率拉回高位,让模型有"反悔"的机会。
4.1 为什么学习率需要周期性反弹
你可以把单方向衰减想象成下山的旅人,越走越慢、越走越谨慎,只在当前山谷里精修,不再翻山。但如果这个山谷其实是个不及格的半山腰呢?周期性反弹做的就是:每隔一段时间,把步长重新放大,带着当前参数状态开始新一轮探索。
这个动机来自对损失曲面几何结构的研究。高维空间里的局部极小值和马鞍点分布极其复杂,一个"看上去不错"的解很可能只是平坦区里的及格水平。周期性增大学习率相当于给模型一个定期跳出当前局部区域的机会,以寻找更平坦、泛化更好的全局解。
4.2 循环学习率的具体做法与参数设置
PyTorch 里的CyclicLR按一个三角形或带平台的曲线,在base_lr和max_lr之间循环。它的核心参数是上下边界和循环步长。实际操作中,max_lr可以用 LR Range Test 来确定(下一章会讲),base_lr一般设为max_lr的十分之一左右。
早期的文献建议每个循环周期设置成训练总步数的 2 到 10 倍,即整个训练过程只走半期到几期循环。也就是说,循环的意义不是高频震荡,而是"爬升—探索—下降—精修"按周期往复。这个策略在训练后期偏向base_lr附近时,相当于保留了稳定收敛能力,同时又定期获得侦察能力。
4.3 热重启的现代实现:SGDR
热重启(SGDR)初看和循环学习率很像,但它的关键差异是每个周期结束后,学习率不是回到一个固定值,而是一次次降低周期内初始值。论文里给出的思路是:每次重启后学习率恢复到一个较高值,但整体上随着周期推进,初始学习率也被逐步压低,形成一种"带重启的总体退火"。
这个设计的精妙之处在于,它平衡了探索和精修。训练前期多来几次大幅重启,充分探索损失曲面;后期重启幅度越来越小,逐渐转入精修模式。在我实测的几个视觉任务里,SGDR 的最终收敛结果经常比纯 Cosine 更稳定,尤其在初始化条件不太理想的情况下,容错性会好很多。
如果你用的是 PyTorch,可以基于CosineAnnealingWarmRestarts来落地。需要注意T_0是首个周期长度,T_mult是周期长度的乘性增长因子。设成 2 的含义是:每个新周期的时间跨度翻倍,也就是"越往后重启越少、精修越多"。
4.4 重启时的损失震荡现象
热重启刚开始那几步,loss 经常会有明显跳升,很多人第一次看到都以为代码写错了。实际上这是正常的,因为学习率被拉高了,模型从精修状态切回探索状态,数值波动自然变大。关键是观察跳升之后有没有持续下降。只要整体趋势向下,或者在几个周期内能够超越上一个谷底,这个策略就是在正常工作的。
如果重启后 loss 跳升幅度特别大,甚至直接发散,优先检查max_lr或重启初始学习率是否过高,其次检查是否在重启前后做了学习率平滑过渡。现在也有不少实现给重启加了一个短预热,我个人的经验是加上确实有助于缓解跳升。
5. 学习率自动搜索:在训练开始之前就定好合理基线
上面讲的都是训练过程中动态调,但启动之前还有一个大问题:初始学习率设多少合适。0.001 是经验默认值,可不代表你的任务也适用。如果初始学习率差了一个数量级,后续不管用多么精致的调度策略,效果都会打折扣。
5.1 LR Range Test 的完整流程
业界常用方法叫 LR Range Test,或者叫 Learning Rate Finder。核心思路:在一个短周期内,让学习率从极小值按指数增长到极大值,同时记录每个学习率下 loss 的表现。画出的曲线上通常有一段明显的"下降区",然后转向上升,这个谷底附近对应的学习率附近,就是合适的初始学习率。
具体操作上我是这样做的:
- 把模型、数据加载和 loss 函数都准备好,跑几个 step 确认没有低级 bug;
- 选取一个非常小的学习率,比如 1e-6 或 1e-7;
- 设定一个较大的终点,比如 1.0 或 10.0,每 step 按指数递增;
- 同时记录 step 对应的学习率和 loss;
- 停止训练,画出 log 坐标下 loss 随学习率变化的曲线;
- 找到 loss 下降最快后转向上升的拐点,将对应的学习率除以 10 作为初始学习率。
如果你的框架里有现成的lr_finder方法,可以直接调用。没有的话自己实现也很快,核心只是在一个 mini-batch 上反复跑前向反向,不做完整训练,几分钟就能拿到曲线。
5.2 拐点选择的经验判断
拐点为什么要除以 10?因为曲线上的最低点通常是瞬时最低,保持在这个点训练容易触碰稳定边界。实际训练中学习率稍微低一点通常更安全,也留出了调度器调整的空间。如果曲线下降段非常陡峭、拐点明显,除以 5 也可以;如果曲线比较平缓,说明模型对学习率不敏感,除以 20 更稳妥。
这个流程做完,你会得到一个专门针对当前任务和当前模型规模的初始学习率,而不是拿别人的默认值硬套。我用了这个方法之后,发现不同数据集、不同模型规模下最优初始学习率可以差出一个量级以上,靠一次设定吃遍所有任务的默认值,确实只能在简单任务上碰运气。
5.3 更自动化的超参搜索方案
LR Range Test 解决的是单次训练初始值的问题。如果你在做实验批量调参,还可以引入网格搜索、随机搜索或者贝叶斯优化。网格搜索在三维以内还可行,维度一高就爆炸了。随机搜索通常比网格更有效率,因为很多超参对结果的影响并不均匀,随机采样更容易覆盖关键区域。
我的建议是:先用 LR Range Test 把学习率范围锁定,把它作为一个先验,再用随机搜索去试探 batch size、权重衰减、调度周期等。别把训练当搜索空间无限大的黑盒,那样算力消耗会非常惊人。能通过一次短实验得到的信息,就不要用一整轮完整训练来验证。
6. 把自动调整组合成一套流程:预热、分阶段与工程落地细节
策略单独看都挺好,但真实训练里很少只用一种。工程经验是把它们组合成一个有节奏的流程,并考虑一些容易被文档忽略的细节。
6.1 预热 + 衰减的经典组合
训练刚开始时,模型的权重接近随机状态,梯度方向的信息量小且噪声大。此时直接上大学习率,容易导致早期震荡甚至发散。预热期先用很小的学习率起步,让模型在一个平坦区域内积累稳定梯度方向,等内部状态(比如 BN 的 running stats、Adam 的动量)稳定之后,再进入正常学习率阶段。
这个组合 checkout 起来非常简单:先定义一个预热阶段的线性或常数学习率,比如前 5% 的总步数用初始学习率的 1/10,然后切到主调度曲线。PyTorch 里的SequentialLR可以拼接两个调度器,也可以手写一个 LambdaLR 来实现任意的拼接函数。
我有一个比较典型的配置(供参考,具体数值要按任务调节):
- 预热:前 500 步,学习率从 1e-5 线性爬到 1e-3;
- 主阶段:CosineAnnealingLR,从 1e-3 经过 10000 步衰减到 1e-5;
- 优化器:AdamW,权重衰减 0.01,beta=(0.9, 0.999)。
这套组合在不少 CV 和 NLP 任务上都能稳定落地,不追求极限效果,但胜在稳。
6.2 微调任务中的分层学习率
迁移学习场景下,预训练模型已经收敛到一个较优的局部解,你只希望它适配目标域的少量语义偏移,而不是把底层特征全部打乱。这时候对所有层统一学习率不太合适:底层通用特征基本可以保持不动,高层的任务相关特征需要较大更新。用分层学习率的思路,让骨干网络使用一个极低学习率(或者干脆冻结),而任务头使用正常学习率,是工程上稳妥的做法。
具体落地时,可以直接给不同参数组配上不同学习率,通常把模型按层分组成几个param_group。更聪明的方式是让学习率按层从低到高指数递增,底层最小,顶层最大——因为越靠近输出的层与目标任务耦合越紧密,越值得加快更新。这个策略在下游任务数据量不大时格外有用,它从本质上抑制了灾难性遗忘。
6.3 分布式训练与混合精度下的学习率差异
换到多卡分布式场景,batch size 变大,梯度估计更稳定,同一学习率下的有效步长相当于变大了。因此常见的做法是随着 batch size 线性或平方根缩放学习率。线性缩放简单直观:batch size 翻倍,学习率翻倍;但你的梯度裁剪阈值、预热长度也最好一起变,否则可能会出现"学习率大了,loss 反而更早爆炸"的情况。
混合精度训练还有一个容易忽略的点:梯度缩放的动态范围会影响较小学习率下的更新精度。如果在 fp16 下训练,学习率调到 1e-6 以下时,梯度可能直接落在 fp16 的最小可表示范围附近,更新效果失真。此时要么用 bf16(如果硬件支持),要么给损失缩放留足余量。这个细节我用 BF16 混合精度时体会很深:fp16 下 loss 卡住不动,排查了半天,最后发现是学习率太小叠加精度截断。
6.4 多阶段实验中的调度策略复盘
组合策略再好用,也要学会复盘。每轮训练结束,保存完整的 log,包括每个 step 的 loss、学习率、梯度范数和参数更新范数。训练完对比不同调度策略的收敛曲线和最终指标,尤其在后面做消融实验时会很有用。
我的经验是,经过几轮对比后,你通常能总结出这个数据集偏好的训练节奏,比如"这个任务适合快速预热 + 长余弦衰减",或者"这个任务用阶梯式衰减比余弦更好"。这些结论是别人给不了你的,只能靠自己的实验记录摸索。不要迷信某个策略在所有任务上都稳定,更不要嫌麻烦跳过这一步。
7. 总结不了的那点事:一个可复用的调参顺序
最后分享一个我自己的操作流程,按这个顺序基本不会出大错:
- 先固定模型结构和数据读取,确认 baseline 能正常跑通;
- 用 LR Range Test 找到一个合理的初始学习率范围;
- 优化器无脑选 AdamW,权重衰减设 0.01 到 0.1 之间;
- 调度器用 Cosine 或带热重启的变体,预热 1% 到 5% 总步数;
- 跑完一轮,记录 loss 曲线、梯度范数和学习率变化;
- 如果 loss 在后期平台期太长,换成 ReduceLROnPlateau 或调整预热长度;
- 收敛效果不满意,再考虑 SGDR、循环学习率或者分层学习率;
- 所有实验保留 log,便于回溯。
这个流程对预训练、微调和从零训练的任务都适用,差别只在具体数值和周期长短。它不能保证你刷到 SOTA,但能保证你的训练过程在大多数情况下是稳定、可控、可诊断的。学习率自动调整从来不是哪个单独技巧的功劳,而是把优化器、调度器、预热、搜索这些机制拼成一个整体,让每一步都发生在合适的时机。