深入理解Transformer及其源碼解讀
- 2019 年 10 月 24 日
- 筆記
1 模型的思想
Transformer中拋棄了傳統的CNN和RNN,整個網路結構完全是由Attention機制組成。 作者採用Attention機制的原因是考慮到RNN(或者LSTM,GRU等)的計算限制為是順序的,也就是說RNN相關演算法只能從左向右依次計算或者從右向左依次計算,這種機制帶來了兩個問題:
Transformer的提出解決了上面兩個問題:
2 模型的架構

如上圖,transformer模型本質上是一個Encoder-Decoder的結構。輸入序列先進行Embedding,經過Encoder之後結合上一次output再輸入Decoder,最後用softmax計算序列下一個單詞的概率。
3 Embedding
transformer的輸入是Word Embedding + Position Embedding。
3.1 Word Embedding
class Embeddings(nn.Module): def __init__(self, d_model, vocab): super(Embeddings, self).__init__() self.lut = nn.Embedding(vocab, d_model) self.d_model = d_model #表示embedding的維度 def forward(self, x): return self.lut(x) * math.sqrt(self.d_model)
3.2 Positional Embedding
在RNN中,對句子的處理是一個個word按順序輸入的。但在 Transformer 中,輸入句子的所有word是同時處理的,沒有考慮詞的排序和位置資訊。因此,Transformer 的作者提出了加入 “positional encoding” 的方法來解決這個問題。“positional encoding“”使得 Transformer 可以衡量 word 位置有關的資訊。
如何實現具有位置資訊的encoding呢?作者提供了兩種思路:
- 通過訓練學習 positional encoding 向量;
- 使用公式來計算 positional encoding向量。
試驗後發現兩種選擇的結果是相似的,所以採用了第2種方法,優點是不需要訓練參數,而且即使在訓練集中沒有出現過的句子長度上也能用。
# Positional Encoding class PositionalEncoding(nn.Module): "實現PE功能" def __init__(self, d_model, dropout, max_len=5000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(p=dropout) pe = torch.zeros(max_len, d_model) position = torch.arange(0., max_len).unsqueeze(1) div_term = torch.exp(torch.arange(0., d_model, 2) * -(math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) # 偶數列 pe[:, 1::2] = torch.cos(position * div_term) # 奇數列 pe = pe.unsqueeze(0) # [1, max_len, d_model] self.register_buffer('pe', pe) def forward(self, x): x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False) return self.dropout(x)
# 在位置編碼下方,將基於位置添加正弦波。對於每個維度,波的頻率和偏移都不同。 plt.figure(figsize=(15, 5)) pe = PositionalEncoding(20, 0) y = pe.forward(Variable(torch.zeros(1, 100, 20))) plt.plot(np.arange(100), y[0, :, 4:8].data.numpy()) plt.legend(["dim %d"%p for p in [4,5,6,7]])
輸出影像:
可以看到某個序列中不同位置的單詞,在某一維度上的位置編碼數值不一樣,即同一序列的不同單詞在單個緯度符合某個正弦或者餘弦,可認為他們的具有相對關係。
4 Encoder

4.1 Muti-Head-Attention
4.1.1 Self-Attention
key(K)
, x經過第三個線性變換得到value(V)
。- key = linear_k(x)
- query = linear_q(x)
- value = linear_v(x)
用矩陣表示即:
注意:這裡的linear_k, linear_q, linear_v是相互獨立、權重($W^Q$, $W^K$, $W^V$)是不同的,通過訓練可得到。得到query(Q),key(K),value(V)之後按照下面的公式計算attention(Q, K, V):

