一步步解析Attention is All You Need
本文將通過細節剖析以及代碼相結合的方式,來一步步解析Attention is all you need這篇文章。
這篇文章的下載地址為:https://arxiv.org/abs/1706.03762
本文的部分圖片來自文章:https://mp.weixin.qq.com/s/RLxWevVWHXgX-UcoxDS70w,寫的非常好!
本文邊講細節邊配合代碼實戰,代碼地址為:https://github.com/princewen/tensorflow_practice/tree/master/basic/Basic-Transformer-Demo
數據地址為:https://pan.baidu.com/s/14XfprCqjmBKde9NmNZeCNg 密碼:lfwu
好了,廢話不多說,我們進入正題!我們從簡單到復雜,一步步介紹該模型的結構!
1、整體架構
模型的整體框架如下:
整體架構看似復雜,其實就是一個Seq2Seq結構,簡化一下,就是這樣的:
Encoder的輸出和decoder的結合如下,即最后一個encoder的輸出將和每一層的decoder進行結合:
好了,我們主要關注的是每一層Encoder和每一層Decoder的內部結構。如下圖所示:
可以看到,Encoder的每一層有兩個操作,分別是Self-Attention和Feed Forward;而Decoder的每一層有三個操作,分別是Self-Attention、Encoder-Decoder Attention以及Feed Forward操作。這里的Self-Attention和Encoder-Decoder Attention都是用的是Multi-Head Attention機制,這也是我們本文重點講解的地方。
在介紹之前,我們先介紹下我們的數據,經過處理之后,數據如下:
很簡單,上面部分是我們的x,也就是encoder的輸入,下面部分是y,也就是decoder的輸入,這是一個機器翻譯的數據,x中的每一個id代表一個語言中的單詞id,y中的每一個id代表另一種語言中的單詞id。后面為0的部分是填充部分,代表這個句子的長度沒有達到我們設置的最大長度,進行補齊。
2、Position Embedding
給定我們的輸入數據,我們首先要轉換成對應的embedding,由于我們后面要在計算attention時屏蔽掉填充的部分,所以這里我們對于填充的部分的embedding直接賦予0值。Embedding的函數如下:
def embedding(inputs,vocab_size,num_units,zero_pad=True,scale=True,scope="embedding",reuse=None):with tf.variable_scope(scope, reuse=reuse):lookup_table = tf.get_variable('lookup_table',dtype=tf.float32,shape=[vocab_size, num_units],initializer=tf.contrib.layers.xavier_initializer())if zero_pad:lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),lookup_table[1:, :]), 0)outputs = tf.nn.embedding_lookup(lookup_table, inputs)if scale:outputs = outputs * (num_units ** 0.5)return outputs在本文中,Embedding操作不是普通的Embedding而是加入了位置信息的Embedding,我們稱之為Position Embedding。因為在本文的模型中,已經沒有了循環神經網絡這樣的結構,因此序列信息已經無法捕捉。但是序列信息非常重要,代表著全局的結構,因此必須將序列的分詞相對或者絕對position信息利用起來。位置信息的計算公式如下
其中pos代表的是第幾個詞,i代表embedding中的第幾維。這部分的代碼如下,對于padding的部分,我們還是使用全0處理。
?
def positional_encoding(inputs,num_units,zero_pad = True,scale = True,scope = "positional_encoding",reuse=None):N,T = inputs.get_shape().as_list()with tf.variable_scope(scope,reuse=True):position_ind = tf.tile(tf.expand_dims(tf.range(T),0),[N,1])position_enc = np.array([[pos / np.power(10000, 2.*i / num_units) for i in range(num_units)]for pos in range(T)])position_enc[:,0::2] = np.sin(position_enc[:,0::2]) # dim 2iposition_enc[:,1::2] = np.cos(position_enc[:,1::2]) # dim 2i+1lookup_table = tf.convert_to_tensor(position_enc)if zero_pad:lookup_table = tf.concat((tf.zeros(shape=[1,num_units]),lookup_table[1:,:]),0)outputs = tf.nn.embedding_lookup(lookup_table,position_ind)if scale:outputs = outputs * num_units ** 0.5return outputs所以對于輸入,我們調用上面兩個函數,并將結果相加就能得到最終Position Embedding的結果:
self.enc = embedding(self.x,vocab_size=len(de2idx),num_units = hp.hidden_units,zero_pad=True, # 讓padding一直是0scale=True,scope="enc_embed") self.enc += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.x)[1]),0),[tf.shape(self.x)[0],1]),vocab_size = hp.maxlen,num_units = hp.hidden_units,zero_pad = False,scale = False,scope = "enc_pe")3、Multi-Head Attention
3.1 Attention簡單回顧
Attention其實就是計算一種相關程度,看下面的例子:
Attention通常可以進行如下描述,表示為將query(Q)和key-value pairs映射到輸出上,其中query、每個key、每個value都是向量,輸出是V中所有values的加權,其中權重是由Query和每個key計算出來的,計算方法分為三步:
1)計算比較Q和K的相似度,用f來表示:
2)將得到的相似度進行softmax歸一化:
3)針對計算出來的權重,對所有的values進行加權求和,得到Attention向量:
計算相似度的方法有以下4種:
在本文中,我們計算相似度的方式是第一種,本文提出的Attention機制稱為Multi-Head Attention,不過在這之前,我們要先介紹它的簡單版本 Scaled Dot-Product Attention。
計算Attention首先要有query,key和value。我們前面提到了,Encoder的attention是self-attention,Decoder里面的attention首先是self-attention,然后是encoder-decoder attention。這里的兩種attention是針對query和key-value來說的,對于self-attention來說,計算得到query和key-value的過程都是使用的同樣的輸入,因為要算自己跟自己的attention嘛;而對encoder-decoder attention來說,query的計算使用的是decoder的輸入,而key-value的計算使用的是encoder的輸出,因為我們要計算decoder的輸入跟encoder里面每一個的相似度嘛。
因此本文下面對于attention的講解,都是基于self-attention來說的,如果是encoder-decoder attention,只要改一下輸入即可,其余過程都是一樣的。
3.2 Scaled Dot-Product Attention
Scaled Dot-Product Attention的圖示如下:
接下來,我們對上述過程進行一步步的拆解:
First Step-得到embedding
給定我們的輸入數據,我們首先要轉換成對應的position embedding,效果圖如下,綠色部分代表填充部分,全0值:
得到Embedding的過程我們上文中已經介紹過了,這里不再贅述。
Second Step-得到Q,K,V
計算Attention首先要有Query,Key和Value,我們通過一個線性變換來得到三者。我們的輸入是position embedding,過程如下:
代碼也很簡單,下面的代碼中,如果是self-attention的話,query和key-value輸入的embedding是一樣的。padding的部分由于都是0,結果中該部分還是0,所以仍然用綠色表示
# Linear projection Q = tf.layers.dense(queries,num_units,activation=tf.nn.relu) # K = tf.layers.dense(keys,num_units,activation=tf.nn.relu) # V = tf.layers.dense(keys,num_units,activation=tf.nn.relu) #Third-Step-計算相似度
接下來就是計算相似度了,我們之前說過了,本文中使用的是點乘的方式,所以將Q和K進行點乘即可,過程如下:
文中對于相似度還除以了dk的平方根,這里dk是key的embedding長度。
這一部分的代碼如下:
outputs = tf.matmul(Q,tf.transpose(K,[0,2,1])) outputs = outputs / (K.get_shape().as_list()[-1] ** 0.5)你可能注意到了,這樣做其實是得到了一個注意力的矩陣,每一行都是一個query和所有key的相似性,對self-attention來說,其效果如下:
不過我們還沒有進行softmax歸一化操作,因為我們還需要進行一些處理。
Forth-Step-增加mask
剛剛得到的注意力矩陣,我們還需要做一下處理,主要有:
我們首先對key中填充的部分進行屏蔽,我們之前介紹了,在進行embedding時,填充的部分的embedding 直接設置為全0,所以我們直接根據這個來進行屏蔽,即對embedding的向量所有維度相加得到一個標量,如果標量是0,那就代表是填充的部分,否則不是:
這部分的代碼如下:
key_masks = tf.sign(tf.abs(tf.reduce_sum(keys,axis=-1))) key_masks = tf.tile(tf.expand_dims(key_masks,1),[1,tf.shape(queries)[1],1]) paddings = tf.ones_like(outputs) * (-2 ** 32 + 1) outputs = tf.where(tf.equal(key_masks,0),paddings,outputs)經過這一步處理,效果如下,我們下圖中用深灰色代表屏蔽掉的部分:
接下來的操作只針對Decoder的self-attention來說,我們首先得到一個下三角矩陣,這個矩陣主對角線以及下方的部分是1,其余部分是0,然后根據1或者0來選擇使用output還是很小的數進行填充:
diag_vals = tf.ones_like(outputs[0,:,:]) tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() masks = tf.tile(tf.expand_dims(tril,0),[tf.shape(outputs)[0],1,1])paddings = tf.ones_like(masks) * (-2 ** 32 + 1) outputs = tf.where(tf.equal(masks,0),paddings,outputs)得到的效果如下圖所示:
接下來,我們對query的部分進行屏蔽,與屏蔽key的思路大致相同,不過我們這里不是用很小的值替換了,而是直接把填充的部分變為0:
query_masks = tf.sign(tf.abs(tf.reduce_sum(queries,axis=-1))) query_masks = tf.tile(tf.expand_dims(query_masks,-1),[1,1,tf.shape(keys)[1]]) outputs *= query_masks經過這一步,Encoder和Decoder得到的最終的相似度矩陣如下,上邊是Encoder的結果,下邊是Decoder的結果:
接下來,我們就可以進行softmax操作了:
outputs = tf.nn.softmax(outputs)Fifth-Step-得到最終結果
得到了Attention的相似度矩陣,我們就可以和Value進行相乘,得到經過attention加權的結果:
這一部分是一個簡單的矩陣相乘運算,代碼如下:
outputs = tf.matmul(outputs,V)不過這并不是最終的結果,這里文中還加入了殘差網絡的結構,即將最終的結果和queries的輸入進行相加:
outputs += queries所以一個完整的Scaled Dot-Product Attention的代碼如下:
def scaled_dotproduct_attention(queries,keys,num_units=None,num_heads = 0,dropout_rate = 0,is_training = True,causality = False,scope = "mulithead_attention",reuse = None):with tf.variable_scope(scope,reuse=reuse):if num_units is None:num_units = queries.get_shape().as_list[-1]# Linear projectionQ = tf.layers.dense(queries,num_units,activation=tf.nn.relu) #K = tf.layers.dense(keys,num_units,activation=tf.nn.relu) #V = tf.layers.dense(keys,num_units,activation=tf.nn.relu) #outputs = tf.matmul(Q,tf.transpose(K,[0,2,1]))outputs = outputs / (K.get_shape().as_list()[-1] ** 0.5)# 這里是對填充的部分進行一個mask,這些位置的attention score變為極小,我們的embedding操作中是有一個padding操作的,# 填充的部分其embedding都是0,加起來也是0,我們就會填充一個很小的數。key_masks = tf.sign(tf.abs(tf.reduce_sum(keys,axis=-1)))key_masks = tf.tile(tf.expand_dims(key_masks,1),[1,tf.shape(queries)[1],1])paddings = tf.ones_like(outputs) * (-2 ** 32 + 1)outputs = tf.where(tf.equal(key_masks,0),paddings,outputs)# 這里其實就是進行一個mask操作,不給模型看到未來的信息。if causality:diag_vals = tf.ones_like(outputs[0,:,:])tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense()masks = tf.tile(tf.expand_dims(tril,0),[tf.shape(outputs)[0],1,1])paddings = tf.ones_like(masks) * (-2 ** 32 + 1)outputs = tf.where(tf.equal(masks,0),paddings,outputs)outputs = tf.nn.softmax(outputs)# Query Maskquery_masks = tf.sign(tf.abs(tf.reduce_sum(queries,axis=-1)))query_masks = tf.tile(tf.expand_dims(query_masks,-1),[1,1,tf.shape(keys)[1]])outputs *= query_masks# Dropoutoutputs = tf.layers.dropout(outputs,rate = dropout_rate,training = tf.convert_to_tensor(is_training))# Weighted sumoutputs = tf.matmul(outputs,V)# Residual connectionoutputs += queries# Normalizeoutputs = normalize(outputs)return outputs3.3 Multi-Head Attention
Multi-Head Attention就是把Scaled Dot-Product Attention的過程做H次,然后把輸出合起來。論文中,它的結構圖如下:
這部分的示意圖如下所示,我們重復做3次相似的操作,得到每一個的結果矩陣,隨后將結果矩陣進行拼接,再經過一次的線性操作,得到最終的結果:
Scaled Dot-Product Attention可以看作是只有一個Head的Multi-Head Attention,這部分的代碼跟Scaled Dot-Product Attention大同小異,我們直接貼出:
def multihead_attention(queries,keys,num_units=None,num_heads = 0,dropout_rate = 0,is_training = True,causality = False,scope = "mulithead_attention",reuse = None):with tf.variable_scope(scope,reuse=reuse):if num_units is None:num_units = queries.get_shape().as_list[-1]# Linear projectionQ = tf.layers.dense(queries,num_units,activation=tf.nn.relu) #K = tf.layers.dense(keys,num_units,activation=tf.nn.relu) #V = tf.layers.dense(keys,num_units,activation=tf.nn.relu) ## Split and ConcatQ_ = tf.concat(tf.split(Q,num_heads,axis=2),axis=0) #K_ = tf.concat(tf.split(K,num_heads,axis=2),axis=0)V_ = tf.concat(tf.split(V,num_heads,axis=2),axis=0)outputs = tf.matmul(Q_,tf.transpose(K_,[0,2,1]))outputs = outputs / (K_.get_shape().as_list()[-1] ** 0.5)# 這里是對填充的部分進行一個mask,這些位置的attention score變為極小,我們的embedding操作中是有一個padding操作的,# 填充的部分其embedding都是0,加起來也是0,我們就會填充一個很小的數。key_masks = tf.sign(tf.abs(tf.reduce_sum(keys,axis=-1)))key_masks = tf.tile(key_masks,[num_heads,1])key_masks = tf.tile(tf.expand_dims(key_masks,1),[1,tf.shape(queries)[1],1])paddings = tf.ones_like(outputs) * (-2 ** 32 + 1)outputs = tf.where(tf.equal(key_masks,0),paddings,outputs)# 這里其實就是進行一個mask操作,不給模型看到未來的信息。if causality:diag_vals = tf.ones_like(outputs[0,:,:])tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense()masks = tf.tile(tf.expand_dims(tril,0),[tf.shape(outputs)[0],1,1])paddings = tf.ones_like(masks) * (-2 ** 32 + 1)outputs = tf.where(tf.equal(masks,0),paddings,outputs)outputs = tf.nn.softmax(outputs)# Query Maskquery_masks = tf.sign(tf.abs(tf.reduce_sum(queries,axis=-1)))query_masks = tf.tile(query_masks,[num_heads,1])query_masks = tf.tile(tf.expand_dims(query_masks,-1),[1,1,tf.shape(keys)[1]])outputs *= query_masks# Dropoutoutputs = tf.layers.dropout(outputs,rate = dropout_rate,training = tf.convert_to_tensor(is_training))# Weighted sumoutputs = tf.matmul(outputs,V_)# restore shapeoutputs = tf.concat(tf.split(outputs,num_heads,axis=0),axis=2)# Residual connectionoutputs += queries# Normalizeoutputs = normalize(outputs)return outputs4、Position-wise Feed-forward Networks
在進行了Attention操作之后,encoder和decoder中的每一層都包含了一個全連接前向網絡,對每個position的向量分別進行相同的操作,包括兩個線性變換和一個ReLU激活輸出:
代碼如下:
def feedforward(inputs,num_units=[2048, 512],scope="multihead_attention",reuse=None):with tf.variable_scope(scope, reuse=reuse):# Inner layerparams = {"inputs": inputs, "filters": num_units[0], "kernel_size": 1,"activation": tf.nn.relu, "use_bias": True}outputs = tf.layers.conv1d(**params)# Readout layerparams = {"inputs": outputs, "filters": num_units[1], "kernel_size": 1,"activation": None, "use_bias": True}outputs = tf.layers.conv1d(**params)# Residual connectionoutputs += inputs# Normalizeoutputs = normalize(outputs)return outputs5、Encoder的結構
Encoder有N(默認是6)層,每層包括兩個sub-layers:
1 )第一個sub-layer是multi-head self-attention mechanism,用來計算輸入的self-attention;
2 )第二個sub-layer是簡單的全連接網絡。
每一個sub-layer都模擬了殘差網絡的結構,其網絡示意圖如下:
根據我們剛才定義的函數,其完整的代碼如下:
with tf.variable_scope("encoder"):# Embeddingself.enc = embedding(self.x,vocab_size=len(de2idx),num_units = hp.hidden_units,zero_pad=True, # 讓padding一直是0scale=True,scope="enc_embed")## Positional Encodingif hp.sinusoid:self.enc += positional_encoding(self.x,num_units = hp.hidden_units,zero_pad = False,scale = False,scope='enc_pe')else:self.enc += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.x)[1]),0),[tf.shape(self.x)[0],1]),vocab_size = hp.maxlen,num_units = hp.hidden_units,zero_pad = False,scale = False,scope = "enc_pe")##Drop outself.enc = tf.layers.dropout(self.enc,rate = hp.dropout_rate,training = tf.convert_to_tensor(is_training))## Blocksfor i in range(hp.num_blocks):with tf.variable_scope("num_blocks_{}".format(i)):### MultiHead Attentionself.enc = multihead_attention(queries = self.enc,keys = self.enc,num_units = hp.hidden_units,num_heads = hp.num_heads,dropout_rate = hp.dropout_rate,is_training = is_training,causality = False)self.enc = feedforward(self.enc,num_units = [4 * hp.hidden_units,hp.hidden_units])6、Decoder的結構
Decoder有N(默認是6)層,每層包括三個sub-layers:
1 )第一個是Masked multi-head self-attention,也是計算輸入的self-attention,但是因為是生成過程,因此在時刻 i 的時候,大于 i 的時刻都沒有結果,只有小于 i 的時刻有結果,因此需要做Mask.
2 )第二個sub-layer是對encoder的輸入進行attention計算,這里仍然是multi-head的attention結構,只不過輸入的分別是decoder的輸入和encoder的輸出。
3 )第三個sub-layer是全連接網絡,與Encoder相同。
其網絡示意圖如下:
其代碼如下:
with tf.variable_scope("decoder"):# Embeddingself.dec = embedding(self.decoder_inputs,vocab_size=len(en2idx),num_units = hp.hidden_units,scale=True,scope="dec_embed")## Positional Encodingif hp.sinusoid:self.dec += positional_encoding(self.decoder_inputs,vocab_size = hp.maxlen,num_units = hp.hidden_units,zero_pad = False,scale = False,scope = "dec_pe")else:self.dec += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.decoder_inputs)[1]), 0), [tf.shape(self.decoder_inputs)[0], 1]),vocab_size=hp.maxlen,num_units=hp.hidden_units,zero_pad=False,scale=False,scope="dec_pe")# Dropoutself.dec = tf.layers.dropout(self.dec,rate = hp.dropout_rate,training = tf.convert_to_tensor(is_training))## Blocksfor i in range(hp.num_blocks):with tf.variable_scope("num_blocks_{}".format(i)):## Multihead Attention ( self-attention)self.dec = multihead_attention(queries=self.dec,keys=self.dec,num_units=hp.hidden_units,num_heads=hp.num_heads,dropout_rate=hp.dropout_rate,is_training=is_training,causality=True,scope="self_attention")## Multihead Attention ( vanilla attention)self.dec = multihead_attention(queries=self.dec,keys=self.enc,num_units=hp.hidden_units,num_heads=hp.num_heads,dropout_rate=hp.dropout_rate,is_training=is_training,causality=False,scope="vanilla_attention")## Feed Forwardself.dec = feedforward(self.dec, num_units=[4 * hp.hidden_units, hp.hidden_units])7、模型輸出
decoder的輸出會經過一層全聯接網絡和softmax得到最終的結果,示意圖如下:
這樣,一個完整的Transformer Architecture我們就介紹完了,對于文中寫的不清楚或者不到位的地方,歡迎各位留言指正!
參考文獻
1、原文:https://arxiv.org/abs/1706.03762
2、https://mp.weixin.qq.com/s/RLxWevVWHXgX-UcoxDS70w
3、https://github.com/princewen/tensorflow_practice/tree/master/basic/Basic-Transformer-Demo
?
作者:石曉文的學習日記
鏈接:https://www.jianshu.com/p/b1030350aadb
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。
總結
以上是生活随笔為你收集整理的一步步解析Attention is All You Need的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文本分类从入门到精通
- 下一篇: AutoML - 数据增广