天天看點

【Seq2Seq】使用神經網絡進行序列到序列學習

🔎大家好,我是Sonhhxg_柒,希望你看完之後,能對你有所幫助,不足請指正!共同學習交流🔎

📝個人首頁-Sonhhxg_柒的部落格_CSDN部落格 📃

🎁歡迎各位→點贊👍 + 收藏⭐️ + 留言📝​

📣系列專欄 - 機器學習【ML】 自然語言處理【NLP】  深度學習【DL】

【Seq2Seq】使用神經網絡進行序列到序列學習

 🖍foreword

✔說明⇢本人講解主要包括Python、機器學習(ML)、深度學習(DL)、自然語言處理(NLP)等内容。

如果你對這個系列感興趣的話,可以關注訂閱喲👋

文章目錄

介紹

準備資料

建構seq2seq模型

編碼器

解碼器                 

​編輯        

 seq2seq

Training the Seq2Seq Model

在本系列中,我們将使用PyTorch和torch文本建構一個機器學習模型,從一個序列到另一個序列。這将在德語到英語的翻譯中完成,但這些模型可以應用于涉及從一個序列到另一個序列的任何問題,例如摘要,即從一個序列到同一語言的較短序列。

介紹

最常見的序列到序列 (seq2seq) 模型是編碼器-解碼器模型,它們通常使用遞歸神經網絡 (RNN) 将源(輸入)句子編碼為單個向量。在本筆記本中,我們将此單個向量稱為上下文向量。我們可以将上下文向量視為整個輸入句子的抽象表示。然後,該向量由第二個RNN解碼,該RNN通過一次生成一個單詞來學習輸出目标(輸出)句子。

【Seq2Seq】使用神經網絡進行序列到序列學習

 上圖顯示了一個示例翻譯。輸入/源句子“guten morgen”通過嵌入層(黃色),然後輸入到編碼器(綠色)。我們還将序列的開頭 (<sos>) 和序列結尾 (<eos>) 标記分别附加到句子的開頭和結尾。在每個時間步長處,編碼器 RNN 的輸入既是嵌入,e,目前單詞,e(Xt),以及上一個時間步長的隐藏狀态,ht-1,編碼器 RNN 輸出新的隐藏狀态ht.到目前為止,我們可以将隐藏狀态視為句子的向量表示。RNN 可以表示為e(Xt)和ht-1:

                                          ht = EncoderRNN(e(xt),ht-1)  

我們在這裡通常使用術語RNN,它可以是任何循環架構,例如LSTM(長短期存儲器)或GRU(門控循環單元)。這裡我們有X = {x1,x2,x3,....,xT},在x1 = <sos>,x2=guten,etc,隐含層狀态為h0,

通常初始化為零或學習的參數。一旦最後一句話,xT,已認證嵌入層傳遞到 RNN 中,我們使用最終的隐藏狀态hT,作為上下文向量,即hT = z這是整個源句子的向量表示。

現在我們有了上下文向量,我們可以開始解碼它以獲得輸出/目标句子“good morning”。同樣,我們将序列标記的開始和結束附加到目标句子。在每個時間步長處,解碼器RNN(藍色)的輸入是嵌入,d,目前字數d(yt)以及上一個時間步長的隐藏狀态,st-1,其中初始解碼器隐藏狀态,s0,是上下文向量,s0=z=hT,即解碼器的初始隐藏狀态是最終編碼器的隐藏狀态。是以,與編碼器類似,我們可以将解碼器表示為:

                                        st = DecoderRNN(d(yt),st-1)

盡管輸入/源嵌入層 e和輸出/目标嵌入層 d在圖中都以黃色顯示,但它們是兩個不同的嵌入層,具有自己的參數。

在解碼器中,我們需要從隐藏狀态轉到實際單詞,是以在每個時間步長中,我們使用st

 預測(通過将其通過線性層,以紫色顯示)我們認為序列中的下一個單詞是什麼,y^t.

                                          y^t =  f(st)                            

解碼器中的單詞總是一個接一個地生成,每個時間步長一個。我們總是使用<sos>解碼器的第一個輸入,y1,但對于後續輸入,yt>1,我們有時會使用序列中的下一個單詞,yt有時使用我們的解碼器預測的單詞,y^t-1.這被稱為teacher forcing,在這裡看到更多關于它的資訊。

在訓練/測試我們的模型時,我們總是知道目标句子中有多少單詞,是以一旦我們達到那麼多單詞,我們就會停止生成單詞。在推理過程中,通常會繼續生成單詞,直到模型輸出<eos>token或生成一定數量的單詞之後。

一旦我們有了預測的目标句子,Y^ = {y^1,y^2,...,y^T},我們将其與實際目标句子進行比較,Y = {y1,y2,...,yT},來計算我們的損失。然後,我們使用此損失來更新模型中的所有參數。

準備資料

我們将在PyTorch中對模型進行編碼,并使用torch文本來幫助我們完成所需的所有預處理。我們還将使用 spaCy 來協助對資料進行标記化。

import torch
import torch.nn as nn
import torch.optim as optim

from torchtext.legacy.datasets import Multi30k
from torchtext.legacy.data import Field, BucketIterator

import spacy
import numpy as np

import random
import math
import time
           

我們将為确定性結果設定随機種子。

SEED = 1234

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
           

接下來,我們将建立分詞器。分詞器用于将包含句子的字元串轉換為構成該字元串的各個标記的清單,例如,“good morning!”變為["good", "morning", "!"]。從現在開始,我們将開始讨論句子是一系列标記,而不是說它們是一系列單詞。有什麼差別?好吧,“好”和“早晨”既是單詞又是token,但是“!”是一個token,而不是一個單詞。

spaCy具有每種語言的模型(德語的“de_core_news_sm”和英語的“en_core_web_sm”),需要加載,以便我們可以通路每個模型的分詞器。

注意:必須首先在指令行上使用以下指令下載下傳模型:

python -m spacy download en_core_web_sm
python -m spacy download de_core_news_sm
           

        我們按如下方式加載模型:

spacy_de = spacy.load('de_core_news_sm')
spacy_en = spacy.load('en_core_web_sm')
           

接下來,我們建立分詞器函數。這些可以傳遞給火炬文本,并将句子作為字元串接收,并将句子作為标記清單傳回。

在我們正在實作的論文中,他們發現颠倒輸入的順序是有益的,他們認為輸入的順序“在資料中引入了許多短期依賴關系,使優化問題變得更加容易”。我們通過在德語句子轉換為标記清單後反轉它來複制它。

def tokenize_de(text):
    """
    将字元串中的德國文本标記化為字元串(标記)清單并反轉它
    """
    return [tok.text for tok in spacy_de.tokenizer(text)][::-1]

def tokenize_en(text):
    """
    将字元串中的英國文本标記化為字元串(标記)清單
    """
    return [tok.text for tok in spacy_en.tokenizer(text)]
           

tensortext的字段處理應如何處理資料。這裡詳細介紹了所有可能的論點。

我們将每個标記化參數設定為正确的标記化函數,德語是 SRC(源)字段,英語是 TRG(目标)字段。該字段還通過init_token和eos_token參數追加“序列開始”和“序列結束”标記,并将所有單詞轉換為小寫。

SRC = Field(tokenize = tokenize_de, 
            init_token = '<sos>', 
            eos_token = '<eos>', 
            lower = True)

TRG = Field(tokenize = tokenize_en, 
            init_token = '<sos>', 
            eos_token = '<eos>', 
            lower = True)
           

接下來,我們下載下傳并加載訓練、驗證和測試資料。

我們将使用的資料集是 Multi30k 資料集。這是一個包含約 30,000 個并行英語、德語和法語句子的資料集,每個句子約 12 個單詞。

exts 指定要用作源和目标的語言(源優先),字段指定要用于源和目标的字段。

train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'), 
                                                    fields = (SRC, TRG))
           

我們可以仔細檢查我們是否加載了正确數量的示例:

print(f"Number of training examples: {len(train_data.examples)}")
print(f"Number of validation examples: {len(valid_data.examples)}")
print(f"Number of testing examples: {len(test_data.examples)}")
           
Number of training examples: 29000
Number of validation examples: 1014
Number of testing examples: 1000      

我們還可以列印出一個示例,確定源句子反轉:

print(vars(train_data.examples[0]))
           
