4. 学习率调参:学习率对收敛的影响,学习率衰减策略,自适应学习率
学习率,说白了就是模型每次更新参数时迈的步子大小。步子迈大了容易扯着蛋——损失函数直接震荡发散;步子迈小了又像蚂蚁搬家,训到地老天荒也不收敛。我刚开始调参那会儿,就吃过这个亏。
这一章咱们就把学习率这件事彻底聊透。从最基础的原理,到各种衰减策略,再到自适应学习率优化器,我会把我在项目里踩过的坑和总结的经验都抖出来。
4.1 学习率对收敛的影响
先看一张图,你就明白学习率到底在控制什么。
为什么会这样?你想想看,梯度下降的本质就是沿着损失函数的负梯度方向走。学习率就是步长。如果步长太大,一步跨过了山谷,反而跳到了更高的地方;步长太小,在山谷里挪半天也出不来。
经验法则:我一般从 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。
学习率调参这件事,说白了就是「快慢结合」的艺术。前期快跑找方向,后期慢走精雕琢。掌握了这个节奏,你的模型收敛速度和最终效果都会有质的提升。