Transformer position encoding 的细节

1. 序列处理的特点

在自然语言处理、语音识别和推荐系统当中,需要处理一类独特的数据: 序列

序列数据的特点,决定了序列处理的特性。在常见的序列处理当中,我们面对的序列主要有以下几个特点。

特点 1: 序列的长度是变化的,长短不一。

特点 2: 序列当中的 Token不是独立同分布的。序列当中每个 token 的所携带的信息不仅与自身有关,也和这个Token 所处的位置有关(也就是我们常说的上下文信息)。

这里以情感分析为例子

I like this movie because it doesn’t have an overhead history. Positive
I don’t like this movie because it has an overhead history. Negative

在上面的例子当中,仅仅只是因为don‘t 位置的不同,就导致了这两个句子的情感标注一个是正向的,一个是负向的。也就是说,在自然语言处理场景中,词汇之间的顺序关系对语意十分重要。词汇的含义可以由其上下文表示。或者说:

我们认为一个Token的含义不仅由其本身表示,还和这个Token所在的场景有关系。

在自然语言处理领域,我们 把Token所在的场景理解为上下文关系,而上下文关系通过词序来表示。可是对于其他场景而言,Token 所在的场景可能不仅仅是这个 Token 的上下文关系。例如推荐场景,可能还包括用户特征。

其实,如果回到真实的自然语言环境下。不同的人说出同一句话,可能表达的含义不同。也就是每个人其实都拥有自己的语言体系。 只是在自然语言处理里面,我们的数据集无法追溯用户和语言的场景,所以只能在大规模数据集下寻找通用的语言范式。

回到自然语言的场景下,如果只考虑序列之间的关系,问题就变成了: “需要通过什么方式来捕获序列当中 Token 之间的位置关系。”

特点 3: 序列的生成过程具备时序特性。也就是在一个序列当中,我们认为前面的 Token需要比后面的 Token 先被生成。

比如,在自然语言处理中生成一句话:

猫坐在帽子上。

这句话生成的时候,Token 的生成顺序是: [“猫”,“坐在”, “帽子”, “上”]。在生成这个序列的时候,生成 Token“猫”的时候,后面的Token是不被知道的。

为了满足上面所说的三个序列特点,在对序列进行建模的时候需要考虑以下几个问题:
(1) 如何处理长短不一的序列信息。
(2) 如何表示一个序列当中不同Token 之间的位置关系,也就是Token 和上下文之间的关系。
(3) 如何在生成过程中,防止序列信息的泄露。

2. 序列处理的方法

在 Transformer 出现以前,主流的处理序列的方法是 RNN 系列,以及 Attention。在这些方法里面也包括 LSTM,GRU 之类的优化结构。这里主要介绍一下 RNN和 Attention如何处理序列,以及在处理序列上的弊端。

2.1 RNN 对序列的处理

在介绍 RNN之前,我们首先来看一下自然语言处理当中的 Seq2Seq 模型架构。在 Seq2Seq模型架构当中,整个模型由两个部分组成:

  • Encoder 部分: 用来对输入的序列进行编码,得到最终的编码表示。
  • Decoder 部分: 对输入序列的编码表示做解码,等到预测的目标输出。

我们以中译英的翻译任务为例:

输入: 猫坐在帽子上。
输出: Cat sitting on hat.

Encoder 完成了对中文输入的编码得到 Context,Decoder 将编码后的序列解码成为英文。整体的结构图示如下:
请添加图片描述

在 RNN 的实现当中,模型结构通过循环传递的 hidden state 来传递序列之间的信息从而捕获输入序列的位置关系。

如下图所示,每个输入在计算输出的时候,会有一个上游的 Hidden state 传入。同时会有一个 Hidden state输出。通过 Hidden State 的传递来实现序列之间位置关系的传递。

请添加图片描述

具体的实现可以参考代码:

def rnn_cell(rnn_input,state):  
    with tf.variable_scope('rnn_cell',reuse=True):  
        W = tf.get_variable('W', [n_classes + state_size, state_size])  
        b = tf.get_variable('b', [state_size], initializer=tf.constant_initializer(0.0))  
  
    # 定义rnn_cell具体的操作,这里使用的是最简单的rnn,不是LSTM  
    return tf.tanh(tf.matmul(tf.concat((rnn_input,state),1),W)+b)  
  
  
state = init_state  
rnn_outputs = []  
  
#循环num_steps次,即将一个序列输入RNN模型  
for rnn_input in rnn_inputs:  
    state = rnn_cell(rnn_input,state)  
    rnn_outputs.append(state)  
final_state = rnn_outputs[-1]

虽然,在处理序列信息方面,RNN 及其变种取得了一些进展,但是仍然存在以下一些问题:
问题 1: 由于 Hidden State一直在更新,如果循环更新的过程会丢失掉前面序列的信息,难以处理长序列。
问题 2: Hidden State 在网络中循环使用,导致梯度消失。
问题 3: 每个计算依赖于上一个 Hidden state 的输出,序列的每个 Token 需要串行计算。

2.2 Attention 对序列的处理

为了解决 RNN 在长序列上的缺陷,在 RNN 的基础上引入了 Attention 的机制。

可是,随着序列长度的增长,通过定长的 Hidden State向量表征整个序列的能力是有限的。此外,处理序列的时候,对不同位置的 Decoder对需要关注输入位置的重点也不一样。例如,在翻译任务中,翻译"I hate you"的时候,Decoder 翻译”我“和翻译”恨“注意力集中的位置可能不一样。

为了解决 RNN 对长序列处理的缺陷。在 RNN 的基础上,通过引入 Attention 机制来解决Decoder 过程的 Hidden State 权重分布问题。具体如下图所示:

请添加图片描述

对于一个输入序列 [ x 1 , x 2 , x 3 , . . . , x T ] [x_1, x_2, x_3, ... ,x_T] [x1,x2,x3,...,xT], 在Encoder 阶段生成了不同的Hidden State ,组成 Hidden State 序列表示为 [ h 1 , h 2 , h 3 , . . . , h T ] [h_1, h_2, h_3,...,h_T] [h1,h2,h3,...,hT]

在Decoder 阶段,如果需要计算 t t t 位置的结果 y t y_t yt ,RNN 结构是直接使用 Hidden state s t − 1 s_{t-1} st1 做解码操作,得到最终的输出结果 s t s_t st

为了分别表示不同位置的 Hidden state 对解码出来的结果 s t s_t st 的影响,通过 Attention 机制计算输入 s t − 1 s_{t-1} st1 对不同位置的Hidden state 的权重, 表示为权重向量 e t e_t et ,然后通过 e t e_t et 对Hidden State 进行加权求和得到上下文编码向量 c t c_t ct,通过 Decoder 对 c t c_t ct进行解码操作,而不是直接使用 s t − 1 s_{t-1} st1 的输出结果。

上述流程的形式化描述具体如下:

  • 假设,当前 Decoder 需要解码 生成 t t t 位置的 Token,上一个 Token 解码后对应的输出为 s t − 1 s_{t-1} st1 y t − 1 y_{t-1} yt1,Encoder 阶段得到的 Hidden State 序列为 [ h 1 , h 2 , h 3 , . . . , h T ] [h_1, h_2, h_3,...,h_T] [h1,h2,h3,...,hT],我们需要计算当前位置的Token y t y_t yt

  • [ h 1 , h 2 , h 3 , . . . , h T ] [h_1, h_2, h_3,...,h_T] [h1,h2,h3,...,hT] 里面的每个 Hidden State h j h_j hj, 计算当前的 s t − 1 s_{t-1} st1和这个 Hidden State h j h_j hj 的相关性 e t j = a ( s t − 1 , h j ) e_{tj} = a(s_{t-1}, h_j) etj=a(st1,hj)。 其中 a a a 表示一种相关性计算方式。例如:内积。

  • 这样我们就可以得到一个向量表示 s t − 1 s_{t-1} st1 和每一个 Encoder Hidden State 的相关性。整个相关性的向量表示为 [ a ( s t − 1 , h 1 ) , a ( s t − 1 , h 2 ) , a ( s t − 1 , h 3 ) , a ( s t − 1 , h T ) ] [a(s_{t-1}, h_1), a(s_{t-1}, h_2), a(s_{t-1}, h_3), a(s_{t-1}, h_T)] [a(st1,h1),a(st1,h2),a(st1,h3),a(st1,hT)]

  • 通过 softmax 对权重向量做 normalize,得到最终的权重向量 α t {\alpha}_t αt

  • 通过权重向量对 Hidden State 进行加权求和,得到最终的 context 向量 c t = ∑ j = 1 T α t j h j c_t=\sum_{j=1}^T {\alpha}_{tj} h_j ct=j=1Tαtjhj

  • 通过 context 向量 c t c_t ct 解码计算得到最终的输出 y t y_t yt

