位置编码
为什么需要位置编码
在transformer中使用了位置编码,为什么需要位置编码。因为对于transformer中的注意力机制而言,交换两个单词,并不会影响注意力的计算,也就是说这里的注意力是对单词位置不敏感的,而单词之间的位置信息往往是很重要的,因此考虑使用位置编码。
绝对位置编码
三角函数位置编码
transformer使用的位置编码。基本公式:
\[p_{k,2i} = sin(\frac{k}{10000^{\frac{2i}{d}}}) \\ p_{k,2i+1} = cos(\frac{k}{10000^{\frac{2i+1}{d}}}) \]\(p_{k}\)表示序列中第k个单词,2i及2i+1是其的两个分量,也就是说,第k个位置编码是由两部分构成的。假设句子长度为512,那么位置编码向量维度就是512×2。那么为什么会使用这种位置编码表示呢?首先三角函数有以下性质:
\[sin(\alpha+\beta) = sin\alpha\cos\beta+cos\alpha\sin\beta \\ cos(\alpha+\beta) = cos\alpha\cos\beta-sin\alpha\sin\beta \]那么:
\[p_{m}=[p_{m,2i},p_{m, 2i+1}] \\ p_{m}=[sin(\frac{m}{10000^{\frac{2i}{d}}}), cos(\frac{m}{10000^{\frac{2i}{d}}})] \\ p_{m+k}=[p_{m+k,2i},p_{m+k, 2i+1}] \\ p_{m+k}=[sin(\frac{m+k}{10000^{\frac{2i}{d}}}), cos(\frac{m+k}{10000^{\frac{2i}{d}}})] \\ \]我们把\(\frac{1}{10000^{\frac{2i}{d}}}\)记为a,则有:
\[p_{m+k} = [sinamcosak+cosamsinak, cosamcosak-sinamsinak] \\ p_{m+k}= \left[ \begin{matrix} cosak&sinak\\ -sinak&cosak\\ \end{matrix} \right] \left[ \begin{matrix} sinam \\ cosam \\ \end{matrix} \right] = \left[ \begin{matrix} cosak&sinak\\ -sinak&cosak\\ \end{matrix} \right]p_{m} \]也就是说第m+k个位置的位置编码可以由第m个位置表示。另有:
\[P_{t+k} = R_{k}P_{t+k} \\ P_{t+k1+k2} = R_{k1+k2}P_{t}=R_{k1}R_{k2}P_{t} \\ 则有:\\ R_{k1+k2} =R_{k1}R_{k2} \\ R_{k1-k2} =R_{k1}R_{-k2} \\ 因为:\\ -sin\alpha=sin-\alpha, cos\alpha=cos-\alpha \\ 所以:\\ R_{-k2}=(R_{k2})^{T} \\ 最终:\\ R_{k1-k2}=R_{k1}(R_{k2})^{T} 或者 R_{k2-k1}=R_{k2}(R_{k1})^{T} \]参考实现:
class PositionalEncoding(nn.Module):
"Implement the PE function."
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(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)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
return self.dropout(x)
可学习的位置编码
这一种位置编码就是Bert模型采用的。为什么bert不用transformer的三角函数编码,因为bert训练采用了更大的预料,使用可学习的位置编码效果可能更好。
递归式位置编码
这里摘录苏剑林的文章:
原则上来说,RNN模型不需要位置编码,它在结构上就自带了学习到位置信息的可能性(因为递归就意味着我们可以训练一个“数数”模型),因此,如果在输入后面先接一层RNN,然后再接Transformer,那么理论上就不需要加位置编码了。同理,我们也可以用RNN模型来学习一种绝对位置编码,比如从一个向量\(p_{0}\)出发,通过递归格式 \(p_{k+1}=f(p_{k})\) 来得到各个位置的编码向量。
ICML 2020的论文《Learning to Encode Position for Transformer with Continuous Dynamical Model》把这个思想推到了极致,它提出了用微分方程(ODE)\(dp_{t}/d_{t=h(p_{t},t)}\) 的方式来建模位置编码,该方案称之为FLOATER。显然,FLOATER也属于递归模型,函数\(h(p_{t},t)\)可以通过神经网络来建模,因此这种微分方程也称为神经微分方程,关于它的工作最近也逐渐多了起来。
理论上来说,基于递归模型的位置编码也具有比较好的外推性,同时它也比三角函数式的位置编码有更好的灵活性(比如容易证明三角函数式的位置编码就是FLOATER的某个特解)。但是很明显,递归形式的位置编码牺牲了一定的并行性,可能会带速度瓶颈。
相对位置编码
直接去看苏剑林的文章:https://zhuanlan.zhihu.com/p/352898810
参考
苏剑林-让研究人员绞尽脑汁的Transformer位置编码
三角函数位置编码实现
六种位置编码的代码实现及性能实验