通俗理解RoPE、2D-RoPE、M-RoPE 原创

发布于 2025-8-18 08:29
浏览
0收藏

本文通过将这些方法可视化呈现为旋转操作和维度拆分,能让旋转位置编码(RoPE)、二维旋转位置编码(2D-RoPE)以及多模态旋转位置编码(M-RoPE)的核心概念更直观、更易于理解。

为什么需要位置嵌入?

假设有两个语言模型:一个一次只能处理一个词,另一个则可以并行处理所有词。

现在,有一个词序列,比如“Dog eats food”。

  • 对于第一个模型,输入的顺序很重要,因为它必须先处理“Dog”,再处理“eats”,最后处理“food”。但显然,这样既缓慢又低效!
  • 对于第二个模型,输入的顺序不重要,因此可以一次性输入所有词,甚至是乱序的,比如“food”、“Dog”、“eats”。由于这个模型可以并行处理所有词,所以速度快得多。

第二个模型的问题在于它不知道词的顺序。因此,需要向输入嵌入中添加一些位置信息。

现在,想象一下,用N个嵌入向量代替词,每个向量的维度为​​n_dim​​​。举个例子:4个嵌入向量,每个的维度​​n_dim=8​​,且初始化为1:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

现在将位置嵌入0、1、2、3分别应用到每个嵌入向量上。还使用了一种假想的方法,即简单地将位置索引加到每个嵌入向量上。因此,第一个嵌入向量就会是1+0,第二个是1+1,以此类推。

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

当然,这只是一个用于说明该概念的简单例子。实际上,这种方法是行不通的。那么,该怎么做呢?

RoPE

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

RoPE是旋转位置嵌入(Rotary Position Embedding)的缩写。目前广泛运用在LLM中,其是一种在Transformer模型的输入嵌入中编码位置信息的方法。

RoPE的工作原理很简单,就是在二维空间中旋转输入嵌入向量。这里不深入数学细节,但举一个简单的例子:

输入向量为​​[x=0, y=1]​​,每个位置会将该嵌入向量逆时针旋转20度()。

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

在现实世界中,并非只有2个维度,而是有n_dim个维度。例如,n_dim = 4:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

那么,如何在n维空间中旋转一个向量呢?答案很简单:将这个向量拆分成多个二维对。

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

为了简化说明,会用箭头来替代每一对(二维对):

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

现在有趣的部分来了:不会用相同的角度旋转每个向量,因为这样很快就会耗尽所有可能的角度

理解这一点的最佳方式就像看待一个时钟🕓:秒针每转一整圈,分针只转动一小部分。

在RoPE中,旋转的量被称为频率(记为f),其定义为:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

为简单起见,假设第一对的频率f仍为20°,第二对的频率f为10°:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

如所见,第一对的旋转速度比第二对快,就像时钟的秒针比分针转得快一样。

就像时钟的三根指针可以用来表示一天中的86400秒那样,可以用同样的思路在RoPE中表示所有的n维维度。

gemma的RoPE实现如下:

def _compute_default_rope_parameters(
    config: Optional[PretrainedConfig] = None,
    device: Optional["torch.device"] = None,
    seq_len: Optional[int] = None,
    **rope_kwargs,
) -> tuple["torch.Tensor", float]:
    """
    Computes the inverse frequencies according to the original RoPE implementation
    Args:
        config ([`~transformers.PretrainedConfig`]):
            The model configuration.
        device (`torch.device`):
            The device to use for initialization of the inverse frequencies.
        seq_len (`int`, *optional*):
            The current sequence length. Unused for this type of RoPE.
        rope_kwargs (`Dict`, *optional*):
            BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
    Returns:
        Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
        post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE).
    """
    base = rope_kwargs["base"]
    dim = rope_kwargs["dim"]
    attention_factor = 1.0  # Unused in this type of RoPE

    # Compute the inverse frequencies
    # 计算RoPE公式40中的theta,每2个维度共用一个inv_freq
    inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim))
    return inv_freq, attention_factor

class GemmaRotaryEmbedding(nn.Module):
    def __init__(self, config: GemmaConfig, device=None):
        super().__init__()
        self.rope_type = "default"
        self.max_seq_len_cached = config.max_position_embeddings
        self.original_max_seq_len = config.max_position_embeddings
        self.config = config
        # 计算前面一半维度旋转角度theta,用于和position_ids相乘
        inv_freq, self.attention_scaling = _compute_default_rope_parameters(self.config, device)
        self.register_buffer("inv_freq", inv_freq, persistent=False)
        self.original_inv_freq = self.inv_freq
    def forward(self, x, position_ids):
        inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
        position_ids_expanded = position_ids[:, None, :].float()
        device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps"else"cpu"
        with torch.autocast(device_type=device_type, enabled=False):  # Force float32
            #这里的freqs只是前面一半维度的
            freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
            # 在hidde_dim维度拼接起来之后freqs最后一维的维度才等于hidden_size
            emb = torch.cat((freqs, freqs), dim=-1)
            # 计算 cos(m*theta)和sin(m*theta)
            cos = emb.cos()
            sin = emb.sin()

        return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)

def rotate_half(x):
    """
    Rotates half the hidden dims of the input.
    将输入x后hidden_dim的半部分取反并拼接到原来前半部分的前面
    """
    x1 = x[..., : x.shape[-1] // 2]
    x2 = x[..., x.shape[-1] // 2 :]
    return torch.cat((-x2, x1), dim=-1)

def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
    """
    Applies Rotary Position Embedding to the query and key tensors.
    将q,k乘以得到的旋转矩阵cos,sin
    """
    cos = cos.unsqueeze(unsqueeze_dim)
    sin = sin.unsqueeze(unsqueeze_dim)
    q_embed = (q * cos) + (rotate_half(q) * sin)
    k_embed = (k * cos) + (rotate_half(k) * sin)
    return q_embed, k_embed

2D-RoPE

到目前为止,只讨论了RoPE,它是一种一维位置嵌入方法。这对于一维序列很有用,比如文本。

但如果想将RoPE用于二维序列(例如图像),该怎么办呢?

二维RoPE(2D-RoPE)是RoPE的一种简单扩展,在这种方法中,每个输入向量都有一个二维位置:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

回到时钟的类比,一个简单的思路是使用两个时钟,一个对应y轴,另一个对应x轴。

为了说明这一点,将初始示例的维度n_dim加倍,这样就有n_dim = 8:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

现在它被表示为4对二维向量:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

其思路是将该向量进一步拆分为两部分,一部分对应y轴,另一部分对应x轴。

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

假设这4个向量的位置列表如下:

  • [0, 0]
  • [0, 1]
  • [1, 2]
  • [1, 3]

使用一组40°和20°的角度值,可以像这样独立旋转每个部分的向量:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

据所知,这种二维旋转位置编码(2D-RoPE)方法被用于Llama 4模型的视觉编码器中。

2D-RoPE with interleaved frequency

在之前的示例中,对两个轴使用了相同的角度值(40°和20°)。但如果想为每个轴使用不同的频率呢?

以Mistral的Pixtral模型为例:

  • 首先为所有二维向量对创建一个频率列表,例如:40°、30°、20°、10°
  • 然后将这些频率按轴交错分配,这样y轴得到40°、20°,x轴得到30°、10°。

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

巧妙之处在于,无需先构建一个频率列表(例如40°、30°、20°、10°),再从中挑选奇数位或偶数位的频率值,只需调整n_dim参数值和频率的缩放比例,就能得到相同的结果。可以查看这个PR(拉取请求)了解的具体实现方式。

M-RoPE

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

Qwen2VL的M-RoPE

M-RoPE是Multimodal-RoPE(多模态旋转位置编码)的缩写,最初由Qwen2VL模型提出。

M-RoPE扩展了二维旋转位置编码(2D-RoPE)的理念,不过现在每个位置包含的维度不止2个。例如,可以有三维[时间、y轴、x轴],甚至更多维度。

其核心思想是,不再将嵌入向量拆分为2部分,而是拆分为……没错,拆分为n部分,其中n是每个位置的维度数量。

如果仔细查看Qwen2VL的config.json文件,会看到一个名为mrope_section的配置,其中包含3个数值。每个数值代表每个部分的二维对数量。

"rope_scaling": {
    "type": "mrope",
    "mrope_section": [
      16,
      24,
      24
    ]
  },

为了便于理解,举一个简单的例子:当嵌入向量的维度​​n_dim=8​​时,最终会得到4对二维向量:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

假设的mrope_section配置为[1,1,2],可以将嵌入向量拆分为3个部分:

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

然后,使用与二维旋转位置编码(2D-RoPE)中所解释的相同方法,对每个部分独立应用旋转位置编码(RoPE)。

通俗理解RoPE、2D-RoPE、M-RoPE-AI.x社区

参考文献

  • RoFormer: Enhanced Transformer with Rotary Position Embedding,https://arxiv.org/abs/2104.09864
  • Qwen2-VL: Enhancing Vision-Language Model’s Perception of the World at Any Resolution,https://arxiv.org/pdf/2409.12191


本文转载自​​​大模型自然语言处理​​​   作者:llmnlp

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-8-18 08:30:32修改
收藏
回复
举报
回复
相关推荐