我是AI时代的无业游民,我游荡在现实与意念之间
深入理解 Transformer:Decoder 与 Masked Attention 速查手册
什么时候用这份手册
你在写课程作业、准备面试,或者第一次读 Transformer 论文源码,看到 Decoder 里的masked_fill、tril、causal_mask就卡住——这份手册按"症状→诊断→改法→代价→边界"的顺序组织,30 秒内能定位到你要的那段代码模板。
前置知识:知道softmax、矩阵乘法、PyTorch 的Tensor基本操作即可。读完你能写出一段可跑通的、带 Mask 的 Decoder 自注意力,并解释清楚"为什么不能让它看到未来"。
① 重构前的症状:Decoder 偷看答案
新手写 Decoder 时,最典型的错误是直接把 Encoder 的自注意力代码复制过来,只改了输入来源。症状是:训练 loss 掉得飞快,推理时却完全不会生成。
# 症状代码:能跑,但训练和推理行为不一致importtorch,torch.nn.functionalasFdefself_attention(x,Wq,Wk,Wv):Q,K,V=x @ Wq,x @ Wk,x @ Wv scores=Q @ K.transpose(-2,-1)/(Q.size(-1)**0.5)attn=F.softmax(scores,dim=-1)# ← 问题在这里returnattn @ V# 序列长度 4,batch=1,d_model=8x=torch.randn(1,4,8)Wq=Wk=Wv=torch.randn(8,8)out=self_attention(x,Wq,Wk,Wv)print(out.shape)# torch.Size([1, 4, 8]) 看起来没问题复现方式:把这段代码放进一个"预测下一个 token"的小任务里,你会看到模型在训练集上几乎瞬间收敛到 100% 准确率——因为它每个位置都直接看到了正确答案。这就是label leakage(标签泄漏)。
另一个常见症状是维度对不上:scores是(B, T, T),但你想 mask 的时候拿到的mask是(T,),广播方向错了,PyTorch 不报错但结果全乱。
② 问题诊断:注意力天生是"全连接"的
根因不在代码写错,而在自注意力的定义:scores[i][j]表示第 i 个位置对第 j 个位置的关注度。对 Decoder 来说,生成第 i 个 token 时,只能依赖0..i这些位置,i+1..T-1必须被屏蔽。
所以问题不是"要不要加 mask",而是"mask 加在哪一步、用什么形状"。三个关键点:
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| mask 位置 | softmax之前把非法位置置为-inf | 在 softmax 之后乘 0(概率不归一) |
| mask 形状 | (T, T)下三角,可广播到(B, H, T, T) | 只传(T,),广播维度错 |
| 数值安全 | 用torch.finfo(dtype).min而非-inf | 直接-inf在某些 dtype 下产生 NaN |
一句话记住:Mask 是给 softmax 的输入加一个"负无穷大"的偏置,让被屏蔽位置的权重指数级趋近 0。
③ 重构后的写法:完整对照
importtorch,torch.nn.functionalasFdefmasked_self_attention(x,Wq,Wk,Wv,causal=True):""" x: (B, T, d_model) 返回: (B, T, d_model) """B,T,D=x.shape Q,K,V=x @ Wq,x @ Wk,x @ Wv scores=Q @ K.transpose(-2,-1)/(D**0.5)# (B, T, T)ifcausal:# 关键改动 1:构造下三角 mask,(T, T),可广播mask=torch.tril(torch.ones(T,T,dtype=torch.bool,device=x.device))# 关键改动 2:用 dtype 最小值,避免 -inf 产生 NaNneg=torch.finfo(scores.dtype).minscores=scores.masked_fill(~mask,neg)# 上三角 → 极小值attn=F.softmax(scores,dim=-1)# 关键改动 3:softmax 在 mask 之后returnattn @ V对照表(把"之前"和"之后"并排看):
| 步骤 | 之前 | 之后 |
|---|---|---|
| 打分 | Q @ K.T / sqrt(d) | 同左,不变 |
| 屏蔽 | 无 | masked_fill(~tril, finfo.min) |
| 归一化 | softmax | softmax(顺序在 mask 后) |
| 聚合 | attn @ V | 同左,不变 |
最小验证(面试/作业常被追问的点):打印attn[0],第 0 行应只有第 0 列非零,第 1 行只有前两列非零,依此类推。如果第 0 行出现多个非零值,说明 mask 没生效。
attn=F.softmax(scores,dim=-1)print(attn[0])# 期望:下三角结构,上三角为 0④ 收益与代价
收益(可量化):
- 训练 loss 从"虚假的 0.01"回到"真实的 2.x 起步",泛化 gap 消失——这是最直接的收益。
- 推理时
mask可缓存为常量,不随 batch 变化,额外显存开销仅T×T布尔矩阵(T=2048 时约 4MB,可忽略)。 - 代码可读性:
masked_fill一行替代了"先 softmax 再乘 mask 再重归一化"的三行易错写法。
代价:
- 多一次
masked_fill的逐元素操作,实测在 T=1024、d_model=512 时前向耗时增加约 3%~5%(取决于硬件,属于可接受范围)。 - 需要理解
finfo.min与-inf的区别,学习成本 +1 小时。
| 维度 | 影响 |
|---|---|
| 可读性 | ↑ 一行 mask 意图明确 |
| 性能 | ↓ 约 3%~5%(可忽略) |
| 正确性 | ↑↑ 从"泄漏"到"因果" |
| 复杂度 | 增加一个tril构造,可缓存 |
⑤ 适用边界与检查清单
什么时候不该这么改:
- Encoder 自注意力:不需要 causal mask,加了反而破坏双向建模能力。
- 交叉注意力(cross-attention):mask 应来自 Encoder 的 padding mask,形状是
(B, 1, 1, S),不是tril。 - 已经用
torch.nn.MultiheadAttention且传了attn_mask的场景:框架内部已处理,别重复 mask(会双重屏蔽,梯度异常)。
上线前检查清单:
- mask 在 softmax之前应用
- 用
finfo(dtype).min而非裸-inf tril的device与输入一致(GPU 上忘写device=会静默回退 CPU 再传输,慢且易错)- 打印
attn[0]验证下三角结构 - 区分 padding mask(变长序列)与 causal mask(因果性),两者可叠加
- 推理时
mask缓存为模块常量,别每步重建
面试常被追问:为什么 mask 用加法而不是乘法?答:softmax 对加法偏置敏感,exp(-inf)=0能精确归零;乘法在 softmax 后做会破坏概率和为 1 的约束,需要重新归一化,多一步且易错。
把这段masked_self_attention写进你的作品集,配上"训练 loss 曲线对比图",就是一个能讲清楚"我理解因果注意力"的最小证据。