循环神经网络
第八章 循环神经网络
8.1 序列模型
1.
很多数据具有时序结构
2.
统计工具
- 在时间t观察到$x_t$,得到T个不独立的随机变量($x_1,…x_T$)~p(x)
- p(x)=p(x1)p(x2|x1)p(x3|x1,x2)…p(xT|x1,…xT-1)
- p(x)=p(xT)p(xT-1|xT)p(xT-2|xT-1,xT-2)…p(x1|x1,…xT-1)
- 对条件概率建模:p(xt|x1…xt-1)=p(xt|f(x1…xt-1)),对见过的数据建模,也称自回归模型
3.
方案A 马尔可夫假设 - 假设当前数据只和$\tau$个过去数据点相关
- p(xt|x1…xt-1)=p(xt|xt-$\tau$…xt-1)=p(xt|f(xt-$\tau$…xt-1)),例如在过去数据上训练MLP模型
- $\tau$=1时,得到一阶马尔可夫模型, p(x)=$\Pi$p(xt|xt-1)
4.
方案B 潜变量模型 - 潜变量ht=f(x1…xt-1)->xt=p(xt|ht)
8.2 文本预处理
1 | import collections |
将数据集读取到由文本行组成的列表中
1 | d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt', |
text lines: 3221
the time machine by h g wells
twinkled and his usually pale face was flushed and animated the
每个文本序列被拆分成一个标记列表
文本行列表lines-文本序列line-词元列表tokens-词元token1
2
3
4
5
6
7
8
9
10
11
12def tokenize(lines, token='word'):
"""将文本行拆分为单词或字符标记。"""
if token == 'word':
return [line.split() for line in lines]
elif token == 'char':
return [list(line) for line in lines]
else:
print('错误:未知令牌类型:' + token)
tokens = tokenize(lines)
for i in range(11):
print(tokens[i])
[‘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’]
[‘twinkled’, ‘and’, ‘his’, ‘usually’, ‘pale’, ‘face’, ‘was’, ‘flushed’, ‘and’, ‘animated’, ‘the’]
构建一个字典,通常也叫做词表(vocabulary),用来将字符串标记映射到从0开始的数字索引中
1 | class Vocab: |
构建词汇表
1 | vocab = Vocab(tokens) |
[(‘unk’, 0), (‘the’, 1), (‘i’, 2), (‘and’, 3), (‘of’, 4), (‘a’, 5), (‘to’, 6), (‘was’, 7), (‘in’, 8), (‘that’, 9)]
将每一行文本转换成一个数字索引列表
1 | for i in [0, 10]: |
words: [‘the’, ‘time’, ‘machine’, ‘by’, ‘h’, ‘g’, ‘wells’]
indices: [1, 19, 50, 40, 2183, 2184, 400]
words: [‘twinkled’, ‘and’, ‘his’, ‘usually’, ‘pale’, ‘face’, ‘was’, ‘flushed’, ‘and’, ‘animated’, ‘the’]
indices: [2186, 3, 25, 1044, 362, 113, 7, 1421, 3, 1045, 1]
将所有内容打包到load_corpus_time_machine
函数中
1 | def load_corpus_time_machine(max_tokens=-1): |
(170580, 28) 28=16+ukn+空格
8.3 语言模型
1.
给定文本序列x1-xT,语言模型的目标是估计联合概率p(x1-xT)
2.
使用计数来建模
- 假设序列长度为2,$p(x,x’)=p(x)p(x’|x)=\frac{n(x)}{n}\frac{n(x,x’)}{n(x)}$,n是语料库中的总词数,n(x),n(x,x’)是单个单词和连续单词对的出现次数
3.
N元语法 - 当序列很长时,因为文本量不够大,很可能n(x1-xT)<=1
- 使用马尔可夫假设
- 一元语法:$p(x_1,x_2,x_3,x_4)=p(x_1)p(x_2)p(x_3)p(x_4)=\frac{n(x_1)}{n}\frac{n(x_2)}{n}\frac{n(x_3)}{n}\frac{n(x_4)}{n}$
- 一元语法:$p(x_1,x_2,x_3,x_4)=p(x_1)p(x_2|x_1)p(x_3|x_2)p(x_4|x_3)=\frac{n(x_1)}{n}\frac{n(x_1,x_2)}{x_1}\frac{n(x_2,x_3)}{x_2}\frac{n(x_3,x_4)}{x_3}$
4.
代码
1.
F1:随机地生成一个小批量数据的特征和标签以供读取。 在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列2.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def seq_data_iter_random(corpus, batch_size, num_steps'''tau'''):
"""使用随机抽样生成一个小批量子序列。"""
'''不同于8.1中的遍历每个都需要用很多次,这个是把总长切成n份,然后一个epoch就n份都过一遍。每个epoch开始的起点都是从0~tau中随机取,这样份就不会重复'''
corpus = corpus[random.randint(0, num_steps - 1):]
num_subseqs = (len(corpus) - 1) // num_steps
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))'''每个子序列开始的下标'''
random.shuffle(initial_indices)
def data(pos):
return corpus[pos:pos + num_steps]
num_batches = num_subseqs // batch_size'''在n段里面取batch'''
for i in range(0, batch_size * num_batches, batch_size):
initial_indices_per_batch = initial_indices[i:i + batch_size]'''取了batch size个开始的下标'''
X = [data(j) for j in initial_indices_per_batch]'''生成batch size个段'''
Y = [data(j + 1) for j in initial_indices_per_batch]
yield torch.tensor(X), torch.tensor(Y)
F2:保证两个相邻的小批量中的子序列在原始序列上也是相邻的
8.4 循环神经网络
1.
更新隐藏状态:$ht=\phi(W{hh}h{t-1}+W{hx}x{t-1}+b_h)$ (去掉$W{hh}h{t-1}$就是MLP)
$o_t=W{ho}h_{t}+b_o$
2.
困惑度 perplexity
- 衡量一个语言模型的好坏可以用平均交叉熵$\pi=\frac{1}{n}\sum{i=1}^{n}-logp(x_t|x{t-1},…)$ 其中p是语言模型预测的概率,xt是真实词
- NLP使用困惑度exp(pi)来衡量,1表示完美,无穷大是最差情况
3.
梯度裁剪 - 迭代中计算T个时间步上的梯度,在反向传播中产生长度为O(T)的矩阵乘法链,导致数值不稳定
- 梯度裁剪能有效预防梯度爆炸
- 如果梯度长度超过$\theta$,那么将拖回$\theta$:$g\leftarrow min(1,\frac{\theta}{\lVert g\rVert})g$
4.
其他应用
5.
由于在当前时间步中, 隐状态使用的定义与前一个时间步中使用的定义相同, 因此 $ht=\phi(W{hh}h{t-1}+W{hx}x_{t-1}+b_h)$的计算是循环的
- 如果梯度长度超过$\theta$,那么将拖回$\theta$:$g\leftarrow min(1,\frac{\theta}{\lVert g\rVert})g$
8.5 循环神经网络的从零开始实现
1 | %matplotlib inline |
独热编码
给一个下标,用向量来表示
将每个索引映射为相互不同的单位向量: 假设词表中不同词元的数目为N(即len(vocab)
), 词元索引的范围为0到N−1。 如果词元的索引是整数i, 那么我们将创建一个长度为N的全0向量, 并将第i处的元素设置为1。 此向量是原始词元的一个独热向量1
F.one_hot(torch.tensor([0, 2]), len(vocab))
tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0]])
小批量形状是(批量大小, 时间步数)
转置的目的:使我们能够更方便地通过最外层的维度, 一步一步地更新小批量数据的隐状态1
2X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape
torch.Size([5, 2, 28])
初始化循环神经网络模型的模型参数
1 | def get_params(vocab_size, num_hiddens, device): |
一个init_rnn_state
函数在初始化时返回隐藏状态
1 | def init_rnn_state(batch_size, num_hiddens, device): |
下面的rnn
函数定义了如何在一个时间步计算隐藏状态和输出
1 | def rnn(inputs, state, params): |
创建一个类来包装这些函数
1 | class RNNModelScratch: |
检查输出是否具有正确的形状
1 | num_hiddens = 512 |
(torch.Size([10, 28]), 1, torch.Size([2, 512]))
首先定义预测函数来生成用户提供的prefix
之后的新字符
1 | def predict_ch8(prefix, num_preds, net, vocab, device): |
梯度裁剪
1 | def grad_clipping(net, theta): |
定义一个函数来训练只有一个迭代周期的模型
1 | def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): |
训练函数支持从零开始或使用高级API实现的循环神经网络模型
1 | def train_ch8(net, train_iter, vocab, lr, num_epochs, device, |
8.6 循环神经网络的简洁实现
第九章 现代循环神经网络
9.1 门控循环单元GRU
1.
不是所有观察值同等重要->只记住相关的观察需要
- 能关注的机制:更新门
- 能遗忘的机制:重置门
2.
门
$\sigma$是sigmoid
候选隐藏状态
Rt可以学习,sigmoid介于0-1之间,0就忘了过去的Ht,只和Xt有关,1就过去的Ht全留着,和正常的隐藏状态一样
Zt可以学习,sigmoid介于0-1之间,0就拿来候选Ht(调整过的,用了
Xt),1就直接用上一个Ht,和新输入Xt没有关系
3.
门控循环单元具有以下两个显著特征: - 重置门有助于捕获序列中的短期依赖关系
- 更新门有助于捕获序列中的长期依赖关系
9.2 长短期记忆网络LSTM
1.
- 忘记门:将值朝0减少
- 输入门:决定是否忽略掉输入数据
- 输出门:决定是不是使用隐状态
2.
只要输出门接近1,我们就能够有效地将所有记忆信息传递给预测部分, 而对于输出门接近0,我们只保留记忆元内的所有信息,而不需要更新隐状态。(只有隐状态才会传递到输出层, 而记忆元Ct不直接参与输出计算)
9.3 深度循环神经网络
9.4 双向循环神经网络
1.
RNN只看过去,但我们也可以看未来(完形填空)
2.
总结:
- 双向循环神经网络通过反向更新的隐藏层来利用反向时间信息
- 通常用来对序列抽取特征、填空,而不是预测未来(推理)
9.5 机器翻译与数据集
- 下载和预处理数据集
- 几个预处理步骤
- 词元化
- 词汇表
- 序列样本都有一个固定的长度 截断或填充文本序列
- 转换成小批量数据集用于训练
- 训练模型
9.6 编码器-解码器架构
1.
CNN
- 编码器:将输入编程成中间表达形式(特征)
- 解码器:将中间表示解码输出
2.
RNN- 编码器:将文本表示成向量
- 解码器:向量表示成输出
3.
架构- 编码器处理输入
- 解码器处理输出
9.7 序列到序列学习seq2seq
1.
seq2seq
- 编码器是一个RNN,读取输入句子(可以是双向)
- 解码器使用另外一个RNN来输出
2.
细节 - 编码器是没有输出的RNN
- 编码器最后时间步的隐藏状态用作解码器的初始隐藏状态
3.
训练
训练时解码器使用目标句子作为输入
4.
衡量生成的好坏:BLEU- pn是预测中所有n-gram的精度
- e.g.:标签序列A B C D E F 和预测序列A B B C D
- p1=4/5,预测序列中五个只有第二个B没有出现
- p2=3/4,预测序列中四个2元,BB没出现
- p3=1/3,p4=0
- BLEU:
- $exp(min(0,1-\frac{len{label}}{len{pred}}))\prod{n=1}^{k}p{n}^{1/2^{n}}$
- if len label> len pred,后一项为负,那么exp负数就变成很小的数了,BLEU越大越好,最大就是exp0=1,所以min用来惩罚过短的预测
- 后面的连乘 pn<1所以n越大这个乘积越大,即长匹配有高权重
9.8 束搜索
1.
贪心搜索
- 在seq2seq中使用了贪心搜索来预测序列,即将当前时刻预测概率最大的词输出
- 但贪心并不一定最优
- e.g.
2.
穷举搜索 - 最优算法:对所有可能的序列,计算概率,选取最好的
- 但计算上不可行
3.
束搜索 - 保存最好的k个候选
- 在每个时刻,对每个候选新加一项(n种可能),在kn中选出最好的k个
- e.g.
- 长句子的概率越低,为了避免每次都只找短的,所以前面乘了一个东西,L是长度,越长,分母越大,这个玩意儿越小,而log出来是负数,所以整体值越大,相当于对长句子做了补偿