Attention Is All You Need

资源

为什么我还是无法理解transformer? - 知乎 https://www.zhihu.com/question/596771388

为什么我还是无法理解transformer? - CodeCrafter的回答 - 知乎 https://www.zhihu.com/question/596771388/answer/1974891633284781738

sequence

在《Attention is All You Need》这篇论文里,sequence(序列) 指的是有顺序的元素序列,具体来说就是模型处理的输入或输出的一串 token。

在论文的语境下,它主要体现在几个层面:

所以论文里反复出现的 "sequence transduction"(序列转换)、"sequence length"(序列长度)、"input/output sequence" 等词,都是围绕这个"有序 token 串"的概念展开的。

简单说:sequence = 一串按顺序排列的 token(或它们对应的向量),可以是一句话、一段代码、一串音符等等,只要是有先后顺序的离散元素。

attention 已经存在,但是 transfomer 只用 attention

The dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder. The best performing models also connect the encoder and decoder through an attention mechanism.

这段话的意思拆解一下就是:

第一句:"主流的序列转换模型都是基于复杂的 RNN 或 CNN,并且包含 encoder 和 decoder。" → 讲的是当时的基础架构是 RNN/CNN。

第二句:"性能最好的那些模型,还会通过 attention 机制把 encoder 和 decoder 连接起来。" → 讲的是在这些 RNN/CNN 模型之上,SOTA 模型额外加了 attention。

所以在 Transformer 出现之前,attention 并不是什么新东西,它早就被用在 RNN/CNN 模型里了,只不过是作为一个辅助组件存在。

We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely.

正是在这个背景下,论文标题 "Attention is All You Need" 才显得很有冲击力——"大家之前都是 RNN/CNN 加上 attention,我们说:其实 RNN 和 CNN 都不需要,只用 attention 就够了。"

这也是论文接下来要论证的核心观点:完全抛弃 recurrence 和 convolution,纯靠 attention(具体来说是 self-attention)也能做得更好,而且还能并行、训练更快。


另外,attention 2014 年首次出现在 RNN-based 翻译模型里,就是 cross-attention。decoder 的隐藏状态作为 Q,去关注 encoder 的所有隐藏状态。当时还没有 self-attention 的概念。

2016 年开始,self-attention 的概念开始出现。

2017 年 transformer 论文使用了 self-attention,并且证明只用 attention(不用 RNN)就够了。这篇论文里同时使用了 self-attention 和 cross-attention。

tensor2tensor

Tensor2Tensor (T2T) 是 Google Brain 团队开源的一个深度学习库,建立在 TensorFlow 之上,专门用来做序列到序列 (seq2seq) 模型的训练和研究。 它和《Attention is All You Need》这篇论文关系很密切——论文最后一段就提到:

"The code we used to train and evaluate our models is available at https://github.com/tensorflow/tensor2tensor"

也就是说,Transformer 最早的官方实现就是放在 Tensor2Tensor 里的。

总结来说,Tensor2Tensor 是 Google 为了方便做 seq2seq 研究而写的一个"全家桶"工具库,Transformer 就是它里面最著名的一个模型。 如果你只是想用 Transformer,现在推荐直接用 Hugging Face 的 transformers;但如果想看 Transformer 最原始的实现,可以去翻 T2T 的源码。

continuous representation

Here, the encoder maps an input sequence of symbol representations (x 1 , ..., x n ) to a sequence of continuous representations z = (z 1 , ..., z n ). Given z, the decoder then generates an output sequence (y 1, ..., y m) of symbols one element at a time.

为什么要变成 continuous?

这是深度学习的核心思想之一,有几个重要原因:

MLP 叫法问题

The Transformer follows this overall architecture using stacked self-attention and point-wise, fully connected layers for both the encoder and decoder

The first is a multi-head self-attention mechanism, and the second is a simple, position-wise fully connected feed-forward network.

Additive attention computes the compatibility function using a feed-forward network with a single hidden layer.

In addition to attention sub-layers, each of the layers in our encoder and decoder contains a fully connected feed-forward network

这几个词经常互换,指的都是那个最经典的"输入 → 若干全连接层 → 输出"的网络。

严格来说,Feedforward 是个大类:只要数据流是单向的、无环的,都算 feedforward。所以 CNN 严格来说也是一种 feedforward network。

MLP 是 feedforward 的一个具体子类:专指"全连接层堆叠"的那种结构。

Layer Normalization

什么是 batch 和 sample

在训练 AI 的语境下:

System 视角 (Tensor 形状): 假设 Embedding 维度是 4(为了方便手算),那么这个 Batch 的 Tensor 形状是 [32, 4, 4]。

实际例子:对 "gpu" 这个词做 LayerNorm

现在我们只关注 Sample 1 ("I love gpu programming")。Transformer 里的 LayerNorm 是“独立”运作的。它不看别的句子,甚至通常不看别的词,它只盯着当前这个词的向量看。

我们把镜头对准第 3 个词 —— "gpu"。

A. 原始数据 (Input)

