天天看點

一文讀懂「Attention is All You Need」| 附代碼實作

<b>前言</b>

2017 年中,有兩篇類似同時也是筆者非常欣賞的論文,分别是 FaceBook 的 Convolutional Sequence to Sequence Learning 和 Google 的 Attention is All You Need,它們都算是 Seq2Seq 上的創新,本質上來說,都是抛棄了 RNN 結構來做 Seq2Seq 任務。 

在本篇文章中,筆者将對 Attention is All You Need 做一點簡單的分析。當然,這兩篇論文本身就比較火,是以網上已經有很多解讀了(不過很多解讀都是直接翻譯論文的,鮮有自己的了解),是以這裡盡可能多自己的文字,盡量不重複網上各位大佬已經說過的内容。

<b>序列編碼</b>

深度學習做 NLP 的方法,基本上都是先将句子分詞,然後每個詞轉化為對應的詞向量序列。這樣一來,每個句子都對應的是一個矩陣 X=(x1,x2,…,xt),其中 xi 都代表着第 i 個詞的詞向量(行向量),次元為 d 維,故

一文讀懂「Attention is All You Need」| 附代碼實作

。這樣的話,問題就變成了編碼這些序列了。

第一個基本的思路是 RNN 層,RNN 的方案很簡單,遞歸式進行:

一文讀懂「Attention is All You Need」| 附代碼實作

不管是已經被廣泛使用的 LSTM、GRU 還是最近的 SRU,都并未脫離這個遞歸架構。RNN 結構本身比較簡單,也很适合序列模組化,但 RNN 的明顯缺點之一就是無法并行,是以速度較慢,這是遞歸的天然缺陷。

另外我個人覺得 RNN 無法很好地學習到全局的結構資訊,因為它本質是一個馬爾科夫決策過程。

第二個思路是 CNN 層,其實 CNN 的方案也是很自然的,視窗式周遊,比如尺寸為 3 的卷積,就是:

一文讀懂「Attention is All You Need」| 附代碼實作

在 FaceBook 的論文中,純粹使用卷積也完成了 Seq2Seq 的學習,是卷積的一個精緻且極緻的使用案例,熱衷卷積的讀者必須得好好讀讀這篇文論。

CNN 友善并行,而且容易捕捉到一些全局的結構資訊,筆者本身是比較偏愛 CNN 的,在目前的工作或競賽模型中,我都已經盡量用 CNN 來代替已有的 RNN 模型了,并形成了自己的一套使用經驗,這部分我們以後再談。

Google的大作提供了第三個思路:純 Attention,單靠注意力就可以。

RNN 要逐漸遞歸才能獲得全局資訊,是以一般要雙向 RNN 才比較好;CNN 事實上隻能擷取局部資訊,是通過層疊來增大感受野;Attention 的思路最為粗暴,它一步到位擷取了全局資訊,它的解決方案是:

一文讀懂「Attention is All You Need」| 附代碼實作

其中 A,B 是另外一個序列(矩陣)。如果都取 A=B=X,那麼就稱為 Self Attention,它的意思是直接将 xt 與原來的每個詞進行比較,最後算出 yt。

<b>Attention 層</b>

Attention 定義 

Google 的一般化 Attention 思路也是一個編碼序列的方案,是以我們也可以認為它跟 RNN、CNN 一樣,都是一個序列編碼的層。

一文讀懂「Attention is All You Need」| 附代碼實作

前面給出的是一般化的架構形式的描述,事實上 Google 給出的方案是很具體的。首先,它先把 Attention 的定義給了出來:

一文讀懂「Attention is All You Need」| 附代碼實作

這裡用的是跟 Google 的論文一緻的符号,其中:

一文讀懂「Attention is All You Need」| 附代碼實作

如果忽略激活函數 softmax 的話,那麼事實上它就是三個 n×dk,dk×m,m×dv 的矩陣相乘,最後的結果就是一個 n×dv 的矩陣。

