正文
作者:chen_h
微信号 & QQ:862251340
微信公众号:coderpai
我的博客:
请点击这里
这篇教程是翻译
Peter Roelants
写的循环神经网络教程,作者已经授权翻译,这是
原文
。
该教程将介绍如何实现一个循环神经网络(RNN),一共包含两部分。你可以在以下链接找到完整内容。
这篇教程中的代码是由 Python 2
IPython Notebook
产生的,在教程的最后,我会给出全部代码的链接,帮助学习。神经网络中有关矩阵的运算我们采用
NumPy
来构建,画图使用
Matplotlib
来构建。如果你来没有安装这些软件,那么我强烈建议你使用
Anaconda Python
来安装,这个软件包中包含了运行这个教程的所有软件包,非常方便使用。
循环神经网络
本教程主要包含三部分:
-
一个非常简单的循环神经网络(RNN)
-
基于时序的反向传播(BPTT)
-
弹性优化算法
循环神经网络
是一种可以解决序列数据的模型。在时序模型上面,这种
循环关系
可以定义成如下式子:
其中,
Sk
表示在时间
k
时刻的状态,
Xk
是在时序
k
时刻的输入数据,
Wrec
和
Wx
都是神经网络的链接权重。如果简单的理解,可以把RNN理解成是一个带
反馈回路
的状态模型。由于循环关系和延时处理,时序状态被加入了模型之中。这个延时操作赋予了模型记忆力,因为它能记住模型前面一个状态。
神经网络最后的输出结果
Yk
是在时间
k
时刻计算出来的,即是通过前面一个或者多个状态
Sk
,....,
Sk+j
计算出来的。
接下来,我们就可以通过输入的数据
Xk
和前一步的状态
S(k-1)
,来计算当前的状态
S(k)
,或者通过输入的数据
Xk
和前一步的状态
S(k)
来预测下一步的状态
S(k+1)
。
这篇教程会说明循环神经网络和一般的前馈神经网络没有很大的不同,但是在训练的方式上面可能会有一些不同。
线性循环神经网络
这部分教程我们来设计一个简单的RNN模型,这个模型的输入是一个二进制的数据流,任务是去计算这个二进制的数据流中存在几个1。
在这个教程中,我们设计的RNN模型中的状态只有一维,在每个时间点上,输入数据也是一维的,最后输出的结果就是序列状态的最后一个状态,即
y = S(k)
。我们将RNN模型进行展开,就可以得到下图的模型。注意,展开的模型可以看做是一个 (n+1) 层的神经网络,每一层使用相同的链接权重
Wrec
和
Wx
。
虽然实现和训练这个模型是一件非常有意思的事情,但是我们可以很容易得到,当
W(rec) = W(x) = 1
时,模型是最优的。
我们先导入教程需要的软件包
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import LogNorm
定义数据集
输入数据集
X
一共有20组数据,每组数据的长度是10,即每组数据的时间状态步长是10。输入数据是由均匀的随机分布产生的,取值 0 或者 1 。
输出结果是输入的二进制数据流中存在几个1,也就是把序列的每一位都加起来求和的结果。
nb_of_samples = 20
sequence_len = 10
X = np.zeros((nb_of_samples, sequence_len))
for row_idx in range(nb_of_samples):
X[row_idx,:] = np.around(np.random.rand(sequence_len)).astype(int)
t = np.sum(X, axis=1)
通过基于时序的反向传播(BPTT)算法进行训练
训练RNN的一个典型算法是
BPTT(backpropagation through time)
算法。通过名字,你也能发现这是一个基于
BP
的算法。
如果你很了解常规的BP算法,那么BPTT算法和常规的BP算法没有很大的不同。唯一的不同是,RNN需要每一个特定的时间步骤中,将每个神经元进行展开处理而已。展开图已经在教程的最前面进行了说明。展开后,模型就和规则的神经网络模型很像了。唯一不同是,RNN有多个输入源(前一个时间步骤的输入状态和当前的输入数据)和每一层中的链接矩阵( W(rec)和W(x) )都是一样的。
正向传播计算RNN的输出结果
正向传播的时候,我们会把RNN展开进行处理,这样就可以按照规则的神经网络进行处理了。RNN模型最后的输出结果将会被使用在损失函数的计算中,用于训练网络。(其实这些都和常规的多层神经网络一样。)
当我们将RNN进行展开计算时,在不同的时间点上面,其实循环关系是相同的,我们将这个相同的循环关系在
update_state
函数中实现了。
forward_states
函数通过 for 循环,将
update_state
函数应用到每一个时间点上面。如果我们将这些步骤都矢量化,那么就可以进行并行计算了。跟常规神经网络一样,我们需要给权重进行初始化。在这个教程中,我们将权重初始化为0。
最后,我们通过累加所以输入数据的误差进行计算均方误差函数(MSE)来得到损失函数
ξ
。在程序中,我们使用
cost
函数来实现。
def update_state(xk, sk, wx, wRec):
"""
Compute state k from the previous state (sk) and current input (xk),
by use of the input weights (wx) and recursive weights (wRec).
"""
return xk * wx + sk * wRec
def forward_states(X, wx, wRec):
"""
Unfold the network and compute all state activations given the input X,
and input weights (wx) and recursive weights (wRec).
Return the state activations in a matrix, the last column S[:,-1] contains the
final activations.
"""
S = np.zeros((X.shape[0], X.shape[1]+1))
for k in range(0, X.shape[1]):
S[:,k+1] = update_state(X[:,k], S[:,k], wx, wRec)
return S
def cost(y, t):
"""
Return the MSE between the targets t and the outputs y.
"""
return ((t - y)**2).sum() / nb_of_samples
反向传播的梯度计算
在进行反向传播过程之前,我们需要先计算误差的对于输出结果的梯度
∂ξ/∂y
,函数
output_gradient
实现了这个梯度计算过程。这个梯度将会被通过反向传播算法一层一层的向前传播,函数
backward_gradient
实现了这个计算过程。具体的数学推导如下所示:
梯度最开始的计算公式为:
其中,n 表示RNN展开之后的时间步长。需要注意的是,参数
Wrec
担当着反向传递误差的角色。
损失函数对于权重的梯度是通过累加每一层中的梯度得到的。具体数学公式如下:
def output_gradient(y, t):
"""
Compute the gradient of the MSE cost function with respect to the output y.
"""
return 2.0 * (y - t) / nb_of_samples
def backward_gradient(X, S, grad_out, wRec):
"""
Backpropagate the gradient computed at the output (grad_out) through the network.
Accumulate the parameter gradients for wX and wRec by for each layer by addition.
Return the parameter gradients as a tuple, and the gradients at the output of each layer.
"""
grad_over_time = np.zeros((X.shape[0], X.shape[1]+1))
grad_over_time[:,-1] = grad_out
wx_grad = 0
wRec_grad = 0
for k in range(X.shape[1], 0, -1):
wx_grad += np.sum(grad_over_time[:,k] * X[:,k-1])
wRec_grad += np.sum(grad_over_time[:,k] * S[:,k-1])
grad_over_time[:,k-1] = grad_over_time[:,k] * wRec
return (wx_grad, wRec_grad), grad_over_time
梯度检查
对于RNN,我们也需要对其进行梯度检查,具体的检查方法可以
参考
在常规多层神经网络中的梯度检查。如果在反向传播中的梯度计算正确,那么这个梯度值应该和
数值计算
出来的梯度值应该是相同的。
params = [1.2, 1.2]
eps = 1e-7
S = forward_states(X, params[0], params[1])
grad_out = output_gradient(S[:,-1], t)
backprop_grads, grad_over_time = backward_gradient(X, S, grad_out, params[1])
for p_idx, _ in enumerate(params):
grad_backprop = backprop_grads[p_idx]
params[p_idx] += eps
plus_cost = cost(forward_states(X, params[0], params[1])[:,-1], t)
params[p_idx] -= 2 * eps
min_cost = cost(forward_states(X, params[0], params[1])[:,-1], t)
params[p_idx] += eps
grad_num = (plus_cost - min_cost) / (2*eps)
if not np.isclose(grad_num, grad_backprop):
raise ValueError('Numerical gradient of {:.6f} is not close to the backpropagation gradient of {:.6f}!'.format(float(grad_num), float(grad_backprop)))
print('No gradient errors found')
No gradient errors found
参数更新
由于
不稳定的梯度
,RNN是非常难训练的。这也使得一般对于梯度的优化算法,比如梯度下降,都不能使得RNN找到一个好的局部最小值。
我们在下面的两张图中说明了RNN梯度的不稳定性。第一张图表示,当我们给定 w(x) 和 w(rec) 时得到的损失表面图。图中带颜色标记的地方,是我们取了几个值做的实验结果。从图中,我们可以发现,当误差表面的值接近于0时,w(x) = w(rec) = 1。但是当 |w(rec)| > 1时,误差表面的值增加的非常迅速。
第二张图我们通过几组数据模拟了梯度的不稳定性,这个随着时间步长而不稳定的梯度的形式和
等比数列
的形式很像,具体数学公式如下:
在状态S(k)时的梯度,反向传播m步得到的状态S(k-m)可以被写成:
在我们简单的线性模型中,如果 |w(rec)| > 1,那么梯度是一个指数爆炸的增长。如果 |w(rec)| < 1,那么梯度将会消失。
关于指数暴涨,在第二张图中,当我们取 w(x) =1, w(rec) = 2时,在图中显示梯度是指数爆炸增长的,当我们取 w(x) =1, w(rec) = -2时,正负徘徊指数增长,为什么会出现徘徊?是因为我们把参数 w(rec) 取成了负数。这个指数爆炸说明了,模型的训练对参数 w(rec) 是非常敏感的。