位置编码:从绝对编码到 RoPE 到 NoPE,外推与内插¶
更新日期:2026-04-14
本文目标:覆盖所有主流位置编码方案,理解每种方案的数学原理、外推能力、工程实现,能根据需求选型和配置。
一、为什么需要位置编码¶
Transformer 的 Self-Attention 是 置换不变的 — 打乱输入顺序,输出不变(忽略 causal mask)。但语言是有序的,"狗咬人"≠"人咬狗"。位置编码向模型注入"谁在前谁在后"的信息。
二、位置编码全景¶
| 方法 | 类型 | 编码位置 | 外推能力 | 代表模型 | 状态 |
|---|---|---|---|---|---|
| Sinusoidal | 绝对 | 加到 embedding | 差 | 原始 Transformer | 弃用 |
| Learned APE | 绝对 | 加到 embedding | 无(固定长度) | GPT-2, BERT | 弃用 |
| ALiBi | 相对 | 注意力偏置 | 好 | BLOOM, MPT | 少用 |
| RoPE | 相对 | Q/K 旋转 | 中(可扩展) | LLaMA, Qwen, Mistral, DeepSeek | 主流 |
| NoPE | 无 | 依赖 causal mask | 好 | 研究/部分层 | 新兴 |
| NTK-Aware | RoPE 扩展 | 调整 θ base | 好 | 社区方案 | 常用 |
| YaRN | RoPE 扩展 | NTK + attention scaling | 很好 | Together AI | 常用 |
| LongRoPE | RoPE 扩展 | 非均匀缩放 | 极好 | Phi-3 | 先进 |
| 方法 | 类型 | 编码位置 | 外推能力 | 代表模型 | 状态 |
|---|---|---|---|---|---|
| 2D-RoPE | 空间 | Q/K 二维旋转 | - | Qwen-VL | 多模态专用 |
| CoPE | 上下文 | 依赖内容的位置 | 好 | Meta 研究 | 前沿 |
三、绝对位置编码(APE)¶
3.1 Sinusoidal(原始 Transformer)¶
def sinusoidal_pe(seq_len, d_model):
pe = zeros(seq_len, d_model)
pos = arange(0, seq_len).unsqueeze(1) # [seq_len, 1]
div = exp(arange(0, d_model, 2) * -(log(10000) / d_model)) # [d_model/2]
pe[:, 0::2] = sin(pos * div) # 偶数维: sin
pe[:, 1::2] = cos(pos * div) # 奇数维: cos
return pe
# 使用: x = x + pe[:seq_len] (加到 token embedding 上)
致命问题:位置编码直接加到 embedding 上,被后续层的非线性变换"冲淡"。且长度固定,无法外推。
3.2 Learned APE(GPT-2)¶
# 直接学一个 position embedding table
pos_embed = Parameter(randn(max_len, d_model)) # max_len 固定!
# 使用: x = token_embed + pos_embed[:seq_len]
问题:max_len 训练时固定(如 2048),推理时超过就崩溃。
四、ALiBi(Attention with Linear Biases)¶
4.1 原理¶
不修改 embedding,直接在注意力分数上加一个线性距离惩罚:
def alibi_attention(Q, K, V, n_heads):
scores = Q @ K.T / sqrt(d_k) # [B, H, S, S]
# ALiBi: 每个头一个固定斜率 m_h
# m_h = 2^(-8/H * h), h = 1, 2, ..., H
# 例如 H=8: m = [1/2, 1/4, 1/8, ..., 1/256]
for h in range(n_heads):
m = 2 (-8.0 / n_heads * (h + 1))
for i in range(S):
for j in range(S):
scores[:, h, i, j] -= m * abs(i - j)
# 距离越远,惩罚越大 → 近的 token 更重要
scores = masked_fill(scores, causal_mask, -inf)
return softmax(scores) @ V
4.2 优劣¶
五、RoPE(Rotary Position Embedding)— 最重要¶
5.1 核心数学¶
RoPE 的关键洞见:通过旋转 Q 和 K 向量编码位置,使得 Q·K 的内积只依赖相对位置。
def apply_rope(x, positions, theta=10000.0):
d = x.shape[-1]
# 频率: 低维高频, 高维低频
freqs = 1.0 / (theta (arange(0, d, 2).float() / d))
# freqs[i] = theta^(-2i/d)
# i=0: freq=1 (最高频, 周期=2π)
# i=d/2-1: freq=1/theta (最低频, 周期=2π·theta≈62832)
# 相位角 = 位置 × 频率
angles = positions.unsqueeze(-1) * freqs # [S, d/2]
# 将 x 的相邻维度配对视为复数
x_pairs = x.view(..., d // 2, 2) # [..., d/2, 2]
x_real = x_pairs[..., 0]
x_imag = x_pairs[..., 1]
# 旋转: (a+bi)(cos θ + i sin θ)
cos_t = cos(angles)
sin_t = sin(angles)
out_real = x_real cos_t - x_imag sin_t
out_imag = x_real sin_t + x_imag cos_t
return stack([out_real, out_imag], dim=-1).flatten(-2)
5.2 为什么旋转能编码相对位置¶
设 q_m = RoPE(q, m), k_n = RoPE(k, n)
则 q_m · k_n = q · R(m-n) · k
其中 R(m-n) 是只依赖 m-n 的旋转矩阵
证明(二维情况):
q_m = [q₁cos(mθ) - q₂sin(mθ), q₁sin(mθ) + q₂cos(mθ)]
k_n = [k₁cos(nθ) - k₂sin(nθ), k₁sin(nθ) + k₂cos(nθ)]
q_m · k_n = (q₁k₁ + q₂k₂)cos((m-n)θ) + (q₁k₂ - q₂k₁)sin((m-n)θ)
→ 只依赖 m-n!
这意味着:
- 同一位置 (m=n): cos(0)=1, sin(0)=0 → 内积最大
- 位置差越大: 旋转角度越大 → 内积变化取决于 q,k 的方向
5.3 theta 的意义¶
直觉:theta 越大 → 低频维度旋转越慢 → 能区分的最大位置距离越远 → 但近距离分辨率下降。
六、RoPE 外推/内插方法¶
6.1 问题定义¶
训练长度 L_train(如 4K),推理时需要处理 L_target(如 128K)。位置 > L_train 的 token 的 RoPE 编码超出训练分布 → 模型困惑。
6.2 方法对比¶
6.3 各方法实现¶
# === 1. Position Interpolation (PI) ===
def rope_pi(x, positions, scale):
# 将 [0, L_target] 压缩到 [0, L_train]
positions_scaled = positions / scale # scale = L_target / L_train
return apply_rope(x, positions_scaled)
# 问题: 所有频率统一缩放 → 高频维度分辨率严重下降
# "1,2,3,4" 变成 "0.25, 0.5, 0.75, 1.0" → 相邻位置几乎无法区分
# === 2. NTK-Aware Interpolation ===
def rope_ntk(x, positions, alpha):
# 增大 theta → 降低所有频率 → 但低频降得多,高频降得少
new_theta = theta alpha * (d / (d - 2))
return apply_rope(x, positions, theta=new_theta)
# 好处: 高频信息保留更多
# alpha 怎么选: alpha = (L_target / L_train) 的某个函数
# === 3. Dynamic NTK ===
def rope_dynamic_ntk(x, positions, current_seq_len, L_train):
if current_seq_len <= L_train:
return apply_rope(x, positions) # 训练长度内不改变
else:
alpha = (current_seq_len / L_train) some_power
return rope_ntk(x, positions, alpha)
# 好处: 无需微调! 推理时自动适应
# 问题: 性能不如微调方案
# === 4. YaRN ===
def rope_yarn(x, positions, scale, alpha, beta):
d = x.shape[-1]
freqs = 1.0 / (theta (arange(0, d, 2) / d))
# NTK-by-parts: 将频率维度分为三组
for i, freq in enumerate(freqs):
wavelength = 2 * pi / freq
if wavelength < L_train * beta:
# 高频: 不做任何修改 (它们本来就在训练范围内)
pass
elif wavelength > L_train * alpha:
# 低频: 做完整的 PI 缩放
freqs[i] = freq / scale
else:
# 中间频率: 线性插值 (平滑过渡)
ratio = (wavelength - L_train beta) / (L_train (alpha - beta))
freqs[i] = freq (1 - ratio) + (freq / scale) ratio
# Attention Scaling: sqrt(1/scale) 缩放注意力分数
# 补偿因缩放导致的注意力分布变化
angles = positions.unsqueeze(-1) * freqs
...
# === 5. LongRoPE ===
def rope_longrope(x, positions, rescale_factors):
# rescale_factors: [d/2] — 每个维度一个独立缩放因子
# 通过搜索算法 (进化搜索) 找到最优组合
freqs = 1.0 / (theta (arange(0, d, 2) / d))
freqs = freqs * rescale_factors # 非均匀缩放!
angles = positions.unsqueeze(-1) * freqs
...
# 效果: 仅 1K 步微调 → 2048K tokens
# 代价: 需要搜索最优因子 (计算量不大)