学习率是深度学习训练中最重要也最难调的超参数之一。它决定了模型参数每次更新的幅度,直接关系到训练能否收敛、收敛速度有多快、最终精度能到多少。很多初学者随手填一个数就开始训练,结果要么损失剧烈震荡不下降,要么训练了几十轮损失几乎纹丝不动。本文用生活中的步长类比来解释学习率的本质,并介绍训练实践中广泛使用的Warmup预热策略。

一、用步长类比理解学习率的本质
可以把训练神经网络想象成一个人在蒙着眼睛下山。他看不见山谷的最低点在哪里,只能通过脚下的坡度(梯度)判断往哪个方向走。学习率就相当于这个人每一步迈多长。如果步长太小,他要走很久才能接近谷底,训练表现为损失下降极其缓慢,浪费大量计算资源;如果步长太大,他可能一步跨过谷底直接迈到对面山坡上,损失不但不下降反而来回震荡,甚至越来越大导致训练发散。
从数学上看,参数更新公式为:新的参数等于旧参数减去学习率乘以梯度。梯度指明了下坡方向,学习率控制沿这个方向走多远。理想的学习率应该随着逐渐接近最优点而不断减小,这就是为什么实际训练中几乎从不使用固定学习率,而是配合各种调度策略动态调整。
经验上,使用SGD优化器时初始学习率常取0.1到0.01,使用Adam这类自适应优化器时常取0.001到0.0001。具体数值还需要根据任务、批大小和模型规模调整,没有放之四海而皆准的万能值。
二、学习率过大与过小的具体表现
学习率过大时,最典型的现象是损失值出现NaN或者剧烈跳动。这是因为参数更新幅度过大,直接跳出了损失函数的平坦区域,进入了梯度爆炸的区间,数值溢出后整个训练就废掉了。如果发现损失在最初几个批次就变成NaN,第一步就应该尝试把学习率缩小十倍。
学习率过小时,损失会下降但速度极慢,训练曲线几乎是平的。这种情况下模型可能陷入较差的局部最优点,或者在有限的训练轮次内根本没有机会接近更好的解。过小的学习率还会让模型对噪声过于敏感,batch带来的随机性可能让参数在原地打转。
一个实用的判断技巧是观察损失曲线的形态:先降后升说明学习率偏大;下降缓慢甚至停滞说明学习率偏小;平稳且持续下降才是合适的表现。PyTorch提供的LR Finder工具可以在正式训练前用递增学习率的方式快速扫描出合理区间,值得尝试。
三、Warmup预热策略的原理与作用
训练刚开始时,参数是随机初始化的,梯度方向噪声很大,模型对数据的分布还没有任何认知。此时如果直接使用较大的学习率,参数会被这些不可靠的梯度推到很离谱的位置,损失可能瞬间飙升,之后即使学习率恢复正常也难以挽回。Warmup的核心思想就是:训练初期用很小的学习率慢慢热身,让模型先稳定下来,再逐步放大到目标学习率。
Warmup在两个场景下尤其重要。一是使用批归一化的模型,训练初期批统计量(均值和方差)非常不稳定,小的学习率可以让这些统计量平稳地建立起来。二是大规模预训练任务,如Transformer类模型,由于注意力机制对参数扰动敏感,几乎都配备了Warmup,例如BERT就采用了前一万个步骤线性预热到峰值学习率的方案。
常见的Warmup方式有线性预热和指数预热两种。线性预热最简单:在第0步到第N步之间,学习率从0(或一个很小的值)线性增长到设定的峰值,之后通常再接余弦退火或阶梯衰减。余弦退火的公式为当前学习率等于峰值学习率乘以0.5倍的括号1加余弦函数,其中余弦的输入是当前进度与圆周率的乘积,它让学习率在训练前期下降慢、后期下降快,兼顾探索与精调。
四、用PyTorch实现带Warmup的学习率调度
下面给出一个完整的PyTorch实现,采用线性预热加余弦退火的组合策略,这是目前最流行的搭配之一。
import math
from torch.optim.lr_scheduler import LambdaLR
def get_cosine_schedule_with_warmup(optimizer, warmup_steps, total_steps):
# 线性预热 + 余弦退火的学习率调度器
def lr_lambda(current_step):
# 阶段一:线性预热,学习率从0增长到峰值
if current_step < warmup_steps:
return current_step / max(1, warmup_steps)
# 阶段二:余弦退火,从峰值平滑衰减到接近0
progress = (current_step - warmup_steps) / max(1, total_steps - warmup_steps)
return max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress)))
return LambdaLR(optimizer, lr_lambda)
# 使用示例
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = get_cosine_schedule_with_warmup(optimizer, warmup_steps=1000, total_steps=100000)
for step, batch in enumerate(dataloader):
outputs = model(batch)
loss = criterion(outputs, batch.labels)
loss.backward()
optimizer.step()
scheduler.step() # 注意:每个step后都要调用scheduler.step
optimizer.zero_grad()
代码中lr_lambda返回的是一个乘法系数,实际学习率等于优化器中设定的初始学习率乘以这个系数。预热阶段系数从0线性涨到1,退火阶段按余弦曲线从1衰减到0。warmup_steps一般设置为总训练步数的百分之五到百分之十,不宜过长,否则会浪费训练预算在低学习率阶段。
还有一种做法是在预热结束后切换到阶梯衰减,即每过固定轮数把学习率乘以0.1。这种方式实现简单、行为可预期,在图像分类任务中至今仍被广泛使用。选择哪种组合没有绝对答案,建议先用余弦退火作为默认方案,效果不理想再尝试其他调度方式。
五、实践中的调参建议
调学习率时建议遵循从粗到细的原则:先用较小的数据子集快速试验几个数量级的学习率(如0.1、0.01、0.001),锁定大致区间后再细化。每次只改一个超参数,否则无法判断是哪个改动起了作用。
批大小与学习率是绑定的关系。经验法则叫线性缩放规则:批大小扩大多少倍,学习率可以相应扩大多少倍。例如批大小从256增加到1024时,学习率可以乘以4。但这个规律在大 batch场景下会失效,需要配合更长的Warmup来稳定训练。
最后要提醒的是,学习率不是孤立存在的,它和优化器选择、权重衰减、梯度裁剪等设置互相影响。遇到训练不稳定时,优先降低学习率并增加Warmup步数,这两个操作成本最低、见效最快。理解了步长类比和预热思想之后,再面对各种复杂的调度策略就不会感到陌生,它们的本质都是在回答同一个问题:当前这一步,应该迈多大。