RoPE

RoPE (Rotary Positional Embedding),旋转位置编码,是目前广泛采用的一种位置编码方式

其核心思想是,将高维表征向量分解为多组正交分量,并分别对分量根据位置的不同做不同角度的“旋转”,从而使得编码后同一对向量的点积结果只与二者的相对位置有关,而与绝对位置无关

要理解 RoPE,可以先从二维情况开始考虑:

矩阵视角

设词向量 位于第 个位置,则RoPE 会将它旋转 角:

对位于第 个位置的词向量 ,有

由此可见同一对向量的点积在 RoPE 后确实只与相对位置有关。

复数视角

从复数视角可以更直观地理解 RoPE

复数中旋转 角相当于乘上一个 ,把二维向量视为一个复数,则 RoPE 相当于对第 个位置的 乘上了

容易发现 ,因此

高维情况

一般来讲 RoPE 会将 维向量分解为 组二维向量,即将 分为 ,对于第 组,,接着分别对每一组做旋转变换。

RoPE 不同维度中 的取值

如果每一组的 取相同值,那么对于两个相同的 token,它们在位置 和位置 处经位置编码后的结果是完全相同的,这显然不符合我们的目标。

相反,如果对每一组取不同的 ,不仅可以使得位置编码失去简单的周期性,还可以让不同维度以不同速度“旋转”

  • 高频维度:随着位置的微小变化,旋转角度会迅速改变,有利于模型捕捉精细的位置差异
  • 低频维度:即使位置有很大变化,旋转角度变化也很小,使得模型可以在处理长距离的 token 时依然保持稳定

另外,可以数学证明当 按照指数形式分布时,Attention Score 会随着相对距离 的增加而产生自然的衰减,符合语言模型的直觉:物理距离越远的词,相互之间越不相关

实现

class RoPE(nn.Module):
    def __init__(self, n_embed, base=10000):
        super().__init__()
        self.base = base
        self.n_embed = n_embed
        self.register_buffer('cos_cache', None) # (T, C)
        self.register_buffer('sin_cache', None)

可以将 cos 和 sin 矩阵缓存起来避免重复计算:

def _build_cache(self, x):
    seq_len = x.size(-2)
    if self.cos_cache is not None and seq_len <= self.cos_cache.size(-2):
        return
    theta = 1. / (self.base ** (torch.arange(0, self.n_embed, 2).float() / self.n_embed))
    seq_idx = torch.arange(seq_len, device=x.device)
    idx_theta = torch.outer(seq_idx, theta) # (T, C / 2)
    idx_theta = torch.cat([idx_theta, idx_theta], dim=-1) # (T, C)
    self.cos_cache = idx_theta.cos()
    self.sin_cache = idx_theta.sin()
def forward(self, x):
    # x: (B, T, C)
    seq_len = x.size(-2)
    self._build_cache(x)
    cos = self.cos_cache[:seq_len, :]
    sin = self.sin_cache[:seq_len, :]
 
    # [x_front, x_back] -> [-x_back, x_front]
    x_front, x_back = torch.chunk(x, 2, dim=-1)
    x_shift = torch.cat([-x_back, x_front], dim=-1)
 
    x_rope = x * cos + x_shift * sin
    return x_rope

为什么这样分组?

从代码中可以发现,选择将 分为一组的好处是对于乘 cos 矩阵和乘 sin 矩阵这两个操作,只需将 分为两半即可,不需要进行复杂的交错切片,对 GPU 缓存友好

Reference

LabML