天天看點

深入了解Transformer及其源碼

  深度學習廣泛應用于各個領域。基于transformer的預訓練模型(gpt/bertd等)基本已統治NLP深度學習領域,可見transformer的重要性。本文結合《Attention is all you need》論文與Harvard的代碼《Annotated Transformer》深入了解transformer模型。 Harvard的代碼在python3.6 torch 1.0.1 上跑不通,本文做了很多修改。修改後的代碼位址:Transformer。

1 模型的思想

  Transformer中抛棄了傳統的CNN和RNN,整個網絡結構完全是由Attention機制組成。 作者采用Attention機制的原因是考慮到RNN(或者LSTM,GRU等)的計算是順序的,RNN相關算法隻能從左向右依次計算或者從右向左依次計算,這種機制帶來了兩個問題: 

  (1) 時間片 tt

的計算依賴 t−1t−1

時刻的計算結果,這樣限制了模型的并行能力;

  (2) 順序計算的過程中資訊會丢失,盡管LSTM等門機制的結構一定程度上緩解了長期依賴的問題,但是對于特别長期的依賴現象,LSTM依舊無能為力。

  Transformer的提出解決了上面兩個問題:

  (1) 首先它使用了Attention機制,将序列中的任意兩個位置之間的距離是縮小為一個常量;

  (2) 其次它不是類似RNN的順序結構,是以具有更好的并行性,符合現有的GPU架構。

2 模型的架構

深入了解Transformer及其源碼

  如上圖,transformer模型本質上是一個Encoder-Decoder的結構。輸入序列先進行Embedding,經過Encoder之後結合上一次output再輸入Decoder,最後用softmax計算序列下一個單詞的機率。

3 Embedding

  transformer的輸入是Word Embedding + Position Embedding。

3.1 Word Embedding

  Word embedding在pytorch中通常用 nn.Embedding 實作,其權重矩陣通常有兩種選擇:

  (1)使用 Pre-trained的Embeddings并固化,這種情況下實際就是一個 Lookup Table。

  (2)對其進行随機初始化(當然也可以選擇 Pre-trained 的結果),但設為 Trainable。這樣在 training 過程中不斷地對 Embeddings 進行改進。 

  transformer選擇後者,代碼實作如下:

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)      

  其中d_model表示embedding的次元,即詞向量的次元;vocab表示詞彙表的數量。

3.2 Positional Encoding

  在RNN中,對句子的處理是一個個word按順序輸入的。但在 Transformer 中,輸入句子的所有word是同時處理的,沒有考慮詞的排序和位置資訊。是以,Transformer 的作者提出了加入 “positional encoding” 的方法來解決這個問題。“positional encoding“”使得 Transformer 可以衡量 word 位置有關的資訊。

  如何實作具有位置資訊的encoding呢?作者提供了兩種思路:

  • 通過訓練學習 positional encoding 向量;
  • 使用公式來計算 positional encoding向量。

  試驗後發現兩種選擇的結果是相似的,是以采用了第2種方法,優點是不需要訓練參數,而且即使在訓練集中沒有出現過的句子長度上也能用。

  Positional Encoding的公式如下:

PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i)=sin(pos/100002i/dmodel)

PE(pos,2i+1)=cos(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)

  其中,pospos

指的是這個 word 在這個句子中的位置;2i2i

指的是 embedding 詞向量的偶數次元,2i+12i+1

指的是embedding 詞向量的奇數次元。

具體實作如下:

# 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)      

  注意:"x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)" 這行代碼表示;輸入模型的整個Embedding是Word Embedding與Positional Encoding直接相加之後的結果。

  為什麼上面的兩個公式能展現單詞的相對位置資訊呢?

  我們寫一段代碼取詞向量的4個次元看下:

# 在位置編碼下方,将基于位置添加正弦波。對于每個次元,波的頻率和偏移都不同。
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]])      

  輸出圖像:

深入了解Transformer及其源碼

  可以看到某個序列中不同位置的單詞,在某一次元上的位置編碼數值不一樣,即同一序列的不同單詞在單個緯度符合某個正弦或者餘弦,可認為他們的具有相對關系。

4 Encoder

  Encoder部分是由個層相同小Encoder Layer串聯而成。小Encoder Layer可以簡化為兩個部分:(1)Multi-Head Self Attention (2) Feed-Forward network。示意圖如下:

深入了解Transformer及其源碼

  事實上multi head self attention 和feed forward network之後都接了一層add 和norm這裡先不講,後面4.1.2再講。

4.1 Muti-Head-Attention

  Multi-Head Self Attention 實際上是由h個Self Attention 層并行組成,原文中h=8。接下來我們先介紹Self Attention。

4.1.1 Self-Attention

  self-attention的輸入是序列詞向量,此處記為x。x經過一個線性變換得到query(Q), x經過第二個線性變換得到

key(K)

,  x經過第三個線性變換得到

value(V)

也就是:

  • key = linear_k(x)
  • query = linear_q(x)
  • value = linear_v(x)

用矩陣表示即:

深入了解Transformer及其源碼

  注意:這裡的linear_k, linear_q, linear_v是互相獨立、權重(WQWQ

, WKWK

, $W^V$)是不同的,通過訓練可得到。得到query(Q),key(K),value(V)之後按照下面的公式計算attention(Q, K, V):

Attention(Q,K,V)=Softmax(QKTdk‾‾√)VAttention(Q,K,V)=Softmax(QKTdk)V

用矩陣表示上面的公式即:

深入了解Transformer及其源碼

   這裡Z就是attention(Q, K, V)。

  (1) 這裡dk=dmodel/h=512/8=64dk=dmodel/h=512/8=64

  (2) 為什麼要用dk‾‾√dk

對 QKTQKT

進行縮放呢?

  dkdk

實際上是Q/K/V的最後一個次元,當dkdk

越大,QKTQKT

就越大,可能會将softmax函數推入梯度極小的區域。

  (3) softmax之後值都介于0到1之間,可以了解成得到了 attention weights。然後基于這個 attention weights 對 V 求 weighted sum 值 Attention(Q, K, V)。 

  Multi-Head-Attention 就是将embedding之後的X按次元dmodel=512dmodel=512

切割成h=8h=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

  x 序列經過multi-head-self-attention 之後實際經過一個“add+norm”層,再進入feed-forward network(後面簡稱FFN),在FFN之後又經過一個norm再輸入下一個encoder layer。

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

  Feed-Forward Network可以細分為有兩層,第一層是一個線性激活函數,第二層是激活函數是ReLU。可以表示為:

FFN=max(0,xW1+b1)W2+b2FFN=max(0,xW1+b1)W2+b2

  這層比較簡單,就是實作上面的公式,直接看代碼吧:

# 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與Encoder有所不同,Encoder與Decoder的關系可以用下圖描述(以機器翻譯為例):

深入了解Transformer及其源碼

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):

深入了解Transformer及其源碼

  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

  Mask 的目的是防止 Decoder “seeing the future”,就像防止考生偷看考試答案一樣。這裡mask是一個下三角矩陣,對角線以及對角線左下都是1,其餘都是0。下面是個10次元的下三角矩陣:

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)      

Mask的代碼實作:

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      

  當mask不為空的時候,attention計算需要将x做一個操作:scores = scores.masked_fill(mask == 0, -1e9)。即将mask==0的替換為-1e9,其餘不變。

5.2 Encoder-Decoder Multi-head Attention

  這部分和Multi-head Attention的差別是該層的輸入來自encoder和上一次decoder的結果。具體實作如下:

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

  Decoder的最後一個部分是過一個linear layer将decoder的輸出擴充到與vocabulary size一樣的次元上。經過softmax 後,選擇機率最高的一個word作為預測結果。假設我們有一個已經訓練好的網絡,在做預測時,步驟如下:

  (1)給 decoder 輸入 encoder 對整個句子 embedding 的結果 和一個特殊的開始符号 </s>。decoder 将産生預測,在我們的例子中應該是 ”I”。

  (2)給 decoder 輸入 encoder 的 embedding 結果和 “</s>I”,在這一步 decoder 應該産生預測 “am”。

  (3)給 decoder 輸入 encoder 的 embedding 結果和 “</s>I am”,在這一步 decoder 應該産生預測 “a”。

  (4)給 decoder 輸入 encoder 的 embedding 結果和 “</s>I am a”,在這一步 decoder 應該産生預測 “student”。

  (5)給 decoder 輸入 encoder 的 embedding 結果和 “</s>I am a student”, decoder應該生成句子結尾的标記,decoder 應該輸出 ”</eos>”。

  (6)然後 decoder 生成了 </eos>,翻譯完成。

  這部分的代碼實作:

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

  4 https://zhuanlan.zhihu.com/p/80986272

  5 https://arxiv.org/abs/1706.03762

作者:ZingpLiu

出處:http://www.cnblogs.com/zingp/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。

繼續閱讀