上一篇速通 AI(二):深度学习与 PyTorch — CNN、RNN/LSTM、注意力机制基础

目标:深入理解 Transformer 架构——Self-Attention、多头注意力、位置编码、Encoder/Decoder 的完整实现。

前置要求:阶段二(神经网络、CNN、RNN/LSTM、PyTorch、注意力机制基础)


本阶段知识依赖图

flowchart TD
    A["阶段二基础<br/>RNN/LSTM + 注意力机制"]
    A --> T["Transformer架构(最核心)"]
    T --> T1[Self-Attention] --> T1a[Multi-Head Attention]
    T --> T2[Positional Encoding]
    T --> T3[Feed-Forward Network]
    T --> T4["Add & Norm<br/>残差连接 + 层归一化"]
    T --> T5[Encoder Block] --> T5a["Encoder × N"]
    T --> T6[Decoder Block] --> T6a["Decoder × N"]

为什么单独成篇? Transformer 是整个"速通 AI"系列中承上启下的关键节点——它解决了上一篇中 RNN 的两大致命缺陷(串行计算、长距离依赖),同时为后续所有文章(BERT/GPT 预训练、LLaMA 架构、应用开发)奠定了架构基础。理解 Transformer 的每一个组件(Self-Attention、多头注意力、位置编码、残差连接、FFN),是理解整个大模型体系的前提。因此,我们用一整篇的篇幅,从直觉、数学、数值示例到代码实现,把 Transformer 讲透。


1. Transformer 架构——大模型的基石

这是整个课程中最重要的一个模块。 Transformer 是 GPT 、 BERT 、 LLaMA 、 ChatGPT 等所有现代大模型的基础架构。理解了 Transformer ,就理解了大模型的"骨架"。

1.1 从宏观视角理解 Transformer

Transformer 之前的问题——为什么需要它?

RNN/LSTM 的两大致命缺陷

  1. 串行计算(效率低):处理第 5 个词时,必须等第 1-4 个词全部处理完 → 无法利用 GPU 的并行能力 → 一个 1000 词的句子,需要串行计算 1000 步。
  2. 长距离依赖(效果差):虽然 LSTM 缓解了梯度消失,但信息仍然需要"逐步传递"。第 1 个词的信息要经过 999 步才能到达第 1000 个词 → 每一步都有信息损失 → 超长序列效果仍然不好。

Transformer 的革命性解决方案

  1. 完全抛弃循环结构,用 Self-Attention 替代 → 所有位置同时计算(并行),训练速度提升 10-100 倍。
  2. 任意两个位置直接相连(不需要逐步传递) → 第 1 个词和第 1000 个词之间只有一步之遥 → 天然支持长距离依赖。

Transformer 的整体结构——先看大局

类比:翻译任务就像"理解+写作"两个步骤。

Encoder(编码器) = “理解” —— 读懂原文。输入:“I love AI”,输出:每个词的"理解向量"(包含上下文信息)。

Decoder(解码器) = “写作” —— 根据理解写出译文。输入:已生成的部分译文 + Encoder 的理解,输出:下一个词的概率分布。

原论文标题:“Attention Is All You Need”(2017 年, Google)

核心洞察:不需要循环(RNN),不需要卷积(CNN),只需要注意力机制就够了!

Transformer 完整架构

上图展示了 Transformer 的完整架构。左侧 Encoder 通过 Self-Attention 理解输入;右侧 Decoder 通过 Masked Self-Attention + Cross-Attention 生成输出。Cross-Attention 的 K、V 来自 Encoder,Q 来自 Decoder——这就是连接编码器和解码器的桥梁。

1.2 Self-Attention——Transformer 的灵魂

直觉理解: Self-Attention 到底在做什么?

类比:一场会议讨论

想象一个 6 人会议,讨论"The cat sat on the mat"这个句子的含义:

每个人(词)都要弄清楚"我和谁的关系最密切"。

“cat"发言时:

  • 看了看"The”:嗯,你是我的限定词,关系一般(权重 0.05)
  • 看了看自己"cat":我当然了解自己(权重 0.15)
  • 看了看"sat":你是我做的动作!关系最紧密(权重 0.60)
  • 看了看"on":介词,关系不大(权重 0.05)
  • 看了看了看"the":和我没关系(权重 0.03)
  • 看了看了看"mat":我坐在你上面,有点关系(权重 0.12)

$"cat"的新表示 = 0.05 \times \text{The} + 0.15 \times \text{cat} + 0.60 \times \text{sat} + 0.05 \times \text{on} + 0.03 \times \text{the} + 0.12 \times \text{mat}$

现在"cat"的向量不仅包含自己的信息,还融合了所有相关词的信息!特别是"sat"的信息(权重最大),所以"cat"现在"知道"自己是句子的主语。

Self-Attention 的数学计算: Query, Key, Value——完整推导

