📚 本文是《Transformer 由浅入深》系列第 7 篇 · 阶段三「搭起完整模型」

上一篇我们造好了一块完整的积木——Transformer Block。这一篇,我们用它搭出 2017 年原版论文《Attention Is All You Need》的完整架构:一个 Encoder-Decoder(编码器-解码器) 结构。

读完这篇,论文里那张被无数次引用的经典架构图,你应该就能看懂每一根线。

如果把整个模型想象成一个专业翻译官在工作,那么它做的事其实和人类译者高度相似:先把原文从头到尾通读一遍、彻底读懂,在脑子里形成对整段话的理解;然后才提起笔,一个词一个词地写出译文,而且每写一个词,都会时不时回头瞄一眼原文,确认自己没跑题。这一前一后的两个阶段,恰好对应模型的两大部件:编码器负责"通读理解",解码器负责"逐词落笔"。记住这个翻译官的比喻,下面所有细节都能挂在它上面。

本篇目标

读完你应该能:

  • 说清编码器和解码器各自的职责;
  • 区分模型里的三处注意力(编码器自注意力、解码器掩码自注意力、cross-attention);
  • 理解因果掩码为什么是生成任务的命根子。

阅读前提

第 6 篇(Transformer Block)。

1. 任务设定:以机器翻译为例

原版 Transformer 是为机器翻译设计的,我们就用它来理解。任务是:输入一句中文,输出对应的英文。

这天然分成两个阶段:

  1. 读懂源句——把整句中文"理解"成一组富含上下文的表示。这是编码器(Encoder) 的活。
  2. 逐词生成译文——一边参考源句的理解,一边一个词一个词地写出英文。这是解码器(Decoder) 的活。

为什么非要拆成两段?因为这两件事的"信息条件"根本不同。读懂原文时,整句话已经摆在眼前,前后文都能看,看得越全理解越准;而生成译文时,后面的词还没写出来,你只能依赖已经写好的部分。一个面对"完整已知"的输入,一个面对"边写边长"的输出,把它们交给两套各有所长的部件,远比硬塞进一套要清爽。这也解释了后文反复出现的"编码器双向、解码器单向"的根本原因——不是设计者的偏好,而是任务本身决定的。

整体长这样:

   源句(中文)              目标句(英文,逐词生成)
      │                            │
      ▼                            ▼
 ┌─────────┐   memory      ┌─────────────┐
 │ Encoder │ ───────────►  │   Decoder   │ ──► 下一个词的概率
 │ × N 层  │  (源句表示)    │   × N 层    │
 └─────────┘               └─────────────┘

2. 编码器(Encoder)

编码器就是把第 6 篇的 Block 叠 \( N \) 层。它的特点是自注意力是"双向"的:编码源句时,每个词都能同时看到左边和右边的所有词——因为读理解时,前后文都该看。

为什么读理解要双向?想想我们做语文阅读理解:遇到一个有歧义的词,常常要往后多读几句才能定下它的意思。比如中文里"苹果"到底是水果还是公司,得看后面是"很甜"还是"发布会"。如果只许从左往右、看不到后文,理解就会瘸腿。编码器面对的是一句完整的、已经给定的源句,既然整句都在,当然要前后通看,把每个词放进它真正的语境里去理解。这就是翻译官"通读全文"的环节——他绝不会只读半句就动笔。

再举个更日常的例子:你读到"他把行李放下,长舒一口气"这句话里的"放下",含义是清楚的;但如果句子是"他始终放不下",这个"放下"就从动作变成了情感。区别全在后文。人脑读句子时是整体感知的,从不真的一个字一个字、看完前面才允许看后面;编码器的双向自注意力,正是在模仿这种"一眼看全、前后互相印证"的理解方式。这也是为什么编码器不需要任何掩码——它面对的是已经躺在桌上的完整源句,没有"未来"要遮挡,遮挡反而会让它看得不全、理解打折。

把源句喂进去,经过 \( N \) 层加工,编码器输出一组向量(每个源词对应一个),我们通常叫它 memory(记忆)。这组 memory 浓缩了对整句源文的理解,稍后供解码器反复"查阅"。你可以把 memory 想象成翻译官读完原文后,在脑海里留下的一份带注解的笔记:原文每个词都对应一条笔记,而每条笔记里不只有这个词本身,还融进了它和上下文的关系。这份笔记一旦写好,在翻译整句的过程中保持不变,译者写每个英文词时都可以翻回去查。

