Transformer的實現原理(tensor2tensor)

背景

       最近在做機器翻譯的優化,接觸到的是谷歌在18年發佈的transformer模型,在經歷過一個星期後的算法原理和源碼閱讀後,基本上對整個模型有了相對透徹的理解,下面對整個流程進行復盤避免以後自己忘記,後面也會對相關優化進行簡單介紹。

預處理

在對翻譯處理的過程,首先需要對一句話進行分詞,比如“我是一個好學生”,分詞出來後可能就是“我”,“是”,“一”,“ 個”,“好”,“學”,“生”,這裏我們只要知道分詞後每一個詞是一個token,而每一個token可以由一個整數來表示就可以了,比如以上可以由7個數字組成,假如是[23, 432, 561, 12, 78, 29, 73]。我們知道大部分翻譯模型是時序的,也就是說“i am a student”這四個單詞每次只能吐出一個詞,所以一共要吐幾次,這個就需要提前約定好,在這裏是將原文句子長度的3倍和一個固定參數(比如100)作比較,取最小值作爲吐詞個數的上限,在這裏是min(7*3, 100)=21,不過一旦在吐詞過程中吐出單詞end的話也會停止翻譯,即使當下還沒有達到21個詞。

編碼器

目前輸入爲[23, 432, 561, 12, 78, 29, 73],在這裏編碼器的輸入爲固定長度,比如是10, 所以要進行補零操作[23, 432, 561, 12, 78, 29, 73,0,0,0],接下來在處理深度模型時我們知道一般是要進行one-hot編碼的,如果整數範圍爲(0~32768),那麼編碼後就變成一個(1, 10,32768)的二維數組(爲了好說明,我們batch一直設置爲1,[[001...00], [000...10], ...]),爲了簡化降維,或者是說選擇一種更合適的編碼方式,我們實際上使用的是embeding編碼,原理比較簡單,最通俗的理解可以是,我們可以根據23,432...這些數字去一個表中(這個表的維度假如是(32748,1024))查找自己對應的編碼,目前輸入input_tensor我們就處理好了,input_tensor.shape=(1,7,1024)。

數據處理好了以後, 接下是進入核心的編碼器,我假如編碼器只有一層,該層中包含,self-attention和ffn兩個部分。首先先介紹self-attention部分。

self-attention:

                                圖一 self_atttention

可以看到首先是一個預處理,直接看預處理代碼:

def layer_preprocess(layer_input):
  with tf.variable_scope("layer_preprocess"):
    with tf.variable_scope("layer_norm"):
      num_units = layer_input.shape[-1]
      scale = tf.get_variable(
        "layer_norm_scale", [num_units], initializer=tf.ones_initializer(), trainable=True)
      bias = tf.get_variable(
        "layer_norm_bias", [num_units], initializer=tf.zeros_initializer(), trainable=True)
      x = layer_input
      # return layer_process_module.opt_layer_preprocess_dthree(x,scale,bias)
      # return tf.user_ops.opt_layer_preprocess_dthree(x,scale,bias)
      epsilon, scale, bias = [tf.cast(t, x.dtype) for t in [tf.constant(1e-6), scale, bias]]
      mean = tf.reduce_mean(x, axis=[-1], keepdims=True)
      variance = tf.reduce_mean(tf.square(x - mean), axis=[-1], keepdims=True)
      norm_x = (x - mean) * tf.rsqrt(variance + epsilon)
      return norm_x * scale + bias

其實原理比較簡單,假如數據爲[[0.12,1.14,0.3,....0.31],,[..], [...] ,[...],[...],[...],[...]],這裏只展示了10個詞中第一個單詞的1024維中的幾個數,這裏預處理就是對[0.12,1.14,0.3,....0.31]求方差mean和均值var,然後對[0.12,1.14,0.3,....0.31]進行一個norm操作,說不清楚這個意義是啥,盡然做了就做了,預處理後維度維(1,7,1024)。

mutihead-attention:

 

Q,K,V

首先還是看圖,圖中顯示首先根據上一個流程得到的數據得到Q,K,V,這一部分原理就不再詳述,網上有一篇文章寫的很明白,這裏我們只是理一下具體的具體的計算流程。這裏我們設置的head爲16,下面一直用這個參數。

def compute_qkv(q_a, m_a, kd, vd, ver):
  if m_a is None: m_a = q_a
  if ver == 1:
    q = compute_attention_component_v1(q_a, kd, "q")
    k = compute_attention_component_v1(m_a, kd, "k")
    v = compute_attention_component_v1(m_a, vd, "v")
  else:
    assert kd == vd
    q, k, v = compute_attention_component_v2([q_a, m_a, m_a], kd, "qkv")
  return q, k, v

