如果只能选一个词来回答“RoPE(旋转位置编码)为什么管用”,我会毫不犹豫地说是:旋转。很多讲位置编码的文章都止步于“RoPE把位置信息通过旋转矩阵加进去”这句话,但这远远不够。你真正需要理解的是:当位置编码以复数形式参与运算时,它对内积——也就是注意力分数——到底施加了怎样的几何作用?这背后不是玄学,而是一套非常漂亮的数学。这篇文章我会从复数形式的推导出发,把 RoPE 对注意力分数的几何影响一层层剥开,顺便聊聊那些论文里没直接写、但实际推导和调参时一定会遇到的细节。
这一篇对应的是系列问答里的 Q38。坦白讲,第38次被问到“RoPE 的复数形式到底对注意力分数做了什么”,我觉得是时候系统写一篇推导笔记了。它适合三类读者:正在啃 Transformer 源码、想知道为什么位置编码要写成q * e^{imθ}的人;做长文本外推、被“注意力漂移”问题折磨的人;以及纯粹想把内积几何吃透的数学型选手。如果你是第一次接触 RoPE,建议先熟悉基本的欧拉公式,剩下的推导我会尽量做到每一步都有出处。
1. 为什么偏偏是“旋转”:RoPE 要解决的位置编码痛点
1.1 绝对位置编码的尴尬:编码进去了,但用不上
在 RoPE 出现之前,最主流的做法是绝对位置编码——把位置 $p$ 直接加进 embedding,或者加进每一层的输入。Transformer 原论文用的是正弦函数,后来 BERT 用可学习的绝对位置向量。这类方法的问题不在于“能不能区分不同位置”,而在于区分的方式和注意力机制本身的计算逻辑是脱节的。
注意力分数的核心是内积:$q \cdot k$。内积天然关注的是两个向量之间的“相对关系”——夹角小、方向接近,分数就高。但绝对位置编码是把位置信息以加法形式揉进向量里的,位置 $m$ 和位置 $n$ 的关系并不是显式地以 $(m-n)$ 的形式出现。模型要隐式地从绝对位置里学出相对关系,这需要额外的参数和训练量。更麻烦的是,一旦序列长度超过训练时的最大长度,绝对位置编码直接“没见过的位置”就出现了,外推能力几乎为零。
所以研究者们一直在找一种方案:能不能让位置编码的效果直接体现在内积结果里,并且让这个内积结果只和相对位置 $(m-n)$ 有关?这就是相对位置编码的动机,RoPE 是其中做得最彻底、数学形式也最优雅的一个。
1.2 为什么“加法不行,乘法才行”
如果我们把位置信息通过乘法注入向量,例如 $q_m = q \cdot r_m$、$k_n = k \cdot r_n$,那么内积就会变成:
$$ \langle q_m, k_n \rangle = \langle q \cdot r_m, k \cdot r_n \rangle $$
如果 $r_m$ 和 $r_n$ 是某种“好”的运算,使得上式能化简为仅依赖 $(m-n)$ 的函数,那我们就实现了相对位置编码的目标。什么样的运算是“好”的?
答案藏在复数乘法里。复数乘法有一个美妙的几何特性:乘以一个模长为 1 的复数,就是旋转。而旋转有一个关键性质——两个向量各自旋转之后再求内积,等于其中一个向量先旋转“相对角度”再做内积。具体来说,对于两个复数 $q e^{im\theta}$ 和 $k e^{in\theta}$,它们的内积(复共轭后相乘)是:
$$ \langle q e^{im\theta}, k e^{in\theta} \rangle = q e^{im\theta} \cdot \overline{k e^{in\theta}} = q \bar{k} e^{i(m-n)\theta} $$
看到了吗?位置 $m$ 和 $n$ 消失了,剩下的只有它们的差 $(m-n)$。这就是“旋转”的魔力:加法做不到的事情,乘法(旋转)做到了。这也是在整个 RoPE 推导中,我要你始终记住的一句话——位置信息以旋转的方式参与,最终以相位差的形式出现在内积里。
2. 从欧拉到矩阵:RoPE 复数形式的完整推导
2.1 复数形式下,旋转位置编码的“定义式”
设词向量对应的 query 和 key 的二维形式为 $q = [q_0, q_1]$、$k = [k_0, k_1]$,把它们看成复数 $q = q_0 + i q_1$、$k = k_0 + i k_1$。RoPE 的核心操作是对 $q$ 和 $k$ 各施加一个与位置相关的旋转:
$$ f_q(q, m) = q \cdot e^{im\theta}, \quad f_k(k, n) = k \cdot e^{in\theta} $$
其中 $\theta$ 是预先设定的旋转角度基频。这时注意力分数(内积的实部)变成:
$$ \text{score}(m, n) = \text{Re}\left[ q \bar{k} e^{i(m-n)\theta} \right] $$
这个式子就是整个 RoPE 复数形式的“源点”。它揭示了三件事:
- 第一,注意力分数只依赖于位置差 $(m-n)$,绝对位置被完全消掉;
- 第二,分数本质上是在原始内积 $q\bar{k}$ 的基础上,做了一个角度为 $(m-n)\theta$ 的旋转投影;
- 第三,$e^{i(m-n)\theta}$ 的实部 $\cos((m-n)\theta)$ 和虚部 $\sin((m-n)\theta)$ 会分别调制 $q\bar{k}$ 的实部与虚部。
2.2 拆开实部和虚部:从复数到旋转矩阵
如果你不习惯复数,可以把上面的式子拆回实数形式。把 $q\bar{k}$ 记为 $a + bi$,其中 $a = \text{Re}(q\bar{k})$、$b = \text{Im}(q\bar{k})$。那么:
$$ q\bar{k} e^{i(m-n)\theta} = (a + bi)(\cos\Delta\theta + i\sin\Delta\theta) $$
其中 $\Delta = m-n$。展开并取实部:
$$ \text{score}(m,n) = a\cos(\Delta\theta) - b\sin(\Delta\theta) $$
这个形式和向量点乘 $\begin{bmatrix} a \ b \end{bmatrix} \cdot \begin{bmatrix} \cos(\Delta\theta) \ -\sin(\Delta\theta) \end{bmatrix}$ 是完全一致的。也就是说,旋转位置编码对一对维度施加的效果,等价于先把 key 对应的向量反向旋转 $\Delta\theta$,再和 query 做标准内积。这跟我们前面说的“两个向量都旋转,等于一个向量旋转相对角度”是同一件事。
在实际实现中,我们并不是真的在复数域做乘法,而是用一个旋转矩阵来操作:
$$ R(\Delta\theta) = \begin{bmatrix} \cos(\Delta\theta) & -\sin(\Delta\theta) \ \sin(\Delta\theta) & \cos(\Delta\theta) \end{bmatrix} $$
也就是:
$$ \text{score}(m,n) = q^T R(\Delta\theta) k $$
这是 RoPE 最常见的向量形式,也是源码里rotate_half函数的数学依据。
2.3 高维扩展:逐对维度旋转与频率分配
上面只讨论了二维的情况。实际 Transformer 的 head 维度通常是 64、128 这样的偶数。RoPE 的处理方式是:把 $d$ 维向量分成 $d/2$ 对,每一对用一个独立的旋转角 $\theta_i$,其中:
$$ \theta_i = 10000^{-2i/d}, \quad i = 0, 1, \dots, \frac{d}{2} - 1 $$
于是完整的内积变成 $d/2$ 个二维内积的累加:
$$ \text{score}(m, n) = \sum_{i=0}^{d/2-1} \text{Re}\left[ q_i \bar{k}_i e^{i(m-n)\theta_i} \right] $$
每一对维度都在自己的二维子空间里旋转,但这些旋转的频率不同——低维度($i$ 小)对应高频旋转,高维度($i$ 大)对应低频旋转。这个“不同频率”的设计是 RoPE 能够像正弦编码一样表达多尺度位置关系的核心。
这里我要补一个源码里才会看到的细节:rotate_half的实现并不是按“相邻两维一组”来旋转的。代码中是把向量分成前一半和后一半,然后交错恢复成 $[q_0, q_1, \dots]$ 和 $[-q_{d/2}, \dots, -q_{d-1}]$ 的形式。数学上这两者等价,但如果你自己手推,用相邻配对的方式更容易理解几何含义,用代码的切分方式更容易对照源码。
3. 旋转如何改写注意力分数:几何层面的逐项拆解
3.1 内积的几何意义:模长、方向、夹角
在进入 RoPE 的几何影响之前,先回到一个最基础的事实:两个向量的内积等于“模长乘积 × 夹角余弦”:
$$ q \cdot k = |q| |k| \cos\phi $$
其中 $\phi$ 是两个向量的夹角。注意力分数的大小由两个因素决定:一个是两个向量各自的长度(模长),另一个是它们的夹角。模长反映的是“这个 token 本身的强弱”,夹角反映的是“两个 token 在语义空间中的方向一致性”。
RoPE 的旋转操作不改变模长——旋转是等距变换,$|R(\Delta\theta)q| = |q|$。它改变的只有夹角。这就引出一个重要的几何结论:RoPE 对注意力分数的影响是纯“方向性”的,它不会因为位置编码而放大或缩小 token 本身的能量。
3.2 相对距离如何调制注意力:从矢量投影看“旋转相位差”
有了模长不变的结论,注意力分数在 RoPE 下的几何意义就变得非常清晰。我们把 $q$ 旋转到角度 $\phi_q + m\theta$ 的方向,把 $k$ 旋转到角度 $\phi_k + n\theta$ 的方向,那么两者夹角变成:
$$ \phi' = (\phi_q + m\theta) - (\phi_k + n\theta) = (\phi_q - \phi_k) + (m-n)\theta $$
注意力分数为:
$$ \text{score}(m,n) = |q||k|\cos\left[(\phi_q - \phi_k) + (m-n)\theta\right] $$
这个式子透露出非常直观的几何行为:
- 当 $m = n$ 时,位置贡献为 0,分数退化为原始语义内积 $|q||k|\cos(\phi_q - \phi_k)$;
- 当 $|m-n|$ 逐渐增大时,余弦函数的自变量会以 $(m-n)\theta$ 的步长线性偏移;
- 当 $(m-n)\theta$ 超过 $\pi$ 时,余弦值可能从正变负,也就是说,原本语义相近的两个 token 会因为距离过远而被抑制。
一个具体例子:设 $\theta = 0.1$,两个 token 的语义夹角 $\phi_q - \phi_k = 0$(完全同向),那么距离为 8 时,$\cos(0.8) \approx 0.696$,分数还行;但距离为 16 时,$\cos(1.6) \approx -0.029$,注意力几乎消失;距离为 32 时,$\cos(3.2) \approx -0.998$,分数变成强烈的负值。这就是“相对距离调制注意力”的几何本质。
3.3 二维配对与语义维度混合:每个子空间都在独立旋转
上面说的是单个二维配对上的行为。整个 head 的注意力分数是 $d/2$ 个配对子空间分数之和。因为 $\theta_i$ 是逐对不同的,所以不同维度对的相位偏移速度不一样。
说个更直白的几何图像:如果 $d=8$,那么有 4 对子空间,每对都是一个小的复平面。query 在这个复平面里的投影长度可能很短,在另一个复平面里可能很长。距离 $m-n$ 增大时,第一对子空间旋转得快(对应 $\theta_0 = 1$),第二对旋转得慢(对应 $\theta_1 = 0.01$),等等。最终的注意力分数是所有“旋转后投影”之和。
这意味着:每个 token pair 的注意力分数,是由多个频率的余弦波的加权叠加决定的。低位维度负责精细的短距离位置感知,高位维度负责粗粒度的长距离位置感知。不同频率叠加之后,整体会出现振荡衰减的现象——这为下一节的长距离衰减埋下了伏笔。
4. 长距离衰减不是偶然:振荡求和与狄利克雷核
4.1 从单维度到完整注意力分数:求和带来的“抵消效应”
如果只有一对维度,那么注意力分数就是一个单纯的余弦函数,距离越大越振荡,没有衰减。但因为 RoPE 有 $d/2$ 对维度,且频率从 $\theta_0$ 到 $\theta_{d/2-1}$ 指数递减,完整分数是一个多频率余弦的叠加:
$$ S(\Delta) = \sum_{i=0}^{d/2-1} \text{Re}\left[ q_i \bar{k}_i e^{i\Delta\theta_i} \right] $$
假设 $|q_i| = |k_i| = 1$ 且 $q_i\bar{k}_i = 1$(所有维度同相位),那么:
$$ S(\Delta) = \sum_{i=0}^{d/2-1} \cos(\Delta\theta_i) $$
问题变成:不同频率的余弦波叠加后,振幅如何随 $\Delta$ 变化?直观上,低频项($\theta_i$ 小)变化慢,高频项($\theta_i$ 大)变化快。当 $\Delta$ 增大时,高频项开始快速振荡,它们的正负号频繁交替,互相抵消;低频项虽然仍在贡献正值,但数量少。整体结果是:$S(\Delta)$ 的峰值出现在 $\Delta$ 较小时,随着 $\Delta$ 增大,净分数被限制在一个围绕 0 的带内振荡。
这个效应在数学上有一个很强的类比——狄利克雷核。它是一系列同频余弦的求和形式,会出现主瓣集中在原点附近、旁瓣衰减振荡的特征。RoPE 的注意力分数正是这种“准狄利克雷核”的行为。
4.2 平均场近似的解析边界:$|\sum e^{i\Delta\theta_i}|$ 的上界
为了更严谨地说明“为什么不会随距离无限累积”,考虑把所有 $q_i\bar{k}_i$ 都设为 1 的简化情况。注意力分数(去掉 Re 之前)是:
$$ \sum_{i=0}^{d/2-1} e^{i\Delta\theta_i} $$
当 $\theta_i = 10000^{-2i/d}$ 时,这是一个关于 $i$ 的等比级数。记 $r = e^{i\Delta \cdot 10000^{-2/d}}$,则:
$$ \sum_{i=0}^{d/2-1} e^{i\Delta\theta_i} = \frac{1 - r^{d/2}}{1 - r} $$
其模长上界为:
$$ \left|\sum_{i=0}^{d/2-1} e^{i\Delta\theta_i}\right| \le \frac{2}{|1 - e^{i\Delta \cdot 10000^{-2/d}}|} = \frac{1}{|\sin(\Delta \cdot 10000^{-2/d} / 2)|} $$
注意这个上界与 $d$ 无关(在等比级数视角下),只与 $\Delta$ 和底数 10000 有关。当 $d$ 增大、$\theta_0$ 更接近 1 时,高频部分的抵消会更充分,但整体仍被 $1/\sin$ 形的包络约束。这就是为什么 RoPE 的长距离注意力分数会落在一个有界区域里振荡,而不是线性增长或发散。
4.3 “注意力漂移”的几何解释:长距离外推时,相位混沌开始出现
热词里有“rope 导致注意力漂移吗”,我直接给结论:RoPE 本身不会导致注意力“漂移”,但它会在长距离外推时引发一种特殊的相位混沌。
什么叫相位混沌?训练时模型见过的距离 $\Delta$ 大多在 0 到训练长度 $L$ 之间。对于高频维度($\theta_i$ 接近 1),$\Delta\theta_i$ 可以快速跨越多个 $2\pi$ 周期。模型在训练中学习到的注意力模式,是这些多频率分量在“已见距离”上的稳定叠加。但当推理时的 $\Delta$ 远超 $L$,高频分量的相位已经进入完全不可预测的周期状态——你不知道 $\cos(\Delta\theta_i)$ 落在哪个区间。而低频率项的贡献太弱,无法压住高频率项的随机振荡。
结果就是:长距离外推时,注意力分数不再遵循训练时学到的平滑衰减规律,而是变成一个类似噪声的振荡函数。这不是“漂移”,而是“相位失稳”。解决思路也因此非常清晰:要么调整频率分配(如 YaRN 的 NTK 缩放),要么直接改变旋转角的基础频率,让高频分量在目标长度内不要转满太多圈。
5. 工程视角下的 RoPE:维度配对、θ_i 取值的隐藏学问
5.1 为什么底数是 10000?频率分布怎么影响感受野
$\theta_i = 10000^{-2i/d}$ 里面那个 10000,和 Transformer 原论文正弦位置编码里的 10000 如出一辙。这个数的选择不是拍脑袋,它决定了各维度旋转频率的跨度。
拿 $d=64$ 举例:
- $i=0$: $\theta_0 = 1$,对应周期 $2\pi \approx 6.28$,最高频;
- $i=31$: $\theta_{31} = 10000^{-62/64} \approx 0.00135$,对应周期约 4654,最低频。
也就是说,最低频维度在距离约 4654 时才会转完一整圈,最高频维度在距离 6.28 时已经转完一圈。这样的跨频率设计让 RoPE 同时具备“短距离高分辨率”和“长距离不饱和”的能力。
如果你希望模型支持更长的序列,一个直接的思路是增大底数(比如从 10000 调到 500000),这会降低所有频率,让高频维度在给定距离内转的圈数变少,从而减小相位混沌风险。这也是 NTK 类缩放方法的核心思想之一。
5.2 不同 head、不同层的旋转角:一个常被忽略的细节
你在看源码时可能会发现,很多实现里所有 head 共用同一组 $\theta_i$。但严格来说,这并非必须。有些改进工作会对不同 head 采用不同的底数或偏移,让不同的 head 有不同的位置感受野——有的 head 专注短距离依赖,有的 head 专注长距离建模。
一个实用的建议是:在做长文本微调时,不要只调底数,还要观察每个 head 的平均注意力距离。如果某个 head 在短距离任务上表现很好,但长距离总是学不动,可以考虑对它的 $\theta_i$ 做分组缩放,而不是一刀切。
5.3 完整的 RoPE 实现:从 NumPy 到推理优化的细节
最后给一个快速可跑的参考实现,用 NumPy 实现 RoPE 前向传播的核心部分。这只是教学用,让你看清维度的组织方式:
import numpy as np def precompute_rope_angles(d_model, seq_len, base=10000.0): # d_model 必须是偶数 assert d_model % 2 == 0 # 每对维度一个频率, shape = (d_model // 2,) freqs = 1.0 / (base ** (np.arange(0, d_model, 2) / d_model)) # 位置索引 shape = (seq_len, 1) positions = np.arange(seq_len)[:, None] # angles shape = (seq_len, d_model // 2) angles = positions * freqs[None, :] return angles def apply_rope(q, k, angles): # q, k shape: (batch, seq_len, num_heads, head_dim) # 这里仅处理单条序列单头的情况 seq_len = q.shape[1] cos = np.cos(angles)[None, :, :] # (1, seq_len, head_dim//2) sin = np.sin(angles)[None, :, :] # (1, seq_len, head_dim//2) def rotate(x): # 把 x 转成两半, 再拼回去 x1 = x[..., : x.shape[-1] // 2] x2 = x[..., x.shape[-1] // 2 :] # 旋转操作: (x1*cos - x2*sin, x1*sin + x2*cos) rotated = np.concatenate( [x1 * cos - x2 * sin, x1 * sin + x2 * cos], axis=-1 ) return rotated q_rot = rotate(q) k_rot = rotate(k) return q_rot, k_rot一个容易踩的坑:rotate里 x1 取前一半、x2 取后一半,这是源码常见的做法。它和数学上“第 0 维和第 1 维配对”并不完全一致,但结果等价,因为这种切分方式等价于把配对顺序重新排列了。你只要保证训练和推理用的切分方式一致即可。
5.4 实测经验:RoPE 在 FP16 下的数值稳定性
RoPE 涉及大量的正弦余弦计算。在 FP16 混合精度训练时,余弦值在接近 $\pm 1$ 时精度尚可,但在角度接近 $\pi/2$、余弦值接近 0 时,相对误差会变大。这个问题在和旋转矩阵的乘法叠加后,可能导致梯度噪声。
我的建议是:旋转角度的计算最好在 FP32 下完成,再 cast 到 FP16/FP32 参与后续矩阵乘。如果你用的是 PyTorch 之类的框架,它会自动处理大部分情况;但如果你在写自定义 CUDA kernel,这个问题一定要留意。我们曾经在长序列训练时遇到过 loss 抖动,排查到最后就是 RoPE 角度在 FP16 下的累积误差导致的,把角度计算改成 FP32 之后就稳定了。
6. 关于“注意力漂移”的讨论与实测经验
6.1 哪些情况下你会真正感知到 RoPE 的几何行为异常
前面讲了相位混沌的理论背景,实际操作中,它通常表现为三种可观测现象:
- 注意力分数的距离-分数曲线出现反常尖峰:某个远距离位置对当前 token 的注意力异常高,不是语义原因,而是相位叠加导致的偶发现象;
- 长文本困惑度(perplexity)在某个长度处突然恶化:这通常是高频维度在这个距离上“转满”了 $2\pi$ 的整数倍,导致位置区分度丢失;
- 外推长度增加时,模型出现重复生成:位置信息对某些 head 失效后,模型只能靠语义信息“猜”当前位置,容易陷入循环。
6.2 调参经验:如何处理 4K 到 32K 的长度外推
做长文本外推时,我试过三种方案,简单分享效果:
| 方案 | 操作 | 效果 | 注意点 |
|---|---|---|---|
| 线性内插 | 把位置索引除以缩放因子 $\lambda$ | 短距离性能略降,但 4K→8K 可稳定 | 需要几万步微调;提升空间有限 |
| NTK 感知缩放(YaRN 风格) | 修改底数 $base' = base \times \lambda^{d/(d-2)}$ | 长距离保持较好,短距离几乎不掉点 | 高频和低频的平衡需要按 head 调试 |
| 随机截断 + 位置丢包 | 训练时随机丢掉部分位置编码 | 稳定常用方案,但效果提升中等 | 综合性不如前两者,适合作为辅助 |
我最常用的组合是:NTK 风格底数修改 + 少量步数的位置插值微调。实测在 4K 训练、32K 推理的任务上,perplexity 能控制在可接受范围。底数修改时,如果 $d$ 是 64,$\lambda=8$,那么新的 base 约等于 $10000 \times 8^{64/62} \approx 10000 \times 8.1 \approx 81000$。不要凭感觉乱设,直接按公式来。
6.3 站在几何视角重新审视 RoPE:它到底“学会”了什么
最后说一点我个人的理解。RoPE 的复数形式之所以优雅,是因为它把位置编码从“附加特征”变成了“算子”。它不是往向量里加信息,而是对向量的方向做了旋转操作。注意力分数因此获得了显式的相对位置调制,这个调制是刚性的——不依赖训练数据的分布,只要你给定 $(m-n)$,相位差就确定了。
但刚性也带来脆弱性:在训练长度之外,刚性的旋转会变成无约束的混沌。所以 RoPE 从来不是一个“安装完就不管”的组件。它需要你理解它的频率分配、理解它的相位行为、理解它和长度的关系。你越懂它的几何,越知道在长文本外推、微调、混合精度、kernel 优化这些环节里该往哪个方向使劲。
如果你接下来要做 RoPE 的源码复现或者长文本优化,我建议你先把 $d=4$ 或 $d=8$ 的完整前向用 NumPy 手写一遍,把每一对维度的余弦值随位置的变化画出来。画完那几张图,你对 RoPE 的直觉会比读十篇论文都管用。