2025, May, 08
重读「Attention is All You Need」

在本文中,我会按照 NLP 发展的脉络,一步一步拆解 Transformer 机制

在 2017 年 Ashish Vaswani 等人在 NeurIPS 发表了 Attention is all you need 。这篇文章首次提出注意力机制进行序列建模,尽管 attention 机制在当初为了解决序列串行训练中的低效问题,实现了在大规模 GPU 上的并行训练,并在多年后成为了现代 LLM 的基础。

但说实话,这篇论文的文笔其实并不怎么样,文辞和图示也比较晦涩,笔者其实也吐槽很久了,它并不符合一般读者的习惯,更不适合新手入门。

在本文中,我会按照 NLP 发展的脉络,一步一步拆解 Transformer 机制中“为什么需要这么设计”,以及“每个公式都是怎么得出来的”。尽管如此,你仍然需要具备基本的线性代数、机器学习常识。

语义与含义

在机器学习中的流形假设中,人的思维空间是一个高维欧式空间,词汇可以通过向量 vRd\boldsymbol{v} \in \mathbb{R}^d 表示。而一个词的语义往往是丰富的,即一个词可能含有多种语义。我们可以认为,一个词的含义是多种语义的组合。

在思维空间里,我们把含义与词汇的概念等同。可以理解为,维度是语义的单元。也是组成含义的最小单元

为了简化说明,我们考虑一个最简单的情况:描述一个颜色可以使用三原色(R,G,B),此时语义可以划分成三种相互正交的维度:红色度、绿色度、蓝色度。

我们可以建立一个三维空间 R3\mathbb{R}^3,一个描述颜色的词汇,就包含三种最简单的语义:R、G、B。可以得出一些简单的颜色词汇的向量表示:

纯红 (255,0,0) 纯绿 (0,255,0)
粉色 (255,192,203) 天蓝 (135,206,235)

R\G\B 的三种维度的语义组成了一个颜色的含义。

向量中各个维度中的数字的组合构成了含义,也就能表达一个词汇,一个词汇表达 dd 种语义。词汇是离散的,所以含义是「分布式表示」的。

在此基础上,提出两种假设:

多个含义的加权组合可以合成一个含义。
一个含义可以分解成多个含义的加权组合。

对于第两种假设很好理解,例如皇后(Queen),含义为「国王的妻子」。

从含义分解视角,可以形式化表示为:

vqueen=w1vking+w2vwife,w1w20\boldsymbol{v}_\text{queen} = w_1 · \boldsymbol{v}_\text{king} + w_2 · \boldsymbol{v}_\text{wife}, \quad w_1 · w_2 \neq 0

或者反过来,得到合成视角:

w1vking+w2vwife=vqueen,w1w20w_1 · \boldsymbol{v}_\text{king} + w_2 · \boldsymbol{v}_\text{wife} = \boldsymbol{v}_\text{queen}, \quad w_1 · w_2 \neq 0

由此观之,一种含义既可以由多个其他含义进行加权组合,也可以和其他含义加权组合成新的含义。

现在,我们把视角从词汇推广到一个句子。句子和词汇也是一样,也可以看做成「一个含义的表达」。

一个句子由多种词汇的有序序列 VRn×dV \in \mathbb{R}^{n \times d} 组成。为了简化说明,我们先暂时忽略掉它们的顺序。我们有:

V=(v1,v2,v3,...,vn)V = (\boldsymbol{v}_1,\boldsymbol{v}_2,\boldsymbol{v}_3,...,\boldsymbol{v}_n)

一个句子也可以表达一种含义。设权重矩阵为 WRn×1W \in \mathbb{R}^{n \times 1},那么此时,句子的含义可以写作各个含义向量的加权组合的向量 s\boldsymbol{s}

s=VTW=i=1nwiviRd\boldsymbol{s} = V^T · W = \sum_{i=1}^{n} w_i · \boldsymbol{v}_i \in \mathbb{R}^d

同理,以上过程还可以推广到一个段落、一篇文章、一本文集。

我们也可以知道,如果我们能把一个含义分解的语义颗粒度越细,那么这个词汇能表达的含义也就越细腻精确,即维度 dd 越高,词汇的含义表达越强。

