注意力机制的"位置盲区"

在上一章中,我们学习了注意力机制如何通过QKV矩阵计算Token之间的相关性。但这里有一个严重的问题

注意力机制天生是"位置不敏感"的!

问题演示

考虑以下两个句子:

  1. "猫 吃 鱼"
  2. "鱼 吃 猫"

对于注意力机制来说,如果我们交换Token的顺序,计算过程是这样的:

句子1的注意力分数矩阵:Scores1=Q1K1T句子2(交换位置后):Scores2=Q2K2Tbegin{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}

由于 QQKKVV 都是通过相同的权重矩阵 WQW_QWKW_KWVW_V 从Embedding计算得到的,如果我们只是交换了Token的顺序,而不告诉模型"位置信息",那么注意力机制会认为这两个句子是等价的!

具体来说,注意力计算公式:

Attention(Q,K,V)=softmax(QKTdk)Vtext{Attention}(Q, K, V) = text{softmax}left(frac{Q cdot K^T}{sqrt{d_k}}right) cdot V

这个公式中,没有任何地方体现Token的位置信息

为什么位置很重要?

自然语言中,位置决定语义

  • "我 不 喜欢 你" vs "我 喜欢 你 不?"(语义完全不同)
  • "小明 打 了 小红" vs "小红 打 了 小明"(主宾关系颠倒)
  • "因为下雨,所以取消" vs "取消,所以因为下雨"(因果关系混乱)

更技术性的原因:

  1. 语法结构:主语在前、谓语在中、宾语在后
  2. 时间顺序:事件发生的先后顺序
  3. 依赖关系:前面的Token被后面的Token引用(代词指代)
  4. 自回归生成:生成第n+1个Token时,只能看前n个Token,不能看"未来"

因此,我们必须给模型注入位置信息,这就是位置编码的作用。

位置编码的核心思想

位置编码的目标很简单:

数学表达:

输入带位置信息的表示=Token Embedding+Positional EncodingXwith_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}

其中:

  • X[i]X[i] 是第i个Token的原始Embedding(dmodeld_{text{model}}维)
  • PE[i]text{PE}[i] 是第i个位置的位置编码向量(同样是dmodeld_{text{model}}维)
  • Xwith_pos[i]X_{text{with_pos}}[i] 是最终输入到注意力层的表示

原始位置编码(Sinusoidal Positional Encoding)

Transformer原始论文(Vaswani et al., 2017)提出了一种基于正弦和余弦函数的位置编码方案。

公式

对于位置 postext{pos}(第几个Token,从0开始)和维度 ii(向量的第几维,从0开始):

PE(pos,2i)=sin(pos100002i/dmodel)PE(pos,2i+1)=cos(pos100002i/dmodel)begin{aligned} text{PE}(text{pos}, 2i) &= sinleft(frac{text{pos}}{10000^{2i/d_{text{model}}}}right) \ text{PE}(text{pos}, 2i+1) &= cosleft(frac{text{pos}}{10000^{2i/d_{text{model}}}}right) end{aligned}

