旋转位置编码
为什么?
位置编码在Transformer论文中被提出, 它对于整个Transformer结构非常重要。

为什么要有位置编码?
整个Transformer的核心是Attention机制,对于Attention机制来说,一个输入句子,其单词不再是顺序输入,而是一次性输入一个序列中的所有词,依靠纯粹的自注意力机制来捕获词之间的联系,直接对这个序列整体进行特征变换。也就是说注意力操作是一种全局操作,可以捕捉句子中较长的依赖关系,序列中每两个元素都能无视其绝对位置和相对位置而进行信息交换,从而计算输入序列中的每个元素与整个序列的注意力权重参考博客来理解,Attention为什么和位置无关 。
然而,词语的顺序对于理解语义至关重要。词序的不同往往会导致句意的完全改变。例如,“我爱你” 与 “你爱我”,虽然包含相同的词,但含义却截然不同。
那么如何解决这个问题?
由于自注意力机制本身是位置无关(permutation-invariant)的,我们必须显式地将位置信息引入模型中。为了解决这个问题,Transformer 的原始论文引入了 位置编码(Positional Encoding) 的概念。其核心思想是:为输入序列中的每个位置分配一个唯一的向量,作为该位置的“身份标识”,并将其与词向量相加,使模型能够识别出每个词在序列中的位置。
换句话说:
-
每个词有一个词向量(Word Embedding),编码其语义。
-
每个位置有一个位置向量(Position Embedding),编码其顺序。
-
两者相加后作为 Transformer 的最终输入。
通过这种方式,Transformer 模型不仅能够理解词之间的语义关系,还能感知它们的排列顺序。
总结来说,位置编码解决了 Attention 机制中缺乏顺序建模能力的问题,使 Transformer 能够在进行信息交互的同时考虑词序,从而更好地理解语言的结构与语义。
位置编码
对于位置向量来说,有两种形式的类型,一种是绝对位置编码,一种是相对位置编码。绝对位置编码是将特征的绝对位置引入,也就是位置向量只和特征所在的位置相关。相对位置编码并没有完整建模每个输入的位置信息,而是在算Attention的时候考虑当前位置与被Attention的位置的相对距离,由于自然语言一般更依赖于相对位置,所以相对位置编码通常也有着优秀的表现。我们这里并不深入讨论绝对位置编码和相对位置编码,感兴趣可以参考这里
绝对位置编码
这里只讨论Transformer论文中提出的三角函数式,也就是正弦位置编码,它是绝对位置编码中的一种形式。
正弦位置编码定义为
\[\begin{aligned} \text{PE}_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right) \\ \text{PE}_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right) \\ \end{aligned}\]其中:
- \(pos\) 表示位置索引(如第 0 个 token、第 1 个 token)
- \(i\) 表示维度索引(第 0 维、第 1 维……)
- \(d_{model}\) 是词向量的维度,比如 512 或 768
Transformer中为什么采用这种方式?论文中提到Transformer作者尝试了两种编码方法:
-
从数据中学习:学习式是“learned and fixed”,即“Postional Embedding”。位置向量是在训练过程中从数据中学习到的,它只能表征有限长度内的位置,无法对任意位置进行建模。
-
正弦函数:公式方法是Position Encoding,此方法使用正弦函数为每个位置构建唯一的嵌入。算出来的可以接受更长的序列长度而不必受训练的干扰.
两种方法取得的效果都差不多。因为通过公式来计算更简单、参数量也更小,所以Transformer作者选择了第二种。 更详细的内容可以参考博客
这个公式一开始理解起来可能不是很直观,用一个例子说明会更加的好理解一些:

正弦位置编码的相对位置关系证明
正弦位置编码中有个非常好的性质是可以反应出相对位置关系。意思是对于相对位置间距\(k\),\(PE_{(pos, k)}\)是\(PE_{(pos)}\)的线性组合,也就是\(PE_{(pos, k)}\)可以用\(PE_{(pos)}\)计算得到。它能反应出相对位置关系,也就是说,你可以通过对向量进行某种变换来捕获相对位移的信息,这是一种隐式的相对位置编码能力。它之所以成立,是因为这些正余弦函数具有良好的平移不变性质。但是它并不显式建模相对位置信息,因此不是一种相对位置编码。
为什么还需要旋转位置编码(ROPE)
在自然语言中,词语的相对顺序远比它们在句子中的绝对位置更重要:
-
“我爱你”和“你爱我”虽然使用了相同的词,但含义完全不同。
-
“the cat sat on the mat” 与 “on the mat sat the cat” 的结构不同但语义接近。
这些例子说明,我们真正想建模的,是词与词之间的相对距离或位置关系。
而正余弦位置编码仅提供绝对位置,不能显式让注意力机制关注 “两个 token 相差几个位置”。这使得模型需要学习去推断相对关系,效率低,效果也不稳。
RoPE 的设计目的正是解决这一点 —— 它让两个 token 在 dot-product 过程中,自然地体现出它们之间的相对距离(角度差),从而在注意力分数中直接注入相对位置信息。
同时绝对位置编码因为不具有外推性,超出训练分布的绝对位置会产生 编码偏移或误导,导致性能下降。 而 RoPE 是通过复数旋转的形式编码频率,即使输入变长,旋转角度依然是连贯的,具有天然的 可扩展性 和 外推能力。
是什么?
旋转位置编码定义
在RoPE中,出发点就是“通过绝对位置编码的方式实现相对位置编码”,这样做既有理论上的优雅之处,也有实践上的实用之处,比如它可以拓展到线性Attention中就是主要因为这一点。
现在进行旋转位置编码的推导,推导基本参考了苏神的博客Transformer升级之路:2、博采众长的旋转式位置编码
为了达到这个目的,我们假设通过下述运算来给 $\boldsymbol{q}, \boldsymbol{k}$ 添加绝对位置信息:
\begin{equation} \tilde{\boldsymbol{q}}_m=\boldsymbol{f}(\boldsymbol{q}, m), \quad \tilde{\boldsymbol{k}}_n=\boldsymbol{f}(\boldsymbol{k}, n) \end{equation}
也就是说,我们分别为 $\boldsymbol{q}, \boldsymbol{k}$ 设计操作 $\boldsymbol{f}(\cdot, m), \boldsymbol{f}(\cdot, n)$ ,使得经过该操作后,$\tilde{\boldsymbol{q}}_m, \tilde{\boldsymbol{k}}_n$ 就带有了位置 $m, n$的绝对位置信息。Attention的核心运算是内积,所以我们希望的内积的结果带有相对位置信息,因此假设存在恒等关系:
\begin{equation} \langle\boldsymbol{f}(\boldsymbol{q}, m), \boldsymbol{f}(\boldsymbol{k}, n)\rangle=g(\boldsymbol{q}, \boldsymbol{k}, m-n) \end{equation}
所以我们要求出该恒等式的一个(尽可能简单的)解。求解过程还需要一些初始条件,显然我们可以合理地设 $f(q, 0)=q$ 和 $f(k, 0)=k$ 。
我们先考虑二维情形,然后借助复数来求解。在复数中有 $\langle\boldsymbol{q}, \boldsymbol{k}\rangle=\operatorname{Re}\left[\boldsymbol{q} \boldsymbol{k}^*\right]$ , $\operatorname{Re}[]$ 代表复数的实部\(\begin{aligned}& q=r_1(\cos \alpha+i \sin \alpha) \\& k=r_2(\cos \beta+i \sin \beta) \\& k^*=r_2(\cos (2 \pi-\beta)+i \sin (2 \pi-\beta)) =r_2(\cos \beta-i \sin \beta) \\& q k^*=r_1 r_2(\cos \alpha+i \sin \alpha)(\cos \beta-i \sin \beta) \\& \operatorname{Re}\left[q k^*\right]=r_1 r_2(\cos \alpha \cos \beta+\sin \alpha \sin \beta) \\& =r_1 r_2 \cos (\alpha-\beta)=r_1 r_2 \cos (\beta-\alpha) \\& =r_1 r_2 \cos \theta=<q, k>\end{aligned}\) ,所以我们有
\begin{equation} \operatorname{Re}\left[\boldsymbol{f}(\boldsymbol{q}, m) \boldsymbol{f}^*(\boldsymbol{k}, n)\right]=g(\boldsymbol{q}, \boldsymbol{k}, m-n) \end{equation}
简单起见,我们假设存在复数 $\boldsymbol{g}(\boldsymbol{q}, \boldsymbol{k}, m-n)$ ,使得 $\boldsymbol{f}(\boldsymbol{q}, m) \boldsymbol{f}^*(\boldsymbol{k}, n)=\boldsymbol{g}(\boldsymbol{q}, \boldsymbol{k}, m-n)$ ,然后我们用复数的指数形式,设
\[\begin{aligned} \boldsymbol{f}(\boldsymbol{q}, m) & =R_f(\boldsymbol{q}, m) e^{\mathrm{i} \Theta_f(\boldsymbol{q}, m)} \end{aligned}\]\begin{equation} \boldsymbol{f}(\boldsymbol{k}, n) =R_f(\boldsymbol{k}, n) e^{\mathrm{i} \Theta_f(\boldsymbol{k}, n)} \end{equation}
\[\begin{aligned} \boldsymbol{g}(\boldsymbol{q}, \boldsymbol{k}, m-n) & =R_g(\boldsymbol{q}, \boldsymbol{k}, m-n) e^{\mathrm{i} \Theta_g(\boldsymbol{q}, \boldsymbol{k}, m-n)} \end{aligned}\]其中 $R_f$和$R_g$代表模长部分(实际大小),$\Theta_f$ 和 $\Theta_g$代表相位角, 这其实就是复数的复指数表达形式,即 $re^{i\theta} = r\cos(\theta) + ri\sin(\theta)$
那么代入方程后就得到方程组
\[\begin{aligned} \boldsymbol{f}(\boldsymbol{q}, m) \boldsymbol{f}^*(\boldsymbol{k}, n) & =\boldsymbol{g}(\boldsymbol{q}, \boldsymbol{k}, m-n) \\ R_f(\boldsymbol{q}, m) e^{\mathrm{i} \Theta_f(\boldsymbol{q}, m)} R_f(\boldsymbol{k}, n) e^{-\mathrm{i} \Theta_f(\boldsymbol{k}, n)} & = R_g(\boldsymbol{q}, \boldsymbol{k}, m-n) e^{\mathrm{i} \Theta_g(\boldsymbol{q}, \boldsymbol{k}, m-n)} \\ R_f(\boldsymbol{q}, m) R_f(\boldsymbol{k}, n) e^{\mathrm{i} (\Theta_f(\boldsymbol{q}, m) - \Theta_f(\boldsymbol{k}, n))} & = R_g(\boldsymbol{q}, \boldsymbol{k}, m-n) e^{\mathrm{i} \Theta_g(\boldsymbol{q}, \boldsymbol{k}, m-n)} \end{aligned}\]得到:
\[\begin{aligned} R_f(\boldsymbol{q}, m) R_f(\boldsymbol{k}, n) & =R_g(\boldsymbol{q}, \boldsymbol{k}, m-n) \end{aligned}\]\begin{equation} \begin{aligned} \Theta_f(\boldsymbol{q}, m)-\Theta_f(\boldsymbol{k}, n) & =\Theta_g(\boldsymbol{q}, \boldsymbol{k}, m-n) \end{aligned} \end{equation}
对于方程(5)第一个方程,代入 $m=n$ 得到
\begin{equation} R_f(\boldsymbol{q}, m) R_f(\boldsymbol{k}, m)=R_g(\boldsymbol{q}, \boldsymbol{k}, 0)=R_f(\boldsymbol{q}, 0) R_f(\boldsymbol{k}, 0)=|\boldsymbol{q}||\boldsymbol{k}| \end{equation}
为什么
\[R_f(\boldsymbol{q}, m) R_f(\boldsymbol{k}, m)=R_g(\boldsymbol{q}, \boldsymbol{k}, 0)=R_f(\boldsymbol{q}, 0) R_f(\boldsymbol{k}, 0)\]因为同时对两个向量加相同的旋转位置编码(同一个 m)后,它们的内积是不变的!
最后一个等号源于初始条件 $\boldsymbol{f}(\boldsymbol{q}, 0)=\boldsymbol{q}$ 和 $\boldsymbol{f}(\boldsymbol{k}, 0)=\boldsymbol{k}$ 。所以现在我们可以很简单地设 $R_f(\boldsymbol{q}, m)=|\boldsymbol{q}|, R_f(\boldsymbol{k}, m)=|\boldsymbol{k}|$ ,即它不依赖于 $m$ 。至于第二个方程,同样代入 $m=n$ 得到
\begin{equation} \Theta_f(\boldsymbol{q}, m)-\Theta_f(\boldsymbol{k}, m)=\Theta_g(\boldsymbol{q}, \boldsymbol{k}, 0)=\Theta_f(\boldsymbol{q}, 0)-\Theta_f(\boldsymbol{k}, 0)=\Theta(\boldsymbol{q})-\Theta(\boldsymbol{k}) \end{equation}
这里的 $\Theta(\boldsymbol{q}), \Theta(\boldsymbol{k})$ 是 $\boldsymbol{q}, \boldsymbol{k}$ 本身的幅角,最后一个等号同样源于初始条件。根据上式得到 $\Theta_f(\boldsymbol{q}, m)-\Theta(\boldsymbol{q})=\Theta_f(\boldsymbol{k}, m)-\Theta(\boldsymbol{k})$ ,所以 $\Theta_f(\boldsymbol{q}, m)-\Theta(\boldsymbol{q})$ 应该是一个只与 $m$ 相关、跟 $\boldsymbol{q}$ 无关的函数,记为 $\varphi(m)$ ,即 $\Theta_f(\boldsymbol{q}, m)=\Theta(\boldsymbol{q})+\varphi(m)$ 。接着代入 $n=m-1$ ,整理得到
\[\begin{aligned} \Theta_f(\boldsymbol{q}, m)-\Theta_f(\boldsymbol{k}, m-1) & =\Theta_g(\boldsymbol{q}, \boldsymbol{k}, 1) \\ (\Theta(\boldsymbol{q})+\varphi(m))-(\Theta(\boldsymbol{k})+\varphi(m-1)) & =\Theta_g(\boldsymbol{q}, \boldsymbol{k}, 1) \\ \end{aligned}\]得到:
\begin{equation} \varphi(m)-\varphi(m-1)=\Theta_g(\boldsymbol{q}, \boldsymbol{k}, 1)+\Theta(\boldsymbol{k})-\Theta(\boldsymbol{q}) \end{equation}
即 ${\varphi(m)}$ 是等差数列,设右端为 $\theta$ ,那么就解得 $\varphi(m)=m \theta$ 。
所以在二维情况下,我们得到二维情况下用复数表示的RoPE:
\[\boldsymbol{f}(\boldsymbol{q}, m)=R_f(\boldsymbol{q}, m) e^{\mathrm{i} \Theta_f(\boldsymbol{q}, m)}=\|q\| e^{\mathrm{i}(\Theta(\boldsymbol{q})+m \theta)}=\boldsymbol{q} e^{\mathrm{i} m \theta}\]根据复数乘法的几何意义,该变换实际上对应着向量的旋转,所以我们称之为"旋转式位置编码",它还可以写成矩阵形式:
\[\boldsymbol{f}(\boldsymbol{q}, m)=\left(\begin{array}{cc} \cos m \theta & -\sin m \theta \\ \sin m \theta & \cos m \theta \end{array}\right)\binom{q_0}{q_1}\]这里是因为利用复数旋转的几何含义(即二维旋转矩阵):
\[e^{i \theta}=\cos \theta+i \sin \theta \Rightarrow \text { 旋转矩阵 }=\left(\begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array}\right)\]
如何理解内积满足线性叠加性?
内积在ROPE中的解释
由于内积满足线性叠加性,因此任意偶数维的RoPE,我们都可以表示为二维情形的拼接,即
\[\underbrace{\left(\begin{array}{ccccccc} \cos m \theta_0 & -\sin m \theta_0 & 0 & 0 & \cdots & 0 & 0 \\ \sin m \theta_0 & \cos m \theta_0 & 0 & 0 & \cdots & 0 & 0 \\ 0 & 0 & \cos m \theta_1 & -\sin m \theta_1 & \cdots & 0 & 0 \\ 0 & 0 & \sin m \theta_1 & \cos m \theta_1 & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & 0 & \cdots & \cos m \theta_{d / 2-1} & -\sin m \theta_{d / 2-1} \\ 0 & 0 & 0 & 0 & \cdots & \sin m \theta_{d / 2-1} & \cos m \theta_{d / 2-1} \end{array}\right)}_{\mathcal{R}_m}\left(\begin{array}{c} q_0 \\ q_1 \\ q_2 \\ q_3 \\ \vdots \\ q_{d-2} \\ q_{d-1} \end{array}\right)\]把 d-维向量拆成d/2 个二维小块,是为了将复数旋转泛化到高维实数空间中,使每一对维度都能独立接受位置编码旋转。
也就是说,给位置为 $m$ 的向量 $\boldsymbol{q}$ 乘上矩阵 $\boldsymbol{\mathcal { R }}_m$ 、位置为 $n$ 的向量 $\boldsymbol{k}$ 乘上矩阵 $\boldsymbol{\mathcal { R }}_n$ ,用变换后的 $\boldsymbol{Q}, \boldsymbol{K}$ 序列做Attention,那么Attention就自动包含相对位置信息了,因为成立恒等式:
\[\left(\boldsymbol{\mathcal { R }}_m \boldsymbol{q}\right)^{\top}\left(\boldsymbol{\mathcal { R }}_n \boldsymbol{k}\right)=\boldsymbol{q}^{\top} \boldsymbol{\mathcal { R }}_m^{\top} \boldsymbol{\mathcal { R }}_n \boldsymbol{k}=\boldsymbol{q}^{\top} \boldsymbol{\mathcal { R }}_{n-m} \boldsymbol{k}\]这里的$\left(\boldsymbol{\mathcal { R }}_m \boldsymbol{q}\right)$是列向量,所以是$\boldsymbol{q}^{\top}\boldsymbol{k}$
值得指出的是, $\boldsymbol{\mathcal { R }}_m$ 是一个正交矩阵,它不会改变向量的模长,因此通常来说它不会改变原模型的稳定性。
由于 $\boldsymbol{\mathcal { R }}_m$ 的稀疏性,所以直接用矩阵乘法来实现会很浪费算力,推荐通过下述方式来实现RoPE:
\[\left(\begin{array}{c} q_0 \\ q_1 \\ q_2 \\ q_3 \\ \vdots \\ q_{d-2} \\ q_{d-1} \end{array}\right) \otimes\left(\begin{array}{c} \cos m \theta_0 \\ \cos m \theta_0 \\ \cos m \theta_1 \\ \cos m \theta_1 \\ \vdots \\ \cos m \theta_{d / 2-1} \\ \cos m \theta_{d / 2-1} \end{array}\right)+\left(\begin{array}{c} -q_1 \\ q_0 \\ -q_3 \\ q_2 \\ \vdots \\ -q_{d-1} \\ q_{d-2} \end{array}\right) \otimes\left(\begin{array}{c} \sin m \theta_0 \\ \sin m \theta_0 \\ \sin m \theta_1 \\ \sin m \theta_1 \\ \vdots \\ \sin m \theta_{d / 2-1} \\ \sin m \theta_{d / 2-1} \end{array}\right)\]其中 $\otimes$ 是逐位对应相乘,即Numpy、Tensorflow等计算框架中的 $*$ 运算。从这个实现也可以看到,RoPE可以视为是乘性位置编码的变体。
怎么做?
旋转位置编码计算
到此,我们已经明白了ROPE的理论解释,我们看看实际中应该怎么计算。
首先我们要解决的是角度的问题,如何计算$\theta$的值。
之前提到,ROPE就是“通过绝对位置编码的方式实现相对位置编码”,所以 RoPE 中用于旋转的角度选择方案,其实与 Transformer 原始论文中的正余弦位置编码方式是一致的 —— 都是基于预定义的频率来进行周期变化。
\[\begin{aligned} {\theta}_i = {10000^{\frac{2i}{d_{\text{model}}}}} \end{aligned}\]然后来看看计算的过程,先了解按照实数计算的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import math
def rope(q,dim, seq_len,theta=10000.0):
half_dim = dim // 2 # 计算频率: [half_dim]
freq_seq = torch.arange(0, half_dim, dtype=torch.float32, device=q.device)
inv_freq = 1.0 / (theta \*\* (freq_seq / half_dim))
# 位置索引: [seq_len]
pos = torch.arange(0, seq_len, dtype=torch.float32, device=q.device)
# 外积得到角度矩阵: [seq_len, half_dim]
# m*theta
angle = torch.outer(pos, inv_freq)
# 计算 cos/sin: [seq_len, half_dim]
sin = angle.sin()[None, :, :] # shape: [1, seq_len, half_dim]
cos = angle.cos()[None, :, :]
# 拆分 q 成两半
q1, q2 = q[..., ::2], q[..., 1::2] # 形状都是 [batch, num_heads,seq_len, half_dim]
# 应用旋转(实数形式):
q_rotated = torch.cat([q1 * cos - q2 * sin, q1 * sin + q2 * cos], dim=-1)
return q_rotated
复数计算的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch
import math
# 生成旋转矩阵
def precompute\*freqs_cis(dim: int, seq_len: int, theta: float = 10000.0): # 计算词向量元素两两分组之后,每组元素对应的旋转角度\theta_i
freqs = 1.0 / (theta \** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) # 生成 token 序列索引 t = [0, 1,..., seq_len-1]
t = torch.arange(seq*len, device=freqs.device) # freqs.shape = [seq_len, dim // 2]
freqs = torch.outer(t, freqs).float() # 计算m \* \theta
# 计算结果是个复数向量
# 假设 freqs = [x, y]
# 则 freqs_cis = [cos(x) + sin(x)i, cos(y) + sin(y)i]
# 极坐标
freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
return freqs_cis
# 旋转位置编码计算
def apply*rotary_emb(
xq: torch.Tensor,
xk: torch.Tensor,
freqs_cis: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]: # xq.shape = [batch_size, seq_len, dim] # xq*.shape = [batch_size, seq_len, dim // 2, 2]
xq* = xq.float().reshape(\*xq.shape[:-1], -1, 2)
xk* = xk.float().reshape(\*xk.shape[:-1], -1, 2)
# 转为复数域
xq_ = torch.view_as_complex(xq_)
xk_ = torch.view_as_complex(xk_)
# 应用旋转操作,然后将结果转回实数域
# xq_out.shape = [batch_size, seq_len, dim]
# 复数乘法 计算 rope,
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(2)
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(2)
return xq_out.type_as(xq), xk_out.type_as(xk)