解读tensorflow之rnn

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

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

本文研究的代码主体来自官方源码ptb-word-lm。但是,如果你直接运行这个代码,可以看到warning:

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

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

代码结构

tf的代码看多了之后就知道其实官方代码的这个结构并不好:

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

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

值得学习的地方

虽说官方给出的代码结构上有点小缺陷,但是毕竟都是大神们写出来的,值得我们学习的地方很多,来总结一下:

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

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)同时作为输入部分,因此可以知道 W

的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时刻都会产生两个内部状态 ct

ht

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

来看官方对这个的解释:

意思是说,如果state_is_tuple=True,那么上面我们讲到的状态 ct

ht

就是分开记录,放在一个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

上图中, xt2

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

时候的第一层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  hLt

to predict  yt  , since  L

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?

举报
kcheng_
  • kcheng_

    2017-03-01 16:5510楼
  • 我认为LSTM的hidden size和词向量的embedding dim可以不一样。如果用H表示hidden size,用D表示embedding dim,那么词向量输入后会与一个矩阵相乘,这个矩阵是D行H列的,相乘的结果就是变成H维向量。换句话说,并不是词向量的D直接用于运算。
kcheng_
  • kcheng_

    2017-03-01 16:569楼
  • 我认为LSTM的hidden size和词向量的embedding dim可以不一样。如果用H表示hidden size,用D表示embedding dim,那么词向量输入后会与一个矩阵相乘,这个矩阵是D行H列的,相乘的结果就是变成H维向量。换句话说,并不是词向量的D直接用于运算。
kcheng_
  • kcheng_

    2017-03-01 16:558楼
  • 我认为LSTM的hidden size和词向量的embedding dim可以不一样。如果用H表示hidden size,用D表示embedding dim,那么词向量输入后会与一个矩阵相乘,这个矩阵是D行H列的,相乘的结果就是变成H维向量。换句话说,并不是词向量的D直接用于运算。

相关文章推荐

使用TensorFlow实现RNN模型入门篇1

最近在看RNN模型,为简单起见,本篇就以简单的二进制序列作为训练数据,而不实现具体的论文仿真,主要目的是理解RNN的原理和如何在TensorFlow中构造一个简单基础的模型架构。其中代码参考了这篇博客...

TensorFlow中RNN样例代码详解

关于RNN的理论部分已经在上一篇文章中讲过了,本文主要讲解RNN在TensorFlow中的实现。与theano不同,TensorFlow在一个更加抽象的层次上实现了RNN单元,所以调用tensorfl...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

tf19: 预测铁路客运量

以前做的练习还没有涉及过时间序列数据(洋文Time Series Data),一个最明显的例子是股票价格。 时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化状...

Tensorflow RNN源代码解析笔记1:RNNCell的基本实现

前言本系列主要主要是记录下Tensorflow在RNN实现这一块的相关代码,不做详细解释,主要是翻译加笔记。RNNCell在Tensorflow中,定义了一个RNNCell的抽象类,具体的所有不同类型...
  • MebiuW
  • MebiuW
  • 2017-03-07 21:58
  • 2237

tensorflow笔记 :常用函数说明

本文章内容比较繁杂,主要是一些比较常用的函数的用法,结合了网上的资料和源码,还有我自己写的示例代码。建议照着目录来看。1.矩阵操作1.1矩阵生成这部分主要将如何生成矩阵,包括全0矩阵,全1矩阵,随机数...

TensorFlow 从入门到精通(五):使用 TensorFlow 实现 RNN

# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0...

Tensorflow - Tutorial (7) : 利用 RNN/LSTM 进行手写数字识别

1. 常用类class tf.contrib.rnn.BasicLSTMCellBasicLSTMCell 是最简单的一个LSTM类,没有实现clipping,projection layer,pee...

Tensorflow RNN源代码解析笔记2:RNN的基本实现

1 前言话说上回说到了RNNCell的基本实现,本来按照道理来说,应该介绍LSTM GRU的,但是奈何这些于我而言也是不太熟悉(然后我又悲伤的想到了那个电话,哎),所以不如先说说RNN网络的实现吧,毕...
  • MebiuW
  • MebiuW
  • 2017-03-16 15:46
  • 2694

深度学习笔记(五):LSTM

看到一篇讲LSTM非常清晰的文章,原文来自Understanding LSTM Networks , 译文来自理解LSTM网络Recurrent Neural Networks人类并不是每时每刻都从一...

使用TensorFlow实现RNN模型入门篇2--char-rnn语言建模模型

这是使用tf实现RNN模型的第二篇,上次用很简单的例子实现了一个简单的RNN用于解释其原理,这次我们开始结合NLP尝试构建一个char-rnn的语言建模模型。和CNN的入门篇一样,我们这里也直接来分析...

RNN代码解读之char-RNN with TensorFlow(util.py)

其实在看这里的代码的时候感觉是最轻松的,但同时又是最费时间的。轻松是因为这里的代码大体上做了些什么都比较好懂,费时间是因为里面涉及了很多python的运算操作,一层套一层,如果不是非常熟练的话(比如说...

RNN代码解读之char-RNN with TensorFlow(train.py)

前面我们看完了model.py的代码,大家可能会产生一个疑惑,那就是模型的参数是怎么传进去的呢?在训练的时候怎么从以往的checkpoint继续训练呢?其实这些很简单,都在train.py里实现,代码...

RNN时间序列预测(1)-Tensorflow入门,MNIST学习

Title Content 原文 https://github.com/jikexueyuanwiki/tensorflow-zh/blob/master/tex_pdf/tensorflo...

TensorFlow——RNN模型

本文介绍TensorFlow官方提供的关于循环神经网络的一个模板。该模型是Zaremba论文中的方法应用在语言模型的实现。首先介绍一下TensorFlow有关RNN的代码布局,其实能用到的RNN文件就...

tensorflow Examples:<4>实现RNN

# -*- coding: utf-8 -*- # @Author: xiaodong # @Date: 2017-06-19 21:26:12 # @Last Modified by: xi...

tensorflow:用dynamic_rnn处理不定长序列,对序列做padding处理

1. 数据处理(padding zero) 首先,我有一个list类型的数据集,按最长将序列的长度存储,不足长度的后面padding zero。将数据集存为ndarray类型的矩阵: impo...

[TensorFlow]入门学习笔记(5)-循环神经网络RNN

前言关于循环神经网络的理论推导和证明,推荐去看论文。参考资料。 https://colah.github.io/posts/2015-08-Understanding-LSTMs/ https://r...

Tensorflow学习: RNN-LSTM应用于MNIST数据分类

本文内容: 1. RNN 2. 转自周莫烦的youtube视频,代码为原作者的Github# View more python learning tutorial on my Youtube an...

tf7: RNN—古诗词

原文链接: http://blog.topspeedsnail.com/archives/tag/tensorflow RNN不像传统的神经网络-它们的输出输出是固定的,而RNN允许我们输入输出向量...

tensorflow之RNN

一、概念 循环神经网络(Recurrent Neural Network,RNN): 它能结合数据点之间的特定顺序和幅值大小等多个特征,来处理序列数据。更重要的是输入序列可以是任意长度的。...

TensorFlow (RNN)深度学习 双向LSTM(BiLSTM)+CRF 实现 sequence labeling 序列标注问题 源码下载

在TensorFlow (RNN)深度学习下 BiLSTM+CRF 跑 sequence labeling  双向LSTM+CRF跑序列标注问题 去年底样子一直在做NLP相关task,是个关于序列...

Tensorflow之RNN实践(一)

最近学tensorflow里面的RNN,tensorflow框架下给RNN封装了很多方便利用的函数模块。RNN在自然语言处理中使用的较为广泛,https://github.com/tensorflow...

Tensorflow Ubuntu16.04上安装及CPU运行Tensorboard、CNN、RNN图文教程

Tensorflow Ubuntu16.04上安装及CPU运行tensorboard、CNN、RNN图文教程

tensorflow 循环神经网络RNN

在 tensorflow 中实现 LSTM 结构的循环神经网络的前向传播过程,即使用 BasicLSTMCell # 定义一个 LSTM 结构,LSTM 中使用的变量会在该函数中自动被声明 lstm ...

tf3: RNN—mnist识别

原文链接: http://blog.topspeedsnail.com/archives/tag/tensorflow 前文《使用Python实现神经网络》和《TensorFlow练习1: 对评论...

tensorflow1.1/利用rnn回归分析

环境:tensorflow1.1,python3,matplotlib2.02#coding:utf-8 from tensorflow.contrib import rnn import tenso...

TensorFlow实战——RNN(LSTM)——预测sin函数

http://blog.csdn.net/u011239443/article/details/73650806关于LSTM可以参阅:http://blog.csdn.net/u011239443/a...

RNN入门详解及TensorFlow源码实现--深度学习笔记

RNN入门详解及TensorFlow源码实现–深度学习笔记一、RNN简介RNNs的目的使用来处理序列数据。在传统的神经网络模型中,是从输入层到隐含层再到输出层,层与层之间是全连接的,每层之间的节点是无...

RNN構造二進制加法器的tensorflow實現

RNN(循环神经网络)对于序列数据的建模有得天独厚的优势,相比一般的前馈神经网络,它对于历史输入数据具有一定的记忆性,能通过隐变量记录历史信息。本文利用RNN来学习二进制加法的进位规则。异国友人写了一...

使用TensorFlow动手实现一个Char-RNN

Char-RNN非常有意思,想要深入了解最好的方式就是用自己最喜欢的工具动手实现一遍。

基于Char-RNN Language Model进行文本生成(Tensorflow生成唐诗)

上一篇文章利用CharRNN进行语言模型的训练,语言模型的本意就是为了判断一个句子的概率。在文本生成领域就可以根据当前词预测下一个词,因此大有用途。比如在各种科技网站上随处可见的生成唐诗,歌词,小说,...

tensorflow1.1/RNN预测

环境:tensorflow1.1,python3,matplotlib2.02#coding:utf-8 from tensorflow.contrib import rnn import tenso...

深度学习(07)_RNN-循环神经网络-02-Tensorflow中的实现

关于基本的RNN和LSTM的概念和BPTT算法可以查看这里 本文个人博客地址:http://lawlite.me/2017/06/16/RNN-%E5%BE%AA%E7%8E%AF%E7%A5%9E%...

tensorflow高阶教程:tf.dynamic_rnn

引言TensorFlow很容易上手,但是TensorFlow的很多trick却是提升TensorFlow心法的法门,之前说过TensorFlow的read心法,现在想说一说TensorFlow在RNN...

tf8:RNN—生成音乐

原文链接: http://blog.topspeedsnail.com/archives/tag/tensorflow 我在GitHub看到了一个使用RNN生成经典音乐的项目:biaxial-rnn...

基于循环神经网络实现基于字符的语言模型(char-level RNN Language Model)-tensorflow实现

前面几章介绍了卷积神经网络在自然语言处理中的应用,这是因为卷积神经网络便于理解并且易上手编程,大多教程(比如tensorflow的官方文档就先CNN再RNN)。但RNN的原理决定了它先天就适合做自然语...

Tensorflow Ubuntu16.04上安装及CPU运行tensorboard、CNN、RNN图文教程

Ubuntu16.04系统安装 Win7 U盘安装Ubuntu16.04 双系统详细教程参看博客:http://blog.csdn.net/coderjyf/article/deta...

tensorflow73 使用RNN生成古诗和藏头诗

01 环境https://github.com/5455945/tensorflow_demo.git# 源码地址:https://github.com/5455945/tensorflow_demo...

Tensorflow-3-使用RNN生成中文小说

这篇文章不涉及RNN的基本原理,只是从选择数据集开始,到最后生成文本,展示一个RNN使用实例的过程。对于深度学习的应用者,最应该关注的除了算法和模型,还应该关注如何预处理好自己的数据,合理降噪,以及如...

tensorflow RNN实例

本实例基于谷歌tensorflow官网RNN tutorial,Basic LSTM,侧重代码分析,包括数据预处理。read.py_read_words函数读取ptb文件,按utf-8格式读入,换行符...

TensorFlow RNN 教程和代码

分析: 看 TensorFlow 也有一段时间了,准备按照 GitHub 上的教程,敲出来,顺便整理一下思路。 RNN部分 定义参数,包括数据相关,训练相关。 ...

Tensorflow学习笔记--RNN精要及代码实现

RNN介绍 代码实现

tensorflow构建RNN识别mnist手写数字

#coding:utf-8 import tensorflow as tf import input_data#加载mnist数据集 mnist = input_data.read_data_sets...

TensorFlow中RNN网络的实现和关键参数选择

主旨TensorFlow提供了方便的API用于快速搭建和实现RNN网络。但是在实际工作中,这些API的关键参数选择令人迷惑,在没有时间详细阅读Tensorflow引用论文和源代码的条件下,仅仅靠网络上...

TensorFlow实战——RNN

http://blog.csdn.net/u011239443/article/details/73136866RNN循环神经网络(RNN)的特殊的地方在于它保存了自己的状态,每次数据输入都会更新状态...

tensorflow70 《深度学习原理与TensorFlow实战》05 RNN能说会道 01 正弦序列预测

01 基本环境#《深度学习原理与TensorFlow实战》05 RNN能说会道 # 书源码地址:https://github.com/DeepVisionTeam/TensorFlowBook.git...

tensorflow72 《深度学习原理与TensorFlow实战》05 RNN能说会道 03 对话机器人(chatbot)

01 基本环境#《深度学习原理与TensorFlow实战》05 RNN能说会道 # 书源码地址:https://github.com/DeepVisionTeam/TensorFlowBook.git...

tensorflow71 《深度学习原理与TensorFlow实战》05 RNN能说会道 02语言模型

01 基本信息#《深度学习原理与TensorFlow实战》05 RNN能说会道 # 书源码地址:https://github.com/DeepVisionTeam/TensorFlowBook.git...

tensorflow dynamic_rnn与static_rnn使用注意

不同有好多,例如:输入输出输入输出要格外注意,敲代码的时候,这个和我们关系最大 帖俩段代码,注意其中的输入输出这个是static_rnndef lstm_model(x,y): # x = tf...

RNN时间序列预测(2)-Tensorflow入门,RNN操作

Title Content 原文 https://www.tensorflow.org/tutorials/recurrent 说明 因为要做RNN的时间序列分析,需要用到LSTM模...

RNN-RBM for music composition 网络架构及程序解读

RNN(recurrent  neural network)是神经网络的一种,主要用于时序数据的分析,预测,分类等。 RNN的general介绍请见下一篇文章《Deep learning F...

RNN-RBM for music composition 网络架构及程序解读

RNN(recurrent neural network)是神经网络的一种,主要用于时序数据的分析,预测,分类等。 RNN的general介绍请见下一篇文章《Deep learning From I...

tensorflow从0开始(4)——解读mnist程序

前言 由于图像的问题学习机器学习,选择TensorFlow,但似乎直接从ImageNet的例子出发,却发现怎么都找不到头(python也不会,机器学习也不懂),但根据我以往的经验,遇到这种情况,又没有...

基于Docker的TensorFlow机器学习框架搭建和实例源码解读

http://blog.csdn.net/dream_an/article/details/55520205 概述:基于Docker的TensorFlow机器学习框架搭建和实例源码解...

tensorflow学习(3):解读mnist_experts例子,训练保存模型并tensorboard可视化

前言官网的mnist例子讲解的很清楚了,要想深入理解这个网络结构到底干了什么,还是需要好好入门一下深度学习的基础知识。好好看看Michael Nielsen大神写的这本书:Neural Network...

Deep Learning-TensorFlow (11) CNN卷积神经网络_解读 VGGNet

VGGNet 是牛津大学计算机视觉组(Visual Geometry Group)和 Google DeepMind 公司的研究员一起研发的的深度卷积神经网络,在 ILSVRC 2014 上取得了第二...

基于Docker的TensorFlow机器学习框架搭建和实例源码解读

概述:基于Docker的TensorFlow机器学习框架搭建和实例源码解读,TensorFlow作为最火热的机器学习框架之一,Docker是的容器,可以很好的结合起来,为机器学习或者科研人员提供便捷的...
没有更多内容了, 返回首页

深度学习与自然语言处理之五:从RNN到LSTM

本文介绍了RNN和LSTM的基本技术原理及其在自然语言处理的应用。

RNN以及LSTM的介绍和公式梳理

转自:http://blog.csdn.net/Dark_Scope/article/details/47056361 前言 好久没用正儿八经地写博客了,csdn居然也有了markdown...

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

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

本文研究的代码主体来自官方源码ptb-word-lm。但是,如果你直接运行这个代码,可以看到warning:

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

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

代码结构

tf的代码看多了之后就知道其实官方代码的这个结构并不好:

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

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

值得学习的地方

虽说官方给出的代码结构上有点小缺陷,但是毕竟都是大神们写出来的,值得我们学习的地方很多,来总结一下:

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

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)同时作为输入部分,因此可以知道 W

的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时刻都会产生两个内部状态 ct

ht

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

来看官方对这个的解释:

意思是说,如果state_is_tuple=True,那么上面我们讲到的状态 ct

ht

就是分开记录,放在一个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

上图中, xt2

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

时候的第一层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  hLt

to predict  yt  , since  L

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?

猜你喜欢

转载自blog.csdn.net/Trasper1/article/details/78063053