深度學習在自然語言處理領域的應用越來越廣泛,序列模型如迴圈神經網路(RNN)和 Transformer 成為了處理文字資料的核心技術。本文將探討如何使用 PyTorch 構建這些模型,並分析它們的架構和應用。RNN 是一種能夠捕捉序列資料中時序資訊的模型,其變體如 LSTM 和 GRU 則透過門控機制解決了傳統 RNN 的梯度消失問題。雙向 RNN 能夠同時從兩個方向處理序列資訊,進一步提升了模型的效能。Transformer 模型則透過自注意力機制捕捉序列中不同元素之間的關係,在許多 NLP 任務中取得了 state-of-the-art 的結果。PyTorch 提供了便捷的工具和 API,可以輕鬆地實作這些模型,並進行訓練和調優。

迴圈神經網路與其他序列模型

在建立學習器(Learner)時,我們使用內建函式從 DataLoaders 和指定的模型類別(本例中為 TextClassifier)建立一個學習器:

learn = Learner(dls, TextClassifier(len(dls.vocab[0]), 100),
                loss_func=CrossEntropyLossFlat(),
                metrics=accuracy)

這樣就完成了大部分的工作!fastai 抽象化了我們目前不需要關心的其他細節(例如損失函式和最佳化器)。剩下的就是訓練步驟:

learn.fit(10)

雖然模型的表現不是非常出色,但它確實能夠運作。讓我們來看看如何改進它。在本章中,我們將專注於嚴格的架構改進,這意味著我們只會編輯/更改 RNNCellRNNTextClassifier 類別。

為什麼要使用三個 nn.Module 子類別?

你可能會想知道為什麼我們選擇使用三個 nn.Module 子類別,而不是把所有東西放在一起以簡化程式碼。首先,將功能分成多個模組是一種良好的工程實踐。更重要的是,由於它們都是獨立的模組,你可以更有效地自定義功能。如果你想要建立一個多層 RNN,只需編輯 RNN 類別。如果你想要用更先進的單元替換 RNNCell,只需用其他東西替換它。想要在最後新增更多的全連線層?或者修改網路以進行多類別分類別?只需自定義 TextClassifier 中的全連線層即可。

使用 PyTorch 的 RNN 模組

事實上,這就是 PyTorch 中實作 RNN 的方式。對於 PyTorch 原生支援的每種迴圈網路型別(RNN、LSTM 和 GRU),都有一個對應的單元和模組類別。當你實作自己的文字分類別器、語言模型等時,你會想要直接使用預設的 PyTorch 模組,而不是自己建立它們。讓我們來看看 PyTorch 版本的 RNN:

import torch
??torch.nn.RNN

它與我們建立的 RNN 模組非常相似,但增加了一些有用的功能,如丟棄(dropout)、多層和選擇啟用函式的能力。你可以嘗試自己實作這些功能以增加樂趣,但對於真正的生產系統來說,這樣做不值得。因此,從現在開始,我們將使用 torch.nn.RNN 而不是自定義的 RNN。它們非常相似,因此 PyTorch 版本可以直接替換 TextClassifier 中的自定義 RNN:

class TextClassifier(nn.Module):
    def __init__(self, vocab_size, hidden_size):
        super(TextClassifier, self).__init__()
        self.emb = nn.Embedding(vocab_size, hidden_size)
        self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True)
        self.fc1 = nn.Linear(hidden_size, 10)
        self.fc2 = nn.Linear(10, 2)

    def forward(self, x):
        x = self.emb(x)
        _, x = self.rnn(x)
        x = self.fc1(x)
        out = self.fc2(x)
        return out

learn = Learner(dls, TextClassifier(len(dls.vocab[0]), 100),
                loss_func=CrossEntropyLossFlat(),
                metrics=accuracy)
learn.fit(10)

訓練結果

epochtrain_lossvalid_lossaccuracytime
00.6950690.6928720.51384000:04
10.6928190.6857380.54736000:04

55% 的準確率並不是非常好,但這很好地說明瞭為什麼我們需要更好的架構。幸運的是,即使在迴圈序列模型的框架內,也有更好的替代方案。

雙向 RNN

雙向 RNN 的想法與 BERT 中使用的雙向轉換器相似,但在本例中,方向性更明顯。雙向 RNN 不僅僅是從左到右遍歷句子,而是同時計算隱藏狀態的兩個“路徑”。事實上,你最終會得到一個從左到右的隱藏狀態序列和一個從右到左的隱藏狀態序列。

為什麼使用雙向 RNN?

雙向 RNN 的簡要總結是,它增加了一些計算量,但通常在不需要太多調整的情況下提高效能。只要計算上可行,你幾乎應該總是使用雙向 RNN。

