在Pytorch下搭建BiLSTM(Reproducible/Deterministic)

什么是LSTM

如果还不知道什么是LSTM ,请移步
http://colah.github.io/posts/2015-08-Understanding-LSTMs/
我第一眼看到LSTM时,还在感概这个网络怎么这多参数。其实接触多了,发现LSTM的精髓就在于3个门,forget,input和output,围绕这3个门的公式也是基本相似,所以记忆LSTM的公式其实相当简单。

为什么要用LSTM

因为简单的RNN很容易就发生梯度消失和梯度爆炸,其中主要的原因是RNN中求导,引起的链式法则,对时间上的追溯,很容易发生系数矩阵的累乘,矩阵元素大于1,那么就会发生梯度爆炸;矩阵元素小于1,就会发生梯度消失。
LSTM通过门的控制,可以有效的防止梯度消失,(敲黑板!!!)但是依旧可能出现梯度爆炸的问题,所以训练LSTM会加入梯度裁剪(Gradient Clipping)。在Pytorch中梯度裁剪可以使用

import torch.nn as nn
nn.utils.clip_grad_norm(filter(lambda p:p.requires_grad,model.parameters()),max_norm=max_norm)

在以下的代码中我不会使用梯度裁剪操作,大家如果有需要可以自己添加以上代码。关于梯度消失和梯度爆炸的具体原因分析可以移步
http://www.cs.toronto.edu/~rgrosse/courses/csc321_2017/readings/L15%20Exploding%20and%20Vanishing%20Gradients.pdf

为什么要用BiLSTM

Bi代表双向。其实使用BiLSTM还是蛮有争议,因为人类理解时序信号的默认顺序其实是时间流逝的顺序,那么将时间倒叙的信号还有没有意义?有人说有,譬如说看一个人写一个字的具体笔画顺序其实不影响我们猜测这个字(这个例子其实是我瞎举的);有人说没有,倒着听一个人说话就不行。不管有什么争议,但是架不住BiLSTM在实际应用中效果十有八九好于LSTM,所以就用吧。
具体双向LSTM的结构其实相当简单,就是两个单向LSTM各自沿着时间和网络层向前传播,然后最后的输出拼接在一起。

不如先搭建一个BiLSTM,为了分类任务

先定义几个符号

  • B代表batch size,
  • L_i代表在batch中第i个序列的长度,L\in R^B是一个长度为B的向量
  • x(i,0:L_i,0:d_{input})代表在batch中第i个序列,其长度为L_i,每一帧的维度是d_{input};每一个batch的数据x的矩阵大小为x\in R^{B\times L_{max}\times d_{input}},其中L_{max}是序列L中的最大值,对于长度不足L_{max}事先应进行补0操作
  • y(i,0:L_i)代表在batch中第i个序列的类别,每一个batch的数据y的矩阵大小为y\in R^{B\times L_{max}},其中L_{max}是序列L中的最大值,对于长度不足L_{max}事先应进行补-1操作(避免和0混淆,其实补什么都无所谓,这里只是为了区分)

在这里,我将先使用Pytorch的原生API,搭建一个BiLSTM。先吐槽一下Pytorch对可变长序列处理的复杂程度。处理序列的基本步骤如下:

  1. 准备torch.Tensor格式的data=x,label=y,length=L,等等
  2. 数据根据length排序,由函数sort_batch完成
  3. pack_padded_sequence操作
  4. 输入到lstm中进行训练

函数sort_batch

def sort_batch(data,label,length):
    batch_size=data.size(0)
    # 先将数据转化为numpy(),再得到排序的index
    inx=torch.from_numpy(np.argsort(length.numpy())[::-1].copy())
    data=data[inx]
    label=label[inx]
    length=length[inx]
    # length转化为了list格式,不再使用torch.Tensor格式
    length=list(length.numpy())
    return (data,label,length)

网络