为什么要引入 Q 、 K 、 V 三个概念?

类比:图书馆找书

  • (Query):想找一本关于"深度学习"的书
  • 书架上的书(Key):每本书的标签/标题
  • 书的内容(Value):每本书的实际内容

匹配过程:

  1. 你的需求(Query)和每本书的标签(Key)做匹配 → 得到相关性分数
  2. 根据相关性分数,从书的内容(Value)中提取信息
  3. 相关性高的书,多提取一些;相关性低的书,少提取一些

具体计算步骤(带维度标注):

假设:句子长度 seq_len=4 ,模型维度 d_model=512 ,头数 n_heads=8 ,每头维度 $d_k = 64$

输入矩阵 $X$:(4, 512) — 4 个词,每个词 512 维

Step 1:生成 Q 、 K 、 V (通过 3 个不同的线性变换)

$$ \begin{aligned} Q &= X \cdot W_Q \quad (4, 512) \times (512, 64) = (4, 64) \quad \text{← 每个词的"需求"} \\ K &= X \cdot W_K \quad (4, 512) \times (512, 64) = (4, 64) \quad \text{← 每个词的"特征标签"} \\ V &= X \cdot W_V \quad (4, 512) \times (512, 64) = (4, 64) \quad \text{← 每个词的"实际内容"} \end{aligned} $$

为什么需要 3 个不同的变换?因为一个词的"我需要什么"(Q)、“我能提供什么”(K)、“我的内容”(V)是不同的。

Step 2:计算注意力分数

$$ \text{scores} = Q \cdot K^T \quad (4, 64) \times (64, 4) = (4, 4) \quad \text{← 每对词之间的相关性} $$

Step 3:缩放(除以 $\sqrt{d_k} = \sqrt{64} = 8$)

$$ \text{scaled\_scores} = \text{scores} / 8 $$

为什么要缩放?当 $d_k$ 很大时,$Q \cdot K^T$ 的值可能很大。大的输入值会导致 softmax 输出接近 one-hot (梯度接近 0)。除以 $\sqrt{d_k}$ 让方差回到 1 , softmax 的梯度正常。

数值对比:缩放 vs 不缩放

假设 $d_k = 64$,某行的原始分数为 $[26, 18, 30, 22]$:

方案输入值Softmax 输出梯度状态
不缩放[26, 18, 30, 22][0.018, 0.000, 0.982, 0.000]梯度接近 0,参数几乎不更新
缩放(÷8)[3.25, 2.25, 3.75, 2.75][0.276, 0.102, 0.455, 0.167]梯度正常,参数有效更新

不缩放时,softmax 输出接近 one-hot(0.982),梯度 $\approx p(1-p) \approx 0.018$,很小。缩放后,softmax 输出更"柔和",梯度 $\approx 0.455 \times 0.545 \approx 0.248$,训练正常进行。

Step 4: Softmax 归一化

$$ \text{attention\_weights} = \text{softmax}(\text{scaled\_scores}) \quad (4, 4) $$

现在每行都是一个概率分布,表示"每个词应该关注谁"。

Step 5:加权求和

$$ \text{output} = \text{attention\_weights} \cdot V \quad (4, 4) \times (4, 64) = (4, 64) $$

每个词的输出 = 所有词的 Value 的加权和(权重=注意力分数)。

一句话总结: Self-Attention 让每个词都能"看到"句子中的所有其他词,并根据相关性加权聚合信息,生成包含上下文信息的新表示。

flowchart LR
    X["输入 X<br/>(seq_len, d_model)"] --> Q["Q = X·W_Q"]
    X --> K["K = X·W_K"]
    X --> V["V = X·W_V"]
    Q --> S["scores = Q·Kᵀ"]
    K --> S
    S --> Sc["÷ √d_k"]
    Sc --> SM["Softmax"]
    SM --> W["attention weights"]
    W --> O["output = weights·V"]
    V --> O

动手计算: Self-Attention 完整数值示例

为了真正理解 Self-Attention ,我们用一个极简例子手算一遍。假设句子是 “I love AI”(3 个词),模型维度 $d_{\text{model}}=4$(实际是 512,这里简化便于手算),单头注意力。

Step 1:输入矩阵 $X$(3×4)

维度 1维度 2维度 3维度 4
I1.00.01.00.0
love0.01.00.01.0
AI1.00.00.01.0

Step 2:生成 Q、K、V(为简化,假设 $W_Q$、$W_K$、$W_V$ 已给出)

经过线性变换后得到:

$Q$$K$$V$
I[1, 0, 1, 1][1, 1, 0, 0][0, 1, 1, 0]
love[0, 1, 0, 1][0, 1, 1, 0][1, 0, 0, 1]
AI[1, 1, 0, 0][0, 0, 1, 1][1, 1, 0, 1]

Step 3:计算 $\text{scores} = Q \cdot K^T$(3×3 矩阵)