有一個重要的考慮因素——在實踐中,你可能想要使用 RNN 的許多問題(與轉換器相比)並沒有真正的雙向結構。例如,在文字生成中,你的模型無法“看到”序列的結尾,因為序列的結尾正是我們正在生成的!雙向 RNN 在較簡單的 NLP 任務(如檔案分類別或摘要)中可能最為有用,因為這些任務具有較大的輸入,而轉換器可能更適合但計算上不可行。

對於 IMDb 文字分類別問題,我們可以一次性存取整個序列,因此可以透過簡單地設定 bidirectional=True 來使用雙向 RNN。這將給我們兩個隱藏狀態,而不是一個,因此我們將它們串聯起來並傳遞給全連線層:

class TextClassifier(nn.Module):
    def __init__(self, vocab_size, hidden_size):
        super(TextClassifier, self).__init__()
        self.emb = nn.Embedding(vocab_size, hidden_size)
        self.rnn = nn.RNN(hidden_size, hidden_size,
                          bidirectional=True, batch_first=True)
        self.fc1 = nn.Linear(hidden_size * 2, 10)
        self.fc2 = nn.Linear(10, 2)

    def forward(self, x):
        x = self.emb(x)
        _, x = self.rnn(x)
        x = torch.cat((x[0], x[1]), dim=-1)
        x = self.fc1(x)
        out = self.fc2(x)
        return out

程式碼解密:

  1. 雙向RNN設定:透過設定 bidirectional=True,RNN 能夠同時處理從左到右和從右到左的序列訊息。
  2. 隱藏狀態拼接:由於雙向RNN會產生兩個方向的隱藏狀態,因此需要將這兩個狀態拼接起來,以提供更豐富的特徵給後續的全連線層處理。
  3. 線性層調整:由於輸入特徵維度變為原來的兩倍(兩個方向的隱藏狀態),因此第一個全連線層的輸入維度需要相應調整為 hidden_size * 2
  4. 前向傳播邏輯:程式碼保持了清晰的前向傳播邏輯,先進行嵌入、RNN 處理、隱藏狀態拼接,然後透過兩個全連線層輸出最終結果。

迴圈神經網路(RNN)與其他序列模型

在前面的章節中,我們探討了Transformer模型及其在自然語言處理(NLP)任務中的應用。然而,Transformer並非唯一的序列模型選擇。迴圈神經網路(RNN)是另一種重要的架構,尤其是在處理序列資料時。

雙向RNN

RNN的一個重要變體是雙向RNN(Bidirectional RNN)。在標準的RNN中,資訊按照序列的順序流動,例如從左到右。但在雙向RNN中,資訊同時從兩個方向流動:從左到右和從右到左。這種架構使得模型能夠捕捉序列中更豐富的上下文資訊。

learn = Learner(dls, TextClassifier(len(dls.vocab[0]), 100),
                loss_func=CrossEntropyLossFlat(),
                metrics=accuracy)
learn.fit(10)

內容解密:

  1. Learner是fastai函式庫中的一個類別,用於建立一個學習器物件。
  2. dls是資料載入器,包含了訓練和驗證資料集。
  3. TextClassifier是一個自定義的模型類別,用於文字分類別任務。
  4. loss_func=CrossEntropyLossFlat()指定了損失函式為交叉熵損失,用於多分類別任務。
  5. metrics=accuracy表示在訓練過程中會跟蹤準確率這一指標。

序列到序列模型

RNN不僅可以用於文字分類別,還可以用於序列到序列(Sequence-to-Sequence)的任務,如機器翻譯。在這種任務中,模型首先透過編碼器(Encoder)將輸入序列編碼為一個隱藏狀態,然後透過解碼器(Decoder)將這個隱藏狀態轉換為目標序列。

長短期記憶網路(LSTM)

LSTM是一種特殊的RNN架構,透過引入門控機制來解決傳統RNN在處理長序列時的梯度消失問題。LSTM使用兩個向量來表示隱藏狀態,並透過門控機制來控制資訊的流動。

class TextClassifier(nn.Module):
    def __init__(self, vocab_size, hidden_size):
        super(TextClassifier, self).__init__()
        self.emb = nn.Embedding(vocab_size, hidden_size)
        self.rnn = nn.LSTM(hidden_size, hidden_size, batch_first=True)
        self.fc1 = nn.Linear(hidden_size, 10)
        self.fc2 = nn.Linear(10, 2)

    def forward(self, x):
        x = self.emb(x)
        x, _ = self.rnn(x)
        x = self.fc1(x[:, -1, :]) # 只取最後一個時間步的輸出
        out = self.fc2(x)
        return out

