LLMs组件系列:Attention排列不变性与位置编码(超详细)

💡 Transformer注意力可并行计算的特性既是其相较于RNN的优势,也使其丢失了时间步语义信息,所以位置编码 (positional encodings) 是Transformer这种并行结构必须要有的,否则机器就会已读乱回。但位置编码原理是什么,工程上怎么实现呢?网上有许多讲解位置编码的方案,但很多都为了文章篇幅而舍弃掉一些前置信息,如果没有基础看起来还是比较吃力的。为此,本文用篇幅换取详细度,详细介绍了当前四种主流的位置编码方案。具体行文上,先在第一部分介绍了Attention机制的排列不变性,也即为什么需要位置编码。之后将当前位置编码方案分为绝对位置编码和相对位置编码分别详述,并附工程代码。

1 Attention排列不变性与语义序列的冲突

1.1 排列不变性

在这里插入图片描述

上图是一个非First Iteration的Attention点积机制。其中query_states是input_embed在经过q_proj投影矩阵后获得的Query隐状态向量(只有一个当前token,position_ids为3),key_states和value_states则是当前token在k_proj和v_proj投影后与kv_cache中之前token隐状态cat的结果(position_ids为0-3)。attn_weights则是QK注意力点积获得的注意力评分,在First Iteration中是一个方阵(对角线下方有效),在这里则直接取方阵最后一行,图中:

  • 上部分为QK点积,结果为attn_weights评分向量(两个position_ids组合为编号,例如query_states编号为3,与key_states中第一列编号为0的向量点积后获得attn_weights向量中第一个编号为30的值);
  • 下部分为注意力权重与值向量的点积,同理,attn_outputs向量中第一个元素A为attn_weights向量与value_states矩阵第一列点积的结果,表示上写为attn_outputs每个编号和value_states第一列中每个编号(0a…3a)拼接相加的结果。

可见Attention机制的输出总体上可以看作是对query_states进行线性变换的结果(从长度为6降维到4),可视作对value_states进行带权垂直压缩的结果。具体来说,attn_outputs每个维度(ABCD)都是注意力评分矩阵作为权重(30, 31, 32, 33)与value矩阵对应行(0,1,2,3)进行加权求和的结果,这个对应关系是固定的,就算将token序列打乱,在下部分注意力权重与值向量的点积的过程中,还是各自token的权重与各自token的value向量相乘后再所有token的value值向量相加的结果,值不变。如下图所示,将token序列0,1,2,3逆转为3,2,1,0再执行一样的过程,attn_outputs结果的值不会改变。

在这里插入图片描述

上图中的结果说明,在Attention结构中,就算token的序列打乱,query_states的变换结果attn_outputs也不会变,这种情况在First Iteration中一致,只不过当query_states有多行时,attn_outputs也在对应的行上与其一致。这就是Attention的排列不变性,本质是token进行全组合相乘然后求和,而全组合本质是一个无序的集合

1.2 语义的序列要求与位置编码概念

上述结果说明了Attention的本质是对token进行组合求积并求和,与序列无关。这很好啊,既然是组合的集合,那么我就可以同时对所有组合进行计算了,这也是Attention可并行计算的根本原因。之前的LSTM需要按照时间步将每个token串行放入进行计算,前一个token的计算结果既可以当作当前时间步的输出,也得传递给下个时间步作为输入。相比之下简直就是串行到并行的飞跃!

但人家LSTM这样串行当然是有其道理的,那就是语义的时序信息。在Attention中,既然token顺序和结果无关,那岂不是说我是狗的主人狗是我的主人主人是我的狗我xxxxx这几句话等价了?简直是倒反天罡!快归快,不能乱来对不对。那怎么办呢?有没有哪种方法既保留Transformer快速并行矩阵计算的特点,又能捕获词序信息?

那就是位置编码Positional Encoding,Transformer原论文中对这一点有比较清楚的叙述:

Since our model contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the tokens in the sequence. To this end, we add “positional encodings” to the input embeddings at the bottoms of the encoder and decoder stacks.

即,位置编码,或者说位置性的编码,就是对输入序列中的token表示进行位置信息(绝对或相对)的注入。位置编码经历了范式上的编码,逐渐发展成如今适用于Transformer的编码,具体可参见Transformer学习笔记一:Positional Encoding(位置编码) - 猛猿的文章 - 知乎。简单来说,位置编码经过以下几个阶段

  • 用整型值标记位置:较为通用且直观,但无界且量纲上与浮点隐藏状态不匹配,不适用于Attention。
  • 用[0,1]范围浮点数表示:机械切分,无法外推
  • 二进制编码:用n位二进制的隐藏状态就可以表示 2 n 2^n 2n个词,但维度上与Embedding不匹配会有空值,且离散
  • 用周期函数如三角函数sin:范围固定[-1,1]浮点数,连续,可外推,三角函数和差化积、积化和差的特性可以编码相对位置信息。如下,但这种表示是非线性的,有待改进。

