注意力机制的"位置盲区"
在上一章中,我们学习了注意力机制如何通过QKV矩阵计算Token之间的相关性。但这里有一个严重的问题:
注意力机制天生是"位置不敏感"的!
问题演示
考虑以下两个句子:
- "猫 吃 鱼"
- "鱼 吃 猫"
对于注意力机制来说,如果我们交换Token的顺序,计算过程是这样的:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 句子1的注意力分数矩阵: Scores 1 = Q 1 ⋅ K 1 T 句子2(交换位置后): Scores 2 = Q 2 ⋅ K 2 T \begin{aligned} \text{句子1的注意力分数矩阵:} & \quad \text{Scores}_1 = Q_1 \cdot K_1^T \\ \text{句子2(交换位置后):} & \quad \text{Scores}_2 = Q_2 \cdot K_2^T \end{aligned} </math>句子1的注意力分数矩阵:句子2(交换位置后):Scores1=Q1⋅K1TScores2=Q2⋅K2T
由于 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q Q </math>Q、 <math xmlns="http://www.w3.org/1998/Math/MathML"> K K </math>K、 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V 都是通过相同的权重矩阵 <math xmlns="http://www.w3.org/1998/Math/MathML"> W Q W_Q </math>WQ、 <math xmlns="http://www.w3.org/1998/Math/MathML"> W K W_K </math>WK、 <math xmlns="http://www.w3.org/1998/Math/MathML"> W V W_V </math>WV 从Embedding计算得到的,如果我们只是交换了Token的顺序,而不告诉模型"位置信息",那么注意力机制会认为这两个句子是等价的!
具体来说,注意力计算公式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Attention ( Q , K , V ) = softmax ( Q ⋅ K T d k ) ⋅ V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q \cdot K^T}{\sqrt{d_k}}\right) \cdot V </math>Attention(Q,K,V)=softmax(dk Q⋅KT)⋅V
这个公式中,没有任何地方体现Token的位置信息。
为什么位置很重要?
自然语言中,位置决定语义:
- "我 不 喜欢 你" vs "我 喜欢 你 不?"(语义完全不同)
- "小明 打 了 小红" vs "小红 打 了 小明"(主宾关系颠倒)
- "因为下雨,所以取消" vs "取消,所以因为下雨"(因果关系混乱)
更技术性的原因:
- 语法结构:主语在前、谓语在中、宾语在后
- 时间顺序:事件发生的先后顺序
- 依赖关系:前面的Token被后面的Token引用(代词指代)
- 自回归生成:生成第n+1个Token时,只能看前n个Token,不能看"未来"
因此,我们必须给模型注入位置信息,这就是位置编码的作用。
位置编码的核心思想
位置编码的目标很简单:
为每个位置生成一个唯一的向量,并将其加到Token的Embedding上,让模型知道"这个Token在序列中的第几个位置"
数学表达:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 输入带位置信息的表示 = Token Embedding + Positional Encoding X with_pos [ i ] = X [ i ] + PE [ i ] \begin{aligned} \text{输入带位置信息的表示} &= \text{Token Embedding} + \text{Positional Encoding} \\ X_{\text{with\_pos}}[i] &= X[i] + \text{PE}[i] \end{aligned} </math>输入带位置信息的表示Xwith_pos[i]=Token Embedding+Positional Encoding=X[i]+PE[i]
其中:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> X [ i ] X[i] </math>X[i] 是第i个Token的原始Embedding( <math xmlns="http://www.w3.org/1998/Math/MathML"> d model d_{\text{model}} </math>dmodel维)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> PE [ i ] \text{PE}[i] </math>PE[i] 是第i个位置的位置编码向量(同样是 <math xmlns="http://www.w3.org/1998/Math/MathML"> d model d_{\text{model}} </math>dmodel维)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> X with_pos [ i ] X_{\text{with\_pos}}[i] </math>Xwith_pos[i] 是最终输入到注意力层的表示
原始位置编码(Sinusoidal Positional Encoding)
Transformer原始论文(Vaswani et al., 2017)提出了一种基于正弦和余弦函数的位置编码方案。
公式
对于位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> pos \text{pos} </math>pos(第几个Token,从0开始)和维度 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i(向量的第几维,从0开始):
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> PE ( pos , 2 i ) = sin ( pos 1000 0 2 i / d model ) PE ( pos , 2 i + 1 ) = cos ( pos 1000 0 2 i / d model ) \begin{aligned} \text{PE}(\text{pos}, 2i) &= \sin\left(\frac{\text{pos}}{10000^{2i/d_{\text{model}}}}\right) \\ \text{PE}(\text{pos}, 2i+1) &= \cos\left(\frac{\text{pos}}{10000^{2i/d_{\text{model}}}}\right) \end{aligned} </math>PE(pos,2i)PE(pos,2i+1)=sin(100002i/dmodelpos)=cos(100002i/dmodelpos)
参数解释:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> pos \text{pos} </math>pos:Token的位置索引(0, 1, 2, 3, ...)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i:位置编码向量的维度索引( <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 , 1 , 2 , ... , d model / 2 − 1 0, 1, 2, \ldots, d_{\text{model}}/2 - 1 </math>0,1,2,...,dmodel/2−1)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2i </math>2i:偶数维度使用sin函数
- <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i + 1 2i+1 </math>2i+1:奇数维度使用cos函数
- <math xmlns="http://www.w3.org/1998/Math/MathML"> 10000 10000 </math>10000:基数,控制频率的衰减速度
- <math xmlns="http://www.w3.org/1998/Math/MathML"> d model d_{\text{model}} </math>dmodel:位置编码向量的维度(与Token Embedding维度相同)
直观理解
这个公式的核心思想是:使用不同频率的正弦波来编码位置
想象一下时钟:
- 秒针 :转得很快,频率高(对应高维度, <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2i </math>2i很大)
- 分针:转得中等,频率中等
- 时针 :转得很慢,频率低(对应低维度, <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2i </math>2i很小)
不同的时刻,秒针、分针、时针的组合是唯一的,这就能唯一标识一个时间点。
类似地:
- 低维度( <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2i </math>2i小):使用低频正弦波,变化慢,能区分远距离的位置
- 高维度( <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2i </math>2i大):使用高频正弦波,变化快,能区分近距离的位置
具体例子
假设 <math xmlns="http://www.w3.org/1998/Math/MathML"> d model = 4 d_{\text{model}} = 4 </math>dmodel=4(简化),我们计算前3个位置的位置编码:
位置 pos=0:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> PE ( 0 , 0 ) = sin ( 0 / 1000 0 0 / 4 ) = sin ( 0 ) = 0 PE ( 0 , 1 ) = cos ( 0 / 1000 0 0 / 4 ) = cos ( 0 ) = 1 PE ( 0 , 2 ) = sin ( 0 / 1000 0 2 / 4 ) = sin ( 0 ) = 0 PE ( 0 , 3 ) = cos ( 0 / 1000 0 2 / 4 ) = cos ( 0 ) = 1 PE [ 0 ] = [ 0 , 1 , 0 , 1 ] \begin{aligned} \text{PE}(0, 0) &= \sin(0 / 10000^{0/4}) = \sin(0) = 0 \\ \text{PE}(0, 1) &= \cos(0 / 10000^{0/4}) = \cos(0) = 1 \\ \text{PE}(0, 2) &= \sin(0 / 10000^{2/4}) = \sin(0) = 0 \\ \text{PE}(0, 3) &= \cos(0 / 10000^{2/4}) = \cos(0) = 1 \\ \\ \text{PE}[0] &= [0, 1, 0, 1] \end{aligned} </math>PE(0,0)PE(0,1)PE(0,2)PE(0,3)PE[0]=sin(0/100000/4)=sin(0)=0=cos(0/100000/4)=cos(0)=1=sin(0/100002/4)=sin(0)=0=cos(0/100002/4)=cos(0)=1=[0,1,0,1]
位置 pos=1:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> PE ( 1 , 0 ) = sin ( 1 / 1000 0 0 / 4 ) = sin ( 1 ) ≈ 0.841 PE ( 1 , 1 ) = cos ( 1 / 1000 0 0 / 4 ) = cos ( 1 ) ≈ 0.540 PE ( 1 , 2 ) = sin ( 1 / 1000 0 2 / 4 ) = sin ( 0.01 ) ≈ 0.01 PE ( 1 , 3 ) = cos ( 1 / 1000 0 2 / 4 ) = cos ( 0.01 ) ≈ 1.0 PE [ 1 ] = [ 0.841 , 0.540 , 0.01 , 1.0 ] \begin{aligned} \text{PE}(1, 0) &= \sin(1 / 10000^{0/4}) = \sin(1) \approx 0.841 \\ \text{PE}(1, 1) &= \cos(1 / 10000^{0/4}) = \cos(1) \approx 0.540 \\ \text{PE}(1, 2) &= \sin(1 / 10000^{2/4}) = \sin(0.01) \approx 0.01 \\ \text{PE}(1, 3) &= \cos(1 / 10000^{2/4}) = \cos(0.01) \approx 1.0 \\ \\ \text{PE}[1] &= [0.841, 0.540, 0.01, 1.0] \end{aligned} </math>PE(1,0)PE(1,1)PE(1,2)PE(1,3)PE[1]=sin(1/100000/4)=sin(1)≈0.841=cos(1/100000/4)=cos(1)≈0.540=sin(1/100002/4)=sin(0.01)≈0.01=cos(1/100002/4)=cos(0.01)≈1.0=[0.841,0.540,0.01,1.0]
位置 pos=2:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> PE [ 2 ] = [ 0.909 , − 0.416 , 0.02 , 0.9998 ] \text{PE}[2] = [0.909, -0.416, 0.02, 0.9998] </math>PE[2]=[0.909,−0.416,0.02,0.9998]
可以看到,每个位置都有一个唯一的向量表示。
Sinusoidal编码的优势
- 确定性:不需要学习,直接用公式计算
- 外推性:即使训练时只见过长度100的序列,也能为长度200的序列生成位置编码
- 相对位置 :通过三角函数的性质,模型可以学习相对位置关系
- 数学性质: <math xmlns="http://www.w3.org/1998/Math/MathML"> PE ( pos + k ) \text{PE}(\text{pos}+k) </math>PE(pos+k) 可以表示为 <math xmlns="http://www.w3.org/1998/Math/MathML"> PE ( pos ) \text{PE}(\text{pos}) </math>PE(pos) 的线性变换
Sinusoidal编码的劣势
- 外推性有限:虽然理论上可以外推,但实际效果在超长序列上会下降
- 位置信息弱:只是简单的加法,位置信息容易被Embedding淹没
- 无法区分绝对位置重要性:远距离和近距离的位置用相同的编码方式
可学习的绝对位置编码(Learned Positional Encoding)
另一种简单的方案是:把位置编码当作模型参数,在训练中学习
实现方式
创建一个可学习的Embedding矩阵:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> PE learned ∈ R max_seq_len × d model \text{PE}_{\text{learned}} \in \mathbb{R}^{\text{max\_seq\len} \times d{\text{model}}} </math>PElearned∈Rmax_seq_len×dmodel
对于位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> pos \text{pos} </math>pos:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> PE [ pos ] = PE learned [ pos ] (直接查表) \text{PE}[\text{pos}] = \text{PE}_{\text{learned}}[\text{pos}] \quad \text{(直接查表)} </math>PE[pos]=PElearned[pos](直接查表)
参数解释:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> max_seq_len \text{max\_seq\_len} </math>max_seq_len:支持的最大序列长度(比如512、2048等)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> PE learned \text{PE}_{\text{learned}} </math>PElearned 是一个可训练的参数矩阵
- 训练时,这个矩阵会通过反向传播更新
优势与劣势
优势:
- 灵活性高,模型可以自己学习最优的位置表示
- 实现简单
劣势:
- 无法外推:如果训练时最大长度是512,那么无法处理长度超过512的序列
- 参数量增加:需要额外存储 <math xmlns="http://www.w3.org/1998/Math/MathML"> max_seq_len × d model \text{max\_seq\len} \times d{\text{model}} </math>max_seq_len×dmodel 个参数
这种方案在BERT、GPT等早期模型中使用,但现代大模型更倾向于使用RoPE等相对位置编码。
RoPE:旋转位置编码(Rotary Position Embedding)
RoPE(Su et al., 2021)是目前最流行的位置编码方案之一,被LLaMA、GPT-NeoX、PaLM等主流大模型采用。
核心思想:直接作用于注意力计算
RoPE与传统位置编码的最大区别 在于:它不是在输入阶段添加位置信息,而是直接作用在注意力机制的计算过程中。
传统方法(Sinusoidal、Learned PE):
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 步骤1:在输入阶段加入位置信息 X with_pos = X + PE 步骤2:计算Q、K、V Q = X with_pos ⋅ W Q K = X with_pos ⋅ W K V = X with_pos ⋅ W V 步骤3:计算注意力分数 Score = Q ⋅ K T \begin{aligned} &\text{步骤1:在输入阶段加入位置信息} \\ &X_{\text{with\pos}} = X + \text{PE} \\ \\ &\text{步骤2:计算Q、K、V} \\ &Q = X{\text{with\pos}} \cdot W_Q \\ &K = X{\text{with\pos}} \cdot W_K \\ &V = X{\text{with\_pos}} \cdot W_V \\ \\ &\text{步骤3:计算注意力分数} \\ &\text{Score} = Q \cdot K^T \end{aligned} </math>步骤1:在输入阶段加入位置信息Xwith_pos=X+PE步骤2:计算Q、K、VQ=Xwith_pos⋅WQK=Xwith_pos⋅WKV=Xwith_pos⋅WV步骤3:计算注意力分数Score=Q⋅KT
RoPE方法:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 步骤1:先计算Q、K(不含位置信息) Q = X ⋅ W Q K = X ⋅ W K V = X ⋅ W V 步骤2:对Q、K应用旋转矩阵(注入位置信息) Q with_pos [ m ] = R Θ ( m ) ⋅ Q [ m ] (位置m的Query向量) K with_pos [ n ] = R Θ ( n ) ⋅ K [ n ] (位置n的Key向量) 步骤3:计算注意力分数 Score ( m , n ) = Q with_pos [ m ] ⋅ K with_pos [ n ] T \begin{aligned} &\text{步骤1:先计算Q、K(不含位置信息)} \\ &Q = X \cdot W_Q \\ &K = X \cdot W_K \\ &V = X \cdot W_V \\ \\ &\text{步骤2:对Q、K应用旋转矩阵(注入位置信息)} \\ &Q_{\text{with\pos}}[m] = R\Theta(m) \cdot Q[m] \quad \text{(位置m的Query向量)} \\ &K_{\text{with\pos}}[n] = R\Theta(n) \cdot K[n] \quad \text{(位置n的Key向量)} \\ \\ &\text{步骤3:计算注意力分数} \\ &\text{Score}(m,n) = Q_{\text{with\pos}}[m] \cdot K{\text{with\_pos}}[n]^T \end{aligned} </math>步骤1:先计算Q、K(不含位置信息)Q=X⋅WQK=X⋅WKV=X⋅WV步骤2:对Q、K应用旋转矩阵(注入位置信息)Qwith_pos[m]=RΘ(m)⋅Q[m](位置m的Query向量)Kwith_pos[n]=RΘ(n)⋅K[n](位置n的Key向量)步骤3:计算注意力分数Score(m,n)=Qwith_pos[m]⋅Kwith_pos[n]T
关键区别:
- 传统方法:位置信息通过加法混入Embedding,然后一起参与Q、K、V的线性变换
- RoPE :位置信息通过旋转变换直接作用在已计算好的Q、K上,在注意力分数计算时才引入位置信息
为什么这样更好?
- 传统加法:位置信息和内容信息在Embedding层混合,不同的线性变换会破坏位置关系
- RoPE旋转:位置信息通过几何旋转方式注入,保持了相对位置的数学性质,使得 <math xmlns="http://www.w3.org/1998/Math/MathML"> Score ( m , n ) \text{Score}(m,n) </math>Score(m,n) 自然地只依赖相对位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( m − n ) (m-n) </math>(m−n)
RoPE的核心:通过旋转矩阵,将位置信息直接融入注意力计算的核心步骤,而不是简单地加在输入上
为什么叫"旋转"?
在二维平面上,旋转一个向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ 角度,可以用旋转矩阵表示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ x ′ y ′ ] = [ cos θ − sin θ sin θ cos θ ] [ x y ] \begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} </math>[x′y′]=[cosθsinθ−sinθcosθ][xy]
RoPE就是将这个思想推广到高维空间:每对维度作为一个平面,进行不同角度的旋转
RoPE的数学公式
对于位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 的Query向量和位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的Key向量,RoPE将它们分别旋转:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> q m = R Θ ( m ) ⋅ W Q ⋅ x m k n = R Θ ( n ) ⋅ W K ⋅ x n \begin{aligned} q_m &= R_\Theta(m) \cdot W_Q \cdot x_m \\ k_n &= R_\Theta(n) \cdot W_K \cdot x_n \end{aligned} </math>qmkn=RΘ(m)⋅WQ⋅xm=RΘ(n)⋅WK⋅xn
其中,旋转矩阵 <math xmlns="http://www.w3.org/1998/Math/MathML"> R Θ ( pos ) R_\Theta(\text{pos}) </math>RΘ(pos) 是一个分块对角矩阵:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> R Θ ( pos ) = [ cos ( pos ⋅ θ 0 ) − sin ( pos ⋅ θ 0 ) 0 0 ⋯ sin ( pos ⋅ θ 0 ) cos ( pos ⋅ θ 0 ) 0 0 ⋯ 0 0 cos ( pos ⋅ θ 1 ) − sin ( pos ⋅ θ 1 ) ⋯ 0 0 sin ( pos ⋅ θ 1 ) cos ( pos ⋅ θ 1 ) ⋯ ⋮ ⋮ ⋮ ⋮ ⋱ ] R_\Theta(\text{pos}) = \begin{bmatrix} \cos(\text{pos} \cdot \theta_0) & -\sin(\text{pos} \cdot \theta_0) & 0 & 0 & \cdots \\ \sin(\text{pos} \cdot \theta_0) & \cos(\text{pos} \cdot \theta_0) & 0 & 0 & \cdots \\ 0 & 0 & \cos(\text{pos} \cdot \theta_1) & -\sin(\text{pos} \cdot \theta_1) & \cdots \\ 0 & 0 & \sin(\text{pos} \cdot \theta_1) & \cos(\text{pos} \cdot \theta_1) & \cdots \\ \vdots & \vdots & \vdots & \vdots & \ddots \end{bmatrix} </math>RΘ(pos)= cos(pos⋅θ0)sin(pos⋅θ0)00⋮−sin(pos⋅θ0)cos(pos⋅θ0)00⋮00cos(pos⋅θ1)sin(pos⋅θ1)⋮00−sin(pos⋅θ1)cos(pos⋅θ1)⋮⋯⋯⋯⋯⋱
参数解释:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> pos \text{pos} </math>pos:Token的位置(0, 1, 2, ...)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> θ i \theta_i </math>θi:第i对维度的旋转频率,计算方式: <math xmlns="http://www.w3.org/1998/Math/MathML"> θ i = 1000 0 − 2 i / d model \theta_i = 10000^{-2i/d_{\text{model}}} </math>θi=10000−2i/dmodel
- 每两个维度一组,共 <math xmlns="http://www.w3.org/1998/Math/MathML"> d model / 2 d_{\text{model}}/2 </math>dmodel/2 组
- 每组使用不同的旋转频率 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ i \theta_i </math>θi
简化表示(逐元素形式)
为了更直观,我们可以用逐元素的方式表示RoPE:
对于Query向量的第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2i </math>2i 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i + 1 2i+1 </math>2i+1 维(一对维度):
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> q m [ 2 i ] = q [ 2 i ] ⋅ cos ( m ⋅ θ i ) − q [ 2 i + 1 ] ⋅ sin ( m ⋅ θ i ) q m [ 2 i + 1 ] = q [ 2 i ] ⋅ sin ( m ⋅ θ i ) + q [ 2 i + 1 ] ⋅ cos ( m ⋅ θ i ) \begin{aligned} q_m[2i] &= q[2i] \cdot \cos(m \cdot \theta_i) - q[2i+1] \cdot \sin(m \cdot \theta_i) \\ q_m[2i+1] &= q[2i] \cdot \sin(m \cdot \theta_i) + q[2i+1] \cdot \cos(m \cdot \theta_i) \end{aligned} </math>qm[2i]qm[2i+1]=q[2i]⋅cos(m⋅θi)−q[2i+1]⋅sin(m⋅θi)=q[2i]⋅sin(m⋅θi)+q[2i+1]⋅cos(m⋅θi)
对于Key向量同理:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> k n [ 2 i ] = k [ 2 i ] ⋅ cos ( n ⋅ θ i ) − k [ 2 i + 1 ] ⋅ sin ( n ⋅ θ i ) k n [ 2 i + 1 ] = k [ 2 i ] ⋅ sin ( n ⋅ θ i ) + k [ 2 i + 1 ] ⋅ cos ( n ⋅ θ i ) \begin{aligned} k_n[2i] &= k[2i] \cdot \cos(n \cdot \theta_i) - k[2i+1] \cdot \sin(n \cdot \theta_i) \\ k_n[2i+1] &= k[2i] \cdot \sin(n \cdot \theta_i) + k[2i+1] \cdot \cos(n \cdot \theta_i) \end{aligned} </math>kn[2i]kn[2i+1]=k[2i]⋅cos(n⋅θi)−k[2i+1]⋅sin(n⋅θi)=k[2i]⋅sin(n⋅θi)+k[2i+1]⋅cos(n⋅θi)
其中:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> θ i = 1000 0 − 2 i / d model \theta_i = 10000^{-2i/d_{\text{model}}} </math>θi=10000−2i/dmodel
RoPE融合进注意力计算的完整流程
让我们详细看看RoPE是如何一步步融合进注意力机制的计算过程的。
假设我们有一个序列:["我", "喜欢", "猫"],共3个Token,位置分别为0、1、2。
步骤1:获取Token Embedding(不含位置信息)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> X = [ x 0 x 1 x 2 ] ∈ R 3 × d model \begin{aligned} X &= \begin{bmatrix} x_0 \\ x_1 \\ x_2 \end{bmatrix} \in \mathbb{R}^{3 \times d_{\text{model}}} \end{aligned} </math>X= x0x1x2 ∈R3×dmodel
其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 0 x_0 </math>x0、 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 1 x_1 </math>x1、 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 2 x_2 </math>x2 分别是"我"、"喜欢"、"猫"的Embedding向量。
步骤2:计算原始的Q、K、V(仍不含位置信息)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Q = X ⋅ W Q = [ q 0 q 1 q 2 ] ∈ R 3 × d k K = X ⋅ W K = [ k 0 k 1 k 2 ] ∈ R 3 × d k V = X ⋅ W V = [ v 0 v 1 v 2 ] ∈ R 3 × d v \begin{aligned} Q &= X \cdot W_Q = \begin{bmatrix} q_0 \\ q_1 \\ q_2 \end{bmatrix} \in \mathbb{R}^{3 \times d_k} \\ K &= X \cdot W_K = \begin{bmatrix} k_0 \\ k_1 \\ k_2 \end{bmatrix} \in \mathbb{R}^{3 \times d_k} \\ V &= X \cdot W_V = \begin{bmatrix} v_0 \\ v_1 \\ v_2 \end{bmatrix} \in \mathbb{R}^{3 \times d_v} \end{aligned} </math>QKV=X⋅WQ= q0q1q2 ∈R3×dk=X⋅WK= k0k1k2 ∈R3×dk=X⋅WV= v0v1v2 ∈R3×dv
注意:到这一步为止,Q、K、V都还没有任何位置信息!
步骤3:对Q、K应用RoPE旋转(注入位置信息)
这是RoPE的核心步骤!对每个位置的Q和K向量应用旋转:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 位置0的Token:"我" Q rot [ 0 ] = R Θ ( 0 ) ⋅ q 0 (旋转0度,保持不变) K rot [ 0 ] = R Θ ( 0 ) ⋅ k 0 位置1的Token:"喜欢" Q rot [ 1 ] = R Θ ( 1 ) ⋅ q 1 (旋转 θ 角度) K rot [ 1 ] = R Θ ( 1 ) ⋅ k 1 位置2的Token:"猫" Q rot [ 2 ] = R Θ ( 2 ) ⋅ q 2 (旋转 2 θ 角度) K rot [ 2 ] = R Θ ( 2 ) ⋅ k 2 \begin{aligned} &\text{位置0的Token:"我"} \\ &\quad Q_{\text{rot}}[0] = R_\Theta(0) \cdot q_0 \quad \text{(旋转0度,保持不变)} \\ &\quad K_{\text{rot}}[0] = R_\Theta(0) \cdot k_0 \\ \\ &\text{位置1的Token:"喜欢"} \\ &\quad Q_{\text{rot}}[1] = R_\Theta(1) \cdot q_1 \quad \text{(旋转}\theta\text{角度)} \\ &\quad K_{\text{rot}}[1] = R_\Theta(1) \cdot k_1 \\ \\ &\text{位置2的Token:"猫"} \\ &\quad Q_{\text{rot}}[2] = R_\Theta(2) \cdot q_2 \quad \text{(旋转}2\theta\text{角度)} \\ &\quad K_{\text{rot}}[2] = R_\Theta(2) \cdot k_2 \end{aligned} </math>位置0的Token:"我"Qrot[0]=RΘ(0)⋅q0(旋转0度,保持不变)Krot[0]=RΘ(0)⋅k0位置1的Token:"喜欢"Qrot[1]=RΘ(1)⋅q1(旋转θ角度)Krot[1]=RΘ(1)⋅k1位置2的Token:"猫"Qrot[2]=RΘ(2)⋅q2(旋转2θ角度)Krot[2]=RΘ(2)⋅k2
V向量不旋转,因为V包含的是"内容信息",只有Q和K需要位置信息来计算相关性。
步骤4:计算注意力分数矩阵(位置信息已融合)
现在计算所有位置对之间的注意力分数:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Scores = Q rot ⋅ K rot T = [ Q rot [ 0 ] ⋅ K rot [ 0 ] T Q rot [ 0 ] ⋅ K rot [ 1 ] T Q rot [ 0 ] ⋅ K rot [ 2 ] T Q rot [ 1 ] ⋅ K rot [ 0 ] T Q rot [ 1 ] ⋅ K rot [ 1 ] T Q rot [ 1 ] ⋅ K rot [ 2 ] T Q rot [ 2 ] ⋅ K rot [ 0 ] T Q rot [ 2 ] ⋅ K rot [ 1 ] T Q rot [ 2 ] ⋅ K rot [ 2 ] T ] \text{Scores} = Q_{\text{rot}} \cdot K_{\text{rot}}^T = \begin{bmatrix} Q_{\text{rot}}[0] \cdot K_{\text{rot}}[0]^T & Q_{\text{rot}}[0] \cdot K_{\text{rot}}[1]^T & Q_{\text{rot}}[0] \cdot K_{\text{rot}}[2]^T \\ Q_{\text{rot}}[1] \cdot K_{\text{rot}}[0]^T & Q_{\text{rot}}[1] \cdot K_{\text{rot}}[1]^T & Q_{\text{rot}}[1] \cdot K_{\text{rot}}[2]^T \\ Q_{\text{rot}}[2] \cdot K_{\text{rot}}[0]^T & Q_{\text{rot}}[2] \cdot K_{\text{rot}}[1]^T & Q_{\text{rot}}[2] \cdot K_{\text{rot}}[2]^T \end{bmatrix} </math>Scores=Qrot⋅KrotT= Qrot[0]⋅Krot[0]TQrot[1]⋅Krot[0]TQrot[2]⋅Krot[0]TQrot[0]⋅Krot[1]TQrot[1]⋅Krot[1]TQrot[2]⋅Krot[1]TQrot[0]⋅Krot[2]TQrot[1]⋅Krot[2]TQrot[2]⋅Krot[2]T
关键 :每个分数 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q rot [ m ] ⋅ K rot [ n ] T Q_{\text{rot}}[m] \cdot K_{\text{rot}}[n]^T </math>Qrot[m]⋅Krot[n]T 自动包含了位置m和位置n之间的相对位置信息 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( m − n ) (m-n) </math>(m−n)!
步骤5:应用Softmax和加权求和(标准流程)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Attention_Weights = softmax ( Scores d k ) Output = Attention_Weights ⋅ V \begin{aligned} \text{Attention\_Weights} &= \text{softmax}\left(\frac{\text{Scores}}{\sqrt{d_k}}\right) \\ \text{Output} &= \text{Attention\_Weights} \cdot V \end{aligned} </math>Attention_WeightsOutput=softmax(dk Scores)=Attention_Weights⋅V
对比总结:RoPE vs 传统方法
| 步骤 | 传统位置编码 | RoPE |
|---|---|---|
| 1. 输入 | <math xmlns="http://www.w3.org/1998/Math/MathML"> X + PE X + \text{PE} </math>X+PE | <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X(纯内容) |
| 2. 计算QKV | <math xmlns="http://www.w3.org/1998/Math/MathML"> Q = ( X + PE ) ⋅ W Q Q = (X + \text{PE}) \cdot W_Q </math>Q=(X+PE)⋅WQ | <math xmlns="http://www.w3.org/1998/Math/MathML"> Q = X ⋅ W Q Q = X \cdot W_Q </math>Q=X⋅WQ |
| 3. 位置注入 | ❌(已在步骤1完成) | ✅ <math xmlns="http://www.w3.org/1998/Math/MathML"> Q rot = R Θ ( pos ) ⋅ Q Q_{\text{rot}} = R_\Theta(\text{pos}) \cdot Q </math>Qrot=RΘ(pos)⋅Q |
| 4. 注意力分数 | <math xmlns="http://www.w3.org/1998/Math/MathML"> Q ⋅ K T Q \cdot K^T </math>Q⋅KT(位置信息已稀释) | <math xmlns="http://www.w3.org/1998/Math/MathML"> Q rot ⋅ K rot T Q_{\text{rot}} \cdot K_{\text{rot}}^T </math>Qrot⋅KrotT(位置信息精确) |
| 结果 | 位置信息间接、可能被削弱 | 位置信息直接、保留相对关系 |
核心优势 :RoPE在注意力分数计算的关键时刻才引入位置信息,通过旋转的几何性质,保证了注意力分数只依赖相对位置差,而不是绝对位置。
RoPE的关键性质
性质1:注意力分数自动包含相对位置信息
当计算位置m和位置n之间的注意力分数时:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Attention_Score ( m , n ) = q m T ⋅ k n = (展开后,对于第i对维度) = [ q [ 2 i ] ⋅ k [ 2 i ] + q [ 2 i + 1 ] ⋅ k [ 2 i + 1 ] ] × cos ( ( m − n ) ⋅ θ i ) \begin{aligned} \text{Attention\_Score}(m, n) &= q_m^T \cdot k_n \\ &= \text{(展开后,对于第i对维度)} \\ &= \left[q[2i] \cdot k[2i] + q[2i+1] \cdot k[2i+1]\right] \times \cos((m-n) \cdot \theta_i) \end{aligned} </math>Attention_Score(m,n)=qmT⋅kn=(展开后,对于第i对维度)=[q[2i]⋅k[2i]+q[2i+1]⋅k[2i+1]]×cos((m−n)⋅θi)
核心发现 :注意力分数只依赖于 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( m − n ) (m-n) </math>(m−n),即相对位置差,而不是绝对位置m或n!
这意味着:
- 位置0的Token看位置1的Token,与位置5的Token看位置6的Token,注意力模式相同(相对位置都是+1)
- 模型自然地学习到相对位置关系
性质2:远距离衰减
由于使用了不同频率的旋转:
- 低频分量( <math xmlns="http://www.w3.org/1998/Math/MathML"> θ i \theta_i </math>θi 小):捕捉长距离依赖
- 高频分量( <math xmlns="http://www.w3.org/1998/Math/MathML"> θ i \theta_i </math>θi 大):捕捉短距离依赖
相对位置距离越远,高频分量的点积越接近0,注意力自然衰减。
具体例子
假设 <math xmlns="http://www.w3.org/1998/Math/MathML"> d model = 4 d_{\text{model}} = 4 </math>dmodel=4,我们计算位置0和位置1的Query向量:
步骤1:计算旋转频率
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> θ 0 = 1000 0 − 0 / 4 = 1 θ 1 = 1000 0 − 2 / 4 = 0.01 \begin{aligned} \theta_0 &= 10000^{-0/4} = 1 \\ \theta_1 &= 10000^{-2/4} = 0.01 \end{aligned} </math>θ0θ1=10000−0/4=1=10000−2/4=0.01
步骤2:对位置m=0,旋转角度为0
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> q 0 [ 0 ] = q [ 0 ] ⋅ cos ( 0 ⋅ 1 ) − q [ 1 ] ⋅ sin ( 0 ⋅ 1 ) = q [ 0 ] q 0 [ 1 ] = q [ 0 ] ⋅ sin ( 0 ⋅ 1 ) + q [ 1 ] ⋅ cos ( 0 ⋅ 1 ) = q [ 1 ] q 0 [ 2 ] = q [ 2 ] ⋅ cos ( 0 ⋅ 0.01 ) − q [ 3 ] ⋅ sin ( 0 ⋅ 0.01 ) = q [ 2 ] q 0 [ 3 ] = q [ 2 ] ⋅ sin ( 0 ⋅ 0.01 ) + q [ 3 ] ⋅ cos ( 0 ⋅ 0.01 ) = q [ 3 ] \begin{aligned} q_0[0] &= q[0] \cdot \cos(0 \cdot 1) - q[1] \cdot \sin(0 \cdot 1) = q[0] \\ q_0[1] &= q[0] \cdot \sin(0 \cdot 1) + q[1] \cdot \cos(0 \cdot 1) = q[1] \\ q_0[2] &= q[2] \cdot \cos(0 \cdot 0.01) - q[3] \cdot \sin(0 \cdot 0.01) = q[2] \\ q_0[3] &= q[2] \cdot \sin(0 \cdot 0.01) + q[3] \cdot \cos(0 \cdot 0.01) = q[3] \end{aligned} </math>q0[0]q0[1]q0[2]q0[3]=q[0]⋅cos(0⋅1)−q[1]⋅sin(0⋅1)=q[0]=q[0]⋅sin(0⋅1)+q[1]⋅cos(0⋅1)=q[1]=q[2]⋅cos(0⋅0.01)−q[3]⋅sin(0⋅0.01)=q[2]=q[2]⋅sin(0⋅0.01)+q[3]⋅cos(0⋅0.01)=q[3]
位置0不旋转,保持原样。
步骤3:对位置m=1,旋转角度为θ
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> q 1 [ 0 ] = q [ 0 ] ⋅ cos ( 1 ⋅ 1 ) − q [ 1 ] ⋅ sin ( 1 ⋅ 1 ) ≈ 0.540 ⋅ q [ 0 ] − 0.841 ⋅ q [ 1 ] q 1 [ 1 ] = q [ 0 ] ⋅ sin ( 1 ⋅ 1 ) + q [ 1 ] ⋅ cos ( 1 ⋅ 1 ) ≈ 0.841 ⋅ q [ 0 ] + 0.540 ⋅ q [ 1 ] q 1 [ 2 ] = q [ 2 ] ⋅ cos ( 1 ⋅ 0.01 ) − q [ 3 ] ⋅ sin ( 1 ⋅ 0.01 ) ≈ 0.99995 ⋅ q [ 2 ] − 0.01 ⋅ q [ 3 ] q 1 [ 3 ] = q [ 2 ] ⋅ sin ( 1 ⋅ 0.01 ) + q [ 3 ] ⋅ cos ( 1 ⋅ 0.01 ) ≈ 0.01 ⋅ q [ 2 ] + 0.99995 ⋅ q [ 3 ] \begin{aligned} q_1[0] &= q[0] \cdot \cos(1 \cdot 1) - q[1] \cdot \sin(1 \cdot 1) \approx 0.540 \cdot q[0] - 0.841 \cdot q[1] \\ q_1[1] &= q[0] \cdot \sin(1 \cdot 1) + q[1] \cdot \cos(1 \cdot 1) \approx 0.841 \cdot q[0] + 0.540 \cdot q[1] \\ q_1[2] &= q[2] \cdot \cos(1 \cdot 0.01) - q[3] \cdot \sin(1 \cdot 0.01) \approx 0.99995 \cdot q[2] - 0.01 \cdot q[3] \\ q_1[3] &= q[2] \cdot \sin(1 \cdot 0.01) + q[3] \cdot \cos(1 \cdot 0.01) \approx 0.01 \cdot q[2] + 0.99995 \cdot q[3] \end{aligned} </math>q1[0]q1[1]q1[2]q1[3]=q[0]⋅cos(1⋅1)−q[1]⋅sin(1⋅1)≈0.540⋅q[0]−0.841⋅q[1]=q[0]⋅sin(1⋅1)+q[1]⋅cos(1⋅1)≈0.841⋅q[0]+0.540⋅q[1]=q[2]⋅cos(1⋅0.01)−q[3]⋅sin(1⋅0.01)≈0.99995⋅q[2]−0.01⋅q[3]=q[2]⋅sin(1⋅0.01)+q[3]⋅cos(1⋅0.01)≈0.01⋅q[2]+0.99995⋅q[3]
可以看到:
- 第0-1维(高频):旋转了约54度,变化明显
- 第2-3维(低频):只旋转了约0.57度,几乎不变
RoPE的优势
- 相对位置编码:注意力分数自动包含相对位置信息,不依赖绝对位置
- 外推性好:理论上可以处理任意长度的序列
- 长距离衰减:远距离Token的注意力自然衰减,符合语言学规律
- 无额外参数:不增加模型参数量
- 高效实现:可以预计算旋转矩阵,推理时直接查表
RoPE的实现细节
在实际代码中,RoPE通常这样实现。下面展示完整的带RoPE的注意力计算流程:
python
import torch
import torch.nn.functional as F
# ============ 第一步:预计算RoPE的旋转矩阵(初始化时执行一次) ============
def precompute_rope_cache(d_model, max_seq_len=2048):
"""
预计算RoPE需要的cos和sin值
"""
# 计算旋转频率 θ_i = 10000^(-2i/d_model)
theta = 10000 ** (-2 * torch.arange(d_model // 2) / d_model)
# theta shape: (d_model/2,)
# 生成位置索引 [0, 1, 2, ..., max_seq_len-1]
pos = torch.arange(max_seq_len)
# pos shape: (max_seq_len,)
# 计算所有位置和所有频率的组合:pos * θ_i
freqs = torch.outer(pos, theta) # shape: (max_seq_len, d_model/2)
# 预计算cos和sin值,推理时直接查表
cos_cache = freqs.cos() # shape: (max_seq_len, d_model/2)
sin_cache = freqs.sin() # shape: (max_seq_len, d_model/2)
return cos_cache, sin_cache
# ============ 第二步:应用RoPE旋转(在每次forward时执行) ============
def apply_rope(x, cos, sin):
"""
对Q或K向量应用RoPE旋转
Args:
x: shape (batch, seq_len, d_model) - Q或K矩阵
cos: shape (seq_len, d_model/2) - 预计算的cos值
sin: shape (seq_len, d_model/2) - 预计算的sin值
Returns:
旋转后的向量,shape (batch, seq_len, d_model)
"""
# 将x分为偶数维和奇数维
x1 = x[..., 0::2] # shape: (batch, seq_len, d_model/2) - 第0,2,4,...维
x2 = x[..., 1::2] # shape: (batch, seq_len, d_model/2) - 第1,3,5,...维
# 应用旋转公式:
# x_rot[2i] = x[2i] * cos(pos*θ_i) - x[2i+1] * sin(pos*θ_i)
# x_rot[2i+1] = x[2i] * sin(pos*θ_i) + x[2i+1] * cos(pos*θ_i)
x_rotated = torch.stack([
x1 * cos - x2 * sin, # 偶数维
x1 * sin + x2 * cos # 奇数维
], dim=-1).flatten(-2) # 交错拼接回 (batch, seq_len, d_model)
return x_rotated
# ============ 第三步:完整的带RoPE的注意力计算 ============
def attention_with_rope(X, W_Q, W_K, W_V, cos_cache, sin_cache):
"""
完整的注意力计算流程,展示RoPE如何融合进来
Args:
X: shape (batch, seq_len, d_model) - 输入的Token Embeddings(不含位置信息)
W_Q, W_K, W_V: 权重矩阵
cos_cache, sin_cache: 预计算的RoPE缓存
"""
batch, seq_len, d_model = X.shape
# 步骤1:计算原始的Q、K、V(不含位置信息)
Q = torch.matmul(X, W_Q) # shape: (batch, seq_len, d_k)
K = torch.matmul(X, W_K) # shape: (batch, seq_len, d_k)
V = torch.matmul(X, W_V) # shape: (batch, seq_len, d_v)
print("步骤1完成:计算Q、K、V(纯内容,无位置信息)")
# 步骤2:对Q、K应用RoPE旋转(注入位置信息)
# 这是RoPE的核心!位置信息在这里融入
cos = cos_cache[:seq_len] # 截取当前序列长度
sin = sin_cache[:seq_len]
Q_rot = apply_rope(Q, cos, sin) # shape: (batch, seq_len, d_k)
K_rot = apply_rope(K, cos, sin) # shape: (batch, seq_len, d_k)
# 注意:V不旋转!V只包含内容信息
print("步骤2完成:对Q、K应用旋转(位置信息已注入)")
# 步骤3:计算注意力分数(位置信息已在Q_rot和K_rot中)
d_k = Q_rot.shape[-1]
scores = torch.matmul(Q_rot, K_rot.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k))
# scores shape: (batch, seq_len, seq_len)
print("步骤3完成:计算注意力分数(自动包含相对位置信息)")
# 步骤4:Softmax + 加权求和(标准流程)
attn_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, V) # shape: (batch, seq_len, d_v)
print("步骤4完成:加权求和得到输出")
return output, attn_weights
# ============ 使用示例 ============
# 初始化(只需一次)
d_model = 512
max_seq_len = 2048
cos_cache, sin_cache = precompute_rope_cache(d_model, max_seq_len)
# 前向传播(每次推理)
batch_size = 2
seq_len = 10
X = torch.randn(batch_size, seq_len, d_model) # 输入Token Embeddings
# 假设已初始化权重矩阵
W_Q = torch.randn(d_model, d_model)
W_K = torch.randn(d_model, d_model)
W_V = torch.randn(d_model, d_model)
# 执行注意力计算(RoPE在步骤2自动应用)
output, attn_weights = attention_with_rope(X, W_Q, W_K, W_V, cos_cache, sin_cache)
print(f"\n最终输出 shape: {output.shape}") # (batch_size, seq_len, d_model)
代码关键点解释:
-
预计算阶段 (
precompute_rope_cache):- 只在模型初始化时执行一次
- 计算所有可能位置的 <math xmlns="http://www.w3.org/1998/Math/MathML"> cos ( pos ⋅ θ i ) \cos(\text{pos} \cdot \theta_i) </math>cos(pos⋅θi) 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> sin ( pos ⋅ θ i ) \sin(\text{pos} \cdot \theta_i) </math>sin(pos⋅θi)
- 存储在缓存中,推理时直接查表
-
RoPE应用阶段 (
apply_rope):- 在计算完Q、K之后立即应用
- 将向量的每对维度 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 2 i , 2 i + 1 ) (2i, 2i+1) </math>(2i,2i+1) 作为一个平面进行旋转
- V向量不旋转,因为V存储的是"内容",不需要位置信息
-
融合进注意力计算 (
attention_with_rope):- 步骤1: <math xmlns="http://www.w3.org/1998/Math/MathML"> Q = X ⋅ W Q Q = X \cdot W_Q </math>Q=X⋅WQ(不含位置)
- 步骤2: <math xmlns="http://www.w3.org/1998/Math/MathML"> Q rot = R Θ ( pos ) ⋅ Q Q_{\text{rot}} = R_\Theta(\text{pos}) \cdot Q </math>Qrot=RΘ(pos)⋅Q(RoPE在这里注入位置)
- 步骤3: <math xmlns="http://www.w3.org/1998/Math/MathML"> Scores = Q rot ⋅ K rot T \text{Scores} = Q_{\text{rot}} \cdot K_{\text{rot}}^T </math>Scores=Qrot⋅KrotT(位置信息自动体现在分数中)
- 步骤4:标准的softmax和加权求和
与传统方法的对比:
| 时间点 | 传统位置编码 | RoPE |
|---|---|---|
| 输入阶段 | X = X + PE(位置信息混入) |
X(纯内容) |
| 计算QKV | Q = X · W_Q(位置已混入) |
Q = X · W_Q(纯内容) |
| 位置注入 | ❌(已完成) | ✅ Q_rot = apply_rope(Q)(在这里!) |
| 计算分数 | Q · K^T |
Q_rot · K_rot^T |
RoPE的优势在于:位置信息在注意力分数计算的关键时刻才引入,通过旋转的几何性质精确地编码了相对位置关系。
RoPE的长度扩展技术
虽然RoPE理论上可以外推,但在实际应用中,当序列长度远超训练时的长度时,性能会下降。为此,研究者提出了多种长度扩展技术。
问题:为什么需要长度扩展?
假设模型在训练时只见过长度≤2048的序列,当推理时输入长度4096的序列:
- 位置编码会遇到"未见过"的旋转角度
- 高频分量的旋转角度过大,导致注意力模式混乱
- 模型性能显著下降
方法1:位置插值(Position Interpolation, PI)
核心思想:将长序列的位置"压缩"到训练时的范围内
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 原始位置: pos ∈ [ 0 , L new ] 压缩后: pos ′ = pos ⋅ L train L new \begin{aligned} \text{原始位置:} & \quad \text{pos} \in [0, L_{\text{new}}] \\ \text{压缩后:} & \quad \text{pos}' = \text{pos} \cdot \frac{L_{\text{train}}}{L_{\text{new}}} \end{aligned} </math>原始位置:压缩后:pos∈[0,Lnew]pos′=pos⋅LnewLtrain
参数解释:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> L train L_{\text{train}} </math>Ltrain:训练时的最大序列长度(如2048)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> L new L_{\text{new}} </math>Lnew:推理时的目标序列长度(如8192)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> pos ′ \text{pos}' </math>pos′:压缩后的位置,范围在 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 0 , L train ] [0, L_{\text{train}}] </math>[0,Ltrain] 内
举例:
- 训练长度2048,推理长度8192
- 推理时位置4096 → 压缩为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4096 × ( 2048 / 8192 ) = 1024 4096 \times (2048/8192) = 1024 </math>4096×(2048/8192)=1024
- 推理时位置8192 → 压缩为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 8192 × ( 2048 / 8192 ) = 2048 8192 \times (2048/8192) = 2048 </math>8192×(2048/8192)=2048
优势:
- 简单有效,只需修改位置索引
- 所有位置都在训练范围内,模型"见过"
劣势:
- 改变了相对位置的含义(相邻Token的距离变小了)
- 需要少量微调来适应
方法2:NTK-Aware插值
核心思想:不是简单压缩位置,而是调整旋转频率的基数(将10000改为更大的值)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 原始频率: θ i = 1000 0 − 2 i / d model NTK频率: θ i ′ = ( 10000 ⋅ scale ) − 2 i / d model \begin{aligned} \text{原始频率:} & \quad \theta_i = 10000^{-2i/d_{\text{model}}} \\ \text{NTK频率:} & \quad \theta_i' = (10000 \cdot \text{scale})^{-2i/d_{\text{model}}} \end{aligned} </math>原始频率:NTK频率:θi=10000−2i/dmodelθi′=(10000⋅scale)−2i/dmodel
其中:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> scale = L new L train \text{scale} = \frac{L_{\text{new}}}{L_{\text{train}}} </math>scale=LtrainLnew
参数解释:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> scale \text{scale} </math>scale:长度扩展倍数
- 基数从10000增大到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 10000 × scale 10000 \times \text{scale} </math>10000×scale
- 旋转频率整体降低,适应更长的序列
举例:
- 训练长度2048,推理长度8192,scale=4
- 基数从10000变为40000
- 旋转速度降低4倍,适配4倍长的序列
优势:
- 保持了相对位置的语义
- 不需要微调,零样本外推效果好
劣势:
- 理论分析较复杂
- 对不同频率分量的影响不均匀
方法3:YaRN(Yet another RoPE extensioN)
核心思想:对不同频率分量采用不同的插值策略
- 低频分量 ( <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ小):捕捉长距离依赖,使用NTK插值
- 高频分量 ( <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ大):捕捉短距离依赖,保持不变或轻微插值
- 中频分量:渐进式插值
这种方法在LLaMA-2等模型中取得了很好的效果,可以将上下文长度扩展到32k甚至更长。
长度扩展对比
| 方法 | 是否需要微调 | 外推效果 | 计算开销 |
|---|---|---|---|
| 位置插值(PI) | 需要少量微调 | 好 | 无额外开销 |
| NTK-Aware | 零样本 | 较好 | 无额外开销 |
| YaRN | 零样本或少量微调 | 很好 | 无额外开销 |
小结
-
位置编码的必要性:注意力机制天生无法感知位置,必须显式注入位置信息
-
传统位置编码:
- Sinusoidal编码:使用sin/cos函数,确定性、可外推,但效果一般
- 可学习编码:灵活但无法外推
-
RoPE(旋转位置编码):
- 通过旋转矩阵将位置信息融入Q、K
- 注意力分数自动包含相对位置信息
- 外推性好、无额外参数、长距离自然衰减
- 成为现代大模型的主流选择
-
长度扩展技术:
- 通过位置插值、频率调整等方法,让模型适应超长序列
- 核心是平衡"训练时的位置模式"和"推理时的长度需求"
位置编码看似简单,但对大模型的性能至关重要。RoPE的成功说明,好的位置编码应该捕捉相对位置关系,而不是绝对位置,这样才能具备良好的泛化能力。