我们来用最真实的计算过程拆解下三角掩码矩阵(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。 它在训练时防止作弊,并确保推理时模型行为的一致性(只能依赖前文)。