TensorFlow中RNN基本结构及其实现方式


  从这篇文章起,将开始学习循环神经网络(RNN)以及相关的项目。这篇文章首先会向大家介绍RNN经典的结构,以及它的几个变体。接着将在TensorFlow 中使用经典的RNN结构实现一个有趣的项目: CharRNN 。Char RNN可以对文本的字符级概率进行建模,从而生成各种类型的文本。

1 RNN的原理

1.1 经典RNN结构

  RNN的英文全称是Recurrent Neural Networks ,即循环神经网络,它是一种对序列型数据进行建模的深度模型。在学习RNN之前,先来复习基本的单层神经网络,如图12-1所示。
在这里插入图片描述
  单层网络的输入是 x x x,经过变焕 W x + b Wx+b Wx+b和激活函数f得到输出 y y y 。在实际应用中,还会遇到很多序列形的数据,如图12-2所示。
在这里插入图片描述
  例如:
  (1)自然语言处理问题。 x 1 x_1 x1可以看作是第一个单词, x 2 x_2 x2可以看作是第二个单词,依次类推。
  (2)语言处理。此时, x 1 、 x 2 、 x 3 、 . . . . . . x_1、x_2、x_3、...... x1x2x3.......是每帧的声音信号。
  (3)时间序列问题。例如每天的股票价格等。

  序列型的数据不太好用原始的神经网络处理。为了处理建模序列问题,RNN引入了隐状态h(hiddenstate)h(hiddenstate)的概念, h h h可以对序列型的数据提取特征,接着再转换为输出。如图12-3所示,先从 h 1 h_1 h1的计算开始看。
在这里插入图片描述
  图12-3 中记号的含义是:
  (1)圆圈或方块表示向量。
  (2)一个箭头表示对该向量做一次变换。如图12-3中的 h 0 h_0 h0 x 1 x_1 x1分别有一个箭头连接 h 1 h_1 h1,表示对 h 0 h_0 h0 x 1 x_1 x1各做了一次变换。图中的 U U U W W W是参数矩阵, b b b是偏置项参数。 f f f是激活函数,在经典的RNN结构中,通常使用tanh作为激活函数。

  在很多论文中也会出现类似的记号,初学的时候很容易混,但只要把握住以上两点, 可以比较轻松地理解图示背后的含义。

  如图12-4所示, h 2 h_2 h2的计算和 h 1 h_1 h1类似。要注意的是,在计算时,每一步使用的参数 U U U W W W b b b都是一样的,即每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记。
在这里插入图片描述
  接下来,如图12-5 所示, 依次计算剩下的h (使用相同的参数 U U U W W W b b b) 。
在这里插入图片描述
  这里为了方便讲解起见, 只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。

  目前的RNN还没再输出,得到输出值的方法是直接通过 h h h进行计算,如图12-6所示。

  此时使用的 V V V c c c是新的参数。通常处理的是分类问题(即输出 y 1 y_1 y1 y 2 y_2 y2……均表示类别),因此使用Softmax函数将输出转换成各个类别的概率。另外, 正如前文所说,一个箭头表示对相应的向量做一次类似于 f ( W x + b ) f(Wx+b) f(Wx+b)的变换,此处的箭头表示对 h 1 h_1 h1进行一次变换,得到输出 y 1 y_1 y1
在这里插入图片描述
  如图12-7所示,剩下输出的计算类似进行(使用和计算 y 1 y_1 y1时同样的参数 V V V c c c)。
在这里插入图片描述
  大功告成!这是最经典的RNN结构,像搭积木一样把它搭好了。它的输入是 x 1 、 x 2 、 . . . . 、 x n x_1、x_2、....、x_n x1x2....xn,输出为 y 1 、 y 2 、 . . . . 、 y n y_1、y_2、....、y_n y1y2....yn,也就是说,输入和输出序列必须是要等长的。

  由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:
  计算视频中的每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。输入为字符,输出为下一个字符的概率。这是著名的Char RNN。

  最后,给出经典RNN结构的严格数学定义,我们可以对照上面的图片进行理解。设输入为 x 1 、 x 2 、 . . . 、 x t 、 . . . 、 x T x_1、x_2、...、x_t、...、x_T x1x2...xt...xT,对应的隐状态为 h 1 、 h 2 、 . . . 、 h t 、 . . . 、 h T h_1、h_2、...、h_t、...、h_T h1h2...ht...hT,输出为 y 1 、 y 2 、 . . . 、 y t 、 . . . 、 y T y_1、y_2、...、y_t、...、y_T y1y2...yt...yT,则经典的RNN的运算过程可以表示为
h t = f ( U x t + W h t − 1 + b ) h_t=f(Ux_t+Wh_{t−1}+b) ht=f(Uxt+Wht1+b)
y t = S o f t m a x ( V h t + c ) y_t=Softmax(Vh_t+c) yt=Softmax(Vht+c)
  其中, U U U V V V W W W b b b c c c均为参数,而f表示激活函数,一般为tanh函数。

1.2 N VS 1 RNN的结构

  有的时候,问题的输入是一个序列,输出是一个单独的值而不是序列,此时应该如何建模呢?实际上,只在最后一个 h h h上进行输出变换可以了,如图12-8 所示。
在这里插入图片描述
  这种结构通常用来处理序列分类问题。 如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

  同样给出该结构的数学表示。设输入为 x 1 、 x 2 、 . . . 、 x t 、 . . . 、 x T x_1、x_2、...、x_t、...、x_T x1x2...xt...xT,对应的隐状态为 h 1 、 h 2 、 . . . 、 h t 、 . . . 、 h T h_1、h_2、...、h_t、...、h_T h1h2...ht...hT,那么运算过程为:
h t = f ( U x t + W h t − 1 + b ) h_t=f(Ux_t+Wh_{t−1}+b) ht=f(Uxt+Wht1+b)
  输出时对最后一个隐状态做运算即可
Y = S o f t m a x ( V h T + c ) Y=Softmax(Vh_T+c) Y=Softmax(VhT+c)

1.3 1 VS N RNN的结构

  输入不是序列而输出为序列的情况怎么处理?可以只在序列开始进行输入计算,如图12-9所示。
在这里插入图片描述
  还有一种结构是把输入信息X作为每个阶段的输入,如图12-10所示。
在这里插入图片描述
  图12-11省略了一些X的圆圈,是图12-10的等价表示。
在这里插入图片描述
  该表示的公式表达为
h t = f ( U x t + W h t − 1 + b ) h_t=f(Ux_t+Wh_{t−1}+b) ht=f(Uxt+Wht1+b)
y t = S o f t m a x ( V h t + c ) y_t=Softmax(Vh_t+c) yt=Softmax(Vht+c)

  这种1 VS N的结构可以处理的问题有:
  (1)从图像生成文字(image caption),此时输入的X是图像的特征,而输出的y序列是一段句子。
  (2)从类别生成语言或音乐等。

2 LSTM的原理

  前一节介绍了RNN和它的几种变体结构,本节介绍RNN的改进版:LSTM ( Long Short-Term Memory,长短期记忆网络)。一个RNN单元的输入由两部分组成,即前一步RNN单元的隐状态相当前这一步的外部输入,此外还有一个输出。从外部结构看, LSTM和RNN的输入输出一模一样,同样是在每一步接受外部输入和前一阶段的隐状态,并输出一个值。因此,可以把第1节中的每一种结构都无缝切换到LSTM,而不会产生任何问题。本节主要关注的是LSTM的内部结构以及它相对于RNN的优点

  回顾RNN的公式 h t = f ( U x t + W h t − 1 + b ) h_t=f(Ux_t+Wh_{t−1}+b) ht=f(Uxt+Wht1+b)。从这个式子中可以看出,RNN每一层的隐状态都由前一层的隐状态经过变换和激活函数得到, 反向传播求导时最终得到的导数会包含每一步梯度的连乘,这会引起梯度爆炸或梯度消失,所以RNN很难处理“长程依赖”问题,即无法学到序列中蕴含的间隔时间较长的规律。LSTM在隐状态计算时以加法代替了这里的迭代变换,可以避免梯度消失的问题, 使网络学到长程的规律

  可以用图12-12来表示RNN。
  从图12-12中的箭头可以看到, h t − 1 h_{t−1} ht1 x t x_t xt合到一起,经过了tanh函数,得到了htht,htht还会被传到下一步的RNN单元中。这对应了公式 h t = f ( U x t + W h t − 1 + b ) h_t=f(Ux_t+Wh_{t−1}+b) ht=f(Uxt+Wht1+b)。在图中,激活函数采用tanh函数。

  图12-13是用同样的方式画出的LSTM的示意图。
