语言模型
语言模型将计算该序列的概率:
语言模型可用于提升语音识别和机器翻译的性能。
语言模型的计算:
n元语法:马尔可夫假设是指一个词的出现只与前面n个词相关,即n阶马尔可夫链(Markov chain of order n)。
循环神经网络
不含隐藏状态的神经网络,
含隐藏状态的循环神经网络,
时间步t的隐藏变量的计算由当前时间步的输入和上一时间步的隐藏变量共同决定:
与多层感知机相比,
from mxnet import nd
X, W_xh = nd.random.normal(shape=(3, 1)), nd.random.normal(shape=(1, 4))
H, W_hh = nd.random.normal(shape=(3, 4)), nd.random.normal(shape=(4, 4))
nd.dot(X, W_xh) + nd.dot(H, W_hh)
nd.dot(nd.concat(X, H, dim=1), nd.concat(W_xh, W_hh, dim=0))
应用:基于字符级循环神经网络的语言模型
语言模型数据集(周杰伦专辑歌词)
将介绍如何预处理一个语言模型数据集,并将其转换成字符级循环网络所需要的输入格式。
语料https://github.com/d2l-ai/d2l-zh/blob/master/data/jaychou_lyrics.txt.zip
这个数据集有6万多个字符。为了打印方便,我们把换行符替换成空格,然后仅使用前1万个字符来训练模型。
from mxnet import nd
import random
import zipfile
with zipfile.ZipFile('./data/jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
print(corpus_chars[:40])
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[:10000]
建立字符索引,将每个字符映射成一个从0开始的连续整数,又称索引。
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)
print(vocab_size)
将训练数据集中每个字符转化称索引,并打印前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)
时序数据的采样,需要每次随机读取小批量样本和标签。
时序数据的一个样本通常包含连续的字符。假设时间步数为5,样本序列为5个字符,即“想”“要”“有”“直”“升”。该样本的标签序列为这些字符分别在训练集中的下一个字符,即“要”“有”“直”“升”“机”。我们有两种方式对时序数据进行采样,分别是随机采样和相邻采样。
随机采样,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相邻。因此,我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。
# 随机采样
def data_iter_random(corpus_indices, batch_size, num_steps, ctx=None):
# 减1是因为输出的索引是相应输入的索引加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 nd.array(X, ctx), nd.array(Y, ctx)
my_seq = list(range(30))
for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY: ', Y, '\n')
相邻采样,
# 相邻采样
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
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY: ', Y, '\n')
RNN的实现
基于字符级RNN的语言模型,训练一个模型来进行歌词创作。
RNN中容易出现梯度衰减或梯度爆炸,为此我们可以裁剪梯度(clip gradient)。假设我们把所有模型参数梯度的元素拼接成一个向量g,并设裁剪的阈值是θ。以下是L2范数不超过θ
def grad_clipping(params, theta, ctx):
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
困惑度(perplexity)来评价语言模型的好坏。对交叉熵损失函数做指数运算后得到的值。特别地,
- 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
- 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷。
- 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。
显然,任何一个有效模型的困惑度必须小于类别个数。在本例中,困惑度必须小于词典大小vocab_size。
时间反向传播,按时间步展开,从而得到模型变量和参数之间的依赖关系,并依据链式法则应用反向传播计算并存储梯度。
L称为有关给定时间步的数据样本的目标函数。
门控循环单元(GRU)
为了更好地捕捉时间序列中时间步距离较大的依赖关系,它引入了重置门和更新门的概念
- 重置门有助于捕捉时间序列里短期的依赖关系;
- 更新门有助于捕捉时间序列里长期的依赖关系。
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = nd.sigmoid(nd.dot(X, W_xz) + nd.dot(H, W_hz) + b_z)
R = nd.sigmoid(nd.dot(X, W_xr) + nd.dot(H, W_hr) + b_r)
H_tilda = nd.tanh(nd.dot(X, W_xh) + nd.dot(R * H, W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = nd.dot(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
长短期记忆(LSTM)
增加了与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一个特殊的隐藏状态),从而记录额外的信息。
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = nd.sigmoid(nd.dot(X, W_xi) + nd.dot(H, W_hi) + b_i)
F = nd.sigmoid(nd.dot(X, W_xf) + nd.dot(H, W_hf) + b_f)
O = nd.sigmoid(nd.dot(X, W_xo) + nd.dot(H, W_ho) + b_o)
C_tilda = nd.tanh(nd.dot(X, W_xc) + nd.dot(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * C.tanh()
Y = nd.dot(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
深度循环神经网络
在深度学习应用里,我们通常会用到含有多个隐藏层的循环神经网络,也称作深度循环神经网络。
双向循环神经网络
有时候,当前时间步也可能由后面时间步决定。