无监督关键短语的生成问题博客05--model.py的分析

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内部结构计算非常类似,首先将当前时间步输入x_t与上一时间步隐含状态h_{t-1}进行拼接,得到[x_t,h_{t-1}],然后通过一个全连接层作变换,最后通过sigmoid函数进行激活得到f_t,我们可以将f_t看作是门值。遗忘门门值作用于上一层的细胞状态上,代表遗忘过去多少信息,由于遗忘门的门值是由x_t,h_{t-1} 计算得到的,整个公式则意味着:根据当前时间步输入和上一时间步隐含状态h_{t-1}来决定遗忘多少上一层的细胞状态所写的过往信息。

激活函数sigmoid用于帮助调节流经网络的值,sigmoid函数将值压缩在0和1之间。

再看输入门的结构图和计算公式:

 图3:输入门结构图与计算公式 

输入门的计算公式有两个,第一个公式产生输入门门值,它和遗忘门的公式几乎相同,这个公式意味着输入信息有多少需要进行过滤。输入门的第二个公式与传统RNN内部结构计算相同。对于 LSTM来说,它得到当前细胞状态,而不是像经典RNN一样得到隐含状态。

细胞状态更新图与计算公式如下:

 图4:细胞状态更新结构图与计算公式 

这里没有全连接层,只是将刚刚得到的遗忘门门值与上一时间步得到的C_{t-1}相乘,再加上输入门门值与当前时间步得到的未更新的C_t相乘的结果,最终得到更新后的C_t作为下一时间步输入的一部分。整个状态更新过程就是对遗忘门和输入门的应用。

最后是输出门:

  图5:输出门结构图与计算公式

输出门的公式也是两个,第一个是计算输出门门值,和遗忘门、输入门计算方式相同,第二个是使用这个门值产生隐含状态h_t,它将作用在细胞状态C_t上,并做tanh激活,最终得到h_t作为下一时间步输入的一部分。整个输入门的过程,就是为了产生隐含状态h_t

接下来我们讨论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。h_0,c_0由随机初始化得到,最后得到outputh_n,c_n

三、嵌入层的理解

在上一篇博客中,我们并未对嵌入层及相关参数详细说明,这里我们进行简述。首先需要从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的详细模型分析,我们将在下一篇博客中介绍。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值