Attention is all your need 谷歌的超強特征提取網絡——Transformer


過年放了七天假,每年第一件事就是立一個flag——希望今年除了能夠將技術學扎實之外,還希望能夠將所學能夠用來造福社會,好像flag立得有點大了。沒關系,套用一句電影台詞為自己開脫一下——人沒有夢想,和咸魚有什么區別。閑話至此,進入今天主題:Transformer。谷歌於2017年提出Transformer網絡架構,此網絡一經推出就引爆學術界。目前,在NLP領域,Transformer模型被認為是比CNN,RNN都要更強的特征提取器。

Transformer算法簡介

Transformer引入了self-attention機制,同時還借鑒了CNN領域中殘差機制(Residuals),由於以上原因導致transformer有如下優勢:

  • 模型表達能力較強,由於self-attention機制考慮到了句子之中詞與詞之間的關聯,
  • 拋棄了RNN的循環結構,同時借用了CNN中的殘差結構加快了模型的訓練速度。

接下來我們來看看transformer的一些細節:

  • 首先Scaled Dot-Product Attention步驟是transformer的精髓所在,作者引入Q,W,V參數通過點乘相識度去計算句子中詞與詞之間的關聯重要程度。其大致過程如圖所示,筆者將會在實戰部分具體介紹此過程如何實現。


     
    Scaled Dot-Product Attention
  • 第二個是muti-head步驟,直白的解釋就是將上面的Scaled Dot-Product Attention步驟重復執行,然后將每次執行的結果拼接起來,需要注意的是每次重復執行Scaled Dot-Product Attention步驟的參數並不共享。


     
    Multi-Head
  • 第三個步驟就是殘差網絡結構——將muti-head步驟的輸出和原始輸入之間相加。這里不明白的可以參考筆者之前介紹殘差網絡的文章

接下來就是實戰部分,實戰部分只使用了muti-head attention或者說是self-attention的向量表示作為最終特征進行文本分類。

Transformer文本分類實戰

數據載入

下方代碼的作用是將情感分析數據讀入,格式為一句話和一個label:
sen_1 : 1
sen_2 : 0
1代表正面情緒,0代表負面情緒。

#! -*- coding: utf-8 -*- from keras import backend as K from keras.engine.topology import Layer import numpy as np from keras.preprocessing import sequence from keras.layers import * from keras import Model from keras.callbacks import TensorBoard data = np.load("imdb.npz") x_test = data["x_test"] x_train = data["x_train"] y_test = data["y_test"] y_train = data["y_train"] 

數據預處理

由於文本數據長短不一,下面代碼可將數據padding到相同的長度。

from itertools import chain all_word = list(chain.from_iterable(list(x_train))) all_word = set(all_word) max_features = len(all_word) data_train = sequence.pad_sequences(x_train,200) 

Self-attention

這里詳細介紹一下模型最關鍵的部分Scaled Dot-Product Attention的構建過程,如圖一 Scaled Dot-Product Attention:

 
圖一 self-attention

 

  • 1.首先申明三個待優化的參數W_k,W_q,W_v,
  • 2.將輸入X分別和W_k,W_q,W_v進行點乘,得到q_1,k_1,v_1,此過程可以理解成將同一句話中的詞映射到三個不同的向量空間,這里筆者將三個不同的向量空間命名為Q空間,K空間和V空間,如圖二 Query,Key,Value metrix
     
    圖二 Query,Key,Value metrix
  • 3.然后計算Q空間的某一個詞在K空間所以詞向量分別點乘得分,之后將這些得分通過softmax函計算一個重要度系數。然后用計算出來的重要度系數乘上該詞在V空間的詞向量並加和得到該詞最終的詞向量表示,整個過程如圖三 Softmax所示,這樣就可以得到一句話經過self-attention后的向量表示Z
     
    圖三 Softmax

上述整個過程就是Scaled Dot-Product Attention,本質上考慮到了一個句子中不同詞之間的關聯程度,這個過程或多或少增強了句子語義的表達。下方為keras定義的self-attention層的代碼,這里加入了muti-head和mask功能的實現。

