为什么我还是无法理解transformer? - 知乎 https://www.zhihu.com/question/596771388
为什么我还是无法理解transformer? - CodeCrafter的回答 - 知乎 https://www.zhihu.com/question/596771388/answer/1974891633284781738
在《Attention is All You Need》这篇论文里,sequence(序列) 指的是有顺序的元素序列,具体来说就是模型处理的输入或输出的一串 token。
在论文的语境下,它主要体现在几个层面:
-
机器翻译任务中的序列
论文主要验证任务是机器翻译,所以:
- 输入 sequence:一句源语言的句子,比如英文 "I love cats" 被拆成 token 序列
[I, love, cats] - 输出 sequence:对应翻译出来的目标语言句子,比如
[我, 爱, 猫]
Transformer 本质上是一个 sequence-to-sequence (seq2seq) 模型,输入一个序列,输出另一个序列。
- 输入 sequence:一句源语言的句子,比如英文 "I love cats" 被拆成 token 序列
-
更抽象的数学定义
在论文里,一个序列通常表示成
,其中每个 是一个向量(token 的 embedding),$n$ 是序列长度 (sequence length)。 encoder 把输入序列映射成一串连续表示
,decoder 再基于它一个一个地生成输出序列。 -
为什么这个概念重要
论文开头专门对比了 RNN 和 Transformer 处理序列的方式差异:
- RNN 必须按顺序一个个处理序列元素($h_t$ 依赖
),没法并行 - Transformer 用 self-attention 让序列里任意两个位置直接交互,可以并行计算整个序列
- RNN 必须按顺序一个个处理序列元素($h_t$ 依赖
所以论文里反复出现的 "sequence transduction"(序列转换)、"sequence length"(序列长度)、"input/output sequence" 等词,都是围绕这个"有序 token 串"的概念展开的。
简单说:sequence = 一串按顺序排列的 token(或它们对应的向量),可以是一句话、一段代码、一串音符等等,只要是有先后顺序的离散元素。
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 (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 的源码。
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.
-
输入端(symbol representations,离散的)
- 一句话被切成 token,每个 token 是词表里的一个整数 ID
- 比如 "I love cats" → [15, 238, 4021]
- 这些是离散符号,它们本身没有"距离""大小"的含义(ID 15 和 16 并不比 15 和 200 更相似)
-
编码后(continuous representations,连续的)
- Encoder 把每个 token 映射成一个实数向量,比如 512 维
- 15 → [0.23, -1.05, 0.78, ..., 0.14](512 个实数)
- 这些向量里的每个数字可以是任意实数,整体构成一个
空间里的点
为什么要变成 continuous?
这是深度学习的核心思想之一,有几个重要原因:
-
可以做数学运算
离散的 token ID 你没法加减乘除("猫"+"狗" 没意义),但向量可以。著名的例子:vec(king)−vec(man)+vec(woman)≈vec(queen)
-
可以表达相似度
连续空间里,语义相近的词,向量也相近(距离小)。"猫"和"狗"的向量会比"猫"和"飞机"的向量更接近。
-
可以求梯度、做反向传播
神经网络训练靠梯度下降,而梯度需要在连续空间里才有定义。离散的东西没法直接求导,这也是为什么 NLP 模型第一步几乎都是把 token 嵌入 (embed) 成连续向量。
-
信息密度更高
一个 512 维的向量能编码的信息,远远超过一个整数 ID。模型可以在这个向量里同时塞进词义、语法角色、上下文信息等各种特征。
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
- Feed-forward (Neural) network
- Fully connected layer / Multi-layer perceptron
这几个词经常互换,指的都是那个最经典的"输入 → 若干全连接层 → 输出"的网络。
严格来说,Feedforward 是个大类:只要数据流是单向的、无环的,都算 feedforward。所以 CNN 严格来说也是一种 feedforward network。
MLP 是 feedforward 的一个具体子类:专指"全连接层堆叠"的那种结构。
在训练 AI 的语境下:
- Batch (批次):是一次塞给 GPU 的一堆数据(比如 32 句话)。
- Sample (样本):就是 Batch 里的**“这一句话”**。
- Sample 1: "I love gpu programming"
- Sample 2: "Transformer is all you need"
- ...
- Sample 32: "Hello world"
System 视角 (Tensor 形状): 假设 Embedding 维度是 4(为了方便手算),那么这个 Batch 的 Tensor 形状是 [32, 4, 4]。
- 32: Batch Size (有多少个样本)。
- 4: Sequence Length (一句话有 4 个词)。
- 4: Embedding Dimension (每个词用 4 个数字表示)。
现在我们只关注 Sample 1 ("I love gpu programming")。Transformer 里的 LayerNorm 是“独立”运作的。它不看别的句子,甚至通常不看别的词,它只盯着当前这个词的向量看。
我们把镜头对准第 3 个词 —— "gpu"。
A. 原始数据 (Input)
假设经过 Embedding 和 Attention 计算后,"gpu" 这个词在计算机里是这样的一串数字(向量):
注意:这 4 个数字代表 "gpu" 的特征(比如第一个数代表名词性,第二代表科技属性...)。这就是我们的操作对象。
B. 为什么要 Norm?
你看这个向量:[2, 6, 2, 6]。它的数值可能偏大(比如到了 6)。
在深度网络里,如果不约束,下一层可能就变成 [20, 60, 20, 60],再下一层就爆炸了(梯度爆炸)。我们需要把它拉回一个“标准范围”。
C. LayerNorm 的计算过程 (手算)
第一步:算平均值 (
算出 "gpu" 这 4 个特征的平均值。
第二步:算方差 (
算出这 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 可以堆很深而不崩溃。
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
- 输入:token ID(一个整数),ID 取值范围在 [0,V),V 是词表大小。
- 输出:dmodel 维度向量
- 参数:一个 (V, dmodel) 的矩阵 WE。
-
Linear
- 输入:dmodel 维度向量(decoder 最后一层的输出)
- 输出:V 维向量,经过 softmax 就是概率分布
- 参数:一个 (dmodel, V) 的矩阵 WO。
-
权重共享
很多模型(包括原始 Transformer、GPT-2 等)做了一个聪明的优化——让 WO=WET,两层共享参数。
为什么这样做合理?直觉理解:
- WE 的第 i 行 = "token i 在向量空间的位置"
- WO 的第 i 列 = "在向量空间什么样的向量应该预测出 token i"
这两个本质上是同一件事——一个 token 的"输入表示"和"输出预测向量"理论上应该一致。所以共享权重不仅减少参数量(省一半 embedding 参数),还能让训练更高效。
而且,如果共享了参数,会有以下视角。Linear 结果向量的每一个标量 i,都是输入向量和 WO 矩阵的第 i 列点积的结果。而 WO 的第 i 列,就是 WE 的第 i 行,就是第 i 个 token 对应的 embedding 表示。换句话说,Linear 结果向量的每一个标量 i,都在衡量输入向量和第 i 个 token 的 embedding 的方向对齐度。标量的值大,对齐度高,值小,对齐度低。
0 1 ... dmodel-1 0 1 --> V-1 0 1 ... dmodel -1 [ x x ... x ] - - [x x ... x ] | | | ... | | | | | ... | | x | | | ... | | = | | | ... | | | | | ... | | | | | ... | | - -
每个 token 向量 x 独立过 FFN。
x0 h0 o0
x1 h1 o1
x2 h2 o2
... ... ...
x511 ... o511
...
h2047
Anthropic 的可解释性研究发现一个非常重要的现象,Transformer 的不同层处理不同抽象层次的信息:
- 底层(前 1/3):偏向词法、语法、局部模式——具体的"字面"特征
- 中层(中间 1/3):偏向语义、句法结构、短程依赖
- 高层(后 1/3):偏向篇章、推理、抽象概念
也就是说,同一个 token 的向量,在不同层经过不同的 attention 加工,强调不同维度的信息。
具体说:
- 第 1 层的 attention 可能在做:"这个 token 的字符模式是什么?"
- 第 6 层的 attention 可能在做:"这个 token 的句法角色是什么?"
- 第 24 层的 attention 可能在做:"这个 token 在整段话的逻辑里扮演什么?"
层数越深,处理的信息越抽象。这就是为什么 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 通常一样,甚至更多。
继续用 "I love GPU programming" 例子,$d_{\text{model}} = 6$。
原始顺序:
打乱后的顺序:把 love 和 GPU 互换 → "I GPU love programming"
注意——这两个矩阵的"内容相同,只是行的顺序变了"(第 2 行和第 3 行互换)。
权重矩阵
原始顺序的 Q:
打乱顺序的 Q':
关键观察:因为
K 和 V 同理。
原始的 scores(假设数值):
我用具体数字代替——
打乱后的 scores':
按定义算
代入数字(注意——每个点积值本身没变,只是它们在矩阵里的位置变了):
关键观察: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 互换。数值完全没变,只是位置被搬了。
softmax 是逐行操作,不跨行。
原始 A 的第 2 行(love asking)softmax 后假设是
注意——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]
原始 output:
打乱后的 output'(love 现在在第 3 行):
注意——这两个加法的结果完全相同!因为加法是可交换的:
所以 love 这个 token 的 output 向量内容完全一样——不管它在序列里是第 2 个还是第 3 个。
原始 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)$。其中
让我们对比两个真实的句子——"cat eats fish" 和 "fish eats cat"。
这两个句子里
- 同样的三个 token(cat, eats, fish)
- 顺序不同 → 语义完全相反
但 attention 看到的:
-
完全一样 - 注意力权重的"配对关系"完全一样(cat 关注 eats、eats 关注 fish 等)
- 每个 token 算出的新表示完全一样
换句话说,attention 把序列当作没顺序的 set 而不是有顺序的 sequence,所以 attention 分不出"cat eats fish" 和 "fish eats cat"。
但是!语言是有顺序的,顺序对语义至关重要。如果 attention 看不到顺序,模型根本学不会语言。
既然 attention 看不到顺序,那就把位置信息直接编码到 token 的向量里:
现在
- "cat eats fish" 中,cat 的输入向量是
- "fish eats cat" 中,cat 的输入向量是
两个句子里 cat 的输入向量已经不一样了,输入向量里既包含“我是什么词“,也包含”我在哪个位置“。Attention 算 QKT 时,位置信息会通过点积自动参与匹配——模型自然就能学到"位置相近的 token 应该如何互动"。
我们需要一个有界的、能区分不同位置的、能扩展到任意长度的编码方法。
论文用了正弦余弦位置编码:
听起来很玄,意思其实简单——用一组不同频率的正弦/余弦波,给每个位置生成一个独特的向量。
公式(论文原版):
其中
算每个频率:
每对维度对应一个频率。分母
-
: -
: -
:
所以波长越来越长(频率越来越低)。
算每个位置的 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
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
token 2 的 PE 向量:
token 3 的 PE 向量:
矩阵形式:
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)
几个观察:
- 前两维变化最快(高频)——0→1 已经从 0 跳到 0.841,相邻位置区分明显。
- 后两维变化最慢(低频)——0→3 几乎不变(最后一列全是 1.000)。这些维度提供"大尺度"位置信息。
- 每个位置的向量都不同——这就是 PE 的核心要求。
每个位置
- 第 0、1 维:高频波(变化快,区分相邻位置)
- 第 2、3 维:稍低频
- ...
- 最后几维:极低频(变化慢,提供大尺度位置信息)
这就像一个多位的二进制时钟:
000 ← 位置 0
001 ← 位置 1
010 ← 位置 2
011 ← 位置 3
100 ← 位置 4
...
二进制每一位的"频率"不同——最低位每 1 步翻转一次,次低位每 2 步翻转一次,再上一位每 4 步翻转一次……
正弦/余弦 PE 就是这个思路的连续版本,用不同频率的正弦波代替不同频率的 0/1 翻转。每个位置都对应一个独特的"波形组合",不会重复。
论文里给出了理由:正弦/余弦有一个数学性质:$PE(pos + k)$ 可以表示为
具体说,对任意固定的 offset k:
其中
这意味着,模型可以很容易地学到"相对位置"的概念。比如学到"距离 5 的 token 之间该如何互动"——它只需要学这个固定的
训练数据是:一句源语言和一句目标语言,配对的句对。每对都是人工翻译过的、对齐的。这种数据叫做平行语料(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]
-
tokenize
这一步由 tokenizer 执行,tokenizer 先根据特定算法切分字符串,然后查 vocabulary,根据字符串得到 token id。
注意,transformer 里两个语言用的 vocabulary 是独立的。
一个英文的 vocabulary 可能是以下样子:
"I" ↔ 15 "love" ↔ 238 "GPU" ↔ 4021 "<BOS>" ↔ 1 "<EOS>" ↔ 2 "<PAD>" ↔ 0 -
加特殊 token id
给两个句子前后加上 BOS 和 EOS 的 token id。这些标记让模型知道句子从哪开始、到哪结束。
BOS 即 begin of sequence,EOS 即 end of sequence。
-
加 padding token id
batch 里不同句子长度不同——找最长的,其他用 [PAD] 补齐。attention 算的时候 [PAD] 位置会被 mask 掉,不参与计算。
假设当前训练 step 的样本是:I love GPU programming, 我 爱 GPU 编程。
目标:把人类的单词
训练和推理的第一阶段基本差不多。
-
Inputs (最底下):
- 输入序列:[1, 15, 238, 4021, 7891, 2, 0, 0, 0]。
- 动作:查表(Embedding)得到 9 个向量,并加上位置编码(Positional Encoding)。
- System 视角:此时 Tensor 形状是 [Batch, 9, 512]。
-
Encoder Block (
) (灰色框): - 数据流经 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: 每个词独立思考一下,升维再降维。
-
Encoder Output (
) (中间那根连接线): 经过 N 层处理后,生成包含了完整上下文信息的矩阵
。 E = [ e_BOS ← 含整句信息 e_I ← 主要是 I + 上下文 e_love ← 主要是 love + 上下文 e_GPU ← 主要是 GPU + 上下文 e_prog ← 主要是 programming + 上下文 e_EOS ← 含整句信息 e_PAD ← 无意义 e_PAD ← 无意义 e_PAD ← 无意义 ]关键点:这个
就是原文里说的 "sequence of continuous representations"。 它会作为 Keys (
) 和 Values ( ),发送给右边的 Decoder 使用。
目标:拿着
注意,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:
- 看到 BOS 的时候,预测「我」
- 看到 BOS 我 的时候,预测「爱」
- 看到 BOS 我 爱 的时候,预测「GPU」
- ...
- 看到 BOS 我 爱 GPU 编程 的时候,预测「EOS」
输入序列:[1, 523, 891, 4021, 12456, 2, 0]。
System 视角:这是一个 [Batch, 7, 512] 的矩阵。所有正确答案都在这里,"未来"就在显存的下一个地址里。
这里应用了上三角 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 ← 无意义
]
这就是 Cross-Attention。
Query (BOS, 我, 爱, GPU, 编程, EOS, PAD 的当前状态)。
- qBOS:BOS 在问"作为目标的开头,我需要源语言里的什么信息?"
- q我:我 在问"我已经知道前面是 BOS 和我自己,现在我需要源语言里的什么信息来帮我决定下一个目标 token?"
- q爱:同理,结合 BOS, 我, 爱 这个上下文问问题
- qGPU:基于 BOS, 我, 爱, GPU 问问题
-
q_BOS 有意义
decoder 在第 0 位看到 BOS——它需要决定"目标句的第一个真实 token 应该是什么"。
q_BOS 的语义是"作为目标句开头,我需要源句里的什么信息?"——通常它会用比较平均的 attention 关注整个源句,因为它要"概览"整个原文才能决定第一个目标词。
-
q_EOS 有意义
decoder 输入的某个位置如果是 EOS,这个位置的标签可能是 PAD。这样的话,这个位置的 loss 不计算,不参与梯度训练。虽然 q_EOS 的计算是有效的。
-
q_PAD 计算有效,但不被使用
q_PAD 会被算出来(GPU 不会单独跳过这些位置),cross-attention 也会给 q_PAD 算出输出向量。
但 q_PAD 对应的位置在 loss 计算时被 mask 掉——梯度不传回来——这些计算的结果不影响模型学习。
所以从语义上,q_PAD 没意义——它只是"占着 GPU 的算力,结果被丢弃"。
Key (
- kI 表达"I 这个源 token 能匹配什么样的目标需求?"
- vI 表达"如果 I 被某个目标 token 关注,它能贡献什么内容?"
- klove vlove、kGPU vGPU、kprog vprog 同理
-
k_BOS 有意义
它代表"源句的开头",某些目标 token 可能想关注它来感知"原文从哪里开始"。
虽然信号通常较弱(BOS 不携带具体语义),但模型可能利用它做"句子结构"层面的对齐。
-
k_EOS 有意义
它代表"源句的结尾",某些目标 token(特别是即将生成 EOS 的那些)可能想关注它来感知"原文已经讲完了"。
研究发现:很多 attention head 实际上喜欢关注 EOS。原因是 EOS 经过 encoder 处理后,会汇集整句的全局信息(因为它是序列里最后一个,所有前面信息都被它看到过)——所以它常常被当作一个"全局摘要"来使用。
-
k_PAD 没意义
PAD 是占位符,不携带任何语言信息。所以:
- 在 attention 算分时,PAD 列被 padding mask 设成 −∞
- softmax 后那一列为 0
- 没有任何目标 token 会关注源 PAD
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 −
- k_PAD 列在 softmax 前被设成 −∞,softmax 后变 0——任何目标 token 都看不到源 PAD
- k_BOS 和 k_EOS 是有意义的——目标 token 可能关注它们获取"句子开头/结尾"的信号
- q_PAD 行虽然会被算出来,但对应位置的 loss 被 mask 掉,实际上不参与训练
假设经过缩放和 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]
- BOS 还没接收任何具体信息——比较平均地关注整个源句
- 它要决定"目标句的第一个真实词是什么"——需要概览整个原文
- 对源 BOS 和 EOS 关注稍少(它们不携带具体语义,只是边界标志)
q_我 这一行:[0.05, 0.65, 0.10, 0.05, 0.05, 0.10, 0, 0, 0]
- "我" 高度关注源 "I"(翻译对齐,权重 0.65)
- 略微关注 EOS(0.10)——可能在用 EOS 的"全局摘要"做辅助判断
- 其他位置都很弱
q_爱 这一行:[0.05, 0.10, 0.70, 0.05, 0.05, 0.05, 0, 0, 0]
- "爱" 高度关注源 "love"(权重 0.70)—— 明显的翻译对齐
q_GPU 这一行:[0.03, 0.07, 0.10, 0.65, 0.10, 0.05, 0, 0, 0]
- 高度关注源 "GPU"(0.65)
- 略微关注 "love" 和 "programming"(0.10)—— 因为上下文里它们和 GPU 相关
q_编程 这一行:[0.03, 0.05, 0.07, 0.15, 0.60, 0.10, 0, 0, 0]
- 高度关注源 "programming"(0.60)—— 翻译对齐
- 也关注 "GPU"(0.15)—— 因为 "programming" 在原句里紧跟 GPU
q_EOS 这一行:[0.05, 0.05, 0.05, 0.05, 0.10, 0.70, 0, 0, 0]
- 高度关注源 EOS(0.70)
- 这是模型在确认"原文已经结束了",准备输出 EOS 的信号
- 这一行虽然算出来,但训练时对应的标签是 PAD,loss 被 mask 掉——不参与梯度
q_PAD 这一行:[0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0, 0, 0]
- q_PAD 没有学到任何有意义的关注模式——分布接近平均
- 算了,但 loss 处会被丢弃——梯度不传回来
注意,每个目标 token 的 output 现在融合了源语言信息:
- "我" 这个目标 token 的新表示主要吸收了 vI 的内容——它带着"原文里那个对应'我'的源 token (I) 的语义"
- "爱" 主要吸收了 vlove 的内容——带着源 token "love" 的语义
- "GPU" 主要吸收了 vGPU 的内容
这就是翻译的核心机制——目标 token 通过 cross-attention 找到自己对应的源 token,吸收源 token 的语义,用来辅助预测下一个目标 token。
上面已经说了,cross-attention 给的是"吸收了源 token 信息的混合向量",但这只是信息的简单加权。FFN 提供了非线性加工,它可以基于已经吸收的信息做更复杂的转换。
具体来说,FFN 在这个位置可能在做这样的事:
- 把"我吸收了 GPU 的语义"和"我自己是目标语言的 GPU"整合
- 利用 FFN 里存储的"知识"(比如"GPU 后面常常跟某些词")
- 把表示调整到适合"预测下一个目标 token"的形式
但 FFN 本身并不输出"下一个 token",它输出的还是一个 dmodel 维向量。
Decoder 通常有多层(比如 6 层),每一层都是 "masked self-attention → cross-attention → FFN"。
第 1 层的输出送到第 2 层,第 2 层的输出送到第 3 层,直到第 6 层输出。每一层都让 token 表示更精炼、更接近"应该预测的下一个 token"的形态。
目标:对比预测结果和真实标签,更新参数。
-
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 是教会模型停止的核心信号。
-
Backpropagation:
梯度同时传回 Decoder 和 Encoder 的所有层,更新权重。
准备数据
↓
初始化模型参数(随机)
↓
循环(几十万到几百万步):
1. 拿一个 batch 的句对
2. 喂给 Encoder + Decoder(teacher forcing)
3. 算 loss
4. 反向传播
5. 更新参数
↓
loss 收敛 / 训完预定步数
↓
模型训练完成
任务:将英文 "I love GPU" 翻译成法文 "J'aime GPU"。
论文原文里提到的数学符号,我们这样对应:
- x (Input): "I", "love", "GPU"
- z (Continuous representations): Encoder 算出来的“语义矩阵”
- y (Generated Output): 模型生成的每一个词 (推理开始时是空的)
目标:把人类的单词
-
Inputs (最底下):
- 输入序列:["I", "love", "GPU"]。
- 动作:查表(Embedding)得到 3 个向量,并加上位置编码(Positional Encoding)。
- System 视角:输入 Tensor 形状 [1, 3, 512]。(Batch=1, Src_Len=3, Dim=512)
-
Encoder Block (
) (灰色框): - 数据流经 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] 不变。
-
Encoder Output (
) (中间那根连接线): 经过 N 层处理后,出来的不再是简单的单词向量,而是一个包含了完整上下文信息的矩阵
。 关键点:这个
就是原文里说的 "sequence of continuous representations"。 它会作为 Keys (
) 和 Values ( ),发送给右边的 Decoder 使用。 System 视角:输出 Tensor z 的形状是 [1, 3, 512]。这是后续 Cross-Attention 的 K 和 V 的来源。
目标:拿着
我们假设现在模型刚启动,要生成第一个词。
Step 1: 生成 "J'aime"
-
Decoder Input (当前时刻输入):
推理模式下没有“右移”的概念,只有“当前已生成的序列”。
初始状态:输入仅包含
["<Start>"]。System 视角:输入 Tensor 形状 [1, 1, 512]。 (Batch=1, Cur_Len=1, Dim=512)
-
Masked Multi-Head Attention (右边第一个粉色块):
<Start>只能看自己。在推理时,后面根本没有词,物理上不存在“未来”。Mask 在这里只是为了维持代码逻辑的一致性,遮住的是无效的 Padding 或空内存。
System 视角:输出 Tensor 形状依然是 [1, 1, 512]。
-
Multi-Head Attention (右边中间的粉色块) —— 核心!
这就是 Cross-Attention。
Query (
): 来自 Decoder 的 <Start>向量。意思是:“我刚开始,我要找主语”。System 视角 (Q): 形状 [1, 1, 512] (来自 Decoder)
Key (
) & Value ( ): 来自左边 Encoder 传过来的 ("I love GPU" 的语义)。 System 视角 (K, V): 形状 [1, 3, 512] (来自 Encoder Output z)
注意:这里发生了长度不匹配的交互 (1 vs 3)。Attention 会计算 [1, 1] 和 [1, 3] 的关系,生成 1x3 的分数矩阵。
交互:Decoder 拿着
去 里找。发现 "I" (英文主语) 的匹配度最高。于是把 "I" 对应的 拿过来。 System 视角 (Output): 加权求和后,输出形状回归到 [1, 1, 512]。
-
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",指的就是这里。
-
Decoder Input (更新后的序列):
现在的输入变成了:
["<Start>", "J'aime"]。把上一步生成的
喂回去了! System 视角:输入 Tensor 形状变长了!现在是 [1, 2, 512]。 (Batch=1, Cur_Len=2, Dim=512)
-
Masked Multi-Head Attention:
"J'aime" 可以看
<Start>和它自己。System 视角:Attention 机制回顾历史。虽然逻辑上是看
<Start>和J'aime,但在实际代码中,Mask 矩阵依然存在,确保我们不会访问到显存里那些未初始化的地址。System 视角:输出 Tensor 形状 [1, 2, 512]。
-
Cross-Attention:
Query (
): 来自 "J'aime"。意思是:“我有主语和谓语了,我要找宾语”。 System 视角 (Q): 形状 [1, 2, 512] (来自 Decoder,注意这里长度是 2)
Key/Value (
): 还是那个 。 System 视角 (K, V): 形状 [1, 3, 512] (Encoder 永远不变)
交互:计算 2 个 Decoder 词和 3 个 Encoder 词的关系。
交互:Decoder 发现 "GPU" (英文宾语) 和当前的
最匹配。把 "GPU" 的信息拿过来。 System 视角 (Output): 输出形状 [1, 2, 512]。
-
Output:
计算概率,输出 y2 = "GPU"。
System 视角 (Linear): 形状 [1, 2, 30000]。我们通常只取最后一个时间步 (Sequence Length 的最后一位) 来做预测。
Step 3: 结束
输入变成 ["<Start>", "J'aime", "GPU"]。
重复上述过程,Decoder 发现句子完整了,输出 <End> 标记。翻译结束。