☰
Warmup预热学习率:解决模型训练开局崩溃与收敛慢的实战指南
2026/10/15 23:15:31 网站建设 项目流程

1. 从一次训练翻车说起:为什么你的模型总在开局就崩

刚接触深度学习那会儿,我踩过一个特别典型的坑。当时在做一个图像分类的小项目,数据集不大,大概两万张图,用的是一个中等规模的卷积网络。我按照习惯把学习率设成 0.01,优化器选了 SGD,信心满满地按下回车开始训练。结果前两百个 step 损失值直接飙到 NaN,梯度爆炸得一塌糊涂。我以为是数据有问题,查了半天归一化、标签编码,全都没毛病。后来把学习率降到 0.0001,训练倒是稳了,但收敛速度慢得让人抓狂,跑了整整一天准确率还在 30% 附近晃悠。

这个两难局面——学习率大了开局就炸,小了又慢得像蜗牛——几乎是每个做模型训练的人都会遇到的经典问题。而解决它的核心手段之一,就是warmup 预热学习率。说白了,warmup 就是在训练刚开始的那一小段时间里,让学习率从一个很小的值逐步爬升到设定的初始学习率,而不是一上来就用满速跑。这个思路听起来简单,但背后的逻辑、具体的实现方式、不同场景下怎么选参数,里面的门道比想象中多得多。

这篇文章适合所有正在做模型训练的人看——不管你是刚入门的新手,还是已经调过不少模型但总觉得收敛不够理想的老手。我会从 warmup 要解决的根本问题讲起,拆解几种主流的预热策略,给出可以直接用的代码实现,再分享一些我在实际项目中踩过的坑和总结出来的调参经验。核心关键词就一个:warmup 预热学习率,围绕它把该讲的东西讲透。

2. 训练初期到底发生了什么:warmup 要解决的根本矛盾

2.1 随机初始化带来的"高梯度陷阱"

要理解 warmup 为什么有用,得先搞清楚训练刚开始时网络内部是什么状态。模型初始化的时候,所有参数都是随机生成的,这时候网络对数据的"理解"基本为零。你喂进去一批数据,前向传播算出来的输出和真实标签之间的差距非常大,反向传播回来的梯度自然也就非常大。

这时候如果学习率设得比较高,参数更新步长就会很大。大梯度乘以大学习率,参数一下子被推到离最优解很远的地方,甚至可能推到损失函数曲面上的"悬崖"区域。一旦进入这种区域,下一步的梯度会更大,形成正反馈循环,几轮下来损失就炸了。这就是我前面说的"开局就崩"的本质原因。

用一个生活化的类比:想象你在一座完全陌生的山上找最低点,但你的眼睛被蒙住了,只能靠脚下的坡度来判断方向。如果一开始你就迈大步走,很可能一步跨过山谷直接冲到对面的陡坡上,然后越走越偏。warmup 的做法相当于让你刚开始小步试探,等大致摸清了地形再逐渐加大步伐。

2.2 自适应优化器也救不了开局

有人可能会说,我用 Adam 或者 RMSProp 这类自适应优化器,它们不是能自动调整每个参数的学习率吗?确实,自适应优化器在一定程度上能缓解这个问题,因为它们在训练初期会对梯度做归一化处理。但注意,Adam 在第一步的时候,二阶矩估计是基于单个梯度的,方差极大,这时候的有效学习率其实非常不稳定。

更关键的是,Adam 里面有一个偏差校正机制,在训练初期校正后的学习率可能比你设定的大得多。我实测过,用 Adam 默认参数 lr=0.001,不加 warmup,在某些 Transformer 结构上前几百步的损失波动非常剧烈。加了 warmup 之后,曲线明显平滑了很多。所以即便是自适应优化器,warmup 依然有它的价值。

2.3 Batch Normalization 的缓冲作用也有边界

另一个常见的说法是,有了 Batch Normalization(BN),训练初期的不稳定性已经被大大缓解了。BN 确实能通过归一化每层的输入分布来稳定训练,但它也不是万能的。BN 本身也有需要学习的参数 gamma 和 beta,这些参数在初始化时也是随机的。而且当 batch size 比较小的时候,BN 的统计量估计不准,稳定性会打折扣。

我在一个小 batch size(大概 8 左右)的语义分割项目里就遇到过这种情况:加了 BN 但没加 warmup,训练前 500 步的 loss 曲线像心电图一样上下跳。后来加了 1000 步的线性 warmup,曲线立刻就平顺了。所以 BN 和 warmup 不是替代关系,而是互补关系。

