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 必须加位置信息
- 一个最小实验:打乱顺序,输出不变
- 置换不变性的数学定义
- 三条注入位置信息的思路
- 正弦位置编码(原 Transformer)
- 公式:奇偶维度不同频率
- 为什么用 sin/cos:相对位置可线性表达
- PyTorch 实现 + 频率曲线
- RoPE:旋转位置编码(Llama / Qwen 默认)
- 2 维情形:把"位置"当成"旋转角度"
- d 维推广:block-diagonal 旋转
- 核心推导:Q·K^T 中只出现相对位置
- PyTorch 实现(极简版)
- ALiBi:线性偏置
- 公式:在 score 上加 -m·|i-j|
- 三种方案对比表
- Masked Self-Attention:Decoder 训练的关键
- 为什么 Decoder 不能看未来
- 三角 Mask 矩阵怎么构造
- PyTorch 实现
- Causal Mask vs Padding Mask
- Encoder vs Decoder 的 Attention 边界
- 形状速查:本篇涉及的张量维度
- FAQ 20 问
- 下一篇预告
一、为什么 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 会发生什么?
- 后果 1:bag-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 版本 |
本篇重点讲 sinusoidal、RoPE、ALiBi 三种。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,输出一个由 sin 和 cos 算出来的标量。它的精妙之处在于:不同维度对应不同频率,低维度变化快、高维度变化慢,从而让任意两个位置的编码都不同。
2.1 一句话公式
这里有几个初看反直觉的设计:
- 奇偶维度交替:偶数维度用 sin,奇数维度用 cos,两者刚好拼成一个2 维向量(见 §2.3 旋转解释)。
- 分母 10000 是"最大波长":当 2i = d_model - 1 时,分母达到 10000,对应波长最长的维度;2i = 0 时波长最短(≈ 2π)。
- 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 最大的优势。利用三角恒等式:
这个性质意味着:模型只要在训练时见过某种相对距离 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 的变化:
图 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),所以 i 取 0, 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:
直观上:位置 m 越大,q 在 2 维平面上转过的角度越大。不同位置的 q 在空间中处于"不同朝向",而 Self-Attention 算的是 q 与 k 的内积——这刚好能感知到位置的差异。
图 2:RoPE 旋转位置编码 — 2 维情形的几何直观
把 q_m 和 k_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(分块对角)矩阵。
频率 θ_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 矩阵的转置乘积等于各块分别相乘)。于是:
结论: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 一句话公式
直觉上:距离越远的两个 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/4、2^(-4)=1/16、2^(-6)=1/64、2^(-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 三种位置编码方案对比
| 维度 | sinusoidal | RoPE | ALiBi |
|---|---|---|---|
| 注入位置 | 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+1、i+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 长什么样:

图 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+1 个 0,剩下的全是 -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.Transformer 的 tgt_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。
- 官方 API:nn.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-Attention | Decoder Masked Self-Attention | Decoder 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 走三条路

图 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_cache 的 d 是每个头的维度(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.Transformer 的 tgt_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,差别在哪。
敬请期待。

浙公网安备 33010602011771号