PyTorch 2.x 深度学习专题【左扬精讲】—— Attention 的两个补丁:Positional Encoding 与 Masked Self-Attention,从 Encoder 跨到 Decoder

PyTorch 2.x 深度学习专题【左扬精讲】—— Attention 的两个补丁:Positional Encoding 与 Masked Self-Attention,从 Encoder 跨到 Decoder

上一篇把 Self-Attention 的四步公式 Softmax(Q K^T / sqrt(d_k)) · V 拆透了,但留了两个补丁没讲:"位置信息"和"未来屏蔽"。本篇就是来补上这两个补丁的。

本篇思路:先用一个最小实验证明 Softmax(X X^T) X 是"置换不变"的(交换两个 token,输出完全不变),由此引出必须显式注入位置信息这一铁律。然后讲三种主流位置编码方案——sinusoidal(原 Transformer 用的固定编码)、RoPE(Llama / Qwen / GLM 用的旋转编码,天然支持相对位置)、ALiBi(BLOOM 用的线性偏置,外推能力强)。最后讲 Decoder 训练时如何用一个上三角 Mask 矩阵把"未来"挡住,并对比 Encoder 双向 Attention 与 Decoder 单向 Attention 的边界。

  torch.arange                              ← 生成位置序列 [0, 1, 2, ..., L-1]
  torch.outer                               ← 外积:把两个向量拼成 (L, d_model) 的网格
  torch.sin / torch.cos                     ← 正弦位置编码的核心三角函数
  torch.triu                                ← 取上三角:构造 causal mask 的关键
  torch.nn.Transformer.generate_square_subsequent_mask   ← PyTorch 官方的 causal mask 生成器
  torch.nn.functional.scaled_dot_product_attention       ← 内部接受 attn_mask 参数
  torch.nn.Transformer                                   ← 默认用 sinusoidal PE,src_mask=causal mask
  RoPE(旋转位置编码)                      ← Llama / Qwen 等现代 LLM 默认方案
  ALiBi(Attention with Linear Biases)     ← BLOOM / Mistral 部分版本使用
  

Positional EncodingSinusoidalRoPEALiBiCausal MaskEncoderDecoderTransformer

学习重点(必读)

    • Self-Attention 是置换不变的:交换输入 token 的顺序,输出严格不变;这意味着它"看不见位置"。
    • 三种位置编码方案sinusoidal 固定不可学、RoPE 用旋转编码位置、ALiBi 直接在 score 上加线性偏置。
    • RoPE 的关键性质q_m^T R((m-n)θ) k_n — Q/K 内积中只出现"相对位置 m-n"。
    • Causal Mask:上三角(不含对角线)为 -inf,保证 Decoder 第 i 步只能看到 ≤ i 的位置。
    • Encoder 双向 / Decoder 单向:同一份 Q K^T 公式,加上 Mask 就从 Encoder 跑到 Decoder。

先修知识(了解即可)

    • 上一篇讲的 Scaled Dot-Product Attention 四步公式。
    • 向量点乘 q · k 的几何含义(投影长度)。
    • Softmax 把任意实数压成 (0, 1) 的概率分布。

一、为什么 Self-Attention 必须加位置信息

上一篇结尾 留下的最大悬念是:"Self-Attention 怎么知道谁在第几位?" 答案是——它不知道。Self-Attention 是一种 置换不变(permutation-invariant)的运算,把输入 token 的顺序打乱,输出严格不变。我们先用一段最小代码证明这件事。

1.1 一个最小实验:打乱顺序,输出不变

  import torch                                          # 导入 PyTorch 主包
  import torch.nn.functional as F                        # 导入函数式 API(用 F.softmax)

  torch.manual_seed(0)                                   # 固定随机种子,保证可复现
  n, d_model = 4, 8                                      # 序列长度 4,模型维度 8

  # 构造一个 4×8 的输入矩阵 X,4 行分别代表 4 个 token 的向量
  x = torch.randn(n, d_model)                            # (4, 8) 随机矩阵

  # 构造 W^Q / W^K / W^V 三个 Linear 层(Self-Attention 中 Q/K/V 同源)
  W_q = torch.nn.Linear(d_model, d_model, bias=False)    # (8, 8) 参数矩阵
  W_k = torch.nn.Linear(d_model, d_model, bias=False)
  W_v = torch.nn.Linear(d_model, d_model, bias=False)

  # 计算 Self-Attention 输出(沿用上一篇的四步公式)
  def self_attn(x):                                      # 封装为可复用函数
      Q = W_q(x); K = W_k(x); V = W_v(x)                 # 同一份 X 投影到 Q/K/V
      scores = Q @ K.transpose(-1, -2) / (d_model ** 0.5) # 相似度 + Scale
      attn = F.softmax(scores, dim=-1)                   # 行归一化得到权重
      return attn @ V                                    # 加权求和

  out_original = self_attn(x)                            # 原顺序下的输出

  # 构造一个"打乱顺序"的索引:把第 0 行放到第 2 位,第 2 行放到第 0 位
  perm = torch.tensor([2, 1, 0, 3])                       # 任意置换
  x_shuffled = x[perm]                                   # 按 perm 重排 X
  out_shuffled = self_attn(x_shuffled)                   # 打乱后的输入再算一次 Self-Attention

  # 把"打乱后的输出"按 perm 反向排回原顺序,逐行比较
  out_shuffled_unperm = out_shuffled[perm.argsort()]     # argsort 是 perm 的逆置换
  diff = (out_original - out_shuffled_unperm).abs().max()# 逐元素最大绝对差
  print("max diff =", diff.item())                       # 应该是 0(或极接近 0 的浮点误差)

运行后 max diff 严格为 0(浮点误差级别可以忽略)。这意味着:无论输入 token 怎么打乱,Self-Attention 对每个 token 给出的"上下文聚合"完全一样。换句话说,模型既不知道 "猫" 在第 0 位还是第 3 位,也不知道 "它在" 是不是修饰 "猫"——这些 "位置/顺序" 信息在 Self-Attention 里彻底消失了。

1.2 置换不变性的数学定义

What — 什么是"置换不变"?

对任意置换矩阵 P(每行每列恰有一个 1,其余为 0),如果函数 f 满足 f(PX) = P f(X),就称 f 对输入是 置换不变 的(permutation invariant);如果满足 f(PX) = f(X)(即输出顺序不随输入顺序改变),则称 f 对输入是置换等变的(permutation equivariant)。Self-Attention 是置换等变的(因为输出顺序跟着输入走),但其信息聚合行为是置换不变的(无论谁在前,权重分配逻辑一样)。

这个性质在数学上很优雅,但在自然语言里是灾难:

      • "狗咬人"和"人咬狗"——同样的 3 个 token,但意思完全相反。
      • "不喜欢"和"不很喜欢"——顺序微调,语义大变。
      • "北京到上海"和"上海到北京"——同样的实体,方向反了。

如果 Self-Attention 看不见这些顺序差异,所有这些区分都丢失了——模型会认为这些句子是 "等价" 的。

Why — 没有 Positional Encoding 会发生什么?

问题一:模型无法区分同集合不同排列。"我爱你"和"你爱我"在 Self-Attention 看来是完全一样的输入,输出也完全一样,下游分类/生成任务拿不到任何"谁主语谁宾语"的信号。

问题二:相对位置信息丢失。很多语言任务关心的不是"绝对第几位",而是"两个 token 之间隔多远"。Self-Attention 本身只能算"哪些 token 之间相似",无法把"距离 5"和"距离 10"区分开。

问题三:没有外推能力。训练时见过的最大序列长度是 512,推理时给一个 1024 的输入,Self-Attention 部分还能算,但没有任何"位置 1000 在哪"的先验。

没有 Positional Encoding 会发生什么?

  • 后果 1bag-of-tokens 模型——只关心"出现过哪些 token",不关心顺序,相当于把语言退化成"词袋"。
  • 后果 2:位置信息完全依赖 Embedding 层的随机初始化去"猜",训练极不稳定,几乎学不到任何语法结构。
  • 后果 3:模型在长序列上完全失效,因为没有任何"距离"先验。

1.3 三条注入位置信息的思路

既然 Self-Attention 看不到位置,我们就显式把位置信息塞进输入。主流方案有三条路:

方案注入位置是否可学习代表模型
正弦位置编码(sinusoidal) Embedding 输出后相加 不可学习,固定公式 原 Transformer、GPT-2 之前
学习型位置编码(learned) Embedding 输出后相加 可学习参数矩阵 GPT-2 / BERT
RoPE(旋转位置编码) Q / K 投影后做旋转 theta 不可学,频率外可学 Llama / Qwen / GLM / Baichuan
ALiBi(线性偏置) Q ·K^T 后加偏置 slope 固定,不可学 BLOOM / 部分 Mistral 版本

本篇重点讲 sinusoidalRoPEALiBi 三种。learned PE 因为只是"用一个可学习参数矩阵替代固定公式",本质上是 sinusoidal 的可学习版本,本篇不再展开。

图 1-1  三种位置编码的注入位置(一条 Encoder block 的数据流)
  ┌──────────────────────────────────────────────────────────────────────┐
  │  Token Embedding  (vocab_size × d_model)                            │
  │  ↓                                                                       │
  │  [+] 位置编码(加到 Embedding 上)                                         │
  │  ↓                                                                       │
  │  X = Embedding + PE          ← sinusoidal / learned 在这里加            │
  │  ↓                                                                       │
  │  W^Q / W^K / W^V 投影                                                       │
  │  ↓                                                                       │
  │  Q, K, V                                                                   │
  │  ↓                                                                       │
  │  [Q, K 上做旋转]            ← RoPE 在这里旋转                          │
  │  ↓                                                                       │
  │  Q K^T / sqrt(d_k)                                                          │
  │  ↓                                                                       │
  │  [+] 线性偏置 -m·|i-j|     ← ALiBi 在这里加偏置                  │
  │  ↓                                                                       │
  │  Softmax → 乘 V → 输出                                              │
  └──────────────────────────────────────────────────────────────────────┘
  

本章小结

  • Self-Attention 是置换等变的:输出顺序跟着输入走,但信息聚合逻辑本身是置换不变的(交换输入 token,权重分布不变)。
  • 自然语言需要位置:"狗咬人"和"人咬狗"必须靠顺序区分。
  • 三条注入位置信息的路:加在 Embedding 上(sinusoidal / learned)、在 Q/K 上做旋转(RoPE)、在 score 上加偏置(ALiBi)。

二、正弦位置编码(原 Transformer)

原论文 Attention Is All You Need 提出的位置编码方案,核心是一个不学任何参数的固定公式:对每个位置 pos、每个维度 i,输出一个由 sincos 算出来的标量。它的精妙之处在于:不同维度对应不同频率,低维度变化快、高维度变化慢,从而让任意两个位置的编码都不同。

2.1 一句话公式

正弦位置编码(Transformer 原论文 §3.5) PE(pos, 2i) = sin( pos / 10000^(2i / d_model) ) PE(pos, 2i+1) = cos( pos / 10000^(2i / d_model) ) 其中 pos 是 token 在序列中的位置(0..L-1),i 是维度索引(0..d_model/2-1)。

这里有几个初看反直觉的设计:

  • 奇偶维度交替:偶数维度用 sin,奇数维度用 cos,两者刚好拼成一个2 维向量(见 §2.3 旋转解释)。
  • 分母 10000 是"最大波长":当 2i = d_model - 1 时,分母达到 10000,对应波长最长的维度;2i = 0 时波长最短(≈ )。
  • 10000 来自哪里:原论文直接选了 10000,后续 LLaMA-2 改用 10000 * 2,GPT-NeoX 改成 10000 * 4,本质都是"最大波长"这个超参。
  图 2-1  正弦 PE:每个维度 i 对应一个频率
  d_model = 8 (示意),奇偶维度交替 sin/cos
  ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
  │ PE(0,0) │ PE(0,1) │ PE(0,2) │ PE(0,3) │ PE(0,4) │ PE(0,5) │ PE(0,6) │ PE(0,7) │
  │sin(0/1) │cos(0/1) │sin(0/T) │cos(0/T) │sin(0/M) │cos(0/M) │sin(0/X) │cos(0/X) │
  │  = 0    │  = 1    │  = 0    │  = 1    │  = 0    │  = 1    │  = 0    │  = 1    │
  └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
          │         │         │         │         │         │         │         │
       高频       高频      中频      中频      低频      低频      极低频    极低频
       (变化快)   (变化快)  (变化中)  (变化中)  (变化慢)  (变化慢)  (几乎不变) (几乎不变)

  其中 T = 10000^(2/d_model), M = 10000^(4/d_model), X = 10000^(6/d_model)
  

2.2 为什么用 sin/cos:相对位置可线性表达

原论文给了两条用 sin/cos 的理由:

理由一:不同位置得到不同的编码。

三角函数族 {sin(k·x), cos(k·x)} 在不同频率 k 上线性无关(傅里叶分析),因此 PE(pos, ·) 是一个高维向量,对任意两个不同 pos,它们的 PE 向量都不相同。这正是我们想要的。

理由二:相对位置 PE(pos+k) 可由 PE(pos) 的线性组合表达。

这是 sin/cos 相比 learned PE 最大的优势。利用三角恒等式:

相对位置的线性表达(Transformer 原论文 §3.5) PE(pos+k, 2i) = PE(pos, 2i) · PE(k, 2i+1) + PE(pos, 2i+1) · PE(k, 2i) PE(pos+k, 2i+1) = PE(pos, 2i+1) · PE(k, 2i+1) − PE(pos, 2i) · PE(k, 2i) 即 PE(pos+k) = M(k) · PE(pos),其中 M(k) 是一个仅依赖相对距离 k 的固定矩阵。

这个性质意味着:模型只要在训练时见过某种相对距离 k,就能通过线性变换推广到任意 pos 上,外推到训练时没见过的长序列更容易。learned PE 完全没有这个性质,它必须见过对应的 pos 才能用。

2.3 PyTorch 实现 + 频率曲线

  import torch                                # 导入 PyTorch 主包

  def sinusoidal_pe(seq_len, d_model):           # 定义正弦 PE 生成函数
      pe = torch.zeros(seq_len, d_model)         # 初始化 (seq_len, d_model) 的零矩阵
      position = torch.arange(seq_len).unsqueeze(1).float()  # (seq_len, 1),每个位置的 pos
      # 计算 10000^(2i / d_model) 的分母,对每个偶数维度 i 各算一个
      div_term = torch.exp(                                       # 用 exp 实现 10000^(2i/d_model)
          torch.arange(0, d_model, 2).float() * (-(torch.log(torch.tensor(10000.0)) / d_model))
      )                                                          # 形状 (d_model/2,)
      # 偶数维度 (0, 2, 4, ...) 填 sin
      pe[:, 0::2] = torch.sin(position * div_term)                # (seq_len, 1) · (d_model/2,) → 广播
      # 奇数维度 (1, 3, 5, ...) 填 cos
      pe[:, 1::2] = torch.cos(position * div_term)                # 同上
      return pe                                                  # 返回 (seq_len, d_model) 的位置编码矩阵

  pe = sinusoidal_pe(seq_len=20, d_model=8)     # 生成 20 个位置、8 维的 PE
  print(pe.shape)                                # 打印形状:torch.Size([20, 8])
  print(pe[0])                                   # 打印位置 0 的编码:[0, 1, 0, 1, 0, 1, 0, 1]
  print(pe[1])                                   # 打印位置 1 的编码:高频维度变化最大,低频维度几乎不变

运行 pe[0][0, 1, 0, 1, 0, 1, 0, 1]pe[1] 在低频维度(最后两位)几乎是 [0, 1],高频维度(前两位)变化剧烈。这正是"低维度快、高维度慢"的频率曲线。

下图(配图)展示了 4 个不同维度(i = 0, 1, 2, 3)下 PE 值随 pos 的变化:

image图 1:正弦位置编码的 4 条频率曲线 — 低维快、高维慢

小贴士:实现细节的两个坑。

  • 分母用 torch.exp 不用 10000 **:写成 10000 ** (2i / d_model) 在大模型 d_model=4096 时数值精度会出问题;用 torch.exp(i * (-log(10000) / d_model)) 在对数空间算更稳。
  • arange(0, d_model, 2) 用 0 不是 1:原论文公式是 10000^(2i / d_model),所以 i0, 1, 2, ...,对应偶数维度索引 0, 2, 4, ...。PyTorch 习惯写法是 arange(0, d_model, 2)

本章小结

  • 公式PE(pos, 2i)=sin(pos/10000^(2i/d_model))PE(pos, 2i+1)=cos(...)
  • 频率:低维度 i 频率高、变化快;高维度 i 频率低、变化慢。
  • 最大优势PE(pos+k) = M(k) · PE(pos),相对位置可线性表达。
  • 参数0 个可学习参数,纯固定公式。

三、RoPE:旋转位置编码(Llama / Qwen 默认)

2021 年 RoPE 论文(Su et al., RoFormer: Enhanced Transformer with Rotary Position Embedding)提出了一种完全不同的注入方式:不在 Embedding 上加,而是对 Q / K 做"旋转"。RoPE 的两大杀手锏是:

      • 天然支持相对位置q_m^T k_n 中只出现 m - n,不需要像 sinusoidal 那样做线性变换。
      • 长度外推:和 sinusoidal 一样不依赖训练时见过的绝对位置,可以外推到更长的序列(配合 Dynamic NTK 等技术效果更好)。

Llama / Qwen / GLM / Baichuan 等现代 LLM 默认使用 RoPE。本节从 2 维情形讲起,再推广到 d 维,最后给出核心推导。

3.1 2 维情形:把"位置"当成"旋转角度"

先看最简单的情况:d = 2。假设 token 在位置 m 的查询向量是 q = (q_1, q_2),RoPE 不修改 q 的数值,而是把它在 2 维平面上旋转 m·θ 角度,得到 q_m

RoPE 2 维情形:旋转矩阵 q_m = R(m·θ) · q 其中 R(φ) = [[cos φ, -sin φ], [sin φ, cos φ]] 是标准 2 维旋转矩阵。 类似地:k_n = R(n·θ) · k。

直观上:位置 m 越大,q 在 2 维平面上转过的角度越大。不同位置的 q 在空间中处于"不同朝向",而 Self-Attention 算的是 q 与 k 的内积——这刚好能感知到位置的差异。

image图 2:RoPE 旋转位置编码 — 2 维情形的几何直观

q_mk_n 做内积,会发生一个神奇的事:

RoPE 2 维:内积只含相对位置 q_m^T · k_n = (R(m·θ) q)^T · (R(n·θ) k) = q^T R(m·θ)^T R(n·θ) k = q^T R((n-m)·θ) k ← 用到旋转矩阵正交性:R(a)^T R(b) = R(b-a) = q^T R((n-m)·θ) k 即:q_m^T k_n 中只包含相对位置 (n - m),不包含 m 和 n 各自的绝对值。

这一步推导里用到了旋转矩阵的正交性R(φ)^T = R(-φ),因此 R(a)^T R(b) = R(-a) R(b) = R(b - a)。这是 RoPE 的全部秘密。

3.2 d 维推广:block-diagonal 旋转

实际模型里 d_model 通常是 64 / 128 / 256(每个头的维度)。RoPE 的处理方式很巧妙:

  • d 维向量两两一组,分成 d/2 个 2 维子空间。
  • 每个 2 维子空间用不同频率 θ_i做旋转。
  • 整个旋转是一个block-diagonal(分块对角)矩阵
RoPE d 维:block-diagonal 旋转 θ_i = 10000^(-2i / d), i = 0, 1, ..., d/2 - 1 R_d(m) = diag( R(m·θ_0), R(m·θ_1), ..., R(m·θ_{d/2-1}) ) 即第 i 个 2 维子空间 (q_{2i}, q_{2i+1}) 旋转 m·θ_i 角度。

频率 θ_i = 10000^(-2i / d) 的形式和 sinusoidal PE 完全一致:低维度 i 对应高频(波长短),高维度 i 对应低频(波长长)。这种"多频率组合"的设计让 RoPE 同时具备"高频精确、低频稳定"的特性。

图 3-1  RoPE 的 block-diagonal 旋转矩阵(d=8 示意)
  R_8(m) = diag( R(m·θ_0), R(m·θ_1), R(m·θ_2), R(m·θ_3) )

  ┌                                  ┐
  │ cos(mθ_0)  -sin(mθ_0)  0              0               0              0              0              0               │
  │ sin(mθ_0)   cos(mθ_0)  0              0               0              0              0              0               │
  │ 0                  0                cos(mθ_1) -sin(mθ_1) 0              0              0              0               │
  │ 0                  0                sin(mθ_1)  cos(mθ_1) 0              0              0              0               │
  │ 0                  0                0               0               cos(mθ_2) -sin(mθ_2) 0              0               │
  │ 0                  0                0               0               sin(mθ_2)  cos(mθ_2) 0              0               │
  │ 0                  0                0               0               0              0               cos(mθ_3) -sin(mθ_3) │
  │ 0                  0                0               0               0              0               sin(mθ_3)  cos(mθ_3) │
  └                                  ┘

  8 维向量被切成 4 个 2 维子空间,每个子空间独立旋转 m·θ_i 角度。
  

3.3 核心推导:Q·K^T 中只出现相对位置

这一节把 2 维推导扩展到 d 维,用线性代数语言完整写出 RoPE 的核心定理。

Why — 为什么 RoPE 能"天然"处理相对位置?

从 2 维推导我们已经看到:R(m·θ)^T R(n·θ) = R((n-m)·θ)。这个性质在 d 维下依然成立(block-diagonal 矩阵的转置乘积等于各块分别相乘)。于是:

RoPE 核心定理(d 维推广) q_m^T k_n = (R_d(m) q)^T · (R_d(n) k) = q^T R_d(m)^T R_d(n) k = q^T R_d(n - m) k 其中 R_d(m)^T R_d(n) = R_d(n - m) 用到了 R(φ)^T = R(-φ) 和 block-diagonal 的性质。

结论:Self-Attention 的核心运算 q_m^T k_n 在 RoPE 编码后只依赖相对位置 m - n,不依赖 m 和 n 各自的绝对值。这就是 RoPE 论文标题里"Enhanced"的来源——它把"相对位置建模"这件事变成了 Self-Attention 的内秉能力,不需要任何额外参数。

没有 RoPE 会发生什么?

  • 后果 1:模型只能用 sinusoidal/learned PE 学"绝对位置",相对距离信息需要模型自己从绝对位置里"减出来",训练效率低。
  • 后果 2:在长序列外推时,绝对位置的 PE 值会跑到训练时没见过的分布上,模型"认不出来"。
  • 后果 3:在 Q/K 维度变化的场景(如 Linear Attention、KV Cache 压缩),sinusoidal PE 因为是固定在 Embedding 上而不是在 Q/K 上,无法跟着 Q/K 一起变;RoPE 是直接对 Q/K 做旋转,与输入无关,更灵活。

3.4 PyTorch 实现(极简版)

  import torch                                # 导入 PyTorch 主包

  def precompute_rope_cache(seq_len, d, base=10000.0):  # 预计算 cos/sin 缓存
      # θ_i = base^(-2i/d), i = 0..d/2-1
      theta = 1.0 / (base ** (torch.arange(0, d, 2).float() / d))   # (d/2,)
      # 位置序列 [0, 1, 2, ..., seq_len-1]
      pos = torch.arange(seq_len).float()                            # (seq_len,)
      # 外积:(seq_len, d/2),每一行是 m * θ_i
      angles = torch.outer(pos, theta)                               # (seq_len, d/2)
      # 缓存 cos 和 sin,后面 forward 时直接查表
      cache = torch.stack([torch.cos(angles), torch.sin(angles)], dim=-1)  # (seq_len, d/2, 2)
      return cache                                                  # 返回 (seq_len, d/2, 2)

  def apply_rope(x, cache):                       # 把 RoPE 应用到 (B, H, L, d) 的 Q 或 K
      # x: (B, H, L, d),cache: (L, d/2, 2)
      # 把最后一维 (x1, x2) 拆成两半,再按 (-x2, x1) 旋转
      x_pair = x.float().reshape(*x.shape[:-1], -1, 2)              # (B, H, L, d/2, 2)
      x_rot = torch.stack([-x_pair[..., 1], x_pair[..., 0]], dim=-1)  # (-x2, x1) ← sin 旋转部分
      # cache 扩展为 (1, 1, L, d/2, 2) 用于广播
      c = cache.unsqueeze(0).unsqueeze(0)                           # (1, 1, L, d/2, 2)
      # 旋转公式:out = x_pair * cos(c) + x_rot * sin(c)
      out = x_pair * c[..., 0:1] + x_rot * c[..., 1:2]              # (B, H, L, d/2, 2)
      return out.reshape_as(x).to(x.dtype)                          # 还原成 (B, H, L, d)

  # 演示:把 RoPE 应用到一个 1×1×5×4 的查询张量上
  d = 4                                                      # 每个头 4 维
  cache = precompute_rope_cache(seq_len=5, d=d)              # 预计算 5 个位置的 cos/sin
  q = torch.tensor([[[[1.0, 0.0, 1.0, 0.0],                  # 5 个位置,每个位置 1 个头、4 维
                     [1.0, 0.0, 1.0, 0.0],
                     [1.0, 0.0, 1.0, 0.0],
                     [1.0, 0.0, 1.0, 0.0],
                     [1.0, 0.0, 1.0, 0.0]]]])               # (1, 1, 5, 4)
  q_rope = apply_rope(q, cache)                              # 应用 RoPE
  print("q_rope.shape =", q_rope.shape)                      # (1, 1, 5, 4)
  print("q_rope[0, 0, 0] =", q_rope[0, 0, 0])               # 位置 0 不旋转:应是 (1, 0, 1, 0)
  print("q_rope[0, 0, 1] =", q_rope[0, 0, 1])               # 位置 1 旋转高频维度
  print("q_rope[0, 0, 4] =", q_rope[0, 0, 4])               # 位置 4 旋转更大角度

上面这段代码是 RoPE 的"教学版"实现:先把位置 m 和频率 θ_i 做外积得到角度 m·θ_i,再预算 cos / sin 缓存;forward 时把 (x_1, x_2) 旋转成 (x_1 cos - x_2 sin, x_1 sin + x_2 cos)。真实工程中还会做 dtype 转换、KV Cache 切片等优化,但核心就是这个 apply_rope

小贴士:RoPE 与 sinusoidal 的本质联系。

两者的频率公式都是 θ_i = 10000^(-2i / d),低维度高频、高维度低频。这不是巧合——RoPE 论文里就明确说:"RoPE 可以被理解为 sinusoidal PE 的一种'向量化'形式,只是把位置信息从 Embedding 转移到了 Q/K 上"。因此 RoPE 也继承了 sinusoidal 的"多频率组合"特性。

本章小结

  • RoPE = 旋转位置编码:把 Q / K 在 d/2 个 2 维子空间里分别旋转 m·θ_i
  • 核心定理(R_d(m) q)^T (R_d(n) k) = q^T R_d(n - m) k,内积只含相对位置。
  • 频率分布θ_i = 10000^(-2i/d),与 sinusoidal 一致。
  • 0 个可学习参数:纯固定公式,参数全在 W^Q/W^K/W^V 上。

四、ALiBi:线性偏置

2022 年 Press et al. (Train Short, Test Long) 提出 ALiBi(Attention with Linear Biases)。它和前两章都不一样——不在 Embedding 上加、也不在 Q/K 上旋转,而是直接给 Attention 的 score 加一个跟距离成正比的负偏置。公式异常简洁,但效果惊艳。

4.1 一句话公式

ALiBi(Press et al., 2022) ALiBi(Q, K, V) = Softmax( Q K^T / sqrt(d_k) − m · |i - j| ) · V 其中 i 是查询位置、j 是被查位置、m 是每个注意力头专属的固定斜率(slope)。

直觉上:距离越远的两个 token,Attention score 被压得越低,模型天然倾向于"关注近处"。这与人眼/认知系统"近处看得清、远处被弱化"的偏置吻合。

4.2 关键设计:slope 的固定值

ALiBi 不学任何参数,但每个头都有一个固定斜率 m。原论文给出的方案是:

  • H 个头,m 从一个几何级数里取:2^(-8/H), 2^(-16/H), ..., 2^(-8H/H)
  • 即不同头有不同衰减速度——有些头看得很近(m 大),有些头看得很远(m 小)。
  • 举例:H=4 时 4 个头的 slope 依次为 2^(-2)=1/42^(-4)=1/162^(-6)=1/642^(-8)=1/256
import torch                                # 导入 PyTorch 主包

  def get_alibi_slopes(num_heads):           # 计算 ALiBi 的 slope 序列
      # 论文公式:m_i = 2^(-8 * (i + 1) / num_heads), i = 0..H-1
      # 实际工程中分奇偶头处理(保证 slope 单调)
      slopes = torch.tensor(
          [2 ** (-8 * (i + 1) / num_heads) for i in range(num_heads)],
          dtype=torch.float32
      )
      return slopes                            # 形状 (num_heads,)

  def alibi_attention(Q, K, V, slopes):       # 带 ALiBi 的 Attention
      # Q / K / V: (B, H, L, d)
      scores = (Q @ K.transpose(-1, -2)) / (Q.shape[-1] ** 0.5)  # 标准 4 步的 Step 1+2
      L = scores.shape[-1]                                       # 序列长度 L
      # 构造 |i - j| 矩阵
      positions = torch.arange(L)                                # [0, 1, ..., L-1]
      distance = (positions.unsqueeze(0) - positions.unsqueeze(1)).abs().float()  # (L, L)
      # slopes 从 (H,) 扩展到 (1, H, 1, 1) 用于广播到 (B, H, L, L)
      bias = -slopes.view(1, -1, 1, 1) * distance                # (1, H, 1, L) · (L, L) → (1, H, L, L)
      scores = scores + bias                                     # 在 score 上加负偏置
      attn = torch.softmax(scores, dim=-1)                       # Softmax
      return attn @ V                                            # 加权求和

ALiBi 实现的关键是构造 |i - j| 矩阵并乘以 -m。注意 bias(1, H, 1, 1) 的 shape 一次性广播到 (B, H, L, L),避免逐头循环。

4.3 三种位置编码方案对比

维度sinusoidalRoPEALiBi
注入位置 Embedding 输出后相加 Q / K 投影后做旋转 Q K^T 后加偏置
可学习参数 0 0 0
相对位置 可线性表达(需 M(k)) 天然存在 天然存在
外推长度 一般(依赖位置编码外推技术) 较好(NTK-aware scaling 可大幅外推) 优秀(原始论文展示了 8 倍以上外推)
代表模型 原 Transformer、GPT-2 之前 Llama / Qwen / GLM / Baichuan BLOOM / 部分 Mistral
主要优势 实现简单、理论性质清晰 与 Linear Attention / KV Cache 兼容 参数量最少、外推最强
主要劣势 外推需要额外技巧 短序列上效果略弱于 sinusoidal(需更长 warmup) 对极短距离的区分度不如 RoPE 精细

这张表给出了一个实际经验法则:

  • 追求训练稳定 + 实现简单:选 sinusoidal(或 learned PE)。
  • 追求现代 LLM 标配 + 长度外推:选 RoPE
  • 追求极致外推 + 极致轻量:选 ALiBi

本章小结

  • ALiBi = 在 score 上加 -m·|i-j|,距离越远越被弱化。
  • slope 不可学,每个头一个固定值,几何级数分配。
  • 最强项:长度外推 — 训练 1024 推理 8192 仍能保持质量。
  • 代表模型:BLOOM 系列。

五、Masked Self-Attention:Decoder 训练的关键

位置编码解决了"模型看见位置",但 Decoder 还面临一个完全不同的问题:训练时不能看未来。本节就来拆这个"三角 Mask"。

5.1 为什么 Decoder 不能看未来

Decoder 的任务是自回归生成:给定已生成的 token,预测下一个 token。比如翻译任务里,生成第 i 个目标词时,模型只能看到前 i 个目标词,不能看到第 i+1i+2——否则就等于在"作弊",看到了标准答案。

但训练时为了并行加速,我们会把整个目标序列 [y_0, y_1, ..., y_L] 一次性喂进 Decoder,让模型同时预测每个位置的输出。这就产生矛盾:

  • 如果不做 Mask:第 i 个位置会看到第 i+1 个位置的特征,等于"作弊"。
  • 如果用 RNN:每个位置只能看前一个 hidden state,天生不作弊,但训练没法并行。
  • Self-Attention 的解法:在 Q K^T 后加一个 Mask 矩阵,把"未来位置"的相似度设为 -inf,Softmax 后这些位置的权重自动为 0。

What — Causal Mask 到底是什么?

Causal Mask(因果 Mask)是一个 (L, L) 的矩阵 M,定义为:

  • j ≤ i 时(被查位置在查询位置之前或当前),M[i, j] = 0
  • j > i 时(被查位置在查询位置之后),M[i, j] = -inf

把 Mask 加到 Attention score 上:scores_masked = scores + M。Softmax(-inf) = 0,因此"未来位置"的权重自然为 0,模型只关注当前和之前的位置。

5.2 三角 Mask 矩阵怎么构造

看一张具体的 5 个 token 的 causal mask 长什么样:

image

  图 3:5 个 token 的三角 Mask 矩阵 — 下三角保留、上三角屏蔽

图 5-1  5 个 token 的三角 Mask 矩阵
  行 = 查询位置 i,列 = 被查位置 j
  ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
  │ M[i,j]  │ j=0     │ j=1     │ j=2     │ j=3     │ j=4     │
  ├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
  │ i=0     │   0     │  -inf   │  -inf   │  -inf   │  -inf   │
  │ i=1     │   0     │    0    │  -inf   │  -inf   │  -inf   │
  │ i=2     │   0     │    0    │    0    │  -inf   │  -inf   │
  │ i=3     │   0     │    0    │    0    │    0    │  -inf   │
  │ i=4     │   0     │    0    │    0    │    0    │    0    │
  └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
  ↑ i=0 只能看 j=0(自己)
  ↑ i=4 能看 j=0..4(全部)
  ↑ 这种"下三角含对角线"的形式叫 causal mask / lower-triangular mask
  