以" I “为例,计算” I “对每个词的注意力分数:

$$ \begin{aligned} \text{score}(I, I) &= [1,0,1,1] \cdot [1,1,0,0] = 1+0+0+0 = 1 \\ \text{score}(I, \text{love}) &= [1,0,1,1] \cdot [0,1,1,0] = 0+0+1+0 = 1 \\ \text{score}(I, \text{AI}) &= [1,0,1,1] \cdot [0,0,1,1] = 0+0+1+1 = 2 \end{aligned} $$

完整分数矩阵:

IloveAI
I112
love111
AI210

Step 4:缩放——除以 $\sqrt{d_k} = \sqrt{4} = 2$

IloveAI
I0.500.501.00
love0.500.500.50
AI1.000.500.00

Step 5: Softmax 归一化(每行求 softmax)

以” I “行为例:$\text{softmax}([0.5, 0.5, 1.0])$

$$ \begin{aligned} e^{0.5} &= 1.649, \quad e^{1.0} = 2.718 \\ \text{sum} &= 1.649 + 1.649 + 2.718 = 6.016 \\ \text{softmax} &= [1.649/6.016,\; 1.649/6.016,\; 2.718/6.016] = [0.274,\; 0.274,\; 0.452] \end{aligned} $$

“love"行:$\text{softmax}([0.5, 0.5, 0.5]) = [0.333, 0.333, 0.333]$(三个位置等权,因为 love 对所有词的关注度相同)

“AI"行:$\text{softmax}([1.0, 0.5, 0.0]) = [0.506, 0.307, 0.186]$

完整注意力权重矩阵:

IloveAI
I0.2740.2740.452
love0.3330.3330.333
AI0.5060.3070.186

解读:“I"最关注"AI”(权重 0.452),“love"对三个词等权关注(0.333),“AI"最关注"I”(权重 0.506)——这符合直觉:“I"是一个代词,需要通过"AI"来确定指代对象。

Step 6:加权求和 $\text{output} = \text{weights} \cdot V$

以” I “为例:

$$ \begin{aligned} \text{output}_I &= 0.274 \times [0,1,1,0] + 0.274 \times [1,0,0,1] + 0.452 \times [1,1,0,1] \\ &= [0, 0.274, 0.274, 0] + [0.274, 0, 0, 0.274] + [0.452, 0.452, 0, 0.452] \\ &= [0.726,\; 0.726,\; 0.274,\; 0.726] \end{aligned} $$

" I “的原始向量是 $[1, 0, 1, 0]$,经过 Self-Attention 后变为 $[0.726, 0.726, 0.274, 0.726]$——已经融合了” love “和” AI “的信息!这正是 Self-Attention 的核心价值:每个词的输出都是所有词的加权组合,权重由词与词之间的相关性决定

Self-Attention 的代价

维度分析
问题RNN 逐词处理,无法并行
方案Self-Attention 让所有词同时互相计算注意力
代价计算复杂度 $O(n^2 \cdot d)$($n$ 是序列长度,$d$ 是维度)。序列长度翻倍,计算量翻 4 倍。当 $n=4096$ 时,注意力矩阵有 1600 万个元素。显存也是 $O(n^2)$——这是 Transformer 处理长序列的主要瓶颈

1.3 位置编码——告诉模型"词的顺序”

为什么需要位置编码?

核心问题: Self-Attention 是"位置无关"的。

“狗咬人"和"人咬狗”——如果只看 Self-Attention 的计算,两个句子包含的词相同, Q 、 K 、 V 的计算方式相同 → Self-Attention 无法区分这两个句子!但它们的意思完全不同!→ 必须额外告诉模型"每个词在哪个位置”。

正弦位置编码——为什么用 sin/cos ?

$$ \begin{aligned} PE(pos, 2i) &= \sin(pos / 10000^{2i/d_{\text{model}}}) \\ PE(pos, 2i+1) &= \cos(pos / 10000^{2i/d_{\text{model}}}) \end{aligned} $$

其中 $pos$ 是词在句子中的位置(0, 1, 2, …),$i$ 是编码维度的索引(0, 1, 2, …, $d_{\text{model}}/2-1$),$d_{\text{model}}$ 是模型维度。

动手计算:位置编码数值示例

用 $d_{\text{model}}=4$(两组 sin/cos)计算 pos=0, 1, 2 的编码,验证"每个位置唯一"和"相对距离可线性变换"两个性质。

Step 1:计算频率

$i=0$:$10000^{0/4} = 1$,频率 $= 1/1 = 1$ $i=1$:$10000^{2/4} = 100$,频率 $= 1/100 = 0.01$

Step 2:计算各位置的编码

