Datawhale AI夏令營-機器翻譯挑戰賽

CASTWJ發表於2024-07-17

Baseline程式碼相關解讀

是否需要使用self判定的標準是是否在類中

在 Python 中,self 是一個類的例項方法中的引數,用於指代呼叫該方法的例項本身。在程式碼示例中,self 並不是必需的,因為這個示例程式碼不是類的一部分,只是一個獨立的函式呼叫。讓我們逐步解釋這些區別。

類中的方法與獨立的函式

  1. 獨立的函式呼叫

    from torchtext.data.utils import get_tokenizer
    
    # 獲取 'basic_english' 分詞器
    tokenizer = get_tokenizer('basic_english')
    
    # 示例文字
    text = "Hello, world! This is an example sentence."
    
    # 使用分詞器分割文字
    tokens = tokenizer(text)
    print(tokens)
    

    在這個例子中,tokenizer 是透過呼叫 get_tokenizer('basic_english') 函式得到的。這個程式碼片段沒有涉及到類和例項,所以沒有 self 的概念。它只是簡單地呼叫函式並使用返回的分詞器進行文字分割。

  2. 類中的方法

    如果我們在一個類中實現同樣的功能,就需要使用 self 來指代例項的屬性或方法。例如:

    from torchtext.data.utils import get_tokenizer
    
    class TextProcessor:
        def __init__(self):
            self.tokenizer = get_tokenizer('basic_english')  # 初始化例項的分詞器屬性
    
        def tokenize(self, text):
            return self.tokenizer(text)  # 使用例項的分詞器屬性進行分詞
    
    # 建立 TextProcessor 的例項
    processor = TextProcessor()
    
    # 示例文字
    text = "Hello, world! This is an example sentence."
    
    # 使用例項的方法分割文字
    tokens = processor.tokenize(text)
    print(tokens)
    

    在這個例子中:

    • __init__ 方法:類的構造方法,用於初始化例項。這裡用 self.tokenizer 來儲存分詞器。
    • self.tokenizer:指代例項的分詞器屬性。
    • tokenize 方法:定義了一個例項方法,用於呼叫例項的分詞器屬性進行分詞。

為什麼這裡沒有 self

在獨立的函式呼叫中,如下面的例子,不涉及到類和例項,所以不需要使用 self

from torchtext.data.utils import get_tokenizer

# 獲取 'basic_english' 分詞器
tokenizer = get_tokenizer('basic_english')

# 示例文字
text = "Hello, world! This is an example sentence."

# 使用分詞器分割文字
tokens = tokenizer(text)
print(tokens)

這個程式碼片段直接使用函式呼叫來獲得分詞器,並使用分詞器對文字進行分割,沒有涉及到類和例項,所以沒有 self 的概念。

類與例項中的 self

當程式碼被封裝到一個類中時,self 用於引用類的例項,並訪問例項的屬性和方法。例如:

class TextProcessor:
    def __init__(self):
        self.tokenizer = get_tokenizer('basic_english')

    def tokenize(self, text):
        return self.tokenizer(text)

在這裡,self.tokenizer 是一個例項屬性,它儲存了分詞器。tokenize 方法透過 self.tokenizer 來呼叫這個例項屬性。

總結

  • 獨立的函式呼叫:直接呼叫函式,返回分詞器並使用它,不涉及 self
  • 類中的方法:透過類和例項來封裝功能,需要使用 self 來引用例項的屬性和方法。

希望這些解釋能幫助你理解為什麼在獨立的函式呼叫中不需要使用 self,而在類的方法中需要使用 self

列表推導式

[word for word, _ in zh_vocab.most_common(10000)] 這種語法叫做列表推導式(List Comprehension),是 Python 中建立列表的一種簡潔方式。

詳細解釋

列表推導式的一般形式是:

[expression for item in iterable if condition]

在你的例子中:

[word for word, _ in zh_vocab.most_common(10000)]

具體解釋如下:

  1. zh_vocab.most_common(10000):

    • 這個方法返回一個包含最多 10000 個 (word, count) 元組的列表,其中 word 是詞彙,count 是該詞彙在資料集中出現的次數。這些元組按 count 的降序排列。
  2. for word, _ in zh_vocab.most_common(10000):

    • 這是一個 for 迴圈,它遍歷 zh_vocab.most_common(10000) 返回的所有 (word, count) 元組。
    • 在這個迴圈中,每個元組會被解包為兩個變數:word_word 是詞彙,_ 是計數(在 Python 中,_ 通常表示一個不需要使用的變數)。
  3. [word for word, _ in zh_vocab.most_common(10000)]:

    • 這個列表推導式的作用是建立一個新列表,其中包含 zh_vocab.most_common(10000) 返回的每個元組中的 word 部分,忽略 count 部分。

