跳转至

位置编码:从绝对编码到 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
    # 代价: 需要搜索最优因子 (计算量不大)

6.4 外推方法选型建议



参考链接

参考链接


上级 · A4. 位置编码:RoPE 原理、扩展方法、2D-RoPE