解读tensorflow之rnn【转】

转自:https://blog.csdn.net/mydear_11000/article/details/52414342node

from: http://lan2720.github.io/2016/07/16/%E8%A7%A3%E8%AF%BBtensorflow%E4%B9%8Brnn/git

 

这两天想搞清楚用tensorflow来实现rnn/lstm如何作,可是google了半天,发现tf在rnn方面的实现代码或者教程都太少了,仅有的几个教程讲的又过于简单。没办法,只能亲自动手一步步研究官方给出的代码了。github

本文研究的代码主体来自官方源码ptb-word-lm。可是,若是你直接运行这个代码,能够看到warning:网络

WARNING:tensorflow:: Using a concatenated state is slower and will soon be deprecated. Use state_is_tuple=True.session

因而根据这个warning,找到了一个相关的issue:https://github.com/tensorflow/tensorflow/issues/2695
回答中有人给出了对应的修改,加入了state_is_tuple=True,笔者就是基于这段代码学习的。app

代码结构

tf的代码看多了以后就知道其实官方代码的这个结构并很差:函数

  1. graph的构建和训练部分放在了一个文件中,至少也应该分开成model.py和train.py两个文件,model.py中只有一个PTBModel类
  2. graph的构建部分所有放在了PTBModel类的constructor中

刚好看到了一篇专门讲如何构建tensorflow模型代码的blog,值得学习,来重构本身的代码吧。学习

值得学习的地方

虽然说官方给出的代码结构上有点小缺陷,可是毕竟都是大神们写出来的,值得咱们学习的地方不少,来总结一下:google

(1) 设置is_training这个标志
这个颇有必要,由于training阶段和valid/test阶段参数设置上会有小小的区别,好比test时不进行dropout
(2) 将必要的各种参数都写在config类中独立管理
这个的好处就是各种参数的配置工做和model类解耦了,不须要将大量的参数设置写在model中,那样可读性不只差,还不容易看清究竟设置了哪些超参数spa

placeholder

两个,分别命名为self._input_data和self._target,只是注意一下,因为咱们如今要训练的模型是language model,也就是给一个word,预测最有可能的下一个word,所以能够看出来,input和output是同型的。而且,placeholder只存储一个batch的data,input接收的是个word在vocabulary中对应的index【后续会将index转成dense embedding】,每次接收一个seq长度的words,那么,input shape=[batch_size, num_steps]

定义cell

在不少用到rnn的paper中咱们会看到相似的图:

这其中的每一个小长方形就表示一个cell。每一个cell中又是一个略复杂的结构,以下图:

图中的context就是一个cell结构,能够看到它接受的输入有input(t),context(t-1),而后输出output(t),好比像咱们这个任务中,用到多层堆叠的rnn cell的话,也就是当前层的cell的output还要做为下一层cell的输入,所以可推出每一个cell的输入和输出的shape是同样。若是输入的shape=(None, n),加上context(t-1)同时做为输入部分,所以能够知道

的shape=(2n, n)。

说了这么多,其实我只是想表达一个重点,就是

别小看那一个小小的cell,它并非只有1个neuron unit,而是n个hidden units

所以,咱们注意到tensorflow中定义一个cell(BasicRNNCell/BasicLSTMCell/GRUCell/RNNCell/LSTMCell)结构的时候须要提供的一个参数就是hidden_units_size。

弄明白这个以后,再看tensorflow中定义cell的代码就无比简单了:

1 2 3 4 5 
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0, state_is_tuple=True) if is_training and config.keep_prob < 1:  lstm_cell = tf.nn.rnn_cell.DropoutWrapper(  lstm_cell, output_keep_prob=config.keep_prob) cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True) 

首先,定义一个最小的cell单元,也就是小长方形,BasicLSTMCell。

问题1:为何是BasicLSTMCell

你确定会问,这个类和LSTMCell有什么区别呢?good question,文档给出的解释是这样的:

划一下重点就是倒数第二句话,意思是说这个类没有实现clipping,projection layer,peep-hole等一些lstm的高级变种,仅做为一个基本的basicline结构存在,若是要使用这些高级variant要用LSTMCell这个类。
由于咱们如今只是想搭建一个基本的lstm-language model模型,可以训练出必定的结果就好了,所以现阶段BasicLSTMCell够用。这就是为何这里用的是BasicLSTMCell这个类而不是别的什么。

