【Transformer系列】深入浅出理解Positional Encoding位置编码

一、参考资料

一文教你彻底理解Transformer中Positional Encoding
Transformer Architecture: The Positional Encoding
The Annotated Transformer
Master Positional Encoding: Part I
如何理解Transformer论文中的positional encoding,和三角函数有什么关系?
图解Transformer系列一:Positional Encoding(位置编码)
Transformer的位置编码详解

二、Positional Encoding

1. 引言

在任何一门语言中,词语的位置和顺序对句子意思表达都是至关重要的。传统RNN模型天然有序,在处理句子时,以序列的模式逐个处理句子中的词语,这使得词语的顺序信息在处理过程中被天然的保存下来,并不需要额外的处理。

由于Transformer模型没有RNN(循环神经网络)或CNN(卷积神经网络)结构,句子中的词语都是同时进入网络进行处理,所以没有明确的关于单词在源句子中位置的相对或绝对的信息。为了让模型理解序列中每个单词的位置(顺序),Transformer论文中提出了使用一种叫做Positional Encoding(位置编码)的技术。这种技术通过为每个单词添加一个额外的编码来表示它在序列中的位置,这样模型就能够理解单词在序列中的相对位置。

2. Positional Encoding的概念

在Transformer等模型中,输入序列通常是一系列嵌入向量(embedding vector),这些向量只包含单词或标记的语义信息,缺乏位置信息。为了解决这个问题,Positional Encoding将每个输入向量加上了一个表示其位置的向量,从而保留单词/标记的语义信息的同时,提供位置信息

一句话概括,Positional Encoding就是将位置信息添加(嵌入)到Embedding词向量中,让Transformer保留词向量的位置信息,可以提高模型对序列的理解能力。

3. 位置编码

以往我们根据单词之间的间隔比例算距离,如果设置整个句子长度为1,如:Attention is all you need ,其中is和you之间的距离为0.5。而 To follow along you will first need to install PyTorch 较长文本中子里的0.5距离则会隔很多单词,这显然不合适。

所以,总结一下理想的位置编码应该满足:

  1. 为每个字输出唯一的编码
  2. 不同长度的句子之间,任何两个字之间的差值应该保持一致;
  3. 编码值应该是有界的。

4. Positional Encoding的特性

  1. 每个位置有一个唯一的Positional Encoding;
  2. 两个位置之间的关系可以通过它们位置间的仿射变换来建模(获得);

5. Positional Encoding原理解析

常用的Positional Encoding方法有Sinusoidal Positional EncodingLearned Positional Encoding。其中,Sinusoidal Positional Encoding 是通过将正弦和余弦函数的不同频率应用于输入序列的位置来计算位置编码;Learned Positional Encoding 是通过学习一组可学习参数来计算位置编码。本节以 Sinusoidal Positional Encoding 为例,介绍 Positional Encoding的原理。

