36. 优化器与训练技巧
一、优化器演进链
Q1: SGD 的核心局限是什么?为什么需要更高级的优化器? ⭐⭐
答:
SGD(随机梯度下降)的更新公式为:
$$\theta_{t+1} = \theta_t - \eta \cdot g_t$$
其中 $g_t = \nabla_\theta L(\theta_t)$ 为当前 mini-batch 的梯度,$\eta$ 为学习率。
SGD 的三大核心局限:
| 局限 | 具体表现 | 影响 |
|---|---|---|
| 学习率固定 | 所有参数共享同一学习率,无法自适应 | 稀疏特征更新不足,密集特征更新过猛 |
| 梯度方向噪声大 | 单 batch 梯度是真实梯度的有偏估计 | 更新方向震荡,收敛路径曲折 |
| 鞍点和局部最优 | 在鞍点梯度接近零,更新极慢 | 高维空间鞍点比局部最优更常见 |
| 无法利用历史信息 | 每步只看当前梯度,无动量概念 | 穿越狭长山谷时反复震荡 |
具体例子——学习率问题:
假设特征 A 出现频率 1%,特征 B 出现频率 99%。SGD 对两者使用相同学习率:
- 特征 A:梯度稀疏,大部分 step 不更新,偶尔大梯度导致跳跃
- 特征 B:梯度密集,每步都更新,容易过度调整
这直接催生了自适应学习率优化器(AdaGrad、RMSProp、Adam)。
Q2: Momentum 的物理直觉是什么?完整推导其更新公式。 ⭐⭐
答:
物理直觉:
Momentum(动量法)模拟物理学中小球在山坡上滚动的场景:
- 小球受重力沿坡面加速(梯度方向)
- 小球具有惯性(历史梯度积累)
- 摩擦力使速度不会无限增大(衰减系数 $\beta$)
在优化中的含义:
- 一致的梯度方向 → 动量累积 → 加速(如沿长斜坡加速下滚)
- 震荡的梯度方向 → 动量相互抵消 → 抑制震荡(如在山谷两壁来回时方向被平均)
完整推导:
引入速度变量 $v_t$,初始 $v_0 = 0$:
$$v_t = \beta \cdot v_{t-1} + g_t$$
$$\theta_{t+1} = \theta_t - \eta \cdot v_t$$
其中 $\beta$ 为动量系数,通常取 0.9。
展开递推:
$$v_t = \beta v_{t-1} + g_t = \beta(\beta v_{t-2} + g_{t-1}) + g_t = \sum_{i=0}^{t} \beta^{t-i} g_i$$
这是一个指数移动平均(EMA),对历史梯度做指数衰减加权:
$$v_t = g_t + \beta g_{t-1} + \beta^2 g_{t-2} + \cdots + \beta^t g_0$$
有效窗口长度: 权重之和 $\sum_{i=0}^{\infty} \beta^i = \frac{1}{1-\beta}$。当 $\beta=0.9$ 时,有效窗口约 10 步。
Nesterov Momentum 改进:
标准 Momentum 先算梯度再更新,Nesterov 先"预更新"再算梯度:
$$v_t = \beta v_{t-1} + \nabla_\theta L(\theta_t - \eta \beta v_{t-1})$$ $$\theta_{t+1} = \theta_t - \eta v_t$$
直觉:先按动量方向走一步"看看",在预判位置算梯度,有"刹车"效果,减少过冲。
Q3: AdaGrad 如何实现自适应学习率?它的问题是什么? ⭐⭐
答:
核心思想: 对每个参数维护其历史梯度平方的累积和,梯度大的参数自动降低学习率,梯度小的参数保持较高学习率。
更新公式:
$$G_t = G_{t-1} + g_t^2 \quad \text{(累积梯度平方和,逐元素)}$$
$$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t$$
其中 $\epsilon \approx 10^{-8}$ 防止除零。
直觉理解:
- 对于频繁更新的参数($G_t$ 大)→ 分母大 → 有效学习率小 → 更新步长小
- 对于稀疏更新的参数($G_t$ 小)→ 分母小 → 有效学习率大 → 更新步长大
这完美解决了 SGD 中稀疏特征的问题。
致命问题——学习率单调递减:
$G_t$ 只增不减(累积和),导致学习率 $\frac{\eta}{\sqrt{G_t + \epsilon}}$ 单调递减。
- 训练后期,$G_t$ 越来越大,学习率趋近于零
- 模型还没收敛就停止学习了
- 对于非凸优化问题尤其严重,可能需要持续探索
Q4: RMSProp 如何改进 AdaGrad? ⭐⭐
答:
核心改进: 用**指数移动平均(EMA)**替代 AdaGrad 的累积和,解决学习率单调递减问题。
更新公式:
$$E[g^2]t = \gamma \cdot E[g^2] + (1-\gamma) \cdot g_t^2$$
$$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} \cdot g_t$$
其中 $\gamma$ 为衰减率,通常取 0.9 或 0.99。
关键区别:
| 对比项 | AdaGrad | RMSProp |
|---|---|---|
| 梯度平方的聚合方式 | 累积和(只增不减) | 指数移动平均(可增可减) |
| 学习率趋势 | 单调递减 | 可自适应调节 |
| 适用场景 | 稀疏数据(NLP、推荐) | 非平稳目标、RNN |
| 历史记忆 | 全部历史等权 | 近期历史权重更大 |
RMSProp 的局限: 只利用了梯度的二阶矩(方差信息),没有利用一阶矩(动量信息)。这直接引出了 Adam。
Q5: Adam 优化器的完整推导——一阶矩、二阶矩、偏差校正。 ⭐⭐⭐
答:
Adam = Adaptive Moment estimation,融合了 Momentum(一阶矩/动量)和 RMSProp(二阶矩/自适应学习率)。
完整算法:
第一步:计算一阶矩估计(梯度的 EMA,类似 Momentum)
$$m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t$$
第二步:计算二阶矩估计(梯度平方的 EMA,类似 RMSProp)
$$v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2$$
第三步:偏差校正(关键!)
由于 $m_0 = 0, v_0 = 0$,训练初期 $m_t, v_t$ 都偏向零。展开一阶矩:
$$m_t = (1-\beta_1) \sum_{i=1}^{t} \beta_1^{t-i} g_i$$
取期望(假设 $g_i$ 的期望为 $g$):
$$E[m_t] = (1-\beta_1) g \sum_{i=1}^{t} \beta_1^{t-i} = g \cdot (1 - \beta_1^t)$$
因此偏差校正为:
$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}$$
当 $t$ 较大时,$\beta_1^t \to 0$,校正效果消失;当 $t$ 较小时(如 $t=1$),校正至关重要。
第四步:参数更新
$$\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$
默认超参数: $\beta_1 = 0.9, \beta_2 = 0.999, \epsilon = 10^{-8}$
为什么除以 $\sqrt{\hat{v}_t}$ 是合理的?
更新步长约为 $\eta \cdot \frac{E[g]}{\sqrt{E[g^2]}}$。由柯西-施瓦茨不等式:
$$|E[g]| \leq \sqrt{E[g^2]}$$
所以步长 $\leq \eta$,且当梯度方向一致时接近 $\eta$,当梯度方向多变时自动缩小。
Adam 的直觉总结:
- 分子 $\hat{m}_t$:平滑的梯度方向(动量,减少震荡)
- 分母 $\sqrt{\hat{v}_t}$:梯度幅度的归一化(自适应学习率)
Q6: AdamW 和 Adam 有什么区别?为什么 LLM 训练都用 AdamW? ⭐⭐⭐
答:
Adam + L2 正则(原版)的问题:
标准做法是在损失函数中加 L2 正则:$L_{total} = L + \frac{\lambda}{2}||\theta||^2$
梯度变为:$g_t = \nabla L + \lambda \theta_t$
Adam 更新变为:
$$\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t(\nabla L + \lambda\theta_t)}{\sqrt{\hat{v}_t(\nabla L + \lambda\theta_t)} + \epsilon}$$
问题: 权重衰减项 $\lambda\theta_t$ 也被自适应学习率缩放了!这意味着:
- 梯度大的参数,权重衰减被削弱($\sqrt{v_t}$ 大,除以大数)
- 梯度小的参数,权重衰减被放大
L2 正则在 Adam 中的效果与 SGD 中完全不同,无法有效约束权重。
AdamW 的解耦方案:
将权重衰减从梯度计算中分离出来,直接在参数上操作:
$$m_t = \beta_1 m_{t-1} + (1-\beta_1) \nabla L \quad \text{(只用原始梯度)}$$ $$v_t = \beta_2 v_{t-1} + (1-\beta_2) (\nabla L)^2$$ $$\theta_{t+1} = \theta_t - \eta \left(\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \theta_t\right)$$
或等价写法:
$$\theta_{t+1} = (1 - \eta\lambda)\theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$
核心区别: 权重衰减 $\lambda\theta_t$ 不经过 Adam 的自适应缩放,直接乘以学习率 $\eta$。
为什么 LLM 都用 AdamW?
- 解耦后权重衰减效果更稳定、可预测
- 与学习率调度配合更好(Loshchilov & Hutter 2019 原论文证明)
- 正则化效果与 SGD 的 L2 更一致
- 几乎所有主流 LLM(GPT、LLaMA、Qwen)都使用 AdamW
Q7: 各主流优化器更新公式对比表。 ⭐⭐
答:
| 优化器 | 更新公式 | 核心思想 | 适用场景 |
|---|---|---|---|
| SGD | $\theta \leftarrow \theta - \eta g$ | 最基本梯度下降 | 简单凸问题 |
| SGD+Momentum | $v = \beta v + g; \theta \leftarrow \theta - \eta v$ | 累积动量减少震荡 | CV任务常用 |
| Nesterov | $v = \beta v + \nabla L(\theta - \eta\beta v); \theta \leftarrow \theta - \eta v$ | 先预判再修正 | 理论更优 |
| AdaGrad | $G \leftarrow G + g^2; \theta \leftarrow \theta - \frac{\eta}{\sqrt{G}+\epsilon}g$ | 累积梯度平方自适应 | 稀疏数据 |
| RMSProp | $E[g^2] = \gamma E[g^2] + (1-\gamma)g^2$ | EMA替代累积和 | RNN |
| Adam | 一阶矩+二阶矩+偏差校正 | 动量+自适应 | 通用默认 |
| AdamW | 解耦权重衰减 | Adam + 正确正则化 | LLM标配 |
| LAMB | Adam + 层自适应学习率 | 大batch训练 | 大规模预训练 |
| Lion | $\text{sign}(\beta_1 m + (1-\beta_1)g)$ 更新 | 只保留梯度符号 | Google提出,显存省 |
显存占用对比(模型参数量 N):
| 优化器 | 额外状态 | 状态大小 |
|---|---|---|
| SGD | 无 | 0 |
| SGD+Momentum | 1个动量 | N |
| Adam/AdamW | $m$ 和 $v$ | 2N |
| 8-bit Adam | 量化后的 $m,v$ | ~0.5N |
二、学习率调度
Q8: 学习率调度为什么重要?主流调度策略有哪些? ⭐⭐
答:
学习率调度的重要性:
- 训练初期: 需要较大的学习率快速下降到损失盆地
- 训练后期: 需要较小的学习率精细调整,在最优解附近收敛
- 固定学习率的两难困境: 学习率太大无法收敛到最优点,太小初期训练太慢
主流调度策略:
① Step Decay(阶梯衰减):
$$\eta_t = \eta_0 \cdot \gamma^{\lfloor t / T \rfloor}$$
每隔 $T$ 个 epoch 将学习率乘以 $\gamma$(如 0.1)。简单有效,但需要手动设定衰减时机。
② Exponential Decay(指数衰减):
$$\eta_t = \eta_0 \cdot \gamma^t$$
每步都乘以 $\gamma$,衰减更平滑。
③ Cosine Annealing(余弦退火):
$$\eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})\left(1 + \cos\left(\frac{t}{T}\pi\right)\right)$$
学习率按余弦曲线从 $\eta_{max}$ 降到 $\eta_{min}$。这是 LLM 训练最常用的策略之一。
优势:
- 前期衰减慢,充分学习
- 中后期衰减快,精细调整
- 平滑无突变,训练更稳定
④ Linear Decay(线性衰减):
$$\eta_t = \eta_0 \cdot (1 - \frac{t}{T})$$
GPT-2、GPT-3 使用的策略。
⑤ Warmup + Decay(预热+衰减):
先线性增大学习率到峰值,再执行衰减。LLM 训练标配。
Q9: Warmup 的作用是什么?为什么 LLM 训练必须 Warmup? ⭐⭐⭐
答:
Warmup 阶段(通常前 1%-5% 的训练步数):
学习率从 0(或很小值)线性增加到设定的峰值 $\eta_{max}$。
为什么需要 Warmup?三大原因:
① Adam 的二阶矩估计不准:
Adam 训练初期 $v_t$ 估计不准(偏差校正后仍有噪声),较大的学习率 + 不准的自适应缩放 = 灾难性的大步更新。Warmup 让模型在小学习率下先积累足够准确的 $m_t, v_t$ 估计。
② 训练初期梯度方差大:
初始权重随机,不同 batch 的梯度方向差异巨大(方差大)。此时大学习率会导致参数剧烈震荡。Warmup 让模型先找到一个相对稳定的优化方向。
③ Layer Norm / Batch Norm 的统计量不稳定:
训练初期统计量(均值、方差)尚未稳定,大步更新会放大不稳定性,导致 loss spike。
LLM 中 Warmup 的典型配置:
- Warmup steps: 2000 步(GPT-3 用 375M tokens warmup)
- 通常占总训练步数的 0.1%-2%
- Warmup 后接 Cosine Decay 到峰值的 10%
不 Warmup 的后果: 训练初期可能出现 loss spike,甚至 loss 发散到 NaN。
Q10: Warmup + Cosine Decay 为什么是 LLM 训练标配? ⭐⭐
答:
完整的学习率曲线:
学习率
| /\
| / \
| / \____
| / \___
| / \___
| / \___
|/ \___
+---|---|---|---|---|---|---|--→ step
0 warmup cosine decay → ε为什么这个组合成为标配?
① Warmup 解决初期不稳定(见 Q9)
② Cosine Decay 提供平滑且理论优越的衰减:
- 起初衰减慢(余弦曲线平缓段),充分探索参数空间
- 中间衰减加速
- 末期衰减又放缓,在最优点附近精细调整
③ 与 AdamW 配合良好:
- Warmup 阶段积累准确的动量估计
- Cosine Decay 平滑降低学习率,避免阶梯衰减的突变
④ 实验验证:
- GPT-3、LLaMA、Qwen、DeepSeek 等所有主流 LLM 都使用此策略
- 消融实验表明比 Step Decay 和 Linear Decay 收敛更优
典型超参数配置(以 LLaMA 为例):
- Peak LR: 3e-4(7B)→ 1.5e-4(70B),模型越大,学习率越小
- Warmup steps: 2000
- Min LR: Peak LR × 0.1(即 cosine 衰减到峰值的 10%)
- Weight decay: 0.1
- $\beta_1$ = 0.9, $\beta_2$ = 0.95(注意不是默认的 0.999)
Q11: One Cycle Policy 和 LR Range Test 是什么? ⭐⭐
答:
LR Range Test(学习率范围测试):
由 Leslie Smith 提出,用于找最优学习率。
方法:
- 从极小学习率(如 $10^{-7}$)开始
- 每个 batch 后线性增大学习率
- 记录每个学习率对应的 loss
- 画出 loss-LR 曲线
判断标准:
- Loss 快速下降区域 → 学习率合适范围
- Loss 最低点 → 最佳学习率
- Loss 开始上升 → 学习率过大
Loss
|\
| \
| \___
| \___
| \____
| \/
+---|---|---|---|---→ LR
1e-7 1e-2
↑最佳LR区间↑One Cycle Policy:
一个完整的训练周期内的学习率调度:
- 上升阶段(~45%训练): 学习率从 $\eta_{min}$ 线性增加到 $\eta_{max}$
- 下降阶段(~45%训练): 学习率从 $\eta_{max}$ 余弦衰减到 $\eta_{min}$
- 微调阶段(~10%训练): 学习率继续衰减到 $\eta_{min}/10$
优势:
- 大学习率起到正则化效果(类似 dropout 的噪声注入)
- 小学习率精细调整
- 训练速度更快,通常可用更少 epoch 达到同等效果
- 可以通过 LR Range Test 找到 $\eta_{max}$
在 LLM 中的适用性: One Cycle 主要用于 CV 任务的 fine-tuning。LLM 预训练通常用 Cosine Decay,但在 fine-tuning 阶段 One Cycle 也很有效。
三、梯度裁剪
Q12: 按值裁剪 vs 按范数裁剪的区别?为什么 LLM 训练必须裁剪? ⭐⭐
答:
按值裁剪(Gradient Clipping by Value):
将每个梯度元素裁剪到 $[-c, c]$ 范围内:
$$g_i = \text{clip}(g_i, -c, c) = \max(-c, \min(c, g_i))$$
按范数裁剪(Gradient Clipping by Norm):
计算梯度的全局范数,若超过阈值则等比缩放:
$$g = \begin{cases} g & \text{if } ||g|| \leq c \ \frac{c}{||g||} \cdot g & \text{if } ||g|| > c \end{cases}$$
核心区别:
| 对比项 | 按值裁剪 | 按范数裁剪 |
|---|---|---|
| 操作对象 | 每个元素独立 | 整体梯度向量 |
| 梯度方向 | 改变(不同元素裁剪比例不同) | 不变(等比缩放) |
| 推荐场景 | 简单场景 | LLM 训练 |
| PyTorch API | torch.clamp | torch.nn.utils.clip_grad_norm_ |
为什么按范数裁剪更好?
按值裁剪会改变梯度方向!假设梯度 $g = [10, 0.5]$,阈值 $c=5$:
- 按值裁剪后:$[5, 0.5]$,方向变了
- 按范数裁剪后:$[5, 0.25]$,方向不变
LLM 为什么必须梯度裁剪?
- 梯度爆炸: Transformer 深层网络中梯度可能指数级增长,尤其在长序列上
- Loss Spike: 某些 batch 可能产生异常大的梯度,不裁剪会导致参数突变
- 训练稳定性: 裁剪提供了"安全网",即使遇到异常梯度也不会破坏训练
- 经验法则: 几乎所有 LLM 训练都设置
max_grad_norm = 1.0
PyTorch 使用方式:
# 训练循环中
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()四、混合精度训练
Q13: FP32、FP16、BF16 的数值范围和精度对比。 ⭐⭐
答:
| 格式 | 总位数 | 符号位 | 指数位 | 尾数位 | 数值范围 | 精度 |
|---|---|---|---|---|---|---|
| FP32 | 32 | 1 | 8 | 23 | $\pm 3.4 \times 10^{38}$ | $\sim 10^{-7}$ |
| FP16 | 16 | 1 | 5 | 10 | $\pm 65504$ | $\sim 10^{-3}$ |
| BF16 | 16 | 1 | 8 | 7 | $\pm 3.4 \times 10^{38}$ | $\sim 10^{-2}$ |
关键差异:
- FP16 vs BF16: BF16 复制了 FP32 的指数位(8位),因此数值范围与 FP32 相同,但精度更低(尾数只有7位)
- FP16 的问题: 数值范围太小,梯度/激活值容易溢出(上溢)或下溢(下溢)
- BF16 的优势: 范围大,不会溢出,几乎不需要 Loss Scaling
为什么 BF16 成为 LLM 训练标配?
- 数值范围: 与 FP32 相同,不会溢出/下溢
- 硬件支持: A100、H100 等 GPU 对 BF16 有原生加速(Tensor Core)
- 简化训练: 不需要 Loss Scaling(FP16 必须),代码更简洁
- 精度足够: LLM 训练中 BF16 精度损失对最终性能影响极小
Q14: 混合精度训练原理是什么?Loss Scaling 如何工作? ⭐⭐⭐
答:
混合精度训练的三个核心技术:
① FP32 主权重(Master Weights):
维护一份 FP32 的模型权重副本,优化器更新在 FP32 精度下进行:
- 前向/反向传播:FP16/BF16 权重 → 计算速度快
- 参数更新:FP32 权重 → 精度有保障
FP32 Master Weights ──cast──→ FP16 Weights ──前向/反向──→ FP16 Gradients
↑ |
└── FP32 更新 ←──cast── FP32 Gradients② Loss Scaling(FP16 必须,BF16 通常不需要):
问题: FP16 的梯度值往往在 $[0, 2^{-24}]$ 范围内,低于 FP16 可表示的最小正规数 $2^{-14}$,导致下溢为零。
解决方案: 在反向传播前将 loss 乘以一个缩放因子 $S$(如 1024),使梯度值乘以 $S$ 后进入 FP16 可表示范围,参数更新前再除以 $S$。
$$L' = S \cdot L \quad \Rightarrow \quad \nabla L' = S \cdot \nabla L \quad \Rightarrow \quad g = \nabla L' / S$$
动态 Loss Scaling:
- 初始缩放因子较大(如 $2^{16}$)
- 如果连续 N 步没有出现
inf/nan→ 缩放因子翻倍 - 如果出现
inf/nan→ 缩放因子减半,跳过本步更新
③ 算子精度控制:
某些运算对精度敏感,需要在 FP32 下执行:
- Softmax(指数运算)
- Layer Normalization
- Loss 计算
PyTorch AMP 自动处理这些。
PyTorch AMP 使用:
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for data, target in dataloader:
optimizer.zero_grad()
with autocast(dtype=torch.bfloat16): # 或 torch.float16
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
scaler.step(optimizer)
scaler.update()显存节省:
| 组件 | FP32 | 混合精度 |
|---|---|---|
| 模型参数 | 4N | 2N + 4N(master) = 6N ≈ 1.5x |
| 梯度 | 4N | 2N = 0.5x |
| 优化器状态(Adam) | 8N | 8N |
| 激活值 | FP32 | FP16/BF16 ≈ 0.5x |
| 总计 | ~16N+ | ~12N+ 激活值减半 |
混合精度主要节省的是激活值显存(通常是大头),这在 batch size 较大时效果显著。
五、分布式训练
Q15: DP 和 DDP 的原理与区别? ⭐⭐
答:
DP(DataParallel)—— 单进程多线程:
主 GPU (GPU 0):
1. 将模型复制到所有 GPU
2. 将 mini-batch 分割到各 GPU
3. 各 GPU 前向传播
4. 收集所有输出到 GPU 0
5. GPU 0 计算 loss 并反向传播
6. 同步梯度到所有 GPU
7. 更新参数DP 的问题:
- GIL 限制: Python 多线程受 GIL 限制,无法真正并行
- GPU 0 过载: 汇总输出、计算 loss 都在 GPU 0 上,GPU 0 显存和计算都是瓶颈
- 通信效率低: 参数服务器模式,通信量随 GPU 数增加线性增长
- 不支持多机
DDP(DistributedDataParallel)—— 多进程:
每个 GPU 一个进程:
1. 每个进程持有完整模型副本
2. 每个进程处理独立的 mini-batch(DistributedSampler 确保不重叠)
3. 各进程独立前向+反向传播
4. AllReduce 同步梯度(Ring AllReduce)
5. 各进程独立更新参数(梯度相同 → 参数保持同步)DDP 优势:
- 无 GIL 限制,真正并行
- 无主 GPU 瓶颈
- Ring AllReduce 通信量恒定,不随 GPU 数增加
- 支持多机多卡
| 对比项 | DP | DDP |
|---|---|---|
| 并行方式 | 单进程多线程 | 多进程 |
| GIL | 受限 | 不受限 |
| 通信模式 | 参数服务器 | Ring AllReduce |
| 负载均衡 | 不均衡 | 均衡 |
| 多机支持 | 不支持 | 支持 |
| 效率 | 低 | 高 |
| 推荐 | 不推荐 | 推荐 |
Q16: DeepSpeed ZeRO 的三个 Stage 分别做了什么? ⭐⭐⭐
答:
问题背景: 训练大模型时,每张卡需要存储:
- 模型参数:$2\Phi$(FP16,$\Phi$ 为参数量)
- 梯度:$2\Phi$
- 优化器状态(Adam):$12\Phi$(FP32 参数副本 4 + 一阶矩 4 + 二阶矩 4)
- 总计: $16\Phi$ 字节
以 LLaMA-70B 为例($\Phi = 70 \times 10^9$):
- 单卡需 $16 \times 70 \times 10^9 \approx 1120$ GB 显存
- 即使用 80GB A100,也需要至少 14 张卡才装得下
ZeRO 通过分片(Partitioning)消除冗余:
Stage 1:优化器状态分片($P_{os}$)
传统: 每张卡存储 4Φ(参数) + 2Φ(梯度) + 12Φ(优化器) = 16Φ
Stage 1: 4Φ + 2Φ + 12Φ/N = 6Φ + 12Φ/N
N=64: 6Φ + 0.19Φ ≈ 6.2Φ(节省 61%)每张卡只存储 $1/N$ 的优化器状态。更新参数时,AllGather 收集所有分片。
Stage 2:优化器状态 + 梯度分片($P_{os+g}$)
Stage 2: 4Φ + 2Φ/N + 12Φ/N = 4Φ + 14Φ/N
N=64: 4Φ + 0.22Φ ≈ 4.2Φ(节省 74%)每张卡只存储它负责更新的那些参数对应的梯度。反向传播后 Reduce-Scatter 汇总梯度。
Stage 3:优化器状态 + 梯度 + 参数分片($P_{os+g+p}$)
Stage 3: 4Φ/N + 2Φ/N + 12Φ/N = 16Φ/N
N=64: 0.25Φ(节省 98.4%)每张卡只存储 $1/N$ 的参数。前向和反向传播时 AllGather 收集所需参数。
| Stage | 分片内容 | 每卡显存 | 通信开销 |
|---|---|---|---|
| 0(DDP) | 无 | 16Φ | 2Φ |
| 1 | 优化器状态 | 6Φ + 10Φ/N | 2Φ |
| 2 | 优化器+梯度 | 4Φ + 12Φ/N | 2Φ |
| 3 | 优化器+梯度+参数 | 16Φ/N | 3Φ |
Stage 3 通信量增加约 50%,但显存节省极大,是训练超大模型的关键。
Q17: 什么是 3D 并行?70B 模型需要多少 GPU? ⭐⭐⭐
答:
3D 并行 = 数据并行 + 张量并行 + 流水线并行
① 数据并行(DP): 不同 GPU 处理不同数据,梯度同步
- 适合:模型放得下单卡时
- 扩展:增加数据吞吐量
② 张量并行(TP,Tensor Parallelism): 将单个算子(矩阵乘法)切分到多卡
Y = XW,将 W 按列切分:
GPU 0: Y_0 = X @ W_0 (W 的前半列)
GPU 1: Y_1 = X @ W_1 (W 的后半列)
Y = concat(Y_0, Y_1)- Megatron-LM 的做法:MLP 和 Attention 都可以切
- 适合:同一机器内的 GPU(需要高带宽 NVLink)
- 通常 2-8 路
③ 流水线并行(PP,Pipeline Parallelism): 将模型按层切分
GPU 0: Layer 0-7
GPU 1: Layer 8-15
GPU 2: Layer 16-23
GPU 3: Layer 24-31- 按层分配到不同 GPU,前一个 stage 输出传给下一个
- 引入 pipeline bubble(流水线气泡),需 micro-batch 填充
- 适合:跨机器(通信量小,只传激活值和梯度)
3D 并行示例(Megatron-LM 风格):
总 GPU 数 = TP × PP × DP
例: 64 GPU = TP(8) × PP(4) × DP(2)
DP 维度
┌─────────────────────────┐
│ DP replica 0 │ DP replica 1 │
PP 维度 ┌────────┼────────────────┼────────────────┤
│ Stage 0 │ TP=8 │ GPU 0-7 │ GPU 32-39 │
│ Stage 1 │ TP=8 │ GPU 8-15 │ GPU 40-47 │
│ Stage 2 │ TP=8 │ GPU 16-23 │ GPU 48-55 │
│ Stage 3 │ TP=8 │ GPU 24-31 │ GPU 56-63 │
└─────────┴──────┴────────────────┴────────────────┘70B 模型需要多少 GPU?
以 LLaMA-70B(70B 参数)为例:
- 模型参数(FP16):140 GB
- 优化器状态(Adam FP32):840 GB
- 梯度(FP16):140 GB
- 总计约 1120 GB(不含激活值)
配置方案:
| 方案 | 配置 | GPU 数 | 说明 |
|---|---|---|---|
| ZeRO-3 | Stage 3 分片 | 8×A100-80G | 勉强可行,batch size 受限 |
| TP+PP | TP=8, PP=10 | 80×A100-80G | 常用方案 |
| 3D 并行 | TP=8, PP=4, DP=8 | 256×A100-80G | 训练速度快 |
实际生产中,70B 模型训练通常使用 128-1024 张 A100/H100,取决于训练时间和预算。
Q18: AllReduce 和 AllGather 通信原语的区别? ⭐⭐
答:
AllReduce:
所有节点各持有数据的一部分,AllReduce 后所有节点得到相同的聚合结果(通常是求和)。
节点0: [a] ──→ [a+b+c+d] AllReduce = Reduce + Broadcast
节点1: [b] ──→ [a+b+c+d]
节点2: [c] ──→ [a+b+c+d]
节点3: [d] ──→ [a+b+c+d]DDP 中的作用: 所有 GPU 的梯度求和/平均,确保所有 GPU 持有相同梯度。
Ring AllReduce 优化:
通信量 = 2 × (N-1)/N × 数据量 ≈ 2 × 数据量(与GPU数无关!)分两个阶段:Reduce-Scatter(每个节点得到部分结果的聚合)→ AllGather(广播完整结果)。
AllGather:
所有节点各持有一部分数据,AllGather 后所有节点得到完整的数据。
节点0: [a] ──→ [a, b, c, d]
节点1: [b] ──→ [a, b, c, d]
节点2: [c] ──→ [a, b, c, d]
节点3: [d] ──→ [a, b, c, d]ZeRO 中的作用: 前向传播前,AllGather 收集所有分片参数。
Reduce-Scatter:
节点0: [a0, a1, a2, a3] ──→ [a0+b0+c0+d0]
节点1: [b0, b1, b2, b3] ──→ [a1+b1+c1+d1]
节点2: [c0, c1, c2, c3] ──→ [a2+b2+c2+d2]
节点3: [d0, d1, d2, d3] ──→ [a3+b3+c3+d3]AllReduce = Reduce-Scatter + AllGather
六、数据增强与数据处理
Q19: NLP 和 CV 领域常见的数据增强方法有哪些? ⭐⭐
答:
NLP 数据增强:
| 方法 | 描述 | 适用场景 |
|---|---|---|
| 同义词替换 | 随机替换非停用词为同义词 | 文本分类 |
| 随机插入 | 在随机位置插入随机同义词 | 文本分类 |
| 随机交换 | 交换句子中两个词的位置 | 文本分类 |
| 随机删除 | 以概率 p 删除句子中的词 | 文本分类 |
| 回译(Back Translation) | 翻译到另一语言再翻回来 | 文本分类、QA |
| EDA(Easy Data Augmentation) | 以上四种的组合 | 文本分类 |
| LLM 生成 | 用 ChatGPT/GPT-4 生成改写 | 各种 NLP 任务 |
| Mixup | 在嵌入空间做线性插值 | 文本分类 |
| 对抗训练 | 在嵌入上加小扰动 | 提升鲁棒性 |
CV 数据增强:
| 方法 | 描述 |
|---|---|
| Random Crop + Resize | 随机裁剪后缩放回原尺寸 |
| Random Horizontal Flip | 随机水平翻转 |
| Color Jitter | 颜色抖动(亮度、对比度、饱和度) |
| Random Erasing / Cutout | 随机遮挡部分区域 |
| Mixup | 两张图的像素加权混合 |
| CutMix | 将一张图的区域粘贴到另一张图 |
| AutoAugment | 用强化学习自动搜索增强策略 |
| RandAugment | 随机选择增强操作和强度 |
Q20: 数据清洗和去重为什么重要? ⭐⭐
答:
数据质量 > 数据数量: 研究反复证明,精心清洗的数据比大量脏数据效果更好。
去重的重要性:
- 训练集-测试集泄漏: 训练数据中包含测试集数据,导致评估指标虚高
- 重复数据的影响:
- 模型过度记忆重复样本,降低泛化能力
- 某些 pattern 被过度强化,产生偏见
- 浪费算力(相同数据反复训练)
去重方法:
| 方法 | 粒度 | 优点 | 缺点 |
|---|---|---|---|
| 精确去重(Hash) | 文档级 | 简单快速 | 无法处理近似重复 |
| MinHash + LSH | 文档/段落 | 可检测近似重复,可扩展 | 有假阳性 |
| N-gram 去重 | 子串级 | 细粒度 | 计算量大 |
| Embedding 去重 | 语义级 | 检测语义相似 | 计算成本高 |
LLM 预训练数据清洗流程:
原始数据 → URL过滤 → 语言检测 → 内容过滤(色情/暴力)
→ 规则清洗(HTML标签/乱码) → 去重(MinHash)
→ 质量过滤(困惑度/分类器) → 最终数据集标签噪声:
- 来源: 众包标注误差、自动标注错误、模糊样本
- 影响: 小噪声可起正则化作用;大噪声严重降低模型性能
- 处理: 标签平滑(Label Smoothing)、置信度学习(Confident Learning)、噪声感知训练
七、过拟合与欠拟合
Q21: 过拟合的 7 种解决方案是什么? ⭐⭐
答:
| 序号 | 方法 | 原理 | 实施建议 |
|---|---|---|---|
| 1 | 增加数据量 | 更多数据覆盖更多分布 | 数据增强、数据合成 |
| 2 | 正则化(L1/L2/Weight Decay) | 限制权重大小 | L2/Weight Decay 最常用,λ=0.01-0.1 |
| 3 | Dropout | 随机丢弃神经元,集成效应 | 一般 p=0.1-0.5,Transformer 中常用 |
| 4 | Early Stopping | 在验证集 loss 开始上升时停止 | 用 validation loss 的 patience |
| 5 | 减小模型容量 | 减少参数量 | 减层数/宽度,但 LLM 中不常用 |
| 6 | 数据增强 | 人为增加数据多样性 | NLP: 回译、LLM生成;CV: 翻转裁剪 |
| 7 | Label Smoothing | 软化标签,降低过度自信 | $\epsilon=0.1$,防止 logit 趋向无穷 |
额外方法:
- Batch Normalization: 有轻度正则化效果(每个 batch 统计量有噪声)
- Mixup / CutMix: 样本混合增加数据多样性
- R-Drop: 两次前向的 KL 散度作为正则项
Q22: 欠拟合的 5 种解决方案是什么? ⭐
答:
| 序号 | 方法 | 原理 |
|---|---|---|
| 1 | 增加模型复杂度 | 更多层、更多参数、更大宽度 |
| 2 | 减小正则化强度 | 降低 λ、减少 Dropout |
| 3 | 增加训练时间 | 更多 epoch/step |
| 4 | 调整学习率 | 可能太小或太大 |
| 5 | 特征工程 | 提供更有信息量的输入特征 |
Q23: 早停法的实现和学习曲线分析? ⭐⭐
答:
早停法(Early Stopping)实现:
class EarlyStopping:
def __init__(self, patience=5, min_delta=0.001):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = float('inf')
def __call__(self, val_loss):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
return False # 继续训练
else:
self.counter += 1
if self.counter >= self.patience:
return True # 停止训练
return False学习曲线分析:
Loss
| \
| \ 训练loss 过拟合区间
| \______________ ───────↗────
| \ ↗────────── 验证loss
| \ /
| \/
| ↑
| 最佳停止点
+──────────────────────→ Epoch常见模式诊断:
| 训练loss | 验证loss | 诊断 | 解决方案 |
|---|---|---|---|
| 高 | 高 | 欠拟合 | 增大模型/调学习率 |
| 低 | 高 | 过拟合 | 正则化/数据增强/早停 |
| 低 | 低且持平 | 良好 | 继续训练 |
| 波动大 | 波动大 | 学习率过大/数据噪声 | 降学习率/增大batch |
Q24: 大模型会过拟合吗? ⭐⭐⭐
答:
简短回答: 大模型在预训练阶段通常不会过拟合,但在微调阶段会过拟合。
预训练阶段不过拟合的原因:
- 数据量巨大: LLaMA-70B 在 1T+ tokens 上训练,远超参数量
- Chinchilla Scaling Law: 计算最优配置要求参数量和数据量同比例增长
- 正则化效果: AdamW 的权重衰减、Dropout(某些模型)、梯度裁剪都有正则化作用
- 隐式正则化: SGD/Adam 的噪声本身就有正则化效果
Chinchilla Scaling Law:
最优训练 tokens 数 ≈ 20 × 模型参数量
| 模型 | 参数量 | 最优训练 tokens |
|---|---|---|
| 1B | 10亿 | 200亿 |
| 7B | 70亿 | 1400亿 |
| 70B | 700亿 | 1.4万亿 |
微调阶段过拟合:
微调数据通常只有几千到几万条,对于 7B+ 模型:
- 模型参数量远大于数据量 → 严重过拟合风险
- 表现:训练 loss 快速下降但评估指标变差
解决方案:
- LoRA:只微调少量参数,降低过拟合风险
- 学习率调小:1e-5 ~ 5e-5
- Early Stopping:监控验证集指标
- 数据增强:用 LLM 生成更多训练数据
八、训练稳定性
Q25: Loss Spike 的原因和处理方法? ⭐⭐⭐
答:
Loss Spike: 训练过程中 loss 突然急剧增大,然后可能恢复也可能发散。
常见原因:
| 原因 | 机制 | 频率 |
|---|---|---|
| 异常数据 | 某些 batch 包含异常样本(极长序列、乱码、重复) | 高 |
| 梯度爆炸 | 某些层的梯度突然很大 | 中 |
| 学习率过大 | 参数更新步长超过损失盆地边界 | 中 |
| 数值溢出 | FP16 激活值溢出(inf/nan) | 中 |
| 注意力坍塌 | Attention 矩阵退化为近似 one-hot | 低但严重 |
处理方法:
① 梯度裁剪(最重要的防线):
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)② 跳过异常 batch:
if torch.isnan(loss) or torch.isinf(loss) or loss > threshold:
optimizer.zero_grad()
continue # 跳过此 batch③ 数据清洗: 移除异常样本(极长文本、乱码、高重复率文本)
④ 降低学习率: 特别是 warmup 结束后的初始学习率
⑤ 从 checkpoint 恢复: loss spike 后回滚到之前的 checkpoint,跳过问题数据后继续
⑥ 使用 BF16 替代 FP16: BF16 数值范围大,不易溢出
Q26: 梯度累积和梯度检查点的作用与实现? ⭐⭐
答:
梯度累积(Gradient Accumulation):
问题: 显存不足以容纳大 batch size,但大 batch 训练更稳定。
解决: 将大 batch 拆成多个 micro-batch,累积梯度后再更新。
accumulation_steps = 4 # 逻辑 batch = 4 × micro-batch
for i, (data, target) in enumerate(dataloader):
loss = model(data, target) / accumulation_steps
loss.backward() # 梯度累积,不立即更新
if (i + 1) % accumulation_steps == 0:
optimizer.step() # 累积 N 步后才更新
optimizer.zero_grad()等价性: 梯度是线性的,$\nabla L = \frac{1}{N}\sum_{i=1}^{N} \nabla L_i$,累积后再除以 N 与大 batch 等价。
注意: loss 要除以 accumulation_steps,否则梯度是正确值的 N 倍。
梯度检查点(Gradient Checkpointing / Activation Checkpointing):
问题: 反向传播需要前向传播保存的中间激活值,占用大量显存。
原理: 前向传播时不保存中间激活值,反向传播时重新计算。
标准: 前向 → 保存所有激活 → 反向(用保存的激活)
检查点: 前向 → 只保存检查点激活 → 反向 → 重算中间激活 → 反向代价: 计算量增加约 33%(前向要重算一次),但显存从 $O(L)$ 降为 $O(\sqrt{L})$,其中 $L$ 为层数。
from torch.utils.checkpoint import checkpoint
class TransformerBlock(nn.Module):
def forward(self, x):
# 普通模式
# return self.attention(x) + self.ffn(x)
# 检查点模式
return checkpoint(self._forward, x)
def _forward(self, x):
return self.attention(x) + self.ffn(x)Q27: Loss 不下降如何排查? ⭐⭐
答:
系统性排查清单:
Loss 不下降
├── 1. Loss 是否是 NaN/Inf?
│ ├── 是 → 学习率过大、数据异常、数值溢出
│ └── 否 → 继续
├── 2. 过拟合一个小 batch 测试
│ ├── 无法过拟合 → 模型/优化器/数据有 bug
│ └── 可以过拟合 → 继续
├── 3. 检查学习率
│ ├── 太大 → loss 震荡不下降
│ ├── 太小 → loss 下降极慢
│ └── 正确 → 继续
├── 4. 检查数据
│ ├── 标签是否正确?
│ ├── 数据加载是否正确(shuffle、tokenization)?
│ └── 数据质量如何?
├── 5. 检查模型
│ ├── 梯度是否流动?(梯度监控)
│ ├── 权重是否更新?(对比前后参数)
│ └── 模型结构是否有 bug?
└── 6. 检查训练配置
├── 权重衰减是否过大?
├── Dropout 是否过大?
└── Batch size 是否合理?关键调试技巧:
# 1. 过拟合测试:用少量数据,看 loss 能否降到 0
for _ in range(1000):
loss = model(small_batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 如果 loss 不下降,说明有 bug
# 2. 梯度监控
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}: grad_norm={param.grad.norm().item()}")Q28: NaN 出现的常见原因和排查方法? ⭐⭐
答:
NaN 的来源:
| 来源 | 原因 | 位置 |
|---|---|---|
| 梯度爆炸 | 梯度值溢出 FP16/FP32 范围 | 反向传播 |
| Log 输入零/负值 | log(0) = -inf | Loss 计算(如 cross entropy) |
| 除以零 | 1/0 或 softmax 中数值问题 | 各种操作 |
| 指数溢出 | exp(x) 中 x 过大 | softmax、attention |
| 数据本身 | 输入数据含 NaN/Inf | 数据加载 |
| 权重更新后 | 参数变成 NaN 后传播 | 参数更新 |
排查方法:
# 1. 检查数据
assert not torch.isnan(data).any()
assert not torch.isinf(data).any()
# 2. 开启 anomaly detection
torch.autograd.set_detect_anomaly(True) # 会打印 NaN 出现的精确位置
# 3. 逐层检查输出
def hook_fn(module, input, output):
if isinstance(output, torch.Tensor):
if torch.isnan(output).any():
print(f"NaN in {module.__class__.__name__}")
for module in model.modules():
module.register_forward_hook(hook_fn)
# 4. 降低学习率 / 增加 warmup / 减小初始权重预防措施:
- 使用
torch.nn.utils.clip_grad_norm_裁剪梯度 - 使用 BF16 而非 FP16(范围更大)
- Layer Normalization 稳定训练
- 合理的权重初始化(Xavier/He)
- 适当的 warmup
九、数学基础补充
Q29: 线性代数核心知识——矩阵运算在深度学习中的应用。 ⭐⭐
答:
矩阵乘法在神经网络中的核心地位:
深度学习的前向传播本质上是一系列矩阵乘法和非线性变换:
$$y = \sigma(Wx + b)$$
关键矩阵运算:
① 矩阵乘法维度分析:
Linear层: Y = XW + b
X: (batch, d_in) × W: (d_in, d_out) → Y: (batch, d_out)
Attention: QK^T / √d_k
Q: (n, d_k) × K^T: (d_k, n) → (n, n)② 矩阵分解(SVD):
$$W = U \Sigma V^T$$
- $U, V$ 为正交矩阵,$\Sigma$ 为奇异值对角矩阵
- 应用: 低秩近似(LoRA 的数学基础)、模型压缩
- 保留前 $r$ 个最大奇异值:$W \approx U_r \Sigma_r V_r^T$
③ 特征值与特征向量:
$$Av = \lambda v$$
- PCA 的数学基础: 协方差矩阵的特征向量是主成分方向
- 理解训练动态: Hessian 矩阵的特征值决定优化景观
④ 矩阵范数:
- Frobenius 范数:$||W||F = \sqrt{\sum W_{ij}^2}$(权重衰减的目标)
- 谱范数:$||W||2 = \sigma(W)$(谱归一化、GAN 训练稳定性)
Q30: 概率论基础——贝叶斯、MLE、KL 散度。 ⭐⭐⭐
答:
贝叶斯定理:
$$P(\theta|D) = \frac{P(D|\theta)P(\theta)}{P(D)}$$
- 后验 $\propto$ 似然 $\times$ 先验
- 在 ML 中的体现: L2 正则化 = 高斯先验下的 MAP 估计
MLE(最大似然估计):
$$\theta^* = \arg\max_\theta P(D|\theta) = \arg\max_\theta \sum_i \log P(y_i|x_i, \theta)$$
MLE 等价于最小化负对数似然(即交叉熵损失)。
KL 散度(Kullback-Leibler Divergence):
$$D_{KL}(P||Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)} = E_P\left[\log \frac{P(x)}{Q(x)}\right]$$
性质:
- $D_{KL}(P||Q) \geq 0$(非负性,Gibbs 不等式)
- $D_{KL}(P||Q) = 0 \iff P = Q$
- 不对称: $D_{KL}(P||Q) \neq D_{KL}(Q||P)$
在深度学习中的应用:
| 场景 | KL 散度的角色 |
|---|---|
| VAE | $D_{KL}(q(z |
| 知识蒸馏 | $D_{KL}(\text |
| RLHF | $D_{KL}(\pi_{\text{old}} |
| 语言模型 | 训练目标等价于最小化与真实分布的 KL 散度 |
Forward KL vs Reverse KL:
- Forward KL: $D_{KL}(P||Q)$ — Q 需要覆盖 P 的所有模式(zero-avoiding),倾向于生成多样但不精确的结果
- Reverse KL: $D_{KL}(Q||P)$ — Q 集中在 P 的某个模式上(zero-forcing),倾向于生成精确但单一的结果
Q31: 信息论基础——熵、交叉熵、互信息。 ⭐⭐
答:
信息熵(Entropy):
$$H(P) = -\sum_x P(x) \log P(x)$$
- 衡量随机变量的不确定性
- 均匀分布熵最大(最不确定),确定性分布熵为 0
- 二元分类中:$H = -p\log p - (1-p)\log(1-p)$
交叉熵(Cross-Entropy):
$$H(P, Q) = -\sum_x P(x) \log Q(x)$$
- 用分布 Q 编码来自分布 P 的数据所需的平均编码长度
- 深度学习中分类任务的损失函数
三者关系:
$$H(P, Q) = H(P) + D_{KL}(P||Q)$$
因为 $H(P)$ 是常数(真实标签分布固定),最小化交叉熵等价于最小化 KL 散度。
互信息(Mutual Information):
$$I(X;Y) = H(X) - H(X|Y) = D_{KL}(P(X,Y)||P(X)P(Y))$$
- 衡量 X 和 Y 之间的依赖关系
- $I(X;Y) = 0$ 当且仅当 X 和 Y 独立
- InfoNCE loss(对比学习)本质上是互信息的下界
Q32: Softmax 的数值稳定性问题及解决方案。 ⭐⭐
答:
Softmax 定义:
$$\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}$$
数值问题:
- 上溢: 若 $z_i$ 很大(如 $z_i = 1000$),$e^{1000}$ 溢出 FP32 范围 →
inf - 下溢: 若 $z_i$ 很小(如 $z_i = -1000$),$e^{-1000}$ 下溢为 0 → 分子/分母为 0
- inf/inf = NaN
解决方案——减去最大值:
令 $c = \max_j z_j$,则:
$$\text{softmax}(z_i) = \frac{e^{z_i - c}}{\sum_j e^{z_j - c}}$$
数学上完全等价(分子分母同除以 $e^c$),但数值上:
- $z_i - c \leq 0$,所以 $e^{z_i-c} \leq 1$,不会上溢
- 至少有一个 $e^{z_i-c} = e^0 = 1$,分母 $\geq 1$,不会除以零
PyTorch 中已内置此优化:
# PyTorch 内部实现(简化版)
def stable_softmax(x):
x_max = x.max(dim=-1, keepdim=True).values
exp_x = torch.exp(x - x_max)
return exp_x / exp_x.sum(dim=-1, keepdim=True)Log-Softmax 的数值稳定性:
直接计算 $\log(\text{softmax}(z_i))$ 可能因为 softmax 结果接近 0 而得到 $-\infty$。
更好的方法:
$$\log \text{softmax}(z_i) = z_i - c - \log\sum_j e^{z_j - c}$$
使用 logsumexp:$\log\sum_j e^{z_j - c}$ 可用 torch.logsumexp(z - c, dim=-1) 稳定计算。
Q33: 交叉熵损失与 KL 散度的关系。 ⭐⭐
答:
交叉熵定义:
$$H(P, Q) = -\sum_x P(x) \log Q(x)$$
KL 散度定义:
$$D_{KL}(P||Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)} = -H(P) + H(P, Q)$$
因此:
$$H(P, Q) = H(P) + D_{KL}(P||Q)$$
在分类任务中:
- 真实分布 P 是 one-hot:$P = [0, 0, ..., 1, ..., 0]$
- 真实分布的熵 $H(P) = 0$(确定性分布)
- 所以 $H(P, Q) = D_{KL}(P||Q)$
结论:对于 one-hot 标签的分类任务,最小化交叉熵等价于最小化 KL 散度。
为什么用交叉熵而不是直接用 KL 散度?
- 交叉熵的梯度更简洁:$\frac{\partial H}{\partial z_i} = Q_i - P_i$(配合 softmax)
- $H(P)$ 是常数,不影响优化
- 计算上更稳定
梯度推导:
$$\frac{\partial}{\partial z_i} \left[-\sum_k P_k \log Q_k\right] = Q_i - P_i$$
这个梯度极其优美——预测值减去真实值,正是 softmax 输出与 one-hot 标签的误差。
十、优化器与训练面试高频题汇总
Q34: 15 道高频面试题及标准答案。 ⭐⭐⭐
答:
题1:请完整推导 Adam 优化器的更新公式,解释偏差校正的必要性。
标准答案: 参见 Q5。关键点:一阶矩 $m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t$,二阶矩 $v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2$。偏差校正 $\hat{m}_t = m_t/(1-\beta_1^t)$ 解决了训练初期 $m_t, v_t$ 偏向零的问题。展开期望 $E[m_t] = g(1-\beta_1^t)$,除以 $(1-\beta_1^t)$ 消除偏差。
题2:AdamW 和 Adam + L2 正则化有什么本质区别?
标准答案: Adam + L2 中,权重衰减项 $\lambda\theta$ 被自适应学习率 $\sqrt{v_t}$ 缩放,导致不同参数的有效衰减强度不同。AdamW 解耦了权重衰减:$\theta_{t+1} = (1-\eta\lambda)\theta_t - \eta\hat{m}_t/(\sqrt{\hat{v}_t}+\epsilon)$,权重衰减直接作用于参数,不经过自适应缩放。
题3:为什么 LLM 训练必须使用 Warmup?
标准答案: 三个原因:①Adam 初期二阶矩估计不准,大 LR 导致灾难性更新;②初始权重随机,梯度方差大,大 LR 导致震荡;③Layer Norm 等统计量初期不稳定。典型 Warmup 占总步数 0.1%-2%。
题4:比较 FP16 和 BF16 的优劣,LLM 训练为什么更倾向 BF16?
标准答案: FP16 范围小(±65504)需要 Loss Scaling,BF16 范围与 FP32 相同(±3.4×10³⁸)不需要。BF16 精度低(7位尾数 vs 10位),但 LLM 训练对精度要求不高。BF16 在 A100+ 有硬件原生支持。
题5:DeepSpeed ZeRO Stage 1/2/3 分别分片了什么?Stage 3 通信量增加多少?
标准答案: Stage 1 分片优化器状态(节省约 60%显存),Stage 2 加分片梯度(节省约 74%),Stage 3 加分片参数(节省约 98%)。Stage 3 需要额外的 AllGather 操作,通信量约增加 50%。
题6:梯度裁剪按值和按范数的区别?为什么按范数更好?
标准答案: 按值裁剪逐元素 clip 到 [-c,c],会改变梯度方向。按范数裁剪当 ||g||>c 时等比缩放 g·c/||g||,保持梯度方向不变。LLM 训练中统一使用 max_norm=1.0 的按范数裁剪。
题7:梯度累积和梯度检查点的原理和代价?
标准答案: 梯度累积:将大 batch 拆成多个 micro-batch 累积梯度后再更新,等价于大 batch 训练,loss 需除以累积步数。梯度检查点:前向不保存中间激活,反向时重算,计算量增加约 33%,显存从 O(L) 降为 O(√L)。
题8:Loss Spike 的常见原因和处理方法?
标准答案: 原因:异常数据、梯度爆炸、学习率过大、数值溢出、注意力坍塌。处理:①梯度裁剪(最重要防线);②跳过异常 batch(检测 NaN/Inf/异常大 loss);③数据清洗;④降低 LR;⑤从 checkpoint 恢复。
题9:为什么最小化交叉熵等价于最小化 KL 散度?
标准答案: $H(P,Q) = H(P) + D_{KL}(P||Q)$。对于 one-hot 标签 $H(P)=0$,所以 $H(P,Q) = D_{KL}(P||Q)$。即使非 one-hot,$H(P)$ 是常数不影响优化,最小化交叉熵等价于最小化 KL 散度。
题10:Softmax 的数值稳定性如何保证?
标准答案: 减去最大值 $c=\max z_j$:$\text{softmax}(z_i) = e^{z_i-c}/\sum e^{z_j-c}$。这保证指数部分 $\leq 1$ 不上溢,分母 $\geq 1$ 不除零。数学上等价(分子分母同除以 $e^c$)。PyTorch 已内置此优化。
题11:3D 并行是什么?如何选择 TP/PP/DP 的维度?
标准答案: 3D 并行 = 张量并行(TP) + 流水线并行(PP) + 数据并行(DP)。TP 适合同机 NVLink 连接的 GPU(2-8路),PP 适合跨机(按层切分),DP 增加数据吞吐。选择原则:先用 TP 放下单层,再用 PP 放下整个模型,最后 DP 提高吞吐。
题12:大模型预训练会过拟合吗?微调呢?
标准答案: 预训练通常不过拟合:数据量巨大(万亿 tokens),Chinchilla Law 要求数据量与参数量同比增长,SGD/Adam 噪声有隐式正则化。微调容易过拟合:数据量小(千级),模型参数远大于数据量。解决:LoRA 降低可训练参数、小 LR、Early Stopping、数据增强。
题13:Cosine Annealing 学习率调度的数学公式和优势?
标准答案: $\eta_t = \eta_{min} + 0.5(\eta_{max}-\eta_{min})(1+\cos(t\pi/T))$。优势:前期衰减慢充分学习,中期衰减加快,末期衰减放缓精细调整,平滑无突变。LLM 配置:Warmup 2000 步 + Cosine 衰减到峰值 10%。
题14:对比 AllReduce、AllGather、Reduce-Scatter 通信原语。
标准答案: AllReduce = Reduce-Scatter + AllGather。AllReduce 所有节点得到相同的聚合结果。AllGather 所有节点收集到完整数据。Reduce-Scatter 每个节点得到聚合结果的一个分片。Ring AllReduce 通信量 ≈ 2×数据量,与节点数无关。
题15:Loss 不下降如何系统性排查?
标准答案: ①检查是否 NaN/Inf;②过拟合小 batch 测试(无法过拟合说明有 bug);③检查 LR(太大震荡,太小慢);④检查数据(标签正确性、数据加载、数据质量);⑤检查模型(梯度是否流动、权重是否更新);⑥检查配置(权重衰减、Dropout、Batch size)。关键技巧:torch.autograd.set_detect_anomaly(True)。
本章总结:
| 核心主题 | 关键考点 |
|---|---|
| 优化器演进 | SGD→Momentum→AdaGrad→RMSProp→Adam→AdamW 的动机和公式 |
| 学习率调度 | Warmup+Cosine Decay 是 LLM 标配,理解每种调度的数学 |
| 梯度裁剪 | 按范数裁剪保持方向,max_norm=1.0 |
| 混合精度 | BF16 > FP16,Loss Scaling 原理,显存节省计算 |
| 分布式训练 | DDP vs DP,ZeRO 三个 Stage,3D 并行配置 |
| 训练稳定性 | Loss spike 处理、梯度累积/检查点、NaN 排查 |
| 数学基础 | KL 散度与交叉熵、Softmax 数值稳定、贝叶斯/MLE |