$$ \begin{aligned} PE(0) &= [\sin(0), \cos(0), \sin(0), \cos(0)] = [0, 1, 0, 1] \\ PE(1) &= [\sin(1), \cos(1), \sin(0.01), \cos(0.01)] = [0.841, 0.540, 0.010, 1.000] \\ PE(2) &= [\sin(2), \cos(2), \sin(0.02), \cos(0.02)] = [0.909, -0.416, 0.020, 1.000] \end{aligned} $$

解读

  • 唯一性:$PE(0) \neq PE(1) \neq PE(2)$,每个位置的编码向量都不同
  • 低维变化快,高维变化慢:第 1、2 维($i=0$,频率=1)变化剧烈,第 3、4 维($i=1$,频率=0.01)几乎不变。这就像"时钟的时针和秒针”——低维是"秒针”(区分相邻位置),高维是"时针”(区分远距离位置)
  • 相对距离可线性变换:$PE(1)$ 可以用 $PE(0)$ 的线性变换得到($\sin(a+b)$ 展开公式),模型可以学到"距离 1"这个概念

为什么选择正弦/余弦?三个巧妙的原因

位置编码方案对比(问题→方案→代价)

方案问题方案代价
可学习位置编码需要为每个位置学一个向量每个位置一个可训练参数,直接优化无法外推到训练时未见过的长度(如训练 512,推理 1024 会出错)
正弦/余弦编码(本文方案)需要一种无需训练就能覆盖任意位置的编码用 sin/cos 函数生成固定编码,利用三角函数的周期性和线性变换性质位置信息是"软注入”(相加),可能被语义信息淹没
RoPE 旋转位置编码(现代方案)需要在注意力计算中直接编码相对位置将位置信息编码为旋转矩阵,直接作用于 Q 和 K计算稍复杂,但支持相对位置建模和长度外推

为什么原论文选择 sin/cos ? 2017 年提出时,这是唯一不需要训练参数且支持长度外推的方案。现代大模型(如 LLaMA)已普遍采用 RoPE,但理解 sin/cos 是理解所有位置编码方案的基础。

  1. 每个位置有唯一的编码: sin 和 cos 的组合可以唯一标识每个位置,就像每个 GPS 坐标是唯一的一样。
  2. 相对距离可以用线性变换表示:$PE(pos+k)$ 可以用 $PE(pos)$ 的线性变换来表示(因为 $\sin(a+b) = \sin(a)\cos(b) + \cos(a)\sin(b)$)→ 模型可以学到"距离关系"(相邻、隔一个、隔两个…)。
  3. 可以外推到更长的序列:正弦函数是周期性的,可以计算任意位置的编码 → 即使训练时没见过 1000 个词的句子,推理时也能处理。

词嵌入 + 位置编码——两者如何结合?

$$ \text{最终输入} = \text{Token Embedding} + \text{Positional Encoding} $$
  • Token Embedding:词本身的语义信息(可学习的权重矩阵)。“猫” → [0.2, 0.8, -0.1, …](512 维向量)
  • Positional Encoding:词的位置信息(固定的正弦函数)。位置 0 → [0.0, 1.0, 0.0, …](512 维向量)

相加后:[0.2, 1.8, -0.1, …]。模型同时知道"这个词是猫"(语义)和"它在第 1 个位置"(位置)。

类比:就像给每个词发了一张"身份证",上面写着姓名(Token Embedding)和地址(Positional Encoding)。

1.4 多头注意力与 Encoder Block

多头注意力——为什么要"多头"?

类比:多角度分析

单头注意力 = 一个人看问题,只能从一个角度理解。多头注意力 = 多个人同时看问题,每人从不同角度理解,最后综合。

例如理解"The animal didn’t cross the street because it was too tired":

  • Head 1 可能学到:“it"指代"animal”(指代关系)
  • Head 2 可能学到:“tired"修饰"animal”(修饰关系)
  • Head 3 可能学到:“didn’t cross"和"tired"有因果关系(逻辑关系)

单头很难同时学到这三种关系,但多头可以!

为什么不能直接用一个大的注意力头? 直觉上,把 8 个小头合并成一个 $d_{\text{model}}=512$ 的大头,信息量不是一样吗?关键区别在于:一个大头只能学到一种"注意力模式”,而多个小头可以并行学到多种不同的模式。就像 CNN 中多个卷积核分别检测竖线、横线、斜线一样,多头注意力的每个头学习不同的"关系检测器"。实验发现,不同的头确实自发地分工:有的关注语法结构,有的关注语义相似性,有的关注位置关系。

数学实现——维度追踪

假设 $d_{\text{model}} = 512$,$n_{\text{heads}} = 8$,$d_k = d_{\text{model}} / n_{\text{heads}} = 64$

输入 $X$:(batch, seq_len, 512)

$$ \text{head}_i = \text{Attention}(X \cdot W_{Q_i}, X \cdot W_{K_i}, X \cdot W_{V_i}) $$$$ = \text{softmax}\left(\frac{(X \cdot W_{Q_i})(X \cdot W_{K_i})^T}{\sqrt{64}}\right) \cdot (X \cdot W_{V_i}) $$

