TensorFlow 代码模版-RNN

1.RNN核心知识点总结

循环神经网络(recurτent neural network ,RNN)的主要用途是处理和预测序列数据。它能够挖掘数据中的时序信息以及语义信息的深度表达能力被充分利用,并在语音识别、语言模型、机器翻译以及时序分析等方面实现了突破。

1.1.RNN网络结构特点

全连接神经网络或卷积神经网络模型中,网络结构都是从输入层到隐含层再到输出层,层与层之间是全连接或部分连接的,但每层之间的结点是无连接的。而 RNN 为了刻画一个序列当前的输出与之前信息的关系,在网络结构的设计上,RNN 的隐藏层之间的结点是有连接的 ,隐藏层的输入不仅包括输入层的输出,还包括上一时刻隐藏层的输出。这样结构设计,可以让 RNN 记忆之前的信息,并利用之前的信息影响后面结点的输出

RNN 经典的网络结构示意图如下

TENSORFLOW 代码模版-RNN

由于模块 A 中的运算和变量在不同时刻是相同的,因此 RNN 理论上可以被看作是同一神经网络结构被无限复制的结果。正如 CNN 在不同的空间位置共享参数,RNN 是在不同时间(序列)位置共享参数,从而能够使用有限的参数处理任意长度的序列。 这种序列不同位置参数共享的特性,可以保证序列中的每一个位置都遵循某个共同的规则。在 RNN 中,这个被复制多次的结构被称之为循环体,如何设计循环体的网络结构是 RNN 解决实际问题的关键。1

1.2.RNN网络架构类型

RNN 按照整体输入和输出的结构应用可分为以下五类:

TENSORFLOW 代码模版-RNN

它们的应用场景示例:

  1. 一对一(one to one):一对一其实就是传统的神经网络结构,固定大小的输入和固定大小的输出,没有 RNN 的常规的处理模式,典型应用如图片分类
  2. 一对多(one to many):输出是一个序列,这种结构有两种方式,一种是只在序列开始进行输入计算,还有一种是把同一个输入信息作为每个阶段的输入,典型应用如生成图片描述,输入一张图片输出由多个单词构成的一句描述文本;
  3. 多对一(many to one):输入是一个序列,输出是一个单独的值,典型应用如文本情感分类,输入一句话判断是积极还是消极的情感分类;
  4. 多对多(many to many):输入输出序列不等长,这种结构又叫 Encoder-Decoder 模型,也可以称之为 Seq2Seq 模型。在实现问题中,我们遇到的大部分序列都是不等长的,典型应用如机器翻译,输入一句英文句子输出中文句子,通常 Encoder-Decoder 结构先将输入数据编码成一个上下文向量 c,之后在通过这个上下文向量输出预测序列;
  5. 多对多(many to many):输入输出序列等长,典型应用如命名实体识别,即序列标注问题,根据输入的句子,预测出其标注序列。

1.3.RNN基本结构类型

对于单个循环体的结构,RNN 主要有三种重要类型:

TENSORFLOW 代码模版-RNN

 

1.3.1.RNN

TENSORFLOW 代码模版-RNN

符号:

$x_t$:输入向量 ($m \times 1$);

$h_t$:隐藏层向量 ($n \times 1$);

$o_t$:输出向量 ($n \times 1$);

$b$,$c$:偏置向量 ($n \times 1$);

$W$:输入向量的参数矩阵 ($n \times m$);

$U$,$V$:状态向量参数矩阵 ($n \times n$);

$\sigma_h$,$\sigma_y$:激活函数;

$\hat{y_t}$,$y_t$:预测输出和真实输出;

$L_t$: 损失函数。

 

前向传播

$$ h_t=\sigma_h(z_t)=\sigma_h(Wx_t+Uh_{t-1}+b) $$

$$\hat{y}_t=\sigma_y(o_t)=\sigma_y(Vh_t+c) $$

状态激活函数 $\sigma_h$ 一般为 tanh,RNN 常会用作分类模型,这个时候输出激活函数 $\sigma_y$ 一般是 softmax。

