【翻譯】圖解transformer

語言: CN / TW / HK

theme: cyanosis

寫在最前邊

看transformer相關文章的時候發現很多人用了相同的圖。直到我搜到原作……於是去申請翻譯了。

image.png

作者博客:@Jay Alammar

原文鏈接:The Illustrated Transformer – Jay Alammar – Visualizing machine learning one concept at a time. (jalammar.github.io)


翻譯講究:信、達、雅。要在保障意思準確的情況下傳遞作者的意圖,並且儘量讓文本優美。 但是大家對我一個理工科少女的語言要求不要太高,本文只能保證在儘量通順的情況下還原原文。

注意本文的組成部分:翻譯 + 我的註釋。

添加註釋是因為在閲讀的過程中,我感覺有的地方可能表述的並不是特別詳細,對於一些真正的小白,像我一樣傻的來説,可能不太好理解。


正文

在之前的文章中,我們講了現代神經網絡常用的一種方法——Attention機制。

本文章我們來介紹一下Transformer——用注意力機制來提高模型訓練速度的模型。 Transformer在某些特定任務上性能比谷歌的機器翻譯模型更為優異。其優點在於並行化計算。並且谷歌雲也推薦使用transformer作為參考模型來運行他們的TPU雲服務。所以讓我們來把它拆解開看一下它是如何運行的。

Transformer是在這篇論文中提出的:Attention is All You Need.

官方版TensorFlow實現:GitHub - tensorflow/tensor2tensor: Library of deep learning models and datasets designed to make deep learning more accessible and accelerate ML research.

哈佛大學NLP組Pytorch實現:The Annotated Transformer (harvard.edu)

在本文中,我們會逐個概念進行介紹,希望能幫助沒接觸過Transformer的人能夠更容易的理解。

從高層面看

我們先把整個Transformer模型看作是一個黑盒。在機器翻譯中,它可以把句子從一種語言翻譯成另一種語言。

image.png

打開這個黑盒,我們首先可以看到一個編碼器(encoder)模塊和一個解碼器(decoder)模塊,以及二者之間存在某種關聯。

image.png

作者原文説的是“Popping open that Optimus Prime goodness”,也就是“噢我的老天鵝讓我們打開這個擎天柱看一下。”

擎天柱:???你在叫我嗎?

image.png

我之前看過一個大佬解釋説transformer其實取的是“變壓器”這個意象,但是因為變形金剛這個印象好像更為廣為人知一點,所以人們提到transformen一般想到的都是變形金剛,作者在這裏就直接默認是擎天柱了。 這倒我想起來微軟和NVIDIA還合作了一個威震天(Megatron Turing—NLG),感興趣的可以自己去了解一下。 CSDN @LolitaAnn

再往裏看一下,編碼器模塊是6個encoder組件堆在一起,同樣解碼器模塊也是6個decoder組件堆在一起。(為什麼選6個呢?沒有什麼原因,論文原文就是這麼寫的,你也可以換成別的層數)

在這裏這個encoder和decoder中的基礎組件個數屬於超參數,可以自己嘗試設置不同的值。這是Transformer中第一個可調參數。

image.png

6個編碼器組件的結構是相同的(但是他們之間的權重是不共享的),每個編碼器都可以分為2個子層。

image.png

編碼器的輸入首先會進入一個自注意力層,這個注意力層的作用是:當要編碼某個特定的詞彙的時候,它會幫助編碼器關注句子中的其他詞彙。之後會進行詳細講解。

自注意力層的輸出會傳遞給一個前饋神經網絡,每個編碼器組件都是在相同的位置使用結構相同的前饋神經網絡。

這裏要注意一點,在前面我們提到6個組件之間的參數是不共享的。在這裏雖然從自注意力層出來的結果進入相同的前饋神經網絡。但是這裏的相同僅限於結構相同,它們的參數是不同的。

