循环神经网络RNN结构与全连接或者卷积神经网络结构相比,RNN比较复杂。通过阅读各种资料,觉得《TensorFlow:实战Google深度学习框架》中RNN讲的最通俗易懂,在这里将简单的总结一下书上的内容。
循环神经网络的主要用途是处理和预测序列数据,为了刻画一个序列当前的输出与之前信息的关系。从网络结构上看,RNN会利用之前的信息影响后面结点的输出,也就是RNN的隐藏层之间的结点是有连接的,隐藏层的输入不仅包含输入层的输入,还包含上一时刻隐藏层的输出。
这张图就是RNN循环体按时间展开后的结构。X为网络输入,A为隐藏层状态,h为输出。在每一个时刻会有一个输入Xl,然后RNN当前的状态Al提供一个输出。而当前状态Al是根据上一时刻状态Al-1和当前的输入Xl共同决定的。
如何将状态信息循环输入神经网络,这个循环结构被称之为循环体。下图是一个使用最简单的循环体结构的RNN,在这个循环体中只是用了一个类似全连接层的神经网络结构。
循环神经网络中的状态是通过一个向量来存储的,这个向量的维度也被称为RNN隐藏层的大小,设为h。从图中可以看出,循环体的输入有两个部分,一部分为上一时刻的状态,另一部分为当前时刻的输入样本。对于语言模型来说,输入样例可以是当前单词对应的单词向量word embedding。
若输入向量的维度为x,则循环体的全连接层神经网络的输入大小为h+x,也就是将上一时刻的状态和输入向量拼接起来。该循环体的输出为当前时刻的状态,所以输出层的大小也为h,所以循环体中神经网络的所有参数个数为(h+x)*h(权重)+h(偏置)个。但是循环体输出还作为RNN的输出,所以,循环体还需要另外一个全连接神经网络来完成这个过程。下图为一个循环神经网络前向传播的全部过程。
图中输入输出的维度都是1,循环体中全连接层权重为wrnn,偏置项为brnn,用于RNN输出的全连接层权重为woutput,偏置项为boutput。初始循环体状态为[0,0],因为当前输出为1,所以拼接得到的向量为[0,0,1],通过循环体中的全连接层神经网络得到的结果为:
这一结果将作为下一时刻的输入状态,同时循环体也会使用该状态生成RNN的输出,将该向量作为输入给用于输出的全连接神经网络可以得到当前时刻的最终输出:
在得到循环神经网络的前向传播过程之后,可以和其他神经网络类似地定义损失函数,唯一区别就在于RNN在每个时刻都有一个输出,所以RNN的总损失为所有(部分)时刻上的损失函数的总和。
下面代码是用python简单的实现了一个RNN的前向传播过程,结合上面的过程图能更好地理解。
import numpy as np
X = [1, 2] # X为输入序列
state = [0.0, 0.0] # 隐藏层初始值
# 循环体中的全连接层参数,分开定义不同输入的权重
w_cell_state = np.asarray([[0.1, 0.2], [0.3, 0.4]]) # 隐藏层状态state权重
w_cell_input = np.asarray([0.5, 0.6]) # 输入序列进隐藏层所对应的权重
b_cell = np.asarray([0.1, -0.1]) # 偏置项
# 用于网络输出的全连接层参数
w_output = np.asarray([[1.0], [2.0]])
b_output = 0.1
# 按照输入序列时间顺序执行循环神经网络的前向传播过程
for i in range(len(X)):
# 计算循环体中的全连接神经网络, np.dot为矩阵乘法
before_activation = np.dot(state, w_cell_state) + X[i] * w_cell_input + b_cell
# 当前的隐藏层状态state
state = np.tanh(before_activation)
# 根据当前时刻状态计算最终输出
final_output = np.dot(state, w_output) + b_output
# 输出每个时刻的信息
print("before activation:", before_activation)
print("state:", state)
print("output", final_output)
输出为:
before activation: [0.6 0.5]
state: [0.53704957 0.46211716]
output [1.56128388]
before activation: [1.2923401 1.39225678]
state: [0.85973818 0.88366641]
output [2.72707101]
理论上RNN可以支持任意长度的序列,然而在实际中,如果序列过长会导致优化时出现梯度弥散的问题,所以实际中一般会规定一个最大长度。