3. 固定学习率与衰减策略:步长衰减、指数衰减、分段衰减的实现与调试

说到学习率,你在 FRL 训练里肯定纠结过:设大了训练不稳,设小了半天不动。固定学习率最直接,但我个人很少直接用它跑到底——太容易陷进局部最优了。衰减策略才是让模型「先探索、后收敛」的关键。下面我把三种常用策略掰开揉碎讲清楚,顺便分享我踩过的一些坑。

3.1 固定学习率的陷阱与适用场景

固定学习率就是把 learning_rate 设成一个常数,全程不变。你想想看,这么做的好处是简单:参数少,调试省心。但坏处也很明显——

  • 太大会震荡:训练后期 loss 在高位反复跳,永远降不下去。
  • 太小会卡住:前期探索不够,模型很快停在某个鞍点。

我记得有一次做连续控制任务,lr 设了 1e-4 就一直不动,后来改成 1e-3 才开始下降。固定学习率其实最适合 调试阶段:先用它跑 50~100 回合,摸清 loss 的大致范围,再决定要不要上衰减。

我的习惯:先固定 lr 跑一个完整 epoch,画出 loss 曲线。如果曲线尾部还明显在下探,说明学习率可以更小或加衰减;如果尾部已经平坦,那当前 lr 可能偏大了。

3.2 步长衰减(Step Decay)

步长衰减是最朴素的衰减方式:每训练 N 步,把学习率乘一个因子 gamma。说白了就是「每隔一段距离砍一刀」,砍的幅度和频率都由你定。

# 伪代码示例
initial_lr = 1e-3
step_size = 2000   # 每 2000 步衰减一次
gamma = 0.5        # 衰减因子

for global_step in range(total_steps):
    # 计算当前学习率
    current_lr = initial_lr * (gamma ** (global_step // step_size))
    # 用 current_lr 更新模型
    optimizer.learning_rate.assign(current_lr)

我在项目中遇到过这样一个坑:步长设得太小(比如每 500 步衰减一次),结果学习率下降太快,训练中期就收敛不动了。后来把步长调大到 3000,效果明显改善。所以 步长要匹配一个 epoch 的步数,一般设为一个 epoch 的 1~3 倍左右。

曾经踩过的雷:步长衰减会导致学习率「阶梯跳变」,模型在跳变瞬间 loss 经常突然抖一下。如果抖动幅度超过 10%,说明衰减因子太大了,建议 gamma 从 0.8 开始试。

3.3 指数衰减(Exponential Decay)

指数衰减比步长衰减更平滑。它的公式是:

lr = initial_lr × decay_rate (global_step / decay_steps)

其中 decay_steps 控制衰减速度,decay_rate 通常取 0.9~0.99。你想想看,指数衰减的好处是每个 step 都衰减一点点,曲线连续可微,对优化器比较友好。在 RL 里尤其常见——Pytorch 的 ExponentialLR 就是这个原理。

# 用 PyTorch 实现
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
for epoch in range(num_epochs):
    train_one_epoch()
    scheduler.step()

我记得有一次做 FRL 训练,用指数衰减配合 Adam,loss 曲线特别漂亮——前期快速下降,中期平稳过渡,后期几乎贴地。但要注意:指数衰减 下降速度固定,如果训练步数很长,后期学习率会变得极小(比如 1e-6 量级),导致模型几乎不再更新。所以一般要设一个 min_lr 做下限。

一句话总结指数衰减:平滑、自动、省心,但小心后期学习率过低导致丧失学习能力。

3.4 分段衰减(Piecewise Decay)

分段衰减就是自己画一条学习率曲线——在几个关键节点手动切换值。比如第 0~10k step 用 1e-3,第 10k~20k step 用 5e-4,之后用 2e-4。这种方式自由度最高,但也最依赖经验。

# 分段衰减示例(TensorFlow 风格)
boundaries = [10000, 20000]          # 切换点
values = [1e-3, 5e-4, 2e-4]         # 对应区间的 lr

def piecewise_lr(global_step):
    # 依次判断当前 step 落在哪个区间
    for i, bound in enumerate(boundaries):
        if global_step < bound:
            return values[i]
    return values[-1]

我曾经掉进一个大坑:boundaries 和 values 长度没对齐,少写了一个值,结果学习率在第 20000 step 之后直接跳到了 NaN。后来我写了个自动化检查——

assert len(values) == len(boundaries) + 1, "values 数量必须比 boundaries 多一个"

分段衰减在 调参经验丰富 的场景下特别高效。如果你已经知道训练到什么时候需要降低探索率,那直接写分段表比任何自动衰减都更可控。

3.5 三种策略对比与选择指南

策略衰减方式收敛速度优点缺点
步长衰减阶梯跳变中直观易调,每段独立跳变时可能震荡
指数衰减连续指数下降快(前期)平滑,自动,广泛适用后期过小,需设下限
分段衰减人工分段常数依赖分段点最灵活,可精细控制调参工作量大,易出错

我个人的选型逻辑很直白:

  • 新手/快速实验 → 指数衰减 + 设 min_lr = 1e-6
  • 中期调优 → 步长衰减,gamma=0.8~0.9,step_size = 2~3 个 epoch
  • 精准控制 → 分段衰减,先看 loss 曲线确定边界
三种衰减策略学习率曲线对比 训练步数 学习率 步长衰减 指数衰减 分段衰减 说明:步长衰减呈阶梯状,指数衰减连续光滑,分段衰减在切换点突变。 实际中可根据 loss 曲线选择最合适的策略。

3.6 调试中的关键检查点

调试时别只看最终 loss,一定要 监控学习率的变化轨迹。我习惯在每个 epoch 记下当前学习率,画一条曲线叠在 loss 曲线下方——如果 loss 突然上升的同时学习率也在跳变,那就是衰减策略有问题。

  1. 检查衰减是否生效:打印每个 step 的学习率,确认曲线形状对得上预设。
  2. 监控 loss 抖动:步长衰减的阶梯跳变处,loss 抖动超过 15% 就调小 gamma 或调大步长。
  3. 设置学习率下限:指数衰减尤其需要,否则后期 lr 趋近于 0,模型直接「死掉」。
  4. 分段衰减对齐边界:用 assert 检查 values 长度 = boundaries 长度 + 1,我吃过这个亏。
核心结论:三种策略没有好坏,关键在于匹配你的训练阶段和目标。固定学习率适合探路,步长衰减适合中等可控场景,指数衰减自动化强,分段衰减留给有经验的人。建议先跑固定 lr 摸清底数,再梯度式选衰减。