学习笔记:RNN基本结构与Char RNN文本生成

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

1 RNN的原理

1.1 经典RNN结构

RNN的英文全称是Recurrent Neural Networks ,即循环神经网络,它是一种对序列型数据进行建模的深度模型。在学习RNN之前,先来复习基本的单层神经网络,如图12-1所示。
这里写图片描述

单层网络的输入是x,经过变焕Wx+b和激活函数f得到输出y 。在实际应用中,还会遇到很多序列形的数据,如图12-2所示。
这里写图片描述

例如:

  • 自然语言处理问题。 x1 x 1 可以看作是第一个单词, x2 x 2 可以看作是第二个单词,依次类推。
  • 语言处理。此时, x1x2x3...... x 1 、 x 2 、 x 3 、 . . . . . . 是每帧的声音信号。
  • 时间序列问题。例如每天的股票价格等。

序列型的数据不太好用原始的神经网络处理。为了处理建模序列问题,RNN引入了隐状态 h(hiddenstate) h ( h i d d e n s t a t e ) 的概念,h可以对序列型的数据提取特征,接着再转换为输出。如图12-3所示,先从 h1 h 1 的计算开始看。
这里写图片描述

图12-3 中记号的含义是:

  • 圆圈或方块表示向量。
  • 一个箭头表示对该向量做一次变换。如图12-3中的 h0 h 0 x1 x 1 分别有一个箭头连接 h1 h 1 ,表示对 h0 h 0 x1 x 1 各做了一次变换。图中的U、W是参数矩阵, b b 是偏置项参数。f是激活函数,在经典的RNN结构中,通常使用tanh作为激活函数

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

如图12-4所示, h2 h 2 的计算和 h1 h 1 类似。要注意的是,在计算时,每一步使用的参数U、W、b都是一样的,即每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记
这里写图片描述

接下来,如图12-5 所示, 依次计算剩下的h (使用相同的参数U、W、b) 。
这里写图片描述

这里为了方便讲解起见, 只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。

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

此时使用的V和c是新的参数。通常处理的是分类问题(即输出 y1 y 1 y2 y 2 …..均表示类别),因此使用Softmax函数将输出转换成各个类别的概率。另外, 正如前文所说,一个箭头表示对相应的向量做一次类似于 f(Wx+b) f ( W x + b ) 的变换,此处的箭头表示对 h1 h 1 进行一次变换,得到输出 y1 y 1
这里写图片描述

如图12-7所示,剩下输出的计算类似进行(使用和计算 y1 y 1 时同样的参数V和c)。
这里写图片描述

大功告成!这是最经典的RNN结构,像搭积木一样把它搭好了。它的输入是 x1x2....xn x 1 、 x 2 、 . . . . x n ,输出为 y1y2....yn y 1 、 y 2 、 . . . . y n ,也就是说,输入和输出序列必须是要等长的。

由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:

  • 计算视频中的每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。
  • 输入为字符,输出为下一个字符的概率。这是著名的Char RNN。在这篇文章中,将会在TensorFlow中实现Char RNN并生成文本。

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

1.2 N VS 1 RNN的结构

有的时候,问题的输入是一个序列,输出是一个单独的值而不是序列,此时应该如何建模呢?实际上,只在最后一个h上进行输出变换可以了,如图12-8 所示。
这里写图片描述

这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

同样给出该结构的数学表示。设输入为 x1x2...xt...xT x 1 、 x 2 、 . . . 、 x t 、 . . . x T ,对应的隐状态为 h1h2...ht...hT h 1 、 h 2 、 . . . 、 h t 、 . . . 、 h T ,那么运算过程为:
ht=f(Uxt+Wht1+b) h t = f ( U x t + W h t − 1 + b )

输出时对最后一个隐状态做运算即可
Y=Softmax(VhT+c) Y = S o f t m a x ( V h T + c )

1.3 1 VS N RNN的结构

输入不是序列而输出为序列的情况怎么处理?可以只在序列开始进行输入计算,如图12-9所示。
这里写图片描述

还有一种结构是把输入信息X作为每个阶段的输入,如图12-10所示。
这里写图片描述

