RoPE 旋转位置编码详解

作者:

一、一句话概览

绝对位置编码是「加法」——把位置向量加到 token 上;RoPE 是「旋转」——用位置角度去旋转每一层 Attention 的 Q 和 K 向量。

二、绝对位置编码的回顾

2.1 原版 Transformer 的 Sin/Cos 编码

对位置 pos 和维度索引 i(0 ≤ i < d/2):

PE(pos, 2i)   = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

然后将 PE(pos) 加到 token embedding 上:

x_pos = Embedding(token) + PE(pos)

2.2 问题

问题原因
破坏语义位置信号直接加到语义向量上,两者混在一起
外推差训练时只见过 0~2047 的位置,推理到 4096 时参数模型没学过
相对位置是间接的需要模型自己从 sin/cos 加法公式里推导相对关系

三、RoPE 的设计思路

3.1 目标约束

找到一个函数 f(q, k, m, n),使得 Attention Score 只依赖相对位置 (n-m),且不破坏语义向量。

写成数学形式:

Attention_Score(m, n) = <g(q_m, m), g(k_n, n)>
                        其中 g 是只依赖位置的变换
                     = 只依赖于 (n-m)

3.2 推导过程

第 1 步:将位置信息施加在 Q/K 上,而非输入的 embedding 上

q_m' = g(q_m, m)
k_n' = g(k_n, n)

这样语义向量本身不受影响,位置信号只在 Attention 计算时生效。

第 2 步:假设变换是线性的(乘以矩阵)

q_m' = R_m · q_m
k_n' = R_n · k_n

内积 = (R_m·q_m)^T · (R_n·k_n) = q_m^T · R_m^T · R_n · k_n

为了让内积只依赖 (n-m),需要:

R_m^T · R_n = R(n-m)

第 3 步:寻找满足条件的矩阵

2D 旋转矩阵完美满足:

R(θ) = [cos θ  -sin θ]
       [sin θ   cos θ]

性质:R(θ_m)^T · R(θ_n) = R(θ_n - θ_m)

验证:

R(θ_m)^T · R(θ_n) = R(-θ_m) · R(θ_n) = R(θ_n - θ_m)

如果让 θ_pos = pos × ω(角度与位置成正比),那么:

R_m^T · R_n = R(n·ω - m·ω) = R((n-m)·ω)    ← 只含 n-m

第 4 步:推广到高维

Q 和 K 不是二维的(通常 128 维)。解决方案:把 d 维向量切成 d/2 个二维块,每块独立旋转:

[ q0, q1, q2, q3, q4, q5, ..., q126, q127 ]
 └─┬─┘ └─┬─┘ └─┬─┘         └───┬───┘
  对0    对1    对2           对63

每对在自己的 2D 平面内独立旋转

每个块的旋转角和频率:

对 i 的旋转角 = pos × θ_i
其中 θ_i = 10000^(-2i/d)

频率设计直接复用了原始 Transformer sin/cos 编码的参数。

第 5 步:验证内积确实只剩相对位置

Score = (R(m)·q_m)^T · (R(n)·k_n)
      = Σ_i [q_m^(2i), q_m^(2i+1)] · R((n-m)·θ_i) · [k_n^(2i), k_n^(2i+1)]^T

每个块的旋转角度只依赖 (n-m)·θ_i,与绝对位置 m、n 无关。

四、发现过程:从问题到方案的完整叙事

前面第三节展示了 RoPE 的数学推导步骤,但只列步骤不看动机,就像读答案不看草稿——看不出设计者的思考过程。本节还原完整的发现历程。

4.1 当时的研究困境(2020-2021)

RoPE 诞生前,位置编码方案已经探索了好几年:

方案代表原理致命缺陷
绝对位置(可学习)BERT,GPT-1每个位置一个向量,当 embedding 训练位置数固定,外推 = 零
Sin/Cos 绝对位置原版 Transformer用三角函数算固定值外推差,相对位置是间接的
相对位置偏置T5,Transformer-XL在 Attention 矩阵上加偏置项无法外推,偏置表尺寸受限
ALiBi论文 “Train Short, Test Long”按距离加线性衰减简单但精度不如 RoPE
可学习相对位置DeBERTa加可学习的相对位置嵌入参数量大,外推差

各方案的共同困境:要么破坏语义(加法类),要么不能外推(偏置类),要么计算量大(可学习类)。

4.2 核心诉求

作者(苏剑林等)把需求提炼成一个精确的数学命题:

是否存在一种变换,使得 Attention Score 天然只含相对位置,同时:
1. 不修改 token embedding(保持语义纯度)
2. 不增加可学习参数(泛化到任意长度)
3. 计算效率高(可融入现有 Attention 流程)

4.3 为什么选择在 Q/K 上做变换?

这是第一个关键决策。前人都在两个地方加位置:

选项 A:加在 x(embedding)上 → 破坏语义
选项 B:加在 Attention 矩阵上(偏置项)→ 不能外推

作者选择了一个前人没试过的第三条路

