恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SGDR学习率热重启策略:原理、PyTorch实现与调参实战
首页
资讯中心
/
SGDR学习率热重启策略:原理、PyTorch实现与调参实战
SGDR学习率热重启策略:原理、PyTorch实现与调参实战
发布时间:2026/10/20 11:51:30
先说个可能很多人都有过的经历开局辛辛苦苦调出一个基础学习率训练了二三十个epoch之后loss卡在0.7死活不动然后你试着把学习率从0.001改成0.0001跑了一百多轮终于降到0.6。这时候你会不会想如果有个机制能让你在训练过程中自动做这种事甚至比手动静默降两次学习率做得更聪明一点该多好。SGDR学习率调整策略就是干这个的。SGDR全称是Stochastic Gradient Descent with Warm Restarts2017年由Ilya Loshchilov和Frank Hutter提出思路其实很朴素——在SGD训练过程中周期性把学习率调大然后让它按余弦方式慢慢衰减衰减到底后再突然拉回高位再来一轮。这种“热重启”机制乍一看有点违背直觉训练到后期不是应该越学越精细吗为什么还要把学习率瞬间拉大会不会直接把已经收敛的权重打飞我自己第一次看到这个策略时也是这个疑问。真正把它跑起来之后才明白SGDR不是一个简单的“分段减学习率”工具而是把“退火”和“重启”结合起来的一种整体训练方法论。下面我尽量把原理、代码、参数选择和我踩过的坑一次讲清楚。1. 学习率为什么是个“老大难”——从SGD的视角看问题1.1 学习率太高太低都是灾难在讨论SGDR之前有必要先把基础对齐。深度学习训练本质上是求解一个高维非凸优化问题最常用的SGD每次迭代用一小批数据的梯度和一个固定的学习率 η 来更新权重θ_{t1} θ_t − η · ∇L_B(θ_t)这里面 η 的选择直接决定了训练行为η 太大损失可能在最优解附近来回震荡甚至发散η 太小前期收敛非常缓慢后期又容易困在尖锐的极小值区域。很多人在调参时都经历过“学习率大了loss飞上天小了半天不动”的死循环。实践中最常见的做法是“衰减策略”每训练若干个epoch把学习率乘以一个小于1的因子比如每30轮衰减一半或者每50轮衰减到0.1倍。这叫step decay简单有效。但它的主要问题是你需要事先确定衰减节点在哪而训练的不确定性很大——有时候loss在epoch 35才开始真正下降结果你的衰减点在epoch 30就到了直接把下降趋势压住了有时候loss在前20轮就降到了一定水平衰减却迟迟不来白白浪费算力。1.2 从“单调递减”到“周期性重启”的转变针对这个痛点研究者提出了很多方案指数衰减、余弦退火cosine annealing、周期性学习率cyclical learning rate、基于验证集指标触发的ReduceLROnPlateau等。这些方法都有一个共同思想学习率随训练进程变得越来越小只是“怎么变小”的策略不同。SGDR完全换了一个思路。它不追求让学习率一直降下去而是允许学习率在训练过程中周期性回升。每一次学习率从“高位”跌到“低位”的过程叫一个退火周期当学习率重新回到高位时就相当于一次“热重启”。这里“热”的关键在于模型权重并没有被重置它的初始条件已经是上一个周期结束时收敛到的状态所以叫热重启而不是冷启动。1.3 直觉类比把训练想象成爬山我把这个策略讲给朋友听时常用一个爬山类比。普通的学习率衰减像是你从山脚往上爬越靠近山顶步子越细CosineAnnealing像是你给自己规定了一个体力曲线先冲得快后冲得慢而SGDR像是你爬了一段之后累了走小碎步慢慢挪突然感觉到了一个平台休息一下然后大步往上窜一段——看起来是往回退了半步实际上是换了一条路继续往高处走。这种“后退半步”反而能让你走到更远的地方背后的数学依据不是玄学在非凸损失面中困住你的往往是周围都是“高原”的鞍点或平坦区域小学习率根本无法挣脱而一次高学习率的“冲刺”可以把你推出当前盆地在另一个更深的盆地继续精细收敛。2. SGDR原理拆解——余弦退火与热重启的完整机制2.1 核心公式逐项拆开SGDR没有引入复杂的数学工具它的学习率调度规则可以直接写成η_t η_min (η_max − η_min) · (1 cos(π · T_cur / T_i)) / 2这个公式在每一个周期i内部生效η_max周期开始时的最大学习率也就是重启后重新放大到的峰值。η_min周期结束时的最小学习率不一定是0可以设成一个很小的值。T_i当前周期的总迭代次数或总epoch数。T_cur当前已经在周期内走完的迭代次数或epoch数。当T_cur0时cos(0)1η_t(η_maxη_min)/2·1?不对仔细算一下η_t η_min (η_max−η_min)·(11)/2 η_max学习率最大。当T_curT_i时cos(π)−1η_t η_min (η_max−η_min)·(1−1)/2 η_min学习率降到最小。中间过程是余弦函数的平滑下降先缓后急再缓而不是线性从高到低。余弦衰减的“慢-快-慢”节奏非常有意思。训练初期和末期学习率的下降幅度都较缓中期下降最快。初期缓慢能让权重充分探索在接近周期末尾时学习率已经很小权重在局部区域精细打磨为下一次重启准备一个“好底子”而周期中段快速下降则避免了在中等学习率区间浪费太多训练时间。2.2 周期长度怎么递进T_i的更新规则每一轮重启后周期长度通常会按固定倍数拉长。最常用的规则是每次重新开始时把T_i乘上一个大于1的常数T_multT_i T_0 · (T_mult)^i其中T_0是第一个周期的长度i是从0开始的周期序号。当T_mult2时第一个周期是100步第二周期就是200步第三周期400步。选2比较多因为它是翻倍增长可以让我们在最前面的周期中快速探索后面的周期逐步提升训练精度。这个递增设计背后的道理也很实际重启初期学习率很大适合在更大范围内“重新出发”但如果你每个周期长度都一样短那模型在低学习率阶段的精细打磨时间就不够周期逐步延长可以让每个周期后半段的“低频打磨”阶段有足够的迭代次数。2.3 和CosineAnnealingLR、CyclicLR的差异对比SGDR的目标函数与Fixing the Learning Rate中的CosineAnnealingLR很相似区别就在于“重启”之后是否再次把学习率拉回最高点。很多人把SGDR和CyclicLR混用但二者机制不同对比项CosineAnnealingLRCyclicLRSGDR学习率形状单次余弦退火至最低三角/余弦周期波动上下限固定余弦退火热重启周期是否重置不重置训练结束就完了周期性但权重不因此额外操作是学习率周期性回到高位权重继承处理粗调与精调的程度仅精调无粗调粗精交替界限模糊每次重启后粗调退火精调轮流适用范围单次训练、短流程快速测试、找最优lr区间长训练、复杂任务、追求finetune能力在PyTorch里torch.optim.lr_scheduler.CosineAnnealingWarmRestarts直接实现了SGDR而CyclicLR实现的是周期学习率两者API看起来很接近实际意义完全不同。用错的话训练曲线长得不一样最终的泛化表现也常有差异。3. 为什么SGDR有效——三个维度的深层解读3.1 非凸优化的视角高学习率帮你逃逸鞍点深度学习模型的loss landscape是非常崎岖的非凸地形其中的局部极小值数量极其庞大但关键问题不是“局部极小值太多”而是“鞍点和平坦区域太多”。在高维空间里鞍点远比真正的局部极小值常见在大多数方向上loss都在下降但至少有一个方向上升SGD很容易被困在这些“看似平坦却暗藏下行方向”的地方。小学习率在鞍点附近的更新非常微弱几乎是原地踏步而SGDR周期性把学习率重新拉高之后梯度的有效步长变大让参数有机会“穿过”这些平坦区域进入另一个更深的盆地。热重启的“热”字这时体现得淋漓尽致因为是带着上一个周期的权重冲出来的而不是从随机初始化重新开始所以冲出去之后能迅速在新的区域进入精细搜索。3.2 泛化视角让多个“低学习率模型”共同投票训练过程中每一个周期都是一个从“大学习率粗找到小学习率精修”的过程最后期的权重可以被看作一个“单模型快照”。SGDR天然产生了多个这样的模型快照周期1结束时的权重是一个快照周期2结束时又有另一个快照……你可以选择只拿最后一个快照做测试也可以把这几个快照的预测结果平均起来这被称为“Snapshot Ensembling”。我不止一次在实际任务里尝到过这种“免费集成”的甜头。某个图像分类任务单个模型最后一轮test acc大约在88.1%我把前四次热重启结束时的四个模型权重各自跑了一遍测试再把softmax输出取平均准确率直接跳到了89.2%。算力成本没有额外增加一分钱因为那些模型是训练过程中本来就产生的。如果单次训练算力有限、又想享受模型集成的红利SGDR是一个绕开“独立训练多个模型”的高性价比方案。3.3 训练动态视角梯度噪声的良性利用高学习率的另一个隐秘好处是等价于放大了梯度中的“噪声比例”。SGD本身就是用Mini-batch去近似全量梯度这个过程天然带有一定噪声当学习率较高时每次更新中噪声带来的随机扰动会更大而这种扰动在优化中有时反而起到正则作用帮助模型逃离尖锐极小值掉进更平缓的极小值区域。平缓极小值通常对参数扰动不那么敏感测试集上的泛化能力更好。当然噪声不能一直大下去否则收敛就不稳定。SGDR的优雅之处是每个周期内部它会慢慢把学习率降下来让噪声逐步减小在周期末段达到“精修”效果。这样在一个训练流程里同时拥有“大噪声探索”和“小噪声精修”两个阶段而且循环不止一次。4. 手把手实现SGDR——PyTorch完整实操过程4.1 用PyTorch内置调度器快速上手PyTorch已经内置了SGDR对应的调度器代码写起来极其简单import torch from torch.optim import SGD from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts model torch.nn.Linear(64, 10) optimizer SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) scheduler CosineAnnealingWarmRestarts( optimizer, T_050, # 第一个周期的epoch数 T_mult2, # 每轮周期结束后周期长度翻倍 eta_min1e-6 # 每个周期内学习率能降到的最小值 ) for epoch in range(1, 401): train_one_epoch(model, optimizer) # 你自己实现的训练函数 # 注意这里一定要传入当前epoch编号从1开始 scheduler.step(epoch - 1) current_lr optimizer.param_groups[0][lr] print(fEpoch {epoch:3d}, LR {current_lr:.6f})这里有几个细节值得强调。第一scheduler.step()不是放在每个iteration之后而是每个epoch结束后调用并且传入的是“当前epoch编号减1”。如果你直接把epoch传进去第一个周期和下一个周期的边界会错位因为SGDR内部计算T_cur时对0和T_i这两种情况做了特殊处理。第二eta_min并不需要是0。实际经验是设为初始学习率的1/100到1/1000这个区间比较合适。设为0会让每个周期末期参数更新几乎停滞反而浪费训练时间。4.2 不依赖内置类——从零推导并实现SGDR调度有些人会怀疑内置实现里的细节是否正确它改过几次版或者需要在自定义框架里手动实现那我建议你自己写一个轻量调度器。核心代码大概就是下面这个结构import math class SGDRScheduler: def __init__(self, optimizer, T_0, T_mult2, eta_max0.01, eta_min1e-6): self.optimizer optimizer self.T_0 T_0 self.T_mult T_mult self.eta_max eta_max self.eta_min eta_min self.T_i T_0 self.T_cur 0 self.cycle 0 def step(self): self.T_cur 1 if self.T_cur self.T_i: self.T_cur 0 self.cycle 1 self.T_i self.T_0 * (self.T_mult ** self.cycle) lr self.eta_min 0.5 * (self.eta_max - self.eta_min) * ( 1 math.cos(math.pi * self.T_cur / self.T_i) ) for param_group in self.optimizer.param_groups: param_group[lr] lr用起来和内置调度器基本一致区别是你自己控制了T_i的更新时机不容易踩到版本差异的坑。这个实现里我故意把eta_max设成构造参数而不是沿用optimizer.param_groups[0][lr]是因为我经常创建一个临时优化器来“试跑”学习率轨迹不想每次改峰值都动优化器。4.3 完整训练循环的可复现示例结合一个迷你图像分类任务来看整体效果。假设我有一个自定义的ResNet-18模型训练集60个epochT_0取20T_mult取2def train_with_sgdr(): model ResNet18(num_classes10).cuda() optimizer SGD(model.parameters(), lr0.05, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingWarmRestarts(optimizer, T_020, T_mult2, eta_min1e-5) criterion torch.nn.CrossEntropyLoss() history [] for epoch in range(1, 181): model.train() total_loss 0.0 for images, labels in train_loader: images images.cuda() labels labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() # 每个epoch结束后推进调度器 scheduler.step(epoch - 1) avg_loss total_loss / len(train_loader) lr_now optimizer.param_groups[0][lr] train_acc evaluate(model, train_loader) test_acc evaluate(model, test_loader) history.append({ epoch: epoch, lr: lr_now, loss: avg_loss, train_acc: train_acc, test_acc: test_acc, }) return model, history你可能注意到我训练的模型第一个周期是20个epoch第二个周期是40个epoch第三个周期是80个epoch第四个周期是160个epoch。但实际我只训练了180个epoch第一个周期20、第二个40、第三个80第四个周期才跑了40个epoch就结束了总共140?我写错了吗重新算204080140180−14040第四个周期还在进行中不到一半。这种做法很常见——总训练预算经常不会刚好覆盖整数个完整周期最后一个周期截断在中间也没关系反而能让你在更低的当前学习率处停止得到一个更精细收敛的模型。4.4 训练曲线怎么读波浪线是坏事吗第一次跑通SGDR后去看loss曲线大多数人的反应是“这玩意是不是坏掉了”。训练loss呈现出明显的锯齿波每个周期开始时冲上去一下然后慢悠悠落下来下一个周期又开始冲……如果你没见过SGDR曲线第一反应绝对是“模型在发散”。其实这是完全正常的。你要注意的并不是单个抄起而是全局底线是不是在往下降。比如第一个周期结束时loss最低到了0.51第二个周期中期又反弹到0.55但周期结束时能降到0.46第三个周期结束时到了0.42——这就是良性曲线。真正需要警惕的是每个周期的“最低点”不再下降开始原地打转甚至逐步抬升那说明重启策略可能已经无法从当前区域再学到新东西了考虑减小eta_max或者提前终止训练。5. 核心参数调优与经验心得——这几个坑我都替你试过了5.1 T_0选多少比较合适T_0第一个周期长度是整个SGDR里最敏感的参数。它太小了每次周期都像还没准备好吃火锅就被拉去重跑模型在低学习率阶段没有充分打磨周期性优势发挥不出来它太大了第一个周期就占掉大半算力相当于退化为一次普通余弦退火。我的经验是让第一个周期覆盖总训练预算的1/10到1/6。比如你总共打算跑600个epochT_0取60到100之间都算合理总预算150个epochT_0取20到25。如果拿不准宁可选偏小一点的值因为有T_mult的放大效应后面的周期会自然变长。5.2 T_mult到底应该用1还是2这是实验者分派最多的争论点。T_mult1表示所有周期等长T_mult2表示后续周期翻倍。我之前跑某跨平台系统里的文本分类任务时统计过T_mult2的最终精度通常比T_mult1略高但训练前期需要多忍受几次“重启后短期掉点”的震荡。T_mult1的优势是每个周期都能公平获得一次精修机会后面不会出现“一个周期动辄一两百轮”的情况。选T_mult1的情境一般是任务本身每个epoch很便宜你希望均衡地多取几次快照做集成选T_mult2的情境是模型较贵、训练预算较长你更看重新周期大范围探索和老周期末尾的精细收敛。我个人对大部分中型任务默认T_mult2除非集成需求特别强烈才会切回T_mult1。5.3 eta_max能不能直接用初始学习率还是另设很多人在用内置调度器时直接把optimizer的lr当成eta_max传给调度器。这样做能跑但有个隐患optimizer的lr和调度器控制的lr是两回事调度器是直接覆盖param_group里的lr值如果之后你手动调了optimizer的lr或者代码里其他位置对它做修改SGDR的曲线会错乱。我建议把eta_max单独设一个变量并在构造优化器时就把optimizer的lr值设成同一个值。比如base_lr 0.05 optimizer SGD(model.parameters(), lrbase_lr, momentum0.9) scheduler CosineAnnealingWarmRestarts(optimizer, T_040, T_mult2, eta_min1e-5)这样做的好处是假设你训到一半发现这个任务学习率太大了导致重启后第一周期难以收敛你只需要把base_lr降低然后再重新初始化优化器和调度器即可不需要在代码里做“全局替换”。至于eta_max具体取多大我有个经验比例用普通StepDecay可以正常收敛的学习率SGDR的eta_max可以比它再大胆一点取1.5到3倍。因为重启后的短期震荡有一定容错性你会给它重新收敛的机会。5.4 每个周期的哪段权重最适合做checkpoint和集成如果按“低学习率阶段更精细”的逻辑很多人会以为每个周期最后一步的权重最好。实际上我在某图像分割任务里比较过一个周期内最后1/3段的多个checkpoint表现结论是每个周期结尾前2-3个epoch的权重往往最优但周期刚结束、T_cur被重置为0之后的那一刹那测试精度通常会突然掉一截。这是因为学习率跳回高位后模型会在第一个epoch里被大幅“扰动”test acc快速下降。所以集成时要用“每个周期末尾的checkpoint”而不能用“调度器重置时刻的checkpoint”。我的策略是设一个回调函数当T_cur大于T_i的85%时每两个epoch保存一次模型周期结束时把这些候选里验证集表现最好的那个作为本周期快照。这样集成的模型质量会明显高于直接拿周期末尾那张权重。5.5 混合精度和EMA搭配SGDR的注意事项现在训练大模型基本离不开混合精度。AMP的GradScaler和SGDR调度器之间没有直接冲突但有一个容易被忽视的坑GradScaler的scale值会根据最近一段时间的梯度幅值自动调整重启初期学习率突然变大梯度norm可能暴涨导致scaler认为“梯度溢出”从而跳过几步更新。如果你发现每个周期一开始训练loss不动甚至变NaN先检查是不是GradScaler把更新跳过了。应对办法之一是给GradScaler设置一个init_scale较大的初始值或者在前几个周期里禁用动态scale固定一个保守的scale。另外EMA指数移动平均和SGDR搭配得很好每个周期因为试验性大学习率造成短期过冲EMA能把这种过冲平滑掉。但EMA的decay因子不能太小0.999到0.9999这个区间比较合适太小了跟权重同步那就完全失去平滑意义了。6. 实战调优心得——参数选择、曲线诊断与零星技巧6.1 保持周期内batch shuffle这里有个容易翻车的细节如果你的DataLoader设置了shuffleFalse每个周期内模型看到的样本顺序完全一致SGDR的周期重启会和固定的梯度序列互相强化导致loss出现诡异的周期性抖动。实际表现是每个周期的收敛曲线形态几乎一模一样而且最终精度偏低。对比实验我做过很多次同样的T_0、T_mult和lrshuffleTrue时最终test acc比shuffleFalse高0.8到1.5个百分点。所以一个很不起眼的点——每次epoch开始时对数据重新洗牌是SGDR能发挥出效果的基本前提。6.2 每个周期重启后要不要重新计算BN统计量这个问题主要出现在“先用大学习率预热、然后重启”的场景。因为重启初期学习率大幅上升模型参数被扰动较大BatchNorm层中记录的running_mean和running_var是上一个周期末期的统计量在新权重下会有一两个epoch的“错位”。如果你用的是很小的batch size这个错位会明显表现为训练loss在重启后短期内升高。处理办法有两种。做法一在每次调度器step之后让模型在中性的数据子集上多跑一次forward用新权重刷新BN的running stats代价是额外一个epoch做法二不做额外前向而是在前3个epoch里把BN层的参数固定不更新只让其他层适应。这两种做法在ImageNet规模的训练里都有实证我在中等规模数据集上更推荐做法一简单省心。6.3 借助lr曲线提前诊断问题把每个epoch的学习率画成折线图之后你能发现很多隐藏信息如果“学习率峰值”处的曲线斜率太大、太陡峭意味着你的eta_max偏大周期内前几个epoch震荡太剧烈。如果每个周期的loss最低点都同比上一个周期抬高了说明整个训练进程已经进入过拟合阶段不是参数问题而是该考虑数据扩充、正则化或者提前终止了。如果重启后test acc要花大半个周期才能勉强回到上一次的最低点意味着T_0偏小模型在单个周期内根本没有充分收敛。这些诊断不需要复杂工具用matplotlib画曲线即可。6.4 和早停、ReduceLROnPlateau混用是一种“禁忌”见到不少人会这样写先用SGDR又在验证集loss连续3轮不降时调用ReduceLROnPlateau。看似双保险实际是画蛇添足。因为ReduceLROnPlateau会“偷偷”把学习率往下乘一个因子而SGDR的调度器接管了param_group里的lr两者相互覆盖最终的lr曲线会变得不可解释。我在某次NLP微调任务中试过这种组合训练曲线出现不规律的骤降最后模型还比纯SGDR低了1个点左右。如果你实在想在周期内“坏了就降一点”更稳妥的做法是自定义一个逻辑当周期内最低点不再下降时减小下一个周期的eta_max而不是在周期内再插一个调度器。这样保持了每次重启的完整语义。7. 常见问题与排查技巧实录7.1 重启后loss暴涨到不收敛怎么办先别慌看看是什么级别的“暴涨”。如果只是从0.50跳到0.60那是正常震荡如果从0.50跳到2.00甚至NaN那大概率是eta_max设太大。解决方案是把eta_max调低到当前值的1/3到1/2同时把momentum略微调高一点例如0.9调到0.95利用历史梯度做平滑减少重启初期的波动。还有一种情况重启后loss暴涨是因为梯度爆炸而这通常发生在模型使用了LayerNorm或BatchNorm但训练不稳定时。这时可以加一个梯度clipmax_norm设在1.0附近。我不建议一开始就加clip因为clip会限制大学习率“冲出盆地”的能力但如果你出现了NaN那就必须加。7.2 每个周期训练到一半验证集指标反而上升了正常吗完全正常。这可能是因为周期中段学习率从高位快速回落模型正在从“大范围搜索”切换到“局部精修”而你看到验证指标上升正好发生在搜索尚未完成时。如果这种现象每周都出现说明周期长度偏长后半段其实是在“原地精修”可以在后半段提前早停或者在下一次周期开始时用更大T_mult。不过多数时候不需要特殊处理。7.3 训练结束了到底拿哪个周期的模型做最终预测如果预算紧张、只能留一个模型我建议选最后一个周期结束点的权重。理由很简单它经过最多轮次的精修在损失面上通常处于最深的盆地。但如果你发现最后一个周期还没完整跑完就在“当前lr已经降到eta_min附近”的权重里选更安全。沙盘推演一下总训练300轮T_030T_mult2那么周期边界是30、90、210第300轮处在第四个周期的中段此时学习率还没降到最低。这种情况下最优模型往往出现在周期接近末尾处但第四个周期要到420轮才结束训练到时已经超预算了。所以更聪明的做法是提前算好总epoch数选择一个让最后一个周期恰好完整结束的T_0和T_mult组合或者把总训练预算设置成刚好覆盖若干个完整周期。7.4 连续重启几次后模型精度不再提升是进入“死区”了吗这种情况下先不要盲目继续跑。我通常的做法是检查周期末loss是否已经接近收敛下限比如与最开始的loss相比只下降了10%以内如果是再做一次“瘦身式重启”——把eta_max降为原来的1/5周期长度缩短一半用一次小周期做最终精修。这个操作几乎成了我所有SGDR训练的固定尾声比直接结束训练平均能再挤出0.2到0.4个点的指标。8. 适用场景与不适用场景——别把SGDR当万能药8.1 适合用SGDR的任务类型从我个人的实践分布看SGDR在以下任务里优势突出视觉分类和分割尤其是深层CNNloss landscape复杂周期性重启的探索-收敛循环非常匹配。NLP预训练和微调Transformer类模型训练曲线往往存在平台期SGDR的周期性反复有机会突破平台。调整成本高的场景比如每次实验很贵大batch、多卡跑几天SGDR能通过周期性探索减少你“手工回调学习率”的交互次数。8.2 不适合甚至有害的任务类型强化学习策略网络的价值函数非平稳性较强周期性抬高学习率可能干扰经验回放中已有的策略稳定性让训练更震荡。小数据量的单轮精调如果总共只有10-20个epoch的预算第一个周期还没走完就结束了SGDR的优势完全发挥不出来用标准余弦退火就够了。在线学习、流式更新场景SGDR预设了完整的“周期”概念在线场景的样本分布不断变化给学习率“重启”不是好事。8.3 和其他训练技巧的组合清单最后给一张我常用的组合表直接抄作业即可训练手法是否建议和SGDR搭配备注数据增强随机裁剪、Mixup等强烈建议增强带来的样本多样性可以让每次重启探索更有效标签平滑建议让loss函数更平滑减少重启初期梯度突变梯度裁剪视情况只有出现NaN或大震荡时才开Warmup不建议SGDR本身的开头就有高位重启额外warmup会打乱节奏EMA建议平滑重启噪声尤其是CV任务Snapshot Ensemble强烈建议白嫖多模型集成效果显著多卡同步BatchNorm建议大数据集上可以让BN统计更稳定我个人在实际操作中的体会是SGDR不是那种“设好参数就不管了”的调度器它的收益很大程度建立在你对每个周期终结点的利用上。如果你只是随手把内置调度器接上去看曲线那它可能只是和普通余弦退火打平但如果你能认真设计T_0和T_mult、保存好每个周期的权重快照、并在训练尾声做一次缩水版精修它往往会带来惊喜。这个方法我已经用了不少年头现在每次开始长训练任务默认配置基本就是“SGDR周期EMA周期末尾快照”很少再手动去调什么学习率衰减节点了。