第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

时间:2024-09-24 11:35:38

本章共两部分,这是第一部分:

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

第十四章——循环神经网络(Recurrent Neural Networks)(第二部分)

这几年提到RNN,一般指Recurrent Neural Networks,至于翻译成循环神经网络还是递归神经网络都可以。wiki上面把Recurrent Neural Networks叫做时间递归神经网络,与之对应的还有一个结构递归神经网络(recursive neural network)。本文讨论的是前者。

RNN是一种可以预测未来(在某种程度上)的神经网络,可以用来分析时间序列数据(比如分析股价,预测买入点和卖出点)。在自动驾驶中,可以预测路线来避免事故。更一般的,它可以任意序列长度作为输入,而不是我们之前模型使用的固定序列长度。例如RNN可以将句子、文档、语音作为输入,进行自动翻译、情感分析、语音转文字。此外,RNN还用于作曲(谷歌Magenta项目作出的the one)、作文图片自动生成标题

14.1 周期神经元(Recurrent Neurons)

此前介绍的大部分是前馈神经网络,激活流只有一个方向,从输入层流向输出层。RNN和前馈神经网络很相似,不过也会向后连接。我们来看一个最简单的RNN,只有一个神经元接受输入,只产生一个输出,然后再将输出传递给自己,如图14-1(左侧)。在每一个time step $t$(也叫做一帧),循环神经元接受输入$x_{(t)}$和前一步的输出$y_{(t-1)}$。可以将这一神经元随时间展开,如图14-1(右)。

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-1 一个循环神经元(左),随时间展开(右)

创建一层循环神经元也很简单,只不过在一个time step,输入和输出都是向量,如图14-2。

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-2 一层循环神经元(左),随时间展开(右)

每个神经元都有两套权重:一个用于本层输入$x_{(t)}$,一个用于上层输出$y_{(t-1)}$。我们分别记为$w_x$和$w_y$。

一个循环神经元关于一个实例的输出:

\begin{align*}
y_{(t)} = \phi(x_{(t)}^T \cdot w_x + y_{(t-1)}^T \cdot w_y + b)
\end{align*}

其中,$b$是偏置项,$\phi(\cdot)$是激活函数,比如ReLU(许多研究者更喜欢使用hyperbolic tangent (tanh)作为RNN的激活函数。例如,可以参考Vu Pham等人的Dropout Improves Recurrent Neural Networks for Handwriting Recognition。不过,基于ReLU的RNN也是可以的,比如Quoc V. Le等人的论文A Simple Way to Initialize Recurrent Networks of Rectified Linear Units。)。

一层循环神经元关于整个mini-batch的输出:

\begin{align*}
Y_{(t)} &= \phi(X_{(t)}^T \cdot W_x + Y_{(t-1)}^T \cdot W_y + \textbf{b}) \\
&= \phi([X_{(t)} \quad Y_{(t)}] \cdot W + \textbf{b}) \quad \mbox{with} \quad
W = \begin{bmatrix}
W_x \\
W_y
\end{bmatrix}
\end{align*}

  • $Y_{(t)}$是一个$m \times n_{\mbox{neurons}}$矩阵,包含该层在time step $t$关于整个mini-batch实例的输出($m$是mini-batch的实例数,$n_{\mbox{neurons}}$是神经元数量)。
  • $X_{(t)}$是一个$m \times n_{\mbox{inputs}}$矩阵,包含该time step $t$所有实例的输入($n_{\mbox{inputs}}$是特征数)。
  • $W_x$是一个$n_{\mbox{inputs}} \times n_{\mbox{neurons}}$矩阵,包含当前time step输入到输出的连接权重。
  • $W_y$是一个$n_{\mbox{neurons}} \times n_{\mbox{neurons}}$矩阵,包含上个time step输出到当前time step输出的连接权重。
  • $W$的形状是$(n_{\mbox{inputs}} + n_{\mbox{neurons}}) \times n_{\mbox{neurons}}$
  • $ \textbf{b}$是一个大小为$n_{\mbox{neurons}}$的向量,包含所有神经元的偏置项。

可以看到,$Y_{(t)}$是关于$X_{(t)}$和$Y_{(t-1)}$的函数,$Y_{(t-1)}$又是关于$X_{(t-1)}$和$Y_{(t-2)}$的函数,等等。这使得$Y_{(t)}$其实是关于$X_{(0)},X_{(1)},\cdots ,X_{(t)}$的函数。

