一、一句话概览
绝对位置编码是「加法」——把位置向量加到 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 为什么每对的旋转角度不同?
| 维度对 | θ_i | pos=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 插值 | 低频维度多缩放,高频维度少缩放 | 保留高频局部信息的同时扩展低频全局感知范围 |
| YaRN | NTK + 温度调参 | 在 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 Embedding | Q 和 K 向量 |
| 对语义影响 | 破坏语义向量 | 旋转是正交变换,不改变向量模长 |
| 位置编码性质 | 绝对位置 | 相对位置(旋转差 = n-m) |
| 外推能力 | 弱 | 强,配合 NTK/YaRN 可扩至 128K |
| 代表模型 | 原版 Transformer、BERT、GPT-1/2 | LLaMA 全系列、Qwen、DeepSeek、Mistral、ChatGLM |
发表回复