Transformer的原理及實現


transformer是谷歌2017年發表的 attention is all you need 中提到的seq2seq模型,我們常用的Bert和GPT等都是基於transformer衍生的。本文主要參考了wmathor大佬的Transformer 詳解Transformer 的 PyTorch 實現兩篇文章。其中第一篇已經詳細說明了transformer的原理,本文主要結合代碼的實現及自己的理解對代碼進一步說明。全部代碼見我的GitHub庫


數據預處理

數據集及詞表

在數據預處理方面為了降低代碼閱讀的難度,下面的代碼手動輸入了兩隊德語-->英語的句子,以及對應的編碼。為了划分句子,有S、E、P三個標志符號,每個部分的具體作用都在代碼中進行了注釋

'''
@author xyzhrrr
@time 2021/2/3

'''

import math
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
'''
 數據預處理
 並沒有用什么大型的數據集,而是手動輸入了兩對德語→英語的句子,
 還有每個字的索引也是手動硬編碼上去的,主要是為了降低代碼閱讀難度,
 要更多關注模型實現的部分
 S: Symbol that shows starting of decoding input
 E: Symbol that shows starting of decoding output
 P: 如果當前批量數據大小短於時間步驟,將填充空白序列的符號
'''

sentences = [
        # encoder輸入              #decoder輸入          #decoder輸出
        ['ich mochte ein bier P', 'S i want a beer .', 'i want a beer . E'],
        ['ich mochte ein cola P', 'S i want a coke .', 'i want a coke . E']
]
#填充應該是0,因為是翻譯所以兩個語言有兩個詞表
src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4, 'cola' : 5}
#源詞表長度,這里直接填序號了,實際中應該使用詞表查詢
src_vocab_size=len(src_vocab)#輸入長度,one-hot長度
tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'coke': 5, 'S': 6, 'E': 7, '.': 8}
#目標詞表序號

idx2word={i: w for i,w in enumerate(tgt_vocab)}
#序號轉換成此的表

tgt_vocab_size=len(tgt_vocab)#輸出的one-hot長度
src_len=5 #encoder輸入的最大句長
tgt_le=6#decoder輸入和輸出的最大句長
'''

生成張量及數據集處理類實現

下面兩個函數分別生數據張量生成以及數據集處理類,其中第二個直接繼承自torch的實現。

  • 對於setences數據我們知道每一行對應一個德語和英語的對應句子中隊,行數即是本次的batchsize的大小。對其進行划分並按詞表編碼,就可以得到encoder輸入,decoder的輸入、decoder的輸入,這里可以參考下圖:

    上圖是Transformer 模型主要分為兩大部分,分別是 Encoder 和 Decoder。Encoder 負責把輸入(語言序列)隱射成隱藏層(下圖中第 2 步用九宮格代表的部分),然后解碼器再把隱藏層映射為自然語言序列。例如下圖機器翻譯的例子(Decoder 輸出的時候,是通過 N 層 Decoder Layer 才輸出一個 token,並不是通過一層 Decoder Layer 就輸出一個 token,上圖只給出了一層的layer示意圖)。而在訓練時encoder_input和decoder_input都是為了訓練模型去學習數據信息及對照關系,而decoder_output則是用於與實際的預測輸出對比的真實數據,它會將與模型通過學習兩個input后decoder輸出的部分進行損失函數計算來修正模型。

  • 對於class MyDataSet(),繼承自Data.Dataset,是為了講轉化為張量的數據整合成一個數據集便於后續進行數據的加載和處理。最后采用Data.DataLoader對數據集進行加載,這里三個參數分別為選擇的數據集、minibatch的大小,是否打亂數據集。

'''
@:param sentenses 數據集
@:returns 數據的張量
默認的數據是floattensor類型,我們需要整形,所以用longtensor
'''
def make_data(sentenses):
        enc_inputs,dec_inputs,dec_outputs=[],[],[]
        for i in range(len(sentenses)):
           #split()以空格為分隔符,即除掉空格
           enc_input=[[src_vocab[n] for n in sentenses[i][0].split()]]
           #讀取輸入數據,並轉換為序號表示,加入inputs后:[[1, 2, 3, 4, 0], [1, 2, 3, 5, 0]]
           dec_input=[[tgt_vocab[n]for n in sentenses[i][1].split()]]
           #[6, 1, 2, 3, 4, 8], [6, 1, 2, 3, 5, 8]]
           dec_output = [[tgt_vocab[n] for n in sentences[i][2].split()]]
           # [[1, 2, 3, 4, 8, 7], [1, 2, 3, 5, 8, 7]]
           enc_inputs.extend(enc_input)
           dec_inputs.extend(dec_input)
           dec_outputs.extend(dec_output)
           #在列表末尾一次性追加另一個序列中的多個值
           return torch.LongTensor(enc_inputs),torch.LongTensor(dec_inputs),torch.LongTensor(dec_outputs)

enc_inputs,dec_inputs,dec_outputs=make_data(sentences)
# 獲取數據張量
'''
數據處理類
'''
class MyDataSet(Data.Dataset):
        def __init__(self,enc_inputs,dec_inputs,dec_outputs):
                self.enc_inputs=enc_inputs
                self.dec_inputs=dec_inputs
                self.dec_outputs=dec_outputs

        def __len__(self):
                return self.enc_inputs.shape[0]
                #返回行數,即數據集大小
        def __getitem__(self, item):
                return self.enc_inputs[item],self.dec_inputs[item],self.dec_outputs[item]
        #返回對應數據的各項內容
#加載數據集
loader=Data.DataLoader(dataset=MyDataSet(enc_inputs, dec_inputs, dec_outputs),
                       batch_size=2, #批處理大小,一次處理多少數據
                       shuffle=True)

一些參數

下面變量代表的含義依次是

  1. 字嵌入 & 位置嵌入的維度,這倆值是相同的,因此用一個變量就行了
  2. FeedForward 層隱藏神經元個數
  3. Q、K、V 向量的維度,其中 Q 與 K 的維度必須相等,V 的維度沒有限制,不過為了方便起見,我都設為 64
  4. Encoder 和 Decoder 的個數
  5. 多頭注意力中 head 的數量
'''
Transformer Parameters
'''
d_model=512 #embedding size詞嵌入大小
d_ff=2048   # FeedForward dimension,全連接層維度
d_k = d_v = 64  # dimension of K(=Q), V
n_layers = 6  # number of Encoder and Decoder Layer
n_heads = 8  # number of heads in Multi-Head Attention

position encoding

由於 Transformer 模型沒有循環神經網絡的迭代操作,所以我們必須提供每個字的位置信息給 Transformer,這樣它才能識別出語言中的順序關系
現在定義一個位置嵌入的概念,也就是 Positional Encoding,位置嵌入的維度為 [max_sequence_length, embedding_dimension], 位置嵌入的維度與詞向量的維度是相同的,都是 embedding_dimension。max_sequence_length 屬於超參數,指的是限定每個句子最長由多少個詞構成

  • 注意,我們一般以字為單位訓練 Transformer 模型。首先初始化字編碼的大小為 [vocab_size, embedding_dimension],vocab_size 為字庫中所有字的數量,embedding_dimension 為字向量的維度,對應到 PyTorch 中,其實就是 nn.Embedding(vocab_size, embedding_dimension)

  • 論文中使用了 sin 和 cos 函數的線性變換來提供給模型位置信息,需要注意的是所謂的2i和2i+1只是用來表示奇和偶,實際在代碼中都是用2i表示只不過從0和1開始的區別:

\[\begin{gathered} P E(p o s, 2 i)=\sin \left(p o s / 10000^{2 i / d_{\text {model }}}\right) \\ P E(p o s, 2 i+1)=\cos \left(p o s / 10000^{2 i / d_{\text {model }}}\right) \end{gathered} \]

  • 位置函數的具體理解可以參考這篇文章Transformer 中的 Positional Encoding,簡單來說通過控制sin函數的周期性來體現出位置的變化,如下圖隨着縱向觀察,可見隨着 embedding_dimension​序號增大,位置嵌入函數的周期變化越來越平緩

t時刻嵌某個詞位置嵌入的向量如下,這里為2i是因為分了奇和偶如下,具體的見前面的參考博客:

\[\overrightarrow{p_{t}}=\left[\begin{array}{c} \sin \left(\omega_{0} \cdot t\right) \\ \cos \left(\omega_{0} \cdot t\right) \\ \sin \left(\omega_{1} \cdot t\right) \\ \cos \left(\omega_{1} \cdot t\right) \\ \vdots \\ \sin \left(\omega_{\frac{d}{2}-1} \cdot t\right) \\ \cos \left(\omega_{\frac{d}{2}-1} \cdot t\right) \end{array}\right]_{d \times 1} \]

'''
位置編碼
'''
class PositionEncoding(nn.Module):
    def __init__(self,d_model,dropout=0.1,max_len=5000):
        super(PositionEncoding,self).__init__()
        self.droupout=nn.Dropout(p=dropout)
        pe=torch.zeros(max_len,d_model)#初始化位置嵌入position_embedding
        position =torch.arange(0,max_len,dtype=torch.float).unsqueeze(1)
        #創建位置張量,unsquuze增加一個維度,最終生成
        # tensor([[0.],[1.],[2.], [3.], [4.]....]),維度為(max_len,1)
        diV_term=torch.exp(torch.arange(0,d_model,2).float()*(-math.log(10000.0)/d_model))
        #這里可以看筆記
        pe[:,0::2]=torch.sin(position*diV_term)
        pe[:,1::2]=torch.cos(position*diV_term)#從1開始,間隔為2
        pe=pe.unsqueeze(0).transpose(0,1)
        #增加一個行維度並且進行轉置,以一維到二維為例,[[, , , , ,]],->[[],[],[],[]...],
        # unsqueeze后維度為(1,max_len,d_model),然后將前兩個維度轉置,即(max_len,1,d_model),相當於一個max—len(句長)個大小為(1,d_model)的位置編碼
        self.register_buffer('pe',pe)
        #像模型中添加緩沖,名字為pe,這通常用於注冊不應被視為模型參數的緩沖區,即表明pe是模型的一部分而不是參數,
        # https://pytorch.org/docs/stable/generated/torch.nn.Module.html
    def forward(self,x):
        '''
        x: [seq_len, batch_size, d_model]
        x是上下文嵌入,輸入這種形式就是為了后續與pe相加
        '''
        x=x+self.pe[:x.size(0),:]
        #size(0)就是seq_len,將位置嵌入加入到上下文嵌入中,這里用seq_len是因為二者(max_len)長度不一定相同,如過比較短就只計算seq_len長度的,防止維度改變
        return self.droupout(x)

  • 針對第12行的代碼可以寫出它的數學形式,經過轉換與位置公式的結果相同如下。

\[\begin{aligned} e^{\left(2 i *-\log (\operatorname{10000}) * \frac{1}{d_{model}}\right)} &=\frac{1}{e^{\log (\operatorname{10000})} \frac{2 i}{d_{model}}} \\ &=\frac{1}{\operatorname{10000} \frac{2 i}{d_{model}}} \end{aligned} \]

  • 最后,實際的到的pe位置嵌入向量,每一行代表一個字的位置編碼,不同行的區別在於字的位置position(0~maxlen)的不同,而就一行來看,每一列實際上就是隨位置嵌入維度下標變化的情況,可以結合彩圖集pe的式子進行理解

Pad Mask

需要注意的是在本文的代碼中如果值為 src_len 或者 tgt_len 的,則一定會寫清楚,但是有些函數或者類,Encoder 和 Decoder 都有可能調用,因此就不能確定究竟是 src_len 還是 tgt_len,對於不確定的,會記作 seq_len。而對於為何需要進行mask,如下:

'''
針對句子不夠長,加了 pad,因此需要對 pad 進行 mask
       seq_q: [batch_size, seq_len]
       seq_k: [batch_size, seq_len]
       seq_len in seq_q and seq_len in seq_k maybe not equal
