华为AI机试选择题知识点大全

华为AI机试选择题知识点大全(150分必背)

一、激活函数(高频考点⭐⭐⭐⭐⭐)

为什么需要激活函数?
如果没有激活函数,多层神经网络就是多个线性变换的叠加,最终还是线性变换。激活函数引入非线性,使网络能够拟合复杂函数。

1. ReLU(Rectified Linear Unit)⭐⭐⭐⭐⭐

数学定义:

1
2
3
4
5
6
7
8
9
f(x) = max(0, x) = {
x, if x > 0
0, if x0
}

导数:f'(x) = {
1, if x > 0
0, if x0
}

深度解析:

ReLU是2012年AlexNet使用后开始流行的激活函数,现在是深度学习的默认选择。

为什么ReLU缓解梯度消失?

  • Sigmoid在x很大或很小时,梯度接近0(饱和)
  • ReLU在正区域梯度恒为1,不会饱和
  • 反向传播时梯度不会衰减(正区域)

稀疏性的好处:

  • 假设50%神经元输出0,相当于网络”自动剪枝”
  • 减少神经元共适应,提升泛化能力
  • 计算更高效(跳过0值)

优点:

  • ✅ 计算极简:只需比较和赋值,无指数/对数运算
  • ✅ 训练速度比Sigmoid快6倍(Krizhevsky 2012)
  • ✅ 缓解梯度消失问题
  • ✅ 引入稀疏性(部分神经元输出0)
  • ✅ 更符合生物神经元特性(单侧抑制)

缺点:

  • Dead ReLU问题:负值输入导致梯度为0,神经元”死亡”
  • ❌ 输出不以0为中心(mean > 0),可能导致zigzag收敛
  • ❌ 输出无上界,可能导致数值不稳定

应对Dead ReLU:

  1. 使用较小的学习率(如0.01而非0.1)
  2. 使用Leaky ReLU变体
  3. He初始化:W ~ N(0, sqrt(2/n_in))
  4. Batch Normalization:保持输入分布稳定
  5. 避免大的负bias初始化

常见陷阱题 ⚠️:

题目1:ReLU的输出范围是?

  • A. (-1, 1)
  • B. (0, 1)
  • C. [0, +∞) ✅
  • D. (-∞, +∞)

💡 解析:ReLU负值输出0,正值保持原样,所以是[0, +∞)


题目2:使用ReLU时,发现训练到一半损失不再下降,可能原因?

  • A. 学习率过大导致大量神经元死亡 ✅
  • B. 数据量太少
  • C. ReLU不适合该任务
  • D. 需要更多层

💡 解析:学习率过大 → 权重剧烈更新 → 激活值变负 → 梯度为0 → 神经元死亡 → 损失卡住


题目3:以下哪个不是ReLU的优点?

  • A. 计算简单
  • B. 缓解梯度消失
  • C. 输出以0为中心 ✅
  • D. 训练速度快

💡 解析:ReLU输出[0,+∞),均值>0,不以0为中心。Tanh的输出(-1,1)才以0为中心。

记忆技巧 🧠:

  • ReLU = “如来留” → 正值如来,负值不留
  • Dead ReLU = “学习太快死得快” → 学习率大导致神经元死亡

2. Leaky ReLU(泄漏ReLU)⭐⭐⭐⭐

数学定义:

1
2
3
4
5
6
7
8
9
10
11
f(x) = maxx, x) = {
x, if x > 0
αx, if x0
}

其中α通常为0.01(即负值斜率为0.01

导数:f'(x) = {
1, if x > 0
α, if x0
}

直观理解:

  • ReLU:负值直接砍成0(斩草除根)
  • Leaky ReLU:负值保留一点点(留一线生机)
  • 就像给”死亡神经元”装了个”心脏起搏器”

为什么能解决Dead ReLU?

1
2
3
4
假设x = -5,α = 0.01

ReLU: f(-5) = 0, 梯度 = 0 ❌ 死亡
Leaky ReLU: f(-5) = -0.05, 梯度 = 0.01 ✅ 还能学习

即使输入为负,梯度仍然是0.01,神经元不会完全”死亡”。

优点:

  • ✅ 解决Dead ReLU问题(负值有小梯度0.01)
  • ✅ 保留ReLU的计算优势(只多一次乘法)
  • ✅ 实践中通常比ReLU效果稍好
  • ✅ 输出可以为负,均值更接近0

缺点:

  • ❌ α需要手动设置(虽然0.01通常够用)
  • ❌ 不同任务最优α可能不同

常见陷阱题 ⚠️:

题目:Leaky ReLU的α通常设为多少?

  • A. 0.001
  • B. 0.01 ✅
  • C. 0.1
  • D. 0.5

💡 解析:α=0.01是最常用的值。太小(0.001)效果接近ReLU,太大(0.1)会破坏ReLU特性。

记忆技巧 🧠:

  • Leaky = “漏一点” → 负值”漏”出来0.01的梯度
  • α=0.01 → “01”倒过来还是”01”(回文数好记)

3. PReLU(Parametric ReLU)⭐⭐⭐

数学定义:

1
2
3
4
f(x) = maxx, x)  其中α是可学习参数

初始化:α = 0.25(经验值)
训练中通过反向传播更新α

与Leaky ReLU的区别:

特性 Leaky ReLU PReLU
α值 固定(0.01) 可学习
参数量 0 每个通道1个α
灵活性
计算成本 略高

参数量计算:

1
2
3
4
5
6
假设卷积层有C个输出通道
- 共享版本:1个α(所有通道共享)
- 通道版本:C个α(每个通道独立)

例:ResNet-50约有100个PReLU层,若每层64通道
参数量:100 × 64 = 6400个α(相比总参数可忽略)

优点:

  • ✅ 自动学习最优的负值斜率
  • ✅ 不同层/通道可以有不同的α
  • ✅ ImageNet上比ReLU提升约1%

缺点:

  • ❌ 参数量略增(但很少)
  • ❌ 可能过拟合小数据集
  • ❌ 增加训练复杂度

常见陷阱题 ⚠️:

题目:PReLU相比Leaky ReLU的主要改进是?

  • A. 计算速度更快
  • B. α可以学习 ✅
  • C. 解决梯度消失
  • D. 输出范围更大

💡 解析:PReLU的核心就是让α变成可学习参数,而不是固定值。

记忆技巧 🧠:

  • PReLU中的P = Parametric(参数化) → α是参数
  • Leaky固定,PReLU灵活 → 就像”固定工资”vs”绩效工资”

4. ELU(Exponential Linear Unit)⭐⭐⭐

数学定义:

1
2
3
4
5
6
7
8
9
10
11
f(x) = {
x, if x > 0
α(e^x - 1), if x0
}

通常α = 1.0

导数:f'(x) = {
1, if x > 0
f(x) + α, if x0
}

直观理解:

  • 正值:和ReLU一样保持原样
  • 负值:指数衰减,有下界-α
  • 就像”软着陆”,不是直接砍到0

为什么输出均值接近0?

1
2
3
ReLU:      输出范围[0, +∞),均值 > 0
Leaky ReLU: 输出范围(-∞, +∞),但负值太小,均值仍 > 0
ELU: 输出范围[-α, +∞),负值有饱和,均值 ≈ 0

输出均值接近0 → 类似Batch Normalization效果 → 加速收敛

负值饱和的好处:

1
2
3
4
5
6
7
x → -∞时,f(x) → -α(有下界)

优点:对噪声输入更鲁棒
例:异常值x = -100
ReLU: 输出0
Leaky ReLU: 输出-1
ELU: 输出-1.0(饱和,不会太负)

优点:

  • ✅ 输出均值接近0,加速收敛(比ReLU快)
  • ✅ 负值饱和区域产生对噪声的鲁棒性
  • ✅ 不会有Dead ReLU问题
  • ✅ 实验表明分类准确率通常高于ReLU

缺点:

  • ❌ 计算exp较慢(比ReLU慢约5倍)
  • ❌ 负值区域计算复杂
  • ❌ 实际应用较少(性价比不如ReLU)

常见陷阱题 ⚠️:

题目:ELU的输出范围是?

  • A. [0, +∞)
  • B. (-1, +∞)
  • C. [-1, +∞) ✅(假设α=1)
  • D. (-∞, +∞)

💡 解析:当x→-∞时,e^x→0,所以f(x)→α(0-1)=-α=-1


题目:ELU相比ReLU的主要优势?

  • A. 计算速度快
  • B. 输出均值接近0 ✅
  • C. 实现简单
  • D. 参数量少

💡 解析:ELU的核心优势是输出均值接近0,其他都不如ReLU。

记忆技巧 🧠:

  • ELU = Exponential(指数) → 负值用e^x
  • 有下界-α → “触底反弹”不会无限负

5. GELU(Gaussian Error Linear Unit)⭐⭐⭐⭐⭐大模型标配

数学定义:

1
2
3
4
f(x) = x * Φ(x)

其中Φ(x)是标准正态分布的累积分布函数(CDF)
Φ(x) = P(X ≤ x), X ~ N(0, 1)

为什么叫”高斯误差”?

  • 把输入x看作随机变量
  • Φ(x)表示”输入小于等于x的概率”
  • 相当于用概率来加权输入

计算方法:

精确版本(慢):

1
2
import scipy.stats
f(x) = x * scipy.stats.norm.cdf(x)

近似版本(快,工程常用):

1
f(x) ≈ 0.5 * x * (1 + tanh[√(2/π) * (x + 0.044715x³)])

更简单的近似:

1
f(x) ≈ x * sigmoid(1.702 * x)

直观理解:

1
2
3
x = 0时:Φ(0) = 0.5,输出 = 0.5 * 0 = 0
x >> 0时:Φ(x) ≈ 1, 输出 ≈ x(接近恒等映射)
x << 0时:Φ(x) ≈ 0, 输出 ≈ 0(接近ReLU)

GELU是”软化版”的ReLU,在0附近更平滑。

与ReLU对比:

1
2
3
输入x:  -3    -1    0    1    3
ReLU: 0 0 0 1 3
GELU: -0.004 -0.16 0 0.84 3

GELU在负值区域不是硬截断,而是平滑过渡。

为什么Transformer用GELU?

  1. 平滑性好:处处可导,训练稳定
  2. 非单调性:在负值区有轻微”回升”,增加表达能力
  3. 概率解释:符合Transformer的注意力概率思想
  4. 实验效果好:BERT、GPT等都验证了优势

优点:

  • ✅ Transformer和BERT中的标准激活函数
  • ✅ 平滑的非线性,训练稳定
  • ✅ 考虑输入的概率分布,理论优雅
  • ✅ 实验效果通常优于ReLU

缺点:

  • ❌ 计算比ReLU复杂(但可用近似)
  • ❌ 理论分析困难

常见陷阱题 ⚠️:

题目:以下哪个模型使用GELU激活函数?

  • A. ResNet
  • B. AlexNet
  • C. BERT ✅
  • D. VGG

💡 解析:BERT(2018)使用GELU,之后的Transformer模型(GPT、T5等)都跟进。早期CNN(ResNet、VGG)用ReLU。


题目:GELU相比ReLU的主要特点?

  • A. 计算更快
  • B. 平滑且有概率解释 ✅
  • C. 解决梯度消失
  • D. 参数量更少

💡 解析:GELU的核心是平滑性和概率解释(用CDF加权)。


题目:GELU在x=0处的输出是?

  • A. 0 ✅
  • B. 0.5
  • C. 1
  • D. undefined

💡 解析:f(0) = 0 * Φ(0) = 0 * 0.5 = 0

记忆技巧 🧠:

  • GELU = Gaussian(高斯) → 用正态分布CDF
  • 大模型三剑客:GELU + LayerNorm + AdamW
  • Transformer全家桶标配

6. Sigmoid(S型函数)⭐⭐⭐⭐

数学定义:

1
2
3
4
f(x) = 1 / (1 + e^(-x)) = e^x / (e^x + 1)

值域:(0, 1)
导数:f'(x) = f(x) * (1 - f(x))

导数推导过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f(x) = 1 / (1 + e^(-x))

u = 1 + e^(-x)
f = 1/u = u^(-1)

df/dx = df/du * du/dx
= -u^(-2) * (-e^(-x))
= e^(-x) / (1 + e^(-x)

进一步化简:
f'(x) = e^(-x) / (1 + e^(-x)
= 1/(1+e^(-x)) * e^(-x)/(1+e^(-x))
= f(x) * [1 - 1/(1+e^(-x))]
= f(x) * (1 - f(x)) ✅优美的形式

为什么叫”S型”?

1
2
3
4
画出曲线像字母S:
x → -∞: f(x) → 0
x = 0: f(x) = 0.5
x → +∞: f(x) → 1

关键值记忆:

1
2
3
4
5
sigmoid(0)  = 0.5
sigmoid(1) ≈ 0.73
sigmoid(-1) ≈ 0.27
sigmoid(5) ≈ 0.993 (接近1
sigmoid(-5) ≈ 0.007 (接近0

梯度消失问题详解:

为什么会梯度消失?

1
2
3
4
5
6
7
f'(x) = f(x)(1-f(x))

f(x)=0.1时:f'(x) = 0.1*0.9 = 0.09
f(x)=0.5时:f'(x) = 0.5*0.5 = 0.25(最大)
f(x)=0.9时:f'(x) = 0.9*0.1 = 0.09

梯度最大值只有0.25!

多层网络的灾难:

1
2
3
4
假设10层网络,每层用Sigmoid
反向传播:梯度 = 0.25^109.5e-7(几乎为0

这就是为什么深层网络不用Sigmoid

输出不以0为中心的问题:

1
2
3
4
5
6
Sigmoid输出(0,1),均值 > 0

假设所有输入的梯度都是正数
→ 权重更新方向受限
→ 收敛路径呈"之字形"(zigzag)
→ 收敛变慢

优点:

  • ✅ 输出范围(0,1),适合概率输出
  • ✅ 平滑可导,导数形式优美
  • ✅ 有明确的概率解释

缺点:

  • 梯度消失严重(两端梯度接近0,最大0.25)
  • 输出不以0为中心(均值>0.5,导致zigzag收敛)
  • ❌ 计算exp较慢
  • ❌ 深层网络训练困难

应用场景:

  1. 二分类输出层(输出概率)
  2. 门控机制(LSTM的输入门、遗忘门、输出门)
  3. 注意力权重(早期Attention)

常见陷阱题 ⚠️:

题目:Sigmoid函数的导数最大值是?

  • A. 1
  • B. 0.5
  • C. 0.25 ✅
  • D. 0.1

💡 解析:f’(x)=f(x)(1-f(x)),在f(x)=0.5时取最大值0.25


题目:为什么深层网络不用Sigmoid?

  • A. 计算太慢
  • B. 梯度消失严重 ✅
  • C. 输出范围太小
  • D. 不可导

💡 解析:Sigmoid梯度最大0.25,多层连乘导致梯度消失。


题目:Sigmoid(0)等于?

  • A. 0
  • B. 0.5 ✅
  • C. 1
  • D. undefined

💡 解析:f(0) = 1/(1+e^0) = 1/2 = 0.5

记忆技巧 🧠:

  • Sigmoid = “挤压函数” → 把任意值挤压到(0,1)
  • 导数 = 自己乘自己的补 → f’(x) = f(x)(1-f(x))
  • 梯度最大0.25 → “四分之一”,记住这个数
  • 深层网络的噩梦 → 梯度消失克星

7. Tanh(双曲正切)⭐⭐⭐⭐

数学定义:

1
2
3
4
5
6
f(x) = tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))
= (e^(2x) - 1) / (e^(2x) + 1)
= 2*sigmoid(2x) - 1

值域:(-1, 1)
导数:f'(x) = 1 - f(x)²

与Sigmoid的关系:

1
2
3
4
5
6
7
8
9
10
tanh(x) = 2*sigmoid(2x) - 1

证明:
sigmoid(2x) = 1/(1+e^(-2x))
2*sigmoid(2x) - 1 = 2/(1+e^(-2x)) - 1
= (2 - 1 - e^(-2x))/(1+e^(-2x))
= (1 - e^(-2x))/(1+e^(-2x))
× (e^x/e^x)
= (e^x - e^(-x))/(e^x + e^(-x))
= tanh(x)

导数推导:

1
2
3
4
5
6
7
8
9
10
f(x) = (e^x - e^(-x)) / (e^x + e^(-x))

u = e^x - e^(-x), v = e^x + e^(-x)
f = u/v

f'(x) = (u'v - uv') / v²
= [(e^x+e^(-x))(e^x+e^(-x)) - (e^x-e^(-x))(e^x-e^(-x))] / v²
= [v² - u²] / v²
= 1 - (u/v)²
= 1 - tanh²(x)

关键值记忆:

1
2
3
4
5
tanh(0)  = 0
tanh(1) ≈ 0.76
tanh(-1) ≈ -0.76
tanh(3) ≈ 0.995 (接近1
tanh(-3) ≈ -0.995 (接近-1

为什么比Sigmoid好?

1. 输出以0为中心:

1
2
Sigmoid: 输出(0,1),均值≈0.5 → 权重更新有偏
Tanh: 输出(-1,1),均值=0 → 权重更新无偏

2. 梯度更大:

1
2
3
4
Sigmoid最大梯度:0.25
Tanh最大梯度: 1.0(在x=0处)

tanh的梯度是sigmoid的4倍!

但仍有梯度消失:

1
2
3
4
5
6
f'(x) = 1 - tanh²(x)

tanh(x)=±0.9时:f'(x) = 1 - 0.81 = 0.19
tanh(x)=±0.99时:f'(x) = 1 - 0.98 = 0.02

两端仍然梯度小,只是比Sigmoid好一些

优点:

  • 输出以0为中心(比Sigmoid好)
  • ✅ 梯度比Sigmoid大(最大为1)
  • ✅ 可以看作Sigmoid的改进版
  • ✅ 收敛速度比Sigmoid快

缺点:

  • 仍有梯度消失问题(两端饱和)
  • ❌ 计算复杂(需要计算exp)
  • ❌ 深层网络仍不适用

应用场景:

  1. RNN/LSTM的隐藏状态激活(输出需要正负)
  2. GAN的判别器(输出范围对称)
  3. 传统神经网络(现在较少)

常见陷阱题 ⚠️:

题目:tanh(0)等于?

  • A. -1
  • B. 0 ✅
  • C. 0.5
  • D. 1

💡 解析:tanh(0) = (e^0-e^0)/(e^0+e^0) = 0/2 = 0


题目:tanh相比Sigmoid的优势?

  • A. 计算更快
  • B. 输出以0为中心 ✅
  • C. 不会梯度消失
  • D. 输出范围更大

💡 解析:tanh输出(-1,1)以0为中心,这是相比Sigmoid的主要优势。


题目:tanh的导数最大值是?

  • A. 0.25
  • B. 0.5
  • C. 1.0 ✅
  • D. 2.0

💡 解析:f’(x)=1-tanh²(x),当tanh(x)=0时取最大值1。


题目:以下哪个关系正确?

  • A. tanh(x) = sigmoid(x) - 1
  • B. tanh(x) = 2*sigmoid(2x) - 1 ✅
  • C. tanh(x) = sigmoid(2x)
  • D. tanh(x) = 2*sigmoid(x)

💡 解析:tanh和sigmoid有数学关系:tanh(x) = 2*sigmoid(2x) - 1

记忆技巧 🧠:

  • Tanh = “双曲正切” → 值域(-1,1),关于原点对称
  • 比Sigmoid强,但仍不如ReLU → 像”青铜晋级白银,但还没到黄金”
  • 导数 = 1减自己的平方 → f’(x) = 1 - f²(x)
  • RNN/LSTM标配 → 需要正负输出的场景

8. Softmax(多分类输出层)⭐⭐⭐⭐⭐

数学定义:

1
2
3
4
5
对于向量x = [x₁, x₂, ..., xₙ]

softmax(x)ᵢ = e^(xᵢ) / Σⱼ e^(xⱼ)

其中i表示第i个类别

直观理解:

  • 把任意实数向量转换为概率分布
  • 输出都是正数,且和为1
  • 值越大的分量,softmax后占比越大

为什么叫”Soft”max?

1
2
Hardmax(硬最大):[2, 1, 3][0, 0, 1](最大的变1,其他变0
Softmax(软最大):[2, 1, 3][0.24, 0.09, 0.67](保留概率分布)

Softmax是”可微分”的max操作。

计算示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输入:x = [1, 2, 3]

步骤1:计算exp
e^1 = 2.72
e^2 = 7.39
e^3 = 20.09

步骤2:求和
sum = 2.72 + 7.39 + 20.09 = 30.20

步骤3:归一化
softmax(x) = [2.72/30.20, 7.39/30.20, 20.09/30.20]
= [0.09, 0.24, 0.67]

验证:0.09 + 0.24 + 0.67 = 1.0

数值稳定技巧(重要!):

问题:直接计算会溢出

1
2
x = [1000, 1001, 1002]
e^1000 = inf ❌ 溢出!

解决:减去最大值

1
2
3
4
5
6
7
8
9
10
def stable_softmax(x):
x = x - np.max(x) # 先减去最大值
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)

# 数学证明:
softmax(x) = e^xᵢ / Σe^xⱼ
= e^(xᵢ-c) / Σe^(xⱼ-c) # c是任意常数

令c = max(x),则所有指数≤0,不会溢出

示例:

1
2
3
4
5
6
7
8
原始:x = [1000, 1001, 1002]
减去max:x' = [-2, -1, 0]
计算:
e^(-2) = 0.135
e^(-1) = 0.368
e^0 = 1.0
sum = 1.503
softmax = [0.09, 0.24, 0.67] ✅ 结果正确且稳定

Softmax的导数(重要):

单个输出对单个输入的导数:

1
2
3
4
5
6
7
∂softmax(xᵢ)/∂xⱼ = {
softmax(xᵢ) * (1 - softmax(xᵢ)), if i = j
-softmax(xᵢ) * softmax(xⱼ), if i ≠ j
}

简写:∂yᵢ/∂xⱼ = yᵢ(δᵢⱼ - yⱼ)
其中δᵢⱼ是Kronecker delta(i=j时为1,否则为0

与交叉熵结合时的导数(超级简洁!):

1
2
3
4
5
6
7
Loss = -Σ ylog(ŷᵢ)  (交叉熵)
其中ŷ = softmax(x)

∂Loss/∂xᵢ = ŷᵢ - yᵢ ✅ 超级简单!

例:真实标签y=[0,1,0],预测ŷ=[0.1,0.7,0.2]
梯度 = [0.1-0, 0.7-1, 0.2-0] = [0.1, -0.3, 0.2]

为什么这么简单? 因为softmax和交叉熵是”天生一对”,求导后很多项抵消。

温度参数(Temperature):

1
2
3
4
5
6
7
8
9
10
softmax_T(xᵢ) = e^(xᵢ/T) / Σⱼ e^(xⱼ/T)

T=1: 标准softmax
T→0: 接近hardmax(输出趋向one-hot)
T→∞: 接近均匀分布

例:x = [1, 2, 3]
T=1: [0.09, 0.24, 0.67] (正常)
T=0.5: [0.04, 0.21, 0.75] (更尖锐)
T=2: [0.16, 0.29, 0.55] (更平滑)

应用:知识蒸馏(Knowledge Distillation)用大T软化概率分布。

优点:

  • ✅ 输出总和为1,表示概率分布
  • ✅ 多分类任务的标准输出激活函数
  • ✅ 与交叉熵配合,梯度形式简洁
  • ✅ 可微分,适合梯度下降

缺点:

  • ❌ 计算exp开销大(需要计算所有类别)
  • ❌ 对异常值敏感(某个值很大会主导输出)
  • ❌ 类别数很多时计算慢

常见陷阱题 ⚠️:

题目:Softmax输出的总和是?

  • A. 0
  • B. 1 ✅
  • C. n(类别数)
  • D. 不确定

💡 解析:Softmax输出是概率分布,所有概率之和=1。


题目:为什么Softmax要减去max(x)?

  • A. 加速计算
  • B. 防止数值溢出 ✅
  • C. 提高精度
  • D. 符合数学定义

💡 解析:直接计算e^x可能溢出,减去最大值保证指数≤0。


题目:Softmax通常与哪个损失函数配合?

  • A. MSE
  • B. MAE
  • C. Cross-Entropy ✅
  • D. Hinge Loss

💡 解析:多分类任务标配:Softmax + Cross-Entropy。


题目:Softmax温度T增大会导致?

  • A. 输出更尖锐
  • B. 输出更平滑 ✅
  • C. 计算更快
  • D. 精度提高

💡 解析:T增大→除以更大的数→指数差异减小→分布更平滑。

记忆技巧 🧠:

  • Softmax = “软化的最大值” → 可微分的argmax
  • 输出必和为1 → 概率分布
  • 计算三步走:exp → sum → 归一化
  • 数值稳定:减最大值 → 防止e^1000溢出
  • 与交叉熵是CP → 梯度超简单ŷ-y

9. Swish(自门控激活函数)⭐⭐⭐

数学定义:

1
2
3
4
5
6
f(x) = x * sigmoid(βx)
= x / (1 + e^(-βx))

其中β通常为1(标准Swish)或可学习参数

导数:f'(x) = f(x) + sigmoid(βx) * (1 - f(x))

β的影响:

1
2
3
4
5
β=0:  f(x) = x/2(线性)
β=1: f(x) = x*sigmoid(x)(标准Swish)
β→∞: f(x) → ReLU

β控制"平滑度":越大越接近ReLU

与ReLU对比:

1
2
3
输入x:   -2    -1    0    1    2
ReLU: 0 0 0 1 2
Swish: -0.24 -0.27 0 0.73 1.76

Swish在负值区域有小的非零输出,更平滑。

为什么叫”自门控”?

1
2
3
4
5
6
7
f(x) = x * sigmoid(x)
↑ ↑
内容 门控信号

sigmoid(x)像个"门",控制有多少x通过
x很负时,门几乎关闭(sigmoid≈0
x很正时,门完全打开(sigmoid≈1

直观理解:

  • x < 0: 轻微负输出(不完全截断)
  • x = 0: 输出0
  • x > 0: 输出接近x(但略小)
  • x >> 0: 输出≈x(接近恒等映射)

优点:

  • ✅ Google通过NAS(神经架构搜索)发现
  • ✅ 性能优于ReLU(ImageNet上提升0.6-0.9%)
  • ✅ 平滑、处处可导
  • ✅ 非单调性(在x≈-1.5处有小dip)增加表达力

缺点:

  • ❌ 计算成本略高于ReLU(需要sigmoid)
  • ❌ 非单调性可能导致优化困难
  • ❌ 实际应用较少(ReLU仍是主流)

应用场景:

  • EfficientNet使用Swish
  • MobileNet V3使用h-swish(硬件友好版本)
  • 追求极致精度的场景

h-swish(硬件优化版本):

1
2
3
4
5
6
7
8
h-swish(x) = x * ReLU6(x + 3) / 6

其中ReLU6(x) = min(max(0, x), 6)

优点:
- 避免sigmoid的exp计算
- 速度快,适合移动端
- 精度接近标准Swish

常见陷阱题 ⚠️:

题目:Swish(0)等于?

  • A. -1
  • B. 0 ✅
  • C. 0.5
  • D. 1

💡 解析:f(0) = 0 * sigmoid(0) = 0 * 0.5 = 0


题目:Swish相比ReLU的优势?

  • A. 计算更快
  • B. 平滑且性能更好 ✅
  • C. 避免梯度消失
  • D. 实现更简单

💡 解析:Swish更平滑,Google实验证明性能优于ReLU。


题目:以下哪个模型使用Swish?

  • A. ResNet
  • B. VGG
  • C. EfficientNet ✅
  • D. AlexNet

💡 解析:EfficientNet(2019)使用Swish激活函数。

记忆技巧 🧠:

  • Swish = x乘sigmoid(x) → 自己给自己当门
  • Google搜出来的 → 通过NAS自动搜索发现
  • 比ReLU平滑 → 像”磨光的ReLU”
  • EfficientNet标配 → 追求极致精度

10. 激活函数对比总结表 📊

激活函数 公式 值域 梯度消失 计算复杂度 适用场景
ReLU max(0,x) [0,∞) ❌ 否 ⭐ 极快 CNN隐藏层(最常用)
Leaky ReLU max(0.01x,x) (-∞,∞) ❌ 否 ⭐ 极快 替代ReLU
PReLU max(αx,x) (-∞,∞) ❌ 否 ⭐ 快 追求精度的CNN
ELU x if x>0 else α(e^x-1) [-α,∞) ❌ 否 ⭐⭐⭐ 慢 对噪声鲁棒
GELU x*Φ(x) (-∞,∞) ❌ 否 ⭐⭐ 中 Transformer标配
Sigmoid 1/(1+e^(-x)) (0,1) ✅ 是 ⭐⭐⭐ 慢 二分类输出、LSTM门
Tanh (e^x-e^(-x))/(e^x+e^(-x)) (-1,1) ✅ 是 ⭐⭐⭐ 慢 RNN隐藏状态
Softmax e^xi/Σe^xj (0,1) ❌ 否 ⭐⭐⭐ 慢 多分类输出
Swish x*sigmoid(x) (-∞,∞) ❌ 否 ⭐⭐ 中 EfficientNet

选择建议:

  1. CNN隐藏层:ReLU(默认)或Leaky ReLU(更稳)
  2. Transformer:GELU(标配)
  3. RNN/LSTM:Tanh(隐藏状态)+ Sigmoid(门控)
  4. 二分类输出:Sigmoid
  5. 多分类输出:Softmax
  6. 追求极致性能:Swish或PReLU

常见错误组合 ⚠️:

  • ❌ CNN用Sigmoid隐藏层(梯度消失)
  • ❌ 多分类用Sigmoid输出(应该用Softmax)
  • ❌ Transformer用ReLU(现在都用GELU)
  • ❌ 二分类用Softmax(太重,Sigmoid就够)

二、神经元死亡问题(Dead Neuron)⭐⭐⭐⭐

什么是神经元死亡?

定义: 当使用ReLU激活函数时,如果神经元的输入持续为负,则:

  • 激活值恒为0
  • 梯度恒为0
  • 权重无法更新
  • 神经元”死亡”,永久失效

数学表达:

1
2
3
4
5
6
7
8
对于神经元j:
z_j = Σ(w_ij * x_i) + b_j (加权和)
a_j = ReLU(z_j) = max(0, z_j) (激活值)

如果z_j < 0(一直为负):
a_j = 0
→ ∂L/∂w_ij = ∂L/∂a_j * ∂a_j/∂z_j * x_i = ∂L/∂a_j * 0 * x_i = 0
→ 权重不更新,永远死亡

形象比喻:

  • 就像工厂流水线上的一个工人”罢工”了
  • 无论后续发生什么,他都不干活(输出0)
  • 也不接受反馈(梯度0),无法改进
  • 最终这个工位永久废弃

产生原因(深度分析)

1. 学习率过大 ⭐⭐⭐⭐⭐(最常见)

机制:

1
2
3
4
5
6
7
假设当前:z = w*x + b = 0.5(正值,活着)
梯度更新:w_new = w - lr * grad

如果lr=0.1,grad=10
w_new = w - 1.0 (权重剧烈变化)
→ z_new = w_new*x + b = -0.5(变负了!)
→ 神经元死亡

为什么大学习率危险?

  • 权重更新幅度大 → 容易”跳过头”
  • 一旦跳到负区域,梯度为0,再也回不来
  • 像开车速度太快,刹不住掉悬崖

实验证据:

1
2
3
学习率0.0110%神经元死亡
学习率0.150%神经元死亡 ⚠️
学习率1.090%神经元死亡 ☠️

2. 权重初始化不当 ⭐⭐⭐⭐

错误初始化示例:

1
2
3
4
5
6
7
8
# 错误1:全0初始化
w = np.zeros(shape) # ❌ 对称性问题,所有神经元学到相同东西

# 错误2:初始化过大
w = np.random.randn(shape) * 10 # ❌ 初始z过大,可能全负

# 错误3:初始化过小
w = np.random.randn(shape) * 0.0001 # ❌ 梯度过小,训练慢

正确初始化(He初始化):

1
2
3
4
5
6
7
8
# 专为ReLU设计
w = np.random.randn(n_in, n_out) * np.sqrt(2.0 / n_in)

# 为什么是sqrt(2/n_in)?
# 理论推导:保持方差在前向和反向传播中稳定
# Var(z) = n_in * Var(w) * Var(x)
# 如果Var(w) = 2/n_in,且Var(x)≈1,则Var(z)≈2
# 考虑ReLU砍掉一半,实际方差≈1(完美)

3. 数据分布问题 ⭐⭐⭐

场景1:输入数据偏负

1
2
3
假设输入特征x全是负数:x = [-1, -2, -3]
即使权重w是正数,z = w*x仍可能是负数
→ 神经元容易死亡

场景2:数据未归一化

1
2
3
4
5
特征1[0, 1, 2](小范围)
特征2[1000, 2000, 3000](大范围)

权重更新会被大特征主导
→ 小特征对应神经元容易死亡

解决:数据预处理

1
2
3
4
5
# 标准化(z-score)
X = (X - X.mean()) / X.std() # 均值0,标准差1

# 归一化(min-max)
X = (X - X.min()) / (X.max() - X.min()) # 范围[0,1]

4. 负bias初始化 ⭐⭐

危险操作:

1
2
3
4
5
bias = -1.0  # ❌ 负bias

假设输入x和权重w都很小,接近0
z = w*x + bias ≈ 0 + (-1.0) = -1.0 (负数!)
→ 神经元一出生就死亡

正确做法:

1
2
3
bias = 0.01  # ✅ 小正数,确保初始激活为正
# 或者
bias = 0.0 # ✅ 零初始化(最常用)

检测方法(实战代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import torch

def detect_dead_neurons(model, dataloader):
"""检测模型中的死亡神经元"""
dead_counts = {}
total_counts = {}

model.eval()
with torch.no_grad():
for batch in dataloader:
x, _ = batch
x = x.to(device)

# 注册hook记录激活值
activations = {}
def hook_fn(name):
def hook(module, input, output):
activations[name] = output
return hook

# 为每个ReLU层注册hook
hooks = []
for name, module in model.named_modules():
if isinstance(module, torch.nn.ReLU):
hooks.append(module.register_forward_hook(hook_fn(name)))

# 前向传播
model(x)

# 统计死亡神经元
for name, act in activations.items():
dead = (act == 0).float().mean().item()
dead_counts[name] = dead_counts.get(name, 0) + dead
total_counts[name] = total_counts.get(name, 0) + 1

# 移除hooks
for hook in hooks:
hook.remove()

# 计算平均死亡率
results = {}
for name in dead_counts:
results[name] = dead_counts[name] / total_counts[name]

return results

# 使用示例
dead_ratios = detect_dead_neurons(model, val_loader)
for layer, ratio in dead_ratios.items():
print(f"{layer}: {ratio*100:.1f}% neurons dead")
if ratio > 0.5:
print(f" ⚠️ 警告:超过50%神经元死亡!")

健康标准:

1
2
3
4
0-20%死亡率:  ✅ 健康
20-40%死亡率: ⚠️ 需要关注
40-60%死亡率: 🔥 有问题,需要调整
60%+死亡率: ☠️ 严重问题,模型几乎废了

解决方案(从简单到复杂)

方案1:降低学习率 ⭐⭐⭐⭐⭐(首选)

1
2
3
4
5
6
7
8
9
10
# 从大学习率开始实验,逐步降低
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 从0.001开始

# 如果发现大量死亡,降低10倍
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)

# 使用学习率调度
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5
)

经验值:

  • Adam优化器:0.001(默认)或0.0001
  • SGD优化器:0.01或0.001

方案2:使用Leaky ReLU ⭐⭐⭐⭐⭐(最简单有效)

1
2
3
4
5
6
7
8
# 替换所有ReLU为Leaky ReLU
# 之前
self.relu = nn.ReLU()

# 之后
self.relu = nn.LeakyReLU(negative_slope=0.01)

# 一行代码,立竿见影!

方案3:Batch Normalization ⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
class Block(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(64, 64, 3, padding=1)
self.bn = nn.BatchNorm2d(64) # BN层
self.relu = nn.ReLU()

def forward(self, x):
x = self.conv(x)
x = self.bn(x) # 归一化,保持输入分布稳定
x = self.relu(x) # 死亡概率大幅降低
return x

为什么BN有效?

  • 归一化使z的分布稳定在均值0附近
  • 正负值分布更均衡
  • 神经元不容易”一边倒”全变负

方案4:He初始化 ⭐⭐⭐⭐

1
2
3
4
5
6
7
def init_weights(m):
if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)

model.apply(init_weights)

方案5:梯度裁剪 ⭐⭐⭐

1
2
3
4
# 训练循环中
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 裁剪
optimizer.step()

防止梯度爆炸 → 防止权重剧变 → 防止神经元死亡

常见陷阱题 ⚠️

题目1:以下哪个不是Dead ReLU的原因?

  • A. 学习率过大
  • B. 权重初始化不当
  • C. 使用Batch Normalization ✅
  • D. 数据分布偏负

💡 解析:BN是解决方案,不是原因。


题目2:检测到50%神经元死亡,首先应该?

  • A. 增加层数
  • B. 降低学习率 ✅
  • C. 增加数据
  • D. 换优化器

💡 解析:学习率过大是最常见原因,先降低学习率试试。


题目3:He初始化的权重方差应该是?

  • A. 1/n_in
  • B. 2/n_in ✅
  • C. 1/n_out
  • D. 2/n_out

💡 解析:He初始化 Var(w) = 2/n_in,专为ReLU设计。

记忆技巧 🧠:

  • Dead ReLU = “学习太快死得快” → 学习率大是主因
  • 三大救星:小学习率、Leaky ReLU、BN
  • He初始化 → “何必用Xavier”(He for ReLU, Xavier for Tanh/Sigmoid)
  • 检测标准:超过40%就要警惕

三、Transformer架构(超高频⭐⭐⭐⭐⭐)

为什么Transformer重要?
Transformer是2017年后AI的革命性架构,BERT、GPT、LLaMA等所有大模型的基础。华为AI机试必考!

核心思想

传统RNN的问题:

  • 顺序计算,无法并行(t时刻依赖t-1时刻)
  • 长序列梯度消失/爆炸
  • 难以捕获长距离依赖

Transformer的解决方案:

  • Self-Attention:直接计算任意两个位置的关系
  • 并行计算:所有位置同时处理
  • 位置编码:弥补失去的位置信息

1. Self-Attention机制(核心中的核心)⭐⭐⭐⭐⭐

直观理解

问题: 翻译句子”The animal didn’t cross the street because it was too tired”中的”it”指什么?

Self-Attention的做法:

1
2
3
4
1. "it"作为Query,去询问所有词:"你和我有关系吗?"
2. "animal"回答:"关系很大!"(高相似度)
3. "street"回答:"关系不大"(低相似度)
4. 最终"it"的表示 = 加权和(主要来自"animal"

这就是Attention:通过相似度加权融合信息

数学定义

三个核心矩阵:

1
2
3
4
5
6
7
输入:XR^(n×d)  (n个token,每个维度d)

Query: Q = XW_Q ∈ R^(n×d_k) "我要找什么"
Key: K = XW_K ∈ R^(n×d_k) "我是什么"
Value: V = XW_V ∈ R^(n×d_v) "我的内容"

其中W_Q, W_K ∈ R^(d×d_k), W_V ∈ R^(d×d_v)

为什么需要三个矩阵?

  • 如果只用X本身:所有信息混在一起,无法区分”查询”和”被查询”
  • Q、K、V是X在不同子空间的投影,各司其职
  • 就像数据库:Query=查询条件,Key=索引,Value=数据

Attention计算公式:

1
2
3
4
5
6
7
8
9
10
11
12
13
Attention(Q, K, V) = softmax(QK^T / √d_k) V

步骤拆解:
1. 计算相似度矩阵:S = QK^T ∈ R^(n×n)
S[i,j] = Q[i]·K[j] (第i个token和第j个token的相似度)

2. 缩放:S = S / √d_k (防止方差过大)

3. 归一化:A = softmax(S) (转为概率分布)
A[i,j] 表示第i个token对第j个token的注意力权重

4. 加权求和:Output = AV ∈ R^(n×d_v)
Output[i] = Σ A[i,j] * V[j] (第i个token的新表示)

计算示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
输入句子:"I love AI"3个词)
假设每个词的嵌入维度d=4,d_k=d_v=2

X = [[1,0,1,0], # "I"
[0,1,0,1], # "love"
[1,1,0,0]] # "AI"

假设W_Q, W_K, W_V已训练好

步骤1:计算Q, K, V
Q = XW_Q = [[0.5, 0.3],
[0.2, 0.8],
[0.7, 0.1]]

K = XW_K = [[0.4, 0.6],
[0.3, 0.7],
[0.8, 0.2]]

V = XW_V = [[1.0, 0.5],
[0.5, 1.0],
[0.8, 0.8]]

步骤2:计算相似度矩阵
QK^T = [[0.38, 0.36, 0.46], # "I"对每个词的相似度
[0.32, 0.62, 0.30], # "love"对每个词的相似度
[0.31, 0.28, 0.58]] # "AI"对每个词的相似度

步骤3:缩放(d_k=2,√21.41
S = QK^T / 1.41 = [[0.27, 0.26, 0.33],
[0.23, 0.44, 0.21],
[0.22, 0.20, 0.41]]

步骤4Softmax归一化
A = softmax(S) = [[0.32, 0.31, 0.37], # "I"的注意力分布
[0.29, 0.42, 0.29], # "love"的注意力分布
[0.29, 0.28, 0.43]] # "AI"的注意力分布

步骤5:加权求和
Output = AV
对于"I"(第1行):
Output[0] = 0.32*[1.0,0.5] + 0.31*[0.5,1.0] + 0.37*[0.8,0.8]
= [0.77, 0.72]

为什么要除以√d_k?(重要!)

问题分析:

1
2
3
4
5
6
7
8
9
假设Q和K的元素独立同分布,均值0,方差1
则QK^T中每个元素 = q_1*k_1 + q_2*k_2 + ... + q_d*k_d

由于独立:
E[q_i * k_i] = E[q_i] * E[k_i] = 0
Var(q_i * k_i) = Var(q_i) * Var(k_i) = 1

因此:
Var(QK^T) = d_k * 1 = d_k (方差随维度线性增长!)

不缩放的后果:

1
2
3
4
5
6
7
8
9
10
当d_k=64时,QK^T的标准差 ≈ √64 = 8

假设某个值QK^T[i,j] = 8
softmax前:[8, 2, 1]
softmax后:[0.9975, 0.0024, 0.0001] ← 几乎one-hot!

进入softmax的饱和区:
- 梯度接近0
- 训练困难
- 无法学习

除以√d_k后:

1
2
3
4
缩放后:[8/8, 2/8, 1/8] = [1, 0.25, 0.125]
softmax后:[0.58, 0.24, 0.18] ✅ 分布更平滑

梯度正常,训练稳定

数学推导:

1
2
3
Var(QK^T / √d_k) = Var(QK^T) / d_k = d_k / d_k = 1

缩放后方差恒为1,无论维度多大!

时间和空间复杂度

时间复杂度:O(n²d)

1
2
3
4
5
6
1. 计算Q, K, V:O(nd²)  (3次矩阵乘法,n×d 乘 d×d)
2. QK^T:O(n²d) (n×d 乘 d×n)← 瓶颈
3. Softmax:O(n²) (对n×n矩阵操作)
4. AV:O(n²d) (n×n 乘 n×d)

总计:O(nd²) + O(n²d) ≈ O(n²d) (n通常较大)

空间复杂度:O(n²)

1
2
3
4
5
6
7
8
存储注意力权重矩阵A ∈ R^(n×n)

例:序列长度n=512
A的大小 = 512×512 = 262,144个元素
如果FP16:262,144 × 2字节 = 512KB(单头)

GPT-396层,每层32头:
总内存 = 512KB × 96 × 321.5GB(只存注意力!)

这就是为什么长序列很贵!

2. Multi-Head Attention(多头注意力)⭐⭐⭐⭐⭐

为什么需要多头?

单头的局限:

  • 只能在一个表示子空间捕获关系
  • 就像只用一个”视角”看问题

多头的优势:

1
2
3
4
5
6
7
8
例:"银行"这个词

1:关注金融语境("存钱""贷款"
2:关注地理语境("河岸""江边"
3:关注语法关系(主语、宾语)
...

多个视角综合,表示更丰富

数学定义

公式:

1
2
3
4
5
6
7
8
9
MultiHead(Q,K,V) = Concat(head_1, ..., head_h) W_O

其中:
head_i = Attention(QW_Q^i, KW_K^i, VW_V^i)

参数:
- h:头数(通常816
- d_model:总维度(如512
- d_k = d_v = d_model / h (每个头的维度,如64

为什么每个头的维度是d_model/h?

  • 保持总计算量不变
  • h个头,每个d_k维度
  • 拼接后:h × d_k = d_model(恢复原维度)

计算流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
输入:X ∈ R^(n×512)  (假设d_model=512, h=8

1. 每个头独立计算(d_k=64):
head_1 = Attention(XW_Q^1, XW_K^1, XW_V^1) ∈ R^(n×64)
head_2 = Attention(XW_Q^2, XW_K^2, XW_V^2) ∈ R^(n×64)
...
head_8 = Attention(XW_Q^8, XW_K^8, XW_V^8) ∈ R^(n×64)

2. 拼接:
Concat = [head_1 | head_2 | ... | head_8] ∈ R^(n×512)

3. 输出投影:
Output = Concat W_O ∈ R^(n×512)

参数量计算(必考!)

单头参数量:

1
2
3
4
W_Q: d_model × d_k
W_K: d_model × d_k
W_V: d_model × d_v
每个头:3 × d_model × d_k

多头参数量:

1
2
3
4
5
6
7
8
9
10
h个头:h × 3 × d_model × d_k
W_O: d_model × d_model

由于d_k = d_model / h
总参数 = h × 3 × d_model × (d_model/h) + d_model²
= 3d_model² + d_model²
= 4d_model²

例:d_model=512
参数量 = 4 × 512² = 1,048,5761M参数

关键结论:多头的参数量 = 4d_model²,与头数h无关!

并行计算

实现技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 不要真的分成h个头分别计算(慢)
# 而是用reshape和transpose巧妙并行

class MultiHeadAttention(nn.Module):
def __init__(self, d_model, h):
self.h = h
self.d_k = d_model // h
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)

def forward(self, x):
# x: (batch, n, d_model)
batch_size, n, d_model = x.shape

# 线性投影
Q = self.W_Q(x) # (batch, n, d_model)
K = self.W_K(x)
V = self.W_V(x)

# 分头:reshape成(batch, h, n, d_k)
Q = Q.view(batch_size, n, self.h, self.d_k).transpose(1, 2)
K = K.view(batch_size, n, self.h, self.d_k).transpose(1, 2)
V = V.view(batch_size, n, self.h, self.d_k).transpose(1, 2)
# 现在形状:(batch, h, n, d_k)

# 计算attention(h个头并行)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
attn = F.softmax(scores, dim=-1)
output = torch.matmul(attn, V) # (batch, h, n, d_k)

# 拼接头
output = output.transpose(1, 2).contiguous() # (batch, n, h, d_k)
output = output.view(batch_size, n, d_model) # (batch, n, d_model)

# 输出投影
return self.W_O(output)

常见陷阱题 ⚠️

题目1:Self-Attention的时间复杂度是?

  • A. O(nd)
  • B. O(n²)
  • C. O(n²d) ✅
  • D. O(nd²)

💡 解析:瓶颈在QK^T(n×d乘d×n)和AV(n×n乘n×d),都是O(n²d)。


题目2:为什么Attention要除以√d_k?

  • A. 加速计算
  • B. 防止softmax饱和 ✅
  • C. 减少参数
  • D. 提高精度

💡 解析:不缩放的话,d_k大时方差大,softmax进入饱和区梯度小。


题目3:Multi-Head Attention中,8个头的参数量是单头的几倍?

  • A. 1倍 ✅
  • B. 2倍
  • C. 4倍
  • D. 8倍

💡 解析:总参数量=4d_model²,与头数无关!每个头维度变小了。


题目4:Transformer处理长度1024的序列,注意力矩阵大小(单头)?

  • A. 1024×d_model
  • B. 1024×1024 ✅
  • C. d_model×d_model
  • D. 1024×d_k

💡 解析:注意力权重矩阵A是n×n,即1024×1024。

记忆技巧 🧠:

  • QKV = “请开门” → Query请求,Key开锁,Value门后的东西
  • 除以√d_k = “防止爆炸” → 控制方差,防止softmax饱和
  • 复杂度O(n²d) → “序列长度的平方很贵”
  • 多头参数4d² → 与头数无关,只看总维度

3. 位置编码(Positional Encoding)⭐⭐⭐⭐⭐

为什么需要位置编码?

问题:Self-Attention是置换不变的

1
2
3
4
5
句子1"猫吃鱼"
句子2"鱼吃猫"

如果没有位置信息,Self-Attention会给出相同的输出!
因为它只看词之间的关系,不管顺序。

必须注入位置信息,否则模型不知道词的顺序。

Sinusoidal位置编码(原始Transformer)⭐⭐⭐⭐⭐

公式:

1
2
3
4
5
6
7
8
PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

其中:
- pos:位置(0, 1, 2, ...)
- i:维度索引(0到d_model/2-1
- 2i:偶数维度用sin
- 2i+1:奇数维度用cos

直观理解:

  • 每个位置编码成一个d_model维向量
  • 不同维度用不同频率的sin/cos波
  • 就像给每个位置一个独特的”指纹”

计算示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
假设d_model=4,计算前3个位置的编码

pos=0:
PE(0,0) = sin(0/10000^(0/4)) = sin(0) = 0
PE(0,1) = cos(0/10000^(0/4)) = cos(0) = 1
PE(0,2) = sin(0/10000^(2/4)) = sin(0) = 0
PE(0,3) = cos(0/10000^(2/4)) = cos(0) = 1
[0, 1, 0, 1]

pos=1:
PE(1,0) = sin(1/10000^0) = sin(1) ≈ 0.841
PE(1,1) = cos(1/10000^0) = cos(1) ≈ 0.540
PE(1,2) = sin(1/10000^0.5) = sin(1/100) ≈ 0.010
PE(1,3) = cos(1/10000^0.5) = cos(1/100) ≈ 1.000
[0.841, 0.540, 0.010, 1.000]

pos=2:
[0.909, -0.416, 0.020, 1.000]

波长的意义:

1
2
3
4
5
6
维度0(i=0):波长 = 2π
维度2(i=1):波长 = 2π × 100 ≈ 628
维度4(i=2):波长 = 2π × 10000 ≈ 62,832

低维度:高频率,能区分相邻位置
高维度:低频率,能表示长距离关系

为什么用sin和cos?

1. 周期性和平滑性

1
2
sincos是连续函数,相邻位置的编码相似
位置0和位置1的编码不会突变

2. 相对位置可以线性表示

1
2
3
4
5
6
7
数学性质:
sin(α+β) = sin(α)cos(β) + cos(α)sin(β)
cos(α+β) = cos(α)cos(β) - sin(α)sin(β)

这意味着:
PE(pos+k) 可以表示为 PE(pos) 的线性组合
→ 模型可以学习到相对位置关系!

3. 可以外推到更长序列

1
2
训练时序列长度512,测试时1024
sin/cos可以直接计算,不需要重新训练

可学习位置编码(Learned Positional Embedding)⭐⭐⭐⭐

实现:

1
2
3
4
5
6
7
8
9
10
class LearnedPositionalEncoding(nn.Module):
def __init__(self, max_len, d_model):
super().__init__()
# 把位置编码当作参数学习
self.pe = nn.Parameter(torch.randn(max_len, d_model))

def forward(self, x):
# x: (batch, seq_len, d_model)
seq_len = x.size(1)
return x + self.pe[:seq_len, :]

特点:

1
2
3
4
5
6
7
8
9
优点:
✅ 灵活,模型自己学习最优编码
✅ 可能比固定编码效果好
✅ 实现简单

缺点:
❌ 有最大长度限制(max_len)
❌ 超过max_len无法外推
❌ 增加参数量(max_len × d_model)

参数量:

1
2
3
4
BERT: max_len=512, d_model=768
位置编码参数 = 512 × 768 = 393,216 ≈ 0.4M

相比总参数110M,占比很小

BERT使用可学习位置编码的原因:

  1. BERT是双向的,相对位置关系复杂
  2. 最大长度512固定,不需要外推
  3. 实验表明效果略好于sin/cos

相对位置编码(Relative Positional Encoding)⭐⭐⭐

思想: 不编码绝对位置,而是编码相对距离

实现方式:

1
2
3
4
5
6
7
8
9
10
11
在计算Attention时加入相对位置偏置

Attention(Q,K,V) = softmax((QK^T + R) / √d_k) V

其中R[i,j]表示位置i和j的相对位置关系

例:
R[0,0] = bias_0 (距离0
R[0,1] = bias_1 (距离1
R[0,2] = bias_2 (距离2
...

优点:

  • 更符合直觉(相对位置比绝对位置重要)
  • 泛化能力强(可以外推到更长序列)
  • Transformer-XL、T5使用

旋转位置编码(RoPE - Rotary Position Embedding)⭐⭐⭐⭐⭐

最新进展,LLaMA/GPT-NeoX使用

核心思想: 通过旋转矩阵注入位置信息

1
2
3
4
5
Q_m = R_m Q  (第m个位置的Query)
K_n = R_n K (第n个位置的Key)

Q_m · K_n^T = Q^T R_m^T R_n K
= Q^T R_(m-n) K (只依赖相对位置m-n

优点:

  • ✅ 自然编码相对位置
  • ✅ 可以外推到任意长度
  • ✅ 计算效率高
  • ✅ 大模型标配(LLaMA, GPT-4等)

位置编码对比表

类型 代表模型 可外推 参数量 相对位置
Sinusoidal 原始Transformer ✅ 是 0 间接
Learned BERT ❌ 否 max_len×d 隐式
Relative T5, Transformer-XL ✅ 是 少量 ✅ 显式
RoPE LLaMA, GPT-NeoX ✅ 是 0 ✅ 显式

选择建议:

  • 固定长度任务(如BERT分类):Learned
  • 生成任务(如GPT):RoPE(最新最好)
  • 经典实现:Sinusoidal(简单有效)

4. Layer Normalization ⭐⭐⭐⭐⭐

为什么Transformer用LayerNorm而非BatchNorm?

Batch Normalization:

1
2
3
4
5
6
7
8
batch维度归一化
x_norm[b,i,d] = (x[b,i,d] - μ_d) / σ_d
其中 μ_d, σ_d 是第d维在整个batch上的均值和方差

问题:
❌ 对batch size敏感(小batch统计不准)
❌ RNN/Transformer中序列长度不同,batch难对齐
❌ 训练和测试行为不一致(测试用全局统计)

Layer Normalization:

1
2
3
4
5
6
7
8
9
对feature维度归一化
x_norm[b,i,d] = (x[b,i,d] - μ_i) / σ_i
其中 μ_i, σ_i 是第i个token在所有维度上的均值和方差

优点:
✅ 对batch size不敏感
✅ 每个样本独立归一化
✅ 训练和测试完全一致
✅ 适合序列模型

数学公式:

1
2
3
4
5
6
7
8
μ = (1/d_model) Σ x_i
σ² = (1/d_model) Σ (x_i - μ)²

LN(x) = γ * (x - μ) / √(σ² + ε) + β

其中:
- γ, β: 可学习参数(缩放和平移)
- ε: 数值稳定项(如1e-5)

计算示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输入:x = [1, 2, 3, 4](某个token的特征向量)

步骤1:计算均值
μ = (1+2+3+4)/4 = 2.5

步骤2:计算方差
σ² = [(1-2.5)² + (2-2.5)² + (3-2.5)² + (4-2.5)²] / 4
= [2.25 + 0.25 + 0.25 + 2.25] / 4
= 1.25

步骤3:标准化
x_norm = (x - 2.5) / √1.25
= [-1.34, -0.45, 0.45, 1.34]

步骤4:缩放和平移(假设γ=2, β=1)
output = 2 * x_norm + 1
= [-1.68, 0.1, 1.9, 3.68]

为什么需要γ和β?

1
2
3
4
5
6
标准化后均值0,方差1
但网络可能需要其他分布
γ和β允许网络学习最优的均值和方差

极端情况:如果γ=σ, β=μ
输出 = σ*(x-μ)/σ + μ = x(恢复原输入)

PyTorch实现:

1
2
3
4
5
6
7
8
9
10
11
12
class LayerNorm(nn.Module):
def __init__(self, d_model, eps=1e-5):
super().__init__()
self.gamma = nn.Parameter(torch.ones(d_model))
self.beta = nn.Parameter(torch.zeros(d_model))
self.eps = eps

def forward(self, x):
# x: (batch, seq_len, d_model)
mean = x.mean(dim=-1, keepdim=True) # (batch, seq_len, 1)
std = x.std(dim=-1, keepdim=True)
return self.gamma * (x - mean) / (std + self.eps) + self.beta

Pre-LN vs Post-LN ⭐⭐⭐

Post-LN(原始Transformer):

1
2
3
4
x = x + Attention(LN(x))
x = x + FFN(LN(x))

归一化在残差之后

Pre-LN(GPT-2后流行):

1
2
3
4
5
6
7
8
x = x + Attention(LN(x))
x = x + FFN(LN(x))

改为:
x = x + LN(Attention(x))
x = x + LN(FFN(x))

归一化在残差之前

对比:

特性 Post-LN Pre-LN
训练稳定性 需要warm-up 更稳定
学习率 需要小心调整 鲁棒
深层网络 困难 容易
性能 收敛慢但最终可能更好 快速收敛

现代大模型选择:Pre-LN(GPT-3、LLaMA等)

5. Feed-Forward Network (FFN) ⭐⭐⭐⭐

结构:两层全连接网络

1
2
3
4
5
FFN(x) = max(0, xW_1 + b_1)W_2 + b_2
= ReLU(xW_1 + b_1)W_2 + b_2

或者用GELU:
FFN(x) = GELU(xW_1 + b_1)W_2 + b_2

维度变化:

1
2
3
4
5
6
7
8
输入:xR^d_model  (如512
中间层:h ∈ R^d_ff (通常4×d_model,如2048
输出:yR^d_model (512

d_model → d_ff → d_model
5122048512

先扩张4倍,再压缩回来

为什么要扩张4倍?

  1. 增加非线性表达能力

    • 中间层维度高,可以表示更复杂的函数
    • 类似”思考空间”:先展开思考,再总结
  2. 参数量平衡

    1
    2
    3
    4
    5
    Attention参数:4d_model²
    FFN参数:2 × d_model × d_ff = 2 × d_model × 4d_model = 8d_model²

    FFN参数量是Attention的2倍!
    实际上FFN占了Transformer大部分参数
  3. 实验验证

    • 原论文尝试了不同的倍数
    • 4倍是性能和效率的最佳平衡

参数量计算(必考!):

1
2
3
4
5
6
7
8
9
10
假设d_model=512, d_ff=2048

W_1: 512 × 2048 = 1,048,576
b_1: 2048
W_2: 2048 × 512 = 1,048,576
b_2: 512

总计:2,097,152 + 2560 2.1M参数

比较Attention(1M):FFN是Attention的约2倍

逐位置应用(Position-wise):

1
2
3
4
5
FFN对每个位置独立应用
位置1和位置2用相同的W_1, W_2
但不共享激活值

就像:每个词独立过一遍相同的两层网络

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)

def forward(self, x):
# x: (batch, seq_len, d_model)
x = self.linear1(x) # (batch, seq_len, d_ff)
x = F.gelu(x) # 激活
x = self.dropout(x) # Dropout
x = self.linear2(x) # (batch, seq_len, d_model)
return x

常见陷阱题 ⚠️

题目1:Sinusoidal位置编码的参数量是?

  • A. max_len × d_model
  • B. d_model²
  • C. 0 ✅
  • D. 与序列长度相关

💡 解析:Sin/Cos编码是固定公式计算的,不是参数,所以参数量为0。


题目2:LayerNorm在哪个维度归一化?

  • A. Batch维度
  • B. 序列维度
  • C. Feature维度 ✅
  • D. 所有维度

💡 解析:LayerNorm对每个样本的feature维度归一化。


题目3:FFN的中间层维度通常是d_model的几倍?

  • A. 2倍
  • B. 4倍 ✅
  • C. 8倍
  • D. 16倍

💡 解析:标准Transformer使用4倍扩张(如512→2048→512)。


题目4:Transformer一个encoder层的参数量(d_model=512)?

  • A. 约1M
  • B. 约3M ✅
  • C. 约10M
  • D. 约30M

💡 解析:Attention 1M + FFN 2M ≈ 3M(忽略LayerNorm的少量参数)。

记忆技巧 🧠:

  • 位置编码 = “给位置发身份证” → 让模型知道顺序
  • Sin/Cos = “波浪指纹” → 不同频率的波组合成独特编码
  • LayerNorm = “自己跟自己比” → 每个token内部归一化
  • FFN = “先胖后瘦” → 512→2048→512,扩张4倍
  • FFN参数是Attention的2倍 → 占大头

四、KV Cache优化技术(大模型推理核心)⭐⭐⭐⭐⭐

为什么KV Cache重要?
大模型推理的主要瓶颈!GPT-4、LLaMA等生成1000个token,如果没有KV Cache,计算量是O(n²),有了只是O(n)。内存占用可达几GB!

什么是KV Cache?

自回归生成过程

逐token生成:

1
2
3
4
5
6
输入:"今天天气"
目标:生成"很好"

步骤1:输入"今天天气" → 生成"很"
步骤2:输入"今天天气很" → 生成"好"
步骤3:输入"今天天气很好" → 生成<EOS>

没有KV Cache的计算:

1
2
3
4
5
6
7
步骤1:计算4token的Attention("今天天气" + "很"
步骤2:计算5token的Attention("今天天气很" + "好"
↑ 前4token的K, V重复计算了!
步骤3:计算6token的Attention
↑ 前5token的K, V又重复计算了!

总计算量:4 + 5 + 6 + ... ≈ O(n²) (n是生成长度)

有KV Cache的计算:

1
2
3
4
5
步骤1:计算4tokenK, V,缓存起来
步骤2:只计算新token"好"K, V,直接用缓存
步骤3:只计算新tokenK, V

总计算量:4 + 1 + 1 + ...O(n) ✅ 线性!

KV Cache的数学原理

Attention计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
假设已经生成了t个token,现在生成第t+1个token

不用缓存:
Q_{1:t+1} = X_{1:t+1} W_Q
K_{1:t+1} = X_{1:t+1} W_K ← 重复计算前t个
V_{1:t+1} = X_{1:t+1} W_V ← 重复计算前t个

用缓存:
K_{1:t} 已缓存
V_{1:t} 已缓存
只需计算:
K_{t+1} = X_{t+1} W_K
V_{t+1} = X_{t+1} W_V

拼接:
K_{1:t+1} = [K_cache; K_{t+1}]
V_{1:t+1} = [V_cache; V_{t+1}]

为什么只缓存K和V,不缓存Q?

1
2
3
4
5
6
7
8
9
10
11
12
Q(Query):当前token去查询历史
K(Key):历史token被查询
V(Value):历史token的内容

每次生成新token时:
- Q是新的(新token的查询)
- K, V是历史的(可以复用)

形象理解:
Q是"今天的问题"
K, V是"历史资料"
问题天天变,资料可以存档

内存占用计算(重要!)

单个token的KV Cache大小:

1
2
3
4
5
6
7
8
9
10
11
12
每层每个头:
K: d_k维
V: d_v维
通常d_k = d_v = d_model / n_heads

每层所有头:
K: n_heads × d_k = d_model
V: n_heads × d_v = d_model
总计:2 × d_model

所有层:
2 × n_layers × d_model

具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
LLaMA-7B参数:
- n_layers = 32
- d_model = 4096
- 数据类型:FP16(2字节)

单个token的KV Cache:
2 × 32 × 4096 × 2字节 = 524,288字节 = 512KB

生成1024个token:
512KB × 1024 = 524MB

批量推理(batch_size=16):
524MB × 16 = 8.4GB ☠️ 巨大!

LLaMA-70B:

1
2
3
4
5
6
n_layers = 80
d_model = 8192

单token:2 × 80 × 8192 × 2 = 2.5MB
1024 token:2.5GB
batch=1640GB ☠️☠️☠️

这就是为什么需要KV Cache压缩!

KV Cache压缩方法

1. Multi-Query Attention (MQA) ⭐⭐⭐⭐⭐

核心思想:所有头共享同一个K和V

标准Multi-Head Attention:

1
2
3
4
5
6
7
1:Q_1, K_1, V_1
2:Q_2, K_2, V_2
...
头h:Q_h, K_h, V_h

每个头有独立的K, V
KV Cache大小:h × (K + V)

Multi-Query Attention:

1
2
3
4
5
6
7
1Q_1
2Q_2 → 共享 K, V
...
hQ_h

只有1K, V,所有头共享
KV Cache大小:1 × (K + V) ✅ 减少h倍!

内存节省:

1
2
3
4
5
6
假设h=32个头

标准MHA:KV Cache = 32 × (K + V)
MQA: KV Cache = 1 × (K + V)

节省比例:32/1 = 32倍! 🚀

代价:性能略微下降

1
2
3
4
5
6
实验表明:
- 精度下降:1-2%
- 速度提升:20-30%
- 内存节省:90%+

权衡:可接受的精度损失换取巨大内存节省

使用案例:

  • PaLM(Google,540B参数)
  • Falcon(开源,40B参数)
  • StarCoder(代码模型)

2. Grouped-Query Attention (GQA) ⭐⭐⭐⭐⭐

核心思想:介于MHA和MQA之间

分组共享K, V:

1
2
3
4
5
6
假设8个头,分成2组

组1:Q_1, Q_2, Q_3, Q_4 → 共享 K_1, V_1
组2:Q_5, Q_6, Q_7, Q_8 → 共享 K_2, V_2

KV Cache:2组 × (K + V)

灵活性:

1
2
3
4
n_heads = 32, n_kv_groups = 8
每组 32/8 = 4个Query头共享1组KV

KV Cache减少:32/8 = 4

性能平衡:

1
2
3
MHA:最好精度,最大内存
GQA:中等精度,中等内存 ← 最佳平衡
MQA:略差精度,最小内存

LLaMA 2使用GQA:

1
2
3
4
5
6
7
8
9
LLaMA 2-7B:
- n_heads = 32
- n_kv_groups = 8
- KV Cache减少4倍

LLaMA 2-70B:
- n_heads = 64
- n_kv_groups = 8
- KV Cache减少8倍

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class GroupedQueryAttention(nn.Module):
def __init__(self, d_model, n_heads, n_kv_groups):
super().__init__()
self.n_heads = n_heads
self.n_kv_groups = n_kv_groups
self.d_k = d_model // n_heads

# Query:每个头独立
self.W_Q = nn.Linear(d_model, d_model)

# Key, Value:只有n_kv_groups组
self.W_K = nn.Linear(d_model, self.d_k * n_kv_groups)
self.W_V = nn.Linear(d_model, self.d_k * n_kv_groups)

self.W_O = nn.Linear(d_model, d_model)

def forward(self, x, kv_cache=None):
batch_size, seq_len, d_model = x.shape

# Q: (batch, n_heads, seq_len, d_k)
Q = self.W_Q(x).view(batch_size, seq_len, self.n_heads, self.d_k)
Q = Q.transpose(1, 2)

# K, V: (batch, n_kv_groups, seq_len, d_k)
K = self.W_K(x).view(batch_size, seq_len, self.n_kv_groups, self.d_k)
V = self.W_V(x).view(batch_size, seq_len, self.n_kv_groups, self.d_k)
K = K.transpose(1, 2)
V = V.transpose(1, 2)

# 如果有缓存,拼接
if kv_cache is not None:
K_cache, V_cache = kv_cache
K = torch.cat([K_cache, K], dim=2)
V = torch.cat([V_cache, V], dim=2)

# 复制K, V给每组的多个头
# (batch, n_kv_groups, seq_len, d_k) → (batch, n_heads, seq_len, d_k)
repeats = self.n_heads // self.n_kv_groups
K = K.repeat_interleave(repeats, dim=1)
V = V.repeat_interleave(repeats, dim=1)

# 标准Attention
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
attn = F.softmax(scores, dim=-1)
output = torch.matmul(attn, V)

# 拼接并输出
output = output.transpose(1, 2).contiguous()
output = output.view(batch_size, seq_len, d_model)
output = self.W_O(output)

return output, (K, V) # 返回新的缓存

3. 量化压缩(Quantization)⭐⭐⭐⭐

FP16 → INT8:

1
2
3
4
5
6
7
8
FP16:2字节/数值
INT81字节/数值

内存减少:50%

例:LLaMA-7B,1024 token
FP16:524MB
INT8262MB ✅ 节省一半

INT8量化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def quantize_kv_cache(kv_fp16):
# 找到最大值
abs_max = torch.abs(kv_fp16).max()

# 计算scale
scale = abs_max / 127

# 量化
kv_int8 = torch.round(kv_fp16 / scale).clamp(-128, 127).to(torch.int8)

return kv_int8, scale

def dequantize_kv_cache(kv_int8, scale):
return kv_int8.to(torch.float16) * scale

更激进:INT4

1
2
3
4
INT4:0.5字节/数值(2个数值打包在1字节)
内存减少:75%

但精度损失更大,需要精细校准

精度影响:

1
2
3
FP16:基准
INT8:困惑度(perplexity)增加<2% ✅ 可接受
INT4:困惑度增加5-10% ⚠️ 需要仔细验证

4. 滑动窗口 (Sliding Window Attention) ⭐⭐⭐⭐

核心思想:只保留最近N个token的KV

实现:

1
2
3
4
5
6
7
窗口大小 W = 2048

生成到第3000token时:
只保留token [1000:3000]的KV Cache
丢弃token [0:1000]

内存上限:固定W × KV_size

适用场景:

1
2
3
4
✅ 长文本生成(小说、报告)
✅ 对话系统(只关注近期上下文)
❌ 代码生成(可能需要引用很早的定义)
❌ 数学推理(需要全部信息)

Mistral使用:

1
2
3
4
Mistral-7B:
- 窗口大小:4096
- 超出部分丢弃
- 内存固定,支持无限长度生成

变体:Attention Sink

1
2
3
4
5
保留最近W个 + 最前面K个
例:保留[0:10] + [2990:3000]

原因:实验发现最前面几个token(如BOS)
对注意力稳定性很重要

5. PagedAttention (vLLM) ⭐⭐⭐⭐⭐

核心思想:像操作系统的虚拟内存一样管理KV Cache

传统KV Cache管理:

1
2
3
4
5
6
7
预分配连续内存
例:为1024 token预留512MB

问题:
❌ 实际只生成500 token → 浪费256MB
❌ 生成1100 token → 内存不够,失败
❌ 碎片化严重

PagedAttention:

1
2
3
4
5
6
7
8
9
10
11
将KV Cache分成固定大小的"页"(如64 token/页)

按需分配:
- 初始只分配1页
- 生成到65个token时,再分配1页
- 页可以非连续存储(通过页表索引)

优点:
✅ 几乎0浪费
✅ 动态扩展
✅ 多请求共享内存(如共同前缀)

内存利用率:

1
2
3
4
传统方法:60-70%(大量碎片和预分配浪费)
vLLM: 90-95% 🚀

相同硬件,吞吐量提升2-3倍!

共享前缀优化:

1
2
3
4
5
请求1"请用Python实现快速排序"
请求2"请用Python实现归并排序"

前缀"请用Python实现"的KV Cache可以共享
只需要为差异部分分配新页

KV Cache压缩技术对比

方法 内存节省 精度影响 实现难度 代表模型
MQA ~95% 1-2%↓ 简单 PaLM
GQA 50-90% <1%↓ 简单 LLaMA 2 ✅
INT8量化 50% <1%↓ 中等 通用
INT4量化 75% 2-5%↓ 困难 GPTQ
滑动窗口 固定上限 长距离↓ 简单 Mistral
PagedAttention ~30% 0 复杂 vLLM ✅

组合使用效果最佳:

1
2
3
4
5
6
GQA + INT8 + PagedAttention
内存节省:50% × 50% × 70% = 82.5% 🚀🚀🚀

LLaMA-70B推理内存:
原始:40GB(batch=16, 1024 token)
优化后:7GB ✅ 从A100降到RTX 4090可跑

常见陷阱题 ⚠️

题目1:KV Cache主要缓存什么?

  • A. Query矩阵
  • B. Key和Value矩阵 ✅
  • C. 所有Attention矩阵
  • D. 输入embedding

💡 解析:只缓存历史token的K和V,Q是当前token的,每次重算。


题目2:MQA相比标准MHA,KV Cache减少多少倍(假设32头)?

  • A. 2倍
  • B. 8倍
  • C. 16倍
  • D. 32倍 ✅

💡 解析:MQA只有1组KV,标准MHA有32组,减少32倍。


题目3:LLaMA-7B生成1024个token(FP16),KV Cache约多大?

  • A. ~50MB
  • B. ~500MB ✅
  • C. ~5GB
  • D. ~50GB

💡 解析:512KB/token × 1024 ≈ 512MB。


题目4:以下哪个不是KV Cache压缩方法?

  • A. Multi-Query Attention
  • B. Layer Normalization ✅
  • C. INT8量化
  • D. 滑动窗口

💡 解析:LayerNorm是归一化方法,不是KV Cache压缩技术。

记忆技巧 🧠:

  • KV Cache = “历史资料库” → 存档历史,避免重复计算
  • MQA = “大家共用一本字典” → 所有头共享KV
  • GQA = “小组共享” → 几个头共享一组KV(LLaMA 2标配)
  • 内存杀手 → 70B模型生成1024 token能吃40GB
  • 三板斧:GQA + 量化 + PagedAttention → 省内存80%+

五、量化技术(模型压缩核心)⭐⭐⭐⭐⭐

为什么需要量化?
GPT-3(175B)用FP32存储需要700GB,量化到INT8只需175GB,推理速度提升2-4倍!华为AI芯片(昇腾)特别重视量化。

量化基础概念

什么是量化?

1
2
3
4
5
6
7
将高精度数值(FP32/FP16)映射到低精度(INT8/INT4
用更少的bit表示,换取速度和内存

形象比喻:
FP32:用尺子精确测量到0.001mm
INT8:用手指目测到1cm
精度降低,但快得多

为什么量化能加速?

  1. 内存带宽 ← 主要瓶颈

    1
    2
    3
    INT8数据量 = FP32的1/4
    从内存读取快4
    GPU计算常受内存带宽限制
  2. 计算速度

    1
    2
    3
    INT8矩阵乘法比FP32快2-4
    现代GPU/NPU有专门的INT8指令
    昇腾910INT8算力 = FP16的2
  3. 模型大小

    1
    2
    3
    4
    FP32: 4字节
    FP16: 2字节
    INT8: 1字节
    INT4: 0.5字节

1. 数值表示

FP32(单精度浮点)

1
2
3
4
5
6
32 bit = 1 sign + 8 exponent + 23 mantissa

范围:±3.4×10³⁸
精度:约7位有效数字

例:123.456 可精确表示

FP16(半精度浮点)

1
2
3
4
5
6
7
8
9
16 bit = 1 sign + 5 exponent + 10 mantissa

范围:±65,504
精度:约3位有效数字

例:123.5(123.456会四舍五入)

优点:内存减半,速度快
缺点:容易溢出,精度低

BF16(Brain Float 16)

1
2
3
4
5
6
7
8
9
10
11
16 bit = 1 sign + 8 exponent + 7 mantissa

范围:±3.4×10³⁸(和FP32一样)
精度:约2位有效数字

优点:
- 范围大(不易溢出)
- FP32截断即可,转换简单
- Google TPU标配

缺点:精度比FP16略低

INT8(8位整数)

1
2
3
4
有符号:-128127
无符号:0255

需要配合scale和zero_point使用

INT4(4位整数)

1
2
3
4
有符号:-8 到 7
无符号:0 到 15

极度压缩,需要精细校准

2. 量化方法

对称量化(Symmetric Quantization)⭐⭐⭐⭐

公式:

1
2
3
4
scale = max(|x|) / 127

量化:x_int8 = clip(round(x / scale), -128, 127)
反量化:x_fp = x_int8 * scale

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
原始权重:x = [-2.5, 0.3, 1.8, -1.2]

步骤1:找最大绝对值
max(|x|) = 2.5

步骤2:计算scale
scale = 2.5 / 127 ≈ 0.0197

步骤3:量化
-2.5 / 0.0197 = -127-127
0.3 / 0.0197 = 15 → 15
1.8 / 0.0197 = 91 → 91
-1.2 / 0.0197 = -61-61

量化后:[-127, 15, 91, -61]

步骤4:反量化验证
-127 * 0.0197 = -2.50 ✅
15 * 0.0197 = 0.30 ✅
91 * 0.0197 = 1.79 ≈1.8
-61 * 0.0197 = -1.20 ✅

特点:

  • 零点映射到0(对称)
  • 只需要存储scale
  • 适合权重(分布对称)

非对称量化(Asymmetric Quantization)⭐⭐⭐⭐

公式:

1
2
3
4
5
scale = (max(x) - min(x)) / 255
zero_point = round(-min(x) / scale)

量化:x_int8 = clip(round(x / scale + zero_point), 0, 255)
反量化:x_fp = (x_int8 - zero_point) * scale

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
激活值:x = [0.1, 0.5, 0.8, 0.3](ReLU后全是正数)

步骤1:计算范围
min(x) = 0.1
max(x) = 0.8

步骤2:计算scale和zero_point
scale = (0.8 - 0.1) / 2550.00275
zero_point = round(-0.1 / 0.00275) = round(-36.4) = -36

步骤3:量化
0.1 / 0.00275 + (-36) = 00
0.5 / 0.00275 + (-36) = 145145
0.8 / 0.00275 + (-36) = 255255
0.3 / 0.00275 + (-36) = 7373

量化后:[0, 145, 255, 73]

特点:

  • 零点可以不是0
  • 需要存储scale和zero_point
  • 适合激活值(分布不对称)

逐通道量化 (Per-Channel) vs 逐张量量化 (Per-Tensor) ⭐⭐⭐⭐⭐

逐张量量化:

1
2
3
4
5
6
7
8
9
10
11
12
整个权重矩阵W (shape: [out, in])用一个scale

优点:简单,速度快
缺点:如果不同通道范围差异大,精度损失大

例:
通道1权重范围:[-0.1, 0.1]
通道2权重范围:[-5.0, 5.0]

统一scale = 5.0/127 = 0.039
通道10.05量化为 round(0.05/0.039) = 1
反量化:1 * 0.039 = 0.039(误差22%!)

逐通道量化:

1
2
3
4
5
6
7
8
每个输出通道用独立的scale

W = [[w_00, w_01, ...], ← 通道0,scale_0
[w_10, w_11, ...], ← 通道1,scale_1
...]

优点:精度高(误差<1%)
缺点:需要存储多个scale

实验对比:

1
2
3
4
5
6
ResNet-50在ImageNet上:

逐张量INT8:Top-1准确率 75.1% → 72.3%(-2.8%)
逐通道INT8:Top-1准确率 75.1% → 74.8%(-0.3%)✅

逐通道是工业界标准!

3. 量化训练方法

PTQ (Post-Training Quantization) ⭐⭐⭐⭐⭐

流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 训练好FP32模型
2. 用少量校准数据(如100-1000样本)
3. 统计激活值范围
4. 计算量化参数(scale, zero_point)
5. 直接量化权重和激活

优点:
✅ 无需重新训练
✅ 几分钟完成
✅ 不需要训练数据

缺点:
❌ 精度损失较大(INT8约1-3%,INT4约5-10%)
❌ 对敏感层效果差

代码示例(PyTorch):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch.quantization as quantization

# 1. 加载模型
model = torchvision.models.resnet50(pretrained=True)
model.eval()

# 2. 配置量化
model.qconfig = quantization.get_default_qconfig('fbgemm') # x86
# 或 'qnnpack' for ARM

# 3. 准备量化(插入观察器)
quantization.prepare(model, inplace=True)

# 4. 校准(统计激活值范围)
with torch.no_grad():
for images, _ in calibration_loader: # 100-1000张图
model(images)

# 5. 转换为量化模型
quantization.convert(model, inplace=True)

# 6. 推理
output = model(input) # 自动用INT8计算

QAT (Quantization-Aware Training) ⭐⭐⭐⭐⭐

核心思想:训练时模拟量化

Fake Quantization:

1
2
3
4
5
6
7
8
9
10
11
12
前向传播:
1. 权重 w_fp32
2. 量化:w_int8 = quantize(w_fp32)
3. 反量化:w_fake = dequantize(w_int8)
4. 用w_fake计算(仍是FP32,但模拟了INT8的误差)

反向传播:
1. 梯度仍用FP32
2. 更新w_fp32
3. STE (Straight-Through Estimator)跳过round的不可导

循环训练多个epoch

为什么有效?

1
2
3
4
5
模型学习适应量化误差
例:某个权重0.123

PTQ:直接量化,可能导致输出偏差
QAT:训练中已经适应了这个偏差,调整了其他权重补偿

精度对比:

1
2
3
4
5
6
7
8
9
ResNet-50 ImageNet:

FP3276.1%
PTQ INT8: 75.0%(-1.1%)
QAT INT8: 75.9%(-0.2%)✅

INT4
PTQ70.2%(-5.9%)☠️
QAT74.8%(-1.3%)✅

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 准备模型
model = Model()
model.qconfig = quantization.get_default_qat_qconfig('fbgemm')
quantization.prepare_qat(model, inplace=True)

# 2. 训练(带fake quantization)
model.train()
for epoch in range(num_epochs):
for images, labels in train_loader:
optimizer.zero_grad()
output = model(images) # 模拟量化
loss = criterion(output, labels)
loss.backward()
optimizer.step()

# 3. 转换为真正的量化模型
model.eval()
quantization.convert(model, inplace=True)

4. LLM专用量化技术

GPTQ (GPT Quantization) ⭐⭐⭐⭐⭐

核心思想:逐层量化,最小化重建误差

算法流程:

1
2
3
4
5
6
7
对每一层:
1. 固定其他层,只量化当前层
2. 用少量数据计算激活值
3. 找到最优量化参数,使得:
||W_fp32 @ X - W_int4 @ X||最小

4. 逐层进行,最终所有层都量化

为什么逐层量化?

1
2
一次性量化所有层:误差累积,崩溃
逐层量化:每层独立优化,误差可控

效果:

1
2
3
4
5
6
7
LLaMA-7B INT4 (GPTQ):
- 模型大小:28GB → 3.5GB(压缩8倍)
- 困惑度:5.68 → 5.82(+0.14,几乎无损)
- 生成速度:提升3-4倍

LLaMA-65B INT4:
- 模型大小:130GB → 16GB ✅ 单卡可跑!

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from auto_gptq import AutoGPTQForCausalLM

# 量化
model = AutoGPTQForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantize_config={
"bits": 4,
"group_size": 128,
"desc_act": False
}
)
model.quantize(calibration_dataset)
model.save_quantized("llama-2-7b-gptq")

# 加载并推理
model = AutoGPTQForCausalLM.from_quantized("llama-2-7b-gptq")
output = model.generate(input_ids)

AWQ (Activation-aware Weight Quantization) ⭐⭐⭐⭐⭐

核心思想:保护重要权重通道

发现:1%的权重贡献了50%的精度

1
2
3
4
5
6
7
8
9
不是所有权重同等重要!

重要权重通道:
- 激活值大的通道
- 对输出影响大的通道

策略:
- 重要通道:保持FP16或用更高精度INT8
- 不重要通道:INT4甚至INT2

计算重要性:

1
2
3
4
对每个权重通道i:
importance_i = mean(|activation_i|)

激活值越大,越重要

混合精度量化:

1
2
3
4
5
6
Top 5%通道:  FP16
Next 15%通道: INT8
剩余80%通道: INT4

整体压缩率:~4
但保留了关键通道的精度

效果:

1
2
3
4
5
LLaMA-7B:

GPTQ INT4: 困惑度 5.82
AWQ INT4: 困惑度 5.71 ✅ 更好
混合精度AWQ: 困惑度 5.69 ✅ 几乎无损

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from awq import AutoAWQForCausalLM

# 量化
model = AutoAWQForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
model.quantize(
tokenizer,
quant_config={
"w_bit": 4,
"q_group_size": 128,
"version": "GEMM"
}
)
model.save_quantized("llama-2-7b-awq")

# 加载并推理(支持动态量化)
model = AutoAWQForCausalLM.from_quantized("llama-2-7b-awq")
output = model.generate(input_ids)

SmoothQuant ⭐⭐⭐

核心思想:平滑激活值的异常峰值

问题:激活值有outlier

1
2
3
4
5
6
正常激活值:[0.1, 0.2, 0.15, 0.18, ...]
异常值: [0.1, 0.2, 0.15, 5.0, ...] ← outlier

量化时:
scale = 5.0 / 127 = 0.039(被outlier主导)
正常值0.15 / 0.039 = 4(精度损失大)

解决:数学等价变换

1
2
3
4
5
6
7
8
9
10
11
Y = W @ X

引入平滑因子s:
Y = (W @ diag(s)) @ (diag(1/s) @ X)
= W' @ X'

其中:
W' = W @ diag(s) (权重缩放)
X' = diag(1/s) @ X (激活缩放)

选择s使得X'的分布更平滑

效果:

1
2
3
4
5
OPT-175B INT8

W8A8(权重INT8,激活INT8):
- 标准量化:困惑度爆炸(>100
- SmoothQuant:困惑度 10.5(基线10.2)✅

5. 量化技术对比

方法 压缩比 精度损失 训练成本 适用场景
FP16 <0.1% 训练+推理
PTQ INT8 1-3% 快速部署 ✅
QAT INT8 <0.5% 精度要求高
GPTQ INT4 1-2% LLM推理 ✅
AWQ INT4 <1% LLM推理(最优)✅

常见陷阱题 ⚠️

题目1:INT8量化能减少多少存储?

  • A. 25%
  • B. 50%
  • C. 75% ✅
  • D. 90%

💡 解析:FP32 4字节 → INT8 1字节,减少75%(剩余25%)


题目2:PTQ和QAT的主要区别?

  • A. 压缩比不同
  • B. QAT需要重新训练 ✅
  • C. PTQ精度更高
  • D. QAT速度更快

💡 解析:QAT在训练中模拟量化,PTQ训练后直接量化。


题目3:以下哪个适合对称量化?

  • A. ReLU后的激活值
  • B. 模型权重 ✅
  • C. Sigmoid输出
  • D. 输入图像

💡 解析:权重分布通常对称(均值接近0),激活值常为正(不对称)。


题目4:LLaMA-7B用GPTQ INT4量化后约多大?

  • A. 1GB
  • B. 3.5GB ✅
  • C. 7GB
  • D. 14GB

💡 解析:7B参数 × 4bit = 28Gb = 3.5GB

记忆技巧 🧠:

  • 量化 = “降低精度换速度” → 牺牲小精度,换取大速度
  • PTQ快QAT准 → PTQ几分钟,QAT几天但更准
  • GPTQ逐层量,AWQ保重点 → LLM量化两大法宝
  • INT8是甜点 → 4倍压缩,<1%精度损失
  • 逐通道量化是工业标准 → 比逐张量精度高得多

六、优化器对比(训练核心)⭐⭐⭐⭐⭐

为什么优化器重要?
选对优化器,训练速度快10倍,精度高5%!Adam系列占据深度学习半壁江山,AdamW是Transformer标配。

优化器基础

优化目标:

1
2
3
4
5
6
最小化损失函数 L(θ)
通过迭代更新参数 θ

θ_{t+1} = θ_t - update

关键:如何计算update?

梯度下降的挑战:

  1. 学习率难选:太大发散,太小收敛慢
  2. 各维度不同:有的特征更新快,有的慢
  3. 鞍点和局部最优:梯度为0但不是最优
  4. 噪声梯度:mini-batch梯度有噪声

优化器的演化:

1
2
3
SGD → Momentum → AdaGrad → RMSprop → Adam → AdamW

现在这里

1. SGD (Stochastic Gradient Descent) ⭐⭐⭐⭐

基础版本:

1
2
3
4
θ = θ - lr * ∇L(θ)

lr: 学习率(如0.01
∇L: 损失对参数的梯度

直观理解:

1
2
3
4
5
6
站在山坡上:
- 梯度:指向最陡的上坡方向
- 负梯度:指向最陡的下坡方向
- 学习率:每次走多大步

目标:走到山谷最低点

优点:

  • ✅ 简单直观
  • ✅ 收敛稳定(局部最优附近)
  • ✅ 泛化能力强(噪声起正则化作用)
  • ✅ 内存占用小(只需梯度)

缺点:

  • ❌ 学习率难调(手动调整)
  • ❌ 收敛慢(震荡)
  • ❌ 对所有参数用相同学习率(不合理)
  • ❌ 容易卡在鞍点

震荡问题示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假设损失函数等高线是扁椭圆:

↑ y方向梯度大
|
| ←→ x方向梯度小
|
+--------

SGD轨迹:
┌─┐
│ │ ← 垂直方向来回震荡
│ │ 水平方向前进缓慢
└─┘→ 目标

需要走很久才能到达最优点

代码实现:

1
2
3
4
5
6
7
8
class SGD:
def __init__(self, params, lr=0.01):
self.params = params
self.lr = lr

def step(self):
for param in self.params:
param.data -= self.lr * param.grad

2. SGD with Momentum ⭐⭐⭐⭐⭐

公式:

1
2
3
4
5
v_t = β * v_{t-1} + ∇L(θ_t)  # 速度累积
θ_t = θ_{t-1} - lr * v_t # 参数更新

β: 动量系数(通常0.9
v: 速度(梯度的指数移动平均)

直观理解:

1
2
3
4
5
6
7
8
物理类比:小球滚下山坡

- SGD:每次只看当前坡度,走一步停一步
- Momentum:有惯性,累积速度
- 连续下坡:越滚越快
- 遇到小坡:凭惯性冲过去(逃离鞍点)
- 方向一致:加速
- 方向震荡:抵消

数学展开:

1
2
3
4
5
6
7
8
9
10
11
v_t = β*v_{t-1} + g_t
= β*(β*v_{t-2} + g_{t-1}) + g_t
= β²*v_{t-2} + β*g_{t-1} + g_t
= β²*(β*v_{t-3} + g_{t-2}) + β*g_{t-1} + g_t
= ...
= g_t + β*g_{t-1} + β²*g_{t-2} + β³*g_{t-3} + ...

指数衰减加权和:
- 最近的梯度权重最大
- 历史梯度权重指数衰减
- β=0.9时,约10步前的梯度权重只剩35%

解决震荡:

1
2
3
4
5
垂直方向:+1 -1 +1 -1(震荡)
累积:+1 + 0.9*(-1) + 0.9²*(+1) + 0.9³*(-1) ≈ 0(抵消)

水平方向:+1 +1 +1 +1(一致)
累积:+1 + 0.9*(+1) + 0.9²*(+1) + 0.9³*(+1) ≈ 3.4(加速)

优点:

  • ✅ 加速收敛(比SGD快2-10倍)
  • ✅ 减少震荡
  • ✅ 更容易逃离鞍点和局部最优
  • ✅ 对噪声梯度鲁棒

缺点:

  • ❌ 新增超参数β(但0.9通常够用)
  • ❌ 内存翻倍(需要存储v)

β的选择:

1
2
3
4
β = 0.0:退化为SGD
β = 0.9:标准选择(约10步历史)
β = 0.99:更长历史(约100步),适合大batch
β接近1:惯性过大,可能冲过头

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
class MomentumSGD:
def __init__(self, params, lr=0.01, momentum=0.9):
self.params = params
self.lr = lr
self.momentum = momentum
self.velocity = [torch.zeros_like(p) for p in params]

def step(self):
for i, param in enumerate(self.params):
self.velocity[i] = (self.momentum * self.velocity[i] +
param.grad)
param.data -= self.lr * self.velocity[i]

Nesterov Momentum变体:

1
2
3
4
5
v_t = β * v_{t-1} + ∇L(θ_t - β*v_{t-1})  # 提前看一步
θ_t = θ_{t-1} - lr * v_t

优点:更精确,收敛更快
PyTorch中设置nesterov=True

3. AdaGrad (Adaptive Gradient) ⭐⭐⭐

核心思想:不同参数用不同学习率

公式:

1
2
3
4
5
G_t = G_{t-1} + (∇L(θ_t))²  # 累积梯度平方
θ_t = θ_{t-1} - (lr / √(G_t + ε)) * ∇L(θ_t)

G: 梯度平方和(每个参数独立)
ε: 数值稳定项(如1e-8

直观理解:

1
2
3
4
5
6
频繁更新的参数(G大)→ 学习率自动变小
稀疏更新的参数(G小)→ 学习率保持较大

就像"刹车系统":
- 走得多的方向:刹车力度大(学习率降低)
- 走得少的方向:刹车力度小(学习率保持)

示例:

1
2
3
4
5
6
7
8
9
10
11
参数θ₁:每步梯度约1.0
参数θ₂:每步梯度约0.1

第100步:
θ₁: G = 100 * 1.0² = 100
实际学习率 = 0.01 / √100 = 0.001

θ₂: G = 100 * 0.1² = 1
实际学习率 = 0.01 / √1 = 0.01

θ₂的学习率是θ₁的10倍!

适用场景:

1
2
3
4
5
6
✅ 稀疏特征(如NLP的词汇表)
- 常见词:梯度大,学习率降低
- 稀有词:梯度小,学习率保持

✅ 不同尺度的特征
- 自动适应不同特征的更新速度

优点:

  • ✅ 自适应学习率
  • ✅ 适合稀疏数据
  • ✅ 不需要手动调整学习率

缺点:

  • 学习率单调递减(G不断累积)
  • ❌ 训练后期学习率过小,停止学习
  • ❌ 不适合深度学习(训练会卡住)

学习率衰减过快问题:

1
2
3
4
5
6
7
8
9
10
假设每步梯度都是1.0

步数 G 实际学习率
1 1 0.01 /1 = 0.01
10 10 0.01 /100.003
100 100 0.01 /100 = 0.001
1000 1000 0.01 /10000.0003

1000步学习率只剩初始的3%!
可能还没收敛就停止学习了

4. RMSprop (Root Mean Square Propagation) ⭐⭐⭐⭐

核心思想:解决AdaGrad学习率递减过快

公式:

1
2
3
4
5
v_t = β * v_{t-1} + (1-β) * (∇L(θ_t))²  # 指数移动平均
θ_t = θ_{t-1} - (lr / √(v_t + ε)) * ∇L(θ_t)

β: 衰减率(通常0.90.99
v: 梯度平方的指数移动平均

与AdaGrad的区别:

1
2
3
4
5
AdaGrad: G_t = G_{t-1} + g_t²     (累加,单调递增)
RMSprop: v_t = β*v_{t-1} + (1-β)*g_t² (移动平均,可增可减)

AdaGrad:考虑全部历史
RMSprop:只考虑近期历史(由β控制)

为什么解决了学习率递减问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
假设前100步梯度很大(训练初期)
第101步开始梯度变小(接近最优)

AdaGrad:
G_100 = 很大(累积了100步的大梯度)
G_101 = G_100 + 小梯度(仍然很大)
学习率:一直很小

RMSprop(β=0.9):
v_100 = 大(但只占约10步的历史)
v_101 = 0.9*v_100 + 0.1*小梯度(逐渐变小)
v_110 ≈ 小(大梯度的影响已衰减)
学习率:恢复正常大小!

优点:

  • ✅ 解决AdaGrad学习率递减过快
  • ✅ 适合非平稳目标(训练中统计量变化)
  • ✅ RNN训练效果好
  • ✅ 对噪声梯度鲁棒

缺点:

  • ❌ 仍需手动调整学习率
  • ❌ 泛化能力不如SGD+Momentum

超参数选择:

1
2
3
lr = 0.001(默认)
β = 0.9(标准)或0.99(更平滑)
ε = 1e-8

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RMSprop:
def __init__(self, params, lr=0.001, beta=0.9, eps=1e-8):
self.params = params
self.lr = lr
self.beta = beta
self.eps = eps
self.v = [torch.zeros_like(p) for p in params]

def step(self):
for i, param in enumerate(self.params):
grad = param.grad
self.v[i] = self.beta * self.v[i] + (1-self.beta) * grad**2
param.data -= self.lr * grad / (torch.sqrt(self.v[i]) + self.eps)

5. Adam (Adaptive Moment Estimation) ⭐⭐⭐⭐⭐

核心思想:结合Momentum和RMSprop

公式:

1
2
3
4
5
6
7
8
9
10
11
12
# 一阶矩(梯度的移动平均,类似Momentum)
m_t = β₁ * m_{t-1} + (1-β₁) * ∇L(θ_t)

# 二阶矩(梯度平方的移动平均,类似RMSprop)
v_t = β₂ * v_{t-1} + (1-β₂) * (∇L(θ_t))²

# 偏差修正(初期m和v接近0,需要修正)
m̂_t = m_t / (1 - β₁ᵗ)
v̂_t = v_t / (1 - β₂ᵗ)

# 参数更新
θ_t = θ_{t-1} - lr * m̂_t / (√v̂_t + ε)

为什么需要偏差修正?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
初始化:m_0 = 0, v_0 = 0

1步(假设梯度g₁=1.0):
m_1 = 0.9*0 + 0.1*1.0 = 0.1
v_1 = 0.999*0 + 0.001*1.0 = 0.001

问题:m_1和v_1都严重低估了!
期望值应该接近g₁,但实际很小

偏差修正:
m̂_1 = 0.1 / (1-0.9¹) = 0.1 / 0.1 = 1.0
v̂_1 = 0.001 / (1-0.999¹) = 0.001 / 0.001 = 1.0

随着t增大,(1-β^t)→1,修正项逐渐失效
100步:(1-0.9^100) ≈ 1.0,几乎不修正

标准超参数:

1
2
3
4
lr = 0.001      # 学习率(也可以是0.0001)
β₁ = 0.9 # 一阶矩衰减率
β₂ = 0.999 # 二阶矩衰减率
ε = 1e-8 # 数值稳定项

为什么这些默认值好用?

1
2
3
β₁ = 0.9:约10步历史的动量
β₂ = 0.999:约1000步历史的二阶矩
β₁ < β₂:方向变化快,步长变化慢(稳定)

Adam的优势:

  • 开箱即用:默认参数适用于大多数任务
  • ✅ 结合Momentum和RMSprop优点
  • ✅ 自适应学习率(每个参数独立)
  • ✅ 对噪声梯度鲁棒
  • ✅ 适合高维稀疏梯度
  • ✅ 内存效率高(只需存m和v)

Adam的劣势:

  • 泛化能力不如SGD+Momentum(图像分类)
  • ❌ 可能不收敛到最优解
  • ❌ 二阶矩估计可能过小(某些情况)

为什么泛化差?

1
2
3
4
5
6
7
8
9
10
11
理论分析:
- Adam倾向于找到"尖锐"的最优解
- SGD+Momentum倾向于找到"平坦"的最优解

平坦的解泛化更好:
- 参数微小扰动,输出变化小
- 对测试数据更鲁棒

尖锐的解容易过拟合:
- 参数微小扰动,输出变化大
- 对测试数据敏感

实验对比(ImageNet):

1
2
3
4
5
ResNet-50训练:
SGD+Momentum: 收敛慢(90 epochs),但Top-1 = 76.5%
Adam: 收敛快(30 epochs),但Top-1 = 75.2%

结论:Adam快,SGD准

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Adam:
def __init__(self, params, lr=0.001, betas=(0.9, 0.999), eps=1e-8):
self.params = params
self.lr = lr
self.beta1, self.beta2 = betas
self.eps = eps
self.m = [torch.zeros_like(p) for p in params]
self.v = [torch.zeros_like(p) for p in params]
self.t = 0

def step(self):
self.t += 1
for i, param in enumerate(self.params):
grad = param.grad

# 更新一阶矩和二阶矩
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**self.t)
v_hat = self.v[i] / (1 - self.beta2**self.t)

# 更新参数
param.data -= self.lr * m_hat / (torch.sqrt(v_hat) + self.eps)

6. AdamW ⭐⭐⭐⭐⭐(Transformer标配)

核心改进:解耦权重衰减(Weight Decay)

Adam中的权重衰减(错误方式):

1
2
3
4
5
6
7
8
9
10
# L2正则化:损失函数加上 λ/2 * ||θ||²
# 等价于梯度加上 λ*θ

grad = grad + λ * param # 修改梯度
# 然后用Adam更新

问题:
- 权重衰减被自适应学习率影响
- 不同参数的衰减强度不同
- 效果不稳定

AdamW中的权重衰减(正确方式):

1
2
3
4
5
6
7
8
# Adam更新
param = param - lr * m̂ / (√v̂ + ε)

# 权重衰减(独立进行)
param = param - lr * λ * param

# 合并
param = param * (1 - lr*λ) - lr * m̂ / (√v̂ + ε)

数学对比:

1
2
3
4
5
6
7
Adam with L2:
θ_t = θ_{t-1} - α * m̂_t / (√v̂_t + ε)
其中 m̂_t 包含了λ*θ的影响

AdamW:
θ_t = θ_{t-1} - α * m̂_t / (√v̂_t + ε) - α*λ*θ_{t-1}
权重衰减独立于梯度

为什么AdamW更好?

1
2
3
1. 解耦:权重衰减强度不受自适应学习率影响
2. 一致:所有参数用相同的衰减比例
3. 泛化:更接近SGD+Momentum的正则化效果

超参数:

1
2
3
4
lr = 0.001        # 学习率
β₁ = 0.9 # 一阶矩
β₂ = 0.999 # 二阶矩
weight_decay = 0.01 # 权重衰减(Transformer常用)

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AdamW:
def __init__(self, params, lr=0.001, betas=(0.9, 0.999),
eps=1e-8, weight_decay=0.01):
self.params = params
self.lr = lr
self.beta1, self.beta2 = betas
self.eps = eps
self.weight_decay = weight_decay
self.m = [torch.zeros_like(p) for p in params]
self.v = [torch.zeros_like(p) for p in params]
self.t = 0

def step(self):
self.t += 1
for i, param in enumerate(self.params):
grad = param.grad

# 标准Adam更新
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**self.t)
v_hat = self.v[i] / (1 - self.beta2**self.t)

# AdamW: 权重衰减独立进行
param.data = param.data * (1 - self.lr * self.weight_decay)
param.data -= self.lr * m_hat / (torch.sqrt(v_hat) + self.eps)

使用场景:

1
2
3
✅ Transformer(BERT、GPT、T5等)
✅ 大模型训练
✅ 需要正则化的任务

实验对比(Transformer训练):

1
2
3
4
5
6
7
WMT翻译任务:
Adam: BLEU = 28.4
AdamW: BLEU = 29.3 ✅ +0.9

BERT预训练:
Adam: 准确率85.2%
AdamW: 准确率86.1% ✅ +0.9%

优化器选择指南

任务类型 推荐优化器 学习率 原因
Transformer/LLM AdamW 1e-4 ~ 1e-3 标准配置 ✅
CNN图像分类 SGD+Momentum 0.01 ~ 0.1 泛化最好
目标检测 SGD+Momentum 0.001 ~ 0.01 稳定收敛
RNN/LSTM Adam 0.001 处理梯度变化
GAN Adam 1e-4 ~ 2e-4 训练稳定
强化学习 Adam 3e-4 标准选择
NLP(非Transformer) Adam 0.001 稀疏梯度

快速决策树:

1
2
3
4
5
6
7
8
9
是Transformer? 
→ 是 → AdamW(weight_decay=0.01)
→ 否 → 是CNN?
→ 是 → 追求速度?
→ 是 → Adam
→ 否 → SGD+Momentum(更好泛化)
→ 否 → 是RNN?
→ 是 → Adam或RMSprop
→ 否 → 先试Adam,不行再换SGD

常见陷阱题 ⚠️

题目1:Adam的默认学习率是?

  • A. 0.01
  • B. 0.001 ✅
  • C. 0.0001
  • D. 0.1

💡 解析:Adam默认lr=0.001,β₁=0.9,β₂=0.999


题目2:AdamW和Adam的主要区别?

  • A. 学习率不同
  • B. 权重衰减方式不同 ✅
  • C. 动量不同
  • D. 收敛速度不同

💡 解析:AdamW解耦权重衰减,不通过梯度而是直接作用于参数。


题目3:以下哪个优化器适合CNN图像分类?

  • A. Adam
  • B. AdamW
  • C. SGD+Momentum ✅
  • D. AdaGrad

💡 解析:SGD+Momentum泛化能力最强,ImageNet冠军模型基本都用它。


题目4:Adam中β₂通常设为?

  • A. 0.9
  • B. 0.99
  • C. 0.999 ✅
  • D. 0.9999

💡 解析:β₁=0.9(一阶矩),β₂=0.999(二阶矩)是标准配置。

记忆技巧 🧠:

  • SGD = “Simple but Good for Deep CNN” → 简单但CNN效果好
  • Momentum = “滚雪球” → 累积速度,越滚越快
  • Adam = “All you need” → 默认首选,适用面广
  • AdamW = “Adam for Worl(d) models” → 大模型标配
  • β₁=0.9, β₂=0.999, lr=0.001 → Adam三件套

七、正则化技术⭐⭐⭐

1. L1正则化 (Lasso)

1
Loss = Loss_original + λ Σ|w_i|

效果:

  • 产生稀疏解(部分权重为0)
  • 特征选择作用

2. L2正则化 (Ridge/Weight Decay)

1
Loss = Loss_original + λ Σw_i²

效果:

  • 权重趋向于小值
  • 防止过拟合
  • 深度学习最常用

3. Dropout

1
2
3
4
5
6
# 训练时
mask = (torch.rand(x.shape) > p).float()
out = x * mask / (1 - p) # 缩放保持期望不变

# 测试时
out = x # 不做dropout

超参数:

  • p = 0.5(全连接层常用)
  • p = 0.1-0.2(卷积层)

效果:

  • 防止神经元共适应
  • 相当于集成学习

4. Batch Normalization

1
2
3
4
5
6
7
# 训练时
μ = x.mean(dim=0)
σ² = x.var(dim=0)
x_norm = (x - μ) / √(σ² + ε)
out = γ * x_norm + β # γ、β可学习

# 测试时使用训练集的全局统计量

效果:

  • 加速训练
  • 允许更大学习率
  • 轻微正则化作用

5. Layer Normalization

1
2
3
μ = x.mean(dim=-1, keepdim=True)
σ² = x.var(dim=-1, keepdim=True)
out = γ * (x - μ) / √(σ² + ε) + β

特点:

  • 对batch size不敏感
  • Transformer标配

6. Data Augmentation

  • 图像:旋转、翻转、裁剪、颜色变换
  • 文本:同义词替换、回译、随机插入删除
  • 最有效的正则化方法

八、损失函数详解(训练目标)⭐⭐⭐⭐⭐

为什么损失函数重要?
损失函数定义了”什么是好模型”,选错损失函数,模型再大也没用!分类用MSE会崩溃,回归用交叉熵会报错。

损失函数基础

作用:

1
2
3
4
5
量化预测值和真实值的差距
优化目标:min L(ŷ, y)

ŷ: 模型预测
y: 真实标签

选择原则:

  1. 任务类型匹配:回归/分类/生成各不同
  2. 数学性质好:可导、凸函数最好
  3. 梯度性质:不能太大也不能太小
  4. 业务需求:是否关注异常值、类别平衡等

1. MSE (Mean Squared Error) - 回归 ⭐⭐⭐⭐⭐

公式:

1
MSE = (1/n) Σᵢ (yᵢ - ŷᵢ)²

直观理解:

1
2
3
4
5
6
预测房价:
真实:100万 预测:120万
误差:20万
平方:400万²(放大)

作用:惩罚大误差

梯度推导:

1
2
3
4
5
6
7
L = (y - ŷ)²

∂L/∂ŷ = 2(y - ŷ) * (-1) = -2(y - ŷ) = 2(ŷ - y)

梯度正比于误差:
- 误差大 → 梯度大 → 更新快
- 误差小 → 梯度小 → 更新慢

为什么用平方而非绝对值?

1
2
3
4
1. 可导性:x²处处可导,|x|在0点不可导
2. 唯一最优解:凸函数,全局最优
3. 统计意义:最大似然估计(假设高斯噪声)
4. 梯度连续:优化更稳定

优点:

  • ✅ 可导性好,处处可导
  • ✅ 凸函数,全局最优
  • ✅ 对大误差惩罚重(关注outlier)
  • ✅ 数学优美,统计意义明确

缺点:

  • 对异常值敏感(平方放大)
  • ❌ 梯度可能很大(误差大时)
  • ❌ 不适合分类任务

异常值敏感示例:

1
2
3
4
正常样本:误差1, 1, 1, 1, 1  → MSE = (1²×5)/5 = 1
有异常值:误差1, 1, 1, 1, 10 → MSE = (4 + 100)/5 = 20.8

一个异常值让损失翻了20倍!

变体:RMSE (Root MSE)

1
2
3
4
RMSE = √MSE

优点:与原数据同单位
例:房价误差20万,而非400万²(更直观)

PyTorch实现:

1
2
3
4
5
6
7
import torch.nn as nn

criterion = nn.MSELoss()
loss = criterion(predictions, targets)

# 或手动实现
loss = ((predictions - targets) ** 2).mean()

2. MAE (Mean Absolute Error) - 回归 ⭐⭐⭐⭐

公式:

1
MAE = (1/n) Σᵢ |yᵢ - ŷᵢ|

梯度:

1
2
3
4
5
6
7
8
9
L = |y - ŷ|

∂L/∂ŷ = {
-1, if ŷ < y
+1, if ŷ > y
undefined, if ŷ = y
}

梯度恒为±1,与误差大小无关!

与MSE对比:

1
2
3
4
5
6
误差    MSE梯度    MAE梯度
0.1 0.2 1.0
1.0 2.0 1.0
10.0 20.0 1.0 ← MSE梯度爆炸

MAE对所有误差"一视同仁"

优点:

  • 对异常值鲁棒(不平方)
  • ✅ 更符合直觉(直接看平均误差)
  • ✅ 单位与原数据一致

缺点:

  • 0点不可导(实际用subgradient)
  • ❌ 非凸优化问题(多个局部最优)
  • ❌ 收敛可能较慢(梯度恒定)

使用场景:

1
2
3
4
5
6
✅ 数据有异常值(如房价中的豪宅)
✅ 关心中位数而非均值
✅ 误差分布非高斯

❌ 需要快速收敛
❌ 需要关注大误差

PyTorch实现:

1
2
criterion = nn.L1Loss()  # MAE也叫L1 Loss
loss = criterion(predictions, targets)

3. Huber Loss(结合MSE和MAE)⭐⭐⭐

公式:

1
2
3
4
5
6
Huber(y, ŷ) = {
(1/2)(y - ŷ)², if |y - ŷ| ≤ δ
δ|y - ŷ| - (1/2)δ², if |y - ŷ| > δ
}

δ: 阈值(如1.0)

直观理解:

1
2
3
4
小误差(|e| ≤ δ):用MSE(平方,快速收敛)
大误差(|e| > δ):用MAE(线性,鲁棒)

"两全其美"

梯度:

1
2
3
4
5
6
∂L/∂ŷ = {
ŷ - y, if |y - ŷ| ≤ δ (类似MSE)
δ * sign(ŷ - y), if |y - ŷ| > δ (类似MAE,限制在±δ)
}

关键:大误差时梯度被clip,不会爆炸

优点:

  • ✅ 结合MSE和MAE优点
  • ✅ 对异常值鲁棒
  • ✅ 收敛稳定(梯度有界)

PyTorch实现:

1
2
criterion = nn.SmoothL1Loss()  # 也叫Smooth L1,类似Huber
loss = criterion(predictions, targets)

4. Cross-Entropy Loss(交叉熵)- 分类 ⭐⭐⭐⭐⭐

信息论基础

熵(Entropy):

1
2
3
4
5
6
7
8
9
10
H(P) = -Σᵢ p(i) log p(i)

衡量不确定性/信息量

例:抛硬币
均匀:P(正)=0.5, P(反)=0.5
H = -0.5*log(0.5) - 0.5*log(0.5) = 1 bit(最大不确定性)

确定:P(正)=1.0, P(反)=0.0
H = -1.0*log(1.0) - 0*log(0) = 0(无不确定性)

交叉熵(Cross-Entropy):

1
2
3
4
5
6
H(P, Q) = -Σᵢ p(i) log q(i)

P: 真实分布
Q: 预测分布

衡量两个分布的差异

KL散度(Kullback-Leibler Divergence):

1
2
3
4
5
D_KL(P || Q) = Σᵢ p(i) log(p(i)/q(i))
= H(P, Q) - H(P)

H(P)是常数(真实分布固定)
最小化交叉熵 = 最小化KL散度

二分类交叉熵(Binary Cross-Entropy)⭐⭐⭐⭐⭐

公式:

1
2
3
4
BCE = -[y*log(ŷ) + (1-y)*log(1-ŷ)]

y ∈ {0, 1}:真实标签
ŷ ∈ (0, 1):预测概率(经过sigmoid

具体计算:

1
2
3
4
5
6
7
样本1:y=1(正类),ŷ=0.9
BCE = -[1*log(0.9) + 0*log(0.1)] = -log(0.9)0.105

样本2:y=1(正类),ŷ=0.1
BCE = -[1*log(0.1) + 0*log(0.9)] = -log(0.1)2.303

预测错误的损失是预测正确的22倍!

梯度推导(重要!):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
假设最后一层是sigmoid:ŷ = sigmoid(z)

L = -[y*log(ŷ) + (1-y)*log(1-ŷ)]

∂L/∂z = ∂L/∂ŷ * ∂ŷ/∂z

∂L/∂ŷ = -y/ŷ + (1-y)/(1-ŷ)

∂ŷ/∂z = ŷ(1-ŷ) (sigmoid导数)

∂L/∂z = [-y/ŷ + (1-y)/(1-ŷ)] * ŷ(1-ŷ)
= -y(1-ŷ) + (1-y
= -y + yŷ + ŷ - yŷ
= ŷ - y ✅ 超级简洁!

梯度 = 预测 - 真实

为什么不用MSE做分类?

1
2
3
4
5
6
7
8
9
10
假设用MSE:L = (y - ŷ)²

∂L/∂z = 2(ŷ - y) * ŷ(1-ŷ) ← 多了个ŷ(1-ŷ)项

问题:
当ŷ接近01时,ŷ(1-ŷ)≈0
→ 梯度消失
→ 训练卡住

交叉熵的梯度只是ŷ-y,没有这个问题!

PyTorch实现:

1
2
3
4
5
6
7
8
# 方法1:手动sigmoid + BCE
criterion = nn.BCELoss()
predictions = torch.sigmoid(logits) # 先sigmoid
loss = criterion(predictions, targets)

# 方法2:BCE with Logits(数值稳定,推荐)
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, targets) # 内部自动sigmoid

多分类交叉熵(Categorical Cross-Entropy)⭐⭐⭐⭐⭐

公式:

1
2
3
4
CE = -Σᵢ yᵢ * log(ŷᵢ)

y: one-hot向量,如[0, 1, 0]
ŷ: 概率分布(经过softmax),如[0.1, 0.7, 0.2]

具体计算:

1
2
3
4
5
6
7
8
9
3分类问题:
真实:y = [0, 1, 0](第2类)
预测:ŷ = [0.1, 0.7, 0.2]

CE = -[0*log(0.1) + 1*log(0.7) + 0*log(0.2)]
= -log(0.7)
0.357

只关心正确类的预测概率!

稀疏表示(Sparse CE):

1
2
3
4
5
6
7
8
9
当类别很多时(如ImageNet 1000类)
存储one-hot向量[0,0,...,1,...,0]浪费内存

稀疏表示:只存类别索引
y = 2(表示第2类)

CE = -log[y]) = -log[2])

PyTorch默认用这种方式

梯度(Softmax + CE):

1
2
3
4
5
6
7
8
9
10
11
12
13
假设最后一层是Softmax

∂L/∂zᵢ = ŷᵢ - yᵢ ✅ 同样简洁!

例:
真实:y = [0, 1, 0]
预测:ŷ = [0.2, 0.5, 0.3]

梯度 = [0.2-0, 0.5-1, 0.3-0]
= [0.2, -0.5, 0.3]

预测对了(类别1):梯度-0.5(降低logit)
预测错了(类别0,2):梯度正(升高logit)

PyTorch实现:

1
2
3
4
5
6
7
8
# 输入:logits(未经softmax)
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, targets) # targets是类别索引,不是one-hot

# 例子
logits = torch.tensor([[2.0, 1.0, 0.1]]) # (batch=1, classes=3)
targets = torch.tensor([0]) # 正确类别是0
loss = criterion(logits, targets)

常见错误 ⚠️:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 错误1:给CrossEntropyLoss传入softmax后的值
logits = model(x)
probs = F.softmax(logits, dim=-1) # 错误!
loss = nn.CrossEntropyLoss()(probs, targets) # 会二次softmax

# ✅ 正确:直接传logits
loss = nn.CrossEntropyLoss()(logits, targets)

# ❌ 错误2:targets用one-hot
targets = torch.tensor([[1, 0, 0]]) # 错误!
loss = nn.CrossEntropyLoss()(logits, targets)

# ✅ 正确:targets用类别索引
targets = torch.tensor([0])
loss = nn.CrossEntropyLoss()(logits, targets)

5. Focal Loss - 类别不平衡 ⭐⭐⭐⭐⭐

背景:目标检测中的类别不平衡

1
2
3
4
5
6
7
8
图像中:
背景像素:99%(容易分类)
目标像素:1%(难分类)

问题:
- 大量简单样本(背景)主导损失
- 少量难样本(目标)被忽略
- 模型倾向于预测背景

Focal Loss公式:

1
2
3
4
5
6
FL = -α(1-p)^γ * log(p)

其中:
p: 正确类的预测概率
α: 类别权重(如0.25)
γ: 聚焦参数(如2.0)

与普通CE对比:

1
2
3
4
5
6
7
8
9
10
11
12
CE = -log(p)
FL = -(1-p)^γ * log(p)

关键:(1-p)^γ项

当p接近1(易分样本):
(1-p)^γ ≈ 0 → FL ≈ 0 ✅ 损失被降低

当p接近0(难分样本):
(1-p)^γ ≈ 1 → FL ≈ -log(p) ✅ 损失保持

自动降低易分样本权重!

具体计算:

1
2
3
4
5
6
7
8
9
10
11
γ = 2, α = 0.25

样本1(易分):p = 0.9
CE = -log(0.9) = 0.105
FL = -0.25 * (1-0.9)² * log(0.9) = -0.25 * 0.01 * 0.105 ≈ 0.0003
FL / CE ≈ 0.3%(损失降低到0.3%)

样本2(难分):p = 0.5
CE = -log(0.5) = 0.693
FL = -0.25 * (1-0.5)² * log(0.5) = -0.25 * 0.25 * 0.693 ≈ 0.043
FL / CE ≈ 6.2%(损失保留较多)

γ的影响:

1
2
3
4
γ = 0: FL = CE(退化为普通交叉熵)
γ = 1: 线性降权
γ = 2: 平方降权(标准选择)
γ = 5: 激进降权(只关注极难样本)

PyTorch实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma

def forward(self, logits, targets):
# 计算概率
probs = torch.sigmoid(logits)

# 计算BCE
bce_loss = F.binary_cross_entropy_with_logits(
logits, targets, reduction='none'
)

# 计算pt(正确类的概率)
pt = torch.where(targets == 1, probs, 1 - probs)

# Focal Loss
focal_weight = (1 - pt) ** self.gamma
focal_loss = self.alpha * focal_weight * bce_loss

return focal_loss.mean()

应用场景:

1
2
3
✅ 目标检测(RetinaNet)
✅ 语义分割(小目标)
✅ 严重类别不平衡

6. Contrastive Loss - 对比学习 ⭐⭐⭐⭐

思想:拉近相似样本,推远不相似样本

公式:

1
2
3
4
5
6
L = y*d² + (1-y)*max(margin - d, 0

其中:
d: 两个样本的距离(如欧式距离)
y: 标签(1表示相似,0表示不相似)
margin: 边界(如1.0

直观理解:

1
2
3
4
5
6
7
8
相似样本对(y=1):
L = d²
目标:让d尽量小(拉近)

不相似样本对(y=0):
L = max(margin - d, 0)²
- 如果d > margin:L=0(已经够远,不管了)
- 如果d < margin:L=(margin-d)²(推远到margin)

具体计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
margin = 1.0

情况1:相似对,d=0.2
L = 1 * 0.2² = 0.04 (希望更近)

情况2:相似对,d=0.8
L = 1 * 0.8² = 0.64 (损失较大,需要大幅拉近)

情况3:不相似对,d=1.5
L = 0 * d² + 1 * max(1.0-1.5, 0)² = 0 (已够远)

情况4:不相似对,d=0.3
L = 0 * d² + 1 * max(1.0-0.3, 0)² = 0.49 (需要推远)

应用:

1
2
3
✅ 人脸识别(Siamese Network)
✅ 图像检索
✅ 度量学习

7. Triplet Loss - 三元组学习 ⭐⭐⭐⭐

思想:锚点、正样本、负样本三元组

公式:

1
2
3
4
5
6
L = max(d(a,p) - d(a,n) + margin, 0)

a: anchor(锚点)
p: positive(正样本,与a同类)
n: negative(负样本,与a不同类)
margin: 边界(如0.2

直观理解:

1
2
3
4
5
6
7
目标:d(a,p) + margin < d(a,n)

即:正样本距离 + 边界 < 负样本距离

形象比喻:
- 朋友(正样本):坐在身边
- 陌生人(负样本):至少隔margin距离

具体计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
margin = 0.2

情况1(良好):
d(a,p) = 0.1, d(a,n) = 0.5
L = max(0.1 - 0.5 + 0.2, 0) = max(-0.2, 0) = 0

情况2(需优化):
d(a,p) = 0.3, d(a,n) = 0.4
L = max(0.3 - 0.4 + 0.2, 0) = max(0.1, 0) = 0.1 ⚠️

情况3(很差):
d(a,p) = 0.5, d(a,n) = 0.2 (正样本比负样本还远!)
L = max(0.5 - 0.2 + 0.2, 0) = 0.5 ☠️

难样本挖掘(Hard Mining):

1
2
3
4
5
6
随机采样三元组效果差(大部分L=0

策略:
1. Hard Negative:选择最接近anchor的负样本
2. Semi-Hard Negative:d(a,p) < d(a,n) < d(a,p)+margin
3. 在线挖掘:训练中动态选择难样本

PyTorch实现:

1
2
criterion = nn.TripletMarginLoss(margin=0.2)
loss = criterion(anchor, positive, negative)

应用:

1
2
3
✅ 人脸识别(FaceNet)
✅ 图像检索
✅ 行人重识别

损失函数对比总结

损失函数 任务类型 优点 缺点 代表应用
MSE 回归 可导、凸优化 对异常值敏感 房价预测
MAE 回归 对异常值鲁棒 0点不可导 鲁棒回归
Huber 回归 结合MSE/MAE 需要调δ 强化学习
BCE 二分类 梯度简洁 类别平衡差 垃圾邮件检测
CE 多分类 标准选择 ✅ 类别平衡差 ImageNet
Focal Loss 不平衡分类 自动加权 多个超参数 RetinaNet
Contrastive 度量学习 学习相似度 需要样本对 人脸识别
Triplet 度量学习 学习排序 采样复杂 FaceNet

常见陷阱题 ⚠️

题目1:为什么分类任务不用MSE?

  • A. 计算太慢
  • B. Sigmoid配MSE梯度消失 ✅
  • C. 精度不够
  • D. 不可导

💡 解析:MSE配Sigmoid的梯度有ŷ(1-ŷ)项,在0和1处梯度消失。


题目2:Focal Loss的γ通常设为?

  • A. 0.5
  • B. 1.0
  • C. 2.0 ✅
  • D. 5.0

💡 解析:γ=2是RetinaNet论文的标准配置。


题目3:CrossEntropyLoss的输入应该是?

  • A. Softmax后的概率
  • B. 原始logits ✅
  • C. One-hot标签
  • D. 归一化特征

💡 解析:PyTorch的CrossEntropyLoss内部会做softmax,输入应是logits。


题目4:MAE相比MSE的优势?

  • A. 收敛更快
  • B. 对异常值鲁棒 ✅
  • C. 梯度更大
  • D. 可导性更好

💡 解析:MAE不平方误差,对异常值不敏感。

记忆技巧 🧠:

  • 回归MSE,分类CE → 最基本的对应
  • Sigmoid+BCE, Softmax+CE = 梯度ŷ-y → 完美配对
  • Focal Loss = “关注难的,忽略易的” → 类别不平衡救星
  • Triplet = “朋友近,陌生人远” → 度量学习
  • 千万别给CE传入softmax后的值 → 会二次softmax

九、梯度问题(训练稳定性核心)⭐⭐⭐⭐⭐

为什么梯度问题重要?
深度学习的”两大杀手”:梯度消失让模型学不动,梯度爆炸让模型炸掉。ResNet、LSTM、BatchNorm都是为了解决梯度问题。

梯度的本质

梯度是什么?

1
2
3
4
5
梯度 = 损失函数对参数的偏导数
∇L = ∂L/∂θ

指向损失增长最快的方向
负梯度 = 下降方向

反向传播中的梯度:

1
2
3
4
5
6
L → 输出层 → 隐藏层N → ... → 隐藏层1 → 输入

链式法则:
∂L/∂W₁ = ∂L/∂hₙ × ∂hₙ/∂hₙ₋₁ × ... × ∂h₂/∂h₁ × ∂h₁/∂W₁

连乘!这是问题的根源

1. 梯度消失(Vanishing Gradient)⭐⭐⭐⭐⭐

问题表现

症状:

1
2
3
4
✓ 训练初期损失下降
✗ 很快停滞不前
✗ 前几层权重几乎不更新
✗ 网络退化为浅层网络

实际例子:

1
2
3
4
5
6
10层Sigmoid网络训练:

Epoch 1: Loss 2.3, 所有层都在更新
Epoch 5: Loss 1.8, 前5层权重变化<0.001
Epoch 10: Loss 1.7, 只有最后2层在学习
最终:前8层相当于随机初始化状态

产生原因

原因1:Sigmoid/Tanh的饱和区 ⭐⭐⭐⭐⭐

数学分析:

1
2
3
4
5
6
7
8
9
sigmoid(x) = 1/(1 + e^(-x))
导数:σ'(x) = σ(x)(1 - σ(x))

关键值:
σ'(0) = 0.25(最大)
σ'5) ≈ 0.01
σ'10) ≈ 0.00005

饱和区(|x| > 3)梯度接近0

链式求导的灾难:

1
2
3
4
5
6
7
8
9
10层Sigmoid网络:

假设每层激活值都在饱和区
每层梯度:0.1

反向传播到第1层:
∂L/∂W₁ ∝ 0.1¹⁰ = 1e-10 ☠️

梯度几乎为0,第1层学不到东西

可视化:

1
2
3
4
5
6
7
层数    梯度大小
10层 1.0
9层 0.1
8层 0.01
7层 0.001
...
1层 1e-10 ← 消失了

为什么ReLU缓解?

1
2
3
4
5
6
7
ReLU: f(x) = max(0, x)
导数:f'(x) = 1 (if x>0)

正区域梯度恒为1,不会衰减!

10层ReLU:
梯度 ∝ 1¹⁰ = 1.0 ✅ 不消失

原因2:权重初始化不当 ⭐⭐⭐⭐

权重过小:

1
2
3
4
5
6
7
W ~ N(0, 0.001²)

每层输出 hₗ = W·hₗ₋₁
|hₗ| ≈ 0.001 × |hₗ₋₁|

10层后:|h₁₀| ≈ 0.001¹⁰ × |h₀| ≈ 1e-30 ☠️
激活值消失 → 梯度消失

权重过大(反而也可能消失):

1
2
3
4
W ~ N(0, 10²)

每层输出很大 → 激活函数饱和(Sigmoid/Tanh
→ 梯度接近0 → 消失

Xavier初始化:

1
2
3
4
5
为Sigmoid/Tanh设计

W ~ N(0, √(2/(n_in + n_out)))

保持方差稳定:Var(h_out) ≈ Var(h_in)

He初始化:

1
2
3
4
5
6
为ReLU设计

W ~ N(0, √(2/n_in))

考虑ReLU砍掉一半神经元
需要更大的初始方差

原因3:网络太深 ⭐⭐⭐

深度与梯度衰减:

1
2
3
4
5
6
7
8
9
10
假设每层梯度衰减率 r = 0.9

层数 梯度保留率
190%
559%(0.9⁵)
1035%(0.9¹⁰)
2012%(0.9²⁰)
500.5%(0.9⁵⁰)☠️

越深越难训练

解决方案

方案1:使用ReLU及其变体 ⭐⭐⭐⭐⭐

1
2
3
4
5
6
# 替换Sigmoid/Tanh
# 之前
self.activation = nn.Sigmoid()

# 之后
self.activation = nn.ReLU() # 或LeakyReLU、GELU

效果对比:

1
2
3
4
5
20层网络,MNIST分类:

Sigmoid: 训练到80%准确率卡住
Tanh: 训练到85%准确率
ReLU: 训练到98%准确率 ✅

方案2:Batch Normalization ⭐⭐⭐⭐⭐

原理:

1
2
3
4
5
6
7
8
9
10
11
归一化激活值,防止进入饱和区

每层后加BN:
x = Conv(input)
x = BN(x) # 归一化到均值0,方差1
x = ReLU(x)

效果:
- 激活值分布稳定
- 不容易进入饱和区
- 梯度流动顺畅

实验证明:

1
2
3
50层CNN:
无BN: Loss卡住,前40层几乎不学习
有BN: 顺利收敛,所有层都在更新 ✅

方案3:残差连接(ResNet) ⭐⭐⭐⭐⭐

核心思想:

1
2
3
4
传统:H(x) = F(x)
ResNet: H(x) = F(x) + x

加了个shortcut(捷径)

梯度流动:

1
2
3
4
5
6
7
反向传播:
∂H/∂x = ∂F/∂x + 1 ← 关键!

即使F的梯度消失(∂F/∂x0
总梯度至少有1(来自shortcut)

梯度可以直接跳过多层传播!

实际效果:

1
2
3
4
5
ImageNet分类:

Plain 50层: 训练误差反而比20层高(退化问题)
ResNet 50层: 训练误差持续下降
ResNet 152层: 仍然能训练(无梯度消失)✅

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)

def forward(self, x):
residual = x # 保存输入

out = self.conv1(x)
out = self.bn1(out)
out = F.relu(out)

out = self.conv2(out)
out = self.bn2(out)

out = out + residual # 残差连接
out = F.relu(out)

return out

方案4:LSTM的门控机制 ⭐⭐⭐⭐

RNN的梯度消失:

1
2
3
4
5
6
7
h_t = tanh(W_hh * h_{t-1} + W_xh * x_t)

反向传播T步:
∂L/∂h_0 ∝ (W_hh)^T × tanh'(...) × ... × tanh'(...)

如果|W_hh|<1: 梯度消失
如果|W_hh|>1: 梯度爆炸

LSTM的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
遗忘门:f_t = σ(...)
输入门:i_t = σ(...)
输出门:o_t = σ(...)

细胞状态更新:
C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t
↑ 保留历史 ↑ 加入新信息

梯度流动:
∂C_t/∂C_{t-1} = f_t

关键:f_t ∈ (0,1),可学习
如果需要记住长期信息,f_t1
梯度几乎无衰减地传播

方案5:梯度裁剪(Gradient Clipping) ⭐⭐⭐

虽然针对爆炸,但也能稳定训练:

1
2
3
4
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)

2. 梯度爆炸(Exploding Gradient)⭐⭐⭐⭐⭐

问题表现

症状:

1
2
3
4
✗ 损失突然变成NaNInf
✗ 权重更新幅度巨大(>1e6
✗ 训练不稳定,震荡
✗ 模型输出异常

实际例子:

1
2
3
4
5
6
7
8
训练RNN:

Epoch 1: Loss 2.5, 正常
Epoch 2: Loss 1.8, 正常
Epoch 3: Loss 0.9, 正常
Epoch 4: Loss NaN ☠️ 爆炸了

检查权重:某些值 > 1e10

产生原因

原因1:权重初始化过大 ⭐⭐⭐⭐

分析:

1
2
3
4
5
6
7
W ~ N(0, 10²)(初始化方差太大)

每层输出:h = W·x
|h| ≈ 10 × |x|

10层后:|h₁₀| ≈ 10¹⁰ × |x₀| ☠️
激活值爆炸 → 梯度爆炸

解决:合理初始化

1
2
3
4
5
# Xavier(Sigmoid/Tanh)
nn.init.xavier_normal_(layer.weight)

# He(ReLU)
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')

原因2:学习率过大 ⭐⭐⭐⭐⭐

机制:

1
2
3
4
5
6
7
8
9
10
θ_new = θ_old - lr × grad

如果lr太大:
θ_new = 0.5 - 1.0 × 10 = -9.5 (跳太远)

下一步:
grad(θ_new) 可能更大(损失函数陡峭区域)
θ_newer = -9.5 - 1.0 × 100 = -109.5

越跳越远,发散!

实验:

1
2
3
4
5
ResNet训练ImageNet:

lr=0.001: 正常收敛
lr=0.01: 前几个epoch震荡但最终收敛
lr=0.1: 第一个epoch就NaN ☠️

解决:学习率调度

1
2
3
4
5
6
7
8
9
10
11
12
# Warm-up:初期用小学习率
scheduler = torch.optim.lr_scheduler.LinearLR(
optimizer,
start_factor=0.01, # 从1%开始
total_iters=1000 # 1000步增长到100%
)

# 然后Cosine衰减
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100
)

原因3:RNN的时序累积 ⭐⭐⭐⭐

长序列梯度爆炸:

1
2
3
4
5
6
7
8
9
RNN展开T步:

∂L/∂h₀ = ∂L/∂h_T × ∂h_T/∂h_{T-1} × ... × ∂h₁/∂h₀

如果|∂h_t/∂h_{t-1}| > 1
梯度 ∝ (>1)^T → 指数增长

例:|∂h/∂h|=1.1, T=100
梯度 ≈ 1.1¹⁰⁰ ≈ 13780 ☠️

解决:LSTM + Gradient Clipping

原因4:Batch Normalization不当 ⭐⭐⭐

问题场景:

1
2
3
4
5
6
7
8
训练初期,BN的统计量不稳定
某个batch的方差很小
归一化时除以很小的数 → 爆炸

x_norm = (x - μ) / √(σ² + ε)

如果σ² ≈ 0,ε太小(如1e-10):
x_norm → 很大 → 梯度爆炸

解决:

1
2
3
# 增大epsilon
nn.BatchNorm2d(channels, eps=1e-5) # 默认,够用
# 极端情况用1e-3

解决方案

方案1:梯度裁剪(Gradient Clipping) ⭐⭐⭐⭐⭐

按范数裁剪(最常用):

1
2
3
4
5
6
7
8
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0 # 梯度L2范数上限
)

工作原理:
if ||grad|| > max_norm:
grad = grad * (max_norm / ||grad||)

数学:

1
2
3
4
5
6
原始梯度:g = [100, 200, 50]
范数:||g|| = √(100² + 200² + 50²) ≈ 229

如果max_norm=1.0
缩放因子 = 1.0 / 2290.0044
裁剪后:g = [0.44, 0.88, 0.22]

按值裁剪(较少用):

1
2
3
4
torch.nn.utils.clip_grad_value_(
model.parameters(),
clip_value=0.5 # 每个梯度分量限制在[-0.5, 0.5]
)

何时使用:

1
2
3
4
✅ RNN/LSTM训练(必须)
✅ GAN训练(提高稳定性)
✅ 强化学习(梯度不稳定)
✅ 大学习率训练

实验效果:

1
2
3
4
5
LSTM语言模型(序列长度100):

无裁剪:30%概率梯度爆炸,训练失败
max_norm=5.0:稳定训练,困惑度降到80
max_norm=1.0:更稳定,困惑度降到75 ✅

方案2:权重正则化 ⭐⭐⭐

L2正则化:

1
2
3
4
5
6
optimizer = torch.optim.Adam(
model.parameters(),
weight_decay=0.01 # L2正则化
)

效果:限制权重大小 → 限制梯度大小

方案3:Batch Normalization ⭐⭐⭐⭐

作用:

  • 归一化激活值 → 稳定梯度
  • 减少对初始化的敏感
  • 允许使用更大学习率

方案4:降低学习率 ⭐⭐⭐⭐⭐

策略:

1
2
3
4
5
6
7
8
9
10
# 方法1:直接降低
optimizer = Adam(model.parameters(), lr=0.0001) # 从0.001降到0.0001

# 方法2:学习率调度
scheduler = ReduceLROnPlateau(
optimizer,
mode='min',
factor=0.5, # 每次减半
patience=5 # 5个epoch无改进就降低
)

方案5:使用更稳定的优化器 ⭐⭐⭐

1
2
3
SGD → Adam/AdamW

Adam的自适应学习率天然防止爆炸

3. 梯度检测方法

检测消失/爆炸:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def check_gradients(model):
for name, param in model.named_parameters():
if param.grad is not None:
grad_norm = param.grad.norm().item()

if grad_norm < 1e-7:
print(f"⚠️ {name}: 梯度消失 ({grad_norm})")
elif grad_norm > 1e3:
print(f"☠️ {name}: 梯度爆炸 ({grad_norm})")
elif torch.isnan(param.grad).any():
print(f"☠️ {name}: 梯度NaN")
else:
print(f"✅ {name}: 梯度正常 ({grad_norm})")

# 在训练循环中使用
loss.backward()
check_gradients(model)
optimizer.step()

梯度流可视化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def plot_grad_flow(named_parameters):
"""绘制梯度流图"""
import matplotlib.pyplot as plt

ave_grads = []
layers = []

for n, p in named_parameters:
if p.grad is not None and "bias" not in n:
layers.append(n)
ave_grads.append(p.grad.abs().mean().item())

plt.plot(ave_grads, alpha=0.3, color="b")
plt.hlines(0, 0, len(ave_grads)+1, linewidth=1, color="k")
plt.xticks(range(0, len(ave_grads), 1), layers, rotation="vertical")
plt.xlim(xmin=0, xmax=len(ave_grads))
plt.xlabel("Layers")
plt.ylabel("Average Gradient")
plt.title("Gradient Flow")
plt.grid(True)
plt.show()

梯度问题对比总结

问题 表现 主要原因 解决方案
梯度消失 前层不学习 Sigmoid饱和 ReLU + BN + ResNet ✅
梯度爆炸 Loss变NaN 学习率大、权重大 Gradient Clipping ✅
梯度震荡 Loss抖动 学习率大、batch小 降低lr、增大batch
梯度为0 完全不学习 Dead ReLU Leaky ReLU

常见陷阱题 ⚠️

题目1:以下哪个不能缓解梯度消失?

  • A. 使用ReLU
  • B. Batch Normalization
  • C. 增大学习率 ✅
  • D. 残差连接

💡 解析:增大学习率解决不了梯度消失,反而可能导致梯度爆炸。


题目2:Gradient Clipping主要解决什么问题?

  • A. 梯度消失
  • B. 梯度爆炸 ✅
  • C. 过拟合
  • D. 欠拟合

💡 解析:梯度裁剪限制梯度上限,防止爆炸。


题目3:ResNet的shortcut连接如何缓解梯度消失?

  • A. 增大梯度值
  • B. 提供恒等映射,梯度至少为1 ✅
  • C. 使用更好的激活函数
  • D. 增加正则化

💡 解析:∂(x+F(x))/∂x = 1 + ∂F/∂x,即使F的梯度消失,还有1。


题目4:LSTM如何缓解RNN的梯度消失?

  • A. 使用更大的学习率
  • B. 门控机制控制信息流 ✅
  • C. 减少层数
  • D. 使用Dropout

💡 解析:LSTM的遗忘门可以学习保留长期信息,梯度可以无衰减传播。

记忆技巧 🧠:

  • 梯度消失 = “信号衰减” → Sigmoid饱和、网络太深
  • 梯度爆炸 = “信号放大” → 学习率大、权重大
  • ReLU三件套对付消失:ReLU + BN + ResNet
  • Gradient Clipping对付爆炸 → 必备武器
  • LSTM的门 = “水闸” → 控制信息流,防止消失

十、模型评估指标(模型好坏的标准)⭐⭐⭐⭐⭐

为什么评估指标重要?
准确率90%可能是个烂模型(99%都是负类)!选对指标才能正确评估模型。医疗诊断看Recall,垃圾邮件看Precision。

分类任务指标

混淆矩阵(Confusion Matrix)⭐⭐⭐⭐⭐

基础定义:

1
2
3
4
5
6
7
8
              预测正例   预测负例
实际正例 TP FN
实际负例 FP TN

TP (True Positive): 真阳性,预测为正,实际为正 ✅
FP (False Positive): 假阳性,预测为正,实际为负 ❌(误报)
FN (False Negative): 假阴性,预测为负,实际为正 ❌(漏报)
TN (True Negative): 真阴性,预测为负,实际为负 ✅

实际例子:癌症检测(100个样本)

1
2
3
4
5
6
7
8
9
10
实际:10个患癌,90个健康

模型预测:
- 患癌的10个中,检测出8个(TP=8),漏了2个(FN=2
- 健康的90个中,误诊5个(FP=5),正确85个(TN=85

混淆矩阵:
预测患癌 预测健康
实际患癌 8 2
实际健康 5 85

记忆技巧:

1
2
3
4
5
6
7
True/False:预测是否正确
Positive/Negative:预测的类别

TP = True说对了,预测Positive
FP = False说错了,预测Positive(误报)
FN = False说错了,预测Negative(漏报)
TN = True说对了,预测Negative

准确率(Accuracy)⭐⭐⭐⭐

公式:

1
2
Accuracy = (TP + TN) / (TP + TN + FP + FN)
= 预测正确的 / 总样本数

癌症检测例子:

1
Accuracy = (8 + 85) / 100 = 93%

看起来很高?但有问题!

准确率的陷阱:类别不平衡 ⚠️⚠️⚠️

1
2
3
4
5
6
7
8
极端例子:
- 实际:1个患癌,99个健康
- 模型:全部预测为"健康"

TP=0, FP=0, FN=1, TN=99
Accuracy = 99/100 = 99% ← 看起来很好!

但完全没用:患癌的人全漏了!

何时使用准确率:

1
2
3
4
5
6
✅ 类别平衡(如MNIST,每类约10%)
✅ 各类别同等重要
✅ 快速评估

❌ 类别不平衡(如欺诈检测,正常占99.9%)
❌ 关心特定类别(如癌症检测,漏报代价大)

精确率(Precision)⭐⭐⭐⭐⭐

公式:

1
2
Precision = TP / (TP + FP)
= 预测为正的样本中,真正为正的比例

直观理解:

1
2
3
"我说是的,有多少真的是?"
"查准率"
"宁可漏报,不要误报"

癌症检测例子:

1
2
3
Precision = 8 / (8 + 5) = 8/1361.5%

预测患癌的13人中,8人真患癌,5人是误诊

应用场景:

1
2
3
4
5
6
7
8
9
10
✅ 垃圾邮件检测
- 高Precision:误判正常邮件为垃圾很糟糕(用户会错过重要邮件)
- 宁可漏几个垃圾邮件

✅ 推荐系统
- 高Precision:推荐的东西用户真的喜欢
- 宁可漏推,不要错推

✅ 搜索引擎
- 高Precision:返回的结果都相关

极端情况分析:

1
2
3
4
5
6
7
8
9
10
11
模型1:非常保守,只在极度确信时预测为正
预测患癌:2人,全对(TP=2, FP=0)
Precision = 2/2 = 100% ← 完美!
但漏了8个患癌的人(FN=8)

模型2:非常激进,大量预测为正
预测患癌:50人,其中8人真患癌(TP=8, FP=42)
Precision = 8/50 = 16% ← 很低
但没漏报(FN=0)

Precision高不代表模型好!还要看Recall

召回率(Recall / Sensitivity / TPR)⭐⭐⭐⭐⭐

公式:

1
2
Recall = TP / (TP + FN)
= 实际为正的样本中,被正确预测的比例

直观理解:

1
2
3
"实际是的,我找到了多少?"
"查全率"
"宁可误报,不要漏报"

癌症检测例子:

1
2
3
Recall = 8 / (8 + 2) = 8/10 = 80%

10个患癌的人中,检测出8个,漏了2

应用场景:

1
2
3
4
5
6
7
8
9
10
✅ 癌症/疾病检测
- 高Recall:不能漏掉病人(漏诊代价大)
- 宁可误诊,不要漏诊(误诊可以复查)

✅ 欺诈检测
- 高Recall:不能漏掉欺诈交易
- 误报可以人工核查

✅ 安全系统
- 高Recall:不能漏掉威胁

Precision vs Recall 的权衡 ⭐⭐⭐⭐⭐

根本矛盾:

1
2
3
4
5
6
7
8
9
提高阈值(更严格):
Precision ↑(预测的更准)
→ Recall ↓(漏报更多)

降低阈值(更宽松):
Precision ↓(误报更多)
→ Recall ↑(漏报更少)

鱼和熊掌不可兼得!

实际例子(癌症检测):

1
2
3
4
5
6
7
8
9
阈值0.9(非常确信才报阳性):
TP=5, FP=1, FN=5
Precision = 5/6 = 83.3% ← 高
Recall = 5/10 = 50% ← 低

阈值0.3(比较宽松):
TP=9, FP=10, FN=1
Precision = 9/19 = 47.4% ← 低
Recall = 9/10 = 90% ← 高

PR曲线(Precision-Recall Curve):

1
2
3
4
5
横轴:Recall
纵轴:Precision

曲线下面积(AUC-PR):越大越好
理想模型:右上角(高PR

F1 Score(调和平均)⭐⭐⭐⭐⭐

公式:

1
2
F1 = 2 * (Precision * Recall) / (Precision + Recall)
= 2TP / (2TP + FP + FN)

为什么用调和平均而非算术平均?

1
2
3
4
5
6
7
8
9
10
11
算术平均:(P + R) / 2
调和平均:2PR / (P + R)

调和平均的特点:更重视较小的值

例子:
P=100%, R=10%
算术平均:(100 + 10) / 2 = 55% ← 虚高
调和平均:2*100*10 / (100+10) = 18.2% ← 真实反映R很低

只有PR都高,F1才高

癌症检测例子:

1
2
3
P = 61.5%, R = 80%
F1 = 2 * 0.615 * 0.8 / (0.615 + 0.8)
= 0.69669.6%

F-beta Score(加权版本):

1
2
3
4
5
F_β = (1 + β²) * (P * R) / (β² * P + R)

β=1: F1 Score(PR同等重要)
β=2: F2 Score(R的权重是P2倍)更关注Recall
β=0.5: F0.5 Score(P的权重是R2倍)更关注Precision

应用场景选择:

1
2
3
癌症检测:用F2(宁可误诊,不要漏诊)
垃圾邮件:用F0.5(宁可漏报,不要误判)
平衡任务:用F1

特异度(Specificity / TNR)⭐⭐⭐

公式:

1
2
Specificity = TN / (TN + FP)
= 实际为负的样本中,被正确预测的比例

与Recall对称:

1
2
Recall(敏感度):正类中找出多少
Specificity(特异度):负类中找出多少

癌症检测例子:

1
2
3
Specificity = 85 / (85 + 5) = 85/9094.4%

90个健康人中,85个被正确识别为健康

ROC曲线和AUC ⭐⭐⭐⭐⭐

ROC(Receiver Operating Characteristic):

1
2
3
4
横轴:FPR (False Positive Rate) = FP / (FP + TN) = 1 - Specificity
纵轴:TPR (True Positive Rate) = TP / (TP + FN) = Recall

不同阈值下的(FPR, TPR)点连线

AUC(Area Under Curve):

1
2
3
4
5
6
7
8
9
ROC曲线下面积
范围:[0, 1]

AUC = 1.0:完美分类器
AUC = 0.9-1.0:优秀
AUC = 0.8-0.9:良好
AUC = 0.7-0.8:一般
AUC = 0.5:随机猜测(无用)
AUC < 0.5:比随机还差(预测反了)

为什么AUC好?

1
2
3
✅ 不受类别不平衡影响
✅ 不需要选择阈值
✅ 评估模型整体性能

直观理解AUC:

1
2
3
4
随机选1个正样本和1个负样本
AUC = 正样本得分 > 负样本得分的概率

AUC=0.990%概率正样本排在负样本前面

计算例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5个样本:
真实:[1, 0, 1, 0, 1]
预测:[0.9, 0.8, 0.7, 0.3, 0.2]

按预测排序:
样本 真实 预测
1 1 0.9 ← 正
2 0 0.8 ← 负
3 1 0.7 ← 正
4 0 0.3 ← 负
5 1 0.2 ← 正(预测错了)

正负配对:3个正 × 2个负 = 6对
正样本排在负样本前:
(1,2)✅ (1,4)✅ (3,2)❌ (3,4)✅ (5,2)❌ (5,4)❌
4对正确

AUC ≈ 4/6 = 0.67

回归任务指标

MAE (Mean Absolute Error) ⭐⭐⭐⭐

1
2
3
4
MAE = (1/n) Σ|y - ŷ|

直观:平均误差
单位:与目标变量相同

MSE (Mean Squared Error) ⭐⭐⭐⭐

1
2
3
MSE = (1/n) Σ(y - ŷ)²

特点:惩罚大误差

RMSE (Root MSE) ⭐⭐⭐⭐

1
2
3
RMSE = √MSE

优点:单位与目标变量相同,更直观

R² (决定系数 / Coefficient of Determination) ⭐⭐⭐⭐⭐

公式:

1
2
3
4
5
6
7
R² = 1 - SS_res / SS_tot

其中:
SS_res = Σ(y - ŷ)² (残差平方和)
SS_tot = Σ(y - ȳ)² (总平方和)

ȳ: 真实值的平均值

直观理解:

1
2
3
4
5
R² = 模型解释的方差 / 总方差

R² = 1.0:完美预测
R² = 0.0:和预测平均值一样差
R² < 0.0:比预测平均值还差

实际例子:房价预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
真实房价:[100, 150, 200, 250, 300]万
平均值:ȳ = 200

模型1预测:[110, 140, 190, 260, 300]
SS_res = (100-110)² + ... + (300-300)² = 700
SS_tot = (100-200)² + ... + (300-200)² = 25000
R² = 1 - 700/25000 = 0.972 ← 优秀

模型2(总是预测平均值):[200, 200, 200, 200, 200]
SS_res = SS_tot = 25000
R² = 1 - 25000/25000 = 0 ← 无用

模型3(很差):[300, 100, 300, 100, 100]
SS_res = 50000
R² = 1 - 50000/25000 = -1 ← 负数,比平均值还差

调整R² (Adjusted R²):

1
2
3
4
5
6
7
8
9
考虑特征数量的惩罚

R²_adj = 1 - (1-R²) * (n-1) / (n-p-1)

n: 样本数
p: 特征数

特征越多,惩罚越大
防止过拟合(添加无用特征)

多分类指标

Macro Average(宏平均)⭐⭐⭐⭐

1
2
3
4
5
6
7
8
分别计算每个类的指标,然后平均

Macro-P = (P₁ + P₂ + ... + Pₙ) / n
Macro-R = (R₁ + R₂ + ... + Rₙ) / n
Macro-F1 = (F1₁ + F1₂ + ... + F1ₙ) / n

特点:每个类同等重要(不管样本数)
适合:关心每个类的表现

Micro Average(微平均)⭐⭐⭐⭐

1
2
3
4
5
6
7
8
先累加所有TP、FP、FN,再计算

Micro-P = Σ TP / (Σ TP + Σ FP)
Micro-R = Σ TP / (Σ TP + Σ FN)
Micro-F1 = 2 * Micro-P * Micro-R / (Micro-P + Micro-R)

特点:样本多的类影响大
适合:关心整体准确性

Weighted Average(加权平均)⭐⭐⭐

1
2
3
4
5
6
7
8
按各类样本数加权

Weighted-F1 = Σ (n_i / n) * F1_i

n_i: 第i类样本数
n: 总样本数

特点:考虑类别分布

三种平均对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
3分类问题:
类别A:100个样本,F1=0.9
类别B:100个样本,F1=0.8
类别C:10个样本,F1=0.3(表现差)

Macro-F1 = (0.9 + 0.8 + 0.3) / 3 = 0.67
→ C虽然样本少,但拉低了整体

Micro-F1 ≈ 0.86
→ C样本少,影响小

Weighted-F1 = (100*0.9 + 100*0.8 + 10*0.3) / 210 ≈ 0.82
→ 考虑样本分布的折中

指标选择指南

场景 推荐指标 原因
类别平衡分类 Accuracy, F1 简单直观
类别不平衡 AUC, F1 不受分布影响
不能漏报 Recall, F2 癌症检测、欺诈
不能误报 Precision, F0.5 垃圾邮件
多分类(关心每类) Macro-F1 每类同等重要
多分类(关心整体) Micro-F1 大类主导
回归(直观) MAE, RMSE 可解释
回归(评估) 标准化指标

常见陷阱题 ⚠️

题目1:类别不平衡时,哪个指标最可靠?

  • A. Accuracy
  • B. Precision
  • C. AUC ✅
  • D. MSE

💡 解析:AUC不受类别分布影响,Accuracy会被多数类主导。


题目2:F1 Score是什么平均?

  • A. 算术平均
  • B. 几何平均
  • C. 调和平均 ✅
  • D. 加权平均

💡 解析:F1 = 2PR/(P+R)是调和平均,更重视较小值。


题目3:R²的取值范围是?

  • A. [0, 1]
  • B. (0, 1)
  • C. (-∞, 1] ✅
  • D. [0, ∞)

💡 解析:R²可以为负(比预测平均值还差),上限是1。


题目4:Recall关注什么?

  • A. 预测为正的有多少是真的
  • B. 实际为正的找到多少 ✅
  • C. 总体准确率
  • D. 负类识别率

💡 解析:Recall=TP/(TP+FN),关注实际正类中找到多少。

记忆技巧 🧠:

  • Precision = “我说的准不准” → 预测为正中有多少真正
  • Recall = “我找全了没” → 实际为正中找到多少
  • F1 = 调和平均 → 只有P和R都高才高
  • AUC = 排序能力 → 正样本排在负样本前的概率
  • R² = 解释了多少方差 → 1是完美,0是无用,负数是垃圾

十一、Attention变体与改进(前沿技术)⭐⭐⭐⭐⭐

为什么需要改进Attention?
标准Self-Attention的O(n²)复杂度是长序列的噩梦!处理100K token需要10GB内存。各种变体应运而生。

1. Sparse Attention(稀疏注意力)⭐⭐⭐⭐

核心思想:不是所有token都需要关注所有token

标准Attention问题:

1
2
3
4
5
每个token关注所有n个token
复杂度:O(n²)

1024 token: 1M注意力权重
4096 token: 16M注意力权重 ☠️

局部窗口注意力(Local Window):

1
2
3
4
5
每个token只关注前后wtoken

复杂度:O(n*w)

w=128: 从O(n²)降到O(128n) ✅

Longformer模式:

1
2
3
4
5
1. 滑动窗口(所有token
2. 全局token(如[CLS])关注所有token
3. 扩张窗口(跳跃式)

兼顾局部和全局

2. Linear Attention(线性注意力)⭐⭐⭐⭐

核心:用核技巧避免显式计算n×n矩阵

标准Attention:

1
2
Attention(Q,K,V) = softmax(QK^T)V
↑ n×n矩阵

Linear Attention:

1
2
3
4
用特征映射φ:
Attention(Q,K,V) = φ(Q)(φ(K)^T V)
↑ 先计算这个(d×d)
复杂度:O(nd²) ✅ 线性于n

代表:Performer、RWKV

3. Flash Attention(闪电注意力)⭐⭐⭐⭐⭐

核心:优化IO效率而非计算量

瓶颈分析:

1
2
3
4
5
6
7
8
GPU计算很快,但内存读写慢

标准实现:
1. 计算QK^T → 写入HBM
2. Softmax → 读HBM,写回HBM
3. 乘V → 读HBM

内存访问次数多 → 慢

Flash Attention:

1
2
3
4
5
分块计算,所有操作在SRAM完成
避免反复读写HBM

速度提升:2-4倍 🚀
内存减少:10-20

LLaMA 2、GPT-4使用

4. Cross Attention(交叉注意力)⭐⭐⭐⭐

用途:Encoder-Decoder架构

与Self-Attention区别:

1
2
3
4
5
6
7
8
Self-Attention:
Q, K, V都来自同一序列

Cross-Attention:
Q来自decoder(当前生成的)
K, V来自encoder(源输入)

应用:机器翻译、图像描述生成

例子:英译中

1
2
3
4
5
6
7
8
Encoder输入:"I love AI"
Decoder生成:"我爱",下一个字?

Cross-Attention:
Q="我爱"的表示 去查询
K,V="I love AI"的表示

找到"AI"相关性最高 → 生成"人工智能"

十二、正则化技术大全⭐⭐⭐⭐

1. Dropout详解⭐⭐⭐⭐⭐

训练时:

1
2
mask = (torch.rand(x.shape) > p).float()
out = x * mask / (1 - p) # 除以(1-p)保持期望

为什么除以(1-p)?

1
2
3
4
E[out] = E[x * mask / (1-p)]
= E[x] * E[mask] / (1-p)
= E[x] * (1-p) / (1-p)
= E[x] ✅ 期望不变

测试时:直接输出,不dropout

为什么有效?

1
2
3
1. 防止神经元共适应(co-adaptation)
2. 相当于训练2^n个子网络的集成
3. 增加鲁棒性

Dropout率选择:

1
2
3
4
全连接层:0.5(标准)
卷积层:0.1-0.2(较小)
RNN:0.2-0.3(不要太大)
输入层:0.2或不用

2. DropConnect⭐⭐⭐

1
2
3
4
Dropout:随机丢弃神经元
DropConnect:随机丢弃权重连接

mask作用在W上而非激活值上

3. Data Augmentation(数据增强)⭐⭐⭐⭐⭐

图像:

1
2
3
4
5
6
- 随机裁剪 (RandomCrop)
- 水平翻转 (HorizontalFlip)
- 颜色抖动 (ColorJitter)
- 随机旋转 (RandomRotation)
- Mixup:混合两张图
- CutMix:剪切粘贴

文本:

1
2
3
4
- 同义词替换
- 随机插入/删除
- 回译(翻译成外语再翻回来)
- EDA:Easy Data Augmentation

效果:

1
2
3
4
5
6
ImageNet训练:
无增强:Top-1 = 70%
标准增强:Top-1 = 76% ✅ +6%
AutoAugment:Top-1 = 77.6% ✅ +7.6%

增强是最便宜的正则化!

4. Early Stopping⭐⭐⭐⭐

原理:

1
2
3
4
训练集损失持续下降
验证集损失先降后升 ← 过拟合开始

在验证集最优点停止训练

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
best_val_loss = float('inf')
patience = 10 # 容忍10个epoch不改善
counter = 0

for epoch in range(num_epochs):
train_loss = train_one_epoch()
val_loss = validate()

if val_loss < best_val_loss:
best_val_loss = val_loss
save_checkpoint()
counter = 0
else:
counter += 1

if counter >= patience:
print("Early stopping!")
break

5. Label Smoothing⭐⭐⭐⭐

硬标签问题:

1
2
3
4
5
6
标签:[0, 1, 0](one-hot)
模型预测:[0.05, 0.9, 0.05]

交叉熵鼓励:[0, 1, 0]
→ 模型过于自信
→ 泛化差

Label Smoothing:

1
2
3
4
5
6
7
8
9
ε = 0.1(平滑系数)
K = 3(类别数)

软化标签:
原始:[0, 1, 0]
平滑:[(0+ε/K), (1-ε+ε/K), (0+ε/K)]
= [0.033, 0.933, 0.033]

不要求模型100%确信

效果:

1
2
3
ImageNet(ResNet-50):
无Label Smoothing:Top-1 = 76.5%
ε=0.1:Top-1 = 77.2% ✅ +0.7%

十三、经典问题快速问答 ⭐⭐⭐⭐

Q1: 为什么要做特征归一化?⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 加速梯度下降收敛
- 不同尺度特征梯度差异大
- 归一化后等高线更圆,收敛更快

2. 避免某些特征主导训练
- 特征1范围[0,1],特征2范围[0,1000]
- 不归一化,特征2主导权重更新

3. 提高数值稳定性
- 避免梯度爆炸/消失
- 激活函数工作在合理区间

常用方法:
- StandardScaler: (x-μ)/σ
- MinMaxScaler: (x-min)/(max-min)
- RobustScaler: 用中位数和四分位数(对异常值鲁棒)

Q2: Batch Size如何选择?⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
小batch(32-64):
✅ 泛化好(噪声起正则化作用)
✅ 内存占用小
❌ 训练慢
❌ BN统计量不准
❌ GPU利用率低

大batch(256-1024):
✅ 训练快(并行化)
✅ BN统计量准确
✅ GPU利用率高
❌ 泛化差(收敛到尖锐最优)
❌ 需要更大学习率

推荐策略:
- CNN图像分类:64-256
- Transformer/LLM:越大越好(受内存限制,常用16-32
- RNN:32-128
- 先从小batch开始,GPU有余量再增大

Q3: 过拟合vs欠拟合⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
过拟合(Overfitting):
表现:训练集准确率高,验证集准确率低
原因:模型太复杂,记住了训练数据
解决:
1. 增加数据(最有效)✅
2. Dropout、L2正则化
3. 数据增强
4. Early Stopping
5. 减小模型复杂度(减层、减参数)

欠拟合(Underfitting):
表现:训练集和验证集准确率都低
原因:模型太简单,学不到规律
解决:
1. 增加模型复杂度(加层、加参数)
2. 减少正则化
3. 训练更多epoch
4. 增加特征
5. 降低Dropout率

Q4: 为什么Transformer要用√d_k缩放?⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
数学原因:
QK^T的方差 = d_k(维度)
d_k=64: 方差64,标准差8
点积结果可能很大(如20)

softmax([20, 5, 3])
= [0.9999, 0.0001, 0.00001] ← 接近one-hot

问题:
1. 梯度消失(softmax饱和区)
2. 训练困难

除以√d_k后:
方差 = d_k / d_k = 1 ✅
分布稳定,梯度正常

Q5: Adam和SGD如何选择?⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Adam优势:
✅ 开箱即用,默认参数好
✅ 收敛快(2-3倍)
✅ 适合稀疏梯度(NLP)
✅ 适合高维参数空间

SGD+Momentum优势:
✅ 泛化能力强
✅ 最终精度高(图像分类)
✅ 收敛到平坦最优(鲁棒)

实战策略:
1. 快速原型 → Adam
2. NLP/Transformer → AdamW(必选)
3. CNN图像分类 → SGD+Momentum
4. 不确定 → 先试Adam
5. 追求极致精度 → 用Adam预训练,SGD精调

Q6: 为什么BERT用[CLS]token?⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
原因:
1. 聚合整个句子信息
- [CLS]通过Self-Attention与所有token交互
- 最后一层的[CLS]包含全局信息

2. 任务无关的句子表示
- 可用于分类、匹配等任务
- 不依赖特定词的位置

3. 训练目标对齐
- NSP(Next Sentence Prediction)任务用[CLS]

为什么不用平均池化?
- 平均池化权重固定(1/n)
- [CLS]的注意力权重可学习
- 更灵活,效果更好

Q7: 什么是Teacher Forcing?⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
场景:序列生成(如翻译)

Teacher Forcing(训练):
输入:"I love AI"
目标:"我 爱 人工智能"

步骤1:输入"我"(真实标签),预测"爱"
步骤2:输入"爱"(真实标签),预测"人工智能"
用真实标签作为下一步输入

自由生成(测试):
步骤1:输入"我"(真实),预测"爱"
步骤2:输入"爱"(模型预测),预测下一个
用模型预测作为下一步输入

问题:训练-测试不一致(Exposure Bias)

解决:
1. Scheduled Sampling:训练时逐渐混合真实标签和预测
2. Beam Search:测试时搜索多条路径

Q8: Attention中Q、K、V的含义?⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
类比数据库查询:

Query(查询):"我要找什么"
例:搜索"Python教程"

Key(索引):"我是什么"
例:文档标题、关键词

Value(内容):"我的具体内容"
例:文档正文

Attention机制:
1. Q和所有K计算相似度(QK^T
2. 相似度高的K对应的V权重大
3. 加权求和所有V

翻译例子:"The cat is on the mat"
翻译"猫"时:
Q = "猫"的表示
K = ["The", "cat", "is", "on", "the", "mat"]的表示
V = 对应的语义内容

QK^T找到"cat"相似度最高
"cat"V(语义)来生成翻译

十四、卷积神经网络(CNN)核心概念⭐⭐⭐⭐

卷积层参数计算⭐⭐⭐⭐⭐

输出尺寸公式(必考):

1
2
3
4
5
6
7
8
9
Output_size = (Input_size - Kernel_size + 2*Padding) / Stride + 1

例:
输入:32×32
卷积核:3×3
Padding:1
Stride:1

输出:(32 - 3 + 2*1) / 1 + 1 = 32 (保持尺寸)

参数量计算:

1
2
3
4
5
6
7
Conv2d(in_channels, out_channels, kernel_size)

参数量 = kernel_size² × in_channels × out_channels + out_channels
↑ 权重 ↑ bias

例:Conv2d(64, 128, 3)
参数 = 3² × 64 × 128 + 128 = 73,856

感受野计算:

1
2
3
4
5
L层后的感受野:
RF = 1 + Σ(kernel_size - 1) × ∏stride

两个3×3卷积 = 一个5×5卷积(感受野相同)
但参数更少:2×(3²) = 18 vs 5² = 25

1×1卷积的作用⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 降维/升维(改变通道数)
输入:256通道 → 1×1卷积(64) → 64通道 ✅ 减少计算

2. 增加非线性
1×1卷积 + ReLU = 额外的非线性变换

3. 跨通道信息融合
不同通道的同一位置信息交互

应用:
- GoogLeNet的Inception模块
- ResNet的bottleneck结构
- MobileNet的深度可分离卷积

深度可分离卷积⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
标准卷积分解为:

1. Depthwise卷积(逐通道)
每个输入通道独立卷积
参数:kernel_size² × in_channels

2. Pointwise卷积(1×1卷积)
跨通道融合
参数:in_channels × out_channels

参数减少:
标准卷积:3² × 64 × 128 = 73,728
深度可分离:3² × 64 + 64 × 128 = 576 + 8,192 = 8,768
减少约8.4倍! 🚀

MobileNet、EfficientNet使用

池化层⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Max Pooling(最大池化):
- 取窗口内最大值
- 保留最显著特征
- 常用于CNN

Average Pooling(平均池化):
- 取窗口内平均值
- 更平滑
- 常用于全局池化(GAP)

Global Average Pooling:
- 每个通道求平均,输出1个值
- 替代全连接层,减少参数
- 防止过拟合

十五、预训练模型与迁移学习⭐⭐⭐⭐

预训练策略

自监督学习(Self-Supervised Learning):

1
2
3
4
5
6
7
8
9
10
11
12
13
不需要人工标注,从数据本身生成监督信号

BERT的预训练任务:
1. MLM(Masked Language Model)
输入:"我[MASK]人工智能"
预测:[MASK] = "爱"

2. NSP(Next Sentence Prediction)
判断句子B是否是句子A的下一句

GPT的预训练任务:
- 语言建模(Language Modeling)
给定前文预测下一个词

微调策略⭐⭐⭐⭐⭐

Feature Extraction(特征提取):

1
2
3
4
5
6
7
8
9
冻结预训练层,只训练新加的层

适用:
- 数据量小
- 任务相似

实现:
for param in model.bert.parameters():
param.requires_grad = False

Fine-tuning(微调):

1
2
3
4
5
6
7
8
9
解冻部分或全部层,小学习率训练

策略:
- 数据少:只微调顶层
- 数据多:微调全部层

学习率:
预训练层:1e-5(小)
新增层:1e-3(大)

逐层解冻(Gradual Unfreezing):

1
2
3
4
5
6
ULMFiT策略:
Epoch 1-5:只训练顶层
Epoch 6-10:解冻倒数第2层
Epoch 11-15:解冻倒数第3层
...
逐步解冻,防止灾难性遗忘

十六、数值稳定技巧(工程实践)⭐⭐⭐⭐

1. Log-Sum-Exp技巧⭐⭐⭐⭐⭐

问题:

1
2
log(exp(x1) + exp(x2) + ...)
# 如果x很大,exp(x)会溢出

解决:

1
2
3
4
max_x = max(x)
log(exp(x1-max_x) + exp(x2-max_x) + ...) + max_x

# 数学等价,但数值稳定

应用:Softmax的稳定计算

2. Softmax数值稳定⭐⭐⭐⭐⭐

1
2
3
4
5
6
7
8
def stable_softmax(x):
x = x - x.max() # 防止exp溢出
exp_x = np.exp(x)
return exp_x / exp_x.sum()

# 为什么减max不改变结果?
# softmax(x) = exp(x) / Σexp(x)
# = exp(x-c) / Σexp(x-c) 对任意常数c

3. 权重初始化⭐⭐⭐⭐⭐

Xavier/Glorot初始化(Sigmoid/Tanh):

1
2
3
4
5
W ~ Uniform(-√(6/(n_in+n_out)), √(6/(n_in+n_out)))
# 或
W ~ Normal(0, √(2/(n_in+n_out)))

目标:保持激活值方差稳定

He初始化(ReLU):

1
2
3
4
W ~ Normal(0, √(2/n_in))

考虑ReLU砍掉一半神经元
需要更大的初始方差

为什么重要:

1
2
3
4
5
6
7
初始化不当 → 激活值/梯度消失或爆炸 → 训练失败

实验(10层网络):
0初始化:Loss不动(对称性问题)
过大初始化:Loss = NaN(爆炸)
过小初始化:梯度消失,前几层不学习
He初始化:正常收敛 ✅

十七、批归一化详解(BN)⭐⭐⭐⭐⭐

Batch Normalization工作原理

训练时:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 对每个mini-batch
μ = x.mean(dim=0) # 计算batch均值
σ² = x.var(dim=0) # 计算batch方差

# 归一化
x_norm = (x - μ) / √(σ² + ε)

# 缩放和平移(可学习)
out = γ * x_norm + β

# 更新全局统计量(指数移动平均)
running_mean = momentum * running_mean + (1-momentum) * μ
running_var = momentum * running_var + (1-momentum) * σ²

测试时:

1
2
3
# 使用训练时累积的全局统计量
x_norm = (x - running_mean) / √(running_var + ε)
out = γ * x_norm + β

BN的放置位置⭐⭐⭐⭐

两种顺序:

1
2
3
4
5
方式1(原论文):
Conv → BN → ReLU

方式2(更常用):
Conv → BN → ReLU

ResNet V2改进:

1
2
3
4
原版:Conv → BN → ReLU → Conv → BN
V2: BN → ReLU → Conv → BN → ReLU → Conv

Pre-activation,训练更稳定

BN的优点和缺点

优点:

1
2
3
4
5
✅ 加速训练(2-3倍)
✅ 允许更大学习率
✅ 减少对初始化的敏感
✅ 轻微正则化作用
✅ 缓解梯度消失

缺点:

1
2
3
4
❌ 对batch size敏感(batch<16效果差)
❌ 训练和测试行为不一致
❌ RNN中难以使用
❌ 增加计算开销

BN的替代方案

Layer Normalization(Transformer用):

1
2
feature维度归一化,不依赖batch
适合序列模型

Group Normalization:

1
2
3
将通道分组,组内归一化
batch size=1也能用
目标检测常用

Instance Normalization:

1
2
每个样本每个通道独立归一化
风格迁移常用

十八、学习率调度策略⭐⭐⭐⭐

常用调度器

StepLR(阶梯衰减):

1
2
3
4
5
6
7
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
# 每30个epoch,学习率乘0.1

例:初始lr=0.1
Epoch 0-29: lr=0.1
Epoch 30-59: lr=0.01
Epoch 60-89: lr=0.001

CosineAnnealingLR(余弦退火):

1
2
3
4
scheduler = CosineAnnealingLR(optimizer, T_max=100)
# 学习率按余弦曲线从初始值降到0

优点:平滑衰减,训练后期精调

ReduceLROnPlateau(自适应):

1
2
3
4
5
6
7
8
9
scheduler = ReduceLROnPlateau(
optimizer,
mode='min',
factor=0.5,
patience=5
)
# 验证集损失5个epoch不降,学习率减半

智能,但需要监控指标

Warm-up + Cosine(大模型标配):

1
2
3
4
5
6
7
1000步:线性增长 0.01×lrlr
然后:余弦衰减 lr0.01×lr

原因:
- 训练初期统计量不稳定
- 小学习率热身
- Adam + Warm-up = 必备组合

学习率查找(Learning Rate Finder)

策略:

1
2
3
4
5
6
1. 从很小的lr开始(如1e-7)
2. 每个batch指数增长
3. 记录loss变化
4. 绘制lr vs loss曲线

选择:loss下降最快处的lr

实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def find_lr(model, train_loader, optimizer, criterion, device):
lrs = []
losses = []

lr = 1e-7
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)

# 设置学习率
for param_group in optimizer.param_groups:
param_group['lr'] = lr

# 训练一步
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()

# 记录
lrs.append(lr)
losses.append(loss.item())

# 增长学习率
lr *= 1.1

# 如果loss爆炸,停止
if loss.item() > losses[0] * 4:
break

# 绘制
plt.plot(lrs, losses)
plt.xscale('log')
plt.xlabel('Learning Rate')
plt.ylabel('Loss')
plt.title('Learning Rate Finder')
plt.show()

# 使用
find_lr(model, train_loader, optimizer, criterion, device)
# 从图中找到下降最快的lr,通常选择该点的1/10作为最大lr

解读LR曲线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Loss
|
| ╲
| ╲___ ← 最佳lr在这里(斜率最大处)
| ╲___
| ╲___
| ╲___
| ╲___
| ╲ ← loss开始上升
+---------------------------→ lr (log scale)
1e-7 1e-5 1e-3 1e-1

选择:最陡下降处,通常是loss爆炸前的1个数量级
例:loss在lr=0.1处爆炸,选择0.01

十九、Transformer变体大全(前沿架构)⭐⭐⭐⭐⭐

为什么需要Transformer变体?
原始Transformer的O(n²)复杂度限制了长序列处理。Longformer、Reformer、Performer等变体将复杂度降到O(n)或O(n log n),使得处理100K+ token成为可能。

1. Longformer(长文档Transformer)⭐⭐⭐⭐⭐

核心创新:稀疏注意力模式

三种注意力组合:

1
2
3
4
5
6
7
8
9
10
11
1. 局部窗口注意力(Sliding Window
每个token关注前后w个token(如w=512
复杂度:O(n*w)

2. 扩张窗口注意力(Dilated Window
跳跃式关注,捕获长距离依赖
例:关注间隔为d的token

3. 全局注意力(Global Attention)
特定token(如[CLS]、问题token)关注所有token
其他token也关注这些全局token

可视化注意力模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
标准Attention(密集):
Token1: [✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓] 每个关注所有
Token2: [✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓]
Token3: [✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓]
...
复杂度:O(n²)

Longformer(稀疏):
Token1: [✓ ✓ ✓ - - - - G] 局部+全局
Token2: [✓ ✓ ✓ ✓ - - - G]
Token3: [- ✓ ✓ ✓ ✓ - - G]
Token4: [- - ✓ ✓ ✓ ✓ - G]
...
G: 全局token,关注所有
复杂度:O(n*w) + O(n*g),其中g是全局token数

参数量计算:

1
2
3
4
5
6
7
8
9
10
11
假设序列长度n=4096,窗口w=512,全局token数g=8

标准Attention注意力矩阵:
4096 × 4096 = 16,777,216个连接

Longformer注意力矩阵:
局部:4096 × 512 = 2,097,152
全局:4096 × 8 × 2 = 65,536(双向)
总计:≈ 2,162,688个连接

内存减少:16,777,216 / 2,162,6887.8倍! 🚀

应用场景:

1
2
3
4
5
6
7
8
9
✅ 长文档分类(论文、法律文件)
✅ 文档问答(跨段落推理)
✅ 书籍级别的文本生成
✅ 代码理解(完整文件)

实验结果(文档分类):
序列长度4096
- BERT(截断到512):F1 = 82.3%
- Longformer:F1 = 89.1% ✅ +6.8%

常见陷阱题 ⚠️:

题目:Longformer的复杂度是?

  • A. O(n²)
  • B. O(n log n)
  • C. O(n*w) ✅(w是窗口大小)
  • D. O(n)

💡 解析:局部窗口注意力是O(n*w),w是固定窗口大小(如512)。


2. Reformer(高效Transformer)⭐⭐⭐⭐⭐

两大创新:

创新1:Locality-Sensitive Hashing (LSH) Attention

标准Attention问题:

1
2
计算QK^T需要n×n次点积
大部分注意力权重很小(接近0),计算浪费

LSH Attention思想:

1
2
3
4
5
6
7
只计算Query和相似的Key之间的注意力
用哈希函数快速找到相似的Key

原理:
1.LSHQK映射到桶(bucket
2. 只计算同一桶内的Q-K注意力
3. 大部分不相似的Q-K对被跳过

LSH哈希示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假设用随机投影哈希:
h(x) = sign(x · r) 其中r是随机向量

Query和Key:
q1: [0.8, 0.3]h(q1) = +1 → 桶A
q2: [0.2, 0.9]h(q2) = +1 → 桶A
k1: [0.7, 0.4]h(k1) = +1 → 桶A
k2: [-0.5, 0.1]h(k2) = -1 → 桶B

只计算:
q1和k1(同在桶A)
q2和k1(同在桶A)
跳过q1/q2和k2(不同桶)

复杂度:O(n log n) ✅ 远小于O(n²)

多轮哈希提高准确率:

1
2
3
4
5
6
7
单轮哈希可能漏掉相似的Key
使用多轮(如4轮)不同的哈希函数
取并集

准确率提升:
1轮:90%的相似Key被找到
4轮:99%的相似Key被找到 ✅

创新2:Reversible Layers(可逆层)

标准Transformer内存问题:

1
2
3
4
5
6
反向传播需要存储所有中间激活值
N层Transformer,内存∝N

例:BERT-Large24层)
每层激活值:100MB
总内存:2.4GB(只是激活值!)

可逆层原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
传统层:y = F(x)
反向传播需要存储x

可逆层(RevNet):
输入分成两半:x1, x2
输出:
y1 = x1 + F(x2)
y2 = x2 + G(y1)

反向传播时可以恢复输入:
x2 = y2 - G(y1)
x1 = y1 - F(x2)

不需要存储中间激活值!

内存节省:

1
2
3
4
5
6
7
标准Transformer:内存∝N(层数)
Reformer:内存∝1(常数,只存最后一层)

例:16层Reformer
标准:1.6GB激活值
可逆:100MB激活值
减少16倍! 🚀

Reformer总体效果:

1
2
3
4
5
6
7
序列长度64K
标准Transformer:内存超限,无法训练
Reformer:内存占用降低8倍,可训练 ✅

速度:
标准:无法运行
Reformer:可以在单GPU训练长序列

常见陷阱题 ⚠️:

题目:Reformer的LSH Attention复杂度是?

  • A. O(n²)
  • B. O(n log n) ✅
  • C. O(n)
  • D. O(log n)

💡 解析:LSH哈希排序需要O(n log n),之后只计算同桶内的注意力。


3. Linformer(线性Transformer)⭐⭐⭐⭐

核心洞察:注意力矩阵是低秩的

低秩近似:

1
2
3
4
5
6
7
8
9
标准Attention:
A = softmax(QK^T) ∈ R^(n×n)

观察:A的秩远小于n
例:n=512,实际秩≈64

低秩近似:
A ≈ Ê F̂^T
其中Ê, F̂ ∈ R^(n×k),k << n

Linformer方法:

1
2
3
4
5
6
7
8
9
KV投影到更低维度:

K̄ = K × ER^(k×d) 其中ER^(n×k)
V̄ = V × FR^(k×d)

Attention(Q,K,V) = softmax(Q K̄^T) V̄
n×k矩阵(不是n×n

复杂度:O(nk)O(n) ✅ 线性!

投影矩阵E的选择:

1
2
3
4
5
6
7
8
9
10
方法1:可学习投影
E是可学习参数,通过训练优化

方法2:平均池化投影
ntoken分成k组,每组平均
E[i,j] = 1/group_size if j在第i
计算快,无额外参数

方法3:卷积投影
1D卷积降采样KV

参数量对比:

1
2
3
4
5
6
7
8
9
序列长度n=512,k=64(压缩8倍)

标准Attention注意力矩阵:
512 × 512 = 262,144

Linformer注意力矩阵:
512 × 64 = 32,768

减少8倍! 🚀

实验结果:

1
2
3
4
5
6
7
8
9
BERT预训练(MLM任务):

序列长度512:
标准BERT:困惑度3.21
Linformer(k=256):困惑度3.23 ✅ 几乎无损

序列长度4096(长序列):
标准BERT:OOM(内存不足)
Linformer(k=256):可运行,困惑度3.45 ✅

优缺点:

1
2
3
4
5
6
7
8
9
优点:
✅ 复杂度真正线性O(n)
✅ 实现简单
✅ 几乎无精度损失

缺点:
k需要预先指定(不同长度可能需要不同k
❌ 理论假设(低秩)不一定总成立
❌ 对某些任务效果不如全注意力

4. Performer(快速Transformer)⭐⭐⭐⭐⭐

核心:用核方法(Kernel Method)近似Attention

标准Attention回顾:

1
2
3
4
Attention(Q,K,V) = softmax(QK^T)V
= [exp(q_i·k_j) / Σ_j exp(q_i·k_j)] V

问题:需要显式计算n×nexp(q_i·k_j)矩阵

Performer的数学技巧:

1
2
3
4
5
6
7
8
用特征映射φ近似exp:
exp(q·k) ≈ φ(q)^T φ(k)

则:
Attention(Q,K,V) ≈ φ(Q) [φ(K)^T V]
↑ 先计算这个(m×d矩阵)

关键:改变计算顺序!

复杂度对比:

1
2
3
4
5
6
7
8
9
标准:softmax(QK^T) V
O(n²d) ↑ O(n²d)
总:O(n²d)

Performer:φ(Q) [φ(K)^T V]
O(nmd)
总:O(nmd),m << n(如m=256

m固定时,复杂度是O(nd),线性!

特征映射φ的选择:

1
2
3
4
5
6
7
8
9
10
FAVOR+(Fast Attention Via Orthogonal Random features):

φ(x) = exp(||x||²/2) [exp(w_1^T x), exp(w_2^T x), ..., exp(w_m^T x)]

其中w_i是随机正交向量

性质:
- 无偏估计:E[φ(q)^T φ(k)] = exp(q^T k)
- 低方差:多个随机特征平均,方差降低
- 正交性:减少相关性,提高准确率

m(特征数)的影响:

1
2
3
4
5
6
7
8
9
m越大,近似越准确,但计算越慢

实验(WMT翻译):
m=64: BLEU=27.8, 速度快
m=128: BLEU=28.3, 速度中
m=256: BLEU=28.6, 接近标准Attention(28.7)✅
m=512: BLEU=28.7, 但慢一些

推荐:m=256(平衡点)

实际性能:

1
2
3
4
5
6
7
蛋白质序列建模(长度8192):

标准Transformer:OOM(内存溢出)
Performer:
- 训练速度:快3倍
- 内存占用:降低5倍
- 精度:与标准Transformer相当 ✅

代码实现(简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def performer_attention(Q, K, V, m=256):
"""
Q, K, V: (batch, n, d)
m: 随机特征数
"""
n, d = Q.shape[1], Q.shape[2]

# 生成随机正交特征
W = torch.randn(d, m)
W = torch.qr(W)[0] # 正交化

# 特征映射
def feature_map(X):
# φ(x) = exp(-||x||²/2) * exp(W^T x)
norm = (X ** 2).sum(dim=-1, keepdim=True) / 2
proj = torch.matmul(X, W) # (batch, n, m)
return torch.exp(proj - norm)

Q_prime = feature_map(Q) # (batch, n, m)
K_prime = feature_map(K) # (batch, n, m)

# 线性Attention: Q' [(K')^T V]
KV = torch.matmul(K_prime.transpose(-2, -1), V) # (batch, m, d)
out = torch.matmul(Q_prime, KV) # (batch, n, d)

# 归一化
normalizer = torch.matmul(Q_prime, K_prime.sum(dim=1, keepdim=True).transpose(-2, -1))
out = out / (normalizer + 1e-8)

return out

优缺点总结:

1
2
3
4
5
6
7
8
9
10
优点:
✅ 真正的线性复杂度O(nd)
✅ 理论严格(核方法)
✅ 可以处理超长序列(100K+)
Google开源,工程实现成熟

缺点:
❌ 近似误差(m越小误差越大)
❌ 对某些任务不如全注意力
❌ 随机特征需要仔细调优

5. FlashAttention(IO优化)⭐⭐⭐⭐⭐

核心思想:不是改变算法,而是优化实现

GPU内存层次:

1
2
3
4
5
6
7
8
9
10
11
SRAM(片上内存):
- 容量:小(<20MB)
- 速度:超快(19 TB/s)
- 访问延迟:极低

HBM(高带宽内存/显存):
- 容量:大(16-80GB)
- 速度:慢(1.5 TB/s)
- 访问延迟:高

瓶颈:HBM访问是主要耗时!

标准Attention实现的问题:

1
2
3
4
5
6
7
8
9
10
11
步骤1:计算S = QK^T
Q从HBM读 → 计算 → S写入HBM

步骤2:计算P = softmax(S)
S从HBM读 → 计算 → P写入HBM

步骤3:计算O = PV
P从HBM读,V从HBM读 → 计算 → O写入HBM

HBM访问次数:5次(Q, K, S, P, V各1次,plus中间结果)
IO成为瓶颈!

FlashAttention的IO优化:

1
2
3
4
5
6
7
8
9
核心:分块计算,所有操作在SRAM完成

算法:
1. 将Q, K, V分成块(block)
2. 每次加载一个块到SRAM
3. 在SRAM内完成QK^T, softmax, 乘V
4. 只将最终结果写回HBM

HBM访问次数:大幅减少(只读Q, K, V,只写O)

分块计算原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
关键技巧:在线Softmax(Online Softmax)

标准Softmax需要两遍:
1遍:计算maxsum(需要看到所有元素)
2遍:归一化

在线Softmax:边读边更新maxsum
只需一遍!

伪代码:
m = -∞ # 当前最大值
l = 0 # 当前sum
o = 0 # 当前输出

for block in K_blocks:
# 计算当前块的QK^T
s = Q @ block.K^T

# 更新全局max和sum
m_new = max(m, max(s))
l = l * exp(m - m_new) + sum(exp(s - m_new))
o = o * exp(m - m_new) + exp(s - m_new) @ block.V
m = m_new

o = o / l # 最终归一化

性能提升:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
序列长度2048,BERT-Large:

标准实现:
- 前向传播:120ms
- 反向传播:250ms
- 内存:16GB

FlashAttention:
- 前向传播:35ms ✅ 3.4倍加速
- 反向传播:80ms ✅ 3.1倍加速
- 内存:8GB ✅ 减半

序列长度越长,加速越明显:
8192: 4倍加速
16384: 5倍加速

FlashAttention 2改进:

1
2
3
4
5
6
7
8
进一步优化:
1. 更好的并行策略(在序列维度并行)
2. 减少非矩阵乘法操作
3. 更好的工作分配

额外加速:
序列2048:2倍(相比FlashAttention 1)
序列16384:3倍

应用:

1
2
3
4
5
6
7
8
9
✅ GPT-4使用FlashAttention
✅ LLaMA 2使用
✅ Stable Diffusion使用
✅ 几乎所有现代大模型

原因:
- 无精度损失(数学等价)
- 纯工程优化,兼容所有模型
- 开源,易集成

常见陷阱题 ⚠️:

题目:FlashAttention相比标准实现的主要优势?

  • A. 降低算法复杂度
  • B. 减少参数量
  • C. 优化IO效率,减少HBM访问 ✅
  • D. 使用稀疏注意力

💡 解析:FlashAttention不改变算法(仍是O(n²)),只是优化实现减少内存访问。


Transformer变体对比总结 📊

变体 复杂度 主要技术 精度 适用场景 代表模型
标准Transformer O(n²) 全注意力 最高 短序列(<512) BERT, GPT-2
Longformer O(n*w) 稀疏注意力 长文档(4K-16K) Longformer ✅
Reformer O(n log n) LSH+可逆层 中高 超长序列(64K) Reformer
Linformer O(n) 低秩投影 固定长度 -
Performer O(n) 核方法 中高 可变长度 -
FlashAttention O(n²) IO优化 最高 所有场景 ✅ GPT-4, LLaMA

选择建议:

1
2
3
4
5
6
序列长度<1K:标准Transformer + FlashAttention
序列长度1K-8KLongformer + FlashAttention
序列长度8K-64KReformerPerformer
内存受限:FlashAttention(必用)
追求精度:标准Transformer + FlashAttention
追求速度:Performer

记忆技巧 🧠:

  • Longformer = “局部+全局” → 窗口+特殊token
  • Reformer = “哈希找朋友+可逆省内存” → LSH+RevNet
  • Linformer = “压缩KV” → 低秩近似
  • Performer = “核技巧线性化” → φ(Q)(φ(K)^T V)
  • FlashAttention = “IO优化王者” → 大模型必备
  • 复杂度记忆:O(n²) > O(n log n) > O(n*w) > O(n)

二十、大模型训练与推理优化⭐⭐⭐⭐⭐

为什么大模型需要特殊优化?
GPT-3有175B参数,FP32存储需700GB!单卡训练根本不可能。混合精度、模型并行、梯度累积等技术是训练大模型的必备武器。

1. 混合精度训练(Mixed Precision Training)⭐⭐⭐⭐⭐

核心思想:用FP16计算,用FP32存储

FP16的优势

1
2
3
4
5
6
7
8
9
10
11
12
13
FP32(单精度):32 bit = 1 sign + 8 exp + 23 mantissa
范围:±3.4×10³⁸
精度:7位有效数字

FP16(半精度):16 bit = 1 sign + 5 exp + 10 mantissa
范围:±65,504
精度:3位有效数字

优势:
✅ 内存减半(32bit → 16bit)
✅ 计算速度快2-3倍(Tensor Core加速)
✅ 带宽需求减半
✅ batch size可以翻倍

纯FP16训练的三大问题

问题1:梯度下溢(Underflow)

1
2
3
4
5
6
FP16最小正数:6×10⁻⁵
很多梯度 < 6×10⁻⁵ → 变成0

例:某参数梯度 = 1×10⁻⁶
FP32:正常存储
FP16:下溢变成0 → 参数不更新 ☠️

问题2:舍入误差累积

1
2
3
4
5
6
FP16精度低,多次累加误差累积

例:1000次梯度累加
每次误差:0.001
FP32累积误差:1.0(可接受)
FP16累积误差:可能更大,导致训练不稳定

问题3:权重更新精度不够

1
2
3
4
5
6
7
8
参数更新:W = W - lr × grad

W很大,lr × grad很小时:
FP16表示的WW-lr×grad可能相同(精度不够)
→ 权重不更新

例:W = 1024.0,lr × grad = 0.01
FP16: 1024.0 - 0.01 = 1024.0(精度不够表示差异)

混合精度训练的解决方案

核心策略:

1
2
3
4
5
1. 权重(W):保留FP32副本
2. 前向传播:用FP16计算(快)
3. 反向传播:用FP16计算梯度
4. 梯度:先缩放(Loss Scaling),再更新FP32权重
5. FP32权重转回FP16用于下一次前向传播

详细流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 伪代码
# 1. 初始化
W_fp32 = initialize_weights() # FP32主权重
W_fp16 = W_fp32.half() # FP16副本

scale = 1024 # 损失缩放因子

for batch in data:
# 2. 前向传播(FP16)
output = model_fp16(batch, W_fp16)
loss = criterion(output, target)

# 3. 损失缩放(防止梯度下溢)
scaled_loss = loss * scale

# 4. 反向传播(FP16)
scaled_loss.backward()
grad_fp16 = W_fp16.grad

# 5. 梯度还原和转换
grad_fp32 = grad_fp16.float() / scale

# 6. 梯度裁剪(FP32)
clip_grad_norm_(grad_fp32, max_norm=1.0)

# 7. 更新FP32主权重
W_fp32 = W_fp32 - lr * grad_fp32

# 8. 转换回FP16供下次使用
W_fp16 = W_fp32.half()

Loss Scaling(损失缩放)详解

为什么需要?

1
2
3
4
5
6
7
FP16能表示的最小正数:6×10⁻⁵

典型梯度分布:
99%的梯度在 [1×10⁻⁷, 0.1] 范围
很多小梯度会下溢变成0

解决:放大损失(和梯度)

静态缩放:

1
2
3
4
5
6
7
8
9
10
# 固定缩放因子
scale = 1024 # 或2048

scaled_loss = loss * scale
scaled_loss.backward()
grad = grad / scale # 还原

效果:
原始梯度:1×10⁻⁶
缩放后:1×10⁻⁶ × 1024 = 1.024×10⁻³ ✅ 不会下溢

动态缩放(推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# PyTorch实现
scaler = torch.cuda.amp.GradScaler()

for batch in data:
optimizer.zero_grad()

# 自动混合精度上下文
with torch.cuda.amp.autocast():
output = model(batch)
loss = criterion(output, target)

# 自动缩放和反向传播
scaler.scale(loss).backward()

# 自动还原梯度并更新
scaler.step(optimizer)

# 动态调整scale
scaler.update()

动态调整策略:
- 如果梯度正常:scale *= 2(每2000步)
- 如果梯度溢出(inf/nan):scale /= 2
- 自动找到最优scale

Scale调整逻辑:

1
2
3
4
5
6
7
8
9
10
初始:scale = 2^16 = 65536

迭代过程:
Step 1-2000:无溢出 → scale = 65536
Step 2001scale尝试翻倍 → 131072
Step 2100:检测到infscale减半 → 65536
Step 3000:连续2000步无溢出 → scale = 131072
...

最终收敛到合适的scale(如4096-8192

BF16(Brain Float 16)- 新趋势

BF16 vs FP16:

1
2
3
4
5
6
7
8
9
10
11
12
13
FP16:1 sign + 5 exp + 10 mantissa
- 范围小:±65,504
- 精度高:3位有效数字

BF16:1 sign + 8 exp + 7 mantissa
- 范围大:±3.4×10³⁸(和FP32一样)
- 精度中:2位有效数字

关键优势:
✅ FP32截断即可转BF16(只保留高16位)
✅ 不需要Loss Scaling(范围够大)
✅ 数值稳定性好
✅ Google TPU、NVIDIA A100原生支持

何时用BF16:

1
2
3
4
5
6
7
8
✅ 硬件支持BF16(A100, H100, TPU v4)
✅ 训练大模型(Transformer)
✅ 追求稳定性(不想调Loss Scale)

实验(BERT训练):
FP32: Baseline
FP16 + Loss Scaling: 0.5%精度损失
BF16: 0.1%精度损失 ✅ 更稳定

PyTorch使用:

1
2
3
4
5
6
7
8
# BF16混合精度
with torch.cuda.amp.autocast(dtype=torch.bfloat16):
output = model(input)
loss = criterion(output, target)

# 不需要GradScaler!
loss.backward()
optimizer.step()

混合精度训练效果

实验数据(ResNet-50,ImageNet):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FP32(Baseline):
- 训练时间:100小时
- 内存:8GB
- Top-1准确率:76.5%

FP16混合精度:
- 训练时间:45小时 ✅ 2.2倍加速
- 内存:4.5GB ✅ 减少44%
- Top-1准确率:76.4% ✅ 仅-0.1%
- Batch size:256 → 512(翻倍)

BF16混合精度:
- 训练时间:50小时 ✅ 2倍加速
- 内存:5GB
- Top-1准确率:76.5% ✅ 无损

BERT-Large预训练:

1
2
3
4
5
6
7
8
FP32:
- 内存:32GB/GPU
- 训练时间:1000 GPU小时

混合精度(FP16):
- 内存:16GB/GPU ✅ 减半
- 训练时间:400 GPU小时 ✅ 2.5倍加速
- 可以用更大batch(提升效果)

常见陷阱题 ⚠️:

题目:混合精度训练中,权重的主副本是?

  • A. FP16主,FP32副
  • B. FP32主,FP16副 ✅
  • C. 都是FP16
  • D. 都是FP32

💡 解析:FP32主权重保证更新精度,FP16副本用于加速计算。


题目2:Loss Scaling的主要作用是?

  • A. 加速训练
  • B. 防止梯度下溢 ✅
  • C. 减少内存
  • D. 提高精度

💡 解析:缩放损失可以放大小梯度,防止FP16下溢变成0。


题目3:BF16相比FP16的主要优势?

  • A. 计算更快
  • B. 内存更小
  • C. 数值范围更大,不易溢出 ✅
  • D. 精度更高

💡 解析:BF16有8位指数(和FP32一样),范围大,训练更稳定。


2. 梯度累积(Gradient Accumulation)⭐⭐⭐⭐⭐

问题背景:

1
2
3
4
5
大模型需要大batch训练(如batch=1024)
但GPU内存有限(如16GB)
实际只能跑batch=32

怎么办?梯度累积!

核心思想:

1
2
3
4
5
将大batch分成多个小batch
每个小batch前向传播、反向传播,累积梯度
多个小batch后统一更新参数

相当于:模拟大batch训练

实现原理:

1
2
3
4
5
6
7
8
9
10
目标:batch_size=1024,但内存只能跑32

方案:
1. 将1024样本分成32组,每组32个
2. 循环32次:
- 前向传播32个样本
- 反向传播,梯度累加(不清零)
3. 梯度除以32(平均)
4. 更新参数
5. 清零梯度,开始下一个大batch

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 方法1:手动实现
model.train()
optimizer.zero_grad()

accumulation_steps = 32 # 累积32个小batch

for i, (input, target) in enumerate(train_loader):
# 前向传播
output = model(input)
loss = criterion(output, target)

# 归一化损失(除以累积步数)
loss = loss / accumulation_steps

# 反向传播(梯度累加)
loss.backward()

# 每accumulation_steps步更新一次
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()

# 方法2:PyTorch 1.10+自动支持
# 在autocast中自动处理

为什么要除以accumulation_steps?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数学推导:

标准训练(batch=1024):
loss = (1/1024) Σ loss_i
grad = ∂loss/∂W

梯度累积(32次,每次batch=32):
1次:grad_1 = ∂[(1/32)Σ₁₋₃₂ loss_i]/∂W
2次:grad_2 = ∂[(1/32)Σ₃₃₋₆₄ loss_i]/∂W
...
累加:grad = grad_1 + grad_2 + ... + grad_32
= (32/32) × ∂[(1/1024)Σ loss_i]/∂W ❌ 多了32

修正:grad = (grad_1 + ... + grad_32) / 32

或者每次loss除以32
grad = ∂[(1/(32×32))Σ loss_i]/∂W ✅ 正确

内存与batch size关系:

1
2
3
4
5
6
7
8
9
假设单个样本需要100MB内存

直接batch=1024
内存 = 1024 × 100MB = 102.4GB ☠️ 爆内存

梯度累积(32步,batch=32):
内存 = 32 × 100MB = 3.2GB ✅ 可行

效果等价于batch=1024

梯度累积的trade-off:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
优点:
✅ 模拟大batch,内存占用小
✅ 训练稳定性提升(大batch
✅ 可以在小GPU上训练大模型
✅ 不需要改模型代码

缺点:
❌ 训练变慢(需要多次前向/反向传播)
❌ Batch Normalization统计量不准(小batch
❌ 增加训练步数(wall-clock time)

时间对比:
batch=1024(直接):100秒/epoch
batch=32(累积32步):150秒/epoch ❌ 慢50%

原因:
- 前向/反向传播无法完全并行
- kernel launch开销

与Batch Normalization的兼容性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
问题:
BN计算均值/方差基于当前batch
梯度累积时每个小batch独立计算BN
统计量不准确

解决方案:
1. 用Group Normalization或Layer Normalization
不依赖batch维度

2. Synchronized Batch Normalization
多个小batch之间同步统计量
但实现复杂

3. 大模型通常用Layer Norm(Transformer)
天然兼容梯度累积 ✅

实战案例(GPT-3预训练):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
目标batch3.2M tokens
GPU:V100 32GB
实际batch3200 tokens(受限于内存)

梯度累积:
accumulation_steps = 3.2M / 3200 = 1000

每个GPU:
- mini_batch = 3200 tokens
- 累积1000
- 才更新一次参数

全局等效:
effective_batch = 1000 × 3200 = 3.2M tokens

常见陷阱题 ⚠️:

题目:梯度累积主要解决什么问题?

  • A. 加速训练
  • B. 提高精度
  • C. 内存不足无法用大batch ✅
  • D. 防止过拟合

💡 解析:梯度累积通过多次小batch模拟大batch,解决内存限制。


3. 梯度检查点(Gradient Checkpointing)⭐⭐⭐⭐⭐

反向传播的内存问题:

1
2
3
4
5
6
7
8
标准反向传播:
需要保存所有中间激活值

例:50Transformer
每层激活值:200MB
总内存:50 × 200MB = 10GB ☠️

内存 ∝ 层数(O(N)

梯度检查点原理:

1
2
3
4
不保存所有激活值,只保存部分(检查点)
反向传播时,重新计算其他激活值

Trade-off:用计算换内存

策略示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
50层网络,设置5个检查点(每10层)

前向传播:
只保存第10, 20, 30, 40, 50层的激活值
丢弃其他45层的激活值

反向传播到第35层时:
1. 从检查点30重新前向传播到35
2. 计算梯度
3. 丢弃重计算的激活值

内存:5 × 200MB = 1GB ✅ 减少10倍!
代价:需要重新计算45层(约1.1倍时间)

数学分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
N层网络,设置√N个检查点

内存复杂度:
标准:O(N)
检查点:O(N)

时间复杂度:
标准:O(N)(一次前向+一次反向)
检查点:O(N) × (1 + 1/N)O(N)

例:N=100
内存:从100单位 → 10单位(减少10倍)
时间:增加10%

超值!

PyTorch实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from torch.utils.checkpoint import checkpoint

class TransformerLayer(nn.Module):
def forward(self, x):
# 正常的Transformer层
return self.layer(x)

class TransformerWithCheckpoint(nn.Module):
def __init__(self):
self.layers = nn.ModuleList([
TransformerLayer() for _ in range(50)
])

def forward(self, x):
for layer in self.layers:
# 使用checkpoint包装
x = checkpoint(layer, x)
return x

# checkpoint会:
# 1. 前向传播时不保存中间激活值
# 2. 反向传播时重新计算

选择性检查点:

1
2
3
4
5
6
7
8
9
10
# 只对部分层使用检查点
def forward(self, x):
for i, layer in enumerate(self.layers):
if i % 2 == 0: # 每2层设一个检查点
x = checkpoint(layer, x)
else:
x = layer(x)
return x

# 平衡内存和速度

实验数据(BERT-Large):

1
2
3
4
5
6
7
8
9
10
11
12
13
无检查点:
- 内存:32GB
- 训练时间:100小时
- Batch size:32

有检查点(每4层):
- 内存:12GB ✅ 减少62.5%
- 训练时间:115小时 ❌ 增加15%
- Batch size:64 ✅ 翻倍(利用省下的内存)

实际效果:
batch size翻倍带来的收益 > 15%的时间开销
总体是加速的! 🚀

与其他技术组合:

1
2
3
4
5
6
7
8
混合精度 + 梯度检查点:
BERT-Large在16GB GPU上:
- 只混合精度:batch=16
- 只梯度检查点:batch=24
- 两者结合:batch=48 ✅ 3倍提升

梯度累积 + 梯度检查点:
可以在消费级GPU(如RTX 3090)上训练GPT规模模型

常见陷阱题 ⚠️:

题目:梯度检查点主要trade-off是?

  • A. 内存换计算 ✅
  • B. 速度换精度
  • C. 内存换精度
  • D. 计算换带宽

💡 解析:梯度检查点通过重新计算激活值来节省内存。


4. 模型并行(Model Parallelism)⭐⭐⭐⭐⭐

什么时候需要模型并行?

1
2
3
4
5
6
7
8
单个模型太大,无法放入一张GPU

例:GPT-3175B参数)
FP16存储:175B × 2字节 = 350GB
单卡A100:80GB
无法放入! ☠️

必须切分模型到多张GPU

流水线并行(Pipeline Parallelism)

朴素方法:

1
2
3
4
5
6
7
8
9
10
11
12
将模型按层切分到多个GPU

例:12层Transformer,4个GPU
GPU 0:Layer 1-3
GPU 1:Layer 4-6
GPU 2:Layer 7-9
GPU 3:Layer 10-12

前向传播:
数据从GPU0 → GPU1 → GPU2 → GPU3
反向传播:
梯度从GPU3 → GPU2 → GPU1 → GPU0

朴素方法的问题:GPU利用率低

1
2
3
4
5
6
7
时间线(单个batch):
GPU0: [前向]----[等待]----[等待]----[反向]
GPU1: [等待][前向]--[等待]----[反向][等待]
GPU2: [等待][等待][前向]--[反向][等待][等待]
GPU3: [等待][等待][等待][前向反向][等待][等待]

利用率:25% ☠️ 太低!

GPipe:微批次流水线

1
2
3
4
5
6
7
8
9
10
11
12
13
将batch分成多个micro-batch,流水线执行

例:4个micro-batch,4GPU

时间线:
GPU0: [F1][F2][F3][F4]------------[B4][B3][B2][B1]
GPU1: ----[F1][F2][F3][F4]--------[B4][B3][B2][B1]--
GPU2: --------[F1][F2][F3][F4]----[B4][B3][B2][B1]----
GPU3: ------------[F1][F2][F3][F4][B4][B3][B2][B1]------

F=前向,B=反向

利用率:从25% → 75% ✅ 3倍提升

GPipe的bubble(气泡)问题:

1
2
3
4
5
6
7
8
9
10
11
起始和结束阶段仍有空闲(bubble)

bubble率 = (p-1) / (p+m-1)
p:GPU数(pipeline stages)
m:micro-batch数

例:p=4, m=16
bubble = 3/19 ≈ 16% ✅ 可接受

增加micro-batch数可以减少bubble
但micro-batch太小会影响训练效果

张量并行(Tensor Parallelism)

原理:切分单个层的计算

1
2
3
4
5
6
7
8
9
10
11
例:线性层 Y = XW
X: (batch, seq_len, d_model)
W: (d_model, d_model)
Y: (batch, seq_len, d_model)

W按列切成两块:W = [W1 | W2]
两个GPU并行计算:
GPU0: Y1 = XW1
GPU1: Y2 = XW2

最后拼接:Y = [Y1 | Y2]

Megatron-LM的张量并行策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer层切分:

1. Self-Attention的QKV投影:
按列切分(每个GPU负责部分头)

2. FFN第一层:
按列切分(每个GPU负责部分隐藏单元)

3. FFN第二层:
按行切分,输出再reduce

通信:
- 前向:最后需要all-reduce(求和)
- 反向:开始需要all-reduce

通信量:
每层2all-reduce,每次传输batch_size × seq_len × d_model个数

张量并行 vs 流水线并行:

1
2
3
4
5
6
7
8
9
10
11
张量并行:
✅ GPU利用率高(几乎100%)
✅ 延迟低(并行计算)
❌ 通信频繁(每层都需要)
❌ 只适合单机多卡(需要快速互联)

流水线并行:
✅ 通信少(只在边界传输)
✅ 适合跨机器(可容忍较慢的网络)
❌ GPU利用率低(有bubble)
❌ 延迟高(顺序执行)

实战:GPT-3训练配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
模型:175B参数
集群:1024个A100 GPU(128台机器,每台8卡)

并行策略(3D并行):
1. 数据并行:512路(512个完整副本)
2. 流水线并行:每个副本8路(8个stage)
3. 张量并行:每个stage 4路(4卡切分)

计算:512 × 8 × 4 / 8 = 2048 ❌ 不对?

实际:
- 每台机器:8卡
- 张量并行:4卡(需要NVLink快速互联)
- 流水线并行:2台机器(2×4卡)
- 数据并行:64组(128台/2)

验证:64 × 2 × 4 = 512份完整模型,共512×2=1024卡 ✅

常见陷阱题 ⚠️:

题目:流水线并行的主要瓶颈是?

  • A. 通信开销
  • B. GPU利用率低(bubble) ✅
  • C. 内存不足
  • D. 精度损失

💡 解析:流水线并行在起始和结束阶段有bubble(空闲),降低利用率。


5. Zero冗余优化器(ZeRO)⭐⭐⭐⭐⭐

数据并行的内存问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
标准数据并行(DDP):
每个GPU保存:
1. 模型参数(W)
2. 梯度(G)
3. 优化器状态(Adam的m和v)

例:GPT-2(1.5B参数),8个GPU数据并行
每个GPU:
- 参数:1.5B × 2字节(FP16)= 3GB
- 梯度:3GB
- 优化器状态:1.5B × 4字节(FP32 m)= 6GB
1.5B × 4字节(FP32 v)= 6GB
- 总计:18GB × 8卡 = 144GB

但实际上8个GPU的参数/梯度/优化器状态完全相同!
冗余度:8倍! ☠️

ZeRO的三个阶段:

Stage 1:切分优化器状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
优化器状态(m和v)切分到各GPU

例:8个GPU,1.5B参数
每个GPU只存储:1.5B / 8 = 187.5M参数的优化器状态

内存节省:
优化器状态:12GB → 1.5GB(每GPU)
总节省:8

更新参数时:
all-gather需要的优化器状态
更新后再分散回去

通信量:小(只传优化器状态)

Stage 2:切分梯度

1
2
3
4
5
6
7
8
9
10
在Stage 1基础上,梯度也切分

每个GPU只存储:1.5B / 8参数的梯度

反向传播时:
计算完梯度立即reduce-scatter到对应GPU
不需要保存完整梯度

内存节省:
梯度:3GB → 375MB(每GPU)

Stage 3:切分参数

1
2
3
4
5
6
7
8
9
10
11
12
13
在Stage 2基础上,参数也切分

每个GPU只存储:1.5B / 8的参数

前向传播时:
all-gather需要的参数
计算后丢弃(不保存)

反向传播时:
再次all-gather需要的参数

内存节省:
参数:3GB → 375MB(每GPU)

ZeRO-Offload:CPU卸载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
进一步优化:将优化器状态卸载到CPU内存

GPU:只保存参数和梯度(前向/反向传播需要)
CPU:保存优化器状态(更新参数时需要)

流程:
1. 反向传播在GPU计算梯度
2. 梯度传到CPU
3. CPU更新优化器状态和参数
4. 参数传回GPU

适合:
- GPU内存极度受限
- CPU内存充足(如256GB)
- PCIe带宽够(如PCIe 4.0)

代价:CPU-GPU通信开销

ZeRO-Infinity:NVMe卸载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
更激进:利用NVMe固态硬盘

层次结构:
GPU显存(最快,最小)

CPU内存(快,中等)

NVMe SSD(慢,巨大)

策略:
- 当前层参数:GPU显存
- 近期层参数:CPU内存
- 其他层参数:NVMe SSD

可以训练万亿参数模型!

内存对比(GPT-2 1.5B,8卡):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
标准DDP:
每GPU 18GB × 8 = 144GB

ZeRO Stage 1
每GPU (3GB参数 + 3GB梯度 + 1.5GB优化器) = 7.5GB
总:60GB(节省58%)

ZeRO Stage 2
每GPU (3GB参数 + 375MB梯度 + 1.5GB优化器) = 4.875GB
总:39GB(节省73%)

ZeRO Stage 3
每GPU (375MB参数 + 375MB梯度 + 1.5GB优化器) = 2.25GB
总:18GB(节省87.5%) 🚀

ZeRO-Offload:
每GPU (375MB参数 + 375MB梯度) = 750MB
CPU (1.5GB优化器) × 8 = 12GB
GPU总:6GB(节省95.8%) 🚀🚀

通信开销:

1
2
3
4
5
6
7
8
9
10
11
12
13
ZeRO Stage 1
通信量小(只传优化器状态)
开销:几乎可忽略

ZeRO Stage 2
通信量中等(梯度reduce-scatter)
开销:~5% 训练时间

ZeRO Stage 3
通信量大(参数all-gather,每层2次)
开销:~15-20% 训练时间

权衡:内存节省 vs 通信开销

实战案例:

1
2
3
4
5
6
7
8
Turing-NLG(17B参数):
使用ZeRO-2
256个V100 GPU(32GB)
训练成功 ✅

如果用标准DDP:
需要1024个GPU
成本增加4倍!

常见陷阱题 ⚠️:

题目:ZeRO Stage 3切分了哪些内容?

  • A. 只有优化器状态
  • B. 优化器状态和梯度
  • C. 参数、梯度、优化器状态 ✅
  • D. 只有参数

💡 解析:ZeRO-3将所有模型状态(参数、梯度、优化器)都切分。


大模型训练技术对比总结 📊

技术 节省内存 时间开销 适用场景 实现难度
混合精度 50% -50%(加速) 所有场景 ✅ 简单
梯度累积 0% +20-50% 内存受限 简单
梯度检查点 60-80% +10-20% 层数多 简单
流水线并行 50-75% bubble损失 跨机器 中等
张量并行 50-75% ~0% 单机多卡 中等
ZeRO-1 优化器4× <5% 大模型必备 简单
ZeRO-2 ~5% 大模型必备 简单
ZeRO-3 16× ~15% 超大模型 中等
ZeRO-Offload 20×+ +30-50% GPU内存极限 复杂

推荐组合策略:

1
2
3
4
5
6
7
8
9
10
11
小模型(<1B):
混合精度 + 梯度累积

中型模型(1B-10B):
混合精度 + ZeRO-2 + 梯度检查点

大型模型(10B-100B):
混合精度 + ZeRO-3 + 张量并行(4-8路) + 流水线并行

超大模型(100B+):
上述所有 + 可能需要ZeRO-Offload

记忆技巧 🧠:

  • 混合精度 = “FP16算,FP32存” → 速度快内存省
  • 梯度累积 = “化整为零” → 小batch模拟大batch
  • 梯度检查点 = “用时间换空间” → 重算激活值
  • 流水线并行 = “接力赛” → 层与层接力,有空闲
  • 张量并行 = “齐头并进” → 同一层多卡并行,通信多
  • ZeRO = “消除冗余” → 切分状态,按需通信
  • 三阶段记忆:优化器→梯度→参数(逐步切分)

实践:

1
2
FastAIlr_find()
找到最优学习率范围

十九、高频陷阱题总结⭐⭐⭐⭐⭐

激活函数类

Sigmoid的梯度最大值?

  • 答案:0.25
  • 陷阱:很多人答0.5或1

ReLU(0)等于?

  • 答案:0
  • 陷阱:有人认为是undefined

Transformer类

Multi-Head Attention的参数量(h头,d维)?

  • 答案:4d²(与头数无关)
  • 陷阱:认为是4hd²

Self-Attention复杂度(n长度,d维度)?

  • 答案:O(n²d)
  • 陷阱:O(n²)或O(nd²)

优化器类

Adam的默认β1和β2?

  • 答案:0.9和0.999
  • 陷阱:0.99和0.999

AdamW和Adam的区别?

  • 答案:权重衰减方式(解耦vs耦合)
  • 陷阱:学习率或收敛速度

评估指标类

F1是什么平均?

  • 答案:调和平均
  • 陷阱:算术平均或几何平均

R²的取值范围?

  • 答案:(-∞, 1]
  • 陷阱:[0, 1]

损失函数类

为什么分类不用MSE?

  • 答案:与Sigmoid配合梯度消失
  • 陷阱:计算慢或精度低

CrossEntropyLoss的输入是?

  • 答案:原始logits(未softmax)
  • 陷阱:softmax后的概率

数值计算类

卷积输出尺寸公式?

  • 答案:(N - K + 2P)/S + 1
  • 陷阱:忘记+1或/S

LLaMA-7B用INT4量化后大小?

  • 答案:约3.5GB
  • 陷阱:7GB或1.75GB

二十、考前30分钟速记清单 🎯

公式速记

1
2
3
4
5
6
7
8
9
10
1. 卷积输出:(N-K+2P)/S+1
2. Attention:softmax(QK^T/√dk)V
3. F1 = 2PR/(P+R)
4. Adam: β1=0.9, β2=0.999, lr=0.001
5. Sigmoid导数:f'(x)=f(x)(1-f(x)),最大0.25
6. 参数量Multi-Head:4
7. 参数量FFN:8
8. KV Cache单token:2×L×d×2字节
9. R² = 1 - SSres/SStot
10. Softmax数值稳定:减max(x)

概念速记

1
2
3
4
5
6
7
8
9
10
1. ReLU死亡 → Leaky ReLU / 降低lr
2. 梯度消失 → ReLU + BN + ResNet
3. 梯度爆炸 → Gradient Clipping
4. 类别不平衡 → Focal Loss / AUC
5. Transformer标配 → GELU + LayerNorm + AdamW
6. CNN分类 → SGD+Momentum
7. NLP任务 → Adam/AdamW
8. 长序列 → Flash Attention / Sparse Attention
9. KV Cache压缩 → GQA + INT8 + PagedAttention
10. LLM量化 → GPTQ / AWQ

对比速记

1
2
3
4
5
6
7
8
9
10
| 项目 | 选项A | 选项B |
|------|-------|-------|
| 激活函数 | ReLU(CNN) | GELU(Transformer) |
| 归一化 | BN(CNN) | LN(Transformer) |
| 优化器 | SGD(CNN) | AdamW(Transformer) |
| 位置编码 | Learned(BERT) | RoPE(LLaMA) |
| 注意力 | MHA(标准) | GQA(LLaMA2) |
| 评估指标 | Accuracy(平衡) | AUC(不平衡) |
| 损失函数 | MSE(回归) | CE(分类) |
| 初始化 | Xavier(Sigmoid) | He(ReLU) |

数字速记

1
2
3
4
5
6
7
8
9
10
重要数字记忆:
- 0.25:Sigmoid最大梯度
- 0.9, 0.999:Adam的β1, β2
- 0.001:Adam默认学习率
- 4d²:Multi-Head参数量
- 8d²:FFN参数量
- O(n²d):Self-Attention复杂度
- 2.0:Focal Loss的γ
- 0.01:Leaky ReLU的α
- 512KB:LLaMA-7B单token KV Cache

考前复习建议 📚

第1遍:知识框架(1小时)

1
2
3
4
1. 快速浏览所有章节标题
2. 标记⭐⭐⭐⭐⭐部分(必考)
3. 记住基本公式和概念
4. 不求完全理解,建立印象

第2遍:重点突破(2小时)

1
2
3
4
1. 精读⭐⭐⭐⭐⭐章节
2. 做所有陷阱题
3. 记忆关键数字和公式
4. 理解"为什么"而非死记

第3遍:实战模拟(30分钟)

1
2
3
4
1. 看"考前速记清单"
2. 默写重要公式
3. 回顾错题和陷阱
4. 放松心态,准备考试

记忆技巧

1
2
3
4
5
1. 口诀记忆(如"ReLU死亡Leaky救")
2. 对比记忆(CNN vs Transformer)
3. 场景记忆(什么任务用什么)
4. 公式推导(理解比死记牢固)
5. 动手计算(实际算一遍印象深)

祝你考试顺利,拿下150分! 🎉

相关文章:


华为AI机试选择题知识点大全
https://whyalwaysme.lol/2026/09/01/华为AI机试-选择题知识点/
作者
Cassiur
发布于
2026年9月1日
许可协议