這裡Z就是attention(Q, K, V)。
(1) 這裡$d_k=d_{model}/h = 512/8 = 64$。
(2) 為什麼要用$sqrt{d_k}$ 對 $QK^T$進行縮放呢?
$d_k$實際上是Q/K/V的最後一個維度,當$d_k$越大,$QK^T$就越大,可能會將softmax函數推入梯度極小的區域。
(3) softmax之後值都介於0到1之間,可以理解成得到了 attention weights。然後基於這個 attention weights 對 V 求 weighted sum 值 Attention(Q, K, V)。
Multi-Head-Attention 就是將embedding之後的X按維度$d_{model}=512$ 切割成$h=8$個,分別做self-attention之後再合併在一起。
源碼如下:
class MultiHeadedAttention(nn.Module): def __init__(self, h, d_model, dropout=0.1): "Take in model size and number of heads." super(MultiHeadedAttention, self).__init__() assert d_model % h == 0 self.d_k = d_model // h self.h = h self.linears = clones(nn.Linear(d_model, d_model), 4) self.attn = None self.dropout = nn.Dropout(p=dropout) def forward(self, query, key, value, mask=None): """ 實現MultiHeadedAttention。 輸入的q,k,v是形狀 [batch, L, d_model]。 輸出的x 的形狀同上。 """ if mask is not None: # Same mask applied to all h heads. mask = mask.unsqueeze(1) nbatches = query.size(0) # 1) 這一步qkv變化:[batch, L, d_model] ->[batch, h, L, d_model/h] query, key, value = [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2) for l, x in zip(self.linears, (query, key, value))] # 2) 計算注意力attn 得到attn*v 與attn # qkv :[batch, h, L, d_model/h] -->x:[b, h, L, d_model/h], attn[b, h, L, L] x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout) # 3) 上一步的結果合併在一起還原成原始輸入序列的形狀 x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k) # 最後再過一個線性層 return self.linears[-1](x)
4.1.2 Add & Norm
class LayerNorm(nn.Module): """構造一個layernorm模組""" def __init__(self, features, eps=1e-6): super(LayerNorm, self).__init__() self.a_2 = nn.Parameter(torch.ones(features)) self.b_2 = nn.Parameter(torch.zeros(features)) self.eps = eps def forward(self, x): "Norm" mean = x.mean(-1, keepdim=True) std = x.std(-1, keepdim=True) return self.a_2 * (x - mean) / (std + self.eps) + self.b_2 class SublayerConnection(nn.Module): """Add+Norm""" def __init__(self, size, dropout): super(SublayerConnection, self).__init__() self.norm = LayerNorm(size) self.dropout = nn.Dropout(dropout) def forward(self, x, sublayer): "add norm" return x + self.dropout(sublayer(self.norm(x)))
注意:幾乎每個sub layer之後都會經過一個歸一化,然後再加在原來的輸入上。這裡叫殘餘連接。
4.2 Feed-Forward Network
# Position-wise Feed-Forward Networks class PositionwiseFeedForward(nn.Module): "實現FFN函數" def __init__(self, d_model, d_ff, dropout=0.1): super(PositionwiseFeedForward, self).__init__() self.w_1 = nn.Linear(d_model, d_ff) self.w_2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): return self.w_2(self.dropout(F.relu(self.w_1(x))))
總的來說Encoder 是由上述小encoder layer 6個串列疊加組成。encoder sub layer主要包含兩個部分:
- SubLayer-1 做 Multi-Headed Attention
- SubLayer-2 做 Feed Forward Neural Network
來看下Encoder主架構的程式碼:
def clones(module, N): "產生N個相同的層" return nn.ModuleList([copy.deepcopy(module) for _ in range(N)]) class Encoder(nn.Module): """N層堆疊的Encoder""" def __init__(self, layer, N): super(Encoder, self).__init__() self.layers = clones(layer, N) self.norm = LayerNorm(layer.size) def forward(self, x, mask): "每層layer依次通過輸入序列與mask" for layer in self.layers: x = layer(x, mask) return self.norm(x)
5 Decoder

