Transformer 解剖:从 Attention 到推理系统

第 2 章 Self-Attention:Q/K/V 三元组与缩放点积

作者 杨艺韬 · 6,118 字

第 2 章 Self-Attention:Q/K/V 三元组与缩放点积

整本书最核心的一个公式:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V

这一章我们不会把它当成「神奇魔法」一笔带过,而是要从它要解决的具体问题出发,一步步把它推出来。读完这章你应该能做到:在一张白板上写出这个公式、解释每一项的形状(shape)、说出为什么是这一项而不是别的、并能在一个 4 token 的小例子上手算一遍它的输出。

我们按下面的顺序展开:先从一个真实的语言现象引出 Self-Attention 要解决的问题,再讲清楚「注意力」作为一种软性查询机制的直觉,然后把 Q/K/V 三元组从这个直觉里推出来,接着拆解缩放点积和 softmax 各自在做什么,最后用一个具体数值例子把整套计算跑一遍。

2.1 一句话引出来的问题

考虑这个英文句子:

The animal didn't cross the street because it was too tired.

你的大脑在读到 "it" 这个词时,几乎瞬间就知道它指代的是 "animal" 而不是 "street"。但这个判断不是凭借词典——"it" 这个词本身没有任何信息能告诉你它是动物还是街道。你的判断依赖上下文:你看到了 "tired",你知道街道不会累,所以 "it" 必然指代某个会累的东西,于是定位到 "animal"。

把同一个句子的最后一个词换一下:

The animal didn't cross the street because it was too wide.

现在 "it" 的指代立刻从 "animal" 切换到 "street"——因为 "wide"(宽)描述的是街道的属性而不是动物的属性。

这个现象给我们提了一个尖锐的工程问题:当我们要给 "it" 这个 token 计算一个表示(embedding)时,这个表示应该是什么?

如果用 word2vec 这种静态嵌入,"it" 永远是同一个向量。但显然,「指代 animal 的 it」和「指代 street 的 it」应该有不同的表示——前者应该带上「动物 / 会累 / 不能过马路」的语义,后者应该带上「街道 / 太宽 / 阻碍通行」的语义。这就要求 token 的表示是上下文相关(contextual)的:同一个词,在不同句子里、不同位置,应该被「染色」成不同的向量。

这就是 Self-Attention 要解决的核心问题:给序列中每个 token 一个上下文相关的表示,让它能根据周围的 token 来调整自己

更具体地说,Self-Attention 要回答:「给我 'it' 这个 token,我应该从其他哪些 token 那里 多少信息,来组合成 'it' 在当前语境下的最终表示?」

flowchart LR
  A[The] -->|"0.05"| IT[it]
  B[animal] ==>|"0.45"| IT
  C["didn't"] -->|"0.02"| IT
  D[cross] -->|"0.05"| IT
  E[the] -->|"0.03"| IT
  F[street] -->|"0.10"| IT
  G[because] -->|"0.05"| IT
  H[was] -->|"0.05"| IT
  I[too] -->|"0.05"| IT
  J[tired] ==>|"0.15"| IT
  IT --> Z["最终的 it 表示<br/>= 0.45·animal + 0.15·tired<br/>+ 0.10·street + ..."]

每条箭头粗细对应权重大小。Self-Attention 实际上就是这样一个**对每个 token 都做一次「按权重加权求和」**的机制——权重是动态算的、依赖于具体的 token 内容。

接下来我们要做的是:把这个直觉翻译成可学习的数学结构。

2.2 注意力作为软性查询

注意力机制的数学本质,是一种软性的、基于内容的检索(content-based soft retrieval)。

类比一下我们熟悉的硬性检索——Python 字典:

d = {"apple": 5, "banana": 3, "orange": 7}
result = d["apple"]   # 5

字典做的事是:你给一个 key("apple"),它精确匹配字典里的某个 key,返回对应的 value。「精确匹配」意味着权重要么是 1 要么是 0。

如果我们把这件事软化——也就是「不要求精确匹配,而是按相似度给所有 key 分配权重」——会怎样?这就是注意力。

考虑这样一个软性查询:

写成公式:

output=i=1nαivi,αi=sim(q,ki)jsim(q,kj)\text{output} = \sum_{i=1}^{n} \alpha_i v_i, \quad \alpha_i = \frac{\text{sim}(q, k_i)}{\sum_j \text{sim}(q, k_j)}

