6.1语言模型
语言模型(language model)是自然语言处理的重要技术。自然语言处理中最常见的数据是文本数据。我们可以把一段自然语言文本看作一段离散的时间序列。
P(w2∣w1) 可以计算为w1,w2两词相邻的频率与w1词频的比值,因为该比值即P(w1,w2)与P(w1)之比
P(w3∣w1,w2)同理可以计算为w1、w2和w3这3个词相邻的频率与w1和w2这2个词相邻的频率的比值。
基于n−1阶马尔可夫链,我们可以将语言模型改写为:
P(w1,w2,…,wT)≈t=1∏TP(wt∣wt−(n−1),…,wt−1). 以上也叫n元语法(n-grams),当n分别为1、2和3时,我们将其分别称作一元语法(unigram)、二元语法(bigram)和三元语法(trigram),长度为4的序列w1,w2,w3,w4在一元语法、二元语法和三元语法中的概率分别为:
P(w1,w2,w3,w4)P(w1,w2,w3,w4)P(w1,w2,w3,w4)=P(w1)P(w2)P(w3)P(w4),=P(w1)P(w2∣w1)P(w3∣w2)P(w4∣w3),=P(w1)P(w2∣w1)P(w3∣w1,w2)P(w4∣w2,w3). 练习
假设训练数据集中有10万个词,四元语法需要存储多少词频和多词相邻频率?
答:
10万个词一共需要存储 p(w1)…p(w100000) 共10万个词频;
一共需要存储
p(w1,w2)…p(w99999,w100000) 共10万-1个 二词相邻频率 p(w1,w2,w3)…p(w99998,w99999,w100000) 共10万-2个 三词相邻频率 p(w1,w2,w3,w4)…p(w99997,w99998,w99999,w100000) 共10万-3个 四词相邻频率
你还能想到哪些语言模型的应用?
答:论文翻译,推荐系统。
6.2循环神经网络
含隐藏状态(受Ht-1影响)的循环神经网络:
练习
如果使用循环神经网络来预测一段文本序列的下一个词,输出个数应该设为多少?
答:输入等于输出
为什么循环神经网络可以表达某时间步的词基于文本序列中所有过去的词的条件概率?
答:因为时间步t的隐藏变量H_t,一直传递给了下一个预测。
6.3语言模型数据集及处理
随机采样:
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')
——————————————————————输出————————————————————————
X:
[[ 0. 1. 2. 3. 4. 5.]
[18. 19. 20. 21. 22. 23.]]
<NDArray 2x6 @cpu(0)>
Y:
[[ 1. 2. 3. 4. 5. 6.]
[19. 20. 21. 22. 23. 24.]]
<NDArray 2x6 @cpu(0)>
X:
[[ 6. 7. 8. 9. 10. 11.]
[12. 13. 14. 15. 16. 17.]]
<NDArray 2x6 @cpu(0)>
Y:
[[ 7. 8. 9. 10. 11. 12.]
[13. 14. 15. 16. 17. 18.]]
<NDArray 2x6 @cpu(0)>
相邻采样:
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y, '\n')
——————————————————————输出————————————————————————
X:
[[ 0. 1. 2. 3. 4. 5.]
[15. 16. 17. 18. 19. 20.]]
<NDArray 2x6 @cpu(0)>
Y:
[[ 1. 2. 3. 4. 5. 6.]
[16. 17. 18. 19. 20. 21.]]
<NDArray 2x6 @cpu(0)>
X:
[[ 6. 7. 8. 9. 10. 11.]
[21. 22. 23. 24. 25. 26.]]
<NDArray 2x6 @cpu(0)>
Y:
[[ 7. 8. 9. 10. 11. 12.]
[22. 23. 24. 25. 26. 27.]]
<NDArray 2x6 @cpu(0)>
练习
你还能想到哪些采样小批量时序数据的方法?
答:倒序,按断句。
如果我们希望一个序列样本是一个完整的句子,这会给小批量采样带来什么样的问题?
答:会导致每次的批量大小不固定。
6.4循环神经网络完整实现
应用:可以用基于字符级循环神经网络的语言模型来生成文本序列,例如创作歌词。
one-hot向量:就是标签向量,只有0和1代表有没有。
超参数:num_hiddens
激活函数:当元素在实数域上均匀分布时,tanh函数值的均值为0。实际应用中比sigmoid好一点。
预测函数:基于前缀prefix
(含有数个字符的字符串)来预测接下来的num_chars
个字符。
裁剪梯度(clip gradient):假设我们把所有模型参数梯度的元素拼接成一个向量 g,并设裁剪的阈值是θ。裁剪后的梯度的L2范数不超过θ。当训练循环神经网络时,为了应对梯度爆炸,可以裁剪梯度。
困惑度(perplexity):来评价语言模型的好坏。困惑越低,越像是正常句子。任何一个有效模型的困惑度必须小于类别个数。困惑度是对交叉熵损失函数做指数运算后得到的值。
循环神经网络的模型训练函数的不同之处:
对时序数据采用不同采样方法将导致隐藏状态初始化的不同。
练习
调调超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。
答:num_hiddens提高到512,有助于更快收敛。batch_size降低,耗时但有助于收敛。lr改变为e大小使得学习不收敛。
不裁剪梯度,运行本节中的代码,结果会怎样?
答:OverflowError: math range errorclipping_theta
将pred_period
变量设为1,观察未充分训练的模型(困惑度高)是如何创作歌词的。你获得了什么启发?
答:困惑度高的模型都是使用重复的,经常出现的词来作为结果。
将相邻采样改为不从计算图分离隐藏状态,运行时间有没有变化?
答:没有。
将本节中使用的激活函数替换成ReLU,重复本节的实验。
答:感觉效果更好,困惑度的下降的更为合理。
——————————————————————————————随机取样——————————————————————————
epoch 50, perplexity 72.627812, time 0.31 sec
- 分开 我不要再想你 哼哼哈觉截棍 哼哼哈兮截棍 哼哼哈兮截棍 哼哼哈兮截棍 哼哼哈兮截棍 哼哼哈兮截棍
- 不分开 我想想你的溪边河知都的 我想要再想你 哼哼哈觉截棍 哼哼哈兮截棍 哼哼哈兮截棍 哼哼哈兮截棍 哼哼
epoch 100, perplexity 12.504797, time 0.31 sec
- 分开 一直在双截棍 哼哼哈兮 快使用双截棍 哼者哈兮 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快
- 不分开不 我不能让呵牵 如果我遇见你是一场悲剧 我想以让生小就定一个人 干什么让我习糗处可躲就耳我想要你说
epoch 150, perplexity 3.392714, time 0.31 sec
- 分开 有杰伦 一步两步三步四步望著天 看星星 一颗两颗三颗四步望著天 看星星 一颗两颗三颗四颗望著天 看
- 不分开吗 我后你很 在颗心悬在半动 我默悔够远照看 就像是童话故事 就么忙跟武当山 你说林苦武一堡 你说在
epoch 200, perplexity 1.976689, time 0.31 sec
- 分开 一直令它心仪的母斑鸠 牛仔红蕃 在小镇 背在背决斗 一只灰狼 问候村日 一场日痛 你的完空 在小
- 不分开吗 我叫你爸 你打我妈 这样对吗干嘛这样 何必让酒牵鼻子走 瞎 说么一口 你爱完我 说你说 干数怎么
epoch 250, perplexity 1.595479, time 0.31 sec
- 分开 有杰伦经三 谁慢苦习 让我爱上你 那场悲剧 是你完美演出的一场戏 宁愿心碎哭泣 再狠狠忘记 你爱过
- 不分开扫把的胖女巫 用拉丁文念咒语啦啦呜 她养的黑猫笑起来像哭 啦啦啦呜 刻在心动 染底夜空 过去种种 象
6.5循环神经网络简洁实现
简洁实现版,速度更快。
训练模型。定义预测函数。使用模型训练预测函数中的参数。
练习
与上一节的实现进行比较。看看Gluon的实现是不是运行速度更快?如果你觉得差别明显,试着找找原因。
答:从0.3s到0.05s,可能原因:1.前向计算使用了全连接层而不是向量运算。2.使用了d2l的梯度计算。3.参数的获取方式不同。
6.6通过时间反向传播
时间步数为3的循环神经网络模型计算中的依赖关系。
方框代表变量(无阴影)或参数(有阴影),圆圈代表运算符。
x输入,w权重,h时间步,o输出,L损失函数,y实际结果。
上式中的指数项可见,当时间步数T较大或者时间步t较小时,目标函数有关隐藏状态的梯度较容易出现衰减和爆炸。所以才需要用以下方式梯度裁剪。
练习
除了梯度裁剪,你还能想到别的什么方法应对循环神经网络中的梯度爆炸?
答:1. 使用 ReLU、LReLU、ELU、maxout 等激活函数。sigmoid函数的梯度随着x的增大或减小和消失,而ReLU不会。2.使用批量归一化。通过规范化操作将输出信号xx规范化到均值为0,方差为1保证网络的稳定性.
6.7GRU门控循环单元
门控循环神经网络(gated recurrent neural network)是为了更好地捕捉时间序列中时间步距离较大的依赖关系。通过可以学习的门来控制信息的流动。
⊙按元素乘法,σ是sigmoid函数,R是重置门,Z是更新门。
重置门Rt和更新门Zt中每个元素的值域都是[0,1]
重置门(reset gate):重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态。重置门可以用来丢弃与预测无关的历史信息。有助于捕捉时间序列里短期的依赖关系
更新门(update gate):更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新。这个设计可以应对循环神经网络中的梯度衰减问题。有助于捕捉时间序列里长期的依赖关系。
练习
假设时间步t′<t。如果只希望用时间步t′的输入来预测时间步t的输出,每个时间步的重置门和更新门的理想的值是多少?
答:重置门Rt=0;更新门Zt=1;能够使得为H_tilda无法生效。
调节超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。
答:num_steps从32改为40,困惑度提高,结果更差,运行时间略微降低。
改为16,困惑度降低,结果更好,运行时间接近翻倍。
batch_size提高和num_steps降低效果类似。
学习率改为1e3的话,前面不收敛,后面才收敛。1e的话收敛特别慢。
num_hiddens翻倍,时间也接近翻倍,但效果基本不变。
在相同条件下,比较门控循环单元和不带门控的循环神经网络的运行时间。
答:门控的慢一点。完整实现的0.7s和0.3s;简洁实现的0.07s和0.05s.
6.8LSTM长短期记忆
长短期记忆(long short-term memory,LSTM)引入了3个门,即输入门(input gate)、遗忘门(forget gate)和输出门(output gate)这3个门元素的值域均为[0,1]。
输入门(input gate):It,来选择取多少C_tilda,也就是正常的循环神经网络的单元。
遗忘门(forget gate):Ft,来选择取多少Ct-1,也就是上一个的记忆细胞。
输出门(output gate):Ot,来选择取多少Ct的激活后的值。
时间步Ht:对下一个循环神经网络单元的影响。
练习
调节超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。
答:感觉跟6.7类似。
在相同条件下,比较长短期记忆、门控循环单元和不带门控的循环神经网络的运行时间。
答:长短期记忆 >(略大于)门控循环单元 > 不带门控的循环神经网络。从计算量很容易可以看出。
既然候选记忆细胞已通过使用tanh函数确保值域在-1到1之间,为什么隐藏状态还需要再次使用tanh函数来确保输出值域在-1到1之间?
答:因为候选记忆细胞只使用了t-1时间步和输入X。下一个公式使用了输入门和遗忘门来生成记忆细胞,所以范围可能又超过了-1到1之间。
6.9深度循环神经网络
隐藏层个数L和隐藏单元个数h都是超参数
将隐藏状态的计算换成门控循环单元或者长短期记忆的计算,我们可以得到深度门控循环神经网络。
练习
将“循环神经网络的从零开始实现”一节中的模型改为含有2个隐藏层的循环神经网络。观察并分析实验现象。
答:修改代码如下。运行结果时间更久了,迷惑度一开始略高于一个隐藏层的。
# 定义参数形状
def get_params():
def _one(shape):
return nd.random.normal(scale=0.01, shape=shape, ctx=ctx)
# 隐藏层参数
W_xh_1 = _one((num_inputs, num_hiddens))
W_hh_1 = _one((num_hiddens, num_hiddens))
b_h_1 = nd.zeros(num_hiddens, ctx=ctx)
# 注意这里的shape和前面不同
W_xh_2 = _one((num_hiddens, num_hiddens))
W_hh_2 = _one((num_hiddens, num_hiddens))
b_h_2 = nd.zeros(num_hiddens, ctx=ctx)
# 输出层参数
W_hq = _one((num_hiddens, num_outputs))
b_q = nd.zeros(num_outputs, ctx=ctx)
# 附上梯度
params = [W_xh_1, W_hh_1, b_h_1, W_xh_2, W_hh_2, b_h_2, W_hq, b_q]
for param in params:
param.attach_grad()
return params
# 返回初始化的隐藏状态
def init_rnn_state(batch_size, num_hiddens, ctx):
return (nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx),
nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx))
# 循环神经网络模型,tanh激活函数
def rnn(inputs, state, params):
# inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
W_xh_1, W_hh_1, b_h_1, W_xh_2, W_hh_2, b_h_2, W_hq, b_q = params
H_1,H_2 = state
outputs = []
for X in inputs:
H_1 = nd.relu(nd.dot(X, W_xh_1) + nd.dot(H_1, W_hh_1) + b_h_1)
H_2 = nd.relu(nd.dot(H_1, W_xh_2) + nd.dot(H_2, W_hh_2) + b_h_2)
Y = nd.dot(H_2, W_hq) + b_q
outputs.append(Y)
return outputs, (H_1,H_2)
6.10双向循环神经网络
公式:
模型参数形状:
然后我们连结两个方向的隐藏状态H_t,再计算输出层。
练习
如果不同方向上使用不同的隐藏单元个数,Ht的形状会发生怎样的改变?
参考图6.11和图6.12,设计含多个隐藏层的双向循环神经网络。