这张矩阵的核心性质:

  • 下三角含对角线0,表示"允许看"。
  • 上三角(不含对角线)-inf,表示"禁止看"。
  • 每行从 0 开始连续 i+10,剩下的全是 -inf

5.3 PyTorch 实现

PyTorch 提供两种主流构造方式:

import torch                                # 导入 PyTorch 主包
  import torch.nn.functional as F             # 导入函数式 API

  # === 方式 1:用 torch.triu 直接构造 causal mask ===
  L = 5                                                    # 序列长度
  # torch.triu(ones, diagonal=1) 取上三角(不含对角线),对角线=1 表示从对角线以上开始
  # 把上三角填 1,下三角填 0,再转成 bool mask
  causal_mask_bool = torch.triu(torch.ones(L, L), diagonal=1).bool()  # (L, L) bool
  # 转为 additive mask:True 位置 = -inf,False 位置 = 0
  causal_mask_add = torch.zeros(L, L).masked_fill(causal_mask_bool, float('-inf'))
  print("causal_mask_add =")
  print(causal_mask_add)

  # === 方式 2:PyTorch 官方 API nn.Transformer.generate_square_subsequent_mask ===
  # 输入是序列长度 L,输出是 (L, L) 的 float mask,下三角 0、上三角 -inf
  official_mask = torch.nn.Transformer.generate_square_subsequent_mask(L)
  print("official_mask =")
  print(official_mask)                                     # 与方式 1 完全等价

  # === 把 mask 应用到 Scaled Dot-Product Attention ===
  torch.manual_seed(0)                                     # 固定种子
  d_k = 8
  scores = torch.randn(1, L, L) / (d_k ** 0.5)             # 假设这是 Q K^T / sqrt(d_k) 的输出
  scores_masked = scores + causal_mask_add                 # 直接相加:-inf 位置变为 -inf
  attn = F.softmax(scores_masked, dim=-1)                  # Softmax:未来位置权重自动为 0
  print("attn row sums =", attn.sum(dim=-1))               # 每行和 = 1
  print("attn[0] =", attn[0])                              # 第 0 行:[1, 0, 0, 0, 0],只看自己

  # === 方式 3:直接传给 F.scaled_dot_product_attention ===
  Q = torch.randn(1, L, d_k)
  K = torch.randn(1, L, d_k)
  V = torch.randn(1, L, d_k)
  # attn_mask 参数会自动加到 Q K^T / sqrt(d_k) 上,等价于手动 masked_fill
  out = F.scaled_dot_product_attention(Q, K, V, attn_mask=causal_mask_add)
  print("out.shape =", out.shape)                          # (1, 5, 8)

注意 torch.nn.Transformer.generate_square_subsequent_mask 是 PyTorch 官方 API,返回的下三角含对角线 0、上三角 -inf,可以直接作为 nn.Transformertgt_mask 参数传进去。在 F.scaled_dot_product_attention 里既支持 float 类型的 additive mask,也支持 bool 类型的 mask(True 位置屏蔽)。

Why — 为什么用加法(+(-inf))而不是乘法(×0)?

两种思路都能"屏蔽未来",但效果有微妙差别:

  • 乘法(×0):屏蔽位置 score 变 0,Softmax 后仍然分到一些概率(虽然很小),数学上不完全"看不到"。
  • 加法(+(-inf)):Softmax(-inf) = 0 严格成立,未来位置的权重精确为 0。这是工程首选。

PyTorch 实现细节:直接写 -float('inf') 在某些 fused kernel 里会触发 NaN(因为 exp(-inf) 在 fp16 下可能溢出)。工程上常用 -1e9 替代,效果几乎一样但数值更稳。

没有 Causal Mask 会发生什么?

  • 后果 1:训练时 Decoder 看到未来 token 的"标准答案",Loss 异常低,但推理时没有未来可看,输出完全崩坏(典型的 train-test mismatch)。
  • 后果 2:模型学会"复制"未来 token 到当前位置,丧失生成能力。
  • 后果 3:无法做 teacher forcing —— 也就是无法并行训练整条目标序列,训练速度降一个数量级。

5.4 Causal Mask vs Padding Mask

Attention 中常见的 mask 有两类,千万别搞混:

维度Causal Mask(因果 Mask)Padding Mask(填充 Mask)
目的 防止看到未来 token 防止看到 padding token(<pad>)
形状 (L, L),跟序列长度有关 (B, L),跟 batch 有关
屏蔽位置 上三角(不含对角线) 每个样本自己的 padding 区域
应用位置 Decoder 的 Self-Attention Encoder / Decoder 任意 Attention
生成方式 generate_square_subsequent_mask key_padding_mask = (input == pad_id)

两类 mask 在 nn.Transformer 中由不同参数处理:

  • tgt_mask:causal mask,形状 (L, L)
  • src_key_padding_mask / tgt_key_padding_mask:padding mask,形状 (B, L)

两者可以同时使用:在 batch 内每个样本有不同的 padding,但 causal mask 对所有样本统一适用。

本章小结

  • Causal Mask 是下三角矩阵:下三角含对角线为 0,上三角为 -inf
  • 加法屏蔽scores + (-inf),而不是 scores × 0
  • 官方 APInn.Transformer.generate_square_subsequent_mask(L)
  • 与 Padding Mask 区分:causal 是 (L, L) 防未来,padding 是 (B, L) 防 pad。

六、Encoder vs Decoder 的 Attention 边界

把全文第一个补丁(位置编码)和第二个补丁(Causal Mask)放在一起看,Encoder 和 Decoder 的 Self-Attention 只差一个 Mask。这张表和流程图给你画清楚边界。

6.1 对比表

维度Encoder Self-AttentionDecoder Masked Self-AttentionDecoder Cross-Attention
是否 Mask 否(双向) 是(causal mask) 否(看 Encoder 全量)
Q 来源 Encoder 输入 X Decoder 上一层输出 Decoder 上一层输出
K / V 来源 Encoder 输入 X(与 Q 同源) Decoder 上一层输出(与 Q 同源) Encoder 最终输出(与 Q 不同源)
位置编码 需要(双向 PE) 需要(causal PE) 需要(Q 用 decoder PE,K 用 encoder PE)
典型用途 编码源语言,理解上下文 自回归生成目标语言 让 decoder 看到 encoder 信息

6.2 一张图说清:同一份 Q K^T 走三条路

image

 

 图 4:Encoder 双向 vs Decoder 因果 — 同一份 Q K^T 公式,三条路

