学习率调度与预热 本节摘要:学习率是最重要的单一超参数——不是架构、不是数据量、不是激活函数。如果只能调一个,就调学习率。把它设成 0.1,训练发散——损失 3 步内冲到无穷;设成 0.0001,训练爬行——100 轮后模型几乎没挪窝;设成 0.01,前 50 轮好好的,之后损失在一个永远到不了的最小周围震荡,因为步子太大。最优学习率不是常数,它在训练中变化:早期你要大步快速铺地盘,后期你要小步钻进尖锐最小。90% 精度的模型与 95% 精度的模型,差距常就在调度上。过去三年发表的每个主流模型都用学习率调度:Llama 3 用峰值 lr=3e-4、2000 步预热、余弦衰减到 3e-5;GPT-3 用 lr=6e-4、3.75 亿 token 预热。
本节摘要:学习率是最重要的单一超参数——不是架构、不是数据量、不是激活函数。如果只能调一个,就调学习率。把它设成 0.1,训练发散——损失 3 步内冲到无穷;设成 0.0001,训练爬行——100 轮后模型几乎没挪窝;设成 0.01,前 50 轮好好的,之后损失在一个永远到不了的最小周围震荡,因为步子太大。最优学习率不是常数,它在训练中变化:早期你要大步快速铺地盘,后期你要小步钻进尖锐最小。90% 精度的模型与 95% 精度的模型,差距常就在调度上。过去三年发表的每个主流模型都用学习率调度:Llama 3 用峰值 lr=3e-4、2000 步预热、余弦衰减到 3e-5;GPT-3 用 lr=6e-4、3.75 亿 token 预热。这些不是拍脑袋,是耗资数百万美元的超参扫描结果。本节从零实现常数、阶跃衰减、余弦退火、预热+余弦、1cycle 五种调度,演示学习率选错的三种失败模式(发散/停滞/震荡),讲清 Adam 类优化器为何需要预热,并在同一任务上比较五种调度的收敛速度。
阅读完本节,你应当能够:
把学习率设成 0.1——训练发散,损失 3 步冲到无穷。设成 0.0001——训练爬行,100 轮后模型几乎没动。设成 0.01——前 50 轮好好的,之后损失在一个永远到不了的最小周围震荡,因为步子太大。
最优学习率不是常数,它在训练中变化。早期你要大步快速铺地盘,后期你要小步钻进尖锐最小。90% 与 95% 精度的模型,差距常就在调度上。
过去三年发表的每个主流模型都用学习率调度。Llama 3 用峰值 lr=3e-4、2000 步预热、余弦衰减到 3e-5;GPT-3 用 lr=6e-4、3.75 亿 token 预热。这些不是拍脑袋,是耗资数百万美元的超参扫描结果。你必须懂调度,因为默认值不会正好适合你的问题——微调预训练模型与从零训练的调度不同,batch 变大预热期要变,训练在第 10000 步崩了,你得知道是调度问题还是别的。
最简:挑一个数,每步都用它。
lr(t) = lr_0
几乎从不最优:要么对训练末期太高(在最周围震荡),要么对初期太低(小步浪费算力)。小模型和调试时还行,任何训练超一小时的都用它就是糟糕选择。
ResNet 时代的老办法:在固定轮次把学习率乘一个因子(通常 10 倍降)。
lr(t) = lr_0 * gamma^(floor(epoch / step_size))
gamma = 0.1、step_size = 30 意味着每 30 轮学习率降 10 倍,ResNet-50 就这么用——lr=0.1,在第 30、60、90 轮各降 10 倍。问题:最优衰减点依赖数据集和架构,换个问题就得重调何时降;切换突兀,学习率突变时损失可能跳。
按余弦曲线从最大学习率平滑衰减到最小:
lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T))
t 是当前步,T 是总步数。t=0 时余弦项为 1,lr = lr_max;t=T 时余弦项为 −1,lr = lr_min。衰减起初温和、中间加速、末尾再变温和。这是多数现代训练的默认,除了 lr_max 与 lr_min 无超参可调。余弦形状契合一个经验观察:大部分学习发生在训练中段——你希望在关键期有合理步长。
Adam 等自适应优化器维护梯度均值与方差的滑动估计。第 0 步这些估计初始化为零,头几次梯度更新基于垃圾统计。若此时学习率大,模型会迈出又大又瞎的步。预热修补这一点:从一个很小的学习率(常为 lr_max / warmup_steps 甚至零)开始,在前 N 步线性升到 lr_max,等你达到完整学习率时,Adam 的统计已稳定。
lr(t) = lr_max * (t / warmup_steps) 当 t < warmup_steps
典型预热:总训练步的 1~5%。Llama 3 训约 1.8 万亿 token、预热 2000 步,GPT-3 预热 3.75 亿 token。
现代默认:线性升,再余弦降。
if t < warmup_steps: lr(t) = lr_max * (t / warmup_steps) else: progress = (t - warmup_steps) / (total_steps - warmup_steps) lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * progress))
Llama、GPT、PaLM 及多数现代 Transformer 都用这套:预热防早期不稳,余弦衰减让模型沉入好的最小。
Leslie Smith(2018)的发现:训练前半段把学习率从低升到高,后半段再降回去。反直觉——为什么中途要加大学习率?理论是:高学习率给优化轨迹加噪,起正则作用,模型在升段探索更多损失面、找到更好的盆;降段则在最好的盆里精修。
阶段 1 (0 到 T/2): lr 从 lr_max/25 升到 lr_max 阶段 2 (T/2 到 T): lr 从 lr_max 降到 lr_max/10000
在固定算力预算下,1cycle 常比余弦退火训练更快,代价是你必须提前知道总步数。
完整代码见原课程 phases/03-deep-learning-core/09-learning-rate-schedules/code/ 相应文件。
每个函数接收当前步、返回该步学习率。
import math def constant_schedule(step, lr=0.01, **kwargs): return lr def step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1, **kwargs): return lr * (gamma ** (step // step_size)) def cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5, **kwargs): if step >= total_steps: return lr_min return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * step / total_steps)) def warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5, **kwargs): if step < warmup_steps: return lr * step / warmup_steps progress = (step - warmup_steps) / (total_steps - warmup_steps) return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * progress)) def one_cycle_schedule(step, lr=0.01, total_steps=1000, **kwargs): mid = max(total_steps // 2, 1) if step < mid: return (lr / 25) + (lr - lr / 25) * step / mid else: progress = (step - mid) / max(total_steps - mid, 1) return lr * (1 - progress) + (lr / 10000) * progress
打印基于文本的图,展示每种调度在训练中的演化。
沿用前几节圆形数据集上的两层网络,但现在轮换调度。
用每种调度训练同一个网络,比较最终损失与收敛行为。预热+余弦通常最稳、1cycle 在固定算力下最快。
演示三种失败模式:lr=1.0 发散(NaN)、lr=0.0001 几乎不动、lr=0.01 收敛但末期震荡。这正是「学习率是最重要超参数」的最直观证据。
PyTorch 在 torch.optim.lr_scheduler 里提供调度器:
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR, StepLR model = nn.Sequential(nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1)) optimizer = optim.Adam(model.parameters(), lr=3e-4) scheduler = CosineAnnealingLR(optimizer, T_max=1000, eta_min=1e-5) for step in range(1000): loss = train_step(model, optimizer) scheduler.step()
预热 + 余弦用 lambda 调度器或 HuggingFace 的 get_cosine_schedule_with_warmup:
from transformers import get_cosine_schedule_with_warmup scheduler = get_cosine_schedule_with_warmup( optimizer, num_warmup_steps=2000, num_training_steps=100000, )
HuggingFace 这个函数是多数 Llama、GPT 微调脚本用的。拿不准时,用预热 + 余弦,预热占总步的 3~5%,几乎万能。
本节产出(位于原课程 outputs/):
prompt-lr-schedule-advisor.md:一个提示,为你的训练设置推荐正确的学习率调度与超参数。下一节,我们把前 9 节的全部零件——层、网络、反向传播、激活、损失、优化器、正则、初始化、调度——焊成一个属于自己的迷你深度学习框架。