DL notes 03:機器翻譯(MT)/NLP 基礎知識

一、機器翻譯

機器翻譯(MT):將一段文本從一種語言自動翻譯爲另一種語言,用神經網絡解決這個問題通常稱爲神經機器翻譯(NMT)。 主要特徵:輸出是單詞序列而不是單個單詞。 輸出序列的長度可能與源序列的長度不同。

1.1 數據預處理和清洗

將數據集清洗、轉化爲神經網絡的輸入minbatch,這是任何一個神經網絡應用的首要步驟。
字符在計算機裏是以編碼的形式存在,我們通常所用的空格是 \x20 ,是在標準ASCII可見字符 0x20~0x7e 範圍內。 而 \xa0 屬於 latin1 (ISO/IEC_8859-1)中的擴展字符集字符,代表不間斷空白符nbsp(non-breaking space),超出gbk編碼範圍,是需要去除的特殊字符。再數據預處理的過程中,我們首先需要對數據進行清洗。

def preprocess_raw(text):
    text = text.replace('\u202f', ' ').replace('\xa0', ' ')
    out = ''
    for i, char in enumerate(text.lower()):
        if char in (',', '!', '.') and i > 0 and text[i-1] != ' ':
            out += ' '
        out += char
    return out

1.2 文本預處理

1.2.1 分詞

字符串—單詞組成的列表,屬於文本預處理環節的一步。我們對每個句子進行分詞,也就是將一個句子劃分成若干個詞(token),轉換爲一個詞的序列。同樣已經有很多工具可以直接用於分詞。
用現有工具進行分詞
我們前面介紹的分詞方式非常簡單,它至少有以下幾個缺點:

  1. 標點符號通常可以提供語義信息,但是我們的方法直接將其丟棄了
  2. 類似“shouldn’t", "doesn’t"這樣的詞會被錯誤地處理
  3. 類似"Mr.", "Dr."這樣的詞會被錯誤地處理

我們可以通過引入更復雜的規則來解決這些問題,但是事實上,有一些現有的工具可以很好地進行分詞,我們在這裏簡單介紹其中的兩個:spaCyNLTK
舉例如下:

# spaCy
import spacy
nlp = spacy.load('en_core_web_sm')
doc = nlp(text)
print([token.text for token in doc])


# NLTK 
from nltk.tokenize import word_tokenize
from nltk import data
data.path.append('/home/kesci/input/nltk_data3784/nltk_data')
print(word_tokenize(text))

1.2.2 建立字典

爲了方便模型處理,我們需要將字符串轉換爲數字。因此我們需要先構建一個字典(vocabulary),將每個詞映射到一個唯一的索引編號。建立字典的過程會得到如下信息:

  1. 去重後詞典,及其中單詞對應的索引列表
  2. 還可以得到給定索引找到其對應的單詞的列表,以及給定單詞得到對應索引的字典。
  3. 原始語料所有詞對應的詞典索引的列表

注意:在構建字典時我們要對每個句子的開頭,結尾,以及未知詞彙特設元素對應。另外我們數據集中的句子並非對齊的,我們也要設置padding特殊詞來對短句子進行補全長度。同樣對於過長的句子我們也要進行截斷操作,節省計算開支。

class Vocab(object):
    def __init__(self, tokens, min_freq=0, use_special_tokens=False):
        counter = count_corpus(tokens)  # : 
        self.token_freqs = list(counter.items())
        self.idx_to_token = []
        if use_special_tokens:
            # padding, begin of sentence, end of sentence, unknown
            self.pad, self.bos, self.eos, self.unk = (0, 1, 2, 3)
            self.idx_to_token += ['<pad>', '<bos>', '<eos>', '<unk>']
        else:
            self.unk = 0
            self.idx_to_token += ['<unk>']
        self.idx_to_token += [token for token, freq in self.token_freqs
                        if freq >= min_freq and token not in self.idx_to_token]
        self.token_to_idx = dict()
        for idx, token in enumerate(self.idx_to_token):
            self.token_to_idx[token] = idx

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

def count_corpus(sentences):
    tokens = [tk for st in sentences for tk in st]
    return collections.Counter(tokens)  # 返回一個字典,記錄每個詞的出現次數

在生成字典時,我們會通常把高頻出現的單詞放在字典開始的部分,這樣可以減少查詢次數(相對於隨機編碼),訓練word2vec中有個HUffman樹,也是這個思想。

 token_freqs = sorted(counter.items(), key=lambda x:x[0])
 token_freqs.sort(key=lambda x:x[1], reverse=True)

1.2.3 將詞轉爲索引

使用字典,我們可以將原文本中的句子從單詞序列轉換爲索引序列

1.3 語言模型

