4. 学习率调参:学习率对收敛的影响,学习率衰减策略,自适应学习率

学习率,说白了就是模型每次更新参数时迈的步子大小。步子迈大了容易扯着蛋——损失函数直接震荡发散;步子迈小了又像蚂蚁搬家,训到地老天荒也不收敛。我刚开始调参那会儿,就吃过这个亏。

这一章咱们就把学习率这件事彻底聊透。从最基础的原理,到各种衰减策略,再到自适应学习率优化器,我会把我在项目里踩过的坑和总结的经验都抖出来。

4.1 学习率对收敛的影响

先看一张图,你就明白学习率到底在控制什么。

学习率对损失函数收敛路径的影响 学习率过大 (η=1.0) 震荡发散,无法收敛 学习率适中 (η=0.01) 稳定收敛到最优解 学习率过小 (η=0.0001) 收敛极慢,可能陷入局部最优 损失值 迭代次数 核心结论:学习率决定收敛速度与稳定性,过大发散、过小低效、适中最佳

为什么会这样?你想想看,梯度下降的本质就是沿着损失函数的负梯度方向走。学习率就是步长。如果步长太大,一步跨过了山谷,反而跳到了更高的地方;步长太小,在山谷里挪半天也出不来。

经验法则:我一般从 0.01 或 0.001 开始试。如果损失震荡,就调小;如果收敛太慢,就调大。调个两三轮,心里就有数了。

4.2 学习率衰减策略

训练初期需要大学习率快速下降,后期需要小学习率精细微调。这就是学习率衰减的核心理念。我见过不少新手从头到尾用一个固定学习率,结果要么前期震荡后期不收敛,要么前期太慢后期过拟合。

下面三种衰减策略,是我项目里最常用的。

4.2.1 Step Decay(阶梯衰减)

每隔固定轮数,学习率乘以一个衰减因子。比如每 30 个 epoch 学习率减半。

# Step Decay 实现
import torch.optim.lr_scheduler as scheduler

optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
step_scheduler = scheduler.StepLR(
    optimizer,
    step_size=30,      # 每30个epoch衰减一次
    gamma=0.5          # 衰减因子,乘以0.5
)

for epoch in range(100):
    train_one_epoch()
    step_scheduler.step()
    current_lr = optimizer.param_groups[0]['lr']
    print(f'Epoch {epoch}: lr = {current_lr:.6f}')

我在项目中遇到过这种情况:用 Step Decay 训练图像分类模型,前 30 个 epoch 学习率 0.1,loss 降得很快;第 30 个 epoch 突然降到 0.05,loss 会有一个小跳变,但很快又稳定下降。这是正常现象,别慌。

调参技巧:step_size 一般设为总 epoch 数的 1/3 到 1/4。gamma 常用 0.1 或 0.5。如果数据集小,可以缩短 step_size。

4.2.2 Exponential Decay(指数衰减)

每个 epoch 都乘以一个衰减因子,学习率呈指数下降。衰减更平滑,但后期可能下降太快。

# Exponential Decay 实现
exp_scheduler = scheduler.ExponentialLR(
    optimizer,
    gamma=0.95         # 每个epoch乘以0.95
)

for epoch in range(100):
    train_one_epoch()
    exp_scheduler.step()

指数衰减适合训练轮数较多的场景。我记得有一次训练一个 200 层的 ResNet,用指数衰减 gamma=0.97,学习率从 0.1 慢慢降到 0.001 左右,效果比 Step Decay 稳定不少。

注意:指数衰减后期学习率会变得非常小,可能导致模型停止学习。建议设置一个最小学习率下限,比如 1e-6。

4.2.3 Cosine Annealing(余弦退火)

这是我个人最喜欢的一种策略。学习率按照余弦函数周期性地下降和上升,既能跳出局部最优,又能精细收敛。

# Cosine Annealing 实现
cosine_scheduler = scheduler.CosineAnnealingLR(
    optimizer,
    T_max=50,          # 半个周期的epoch数
    eta_min=1e-6       # 最小学习率
)

for epoch in range(100):
    train_one_epoch()
    cosine_scheduler.step()

你想想看,余弦退火为什么有效?它让学习率先快速下降找到好区域,然后突然跳高一点,看看有没有更好的坑。这种「探索-利用」的节奏,特别适合复杂损失曲面。

三种策略的对比,我整理了一张表:

策略 衰减方式 适用场景 优点 缺点
Step Decay 阶梯式下降 通用场景,CV 任务 简单直观,容易调参 衰减点选择敏感
Exponential Decay 指数平滑下降 长训练周期 平滑稳定,无突变 后期学习率过小
Cosine Annealing 余弦周期下降 复杂损失曲面 可跳出局部最优 需要调 T_max

4.3 自适应学习率优化器

手动调学习率衰减确实麻烦。后来业界搞出了自适应学习率优化器——让优化器自己根据梯度历史动态调整每个参数的学习率。说白了,就是帮你省事。

4.3.1 Adam 优化器

Adam = Momentum + RMSprop。它既保留了动量项的惯性,又用梯度平方的指数移动平均来缩放学习率。

# Adam 优化器
optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001,           # 默认学习率
    betas=(0.9, 0.999), # 一阶和二阶动量衰减系数
    eps=1e-8            # 防止除零
)

Adam 的默认参数(lr=0.001, betas=(0.9, 0.999))在大多数任务上表现都不错。我曾经在一个 NLP 文本分类项目里,直接用默认参数训了一个 Transformer,收敛速度比 SGD 快了 3 倍。

我的习惯:Adam 做快速原型验证,SGD + Cosine Annealing 做最终精调。Adam 省心,但有时候泛化性不如 SGD。

4.3.2 RMSprop 优化器

RMSprop 是 Adam 的前身,只用了梯度平方的指数移动平均来调整学习率,没有动量项。适合处理非平稳目标。

# RMSprop 优化器
optimizer = torch.optim.RMSprop(
    model.parameters(),
    lr=0.001,
    alpha=0.99,         # 衰减因子
    eps=1e-8
)

RMSprop 在 RNN 和强化学习任务中表现不错。我记得有一次做时间序列预测,Adam 反而震荡,换成 RMSprop 就稳了。嗯,这里要注意:RMSprop 对学习率更敏感,建议从 0.001 开始试。

两个自适应优化器的对比:

特性 Adam RMSprop
动量项 有(一阶动量) 无
适用场景 通用,CV/NLP/推荐 RNN、强化学习、非平稳
默认学习率 0.001 0.001
收敛速度 快 中等
泛化性 一般(可能过拟合) 较好

4.4 避坑指南与实战建议

最后,分享几个我踩过的坑:

  • 学习率太大导致 NaN:我曾经有一次把学习率设成 0.5,结果 loss 直接变成 NaN。排查了半天才发现是学习率的问题。建议从 0.01 或 0.001 开始,逐步放大。
  • 衰减策略选错:有一次用 Exponential Decay 训练短周期任务,学习率还没怎么降就训完了,等于没用。短周期用 Step Decay,长周期用 Cosine Annealing。
  • Adam 不收敛:Adam 有时候在验证集上 loss 降不下去。这时候可以试试 SGD + Momentum,或者调低 Adam 的 betas 参数。
  • 忘记 scheduler.step():嗯,这个低级错误我犯过不止一次。写了 scheduler 但忘了在每个 epoch 后调用 step(),学习率一直没变。检查一下代码。

终极建议:新手先用 Adam + 默认学习率 0.001 跑通流程。然后换成 SGD + Cosine Annealing 精调。等经验丰富了,再尝试手动调 Step Decay 或 Exponential Decay。

学习率调参这件事,说白了就是「快慢结合」的艺术。前期快跑找方向,后期慢走精雕琢。掌握了这个节奏,你的模型收敛速度和最终效果都会有质的提升。