图12-11省略了一些X的圆圈,是图12-10的等价表示。
这里写图片描述

该表示的公式表达为
ht=f(UX+Wht1+b) h t = f ( U X + W h t − 1 + b )
yt=Softmax(Vht+c) y t = S o f t m a x ( V h t + c )

这种1 VS N的结构可以处理的问题有:

  • 从图像生成文字(image caption),此时输入的X是图像的特征,而输出的y序列是一段句子。
  • 从类别生成语言或音乐等。

2 LSTM的原理

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

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

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

图12-13是用同样的方式画出的LSTM的示意图。
这里写图片描述

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

和RNN有所不同,LSTM的隐状态有两部分,一部分是 h1 h 1 ,一部分是 Ct C t Ct C t 是在各个步骤传递的主要信息,而图12-14中的水平线可以看作是LSTM的“主干道”。通过加法, Ct C t 可以无障碍地在这条主干道上传递,因此较远的梯度也可以在长程上传播。这是LSTM的核心思想。
这里写图片描述

不过,每一步的信息 Ct C t 并不是完全照搬前一步的 Ct1 C t − 1 ,而是在 Ct1 C t − 1 的基础上“遗忘”掉一些内容,以及“记住”一些新内容。

LSTM的每一个单元都有一个“遗忘门”,用来控制遗忘掉 Ct1 C t − 1 的那些部分。遗忘门的结构如图12-15所示。 σ σ 是Sigmoid激活函数,它的输出在0-1之间。最终遗忘门的输出是和 Ct1 C t − 1 相同形状的矩阵,这个矩阵会和 Ct1 C t − 1 逐点相乘,决定遗忘哪些东西。显然,遗忘门输出接近0的位置的内容是要遗忘的,而接近1的部分是要保留的。遗忘门的输入时 xt x t ht1 h t − 1 xt x t 是当前时刻的输入。而 ht1 h t − 1 为上一个时刻的隐状态。
这里写图片描述

光遗忘肯定是不行,LSTM单元还得记住新东西。所以又有如图12-16所示的“记忆门”。记忆门的输入同样是 xt x t ht1 h t − 1 ,它的输出有两项,一项是 it i t it i t 同样经过Sigmoid函数运算得到,因此值都在0-1之间,还有一项 C~t C ~ t ,最终要“记住”的内容是 C~t C ~ t it i t 逐点相乘。
这里写图片描述

“遗忘” “记忆”的过程如图12-17所示, ft f t 是遗忘门的输出(0~1之间),而 C~tit C ~ t ∗ i t 就是要记住的新东西。
这里写图片描述

最后,还需要一个“输出门”,用于输出内容。这里说是输出,其实是去计算另一个隐状态 ht h t 的值,真正的输出(如类别)需要通过 ht h t 做进一步运算得到。输出门的结构如图12-18所示。同样是根据 xt x t ht1 h t − 1 计算, ot o t 中每一个数值在0~1之间, ht h t 通过 ottanh(Ct) o t ∗ t a n h ( C t ) 得到。
这里写图片描述

总结一下,LSTM每一步的输入是 xt x t ,隐状态是 ht h t Ct C t ,最终的输出通过 ht h t 进一步变换得到。在大多数情况下,RNN和LSTM都是可以相互替换的,因此再很多论文以及文档中都会看到类于“RNN(LSTM)”这样的表述,意思是两者可以相互替换。

3 Char RNN 的原理

Char RNN是用于学习RNN的一个非常好的例子。它使用的是RNN最经典的N vs N的模型, 即输入是长度为N的序列,输出是与之长度相等的序列。Char RNN可以用来生成文章、诗歌甚至是代码。在学习Char RNN之前先来看下“N vs N”的经典RNN的结构,如图12-19所示。
这里写图片描述

对于Char RNN,输入序列是句子中的字母,输出一次是该输入的下一个字母,换句话说,是用已经输入的字母去预测下一个字母的概率。如一个简单的英文句子Hello!输入序列是{H,e,l,l,o},输出序列依次是{e,l,l,o,!}。注意到这两个序列是等长的,因此可以用N VS N RNN来建模,如图12-20所示。