解碼器組件也含有前面編碼器中提到的兩個層,區別在於這兩個層之間還夾了一個注意力層,多出來的這個自注意力層的作用是讓解碼器能夠注意到輸入句子中相關的部分(和seq2seq中的attention一樣的作用)。

image.png

圖解張量

現在我們要開始瞭解整個模型了。

在一個已經訓練好的Transformer模型中,輸入是怎麼變為輸出的呢? 首先我們要知道各種各樣的張量或者向量是如何在這些組件之間變化的。

與其他的NLP項目一樣,我們首先需要把輸入的每個單詞通過詞嵌入(embedding)轉化為對應的向量。

在這裏插入圖片描述

在原文中每個詞的嵌入向量是512維,這裏為了便於理解,就用這幾個格子進行表示。

雖然這裏只畫了4個格子,但是我們用4個格子表示512的維度,不是説用4個格子表示四維。

所有編碼器接收一組向量作為輸入,論文中的輸入向量的維度是512。最底下的那個編碼器接收的是嵌入向量,之後的編碼器接收的是前一個編碼器的輸出。

向量長度這個超參數是我們可以設置的,一般來説是我們訓練集中最長的那個句子的長度。

這是Transformer中第二個超參數,我們可以自行設置。到這裏嚴格意義上講,Transformer的超參數都出來了。

“這個簡單的設計影響到後面一系列的網絡,BERT啊、GPT啊其實只有兩個參數可以調的。”——李沐

當我們的輸入序列經過詞嵌入之後得到的向量會依次通過編碼器組件中的兩個層。

在這裏插入圖片描述

在這裏,我們開始看到Transformer的一個關鍵屬性,即每個位置上的單詞在編碼器中有各自的流通方向。在自注意力層中,這些路徑之間存在依賴關係。 然而,前饋神經網絡中沒有這些依賴關係,因此各種路徑可以在流過前饋神經網絡層的時候並行計算。

接下來,我們用一個短句(Thinking Machine)作為例子,看看在編碼器的每個子層中發生了什麼。

現在我們來看一下編碼器

上邊我們已經説了,每個編碼器組件接受一組向量作為輸入。在其內部,輸入向量先通過一個自注意力層,再經過一個前饋神經網絡,最後將其將輸出給下一個編碼器組件。

image.png

不同位置上的單詞都要經過自注意力層的處理,之後都會經過一個完全相同的前饋神經網絡。

自注意力

不要一看“self-attention”就覺得這是個每個人都很很熟悉的詞,其實我個人感覺,在看《Attention is all you need》之前我都沒有真正理解自注意力機制。現在讓我們看一下自注意力機制。

假設我們要翻譯下邊這句話:

“The animal didn't cross the street because it was too tired”

這裏it指的是什麼?是street還是animal?人理解起來很容易,但是對算法來講就不那麼容易了。

當模型處理it這個詞的時候,自注意力會讓itanimal關聯起來。

當模型編碼每個位置上的單詞的時候,自注意力的作用就是:看一看輸入句子中其他位置的單詞,試圖尋找一種對當前單詞更好的編碼方式。

如果你熟悉RNNs模型,回想一下RNN如何處理當前時間步的隱藏狀態:將之前的隱藏狀態與當前位置的輸入結合起來。 在Transformer中,自注意力機制也可以將其他相關單詞的“理解”融入到我們當前處理的單詞中。

image.png

當我們在最後一個encoder組件中對it進行編碼的時候,注意力機制會更關注The animal,並將其融入到it的編碼中。

可以去Tensor2Tensor ,自己體驗一下上圖的可視化。

細説自注意力機制

先畫圖用向量解釋一下自注意力是怎麼算的,之後再看一下實際實現中是怎麼用矩陣算的。

第一步 對編碼器的每個輸入向量都計算三個向量,就是對每個輸入向量都算一個query、key、value向量。 怎麼算的? 把輸入的詞嵌入向量與三個權重矩陣相乘。權重矩陣是模型訓練階段訓練出來的。

