機器學習算法與自然語言處理出品
@公眾號原創專欄作者 Don.hub
單位 | 京東算法工程師
學校 | 帝國理工大學
Transformer
什麼是transformer
為什麼需要用transformer
encoder
decoder
output layer
summary
transformer的缺點
transformer的應用
ref
Transformer什麼是transformer首先我們先說結論:Attention Is All You Need提出的transformer 其實就是 seq2seq + self attention。 代碼實現, 非常清晰
seq2seq 任務指的是輸入和輸出都是序列的任務。例如說法語翻譯成英文。
通常來說,Seq2Seq任務最常見的是使用encoder+decoder的模式,先將一個序列編碼成一個上下文矩陣,在使用decoder來解碼。當然,我們僅僅把context vector作為編碼器到解碼器的輸入。
這樣子往往得不到好的效果,因為我們的編碼器的很多信息都無法完全編碼在這個向量中,並且我們在解碼的時候,對於輸入的每個單詞的權重是不一致的,所以在NMT任務上,還添加了attention的機制。
所以目前來說,我們可以直接先把transformer當成一個黑盒,就是transformer可以當成是一個序列轉碼的模型,只是它其中用了特殊的self-attention的機制。如下圖所示:
在提到為什麼需要用transformer的時候,我們需要了解,在沒有transformer的時候,我們都是用什麼來完成這系列的任務的呢?
其實在之前我們使用的是RNN(或者是其的單向或者雙向變種LSTM/GRU等) 來作為編解碼器。
RNN模塊每次只能夠吃進一個輸入token和前一次的隱藏狀態,然後得到輸出。它的時序結構使得這個模型能夠得到長距離的依賴關係,但是這也使得它不能夠並行計算,模型效率十分低。
當然這邊的的RNN可以通過CNN替換,從而達到並行的效果,可以看到下圖,總共是兩層的卷積層,第一層畫出了兩個filter,每個1D filter的size是2,到了第二層的卷積層的filter的size是3。
第一層的filter考慮的是兩個字之間的關聯,但是到了第二層,考慮了三個前一層輸出的交互,從而考慮到了較長序列之間的關係。比如說這邊序列是 , 第一層只考慮了 , .. 的交互,第二層考慮了 ,而 是前一層兩兩交互關係的結果,所以第二層考慮了 這個序列的結果了。
但是對於CNN每次一般我們的卷積核設的長度為3/5這種較小的值,對於序列長度較長的,比如512,就需要堆疊多層的卷積層,導致模型過於冗雜。
那麼,我們有沒有辦法提出一個新的模型,能夠並行,並且能夠考慮到輸入序列不同token的權重?聰明的科學家們提出了一種新的模型叫做transformer。
其實他就encoder+decoder模式,只是其中的編解碼器採用了self-attention的機制。
當然transformer真的就比RNN好嗎?有人提出,凡事用RNN做的模型,都可以直接用self-attention替代。這個我們會在transformer的缺點中討論。# tranformer的內部結構
transformer其實是由encoder以及decoder不是單一模塊,而是由小的多個sub-encoder block和sub-decoder block組成。
我們來看看transformer的具體結構圖。由下圖所示,它主要由左邊的encoder+input以及右邊的decoder+input+output組成。我們將會一一介紹。
這邊的encoder由input以及多個sub-encoder blocks組成。我們將會先講sub-encoder,再講輸入,因為輸入的設計是為了彌補self-attention的缺陷的。
sub-encoder block首先每個sub-encoder都由兩個主要的部分組成(略過部分細節,之後會寫),分別是self-attention layer以及ffn layer。
具體的實現機制就是:我們的輸入每個詞經過embedding 之後,然後經過self-attention ,根據自己的路徑,經過轉換得到新的輸出vector,最後再經過ffn layer,得到新的輸出,作為下一層sub-encoder的輸入。
首先我們先了解一下self-attention的作用,其實self attention大家並不陌生,比如我們有一句話,the animal didnot cross the street, because it was too tired. 這裡面的it,指代的是the animal。我們在翻譯it的時候會將更多的注意力放在the animal身上,self-attention起的作用跟這個類似,就是關注句子中的每個字,和其它字的關聯關係。參考實現
我們來看看這些詞是怎麼經過multi-head attention,得到轉換的。
首先我們每個字的輸入vector 會經過變換得到三個vector,分別是query , key 以及value , 這些向量是通過輸入 分別和query矩陣 ,key矩陣 ,value矩陣 相乘得來的。query矩陣 ,key矩陣 ,value矩陣 都是訓練時學習而來的。
將 x1 和 WQ weight matrix 做矩陣乘法得到 q1, 即這個字對應的query向量. 類似地,我們最終得到這個字對應query向量,value向量,key向量。- query向量:query顧名思義,是負責尋找這個字的於其他字的相關度(通過其它字的key) - key向量:key向量就是用來於query向量作匹配,得到相關度評分的 - value向量:Value vectors 是實際上的字的表示, 一旦我們得到了字的相關度評分,這些表示是用來加權求和的
得到每個字的 之後,我們要得到每個字和句子中其他字的相關關係,我們只需要把這個字的query去和其他字的key作匹配,然後得到分數,最後在通過其它字的value的加權求和(權重就是哪個分數)得到這個字的最終輸出。
我們來具體看看這個分數是怎麼計算得到的。我們之前看到的都是單個字作self-attention,但是在GPU中,其實整個過程是並行的,一個序列 是同時得到每個 對應的Q,K,V的,這是通過矩陣乘法。
然後每個字與其他字對應的score的算法採用的是Scaled Dot-product Attention
具體就是以下公式
總結來說:
等等,那麼什麼是multi-head呢?首先我們先了解一下什麼是multi-head,其實很簡單,就是我們剛才這個sub-encoder裡面,我們的self-attention,只做了一次, 如果我們引入多個不同的 , 然後重複剛才的步驟,我們就可以得到multi-head了。
在得到多個 向量之後,我們把這些向量concat起來,然後再經過線性變換,得到最終的輸出。
那麼我們為什麼需要multi-head呢?這是因為,他可以提高模型的能力 - 這使得模型能夠關注不同的位置,比如句子經濟。。。,教育。。。,這使得這座城市發展起來了,句子中的這在不同的head中,可以著重關注不同的地方例如經濟,教育。亦或者如下面的慄子。
就像是CNN採用不同的不同的kernel的效果,不同的kernel能過獲取的信息不同,類似的,不同的head,能夠擴展模型的不同表示空間(different representation subspaces),因為我們有不同的QKV,這些都是隨機初始化,然後通過訓練得到最總結果,並且結果往往不同。關於different representation subspaces,舉一個不一定妥帖的例子:當你瀏覽網頁的時候,你可能在顏色方面更加關注深色的文字,而在字體方面會去注意大的、粗體的文字。這裡的顏色和字體就是兩個不同的表示子空間。同時關注顏色和字體,可以有效定位到網頁中強調的內容。使用多頭注意力,也就是綜合利用各方面的信息/特徵。
我覺得也可以把多頭注意力看作是一種ensemble,模型內部的集成。
FFN在self-attention層之後模型會經過FFN層。\begin{equation} FFN(x) = max(0, xW_1 + b_1 )W_2 + b_2 \end{equation} 這邊的實現就是兩層的Dense layer,第一層的激活函數是RELU。
兩個sub-layer的連接並不是直接相連,而是先通過ADD&Normalize層,所謂的ADD&Normalize層,由以下兩個組成
- ADD:將輸入+self-attention的輸出 - Normalize:在經過layer-normalization以及dropout操作。
layer normalization:其實很簡單就是每一條樣本都經過(x-mean) / std, 其mean和std 都是按照單條樣本進行計算的。
input對於encoder的輸入,由於self-attention的機制講沒有考慮輸入序列的順序,但是一個句子的輸入順序其實很重要,例如你喜歡蘋果不,你不喜歡蘋果,兩個句子的含義不同,所以我們需要為輸入embedding添加position encoding。
這邊的position encoding,主要可以分為通過序列的關係可以分為 - 絕對位置:例如每個sequence , 位置都是從0,1..n開始 - 相對位置:位置的表示是由字與字之間的差表示的。相對位置表達Relative Position Representations (RPR)是Shaw et al., 2018,這個論文指出,同一個sequence中使用相對位置更好。
它根據encoding的方式也可以分為, - functional encoding: 這個是指的是通過特定函數的方式,將輸入的位置idx變換為embedding。- parametric encoding:指的是通過embedding loopup的方式,讓模型自己學習位置的embedding 這兩種方式的效果都差不多,但是functional的可以減少模型的參數。
BERT使用的是 parametric absolute positional encoding (PAPE) 而transformer使用的是functional absolute positional encoding (FAPE)。
這邊的函數使用的是正弦位置編碼:
這個編碼函數的可視化結果:
編碼器完成之後我們需要解碼器進行工作,最後一層的輸出會被轉化為一組 attention vectors K and V. 作為encoder-decoder attention層的K,V矩陣使用,這些能夠幫助decoder關注輸入的合適位置。
每一個timestamp的輸出都會被餵給decoder,我們將這個輸出做embedding 輸出在添加position encoding。decoder的解碼工作的停止條件就是知道特殊字符\<end of sentence> 得到了。
input with look-ahead maskdecoder的輸入和encoder的輸入不太一樣,引文decoder的self-attention layer只能夠關注輸出序列當前位置以及之前的字,不能夠關注之後的字。所以這邊需要將這之後的字都添加上mask,即q*k之後加上負無窮(-inf),使得其再經過softmax之後的權重變為0。
The look-ahead mask is used to mask the future tokens in a sequence. In other words, the mask indicates which entries should not be used.look-ahead mask 是用來mask序列的future tokens。具體的做法如下:
def create_look_ahead_mask(size):
mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
return mask # (seq_len, seq_len)
x = tf.random.uniform((1, 3))
temp = create_look_ahead_mask(x.shape[1])
>><tf.Tensor: shape=(3, 3), dtype=float32, numpy=
>>array([[0., 1., 1.],
>> [0., 0., 1.],
>> [0., 0., 0.]], dtype=float32)>
剛看到這邊的時候,我有個問題,就是decoder的每次timestamp的輸入不都是之前的前一次的輸出嗎,如何並行?這不是跟RNN一樣?但是其實在訓練的時候,我們是把所有的target 的序列直接作為decoder的輸入的!然後通過look-ahead mask來模擬不同timestamp。
sample_decoder = Decoder(num_layers=2, d_model=512, num_heads=8,
dff=2048, target_vocab_size=8000,
maximum_position_encoding=5000)
target_input = tf.random.uniform((64, 26), dtype=tf.int64, minval=0, maxval=200)
output, attn = sample_decoder(target_input,
enc_output=sample_encoder_output,
training=False,
look_ahead_mask=None,
padding_mask=None)
在預測的時候,才是真正將decoder的輸出作為下一次的輸入。但這時候模型已經是一個黑盒了。
def evaluate(inp_sentence):
start_token = [tokenizer_pt.vocab_size]
end_token = [tokenizer_pt.vocab_size + 1]
# inp sentence is portuguese, hence adding the start and end token
inp_sentence = start_token + tokenizer_pt.encode(inp_sentence) + end_token
encoder_input = tf.expand_dims(inp_sentence, 0)
# as the target is english, the first word to the transformer should be the
# english start token.
decoder_input = [tokenizer_en.vocab_size] # <start_of_sentence>
output = tf.expand_dims(decoder_input, 0)
for i in range(MAX_LENGTH):
print(output)
enc_padding_mask, combined_mask, dec_padding_mask = create_masks(
encoder_input, output)
predictions, attention_weights = transformer(encoder_input,
output,
False,
enc_padding_mask,
combined_mask,
dec_padding_mask)
# select the last word from the seq_len dimension
predictions = predictions[: ,-1:, :] # (batch_size, 1, vocab_size)
predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)
# return the result if the predicted_id is equal to the end token
if predicted_id == tokenizer_en.vocab_size+1: # <end_of_sentence>
return tf.squeeze(output, axis=0), attention_weights
# concatentate the predicted_id to the output which is given to the decoder
# as its input.
output = tf.concat([output, predicted_id], axis=-1)
return tf.squeeze(output, axis=0), attention_weights
translate("este é um problema que temos que resolver.")
print ("Real translation: this is a problem we have to solve .")
>> tf.Tensor([[8087]], shape=(1, 1), dtype=int32)
>> tf.Tensor([[8087 16]], shape=(1, 2), dtype=int32)
>> tf.Tensor([[8087 16 13]], shape=(1, 3), dtype=int32)
>> tf.Tensor([[8087 16 13 7]], shape=(1, 4), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328]], shape=(1, 5), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10]], shape=(1, 6), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10 14]], shape=(1, 7), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10 14 24]], shape=(1, 8), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10 14 24 5]], shape=(1, 9), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10 14 24 5 966]], shape=(1, 10), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10 14 24 5 966 19]], shape=(1, 11), dtype=int32)
>> tf.Tensor([[8087 16 13 7 328 10 14 24 5 966 19 2]], shape=(1, 12), dtype=int32)
Input: este é um problema que temos que resolver.
Predicted translation: this is a problem that we have to solve it .
Real translation: this is a problem we have to solve .
sub-decoder block 跟encoder幾乎一樣,只是它比普通的encoder多了一個Encoder-Decoder Attention,The 「Encoder-Decoder Attention」 layer和multiheaded self-attention的工作機制一樣,除了它使用的是 Keys 和 Values matrix 是encoder的輸出, 這就意味著,我們decoder的query考慮到了encoder的所有的字了。
decoder的output是一個vector,這時候再經過一個dense層得到vocabulary size的logits,再經過softmax在取argmax得到輸出的字。
class Transformer(tf.keras.Model):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
target_vocab_size, pe_input, pe_target, rate=0.1):
super(Transformer, self).__init__()
self.encoder = Encoder(num_layers, d_model, num_heads, dff,
input_vocab_size, pe_input, rate)
self.decoder = Decoder(num_layers, d_model, num_heads, dff,
target_vocab_size, pe_target, rate)
self.final_layer = tf.keras.layers.Dense(target_vocab_size)
def call(self, inp, tar, training, enc_padding_mask,
look_ahead_mask, dec_padding_mask):
enc_output = self.encoder(inp, training, enc_padding_mask) # (batch_size, inp_seq_len, d_model)
# dec_output.shape == (batch_size, tar_seq_len, d_model)
dec_output, attention_weights = self.decoder(
tar, enc_output, training, look_ahead_mask, dec_padding_mask)
final_output = self.final_layer(dec_output) # (batch_size, tar_seq_len, target_vocab_size)
return final_output, attention_weights
tranformer 的空間以及時間複雜度非常大,sequence length , 達到 ,這是因為每一層的self attention 都要儲 的score用於之後的更新,所以L的長度不能很大,否則會遇到OOM的問題。在這種情況下,如果一個句子特別長, 那麼他就不得不被分為兩個sequence作為輸入,但是這個時候前後句子之間的關係就沒了,但是RNN可以不管多長的輸入都能handle。
運行時間太慢,模型太大
position encoding 使用absolute encoding,而Self-Attention with Relative Position Representations指出了相對位置更好
transformer的應用翻譯等, summary
ref李宏毅transformer
Attention Is All You Need
the-illustrated-transformer
The Evolved Transformer – Enhancing Transformer with Neural Architecture Search
Transformer-XL – Combining Transformers and RNNs Into a State-of-the-art Language Model7
code
Transformer-XLThe motivation for Transformer-XL.首先,為什麼會提出transformerXL呢,它的提出主要是為了解決transformer的問題。我們首先先分析一下RNN以及Transformer的優缺點。
但是segment之間沒有信息傳遞,This problem is called context fragmentation.!
The daughter had a nice umbrella that her mother gave her. daughter and her are in different segment前後句就不能夠了解這個雨傘是他媽媽給的
那麼如果解決這個問題呢?我們其實只需要使用RNN的 hidden state來解決信息的傳遞,我們在不同的segment之間傳入傳遞memory信息。
所以transformer:(1+2: positional embedding, 3: stacks of encoders)
升級變成下圖(注意是embedding/hidden output的concat,不是score的concat)
可以簡單的理解 transformerXL = transformer + RNN => segment-wise的RNN Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context
對於所有的encoder i 除了最後一個encoder
Set h_{-1,i } 為全0矩陣,矩陣形狀和之後的segment的output矩陣形狀一致
當我們計算 segment 0時:
對於所有的encoder i 除了最後一個encoder:
Combine the saved hidden states: h_{-1,i-1} and h_{0,i-1}.
對於所有的encoder i 除了最後一個encoder:
Make a copy of its output h_{0,i }(hidden state).
當我們計算segment 1時:
對於所有的encoder i 除了最後一個encoder:
Combine the saved hidden states: h_{0,i-1} and h_{1,i-1}.
對於所有的encoder i 除了最後一個encoder:
Make a copy of its output h_{1,i }(hidden state).
…
當我們計算 segment t:
對於所有的encoder i 除了最後一個encoder:
Combine the saved hidden states: h_{t-1,i-1} and h_{t,i-1}.
對於所有的encoder i 除了最後一個encoder:
Make a copy of its output h_{t,i }(hidden state).
* This shape will be (batch_size, segment_len, emb_dim).
我們來看看如何Combine the saved hidden states: h_{t-1,i-1} and h_{t,i-1}.,其實很簡單,就是直接直接在 segment 這個維度上面concat起來。
原本的輸出shape(batch_size, segment_len, emb_dim), 現在的combinate之後,輸出變成了(batch_size, 2*segment_len, emb_dim)
值得注意的是,在訓練的時候,我們是不用反向傳播更新我們的memery的,我們的memory是之前的sequence的結果,我們可以在pytorch中設置.requires_grad=False。
how to compute self-attention在做self-attention的時候,輸入的 作為from_tensor 和to_tensor自己attend to 自己, 用來產生Q,K,V矩陣,但是在transformer-XL中,我們的query Q用的仍然是我們的輸 產生,但是K,V,都是用的是 , 其中
softmax 出來的結果:
對於decoder來說我們需要加上一個look-ahead mask,就和trasnformer
我們每次都只concat前一次的 ,這是因為我們認為我們前一次的輸出已經包括了之前所有的信息了。
Absolute Positional Encoding & Memory:如果我們繼續使用之前的absolute positing encoding的話,對於所有的sequence的序列,只要這個字在序列中的位置一樣的話,它的position encoding也會一樣,這樣的話,對於我們concat之後的輸出,我們無法區別每個字的位置。
如下圖:The和that的position encoding完全一樣,模型無法區分兩者位置區別。
所以Transformer-XL 首先分析了position encoding在計算中的作用,然後根據這個結果將交互項轉化為relative position encoding。
The notation refers to the entire row and to the entire column . 經過計算,這個式子可以分為4項。
a) 這一項中沒有包含 位置信息,代表的是在第 行的字應該對第 列的字提供多大的注意力。這是不管他們兩個字的位置信息的。
b) 這一項捕獲的是模型的global attention,指的是一個字在position 應該要對 position 付出多大的注意力。例如兩個字的位置越遠,期望它們之間的注意力越小。
c) 這一項捕獲的是在row i的字對其他位置的關注信息,例如在position i是一個字"狗", 應該要對j=i-1 這個位置特別注意,否則可能出現j=i-1是「熱」, 出現是「熱狗」的情況。
d) 這個是c) 的逆向表示,指的是j的字要pay attention to 位置i的字。
根據這個觀測,轉化relative position 通過了解了每一項的意義,我們了解了兩個字的相對位置對這個score的作用。我們將 b), c) and d) 替換為如下式子。
我們可以看到主要的變化
所以 的公式被替換為:
summary應用和不足最主要的應用是他用在XLNET上 不足的話,memory的公式的設計不好,直接concat。以及relative position encoding的設計也不是很合理。
refDissecting Transformer-XL
Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context
Self-Attention with Relative Position RepresentationsSelf-Attention with Relative Position Representations 論文中還發現,+relative position encoding 在transformer的translation的task上得到了提升,但是結合了absolute以及relative的話,效果沒提升。
論文很短,很容易理解。
首先我們先了解在self-attention中,我們 的計算:
Relation-aware Self-Attention文章中引入了兩個位置相關的向量,量: ,之所以採用 維向量的表示形式,主要是為了套用原來self-attention的計算公式, 因為 的維度是這個。 是在所有的attention layer中共享的。
在引入了這兩個相對位置信息向量之後上式(1)將改編為:
Relative Position RepresentationsRelative Position Representations的目標是給出 的計算方式。作者假設如果序列中兩個元素的距離超過k,則這兩元素之間的位置信息就沒有意義了。同時, 應該只跟相對位置有關,而與 沒有關係。作者直接將 定義為了可訓練的向量,本質上是訓練 和 :
其中clip函數的作用就是截斷 的長度,使得其落在 之間
A矩陣的示意圖,k代表了考慮的距離,箭頭表示的一對相對位置表示。
注意:這邊的主要給出了 的表示方式,這是論文中最難的部分,但是理解了就不難了,其實就是一個一個可訓練的矩陣
Implementtensor reshaping can be used to compute n parallel multiplications of bh×d zand d z×n matrices. Each matrix multiplication computes contributions to eij for all heads and batches, corresponding to a particular sequence position. Further reshaping allows adding the two terms. The same approach can be used to efficiently compute z_iSelf-Attention with Relative Position Representations Self-Attention with Relative Position Representations 解讀
ReformerREFORMER : THE EFFICIENT TRANSFORMER是google 2020 的一篇重量級的文章,文章中主要做了如下的改進,是的模型的複雜度從 變為了 。文章思路還是很清晰的,但是不好理解,需要多讀幾遍,先佔坑。主要解決的痛點是
採用的方式
推薦閱讀
AINLP年度閱讀收藏清單
用 GPT-2 自動寫詩,從五言絕句開始
BERT源碼分析(PART I)
BERT源碼分析(PART II)
Transformers Assemble(PART I)
Transformers Assemble(PART II)
站在BERT肩膀上的NLP新秀們(PART I)
站在BERT肩膀上的NLP新秀們(PART II)
站在BERT肩膀上的NLP新秀們(PART III)
大幅減少GPU顯存佔用:可逆殘差網絡(The Reversible Residual Network)
鼠年春節,用 GPT-2 自動寫對聯和對對聯
transformer-XL與XLNet筆記
AINLP-DBC GPU 雲伺服器租用平臺建立,價格足夠便宜
徵稿啟示 | 稿費+GPU算力+星球嘉賓一個都不少
關於AINLP
AINLP 是一個有趣有AI的自然語言處理社區,專注於 AI、NLP、機器學習、深度學習、推薦算法等相關技術的分享,主題包括文本摘要、智能問答、聊天機器人、機器翻譯、自動生成、知識圖譜、預訓練模型、推薦系統、計算廣告、招聘信息、求職經驗分享等,歡迎關注!加技術交流群請添加AINLP君微信(id:AINLP2),備註工作/研究方向+加群目的。