在测试时,应该怎样生成序列呢?方法是首先选择一个 x1 x 1 当作起始字符,使用训练好的模型得到对应的下一个字符的概率。根据这个概率,选择一个字符输出,并将该字符当作下一步的 x2 x 2 输入模型,再生成一个字符。以此类推,可以生成任意长度的文字。

这里写图片描述

使用独热向量来表示字母,然后 依次输入网络。假设一共有26个字母,那么字母a的表示为第一位为1,其他25位都是0,即(1,0,0,…,0),字母b的表示是第二位为1,其他25位都是0,即(0,1,0,…,0)。输出相当于一个26类分类问题,因此每一步输出的向量也是26维,每一维代表对应字母的概率,最后损失使用交叉熵可以直接得到。在实际模型中,由于字母有大小写之分以及其他标点符号,因此总共的类别墅会比26多。

最后,在对中文建模时,为了简单起见,每一步输入模型的是一个汉字。相对于字母来说,汉字种类比较多,可能会导致模型过大,对此有以下两种优化方法:

  • 取常用的N个汉字。将剩下的汉字变成单独一类,并用一个特殊的字符进行标注。
  • 在输入时,可以加入一层embedding层,这个embedding层可以将汉字转换为较为稠密的表示,它可以代替稀疏的独热表示,取得更好的效果。之所以对字母不适用embedding,是因为单个字母不具备任何含义,只需要使用独热表示即可。而单个汉字还是具有一定的实际意义的,因此可以使用embedding将其映射到一个较为稠密的空间。embedding的参数可以直接从数据中学到,具体的实现方法可以参考下面小节重处理输入数据部分的代码。

中文汉字的输出层和之前处理英文字母时是一样的,都相当于N类分类问题。

4 TensorFlow中的RNN实现方式

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

4.1 实现RNN的基本单元:RNNCell

RNNCell是TensorFlow中的RNN基本单元。它本身是一个抽象类,在本节中学习他两个可以直接使用的子类,一个是BasicRNNCell,还有一个是BasicLSTMCell,前者对应基本的RNN,后者是基本的LSTMLSTM源码地址
学习RNNCell要重点关注三个地方:
- 类方法call
- 类属性state_size
- 类属性output_size

先来说下call方法。所有RNNCell的子类都会实现一个call函数。利用call函数可以实现RNN的单步计算,它的调用形式为(output,next_state)=call(input,state)。例如,对于已经实例化好的基本单元cell(再次强调,RNNCell是抽象类不能进行实例化,可以使用它的子类BasicRNNCell或BasicLSTMCell进行实例化,得到cell),初始输入为 x1 x 1 ,而初始的隐层状态为 h0 h 0 ,可以调用(output,h1)=cell.call( x1 x 1 h0 h 0 )得到当前的隐层状态 h1 h 1 。接着调用(output2,h2)=cell.call( x2 x 2 h1 h 1 )可以得到 h2 h 2 ,依此类推。

RNNCell的类属性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) # 打印出sytate_szie看一下,此处应有state_size=128

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

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)

正如第2节中所说,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_szie
h0 = lstm_cell.zero_state(32, np.float32) # 通过zero_state得到一个全0的初始状态
output, h1 = lstm_cell.call(inputs, h0)

print(h1.h) # shape=(32, 128)
print(h1.c) # shape=(32, 128)

4.2 对RNN进行堆叠:MutiRNNCell

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

import tensorflow as tf
import numpy as np

# 每调用一次这个函数返回一个BasicRNNCell
def get_a_cell():
    return tf.nn.rnn_cell.BasicRNNCell(num_units=128)
# 用tf.MultiRNNCellnn.rnn_cell 创建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) # 通过zaro_state得到一个全0的初始状态
output, h1 = cell.call(inputs, h0)
print(h1) # tuple中含有3个32x128的向量

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

4.3 BasicRNN和BasicLSTMCell的output

在第4.1节和第4.2节中,有意省略了调用call函数后得到output的介绍,先通过图12-19回忆RNN的基本结构。
这里写图片描述

将图12-19与TensorFlow的BasicRNNCell对照来看,h对应了BasicRNNCell的state_size。那么,y是不是对应了BasicRNNCell的output_size呢?答案是否定的