注:

  • 这里将输入向量的权重和上一时刻隐藏层向量的权重作了区分,其实二者实质是一样的,因此有些地方也会写成将两个向量拼接,直接匹配一个权重。

  • 为了将当前时刻的状态转化为最终的输出,循环神经网络还需要另外一个全连接神经网络来完成这个过程。这和卷积神经网络中最后的全连接层的意义是一样的。类似的,不同时刻用于输出的全连接神经网络中的参数也是一致的

  • 在得到循环神经网络的前向传播结果之后,可以和其他神经网络类似地定义损失函数。循环神经网络唯一的区别在于因为它每个时刻都有一个输出,所以循环神经网络的总损失为所有时刻(或者部分时刻,如机器翻译只计算最后 4 个时刻)上的损失函数的总和,即:

    $$L(\hat y , y)=\sum_{t=1}^{T_y} L(\hat y_t, y_t)$$

反向传播

RNN 的反向传播是基于时间的,所以 RNN 的反向传播也叫做 BPTT(back-propagation through time)。这里所有的 W、U、V、b、c 在序列的各个位置是共享的,反向传播时更新的是相同的参数

BPTT 其实还是基于普通的 BP 算法,只是多了一个时间上的反向误差传播过程,如下图2

TENSORFLOW 代码模版-RNN

左边的网络结构图中可以看出,除了最后一个时刻,每个神经元都会受到深层网络和时间序列上的两条链路的反向传播, 绿色的表示的是时间上的反向传播的过程,红色的是同一个时刻空间上的传播的过程(其实也就是常规的深层到浅层的反向传播过程)。

右边是反向传播的计算公式,很好地表达了来自时空两个方向的误差分解,用到了链式求导法则,注意时间序列中的损失是当前时刻到最后一个时刻的时间的损失总和。

详细的 RNN 反向传播推导可参考🔗循环神经网络 (RNN) 模型与前向反向传播算法

梯度消失和梯度爆炸

梯度消失(gradient vanishing)和梯度爆炸(gradient exploding)是深度神经网络梯度和长序列循环神经网络不稳定性的一种常见表现,造成梯度爆炸和梯度消失的原因有很多,比如初始化方式不合理,选择的激活函数不太好等等,但本质原因在于反向传播的链式求导法则(网络深度、时序长度,梯度优化都有关系),浅层梯度是来自于后面深层梯度的连乘累积,属于先天不足。

梯度消失和梯度爆炸的发生时的表现

  • 梯度消失发生时,靠近输出层的深层隐藏层权值更新比较正常,但靠近输入层的浅层隐藏层更新非常缓慢,权值几乎不变,对应于 RNN 的时序一样,靠前的时间比靠后时间的权值更新变换要缓慢得多;

    TENSORFLOW 代码模版-RNN

  • 梯度爆炸发生时,靠近输入层的浅层隐藏层权值更新变化比靠近输出层的深层隐藏层的权值更新变化要快得多,同样对于 RNN,靠前的时间比靠后时间的权值更新变换要快得多。

梯度消失和梯度爆炸发生可能的原因

以下图的网络结构为例:

TENSORFLOW 代码模版-RNN

简单起见,假设每一层只有一个神经元且对于每一层 $y_i=\sigma\left(z_i\right)=\sigma\left(w_ix_i+b_i\right)$,其中 $\sigma$ 为 sigmoid 函数(假设这里用 sigmoid 激活函数,其他激活函数可同理分析)。

根据反向传播,可以推导出:

$$\begin{align} &\frac{\partial C}{\partial b_1}=\frac{\partial C}{\partial y_4}\frac{\partial y_4}{\partial z_4}\frac{\partial z_4}{\partial x_4}\frac{\partial x_4}{\partial z_3}\frac{\partial z_3}{\partial x_3}\frac{\partial x_3}{\partial z_2}\frac{\partial z_2}{\partial x_2}\frac{\partial x_2}{\partial z_1}\frac{\partial z_1}{\partial b_1}\\ &=\frac{\partial C}{\partial y_4}\sigma'\left(z_4\right)w_4\sigma'\left(z_3\right)w_3\sigma'\left(z_2\right)w_2\sigma'\left(z_1\right) \end{align}$$

而 sigmoid 的导数 $\sigma'\left(x\right)$,如下图:

TENSORFLOW 代码模版-RNN

由上图可知,$\sigma'(x)$ 的最大值为 $\frac{1}{4}$,而我们一般会即使用一个均值为 0 标准差为 1 的高斯分布来初始化网络权重。因此,初始化的网络权值通常都小于 1,即 $|w|<1$,则 $|\sigma '(z)w| \le \frac{1}{4}$,由此可以看出,对于上面的链式求导,层数越多,求导结果 $\frac{\partial C}{\partial b_1}$ 越小,因而导致梯度消失的情况出现。

梯度爆炸则是因为 $|\sigma '(z)w| \gt 1$,也就是 $w$ 比较大的情况,通常是因为人为设置的初始化权重很大。则前面的网络层比后面的网络层梯度变化更快,引起了梯度爆炸的问题。

但对于使用 sigmoid 激活函数来说,梯度爆炸的情况很少出现。因为 $\sigma'(z)$ 的大小也与 $w$ 有关($z=wx+b$),除非该层的输入值 $x$ 一直在一个比较小的范围内 (小于 0.45 且 abs(w)=6.5)。3

对于 BPTT 的梯度消失和梯度爆炸,出现的原因是一样的,如下图:

TENSORFLOW 代码模版-RNN

图中表示从 $\sum L_{j}$ 中取出最后一个 $L_{t+n}$ 求关于 $c_{t}^{l}$ 的梯度,由于在时间序列上的反向传播影响,存在 $n$ 个 $||W_{h}||||\delta'(c_{\tau}^{l})||$ 相乘,一般来说|$|\delta'(c_{\tau}^{l})||$ 小于等于 $\frac{1}{4}$,那么如果 $||W_{h}||$ 小于 4,那么就会出现梯度消失;如果大于 4,那么就会出现梯度爆炸。

注:图中那个连乘的梯度,数学上称为序列雅克比(Sequential Jacobian),可以用来衡量输出向量对于输入微小改变的敏感程度,即评估循环网络在某个特定时间步的输出下,整个输入序列中每个时间步对此分别的影响。

梯度消失和梯度爆炸的解决方法

下面罗列一些可以缓解梯度消失和梯度爆炸的思路,各有优缺点,根据实际情况来选择:

  1. CEC(constant error carrousel)

    主要解决 RNN 的梯度消失问题,令 $\frac{\partial c_{\tau +1}^l}{\partial c_\tau^l}=W_h^T \sigma'(c_\tau^l) \approx I$,$I$ 是一个常数,这样就获得常数误差流了,但也导致激活函数变成了线性,是比较落后的做法,不推荐。

  2. 使用梯度裁剪、权重正则来解决梯度爆炸问题;

    TENSORFLOW 代码模版-RNN

  3. 使用好的参数初始化方式,如 He 初始化;

  4. 使用更合适的激活函数,如非饱和的激活函数 ReLU;

    关于 RNN 选择 tanh 还是 ReLU 的经验:

    RNN 是可以用 RELU 的,但是初始化参数的值应该在 1 附近,不然也会梯度爆炸或者梯度消失。

    (@ 飞龙)看具体场景,比如要预测某个时刻的用电量,用电量是个正值,显然 ReLU 合适。 如果要预测事件流的未来情况,0 是不发生,1 是发生,显然 tanh 更合适。

  5. 使用 BN(Batch Normalization,批标准化)结构;

  6. 用一些优化的网络结构,如 ResNet、LSTM。

1.3.2.LSTM

TENSORFLOW 代码模版-RNN

LSTM(Long-Short Term Memory)称为长短时记忆网络,是一种特殊的 RNN 网络,普通的简单循环神经网络有可能会丧失学习到远距离信息的能力,反向传播也存在梯度不稳定的问题,或者在复杂语言场景中,有用信息的间隔有大有小、长短不一 ,导致循环神经网络的性能也会受到限制,LSTM 设计出来就是为了解决这种长依赖问题的。在很多的任务上,采用 LSTM 结构的循环神经网络比标准的循环神经网络表现更好。

为了解决模型学习过程中信息的长依赖问题,LSTM 设计了三个特殊的 “门” 结构:输入门(Input Gate)、输出门(Output Gate)、遗忘门(Forget Gate),如下图所示:

TENSORFLOW 代码模版-RNN

“门” 的结构可以让信息有选择性地影响循环神经网络中每个时刻的状态,它其实只是一个 sigmoid 和按位乘法结合起来的操作,之所以叫做 “门”,是因为使用 sigmoid 作为激活函数的全连接神经网络层会输出一个 0 到 1 之间的数值,相当于控制着当前输入有百分之多少的信息量可以通过这个结构。当 sigmoid 结果为 1 时,这个 “门” 处于完全打开状态,全部信息都可以通过,而当 sigmoid 结果为 0 时,表示这个 “门” 处于完全关闭状态,任何信息都无法通过。

符号

$h_t$,$C_t$:隐藏层向量

$x_t$:输入向量

$b_f$,$b_i$,$b_c$,$b_o$:偏置向量

$W_f$,$W_i$,$W_c$,$W_o$:参数矩阵

$\sigma$,$tanh$:激活函数

$f_t$,$i_t$, $o_t$:遗忘门,输入门,输出门

前向传播

$$f_t=\sigma(W_f\cdot[h_{t-1},x_t]+b_f) $$

$$i_t=\sigma(W_i\cdot[h_{t-1},x_t]+b_i)$$

$$o_t=\sigma(W_o\cdot[h_{t-1},x_t]+b_o) $$

$$\tilde{C}_t=\tanh(W_c\cdot[h_{t-1},x_t]+b_c) $$

$$ C_t=f_t\odot C_{t-1}+i_t\odot\tilde{C}_t $$

$$h_t=o_t\odot\tanh(C_t) $$

 

1.3.3.GRU

TENSORFLOW 代码模版-RNN

GRU(Gate Recurrent Unit)称为门控循环单元,也是一种特殊的 RNN 网络,和 LSTM 一样,GRU 也是为了解决长期记忆和反向传播中的梯度等问题而提出来的,结构也非常类似。之所以在 LSTM 之后又提出 GRU,是因为 LSTM 加入 “门” 结构导致参数较多、内部计算复杂、训练时间长,而 GRU 不但保持了 LSTM 效果,还具有更加简单的结构、更少的参数、更好的收敛性,相当于是基于 LSTM 的一个优化变种版本

GRU 将 LSTM 的三个门结构缩减为两个门结构,即更新门(Update Gate)和重置门(Reset Gate),如下图所示:

TENSORFLOW 代码模版-RNN

更新门用于控制前一时刻的状态信息被带入到当前状态中的程度,更新门的值越大说明前一时刻的状态信息带入越多。

重置门用于控制忽略前一时刻的状态信息的程度,重置门的值越小说明忽略得越多。

符号

$h_t$:隐藏层向量

$x_t$:输入向量

$b_z$,$b_r$,$b_h$:偏置向量

$W_z$,$W_r$,$W_h$:参数矩阵

$\sigma$,$tanh$:激活函数

$z_t$,$r_t$:更新门,重置门

前向传播

$$ z_t=\sigma(W_z \cdot[h_{t-1},x_t]+b_z)$$

$$r_t=\sigma(W_r \cdot [h_{t-1},x_t]+b_r)$$

$$\tilde{h}_t=\tanh(W_h\cdot[r_t \odot h_{t-1},x_t]+b_h)$$

$$h_t=(1-z_t)\odot h_{t-1}+z_t\odot \tilde{h}_t $$

反向传播

LSTM 和 GRU 的反向传播数学推导可参考🔗LSTM 和 GRU 的反向传播公式推导

1.4.RNN的变种结构

1.4.1.BRNN

双向循环神经网络(bidirectional RNN,BRNN)是用来解决当前时刻的输出不仅和之前状态有关还和之后状态有关的这类问题,它是由两个独立的循环神经网络叠加在一起组成的,输出由这两个循环神经网络的输出拼接而成,RNN、LSTM、GRU 均作为双向循环网络的循环体,整体结构如下图所示:

TENSORFLOW 代码模版-RNN

1.4.2.DRNN

深层循环神经网络(Deep RNN,DRNN)是循环神经网络的另外一种变种,DRNN 可以在网络中设置多个循环层,将每层循环网络的输出传给下一层进行处理,和卷积神经网络类似,不同时间序列上同一空间层的循环体中参数是一致的,而不同空间层中的参数可以不同。DRNN 通过增加网络隐藏层深度来主增强模型的表达能力,从而抽取更加高层的信息。其整体结构如下图所示:

TENSORFLOW 代码模版-RNN

DRNN 也可以类似 CNN 一样进行 dropout 操作提高鲁棒性,不过 RNN 中的 dropout 通常只在空间上(深层网络传递)使用,而不会在时序上进行的 dropout 操作。

2.RNN代码实践

TensorFlow 提供了两个大类来灵活实现不同结构的 RNN,一类是基础循环体单元类,即包括 RNN、LSTM、GRU 等基础结构的循环体单元,另外一类是构建循环结构的封装类,通常包括静态、动态、双向 RNN 等,整个 RNN 结构的实现就是用循环结构的封装类来把基础循环体单元类进行多次复制和重复。

2.1.TensorFlow提供基本类和方法

下面列举两个大类常用的类或函数。

基础循环体单元 (Cell)

  • tf.nn.rnn_cell.BasicRNNCell

  • tf.nn.rnn_cell.BasicLSTMCell

    带 Basic 的 Cell 类是一种参考或者标准实现,一般情况下,如果有其他可替代的 Cell 存在都不应该是首选。

  • tf.nn.rnn_cell.LSTMCell

  • tf.nn.rnn_cell.GRUCell

  • tf.nn.rnn_cell.MultiRNNCell

    深层循环神经网络基础单元,其实他也是一个结构单元,构建空间方向的结构。

循环结构封装类

  • tf.nn.static_rnn 静态 RNN 结构

  • tf.nn.dynamic_rnn 动态 RNN 结构

    $ 动态和静态的区别:

    1)默认的 input 以及 output 的形状是不一样的,动态可设定 time_major = True 来实现与静态的 shape 一样。 2)带有 static 前缀的 api 要求输入的序列具有固定长度。而带有 dynamic 前缀的 api 可以选择输入一个 sequence_length(可以是一个 list)参数,该参数对应的是输入 sequence 的序列长度,用来动态处理 sequence 的长度(代码中是设置了一个专门记录序列长度的 tensor,控制 rnn 自环的轮数)。 3)内部训练机制不同,静态是一个个 cell 链接到一起展开的,所以比较耗内存,而动态是循环生成,前一个 cell 计算出结果后就会被下一个 cell 替换掉。

  • tf.nn.static_bidirectional_rnn 静态双向 RNN 结构

  • tf.nn.bidirectional_dynamic_rnn 动态双向 RNN 结构

另外,tf.contrib 中也提供了 rnn 模块,工具类更多一点,用法和 nn 模块中的基本一致,是 nn 的进一步封装。

2.2.RNN基础实现代码模版

我们使用 RNN 简单实现 MNIST 数字识别,MNIST 的单张图像大小为 28x28,我们可以把每张图像看作是 28 个总时序,每个时序是 28 个值,然后送入 RNN 网络,如下图所示:

TENSORFLOW 代码模版-RNN

2.2.1.手写LSTM

2.2.2.RNN、LSTM、GRU

2.2.3.BRNN

2.2.4.DRNN

3.参考资料


1 郑泽宇等. TensorFlow:实战 Google 深度学习框架(第 2 版). 电子工业出版社
© 除特别注明外,本站所有文章均为卢明冬的博客原创 , 转载请注明作者和文章链接。
© 本文链接:https://lumingdong.cn/tensorflow-code-template-rnn.html
相关文章
写下您的评论...