14.1.1 Memory Cells

由于神经网络在第$t$个time step的输出是一个关于前$t$个time step所有输入的函数,这可以理解为一种形式的记忆(memory)。神经网络中保存前面时刻状态的部分称为memory cell(或者简单称为cell)。一个单独的周期神经元,或者一层周期神经元,就是一个很基础的cell。随后我们会看到更加复杂和强大的cell。

一般一个cell在时刻$t$(姑且把time step称作时刻把,不然太麻烦)的状态,记做$\textbf{h}_{(t)}$(“h”代表“hidden”),这是一个关于当前时刻输入和前一时刻状态的函数:$\textbf{h}_{(t)} = f(\textbf{h}_{(t-1)}, \textbf{x}_t)$。在$t$时刻的输出,记做$\textbf{y}_{(t)}$,这也是一个关于当前时刻输入和前一时刻状态的函数。在前面讨论的基本cell中,状态和输出是一致的,但在复杂的模型中这是不一致的,如图14-3。

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-3 一个cell的隐状态可能与它的输出不一致

14.1.2 输入和输出序列

RNN可以一个序列作为输入,再同时输出一个序列(如图14-4左上)。该模型可用于股价预测,输入前$N$天的股价,输出每一天的股价,知道第$N+1$天。每增加一天的输入,就预测下一天的输出。

此外,还可以序列作为输入,忽略除了最后一个之外所有的输出(如图右上)。例如用于情感分析,可以将电影评论作为输入,输出情感分值。

相反的,也可以输入单一的样本,输出一个序列(如图左下)。例如,输入可以是一幅图像,输出是该图像的标题。

最后,右下角的神经网络就是一个翻译系统了。这是序列到向量神经网络(称为encoder)和向量到序列神经网络(称为decoder)的组合。比如,输入可以是一种语言的一句话,encoder将这句话转换为向量表示,decoder再把这个向量表示转换成另一种语言的一句话。这是一个two-step模型,称为Encoder–Decoder,执行翻译任务时,效果比一个序列序列的神经网络好得多。因为原文的最后一个词可能会影响译文的第一个词,所以需要读完全句后再进行翻译。

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-4 序列到序列(左上),序列到向量(右上),向量到序列(左下),延时的序列到序列(右下)

14.2 基本RNN的TensorFlow实现

首先,我们来实现一个很简单的RNN模型,不使用TensorFlow的任何运算,以便了解底层原理。我们会创建一层有5个训练神经元的RNN(如图14-2),使用tanh激活函数。假设这一RNN有两个时刻,每一时刻的输入是大小为3的向量。下面的代码创建这一RNN,并随时间展开:

n_inputs = 3
n_neurons = 5 X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs]) Wx = tf.Variable(tf.random_normal(shape=[n_inputs, n_neurons],dtype=tf.float32))
Wy = tf.Variable(tf.random_normal(shape=[n_neurons,n_neurons],dtype=tf.float32))
b = tf.Variable(tf.zeros([1, n_neurons], dtype=tf.float32)) Y0 = tf.tanh(tf.matmul(X0, Wx) + b)
Y1 = tf.tanh(tf.matmul(Y0, Wy) + tf.matmul(X1, Wx) + b) init = tf.global_variables_initializer()

输入训练数据并运行:

import numpy as np

# Mini-batch:         instance 0,instance 1,instance 2,instance 3
X0_batch = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 0, 1]]) # t = 0
X1_batch = np.array([[9, 8, 7], [0, 0, 0], [6, 5, 4], [3, 2, 1]]) # t = 1 with tf.Session() as sess:
init.run()
Y0_val, Y1_val = sess.run([Y0, Y1], feed_dict={X0: X0_batch, X1: X1_batch})

这一mini-batch有4个实例,每个实例都是包含两个输入的序列。最后Y0_val和Y1_val包含神经网络在两个时刻关于所有实例的输出:

>>> print(Y0_val) # output at t = 0
[[-0.2964572 0.82874775 -0.34216955 -0.75720584 0.19011548] # instance 0
[-0.12842922 0.99981797 0.84704727 -0.99570125 0.38665548] # instance 1
[ 0.04731077 0.99999976 0.99330056 -0.999933 0.55339795] # instance 2
[ 0.70323634 0.99309105 0.99909431 -0.85363263 0.7472108 ]] # instance 3
>>> print(Y1_val) # output at t = 1
[[ 0.51955646 1. 0.99999022 -0.99984968 -0.24616946] # instance 0
[-0.70553327 -0.11918639 0.48885304 0.08917919 -0.26579669] # instance 1
[-0.32477224 0.99996376 0.99933046 -0.99711186 0.10981458] # instance 2
[-0.43738723 0.91517633 0.97817528 -0.91763324 0.11047263]] # instance 3

下面我们看一下,如何使用TensorFlow的RNN运算来实现相同的模型。

14.2.1 随时间静态展开

static_rnn()函数可以创建一个展开的RNN。以下代码可创建与先前相同的模型:

X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs]) basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, [X0, X1], dtype=tf.float32) Y0, Y1 = output_seqs

首先我们创建了输入占位符,然后是BasicRNNCell,可以将其看作cell工厂。static_rnn()函数调用cell工厂的__call__()函数,为每一时刻创建一个cell,并共享权重和偏置项。static_rnn()返回两个对象,第一个是包含每一时刻输出张量的Python list,另一个是整个网络最终的状态。由于我们使用了最基本的cell,最终的状态其实与第二时刻的输出是一致的。

如果有50个时刻,操作50个输入占位符和50个输出张量实在太繁琐了,需要简化这一过程。下面的代码创建同样的模型,但输出占位符的形状是[None, n_steps, n_inputs],第一个维度是mini-batch的尺寸。X_seqs是一个大小为n_steps的Python list,该list每个元素都是形状为[None, n_inputs]的张量,第一维同样是mini-batch尺寸。为了得到X_seqs,我们首先使用transpose()转置函数交换前两个维度,转置之后时刻就位于第一维度了。然后使用unstack()关于第一维度提取张量list。随后的两行与之前一样。最后再将输出转换成一个形状为[None, n_steps, n_neurons]的张量。

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
X_seqs = tf.unstack(tf.transpose(X, perm=[1, 0, 2])) basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, X_seqs, dtype=tf.float32) outputs = tf.transpose(tf.stack(output_seqs), perm=[1, 0, 2])

然后输入训练数据,运行这一网络:

X_batch = np.array([
# t = 0 t = 1
[[0, 1, 2], [9, 8, 7]], # instance 0
[[3, 4, 5], [0, 0, 0]], # instance 1
[[6, 7, 8], [6, 5, 4]], # instance 2
[[9, 0, 1], [3, 2, 1]], # instance 3
])
with tf.Session() as sess:
init.run()
outputs_val = outputs.eval(feed_dict={X: X_batch})

最终的outputs_val是一个包含所有实例、任一时刻、所有神经元的输出的张量。

然而,这一过程所创建的图仍然是每一时刻包含一个cell。如果有50个时刻,这个图看起来就很丑陋。这就像是写程序而不使用循环(比如Y0=f(0, X0); Y1=f(Y0, X1); Y2=f(Y1, X2); ...;Y50=f(Y49, X50))。图这么大,在反向传播时也很容易造成内存溢出(尤其是运行与GPU内存时),因为需要记录前向传播时每层的所有输出,以便反向传播时计算梯度。

幸运的是,还有更好的解决方案,那就是dynamic_rnn()函数。

14.2.2 随时间动态展开

dynamic_rnn()函数通过while_loop()对cell运算适当地次数。还可以设置swap_memory=True,在反向传播时交换GPU内存和CPU内存来防止OOM错误。更方便的是,它可以接受形状为[None, n_steps, n_inputs])的张量,这就不需要stack,unstack,以及transpose。下面简洁的代码实现了相同的模型:

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

14.2.3 处理变长输入序列

前面我们使用的输入序列都是定长的(都是两个时刻),如果输入序列是变长的呢(比如句子)?这样的话,在调用dynamic_rnn()(或者static_rnn())时,就要使用sequence_length参数了。这是一个1D张量,指明了每个实例的序列长度。例如:

seq_length = tf.placeholder(tf.int32, [None])

[...]
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32, sequence_length=seq_length)

假设我们的第二个实例只有一个时刻的输入,表示该实例张量的第二维需要补零,如下所示:

X_batch = np.array([
# step 0 step 1
[[0, 1, 2], [9, 8, 7]], # instance 0
[[3, 4, 5], [0, 0, 0]], # instance 1 (padded with a zero vector)
[[6, 7, 8], [6, 5, 4]], # instance 2
[[9, 0, 1], [3, 2, 1]], # instance 3
])
seq_length_batch = np.array([2, 1, 2, 2]) with tf.Session() as sess:
init.run()
outputs_val, states_val = sess.run([outputs, states], feed_dict={X: X_batch, seq_length: seq_length_batch})

14.2.4 处理变长输出

如果输出序列是变长的怎么办呢?如果你预先知道输出序列的长度(比如输出序列与输入序列等长),那就可以像先前那样定义一个类似的sequence_length参数。不幸的是,一般无法预测输出的序列长度。这种情况下,最常见的做法是定义一个end-of-sequence token (EOS token)的特殊输出(这将后面自然语言处理的小节进行讨论)。

14.3 训练模型

训练RNN,技巧就是随时间展开,然后应用常规的反向传播(如图14-5)。这一策略称作随时间反向传播(backpropagation through time,BPTT)。

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-5 随时间反向传播

和常规的反向传播类似,首先展开神经网络前向传播(如上图虚箭头所示),然后使用损失函数$C(Y_{(t_{min})},Y_{(t_{min} + 1)},\cdots,Y_{(t_{max})})$(其中,$t_{min}$、$t_{max}$是第一个和最后一个输出,并且不计算被忽略的输出)对输出进行评估。最后使用梯度更新参数。上图中,损失函数用到了$Y_{(2)}$、$Y_{(3)}$、$Y_{(4)}$三个输出,并没有使用$Y_{(0)}$、$Y_{(1)}$。

14.3.1 训练一个序列分类器

我们来训练一个RNN对MNIST图片进行分类。虽然CNN更适合做图像分类,这里只是使用这个例子来熟悉RNN。可以将MNIST中的每一个图像都看作是28行的序列,每一行又有28个像素点。我们使用150个循环神经元,加上一个全连接层,与输出层连接。最后是softmax层。如图14-6:

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-6 序列分类器

构建过程是很直接的,并且和第十章的MNIST分类器很相似,只是用RNN的展开替换掉了之前的隐层。与输出层进行全连接的是states张量,只包含最后一个时刻的输出。$y$是目标类别的占位符。

from tensorflow.contrib.layers import fully_connected

n_steps = 28
n_inputs = 28
n_neurons = 150
n_outputs = 10 learning_rate = 0.001 X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.int32, [None]) basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32) logits = fully_connected(states, n_outputs, activation_fn=None)
xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits) loss = tf.reduce_mean(xentropy)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(loss)
correct = tf.nn.in_top_k(logits, y, 1)
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32)) init = tf.global_variables_initializer()

然后加载MNIST数据,并将训练数据转换为[batch_size, n_steps, n_inputs]的形状。

from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets("/tmp/data/")
X_test = mnist.test.images.reshape((-1, n_steps, n_inputs))
y_test = mnist.test.labels

接着是模型的训练,这与第十章是类似的,只不过要改下训练数据的形状:

n_epochs = 100
batch_size = 150 with tf.Session() as sess:
init.run()
for epoch in range(n_epochs):
for iteration in range(mnist.train.num_examples // batch_size):
X_batch, y_batch = mnist.train.next_batch(batch_size)
X_batch = X_batch.reshape((-1, n_steps, n_inputs))
sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
acc_train = accuracy.eval(feed_dict={X: X_batch, y: y_batch})
acc_test = accuracy.eval(feed_dict={X: X_test, y: y_test})
print(epoch, "Train accuracy:", acc_train, "Test accuracy:", acc_test)

14.3.2 训练时序数据

这次我们处理时序数据,比如股价、气温、脑电波等等。每一个训练实例都是从时序中随机选出20个时刻(如图14-7左侧)。目标序列与训练序列是相同的,只不是目标序列始终比训练序列晚一个时刻(如图14-7右侧,最下角的实心篮圈,是$t_0$时刻的输入。紧接着的空心篮圈,是$t_1$时刻的输入同时也是$t_0$时刻的目标值。依此类推)。

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-7 时序数据(左),从时序中选出的一个实例(右)

首先,我们来创建RNN。它包含100个循环神经元,并展开为20个时刻。每个时刻的输入只有一个特征。目标值也是同样的20个时刻。代码如下:

n_steps = 20
n_inputs = 1
n_neurons = 100
n_outputs = 1 X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.float32, [None, n_steps, n_outputs])
cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons, activation=tf.nn.relu)
outputs, states = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32)

一般情况下,每一时刻的输入可能不止一个特征。比如,进行股价预测时,可能还会使用专家评级等信息,来提高预测准确性。我们这里是对模型进行了简化。

在这个模型中,每一时刻都会输出一个大小为100的向量。但我们需要在每一时刻的输出是个标量。最简单的解决方案是使用OutputProjectionWrapper,将cell封装起来。OutputProjectionWrapper在每一时刻的输出之上增加一层全连接的线性神经元(比如不使用激活函数),而且不会影响cell状态。所有这些全连接层共享同样的权重的偏置项(可训练),如图14-8:

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-8 使用输出投影的RNN cells

封装cell很容易,简单改变之前的代码即可:

cell = tf.contrib.rnn.OutputProjectionWrapper(
tf.contrib.rnn.BasicRNNCell(num_units=n_neurons, activation=tf.nn.relu),
output_size=n_outputs)

然后就是定义损失函数,创建Adam优化器,等等:

learning_rate = 0.001

loss = tf.reduce_mean(tf.square(outputs - y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(loss) init = tf.global_variables_initializer()

接着就可以执行了:

n_iterations = 10000
batch_size = 50 with tf.Session() as sess:
init.run()
for iteration in range(n_iterations):
X_batch, y_batch = [...] # fetch the next training batch
sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
if iteration % 100 == 0:
mse = loss.eval(feed_dict={X: X_batch, y: y_batch})
print(iteration, "\tMSE:", mse)

程序的输出如下:

0   MSE: 379.586
100 MSE: 14.58426
200 MSE: 7.14066
300 MSE: 3.98528
400 MSE: 2.00254
[...]

训练好之后,就可以预测了:

X_new = [...] # New sequences
y_pred = sess.run(outputs, feed_dict={X: X_new})

图14-9显示了迭代训练1000次之后的预测序列:

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-9 时序预测

虽然OutputProjectionWrapper是一种解决方案,但还有一种更高效的方案:首先将RNN的输出从[batch_size, n_steps, n_neurons]转换成[batch_size * n_steps, n_neurons],然后使用一个全连接层给下恰当的输出个数(在我们的例子中,输出1个标量)。 然后再将计算结果从[batch_size * n_steps, n_outputs]转换回[batch_size, n_steps, n_outputs],如图14-10所示:

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-10 堆叠所有输出,应用投影,最后再展开

为实现这一方案,我们首先将代码还原成基本的cell,不再使用OutputProjectionWrapper:

cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons, activation=tf.nn.relu)
rnn_outputs, states = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32)

然后就是改变RNN输出数据的形状,做映射,再改变回原来的形状:

stacked_rnn_outputs = tf.reshape(rnn_outputs, [-1, n_neurons])
stacked_outputs = fully_connected(stacked_rnn_outputs, n_outputs, activation_fn=None)
outputs = tf.reshape(stacked_outputs, [-1, n_steps, n_outputs])

剩下的代码就和之前的一样了。

14.3.3 创造性的RNN

既然我们有了一个可以预测未来的模型,当然也可以用它来产生一些创造性的东西,正如本章开头所提到的。我们只需提供一个包含n_steps个值的种子序列(该种子序列可以全是0), 模型就可以预测下一时刻值。将预测出的值在作为输入,又能得到一个预测值,如此循环下去。代码如下:

sequence = [0.] * n_steps
for iteration in range(300):
X_batch = np.array(sequence[-n_steps:]).reshape(1, n_steps, 1)
y_pred = sess.run(outputs, feed_dict={X: X_batch})
sequence.append(y_pred[0, -1, 0])

可以得到一个新的时序,并且与原来的时序有相似之处,如图14-11

第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

图14-11 创造性的序列,左侧种子是0,右侧种子是一个实例

本文较长,剩下的将写在一篇新的博客中。