{'src': ['.', 'büsche', 'vieler', 'nähe', 'der', 'in', 'freien', 'im', 'sind', 'männer', 'weiße', 'junge', 'zwei'], 'trg': ['two', 'young', ',', 'white', 'males', 'are', 'outside', 'near', 'many', 'bushes', '.']}
      

句點位于德語 (src) 句子的開頭,是以看起來句子已被正确反轉。

接下來,我們将為源語言和目智語言建構詞彙表。詞彙用于将每個唯一标記與索引(整數)相關聯。源語言和目智語言的詞彙表是不同的。

使用min_freq參數,我們隻允許在詞彙表中出現至少 2 次的标記。僅出現一次的token将轉換為<unk>(未知)token。

重要的是要注意,我們的詞彙表隻能從訓練集建構,而不是從驗證/測試集建構。這可以防止“資訊洩露”到我們的模型中,進而人為地誇大驗證/測試分數。

SRC.build_vocab(train_data, min_freq = 2)
TRG.build_vocab(train_data, min_freq = 2)
           
print(f"Unique tokens in source (de) vocabulary: {len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary: {len(TRG.vocab)}")
           
Unique tokens in source (de) vocabulary: 7853
Unique tokens in target (en) vocabulary: 5893      

準備資料的最後一步是建立疊代器。可以對這些參數進行疊代,以傳回一批資料,這些資料将具有 src 屬性(包含一批數值化源句子的 PyTorch 張量)和 trg 屬性(包含一批數值化目标句子的 PyTorch 張量)。數字化隻是一種花哨的方式,說它們已經從一系列可讀的标記轉換為一系列相應的索引,使用詞彙。

我們還需要定義一個火炬裝置。這用于告訴火炬文本是否将張量放在GPU上。我們使用torch.cuda.is_available()函數,如果我們的計算機上檢測到 GPU,該函數将傳回 True。我們将此裝置傳遞給疊代器。

當我們使用疊代器獲得一批示例時,我們需要確定所有源句子都填充到相同的長度,與目标句子相同。幸運的是,火炬文本疊代器為我們處理這個問題!

我們使用 BucketIterator 而不是标準疊代器,因為它以這樣一種方式建立批處理,進而最大限度地減少源句子和目标句子中的填充量。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
           
BATCH_SIZE = 128

train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data), 
    batch_size = BATCH_SIZE, 
    device = device)
           

建構seq2seq模型

我們将分三部分構模組化型。編碼器、解碼器和 seq2seq 模型封裝編碼器和解碼器,并提供一種與每個編碼器和解碼器接口的方法。

編碼器

首先是編碼器,一個2層的LSTM。我們正在實作的論文使用4層LSTM,但為了訓練時間,我們将其減少到2層。多層 RNN 的概念很容易從 2 層擴充到 4 層。

對于多層 RNN,X輸入句子在嵌入到 RNN 的第一層(底部)和隐藏狀态後,H = {h1,h2,...,hT}

,則此層的輸出将用作上一層中 RNN 的輸入。是以,用上标表示每個層,第一層中的隐藏狀态由下式給出:

【Seq2Seq】使用神經網絡進行序列到序列學習

第二層中的隐藏狀态由下式給出: 

【Seq2Seq】使用神經網絡進行序列到序列學習

 使用多層RNN還意味着我們還需要一個初始隐藏狀态作為每層的輸入,h0l,我們還将每層輸出一個上下文向量,zl

在不詳細介紹 LSTM 的情況下(請參閱此部落格文章以了解有關它們的更多資訊),我們需要知道的是,它們是一種 RNN,它不僅僅是在隐藏狀态中并按時間步長傳回新的隐藏狀态,還接受并傳回單元格狀态,ct,每個時間步長。

【Seq2Seq】使用神經網絡進行序列到序列學習

我們隻能想到ct作為另一種類型的隐藏狀态。似h0l,c0l

,

 将初始化為所有零的張量。此外,我們的上下文向量現在既是最終的隐藏狀态,也是最終的單元格狀态,即zl = (hTl,cTl)

.将我們的多層方程擴充到 LSTM,我們得到:

【Seq2Seq】使用神經網絡進行序列到序列學習

請注意,隻有第一層的隐藏狀态作為輸入傳遞到第二層,而不是單元格狀态。

是以,我們的編碼器如下所示:

【Seq2Seq】使用神經網絡進行序列到序列學習

 我們通過制作一個編碼器子產品在代碼中建立它,這需要我們從torch.nn.Module繼承并使用super().__init__()作為一些樣闆代碼。編碼器采用以下參數:

  • input_dim是将輸入到編碼器的單熱矢量的大小/次元。這等于輸入(源)詞彙大小。
  • emb_dim是嵌入層的次元。此層将單熱向量轉換為具有emb_dim次元的密集向量。
  • hid_dim是隐藏狀态和細胞狀态的次元。
  • n_layers是 RNN 中的層數。
  • dropout是要使用的dropout量。這是一個正則化參數,用于防止過度拟合。有關dropout的更多詳細資訊,請檢視此内容。

在這些教程中,我們不會詳細讨論嵌入層。我們需要知道的是,在将單詞(從技術上講,單詞的索引)傳遞到RNN之前有一個步驟,其中單詞被轉換為向量。要閱讀有關詞嵌入的更多資訊,請檢視以下文章:1,2,3,4。

嵌入層是使用 nn 建立的。nn.Embedding,具有 nn 的 LSTM。 

nn.Dropout

和帶有 nn 的壓差層。dropout。有關這些内容的更多資訊,請檢視 PyTorch 文檔。

需要注意的一點是,LSTM的壓差參數是在多層RNN的層之間應用多少壓差,即在層輸出的隐藏狀态l和用于層輸入的相同隐藏狀态之間。l+1

在 forward 方法中,我們傳入源句子 ,X該句子使用嵌入層轉換為密集向量,然後應用 

embedding

。然後,嵌入被傳遞到RNN中。當我們将整個序列傳遞給RNN時,它将自動為我們對整個序列進行隐藏狀态的循環計算!請注意,我們不會将初始隐藏狀态或單元格狀态傳遞給 RNN。這是因為,如文檔中所述,如果沒有隐藏/單元格狀态傳遞給RNN,它将自動建立一個初始隐藏/單元格狀态作為所有零的張量。

RNN 傳回:輸出(每個時間步的頂層隐藏狀态)、隐藏(每層的最終隐藏狀态、hT,堆疊在彼此之上)和單元格(每層的最終單元格狀态,cT,堆疊在彼此之上)。

由于我們隻需要最終的隐藏和單元格狀态(以使我們的上下文向量),是以轉發僅傳回隐藏和單元格。

每個張量的大小在代碼中作為注釋保留。在此實作n_directions将始終為 1,但請注意,雙向 RNN(在教程 3 中介紹)的n_directions為 2。

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()
        
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        
        self.embedding = nn.Embedding(input_dim, emb_dim)
        
        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout = dropout)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src):
        
        #src = [src len, batch size]
        
        embedded = self.dropout(self.embedding(src))
        
        #embedded = [src len, batch size, emb dim]
        
        outputs, (hidden, cell) = self.rnn(embedded)
        
        #outputs = [src len, batch size, hid dim * n directions]
        #hidden = [n layers * n directions, batch size, hid dim]
        #cell = [n layers * n directions, batch size, hid dim]
        
        #outputs are always from the top hidden layer
        
        return hidden, cell
           

解碼器                 

接下來,我們将建構我們的解碼器,它也将是一個2層(論文中有4層)LSTM。

【Seq2Seq】使用神經網絡進行序列到序列學習

解碼器類執行單個解碼步驟,即它每個時間步長輸出單個令牌。第一層将接收來自上一個時間步長的隐藏和單元格狀态,(slt-1,clt-1),并使用目前嵌入的令牌通過 LSTM 向其饋送,yt,以産生新的隐藏和單元狀态,(slt,clt).後續層将使用下面層中的隐藏狀态,sl-1t.,以及其層中先前的隐藏狀态和單元格狀态,(slt-1,clt-1)這提供了與編碼器中非常相似的方程式。

【Seq2Seq】使用神經網絡進行序列到序列學習

 請記住,解碼器的初始隐藏和單元格狀态是我們的上下文向量,它們是來自同一層的編碼器的最終隐藏和單元格狀态,即

.

【Seq2Seq】使用神經網絡進行序列到序列學習

 然後,我們從RNN的頂層傳遞隐藏狀态,stT,通過線性層,f 來預測目标(輸出)序列中的下一個token應該是什麼,y^t+1