看代碼,其中的m_a目前先不用管,設置爲None,可以看看到qkv可以通過函數compute_attention_component_v1函數,這個函數的就是一個矩陣乘法,第一個參數爲輸入數據(1,7,1024),第二數據爲希望得到的維度k,即(1,7,k),在這裏我們假設維度沒有被壓縮,k依然是1024,那麼每一個qkv的維度都是(1,7,1024),接下來我們需要進行分頭,那麼qkv的維度就會變爲(1,7,16,64),16是head,1024被分爲16份後變成64。接下爲了計算方便做了一個轉置,qkv數據維度爲(1,16,7,64)。

dot_product_attention

圖中顯示接下來需要將qkv做一次大整合,其中q在整合之前還乘了1/(根號64), 爲什麼除8也不清楚,感覺是爲了壓縮q值,這裏的 dot_product_attention是根據谷歌一篇論文來的《Self-Attention with Relative Position Representations》,理論比較簡單,計算流程分爲幾個小步驟:

1. 計算q乘k,二者相乘得到一個x, x的維度(1,16,7,7),也就說每一單詞的q與每一個單詞的k乘法

2.計算position_k:

具體操作時,首先去確定一個最大位置相關度的數,這裏假定爲20,也就是說位置相差20以內的單詞有關聯,也就是說我們需要把一句話的所有單詞相對位置鉗位到【-20~20】之間,舉個例子,如果max_position爲5的話,那麼第一個單詞相對位置序列爲[0,1,2,3,4,5,6],  可以鉗位到[0,1,2,3,4,5,5]這個序列,那麼第二個單詞位置相對於其他位置爲(-1, 0,1,2,3,4,5),具體如下圖,然後去一個embeding_table(二維矩陣可以訓練)中查表(和上面詞embed一個意思,就是將這個7*7的矩陣中的數字換成64位的向量),其中table的維度是(2*max_position+1, 64)(因爲有個0,所以要+1),得到的結果是position_k(每個單詞距離其他位置的7*7*64矩陣),其中position.shape=(7,7,64)(意思是每個head的position都是一樣的)。注意:在decode中和該處處理方式有差異,細節可以參考源碼

3.計算position_k與q:

將q與position進行矩陣乘,q的維度爲[1,16,7,64],position的維度爲(7,7,64),那麼得到結果是(7,1*16,7),接下來再reshape和transpose到y,y的shape爲(1,16,7,7),這裏的計算可以理解爲:對每一個單詞的每一個q做乘加,算出他的位置信息在v中的比重,以前的話只有x,現在加了一個y。

4,將x和y相加得到z,z的shape(1,16,7,7)

5.  對z先mask(爲了讓之前pading出來的單詞的影響力將爲0,這裏可以不用在意),然後求softmax,結果爲r,shape(1,16,7,7)

6   r 與v相乘得到m,維度爲(1,16,7,64),r與position_v相乘得到n,n的維度(1,16,7,64),position_k和上面的position_k的獲取完全一樣。

7. m與n相加得到result,shape爲(1,16,7,64)

由於對nlp不是很瞭解,只能模糊感覺利用position_k,和position_v是爲了將位置信息編碼進去,反正人家谷歌說好那就好白,神經網絡只要捨得加參數,無非就是一個大黑盒,參數越多,擬合的效果一般都會更好一些。

combine_head:

輸出的結果當時是(1,7,1024),等於是折騰一圈又回到最初的原點,但其實已經物是人非。

output_transformer:

本來以爲上面的記過已經是最終的結果,其實不然,在離開的時候又做了一次矩陣乘,得到(1,7,1024),整個mutihead_attention結束後是一個後處理。

layer_postprocess:

殘差相加,和圖像中的殘差網絡一樣,沒啥可說的,至此,整個self_attention全部結束,該部分也是transfomer最核心的計算,

可以看到這是一頓操作,參數不夠一直加,哎,大力出奇跡,還是希望以後有大佬來解釋解釋。

fnn:

這個fnn比較簡單,就是上一層結果做兩次矩陣乘,可以看到fnn的輸入還有一個pad_reduce,這個還是開始說過的pad時候的一些操作,具體比較簡單,不在此詳述,可以說目前,我們已經拿到了encode部分得到的編碼信息。

decode:

上面的整個encode過程的結果爲[1,7,1024](batch, length, hidden_num),簡單的說就是將一句話進行了編碼,考慮到解碼應用到

多batch的場景下,所以這裏我們假設是batch爲3,也就是encode的輸出結果是[3, 7, 1024].在細節講述之前先對整體的思路進行概述。

我們先把解碼器想像爲一個黑盒,那麼每次的輸入就是維度爲3的一維數組,

1.   初始的時候輸入爲【1,1,1】,1是開始的標誌id,個數爲batch_size

2.   然後進行embedding, embedding table和encode中的一樣,都是將整型id轉爲1024的維度數組,【3,1024】