每个 head 的输出:(batch, seq_len, 64)

合并所有头:

$$ \text{MultiHead} = \text{Concat}(\text{head}_1, \ldots, \text{head}_8) \cdot W_O = (batch, \text{seq\_len}, 8 \times 64) \cdot (512, 512) = (batch, \text{seq\_len}, 512) $$

输出维度和输入维度相同(512),方便堆叠多层!

动手计算: Multi-Head Attention 数值示例

延续 §1.2 的"I love AI"例子(3 词,$d_{\text{model}}=4$),演示 2 头注意力($d_k = 4/2 = 2$)。关键区别在于:每个头只看维度的一半,不同头可能学到不同的"关注模式"。

假设:两个头的权重矩阵为

$$ W_{Q_1} = \begin{bmatrix} 1&0\\0&1\\0&0\\0&0 \end{bmatrix},\quad W_{K_1} = \begin{bmatrix} 1&0\\0&0\\0&1\\0&0 \end{bmatrix},\quad W_{V_1} = \begin{bmatrix} 0&1\\1&0\\0&0\\0&0 \end{bmatrix} $$$$ W_{Q_2} = \begin{bmatrix} 0&0\\0&0\\1&0\\0&1 \end{bmatrix},\quad W_{K_2} = \begin{bmatrix} 0&0\\0&0\\0&1\\1&0 \end{bmatrix},\quad W_{V_2} = \begin{bmatrix} 0&0\\0&0\\0&1\\1&0 \end{bmatrix} $$

Head 1(关注前两维):

输入 $X$(3×4)同 §1.2,以"I"为例:$X_I = [1, 0, 1, 0]$

$$ \begin{aligned} Q_{I,1} &= X_I \cdot W_{Q_1} = [1, 0] \quad \text{← 只取前两维} \\ K_{I,1} &= X_I \cdot W_{K_1} = [1, 1] \\ V_{I,1} &= X_I \cdot W_{V_1} = [0, 1] \end{aligned} $$

三个词的 Q/K/V(Head 1):

$Q_1$$K_1$$V_1$
I[1, 0][1, 1][0, 1]
love[0, 1][0, 0][1, 0]
AI[1, 0][1, 0][0, 1]

分数矩阵 $Q \cdot K^T / \sqrt{2}$:

$$ \begin{aligned} \text{score}(I,I) &= [1,0]\cdot[1,1] / 1.414 = 1/1.414 = 0.707 \\ \text{score}(I,\text{love}) &= [1,0]\cdot[0,0] / 1.414 = 0 \\ \text{score}(I,\text{AI}) &= [1,0]\cdot[1,0] / 1.414 = 0.707 \end{aligned} $$

Softmax 后(I 行):$\text{softmax}([0.707, 0, 0.707]) = [0.405, 0.190, 0.405]$

Head 1 的输出(I):$0.405 \times [0,1] + 0.190 \times [1,0] + 0.405 \times [0,1] = [0.190, 0.810]$

Head 2(关注后两维):

三个词的 Q/K/V(Head 2):

$Q_2$$K_2$$V_2$
I[1, 0][0, 1][0, 1]
love[0, 1][1, 0][1, 0]
AI[0, 1][1, 0][1, 0]

分数矩阵 $Q \cdot K^T / \sqrt{2}$(I 行):

$$ \begin{aligned} \text{score}(I,I) &= [1,0]\cdot[0,1] / 1.414 = 0 \\ \text{score}(I,\text{love}) &= [1,0]\cdot[1,0] / 1.414 = 0.707 \\ \text{score}(I,\text{AI}) &= [1,0]\cdot[1,0] / 1.414 = 0.707 \end{aligned} $$

Softmax 后(I 行):$\text{softmax}([0, 0.707, 0.707]) = [0.198, 0.401, 0.401]$

Head 2 的输出(I):$0.198 \times [0,1] + 0.401 \times [1,0] + 0.401 \times [1,0] = [0.802, 0.198]$

Head 2 关注的是不同的维度子空间,可能学到与 Head 1 不同的注意力模式——这正是多头的价值

拼接与输出投影

$$ \begin{aligned} \text{MultiHead}(I) &= \text{Concat}(\text{head}_1, \text{head}_2) \cdot W_O \\ &= [0.190, 0.810, 0.802, 0.198] \cdot W_O \end{aligned} $$

其中 $W_O$ 是 4×4 的输出投影矩阵,将拼接后的向量映射回 $d_{\text{model}}$ 维。

解读:单头注意力用全部 4 维计算一种注意力模式;多头注意力用前 2 维和后 2 维分别计算两种模式,再拼接——模型可以同时关注"语法关系"和"语义关系"。这就像从两个不同的角度分析同一段文字,综合后理解更全面。