找到源码中的BasicRNNCell的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。TensorFlow是出于尽量精简的目的来定义BasicRNNCell的,所以省略了输出参数,这里一定要弄清楚它和图中原始RNN定义的联系与区别。

再来看BasicLSTMCell的call函数定义(函数的最后几行):

new_c = add(multiply(c, sigmoid(add(f, forget_bias_tensor))),
            multiply(sigmoid(i), self._activation(j)))
new_h = multiply(self._activation(new_c), sigmoid(o))

if self._state_is_tuple:
  new_state = LSTMStateTuple(new_c, new_h)
else:
  new_state = array_ops.concat([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层才能得到最后的分类概率输出。

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

对于单个的RNNCell,使用它的call函数进行运算时,只是在序列时间上前进了一步。如果使用 x1 x 1 h0 h 0 得到 h1 h 1 ,通过 x2 x 2 h1 h 1 得到 h2 h 2 等。如果序列长度为n,要调用n次call函数,比较麻烦。对此,TensorFlow提供了一个tf.nn.dynamic_rnn函数,使用该函数相当于调用了n次call函数。即通过 h0,x1,x2,...,xn h 0 , x 1 , x 2 , . . . , x n 直接得到 h1,h2,...,hn h 1 , h 2 , . . . , h n

具体来说,设输入数据的格式为(batch_size,time_steps,input_size),其中batch_size表示baytch的大小,即一个batch中序列的格式。time_steps表示序列本身的长度,如在Char RNN中,长度为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).初始状态一般可以取0矩阵
outputs, state=tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)

此时,得到的outputs是time_steps步里所有的输出。它的形状为
(batch_ size, time_ steps, cell.output size) 。state是最后一步的隐状态,色的形状为(batch_size, cell.state_size) 。

另外,如果输入数据形状的格式为(time_steps, batch_size , input_ size),
那么可以在调用tf. nn.dynamic_rnn函数中设定参数time_major=True(默认情况是False),此时得到的outputs形状变成(time_ steps, batch_ size,
cell.output_size) , 而state的形状不变。

至此,再对每一步的输出进行变换,可以得到损失并训练模型了。具体
的代码组合方式可以参考下一节的代码。

5 使用TensorFlow实现Char RNN

给出了一个Char RNN的TensorFlow实现。该实现需要的运行环境为Python 2.7, TensorFlow1.2及以上。会先结合第3 、4 节中的内容讲解定义RNN模型的方法,最后会给出一些生成例子。

5.1 定义输入数据

模型定义主要放在了model.py文件中,从头开始,先来看下输入数据的定义。

def build_inputs(self):
    with tf.name_scope('inputs'):
        self.inputs = tf.placeholder(tf.int32, shape=(
            self.num_seqs, self.num_steps), name='inputs')
        self.targets = tf.placeholder(tf.int32, shape=(
            self.num_seqs, self.num_steps), name='targets')
        self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')

        # 对于中文,需要使用embedding层
        # 英文字母没有必要用embedding层
        if self.use_embedding is False:
            self.lstm_inputs = tf.one_hot(self.inputs, self.num_classes)
        else:
            with tf.device("/cpu:0"):
                embedding = tf.get_variable('embedding', [self.num_classes, self.embedding_size])
                self.lstm_inputs = tf.nn.embedding_lookup(embedding, self.inputs)

self.inputs是外部传入的一个batch内的输入数据,它的形状为(self.num_seqs, self.num_steps), self.num_seqs是一个batch内句子的个数(相当于batch_size),而self.num_steps表示每个句子的长度。

seIf.targets是self.inputs对应的训练目标,它的形状和self.inputs相同,内容是self.inputs每个字母对应的下一个字母。它的详细含义可以参考第3节进行理解。

除了self.inputs和self.targets外,还走义了一个输入self.keep_prob,因为在后面的模型中高Dropout层,这里的self.keep_prob控制了Dropout层所需要的概率。在训练时,使用self.keep_prob=0.5,在测试时,使用self.keep prob=1.0。

