先说个可能很多人都有过的经历:开局辛辛苦苦调出一个基础学习率,训练了二三十个epoch之后loss卡在0.7死活不动,然后你试着把学习率从0.001改成0.0001,跑了一百多轮终于降到0.6。这时候你会不会想,如果有个机制能让你在训练过程中自动做这种事,甚至比手动静默降两次学习率做得更聪明一点,该多好。SGDR学习率调整策略就是干这个的。
SGDR全称是Stochastic Gradient Descent with Warm Restarts,2017年由Ilya Loshchilov和Frank Hutter提出,思路其实很朴素——在SGD训练过程中,周期性把学习率调大,然后让它按余弦方式慢慢衰减,衰减到底后再突然拉回高位,再来一轮。这种“热重启”机制乍一看有点违背直觉:训练到后期不是应该越学越精细吗,为什么还要把学习率瞬间拉大,会不会直接把已经收敛的权重打飞?
我自己第一次看到这个策略时也是这个疑问。真正把它跑起来之后才明白,SGDR不是一个简单的“分段减学习率”工具,而是把“退火”和“重启”结合起来的一种整体训练方法论。下面我尽量把原理、代码、参数选择和我踩过的坑一次讲清楚。
1. 学习率为什么是个“老大难”——从SGD的视角看问题
1.1 学习率太高太低都是灾难
在讨论SGDR之前,有必要先把基础对齐。深度学习训练本质上是求解一个高维非凸优化问题,最常用的SGD每次迭代用一小批数据的梯度和一个固定的学习率 η 来更新权重:
θ_{t+1} = θ_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_cur=0时,cos(0)=1,η_t=(η_max+η_min)/2·1?不对,仔细算一下:η_t = η_min + (η_max−η_min)·(1+1)/2 = η_max,学习率最大。当T_cur=T_i时,cos(π)=−1,η_t = η_min + (η_max−η_min)·(1−1)/2 = η_min,学习率降到最小。中间过程是余弦函数的平滑下降,先缓后急再缓,而不是线性从高到低。
余弦衰减的“慢-快-慢”节奏非常有意思。训练初期和末期学习率的下降幅度都较缓,中期下降最快。初期缓慢能让权重充分探索;在接近周期末尾时,学习率已经很小,权重在局部区域精细打磨,为下一次重启准备一个“好底子”;而周期中段快速下降则避免了在中等学习率区间浪费太多训练时间。
2.2 周期长度怎么递进:T_i的更新规则
每一轮重启后,周期长度通常会按固定倍数拉长。最常用的规则是每次重新开始时把T_i乘上一个大于1的常数T_mult:
T_i = T_0 · (T_mult)^i
其中T_0是第一个周期的长度,i是从0开始的周期序号。当T_mult=2时,第一个周期是100步,第二周期就是200步,第三周期400步。选2比较多,因为它是翻倍增长,可以让我们在最前面的周期中快速探索,后面的周期逐步提升训练精度。
这个递增设计背后的道理也很实际:重启初期,学习率很大,适合在更大范围内“重新出发”,但如果你每个周期长度都一样短,那模型在低学习率阶段的精细打磨时间就不够;周期逐步延长,可以让每个周期后半段的“低频打磨”阶段有足够的迭代次数。
2.3 和CosineAnnealingLR、CyclicLR的差异对比
SGDR的目标函数与Fixing the Learning Rate中的CosineAnnealingLR很相似,区别就在于“重启”之后是否再次把学习率拉回最高点。很多人把SGDR和CyclicLR混用,但二者机制不同:
| 对比项 | CosineAnnealingLR | CyclicLR | SGDR |
|---|---|---|---|
| 学习率形状 | 单次余弦退火至最低 | 三角/余弦周期波动,上下限固定 | 余弦退火+热重启 |
| 周期是否重置 | 不重置,训练结束就完了 | 周期性但权重不因此额外操作 | 是,学习率周期性回到高位,权重继承 |
| 处理粗调与精调的程度 | 仅精调,无粗调 | 粗精交替,界限模糊 | 每次重启后粗调+退火精调轮流 |
| 适用范围 | 单次训练、短流程 | 快速测试、找最优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(), lr=0.01, momentum=0.9, weight_decay=1e-4) scheduler = CosineAnnealingWarmRestarts( optimizer, T_0=50, # 第一个周期的epoch数 T_mult=2, # 每轮周期结束后,周期长度翻倍 eta_min=1e-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(f'Epoch {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_mult=2, eta_max=0.01, eta_min=1e-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个epoch,T_0取20,T_mult取2:
def train_with_sgdr(): model = ResNet18(num_classes=10).cuda() optimizer = SGD(model.parameters(), lr=0.05, momentum=0.9, weight_decay=5e-4) scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=20, T_mult=2, eta_min=1e-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?我写错了吗?重新算:20+40+80=140,180−140=40,第四个周期还在进行中不到一半。这种做法很常见——总训练预算经常不会刚好覆盖整数个完整周期,最后一个周期截断在中间也没关系,反而能让你在更低的当前学习率处停止,得到一个更精细收敛的模型。
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个epoch,T_0取60到100之间都算合理;总预算150个epoch,T_0取20到25。如果拿不准,宁可选偏小一点的值,因为有T_mult的放大效应,后面的周期会自然变长。
5.2 T_mult到底应该用1还是2
这是实验者分派最多的争论点。T_mult=1表示所有周期等长;T_mult=2表示后续周期翻倍。我之前跑某跨平台系统里的文本分类任务时统计过,T_mult=2的最终精度通常比T_mult=1略高,但训练前期需要多忍受几次“重启后短期掉点”的震荡。T_mult=1的优势是每个周期都能公平获得一次精修机会,后面不会出现“一个周期动辄一两百轮”的情况。
选T_mult=1的情境一般是:任务本身每个epoch很便宜,你希望均衡地多取几次快照做集成;选T_mult=2的情境是:模型较贵、训练预算较长,你更看重新周期大范围探索和老周期末尾的精细收敛。我个人对大部分中型任务默认T_mult=2,除非集成需求特别强烈,才会切回T_mult=1。
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(), lr=base_lr, momentum=0.9) scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=40, T_mult=2, eta_min=1e-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设置了shuffle=False,每个周期内模型看到的样本顺序完全一致,SGDR的周期重启会和固定的梯度序列互相强化,导致loss出现诡异的周期性抖动。实际表现是每个周期的收敛曲线形态几乎一模一样,而且最终精度偏低。
对比实验我做过很多次:同样的T_0、T_mult和lr,shuffle=True时最终test acc比shuffle=False高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但训练不稳定时。这时可以加一个梯度clip,max_norm设在1.0附近。我不建议一开始就加clip,因为clip会限制大学习率“冲出盆地”的能力;但如果你出现了NaN,那就必须加。
7.2 每个周期训练到一半,验证集指标反而上升了,正常吗
完全正常。这可能是因为周期中段学习率从高位快速回落,模型正在从“大范围搜索”切换到“局部精修”,而你看到验证指标上升正好发生在搜索尚未完成时。如果这种现象每周都出现,说明周期长度偏长,后半段其实是在“原地精修”,可以在后半段提前早停或者在下一次周期开始时用更大T_mult。不过多数时候不需要特殊处理。
7.3 训练结束了,到底拿哪个周期的模型做最终预测
如果预算紧张、只能留一个模型,我建议选最后一个周期结束点的权重。理由很简单:它经过最多轮次的精修,在损失面上通常处于最深的盆地。但如果你发现最后一个周期还没完整跑完,就在“当前lr已经降到eta_min附近”的权重里选,更安全。
沙盘推演一下:总训练300轮,T_0=30,T_mult=2,那么周期边界是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在以下任务里优势突出:
- 视觉分类和分割:尤其是深层CNN,loss 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+周期末尾快照”,很少再手动去调什么学习率衰减节点了。