在同一個編碼器組件中,一組輸入向量使用相同的權重矩陣,就是所有輸入都用一樣的$W^Q、W^K、W^V$)

注意,這三個向量維度是64,比嵌入向量的維度小,嵌入向量、編碼器的輸入輸出維度都是512。這三個向量不是必須比編碼器輸入輸出的維數小,這樣做主要是為了讓多頭注意力的計算更穩定。

image.png

將 $x_{1}$ 和 ${W}^{Q}$ 權重矩陣相乘得到 ${q}{1}$, 就得到與該單詞 $\left({x}{1}\right)$ 相關的查詢(query)。 按這樣的方法,最終我們給輸入的每一個單詞都計算出一個“query”、一個 “key"和一個 “value"。

什麼是 “query”、“key”、“value” 向量?

這三個向量是計算注意力時的抽象概念,繼續往下看注意力計算過程,看完了就懂了。

第二步 計算注意力得分。

假設我們現在在計算輸入中第一個單詞Thinking自注意力。我們需要使用自注意力給輸入句子中的每個單詞打分,這個分數決定當我們編碼某個位置的單詞的時候,應該對其他位置上的單詞給予多少關注度。

這個得分是query和key的點乘積得出來的。

舉個栗子,我們要算第一個位置的注意力得分的時候就要將第一個單詞的query和其他的key依次相乘,在這裏就是$q_1·k_1$,$q_1·k_2$。

image.png

第三步 將計算獲得的注意力分數除以8。

為什麼選8?是因為key向量的維度是64,取其平方根,這樣讓梯度計算的時候更穩定。默認是這麼設置的,當然也可以用其他值。

第四步 除8之後將結果扔進softmax計算,使結果歸一化,softmax之後注意力分數相加等於1,並且都是正數。

image.png

這個softmax之後的注意力分數表示 在計算當前位置的時候,其他單詞受到的關注度的大小。顯然在當前位置的單詞肯定有一個高分,但是有時候也會注意到與當前單詞相關的其他詞彙。

第五步 將每個value向量乘以注意力分數。 這是為了留下我們想要關注的單詞的value,並把其他不相關的單詞丟掉。

在第一個單詞位置得到新的$v_1$。

第六步 將上一步的結果相加,輸出本位置的注意力結果。

第一個單詞的注意力結果就是$z_1$

image.png

這就是自注意力的計算。計算得到的向量直接傳遞給前饋神經網絡。但是為了處理的更迅速,實際是用矩陣進行計算的。接下來我們看一下怎麼用矩陣計算。

用矩陣計算self-attention

計算Query, Key, Value矩陣。直接把輸入的向量打包成一個矩陣$X$,再把它乘以訓練好的$W^Q$$W^K$$W^V$

X矩陣每一行都代表輸入句子中的一個詞,整個矩陣代表輸入的句子。

image.png

$X$矩陣中的一行相當於輸入句子中的一個單詞。 我們看一下維度的差異:原文中嵌入矩陣的長度為 512 , $q、k、v$ 矩陣的長度為 64 ; 在這裏我們分別用 4 個格子表示和3個格子表示。

因為我們現在用矩陣處理,所以可以直接將之前的第二步到第六步壓縮到一個公式中一步到位獲得最終的注意力結果$Z$。

image.png

補充一個有趣的知識。注意力的計算方法不止這一種,常見的有點積注意力、加性注意力等。但是論文中用的是點積注意力。作者的理由是:“簡單”。雖然二者難度是差不多的,但是點積注意力計算起來更快,代碼寫起來更方便。

多頭注意力

論文進一步改進了自注意力層,增加了一個機制,也就是多頭注意力機制。這樣做有兩個好處: 1. 它擴展了模型專注於不同位置的能力。

