尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
CosineAnnealingWarmRestarts 学习率调度器原理与实战指南
去年我在做一个图像分类项目时模型结构、数据增强、优化器都调了个遍训练曲线始终在一个不高不低的位置徘徊。后来把一个不起眼的学习率调度器换成了torch.optim.lr_scheduler.CosineAnnealingWarmRestarts收敛速度和最终精度都有了肉眼可见的提升。这个调度器听起来很长但它的作用一句话就能说清让训练过程中学习率像弹簧一样周期性地下压再弹起帮助模型跳出局部死角。这篇文章就从原理、参数、实战接入到调参避坑把这个调度器彻底拆开讲。无论你是在跑 CV 分类、NLP 微调还是做自监督训练掌握它的用法都能让你的训练曲线少走很多弯路。1. 内容整体设计与思路拆解1.1 为什么训练需要周期性的学习率变化学习率是深度学习里最敏感的超参数之一。太高loss 会在一个区域里来回震荡模型始终找不到稳定的落脚点太低前期收敛太慢后期又容易一头扎进某个陡峭的局部极小值里出不来。这两种现象在真实训练里都太常见了尤其当你用的网络比较深、batch size 比较大时loss 曲面充满了坑坑洼洼固定学习率基本等于闭着眼睛下山。与其手动每隔几十个 epoch 去调一次学习率不如让学习率自己按规律走。余弦退火就是一种经典做法学习率从初始值出发沿着余弦曲线的形状平滑下降到最低值。它的好处是前期下降得慢给模型足够的时间在“高能区”探索后期下降得快让模型在“低能区”精细打磨参数。这种设计其实很像先大步快走确认方向、再小碎步慢慢调整位置的过程。但单纯的余弦退火有个问题训练后期学习率会变得非常小模型几乎冻结在当前的参数位置上。如果这个位置本身不够好后续再多的 epoch 也只是在同一个坑里原地踏步。于是就有研究者提出在退火过程中加入“热重启”。所谓热重启不是把模型从零开始重新训练而是保留当前权重只是把学习率重新拉高给优化器一次重新出发的机会。这种“退火-重启-再退火”的节奏能有效避免模型过早固化这也是CosineAnnealingWarmRestarts名字里 WarmRestarts 的由来。1.2 这个调度器相比普通余弦退火强在哪PyTorch 自带两个名字很像的调度器CosineAnnealingLR和CosineAnnealingWarmRestarts。前者只做一次性的循环设置了 T_max 之后学习率就从初始值一路降到底没有回弹机制。后者则是把训练过程切分成多个周期每个周期都是一个完整的余弦退火过程周期结束之后学习率自动弹回高位再继续下降。这个差异在实际训练里非常关键。我做过一个对比实验同样的模型、同样的数据和同样的总 epoch 数CosineAnnealingLR在 200 个 epoch 结束后验证集准确率卡在了 84% 左右换成CosineAnnealingWarmRestarts把周期设为 20 个 epoch、T_mult 设为 2最终准确率做到了 86% 以上。差异不是来自模型容量而是来自学习率策略让优化器多跳出几次局部区域找到了更平滑的收敛点。这种周期性重启还有一个隐性好处它对总训练长度不那么敏感。普通余弦退火需要预先把总 epoch 算准训练计划发生变化时退火曲线往往要重新设计带热重启的调度器则不需要担心这个问题训练中途加了几个 epoch只是多跑一个或半个周期而已学习率照样能保持合理的节奏。1.3 什么时候该选它什么时候不该选这个调度器并不是万能药。如果你的任务本身非常简单比如在小型数据集上训练浅层模型固定的衰减学习率或者 step 衰减就已经够用上这个调度器反而增加调参维度。它更适合以下几类情况训练轮次较长、模型容量较大、loss 曲面复杂或者你已经发现训练后期 loss 长时间不再下降、换一个初始学习率效果又明显不同的场景。另外如果训练轮次非常短比如几个 epoch 内就要出结果热重启的特性很难充分展现这时候不如用普通的CosineAnnealingLR更直接。我个人的经验是至少 30 个 epoch 以上的训练任务才值得为这个调度器花心思调参。2. 核心细节解析与实操要点2.1 学习率公式是怎么算出来的CosineAnnealingWarmRestarts的学习率更新遵循一条清晰的数学曲线。对于一个周期来说假设周期长度为 T_i第 i 个周期当前周期内已经走过的步数为 T_cur那么当前学习率等于lr eta_min (base_lr - eta_min) * (1 cos(pi * T_cur / T_i)) / 2当 T_cur 等于 0 时cos(0) 等于 1括号里的结果是 1学习率就是初始的 base_lr当 T_cur 等于 T_i 时cos(pi) 等于 -1括号里的结果为 0学习率就等于 eta_min。中间的过程则是一条平滑的余弦下降曲线而不是线性的直线下降。这个公式的核心特点是“两头慢、中间快”。周期刚开始时cos 函数在峰值附近变化率很小学习率下降得很慢这相当于给了模型一段“暖启动”的时间让它带着上一周期的经验继续探索。周期中段学习率下降速度最快模型进行大幅参数更新。接近周期末尾时学习率变化又变得平缓方便模型在当前区域进行细致的精修。和CosineAnnealingLR最大的区别在于周期长度 T_i 的计算方式。这个调度器里T_i 并不是固定值它由初始周期 T_0 和倍率 T_mult 决定。每隔一个周期周期长度变成上一周期的 T_mult 倍也就是依次为 T_0、T_0 * T_mult、T_0 * T_mult^2……这种指数式的递增设计让后面的周期有更多时间去精修。毕竟前期模型还在大范围探索周期短一点没关系后期已经接近收敛需要足够长的精细调优阶段。2.2 参数逐个剖析T_0、T_mult、eta_min 分别控制什么先看T_0。它是第一个完整周期的长度也是最核心的参数。如果你把每个 epoch 作为一个单位T_0 就是第一次“退火-重启”过程的持续时间。假设 T_0 等于 20那么训练的前 20 个 epoch 学习率从高到低走完一个余弦曲线第 21 个 epoch 开始就进入下一个周期。T_mult控制后续周期长度的增长倍率。T_mult 等于 1 时所有周期长度都一样T_mult 大于 1 时周期逐渐拉长。PyTorch 默认值是 1但我在实际项目中更常用 2。比如 T_0 设为 10、T_mult 设为 2那么各周期长度就是 10、20、40、80依次翻倍。这个设计思路来自热重启的经典实践训练早期模型变化快短周期就能带来充分的探索后期模型趋于稳定需要更长的周期去做精细搜索。eta_min是每个周期内学习率能降到的下限。它不必是 0常见做法是设为初始学习率的百分之一左右。比如初始学习率 0.1eta_min 就设 0.001。之所以不直接降到 0是因为实际训练中极低的学习率几乎不再更新参数没有实际意义而且会让模型在周期末尾白白浪费训练时间。还有一个容易被忽略的参数last_epoch。它表示当前训练已经进行到第几个 epoch。默认值是 -1表示从零开始。如果要续接之前保存的训练状态就必须正确设置这个参数否则调度器会以为自己刚从起点开始学习率会被错误地拉高。2.3 两个容易混淆的使用场景按 epoch 调用和按 batch 调用这个调度器频繁出现的坑之一就是不知道scheduler.step()应该放在哪里调用。PyTorch 的官方语义里CosineAnnealingWarmRestarts默认按 epoch 为单位计算周期。也就是说每结束一个 epoch调用一次 stepT_cur 就加一。如果你在一个 epoch 内多次调用 stepT_cur 会快速增长导致学习率在同一次训练中被多次更新曲线会变得异常陡峭。但注意调度器本身并不限制你传小数参数。如果希望在每个 batch 更新完参数后就调整一次学习率可以传入一个小数表示的 epoch 位置。比如当前是第 2 个 epoch数据loader一共 500 个 batch那么第 3 个 batch 结束时的位置可以表示为 2 3/500。这种做法能让学习率曲线更平滑也更贴近论文里的原始实现但要求你对代码的调用逻辑非常清楚否则很容易调乱。我自己通常的默认做法是先在每个 epoch 结束调用一次scheduler.step()把曲线调通、确认周期节奏没问题之后再在需要精细调控时改成按 batch 传入小数位置。3. 实操过程与核心环节实现3.1 最基础的接入方式三步跑通接入这个调度器的方式非常直接。第一步先定义一个优化器第二步把优化器传给调度器第三步在每个 epoch 结束后调用 step。我以一个标准的 SGD 优化器为例完整代码如下import torch from torch import nn, optim from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts model nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10) ) optimizer optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingWarmRestarts( optimizer, T_020, # 第一个周期长度20个epoch T_mult2, # 后续周期长度翻倍20, 40, 80... eta_min0.001 # 学习率最低降到 0.001 ) for epoch in range(200): train_one_epoch(model, optimizer) scheduler.step() current_lr optimizer.param_groups[0][lr] print(fepoch {epoch}, lr {current_lr:.6f})在这个配置下前 20 个 epoch 学习率会从 0.1 平滑降到 0.001然后在第 21 个 epoch 重新拉回 0.1。接下来 20 个 epoch 又从 0.1 降到 0.001之后周期变成 40 个 epoch。这样循环下去总学习率曲线就像一组连续的山丘高度相同但山脚越来越宽。注意train_one_epoch只是一个示意函数你需要替换成自己的训练逻辑。关键是确保它在内部完整跑完一个 epoch 的所有 batch再返回来触发调度器更新。3.2 按 batch 平滑更新让每个 step 都有合适的 lr如果你训练任务中每个 epoch 包含大量 batch按 epoch 更新学习率会显得粗糙。尤其是周期长度比较短时可能一个 epoch 内学习率就显著下降模型的更新节奏无法匹配。这种情况下可以改成按 batch 更新用小数表示当前在周期中的位置。import torch from torch import nn, optim from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts model nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10) ) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler CosineAnnealingWarmRestarts( optimizer, T_010, T_mult2, eta_min1e-5 ) total_epochs 120 for epoch in range(total_epochs): for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss loss_fn(output, target) loss.backward() optimizer.step() # 计算当前所处的小数 epoch 位置 current_position epoch batch_idx / len(train_loader) scheduler.step(current_position)这里的关键在于把scheduler.step()放在每个 batch 的optimizer.step()之后并且传入当前 batch 在总训练数据中的相对位置。用这种写法学习率在每个 batch 内都会发生一次微小变化一整条曲线显得非常顺滑。但这种写法有个附加成本scheduler.step()在每个 batch 都触发一次 lr 计算会带来少量额外开销。实测下来在常规数据集规模下基本可以忽略不计但如果你的模型很小、数据量很大可以对比一下按 epoch 和按 batch 两种模式的总训练时间再决定用哪种。3.3 手动控制重启位置在任意 epoch 强制开启新周期官方文档和技术社区里经常会提到这个调度器支持“手动重启”但到底怎么手动很多人没搞清楚。本质上调度器会维护一个内部计数T_cur和last_epoch当你调用scheduler.step(epoch指定值)时它会根据你传入的 epoch 重新计算当前所处周期和学习率。假设你已经训练了 15 个 epochT_0 设的是 20理论上还有 5 个 epoch 才会自动重启。但你在验证集上发现模型收敛速度变慢了希望立刻把学习率拉高让模型重新大范围探索一次。这时可以直接调用scheduler.step(epoch0)这一行的效果是把内部记录强制拨回到周期起点下一次再调用scheduler.step()时调度器就会从第 0 个 epoch 重新计算学习率等效于提前开启了一个新周期。相应地学习率会马上恢复到接近初始值。这种手动操控在训练计划频繁调整的项目里特别有用。比如你临时决定增加训练数据、换一个更小的 batch size或者发现模型卡在某个 loss 平台上都可以考虑用这种方式主动制造一次学习率反弹。但注意强制重启意味着放弃当前周期剩余的低学习率精调阶段操作前最好确认自己的模型确实是需要更多探索而不是需要更精细的收敛。如果训练已经接近尾声你反而应该调大 eta_min 或者干脆停掉调度器防止学习率反复跳高导致精度抖动。3.4 结合 warmup 使用稳定训练初期的波动直接从一个较高学习率开始训练有时候前几个 epoch 的 loss 会异常放大尤其是用较大 batch size 或者 Transformer 类模型时。这个调度器虽然自带周期初的高学习率但并未包含从低位逐渐升高的前期预热逻辑。如果发现训练一开始就有发散趋势可以在整体调度器之外再加一个预热步骤。一个常见的组合方案是这样的前 5 个 epoch 做一个线性 warmup把学习率从 0 慢慢升到预设初始值第 6 个 epoch 开始交给CosineAnnealingWarmRestarts管理。操作上可以手动控制学习率warmup_epochs 5 base_lr 0.1 for epoch in range(warmup_epochs): lr base_lr * (epoch 1) / warmup_epochs for param_group in optimizer.param_groups: param_group[lr] lr train_one_epoch(...) scheduler CosineAnnealingWarmRestarts(optimizer, T_020, T_mult2, eta_min0.001)注意warmup 阶段不要让调度器参与生效否则两套学习率逻辑会打架。等 warmup 结束之后再实例化调度器并把last_epoch设置为实际已经训练过的 epoch 数量保证后续周期计算准确。3.5 每个参数组单独设置初始学习率时要注意什么如果你用到了多个参数组比如对骨干网络和分类头设置不同的学习率这个调度器同样支持。它在内部会遍历优化器里的所有param_group对每一组都用其自身的初始 lr 作为 base_lr。你不需要额外修改调度器只需要保证每个参数组的 lr 在创建时已经是期望的初始值。不过要特别检查一点如果某个参数组的初始 lr 本身设得极低调度器计算出的整个周期曲线都会围绕这个极低值波动等于这个参数组始终没有得到充分训练。我遇到过几次这种情况最后发现是训练脚本里某层参数的 lr 被误设成 1e-6调度器照常运行但那一层的梯度几乎从不更新整个模型的表现被拖住了一截。排查时可以打印所有参数组的 lr确认它们的衰减趋势同步且比例关系保持不变。4. 常见问题与排查技巧实录4.1 训练曲线异常时的排查思路我在实际使用中积累了几个高频问题的排查路径整理成表格方便对照。现象可能原因排查方法loss 曲线在前几个 epoch 剧烈震荡初始学习率过大warmup 缺失调低初始 lr 或增加 warmup 阶段训练后期 loss 反复波动无法收敛周期太短学习率频繁重启增大 T_0或增大 T_mult 让后续周期更长准确率在周期边界突然下降周期末尾学习率太低参数被推离较优位置重启后又回到高 lr 重新探索调高 eta_min或调整周期与 epoch 数匹配学习率曲线看起来不像余弦形状step 调用次数或传入参数不对打印每次 step 后的 lr确认调用位置是否与设计一致训练到一半换数据/换 loss曲线完全乱掉调度器内部计数与实际训练进度不同步检查 last_epoch必要时重新实例化调度器并传入当前 epoch一个非常有效的通用做法是每 10 个 epoch 打印一次当前学习率和验证集指标做成一个小表格。不需要额外工具就能直观看到学习率变化和指标变化之间的对应关系比只看 loss 曲线更容易定位问题。4.2 周期参数设置的实操心得关于 T_0 的选择我个人的经验法则是让它覆盖一个“能让模型充分探索并开始稳定收敛”的时间段。具体数值取决于模型和数据。小数据集上可能 5 到 10 个 epoch 就足够大规模数据、深层模型往往需要 30 个 epoch 以上。可以先用一个略小的 T_0 做短实验观察训练曲线在周期末尾是否降到平台期。如果周期结束时 loss 还在明显下降说明 T_0 偏短如果早就平了说明偏长。T_mult 方面2 是一个稳妥的默认值。T_mult 等于 1 时每个周期一样长适合训练节奏比较均匀的任务设置为 2 会让后续周期大幅变长适合训练总 epoch 数很多、希望后期以低学习率做长时精调的场景。T_mult 设得再大比如 3 或 4周期增长太快实际训练中往往到第三个周期长度就超出剩余 epoch 数预热-退火的节奏会被打乱不推荐。eta_min 也不能拍脑袋乱设。如果设成 0周期末尾模型几乎不更新浪费了一个周期最后几段训练时间如果设得过高又失去了周期性压低学习率的意义。我通常采用初始学习率的百分之一作为起始值再根据训练后期 loss 曲线的形态微调。如果发现每个周期末尾都出现明显性能回退可以考虑把 eta_min 稍微调高。4.3 与其他常用调度器互换时的注意事项有时候你会在不同调度器之间切换做对照实验这时容易踩一些隐形的坑。CosineAnnealingWarmRestarts和CosineAnnealingLR的适配成本最低前者只是把单次退火改成了多次。但如果从ReduceLROnPlateau切过来需要注意一个本质差异ReduceLROnPlateau是等验证集指标停止提升后再降低学习率属于“响应式”调度器CosineAnnealingWarmRestarts则是“预定式”调度器它不管验证集指标如何到了时间点就调整学习率。所以有时你会看到学习率已经拉高但指标还在下降这未必是问题而是训练节奏的体现。在代码迁移时还要留意ReduceLROnPlateau的 step 参数通常是验证集指标数值而CosineAnnealingWarmRestarts的 step 参数需要传入 epoch 或小数 epoch。把两者混用最常见的报错就是 TypeError因为参数的类型和含义完全不同。4.4 保存和恢复训练状态时最容易丢的细节断点续训是在训练长任务时几乎必然遇到的需求。这个调度器本身会把last_epoch、T_cur、T_0、T_mult这些属性保存在对象里所以长周期训练中断后要把scheduler.state_dict()一并保存恢复训练时再load_state_dict回去。不要只保存 model 和 optimizer 的状态否则调度器的周期计数丢了学习率会从头开始严重时直接把训练搞坏。我还习惯在保存 checkpoint 时记录当前的 epoch 整数和一个小数偏移值。恢复训练后如果原本是按 batch 更新的需要用这个偏移值恢复当前 batch 位置。具体做法是保存一个字典包含epoch、batch_idx、scheduler_state、optimizer_state、model_state恢复时依次加载。checkpoint { epoch: epoch, batch_idx: batch_idx, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), scheduler_state: scheduler.state_dict(), } torch.save(checkpoint, checkpoint.pt) # 恢复时 checkpoint torch.load(checkpoint.pt, map_locationdevice) model.load_state_dict(checkpoint[model_state]) optimizer.load_state_dict(checkpoint[optimizer_state]) scheduler.load_state_dict(checkpoint[scheduler_state])如果恢复时发现学习率曲线莫名其妙地断了大概率是这里的状态没配对重头检查一遍准能查到。5. 走进实战场景后的一点个人体会调这个调度器最有意思的地方在于它不是让你去死记硬背一堆参数而是逼着你去理解“训练本身是一个不断探索和精修交替的过程”。早期模型对参数位置一无所知需要偏高的学习率快速遍历中期有了初步的模型能力适合通过多次重启切入不同区域后期接近收敛需要细水长流式的低学习率打磨。用这个视角去看训练曲线很多看起来奇怪的现象都能解释清楚。以前我总觉得调参是靠运气后来把学习率曲线打印出来才发现很多次模型表现不佳其实是因为学习率在一个不合适的时机被拉到了不合适的数值。比如有次模型性能在 60 个 epoch 后骤降翻日志才发现是调度器进入了第 4 个周期T_mult 为 2 导致周期长度刚翻倍到 80训练总 epoch 数只有 100最后 20 个 epoch 恰好落在学习率快速下降的区间本来可以做更长时间的低学习率归拢却在半途中被新一轮高学习率打断了。后来把 T_0 从小调大、T_mult 从 2 调回 1问题立刻缓解。如果你也在为训练后期收敛缓慢或者结果反复波动发愁不妨先别急着换模型、调结构把现有优化器上挂一个CosineAnnealingWarmRestarts把训练曲线打印出来看一遍。很多时候问题的解法就藏在这条看似平淡的学习率曲线里。
RELATED

相关推荐

计算机博弈竞赛入门:从博弈树到α-β剪枝的算法地图

计算机博弈竞赛入门:从博弈树到α-β剪枝的算法地图

简介:面向计算机博弈竞赛选手与初学者的辅导课件,源自东北大学机器博弈研究室《计算机博弈原理与方法学概述》第一讲,系统讲解机器博弈的核心原理与方法。内容按棋类介绍与分类、基本搜索算法、剪枝策略、棋局评估、博弈树展开与分析、博弈软…

📅 2026/10/17 13:29:46
DSP2812音频播放器实战:从SPI Flash到McBSP外接DAC输出

DSP2812音频播放器实战:从SPI Flash到McBSP外接DAC输出

简介:基于TMS320F2812的音乐播放器项目,面向嵌入式系统与数字信号处理学习者,完整演示了用DSP芯片实现音乐播放的硬件与软件方案,解决从音频数据读取、解码到模拟输出的核心问题。压缩包仅约70KB,内含40个文件&#xf…

📅 2026/10/17 13:29:46
自动驾驶生态报告深度拆解:从技术架构到商业落地

自动驾驶生态报告深度拆解:从技术架构到商业落地

简介:这是一份由普华永道旗下思略特(Strategy&)出品的全自动驾驶汽车(FAV)生态系统研究报告,源自中东专业团队,面向汽车产业高管、出行服务运营商、城市规划者及政策制定者。报告系统梳理了…

📅 2026/10/17 13:24:45
MORE NEWS

更多资讯

📰

mac本地部署大模型:oMLX 项目实战与 TaoToken 统一接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Cursor 报 Unauthorized 账号封禁?把 Base URL 改到 TaoToken 的排查路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

从终端到画布:Claude Code 可视化编程生态演进与 TaoToken 接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

办公Agent工具有哪些:从任务需求看主流工具选型与TaoToken统一接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

OpenCV图像矫正:旋转与仿射变换的区别、实现与选型指南

1. 从一次图像矫正的翻车经历说起去年帮一个做文档扫描的朋友处理一批手机拍摄的表格图片,需求听起来特别简单:把拍歪的表格摆正,裁掉黑边,输出规整的矩形图。我第一反应就是用旋转,cv2.getRotationMatrix2D加cv2.warp…

📰

ASP.NET WebForms 入门实战:30分钟跑通登录页

简介:本资源是面向零基础初学者的《ASP.Net入门经典》系统性学习包,聚焦Web开发核心技能培养,帮助开发者快速掌握ASP.Net Web Forms、MVC架构、Razor语法、页面生命周期、数据绑定及IIS部署等关键能力,适用于高校计算机专业学生、…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