位置编码

2022-05-10 09:24:29 浏览数 (1)

为什么需要位置编码

在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) = sinalphacosbeta cosalphasinbeta \ cos(alpha beta) = cosalphacosbeta-sinalphasinbeta

那么:

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} \ 因为:\ -sinalpha=sin-alpha, cosalpha=cos-alpha \ 所以:\ R_{-k2}=(R_{k2})^{T} \ 最终:\ R_{k1-k2}=R_{k1}(R_{k2})^{T} 或者 R_{k2-k1}=R_{k2}(R_{k1})^{T}

参考实现:

代码语言:javascript复制
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 旋转位置编码:

代码语言:javascript复制
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
 

0 人点赞