学习率调度策略

一句话概述

学习率(Learning Rate)是深度学习中最关键的超参数——太大则无法收敛(震荡甚至发散),太小则收敛极慢。学习率调度(Learning Rate Scheduling)的核心思想是在训练过程中动态调整学习率:前期使用较大学习率快速逼近最优点,后期逐步减小学习率精细搜索。常见策略包括:阶梯衰减(Step Decay)、指数衰减(Exponential Decay)、余弦退火(Cosine Annealing)、预热(Warmup)和循环学习率(Cyclical LR)。现代训练中,Warmup + Cosine Annealing是Transformer类模型的标准配置——先用少量步数将学习率从极小值线性提升到峰值(预热),再用余弦曲线将学习率衰减到接近0(退火)。

💡 核心要点:①学习率调度是平衡收敛速度和最终效果的艺术——前期大步快走,后期小步精调 ②Warmup预热防止训练初期的不稳定,是Transformer训练的必备技巧 ③余弦退火是最常用的衰减策略,相比阶梯衰减更平滑 ④循环学习率通过周期性振荡帮助跳出局部最优 ⑤好的调度策略能让模型在相同训练步数下获得更低的最终损失

教学与演示

一、学习率的核心作用与基本调度

是什么(定义):学习率η控制着每次参数更新时的步长:θ_t+1 = θ_t - η · ∇L(θ_t)。在随机梯度下降中,梯度∇L(θ_t)是"前进方向",学习率η是"前进速度"。学习率调度器在训练过程中按预定规则改变η的值,通常从大到小——初始值η_0约为1e-3到1e-1(取决于优化器),最终衰减三个数量级以上。

大白话 学习率就是"迈步子的大小"。如果步子太大,可能直接跨过最低点甚至从山谷另一边滚出去(震荡发散);如果步子太小,走到天黑还没到终点(收敛慢)。调度策略是说:一开始山谷还很远时大步流星地走;快接近谷底时换成小碎步;刚开始走时先"热身"慢慢加速(warmup);如果觉得陷入了一个假谷底,就左右晃一晃看看能不能跳出去(循环学习率)。

为什么(原理):基于SGD的收敛理论,当步长逐步减小时,SGD以概率1收敛到局部最优。实际情况中:①训练初期参数离最优很远,大的学习率能快速减少损失;②训练后期参数接近最优,需要小步长精细调整,否则会在最优附近震荡;③过大的学习率导致梯度的方差主导更新方向,模型无法收敛;④过小的学习率导致参数更新近乎停滞,表现为loss plateaus(损失平台)。

import numpy as np

# 实现常见的学习率调度器
# 展示不同调度策略的学习率变化曲线

class LRScheduler:
    """学习率调度器基类"""
    def __init__(self, initial_lr):
        self.initial_lr = initial_lr
        self.history = []

    def get_lr(self, step):
        raise NotImplementedError

    def simulate(self, total_steps):
        self.history = [self.get_lr(s) for s in range(total_steps)]
        return self.history