在这里插入图片描述
  这里的符号比较复杂,不用担心,下面会分拆开来进行讲解。在讲解之前,先给出图12-13中各个记号的含义。长方形表示对输入的数据做变换或激活函数,圆形表示逐点运算。所谓逐点运算,是指将两个形状完全相同的矩形的对应位置进行相加、相乘或其它运算。箭头表示向量会在哪里进行运算。

  和RNN有所不同,LSTM的隐状态有两部分,一部分是 h 1 h_1 h1,一部分是 C t C_t Ct C t C_t Ct是在各个步骤传递的主要信息,而图12-14中的水平线可以看作是LSTM的“主干道”。通过加法, C t C_t Ct可以无障碍地在这条主干道上传递,因此较远的梯度也可以在长程上传播。这是LSTM的核心思想。
在这里插入图片描述
  不过,每一步的信息CtCt并不是完全照搬前一步的 C t − 1 C_{t−1} Ct1,而是在 C t − 1 C_{t−1} Ct1的基础上“遗忘”掉一些内容,以及“记住”一些新内容。

  LSTM的每一个单元都有一个“遗忘门”,用来控制遗忘掉 C t − 1 C_{t−1} Ct1的那些部分。遗忘门的结构如图12-15所示。 σ σ σ是Sigmoid激活函数,它的输出在0-1之间。最终遗忘门的输出是和 C t − 1 C_{t−1} Ct1相同形状的矩阵,这个矩阵会和 C t − 1 C_{t−1} Ct1逐点相乘,决定遗忘哪些东西。显然,遗忘门输出接近0的位置的内容是要遗忘的,而接近1的部分是要保留的。遗忘门的输入时 x t x_t xt h t − 1 h_{t−1} ht1 x t xt xt是当前时刻的输入。而 h t − 1 h_{t−1} ht1为上一个时刻的隐状态。
在这里插入图片描述
  光遗忘肯定是不行,LSTM单元还得记住新东西。所以又有如图12-16所示的“记忆门”。记忆门的输入同样是 x t x_t xt h t − 1 h_{t−1} ht1,它的输出有两项,一项是 i t i_t it i t i_t it同样经过Sigmoid函数运算得到,因此值都在0-1之间,还有一项 C ˇ t \check{C}_t Cˇt,最终要“记住”的内容是 C ˇ t \check{C}_t Cˇt i t i_t it逐点相乘。
在这里插入图片描述
  “遗忘” “记忆”的过程如图12-17所示,ftft是遗忘门的输出(0 ~ 1之间),而 C ˇ t \check{C}_t Cˇt* i t i_t it就是要记住的新东西。
在这里插入图片描述
  总结一下,LSTM每一步的输入是xtxt,隐状态是 h t h_t htt和 C t C_t Ct,最终的输出通过htht进一步变换得到。在大多数情况下,RNN和LSTM都是可以相互替换的,因此再很多论文以及文档中都会看到类于“RNN(LSTM)”这样的表述,意思是两者可以相互替换。

3 TensorFlow中的RNN实现方式

  在本小节中,讲述在TensorFlow中实现RNN的主要方法,帮助大家循序渐进的梳理其中最重要的几个概念。首先使用RNNCell对RNN模型进行单步建模。RNNCell可以处理时间上的“一步”,即输入上一步的隐层状态和这一步的数据,计算这一步的输出和隐层状态。接着,TensorFlow使用tf.nn.dynamic_rnn方法在时间维度上多次运行RNNCell。最后还需要对输出结果建立损失。

3.1 实现RNN的基本单元:RNNCell

  RNNCall是TensorFlow中的RNN基本单元。它本身是一个抽象类,在本节中学习它两个可以直接使用的子类,一个是BasicRNNCell,还有一个是BasicLSTMCell,前者对应基本的RNN,后者是基本的LSTM。