由於在 Encoder 和 Decoder 中都需要進行 mask 操作,因此就無法確定這個函數的參數中 seq_len 的值,
如果是在 Encoder 中調用的,seq_len 就等於 src_len;
如果是在 Decoder 中調用的,seq_len 就有可能等於 src_len,也有可能等於 tgt_len(因為 Decoder 有兩次 mask)
    返回的mask用於計算attention時,消除填充的0的影響,可見博客
'''
def get_attn_pad_mask(seq_q,seq_k):
    '''
   這個函數最核心的一句代碼是 seq_k.data.eq(0),
   這句的作用是返回一個大小和 seq_k 一樣的 tensor,只
   不過里面的值只有 True 和 False。
   如果 seq_k 某個位置的值等於 0,那么對應位置就是 True,否則即為 False。
   舉個例子,輸入為 seq_data = [1, 2, 3, 4, 0],seq_data.data.eq(0) 就會返回 [False, False, False, False, True]
    '''
    batch_size,len_q=seq_q.size()
    batch_size,len_k=seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask=seq_k.data.eq(0).unsqueeze(1)
    # [batch_size, 1, len_k], True is masked
    return pad_attn_mask.expand(batch_size, len_q, len_k)
    # [batch_size, len_q, len_k]
    #維度是這樣的,因為掩碼用在softmax之前,那他的維度就是Q*k.T的維度

  • 這里只是先判斷力哪些是掩碼,是掩碼就標True,便於后續操作。並且選擇K進行判斷是因為softmax的計算是

Subsequence Mask

Subsequence Mask 只有 Decoder 會用到,主要作用是屏蔽未來時刻單詞的信息。

'''
Subsequence Mask 只有 Decoder 會用到,
主要作用是屏蔽未來時刻單詞的信息。首先通過 np.ones() 生成一個全 1 的方陣,
然后通過 np.triu() 生成一個上三角矩陣,
'''
def get_attn_subsequence_mask(seq):
    '''
        seq: [batch_size, tgt_len]
    '''
    attn_shape=[seq.size(0),seq.size(1),seq.size(1)]
    subsequence_mask=np.triu(np.ones(attn_shape),k=1)
    #形成上三角矩陣,其中k=1就是對角線位置向上移一個對角線,可看原博客
    subsequence_mask=torch.from_numpy(subsequence_mask).byte()
    #轉換為tensor,byte就是大小為8bite的int
    return subsequence_mask
    # [batch_size, tgt_len, tgt_len]

ScaledDotProductAttention 計算上下文向量

結合下面的圖,也可以參考這里的動圖

'''
計算上下文向量
這里要做的是,通過 Q 和 K 計算出 scores,
然后將 scores 和 V 相乘,得到每個單詞的 context vector
'''
class ScaleDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaleDotProductAttention, self).__init__()
    def forward(self,Q,K,V,attn_mask):
        '''
        Q: [batch_size, n_heads, len_q, d_k]
        K: [batch_size, n_heads, len_k, d_k]
        V: [batch_size, n_heads, len_v(=len_k), d_v]
        如果len_v不等於len_k則后續無法計算context
        attn_mask: [batch_size, n_heads, seq_len, seq_len]
        '''
        scores=torch.matmul(Q,K.transpose(-1,-2))/np.sqrt(d_k)
        # scores : [batch_size, n_heads, len_q, len_k]
        scores.masked_fill_(attn_mask,-1e9)
        #masked_fill_()函數可以將attn_mask中為1(True,也就是填充0的部分)的位置填充為-1e9
        #相當於對填0的地方加上一個極小值以消除在計算attention時softmax時的影響。
        attn=nn.Softmax(dim=-1)(scores)
        #對行進行softmax,每一行其實就是一個字的注意力機制,可以看博客
        context=torch.matmul(attn,V)
        # [batch_size, n_heads, len_q, d_v]
        return context,attn
  • 需要注意每一行即一個數據集代表一個句子的計算,而我們計算softmax是按照最后一個維度也就相當於[q_len,k_len]的最后一個維度,即按照列之間進行計算,那么每一行(這里指的是q_Len的)就代表一個字。但是對更細節的比如為什么選擇用seq_k檢測是否有掩碼

MultiHeadAttention 多頭注意力機制

完整代碼中一定會有三處地方調用 MultiHeadAttention(),Encoder Layer 調用一次,傳入的 input_Q、input_K、input_V 全部都是 encoder_inputs;Decoder Layer 中兩次調用,第一次傳入的全是 decoder_inputs,第二次傳入的分別是 decoder_outputs,encoder_outputs,encoder_outputs.

  • 其實際的作用就是講輸入的三個部分進行集權計算並計算自注意力,最后然后輸出結果。也一=因此會調用前面的生成掩碼以及計算上下文向量
  • 從結構圖中可以看到多頭注意力實際上后面還跟着一個殘差和LayerNorm歸一化操作,這里也一並實現

class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        self.W_Q=nn.Linear(d_model,d_k*n_heads,bias=False)
        #輸入維度為embedding維度,輸出維度為Q(=K的維度)的維度*頭數,
        # bias為False就是不要學習偏差,只更新權重即可(計算的就是權重)
        self.W_K=nn.Linear(d_model,d_k*n_heads,bias=False)
        self.W_V=nn.Linear(d_model,d_v*n_heads,bias=False)
        self.fc=nn.Linear(n_heads*d_v,d_model,bias=False)
        #通過一個全連接層將維度轉為embedding維度好判斷預測結果
    def forward(self,input_Q,input_K,input_V,attn_mask):
        '''
        input_Q: [batch_size, len_q, d_model]
        input_K: [batch_size, len_k, d_model]
        input_V: [batch_size, len_v(=len_k), d_model]
        attn_mask: [batch_size, seq_len, seq_len]
        '''
        residual,batch_size=input_Q,input_Q.size(0)
        #residual,剩余的,用於后續殘差計算
# (B, S, D) -proj-> (B, S, D_new) -split-> (B, S, H, W) -trans-> (B, H, S, W)
        Q=self.W_Q(input_Q).view(batch_size,-1,n_heads,d_k).transpose(1,2)
        # Q: [batch_size, n_heads, len_q, d_k],-1就是在求長度
        #其實self.W_Q就是一個線性層,輸入的時input_Q,然后對輸出進行變形,
        # 這也是linear的特點,即只需要最后一個滿足維度就可以即[batch_size,size]中的size
        K=self.W_K(input_K).view(batch_size,-1,n_heads,d_k).transpose(1,2)
        # K: [batch_size, n_heads, len_k, d_k]
        V=self.W_V(input_V).view(batch_size,-1,n_heads,d_v).transpose(1,2)
        # V: [batch_size, n_heads, len_v(=len_k), d_v]
        '''     
        我們知道為了能夠計算上下文context我們需要len_v==len_k,這就要求d_v=d_k
        所以實際上Q、K、V的維度都是相同的
        我猜測這里僅將Q、K一起表示是為了便於管理參與加權計算的和不參與的。
        '''
        attn_mask=attn_mask.unsqueeze(1).repeat(1,n_heads,1,1)
        # attn_mask : [batch_size, n_heads, seq_len, seq_len]
        #根據生成attn_mask的函數生成的大小應該為# [batch_size, len_q, len_k]
        #所以顯示增加了一個1個列的維度變為[batch_size, 1,len_q, len_k]在通過repeat變為上面結果
        context,attn=ScaleDotProductAttention()(Q,K,V,attn_mask)
        context=context.transpose(1,2).reshape(batch_size,-1,n_heads*d_v)
        # [batch_size, n_heads, len_q, d_v]->[batch_size, len_q, n_heads * d_v],為了最后一個維度符合全連接層的輸入
        output=self.fc(context)# [batch_size, len_q, d_model]
        return nn.LayerNorm(d_model).cuda()(output+residual), attn
        
  • 最后進行殘差運算以及通過LayerNorm把神經網絡中隱藏層歸一為標准正態分布,也就是獨立同分布以起到加快訓練速度,加速收斂的作用
  • 殘差連接實際上是為了防止防止梯度消失,幫助深層網絡訓練

FeedForward Layer 前饋連接層

就是做兩次線性變換,殘差連接后再跟一個 Layer Norm,需要注意,每個 Encoder Block 中的 FeedForward 層權重都是共享的

  • 前饋連接層的作用如下:

class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.fc=nn.Sequential(
            nn.Linear(d_model,d_ff,bias=False),
            nn.ReLU(),
            nn.Linear(d_ff,d_model,bias=False)
        ) #torch.nn.Sequential是一個Sequential容器,模塊將按照構造函數中傳遞的順序添加到模塊中。
        #先映射到高維在回到低維以學習更多的信息

    def forward(self,inputs):
        '''
        inputs: [batch_size, seq_len, d_model]
        '''
        residual=inputs
        outputs=self.fc(inputs)
        return nn.LayerNorm(d_model).cuda()(outputs+residual)
        # [batch_size, seq_len, d_model]

Encoder Layer

我們按照下圖的結構講所有部件進行串聯就得到了encoder layer,灰框內就是layer。

'''
encoder layer
就是將上述組件拼起來
'''
class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn=MultiHeadAttention()#多頭注意力層
        self.pos_ffn=PoswiseFeedForwardNet()#前饋層,注意殘差以及歸一化已經在各自層內實現
    def forward(self,enc_inouts,enc_self_attn_mask):
        '''
        enc_inputs: [batch_size, src_len, d_model]
        nc_self_attn_mask: [batch_size, src_len, src_len]
        '''
        enc_outputs,attn=self.enc_self_attn(enc_inouts,enc_inouts,enc_inouts,enc_self_attn_mask)
        #三個inputs對應了input_Q\K\V.attn其實就是softmax后沒有乘以V之前的值。
        enc_outputs=self.pos_ffn(enc_outputs)# enc_outputs: [batch_size, src_len, d_model]
        return enc_outputs, attn

Encoder

一個encoder往往有多層encodelayer ,我們使用nn.ModuleList() 里面的參數是列表,列表里面存了 n_layers 個 Encoder Layer,這里我們設定n_layers為8

'''
Encode實現,即將多個encoderlayer套起來
使用 nn.ModuleList() 里面的參數是列表,列表里面存了 n_layers 個 Encoder Layer
由於我們控制好了 Encoder Layer 的輸入和輸出維度相同(最后一個維度都變味了embedding維度大小),
所以可以直接用個 for 循環以嵌套的方式,
將上一次 Encoder Layer 的輸出作為下一次 Encoder Layer 的輸入
 '''
class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        self.src_emb=nn.Embedding(src_vocab_size,d_model)
        #src_vocab_size實際上就是輸入詞表大小,也就是用one-hot表示的長度,d_model是embedding長度
        self.pos_emb=PositionEncoding(d_model)
        self.layers=nn.ModuleList([EncoderLayer() for _ in range(n_layers)])
    def forward(self,enc_inputs):
        '''
        enc_inputs: [batch_size, src_len]
        '''
        enc_outputs=self.src_emb(enc_inputs) # [batch_size, src_len, d_model]
        enc_outputs=self.pos_emb(enc_outputs.transpose(0,1)).transpose(0,1)
        # [batch_size, src_len, d_model],這樣改變維度是為了與pe維度匹配(可以看實現部分的注釋)
        enc_self_attn_mask=get_attn_pad_mask(enc_inputs,enc_inputs)
        # [batch_size, src_len, src_len]
        enc_self_attns=[]
        '''
        可以看見所有encoder block都是用的一個mask
        此外,每個 Encoder Block 中的 FeedForward 層權重都是共享的,雖然我沒看出來咋共享的
        '''
        for layer in self.layers:
            enc_outputs,enc_self_attn=layer(enc_outputs,enc_self_attn_mask)
            enc_self_attns.append(enc_self_attn)
        return enc_outputs,enc_self_attns

Decoder Layer

在 Decoder Layer 中會調用兩次 MultiHeadAttention,第一次是計算 Decoder Input 的 self-attention,得到輸出 dec_outputs。然后將 dec_outputs 作為生成 Q 的元素,enc_outputs 作為生成 K 和 V 的元素,再調用一次 MultiHeadAttention,得到的是 Encoder 和 Decoder Layer 之間的 context vector。最后將 dec_outptus 做一次維度變換,然后返回.
因為需要來自encoder的輸入,所以按照下圖講部件組裝好就好,同樣灰框內就是layer:

class DecoderLayer(nn.Module):
    def __init__(self):
        super(DecoderLayer, self).__init__()
        self.dec_self_attn=MultiHeadAttention()
        self.dec_enc_attn=MultiHeadAttention()
        self.pos_ffn=PoswiseFeedForwardNet()
    def forward(self,dec_inputs,enc_outputs,dec_self_attn_mask,dec_enc_attn_mask):
        '''
        dec_inputs: [batch_size, tgt_len, d_model]
        enc_outputs: [batch_size, src_len, d_model]
        dec_self_attn_mask: [batch_size, tgt_len, tgt_len]
        dec_enc_attn_mask: [batch_size, tgt_len, src_len]
        '''
        dec_outputs,dec_self_attn=self.dec_self_attn(dec_inputs,dec_inputs,dec_inputs,dec_self_attn_mask)
        # dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len]
        dec_outputs,dec_enc_attn=self.dec_enc_attn(dec_outputs,enc_outputs,enc_outputs,dec_enc_attn_mask)
        # dec_outputs: [batch_size, tgt_len, d_model], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
        dec_outputs=self.pos_ffn(dec_outputs)# [batch_size, tgt_len, d_model]
        return dec_outputs,dec_self_attn,dec_enc_attn

Decoder

同樣按照上圖組裝,需要注意的是decoder的輸入是需要將當前時刻后的數據mask掉的,並且最還需要再一次的歸一化和softmax求得最后的翻譯結果,但是由於在進行計算損失函數時nn.CrossEntropyLoss () 里面算了softmax,所以transformer中最后的softmax就省略了。

'''
Decoder 
'''
class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.tgt_emb=nn.Embedding(tgt_vocab_size,d_model)
        self.pos_emb=PositionEncoding(d_model)
        self.layers=nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

    def forward(self,dec_inputs,enc_inputs,enc_outputs):
        '''
        dec_inputs: [batch_size, tgt_len]
        enc_intpus: [batch_size, src_len]
        enc_outputs: [batch_size, src_len, d_model]
        '''
        dec_outputs=self.tgt_emb(dec_inputs)#[batch_size, tgt_len, d_model]
        dec_outputs=self.pos_emb(dec_outputs.transpose(0,1)).transpose(0,1).cuda()
        # [batch_size, tgt_len, tgt_len]
        '''
        Decoder 中不僅要把 "pad"mask 掉,還要 mask 未來時刻的信息,
        因此就有了下面這三行代碼,其中 torch.gt(a, value) 的意思是,
        將 相加后的mask中各個位置上的元素和 0 比較,若大於 0,則該位置取 1,否則取 0
        '''
        dec_self_attn_pad_mask=get_attn_pad_mask(dec_inputs,dec_inputs).cuda()
        # [batch_size, tgt_len, tgt_len],這是獲取用於計算self-attention的mask
        dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).cuda()
        # [batch_size, tgt_len, tgt_len],這是用於屏蔽未來時刻單詞的信息的mask
        dec_self_attn_mask=torch.gt((dec_self_attn_pad_mask+dec_self_attn_subsequence_mask),0).cuda()
        # [batch_size, tgt_len, tgt_len]
        #torch.gt(a,b)函數比較a中元素大於(這里是嚴格大於)b中對應元素,大於則為1,不大於則為0,
        # 這里a為Tensor,b可以為與a的size相同的Tensor或常數。
        dec_enc_attn_mask=get_attn_pad_mask(dec_inputs,enc_inputs)
        # [batc_size, tgt_len, src_len],我想可能是因為第二個部分有encoder和第一個decoder兩個的輸出,所以需要考慮兩個的輸入mask
        dec_self_attns,dec_enc_attns=[],[]
        for layer in self.layers:
            '''
            dec_outputs: [batch_size, tgt_len, d_model], 
            dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len], 
            dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
            '''
            dec_outputs,dec_self_attn,dec_enc_attn=layer(dec_outputs,enc_outputs,dec_self_attn_mask,dec_enc_attn_mask)
            dec_self_attns.append(dec_self_attn)
            dec_enc_attns.append(dec_enc_attn)
        return dec_outputs,dec_self_attns,dec_enc_attns

Transformer

我們按照transformer的整體結構圖將decoder和encoder連接起來,要注意其中信息的傳遞,如下圖

  • 最后返回 dec_logits 的維度是 [batch_size * tgt_len, tgt_vocab_size],可以理解為,一個句子,這個句子有 batch_size*tgt_len 個單詞,每個單詞有 tgt_vocab_size 種情況,取概率最大者
class Transformer(nn.Module):
    def __init__(self):
        super(Transformer, self).__init__()
        self.encoder=Encoder().cuda()
        self.decoder=Decoder().cuda()
        self.projection=nn.Linear(d_model,tgt_vocab_size,bias=False).cuda()
    def forward(self,enc_inputs,dec_inputs):
        '''
        enc_inputs: [batch_size, src_len]
        dec_inputs: [batch_size, tgt_len]
        '''
        enc_outputs,enc_self_attn=self.encoder(enc_inputs)
        # enc_outputs: [batch_size, src_len, d_model],
        # enc_self_attns: [n_layers, batch_size, n_heads, src_len, src_len]
        dec_outputs,dec_self_attns,dec_enc_attns=self.decoder(dec_inputs,enc_inputs,enc_outputs)
        # dec_outpus: [batch_size, tgt_len, d_model],
        # dec_self_attns: [n_layers, batch_size, n_heads, tgt_len, tgt_len],
        # dec_enc_attn: [n_layers, batch_size, tgt_len, src_len]
        dec_logits=self.projection(dec_outputs)
        # dec_logits: [batch_size, tgt_len, tgt_vocab_size]
        return dec_logits.view(-1,dec_logits.size(-1)), enc_self_attn,dec_self_attns,dec_enc_attns

模型 & 損失函數 & 優化器

這里的損失函數里面我設置了一個參數 ignore_index=0,因為 "pad" 這個單詞的索引為 0,這樣設置以后,就不會計算 "pad" 的損失(因為本來 "pad" 也沒有意義,不需要計算),關於這個參數更詳細的說明,可以看這篇文章的最下面,稍微提了一下

model=Transformer().cuda()
criterion=nn.CrossEntropyLoss(ignore_index=0)
# nn.CrossEntropyLoss () 里面算了softmax,所以transformer中最后的softmax就省略了。
#因為 "pad" 這個單詞的索引為 0,這樣設置以后,就不會計算 "pad" 的損失(因為本來 "pad" 也沒有意義,不需要計算)
optimizer=optim.SGD(model.parameters(),lr=1e-3,momentum=0.99)
# –動量系數

訓練

訓練階段與傳統的訓練並沒有很大的差異,需要注意的時zaidecoder部分也有輸入。


for epoch in range(1000):
    for enc_inputs,dec_inputs,dec_outputs in loader:
        '''
        enc_inputs: [batch_size, src_len]
        dec_inputs: [batch_size, tgt_len]
        dec_outputs: [batch_size, tgt_len]
        '''
        enc_inputs,dec_inputs,dec_outputs=enc_inputs.cuda(),dec_inputs.cuda(),dec_outputs.cuda()
        outputs,enc_self_attns,dec_self_attns,dec_enc_attns=model(enc_inputs,dec_inputs)
        # outputs: [batch_size * tgt_len, tgt_vocab_size]
        loss=criterion(outputs,dec_outputs.view(-1))
        #dec_outputs變成大小batch_size * tgt_len的一維的結構。並不是變成行向量
        print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))
        optimizer.zero_grad()#梯度清零
        loss.backward()#反向傳播計算梯度
        optimizer.step()#梯度清零

測試

這里應為最后的輸出是一個句子,所以用到了貪心搜索,簡單來說在知道第一個詞的情況下選出第二個詞的前幾名的概率,比如選出三個較大的后,在對這三個依次預測第三個詞並同樣保持概率最高的三個,以此類推。

def greedy_decoder(model,enc_input,start_symbol):
    '''
    為簡單起見,當 K=1 時,貪心解碼器是波束搜索。
    這對於推理是必要的,因為我們不知道目標序列輸入。
    因此,我們嘗試逐字生成目標輸入,然后將其輸入到transformer中。
    :param start_symbol:開始符號。 在這個例子中,它是“S”,
    最后返回了一個經過多次decoder計算出來的一個初步的句子,
    並且可以看到,在這里的decoder和seq2seq的思想一個不斷把前面預測出的結果進行輸入
    '''
    enc_outputs,enc_self_attns=model.encoder(enc_input)
    dec_input=torch.zeros(1,0).type_as(enc_input.data)
    terminal=False
    next_symbol=start_symbol
    while not terminal: #若terminal為假
        dec_input=torch.cat([dec_input.detach(),torch.tensor([[next_symbol]],dtype=enc_input.dtype).cuda()],  -1)
        #detach是將某個node變成不需要梯度的Varibale。因此當反向傳播經過這個node時,梯度就不會從這個node往前面傳播。
        #因為是預測時期,所以這里沒有反向傳播所以應該沒有梯度積累
        #不斷地將next_symbol加入dec_input,知道結尾,開始時也加入了S,-1是指以最后一維維度為基准
        dec_outputs,_,_=model.decoder(dec_input,enc_input,enc_outputs)
        projected=model.projection(dec_outputs)#最后的預測結果,為啥也沒有softmax啊
        # projected:[batch_size, tgt_len, tgt_vocab_size]
        prob=projected.squeeze(0).max(dim=-1,keepdim=False)[1]
        #首先去掉第一個維,然后求最后一個維度的最大值,不保留維度,即把所有的結果存在一行中,然后選取下標為1的
        next_word=prob.data[-1]
        next_symbol=next_word
        if next_symbol==tgt_vocab["."]:#這表示到了一個句子的末尾,此時dec_input已經存了一個句子的結果。
            terminal=True
        print(next_word)
    return dec_input

  • 最后就是一些傳統的操作。
enc_inputs,_,_=next(iter(loader))
enc_inputs = enc_inputs.cuda()
for i in range(len(enc_inputs)):
    greedy_dec_input=greedy_decoder(model,enc_inputs[i].view(1,-1),start_symbol=tgt_vocab["S"])
    #enc_inputs[i]每次輸入的是一個句子,並變成一行,n列的矩陣(n,就是句子長度)
    predict,_,_,_=model(enc_inputs[i].view(1,-1),greedy_dec_input)#又預測了一遍
    predict=predict.data.max(1,keepdim=True)[1]
    print(enc_inputs[i],'->',[idx2word[n.item()]for n in predict.squeeze()])

一些疑問

  • 預測階段的各個維度和模型原來的維度的關系
  • 以及為什么在損失函數里使用了softmax可以抵消模型里的呢,那再最后預測的是否不用加上么
  • 總之,只是作為參考,一般直接調用已經寫好的框架即可,這里僅供學習使用。


免責聲明!

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



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