训练神经网络这件事,我踩得最多的坑就是学习率不会调。网络结构可以照搬,数据可以按部就班地准备,但同样的模型,学习率差一个数量级,结果就能从收敛变成发散,甚至完全不收敛。这背后最常用的解题手段,就是Pytorch学习率衰减(learning rate decay)。这个技巧说穿了并不复杂:训练后期把学习率逐步降下来,让模型在一个更精细的尺度上打磨参数。可它涉及到的调度器多得能看花眼,参数也是环环相扣,用错了往往比不用更糟。这篇文章我会把Pytorch里常见的学习率衰减策略逐个拆开,讲清楚每个调度器在什么场景下用、参数怎么设置、训练循环里应该怎么写,最后再把训练中踩过的坑一并整理出来。无论你是刚入门的学生,还是已经训练过若干模型但学习率调整全靠试的工程师,这篇都能给你一套可以直接上手的用法。
1. 学习率衰减的核心思路与设计动机
1.1 为什么训练后期必须把学习率降下来
先理解优化器里的学习率到底在做什么。梯度下降更新参数,可以类比成在山坡上找最低点:学习率就是每次迈出的步子大小。步子大了,能快速跨过很多局部凹陷,但等你接近最低点的时候,它会在谷底附近来回弹跳,永远踩不准;步子小了,训练又慢得让人失去耐心。
固定学习率训练到底会出现什么问题?最典型的现象是:loss曲线前期降得很快,后期陷入平台,死活下不去。你放大一看,loss在一个平坦区域来回摩擦,每一步更新都被下一步抵消掉。这其实说明了模型已经进入一个相对优秀的局部区域,但以当前步长已经无法继续精确定位了。此时如果不把学习率收小,训练就只能停滞在这里,直到训练结束。
学习率衰减做的一件事,就是把训练过程拆成两段:前期用较大的学习率快速逼近目标区域,后期用小学习率在局部做精细搜索。数学上的直觉也很直接:参数更新公式可以简写为 w_{t+1} = w_t - η·g_t,当 w 已经接近最优解时,梯度 g 的范数会变小。如果 η 还是原来那么大,更新就可能在最优点附近来回横跳,loss 无法继续下降。把 η 逐步调小,相当于让每一步都落得更稳、更接近谷底。
实际训练中还有一层重要含义:衰减可以压住随机梯度噪声。即便同一个模型,换成不同批次的数据,梯度本身就带随机扰动。后期 loss 已经很低时,这种扰动会影响收敛精度。主动降低学习率,就是强行把这个噪声的抖动幅度压下去,让模型在最优解附近尽可能稳定。
这里要澄清一个误区:很多人觉得用了 Adam 就没必要再做学习率衰减。Adam 确实会根据梯度的历史动态调整每个参数的更新幅度,看公式像是自带自适应学习率,但它调整的是归一化后的系数,不等于你设置的全局学习率。全局学习率依然控制着整体收敛尺度,所以实际项目里“Adam + 学习率衰减”才是常见配置,Adam只能减少你手动初始化学习率的难度,不能完全替代全局衰减策略。
1.2 常见衰减策略横向对比
学习率衰减并不是只有“训练到一半突然除以10”这一种做法。在 torch.optim.lr_scheduler 里,官方提供了非常多的调度器,每一种策略背后的假设和适用场景差别很大。我把最常用的一批放在一起,先看一张速查表。
| 策略 | 衰减方式 | 核心参数 | 典型场景 | Pytorch 类 |
|---|---|---|---|---|
| 固定步长衰减 | 每 N 个 epoch 乘一次 gamma | step_size、gamma | 视觉任务、CNN 分类 | StepLR |
| 指定节点衰减 | 到达指定 epoch 列表时乘 gamma | milestones、gamma | 有明显阶段划分的长线实验 | MultiStepLR |
| 指数衰减 | 每个 epoch 都乘 gamma | gamma | 平滑衰减、快速验证小模型 | ExponentialLR |
| 余弦退火 | 按余弦曲线平滑降到最低 | T_max、eta_min | 通用无脑默认选择 | CosineAnnealingLR |
| 带重启余弦退火 | 降到极低值后跳回大学习率重新探索 | T_0、T_mult、eta_min | 超参数搜索、逃离局部极值 | CosineAnnealingWarmRestarts |
| 指标自适应衰减 | 验证指标连续不降时乘 factor | mode、factor、patience | NLP、推荐、验证集信号明确 | ReduceLROnPlateau |
| 自定义映射 | 自己写一个 epoch 到倍率的函数 | lr_lambda | warmup、组合策略、特殊曲线 | LambdaLR |
| 循环式学习率 | 在大小学习率间循环往返 | base_lr、max_lr | GAN、摆脱鞍点 | CyclicLR / OneCycleLR |
一句话的挑选原则:没有特别理由时,CosineAnnealingLR 是现代实践里最稳的默认选择,几乎适用于所有监督训练任务。希望精确控制训练阶段,用 MultiStepLR。任务有可靠的验证指标且你想自动化调节,用 ReduceLROnPlateau。至于 LambdaLR,它更像是给你自己拼方案用的积木,灵活性极高,但也很容易把自己绕进去。
2. 实操关键:Pytorch里的学习率调度器怎么选
2.1 先搞懂 scheduler 和 optimizer 的关系
刚开始用 Pytorch 的调度器时,很多人会混淆 optimizer.step() 和 scheduler.step(),必须把这两者的角色分清楚。optimizer.step() 是根据当前梯度更新模型参数,真正在做优化;scheduler.step() 只负责修改优化器里 param_groups 中的 lr 值,它不碰模型参数。
机制上,lr_scheduler 在创建时会拿到 optimizer 的引用,之后每次调用 step(),它按预设规则重新计算当前 lr 并写回。你要知道,每个优化器内部都维护了 param_groups,里面每个 group 都有一个 lr 字段。默认情况下你只有一个 group,想让不同层使用不同学习率时,才会拆成多个 group 分别设置。无论拆成几个组,调度器默认都会作用到所有 group 上,除非你用 LambdaLR 之类的接口单独处理。
实际写训练循环时,我见过最多的问题就是把 scheduler.step() 放在了错误的位置。常规做法是:每个 epoch 完成训练和验证之后,调用一次 scheduler.step()。如果你的调度器是按 step_size 或 T_max 的 epoch 数来设计的,你却在每个 batch 里也调一次,那学习率会在一个 epoch 内被飞速消耗完,后期早就变成超小学习率,等同于模型提前冻结。
还有一类调度器需要按 batch 级别调用,典型就是 CyclicLR 和 OneCycleLR。它们的整体规划就是基于总 batch 步数的,放到 epoch 粒度上要么曲线不对,要么干脆失效。所以拿到一个调度器,第一步就是看它的文档要求你多久调用一次 step,是按 epoch 还是按 iteration。
2.2 逐个拆解常用调度器
先看 StepLR。它是最直白的衰减策略:每经过 step_size 个 epoch,学习率乘一次 gamma,也就是 lr = lr * gamma。参数就两个,step_size 和 gamma。经典用法是 CNN 训练 90 个 epoch 时,每 30 个 epoch 让学习率从 1e-3 降到 1e-4,再降到 1e-5。这种策略的好处是可控、可预期,实验复现起来非常容易。缺点也很明显:节点设置依赖经验,step_size 和 gamma 都得你自己定,设得不合适,要么衰减过早导致后期鞭长莫及,要么衰减过晚导致前期浪费大量时间。
MultiStepLR 是 StepLR 的改进版,它允许你指定一个里程碑列表,比如 milestones=[60, 120, 160],到第60、120、160个epoch时各乘一次 gamma。视觉任务的经典训练规范里经常能读到这种写法,因为研究者已经清楚在哪个阶段数据增强策略、评价指标变化,就在那个时间点把学习率调低一档。这种按需定点的方式最贴近实验人员的心智模型,但前提是你对任务足够熟悉。
ExponentialLR 则让学习率在每个 epoch 都乘 gamma。第 t 个 epoch 时 lr = base_lr * gamma^t。如果 gamma 取 0.95,100 个 epoch 后的学习率大约只有初始值的 0.6%;如果 gamma 取 0.9,30 个 epoch 后就降到初始值的 4%。它的衰减非常平滑,适合在小型模型或者快速验证实验里观察不同学习率阶段的行为。但如果训练轮次特别多,它衰减的速度会非常快,模型很可能在中后期就已经步长过小、学不动东西了。
CosineAnnealingLR 是当前我认为最省心的默认选项。它让学习率从初始值按照余弦曲线平滑下降到 eta_min,T_max 表示完成整个周期的 epoch 数。数学形式可以理解成:lr = eta_min + (base_lr - eta_min) * (1 + cos(π * epoch / T_max)) / 2。前期下降得比较平缓,后期快速逼近最小值,因此它既保证前期有足够探索,又保证后期精细收敛。你把 T_max 设成总 epoch 数,eta_min 设成 1e-6 或者 1e-8,剩下基本不用管。很多研究团队现在干脆把“CosineAnnealingLR + 适当的初始学习率”作为默认训练配方。
CosineAnnealingWarmRestarts 在余弦退火的基础上加入了热重启机制:学习率降到低点后,不是一直停在低点,而是跳回一个较高的学习率重新开始新一轮余弦衰减。T_0 是第一个周期长度,T_mult 控制每个周期是否成倍拉长。这种策略常用于超参数搜索、复杂损失面的探索,因为突然间的大学习率有助于跳出尖锐的局部极值。但它也会让训练曲线出现周期性波动,不符合“永远往下走”的直觉,因此使用时要明确自己需要这种探索性。
ReduceLROnPlateau 和前几个都不太一样。它不按固定的 epoch 时间表衰减,而是根据验证指标的表现动态触发:当连续 patience 个 epoch 验证指标都没有改善时,学习率乘以 factor。mode='min' 表示指标越低越好,mode='max' 表示越高越好。使用它时,step() 方法必须传入指标数值,例如 scheduler.step(val_loss)。这是它最容易出错的地方:如果忘了传 metrics,代码不会报错,但衰减可能完全不触发,训练看起来一切正常,实际学习率却一直没变。
LambdaLR 则提供了最大的自由度。你给它一个函数 lr_lambda,参数是当前 epoch,返回值是初始学习率的倍率。比如我做 warmup 加衰减时,就会用 LambdaLR 自己拼一个分段函数。但在实际项目中,写死 LambdaLR 也会带来维护负担,尤其是函数片段多了以后,读代码的人很难一眼看懂衰减逻辑。
2.3 大模型训练别漏了 warmup,它是衰减的前置步骤
很多新手只关注衰减,却忽视了一个特殊阶段:warmup。尤其训练 Transformer、大模型或者数据量大的任务时,训练一开始梯度波动非常剧烈,如果直接把学习率加到目标值,很容易出现 loss 爆炸甚至 NaN。warmup 的做法是让学习率从很小的值线性增长到目标值,一般只持续几个 epoch,然后再让调度器开始衰减。
Pytorch 里可以用 LambdaLR 自己实现,也可以用官方提供的 LinearLR 加 SequentialLR 组合。我常用下面这种写法完成“前 5 个 epoch 线性升温 + 之后余弦退火”的整体调度:
import torch from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR, SequentialLR optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) warmup = LinearLR(optimizer, start_factor=0.01, total_iters=5) decay = CosineAnnealingLR(optimizer, T_max=95, eta_min=1e-6) scheduler = SequentialLR(optimizer, schedulers=[warmup, decay], milestones=[5])这里的语义是:前 5 个 epoch,学习率从初始 lr 的 1% 线性升到 100%;第 6 个 epoch 开始走余弦退火。注意,SequentialLR 里的 milestones 指的是切换调度器的 epoch 节点,5 表示第 5 个 epoch 结束后从 warmup 切到 decay。这种组合对大模型尤其友好,原因在于充分 warmup 能让模型先处理掉数据分布中的异常噪声,再进入正常的搜索阶段。
3. 实操:在Pytorch中完整实现学习率衰减
3.1 最小但正确的训练循环骨架
一段代码胜过千行文档。我给你看一下我练习时最常用的最小训练循环骨架。重点不是模型本身,而是 scheduler 在整个训练流程中的位置。
import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR EPOCHS = 100 model = nn.Sequential( nn.Linear(32, 64), nn.ReLU(), nn.Linear(64, 1) ) optimizer = optim.AdamW(model.parameters(), lr=1e-3) scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6) def train_one_epoch(): # 这里只示意一个 basic loop,真实项目中替换成你自己的训练逻辑 model.train() for batch in dataloader: x, y = batch optimizer.zero_grad() loss = nn.functional.mse_loss(model(x), y) loss.backward() optimizer.step() for epoch in range(EPOCHS): train_loss = train_one_epoch() val_loss = evaluate(model, val_dataloader) # 实际上你需要定义这个函数 scheduler.step() current_lr = optimizer.param_groups[0]['lr'] print(f"epoch={epoch:03d} train_loss={train_loss:.4f} val_loss={val_loss:.4f} lr={current_lr:.2e}")这里需要注意几个细节。
scheduler.step() 必须放在 epoch 循环体内部,并且通常是在验证指标计算之后调用。这样当前 epoch 的训练和验证都运行在同一个学习率下,之后再调整下一个 epoch 用新的学习率。
current_lr 必须从 optimizer.param_groups[0]['lr'] 里读取,而不是你自己维护一个变量。因为调度器直接修改的是 optimizer 内部字段,你自己存的数字很可能会跟实际不一致,排查问题时会非常迷惑。
val_loss 的引入不是摆设。学习率衰减策略的效果本来就应该通过验证指标来看,只看训练 loss 容易被过拟合假象骗到。
如果你用的是 ReduceLROnPlateau,那最后一个 scheduler.step() 就不能写成无参形式,而是 scheduler.step(val_loss)。
3.2 关键参数的选择与计算思路
调度器参数看起来只是几个数字,但选不好就是整个实验的灾难。常见目标:训练 100 个 epoch,一批数据 64 条,总样本大约 5 万条。
如果选择 CosineAnnealingLR,直接 T_max=100,eta_min=1e-6,不需要额外思考。这也是我推荐它的原因之一:参数最少,效果稳定。
如果选择 StepLR,就需要自己规划。假设从 1e-3 开始,总共希望经历 3 次数量级下降,最终到 1e-6 附近。那么 step_size 可以选 30,gamma 选 0.1,这样学习率在 epoch 30、60、90 各降一个数量级。训练初期 30 个 epoch 用 1e-3 探索,中期 30 个 epoch 用 1e-4 精细调整,后期 40 个 epoch 在 1e-5 附近收尾。
如果选择 MultiStepLR,参考广泛使用的规范:总 200 epoch,milestones=[60, 120, 160],gamma=0.1。这种设置的前 60 个 epoch 属于充分探索,第 60 到第 120 个 epoch 属于精细拟合,最后 40 个 epoch 用来打磨最终决策边界。
如果选择 ReduceLROnPlateau,参数就变成 factor、patience 和 mode。factor 通常取 0.5 或 0.1,patience 取 10 到 15 个 epoch。太小的 patience 会让学习率下降得过于频繁,模型还来不及在验证集上体现改善就又被降学习率。太大的 patience 又会浪费训练时间。我一般从 10 开始,结合验证指标曲线微调。
3.3 可视化验证学习率衰减是否生效
我强烈建议你在每次实验里都保存并绘制学习率曲线。这种习惯能省下大量排查时间。很多看起来“训练异常”的问题,实际上是学习率曲线和自己预期不一致,画出来一眼就能看出来。
记录和绘制的方法很简单:
import matplotlib.pyplot as plt lr_history = [] for epoch in range(EPOCHS): train_loss = train_one_epoch() val_loss = evaluate(model, val_dataloader) scheduler.step() lr_history.append(optimizer.param_groups[0]['lr']) plt.plot(range(EPOCHS), lr_history) plt.xlabel('epoch') plt.ylabel('learning rate') plt.yscale('log') plt.grid(True, which="both", ls="--", alpha=0.5) plt.show()这里用对数坐标很关键。学习率衰减经常是数量级级别的变化,如果用线性坐标,前期的 1e-3 和后期的 1e-6 根本显示不出中间过程,整条曲线会像一条断崖直线,看不出衰减细节。改成对数坐标后,每一步衰减的曲线形状看得清清楚楚。
画完之后,你要问自己几个问题:学习率是否按预期节奏下降了?在什么 epoch 出现了台阶或突变?训练末期学习率是否真的达到了目标量级?如果学习率曲线显示某一段学习率已经低于 1e-7,那模型基本就处于休眠状态,损失不再变化也正常。
3.4 保存与恢复训练时调度器状态
训练中途被打断、先保存 checkpoint 再重启继续训练,是再常见不过的操作。很多人会保存模型和优化器,却不保存调度器,导致恢复后学习率重置回初始值,破坏整个训练节奏,后续所有实验对比都失真。
正确的做法是把 scheduler 的状态也放进 checkpoint 中:
checkpoint = { 'epoch': epoch, 'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'scheduler': scheduler.state_dict(), } torch.save(checkpoint, f'ckpt_{epoch}.pt') # 恢复训练时 ckpt = torch.load(f'ckpt_{epoch}.pt') model.load_state_dict(ckpt['model']) optimizer.load_state_dict(ckpt['optimizer']) scheduler.load_state_dict(ckpt['scheduler'])这里尤其要注意:恢复训练后,当前学习率并不会重新从初始值开始,而是立刻恢复到保存时刻的学习率。这就是 load_state_dict 帮我们保存下来的价值。如果你忘了 scheduler.state_dict(),那么恢复后学习率会回到初始大值,模型极大概率直接把原有的训练成果扰乱了。
如果你用的是 ReduceLROnPlateau,同样要保存和恢复 scheduler 状态。因为它内部记录了“已经连续多少轮没有改善”“当前学习率是多少”等状态,丢失这些信息会让调度行为完全失去连续性。
4. 训练实战中的常见问题与排查技巧
4.1 明明设置了衰减,学习率却没有变化
这个问题我见过太多次。第一反应是检查代码中是否真正调用了 scheduler.step()。很多人创建了 scheduler 对象,却忘了在 epoch 循环里调用 step(),导致 scheduler 一直停留在初始状态。这种情况不会报任何错误,训练照常进行,只是学习率始终不变。
第二个常见问题:调度器按 epoch 设计,你却在 batch 循环里调用。如果你计算过,会发现一个 epoch 内有几百个 batch,几百次 scheduler.step() 叠加后,学习率早就被压到接近零,后续训练等同于冻结。这类问题画一次学习率曲线就能立刻发现,因为曲线会呈现极陡的下降,而不是平滑的余弦或台阶。
第三个问题是加载 checkpoint 后恢复训练时,忘记加载 scheduler 状态。这样学习率重新从初始值开始,而模型参数已经是中后期状态,大学习率会把训练曲线搅碎。排查时先打印十来个 epoch 的学习率,看看是否在往下走。如果第一个 epoch 就是初始大值,那大概率就是 scheduler 恢复缺失。
还有一类隐蔽问题:你设置了多个 scheduler,后面创建的 scheduler 覆盖了前面 scheduler 修改过的学习率。如果排查了很久都找不到原因,试着全局搜索自己创建了几个 lr_scheduler 实例,把多余的全删掉。
4.2 训练震荡、loss 不降:优先怀疑学习率
训练震荡,首先怀疑的是初始学习率太高。典型表现是 loss 曲线在某个区间上下抖动幅度大,或者周期性出现尖峰。此时把初始学习率调小一个数量级重新跑一小段,比如 20 个 epoch,通常就会缓解。不要一上来就改网络结构或加正则化,先把学习率这个变量排除掉。
另一种情况:loss 前期下降后稳定在一个并不低的数值,然后停止移动。这时候调度器的衰减可能过于激进,比如 gamma=0.1 且 step_size 很小,导致模型在还没有充分探索时就进入微小步长阶段。这时候应该调整衰减节奏,比如把 gamma 调到 0.3,或者把 step_size 拉长,让衰减更缓和。
如果是 loss 变成 NaN,那大概率是初始学习率太大,梯度发生爆炸。这时除了调低学习率,我还会建议先跑 20 到 30 个 batch,把梯度范数打印出来看看。梯度范数如果动不动就超过 100,说明学习率必须下降;如果梯度范数正常,那问题可能在数据预处理上。
这里有一个我在实际项目中形成的排查顺序:先可视化学习率曲线,确认衰减逻辑没问题;再抓前 20 个 epoch 的 loss 和 lr 对应变化;最后再动超参数。不要一上来把学习率调度器换掉,那样你连原有衰减曲线是否正确都不知道,只会越调越乱。
4.3 不同任务场景的调度策略参考
不同任务对学习率衰减的敏感程度不一样。我把常见任务场景整理成一张参考表,方便你直接抄作业,然后再根据自己实验结果微调。
| 任务类型 | 推荐策略 | 初始学习率参考 | 备注 |
|---|---|---|---|
| 图像分类 CNN | CosineAnnealingLR | 1e-3 到 3e-3 | T_max=总epoch,eta_min=1e-6 |
| 图像分类经典论文复现 | MultiStepLR | 1e-1 到 1e-2 | 记得配合某种归一化和数据增强 |
| Transformer / NLP 预训练微调 | warmup + CosineAnnealingLR | 5e-5 到 3e-4 | 微调时初始学习率一定要小 |
| 目标检测 | MultiStepLR 或 CosineAnnealingLR | 1e-4 到 1e-3 | 多尺度训练时慎用太激进的衰减 |
| GAN 类模型 | CyclicLR / OneCycleLR | 1e-4 到 1e-3 | 循环学习率有助于探索均衡 |
| 强化学习策略网络 | ReduceLROnPlateau | 1e-3 到 1e-4 | 环境噪声大,按指标触发更稳 |
这个表只是一个出发点。任何一次实验,学习率衰减策略的选择都要结合数据规模、模型深度、总训练周期来调整。比如你的数据只有 1 万张图,训练 30 个 epoch 就想看到结果,那 CosineAnnealingLR 的 T_max 就别写到 100,直接跟着你的训练计划走。
4.4 一个真实的排查记录
这里分享一个我上周才处理过的问题,完全符合多数人的经历。某开发者在训练一个图像分类 demo 时报告 loss 不降,曲线前 10 个 epoch 从 2.1 降到 1.6,然后彻底不动了,验证准确率也在 65% 附近来回波动。
我先让他打印验证时实际使用的 lr,结果发现当前 lr 是 5e-3,也就是他设定的初始值,调度器设的是 MultiStepLR,milestones=[30, 60],当前 epoch 才 15,所以学习率根本没有开始衰减。这说明问题不在于调度器,而在于初始学习率太大,模型已经停在了一个不理想的平缓地带。
听他描述验证集曲线上下波动时,我第一反应就是 lr 余量不足。经过调整:初始 lr 改为 1e-3,调度器换成 CosineAnnealingLR,T_max 设为 60,eta_min 设为 1e-6。同样训练到第 15 个 epoch,loss 已经降到 1.3 附近,验证准确率开始平稳上升。
这个例子的教训很简单:不要因为“我设了衰减”就认为一切正常。衰减是一种节奏控制工具,它救不了初始学习率选错的病。你先要确认模型在当前学习率下能稳定下降,再让衰减在训练后期发力。当前 lr 如果一开始就让 loss 波动,后面无论如何衰减都只能看着动荡。
4.5 调试学习率的一些个人经验
最后分享几条我长期训练积累下来的经验。
第一,每个实验都用一个独立的 log 文件记录 lr。无论是用自写 print 还是现成的日志库,至少要把每个 epoch 的实际 lr 存下来。这样哪怕几个月后再翻看实验记录,也能快速定位当时训练曲线每个阶段到底用了什么学习率。
第二,默认从 CosineAnnealingLR 起步,除非你有明确反对理由。它的好处是单参数少、行为可预期,而且基本不会出现“衰减过早导致后期冻结”的反直觉问题。先把问题模型调通,再去尝试 StepLR 和 MultiStepLR 的精细手动控制。
第三,改变衰减策略时,固定其他所有超参数。很多实验对比失真,是因为一边改 lr 调度器一边又换了数据增强、网络结构或 batch size,结果根本无法定位导致提升的具体原因。你会得到很多“看起来差不多”的结论,却不知道它们是哪个变量带来的。
第四,scheduler 不是越复杂越好。我见过有人把 warmup、cosine、plateau 全拼到一个 lambda 里,最后自己都说不清曲线长什么样。学习率衰减的本质是给训练一个清晰的节奏,如果你自己都看不懂这个节奏,那就简化它。
学习率衰减不是一个高深技巧,但它是训练体系里最容易埋雷的一环。调好衰减策略,loss 曲线的尾部会漂亮地收窄,模型也能更接近真正的好结果。先掌握原理,再熟练用 Pytorch 的调度器,最后别忘记随时记录和可视化。训练这件事,省去的每一步排查时间,最后都会以另一种方式还回来。