假设经过 Embedding 和 Attention 计算后,"gpu" 这个词在计算机里是这样的一串数字(向量):

x = [ 2.0 , 6.0 , 2.0 , 6.0 ]

注意:这 4 个数字代表 "gpu" 的特征(比如第一个数代表名词性,第二代表科技属性...)。这就是我们的操作对象。

B. 为什么要 Norm?

你看这个向量:[2, 6, 2, 6]。它的数值可能偏大(比如到了 6)。

在深度网络里,如果不约束,下一层可能就变成 [20, 60, 20, 60],再下一层就爆炸了(梯度爆炸)。我们需要把它拉回一个“标准范围”。

C. LayerNorm 的计算过程 (手算)

第一步:算平均值 ( μ )

算出 "gpu" 这 4 个特征的平均值。

第二步:算方差 ( σ 2 ) 和标准差 ( σ )

算出这 4 个数偏离平均值(4.0)有多远。

第三步:归一化 (Normalize)

把原始数字减去平均值,再除以标准差。

输出结果:$$LayerNorm(x) = [-1.0, \quad 1.0, \quad -1.0, \quad 1.0]$$

第四步,仿射变换

归一化完不是直接输出,而是还要再乘一个 γ,加一个 β。这两个参数可以学习。

如果只做归一化,强行把所有向量都压到"均值 0、方差 1"的分布,会限制模型的表达能力——有时候模型可能确实需要某个特征保持大数值。

γ 和 β 给了模型一个"反悔"的机会:如果归一化过头了,模型可以学到 γ=σ、β=μ 来部分恢复原始分布。换句话说,模型自己决定归一化的强度。

假设 γ=[1.2,0.8,1.0,1.5],β=[0.1,−0.2,0.0,0.3],那实际输出就是 [−1.1,0.6,−1.0,1.8]。

结果的物理意义

之前的 [2, 6, 2, 6] 变成了 [−1.1,0.6,−1.0,1.8]。

这就保证了无论你的输入原始数值是几百还是几千,经过 LayerNorm 之后,丢给下一层的永远是这种**“清爽、标准”**的小数值。

这就是为什么 Transformer 可以堆很深而不崩溃。

Transformer 结构

In addition to the two sub-layers in each encoder layer, the decoder inserts a third sub-layer, which performs multi-head attention over the output of the encoder stack.

In "encoder-decoder attention" layers, the queries come from the previous decoder layer, and the memory keys and values come from the output of the encoder.

左边 encoder 部分,N 个 layer 相连。最后一个 layer 的输出,喂到右边 decoder 部分的每一个 layer 中。

Embedding 和 Linear

FFN

FFN ( x ) = max ( 0 , x W 1 + b 1 ) W 2 + b 2

每个 token 向量 x 独立过 FFN。

x0        h0        o0
x1        h1        o1
x2        h2        o2
...       ...       ...
x511      ...       o511
          ...
          h2047

多层结构

Anthropic 的可解释性研究发现一个非常重要的现象,Transformer 的不同层处理不同抽象层次的信息:

也就是说,同一个 token 的向量,在不同层经过不同的 attention 加工,强调不同维度的信息。

具体说:

层数越深,处理的信息越抽象。这就是为什么 Transformer 需要堆叠这么多层——每一层负责一种抽象层次的信息整合。


encoder 多层的作用是:逐层把输入序列"理解"得越来越深。

Encoder Layer 1:  "I love GPU programming" → 每个 token 知道自己的邻居
Encoder Layer 2:  → 知道自己所在的短语
...
Encoder Layer 6:  → 每个 token 都是"理解了整句话"的饱和向量

最终输出:一组深度理解后的源序列表示,喂给 decoder 用。

decoder 多层的作用是:逐层把"已生成的目标序列 + encoder 提供的源序列信息"加工成"下一个 token 的预测"。

Decoder Layer 1: "我 爱" → 简单上下文化
Decoder Layer 2: → 整合 encoder 信息("原文是关于什么的")
Decoder Layer 3: → 整合长程依赖
...
Decoder Layer 6: → 最终决定下一个 token 是什么

Decoder 的任务比 encoder 更难——它要同时处理两个序列(自己已生成的 + encoder 输出的),还要生成新 token。所以 decoder 的层数和 encoder 通常一样,甚至更多。

Positional Encoding

Permutation Equivariance 置换等变性

继续用 "I love GPU programming" 例子,$d_{\text{model}} = 6$。

原始顺序:

X = [ x I   x l o v e   x G P U   x p r o g ] = [ 1.0 0.2 0.3 0.5 0.1 0.4   0.4 0.9 0.1 0.2 0.3 0.5   0.3 0.1 0.8 0.6 0.2 0.1   0.2 0.5 0.7 0.9 0.4 0.3 ] 4 × 6

打乱后的顺序:把 love 和 GPU 互换 → "I GPU love programming"

