资讯动态

学习率调度的救星:SGDR热重启原理与PyTorch实战

发布时间:2026/10/20 11:52:16 来源:尧图企业网站定制
开头说到学习率调度很多人习惯一上来就配一个阶梯下降或者余弦退火训练到一半发现loss纹丝不动才想起来去调scheduler。我在之前一个图像分类项目里也踩过类似的坑固定学习率1e-3跑到第30个epochloss停在0.35左右上不去了换成指数衰减之后后面几十个epoch几乎看不到任何收益。后来我把调度策略换成SGDR效果立竿见影同一个模型、同样的初始化收敛精度提升了将近2个点。这个经历让我开始认真研究这个来自显式正则化论文里的调度策略。SGDR全称是Stochastic Gradient Descent with Warm Restarts核心就一句话用余弦退火曲线周期性把学习率拉高到初始值再慢慢降下去。它不是什么高深莫测的新算法但它解决了一个很实际的问题——训练后期学习率过低导致模型卡在局部极小点动弹不得。如果你也在为loss下不去、验证集指标长周期不涨而发愁这篇文章值得看完。我会把原理、参数、PyTorch实现、训练曲线观察经验一次性讲透希望你看完能直接用起来。1. 学习率衰减的“死胡同”为什么热重启能打破训练僵局1.1 单调衰减策略的共同短板大多数人训练深度模型的习惯是选一个初始学习率然后让学习率随时间单调递减。阶梯下降是每30个epoch打五折指数衰减是每个epoch乘0.95余弦退火是从初始值平滑降到零附近。这些策略的共同点是——学习率一旦降下去就回不来了。这个设计在当前视角下看是有问题的。想象一个损失曲面里面有一堆局部极小值有的浅、有的深有的平坦、有的陡峭。训练初期学习率高模型在大范围空间里跳动能快速滑进某个看起来不错的“山谷”。但是随着学习率单调缩小模型的活动范围越来越小它只能在这个山谷里做精细调整。问题在于这个山谷可能根本不是全局最优点附近的山谷它只是一个“看起来还行”的坑。我见过很多训练日志验证集loss在某个区间横盘十几个epoch大家第一反应是“降低学习率再看”结果降到1e-6之后loss纹丝不动。这时候其实已经不是梯度方向的问题而是模型压根就被困在一个平坦区域里梯度极小权重更新距离几乎是零。你继续训练再久它也出不来。这种情况靠继续调低学习率是死路。1.2 余弦退火的直觉训练一程休息一程SGDR的思路完全不同。它不追求让学习率一路走低而是主动把学习率从接近零的状态重新拉回初始值再退火到低点。这个过程就叫“热重启”Warm Restart。为什么叫“热”因为它和“冷重启”相对。冷重启是把模型权重重新随机初始化从头再训一遍之前的训练成果全部报废。热重启则保留当前权重只把学习率调高相当于让模型从当前这个位置醒来带着已经学到的特征表示重新获得大步探索的能力。这样既不会丢失前几个周期学会的抽象特征又能让模型跳出当前所在的局部盆地去别的盆地看看。这种“训练一段时间重置学习率再训练”的节奏很像训练运动员的力量周期化练几周大重量降下来休息恢复然后再上重量。每次休息之后重新冲反而能突破之前的平台期。深度学习里loss曲面比运动训练还要复杂一个周期完成后可能处于某个局部极小的底部重置学习率相当于强迫模型站起来开始找下一个更好的极小点。论文里对这种现象还有更理论化的支撑在非凸优化里简单地将学习率置零收敛理论上只能保证进入某个局部极小而无法保证全局性。而带热重启的SGD在凸优化中已经被证明即便函数不满足强凸条件重启机制也能以较大概率收敛到最优点附近。这就给了我们在实践中大胆使用它的底气。2. 余弦退火的数学原理与参数直觉2.1 公式拆解从余弦函数到调度曲线SGDR的学习率计算公式长这样LR(t) eta_min 0.5 * (eta_max - eta_min) * (1 cos(π * t_cur / T_s))看着唬人拆开就很简单。eta_max是初始最大学习率eta_min是每个周期末尾降到的最低学习率。t_cur是当前周期内的步数T_s是当前周期的总步数。cos函数在0到π之间从1单调降到-1所以公式里“1 cos”这一项就从2降到0乘上0.5之后整条曲线从eta_max平滑过渡到eta_min。为什么用余弦而不是线性下降论文作者做过对比余弦退火在图像分类数据集上的表现略优于线性退火。直觉上余弦曲线前半段下降缓慢能较长时间维持较高学习率保留探索能力后半段快速逼近最低值又在最底部附近停留较长时间让模型有足够多的迭代次数在最优区域附近做精细打磨。这个节奏刚好契合深度学习训练“先探索、后收敛”的需求。2.2 T_0、T_mult、eta_min三个旋钮分别控制什么第一个参数是T_0代表第一个周期的长度。T_mult是周期长度的倍增因子T_mult2意味着第一个周期训练20个epoch后重启第二个周期40个epoch第三个80个epoch。这样安排很巧妙随着训练推进模型已经积累了足够成熟的特征重启后可能处于一个更接近好的极小值的起点位置所以不需要那么频繁地重启来寻找新盆地而是给每个周期更多的时间去充分潜行和收敛。eta_min是每个周期末尾学习率的最低值。实践中通常设为初始学习率的1/10到1/100。如果设成0周期末尾权重更新几乎停滞配合热重启虽然也可以但容易让模型在每个周期最后几十个epoch里空转。我个人的经验是eta_min至少给到1e-5让周期末尾还能做有意义的梯度更新。这三点决定了整条学习率曲线的节奏T_0决定多久迎来第一次“重启”T_mult决定之后的重启间隔怎么变化eta_min决定每个周期的“深蹲”幅度。调这三个参数本质上就是在调整训练任务的勘探—开发平衡。3. PyTorch实现与训练循环改造的完整细节3.1 直接使用现成实现CosineAnnealingWarmRestartsPyTorch在torch.optim.lr_scheduler里内置了CosineAnnealingWarmRestarts用法非常直接import torch from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingWarmRestarts(optimizer, T_020, T_mult2, eta_min1e-5) for epoch in range(100): train_one_epoch(model, train_loader, optimizer, criterion) validate(model, val_loader, criterion) if epoch 0: scheduler.step()这里有一个非常容易踩的坑我必须先说出来CosineAnnealingWarmRestarts和PyTorch里其他scheduler的step时机不同。对StepLR或ReduceLROnPlateau常规操作是每个epoch结束后调用step。但这个余弦热重启调度器官方推荐在每个batch之后调用step并且传入当前进度作为参数for epoch in range(100): for batch_idx, (images, labels) in enumerate(train_loader): train_step(...) # 每个batch更新一次学习率 scheduler.step(epoch batch_idx / len(train_loader))这样学习率在每一个迭代步都按照余弦曲线平滑变化和论文中的原始设定一致。如果只在epoch末尾调用学习率在整个epoch内保持不变呈现的是“阶梯状逼近余弦包络”的效果。实际训练中只要batch数不太大两种写法的最终精度差异不算明显但用官方推荐的batch级别调用会更平顺也避免一些边界问题。3.2 手动实现SGDR调度器如果你不想依赖现成API或者想在训练循环里获得更多控制权手动实现也不难。核心就是维护当前周期长度和周期内已走步数import math class SGDRScheduler: def __init__(self, optimizer, T_0, T_mult2, eta_min0.0, eta_maxNone): self.optimizer optimizer self.T_0 T_0 self.T_mult T_mult self.eta_min eta_min self.eta_max eta_max if eta_max is not None else optimizer.param_groups[0][lr] self.T_current T_0 self.t_cur 0 self.cycle_count 0 def get_lr(self): return self.eta_min 0.5 * (self.eta_max - self.eta_min) * ( 1 math.cos(math.pi * self.t_cur / self.T_current) ) def step(self): self.t_cur 1 if self.t_cur self.T_current: self.t_cur 0 self.cycle_count 1 self.T_current self.T_0 * (self.T_mult ** self.cycle_count) lr self.get_lr() for param_group in self.optimizer.param_groups: param_group[lr] lr这段代码的核心逻辑在step方法里每调用一次周期内步数加一走到周期末尾就把周期数加一然后用T_mult重新计算新的周期长度并把t_cur清零。这样写的好处是你可以很清楚地看到“重启”到底发生在哪个节点——就是T_current被重置的那个时刻。手动实现方便加日志、加自定义逻辑比如想在重启点额外做一次验证集评估都可以平滑插入。3.3 训练循环中需要注意的边界情况第一个情况是last_epoch参数。如果你训练到一半中断了想从checkpoint恢复记得把last_epoch设成之前保存的scheduler状态。PyTorch内置的CosineAnnealingWarmRestarts支持scheduler.load_state_dict()手动实现的话最少也要保存T_current和t_cur两个变量否则恢复训练后学习率曲线会从头开始前功尽弃。第二个情况是多GPU或混合精度。数据并行本身不影响学习率调度因为optimizer只有一份。混合精度AMP也不影响学习率调度器只负责修改param_group里的数值。要注意的是如果你在batch级别调用step分布式训练中确保所有卡都做同样的scheduler.step调用不能只在某一张卡上执行。第三个情况是预热阶段。在实践中我会在SGDR之前加入一个5个epoch的线性预热linear warmup先把学习率从0慢慢升到初始值再进行正常的余弦周期。尤其是batch size调得比较大的时候跳过热启动的直接结果就是前几个step的loss大幅震荡部分敏感的网络甚至会在前期发散。预热代码非常简单一个linear schedule即可但它能让热重启策略的稳定性明显提升。4. 调参经验与真实训练效果观察4.1 三个超参数的具体建议T_0的设置我给一个经验值把它设为总训练epoch数的1/5到1/3。比如总计划训100个epochT_020或30比较合适。这样第一个周期有足够时间完成一次完整的“探索—收敛”循环第一次重启来得不会太早导致还没收敛就打断也不会太晚导致被动单调退火占据主导。T_mult2时周期长度依次为20、40、80总长度140个epoch在100个epoch内可以经历三次以上完整重启。eta_min的设置我一般取初始学习率的1/50到1/100。初始学习率0.1eta_min给1e-3到2e-3初始学习率1e-3eta_min给1e-5到2e-5。设太小等于周期末尾学习率归零最后一周期的后半程基本在“原地踏步”清理梯度噪声收益甚微。T_mult要不要用2取决于你总训练时长。如果训练资源紧张只够训几十个epoch用T_mult1等长周期更简单因为周期长度固定不容易出现第二周期还没走完就强制结束的情况。如果训练资源充足用T_mult2能明显提高后期收敛质量——因为长周期允许模型在最后一段拥有足够多的低学习率微调步数。4.2 训练曲线的形态变化从“死水”到“潮汐”用了SGDR之后训练loss曲线最直观的变化就是一次次周期性的锯齿波动。每个周期开始学习率被猛地拉高loss会短暂上跳几个点这是正常的因为模型突然获得了较大的更新步长相当于从精细加工状态切换到粗搜索状态。接着loss快速下滑冲到比上个周期更低的水平然后随着学习率接近周期末尾的低值曲线趋于平缓。我第一次看到这个曲线时挺慌的以为训练出了什么问题后来发现这是SGDR的标准形态。理解这个形态之后评估训练状态的方法就变了别盯着某一次loss上跳较劲要看每个周期底部的loss最低点是否在逐周期下降。如果连续两三个周期的底部都没有降低这说明模型可能已经达到该网络结构在这个数据集上的表达上限再增加周期数收益有限。这里还要补充一个观察小技巧很多训练框架都支持记录每个step的学习率建议训练时把学习率曲线和loss曲线画在同一张图上对齐坐标轴。这样每次loss跳升你都能立刻确认“是不是当期重启点到了”而不是怀疑代码写错了。4.3 需要配合使用的权重选择策略训练结束后选哪个checkpoint做预测对含SGDR的训练流程来说这个问题的答案不是“看最后一个epoch”而应该是“看每个周期末尾”。周期末尾学习率最低训练loss和验证loss往往处于低位这些位置通常就是局部最优解附近。我在实践中一般把每个周期末尾的权重单独保存成snapshot_cycle_1.pth、snapshot_cycle_2.pth这样的文件。如果没有做集成就从这些snapshot里挑验证集指标最高的那一个如果追求极限性能把这些snapshot的预测结果做一个简单平均也就是后面会提到的Snapshot Ensemble获得额外的提升。另外要提醒一点不要在周期中段学习率刚被拉高、loss还在上跳的位置保存checkpoint并用于最终测试。这时模型处于高学习率的探索状态权重本身没有完全收敛单独拿出来效果通常差于周期末尾。除非你的目标是分析训练动态否则保存中段权重意义不大。5. SGDR与其他学习率策略的定位对比5.1 三种常见调度策略的核心差异很多朋友会问SGDR和ReduceLROnPlateau、OneCycle有什么本质区别它们都能在训练后期挽救停滞的loss但解决问题的思路完全不同。ReduceLROnPlateau是“被动响应型”它盯着验证集指标一旦发现几个epoch不涨就把学习率砍半。优点是省心不需要预先设定太多超参数缺点是需要一个可靠的验证指标作为触发器而且每次降低学习率都是一次性的降过头就回不去。SGDR是“主动探索型”它不管当前loss处于什么位置到点就重启。这种主动性在验证集指标出现噪动时尤其有价值——ReduceLROnPlateau可能因为一个偶然的波动误判为平台期SGDR不会受这种短期噪声的影响。OneCycle则是“单次余弦全家桶”它在训练的总步数内先把学习率从低拉到高预热再慢慢降回低值整个训练过程只经历一次这样的弧线。它追求的是在大学习率阶段快速跨越非凸区域的障碍然后用后半段缓慢降学习率收敛到最优点。SGDR的区别在于它把这套过程重复多次因此适合训练轮数较长的情况OneCycle在总轮数较短时更有优势因为它在单一周期内把高学习率阶段幂次安排得很充沛。5.2 Snapshot Ensemble热重启的额外红利SGDR论文里专门提到热重启策略有一个额外的“福利”每个周期末尾的权重可以看作从不同局部最优区域采样出的模型把这些模型做集成就叫Snapshot Ensemble。这背后的逻辑很直接普通集成要训练多个独立的模型成本高。SGDR在单次训练过程中天然生成了若干个差异化的snapshot——它们都来自同一个训练paste但由于每次重启后的高学习率阶段把参数带到了不同的局部盆地模型之间具有足够的多样性。把这些snapshot的预测概率做平均往往能比单独最好的模型再提升1到2个百分点的准确率而几乎不增加训练成本。我在实际项目里试过一次一个100个epoch的SGDR训练T_020T_mult2保存最后三个周期末尾的权重做预测平均。单独最好的一个snapshot在验证集上有94.2%的准确率三个snapshot平均后到了94.8%。训练成本完全一样只多了几次前向推理收益已经很可观。5.3 和OneCycle冲高学习率的思路差异有人可能会问SGDR既然每个周期都要把学习率拉高那不如像OneCycle那样在周期开始时用很高的学习率“冲”一下这个想法部分成立但两者的高学习率阶段目的不同。OneCycle的高学习率是为了在训练初期快速到达一个良好的参数区域然后缓慢收敛。SGDR每次重启时的高学习率更像是在已经到达某个局部盆地之后主动制造一次“位移”让参数有机会从当前盆地翻越障碍走向另一个盆地。因此SGDR初始阶段的最高学习率不宜设得过大——不超过普通固定学习率的2倍就够了。设太大重启时参数位移过大会把之前学到的特征全部冲乱模型退化到一个比较差的状态反而得不偿失。如果还想继续压榨性能可以把SGDR和权重平均SWA结合在每个周期末尾保存权重最后把这些权重按算术平均合并成一个最终模型。SWA论文里给的思想是简单平均局部最优解附近的权重可以获得更平缓的损失曲面泛化性更好。SGDR产出的多个周期末尾权重因为落在不同的局部盆地平均后往往比单独最后周期权重更稳。不过要注意SWA平均的权重要求它们来自同一学习率的晚期阶段把学习率差异过大的snapshot混进来平均会引入偏差所以我一般只取最后2到3个周期的snapshot做平均。最后分享一个实操层面的小技巧。有一次训练任务中断了我需要从checkpoint恢复并把scheduler状态一起恢复当时因为手动实现的调度器没有保存t_cur恢复后学习率直接回到周期起点整个训练节奏被打乱损失了不少时间。后来我养成了一个习惯每次保存model checkpoint时把当前的epoch、T_current、t_cur、optimizer state一并写入一个json文件里恢复时先读取scheduler状态再重建训练循环。这套“状态五件套”的保存逻辑看着不起眼但在长周期训练里是救命级别的细节。SGDR本身不复杂把它用好靠的就是这些训练工程上的严谨。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