3.  經過一個decoder黑盒得到【3,1024】的結果

4.  將步驟3的結果進行一個矩陣乘法,【3,1024】* 【1024, 32768】得到【3, 32768】

5. 步驟4的結果一共有3行,每行32768維,這裏取32768維中值最大的位置信息作爲其每一句話翻譯結果比如【32, 456,1023】,並將結果保存下來存放在result表格中。

6. 將【32, 456,1023】替換步驟1中的【1,1,1】重複上述操作直到達到翻譯終止條件

7.這樣第一句話(batch)的翻譯結果就是(32,45, 67, ....),第二句話就是(456,657, 45 ....)

result--table

32 45 67 。。。
456 657 45 。。。
1023 879 23 。。。

 

 

 

步驟3中的細節

 

 

黑盒解碼器是由上圖的小單元組成,如果解碼器有六層,那麼就有六個小單元串聯起來,下面對每個小模塊進行詳細的分析。

layer_0

layer_0中主要是兩塊,self_attention和encdec_attention,

先看self_attention:

self_attention中基本是和encode裏面的操作相同,layer_preposition和layer_postprocess兩個部分不再詳述,和encode部分完成相同

下面主要對multihead_attention進行闡述。

首先multihead_attention的輸入爲【3,1024】,那麼計算q,k,v得到結果都是【3,1024】維,q我們先放着,現在我們需要把k和v

concat到cache_k, 和cache_v中。cache_K和cache_v是一個全局變量,一開始爲空,且每一個單元都有自己cacha_k和cache_v,

有點細節需要主要的是,concat的形式應該如下,cache_k_0爲第一次的結果,ABC代表batch爲3的cache_k,cache_k_1爲第二次cache應該有的形式,cache_V也是同理,cache_k和cache_v的維度爲【time*batch_size, 1024】,在這裏我們假設我們是做第二次,也就是上面概述的【32, 456,1023】替換步驟1中的【1,1,1】的過程那麼cache_k的維度是【6, 1024】。

cache_k_0
A0 [1.2,0.4,0.2.........]
B0  
C0  
cache_k_1
A0 [1.2,0.4,0.2.........]
A1  
B0  
B1  
C0  
C1  

我們知道q的形式如下, 維度爲【3,1024】

q
A1 [0.2,0.3,0.4.........]
B1  
C1  

整體的tensorboard可以看上面encoder部分。

第一步計算cache_k*q,圖中左邊的流程就是該過程,得到的是(3,16*2)的矩陣,這裏解釋一下,3是batch, 16是head, 2

是概述中第二次循環。

第二步得到position_key,首先當下相對位置爲0,畢竟自己相對於自己位置肯定是0,左邊相對於自己的位置就是負數,因爲

是第二次循環,所以左邊只有一個結果,編碼結果爲【-1,0】,embedding 後爲(2, 64)的矩陣。

第三步是計算position_key*q, 這裏要注意的是,考慮到加速問題,我們不可能像原始計算方式那樣擴充,翻轉矩陣,我們需要

知道細節,通過指針調用減少不必要的內存拷貝的操作,圖中的步驟三就是一個矩陣計算草圖,position_key被重複應用的每一

個頭和batch上,最終得到(3, 16*2)的矩陣。

第四步是將position_key*q+cache_k*q相加的到softmax,人後算一個softmax,這個操作就沒畫圖了,理解很容易。這裏可以直觀感覺可以利用結合律將position_key+cache_k先相加,這個可以根據實際情況自主選擇。

第五步是softmax*cache_v, 得到(3, 16*64)

第六步計算position_value,然後計算softmax*position_value+softmax*cache_v,得到(3,16*64)=(3,1024)

先看encdec_attention:

 

從tensorboard可以看到encdec的attention比較簡單,首先multihead_attention的輸入爲【3,1024】,那麼計算q得到結果是【3,1024】維,這裏發現沒有k,和v了,在dot_product_attention裏面操作是,softmax(q*k)*V, 那麼這裏的k和v是什麼,k和v就是將encode的輸出(3,7,1024)的矩陣分別乘encdec_layer_0_weight_k和encdec_layer_0_weight_k得到k ,v    encdec_layer_0_weight_k的維度是(1024,1024),則k和v的維度始終都是(3, 7, 1024),佈局和上面的self_attention中的cache_k和cahe_v一樣。整個計算中也沒有了relative position的操作,矩陣計算細節和上面softmax(q*k)*V一樣,最終輸出(3,1024)。

layer_0

這部分就不說,一個ffn,也就是裏面做了兩次矩陣計算,先從(3,1024)變換到(3,4096),再從(3,4096)變換到(3,1024)。

整個解碼部分的單層layer就是這樣,最後至於選擇是greedy_search還是beam_search就不是解碼器的工作了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章