其中 sim(,)\text{sim}(\cdot, \cdot) 是某种衡量相似度的函数。把分母里的归一化看作是把分数化成概率,权重 αi\alpha_i 加起来等于 1。

这个结构有两个非常重要的性质:

性质一:可微。所有运算(点积、加权求和、归一化)都是可微的,意味着这件事可以塞进一个用反向传播训练的神经网络里——权重 αi\alpha_i 不是手工设计的,是端到端学出来的。

性质二:内容寻址。检索结果完全由 qqkik_i 的内容决定,不依赖位置。同一个 qq 来查询,无论 kik_i 排在第 1 位还是第 100 位,只要它和 qq 的相似度高,就能拿到大权重。这是和 RNN 最根本的区别——RNN 是「按位置传递信息」,Attention 是「按内容寻找信息」。

到这里,我们手上有了一个抽象的查询机制。下一步是把它套到 Self-Attention 这个具体场景:输入是一串 token,输出也是一串 token,每个输出 token 的表示是从所有输入 token「软查询」出来的

2.3 从直觉到 Q/K/V 三元组

现在的核心问题变成:输入序列 x1,,xTx_1, \dots, x_T,每个 xix_i 都既是「问者」(要查询其他 token 来组合自己的新表示),又是「被问者」(其他 token 也要从它这里取信息)。我们要给每个 xix_i 配上 qqkkvv 三个向量来分别扮演这三种角色

为什么是三个不同的向量,而不是一个 token 用同一个向量分别充当 query、key、value?

考虑一下「问者」和「被问者」需要的信息可能是不一样的。当 "it" 作为问者去查询时,它要传达的是「我是一个代词,正在找指代对象」——这是一个关于「我要什么」的描述。当 "animal" 作为被问者被查询时,它要让自己「容易被找到」——它需要一个描述「我能提供什么」的指纹。这两件事本质上是两套不同的语义表示,强行让它们用同一个向量会损失表达能力。

更进一步:被找到之后,"animal" 实际要提供给 "it" 的信息(value),又是另一回事——比如「我是动物 / 会累 / 是有生命的实体 / 当前句子的主语」这样的具体属性。这又是第三套表示。

所以 Self-Attention 让每个输入 xix_i 通过三个独立的线性投影得到三个向量:

qi=xiWQ,ki=xiWK,vi=xiWVq_i = x_i W_Q, \quad k_i = x_i W_K, \quad v_i = x_i W_V

其中 WQ,WK,WVRdmodel×dkW_Q, W_K, W_V \in \mathbb{R}^{d_{\text{model}} \times d_k} 是三个可学习的参数矩阵(实际工程上通常 dq=dkd_q = d_kdvd_v 可以不同;本书后续除非特别说明,按主流约定 dq=dk=dv=dkd_q = d_k = d_v = d_k 处理)。

把整批 token 的嵌入堆成矩阵 XRT×dmodelX \in \mathbb{R}^{T \times d_{\text{model}}}(第 ii 行是 xix_i),就有:

Q=XWQ,K=XWK,V=XWVQ = X W_Q, \quad K = X W_K, \quad V = X W_V

矩阵形状:Q,K,VRT×dkQ, K, V \in \mathbb{R}^{T \times d_k}

这个做法在直觉上有一个非常迷人的解读:模型在学习的过程中,会自动让 WQW_Q 把每个 token 投影到一个「我要查什么」的空间,让 WKW_K 把每个 token 投影到一个「我是谁」的空间,让 WVW_V 把每个 token 投影到一个「我能提供什么」的空间。三个空间的几何结构,决定了模型在不同语境下的注意力分布。

flowchart TB
  subgraph 输入
    X1[x_1: 'The']
    X2[x_2: 'animal']
    X3[x_3: 'it']
  end
  subgraph 三套投影
    X1 --> Q1[q_1]
    X1 --> K1[k_1]
    X1 --> V1[v_1]
    X2 --> Q2[q_2]
    X2 --> K2[k_2]
    X2 --> V2[v_2]
    X3 --> Q3[q_3]
    X3 --> K3[k_3]
    X3 --> V3[v_3]
  end
  Q1 -.我要找什么.-> SPACE1[Query 空间]
  K1 -.我是谁.-> SPACE2[Key 空间]
  V1 -.我能提供什么.-> SPACE3[Value 空间]