Decoder的程式碼主要結構:
# Decoder部分 class Decoder(nn.Module): """帶mask功能的通用Decoder結構""" def __init__(self, layer, N): super(Decoder, self).__init__() self.layers = clones(layer, N) self.norm = LayerNorm(layer.size) def forward(self, x, memory, src_mask, tgt_mask): for layer in self.layers: x = layer(x, memory, src_mask, tgt_mask) return self.norm(x)
Decoder子結構(Sub layer):
Decoder 也是N=6層堆疊的結構。被分為3個 SubLayer,Encoder與Decoder有三大主要的不同:
(1)Decoder SubLayer-1 使用的是 “Masked” Multi-Headed Attention 機制,防止為了模型看到要預測的數據,防止泄露。
(2)SubLayer-2 是一個 Encoder-Decoder Multi-head Attention。
(3) LinearLayer 和 SoftmaxLayer 作用於 SubLayer-3 的輸出後面,來預測對應的 word 的 probabilities 。
5.1 Mask-Multi-Head-Attention
tensor([[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]], dtype=torch.uint8)
def subsequent_mask(size): """ mask後續的位置,返回[size, size]尺寸下三角Tensor 對角線及其左下角全是1,右上角全是0 """ attn_shape = (1, size, size) subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8') return torch.from_numpy(subsequent_mask) == 0
5.2 Encoder-Decoder Multi-head Attention
class DecoderLayer(nn.Module): "Decoder is made of self-attn, src-attn, and feed forward (defined below)" def __init__(self, size, self_attn, src_attn, feed_forward, dropout): super(DecoderLayer, self).__init__() self.size = size self.self_attn = self_attn self.src_attn = src_attn self.feed_forward = feed_forward self.sublayer = clones(SublayerConnection(size, dropout), 3) def forward(self, x, memory, src_mask, tgt_mask): "將decoder的三個Sublayer串聯起來" m = memory x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask)) x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask)) return self.sublayer[2](x, self.feed_forward)
注意:self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask)) 這行就是Encoder-Decoder Multi-head Attention。
query = x,key = m, value = m, mask = src_mask,這裡x來自上一個 DecoderLayer,m來自 Encoder的輸出。
5.3 Linear and Softmax to Produce Output Probabilities
這部分的程式碼實現:
class Generator(nn.Module): """ Define standard linear + softmax generation step。 定義標準的linear + softmax 生成步驟。 """ def __init__(self, d_model, vocab): super(Generator, self).__init__() self.proj = nn.Linear(d_model, vocab) def forward(self, x): return F.log_softmax(self.proj(x), dim=-1)
在訓練過程中,模型沒有收斂得很好時,Decoder預測產生的詞很可能不是我們想要的。這個時候如果再把錯誤的數據再輸給Decoder,就會越跑越偏。這個時候怎麼辦?
(1)在訓練過程中可以使用 “teacher forcing”。因為我們知道應該預測的word是什麼,那麼可以給Decoder喂一個正確的結果作為輸入。
(2)除了選擇最高概率的詞 (greedy search),還可以選擇是比如 “Beam Search”,可以保留topK個預測的word。 Beam Search 方法不再是只得到一個輸出放到下一步去訓練了,我們可以設定一個值,拿多個值放到下一步去訓練,這條路徑的概率等於每一步輸出的概率的乘積。
6 Transformer的優缺點
6.1 優點
(1)每層計算複雜度比RNN要低。
(2)可以進行並行計算。
(3)從計算一個序列長度為n的資訊要經過的路徑長度來看, CNN需要增加卷積層數來擴大視野,RNN需要從1到n逐個進行計算,而Self-attention只需要一步矩陣計算就可以。Self-Attention可以比RNN更好地解決長時依賴問題。當然如果計算量太大,比如序列長度N大於序列維度D這種情況,也可以用窗口限制Self-Attention的計算數量。
(4)從作者在附錄中給出的栗子可以看出,Self-Attention模型更可解釋,Attention結果的分布表明了該模型學習到了一些語法和語義資訊。
6.2 缺點
在原文中沒有提到缺點,是後來在Universal Transformers中指出的,主要是兩點:
(1)實踐上:有些RNN輕易可以解決的問題transformer沒做到,比如複製string,或者推理時碰到的sequence長度比訓練時更長(因為碰到了沒見過的position embedding)。
(2)理論上:transformers不是computationally universal(圖靈完備),這種非RNN式的模型是非圖靈完備的的,無法單獨完成NLP中推理、決策等計算問題(包括使用transformer的bert模型等等)。
7 References
1 http://jalammar.github.io/illustrated-transformer/
2 https://zhuanlan.zhihu.com/p/48508221
3 https://zhuanlan.zhihu.com/p/47063917