选项 C:加在 Q 和 K 上(Attention 计算前一步)

理由很巧妙——Attention Score 本身就是 q^T · k 的内积,如果在 Q 和 K 上分别施加位置相关的变换,内积就有可能天然产生相对位置。而且 Q/K 在每一层都重新计算,不绑定到输入层。

4.4 为什么假设变换是线性的?

g(q, pos) 出发,最自然的简化是假设它是某种线性变换(即乘以矩阵):

q' = R(pos) · q

为什么这么假设?

  • 可解析性:线性变换有丰富的数学工具可用(特征值、正交性、群论)
  • 计算效率:矩阵乘法是 GPU 最擅长的事
  • 已有的工作支撑:相对位置编码的论文已经暗示过”q 和 k 的某种变换可以编码位置”

这一步没有天才的直觉——它就是逐次尝试最简单方案的自然选择。如果线性不成立,再考虑非线性。

4.5 关键突破:发现旋转矩阵恰好满足约束

代入线性假设后,内积变成:

Score = q_m^T · R_m^T · R_n · k_n

为了让 Score 只依赖 (n-m),需要:

R_m^T · R_n = 某个只依赖 (n-m) 的矩阵

现在问题变成了纯线性代数:什么样的矩阵满足 “相邻两个的乘积只依赖差”?

这个条件等价于:

R_m 构成一个群,且群运算是矩阵乘法
R_m^T · R_n 只依赖 n-m

在 2×2 矩阵中逐一排查:

  • 对角矩阵?D_m^T · D_n = D_m·D_n → 依赖 m+n,不满足
  • 三角矩阵?T_m^T · T_n → 结构复杂,不满足
  • 正交矩阵?O_m^(-1) · O_n → 依赖 n-m 当且仅当 O_pos 是旋转矩阵!

这就是那一下”叮”的时刻——2D 旋转矩阵的转置等于逆矩阵,而两个旋转矩阵的乘积 = 角度相加。

R(θ_m)^T · R(θ_n) = R(-θ_m) · R(θ_n) = R(θ_n - θ_m)

旋转角相减 = 只剩下相对角度。只需要设定 θ_pos = pos × ω(角度与位置成正比),就完美得到:内积只依赖 pos_n - pos_m,即相对位置。

4.6 为什么 n-m 直接落到内积里就够了?

这其实是整个设计最精妙的一步。

Attention 的语义是”当前词应该关注序列中的哪些词”。如果两个词的距离 n-m 直接编码在 Attention Score 中,那模型就不需要从绝对位置去推算距离——距离本身就是输入信号的一部分。

类比:你不需要知道自己在屋子里的绝对坐标 (3.2, 5.7) 才能判断”门在你左边 2 米”——因为你直接感知的是相对距离。RoPE 让 Attention 同样直接感知相对距离。

4.7 为什么是用复数旋转而不是其他正交变换?

线性代数中,2D 空间的保距变换只有两类:纯旋转镜面反射

反射矩阵虽然也满足 det=1 和正交性,但反射会改变向量的手性(左手系变右手系),对于语义向量的表示是不必要的扰动。旋转则不改变任何额外属性,是最干净的正交变换

另外,旋转还有复数表示:

R(θ) 等价于 e^(iθ) = cos θ + i sin θ

这使得计算极其高效——直接在复数域上做逐元素乘法即可,不需要构造完整的 2×2 矩阵。现代实现(如 LLaMA 的 xformers)正是利用了这一点。

4.8 推广到高维的巧思

二维旋转搞定了,但 Q/K 是 128 维的,怎么推广?

作者的做法简单到近乎粗暴:

把 128 维切成 64 个二维块,每个块独立旋转。

频率设计直接复用原版 Transformer 的 θ_i = 10000^(-2i/d)——这不是随手选的,而是为了让 RoPE 退化回 sin/cos 编码的频谱特性,保证和已有工作的兼容性。

高低频分工的效果:

  • 低频维度转得快(θ_0 ≈ 1.0)→ 位置差 1 就会转 ~57°,向量变化剧烈 → 擅长分辨相邻词
  • 高频维度几乎不转 → 对位置不敏感 → 保留语义信息不被位置污染
  • 中间的维度 → 在局部和全局感知之间平滑过渡

4.9 发现过程总结:关键决策点

问题:如何让 Attention 只依赖相对位置?
  ├─ 前人试过 A "加在 x 上" → 破坏语义 ❌
  ├─ 前人试过 B "加在 Attention 矩阵上" → 不能外推 ❌
  └─ 作者选了 C "加在 Q/K 上" → 新路子 ✓
       ├─ 线性变换?先试最简单的——假设乘矩阵 ✓
       │    └─ 约束:R_m^T · R_n = R(n-m)
       │       ├─ 对角矩阵? → 不满足 ❌
       │       ├─ 三角矩阵? → 不满足 ❌
       │       └─ 旋转矩阵? → 恰好满足!!✓✓✓ (核心突破)
       ├─ 推广到高维?
       │    └─ d 维切成 d/2 对,每对独立旋转 ✓
       └─ 每对的频率?
            └─ 复用 sin/cos 的 10000^(-2i/d) ✓ (兼容现有工作)

