跳转至

位置编码:从绝对编码到 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. 基础理论