s i n ( x + Δ x ) = sin ⁡ ( x ) cos ⁡ ( Δ x ) + cos ⁡ ( x ) sin ⁡ ( Δ x ) = [ sin ⁡ x cos ⁡ x ] [ cos ⁡ Δ x sin ⁡ Δ x ] sin(x+\Delta x)=\sin(x)\cos(\Delta x)+\cos(x)\sin(\Delta x)=[\begin{matrix} \sin x & \cos x \end{matrix}] [\begin{matrix} \cos \Delta x \\ \sin \Delta x \end{matrix}] sin(x+Δx)=sin(x)cos(Δx)+cos(x)sin(Δx)=[sinxcosx][cosΔxsinΔx]

  • 三角函数坐标系(一对sin和cos):具备以上所有优点。为了表示线性变换,可以引入三角函数坐标系,将输入的x理解为角度 θ \theta θ。那么直角坐标系中的一个位置就可以写作一对正余弦三角函数 [ sin ⁡ x cos ⁡ x ] [\begin{matrix} \sin x \\ \cos x \end{matrix}] [sinxcosx]。此时其经过一个线性变换可以表示任意相对位置的旋转(即 x + Δ x x+\Delta x x+Δx视作 x x x的旋转), [ sin ⁡ x + Δ x cos ⁡ x + Δ x ] [ \begin{matrix} \sin x +\Delta x\\ \cos x+\Delta x \end{matrix}] [sinx+Δxcosx+Δx],如下:
    [ sin ⁡ ( x + Δ x ) cos ⁡ ( x + Δ x ) ] = [ cos ⁡ Δ x sin ⁡ Δ x − sin ⁡ Δ x cos ⁡ Δ x ] [ sin ⁡ x cos ⁡ x ] [ \begin{matrix} \sin (x +\Delta x)\\ \cos (x+\Delta x) \end{matrix}] = [\begin{matrix} \cos \Delta x & \sin \Delta x\\ -\sin \Delta x & \cos \Delta x \end{matrix}] [ \begin{matrix} \sin x \\ \cos x \end{matrix}] [sin(x+Δx)cos(x+Δx)]=[cosΔxsinΔxsinΔxcosΔx][sinxcosx]

Transformer原文紧接着还有两句:

The positional encodings have the same dimension dmodel as the embeddings, so that the two can be summed. There are many choices of positional encodings, learned and fixed [8].

也就是说,位置编码一般和Embedding的shape一样,注入的方式就是加到Embedding上。此外,在当时已经有了一些位置编码方案,分为习得的learned的和固定的Fixed。当然了这种分类方式在当时条件下是成立的,但从我们现在的视角来看,位置编码首先应该按照是否是绝对的位置编码进行分类:

  • 绝对位置编码:一个位置一个定死的编码,即只要position ids一样,那么token的embedding的所有分量上加的编码信息值就一样;实现上是直接加到embedding上。
  • 相对位置编码:根据token的相对位置进行编码,实现上是分别成到query和key向量上,并通过QK点积实现相对性。

2 绝对位置编码

在Embedding层加入位置编码信息。

2.1 Learned Positional Embedding(学习式编码):习得Learned

在出现时间上早于Transformer的Sinusoidal编码,也在其原始论文中被讨论过,之后也比Sinusoidal应用更广。其基本原理是,将位置变量作为一个 s h a p e = [ 最大可编码长度 , e m b e d d i n g 维度 ] shape = [最大可编码长度, embedding维度] shape=[最大可编码长度,embedding维度]的可学习的参数矩阵,与模型一起训练。早期的BERT, Roberta, GPT, GPT2使用的就是这种,最早使用则可溯源至2017年Facebook的一篇论文Convolutional sequence to sequence learning。Transformer原始论文中也提到了这个方法并用于对比实验,实验结果是Transformer的位置编码方案(Sinusoidal)与这种可学习的方案的结果差不多,但这种方案无法外推extrapolate至超过最大可编码长度的序列(目前也有研究表明可以通过其他方式让学习式编码具备外推的能力)。

2.2 Sinusoidal(正弦曲线编码):固定Fixed

Transformer论文中给出的基于三角函数余计算、固定的位置编码。即不是通过学习习得一个 s h a p e = [ 最大可编码长度 , e m b e d d i n g 维度 ] shape = [最大可编码长度, embedding维度] shape=[最大可编码长度,embedding维度]的参数矩阵,而是通过一个函数计算任意长度的序列的位置编码与embedding后的结果相加,也就是具备可外推性质。但实际上这个方法并没有很优越,只有Transformer, Transformer XL在使用。

在这里插入图片描述

编码函数 P E PE PE如下。 P E PE PE函数接收两个参数, p o s pos postoken在序列中的位置编号 2 i ( + 1 ) 2i(+1) 2i(+1)token嵌入向量每个元素的编号,最大为嵌入维度 d m o d e l − 1 d_{model}-1 dmodel1。这个函数对输入序列中每个token、每个token的嵌入空间中每个嵌入值都赋予一个特定、通用、绝对的位置编码。

P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i d m o d e l ) P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i d m o d e l ) PE(pos, 2i)=sin(pos/10000^{\frac{2i}{d_{model}}})\\ PE(pos, 2i+1)=cos(pos/10000^{\frac{2i}{d_{model}}}) PE(pos,2i)=sin(pos/10000dmodel2i)PE(pos,2i+1)=cos(pos/10000dmodel2i)

式子有点抽象,看一下原文的解释:

where pos is the position and i is the dimension. That is, each dimension of the positional encoding corresponds to a sinusoid. The wavelengths form a geometric progression from 2π to 10000 · 2π. We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset k, P E p o s + k PE_{pos+k} PEpos+k can be represented as a linear function of P E p o s PE_{pos} PEpos.

重点有二:

  • 对于Dimension,在横向的Embedding分量维数值等差增长时(例如对一个128维的Embedding维度,从第0维到127维),关于pos的三角函数波长呈现一个从2 π \pi π到 10000 · 2 π \pi π的几何级数;
  • 对于Position,其能允许模型简单地基于相对位置施加关注,即 P E p o s + k PE_{pos+k} PEpos+k可以由 P E p o s PE_{pos} PEpos线性表示。

(1)首先,让我们来看波长呈现几何级数是什么意思

如上图,对一个token长度为5、嵌入维度为128( d m o d e l d_{model} dmodel)的输入序列进行位置编码,形式上要生成一个shape=[5, 128]的位置编码矩阵用以与Embedding矩阵相加。128个维数分量的正弦函数的输入 p o s / 1000 0 2 i d m o d e l pos/10000^{\frac{2i}{d_{model}}} pos/10000dmodel2i其实可以转化为标准形式观察波长函数:

2 π ⋅ p o s 2 π ⋅ 1000 0 2 i d m o d e l = 2 π x λ \frac{2\pi ·pos}{ 2\pi ·10000^{\frac{2i}{d_{model}}}} = \frac{2\pi x}{\lambda} 2π10000dmodel2i2πpos=λ2πx

其中 λ = 2 π ⋅ 1000 0 2 i d m o d e l \lambda=2\pi ·10000^{\frac{2i}{d_{model}}} λ=2π10000dmodel2i为波长, i i i取值从0到 d m o d e l 2 \frac{d_{model}}{2} 2dmodel,128个维度相邻两个为一组(共享一个波长),那么总共有64组(上图中彩色部分及其下方对应的波长)。那么这个位置编码正弦函数的波长是关于dimension的一个等比数列,即呈几何级数(The wavelengths form a geometric progression from 2π to 10000 · 2π)。当 d m o d e l = 128 d_{model}=128 dmodel=128时,波长数列公比为 1000 0 1 64 ≈ 1.15 10000^{\frac{1}{64}}\approx1.15 100006411.15,如下图。下图画出了embedding维度为128的Sinusoidal位置编码在前7个dimension的值情况,红色为偶数维度,蓝色为奇数维度。可以观察到奇偶数的波长一样但存在一个位移,奇数和偶数维度的波长则递增,这就使得不同维度的编码频率是有规律且唯一的。

在这里插入图片描述

此外,上图展现的一个重要规律是,在Dimension层面,维数分量从0-127不断变大时,每个分量上对于所有Position Ids处理的三角函数的波长越大,这会导致数值变化非常小,如下。颜色表示分量上的数值([-1,1]范围),越小越蓝,越大越红,横轴为128个维度,竖轴为50个token。可以看到从左往右(0到127维分量)颜色越单一,即数值上变化越小。
在这里插入图片描述

(2)其次基于相对位置施加关注是什么意思?

让我们先在形式上稍稍改变 P E PE PE函数,将其参数列表中的维度分量变量i去掉并合为一个式子,即 P E ( p o s ) PE(pos) PE(pos)。其可以自动对pos位置的token的所有embedding分量进行两两一组的分块矩阵的编码,此时将原式中 p o s / 1000 0 2 i d m o d e l pos/10000^{\frac{2i}{d_{model}}} pos/10000dmodel2i简化为 p o s ⋅ λ i pos·\lambda_i posλi,其中 i i i和原式中保持一致两两共享,如下。

P E ( p o s ) = ( [ s i n ( p o s ⋅ λ 0 ) , c o s ( p o s ⋅ λ 0 ) ] , [ s i n ( p o s ⋅ λ 1 ) , c o s ( p o s ⋅ λ 1 ) ] , . . . . . . , [ s i n ( p o s ⋅ λ d m o d e l / 2 − 1 ) , c o s ( p o s ⋅ λ d m o d e l / 2 − 1 ) ] ) PE(pos)= \begin{pmatrix} \begin{bmatrix} sin(pos·\lambda_0) ,& cos(pos·\lambda_0) \end{bmatrix} ,& \begin{bmatrix} sin(pos·\lambda_1) ,& cos(pos·\lambda_1) \end{bmatrix} ,& ......,& \begin{bmatrix} sin(pos·\lambda_{d_{model}/2-1}) ,& cos(pos·\lambda_{d_{model}/2-1}) \end{bmatrix} \end{pmatrix} PE(pos)=([sin(posλ0),cos(posλ0)],[sin(posλ1),cos(posλ1)],......,[sin(posλdmodel/21),cos(posλdmodel/21)])

此时