参数解释:

  • postext{pos}:Token的位置索引(0, 1, 2, 3, ...)
  • ii:位置编码向量的维度索引(0,1,2,,dmodel/210, 1, 2, ldots, d_{text{model}}/2 - 1
  • 2i2i:偶数维度使用sin函数
  • 2i+12i+1:奇数维度使用cos函数
  • 1000010000:基数,控制频率的衰减速度
  • dmodeld_{text{model}}:位置编码向量的维度(与Token Embedding维度相同)

直观理解

这个公式的核心思想是:使用不同频率的正弦波来编码位置

想象一下时钟:

  • 秒针:转得很快,频率高(对应高维度,2i2i很大)
  • 分针:转得中等,频率中等
  • 时针:转得很慢,频率低(对应低维度,2i2i很小)

不同的时刻,秒针、分针、时针的组合是唯一的,这就能唯一标识一个时间点。

类似地:

  • 低维度(2i2i小):使用低频正弦波,变化慢,能区分远距离的位置
  • 高维度(2i2i大):使用高频正弦波,变化快,能区分近距离的位置

具体例子

假设 dmodel=4d_{text{model}} = 4(简化),我们计算前3个位置的位置编码:

位置 pos=0:

PE(0,0)=sin(0/100000/4)=sin(0)=0PE(0,1)=cos(0/100000/4)=cos(0)=1PE(0,2)=sin(0/100002/4)=sin(0)=0PE(0,3)=cos(0/100002/4)=cos(0)=1PE[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}

位置 pos=1:

PE(1,0)=sin(1/100000/4)=sin(1)0.841PE(1,1)=cos(1/100000/4)=cos(1)0.540PE(1,2)=sin(1/100002/4)=sin(0.01)0.01PE(1,3)=cos(1/100002/4)=cos(0.01)1.0PE[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}

位置 pos=2:

PE[2]=[0.909,0.416,0.02,0.9998]text{PE}[2] = [0.909, -0.416, 0.02, 0.9998]

可以看到,每个位置都有一个唯一的向量表示。

Sinusoidal编码的优势

  1. 确定性:不需要学习,直接用公式计算
  2. 外推性:即使训练时只见过长度100的序列,也能为长度200的序列生成位置编码
  3. 相对位置:通过三角函数的性质,模型可以学习相对位置关系
    • 数学性质:PE(pos+k)text{PE}(text{pos}+k) 可以表示为 PE(pos)text{PE}(text{pos}) 的线性变换

Sinusoidal编码的劣势

  1. 外推性有限:虽然理论上可以外推,但实际效果在超长序列上会下降
  2. 位置信息弱:只是简单的加法,位置信息容易被Embedding淹没
  3. 无法区分绝对位置重要性:远距离和近距离的位置用相同的编码方式

可学习的绝对位置编码(Learned Positional Encoding)

另一种简单的方案是:把位置编码当作模型参数,在训练中学习

实现方式

创建一个可学习的Embedding矩阵:

PElearnedRmax_seq_len×dmodeltext{PE}_{text{learned}} in mathbb{R}^{text{max_seq_len} times d_{text{model}}}

对于位置 postext{pos}

PE[pos]=PElearned[pos](直接查表)text{PE}[text{pos}] = text{PE}_{text{learned}}[text{pos}] quad text{(直接查表)}

参数解释:

  • max_seq_lentext{max_seq_len}:支持的最大序列长度(比如512、2048等)
  • PElearnedtext{PE}_{text{learned}} 是一个可训练的参数矩阵
  • 训练时,这个矩阵会通过反向传播更新

优势与劣势

优势:

  • 灵活性高,模型可以自己学习最优的位置表示
  • 实现简单

劣势:

  • 无法外推:如果训练时最大长度是512,那么无法处理长度超过512的序列
  • 参数量增加:需要额外存储 max_seq_len×dmodeltext{max_seq_len} times d_{text{model}} 个参数

这种方案在BERT、GPT等早期模型中使用,但现代大模型更倾向于使用RoPE等相对位置编码。

RoPE:旋转位置编码(Rotary Position Embedding)

RoPE(Su et al., 2021)是目前最流行的位置编码方案之一,被LLaMA、GPT-NeoX、PaLM等主流大模型采用。

核心思想:直接作用于注意力计算

RoPE与传统位置编码的最大区别在于:它不是在输入阶段添加位置信息,而是直接作用在注意力机制的计算过程中

传统方法(Sinusoidal、Learned PE)

步骤1:在输入阶段加入位置信息Xwith_pos=X+PE步骤2:计算Q、K、VQ=Xwith_posWQK=Xwith_posWKV=Xwith_posWV步骤3:计算注意力分数Score=QKTbegin{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}

RoPE方法

步骤1:先计算Q、K(不含位置信息)Q=XWQK=XWKV=XWV步骤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]Tbegin{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}

关键区别

  1. 传统方法:位置信息通过加法混入Embedding,然后一起参与Q、K、V的线性变换
  2. RoPE:位置信息通过旋转变换直接作用在已计算好的Q、K上,在注意力分数计算时才引入位置信息

为什么这样更好?

  • 传统加法:位置信息和内容信息在Embedding层混合,不同的线性变换会破坏位置关系
  • RoPE旋转:位置信息通过几何旋转方式注入,保持了相对位置的数学性质,使得 Score(m,n)text{Score}(m,n) 自然地只依赖相对位置 (mn)(m-n)

为什么叫"旋转"?

在二维平面上,旋转一个向量 θtheta 角度,可以用旋转矩阵表示:

[xy]=[cosθsinθsinθcosθ][xy]begin{bmatrix} x' \ y' end{bmatrix} = begin{bmatrix} costheta & -sintheta \ sintheta & costheta end{bmatrix} begin{bmatrix} x \ y end{bmatrix}