问题2:state_is_tuple=True是什么


(此图偷自recurrent neural network regularization)
能够看到,每一个lstm cell在t时刻都会产生两个内部状态

,都是在t-1时刻计算要用到的。这两个状态在tensorflow中都要记录,记住这个就好理解了。

来看官方对这个的解释:

意思是说,若是state_is_tuple=True,那么上面咱们讲到的状态

就是分开记录,放在一个tuple中,若是这个参数没有设定或设置成False,两个状态就按列链接起来,成为[batch, 2n](n是hidden units个数)返回。官方说这种形式立刻就要被deprecated了,全部咱们在使用LSTM的时候要加上state_is_tuple=True

问题3:forget_bias是什么

暂时还没管这个参数的含义

DropoutWrapper

dropout是一种很是efficient的regularization方法,在rnn中如何使用dropout和cnn不一样,推荐你们去把recurrent neural network regularization看一遍。我在这里仅讲结论,

对于rnn的部分不进行dropout,也就是说从t-1时候的状态传递到t时刻进行计算时,这个中间不进行memory的dropout;仅在同一个t时刻中,多层cell之间传递信息的时候进行dropout

上图中,

时刻的输入首先传入第一层cell,这个过程有dropout,可是从时刻的第一层cell传到,,的第一层cell这个中间都不进行dropout。再从

时候的第一层cell向同一时刻内后续的cell传递时,这之间又有dropout了。

所以,咱们在代码中定义完cell以后,在cell外部包裹上dropout,这个类叫DropoutWrapper,这样咱们的cell就有了dropout功能!

能够从官方文档中看到,它有input_keep_prob和output_keep_prob,也就是说裹上这个DropoutWrapper以后,若是我但愿是input传入这个cell时dropout掉一部分input信息的话,就设置input_keep_prob,那么传入到cell的就是部分input;若是我但愿这个cell的output只部分做为下一层cell的input的话,就定义output_keep_prob。不要太方便。
根据Zaremba在paper中的描述,这里应该给cell设置output_keep_prob。

1 2 3 
if is_training and config.keep_prob < 1:  lstm_cell = tf.nn.rnn_cell.DropoutWrapper(  lstm_cell, output_keep_prob=config.keep_prob) 

Stack MultiCell

如今咱们定义了一个lstm cell,这个cell仅是整个图中的一个小长方形,咱们但愿整个网络能更deep的话,应该stack多个这样的lstm cell,tensorflow给咱们提供了MultiRNNCell(注意:multi只有这一个类,并无MultiLSTMCell之类的),所以堆叠多层只生成这个类便可。

1 
cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True) 

咱们仍是看看官方文档,

咱们能够从描述中看出,tensorflow并非简单的堆叠了多个single cell,而是将这些cell stack以后当成了一个完整的独立的cell,每一个小cell的中间状态仍是保存下来了,按n_tuple存储,可是输出output只用最后那个cell的输出。

这样,咱们就定义好了每一个t时刻的总体cell,接下来只要每一个时刻传入不一样的输入,再在时间上展开,就能获得上图多个时间上unroll graph。

initial states

接下来就须要给咱们的multi lstm cell进行状态初始化。怎么作呢?Zaremba已经告诉咱们了

We initialize the hidden states to zero. We then use the
final hidden states of the current minibatch as the initial hidden state of the subsequent minibatch
(successive minibatches sequentially traverse the training set).

也就是初始时所有赋值为0状态。


那么就须要有一个self._initial_state来保存咱们生成的全0状态,最后直接调用MultiRNNCell的zero_state()方法便可。

1 
self._initial_state = cell.zero_state(batch_size, tf.float32) 

注意:这里传入的是batch_size,我一开始没看懂为何,那就看文档的解释吧!

state_size是咱们在定义MultiRNNCell的时就设置好了的,只是咱们的输入input shape=[batch_size, num_steps],咱们刚刚定义好的cell会依次接收num_steps个输入而后产生最后的state(n-tuple,n表示堆叠的层数)可是一个batch内有batch_size这样的seq,所以就须要[batch_size,s]来存储整个batch每一个seq的状态。

embedding input