P E ( p o s + Δ k ) = ( [ s i n ( ( p o s + Δ k ) ⋅ λ 0 ) , c o s ( ( p o s + Δ k ) ⋅ λ 0 ) ] , [ s i n ( ( p o s + Δ k ) ⋅ λ 1 ) , c o s ( ( p o s + Δ k ) ⋅ λ 1 ) ] , . . . . . . , [ s i n ( ( p o s + Δ k ) ⋅ λ d m o d e l / 2 − 1 ) , c o s ( ( p o s + Δ k ) ⋅ λ d m o d e l / 2 − 1 ) ] ) PE(pos+\Delta k)= \begin{pmatrix} \begin{bmatrix} sin( (pos+\Delta k)·\lambda_0) ,& cos((pos+\Delta k)·\lambda_0) \end{bmatrix}, & \begin{bmatrix} sin((pos+\Delta k)·\lambda_1) ,& cos((pos+\Delta k)·\lambda_1) \end{bmatrix}, & ......,& \begin{bmatrix} sin((pos+\Delta k)·\lambda_{d_{model}/2-1}) ,& cos((pos+\Delta k)·\lambda_{d_{model}/2-1}) \end{bmatrix} \end{pmatrix} PE(pos+Δk)=([sin((pos+Δk)λ0),cos((pos+Δk)λ0)],[sin((pos+Δk)λ1),cos((pos+Δk)λ1)],......,[sin((pos+Δk)λdmodel/21),cos((pos+Δk)λdmodel/21)])

正如前一部分提到的基于三角函数坐标系组合使用sin和cos的位置编码可以实现用线性变换表示相对位置,如下式:

[ sin ⁡ ( x + Δ x ) cos ⁡ ( x + Δ x ) ] = [ cos ⁡ Δ x sin ⁡ Δ x − sin ⁡ Δ x cos ⁡ Δ x ] [ sin ⁡ x cos ⁡ x ] [ \begin{matrix} \sin (x +\Delta x)\\ \cos (x+\Delta x ) \end{matrix}] = [\begin{matrix} \cos \Delta x & \sin \Delta x\\ -\sin \Delta x & \cos \Delta x \end{matrix}] [ \begin{matrix} \sin x \\ \cos x \end{matrix}] [sin(x+Δx)cos(x+Δx)]=[cosΔxsinΔxsinΔxcosΔx][sinxcosx]

现在非常清晰了,可以将下式的线性变换规则套用到上面两个式子。套用结果如下(分块矩阵的转置是内外都转置):

P E ( p o s + Δ k ) T = T Δ k ∗ P E ( p o s ) T = ( [ cos ⁡ ( Δ k ⋅ λ 0 ) sin ⁡ ( Δ k ⋅ λ 0 ) − sin ⁡ ( Δ k ⋅ λ 0 ) cos ⁡ ( Δ k ⋅ λ 0 ) ] . . . . 0 . . . . . . . . . 0 . . . [ cos ⁡ ( Δ k ⋅ λ d m o d e l / 2 − 1 ) sin ⁡ ( Δ k ⋅ λ d m o d e l / 2 − 1 ) − sin ⁡ ( Δ k ⋅ λ d m o d e l / 2 − 1 ) cos ⁡ ( Δ k ⋅ λ d m o d e l / 2 − 1 ) ] ) ( [ s i n ( p o s ⋅ λ 0 ) c o s ( p o s ⋅ λ 0 ) ] . . . . . . [ s i n ( p o s ⋅ λ d m o d e l / 2 − 1 ) c o s ( p o s ⋅ λ d m o d e l / 2 − 1 ) ] ) PE(pos+\Delta k)^T=T_{\Delta k}*PE(pos)^T = \\\begin{pmatrix} \begin{bmatrix} \cos(\Delta k·\lambda_0) & \sin(\Delta k·\lambda_0)\\ -\sin(\Delta k·\lambda_0) & \cos(\Delta k·\lambda_0) \end{bmatrix} & .... & 0 \\ ... & ... & ... \\ 0 & ... & \begin{bmatrix} \cos(\Delta k·\lambda_{d_{model}/2-1}) & \sin(\Delta k·\lambda_{d_{model}/2-1}) \\ -\sin(\Delta k·\lambda_{d_{model}/2-1}) & \cos(\Delta k·\lambda_{d_{model}/2-1}) \end{bmatrix} \end{pmatrix} \begin{pmatrix} \begin{bmatrix} sin(pos·\lambda_0) \\ cos(pos·\lambda_0) \end{bmatrix} \\ ......\\ \begin{bmatrix} sin(pos·\lambda_{d_{model}/2-1}) \\ cos(pos·\lambda_{d_{model}/2-1}) \end{bmatrix} \end{pmatrix} PE(pos+Δk)T=TΔkPE(pos)T= [cos(Δkλ0)sin(Δkλ0)sin(Δkλ0)cos(Δkλ0)]...0..........0...[cos(Δkλdmodel/21)sin(Δkλdmodel/21)sin(Δkλdmodel/21)cos(Δkλdmodel/21)] [sin(posλ0)cos(posλ0)]......[sin(posλdmodel/21)cos(posλdmodel/21)]

除此之外,Sinusoidal还有一个相对位置编码的特点,即任何两个Pos位置的编码结果向量的点积结果仅仅取决于两个pos的差值 Δ k \Delta k Δk,用到的还是三角函数和差化积、积化和差的定理,如下:

P E ( p o s + Δ k ) ∗ P E ( p o s ) T = ∑ i = 0 d m o d e l 2 − 1 s i n ( ( p o s + Δ k ) ⋅ λ i ) s i n ( p o s ⋅ λ i ) + c o s ( ( p o s + Δ k ) ⋅ λ i ) c o s ( p o s ⋅ λ i ) = ∑ i = 0 d m o d e l 2 − 1 cos ⁡ ( ( p o s + Δ k − p o s ) ⋅ λ i ) = ∑ i = 0 d m o d e l 2 − 1 cos ⁡ ( Δ k ⋅ λ i ) PE(pos+\Delta k)*PE(pos)^T = \sum_{i=0}^{\frac{d_{model}}{2}-1}sin((pos +\Delta k)·\lambda_i)sin(pos·\lambda_i)+ cos((pos +\Delta k)·\lambda_i)cos(pos·\lambda_i) =\\ \sum_{i=0}^{\frac{d_{model}}{2}-1} \cos((pos +\Delta k -pos)·\lambda_i) = \sum_{i=0}^{\frac{d_{model}}{2}-1} \cos(\Delta k·\lambda_i) PE(pos+Δk)PE(pos)T=i=02dmodel1sin((pos+Δk)λi)sin(posλi)+cos((pos+Δk)λi)cos(posλi)=i=02dmodel1cos((pos+Δkpos)λi)=i=02dmodel1cos(Δkλi)
请注意,这个特性在Sinusoidal中没有被充分利用,但在苏神的RoPE中被进一步使用了(见下)。至此,Sinusoidal的细节已拆解完毕,最后总结以下,Sinusoidal有以下功能:

  • 基本要求:有界连续函数唯一性编码:使用三角函数进行编码,编码值在[-1,1]之间且连续。使用token序列编号pos和嵌入维度分量序号i输入编码函数 P E PE PE进行唯一性编码。
  • 嵌入维度层面(i):相邻两个i一组,嵌入维度的分量数值i(0-127)数值越大,三角函数的波长越长,且随i的变大呈几何级数增长,导致越大的i会使得pos的数值能造成的编码数值变化越小。
  • 相对位置信息编码(pos):进阶使用三角函数,借鉴三角函数坐标轴形式,在嵌入维度层面让相邻两个i作为一组波长相同的sin与cos。此时所有pos的token的相同emnbedding分量i(一列)使用一个三角函数,在token位置层面 P E ( p o s + Δ k ) PE(pos + \Delta k) PE(pos+Δk)可以用 P E ( p o s ) PE(pos) PE(pos)经过线性变换得到(左乘变换矩阵),两者的点积也仅由 Δ k \Delta k Δk这个Pos的相对位置差决定。

3 相对位置编码

在Attention前加入位置编码信息。Llama/GLM/Qwen/Baichuan7B用Rotary,BLOOM/ BloombergGPT/Falcon/Baichuan13B使用ALiBi

3.1 RoPE旋转位置编码

苏剑林在RoFormer中提出,位置编码是通过旋转向量来实现的。这种旋转依赖于对应位置的正弦和余弦值。
与Sinusoidal非常像,但不是直接加到embedding上,而是逐位乘到QK向量上,并通过点积实现相对位置编码,具体的原理可以惨遭Sinusoidal中演示的最后一个特性,即两个位置编码的点积结果由相对位置决定。为了让这个结果更清楚一点,下面我们深入展开。

首先,让我回顾一下Sinusoidal中使用的那个 T Δ k T_{\Delta k} TΔk线性变换矩阵。其作用是对输入的hidden_states进行两两一组的编码,在Sinusoidal中为了便于理解,我们将其两两一组的特性显式地表示为分块矩阵的形式,下面我们将展示其原本的样子:

  • 设QK的hidden_size,即head_dim设为 d h e a d d_{head} dhead;设token序列中的位置序号为 p o s pos pos
  • 复用Sinusoidal中 P E PE PE编码函数中三角函数内pos的系数,由于这个系数的变量是嵌入维度的分量i,所以我们将其单独命名为 Θ \Theta Θ并将其表示为一个长度为 d h e a d 2 \frac{d_{head}}{2} 2dhead向量形式(两两一组),即
    Θ = { θ i = 1000 0 − 2 ( i − 1 ) d h e a d , i = ∈ [ 1 , 2 , . . . , d h e a d 2 ] } \Theta= \begin{Bmatrix} \theta_i= 10000^{-\frac{2(i-1)}{d_{head}}}, i=\in[1,2,..., \frac{d_{head}}{2}] \end{Bmatrix} Θ={θi=10000dhead2(i1),i=∈[1,2,...,2dhead]}
  • 将Sinusoidal中使用的关于位置pos和维度分量i的矩阵 T Δ k T_{\Delta k} TΔk线性变换矩阵的转置展开(将显式的分块矩阵[]去掉)为一个 s h a p e = [ d h e a d , d h e a d ] shape = [d_{head}, d_{head}] shape=[dhead,dhead]的方阵,并重新命名为RoPE旋转位置编码矩阵 R Θ , p o s R_{\Theta, pos} RΘ,pos,表示这个矩阵的要素由关于维度分量i的函数 Θ ( θ i ) \Theta(\theta_i) Θ(θi)和位置pos构成,如下。
    R Θ , p o s d h e a d = ( cos ⁡ p o s θ 1 − sin ⁡ p o s θ 1 0 0 . . . 0 0 sin ⁡ p o s θ 1 cos ⁡ p o s θ 1 0 0 . . . 0 0 0 0 cos ⁡ p o s θ 2 − sin ⁡ p o s θ 2 . . . 0 0 0 0 sin ⁡ p o s θ 2 cos ⁡ p o s θ 2 . . . 0 0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 0 0 0 0 . . . cos ⁡ p o s θ d m o d e l / 2 − sin ⁡ p o s θ d m o d e l / 2 0 0 0 0 . . . sin ⁡ p o s θ d m o d e l / 2 cos ⁡ p o s θ d m o d e l / 2 ) R_{\Theta, pos}^{d_{head}}= \begin{pmatrix} \cos pos\theta_1 & -\sin pos\theta_1 & 0 & 0 & ... & 0 & 0 \\ \sin pos\theta_1 & \cos pos\theta_1 & 0 & 0 & ... & 0 & 0 \\ 0 & 0 & \cos pos\theta_2 & -\sin pos\theta_2 & ... & 0 & 0 \\ 0 & 0 & \sin pos\theta_2 & \cos pos\theta_2 & ... & 0 & 0 \\ ...... & ...... & ...... & ...... & ... & ...... & ...... & \\ 0 & 0 & 0 & 0 & ... & \cos pos\theta_{d_{model}/2} & -\sin pos\theta_{d_{model}/2} \\ 0 & 0 & 0 & 0 & ... & \sin pos\theta_{d_{model}/2} & \cos pos\theta_{d_{model}/2} \end{pmatrix} RΘ,posdhead= cosposθ1sinposθ100......00sinposθ1cosposθ100......0000cosposθ2sinposθ2......0000sinposθ2cosposθ2......00.....................0000......cosposθdmodel/2sinposθdmodel/20000......sinposθdmodel/2cosposθdmodel/2