class Net(nn.Module):
    def __init__(self,input_dim,hidden_dim,output_dim,num_layers,biFlag,dropout=0.5):
        # input_dim 输入特征维度d_input
        # hidden_dim 隐藏层的大小
        # output_dim 输出层的大小(分类的类别数)
        # num_layers LSTM隐藏层的层数
        # biFlag 是否使用双向
        super(Net,self).__init__()
        self.input_dim=input_dim
        self.hidden_dim=hidden_dim
        self.output_dim=output_dim
        self.num_layers=num_layers
        if(biFlag):self.bi_num=2
        else:self.bi_num=1
        self.biFlag=biFlag
        # 根据需要修改device
        self.device=torch.device("cuda")

        # 定义LSTM网络的输入,输出,层数,是否batch_first,dropout比例,是否双向
        self.layer1=nn.LSTM(input_size=input_dim,hidden_size=hidden_dim, \
                        num_layers=num_layers,batch_first=True, \
                        dropout=dropout,bidirectional=biFlag)
        # 定义线性分类层,使用logsoftmax输出
        self.layer2=nn.Sequential(
            nn.Linear(hidden_dim*self.bi_num,output_dim),
            nn.LogSoftmax(dim=2)
        )
        
        self.to(self.device)

    def init_hidden(self,batch_size):
        # 定义初始的hidden state
        return (torch.zeros(self.num_layers*self.bi_num,batch_size,self.hidden_dim).to(self.device),
                torch.zeros(self.num_layers*self.bi_num,batch_size,self.hidden_dim).to(self.device))
    def forward(self,x,y,length):
        # 输入原始数据x,标签y,以及长度length
        # 准备
        batch_size=x.size(0)
        max_length=torch.max(length)
        # 根据最大长度截断
        x=x[:,0:max_length,:];y=y[:,0:max_length]
        x,y,length=sort_batch(x,y,length)
        x,y=x.to(self.device),y.to(self.device)
        # pack sequence
        x=pack_padded_sequence(x,length,batch_first=True)

        # run the network
        hidden1=self.init_hidden(batch_size)
        out,hidden1=self.layer1(x,hidden1)
        # out,_=self.layerLSTM(x) is also ok if you don't want to refer to hidden state
        # unpack sequence
        out,length=pad_packed_sequence(out,batch_first=True)
        out=self.layer2(out)
        # 返回正确的标签,预测标签,以及长度向量
        return y,out,length

官方的BiLSTM有缺陷

以上的代码看似没问题了,实际上却有一个无法容忍的问题就是non-reproducible。也就是这个双向LSTM,每次出现的结果会有不同(在固定所有随机种子后)。老实说,这对科研狗是致命的。所以reproducible其实是我对模型最最基本的要求。

根据实验,以下情况下LSTM是non-reproducible,

  • 使用nn.LSTM中的bidirectional=True,且dropout>0

根据实验,以下情况下LSTM是reproducible,

  • 使用nn.LSTM中的bidirectional=True,且dropout=0
  • 使用nn.LSTM中的bidirectional=False

也就是说双向LSTM在加上dropout操作后,会导致non-reproducible,据说这是Cudnn的一个问题,Pytorch无法解决,具体可见
https://discuss.pytorch.org/t/non-deterministic-result-on-multi-layer-lstm-with-dropout/9700
https://github.com/soumith/cudnn.torch/issues/197

作为一个强迫症,显然无法容忍non-reproducible。所幸单向的LSTM是reproducible,所以只能自己搭建一个双向的LSTM

自己动手丰衣足食

这里要引入一个新的函数reverse_padded_sequence,作用是将序列反向(可以理解为将batch x\in R^{B\times L_{max}\times d_{input}}的第二个维度L反向,但是补零的地方不反向,作用同tensorflow中的tf.reverse_sequence函数一致)

import torch
from torch.autograd import Variable

