掩码注意力

要逐词生成句子,就不能让模型偷看答案,一张掩码矩阵解决因果性。

知识课 · 约 25 分钟 · 更新于 2026-08-14

会写的那一半:解码器

编码器负责「读」,而负责「写」的另一半模型叫 解码器(Decoder) , 它逐词生成输出句。本课和「交叉注意力与解码器」两课,会把它一步步搭出来。

先看清「写」是什么形态。以翻译为例(把 I love you 译成「我爱你」), 输出句不是一次性生成的,而是 逐词生成 。 下面的小演示把两种情形摆在一起:左边的「逐词生成」是真实的生成方式, 右边的「整句并行」是训练时图省事的做法,切换着看就能看出问题所在。

左边每个词都基于 已经生成的词 来产生,这种「自己产出、再喂给自己」的生成方式称为 自回归(autoregressive) 。 右边一旦放开限制,靠前的位置就会看到它本该预测的后续词,这正是下一节要解决的核心矛盾。

训练的难题:想并行,又怕偷看

生成是一步一步来的,那训练也要一步一停吗?一个长句几十个词,就要前向几十次,太慢了。 理想的方案是:把整个目标句「我 爱 你」 一次喂入、一次前向算完所有位置 ,每个位置都同时学习「预测下一个词」。

于是需求清晰了: 因果性约束 ,无论训练还是生成,计算第 i 个位置时,只允许依赖位置 1…i(含自身)的信息,绝不能看到 i 之后的内容。 既要整句并行,又要「看不到未来」,这就是本课要解决的问题。

「预测下一个词只依赖前面的词」与这里「可以看到自己」的下标约定如何对上? 到「训练与推理通路」讲训练输入时自会清楚。

一张掩码矩阵解决

手法在「编码器」屏蔽 padding 时已经见过: softmax 前置 −∞ 。这次屏蔽的对象换成「未来位置」。构造一张 L×L 的 掩码矩阵 M:

把它加到注意力分数上,其余一切照旧:

softmax 会把 的分数变成权重 0,每个位置只能注意自己和更早的位置。这就是 掩码自注意力(masked self-attention) 。因为屏蔽的是「未来」,这张 M 也叫 因果掩码(causal mask) 。

关键收益:因果性有了, 并行一点没丢 ,整句仍然是一次矩阵运算, 只是分数表的右上角被盖住了而已。

实现因果掩码

M 把「未来位置」标成 、其余标成 0,它的非零区落在主对角线以上, 是一个上三角的形状。代码里换一种等价、更省事的写法:先造一张 0/1 掩码 ( 1 = 放行, 0 = 屏蔽), 放行的位置恰好是主对角线 及其以下 的下三角,用 torch.tril 取下三角一行就能得到;再把它交给带 mask 参数的注意力函数,由函数在 softmax 之前把 0 的位置压成 。下面一步步看这行代码怎么造出这张掩码:

掩码后的注意力权重矩阵是严格的 下三角 :第 1 行只有自己,第 2 行能看前两个词……一行比一行「视野」宽。

下面这张热力图把这件事画了出来。以句子「 」为例, 每一行是一个 正在计算的位置 ,每一列是它 注意到的位置 , 格子的明暗代表该位置分给对应词的注意力权重(每一行的权重之和为 1)。 用上方开关在「不加掩码」与「加因果掩码」之间切换,对照着看右上角发生了什么。

  • 横读一行 = 位置 i 在计算自己的输出时,把注意力分给了各个词,权重之和为 1。
  • 竖读一列 = 位置 j 这个词被各个位置注意到的程度。
  • 格子越亮 = 权重越大;最亮处是该行注意力最集中的词。
  • 「加因果掩码」后,每行里排在自己之后的「未来」格子被整体屏蔽(标为「屏蔽」、权重归零),剩下的有效权重恰好是一个下三角。

代码级证明:真的看不到未来

口说无凭,验证一下。固定前 2 个词, 随意改动第 3 个词之后的内容 ,看前面位置的输出会不会变。下面一步步跟着代码看这个对照实验:

后面的词怎么换,前面位置的输出 一个比特都不变 , 「看不到未来」得到了代码级的证明。训练时整句并行、又绝无偷看,两全其美。

两种掩码,一个手法

到目前为止我们见过两种掩码,机制完全相同,只是屏蔽的位置不同:

都是「softmax 前置 −∞」。实际使用中两者还可以 叠加 : 既屏蔽 padding,又屏蔽未来。

还缺一条线

现在解码器能靠掩码自注意力「回顾自己已经写出的词」了。但翻译时光回顾自己可不够, 写每个词都得 盯着原文 :译到第二个词时,该对齐原文的哪个词?

解码器怎么去「看」编码器的输出?之后的《交叉注意力与解码器》会补上这条线, Transformer 全图就齐了。