前沿

注意力(attention)--是一個在深度學習模型中普遍使用的方法。注意力是一個幫助改善神經機器翻譯應用性能的概念。在這篇博客中,我會著眼於Transformer--該模型運用注意力來提高訓練這些模型的速度。Transformer在一些特定任務中性能表現超過Google Neural Machine。然而,最大的益處在於Transformer並行化運算。事實上,Google Cloud的建議是使用Transformer作為參考模型來使用他們的Cloud TPU產品。因此讓我們嘗試拆解模型來看看它是怎麼工作的。

Transformer在Attention is All You Need這篇論文中被提出。它的一個tensorflow版本實現可以在Tensor2Tensor得到。哈佛大學的NLP小組寫了pytorch的實現guide annotating the paper with PyTorh implementation.在這篇博客中,我們嘗試使一些東西簡單化,逐一介紹概念,以便在沒有深入瞭解主題的情況下讓人們更容易理解。

從整體看

我們可以把這些模塊看成單個黑盒。在機器翻譯應用中,輸入是一種語言的句子,輸出翻譯成另一種語言。

拋開其它的一切,我們看到了一個編碼組件,一個解碼組件以及他們之間的連接。

編碼部分是編碼器的堆疊(在這篇論文中堆疊了6個,在彼此之上一個一個互相疊加-數字6並沒有什麼奇特之處,你也可以在實驗中嘗試其他的數字)。解碼部分是相同數目解碼器的堆疊。

編碼器在結構上完全相同(當然他們不共享權重)。每一個編碼器分解成兩個子層:

編碼器的輸入首先經過一個自注意力層-該層幫助編碼器著眼於輸入句子中其他的單詞,因為它對特定單詞編碼。我們會在後面的部分詳細介紹自注意力(self-attention)。

自注意力層的輸出進入前向神經網路(feed-forward neural network)。完全相同的前饋網路獨立應用於每個位置。

解碼器也有這些層,但是在兩者之間有一個注意力層,幫助解碼器專註於輸入句子的相關部分(類似於seq2seq models模型中的attention)。

將張量圖形化表示

既然我們已經看過了模型的主要部分,下面我們來看一下不同的矢量/張量以及它們是怎麼在不同部分之間流動,來將訓練模型的輸入轉換為輸出。

與NLP應用中的情況一樣,我們先利用embedding algorithm演算法,把每一個輸入單詞變成一個矢量。

單詞嵌入只在最低端的編碼器。它們接收每個大小為512的向量表-在最底部的編碼器會進行單詞嵌入,但是在其他編碼器中,它是直接將位於下方的編碼器輸出。向量表的大小是一個我們可以設置的超參數,該參數是訓練集中最長句子的長度。

在輸入序列單詞嵌入之後,它們中的每一個都流過編碼器的兩層中的每一層。

在此,我們看到了Transformer的一個關鍵特徵,即每個位置中的單詞在編碼器中流經自己的路徑。在自注意力層這些路徑存在依賴關係。前饋網路層不存在這些依賴關係,因此,各種路徑可以在流過前饋層的同時並行執行。

接下來,我們將示例切換為較短的句子,我們將查看編碼器的每個子層中發生的情況。

編碼器

就像我們上面提到的那樣,一個編碼器接收一系列矢量作為輸入。它通過將這些向量傳遞到自注意力層來處理列表,然後進入前饋神經網路,接著把這些輸出送到下一個編碼器。

每一個位置處的單詞都經過自編碼過程。然後,它們各自通過一個前饋神經網路-完全相同的網路,每個向量分別流過它。

自注意力

不要因為我提出一個「自注意力」詞語而被迷惑,這是每個人都應該熟悉的概念。我自己從來沒有碰到過這個概念直到閱讀了Attention is All You Need 這篇論文。讓我們探究一下它是如何工作的。

下面的句子是一個我們想要翻譯的輸入語句:

「The animal didnt cross the street because it was too tired」

it在句子中指代什麼?是指street還是animal?對人類來說這是一個簡單的問題,但是對演算法來說不簡單。

當模型處理單詞「it」時,自注意力允許it和animal聯繫起來。

當模型處理每個單詞(輸入序列中的每個位置),自注意力允許它著眼於輸入序列中的其他位置,來尋找可以幫助更好地編碼該單詞的線索。

如果你對RNN熟悉,請考慮如何保持隱藏狀態允許RNN將其已處理的先前單詞/向量的表示與其正在處理的當前單詞/向量合併。自注意力是Transformer用於將其他相關單詞的「理解」 bake到我們當前正在處理的單詞中。

當我們在編碼器中編碼單詞it時(最上面的編碼器),一部分的注意力機制正在關注「The Animal」,然後其將一部分注意力影響到「it」的編碼中。

檢查Tensor2Tensor notebook確保你可以載入Transformer模型,並使用此互動式可視化對其進行檢查。

詳解自注意力

首先看一下如何利用向量計算自注意力,然後繼續看它是如何用矩陣實現的。

第一步,計算自注意力需要從每個編碼器的輸入向量創建3個向量(在這種情況下,嵌入每一個單詞)。因此對於每一個單詞,我們創建一個Query向量,一個Key向量,一個Value向量。這些向量通過將嵌入乘以我們在訓練過程中訓練的3個矩陣而創建。

注意到這些新的向量在維度上小於嵌入向量。它們的維度是64,而單詞嵌入和編碼器輸入輸出向量是512維。這個維度不必要變小,這是一種架構選擇,可以使多頭注意力的計算量保持不變。

將x1乘以權重矩陣WQ會產生q1,query向量和與輸入對應的那個詞有關,我們最終為輸入句子中每個單詞創建query,key,value投影。

query,key和value向量是什麼?

它們是摘要信息,有助於計算和思考注意力。繼續閱讀以下計算自注意力的方法。你幾乎可以瞭解這些向量所扮演的角色。

第二步,計算自注意力就是計算得分(score)。假設我們正在計算例子中的第一個單詞「Thinking」的自注意力。我們需要根據這個單詞對輸入句子的每個單詞進行評分。這個分數決定當我們在某個位置編碼單詞時,應該對輸入句子中的其他部分放置多大的焦點。

通過將query向量和相應單詞的key向量點積來計算得分。因此,如果我們處理位置1處的單詞的自注意力,第一個分數就是q1和k1的點積。第二個分數就是q1和k2的點積。

第三步和第四步,把這個分數除以8(在論文中,key向量維度64的平方根=8,如果是64,這會導致更平滑的梯度,因為當取值較大時,softmax的梯度比較小。所以它求了平方根。這個值可以是其他可能的值,64隻是一個默認值),然後把結果進行softmax操作。softmax歸一化這些分數,保證它們都是正數並且和為1.

該softmax分數決定每個單詞在該位置受關注的程度。很明顯,這個位置上的單詞將具有最高的softmax分數,但有時候關注與當前單詞相關的另一個單詞是有用的。

第五步,每一個value向量乘以softmax分數(為了求和做準備)。這裡的直覺是,保持我們關注單詞的原樣,淹沒了無關的單詞(例如,將它們乘以0.001之類的微小數字)。

第六步,value向量加權求和。這會在此位置(對於第一個單詞)產生自注意力層的輸出。

這就是自注意力計算的結論。得到的向量是我們可以發送到前饋神經網路的向量。在實際的實現中,為了快速計算,採用矩陣乘法來計算。因此,下面我們看一下矩陣是怎麼運算的。

自注意力中的矩陣運算

第一步,計算Query,Key和Value矩陣。我們通過把多個embedding向量整合成一個矩陣X,然後將矩陣X乘以訓練過的權重矩陣(WQ,WK,WV).

矩陣X中的每一行對應於輸入句子中的一個單詞。圖中的4個方塊在實際中表示512維,q/k/v向量是64維。

最終,由於我們用矩陣處理,我們可以在一個公式中把6個過程壓縮為2個來計算自注意力層的輸出。

多頭注意力(multi-headed)

這篇論文通過增加一種稱為「多頭注意力」機制,進一步完善了自注意力層。這在兩方面提高了注意力層的性能:

  1. 它增大了模型聚焦不同位置的能力。當然在上面的例子中,z1包含了一些其他詞的編碼,但它可以由實際的單詞本身支配。當我們翻譯句子「The animal didnt cross the street because it was too tired」,我們想知道it指代什麼?
  2. 它提供注意力層多重的「表示子空間」(representation subspaces)。正如我們將在下面看到的一樣,用多頭注意力,我們有多組Query/Key/Value權重矩陣(Transformer用了8個注意力頭,所以我們有8組編碼器和解碼器)。每一組都隨機初始化。然後經過訓練,每組用於將輸入嵌入(或來自較低編碼器/解碼器的向量)投影到不同的表示子空間。