图 6-1  Encoder / Decoder 两条路的核心区别
  ┌────────────────────────────────────────────────────────────────────────┐
  │ Encoder  Self-Attention  双向                                              │
  │                                                                            │
  │  q0 →─┐                                                              │
  │  q1 →─┤                                                              │
  │  q2 →─┼─→ Q K^T / sqrt(d_k) → Softmax → attn @ V         │
  │  q3 →─┤        ↑ Mask 矩阵全 0(不屏蔽任何位置)                  │
  │  q4 →─┘                                                              │
  │                                                                            │
  │  每个位置 i 能看到所有位置 j(包括 j > i 的"未来")                        │
  └────────────────────────────────────────────────────────────────────────┘

  ┌────────────────────────────────────────────────────────────────────────┐
  │ Decoder  Masked Self-Attention  因果(causal)                            │
  │                                                                            │
  │  q0 →─┐                                                              │
  │  q1 →─┤                                                              │
  │  q2 →─┼─→ Q K^T / sqrt(d_k)  +  causal_mask  → Softmax → × V  │
  │  q3 →─┤        ↑ 上三角 -inf,只让看 i 自己及之前的 j              │
  │  q4 →─┘                                                              │
  │                                                                            │
  │  每个位置 i 只能看到 j ≤ i 的位置(看不到未来)                          │
  └────────────────────────────────────────────────────────────────────────┘
  

本章小结

  • Encoder 双向,Decoder 自回归单向:差一个 causal mask。
  • Cross-Attention 不是 Self:Q 来自 Decoder,K/V 来自 Encoder,是 Encoder-Decoder 桥梁。
  • 三套位置编码方案(sinusoidal / RoPE / ALiBi)都能用于三种 Attention,区别仅在"位置信息怎么注入"。

七、形状速查:本篇涉及的张量维度

把上一篇的四步 + 本篇的位置编码和 Mask 都放到一起,给一张"形状速查表"。其中 B 表示 batch size、L 表示序列长度、E 表示 embedding 维度、H 表示头数、d 表示每个头的维度(E = H * d)。

张量形状含义本篇相关章节
PE(sinusoidal) (L, E) 正弦位置编码矩阵 §2
rope_cache (L, d/2, 2) RoPE 的 cos/sin 缓存 §3
alibi_slopes (H,) ALiBi 每个头的固定斜率 §4
causal_mask (L, L) 三角 Mask 矩阵 §5
alibi_bias (H, L, L) ALiBi 的 -m·|i-j| 偏置 §4
padding_mask (B, L) 每个样本的 padding 位置 §5.4

小贴士:常见形状错误。

  • rope_cached 是每个头的维度(E / H),不是 E。RoPE 是在每个头的 d 维空间里旋转。
  • alibi_slopes 长度是 H(头数),不是 L。每个头共用一个 slope,broadcast 到所有样本。
  • causal_mask 必须放在 score 上,不能直接乘 attn;相加才能让 Softmax 严格屏蔽。

八、FAQ 20 问

FAQ 分组说明

  • Q1~Q5 概念类:澄清"为什么需要位置编码"和"Self-Attention 看不见位置"。
  • Q6~Q10 公式类:拆解 sinusoidal / RoPE / ALiBi 的具体公式。
  • Q11~Q15 Mask 类:causal mask 的构造、实现细节、与 padding mask 的区分。
  • Q16~Q20 工程与对比类:PyTorch 2.x 的 API、长度外推、Encoder/Decoder 差异。

Q1. Self-Attention 为什么需要位置编码?一句话能讲清楚吗?

一句话结论:Self-Attention 是置换等变的,对输入 token 顺序完全不敏感,所以模型默认"看不见位置",必须显式注入。展开讲,Self-Attention 的核心运算 Softmax(Q K^T / sqrt(d_k)) V 中,Q、K、V 都从同一个输入矩阵 X 投影而来;交换 X 的任意两行,Q/K/V 也交换两行,但 Attention 的"权重分布"和"加权求和"结果严格不变。这意味着模型无法区分"猫咬人"和"人咬猫"。

Q2. 什么叫"置换不变"和"置换等变"?两者有什么不同?

置换不变指输出顺序不随输入顺序改变;置换等变指输出顺序跟着输入顺序改变。Self-Attention 是置换等变的(每个 token 的输出仍在对应位置),但其权重计算是置换不变的(交换输入两行,每个位置的权重分布不变)。数学上:置换不变满足 f(PX) = f(X),置换等变满足 f(PX) = P f(X)

Q3. 位置编码必须在 Embedding 阶段加吗?能加在别的地方吗?

不是。三种主流方案分别在三个不同位置:sinusoidal/learned 加在 Embedding 上、RoPE 加在 Q/K 上、ALiBi 加在 Q K^T 上。它们数学上各有特点:sinusoidal 注入早、信息保留完整但相对位置要靠线性变换还原;RoPE 注入晚、直接把相对位置编码到内积里;ALiBi 注入最晚、直接给 score 加偏置最简洁。

Q4. learned PE 是不是 sinusoidal 的可学习版本?

本质上是,但少了 sinusoidal 的关键性质:相对位置可线性表达。learned PE 就是一个 (max_len, d_model) 的可学习参数矩阵,每个位置一个向量。它的好处是简单直接、训练数据足够时效果不差;坏处是没有 sinusoidal 的 PE(pos+k) = M(k) · PE(pos) 这种线性结构,外推到训练时没见过的位置完全靠模型"猜"。

Q5. 没有位置编码模型就完全不能用吗?

不是"完全不能用",而是退化成"词袋模型"(bag-of-tokens)。模型只能学到"哪些 token 共现概率高",但学不到语法结构、时序、指代等所有依赖顺序的信息。简单分类任务(如情感分析)还能凑合,但任何需要区分"我爱你"和"你爱我"的任务都会失败。

Q6. sinusoidal PE 里的 10000 是什么?为什么是它?

10000 是"最大波长"超参,决定了最高频维度的"周期"长度。原论文直接选了 10000,没有严谨理论推导,只是个经验值。后续 LLaMA-2 改用 10000×2、GPT-NeoX 改成 10000×4,本质都是调整这个超参。更大 = 波长更长 = 能编码更长序列;更小 = 波长更短 = 短序列上区分更精细。

Q7. sinusoidal PE 为什么要分奇偶维度用 sin/cos?

为了凑出"2 维旋转"的形式,让相对位置 PE(pos+k) 可以由 PE(pos) 的线性组合表达。利用三角恒等式 sin(a+b) = sin(a)cos(b) + cos(a)sin(b)cos(a+b) = cos(a)cos(b) - sin(a)sin(b),可以证明 PE(pos+k) = M(k) · PE(pos),其中 M(k) 只依赖相对距离 k。这也是 RoPE 在 2 维上选旋转的核心动机。

Q8. RoPE 名字里的"Ro"是什么意思?

"Rotary",意为"旋转"。RoPE 全称 Rotary Position Embedding,核心思想是把位置编码成 Q/K 在多维子空间中的旋转角度。论文标题也直接叫 RoFormer: Enhanced Transformer with Rotary Position Embedding