要强调的是,memory 里的每个向量早已不是孤立的词义了。经过 \( N \) 层双向自注意力的反复揉合,“猫"这个位置的向量里,已经掺进了"它是句子的主语"“它后面跟着的动作是抓老鼠"之类的语境信息。所以 memory 与其说是"原文的词”,不如说是"原文被彻底读懂之后的理解快照”。也正因如此,解码器查阅它时,拿到的不是干巴巴的词,而是一份已经消化好的、随时可用的理解。

3. 解码器(Decoder)的三件套

解码器的 Block 比编码器多一个子层。一个解码器 Block 依次包含三个子层:

  1. 带掩码的多头自注意力——让已生成的译文内部互相关注(但不能偷看未来,见第 4 节);
  2. Cross-Attention(交叉注意力)——回头去"查阅"编码器的 memory(见第 5 节);
  3. 前馈网络 FFN——和编码器里的一样。

(同样,每个子层都裹着残差 + LayerNorm。)

这里还藏着一个常被问到的细节:三个子层是严格按顺序串起来的,不是并排的。先让译文内部理顺自己(①),拿到一个"我现在大概想说什么"的状态;带着这个状态去查原文(②);最后把查回来的信息和原有状态一起交给 FFN 深加工(③)。顺序不能乱——如果还没理顺译文就去查原文,等于揣着模糊的问题去检索,问得不准答得也偏。这正像翻译官一定是先想清楚"这半句我要表达什么",再带着这个明确意图去回看原文,而不是漫无目的地乱翻。

为什么解码器要比编码器多这一层?因为它要同时盯着两份材料。翻译官落笔时,脑子里其实在并行处理两件事:一是"我已经写出来的英文通不通顺、主谓宾对不对",这靠的是①掩码自注意力,在译文内部回看;二是"我接下来要写的,对得上原文吗",这靠的是②cross-attention,回头查那份 memory 笔记。一个向内看自己写的,一个向外看原文,两路信息汇齐,再交给③FFN 做一轮加工,才决定下一个词。编码器只需"理解",所以两件套就够;解码器既要"自洽"又要"忠于原文",自然要三件套。

解码器 Block:
  输入(已生成的译文)
    ▼
  ① 掩码自注意力   ← 只能看自己和左边
    ▼
  ② Cross-Attention ← Q 来自这里,K/V 来自 Encoder memory
    ▼
  ③ FFN
    ▼
  输出

4. 因果掩码(Causal Mask)

这是解码器的命根子。生成是逐词进行的:写第 5 个词时,模型只应该看到前 4 个词,绝不能看到第 5 个及以后的词——否则就是"抄答案",训练出来的模型在真实生成时根本没有未来可看。

打个比方:这就像考试时不许翻看后面的题。训练阶段我们手里其实握着完整的标准答案(整句译文),为了高效,会把整句一次性喂进模型并行训练;但如果不加约束,模型在预测第 5 个词时就能"偷瞄"到正确的第 5 个词,等于开卷抄答案。它会养成依赖未来的坏习惯,可真到了考场(真实生成)上,后面的题目根本还是空白,它立刻露馅。因果掩码就是那位监考老师,死死盖住每个人面前还没轮到的题。

可是注意力天生会"环顾全场",怎么挡住未来?办法是掩码(mask):在 softmax 之前,把"当前位置看向未来位置"的那些分数,统统设成 \( -\infty \)。经过 softmax,\( e^{-\infty}=0 \),这些位置的权重就变成 0,等于被屏蔽了:

$$ A = \text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}} + M\right), \qquad M_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases} $$

为什么偏偏用 \( -\infty \) 而不是直接删掉那些位置?因为这样最"顺手"。softmax 的本质是先取指数 \( e^{x} \) 再归一化,而 \( e^{-\infty}=0 \)。给某个分数加上 \( -\infty \),它在指数后就变成 0,归一化时自然不占任何权重,且整套矩阵运算的形状、并行性一点都不用改——不需要为每个位置单独裁剪序列,只要叠加一个固定的掩码矩阵即可。这是工程上极其优雅的一招:用一次加法,实现了"假装看不见未来"。(实际代码里常用一个很大的负数,如 \(-10^9\),来近似 \( -\infty \)。)

掩码矩阵 \( M \) 是一个上三角为 \( -\infty \) 的形状(行=当前词,列=被看的词):

        词1   词2   词3   词4
 词1  [  0   -∞   -∞   -∞ ]   ← 词1 只能看词1
 词2  [  0    0   -∞   -∞ ]   ← 词2 能看词1,2
 词3  [  0    0    0   -∞ ]
 词4  [  0    0    0    0 ]   ← 词4 能看全部已生成

读这张表的窍门:每一行代表"我在写这个位置时,能看见谁"。第 1 行全是只许看自己,越往下能看的越多,到最后一行才看得见全部已生成的词——0 在下三角,\( -\infty \) 在上三角,正好画出一道"不许越界看未来"的台阶。这种"只能看左边"的注意力,叫因果注意力(causal attention) 或掩码自注意力。它是后面 GPT 那类生成模型的根基(下一篇详谈)。

5. Cross-Attention:解码器如何「查阅」源句

解码器 Block 的第二个子层 cross-attention,是连接两半的桥梁。它和普通自注意力的唯一区别在于 Q、K、V 的来源:

  • Q 来自解码器自己(当前正在生成的位置在"提问");
  • K、V 来自编码器的 memory(源句的表示在"应答")。

直觉上:解码器每生成一个词,都拿当前状态作为 Query,去源句里检索最相关的部分。翻译"猫"时,它会重点对齐到中文源句里"猫"那个位置的 memory。这正是第 2 篇"图书馆检索"的跨语言版——只不过这次,提问的是译文,被检索的是源文。

再回到翻译官的画面:cross-attention 就是译者写到一半,抬头回看原文的那个动作。他心里揣着"我现在要翻这个意思"(这就是 Q,问题来自他正在写的译文),然后目光扫过原文每个词的笔记(K,用来匹配哪条笔记最相关),最后把命中的那几条笔记的内容(V)取回来参考。所以 Q 必须来自解码器——是译文在发问;K、V 必须来自编码器 memory——是原文在作答。两边一旦接反,就变成"拿原文去问译文",方向全错。这也顺带说明:为什么 memory 在整个生成过程中是固定的——原文摆在桌上不会变,变的只是译者每次抬头时带着什么问题去看它。

还有一个常被忽略的好处:cross-attention 天然实现了对齐(alignment)。早年的统计机器翻译要专门训练一个"对齐模型",费力地猜测源语言第几个词对应目标语言第几个词;而在 Transformer 里,这件事被 cross-attention 顺手做掉了。当解码器写到英文"cat"时,它的注意力权重会自动在中文"猫"那一列上变高——把这些权重画成热力图,你能清晰看到一条源词与译词之间的对应脉络。换句话说,模型并没有人教它"猫=cat",但为了把译文写对,它在 cross-attention 里自己学出了这种对应关系。这也是为什么这一子层对翻译质量如此关键:它是译文与原文之间唯一的、可学习的信息通道。

自注意力Cross-Attention
Q 来自当前序列解码器(当前序列)
K, V 来自当前序列编码器 memory
作用序列内部信息融合让译文对齐、参考源句

6. 输出层:线性 + softmax

解码器最顶层输出每个位置的向量后,要把它变成"下一个词是什么"的预测:过一个线性层映射到词表大小的维度,再 softmax 成一个覆盖整个词表的概率分布

$$ p(\text{下一个词}) = \text{softmax}(h_{\text{top}}\, W_{\text{vocab}}) $$

这一步可以理解成翻译官最终落笔前的权衡:脑子里那个抽象的"下一个词的感觉"(顶层向量),要兑现成词表里一个具体的词。线性层先给词表里每一个候选词打一个分,softmax 再把这些分数压成一组加起来等于 1 的概率,比如"cat"占 70%、“dog"占 5%……。模型并不直接吐一个词,而是给出整张词表上的概率分布,至于最后挑哪个,那是采样策略的事(留到第 9 篇)。

为什么要输出"分布"而不是直接给一个词?因为语言天然有多种合理写法。同一句中文,译成"the cat"还是"a cat"往往都说得通,模型把这种不确定性如实表达成概率,比硬性二选一更诚实,也给了后续生成策略调节"保守还是发散"的空间。这也呼应了前面反复出现的主题:Transformer 的每一步,几乎都在用"软性的、可微的权重"代替"硬性的、非黑即白的选择”——注意力如此,输出层亦如此。

一个常见技巧叫权重绑定(weight tying):让这个输出投影矩阵和最底层的词嵌入矩阵共享同一份参数。既省参数,效果通常还更好。

7. 完整数据流回顾

把前面几节串成一条端到端的链路:

源句 → 词嵌入(+位置编码) → Encoder×N(双向自注意力) → memory
                                                          │
                                                          ▼ (K,V)
已生成译文 → 词嵌入(+位置编码) → Decoder×N:
                                  ① 掩码自注意力(只看左边)
                                  ② cross-attention(Q×memory)──┘
                                  ③ FFN
                              → 线性 + softmax → 下一个词的概率分布

生成时,这个过程自回归地反复进行:产出一个词,就把它接到译文末尾,再跑一遍解码器产出下一个,直到输出"结束符"。(这个生成循环和它的效率问题,是第 9、10 篇的主题。)