咱们预处理了数据以后获得的是一个二维array,每一个位置的元素表示这个word在vocabulary中的index。
可是传入graph的数据不能讲word用index来表示,这样词和词之间的关系就无法刻画了。咱们须要将word用dense vector表示,这也就是广为人知的word embedding。
paper中并无使用预训练的word embedding,全部的embedding都是随机初始化,而后在训练过程当中不断更新embedding矩阵的值。

1 2 3 
with tf.device("/cpu:0"):  embedding = tf.get_variable("embedding", [vocab_size, size])  inputs = tf.nn.embedding_lookup(embedding, self._input_data) 

首先要明确几点:

  1. 既然咱们要在训练过程当中不断更新embedding矩阵,那么embedding必须是tf.Variable而且trainable=True(default)
  2. 目前tensorflow对于lookup embedding的操做只能再cpu上进行
  3. embedding矩阵的大小是多少:每一个word都须要有对应的embedding vector,总共就是vocab_size那么多个embedding,每一个word embed成多少维的vector呢?由于咱们input embedding后的结果就直接输入给了第一层cell,刚才咱们知道cell的hidden units size,所以这个embedding dim要和hidden units size对应上(这也才能和内部的各类门的W和b完美相乘)。所以,咱们就肯定下来embedding matrix shape=[vocab_size, hidden_units_size]

最后生成真正的inputs节点,也就是从embedding_lookup以后获得的结果,这个tensor的shape=batch_size, num_stemps, size

input data dropout

刚才咱们定义了每一个cell的输出要wrap一个dropout,可是根据paper中讲到的,

We can see that the information is corrupted by the dropout operator exactly L + 1 times

We use the activations

to predict , since

is the number of layers
in our deep LSTM.

cell的层数一共定义了L层,为何dropout要进行L+1次呢?就是由于输入这个地方要进行1次dropout。好比,咱们设置cell的hidden units size=200的话,input embbeding dim=200维度较高,dropout一部分,防止overfitting。

1 2 
if is_training and config.keep_prob < 1:  inputs = tf.nn.dropout(inputs, config.keep_prob) 

和上面的DropoutWrapper同样,都是在is_training and config.keep_prob < 1的条件下才进行dropout。
因为这个仅对tensor进行dropout(而非rnn_cell进行wrap),所以调用的是tf.nn.dropout。

RNN循环起来!

到上面这一步,咱们的基本单元multi cell和inputs算是所有准备好啦,接下来就是在time上进行recurrent,获得num_steps每一时刻的output和states。
那么很天然的咱们能够猜想output的shape=[batch_size, num_steps, size],states的shape=[batch_size, n(LSTMStateTuple)]【state是整个seq输入完以后获得的每层的state

1 2 3 4 5 6 7 
outputs = [] state = self._initial_state with tf.variable_scope("RNN"):  for time_step in range(num_steps):  if time_step > 0: tf.get_variable_scope().reuse_variables()  (cell_output, state) = cell(inputs[:, time_step, :], state)  outputs.append(cell_output) 

以上这是官方给出的代码,我的以为不是太好。怎么办,查文档。

能够看到,有四个函数能够用来构建rnn,咱们一个个的讲。
(1) dynamic rnn

这个方法给rnn()很相似,只是它的inputs不是list of tensors,而是一整个tensor,num_steps是inputs的一个维度。这个方法的输出是一个pair,

因为咱们preprocessing以后获得的input shape=[batch_size, num_steps, size]所以,time_major=False。
最后的到的这个pair的shape正如咱们猜想的输出是同样的。

sequence_length: (optional) An int32/int64 vector sized [batch_size].表示的是batch中每行sequence的长度。

调用方法是:

1 
outputs, state = tf.nn.dynamic_rnn(cell, inputs, sequence_length=..., initial_state=state) 

state是final state,若是有n layer,则是final state也有n个元素,对应每一层的state。

(2)tf.nn.rnn
这个函数和dynamic_rnn的区别就在于,这个须要的inputs是a list of tensor,这个list的长度是num_steps,也就是将每个时刻的输入切分出来了,tensor的shape=[batch_size, input_size]【这里的input每个都是word embedding,所以input_size=hidden_units_size】

除了输出inputs是list以外,输出稍有差异。

能够看到,输出也是一个长度为T(num_steps)的list,每个output对应一个t时刻的input(batch_size, hidden_units_size),output shape=[batch_size, hidden_units_size]

(3)state_saving_rnn
这个方法能够接收一个state saver对象,这是和以上两个方法不一样之处,另外其inputs和outputs也都是list of tensors。

(4)bidirectional_rnn
等研究bi-rnn网络的时候再讲。

以上介绍了四种rnn的构建方式,这里选择dynamic_rnn.由于inputs中的第2个维度已是num_steps了。

获得output以后传到下一层softmax layer

既然咱们用的是dynamic_rnn,那么outputs shape=[batch_size, num_steps, size],而接下来须要将output传入到softmax层,softmax层并无显式地使用tf.nn.softmax函数,而是只是计算了wx+b获得logits(其实是同样的,softmax函数仅仅只是将logits再rescale到0-1之间)

计算loss

获得logits后,用到了nn.seq2seq.sequence_loss_by_example函数来计算“所谓的softmax层”的loss。这个loss是整个batch上累加的loss,须要除上batch_size,获得平均下来的loss,也就是self._cost。

1 2 3 4 5 6 
loss = tf.nn.seq2seq.sequence_loss_by_example(  [logits],  [tf.reshape(self._targets, [-1])],  [tf.ones([batch_size * num_steps])]) self._cost = cost = tf.reduce_sum(loss) / batch_size self._final_state = state 

求导,定义train_op

若是is_training=False,也就是仅valid or test的话,计算出loss这一步也就终止了。之因此要求导,就是train的过程。因此这个地方对is_training进行一个判断。

1 2 
if not is_training:  return 

若是想在训练过程当中调节learning rate的话,生成一个lr的variable,可是trainable=False,也就是不进行求导。

1 
self._lr = tf.Variable(0.0, trainable=False) 

gradient在backpropagate过程当中,很容易出现vanish&explode现象,尤为是rnn这种back不少个time step的结构。
所以都要使用clip来对gradient值进行调节。
既然要调节了就不能简单的调用optimizer.minimize(loss),而是须要显式的计算gradients,而后进行clip,将clip后的gradient进行apply。
官方文档说明了这种操做:

并给出了一个例子:

1 2 3 4 5 6 7 8 9 10 11 12 
# Create an optimizer. opt = GradientDescentOptimizer(learning_rate=0.1)  # Compute the gradients for a list of variables. grads_and_vars = opt.compute_gradients(loss, <list of variables>)  # grads_and_vars is a list of tuples (gradient, variable). Do whatever you # need to the 'gradient' part, for example cap them, etc. capped_grads_and_vars = [(MyCapper(gv[0]), gv[1]) for gv in grads_and_vars]  # Ask the optimizer to apply the capped gradients. opt.apply_gradients(capped_grads_and_vars) 

模仿这个代码,咱们能够写出以下的伪代码:

1 2 3 4 5 6 7 8 
optimizer = tf.train.AdamOptimizer(learning_rate=self._lr)  # gradients: return A list of sum(dy/dx) for each x in xs. grads = optimizer.gradients(self._cost, <list of variables>) clipped_grads = tf.clip_by_global_norm(grads, config.max_grad_norm)  # accept: List of (gradient, variable) pairs, so zip() is needed self._train_op = optimizer.apply_gradients(zip(grads, <list of variables>)) 

能够看到,此时就差一个<list of variables>不知道了,也就是须要对哪些variables进行求导。
答案是:trainable variables
所以,咱们获得

1 
tvars = tf.trainable_variables() 

用tvars带入上面的代码中便可。

how to change Variable value

使用tf.assign(ref, value)函数。ref应该是个variable node,这个assign是个operation,所以须要在sess.run()中进行才能生效。这样以后再调用ref的值就发现改变成新值了。
在这个模型中用于改变learning rate这个variable的值。

1 2 
def assign_lr(self, session, lr_value):  session.run(tf.assign(self.lr, lr_value)) 

run_epoch()

Tensor.eval()


好比定义了一个tensor x,x.eval(feed_dict={xxx})就能够获得x的值,而不用sess.run(x, feed_dict={xxx})。返回值是一个numpy array。

遗留问题

1 2 3 4 5 6 7 
state = m.initial_state.eval() for step, (x, y) in enumerate(reader.ptb_iterator(data, m.batch_size,  m.num_steps)): cost, state, _ = session.run([m.cost, m.final_state, eval_op],  {m.input_data: x,  m.targets: y,  m.initial_state: state}) 

为何feed_dict中还须要传入initial_statel?

相关文章
相关标签/搜索