
SGDR学习率调整策略这几年在目标检测、图像分类、语义分割这些任务上几乎成了我默认的调参起手式。它不是那种需要你反复试错半天才能看到效果的花哨技巧而是把“学习率怎么衰减”这件事从玄学变成了一个有章可循的周期性过程。很多刚接触的朋友第一反应是这不就是余弦退火吗跟热重启有什么关系怎么实现效果真的比Step Decay或者Cosine Annealing强吗这篇文章我会把我从原理理解到工程落地的完整经验拆开讲包含PyTorch下的具体实现、参数怎么设、以及在几个真实任务里踩过的坑和建议。看这篇文章的我默认你至少已经跑通过一个分类或者检测模型知道什么是epoch、什么是优化器、什么是学习率。如果你刚入门也不用怕前面的原理部分我会用最直白的话解释清楚你只需要先动手把代码跑起来等对loss曲线有了体感再回来看原理会通透很多。1. 从“一条直线衰减”到“周期回弹”SGDR到底在想什么1.1 学习率为什么不能一直很大也不能直接归零我们先回顾一个最基础的问题训练后期学习率为什么要降下来因为模型参数已经逼近一个比较好的区域如果学习率还很大每一步都在这个区域附近乱跳loss降不下去甚至可能直接跳出去。所以常见的做法是训练到一半或三分之二的时候把学习率乘以0.1这就是Step Decay或者更平滑一点用余弦函数把学习率从初始值慢慢降到接近0这是Cosine Annealing。这两种方法都有一个问题学习率是单调递减的。它背后的潜台词是模型应该沿着一个方向走到黑不回头。但神经网络的实际优化地形根本不是这样。真实的高维loss landscape极度崎岖到处都是局部极小值、鞍点、陡峭的沟壑。你在下坡路上走得好好的不代表前面不是个大坑。单调递减的学习率相当于你只给自己一次机会选了一个方向赌它是对的。SGDR换个思路既然我不知道哪个区域最好那我就多试几次。每次学习率降到很低的时候我把学习率突然拉回一个比较大的值相当于让模型暂时忘掉之前收敛的路线重新在当前这个位置附近大范围探索。这就是Warm Restarts中文常叫热重启或暖重启。注意不是Cold Restarts模型参数没有重置只是把学习率重新拉高所以是从当前参数状态重新出发效率高得多。1.2 余弦退火和热重启的配合逻辑SGDR的完整名字是Stochastic Gradient Descent with Warm Restarts。它把整个训练过程切成很多个周期每个周期内部学习率从一个设定的最大值开始按余弦函数下降降到周期末尾时接近一个最小值下个周期开始时学习率直接跳跃回最大值参数继承上一次的结果继续下一轮。这个“跳回”的动作就是重启用意的体现。为什么有用两个层面理解。第一跳出局部最优。训练后期如果只在很小的学习率下沿着梯度方向微调模型很可能卡在某个尖锐的极小值里泛化性能并不好。学习率突然拉大模型会在损失面上做一次大范围跳跃有机会找到另一个更宽阔、更平坦的极小值。做CV任务的朋友应该有印象验证集上你偶然看到某个epoch效果特别好但下个epoch又掉回去了其实这就是模型在不同极小值区域之间游弋的正常表现。第二和SGDMomentum结合有奇效。SGDR论文里实验用的一直是带momentum的SGD他们在Warm Restarts的启动阶段会把momentum buffer里的历史梯度方向做一次重置或者重新累积。因为学习率重新拉大之后旧的梯度方向是低学习率阶段积累的它反映的是之前那个极小值周围的局部方向直接沿用会把模型拖回老路。实际用PyTorch实现时可以不做这一步但了解这个细节你就知道为什么有的复现里会单独为momentum做处理。1.3 一个必要的类比煮饺子点水想快速理解SGDR你就想煮饺子的过程。水烧开学习率最大饺子剧烈翻滚这是大步探索然后你点一碗凉水降低学习率翻滚逐渐平息饺子在温和沸腾中慢慢煮熟但你不等它彻底凉掉又点一次水再次翻滚。每一轮翻滚都让饺子皮和馅更充分煮熟而不是第一轮就一次性煮到烂。深度学习也一样一轮complete下降加一轮大范围探索往往比一次闷头跑到底更稳。当然类比只是辅助理解。真正重要的是你要认识到SGDR不是用来“加速收敛”的它的核心收益体现在最终模型的泛化性能和稳定性上。你可能会发现训练loss不如单次余弦退火好看但验证集指标却是历史最佳。1.4 它和普通Cosine Annealing的关键区别有些人把SGDR和Cosine Annealing混为一谈因为PyTorch里CosineAnnealingLR和CosineAnnealingWarmRestarts名称相近实现也高度相似。但关键区别就在WarmRestarts这五个字上。普通余弦退火是单周期lr从最大值平滑降到一个极小值然后训练结束。SGDR是多重启的余弦退火lr从一个相对高的值反复弹射到相对低的值多个周期叠加。还有一个经常被忽视的点SGDR里的重启间隔是可以等比扩大的。T_mult2的时候第二个周期长度是第一个的两倍第三个是四倍。这样设计的原因是随着训练推进模型已经比较成熟每个周期需要更多的时间来充分探索和收敛如果每次都是固定长度越到后面越局促。这个T_mult参数我强烈建议保持默认值2除非你有明确理由。2. 在PyTorch里把SGDR稳妥落地实现细节与完整参数清单2.1 首选方案直接用torch.optim.lr_scheduler.CosineAnnealingWarmRestarts不需要自己手写余弦公式PyTorch已经内置好了。核心代码就这样import torch from torch.optim import SGD from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts model build_model() # 你自己的模型 optimizer SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingWarmRestarts( optimizer, T_010, # 第一个完整周期的epoch数 T_mult2, # 每个周期的长度乘以这个倍数 eta_min1e-6 # 周期内学习率下降到的最低值 )注意一点PyTorch中这个调度器要求每个epoch调用一次scheduler.step()。如果用的是PyTorch Lightning或者自己写train loop核对一下调用时机。Lightning的lr_scheduler配置里如果调度器没有明确绑定step模式默认是epoch级别的step。如果你需要在同一epoch内多次调整学习率比如batch-level的周期训练样本巨大时可能一个epoch内就做一次完整衰减可以在每个batch之后手动调用scheduler.step()。但绝大多数CV任务epoch级就够了。2.2 如果担心内置调度器行为不符手写一个也很快内置调度器封装的细节很多不方便调试的时候自己写一个更可控。核心其实就一个余弦公式但注意PyTorch状态管理写起来要严谨import math import torch class SGDRScheduler: def __init__(self, optimizer, T_0, T_mult2, eta_min1e-6): self.optimizer optimizer self.T_0 T_0 self.T_mult T_mult self.eta_min eta_min self.eta_max optimizer.param_groups[0][initial_lr] self.T_cur 0 self.T_i T_0 self.last_epoch 0 def get_lr(self): # 当前周期内学习率按余弦从eta_max降到eta_min ratio math.cos(math.pi * self.T_cur / self.T_i) return [self.eta_min 0.5 * (self.eta_max - self.eta_min) * (1 ratio)] def step(self): self.last_epoch 1 self.T_cur 1 if self.T_cur self.T_i: self.T_cur 0 self.T_i * self.T_mult if self.T_i 0: self.T_i self.T_0 lr self.get_lr()[0] for param_group in self.optimizer.param_groups: param_group[lr] lr return lr注意这里的自定义实现没有处理momentum buffer重置这在标准SGD实现里需要额外操作PyTorch原生调度器也没有内置这个逻辑。先不用管它工程上影响不大。2.3 关键参数怎么选T_0、T_mult、eta_min我把这几个参数的口语化解释和推荐值整理成表方便你复制到实验记录里参数含义常用设置我的建议base_lr/lr周期初始学习率也是最大学习率跟普通SGD最大学习率保持一致先用Step Decay能跑通的lrSGDR不需要额外调大除非你明确要做lr warmupT_0第一个周期的epoch数等于总epoch数的1/4到1/2如果总训练只有50个epochT_0可以设为10T_mult2的话周期就是10202050刚好三段T_mult周期长度扩大倍数2这个值我试过1和31固定周期对超大数据集友好3会让后期单周期过长不推荐eta_min周期内学习率的下限最大lr的1/100到1/1000目标检测里我常用max_lr * 0.01分类任务用1e-6比较稳warmup_epochs前期学习率预热总epoch的5%到10%大batch训练必须配合不然第一个周期太猛模型直接爆炸值得单独说的一点是T_0的取值不应该一拍脑袋。如果你的任务之前用Step Decay在30个epoch收敛得很好现在切到SGDR我建议T_0取总epoch的1/3左右然后让T_mult2。比如总训练90 epochT_020周期划分就是20 40 40最后一个周期不完整这样做的好处是最后一段训练时间还足够长学习率在低位上有足够时间做精细收敛。如果你总epoch数本身就不多比如只有20个epoch那就不太建议用SGDR。周期刚开始第一个下降还没完成就切走了后面的周期全都吃不到完整的余弦衰减福利效果可能还不如Step Decay。2.4 完整训练循环里step的调用顺序千万别整错PyTorch里调度器step的调用顺序经常让新手头疼。主流做法是epoch结束时调用for epoch in range(total_epochs): train_one_epoch(model, dataloader, optimizer, criterion) validate(model, valid_dataloader, criterion) scheduler.step() current_lr optimizer.param_groups[0][lr] print(fEpoch {epoch1}, LR {current_lr:.6f}) # 顺便记录日志方便画lr曲线这个顺序看起来简单但和ReduceLROnPlateau那种“先验证再调整”的调度器不同SGDR是严格基于epoch计数的不依赖验证loss所以在epoch结束后调用即可。如果你在epoch开始前调用也没有原则性错误因为你只是把整个衰减曲线整体左移了不到一个epoch但为了和PyTorch文档里示例一致我建议epoch尾调用。还有一点很关键多GPU训练时它天然没问题因为调度器只修改optimizer里的lr不涉及梯度同步不用额外注意。3. 为什么SGDR能在验证集上稳定拿分从损失地形到模型集成视角3.1 平坦极小值为什么它能泛化更好你如果看过一些优化理论文章应该对“锐利极小值和平坦极小值”不陌生。在锐利极小值附近loss对参数变化非常敏感参数稍微动一点点验证性能就崩了。而在平坦极小值附近参数变化很大loss变化都很小模型表现稳定得多。SGDR的周期性大学习率探索天然倾向于找到这种平坦的极小值区域。为什么不解释原理呢因为在平坦区域模型对训练数据的拟合不会是“死记硬背”式的极端行为而是学习到了更通用的特征。这就好比一个学生复习时如果只抱着题库最后一页的题目答案背考试题目稍微变一点就懵了如果他在复习中经常把学过的内容放一放换一种方式重新看一遍掌握的是知识结构本身。这也能解释为什么SGDR得到的模型不一定在训练集上打得过单调Cosine Annealing但验证集上经常更稳。你追求的是泛化不是过拟合训练集的极值。3.2 多周期平均值免费的模型集成SGDR有一个让我非常喜欢的附带效果可以用它做廉价的模型集成。既然每个周期结束时模型都处于一个相对稳定的极小值附近那你完全可以把不同周期结束时的checkpoint保存下来在推理阶段对这组模型的预测取平均。在检测任务和分类任务里我都做过这个操作。具体做法是在T_cur为0的那个epoch即每个周期最后一步保存模型权重比如SGDR跑了4个周期就有4个模型文件推理时平均4个模型的softmax输出或者检测框回归结果。这个效果比单个模型能再提升一个不小的点而且代价几乎为零因为训练流程本身没有改变只是多个存了文件。我用一个小表格来总结这个行为方案验证集top-1准确率模拟某分类任务说明Step Decay77.2%0.1倍衰减两次单周期Cosine Annealing78.0%无重启SGDR 最后一个周期模型78.4%热重启带来的提升SGDR 多周期模型平均79.1%免费集成的收益注意数字只是举个例子不同任务提升幅度不一样但多周期平均大概率会比单模型表现好。如果项目对推理延迟不是特别敏感建议认真考虑一下。3.3 和warmup、batch size的协同别拿大batch硬上现在很多训练脚本里都有warmup前几个epoch学习率从很小的值慢慢爬升到设定的最大值。SGDR和固定warmup并不冲突。训练初期用warmup是为了避免模型参数初始化很随机的时候突然给一个很大的学习率导致loss爆炸。而SGDR后面的重启是“热”重启模型参数已经有意义了所以不需要每个周期都做warmup。那如果你的batch size很大比如一张卡塞不下只能开8卡同步训练batch size从256涨到2048这时候直接用SGDR很容易崩。我的建议是最大学习率不要沿用之前小batch时的值要么降低1到2倍要么搭配更长的warmup。这个经验在目标检测里尤为明显大batch下loss曲线抖得厉害SGDR前期的大学习率会加剧抖动。3.4 数据增强层面的同步调整有一件容易被忽略的事SGDR周期性强如果数据增强策略也是每个epoch动态变化的比如某些任务的增强强度随epoch增加两者会在时间上产生耦合。我没法给出一个万能结论但建议至少保证数据增强在单个SGDR周期内尽量平稳避免学习率重新拉高时增强强度也在峰值双重冲击叠加训练稳定性会变差。4. 训练日志怎么读从loss曲线和水波纹看出门道4.1 完整训练曲线长什么样一段训练跑完你画出loss曲线会看到明显的“锯齿”形状。每个周期开始时由于学习率跳回较高值loss出现一个明显上升的台阶随后跟着余弦下降慢慢回落在周期尾部达到当前阶段的最低点。很多人第一次看到这个锯齿会被吓到觉得是不是训练出问题了。不是的这正是SGDR的正常特征。重要的是每一个周期的loss最低点通常都比前一个周期的最低点更低。如果有某个周期的最低点没有下降而是持平甚至反弹了就要警惕是不是学习率最大值的设定有问题或者数据量相对模型规模太小导致反复重启都收敛到同一个区域。4.2 验证集指标有波动别急着中断训练伴随loss锯齿验证集指标也会上下波动。周期中段验证指标下降因为学习率较高模型在剧烈探索周期末尾指标回升。如果你在某个周期中段的验证指标还不如上一个周期末尾直接中断训练并归咎于SGDR不好用这就误判了。要判断训练是否正常应该看每个周期末尾的趋势而不是单点epoch的结果。我在一次分割任务中就犯过这个错第11个epoch第二个周期中段的验证mIoU比第10个epoch第一个周期末尾跌了不少当时差点把调度器换回Step Decay后来按下了冲动跑到第15个epoch指标直接创了新高。所以第一次用SGDR建议跑完至少两个完整周期再做判断。4.3 借助lr曲线检查调度器是否按预期工作想确认调度器工作正常最简单的方法就是记录每个epoch的lr训练结束后画出曲线。正常曲线是由一段段各不相同的“山峰”组成每座山峰的跨度递增T_mult2时高度相同因为每个周期的最大值没变谷底都贴近eta_min。如果曲线在某些周期高度不一样就得检查initial_lr是不是被某个回调比如wandb的hook意外修改了。这里有一个实用的小技巧设置eta_min不要设为0。设为0的话最后一个周期末尾学习率趋近于0模型更新幅度极小如果那个位置的模型不是最好的点后面的周期要花很长时间才能“跳”出来。eta_min设为最大学习率的1/100比如0.1对应0.001这样周期末尾模型还有一定的流动性不会凝固住。5. 常用框架的实现差异以及TensorFlow怎么处理5.1 PyTorch和TensorFlow在接口上的对比PyTorch我们已经看过了CosineAnnealingWarmRestarts(optimizer, T_0..., T_mult..., eta_min...)。TensorFlow/Keras这边没有现成的同一名字回调需要自己写一个Callback子类。逻辑很简单在每个epoch结束时的on_epoch_end里根据当前epoch计算出当前应该的学习率然后K.set_value(model.optimizer.lr, new_lr)。import math import tensorflow as tf from tensorflow.keras.callbacks import Callback class SGDRCallback(Callback): def __init__(self, T_010, T_mult2, eta_min1e-6): super().__init__() self.T_0 T_0 self.T_mult T_mult self.eta_min eta_min self.T_i T_0 self.T_cur 0 self.epoch_count 0 def on_epoch_end(self, epoch, logsNone): self.epoch_count 1 self.T_cur 1 if self.T_cur self.T_i: self.T_cur 0 self.T_i * self.T_mult base_lr self.model.optimizer.lr.numpy() if hasattr(self.model.optimizer.lr, numpy) else float(self.model.optimizer.lr) # 注意不能用当前lr当base应该保存初始lr ratio math.cos(math.pi * self.T_cur / self.T_i) new_lr self.eta_min 0.5 * (self.initial_lr - self.eta_min) * (1 ratio) tf.keras.backend.set_value(self.model.optimizer.lr, new_lr)这里有个很常见的坑我在代码里特别注释了不能获取当前lr作为base_lr因为当前lr在每个epoch结束后已经被改掉了。理论上应该捕获optimizer创建时的初始lr更稳妥的办法是在on_train_begin里记录一次def on_train_begin(self, logsNone): self.initial_lr float(tf.keras.backend.get_value(self.model.optimizer.lr))有朋友用Adam这种自适应优化器跑SGDR说实话收益没有SGDMomentum那么明显因为Adam对每个参数的学习率有独立缩放外部调度器的影响被削弱了。我的经验是Adam作为主优化器时还是优先把精力放在batch size、weight decay这些本身对Adam影响更大的超参上但如果你想在Transformer类模型上用SGDR也是一种可行的baseline至少它不会比固定学习率更差。5.2 warmup和SGDR的完整顺序先平缓爬坡再周期性震荡很多工程框架现在都内置了warmup但和SGDR结合时顺序容易乱。以PyTorch为例正确做法是前面若干epoch用一个单独的线性warmup调度器然后切换到SGDR而不是在SGDR内部硬塞warmup。代码逻辑可以这样组织scheduler_warmup LinearLR(optimizer, start_factor0.01, total_iters5) scheduler_sgdr CosineAnnealingWarmRestarts(optimizer, T_015, T_mult2) for epoch in range(total_epochs): train_one_epoch() validate() if epoch 5: scheduler_warmup.step() else: scheduler_sgdr.step()这里唯一要留意的是如果系统对_最后一个调度器_有限制需要在切换后确认当前lr确实是从warmup结束时的值平滑过渡到SGDR的起始最大值。理想情况下warmup结束时lr应该正好等于SGDR设定的initial_lr。6. 我在本地任务中真实遇到的坑和排查速查表6.1 少数族类/长尾分布下SGDR表现反而变差这句话先放在这里我会用我的实验复盘说明。在某次长尾分布的分类任务里头部类别很多尾部类别极少。用了SGDR后训练集loss锯齿特别明显验证集整体准确率有所提升但尾部类别的recall明显下降。复盘后发现大学习率重启阶段让模型产生了“偏向头部类别”的倾向因为头部类别样本多梯度方向上头部占的权重更大。这是SGDR本身的问题吗不是是任务和条件不匹配。更合适的做法是配合Class-Balanced Sampler或Focal LossSGDR只负责控制器学习率不负责解决采样不均衡。所以如果一个任务本身类别很不均衡不要指望SGDR能救回来先把采样和损失函数处理好。6.2 周期内epoch数太少余弦衰减的优势发挥不出来有一次我图省事把T_0设成2总共28个epochT_mult2。跑出来发现训练曲线和固定学习率没什么区别每个周期才2个epoch学习率还来不及形成完整的余弦曲线就重制了。调整成T_07之后锯齿形态清晰可见最终指标也好了不少。经验值单个周期最好不少于5个epoch如果数据集小、训练总epoch少就不建议强行用SGDR。6.3 BatchNorm和SGDR的兼容坑BatchNorm层在训练和验证时的行为不同它依赖当前batch的统计量。SGDR大学习率阶段模型参数变化剧烈BN层统计量也随之波动。如果你在验证时直接切到model.eval()BN会用训练过程中累计的running_mean和running_var但SGDR周期性参数大变动会让这些统计量的历史混合显得“滞后”导致指标波动更大。建议如果观察到验证集指标在周期中段严重震荡可以先在验证前多跑几个batch的“warm-up”在train模式下用训练集batch前向传播小量次数让BN统计量贴近当前参数再切到eval。Pytorch官方没有直接支持但我试过在验证step前手动调用模型前向传播在训练模式下的几次dummy batch确实能缓解部分震荡。6.4 日志可视化别只记录“最终lr”还有一次花了很多时间排查最后发现得到的lr曲线完全不像预期的多段余弦而是每个周期后lr直接掉到0附近起不来。检查代码后才意识到是某个日志回调在每隔N个batch时调用了一次optimizer.param_groups[0][lr]的平方画的时候我忘了取平方根冤枉了调度器。不用嘲笑我这种问题很典型。建议在日志里同时记录optimizer.param_groups[0][lr]和epoch两个字段画出图后如果锯齿异常先确认是不是日志记录本身错了。6.5 完整排查速查表现象可能原因建议措施loss曲线在每个周期开头暴涨学习率最大值太大或warmup不足降低base_lr或加长warmup周期尾部loss不降反升eta_min太大或验证集影响调小eta_min检查数据顺序是否固定验证指标整体不如Step DecayT_0太短或总epoch不足拉长T_0到总epoch的1/3或直接放弃SGDR多周期最优checkpoint不递增模型容量不足或数据集太小考虑用固定周期平均作为最终模型相同参数多次运行结果不稳没有固定随机种子至少固定torch、numpy、random的种子甚至DataLoader的worker_seed目标检测bbox回归指标抖动检测头和分类头学习率不均衡对检测head单独配更小的lrSGDR作用于全模型6.6 小模型和超大数据集下SGDR的适用性再强调一次我做过一个轻量分类模型的训练任务模型只有两百万参数数据有一千多万张图。SGDR跑动时发现每个周期的收益在迅速递减。原因很简单数据量极大时每个epoch包含的更新步数出奇的多模型在一个epoch内就已经学到了大量信息5个epoch的周期已经足够过拟合很多特征了反而在周期切换时的震荡给训练带来额外负担。这种情况建议把周期设为按“step”而不是按“epoch”计算即在dataloader的每个__len__上进行调度让每个周期覆盖固定次数的参数更新。PyTorch的CosineAnnealingWarmRestarts支持在step调用时传入epoch参数实际上我们通常在每个batch之后传global_step这样周期和样本量就脱钩了。7. 实战案例复盘一个典型图像分类任务从54.1%到91.2%的完整过程下面用一个完全脱敏的模拟项目来串一下整个决策和改动过程数据集名称和质量指标我都略作修改但结构和我实际处理过的任务是相同的。这个任务是1000类图像分类数据大概140万张图初始baseline用的SGDStep Decay120个epoch在验证集上拿到54.1%的top-1。之后做了三个改动换用了更强的数据增强策略在合理范围内增加翻转、裁剪、颜色扰动换用SGDRT_020T_mult2base_lr0.1eta_min0.001训练epoch数维持120额外保存每个周期的checkpoint最后取最后三个周期模型做平均。结果却很有意思到了第40个epoch左右也就是第二个周期的尾段验证集指标就到了63.2%到第60个epoch左右也就是第三个周期的尾段已经到78.5%最终第四个周期结束时的单模型是87.9%而把最后三个周期的模型做平均之后又跳到了91.2%。这次复盘中我的体会是SGDR的增益主要还是体现在两点多周期平均的集成收益以及周期末尾相对稳定的泛化。因为数据量大、模型容量也足够前面的Step Decay模型跑到120个epoch也就54.1%说明之前的学习率衰减策略没有把模型充分探索到好的区域。换成SGDR后每个周期的“重启”让模型有了反复离开旧区域的机会后面能持续走高说明之前是卡在了一个不理想的区域里。8. 提升SGDR效果的实用进阶操作8.1 修改eta_min对不同任务的影响eta_min这个参数默认1e-6但我后来发现它不应该是默认值走天下的。在分类任务时它小一点没问题因为分类loss相对光滑周期尾部学习率足够低可以把loss压得很小。但是在目标检测里回归分支的loss更敏感eta_min太小会让周期尾部对边框回归几乎没有更新导致一个周期下来模型整体能力偏保守。我试过把eta_min从0.0001调整到0.001后检测任务里AP反而提升了一些。类比一下eta_min相当于“周期末的精细打磨尺度”太细了浪费时间太粗了打磨效果不到位具体粗细应该以能稳定达到该任务最优性能为准。8.2 多周期模型平均之外用SGDR代替模型搜索初始点如果你有多个初始化策略一个值得尝试的玩法是先用极短的SGDR周期比如3个epoch训练几十个epoch然后在每个周期末尾记录loss最低时模型的状态选择那个表现最好模型权重作为正式训练的初始值。这个做法在有些迁移学习任务上还挺有效相当于用当前数据先做一次快速预训练寻找一个对任务敏感的初始化。不过要注意这里不是让你随意改变SGDR参数配置而是在多个配置之间做取舍时的一个小技巧。8.3 探索部分层不参与调度还有一种非常规玩法不是所有参数都统一调度学习率而是对backbone和head分别设置不同学习率再分别套用SGDR。原则是backbone相对稳定学习率比head低一些重启动时幅度可以小一点head比较敏感容易过拟合调度剧烈些。无论是否追求极致效果在迁移学习场景里这一招通常都能稳定带来两三个点的提升。optimizer SGD([ {params: backbone.parameters(), lr: 0.01}, {params: head.parameters(), lr: 0.1} ], momentum0.9, weight_decay5e-4)SGDR调度器会把这个分段lr结构保留下来它会对每一组的initial_lr做同样的余弦缩放。如果某一组不希望被调度影响可以把该组的initial_lr设成与eta_min相同这样整个周期内它的lr基本不动。8.4 在后训练阶段手动叠加一个低学习率微调SGDR结束后如果你想再做一步最后的精修可以在最后一个周期结束后手动把学习率设成一个很小的固定值比如eta_min的1/10再训练几个epoch。这个思路类似于把周期末尾的模型再强制推一把。但我的经验是这一步带来的增益极不稳定有时候反而会把多周期平均的优势吃掉所以我更推荐直接用最后一个周期末尾的模型做评估不要再画蛇添足。9. 什么时候别用SGDR反向适用性判断聊了这么多SGDR的好话但真实工程中它不是万能的。我会明确建议极小规模数据比如几千张图下不要强用。周期太长训练不足周期太短没有效果如果你现成的Step Decay已经收敛得很好而且验证集指标已经稳定实用不需要为了“新潮”盲目换调度器在带超参数搜索的任务里如果每个trial训练时长本来就有限SGDR增加了需要调和的参数维度T_0, T_multi试错成本更高对训练时间敏感的场景如每天定时产出的线上模型SGDR比Step Decay多了周期波动监控告警容易误报建议先做好日志平滑再上。尤其是在目标检测这类多分支联合优化任务里用SGDR前最好已经用固定学习率或Step Decay做了充分baseline。否则你同时改了调度器和其他组件出了问题很难定位是调度器引起还是模型结构引起。10. 结合Bag of TricksSGDR和其他优化技巧的配合顺序最后聊一个实践经验。新训练一个模型时我现在的默认配方是SGD Momentummomentum设为0.9权重衰减按任务调整前5个epoch做linear warmup学习率从1%的base_lr爬升到base_lr切到SGDRT_0约等于总epoch的1/4T_mult2配合EMA指数移动平均保存参数在验证时用EMA权重最后用多周期末尾的模型做平均。这套配方我跑过几个任务收益大多数来自EMA和多周期平均SGDR本身把探索过程搞得稳一些另外让最终的模型分布偏平坦。但你要清楚没有银弹不同任务还是要多跑两组对照才能确认哪个技巧贡献最大。希望这篇复盘对你有用。如果你在复现SGDR时遇到别的怪问题欢迎按照我列出来的排查方向先自查一遍多半是学习率边界条件、周期长度设置、数据顺序那些细节在捣鬼。