Attention出自NMT(神經網絡機器翻譯)以處理文本對齊問題,目前已經在各個領域發光發彩,玩出各種花樣帶出多少文章。而Attention的本質其實就是–加權重。
通用的NMT的架構如上圖所示,其中會由兩個Deep LSTM做encoder 和 decoder。( NMT大部分以Encoder-Decoder結構爲基礎結構,而且特別喜歡bidirectional,但它無法適應在線的場景,所以目前爲止RNN系列在NLP領域中是淘汰趨勢,基本上都可以用transformer做代替了)而文本對齊的問題是 對輸入的一個句子對,這個句子對中相對應部分的映射,比如
- 每天都喜歡我居一點點
- I love juju a little bit every day
那麼每個單詞之間應該如何實現這種對應(特別是這種輸入輸出雙方都不定長),即在翻譯“every day”的時候,顯然對於輸入的句子“每天”的重要性相比於其他詞的重要性是不能比的。那麼對於不同詞的重要性每一時刻都是動態的嗎?那麼究竟應該關注哪些時刻的encoder狀態呢?而且關注的強度是多少呢?
於是可以構想一種打分機制,結合輸入和正在預測的輸出聯合計算當前時刻的Attention:那麼以前一時刻t-1的decoder狀態和某個encoder狀態爲參數,輸出得分,即在BiLSTM的基礎上又額外算了一種權重:然後利用c,在所有輸入的上下文+已經預測的結果去預測下一時刻:
對於每個輸入序列的詞,都有個中間隱層的解釋向量(包含了j和其前後信息),那麼對當前預測詞的貢獻權重α採用softmax方式計算(也被稱爲對齊權值(alignment weights)),即用來衡量某個詞對當前預測詞的匹配度。這樣對所有輸入序列中的詞都通過h對注意力向量c作貢獻,而且每一時刻都會做這樣的動態計算,很簡單,詳細公式如上圖。
- 目標:query —> key-value對的映射 以完成自動加權
- 1 Q與K進行相似度計算得到權重
- 2 softmax歸一化
- 3 應用權重,和value進行加權求和便得到attention
pytorch官方示例:
class AttnDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.dropout_p = dropout_p
self.max_length = max_length
self.embedding = nn.Embedding(self.output_size, self.hidden_size)
self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
self.gru = nn.GRU(self.hidden_size, self.hidden_size)
self.out = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input, hidden, encoder_outputs):
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
#計算權重,cat->linear->softmax一步算完
#cat了Q K,然後讓linear去自己學習權重再softmax
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
#算好的權重乘原向量
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0))
#再用linear融合decoder和Attention結果
output = torch.cat((embedded[0], attn_applied[0]), 1)
output = self.attn_combine(output).unsqueeze(0)
#利用結果預測輸出就可以了
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1)
return output, hidden, attn_weights
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
常用Attention形式
用的比較多的主要有點積,通用,拼接,感知機等,形式如下:
其實還有一些加入正則,局部偏好,採樣微分優化梯度等改進方法,以及其他小trick。
Attention變體
在實踐應用中Attention已經被玩到emmm,很繽紛的程度了。
- Soft-Attention,Hard-Attention,不軟不硬Attention(用hard確定範圍,再用soft在窗口中分配)
- Mutil-Attention,Co-Attention,Cross-Attention,Hierarchical-Attention,Group-Attention,High-order-Attention
- Channel-wise-Attention,Spatial-Attention
- Bahdanau-Attention(https://arxiv.org/abs/1409.0473), Loung-Attention(https://arxiv.org/abs/1508.04025)
升級版:normed_Bahdanau-Attention,和scaled_Loung-Attention.(https://arxiv.org/pdf/1602.07868.pdf)。 - Monotonic-Attention
Attention in CNN
- 直接計算Attention
- 卷積完後計算Attention
- 加入附加信息計算Attention
- 全部結合起來計算
Transformer
但是,attention is all you need 。只要有attention本身就夠了!不止是不要一些無關痛癢的進化,我們不需要RNN!(特別是RNN串行無法並行化,訓練時間太長了),只需要 暴力算算算不要錢 Self-Attention,3種Attention,多套Attention 足矣!
self-attention
首先是self-attention(也被稱爲 scaled dot-product attention),相比普通的注意力,這裏是“自”。普通的方法是別人跟自己家人算相似算權重,“自”則是自己家人相互算權重,即句子中詞和詞之間算權重。:dk是歸一化係數,用來scaled。先看具體的計算方式如下圖:
輸入是‘thinking’和‘machines’的向量和,兩者共享的映射矩陣得到query q,key k和value v,自注意力的自就在於這三個都是從自己的原向量得到的。然後對於權重計算,將和,再divide放縮之後softmax,即可以理解爲要得到‘thinking’的向量,需要將自己的查詢q跟所有其他詞的向量進行一個相似度的比較,然後整個進行加權平均得到最後的權重,再乘value值即得到最後的表示。
- 爲什麼算Q和K要內積?向量的內積以計算兩個向量的相似度,即起到Q查詢K的作用。
- Softmax怎麼做?先exp放大明顯一下,再歸一化算分數。
- 爲什麼歸一化係數?維度越大內積就越大,但不能就這樣判斷重要性(如10維的向量和100維的向量的結果,100維的值顯然會更大),所以要Scala與維度數無關。
- 歸一化d的另一種解釋。加和Attention效果其實要好於點積(因爲積的操作會比和大很多,softmax的結果會偏向梯度小區域),但是點積可以用矩陣來加速,爲了保留點積所以需要除以一個係數嘗試抵消這一點。
- 矩陣乘法實際上是所有詞一起而不是一個一個的並行算。
- 多頭直接拼接再FC降維。
- “自”在哪裏?由於不使用RNN後,原先的隱層h直接變成word,整體看到就是自己通過與自己比較來算Attention,此時K,V是一樣的,即self-attention。
#self-attention
def forward(self, q, k, v, mask=None):
attn = torch.bmm(q, k.transpose(1, 2))
attn = attn / self.temperature
if mask is not None:
attn = attn.masked_fill(mask, -np.inf)
attn = self.softmax(attn)
attn = self.dropout(attn)
output = torch.bmm(attn, v)
return output, attn
Transformer一共使用了三種Attention分別是encoder的self-attention,decoder的mask self-attention,以及連接encoder和decoder之間的cross attention,如上圖的模型結構。
- Encoder-Decoder層,Q來自先前的解碼器,並且K和V來自Encoder的輸出。屬於兩塊地方的Cross部分,Encoder過來的是k和v,output是產生q,可以理解爲目前已經輸出的一些單詞去查詢當前輸入的詞最後翻譯爲我們想要得到的結果。
- Encoder中的Self-attention層。所有的K、V和Q來自同一個地方,都是Encoder中前一層的輸出。
- Decoder中的Self-attention層。它不能計算所有位置,需要遮住decoder中向左的信息流以保持自迴歸屬性。(目前只翻譯出了一半的結果,那麼輸入只能有一部分而不是全部的輸出)
而多套Attention是,上述三種都是multi-head attention,即把self-attention重複做多次如N=8(參數不共享,可並行),然後拼起來,以多套視角對數據進行操作:
其中Encoder和Decoder都有6個子層,每兩個子層之間都使用了殘差(Residual Connection,解決梯度問題) 和歸一化(加快收斂),並dropout了(rate=0.1)再輸出。
#MultiHeadAttention
def forward(self, q, k, v, mask=None):
#歸一化係數,維度和多頭數
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, _ = q.size()
sz_b, len_k, _ = k.size()
sz_b, len_v, _ = v.size()
#不是concat 每兩個子層之間使用殘差形式
residual = q
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
#這裏把batch和分塊數放在一起,便於使用bmm
q = q.permute(2, 0, 1, 3).contiguous().view(-1, len_q, d_k) # (n*b) x lq x dk
k = k.permute(2, 0, 1, 3).contiguous().view(-1, len_k, d_k) # (n*b) x lk x dk
v = v.permute(2, 0, 1, 3).contiguous().view(-1, len_v, d_v) # (n*b) x lv x dv
#多頭
#Masked是考慮到輸出Embedding會偏移一個位置
#錯位:從前到後(LTR)預測下一個詞,從後到前(RTL)預測前一個詞
#確保預測時僅此時刻前的已知輸出,而把後面不該看到的信息屏蔽掉(能看到就作弊了)
mask = mask.repeat(n_head, 1, 1) # (n*b) x .. x ..
output, attn = self.attention(q, k, v, mask=mask)
output = output.view(n_head, sz_b, len_q, d_v)
output = output.permute(1, 2, 0, 3).contiguous().view(sz_b, len_q, -1) # b x lq x (n*dv)
output = self.dropout(self.fc(output))
output = self.layer_norm(output + residual)
return output, attn
更多Transformer的細節源代碼逐行註釋:https://github.com/nakaizura/Source-Code-Notebook/tree/master/Transformer
Transformer運行動圖:
一些訓練trick
soft Attention
hard Attention就是對於某些選定的區域是1,而其他直接爲0,這顯然不太好。soft軟性注意力機制有兩種:普通模式(Key=Value=X)和鍵值對模式(Key!=Value)。其選擇的信息是所有輸入信息在注意力分佈下的期望。
Feed forward
爲了得到更好的更抽象能力的向量而加的,而多個自注意力堆一起也是爲了這種“深度”。
Skip connection
模仿殘差。設計直覺上是至少不必原來差(做了深度學習抽象特徵等一堆事之後並不能保證這個向量結果比原來好),另一方面也是幫助深度學習學習緩解梯度消失。
Layer normalization
Normalization有很多種,但是它們都有一個共同的目的,那就是把輸入轉化成均值爲0方差爲1的數據,儘量不使輸入數據落在激活函數的飽和區。
把普通BN用可學習的參數g和b進行一種可學習的縮放移動。
BatchNorm和LayerNorm的區別?
- BatchNorm — 爲每一個小batch計算每一層的平均值和方差,即是所有樣本的各個維度位置的歸一化,以求梯度的“圓”化。
- LayerNorm — 獨立計算每一層每一個樣本的均值和方差,即歸一某樣本自己維度。
從LayerNorm的優點來看,它對於batch大小是健壯的,並且在樣本級別而不是batch級別工作得更好。
(實際上BN後的輸出,經過網絡層後,仍然不再是歸一化的了。然後不斷BN,會使數據的偏差越來越大,當網絡在反向傳播需要考慮到這些大的偏差,就迫使只能使用較小的學習率來防止梯度消失或者梯度爆炸)
Label smoothing
也是一種soft方法,把絕對的0,1標籤,變成,部分其他地方平分,如[0 1 0 0 0 0]變成[0.02 0.9 0.02 0.02 0.02 0.02]。另一方面如果訓練數據存在誤差(這很常見),通過這種表情平滑使用類權值來修正損失對健壯性都是很有好處的。
Noam learning rate schedule
學習率先直線上升,再指數衰減。
Encoder和Decoder的mask不同
- Encoder中沒有Masked,而Decoder中需要使用Masked,因爲在序列生成過程中,在 i 時刻,大於 i 的時刻都是未知的,只有小於 i 時刻的預測是一樣的,因此需要做Mask來屏蔽,即保持部分的輸出。
爲什麼Transformer可以代替RNN/CNN
RNN其實只比NN多一個前一時刻的向量,本質上仍然是“局部編碼”,而它無法並行速度太慢,至於CNN…無法捕捉長距離。Self-Attention是圖神經網絡的一個特例,且已經可以考慮到前時刻的狀態進行計算,“動態”地生成不同連接的權重,從而處理變長的信息序列。所以也因爲RNN+word2ve的缺點1不能並行2層數太少3考慮不到語境,也就誕生了BERT等模型,這在下一篇文章進行整理。
爲什麼要位置信息?
另外由於Transformer不包含遞歸和卷積結構了,爲了加強有效利用序列的順序特徵,會加入序列中各個Token間相對位置或絕對位置的信息(因爲自注意力中每個詞其實都會對整個序列加權,那麼詞在哪個位置都是一樣的,這顯然和實際句子有順序是相悖的)。BERT一般使用不同頻率的正弦和餘弦函數Embedding:
其中pos是位置,i是維度,位置編碼的每個維度都對應於一個正弦曲線.(容易學會Attend相對位置),在偶數位置用正弦,奇數位置用餘弦,最後把這個positional encoding 與 embedding直接相加,再輸入到下一層。
看公式可以明白sin後面的值是很小的,不管是sin還是cos的週期信號在第一遞減,所以實際上也是位置越遠權重越小。
(不用這種複雜的計算也是可以的,比如用隨着與當前單詞位置距離增大而權重減小等,但是把這種複雜的方法可視化還真的很數學之美。。。如上圖,縱座標是位置從0-50)
爲什麼要多頭 multi-head
- 擴展了模型關注不同位置的能力
- 多組映射子空間
類似CNN多通道,從多個角度以增強信息,利於捕捉更豐富的特徵(特別是自從Transformer逐漸日常化後,不同的Transformer所側重的點確實有很大的不同)。而且,可以並行,時間效率上差別並不大。
Transformer的時間複雜度
LSTM是序列長度 x hidden2,Transformer是序列長度2 x hidden。當hidden大於序列長度時(往往都是這種情況),Transformer比LSTM要快很多。
Adam優化的侷限性
雖然Adam有自適應的學習率有助於模型快速收斂,但結果的泛化能力學不如SGD。這可能是因爲在初期學習率的設置上,太小了在訓練初期的偏差會比較大,太大了有可能收斂不到最佳。(解決:可以用學習率預熱。或者AdamW使用了L2正則,這樣小的權重泛化性能會更好)
Transformer的優缺點
優點
- 每層的計算複雜度低.。LSTM的複雜度是:序列長度n x hidden²,Transformer的複雜度是:序列長度n² x hidden。當序列長度n小於表示維數時,self-attention層速度會很快。
- 利於並行。多套注意力之間互不干擾,一起計算節省時間。
- 模型可解釋很高。注意力具有天生的解釋能力。
缺點
- 對新出現的詞表現不優。
- RNN圖靈完備,而transformer不是。圖靈完備是隻要圖靈機的算力足夠,理論上可以近似任何值的算法。
Transformer做文本分類
文本分類不需要sq2sq,所以只使用Transformer編碼器。即模型圖左邊的內容,得到一個分類概率就行。
class EncoderLayer(nn.Module):
def __init__(self, d_model, n_heads, p_drop, d_ff):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, n_heads)
self.dropout1 = nn.Dropout(p_drop)
self.layernorm1 = nn.LayerNorm(d_model, eps=1e-6)
self.ffn = PositionWiseFeedForwardNetwork(d_model, d_ff)
self.dropout2 = nn.Dropout(p_drop)
self.layernorm2 = nn.LayerNorm(d_model, eps=1e-6)
#圖左邊的邏輯
def forward(self, inputs, attn_mask):
# |inputs| : (batch_size, seq_len, d_model)
# |attn_mask| : (batch_size, seq_len, seq_len)
attn_outputs, attn_weights = self.mha(inputs, inputs, inputs, attn_mask) #多頭注意力
attn_outputs = self.dropout1(attn_outputs) #dropout
attn_outputs = self.layernorm1(inputs + attn_outputs) #層正則
# |attn_outputs| : (batch_size, seq_len(=q_len), d_model)
# |attn_weights| : (batch_size, n_heads, q_len, k_len)
ffn_outputs = self.ffn(attn_outputs) #前向
ffn_outputs = self.dropout2(ffn_outputs) #dropout
ffn_outputs = self.layernorm2(attn_outputs + ffn_outputs) #add+ln
# |ffn_outputs| : (batch_size, seq_len, d_model)
return ffn_outputs, attn_weights
完整代碼:
code:https://github.com/lyeoni/nlp-tutorial/blob/master/text-classification-transformer/