赞
踩
从这篇文章起,将开始学习循环神经网络(RNN)以及相关的项目。这篇文章首先会向大家介绍RNN经典的结构,以及它的几个变体。接着将在TensorFlow 中使用经典的RNN结构实现一个有趣的项目: CharRNN 。Char RNN可以对文本的字符级概率进行建模,从而生成各种类型的文本。
RNN的英文全称是Recurrent Neural Networks ,即循环神经网络,它是一种对序列型数据进行建模的深度模型。在学习RNN之前,先来复习基本的单层神经网络,如图12-1所示。
单层网络的输入是x,经过变焕Wx+b和激活函数f得到输出y 。在实际应用中,还会遇到很多序列形的数据,如图12-2所示。
例如:
序列型的数据不太好用原始的神经网络处理。为了处理建模序列问题,RNN引入了隐状态
图12-3 中记号的含义是:
在很多论文中也会出现类似的记号,初学的时候很容易混,但只要把握住以上两点, 可以比较轻松地理解图示背后的含义。
如图12-4所示,
接下来,如图12-5 所示, 依次计算剩下的h (使用相同的参数U、W、b) 。
这里为了方便讲解起见, 只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。
目前的RNN还没再输出,得到输出值的方法是直接通过h进行计算,如图12-6所示。
此时使用的V和c是新的参数。通常处理的是分类问题(即输出
如图12-7所示,剩下输出的计算类似进行(使用和计算
大功告成!这是最经典的RNN结构,像搭积木一样把它搭好了。它的输入是
由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:
最后,给出经典RNN结构的严格数学定义,我们可以对照上面的图片进行理解。设输入为
其中,U,V,W,b,c均为参数,而f表示激活函数,一般为tanh函数。
有的时候,问题的输入是一个序列,输出是一个单独的值而不是序列,此时应该如何建模呢?实际上,只在最后一个h上进行输出变换可以了,如图12-8 所示。
这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。
同样给出该结构的数学表示。设输入为
输出时对最后一个隐状态做运算即可
输入不是序列而输出为序列的情况怎么处理?可以只在序列开始进行输入计算,如图12-9所示。
还有一种结构是把输入信息X作为每个阶段的输入,如图12-10所示。
图12-11省略了一些X的圆圈,是图12-10的等价表示。
该表示的公式表达为
这种1 VS N的结构可以处理的问题有:
前一节介绍了RNN和它的几种变体结构,本节介绍RNN的改进版:LSTM ( Long Short-Term Memory,长短期记忆网络)。一个RNN单元的输入由两部分组成,即前一步RNN单元的隐状态相当前这一步的外部输入,此外还有一个输出。从外部结构看, LSTM和RNN的输入输出一模一样,同样是在每一步接受外部输入和前一阶段的隐状态,并输出一个值。因此,可以把第1节中的每一种结构都无缝切换到LSTM,而不会产生任何问题。本节主要关注的是LSTM的内部结构以及它相对于RNN的优点。
回顾RNN的公式
可以用图12-12来表示RNN。
从图12-12中的箭头可以看到,
图12-13是用同样的方式画出的LSTM的示意图。
这里的符号比较复杂,不用担心,下面会分拆开来进行讲解。在讲解之前,先给出图12-13中各个记号的含义。长方形表示对输入的数据做变换或激活函数,圆形表示逐点运算。所谓逐点运算,是指将两个形状完全相同的矩形的对应位置进行相加、相乘或其它运算。箭头表示向量会在哪里进行运算。
和RNN有所不同,LSTM的隐状态有两部分,一部分是
不过,每一步的信息
LSTM的每一个单元都有一个“遗忘门”,用来控制遗忘掉
光遗忘肯定是不行,LSTM单元还得记住新东西。所以又有如图12-16所示的“记忆门”。记忆门的输入同样是
“遗忘” “记忆”的过程如图12-17所示,
最后,还需要一个“输出门”,用于输出内容。这里说是输出,其实是去计算另一个隐状态
总结一下,LSTM每一步的输入是
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所示。
在测试时,应该怎样生成序列呢?方法是首先选择一个
使用独热向量来表示字母,然后 依次输入网络。假设一共有26个字母,那么字母a的表示为第一位为1,其他25位都是0,即(1,0,0,…,0),字母b的表示是第二位为1,其他25位都是0,即(0,1,0,…,0)。输出相当于一个26类分类问题,因此每一步输出的向量也是26维,每一维代表对应字母的概率,最后损失使用交叉熵可以直接得到。在实际模型中,由于字母有大小写之分以及其他标点符号,因此总共的类别墅会比26多。
最后,在对中文建模时,为了简单起见,每一步输入模型的是一个汉字。相对于字母来说,汉字种类比较多,可能会导致模型过大,对此有以下两种优化方法:
中文汉字的输出层和之前处理英文字母时是一样的,都相当于N类分类问题。
在本小节中,讲述在TensorFlow中实现RNN的主要方法,帮助大家循序渐进的梳理其中最重要的几个概念。首先使用RNNCell对RNN模型进行单步建模。RNNCell可以处理时间上的“一步”,即输入上一步的隐层状态和这一步的数据,计算这一步的输出和隐层状态。接着,TensorFlow使用tf.nn.dynamic_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),初始输入为
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)
很多时候,单层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.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层才能得到最后的分类概率输出。
对于单个的RNNCell,使用它的call函数进行运算时,只是在序列时间上前进了一步。如果使用
具体来说,设输入数据的格式为(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的形状不变。
至此,再对每一步的输出进行变换,可以得到损失并训练模型了。具体
的代码组合方式可以参考下一节的代码。
给出了一个Char RNN的TensorFlow实现。该实现需要的运行环境为Python 2.7, TensorFlow1.2及以上。会先结合第3 、4 节中的内容讲解定义RNN模型的方法,最后会给出一些生成例子。
模型定义主要放在了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的数据。
下面的函数定义了多层的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 。
得到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)
以上是模型从输入到损失的全过程。
在本节中,将讲解如何使用定义好的模型训练、并生成文字。
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
此处参数含义为:
运行后,模型会被保存在model/shakespeare目录下。使用下面的命令可以执行测试:
python sample.py \
--convert_path model/shakespeare/conver.pkl \
--checkpoint_path model/shakespeare/ \
--max_length 1000
对应的参数含义为:
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);

除了上面提到的几个参数外。程序中还提供了一些参数,用于对模型进行微调,这些参数有:
调整这几个参数就可以调整模型的大小,以获得更好的生成效果。读者
可以自己进行尝试。需要注意的是,在train.py运行时使用了参数,如–lstm_size 256,那么在运行sample.py时也必须使用同样的参数,即加上
–lstm_size 256 ,否则模型将无法正确载入。
最后还剩下两个运行参数,一个是–log_every,默认为10,即每隔10步会在屏幕上打出曰志。另外一个是–save_every_n ,默认为1000 ,即每隔
1000步会将模型保存下来。
在运行自己的数据时。需要更改–input_file和–name,其他的参数仿照设定即可。需要注意的是,使用的文本文件一定要是utf-8编码的,不然会出现解码错误。
在这篇文章中,首先介绍了RNN和LSTM的基本结构,接着介绍了使用
TensorFlow实现RNN ( LSTM )的基本步骤,最后通过一个CharRNN项目
展示了使用经典RNN结构的方法。希望通过本文的介绍,对TensorFlow和真中RNN的实现有较为详细的了解。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。