这么多年,终于有人讲清楚 Transformer 了!
注意力機制是一種在現代深度學習模型中無處不在的方法,它有助于提高神經機器翻譯應用程序性能的概念。在本文中,我們將介紹Transformer這種模型,它可以通過注意力機制來提高訓練模型的速度。在特定任務中,Transformer的表現優于Google神經機器翻譯模型。但是,最大的好處來自于Transformer如何適用于并行化。實際上,Google Cloud建議使用Transformer作為參考模型來使用其Cloud TPU產品。因此,我們試試將模型分解開吧,看看它是如何工作的。
?
Attention is All You Need一文中提出了Transformer。它的TensorFlow實現是Tensor2Tensor包的一部分。哈佛大學的NLP團隊創建了一份指南,用PyTorch實現對這篇文章進行注釋。在本文中,我們將試著盡可能地簡化講解,并逐一介紹概念,希望能讓那些對這方面沒有深入知識的人們更容易理解Transformer。
?
Transformer概覽
?
首先,讓我們先將模型視為一個黑盒。在機器翻譯應用程序中,這個模型將拿一種語言中的一個句子,然后以另一種語言輸出其翻譯。
?
打開擎天柱的引擎蓋(Optimus Prime,Transformer與變形金剛是同一個詞,故而產生這個梗),我們能看到編碼組件,解碼組件,以及它們之間的連接。
?
編碼組件是一個編碼器組成的堆棧(論文上一個摞一個地堆疊了六個編碼器——六這個數字本身沒有什么神奇之處,人們肯定可以嘗試其他個數)。解碼組件是一個由相同數量的解碼器組成的堆棧。
編碼器的結構均相同(但它們權重不同)。每一層都可以被分為兩個子層:
編碼器的輸入首先流經自注意力層,這一層可以幫助編碼器在對特定單詞進行編碼時查看輸入句子中的其他單詞。稍后我們將會進一步關注自注意力層。
?
自注意層的輸出被送到前饋神經網絡(feed-forward neural network)。每個位置都獨立應用了完全相同的前饋網絡。
?
解碼器也具有這兩層,但是在它們之間還有一個注意力層,可以幫助解碼器專注于輸入語句的相關部分上(類似于seq2seq模型中的注意力機制)。
?
引入張量(Tensor)
?
現在,既然我們已經了解了模型的主要組成部分,那就開始研究一下各種向量/張量,以及它們在這些組成部分之間是如何流動的,才能使經過訓練的模型把輸入轉化為輸出。
?
通常,在NLP應用程序中,我們首先使用embedding算法將輸入的每個字變成向量。
?
每個單詞都被嵌入到大小為512的向量中。我們將用這些簡單的框代表這些向量。
?
嵌入僅發生在最底層的編碼器中。對于所有編碼器都適用的抽象概念是,它們都會收到一系列向量,每個向量的大小均為512——在最底層的編碼器中是單詞的嵌入,但在其他編碼器中將是直接在下面的編碼器的輸出。向量列表的大小是一個我們可以設置的超參數,基本上來說,這個參數就是訓練數據集中最長句子的長度。
?
在將我們輸入序列中的單詞嵌入以后,每個單詞都分別流經編碼器的兩層。
?
從這里開始,我們就可以看到Transformer的一個關鍵屬性了,那就是每個位置的單詞都沿著自己的路徑流經編碼器。自注意力層中的這些路徑之間存在依賴性。但是,前饋層不具有這些依賴性,因此各種路徑可以在流過前饋層的同時被并行執行。
?
接下來,我們將換一個較短的句子作為示例,然后看一下在編碼器的每個子層中都發生了些什么。
?
現在,我們才是在編碼!
?
正如我們已經提到的,編碼器接收一個向量列表作為輸入。它首先將這些向量傳遞到自注意力層,然后傳遞到前饋神經網絡,然后將輸出向上發送到下一個編碼器,以這樣的一個流程來處理向量列表。
?
每個位置的單詞都會經過一個自注意力流程。然后,它們中的每個會都通過前饋神經網絡——完全相同的網絡,每個向量分別獨立流過。
?
自注意力機制概覽
?
不要因為我一直在講“自注意力”(self-attention)這個詞,就誤認為這是每個人都應該熟悉的概念。在閱讀Attention is All You Need論文之前,我自己從未碰到過這個概念。讓我們來提煉總結一下它的工作原理。
?
比方說,下面的句子是我們要翻譯的輸入:
?
“The animal didn't cross the street because it was too tired.”
?
這句話中的“it”指的是什么?是指街道還是動物?對人類來說,這是一個簡單的問題,但對算法而言卻不那么簡單。
?
當模型處理“ it”一詞時,自注意力機制使其能夠將“it”與“animal”相關聯。
?
在模型處理每個單詞(輸入序列中的每個位置)時,自注意力使其能夠查看輸入序列中的其他位置,以尋找思路來更好地對該單詞進行編碼。
?
如果你熟悉RNN,請想一下如何通過保持隱狀態來使RNN將其已處理的先前單詞/向量的表示與當前正在處理的單詞/向量進行合并。Transformer使用自注意力機制來將相關詞的理解編碼到當前詞中。
當我們在編碼器#5(堆棧中的頂部編碼器)中對單詞“ it”進行編碼時,注意力機制的一部分集中在“The Animal”上,并將其表示的一部分合并到“it”的編碼中。
?
一定要去看一下Tensor2Tensor notebook,你可以在在里面加載Transformer模型,并使用交互式可視化檢查一下。
?
自注意力詳解
?
首先,讓我們看一下如何使用向量來計算自注意力,然后著眼于如何使用矩陣來實現。
?
計算自注意力的第一步是依據每個編碼器的輸入向量(在這種情況下,是每個單詞的embedding)創建三個向量。因此,對于每個單詞,我們創建一個Query向量,一個Key向量和一個Value向量。通過將embedding乘以我們在訓練過程中訓練的三個矩陣來創建這些向量。
?
請注意,這些新向量的維數小于embedding向量的維數。新向量的維數為64,而embedding和編碼器輸入/輸出向量的維數為512。新向量不一定非要更小,這是為了使多頭注意力(大部分)計算保持一致的結構性選擇。
x1乘以WQ權重矩陣可得出q1,即與該單詞關聯的“Query”向量。我們最終為輸入句子中的每個單詞創建一個“Query”,一個“Key”和一個“Value”投射。
?
什么是“Query”,“Key”和“Value”向量?
?
它們是一種抽象,對于注意力的計算和思考方面非常有用。繼續閱讀下面的注意力計算方式,你幾乎就能了解所有這些媒介所起的作用了。
?
計算自注意力的第二步是計算一個分數(score)。假設我們正在計算這個例子中第一個單詞“Thinking”的自注意力。我們需要根據該單詞對輸入句子中的每個單詞打分。這個分數決定了當我們為某個位置的單詞編碼時,在輸入句子的其他部分上的重視程度。
?
分數是通過將Query向量的點積與我們要評分的各個單詞的Key向量相乘得出的。因此,如果我們正在處理位置#1上的單詞的自注意,則第一個分數將是q1和k1的點積。第二個得分將是q1和k2的點積。
?
?
第三和第四步是將分數除以8(論文中使用的Key向量維數的平方根,即64。這將引入更穩定的漸變。此處也許會存在其他可能的值,但這是默認值),然后將結果通過一個softmax操作傳遞。Softmax對分數進行歸一化,使它們均為正數,并且和為一。
這個softmax分數將會決定在這個位置上的單詞會在多大程度上被表達。顯然,當前位置單詞的softmax得分最高,但有時候,注意一下與當前單詞相關的另一個單詞也會很有用。
?
第五步是將每個Value向量乘以softmax分數(對后續求和的準備工作)。這里直覺的反應是保持我們要關注的單詞的value完整,并壓過那些無關的單詞(例如,通過把它們乘以0.001這樣的很小的數)。
?
第六步是對加權向量進行求和。這將在此位置(對于第一個單詞)產生自注意層的輸出。
?
這樣就完成了自注意力的計算。生成的向量是可以被發送到前饋神經網絡的。但是,在實際的實現過程中,此計算以矩陣形式進行,以實現更快的處理速度。現在,看完了單詞級計算,讓我們接著看矩陣計算吧。
?
自注意力的矩陣計算
?
第一步是計算Query,Key和Value矩陣。我們將嵌入內容打包到矩陣X中,然后將其乘以我們訓練過的權重矩陣(WQ,WK,WV)。
?
X矩陣中的每一行對應于輸入句子中的一個單詞。我們再次看到嵌入向量(圖中的512或4個框)和q / k / v向量(圖中的64或3個框)的大小差異。
?
最后,由于我們要處理的是矩陣,因此我們可以通過一個公式將步驟2到6壓縮來計算自注意力的輸出。
?
矩陣形式的自注意力計算
?
?
長著很多頭的野獸
?
論文通過添加一種名為“多頭”注意力的機制,進一步完善了自注意力層。這樣可以通過兩種方式提高注意力層的性能:
?
1、它擴展了模型專注于不同位置的能力。是的,在上面的例子中,z1包含所有其他編碼的一小部分,但是它可能由實際單詞本身主導。如果我們要翻譯這樣的句子,例如“The animal didn’t cross the street because it was too tired”,那么我們會想知道這里面的“it”指的是什么。
?
2、它為注意力層提供了多個“表示子空間”(representation subspaces)。正如我們接下來將要看到的,在多頭注意力機制下,我們擁有多組Query/Key/Value權重矩陣(Transformer使用八個注意力頭,因此每個編碼器/解碼器最終都能得到八組)。這些集合中的每一個都是隨機初始化的。然后,在訓練之后,將每個集合用于將輸入的embedding(或來自較低編碼器/解碼器的向量)投影到不同的表示子空間中。
?
在多頭注意力下,我們單獨為每個頭維護不同的Q / K / V權重矩陣,從而就會得到不同的Q / K / V矩陣。就像之前那樣,我們將X乘以WQ / WK / WV矩陣以生成Q / K / V矩陣。
?
如果我們執行上面概述的自注意力計算,每次使用不同的權重矩陣,計算八次,我們最終將得到八個不同的Z矩陣。
?
這給我們帶來了一些挑戰。前饋層所預期的并不是8個矩陣,而是一個單一的矩陣(每個單詞一個向量)。因此,我們需要一種方法來將這八個矩陣壓縮為單個矩陣。
?
我們該怎么做?我們把這些矩陣合并,然后將它們乘以一個另外的權重矩陣WO。
?
這差不多就是多頭注意力的全部內容。我發現其中的矩陣還是很多的。下面我試試將它們全部放在一個視圖中,以便我們可以統一查看。
?
既然我們已經涉及到注意力頭的內容,那么讓我們重新回顧一下前面的例子,看看在示例句中對“ it”一詞進行編碼時,不同的注意力頭關注的位置分別在哪:
當我們對“it”一詞進行編碼時,一個注意力頭專注于“the animal”一詞,而另一個則專注于“tired”一詞——從某種意義上來說,模型對單詞“it”的表示既依賴于對“animal”的表示又依賴于對“tired”的表示。
?
但是,如果我們將所有的注意力頭都加到圖片中,則可能會比較難以直觀解釋:
使用位置編碼表示序列的順序
?
到目前為止,我們對這個模型的描述中尚且缺少一種表示輸入序列中單詞順序的方法。
?
為了解決這個問題,Transformer為每個輸入的embedding添加一個向量。這些向量遵循模型學習的特定模式,能夠幫助我們確定每個單詞的位置,或序列中不同單詞之間的距離。在這個地方我們的直覺會是,將這些值添加到embedding中后,一旦將它們投影到Q / K / V向量中,以及對注意力點積,就可以在embedding向量之間提供有意義的距離。
?
為了使模型感知到單詞的順序,我們添加了位置編碼向量,它的值遵循特定的規律。
?
如果我們假設embedding的維數為4,則實際的位置編碼則應如下圖所示:
一個真實示例,其embedding大小為4的位置編碼
?
這種規律看起來會是什么樣的?
?
在下圖中,每行對應一個向量的位置編碼。因此,我們要把第一行添加到輸入序列中第一個單詞的embedding向量。每行包含512個值,每個值都在1到-1之間。我們對它們進行了顏色編碼,從而使變化規律更加明顯。
一個真實例子的位置編碼,embedding大小為512(列),20個單詞(行)。你會發現,它看起來像是從中心位置向下分開的。這是因為左半部分的值是由一個函數(使用正弦函數)生成的,而右半部分的值是由另一個函數(使用余弦函數)生成的。然后它們被合并起來形成每個位置的編碼向量。
?
論文中描述了位置編碼用到的公式(第3.5節)。你可以在get_timing_signal_1d()中查看用于生成位置編碼的代碼。這不是唯一的位置編碼方法。但是,它的優勢在于能夠放大到看不見的序列長度(例如,我們訓練后的模型被要求翻譯一個句子,而這個句子比我們訓練集中的任何句子都長)。
?
(代碼地址:
https://github.com/tensorflow/tensor2tensor/blob/23bd23b9830059fbc349381b70d9429b5c40a139/tensor2tensor/layers/common_attention.py)
?
2020年7月更新:上面顯示的位置編碼來自Transformer的Tranformer2Transformer實現。論文中用的方法略有不同,論文中沒有直接鏈接,而是將兩個信號交織。下面的圖顯示了這種方式的樣子。這是用來生成它的代碼:
https://github.com/jalammar/jalammar.github.io/blob/master/notebookes/transformer/transformer_positional_encoding_graph.ipynb
?
殘差
?
在繼續進行講解之前,我們需要提一下編碼器結構中的一個細節,那就是每個編碼器中的每個子層(自注意力,ffnn)在其周圍都有殘差連接,后續再進行層歸一化(layer-normalization)步驟。
(layer-normalization :https://arxiv.org/abs/1607.06450)
?
如果我們要對向量和與自注意力相關的層規范操作進行可視化,則看起來應該像這樣:
這也適用于解碼器的子層。如果我們設想由2個編碼器解碼器堆棧組成的Transformer,它看起來像這樣:
?
解碼器端
現在,我們已經講解了編碼器方面的大多數概念,同時也基本了解了解碼器各組件是如何工作的。然而,接下來讓我們看一下它們如何協同。
?
編碼器首先處理輸入序列。然后,頂部編碼器的輸出轉換為注意力向量K和V的集合。每個解碼器將在其“編碼器-解碼器注意力”層中使用它們,這有助于解碼器將重心放在輸入序列中合適的位置:
?
在完成編碼階段之后,我們開始解碼階段。解碼階段的每個步驟都從輸出序列中輸出一個元素(在這個例子下,為語句的英文翻譯)。
?
后續步驟一直重復該過程,直到得到一個特殊符號,標志著Transformer解碼器已完成其輸出。每個步驟的輸出都被饋送到下一個步驟的底部解碼器,并且解碼器會像編碼器一樣,將其解碼結果冒泡。就像我們對編碼器輸入所做的操作一樣,我們給這些解碼器輸入做嵌入并添加位置編碼來表示每個單詞的位置。
?
解碼器中的自注意力層與編碼器中的略有不同:
?
在解碼器中,自注意力層僅被允許參與到輸出序列中的較早位置。這是通過在自注意力計算中的softmax步驟之前屏蔽將來的位置(將它們設置為-inf)來完成的。
?
“編碼器-解碼器注意力”層的工作方式與多頭自注意力類似,不同之處在于它從下一層創建其Queries矩陣,并從編碼器堆棧的輸出中獲取Keys和Values矩陣。
?
最終的線性層和Softmax層
?
解碼器堆棧輸出一組浮點數組成的向量。我們如何把它變成一個詞?最后的線性層,以及它之后Softmax層做的就是這項工作。
?
線性層(he Linear layer)是一個簡單的完全連接的神經網絡,將解碼器堆棧產生的向量投射到一個大得多的對數向量中。
?
我們假設自己的模型從訓練數據集中共學會了10,000個不同的英語單詞(我們模型的“輸出詞匯表”)。這將使對數向量的寬度變為10,000個單元,每個單元對應各個單詞的得分。我們將會通過這樣的方式來解釋模型的輸出。
?
然后,softmax層將會把這些分數轉換為概率(全部為正數,各項相加和為1.0)。概率最高的單元被選中,且與該單元相關聯的單詞將成為該步的輸出。
?
該圖從底部開始,生成的向量作為解碼器堆棧的輸出,后續會被轉換為文字輸出。
?
?
訓練過程回顧
?
現在,我們已經講解了一個訓練完畢的Transformer的前向過程,那么再看一下模型的訓練過程也是很有用的。
?
在訓練過程中,未經訓練的模型將歷經完全相同的前向過程。但是,由于我們正在用已標記的訓練數據集對其進行訓練,因此我們可以將其輸出與正確的輸出進行比較。
?
為了直觀地視覺化講解這一點,我們假設輸出詞匯表僅包含六個單詞(“a”,“am”,“i”,“thanks”,“student”和“ <eos>”(“end of sentence”的縮寫)) 。
?
我們模型的輸出詞匯表是在預處理階段創建的,那時候還沒有開始訓練。
?
一旦定義好了輸出詞匯表,我們就可以使用一個相同寬度的向量來表示詞匯表中的每個單詞了。這也被稱為one-hot encoding。因此,例如,我們可以使用下面這個向量來表示單詞“am”:
示例:我們輸出詞匯表的one-hot encoding
?
回顧完了之后,接下來讓我們討論一下模型的損失函數(loss function)——我們在訓練階段想要優化的指標,以期最終可以得到一個非常準確模型。
?
損失函數
?
假設我們正在訓練我們的模型。假設這是我們訓練階段的第一步,我們用一個簡單的例子訓練它,使其將“merci”轉換為“thanks”。
?
這意味著,我們希望輸出的是一個能表示單詞“thanks”的概率分布。但是,由于該模型尚未經過訓練,因此目前這還不太可能發生。
由于模型的參數(權重)在初始化的的時候都是隨機分配的,因此(未經訓練的)模型為每個單元格/單詞生成的概率分布值都是隨機的。我們可以將其與實際輸出進行比較,然后使用反向傳播來調整所有模型的權重,讓輸出結果更接近我們想要的輸出。
如何比較兩個概率分布?我們只需用一個減去另一個就可以。欲知更多詳細信息,請查閱交叉熵(cross-entropy)和Kullback-Leibler散度(Kullback–Leibler divergence)相關內容。
(https://colah.github.io/posts/2015-09-Visual-Information/
https://www.countbayesie.com/blog/2017/5/9/kullback-leibler-divergence-explained)
?
但是請注意,這個例子過于簡單了。更貼近實際一點,我們將使用由不止一個單詞組成的句子。例如,輸入:“je suis étudiant”,預期輸出:“I am a student”。這實際上意味著,我們希望自己的模型連續輸出一些概率分布,其中:
-
每個概率分布都由一個寬度為vocab_size的向量表示(在我們的簡單示例中vocab_size為6,但更貼近實際情況的數量往往為30,000或50,000)
-
第一個概率分布在單詞“i”的相關單元中具有最高概率
-
第二個概率分布在單詞“am”的相關單元中具有最高概率
-
依此類推,直到第五個輸出分布標志著“ <end of sentense>”符號,該符號也具有自己的單元格,也處在10,000個元素詞匯表中。
?
在訓練示例中針對一個樣本句子,我們將會參照這個目標概率分布訓練我們的模型。
將模型在一個在足夠大的數據集上訓練足夠長的時間之后,我們希望產生的概率分布能像下面這樣:
理想情況下,經過訓練,該模型將輸出我們所期待的正確譯文。當然,這并不能表明該短語是否屬于訓練數據集(請參閱:交叉驗證https://www.youtube.com/watch?v=TIgfjmp-4BA)。請注意,即使不可能成為該步的輸出,每個位置也會獲得一點概率——這是softmax的一個非常有用的特性,可以幫助訓練過程。
?
由于該模型每次生成一個輸出,因此我們可以假定模型會從概率分布中選擇具有最高概率的一個單詞,然后丟棄其余的。這是其中的一種方法(稱為貪婪解碼,greedy decoding)。還有另一種方法是,比如先確定前兩個單詞(例如,“ I”和“ a”),然后下一步,運行模型兩次:第一次假設第一個輸出位置為單詞“I”,第二次假設第一個輸出位置是單詞“a”,并且最終采用在位置#1和#2誤差更小的版本。我們在#2和#3等位置重復此操作。此方法稱為“beam search”,在我們舉的例子中,beam_size為2(這意味著在內存中始終都保留有兩個部分假設(未完成的翻譯)),top_beams也為2(意味著我們將返回兩份譯文)。對于這些超參數你都可以自己進行試驗。
?
?
希望本文能對你有用,讓你能開始逐漸理解Transformer的主要概念。如果你想更進一步,建議按照以下步驟逐步學習:
?
-
閱讀Attention is All You Need論文,Transformer博客文章(Transformer:A Novel Neural Network Architecture for Language Understanding)以及Tensor2Tensor公告。
博客地址:https://ai.googleblog.com/2017/08/transformer-novel-neural-network.html
-
觀看?ukaszKaiser詳細講解模型的演講
(https://www.youtube.com/watch?v=rBCqOTEfxvg)
-
探索Tensor2Tensor?repo中提供的Jupyter Notebook
-
探索Tensor2Tensor repo
?
進一步學習:
?
-
Depthwise Separable Convolutions for Neural Machine Translation(https://arxiv.org/abs/1706.03059)
-
One Model To Learn Them All(https://arxiv.org/abs/1706.05137)
-
Discrete Autoencoders for Sequence Models(https://arxiv.org/abs/1801.09797)
-
Generating Wikipedia by Summarizing Long Sequences(https://arxiv.org/abs/1801.10198)
-
Image Transformer(https://arxiv.org/abs/1802.05751)
-
Training Tips for the Transformer Model(https://arxiv.org/abs/1804.00247)
-
Self-Attention with Relative Position Representations(https://arxiv.org/abs/1803.02155)
-
Fast Decoding in Sequence Models using Discrete Latent Variables(https://arxiv.org/abs/1803.03382)
-
Adafactor: Adaptive Learning Rates with Sublinear Memory Cost(https://arxiv.org/abs/1804.04235)
原文鏈接:
http://jalammar.github.io/illustrated-transformer/
?
總結
以上是生活随笔為你收集整理的这么多年,终于有人讲清楚 Transformer 了!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Transformer 模型详解
- 下一篇: The Illustrated Tran