为什么需要位置编码
在transformer中使用了位置编码,为什么需要位置编码。因为对于transformer中的注意力机制而言,交换两个单词,并不会影响注意力的计算,也就是说这里的注意力是对单词位置不敏感的,而单词之间的位置信息往往是很重要的,因此考虑使用位置编码。
绝对位置编码
三角函数位置编码
transformer使用的位置编码。基本公式:
表示序列中第k个单词,2i及2i 1是其的两个分量,也就是说,第k个位置编码是由两部分构成的。假设句子长度为512,那么位置编码向量维度就是512×2。那么为什么会使用这种位置编码表示呢?首先三角函数有以下性质:
那么:
我们把
记为a,则有:
也就是说第m k个位置的位置编码可以由第m个位置表示。另有:
参考实现:
代码语言: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模型来学习一种绝对位置编码,比如从一个向量
出发,通过递归格式
来得到各个位置的编码向量。
ICML 2020的论文《Learning to Encode Position for Transformer with Continuous Dynamical Model》把这个思想推到了极致,它提出了用微分方程(ODE)
的方式来建模位置编码,该方案称之为FLOATER。显然,FLOATER也属于递归模型,函数
可以通过神经网络来建模,因此这种微分方程也称为神经微分方程,关于它的工作最近也逐渐多了起来。 理论上来说,基于递归模型的位置编码也具有比较好的外推性,同时它也比三角函数式的位置编码有更好的灵活性(比如容易证明三角函数式的位置编码就是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