0x00 循环神经网络

循环神经网络(Recurrent Neural Network)适用于学习和处理成序列的信息,其最早在自然语言处理领域被应用,因其可以对语言进行建模。这是个很有意思的话题,来看一个例子:

我昨天上学迟到了,老师批评了____。

我们让电脑预测空白处应填什么词,初期的算法,语言模型处理主要使用N-Gram算法,N是一个数字,它的含义是假设一个词出现的频率,只与前面N个词有关。比如对于上述的例子使用3-Gram算法,即假设空白处所推测的值只与词组『批评了』相关,于是电脑就会在词库中搜索,词组『批评了』后面最有可能出现的词是什么,然后进行填充。但是这样是远远不够的,因为我们真正要填的词『我』,在整句话的开头,词组『批评了』的大前方。这个时候,我们可以通过提升N的值,来做更让电脑作出精确的预判,但是这无疑会增加电脑计算时的工作量。

由此,引入了循环神经网络RNN,因为RNN可以一次往前或往后看任意多个的词。

0x01 RNN的结构

一个循环神经网络往往由输入层、一个隐藏层和一个输出层组成,如图:

这个网络在t时刻收到输入xtx_t之后,隐藏层的值为sts_t,输出层的值为oto_t。其中关键的一点是sts_t的值不仅仅取决于xtx_t,还取决于st1s_{t-1}。其计算方法可以表示为:

ot=g(Vst)st=f(Uxt+Wst1)o_t=g(Vs_t) \\ s_t=f(Ux_t+Ws_{t-1})

在上式中,式一表示输出层公式,输出层是一个全连接层,V是输出层的权重矩阵,g是激活函数。式二是隐藏层(循环层)的计算公式,U是输入x的权重矩阵,W是上一次的值st1s_{t-1}作为这一次的输入的权重矩阵,f是激活函数。如果把第二个式子中的sts_t反复代入第一个式子,可以得到:

ot=g(Vst)=Vf(Uxt+Wst1)=Vf(Uxt+Wf(Uxt1+Wst2))=Vf(Uxt+Wf(Uxt1+Wf(Uxt2+Wst3)))=Vf(Uxt+Wf(Uxt1+Wf(Uxt2+Wf(Uxt3+...))))\begin{align*} \mathrm{o}_t&=g(V\mathrm{s}_t) \\ &=Vf(U\mathrm{x}_t+W\mathrm{s}_{t-1}) \\ &=Vf(U\mathrm{x}_t+Wf(U\mathrm{x}_{t-1}+W\mathrm{s}_{t-2})) \\ &=Vf(U\mathrm{x}_t+Wf(U\mathrm{x}_{t-1}+Wf(U\mathrm{x}_{t-2}+W\mathrm{s}_{t-3}))) \\ &=Vf(U\mathrm{x}_t+Wf(U\mathrm{x}_{t-1}+Wf(U\mathrm{x}_{t-2}+Wf(U\mathrm{x}_{t-3}+...)))) \end{align*}

由此,我们看出,循环神经网络的输出值,是受前面任意一个输入值影响的。

0x02 双向循环神经网络

对于语言模型来说,有的时候光往前看是不够的,还得往后看,比如:

我的手机坏了,我打算____一部新手机。

对于上述例子而言,光往前看还不够还要往后看,此时,我们需要引入双向循环神经网络,看图:

双向循环神经网络,不仅仅要保存一个正向的A还要保存一个反向的A’,最终的输出与正向的A和反向的A’都有关系,我们以其中一个结点A2A_2为例,来看一下输出数据是怎么算出来的:

y2=g(VA2+VA2)A2=f(WA1+Ux2)A2=f(WA3+Ux2)\begin{align*} \mathrm{y}_2&=g(VA_2+V'A_2') \\ A_2&=f(WA_1+U\mathrm{x}_2) \\ A_2'&=f(W'A_3'+U'\mathrm{x}_2) \\ \end{align*}

其中,正向计算和反向计算不共享权重,也就是说U和U’、W和W’、V和V’都是不同的权重矩阵。由此我们来深入探讨一下隐藏层的值s和s’的计算。在正向计算时,隐藏层sts_t的值只与st1s_{t-1}有关,在反向计算时,隐藏层sts'_t只与st+1s'_{t+1}有关。二者的最终结果都取决于正向和反向计算时的加和。由此,我们可以写出:

ot=g(Vst+Vst)st=f(Uxt+Wst1)st=f(Uxt+Wst+1)\begin{align*} \mathrm{o}_t&=g(V\mathrm{s}_t+V'\mathrm{s}_t') \\ \mathrm{s}_t&=f(U\mathrm{x}_t+W\mathrm{s}_{t-1}) \\ \mathrm{s}_t'&=f(U'\mathrm{x}_t+W'\mathrm{s}_{t+1}') \\ \end{align*}

上述公式中,oto_t代表输出结果的值,xtx_t表示输入数据。

0x03 深度循环神经网络

多堆叠几个上述的循环神经网络的隐藏层就可以得到深度循环神经网络了,如图:

如果我们把第i个隐藏层的值表示为st(i)s_t^{(i)}st(i)s{'}_t^{(i)},可以有:

ot=g(V(i)st(i)+V(i)st(i))st(i)=f(U(i)st(i1)+W(i)st1)st(i)=f(U(i)st(i1)+W(i)st+1)...st(1)=f(U(1)xt+W(1)st1)st(1)=f(U(1)xt+W(1)st+1)\begin{align*} \mathrm{o}_t&=g(V^{(i)}\mathrm{s}_t^{(i)}+V'^{(i)}\mathrm{s}_t'^{(i)}) \\ \mathrm{s}_t^{(i)}&=f(U^{(i)}\mathrm{s}_t^{(i-1)}+W^{(i)}\mathrm{s}_{t-1}) \\ \mathrm{s}_t'^{(i)}&=f(U'^{(i)}\mathrm{s}_t'^{(i-1)}+W'^{(i)}\mathrm{s}_{t+1}') \\ ... \\ \mathrm{s}_t^{(1)}&=f(U^{(1)}\mathrm{x}_t+W^{(1)}\mathrm{s}_{t-1}) \\ \mathrm{s}_t'^{(1)}&=f(U'^{(1)}\mathrm{x}_t+W'^{(1)}\mathrm{s}_{t+1}') \\ \end{align*}

0x04 长短时记忆网络(LSTM)

长短时记忆网络是一种改进后的循环神经网络,循环神经网络难以处理长距离的依赖,因为随着间隔的不断增大,RNN会丧失学习到连接如此远的信息的能力。所以引入了长短时记忆网络成功解决了原始RNN的缺陷,成为目前最流行的循环神经网络。

LSTM通过可以的设计来避免长期依赖问题,所以记住长期的信息在实践中是LSTM的默认行为,并非需要付出很大代价才能获得的能力。原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么,假如我们再增加一个状态c,让它来保存长期的状态,那问题不就解决了么。如图:

理论上而言,RNN也具有处理长期依赖的能力,但是需要精心挑选参数付出巨大的代价来解决这类问题。标准的RNN中,重复的模块只有一个非常简单的结构,例如一个tanh层。

LSTM在每一个结点上做了改进,使重复的模块拥有一个不同的结构,不同于上图中的单一神经网络,LSTM中有4个,而且是以一种特殊的形式进行交互,如图:

下面我们来深入分析其中一个细胞的交互方式。LSTM的第一步是决定我们会从细胞状态中丢弃什么信息,这通过其中一个名为遗忘门的结构来完成。

遗忘门包含一个sigmoid层,sigmoid层输出一个0-1之间的数值,描述每个部分有多少量可以通过,0表示不允许任何量通过,而1表示允许任何量通过。该门会读取ht1h_{t-1}xtx_t,输出一个0-1之间的数值给每个在细胞状态Ct1C_{t-1}的数字。

下一步我们将确定将什么样的信息存放在细胞状态中。如下图所示,这里包含2个部分。第一个部分为sigmoid层,意为输入门层,决定要将什么值更新,然后一个tanh层创建一个新的候选值向量C~t\widetilde{C}_t

此后我们的工作就是更新细胞状态CtC_t。首先我们把旧状态Ct1C_{t-1}ftf_t相乘,丢弃掉我们确定需要丢弃的信息,接着加上it×C~ti_t\times \widetilde{C}_t,这就是新的细胞状态CtC_t

得到新的细胞状态之后,我们需要决定下一步要输出什么值,也就是hth_t的值。首先将ht1h_{t-1}xtx_t经过一个sigmoid层得到oto_t,确定哪个部分将输出出去,接着把细胞状态CtC_t通过一个tanh层进行处理,然后和oto_t相乘得到最终结果。

0x05 使用LSTM训练MNIST

LSTM适用于对序列的处理,对于MNIST来说,如何将其转换成一个序列呢?我们在这使用的方法是将这个28x28的图片的每一列看成是某一时刻的向量,这样每张图片就转化成了一个含有28个向量的图片。然后我们就可以强行使用LSTM训练MNIST,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets("data", one_hot=True)

x = tf.placeholder(tf.float32, shape=[None, 784])
# 将输入的2维张量转化为3维张量
# 维度是[BATCH_SIZE,TIME_STEP * INPUT_SIZE]
# 此处我们将一张28*28的图片分为行和列,将每一列看为某一时刻的向量
# 每张图片就是有28个向量的图片
image = tf.reshape(x, [-1, 28, 28])
y = tf.placeholder(tf.float32, shape=[None, 10])

# 定义LSTM的结构
# 设置100个LSTM单元
cell = tf.keras.layers.LSTMCell(units=100)
# tf.nn.dynamic_rnn返回值说明
# final_state包含两个量
# 第一个为c保存了每个LSTM任务**最后一个cell中**每个神经元的状态值
# 第二个量h保存了每个LSTM任务**最后一个cell中**每个神经元的输出值
# c和h的维度都是[BATCH_SIZE, NUM_UNITS(LSTM单元的个数)]
# outputs的维度是[BATCH_SIZE, TIME_STEP, NUM_UNITS]
# 保存了**每个step中**cell的输出值h
rnn_out, final_state = tf.nn.dynamic_rnn(
cell=cell, # 选择传入的cell
inputs=image, # 设置传入的数据
initial_state=None, # 设置RNN的初始状态
dtype=tf.float32,
# The shape format of the inputs and outputs tensors.
# If True, the inputs and outputs will be in shape (timesteps, batch, ...),
# whereas in the False case, it will be (batch, timesteps, ...).
# Using time_major = True is a bit more efficient
# because it avoids transposes at the beginning
# and end of the RNN calculation.
# However, most TensorFlow data is batch-major,
# so by default this function accepts input and emits output in batch-major form.
time_major=False
)

# tf.layers.dense用于创建一个全连接层
# inputs为所需训练的参数,units为输出空间的维度
prediction = tf.layers.dense(inputs=final_state[1], units=10)

loss = tf.losses.softmax_cross_entropy(onehot_labels=y, logits=prediction)

train = tf.train.AdamOptimizer(0.01).minimize(loss)

with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
batch_size = 100
train_batch_num = mnist.train.num_examples // batch_size
# 循环训练30次
for _ in range(30):
for i in range(train_batch_num):
images, labels = mnist.train.next_batch(batch_size)
sess.run(train, feed_dict={x: images, y: labels})
correct_prediction = tf.equal(tf.argmax(prediction, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print('========== Iter ' + str(_) + ' ==========')
# 训练集精确度
train_acc = 0
for i in range(train_batch_num):
images, labels = mnist.train.next_batch(batch_size)
train_acc = train_acc + sess.run(accuracy, feed_dict={x: images, y: labels})
print('Train set accuracy: ' + str(
train_acc / train_batch_num * 100), "%")
# 测试集精度
test_acc = 0
test_batch_num = mnist.test.num_examples // batch_size
for i in range(test_batch_num):
images, labels = mnist.test.next_batch(batch_size)
test_acc = test_acc + sess.run(accuracy, feed_dict={x: images, y: labels})
print('Test set accuracy: ' + str(
test_acc / test_batch_num * 100), "%")

训练完成后,其最终的训练集准确率在98.51%左右,测试集准确率在98.27%左右。