值得停下来强调一下:Q、K、V 都是从同一个输入 XX 算出来的。这就是 "Self"-Attention 中 "Self" 的含义——查询、键、值都来自同一个序列。在 Encoder-Decoder 的 Cross-Attention 里(第 6 章会讲),QQ 来自 Decoder,K,VK, V 来自 Encoder——那是「跨」(cross)注意力。Self-Attention 是「自己」对「自己」的注意力。

2.4 用点积衡量相似度

有了 QQKK,下一步是计算第 ii 个 query 和第 jj 个 key 之间的「相似度分数」。Self-Attention 选择了点积

scoreij=qikj=qikjT\text{score}_{ij} = q_i \cdot k_j = q_i k_j^T

为什么是点积?这是一个值得展开的问题。

几何直觉:两个向量的点积等于它们的模长之积乘以夹角的余弦:

qikj=qikjcosθq_i \cdot k_j = \|q_i\| \|k_j\| \cos\theta

当两个向量方向一致时,余弦接近 1,点积大;方向正交时,余弦为 0,点积为 0;方向相反时,余弦为 -1,点积大幅为负。所以点积可以作为「方向相似度」的度量

计算便利:点积只需要一次乘加(multiply-add),是 GPU 上最高效的操作之一。一个 QRT×dkQ \in \mathbb{R}^{T \times d_k}KRT×dkK \in \mathbb{R}^{T \times d_k} 之间的全部相似度可以一次矩阵乘法 QKTQK^T 完成,结果是 RT×T\mathbb{R}^{T \times T},第 (i,j)(i, j) 项就是 qikjq_i \cdot k_j。这是 Transformer 能高效跑在 GPU 上的关键工程性质。

对比其他选择:早期 Bahdanau attention 用的是「加性注意力」(additive attention):

scoreij=wTtanh(Wqqi+Wkkj)\text{score}_{ij} = w^T \tanh(W_q q_i + W_k k_j)

它用一个小的前馈网络计算分数。理论上加性注意力的表达能力更强,但实践中点积注意力dkd_k 不太小时性能相当,且计算速度快得多——所以工业界统一选了点积(这一选择在 Attention Is All You Need 论文 3.2.1 节有详细对比)。

把所有 query 和所有 key 之间的分数排成一张表:

S=QKTRT×T,Sij=qikjS = QK^T \in \mathbb{R}^{T \times T}, \quad S_{ij} = q_i \cdot k_j

这就是 Self-Attention 的「分数矩阵」。它是一张 T×TT \times T 的方阵,第 ii 行表示「token ii 作为 query 时,对所有 token 的关注度」,第 jj 列表示「token jj 作为 key 时,被所有 query 关注的程度」。

2.5 那个 √d_k 不能省

接下来是公式里很多人困惑的一项:分数为什么要除以 dk\sqrt{d_k}

短答:为了防止点积的方差随维度增长,把 softmax 推向饱和区,导致梯度消失

长答需要一点概率推导。假设 qqkk 的每个分量都是独立、零均值、方差为 1 的随机变量(这在初始化阶段近似成立)。它们的点积是:

qk=i=1dkqikiq \cdot k = \sum_{i=1}^{d_k} q_i k_i

对独立零均值变量 qikiq_i k_i,有:

由独立性,方差具有可加性:

Var(qk)=i=1dkVar(qiki)=dk\text{Var}(q \cdot k) = \sum_{i=1}^{d_k} \text{Var}(q_i k_i) = d_k

也就是说,点积 qkq \cdot k 的方差正比于维度 dkd_k,标准差是 dk\sqrt{d_k}

这意味着什么?当 dkd_k 很大时(典型 64、128 甚至 256),点积分数的尺度会非常大——可能从几十到几百。把这样的大分数喂进 softmax 会发生什么?

考虑一个简单的两个 score 的例子:score 1 = 5, score 2 = 4,softmax 后是 (0.73,0.27)(0.73, 0.27),分布较平。但 score 1 = 50, score 2 = 40,softmax 后是 (0.99995,0.00005)(0.99995, 0.00005),几乎是「one-hot 化」的——所有概率都集中到最大值,其他的几乎为 0。

这种「极端尖锐」的分布在反向传播时会带来一个致命问题:softmax 在饱和区的梯度极小。形式化地,softmax 的雅可比矩阵是:

αisj=αi(δijαj)\frac{\partial \alpha_i}{\partial s_j} = \alpha_i (\delta_{ij} - \alpha_j)

αi1\alpha_i \approx 1 而其他 αj0\alpha_j \approx 0 时,雅可比矩阵几乎全为 0,梯度无法回传。模型陷入「梯度消失」——一旦初始化阶段 attention 进入饱和区,就出不来了。

解决办法:把 score 除以 dk\sqrt{d_k},让方差归一化回到 1:

Var(qkdk)=dkdk=1\text{Var}\left(\frac{q \cdot k}{\sqrt{d_k}}\right) = \frac{d_k}{d_k} = 1

这样无论 dkd_k 是 64 还是 256,进入 softmax 的 score 尺度都稳定在一个合理范围(标准差为 1),softmax 输出既不会饱和,也能保留有意义的梯度。

公式里的 dk\sqrt{d_k} 就是从这个方差归一化推出来的。它不是一个「凑出来」的数字,而是有清晰统计学根据的工程修正项。

dkd_k 不缩放时点积标准差 不缩放 softmax 行为 缩放后行为
16 4 略尖锐,但还可学 平滑
64 8 接近饱和 平滑
256 16 严重饱和 平滑
1024 32 完全饱和,模型废了 平滑

这一行的最后一栏说明:没有 dk\sqrt{d_k} 这一刀,Transformer 在 dkd_k 较大的实际配置下根本训不起来。这是早期工程的一个真实痛点,也是论文 3.2.1 节专门强调的设计决定。

2.6 softmax 不是分类,是注意力归一化

把缩放过的分数喂进 softmax:

A=softmax(QKTdk),Aij=exp(sij/dk)j=1Texp(sij/dk)A = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right), \quad A_{ij} = \frac{\exp(s_{ij}/\sqrt{d_k})}{\sum_{j'=1}^{T} \exp(s_{ij'}/\sqrt{d_k})}

注意 softmax 是沿着每一行做归一化的——不是沿列,也不是全矩阵。每一行对应一个 query token 对所有 key 的注意力分布,加起来等于 1。

很多读者会困惑:softmax 不是用来做分类的吗?这里它在干什么?

答案:在分类任务里,softmax 把模型 logits 转成「每个类别的概率」;在 Self-Attention 里,softmax 把 score 转成「每个 key 被关注的权重」。两者的数学是同一个 softmax,语义不同——一个是「概率分布」,一个是「注意力分布」。

但这两个用途共享一个核心动机:把任意实数 score 转成「合法的权重」(非负、和为 1)。非负保证「负相关也是相关,但不会反向贡献」;和为 1 保证最终的加权求和不会爆炸。

flowchart LR
  S[score 矩阵<br/>T x T<br/>实数] --> SM[softmax<br/>沿行归一化]
  SM --> A[注意力矩阵 A<br/>T x T<br/>每行加起来=1]
  A --> WS["A · V<br/>每个 query 拿到的<br/>加权 value"]

得到注意力矩阵 AA 之后,最后一步:用它对 VV 做加权求和:

output=AV\text{output} = AV

形状:ART×TA \in \mathbb{R}^{T \times T}VRT×dkV \in \mathbb{R}^{T \times d_k},结果 RT×dk\in \mathbb{R}^{T \times d_k}。第 ii 行就是「token ii 的输出表示」,等于所有 vjv_j 的加权和:

outputi=j=1TAijvj\text{output}_i = \sum_{j=1}^{T} A_{ij} v_j

这正是 2.1 节我们的直觉——「it」的最终表示是 0.45·animal + 0.15·tired + 0.10·street + ...——的精确数学形式。

2.7 整个公式串起来

