训练时怎么防止模型偷看答案?
本课一问:训练时怎么防止模型偷看答案?
这一课先不碰:权重拿去乘什么(第 5 课);生成时怎么避免重复计算(第 8 课)。
从已有出发
前三课里,「给了」一直在看「小王」。现在回头想想它是在什么场景下看的。
训练一个语言模型,目标是「预测下一个 token」。一句话「小明把苹果给了小王」其实是 4 道题:看到「小明」预测「把」,看到「小明把」预测「苹果」……看到「小明把苹果给了」预测「小王」。为了快,训练时整句一次喂进模型,每个位置同时输出一个预测,4 道题的 loss 一起算。这就带来一个问题:第 i 个位置在做题时,第 i+1 个 token(答案)也在输入里。
先自己设计:什么也不做
先按前三课的样子,让每个 token 都能看全句。每一行第 i+1 列标了绿框:那是这一行要预测的答案。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
为什么 loss 会「低得虚假」。不加掩码时,位置 i 的输出可以直接由第 i+1 个 token 的 V 构成,而第 i+1 个 token 正是它的标签。存在一个几乎不用理解语言的解:把右边的 token 原样搬过来。这条捷径在训练集和验证集上都有效(验证时同样整句喂入、同样不加掩码),所以两条 loss 曲线都会好看;只有真正去生成时——第 i+1 个 token 还没生出来——它才暴露。项目里做了这个对照实验:同配置开、关掩码各训一次。看下面三处:关掉掩码那一组的 loss 曲线降到了哪里;同一组权重在评估时把掩码加回去(相当于生成时的处境),loss 又回到哪里,和「瞎猜」的 ln(词表大小)比一比;两组生成的文本各是什么样。
改进:因果掩码
做法只有一步:在 softmax 之前,给分数矩阵加一个掩码矩阵 M——左下三角(含对角线)是 0,右上三角是 −∞。e−∞ = 0,所以 softmax 之后,每一行在自己右边的权重恰好是 0,左边的权重仍然和为 1 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条e^(−∞) = 0;归一化只在可见位置之间进行。于是位置 i 的输出与它右边的任何 token 无关 出处Vaswani et al. 2017 §3.2.3 ↗ 。
它带来一个很强的保证:位置 i 的输出只依赖位置 0…i 的 token,和「只把前 i+1 个 token 喂进模型」算出来的完全一样。于是训练时一次前向,就同时做完了 4 道「只看前缀」的题——如果真的逐个前缀喂,5 个 token 要做 5 次前向、处理 1 + 2 + … + 5 = 15 个 token 位置,而掩码只要 1 次、5 个位置。
| 维度 | 含义 | 数值 |
|---|---|---|
| T | 序列长度(贯穿例句) | 5 |
下面可以开关掩码、切换四个视图,并比较两种放掩码的位置。点任何一格,看它的完整算术。
试三件事:① 在「③ S + M」视图里开关掩码,看右上三角整片变成 −∞;切到「④ 权重」,留下的是下三角;② 在「④ 权重」里把掩码的位置切到「softmax 后清零」,看每行的和掉到多少,再切回来;③ 点「给了」对「小王」那一格,分别在开、关掩码时看它的算术:分数一样,权重一个是 32.5%、一个恰为 0。
⚠ 常见误解
「因果掩码就是把未来的词删掉。」不是删,是把分数置为 −∞。矩阵始终是完整的 T×T 方阵,所有位置用同一次矩阵乘并行算完——这正是 Transformer 训练快的前提。真删掉的话,每个位置的输入长度都不一样,就回到了「逐个前缀喂」。
「softmax 之后再把未来的权重清零也一样。」不一样。那样每一行剩下的权重之和小于 1,少掉多少取决于未来 token 的分数——未来仍然在影响现在;要补救就得再归一化一次,结果才和「softmax 之前置 −∞」相同。上面的组件里可以直接比较两种放法的行和。
注意力的演进,你在这里
① 训练时:一个 token 怎么从别的 token 那里取信息
- 解决了能读任意长的序列,参数与长度无关;手里只带一个固定大小的状态 h。卡在ht 要等 ht−1 算完:T = 4,096 时每层 4,096 步不能并行,第 1 个 token 的信息要经过 4,095 次更新才到最后;翻译时整句还得挤进一个定长向量——Bahdanau 2014 说的瓶颈。展开岔路 B1
- 解决了解码器每一步回头给源句的每个位置打分、加权取回,不再只靠那一个定长向量:score = vT·tanh(W1q + W2k)。卡在每一对都要单独过 tanh,交不给矩阵乘单元;朴素实现要先落一个 T×T×da 的中间张量:T = 4,096、da = 256 时单头单层 8.0 GiB(BF16),点积只留 T×T 个分数,32 MiB。它还长在 RNN 翻译模型里,串行没解决。展开第 2 课
- 解决了直接比 embedding 时「给了」最像的是它自己(余弦 1.000),小明、苹果、小王排在最后;投影成 Q、K 再点积,它自己排到最后、前三名是苹果、小王、小明,全部配对一次矩阵乘 QKT;取回用单独的 V——两个人名的 K 余弦 0.995,取回 K 分不清是谁。卡在不缩放:锚点每头 256 维,点积标准差 √256 = 16,同一行分差十几二十是常态(e20 ≈ 4.9 亿),softmax 几乎独占,梯度传不回 WQ、WK。
- 解决了softmax 把分数变成正的、和为 1 的权重,只看分差(按占比分,对话样本的 10、3、−4 会给「汽车」−44.4%);除以 √dk = 16,点积方差回到 1。卡在训练时整句一次喂进去:不加掩码,「给了」一行有 32.5% 的注意力直接落在它要预测的答案「小王」上,loss 低得虚假。展开第 3 课
- 解决了旋转后 q·k 只依赖距离 j − i;V 不参与打分,不转(转了会把绝对位置混进取回的内容)。锚点每头 256 维里只转 64 维。卡在旋转矩阵随位置变,夹在展开矩阵和缓存之间就没法事先并进 Q:MLA 因此把 RoPE 单独拆出 64 维另存(DeepSeek-V2 config,第 10 课)。
- 解决了标准 softmax 逼每行分完 100%:「苹果」一行 3 个分数只差零点几(0.04、0.36、0.00),照样分成 30% / 41% / 29%;分母加可学习的 sink logit,或在输出上乘门,这个头就能「这次不取」——锚点开着输出门(attn_output_gate = true)。卡在出口的效果只有实验证据:Gated Attention 原文说门控「缓解」sink,不是消除;DeepSeek-V4 没给 sink logit 的单独消融。展开岔路 B4
② 生成时:K、V 怎么越存越少
- 解决了缓存直接除以头数:锚点形状下每 token 16 KiB,262,144 上下文 4 GiB。卡在质量会掉、训练可能不稳:GQA 论文表 1(T5-XXL,七个任务平均)MHA 47.2 → MQA 46.6。展开第 9 课
- 解决了24 个 Q 头分 4 组、每组 6 个共用 1 个 KV 头(组数 = KV 头数):每 token 64 KiB、262,144 上下文 16 GiB;T5-XXL 上 GQA-8 平均 47.1、推理 0.28 s,质量接近 MHA(47.2、1.51 s),速度接近 MQA(0.24 s)。卡在再减 KV 头就回到 MQA:DeepSeek LLM 67B 已是 GQA-8,每 token 仍要 389,120 B(每头一份的 MHA 是 3,112,960 B),1M token 单条请求 380 GiB。展开第 9 课
- 解决了不存 K、V,每层每 token 只存 512 维潜向量 + 64 维 RoPE = 576 个数,用时再展开:DeepSeek-V2(60 层)每 token 69,120 B、V3(61 层)70,272 B;相对 67B 按 BF16 降 82.2%,官方「−93.3%」还含约 6 bit 的 KV 量化。卡在仍随长度线性增长,打分仍是 T²:GLM-5.3 同是 576 个数、78 层,每 token 89,856 B,原生 1,048,576 上下文时单条请求只算潜向量就要 87.75 GiB。展开第 10 课
- 解决了换掉「按长度存」本身:锚点 64 层里 48 层不存 KV,只存固定大小的状态(FP32 共 144 MiB),与上下文长度无关(这些层是什么、怎么更新,C11 第 4 课揭晓);DeepSeek-V4.1 叠加压缩、稀疏与低精度,官方报告全局 KV 890 B/token。卡在固定状态装不下任意多对键值,锚点仍留 16 层全注意力;稀疏注意力每步只读挑出来的一部分,挑本身也有代价——各自省了什么、没省什么,C11 讲。
证据三问
- 它解决什么问题?让训练时整句并行计算,同时保证每个位置只用到它左边的信息,和生成时的处境一致。
- 数学上为什么有效?e−∞ = 0,且 softmax 的归一化只在可见位置之间进行,所以位置 i 的输出与右边的 token 无关 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条逐位置可验证;项目测试 test_attention_weights_are_causal 断言未来权重恰为 0 出处待补 。
- 实验上呢?这一条不需要实验来选:它是「预测下一个 token」这个任务本身的要求。项目里的对照实验(上面)只是让你看到不加会怎样。
一句话
因果掩码在 softmax 之前把未来置为 −∞:训练时一次算完整句,每个位置又看不到自己的答案。
下一个缺口
权重现在干净了:「给了」只在 小明、把、苹果、给了 之间分配注意力。可前三课一直在说「取回信息」——按这组权重,到底取回什么?是 K 吗?→ 第 5 课:找到该关注谁之后,取回来的是什么?
你可能会问
这一课还没有收到真实提问;你的提问会记到 05-问答记录/C3-N1.md。
🔍 生成的时候还需要掩码吗?
要看怎么算。生成时每步只喂一个新 token,它的 q 本来就只能和已经存在的 k 打分,右边什么都没有,掩码是空的(第 8 课)。但处理 prompt 的那一步(prefill)是整段并行算的,仍然需要掩码;如果 prompt 分几块续写,掩码还要带上偏移——第 k 个新 token 能看见缓存里的全部旧 token 加上本块内它左边的新 token。项目代码 Attention.forward 里的 tril(diagonal=offset) 就是这个。
🔍 掩码本身是不是也泄露了位置信息?
是的。第 i 行能看 i+1 个 token,这个「能看多少」本身就和位置有关。所以即使完全不用位置编码(NoPE),带因果掩码的模型也能从中推断出一部分位置信息——这是 C2 第 5 课「NoPE 就是没有位置」那条误解的由来。