0 前言
循环神经网络(Recurrent Neural Network, RNN)的主要用途是处理和预测序列数据。无论是全连接神经网络还是卷积神经网络,其网络结构都是从输入层到隐藏层再到输出层,层与层之间是全连接或者部分连接,层之间的节点是无连接的。而循环神经网络隐藏层之间的结点是有连接的,隐藏层的输入不仅包括输入层的输出,还包括上一时刻隐藏层的输出。刚开始学的时候,看到这句话一般都是懵的,我也是。
1 RNN结构
下图是RNN经典的网络结构示意图:
理论上,只应该有输入层,RNN层,也就是途中的A所表示的部分,最后再有一部分的全连接层等。其中RNN层的输出会参与到下一时刻的运算,于是就可以将其展开成有图所示的结构。但其实其结构还是与之前所见的网络结构类似,只不过RNN层的输出会参与下一时刻的运算。
另一方面,如果按照上述的结构展开,循环神经网络可以无限展开,也就是可以对应无限长的时间的跨度,但是目前还不能做到真正的无限循环,于是这里我们就要引入一个时间步长(time_steps),这个时间步长则表示循环神经网络展开的次数。
在这篇博客手把手教你用Tensorflow搭建RNN,作者采用TensorFlow搭建RNN对MNIST数据集中的手写数字进行识别。由于每张图片的大小是28X28,于是作者设计输入节点为28,时间步长time_step也是28,于是就有了下面所示的结构:
从图中可以看出,作者依次将图片中的28行数据按顺序输入到网络结构中,在这28个时间内,他们是共享权值的,唯一有变化的就是进入RNN层的输入是输入+上一时刻的状态,这两个向量共同组成了RNN层的输入。而RNN层的输出一方面进入下一层的网络结构进行计算,另一方面则进入下一时刻参与计算。等到28次计算完成之后,一般取最后时刻的输出作为最后的输出,参与loss计算。
通过上面的例子我们不难发现RNN层起到的作用无非就是两个,一个是输出结果参与下一层网络的计算,另一方面则是参与下一时刻的运算。
在郑泽宇等编写的《TensorFlow-实战Google深度学习框架》中关于RNN一章的描述中举了一个实际的例子,可以帮助我们更好的理解RNN:
从图中我们可以看出这是一个时间步长为2的RNN,也就是说需要每次需要对整个网络计算两遍。
-
首先初始化RNN的初始状态为[0, 0];
-
然后在t0时刻,输入1与当前状态(初始状态)组成一个向量[0, 0, 1](在tensorflow中可以通过tf.concat()来实现)参与RNN内部的运算:WX+b,运算的结果可以经过一个激励函数,然后得到RNN层的输出[0.537, 0.462]。
-
RNN的输出可以参与下一层网络的计算,另一部分进入了t1时刻,此时RNN层的输入是t0时刻RNN的输出和输入层输入的2,于是t1时刻RNN层的输入是[0.537, 0.462, 2],然后再进行相同的计算。
这里我们就很轻松的理解了为什么tensorflow给的关于RNN的接口中有关于zero_state(), 以及static_rnn()中要求输入的x是一个sequence,zero_state比较好理解,就是初始化初始状态,而sequence其实就是一个list,他通过求这个list的长度就能知道rnn的时间步长,也就是需要循环几次。而在dynamic_rnn()中则可以动态的设置时间步长。
2 手动实现一个RNN
基于上面的分析,我们不难使用tensorFlow写出一个RNN:
# 定义batch_size
batch_size = 5
# 定义状态变量的大小
state_size = 4
# 定义输出层的大小
input_size = 5
# 定义RNN层中的参数
w_rnn_cell = tf.Variable(tf.random_normal([state_size+input_size, state_size]), dtype=tf.float32)
b_rnn_cell = tf.Variable(tf.random_normal([1, state_size]), dtype=tf.float32)
# 定义初始状态
init_state = tf.Variable(tf.zeros([batch_size, state_size]), dtype=tf.float32)
current_state = init_state
def RNN(input_series):
for current_input in input_series:
input_and_state_concatenated = tf.concat([current_input, current_state],1)
current_state = tf.tanh(tf.matmul(input_and_state_concatenated, W) + b)
return current_state
3 RNN的推导
借用这张图(来源(循环神经网络(RNN)模型与前向反向传播算法)),其前向传播,可以描述为:
对于任意一个序列索引号
t
t
t,我们隐藏状态
h
(
t
)
h^{(t)}
h(t)由
x
(
t
)
x^{(t)}
x(t)和
h
(
t
−
1
)
h^{(t-1)}
h(t−1)得到:
h
(
t
)
=
σ
(
z
(
t
)
)
=
σ
(
U
x
(
t
)
+
W
h
(
t
−
1
)
+
b
;
)
h^{(t)} = \sigma(z^{(t)}) = \sigma(U_x^{(t)} + W_h^{(t-1)} +b;)
h(t)=σ(z(t))=σ(Ux(t)+Wh(t−1)+b;)
其中
σ
\sigma
σ为RNN的激活函数,一般为
t
a
n
h
tanh
tanh,
b
b
b为线性关系的偏倚。
序列索引号
t
t
t时模型的输出
o
(
t
)
o^{(t)}
o(t)的表达式比较简单:
o
(
t
)
=
V
h
(
t
)
+
c
;
o^{(t)} = V_h^{(t)} +c;
o(t)=Vh(t)+c;
在最终在序列索引号
t
t
t时我们的预测输出为:
y
^
(
t
)
=
σ
(
o
(
t
)
)
\hat{y}^{(t)} = \sigma(o^{(t)})
y^(t)=σ(o(t))
通常由于RNN是识别类的分类模型,所以上面这个激活函数一般是softmax。
通过损失函数
L
(
t
)
L^{(t)}
L(t),比如对数似然损失函数,我们可以量化模型在当前位置的损失,即
y
^
(
t
)
\hat{y}^{(t)}
y^(t)和
y
(
t
)
y^{(t)}
y(t)的差距。
而反向传播称之为BPTT(Back Propagation Through Time),其差别在于我们每一个时间步长中都是共享权值的,所以我们也只需要考虑最后的输出结果与真实值之间的误差来进行参数更新。
有了RNN前向传播算法的基础,就容易推导出RNN反向传播算法的流程了。RNN反向传播算法的思路和DNN是一样的,即通过梯度下降法一轮轮的迭代,得到合适的RNN模型参数
U
,
W
,
V
,
b
,
c
U,W,V,b,c
U,W,V,b,c。由于我们是基于时间反向传播,所以RNN的反向传播有时也叫做BPTT(back-propagation through time)。当然这里的BPTT和DNN也有很大的不同点,即这里所有的
U
,
W
,
V
,
b
,
c
U,W,V,b,c
U,W,V,b,c在序列的各个位置是共享的,反向传播时我们更新的是相同的参数。
为了简化描述,这里的损失函数我们为交叉熵损失函数,输出的激活函数为softmax函数,隐藏层的激活函数为tanh函数。对于RNN,由于我们在序列的每个位置都有损失函数,因此最终的损失
L
L
L为:
L
=
∑
t
=
1
τ
L
(
t
)
L = \sum\limits_{t=1}^{\tau}L^{(t)}
L=t=1∑τL(t)
其中
V
,
c
,
V,c,
V,c,的梯度计算是比较简单的:
∂
L
∂
c
=
∑
t
=
1
τ
∂
L
(
t
)
∂
c
=
∑
t
=
1
τ
y
^
(
t
)
−
y
(
t
)
∂
L
∂
V
=
∑
t
=
1
τ
∂
L
(
t
)
∂
V
=
∑
t
=
1
τ
(
y
^
(
t
)
−
y
(
t
)
)
(
h
(
t
)
)
T
\frac{\partial L}{\partial c} = \sum\limits_{t=1}^{\tau}\frac{\partial L^{(t)}}{\partial c} = \sum\limits_{t=1}^{\tau}\hat{y}^{(t)} - y^{(t)}\frac{\partial L}{\partial V} =\sum\limits_{t=1}^{\tau}\frac{\partial L^{(t)}}{\partial V} = \sum\limits_{t=1}^{\tau}(\hat{y}^{(t)} -y^{(t)}) (h^{(t)})^T
∂c∂L=t=1∑τ∂c∂L(t)=t=1∑τy^(t)−y(t)∂V∂L=t=1∑τ∂V∂L(t)=t=1∑τ(y^(t)−y(t))(h(t))T
但是
W
,
U
,
b
W,U,b
W,U,b的梯度计算就比较的复杂了。从RNN的模型可以看出,在反向传播时,在在某一序列位置t的梯度损失由当前位置的输出对应的梯度损失和序列索引位置
t
+
1
t+1
t+1时的梯度损失两部分共同决定。对于
W
W
W在某一序列位置t的梯度损失需要反向传播一步步的计算。我们定义序列索引
t
t
t位置的隐藏状态的梯度为:
δ
(
t
)
=
∂
L
∂
h
(
t
)
\delta^{(t)} = \frac{\partial L}{\partial h^{(t)}}
δ(t)=∂h(t)∂L
这样我们可以像DNN一样从
δ
(
t
+
1
)
\delta^{(t+1)}
δ(t+1)递推
δ
(
t
)
\delta^{(t)}
δ(t) 。
δ
(
t
)
=
(
∂
o
(
t
)
∂
h
(
t
)
)
T
∂
L
∂
o
(
t
)
+
(
∂
h
(
t
+
1
)
∂
h
(
t
)
)
T
∂
L
∂
h
(
t
+
1
)
=
V
T
(
y
^
(
t
)
−
y
(
t
)
)
+
W
T
δ
(
t
+
1
)
d
i
a
g
(
1
−
(
h
(
t
+
1
)
)
2
)
\delta^{(t)} =(\frac{\partial o^{(t)}}{\partial h^{(t)}})^T\frac{\partial L}{\partial o^{(t)}} + (\frac{\partial h^{(t+1)}}{\partial h^{(t)}})^T\frac{\partial L}{\partial h^{(t+1)}} = V^T(\hat{y}^{(t)} - y^{(t)}) + W^T\delta^{(t+1)}diag(1-(h^{(t+1)})^2)
δ(t)=(∂h(t)∂o(t))T∂o(t)∂L+(∂h(t)∂h(t+1))T∂h(t+1)∂L=VT(y^(t)−y(t))+WTδ(t+1)diag(1−(h(t+1))2)
对于
δ
(
τ
)
\delta^{(\tau)}
δ(τ),由于它的后面没有其他的序列索引了,因此有:
δ
(
τ
)
=
(
∂
o
(
τ
)
∂
h
(
τ
)
)
T
∂
L
∂
o
(
τ
)
=
V
T
(
y
^
(
τ
)
−
y
(
τ
)
)
\delta^{(\tau)} =(\frac{\partial o^{(\tau)}}{\partial h^{(\tau)}})^T\frac{\partial L}{\partial o^{(\tau)}} = V^T(\hat{y}^{(\tau)} - y^{(\tau)})
δ(τ)=(∂h(τ)∂o(τ))T∂o(τ)∂L=VT(y^(τ)−y(τ))
有了
δ
(
t
)
\delta^{(t)}
δ(t),计算
W
,
U
,
b
W,U,b
W,U,b就容易了,这里给出
W
,
U
,
b
W,U,b
W,U,b的梯度计算表达式:
∂
L
∂
W
=
∑
t
=
1
τ
d
i
a
g
(
1
−
(
h
(
t
)
)
2
)
δ
(
t
)
(
h
(
t
−
1
)
)
T
∂
L
∂
b
=
∑
t
=
1
τ
d
i
a
g
(
1
−
(
h
(
t
)
)
2
)
δ
(
t
)
∂
L
∂
U
=
∑
t
=
1
τ
d
i
a
g
(
1
−
(
h
(
t
)
)
2
)
δ
(
t
)
(
x
(
t
)
)
T
\frac{\partial L}{\partial W} = \sum\limits_{t=1}^{\tau}diag(1-(h^{(t)})^2)\delta^{(t)}(h^{(t-1)})^T\frac{\partial L}{\partial b}\\ = \sum\limits_{t=1}^{\tau}diag(1-(h^{(t)})^2)\delta^{(t)}\frac{\partial L}{\partial U}\\ =\sum\limits_{t=1}^{\tau}diag(1-(h^{(t)})^2)\delta^{(t)}(x^{(t)})^T
∂W∂L=t=1∑τdiag(1−(h(t))2)δ(t)(h(t−1))T∂b∂L=t=1∑τdiag(1−(h(t))2)δ(t)∂U∂L=t=1∑τdiag(1−(h(t))2)δ(t)(x(t))T
参考资料
An Introduction to Recurrent Neural Networks
简单案例](https://blog.csdn.net/sinat_24070543/article/details/75113014)