X ′ = [ x I   x G P U   x l o v e   x p r o g ] = [ 1.0 0.2 0.3 0.5 0.1 0.4   0.3 0.1 0.8 0.6 0.2 0.1   0.4 0.9 0.1 0.2 0.3 0.5   0.2 0.5 0.7 0.9 0.4 0.3 ] 4 × 6

注意——这两个矩阵的"内容相同,只是行的顺序变了"(第 2 行和第 3 行互换)。

Step 1 算 Q K V

权重矩阵 W Q , W K , W V 不变(它们是模型参数,和输入顺序无关)。

原始顺序的 Q:

Q = X W Q = [ q I   q l o v e   q G P U   q p r o g ]

打乱顺序的 Q':

Q ′ = X ′ W Q = [ q I   q G P U   q l o v e   q p r o g ]

关键观察:因为 W Q 是按行变换的(每个 token 独立投影), Q ′ 就是 Q 把第 2 行和第 3 行互换。$q_{love}$ 这个向量本身没变(还是同样的 6 个数字),只是它在矩阵里的位置变了。

K 和 V 同理。

Step 2.1 算 Q K T

原始的 scores(假设数值):

Q K ⊤ = [ q I ⋅ k I q I ⋅ k l o v e q I ⋅ k G P U q I ⋅ k p r o g   q l o v e ⋅ k I q l o v e ⋅ k l o v e q l o v e ⋅ k G P U q l o v e ⋅ k p r o g   q G P U ⋅ k I q G P U ⋅ k l o v e q G P U ⋅ k G P U q G P U ⋅ k p r o g   q p r o g ⋅ k I q p r o g ⋅ k l o v e q p r o g ⋅ k G P U q p r o g ⋅ k p r o g ]

我用具体数字代替——

scores = [ 0.85 0.68 0.72 0.88   0.78 0.72 0.75 0.90   0.82 0.70 0.76 0.91   0.93 0.75 0.82 1.02 ]

打乱后的 scores':

按定义算 Q ′ K ′ ⊤ ——但因为 Q ′ 是 Q 的行互换,$K'$ 也是 K 的行互换(同样是 love、GPU 互换),所以:

scores ′ = [ q I ⋅ k I q I ⋅ k G P U q I ⋅ k l o v e q I ⋅ k p r o g   q G P U ⋅ k I q G P U ⋅ k G P U q G P U ⋅ k l o v e q G P U ⋅ k p r o g   q l o v e ⋅ k I q l o v e ⋅ k G P U q l o v e ⋅ k l o v e q l o v e ⋅ k p r o g   q p r o g ⋅ k I q p r o g ⋅ k G P U q p r o g ⋅ k l o v e q p r o g ⋅ k p r o g ]

代入数字(注意——每个点积值本身没变,只是它们在矩阵里的位置变了):

scores ′ = [ 0.85 0.72 0.68 0.88   0.82 0.76 0.70 0.91   0.78 0.75 0.72 0.90   0.93 0.82 0.75 1.02 ]

关键观察:scores' 是 scores 的"行列同时互换"

把两个矩阵放一起对比:

原始 scores:                       打乱 scores':
        I    love  GPU   prog              I    GPU   love  prog
   I  [0.85  0.68  0.72  0.88]        I  [0.85  0.72  0.68  0.88]
 love [0.78  0.72  0.75  0.90]      GPU  [0.82  0.76  0.70  0.91]
  GPU [0.82  0.70  0.76  0.91]     love  [0.78  0.75  0.72  0.90]
 prog [0.93  0.75  0.82  1.02]     prog  [0.93  0.82  0.75  1.02]

仔细看——scores' 就是 scores 把 row 2/row 3 互换、然后 col 2/col 3 互换。数值完全没变,只是位置被搬了。

Step 2.2 Softmax

softmax 是逐行操作,不跨行。

原始 A 的第 2 行(love asking)softmax 后假设是 [ 0.25 , 0.15 , 0.10 , 0.50 ] 。打乱后 scores' 的第 3 行(love asking)的数值组成完全相同(只是顺序对应到新位置),softmax 后是 [ 0.25 , 0.10 , 0.15 , 0.50 ] 。

注意——love 关注 love 的权重还是 0.15,关注 GPU 的还是 0.10,关注 prog 的还是 0.50——只是这些权重在 A' 矩阵里的位置变了。

完整的 A 和 A':

A (原始):                          A' (打乱):
        I    love  GPU   prog              I    GPU   love  prog
   I  [0.60  0.15  0.10  0.15]        I  [0.60  0.10  0.15  0.15]
 love [0.25  0.15  0.10  0.50]      GPU  [0.10  0.30  0.10  0.50]
  GPU [0.10  0.10  0.30  0.50]     love  [0.25  0.10  0.15  0.50]
 prog [0.10  0.20  0.55  0.15]     prog  [0.10  0.55  0.20  0.15]

Step 4 A V

原始 output:

output l o v e = 0.25 ⋅ v I + 0.15 ⋅ v l o v e + 0.10 ⋅ v G P U + 0.50 ⋅ v p r o g

打乱后的 output'(love 现在在第 3 行):

output l o v e ′ = 0.25 ⋅ v I + 0.10 ⋅ v G P U + 0.15 ⋅ v l o v e + 0.50 ⋅ v p r o g

注意——这两个加法的结果完全相同!因为加法是可交换的:

0.25 ⋅ v I + 0.15 ⋅ v l o v e + 0.10 ⋅ v G P U + 0.50 ⋅ v p r o g = 0.25 ⋅ v I + 0.10 ⋅ v G P U + 0.15 ⋅ v l o v e + 0.50 ⋅ v p r o g

所以 love 这个 token 的 output 向量内容完全一样——不管它在序列里是第 2 个还是第 3 个。

Step 5 结果对比

原始 output:                        打乱 output':
[ output_I   ]                      [ output_I   ]
[ output_love] ←───────────────→    [ output_GPU ]   ← GPU 在第 2 行
[ output_GPU ] ←───────────────→    [ output_love]   ← love 在第 3 行
[ output_prog]                      [ output_prog]

每个 token 的输出向量内容完全一样——love 的新表示就是 love 的新表示,不管它排在哪。只是它在 output 矩阵里的"行号"变了。

这就是 permutation equivariance。置换输入序列 → 输出序列也跟着置换,但每个元素的内容完全不变。数学上写成:$\text{Attention}(P \cdot X) = P \cdot \text{Attention}(X)$。其中 P 是置换矩阵(行交换矩阵)。

为什么置换等变性是问题?

让我们对比两个真实的句子——"cat eats fish" 和 "fish eats cat"。

这两个句子里

但 attention 看到的:

换句话说,attention 把序列当作没顺序的 set 而不是有顺序的 sequence,所以 attention 分不出"cat eats fish" 和 "fish eats cat"。

但是!语言是有顺序的,顺序对语义至关重要。如果 attention 看不到顺序,模型根本学不会语言。

引入 PE

既然 attention 看不到顺序,那就把位置信息直接编码到 token 的向量里:

x c a t ′ = x c a t + P E ( 0 ) x e a t s ′ = x e a t s + P E ( 1 ) x f i s h ′ = x f i s h + P E ( 2 )

现在

两个句子里 cat 的输入向量已经不一样了,输入向量里既包含“我是什么词“,也包含”我在哪个位置“。Attention 算 QKT 时,位置信息会通过点积自动参与匹配——模型自然就能学到"位置相近的 token 应该如何互动"。

如何设计 PE?

我们需要一个有界的、能区分不同位置的、能扩展到任意长度的编码方法。

论文用了正弦余弦位置编码:

P E ( p o s , 2 i ) = sin ⁡ ( p o s / 10000 2 i / d model ) P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s / 10000 2 i / d model )

听起来很玄,意思其实简单——用一组不同频率的正弦/余弦波,给每个位置生成一个独特的向量。

具体例子

d model = 6 ,序列长度 n = 4 。

公式(论文原版):

P E ( p o s , 2 i ) = sin ⁡ ( p o s / 10000 2 i / d ) P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s / 10000 2 i / d )

其中 d = 6 ,$i = 0, 1, 2$(对应维度对 0/1, 2/3, 4/5)。

算每个频率:

每对维度对应一个频率。分母 10000 2 i / d :

所以波长越来越长(频率越来越低)。

算每个位置的 PE 向量:

token 0 的 PE 向量:

d  pos  i  equation     result
0  0    0  PE(0,2*0)    sin(0/1)      0.000
1  0    0  PE(0,2*0+1)  cos(0/1)      1.000
2  0    1  PE(0,2*1)    sin(0/21.54)  0.000
3  0    1  PE(0,2*1+1)  cos(0/21.54)  1.000
4  0    2  PE(0,2*2)    sin(0/464.16) 0.000
5  0    2  PE(0,2*2+1)  cos(0/464.16) 1.000

P E ( 0 ) = [ 0 , 1 , 0 , 1 , 0 , 1 ]

token 1 的 PE 向量:

d  pos  i  equation     result
0  1    0  PE(1,2*0)    sin(1/1)       0.841
1  1    0  PE(1,2*0+1)  cos(1/1)       0.540
2  1    1  PE(1,2*1)    sin(1/21.54)   0.046
3  1    1  PE(1,2*1+1)  cos(1/21.54)   0.999
4  1    2  PE(1,2*2)    sin(1/464.16)  0.002
5  1    2  PE(1,2*2+1)  cos(1/464.16)  1.000

P E ( 1 ) = [ 0.841 , 0.540 , 0.046 , 0.999 , 0.002 , 1.000 ]

token 2 的 PE 向量:

P E ( 2 ) = [ sin ⁡ ( 2 ) , cos ⁡ ( 2 ) , sin ⁡ ( 2 / 21.54 ) , cos ⁡ ( 2 / 21.54 ) , sin ⁡ ( 2 / 464.16 ) , cos ⁡ ( 2 / 464.16 ) ] = [ 0.909 , − 0.416 , 0.093 , 0.996 , 0.004 , 1.000 ]

