本文是Transformer架构梳理的第一篇文章。
内容为经典Encoder-Decoder架构的可视化拆解,它本身只是我的个人笔记
Transformer是一台翻译机
Transformer架构登场于Google的Attention is all you need论文里,是Machine Learning领域的一个革新点,它的创作初衷是更高效地解决机器翻译Seq2seq(Sequence to Sequence)问题。
所以,我将Transformer视作翻译机。 请看下面的迷你架构图:
图中,原先的句子先是被encoder处理成中间态的语义上下文,decoder再据上下文,逐字(自回归)地生成一个结果句。
整个过程是一个翻译行为。所以,理解经典Transformer之前,得先理解翻译本身。
翻译的难点
在现实物理世界里,我将翻译行为分为两步:
- 译者收到原信息,结合自身知识理解上下文,并暂存上下文
- 译者根据上下文陈述
实际上,Transformer也是这样子做的。Encoder对应第一步,Decoder对应第二步。 不过, 机器过程没有人类逻辑复杂,我们早已习惯翻译,胆子大点,来理一理机器过程吧。
看起来只有两个步骤,而难点在于——要怎么实现呢?
Encoder - 理解的实现
Encoder是这样做的。例,源句为Iloveyourdog:
嵌入(Embedding)
嵌入分为几步:
- Tokenization
- Embedding
- Position Embedding
原 Sequence(语句)经过Tokenization被拆解为多个片段,成为Token(词元)序列(不同算法拆分结果不一,但为了保持示例简洁,下文的所有示例token都采用整词拆解形式)。设拆解个数为 n,表示为:
X=(x1,x2,…,xn)
上述过程中,Iloveyourdog首先被拆解为(I,love,your,dog)
然后,对每个token进行数学建模。 比如第一个token 可以写成: x1=[0.12,−0.31,0.77,…]∈Rdmodel
把整句堆起来后,就得到输入矩阵: X∈Rn×dmodel
假设我们的模型训练维度为6维,每个 token 就被转化成 6 维向量表示:
X=0.120.0−0.20.05−0.50.880.1−0.11.00.10.950.00.01.2−0.10.80.45−0.31.10.3−0.10.50.01.5(I)(love)(your)(dog)
几何视角上,这就是 4 个漂浮在 6 维空间里的点。它们现在还比较“孤立”:I 只是I,love 只是love,dog 也还只是词典意义上的dog。 如何让这6各点互相分享信息,各自影响,最终为矩阵增加语义, 这是下一步Attention要做的事情。
1) Self-Attention: 自注意力机制
Attention 解决的是:让表示矩阵中的每一行都完成它自身的语义补充。流程如下:
数学表示见:
Attention(Q,K,V)=softmax(dkQK⊤)V
式子中参数很多,但有理可循。
Q、K、V
式子中,被Attention函数包裹的是QKV,而Attention要操作的是输入矩阵X, 所以QKV和X必然存在关联。 实际上,QKV代表的, 正是模型使用它在训练中习得的权重经验从三个角度对源句矩阵X进行投影所得的具有特殊目的矩阵——权重矩阵。
权重矩阵通常称为 W,像一份被反复打磨过的"舞台剧本";每个 token 都是一个演员,它一开始只知道自己拿到的剧本内容,整出戏是否完美(语义是否完美),要靠它和其余演员充分对戏。
到了 Self-Attention 这里,这份“剧本”分成三部分:WQ,WK,WV。它们不是输入句子的一部分,而是模型训练后留下来的参数。输入表示矩阵 X 每进入一层,都会按这三组参数生成对应的 Q,K,V 矩阵。
实现上,先把当前表示矩阵 X 投影成三组矩阵:
Q=XWQ,K=XWK,V=XWV
如果只看矩阵中的第 i 行,它们分别有这样的角色:
- qi:这一行表示“我在找什么”
- ki:这一行表示“我如何被匹配”
- vi:这一行表示“我能贡献什么内容”
所以 X 一次性乘上 WQ,WK,WV,只是并行矩阵写法。它等价于每一行分别生成自己的 qi,ki,vi:
qi=xiWQ,ki=xiWK,vi=xiWV
这一步很像“领剧本”:同一个 token 表示行,经过三套不同参数,被投影成三种职能。
第二步:看一个完整的 Q、K、V 生成例子
假设当前输入矩阵 X 是 4×6。为了让后面的计算更有实感,下面拟三组同尺度的模拟权重。注意:这些数值只是为了演示矩阵链路,不代表真实模型训练出来的参数。
三组权重矩阵可以一起写成:
WQ=100.5001010.500.5000010.51∈R6×3
WK=0.60.00.40.10.00.50.10.80.30.00.70.00.00.20.10.90.40.8∈R6×3
WV=0.90.00.20.00.10.00.00.80.10.20.00.10.10.00.90.00.30.00.00.20.00.80.10.40.20.00.30.10.90.00.00.10.00.40.00.9∈R6×6
于是整句一次性相乘,得到三组结果矩阵:
Q=XWQ=0.5200.5500.2751.5500.2250.7801.1250.0500.1251.5500.4502.450(I)(love)(your)(dog)
K=XWK=0.4220.4100.2500.8600.2270.5241.1150.1350.1001.5460.4652.020(I)(love)(your)(dog)
V=XWV=0.353−0.0100.1200.075−0.3101.0040.1550.2301.0470.0001.1650.095−0.0951.3060.0501.2500.729−0.1201.2250.360−0.1401.018−0.0301.660(I)(love)(your)(dog)
这三次乘法的输入都是同一个 X,区别只在于右侧乘上的权重矩阵不同。于是同一批 token 表示,被投影到了三种不同的观察空间里:Q 负责“我在找什么”,K 负责“我如何被匹配”,V 负责“如果别人关注我,我能贡献什么内容”。
注意,Q,K,V 都是整句话共同组成的矩阵,而不是 1×n 的临时向量。后面之所以能抽出 qlove 或 kI,只是因为它们分别是 Q,K 矩阵中的某一行。
这里让 WV 输出 6 维,是为了让 AV 的结果可以直接回到主模型维度;真实 Multi-Head Attention 里也常见先输出较短的 dv,最后再用 WO 投回 dmodel。
这也不是简单地“缩短向量”,而是把原始语义放到不同的新坐标系里观察。借用 3Blue1Brown 常讲的线性变换直觉:矩阵的列向量像新的基坐标轴,矩阵乘法就是把一个点投影到这些新轴上。WQ 让 token 表示变成“搜索信号”,WK 让 token 表示变成“可被搜索的标签”,WV 则保留“真正能被取走的内容”。
第三步:用 QK 转置计算表示行之间的打分
生成 Q,K,V 后,不是只算 qiki⊤,而是让第 i 行和所有 token 表示行做匹配:
si=[qik1⊤,qik2⊤,…,qikn⊤]
整体写成矩阵,就是:
S=QK⊤∈Rn×n
这里第 i 行表示“第 i 个 token 表示行在看谁”,第 j 列表示“它看第 j 个 token 表示行的分数”。所以 QK⊤ 得到的是 token 表示行之间的相似度,而不是特征维度之间的相关性。K⊤ 也没有改变 key 的含义,只是把所有 key 摆成适合被 query 批量点积的方向。
这里拿 love 看 I 做一次完整展开。为了避免跳读,先把完整 Q,K 再摆出来:
Q=0.5200.5500.2751.5500.2250.7801.1250.0500.1251.5500.4502.450(I)(love)(your)(dog)
K=0.4220.4100.2500.8600.2270.5241.1150.1350.1001.5460.4652.020(I)(love)(your)(dog)
于是:
qlove=Qlove,:=[0.5500.7801.550]
kI=KI,:=[0.4220.2270.100]
因为本文采用 token-as-row 写法,所以计算 love 对 I 的注意力打分时,是把 kI 转置成列向量,放在 qlove 的右侧:
Scorelove,I=qlovekI⊤
qlovekI⊤=[0.550.781.55]0.4220.2270.100
$$
\begin{bmatrix} 0.55\times0.422+0.78\times0.227+1.55\times0.100 \end{bmatrix} $$
$$
\begin{bmatrix} 0.564 \end{bmatrix} $$
这个 1×1 的结果,就是 love 这个 query 对 I 这个 key 的原始注意力分数。
几何上,kI⊤ 可以理解成一把由 I 定义出来的线性标尺。qlove 乘上 kI⊤,不是把整个空间“倒过来”,而是用这把标尺去测量 qlove:它在 I 所代表的 key 方向上投影有多强。
如果分数很大,说明在当前语境里,love 这个 token 很需要 attend to I;如果分数很小,说明 I 对当前的 love 来说暂时不重要。把这件事对所有 key 都做一遍,就得到 love 那一整行 attention scores。
第四步:用 softmax 变成比例,再混合 Value
得到 scores 后,每一行会先除以 dk 做缩放,再过 softmax:
ai=softmax(dksi)
最后用这一行比例混合所有 value:
oi=j=1∑naijvj
这就是输出矩阵 O 的第 i 行,也就是第 i 个 token 表示行吸收全场信息后的新表示。
比如某一层里,love 对全句的注意力比例可能是:
alove=[0.30,0.45,0.10,0.15]
那么它的新表示就是:
olove=0.30vI+0.45vlove+0.10vyour+0.15vdog
这一步就是“向量修正”。原本 love 所在的坐标点,会吸收 I,your,dog 等 token 的信息。于是它不再只是词典里那个孤立的动词,而变成了“由 I 发出、指向某个对象的 love”。这就是 attend to 在数学上的体现:一个 token 因为关注了别的 token,自己的向量位置被重新塑形。
所以 Self-Attention 的核心并不只是“算相关性”,而是:
- 用 WQ,WK,WV 把表示矩阵投影成三种视角
- 用 QK⊤ 得到 token 表示行之间的对戏强度
- 用 softmax 把强度变成关注比例
- 用这些比例混合 V,完成每个 token 的向量修正
也就是说,每一层 Attention 都在让词向量发生一次空间位移。层数堆起来后,每个 token 的向量里都会逐渐带上全句的影子。
第五步:多头就是多种视角同时对戏
如果是 Multi-Head Attention,则相当于同一群演员同时从多种角度排练:有的头关注语法依赖,有的头关注指代关系,有的头关注语义搭配。多个 head 的结果拼接后,再通过 WO 投回主模型空间,继续交给后续层处理。
总结,Attention 要做的事,不是让某个 token 单独“理解全句”,而是让每个 token 在全句语境里重新理解自己:我是谁、我和谁有关、我该吸收谁的信息、最后我应该带着怎样的语义继续往后走。
2) Add & Norm:稳定训练并保留原信息
注意力输出不会直接替换输入,而是残差相加,再归一化: X′=LayerNorm(X+SelfAttention(X))
这样做有两个好处:
- 原始信息不丢(有“捷径”可走)
- 深层网络更稳定(梯度不容易炸或消失)
3) FFN:逐位置非线性加工
然后每个位置独立走同一个前馈网络: FFN(x)=max(0,xW1+b1)W2+b2
再做一次 Add & Norm: Xnext=LayerNorm(X′+FFN(X′))
这表示:先“看全局关系”,再“做本地提纯”。
这里的“逐位置”很重要。Self-Attention 会让 token 之间交换信息,而 FFN 不再让 token 彼此通信。它对每个位置单独处理,但所有位置共享同一套 W1,b1,W2,b2 参数。
也就是说,对第 i 个 token 来说:
xi′→FFN(xi′)
对第 j 个 token 来说:
xj′→FFN(xj′)
两者互不读取,但使用的是同一个函数。这有点像给每个已经融合上下文的 token 都做一次相同规格的“特征重组”。
这个加工通常可以理解为一次“升维—激活—降维”:
- 先升维:W1 把向量投影到更宽的隐藏空间。低维里挤在一起的语义,到了高维后更容易被展开。
- 再激活:ReLU 或 GELU 引入非线性。否则两次矩阵乘法本质上仍然可以合并成一次线性变换,表达能力不够。
- 最后降维:W2 把结果压回 dmodel,保证输出尺寸和输入一致,下一层 Encoder 才能继续接上。
所以,Attention 负责把“别的 token 的信息”写进当前位置,FFN 负责在当前位置内部重新组合这些信息。前者解决“该看谁”,后者解决“看完以后怎么消化”。
当这样的层堆叠多次后,Encoder输出最终上下文表示: Z=(z1,z2,…,zn)
姑且把Z叫作memory(Attention is all you need论文上并没有对该产物进行命名,但是多数代码实现上把它命名为memory),它可以理解为“机器已理解后的语义记忆”,随后交给 Decoder 去完成陈述与生成。
Decoder - 陈述的实现
Encoder 做完以后,机器已经有了一份对源句的理解,也就是前面说的 memory:
Z=(z1,z2,…,zn)
如果 Encoder 像“读懂原句”,那么 Decoder 就像“根据理解往外说”。但它不是一次性把目标句全部吐出来,而是一步一步生成:
<BOS>→I→love→your→dog
在翻译里,更真实的目标句可能是中文,比如:
<BOS>→我→爱→你的→狗
这里的 <BOS> 表示 begin of sentence,也就是“开始说话”的信号。
Decoder 的整体流程可以先看成这样:
它比 Encoder 多了一件关键事情:Decoder 既要看自己已经说过的话,也要看 Encoder 理解出来的源句上下文。
1) 目标端 Embedding:把已经说出的词变成矩阵
Decoder 的输入不是源句,而是目标句中“已经生成出来的部分”。
假设现在模型已经生成了:
Y=(y1,y2,…,ym)
经过目标端 Embedding 和 Position Embedding 后,也会得到一个矩阵:
Y∈Rm×dmodel
这和 Encoder 的输入矩阵 X 很像,只不过 X 来自源句,Y 来自目标端已经生成的部分。
2) Masked Self-Attention:只能看过去,不能偷看未来
Decoder 的第一层 Attention 叫 Masked Self-Attention。
它和 Encoder 的 Self-Attention 很像,也会从 Y 中生成:
QY=YWQ,KY=YWK,VY=YWV
区别在于:Decoder 生成第 t 个词时,不能提前看到第 t+1 个词。否则训练时它会作弊,直接抄答案。
所以,它需要一个 mask,把未来位置遮住:
矩阵上可以理解成:
$$ \text{MaskedAttention}(Q_Y,K_Y,V_Y)
\text{softmax}\left(\frac{Q_YK_Y^\top+M}{\sqrt{d_k}}\right)V_Y $$
其中 M 是 mask 矩阵。允许看的地方是 0,不允许看的未来位置是 −∞。这样 softmax 之后,未来位置的概率就会变成 0。
比如目标端已有 4 个位置时,mask 大概长这样:
M=0000−∞000−∞−∞00−∞−∞−∞0
它的含义很直接:
- 第 1 个位置只能看第 1 个位置
- 第 2 个位置可以看第 1、2 个位置
- 第 3 个位置可以看第 1、2、3 个位置
- 越往后,能看的历史越多,但永远不能看未来
这就是自回归的核心:每一步只能根据过去和现在,猜下一个词。
3) Cross-Attention:一边说,一边回看原文
Masked Self-Attention 解决的是“目标句内部的上下文”。比如已经说了“我 爱”,那下一步很可能要说一个宾语。
但翻译不能只看自己说过什么,还要回看原文。这个动作就是 Cross-Attention。
Cross-Attention 的关键在于:Q 来自 Decoder,K,V 来自 Encoder。
Q=Y′WQ,K=ZWK,V=ZWV
这里 Y′ 是 Masked Self-Attention 之后的目标端表示,Z 是 Encoder 输出的 memory。
公式仍然是熟悉的 Attention:
CrossAttention(Q,K,V)=softmax(dkQK⊤)V
只是这次含义变了:
- Query 来自 Decoder:我现在要说什么?
- Key 来自 Encoder:原文里哪些位置能回应我?
- Value 来自 Encoder:如果我关注了这些原文位置,可以取走什么语义?
如果继续用演员比喻:Masked Self-Attention 是目标句演员内部先对戏,确认“我前面已经说了什么”;Cross-Attention 则是目标句演员回头看原文演员,确认“我现在该翻译原文里的哪一部分”。
比如在生成“狗”时,Decoder 里当前这个位置可能会强烈关注 Encoder memory 中对应 dog 的那一行;生成“你的”时,则可能更关注 your。这就是翻译里的对齐感。
4) Decoder 里的 Add & Norm 和 FFN
Decoder 每个 Attention 后也会做 Add & Norm。
第一处:
Y′=LayerNorm(Y+MaskedSelfAttention(Y))
第二处:
Y′′=LayerNorm(Y′+CrossAttention(Y′,Z))
然后再经过 FFN:
Ynext=LayerNorm(Y′′+FFN(Y′′))
这部分和 Encoder 的逻辑很像:Attention 负责交换信息,FFN 负责在每个位置内部做非线性加工,Add & Norm 则负责稳定训练并保留原信息。
区别在于 Decoder 有两次 Attention:
- 第一次看目标端自己,但只能看过去
- 第二次看 Encoder 的 memory,把原文语义接进来
5) Linear + Softmax:从向量变成词
Decoder 最后一层输出后,还只是一个表示矩阵。要真正生成词,还需要把最后一个位置的向量变成词表上的概率分布。
假设最后一个位置的输出向量是 ot,先经过线性层:
logits=otWvocab+b
其中 Wvocab 会把模型维度投影到词表大小:
Rdmodel→R∣V∣
然后 softmax:
pt=softmax(logits)
如果词表里有很多候选词,softmax 会给每个词一个概率。比如:
P(狗)=0.62,P(猫)=0.11,P(人)=0.04
模型就会根据解码策略选择下一个 token。最简单的是取概率最高的词,也就是 greedy decoding;更复杂的还有 beam search、top-k、top-p 等方法。这里先不展开,否则会跑出 Transformer 主线。
生成出下一个 token 后,它会被追加到目标序列末尾,再送回 Decoder,继续生成下一个:
所以 Decoder 的本质是循环陈述:
- 看自己已经说过什么
- 回看 Encoder 理解出的原文 memory
- 生成下一个最合适的 token
- 把这个 token 接回输入,继续下一轮
直到生成 <EOS>,也就是 end of sentence,整句翻译才结束。
到这里,经典 Encoder-Decoder Transformer 的主线就闭合了:Encoder 负责把源句读成 memory,Decoder 负责根据 memory 一步步说出目标句。
为什么是Transformer
讲完 Encoder 和 Decoder 后,还剩一个问题:为什么偏偏是 Transformer?
它不是唯一能做翻译的模型。RNN、LSTM、GRU、CNN Seq2Seq 都能做。但 Transformer 真正厉害的地方在于:它把“语义建模”这件事,改造成了特别适合扩大规模、特别适合 GPU 并发、特别适合大数据训练的矩阵计算。
可以粗略概括为三点:
- 它能比较自然地把模型做大
- 它能充分利用 GPU 并发
- 它把长距离依赖的路径压短了,训练效率更高
1) 深度学习为什么喜欢大模型
深度学习里有一个很朴素经验:在数据足够、训练足够稳定的前提下,模型规模越大,通常效果越好。
这里的“大”是指:
- 层数更深,可以做更多轮抽象
- 隐藏维度更宽,可以容纳更丰富的语义
- FFN 中间层更大,可以提供更强的非线性加工能力
- Attention head 更多,可以从更多关系角度观察句子
Transformer 正好非常适合做这些扩展。
前面说过,一个 Transformer Block 大致由 Self-Attention、Add & Norm、FFN 组成。其中真正吃参数的大头,往往不是 Attention,而是 FFN。
典型 FFN 是: FFN(x)=σ(xW1+b1)W2+b2
如果模型维度是 dmodel,中间层维度是 dff,那么 FFN 的主要参数量大约是:
2dmodeldff
这意味着 Transformer 可以很直接地通过扩大 dmodel、扩大 dff、增加层数、增加 head 数来提升容量。
但“能变大”不等于“变大后还能训练”。Transformer 能撑住规模,基于一下:
- 残差连接让信息有捷径可走,深层时不容易断
- LayerNorm 稳定每层输入输出的分布
- Attention 让每层都能直接交换全局信息
- FFN 提供大规模参数容量,负责局部非线性加工
所以它不是单纯把 MLP 堆大,而是把“全局通信”和“大规模非线性加工”组合到一个稳定的模块里,再一层层堆起来。
从这个角度看,Transformer 的核心不是某个单独公式,而是一个非常适合 scale up 的工程结构。
2) Transformer 为什么适合 GPU
GPU 擅长什么并行计算?
RNN 类模型的问题在于,它天然有时间顺序依赖。要算第 t 个位置,往往得先算完第 t−1 个位置:
ht=f(ht−1,xt)
这就像排队传话。第 10 个人想开口,必须等前 9 个人依次说完。
而 Transformer 不这样做。
Self-Attention 会把整句表示矩阵一次性拿出来:
X∈Rn×dmodel
然后直接做矩阵乘法:
Q=XWQ,K=XWK,V=XWV
再做:
S=QK⊤
这类操作非常适合 GPU,因为它们都是大块矩阵乘法,也就是 GEMM。
这就是 Transformer 的硬件友好性:它让很多 token 的计算同时发生,而不是按 token 顺序一个个推进。
所以在训练时,Transformer 可以同时利用多个维度的并行:
- batch 维度:多条句子一起算
- sequence 维度:一句话里的多个 token 一起算
- hidden 维度:每个向量里的多个维度一起算
- head 维度:多个 attention head 一起算
Transformer贴合GPU并行优势。它不只是“算法好”,而是它的计算形态能够充分发挥硬件优势, 吕布骑上了赤兔。
3) 计算量优势到底在哪里
严格说,Transformer 的 Self-Attention 不是在所有情况下都更省计算。
Self-Attention 的核心打分是:
QK⊤
如果句长是 n,维度是 d,那么它大致需要:
O(n2d)
因为每个 token 都要看所有 token,所以会有 n×n 的注意力矩阵。句子特别长时,这个二次复杂度并不便宜。
但 Transformer 的优势不只看理论 FLOPs,还要看三件事。
第一,Transformer 的计算可以并行。
RNN 可能每一步计算量不大,但它很多步骤必须顺序做;Transformer 单层计算量可能更大,但能一次性并行铺开。在 GPU 上,并行矩阵乘法往往比串行小计算更有效率。
第二,Transformer 的信息路径更短。
在 RNN 中,句首 token 要影响句尾 token,需要沿着时间步一步步传过去:
x1→h1→h2→⋯→hn
路径长度大约是 O(n)。
而在 Self-Attention 中,任意两个 token 可以在一层里直接交互:
xi→xj
路径长度接近 O(1)。
这对翻译很关键。因为一个词的含义经常由很远处的词决定,比如主语、宾语、修饰关系、指代关系。路径越短,信息越不容易在传递过程中变弱。
第三,Transformer 的主要计算是规则矩阵乘法。
规则矩阵乘法有大量成熟优化:GPU kernel、张量核心、混合精度、批量化、算子融合等。理论复杂度只是一部分,实际训练速度还取决于硬件利用率。
所以 Transformer 的“计算量优势”更准确地说,是:
- 它不一定在 FLOPs 上永远最少
- 但它把计算组织成了更容易并行、更容易优化、更容易堆规模的形式
- 在大数据和大 GPU 集群上,它能把更多计算真正转化成模型能力
小结
Transformer 之所以成为主流,不只是因为 Attention 这个想法漂亮,而是因为它同时满足了三个条件:
- 结构清晰:Attention + MLP
- 可扩展:宽度、深度、head 数、FFN 维度都能继续放大
- 硬件友好:核心计算是大规模矩阵乘法,能吃满 GPU 并发
换句话说,Transformer 不只是一个翻译模型结构,它更像是一种把“语言理解”改写成“可并行矩阵计算”的方法。
这也是为什么后来的大语言模型基本都沿着 Transformer 继续放大:模型越大,数据越多,算力越强,这个结构越能显出优势。