在上面例子裏只計算一個自注意力的的例子中,編碼“Thinking”的時候,雖然最後<font color = pink>$Z_1$</font>或多或少包含了其他位置單詞的信息,但是它實際編碼中還是被“Thinking”單詞本身所支配。

如果我們翻譯一個句子,比如“The animal didn’t cross the street because it was too tired”,我們會想知道“it”指的是哪個詞,這時模型的“多頭”注意力機制會起到作用。

> 我個人理解:每個詞最終的編碼都是主要受該單詞本身的影響,雖然關注到其他位置的詞,但是它的關注點可能和你想要的點不一樣。但是你有多head的時候就好辦了,一個詞一共才多少種含義,總能有一個關注到我想的方面吧。作者舉it這個例子,如果你現在感覺看的有點迷惑沒關係,看完這一小節最後那兩張多頭自注意力的可視化就理解了。
  1. 它給了注意層多個“表示子空間”。

    就是在多頭注意力中同時用多個不同的$W^Q$$W^K$$W^V$權重矩陣(Transformer使用8個頭部,因此我們最終會得到8個計算結果),每個權重都是隨機初始化的。經過訓練每個$W^Q$$W^K$$W^V$都能將輸入的矩陣投影到不同的表示子空間。

image.png

在多頭注意力中, 我們給每個頭單獨的權重矩陣, 從而產生不同的Q、 K 、 矩陣。

Transformer中的一個多頭注意力(有8個head)的計算,就相當於用自注意力做8次不同的計算,並得到8個不同的結果$Z$。

image.png

但是這會存在一點問題,多頭注意力出來的結果會進入一個前饋神經網絡,這個前饋神經網絡可不能一下接收8個注意力矩陣,它的輸入需要是單個矩陣(矩陣中每個行向量對應一個單詞),所以我們需要一種方法把這8個壓縮成一個矩陣。

怎麼做呢?我們將這些矩陣連接起來,然後將乘以一個附加的權重矩陣$W^O$

image.png

以上就是多頭自注意力的全部內容。讓我們把多頭注意力上述內容 放到一張圖裏看一下子:

在這裏插入圖片描述

現在我們已經看過什麼是多頭注意力了,讓我們回顧一下之前的一個例子,再看一下編碼“it”的時候每個頭的關注點都在哪裏:

image.png

編碼it,用兩個head的時候:其中一個更關注the animal,另一個更關注tired。 此時該模型對it的編碼。除了it本身的表達之外,同時也包含了the animal和tired的相關信息

如果我們把所有的頭的注意力都可視化一下,就是下圖這樣,但是看起來事情好像突然又複雜了。

image.png

使用位置編碼表示序列的位置

強烈安利一個詳細解釋位置編碼的文章Transformer的位置編碼詳解 - 掘金 (juejin.cn)

到現在我們還沒提到過如何表示輸入序列中詞彙的位置。

Transformer在每個輸入的嵌入向量中添加了位置向量。這些位置向量遵循某些特定的模式,這有助於模型確定每個單詞的位置或不同單詞之間的距離。將這些值添加到嵌入矩陣中,一旦它們被投射到Q、K、V中,就可以在計算點積注意力時提供有意義的距離信息。

image.png

為了讓模型能知道單詞的順序,我們添加了位置編碼,位置編碼是遵循某些特定模式的。

位置編碼向量和嵌入向量的維度是一樣的,比如下邊都是四個格子:

image.png

舉個例子,當嵌入向量的長度為4的時候,位置編碼長度也是4

一直説位置向量遵循某個模式,這個模式到底是什麼。

在下面的圖中,每一行對應一個位置編碼。所以第一行就是我們輸入序列中第一個單詞的位置編碼,之後我們要把它加到詞嵌入向量上。

看個可視化的圖:

image.png

這裏表示的是一個句子有20個詞,詞嵌入向量的長度為512。 可以看到圖像從中間一分為二,因為左半部分是由正弦函數生成的。右半部分由余弦函數生成。 然後將它們二者拼接起來,形成了每個位置的位置編碼。