学习RNNCall要重点注意三个地方;
  (1)类方法;call
  (2)类属性:state_size
  (3)类属性:output_size
  先来说下call方法。所有RNNCell的子类都会实现一个call函数。利用call函数可以实现RNN的单步计算,它的调用形式是(output,next_state)=call(input,state)。例如,对于一个已经实例化好的基本单元call(再次强调,RNNCell是抽象类,不能进行实例化,可以使它的子类BasicRNNCell或BasicLSTMCell进行实例化,得到call),初始输入x1,而初始的隐层状态为h0,可以调用(output,h1)=cell.call(x1,h0)得到当前的隐层  状态h1.接着调用(output,h2)=call.call(x2,h1)可以得到h2,依次类推。
RNNCall的类属性state_size和output_size分别规定了隐层的大小和输出向量的大小。通常是以batch形式输入数据,即input的形状为(batch_size,input_size),调用call函数时对应的隐层的形状为(batch_size,state_size),输出的形状为(batch_size,output_size)。
  在TensorFlow中定义一个基本RNN单元的方法为:

import tensorflow as tf
rnn_cell = tf.nn.rnn_cell.BasicRNNCell(num_units=128)
print(rnn_cell.state_size) #打印state_size看一下,此处应有state_size=128

  在TensorFlow中定义一个基本LSTM单元的方法为:

import tensorflow as tf
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
print(lstm_cell.state_size) #state_size=LSTMStateTuple(c=128,h=128)

  LSTM可以看作有h和c两个隐层。在TensorFlow中,LSTM基本单元的state_size由两部分组成,一部分是c,另一部分是h。在具体使用时,可以通过state.h以及state.c进行访问,下面是一个示例代码;

import tensorflow as tf
import numpy as np
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
inputs = tf.placeholder(np.float32,shape=(32,100)) #32是batch_size
h0 = lstm_cell.zero_state(32,np.float32) #通过zero_state得到一个全0的初始状态
output,h1 = lstm_cell.call(inputs,h0)
print(h1.h) #sTensor("mul_2:0", shape=(32, 128), dtype=float32)
print(h1.c) #Tensor("add_1:0", shape=(32, 128), dtype=float32)

3.2 对RNN进行堆叠:MultiRNNCall

  很多时候,单层RNN的能力有限,需要多层的RNN。将x输出第一层RNN后得到隐层状态h,这个隐层住哪个要相当于第二层RNN的输入,第二层RNN的隐层状态又相当于第三层RNN的输入,依次类推。在TensorFlow中,可以使用tf.nn.rnn_cell.MultiRNNCell函数对RNN进行堆叠,相应的示例程序如下:

import tensorflow as tf
import numpy as np

#每调用一次这个函数返回一个BasicRNNCell
def get_a_cell():
    return tf.nn.rnn_cell.BasicRNNCell(num_units=128)
#用tf.nn.rnn_cell.MultiRNNCall创建3层RNN
cell = tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)]) #3层RNN

#得到的cell实际也是RNNCell的子类
#它的state_size是(128,128,128)
#(128,128,128)的并不是128x128x128的意思,而是表示共有3个隐层状态,每个隐层状态的大小为128
print(cell.state_size)#(128,128,128)
#使用对应的call函数
inputs = tf.placeholder(np.float32,shape=(32,100)) #32是batch_size
h0 = cell.zero_state(32,np.float32) #通过zero_state得到一个全为0的初始状态
output,h1 = cell.call(inputs,h0)
print(h1) #tuple中含有3个32x128的向量

  堆叠RNN后,得到的call也是RNNCAll的子类,因此同样也有在上节中所说的call方法、state_size属性和output_size属性。

3.3 BasicRNNCell和BasicLSTMCell的output

  找到源码中BasicRNNCall的call函数实现:

def call(self,inputs,state):
    '''Most basic RNN:output = new_state = act(W * input + U * state + B).'''
    output = self._activation(_linear([inputs,state],self._num_units,True))
    return output,output

  通过“return output,output”,可以看出在BasicRNNCell中,output其实是和隐状态的值是一样的。因此,还需要额外对输出定义新的变换,才能得到真正的输出y。由于output和隐状态是一回事,所以在BasicRNNCell中,state_size永远等于output_size。
再来看看BasicLSTMCell的call函数定义(函数的最后几行):

new_c = (c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

if self._state_is_tuple:
    new_state = LSTMStateTuple(new_c,new_h)
else:
    new_state = array_ops([new_c,new_h],1)
return new_h,new_state

  只需要关注self._state_is_tuple == True的情况,因为self._state_is_tuple==false的情况将在未来被弃用。返回的隐状态是new_c和new_h的组合,而output是单独的new_h。如果处理的是分类的问题,那么还需要对new_h添加单独的Softmax层才能得到最后的分类概率输出。

3.4 使用tf.nn.dynamic_rnn展开时间维度

  对于单个的RNNCall,使用它的call函数进行运算时,只是在序列时间上前进一部。如使用x1,h0得到h1,通过x2,h1得到h2等。如果序列长度为n,要调用n次call函数,比较麻烦。对此,TensorFlow提供一个tf.nn.dynamic_rnn函数,使用该函数相当于调用了n次call函数。即通过{h0,x1,x2,…,xn}直接得到{h1,h2,…,hn}.
  具体来说,设输入数据的格式为(batch_size,time_steps,input_size),其中batch_size表示batch的大小,即一个batch中序列的个数。time_steps表示序列本身的长度,如长度为10 的句子对应的time_steps等于10.最后的input_size表示输入数据单个序列单个时间维度上固有的长度。假设已经定义好了一个RNNCell,如果要调用time_steps次该RNNCell的call函数,对应的代码是;

#inputs;shape = (batch_size,time_steps,input_size)
#cell:RNNCell
#initial_state:shape = (batch_size,cell.state_size)。初始状态。一般可以取零矩阵
outputs,state = tf.nn.dynamic_rnn(cell,inputs,initial_state=initial_state)

  此时,得到的output是time_steps步里所有的输出。它的形状为(batch_size,time_steps,cell.output_size)。state是最后一步的隐状态,它的形状为(batch_size,call.state_size)
  另外,如果输入数据形状的格式(time_steps,batch_size,input_size),那么可以在调用tf.nn.dynamic_rnn函数中设定参数time_major=True(默认情况为False),此时得到的outputs形状变成(time_steps,batch_size,input_size),而state形状不变。
tf.nn.dynamic_rnn函数介绍:

tf.nn.dynamic_rnn(
    cell,
    inputs,
    sequence_length=None,
    initial_state=None,
    dtype=None,
    parallel_iterations=None,
    swap_memory=False,
    time_major=False,
    scope=None
)

参数说明:
  cell: RNNCell的一个实例.

  inputs: RNN输入.如果time_major == False(默认), 则是一个shape为[batch_size, max_time, input_size]的Tensor,或者这些元素的嵌套元组。如果time_major ==True,则是一个shape为[max_time, batch_size, input_size]的Tensor,或这些元素的嵌套元组。

  sequence_length: (可选)大小为[batch_size],数据的类型是int32/int64向量。如果当前时间步的index超过该序列的实际长度时,则该时间步不进行计算,RNN的state复制上一个时间步的,同时该时间步的输出全部为零。

  initial_state: (可选)RNN的初始state(状态)。如果cell.state_size(一层的RNNCell)是一个整数,那么它必须是一个具有适当类型和形状的张量[batch_size,cell.state_size]。如果cell.state_size是一个元组(多层的RNNCell,如MultiRNNCell),那么它应该是一个张量元组,每个元素的形状为[batch_size,s] for s in cell.state_size。

  time_major: inputs 和outputs 张量的形状格式。如果为True,则这些张量都应该是(都会是)[max_time, batch_size, depth]。如果为false,则这些张量都应该是(都会是)[batch_size,max_time, depth]。time_major=true说明输入和输出tensor的第一维是max_time。否则为batch_size。

  使用time_major =True更有效,因为它避免了RNN计算开始和结束时的转置.但是,大多数TensorFlow数据都是batch-major,因此默认情况下,此函数接受输入并以batch-major形式发出输出.

返回值: 一对(outputs, state),其中:

  outputs: RNN输出Tensor.如果time_major == False(默认),这将是shape为[batch_size, max_time, cell.output_size]的Tensor.如果time_major ==True,这将是shape为[max_time, batch_size, cell.output_size]的Tensor.

  state: 最终的状态.一般情况下state的形状为 [batch_size, cell.output_size ]
如果cell是LSTMCells,则state将是包含每个单元格的LSTMStateTuple的元组,state的形状为[2,batch_size, cell.output_size ]

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zking~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值