作用總結

[word for word, _ in zh_vocab.most_common(10000)] 建立了一個包含 zh_vocab 中最常見的前 10000 個詞彙的列表,忽略每個詞彙的計數。

具體例子

假設 zh_vocab.most_common(5) 返回的結果如下:

[('你', 1000), ('好', 800), ('我', 600), ('是', 400), ('的', 200)]

那麼列表推導式:

[word for word, _ in zh_vocab.most_common(5)]

將建立以下列表:

['你', '好', '我', '是', '的']

這種方式非常簡潔且易於理解,特別是在處理資料時,可以快速地從一個複雜的資料結構中提取出所需的部分。

字典的get方法:

self.en_word2idx.get(word, self.en_word2idx['<sos>'])
這個部分將每個單詞轉換為對應的索引。讓我們詳細拆解這個方法:

  • self.en_word2idx 是一個字典,字典的鍵是單詞,值是該單詞對應的索引。
  • .get(word, self.en_word2idx['<sos>']) 是字典的 get 方法,它嘗試獲取鍵 word 對應的值(索引)。如果 word 不在字典中,它將返回第二個引數 self.en_word2idx['<sos>'],即 <sos>(開始標記)的索引。

這樣,程式碼確保了即使某個單詞不在詞彙表中,也會有一個預設的索引(<sos>)作為替代。

nn.Embedding

nn.Embedding 預設情況下並不是預訓練的。它在初始化時會隨機生成一個嵌入矩陣,然後在訓練過程中學習每個單詞的嵌入向量。讓我們詳細解釋它的工作原理和訓練過程。

nn.Embedding 的工作原理

初始化嵌入矩陣

當你建立一個 nn.Embedding 物件時,會初始化一個嵌入矩陣。這個矩陣的每一行表示一個單詞的嵌入向量,矩陣的行數是詞彙表大小,列數是嵌入向量的維度。

import torch
import torch.nn as nn

# 假設 input_dim = 10, emb_dim = 4
embedding = nn.Embedding(10, 4)

嵌入矩陣在初始化時通常使用均勻分佈或正態分佈進行隨機填充。例如,初始化後的嵌入矩陣可能如下所示:

[
 [ 0.1, -0.2,  0.3,  0.4],
 [-0.1,  0.2, -0.3, -0.4],
 [ 0.5,  0.6,  0.7,  0.8],
 ...
]

索引查詢

給定一個包含單詞索引的序列,nn.Embedding 會查詢嵌入矩陣中對應的行,並返回這些行作為嵌入向量序列。例如:

# 輸入序列
input_seq = torch.tensor([2, 5, 7])

# 獲取嵌入向量
embedded_seq = embedding(input_seq)
print(embedded_seq)

如果 input_seq[2, 5, 7]nn.Embedding 會返回嵌入矩陣的第 2、5、7 行。

如何轉換整數序列

當呼叫 embedding(input_seq) 時,PyTorch 會在內部執行以下操作:

  1. 輸入input_seq 是一個包含單詞索引的整數張量,形狀為 [sequence_length],例如 [2, 5, 7]
  2. 索引查詢:PyTorch 在嵌入矩陣中查詢這些索引對應的行。
  3. 返回嵌入向量:查詢到的行組成一個新的張量,形狀為 [sequence_length, emb_dim]

訓練過程

嵌入矩陣的值在訓練過程中透過反向傳播演算法進行更新。具體步驟如下:

  1. 前向傳播

    • 輸入序列透過嵌入層轉換為嵌入向量序列。
    • 嵌入向量序列作為神經網路的輸入進行前向傳播,計算出模型的預測輸出。
  2. 計算損失

    • 使用模型的預測輸出和真實標籤計算損失。
  3. 反向傳播

    • 透過反向傳播演算法計算損失相對於模型引數的梯度。
    • 梯度會傳遞迴嵌入層,更新嵌入矩陣的權重。
  4. 更新權重

    • 使用最佳化器更新模型引數,包括嵌入矩陣。

總結

  • nn.Embedding 不是預訓練的:它在初始化時隨機生成嵌入矩陣。
  • 索引查詢:給定一個包含單詞索引的序列,nn.Embedding 會查詢嵌入矩陣中對應的行,並返回這些行作為嵌入向量序列。
  • 訓練過程:嵌入矩陣的權重在訓練過程中透過反向傳播進行更新,逐步學習每個單詞的嵌入向量。