一段自然語言文本可以看作是一個離散時間序列,給定一個長度TT爲的詞的序列w1,w2,,wTw_1, w_2, \ldots, w_T,語言模型的目標就是評估該序列是否合理,即計算該序列的概率:
P(w1,w2,,wT).P(w_1, w_2, \ldots, w_T).
本節我們介紹基於統計的語言模型,主要是nn元語法(nn-gram)。在後續內容中,我們將會介紹基於神經網絡的語言模型。
假設序列w1,w2,,wTw_1, w_2, \ldots, w_T中的每個詞是依次生成的,我們有
P(w1,w2,,wT)=t=1TP(wtw1,,wt1)=P(w1)P(w2w1)P(wTw1w2wT1) \begin{aligned} P(w_1, w_2, \ldots, w_T) &= \prod_{t=1}^T P(w_t \mid w_1, \ldots, w_{t-1})\\ &= P(w_1)P(w_2 \mid w_1) \cdots P(w_T \mid w_1w_2\cdots w_{T-1}) \end{aligned}
例如,一段含有4個詞的文本序列的概率
P(w1,w2,w3,w4)=P(w1)P(w2w1)P(w3w1,w2)P(w4w1,w2,w3).P(w_1, w_2, w_3, w_4) = P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_1, w_2) P(w_4 \mid w_1, w_2, w_3).
語言模型的參數就是詞的概率以及給定前幾個詞情況下的條件概率。設訓練數據集爲一個大型文本語料庫,如維基百科的所有條目,詞的概率可以通過該詞在訓練數據集中的相對詞頻來計算,例如,w1w_1的概率可以計算爲:
P^(w1)=n(w1)n\hat P(w_1) = \frac{n(w_1)}{n}
其中n(w1)n(w_1)爲語料庫中以作爲第一個詞的文本的數量,nn爲語料庫中文本的總數量。

類似的,給定w1w_1情況下,w2w_2的條件概率可以計算爲:
P^(w2w1)=n(w1,w2)n(w1)\hat P(w_2 \mid w_1) = \frac{n(w_1, w_2)}{n(w_1)}
其中n(w1,w2)n(w_1, w_2)爲語料庫中以w1w_1作爲第一個詞,w2w_2作爲第二個詞的文本的數量。

nn元語法

序列長度增加,計算和存儲多個詞共同出現的概率的複雜度會呈指數級增加。nn元語法通過馬爾可夫假設簡化模型,馬爾科夫假設是指一個詞的出現只與前面nn個詞相關,即nn階馬爾可夫鏈(Markov chain of order nn),如果n=1n=1,那麼有P(w3w1,w2)=P(w3w2)P(w_3 \mid w_1, w_2) = P(w_3 \mid w_2)。基於n1n-1階馬爾可夫鏈,我們可以將語言模型改寫爲
P(w1,w2,,wT)=t=1TP(wtwt(n1),,wt1).P(w_1, w_2, \ldots, w_T) = \prod_{t=1}^T P(w_t \mid w_{t-(n-1)}, \ldots, w_{t-1}) .
以上也叫nn元語法(nn-grams),它是基於n1n-1階馬爾可夫鏈的概率語言模型。例如,當時n=2n=2,含有4個詞的文本序列的概率就可以改寫爲:
P(w1,w2,w3,w4)=P(w1)P(w2w1)P(w3w1,w2)P(w4w1,w2,w3)=P(w1)P(w2w1)P(w3w2)P(w4w3) \begin{aligned} P(w_1, w_2, w_3, w_4) &= P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_1, w_2) P(w_4 \mid w_1, w_2, w_3)\\ &= P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_2) P(w_4 \mid w_3) \end{aligned}
nn分別爲1、2和3時,我們將其分別稱作一元語法(unigram)、二元語法(bigram)和三元語法(trigram)。例如,長度爲4的序列w1,w2,w3,w4w_1, w_2, w_3, w_4在一元語法、二元語法和三元語法中的概率分別爲
P(w1,w2,w3,w4)=P(w1)P(w2)P(w3)P(w4),P(w1,w2,w3,w4)=P(w1)P(w2w1)P(w3w2)P(w4w3),P(w1,w2,w3,w4)=P(w1)P(w2w1)P(w3w1,w2)P(w4w2,w3). \begin{aligned} P(w_1, w_2, w_3, w_4) &= P(w_1) P(w_2) P(w_3) P(w_4) ,\\ P(w_1, w_2, w_3, w_4) &= P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_2) P(w_4 \mid w_3) ,\\ P(w_1, w_2, w_3, w_4) &= P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_1, w_2) P(w_4 \mid w_2, w_3) . \end{aligned}
nn較小時,nn元語法往往並不準確。例如,在一元語法中,由三個詞組成的句子“你走先”和“你先走”的概率是一樣的。然而,當nn較大時,nn元語法需要計算並存儲大量的詞頻和多詞相鄰頻率。