正如第3节中所说,对于单个的英文字母,一般不使用embedding层,而对于汉字生成,使用embedding层会取得更好的效果。程序中用self.use_embedding参数控制是否使用embedding。当不使用embedding时,会直接对self.inputs做独热编码得到self.lstm_inputs;当使用embedding时,会先定义一个embedding变量,接着使用tf.nn.embedding lookup查找embedding。请注意embedding变量也是可以训练的,因此是通过训练得到embedding的具体数值。self.lstm_inputs是直接输入LSTM的数据。

5.2 定义多层LSTM模型

下面的函数定义了多层的N VS N LSTM模型:

def build_lstm(self):
    # 创建单个cell并堆叠多层
    def get_a_cell(lstm_size, keep_prob):
        lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_size)
        drop = tf.nn.rnn_cell.DropoutWrapper(lstm, output_keep_prob=keep_prob)
        return drop

    with tf.name_scope('lstm'):
        cell = tf.nn.rnn_cell.MultiRNNCell(
            [get_a_cell(self.lstm_size, self.keep_prob) for _ in range(self.num_layers)]
        )
        self.initial_state = cell.zero_state(self.num_seqs, tf.float32)

        # 通过dynamic_rnn对cell展开时间维度
        self.lstm_outputs, self.final_state = tf.nn.dynamic_rnn(cell, self.lstm_inputs, initial_state=self.initial_state)

        # 通过lstm_outputs得到概率
        seq_output = tf.concat(self.lstm_outputs, 1)
        x = tf.reshape(seq_output, [-1, self.lstm_size])

        with tf.variable_scope('softmax'):
            softmax_w = tf.Variable(tf.truncated_normal([self.lstm_size, self.num_classes], stddev=0.1))
            softmax_b = tf.Variable(tf.zeros(self.num_classes))

        self.logits = tf.matmul(x, softmax_w) + softmax_b
        self.proba_prediction = tf.nn.softmax(self.logits, name='predictions')

在这段代码中,首先仿照第4.2节中的代码,定义了一个多层的BasicLSTMCell。唯一的区别在于,在这里对每个BasicLSTMCell使用了tf.nn.rnn_cell.DropoutWrapper函数,即加入了一层Dropout,以减少过拟合。

定义了cell后,如第4.4节中所说,使用tf.nn.dynamic_rnn函数展开了时间维度atf.nn.dynamic_rnn的输入为cell、self.lstm_inputs、self.initial_state其中cell已经解释过了,self.inputs是在第5.1节中的输出层定义的,self.initial_state是通过调用cell.zero_state得到的一个全0的Tensor,表示初始的隐层状态。

tf.nn.dynamic_rnn的输出为self.outputs和self.final_state。这里又需要对照第4.3节中的内容。self.outputs是多层LSTM的隐层h,因此需要得到最后的分类概率,还需要再定义一层Softmax层才可以。这里经过一次类似于Wx+b的变换后得到self.logits,再做Softmax处理,输出为self.proba_prediction 。

5.3 定义损失

得到self.proba_prediction后,可以使用它和self.targets的独热编码做交叉熵得到损失。另外,也可以使用tf.nn.softmax_cross_entropy_with_logits函数,通过self.logits直接定义损失,对应的代码如下:

def build_loss(self):
    with tf.name_scope('loss'):
        y_one_hot = tf.one_hot(self.targets, self.num_classes)
        y_reshaped = tf.reshape(y_one_hot, self.logits.get_shape())
        loss = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=y_reshaped)
        self.loss = tf.reduce_mean(loss)

以上是模型从输入到损失的全过程。

5.4 训练模型与生成文字

在本节中,将讲解如何使用定义好的模型训练、并生成文字。
1.生成英文
首先来看较为简单的生成英文的例子。将使用的训练、文件shakespeare.txt保存在项目的data/文件夹下,对应的训练的命令为:

python train.py \
    --input_file data/shakespeare.txt  \
    --name shakespeare \
    --num_steps 50 \
    --num_seqs 32 \
    --learning_rate 0.01 \
    --max_steps 20000

