第 6 课那张注意力表已经能让每个字「环顾全班」了,可它还有两个大问题。 第一,一张表只能表达一种「看谁」的方式,而语言里同时存在好几种关系; 第二,我们的任务是「猜下一个字」——训练时如果第 3 个字能直接看到第 7 个字, 它根本不用学,抄答案就行了。 这一课补上这两块:多头和因果掩码。
先玩这块挡板。下面是一句话 6 个字排成的 6 × 6 表:行 = 谁在问,列 = 谁能被看到。 右上那半个三角被一块灰板盖着 —— 那就是「未来」。 点任意格子看「第 i 个字能不能看到第 j 个字」,再点「掀掉挡板看看」。
每个考生只能看自己左边的卷子(前文),不能看右边(未来)。 这就是因果掩码。真实代码里它只有一行:
注意为什么是 −∞ 而不是 0:softmax 第一步是取指数,e⁰ = 1 照样能分到一份百分比; 只有 e^(−∞) = 0 才是真正的「看不到」。这一条面试很爱问。
第 6 课那张表长这样:第 i 行是「第 i 个字怎么分配它的 100%」。 它只能装下一种分配方案。
可同一句话里,关系从来不止一种。「三加五等于?」这 6 个字里, 至少同时存在着三件事:谁挨着谁(顺序)、数字该找哪个运算符(语法)、答案位该凑哪两个操作数(语义)。
做法就是「拆开算、拼回来」,一步都不新:
d → H × d_h自己的 Wq/Wk/Wvconcat → WoWo 把所有头的结论混一次。打开「因果掩码」开关,你会看到每张小表右上角都被挖掉一块 ——挡板对每个头一视同仁:不管这个头在看什么,它都不许看未来。
页首动画里那块挡板,装的位置只有一个正确答案:softmax 之前。这件事看起来像细节,其实是「掩码到底有没有用」的分水岭。
两种装法都试一遍:同一行分数,一种先设 −∞ 再 softmax, 一种先 softmax 再清零。
看清楚差别:掩码不是「把某些格子调小」,而是「把这几个格子从这张表里删掉, 然后把 100% 重新分给剩下的」。只有 −∞ 能做到这一点,因为 e^(−∞) = 0。
回到页首那个「掀掉挡板」的开关。如果训练时真的不遮,会发生什么? 看看每个位置会分给「未来」多少注意力:
到这儿零件齐了。把第 6 课那五行加上这两块,就是真实模型里的一层注意力:
形状一路走一遍:x [6, 8] → 每头 Q/K/V [6, 2] → 每头权重 [6, 6] → 每头输出 [6, 2] → 拼接 [6, 8] → Wo → [6, 8]。进出的形状一模一样,所以它能一层一层叠起来。
到这里,注意力的全部零件就到手了:Q/K/V(第 6 课)+ 多头(这一课)+ 掩码(这一课)。 但它现在还是一个「孤零零的机制」——下一课我们要给它配上残差、 LayerNorm 和 FFN,把它拼成一个能真正堆很多层的完整模块。
第 6 课那张注意力表只能装下一种关系,而语言里同时存在好几种 —— 于是把宽度拆成 H 份,每个头用自己的 Wq/Wk/Wv 各算一张表, 拼回 [T, d] 再乘 Wo。头数变多只是「每头变窄」,总宽度、总参数(恒 3d² + d²)、总计算量(恒 T×T×d)一个都不变。 另一件事更硬:我们的任务是猜下一个字,所以第 i 个字不许看到未来—— 把 j > i 的格子分数设成 −∞(不是 0,e⁰ = 1 照样分到百分比), 而且必须装在 softmax 之前:掩码不是「少看一点」, 而是把 100% 重新分给能看的那几个。少了这块挡板, 模型会学会抄答案 —— 训练 loss 漂亮得可疑,一生成就崩。