2021SC@SDUSC
在上一篇博客中,我们介绍了RNN模型和代码中的Encoder类,Encoder-Decoder并不是一个具体的模型,而是一类框架。Encoder和Decoder部分可以是任意的文字,语音,图像,视频数据,模型可以采用CNN,RNN,BiRNN、LSTM、GRU等等。所以基于Encoder-Decoder,可以设计出各种各样的应用算法。我们研究的论文基于LSTM实现了编码-解码模型,并以Encoder-Decoder框架为基础实现了Seq2Seq。
一、LSTM框架
我们首先分析LSTM框架。LSTM(long short-term memory,长短时记忆结构)是传统RNN的变体,它可以有效捕捉长序列之间的语意关联,缓解梯度消失/爆炸现象。
传统RNN内部结构简单,对计算资源要求低,参数很少,在短序列任务上性能和效果都表现优异。但传统RNN在解决长序列之间的关联时候,通过实践被证明表现很差,在进行反向传播石,过长的序列导致梯度计算异常,发生梯度消失或爆炸。如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败。梯度爆炸所带来的梯度过大, 大幅度更新网络参数,在极端情况下,结果会溢出(NaN值)。
LSTM的核心结构可以分为四个部分去解析:
- 遗忘门
- 输入门
- 细胞状态
- 输出门
图1:LSTM内部结构图
我们先来分析遗忘门:
图2:遗忘门结构图与计算公式
与传统的RNN内部结构计算非常类似,首先将当前时间步输入与上一时间步隐含状态
进行拼接,得到
,然后通过一个全连接层作变换,最后通过sigmoid函数进行激活得到
,我们可以将
看作是门值。遗忘门门值作用于上一层的细胞状态上,代表遗忘过去多少信息,由于遗忘门的门值是由
计算得到的,整个公式则意味着:根据当前时间步输入和上一时间步隐含状态
来决定遗忘多少上一层的细胞状态所写的过往信息。
激活函数sigmoid用于帮助调节流经网络的值,sigmoid函数将值压缩在0和1之间。
再看输入门的结构图和计算公式:
图3:输入门结构图与计算公式
输入门的计算公式有两个,第一个公式产生输入门门值,它和遗忘门的公式几乎相同,这个公式意味着输入信息有多少需要进行过滤。输入门的第二个公式与传统RNN内部结构计算相同。对于 LSTM来说,它得到当前细胞状态,而不是像经典RNN一样得到隐含状态。
细胞状态更新图与计算公式如下:
图4:细胞状态更新结构图与计算公式
这里没有全连接层,只是将刚刚得到的遗忘门门值与上一时间步得到的相乘,再加上输入门门值与当前时间步得到的未更新的
相乘的结果,最终得到更新后的
作为下一时间步输入的一部分。整个状态更新过程就是对遗忘门和输入门的应用。
最后是输出门:
图5:输出门结构图与计算公式
输出门的公式也是两个,第一个是计算输出门门值,和遗忘门、输入门计算方式相同,第二个是使用这个门值产生隐含状态,它将作用在细胞状态
上,并做tanh激活,最终得到
作为下一时间步输入的一部分。整个输入门的过程,就是为了产生隐含状态
。
接下来我们讨论Bi-LSTM,Bi-LSTM即双向LSTM,它并没有改变LSTM内部的结构,只是将LSTM应用两次且方向不同,再将两次得到的LSTM结果进行拼接作为最终输出。这种结构能捕捉语言语法中一些特定的前置或后置特征,增强语意关联,但是模型的参数和计算复杂度也增加了一倍。
二、LSTM使用实例
在torch.nn包中,可以通过torch.nn.LSTM调用
# 导入若干工具包
import torch
import torch.nn as nn
# 实例化LSTM对象
# 第一个参数:input_size(输入张量x的维度)
# 第二个参数:hidden_size(隐藏层维度,隐藏层神经元数量)
# 第三个参数:num_layers(隐藏层的层数)
lstm = nn.LSTM(5,6,2)
# 设定输入的张量x
# 第一个参数:sequence_length(输入序列的长度)
# 第二个参数:batch_size(批次的样本数)
# 第三个参数:input_size(输入张量x的维度)
input1=torch.randn(1,3,5)
# 初始化隐藏层张量h0,和细胞状态c0
# 第一个参数:num_layers*num_directions(层数*网络层方向数)
# 第二个参数:batch_size(批次的样本数)
# 第三个参数:hidden_size(隐藏层维度)
h0=torch.randn(2,3,6)
c0=torch.randn(2,3,6)
#input1,h0,c0输入lstm中,得到输出张量结果
output,(hn,cn)=lstm(input1,(h0,c0))
print(output)
print(output.shape)
print(hn)
print(hn.shape)
print(cn)
print(cn.shape)
输出结果如下 :
这里指定了输入张量维度为5,隐藏层维度为6,层数为2,输入的批次样本数为3,序列长度为1。,
由随机初始化得到,最后得到
,
,
。
三、嵌入层的理解
在上一篇博客中,我们并未对嵌入层及相关参数详细说明,这里我们进行简述。首先需要从one-hot独热编码说起,由于计算机只能处理矩阵和数字,而无法直接处理文档中经过划分得到的tokens,所以最自然的想法就是对tokens进行编码得到词向量。对于one-hot编码,可以简单理解成一句话的总字数为向量长度n,对应的字出现时编码1,其他编码为0。
比如,有一句话“这是一个例子用来说明”,其分别对应“0-9”,如下:
这 是 一 个 例 子 用 来 说 明
0 1 2 3 4 5 6 7 8 9
那么“这是一个例子用来说明”的one-hot编码为:
[[1 0 0 0 0 0 0 0 0 0]
[0 1 0 0 0 0 0 0 0 0]
[0 0 1 0 0 0 0 0 0 0]
[0 0 0 1 0 0 0 0 0 0]
[0 0 0 0 1 0 0 0 0 0]
[0 0 0 0 0 1 0 0 0 0]
[0 0 0 0 0 0 1 0 0 0]
[0 0 0 0 0 0 0 1 0 0]
[0 0 0 0 0 0 0 0 1 0]
[0 0 0 0 0 0 0 0 0 1]]
one-hot编码用稀疏矩阵处理语句,计算方便快捷、表达能力强。 但过于稀疏时,过度占用资源。于是需要用Embedding层对数据降维,可以简单理解为Embedding层是在one-hot独热编码后得到的编码矩阵再与相容(可相乘)的矩阵相乘,使得矩阵的维度降低。
更具体地,embedding层把稀疏矩阵,通过一些线性变换(在CNN中用全连接层进行转换,也称为查表操作),变成了一个密集矩阵,这个密集矩阵用了N(比输入层张量维度小的多)个特征来表征所有的文字,在这个密集矩阵中,蕴含了大量的字与字之间,词与词之间甚至句子与句子之间的内在关系。他们之间的关系,用的是嵌入层学习来的参数进行表征。从稀疏矩阵到密集矩阵的过程,叫做embedding,它们之间有一一映射关系。
更重要的是,这种关系在反向传播的过程中,一直在更新,多次更新后可以正确地表达整个语义以及各个语句之间的关系。最后得到embedding层的所有权重参数。Embedding把独立的向就关联起来。现在用的Embedding大多都是采用预训练好的Embedding模型来做,例如word2vec和GloVe等。
于是,我们不难理解上篇博客中与Embedding有关的操作。
self.embedding = nn.Embedding(input_dim, emb_dim)
# input_dim就是输入的维度,也就是将输入的单词转成one-hot向量的向量大小
# emb_dim就是进行embedding后的向量大小
embedded = self.dropout(self.embedding(src))
# 源语句embedding后再经过一层drop得到输入
# embedded = [src len, batch size, emb dim]
# 每个单词都变成了一个向量,emb_dim就是向量的大小
四、model.py中Decoder类的分析
我们再来看Decoder类。先分析init函数:
class Decoder(nn.Module):
def __init__(self, output_dim=50004, emb_dim=200, hid_dim=256, dropout=0.5, name='emb_kp20k2.npy'):
super().__init__()
#调用父类的初始化方法
self.hid_dim = hid_dim
#指定隐藏层维度
self.output_dim = output_dim
#指定输出维度,就是将输出的单词转成one-hot向量的向量大小
self.embedding = nn.Embedding(output_dim, emb_dim)
#emb_dim就是进行embedding后的向量大小
self.attention_layer = nn.Sequential(nn.Linear(self.hid_dim, self.hid_dim),
nn.ReLU(inplace=True))
self.rnn = nn.LSTM(emb_dim, hid_dim)
#采用之前得到的嵌入层、隐藏层维度初始化LSTM模型
self.fc_out = nn.Linear(emb_dim + hid_dim, output_dim)
全连接层的输入维度为嵌入层维度+隐藏层维度,输出维度为output_dim
self.dropout = nn.Dropout(dropout)
#设置dropout参数为0.5,与之前的Encoder一样
与Encoder相似,Decoder的初始化函数首先继承了父类的初始化方法, 指定输出维度为50004维,隐藏层维度为256维,dropout参数为0.5,在Decoder层也进行了Embedding操作,初始化LSTM模型,这里还设定了全连接层的参数。
接着分析forward函数
def forward(self, input, hidden, context):
#input = [batch size]为句子条数
#hidden = [n layers * n directions, batch size, hid dim]
#context = [n layers * n directions, batch size, hid dim]
#这里指定为1层,且为单向,可以构造出hidden、context的相关参数
#n layers and n directions in the decoder will both always be 1, therefore:
#hidden = [1, batch size, hid dim]
#context = [1, batch size, hid dim]
input = input.unsqueeze(0)
#input层增加一个维度
#input = [1, batch size]
embedded = self.dropout(self.embedding(input))
# input经过embedding后再经过一层drop得到输入
# embedded = [1, batch size, emb dim]
#emb_con = torch.cat((embedded, context), dim = 2)
#context就是Encoder输出的hidden,在这里作为初始隐状态,与embedded实现拼接
#emb_con = [1, batch size, emb dim + hid dim]
output, hidden = self.rnn(embedded, hidden)
# 传入嵌入层和隐藏层参数得到输出和最后的隐层
#output = [seq len, batch size, hid dim * n directions]
#output由序列长度,句子数,隐藏层维度*方向构成,这里序列长度,层数为1,为单向
#hidden = [n layers * n directions, batch size, hid dim]
#输出的hidden由层数*方向,句子数,隐藏层维度构成
#seq len, n layers and n directions will always be 1 in the decoder, therefore:
#output = [1, batch size, hid dim]
#hidden = [1, batch size, hid dim]
h,c = hidden
context = nn.Tanh()(context)
h = self.attention_layer(h)
w = torch.bmm(context, h.permute(1,2,0))
#将context与h换位后的结果相乘
w = w.squeeze()#用squeeze函数减少一个维度,只能减少长度为1的维度
w = F.softmax(w,dim=-1) #采用归一化指数函数softmax
w = torch.bmm(w.unsqueeze(1), context)
#先用unsqueez增加一个维度,再计算与context的乘积
w = w.squeeze() #用squeeze函数减少一个维度
output = torch.cat((embedded.squeeze(0),w),
dim = 1)
#去除embedding的第一个维度后与w进行拼接
#output = [batch size, emb dim + hid dim * 2]
prediction = self.fc_out(output)# 预测的单词
#prediction = [batch size, output dim]
return prediction, hidden
forward函数的大部分实现细节已写在了注释中,注意unsqueeze ()函数实现了维度的增加,后面的参数可以指定增加维度的位置,squeeze()函数可以减少维度,其使用方法与unsqueeze函数类似,但注意这里只能实现长度为1的维度的减少,函数forward的形参context就是Encoder输出的hidden,在这里作为初始隐状态。这里的输入同样要经过一层embedding后再实现一层drop,经过模型可以得到输出和最后的隐层。简单来时,Decoder层的作用就是将Encoder中的最终隐状态拿来做这里的初始隐状态,然后以作为第一个token,生成的词作为第二个token,以此进行下去直到生成的token为或者达到指定的长度为止。关于Seq2Seq的详细模型分析,我们将在下一篇博客中介绍。