位置编码:从绝对编码到 RoPE 到 NoPE,外推与内插¶
更新日期:2026-04-14
本文目标:覆盖所有主流位置编码方案,理解每种方案的数学原理、外推能力、工程实现,能根据需求选型和配置。
一、为什么需要位置编码¶
Transformer 的 Self-Attention 是 置换不变的 — 打乱输入顺序,输出不变(忽略 causal mask)。但语言是有序的,"狗咬人"≠"人咬狗"。位置编码向模型注入"谁在前谁在后"的信息。
二、位置编码全景¶
| 方法 | 类型 | 编码位置 | 外推能力 | 代表模型 | 状态 | 为什么 |
|---|---|---|---|---|---|---|
| Sinusoidal | 绝对编码:位置信息与内容无关,按固定函数生成 | 加到 token embedding 上,与内容向量直接相加 | 差:sin/cos 函数虽然数学上可外推,但加法注入方式导致位置信号在深层被非线性变换稀释,超出训练长度后注意力模式崩溃 | 原始 Transformer (Vaswani et al., 2017) | 已被后续方案全面取代,仅存于教学和历史文献中 | 最早的无参数位置编码尝试;证明了正弦函数的频率分解思路可行,但加法注入的信噪比问题促使了后续相对编码的发展 |
| Learned APE | 绝对编码:每个位置学习一个独立的 embedding 向量 | 加到 token embedding 上,训练时通过反向传播优化 | 无法外推:位置 embedding 表大小在训练时固定(如 2048),超出该长度的位置没有对应的 learned vector,推理直接报错 | GPT-2, BERT, ViT | 已被后续方案全面取代,GPT-3 之后的模型均不再使用 | 思路最直接——让模型自己学位置表示;但固定长度的 lookup table 天生无法泛化到更长序列,参数量随 max_len 线性增长 |
| ALiBi | 相对编码:不修改 embedding,通过注意力分数的距离偏置隐式编码位置 | 在注意力得分矩阵上加线性距离惩罚 bias,不改变 Q/K/V | 外推能力强:线性衰减 bias 对超出训练长度的位置自然延续,远距离 token 被逐渐压制,注意力分布保持合理 | BLOOM (176B), MPT-7B/30B | 被部分模型采用但未成为主流,已被 RoPE 系方案在多数场景超越 | 设计初衷是完全免参数地支持长度外推;线性 bias 思路优雅但每头斜率固定(2^(-8/H)),无法学习不同距离的非线性依赖模式 |
| RoPE | 相对编码:通过旋转 Q/K 向量使内积只依赖相对位置差 m-n | 在 Q 和 K 上施加位置相关的旋转变换,不修改 V | 中等但可扩展:原始 theta=10000 在 ~8K 以内表现良好,超出后需配合 NTK/YaRN 等扩展方案 | LLaMA ½/3, Qwen, Mistral, DeepSeek, Gemma | 当前绝大多数开源和闭源 LLM 的标准选择 | 旋转变换保持向量范数不变(正交变换),内积天然只依赖相对位置;频率分解结构允许后续通过调整 theta/缩放因子灵活扩展上下文窗口 |
| NoPE | 无显式位置编码:完全依赖 causal attention mask 的三角结构提供顺序信息 | 不注入任何位置信号,causal mask 本身隐含"只能看到前面的 token"这一顺序约束 | 外推能力强:没有位置编码就不存在超出训练分布的问题,长度泛化取决于 attention pattern 本身 | 学术研究实验、部分混合架构中的子层 | 新兴研究方向,已有论文证明在部分层去除 PE 不损失性能甚至有益 | 研究发现 Transformer 的 causal mask 已隐式编码了位置信息(位置 0 只能 attend 自己,位置 1 能看到 2 个 token...),显式 PE 可能是冗余的 |
| NTK-Aware | RoPE 扩展:通过增大 theta base 在频域上非均匀拉伸旋转频率 | 修改 RoPE 的 theta 参数,使低频维度降频更多、高频维度基本保留 | 外推能力强:高频维度保留了近距离分辨率,低频维度的拉伸覆盖了更长距离,兼顾两端 | 社区方案 (Reddit/GitHub),被多个推理框架集成 | 广泛用于推理阶段的免微调外推场景 | 核心洞见:PI 的均匀缩放会破坏高频(近距离)信息,而 NTK 参数化可以在保留高频的同时只拉伸低频,类似于神经正切核的频率缩放行为 |
| YaRN | RoPE 扩展:NTK-by-parts 分频段处理 + attention score 缩放补偿 | 将频率维度分三组(高频不动、低频做 PI、中间线性插值),并对注意力分数乘 sqrt(1/scale) | 外推效果很好:分频段策略避免了高频信息损失,attention scaling 补偿了缩放导致的熵增大问题 | Together AI, Mistral 社区微调 | 需要少量微调时的高性价比外推方案 | 分析发现不同频率维度在外推中的作用不同——高频编码近距离语法关系不可破坏,低频编码远距离依赖需要拉伸,中间平滑过渡避免不连续性 |
| LongRoPE | RoPE 扩展:通过进化搜索为每个频率维度找到独立的最优缩放因子 | 对 d/2 个频率维度分别赋予搜索得到的非均匀 rescale factor | 外推效果极好:每个维度的缩放因子都是针对目标长度优化的,避免了手工规则的次优性,仅 1K 步微调可达 2048K | Microsoft Phi-3, Phi-3.5 | 目前已知外推能力最强的 RoPE 扩展方案 | 手工设计的缩放规则(NTK/YaRN)仍是近似最优,用搜索算法直接在 loss landscape 上找每个维度的最佳缩放因子,以极小微调代价达到 SOTA |
| 方法 | 类型 | 编码位置 | 外推能力 | 代表模型 | 状态 | 为什么 |
|---|---|---|---|---|---|---|
| 2D-RoPE | 空间位置编码:将一维旋转扩展到二维平面,分别对 (row, col) 施加旋转 | 将 Q/K 的维度拆成两半,前半编码行位置、后半编码列位置,各自独立旋转 | 在二维空间内有效,不涉及一维长度外推 | Qwen-VL, 部分 Vision Transformer 变体 | 多模态视觉-语言模型的专用位置编码方案 | 图像 patch 天然具有二维空间结构,一维 RoPE 无法表达上下左右的空间邻近关系,2D 旋转让模型能学习水平和垂直方向的空间依赖 |
| CoPE | 上下文相关位置编码:位置不由索引决定,而由注意力权重加权求和动态计算 | 通过 attention score 对位置索引加权求和,得到依赖内容的"软位置",再用该位置计算位置编码 | 外推能力强:位置由内容决定而非硬编码索引,对未见过的绝对位置天然鲁棒 | Meta FAIR 研究论文 (2024) | 前沿研究阶段,尚未被生产模型大规模采用 | 传统 PE 假设"第 5 个 token"总是意味着相同的位置关系,但实际语言中位置的意义依赖上下文(如计数、跳过标点等),CoPE 让位置本身成为可学习的函数 |
三、绝对位置编码(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
# 代价: 需要搜索最优因子 (计算量不大)
6.4 外推方法选型建议¶
参考链接¶
参考链接¶
↑ 上级 · A. 基础理论