論文分享-- >Attention is all you need

本次分享的論文是鼎鼎有名的attention is all you needattention\ is\ all\ you\ need,論文鏈接attention is all you need,其參考的tensorflowtensorflow 實現代碼tensorflow代碼實現

自己水平有限,在讀這篇論文和實現代碼時,感覺比較喫力,花了兩三天才搞懂了一些,在此總結下。

廢話不多說,直接帶着代碼看論文介紹的網絡結構。

下面總結是以論文實驗 機器翻譯來說的。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bTSPmlAh-1584426097531)(https://github.com/Njust-taoye/transformer-tensorflow/raw/master/images/transformer-architecture.png)]

我們分部分來看:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7SVz5ldn-1584426097532)(https://mchromiak.github.io/articles/2017/Sep/12/Transformer-Attention-is-all-you-need/img/encoder.png)]
建議可以先看看 臺大教授李宏毅關於transformer的課程:https://www.bilibili.com/video/av56239558?from=search&seid=17256210640645614178

Stage1 Encode Input

和普遍的做法一樣,對文本輸入做word embeddingword\ embedding 操作,

embedding_encoder = tf.get_variable("embedding_encoder", [Config.data.source_vocab_size, Config.model.model_dim], self.dtype)(注意這裏的model_dim)

embedding_inputs = embedding_encoder

上面其實就是做個輸入文本的embeddingembedding矩陣而已。

模型裏已經剔除了RNNRNNCNNCNN,如何體現輸入文本的先後關係呢?而這種序列的先後關係對模型有着至關重要的作用,於是論文中提出了Position EncodingPosition\ Encoding 騷操作~,論文是這樣做的:

PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i) = sin(pos/10000^{2i/d_{model}})(偶數位置處)
PE(pos,2i+1)=cos(pos/100002i/dmodel)PE(pos,2i+1) = cos(pos/10000^{2i/d_{model}})(奇數位置處)

這裏的dmodeld_{model}指的是上面word embeddingword\ embedding 的維度,pospos就是當前的詞在整個句子中的位置,例如第一個詞還是第二個詞等,ii 就是遍歷dmodeld_{model}時的值,在代碼中是這樣做的:

def positional_encoding(dim, sentence_length, dtype=tf.float32):

    encoded_vec = np.array([pos/np.power(10000, 2*i/dim) for pos in range(sentence_length) for i in range(dim)])#對每個位置處都產生一個維度爲dim的向量。
    encoded_vec[::2] = np.sin(encoded_vec[::2])#偶數位置處
    encoded_vec[1::2] = np.cos(encoded_vec[1::2])#奇數位置處

    return tf.convert_to_tensor(encoded_vec.reshape([sentence_length, dim]), dtype=dtype)
#Positional Encoding
with tf.variable_scope("positional-encoding"):
                positional_encoded = positional_encoding(Config.model.model_dim, Config.data.max_seq_length, dtype=self.dtype)

上面生成的positional_encodedpositional\_encoded其實就是位置信息的embeddingembedding 矩陣。論文中提到這樣做的原因,就是希望模型能很容易的學到相對先後的位置信息。

# Add
position_inputs = tf.tile(tf.range(0, Config.data.max_seq_length), [self.batch_size])#將range(0, max_seq_length)列表複製batch_size次,生成shape爲[batch_size, max_seq_length]的張量。

position_inputs = tf.reshape(position_inputs,[self.batch_size, Config.data.max_seq_length]) # batch_size x [0, 1, 2, ..., n]#未經過embedding的位置輸入信息。

好了embedding輸入文本和位置信息的embedding矩陣都做好了,該look_uplook\_up 了。

encoded_inputs = tf.add(tf.nn.embedding_lookup(embedding_inputs, inputs),  tf.nn.embedding_lookup(positional_encoded, position_inputs))

這與輸入文本信息就結合的其位置信息了,作爲encoderencoder的整體輸入,這部分對應的上面那張圖的**stage1stage1**部分,這部分的操作就是如下:

positional encodings ddedembedded inputspositional\ encodings\ dded ⊕ embedded\ inputs

decode_input embeddingdecode\_input\ embedding同理,就不再贅述了。

Stage2 Multi Head Attention

multi head attention 絕對是transform裏面的 一個重點和優點,正是有了這個機制,transform纔能有這麼好的效果。

當時論文讀到這裏有點懵逼,什麼叫multi headmulti\ head?再仔細看看論文吧?

這裏寫圖片描述

由上圖我們可以看出multi head attentionmulti\ head\ attention 有三個相同的輸入,不妨分別記爲QKVQ、K、V,其實就是上面的Encode InputEncode\ Inputshapeshape均爲[batch_size,max_seq_length,dim][batch\_size, max\_seq\_length, dim]。論文中提到對三個輸入做num_headnum\_head次不同的線性映射,即爲:

def _linear_projection(self, q, k, v):
    q = tf.layers.dense(q, self.linear_key_dim, use_bias=False)
    k = tf.layers.dense(k, self.linear_key_dim, use_bias=False)
    v = tf.layers.dense(v, self.linear_value_dim, use_bias=False)
	return q, k, v

上述代碼就是做線性映射,其中linear_key_dimlinear_value_dimlinear\_key\_dim、linear\_value\_dim就是映射的unitsunits個數。這裏面相當於把num_headnum\_head次的線性映射一起做了,後面需要把每一個headhead映射結果分割開,故需要保證linear_key_dimlinear\_key\_dimlinear_value_dimlinear\_value\_dim 能整除num_headnum\_head。 經過線性映射後生成的qkvq、k、vshapeshape分別爲[batch_size,max_seq_length,linear_key_dim],[batch_size,max_seq_length,linear_key_dim],[batch_size,max_seq_length,linear_value_dim][batch\_size, max\_seq\_length, linear\_key\_dim], [batch\_size, max\_seq\_length, linear\_key\_dim], [batch\_size, max\_seq\_length, linear\_value\_dim]

然後按num_headsnum\_heads分割開來得:(這裏分割開,相當於初始個num_head 不同的權重,下面將會做num_head 次不同的attention操作理解這非常重要

def _split_heads(self, q, k, v): 
    def split_last_dimension_then_transpose(tensor, num_heads, dim):
    ┆   t_shape = tensor.get_shape().as_list()
    ┆   tensor = tf.reshape(tensor, [-1] + t_shape[1:-1] + [num_heads, dim // num_heads])
    ┆   return tf.transpose(tensor, [0, 2, 1, 3]) # [batch_size, num_heads, max_seq_len, dim]

    qs = split_last_dimension_then_transpose(q, self.num_heads, self.linear_key_dim)
    ks = split_last_dimension_then_transpose(k, self.num_heads, self.linear_key_dim)
    vs = split_last_dimension_then_transpose(v, self.num_heads, self.linear_value_dim)

    return qs, ks, vs

論文提到,這時生成的qsksvsqs、ks、vs可以並行的放入到attention_functionattention\_function 中,那麼這個attention_functionattention\_function 是個什麼樣的結構呢?

這裏寫圖片描述

上圖所示的結構在論文中被稱爲Scaled Dot_Product AttentionScaled\ Dot\_Product\ Attention,其attentionattention值的計算公式如下:
Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=softmax\left ( \frac{QK^{T}}{\sqrt{d_k}} \right )V

在這裏插入圖片描述


由上面可知,其公式中的QKQ、K、V 分別對應的是qsksvsqs、ks、vs,其實都是Encoder InputEncoder\ Input只是做了不同的線性映射,其中qsksqs、ks 的維度相同。我們可以這樣理解QKTQK^T操作,假設QQshapeshape[max_seq_length,dim][max\_seq\_length, dim]的矩陣,VVshapeshape相同,那麼經過QKTQK^T操作以後,變成了shapeshape[max_seq_length,max_seq_length][max\_seq\_length, max\_seq\_length]的矩陣,怎樣理解這個生成的矩陣呢?其實就是做了個self_attentionself\_attention操作,即是當前句子中每個詞和其他詞做個乘積形成的矩陣,以得到每個詞的權重,以便學習當前應該foucsfoucs到哪個詞。那麼爲什麼要除以dk\sqrt{d_k}呢?論文中說到,當兩個矩陣做dot productdot\ product時,可能會變得很大(試想一下,兩個矩陣相互獨立,且均值爲0,方差爲1,那麼經過矩陣相乘以後,均值還爲0,方差變成dkd_k),經過softmaxsoftmax後,梯度可能會變得很小,爲了抵消這種效果,再除以dk\sqrt{d_k}。其代碼如下:

def _scaled_dot_product(self, qs, ks, vs):
    key_dim_per_head = self.linear_key_dim // self.num_heads

    o1 = tf.matmul(qs, ks, transpose_b=True)
    o2 = o1 / (key_dim_per_head**0.5)

    if self.masked:
    ┆   diag_vals = tf.ones_like(o2[0, 0, :, :]) # (batch_size, num_heads, query_dim, key_dim)
    ┆   tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (q_dim, k_dim)
    ┆   masks = tf.tile(tf.reshape(tril, [1, 1] + tril.get_shape().as_list()),
    ┆   ┆   ┆   ┆   ┆   [tf.shape(o2)[0], tf.shape(o2)[1], 1, 1])
    ┆   paddings = tf.ones_like(masks) * -1e9
    ┆   o2 = tf.where(tf.equal(masks, 0), paddings, o2)

    o3 = tf.nn.softmax(o2)
    return o3

好了,過了self_attentionself\_attention後:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XPjiunL2-1584426097533)(https://mchromiak.github.io/articles/2017/Sep/12/Transformer-Attention-is-all-you-need/img/MultiHead.png)]

由上圖可知,再過concatconcat操作:(不同的head關注的點可能不一樣,這裏concat操作,相當於把num_head次不同的attention結果集成在一起,理解這非常重要

def _concat_heads(self, outputs):

    def transpose_then_concat_last_two_dimenstion(tensor):
    ┆   tensor = tf.transpose(tensor, [0, 2, 1, 3]) # [batch_size, max_seq_len, num_heads, dim]
    ┆   t_shape = tensor.get_shape().as_list()
    ┆   num_heads, dim = t_shape[-2:]
    ┆   return tf.reshape(tensor, [-1] + t_shape[1:-2] + [num_heads * dim])

    return transpose_then_concat_last_two_dimenstion(outputs)

論文中提到,這樣做後,再過一層線性映射。

output = tf.layers.dense(output, self.model_dim)

故整個Multi Head AttentionMulti\ Head\ Attention 操作如下:

def multi_head(self, q, k, v):
    q, k, v = self._linear_projection(q, k, v)
    qs, ks, vs = self._split_heads(q, k, v)
    outputs = self._scaled_dot_product(qs, ks, vs)
    output = self._concat_heads(outputs)
    output = tf.layers.dense(output, self.model_dim)
    return tf.nn.dropout(output, 1.0 - self.dropout)

然後在做個resNetresNetlayerNormalizationlayerNormalization

def _add_and_norm(self, x, sub_layer_x, num=0):
    with tf.variable_scope(f"add-and-norm-{num}"):
    ┆   return tf.contrib.layers.layer_norm(tf.add(x, sub_layer_x)) # with Residual connection

這裏面的sub_layer_xsub\_layer\_x 就是上面mutli headmutli\ head的輸出,xx就是encoder_inputencoder\_input
矩陣並行化計算過程:
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

上面就是Stage2Stage2 的整個過程。

Stage3 Feed Forward

這一步就比較簡單了,就是做兩層的fully_connectionfully\_connection 而已,只不過內層的fully_connectionfully\_connection 會過relurelu 激活。

FFN(x)=max(0,xW1+b1)W2+b2FFN(x) = max(0, xW_1 + b_1)W_2 + b_2

別問我爲啥maxmax

同理再過reNetnormalizationreNet、normalization

DecodeDecode部分和上面差不多,只不過在Decode InputDecode\ InputStage1Stage1部分,做self attentionself\ attention時,我們不能使當前詞的後面的詞對當前詞產生影響,因爲在當前我們實際是不知道後面應該有哪些詞的,只不過在traintrain的時候可以批量的訓練,但是在decodedecode的時候是不知道的。那麼該怎麼消除後面詞對當前詞的影響呢?

self attentionself\ attention時,會得到attentionattention矩陣,我們只需要保留該矩陣的下三角部分即可,然後再做softmaxsoftmax,既可消除後面詞對當前詞的影響。

def _scaled_dot_product(self, qs, ks, vs):
    key_dim_per_head = self.linear_key_dim // self.num_heads

    o1 = tf.matmul(qs, ks, transpose_b=True)
    o2 = o1 / (key_dim_per_head**0.5)

    if self.masked:
    ┆   diag_vals = tf.ones_like(o2[0, 0, :, :]) # (batch_size, num_heads, query_dim, key_dim)
    ┆   tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (q_dim, k_dim)
    ┆   masks = tf.tile(tf.reshape(tril, [1, 1] + tril.get_shape().as_list()),
    ┆   ┆   ┆   ┆   ┆   [tf.shape(o2)[0], tf.shape(o2)[1], 1, 1])
    ┆   paddings = tf.ones_like(masks) * -1e9
    ┆   o2 = tf.where(tf.equal(masks, 0), paddings, o2)

    o3 = tf.nn.softmax(o2)
    return o3

剩餘部分的Stage4Stage5Stage4、Stage5與上面類似,就不再贅述了。

整個論文的過程可以用如下動畫解釋:

在這裏插入圖片描述

個人看法

  1. 該論文擯棄了RNNCNNRNN、CNN 等作爲基本的模型,而是單純的採用AttentionAttention結構,使得計算並行性大大提高。
  2. 沒想到attentionattention也可以單獨的作爲神經網絡的一層,甚至可以看作對inputinputrepesentationrepesentation
  3. Transformer的多頭注意力機制能從不同角度對每個對象的重要性進行評價,從而能更好的學習輸入對象中存在的各種關係並對其進行表徵。
    在這裏插入圖片描述
    不同的head, attention的注意力不一樣
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章