Q9. RoPE 公式 θ_i = 10000^(-2i/d) 和 sinusoidal 完全一样,这是巧合吗?

不是巧合,是 RoPE 论文刻意保留的设计。RoPE 论文里明确说:RoPE 可以被理解为 sinusoidal PE 的一种"向量化"形式,只是把位置信息从 Embedding 转移到了 Q/K 上。两者使用相同的频率分布 θ_i = 10000^(-2i/d),所以低维度高频、高维度低频的特性完全一致。

Q10. ALiBi 里的 slope 是怎么定的?为什么是几何级数?

原论文给了一个固定公式 m_i = 2^(-8 * (i + 1) / H),几何级数是为了让不同头的衰减速度差异覆盖多个数量级。比如 H=4 时 slope 依次为 1/4、1/16、1/64、1/256——最近的头衰减很快、最远的头衰减很慢,让模型同时具备"局部细节"和"远距离依赖"两种能力。slope 是固定的,不可学习。

Q11. 三角 Mask 矩阵的"上三角"是包含对角线还是不包含?

不包含对角线。"上三角(不含对角线)"才是被屏蔽的区域,下三角(含对角线)是允许看到的区域。PyTorch 中 torch.triu(ones, diagonal=1)diagonal=1 就是这个意思——从对角线"上方一行"开始取。如果传 diagonal=0 会包含对角线,模型就连自己都看不到了。

Q12. 为什么要用加 -inf 而不是乘 0

因为 Softmax(-inf) 严格等于 0,而 Softmax(0) 不等于 0。乘法屏蔽让 score=0,但 Softmax 之后这些位置仍然分到少量概率(虽然很小),数学上不完全"看不到"。加法屏蔽让 score=-inf,Softmax 之后权重严格为 0,实现"彻底屏蔽"。工程上常用 -1e9 替代 -inf 防止 fp16 溢出。

Q13. Causal Mask 能不能用 PyTorch 自带的 nn.Transformer.generate_square_subsequent_mask

能,这正是 PyTorch 官方推荐的方式。generate_square_subsequent_mask(L) 返回 (L, L) 的 float mask,下三角(含对角线)= 0,上三角(不含对角线)= -inf,可以直接作为 nn.Transformertgt_mask 参数。在 F.scaled_dot_product_attention 里也接受 float 类型的 attn_mask。

Q14. Causal Mask 和 Padding Mask 能同时用吗?

能,且实际工程中几乎总是同时用。nn.Transformer 中,tgt_mask(causal)形状是 (L, L)tgt_key_padding_mask(padding)形状是 (B, L),两者通过广播机制同时生效:先加 causal mask 屏蔽未来,再加 padding mask 屏蔽 pad token。

Q15. Padding Mask 的 True/False 含义是什么?

True 表示"这是 padding,屏蔽";False 表示"这是真实 token,保留"。PyTorch 的约定是 key_padding_mask[i, j] = True 表示第 i 个样本的第 j 个位置是 padding,需要屏蔽。这与 numpy 中 bool 数组的语义一致。在加法形式下,True 位置会被填成 -inf

Q16. RoPE 和 sinusoidal 在短序列(< 512)上效果有区别吗?

短序列上差异很小,长序列上 RoPE 略优。RoPE 论文的实验显示:在 LLaMA-7B 上,RoPE 和 sinusoidal 在 L=512 时的 perplexity 几乎一致;L=2048 时 RoPE 略优;L=4096 时 RoPE 优势明显。这与 RoPE 的"相对位置天然存在"性质有关——长距离依赖的建模更精准。

Q17. RoPE 怎么支持长度外推?Dynamic NTK 是什么?

RoPE 本身支持一定程度外推,但效果有限;Dynamic NTK 通过动态调整 base 超参(默认 10000)大幅提升外推能力。具体做法是:训练时 base=10000,推理时根据序列长度动态放大 base,例如 L=8192 时用 base=200000、L=32768 时用 base=800000。原理是让低频维度的波长变长,能覆盖推理时更大的位置值。Llama-2 的"上下文长度扩展"就用这个技巧。

Q18. ALiBi 真的能外推 8 倍以上吗?

原论文的实验确实展示了从训练 L=1024 推理 L=8192 仍能保持 perplexity 不急剧恶化。具体数字:在 WikiText-103 上训练 L=1024,sinusoidal 在 L=2048 时 perplexity 就开始爆炸,而 ALiBi 在 L=8192 时仍保持接近训练时的水平。这个性质源于 ALiBi 的偏置项 -m·|i-j| 只跟"相对距离"有关,与绝对位置无关。

Q19. Encoder 和 Decoder 都能用位置编码吗?

都能,Encoder 和 Decoder 都必须加位置编码。Encoder 看到的是源语言序列(如翻译里的英文),需要区分 "dog bites man" 和 "man bites dog";Decoder 看到的是目标语言序列(如翻译里的中文),同样需要位置区分。区别在于:Encoder 双向、无 Mask;Decoder 单向、有 causal Mask。

Q20. 同一份 Q K^T 公式,真的只要加 Mask 就能从 Encoder 跑到 Decoder 吗?

是的,核心公式完全一样,差异只在 Mask 和 Q/K/V 来源。Encoder Self-Attention 是"双向"(不加 Mask),Decoder Masked Self-Attention 是"因果"(加 causal Mask)。再加上 Decoder 还有一个 Cross-Attention 用 Encoder 输出做 K/V,那已经是"另一种 Attention"(Cross-Attention)而不是 Self-Attention 了。

FAQ 总纲

  • 位置编码三方案:sinusoidal(固定公式、相对位置线性表达)、RoPE(旋转 Q/K、内积只剩相对位置)、ALiBi(score 加偏置、外推最强)(Q1~Q10)。
  • Causal Mask 核心:下三角含对角线为 0、上三角 -inf,加法屏蔽(Q11~Q15)。
  • 工程与对比:PyTorch API 齐全、RoPE 短序列上差异小、ALiBi 外推能力强、Encoder/Decoder 差一个 Mask(Q16~Q20)。

九、下一篇预告

位置编码和 Mask 解决了 Self-Attention 的两个补丁,但 Transformer 真正威力在于堆叠多层 + 跨子层连接。下一篇会拆解:

  • Transformer Encoder Block:Self-Attention + Add & Norm + FFN + 残差连接,堆叠 N 层。
  • Transformer Decoder Block:Masked Self-Attention + Cross-Attention + FFN,三段子层的执行顺序。
  • 残差连接与 LayerNorm:为什么必须有它们,否则训练深度 Transformer 梯度消失。
  • Pre-LN vs Post-LN:现代 LLM(Llama / Qwen)都用 Pre-LN,原 Transformer 用 Post-LN,差别在哪。

敬请期待。

posted @ 2026-08-02 17:58  左扬  阅读(15)  评论(0)    收藏  举报