任務說明
通過用周杰倫的歌詞數據生成新的歌詞的任務,分析比較不同的語言模型。
數據集
數據集說明
數據集中的訓練集合採用的是周杰倫十張專輯中的歌詞,用此來訓練一個語言模型,並用其來生成新的歌詞。
數據集讀取
通過with open讀取數據集,並將換行符替換成空格。去除換行符時,需要同時去除’\n’和’\r’:
with open ('jaychou_lyrics.txt')as f:
# 將文件讀取爲字符串
corpus_chars = f.read()
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
print(corpus_chars)
建立字符索引
將每個字符映射成一個從0開始的連續整數作爲索引。
先把數據集中所有不同字符取出,然後將其映射到索引倆構造詞典,將訓練數據集中每個字符轉化爲索引。
# 轉化爲詞典字符的list
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)
# 將數據集終得每個字符轉化爲索引,並打印前20個字符及其對應得索引
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)
時序數據採樣
在訓練中需要每次隨機讀取小批量樣本和標籤,這裏的時序數據的一個樣本通常包含連續的字符。
將採樣後的樣本分爲樣本序列和標籤序列,標籤序列是對應樣本序列字符的下一個字符。
隨機採樣:
每次從數據裏隨機採樣一個小批量,batch_size指每個小批量的樣本數,num_steps爲每個樣本所包含的時間步數;
隨機採樣中每個樣本是原始序列上任意截取的一段序列,相鄰的兩個隨機小批量在原始序列上的位置不一定相互相鄰;
每次隨機採樣前都需要重新初始化隱藏狀態,因爲無法用一個小批量最終時間步的隱藏狀態來初始化下一個小批量的隱藏狀態;
def data_iter_random(corpus_indices, batch_size, num_steps, ctx=None):
# 減1是因爲輸出的索引x是相應輸入的索引y加1
num_examples = (len(corpus_indices) - 1) // num_steps
epoch_size = num_examples // batch_size
example_indices = list(range(num_examples))
random.shuffle(example_indices)
# 返回從pos開始的長爲num_steps的序列
def _data(pos):
return corpus_indices[pos: pos + num_steps]
for i in range(epoch_size):
# 每次讀取batch_size個隨機樣本
i = i * batch_size
batch_indices = example_indices[i: i + batch_size]
X = [_data(j * num_steps) for j in batch_indices]
Y = [_data(j * num_steps + 1) for j in batch_indices]
# yield torch.tensor(X, dtype=torch.float32, device=device), torch.tensor(Y, dtype=torch.float32, device=device)
yield nd.array(X, ctx), nd.array(Y, ctx)
相鄰採樣
相鄰採樣的目的是可以用一個小批量最終時間步的隱藏狀態來初始化下一個小批量的隱藏狀態,從而使下一個小批量的輸出也取決於當前小批量的輸入,並如此循環。
- 只需再每一個迭代週期開始時初始化隱藏狀態,無需每次重新初始化隱藏狀態;
- 多個相鄰小批量通過傳遞隱藏狀態串聯時,模型參數的梯度計算將依賴所有串聯起來的小批量序列。
def data_iter_consecutive(corpus_indices, batch_size, num_steps, ctx=None):
corpus_indices = nd.array(corpus_indices, ctx=ctx)
data_len = len(corpus_indices)
batch_len = data_len // batch_size
indices = corpus_indices[0: batch_size*batch_len].reshape((batch_size, batch_len))
epoch_size = (batch_len - 1) // num_steps
for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
評價指標
使用困惑度(perplexity)來評價語言模型的好壞,困惑度是對交叉熵損失函數做指數運算後得到的值
模型一:循環神經網絡RNN
問題一:
- 解釋爲什麼循環神經網絡可以表達某時間步的詞基於文本序列中所有過去的詞的條件概率?
- 調節循環神經網絡的超參數以及深度,分析不同超參和深度下,訓練時間、語言模型困惑度(perplexity)以及創作歌詞的結果等相關指標的變化,並以表格的形式進行總結。
# 數據預處理
def load_data_jay_lyrics():
"""Load the Jay Chou lyric data set (available in the Chinese book)."""
with zipfile.ZipFile('jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
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
'''
idx_to_char:字符詞典列表;
char_to_idx:字符詞典--key爲字符,value爲索引index
corpus_indices:字符詞典的索引
'''
(corpus_indices, char_to_idx, idx_to_char, vocab_size) = load_data_jay_lyrics()
# 構造一個含單隱藏層、隱藏單元個數爲256的循環神經網絡層rnn_layer,並對權重做初始化
num_hiddens = 256
rnn_layer = rnn.RNN(num_hiddens)
rnn_layer.initialize()
# 使用begin_state 來返回初始化的隱藏狀態列表,包括形狀爲(隱藏層,批量大小,隱藏單元個數)的元素
batch_size = 2
state = rnn_layer.begin_state(batch_size=batch_size)
print(state[0].shape) # (1, 2, 256)
num_steps = 35
X = nd.random.uniform(shape=(num_steps, batch_size, vocab_size))
Y, state_new = rnn_layer(X, state)
# 繼承block類來定義一個完整的循環神經網絡,首先將輸入數據使用one-hot向量表示後輸入道rnn_layer中,然後使用全連接輸出層得到輸出。輸出個數等於詞典大小;
class RNNModel(nn.Block):
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.dense = nn.Dense(vocab_size)
def forward(self, inputs, state):
# 將輸入轉置爲(num_steps, batch_size)後獲取one-hot向量表示
X = nd.one_hot(inputs.T, self.vocab_size)
Y, state = self.rnn(X, state)
# 全連接層會首先將Y的形狀變爲(num_steps * batch_size, num_hiddens)
output = self.dense(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, *args, **kwargs):
return self.rnn.begin_state(*args, **kwargs)
# RNN值預測
def predict_rnn_gluon(prefix, num_chars, model, vocab_size, ctx, idx_to_char,
char_to_idx):
"""Precit next chars with a Gluon RNN model"""
state = model.begin_state(batch_size=1, ctx=ctx)
output = [char_to_idx[prefix[0]]]
for t in range(num_chars + len(prefix) - 1):
X = nd.array([output[-1]], ctx=ctx).reshape((1, 1))
(Y, state) = model(X, state)
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(int(Y.argmax(axis=1).asscalar()))
return ''.join([idx_to_char[i] for i in output])
# 使用權重爲隨機值的模型進行預測
ctx = mx.cpu()
model = RNNModel(rnn_layer, vocab_size)
model.initialize(force_reinit=True, ctx=ctx)
predict_rnn_gluon('分開', 10, model, vocab_size, ctx, idx_to_char, char_to_idx)
使用相鄰採樣實現訓練函數:
def grad_clipping(params, theta, ctx):
"""Clip the gradient."""
if theta is not None:
norm = nd.array([0], ctx)
for param in params:
norm += (param.grad ** 2).sum()
norm = norm.sqrt().asscalar()
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
def train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes):
"""Train an Gluon RNN model and predict the next item in the sequence."""
loss = gloss.SoftmaxCrossEntropyLoss()
model.initialize(ctx=ctx, force_reinit=True, init=init.Normal(0.01))
trainer = gluon.Trainer(model.collect_params(), 'sgd',
{'learning_rate': lr, 'momentum': 0, 'wd': 0})
for epoch in range(num_epochs):
l_sum, n, start = 0.0, 0, time.time()
data_iter = data_iter_consecutive(
corpus_indices, batch_size, num_steps, ctx)
state = model.begin_state(batch_size=batch_size, ctx=ctx)
for X, Y in data_iter:
for s in state:
s.detach()
with autograd.record():
(output, state) = model(X, state)
y = Y.T.reshape((-1,))
l = loss(output, y).mean()
l.backward(retain_graph=True)
params = [p.data() for p in model.collect_params().values()]
grad_clipping(params, clipping_theta, ctx)
trainer.step(1)
l_sum += l.asscalar() * y.size
n += y.size
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_gluon(
prefix, pred_len, model, vocab_size, ctx, idx_to_char,
char_to_idx))
# 使用超參數來訓練模型
num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']
train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx, corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,batch_size, pred_period, pred_len, prefixes)
模型二:門控循環單元GRU
問題二:
- 請比較循環神經網絡與門控循環網絡之間結構的聯繫以及區別,並且從理論的角度描述門控循環神經網絡的出現是在嘗試解決循環神經網絡中存在的哪幾方面不足的。
- 調節門控循環單元(GRU)以及長短期記憶(LSTM)的超參數以及深度,分析不同超參和深度下,訓練時間、語言模型困惑度(perplexity)以及創作歌詞的結果等相關指標的變化,並以表格的形式進行總結。
模型三:長短期記憶LSTM
模型四:簡單循環單元SRU
問題三:
- 通過閱讀論文,試着詳細分析簡單循環單元(SRU)還在哪些方面進行了計算優化,嘗試解決循環神經網絡無法並行訓練的問題,從而大大提高了訓練速度;
- 調節簡單循環單元(SRU)的超參數以及深度,分析不同超參和深度下,訓練時間、語言模型困惑度(perplexity)以及創作歌詞的結果等相關指標的變化,並以表格的形式進行總結,並與之前的網絡結構進行對比分析。