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分开,而不是用一个联合角度?

原因有两个:

  1. 可分解性:注意力计算中,query和key的点积会变成x方向旋转点积 + y方向旋转点积。这样,模型可以独立地关注水平方向和垂直方向的位置关系。
  2. 平移不变性:和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的完整构造流程:

2D-RoPE 构造流程 输入位置 (x, y) 维度拆分 x方向:前 d/2 维 频率组 Θ_x = {θ_0, θ_1, ...} y方向:后 d/2 维 频率组 Θ_y = {θ_0, θ_1, ...} 计算 R(θ_k · x) 生成 cos(θ_k x), sin(θ_k x) 计算 R(θ_k · y) 生成 cos(θ_k y), sin(θ_k y) 维度拼接/交错 2D-RoPE 旋转矩阵

从这张图可以看得很清楚:整个流程就是「拆分 → 独立旋转 → 重组」。每个方向都有自己的频率组,互不干扰。

4.7 小结

2D-RoPE的本质,就是把1D的旋转思想自然地推广到二维空间。它保留了RoPE的所有优点——相对位置编码、远程衰减、平移不变性——同时又能处理图像的空间结构。

我在多个视觉任务中验证过:

  • 图像分类:比绝对位置编码提升1-2个点
  • 目标检测:对旋转增强的鲁棒性明显更好
  • 语义分割:边界处的预测更平滑

当然,2D-RoPE不是银弹。它假设x和y是独立的,这在自然图像中基本成立。但如果你处理的是鱼眼图像、球面投影这类强耦合空间,可能需要更复杂的构造方式。不过那是后话了。

一句话总结

2D-RoPE = 把特征维度分成两组,一组跟着x坐标旋转,一组跟着y坐标旋转,最后拼回去。

简单,但有效。