RoPE(Rotary Position Embedding)不像绝对位置编码那样把位置向量加到 token embedding 上,而是对 query 和 key 做位置相关的旋转:表面上用绝对位置 旋转向量,点积后却自然转化为相对位置 ,让位置信息以简洁的方式进入 attention score。
本文依次讲解 RoPE 的基本定义、二维分组与频率设计、相对位移性质、复数视角,并给出 PyTorch 实现。阅读前可先了解 绝对位置编码 中的动机,以及 旋转矩阵基础 中的代数性质。
一、绝对位置编码与旋转位置编码
绝对位置编码先将位置向量加入输入,再进行线性投影。对于位置 :
其中 是 token 的内容向量, 是对应的位置向量,因此 、、 三条路径都会直接携带位置编码。
RoPE 改变了位置注入的顺序:先从内容向量完成线性投影,
再仅对 query 和 key 施加位置相关的旋转。记 ,其中 是 query 和 key 的 head dimension,通常要求为偶数。对于位置 的 query 和位置 的 key:
旋转后的向量用于计算注意力分数:
因此,位置信息通过旋转后的 进入注意力权重,而 本身不执行旋转;不过最终输出仍会受到位置影响,因为这些权重决定了 的加权组合。
二、二维分组与频率设计
1. 高维向量的二维配对
query 向量 按相邻两维分组,写成 个二维子向量:
RoPE 让每个二维子向量独立旋转,对应的 旋转矩阵就是这些二维旋转的拼接,写成块对角形式:
实现中不会显式构造 矩阵——因为绝大多数元素是 ,直接做矩阵乘法会浪费大量算力。工程上通常把旋转展开成逐元素的 乘法,避免稀疏矩阵的无效计算。
其中第 个二维子空间的旋转角度为:
并且:
是第 个二维子空间对应的旋转频率,随着 增大而单调递减。
2. 高频与低频
不同维度组使用不同频率,是 RoPE 能表达多尺度位置关系的关键。
- 当 较小时, 较大,旋转角变化快,适合捕捉短距离关系,也就是高频分量。
- 当 较大时, 较小,旋转角变化慢,适合捕捉长距离关系,也就是低频分量。
三、相对位移性质
1. RoPE 后的注意力分数
原始注意力分数为 。RoPE 旋转后:
由 旋转矩阵基础 中已证明的性质 ,可以直接得到:
2. 相对位置差推导
对第 个二维子空间,有:
也就是说,RoPE 旋转后的注意力分数依赖的是相对位置差 ,而不是单独的 和 。
相对位置信息被隐式内置在旋转操作中,不需要额外的相对位置表。
这一点可以理解为:
- 模型看到的不是两个 token 各自的绝对位置,而是它们在不同频率维度上的相对相位差;
- 这种“旋转差”自然反映 token 之间的相对距离;
- 公式本身可以计算任意位置,因此形式上支持更长位置的外推。
需要注意的是,公式能计算更长位置,不等于模型一定能无损处理任意长上下文。实际外推效果仍受训练长度和频率分布影响。
3. 直观理解
可以把每个二维子空间想象成一个平面:query、key 各自旋转到 ,两者的夹角就是上面推导出的相对相位 。两个 token 的相对距离越近,它们在大多数频率维度上的夹角就越小;距离越远,夹角越大——这就是“旋转差编码相对距离”在几何上的直观图像。
不过这只是提供了一个结构化的位置通道,而不是“距离越近注意力越强”的硬性规定:注意力的实际强弱仍由内容向量和模型参数共同决定。
4. 复数视角
二维旋转与复数乘法等价(证明见 旋转矩阵基础):把 看作复数 ,位置 对第 组的旋转就是让 乘上单位复数 。于是 query 的第 组带上相位 ,key 的第 组带上相位 ;计算相似度时两者共轭相乘,相位相减,得到 ,这正对应矩阵形式中的旋转 。可见复数写法与矩阵写法本质相同,只是把二维旋转压缩成了一次复数乘法,形式更紧凑。
5. 与绝对位置编码的区别
| 方法 | 注入位置 | 位置进入方式 | 相对距离 |
|---|---|---|---|
| 绝对位置编码 | 输入 embedding | 需要模型间接学习 | |
| RoPE | Attention 的 Q/K | 点积中自然出现 |
RoPE 不需要额外 attention bias,也不改变 attention 的矩阵形状,因此在工程上比较简洁。
四、代码实现
1. 生成 cos/sin cache
import torch
def build_rope_cache(seq_len: int, dim: int, base: float = 10000.0, device=None):
index = torch.arange(0, dim, 2, device=device).float()
inv_freq = 1.0 / (base ** (index / dim)) # [dim / 2]
positions = torch.arange(seq_len, device=device).float()
angles = torch.outer(positions, inv_freq) # [seq_len, dim / 2]
angles = torch.repeat_interleave(angles, repeats=2, dim=-1)
cos = torch.cos(angles) # [seq_len, dim]
sin = torch.sin(angles) # [seq_len, dim]
return cos, sin
这里把每个角度重复两次,是为了和原始向量的偶数/奇数维度对齐。
2. rotate_half
二维旋转公式为:
可以写成:
其中:
def rotate_half(x):
x_even = x[..., 0::2]
x_odd = x[..., 1::2]
rotated = torch.stack((-x_odd, x_even), dim=-1)
return rotated.flatten(-2)
3. 应用 RoPE
def apply_rope(q, k, cos, sin, position_ids):
# q, k: [batch, heads, seq_len, head_dim]
# cos, sin: [max_seq_len, head_dim]
cos = cos[position_ids].unsqueeze(1) # [batch, 1, seq_len, head_dim]
sin = sin[position_ids].unsqueeze(1)
q_embed = (q * cos) + (rotate_half(q) * sin)
k_embed = (k * cos) + (rotate_half(k) * sin)
return q_embed, k_embed
这段代码表达的正是: