1. 程式人生 > >Attention is All You Need -- 淺析

Attention is All You Need -- 淺析

       由於最近bert比較火熱,並且bert的底層網路依舊使用的是transformer,因此再學習bert之前,有必要認真理解一下Transformer的基本原理以及self-attention的過程,本文參考Jay Alammar的一篇博文,翻譯+學習

原文連結:https://jalammar.github.io/illustrated-transformer/

 



       Transformer模型利用注意力機制來提高模型訓練速度練。Transformer在特定任務中的效能優於谷歌的基於神經網路的翻譯模型。然而,Transformer最大的好處是可以平行計算。

       整個模型如果用黑盒的視角來看的話,將一句話輸入到Transformer模型中,它的輸出將是該句的翻譯。

       將這個黑盒開啟,我們可以看到一組編碼器和一組解碼器: 


       整個Encoders是由6個相同的encoder組成,數字6沒有什麼特殊的含義,並且能夠嘗試一下其他的組合。同樣,Decoders元件是一堆相同數量的decoder組成。 

       編碼器在結構上都是相同的(但它們不共享權重)。每一個被分解成兩個子層: 

        一個self-attention層和一個feed forwoard network,Encoder的輸入首先通過一個self-attention層,這個層的作用是在處理一個特定單詞的時候,編碼器也能照顧到句子中的其他單詞(作用類似與RNN的記憶傳播功能)。我們將在稍後的文章中更深入地研究self-attention。self-attention的輸出被輸入到前饋神經網路。每個位置的前饋網路完全相同,但是相互是獨立的

解碼器也有這兩層,但在這兩層之間是一個Encoder-Decoder-attention層,它幫助解碼器將注意力集中在輸入句子的相關部分(類似於注意在seq2seq模型中所做的事情)。

 

 Bringing The Tensors Into The Picture

 

        現在我們已經瞭解了模型的主要元件,讓我們開始研究各種向量/張量,以及它們如何在這些元件之間流動,從而將訓練好的模型的輸入變成輸出。
        與NLP應用程式中的一般情況一樣,我們首先使用Embedding演算法將每個輸入單詞轉換為一個向量。

       Embedding只作用在最底層的編碼器中。所有編碼器都會接收到一個embedding vector,每個vector的大小都是512,對於Encoder的最底層,其輸入是embedding矩陣,但是對於其他層編碼器,其輸入是下面的編碼器的輸出。這個embedding vector的超引數是自定義的,基本上就是我們的訓練資料集中最長句子的長度。
       在輸入序列進行word embedding之後,每個單詞都流經編碼器的兩層。

 

       在這裡,我們看到Transformer的一個關鍵特性,即每個位置上的字在編碼器中通過它自己的路徑。在self-attention層中,這些路徑之間存在依賴性。但是,前饋層沒有這些依賴關係,因此在流經前饋層時可以並行執行各種路徑。
       接下來,我們將把示例轉換為一個更短的句子,並檢視在編碼器的每個子層中發生了什麼。

Now We’re Encoding!


       正如我們已經提到的,編碼器接收一個向量作為輸入。它通過將這些向量傳遞到一個“self-attention”層,然後進入前饋神經網路來處理這個向量,然後將輸出向上傳送到下一個編碼器。

 Self-Attention at a High Level

        不要被我用“self-attention”這個詞愚弄,好像每個人都應該熟悉這個概念。我個人從來沒有遇到過這個概念,直到讀了Attention is All You Need。讓我們提煉一下它是如何工作的。
         假設以下句子是我們要翻譯的輸入句子:
        “這隻動物沒有過馬路,因為它太累了。”
        這句話中的“it”是什麼意思?它指的是街道還是動物?這對人來說是一個簡單的問題,但對演算法來說就不那麼簡單了。
        當模型處理單詞“it”時,自我注意允許它將“it”與“animal”聯絡起來。
        當模型處理每個單詞(輸入序列中的每個位置)時,self attention允許它檢視輸入序列中的其他位置,以尋找有助於對該單詞進行更好編碼的線索。
        如果您熟悉RNN,請考慮如何維護一個隱藏狀態,以允許RNN將它已處理的前一個單詞/向量的表示與它正在處理的當前單詞/向量結合起來。self-attention是Transformer用來將其他相關單詞的“理解”轉化為我們正在處理的單詞的方法。

 Self-Attention in Detail

       我們先來看看如何用向量來計算自我關注,然後再來看看它是如何實現的——使用矩陣。
       計算自我注意的第一步是從編碼器的每個輸入向量中建立三個向量(在本例中,是每個單詞的嵌入)。因此,對於每個單詞,我們建立一個Query向量、一個Key向量和一個Value向量。這些向量是通過將embedding乘以我們在訓練過程中訓練的三個矩陣得到的。
       注意,這些新向量在維數上比嵌入向量小。它們的維數為64,而嵌入和編碼器的輸入/輸出向量的維數為512。它們不必更小,這是一種架構選擇,可以使多頭注意力(大部分)的計算保持不變。

什麼是Query向量、一個Key向量和一個Value向量?
       這些抽象概念對於計算和思考注意力很有用。當你開始閱讀下面是如何計算注意力的,你就會知道你需要知道的關於這些向量所扮演的角色的幾乎所有東西。
        self-attention計算的第二步依舊是計算一個分數。假設我們計算本例中第一個單詞“Thinking”的self-attention score。我們需要對輸入句子中的每個單詞進行打分。分數決定了我們在某個位置編碼一個單詞時,對輸入句子的其他部分的關注程度。
        分數是通過Query向量與對應單詞的Key向量的點積計算出來的。所以如果我們處理位置1的單詞的self-attention,第一個分數就是q1和k1的點積。第二個分數是q1和k2的點積。

 第三步和第四步是將分數除以8(論文中使用的關鍵向量的維數的平方根- 64)。這可以使得梯度更加穩定。這裡可能有其他可能的值,但這是預設值),然後通過softmax操作傳遞結果。Softmax將分數標準化,使它們都是正數,加起來等於1。

        這個softmax得分決定了每個單詞在這個位置的表達量。很明顯,這個位置的單詞會有最高的softmax得分,但是有時候更加關注與這個詞相關的詞也是有幫助的。
        第五步是將每個Value向量乘以softmax分數(為求和做準備)。這裡的直覺是保持我們想要關注的單詞的值不變,以及淹沒無關單詞的值(例如,將它們乘以0.001這樣的小數字)。
        第六步是加權值向量的求和。這會在這個位置(對於第一個單詞)產生self-attention層的輸出。

         這就是self-attention計算的結論。得到的向量是一個我們可以傳送到前饋神經網路的向量。但是在實際的實現中,為了更快的處理,這種計算是以矩陣形式進行的。現在我們來看看這個我們已經在單詞層面上看到了計算的直覺。

Matrix Calculation of Self-Attention

矩陣中的每一行對應輸入句子中的一個單詞。我們再次看到嵌入向量(512,或圖中4個方框)和q/k/v向量(64,或圖中3個方框)的大小差異

最後,由於我們處理的是矩陣,我們可以將步驟2到步驟6濃縮到一個公式中來計算self-attention層的輸出。

The Beast With Many Heads 

通過新增“多頭”注意機制,進一步細化了self-attention層。這從兩個方面提高了注意力層的效能:

  • 它擴充套件了模型關注不同位置的能力。是的,在上面的例子中,z1包含了一些其他編碼,但是它可以被實際單詞本身所控制。如果我們翻譯像“動物沒過馬路是因為它太累了”這樣的句子,它會很有用,我們想知道“它”是指哪個單詞。
  • 它給了注意層多重“表示子空間”。正如我們接下來將看到的,對於多頭注意力,我們不僅有一個,而且有多個查詢/鍵/值權重矩陣(轉換器使用8個注意頭,所以對於每個編碼器/解碼器,我們最終得到8個集合)。這些集合中的每一個都是隨機初始化的。然後,經過訓練後,使用每個集合將輸入嵌入(或來自較低編碼器/解碼器的向量)投影到不同的表示子空間中。

