原文地址
目录
1. 前言
2. Transformer总体架构
3. 各个技术细节
4. 总结
5. 参考资料
注意力机制的原理是计算query和每个key之间的相似性以获得注意力分配权重。在大部分NLP任务中,key一般也是value(basic Attention)。
注意力机制一般是用于提升seq2seq或者encoder-decoder架构的表现。但这篇2017 NIPS的文章Attention is all you need提出我们可以仅依赖注意力机制就可以完成很多任务, 即所谓自注意力机制. 与过去流行的使用基于RNN(LSTM,GRU)的Seq2Seq模型框架不同:
1. 文章中使用注意机制完全取代了RNN来构建整个模型框架.
2. Multi-Headed Attention Mechanism: 在编码器和解码器中使用 Multi-Headed self-attention。
3. 比LSTM更快的计算速度。
文章的实验数据暂且不论,其提出的核心想法很重要,就是LSTM这种时序模型(无法并行)速度实在是太慢了,很难应用到超大规模的数据和线上的应用中。Google因其自身业务的大规模性质,本身是很工程化的。所以他们非常反感需要繁琐特征提取的任务,所以他们早早就转投了神经网络。也非常不看好繁琐的神经网络,特别是那些无法并行化计算的。
近些年来,RNN(及其变种 LSTM, GRU)已成为很多nlp任务如机器翻译的经典网络结构。RNN从左到右或从右到左的方式顺序处理语言。RNN的按顺序处理的性质也使得其更难以充分利用现代快速计算设备,例如GPU等优于并行而非顺序处理的计算单元。虽然卷积神经网络(CNN)的时序性远小于RNN,但CNN体系结构如ByteNet或ConvS2S中,糅合远距离部分的信息所需的步骤数仍随着距离的增加而增长。
因为一次处理一个单词,RNN需要处理多个时序的单词来做出依赖于长远离单词的决定。但各种研究和实验逐渐表明,决策需要的步骤越多,循环网络就越难以学习如何做出这些决定。而本身LSTM就是为了解决long term dependency问题,但是解决得并不好。很多时候还需要额外加一层注意力层来处理long term dependency。
所以这次他们直接在编码器和解码器之间直接用attention,这样句子单词的依赖长度最多只有1,减少了信息传输路径。他们称之为Transformer。Transformer只执行一小段constant的步骤(根据经验选择)。在每个步骤中,应用self-attention机制,直接模拟句子中所有单词之间的关系,不管它们之间的位置如何。比如子“I arrived at the bank after crossing the river”,要确定“bank”一词是指河岸而不是金融机构,Transformer可以学会立即关注“river”这个词并在一步之内做出这个决定。
Transformer也是基于编码器(读取输入句子并生成其表达)-解码器(把新表达转换为目的词)的架构。具体地说,为了计算给定单词的下一个表示 - 例如“bank” - Transformer将其与句子中的所有其他单词进行比较。这些比较的结果就是其他单词的注意力权重。这些注意力权重决定了其他单词应该为“bank”的下一个表达做出多少贡献。在计算“bank”的新表示时,能够消除歧义的“river”可以获得更高的关注。将注意力权重用来加权平均所有单词的表达,然后将加权平均的表达喂给一个全连接网络以生成“bank”的新表达,以反映出该句子正在谈论的是“河岸”。
Transformer的编码阶段概括起来就是:
1. 首先为每个单词生成初始表达或embeddings。这些由空心圆表示。
2. 然后,对于每一个词, 使用自注意力聚合来自所有其他上下文单词的信息,生成参考了整个上下文的每个单词的新表达,由实心球表示。并基于前面生成的表达, 连续地构建新的表达(下一层的实心圆)对每个单词并行地重复多次这种处理。
解码器操作类似,只是从左到右依次生成一个字/词。它不仅关注先前生成的单词,而且还参考编码器生成的最终表示。
N = 6
实际超过6层。这些“层”中的每一个实际上由两层组成:position-wise FNN 和一个(编码器),或两个(解码器),基于注意力的子层。其中每个还包含4个线性投影和注意逻辑。
1. Stage1:输入编码: 序列的顺序信息是非常重要的。由于没有循环,也没有卷积,因此使用“位置编码”表示序列中每个标记的绝对(或相对)位置的信息。 positional encodings ⊕ embedded input
2. Stage 2 – Multi-head attention 和 Stage 3 – position-wise FFN. 两个阶段都是用来残差连接, 接着正则化输出层。
Stage1_out = Embedding512 + TokenPositionEncoding512
Stage2_out = layer_normalization(multihead_attention(Stage1_out)+Stage1_out)
Stage3_out = layer_normalization(FFN(Stage2_out)+Stage2_out)
out_enc = Stage3_out
上述是Transformer一个Encoder块的大体操作,实际上这种Encoder块可以堆叠多层,比如6层。
解码器的架构类似,但它在第2阶段采用了附加层, 在输出层上的 mask multi-head attention:
1. Stage 1 – 输入解码: 输入 output embedding,偏移一个位置以确保对位置i
的预测仅取决于< i
的位置。
2. Stage 2 - Masked Multi-head attention: 需要有一个mask来防止当前位置i
的生成任务看到后续> i
位置的信息
#使用pytorch版本的教程中提供的范例
#http://nlp.seas.harvard.edu/2018/04/03/attention.html
def subsequent_mask(size):
"Mask out subsequent positions."
attn_shape = (1,size,size)
subsequent_mask = np.triu(np.ones(attn_shape),k=1).astype('uint8')
return torch.from_numpy(subsequent_mask) == 0
# # The attention mask shows the position each tgt word (row) is allowed to look at (column).
# Words are blocked for attending to future words during training.
plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])
阶段2,3和4同样使用了残差连接,然后在输出使用归一化层。
Stage1_out = OutputEmbedding512 + TokenPositionEncoding512
Stage2_Mask = masked_multihead_attention(Stage1_out)
Stage2_Morm = layer_normalization(Stage2_Mask) + Stage1_out
Stage3_Multi = multihead_attention(Stage2_Norm+out_enc) + Stage2_Norm
Stage3_Norm = layer_normalization(Stage3_Multi) + Stage3_Multi
Stage4_FNN = FNN(Stage3_Norm)
Stage4_Norm = layer_normalization(Stage4_FNN) + Stage3_Norm
out_dec = Stage4_Norm
上述是Transformer一个Decoder块的大体操作,实际上这种Decoder块可以堆叠多层,比如6层。
可以利用开源的Tensor2Tensor,通过调用几个命令来训练Transformer网络进行翻译和解析。
可以看到,SA算法还有一个好处,处理共指消解(coreference resolution),例如句子中的单词“it”可以根据上下文引用句子的不同名词。
传统的Scaled Dot-Product Attention, 分数/权重由query和key的点积求得,权重再与value加权求和。点乘如果按照比例因子缩放, 可以计算得更快更省空间, 因为使用了优化过的矩阵乘法代码. (特别是对于较大的,点乘会数量级地放大, softmax可能会被推到梯度消失区域, 此时更推荐加缩放因子)
def attention(query, key,value,mask=None,dropout=0):
'Compute "Scaled Dot Product Attention" '
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2,-1))/math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask==0,-1e9)
p_attn = F.softmax(scores,dim=-1)
p_attn = F.dropout(p_attn,p=dropout)
return torch.matmul(p_attn,value),p_attn
Transformer将关联输入和输出序列中的(特别是远程)位置的计算量减少到O(1)。然而,这是以降低有效分辨率为代价的,因为注意力加权位置被平均了。为了弥补这种损失, 文章提出了 Multi-head Attention:
投影是参数矩阵
因为Transformer只是把原来维度的注意力函数计算并行分割为hh个独立的dmodel/hdmodel/h维度的head, 所以计算量相差不大.
class MultiHeadAttention(nn.Module):
def __init__(selfish,d_model,dropout=0.1):
"Take in model size and number of heads."
super(MultiHeadAttention,self).__init__()
assert d_model % h ==0
#假设 d_v = d_k
self.d_k = d_model // h
self.h = h
self.p = dropout
self.linears = clones(nn.Linear(d_model,d_model),4)
self.attn = None
def forward(self,query,key,value,mask=None):
if mask is not None:
#Same mask applied to all h heads.
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 1) Do all the linear projections in batch from d_model => h x d_k
query, key,value = [l(x).view(nbatches,-1,self.h,self.d_k).transpose(1,2) for lax in zip(self.linears,(query, key,value))]
#2) Apply attention on all the projected vectors in batch.
x, self.attn = attention(query, key,value,mask=mask,dropout=self.p)
#3) "Concat" using a view and apply a final linear.
x = x.transpose(1,2).contiguous().view(batches,-1,self.h*self.d_k)
return self.linears[-1](x)
Transformer以三种不同的方式使用Multi-head Attention:
1. 在解码器的encoder-decoder attention
层中,queries
来自前一层decoder层,并且 memory keys and values 来自encoder的输出。这让decoder的每个位置都可以注意到输入序列的所有位置。这其实还原了典型的seq2seq模型里常用的编码器 - 解码器注意力机制(例如Bahdanau et al., 2014或Conv2S2)。
2. 编码器本身也包含了self-attention layers。在self-attention layers中,所有 keys, values and queries 来自相同的位置,在这里是编码器中前一层的输出。这样,编码器的每个位置都可以注意到前一层的所有位置。
3. 类似地,解码器中的 self-attention layers 允许解码器的每个位置注意到解码器中包括该位置在内的所有前面的位置(有mask屏蔽了后面的位置)。需要阻止解码器中的向左信息流以保持自回归
属性(auto-regressive 可以简单理解为时序序列的特性, 只能从左到右, 从过去到未来)。我们通过在scaled dot-product attention层中屏蔽(设置为-∞)softmax输入中与非法连接相对应的所有值来维持该特性。
在编码器中,自注意力层处理来自相同位置的输入queries,keys,valuequeries,keys,value,即编码器前一层的输出。编码器中的每个位置都可以关注前一层的所有位置.
在解码器中,SA层使每个位置能够关注解码器中当前及之前的所有位置。为了保持 auto-regressive 属性,需要阻止解码器中的向左信息流, 所以要在scaled dot-product attention层中屏蔽(设置为-∞)softmax输入中与非法连接相对应的所有值.
作者使用SA层而不是CNN或RNN层的动机是:
1. 最小化每层的总计算复杂度: SA层通过O(1)数量的序列操作连接所有位置. (O(n)in RNN)
2. 最大化可并行化计算:对于序列长度n < representation dimensionality d(对于SOTA序列表达模型,如word-piece, byte-pair)。对于非常长的序列n>d, SA可以仅考虑以相应输出位置为中心的输入序列中的某个大小r的邻域,从而将最大路径长度增加到O(n/r)
3. 最小化由不同类型层组成的网络中任意两个输入和输出位置之间的最大路径长度。任何输入和输出序列中的位置组合之间的路径越短,越容易学习长距离依赖。
在编码器和解码器中,每个层都包含一个全连接的前馈网络(FFN),FFN 分别应用于每个位置,使用相同的两个线性变换和一个ReLU
虽然线性变换在不同位置上是相同的,但它们在层与层之间使用不同的参数。它的工作方式类似于两个内核大小为1的卷积层. 输入/输出维度是, 内层的维度.
class PositionwiseFeedForward(nn.Module):
'Implements FFN equation'
def __init__(self,d_model,d_ff,dropout=0.1):
super(PositionwiseFeedForward,self).__init__()
#Torch liners have a `b` by default
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))))
在解码时序信息时,LSTM模型通过让时间步的概念以输入/输出流一次一个的形式编码的. 而Transformer选择把时序编码为正弦波。这些信号作为额外的信息加入到输入和输出中以表达时间的流逝.
这种编码使模型能够感知到当前正在处理的是输入(或输出)序列的哪个部分。位置编码可以学习或者使用固定参数。作者进行了测试(PPL,BLEU),显示两种方式表现相似。文中作者选择使用固定的位置编码参数:
其中pos是位置,i是维度。
波长形成从2π到10000*2π的几何级数
也就是说,位置编码的每个维度对应于正弦曲线。波长形成从2π到10000⋅2π的几何级数。选择这个函数,是因为假设它能让模型容易地学习相对位置,因为对于任意固定偏移k,可以表示为PEpos的线性函数。
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[L,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)
位置编码将根据位置添加正弦波。每个维度的波的频率和偏移是不同的。
plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)
y = pe.forward(Variable(torch.zeros(1, 100, 20)))
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
plt.legend(["dim %d"%p for p in [4,5,6,7]])
与其他序列转导模型类似,使用可学习的Embeddings将 input tokens and output tokens 转换为维度l的向量。通过线性变换和softmax函数将解码器的输出向量转换为预测的token概率。在Transformer模型中,两个嵌入层和pre-softmax线性变换之间共享相同的权重矩阵,在Embeddings层中,将权重乘以 这些都是当前主流的操作。
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)
作者已经进行了一系列测试(论文表3),其中他们讨论N = 6层的建议,模型大小为512,基于h = 8个heads,键值维度为64,使用100K步。
还指出,由于模型质量随着(行B)的减小而降低,因此可以进一步优化点积兼容性功能
其声称提出的固定正弦位置编码,与学习到的位置编码相比,产生几乎相等的分数。
1. 序列转导Seq2Seq(机器翻译,语音翻译,ASR)
2. 语法选区解析的经典语言分析任务 syntactic constituency parsing
3. 共指消解 coreference resolution
https://research.googleblog.com/2017/08/transformer-novel-neural-network.html
https://research.googleblog.com/2017/06/accelerating-deep-learning-research.html
https://mchromiak.github.io/articles/2017/Sep/12/Transformer-Attention-is-all-you-need/
http://nlp.seas.harvard.edu/2018/04/03/attention.html