《Attention Is All You Need》
6 Dec 2017
Transformer开山之作
https://github.com/tensorflow/tensor2tensor;
主要的序列转导模型基于包括编码器和解码器的复杂RNN和CNN。性能最好的模型还通过注意力机制连接编码器和解码器。我们提出了一种新的简单网络架构,即Transformer,它完全基于注意力机制,完全不用递归和卷积。在两个机器翻译任务上的实验表明,这些模型在质量上更优,同时更易于并行化,并且需要的训练时间明显更少。
RNN,固有的顺序性使得不能并行化,在较长的序列长度问题中,限制模型,效率低下;
注意力机制已成为各种任务序列建模和转导模型的一个重要组成部分,它允许对依赖关系进行建模,而不考虑输入或输出序列中的距离。然而,在除少数情况外,在所有情况下,这种注意力机制都与RNN结合使用。
在这项工作中提出了Transformer,这是一种避免重复的模型架构,是完全依赖于注意力机制来绘制输入和输出之间的全局依赖关系。
关联来自两个任意输入或输出位置的信号所需的操作数量随着位置之间的距离而增加。在Transformer中,这被减少到恒定数量的操作,尽管代价是由于平均注意力加权位置而降低了有效分辨率,如第3.2节所述,本文通过多头注意力抵消了这种影响。
自注意力机制,有时称为内部注意力,是一种将单个序列的不同位置联系起来以计算序列表示的注意机制。
端到端记忆网络基于循环注意力机制,而不是序列设计的循环,已证明在简单的语言问答和语言建模任务中表现良好。
大多数竞争性神经序列转导模型具有编码器-解码器结构。
编码器将符号表示的输入序列 ( x 1 , … , x n ) (x_1,…,x_n) (x1,…,xn)映射到连续表示的序列 z = ( z 1 , … , z n ) z=(z_1,…,z_n) z=(z1,…,zn)。
给定z,解码器一次生成一个元素的符号输出序列 ( y 1 , … , y m ) (y_1,…,y_m) (y1,…,ym)。
常规编码器-解码器架构(上图)
Transformer遵循这种整体架构,为编码器和解码器使用堆叠的自关注和逐点、完全连接的层,分别如图1的左半部分和右半部分所示。
图1:Transformer模型架构
注:Transformer是可以并行的,会忽略前后顺序。因此,此时需要位置编码解决这个问题。
代码:
class EncoderLayer(nn.Module):
"EncoderLayer由self-attn和feed forward组成"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
# 多头自注意力机制
self.self_attn = self_attn
# 前馈网络
self.feed_forward = feed_forward
# 封装LayerNorm + sublayer(Self-Attenion/Dense) + dropout + 残差连接
self.sublayer = clones(SublayerConnection(size, dropout), 2)
# 特征大小
self.size = size
def forward(self, x, mask):
"Follow Figure 1 (left) for connections."
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
return self.sublayer[1](x, self.feed_forward)
class Encoder(nn.Module):
"Encoder是N个EncoderLayer的stack"
def __init__(self, layer, N):
super(Encoder, self).__init__()
# layer是一个SubLayer,我们clone N个
self.layers = clones(layer, N)
# 再加一个LayerNorm层
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
"逐层进行处理"
for layer in self.layers:
x = layer(x, mask)
# 最后进行LayerNorm,后面会解释为什么最后还有一个LayerNorm。
return self.norm(x)
class DecoderLayer(nn.Module):
"Decoder包括self-attn, src-attn, 和feed forward "
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
# 多头自注意力机制
self.self_attn = self_attn
# masked
self.src_attn = src_attn
# FFN
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
m = memory
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
return self.sublayer[2](x, self.feed_forward)
class Decoder(nn.Module):
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
self.norm = LayerNorm(layer.size)
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
一般只有0和1,代表遮掩或者不遮掩;
在transformer中,掩码主要的作用有两个:
Encoder中的掩码主要是起到第一个作用,Decoder中的掩码则同时发挥着两种作用。
屏蔽掉无效的padding区域:我们训练需要分batch进行,以机器翻译任务为例,一个batch中不同样本的输入长度很可能是不一样的,此时我们要设置一个最大句子长度,然后对空白区域进行padding填充,而填充的区域无论在Encoder还是Decoder的计算中都是没有意义的,因此需要用mask进行标识,屏蔽掉对应区域的响应。
屏蔽掉来自未来的信息:我们已经学习了attention的计算流程(后文),它是会综合所有时间步的计算的,那么在解码的时候,就有可能获取到未来的信息,这是不行的。因此,这种情况也需要我们使用mask进行屏蔽。
def subsequent_mask(size):
#生成向后遮掩的掩码张量,参数size是掩码张量最后两个维度的大小,它最后两维形成一个方阵
"Mask out subsequent positions."
attn_shape = (1, size, size)
#然后使用np.ones方法向这个形状中添加1元素,形成上三角阵
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
#最后将numpy类型转化为torch中的tensor,内部做一个1- 的操作。这个其实是做了一个三角阵的反转,subsequent_mask中的每个元素都会被1减。
#如果是0,subsequent_mask中的该位置由0变成1
#如果是1,subsequect_mask中的该位置由1变成0
return torch.from_numpy(subsequent_mask) == 0
Attention(包括Self-Attention和普通的Attention)可以看成一个函数,它的输入是Query,Key,Value和Mask,输出是一个Tensor。其中输出是Value的加权平均,而权重来自Query和Key的计算。
【Q/K/V:注意力机制】
【Q=K=V:自注意力机制】
图2:(左)Scaled Dot-Product attention。(右)多头注意力由几个平行的注意力层组成。
输入由维度 d k d_k dk的Q和K以及维度 d v d_v dv的V组成。
计算Q和K的点积,每个K除以 √ d k √dk √dk,并应用softmax函数以获得值上的权重。
# 图2左
def attention(query, key, value, mask=None, dropout=None):
# 词嵌入的维度
d_k = query.size(-1)
# Q*K/sqrt(d_k)
# 得到注意力得分
scores = torch.matmul(query, key.transpose(-2, -1)) \
/ math.sqrt(d_k)
if mask is not None:
# 用于把mask是0的变成一个很小的数
# 这样后面经过softmax之后的概率就很接近零 不会被选择
scores = scores.masked_fill(mask == 0, -1e9)
# 得到最终的注意力张量
p_attn = F.softmax(scores, dim = -1)
# 是否随机置0
if dropout is not None:
p_attn = dropout(p_attn)
# 矩阵乘法
return torch.matmul(p_attn, value), p_attn
大白话翻译一下:每个头获得一组Q,K,V进行注意力机制的计算(每个头开始从词义层面分割输出的张量),但是句子中的每个词的表示只获得了一部分,也就是只分割了最后一维的词嵌入向量(分成8份 h = 8),然后讲每个头的结果送入注意力机制中,形成多头注意力机制;
作用:多头注意力允许模型在不同位置共同关注来自不同表示子空间的信息。对于一个注意力集中的人,平均值会抑制这种情况。
翻译一下:多头注意力机制去优化每个词的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达(本文h=8,即每个头负责1/8);实验表明这种机制可以提升性能;
线性变换参数为 W i Q ∈ R d m o d e l × d k W_i^Q∈R^{d_{model}×d_k} WiQ∈Rdmodel×dk , W i K ∈ R d m o d e l × d k W_i^K∈R^{d_{model}×d_k} WiK∈Rdmodel×dk , W i V ∈ R d m o d e l × d v W_i^V∈R^{d_{model}×d_v} WiV∈Rdmodel×dv , 第4步的线性变化参数为 W O ∈ R h d v × d m o d e l W^O∈R^{hd_v×d_{model}} WO∈Rhdv×dmodel 。而第三步计算的次数是 h 。
本文: d m o d e l = 512 , h = 8 d_{model}=512,h=8 dmodel=512,h=8;
# 图2右
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super(MultiHeadedAttention, self).__init__()
# 如果它的条件返回错误,则终止程序执行
assert d_model % h == 0
# We assume d_v always equals d_k
# 512 // 8 = 64
# 每个头分配的维度
self.d_k = d_model // h
self.h = h
# 获取线性层对象,
# 内部变换矩阵 d_model * d_model(一定是个方阵)
# 为什么是4个?
# QKV各需要一个,最后拼接的矩阵还需要一个;
self.linears = clones(nn.Linear(d_model, d_model), 4)
# 初始化为None 代表最后得到的注意力张量
# 现在还没有 所以为None
self.attn = None
# 置0比率
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
if mask is not None:
# 所有h个head的mask都是相同的
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 核心
# .view 划分为4个维度
# 1) 首先使用线性变换,然后把d_model分配给h个Head,每个head为d_k=d_model/h
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2) for l, x in zip(self.linears, (query, key, value))]
# 2) 使用attention函数计算
x, self.attn = attention(query, key, value, mask=mask,
dropout=self.dropout)
# 3) 把8个head的64维向量拼接成一个512的向量。然后再使用一个线性变换(512,521),shape不变。
x = x.transpose(1, 2).contiguous() \
.view(nbatches, -1, self.h * self.d_k)
return self.linears[-1](x)
在Transformer中,前馈全连接层就是具有两层线性的全连接网络;
考虑注意力机制可能对复杂过程的拟合程度不够,通过增加两层网络来增强模型的能力;
注:RELU (x) = max(0,x)
虽然线性变换在不同位置上是相同的,但它们在不同层之间使用不同的参数。另一种描述方式是两个内核大小为1的卷积。
输入和输出的维度为 d m o d e l = 512 d_{model}=512 dmodel=512,内层的维度 d f f = 2048 d_{f f}=2048 dff=2048。
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(F.relu(self.w_1(x))))
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model
def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)
class Generator(nn.Module):
# 根据Decoder的隐状态输出一个词
# d_model是Decoder输出的大小,vocab是词典大小
def __init__(self, d_model, vocab):
super(Generator, self).__init__()
self.proj = nn.Linear(d_model, vocab)
# 全连接再加上一个softmax
def forward(self, x):
return F.log_softmax(self.proj(x), dim=-1)
class PositionalEncoding(nn.Module):
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)