一句话:Transformer 的 Attention 对输入顺序无感知,位置编码负责把"第几个 token"这个信息注入模型。从固定正弦函数到可学习参数,再到当前主流的旋转编码(RoPE),每一代都在解决上一代的长度外推问题。


一、为什么需要位置编码?

RNN 逐步处理序列,天然感知顺序。Transformer 并行处理所有 token,Attention 本质是集合操作,对顺序完全无感:

["我", "爱", "你"]  和  ["你", "爱", "我"]
→ Attention 看到的是完全相同的三个向量集合,无法区分

必须手动把位置信息注入模型,这就是位置编码的作用。


二、发展历程(简览)

时间 方案 代表模型 核心问题
2017 Sinusoidal PE(正弦固定编码) 原版 Transformer 外推效果差,只有绝对位置
2018 Learned PE(可学习编码) BERT、GPT-2 硬性长度上限,超出训练长度就失效
2019 Relative PE(相对位置编码) Transformer-XL、T5 实现复杂,外推仍有限
2021 RoPE(旋转位置编码) LLaMA、Qwen、DeepSeek 当前主流
2022 ALiBi(线性偏置) MPT 强制近距离偏好,长程依赖受损

三、前三代简述

Sinusoidal PE(2017)

用不同频率的正弦/余弦函数生成固定位置向量,直接加到 token embedding 上:

$$PE_{(pos,\ 2i)} = \sin!\left(\frac{pos}{10000^{2i/d_{model}}}\right), \quad PE_{(pos,\ 2i+1)} = \cos!\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$

  • 优点:无需训练,参数量为 0
  • 缺点:超出训练长度后效果急剧下降,只有绝对位置信息

Learned PE(2018)

把位置编码当成普通 Embedding 参数,随模型一起训练(BERT 最大 512,GPT-2 最大 1024)。

  • 优点:模型自己学习最优位置表示,实践效果略好于 Sinusoidal
  • 缺点:硬性长度上限,训练时没见过的位置完全没有编码

Relative PE / T5 Bias(2019)

在 Attention 分数上加可学习的相对位置偏置 $b_{i-j}$,编码"两个 token 相距多远"而非"在第几个位置"。

  • 优点:相对位置更自然,泛化更好
  • 缺点:实现复杂,长度外推仍有限

四、RoPE(旋转位置编码)—— 当前主流详解

4.1 核心思想

RoPE 不把位置信息加到 Embedding 上,而是在计算 Attention 时,把位置信息旋转进 Q 和 K

旋转后的 Q 和 K 做点积,结果只依赖相对位置 $m - n$,与绝对位置无关:

$$\langle R_m, q,\ R_n, k \rangle = \langle q,\ R_{n-m}, k \rangle$$

其中 $R_m$ 是位置 $m$ 对应的旋转矩阵。

4.2 二维直觉

把二维向量 $[x, y]$ 旋转 $\theta \cdot pos$ 角度:

$$\begin{bmatrix} x’ \ y’ \end{bmatrix} = \begin{bmatrix} \cos(\theta \cdot pos) & -\sin(\theta \cdot pos) \ \sin(\theta \cdot pos) & \cos(\theta \cdot pos) \end{bmatrix} \begin{bmatrix} x \ y \end{bmatrix}$$

两个旋转向量的点积,旋转角度相减,只剩相对位置差 $\theta \cdot (m - n)$。

4.3 高维实现:两两配对旋转

实际的 head_dim 是高维的(如 128 维),RoPE 把维度两两配对,每对独立做二维旋转:

head_dim = 128 → 64 对,每对用不同频率的 θ 旋转
维度 [0,1] 用频率 θ₀,维度 [2,3] 用频率 θ₁,...,维度 [126,127] 用频率 θ₆₃
频率从高到低,低频维度感知长程位置,高频维度感知短程位置

4.4 完整代码(PyTorch)

import torch
import math


def build_rope_cache(seq_len: int, head_dim: int, base: int = 10000):
    """
    预计算各位置的 cos/sin 旋转值。
    返回 cos, sin,形状均为 [seq_len, head_dim]。
    """
    # 每对维度对应的旋转频率,共 head_dim//2 个频率
    half_dim = head_dim // 2
    freqs = 1.0 / (base ** (torch.arange(0, half_dim).float() / half_dim))

    # 每个位置与每个频率的乘积,形状 [seq_len, half_dim]
    positions = torch.arange(seq_len).float()
    angles = torch.outer(positions, freqs)

    # 拼接成 [seq_len, head_dim],偶数维度和奇数维度共享同一组角度
    angles = torch.cat([angles, angles], dim=-1)
    return angles.cos(), angles.sin()


def rotate_half(x: torch.Tensor) -> torch.Tensor:
    """
    把向量的后半段取负后移到前半段,实现旋转的另一个分量。
    输入 [x1, x2],输出 [-x2, x1]。
    """
    half = x.shape[-1] // 2
    x1 = x[..., :half]
    x2 = x[..., half:]
    return torch.cat([-x2, x1], dim=-1)