RoPE就是将这个思想推广到高维空间:每对维度作为一个平面,进行不同角度的旋转

RoPE的数学公式

对于位置 mm 的Query向量和位置 nn 的Key向量,RoPE将它们分别旋转:

qm=RΘ(m)WQxmkn=RΘ(n)WKxnbegin{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}

其中,旋转矩阵 RΘ(pos)R_Theta(text{pos}) 是一个分块对角矩阵:

RΘ(pos)=[cos(posθ0)sin(posθ0)00sin(posθ0)cos(posθ0)0000cos(posθ1)sin(posθ1)00sin(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}

参数解释:

  • postext{pos}:Token的位置(0, 1, 2, ...)
  • θitheta_i:第i对维度的旋转频率,计算方式:θi=100002i/dmodeltheta_i = 10000^{-2i/d_{text{model}}}
  • 每两个维度一组,共 dmodel/2d_{text{model}}/2
  • 每组使用不同的旋转频率 θitheta_i

简化表示(逐元素形式)

为了更直观,我们可以用逐元素的方式表示RoPE:

对于Query向量的第 2i2i2i+12i+1 维(一对维度):

qm[2i]=q[2i]cos(mθi)q[2i+1]sin(mθi)qm[2i+1]=q[2i]sin(mθi)+q[2i+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}

对于Key向量同理:

kn[2i]=k[2i]cos(nθi)k[2i+1]sin(nθi)kn[2i+1]=k[2i]sin(nθi)+k[2i+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}

其中:

θi=100002i/dmodeltheta_i = 10000^{-2i/d_{text{model}}}

RoPE融合进注意力计算的完整流程

让我们详细看看RoPE是如何一步步融合进注意力机制的计算过程的。

假设我们有一个序列:["我", "喜欢", "猫"],共3个Token,位置分别为0、1、2。

步骤1:获取Token Embedding(不含位置信息)

X=[x0x1x2]R3×dmodelbegin{aligned} X &= begin{bmatrix} x_0 \ x_1 \ x_2 end{bmatrix} in mathbb{R}^{3 times d_{text{model}}} end{aligned}

其中 x0x_0x1x_1x2x_2 分别是"我"、"喜欢"、"猫"的Embedding向量。

步骤2:计算原始的Q、K、V(仍不含位置信息)

Q=XWQ=[q0q1q2]R3×dkK=XWK=[k0k1k2]R3×dkV=XWV=[v0v1v2]R3×dvbegin{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}

注意:到这一步为止,Q、K、V都还没有任何位置信息!

步骤3:对Q、K应用RoPE旋转(注入位置信息)

这是RoPE的核心步骤!对每个位置的Q和K向量应用旋转:

位置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)k2begin{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{(旋转}thetatext{角度)} \ &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{(旋转}2thetatext{角度)} \ &quad K_{text{rot}}[2] = R_Theta(2) cdot k_2 end{aligned}

V向量不旋转,因为V包含的是"内容信息",只有Q和K需要位置信息来计算相关性。

步骤4:计算注意力分数矩阵(位置信息已融合)

现在计算所有位置对之间的注意力分数:

Scores=QrotKrotT=[Qrot[0]Krot[0]TQrot[0]Krot[1]TQrot[0]Krot[2]TQrot[1]Krot[0]TQrot[1]Krot[1]TQrot[1]Krot[2]TQrot[2]Krot[0]TQrot[2]Krot[1]TQrot[2]Krot[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}

关键:每个分数 Qrot[m]Krot[n]TQ_{text{rot}}[m] cdot K_{text{rot}}[n]^T 自动包含了位置m和位置n之间的相对位置信息 (mn)(m-n)

步骤5:应用Softmax和加权求和(标准流程)

Attention_Weights=softmax(Scoresdk)Output=Attention_WeightsVbegin{aligned} text{Attention_Weights} &= text{softmax}left(frac{text{Scores}}{sqrt{d_k}}right) \ text{Output} &= text{Attention_Weights} cdot V end{aligned}

对比总结:RoPE vs 传统方法

步骤传统位置编码RoPE
1. 输入X+PEX + text{PE}XX(纯内容)
2. 计算QKVQ=(X+PE)WQQ = (X + text{PE}) cdot W_QQ=XWQQ = X cdot W_Q
3. 位置注入(已在步骤1完成) Qrot=RΘ(pos)QQ_{text{rot}} = R_Theta(text{pos}) cdot Q
4. 注意力分数QKTQ cdot K^T(位置信息已稀释)QrotKrotTQ_{text{rot}} cdot K_{text{rot}}^T(位置信息精确)
结果位置信息间接、可能被削弱位置信息直接、保留相对关系

核心优势:RoPE在注意力分数计算的关键时刻才引入位置信息,通过旋转的几何性质,保证了注意力分数只依赖相对位置差,而不是绝对位置。

RoPE的关键性质

性质1:注意力分数自动包含相对位置信息

当计算位置m和位置n之间的注意力分数时:

Attention_Score(m,n)=qmTkn=(展开后,对于第i对维度)=[q[2i]k[2i]+q[2i+1]k[2i+1]]×cos((mn)θ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}

核心发现:注意力分数只依赖于 (mn)(m-n),即相对位置差,而不是绝对位置m或n!

这意味着:

  • 位置0的Token看位置1的Token,与位置5的Token看位置6的Token,注意力模式相同(相对位置都是+1)
  • 模型自然地学习到相对位置关系

性质2:远距离衰减

由于使用了不同频率的旋转:

  • 低频分量(θitheta_i 小):捕捉长距离依赖
  • 高频分量(θitheta_i 大):捕捉短距离依赖

相对位置距离越远,高频分量的点积越接近0,注意力自然衰减。

具体例子

假设 dmodel=4d_{text{model}} = 4,我们计算位置0和位置1的Query向量:

步骤1:计算旋转频率

θ0=100000/4=1θ1=100002/4=0.01begin{aligned} theta_0 &= 10000^{-0/4} = 1 \ theta_1 &= 10000^{-2/4} = 0.01 end{aligned}

步骤2:对位置m=0,旋转角度为0

q0[0]=q[0]cos(01)q[1]sin(01)=q[0]q0[1]=q[0]sin(01)+q[1]cos(01)=q[1]q0[2]=q[2]cos(00.01)q[3]sin(00.01)=q[2]q0[3]=q[2]sin(00.01)+q[3]cos(00.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}

位置0不旋转,保持原样。

步骤3:对位置m=1,旋转角度为θ

q1[0]=q[0]cos(11)q[1]sin(11)0.540q[0]0.841q[1]q1[1]=q[0]sin(11)+q[1]cos(11)0.841q[0]+0.540q[1]q1[2]=q[2]cos(10.01)q[3]sin(10.01)0.99995q[2]0.01q[3]q1[3]=q[2]sin(10.01)+q[3]cos(10.01)0.01q[2]+0.99995q[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}

可以看到:

  • 第0-1维(高频):旋转了约54度,变化明显
  • 第2-3维(低频):只旋转了约0.57度,几乎不变

RoPE的优势

  1. 相对位置编码:注意力分数自动包含相对位置信息,不依赖绝对位置
  2. 外推性好:理论上可以处理任意长度的序列
  3. 长距离衰减:远距离Token的注意力自然衰减,符合语言学规律
  4. 无额外参数:不增加模型参数量
  5. 高效实现:可以预计算旋转矩阵,推理时直接查表

RoPE的实现细节

在实际代码中,RoPE通常这样实现。下面展示完整的带RoPE的注意力计算流程

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)

代码关键点解释

  1. 预计算阶段precompute_rope_cache):

    • 只在模型初始化时执行一次
    • 计算所有可能位置的 cos(posθi)cos(text{pos} cdot theta_i)sin(posθi)sin(text{pos} cdot theta_i)
    • 存储在缓存中,推理时直接查表
  2. RoPE应用阶段apply_rope):

    • 计算完Q、K之后立即应用
    • 将向量的每对维度 (2i,2i+1)(2i, 2i+1) 作为一个平面进行旋转
    • V向量不旋转,因为V存储的是"内容",不需要位置信息
  3. 融合进注意力计算attention_with_rope):

    • 步骤1:Q=XWQQ = X cdot W_Q(不含位置)
    • 步骤2:Qrot=RΘ(pos)QQ_{text{rot}} = R_Theta(text{pos}) cdot QRoPE在这里注入位置
    • 步骤3:Scores=QrotKrotTtext{Scores} = Q_{text{rot}} cdot K_{text{rot}}^T(位置信息自动体现在分数中)
    • 步骤4:标准的softmax和加权求和

