Seq2seq Attention
Normal Attention
1. 在decoder端,encoder state要進行一個線性變換,得到r1,可以用全連接,可以用conv,取決於自己,這里不需要加激活函數。
2. decoder端t時刻的輸入和上一時刻的context vector(相當於在這個時刻,用上一時刻的state求context vector,然后再輸入LSTM求出cell output)做一個線性變換(先拼接再輸入到一個全連接網絡),得到LSTM的輸入;LSTM輸出output和state,output可以用來經過一個線性變換求詞表,state用於計算attention,這一個的求attention相當於是為下一步求attention(前面所說到的context vector)
1)在訓練時,第一的時間步驟時,context vector為0
2)在預測時,context vector為encoder輸出的state經過attention后的結果
c. 用state經過一個線性變化,並加上bias,就等於這部分 WS * st + b,得到r2,shape [batch_size, embedd_size]
d. 最后再經過vt * tanh(r1 + r2)得到的r3,再對r3進行求和,得到r4, shape [batch_size, time_step], 最后再經過softmax,shape [batch_size, time_step],這里要經過mask,即讓為0的部分概率為0
加性Attention
當seq2seq decoder段去解碼時,是每個時間步驟去與encoder端的進行attention,所以加性attention為
1. decoder端t時刻的輸入和上一時刻的context vector(相當於在這個時刻,用上一時刻的state求context vector,然后再輸入LSTM求出cell output)做一個線性變換(先拼接再輸入到一個全連接網絡),得到LSTM的輸入;LSTM輸出output和state,output可以用來經過一個線性變換求詞表,state用於計算attention,這一個的求attention相當於是為下一步求attention(前面所說到的context vector)
1)在訓練時,第一的時間步驟時,context vector為0
2)在預測時,context vector為encoder輸出的state經過attention后的結果
2. 用state拼接編碼器的輸出,得到[B, T, embedding1]
1)輸入到第一個全連接神經網絡,不采用bias,激活函數用tanh,shape保持一致
2) 將上一全連接神經網絡的輸出,輸入到下一個全連接神經網絡,不用激活函數,不加bias,shape為[B, T, 1]
3)在列的維度上求softmax,相當於求了每個時間的貢獻度,這里要進行mask
4)相乘encoder端的輸出,得到[B, T, embedding2],這里可以用來求context vector了(只需要在時間的維度上相加就行)
Self Attention
All Attention Is You Need
先來看一個翻譯的例子“I arrived at the bank after crossing the river” 這里面的bank指的是銀行還是河岸呢,這就需要我們聯系上下文,當我們看到river之后就應該知道這里bank很大概率指的是河岸。在RNN中我們就需要一步步的順序處理從bank到river的所有詞語,而當它們相距較遠時RNN的效果常常較差,且由於其順序性處理效率也較低。Self-Attention則利用了Attention機制,計算每個單詞與其他所有單詞之間的關聯,在這句話里,當翻譯bank一詞時,river一詞就有較高的Attention score。利用這些Attention score就可以得到一個加權的表示,然后再放到一個前饋神經網絡中得到新的表示,這一表示很好的考慮到上下文的信息。如下圖所示,encoder讀入輸入數據,利用層層疊加的Self-Attention機制對每一個詞得到新的考慮了上下文信息的表征。Decoder也利用類似的Self-Attention機制,但它不僅僅看之前產生的輸出的文字,而且還要attend encoder的輸出。以上步驟如下動圖所示:
注:Multi-head Attention其實就是多個Self-Attention結構的結合,每個head學習到在不同表示空間中的特征,如下圖所示,兩個head學習到的Attention側重點可能略有不同,這樣給了模型更大的容量
詳解:
1. 對於self-attention來講,Q(Query), K(Key), V(Value)三個矩陣均來自同一輸入,首先我們將QK矩陣相乘,然后為了防止其結果過大,起到了縮放的作用,會除以一個尺度標度 ,其中
為一個query和key向量的維度。再利用Softmax操作將其結果歸一化為概率分布,然后再乘以矩陣V就得到權重求和的表示。該操作可以表示為
2. mask,在Q*KT后shape是[batch_size, Q, K]
1). 先進行key的mask,相當於找出key的padding,讓它softmax后的概率為0,在計算context vector的時候,讓其貢獻為0
a. 先對key的最后一個維度的每一個值進行絕對值,然后再求和,如果詞向量全部是0的話,那么和出來就全部是0,就說明這個時間是padding來的,shape是[batch_size, K]
b. 然后擴展第二個維度,shape是[batch_size, 1, K]
c. 然后進行復制,復制Q次,因為是query的key,有query的長度,shape[batch_size, Q, K]
d. 定義一個極小值,這個值得目的是讓softmax后的值為0
e. 最后讓mask映射到input里面,為0的部分就為極小值,不為0的部分就為原來的值;這里的意思是,讓key中pad的部分在softmax后為0,所以放置極小值
f. 代碼:
padding_num = -2 ** 32 + 1 if type in ("k", "key", "keys"): # Generate masks a = queries.get_shape().as_list() masks = tf.sign(tf.reduce_sum(tf.abs(keys), axis=-1)) # (N, T_k) masks = tf.expand_dims(masks, 1) # (N, 1, T_k) masks = tf.tile(masks, [1, tf.shape(queries)[1], 1]) # (N, T_q, T_k) # Apply masks to inputs paddings = tf.ones_like(inputs) * padding_num outputs = tf.where(tf.equal(masks, 0), paddings, inputs) # (N, T_q, T_k)
2). 對未來信息進行mask,讓self attention的時候看不到未來的詞,即在計算context vector的時候,未來的詞的概率為0,對計算context vector的貢獻為0,這個只在transformer decoder端使用
a. 因為每個時刻只能看到前面的信息,所以這里就使用下三角矩陣,即下三角為1,上三角為0,shape[batch_size, Q, K],每個batch里面的下三角矩陣都是一樣的
b. 定義一個極小的值,目的是讓softmax后的值為0
c. 然后進行映射,讓上三角為0的全部為極小值,下三角的值為原來的值
d. 代碼:
padding_num = -2 ** 32 + 1 elif type in ("f", "future", "right"): diag_vals = tf.ones_like(inputs[0, :, :]) # (T_q, T_k) tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense() # (T_q, T_k) masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(inputs)[0], 1, 1]) # (N, T_q, T_k) paddings = tf.ones_like(masks) * padding_num outputs = tf.where(tf.equal(masks, 0), paddings, inputs)
3). 對query進行mask,讓query中的padding,在計算context vector的時候為0,即要讓query padding部分的時間步驟對K的attention全部為0,這樣在計算context vector后才能為0
a. 先對query的最后一個維度的每一個值進行絕對值,然后再求和,如果詞向量全部是0的話,那么和出來就全部是0,就說明這個時間是padding來的,shape是[batch_size, Q]
b. 對最后一個維度進行擴展,shape是[batch_size, Q, 1]
c. 是用query去attention key,而key有K個時間步驟,所有要對最后一個時間步驟復制K次,如果原來求出來為0,那么復制出來的值也為0。shape是[batch_size, Q, K],
d. 這個時候求出來的值每行中的值都相等。再乘以input,這里是進行點乘;這里為什么不乘以1,我的理解是相當於乘以了一個常數,即query的每個時間步驟乘以了它自己本身。
e. 代碼:
# Generate masks masks = tf.sign(tf.reduce_sum(tf.abs(queries), axis=-1)) # (N, T_q) masks = tf.expand_dims(masks, -1) # (N, T_q, 1) masks = tf.tile(masks, [1, 1, tf.shape(keys)[1]]) # (N, T_q, T_k) # Apply masks to inputs outputs = inputs*masks
4). softmax:這里涉及到多頭,我的理解是將多頭分開,分開以后有[num_heads, batch_size, Q, K],然后進行矩陣相加[batch_size, Q, K],然后再進行softmax
3. 將最后的attention乘以 V,得到的shape是[batch_size, Q, emdedding_size]
4. 將多頭context vector進行復原,例,原來如果詞向量是embedding,切分為8個頭,那么就是[N*8, Q, embedding/8],attention后,再還原就是[N, Q,embedding]
Hierarchical Attention Networks for Document Classification
這里相當於是self attention,在transformer的self attention里面求的是所有的詞對當前詞的貢獻度,而在這個里面是求的當前詞對這段sequence的貢獻度
這里只對詞級別的attention為例進行說明
1. 將輸入進行embedding,shape是[batch_size * num_sentences, sequence, embedding]
2. 將embedding輸入到雙向LSTM或者GRU,並將輸出(不是隱藏層狀態)進行拼接, 得到的shape是[batch_size * num_sequence, sequence, output_size * 2]
3. 將上面得到的輸出進行一個全連接網絡,並用tanh進行激活,得到的shape是[batch_size * num_sequence, sequence, output_size * 2]
4. 將上面得到的輸出進行點乘一個context vector,這個context vector是預定義的,shape是[output_size * 2],可以用於訓練,目的是衡量哪些詞比較重要,得到的shape是[batch_size * num_sequence, sequence, output_size * 2]
5. 將上面得到的輸出進行在最后一個維度求和,得到的shape是[batch_size * num_sequence, sequence]。
6. 進行softmax,進行mask,然后再進行re_normal,具體可以參考pointer-genertor中的mask。得到的是每個詞對於這個sequence的貢獻度,這里就是attention的值, shape是batch_size * num_sequence, sequence]
7. 再將上面得到的結果與雙向LSTM或GRU輸出的進行一個點乘,得到的結果是[batch_size * num_sequence, sequence, output_size * 2]
8. 在將上面得到的結果進行在第一個維度的相加,得到的就是這個sequence的context vector
稀疏Attention
在上面描述到的都是標准的Self Attention。
優點:能夠直接捕捉X中任意兩個向量的關聯,而且易於並行
缺點:從理論上來講,Self Attention的計算時間和顯存占用量都是O(n2)級別的(n是序列長度),這就意味着如果序列長度變成原來的2倍,顯存占用量就是原來的4倍,計算時間也是原來的4倍。當然,假設並行核心數足夠多的情況下,計算時間未必會增加到原來的4倍,但是顯存的4倍卻是實實在在的,無可避免,這也是微調Bert的時候時不時就來個OOM的原因了。因為它要對序列中的任意兩個向量都要計算相似度,得到n2大小的相關度矩陣
從上面缺點來看,如果要減少關聯性的計算,也就是認為每個元素只跟序列內的一部分元素有關,這就是稀疏Attention的基本原理。
Atrous Self Attention
第一個要引入的概念是Atrous Self Attention,中文可以稱之為“膨脹自注意力”、“空洞自注意力”、“帶孔自注意力”等。
很顯然,Atrous Self Attention就是啟發於“膨脹卷積(Atrous Convolution)”,如下右圖所示,它對相關性進行了約束,強行要求每個元素只跟它相對距離為k,2k,3k,…的元素關聯,其中k>1是預先設定的超參數。從下左的注意力矩陣看,就是強行要求相對距離不是k的倍數的注意力為0(白色代表0):
由於現在計算注意力是“跳着”來了,所以實際上每個元素只跟大約 n / k 個元素計算相關性,這樣一來理想情況下運行效率和顯存占用都變成了O(n2/k),也就是說能直接降低到原來的1/k
Local Self Attention
另一個要引入的過渡概念是Local Self Attention,中文可稱之為“局部自注意力”。其實自注意力機制在CV領域統稱為“Non Local”,而顯然Local Self Attention則要放棄全局關聯,重新引入局部關聯。具體來說也很簡單,就是約束每個元素只與前后k個元素以及自身有關聯,如下圖所示:
從注意力矩陣來看,就是相對距離超過kk的注意力都直接設為0。
都是保留了一個2k+1大小的窗口,然后在窗口內進行一些運算,不同的是普通卷積是把窗口展平然后接一個全連接層得到輸出,而現在是窗口內通過注意力來加權平均得到輸出。對於Local Self Attention來說,每個元素只跟2k+1個元素算相關性,這樣一來理想情況下運行效率和顯存占用都變成了O((2k+1)n)∼O(kn),也就是說隨着nn而線性增長,這是一個很理想的性質——當然也直接犧牲了長程關聯性。
Sparse Self Attention
到此,就可以很自然地引入OpenAI的Sparse Self Attention了。我們留意到,Atrous Self Attention是帶有一些洞的,而Local Self Attention正好填補了這些洞,所以一個簡單的方式就是將Local Self Attention和Atrous Self Attention交替使用,兩者累積起來,理論上也可以學習到全局關聯性,也省了顯存。
例:輸入的向量進行兩個Attention,一個是Local Self Attention, 那么輸出的向量都融合了局部的相聯特征,然后第二層用 Atrous Self Attention,雖然它是跳着來,但是因為第一層的輸出融合了局部的輸入向量,所以第二層的輸出理論可以跟任意的輸入向量相關(因為的空洞為k,而k中的每個元素經過了2k+1的局部,所以相當於和任意輸入關聯),也就是說實現了長程關聯。
但是OpenAI沒有這樣做,它直接將兩個Atrous Self Attention和Local Self Attention合並為一個,如
從注意力矩陣上看就很容易理解了,就是除了相對距離不超過k的、相對距離為k,2k,3k,…的注意力都設為0,這樣一來Attention就具有“局部緊密相關和遠程稀疏相關”的特性,這對很多任務來說可能是一個不錯的先驗,因為真正需要密集的長程關聯的任務事實上是很少的。