如上,此时还是可以套用Sinusoidal中的相对位置线性变换特性 P E ( p o s + Δ k ) = P E ( p o s ) R Θ , Δ k d h e a d PE(pos+\Delta k)=PE(pos)R_{\Theta, \Delta k}^{d_{head}} PE(pos+Δk)=PE(pos)RΘ,Δkdhead,但我们的目的却是将其用于Sinusoidal中最后一条关于点积取决于相对位置的特性。因为RoPE与Sinusoidal最大的区别在于,前者将位置编码作用于Attention机制中将input_embed分别经由q_proj和k_proj投影矩阵变化后获得的query_states和key_states,而后者直接作用于input_embed。这一点不同带来的结果就是,Sinusoidal中演示的最后一个特性大放异彩。为了直观体现,设经过进入Attention机制的经过embedding的输入 i n p u t _ e m b e d input\_embed input_embed维矩阵为 x x x,其形为 n n n * d m o d e l d_{model} dmodel,其中n为token序列长度, x m x_{m} xm为其中第m个token的嵌入向量;设QK的投影权重矩阵q_proj和k_proj为 W q , W k W_q, W_k Wq,Wk,形状均为 d m o d e l d_{model} dmodel* d h e a d d_{head} dhead,也就是将嵌入维度映射到 h e a d _ d i m head\_dim head_dim维度。则RoPE的针对q或k的特定序列位置m的编码函数如下:
R o P E q , k ( x m , m ) = x m W q , k R Θ , m d h e a d RoPE_{q,k}(x_m, m) = x_m W_{q,k} R_{\Theta, m}^{d_{head}} RoPEq,k(xm,m)=xmWq,kRΘ,mdhead
之后在Attention中就是QK点积,为了更清楚看到query_states和key_states向量中不同位置pos的向量点积的结果,我们令query_states中的pos(m值)为a,key_states中的pos(m值)为b,则a和b两个位置的向量点积结果为
R o P E q ( x a , a ) R o P E k ( x b , b ) T = x a W q R Θ , a d h e a d ( x b W k R Θ , b d h e a d ) T = x a W q R Θ , a d h e a d R Θ , b d h e a d T W k T x b T RoPE_{q}(x_a, a)RoPE_{k}(x_b, b)^T = x_a W_q R_{\Theta, a}^{d_{head}} (x_b W_k R_{\Theta, b}^{d_{head}})^T = x_a W_q R_{\Theta, a}^{d_{head}} R_{\Theta, b}^{d_{head}T} W_k^T x_b^T RoPEq(xa,a)RoPEk(xb,b)T=xaWqRΘ,adhead(xbWkRΘ,bdhead)T=xaWqRΘ,adheadRΘ,bdheadTWkTxbT

