学习率调度策略
一句话概述
学习率(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训练的标准配置")
二、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 + Cosine | warmup=10%步数, T_max=总步数 | 训练初期不稳定,需要预热 |
| CNN分类(ResNet) | Step Decay 或 Cosine | step_size=30epoch, gamma=0.1 | 经典简单任务,阶梯衰减足够 |
| GAN训练 | ReduceLROnPlateau | patience=5, factor=0.5 | 对抗训练不稳定,自适应调整 |
| 小数据集 | Cosine Annealing | T_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建立合理的统计量。