整个 transformer 的前向传播过程如下所示:
定义一个长度为 $N$ 的输入序列,$w_i$ 表示输入序列中第 $i$ 个 token,$x_i$ 表示第 $i$ 个 token $w_i$ 对应的 $d$ 维词嵌入向量,$q_m$ 表示第 $m$ 个 token 对应的词向量 $x_m$ 集成位置信息 $m$ 之后的 query 向量,而 $k_n$ 和 $v_n$ 则表示第 $n$ 个 token 对应的词向量 $x_n$ 集成位置信息 $n$ 之后的 key 和 value 向量,基于 transformer 的位置编码方法都是着重于构造一个合适的 $f(q,k,v)$ 函数形式。计算第 $m$ 个词嵌入向量 $x_m$ 对应的 self-attention 输出结果,就是 $q_m$ 和其他 $k_n$ 都计算一个 attention score,然后再将 attention score 乘以对应的 $v_n$ 再求和得到输出向量 $o_m$ :
$$ \begin{equation}\begin{split} q_m &= f_d(x_m, m) \\ k_n &= f_k(x_n, n) \\ v_n &= f_v(x_n, n) \\ a_{m,n} &= \frac{\text{exp}(\frac{q_m^{\intercal} k_n}{\sqrt{d}})}{\sum_{j=1}^N \text{exp}(\frac{q_m^{\intercal} k_j}{\sqrt{d}})} \\ o_m &= \sum_{n=1}^N a_{m,n} v_n \end{split}\end{equation} $$
原始的 transformer 和 bert 这两篇论文中所使用的位置编码都是 "相加" 的方式,即如下公式:
对于位置编码,常规的做法是在计算 query, key 和 value 向量之前,会计算一个位置编码向量 $p_i$ 加到词嵌入 $x_i$ 上,位置编码向量 $p_i$ 同样也是 $d$ 维向量,然后再乘以对应的变换矩阵 $W$:
$$ \begin{equation}f_{\{q,k,v\}}(x_i,i)=W_{\{q,k,v\}}(x_i+p_i)\end{equation} $$
不过 bert 中的位置编码是可训练的,transformer 中的编码是一种绝对位置编码,公式如下所示:
$$ \begin{equation}\begin{split} PE_{(pos, 2i)} &= sin(pos/10000^{2i/d_{model}}) \\ PE_{(pos, 2i+1)} &= cos(pos/10000^{2i/d_{model}}) \end{split}\end{equation} $$
绝对位置编码通过可学习的Positional Embedding来编码位置信息,这种方案直接对不同的位置随机初始化一个postion embedding,然后与word embedding相加后输入模型。postion embedding作为模型参数的一部分,在训练过程中进行更新。比如下面是绝对位置编码的实现:
# 初始化
self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
# 计算
position_embeddings = self.position_embeddings(position_ids)
embeddings += position_embeddings

绝对位置编码可视化
可以看到每个位置都会被区分开,并且相邻的位置比较接近。这个方案的问题也非常明显,不具备外推的性质。长度在预设定好之后就被固定了。
基于Sinusoidal的位置编码最初是由谷歌在论文Attention is All You Need中提出的方案,用于Transformer的位置编码。通过sin和cos函数将位置编码的取值固定在了[-1, 1]之前,能够为模型提供每个token的绝对位置信息,但是为什么不用线性函数?而且这里面的10000是怎么想到的?
谷歌在论文里面给出了解释: