注意:以下圖片均引用自 《動手學深度學習》
一、文本預處理
預處理一般有四步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))