token 3 的 PE 向量:

P E ( 3 ) = [ sin ⁡ ( 3 ) , cos ⁡ ( 3 ) , sin ⁡ ( 3 / 21.54 ) , cos ⁡ ( 3 / 21.54 ) , sin ⁡ ( 3 / 464.16 ) , cos ⁡ ( 3 / 464.16 ) ] = [ 0.141 , − 0.990 , 0.139 , 0.990 , 0.006 , 1.000 ]

矩阵形式:

PE =
sin(0/1) cos(0/1) sin(0/21.54) cos(0/21.54) sin(0/464.16) cos(0/464.16)
sin(1/1) cos(1/1) sin(1/21.54) cos(1/21.54) sin(1/464.16) cos(1/464.16)
sin(2/1) cos(2/1) sin(2/21.54) cos(2/21.54) sin(2/464.16) cos(2/464.16)
sin(3/1) cos(3/1) sin(3/21.54) cos(3/21.54) sin(3/464.16) cos(3/464.16)

0.000    1.000    0.000    1.000    0.000    1.000  <- PE(0)
0.841    0.540    0.046    0.999    0.002    1.000  <- PE(1)
0.909    -0.416   0.093    0.996    0.004    1.000  <- PE(2)
0.141    -0.990   0.139    0.990    0.006    1.000  <- PE(3)

几个观察:

如何理解

每个位置 p o s 对应一个 d model 维的向量。这个向量的不同维度对应不同频率的波:

这就像一个多位的二进制时钟:

000  ← 位置 0
001  ← 位置 1
010  ← 位置 2
011  ← 位置 3
100  ← 位置 4
...

二进制每一位的"频率"不同——最低位每 1 步翻转一次,次低位每 2 步翻转一次,再上一位每 4 步翻转一次……

正弦/余弦 PE 就是这个思路的连续版本,用不同频率的正弦波代替不同频率的 0/1 翻转。每个位置都对应一个独特的"波形组合",不会重复。

为什么用三角函数而不是其他函数?

论文里给出了理由:正弦/余弦有一个数学性质:$PE(pos + k)$ 可以表示为 P E ( p o s ) 的线性变换。

具体说,对任意固定的 offset k:

P E ( p o s + k ) = M k ⋅ P E ( p o s )

其中 M k 是一个固定矩阵(只依赖 k,不依赖 pos)。

这意味着,模型可以很容易地学到"相对位置"的概念。比如学到"距离 5 的 token 之间该如何互动"——它只需要学这个固定的 M 5 ​,就能应用到所有位置上。

整体训练流程

训练数据格式

训练数据是:一句源语言和一句目标语言,配对的句对。每对都是人工翻译过的、对齐的。这种数据叫做平行语料(parallel corpus)。

对英译中,有以下例子:

源:  "I love GPU programming"
目标:"我 爱 GPU 编程"

源:  "Hello world"
目标:"你好 世界"

源:  "The cat sat on the mat"
目标:"猫 坐在 垫子上"

WMT 机器翻译竞赛会提供英德、英法等几千万对句子。

输入处理

每对句子要经过:

"I love GPU programming"
   ↓ tokenize
[15, 238, 4021, 7891]   ← token IDs
   ↓ 加特殊 token ID
[1, 15, 238, 4021, 7891, 2]    ← 假设 BOS 的 ID 是 1,EOS 是 2
   ↓ padding
[1, 15, 238, 4021, 7891, 2, 0, 0, 0]   ← 假设 PAD 的 ID 是 0

"我 爱 GPU 编程"
   ↓ tokenize
[523, 891, 4021, 12456]
   ↓ 加特殊 token
[1, 523, 891, 4021, 12456, 2]
   ↓ padding(如果需要)
[1, 523, 891, 4021, 12456, 2, 0, 0]

模型第一阶段:Encoder(图的左半部分)—— 并行读书

假设当前训练 step 的样本是:I love GPU programming, 我 爱 GPU 编程。

目标:把人类的单词 x ,变成机器理解的向量 z 。

