Attention is all you need 是一篇將 Attention 思想發揮到極致的論文,出自 Google。這篇論文中提出一個全新的模型,叫 Transformer。
論文地址:https://arxiv.org/abs/1706.03762
接下來我想講講自己對Transformer模型的理解:
**
1.模型結構
**
Transformer模型有傳統的seq2seq一樣,也是由encoder和decoder組成。
**
1.1 Encoder
**
encoder由 6 層相同的層組成,每一個大模塊分別由兩部分組成:
第一部分是 multi-head self-attention
第二部分是 position-wise feed-forward network,是一個全連接層
這個兩個sub_layer部分,都有一個殘差連接(residual connection),然後接着一個 Layer Normalization,故每一個sub_layer輸出可表示爲:
**注意:**Encoder端每個大模塊接收的輸入是不一樣的,第一個大模塊(最底下的那個)接收的輸入是輸入序列的embedding(embedding可以通過word2vec預訓練得來),其餘大模塊接收的是其前一個大模塊的輸出,最後一個模塊的輸出作爲整個Encoder端的輸出。
1.2 Decoder
**
和 encoder 類似,decoder 也是由6個相同的層組成,每一個大模塊包括以下3個部分:
第一個部分是 multi-head self-attention mechanism
第二部分是 multi-head context-attention mechanism
第三部分是一個 position-wise feed-forward network和 encoder 一樣,上面三個部分的每一個部分,都有一個殘差連接,後接一個 Layer Normalization。
decoder 和 encoder 不同的地方在 multi-head context-attention mechanism
同樣需要注意的是,Decoder端每個大模塊接收的輸入也是不一樣的,其中第一個大模塊(最底下的那個)訓練時和測試時的接收的輸入是不一樣的,並且每次訓練時接收的輸入也可能是不一樣的(也就是模型總覽圖示中的"shifted right"),其餘大模塊接收的是同樣是其前一個大模塊的輸出,最後一個模塊的輸出作爲整個Decoder端的輸出。
**
對於第一個大模塊,簡而言之,其訓練及測試時接收的輸入爲:
訓練的時候每次的輸入爲上次的輸入加上輸入序列向後移一位的ground truth(例如每向後移一位就是一個新的單詞,那麼則加上其對應的embedding),特別地,當decoder的time step爲1時(也就是第一次接收輸入),其輸入爲一個特殊的token,可能是目標序列開始的token(如),也可能是源序列結尾的token(如),也可能是其它視任務而定的輸入等等,不同源碼中可能有微小的差異,其目標則是預測下一個位置的單詞(token)是什麼,對應到time step爲1時,則是預測目標序列的第一個單詞(token)是什麼,以此類推;
這裏需要注意的是,在實際實現中可能不會這樣每次動態的輸入,而是一次性把目標序列的embedding通通輸入第一個大模塊中,然後在多頭attention模塊對序列進行mask即可(後邊說的sentence mask)
而在測試的時候,是先生成第一個位置的輸出,然後有了這個之後,第二次預測時,再將其加入輸入序列,以此類推直至預測結束。
2.Attention
**
- scaled dot-product attention
Tansformer模型採用的Attention是scaled dot-product attention,原始論文的描述爲:
An attention function can be described as a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. The output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility of the query with the corresponding key.
通過 query 和 key 的相似性程度來確定 value 的權重分佈,即:
caled dot-product attention 和 dot-product attention 唯一的區別就是,scaled dot-product attention 有一個縮放因子, 叫 。 表示 Key 的維度,默認用 64。
論文裏對於 的作用這麼來解釋:對於很大的時候,點積得到的結果維度很大,使得結果處於softmax函數梯度很小的區域。這時候除以一個縮放因子,可以一定程度上減緩這種情況。caled dot-product attention結構圖如下所示:
現在來說下 K、Q、V 分別代表什麼:
在 encoder 的 self-attention 中,Q、K、V 都來自同一個地方,它們是上一層 encoder 的輸出。對於第一層 encoder,它們就是 word embedding 和 positional encoding 相加得到的輸入。
在 decoder 的 self-attention 中,Q、K、V 也是自於同一個地方,它們是上一層 decoder 的輸出。對於第一層 decoder,同樣也是 word embedding 和 positional encoding 相加得到的輸入。但是對於 decoder,我們不希望它能獲得下一個 time step (即將來的信息,不想讓他看到它要預測的信息),因此我們需要進行 sequence masking。
在 encoder-decoder attention 中,Q 來自於 decoder 的上一層的輸出,K 和 V 來自於 encoder 的輸出,K 和 V 是一樣的。
Q、K、V 的維度都是一樣的,分別用 、 和 來表示
目前可能描述有有點抽象,不容易理解。結合一些應用來說,比如,如果是在自動問答任務中的話,Q 可以代表答案的詞向量序列,取 K = V 爲問題的詞向量序列,那麼輸出就是所謂的 Aligned Question Embedding。
其pytorch實現爲:
import torch
import torch.nn as nn
import torch.functional as F
import numpy as np
class ScaledDotProductAttention(nn.Module):
"""Scaled dot-product attention mechanism."""
def __init__(self, attention_dropout=0.0):
super(ScaledDotProductAttention, self).__init__()
self.dropout = nn.Dropout(attention_dropout)
self.softmax = nn.Softmax(dim=2)
def forward(self, q, k, v, scale=None, attn_mask=None):
"""
前向傳播.
Args:
q: Queries張量,形狀爲[B, L_q, D_q]
k: Keys張量,形狀爲[B, L_k, D_k]
v: Values張量,形狀爲[B, L_v, D_v],一般來說就是k
scale: 縮放因子,一個浮點標量
attn_mask: Masking張量,形狀爲[B, L_q, L_k]
Returns:
上下文張量和attention張量
"""
attention = torch.bmm(q, k.transpose(1, 2))
if scale:
attention = attention * scale
if attn_mask:
# 給需要 mask 的地方設置一個負無窮
attention = attention.masked_fill_(attn_mask, -np.inf)
# 計算softmax
attention = self.softmax(attention)
# 添加dropout
attention = self.dropout(attention)
# 和V做點積
context = torch.bmm(attention, v)
return context, attention
- Multi-head attention
Multi-Head Attention相當於多個不同的self-attention的集成,論文中h=8。論文提到,他們發現將 Q、K、V 通過一個線性映射之後,分成 h 份,對每一份進行 scaled dot-product attention 效果更好。然後,把各個部分的結果合併起來,再次經過線性映射,得到最終的輸出。Multi-Head Attention圖如下所示:
論文中的具體實現爲:
**
3.Layer normalization
Normalization 有很多種,但是它們都有一個共同的目的,那就是把輸入轉化成均值爲 0 方差爲 1 的數據。我們在把數據送入激活函數之前進行 normalization(歸一化),因爲我們不希望輸入數據落在激活函數的飽和區。
說到 normalization,那就肯定得提到 Batch Normalization。
BN 的主要思想就是:在每一層的每一批數據上進行歸一化。我們可能會對輸入數據進行歸一化,但是經過該網絡層的作用後,我們的數據已經不再是歸一化的了。隨着這種情況的發展,數據的偏差越來越大,我的反向傳播需要考慮到這些大的偏差,這就迫使我們只能使用較小的學習率來防止梯度消失或者梯度爆炸。
BN 的具體做法就是對每一小批數據,在批這個方向上做歸一化。
什麼是 Layer normalization 呢?它也是歸一化數據的一種方式,不過 LN 是在每一個樣本上計算均值和方差,而不是 BN 那種在批方向計算均值和方差!
下面看一下 LN 的公式:
**
4.Mask
**
mask 表示掩碼,它對某些值進行掩蓋,使其在參數更新時不產生效果。Transformer 模型裏面涉及兩種 mask,分別是 padding mask 和 sequence mask。
其中,padding mask 在所有的 scaled dot-product attention 裏面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 裏面用到。
- Padding Mask
每個批次輸入序列長度是不一樣的,要對輸入序列進行對齊。具體來說,就是給在較短的序列後面填充 0。因爲這些填充的位置,其實是沒什麼意義的,所以我們的 attention 機制不應該把注意力放在這些位置上,所以我們需要進行一些處理。
具體的做法是,把這些位置的值加上一個非常大的負數(負無窮),這樣的話,經過 softmax,這些位置的概率就會接近0!
def padding_mask(seq_k, seq_q):
# seq_k 和 seq_q 的形狀都是 [B,L]
len_q = seq_q.size(1)
# `PAD` is 0
pad_mask = seq_k.eq(0)
pad_mask = pad_mask.unsqueeze(1).expand(-1, len_q, -1) # shape [B, L_q, L_k]
return pad_mask
- Sequence mask
Decoder端多頭self-attention模塊與Encoder端的一致,但是需要注意的是Decoder端的多頭self-attention需要做mask,因爲它在預測時,是“看不到未來的序列的”,所以要將當前預測的單詞(token)及其之後的單詞(token)全部mask掉。也就是對於一個序列,在 time_step 爲 t 的時刻,我們的解碼輸出應該只能依賴於 t 時刻之前的輸出,而不能依賴 t 之後的輸出。因此我們需要想一個辦法,把 t 之後的信息給隱藏起來。
那麼具體怎麼做呢?也很簡單:產生一個上三角矩陣,上三角的值全爲 1,下三角的值權威0,對角線也是 0。把這個矩陣作用在每一個序列上,就可以達到我們的目的了。
def sequence_mask(seq):
batch_size, seq_len = seq.size()
mask = torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8),
diagonal=1)
mask = mask.unsqueeze(0).expand(batch_size, -1, -1) # [B, L, L]
return mask
注意:
對於 decoder 的 self-attention,裏面使用到的 scaled dot-product attention,同時需要padding mask 和 sequence mask 作爲 attn_mask,具體實現就是兩個 mask 相加作爲attn_mask。
其他情況,attn_mask 一律等於 padding mask。
5.Positional Embedding
我們對每一個 word 進行 embedding 作爲 input 表達。但是還有問題,embedding 本身不包含在句子中的相對位置信息。
那 RNN 爲什麼在任何地方都可以對同一個 word 使用同樣的向量呢?因爲 RNN 是按順序對句子進行處理的,一次一個 word。但是在 Transformer 中,輸入句子的所有 word 是同時處理的,沒有考慮詞的排序和位置信息。
對此,Transformer 的作者提出了加入 ”positional encoding“ 的方法來解決這個問題。”positional encoding“ 使得 Transformer 可以衡量 word 位置有關的信息。論文中的具體實現爲:
pos 指的是這個 word 在這個句子中的位置
i指的是 embedding 維度。比如選擇 d_model=512,那麼i就從1數到512
爲什麼選擇 sin 和 cos ?positional encoding 的每一個維度都對應着一個正弦曲線,作者假設這樣可以讓模型相對輕鬆地通過對應位置來學習。
其具體實現爲:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_seq_len):
"""初始化。
Args:
d_model: 一個標量。模型的維度,論文默認是512
max_seq_len: 一個標量。文本序列的最大長度
"""
super(PositionalEncoding, self).__init__()
# 根據論文給的公式,構造出PE矩陣
position_encoding = np.array([
[pos / np.power(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)]
for pos in range(max_seq_len)])
# 偶數列使用sin,奇數列使用cos
position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])
# 在PE矩陣的第一行,加上一行全是0的向量,代表這`PAD`的positional encoding
# 在word embedding中也經常會加上`UNK`,代表位置單詞的word embedding,兩者十分類似
# 那麼爲什麼需要這個額外的PAD的編碼呢?很簡單,因爲文本序列的長度不一,我們需要對齊,
# 短的序列我們使用0在結尾補全,我們也需要這些補全位置的編碼,也就是`PAD`對應的位置編碼
pad_row = torch.zeros([1, d_model])
position_encoding = torch.cat((pad_row, position_encoding))
# 嵌入操作,+1是因爲增加了`PAD`這個補全位置的編碼,
# Word embedding中如果詞典增加`UNK`,我們也需要+1。看吧,兩者十分相似
self.position_encoding = nn.Embedding(max_seq_len + 1, d_model)
self.position_encoding.weight = nn.Parameter(position_encoding,
requires_grad=False)
def forward(self, input_len):
"""神經網絡的前向傳播。
Args:
input_len: 一個張量,形狀爲[BATCH_SIZE, 1]。每一個張量的值代表這一批文本序列中對應的長度。
Returns:
返回這一批序列的位置編碼,進行了對齊。
"""
# 找出這一批序列的最大長度
max_len = torch.max(input_len)
tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
# 對每一個序列的位置進行對齊,在原序列位置的後面補上0
# 這裏range從1開始也是因爲要避開PAD(0)的位置
input_pos = tensor(
[list(range(1, len + 1)) + [0] * (max_len - len) for len in input_len])
return self.position_encoding(input_pos)
**
6.Position-wise Feed-Forward network
**
這是一個全連接網絡,包含兩個線性變換和一個非線性函數(實際上就是 ReLU)。公式如下:
具體實現爲:
class PositionalWiseFeedForward(nn.Module):
def __init__(self, model_dim=512, ffn_dim=2048, dropout=0.0):
super(PositionalWiseFeedForward, self).__init__()
self.w1 = nn.Conv1d(model_dim, ffn_dim, 1)
self.w2 = nn.Conv1d(ffn_dim, model_dim, 1)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(model_dim)
def forward(self, x):
output = x.transpose(1, 2)
output = self.w2(F.relu(self.w1(output)))
output = self.dropout(output.transpose(1, 2))
# add residual and norm layer
output = self.layer_norm(x + output)
return output
對於Transformer的一些常見問題總結:
1.Transformer爲什麼需要進行Multi-head Attention?這樣做有什麼好處?Multi-head Attention的計算過程?各方論文的觀點是什麼?
Multi-head Attention的原因是將模型分爲多個頭,形成多個子空間,可以讓模型去關注不同方面的信息,最後再將各個方面的信息綜合起來。其實直觀上也可以想到,如果自己設計這樣的一個模型,必然也不會只做一次attention,多次attention綜合的結果至少能夠起到增強模型的作用,也可以類比CNN中同時使用多個卷積核的作用,直觀上講,多頭的注意力有助於網絡捕捉到更豐富的特徵/信息。
2.Transformer相比於RNN/LSTM,有什麼優勢?爲什麼?
(1).RNN系列的模型,並行計算能力很差
RNN系列的模型T時刻隱層狀態的計算,依賴兩個輸入,一個是T時刻的句子輸入單詞X_t,另一個是T−1時刻的隱層狀態的輸出,這是最能體現RNN本質特徵的一點,RNN的歷史信息是通過這個信息傳輸渠道往後傳輸的。而RNN並行計算的問題就出在這裏,因爲T時刻的計算依賴T−1時刻的隱層計算結果,而T−1時刻的計算依賴T-2T−2時刻的隱層計算結果,如此下去就形成了所謂的序列依賴關係。
(2).Transformer的特徵抽取能力比RNN系列的模型要好
相關對比實驗可見:
放棄幻想,全面擁抱Transformer:自然語言處理三大特徵抽取器(CNN/RNN/TF)比較
3.Transformer是如何訓練的?測試階段如何進行測試呢?
Transformer訓練過程與seq2seq類似,首先Encoder端得到輸入的encoding表示,並將其輸入到Decoder端做交互式attention,之後在Decoder端接收其相應的輸入,經過多頭self-attention模塊之後,結合Encoder端的輸出,再經過FFN,得到Decoder端的輸出之後,最後經過一個線性全連接層,就可以通過softmax來預測下一個單(token),然後根據softmax多分類的損失函數,將loss反向傳播即可,所以從整體上來說,Transformer訓練過程就相當於一個有監督的多分類問題。
需要注意的是,Encoder端可以並行計算,一次性將輸入序列全部encoding出來,但Decoder端不是一次性把所有單詞(token)預測出來的,而是像seq2seq一樣一個接着一個預測出來的。
而對於測試階段,其與訓練階段唯一不同的是Decoder端最底層的輸入,具體地前邊講過。