于是我們可以認為:這是一個 Attention 層,将 n×dk 的序列 Q 編碼成了一個新的 n×dv 的序列。

那怎麼了解這種結構呢?我們不妨逐個向量來看。

一文讀懂「Attention is All You Need」| 附代碼實作

其中 Z 是歸一化因子。事實上 q,k,v 分别是 query,key,value 的簡寫,K,V 是一一對應的,它們就像是 key-value 的關系,那麼上式的意思就是通過 qt 這個 query,通過與各個 ks 内積的并 softmax 的方式,來得到 qt 與各個 vs 的相似度,然後權重求和,得到一個 dv 維的向量。

其中因子

一文讀懂「Attention is All You Need」| 附代碼實作

起到調節作用,使得内積不至于太大(太大的話 softmax 後就非 0 即 1 了,不夠“soft”了)。

事實上這種 Attention 的定義并不新鮮,但由于 Google 的影響力,我們可以認為現在是更加正式地提出了這個定義,并将其視為一個層地看待。

此外這個定義隻是注意力的一種形式,還有一些其他選擇,比如 query 跟 key 的運算方式不一定是點乘(還可以是拼接後再内積一個參數向量),甚至權重都不一定要歸一化,等等。

Multi-Head Attention

這個是 Google 提出的新概念,是 Attention 機制的完善。

一文讀懂「Attention is All You Need」| 附代碼實作

不過從形式上看,它其實就再簡單不過了,就是把 Q,K,V 通過參數矩陣映射一下,然後再做 Attention,把這個過程重複做 h 次,結果拼接起來就行了,可謂“大道至簡”了。具體來說:

一文讀懂「Attention is All You Need」| 附代碼實作

這裡

一文讀懂「Attention is All You Need」| 附代碼實作

,然後:

一文讀懂「Attention is All You Need」| 附代碼實作

最後得到一個 n×(hd̃v) 的序列。所謂“多頭”(Multi-Head),就是隻多做幾次同樣的事情(參數不共享),然後把結果拼接。

Self Attention 

到目前為止,對 Attention 層的描述都是一般化的,我們可以落實一些應用。比如,如果做閱讀了解的話,Q 可以是篇章的詞向量序列,取 K=V 為問題的詞向量序列,那麼輸出就是所謂的 Aligned Question Embedding。 

而在 Google 的論文中,大部分的 Attention 都是 Self Attention,即“自注意力”,或者叫内部注意力。 

所謂 Self Attention,其實就是 Attention(X,X,X),X 就是前面說的輸入序列。也就是說,在序列内部做 Attention,尋找序列内部的聯系。 

Google 論文的主要貢獻之一是它表明了内部注意力在機器翻譯(甚至是一般的 Seq2Seq 任務)的序列編碼上是相當重要的,而之前關于 Seq2Seq 的研究基本都隻是把注意力機制用在解碼端。

類似的事情是,目前 SQUAD 閱讀了解的榜首模型 R-Net 也加入了自注意力機制,這也使得它的模型有所提升。 

當然,更準确來說,Google 所用的是 Self Multi-Head Attention:

一文讀懂「Attention is All You Need」| 附代碼實作

<b>Position Embedding</b>

然而,隻要稍微思考一下就會發現,這樣的模型并不能捕捉序列的順序。換句話說,如果将 K,V 按行打亂順序(相當于句子中的詞序打亂),那麼 Attention 的結果還是一樣的。

這就表明了,到目前為止,Attention 模型頂多是一個非常精妙的“詞袋模型”而已。 

這問題就比較嚴重了,大家知道,對于時間序列來說,尤其是對于 NLP 中的任務來說,順序是很重要的資訊,它代表着局部甚至是全局的結構,學習不到順序資訊,那麼效果将會大打折扣(比如機器翻譯中,有可能隻把每個詞都翻譯出來了,但是不能組織成合理的句子)。 

于是 Google 再祭出了一招——Position Embedding,也就是“位置向量”,将每個位置編号,然後每個編号對應一個向量,通過結合位置向量和詞向量,就給每個詞都引入了一定的位置資訊,這樣 Attention 就可以分辨出不同位置的詞了。 