這種方式允許模型在訓練過程中根據任務的具體需求學習到最適合的詞嵌入,從而提高模型的效能。

nn.GRU

GRU(Gated Recurrent Unit)是一種改進的迴圈神經網路(RNN),旨在解決傳統 RNN 的梯度消失和梯度爆炸問題。GRU 透過引入門控機制來捕捉序列中的長期依賴關係。

GRU 層的基本概念

GRU 是一種特殊的 RNN,它透過更新門(update gate)和重置門(reset gate)來控制資訊的流動。與 LSTM 不同的是,GRU 只有兩個門,比 LSTM 更簡單但效果相近。

GRU 結構

  1. 重置門(Reset Gate):控制前一時刻的隱藏狀態有多少資訊需要遺忘。
  2. 更新門(Update Gate):控制當前時刻的隱藏狀態有多少資訊需要保留。

具體程式碼解釋

self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)

引數解釋

  1. emb_dim(輸入特徵的維度)

    • 這是 GRU 層的輸入維度,即輸入序列的每個時間步的特徵向量的維度。在這個上下文中,emb_dim 是嵌入層的輸出維度。
  2. hid_dim(隱藏層的維度)

    • 這是 GRU 層的隱藏狀態的維度。每個時間步的隱藏狀態會有 hid_dim 個特徵。
  3. n_layers(GRU 層的數量)

    • 這是堆疊的 GRU 層的數量。多層 GRU 允許模型捕捉更復雜的序列模式。
  4. dropout(Dropout 比率)

    • 在各層之間應用 Dropout,用於防止過擬合。
  5. batch_first=True(批次維度放在第一位)

    • 指定輸入和輸出張量的形狀,批次大小放在第一位。這意味著輸入和輸出張量的形狀為 [batch_size, seq_len, feature_dim]

GRU 的前向傳播過程

當你將輸入序列傳遞給 GRU 層時,它會進行以下步驟:

  1. 輸入處理

    • 輸入序列的形狀為 [batch_size, seq_len, emb_dim]
  2. 計算每個時間步的隱藏狀態

    • GRU 使用當前時間步的輸入和前一時間步的隱藏狀態來計算當前時間步的隱藏狀態。
  3. 輸出和隱藏狀態

    • 輸出:每個時間步的輸出序列,形狀為 [batch_size, seq_len, hid_dim]
    • 隱藏狀態:最後一個時間步的隱藏狀態,形狀為 [n_layers, batch_size, hid_dim]

具體實現步驟

1. 初始化 GRU 層

self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)

2. 前向傳播方法

def forward(self, src):
    # src shape: [batch_size, src_len]
    embedded = self.dropout(self.embedding(src))
    # embedded shape: [batch_size, src_len, emb_dim]
    outputs, hidden = self.rnn(embedded)
    # outputs shape: [batch_size, src_len, hid_dim]
    # hidden shape: [n_layers, batch_size, hid_dim]
    return outputs, hidden
  • 輸入序列 src

    • src 的形狀為 [batch_size, src_len]
    • 經過嵌入層和 Dropout 後,得到 embedded,形狀為 [batch_size, src_len, emb_dim]
  • 傳遞給 GRU 層

    • embedded 作為輸入傳遞給 GRU 層,得到 outputshidden
    • outputs 包含每個時間步的輸出,形狀為 [batch_size, src_len, hid_dim]
    • hidden 是最後一個時間步的隱藏狀態,形狀為 [n_layers, batch_size, hid_dim]

總結

  • GRU 層的作用:GRU 層透過更新門和重置門控制資訊流動,有效捕捉序列中的長期依賴關係,解決傳統 RNN 的梯度消失問題。
  • 引數
    • emb_dim:輸入特徵的維度,即嵌入向量的維度。
    • hid_dim:隱藏狀態的維度。
    • n_layers:GRU 層的數量。
    • dropout:Dropout 比率,用於防止過擬合。
    • batch_first=True:指定輸入和輸出的張量形狀。

透過這種方式,GRU 層能夠處理序列資料,捕捉其中的時序資訊,為後續的解碼或其他處理提供有用的特徵表示。

Baseline流程

環境配置

  • torchtext :是一個用於自然語言處理(NLP)任務的庫,它提供了豐富的功能,包括資料預處理、詞彙構建、序列化和批處理等,特別適合於文字分類、情感分析、機器翻譯等任務

  • jieba:是一箇中文分詞庫,用於將中文文字切分成有意義的詞語

  • sacrebleu:用於評估機器翻譯質量的工具,主要透過計算BLEU(Bilingual Evaluation Understudy)得分來衡量生成文字與參考譯文之間的相似度.

  • spacy:是一個強大的自然語言處理庫,支援70+語言的分詞與訓練
    在此我們使用其用於英文的 tokenizer(分詞,就是將句子、段落、文章這種長文字,分解為以字詞為單位的資料結構,方便後續的處理分析工作)