Encoder Block 的完整结构——逐层理解

输入 $x$:(batch, seq_len, 512)

  1. Multi-Head Self-Attention(x, x, x) → 输出:(batch, seq_len, 512)
  2. Add & LayerNorm:$\text{norm}_1(x + \text{attn\_output})$。残差连接:保留原始信息 + 新学到的信息。层归一化:稳定数值范围。

为什么用 LayerNorm 而不是 BatchNorm ?

特性BatchNormLayerNorm
归一化维度跨 batch(同一特征在不同样本上归一化)跨特征(同一样本在不同维度上归一化)
依赖 batch size是(batch 小时统计量不稳定)否(每个样本独立计算)
适合变长序列不适合(序列长度不同时 padding 影响统计量)适合(每个 token 独立归一化)
推理时行为需要维护 running mean/var直接计算,无额外状态

直觉:BatchNorm 是"看同一道题在不同同学中的表现",LayerNorm 是"看同一个同学各科的成绩"。NLP 中每个句子长度不同、每个 token 的含义不同,用 LayerNorm 更合适。

动手计算:LayerNorm 数值示例

输入向量:$x = [1, 5, 3, -1]$

Step 1:计算均值

$$\mu = (1 + 5 + 3 + (-1)) / 4 = 2.0$$

Step 2:计算方差

$$\sigma^2 = \frac{(1-2)^2 + (5-2)^2 + (3-2)^2 + (-1-2)^2}{4} = \frac{1+9+1+9}{4} = 5.0$$

Step 3:归一化

$$\hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \varepsilon}} = \frac{x_i - 2.0}{\sqrt{5.0 + 10^{-5}}} \approx \frac{x_i - 2.0}{2.236}$$$$\hat{x} = [-0.447, 1.342, 0.447, -1.342]$$

Step 4:缩放和平移($\gamma = 1, \beta = 0$ 时简化为直接输出)

$$y = \gamma \cdot \hat{x} + \beta = [-0.447, 1.342, 0.447, -1.342]$$

解读:原始向量 $[1, 5, 3, -1]$ 的值范围差异大(-1 到 5),LayerNorm 后变为 $[-0.45, 1.34, 0.45, -1.34]$,均值为 0、标准差为 1。这稳定了数值范围,让后续层的梯度更平稳。$\gamma$ 和 $\beta$ 是可学习参数,让模型可以"恢复"需要的分布。

  1. Feed-Forward Network: Linear(512 → 2048) → ReLU → Linear(2048 → 512)
  2. Add & LayerNorm:$\text{norm}_2(h + \text{ffn\_output})$

输出:(batch, seq_len, 512) ← 维度不变,可以堆叠 N 层。

Feed-Forward Network (FFN)——每个位置的"独立思考"

$$ \text{FFN}(x) = \max(0, x \cdot W_1 + b_1) \cdot W_2 + b_2 $$

维度变化: 512 → 2048 → 512 。先升维 4 倍(512→2048),增加表达能力,再降回原维度(2048→512),方便后续处理。

为什么升维 4 倍?(问题→方案→代价)

维度分析
问题512 维空间中的非线性变换(只有 1 个隐藏层)表达能力有限,就像在二维平面上画一条曲线来分类三维空间的数据——维度不够,分类边界太简单
方案先升维到 2048(4 倍),在高维空间中做 ReLU 非线性变换,可以画出更复杂的"分类边界",再投影回 512 维。直觉:升维就像把数据"摊开"到更大的桌子上,更容易找到分界线
代价参数量增加:$512 \times 2048 + 2048 \times 512 = 2 \times 10^6$ 个参数(占 Encoder Block 总参数的 ~2/3)。但相对于 Attention 的 $O(n^2)$ 计算量,FFN 的参数量增加对训练速度影响较小

关键: FFN 对每个位置是独立计算的!位置 1 的 FFN 不看位置 2 、 3 、 4 的输出。 FFN 的作用是"独立思考":在 Attention 已经收集了全局信息后,每个位置独立地对这些信息做非线性变换。

动手计算:FFN 数值示例(简化为 2→4→2,实际是 512→2048→512)

输入:$x = [1, 2]$

Step 1:升维($W_1$:2×4)

$$x \cdot W_1 = [1, 2] \cdot \begin{bmatrix} 1 & 0 & 1 & -1 \\ 0 & 1 & -1 & 1 \end{bmatrix} = [1, 2, -1, 1]$$

Step 2:ReLU(关闭负值)

$$\text{ReLU}([1, 2, -1, 1]) = [1, 2, 0, 1]$$

第 3 维从 $-1$ 变为 $0$——ReLU 把它"关闭"了。

Step 3:降维($W_2$:4×2)

$$[1, 2, 0, 1] \cdot \begin{bmatrix} 1 & 1 \\ 1 & -1 \\ 0 & 1 \\ 1 & 0 \end{bmatrix} = [4, -1]$$