思考:nn元語法可能有哪些缺陷?
1.參數空間過大
2.數據稀疏

1.4 時序數據的採樣

在訓練中我們需要每次隨機讀取小批量樣本和標籤。與之前章節的實驗數據不同的是,時序數據的一個樣本通常包含連續的字符。假設時間步數爲5,樣本序列爲5個字符,即“想”“要”“有”“直”“升”。該樣本的標籤序列爲這些字符分別在訓練集中的下一個字符,即“要”“有”“直”“升”“機”,即XX=“想要有直升”,YY=“要有直升機”。

現在我們考慮序列“想要有直升機,想要和你飛到宇宙去”,如果時間步數爲5,有以下可能的樣本和標籤:

  • XX:“想要有直升”,YY:“要有直升機”
  • XX:“要有直升機”,YY:“有直升機,”
  • XX:“有直升機,”,YY:“直升機,想”
  • XX:“要和你飛到”,YY:“和你飛到宇”
  • XX:“和你飛到宇”,YY:“你飛到宇宙”
  • XX:“你飛到宇宙”,YY:“飛到宇宙去”
    可以看到,如果序列的長度爲TT,時間步數爲nn,那麼一共有TnT-n個合法的樣本,但是這些樣本有大量的重合,我們通常採用更加高效的採樣方式。我們有兩種方式對時序數據進行採樣,分別是隨機採樣和相鄰採樣。

隨機採樣

在隨機採樣中,每個樣本是原始序列上任意截取的一段序列,相鄰的兩個隨機小批量在原始序列上的位置不一定相毗鄰。

相鄰採樣

在相鄰採樣中,相鄰的兩個隨機小批量在原始序列上的位置相毗鄰。

二、Encoder-Decoder

針對輸入輸出不等價來進行先編碼再解碼
encoder:輸入到隱藏狀態
decoder:隱藏狀態到輸出
encoder-decoder

觀察decoder的template代碼可以發現decoder包含一個初始化輸入狀態的函數,當decoder輸出<eos>等結束符則代表結束:

class Decoder(nn.Module):
    def __init__(self, **kwargs):
        super(Decoder, self).__init__(**kwargs)

    def init_state(self, enc_outputs, *args):
        raise NotImplementedError

    def forward(self, X, state):
        raise NotImplementedError

encoder-decoder 模式常用於NLP領域的對話系統、生成式任務(生成歌詞)、翻譯。

Seq2Seq 模型初探

訓練模式:
seq2seq_train
預測模式:
seq2seq_test
具體結構:
seq2seq_model
注意這裏存在Embedding層用來將進行詞嵌入,由單詞獲得詞向量。一般情況下輸入到編碼網絡中的數據不是一個onehot向量而是經過了編碼之後的向量,比如由word2vec技術,讓編碼後的向量由更加豐富的含義。
損失函數的計算時需要考慮句子的有效長度,這裏介紹序列掩膜的函數來計算有效部分的損失和加入掩膜的交叉熵損失函數。

def SequenceMask(X, X_len,value=0):
    maxlen = X.size(1)
    mask = torch.arange(maxlen)[None, :].to(X_len.device) < X_len[:, None]   
    X[~mask]=value
    return X
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    # pred shape: (batch_size, seq_len, vocab_size)
    # label shape: (batch_size, seq_len)
    # valid_length shape: (batch_size, )
    def forward(self, pred, label, valid_length):
        # the sample weights shape should be (batch_size, seq_len)
        weights = torch.ones_like(label)
        weights = SequenceMask(weights, valid_length).float()
        self.reduction='none'
        output=super(MaskedSoftmaxCELoss, self).forward(pred.transpose(1,2), label)
        return (output*weights).mean(dim=1)

Beam Search

Seq2Seq模型的每個詞的輸出都是在整個詞的字典空間中先確定分數最高的詞,然後再組成整個句子,然而這樣並沒有對輸出的整個句子的通順程度做進一步的衡量,只依賴hidden state能夠學習到上下文。通過大量的實驗和應用,人們發現只考慮單個詞的優化是不足夠的。那如何找到最好的句子呢?
greedy
一般的貪心搜索(greedy search)方法會在整個字典空間內選擇最優解。如果一句話的長度爲NN個單詞,單詞的字典容量爲KK.則搜索空間爲KNK^N,這種搜索方法叫做維特比算法.
集束搜索則以時間序列去進行逐步搜索,每時間步上只取top-n的候選結果,最終獲得nn個搜索出的候選結果,nn稱爲beam size,過程如圖所示:
beam-search
針對Transformer我將單獨用一篇blog進行詳述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章