Task02:文本預處理;語言模型;循環神經網絡基礎

注意:以下圖片均引用自 《動手學深度學習》

一、文本預處理

預處理一般有四步1.讀入文本 2.分詞 3.建立字典,將每個詞映射到一個唯一的索引(index)
4.將文本從此的序列轉換爲索引序列

1.讀入文本

import collections
import re

def read_time_machine():
    with open('/home/kesci/input/timemachine7163/timemachine.txt', 'r') as f: ##將文本打開
        lines = [re.sub('[^a-z]+', ' ', line.strip().lower()) for line in f]   ##運用正則表達式處理文本
    return lines

re.sub 表示對 符合 [^a-z]+ 規則的文本 , 用‘ ’ 進行替換,也就是替換成空格
並且用 line.strip().lower() 將每行文本開頭或結尾的空格或者換行符去除,並且將所有字母小寫化
上面的正則表達式 是指 所有非a-z的字符並且長度大於等於1
經過此次變換我們 只剩下 所有英文字符和空格。
當然 對於 類似於 doesn’t 的處理 會出現 [‘doesn’, ’ ', ‘t’]的結果,這個並不是我們想要的
這裏展示一下部分 讀取結果在這裏插入圖片描述

2.分詞

現在我們需要把這個list變成每個元素都是一個單詞,而不是一個元素有好幾個單詞的情況,這就是我們要做的分詞

def tokenize(setences, token='word'):
	if token == 'word':  ## 詞分詞,列表中每個最小元素是個單詞
		return [setence.split() for setence in setences]
	elif token == ‘char’:  ##字母級分詞,每個最小元素是個字母
		return [list(sentence) for sentence in sentences]
    else:
        print('ERROR: unkown token type '+token)
		

我們現在做 詞分詞 所以用默認展示結果
在這裏插入圖片描述

3.建立字典

我們要將上面每個詞建立獨立的索引編號,並且每個詞在字典裏不會重複出現

def  vocab(tokens):
##set(sum(tokens, []))              sum(list, []) 對列表含列表情況進行去除, 只剩下一個列表
	a = {}
	for i,j in enumerate(list(set(sum(tokens[0:52], [])))):
    	a[j] = i
    return a

在這裏插入圖片描述

4.將文本從此的序列轉換爲索引序列

b = []
for i in range(len(tokens[0])):
    b.append(a[tokens[0][i]])
b

在這裏插入圖片描述

5.分詞工具推薦

現在有幾個比較好的現成庫,可以直接對語句進行分詞
英文分詞庫我們推薦spacy和nltk
中文分詞庫我們推薦jieba
現在對jieba進行演示
在這裏插入圖片描述

二、語言模型

學習提問:動手學深度學習 P208 頁上原文對 P(W2|W1)的 解釋 是 w1,w2兩個詞相鄰的頻率和 w1 詞頻的比值。 p(w1)是w1在訓練集中詞出現的次數與總次數的比的
而 視頻上是說 其中 n(w1) 爲語料庫中以 w1 作爲第一個詞的文本的數量, n 爲語料庫中文本的總數量。其中 n(w1,w2) 爲語料庫中以 w1 作爲第一個詞, w2 作爲第二個詞的文本的數量。
兩種說法不一致,是否應該以書本上爲準。
個人認爲1. P(W1)說法應該以書本爲準,是單個單詞佔所有單詞的比重。
2. P(W2|W1)的 解釋 是 w1,w2兩個詞相鄰的頻率和 w1 詞頻的比值。 書本上的相鄰是否應該改爲,以 w1 作爲第一個詞, w2 作爲第二個詞的文本的數量。

1.n元語法

在建立語言模型之前,我們需要對句子中的詞出現概率,以及一個詞在給定前幾個詞的情況下出現的條件概率。這裏我們舉個例:
一段含有4個詞的文本序列在訓練數據集中出現的概率在這裏插入圖片描述
P(w1)爲詞w1在訓練集中出現的總次數和訓練集中總詞數的比,即詞頻。
P(W2|W1)是 給定第一個詞是w1的情況下,第二個詞是w2的出現總次數,佔w1出現總次數的比。以此類推。

然而隨着序列長度增加,長詞的計算複雜度會指數級增加。n元語法通過馬爾科夫假設簡化了語言模型的計算,即假設一個詞的出現只與前面n個詞相關。舉例:
n =1時, p(w3|w1,w2) = p(w3|w2)
在實踐的時候我們習慣基於n-1階馬爾科夫鏈
當n爲1,2,3時 ,我們將其分別稱作一元語法(unigram)、二元語法(bigram)和三元語法(trigram)。注意這時候n-1爲0,1,2,所以一元語法與前面詞無關,二元語法與前面一個詞相關,三元與前面2個詞相關。
在這裏插入圖片描述

時序數據採樣

現在我們先界定一下我們的輸入 以及想要的輸出.我們現在做的就是希望通過輸入一段文字,來預測他將會要出現的下一段文字是什麼.
假設 我們完整的一個訓練集是 “想要有直升機,想要和你飛到宇宙去”
對於時序數據,我們還有個 時間步數 作爲額外的維度, 簡單來說,時間步數就是指我們單次採樣的大小, 比如我們這次採樣 指定步長爲5,就是每一次採樣 抽取5個字符做爲一個樣本.這裏我們展示一下 步長爲5的可能樣本和標籤:
在這裏插入圖片描述

隨機採樣

隨機採樣 顧名思義,就是在給定的訓練集中以及給定步長下, 隨機抽取連續的字符且長度等於步長的抽樣方法.
每次抽取的批量樣本 沒有關係

相鄰採樣

在相鄰採樣中,相鄰的兩個隨機小批量在原始序列上的位置相鄰
舉個例子比如有一段序列[0,1,2,3,4,5,6,7,8,9,10,11,12]
我們每次抽取3步長的樣本, 每個批量抽取三個樣本
第一批的數值 可能爲:[[0,1,2], [6,7,8],[3,4,5]]
那麼第二批的數值 必然要與 第一批數值相鄰 則[[3,4,5],[9,10,11],[6,7,8]]

三、循環神經網絡基礎

雖然n元語法簡化了計算難度,n-1個詞前面的詞也可能對要出現的詞出現影響,爲了精度我們就需要擴大n,但這樣又會指數級增加計算量。這時候我們又引入了循環神經網絡。現在我們簡單舉例一個字符級循環神經網絡(RNN), 字符級也就是單個字爲分詞, 不以詞組爲分詞。
原句是“想要有直升機”, 現在我們依次輸入“想要有直升”,來讓模型預測每個字符的下一個字。
在這裏插入圖片描述
如圖,在每次預測下一個詞是什麼的時候,我們總是有兩個輸入一個是下一個詞的前面一個字 比如想要預測“有”, 則一個輸入信息是“要”,另一個輸入信息是H1,而H1又是又“想”作爲輸入得到的。所以另一個信息是包含了之前的輸入,到最後輸入“升”的時候,我們會發現另一個信息包含了之前所有字符的輸入。

知識點:one-hot向量, 梯度裁剪, 困惑度

困惑度是對交叉熵損失函數做指數運算後得到的值。特別地,

最佳情況下,模型總是把標籤類別的概率預測爲1,此時困惑度爲1;
最壞情況下,模型總是把標籤類別的概率預測爲0,此時困惑度爲正無窮;
基線情況下,模型總是預測所有類別的概率都相同,此時困惑度爲類別個數。

循環神經網絡從零實現

def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
                          vocab_size, device, corpus_indices, idx_to_char,
                          char_to_idx, is_random_iter, num_epochs, num_steps,
                          lr, clipping_theta, batch_size, pred_period,
                          pred_len, prefixes):
	if is_random_iter:                                           ## 選用的隨機採樣還是相鄰採樣
		data_iter_fn = d2l.data_iter_random
    else:
        data_iter_fn = d2l.data_iter_consecutive
    params = get_params()   ## 獲取(W_xh, W_hh, b_h, W_hq, b_q) 5個參數, 並已經予以初始化
    loss = nn.CrossEntropyLoss()  ##用交叉熵定義損失函數
	for epoch in range(num_epochs):
		if not is_random_iter:
			state = init_rnn_state(batch_size, num_hiddens, device)      ## 如果採樣相鄰採樣, 將初始隱藏層狀態初始化, 全爲0
		l_sum, n, start = 0.0, 0, time.time()
        data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)  ## 獲取inputs  樣本和標籤
        for x,y in data_iter:
        	if is_random_iter:
        		state = init_rnn_state(batch_size, num_hiddens, device)      ## 如果採用隨機採樣, 在每個小批量更新前初始化隱藏狀態
        	else:
        		for s in state:
        			s.detach_()
        	inputs = to_onehot(x, vocab_size)         ## inputs.shape =  時間步長, 字典大小   輸入
            (outputs, state) = rnn(inputs, state, params)  ## outputs有num_steps個形狀爲(batch_size, vocab_size)的矩陣!!!!! 運行網絡
           
            outputs = torch.cat(outputs, dim=0)
            # Y的形狀是(batch_size, num_steps),轉置後再變成形狀爲
            # (num_steps * batch_size,)的向量,這樣跟輸出的行一一對應
            y = torch.flatten(Y.T)
            
            l = loss(outputs, y.long()) ## 使用交叉熵損失計算平均分類誤差  計算誤差
            
            # 梯度清0
            if params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            l.backward()
            grad_clipping(params, clipping_theta, device)  # 裁剪梯度
            d2l.sgd(params, lr, 1)  # 因爲誤差已經取過均值,梯度不用再做平均
            l_sum += l.item() * y.shape[0]
            n += y.shape[0]

        if (epoch + 1) % pred_period == 0:
            print('epoch %d, perplexity %f, time %.2f sec' % (
                epoch + 1, math.exp(l_sum / n), time.time() - start))
            for prefix in prefixes:
                print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
                    num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
        	

循環神經網絡簡潔實現

我們將 字典裏的每個詞 用one-hot 向量進行編碼,所以每個詞的input_size會等於字典裏詞的個數, 之後的隱藏層鍾神經單元的個數 是個超參數,這裏我們假定個數爲256個

num_hiddens = 256
num_steps, batch_size = 35, 2
rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=num_hiddens) ##創建RNN

X = torch.rand(num_steps, batch_size, vocab_size) ##輸入 (步長即每次的單詞個數, 批量大小即每次樣本數量, one-hot向量長度)
state = None  ##隱藏層初始狀態 None
Y, state_new = rnn_layer(X, state)  ## 在RNN網絡中 放入 X和state兩個輸入, 生成Y和 state_new兩個新輸出

class RNNModel(nn.Module):
    def __init__(self, rnn_layer, vocab_size):
        super(RNNModel, self).__init__()
        self.rnn = rnn_layer
        self.hidden_size = rnn_layer.hidden_size * (2 if rnn_layer.bidirectional else 1) 
        self.vocab_size = vocab_size
        self.dense = nn.Linear(self.hidden_size, vocab_size)       ## 定義hidden的輸出 轉爲y

    def forward(self, inputs, state):
        # inputs.shape: (batch_size, num_steps)
        X = to_onehot(inputs, vocab_size)
        X = torch.stack(X)  # X.shape: (num_steps, batch_size, vocab_size)
        hiddens, state = self.rnn(X, state)
        hiddens = hiddens.view(-1, hiddens.shape[-1])  ## hiddens.shape: (num_steps * batch_size, hidden_size)   調整hiddens 的大小
        output = self.dense(hiddens)  ## 將hiddens輸出 轉爲Y類型輸出
        return output, state

def train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
                                corpus_indices, idx_to_char, char_to_idx,
                                num_epochs, num_steps, lr, clipping_theta,
                                batch_size, pred_period, pred_len, prefixes):
    loss = nn.CrossEntropyLoss()  ##損失函數
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)  ##優化器 
    model.to(device)  ## RNN網絡構建
    for epoch in range(num_epochs):
        l_sum, n, start = 0.0, 0, time.time()
        data_iter = d2l.data_iter_consecutive(corpus_indices, batch_size, num_steps, device) # 相鄰採樣
        state = None
        for X, Y in data_iter:
            if state is not None:
                # 使用detach函數從計算圖分離隱藏狀態
                if isinstance (state, tuple): # LSTM, state:(h, c)  
                    state[0].detach_()
                    state[1].detach_()
                else: 
                    state.detach_()
            (output, state) = model(X, state) # output.shape: (num_steps * batch_size, vocab_size)
            y = torch.flatten(Y.T)
            l = loss(output, y.long())   ##計算損失
            
            optimizer.zero_grad()  ##梯度歸零
            l.backward()
            grad_clipping(model.parameters(), clipping_theta, device) ##梯度裁剪 
            optimizer.step()  ##運行優化器
            l_sum += l.item() * y.shape[0]
            n += y.shape[0]
        

        if (epoch + 1) % pred_period == 0:
            print('epoch %d, perplexity %f, time %.2f sec' % (
                epoch + 1, math.exp(l_sum / n), time.time() - start))
            for prefix in prefixes:
                print(' -', predict_rnn_pytorch(
                    prefix, pred_len, model, vocab_size, device, idx_to_char,
                    char_to_idx))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章