Transformer论文中使用了三角函数来实现Positional Encoding,因为三角函数具有周期性,可以很好地表示序列中单词的相对位置。Transformer论文的作者使用了不同频率的正弦和余弦函数来作为位置编码:
{ P E ( p o s , 2 i ) = sin ⁡ ( p o s / 1000 0 2 i / d m o d e l ) P E ( pos , 2 i + 1 ) = cos ⁡ ( p o s / 1000 0 2 i / d m o d e l ) (1) \begin{equation} \begin{cases} PE(pos, 2i)=\sin \left(pos / 10000^{2 i / d_{model}}\right) \\ PE(\text {pos}, 2 i+1)=\cos \left(pos / 10000^{2 i / d_{model}}\right) \end{cases} \end{equation} \\ \tag{1} { PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)(1)
其中,pos即 position,表示token在序列中的位置,设句子长度为 L,则 p o s = 0 , 1 , … , L − 1 pos=0,1,\ldots,L-1 pos=0,1,,L1 P E PE PE 是token的位置向量, P E ( p o s , 2 i ) PE(pos, 2i) PE(pos,2i) 表示这个位置向量里的第i个元素; d m o d e l d_{model} dmodel 表示token的维度(通常为512), i i i 代表奇数维度, 2 i 2i 2i 表示偶数维度。

从公式中可以看出,其实一个词语的位置编码是由不同频率的余弦函数组成的,从低位到高位,余弦函数对应的频率由1降低到 1 10000 \frac{1}{10000} 100001,波长从 2 π 2\pi 2π 增加到 10000 ⋅ 2 π 10000\cdot2\pi 100002π

例如,第一个token就是0。2i2i+1 表示Positional Encoding的维度,i的取值范围是: [ 0 , … , d m o d e l / 2 ) [0,\ldots,d_{model}/2) [0,,dmodel/2),其中 d m o d e l d_{model} dmodel 为512。所以,当pos为1时,对应的Positional Encoding可以写成:
P E ( 1 ) = [ sin ⁡ ( 1 / 1000 0 0 / 512 ) , cos ⁡ ( 1 / 1000 0 0 / 512 ) , sin ⁡ ( 1 / 1000 0 2 / 512 ) , cos ⁡ ( 1 / 1000 0 2 / 512 ) , … b m a t r i x \begin{aligned} &\left.PE\left(1\right)=\left[\sin\left(1/10000^{0/512}\right.\right),\cos\left(1/10000^{0/512}\right),\sin\left(1/10000^{2/512}\right.\right),\cos \\ &\begin{pmatrix}1/10000^{2/512}\end{pmatrix},\ldots {bmatrix} \end{aligned} PE(1)=[sin(1/100000/512),cos(1/100000/512),sin(1/100002/512),cos(1/100002/512),bmatrix
借助上述公式,我们可以得到一个特定位置的 d m o d e l d_{model} dmodel 维的位置向量,并且借助三角函数的性质:
{ s i n ⁡ ( α + β ) = s i n ⁡ α c o s ⁡ β + c o s ⁡ α s i n ⁡ β c o s ⁡ ( α + β ) = c o s ⁡ α c o s ⁡ β − s i n ⁡ α s i n ⁡ β (2) \begin{cases} sin⁡(α+β)=sin⁡αcos⁡β+cos⁡αsin⁡β \\ cos⁡(α+β)=cos⁡αcos⁡β−sin⁡αsin⁡β \\ \end{cases} \tag{2} { sin(α+β)=sinαcosβ+cosαsinβcos(α+β)=cosαcosβsinαsinβ(2)
可以得到:
{ P E ( p o s + k , 2 i ) = P E ( p o s , 2 i ) × P E ( k , 2 i + 1 ) + P E ( p o s , 2 i + 1 ) × P E ( k , 2 i ) P E ( p o s + k , 2 i + 1 ) = P E ( p o s , 2 i + 1 ) × P E ( k , 2 i + 1 ) − P E ( p o s , 2 i ) × P E ( k , 2 i ) (3) \begin{cases} PE(pos + k,2i) = PE(pos,2i) \times PE(k,2i+1) + PE(pos, 2i+1) \times PE(k,2i) \\ PE(pos + k,2i+1) = PE(pos,2i+1) \times PE(k,2i+1) - PE(pos, 2i) \times PE(k,2i) \end{cases} \tag{3} { PE(pos+k,2i)=PE(pos,2i)×PE(k,2i+1)+PE(pos,2i+1)×PE(k,2i)PE(pos+k,2i+1)=PE(pos,2i+1)×PE(k,2i+1)PE(pos,2i)×PE(k,2i)(3)
可以看出,对于 p o s + k pos+k pos+k 位置的位置向量某一维 2 i 2i 2i 2 i + 1 2i+1 2i+1 而言,可以表示为: p o s pos pos 位置与 k k k 位置的位置向量的 2 i 2i 2i 2 i + 1 2i+1 2i+1 维的线性组合,这样的线性组合意味着位置向量中蕴含了相对位置信息。

BERT用了Transformer,但位置信息是训练出来的,没有用正弦余弦;正弦余弦是考虑到语言的语义和相对位置有关而与绝对位置关系不大,一句话放在文首还是文中还是文末,排除特殊情况后语义应该是差不多的。所以只要合理设计,用其他周期函数也可以。

6. 通俗理解Positional Encoding

最简单直观的加入位置信息的方式就是使用1,2,3,4,…直接对句子进行位置编码(one-hot)。用二进制转化举个例子:
在这里插入图片描述

上表中维度0,维度1,维度2,维度3拼成的数字就是该位置对应的二进制表示。可以看到每个维度(每一列)其实都是有周期的,并且周期是不同的。具体来说,每个比特位的变化率都是不一样的,越低位的变化越快(越往右边走,变化频率越快),红色位置0和1每个数字会变化一次,而黄色位,每8个数字才会变化一次。这样就能够说明使用多个周期不同的周期函数组成的多维度编码和递增序列编码其实是可以等价的。这也回答了为什么周期函数能够引入位置信息。

同样的道理,不同频率的sin正弦函数和cos余弦函数组合,通过调整三角函数的频率,可以实现这种低位到高位的变化,这样就能把位置信息表示出来。128维位置编码2D示意图,如下图所示:
在这里插入图片描述

7. 位置向量与词向量

一般来说,可以使用向量拼接或者相加的方式,将位置向量和词向量相结合。

input = input_embedding + positional_encoding

这里,input_embedding是通过常规Embedding层,将每一个token的向量维度从vocab_size映射到d_model。由于是相加关系,则positional_encoding也是一个d_model维度的向量。(原论文中,d_model=512)
在这里插入图片描述

8. Positional Encoding的代码实现

8.1 方式一

参考OpenNMT中的代码实现:onmt/modules/embeddings.py

class PositionalEncoding(nn.Module):

    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()       
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        #pe.requires_grad = False
        self.register_buffer('pe', pe)

    def forward(self, x):
        return x + self.pe[:x.size(0), :]

8.2 方式二

大语言模型核心技术-Transformer 详解

class PositionalEncoding(nn.Module):
    """
    compute sinusoid encoding.
    """
    def __init__(self, d_model, max_len, device):
        """
        constructor of sinusoid encoding class

        :param d_model: dimension of model
        :param max_len: max sequence length
        :param device: hardware device setting
        """
        super(PositionalEncoding, self).__init__()

        # same size with input matrix (for adding with input matrix)
        self.encoding = torch.zeros(max_len, d_model, device=device)
        self.encoding.requires_grad = False  # we don't need to compute gradient

        pos = torch.arange(0, max_len, device=device)
        pos = pos.float().unsqueeze(dim=1)
        # 1D => 2D unsqueeze to represent word's position

        _2i = torch.arange(0, d_model, step=2, device=device).float()
        # 'i' means index of d_model (e.g. embedding size = 50, 'i' = [0,50])
        # "step=2" means 'i' multiplied with two (same with 2 * i)

        self.encoding[:, 0::2] = torch.sin(pos / (10000 ** (_2i / d_model)))
        self.encoding[:, 1::2] = torch.cos(pos / (10000 ** (_2i / d_model)))
        # compute positional encoding to consider positional information of words

    def forward(self, x):
        # self.encoding
        # [max_len = 512, d_model = 512]

        batch_size, seq_len = x.size()
        # [batch_size = 128, seq_len = 30]

        return self.encoding[:seq_len, :]
        # [seq_len = 30, d_model = 512]
        # it will add with tok_emb : [128, 30, 512]         

class TokenEmbedding(nn.Embedding):
    """
    Token Embedding using torch.nn
    they will dense representation of word using weighted matrix
    """

    def __init__(self, vocab_size, d_model):
        """
        class for token embedding that included positional information
        :param vocab_size: size of vocabulary
        :param d_model: dimensions of model
        """
        super(TokenEmbedding, self).__init__(vocab_size, d_model, padding_idx=1)

class TransformerEmbedding(nn.Module):
    """
    token embedding + positional encoding (sinusoid)
    positional encoding can give positional information to network
    """

    def __init__(self, vocab_size, max_len, d_model, drop_prob, device):
        """
        class for word embedding that included positional information
        :param vocab_size: size of vocabulary
        :param d_model: dimensions of model
        """
        super(TransformerEmbedding, self).__init__()
        self.tok_emb = TokenEmbedding(vocab_size, d_model)
        self.pos_emb = PositionalEncoding(d_model, max_len, device)
        self.drop_out = nn.Dropout(p=drop_prob)

    def forward(self, x):
        tok_emb = self.tok_emb(x)
        pos_emb = self.pos_emb(x)
        return self.drop_out(tok_emb + pos_emb)

猜你喜欢

转载自blog.csdn.net/m0_37605642/article/details/132866365