RoPE 之所以漂亮,不是因为某一步特别天才,而是因为每一步都是顺着问题约束自然推演——先定目标,再排除不行的方法,留下的那一条恰好是对的。

五、为什么每层都要施加 RoPE?

4.1 对比:施加的位置和层次

绝对位置编码RoPE
作用对象Token Embedding(只在输入层)Q 和 K 向量(每一层 Attention)
位置信号加到语义向量上,混在一起通过旋转角度编码,不改变向量模长

4.2 为什么 RoPE 每层都需要?

Attention 是每一层独立计算的。如果第 i 层的 Q/K 不包含位置信息,这层的 Attention 就无法利用位置做推理。

浅层 Q/K:关注局部语法、相邻词关系 → 旋转让它知道"谁在附近"
深层 Q/K:关注全局语义、长距离依赖 → 旋转让它知道"谁离得远"

位置信息通过所有层逐层传递和强化。

4.3 每层的旋转矩阵值相同吗?

R(pos) 只依赖位置索引 pos 和全局频率 θ_i,不依赖层数:

R(pos) 对同一 token 在所有层中完全相同
pos=0 时 R(0) = 单位矩阵,等于没有旋转(位置 0 作为原点)

变的是被旋转的 Q/K 向量本身——每层的 W_Q/W_K 不同,输入 x 也不同。

六、直观理解:频率分层

5.1 为什么每对的旋转角度不同?

维度对θ_ipos=2 时的旋转角直觉
(0,1) 高频≈ 1.0≈ 115°转得猛 → 擅长感知”谁紧挨着我”
(2,3) 中频≈ 0.05≈ 6°转得适中
(63,64) 极低频≈ 0.00001≈ 0°几乎不转 → 对位置不敏感,专注语义

5.2 类比:钟表

秒针(高频):转得飞快 → 区分 t=0s 和 t=1s,捕捉瞬间变化
时针(低频):几乎不动 → 区分 12:00 和 13:00,感知大范围位置
─────────────────────────────────────────────────────────
两者同时看,就能精确得知当前时间

同理:高频对负责感知局部邻居关系,低频对负责感知长距离段落位置,两者结合完整编码位置信息。

七、为什么 RoPE 外推能力强?

推理长度超过训练长度时,需要让模型适应没见过的大位置。RoPE 支持多种扩展策略:

方法做法原理
线性插值将所有位置除以缩放因子 s(pos’ = pos/s)把大位置映射回训练范围内,但高频信息丢失
NTK-aware 插值低频维度多缩放,高频维度少缩放保留高频局部信息的同时扩展低频全局感知范围
YaRNNTK + 温度调参在 NTK 基础上进一步优化,是目前效果最好的方案

核心优势来自 RoPE 的相对位置本质——模型学到的是「两个 token 相距多远」,而不是「token 在第几个位置」。相对距离的概念是连续的、可泛化的。

八、实现伪代码

def apply_rope(q, k, position_ids, dim, theta=10000.0):
    """
    q, k: [batch, heads, seq_len, dim]
    position_ids: [seq_len]
    """
    # 计算每对维度的频率
    freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
    # 每对维度的旋转角 = 位置 × 频率
    angles = position_ids.unsqueeze(-1) * freq.unsqueeze(0)  # [seq_len, dim/2]
    cos = angles.cos().unsqueeze(-1)  # [seq_len, dim/2, 1]
    sin = angles.sin().unsqueeze(-1)

    # 把 q, k 拆成 [..., dim/2, 2] 的形状,每对的 (d_2i, d_2i+1)
    q = q.view(*q.shape[:-1], -1, 2)
    k = k.view(*k.shape[:-1], -1, 2)

    # 2D 旋转: [x, y] → [x·cos - y·sin, x·sin + y·cos]
    q_rot = torch.stack([
        q[..., 0] * cos - q[..., 1] * sin,
        q[..., 0] * sin + q[..., 1] * cos
    ], dim=-1)
    k_rot = torch.stack([
        k[..., 0] * cos - k[..., 1] * sin,
        k[..., 0] * sin + k[..., 1] * cos
    ], dim=-1)

    return q_rot.flatten(-2), k_rot.flatten(-2)

九、对比总结

维度绝对位置编码 (Sin/Cos)RoPE
操作方式x = emb + PE(加法,输入层)q' = R(pos)·q(旋转,每层 Attention)
作用对象Token EmbeddingQ 和 K 向量
对语义影响破坏语义向量旋转是正交变换,不改变向量模长
位置编码性质绝对位置相对位置(旋转差 = n-m
外推能力强,配合 NTK/YaRN 可扩至 128K
代表模型原版 Transformer、BERT、GPT-1/2LLaMA 全系列、Qwen、DeepSeek、Mistral、ChatGLM

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注