.

【Seq2Seq】使用神經網絡進行序列到序列學習

 參數和初始化類似于 Encoder 類,隻是我們現在有一個output_dim它是輸出/目标的詞彙表大小。還添加了線性層,用于從頂層隐藏狀态進行預測。

 在 forward 方法中,我們接受一批輸入令牌、以前的隐藏狀态和以前的單元格狀态。由于我們一次隻解碼一個令牌,是以輸入令牌的序列長度将始終為 1。我們取消擠壓輸入标記以添加句子長度次元 1。然後,與編碼器類似,我們穿過嵌入層并應用丢棄。然後,這批嵌入的令牌被傳遞到具有先前隐藏狀态和單元格狀态的 RNN 中。這将生成輸出(來自RNN頂層的隐藏狀态),新的隐藏狀态(每層一個,彼此堆疊)和一個新的單元狀态(每層一個,堆疊在一起)。然後,我們将輸出(在擺脫句子長度次元之後)通過線性層來接收我們的預測。然後,我們傳回預測、新的隐藏狀态和新的單元格狀态。.

注意:由于我們的序列長度始終為1,是以可以使用  

nn.LSTMCell

, ,而不是 nn.LSTM,因為它旨在處理一批不一定在序列中的輸入。嗯。低鐵礦細胞隻是一個單一的細胞和nn。LSTM是圍繞潛在多個細胞的包裝器。使用 nn.在這種情況下,LSTMCell意味着我們不必unsqueeze添加一個假序列長度次元,但我們需要一個 nn。LSTM細胞每層在解碼器中并確定每一個nn。LSTMCell 從編碼器接收正确的初始隐藏狀态。所有這些都使代碼不那麼簡潔 - 是以決定堅持使用正常nn。新浪網.

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()        
        self.output_dim = output_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers       
        self.embedding = nn.Embedding(output_dim, emb_dim)        
        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout = dropout)       
        self.fc_out = nn.Linear(hid_dim, output_dim)      
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, input, hidden, cell):
        
        #input = [batch size]
        #hidden = [n layers * n directions, batch size, hid dim]
        #cell = [n layers * n directions, batch size, hid dim]        
        #n directions in the decoder will both always be 1, therefore:
        #hidden = [n layers, batch size, hid dim]
        #context = [n layers, batch size, hid dim]
        
        input = input.unsqueeze(0)        
        #input = [1, batch size]        
        embedded = self.dropout(self.embedding(input))        
        #embedded = [1, batch size, emb dim]               
        output, (hidden, cell) = self.rnn(embedded, (hidden, cell))        
        #output = [seq len, batch size, hid dim * n directions]
        #hidden = [n layers * n directions, batch size, hid dim]
        #cell = [n layers * n directions, batch size, hid dim]       
        #seq len and n directions will always be 1 in the decoder, therefore:
        #output = [1, batch size, hid dim]
        #hidden = [n layers, batch size, hid dim]
        #cell = [n layers, batch size, hid dim]        
        prediction = self.fc_out(output.squeeze(0))       
        #prediction = [batch size, output dim]        
        return prediction, hidden, cell
           

 seq2seq

對于實作的最後一部分,我們将實作 seq2seq 模型。這将處理:

  • 接收輸入/源句子
  • 使用編碼器生成上下文向量
  • 使用解碼器生成預測的輸出/目标句子

我們的完整模型将如下所示:

【Seq2Seq】使用神經網絡進行序列到序列學習

Seq2Seq 模型采用編碼器、解碼器和裝置(如果存在,用于在 GPU 上放置張量)。

對于此實作,我們必須確定編碼器和解碼器中的層數和隐藏(和單元)尺寸相等。情況并非總是如此,在序列到序列模型中,我們不一定需要相同數量的層數或相同的隐藏次元大小。但是,如果我們做了類似的事情具有不同數量的層,那麼我們需要決定如何處理它。例如,如果我們的編碼器有2層,而我們的解碼器隻有1層,這是如何處理的?我們是否對解碼器輸出的兩個上下文向量進行平均?我們是否通過線性層傳遞兩者?我們是否隻使用最高層的上下文向量?等。