把上面五步汇总:

  1. 三套投影Q=XWQQ = XW_QK=XWKK = XW_KV=XWVV = XW_V
  2. 算分数S=QKTS = QK^T
  3. 缩放S=S/dkS' = S / \sqrt{d_k}
  4. softmax 归一化A=softmax(S)A = \text{softmax}(S')
  5. 加权求和output=AV\text{output} = AV

合起来就是一行:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V

整个过程的张量形状流转,画在一起:

flowchart LR
  X["X<br/>(T, d_model)"] --> WQ["× W_Q<br/>(d_model, d_k)"] --> Q["Q<br/>(T, d_k)"]
  X --> WK["× W_K<br/>(d_model, d_k)"] --> K["K<br/>(T, d_k)"]
  X --> WV["× W_V<br/>(d_model, d_v)"] --> V["V<br/>(T, d_v)"]
  Q --> MUL["matmul<br/>QK^T"]
  K --> MUL
  MUL --> S["S<br/>(T, T)"] --> SCALE["÷ √d_k"] --> SM[softmax 沿行] --> A["A<br/>(T, T)"]
  A --> MUL2[matmul AV]
  V --> MUL2
  MUL2 --> OUT["output<br/>(T, d_v)"]

每个箭头都标了形状,读者可以对着这张图核对自己的实现。

值得注意的两件事:

第一WQ,WK,WVW_Q, W_K, W_V 是参数(训练时学习),XX 是输入。模型学到的「关注什么」,最终都体现在这三个权重矩阵上。

第二,整个计算路径里没有任何递归——所有的矩阵乘法和 softmax 都是「一击即中」的并行计算。这是第 1 章我们说的「训练完全并行」的具体兑现。

2.8 一个手算例子

光看公式没感觉,我们用一个最小的具体例子从头到尾算一遍。

设输入是 4 个 token(不必关心是什么词,把它们当作已经嵌入到 dmodel=4d_{\text{model}} = 4 维的向量),dk=2d_k = 2(为了手算可行):

X=(1010010111000011)R4×4X = \begin{pmatrix} 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 1 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 \end{pmatrix} \in \mathbb{R}^{4 \times 4}

随机但固定的投影矩阵:

WQ=(10011100),WK=(01100110),WV=(11011001)W_Q = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \\ 0 & 0 \end{pmatrix}, \quad W_K = \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ 0 & 1 \\ 1 & 0 \end{pmatrix}, \quad W_V = \begin{pmatrix} 1 & 1 \\ 0 & 1 \\ 1 & 0 \\ 0 & 1 \end{pmatrix}

第一步:计算 Q=XWQQ = XW_Q。第 ii 行的 qiq_ixix_iWQW_Q 的乘积。

Q=(11+00+11+0010+01+11+00)=(21011111)Q = \begin{pmatrix} 1\cdot 1+0\cdot 0+1\cdot 1+0\cdot 0 & 1\cdot 0+0\cdot 1+1\cdot 1+0\cdot 0 \\ \dots \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 0 & 1 \\ 1 & 1 \\ 1 & 1 \end{pmatrix}

类似地:

K=XWK=(02201111),V=XWV=(21021211)K = XW_K = \begin{pmatrix} 0 & 2 \\ 2 & 0 \\ 1 & 1 \\ 1 & 1 \end{pmatrix}, \quad V = XW_V = \begin{pmatrix} 2 & 1 \\ 0 & 2 \\ 1 & 2 \\ 1 & 1 \end{pmatrix}

第二步:算 S=QKTS = QK^T

S=(21011111)(02112011)=(2433201122222222)S = \begin{pmatrix} 2 & 1 \\ 0 & 1 \\ 1 & 1 \\ 1 & 1 \end{pmatrix} \begin{pmatrix} 0 & 2 & 1 & 1 \\ 2 & 0 & 1 & 1 \end{pmatrix} = \begin{pmatrix} 2 & 4 & 3 & 3 \\ 2 & 0 & 1 & 1 \\ 2 & 2 & 2 & 2 \\ 2 & 2 & 2 & 2 \end{pmatrix}

第三步:缩放 S=S/dk=S/2S' = S / \sqrt{d_k} = S / \sqrt{2}S/1.414\approx S/1.414)。

S(1.412.832.122.121.4100.710.711.411.411.411.411.411.411.411.41)S' \approx \begin{pmatrix} 1.41 & 2.83 & 2.12 & 2.12 \\ 1.41 & 0 & 0.71 & 0.71 \\ 1.41 & 1.41 & 1.41 & 1.41 \\ 1.41 & 1.41 & 1.41 & 1.41 \end{pmatrix}

第四步:沿行 softmax。以第 1 行为例:

exp(1.41),exp(2.83),exp(2.12),exp(2.12)4.10,16.95,8.33,8.33\exp(1.41), \exp(2.83), \exp(2.12), \exp(2.12) \approx 4.10, 16.95, 8.33, 8.33

行和 37.71\approx 37.71。归一化后:

A1,:(0.109,0.450,0.221,0.221)A_{1,:} \approx (0.109, 0.450, 0.221, 0.221)

第 2 行(注意 0 的存在让分布更尖锐些):

exp(1.41),exp(0),exp(0.71),exp(0.71)4.10,1.00,2.03,2.03\exp(1.41), \exp(0), \exp(0.71), \exp(0.71) \approx 4.10, 1.00, 2.03, 2.03

行和 9.16\approx 9.16,归一化:

A2,:(0.448,0.109,0.222,0.222)A_{2,:} \approx (0.448, 0.109, 0.222, 0.222)

第 3、4 行所有 score 相等(都是 1.41),softmax 后是均匀分布:

A3,:=A4,:(0.25,0.25,0.25,0.25)A_{3,:} = A_{4,:} \approx (0.25, 0.25, 0.25, 0.25)

完整:

A(0.1090.4500.2210.2210.4480.1090.2220.2220.250.250.250.250.250.250.250.25)A \approx \begin{pmatrix} 0.109 & 0.450 & 0.221 & 0.221 \\ 0.448 & 0.109 & 0.222 & 0.222 \\ 0.25 & 0.25 & 0.25 & 0.25 \\ 0.25 & 0.25 & 0.25 & 0.25 \end{pmatrix}

第五步:算 output=AV\text{output} = AV。以第 1 行为例:

output1=0.109(2,1)+0.450(0,2)+0.221(1,2)+0.221(1,1)\text{output}_1 = 0.109 \cdot (2,1) + 0.450 \cdot (0,2) + 0.221 \cdot (1,2) + 0.221 \cdot (1,1)

逐元素相加:

=(0.218,0.109)+(0,0.900)+(0.221,0.442)+(0.221,0.221)(0.660,1.672)= (0.218, 0.109) + (0, 0.900) + (0.221, 0.442) + (0.221, 0.221) \approx (0.660, 1.672)

类似地算其余行,最终:

output(0.6601.6721.3371.2241.0001.5001.0001.500)\text{output} \approx \begin{pmatrix} 0.660 & 1.672 \\ 1.337 & 1.224 \\ 1.000 & 1.500 \\ 1.000 & 1.500 \end{pmatrix}

到这里,第一个 token 已经从最初的 x1=(1,0,1,0)x_1 = (1, 0, 1, 0),经过 Self-Attention 变成了一个新表示 (0.660,1.672)(0.660, 1.672)——这个新表示融合了所有四个输入 token 的 value 信息,按它们与 x1x_1 的 query/key 相似度加权。

这就是 Self-Attention 的全部计算。所有后面要讲的 Multi-Head、位置编码、KV Cache、PagedAttention,都是建立在这五步之上的优化和扩展。

2.9 Self-Attention 的几个关键性质

到这里,我们可以从这个小例子里读出 Self-Attention 的几个本质性质。这些性质后面会反复用到。

排列等变性(Permutation Equivariance)

如果你把输入序列的 token 顺序打乱,Self-Attention 的输出会跟着同样地打乱(值不变,只是位置换了)。也就是说,Self-Attention 本身不感知位置——它把序列当作一个无序的集合在处理。

形式化:设 PP 是某个置换矩阵,则 Attention(PXWQ,PXWK,PXWV)=PAttention(XWQ,XWK,XWV)\text{Attention}(PX W_Q, PX W_K, PX W_V) = P \cdot \text{Attention}(XW_Q, XW_K, XW_V)

这是一个关键的发现,因为它揭示了一个问题:Self-Attention 自己看不到「animal 在 it 之前」这个事实。要让它知道位置信息,必须从外部注入——这就是位置编码(第 4 章)的来历。

全局连接性

Self-Attention 中任意两个位置都可以直接交流,距离是 O(1) 的——一次矩阵乘法。这彻底打破了 RNN 的 O(N) 信息传递路径。

内容寻址而非位置寻址

注意力权重由 query 和 key 的内容相似度决定,与它们在序列中的位置无关。这意味着模型能学会「相同句法角色的 token 更应互相关注」「同一个语义实体的不同提及应该聚类」这类高阶模式。

二次复杂度

QKTQK^T 的存储和计算都是 O(T2)O(T^2),softmax 之后还要再用 AAVV 也是 O(T2)O(T^2) 量级。这是 Transformer 在长上下文场景下的主要瓶颈——一个 1 万 token 的序列,attention 矩阵的元素数就是 1 亿。第 13 章会深入讨论怎么对付它。

2.10 因果掩码:Decoder 怎么用 Self-Attention

到这里我们讲的是 Self-Attention 的「双向版」——每个 token 都能看到序列里所有其他 token,包括它后面的。但语言模型(如 GPT)做的是自回归生成(autoregressive generation):在预测第 ii 个 token 时,模型只能看到第 1 到 i1i-1 个 token,不能偷看未来

怎么在 Self-Attention 里实现这件事?答案优雅得令人吃惊:在 softmax 之前,把上三角部分的 score 设成 -\infty

形式化:定义因果掩码(causal mask)MM,是一个上三角全为 -\infty、下三角及对角线为 0 的 T×TT \times T 矩阵:

Mij={0jij>iM_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases}

然后修改 attention 计算:

A=softmax(QKTdk+M)A = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right)

