学习率:模型训练的“油门”与“刹车”

说实话,学习率是超参数里最核心的一个。我见过太多新手,花大量精力调网络结构、改损失函数,结果模型根本不收敛——最后发现只是学习率设错了。

学习率决定了参数更新的步长。步子太大,跑过了最优点;步子太小,走到天黑也到不了。说白了,它就是训练过程的“油门”和“刹车”。

核心直觉:学习率越高,模型学得越快,但也越容易震荡;学习率越低,训练越稳定,但收敛速度慢得像蜗牛。

学习率对训练过程的影响

咱们先看看学习率不同取值时的表现。我习惯用一个简单的实验来说明问题:在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内的梯度均值更稳定了,可以迈更大的步子。

在实际操作中,我一般这样处理:

  1. 先用小batch size(比如64)找到一个合适的学习率
  2. 然后按比例放缩:new_lr = base_lr * (new_batch_size / base_batch_size)
  3. 再微调一下,通常差不了太多

学习率调优的完整流程

下面的流程图总结了学习率调优的完整路径。嗯,从初始化到最终策略选择,每一步都决定了训练的成败。

学习率调优流程 选择初始学习率 (推荐从0.01或0.001开始) 观察训练行为 (损失曲线、准确率变化) 训练是否 正常收敛? 否 调整学习率 (太高/太低) 是 选择学习率策略 (阶梯/余弦/循环) 评估最终结果 (验证集性能、泛化能力) 确定最终学习率方案

一些经验总结

学完了这些,来点实战经验吧。

  • 从小开始:新手建议从0.001开始,大部分场景都适用。
  • 观察损失曲线:损失下降太慢就提高学习率,震荡就降低。我每次看训练日志都是先扫一眼损失曲线。
  • 别迷信Adam:虽然Adam自带自适应学习率,但调好的SGD+余弦退火在很多任务上表现更好。
  • 记录每次实验:把学习率、优化器、batch size都记下来。这样你就能慢慢积累直觉——某种模型配多少学习率合适。

我的习惯:每次实验先用小规模数据跑几个epoch,快速粗调学习率区间。确认收敛趋势后,再用全量数据精调。这样能节省大量时间。

学习率调优,说到底就是平衡“快”和“稳”。快一点,可能早期效果好,但后期不精细;稳一点,可能花了时间却拿到了更好的结果。你想想看,这个平衡点在哪里?只能靠实验和日志来回答。