学习率调参:影响、策略与实战经验
学习率,说白了就是模型每次更新参数时迈的步子大小。步子太大容易扯着蛋——损失函数震荡不收敛;步子太小又像蜗牛爬,训练到天荒地老。我做了这么多年模型调优,可以负责任地告诉你:学习率是超参数里最值得花时间调的一个。
学习率的影响
先说说学习率到底影响什么。你想想看,梯度下降的本质就是沿着山坡往下走。学习率决定了你每一步跨多远。
- 学习率太大:损失函数剧烈震荡,甚至直接发散。我在项目中遇到过,一个BERT微调任务,学习率设成1e-3,结果loss直接飞到NaN。嗯,那次我花了半天排查数据问题,最后发现是学习率惹的祸。
- 学习率太小:收敛极慢,模型卡在局部最优出不来。我记得有个图像分类任务,学习率设了1e-6,训练了20个epoch准确率还在50%晃悠。后来改成1e-4,3个epoch就上到80%。
- 学习率适中:损失平滑下降,收敛速度快,泛化能力也好。
核心结论:学习率直接影响训练速度、收敛质量和最终性能。调参的第一步,就是找到合适的学习率范围。
学习率衰减策略
实际训练中,我们很少用固定学习率。为什么?因为训练初期需要大步子快速接近最优区域,后期需要小步子精细搜索。这就是学习率衰减的核心理念。
Step Decay(阶梯式衰减)
最简单粗暴的方式。每隔固定轮数,学习率乘以一个衰减因子。
# PyTorch 实现
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
train_one_epoch()
scheduler.step()
print(f"Epoch {epoch}, LR: {scheduler.get_last_lr()[0]}")
我个人习惯用Step衰减做基线。step_size设成总epoch的1/3左右,gamma设0.1。比如训练90个epoch,就在第30和60轮各衰减一次。简单有效,但不够平滑。
实战技巧:Step衰减适合CV任务,尤其是ImageNet预训练模型微调时。我曾经用Step衰减在ResNet50上做过实验,比固定学习率提升了2-3个点。
Cosine Annealing(余弦退火)
学习率按照余弦函数从初始值下降到最小值。曲线平滑,没有突变。
scheduler = lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # 总周期数
eta_min=0 # 最小学习率
)
Cosine衰减的好处是:前期下降慢,中期下降快,后期又变慢。这符合我们的直觉——初期探索,中期快速收敛,后期精细调优。
我记得有个NLP项目,用Cosine衰减比Step衰减收敛速度快了30%,最终指标也高了0.5个点。不过要注意,Cosine衰减在训练后期学习率已经很小了,如果还想继续提升,可以考虑重启策略。
注意:Cosine衰减的T_max参数要设成总epoch数。如果设小了,学习率还没降到最低就结束了,效果打折扣。
Warmup(预热策略)
训练刚开始时,模型参数是随机初始化的,梯度方向不稳定。如果一上来就用大学习率,很容易把参数推到奇怪的地方。Warmup就是让学习率从0(或很小)逐渐增加到目标值。
# 自定义 Warmup + Cosine 调度器
class WarmupCosineScheduler:
def __init__(self, optimizer, warmup_epochs, total_epochs, base_lr):
self.optimizer = optimizer
self.warmup_epochs = warmup_epochs
self.total_epochs = total_epochs
self.base_lr = base_lr
def step(self, epoch):
if epoch < self.warmup_epochs:
# 线性预热
lr = self.base_lr * (epoch + 1) / self.warmup_epochs
else:
# 余弦衰减
progress = (epoch - self.warmup_epochs) / (self.total_epochs - self.warmup_epochs)
lr = self.base_lr * 0.5 * (1 + math.cos(math.pi * progress))
for param_group in self.optimizer.param_groups:
param_group['lr'] = lr
Warmup在Transformer类模型上几乎是标配。我曾经在训练一个模型时,没用Warmup,结果前几个batch的loss直接炸到100+。加了5个epoch的Warmup后,loss稳定下降,最终效果也好了很多。
经验值:Warmup epoch数一般设为总epoch的5%-10%。比如训练100个epoch,Warmup设5-10个epoch就够了。太大反而浪费训练时间。
实战经验总结
说了这么多理论,来点实在的。我整理了一份学习率调参的checklist,你可以直接拿去用。
| 场景 | 推荐策略 | 初始学习率 | 备注 |
|---|---|---|---|
| CV分类任务 | Step衰减 | 0.1(SGD) | 预训练模型用1e-3 |
| NLP Transformer | Warmup + Cosine | 1e-4(Adam) | Warmup占5% epoch |
| GAN训练 | Cosine衰减 | 2e-4(Adam) | 生成器和判别器可不同 |
| 强化学习 | 固定学习率 | 3e-4(Adam) | 衰减反而可能不稳定 |
我个人习惯的调参流程是这样的:先用1e-3、1e-4、1e-5三个数量级跑几个epoch,看loss下降趋势。哪个下降最快且稳定,就用哪个作为初始学习率。然后加上衰减策略,再跑完整训练。
曾经有个项目,我调了三天学习率都没调好。后来发现是数据预处理出了问题——归一化没做好,导致梯度尺度不一致。所以啊,调参之前先确认数据没问题,不然白费功夫。
避坑指南:我曾经在分布式训练中踩过坑——每个GPU的学习率没同步,导致模型参数更新混乱。如果你用多卡训练,记得检查学习率是否在所有卡上保持一致。
知识体系图
下面这张图总结了学习率调参的核心知识结构,你可以保存下来当参考。
学习率调参说难不难,说简单也不简单。关键是理解每个策略背后的原理,然后根据你的任务和数据特点去选择。别指望一招鲜吃遍天,多试几次,找到最适合你模型的组合。
嗯,今天就聊到这里。记住:调参不是玄学,是有方法论可循的。多积累经验,你也能成为调参高手。