一、一个自注意力默认允许、但语言模型不能接受的漏洞

把第 1 讲和第 9 讲放在一起看,会发现一个矛盾。

第 1 讲的任务是 P(xₜ₊₁ | x₁...xₜ)——只用前面的词,预测下一个词。

第 9 讲讲的自注意力,默认是没有方向限制的:位置 i 的 Query 会去和序列里所有位置的 Key 做点积打分,包括排在 i 后面的位置。

⭐ 把这两件事放在一起:如果用第 9 讲原始的自注意力去做第 1 讲的任务,预测第 5 个词的时候,模型的注意力机制会直接看到第 5 个词本身(以及它后面的词)参与计算。训练时,这等于让模型"抄答案"——它不需要学会预测,只需要学会"把 Query 打在正确答案那个位置上,直接把答案的 Value 拿过来";推理时,这更是不可能发生的事,因为第 6、7、8 个词还没有被生成出来,根本不存在,无从"看"起。

这一讲要把这个漏洞堵上,并且在这个过程中,把 Transformer 家族的几种主流架构一次性理清楚。

二、打个比方:写小说 vs 校对小说

一个作者在写小说的第 200 页时,只能依赖已经写下的前 199 页——第 201 页写什么,此刻根本不存在,作者没法"参考"一个还不存在的东西。这是一种天生单向的创作过程。

一个编辑在校对第 200 页的措辞是否合适时,情况完全不同——全书已经写完,编辑可以前后随意参照,甚至可以说"这里要和第 350 页的一个伏笔呼应,所以这个词要这么改"。这是一种天生双向的理解过程。

⭐ 语言模型的生成任务,结构上就是"作者写作",不是"编辑校对"——必须是单向的。第 9 讲的自注意力默认是"编辑校对"模式(双向),这一讲要做的,就是把它改造成"作者写作"模式(单向)。

三、把直觉钉成机制

因果掩码:禁止看向未来

改造的办法极其直接:在 softmax 之前,把"位置 i 看向位置 j(其中 j 排在 i 后面)“的打分,强行设成负无穷:

score(i, j) = Qᵢ·Kⱼ / √dₖ        如果 j ≤ i(允许看,j 是过去或自己)
score(i, j) = −∞                如果 j > i(禁止看,j 是未来)

负无穷经过 softmax 之后,exp(−∞) = 0——这些位置的权重被精确地压成 0,不管 Q、K 算出来的原始匹配程度是多少,模型在数学上就是"看不到"那些位置,不是"不愿意看”,是根本拿不到任何非零权重。这套做法叫因果掩码(causal mask),也叫前瞻掩码(look-ahead mask)。

接着第 9 讲的例子往下算

用第 9 讲手算过的例子直接验证。x₁=[1,0],x₂=[0,1],Wq=Wk=单位矩阵,Wv=2×单位矩阵,算出的 Q、K、V 和第 9 讲完全一样。

位置 1 是序列里的第一个词,它后面只有位置 2 是"未来"。加上因果掩码:

score(1,1) = 0.707(不变,可以看自己)
score(1,2) = −∞(禁止看位置 2,因为它排在位置 1 后面)

softmax([0.707, −∞]) = [1, 0]    ← 位置 2 的权重被精确压成 0

输出₁(因果版)= 1×V₁ + 0×V₂ = [2, 0]

对比第 9 讲没有掩码时算出的 输出₁=[1.340, 0.660]——那个结果混入了 33% 的位置 2 信息,在语言模型的场景下,这 33% 就是在偷看还不存在的未来,是必须被禁止的。加上因果掩码后,位置 1 的输出变成了 [2,0],正好等于它自己的 V₁,一点没有偷看到位置 2。

位置 2 是序列里的最后一个词,它后面没有任何位置,所有允许看的位置(1 和 2)都在掩码规则里保留,所以计算完全不受影响:

输出₂(因果版)= 0.330×V₁ + 0.670×V₂ = [0.660, 1.340]   ← 和第 9 讲的结果完全一样

⭐ 这个小例子精确地展示了因果掩码在做什么:排在后面的位置,能看到的信息不受影响;排在前面的位置,凡是原本会参考"未来"的部分,全部被清零。

三种宏观架构

有了"要不要加掩码"这个开关,第 10 讲拼好的 Transformer 模块,可以组织成三种不同的宏观架构:

Encoder(编码器,双向,不加掩码):每个位置能看到完整的上下文(前后都看),对应"编辑校对"模式。适合不需要逐词生成、只需要给整段输入生成一个"理解"表示的任务(比如判断一句话的情感、抽取里面的关键信息)。

Decoder-only(纯解码器,全程加因果掩码):每个位置只能看自己和之前的位置,对应"作者写作"模式,直接对应第 1 讲 P(xₜ₊₁|x₁...xₜ) 这个任务本身。这门课后面所有讲义默认讨论的,都是这种架构——今天绝大多数生成式大模型都是 Decoder-only。

Encoder-Decoder(编码器-解码器,两者都要):最早的 Transformer 论文用的就是这种架构,为翻译这类"把一段话转换成另一段话"的任务设计。一个 Encoder 栈双向处理输入的源语言句子(作者已经知道整句"你好"要翻译,可以双向理解它),一个 Decoder 栈单向逐词生成目标语言句子(生成"Hello"的第二个词时,还不知道后面会写什么,必须单向)。

Encoder-Decoder 架构里还多了一种新的注意力模式:交叉注意力(cross-attention)——Decoder 的每个位置,除了对自己之前的位置做因果自注意力,还会额外做一次注意力,Query 来自 Decoder 当前位置,Key 和 Value 来自 Encoder 的全部输出:

