3、RoPE公式推导:从绝对位置到相对位置
好,咱们直接进入正题。位置编码这事儿,在语音识别里其实挺头疼的。你想想看,一段语音可能有几百帧,模型得知道「这个词在第几帧说的」。早期大家用绝对位置编码,说白了就是给每个位置发个身份证号。但后来发现,模型真正关心的不是「你在第5帧」,而是「你离我有多远」——这就是相对位置的核心思想。
我个人习惯把RoPE理解成「让位置信息随着旋转自然融入」。嗯,咱们一步步来拆。
3.1 为什么绝对位置不够用?
先看一个简单例子。假设句子是「我今天很开心」。用绝对位置编码,每个词的位置是固定的:
- 「我」→ 位置1
- 「今天」→ 位置2
- 「很」→ 位置3
- 「开心」→ 位置4
模型学到的其实是「位置1的词是主语,位置4的词是谓语」。但问题来了——如果句子变成「今天我很开心」呢?位置全乱了。模型得重新学。
我在项目中遇到过类似情况。训练一个中文语音识别模型,发现长句子的后半段准确率明显下降。后来排查发现,绝对位置编码让模型对「绝对距离」产生了依赖,一旦语速变化或插入停顿,位置信息就失效了。
核心矛盾:语音识别里,同一句话可能说快说慢,帧数不一样。绝对位置编码无法泛化到未见过的长度。
3.2 二维旋转矩阵的构造
RoPE的思路很巧妙。它把位置信息编码成旋转角度,而不是加性偏置。咱们先从二维情况说起。
假设有一个二维向量 [x, y],代表某个token的query或key。我们想给它加上位置信息。怎么做?
旋转!
给定位置 pos,我们构造一个旋转矩阵:
R(pos) = [[cos(pos*θ), -sin(pos*θ)],
[sin(pos*θ), cos(pos*θ)]]
其中 θ 是一个预设的角速度。那么位置编码后的向量就是:
[x', y'] = R(pos) · [x, y]
说白了,就是把原始向量在二维平面上转了个角度。位置不同,转的角度不同。
为什么这样能表达相对位置?
你想想看,两个位置 pos1 和 pos2 的向量做内积时:
q_pos1 · k_pos2 = (R(pos1)·q) · (R(pos2)·k)
= q · (R(pos1)^T · R(pos2)) · k
= q · R(pos2 - pos1) · k
看到了吗?内积结果只依赖于 pos2 - pos1,也就是相对位置!这就是RoPE的精髓——绝对位置编码进去,相对位置自然出来。
关键洞察:旋转矩阵的群性质保证了 R(a)^T · R(b) = R(b-a)。这个性质让绝对位置编码自动具备了相对位置感知能力。
3.3 高维空间的扩展方法
二维的情况很美好,但实际模型里,embedding维度通常是512、768甚至更大。怎么扩展到高维?
方法其实很直接——分块旋转。
假设维度是 d,我们把它分成 d/2 个二维子空间。每个子空间独立旋转,但角速度不同。
具体来说:
- 将
d维向量按两两一组,分成d/2组 - 第
i组使用角速度θ_i = base^(-2i/d) - 每组独立应用二维旋转矩阵
代码实现大概是这样的:
def apply_rope(x, pos):
"""
x: [batch, seq_len, num_heads, head_dim]
pos: [batch, seq_len] 或标量
"""
d = x.shape[-1]
# 计算每个子空间的角速度
theta = 10000.0 ** (-torch.arange(0, d, 2) / d) # [d/2]
# 计算旋转角度
angles = pos.unsqueeze(-1) * theta # [batch, seq_len, d/2]
# 构造cos和sin
cos = torch.cos(angles) # [batch, seq_len, d/2]
sin = torch.sin(angles) # [batch, seq_len, d/2]
# 将x分成两半,分别旋转
x1 = x[..., 0::2] # 偶数索引
x2 = x[..., 1::2] # 奇数索引
# 旋转公式
x1_new = x1 * cos - x2 * sin
x2_new = x1 * sin + x2 * cos
# 合并
return torch.stack([x1_new, x2_new], dim=-1).reshape(x.shape)
避坑指南:我曾经在实现时把 cos 和 sin 的维度搞反了,结果模型训练loss死活不降。后来发现是旋转方向反了。记住:x1_new = x1*cos - x2*sin,这是逆时针旋转。
3.4 为什么选择不同的角速度?
你可能想问:为什么每个子空间用不同的 θ?
原因很简单——让不同维度捕捉不同尺度的位置信息。
- 低维度(
i小):θ大,旋转快,适合捕捉短距离相对位置 - 高维度(
i大):θ小,旋转慢,适合捕捉长距离依赖
这有点像傅里叶变换里的不同频率分量。我在做语音识别时发现,这种多尺度设计特别适合处理语音信号——因为语音既有短时的音素变化(几十毫秒),也有长时的语义依赖(几秒钟)。
下面这张图展示了RoPE的核心逻辑:
3.5 实际应用中的注意事项
最后聊几个我在实践中踩过的坑:
注意1:位置索引从0开始还是从1开始?
我建议从0开始。因为 pos=0 时旋转角度为0,相当于没有旋转,这符合「第一个位置不需要偏移」的直觉。曾经有个同事从1开始,结果模型在短序列上表现异常。
注意2:base值的选择
默认 base=10000 是Transformer论文里的值。但在语音识别中,如果帧率是100Hz(每帧10ms),序列长度可能达到2000帧。我建议适当增大base值(比如50000),让高频维度旋转慢一点,避免角度溢出。
小技巧:如果你用PyTorch,记得把 cos 和 sin 注册为 buffer,不要作为可训练参数。它们只依赖位置,不需要梯度。
嗯,RoPE的公式推导就到这里。说白了就是「用旋转代替加法,用角度差表达相对位置」。下一节咱们会看到它在语音识别模型里具体怎么用,包括怎么处理变长序列和流式推理。
公众号:蓝海资料掘金营,微信deep3321