def reverse_padded_sequence(inputs, lengths, batch_first=True):
    '''这个函数输入是Variable,在Pytorch0.4.0中取消了Variable,输入tensor即可
    '''
    """Reverses sequences according to their lengths.
    Inputs should have size ``T x B x *`` if ``batch_first`` is False, or
    ``B x T x *`` if True. T is the length of the longest sequence (or larger),
    B is the batch size, and * is any number of dimensions (including 0).
    Arguments:
        inputs (Variable): padded batch of variable length sequences.
        lengths (list[int]): list of sequence lengths
        batch_first (bool, optional): if True, inputs should be B x T x *.
    Returns:
        A Variable with the same size as inputs, but with each sequence
        reversed according to its length.
    """
    if batch_first:
        inputs = inputs.transpose(0, 1)
    max_length, batch_size = inputs.size(0), inputs.size(1)
    if len(lengths) != batch_size:
        raise ValueError("inputs is incompatible with lengths.")
    ind = [list(reversed(range(0, length))) + list(range(length, max_length))
           for length in lengths]
    ind = torch.LongTensor(ind).transpose(0, 1)
    for dim in range(2, inputs.dim()):
        ind = ind.unsqueeze(dim)
    ind = Variable(ind.expand_as(inputs))
    if inputs.is_cuda:
        ind = ind.cuda(inputs.get_device())
    reversed_inputs = torch.gather(inputs, 0, ind)
    if batch_first:
        reversed_inputs = reversed_inputs.transpose(0, 1)
    return reversed_inputs

接下来就是手动搭建双向LSTM的网络,和之前基本类似

class Net(nn.Module):
    def __init__(self,input_dim,hidden_dim,output_dim,num_layers,biFlag,dropout=0.5):
        super(Net,self).__init__()
        self.input_dim=input_dim
        self.hidden_dim=hidden_dim
        self.output_dim=output_dim
        self.num_layers=num_layers
        if(biFlag):self.bi_num=2
        else:self.bi_num=1
        self.biFlag=biFlag

        self.layer1=nn.ModuleList()
        self.layer1.append(nn.LSTM(input_size=input_dim,hidden_size=hidden_dim, \
                        num_layers=num_layers,batch_first=True, \
                        dropout=dropout,bidirectional=0))
        if(biFlag):
        # 如果是双向,额外加入逆向层
                self.layer1.append(nn.LSTM(input_size=input_dim,hidden_size=hidden_dim, \
                        num_layers=num_layers,batch_first=True, \
                        dropout=dropout,bidirectional=0))


        self.layer2=nn.Sequential(
            nn.Linear(hidden_dim*self.bi_num,output_dim),
            nn.LogSoftmax(dim=2)
        )

        self.to(self.device)

    def init_hidden(self,batch_size):
        return (torch.zeros(self.num_layers*self.bi_num,batch_size,self.hidden_dim).to(self.device),
                torch.zeros(self.num_layers*self.bi_num,batch_size,self.hidden_dim).to(self.device))
    

    def forward(self,x,y,length):
        batch_size=x.size(0)
        max_length=torch.max(length)
        x=x[:,0:max_length,:];y=y[:,0:max_length]
        x,y,length=sort_batch(x,y,length)
        x,y=x.to(self.device),y.to(self.device)
        hidden=[ self.init_hidden(batch_size) for l in range(self.bi_num)]

        out=[x,reverse_padded_sequence(x,length,batch_first=True)]
        for l in range(self.bi_num):
            # pack sequence
            out[l]=pack_padded_sequence(out[l],length,batch_first=True)
            out[l],hidden[l]=self.layer1[l](out[l],hidden[l])
            # unpack
            out[l],_=pad_packed_sequence(out[l],batch_first=True)
            # 如果是逆向层,需要额外将输出翻过来
            if(l==1):out[l]=reverse_padded_sequence(out[l],length,batch_first=True)
    
        if(self.bi_num==1):out=out[0]
        else:out=torch.cat(out,2)
        out=self.layer2(out)
        out=torch.squeeze(out)
        return y,out,length

大功告成,实测此网络reproducible

Appendix

固定Pytorch中的随机种子

import torch
import numpy as np
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)

你可能感兴趣的:(在Pytorch下搭建BiLSTM(Reproducible/Deterministic))