训练和推理的第一阶段基本差不多。

  1. Inputs (最底下):

    • 输入序列:[1, 15, 238, 4021, 7891, 2, 0, 0, 0]。
    • 动作:查表(Embedding)得到 9 个向量,并加上位置编码(Positional Encoding)。
    • System 视角:此时 Tensor 形状是 [Batch, 9, 512]。
  2. Encoder Block ( N × ) (灰色框):

    • 数据流经 Multi-Head Attention(Self-Attention)。
    • 发生的事情(注意:这里没有 Mask,或者只有 Padding Mask):
      • "I", "love", "GPU" 三者之间两两可见。
      • 模型一次性理解整句语义。
      • 特殊 token
        • BOS 的 KV 正常参与 attention(其他 token 可以关注它)
        • BOS 的 Q 正常参与 attention(它可以关注其他 token)
        • 经过 N 层 encoder 后,BOS 位置的输出向量融合了整个源句的语义。它常常被当作"句子全局表示"使用(在 BERT 里就是这个机制——[CLS] token 的角色和 BOS 类似,作为句子整体的代表)。
        • EOS 的 KV 正常参与 attention(被关注)
        • EOS 的 Q 正常参与(关注别人)
        • 经过 encoder 后,EOS 位置的输出向量也是个"全局信息浓缩"。因为序列里所有信息都通过 attention 流到了它身上。
        • PAD 对应的 k 向量和任意 q 向量的点积为负无穷,softmax 后变 0,所以任何一个 token 对 PAD 的注意力是 0。
        • PAD 对应的 q 向量 GPU 会照常计算,不会设成负无穷,因为:
          • PAD 位置的 output 反正不会被使用:encoder 输出送给 decoder 时,PAD 位置的输出对应 cross-attention 的 K/V 也会被屏蔽
          • 即使算了,也没有"训练它",encoder 不直接产生 loss,PAD 行的输出不会反向传播到任何标签
    • Add & Norm: 残差连接 + 归一化。
    • Feed Forward: 每个词独立思考一下,升维再降维。
  3. Encoder Output ( z ) (中间那根连接线):

    经过 N 层处理后,生成包含了完整上下文信息的矩阵 z 。

    E = [
      e_BOS    ← 含整句信息
      e_I      ← 主要是 I + 上下文
      e_love   ← 主要是 love + 上下文
      e_GPU    ← 主要是 GPU + 上下文
      e_prog   ← 主要是 programming + 上下文
      e_EOS    ← 含整句信息
      e_PAD    ← 无意义
      e_PAD    ← 无意义
      e_PAD    ← 无意义
    ]
    

    关键点:这个 z 就是原文里说的 "sequence of continuous representations"。

    它会作为 Keys ( K ) 和 Values ( V ),发送给右边的 Decoder 使用。

第二阶段:Decoder(图的右半部分)—— 并行计算 (Teacher Forcing)

目标:拿着 z 和“正确答案的前缀”,一次性计算出所有位置的预测结果。

注意,BOS 我 爱 GPU 编程 EOS PAD PAD 对应的 token id 数组是 [1, 523, 891, 4021, 12456, 2, 0, 0]。

但是为了训练,decoder 的输入和标签为:

decoder 输入:[BOS 我 爱  GPU  编程 EOS PAD] 对应的 token id 数组,去掉了最后一个元素
decoder 标签:[我  爱 GPU 编程  EOS PAD PAD] 对应的 token id 数组,去掉了第一个元素

为什么这么做?因为我们想让 decoder:

1. Decoder Inputs (右下角输入)

输入序列:[1, 523, 891, 4021, 12456, 2, 0]。

System 视角:这是一个 [Batch, 7, 512] 的矩阵。所有正确答案都在这里,"未来"就在显存的下一个地址里。

2. Masked Multi-Head Attention (右边第一个粉色块)

这里应用了上三角 Mask 矩阵(Causal Mask),防止模型训练作弊。

计算注意力时同时使用 padding mask,让 padding 列经过 softmax 结果为 0。

这一层的输出:

M = [
  m_BOS    ← 只融合了 BOS 自己
  m_我     ← 融合了 BOS, 我
  m_爱     ← 融合了 BOS, 我, 爱
  m_GPU    ← 融合了 BOS, 我, 爱, GPU
  m_编程    ← 融合了 BOS, 我, 爱, GPU, 编程
  m_EOS    ← 融合了 BOS, 我, 爱, GPU, 编程, EOS
  m_PAD    ← 无意义
]

3. Multi-Head Attention (右边中间的粉色块)

这就是 Cross-Attention。

Query ( Q ): 来自 Decoder 的 7 个向量(分别代表 BOS, 我, 爱, GPU, 编程, EOS, PAD 的当前状态)。



Key ( K ) & Value ( V ): 来自左边 Encoder 传过来的 z 。



QKT 矩阵反映的是,每个目标 token 对每个源 token 的关注度:

          k_BOS  k_I   k_love  k_GPU  k_prog  k_EOS  k_PAD  k_PAD  k_PAD ← 源 token 的 K
q_BOS     [ ?      ?       ?       ?   ?       ?      -∞     -∞     -∞]
q_我      [ ?      ?       ?       ?    ?       ?     -∞     -∞     -∞]
q_爱      [ ?      ?       ?       ?    ?      ?      -∞     -∞     -∞]
q_GPU     [ ?      ?       ?       ?    ?      ?      -∞     -∞     -∞]
q_编程     [ ?      ?       ?       ?    ?      ?      -∞     -∞     -∞]
q_EOS     [ ?      ?       ?       ?    ?      ?      -∞     -∞     -∞]
q_PAD     [ ?      ?       ?       ?    ?      ?      -∞     -∞     -∞]
   ↑
目标 token 的 Q −

假设经过缩放和 softmax 的 attention 矩阵长这样:

        k_BOS  k_I   k_love  k_GPU  k_prog k_EOS  k_PAD  k_PAD  k_PAD
        ──────────────────────────────────────────  ─────────────
                 源 token 的 K(有意义)              被 mask 屏蔽

