《Attention is All You Need》浅读(简介+代码)
2017年中,有兩篇類似同時也是筆者非常欣賞的論文,分別是FaceBook的《Convolutional Sequence to Sequence Learning》和Google的《Attention is All You Need》,它們都算是Seq2Seq上的創新,本質上來說,都是拋棄了RNN結構來做Seq2Seq任務。
這篇博文中,筆者對《Attention is All You Need》做一點簡單的分析。當然,這兩篇論文本身就比較火,因此網上已經有很多解讀了(不過很多解讀都是直接翻譯論文的,鮮有自己的理解),因此這里盡可能多自己的文字,盡量不重復網上各位大佬已經說過的內容。
序列編碼?#
深度學習做NLP的方法,基本上都是先將句子分詞,然后每個詞轉化為對應的詞向量序列。這樣一來,每個句子都對應的是一個矩陣X=(x1,x2,…,xt)X=(x1,x2,…,xt),其中xixi都代表著第ii個詞的詞向量(行向量),維度為dd維,故X∈Rn×dX∈Rn×d。這樣的話,問題就變成了編碼這些序列了。
第一個基本的思路是RNN層,RNN的方案很簡單,遞歸式進行:
yt=f(yt?1,xt)yt=f(yt?1,xt)
不管是已經被廣泛使用的LSTM、GRU還是最近的SRU,都并未脫離這個遞歸框架。RNN結構本身比較簡單,也很適合序列建模,但RNN的明顯缺點之一就是無法并行,因此速度較慢,這是遞歸的天然缺陷。另外我個人覺得RNN無法很好地學習到全局的結構信息,因為它本質是一個馬爾科夫決策過程。
?
第二個思路是CNN層,其實CNN的方案也是很自然的,窗口式遍歷,比如尺寸為3的卷積,就是
yt=f(xt?1,xt,xt+1)yt=f(xt?1,xt,xt+1)
在FaceBook的論文中,純粹使用卷積也完成了Seq2Seq的學習,是卷積的一個精致且極致的使用案例,熱衷卷積的讀者必須得好好讀讀這篇文論。CNN方便并行,而且容易捕捉到一些全局的結構信息,筆者本身是比較偏愛CNN的,在目前的工作或競賽模型中,我都已經盡量用CNN來代替已有的RNN模型了,并形成了自己的一套使用經驗,這部分我們以后再談。
?
Google的大作提供了第三個思路:純Attention!單靠注意力就可以!RNN要逐步遞歸才能獲得全局信息,因此一般要雙向RNN才比較好;CNN事實上只能獲取局部信息,是通過層疊來增大感受野;Attention的思路最為粗暴,它一步到位獲取了全局信息!它的解決方案是:
yt=f(xt,A,B)yt=f(xt,A,B)
其中A,BA,B是另外一個序列(矩陣)。如果都取A=B=XA=B=X,那么就稱為Self Attention,它的意思是直接將xtxt與原來的每個詞進行比較,最后算出ytyt!
?
Attention層?#
Attention定義?#
?
Attention
?
Google的一般化Attention思路也是一個編碼序列的方案,因此我們也可以認為它跟RNN、CNN一樣,都是一個序列編碼的層。
前面給出的是一般化的框架形式的描述,事實上Google給出的方案是很具體的。首先,它先把Attention的定義給了出來:
Attention(Q,K,V)=softmax(QK?dk??√)VAttention(Q,K,V)=softmax(QK?dk)V
這里用的是跟Google的論文一致的符號,其中Q∈Rn×dk,K∈Rm×dk,V∈Rm×dvQ∈Rn×dk,K∈Rm×dk,V∈Rm×dv。如果忽略激活函數softmaxsoftmax的話,那么事實上它就是三個n×dk,dk×m,m×dvn×dk,dk×m,m×dv的矩陣相乘,最后的結果就是一個n×dvn×dv的矩陣。于是我們可以認為:這是一個Attention層,將n×dkn×dk的序列QQ編碼成了一個新的n×dvn×dv的序列。
?
那怎么理解這種結構呢?我們不妨逐個向量來看。
Attention(qt,K,V)=∑s=1m1Zexp(?qt,ks?dk??√)vsAttention(qt,K,V)=∑s=1m1Zexp?(?qt,ks?dk)vs
其中ZZ是歸一化因子。事實上q,k,vq,k,v分別是query,key,valuequery,key,value的簡寫,K,VK,V是一一對應的,它們就像是key-value的關系,那么上式的意思就是通過qtqt這個query,通過與各個ksks內積的并softmax的方式,來得到qtqt與各個vsvs的相似度,然后加權求和,得到一個dvdv維的向量。其中因子dk??√dk起到調節作用,使得內積不至于太大(太大的話softmax后就非0即1了,不夠“soft”了)。
?
事實上這種Attention的定義并不新鮮,但由于Google的影響力,我們可以認為現在是更加正式地提出了這個定義,并將其視為一個層地看待;此外這個定義只是注意力的一種形式,還有一些其他選擇,比如queryquery跟keykey的運算方式不一定是點乘(還可以是拼接后再內積一個參數向量),甚至權重都不一定要歸一化,等等。
Multi-Head Attention?#
?
Multi-Head Attention
?
這個是Google提出的新概念,是Attention機制的完善。不過從形式上看,它其實就再簡單不過了,就是把Q,K,VQ,K,V通過參數矩陣映射一下,然后再做Attention,把這個過程重復做hh次,結果拼接起來就行了,可謂“大道至簡”了。具體來說
headi=Attention(QWQi,KWKi,VWVi)headi=Attention(QWiQ,KWiK,VWiV)
這里WQi∈Rdk×d~k,WKi∈Rdk×d~k,WVi∈Rdv×d~vWiQ∈Rdk×d~k,WiK∈Rdk×d~k,WiV∈Rdv×d~v,然后
最后得到一個n×(hd~v)n×(hd~v)的序列。所謂“多頭”(Multi-Head),就是只多做幾次同樣的事情(參數不共享),然后把結果拼接。
?
Self Attention?#
到目前為止,對Attention層的描述都是一般化的,我們可以落實一些應用。比如,如果做閱讀理解的話,QQ可以是篇章的向量序列,取K=VK=V為問題的向量序列,那么輸出就是所謂的Aligned Question Embedding。
而在Google的論文中,大部分的Attention都是Self Attention,即“自注意力”,或者叫內部注意力。
所謂Self Attention,其實就是Attention(X,X,X)Attention(X,X,X),XX就是前面說的輸入序列。也就是說,在序列內部做Attention,尋找序列內部的聯系。Google論文的主要貢獻之一是它表明了內部注意力在機器翻譯(甚至是一般的Seq2Seq任務)的序列編碼上是相當重要的,而之前關于Seq2Seq的研究基本都只是把注意力機制用在解碼端。類似的事情是,目前SQUAD閱讀理解的榜首模型R-Net也加入了自注意力機制,這也使得它的模型有所提升。
當然,更準確來說,Google所用的是Self Multi-Head Attention:
Y=MultiHead(X,X,X)Y=MultiHead(X,X,X)?
Position Embedding?#
然而,只要稍微思考一下就會發現,這樣的模型并不能捕捉序列的順序!換句話說,如果將K,VK,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的公式:
?????PE2i(p)=sin(p/100002i/dpos)PE2i+1(p)=cos(p/100002i/dpos){PE2i(p)=sin?(p/100002i/dpos)PE2i+1(p)=cos?(p/100002i/dpos)
這里的意思是將id為pp的位置映射為一個dposdpos維的位置向量,這個向量的第ii個元素的數值就是PEi(p)PEi(p)。Google在論文中說到他們比較過直接訓練出來的位置向量和上述公式計算出來的位置向量,效果是接近的。因此顯然我們更樂意使用公式構造的Position Embedding了。?
3、Position Embedding本身是一個絕對位置的信息,但在語言中,相對位置也很重要,Google選擇前述的位置向量公式的一個重要原因是:由于我們有sin(α+β)=sinαcosβ+cosαsinβsin?(α+β)=sin?αcos?β+cos?αsin?β以及cos(α+β)=cosαcosβ?sinαsinβcos?(α+β)=cos?αcos?β?sin?αsin?β,這表明位置p+kp+k的向量可以表示成位置pp的向量的線性變換,這提供了表達相對位置信息的可能性。
結合位置向量和詞向量有幾個可選方案,可以把它們拼接起來作為一個新向量,也可以把位置向量定義為跟詞向量一樣大小,然后兩者加起來。FaceBook的論文和Google論文中用的都是后者。直覺上相加會導致信息損失,似乎不可取,但Google的成果說明相加也是很好的方案。看來我理解還不夠深刻。
還有,盡管論文給出的Position Embedding是sin,cossin,cos交錯的形式,但其實這個交錯形式沒有特別的意義,你可以按照任意的方式重排它(比如前sinsin后coscos地拼接),原因如下:
1、假如你的Position_Embedding是拼接到原來的詞向量中,那么將coscos和sinsin前后連接還是交叉連接,都是沒區別的,因為你下一步都是接一個變換矩陣而已;
2、如果你的Position_Embedding是加到原來的詞向量中,那么兩種方式貌似是有點區別的,但是要注意的是,詞向量本身沒有局部結構,也就是說,50維的詞向量,將每一維打亂重新排個序(當然整體要按同樣的順序來重新排序),它還是等價于原來的詞向量。既然相加的對象(詞向量)都沒有局部結構,我們也沒必要強調被加的對象(Position_Embedding)的局部結構(也就是交叉連接)了。
一些不足之處?#
到這里,Attention機制已經基本介紹完了。Attention層的好處是能夠一步到位捕捉到全局的聯系,因為它直接把序列兩兩比較(代價是計算量變為O(n2)O(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(不過論文正文應該沒有用到它),它假設當前詞只與前后rr個詞發生聯系,因此注意力也只發生在這2r+12r+1個詞之間,這樣計算量就是O(nr)O(nr),這樣也能捕捉到序列的局部結構了。但是很明顯,這就是卷積核中的卷積窗口的概念!
通過以上討論,我們可以體會到,把Attention作為一個單獨的層來看,跟CNN、RNN等結構混合使用,應該能更充分融合它們各自的優勢,而不必像Google論文號稱Attention is All You Need,那樣實在有點“矯枉過正”了(“口氣”太大),事實上也做不到。就論文的工作而言,也許降低一下身段,稱為Attention is All Seq2Seq Need(事實上也這標題的“口氣”也很大),會獲得更多的肯定。
代碼實現?#
最后,為了使得本文有點實用價值,筆者試著給出了論文的Multi-Head Attention的實現代碼。有需要的讀者可以直接使用,或者參考著修改。
注意的是,Multi-Head的意思雖然很簡單——重復做幾次然后拼接,但事實上不能按照這個思路來寫程序,這樣會非常慢。因為tensorflow是不會自動并行的,比如
a = tf.zeros((10, 10))
b = a + 1
c = a + 2
其中b,c的計算是串聯的,盡管b、c沒有相互依賴。因此我們必須把Multi-Head的操作合并到一個張量來運算,因為單個張量的乘法內部則會自動并行。
此外,我們要對序列做Mask以忽略填充部分的影響。一般的Mask是將填充部分置零,但Attention中的Mask是要在softmax之前,把填充部分減去一個大整數(這樣softmax之后就非常接近0了)。這些內容都在代碼中有對應的實現。
tensorflow版?#
下面是tf的實現:
https://github.com/bojone/attention/blob/master/attention_tf.py
Keras版?#
Keras仍然是我最喜愛的深度學習框架之一,因此必須也得給Keras寫一個出來:
https://github.com/bojone/attention/blob/master/attention_keras.py
代碼測試?#
在Keras上對IMDB進行簡單的測試(不做Mask):
from __future__ import print_function
from keras.preprocessing import sequence
from keras.datasets import imdbmax_features = 20000
maxlen = 80
batch_size = 32print('Loading data...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')print('Pad sequences (samples x time)')
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)from keras.models import Model
from keras.layers import *S_inputs = Input(shape=(None,), dtype='int32')
embeddings = Embedding(max_features, 128)(S_inputs)
# embeddings = Position_Embedding()(embeddings) # 增加Position_Embedding能輕微提高準確率
O_seq = Attention(8,16)([embeddings,embeddings,embeddings])
O_seq = GlobalAveragePooling1D()(O_seq)
O_seq = Dropout(0.5)(O_seq)
outputs = Dense(1, activation='sigmoid')(O_seq)model = Model(inputs=S_inputs, outputs=outputs)
# try using different optimizers and different optimizer configs
model.compile(loss='binary_crossentropy',optimizer='adam',metrics=['accuracy'])print('Train...')
model.fit(x_train, y_train,batch_size=batch_size,epochs=5,validation_data=(x_test, y_test))
無Position Embedding的結果:
Train on 25000 samples, validate on 25000 samples
25000/25000 [==============================] - 9s - loss: 0.4090 - acc: 0.8126 - val_loss: 0.3541 - val_acc: 0.8430
Epoch 2/5
25000/25000 [==============================] - 9s - loss: 0.2528 - acc: 0.8976 - val_loss: 0.3962 - val_acc: 0.8284
Epoch 3/5
25000/25000 [==============================] - 9s - loss: 0.1731 - acc: 0.9335 - val_loss: 0.5172 - val_acc: 0.8137
Epoch 4/5
25000/25000 [==============================] - 9s - loss: 0.1172 - acc: 0.9568 - val_loss: 0.6185 - val_acc: 0.8009
Epoch 5/5
25000/25000 [==============================] - 9s - loss: 0.0750 - acc: 0.9730 - val_loss: 0.9310 - val_acc: 0.7925
有Position Embedding的結構:
Train on 25000 samples, validate on 25000 samples
Epoch 1/5
25000/25000 [==============================] - 9s - loss: 0.5179 - acc: 0.7183 - val_loss: 0.3540 - val_acc: 0.8413
Epoch 2/5
25000/25000 [==============================] - 9s - loss: 0.2880 - acc: 0.8786 - val_loss: 0.3464 - val_acc: 0.8447
Epoch 3/5
25000/25000 [==============================] - 9s - loss: 0.1584 - acc: 0.9404 - val_loss: 0.4398 - val_acc: 0.8313
Epoch 4/5
25000/25000 [==============================] - 9s - loss: 0.0588 - acc: 0.9803 - val_loss: 0.5836 - val_acc: 0.8243
Epoch 5/5
25000/25000 [==============================] - 9s - loss: 0.0182 - acc: 0.9947 - val_loss: 0.8095 - val_acc: 0.8178
貌似最高準確率比單層的LSTM準確率還高一點,另外還可以看到Position Embedding能提高準確率、減弱過擬合。
計算量分析?#
可以看到,事實上Attention的計算量并不低。比如Self Attention中,首先要對XX做三次線性映射,這計算量已經相當于卷積核大小為3的一維卷積了,不過這部分計算量還只是O(n)O(n)的;然后還包含了兩次序列自身的矩陣乘法,這兩次矩陣乘法的計算量都是O(n2)O(n2)的,要是序列足夠長,這個計算量其實是很難接受的。
這也表明,restricted版的Attention是接下來的研究重點,并且將Attention與CNN、RNN混合使用,才是比較適中的道路。
結語?#
感謝Google提供的精彩的使用案例,讓我等在大開眼界之余,還對Attention的認識更深一層。Google的這個成果在某種程度上體現了“大道至簡”的理念,的確是NLP中不可多得的精品。本文圍繞著Google的大作,班門弄斧一番,但愿能夠幫助有需要的讀者更好的理解Attention。最后懇請大家建議和批評。
轉載到請包括本文地址:https://kexue.fm/archives/4765
更詳細的轉載事宜請參考:《科學空間FAQ》
如果您還有什么疑惑或建議,歡迎在下方評論區繼續討論。
如果您覺得本文還不錯,歡迎分享/打賞本文。打賞并非要從中獲得收益,而是希望知道科學空間獲得了多少讀者的真心關注。當然,如果你無視它,也不會影響你的閱讀。再次表示歡迎和感謝!
總結
以上是生活随笔為你收集整理的《Attention is All You Need》浅读(简介+代码)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: torch.max
- 下一篇: 五分钟搭建一个基于BERT的NER模型