1. RoPE基础回顾:从绝对位置编码到相对位置编码,RoPE的数学原理与几何意义
各位同学好,我是老张。今天咱们开始聊RoPE在视觉Transformer里的改造。不过在动手改之前,得先把RoPE的老底儿摸清楚。说实话,我见过太多人一上来就调代码,结果位置编码搞错了,模型怎么训都不收敛。嗯,咱们先把地基打牢。
1.1 为什么Transformer需要位置编码?
Transformer本身是置换不变的。什么意思?你想想看,自注意力机制里,Q和K做点积,它只关心两个token之间的语义相似度,完全不关心谁在前谁在后。对于文本来说,「我打你」和「你打我」在Transformer眼里是一样的——这显然不对。
所以我们需要给每个token注入位置信息。这就是位置编码的由来。
1.2 绝对位置编码:最朴素的想法
最早的做法很简单——给每个位置分配一个唯一的向量。比如位置1用向量p₁,位置2用p₂,以此类推。然后把这个向量加到token的embedding上。
公式长这样:
x'_i = x_i + p_i
其中p_i就是位置i的绝对位置编码。Transformer原论文用的是正弦余弦函数:
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
这样做的好处是能外推到更长的序列。但有个硬伤——它只编码了「绝对位置」,没有编码「相对位置关系」。
核心问题:两个token之间的距离信息,在绝对位置编码里是隐式存在的,但模型学起来很费劲。说白了,模型需要自己从p₁和p₅的数值差异里去推断「它们相隔4个位置」——这效率太低了。
1.3 相对位置编码:更聪明的做法
后来大家发现,其实我们更关心的是「token A和token B之间隔了几个位置」,而不是「token A在第几个位置」。这就引出了相对位置编码。
我最早接触相对位置编码是在做机器翻译项目时。当时用绝对位置编码,长句子翻译质量总上不去。后来换成相对位置编码,效果立竿见影。为什么?因为相对位置编码直接告诉模型「这两个词距离多远」,模型不需要自己去猜了。
相对位置编码的核心思想:在计算注意力分数时,把位置差(i - j)作为一个偏置项加进去。
Attention(Q, K, V) = softmax( QK^T / √d + B ) V
其中B_ij就是位置i和位置j之间的相对位置编码。常见的做法是用一个可学习的向量表,索引是(i - j)。
1.4 RoPE的数学原理
RoPE(Rotary Position Embedding)是2021年提出的。它既不是绝对位置编码,也不是传统的相对位置编码。它用一种很巧妙的方式——旋转——把位置信息揉进了token的表示里。
核心思想:对query和key向量做旋转操作,旋转的角度取决于它们的位置。
公式如下:
f_q(x_m, m) = R_m · W_q · x_m
f_k(x_n, n) = R_n · W_k · x_n
其中R_m是一个旋转矩阵:
R_m = [cos(mθ) -sin(mθ)]
[sin(mθ) cos(mθ)]
注意,这里是针对二维子空间。对于d维向量,RoPE把它分成d/2个二维子空间,每个子空间独立旋转。
然后计算注意力分数时:
q_m^T · k_n = (R_m · W_q · x_m)^T · (R_n · W_k · x_n)
= (W_q · x_m)^T · R_m^T · R_n · (W_k · x_n)
= (W_q · x_m)^T · R_{n-m} · (W_k · x_n)
看到了吗?R_m^T · R_n = R_{n-m},结果只依赖于位置差(n - m)。这就是RoPE的精妙之处——绝对位置编码的形式,实现了相对位置编码的效果。
我的经验:RoPE的实现其实很简单,就是给每个token的q和k向量乘一个旋转矩阵。但要注意,这个旋转矩阵是稀疏的,实际实现时用「两两旋转」的方式,效率很高。我曾经在项目里手写过这个操作,比用全尺寸旋转矩阵快了10倍以上。
1.5 RoPE的几何意义
咱们来聊聊几何意义。说白了,RoPE就是在高维空间里做旋转。
你想想看,一个二维向量(x, y)旋转θ角度后变成(xcosθ - ysinθ, xsinθ + ycosθ)。RoPE对每个二维子空间都做这样的操作,旋转角度由位置决定。
这意味着什么?
- 位置越远,旋转角度越大。位置1旋转θ,位置2旋转2θ,位置3旋转3θ...以此类推。
- 两个token之间的相对距离,体现在旋转角度的差值上。位置m和位置n的向量,它们的旋转角度差是(m-n)θ。
- 旋转操作保持向量的模长不变。这意味着RoPE不会改变token的语义信息,只改变它的「方向」。
下面这张图展示了RoPE的核心逻辑:
1.6 RoPE的代码实现
咱们来看看RoPE的PyTorch实现。这段代码我改了好几版,现在这个版本是最简洁的:
import torch
import torch.nn as nn
import math
class RotaryPositionEmbedding(nn.Module):
def __init__(self, dim, max_seq_len=2048):
super().__init__()
# 计算旋转频率
inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
self.register_buffer('inv_freq', inv_freq)
# 预计算所有位置的cos和sin值
pos = torch.arange(max_seq_len).float()
freqs = torch.einsum('i,j->ij', pos, inv_freq)
emb = torch.cat((freqs, freqs), dim=-1)
self.register_buffer('cos_cached', emb.cos())
self.register_buffer('sin_cached', emb.sin())
def forward(self, x, seq_len=None):
# x shape: [batch, seq_len, num_heads, dim]
cos = self.cos_cached[:seq_len].unsqueeze(0).unsqueeze(2)
sin = self.sin_cached[:seq_len].unsqueeze(0).unsqueeze(2)
return self.apply_rotary(x, cos, sin)
def apply_rotary(self, x, cos, sin):
# 将x分成两半,分别旋转
x1 = x[..., :x.shape[-1]//2]
x2 = x[..., x.shape[-1]//2:]
# 旋转操作
rotated_x1 = x1 * cos - x2 * sin
rotated_x2 = x1 * sin + x2 * cos
return torch.cat([rotated_x1, rotated_x2], dim=-1)
避坑指南:我曾经在实现RoPE时犯过一个低级错误——把cos和sin的维度搞反了。结果模型训练了三天,loss死活不降。后来发现是旋转方向反了。记住:cos乘x₁,sin乘x₂,顺序不能乱。
1.7 RoPE vs 其他位置编码
| 方法 | 类型 | 可外推 | 相对位置 | 额外参数 |
|---|---|---|---|---|
| Sinusoidal | 绝对 | ✅ | ❌ 隐式 | 0 |
| 可学习绝对编码 | 绝对 | ❌ | ❌ 隐式 | max_len × d |
| 相对位置偏置 | 相对 | ✅ | ✅ 显式 | 2×max_len - 1 |
| RoPE | 旋转 | ✅ | ✅ 显式 | 0 |
从表格能看出来,RoPE在各方面都很均衡。没有额外参数,天然支持外推,还能显式编码相对位置。这也是为什么它成了LLaMA、ChatGLM等主流模型的首选。
1.8 小结
RoPE的核心就三句话:
- 用旋转矩阵给每个位置编码
- 旋转角度差天然表示相对位置
- 不引入额外参数,计算高效
嗯,基础就这些。下一节咱们开始聊RoPE在视觉Transformer里的适配改造——你会发现,图像和文本的位置编码需求完全不一样,直接套用会出大问题。
个人建议:如果你现在手头有项目在用Transformer做图像任务,不妨先试试RoPE。我去年在一个图像分类项目里把绝对位置编码换成RoPE,准确率提升了0.8%。虽然不多,但胜在零成本改动。
公众号:蓝海资料掘金营,微信deep3321