class Attention(Layer): def __init__(self, nb_head, size_per_head, **kwargs): self.nb_head = nb_head self.size_per_head = size_per_head self.output_dim = nb_head * size_per_head super(Attention, self).__init__(**kwargs) def build(self, input_shape): self.WQ = self.add_weight(name='WQ', shape=(input_shape[0][-1], self.output_dim), initializer='glorot_uniform', trainable=True) self.WK = self.add_weight(name='WK', shape=(input_shape[1][-1], self.output_dim), initializer='glorot_uniform', trainable=True) self.WV = self.add_weight(name='WV', shape=(input_shape[2][-1], self.output_dim), initializer='glorot_uniform', trainable=True) super(Attention, self).build(input_shape) def Mask(self, inputs, seq_len, mode='mul'): if seq_len == None: return inputs else: mask = K.one_hot(seq_len[:, 0], K.shape(inputs)[1]) mask = 1 - K.cumsum(mask, 1) for _ in range(len(inputs.shape) - 2): mask = K.expand_dims(mask, 2) if mode == 'mul': return inputs * mask if mode == 'add': return inputs - (1 - mask) * 1e12 def call(self, x): # 如果只傳入Q_seq,K_seq,V_seq,那么就不做Mask # 如果同時傳入Q_seq,K_seq,V_seq,Q_len,V_len,那么對多余部分做Mask if len(x) == 3: Q_seq, K_seq, V_seq = x Q_len, V_len = None, None elif len(x) == 5: Q_seq, K_seq, V_seq, Q_len, V_len = x # 對Q、K、V做線性變換 Q_seq = K.dot(Q_seq, self.WQ) Q_seq = K.reshape(Q_seq, (-1, K.shape(Q_seq)[1], self.nb_head, self.size_per_head)) Q_seq = K.permute_dimensions(Q_seq, (0, 2, 1, 3)) K_seq = K.dot(K_seq, self.WK) K_seq = K.reshape(K_seq, (-1, K.shape(K_seq)[1], self.nb_head, self.size_per_head)) K_seq = K.permute_dimensions(K_seq, (0, 2, 1, 3)) V_seq = K.dot(V_seq, self.WV) V_seq = K.reshape(V_seq, (-1, K.shape(V_seq)[1], self.nb_head, self.size_per_head)) V_seq = K.permute_dimensions(V_seq, (0, 2, 1, 3)) # 計算內積,然后mask,然后softmax A = K.batch_dot(Q_seq, K_seq, axes=[3, 3]) / self.size_per_head ** 0.5 A = K.permute_dimensions(A, (0, 3, 2, 1)) A = self.Mask(A, V_len, 'add') A = K.permute_dimensions(A, (0, 3, 2, 1)) A = K.softmax(A) # 輸出並mask O_seq = K.batch_dot(A, V_seq, axes=[3, 2]) O_seq = K.permute_dimensions(O_seq, (0, 2, 1, 3)) O_seq = K.reshape(O_seq, (-1, K.shape(O_seq)[1], self.output_dim)) O_seq = self.Mask(O_seq, Q_len, 'mul') return O_seq def compute_output_shape(self, input_shape): return (input_shape[0][0], input_shape[0][1], self.output_dim) 

位置編碼

接下來定義一個位置編碼層,由於是輸入是句子屬於一個序列,加入位置編碼會使得語義表達更准確。

class Position_Embedding(Layer): def __init__(self, size=None, mode='sum', **kwargs): self.size = size # 必須為偶數 self.mode = mode super(Position_Embedding, self).__init__(**kwargs) def call(self, x): if (self.size == None) or (self.mode == 'sum'): self.size = int(x.shape[-1]) batch_size, seq_len = K.shape(x)[0], K.shape(x)[1] position_j = 1. / K.pow(10000., 2 * K.arange(self.size / 2, dtype='float32') / self.size) position_j = K.expand_dims(position_j, 0) position_i = K.cumsum(K.ones_like(x[:, :, 0]), 1) - 1 # K.arange不支持變長,只好用這種方法生成 position_i = K.expand_dims(position_i, 2) position_ij = K.dot(position_i, position_j) position_ij = K.concatenate([K.cos(position_ij), K.sin(position_ij)], 2) if self.mode == 'sum': return position_ij + x elif self.mode == 'concat': return K.concatenate([position_ij, x], 2) def compute_output_shape(self, input_shape): if self.mode == 'sum': return input_shape elif self.mode == 'concat': return (input_shape[0], input_shape[1], input_shape[2] + self.size) 

而谷歌的論文直接給出了position embedding 層的公式,如下圖所示。

 
position embeding

此公式的含義是將
id
 
p
 
的位置映射為一個
d_{pos}
 
維的位置向量,此向量的第
i
 
個元素的值就是通過上述公式算出來的
PE_i(p)
 
。position embeding背后的物理意義參考於參考文獻第一篇: 由於在數學上有sin(α+β)=sinαcosβ+cosαsinβ以及cos(α+β)=cosαcosβ−sinαsinβ,這表明位置p+k的向量可以表示成位置p的向量的線性變換,這提供了表達p相對位置信息的可能性。

 

模型構建

接下來使用上方定義好的的self-attention層和position embedding層進行模型構建,這里設置的8個head,意味着將self-attention流程重復做8次,這里的代碼實現不是講8個head向量拼接,而是通過keras自帶的 GlobalAveragePooling1D函數將8個head的向量求和平均一下。

K.clear_session()
callbacks = [TensorBoard("log/")] 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])# Self Attention 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) model.summary() 

模型的網絡結構可視化輸出如下:


 
model

模型訓練

將之前預處理好的數據喂給模型,同時設置好batch size 和 epoch就可以跑起來了。由於筆者是使用的是筆記本的cpu,所以只跑一個epoch。

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(data_train, y_train, batch_size=2, epochs=1, callbacks=callbacks, validation_split=0.2) 
 
train

結語

Transformer在各方面性能上都超過了RNN和CNN,但是其最主要的思想還是引入了self-attention,使得模型可以考慮到句子中詞與詞之間的相互聯系,這個思想在NLP很多領域,如機器閱讀(R-Net)中也曾出現。所以如何在embeding時的更好挖掘句子的語義,才是深度學習在nlp領域最需要解決的難題。

參考文獻

https://spaces.ac.cn/archives/4765
https://blog.csdn.net/qq_41664845/article/details/84969266
Attention Is All You Need



作者:王鵬你妹
鏈接:https://www.jianshu.com/p/704893b996f9
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM