大模型如何"学习":从梯度下降到AdamW优化器
引言:什么是"学习"?
在前面的章节中,我们学习了Transformer的各个组件:注意力机制、MLP、残差连接、LM Head等。但有一个核心问题我们还没有回答:
这些参数(、、、、Embedding等)是怎么得到的?
答案是:通过训练(Training)学习得到的!
类比:
想象你在学习投篮:
- 初始状态:随便投,命中率很低(随机初始化)
- 观察结果:投偏了,偏左边10厘米(计算误差)
- 调整姿势:下次往右边调整一点(参数更新)
- 重复练习:不断投篮、观察、调整(迭代训练)
- 最终:命中率很高(模型收敛)
深度学习的训练过程就是这样:通过不断调整参数,让模型的输出越来越接近正确答案。
前向传播(Forward Propagation)
定义
前向传播是指数据从输入层经过各层计算,最终得到输出的过程。
Transformer的前向传播
让我们用一个具体例子看完整的前向传播流程:
输入:"今天天气" 目标:预测下一个词(应该是"很好"、"不错"等)
步骤1:Token Embedding
步骤2:位置编码
步骤3:通过第1层Transformer
步骤4:通过第2-12层
步骤5:LM Head
取最后一个位置的隐藏状态,映射到词表。
步骤6:Softmax
得到每个词的概率。
假设输出:
| 词 | 概率 |
|---|---|
| 很好 | 45% |
| 不错 | 30% |
| 真棒 | 15% |
| 差 | 5% |
| ... | ... |
关键点
前向传播的特点:
- 确定性:给定输入和参数,输出是确定的
- 单向性:只能从输入到输出,不能反过来
- 快速:主要是矩阵乘法,GPU加速很快
用途:
- 训练时:计算输出,然后计算Loss
- 推理时:直接用来生成文本
损失函数(Loss Function)
前向传播得到了输出(概率分布),但如何知道这个输出好不好?这就需要损失函数。
什么是损失函数?
**损失函数(Loss Function)**衡量模型输出与真实答案之间的差距:
Loss越小,说明模型越好!
交叉熵损失(Cross-Entropy Loss)
对于语言模型,最常用的是交叉熵损失:
直观理解:
- 如果模型给正确答案的概率很高(接近1),则 (损失小)
- 如果模型给正确答案的概率很低(接近0),则 (损失大)
具体例子
输入:"今天天气" 真实答案:"很好"(Token ID: 9527)
模型输出(前向传播后):
| 词 | 概率 | |
|---|---|---|
| 很好 | 0.45 | 0.80 |
| 不错 | 0.30 | 1.20 |
| 真棒 | 0.15 | 1.90 |
| 差 | 0.05 | 3.00 |
| ... | ... | ... |
计算Loss:
如果模型预测得更准(概率0.9):
如果模型预测得很差(概率0.01):
数学形式
更正式地,对于词表大小为 的分类问题:
其中:
- :真实标签的one-hot编码(正确答案为1,其余为0)
- :模型预测的概率
由于 是one-hot,只有正确答案那一项不为0,所以简化为:
训练目标
训练的目标就是最小化Loss:
其中:
- :所有模型参数(、、Embedding等)
- :模型的输出
- :对所有训练数据的期望
通俗理解:找到一组参数,让模型在所有训练样本上的平均Loss最小。
反向传播(Backward Propagation)
知道了Loss,如何调整参数让Loss变小?这就需要反向传播。
核心思想
**梯度(Gradient)**告诉我们:参数往哪个方向调整,Loss会下降最快。
直观理解:
想象你在山上迷雾中,想下山(最小化Loss):
- 梯度:指向上坡最陡的方向
- 负梯度:指向下坡最陡的方向
- 沿着负梯度走:最快下山
链式法则(Chain Rule)
反向传播的数学基础是链式法则:
从后往前传:
- 计算 (Loss对输出的梯度)
- 计算 (输出对中间层的梯度)
- 计算 (中间层对参数的梯度)
- 相乘得到 (Loss对参数的梯度)
具体例子:简单MLP的反向传播
假设一个简单的MLP:
各步骤说明:
- 第一层线性变换
- ReLU激活函数
- 第二层线性变换(输出)
- 计算损失(均方误差)
前向传播(假设 ,):
反向传播:
注意:ReLU的导数在 时为1,在 时为0。这里 ,所以导数为1。
得到梯度:
自动微分(Automatic Differentiation)
好消息:我们不需要手动计算梯度!
现代深度学习框架(PyTorch、TensorFlow)会自动计算梯度:
import torch
# 定义参数(requires_grad=True表示需要计算梯度)
W1 = torch.tensor([[3.0]], requires_grad=True)
b1 = torch.tensor([1.0], requires_grad=True)
W2 = torch.tensor([[2.0]], requires_grad=True)
b2 = torch.tensor([0.0], requires_grad=True)
# 前向传播
x = torch.tensor([2.0])
y_true = torch.tensor([5.0])
h = W1 @ x + b1
h_act = torch.relu(h)
y = W2 @ h_act + b2
loss = (y - y_true) ** 2
# 反向传播(自动计算梯度)
loss.backward()
# 查看梯度
print(f"∂L/∂W1 = {W1.grad}") # tensor([[72.]])
print(f"∂L/∂b1 = {b1.grad}") # tensor([36.])
print(f"∂L/∂W2 = {W2.grad}") # tensor([[126.]])
print(f"∂L/∂b2 = {b2.grad}") # tensor([18.])
完全自动!我们只需要定义前向传播,反向传播由框架完成。
Transformer的反向传播
对于一个12层的GPT-2模型:
- 前向传播:输入 → Embedding → Layer1 → ... → Layer12 → LM Head → Loss
- 反向传播:Loss → → → ... → →
计算图:
前向:
x → E_token → Layer1 → Layer2 → ... → Layer12 → LM_Head → logits → softmax → loss
↓ ↓ ↓ ↓ ↓ ↓ ↓
反向: ← ← ... ← ← ← ← ←
∂L/∂E ∂L/∂W1 ∂L/∂W12 ∂L/∂Wlm ∂L/∂logits ∂L/∂P ∂L/∂L
每个参数都会得到一个梯度,告诉我们如何更新它。
梯度下降(Gradient Descent)
有了梯度,就可以更新参数了。
基本思想
沿着负梯度方向更新参数:
其中:
- :参数(如 、 等)
- :学习率(Learning Rate)
- :梯度
直观理解:
- 梯度正:参数增大会让Loss增大 → 减小参数
- 梯度负:参数增大会让Loss减小 → 增大参数
具体例子
继续上面的MLP例子:
当前参数:,梯度
选择学习率:
更新参数:
同理:
验证:用新参数再做一次前向传播,Loss应该变小了。
三种梯度下降
1. Batch Gradient Descent(批量梯度下降)
每次使用所有训练数据计算梯度:
优点:
- 梯度准确,收敛稳定
缺点:
- 计算量大(N可能有百万级)
- 内存不够(无法一次加载所有数据)
- 更新慢(每个epoch只更新一次)
2. Stochastic Gradient Descent (SGD,随机梯度下降)
每次只用一个样本计算梯度:
优点:
- 更新快(每个样本都更新)
- 内存友好
缺点:
- 梯度噪声大,不稳定
- 可能震荡,难以收敛到最优点
3. Mini-Batch Gradient Descent(小批量梯度下降)
每次用一小批样本(如32、64、128个)计算梯度:
其中 是batch size(批量大小)。
优点:
- 平衡了Batch GD和SGD的优缺点
- 梯度相对稳定
- 可以利用GPU并行计算
缺点:
- 需要调整batch size
这是现代深度学习的标准做法!
训练循环
完整的训练过程:
for epoch in range(num_epochs): # 遍历多个epoch
for batch in dataloader: # 遍历所有batch
# 1. 前向传播
outputs = model(batch['input'])
loss = loss_fn(outputs, batch['target'])
# 2. 反向传播
loss.backward() # 计算梯度
# 3. 参数更新
optimizer.step() # 更新参数
# 4. 梯度清零(为下一个batch准备)
optimizer.zero_grad()
学习率(Learning Rate)
学习率 是梯度下降中最重要的超参数。
学习率的影响
学习率太大(如 )
- 参数更新幅度太大
- 可能"跳过"最优点
- Loss震荡或发散
Loss
^
| /\ /\
| / \ / \
| / \/ \
| /
+-----------------> Iteration
学习率太小(如 )
- 参数更新幅度太小
- 收敛极其缓慢
- 可能卡在局部最优
Loss
^
|
| ___
| /
| /
|/_______________
+-----------------> Iteration
很长时间才下降一点
学习率合适(如 )
- 稳定下降
- 收敛速度合理
Loss
^
|
|\
| \
| \___
| ----____
+-----------------> Iteration
典型的学习率值
| 模型规模 | 典型学习率 |
|---|---|
| 小模型(<100M) | 1e-3 ~ 1e-4 |
| 中型模型(100M-1B) | 5e-4 ~ 1e-4 |
| 大模型(1B-100B+) | 1e-4 ~ 1e-5 |
为什么大模型用更小的学习率?
- 参数量大,梯度累积效应强
- 需要更细致的调整
- 避免训练不稳定
学习率调度(Learning Rate Scheduling)
问题:固定学习率不是最优的。
解决方案:在训练过程中动态调整学习率。
1. Warmup(预热)
训练初期使用很小的学习率,然后逐渐增大:
原因:
- 训练初期,参数是随机的,梯度可能很大
- 小学习率避免参数被"破坏"
- 通常warmup 1000-10000步
可视化:
Learning Rate
^
| /----------
| /
| /
| /
| /
+----+--------------> Step
warmup
2. Cosine Annealing(余弦退火)
学习率按余弦曲线衰减:
可视化:
Learning Rate
^
| ___
| / \___
| / \___
| / \___
| / ---
+----------------------->\__Step
warmup cosine decay
3. 组合:Warmup + Cosine Decay
GPT-3等大模型常用的策略:
def get_lr(step, warmup_steps, max_steps, lr_max, lr_min):
if step < warmup_steps:
# Warmup阶段:线性增长
return lr_max * step / warmup_steps
else:
# Cosine衰减阶段
progress = (step - warmup_steps) / (max_steps - warmup_steps)
return lr_min + 0.5 * (lr_max - lr_min) * (1 + math.cos(math.pi * progress))
AdamW优化器
虽然梯度下降很简单,但实践中有很多问题。AdamW是目前训练大模型的标准优化器。
SGD的问题
标准的梯度下降(SGD):
其中 是当前梯度。
问题1:不同参数的梯度尺度差异大
- 某些参数的梯度很大(如1000)
- 某些参数的梯度很小(如0.001)
- 用相同的学习率无法兼顾
问题2:梯度噪声
- Mini-batch的梯度有噪声
- 可能在最优点附近震荡
问题3:高维空间的"峡谷"
- 某些方向梯度大,某些方向梯度小
- SGD在峡谷中会"之字形"前进,效率低
Adam优化器
Adam(Adaptive Moment Estimation) 通过维护梯度的一阶矩(均值)和二阶矩(方差)来自适应调整每个参数的学习率。
Adam的核心思想
- 动量(Momentum):累积历史梯度的指数移动平均
- 自适应学习率:根据梯度的历史方差调整学习率
Adam算法(简化版)
初始化:
其中:
- :一阶矩初始值(梯度的均值)
- :二阶矩初始值(梯度的方差)
每次迭代:
各步骤说明:
- :计算当前梯度
- :更新一阶矩(梯度的指数移动平均)
- :更新二阶矩(梯度平方的指数移动平均)
- :偏差修正后的一阶矩
- :偏差修正后的二阶矩
- :根据修正后的矩更新参数
参数解释:
- :一阶矩的衰减率(通常)
- :二阶矩的衰减率(通常)
- :防止除零的小常数
- :偏差修正后的一阶矩
- :偏差修正后的二阶矩
直观理解
一阶矩 (动量):
- 累积历史梯度的加权平均
- 让参数更新更平滑,减少震荡
- 类似物理中的"惯性"
二阶矩 (方差):
- 累积历史梯度平方的加权平均
- 反映梯度的波动程度
- 梯度波动大的参数,学习率自动减小
自适应学习率:
- 梯度波动大(大):学习率小
- 梯度波动小(小):学习率大
AdamW:Adam + Weight Decay
AdamW是Adam的改进版本,专门针对深度学习中的权重衰减(Weight Decay)。
什么是权重衰减?
问题:如果不加限制,神经网络的参数可能会变得非常大。
# 举个例子
# 假设模型学到了这样的参数:
W = [[100, -200],
[300, -400]]
# 过大的参数会导致:
1. 过拟合:模型对训练数据"记住"而不是"理解"
2. 数值不稳定:计算时容易溢出
3. 泛化能力差:在新数据上表现不好
权重衰减是一种正则化技术,通过惩罚大参数来防止上述问题:
其中:
- :原始损失函数
- :权重衰减系数(如0.01、0.1)
- :参数的L2范数
直观理解:
不用权重衰减:
Loss = 预测错误程度
使用权重衰减:
Loss = 预测错误程度 + λ × (参数大小的惩罚)
模型需要在"预测准确"和"参数不要太大"之间取得平衡
Adam中的权重衰减问题
在标准Adam中,权重衰减是通过把 加到梯度上实现的:
然后用这个"带权重衰减的梯度"来更新动量和方差:
注意:这里的 包含了权重衰减项 ,因此 和 都会受到权重衰减的影响。
问题在哪里?
权重衰减项 被混入了自适应学习率的计算中:
# 假设某个参数 θ = 10(比较大)
# 真实梯度 g = 0.1(比较小)
# λ = 0.01
# Adam中:
g_with_decay = 0.1 + 0.01 × 10 = 0.2
# 这个0.2会被用来计算 v_t(梯度方差)
v_t = 0.999 × v_{t-1} + 0.001 × 0.2² = 0.999 × v_{t-1} + 0.00004
# 因为参数大 → 权重衰减项大 → g_with_decay大 → v_t变大
# 而实际学习率 = lr / √v_t
# v_t变大 → 实际学习率变小 → 权重衰减效果被削弱!
# 结果:权重衰减的效果被自适应学习率"对冲"了
本质问题:权重衰减应该独立于梯度大小,但在Adam中它却受到自适应学习率的影响。
AdamW的解决方案
AdamW将权重衰减从梯度中解耦,直接在参数更新时应用:
标准Adam的做法(错误):
步骤1:g_t = ∂L/∂θ + λθ (梯度混入权重衰减)
步骤2:m_t = β₁×m_{t-1} + (1-β₁)×g_t
步骤3:v_t = β₂×v_{t-1} + (1-β₂)×g_t²
步骤4:θ_{t+1} = θ_t - lr × m̂_t/(√v̂_t + ε)
↑
权重衰减效果被自适应学习率削弱
AdamW的做法(正确):
步骤1:g_t = ∂L/∂θ
(纯梯度,不含权重衰减)
步骤2:m_t = β₁×m_{t-1} + (1-β₁)×g_t
步骤3:v_t = β₂×v_{t-1} + (1-β₂)×g_t²
步骤4:θ_{t+1} = θ_t - lr × [m̂_t/(√v̂_t + ε) + λ×θ_t]
↑ ↑
Adam更新 权重衰减(独立)
数学形式:
或者分两步写更清楚:
其中:
- 第一步是标准的Adam更新
- 第二步是权重衰减,独立应用
具体例子对比:
# 继续上面的例子
# θ = 10, 真实梯度 g = 0.1, λ = 0.01
# AdamW中:
g_t = 0.1 # 只用真实梯度
v_t = 0.999 × v_{t-1} + 0.001 × 0.1² # 不受权重衰减影响
# Adam更新部分:
Δθ_adam = lr × 0.1 / √v_t
# 权重衰减部分(独立应用):
Δθ_decay = lr × λ × θ = lr × 0.01 × 10 = lr × 0.1
# 总更新:
θ_{t+1} = θ - Δθ_adam - Δθ_decay
# 关键:权重衰减不受自适应学习率影响!
# 参数越大,衰减越强,完全符合正则化的本意
可视化对比:
Adam (标准权重衰减):
┌─────────────────────────────────────┐
│ 梯度 g + 权重衰减 λθ │
└──────────┬──────────────────────────┘
↓
┌──────────────┐
│ 计算 m_t │ ← 权重衰减混入动量
│ 计算 v_t │ ← 权重衰减影响方差
└──────┬───────┘
↓
自适应学习率 lr/√v_t
↓
参数更新 ← 权重衰减效果被削弱 ✗
AdamW (解耦权重衰减):
┌─────────────────────────────────────┐
│ 纯梯度 g (不含权重衰减) │
└──────────┬──────────────────────────┘
↓
┌──────────────┐
│ 计算 m_t │ ← 只用真实梯度
│ 计算 v_t │ ← 只用真实梯度
└──────┬───────┘
↓
┌──────────────┐ ┌──────────────┐
│ Adam更新 │ + │ 权重衰减独立 │
│ lr×m̂/√v̂ │ │ lr×λ×θ │
└──────┬───────┘ └──────┬───────┘
└──────────┬───────────┘
↓
参数更新 ← 权重衰减效果完整 ✓
效果对比:
| 对比项 | Adam | AdamW |
|---|---|---|
| 权重衰减位置 | 混入梯度中 | 独立于梯度 |
| 权重衰减是否受自适应学习率影响 | 是(被削弱) | 否(独立) |
| 大参数的衰减效果 | 较弱 | 强(符合预期) |
| 大模型训练效果 | 较差 | 更好 |
| 泛化能力 | 一般 | 更好 |
| 是否被广泛采用 | 较少 | GPT-3、LLaMA标准 |
总结:
- Adam:,然后用 更新 和 → 权重衰减效果被自适应学习率削弱
- AdamW:,更新 和 后,再独立应用权重衰减 → 权重衰减效果完整保留
这就是为什么现代大模型训练都使用AdamW而不是Adam!
AdamW的超参数
| 参数 | 典型值 | 说明 |
|---|---|---|
| (学习率) | 1e-4 ~ 1e-5 | 最重要的超参数 |
| 0.9 | 一阶矩衰减率 | |
| 0.999 或 0.95 | 二阶矩衰减率 | |
| (weight decay) | 0.01 ~ 0.1 | 权重衰减系数 |
| 1e-8 | 数值稳定性常数 |
GPT-3的设置:
- Learning rate: 6e-5(with warmup and cosine decay)
- = 0.9
- = 0.95
- Weight decay: 0.1
PyTorch实现
import torch
import torch.optim as optim
# 定义模型
model = GPT2Model(...)
# 创建AdamW优化器
optimizer = optim.AdamW(
model.parameters(),
lr=6e-5, # 学习率
betas=(0.9, 0.95), # (β₁, β₂)
weight_decay=0.1, # 权重衰减
eps=1e-8 # ε
)
# 训练循环
for batch in dataloader:
# 前向传播
outputs = model(batch['input'])
loss = loss_fn(outputs, batch['target'])
# 反向传播
loss.backward()
# 梯度裁剪(防止梯度爆炸)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 参数更新(AdamW)
optimizer.step()
# 梯度清零
optimizer.zero_grad()
完整的训练流程
让我们把所有概念串起来,看一个完整的训练流程:
伪代码
# 1. 初始化
model = Transformer(...) # 随机初始化参数
optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.1)
lr_scheduler = CosineAnnealingWarmup(optimizer, warmup_steps=2000)
# 2. 训练循环
for epoch in range(num_epochs):
for batch in dataloader:
# (1) 前向传播
input_ids = batch['input'] # [batch_size, seq_len]
target_ids = batch['target'] # [batch_size, seq_len]
logits = model(input_ids) # [batch_size, seq_len, vocab_size]
# (2) 计算Loss
loss = cross_entropy(logits, target_ids)
# (3) 反向传播
loss.backward() # 自动计算所有参数的梯度
# (4) 梯度裁剪(可选,防止梯度爆炸)
clip_grad_norm_(model.parameters(), max_norm=1.0)
# (5) 参数更新(AdamW)
optimizer.step()
# (6) 学习率调整
lr_scheduler.step()
# (7) 梯度清零
optimizer.zero_grad()
# (8) 打印进度
if step % 100 == 0:
print(f"Epoch {epoch}, Step {step}, Loss: {loss.item():.4f}, LR: {lr_scheduler.get_last_lr()[0]:.6f}")
完整流程图
初始化模型参数(随机)
↓
┌─────────────────────── Training Loop ───────────────────────┐
│ │
│ 加载一个batch的数据 │
│ ↓ │
│ 前向传播:input → Transformer → logits │
│ ↓ │
│ 计算Loss:Cross-Entropy(logits, target) │
│ ↓ │
│ 反向传播:计算梯度 ∂L/∂θ │
│ ↓ │
│ 梯度裁剪:防止梯度爆炸 │
│ ↓ │
│ 参数更新:θ ← θ - AdamW(∂L/∂θ) │
│ ↓ │
│ 学习率调整:Warmup + Cosine Decay │
│ ↓ │
│ 梯度清零:准备下一个batch │
│ ↓ │
│ [循环] │
│ │
└───────────────────────────────────────────────────────────────┘
↓
训练完成,得到优化后的参数
小结
-
前向传播:
- 数据从输入到输出的计算过程
- 得到模型的预测结果
-
损失函数:
- 衡量模型输出与真实答案的差距
- 语言模型常用交叉熵损失:
-
反向传播:
- 利用链式法则计算梯度
- 现代框架自动完成(AutoGrad)
-
梯度下降:
- 沿着负梯度方向更新参数:
- 实践中使用Mini-Batch GD
-
学习率:
- 控制参数更新的步长
- 需要careful tuning
- 通常使用Warmup + Cosine Decay
-
AdamW优化器:
- 结合动量和自适应学习率
- 独立的权重衰减
- 是训练大模型的标准选择
训练的本质:通过不断的"前向计算-计算误差-反向传播-更新参数"循环,让模型的输出越来越接近正确答案,最终学会预测下一个词。
这就是大模型"学习"的秘密!