与传统方法的对比

时间点传统位置编码RoPE
输入阶段X = X + PE(位置信息混入)X(纯内容)
计算QKVQ = X · W_Q(位置已混入)Q = X · W_Q(纯内容)
位置注入(已完成) Q_rot = apply_rope(Q)(在这里!)
计算分数Q · K^TQ_rot · K_rot^T

RoPE的优势在于:位置信息在注意力分数计算的关键时刻才引入,通过旋转的几何性质精确地编码了相对位置关系。

RoPE的长度扩展技术

虽然RoPE理论上可以外推,但在实际应用中,当序列长度远超训练时的长度时,性能会下降。为此,研究者提出了多种长度扩展技术。

问题:为什么需要长度扩展?

假设模型在训练时只见过长度≤2048的序列,当推理时输入长度4096的序列:

  • 位置编码会遇到"未见过"的旋转角度
  • 高频分量的旋转角度过大,导致注意力模式混乱
  • 模型性能显著下降

方法1:位置插值(Position Interpolation, PI)

核心思想:将长序列的位置"压缩"到训练时的范围内

原始位置:pos[0,Lnew]压缩后:pos=posLtrainLnewbegin{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}

参数解释:

  • LtrainL_{text{train}}:训练时的最大序列长度(如2048)
  • LnewL_{text{new}}:推理时的目标序列长度(如8192)
  • postext{pos}':压缩后的位置,范围在 [0,Ltrain][0, L_{text{train}}]

举例

  • 训练长度2048,推理长度8192
  • 推理时位置4096 → 压缩为 4096×(2048/8192)=10244096 times (2048/8192) = 1024
  • 推理时位置8192 → 压缩为 8192×(2048/8192)=20488192 times (2048/8192) = 2048

优势

  • 简单有效,只需修改位置索引
  • 所有位置都在训练范围内,模型"见过"

劣势

  • 改变了相对位置的含义(相邻Token的距离变小了)
  • 需要少量微调来适应

方法2:NTK-Aware插值

核心思想:不是简单压缩位置,而是调整旋转频率的基数(将10000改为更大的值)

原始频率:θi=100002i/dmodelNTK频率:θi=(10000scale)2i/dmodelbegin{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}

其中:

scale=LnewLtraintext{scale} = frac{L_{text{new}}}{L_{text{train}}}

参数解释:

  • scaletext{scale}:长度扩展倍数
  • 基数从10000增大到 10000×scale10000 times text{scale}
  • 旋转频率整体降低,适应更长的序列

举例

  • 训练长度2048,推理长度8192,scale=4
  • 基数从10000变为40000
  • 旋转速度降低4倍,适配4倍长的序列

优势

  • 保持了相对位置的语义
  • 不需要微调,零样本外推效果好

劣势

  • 理论分析较复杂
  • 对不同频率分量的影响不均匀

方法3:YaRN(Yet another RoPE extensioN)

核心思想:对不同频率分量采用不同的插值策略

  • 低频分量θtheta小):捕捉长距离依赖,使用NTK插值
  • 高频分量θtheta大):捕捉短距离依赖,保持不变或轻微插值
  • 中频分量:渐进式插值

这种方法在LLaMA-2等模型中取得了很好的效果,可以将上下文长度扩展到32k甚至更长。

长度扩展对比

方法是否需要微调外推效果计算开销
位置插值(PI)需要少量微调无额外开销
NTK-Aware零样本较好无额外开销
YaRN零样本或少量微调很好无额外开销

小结

  1. 位置编码的必要性:注意力机制天生无法感知位置,必须显式注入位置信息

  2. 传统位置编码

    • Sinusoidal编码:使用sin/cos函数,确定性、可外推,但效果一般
    • 可学习编码:灵活但无法外推
  3. RoPE(旋转位置编码)

    • 通过旋转矩阵将位置信息融入Q、K
    • 注意力分数自动包含相对位置信息
    • 外推性好、无额外参数、长距离自然衰减
    • 成为现代大模型的主流选择
  4. 长度扩展技术

    • 通过位置插值、频率调整等方法,让模型适应超长序列
    • 核心是平衡"训练时的位置模式"和"推理时的长度需求"

位置编码看似简单,但对大模型的性能至关重要。RoPE的成功说明,好的位置编码应该捕捉相对位置关系,而不是绝对位置,这样才能具备良好的泛化能力。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com