解读:输入 $[1, 2]$ 经过升维→ReLU→降维后变为 $[4, -1]$。ReLU 在高维空间中"关闭"了第 3 维,相当于做了一次非线性变换,改变了信息的表达方式。如果不用 ReLU(纯线性变换),$W_1 \cdot W_2$ 可以合并为一个矩阵,升维就失去意义了——这就是"万能近似定理"的核心:非线性 + 高维 = 任意函数拟合能力。

Attention vs FFN 的分工

  • Attention:收集信息(“看看别人说了什么”)→ 类比:看参考资料
  • FFN:处理信息(“想想自己该怎么理解”)→ 类比:独立答题

1.5 Masked Self-Attention 与 Decoder

为什么 Decoder 需要 Mask ?

类比:考试时不能偷看答案

训练时,我们知道完整的译文:“我 爱 大 模型”。但我们不能让模型"偷看"未来的词!

  • 预测"爱"时:只能看到 “<start>” 和 “我”
  • 预测"大"时:只能看到 “<start>"、“我” 和 “爱”
  • 预测"模型"时:只能看到 “<start>"、“我”、“爱” 和 “大”

如果不加 Mask ,模型会直接抄答案,什么都学不到!

Mask 的实现——下三角矩阵

注意力分数矩阵(4 个词):

start
start可见隐藏隐藏隐藏
可见可见隐藏隐藏
可见可见可见隐藏
可见可见可见可见

“可见” = 正常计算注意力,“隐藏” = 设为 $-\infty$,softmax 后变为 0

$$ \text{scores} = \text{scores.masked\_fill}(\text{mask} == 0, -10^9) $$

动手计算:Mask 前后的 Softmax 数值对比

假设 4 个词的注意力分数矩阵(以"爱"这一行为例,正在预测第 3 个词):

遮盖前(无 Mask):模型可以看到所有位置

<start>
(score)1.20.82.11.5
$$ \text{softmax}([1.2, 0.8, 2.1, 1.5]) = [0.18, 0.12, 0.45, 0.25] $$

“爱"对所有词都有注意力权重——包括未来的"大”(0.25),这就是"偷看答案”!

遮盖后(有 Mask):将未来位置设为 $-10^9$,softmax 后变为 0

<start>
(score)1.20.8$-10^9$$-10^9$
$$ \text{softmax}([1.2, 0.8, -10^9, -10^9]) = [0.60, 0.40, 0.00, 0.00] $$

解读:Mask 后,“爱"只能关注”<start>"(0.60)和"我”(0.40),未来位置的权重严格为 0。这迫使模型在预测"爱"时只能基于已生成的前两个词,学会了真正的自回归生成能力。

数值直觉:$-10^9$ 经过 softmax 后 $e^{-10^9} \approx 0$,相当于把该位置的注意力"彻底关闭"。实际实现中常用 $-\infty$ 或一个足够大的负数。

Encoder-Decoder Cross-Attention——连接两个世界的桥梁

Decoder 的第二个 Attention 层(Cross-Attention):

  • Query 来自 Decoder(“我在找什么信息?")
  • Key 和 Value 来自 Encoder(“源语言提供了什么信息?")

类比:翻译时的"参考原文”。 Query = 你正在翻译的当前词的需求(“我现在需要翻译’爱’,原文中哪里有相关信息?")。 Key/Value = 原文中每个词的信息。

Cross-Attention 让 Decoder 在生成每个译文词时,都能"回头看"原文的相关部分 → 这就是"注意力对齐”:自动学到"哪个译文词对应哪个原文词”。

动手计算:Cross-Attention 数值示例

翻译任务:源语言 “I love”(Encoder 输出),目标语言 “我 爱”(Decoder 已生成)。

假设 Encoder 输出的 K、V(2 词,$d_k=4$):

$K$(来自 Encoder)$V$(来自 Encoder)
I[1, 0, 1, 0][0, 1, 1, 0]
love[0, 1, 0, 1][1, 0, 0, 1]

Decoder 当前正在翻译"爱",其 Q(来自 Decoder):$Q_{\text{爱}} = [0, 1, 1, 0]$

Step 1:计算注意力分数

$$ \begin{aligned} \text{score}(\text{爱}, I) &= [0,1,1,0] \cdot [1,0,1,0] = 0+0+1+0 = 1 \\ \text{score}(\text{爱}, \text{love}) &= [0,1,1,0] \cdot [0,1,0,1] = 0+1+0+0 = 1 \end{aligned} $$

Step 2:缩放 + Softmax → $\text{softmax}([0.5, 0.5]) = [0.500, 0.500]$

Step 3:加权求和 → $\text{output} = 0.5 \times [0,1,1,0] + 0.5 \times [1,0,0,1] = [0.5, 0.5, 0.5, 0.5]$

解读:“爱"对 “I” 和 “love” 的关注度相同(各 0.5),因为"爱"既关联主语 “I” 也关联动词 “love”。实际训练中,Q/K/V 都是可学习的,模型会自动学到"翻译’爱’时应该更关注 ’love’“这种对齐关系。

1.6 Transformer 代码实现

import torch
import torch.nn as nn
import math

class MultiHeadAttention(nn.Module):
    """多头注意力:将输入拆分为多个头,并行计算不同子空间的注意力"""
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads  # 每个头的维度(如 512/8 = 64)
        
        # Q/K/V 三个不同的线性变换(为什么三个?因为需求、标签、内容是不同的)
        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)  # 输出投影:将多头拼接结果映射回 d_model
    
    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)
        
        # 线性变换 + 拆分为多头:(batch, seq_len, d_model) → (batch, n_heads, seq_len, d_k)
        Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        
        # 计算注意力分数并缩放(防止 softmax 梯度消失)
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        
        if mask is not None:  # Decoder 的 Masked Attention:遮盖未来位置
            scores = scores.masked_fill(mask == 0, -1e9)
        
        attn_weights = torch.softmax(scores, dim=-1)  # 归一化为概率分布
        context = torch.matmul(attn_weights, V)        # 加权求和
        
        # 合并多头:(batch, n_heads, seq_len, d_k) → (batch, seq_len, d_model)
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        output = self.W_O(context)  # 输出线性投影
        return output