利用多頭注意力,我們為每一個頭維護單獨的Q/K/V權重矩陣,得到不同的Q/K/V矩陣。正如我們在前面做的一樣,我們用X乘以WQ/WK/WV矩陣來產生Q/K/V矩陣。

如果我們進行上面概述的相同的自注意力計算,使用不同權重矩陣計算8次,我們最終得到8個不同的Z矩陣。

這讓我們面臨一些挑戰,前饋網路層並不期待8個矩陣--它期待一個單獨的矩陣(每個單詞一個向量)。所有我們需要一種方法把8個矩陣壓縮成一個矩陣。

我們怎麼做呢?我們將矩陣連接起來,然後將他們乘以另外的權重矩陣WO。

這就是多頭注意力的全部內容。這是一小部分矩陣,讓我嘗試將他們全部放在一個視覺中,這樣我們就可以在一個地方看到它們。

既然我們已經接觸到注意力頭,讓我們重新審視我們之前的例子,當我們在編碼it這個詞時,看看不同的注意力頭在哪裡聚焦:

當我們編碼單詞it時,一個注意力聚焦於"the animal",另外的聚焦於"tired"

如果我們把所有注意力頭加在圖片上,那麼看起來可能更難理解:

用Positional Encoding表示序列的順序

正如我們目前所描述的,該模型缺少統計輸入序列中單詞順序的方式。 要解決這個問題,transformer為每一個輸入嵌入增加了一個向量。這些向量遵循模型學習的特定模式,這有助於確定每個單詞的位置,或者序列中不同單詞之間的距離。這裡的直覺是,將這些value添加到嵌入中,一旦它們被投影到Q/K/V向量中並且在點積注意力之間,就在嵌入向量之間提供有意義的距離。

如果我們假設嵌入有4維,實際的位置編碼將如下所示:

所謂的特定模式是什麼? 在接下來的圖片中,每一行對應一個向量的位置編碼。因此,第一行就是輸入序列中的第一個單詞的嵌入向量。每一行包含512個值--每個值的取值介於1和-1之間。我們對它們進行了顏色編碼,使圖案可見。

一個真實位置編碼的例子,用512(列)維的嵌入編碼20(行)個單詞。你可以看到它在中心位置分成兩半。那是因為左半部分用一個sin函數生成的數值,右半部分是另一個函數(cos)生成的數值。然後他們拼接一塊形成每一個位置編碼向量。

位置編碼的公式在論文中3.5節描述。你可以在get_ timing_ signal_ld()看到位置編碼的源碼。不只有一種可能的位置編碼方法。然而,它具有能夠擴展到看不見的序列長度的優點。(例如,如果讓訓練過的模型翻譯一個比我們在訓練集中更長的句子)

殘差連接

編碼器結構中,我們需要提到的一個細節是每一個編碼器的每一個子層(self-attention,ffnn)在其周圍具有殘差連接,然後是層標準化(layer-normalization)步驟。

如果我們將向量和與自注意相關的層標準化可視化,它將如下所示:

這也適用於解碼器的子層。如果我們堆疊2個編碼器和解碼器的Transformer結構,它看起來像這樣:

解碼器端

現在我們已經涵蓋了編碼器方面的大多數概念,我們基本上知道了解碼器的組件如何工作。現在我們看一下它們是如何一起工作的。

編碼器通過處理輸入序列開始。頂端編碼器的輸出轉換為注意力向量K和V的集合。這些將由每個解碼器在其「編碼器-解碼器注意力」層中使用,這有助於解碼器關注輸入序列中的適當位置:

完成編碼階段,我們開始進入解碼階段。解碼階段中的每個步驟從輸出序列輸出一個元素。

接下來的步驟重複這個過程,直到一個特殊的符號到達表明transformer解碼器完成了輸出。每一步的輸出在下一個時間點被喂到底部的解碼器,解碼器就像編碼器一樣往上流動冒出解碼結果。就像我們對編碼器輸入所做的那樣,我們在這些解碼器輸入端嵌入並增加位置編碼,來指示每個單詞的位置。

在解碼器中自注意力層操作與在編碼器中的操作方式略有不同:

在解碼器中,自注意力層只被允許注意到輸出序列中早一些的位置。這是通過在自注意計算中的softmax步驟之前屏蔽未來位置(將它們設置為-inf)來完成的。

「編碼器--解碼器注意力」層像多頭自注意力一樣工作,除了它從下面的層創建它的Queries矩陣,和從編碼器輸出中獲取Keys和Value矩陣。

線性層和softmax層

解碼器輸出浮點型向量。我們怎麼把它轉化成一個單詞?這是最終線性層的工作,其後是softmax層。

線性層是一個簡單的全連接神經網路,把解碼器的輸出向量投影到一個更大、更大的logits向量。

我們假設我們的模型知道10000個獨特的單詞(模型的輸出辭彙表),這些單詞從訓練集中學習得到。這將使logits向量10000個cell寬--每個cell對應於一個唯一單詞的得分。這就是我們如何解釋模型的輸出,然後是線性層。

softmax層把這些得分轉換成概率(都是正數,和為1)。概率最高的cell被選中,並且與其關聯的單詞將作為此時間步的輸出。

該圖從底部開始,產生的矢量作為解碼器的輸出。然後它變成了一個輸出單詞。

訓練過程的扼要重述

既然我們通過訓練過的Transformer涵蓋了整個前向過程,看一下訓練模型的直覺是有用的。

在訓練過程中,一個未訓練過的模型會經歷同樣的前向過程。但是因為我們在一個標註的訓練集訓練它,我們可以比較模型的輸出和實際正確的輸出。

為了可視化這個過程,假設我們輸出辭彙只包含6個單詞(a,am,I,thanks,student,和end of sentence的縮寫)

模型的輸出辭彙在預處理階段被創建,這在我們開始訓練之前。

一旦我們定義好輸出辭彙,我們可以用一個同樣寬度的向量指示我們辭彙表中的每一個單詞。這也被稱為one-hot編碼。因此,舉個例子,我們可以用下面的向量指示單詞am:

下面接著討論一下模型的損失函數--在訓練過程中我們優化的準則,來訓練出訓練有素、令人驚奇的精確模型。

損失函數

假設我們正在訓練我們的模型。假設我們現在處在訓練階段的第一步,我們在一個簡單的例子上訓練它--把「merci」翻譯成「thanks」。

這意味著,我們想輸出一個概率分佈來指示單詞「thanks」。但是因為這個模型還沒有被訓練,這還不太可能發生。

因為模型的參數(權重)隨機初始化,(未訓練的)模型為每一個cell/word產生一個任意值的概率分佈。我們可以與真實的輸出比較,然後利用反向傳播法調整模型的權重,來保證輸出更接近期望的輸出。

我們怎麼比較兩個概率分佈呢?我們簡單的用一個分佈減去另一個。更多細節請看cross-entorpy和Kullback-Leibler divergence

需要注意的是,這是一個過度簡化的例子。更真實地,我們可以用一個長於一個單詞的句子。例如-輸入「je suis étudiant」期望輸出是「i am a student」。這實際意味著,我們希望我們的模型能夠連續輸出概率分佈,其中:

  • 每一個概率分佈用一個寬度為vocab_size(在下面的例子中為6,真實的例子中為3000或10000)的向量來表示。
  • 第一個概率分佈在與單詞「i」相關聯的單元處具有最高概率
  • 第二個概率分佈在與單詞「am」相關聯的單元處具有最高概率
  • 直到第5個輸出概率指示符號,即使在10000個元素的單詞表也有一個單元與它對應。

我們將在一個樣本句子的訓練示例中訓練我們模型的目標概率分佈。

在一個足夠大的數據集上訓練足夠的時間之後,我們希望產生的概率分佈會是這樣:

希望經過訓練,模型會輸出我們期望的正確的翻譯。當然,這個短語是否是訓練數據集的一部分沒有真正的指示(見cross validation)。請注意,即使不太可能是該時間步的輸出,每個位置都會獲得一點概率--這是softmax函數非常有用的特性,這有利於訓練過程。

現在,因為模型一次產生一個輸出,我們假設模型從那些概率分佈中選擇具有最大概率分佈的單詞,並扔掉其餘的部分。這是處理的一種方式(被稱為greedy decoding)。另一種處理的方式是假設保留兩個具有最高概率的單詞(假設單詞「I」和「a」),下一步,運行模型兩次:一次假設第一個輸出位置是單詞「I」,另一次假設第一個輸出單詞是「me」,考慮位置#1和位置#2,保留產生最少誤差的版本。我們在位置#2和位置#3重複同樣的操作,以此類推。這種方法稱為「beam search」,在我們的例子中,beam_size是2(因為我們在計算位置#1和位置#2的beams後比較了結果),並且top_beams也是2(因為我們保留了兩個單詞)。這些都是你可以試驗的超參數。

參考

翻譯自The Illustrated Transformer


推薦閱讀:
相關文章