zoukankan      html  css  js  c++  java
  • NLP与深度学习(二)循环神经网络

    1. 循环神经网络

    在介绍循环神经网络之前,我们先考虑一个大家阅读文章的场景。一般在阅读一个句子时,我们是一个字或是一个词的阅读,而在阅读的同时,我们能够记住前几个词或是前几句的内容。这样我们便能理解整个句子或是段落所表达的内容。循环神经网络便是采用的与此同样的原理。

    循环神经网络(RNN,Recurrent Neural Network)与其他如全连接神经网络、卷积神经网络相比,最大的特点在于:它的内部保存了一个状态,其中包含了与已经查看过的内容的相关信息。

    下面便先以SimpleRNN为例,介绍这一特点。

    2. SimpleRNN

    SimpleRNN的结构图如下所示:

    Fig. 1. ShusenWang. Simple RNN 模型[2]

     

    可以看到,SimpleRNN的模型比较简单,在t时刻的输出,等于t-1 时刻的状态ht-1与t时刻的输入Xt的集成。

    用公式表示为:

    outputt = tanh( (W * Xt) + (U * ht-1) + bias )

    其中W为输入数据X的参数矩阵,U为上一状态 ht-1的参数矩阵。且这2个参数矩阵全局共享(也就是说,每个时间步t的W与U矩阵都相同)。

    举个例子,如图中的文本序列:the cat sat on the mat。假设输入只有这单个序列,则输入SimpleRNN时,输入维度为(1, 6, 32)。这里1对应的是batch_size(RN也和其他神经网络一样,可以接收batch数据),6对应的是timesteps(也可以理解为序列长度);32对应的是词向量维度(这里假设词嵌入维度为32维)。所以SimpleRNN的输入参数shape为(batch_size, timesteps, input_features)。

    在第一个单词the进入RNN后,会进行第一个状态和输出h0 的计算。假设单词the的向量为 Xthe,初始化的状态为 hfirst(最初始的hfirst取全0),则:

    h0 = tanh( (W * Xthe) + (U * hfirst) + bias)

    到输出最后一个状态 h5 时(此时输入单词为mat),即为:

    h5 = tanh( (W * Xmat) + (U * h4) + bias)

    最终输出的状态 h5 即包含了前面输入的所有状态(也就是整个序列的信息),此输出即可输入到例如Dense层中用于各类序列任务,如情感分析,文本生成等NLP任务中。

    在tensorflow中调用SimpleRNN非常简单,下面是一个简单的单个SimpleRNN的例子:

    from tensorflow.keras import Sequential
    from tensorflow.keras.layers import Embedding, SimpleRNN
    
    model = Sequential()
    model.add(Embedding(10000, 64))
    model.add(SimpleRNN(32))
    model.summary()
    
    
    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    embedding (Embedding)        (None, None, 64)          640000    
    _________________________________________________________________
    simple_rnn (SimpleRNN)       (None, 32)                3104      
    =================================================================
    Total params: 643,104
    Trainable params: 643,104
    Non-trainable params: 0
    _________________________________________________________________
     

    其中可以看到SimpleRNN层的输出仅为最终状态ht的维度。 

    需要注意的是,给SimpleRNN的参数,我们给的是32。这里可能刚接触SimpleRNN时容易弄混的一点是:参数32并非是时间步长数,而是SimpleRNN的输出维度,也就是ht的维度。

    还有之前遇到过的一个问题是:在SimpleRNN中,第一层Embedding的输出为64,第二层的输出为32 是如何计算得出的?

    对于这个问题,我们看一下这个例子中SimpleRNN层的参数shape:

    for w in model.layers[1].get_weights():
        print(w.shape)
    
    (64, 32)
    (32, 32)
    (32,)

    从输出可以看到,这层SimpleRNN有3个参数,分别对应的就是前面提到的公式W,U与bias。在Embedding层的输出经过了与第一个参数W的矩阵运算后,输出即转换为了32维度。 

    3. RNN

    上面提到的SimpleRNN之所以叫SimpleRNN,是因为它相对于普通RNN做了部分简化。实际上SimpleRNN并非是原始RNN。为了避免读者对这2个模型产生混淆,下面简单介绍RNN。

    RNN与SimpleRNN的最大区别在于:SimpleRNN少了一个输出计算步骤。下面是2者的对比:

     

    Fig. 2. Rowel Atienza. Introucing Advanced Deep Learning with Keras[3]

    可以看到在,在计算得到timestep t时刻的状态ht后,相对于SimpleRNN立即将ht输出到softmax(此处的softmax层并非属于RNN/SimpleRNN里的结构),RNN还对输出进行了进一步处理 ot = V*ht + c,然后再输出到下一步的softmax中。

    4. SimpleRNN的局限性

    前面我们介绍了SimpleRNN可以用于处理序列(或是时序数据),其中每个timestep t 的输出状态ht包含了t时刻前的所有输入信息。

    但是,SimpleRNN有它的局限性:管理长序列的能力有限。对于长序列,使用SimpleRNN时会带来2个问题:

    1. 梯度爆炸&消失问题:随着序列的长度增长,在反向传播更新参数的过程中,越靠近顶层的梯度会越来越小。这样便会导致网络的训练速度变慢,甚至时无法学习。本质上是由于网络层数增加后,反向传播中梯度连乘效应导致;
    2. 忘记最早的输入信息:同样,随着序列长度的增加,在最终输出时,越靠近顶部的单词对最终输出状态ht的占比会越来越小。此原因也是由于参数U的连乘导致的。

    由于SimpleRNN对处理长序列的局限性,后续又提出了更高级的循环层:LSTM与GRU。这2个层都是为了解决SimpleRNN所存在的问题而提出。

    5. LSTM

    LSTM(Long short-term memory)称为长短记忆,由Hochreiter和Schmidhuber在1997年提出。当今仍在被使用在各类NLP任务中。下面是LSTM的结构图:

     

    Fig. 3. colah. Understanding LSTM Networks[4]

    LSTM也属于RNN中的一种,所以它的输入数据也是时序或序列数据。同样,它在t时间步的输入也是Xt,输出为状态ht。但是它的结果比SimpleRNN要复杂的多,有4个参数矩阵。它最重要的设计是一个传输带向量C(也称为Cell或Carry):

     

    过去的信息可以通过传输带向量C送到下一个时刻,并且不会发生太大的变化(仅有上图中的乘法与加法2种线性变换)。LSTM就是通过传输带来避免梯度消失的问题。

    在LSTM中,有几种类型的门(Gate), 用于控制传输带向量C的状态。下面分别介绍这几个Gate,以及输出状态的计算方式。

    5.1. Forget Gate

    Forget Gate 称为遗忘门,结构如下:

    从上图可以看出,遗忘门是将输入xt与上一个状态ht-1 进行concatenate合并后,与Forget Gate参数矩阵Wf进行矩阵乘法,加上偏移量bf。经过激活函数sigmoid函数进行处理,得出ft

    由于ft为sigmoid函数的结果,所以它的每个元素范围均为(0,1)。举个例子,假设a = Wf * [ht-1, xt] + bf,且a的结果为[1, 3, 0, -2],则经过softmax后,ft为:

    import tensorflow as tf
    import numpy as np
    
    a = np.array([[1., 3., 0., -2.]])
    a = tf.convert_to_tensor(x)
    
    f_t = tf.keras.activations.softmax(x)
    f_t.numpy()
    
    array([[0.73105858, 0.95257413, 0.5, 0.11920292]])

    然后ft会与传输带向量Ct-1做元素级乘法。举个例子,假设Ct-1向量为[0.9, 0.2, -0.5, -0.1],ft向量为[0.5, 0, 1, 0.8],则它们的乘积为: 

     Output = [ (0.9 * 0.5), (0.2 * 0), (-0.5 * 1), (-0.1 * 0.8) ] = [0.45, 0, -0.5, -0.08]

    很明显可以看出,遗忘门ft向量对传输带向量Ct的信息进行了过滤:

    1. 对于ft中数值为1的元素,可以让对应Ct-1位置上的元素通过(如Output中的第3个元素,其值与Ct-1中的值一致)
    2. 对于ft中数值为0的元素,可以让对应Ct-1位置上的元素不能通过(如Output中的第2个元素,其值为0)
    3. 对于ft中数值为 (0, 1) 范围的元素,可以让对应Ct-1位置上的元素部分通过(如Output中的第1个元素与第4个元素,其值分别为Ct-1中值的50%与80%)

    这样Forget Gate便对传输带向量C进行了信息过滤,也可以说决定了传输带向量C需要遗忘的信息。

    5.2. Input Gate

    下一步需要决定的是:什么样的新信息被存放在传输带向量C中。这里引入了另一个门,称为输入门(Input Gate)。

    这一步的过程图如下:

     

    可以看到这里出现了2个新的向量it与C~t。需要注意的是,Input gate仅代表it

    Input Gate 的输出it 与前面的Forget Gate中ft的计算方法一模一样,可以理解为最终也是起到一个过滤的作用。

    C~t的计算也与it基本一样,不同的是,激活函数由sigmoid替换为了tanh。由于使用了tanh,所以C~t向量中所有元素都位于(-1, 1) 之间。

    5.3. 更新传输带向量C

    在计算得出了ft,it与C~t后,便可更新传输带向量Ct的值。更新过程如下图所示:

    更新过程分为2部分,第1部分是遗忘门ft部分,前面在介绍Forget Gate的作用时已经进行了描述,在此不再阐述。

    第2部分为it * C~t,前面Input Gate中提到的作用it也类似与对信息进行过滤,而C~t也是输入信息xt与上一状态ht-1的另一种整合方法。这2个向量进行矩阵点乘后,将结果数据通过矩阵加法的运算,添加到第1部分的输出中,便得到了t时刻的传输带向量Ct的值。

    简单地说,Ct就是先通过遗忘门ft忘记了Ct-1中的部分信息,然后又添加了来自Input Gate中部分新的信息。

    5.4. Output Gate

    在更新完传输带向量Ct后,下一步便是计算t时刻的状态ht,这个过程中引入了最后一个门,称为输出门(Output Gate)。

    最后输出ht的计算过程如下图所示:

    从图中我们可以看到,Output Gate的输出ot的计算方式与Forget Gate、Input Gate的计算方式完全一样。

    输出门ot向量由于经过了sigmoid函数,所以其所有元素的范围均在(0, 1) 之间。

    最后在计算ht时,先对传输带向量Ct做tanh变换,这样其结果中每个元素的范围便均在(-1, 1) 之间。然后使用输出门ot向量与此结果做矩阵点乘,便得到t时刻的状态输出ht

    ht会有2个副本,1个副本用于输出,另1个副本用于输入到下一个时间步t+1中,作为输入。

    5.5. LSTM总结

    LSTM与SimpleRNN最大的区别在于:LSTM使用了一个“传输带“,可以让过去的信息更容易地传输到下一时刻,这样便使得LSTM对序列的记忆更长。从实际使用上来看,LSTM的效果基本都是优于SimpleRNN。

    对于LSTM中3个门的进一步理解,在《Deep Learning with Python》[1]这本书中,作者Francois Chollet提到了非常好的一点:对于这些门的解释,例如遗忘门用于遗忘传输带向量C中的部分信息,输入门用于决定多少信息输入到传输带向量C中等。对于这些门的功能解释并没有多大意义。因为这些运算的实际效果,是由参数权重决定的。而参数权重矩阵每次都是以训练的方式,从端到端中学习而来,每次训练都需要从头开始,所以不可能为某个运算赋予特定的目的。所以,对RNN中的各类运算组合,最好是将其解释为对参数搜索的一组约束,而非是出于工程意义上的一种设计。

    前面介绍过,在解决SimpleRNN的问题时,除了LSTM,还有另一种模型称为GRUs(Gated recurrent units)。GRUs也是引入了Gate的概念,不过相对与LSTM来说更简单,门也更少。

    在实际应用中,大部分场景还是会使用LSTM,而非GRUs。所以本文不会再具体介绍GRUs。

    6. Stacked RNN

    与其他常规神经网络层一样,RNN的网络也可以进行堆叠。前面我们介绍SimpleRNN时,提到它的输出仅为最终的ht向量,但是RNN的输入是一个序列,无法直接将单个 ht向量输入到RNN中。

    在这种情况下,对RNN进行堆叠,就需要每个时间步t的输出,如[h0, h1, h2, …, ht],然后将这些状态h,作为下一层RNN的输入即可。如下图所示:

    Fig. 5. Deep RecurrentNeuralNetworks[5]

    在keras中实现的方式也非常简单,指定RNN的return_sequences=True参数即可(最后一层RNN不指定),如下所示:

    import tensorflow as tf
    from tensorflow import keras
    from tensorflow.keras.models import Sequential
    from tensorflow.keras.layers import LSTM, Embedding, Dense
    
    vocabulary = 10000
    embedding_dim = 32
    word_num = 500
    state_dim = 32
    
    model = Sequential([
        Embedding(vocabulary, embedding_dim, input_length=word_num),
        LSTM(state_dim, return_sequences=True, dropout=0.2),
        LSTM(state_dim, return_sequences=True, dropout=0.2),
        LSTM(state_dim, return_sequences=False, dropout=0.2),
        Dense(1, activation='sigmoid')
    ])
    

    7. 双向RNN网络

    前面我们看到的SimpleRNN,LSTM都是从左往右,单向地处理序列。在NLP任务中,还常常用到双向RNN。双向RNN是RNN的一个变体,在某些任务上比单向RNN性能更好。

    在机器学习中,如果一种数据的表示方式不同,但是数据是有价值的话,则是非常值得探索不同的表示方式。若是这种表示方式的差异越大则越好,因为它们提供了其他查看数据的角度,从而获取数据数据中被其他方法所忽略的信息。这个便是集成(ensembling)方法背后的直觉。在图像识别任务中,数据增强的方法也是基于这一理念。

    双向RNN的示例图如下所示:

     

    Fig. 6. Colah, Neural Networks, Types, and Functional Programming[6]

    从上图中,我们可以看到,双向神经网络是分别从2个方向(从左到右,从右到左),独立地训练了2个神经网络。输入数据均为X。在得到2个神经网络的输出状态hleft, hright后,再将2个向量进行拼接(concatenate)操作,即得到了输出向量y。这个输出向量y [y0, y1, y2,… yi] 即可输入到下一层RNN中。

    若是仅需要类似SimpleRNN中ht的单个输出,则将y向量丢弃,仅将si 与s’I 做拼接后输出即可。

    在keras中,实现双向RNN的网络也非常简单,仅需要将layer用Bidirectional() 方法进行包装即可。例如:

    # Bidirectional LSTM
    
    vocabulary = 10000
    embedding_dim = 32
    word_num = 500
    state_dim = 32
    
    from tensorflow.keras.layers import Bidirectional
    
    model_blstm = Sequential([
        Embedding(vocabulary, embedding_dim, input_length=word_num),
        Bidirectional(LSTM(state_dim, return_sequences=False, dropout=0.2)),
        Dense(1, activation='sigmoid')
    ])
    
    model_blstm.summary()
    
    Model: "sequential_1"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    embedding_1 (Embedding)      (None, 500, 32)           320000    
    _________________________________________________________________
    bidirectional (Bidirectional (None, 64)                16640     
    _________________________________________________________________
    dense_1 (Dense)              (None, 1)                 65        
    =================================================================
    Total params: 336,705
    Trainable params: 336,705
    Non-trainable params: 0

    可以看到,我们给定的LSTM的输出维度为32,但是在经过了Bidirectional后,输出维度增加到了64。这是由于Bidirectional RNN的输出是由2个LSTM(一左一右)的输出向量的拼接而得出。

    总结

    本文介绍了常用的循环神经网络,其中更有用的是LSTM网络。而双向RNN在普遍场景下会比单向RNN的效果更好(除非输入序列需要遵守严格的输入顺序),所以可以优先考虑使用双向RNN。

    对于复杂任务,Stacked RNN的参数容量会更多,能解决的问题也会更复杂。如果有足够的训练样本,可以使用Stacked RNN。

    另一方面,从现在的趋势来看,现在的RNN没有以前流行了。尤其是在NLP问题中,RNN其实显得有些过时了。在训练数据足够多的情况下,已经见到的事实是:RNN的效果不如Transformer模型。不过若是问题是比较小的规模,则RNN还是比较有用的。

    下一章节我们会介绍对NLP领域产生变革性提升的Attention机制与Transformer模型。

    References

    [1] Francois Chollet. Deep Learning with Python. 2017. Chapter 6. Deep learning for text and sequences | Deep Learning with Python (oreilly.com)

    [2] RNN模型与NLP应用(3/9):Simple RNN模型_哔哩哔哩_bilibili

    [3] Introducing Advanced Deep Learning with Keras | Advanced Deep Learning with TensorFlow 2 and Keras - Second Edition (oreilly.com)

    [4] Understanding LSTM Networks -- colah's blog

    [5] 9.3. Deep Recurrent Neural Networks — Dive into Deep Learning 0.17.0 documentation (d2l.ai)

    [6] http://colah.github.io/posts/2015-09-NN-Types-FP/

  • 相关阅读:
    如何理解显示卡的驱动模块(DDX,DRM,DRI,XVMC)
    基于Linux的嵌入式文件系统构建与设计
    Windows系统——后缀为.zip.00X的zip分卷解压
    windows系统——U 盘损坏修复
    windows系统——常用命令
    U盘用FAT32还是用NTFS格式好
    linux系统程序设计教程
    Posix线程编程指南
    编程风格——UNIX 高手的 10 个习惯
    linux压缩文件——解压方法
  • 原文地址:https://www.cnblogs.com/zackstang/p/15200651.html
Copyright © 2011-2022 走看看