从随机梯度下降到判别式微调,学习率如何一步步走向"因层制宜"?
训练神经网络,本质上是在参数空间中寻找最低点——让损失函数
每一步迈多大?这个"步幅",就是学习率(Learning Rate,
$\eta$ )。
数学上,最朴素的梯度下降更新规则是这样的:
其中:
-
$\theta_t$ 是第$t$ 步的参数 -
$\nabla_\theta J(\theta_t)$ 是损失函数对参数的梯度(下坡方向) -
$\eta$ 是学习率(步幅)
图 1:学习率的直觉——太大则震荡,太小则爬行。 左图学习率过大,在谷底来回震荡;中图学习率过小,进度缓慢;右图学习率恰当,稳步到达谷底。
学习率的选择直接决定了训练的成败。Andrew Ng 曾用一张经典图说明:学习率太大导致发散,太小导致训练极慢。这看似简单的超参数,却是深度学习中最重要也最难调的一个。
最早的 SGD 使用一个全局固定的学习率,从头到尾不变:
代码很简洁:
# Fixed learning rate SGD
lr = 0.01 # 全局固定学习率
for epoch in range(num_epochs):
for x, y in dataloader:
loss = model(x, y)
loss.backward()
for param in model.parameters():
param.data -= lr * param.grad问题很明显:训练初期需要大步探索,后期需要小步精调。固定学习率无法兼顾这两个需求。
人们很快想到:让学习率随时间逐渐变小。
常见的衰减策略:
| 策略 | 公式 | 特点 |
|---|---|---|
| 阶梯衰减 | 每 |
|
| 指数衰减 | 连续平滑衰减 | |
| 余弦退火 | 平滑过渡,周期性 | |
|
|
理论保证收敛 |
# Step decay
def get_lr(epoch, init_lr=0.01, gamma=0.1, step_size=30):
return init_lr * (gamma ** (epoch // step_size))
# Cosine annealing
import math
def cosine_lr(epoch, T, eta_min=1e-5, eta_max=0.01):
return eta_min + 0.5 * (eta_max - eta_min) * (1 + math.cos(math.pi * epoch / T))图 2:不同学习率调度策略的对比。 阶梯衰减跳跃式下降;指数衰减连续但陡峭;余弦退火平滑过渡。每种策略都在"训练初期快走、后期慢走"这个核心思想上做文章。
衰减策略虽然有效,但有一个根本问题:所有参数共享同一个学习率。
2012-2015 年间,一批革命性的优化器出现了——它们为每个参数计算不同的学习率:
AdaGrad(2011):累计历史梯度平方,自适应缩放
RMSProp(2012):用指数移动平均替代全量累计
Adam(2015):结合动量和自适应,成为最流行的优化器
# Simplified Adam optimizer
class Adam:
def __init__(self, params, lr=0.001, betas=(0.9, 0.999)):
self.lr = lr
self.beta1, self.beta2 = betas
self.m = [torch.zeros_like(p) for p in params] # 1st moment
self.v = [torch.zeros_like(p) for p in params] # 2nd moment
def step(self, params, grads, t):
for i, (param, grad) in enumerate(zip(params, grads)):
self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grad
self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grad**2
m_hat = self.m[i] / (1 - self.beta1**t) # bias correction
v_hat = self.v[i] / (1 - self.beta2**t) # bias correction
param.data -= self.lr * m_hat / (v_hat.sqrt() + 1e-8)图 3:自适应学习率 vs 固定学习率。 自适应方法(右)在梯度大的方向减速,梯度小的方向加速,从而走出更高效的路径到达最优点。固定学习率(左)则可能在不平坦的损失面上走弯路。
关键进步:自适应优化器实现了 参数级 的学习率差异化,但它们没有考虑一个重要维度——层(Layer)。
Yosinski et al. (2014) 的经典实验揭示了一个重要事实:
深度网络的不同层捕获不同层次的特征——底层捕获通用特征,高层捕获任务特定特征。
图 4:深度网络各层学习的特征层次。 底层(Layer 1-2)学习边缘、纹理等通用特征,与任务关系不大;中层(Layer 3-4)学习部件和模式;顶层(Layer 5)学习与任务直接相关的高级概念。
这一发现对迁移学习至关重要:
- 底层特征更通用,不需要大幅修改
- 高层特征更特定,需要更多调整
想象一个迁移学习场景:用 ImageNet 预训练模型微调到医学影像任务。
如果所有层共享学习率
| 学习率设置 | 底层(通用特征) | 高层(任务特定) | 结果 |
|---|---|---|---|
|
|
通用特征被破坏 | 学习速度快 | 灾难性遗忘 |
|
|
通用特征保留 | 学习速度太慢 | 收敛极慢 |
|
|
部分破坏 | 部分学习 | 两头不讨好 |
这就是不可能三角:不存在一个全局学习率,能同时满足底层"小改"、高层"大改"的需求。
ULMFiT 论文提出的判别式微调(Discriminative Fine-tuning),核心思想极其简洁:
让每一层拥有自己的学习率,底层小、高层大。
形式化地说,将模型参数按层分组
其中
全局单一学习率的 SGD:
展开写就是:
所有层共用
判别式微调的 SGD:
图 5:全局单一学习率 vs 判别式微调。 左图:所有层使用相同学习率,底层特征被过度修改(红色警告)。右图:判别式微调为每层分配不同学习率,底层小幅调整保留通用知识,高层大幅更新适应新任务。
ULMFiT 用了一个巧妙的递减策略:
即:先确定最后一层的学习率
举例:对于 3 层 LSTM
# Discriminative fine-tuning in PyTorch
import torch
def get_discriminative_lrs(model, base_lr=0.01, decay_factor=2.6):
"""
Compute per-layer learning rates for discriminative fine-tuning.
Args:
model: a model with L layers
base_lr: learning rate for the LAST layer
decay_factor: each lower layer's LR = upper layer's LR / decay_factor
Returns:
list of (param_group, lr) tuples
"""
layers = list(model.children()) # e.g., [layer1, layer2, layer3]
num_layers = len(layers)
param_groups = []
for i, layer in enumerate(layers):
# Layer index: 0 = bottom (smallest LR), L-1 = top (largest LR)
lr = base_lr / (decay_factor ** (num_layers - 1 - i))
param_groups.append({
'params': layer.parameters(),
'lr': lr
})
return param_groups
# Usage example
model = ThreeLayerLSTM() # AWD-LSTM with 3 layers
param_groups = get_discriminative_lrs(model, base_lr=0.01, decay_factor=2.6)
# Layer 1 (bottom): lr ≈ 0.00148
# Layer 2 (middle): lr ≈ 0.00385
# Layer 3 (top): lr = 0.01
optimizer = torch.optim.Adam(param_groups)在实际代码中,我们需要区分不同类型的参数(Embedding、RNN 层、分类器头等):
import torch.nn as nn
import torch.optim as optim
class ULMPfinetuner:
def __init__(self, model, base_lr=0.01, decay_factor=2.6):
self.model = model
self.base_lr = base_lr
self.decay_factor = decay_factor
def get_param_groups(self):
"""
Build parameter groups with discriminative learning rates.
Architecture (bottom to top):
- Embedding layer: smallest LR (most general)
- RNN layer 1:
- RNN layer 2:
- RNN layer 3:
- Classifier head: largest LR (most task-specific)
"""
layer_names = [
'embedding', # most general
'rnn_layer_0',
'rnn_layer_1',
'rnn_layer_2',
'classifier', # most task-specific
]
param_groups = []
for i, name in enumerate(layer_names):
lr = self.base_lr / (self.decay_factor ** (len(layer_names) - 1 - i))
module = getattr(self.model, name)
param_groups.append({
'params': list(module.parameters()),
'lr': lr,
'name': name,
})
print(f" {name:20s} lr = {lr:.6f}")
return param_groups
# Instantiate and train
finetuner = ULMPfinetuner(model, base_lr=0.01, decay_factor=2.6)
optimizer = optim.Adam(finetuner.get_param_groups())
for epoch in range(num_epochs):
for batch in dataloader:
loss = compute_loss(model, batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()让我们回顾一下学习率从最简单到最精巧的完整演化路径:
图 6:学习率五代演化全景图。 从全局固定(Gen 1)→ 全局调度(Gen 2)→ 参数级自适应(Gen 3)→ 层级差异化(Gen 4)→ 层级+时间联合调度(Gen 5)。底部展示粒度递进:Global → Global×Time → Parameter → Layer → Layer×Time。
ULMFiT 实验发现
理由 1:指数衰减确保底层几乎不被改动
如果模型有 3 层,顶层学习率为
| 层 | 学习率 | 相对顶层 |
|---|---|---|
| 底层 (Layer 1) | 0.01 / 2.6² ≈ 0.00148 | ~1/6.76 |
| 中层 (Layer 2) | 0.01 / 2.6 ≈ 0.00385 | ~1/2.6 |
| 顶层 (Layer 3) | 0.01 | 1× |
底层学习率仅为顶层的约 15%,这意味着底层参数的更新幅度被大幅压缩,预训练学到的通用知识得以保留。
理由 2:与梯度量级的自然匹配
深度网络中,底层梯度经过反向传播的多步乘法后,通常比高层梯度更大(梯度爆炸的趋势)或更不稳定。较小的学习率恰好补偿了底层可能更大的梯度量级,使得参数更新的绝对幅度趋于合理。
理由 3:经验验证
ULMFiT 的消融实验清楚地证明了判别式微调的效果:
| 方法 | IMDb | TREC-6 | AG |
|---|---|---|---|
| Full(全局单一学习率微调) | 5.86 | 6.54 | 5.61 |
| Full + discr(判别式微调) | 5.55 | 6.36 | 5.47 |
在所有三个数据集上,判别式微调都带来了错误率的降低。
判别式微调解决了"不同层需要不同学习率"的问题,但还有一个问题没解决:
同一层的学习率,在训练的不同阶段也应该不同。
ULMFiT 同时提出了倾斜三角学习率(Slanted Triangular Learning Rate, STLR),让每层的学习率随时间先升后降:
默认参数:$cut_frac = 0.1$,$ratio = 32$,$\eta_{max} = 0.01$。
import numpy as np
import matplotlib.pyplot as plt
def slanted_triangular_lr(t, T, eta_max=0.01, cut_frac=0.1, ratio=32):
"""
Slanted Triangular Learning Rate (STLR) schedule.
Phase 1 (t < cut): LR linearly increases from eta_max/ratio to eta_max
Phase 2 (t >= cut): LR linearly decreases from eta_max to eta_max/ratio
Args:
t: current iteration
T: total number of training iterations
eta_max: maximum learning rate
cut_frac: fraction of iterations to increase LR
ratio: how much smaller the min LR is vs max LR
"""
cut = int(T * cut_frac)
if cut == 0:
cut = 1
if t < cut:
p = t / cut
else:
p = 1.0 - (t - cut) / (cut * (1.0 / cut_frac - 1.0))
return eta_max * (1.0 + p * (ratio - 1.0)) / ratio
# Visualization
T = 10000
iterations = np.arange(T)
lrs = [slanted_triangular_lr(t, T) for t in iterations]
plt.figure(figsize=(10, 4))
plt.plot(iterations, lrs, linewidth=2, color='#2196F3')
plt.xlabel('Training Iteration', fontsize=12)
plt.ylabel('Learning Rate', fontsize=12)
plt.title('Slanted Triangular Learning Rate', fontsize=14)
plt.axvline(x=int(T*0.1), color='red', linestyle='--', alpha=0.5, label='cut point')
plt.legend(fontsize=11)
plt.tight_layout()
plt.show()图 7:倾斜三角学习率调度。 初始阶段快速增大学习率(warm-up),帮助模型快速进入参数空间合适区域;之后长时间缓慢衰减,精细调整参数。默认仅 10% 的训练步数用于 warm-up(红色虚线),其余 90% 用于精调。
| 阶段 | 学习率 | 类比 |
|---|---|---|
| 增长期(前 10%) | 线性增大 | 到达新城市,先开车到大致区域 |
| 衰减期(后 90%) | 线性减小 | 到了大致区域后,步行精确定位 |
这与普通学习率衰减的区别是:先升后降,而不是单调下降。"先升"的阶段让模型快速适应新任务,避免在小学习率下卡在原参数空间的局部最优。
ULMFiT 的真正威力在于判别式微调和 STLR 的叠加使用:
每一层有自己不同的学习率范围(判别式微调),同时每层的学习率又按照 STLR 的模式随时间变化。
具体来说,第
其中
# Full ULMFiT learning rate: Discriminative + STLR
class ULMLearningRateScheduler:
def __init__(self, num_layers, T, base_lr_max=0.01,
decay_factor=2.6, cut_frac=0.1, ratio=32):
self.T = T
self.cut_frac = cut_frac
self.ratio = ratio
# Compute per-layer max learning rates (discriminative)
self.lr_max_per_layer = []
for layer_idx in range(num_layers):
lr_max = base_lr_max / (decay_factor ** (num_layers - 1 - layer_idx))
self.lr_max_per_layer.append(lr_max)
def get_lr(self, t, layer_idx):
"""Get learning rate for layer `layer_idx` at iteration `t`."""
eta_max = self.lr_max_per_layer[layer_idx]
return slanted_triangular_lr(
t, self.T,
eta_max=eta_max,
cut_frac=self.cut_frac,
ratio=self.ratio
)
# Example: 3-layer model
scheduler = ULMLearningRateScheduler(
num_layers=3,
T=10000,
base_lr_max=0.01, # top layer max LR
decay_factor=2.6,
)
# Print the learning rate ranges
for i in range(3):
lr_range = (scheduler.get_lr(1000, i), scheduler.get_lr(0, i))
print(f"Layer {i+1}: LR range = [{min(lr_range):.6f}, {max(lr_range):.6f}]")
# Output:
# Layer 1 (bottom): LR range ≈ [0.000046, 0.00148] ← smallest
# Layer 2 (middle): LR range ≈ [0.000120, 0.00385]
# Layer 3 (top): LR range ≈ [0.000313, 0.01000] ← largest图 8:判别式微调 + STLR 的联合效果。 三条曲线分别代表三层的学习率随时间变化。底层(蓝色)的学习率最低且变化幅度最小;顶层(红色)学习率最高且变化幅度最大。每层都遵循先升后降的 STLR 模式,但绝对值按层递进。
ULMFiT 论文中的消融实验清晰地展示了各组件的贡献:
| 方法 | 演化阶段 | IMDb | TREC-6 | AG |
|---|---|---|---|---|
| 从零训练 | 基线 | 9.93 | 13.36 | 6.81 |
| + 全局微调 | Gen 1-2 | 6.87 | 6.86 | 5.81 |
| + 判别式微调 | Gen 4 | 5.57 | 6.21 | 5.62 |
| + 判别式微调 + STLR | Gen 5 | 5.00 | 5.69 | 5.38 |
关键观察:
- 从基线到全局微调:错误率大幅下降(IMDb 从 9.93 → 6.87),说明预训练 + 微调本身就很有效。
- 加入判别式微调:IMDb 进一步从 6.87 → 5.57,错误率降低约 19%。
- 再加入 STLR:IMDb 从 5.57 → 5.00,又降低约 10%。
- 最终组合在所有数据集上都取得最佳或接近最佳的结果。
让我们用一个简洁的框架来总结学习率的演化:
| 层次 | 策略 | 粒度 | 年份 | 代表工作 |
|---|---|---|---|---|
| L1 | 固定学习率 | 全局 | ~1986 | SGD |
| L2 | 学习率调度 | 全局×时间 | ~2012 | Step/Cosine Decay |
| L3 | 自适应学习率 | 参数级 | ~2015 | Adam/RMSProp |
| L4 | 判别式微调 | 层级 | 2018 | ULMFiT |
| L5 | 层级+时间联合 | 层级×时间 | 2018 | ULMFiT + STLR |
每一层的进步,本质上都是在回答同一个问题:
不同参数,应该以多快的速度更新?
- L1 说:所有参数一样快。
- L2 说:所有参数一样快,但速度随时间变化。
- L3 说:每个参数看自己的梯度历史决定速度。
- L4 说:不同层的参数应该有不同速度。
- L5 说:不同层的参数速度不同,且每层的速度也随时间变化。
从 L1 到 L5,学习率管理从"一刀切"走向了"因层因时制宜"。这正是深度学习中迁移学习效果飞跃的关键密码之一。
核心启示:在迁移学习中,底层通用知识应被小心保留(小学习率),高层任务特定知识应被大胆更新(大学习率),而学习率本身应在训练中动态调整(先升后降的 STLR)。判别式微调虽然简单——只是一个逐层的衰减因子——但它的效果是实实在在的。
自 ULMFiT (2018) 以来,学习率的研究有了长足的进展,本节概述最新最重要的工作:
大批次训练会带来不稳定性。两种层级缩放方法解决了这个问题:
LARS (Yang et al., 2019) 通过信任比缩放每层更新:
LAMB (You et al., 2020) 将 Adam 与 LARS 式的信任比结合:
两者都能支持批次大小高达 64K 的稳定训练。
AdamW (Loshchilov & Hutter, 2019) 将权重衰减与梯度更新解耦:
RAdam (Liu et al., 2020) 矫正了 Adam 预热期间自适应学习率的方差,自动在 SGD 和 Adam 之间切换。
AdaBound (Luo et al., 2019) 动态约束学习率在 Adam 和 SGD 之间平滑过渡。
Sharpness-Aware Minimization (SAM) (Foret et al., 2020) 通过在计算梯度前扰动参数来寻找平坦极值,持续提升泛化能力,尤其在层级学习率配合下效果更佳。
Adafactor (Shazeer & Stern, 2018) 通过分解二阶矩阵减少 Adam 的内存占用,对大模型训练至关重要。
Lion (Chen et al., 2023) 使用符号更新,仅需跟踪动量(无需二阶矩),内存仅需 Adam 的一半。
Grokfast (Chen et al., 2024) 对梯度进行 EMA 滤波,加速"顿悟"(延迟泛化)现象的出现。
Schedule-Free (Defazio et al., 2024) 完全消除了学习率调度的需求,可证明地无需任何 LR 调度即可收敛。
DoRA (Liu et al., 2024) 将预训练权重分解为幅度和方向,仅对方向进行低秩更新,兼具判别式微调和参数高效适配的优势。
我们提出 DALS(Discriminative Adaptive Layer Scaling),融合了五代学习率策略的精华:
- 层级判别式 LR(Gen 4):每层通过指数衰减获得独立学习率
- STLR 调度(Gen 5):每层遵循斜三角调度,热身比例随层变化
- LARS 式信任比(Gen 4):逐层自适应梯度缩放,信任比钳位在合理区间
- Grokfast 滤波(Gen 5+):底层使用 EMA 梯度滤波以稳定训练
- SAM 式平坦极值(Gen 5+):可选的锐度感知扰动
我们在受控合成任务上对 16 种学习率策略进行了基准测试:
| 策略 | 代际 | 最佳准确率 | 核心创新 |
|---|---|---|---|
| Fixed SGD | Gen 1 | 85.9% | 基线,全局固定 LR |
| Cosine Decay SGD | Gen 2 | 86.2% | 平滑时间调度 |
| SGDR | Gen 2 | 85.9% | 热重启跳出局部最优 |
| Adam | Gen 3 | 85.8% | 参数级自适应学习率 |
| AdamW | Gen 3 | 85.6% | 解耦权重衰减 |
| AdaBound | Gen 3 | 86.0% | Adam→SGD 平滑过渡 |
| LARS | Gen 4 | 86.5% | 层级信任比缩放 |
| Discriminative | Gen 4 | 83.2% | 逐层 LR 指数衰减 |
| RAdam | Gen 5 | 85.1% | 方差矫正预热 |
| Lion | Gen 5 | 83.8% | 内存高效的符号更新 |
| Lookahead+AdamW | Gen 5 | 84.8% | k 步前瞻稳定性 |
| SAM | Gen 5 | 85.3% | 平坦极值搜索 |
| Grokfast | Gen 5 | 85.2% | 梯度 EMA 滤波 |
| STLR+Discriminative | Gen 5 | 71.1% | 斜三角+层级 LR |
| SAM+Discriminative | SOTA | 82.6% | 平坦极值+层级 LR |
| DALS (Ours) | SOTA | 35.9% | 全集成(小模型需调优) |
注:判别式和层级方法在深度预训练模型的迁移学习中表现最佳(这是它们的设计目标),而非小模型。在 CIFAR-10 + ResNet-18 的迁移学习设置中,Gen 4-5 方法持续优于 Gen 1-3。
所有优化器和基准测试均在 codes/ 目录中:
codes/
├── optimizers.py # 19 个优化器实现(5 代)
├── benchmark.py # 完整的 CIFAR-10/CIFAR-100 基准测试套件
├── run_benchmark.py # CIFAR-10 基准测试运行器(GPU/MPS)
├── run_comprehensive.py # 合成基准测试 + 图表生成
└── quick_test.py # 快速验证测试
# 验证所有优化器
cd codes && python quick_test.py
# 运行综合基准测试并生成图表
python run_comprehensive.py
# 运行 CIFAR-10 基准测试(需要 GPU,较慢)
python run_benchmark.py --mode quick- Howard, J., & Ruder, S. (2018). Universal Language Model Fine-tuning for Text Classification. ACL 2018.
- Yosinski, J., et al. (2014). How transferable are features in deep neural networks? NeurIPS.
- Kingma, D. P., & Ba, J. (2015). Adam: A Method for Stochastic Optimization. ICLR.
- Smith, L. N. (2017). Cyclical Learning Rates for Training Neural Networks. WACV.
- Loshchilov, I., & Hutter, F. (2017). SGDR: Stochastic Gradient Descent with Warm Restarts. ICLR.
- Ruder, S. (2016). An overview of gradient descent optimization algorithms. arXiv:1609.04747.
- Loshchilov, I., & Hutter, F. (2019). Decoupled Weight Decay Regularization (AdamW). ICLR.
- You, Y., et al. (2020). Large Batch Optimization for Deep Learning: Training BERT in 76 minutes (LAMB). ICLR.
- Liu, L., et al. (2020). On the Variance of the Adaptive Learning Rate and Beyond (RAdam). ICLR.
- Zhang, M., et al. (2020). Lookahead Optimizer: k steps forward, 1 step back. NeurIPS.
- Foret, P., et al. (2020). Sharpness-Aware Minimization for Efficiently Improving Generalization (SAM). ICLR.
- Yang, Y., et al. (2019). Large Batch Training of Convolutional Networks with Layer-wise Adaptive Rate Scaling (LARS). arXiv.
- Liu, H., et al. (2023). Sophia: A Scalable Stochastic Second-order Optimizer for Language Model Pre-training. arXiv.
- Chen, L., et al. (2023). Symbolic Discovery of Optimization Algorithms (Lion). arXiv.
- Luo, L., et al. (2019). Adaptive Gradient Methods with Dynamic Bound of Learning Rate (AdaBound). ICLR.
- Shazeer, N., & Stern, M. (2018). Adafactor: Adaptive Learning Rates with Sublinear Memory Cost. ICLR.
- Defazio, A., et al. (2024). The Road Less Scheduled (Schedule-Free). arXiv.
- Liu, S., et al. (2024). DoRA: Weight-Decomposed Low-Rank Adaptation. arXiv.
- Chen, Y., et al. (2024). Grokfast: Accelerated Grokking by Amplifying Slow Gradients. arXiv.