探秘Transformer之(8)--- 位置编码
探秘Transformer之(8)--- 位置编码目录
[*]探秘Transformer之(8)--- 位置编码
[*]0x00 概述
[*]0x01 问题
[*]1.1 词序的重要性
[*]1.2 Transformer架构的缺陷
[*]位置不变性
[*]证明
[*]查询矩阵的变化
[*]注意力矩阵的变化
[*]注意力分数的变化
[*]最终结果
[*]后果
[*]1.3 解决思路
[*]1.4 应具备的性质
[*]0x02 编码方案演化
[*]2.1 整型数字位置编码
[*]2.2 乘法表示
[*]2.3 归一化
[*]2.4 二进制位置编码
[*]2.5 需求拓展
[*]2.6 三角函数编码
[*]性质
[*]定义
[*]编码方式
[*]公式解读
[*]具体样例
[*]优点
[*]0x03 三角函数编码思路分析
[*]3.1 作者的话
[*]3.2 为何要多维度
[*]利于处理
[*]避免重复
[*]区分特征维度
[*]3.3 为何要多种频率
[*]3.4 为何同时使用cos,sin
[*]3.5 表示绝对位置
[*]结论
[*]解读
[*]周期性
[*]3.6 表示相对位置
[*]相对语义的重要性
[*]结论
[*]证明
[*]3.7 旋转
[*]3.8 为何是相加
[*]0x04 三角函数编码的特性
[*]4.1 无向性
[*]4.2 远距离衰减性
[*]4.3 外推性
[*]问题
[*]论证
[*]0x05 NoPE
[*]5.1 不需要
[*]5.2 需要
[*]0xFF 参考
0x00 概述
位置编码(Positional Embedding)是一种用于处理序列数据的技术,被用来表示输入序列中的单词位置。在Transformer 实现中起到了举足轻重的作用。Transformer 需要关注每个输入词的两个信息:该词的含义和它在序列中的位置。位置编码可以针对这两个关注点做关键的补充。
[*]针对“输入词的含义”这个关注点,Transformer通过嵌入层对词的含义进行编码,位置编码可以在此之上注入位置相关的先验知识,比如:"相近的token应该具有相近的Embedding"、“相对位置比绝对位置更重要”、“远距离的相对位置可以不用那么准确”、“越远的相对位置越模糊”、“越近的Token越重要”和”越远的Token平均而言越不重要“等。
[*]针对“输入词在序列中的位置”这个关注点,Transformer通过位置编码层表示该词的位置。在序列数据中,单词的顺序和位置对于语义的理解非常重要。传统的词向量表示只考虑了单词的语义信息,而没有考虑单词的位置,而注意力机制又有置换不变性的弊端。于是,位置编码为每个单词分配一个唯一的位置向量,这样可以将单词的位置信息融入到模型的表示中,进而克服注意力机制的置换不变性,使得模型在处理序列数据时更好地理解单词的上下文和关系。
最终,Transformer 通过结合这两个层的输出来完成两种不同的信息编码。
注:在深度学习中,一般将学习出来的编码称之为embedding,有将位置信息"嵌入" 到某个向量空间的意思。例如Bert的位置向量就是学习得到,所以称为"Position Embedding"。而原始Transformers模型中位置向量的思路是通过规则(三角函数)直接计算出来,不涉及学习过程,被称为Position Encoding。
0x01 问题
1.1 词序的重要性
无论何种语言,其语句都是一种时序型数据,即每个词都是位置相关的(position-wise)。句子中词的顺序和位置决定了句子的实际语义。单词相同但顺序不同可能会导致句子的语义发生变化。比如下面两个句子的意义就完全不同。
[*]从北京到上海。
[*]从上海到北京。
历史上还有经典的曾国藩例子:如果写“臣屡战屡败”,结果可能是曾国藩被拖出去斩首。如果写“臣屡败屡战”,结果曾国藩是忠勇无双有赏赐。
因此,自然语言文本信息的处理是一个具有先后顺序的序列任务,位置信息对于理解语言是相当重要的,学习不到顺序信息,那么模型效果将会大打折扣,因此需在模型中引入某种表达位置的机制。
1.2 Transformer架构的缺陷
位置不变性
Transformer模型抛弃了RNN、CNN作为序列学习的基本模型,完全采用注意力机制取而代之。对于一个输入句子,其单词不再是顺序输入,而是一次性输入一个序列中的所有词,依靠纯粹的自注意力机制来捕获词之间的联系,直接对这个序列整体进行特征变换。
注意力操作是一种全局操作,可以捕捉句子中较长的依赖关系。比如,Transformer可以让序列中每两个元素\(x_t\),\(x_s\)都能无视其绝对位置\(t\),\(s\)和相对位置\(t-s\)而进行信息交换,从而计算输入序列中的每个元素与整个序列的注意力权重。或者说,序列中位置信息是以常数形式进行变换,这样才能够防止长时信息丢失和遗忘问题。
\
然而,如果考虑到位置关系,则Transformer的优势就变成了劣势,因为Transformer本身来说是没有位置或者顺序的概念的,它具有位置不变性(ORDER INVARIANCE)或者说置换不变性(Permutation Invariance)。
置换不变性的意思是:词与词的位置随意变动,并不会导致这些词的注意力权重产生变化,注意力层的整个结果维持不变。即,无论位置如何变化,每一个词向量计算结果和位置变化之前完全一致,仅仅是词向量在输出矩阵的排列随着词和词位置互换而对应调整了一下。我们假设目前没有加入位置编码,则对于自注意力的计算公式\(A=softmax(QK^T)V\)来说,有如下例子。如果q是“我”,无论句子是“我爱你”还是“你爱我”,其注意力输出都是完全一致的。这说明在没有位置序列信息的情况下,改变词语顺序的句子实际语义是不一样的,但是注意力输出相同,无法准确建模。
import torch
import torch.nn.functional as F
d = 8 # 词嵌入维度
l = 3 # 句子长度
q = torch.randn(1,d) # 我
k = torch.randn(l,d) # 我爱你
v = torch.randn(l,d) # 我爱你
orig_attn = F.softmax(q@k.transpose(1,0),dim=1)@v
# 调转位置
k_shift = k[,:] # 你爱我
v_shift = v[,:] # 你爱我
shift_attn = F.softmax(q@k_shift.transpose(1,0),dim=1)@v_shift
print('我爱你:',orig_attn)
print('你爱我:',shift_attn)证明
我们接下来证明下位置不变性。在 Transformer-NoPE 架构中,Embedding 层和 FFN 层都是point-wise(逐点式),均与位置或者说顺序无关,只有注意力模块与位置相关。我们只需要关注注意力机制是否为置换等价或者顺序不变。
假设有置换矩阵\(\mathbf{P_\pi}\), 则\(\mathbf{P_\pi X}\) 是将\(\mathbf{X}\)的行依据\(\pi\)重新排列的结果(可以理解为把K,V按行打乱顺序,相当于把句子中的语序打乱),注意,\(\mathbf{P_\pi}\)只作用于\(\mathbf{X}\)的行,不改变列的顺序。我们看看把\(\mathbf{P_\pi X}\)输入注意力机制的运行逻辑中,是否会影响其运行结果。
查询矩阵的变化
原来的查询矩阵是:
\[\mathbf{Q =XW^{(q)}} \\\mathbf{K =XW^{(k)}}\]
则置换后的查询矩阵也要发生变化,即:
\
注意力矩阵的变化
原来的注意力计算公式是:
\[\mathbf{A} = \frac{1}{\sqrt d} \mathbf{QK}^\top\]
则置换后的注意力计算也要发生变化,即:
\[\mathbf{A}' = \frac{1}{\sqrt d} \mathbf{Q'(K')}^\top \\= \frac{1}{\sqrt d}({\mathbf{P_{\pi}Q)(P_{\pi}K)}}^\top \\= \frac{1}{\sqrt d}\mathbf{P_{\pi}QK}^\top P_{\pi}^\top \\= \mathbf{P_{\pi}}\mathbf{A} P_{\pi}^\top \\\]
注意力分数的变化
原来的注意力分数的计算公式是:
\
则置换后的注意力分数的计算也要发生变化,即
\
因为置换矩阵只是排列的行,而Softmax的操作是行独立的,实际上对行和列进行重新排列并不会改变Softmax的结果,因此有:
\
最终结果
我们把上述变化综合起来,最终推导如下所示:无论句子顺序如何,其注意力计算结果完全一致。即假设\(x_s\),\(x_t\)分别代表第s和第t个输入单词,则有\(T(...,x_s,..., x_t, ...) = T(...,x_t,..., x_s, ...)\),T函数天然满足恒等式T(x,y)=T(y,x),无法区分输入的是\(x,y\)还是\(y,x\)。
后果
因此,在未加入位置信息的情况下,Transformer存在两个问题。
第一个问题是模型不能捕捉序列的顺序。
没有位置信息的自注意力模型顶多是一个非常精妙的“类词袋模型“,即模型把序列看成是一个集合,既然是集合,那么模型把输入序列的每一个单词都同等看待,自然就没有位置信息,那么隐状态就和时序无关。某个单词如果在不同的位置出现多次,其每次计算出的注意力加权求和结果都完全一致。
比如位置 j 的token最终的注意力输出如下。可以看到,因为位置信息是以常数形式进行变换,所以计算公式中没有任何位置信息的描述,只有求和算子,这就是类词袋模型。
\
而且,给定一个句子,最终的这个句子的词嵌入组合只来源于句子中所有单词的特征,和句子中单词的排序没有任何关系,即丢失了词之间的位置信息。输入元素位置的变动不会对注意力结果产生影响,从而只要集合中包含的元素是确定的,输出结果就是确定的。
然而,这显然和语言、代码、语音等序列的内在特征相违背:一句话打乱单词顺序后,所表达的意思、单词指代或修饰的对象、甚至单词对应的语义,可能都会随之改变。举个例子。将[我,爱,你]和[你,爱,我] 都输入Transformer,这个类词袋模型给出的句子表征会完全一致。而我们期望的是:“爱”这个词的词向量,在“我爱你”和“我你爱”这两个句子中,经过神经网络计算应该得到不同的输出,因为我们的输入中,“爱”这个词在句子中的位置实际上已经发生了变化,我们输入的是两个不同的句子,一个词在两个不同的句子中,应该是不同的向量表示,但是神经网络无法捕捉到这个变化。
[
[我,爱,你]=> Transformer => ,
]
[
[你,爱,我]=> Transformer => ,
]第二个问题是单词间的权重和位置无关。
无论 t 和 s 所处的位置如何变化,它们之间的注意力权重 \(A_{t,s}\) 均不会发生变化,也就是位置无关。然而,这又和语言的特性相违背:多数的时候,离得越近的单词相关性可能越高,我们希望它们之间的的注意力权重更大;离得很远的两个单词可能毫无关系,我们希望其注意力权重更小。
1.3 解决思路
既然Transformer中的自注意力机制无法捕捉输入元素序列的顺序,因此我们需要对位置关系进行建模,把单词的顺序合并到Transformer架构中,从而打破这种置换不变性,于是Transformer作者提出了 Position Embedding 的方法,也就是“位置向量”或者说”位置编码“。
位置编码的作用就是给每个位置都加上一个唯一的位置编码向量,即将词序信息向量化。对于输入的每个单词,每个单词都有对应的向量 (与位置无关)。为了给每个位置都加上一个唯一的位置编码向量码,需要使用另一个具有相同维度的向量,其中每个向量唯一地代表句子中的一个位置。然后通过将词嵌入与其相应的位置嵌入求和来形成 Transformer 层的输入,即输入模型的整个Embedding是Word Embedding与Positional Embedding直接相加之后的结果。模型会将这个结果矩阵作为输入提供给后续层。
这样给每个词都引入了其在句子中特定位置的信息,类似\(T(..., x_s,..., x_t, ...) = T(...,x_s+p_s,..., x_t+p_t, ...)\)。注意力机制就可以分辨出不同位置的词,从而模型不但知道注意力要聚焦在哪个单词上面,还要知道单词之间的互相距离有多远,在计算注意力得分时就可以考虑两个元素之间的相对位置。模型也就具备了处理序列问题的能力。后面无论每个输入向量学习到了什么信息,都能够通过位置向量回溯到模型中的具体位置,也就为后面的输出提供了可参考的依据。
1.4 应具备的性质
论文"A Length-Extrapolatable Transformer"论文中提到了了transformers位置建模的三条设计原则:具备位置敏感性;针对位置平移具备鲁棒性;可以外推。原文摘录如下:
[*]First, a Transformer should be sensitive to order. Otherwise, it will degenerate into a bag-of-word model which confuses the whole meaning.
[*]Then, position translation can’t hurt the representation a lot especially combing with the proper attention-mask operations.
[*]After that, a good sequence model needs to deal with any input length.
论文"On Position Embeddings in BERT"则指出,Position Embedding是为了位置的时序特点进行建模。基于此,该论文提出Position Embedding应该具有三个特性:平移不变性(translation invariance,两个位置的关系只与相对位置有关)、单调性(monotonicity,随着距离的增大而衰减),对称性( symmetry,两个位置的关系是对称的,i,j和i,j相同)。原文摘录如下:
Informally, as positions are originally positive integers, one may expect position vectors in vector space to have the following properties: 1) neighboring positions are embedded closer than faraway ones; 2) distances of two arbitrary m-offset position vectors are identical; 3) the metric (distance) itself is symmetric.
[*]Property 1. Monotonicity: The proximity of embedded positions decreases when positions are further apart.
[*]Property 2. Translation invariance: The proximity of embedded positions are translation invariant.
[*]Property 3. Symmetry: The proximity of embedded positions is symmetric
我们顺着这些展开,详细看看一个良好的位置编码应该具备的性质,理想情况下,位置编码应满足以下标准:
[*]唯一性/确定性。每个位置都需要一个无论序列长度如何,都保持一致的编码,即无论当前序列的长度是 10 还是 10,000,位置 5 处的标记都应该具有相同的编码。而且,该位置编码必须是确定性的,即每个位置都有唯一的编码(或者尽量不同),这样才能体现同一个token在不同位置的区别,确保模型对位置有分辨能力。另外,如果位置编码能从一个确定的过程中产生,那将是最理想的。这样,模型就能有效地学习编码方案背后的机制。
[*]有界性:编码范围是有界的,值要在一定的范围内不会过大导致溢出。因为位置信息本身就是矫正量,不应该随着句子加长,位置编码的数字就无限增大,那样容易对单词的本体语义向量造成影响。
[*]相对性:对模型来说,真正重要的往往不是绝对位置,而是token之间的相对位置。所以我们期望位置编码即可以表达绝对位置信息(表示同一个单词在序列之中不同位置的区别,即token在序列之中的绝对位置),也可以表达相对位置信息(如果有一组词无论在什么位置都不会发生词义的变化,则我们认为这组词的实际含义与绝对位置没有关系)。
[*]单调性或者说距离衰减性:位置编码的最大意义就是给模型提供位置语义相关性。而位置相关性应该随相对位置距离增大而减少,并且是单调衰减关系。具体就是距离近的相关性高,距离远的相关性低。距离衰减性相当于软的窗口注意力。这符合人类自然语言的习惯,即相近的文字关联性更强,位置相近的Token平均来说应该获得更多的注意力,而距离比较远的Token平均获得更少的注意力。单调性也可以等同为一个卷积神经网络,在做信息聚合的时候会优先考虑局部信息。距离越近的元素则会被考虑的越多。
[*]平移不变性:任何位置之间的相对距离在不同长度的句子中应该是一致的。具体来说是,两个位置的关系只与相对位置有关,与序列长度无关。在任何长度不同的序列中,相同位置的Token之间的相对位置/距离保持一致(体现Token位置之间差异的不变性)。比如长度10和长度100的句子中,第1个单词和第5个单词之间的距离应该相同。即,如果两个token在句子1中的相对距离为k,在句子2中的相对距离也是k,那么这两个句子中,两个token之间的相关性应该是一致的,也就是attention_sample1(token1, token2) = attention_sample2(token1, token2)。
[*]线性关系。位置之间的关系在数学上应该是简单的,或者说存在线性关系。如果知道位置 p 的编码,那么计算位置 p+k 的编码就应该很简单,这样模型就能更容易地学习位置模式。
[*]多维度:随着多模态模型成为常态,位置编码方案应该能够自然地拓展至多个维度,从 1D 扩展到 nD。这将使模型能够使用图像或脑部扫描这样的数据,它们分别是 2D 和 4D 的。
[*]外推性或者说泛化性:位置编码可泛化到比训练中遇到的序列更长的序列上。为了提高模型在现实世界中的实用性,它们应该在训练分布之外泛化。因此,编码方案需要有足够的适应性,以处理意想不到的输入长度,同时又不违反任何其他理想特性。具体说就是编码系统不受句子长短的影响(即适用于任意文本长度),在训练没有见到的样本上,没有见过的长度上也能表现不错。化未见为已见,化分布外为分布内。
[*]周期性:这个性质是出于实现上的考虑。因为要求是相对且有界,所以容易联想到一个性质 —— 周期性,这样更远距离的值可以和较近距离的值相同,从而有一定的外推性。
[*]结合语义信息。在涉及长上下文理解和搜索的任务中,注意力机制应该优先考虑语义相似性,而不是被与位置编码相关的信息所掩盖,因为在较长距离上位置编码的相关性可能较低。因此,PE 应该结合语义和位置信息,确保语义信息不受位置距离的过度影响。
0x02 编码方案演化
既然知道了理想位置编码的属性,我们就来尝试从无到有一步一步设计和迭代位置编码方案,并且比对各种方案和期望性质之间的差距。Positional encoding有一些想象+实验+论证的意味,或者说,在 LLM 引入位置信息更像是构建特征工程,这里特征对应的信息就是位置。
为了更好的说明,我们先给出哈佛代码。这就是Transformer论文中的方式。函数的总体目标是计算每个维度(每一列)的相关位置信息。因此需要:
[*]初始化一个形状为 (max_len, 1) 的绝对位置矩阵position。对应代码中的position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) 。在position中,词汇的绝对位置用它的索引表示。绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中。因此,需要将形状为 (max_len, 1) 的绝对位置矩阵,变换成(max_len, d_model) 形状,然后覆盖初始位置编码矩阵。这就需要构建一个转换矩阵 div_term。
[*]构建转换矩阵div_term 就对应代码中的 div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)),具体操作为。
[*]将自然数的绝对位置编码缩放成足够小的数字 10000 ^ (2i / d_model),这样有助于在之后的梯度下降过程中更快的收敛。
[*]用 torch.exp()函数来构建一个形状为(1, d_model) 变换矩阵div_term,用于缩放不同位置的正弦和余弦函数。div_term是256维的张量,即公式里面sin和cos括号中的内容。和原始论文不同,哈佛代码通过e和ln进行了变换,这样速度会快一些。
[*]div_term具体如下:
\[{div\_term}_i = 10000^{-\frac{i}{d_{model}}}\]
[*]使用position和div_term构建位置编码pe,计算每个维度(每一列)的相关位置信息。具体是使用正弦torch.sin(position * div_term)和余弦函数torch.cos(position * div_term)来生成位置编码,位置编码是一个d_model维的向量,对于这个向量的每一个维度,如果这个维度为偶数,则用正弦函数进行编码,如果这个维度为奇数,则用余弦函数编码。
[*]使用unsqueeze(0)在第一个维度添加一个维度batch_size,以便进行批处理。
代码中的几个注意点如下:
[*]Transformer模型的输入X:, d_model],是batch_size个句子的编码。位置编码是对一条句子中所有word的位置进行编码,并且由于我们对位置编码后要加到X上,因此,一个位置的编码的维度与一个word编码的维度相同,都是d_model。这样,一条句子的位置就编码为, d_model]维度的张量,batch_size条句子的位置编码就是, d_model]维度的张量。
[*]代码div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))使用等价的指数+对数运算,其原因是为了确保数值稳定性和计算效率。一方面,当d_model较大时,直接使用幂运算会导致10000 ^ (2i / d_model) 变得非常小,以至于在数值计算中产生下溢。通过将其转换为指数和对数运算,这样可以在计算过程中保持更好的数值范围,从而避免这种情况。另一方面,在许多计算设备和库中,指数和对数运算的实现通常比幂运算更快。
[*]调用 Module.register_buffer 函数。register_buffer通常用于保存一些模型参数之外的值,比如在 BatchNorm 中的 running_mean,它不是模型的参数,但是模型也会修改它,而且在预测的时候也要使用它。在此处,pe 是一个提前计算好的常量,在前向传播时候要用到它。因此register_buffer()函数会把 pe保存下来。
具体代码如下。
class PositionalEncoding(nn.Module): def __init__(self, d_model, dropout=0.1, max_len=5000): """ d_model: 词嵌入维度 max_len: 序列的最大长度 dropout: 置0比率 PE(pos, 2i) = sin(pos/pow(10000, 2*i/d_model)), (0
页:
[1]