文本預處理
文本是一類序列數(shù)據(jù),一篇文章可以看作是字符或單詞的序列,本節(jié)將介紹文本數(shù)據(jù)的常見預處理步驟,預處理通常包括四個步驟:
- 讀入文本
- 分詞
- 建立字典,將每個詞映射到一個唯一的索引(index)
- 將文本從詞的序列轉換為索引的序列,方便輸入模型
讀入文本
我們用一部英文小說,即H. G. Well的Time Machine,作為示例,展示文本預處理的具體過程。
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
lines = read_time_machine()
print('# sentences %d' % len(lines))
# sentences 3221
分詞
我們對每個句子進行分詞,也就是將一個句子劃分成若干個詞(token),轉換為一個詞的序列。
def tokenize(sentences, token='word'):
"""Split sentences into word or char tokens"""
if token == 'word':
return [sentence.split(' ') for sentence in sentences]
elif token == 'char':
return [list(sentence) for sentence in sentences]
else:
print('ERROR: unkown token type '+token)
tokens = tokenize(lines)
tokens[0:10]
[['the', 'time', 'machine', 'by', 'h', 'g', 'wells', ''],
[''],
[''],
[''],
[''],
['i'],
[''],
[''],
['the',
'time',
'traveller',
'for',
'so',
'it',
'will',
'be',
'convenient',
'to',
'speak',
'of',
'him',
''],
['was',
'expounding',
'a',
'recondite',
'matter',
'to',
'us',
'his',
'grey',
'eyes',
'shone',
'and']]
建立字典
為了方便模型處理,我們需要將字符串轉換為數(shù)字。因此我們需要先構建一個字典(vocabulary),將每個詞映射到一個唯一的索引編號。
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 += ['', '', '', '']
else:
self.unk = 0
self.idx_to_token += ['']
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) # 返回一個字典,記錄每個詞的出現(xiàn)次數(shù)
我們看一個例子,這里我們嘗試用Time Machine作為語料構建字典
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[0:20])
[('', 0), ('the', 1), ('time', 2), ('machine', 3), ('by', 4), ('h', 5), ('g', 6), ('wells', 7), ('i', 8), ('traveller', 9), ('for', 10), ('so', 11), ('it', 12), ('will', 13), ('be', 14), ('convenient', 15), ('to', 16), ('speak', 17), ('of', 18), ('him', 19)]
將詞轉為索引
使用字典,我們可以將原文本中的句子從單詞序列轉換為索引序列
for i in range(0, 10):
print('words:', tokens[i])
print('indices:', vocab[tokens[i]])
words: ['the', 'time', 'machine', 'by', 'h', 'g', 'wells', '']
indices: [1, 2, 3, 4, 5, 6, 7, 0]
words: ['']
indices: [0]
words: ['']
indices: [0]
words: ['']
indices: [0]
words: ['']
indices: [0]
words: ['i']
indices: [8]
words: ['']
indices: [0]
words: ['']
indices: [0]
words: ['the', 'time', 'traveller', 'for', 'so', 'it', 'will', 'be', 'convenient', 'to', 'speak', 'of', 'him', '']
indices: [1, 2, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0]
words: ['was', 'expounding', 'a', 'recondite', 'matter', 'to', 'us', 'his', 'grey', 'eyes', 'shone', 'and']
indices: [20, 21, 22, 23, 24, 16, 25, 26, 27, 28, 29, 30]
用現(xiàn)有工具進行分詞
我們前面介紹的分詞方式非常簡單,它至少有以下幾個缺點:
- 標點符號通??梢蕴峁┱Z義信息,但是我們的方法直接將其丟棄了
- 類似“shouldn't", "doesn't"這樣的詞會被錯誤地處理
- 類似"Mr.", "Dr."這樣的詞會被錯誤地處理
我們可以通過引入更復雜的規(guī)則來解決這些問題,但是事實上,有一些現(xiàn)有的工具可以很好地進行分詞,我們在這里簡單介紹其中的兩個:spaCy和NLTK。
下面是一個簡單的例子:
text = "Mr. Chen doesn't agree with my suggestion."
spaCy:
import spacy
nlp = spacy.load('en_core_web_sm')
doc = nlp(text)
print([token.text for token in doc])
['Mr.', 'Chen', 'does', "n't", 'agree', 'with', 'my', 'suggestion', '.']
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))
語言模型
一段自然語言文本可以看作是一個離散時間序列,給定一個長度為的詞的序列
,語言模型的目標就是評估該序列是否合理,即計算該序列的概率:
本節(jié)我們介紹基于統(tǒng)計的語言模型,主要是元語法(
-gram)。在后續(xù)內(nèi)容中,我們將會介紹基于神經(jīng)網(wǎng)絡的語言模型。
語言模型
假設序列中的每個詞是依次生成的,我們有
例如,一段含有4個詞的文本序列的概率
語言模型的參數(shù)就是詞的概率以及給定前幾個詞情況下的條件概率。設訓練數(shù)據(jù)集為一個大型文本語料庫,如維基百科的所有條目,詞的概率可以通過該詞在訓練數(shù)據(jù)集中的相對詞頻來計算,例如,的概率可以計算為:
其中為語料庫中以
作為第一個詞的文本的數(shù)量,
為語料庫中文本的總數(shù)量。
類似的,給定情況下,
的條件概率可以計算為:
其中為語料庫中以
作為第一個詞,
作為第二個詞的文本的數(shù)量。
n元語法
序列長度增加,計算和存儲多個詞共同出現(xiàn)的概率的復雜度會呈指數(shù)級增加。元語法通過馬爾可夫假設簡化模型,馬爾科夫假設是指一個詞的出現(xiàn)只與前面
個詞相關,即
階馬爾可夫鏈(Markov chain of order
),如果
,那么有
?;?img class="math-inline" src="https://math.jianshu.com/math?formula=n-1" alt="n-1" mathimg="1">階馬爾可夫鏈,我們可以將語言模型改寫為
以上也叫元語法(
-grams),它是基于
階馬爾可夫鏈的概率語言模型。例如,當
時,含有4個詞的文本序列的概率就可以改寫為:
當分別為1、2和3時,我們將其分別稱作一元語法(unigram)、二元語法(bigram)和三元語法(trigram)。例如,長度為4的序列
在一元語法、二元語法和三元語法中的概率分別為
當較小時,
元語法往往并不準確。例如,在一元語法中,由三個詞組成的句子“你走先”和“你先走”的概率是一樣的。然而,當
較大時,
元語法需要計算并存儲大量的詞頻和多詞相鄰頻率。
思考:元語法可能有哪些缺陷?
- 參數(shù)空間過大
- 數(shù)據(jù)稀疏
語言模型數(shù)據(jù)集
讀取數(shù)據(jù)集
with open('/home/kesci/input/jaychou_lyrics4703/jaychou_lyrics.txt') as f:
corpus_chars = f.read()
print(len(corpus_chars))
print(corpus_chars[: 40])
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[: 10000]
63282
想要有直升機
想要和你飛到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每
建立字符索引
idx_to_char = list(set(corpus_chars)) # 去重,得到索引到字符的映射
char_to_idx = {char: i for i, char in enumerate(idx_to_char)} # 字符到索引的映射
vocab_size = len(char_to_idx)
print(vocab_size)
corpus_indices = [char_to_idx[char] for char in corpus_chars] # 將每個字符轉化為索引,得到一個索引的序列
sample = corpus_indices[: 20]
print('chars:', ''.join([idx_to_char[idx] for idx in sample]))
print('indices:', sample)
1027
chars: 想要有直升機 想要和你飛到宇宙去 想要和
indices: [824, 903, 535, 895, 691, 561, 239, 824, 903, 559, 849, 19, 179, 32, 746, 1000, 239, 824, 903, 559]
定義函數(shù)load_data_jay_lyrics,在后續(xù)章節(jié)中直接調用。
def load_data_jay_lyrics():
with open('/home/kesci/input/jaychou_lyrics4703/jaychou_lyrics.txt') as f:
corpus_chars = f.read()
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx)
corpus_indices = [char_to_idx[char] for char in corpus_chars]
return corpus_indices, char_to_idx, idx_to_char, vocab_size
時序數(shù)據(jù)的采樣
在訓練中我們需要每次隨機讀取小批量樣本和標簽。與之前章節(jié)的實驗數(shù)據(jù)不同的是,時序數(shù)據(jù)的一個樣本通常包含連續(xù)的字符。假設時間步數(shù)為5,樣本序列為5個字符,即“想”“要”“有”“直”“升”。該樣本的標簽序列為這些字符分別在訓練集中的下一個字符,即“要”“有”“直”“升”“機”,即=“想要有直升”,
=“要有直升機”。
現(xiàn)在我們考慮序列“想要有直升機,想要和你飛到宇宙去”,如果時間步數(shù)為5,有以下可能的樣本和標簽:
-
:“想要有直升”,
:“要有直升機”
-
:“要有直升機”,
:“有直升機,”
-
:“有直升機,”,
:“直升機,想”
- ...
-
:“要和你飛到”,
:“和你飛到宇”
-
:“和你飛到宇”,
:“你飛到宇宙”
-
:“你飛到宇宙”,
:“飛到宇宙去”
可以看到,如果序列的長度為,時間步數(shù)為
,那么一共有
個合法的樣本,但是這些樣本有大量的重合,我們通常采用更加高效的采樣方式。我們有兩種方式對時序數(shù)據(jù)進行采樣,分別是隨機采樣和相鄰采樣。
隨機采樣
下面的代碼每次從數(shù)據(jù)里隨機采樣一個小批量。其中批量大小batch_size是每個小批量的樣本數(shù),num_steps是每個樣本所包含的時間步數(shù)。
在隨機采樣中,每個樣本是原始序列上任意截取的一段序列,相鄰的兩個隨機小批量在原始序列上的位置不一定相毗鄰。
import torch
import random
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
# 減1是因為對于長度為n的序列,X最多只有包含其中的前n - 1個字符
num_examples = (len(corpus_indices) - 1) // num_steps # 下取整,得到不重疊情況下的樣本個數(shù)
example_indices = [i * num_steps for i in range(num_examples)] # 每個樣本的第一個字符在corpus_indices中的下標
random.shuffle(example_indices)
def _data(i):
# 返回從i開始的長為num_steps的序列
return corpus_indices[i: i + num_steps]
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for i in range(0, num_examples, batch_size):
# 每次選出batch_size個隨機樣本
batch_indices = example_indices[i: i + batch_size] # 當前batch的各個樣本的首字符的下標
X = [_data(j) for j in batch_indices]
Y = [_data(j + 1) for j in batch_indices]
yield torch.tensor(X, device=device), torch.tensor(Y, device=device)
測試一下這個函數(shù),我們輸入從0到29的連續(xù)整數(shù)作為一個人工序列,設批量大小和時間步數(shù)分別為2和6,打印隨機采樣每次讀取的小批量樣本的輸入X和標簽Y。
my_seq = list(range(10))
for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=2):
print('X: ', X, '\nY:', Y, '\n')
X: tensor([[4, 5],
[6, 7]])
Y: tensor([[5, 6],
[7, 8]])
X: tensor([[2, 3],
[0, 1]])
Y: tensor([[3, 4],
[1, 2]])
相鄰采樣
在相鄰采樣中,相鄰的兩個隨機小批量在原始序列上的位置相毗鄰。
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
corpus_len = len(corpus_indices) // batch_size * batch_size # 保留下來的序列的長度
corpus_indices = corpus_indices[: corpus_len] # 僅保留前corpus_len個字符
indices = torch.tensor(corpus_indices, device=device)
indices = indices.view(batch_size, -1) # resize成(batch_size, )
batch_num = (indices.shape[1] - 1) // num_steps
for i in range(batch_num):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
同樣的設置下,打印相鄰采樣每次讀取的小批量樣本的輸入X和標簽Y。相鄰的兩個隨機小批量在原始序列上的位置相毗鄰。
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=2):
print('X: ', X, '\nY:', Y, '\n')
X: tensor([[0, 1],
[5, 6]])
Y: tensor([[1, 2],
[6, 7]])
X: tensor([[2, 3],
[7, 8]])
Y: tensor([[3, 4],
[8, 9]])
循環(huán)神經(jīng)網(wǎng)絡
本節(jié)介紹循環(huán)神經(jīng)網(wǎng)絡,下圖展示了如何基于循環(huán)神經(jīng)網(wǎng)絡實現(xiàn)語言模型。我們的目的是基于當前的輸入與過去的輸入序列,預測序列的下一個字符。循環(huán)神經(jīng)網(wǎng)絡引入一個隱藏變量,用
表示
在時間步
的值。
的計算基于
和
,可以認為
記錄了到當前字符為止的序列信息,利用
對序列的下一個字符進行預測。
循環(huán)神經(jīng)網(wǎng)絡的構造
我們先看循環(huán)神經(jīng)網(wǎng)絡的具體構造。假設是時間步
的小批量輸入,
是該時間步的隱藏變量,則:
其中,,
,
,
函數(shù)是非線性激活函數(shù)。由于引入了
,
能夠捕捉截至當前時間步的序列的歷史信息,就像是神經(jīng)網(wǎng)絡當前時間步的狀態(tài)或記憶一樣。由于
的計算基于
,上式的計算是循環(huán)的,使用循環(huán)計算的網(wǎng)絡即循環(huán)神經(jīng)網(wǎng)絡(recurrent neural network)。
在時間步,輸出層的輸出為:
其中,
。
從零開始實現(xiàn)循環(huán)神經(jīng)網(wǎng)絡
我們先嘗試從零開始實現(xiàn)一個基于字符級循環(huán)神經(jīng)網(wǎng)絡的語言模型,這里我們使用周杰倫的歌詞作為語料,首先我們讀入數(shù)據(jù):
import torch
import torch.nn as nn
import time
import math
import sys
sys.path.append("/home/kesci/input")
import d2l_jay9460 as d2l
(corpus_indices, char_to_idx, idx_to_char, vocab_size) = d2l.load_data_jay_lyrics()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
one-hot向量
我們需要將字符表示成向量,這里采用one-hot向量。假設詞典大小是,每次字符對應一個從
到
的唯一的索引,則該字符的向量是一個長度為
的向量,若字符的索引是
,則該向量的第
個位置為
,其他位置為
。下面分別展示了索引為0和2的one-hot向量,向量長度等于詞典大小。
def one_hot(x, n_class, dtype=torch.float32):
result = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device) # shape: (n, n_class)
result.scatter_(1, x.long().view(-1, 1), 1) # result[i, x[i, 0]] = 1
return result
x = torch.tensor([0, 2])
x_one_hot = one_hot(x, vocab_size)
print(x_one_hot)
print(x_one_hot.shape)
print(x_one_hot.sum(axis=1))
tensor([[1., 0., 0., ..., 0., 0., 0.],
[0., 0., 1., ..., 0., 0., 0.]])
torch.Size([2, 1027])
tensor([1., 1.])
我們每次采樣的小批量的形狀是(批量大小, 時間步數(shù))。下面的函數(shù)將這樣的小批量變換成數(shù)個形狀為(批量大小, 詞典大?。┑木仃?,矩陣個數(shù)等于時間步數(shù)。也就是說,時間步的輸入為
,其中
為批量大小,
為詞向量大小,即one-hot向量長度(詞典大小)。
def to_onehot(X, n_class):
return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]
X = torch.arange(10).view(2, 5)
inputs = to_onehot(X, vocab_size)
print(len(inputs), inputs[0].shape)
5 torch.Size([2, 1027])
初始化模型參數(shù)
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
# num_inputs: d
# num_hiddens: h, 隱藏單元的個數(shù)是超參數(shù)
# num_outputs: q
def get_params():
def _one(shape):
param = torch.zeros(shape, device=device, dtype=torch.float32)
nn.init.normal_(param, 0, 0.01)
return torch.nn.Parameter(param)
# 隱藏層參數(shù)
W_xh = _one((num_inputs, num_hiddens))
W_hh = _one((num_hiddens, num_hiddens))
b_h = torch.nn.Parameter(torch.zeros(num_hiddens, device=device))
# 輸出層參數(shù)
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device))
return (W_xh, W_hh, b_h, W_hq, b_q)
定義模型
函數(shù)rnn用循環(huán)的方式依次完成循環(huán)神經(jīng)網(wǎng)絡每個時間步的計算。
def rnn(inputs, state, params):
# inputs和outputs皆為num_steps個形狀為(batch_size, vocab_size)的矩陣
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
H = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
函數(shù)init_rnn_state初始化隱藏變量,這里的返回值是一個元組。
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
做個簡單的測試來觀察輸出結果的個數(shù)(時間步數(shù)),以及第一個時間步的輸出層輸出的形狀和隱藏狀態(tài)的形狀。
print(X.shape)
print(num_hiddens)
print(vocab_size)
state = init_rnn_state(X.shape[0], num_hiddens, device)
inputs = to_onehot(X.to(device), vocab_size)
params = get_params()
outputs, state_new = rnn(inputs, state, params)
print(len(inputs), inputs[0].shape)
print(len(outputs), outputs[0].shape)
print(len(state), state[0].shape)
print(len(state_new), state_new[0].shape)
torch.Size([2, 5])
256
1027
5 torch.Size([2, 1027])
5 torch.Size([2, 1027])
1 torch.Size([2, 256])
1 torch.Size([2, 256])
裁剪梯度
循環(huán)神經(jīng)網(wǎng)絡中較容易出現(xiàn)梯度衰減或梯度爆炸,這會導致網(wǎng)絡幾乎無法訓練。裁剪梯度(clip gradient)是一種應對梯度爆炸的方法。假設我們把所有模型參數(shù)的梯度拼接成一個向量 ,并設裁剪的閾值是
。裁剪后的梯度
的范數(shù)不超過
。
def grad_clipping(params, theta, device):
norm = torch.tensor([0.0], device=device)
for param in params:
norm += (param.grad.data ** 2).sum()
norm = norm.sqrt().item()
if norm > theta:
for param in params:
param.grad.data *= (theta / norm)
定義預測函數(shù)
以下函數(shù)基于前綴prefix(含有數(shù)個字符的字符串)來預測接下來的num_chars個字符。這個函數(shù)稍顯復雜,其中我們將循環(huán)神經(jīng)單元rnn設置成了函數(shù)參數(shù),這樣在后面小節(jié)介紹其他循環(huán)神經(jīng)網(wǎng)絡時能重復使用這個函數(shù)。
def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx):
state = init_rnn_state(1, num_hiddens, device)
output = [char_to_idx[prefix[0]]] # output記錄prefix加上預測的num_chars個字符
for t in range(num_chars + len(prefix) - 1):
# 將上一時間步的輸出作為當前時間步的輸入
X = to_onehot(torch.tensor([[output[-1]]], device=device), vocab_size)
# 計算輸出和更新隱藏狀態(tài)
(Y, state) = rnn(X, state, params)
# 下一個時間步的輸入是prefix里的字符或者當前的最佳預測字符
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(Y[0].argmax(dim=1).item())
return ''.join([idx_to_char[i] for i in output])
我們先測試一下predict_rnn函數(shù)。我們將根據(jù)前綴“分開”創(chuàng)作長度為10個字符(不考慮前綴長度)的一段歌詞。因為模型參數(shù)為隨機值,所以預測結果也是隨機的。
predict_rnn('分開', 10, rnn, params, init_rnn_state, num_hiddens, vocab_size,
device, idx_to_char, char_to_idx)
'分開玫東香代丘恨妥雕微蕃'
困惑度
我們通常使用困惑度(perplexity)來評價語言模型的好壞。回憶一下“softmax回歸”一節(jié)中交叉熵損失函數(shù)的定義。困惑度是對交叉熵損失函數(shù)做指數(shù)運算后得到的值。特別地,
- 最佳情況下,模型總是把標簽類別的概率預測為1,此時困惑度為1;
- 最壞情況下,模型總是把標簽類別的概率預測為0,此時困惑度為正無窮;
- 基線情況下,模型總是預測所有類別的概率都相同,此時困惑度為類別個數(shù)。
顯然,任何一個有效模型的困惑度必須小于類別個數(shù)。在本例中,困惑度必須小于詞典大小vocab_size。
定義模型訓練函數(shù)
跟之前章節(jié)的模型訓練函數(shù)相比,這里的模型訓練函數(shù)有以下幾點不同:
- 使用困惑度評價模型。
- 在迭代模型參數(shù)前裁剪梯度。
- 對時序數(shù)據(jù)采用不同采樣方法將導致隱藏狀態(tài)初始化的不同。
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()
loss = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
if not is_random_iter: # 如使用相鄰采樣,在epoch開始時初始化隱藏狀態(tài)
state = init_rnn_state(batch_size, num_hiddens, device)
l_sum, n, start = 0.0, 0, time.time()
data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
for X, Y in data_iter:
if is_random_iter: # 如使用隨機采樣,在每個小批量更新前初始化隱藏狀態(tài)
state = init_rnn_state(batch_size, num_hiddens, device)
else: # 否則需要使用detach函數(shù)從計算圖分離隱藏狀態(tài)
for s in state:
s.detach_()
# inputs是num_steps個形狀為(batch_size, vocab_size)的矩陣
inputs = to_onehot(X, vocab_size)
# outputs有num_steps個形狀為(batch_size, vocab_size)的矩陣
(outputs, state) = rnn(inputs, state, params)
# 拼接之后形狀為(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) # 因為誤差已經(jīng)取過均值,梯度不用再做平均
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))
訓練模型并創(chuàng)作歌詞
現(xiàn)在我們可以訓練模型了。首先,設置模型超參數(shù)。我們將根據(jù)前綴“分開”和“不分開”分別創(chuàng)作長度為50個字符(不考慮前綴長度)的一段歌詞。我們每過50個迭代周期便根據(jù)當前訓練的模型創(chuàng)作一段歌詞。
num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']
下面采用隨機采樣訓練模型并創(chuàng)作歌詞。
train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, True, num_epochs, num_steps, lr,
clipping_theta, batch_size, pred_period, pred_len,
prefixes)
接下來采用相鄰采樣訓練模型并創(chuàng)作歌詞。
train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, False, num_epochs, num_steps, lr,
clipping_theta, batch_size, pred_period, pred_len,
prefixes)
循環(huán)神經(jīng)網(wǎng)絡的簡介實現(xiàn)
定義模型
我們使用Pytorch中的nn.RNN來構造循環(huán)神經(jīng)網(wǎng)絡。在本節(jié)中,我們主要關注nn.RNN的以下幾個構造函數(shù)參數(shù):
-
input_size- The number of expected features in the input x -
hidden_size– The number of features in the hidden state h -
nonlinearity– The non-linearity to use. Can be either 'tanh' or 'relu'. Default: 'tanh' -
batch_first– If True, then the input and output tensors are provided as (batch_size, num_steps, input_size). Default: False
這里的batch_first決定了輸入的形狀,我們使用默認的參數(shù)False,對應的輸入形狀是 (num_steps, batch_size, input_size)。
forward函數(shù)的參數(shù)為:
-
inputof shape (num_steps, batch_size, input_size): tensor containing the features of the input sequence. -
h_0of shape (num_layers * num_directions, batch_size, hidden_size): tensor containing the initial hidden state for each element in the batch. Defaults to zero if not provided. If the RNN is bidirectional, num_directions should be 2, else it should be 1.
forward函數(shù)的返回值是:
-
outputof shape (num_steps, batch_size, num_directions * hidden_size): tensor containing the output features (h_t) from the last layer of the RNN, for each t. -
h_nof shape (num_layers * num_directions, batch_size, hidden_size): tensor containing the hidden state for t = num_steps.
現(xiàn)在我們構造一個nn.RNN實例,并用一個簡單的例子來看一下輸出的形狀。
rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=num_hiddens)
num_steps, batch_size = 35, 2
X = torch.rand(num_steps, batch_size, vocab_size)
state = None
Y, state_new = rnn_layer(X, state)
print(Y.shape, state_new.shape)
我們定義一個完整的基于循環(huán)神經(jīng)網(wǎng)絡的語言模型。
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)
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)
output = self.dense(hiddens)
return output, state
類似的,我們需要實現(xiàn)一個預測函數(shù),與前面的區(qū)別在于前向計算和初始化隱藏狀態(tài)。
def predict_rnn_pytorch(prefix, num_chars, model, vocab_size, device, idx_to_char,
char_to_idx):
state = None
output = [char_to_idx[prefix[0]]] # output記錄prefix加上預測的num_chars個字符
for t in range(num_chars + len(prefix) - 1):
X = torch.tensor([output[-1]], device=device).view(1, 1)
(Y, state) = model(X, state) # 前向計算不需要傳入模型參數(shù)
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(Y.argmax(dim=1).item())
return ''.join([idx_to_char[i] for i in output])
使用權重為隨機值的模型來預測一次。
model = RNNModel(rnn_layer, vocab_size).to(device)
predict_rnn_pytorch('分開', 10, model, vocab_size, device, idx_to_char, char_to_idx)
接下來實現(xiàn)訓練函數(shù),這里只使用了相鄰采樣。
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)
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函數(shù)從計算圖分離隱藏狀態(tài)
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))
訓練模型:
num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e-3, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']
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)