3. 几种主流预热策略的拆解与对比

3.1 线性预热:最常用也最稳妥的选择

线性预热(Linear Warmup)是目前最主流的做法,逻辑非常直白:从第 0 步到第 W 步(W 就是预热步数),学习率从 0 或者一个极小的值线性增加到设定的初始学习率。用公式表示就是:

lr(t) = base_lr * (t / W) , 当 t < W lr(t) = 后续调度策略 , 当 t >= W

这里的 base_lr 是你希望达到的峰值学习率,W 是预热步数。比如你设 base_lr=0.001,W=2000,那么第 500 步时学习率就是 0.00025,第 1000 步时是 0.0005,到第 2000 步才达到 0.001。

线性预热的优点是行为可预测,调参直观。你只需要决定两个东西:峰值学习率和预热步数。而且它和后续的学习率调度策略(比如余弦退火、阶梯下降)衔接得很自然。我在大多数项目里默认就用线性预热,除非有特别的理由才会换别的。

3.2 指数预热:更平滑的起步方式

指数预热(Exponential Warmup)让学习率按指数曲线从很小的值增长到峰值。公式大概是:

lr(t) = base_lr * (t / W) ^ p , 当 t < W

其中 p 是一个指数参数,通常取 2 到 4 之间。当 p=1 时退化成线性预热,p>1 时前期增长更慢、后期增长更快。

这种策略的好处是在训练最开始的几十步里,学习率被压得非常低,给网络更多时间来"适应"数据分布。坏处是如果 p 设得太大,预热阶段会显得拖沓,前期几乎没怎么学。我一般只在训练非常深、非常容易发散的网络时才会考虑指数预热,而且 p 通常不超过 2。

3.3 余弦预热:和余弦退火配套使用

余弦预热(Cosine Warmup)的思路是让学习率按照余弦曲线的前半段从低到高爬升。它和余弦退火(Cosine Annealing)天然配套,整个训练过程的学习率曲线就是先升后降的一个完整余弦波。

具体来说,预热阶段的学习率是:

lr(t) = base_lr * 0.5 * (1 - cos(pi * t / W)) , 当 t < W

这种方式的起步比线性预热更柔和,因为余弦函数在 0 附近的导数接近 0,学习率增长非常缓慢。到了预热后期,增长又逐渐加快。整个过渡非常平滑,没有突兀的拐点。

我在 Transformer 类模型的训练中比较喜欢用这种策略,因为它和后续的余弦退火衔接得浑然一体,整个训练过程的学习率曲线看起来非常优雅。不过它的调参空间比线性预热稍大一些,需要同时考虑预热步数和总训练步数的比例关系。

3.4 三种策略的横向对比

策略起步速度调参难度适用场景与后续调度的衔接
线性预热匀速低大多数场景通用好,几乎百搭
指数预热先慢后快中极深网络、易发散场景一般,需要手动对齐
余弦预热先极慢后快中配合余弦退火使用极好,天然一体

选哪种没有绝对的对错,关键看你的具体场景。如果你是刚开始用 warmup,我建议从线性预热入手,把峰值学习率和预热步数这两个参数摸清楚之后,再尝试其他策略。

4. 预热步数怎么定:一个被大多数人忽略的关键参数

4.1 预热步数不是拍脑袋决定的

很多人设 warmup 步数的时候就是随手填一个 1000 或者 2000,问他为什么,答曰"网上都这么写"。但实际上预热步数的选择应该和你的总训练步数、batch size、模型规模都有关系。

一个比较通用的经验法则是:预热步数占总训练步数的 5% 到 10% 左右。比如你总共要训练 10000 步,那预热 500 到 1000 步是比较合理的。如果总步数只有 2000 步,预热 1000 步就太长了,网络还没来得及好好学习就进入衰减阶段了。

但这个比例也不是铁律。如果你的 batch size 特别大(比如 4096 甚至更大),梯度估计的方差比较小,预热可以短一些。反过来,batch size 很小的时候,梯度噪声大,预热需要长一些来稳定训练。

4.2 从损失曲线反推预热是否合适

我判断预热步数设得对不对,主要看训练前期的损失曲线形态。如果预热步数合适,损失曲线应该是平滑下降的,没有明显的尖峰或者平台期。如果预热太短,你会看到损失在预热结束后突然出现一个跳变或者抖动加剧。如果预热太长,损失曲线在前半段会显得过于平缓,像是被"压住"了。

