专栏此前的《一篇读懂 Attention》讲过:注意力就是每个 token 拿自己的 Q 去和所有 K 算相似度、再加权求和 V。但当时留了个尾巴没展开——这套计算完全不认识 token 的顺序。今天补上这一课:位置编码,重点讲清它现在的标准答案 RoPE(Rotary Position Embedding,旋转位置编码)。
一、它解决什么问题
自注意力是「集合运算」:把「猫吃鱼」打乱成「鱼吃猫」喂进去,输出只是跟着重排,注意力本身不会察觉语义天翻地覆——这叫排列等变性。所以顺序信息必须额外注入,这是位置编码的职责。
2018 到 2021 年间的两种主流做法都是绝对位置编码:给每个绝对位置 m 发一个向量,加到词向量上。
- 学习式(GPT-2、BERT):位置向量当参数训练。简单有效,但序列长度在训练时就焊死了——位置表只有 1024 行,第 1025 个 token 无处安放,更谈不上外推。
- 正弦式(Transformer 原论文):用不同频率的 sin/cos 生成,不占参数。原作者希望「位置 m+k 的编码能表示成位置 m 编码的线性函数」,但这个近似只停留在编码向量层面;注意力实际消费的是 Q·K 内积,交叉项里混着 m+n 一类的绝对量,相对位置信息在内积里是失真的。
于是问题变成:有没有一种注入方式,既能像绝对编码一样「来一个位置算一个」,又让注意力内积天然只依赖相对距离?2021 年 3 月,苏剑林在博客给出答案,同年与团队发表 RoFormer 论文,这就是 RoPE。
二、它怎么工作:二维复数就能讲透
先把设计目标写死。 RoPE 不动词向量,而是作用在注意力内部的 Q 和 K 上:找一个函数 f,使得
⟨f(q, m), f(k, n)⟩ = g(q, k, m−n)
即 q 在位置 m、k 在位置 n 时,无论 m、n 具体是多少,内积只随差值 m−n 变化。这一步是「绝对式注入、相对式生效」的分水岭。
二维情形用复数解。 把二维向量 q 看成复数 x + yi,令位置 m 处的编码为
f(q, m) = q · e^(imθ)
由欧拉公式 e^(imθ) = cos(mθ) + i·sin(mθ),乘上它的几何效果就是把向量旋转 mθ 角度——「旋转位置编码」由此得名。
现在算内积(复数内积取共轭):
⟨f(q, m), f(k, n)⟩ = Re( q · k* · e^(i(m−n)θ) )
其中 k* 是 k 的共轭。写成极坐标形式一目了然:
= |q| · |k| · cos( ∠q − ∠k + (m−n)·θ )
绝对位置 m 和 n 整个消失了,只剩差值 m−n。旋转不改变向量长度,两个向量的夹角被加上同一个相位差,内积便只由相对距离决定。注意这全是乘法带来的:位置以 e^(imθ) 的形式乘进向量,两处位置在内积里一减,绝对量自然消去。
等价的实数形式是一个 2×2 旋转矩阵 R(mθ):把二维向量 (x, y) 转成 (x·cos mθ − y·sin mθ, x·sin mθ + y·cos mθ)。矩阵视角下性质同样干净——R(mθ)ᵀ·R(nθ) = R((n−m)θ),于是 (R(mθ)q)ᵀ·(R(nθ)k) = qᵀ·R((n−m)θ)·k,与复数推导殊途同归。
高维推广:两两一组,各转各的。 head_dim 维向量拆成 d/2 组,每组占据一个二维平面,配一个专属频率:
θ_i = base^(−2i/d),i = 0, 1, …, d/2−1(base 常取 10000)
第 0 组频率是 1,转得最快;越靠后频率越低,最后一组几乎原地不动。像时钟的秒针、分针、时针:低维高频分辨近处的细粒度位置差,高维低频标记远距离,一个绝对位置由 d/2 个转速不同的指针的刻度组合唯一确定。
实现只需十几行 numpy(本文这段在本地跑通过):
def rope_rotate(x, positions, theta=10000.0):
# x: (seq_len, head_dim),positions: (seq_len,)
half = x.shape[-1] // 2
i = np.arange(half)
freq = theta ** (-2.0 * i / x.shape[-1]) # 各组旋转频率 theta_i
ang = positions[:, None] * freq[None, :] # 角度 = m * theta_i
cos, sin = np.cos(ang), np.sin(ang)
x1, x2 = x[..., :half], x[..., half:] # 前一半配后一半(HF rotate_half 惯例)
return np.concatenate([x1 * cos - x2 * sin,
x1 * sin + x2 * cos], axis=-1)
def attention_scores(Q, K, pos_q, pos_k, theta=10000.0):
Qr = rope_rotate(Q, pos_q, theta)
Kr = rope_rotate(K, pos_k, theta)
return Qr @ Kr.T / np.sqrt(Q.shape[-1])
(原论文把相邻两个维度配成一对,HuggingFace 的 rotate_half 把前一半与后一半配对,两者只是配对约定不同,性质等价。)
验证:整体平移位置,注意力分数纹丝不动。 「分数只依赖相对距离」意味着把所有 token 的位置同时加一个偏移——等价于长对话里「从中间某处继续生成」——注意力应当毫无察觉:
rng = np.random.default_rng(0)
seq_len, head_dim = 16, 64
Q = rng.normal(size=(seq_len, head_dim))
K = rng.normal(size=(seq_len, head_dim))
pos = np.arange(seq_len)
S1 = attention_scores(Q, K, pos, pos)
for shift in (7, 1000, 100000):
S2 = attention_scores(Q, K, pos + shift, pos + shift)
print(f"shift={shift:>6} max|delta| = {np.abs(S1 - S2).max():.3e}")
# 对照:只推 Q 的位置(相对距离变了),分数必须变
S3 = attention_scores(Q, K, pos + 1000, pos)
print(f"relative changed, max|delta| = {np.abs(S3 - S1).max():.3f}")
float64 下实测:三次平移的最大偏差分别是 2.0e-15、4.9e-14、3.6e-12,全是浮点精度量级;而相对距离改变后偏差达 3.76。**「绝对位置编码的形式,相对位置编码的效果」**由此得到实验确认——这也是 RoPE 一切长上下文玩法的根基。
三、它为什么成了事实标准
2023 年 LLaMA 采用 RoPE 后,它成了开源大模型的默认配置:Qwen、DeepSeek、Mistral 等主流架构清一色 RoPE。原因有三:
- 实现干净:只旋转 Q/K,不碰 V 和残差流,KV cache 不需要存任何位置向量;推理时来一个新位置就转一次,天然适配增量解码。
- 理论红利:原论文用 Abel 求和证明内积随相对距离增大有衰减趋势(远程衰减),且 RoPE 可迁移到线性注意力。
- 位置是连续函数:这是与学习式编码的本质区别——位置可插值、频率可缩放,为长上下文扩展留好了旋钮。
代价也藏在这里:训练没见过的位置会让注意力崩掉。PI 论文在 LLaMA 上做过对照——直接外推位置索引,注意力出现「灾难性的高分」,模型彻底失灵。三代补救方案(点到为止):
- 位置插值 PI:把位置除以缩放因子 s 再喂给 RoPE,硬把长序列压回训练区间。LLaMA 2 从 2k/4k 扩到 32k 只需 1000 步微调,理论分析显示插值的上界比直接外推小约 600 倍。
- NTK-aware 缩放:不做均匀压缩——低维高频多压、高维低频少压,兼顾近处刻度不挤坏与远处分辨率不丢失。这项社区工作让 LLaMA 不微调就能用到 8k 以上,困惑度几乎不伤。
- YaRN:按维度分段的插值(NTK-by-parts)加注意力温度修正,效率较此前方法再提一档——少用 10 倍 token、2.5 倍训练步数。
这个旋钮如今直接暴露在配置文件里:Llama 3 把 rope_theta 从默认 10000 提到 500000 以支撑 8k 窗口;Qwen3 密集模型用 1000000,配合 YaRN 做到百万级上下文;DeepSeek-V3 的 MLA 注意力因 KV 被压缩进低维隐向量、装不下位置信息,还专门加了「解耦 RoPE」的额外维度来补位置。
四、边界在哪
- 频率分配是权衡,不是定理。 低维高频分辨近邻,但距离差一大就转过头(周期混叠),远处位置在这些维度近乎随机;base 调太大,高维在训练长度内转不满一圈、学不到东西;调太小,远距分辨率又不够。苏剑林试过把频率当可学习参数,结果几乎不动——说明固定配比已够用,但这也意味着它不会自己适应任务。
- 「塞得下」不等于「用得好」。 各类缩放扩展解决的是注意力分数不崩;超长上下文里中段信息利用率低的问题依然存在(此前《上下文窗口不是越长越好》专门聊过,不展开)。
- 与 ALiBi 的取舍。 ALiBi(BLOOM、MPT 采用)不走编码路线,直接在注意力分数上减一个随距离线性增长的惩罚:实现极简、开箱外推友好,但「近处偏好」是硬编码先验,不可插值。RoPE 把位置表示成连续可变换的函数,扩展手段丰富、生态成熟——它赢得主流,不是每个单点最优,而是在表达力、可扩展性与实现成本之间综合最划算。
一句话收束:RoPE 的全部魔法是把「位置」从加法的修饰变成乘法的旋转——于是绝对的表达方式里,长出了相对的灵魂。
参考资料
- RoFormer: Enhanced Transformer with Rotary Position Embedding(Su et al., arXiv:2104.09864)
- 苏剑林《Transformer升级之路:2、博采众长的旋转式位置编码》(2021,RoPE 原始推导)
- Extending Context Window of Large Language Models via Positional Interpolation(arXiv:2306.15595)
- YaRN: Efficient Context Window Extension of Large Language Models(arXiv:2309.00071)
- HuggingFace Transformers Llama 文档(RoPE 配置与实现说明)
- NTK-Aware Scaled RoPE(bloc97, Reddit r/LocalLLaMA, 2023)
读者留言
COMMENTS 暂无还没有留言,来说第一句?