如果我們做同樣的self-attention計算,就像我們上面概述的,用不同的權重矩陣做8次不同的計算,我們最終得到8個不同的Z矩陣。

這給我們留下了一點挑戰。前饋層不需要八個矩陣——它只需要一個矩陣(每個單詞對應一個向量)。所以我們需要一種方法把這8個壓縮成一個矩陣。
我們怎麼做呢?我們把這些矩陣連起來然後用一個額外的權重矩陣把它們乘起來。

 

這就是多頭自我關注的全部內容。讓我試著把它們都放在一個影象中這樣我們就能在一個地方看到它們 

既然我們已經提到了注意力頭,讓我們回顧一下之前的例子,看看當我們在例句中編碼單詞“it”時,不同的注意力頭部集中在哪裡: 

然而,如果我們把所有注意力都集中到畫面上,事情就很難解釋了: 

Representing The Order of The Sequence Using Positional Encoding 

正如我們目前所描述的,模型中缺少的一件事是一種解釋輸入序列中單詞順序的方法。

為了解決這個問題,Transformer向每個embedding的輸入新增一個向量。這些向量具有固定的模式,這有助於它確定每個單詞的位置,或序列中不同單詞之間的距離。直觀的感覺是,將這些值新增到embedding vector中可以提供嵌入向量之間有意義的距離,這些向量一旦被投影到Q/K/V向量中,並且在點積注意期間。

如果我們假設embdding的維數為4,那麼實際的位置編碼應該是這樣的: 

這個模式是什麼樣的?
在下面的圖中,每一行對應一個向量的位置編碼。所以第一行就是我們要在輸入序列中嵌入第一個單詞的向量。每行包含512個值—每個值在1到-1之間。我們用顏色標記了它們,所以圖案是可見的。

 位置編碼的公式在論文(3.5節)中有描述。您可以在get_timing_signal_1d()中看到生成位置編碼的程式碼。這不是唯一可能的位置編碼方法。然而,它的優勢在於能夠縮放到看不見的序列長度(例如,如果我們訓練過的模型被要求翻譯比我們訓練集中的任何一個句子都長的句子)。

The Residuals

在繼續之前,我們需要提到編碼器架構中的一個細節,即每個編碼器中的每個子層(self-attention, ffnn)在其周圍都有一個剩餘連線,然後是一個分層規範化步驟。

如果我們將向量和與自我關注相關的層範數運算形象化,它會是這樣的: 

這也適用於解碼器的子層。如果我們考慮一個由兩個堆疊編碼器和解碼器組成的變壓器,它看起來是這樣的:

 

The Decoder Side

既然我們已經介紹了編碼器方面的大部分概念,我們基本上也知道了解碼器的元件是如何工作的。但是讓我們來看看它們是如何一起工作的。


        編碼器從處理輸入序列開始。將頂層編碼器的輸出轉換為一組注意向量K和v,每一個譯碼器在其“編碼器-譯碼器注意”層中使用,使譯碼器在輸入序列中適當的位置集中注意力:

 

下面的步驟重複這個過程,直到到達一個特殊的符號,表明變壓器解碼器已經完成了它的輸出。每一步的輸出都會在下一次的時候反饋給底層解碼器,解碼器就會像編碼器一樣,將解碼結果放大。就像我們對編碼器輸入所做的那樣,我們將位置編碼嵌入並新增到這些譯碼器輸入中以指示每個單詞的位置。 

解碼器中的自注意層與編碼器中的自注意層的工作方式略有不同:
在解碼器中,自注意層只允許注意到輸出序列中較早的位置。這是通過在自我注意計算的softmax步驟之前遮蔽未來位置(將它們設定為-inf)來實現的。
編碼器-解碼器注意層的工作原理與多頭自注意層類似,只是它從下面的層建立查詢矩陣,並從編碼器堆疊的輸出中獲取鍵和值矩陣。

 The Final Linear and Softmax Layer

解碼器堆疊輸出一個浮點向量。我們怎麼把它變成一個詞?這是最後一個線性層的工作,然後是一個Softmax層。
線性層是一個簡單的完全連線的神經網路,它將解碼器堆疊產生的向量投影到一個更大的稱為邏輯向量的向量上。
讓我們假設我們的模型知道10,000個從它的訓練資料集中學習的惟一英語單詞(我們的模型的“輸出詞彙”)。這將使logits向量寬10000個單元格——每個單元格對應一個唯一單詞的分數。這就是我們解釋線性層之後的模型輸出的方式。
softmax層然後將這些分數轉換為概率(都是正數,加起來等於1.0)。選擇概率最大的單元格,並生成與之關聯的單詞作為此時間步驟的輸出。


既然我們已經介紹了通過一個經過訓練的轉換器的整個前向傳遞過程,那麼瞭解一下訓練模型的直觀感受將是非常有用的。
在訓練過程中,未經訓練的模型會經歷完全相同的傳遞過程。但是由於我們是在一個標記的訓練資料集上訓練它,我們可以將它的輸出與實際正確的輸出進行比較。
為了理解這一點,我們假設輸出詞彙表只包含6個單詞(“a”、“am”、“i”、“thanks”、“student”和“”(“句子結束”的縮寫))。

 

一旦定義了輸出詞彙表,就可以使用相同寬度的向量來表示詞彙表中的每個單詞。這也稱為一熱編碼。例如,我們可以用以下向量表示am:

 

The Loss Function 

假設我們正在訓練我們的模型。假設這是我們培訓階段的第一步,我們正在用一個簡單的例子來培訓它——把“謝謝”翻譯成“謝謝”。
這意味著,我們希望輸出是一個表示“謝謝”的概率分佈。但是由於這個模型還沒有經過訓練,所以現在還不太可能實現。

如何比較兩個概率分佈?我們只要從另一箇中減去一個。要了解更多細節,請檢視交叉熵和庫爾巴克-萊布林散度。
但請注意,這是一個過於簡化的示例。更實際一點,我們將使用一個句子,而不是一個單詞。例如,輸入:“je suis etudiant”,預期輸出:“我是一名學生”。這真正的意思是,我們想要我們的模型連續輸出概率分佈,其中:
每個概率分佈都由一個寬度為vocab_size的向量表示(在我們的玩具示例中是6,但更實際的情況是一個數字,比如3,000或10,000)

第一個概率分佈在與“i”相關聯的單元格上具有最高的概率

第二個概率分佈在與am相關的單元格中具有最高的概率

以此類推,直到第五次輸出分佈表示“語句>的結尾”符號,該符號也有一個從10,000個元素詞彙表中關聯的單元格。

在對模型進行足夠長時間的大資料集訓練之後,我們希望得到的概率分佈是這樣的: 

 

現在,因為這個模型每次產生一個輸出,我們可以假設這個模型從概率分佈中選擇概率最大的單詞,然後扔掉其餘的。這是一種方法(稱為貪婪解碼)。另一個方法是堅持,說,前兩個單詞(說,比如“我”和“a”),然後在下一步中,執行模型兩次:一次假設第一個輸出位置是“我”這個詞,而另一個假設第一個輸出位置是‘我’這個詞,和哪個版本產生更少的錯誤考慮# 1和# 2儲存位置。我們對2號和3號位置重複這個。這種方法稱為“beam search”,在我們的例子中,beam_size是2(因為我們在計算位置#1和#2的beam之後比較了結果),top_beam也是2(因為我們保留了兩個單詞)。這兩個超引數都可以進行實驗。