六、位置编码Positional Encoding
(一)为什么需要位置编码器?
前面我们花了大幅的笔墨、详细的描述了embedding层和注意力模块的计算流程,可以看出attention模块确实是学习了样本与样本之间的关系。每个样本都计算了它和所有样本(包括它自己)之间的attention分数。但是有没有发现:这个attention score矩阵是没有反映样本和样本之间的顺序关系的!仍然以上篇的例子来展示:
(1)Input Embedding词嵌入层
当我们的文本数据确定下来了,那文本->字典之间的映射就确定了,那字典->每个tocken的标签编码就确定了,那每个tocken->词嵌入向量之间的映射也就确定了。所以如上图的句子never give up和up give never,这两句语义完全不一样的文本,映射出来的词向量却是一样的。所以embedding层是不携带样本在它自己sequence中的位置信息的!就是embedding层是不携带词序信息的。或者说embedding层解决的仅仅是文本的表示问题。如果说embedding层迭代后携带了语义信息,那也是比如同义、反义、同类、异类这种语义信息。
(2)Attention模块
对于压根就不含词序信息的词向量喂入attention模块后,attention模块计算出来的attention score自然也是没有考虑词序信息的注意力分值喽。以上图A_attention_score矩阵中的第3行第1列的0.7818为例,0.7818是Q矩阵中的up词向量与K矩阵中的never词向量之间的注意力分数。而在B_attention_score矩阵中,Q矩阵中的up和K矩阵中的never之间的注意力分数也是0.7818。这说明attention模块计算出来的注意力分数是无法反映样本与样本之间的词序差别的。也说明序列A和B的Q\K矩阵都是不携带词序信息的,attention就也无法计算出不同词序的两个词之间的注意力分数了。
我们知道每个词在句子中出现的顺序是极其重要的。例如中文文本中的"屡战屡败"和"屡败屡战","战"和"败"的位置不一样,这两个sequence的语义是大为不同的。我们希望如果"战"在"败"前面,那"屡"的词向量就多往有讽刺的语义方向靠近,"战"的词向量就多往消极的语义方向靠近,"败"的词向量也多向不好的语义方向旋转,这样模型对整个seqence的理解也是往不好的方向的语义去理解的。如果"败"在"战"前面,那"屡"的词向量就多往有正向的、坚持不懈的语义方向靠近,"战"的词向量就多往积极的语义方向靠近,"败"的词向量也多向叹息、意难平的语义方向旋转,那模型对整个sequence的语义理解也是往好的方向去理解。这才是我们想要attention达到的效果。
所以,要使模型在处理文本时能够更好地理解单词的语义,提高模型对语义的建模能力,我们还得解决词序的问题。也所以Transformer架构中单独设置了一个位置编码器(Position Encoding, PE),希望使用合适的位置编码引入一定的位置先验信息,让attention一起学,以提高模型对序列的理解能力。
(二)如何编码样本的位置信息?
1、位置编码的目的:
编码样本的位置信息是给样本编码一个它所在的sequence中的位置数字。也就是在词向量上加入词序信号,让模型对样本的位置有一定的先验信息,比如:
样本的绝对位置信息:a1是第一个token,a2是第二个token......
样本的相对位置信息:a2在a1的后面一位,a4在a2的后面两位......
不同位置间的距离信息:a1和a3差两个位置,a1和a4差三个位置....
所以,位置编码就是把词序信号加到词向量上,或者说,通过注入词的顺序信息来增强模型输入,让模型可以获取词序信息并进行学习,增强模型对序列理解能力。所以,input embedding和 positional encoding两者都是为了帮助模型更好地理解文本数据,但它们解决的是不同层面的问题:input embedding解决的是语义表示问题,而positional encoding解决的是位置信息丢失问题。这两者结合起来能够提高模型对文本数据的建模能力。
2、位置编码的原则:
一种好的位置编码方案需要满足以下3点要求:
一是,它能为每个token输出一个独一无二的、确定性的编码,也就是能用来表示一个token在序列中的绝对位置。
二是,它还可以表示一个token的相对位置,也就是不同的位置向量是可以通过线性转换得到的,也就是两个token之间得有线性关系。
三是,编码的值应该是有界且连续的。这样模型在处理位置向量时更容易泛化,即更好的泛化处理更长的句子、更好的泛化到训练数据分布不一致的序列。
所以位置编码很大程度上是一个数学问题,就是如何找到一个数学关系,可以恰当的拟合上面的要求。也所以本部分的原理比较偏难,因为有大量的数学推导,但是本文不去做理论研究,重点讲每种编码的特性以及怎么使用。
3、目前主流的位置编码算法
在实践中发现,位置编码对模型性能有很大影响,对其进行改进也会带来进一步的性能提升。所以我先简单归纳总结一下目前一些主流的位置编码算法:
(1)绝对位置编码(absolute PE):在词嵌入向量中添加固定的位置信息来表示单词在序列中的绝对位置。就是不同词的位置编码仅由其位置唯一决定。这种编码方式通常采用固定的公式来计算每个位置的位置编码。比如Sinusoidal, Complex-order等。
(2)相对位置编码(relative PE):根据单词之间的相对位置关系来计算位置编码。这种编码方式更加灵活,能够捕捉到不同单词之间的相对位置信息,有助于模型更好地理解序列中单词之间的关系。
这种编码方式可以通过,比如微调自注意力运算过程,使其能分辨token之间的相对位置。就是在算Attention的时候考虑当前位置与被Attention的位置的相对距离,比如XLNet模型,T5模型,DeBERTa模型, 通用相对位置编码(Universal RPE)等,通常也有着优秀的表现。
相对位置编码起源于Google的论文《Self-Attention with Relative Position Representations》,华为开源的NEZHA模型也用到了这种位置编码,后面各种相对位置编码变体基本也是依葫芦画瓢的简单修改。
(3)旋转位置编码(Rotary Position Embedding,RoPE):旋转位置编码RoPE是通过绝对位置编码的方式实现相对位置编码。RoPE是一种相对较新的位置编码技术。RoPE的核心思想是将位置信息直接融合进每个词元的表示中,而这一过程是通过旋转变换来实现的。就是在构造查询矩阵和键矩阵时,根据其绝对位置引入旋转矩阵。这种方法特别适用于基于Transformer的模型,典型的就是大模型,可以有效改善这些模型在处理长序列时的性能。但是常见的大模型代码RoPE实现和论文中表达有所差别(GPT-J style,GPT-NeoX)。
(4)可学习位置编码(Learnable PE):又叫训练式位置编码。比如bert、GPT等模型所用的position encoder,它们是随机初始化一个embedding[512,768]矩阵作为位置向量,让它随着训练过程更新,让模型自己学到位置信息。
(5)递归式位置编码(FLOATER编码):使用RNN作为特征提取器获取位置编码信息。前面讲RNN时一般都会有一个ht的输出,这个输出就是样本与样本之间的信息,而且RNN是时序循环的,所以RNN本身的结构,在物理上,就自带词的前后顺序信息的。如果在Transformer的输入encoder之前先接上一层RNN,那么理论上就不需要位置编码。虽然递归式的FLOATER编码具有更好的灵活性和外推性。但是递归形式的位置编码牺牲了并行性,带来速度瓶颈。
下面展开介绍几种常见的编码算法的原理和计算流程。
(三)正余弦位置编码Sinusoidal
正余弦位置编码又叫三角函数式位置编码。也是Transformer论文《Attention Is All You Need》中使用的编码方法,论文中编码公式如下:
上图左上角的公式和文字是论文《Attention is all you need》中的截图。
我们咋一看论文中的公式,首先就是懵,它是怎么想到这个公式的?!作者的设计思路,这篇博文(超易懂) Transformer位置编码设计原理详解-CSDN博客 写得非常生动,感兴趣的可以参考。
上图下半部分的数学推导,是证明,不同位置的向量是可以通过线性转换得到的。中间的矩阵就是变换矩阵。这里不对数学推导解读,其实我自己也是看得费劲,呵呵。这里我重点是展示一下如何使用这种位置编码,从更加直观的角度认识它。
那我就先用论文中的公式,手动计算一个小序列的位置编码,看看有啥规律:
可见:
一是,每个单词的位置编码向量和它的embedding词嵌入向量的长度是一样的。也就是我们为每个词向量的每个特征都进行了位置编码。
二是,每个特征的编码值都在[-1,1]之间。
三是,序列长度、超参数n和dmodel,都对编码有影响,有必要看看它们都是如何影响编码的。
1、先看序列长度对位置编码结果的影响
(1)说明只要n和dmodel两个超参不变,任意两个序列,不管这两个序列长度是否一样,只要序列中的样本的位置一样,那位置编码就一样。就是位置编码只与位置有关,和词向量本身无关。而且由于正余弦函数是周期函数,你可以依次采样任意长度序列的位置编码:
(2)比如词never,不管never本身的词向量是什么,只要它在序列中排第一个位置,它的位置向量就是[0,1,0,1];如果排第三个位置,它的位置向量就是[0.9,-0,4,0.2,1.0]。在序列中的位置不同,位置编码就不同。
也所以,只要n和dmodel两个超参不变,位置编码的数值就只与位置有关,位置编码是不需要模型迭代的,也是不变的。
也所以,以单词never为例,它的embedding词嵌入+ position encoding = never'的新向量,never是在第一个位置和在第三个位置的never'是不一样的!这样不一样的never'进入attention模块后 -> Q和K矩阵就不一样了 -> Q、K矩阵不一样计算的QKt自然也不一样 -> 那计算的attention分数就不一样了->此时这个注意力分数矩阵就学习了样本的位置信息了。
也所以,你可以理解为:embedding后的词嵌入,PE按照每个样本在sequence中的位置,把embedding后的词嵌入加入词序信息后,才送入attention的。所以PE是强化模型输入的。是给模型输入增加了词序信号的。
2、再看dmodel对位置编码的影响
上图A处的位置虽然没变,但位置编码已经悄悄变了!因为dmodel变了,计算公式就变了,自然计算结果也变了。
下面我们也来看看取点的正余弦波的分布:
就是dmodel是几,就得有几个正余弦波,而且这些正余弦波的波长是从小逐渐到大的。也所以上图的矩阵图和热力图会呈现那种分布状态。
3、最后看看n是如何影响编码的:
(1)上图A处的编码又又悄悄变了,因为n也是计算公式中一个因子啊,n从100变成10000了,计算结果自然也变了。
(2)n是控制波长变化的速度的:
可见,如果n比较大,就是波长变化比较快,那词向量中越往后的特征,它们的位置编码就变得几乎一样,就是越往后面的特征位置编码的差异越小。因为此时正余弦波长太长了,甚至呈现出水平的状态。所以此时位置信息就主要集中在词向量的前面部分特征中。
到此大家对正余弦位置编码Sinusoidal的特点就了解得差不多了。我到网上看到一个特别生动的动图,这里根据这个动图把Sinusoidal编码再小结一下:
1、正余弦位置编码是构造一组、波长从小到大的正余弦函数组,然后按照顺序依次采样,而得到位置编码的。
2、dmodel决定要设置多少个正余弦函数。dmodel越大,正余弦函数组中的函数个数越多。因为正余弦函数组中的函数个数=dmodel嘛。上面动图中的dmodel就是8,所以它有8个正余弦函数。
3、n决定这组正余弦函数的波长依次变化快慢的速度的。n越大,后面的正余弦波就越长,甚至看似水平了,所以越靠后的特征的位置信息差异就越小。所以如果你想让所有的位置编码都携带位置信息,那你最好不要把n设置得太大。
4、所以,一旦n和dmodel确定下来了,正余弦函数组就确定了。dmodel决定有几根正余弦函数,n决定每个正余弦函数的波长。 我们只要按照X轴,0123456...,离散的采样即可。每个点采dmodel个值,这些值就是这个点的位置编码。
这个编码方式让我不禁联想到钟表,在24小时内,我们把24小时平均切分成24*60*60=86400这么多个小块,也就是切分成秒,是不是这86400个样本中,每个样本中的(时、分、秒)都是唯一的。这种是不是也是一种编码呢。当然我们可以无限细分,细分到毫秒、微秒、纳秒。更当然你可以不按照60进制,anyway随便进制。。。思绪飘了。。。。
(四)旋转位置编码RoPE
待续。。。。
几种常用的位置编码介绍及pytorch实现_正弦位置编码-CSDN博客
不同的位置编码方法各有优劣,适用于不同类型的任务。正弦和余弦位置编码适用于序列长度不定且具有相对位置信息的任务;可学习的位置编码适用于固定序列长度且需要灵活调整的位置编码任务;经典相对位置编码适用于需要捕捉相对位置信息的任务;旋转位置编码适用于长序列建模任务。
应用建议:
自然语言处理任务:正弦和余弦位置编码、可学习的位置编码。
固定序列长度任务:可学习的位置编码。
相对位置信息重要任务:经典相对位置编码。
长序列建模任务:旋转位置编码。
其他建议:
在选择位置编码方法时,应结合具体任务需求和模型结构,选择最适合的位置编码方式。
对于需要处理长序列的任务,可以尝试结合多种位置编码方法,提高模型的效果。
对于计算资源有限的情况,可以优先选择实现简单且计算开销较低的位置编码方法,如正弦和余弦位置编码、可学习的位置编码。