你可以在get_timing_signal_1d()中看到生成位置編碼的代碼。 這不是位置編碼的唯一方法。但是使用正餘弦編碼有諸多好處,具體可以看這裏:Transforme 結構:位置編碼詳解

但是需要注意注意一點,上圖的可視化是官方Tensor2Tensor庫中的實現方法,將sin和cos拼接起來。但是和論文原文寫的不一樣,論文原文的3.5節寫了位置編碼的公式,論文不是將兩個函數concat起來,而是將sin和cos交替使用。論文中公式的寫法可以看這個代碼:transformer_positional_encoding_graph,其可視化結果如下:

image.png

這裏表示的是一個句子有10個詞,詞嵌入向量的長度為64。

殘差

在繼續往下講之前,我們還需再提一下編碼器中的一個細節:每個編碼器中的每個子層(自注意力層、前饋神經網絡)都有一個殘差連接,之後是做了一個層歸一化(layer-normalization)。

image.png

將過程中的向量相加和layer-norm可視化如下所示:

image.png

當然在解碼器子層中也是這樣的。

我們現在畫一個有兩個編碼器和解碼器的Transformer,那就是下圖這樣的:

image.png

解碼器

現在我們已經介紹了編碼器的大部分概念,(因為encoder的decoder組件差不多)我們基本上也知道了解碼器的組件是如何工作的。那讓我們直接看看二者是如何協同工作的。

編碼器首先處理輸入序列,將最後一個編碼器組件的輸出轉換為一組注意向量K和V。每個解碼器組件將在“encoder-decoder attention”層中使用編碼器傳過來的K和V,這有助於解碼器將注意力集中在輸入序列中的適當位置: 在這裏插入圖片描述

完成編碼階段後,我們開始進行解碼階段。
在解碼階段每一輪計算都只往外蹦一個輸出,在本例中是輸出一個翻譯之後的英語單詞。

解碼器不是咔咔咔一個句子一下給你輸出出來,是每次只輸出一個詞!!!

輸出步驟會一直重複,直到遇到句子結束符 表明transformer的解碼器已完成輸出。

每一步的輸出都會在下一個時間步餵給給底部解碼器,解碼器會像編碼器一樣運算並輸出結果(每次往外蹦一個詞)。

跟編碼器一樣,在解碼器中我們也為其添加位置編碼,以指示每個單詞的位置。

在這裏插入圖片描述

解碼器中的自注意力層和編碼器中的不太一樣:

在解碼器中,自注意力層只允許關注已輸出位置的信息。實現方法是在自注意力層的softmax之前進行mask,將未輸出位置的信息設為極小值。

“encoder-decoder attention”層的工作原理和前邊的多頭自注意力差不多,但是Q、K、V的來源不用,Q是從下層創建的(比如解碼器的輸入和下層decoder組件的輸出),但是其K和V是來自編碼器最後一個組件的輸出結果。

編碼器是對整個輸入序列進行編碼嘛,然後將其結果轉化成K和V傳給解碼器,這個K、V包含整個句子的所有信息。但是解碼器的輸入是什麼?是拼接之前解碼器的輸出的單詞。所以解碼器造出來的Q僅包含已經輸出的內容。

最後的線性層和softmax層

Decoder輸出的是一個浮點型向量,如何把它變成一個詞?

這就是最後一個線性層和softmax要做的事情。

線性層就是一個簡單的全連接神經網絡,它將解碼器生成的向量映射到logits向量中。 假設我們的模型詞彙表是10000個英語單詞,它們是從訓練數據集中學習的。那logits向量維數也是10000,每一維對應一個單詞的分數。

然後,softmax層將這些分數轉化為概率(全部為正值,加起來等於1.0),選擇其中概率最大的位置的詞彙作為當前時間步的輸出。

image.png

