学习率:模型训练的“油门”与“刹车”
说实话,学习率是超参数里最核心的一个。我见过太多新手,花大量精力调网络结构、改损失函数,结果模型根本不收敛——最后发现只是学习率设错了。
学习率决定了参数更新的步长。步子太大,跑过了最优点;步子太小,走到天黑也到不了。说白了,它就是训练过程的“油门”和“刹车”。
核心直觉:学习率越高,模型学得越快,但也越容易震荡;学习率越低,训练越稳定,但收敛速度慢得像蜗牛。
学习率对训练过程的影响
咱们先看看学习率不同取值时的表现。我习惯用一个简单的实验来说明问题:在MNIST数据集上训练一个简单的MLP,只改学习率,其他全固定。
1. 学习率太高
模型直接炸了。损失值一路飙升,或者剧烈震荡,完全没法收敛。
Epoch 10 | Loss: 2.3012 | Acc: 11.35%
Epoch 20 | Loss: 2.4510 | Acc: 10.98%
Epoch 30 | Loss: 2.1998 | Acc: 12.02%
Epoch 40 | Loss: 2.6701 | Acc: 10.11%
你看,损失不降反升,准确率跟瞎猜差不多。我在项目里遇到过这种情况,当时还以为是代码写错了,查了半天——结果只是学习率设成了0.1,对很多模型来说确实太大了。
2. 学习率太低
模型能收敛,但慢得要命。损失值缓慢下降,训练几百个epoch才看到明显效果。
Epoch 100 | Loss: 1.8521 | Acc: 32.15%
Epoch 200 | Loss: 1.4210 | Acc: 48.98%
Epoch 300 | Loss: 1.0998 | Acc: 62.02%
Epoch 400 | Loss: 0.8701 | Acc: 71.11%
嗯,这是可以训练出来的,但时间成本太高了。你想想看,如果每个实验都要跑400个epoch,那调参效率得多低。
3. 合适的学习率
损失平滑下降,快速收敛到较低水平。
Epoch 10 | Loss: 0.4512 | Acc: 85.35%
Epoch 20 | Loss: 0.3010 | Acc: 90.98%
Epoch 30 | Loss: 0.2398 | Acc: 93.02%
Epoch 40 | Loss: 0.2001 | Acc: 94.11%
这才是我想要的效果。几十个epoch就搞定了,准确率也上去了。
学习率调优的核心原则
调学习率没有银弹,但有几个原则我一直用着。
| 训练阶段 | 学习率策略 | 原因 |
|---|---|---|
| 起步阶段 | 中等大小(0.01~0.001) | 快速从随机初始化进入有效区域 |
| 中期 | 逐步下降 | 到达较优区域后精细搜索 |
| 后期 | 很低(1e-5以下) | 微调参数,防止跳过最优点 |
| 震荡时 | 降低或减小衰减率 | 稳定训练过程 |
我个人习惯从0.01开始试。如果损失震荡或者不降,就改0.001;如果降得太慢,就试试0.1。这样两三轮就能找到一个大致的“合适区间”。
小技巧:试试“学习率范围测试”(LR range test)。从一个很小的学习率开始,每个batch增加一点,最后看损失曲线的变化——损失开始上升的那个点,就是学习率的上限。
常见的学习率策略
实际项目中很少用固定学习率从头到尾。来看看几种主流策略。
1. 阶梯式下降
每N个epoch下降一次,乘以一个衰减因子。
# PyTorch 示例
scheduler = torch.optim.lr_scheduler.StepLR(
optimizer,
step_size=30,
gamma=0.1
)
# 每30个epoch 学习率乘以0.1
2. 余弦退火
学习率按余弦曲线从初始值下降到最小值,然后可能再回升(带重启的版本)。
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100,
eta_min=1e-6
)
# 100个epoch内从初始值平滑降到1e-6
这种策略我特别喜欢。很多实验里,余弦退火比阶梯下降收敛出来的效果要好那么一两个点。为什么?因为它不会在某个边界突然跳变,训练过程更平滑。
3. 循环学习率
学习率在上下界之间周期性变化。说白了就是故意让它震荡一下,有时候能跳出局部最优。
scheduler = torch.optim.lr_scheduler.CyclicLR(
optimizer,
base_lr=0.001,
max_lr=0.01,
step_size_up=2000,
mode='triangular2'
)
4. Warmup + 下降
先慢慢升温,再逐渐下降。这对于训练初期特别有用,尤其是大模型或使用大batch size时。
# 自定义 warmup 实现
def warmup_cosine_scheduler(epoch, warmup_epochs=5, total_epochs=100):
if epoch < warmup_epochs:
return (epoch + 1) / warmup_epochs
else:
progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs)
return 0.5 * (1 + math.cos(math.pi * progress))
学习率与优化器的配合
不同的优化器对学习率的敏感度天差地别。
| 优化器 | 推荐初始学习率 | 特点 |
|---|---|---|
| SGD | 0.01 ~ 0.1 | 对学习率敏感,需要精细调参 |
| SGD+Momentum | 0.01 ~ 0.1 | 比普通SGD稳定一些 |
| Adam | 0.001(默认) | 自带自适应学习率,大多数情况表现不错 |
| AdamW | 0.001 ~ 0.005 | 带权重衰减的Adam,推荐用于Transformer |
| RMSprop | 0.001 | 适合RNN和序列模型 |
避免陷阱:不要直接在代码里乘一个系数就换个学习率。要用优化器提供的接口或scheduler来统一管理,否则容易出现不一致的问题。我曾经吃过这个亏,一个实验里手动改了lr变量,但优化器内部还有自己维护的缓存状态,结果训练崩溃了。
一个实战陷阱:学习率与batch size的关系
我完全同意“线性缩放法则”——当你把batch size翻倍时,学习率也应该大致翻倍。为什么?因为一个batch内的梯度均值更稳定了,可以迈更大的步子。
在实际操作中,我一般这样处理:
- 先用小batch size(比如64)找到一个合适的学习率
- 然后按比例放缩:
new_lr = base_lr * (new_batch_size / base_batch_size) - 再微调一下,通常差不了太多
学习率调优的完整流程
下面的流程图总结了学习率调优的完整路径。嗯,从初始化到最终策略选择,每一步都决定了训练的成败。
一些经验总结
学完了这些,来点实战经验吧。
- 从小开始:新手建议从0.001开始,大部分场景都适用。
- 观察损失曲线:损失下降太慢就提高学习率,震荡就降低。我每次看训练日志都是先扫一眼损失曲线。
- 别迷信Adam:虽然Adam自带自适应学习率,但调好的SGD+余弦退火在很多任务上表现更好。
- 记录每次实验:把学习率、优化器、batch size都记下来。这样你就能慢慢积累直觉——某种模型配多少学习率合适。
我的习惯:每次实验先用小规模数据跑几个epoch,快速粗调学习率区间。确认收敛趋势后,再用全量数据精调。这样能节省大量时间。
学习率调优,说到底就是平衡“快”和“稳”。快一点,可能早期效果好,但后期不精细;稳一点,可能花了时间却拿到了更好的结果。你想想看,这个平衡点在哪里?只能靠实验和日志来回答。