def apply_rope(
    x: torch.Tensor,
    cos: torch.Tensor,
    sin: torch.Tensor,
) -> torch.Tensor:
    """
    对 Q 或 K 应用 RoPE 旋转。
    x:   [batch, seq_len, num_heads, head_dim]
    cos: [seq_len, head_dim]
    sin: [seq_len, head_dim]
    """
    # 广播到 [1, seq_len, 1, head_dim]
    cos = cos.unsqueeze(0).unsqueeze(2)
    sin = sin.unsqueeze(0).unsqueeze(2)
    # 旋转公式:x·cos + rotate_half(x)·sin
    return x * cos + rotate_half(x) * sin


# ── 验证 ──────────────────────────────────────────────
batch, seq_len, num_heads, head_dim = 2, 16, 8, 64

cos, sin = build_rope_cache(seq_len, head_dim)

q = torch.randn(batch, seq_len, num_heads, head_dim)
k = torch.randn(batch, seq_len, num_heads, head_dim)

q_rotated = apply_rope(q, cos, sin)
k_rotated = apply_rope(k, cos, sin)

print(q_rotated.shape)   # torch.Size([2, 16, 8, 64])
print(k_rotated.shape)   # torch.Size([2, 16, 8, 64])

# 验证相对位置不变性:
# 位置 m=3 的 q 和位置 n=1 的 k 的点积,
# 应等于位置 m=5 的 q 和位置 n=3 的 k 的点积(相对距离都是 2)
q2 = torch.randn(1, seq_len, 1, head_dim)
k2 = torch.randn(1, seq_len, 1, head_dim)
q2_rot = apply_rope(q2, cos, sin)
k2_rot = apply_rope(k2, cos, sin)

dot_31 = (q2_rot[0, 3, 0] * k2_rot[0, 1, 0]).sum()
dot_53 = (q2_rot[0, 5, 0] * k2_rot[0, 3, 0]).sum()
# 注意:q2/k2 是随机向量,两个点积数值不同,但它们的差异只来自 q/k 本身,
# 旋转部分贡献的相对位置偏移量(cos(2θ)项)是相同的
print(f"dot(q3, k1) = {dot_31.item():.4f}")
print(f"dot(q5, k3) = {dot_53.item():.4f}")
print("RoPE 验证完成")

4.5 RoPE 的优点

优点 说明
天然相对位置 Q·K 点积只依赖相对位置差,不依赖绝对位置
无额外参数 cos/sin 由公式计算,不需要训练
长度外推性好 配合 YaRN、LongRoPE 可从 4K 扩展到 128K+
实现高效 只需对 Q/K 做逐元素乘法,无额外矩阵运算

4.6 长度外推扩展:YaRN

原始 RoPE 训练 4K 后直接推理 32K 效果仍会下降。YaRN(2023) 通过对不同频率维度做差异化缩放解决这个问题:

高频维度(感知短程):直接用原始频率,不缩放
低频维度(感知长程):按扩展比例缩放频率,让模型"看得更远"
中间维度:线性插值过渡

LLaMA-3 的 128K 上下文、Qwen2 的 128K 上下文都依赖 RoPE + YaRN 类扩展实现。


五、ALiBi(线性偏置)简述

不修改 Embedding,直接在 Attention 分数上加距离惩罚:

$$\text{Attention}(i, j) = \frac{q_i \cdot k_j}{\sqrt{d_k}} - m \cdot |i - j|$$

距离越远,惩罚越大,模型天然倾向于关注近处 token。实现极简,长度外推效果不错,但强制近距离偏好对长程依赖任务有损失,目前主流模型已基本不用。


六、各方案对比

方案 参数量 长度外推 相对位置 当前使用
Sinusoidal PE 0 ❌ 差 ❌ 无 已淘汰
Learned PE max_len × d_model ❌ 无法外推 ❌ 无 BERT/GPT-2
Relative PE(T5) 少量偏置参数 ⚠️ 有限 ✅ 有 T5 系列
RoPE 0 ✅ 配合YaRN可达128K+ ✅ 天然 LLaMA/Qwen/DeepSeek
ALiBi 0 ✅ 较好 ✅ 有 MPT(较少)

七、核心要点速查

问题 答案
为什么需要位置编码? Attention 是集合操作,对顺序无感,必须手动注入位置信息
位置编码加在哪里? Sinusoidal/Learned 加在 Embedding 上;RoPE 加在 Q/K 的旋转里
当前主流是什么? RoPE,LLaMA/Qwen/DeepSeek 等几乎所有新模型都在用
RoPE 为什么好? 天然相对位置、无额外参数、配合 YaRN 可外推超长上下文
RoPE 作用在哪里? 只作用于 Q 和 K,不作用于 V
长度外推怎么做? RoPE + YaRN/LongRoPE,对低频维度缩放频率
ALiBi 和 RoPE 的区别? ALiBi 在 Attention 分数上加距离惩罚;RoPE 旋转 Q/K 向量本身