q_BOS  [  0.10   0.20   0.20   0.20   0.20   0.10   0.00   0.00   0.00 ]
q_我    [  0.05   0.65   0.10   0.05   0.05   0.10   0.00   0.00   0.00 ]
q_爱    [  0.05   0.10   0.70   0.05   0.05   0.05   0.00   0.00   0.00 ]
q_GPU  [  0.03   0.07   0.10   0.65   0.10   0.05   0.00   0.00   0.00 ]
q_编程  [  0.03   0.05   0.07   0.15   0.60   0.10   0.00   0.00   0.00 ]
q_EOS  [  0.05   0.05   0.05   0.05   0.10   0.70   0.00   0.00   0.00 ]
q_PAD  [  0.11   0.11   0.11   0.11   0.11   0.11   0.00   0.00   0.00 ] ← 计算了但无意义

每一行的语义解读:

q_BOS 这一行:[0.10, 0.20, 0.20, 0.20, 0.20, 0.10, 0, 0, 0]

q_我 这一行:[0.05, 0.65, 0.10, 0.05, 0.05, 0.10, 0, 0, 0]

q_爱 这一行:[0.05, 0.10, 0.70, 0.05, 0.05, 0.05, 0, 0, 0]

q_GPU 这一行:[0.03, 0.07, 0.10, 0.65, 0.10, 0.05, 0, 0, 0]

q_编程 这一行:[0.03, 0.05, 0.07, 0.15, 0.60, 0.10, 0, 0, 0]

q_EOS 这一行:[0.05, 0.05, 0.05, 0.05, 0.10, 0.70, 0, 0, 0]

q_PAD 这一行:[0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0, 0, 0]

注意,每个目标 token 的 output 现在融合了源语言信息:

这就是翻译的核心机制——目标 token 通过 cross-attention 找到自己对应的源 token,吸收源 token 的语义,用来辅助预测下一个目标 token。

4. FFN

上面已经说了,cross-attention 给的是"吸收了源 token 信息的混合向量",但这只是信息的简单加权。FFN 提供了非线性加工,它可以基于已经吸收的信息做更复杂的转换。

具体来说,FFN 在这个位置可能在做这样的事:

但 FFN 本身并不输出"下一个 token",它输出的还是一个 dmodel​ 维向量。

多层 decoder

Decoder 通常有多层(比如 6 层),每一层都是 "masked self-attention → cross-attention → FFN"。

第 1 层的输出送到第 2 层,第 2 层的输出送到第 3 层,直到第 6 层输出。每一层都让 token 表示更精炼、更接近"应该预测的下一个 token"的形态。

第三阶段:计算 Loss 与反向传播

目标:对比预测结果和真实标签,更新参数。

  1. Loss Calculation:

    每个位置的预测是一个词表大小的概率分布(过 softmax 后的)。

    用 cross-entropy loss 衡量"预测分布"和"真实标签"的差距:

    位置 1: 模型预测 [P(我)=0.7, P(你)=0.1, P(他)=0.05, ...]
            真实标签 是 "我"
            loss = -log(0.7) ≈ 0.36
    
    位置 2: 模型预测 [P(爱)=0.9, P(恨)=0.02, ...]
            真实标签 是 "爱"
            loss = -log(0.9) ≈ 0.10
    
    ...每个位置都算一个 loss
    

    总 loss = 所有位置 loss 的平均。

    注意,如果一个位置的 label 是 PAD,那么该位置的 loss 不算,避免模型被训练成“要预测 PAD"。

    如果 label 是 EOS,正常算 loss。EOS 是教会模型停止的核心信号。

  2. Backpropagation:

    梯度同时传回 Decoder 和 Encoder 的所有层,更新权重。

整体流程

准备数据
    ↓
初始化模型参数(随机)
    ↓
循环(几十万到几百万步):
    1. 拿一个 batch 的句对
    2. 喂给 Encoder + Decoder(teacher forcing)
    3. 算 loss
    4. 反向传播
    5. 更新参数
    ↓
loss 收敛 / 训完预定步数
    ↓
模型训练完成

整体推理流程

任务:将英文 "I love GPU" 翻译成法文 "J'aime GPU"。

论文原文里提到的数学符号,我们这样对应:

第一阶段:Encoder(图的左半部分)—— 并行读书