3. Position Encoding的引入

在 Transformer 的结构里面,为了让 Decoder能够并行处理输入序列,放弃了 RNN + Attention 的方式,只保留了 Attention 的结构。

在原有的 RNN + Attention结构中,Source(也就是 Q , K , V Q,K,V Q,K,V 当中 的 < K , V > <K,V> <K,V> pair 对)是 RNN 按照顺序生成的 Hidden State。在Transformer 里面,直接使用 Attention 处理输入序列。

可是,Attention 的结构本质上是将输入序列当成一个词包,词包模型无法处理序列关系。

对于 Attention 结构来说,词汇的位置变化之后,通过 Attention 生成的Embedding 不会发生任何变化。在同一个序列中,位置调换并不会导致序列结果的变化。在Transformer 这种新的结构当中,RNN 原本可以表示序列关系的Hidden State 被丢弃了。所以,Transformer 的新结构如何表示词汇序列之间的位置关系成了一个需要被解决的问题。

在 Transformer 当中,引入了 三角函数的Position encoding 来解决位置编码的问题,表示词汇之间的位置关系。

在词汇学习中为了学习到这种相对的位置,可以通过周期函数来实现。在 Transformer 使用三角函数来表示相对位置的信息,具体的编码方式如下:
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 ) \begin{align} & PE(pos, 2i) = sin(pos / 10000 ^ {2i/d_{model}}) \\ & PE(pos, 2i+1) = cos(pos / 10000 ^ {2i/d_{model}}) \\ \end{align} PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)

Position encoding 的具体编码方式如下:

def get_angles(pos, i, d_model):  
    angle_rates = 1 / np.power(10000, (2 * (i // 2)) / np.float32(d_model))  
    return pos * angle_rates  
  
  
def positional_encoding(position, d_model):  
    angle_rads = get_angles(np.arange(position)[:, np.newaxis],  
                            np.arange(d_model)[np.newaxis, :],  
                            d_model)  
  
    # 将 sin 应用于数组中的偶数索引(indices);2i  
    angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])  
  
    # 将 cos 应用于数组中的奇数索引;2i+1  
    angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])  
  
    pos_encoding = angle_rads[np.newaxis, ...]  
  
    return tf.cast(pos_encoding, dtype=tf.float32)

4. 为什么这样做 Position Encoding

以往生成词汇位置编码的方式都是采用学习词汇位置编码的方式,将词序信息加入到模型当中去,随机初始化之后让模型学习。比如论文《 Convolutional Sequence to Sequence Learning》

但是采用学习 position encoding 的方式有一个弊端,在模型训练的时候我们需要构建一个 Position Embedding Table,这个 Position Embedding Table 必须要有固定的大小。因此在模型训练的过程中只能处理固定长度的序列。

综合来看,结合第一小节当中序列的特性,在选择Position Encoding 的方式的时候需要满足以下几个特点:

  1. position encoding 能够表示一个 token 在序列当中的绝对位置
  2. position encoding 能够表示两个词汇在序列当中的相对位置。
  3. 可以用来表示模型训练过程当中从来没有看到过的句子长度。
  4. 函数需要连续并且有界。

下图,我们绘制了不同 Position 512 维的编码

请添加图片描述

可以看到随着 token pos 的增加,函数的周期在减小。

根据三角函数计算的性质:
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 β \begin{align} & sin(\alpha, \beta) = sin\alpha cos\beta + cos\alpha sin\beta \\ & cos(\alpha, \beta) = cos\alpha cos\beta + sin\alpha sin\beta \\ \end{align} sin(α,β)=sinαcosβ+cosαsinβcos(α,β)=cosαcosβ+sinαsinβ
这个时候对于,token 的索引位置为 p o s + k pos + k pos+k的时候,其位置编码是:
P E ( p o s + k , 2 i ) = s i n ( ( p o s + k ) / 1000 0 2 i / d m o d e l ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) c o s ( k / 1000 0 2 i / d m o d e l ) + c o s ( p o s / 1000 0 2 i / d m o d e l ) s i n ( k / 1000 0 2 i / d m o d e l ) = 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 ( p o s , 2 i + 1 ) − P E ( k , 2 i ) × P E ( k , 2 i ) \begin{align} & PE(pos + k, 2i) = sin((pos+k) / 10000 ^{2i / d_{model}}) \\ & = sin(pos / 10000 ^{2i/d_{model}})cos(k / 10000 ^{2i/d_{model}}) + cos(pos / 10000 ^{2i/d_{model}})sin(k / 10000 ^{2i/d_{model}}) \\ & = 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(pos, 2i+1) - PE(k, 2i)\times PE(k, 2i) \end{align} PE(pos+k,2i)=sin((pos+k)/100002i/dmodel)=sin(pos/100002i/dmodel)cos(k/100002i/dmodel)+cos(pos/100002i/dmodel)sin(k/100002i/dmodel)=PE(pos,2i)×PE(k,2i+1)+PE(pos,2i+1)×PE(k,2i)PE(pos+k,2i+1)=PE(pos,2i+1)×PE(pos,2i+1)PE(k2i)×PE(k,2i)

从上面的encoding 的方式可以发现:

  1. 三角函数在维度上的频率解决了绝对位置的编码
  2. 通过三角函数在维度上的周期性解决了相对位置的编码
  3. 通过三角函数的连续和有界保证在编码值在 0~1 之间。
  • 为什么超参选用了一个 10000 的值?

我们将超参设置为一个变量 n n n,分别考虑不同情况下 n = 10000 n=10000 n=10000, n = 100 n=100 n=100
n = 20 n=20 n=20 以及 n = 1 n=1 n=1的情况下,一个长度为 100的序列编码为 512 维的position encoding。不同的 n n n 对应不同情况下的 position encoding 的情况分别如下:

n = 10000 n=10000 n=10000
请添加图片描述
n = 100 n=100 n=100
请添加图片描述
n = 20 n=20 n=20
请添加图片描述
n = 1 n=1 n=1
请添加图片描述

在此基础上,统计序列第一个位置的 position encoding 和其他位置的 position encoding的距离,绘制成如下图。

请添加图片描述

上图中,橙色的线条表示 n = 10000 n=10000 n=10000 蓝色的线条表示 n = 20 n=20 n=20,绿色的线条表示 n = 1 n=1 n=1。根据上面的结果可知,随着 n n n的增大。position encoding 可表示的空间会增大,但是 Position Embedding 能够表示的不同位置的差异性在减小。将 n n n 设置为 10000 能够减少位置编码的重复。

这样可以理解在Transformer 当中使用 n = 10000 n=10000 n=10000 可能是因为这种情况下已经覆盖到了大量的序列长度。当某个序列长度 n > 10000 n>10000 n>10000 的时候,依旧能够对位置进行编码,但是位置编码可能不再具备唯一性。

5 参考文献

[1] Transformer Architecture: The Positional Encoding
[2] 如何理解Transformer论文中的positional encoding,和三角函数有什么关系?
[3] Why is 10000 used as the denominator in Positional Encodings in the Transformer Model?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值