上下文关联

在上面的演示中,我们假设的是一个词汇对应着一个唯一的向量,表达的是唯一的含义,所以我们忽略了句子 s\boldsymbol{s} 里的词向量顺序。

但是实际上,一个句子中,同一个词,在不同的位置,也可能具有不同的含义。所以,词汇的含义和其所在的序列顺序也是有关的。举个例子:

I went to the bank to sit by the bank of the river.

第一个 bank 指的是银行(金融机构),第二个 bank 指的是河岸(一处地点)。同一个词 bank 却有两种截然不同的含义,而造就含义不同的原因,就是因为它们所在的句子中词序位置不同。这就是 上下文关联,指的是一个词的含义会随着序列的位置变化而变化。位置变了,含义也就变了。

此时,“一个词汇对应着一个唯一的向量,表达的是唯一的含义”这个假设就完全不适用了。此时就变成了“一个句子中,词的含义不仅和本身有关,还和其位置有关”。

我们必须做点什么,把位置信息也加入到词汇向量里面。

我们再细想一下,其实「词序位置」本身也是一种含义,同样也可以像词向量那样,用 dd 维向量来表达,也可以与词向量进行加权组合,形成新的含义。

假如没有位置编码,那么一个词就只能有一种含义,就无法处理一个句子中“一词多义”的情况。

我们为词汇位置也定义一种含义,可以用向量 p\boldsymbol{p} 表示。所以,一个词的含义加上位置的含义,就组成了一个新的含义:

vnew meaning=p+vold meaning\boldsymbol{v}_\text{new meaning} = \boldsymbol{p} + \boldsymbol{v}_\text{old meaning}

同理,在一个句子中,我们为每个位置 ii 的词汇 vi\boldsymbol{v}_i ,其位置上的含义为 pi\boldsymbol{p}_i,加权组合后,我们就可以为这个句子里每一个个词汇都建立起上下文关联。

在原文中,使用的是直接相加的组合。

s=i=1n(vi+pi)\boldsymbol{s} = \sum_{i=1}^{n} (\boldsymbol{v}_i + \boldsymbol{p}_{i})

在原文中, pi\boldsymbol{p}_i 用的是三角函数 PE 编码,定义为

pi=(r1r2r3...rj...rd)\boldsymbol{p}_i=\begin{pmatrix} r_1 \\ r_2 \\ r_3 \\ ... \\ r_j \\ ... \\ r_d \end{pmatrix}

其中每一个维度的分量 rjr_j