具体操作上,我通常会在正式训练之前先跑一个短的试验:用完整的预热设置跑 500 到 1000 步,把损失曲线画出来看看。如果曲线形态不对,调整预热步数再试。这个试验成本很低,但能避免正式训练跑歪了浪费大量时间。

4.3 不同模型规模下的预热步数参考

根据我自己的项目经验,整理了一个粗略的参考表:

模型规模参数量级建议预热步数说明
小型 CNN< 1M200-500网络浅,梯度问题不严重
中型 ResNet10M-50M500-2000最常见的区间
大型 Transformer100M+2000-10000越深越需要长预热
微调场景任意总步数的 5%-10%预训练权重已经很好,预热可以短

注意这个表只是参考,实际还要结合你的数据量、batch size、优化器选择来调整。比如你用 LAMB 优化器配合超大 batch,预热步数可能需要按比例放大。

5. 代码实现:从零手写一个带 warmup 的学习率调度器

5.1 PyTorch 版本的完整实现

下面是一个我常用的 PyTorch 学习率调度器实现,支持线性预热加余弦退火,可以直接拿去用:

import math from torch.optim.lr_scheduler import _LRScheduler class WarmupCosineScheduler(_LRScheduler): def __init__(self, optimizer, warmup_steps, total_steps, base_lr=1e-3, min_lr=1e-6, last_epoch=-1): self.warmup_steps = warmup_steps self.total_steps = total_steps self.base_lr = base_lr self.min_lr = min_lr super().__init__(optimizer, last_epoch) def get_lr(self): step = self.last_epoch if step < self.warmup_steps: # 线性预热阶段 scale = step / max(1, self.warmup_steps) lr = self.base_lr * scale else: # 余弦退火阶段 progress = (step - self.warmup_steps) / \ max(1, self.total_steps - self.warmup_steps) progress = min(1.0, progress) lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * \ (1 + math.cos(math.pi * progress)) return [lr for _ in self.optimizer.param_groups]

这个实现有几个细节值得说明。第一,预热阶段我用max(1, self.warmup_steps)来防止除以零。第二,余弦退火阶段我把 progress 截断在 1.0,避免超出总步数后学习率反弹。第三,min_lr 设了一个很小的值而不是 0,因为学习率完全为 0 会导致参数完全不更新,在某些场景下可能不是我们想要的。

5.2 用 LambdaLR 快速实现

如果你不想写一个完整的类,PyTorch 的 LambdaLR 也能快速搞定:

from torch.optim.lr_scheduler import LambdaLR def warmup_lambda(warmup_steps, total_steps): def fn(step): if step < warmup_steps: return step / max(1, warmup_steps) progress = (step - warmup_steps) / max(1, total_steps - warmup_steps) return max(0.0, 0.5 * (1 + math.cos(math.pi * min(1.0, progress)))) return fn scheduler = LambdaLR(optimizer, lr_lambda=warmup_lambda(1000, 10000))

这种方式更简洁,适合快速实验。但它的灵活性不如自定义类,比如你想在预热阶段用指数增长而不是线性增长,就得改 lambda 函数。

5.3 一个容易踩的坑:step 的调用时机

不管你用哪种实现,有一个坑我踩过不止一次:scheduler.step() 的调用位置。在 PyTorch 中,如果你用的是按 step 更新的调度器,scheduler.step() 应该在 optimizer.step() 之后调用。但如果你用的是按 epoch 更新的调度器,调用位置又不一样。

更麻烦的是,有些训练框架会在每个 batch 自动调用 scheduler.step(),有些则需要你手动调用。如果你不小心调用了两次,学习率会以两倍速度变化,预热阶段直接缩短一半。我在一个分布式训练的项目里就遇到过这个问题,因为每个进程都调了一次 scheduler.step(),导致实际预热步数只有设定值的一半。后来改成只在主进程调用才解决。

提示:训练前先打印几步的学习率值,确认调度器行为符合预期。这个检查只需要几秒钟,但能避免几个小时的无效训练。

6. 实战中的调参经验与避坑指南

6.1 预热峰值学习率怎么选

warmup 的峰值学习率(也就是 base_lr)是整个训练中最重要的超参数之一。我的经验是,先用一个较小的学习率(比如 1e-4)跑几百步确认模型能正常收敛,然后逐步放大学习率,直到损失曲线在预热结束后开始出现明显抖动,再往回退一点。这个"临界点"附近的学习率通常就是比较优的峰值。