class StepDecay(LRScheduler):
    """阶梯衰减:每隔step_size步,学习率乘以gamma"""
    def __init__(self, initial_lr, step_size, gamma=0.1):
        super().__init__(initial_lr)
        self.step_size = step_size
        self.gamma = gamma

    def get_lr(self, step):
        return self.initial_lr * (self.gamma ** (step // self.step_size))


class ExponentialDecay(LRScheduler):
    """指数衰减:lr = lr_0 * gamma^step"""
    def __init__(self, initial_lr, gamma=0.999):
        super().__init__(initial_lr)
        self.gamma = gamma

    def get_lr(self, step):
        return self.initial_lr * (self.gamma ** step)


class CosineAnnealing(LRScheduler):
    """余弦退火:按余弦曲线从初始值衰减到0"""
    def __init__(self, initial_lr, T_max, eta_min=0):
        super().__init__(initial_lr)
        self.T_max = T_max
        self.eta_min = eta_min

    def get_lr(self, step):
        if step >= self.T_max:
            return self.eta_min
        return self.eta_min + 0.5 * (self.initial_lr - self.eta_min) * (1 + np.cos(np.pi * step / self.T_max))


class WarmupCosine(LRScheduler):
    """预热+余弦退火:前warmup_steps步线性增长,后续余弦衰减"""
    def __init__(self, initial_lr, warmup_steps, T_max, eta_min=0):
        super().__init__(initial_lr)
        self.warmup_steps = warmup_steps
        self.T_max = T_max
        self.eta_min = eta_min

    def get_lr(self, step):
        if step < self.warmup_steps:
            # 线性预热:从接近0线性增长到initial_lr
            return self.initial_lr * (step + 1) / self.warmup_steps
        elif step >= self.T_max:
            return self.eta_min
        else:
            # 余弦退火
            cos_inner = np.pi * (step - self.warmup_steps) / (self.T_max - self.warmup_steps)
            return self.eta_min + 0.5 * (self.initial_lr - self.eta_min) * (1 + np.cos(cos_inner))


class CyclicalLR(LRScheduler):
    """循环学习率:在[min_lr, max_lr]之间周期性三角波振荡"""
    def __init__(self, initial_lr, min_lr, step_size_up, step_size_down=None):
        super().__init__(initial_lr)
        self.min_lr = min_lr
        self.step_size_up = step_size_up
        self.step_size_down = step_size_down if step_size_down else step_size_up

    def get_lr(self, step):
        cycle = step // (self.step_size_up + self.step_size_down)
        pos = step % (self.step_size_up + self.step_size_down)
        if pos < self.step_size_up:
            # 上升阶段
            return self.min_lr + (self.initial_lr - self.min_lr) * pos / self.step_size_up
        else:
            # 下降阶段
            pos_down = pos - self.step_size_up
            return self.initial_lr - (self.initial_lr - self.min_lr) * pos_down / self.step_size_down


print("=== 学习率调度策略:曲线对比 ===\n")

total_steps = 1000
warmup_steps = 100

schedulers = {
    'Step (step=200, gamma=0.5)': StepDecay(0.1, 200, 0.5),
    'Exponential (gamma=0.997)': ExponentialDecay(0.1, 0.997),
    'Cosine Annealing': CosineAnnealing(0.1, T_max=total_steps),
    'Warmup + Cosine': WarmupCosine(0.1, warmup_steps, total_steps),
    'Cyclical (up=200)': CyclicalLR(0.1, 1e-4, 200),
}

for name, scheduler in schedulers.items():
    lr_history = scheduler.simulate(total_steps)
    final_lr = lr_history[-1]
    max_lr = max(lr_history)
    print(f"  {name:25s}: 初始={lr_history[0]:.4f}, 最大={max_lr:.4f}, 最终={final_lr:.6f}")

print("\n→ 不同调度策略的学习率变化曲线各具特点")
print("→ Warmup+Cosine是现代Transformer训练的标准配置")
余弦退火公式\(\eta_t = \eta_{\min} + \frac{1}{2}(\eta_0 - \eta_{\min})\left(1 + \cos\left(\frac{t}{T_{\max}}\pi\right)\right)\)

二、Warmup预热——Transformer训练的必备技巧

是什么(定义):Warmup(预热)指训练开始时先用若干步将学习率从极小值(或0)线性增长到目标学习率。典型的Transformer配置:前4000步(或10k步)学习率从0线性增长到峰值(如1e-3),之后再按余弦曲线衰减。预热步数通常占总步数的5%-15%。

大白话 Warmup就是"先慢跑再冲刺"。刚开跑时如果直接全速冲刺(大学习率),容易扭伤(梯度不稳定)。先慢慢加速让身体适应(参数分布调整),等状态稳定了再提上目标速度。对于Transformer尤其重要——因为它有一堆残差连接和LayerNorm,初始参数和梯度都比较"野",需要预热来稳定。

为什么(原理):Transformer需要Warmup的原因:①训练初期参数分布混乱,残差连接前的方差很大,大学习率会导致梯度爆炸;②Layer Normalization在训练初期统计量不稳定,需要时间来建立合理的running statistics;③多头注意力在初始化时各头的梯度差异大,大学习率会放大这种差异导致训练不稳定。预热给网络"缓冲时间"——参数先在小步更新下逐渐调整到合理范围,之后再用大学习率加速。

import numpy as np

# 模拟warmup对训练稳定性的影响
# 对比有无warmup时梯度和损失的演化

def simulate_training_with_warmup(total_steps=500, warmup_steps=50, peak_lr=0.01, noise_level=0.3):
    """模拟一个简单二次优化问题,观察warmup的效果"""
    np.random.seed(42)
    
    # 真实最优参数
    theta_true = np.array([1.0, -0.5, 2.0, 0.8, -1.2])
    dim = len(theta_true)
    
    # 初始化参数(随机初始化,离最优较远)
    theta = np.random.randn(dim) * 2.0
    
    # 损失函数:二次型 f(theta) = 0.5 * ||theta - theta_true||^2
    def loss(theta):
        return 0.5 * np.sum((theta - theta_true) ** 2)
    
    def gradient(theta):
        return theta - theta_true
    
    # 无warmup训练
    theta_no_warmup = theta.copy()
    losses_no_warmup = []
    grad_norms_no_warmup = []
    
    for t in range(total_steps):
        lr = peak_lr  # 固定学习率,无预热
        grad = gradient(theta_no_warmup) + np.random.randn(dim) * noise_level
        grad_norm = np.linalg.norm(grad)
        theta_no_warmup -= lr * grad
        losses_no_warmup.append(loss(theta_no_warmup))
        grad_norms_no_warmup.append(grad_norm)
    
    # 有warmup训练
    theta_warmup = theta.copy()
    losses_warmup = []
    grad_norms_warmup = []
    
    for t in range(total_steps):
        if t < warmup_steps:
            lr = peak_lr * (t + 1) / warmup_steps  # 线性预热
        else:
            lr = peak_lr  # 预热后使用目标学习率
        
        grad = gradient(theta_warmup) + np.random.randn(dim) * noise_level
        grad_norm = np.linalg.norm(grad)
        theta_warmup -= lr * grad
        losses_warmup.append(loss(theta_warmup))
        grad_norms_warmup.append(grad_norm)
    
    return losses_no_warmup, losses_warmup, grad_norms_no_warmup, grad_norms_warmup


# 执行模拟
losses_nw, losses_w, grads_nw, grads_w = simulate_training_with_warmup(
    total_steps=500, warmup_steps=50, peak_lr=0.01, noise_level=0.3
)

print("=== Warmup预热效果:训练稳定性对比 ===\n")

print("【损失函数值对比】")
print(f"  无预热 - 初始损失: {losses_nw[0]:.4f}")
print(f"  无预热 - 第10步: {losses_nw[9]:.4f}")
print(f"  无预热 - 最终损失: {losses_nw[-1]:.6f}")
print(f"  有预热 - 初始损失: {losses_w[0]:.4f}")
print(f"  有预热 - 第10步: {losses_w[9]:.4f}")
print(f"  有预热 - 最终损失: {losses_w[-1]:.6f}")

print(f"\n【梯度范数对比(前10步)】")
print(f"  无预热梯度范数: {[round(g, 3) for g in grads_nw[:10]]}")
print(f"  有预热梯度范数: {[round(g, 3) for g in grads_w[:10]]}")

# 统计稳定性指标
early_loss_std_nw = np.std(losses_nw[:50])
early_loss_std_w = np.std(losses_w[:50])
print(f"\n【前50步损失标准差(稳定性指标)】")
print(f"  无预热: {early_loss_std_nw:.4f}")
print(f"  有预热: {early_loss_std_w:.4f}")
print(f"  稳定性提升: {(1 - early_loss_std_w / early_loss_std_nw) * 100:.1f}%")

print("\n→ Warmup能在训练初期减少震荡,提高稳定性")
print("→ 虽然最终损失差异不大,但预热训练更平滑可靠")

三、高级调度策略与实战选择

是什么(定义):除了基础的衰减曲线,还有多种进阶策略:①OneCycleLR——学习率先上升后下降,在单个周期内覆盖整个训练过程,配合动量反向变化;②ReduceLROnPlateau——当验证损失停滞(plateau)时降低学习率,是一种自适应策略;③多项式衰减——按幂函数曲线衰减lr_t = lr_0 * (1 - t/T)^power;④分段常数——手动指定每段的学习率。

大白话 OneCycleLR的哲学是"先冲后收"——像赛车手过弯,直线加速(学习率上升),弯道减速(学习率下降)。这样可以更快地遍历参数空间,尤其在训练初期快速逃离"高原区"。ReduceLROnPlateau则是"看菜下饭"——当validation loss不降了,说明当前学习率太大在最优附近震荡,就减小学习率。

怎么做(选择指南)

场景推荐调度参数建议原因
Transformer(BERT/GPT)Warmup + Cosinewarmup=10%步数, T_max=总步数训练初期不稳定,需要预热
CNN分类(ResNet)Step Decay 或 Cosinestep_size=30epoch, gamma=0.1经典简单任务,阶梯衰减足够
GAN训练ReduceLROnPlateaupatience=5, factor=0.5对抗训练不稳定,自适应调整
小数据集Cosine AnnealingT_max=总步数平滑衰减避免阶梯突变
快速实验OneCycleLR峰值lr=0.01, 周期=总步数快速遍历参数空间
import numpy as np

# 实现更多调度策略并对比
# 包含OneCycleLR和多项式衰减

class OneCycleLR(LRScheduler):
    """OneCycle学习率:前一半上升,后一半下降"""
    def __init__(self, initial_lr, max_lr, total_steps, pct_start=0.3):
        super().__init__(initial_lr)
        self.max_lr = max_lr
        self.total_steps = total_steps
        self.step_size_up = int(total_steps * pct_start)
        self.step_size_down = total_steps - self.step_size_up

    def get_lr(self, step):
        if step < self.step_size_up:
            # 上升阶段:余弦式上升
            cos_val = np.cos(np.pi * step / self.step_size_up)
            return self.initial_lr + 0.5 * (self.max_lr - self.initial_lr) * (1 - cos_val)
        else:
            # 下降阶段:余弦式下降到initial_lr的0.01倍
            pos = step - self.step_size_up
            cos_val = np.cos(np.pi * pos / self.step_size_down)
            return self.initial_lr * 0.01 + 0.5 * (self.max_lr - self.initial_lr * 0.01) * (1 + cos_val)


class PolynomialDecay(LRScheduler):
    """多项式衰减:lr = lr_0 * (1 - step/T_max)^power"""
    def __init__(self, initial_lr, T_max, power=1.0, eta_min=0):
        super().__init__(initial_lr)
        self.T_max = T_max
        self.power = power
        self.eta_min = eta_min

    def get_lr(self, step):
        if step >= self.T_max:
            return self.eta_min
        return (self.initial_lr - self.eta_min) * (1 - step / self.T_max) ** self.power + self.eta_min


# 对比所有调度策略在关键时间点的学习率
print("=== 高级学习率调度策略 ===\n")

total = 1000
warmup = 100

all_schedulers = {
    'Warmup+Cosine': WarmupCosine(0.1, warmup, total),
    'OneCycle': OneCycleLR(1e-4, 0.1, total, pct_start=0.3),
    'Step(gamma=0.1)': StepDecay(0.1, 250, 0.1),
    'Cosine': CosineAnnealing(0.1, total),
    'Poly(power=2)': PolynomialDecay(0.1, total, power=2.0),
    'Exponential(0.997)': ExponentialDecay(0.1, 0.997),
}

print(f"调度策略在关键步数的学习率:\n")
print(f"{'调度器':<20s} {'step=0':>8s} {'step=100':>8s} {'step=500':>8s} {'step=999':>8s}")
print("-" * 56)

for name, sched in all_schedulers.items():
    lrs = [sched.get_lr(s) for s in [0, 99, 499, 999]]
    print(f"  {name:<18s} {lrs[0]:8.6f} {lrs[1]:8.6f} {lrs[2]:8.6f} {lrs[3]:8.6f}")

print("\n→ Warmup+Cosine:蓝色——先上升后平滑下降,初始值为0然后线性增加")
print("→ OneCycle:红色——先上升到峰值再下降,适合快速实验")
print("→ Step:离散跳跃下降,每250步衰减10倍")
print("→ Cosine:从最大值平滑降到0,没有预热阶段")

概念关系图谱

概念核心含义与AI的关系关联概念
学习率调度训练过程中动态调整学习率平衡收敛速度和最终精度的关键技巧优化器、梯度下降
Warmup预热训练初期学习率从极小值线性增长Transformer训练必备,防止初期不稳定余弦退火
余弦退火按余弦曲线从峰值衰减到0最常用的衰减策略,平滑优于阶梯式学习率调度
阶梯衰减每隔固定步数学习率乘以系数(<1)经典策略,简单有效指数衰减
循环学习率学习率在上下界间周期性振荡帮助跳出局部最优,提升泛化SGD
OneCycleLR先上升后下降的单周期策略快速实验,覆盖广泛学习率范围超参数搜索

重点答疑

Q1: 为什么学习率不能一直保持很大?

大学习率在训练初期有益——快速降低损失、越过"高原区"。但接近最优时,大学习率会导致参数在最优附近来回震荡而无法精确收敛。从SGD收敛理论看,需要满足∑η_t → ∞(保证能走足够远)和∑η_t² < ∞(保证能停下来)这两个条件,只有递减学习率能同时满足。

Q2: Warmup预热多少步合适?

经验法则:占总训练步数的5%-15%。具体参考:①BERT原论文4000步预热(总约1M步,0.4%);②GPT-3使用前375M tokens作为warmup(总300B tokens);③Vision Transformer使用10k步预热(总约300k步,3%)。核心是让网络在前几千步稳定下来。预热太短则不足以稳定,太长则浪费训练时间。

Q3: 余弦退火比阶梯衰减好在哪里?

余弦退火在衰减初期较慢(微积分意义上dη/dt在t=0附近为0),给予了充分的大学习率探索时间;在中后期加速衰减,使模型快速聚焦精细搜索。而阶梯衰减在衰减点突然跳跃,可能导致损失突然上升(loss spike)。此外余弦退火是平滑的,更容易配合Warmup形成连续的"上升-下降"曲线。

章节单词汇总

英文音标术语/释义
Learning Rate/ˈlɜːrnɪŋ reɪt/学习率,控制每次参数更新的步长
Scheduling/ˈskedʒuːlɪŋ/调度,训练过程中动态调整学习率
Warmup/ˈwɔːrmʌp/预热,训练初期逐步提升学习率
Cosine Annealing/ˈkoʊsaɪn əˈniːlɪŋ/余弦退火,按余弦曲线衰减学习率
Step Decay/step dɪˈkeɪ/阶梯衰减,每隔固定步数成倍衰减
Plateau/plæˈtoʊ/平台期,损失不再下降的阶段
OneCycle/wʌn ˈsaɪkəl/单周期策略,学习率先升后降
Cyclical LR/ˈsaɪklɪkəl/循环学习率,周期性地在范围内振荡

面试练习

Q1 [单选] Transformer训练中warmup的主要目的是什么?

  • A. 加速训练收敛
  • B. 防止训练初期梯度不稳定
  • C. 减少显存占用
  • D. 提高最终准确率
解答:Transformer由于残差连接和LayerNorm,训练初期参数分布混乱、梯度方差大。Warmup通过让小学习率过渡到目标值,给参数分布时间稳定下来,防止初期梯度爆炸或震荡。

Q2 [单选] 余弦退火(Cosine Annealing)的最终学习率通常是多少?

  • A. 保持初始值
  • B. 初始值的50%
  • C. 接近0(或设定的eta_min)
  • D. 初始值的10倍
解答:余弦退火从初始学习率η_0按余弦曲线衰减到eta_min(通常为0或η_0/1000)。在T_max步时学习率接近最低值,然后再重新开始(如有重启)。

Q3 [多选] 以下哪些是学习率过大的表现?

  • A. 训练损失剧烈震荡
  • B. 验证损失不降反升
  • C. 梯度范数急剧增大
  • D. 损失下降极其缓慢
  • E. 损失变为NaN
解答:学习率过大导致梯度更新过猛,表现为loss震荡、验证loss反弹、梯度爆炸、NaN。损失下降缓慢是学习率过小的表现。

Q4 [单选] ReduceLROnPlateau调度器的触发条件是什么?

  • A. 训练步数达到阈值
  • B. 验证损失在一段时间内不再下降
  • C. 训练损失降为0
  • D. 梯度范数超过阈值
解答:ReduceLROnPlateau监控验证集损失,当patience个epoch内验证损失没有改善时,自动将学习率乘以factor(如0.5)。属于自适应调度策略。

Q5 [多选] 关于学习率调度,以下哪些说法正确?

  • A. Warmup+Cosine是Transformer的标准配置
  • B. 阶梯衰减在衰减点可能导致loss spike
  • C. 余弦退火的衰减曲线比指数衰减更平滑
  • D. 学习率越大越好(只要不NaN)
  • E. 循环学习率可能帮助模型找到更好的局部最优
解答:A是标准实践;B因为突变的参数更新方向;C余弦在t=0附近趋于平坦;E循环学习率通过周期振荡提供了逃离不良局部最优的机会。学习率不是越大越好,过大导致震荡。

Q6 [单选] 指数衰减中gamma=0.999表示什么?

  • A. 每步学习率增加0.1%
  • B. 每步学习率不变
  • C. 每步学习率减小0.1%
  • D. 每10步学习率减半
解答:gamma=0.999表示每步学习率乘以0.999,即每步衰减0.1%。经过ln(2)/ln(1/0.999)≈693步后学习率减半。

Q7 [单选] OneCycleLR策略中学习率先上升的主要原因是什么?

  • A. 快速增大步长以探索更广的参数空间
  • B. 模拟warmup效果
  • C. 弥补梯度消失
  • D. 减少计算开销
解答:OneCycleLR在训练前半段(通常前30%)快速提升学习率,让模型大步探索参数空间,加速逃离初始的高损失区域。后半段逐渐减小学习率以精细收敛。

Q8 [多选] 以下哪些场景适合使用Step Decay(阶梯衰减)?

  • A. ResNet在CIFAR-10上的分类训练
  • B. GPT-4的大规模预训练
  • C. 小型CNN的实验性训练
  • D. 传统机器学习中的SGD优化
  • E. 需要极精细调优的少量步数训练
解答:Step Decay简单有效,适合中等规模的分类任务和小型实验。大规模预训练(如GPT-4)通常用Warmup+Cosine,精细调优任务需要更平滑的衰减策略。

Q9 [单选] 训练BERT时典型的学习率峰值是多少?

  • A. 0.1
  • B. 0.01
  • C. 1e-4到3e-4(Adam优化器下)
  • D. 1e-6
解答:BERT原论文使用Adam优化器,峰值学习率为1e-4,配合前10k步的warmup。使用Adam时学习率通常比SGD小一个数量级,因为Adam自动做参数级别的自适应缩放。

Q10 [多选] 关于warmup,以下哪些说法正确?

  • A. 预热步数通常占总步数的5%-15%
  • B. Transformer需要warmup而简单CNN不一定需要
  • C. 预热期间学习率通常线性增长
  • D. 预热越多越好,应占总步数50%以上
  • E. 预热可以缓解LayerNorm初始化统计量不稳定的问题
解答:预热5%-15%足够;Transformer需要而简单CNN可选;通常线性增长最常用。预热过长(>50%)浪费训练时间却收益递减。预热确实帮助LayerNorm建立合理的统计量。