RoPE 解析
RoPE(旋转位置编码)是一种结合了绝对位置编码和相对位置编码的一种编码方法,出自苏剑林老师提出的RoFormer,现如今已经作为LLM结构的标配了,可见其效果强大。这篇文章就来具体解析一下,RoPE的原理和优势到底是什么。
本文对RoPE的解析主要参考自苏剑林老师本人对RoPE的解析系列文章[1,2],并结合了个人对其的一些思考。
位置编码
大模型通过理解用户的输入而进行输出。在大模型的整个理解过程中,而对句子的理解来说,字词表示和位置的准确性表示无疑非常重要,本文所探讨的就是对位置的准确性表示。
对输入的自然语言来说,由一组相同字词组成的句子,字词在句子中出现的顺序不同,都可能会导致句子所表达的含义出现偏差,简单举个例子:
用户层面
用户输入:
我 爱 你
如果对位置的准确性无法进行准确区分,模型可能会理解成:
你 爱 我
完全偏离了用户输入的意思。
给定token 序列 ,模型编码成对应的词向量,向量开始为随机初始化,具有轮换对称性,即:,这就会使得模型无法从结果上区分输入是还是,导致出现理解的偏差。
因此,我们要做的事情,就是要打破这种对称性,比如在每个位置上都加上一个不同的编码向量:,只要每个位置的编码向量不同,那么这种对称性就被打破了。
下面进一步分析位置编码的性质。
为了简化问题,我们先只考虑两个位置上的位置编码,将其泰勒展开到二阶:
可以看到,展开式中与位置无关, 是一个包含和位置信息的交互项,其余的各项都依赖于单一的位置信息。 这样就能去区分token之间的位置信息了。
针对token位置的准确性表示,主要有两个方向的研究,即相对位置编码和绝对位置编码。
绝对位置编码
绝对位置嵌入类似于为每个位置分配唯一编号,在标准的Transformer架构即使用的绝对位置编码,第一层属于除了InputEmbedding之外,还有一个同维度的PositionalEmbedding,如下所示。
\begin{array}{lr} PE(pos,2i+1) = cos(\frac{pos}{10000^{2i/d}}) \right. Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt d_k})V q.k=x_1.x_2+y_1.y_2 q=x_1+y_1i\\ ⟨q,k⟩=(x_1+y_1i).(x_2+y_2i)\\可以看到向量内积和复数乘法的实数部分仅相差一个符号,所以我们在复数乘法中,通过共轭复数替代,再取乘法实数部分,获得向量内积的结果。
如此一来,二维向量的内积结果可表示如下:
其中表示序列中token的绝对位置,表示取结果的实部。二维平面向量可由复数表示,对复数乘以,相当于把该向量逆时针旋转角度。只改变了向量的方向,但不改变向量的模长,如下图上部分所示:

这样做相当于给向量配上了绝对位置编码,然后进行内积,可得:
RoPE在的选择上,RoPE同样沿用了Transformer位置编码的方案,即,因为它可以带来一定的远程衰减性。
注意中的表示向量中的第维,不是虚部
这样一来,就得到了一种融合绝对位置和相对位置于一体的位置编码方案。RoPE同时也巧妙的在Attention计算的QK中将绝对位置和相对位置融合到了一起。使用变换后的Q,K序列做Attention。
k_n=RoPE(W^Ke_n) q_m^Tk_n =e_m^T(W^Q)^TR^T_mR_nW^Ke_n q_me^{i\theta}.k_ne^{i\theta}=Re[⟨q_me^{i\theta},(k_ne^{i\theta})^*] ⟩ =Re[q_mk_n^*e^{i\theta} ] e^{i\theta}=cos\theta+sin\theta.i qe^{i\theta}=(x+yi)e^{i\theta} =(xcos\theta-ysin\theta)+(xsin\theta+ycos\theta)i\\ q = \begin{Bmatrix} q^{(2)} k = \begin{Bmatrix} k^{(2)}其复数形式为,,由欧拉公式展开后仅保留实部转化后的复数域内积为:
\begin{pmatrix} \end{pmatrix} k^{(1)}\\ \end{pmatrix} R_m =\begin{pmatrix} sin\frac{m}{10000^{2i/d}}& cos\frac{m}{10000^{2i/d}}cos \frac{n}{10000^{2i/d}} & -sin\frac{n}{10000^{2i/d}} \\ \end{pmatrix} R^T_mR_n=\begin{pmatrix} \end{pmatrix} Re[q_mk_n^*e^{i\theta} ] = q^TR^T_mR_nk\\ \begin{pmatrix} \end{pmatrix} k^{(1)}\\ \end{pmatrix}\\ (q^{(1)} \ q^{(2)}) cos \theta & -sin\theta \\sin\theta & cos\theta \begin{pmatrix} k^{(2)}与复数域内积计算完全一致。
代码实现时,常用实数矩阵旋转(直接用三角函数算)
理论推导时,常用复数旋转
两种实现,最终都是让注意力分数里隐含位置差,让模型记住语序信息
但是这种变换仅适用于二维向量,而真实场景中都是高维向量,所以直观的想法就是将高维向量拆分成多个二维向量的拼接每两个二维向量之间做内积。设(Attention中的)的位置向量长度为,每两个元素为一组二维向量,一共有个组合,可以直接拼接作为维度的旋转位置编码。(k同理)
cos \theta_0 & -sin\theta_0 & 0 & 0&... &0&0\\ 0 & 0&cos \theta_1 & -sin\theta_1 & ... &0&0 \\ ...&...&...&...&...&...&...& \\ 0&0&0&0&...&sin\theta_{d/2-1}&cos \theta_{d/2-1} q_0\\q_1\\q_2\\q_3\\...\\...\\q_{d-1}\\其中
既然向量拆分了,那就可以采用分块矩阵来进行变换,每个块只关注两个分量的信息,且每个块的幅角也可以不同。因为采用矩阵相乘的方式来实现RoPE会浪费很多显存,所以在实际计算中采用逐位相乘的方式来实现 RoPE:
同时RoPE是目前唯一一种可以用于线性Attention的相对位置编码。这是因为其他的相对位置编码,都是直接基于Attention矩阵进行操作的,但是线性Attention并没有事先算出Attention矩阵,因此也就不存在操作Attention矩阵的做法,所以其他的方案无法应用到线性Attention中。而对于RoPE来说,它是用绝对位置编码的方式来实现相对位置编码,不需要操作Attention矩阵,因此有了应用到线性Attention的可能性。
线性Attention的常见形式:
其中是值域非负的激活函数
RoPE实现代码
Roformer的实现,完整的代码在这里,其中RoPE计算的核心功能如下,使用了逐位相乘的形式:
def apply_rotary_position_embeddings(sinusoidal_pos, query_layer, key_layer, value_layer=None):
# https://kexue.fm/archives/8265
# sin [batch_size, num_heads, sequence_length, embed_size_per_head//2]
# cos [batch_size, num_heads, sequence_length, embed_size_per_head//2]
sin, cos = sinusoidal_pos.chunk(2, dim=-1)
# sin [θ0,θ1,θ2......θd/2-1] -> sin_pos [θ0,θ0,θ1,θ1,θ2,θ2......θd/2-1,θd/2-1]
sin_pos = torch.stack([sin, sin], dim=-1).reshape_as(sinusoidal_pos)
# cos [θ0,θ1,θ2......θd/2-1] -> cos_pos [θ0,θ0,θ1,θ1,θ2,θ2......θd/2-1,θd/2-1]
cos_pos = torch.stack([cos, cos], dim=-1).reshape_as(sinusoidal_pos)
# rotate_half_query_layer [-q1,q0,-q3,q2......,-qd-1,qd-2]
rotate_half_query_layer = torch.stack([-query_layer[..., 1::2], query_layer[..., ::2]], dim=-1).reshape_as(
query_layer
)
query_layer = query_layer * cos_pos + rotate_half_query_layer * sin_pos
# rotate_half_key_layer [-k1,k0,-k3,k2......,-kd-1,kd-2]
rotate_half_key_layer = torch.stack([-key_layer[..., 1::2], key_layer[..., ::2]], dim=-1).reshape_as(key_layer)
key_layer = key_layer * cos_pos + rotate_half_key_layer * sin_pos
if value_layer is not None:
# rotate_half_value_layer [-v1,v0,-v3,v2......,-vd-1,vd-2]
rotate_half_value_layer = torch.stack([-value_layer[..., 1::2], value_layer[..., ::2]], dim=-1).reshape_as(
value_layer
)
value_layer = value_layer * cos_pos + rotate_half_value_layer * sin_pos
return query_layer, key_layer, value_layer
return query_layer, key_layerch_size, seq_len, d_model)
return output
参考文献
[1] Transformer升级之路:1、Sinusoidal位置编码追根溯源 - 科学空间|Scientific Spaces
[2] Transformer升级之路:2、博采众长的旋转式位置编码 - 科学空间|Scientific Spaces
[3] Understanding Positional Embeddings in Transformers: From Absolute to Rotary | Towards Data Science
[4] 介绍 RoPE 旋转位置编码