另外一个技巧是参考同类模型在类似数据集上的公开配置。比如你在做 ImageNet 分类,ResNet-50 的经典配置是 SGD lr=0.1、batch size=256、预热 5 个 epoch。你可以把这个作为起点,根据自己的 batch size 做线性缩放。注意是线性缩放,不是平方根缩放——这是 Facebook 在一篇大规模训练论文里提出的规则,我实测下来确实比平方根缩放更靠谱。

6.2 warmup 和梯度裁剪的配合

warmup 和梯度裁剪(Gradient Clipping)是一对好搭档。梯度裁剪能在单步梯度异常大时把更新幅度限制住,而 warmup 从学习率层面降低初期的更新幅度。两者结合,训练初期的稳定性会好很多。

我通常会把梯度裁剪的阈值设在 1.0 到 5.0 之间。设太小会限制正常的梯度更新,设太大又起不到保护作用。具体值可以通过观察训练前几百步的梯度范数分布来确定:如果大部分梯度范数在 0.5 到 2.0 之间,那阈值设 5.0 就比较合适。

6.3 微调场景下 warmup 还要不要

这个问题我被问过很多次。答案是:要,但可以短一些。微调的时候,模型已经在一大堆数据上预训练过了,参数处在一个比较好的位置,不像随机初始化那样容易发散。所以预热步数可以设得比较短,通常是总步数的 3% 到 5% 就够了。

但有一种情况例外:如果你的微调数据集和预训练数据集分布差异很大,或者你解冻了很多层,那预热还是不能太短。我在一个跨领域微调的项目里,因为新数据和预训练数据差异太大,用了 10% 的预热步数才稳住。

6.4 常见错误排查清单

现象可能原因排查方向
预热结束后损失突然跳变预热步数太短增加预热步数,观察过渡是否平滑
预热阶段损失几乎不降预热步数太长或峰值学习率太低缩短预热或提高峰值学习率
训练全程损失波动大峰值学习率过高降低峰值学习率或加梯度裁剪
学习率曲线不符合预期scheduler.step() 调用时机错误打印每步学习率确认
分布式训练预热异常多进程重复调用 scheduler只在主进程调用

这张表里的每一行都是我实际遇到过的问题。特别是最后一条,分布式训练中 scheduler 的调用问题非常隐蔽,因为单机测试的时候一切正常,一上多卡就出问题。

7. 不同任务场景下的 warmup 策略选择

7.1 计算机视觉任务

在图像分类、目标检测这类 CV 任务中,我通常用线性预热配合阶梯下降或者余弦退火。预热步数一般设在 500 到 2000 之间,具体看数据集大小和 batch size。如果是从头训练一个大模型,预热会长一些;如果是微调预训练模型,预热可以短到 200 到 500 步。

有一个 CV 特有的注意点:如果你用了数据增强(比如 MixUp、CutMix),训练初期的损失值会比较高,因为增强后的样本更难学。这时候预热步数可以适当加长,给网络更多时间来适应增强后的数据分布。

7.2 自然语言处理任务

NLP 领域的 Transformer 模型对 warmup 的依赖比 CV 模型更强。原始 Transformer 论文里就用了 4000 步的线性预热,这个配置后来被广泛沿用。BERT 的训练也用了 warmup,虽然具体步数没有明确公开,但社区复现时通常用总步数的 10% 左右。

NLP 任务中我比较推荐线性预热加线性衰减的组合,这是最经典的配置。如果你用的是 HuggingFace 的 Transformers 库,它的 Trainer 默认就带了 warmup 功能,你只需要设置 warmup_steps 或者 warmup_ratio 就行。我一般用 warmup_ratio=0.1 作为起点。

7.3 强化学习场景

强化学习的训练稳定性问题比监督学习更严重,因为数据分布会随着策略更新而不断变化。在 RL 中,warmup 不仅用于稳定初始训练,还经常用于策略更新的平滑过渡。

我做过的一个 RL 项目里,用了 1000 步的线性预热,配合一个较小的峰值学习率(3e-4),训练稳定性比不加 warmup 好了很多。但 RL 的 warmup 步数很难提前确定,因为训练总步数本身就不太可预测。我的做法是设一个相对保守的预热步数,然后在训练过程中监控策略熵和回报曲线,如果发现预热结束后性能下降,就回头调整。

7.4 生成模型训练

GAN 和扩散模型的训练对超参数极其敏感,warmup 在这里的作用更加关键。GAN 的生成器和判别器通常需要分别设置预热策略,因为两者的学习难度不一样。我一般给判别器设短一点的预热(比如 500 步),给生成器设长一点(比如 2000 步),因为生成器通常更难训练。

扩散模型方面,我参与过的一个项目用了 5000 步的线性预热,峰值学习率 2e-4,配合余弦退火。这个配置在多个数据集上都表现稳定。但扩散模型的训练步数通常很大(几十万步),所以 5000 步的预热其实只占总步数的很小一部分。

8. 我踩过的三个真实坑

8.1 预热步数按 epoch 算还是按 step 算

这个问题看起来很小,但搞错了会导致预热行为完全不对。有些框架的 warmup 参数是按 epoch 计的,有些是按 step 计的。我曾经在一个项目里把按 step 的配置直接搬到了按 epoch 的框架里,结果预热了整整 1000 个 epoch——训练早就结束了预热还没完。

判断方法很简单:看你的调度器是在每个 batch 后调用还是每个 epoch 后调用。如果是每个 batch 后调用,那 warmup_steps 就是按 step 算的;如果是每个 epoch 后调用,那 warmup_epochs 就是按 epoch 算的。混用这两个概念是新手常犯的错误。

8.2 恢复训练时预热被重新触发

如果你在训练中断后从 checkpoint 恢复,一定要确认调度器的状态也被正确恢复了。我有一次恢复训练后发现损失突然飙升,查了半天才发现是调度器被重新初始化了,学习率从预热起点重新开始爬升。而模型已经训练了很久,突然遇到一个很小的学习率,相当于急刹车,训练节奏完全被打乱。

解决办法是在保存 checkpoint 的时候把 scheduler 的 state_dict 也存下来,恢复的时候一起加载。PyTorch 的 scheduler 都支持 state_dict() 和 load_state_dict(),用起来很方便。

8.3 预热和权重衰减的交互

权重衰减(Weight Decay)在预热阶段也会影响参数更新。如果你用的是 AdamW 优化器,权重衰减是解耦的,和 warmup 的交互比较简单。但如果你用的是 SGD 加 L2 正则,权重衰减会直接加到梯度上,预热阶段学习率很小的时候,权重衰减的相对影响就会变大。

我在一个项目里发现,预热阶段权重衰减把参数拉向零的速度比学习率把参数拉向最优解的速度还快,导致预热结束后模型性能反而下降了。后来把权重衰减系数调小了一个数量级才解决。这个坑比较隐蔽,因为损失曲线看起来是正常下降的,但验证集指标就是上不去。

9. 一些进阶玩法和个人体会

9.1 分层学习率配合 warmup

当你微调一个预训练模型时,不同层可能需要不同的学习率。通常靠近输入的层学习率要小一些,靠近输出的层学习率可以大一些。这时候 warmup 也可以分层设置:底层用更长的预热,顶层用更短的预热。

这种做法的逻辑是底层学的是更通用的特征,不需要大幅调整;顶层学的是任务相关的特征,可以快速适应。我在一个迁移学习项目里试过这种策略,比统一 warmup 的收敛速度快了大概 20%。

9.2 自适应 warmup

最近有一些工作在研究根据训练状态动态调整预热步数,而不是提前设死。比如监控损失曲线的斜率,如果下降太慢就缩短预热,如果波动太大就延长预热。这个思路很有意思,但目前还没有特别成熟的通用方案。我自己试过简单的版本:每 100 步检查一次损失方差,如果方差超过阈值就把预热步数延长 10%。效果还行,但增加了不少实现复杂度,适合对训练稳定性要求极高的场景。

9.3 关于 warmup 的一个常见误解

最后说一个我经常听到的误解:有人认为 warmup 只是为了"让模型慢慢适应",所以预热步数越长越好。实际上预热步数过长会导致训练前期浪费大量时间在很小的学习率上,特别是当总训练预算有限的时候,这部分浪费可能直接影响最终性能。

我的经验是,预热步数应该刚好够让训练稳定下来就行,不需要留太多余量。判断标准就是前面说的:看损失曲线在预热结束后的过渡是否平滑。如果平滑,说明预热步数够了;如果还有抖动,再加一些。

在实际项目中,我通常会把 warmup 相关的配置单独抽出来放在配置文件里,方便快速调整。预热步数、峰值学习率、预热策略类型这三个参数是我调得最频繁的。每次换数据集或者换模型结构,我都会重新跑一遍短试验来确定这三个值。这个习惯帮我避免了很多次无效的长训练。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询