Fork me on GitHub

位置编码

为什么需要位置编码

在transformer中使用了位置编码,为什么需要位置编码。因为对于transformer中的注意力机制而言,交换两个单词,并不会影响注意力的计算,也就是说这里的注意力是对单词位置不敏感的,而单词之间的位置信息往往是很重要的,因此考虑使用位置编码。

绝对位置编码

三角函数位置编码

transformer使用的位置编码。基本公式:

\[p_{k,2i} = sin(\frac{k}{10000^{\frac{2i}{d}}}) \\ p_{k,2i+1} = cos(\frac{k}{10000^{\frac{2i}{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}] \\ 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}] \\ 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} \\ 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
旋转位置编码:

import math

import torch
import torch.nn as nn
import torch.nn.functional as F

context_outputs = torch.rand((32, 512, 768))
last_hidden_state = context_outputs  # 这里的context_outputs是bert的输出
# # last_hidden_state:[batch_size, seq_len, hidden_size]
batch_size = last_hidden_state.size()[0]
seq_len = last_hidden_state.size()[1]

hidden_size = 768
ent_type_size = 10
inner_dim = 64
# self.ent_type_size表示的是实体的总数, inner_dim自定义为64
# outputs:(batch_size, seq_len, ent_type_size*inner_dim*2)=[32, 512, 10*64*2]
outputs = nn.Linear(hidden_size, ent_type_size * inner_dim * 2)(last_hidden_state)
# 得到10个[32, 512, 64*2]
outputs = torch.split(outputs, inner_dim * 2, dim=-1)
# [32, 512, 10, 64*2]
outputs = torch.stack(outputs, dim=-2)
# qw和kw都是:[32, 512, 10, 64]
qw, kw = outputs[..., :inner_dim], outputs[..., inner_dim:]
"""这下面就是旋转位置编码主代码"""


def sinusoidal_position_embedding(batch_size, seq_len, output_dim):
    """这里是最初得正余弦位置编码"""
    # position_ids:[512, 1]
    position_ids = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(-1)
    # [32],从0-31
    indices = torch.arange(0, output_dim // 2, dtype=torch.float)
    # 10000^(-[0,...,31]/64)
    indices = torch.pow(10000, -2 * indices / output_dim)
    # [512, 32]
    embeddings = position_ids * indices
    # torch.Size([512, 32, 2])
    embeddings = torch.stack([torch.sin(embeddings), torch.cos(embeddings)], dim=-1)
    # [32, 512, 32, 2]
    embeddings = embeddings.repeat((batch_size, *([1] * len(embeddings.shape))))
    # [32, 512, 64]
    embeddings = torch.reshape(embeddings, (batch_size, seq_len, output_dim))
    return embeddings


pos_emb = sinusoidal_position_embedding(batch_size,
                                        seq_len,
                                        output_dim=inner_dim)
# 取奇数位,奇数位是cos
# repeat_interleave重复张量得元素
# torch.Size([32, 512, 1, 64])
cos_pos = pos_emb[..., None, 1::2].repeat_interleave(2, dim=-1)
# torch.Size([32, 512, 1, 64])
# 偶数位是sin
sin_pos = pos_emb[..., None,::2].repeat_interleave(2, dim=-1)

# torch.Size([32, 512, 10, 32, 2])
# 重新排列
qw2 = torch.stack([-qw[..., 1::2], qw[..., ::2]], -1)
# [32, 512, 10, 64]
qw2 = qw2.reshape(qw.shape)
# [32, 512, 10, 64] * [32, 512, 1, 64] + [32, 512, 10, 64] * [32, 512, 1, 64]
qw = qw * cos_pos + qw2 * sin_pos  # 这就是旋转位置编码得最终结果
kw2 = torch.stack([-kw[..., 1::2], kw[...,::2]], -1)
kw2 = kw2.reshape(kw.shape)
kw = kw * cos_pos + kw2 * sin_pos

参考

苏剑林-让研究人员绞尽脑汁的Transformer位置编码
三角函数位置编码实现
六种位置编码的代码实现及性能实验

posted @ 2022-04-24 17:35  西西嘛呦  阅读(1170)  评论(0编辑  收藏  举报