CrossAttention(Q, K, V) = softmax(QKᵀ/√dₖ)·V
    其中:Q 来自 Decoder,K、V 来自 Encoder

公式和第 9 讲的自注意力完全一样,唯一的区别是 Q 和 K、V 不再来自同一个序列——因为源语言整句话是已知、固定、双向可见的(不像 Decoder 自己生成的目标句子那样"还没写完"),交叉注意力不需要加因果掩码,Decoder 的每个位置都可以看到源句子的全部内容。

四、三种架构各自的代价

⭐ 关键的权衡在这里:Decoder-only 的每一个位置,天生比同一个位置的 Encoder 表示"信息更少"——同一个词,Encoder 能利用它前后两个方向的全部上下文,Decoder-only 因为因果掩码,只能利用它前面的部分。这不是 Decoder-only 的设计缺陷,是自回归生成任务本身必须付出的代价:你没法在生成一个词的时候,参考一个还不存在的未来。

Encoder-Decoder 理论上能吃到"双向理解源句 + 单向生成目标句"两头的好处,但代价是两套独立的 Transformer 栈——参数量、计算量基本翻倍,训练和推理的流程也更复杂(需要先完整跑一遍 Encoder,再一步步跑 Decoder,还要维护交叉注意力这第三种计算模式)。

⭐ 那么问题来了:如果 Encoder-Decoder 理论上更强,为什么今天几乎所有主流的生成式大模型都是纯 Decoder-only? 答案不在"哪个架构表达能力更强",而在训练目标能不能被统一:

回忆第 4 讲的链式法则——任何"输入 → 输出"的任务,只要把输入和输出拼接成一整段文本,都可以被写成一串"预测下一个词"的问题,不需要单独设计一个"理解输入"的模块和一个"生成输出"的模块。比如把翻译任务写成:

"把这句话翻译成英文:你好。答案是:Hello"

整段话直接喂给 Decoder-only 模型,用完全相同的"预测下一个词"目标训练(只在"答案是:“之后的部分计算损失),不需要交叉注意力,不需要两套堆栈,一套统一的架构、一个统一的训练目标,就能覆盖 Encoder-Decoder 原本需要专门设计才能做的任务。这套"把任何任务改写成一段文本,交给统一的自回归模型"的思路,第 13 讲会正式讲清楚,第 16 讲讲 SFT 时你会看到它变成实际可操作的数据格式。

五、和后面课程的关系

  • **第 13 讲(预训练目标)**会把这一讲第四节最后提到的洞察正式讲透:为什么"把任务写成一段文本,只用统一的下一词预测训练"这件事,是 Decoder-only 架构能够一统天下的根本原因。
  • **第 16 讲(SFT)**里,“指令微调"用的训练数据格式——把指令和期望回答拼接成一整段文本,只在回答部分算损失——正是这一讲讲的思路在实践中的具体样子。
  • 这门课从这一讲开始,后面默认讨论的架构都是 Decoder-only:第 12 讲的 MoE 改造的是 Decoder-only 模型里的前馈层,第 21–24 讲的推理优化,都是围绕自回归、逐词生成这个过程展开的。

六、本讲小结

  • 自注意力默认没有方向限制,但语言模型的生成任务要求单向——预测下一个词时,看到"未来"的词等于抄答案(训练时)或者根本不可能(推理时)。
  • ⭐ 因果掩码:把 j>i 的打分设成负无穷,softmax 之后这些位置的权重精确变成 0——本讲用第 9 讲的例子具体验证了这一点:位置 1 加掩码后的输出从"混入 33% 未来信息"变成"精确等于自己的 V”。
  • 三种宏观架构:Encoder(双向,理解)、Decoder-only(单向,生成,对应第 1 讲任务本身)、Encoder-Decoder(两者都要,多一种交叉注意力,Q 来自 Decoder、K/V 来自 Encoder,不需要掩码)。
  • ⭐ Decoder-only 每个位置天生比 Encoder 信息更少,这是自回归生成必须付出的代价;但它能靠"把任何任务拼接成一段文本、统一用下一词预测训练"这个思路,省掉 Encoder-Decoder 需要的第二套堆栈和交叉注意力,这是今天生成式大模型几乎全部收敛到 Decoder-only 的根本原因。

思考题

  1. 用因果掩码重新审视第 9 讲"苹果公司/吃苹果"的例子:如果"苹果"是句子里的第一个词,后面才出现"公司"或"吃”,因果掩码会不会让"苹果"这个位置的表示完全没法参考后面的词?这对第 9 讲说的"一词多义被自注意力解决"这句话,是不是需要打一个补丁?
  2. 在本讲第三节的手算例子里,如果序列有 3 个位置,位置 2 的因果掩码会挡住哪个位置?位置 3 呢?
  3. 交叉注意力的 K、V 来自 Encoder,不需要因果掩码——请解释为什么源句子(Encoder 的输入)不存在"偷看未来"这个问题(提示:源句子在生成目标句子之前,是不是已经完整存在了)?
  4. 把一个翻译任务改写成"把这句话翻译成英文:你好。答案是:Hello"这种拼接格式,喂给 Decoder-only 模型训练——如果只在"答案是:“之后的部分计算损失,前半部分(问题描述)不计算损失,这样做对模型的训练目标有什么影响?如果连前半部分也一起计算损失会有什么问题?
  5. 结合第四节,Encoder 和 Decoder-only 各自的代价是什么?如果一个任务只需要"给一整段话打一个分类标签”(不需要逐词生成),你觉得用纯 Encoder 还是纯 Decoder-only 更合适,为什么?