內容解密:

  1. nn.LSTM是PyTorch中實作LSTM層的模組。
  2. batch_first=True表示輸入資料的第一維是批次大小。
  3. forward方法中,x, _ = self.rnn(x)表示只取LSTM輸出的最後一個隱藏狀態。
  4. x = self.fc1(x[:, -1, :])表示只使用LSTM輸出序列的最後一個元素進行後續的全連線層處理。

閘控迴圈單元(GRU)

GRU是另一種改進的RNN架構,旨在簡化LSTM的結構同時保留其處理長序列依賴的能力。GRU使用一個門控機制來控制隱藏狀態的更新。

class TextClassifier(nn.Module):
    def __init__(self, vocab_size, hidden_size):
        super(TextClassifier, self).__init__()
        self.emb = nn.Embedding(vocab_size, hidden_size)
        self.rnn = nn.GRU(hidden_size, hidden_size, batch_first=True)
        self.fc1 = nn.Linear(hidden_size, 10)
        self.fc2 = nn.Linear(10, 2)

    def forward(self, x):
        x = self.emb(x)
        _, x = self.rnn(x)
        x = self.fc1(x)
        out = self.fc2(x)
        return out

內容解密:

  1. nn.GRU是PyTorch中實作GRU層的模組。
  2. GRU層直接傳回最後一個時間步的隱藏狀態,因此在forward方法中可以直接使用_來忽略其他輸出。

總之,RNN及其變體(如LSTM和GRU)在處理序列資料時具有獨特的優勢。透過適當的架構選擇和訓練,可以在諸如文字分類別、機器翻譯等NLP任務中取得良好的效能。

Transformer架構深度解析

在前一章中,我們探討了迴歸神經網路(RNN),這是自然語言處理(NLP)領域中曾經的主流架構,直到Transformer架構的出現。Transformer已成為現代NLP的核心工作馬,自2017年首次提出以來,便在深度學習領域掀起了一波熱潮。隨後,NLP領域湧現了大量新的架構,這些架構要麼以《芝麻街》角色命名,要麼以「-former」結尾。

從零開始建構Transformer

在第2章和第3章中,我們探討瞭如何實際使用Transformer,以及如何利用預訓練的Transformer解決複雜的NLP問題。現在,我們將探討Transformer的架構本身,瞭解其工作原理。

首先,我們需要了解「第一原理」(first principles)指的是什麼。簡而言之,這意味著我們不依賴Hugging Face Transformers函式庫,而是使用原始的PyTorch來實作。這樣做有助於我們瞭解Transformer的底層運作原理。

為何選擇PyTorch?

PyTorch是一個功能完整的深度學習函式庫,大多數研究人員都使用它。它自然包含了Transformer架構的實作,就像Hugging Face函式庫一樣。不過,PyTorch的版本更具DIY精神,設計用於與其他熟悉的PyTorch工具(如dataloaders、optimizers等)一起使用。

Transformer架構分析

讓我們來看看Transformer架構的核心創新,並探索一種新型的神經網路層:注意力機制(attention mechanism)。

import torch
model = torch.nn.Transformer()
model.encoder.layers[0]

輸出結果顯示了TransformerEncoderLayer的結構:

TransformerEncoderLayer(
  (self_attn): MultiheadAttention(
    (out_proj): Linear(in_features=512, out_features=512, bias=True)
  )
  (linear1): Linear(in_features=512, out_features=2048, bias=True)
  (dropout): Dropout(p=0.1, inplace=False)
  (linear2): Linear(in_features=2048, out_features=512, bias=True)
  (norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
  (norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
  (dropout1): Dropout(p=0.1, inplace=False)
  (dropout2): Dropout(p=0.1, inplace=False)
)

內容解密:

  1. TransformerEncoderLayer是Transformer編碼器中的一個基本層。
  2. self_attn代表自注意力機制,用於計算輸入序列中不同元素之間的相關性。
    • MultiheadAttention允許多個注意力頭平行運作,提高模型的表達能力。
    • out_proj是一個線性層,將多頭注意力的輸出投影到指定的維度。
  3. linear1linear2是兩個線性層,用於轉換輸入資料的維度。
  4. dropout層用於防止過擬合,隨機丟棄部分神經元。
  5. norm1norm2是層標準化(LayerNorm),用於穩定和加速訓練過程。

隨著深度學習技術的不斷進步,新的架構和技術不斷湧現。對於NLP領域的研究人員和從業者來說,瞭解這些新技術的工作原理和應用場景至關重要。同時,我們也需要關注現有架構的侷限性,例如Transformer在處理極長序列時的記憶體複雜度問題。未來,RNN和Transformer等架構可能會繼續演進,或者出現新的、更高效的模型。保持對新技術的關注和理解,將有助於我們在NLP領域取得更好的成果。