由于 R Θ , a d h e a d R_{\Theta, a}^{d_{head}} RΘ,adhead为一个对角分块矩阵,所以上式中 R Θ , a d h e a d R Θ , b d h e a d T R_{\Theta, a}^{d_{head}} R_{\Theta, b}^{d_{head}T} RΘ,adheadRΘ,bdheadT实际上是对角线上的所有shape=[2,2]的分块矩阵进行点积。例如,其中第一组分块矩阵的乘积结果如下:
[ cos ⁡ a θ 1 − sin ⁡ a θ 1 sin ⁡ a θ 1 cos ⁡ a θ 1 ] [ cos ⁡ b θ 1 sin ⁡ b θ 1 − sin ⁡ b θ 1 cos ⁡ b θ 1 ] = [ cos ⁡ a θ 1 cos ⁡ b θ 1 + sin ⁡ a θ 1 sin ⁡ b θ 1 cos ⁡ a θ 1 sin ⁡ b θ 1 − sin ⁡ a θ 1 cos ⁡ b θ 1 sin ⁡ a θ 1 cos ⁡ b θ 1 − cos ⁡ a θ 1 sin ⁡ b θ 1 sin ⁡ a θ 1 sin ⁡ b θ 1 + cos ⁡ a θ 1 cos ⁡ b θ 1 ] = [ cos ⁡ ( a − b ) θ 1 − sin ⁡ ( a − b ) θ 1 sin ⁡ ( a − b ) θ 1 cos ⁡ ( a − b ) θ 1 ] = R Θ , a − b d h e a d \begin{bmatrix} \cos a\theta_1 & -\sin a\theta_1 \\ \sin a\theta_1 & \cos a\theta_1 \end{bmatrix} \begin{bmatrix} \cos b\theta_1 & \sin b\theta_1\\ -\sin b\theta_1 & \cos b\theta_1 \end{bmatrix} = \begin{bmatrix} \cos a\theta_1 \cos b\theta_1 + \sin a\theta_1 \sin b\theta_1 & \cos a\theta_1 \sin b\theta_1 - \sin a\theta_1 \cos b\theta_1 \\ \sin a\theta_1 \cos b\theta_1 - \cos a\theta_1 \sin b\theta_1 & \sin a\theta_1 \sin b\theta_1 + \cos a\theta_1 \cos b\theta_1 \end{bmatrix} = \begin{bmatrix} \cos (a-b)\theta_1 & -\sin (a-b)\theta_1 \\ \sin (a-b)\theta_1 & \cos (a-b)\theta_1 \end{bmatrix} = \\R_{\Theta, a-b}^{d_{head}} [cosaθ1sinaθ1sinaθ1cosaθ1][cosbθ1sinbθ1sinbθ1cosbθ1]=[cosaθ1cosbθ1+sinaθ1sinbθ1sinaθ1cosbθ1cosaθ1sinbθ1cosaθ1sinbθ1sinaθ1cosbθ1sinaθ1sinbθ1+cosaθ1cosbθ1]=[cos(ab)θ1sin(ab)θ1sin(ab)θ1cos(ab)θ1]=RΘ,abdhead

乘积后结果与之前形态一致,其他位置的分块矩阵类似。所以,QK点积的结果如下。

R o P E q ( x a , a ) R o P E k ( x b , b ) T = x a W q R Θ , a − b d h e a d W k T x b T RoPE_{q}(x_a, a)RoPE_{k}(x_b, b)^T = x_a W_q R_{\Theta, a-b}^{d_{head}} W_k^T x_b^T RoPEq(xa,a)RoPEk(xb,b)T=xaWqRΘ,abdheadWkTxbT

值得一提的是,旋转编码矩阵 R Θ , m d h e a d R_{\Theta, m}^{d_{head}} RΘ,mdhead是一个正交矩阵,因此可以确保在位置编码过程中的稳定性,简单理解就是,query_states和key_states中相同pos位置的token的隐向量点积后为0,只有不同位置才表示位置信息。

具体工程实现上,大模型一般和Sinusoidal一样先缓存一个token序列足够长的的缓存这些值后,模型在每次处理输入序列时,可以快速应用这些位置编码。下面跟随工程代码了解具体实现。