为什么是 -\infty?因为 exp()=0\exp(-\infty) = 0,softmax 的分母里这些项就贡献 0,分子里也贡献 0——也就是「这些位置不会被关注」。

实际工程实现里通常用一个非常大的负数(例如 109-10^9)代替 -\infty,因为浮点数没法直接表示无穷。

flowchart LR
  subgraph 原始 score 矩阵
    direction TB
    M0["
    s11 s12 s13 s14
    s21 s22 s23 s24
    s31 s32 s33 s34
    s41 s42 s43 s44
    "]
  end
  subgraph 加因果掩码后
    direction TB
    M1["
    s11 -∞  -∞  -∞
    s21 s22 -∞  -∞
    s31 s32 s33 -∞
    s41 s42 s43 s44
    "]
  end
  M0 --> M1 --> SM[softmax<br/>每行] --> A1["
    1.0  0.0  0.0  0.0
    a21  a22  0.0  0.0
    a31  a32  a33  0.0
    a41  a42  a43  a44
  "]

这意味着:

这正是自回归生成需要的「因果性」——在预测第 ii 个 token 时,模型的注意力只能落在已经生成的 token 上。

GPT、Llama、Claude 这一系列模型用的都是这种带因果掩码的 Self-Attention。BERT 这一系列则用不带掩码的双向 Self-Attention。第 6 章会讲清楚两种 mask 策略对应的两条架构路线。

2.11 Self-Attention 不止于此

第 2 章我们讲的是 Self-Attention 的「裸」版本:单个头、没有位置信息、没有掩码(或只有最简单的因果掩码)。但真正的 Transformer 不是这样工作的:

但这些都是建立在你刚才理解的这五步(投影 → 点积 → 缩放 → softmax → 加权求和)之上的扩展。把第 2 章这个「最小内核」吃透,后面所有内容都有了根基。

本章小结

这一章的关键 takeaway:

  1. Self-Attention 解决的核心问题:给每个 token 一个上下文相关的表示——同一个词在不同语境下应该被「染色」成不同向量。
  2. 三个角色三套投影:Q(我要查什么)、K(我是谁)、V(我能提供什么)通过三个独立的线性层从同一个输入生成,是同一个 token 在三种不同语义空间下的投影。
  3. 点积衡量相关性:通过几何上的方向相似度,配合 GPU 友好的矩阵乘法实现。
  4. 缩放因子 dk\sqrt{d_k} 是必要的:源自方差归一化,不缩放则 softmax 会饱和、梯度消失。
  5. softmax 在做注意力归一化:把分数转成「合法的权重分布」(非负、和为 1)。
  6. 完整公式 softmax(QKT/dk)V\text{softmax}(QK^T/\sqrt{d_k})V 是这五步的紧凑表达,张量形状从 X(T,dmodel)X(T, d_{\text{model}}) 走到 (T,dv)(T, d_v),整个过程没有任何递归。
  7. 因果掩码通过把上三角设成 -\infty,让 Self-Attention 在自回归场景下保持因果性,是 Decoder-only 架构的核心机制。

下一章我们升级到 Multi-Head Attention。直观上你可能觉得「多搞几个头无非是多做几次相同的事」,但实际上 Multi-Head 的设计有更深的几何意义:把 dkd_k 维空间切成 hh 个子空间,每个头在自己的子空间里独立做注意力——这让模型能在不同子空间里同时学习不同类型的关系(语法、共指、语义等)。第 3 章会讲清楚这件事的代数和几何含义。

延伸阅读