2021SC@SDUSC
在前两篇博客中,我们分析了extract.py文件,从本篇博客开始,我们将讨论本篇论文的核心文件model.py,并结合nlp中的经典Encoder-Decoder模型进行分析。model.py主要由三个类,Encoder类编码,Decoder类解码,Seq2Seq模型生成序列。对于此类的序列编码模型,实际上就是对经典的RNN神经网络的一个变形,我们将在下文详细分析。
图1:model.py的主要类与函数
我们先看第一个类Encoder,其定义了两个函数__init__函数与forward函数,其中init是类初始化函数,forward函数必须重写,在传统的CNN中,forward是非常重要的函数,forward接受张量作为输入,然后返回张量作为输出,在构建实现之后,返回的张量也就是网络的输出。
为了更好的理解编码器、解码器相关的扩展模型与参数,我们将首先对经典的RNN作出介绍。
一、关于经典RNN架构
RNN(Recurrent Neural Network)为循环神经网络,以序列数据为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,也以序列的形式输出。
由于博主之前没有深度学习的相关经验,也通过此次分析学习了神经网络的基础知识并深入理解了与自然语言处理的深度学习知识。首先,我们讨论传统的神经网络模型。
注:该模型比较基础,介绍该模型以便于与RNN模型作对比并解释对应参数。
图2:传统的神经网络
可以看到, 在传统的全连接神经网络中,有输入层、隐藏层和输出层,可以理解为输入层的数据经过隐藏层的变换得到了输出层的数据。更具体地,我们可以看到输入层的每个节点(可以理解为神经元)都与隐藏层的所有节点相连,隐藏层之间的连接和隐藏层与输出层的连接也是如此,故这种经典的神经网络被称为全连接网络。隐层的变换函数一般为线性函数,再经过激活函数的作用,不仅实现了归一化,也实现了非线形变化。我们通过已有的数据训练这样的神经网络使得面对新的输入时,通过隐层的计算在输出层可以得到我们期望的输出(有一定的衡量标准,这里不再赘述),接下来,我们来看应用于自然语言处理的循环神经网络。
对于传统CNN而言,只能单独地处理一个个的输入,前一个输入和后一个输入是完全没有关系的。但是,自然语言任务要求处理序列的信息,即前面的输入和后面的输入是有关系的。对于自然语言处理来说,我们理解一句话常常需要结合前面的或后面的词语,理解一个段落也许理解上下文,这就像电影的帧一样,不能单独分析其中某一帧。
图3:RNN单层网络结构
我们再按时间步对RNN进行展开后研究其网络结构。
图4:按时间步展开RNN结构
RNN的循环机制使得模型隐藏层上一时间步产生的结果,能够作为当下时间步输入的一部分(当下时间步的输入除了正常的输入外还包括上一步隐藏层的输出),对当下时间步的输出产生影响。在这里,U是输入层到隐藏层的权重矩阵,V是隐藏层到输出层的权重矩阵,权重矩阵 W是隐藏层上一次的值作为这一次的输入的权重。用公式表示如下:
因为RNN结构能够很好利用序列之间的关系,因此针对自然界具有连续性的输入序列,如人类的语言、语音等可以进行很好的处理,广泛引用于NLP领域的各项任务,如文本分类、情感分析、意图识别、机器翻译等。
关于RNN的分类问题,我们将在之后介绍LSTM的博客中介绍。按照RNN内部构造进行分类,可分为传统RNN、LSTM、Bi-LSTM、GRU、Bi-GRU等。接下来我们举一个传统RNN代码构建的例子。
图5:RNN架构
我们研究最中间的方框,它的输入有两部分,分别是以及,代表上一时间步隐藏层的输出和此时间步的输入,它们进入RNN结构体后,会“融合”到一起,可以理解为将二者进行拼接,形成新的张量,新的张量通过一个全连接层(线性层),该层通过tanh作为激活函数,最终得到该时间步的输出,它将作为下一个时间步的输入和一起进入结构体。
根据分析我们可以得到其内部计算公式:,这里的激活函数tanh可以将函数值压缩在-1到1之间,用于帮助调节流经网络的值。
二、RNN的代码实例
# 导入若干工具包
import torch
import torch.nn as nn
# 实例化rnn对象
# 第一个参数:input_size(输入张量x的维度)
# 第二个参数:hidden_size(隐藏层维度,隐藏层神经元数量)
# 第三个参数:num_layers(隐藏层的层数)
rnn = nn.RNN(5,6,1)
# 设定输入的张量x
# 第一个参数:sequence_length(输入序列的长度)
# 第二个参数:batch_size(批次的样本数)
# 第三个参数:input_size(输入张量x的维度)
input1=torch.randn(1,3,5)
# 设定初始化h0
# 第一个参数:num_layers*num_directions(层数*网络层方向数)
# 第二个参数:batch_size(批次的样本数)
# 第三个参数:hidden_size(隐藏层维度)
h0=torch.randn(1,3,6)
#输出张量放在RNN中,得到输出结果
output,hn=rnn(input1,h0)
print(output)
print(output.shape)
print(hn)
print(hn.shape)
输出结果如下:
这个简单的例子指定只有一层隐藏层,且隐藏层的维度为6,输入张量的维度为5,再定义输入张量的批次样本数为3,序列长度为1,同时设定初始化的批次样本数为3,隐藏层维度为6。(输入张量与都通过randn初始化)最后将输入张量放入RNN中,得到输出的结果。
这里的参数存在着对应关系:
- rnn对象的需要与输入神经网络张量的相同。对应输入张量的维度
- rnn对象的需要与初始化中的 相同。对应隐藏层层数。
- 输入张量x与初始化的应相同,对应批次的样本数。
三、分析Encoder类
在了解了这些参数之后,我们来看论文中的Encoder类,先看init函数。
class Encoder(nn.Module):
def __init__(self, input_dim=50004, emb_dim=200, hid_dim=256,
dropout=0.5,name='emb_kp20k2.npy'):
super().__init__()
# 调用父类的构造方法
self.hid_dim = hid_dim
# 传入隐藏层维度,hid_dim是hidden和cell状态的维度(即向量的大小)
self.embedding = nn.Embedding(input_dim, emb_dim)
# 通过函数调用得到嵌入层相关信息
# input_dim就是输入的维度,也就是将输入的单词转成one-hot向量的向量大小
# emb_dim就是进行embedding后的向量大小
# no dropout as only one layer!
# emb = np.load(name)
# self.embedding.weight.data.copy_(torch.from_numpy(emb))
# 加载在数据集上训练好的npy文件
self.rnn = nn.LSTM(emb_dim, hid_dim , bidirectional=True)
# 构建rnn模型,用LSTM变式
# 传入嵌入层维度(降维后的输入层),隐藏层维度,使用双向LSTM
self.dropout = nn.Dropout(dropout)
# 设定dropout为0.5,防止过拟合,是一个正则化参数,在embedding层使用
输入张量的维度为50004,嵌入层的维度为200(可以理解为对输入张量的降维,将在后续博客详细介绍),隐藏层的维度为256。这里采用了双向传播的LSTM模型,也会在后续博客介绍。
关于dropout参数:
dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。注意是暂时,对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。
对于一个神经元来说,有保留和丢掉两种状态,很自然的就会给两种状态设置同样的概率,也就是dropout(0.5)。有p的概率神经元会失活,这样所有的神经元的排列组合数在p=0.5时候取得最大值,这就意味着随机性更大,符合dropout的初衷。
再看forward函数:
def forward(self, src):
# src是输入语句,因为实际训练的过程中不是一句一句训练而是一个batch一个batch训练
# batch size是句子的条数。src的大小就是句子长度*句子条数
# src = [src len, batch size]
embedded = self.dropout(self.embedding(src))
# 源语句embedding后再经过一层drop得到输入
# embedded = [src len, batch size, emb dim]
# 每个单词都变成了一个向量,emb_dim就是向量的大小
outputs, hidden = self.rnn(embedded)
# no cell state!即不需要细胞状态,不用返回(hidden, cell)
# outputs是每一个时间步最顶层的输出结果,hidden是最后一个时间步的输出结果
# outputs = [src len, batch size, hid dim * n directions]
# n directions指的是单向还是双向
# hidden = [n layers * n directions, batch size, hid dim]
# outputs are always from the top hidden layer
return hidden, outputs
LSTM有细胞状态和隐层状态,这里不需要细胞状态,返回每一时间步顶层的输入结果和最后一个时间步的隐层状态。
基于rnn的encoder代码,其大致逻辑和上述分析的基本一致,这里不再赘述。
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, hid_dim, dropout):
super().__init__()
self.hid_dim = hid_dim
self.embedding = nn.Embedding(input_dim, emb_dim)
self.rnn = nn.GRU(emb_dim, hid_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
#src = [src len, batch size]
embedded = self.dropout(self.embedding(src))
#embedded = [src len, batch size, emb dim]
outputs, hidden = self.rnn(embedded) #no cell state!
#outputs = [src len, batch size, hid dim * n directions]
#hidden = [n layers * n directions, batch size, hid dim]
#output是最高的隐藏层
return hidden
这里我们提到了LSTM模型,我们将在下一篇博客进行讲解,同时也可以看到,在构建RNN时架构时,我们用到了输入层、隐藏层和输出层,这里引入了嵌入层的概念,也会在后续博客中进行介绍。