rj={sin(r/100002j/d),jis evencos(r/100002j/d),jis odd{r}_j =\begin{cases} \sin (r / 10000^{{2j}/{d}}), \quad j \quad \text{is even} \\ \cos ({r}/{10000^{{2j}/{d}}}), \quad j \quad \text{is odd} \end{cases}

注意力机制

注意力机制是模仿人类在阅读文本的过程中,对每个词汇的理解过程。

事实上,一个人在阅读文本的过程中并不是逐词扫描一遍过的。 而是分为两步。

第一步,逐词扫描,将一段句子种的所有词汇载入到大脑的短期记忆中。这里,对应的是上面所提的,建立上下文关联、将每个词的含义糅合位置含义,得到新含义的过程。

第二步,再进行扫描,在理解每个词汇时,同时关注其他所有剩余词汇,结合剩余的词汇来分析它的含义。

这么说可能有点抽象,但是换一种说法就明确了,它可以被表述为:

一句话中每一个词的含义,可以视为剩余所有词汇含义的加权组合。

以形式化的方法说明,例如在句子 s=(v1,v2,v3,...,vn)\boldsymbol{s} = (\boldsymbol{v}_1,\boldsymbol{v}_2,\boldsymbol{v}_3,...,\boldsymbol{v}_n) 中,对于 vi\boldsymbol{v}_i,可以写作

vi=jinwjvj\boldsymbol{v}_i = \sum_{j \neq i}^{n} w_j \boldsymbol{v}_j

这里的问题是:如何确定权重 wjw_j?也就是说,如何衡量剩余词汇对目标词含义的贡献比例?

我们可以提出一个假设:

如果一个词 vi\boldsymbol{v}_i 与剩余某个词 vj\boldsymbol{v}_j 的含义越相似,那么该词对 vi\boldsymbol{v}_i 含义的贡献就越大。所以,权重可以写作两个词向量的内积相似度。

wj=vivjw_j = \boldsymbol{v}_i · \boldsymbol{v}_j

以一句话举个例子,现在我们分析「Cat sat seat」这一句子。

我们先对「Cat」这一个词进行分析,计算它和剩余词汇的相似度,我们假设,它与「sat」和「seat」的相似度分别为 wsat=0.1,wseat=0.05w_\text{sat}=0.1,w_\text{seat}=0.05。那么,我们就可以认为

vcat=0.1vsat+0.05vseat\boldsymbol{v}_\text{cat} = 0.1 \boldsymbol{v}_\text{sat} + 0.05 \boldsymbol{v}_\text{seat}

含义是:「Cat」这个词的含义,是由「sat」的 10% 和「seat」的 5% 组成的。对于剩下的词「sat」和「seat」也是同理。

上面的过程,就是分别计算 “一个词的含义,分别可以由剩下的词各自以多少比例组成”的过程,就是注意力机制的雏形。

然而,上面的权重 ww 的总和不是 100%100\%,为了保证在数值上有一致性,我们需要把权重值进行 Softmax 正则化一下:

w~j=softmax(wj)\tilde{w}_j = \text{softmax}(w_j)

这样使得 w~sat+w~seat=100%\tilde{w}_\text{sat} + \tilde{w}_\text{seat} = 100\%,计算得到 w~sat=51.25%,w~seat=48.75%\tilde{w}_\text{sat} = 51.25\%, \quad \tilde{w}_\text{seat} = 48.75\%

在注意力机制中,我们把上面的 vcat\boldsymbol{v}_\text{cat} 作为查询向量 q\boldsymbol{q},剩余词汇 vsat,vseat\boldsymbol{v}_\text{sat}, \boldsymbol{v}_\text{seat} 作为键向量 k\boldsymbol{k},而加权组合 w~satvsat+w~seatvseat\tilde{w}_\text{sat} \boldsymbol{v}_\text{sat} + \tilde{w}_\text{seat} \boldsymbol{v}_\text{seat} 则为值向量 v\boldsymbol{v}.

值得注意的是,上面的示例均为简化版示例,不涉及线性变换。

点积注意力公式

上面的示例中已经演示了简单的注意力机制。但是,我们仍然感觉,似乎还是缺了点什么。

我以前上英语的时候,读到过一个笑话:

有一个大学生小明出国读研,在选课的时候发现了这样一门课,名字叫做:《Options,Futures and Other Derivatives》。

于是小明心里边翻译边想:嗯,这门课好啊,讲的是选择,未来,和衍生,一看就是那种人生的哲学,教人们怎么在生活中选择的那种课,就它了,选了!

结果呢,到了上课那天,他傻眼了:老师在前面讲些什么呢?我怎么一句话都听不懂?怎么还有那么多的数学公式?人生选择难不成也要用数学吗?

第二天他才明白,噢,原来这个课的实际名字叫:《期权,期货和其他衍生品》。

这个笑话已经说明了一个问题:在实际的语料库中,一个词可能往往有多个子含义。例如这里,「options」的子含义有两个:「期权」和「选择」。但是有时候,我们需要偏重其中一个子含义,例如在这里,「期权」的含义成分更多,而不是往另一个子含义「选择」上面去理解。

我们在前面的第一节「语义和含义」的一节,已经知道,一个词向量(含义)可以写作多个词向量(子含义)的加权组合。我们假设,一个词的完整含义可以拆分成 kk 个子含义,每个子含义对应一个向量,并通过权重加权得到最终含义。

v=i=1kwivi,s.t.i=1kwi=1\boldsymbol{v} = \sum^{k}_{i=1} w_i\boldsymbol{v}_i, \quad \text{s.t.} \sum_{i=1}^{k} w_i = 1

就比如,「Options」这个词,它的完整含义,本质上可以拆分成是「选择」和「期权」两个「子含义」的加权组合。

就像上面的例子,如果放在金融语境下,它实际上需要以更大的权重往「期权」的子含义上去理解。对于「选择」方向上的子含义权重要小一些,以避免干扰。

之前,我们的一个句子是有序词元素,这个序列每一个元素,是一个词含义与位置含义的加权组合成的“新含义”。

vi=vi+pi\boldsymbol{v}_i' = \boldsymbol{v}_i + \boldsymbol{p}_i

一个“含义”既然可以拆分成多个“子含义”的加权组合,假设拆成 kk 个子含义,那么我们就有

vi=xiW\boldsymbol{v}_i' = x_iW

其中 WRd×k,xiR1×dW \in \mathbb{R}^{d \times k},x_i \in \mathbb{R}^{1 \times d}. 那么,如果我们有一个长度为 nn 的句子序列,把每个词拆成子含义后,就可以把整个序列写成矩阵形式:

M=XWM = XW

其中 WRd×k,XRn×dW \in \mathbb{R}^{d \times k},X \in \mathbb{R}^{n \times d}.

如果我们把这一层也考虑进注意力机制,那么在上面的过程中,q,k,v\boldsymbol{q},\boldsymbol{k},\boldsymbol{v} 的操作对象,实际上是每个词之间的子含义。那么此时,我们有

Q=XWq,K=XWk,V=XWvQ = XW_q, \quad K = XW_k, \quad V = XW_v

我们得到注意力表达式为:

Attention(Q,K,V)=softmax(QKT)V\text{Attention}(Q,K,V) = \text{softmax}({Q·K^T}) · V

然而,为了避免 QKTQ·K^T 在数值上爆炸,我们需要手动调整数值规模,将矩阵元素除 d\sqrt{d}.我们得到最终的注意力公式为:

Attention(Q,K,V)=softmax(QKTd)V\text{Attention}(Q,K,V) = \text{softmax}(\frac{Q·K^T}{\sqrt{d}}) · V

换句话说,注意力机制实际上是在词的子含义层面上计算匹配分数,不同子含义之间的相似度决定了信息传递的权重。

上面的矩阵 X,Wq,Wk,WvX,W_q,W_k,W_v 均为可学习矩阵,在训练阶段进行随机初始化,后期通过反向传播和梯度下降进行矩阵元素更新。这就是单个注意力头。

Encoder 和多头注意力

X,Wq,Wk,WvX,W_q,W_k,W_v 均为可学习矩阵,在训练阶段进行随机初始化,也就意味着,每一次训练,得到的模型参数都不一定是最优的。在不同的语料上训练时,模型参数也有所差异。

所以,我们需要把矩阵 X,Wq,Wk,WvX,W_q,W_k,W_v 复制多份,以不同的种子进行随机初始化,同时去训练多个注意力头。如果有 hh 个头,每个头的注意力计算为

headi=Attention(Qi,Ki,Vi)\text{head}_i = \text{Attention}(Q_i,K_i,V_i)

然后将 hh 个头的输出拼接:

MultiHead=Concat(head1,...,headh)\text{MultiHead} = \text{Concat}(\text{head}_1,...,\text{head}_h)

然而此时 Multi Head 输出维度是非常大的,其形状为 n×(dh)n \times (d · h)。Transformer 中后续还引入了残差链接和前馈神经网络,要求输出维度特征必须与输入 XX 一致。

因此,需要引入一个可学习的线性映射矩阵 WOR(hd)×dW_O \in \mathbb{R}^{(h \cdot d) \times d},将拼接后的多头输出,映射回原来的模型维度 dd。这个矩阵也是通过反向传播进行更新。

因此,多头拼接再经过线性变换,有

MultiHead=Concat(head1,...,headh)WO\text{MultiHead} = \text{Concat}(\text{head}_1,...,\text{head}_h) · W_O

*高阶注意力

在上面的多头输出后,我们得到输出 ORn×dO \in \mathbb{R}^{n \times d},与输入 XX 维度特征一致,那么,我们可以将输出 OO 作为新的输入 X=OX = O,再来一轮多头注意力。以此类推,经过多轮多头注意力,连续经过 ll 轮,得到最终输出:

O1,O2,...,OlO_1,O_2,...,O_l

最终输出 OlO_l 就是模型对序列的高阶表示,它融合了多轮注意力捕捉到的复杂语义和依赖关系。第一层多头注意力:捕捉局部和短距离的依赖。第二层多头注意力:在上一层表示基础上,捕捉更高级的组合模式。直到第 ll 层:得到全局、高阶语义的向量表示。

在工程上,通常每一层通常都有 残差连接 + LayerNorm,即

O~i=LayerNorm(Xi+Oi)\tilde{O}_i = \text{LayerNorm}(X_i + O_i)

保证梯度稳定和信息传递。高阶注意力就是将多头注意力层按序堆叠,让模型能够逐层捕捉更复杂、更抽象的语义和依赖关系。

有最近的研究表明,阶数越低,捕捉到的特征成分主要是语法、句法和词法结构。阶数越高,捕捉到的特征成分主要是语义信息。在文章 BERT Rediscovers the Classical NLP Pipeline 中提到,发现 Transformer 层次表示呈现 由语法到语义的递进规律。

Decoder 和自回归生成

在前面的章节中,我们详细讨论了 Encoder 部分——它的作用是把输入的序列(比如一句英文)编码成一串富含上下文信息的高阶表示向量。那么,问题来了:如果我们想要生成一个新的序列(比如把英文翻译成中文,或者根据上文续写下文),该怎么办?

Encoder 的目标是“理解”,Decoder 的目标是“生成”。在 Transformer 原始架构中(用于机器翻译),Decoder 和 Encoder 长得非常像,但有三个关键的区别:

  1. Decoder 是自回归的:它一个词一个词地往外蹦,一次只预测下一个词,每次生成下一个词时,会把之前生成的所有词都当作输入。
  2. Decoder 包含 Masked Attention(掩码注意力):在生成第 tt 个词时,它不能“偷看”未来的词。
  3. Decoder 包含 Cross-Attention(交叉注意力):它不仅要关注自己已经生成的内容,还要时刻关注 Encoder 输出的源语言信息。

假设我们正在进行中文 - 英文翻译任务,输入是 我 爱 机 器 学 习,期望输出是 I love machine learning

在训练阶段,我们可以并行计算(因为知道标准答案)。但在推理阶段(即模型真正使用时),模型必须自己生成答案。这个过程就是自回归(Autoregressive)。

自回归假设:当前时刻的输出,依赖于之前所有时刻的输出。

用形式化表示就是:

P(y1,y2,...,yT)=t=1TP(yty1,y2,...,yt1,X)P(y_1, y_2, ..., y_T) = \prod_{t=1}^{T} P(y_t \mid y_1, y_2, ..., y_{t-1}, X)

其中 XX 是 Encoder 的输出(源语言信息),yty_t 是当前生成的词。

举个例子

  1. 输入 <START>(起始标记),模型预测第一个词:y1=‘I’y_1 = \text{`I'}
  2. 输入 <START> I,模型预测第二个词:y2=‘love’y_2 = \text{`love'}
  3. 输入 <START> I love,模型预测第三个词:y3=‘machine’y_3 = \text{`machine'}
  4. ……一直持续到模型预测出 <END>(结束标记)。

注意:Decoder 在生成第 tt 个词时,是看不到第 t+1,t+2,...t+1, t+2, ... 个词的。这很合理——就像考试做填空默写题,你只能看到题目和你自己已经写下来的内容,你不能去翻后面的答案。

Decoder 训练与掩码注意力

这就引出了一个问题:在训练 Decoder 的时候,我们是希望并行计算的(否则 GPU 利用率太低)。但如果我们像 Encoder 那样直接把整个目标序列 y1,y2,...,yTy_1, y_2, ..., y_T 一次性输入给 Decoder,那么它在计算 y1y_1 的时候就能看到 y2y_2,这就叫做“信息泄露”(cheating)。

解决方案是 Masked Attention(掩码注意力),也叫 Causal Attention(因果注意力)。

它的原理非常简单:在计算注意力分数矩阵 QKTQ·K^T 时,我们把“未来”位置的分数全部设为 -\infty(或者一个非常大的负数,比如 109-10^9)。

矩阵形状是这样的(假设序列长度为 4),是一个上三角全被遮盖的矩阵:

Mask=(0000000000)\text{Mask} = \begin{pmatrix} 0 & -\infty & -\infty & -\infty \\ 0 & 0 & -\infty & -\infty \\ 0 & 0 & 0 & -\infty \\ 0 & 0 & 0 & 0 \end{pmatrix}

解释

  • 第一行(y1y_1 的位置):只能看到 y1y_1 自己,不能看到 y2,y3,y4y_2, y_3, y_4
  • 第二行(y2y_2 的位置):只能看到 y1,y2y_1, y_2,不能看到 y3,y4y_3, y_4

以此类推。

当 Softmax 处理这些分数时,-\infty 会被映射成 0(因为 e=0e^{-\infty} = 0)。这样一来,Decoder 在移动到 y1y_1 ,计算其含义表示时,y2,y3,y4y_2, y_3, y_4 的含义贡献就是 0。当 Decoder 移动到 y2y_2 时,其含义表示为自身含义与 y1y_1 含义的加权组合,y3,y4y_3, y_4 的含义贡献就是 0。

掩码注意力的公式可以表示为:

Attention(Q,K,V)=softmax(QKT+Md)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q·K^T + M}{\sqrt{d}}\right) · V

其中 MM 就是掩码矩阵(Mij=0M_{ij} = 0 如果 iji \ge j,否则 Mij=M_{ij} = -\infty)。

Decoder 输出与交叉注意力

在推理阶段,Decoder 不能只靠自言自语来生成翻译——它必须参考编码器的原始输入句子。比如翻译 我爱机器学习 时,Decoder 需要知道 Encoder 为 machine learning 生成的表示,对应的是 机器学习 这部分。

这就引入了 Cross-Attention(交叉注意力)层。

在 Cross-Attention 中,Q、K、V 的来源和 Encoder 不同:

  • Q(Query):来自 Decoder 当前的词汇表示。这个词汇表示为该词含义与之前所有词汇含义的加权组合。
  • K(Key):来自 Encoder 的输出(即“源语言里有哪些信息可供我查询”)。
  • V(Value):同样来自 Encoder 的输出(即“这些信息的实际内容是什么”)。

公式和之前的类似:

CrossAttention(Qdec,Kenc,Venc)=softmax(QdecKencTd)Venc\text{CrossAttention}(Q_{\text{dec}}, K_{\text{enc}}, V_{\text{enc}}) = \text{softmax}\left(\frac{Q_{\text{dec}}·K_{\text{enc}}^T}{\sqrt{d}}\right) · V_{\text{enc}}
  1. Decoder 当前想生成“love”这个词,它的 Q 矩阵中包含了“我正在翻译「爱」”这个意图。
  2. 这个 Q 去和 Encoder 的 K 做相似度计算——发现 love 和源语言中的 这个含义高度相似。
  3. 于是,注意力权重集中在 的那个位置上,把那个位置的 V(即 的语义向量)加权提取出来,融合进 Decoder 的当前表示中。

这样一来,Decoder 既知道“我已经生成了哪些词”(通过 Masked Self-Attention),又知道“源语言在说什么”(通过 Cross-Attention),就能很好地完成翻译任务了。

可视化翻译步骤

假设 Encoder 输入 我 爱 AI,训练完 Encoder,得到输出 E=(e1,e2,e3)E = (e_1, e_2, e_3)

Decoder 正在生成第 3 个词(已经生成了 I love,现在要生成 AI):

  1. Masked Self-AttentionIlove 互相“看”对方,计算出它们之间的关联关系。I 的含义会包含 love 的含义,love 的含义也会有 I 的含义。但它们都不能看到未来的第 3 个位置(因为那个位置还没生成)。
  2. Cross-Attention:Decoder 把当前的词汇表示(也就是 love ,它包含了自身以及 I 的含义的加权表示)作为 QQ ,去和 Encoder 的三个位置 e1,e2,e3e_1, e_2, e_3 计算相似度。如果模型学得好,它应该发现“love”和“爱”(e2e_2)最相关,于是注意力权重集中在 e2e_2 上,把 e2e_2 的特征提取出来。
  3. FFN:根据向量 e2e_2 ,发现 e2e_2 这个向量的含义中,包含词汇 AI 的含义最高,于是可以输出 AI

上面就是 Encoder-Decoder 联合进行翻译的全过程。