我们来用最真实的计算过程拆解下三角掩码矩阵(Look-Ahead Mask)到底做了什么,如何确保预测时模型只能看前面的词,不能看自己和未来的词 。我们用生成句子 "我爱吃苹果"
中第三个词 吃
的位置 (假设位置索引是 2
) 作为例子。
🔍 场景设定
-
序列状态(训练阶段):
假设模型正在训练,我们喂给它完整的正确目标序列:
-
位置索引:
0: <SOS>
,1: 我
,2: 吃
,3: 苹果
,4: <EOS>
-
向量: 每个位置都有一个
d_model
维的向量(比如d_model=3
,简化计算):x0 = [0.1, 0.2, 0.3]
//<SOS>
x1 = [0.4, 0.5, 0.6]
//我
x2 = [0.7, 0.8, 0.9]
//吃
(当前关注的位置!)x3 = [1.0, 1.1, 1.2]
//苹果
(未来的词,模型预测吃
时不该看!)x4 = [1.3, 1.4, 1.5]
//<EOS>
(未来的词,模型预测吃
时不该看!)
-
输入张量:
X = [x0, x1, x2, x3, x4]
,形状[5, 3]
-
-
目标:
模型需要根据
<SOS>
和我
预测出吃
。但在并行训练时,x
向量里含有所有位置的信息(包括未来的苹果
和<EOS>
)。这会导致模型"作弊",直接用未来的信息来预测吃
,而不是真正学习到我
后面应该接吃
的规律。下三角掩码就是要阻止这种作弊!
🛡️ 下三角掩码矩阵 (mask
) 的作用和计算
1. 构造 Look-Ahead Mask 矩阵(尺寸 [5, 5]
):
规则:位置 i
可以看位置 j
,当且仅当 j <= i
(即只能看前面和自身,通常也要掩码自身)。
具体值:允许看的位置设为 0
(不影响分数),禁止看的位置设为 -∞
(或一个极大的负数如 -1e9
),让 Softmax 后概率为 0。
ini
// j=0 j=1 j=2 j=3 j=4 <-- Key 位置 (允许看哪些位置?)
// i=0 (<SOS>) [ 0 , -∞ , -∞ , -∞ , -∞ ] // 位置0只能看j=0
// i=1 (我) [ 0 , 0 , -∞ , -∞ , -∞ ] // 位置1可以看j=0,1
// i=2 (吃) [ 0 , 0 , 0 , -∞ , -∞ ] // 位置2可以看j=0,1,2 (目标!)
// i=3 (苹果) [ 0 , 0 , 0 , 0 , -∞ ] // 位置3可以看j=0,1,2,3
// i=4 (<EOS>) [ 0 , 0 , 0 , 0 , 0 ] // 位置4可以看所有
重点看 i=2
(吃 所在行):
- 允许看
j=0 (<SOS>)
,j=1 (我)
,j=2 (吃)
。✅ - 禁止看
j=3 (苹果)
,j=4 (<EOS>)
(设为-∞
)。❌
2. 计算 Query (Q
), Key (K
), Value (V
):
假设我们已计算好 Q
, K
, V
(具体参数不重要,关注数值变化):
Q = X * W_q
(形状[5, 3]
,假设W_q
是参数矩阵)K = X * W_k
(形状[5, 3]
)V = X * W_k
(形状[5, 3]
)
为简化,我们只看位置i=2
(吃) 的向量:q2 = [0.5, 0.6, 0.7]
//吃
位置的 Query 向量K = [ [0.2, 0.3, 0.4], // j=0 (<SOS>) [0.5, 0.6, 0.7], // j=1 (我) [0.8, 0.9, 1.0], // j=2 (吃) [1.1, 1.2, 1.3], // j=3 (苹果) [1.4, 1.5, 1.6] // j=4 (<EOS>) ]
// 形状[5, 3]
3. 计算相似度分数 scores
(q2
与 K
中每一个 kj
的点积):
scores = q2 · K^T
(点积计算相似度)
具体计算:
less
score_j0 = [0.5, 0.6, 0.7] · [0.2, 0.3, 0.4] = 0.5 * 0.2 + 0.6 * 0.3 + 0.7 * 0.4 = 0.1 + 0.18 + 0.28 = 0.56
score_j1 = [0.5, 0.6, 0.7] · [0.5, 0.6, 0.7] = 0.5 * 0.5 + 0.6 * 0.6 + 0.7 * 0.7 = 0.25 + 0.36 + 0.49 = 1.10
score_j2 = [0.5, 0.6, 0.7] · [0.8, 0.9, 1.0] = 0.5 * 0.8 + 0.6 * 0.9 + 0.7 * 1.0 = 0.40 + 0.54 + 0.70 = 1.64
score_j3 = [0.5, 0.6, 0.7] · [1.1, 1.2, 1.3] = 0.5 * 1.1 + 0.6 * 1.2 + 0.7 * 1.3 = 0.55 + 0.72 + 0.91 = 2.18 // 和未来词相似度很高!
score_j4 = [0.5, 0.6, 0.7] · [1.4, 1.5, 1.6] = 0.5 * 1.4 + 0.6 * 1.5 + 0.7 * 1.6 = 0.70 + 0.90 + 1.12 = 2.72 // 和<EOS>相似度也高!
计算得到初始 scores = [0.56, 1.10, 1.64, 2.18, 2.72]
大问题: 吃 (i=2)
和未来的 苹果 (j=3)
和 <EOS> (j=4)
的分数最高!如果直接用,它会大量参考未来信息,这是严重作弊!
4. 应用 Look-Ahead Mask (mask
)!
回想 i=2
(吃) 对应的掩码行:[0, 0, 0, -∞, -∞]
我们将这个掩码 加(+
)到 scores
上(通常除以 √d_k
后进行,这里简化略过除法):
ini
// 掩码行(对应于 i=2): [0, 0, 0, -1e9, -1e9]
// 原始 scores: [0.56, 1.10, 1.64, 2.18, 2.72]
// 相加(masked_scores): [0.56+0, 1.10+0, 1.64+0, 2.18 + (-1e9), 2.72 + (-1e9)]
= [0.56, 1.10, 1.64, ≈ -1000000000, ≈ -1000000000]
关键效果: j=3
(苹果) 和 j=4
() 的分数被压成了极负值(≈-1e9) ,而允许看的 j=0, 1, 2
的分数保持不变!
5. 对 masked_scores
做 Softmax:
Softmax 对所有位置的值做指数归一化。极大负数的指数 ≈ 0。
ini
// masked_scores = [0.56, 1.10, 1.64, -1e9, -1e9]
exp(0.56) ≈ 1.75
exp(1.10) ≈ 3.00
exp(1.64) ≈ 5.16
exp(-1e9) ≈ 0.0
exp(-1e9) ≈ 0.0
// 总和 ≈ 1.75 + 3.00 + 5.16 + 0 + 0 = 9.91
// Softmax 概率:
prob_j0 = 1.75 / 9.91 ≈ 0.18
prob_j1 = 3.00 / 9.91 ≈ 0.30
prob_j2 = 5.16 / 9.91 ≈ 0.52 // 它对自己("吃")关注度最高(在没有未来信息干扰下)
prob_j3 = 0.0
prob_j4 = 0.0
最终注意力权重:attn_weights = [0.18, 0.30, 0.52, 0.0, 0.0]
6. 加权求和得到位置 i=2
(吃)的新表示:
z2 = attn_weights * V = 0.18 * V[j0] + 0.30 * V[j1] + 0.52 * V[j2] + 0.0 * V[j3] + 0.0 * V[j4]
因为 prob_j3 = 0.0
, prob_j4 = 0.0
,所以 V[j3]
(苹果
) 和 V[j4]
(<EOS>
) 完全没贡献!
z2
只融合了 j=0 (<SOS>), j=1 (我), j=2 (吃)
的信息!
✅ 总结:下三角掩码矩阵到底做了什么?
- 物理位置: 在计算位置
i
的 Self-Attention 时,输入包含整个序列所有位置(包括未来的位置)的向量(因为训练是批量的)。 - 作弊风险: 位置
i
可以轻易计算出和未来位置j>i
的高相似度(分数)。 - 掩码介入: Look-Ahead Mask(下三角阵)在计算
Softmax
之前,将位置i
对应的行中j>i
(未来)的分数加了一个极大的负值(≈-1e9)。 - Softmax效果: 加上掩码后,
j>i
位置的分数经过Softmax
后概率 ≈0.0 ,它们对应的 Value(Vj
)在加权求和时贡献为0。 - 最终结果: 位置
i
的新向量zi
仅由位置0
到i
的信息(通常也掩码自身,j=i
被屏蔽,使其不关注自己)融合而来,完全屏蔽了未来信息。
📌 一句话核心
Look-Ahead Mask(下三角掩码矩阵)通过在计算注意力权重时,把未来位置的分数强制设为极负值(≈-1e9),从而保证位置 i
在 Softmax 后,权重只能分配给位置 0
到 i
(自身通常也被掩码),无法分配给未来的位置 j>i
。 它在训练时防止作弊,并确保推理时模型行为的一致性(只能依赖前文)。