這張圖從下往上看,假設具體上的那個向量是解碼器的輸出,然後將其轉換為最終輸出的單詞。

訓練過程概述

現在我們已經瞭解了Transformer的整個前向傳播的過程,那我們繼續看一下訓練過程。 在訓練期間,未經訓練的模型會進行相同的前向傳播過程。由於我們是在有標記的訓練數據集上訓練它,所以我們可以將其輸出與實際的輸出進行比較。

為了便於理解,我們假設預處理階段得到的詞彙表只包含六個單詞(“a”, “am”, “i”, “thanks”, “student”, “\<eos>”)。

image.png

注意這個詞彙表是在預處理階段就創建的,在訓練之前就已經得到了。

一旦我們定義好了詞彙表,我們就可以使用長度相同的向量(獨熱碼,one-hot 向量)來表示詞彙表中的每個單詞。例如,我們可以用以下向量表示單詞“am”:

image.png

接下來讓我們討論一下模型的損失函數,損失函數是我們在訓練階段優化模型的指標,通過損失函數,可以幫助我們獲得一個準確的、我們想要的模型。

損失函數

假設我們正要訓練我們的模型。

假設現在是訓練階段的第一步,我們用一個簡單的例子(一個句子就一個詞)來訓練模型:把“merci” 翻譯成 “thanks”。 這意味着,我們希望輸出是表示“謝謝”的概率分佈。但由於這個模型還沒有經過訓練,所以目前還不太可能實現。

image.png

由於模型的參數都是隨機初始化的,未經訓練的模型為每個單詞生成任意的概率分佈。 我們可以將其與實際輸出進行比較,然後使用反向傳播調整模型的權重,使輸出更接近我們所需要的值。

如何比較兩種概率分佈?在這個例子中我們只是將二者相減。實際應用中的損失函數請查看交叉熵損失Kullback–Leibler散度

上述只是最最簡單的一個例子。現在我們來使用一個短句子(一個詞的句子升級到三三個詞的句子了),比如輸入 “je suis étudiant”預期的翻譯結果為: “i am a student”

所以我們希望模型不是一次輸出一個詞的概率分佈了,能不能連續輸出概率分佈,最好滿足下邊要求:

  • 每個概率分佈向量長度都和詞彙表長度一樣。我們的例子中詞彙表長度是6,實際操作中一般是30000或50000。
  • 在我們的例子中第一個概率分佈應該在與單詞“i”相關的位置上具有最高的概率
  • 第二種概率分佈在與單詞“am”相關的單元處具有最高的概率
  • 以此類推,直到最後輸出分佈指示“\<eos>”符號。除了單詞本身之外,單詞表中也應該包含諸如“\<eos>”的信息,這樣softmax之後指向“\<eos>”位置,標誌解碼器輸出結束。

image.png

對應上面的單詞表,我們可以看出這裏的one-hot向量是我們訓練之後想要達到的目標。

在足夠大的數據集上訓練模型足夠長的時間後,我們希望生成的概率分佈如下所示:

image.png

這個是我們訓練之後最終得到的結果。當然這個概率並不能表明某個詞是否是訓練集之中的詞彙。 在這裏你可以看到softmax的一個特性,就是即使其他單詞並不是本時間步的輸出, 也會有一丁點的概率存在,這一特性有助於幫助模型進行訓練。

模型一次產生一個輸出,在這麼多候選中我們如何獲得我們想要的輸出呢?現在有兩種處理結果的方法:

一種是貪心算法(greedy decoding):模型每次都選擇分佈概率最高的位置,輸出其對應的單詞。

另一種方法是束搜索(beam search):保留概率最高前兩個單詞(例如,“I”和“a”),然後在下一步繼續選擇兩個概率最高的值,以此類推,在這裏我們把束搜索的寬度設置為2,當然你也可以設置其他的束搜索寬度。

更多內容

如果你想更深入瞭解Transformer: