譯自:https://jalammar.github.io/illustrated-transformer/
在之前的文章中作者介紹了Attention機制,在處理當下的深度學習任務中attention機制無處不在,他可以提升機器翻譯應用的表現。在接下來的這篇文章中作者將介紹Transformer,一個用attention加速並且可訓練的模型。在一些特定的任務上Transformer模型的表現效果要比Google Neural Machine Translation的效果還要好,其最大的優點是它的並行性。實際上 Google Cloud’s推薦將Transformer作為Cloud TPU的推導模型。現在我們將Transformer拆解開來看看它是如何工作的。
Transformer在文章 Attention is All You Need中提出,其中的TensorFlow中的Transformer應用的是Tensor2Tensor的子模塊。哈佛的NLP團隊專門制作了對應的PyTorch的指南說明。本文旨在簡化難度,一步一步地解釋其中的概念,希望有助於初學者更容易地理解。
A High-Level Look
我們先將Transformer視為一個黑箱,在一個機器翻譯的應用中,輸入應該是一種語言的句子,輸出是翻譯過后的另一種語言的句子。
黑盒下是由encoding部分,decoding部分還有他們之間的連接組成的。
encoding部分是由一系列的encoder所組成的(本例中堆疊了6個encoder),decoding部分堆疊相同數量的decoder。
上述的encoders擁有相同的結構(但是,它們不共享權重)。每一個encoder可以分解成兩個子層:
encoder的輸入首先會傳入進一個Self-Attention層,該層幫助encoder能夠看到輸入序列中的其他單詞當它編碼某個詞時。后面,我們會細看self-attention的內部結構。Self-Attention的輸出會傳遞到前饋神經網中,每個對應位置的前饋神經網絡是相互獨立的。
deocder也同樣地擁有這兩層,不同的是,decoder中Self-Attention和前饋神經網絡之間還有一層Attention結構,該Attention使decoder將注意力集中在輸入句子的相關部分上(與seq2seq models的attention作用相似)。
Bringing The Tensors Into The Picture
我們已經了解了transformer的關鍵組成部分,下面讓我們把vectors/tensors帶入其中,看看輸入是如何通過各個模塊計算出輸出的。
與一般的NLP任務類似,我們需要用embedding algorithm將輸入單詞轉換為向量。
每個單詞都被嵌入成一個512維的向量,我們用上圖的形式來表示向量
詞嵌入的過程只在最低端的encoder中進行。這樣每個encoder的都會接收到一個list(其中每個元素都是512維的詞嵌入表示),最底層的encoder的輸入是word embedding,其他encoder的輸入是前個encoder的輸出。list的尺寸是可以設置的超參,通常是訓練集的最長句子的長度。
在對輸入序列做詞嵌入表示之后,它們會傳入encoder的兩個子層。
這里能看到Transformer的一個關鍵特性,每個位置的詞僅僅流過它自己的encoder路徑。在self-attention層中,這些路徑兩兩之間是相互依賴的。前向網絡層則沒有這些依賴性,但這些路徑在流經前饋神經網絡時可以並行執行。
接下來我們會拿短句進行舉例,我們會展示encoder的子層到底進行了什么操作。
Now We’re Encoding!
encoder接受一組向量作為輸入,先經過一個self-attention層,之后傳入進一個前饋神經網絡之中,之后將輸出傳給下一個encoder。
每個位置的詞向量被送入self-attention模塊,然后是前饋神經網絡(每個向量都流經完全相同的網絡結構)
Self-Attention at a High Level
不要被self-attention這個詞的表面意思所迷惑,看起來好像每個人都對他很熟悉,但是在讀到Attention is all you need這篇文章的之前,下面我們將逐步講解它是如何工作的。以下面我們想翻譯的這句話為例,”The animal didn't cross the street because it was too tired
”,在這句話里“it”指的是什么?“it”指的是“street”還是“animal”,這對人來說是一個簡單的問題,但是它對算法來說是不簡單。在模型處理“it”時,self-attention會將“it”與“animal”關聯起來。
在模型處理每個單詞時(輸入序列的每一個位置),self-attention允許模型看到輸入句子的其他位置信息作輔助線索來更好地編碼當前詞。如果你對RNN熟悉,就能想到RNN的隱狀態是如何允許之前的詞向量來解釋合成當前詞的解釋向量。Transformer使用self-attention來將相關詞的理解編碼到當前詞中。
當編碼"it"時(編碼器的最后層輸出),部分attention集中於"the animal",並將其表示合並進入到“it”的編碼中
上圖是Tensor2Tensor notebook的可視化例子
Self-Attention in Detail
我們先看下如何利用向量計算self-attention,再看下如何以矩陣方式計算。
第一步,根據encoder的輸入向量(在本例中,是每個單詞的詞嵌入表示),生成三個向量,對每個詞向量,生成Query-vector, Key-vector, Value-vector,生成方法為當前詞的詞嵌入表示分別乘以三個矩陣,這些矩陣在訓練過程中需要學習。【注意:不是每個詞向量獨享3個matrix,而是所有輸入共享3個轉換矩陣;權重矩陣是基於輸入位置的轉換矩陣;有個可以嘗試的點,如果每個詞獨享一個轉換矩陣,會不會效果更厲害呢?】
注意到這些新向量的維度比輸入詞的詞嵌入向量的維度要小(512–>64),它們的維度是64維,然而詞嵌入表示,encoder的輸入\輸出是512維,並不是必須要小的,這是為了讓多頭attention的計算更穩定。
利用x1與權重矩陣WQ相乘得到q1,Query-vector會與這個詞產生關聯。我們最終會為輸入句子中的每一個單詞創建一個Query-vector, 一個Key-vector, 一個Value-vector。
所謂的Query/Key/Value-vector是什么?
這種抽象化的表示對計算和思考attention是有益的,當你讀完下面attention是如何計算的之后,你將對這些向量的角色有更清晰的了解。
第二步,計算attention就是計算一個分值。對“Thinking Matchines”這句話,對“Thinking”(pos#1)計算self-attention 分值。我們需要計算輸入句子中每個詞與“Thinking”的評估分,這個分決定着編碼“Thinking”時(某個固定位置的單詞時),對每個輸入詞需要集中多少關注度。
這個分,通過“Thing”對應query-vector與所有詞的key-vector依次做點積得到。所以當我們計算位置#1的self-attention時,第一個分值是q1和k1的點積,第二個分值是q1和k2的點積。
第三、四步,除以8(8是由文中key-vector的維度64開根號得到的,當然還可以默認其他數值),這樣梯度會更穩定。然后加上softmax操作,歸一化分值使得全為正數且加和為1。
softmax分值決定着在這個位置,每個詞的表達程度(關注度)。很明顯,在這個位置的詞應該有最高的歸一化分數,但有些時候會有助於關注該詞的相關的詞。
第五步,將softmax分值與value-vector按位相乘。保留關注詞的value值,削弱非相關詞的value值。
第六步,將所有加權向量加和,產生該位置的self-attention的輸出結果。
上述就是self-attention的計算過程,生成的向量流入前向網絡。在實際應用中,上述計算是以速度更快的矩陣形式進行的。下面我們看下在單詞級別的矩陣計算。
Matrix Calculation of Self-Attention
第一步,計算Query/Key/Value matrix,將所有輸入詞向量合並成輸入矩陣X,並且將其分別乘以權重矩陣Wq,Wk,Wv。
輸入矩陣X的每一行表示輸入句子的一個詞向量。可以看到這個運算是一個降維的過程。(512->64,圖中是4->3)
最后,鑒於我們使用矩陣處理,將步驟2~6合並成一個計算self-attention層輸出的公式。
矩陣形式的self-attention計算過程
The Beast With Many Heads
論文進一步增加了multi-headed的機制到self-attention上,在如下兩個方面提高了attention層的效果:
- multi-headed機制擴展了模型集中於不同位置的能力。在上面的例子中,z1實際大部分由自己詞所決定,只包含了其他詞的很少信息。在翻譯 “The animal didn’t cross the street because it was too tired”時,當我們想知道單詞"it"指的是什么,這個模型會起到作用。
- multi-headed機制賦予attention多種子表達方式。像下面的例子所示,在multi-headed attention下有多組Query/Key/Value權重矩陣,而非僅僅一組(論文中使用8-heads,所以針對每組encoder/decoder我們都設計8組權重矩陣)。每一組都是隨機初始化。經過訓練之后,每一組都可以將輸入向量(inputs embedding)映射到不同的子表達空間中。
每個head都有一組WQ/WK/WV matrix,以生成不同的Q/K/V
如果我們計算multi-headed的self-attention,會分別有八組不同的Q/K/V matrix,我們會得到八個不同的Z。
這么多的矩陣這會帶來點麻煩,前向網絡並不能接收8個矩陣,而是希望輸入是一個矩陣(每個單詞一個向量表示),所以我們需要一種可以將8個矩陣合並成1個矩陣的方法。
我們應該怎么做呢?我們concat上述8個矩陣,並將其與一個額外的矩陣WO相乘。
上述就是multi-headed self-attention的內容,我認為還僅是一部分矩陣,下面嘗試着將它們放到一個圖上,可視化如下:
現在加入multi-head attention之后,重新看下當編碼“it”時,attention head會被集中到哪里?
編碼"it"時,一個attention head集中於"the animal",另一個head集中於“tired”,某種意義上講,模型對“it”的表達融合了的“animal”和“tired”兩者
如果我們將所有的attention heads都放入到圖中,就很難直觀地解釋了:
Representing The Order of The Sequence Using Positional Encoding
截止到目前為止,我們還沒有討論如何理解輸入語句中詞的順序。
為解決詞序的利用問題,Transformer對輸入中的每個詞都新增了一個向量,這些向量遵循模型學習的指定模式,來決定詞的位置,或者序列中不同詞的距離。對其理解,當其映射到Q/K/V向量以及點乘的attention時,增加這些值來提供詞向量間的距離。
為了能夠給模型提供詞序的信息,新增位置encoding向量,每個向量值都遵循指定模式
如果假設embedding有4維,實際的位置向量將如下所示:
一個只有4維的位置向量表示例子
所謂的指定模式是什么樣的呢?
在下圖中,每一行代表一個向量的positional-encoding,所以第一行是我們將要加到句子中第一個詞向量上的vector。每行有512值,每個值范圍在[-1,1],我們將其塗色以便於能夠將模式可視化。
一個真實的例子有20個詞(行),每個詞512維(列)。可以觀察中間具有明顯的分隔,那是因為左側是用sine函數生成,右側是用cosine生成,之后將他們concat。
位置向量編碼方法在論文的3.5節有提到,也可以看代碼get_timing_signal_ld(),對位置編碼而言並不只有這一種方法。需要注意的是,編碼方法必須能夠處理未知長度的序列。
上面展示的pos-encoding圖是Transformer2Transformer的實施。文章中的方法略有不同,文中的方法沒有直接進行concat,是交替地整合兩組信號,下圖進行了展示。Here’s the code to generate it:
The Residuals
encoder中值得提出注意的一個細節是,在每個子層中(slef-attention, ffnn),都有殘差連接,並且緊跟着layer-normalization。
如果我們可視化向量和layer-norm操作,將如下所示:
decoder中的子層也是如此,2層encoder+2層decoder組成的Transformer結構如下:
The Decoder Side
現在我們已經了解了encoder側的大部分概念,也基本了解了decoder的工作方式,下面看下他們是如何共同工作的。
encoder從輸入序列的處理開始,最后的encoder的輸出被轉換為一組attention向量K和V,它倆被每個decoder的"encoder-decoder atttention"層來使用,幫助decoder集中於輸入序列的合適位置。
在編碼之后,是解碼過程;解碼的每一步輸出一個元素作輸出序列
下面的步驟一直重復直到一個特殊符號出現表示transformer的decoder完成了翻譯輸出。每一步的輸出被喂到下一個decoder中,與encoders一樣decoders以冒泡的形式傳遞decoding的結果。與對encoder的輸入所做的處理一樣,對decoder的輸入詞嵌入增加pos-encoding以表明decoder輸入的每個詞的位置。
decoder中的self attention層與encoder中的稍有不同:
在decoder中,self-attention層僅關注早於當前輸出位置的單詞。在softmax之前,通過遮擋未來位置(將它們設置為-inf)來實現。
"Encoder-Decoder Attention "層工作方式跟multi-headed self-attention是一樣的,除了一點,它從前層獲取輸出生成Query矩陣,接收最后的encoder層的Key和Value矩陣做Key和Value矩陣。
The Final Linear and Softmax Layer
decoder最后輸出浮點向量,如何將它轉成詞?這是最后的Linear層和Softmax層的主要工作。
Linear層是個簡單的全連接層,將decoder的最后輸出映射到一個非常大的logits向量上。
假設模型已知有1萬個單詞(輸出的詞表)從訓練集中學習得到。那么,logits向量就有1萬維,每個值表示是某個詞的可能傾向值,這就是線性層應該做的。softmax層將這些分數轉換成概率值(都是正值,且加和為1),最大值對應的單詞就是這一步的輸出單詞。
圖中從decoder的最后的輸出開始,直到最后得到一個輸出單詞
Recap Of Training
現在我們已經了解了一個訓練完畢的Transformer的前向過程,順道看下如何訓練模型也是非常有用的。
在訓練時,未訓練的模型將經歷上述的前向過程,當我們在標記訓練集上訓練時,可以對比預測輸出與實際輸出。
為了可視化,假設我們的輸出詞表一共只有6個單詞(“a”, “am”, “i”, “thanks”, “student”, “<eos>”(句子結尾的標志))
在訓練之前需要把輸出詞典進行預處理。
一旦定義了詞表,我們就能夠構造一個同維度的向量來表示詞表中的每個單詞,比如one-hot編碼,下面拿“am”進行編碼舉例。
舉例采用one-hot編碼輸出詞表
下面讓我們討論下模型的loss損失,在訓練過程中用來優化的指標,指導學習得到一個非常准確的模型。
The Loss Function
我們用一個簡單的例子來示范訓練,比如翻譯“merci”為“thanks”。那意味着輸出的概率分布指向單詞“thanks”,但是由於模型未訓練是隨機初始化的,不太可能就是期望的輸出。
由於模型參數是隨機初始化的,未訓練的模型輸出隨機值。我們可以對比真實輸出,然后利用誤差反向傳播算法調整模型權重,使得模型輸出與真實輸出更接近
如何對比兩個概率分布呢?簡單采用 cross-entropy或者Kullback-Leibler divergence中的一種。
鑒於這是個極其簡單的例子,更真實的情況是,我們會使用一個句子作為輸入。比如,輸入是“je suis étudiant”,期望輸出是“i am a student”。在這個例子下,我們期望模型輸出連續的概率分布滿足如下條件:
1 每個概率分布都與詞表同維度。(圖中的例子是6,但實景情況下會是30000或者50000)。
2 第一個概率分布對“i”具有最高的預測概率值。
3 第二個概率分布對“am”具有最高的預測概率值。
4 等等,一直到第五個輸出指向"<end of sentence>"標記。<eos>標記也在詞典中。
對一個句子而言,訓練模型的目標概率分布
在足夠大的訓練集上訓練足夠時間之后,我們期望產生的概率分布如下所示:
訓練好之后,模型的輸出是我們期望的翻譯。當然,這並不意味着這一過程是來自訓練集(詳見: cross validation).。
注意,每個位置都有概率值,即便與輸出無關,這也是softmax對訓練有幫助的地方。
現在,因為模型每步只產生一組輸出,假設模型從詞表的概率分布中選擇最高概率的單詞,扔掉其他的部分,這是種產生預測結果的方法,叫做greedy 解碼。
另外一種方法是beam search,每一步僅保留最頭部高概率的兩個輸出(比如說‘I’和‘a’),再根據這倆輸出再預測下一步的輸出(會執行兩遍,一次假設第一個輸出是‘I’,第二次假設第一個輸出是‘a’),兩種假設都會被保留,無論哪個版本產生的錯誤都更少,再保留頭部高概率的兩個輸出。重復直到預測結束。在我們的實驗中。beam_size被設置成了2(這意味着在任何時候都將兩個部分假設(未完成的翻譯)保留在內存中),top_beams也被設置成了2(表示我們將生成;兩份翻譯結果),beam_size和top_beams都是可以通過實驗調整的超參數。
Go Forth And Transform
希望本文能夠幫助讀者對Transformer的主要概念理解有個破冰效果,如果想更深入了解,建議如下步驟:
1 閱讀 Attention Is All You Needpaper,Transformer的博客文章Transformer: A Novel Neural Network Architecture for Language Understanding,Tensor2Tensor使用說明。
2 觀看"Łukasz Kaiser’s talk",梳理整個模型及其細節。
3 做一下項目Jupyter Notebook provided as part of the Tensor2Tensor repo
4 嘗試下項目Tensor2Tensor repo
相關工作:
- Depthwise Separable Convolutions for Neural Machine Translation
- One Model To Learn Them All
- Discrete Autoencoders for Sequence Models
- Generating Wikipedia by Summarizing Long Sequences
- Image Transformer
- Training Tips for the Transformer Model
- Self-Attention with Relative Position Representations
- Fast Decoding in Sequence Models using Discrete Latent Variables
- Adafactor: Adaptive Learning Rates with Sublinear Memory Cost