RNN的基础知识和公式推导参考:
https://www.zybuluo.com/hanbingtao/note/541458
下面的部分实现代码也是基于上面的文章给出的,通过实现其中的组件来加深对RNN原理的认识,并且可以熟悉代码实现的框架。
这里强调下:循环神经网络的训练
循环神经网络的训练算法:BPTT
BPTT算法是针对循环层的训练算法,它的基本原理和BP算法是一样的,也包含同样的三个步骤:
- 前向计算每个神经元的输出值;
- 反向计算每个神经元的误差项值,它是误差函数E对神经元j的加权输入的偏导数;
- 计算每个权重的梯度。
最后再用随机梯度下降算法更新权重。
这里直接贴出代码部分:
activators.py
# -*- coding: UTF-8 -*-
import numpy as np
class ReluActivator(object):
def forward(self, weighted_input):
#return weighted_input
return max(0, weighted_input)
def backward(self, output):
return 1 if output > 0 else 0
class IdentityActivator(object):
def forward(self, weighted_input):
return weighted_input
def backward(self, output):
return 1
class SigmoidActivator(object):
def forward(self, weighted_input):
return 1.0 / (1.0 + np.exp(-weighted_input))
def backward(self, output):
return output * (1 - output)
class TanhActivator(object):
def forward(self, weighted_input):
return 2.0 / (1.0 + np.exp(-2 * weighted_input)) - 1.0
def backward(self, output):
return 1 - output * output
下面是代码的主要部分:
# -*- coding: UTF-8 -*-
import numpy as np
#from cnn import element_wise_op
from activators import ReluActivator, IdentityActivator
# 对numpy数组进行element wise操作
# element_wise_op函数实现了对numpy数组进行按元素操作,并将返回值写回到数组中
def element_wise_op(array, op):
for i in np.nditer(array,op_flags=['readwrite']):
i[...] = op(i)
# 用RecurrentLayer类来实现一个循环层
class RecurrentLayer(object):
def __init__(self, input_width, state_width,activator, learning_rate):
self.input_width = input_width
self.state_width = state_width
self.activator = activator
self.learning_rate = learning_rate
self.times = 0 # 当前时刻初始化为t0
self.state_list = [] # 保存各个时刻的state
self.state_list.append(np.zeros((state_width, 1))) # 初始化s0
self.U = np.random.uniform(-1e-4, 1e-4,(state_width, input_width)) # 初始化U
self.W = np.random.uniform(-1e-4, 1e-4,(state_width, state_width)) # 初始化W
def forward(self, input_array):
'''
根据『式2』进行前向计算
'''
self.times += 1
state = (np.dot(self.U, input_array) +np.dot(self.W, self.state_list[-1]))
element_wise_op(state, self.activator.forward)
self.state_list.append(state)
print 'state_list:\n',self.state_list
def backward(self, sensitivity_array,activator):
'''
实现BPTT算法
'''
self.calc_delta(sensitivity_array, activator)
self.calc_gradient()
def update(self):
'''
按照梯度下降,更新权重
'''
self.W -= self.learning_rate * self.gradient
def calc_delta(self, sensitivity_array, activator):
self.delta_list = [] # 用来保存各个时刻的误差项
for i in range(self.times):
self.delta_list.append(np.zeros((self.state_width, 1)))
self.delta_list.append(sensitivity_array)
print 'delta_list:\n',self.delta_list
# 迭代计算每个时刻的误差项
for k in range(self.times - 1, 0, -1): # [1]
self.calc_delta_k(k, activator)
def calc_delta_k(self, k, activator):
'''
根据k+1时刻的delta计算k时刻的delta
'''
state = self.state_list[k+1].copy()
print 'state_1:\n',state,np.shape(state)
print 'state_2:\n',state[:,0],np.shape(state[:,0])
print 'state_3:\n',np.diag(state[:,0]),np.shape(np.diag(state[:,0]))
element_wise_op(state,activator.backward)
self.delta_list[k] = np.dot(
np.dot(self.delta_list[k+1].T, self.W),np.diag(state[:,0])).T
def calc_gradient(self):
self.gradient_list = [] # 保存各个时刻的权重梯度
for t in range(self.times + 1):
self.gradient_list.append(np.zeros((self.state_width, self.state_width)))
for t in range(self.times, 0, -1):
self.calc_gradient_t(t)
# 实际的梯度是各个时刻梯度之和
self.gradient = reduce(lambda a, b: a + b, self.gradient_list,self.gradient_list[0]) # [0]被初始化为0且没有被修改过
def calc_gradient_t(self, t):
'''
计算每个时刻t权重的梯度
'''
print 'tt..\n',self.delta_list[t],'\n',self.state_list[t-1].T
gradient = np.dot(self.delta_list[t],self.state_list[t-1].T)
print 'np.shape(gradient):',np.shape(gradient)
self.gradient_list[t] = gradient
# 初始化数据
def data_set():
x = [np.array([[1], [2], [3]]),
np.array([[2], [3], [4]])]
d = np.array([[1], [2]])
return x, d
def gradient_check():
'''
梯度检查
'''
# 设计一个误差函数,取所有节点输出项之和
error_function = lambda o: o.sum()
rl = RecurrentLayer(3, 2, IdentityActivator(), 1e-3)
# 计算forward值
x, d = data_set()
rl.forward(x[0])
rl.forward(x[1])
# 求取sensitivity map
sensitivity_array = np.ones(rl.state_list[-1].shape,
dtype=np.float64)
# 计算梯度
rl.backward(sensitivity_array, IdentityActivator())
def test():
# input_width, state_width,activator, learning_rate
l = RecurrentLayer(3, 2, ReluActivator(), 1e-3)
x, d = data_set()
l.forward(x[0])
l.forward(x[1])
l.backward(d, ReluActivator())
return l
if __name__ == '__main__':
test()
运行结果:
state_list:
[array([[ 0.],
[ 0.]]), array([[ 0. ],
[ 0.0001993]])]
state_list:
[array([[ 0.],
[ 0.]]), array([[ 0. ],
[ 0.0001993]]), array([[ 0. ],
[ 0.00026813]])]
delta_list:
[array([[ 0.],
[ 0.]]), array([[ 0.],
[ 0.]]), array([[1],
[2]])]
state_1:
[[ 0. ]
[ 0.00026813]] (2L, 1L)
state_2:
[ 0. 0.00026813] (2L,)
state_3:
[[ 0. 0. ]
[ 0. 0.00026813]] (2L, 2L)
tt..
[[1]
[2]]
[[ 0. 0.0001993]]
np.shape(gradient): (2L, 2L)
tt..
[[ 0.00000000e+00]
[ -3.08449714e-05]]
[[ 0. 0.]]
np.shape(gradient): (2L, 2L)
在代码中添加打印输出,可以清楚的看到中间的运行细节。
注意:
- 这里仅仅是计算了循环层权重矩阵W的梯度更新,没有对误差函数在t时刻对权重矩阵U的梯度进行更新计算,所以这里只是RNN实现过程中的一步。
- 还有一点就是对作者的代码进行了一点改动
def calc_delta_k(self, k, activator):
'''
根据k+1时刻的delta计算k时刻的delta
'''
state = self.state_list[k+1].copy()
element_wise_op(state,activator.backward)
self.delta_list[k] = np.dot(
np.dot(self.delta_list[k+1].T, self.W),np.diag(state[:,0])).T
个人认为这里的element_wise_op中的应用在self.state_list[k+1]的拷贝,而不是self.state_list[k+1],这样就和文章中的推导公式一致了。