目标:把人类的单词 x ,变成机器理解的向量 z 。

  1. Inputs (最底下):

    • 输入序列:["I", "love", "GPU"]。
    • 动作:查表(Embedding)得到 3 个向量,并加上位置编码(Positional Encoding)。
    • System 视角:输入 Tensor 形状 [1, 3, 512]。(Batch=1, Src_Len=3, Dim=512)
  2. Encoder Block ( N × ) (灰色框):

    • 数据流经 Multi-Head Attention(Self-Attention)。
    • 发生的事情:
      • "I" 作为一个 Query,去问 "love" 和 "GPU"(Keys)。
      • "I" 发现自己在句子开头(Positional)。
      • "love" 告诉 "I",它是一个动词。
      • "GPU" 告诉 "I",它是宾语。
    • Add & Norm: 残差连接 + 归一化。
    • Feed Forward: 每个词独立思考一下,升维再降维。
    • System 视角:无论经过多少层,Tensor 形状始终保持 [1, 3, 512] 不变。
  3. Encoder Output ( z ) (中间那根连接线):

    经过 N 层处理后,出来的不再是简单的单词向量,而是一个包含了完整上下文信息的矩阵 z 。

    关键点:这个 z 就是原文里说的 "sequence of continuous representations"。

    它会作为 Keys ( K ) 和 Values ( V ),发送给右边的 Decoder 使用。

    System 视角:输出 Tensor z 的形状是 [1, 3, 512]。这是后续 Cross-Attention 的 K 和 V 的来源。

第二阶段:Decoder(图的右半部分)—— 串行写书

目标:拿着 z ,一个字一个字地吐出 y 。这就是原文说的 "Auto-regressive"(自回归)。

我们假设现在模型刚启动,要生成第一个词。

Step 1: 生成 "J'aime"

  1. Decoder Input (当前时刻输入):

    推理模式下没有“右移”的概念,只有“当前已生成的序列”。

    初始状态:输入仅包含 ["<Start>"]。

    System 视角:输入 Tensor 形状 [1, 1, 512]。 (Batch=1, Cur_Len=1, Dim=512)

  2. Masked Multi-Head Attention (右边第一个粉色块):

    <Start> 只能看自己。

    在推理时,后面根本没有词,物理上不存在“未来”。Mask 在这里只是为了维持代码逻辑的一致性,遮住的是无效的 Padding 或空内存。

    System 视角:输出 Tensor 形状依然是 [1, 1, 512]。

  3. Multi-Head Attention (右边中间的粉色块) —— 核心!

    这就是 Cross-Attention。

    Query ( Q ): 来自 Decoder 的 <Start> 向量。意思是:“我刚开始,我要找主语”。

    System 视角 (Q): 形状 [1, 1, 512] (来自 Decoder)

    Key ( K ) & Value ( V ): 来自左边 Encoder 传过来的 z ("I love GPU" 的语义)。

    System 视角 (K, V): 形状 [1, 3, 512] (来自 Encoder Output z)

    注意:这里发生了长度不匹配的交互 (1 vs 3)。Attention 会计算 [1, 1] 和 [1, 3] 的关系,生成 1x3 的分数矩阵。

    交互:Decoder 拿着 Q 去 K 里找。发现 "I" (英文主语) 的匹配度最高。于是把 "I" 对应的 V 拿过来。

    System 视角 (Output): 加权求和后,输出形状回归到 [1, 1, 512]。

  4. Linear + Softmax (最顶上):

    计算概率,发现 "J'aime" 概率最高。

    System 视角 (Linear): 把 512 维投影到词表大小。形状变为 [1, 1, 30000]。

    System 视角 (Softmax): 在 30000 个概率中找最大的 index。

    输出:y1 = "J'aime"。

Step 2: 生成 "GPU" (Auto-regressive 发生的地方)

原文说 "consuming the previously generated symbols as additional input",指的就是这里。

  1. Decoder Input (更新后的序列):

    现在的输入变成了:["<Start>", "J'aime"]。

    把上一步生成的 y 1 喂回去了!

    System 视角:输入 Tensor 形状变长了!现在是 [1, 2, 512]。 (Batch=1, Cur_Len=2, Dim=512)

  2. Masked Multi-Head Attention:

    "J'aime" 可以看 <Start> 和它自己。

    System 视角:Attention 机制回顾历史。虽然逻辑上是看 <Start> 和 J'aime,但在实际代码中,Mask 矩阵依然存在,确保我们不会访问到显存里那些未初始化的地址。

    System 视角:输出 Tensor 形状 [1, 2, 512]。

  3. Cross-Attention:

    Query ( Q ): 来自 "J'aime"。意思是:“我有主语和谓语了,我要找宾语”。

    System 视角 (Q): 形状 [1, 2, 512] (来自 Decoder,注意这里长度是 2)

    Key/Value ( z ): 还是那个 z 。

    System 视角 (K, V): 形状 [1, 3, 512] (Encoder 永远不变)

    交互:计算 2 个 Decoder 词和 3 个 Encoder 词的关系。

    交互:Decoder 发现 "GPU" (英文宾语) 和当前的 Q 最匹配。把 "GPU" 的信息拿过来。

    System 视角 (Output): 输出形状 [1, 2, 512]。

  4. Output:

    计算概率,输出 y2 = "GPU"。

    System 视角 (Linear): 形状 [1, 2, 30000]。我们通常只取最后一个时间步 (Sequence Length 的最后一位) 来做预测。

Step 3: 结束

输入变成 ["<Start>", "J'aime", "GPU"]。

重复上述过程,Decoder 发现句子完整了,输出 <End> 标记。翻译结束。