这里有个值得记住的"不对称":编码器只跑一次,解码器要跑很多次。源句一旦读完,memory 就固定下来,不必重算;而译文每多写一个词,解码器就要重新前向一遍。这正像翻译官读原文只通读一遍、心里有了底,接下来落笔却是字斟句酌、反复回看的漫长过程。理解这个不对称,也就明白了为什么后续讨论推理效率时,优化的重点几乎都压在解码器这一侧——它才是真正被反复执行、决定生成快慢的瓶颈。

常见疑问与易错点

问:memory 到底是什么?是一个向量吗? 答:不是一个,而是一组。编码器对源句里每个词都输出一个向量,这组向量合起来就是 memory,长度和源句词数一样。可以把它想成"原文每个词的带注解笔记"的集合。它在解码生成的全过程中保持不变,被每一步 cross-attention 反复查阅。

问:为什么解码器要两种注意力,编码器只要一种? 答:因为解码器要同时满足两个要求。①掩码自注意力让它回看自己已经写出的译文,保证译文内部连贯、不偷看未来;②cross-attention 让它回看原文 memory,保证忠于源句。编码器只需"理解一句已知的输入",所以一种自注意力就够了。

问:因果掩码只在训练时需要,推理(生成)时还要吗? 答:概念上始终需要"不能看未来"。训练时我们把整句答案并行喂入,必须靠掩码挡住未来;推理时是一个词一个词地生成,未来本来就还不存在,看似不需要掩码。但在工程实现上,为了让训练和推理走同一套代码、形状一致,通常仍然挂着掩码(它对"未来为空"的情况不产生任何副作用)。所以记住:约束一直在,只是推理时未来天然为空

问:cross-attention 里 Q 和 K/V 接反了会怎样? 答:方向就彻底错了。正确是"译文(Q)去问原文(K/V)"。如果接成"原文去问译文",相当于让还没读懂的译文当答案库,模型既无法对齐源句,也学不到翻译关系。记法:谁在生成谁就出 Q,被参考的源句出 K/V

问:这套 Encoder-Decoder 和 GPT 有什么区别? 答:GPT 只保留了解码器那一半,而且因为没有独立的源句要对齐,它砍掉了 cross-attention,只留下掩码自注意力——也就是"看着自己已经写的,接着往下写"。它没有编码器、没有 memory。完整 Encoder-Decoder 适合有明确"输入→输出"映射的任务(翻译、摘要);GPT 这类纯解码器更适合开放式续写。下一篇会专门讲这三大流派的分野。

问:编码器双向、解码器单向,这个"方向"指的是什么? 答:指注意力允许看的范围。双向=每个词能看到序列里左右所有词(适合理解一句完整输入);单向(因果)=每个词只能看到自己和左边的词(适合从左往右逐词生成)。它不是数据流动的方向,而是"谁能注意到谁"的可见范围。

问:训练时整句答案一次喂入,和推理时逐词生成,模型结构一样吗? 答:结构完全一样,变的只是"喂进去多少"。训练时为了高效,把整句目标译文一次性塞进解码器并行算,靠因果掩码保证每个位置只看左边——这叫 teacher forcing(用标准答案当已生成内容)。推理时没有标准答案,只能真的一个词一个词生成,把刚产出的词接回末尾再跑一遍。所以训练像"开着答案做整套卷子但盖住后面",推理像"闭卷一题一题往下答",用的是同一张卷子、同一套规则。

问:为什么编码器不需要掩码,解码器却离不开? 答:因为两者面对的"信息完整度"不同。编码器拿到的是一句完整、静态的源句,没有"未来"可言,看全才理解得准,加掩码反而有害;解码器是在动态生成,后面的词此刻尚不存在,若训练时让它偷看完整答案,它就学会依赖未来,真实生成时立刻崩。掩码的存在,本质是为了让训练阶段如实模拟"未来还没发生"这个生成时的真实处境。

小结 & 下一篇预告

这一篇,我们用 Block 搭出了原版完整架构:

  • 编码器:双向自注意力,把源句压成 memory;
  • 解码器:三件套——掩码自注意力 + cross-attention + FFN;
  • 因果掩码:把"看向未来"的分数置 \( -\infty \),保证生成时不偷看答案;
  • cross-attention:Q 来自解码器、K/V 来自编码器 memory,实现译文对源句的对齐;
  • 顶部 线性 + softmax 输出词表概率,常配合权重绑定。

有趣的是,今天的主流大模型,大多并不用这个完整的 Encoder-Decoder,而是只取其中一半。GPT 只要解码器,BERT 只要编码器——这是怎么回事?各自又适合什么任务?下一篇,我们就来理清 Transformer 的三大流派


📖 系列目录

第 1 篇 文末完整目录。