Position Embedding 并不算新鮮的玩意,在 FaceBook 的 Convolutional Sequence to Sequence Learning 也用到了這個東西。但在 Google 的這個作品中,它的 Position Embedding 有幾點差別:

1. 以前在 RNN、CNN 模型中其實都出現過 Position Embedding,但在那些模型中,Position Embedding 是錦上添花的輔助手段,也就是“有它會更好、沒它也就差一點點”的情況,因為 RNN、CNN 本身就能捕捉到位置資訊。

但是在這個純 Attention 模型中,Position Embedding 是位置資訊的唯一來源,是以它是模型的核心成分之一,并非僅僅是簡單的輔助手段。 

2. 在以往的 Position Embedding 中,基本都是根據任務訓練出來的向量。而 Google 直接給出了一個構造 Position Embedding 的公式:

一文讀懂「Attention is All You Need」| 附代碼實作

這裡的意思是将 id 為 p 的位置映射為一個 dpos 維的位置向量,這個向量的第 i 個元素的數值就是 PEi(p)。

Google 在論文中說到他們比較過直接訓練出來的位置向量和上述公式計算出來的位置向量,效果是接近的。是以顯然我們更樂意使用公式構造的 Position Embedding 了。 

3. Position Embedding 本身是一個絕對位置的資訊,但在語言中,相對位置也很重要,Google 選擇前述的位置向量公式的一個重要原因如下:

由于我們有 sin(α+β)=sinα cosβ+cosα sinβ 以及 cos(α+β)=cosα cosβ−sinα sinβ,這表明位置 p+k 的向量可以表明位置 p 的向量的線性變換,這提供了表達相對位置資訊的可能性。

結合位置向量和詞向量有幾個可選方案,可以把它們拼接起來作為一個新向量,也可以把位置向量定義為跟詞向量一樣大小,然後兩者加起來。

FaceBook 的論文用的是前者,而 Google 論文中用的是後者。直覺上相加會導緻資訊損失,似乎不可取,但 Google 的成果說明相加也是很好的方案。看來我了解還不夠深刻。

<b>一些不足之處</b>

到這裡,Attention 機制已經基本介紹完了。Attention 層的好處是能夠一步到位捕捉到全局的聯系,因為它直接把序列兩兩比較(代價是計算量變為 𝒪(n2),當然由于是純矩陣運算,這個計算量相當也不是很嚴重)。

相比之下,RNN 需要一步步遞推才能捕捉到,而 CNN 則需要通過層疊來擴大感受野,這是 Attention 層的明顯優勢。 

Google 論文剩下的工作,就是介紹它怎麼用到機器翻譯中,這是個應用和調參的問題,我們這裡不特别關心它。當然,Google 的結果表明将純注意力機制用在機器翻譯中,能取得目前最好的效果,這結果的确是輝煌的。 

然而,我還是想談談這篇論文本身和 Attention 層自身的一些不足的地方。 

1. 論文标題為 Attention is All You Need,是以論文中刻意避免出現了 RNN、CNN 的字眼,但我覺得這種做法過于刻意了。

事實上,論文還專門命名了一種 Position-wise Feed-Forward Networks,事實上它就是視窗大小為 1 的一維卷積,是以有種為了不提卷積還專門換了個名稱的感覺,有點不厚道。(也有可能是我過于臆測了)。 

2. Attention 雖然跟 CNN 沒有直接聯系,但事實上充分借鑒了 CNN 的思想,比如 Multi-Head Attention 就是 Attention 做多次然後拼接,這跟 CNN 中的多個卷積核的思想是一緻的;還有論文用到了殘差結構,這也源于 CNN 網絡。 

3. 無法對位置資訊進行很好地模組化,這是硬傷。盡管可以引入 Position Embedding,但我認為這隻是一個緩解方案,并沒有根本解決問題。

舉個例子,用這種純 Attention 機制訓練一個文本分類模型或者是機器翻譯模型,效果應該都還不錯,但是用來訓練一個序列标注模型(分詞、實體識别等),效果就不怎麼好了。

那為什麼在機器翻譯任務上好?我覺得原因是機器翻譯這個任務并不特别強調語序,是以 Position Embedding 所帶來的位置資訊已經足夠了,此外翻譯任務的評測名額 BLEU 也并不特别強調語序。 

4、并非所有問題都需要長程的、全局的依賴的,也有很多問題隻依賴于局部結構,這時候用純 Attention 也不大好。

事實上,Google 似乎也意識到了這個問題,是以論文中也提到了一個 restricted 版的 Self-Attention(不過論文正文應該沒有用到它)。

它假設目前詞隻與前後 r 個詞發生聯系,是以注意力也隻發生在這 2r+1 個詞之間,這樣計算量就是 𝒪(nr),這樣也能捕捉到序列的局部結構了。但是很明顯,這就是卷積核中的卷積視窗的概念。 

通過以上讨論,我們可以體會到,把 Attention 作為一個單獨的層來看,跟 CNN、RNN 等結構混合使用,應該能更充分融合它們各自的優勢,而不必像 Google 論文号稱 Attention is All You Need,那樣實在有點“矯枉過正”了(“口氣”太大),事實上也做不到。

就論文的工作而言,也許降低一下身段,稱為 Attention is All Seq2Seq Need(事實上也這标題的“口氣”也很大),會獲得更多的肯定。

<b>代碼實作</b>

最後,為了使得本文有點實用價值,筆者試着給出了論文的 Multi-Head Attention 的實作代碼。有需要的讀者可以直接使用,或者參考着修改。 

注意的是,Multi-Head 的意思雖然很簡單——重複做幾次然後拼接,但事實上不能按照這個思路來寫程式,這樣會非常慢。因為 TensorFlow 是不會自動并行的,比如:

其中 b,c 的計算是串聯的,盡管 b,c 沒有互相依賴。是以我們必須把 Multi-Head 的操作合并到一個張量來運算,因為單個張量的乘法内部則會自動并行。

此外,我們要對序列做 Mask 以忽略填充部分的影響。一般的 Mask 是将填充部分置零,但 Attention 中的 Mask 是要在 softmax 之前,把填充部分減去一個大整數(這樣 softmax 之後就非常接近 0 了)。這些内容都在代碼中有對應的實作。

TensorFlow 版

https://github.com/bojone/attention/blob/master/attention_tf.py

Keras 版 

https://github.com/bojone/attention/blob/master/attention_keras.py

代碼測試

在 Keras 上對 IMDB 進行簡單的測試(不做 Mask):

無 Position Embedding 的結果:

一文讀懂「Attention is All You Need」| 附代碼實作

有 Position Embedding 的結果:

一文讀懂「Attention is All You Need」| 附代碼實作

貌似最高準确率比單層的 LSTM 準确率還高一點,另外還可以看到 Position Embedding 能提高準确率、減弱過拟合。 

計算量分析 

可以看到,事實上 Attention 的計算量并不低。比如 Self Attention 中,首先要對 X 做三次線性映射,這計算量已經相當于卷積核大小為 3 的一維卷積了,不過這部分計算量還隻是 𝒪(n) 的;然後還包含了兩次序列自身的矩陣乘法,這兩次矩陣乘法的計算量都是 𝒪(n2) 的,要是序列足夠長,這個計算量其實是很難接受的。 

這也表明,restricted 版的 Attention 是接下來的研究重點,并且将 Attention 與 CNN、RNN 混合使用,才是比較适中的道路。

<b>結語</b>

感謝 Google 提供的精彩的使用案例,讓我等在大開眼界之餘,還對 Attention 的認識更深一層。Google 的這個成果在某種程度上展現了“大道至簡”的理念,的确是 NLP 中不可多得的精品。

原文釋出時間為:2018-01-10

本文作者:蘇劍林

繼續閱讀