一句话: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 向量本身 |