学习率调参:影响、策略与实战经验

学习率,说白了就是模型每次更新参数时迈的步子大小。步子太大容易扯着蛋——损失函数震荡不收敛;步子太小又像蜗牛爬,训练到天荒地老。我做了这么多年模型调优,可以负责任地告诉你:学习率是超参数里最值得花时间调的一个。

学习率的影响

先说说学习率到底影响什么。你想想看,梯度下降的本质就是沿着山坡往下走。学习率决定了你每一步跨多远。

  • 学习率太大:损失函数剧烈震荡,甚至直接发散。我在项目中遇到过,一个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的学习率没同步,导致模型参数更新混乱。如果你用多卡训练,记得检查学习率是否在所有卡上保持一致。

知识体系图

下面这张图总结了学习率调参的核心知识结构,你可以保存下来当参考。

学习率调参 学习率的影响 太大 → 震荡/发散 太小 → 收敛慢/局部最优 适中 → 平滑收敛 衰减策略 Step:阶梯式衰减 Cosine:余弦退火 Warmup:预热策略 实战经验 先试3个数量级 确认数据预处理 多卡训练注意同步 核心原则:先粗调范围,再精调策略 Warmup + Cosine 是通用性最强的组合

学习率调参说难不难,说简单也不简单。关键是理解每个策略背后的原理,然后根据你的任务和数据特点去选择。别指望一招鲜吃遍天,多试几次,找到最适合你模型的组合。

嗯,今天就聊到这里。记住:调参不是玄学,是有方法论可循的。多积累经验,你也能成为调参高手。