class RotaryEmbedding( nn.Module ):
    def __init__(self, head_dim, max_position_embeddings=151643, base = 10000 ):
        super().__init__()
        self.base = base
        self.head_dim = head_dim
        self.max_position_embeddings = max_position_embeddings
        '''
        inv_freq逆频率向量,就是前面RoPE部分的第一个式子,一个长度为d_head/2的\Theta向量。base默认为10000
        '''
        self.inv_freq = 1.0 / ( base ** ( torch.arange( 0, self.head_dim, 2, dtype=torch.int64).float() / self.head_dim ) )
        self.cos_cached, self.sin_cached = self._set_cos_sin_cache( self.max_position_embeddings )
    
    def _set_cos_sin_cache(self, max_position_embeddings ):
        """
        (1)足够长(151643)的序列的position ids向量t,和inv_freq向量外积,double,变成一个shape=[max_position_embedding * head_dim]的矩阵,形式如下
        [
        [0* \theta1, 0* \theta2,...,  0* \theta_(d_head/2), 0* \theta1, 0* \theta2,...,  0* \theta_(d_head/2)],
        [1* \theta1, 1* \theta2,...,  1* \theta_(d_head/2), 1* \theta1, 1* \theta2,...,  1* \theta_(d_head/2)],
        ......,
        [151642* \theta1, 151642* \theta2,...,  151642* \theta_(d_head/2), 151642* \theta1, 151642* \theta2,...,  151642* \theta_(d_head/2)],
        ]
        (2)将double_freqs作为输入逐位计算正弦和余弦值并分别返回两个矩阵进行缓存备用。
        (3)请注意,这里实现的功能等价于前面的旋转位置编码矩阵RoPE_{q,k}(x_m,m),但为了工程上快速计算,而取巧了。具体见下。
        """
        t = torch.arange(0, max_position_embeddings)
        freqs = torch.outer( t, self.inv_freq)
        double_freqs = torch.cat( (freqs, freqs), dim=-1)
        cos_cached = double_freqs.cos().to(dtype)
        sin_cached = double_freqs.sin().to(dtype)
        return cos_cached, sin_cached

    def forward(self, x, seq_len = None):
        """
        返回和当前对话序列长度等长的正弦余弦序列,若序列长度大于max_position_embeddings,则根据序列长度重新生成正余弦序列
        """
        if seq_len == None:
            seq_len = 1
        if seq_len > self.max_position_embeddings:
            self._set_cos_sin_cache( seq_len )
        return (
            self.cos_cached[:seq_len].to(dtype=x.dtype),
            self.sin_cached[:seq_len].to(dtype=x.dtype)
        )
    @classmethod
    def rotary_inputs( cls, x ):
      '''
      分别将query_states或key_states按d_head维度一分为二,后半部分取符号前后调换位置再cat合并。本质是语义向量复数化。将前半部分划为实部,后半部分划为虚部。
      '''
        x1, x2 = x[..., : x.shape[-1]//2], x[..., x.shape[-1]//2: ]
        return torch.cat( ( -x2, x1 ), dim=-1)

    @classmethod
    def apply_rotary_pos_emb(cls, query, key, cos, sin, position_ids=None, unsqueeze_dim=1 ):
        '''
        下式对query和key进行旋转。对上述提到的分别对query_states或key_states向量进行位置编码的操作进行取巧,具体操作是:
        (1)cos和sin分别是预先计算的足够长的token序列的self.cos_cached和self.sin_cached中对应于本次编码长度的截取部分,shape均为[seq_len, d_head],其中d_head维度为两组[0, d_head/2]部分的拼接。也可以表示为sin_double=[sin, sin], cos_double=[cos, cos]
        (2)query和key分别是被计算的query_states或key_states,shape均为[seq_len, d_head]
        (3)位置信息注入的方式是逐位积,将query向量切一半,表示为query=[q_1, q_2],则q_embed=[q_1*cos-q_2*sin, q_2*cos+q_1*sin],同样的k_embed=[k_1*cos-k_2*sin, k_2*cos+k_1*sin]
        '''
        cos = cos[ position_ids ].unsqueeze( unsqueeze_dim ) 
        sin = sin[ position_ids ].unsqueeze( unsqueeze_dim )
        q_embed = query * cos + cls.rotary_inputs(query) * sin 
        k_embed = key * cos + cls.rotary_inputs(key) * sin
        return q_embed, k_embed

因此,当上面代码中apply_rotary_pos_emb返回的q_embed与k_embed的转置进行点积时,实际上得到矩阵结果为,
[ q 1 , q 2 ] [ c o s s i n − s i n c o s ] [ c o s − s i n s i n c o s ] [ k 1 , k 2 ] \begin{bmatrix} q_1, q_2 \end{bmatrix} \begin{bmatrix} cos & sin \\ -sin & cos \end{bmatrix} \begin{bmatrix} cos & -sin \\ sin & cos \end{bmatrix} \begin{bmatrix} k_1, k_2 \end{bmatrix} [q1,q2][cossinsincos][cossinsincos][k1,k2]

当涉及到矩阵中具体行列的值时,同 R o P E q ( x a , a ) R o P E k ( x b , b ) T RoPE_{q}(x_a, a)RoPE_{k}(x_b, b)^T RoPEq(xa,a)RoPEk(xb,b)T结果,位置相同时为0,不相同时表示为相对位置编码结果。这种相对位置关系可以写成Sinusoidal中提到的线性变换关系,由于是维度分量两两一组为sin和cos,输入值可以理解为角度,所以RoPE又称为旋转位置编码。这种代码在旋转矩阵的可扩建上以及乘积的位置相对性上都是可以外推的。

3.2 ALiBi线性偏置注意力,2021

之前的两个位置编码方式都是对embedding或者query_states加上或者逐位乘一个基于三角函数的位置编码向量,而Attention with Linear Biases (ALiBi)(论文Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation)的核心思想是用一个和query_states, key_states的特定token之间的距离成比例的一个惩罚项来偏置query_states和key_states点积后的注意力权重得分。原始论文宣称可以加快11%的训练速度,以及减少11%的内存使用

简单来说,该方法认为RoPE通过三角函数来编码相对位置的思路过于冗杂,既然最终结果是要表示不同两个token的相对位置信息,何不直接根据相隔的距离直接编码?两个token在序列中相隔一个位置,就对qk点积后的注意力得分矩阵中的位置-1,相差2就-2,简单明了。如下图:

在这里插入图片描述

此外,为了对多个head的情况也加以区分,还引入了一个坡度概念m,当有n个head时,m的计算公式为:

m = { 2 − 8 i , i ∈ [ 1 , 2 , . . . , n ] } m= \begin{Bmatrix} 2^\frac{-8}{i}, i\in [1,2,..., n] \end{Bmatrix} m={2i8,i[1,2,...,n]}
也就是说,在不同的head的注意力评分矩阵中,根据head的序号i,对位置差这一乘法统一乘以一个坡度 2 − 8 i 2^\frac{-8}{i} 2i8
仔细一想,确实,这个方法简单粗暴有效,目前已经和RoPE并称两大位置编码方案。但如果自行设计一个大模型,到底用什么position encoding其实还没有一个公认的最优方案,这两者(RoPE和ALiBi)目前看都可尝试,具体情况具体分析。

  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值