此处参数含义为:

  • input_file:用于训练的文本数据。程序要求训练的文本必须为使用utf-8编码的文件
  • –name:指定模型的名称,该名称决定了模型的保存位置。如这里指定模型的名称为shakespeare,那么训练中模型的保存位置是在model/shakespeare目录下。
  • –num_steps、–num_seqs:这两个参数决定了一个batch内序列的个数(相当于batch_size)和单个序列的长度。其中–num_steps对应序列度,–num_seqs对应序列个数。
  • –leaming_rate:训练时使用的学习率。
  • –max_ steps :一个step 是运行一个batch ,一max_steps固定了最大的运行步数。

运行后,模型会被保存在model/shakespeare目录下。使用下面的命令可以执行测试:

python sample.py \
    --convert_path model/shakespeare/conver.pkl \
    --checkpoint_path model/shakespeare/ \
    --max_length 1000

对应的参数含义为:

  • –convert_path:其实神经网络生成的是字母类别id。并不是字母。这些类别id在输入模型时是通过一个convert转换的,程序会自动把converter保存在model/shakespeare目录下。在输出时需要使用converter将类别id转换回字母。
  • –checkpoint_path:模型的保存路径。
  • –max_length:生成的序列长度。
SEIISHALIO:
Heaven thou with temper as that that sack and heavand.

LEINANUS:
And thou art monster, and my man is he say it,
As he thee that a sumsel thee, as some tount
And hear in a man is that his heart of his;
And so his word to this hearing of him.

SIR TOBY BELCH:
The stand, they we as stay there's the parts and think
And to me he may some the man of the stip
The way it.

PRINCESS:
Him, here's the took and men of a poor time.

SALINA:
How say my lord, had this shall stone, sir.

SEBISTIA:
He says to thy soure and misprace. The drawn
Is the hand in this wond is more than and this house
We will not aled this bound into a matter
And have that so to thou hast honest to them.

2. 用机器来写诗
这里还准备了一个data/poetry.txt,该文件中存放了大量唐诗中的五言诗歌。用下面的命令来进行训练:

python train.py \
    --use_embedding \
    --input_file data/poetry.txt \
    --name poetry.txt \
    --learning_rate 0.005 \
    --num_steps 26 \
    --num_seqs 32 \
    --max_steps 10000

这里出现了一个新的参数–use_embedding。在第3节和第5.1节中都提到过它的作用一一为输入数据加入一个巳embedding层。默认是使用独热编码而不使用embedding的,这里对汉字生成加入embedding层,可以获得更好的效果。

测试的命令为:

python sample.py \
    --use_embedding \
    --converter_path model/poetry/converter.pkl \
    --checkpoint_path model/poetry/ \
    --max_length 300

生成古诗如下:

不得何人尽,人来一水人。
一君何处处,一马有山风。
山水一相见,何人不可亲。
一君不可见,山色不相亲。
不见何人尽,何人不一人。
白人无未见,春水不相知。
山水多人去,江山不有人。
不见何年去,人归一月深。
何年何处去,何处不相知。
一日无人尽,山声不有年。
不见江城去,山云一上风。
一人归不去,一日一相闻。
一人不相见,春水自无来。
不知人自见,何月一相亲。
不知何处去,春月不无人。
不有人人去,江风一草深。
山云多自去,一月有相知。
白路多无日,何人不可知。
白人无不尽,春色不相归。
山水不相在,山云有未同。
山风无此日,不见不何时。
一日无无日,江城落日归

3. c代码生成
也可以利用Char RNN来生成代码。文件data /linux.txt为Linux源码。使用下面的命令来训练对应的模型。

python train.py \
    --input_file data/linux.txt \
    --num_steps 100 \
    --name linux \
    --learning_rate 0.01 \
    --num_seqs 32 \
    --max_steps 20000

这里使用了更大的序列长度100 (即num _steps 参数) 。对于代码来说,依赖关系可能在较长的序列中才能体现出来(如函数的大括号等)。代码同样采用单个字母或符号输入, 因此没再必要使用embedding层。

对应的测试命令为:

python sample.py \
    --converter_path model/linux/converter.pkl \
    --checkpoint_path model/linux/ \
    --max_length 1000

多次运行测试命令可以生成完全不一样的程序段。

<unk>_elaces */
    if (if (info);

    return 0)) {
        struct rq_trace_rt_prace_cpu(chal &= struct contait(chal && 0)
        if (return trace)
{
    struct int seq_reach_connt *pod);
}

/**
 * sibly() to currents same or read ath calle ared
     * interruns stating in so if char ards a trict the seq traces.chale oution. */
static insigned int commack_time_tree(struct task_class = trace_cpus(struct seq_parent(call * __is_rq_class);
    return tracing_tick(struct task_chor_tose_prist_state = check(cpu, cpu, struct trace_seq->regiter->signes_spin_trace_chat (inder) || ret_proctroc_procester_spor = spund(&compid &= case & _TRACK_INQ);
}

static scaunt;
                    put_set_trace_read(&trace_prise_t tist,
                      times_ret_to *can_cless_reac_try, int struct sent_puse());
        return state(struct call *tr);
        return -EINFE_SOST); i < 0 |= sched_reace = 0;
    } size_stop_stall->reloic && conting_state *tring_trace_copp_sage_can = 0;
        }
    return 0);
}

static int struct sproust_since()
{
}

        sched_pask_trace(&thread);

5.5 更多参数说明

除了上面提到的几个参数外。程序中还提供了一些参数,用于对模型进行微调,这些参数有:

  • –lstm_size:LSTM隐层的大小。默认为128。
  • –embedding_size:embedding空间的大小。默认为128。该参数只有在使用–use_embedding时生效。
  • –num_layers : LSTM的层数。默认为2 。
  • –max_vocab:使用的字母(汉字)的最大个数。默认为3500 。程序会自动挑选出使用最多的字,井将剩下的字归为一类,并标记为<unk>。

调整这几个参数就可以调整模型的大小,以获得更好的生成效果。读者
可以自己进行尝试。需要注意的是,在train.py运行时使用了参数,如–lstm_size 256,那么在运行sample.py时也必须使用同样的参数,即加上
–lstm_size 256 ,否则模型将无法正确载入。

最后还剩下两个运行参数,一个是–log_every,默认为10,即每隔10步会在屏幕上打出曰志。另外一个是–save_every_n ,默认为1000 ,即每隔
1000步会将模型保存下来。

5.6 运行自己的数据

在运行自己的数据时。需要更改–input_file和–name,其他的参数仿照设定即可。需要注意的是,使用的文本文件一定要是utf-8编码的,不然会出现解码错误。

6 总结

在这篇文章中,首先介绍了RNN和LSTM的基本结构,接着介绍了使用
TensorFlow实现RNN ( LSTM )的基本步骤,最后通过一个CharRNN项目
展示了使用经典RNN结构的方法。希望通过本文的介绍,对TensorFlow和真中RNN的实现有较为详细的了解。

  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RNN是一种递归神经网络,它可以处理序列数据,如时间序列数据、自然语言文本等。RNN基本结构是一个循环单元,它接收输入和前一时刻的隐藏状态,并输出当前时刻的隐藏状态和预测值。RNN的隐藏状态可以看作是网络对历史信息的记忆,因此它可以在处理序列数据时保留一些上下文信息。 基于门结构RNN是对传统RNN的改进,它引入了门控机制来控制信息的流动。门控机制是一种可以控制信息流量的方法,它可以让网络更加灵活地处理输入。其中最常见的是LSTM(长短时记忆网络)和GRU(门控循环单元)。 LSTM引入了三个门:输入门、遗忘门和输出门。输入门控制着新输入的权重,遗忘门控制着前一时刻隐藏状态的权重,输出门控制着当前时刻的输出。这三个门通过一些非线性函数来计算,并使用sigmoid激活函数将它们的取值范围限制在0到1之间。LSTM的结构比传统RNN更加复杂,但可以更好地处理长序列数据。 GRU是LSTM的简化版本,它只有两个门:重置门和更新门。重置门控制前一时刻隐藏状态的遗忘,更新门控制当前时刻的新输入。GRU可以看作是LSTM的变体,它的结构更加简单,但依然具有一定的门控能力。 基于门结构RNN比传统RNN具有更好的记忆能力和处理长序列数据的能力,因此在自然语言处理、语音识别等领域得到了广泛应用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值