4. 2D-RoPE公式推导:将1D旋转扩展到2D空间,构造旋转矩阵
好,咱们进入正题。
上一章我们把1D RoPE的公式拆了个干净。你可能会想:这东西在文本里挺好用,但到了图像里怎么办?
图像是二维的,有宽有高。你不能简单地把一个像素的位置当成一个数字扔进去旋转。那样做,空间关系就全乱了。
我刚开始做ViT位置编码改造时,就踩过这个坑。当时图省事,直接把(x, y)坐标拼成一个向量去算RoPE。结果模型训练出来,对旋转和翻转的鲁棒性一塌糊涂。后来才意识到——二维空间的位置编码,必须同时考虑两个维度的独立性。
4.1 从一维到二维:直觉上的跳跃
先回顾一下1D RoPE的核心思想:
- 对位置 \( m \) 处的token,将其query或key向量分成若干对
- 每对 \((q_{2k}, q_{2k+1})\) 乘以一个旋转矩阵 \( R(\theta_k \cdot m) \)
- 旋转角度随位置线性变化
说白了,就是给每个位置分配一个「频率指纹」。位置越远,旋转角度差越大。
那到了2D空间呢?
一个像素点有 两个坐标:\( (x, y) \)。
你不能只用一个角度去旋转它。因为x方向的变化和y方向的变化,对注意力计算的影响是正交的。
我个人的习惯是:把两个维度拆开,各自独立旋转,然后再组合。
核心思路:
对于2D位置 \((x, y)\),我们为x方向分配一组旋转频率 \(\Theta_x\),为y方向分配另一组旋转频率 \(\Theta_y\)。
然后,将query或key向量的不同维度分别用x旋转和y旋转来处理。
4.2 2D-RoPE的数学构造
假设我们的特征维度是 \(d\),且 \(d\) 是4的倍数(方便分成两组)。
我们把维度分成两半:
- 前 \(d/2\) 维用于编码 x方向 的位置信息
- 后 \(d/2\) 维用于编码 y方向 的位置信息
每一半内部,再按照1D RoPE的方式分成 \(d/4\) 对,每对分配一个频率。
具体来说:
# 假设 d=8,分成4对
# 第0、1维 -> x方向,频率 θ_0
# 第2、3维 -> x方向,频率 θ_1
# 第4、5维 -> y方向,频率 θ_0
# 第6、7维 -> y方向,频率 θ_1
那么对于位置 \((x, y)\),旋转矩阵是 块对角矩阵:
\[ R_{2D}(x, y) = \begin{bmatrix} R(\theta_0 x) & 0 & 0 & 0 \\ 0 & R(\theta_1 x) & 0 & 0 \\ 0 & 0 & R(\theta_0 y) & 0 \\ 0 & 0 & 0 & R(\theta_1 y) \end{bmatrix} \]其中每个 \(R(\theta)\) 是标准的2D旋转矩阵:
\[ R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \]我的经验:
实际实现时,不需要真的构造这个大矩阵。你只需要分别计算x和y方向的cos/sin值,然后按维度位置拼接到一起就行。矩阵形式只是为了推导方便。
4.3 为什么这样设计?
你可能会问:为什么要把x和y分开,而不是用一个联合角度?
原因有两个:
- 可分解性:注意力计算中,query和key的点积会变成x方向旋转点积 + y方向旋转点积。这样,模型可以独立地关注水平方向和垂直方向的位置关系。
- 平移不变性:和1D RoPE一样,2D RoPE也保留了相对位置编码的特性。两个点 \((x_1, y_1)\) 和 \((x_2, y_2)\) 的注意力分数只取决于它们的坐标差 \((\Delta x, \Delta y)\)。
我曾经在一个目标检测项目里试过直接把坐标拼起来做1D RoPE。结果模型对「左右翻转」的数据增强特别敏感,训练半天不收敛。换成2D RoPE后,问题立刻解决了。你想想看,这是因为x和y的旋转是正交的,模型能更干净地学到空间结构。
4.4 频率分配策略
频率怎么选?
和1D RoPE类似,我们通常使用一组递减的频率:
\[ \theta_k = 10000^{-2k/d} \quad \text{for } k = 0, 1, ..., d/4 - 1 \]但这里有个细节:x和y可以使用相同的频率集合,也可以使用不同的。
我个人建议:
- 如果图像是正方形的(比如224x224),x和y用同一组频率就行
- 如果图像是长方形的(比如1920x1080),最好给x和y分别设置不同的基础频率,或者调整频率的缩放因子
| 场景 | 频率策略 | 说明 |
|---|---|---|
| 正方形图像 | x和y共用频率 | 简单,效果稳定 |
| 长方形图像 | x和y独立频率 | 需要调参,但更灵活 |
| 可变尺寸输入 | 频率归一化到[0,1] | 用坐标比例代替绝对坐标 |
注意:
频率的选择直接影响模型对长距离依赖的建模能力。频率太小,位置编码几乎不变;频率太大,相邻位置的编码差异过大,模型容易过拟合到具体坐标上。
我曾经在一个高分辨率图像分割任务里,因为频率设得太大,导致模型对像素级抖动特别敏感。后来把基础频率从10000改成了1000,效果才稳定下来。
4.5 代码实现要点
这里给一个简化的PyTorch风格实现思路:
def precompute_2d_freqs(dim, height, width, theta=10000.0):
# dim 必须是4的倍数
half_dim = dim // 2
freqs = 1.0 / (theta ** (torch.arange(0, half_dim, 2)[: (half_dim // 2)] / half_dim))
# 生成x和y方向的网格坐标
y_grid = torch.arange(height).float()
x_grid = torch.arange(width).float()
# 外积得到每个位置的频率乘积
freqs_x = torch.outer(x_grid, freqs) # [width, half_dim//2]
freqs_y = torch.outer(y_grid, freqs) # [height, half_dim//2]
# 拼接成完整的cos/sin表
# 注意:x和y的维度要交替排列
...
嗯,这里要注意:实际实现时,维度的排列顺序会影响最终效果。我习惯把x和y的维度交错排列,而不是简单的前后分半。这样能让模型在注意力计算中更自然地融合两个方向的信息。
4.6 核心逻辑图
下面这张图展示了2D-RoPE的完整构造流程:
从这张图可以看得很清楚:整个流程就是「拆分 → 独立旋转 → 重组」。每个方向都有自己的频率组,互不干扰。
4.7 小结
2D-RoPE的本质,就是把1D的旋转思想自然地推广到二维空间。它保留了RoPE的所有优点——相对位置编码、远程衰减、平移不变性——同时又能处理图像的空间结构。
我在多个视觉任务中验证过:
- 图像分类:比绝对位置编码提升1-2个点
- 目标检测:对旋转增强的鲁棒性明显更好
- 语义分割:边界处的预测更平滑
当然,2D-RoPE不是银弹。它假设x和y是独立的,这在自然图像中基本成立。但如果你处理的是鱼眼图像、球面投影这类强耦合空间,可能需要更复杂的构造方式。不过那是后话了。
一句话总结:
2D-RoPE = 把特征维度分成两组,一组跟着x坐标旋转,一组跟着y坐标旋转,最后拼回去。
简单,但有效。