class TransformerBlock(nn.Module):
    """Encoder Block:Self-Attention + FFN + 残差连接 + LayerNorm"""
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()
        self.attention = MultiHeadAttention(d_model, n_heads)
        self.norm1 = nn.LayerNorm(d_model)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff),  # 升维(512→2048)
            nn.ReLU(),                 # 非线性变换
            nn.Linear(d_ff, d_model)   # 降维(2048→512)
        )
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)  # 防止过拟合
    
    def forward(self, x, mask=None):
        # 子层 1:多头自注意力 + 残差连接 + LayerNorm
        attn_output = self.attention(x, x, x, mask)  # Q=K=V=x(自注意力)
        x = self.norm1(x + self.dropout(attn_output))  # 残差连接保留原始信息
        
        # 子层 2:FFN + 残差连接 + LayerNorm
        ffn_output = self.ffn(x)
        x = self.norm2(x + self.dropout(ffn_output))
        
        return x

1.7 核心总结

Transformer 的五大核心创新

  1. Self-Attention:让每个位置都能直接访问其他所有位置 → 解决了 RNN 的"逐步传递"问题
  2. Multi-Head Attention:从多个角度并行建模关系 → 同时捕获语法、语义、位置等多种关系
  3. Positional Encoding:用正弦/余弦函数注入位置信息 → 让模型知道"词的顺序”
  4. 残差连接 + LayerNorm:稳定深层网络训练 → 来自 ResNet 的智慧,让梯度可以"跳过"层
  5. 并行计算:所有位置同时计算 → 训练速度比 RNN 快 10-100 倍

Transformer vs RNN 对比

特性RNNTransformer
计算方式串行(逐步)并行(所有位置同时)
长距离依赖困难(梯度消失)容易(直接连接)
计算复杂度$O(n \cdot d^2)$$O(n^2 \cdot d)$
复杂度直觉序列越长计算量线性增长(n),但每个时间步的矩阵运算($d^2$)很大序列越长计算量平方增长($n^2$),但可并行;当 $n < d$ 时 Transformer 更快
训练速度快(10-100 倍)
序列长度限制无理论限制受限于显存($n^2$ 注意力)
参数量多(但效果更好)
实际表现较好显著更好(大模型时代的基石)

模块小结: Transformer 架构

你学到了什么为什么重要
Self-Attention 机制让每个词直接访问所有其他词
Q/K/V 三组向量分别表示需求、标签、内容
多头注意力从多个角度并行建模关系
位置编码注入词序信息
Encoder Block 完整结构Attention + FFN + 残差 + LayerNorm
Masked Self-Attention防止解码器"偷看"未来信息
前面我们深入理解了 Transformer 的内部结构——Self-Attention 如何让每个词"看到"所有其他词、多头注意力如何从不同角度捕获关系、位置编码如何注入词序信息。但 Transformer 只是一个骨架,真正的威力来自于在海量文本上预训练出的语言模型。接下来我们看两个最重要的预训练范式:BERT(双向理解)和 GPT(自回归生成),它们分别统治了 NLP 的"理解"和"生成"两大领域。

下一篇预告:本文深入理解了 Transformer 的内部结构。在速通 AI(四):预训练语言模型——BERT 与 GPT中,你将看到 Transformer 如何通过预训练变成 BERT(双向理解)和 GPT(自回归生成),以及如何用 HuggingFace 生态快速完成 NLP 任务开发。