!pip install torchtext    
!pip install jieba
!pip install sacrebleu
!python -m spacy download en_core_web_trf

資料預處理

預處理階段通常包括多個步驟,旨在清理、標準化和轉換資料,使之適合模型訓練

清洗和規範化資料

  • 去除無關資訊:刪除HTML標籤、特殊字元、非文字內容等,確保文字的純淨性(本賽題的訓練集中出現了非常多的髒資料,如“Joey. (掌聲) (掌聲) 喬伊”、“Thank you. (馬嘶聲) 謝謝你們”等這種聲音詞)
  • 統一格式:轉換所有文字為小寫,確保一致性;標準化日期、數字等格式。
  • 分句和分段:將長文字分割成句子或段落,便於處理和訓練。

分詞

將句子分解成單詞或詞素(構成單詞的基本組成部分,一個詞素可以是一個完整的單詞,也可以是單詞的一部分,但每一個詞素都至少攜帶一部分語義或語法資訊),這是NLP中最基本的步驟之一。我們這裡使用了使用jieba 對中文進行分詞,使用spaCy對英文進行分詞。

構建詞彙表和詞向量

  • 詞彙表構建:從訓練資料中收集所有出現過的詞彙,構建詞彙表,併為每個詞分配一個唯一的索引。
  • 詞向量:使用預訓練的詞向量或自己訓練詞向量,將詞彙表中的詞對映到高維空間中的向量,以捕捉語義資訊(當前大模型領域訓練的 embedding 模型就是用來完成此任務的)。

序列截斷和填充

  • 序列截斷:限制輸入序列的長度,過長的序列可能增加計算成本,同時也可能包含冗餘資訊。
  • 序列填充:將所有序列填充至相同的長度,便於批次處理。通常使用標記填充。

新增特殊標記

  • 序列開始和結束標記:在序列兩端新增(Sequence Start)和(Sequence End)標記,幫助模型識別序列的起始和結束。
  • 未知詞標記:為不在詞彙表中的詞新增(Unknown)標記,使模型能夠處理未見過的詞彙。

資料增強

  • 隨機替換或刪除詞:在訓練資料中隨機替換或刪除一些詞,增強模型的魯棒性。
  • 同義詞替換:使用同義詞替換原文中的詞,增加訓練資料的多樣性。

資料分割

將資料劃分為訓練集、驗證集和測試集,分別用於模型訓練、引數調整和最終效能評估

編碼器-解碼器模型

在日常生活中,針對編碼器­解碼器這個概念我們可以看到有:

  • 在電視系統上為了便於影片的傳播,會使用各種編碼器將影片編碼成數字訊號,在客戶端,相應的解碼器元件會把收到的數字訊號解碼為影片。
  • 電話透過對聲波和電訊號進行相互轉換,達到傳遞聲音的目的。

針對神經網路的翻譯過程,我們可以這樣子理解:
針對給定的中文句子“我/對/你/感到/滿意”,編碼器會將這句話編碼成一個實數向量(0.2, −1, 6, 5, 0.7, −2),這個向量就是源語言句子的“表示”結果。雖然有些不可思議,但是神經機器翻譯模型把這個向量等同於輸入序列。向量中的數字並沒有實際的意義,然而解碼器卻能從中提取到源語言句子中所包含的資訊。也有研究人員把向量的每一個維度看作是一個“特徵”,這樣源語言句子就被表示成多個“特徵”的聯合,而且這些特徵可以被自動學習。有了這樣的源語言句子的“表示”,解碼器可以把這個實數向量作為輸入,然後逐詞生成目標語言句子“I am satisfied with you”。

編碼器

編碼器由詞嵌入層和中間網路層組成:

  • 當輸入一串單詞序列時,詞嵌入層(embedding)會將每個單詞對映到多維實數表示空間,這個過程也被稱為詞嵌入。
  • 之後中間層會對詞嵌入向量進行更深層的抽象,得到輸入單詞序列的中間表示。中間層的實現方式有很多,比如:迴圈神經網路、卷積神經網路、自注意力機制等都是模型常用的結構。

解碼器

解碼器的結構基本上和編碼器是一致的,在基於迴圈神經網路的翻譯模型中,解碼器只比編碼器多了輸出層,用於輸出每個目標語言位置的單詞生成機率,而在基於自注意力機制的翻譯模型中,除了輸出層,解碼器還比編碼器多一個編碼­解碼注意力子層,用於幫助模型更好地利用源語言資訊。

RNN編碼器-解碼器

以基於迴圈神經網路的機器翻譯模型為例。

左側為編碼器部分,源語言單詞按照其在文字序列中的先後順序被依次送入到迴圈神經網路(RNN)當中。在每個時間步 t 中,模型依據送入的源語言單詞$$x_{t} $$對應修改並維護其模型內部的隱狀態 $$h_{t}$$,這個隱狀態編碼了輸入的源語言序列前 t 個時刻的所有必要資訊。按照這種方式當 m 個輸入全部被送入到編碼器之後,所對應的 $$h_{m}$$可以認為包含了源語言序列的所有資訊。

blob:https://datawhaler.feishu.cn/734451ef-196c-48b7-b501-3042b2b37fc4

右半部分是 RNN 解碼器部分,它接收編碼器輸出的編碼源語言句子資訊的向量 $$h_{m}$$作為初始隱狀態 $$s_{0}$$。由於 RNN 的迴圈過程在每個時間步都要求一個輸入單詞,為了啟動解碼過程,一般會使用一個保留的特殊符號 “[Start]” 作為翻譯開始的標記送入到 RNN 解碼器當中並解碼出目標語言序列的第一個單詞 $$z_{1}$$。接下來,$$z_{1}$$ 會作為下一個時刻的輸入被送入到迴圈神經網路當中,並按照不斷迭代產生後續的預測。由於目標語言序列的長度無法被提前預知,因此使用另一個保留符號 “[Stop]” 作為預測結束的標誌。當某一個時刻 t 預測出的目標語言單詞為 zt =“[Stop]” 時,解碼過程動態地停止。在上述過程當中,主要涉及到兩步運算,第一步是 RNN 接收前一時刻隱狀態 $$s_{t-1}$$ 並依據當前時刻輸入 $$z_{t-1}$$(目標語言單詞 $$z_{t-1}$$ 對應的語義嵌入)對隱狀態進行維護並生成$$s_{t}$$的運算過程,第二步是依據當前時刻隱狀態生成目標語言單詞的過程:

blob:https://datawhaler.feishu.cn/2ef84fa6-4299-42ff-aed0-ae058cb773cd

但是僅僅使用一個定長的向量 $$h_{m}$$ 編碼整個源語言序列。這對於較短的源語言文字沒有什麼問題,但隨著文字序列長度的逐漸加長,單一的一個向量 hm 可能不足以承載源語言序列當中的所有資訊。

當文字長度在 20 個單詞以內時,單一向量能夠承載源語言文字中的必要資訊。隨著文字序列的進一步增加,翻譯效能的評價指標 BLEU 的值就開始出現明顯地下降。因此,這就啟發我們使用更加有效地機制從編碼器向解碼器傳遞源語言資訊,這就是接下來要講到的注意力機制。

Attention Mechanism

注意力機制的引入使得不再需要把原始文字中的所有必要資訊壓縮到一個向量當中

blob:https://datawhaler.feishu.cn/eb85be7f-e0b8-46d5-b3a5-596ac4d9433a

注意力機制允許解碼器在生成每個輸出詞時,關注編碼器產生的所有中間狀態,從而更好地利用源序列的資訊。具體來說,給定源語言序列經過編碼器輸出的向量序列 $$h_{1},h_{2},h_{3},...,h_{m}$$,注意力機制旨在依據解碼端翻譯的需要,自適應地從這個向量序列中查詢對應的資訊。

翻譯質量評價

人們在使用機器翻譯系統時需要評估系統輸出結果的質量。這個過程也被稱作機器翻譯譯文質量評價,簡稱為譯文質量評價(Quality Evaluation of Translation)。在機器翻譯的發展程序中,譯文質量評價有著非常重要的作用。不論在系統研發的反覆迭代中,還是在諸多的機器翻譯應用場景中,都存在大量的譯文質量評價環節。從某種意義上說,沒有譯文質量評價,機器翻譯也不會發展成今天的樣子。比如,本世紀初研究人員提出了譯文質量自動評價方法 BLEU(Bilingual Evaluation Understudy)(Task 1知識文件已詳細介紹過)。該方法使得機器翻譯系統的評價變得自動、快速、便捷,而且評價過程可以重複。正是由於 BLEU 等自動評價方法的提出,機器翻譯研究人員可以在更短的時間內得到譯文質量的評價結果,加速系統研發的程序。

blob:https://datawhaler.feishu.cn/4357d981-60e1-411b-a384-5d5be9901a59

相關文章