我們的前向方法采用源句子,目标句子和教師強迫比率。在訓練我們的模型時使用教師強迫比率。解碼時,在每個時間步長,我們将從先前解碼的令牌中預測目标序列中的下一個令牌,y^t+1 = f(stL).當機率等于示教強制比(teacher_forcing_ratio)時,我們将在下一個時間步長中使用序列中實際的下一個真實值作為解碼器的輸入。但是,在機率為 1 - teacher_forcing_ratio的情況下,我們将使用模型預測的令牌作為模型的下一個輸入,即使它與序列中的實際下一個令牌不比對。

我們在正向方法中做的第一件事是建立一個輸出張量,它将存儲我們所有的預測,Y^

.然後,我們将輸入/源句子 src 饋送到編碼器中,并接收最終的隐藏狀态和單元格狀态。

解碼器的第一個輸入是序列 (<sos>) 标記的開始。由于我們的 trg 張量已經<sos>附加了标記(一直追溯到我們在 TRG 字段中定義init_token),我們得到

 通過切入它。我們知道目标句子應該有多長(max_len),是以我們循環了很多次。輸入解碼器的最後一個<eos>令牌是令牌之前的令牌 - <eos> 令牌永遠不會輸入到解碼器中。

在循環的每次疊代中,我們:

  • 傳遞輸入、以前的隐藏和以前的單元格狀态 (yt,st-1,ct-1) 進入解碼器
  • 接收預測、下一個隐藏狀态和下一個單元格狀态 (y^t+1,st,ct) 來自解碼器
  • 放置我們的預測,y^t+1/output 在我們的預測張量中,Y^/outputs
  • 決定我們是否要“teacher force”
  • 如果我們這樣做,下一個input是序列中的下一個真實标記,yt+1/tg[t]
  • 如果我們不這樣做,則下一個input是序列中預測的下一個令牌,y^t+1/top1,我們通過在輸出張量上做一個 argmax 得到

    一旦我們做出了所有的預測,我們就會傳回充滿預測的張量,Y^/outputs。

    注意:我們的解碼器循環從1開始,而不是0.這意味着我們的output張量的第0個元素保持全部為零。是以,我們的 trg 和outputs如下所示:

    【Seq2Seq】使用神經網絡進行序列到序列學習
     稍後,當我們計算損失時,我們切斷每個張量的第一個元素得到:
    【Seq2Seq】使用神經網絡進行序列到序列學習
    class Seq2Seq(nn.Module):
        def __init__(self, encoder, decoder, device):
            super().__init__()
            
            self.encoder = encoder
            self.decoder = decoder
            self.device = device
            
            assert encoder.hid_dim == decoder.hid_dim, \
                "Hidden dimensions of encoder and decoder must be equal!"
            assert encoder.n_layers == decoder.n_layers, \
                "Encoder and decoder must have equal number of layers!"
            
        def forward(self, src, trg, teacher_forcing_ratio = 0.5):
            
            #src = [src len, batch size]
            #trg = [trg len, batch size]
            #teacher_forcing_ratio是使用eacher forcing的機率
            #例如,如果teacher_forcing_ratio為 0.75,我們在 75% 的時間内使用地面實況輸入
            
            batch_size = trg.shape[1]
            trg_len = trg.shape[0]
            trg_vocab_size = self.decoder.output_dim
            
            #tensor 存儲解碼器輸出
            outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
            
            #編碼器的最後隐藏狀态用作解碼器的初始隐藏狀态
            hidden, cell = self.encoder(src)
            
            #first input to the decoder is the <sos> tokens
            input = trg[0,:]
            
            for t in range(1, trg_len):
                
                #insert input token embedding, previous hidden and previous cell states
                #receive output tensor (predictions) and new hidden and cell states
                output, hidden, cell = self.decoder(input, hidden, cell)
                
                #place predictions in a tensor holding predictions for each token
                outputs[t] = output
                
                #decide if we are going to use teacher forcing or not
                teacher_force = random.random() < teacher_forcing_ratio
                
                #get the highest predicted token from our predictions
                top1 = output.argmax(1) 
                
                #if teacher forcing, use actual next token as next input
                #if not, use predicted token
                input = trg[t] if teacher_force else top1
            
            return outputs
               

Training the Seq2Seq Model

現在我們已經實作了我們的模型,我們可以開始訓練它了。

首先,我們将初始化模型。如前所述,輸入和輸出次元由詞彙表的大小定義。編碼器和解碼器的嵌入角和間隔可以不同,但層數和隐藏/單元格狀态的大小必須相同。

然後,我們定義編碼器,解碼器,然後定義我們的Seq2Seq模型,并将其放置在裝置上。

INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5

enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)

model = Seq2Seq(enc, dec, device).to(device)
           

接下來是初始化模型的權重。在論文中,他們聲明他們初始化所有權重從-0.08和+0.08之間的均勻分布,即。u(-0.08,0.08)

我們在PyTorch中通過建立一個應用于模型的函數來初始化權重。使用 apply 時,将在模型中的每個子產品和子子產品上調用init_weights函數。對于每個子產品,我們周遊所有參數,并從具有nn.init.uniform_的均勻分布中對它們進行采樣。

def init_weights(m):
    for name, param in m.named_parameters():
        nn.init.uniform_(param.data, -0.08, 0.08)
        
model.apply(init_weights)
           
Seq2Seq(
  (encoder): Encoder(
    (embedding): Embedding(7853, 256)
    (rnn): LSTM(256, 512, num_layers=2, dropout=0.5)
    (dropout): Dropout(p=0.5, inplace=False)
  )
  (decoder): Decoder(
    (embedding): Embedding(5893, 256)
    (rnn): LSTM(256, 512, num_layers=2, dropout=0.5)
    (fc_out): Linear(in_features=512, out_features=5893, bias=True)
    (dropout): Dropout(p=0.5, inplace=False)
  )
)      

我們還定義了一個函數,用于計算模型中可訓練參數的數量。

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')
           
The model has 13,898,501 trainable parameters      

我們定義優化器,用于更新訓練循環中的參數。檢視這篇文章,了解有關不同優化器的資訊。在這裡,我們将使用Adam。

optimizer = optim.Adam(model.parameters())
           

接下來,我們定義損失函數。 

CrossEntropyLoss

計算對數軟最大值以及預測的負對數可能性。

我們的損失函數計算每個令牌的平均損失,但是通過将令牌的索引<pad>作為ignore_index參數傳遞,隻要目标令牌是填充令牌,我們就會忽略損失。

TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]

criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX)
           

接下來,我們将定義訓練循環。

首先,我們将模型設定為“訓練模式”,使用 model.train()。這将打開丢棄(和批處理規範化,我們沒有使用),然後循環通路我們的資料疊代器。

如前所述,我們的解碼器循環從 1 開始,而不是 0。這意味着輸出張量的第 0 個元素保持所有零。是以,我們的 trg 和輸出如下所示:

【Seq2Seq】使用神經網絡進行序列到序列學習

 在這裡,當我們計算損失時,我們切斷每個張量的第一個元素得到:

【Seq2Seq】使用神經網絡進行序列到序列學習

每次疊代時:

  • 從批進行中擷取源句子和目标句子,X以及Y
  • 從最後一批計算的梯度歸零
  • 将源和目标饋送到模型中擷取輸出,Y^

由于損失函數僅适用于具有 1d 目标的 2d 輸入,是以我們需要使用 .view 将其中的每一個扁平化 

  •  計算有損失的loss.backward()
  • 裁剪漸變以防止它們爆炸(RNN 中的常見問題)
  • 通過執行優化器步驟來更新模型的參數
  • 将損失值與運作總計相加

 最後,我們傳回所有批次的平均損失。

def train(model, iterator, optimizer, criterion, clip):   
    model.train()   
    epoch_loss = 0    
    for i, batch in enumerate(iterator):       
        src = batch.src
        trg = batch.trg        
        optimizer.zero_grad()        
        output = model(src, trg)        
        #trg = [trg len, batch size]
        #output = [trg len, batch size, output dim]        
        output_dim = output.shape[-1]        
        output = output[1:].view(-1, output_dim)
        trg = trg[1:].view(-1)        
        #trg = [(trg len - 1) * batch size]
        #output = [(trg len - 1) * batch size, output dim]        
        loss = criterion(output, trg)        
        loss.backward()        
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)        
        optimizer.step()        
        epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)
           

我們的評估循環類似于我們的訓練循環,但是由于我們不更新任何參數,是以不需要傳遞優化器或剪輯值。

我們必須記住使用 model.eval() 将模型設定為求值模式。這将關閉丢棄(以及批量規範化,如果使用)。

我們使用 with torch.no_grad() 塊來確定塊内不計算任何梯度。這減少了記憶體消耗并加快了速度。

疊代循環是相似的(沒有參數更新),但是我們必須確定關閉教師強制進行評估。這将導緻模型僅使用其自己的預測在句子中進行進一步的預測,這反映了它在部署中的使用方式。

def evaluate(model, iterator, criterion):    
    model.eval()   
    epoch_loss = 0    
    with torch.no_grad():    
        for i, batch in enumerate(iterator):
            src = batch.src
            trg = batch.trg
            output = model(src, trg, 0) #turn off teacher forcing
            #trg = [trg len, batch size]
            #output = [trg len, batch size, output dim]
            output_dim = output.shape[-1]            
            output = output[1:].view(-1, output_dim)
            trg = trg[1:].view(-1)
            #trg = [(trg len - 1) * batch size]
            #output = [(trg len - 1) * batch size, output dim]
            loss = criterion(output, trg)            
            epoch_loss += loss.item()        
    return epoch_loss / len(iterator)
           

接下來,我們将建立一個函數,用于告訴我們一個 epoch 需要多長時間。

def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs
           

我們終于可以開始訓練我們的模型了!

在每個 epoch,我們将檢查我們的模型是否實作了迄今為止的最佳驗證損失。如果有,我們将更新最佳驗證損失并儲存模型的參數(在PyTorch中稱為state_dict)。然後,當我們來測試我們的模型時,我們将使用儲存的參數來實作最佳的驗證損失。

我們将列印出每個時代的損失和困惑。看到困惑的變化比損失的變化更容易,因為數字要大得多。

N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):
    
    start_time = time.time()
    
    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)
    
    end_time = time.time()
    
    epoch_mins, epoch_secs = epoch_time(start_time, end_time)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut1-model.pt')
    
    print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')
           
Epoch: 01 | Time: 0m 26s
	Train Loss: 5.052 | Train PPL: 156.386
	 Val. Loss: 4.916 |  Val. PPL: 136.446
Epoch: 02 | Time: 0m 26s
	Train Loss: 4.483 | Train PPL:  88.521
	 Val. Loss: 4.789 |  Val. PPL: 120.154
Epoch: 03 | Time: 0m 25s
	Train Loss: 4.195 | Train PPL:  66.363
	 Val. Loss: 4.552 |  Val. PPL:  94.854
Epoch: 04 | Time: 0m 25s
	Train Loss: 3.963 | Train PPL:  52.625
	 Val. Loss: 4.485 |  Val. PPL:  88.672
Epoch: 05 | Time: 0m 25s
	Train Loss: 3.783 | Train PPL:  43.955
	 Val. Loss: 4.375 |  Val. PPL:  79.466
Epoch: 06 | Time: 0m 25s
	Train Loss: 3.636 | Train PPL:  37.957
	 Val. Loss: 4.234 |  Val. PPL:  69.011
Epoch: 07 | Time: 0m 26s
	Train Loss: 3.506 | Train PPL:  33.329
	 Val. Loss: 4.077 |  Val. PPL:  58.948
Epoch: 08 | Time: 0m 27s
	Train Loss: 3.370 | Train PPL:  29.090
	 Val. Loss: 4.018 |  Val. PPL:  55.581
Epoch: 09 | Time: 0m 26s
	Train Loss: 3.241 | Train PPL:  25.569
	 Val. Loss: 3.934 |  Val. PPL:  51.113
Epoch: 10 | Time: 0m 26s
	Train Loss: 3.157 | Train PPL:  23.492
	 Val. Loss: 3.927 |  Val. PPL:  50.743      

我們将加載為模型提供最佳驗證損失的參數(state_dict),并在測試集上運作模型。

model.load_state_dict(torch.load('tut1-model.pt'))

test_loss = evaluate(model, test_iterator, criterion)

print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')
           
| Test Loss: 3.951 | Test PPL:  52.001 |
      

繼續閱讀