0x00 循环神经网络
循环神经网络(Recurrent Neural Network)适用于学习和处理成序列的 信息,其最早在自然语言处理领域被应用,因其可以对语言进行建模。这是个很有意思的话题,来看一个例子:
我昨天上学迟到了,老师批评了____。
我们让电脑预测空白处应填什么词,初期的算法,语言模型处理主要使用N-Gram算法,N是一个数字,它的含义是假设一个词出现的频率,只与前面N个词有关。比如对于上述的例子使用3-Gram算法,即假设空白处所推测的值只与词组『批评了』相关,于是电脑就会在词库中搜索,词组『批评了』后面最有可能出现的词是什么,然后进行填充。但是这样是远远不够的,因为我们真正要填的词『我』,在整句话的开头,词组『批评了』的大前方。这个时候,我们可以通过提升N的值,来做更让电脑作出精确的预判,但是这无疑会增加电脑计算时的工作量。
由此,引入了循环神经网络RNN,因为RNN可以一次往前或往后看任意多个的词。
0x01 RNN的结构
一个循环神经网络往往由输入层、一个隐藏层和一个输出层组成,如图:
这个网络在t时刻收到输入x t x_t x t 之后,隐藏层的值为s t s_t s t ,输出层的值为o t o_t o t 。其中关键的一点是s t s_t s t 的值不仅仅取决于x t x_t x t ,还取决于s t − 1 s_{t-1} s t − 1 。其计算方法可以表示为:
o t = g ( V s t ) s t = f ( U x t + W s t − 1 ) o_t=g(Vs_t) \\
s_t=f(Ux_t+Ws_{t-1})
o t = g ( V s t ) s t = f ( U x t + W s t − 1 )
在上式中,式一表示输出层公式,输出层是一个全连接层,V是输出层的权重矩阵,g是激活函数。式二是隐藏层(循环层)的计算公式,U是输入x的权重矩阵,W是上一次的值s t − 1 s_{t-1} s t − 1 作为这一次的输入的权重矩阵 ,f是激活函数 。如果把第二个式子中的s t s_t s t 反复代入第一个式子,可以得到:
o t = g ( V s t ) = V f ( U x t + W s t − 1 ) = V f ( U x t + W f ( U x t − 1 + W s t − 2 ) ) = V f ( U x t + W f ( U x t − 1 + W f ( U x t − 2 + W s t − 3 ) ) ) = V f ( U x t + W f ( U x t − 1 + W f ( U x t − 2 + W f ( U x t − 3 + . . . ) ) ) ) \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*}
o t = g ( V s t ) = V f ( U x t + W s t − 1 ) = V f ( U x t + W f ( U x t − 1 + W s t − 2 )) = V f ( U x t + W f ( U x t − 1 + W f ( U x t − 2 + W s t − 3 ))) = V f ( U x t + W f ( U x t − 1 + W f ( U x t − 2 + W f ( U x t − 3 + ... ))))
由此,我们看出,循环神经网络的输出值,是受前面任意一个输入值影响的。
0x02 双向循环神经网络
对于语言模型来说,有的时候光往前看是不够的,还得往后看,比如:
我的手机坏了,我打算____一部新手机。
对于上述例子而言,光往前看还不够还要往后看,此时,我们需要引入双向循环神经网络,看图:
双向循环神经网络,不仅仅要保存一个正向的A还要保存一个反向的A’,最终的输出与正向的A和反向的A’都有关系,我们以其中一个结点A 2 A_2 A 2 为例,来看一下输出数据是怎么算出来的:
y 2 = g ( V A 2 + V ′ A 2 ′ ) A 2 = f ( W A 1 + U x 2 ) A 2 ′ = f ( W ′ A 3 ′ + U ′ x 2 ) \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*}
y 2 A 2 A 2 ′ = g ( V A 2 + V ′ A 2 ′ ) = f ( W A 1 + U x 2 ) = f ( W ′ A 3 ′ + U ′ x 2 )
其中,正向计算和反向计算不共享权重 ,也就是说U和U’、W和W’、V和V’都是不同的权重矩阵 。由此我们来深入探讨一下隐藏层的值s和s’的计算。在正向计算时,隐藏层s t s_t s t 的值只与s t − 1 s_{t-1} s t − 1 有关,在反向计算时,隐藏层s t ′ s'_t s t ′ 只与s t + 1 ′ s'_{t+1} s t + 1 ′ 有关。二者的最终结果都取决于正向和反向计算时的加和。由此,我们可以写出:
o t = g ( V s t + V ′ s t ′ ) s t = f ( U x t + W s t − 1 ) s t ′ = f ( U ′ x t + W ′ s t + 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*}
o t s t s t ′ = g ( V s t + V ′ s t ′ ) = f ( U x t + W s t − 1 ) = f ( U ′ x t + W ′ s t + 1 ′ )
上述公式中,o t o_t o t 代表输出结果的值,x t x_t x t 表示输入数据。
0x03 深度循环神经网络
多堆叠几个上述的循环神经网络的隐藏层就可以得到深度循环神经网络了,如图:
如果我们把第i个隐藏层的值表示为s t ( i ) s_t^{(i)} s t ( i ) 和s ′ t ( i ) s{'}_t^{(i)} s ′ t ( i ) ,可以有:
o t = g ( V ( i ) s t ( i ) + V ′ ( i ) s t ′ ( i ) ) s t ( i ) = f ( U ( i ) s t ( i − 1 ) + W ( i ) s t − 1 ) s t ′ ( i ) = f ( U ′ ( i ) s t ′ ( i − 1 ) + W ′ ( i ) s t + 1 ′ ) . . . s t ( 1 ) = f ( U ( 1 ) x t + W ( 1 ) s t − 1 ) s t ′ ( 1 ) = f ( U ′ ( 1 ) x t + W ′ ( 1 ) s t + 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*}
o t s t ( i ) s t ′ ( i ) ... s t ( 1 ) s t ′ ( 1 ) = g ( V ( i ) s t ( i ) + V ′ ( i ) s t ′ ( i ) ) = f ( U ( i ) s t ( i − 1 ) + W ( i ) s t − 1 ) = f ( U ′ ( i ) s t ′ ( i − 1 ) + W ′ ( i ) s t + 1 ′ ) = f ( U ( 1 ) x t + W ( 1 ) s t − 1 ) = f ( U ′ ( 1 ) x t + W ′ ( 1 ) s t + 1 ′ )
0x04 长短时记忆网络(LSTM)
长短时记忆网络是一种改进后的循环神经网络,循环神经网络难以处理长距离的依赖,因为随着间隔的不断增大,RNN会丧失学习到连接如此远的信息的能力。所以引入了长短时记忆网络成功解决了原始RNN的缺陷,成为目前最流行的循环神经网络。
LSTM通过可以的设计来避免长期依赖问题,所以记住长期的信息在实践中是LSTM的默认行为,并非需要付出很大代价才能获得的能力。原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么,假如我们再增加一个状态c,让它来保存长期的状态,那问题不就解决了么。如图:
理论上而言,RNN也具有处理长期依赖的能力,但是需要精心挑选参数付出巨大的代价来解决这类问题。标准的RNN中,重复的模块只有一个非常简单的结构,例如一个tanh
层。
LSTM在每一个结点上做了改进,使重复的模块拥有一个不同的结构,不同于上图中的单一神经网络,LSTM中有4个,而且是以一种特殊的形式进行交互,如图:
下面我们来深入分析其中一个细胞的交互方式。LSTM的第一步是决定我们会从细胞状态中丢弃什么信息,这通过其中一个名为遗忘门的结构来完成。
遗忘门包含一个sigmoid层,sigmoid层输出一个0-1之间的数值,描述每个部分有多少量可以通过,0表示不允许任何量通过,而1表示允许任何量通过。该门会读取h t − 1 h_{t-1} h t − 1 和x t x_t x t ,输出一个0-1之间的数值给每个在细胞状态C t − 1 C_{t-1} C t − 1 的数字。
下一步我们将确定将什么样的信息存放在细胞状态中。如下图所示,这里包含2个部分。第一个部分为sigmoid层,意为输入门层,决定要将什么值更新,然后一个tanh层创建一个新的候选值向量C ~ t \widetilde{C}_t C t 。
此后我们的工作就是更新细胞状态C t C_t C t 。首先我们把旧状态C t − 1 C_{t-1} C t − 1 与f t f_t f t 相乘,丢弃掉我们确定需要丢弃的信息,接着加上i t × C ~ t i_t\times \widetilde{C}_t i t × C t ,这就是新的细胞状态C t C_t C t 。
得到新的细胞状态之后,我们需要决定下一步要输出什么值,也就是h t h_t h t 的值。首先将h t − 1 h_{t-1} h t − 1 和x t x_t x t 经过一个sigmoid层得到o t o_t o t ,确定哪个部分将输出出去,接着把细胞状态C t C_t C t 通过一个tanh层进行处理,然后和o t o_t o 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 tffrom tensorflow.examples.tutorials.mnist import input_datamnist = input_data.read_data_sets("data" , one_hot=True ) x = tf.placeholder(tf.float32, shape=[None , 784 ]) image = tf.reshape(x, [-1 , 28 , 28 ]) y = tf.placeholder(tf.float32, shape=[None , 10 ]) cell = tf.keras.layers.LSTMCell(units=100 ) rnn_out, final_state = tf.nn.dynamic_rnn( cell=cell, inputs=image, initial_state=None , dtype=tf.float32, time_major=False ) 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 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%左右。