1、概述
本来想继续学习tensorflow图像方面的应用,但是循环神经网络的某一个应用吸引到了我,所以就先学学这个循环神经网络。
2、用处
前面学习的全连接神经网络或者卷积神经网络,网络结构都是从输入层,到隐含层,最后到输出层,层与层之间是全连接或者部分连接,但是,每层之间的节点是没有连接的。这样就无法处理和预测序列数据,或者说是没有“记忆”的。而循环神经网络的主要用途就是处理和预测序列数据的。
3、结构
如上图就是循环神经网络经典结构,循环神经网络结构一个很重要的概念是时刻,网络会对每一个时刻的输入结合当前模型的状态给出一个输出。将上面的模型展开可以得到下图所示的结构图。
从上图可以看到,对于每一个时刻都会有一个输入Xt,根据当前状态At和上一个时刻At-1的结果得到一个输出ht。
4、前向传播
为了更直观的了解这个过程,下面给出一个简单的循环神经网络前向传播的具体计算过程。
如上图所示,输入和输出的维度就是1,状态的维度为2,状态内部全连接层权重Wrnn为
Wrnn = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]
偏置brnn为
brnn = [0.1, -0.1]
输出的全连接层权重Wout为
Wout = [[1.0], [2.0]]
偏置bout为
bout = 0.1
对于t0时刻,由于没有上一时刻,所以初始化状态为[0, 0],当前的输出为1,将这两个拼接为一个向量[0, 0, 1],通过循环体的全连接神经网络后得到的结果h0为:
tanh([0, 0, 1]×[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] + [0.1, -0.1]) = tanh([0.6, 0.5])=[0.537, 0.462]
这个结果既作为下一个时刻的输入,又作为输入提供给输出全连接神经网络,所以t0时刻的输出可以计算为:
[0.537, 0.462]×[[1.0], [2.0]]+0.1 = 1.56
而下一刻t1的输入2与t0时刻得到的结果h0拼接成的向量[0.537, 0.462, 2]又作为t1时刻的循环体的输入。
从上面的例子就可以看到,循环神经网络的输入是跟上一时刻循环体的输出是有关联的。得到前向传播结果后,就可以定义损失函数了,跟以前的神经网络不同的是,循环神经网络的总损失为所有时刻(或部分时刻)损失函数的和。
5、用循环神经网络模仿二进制减法
下面用一个网上比较流行的RNN模仿二进制减法的例子用来说明RNN工作原理,该例子用python写的,没用到tensorflow框架。
5.1、导入库,并定义sigmoid函数及其导数
#coding: utf-8
import copy
import numpy as np
#定义sigmoid激活函数
def sigmoid(x):
output = 1/(1+np.exp(-x))
return output
#定义sigmoid激活函数的导数,用于计算梯度下降
#sigmoid(x)函数的额导数为 y` = y * (1 - y),其中y指的是sigmoid函数
#所以output传入的是sigmoid函数的输出
def sigmoid_output_to_derivative(output):
return output*(1-output)
5.2、定义十进制数转二进制映射
#二进制的位数,这里只计算8位的
binary_dim = 8
#8位二进制的最大数,即2的8次方
largest_number = pow(2,binary_dim)
#int2binary用于整数到二进制表示的映射
#比如十进制数2的二进制表示,可以写为int2binary[2]
int2binary = {}
#这里将十进制数0-255转成二进制表示,
# 再将其存到int2binary中,所以十进制数2的二进制才可以用int2binary[2]表示
binary = np.unpackbits(
np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
for i in range(largest_number):
int2binary[i] = binary[i]
5.3、设置学习率、输入层维度、隐藏层维度、输出层维度
#学习率
learning_rate = 0.9
#循环体的输入的维度,比如计算 11 - 6,其二进制形式分别对应如下:
# 00001011
# 00000110
# 这里的输入分别是这两个数对应下标的bit,
# 比如第7个bit的输入为[1, 0],第6个bit为[1, 1],第5个为[0, 1],以此类推
input_dim = 2
#循环体内隐藏层的维度
hidden_dim = 16
#输出的维度,比如上面第7个bit输入为[1, 0],不管做加法还是减法,其都对应一个bit的输出
output_dim = 1
5.4、定义权重并初始化,以及定义用于存放反向传播权重梯度值的变量
# 定义神经网络的权重的形状并初始化,
# W_input_hidden 是输入层到隐藏层的权重
# W_hidden_output 是隐藏层到输出层的权重
# W_hidden 是隐藏层的权重
# 因为输入层形状为(1, 2),输出层形状为(1),隐藏层形状为(1, 16)
# W_input_hidden 的形状为(2, 16), W_hidden_output 形状为(16, 1), W_hidden 的形状为(16, 16)
# 训练过程就是更新这三个权重的过程
W_input_hidden = (2 * np.random.random((input_dim, hidden_dim)) - 1) * 0.05
W_hidden_output = (2 * np.random.random((hidden_dim, output_dim)) - 1) * 0.05
W_hidden = (2 * np.random.random((hidden_dim, hidden_dim)) - 1) * 0.05
# 用于存放反向传播的权重梯度值
W_input_hidden_update = np.zeros_like(W_input_hidden)
W_hidden_output_update = np.zeros_like(W_hidden_output)
W_hidden_update = np.zeros_like(W_hidden)
5.5、开始训练
# 开始训练
for j in range(10000):
下面所有步骤都在这个循环内,一共10000次训练
5.5、随机生成被减数和减数,并计算结果,然后将它们都转成二进制形式
# 生成一个被减数a,a的范围在[0,256)的整数
a_int = np.random.randint(largest_number)
# 生成减数b,减数的范围在[0, 128)的整数。
b_int = np.random.randint(largest_number / 2)
# 如果被减数比减数小,则互换,
# 我们暂时不考虑负数,所以要确保被减数比减数大
if a_int < b_int:
tmp = a_int
b_int = a_int
a_int = tmp
#将其转为二进制的形式
a = int2binary[a_int]
b = int2binary[b_int]
#这里c保存的是a-b的答案的二进制形式
c_int = a_int - b_int
c = int2binary[c_int]
5.6、定义用来存储循环体输出层的误差倒数和隐藏层的值的list
# 存储每个循环体输出层的误差导数
layer_output_deltas = list()
# 存储每个循环体隐藏层的值
layer_hidden_values = list()
5.7、初始化总误差和隐藏层
# 初始化总误差
over_all_error = 0
# 一开始没有隐藏层,所以初始化一下原始值为0.1
layer_hidden_values.append(np.ones(hidden_dim) * 0.1)
5.8、前向传播
# 前向传播, 循环遍历每一个二进制位
# 算法如下:
# 隐藏层 layer_hidden = sigmoid(np.dot(X, W_input_hidden) + np.dot(layer_hidden_values[-1], W_hidden))
# 输出层 layer_output = sigmoid(np.dot(layer_hidden, W_hidden_output))
for position in range(binary_dim):
# 从低位开始,每次取被减数a和减数b的一个bit位作为循环体的输入
X = np.array([[a[binary_dim - position - 1], b[binary_dim - position - 1]]])
# 这里则取答案c相应的bit位,作为与预测值的对比,以取得预测误差
y = np.array([[c[binary_dim - position - 1]]]).T
# 计算隐藏层,新的隐藏层 = 输入层 + 旧隐藏层
# 这里 X是循环体的输入,layer_hidden_values[-1]是上一个循环体的隐藏层的值,
# 从这里就能看到,循环体的输出不止跟本次的输入有关,还跟上一个循环体也有关
layer_hidden = sigmoid(np.dot(X, W_input_hidden) + np.dot(layer_hidden_values[-1], W_hidden))
# 输出层,这个就是本次循环体的预测值
layer_output = sigmoid(np.dot(layer_hidden, W_hidden_output))
# 预测值与实际值的误差
layer_output_error = y - layer_output
# 把每一个循环体的误差导数都保存下来
layer_output_deltas.append((layer_output_error) * sigmoid_output_to_derivative(layer_output))
# 计算总误差
over_all_error += np.abs(layer_output_error[0])
# 保存预测bit位,这里对预测值进行四舍五入,即保存的bit值要么是0,要么是1
d[binary_dim - position - 1] = np.round(layer_output[0][0])
# 保存本次循环体的隐藏层,供下个循环体使用
layer_hidden_values.append(copy.deepcopy(layer_hidden))
5.9、反向传播
future_layer_hidden_delta = np.zeros(hidden_dim)
# 反向传播,从最后一个循环体到第一个循环体
for position in range(binary_dim):
# 获取循环体的输入
X = np.array([[a[position], b[position]]])
# 当前循环体的隐藏层
layer_hidden = layer_hidden_values[-position - 1]
# 上一个循环体的隐藏层
prev_layer_hidden = layer_hidden_values[-position - 2]
# 获取当前循环体的输出误差导数
layer_output_delta = layer_output_deltas[-position - 1]
# 计算当前隐藏层的误差
# 通过后一个循环体(因为是反向传播)的隐藏层误差和当前循环体的输出层误差,计算当前循环体的隐藏层误差
layer_hidden_delta = (future_layer_hidden_delta.dot(W_hidden.T) +
layer_output_delta.dot(W_hidden_output.T)) * sigmoid_output_to_derivative(layer_hidden)
# 等到完成了所有反向传播误差计算, 才会更新权重矩阵,先暂时把更新矩阵存起来。
W_input_hidden_update += X.T.dot(layer_hidden_delta)
W_hidden_output_update += np.atleast_2d(layer_hidden).T.dot(layer_output_delta)
W_hidden_update += np.atleast_2d(prev_layer_hidden).T.dot(layer_hidden_delta)
future_layer_hidden_delta = layer_hidden_delta
# 完成所有反向传播之后,更新权重矩阵。并把矩阵变量清零
W_input_hidden += W_input_hidden_update * learning_rate
W_hidden_output += W_hidden_output_update * learning_rate
W_hidden += W_hidden_update * learning_rate
W_input_hidden_update *= 0
W_hidden_output_update *= 0
W_hidden_update *= 0
5.10、打印训练效果
# 每800次打印一次结果
if (j % 800 == 0):
print("All Error:" + str(over_all_error))
print("Pred:" + str(d))
print("True:" + str(c))
out = 0
#将二进制形式转成十进制
for index, x in enumerate(reversed(d)):
out += x * pow(2, index)
print(str(a_int) + " - " + str(b_int) + " = " + str(out))
print("------------")
5.11、运行结果
All Error:[4.03416348]
Pred:[1 1 1 1 1 1 1 1]
True:[0 1 0 1 0 0 0 1]
86 - 5 = 255
------------
All Error:[3.78163038]
Pred:[0 0 0 0 0 0 0 0]
True:[0 1 0 0 1 0 1 1]
118 - 43 = 0
------------
。。。。。。
All Error:[0.13360963]
Pred:[0 1 1 1 0 0 0 1]
True:[0 1 1 1 0 0 0 1]
132 - 19 = 113
------------
All Error:[0.06920067]
Pred:[0 0 0 1 0 1 0 1]
True:[0 0 0 1 0 1 0 1]
133 - 112 = 21
------------
可以看到,一开始总误差很大,随着训练次数的增加,总误差也在减小,一开始做的减法也是错的,后来就对了。
6、完整代码
#coding: utf-8
import copy
import numpy as np
#定义sigmoid激活函数
def sigmoid(x):
output = 1/(1+np.exp(-x))
return output
#定义sigmoid激活函数的导数,用于计算梯度下降
#sigmoid(x)函数的额导数为 y` = y * (1 - y),其中y指的是sigmoid函数
#所以output传入的是sigmoid函数的输出
def sigmoid_output_to_derivative(output):
return output*(1-output)
#二进制的位数,这里只计算8位的
binary_dim = 8
#8位二进制的最大数,即2的8次方
largest_number = pow(2,binary_dim)
#int2binary用于整数到二进制表示的映射
#比如十进制数2的二进制表示,可以写为int2binary[2]
int2binary = {}
#这里将十进制数0-255转成二进制表示,
# 再将其存到int2binary中,所以十进制数2的二进制才可以用int2binary[2]表示
binary = np.unpackbits(
np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
for i in range(largest_number):
int2binary[i] = binary[i]
#学习率
learning_rate = 0.9
#循环体的输入的维度,比如计算 11 - 6,其二进制形式分别对应如下:
# 00001011
# 00000110
# 这里的输入分别是这两个数对应下标的bit,
# 比如第7个bit的输入为[1, 0],第6个bit为[1, 1],第5个为[0, 1],以此类推
input_dim = 2
#循环体内隐藏层的维度
hidden_dim = 16
#输出的维度,比如上面第7个bit输入为[1, 0],不管做加法还是减法,其都对应一个bit的输出
output_dim = 1
# 定义神经网络的权重的形状并初始化,
# W_input_hidden 是输入层到隐藏层的权重
# W_hidden_output 是隐藏层到输出层的权重
# W_hidden 是隐藏层的权重
# 因为输入层形状为(1, 2),输出层形状为(1),隐藏层形状为(1, 16)
# W_input_hidden 的形状为(2, 16), W_hidden_output 形状为(16, 1), W_hidden 的形状为(16, 16)
# 训练过程就是更新这三个权重的过程
W_input_hidden = (2 * np.random.random((input_dim, hidden_dim)) - 1) * 0.05
W_hidden_output = (2 * np.random.random((hidden_dim, output_dim)) - 1) * 0.05
W_hidden = (2 * np.random.random((hidden_dim, hidden_dim)) - 1) * 0.05
# 用于存放反向传播的权重梯度值
W_input_hidden_update = np.zeros_like(W_input_hidden)
W_hidden_output_update = np.zeros_like(W_hidden_output)
W_hidden_update = np.zeros_like(W_hidden)
# 开始训练
for j in range(10000):
# 生成一个被减数a,a的范围在[0,256)的整数
a_int = np.random.randint(largest_number)
# 生成减数b,减数的范围在[0, 128)的整数。
b_int = np.random.randint(largest_number / 2)
# 如果被减数比减数小,则互换,
# 我们暂时不考虑负数,所以要确保被减数比减数大
if a_int < b_int:
tmp = a_int
b_int = a_int
a_int = tmp
#将其转为二进制的形式
a = int2binary[a_int]
b = int2binary[b_int]
#这里c保存的是a-b的答案的二进制形式
c_int = a_int - b_int
c = int2binary[c_int]
# 存储神经网络的预测值的二进制形式
d = np.zeros_like(c)
# 存储每个循环体输出层的误差导数
layer_output_deltas = list()
# 存储每个循环体隐藏层的值
layer_hidden_values = list()
# 初始化总误差
over_all_error = 0
# 一开始没有隐藏层,所以初始化一下原始值为0.1
layer_hidden_values.append(np.ones(hidden_dim) * 0.1)
# 前向传播, 循环遍历每一个二进制位
# 算法如下:
# 隐藏层 layer_hidden = sigmoid(np.dot(X, W_input_hidden) + np.dot(layer_hidden_values[-1], W_hidden))
# 输出层 layer_output = sigmoid(np.dot(layer_hidden, W_hidden_output))
for position in range(binary_dim):
# 从低位开始,每次取被减数a和减数b的一个bit位作为循环体的输入
X = np.array([[a[binary_dim - position - 1], b[binary_dim - position - 1]]])
# 这里则取答案c相应的bit位,作为与预测值的对比,以取得预测误差
y = np.array([[c[binary_dim - position - 1]]]).T
# 计算隐藏层,新的隐藏层 = 输入层 + 旧隐藏层
# 这里 X是循环体的输入,layer_hidden_values[-1]是上一个循环体的隐藏层的值,
# 从这里就能看到,循环体的输出不止跟本次的输入有关,还跟上一个循环体也有关
layer_hidden = sigmoid(np.dot(X, W_input_hidden) + np.dot(layer_hidden_values[-1], W_hidden))
# 输出层,这个就是本次循环体的预测值
layer_output = sigmoid(np.dot(layer_hidden, W_hidden_output))
# 预测值与实际值的误差
layer_output_error = y - layer_output
# 把每一个循环体的误差导数都保存下来
layer_output_deltas.append((layer_output_error) * sigmoid_output_to_derivative(layer_output))
# 计算总误差
over_all_error += np.abs(layer_output_error[0])
# 保存预测bit位,这里对预测值进行四舍五入,即保存的bit值要么是0,要么是1
d[binary_dim - position - 1] = np.round(layer_output[0][0])
# 保存本次循环体的隐藏层,供下个循环体使用
layer_hidden_values.append(copy.deepcopy(layer_hidden))
future_layer_hidden_delta = np.zeros(hidden_dim)
# 反向传播,从最后一个循环体到第一个循环体
for position in range(binary_dim):
# 获取循环体的输入
X = np.array([[a[position], b[position]]])
# 当前循环体的隐藏层
layer_hidden = layer_hidden_values[-position - 1]
# 上一个循环体的隐藏层
prev_layer_hidden = layer_hidden_values[-position - 2]
# 获取当前循环体的输出误差导数
layer_output_delta = layer_output_deltas[-position - 1]
# 计算当前隐藏层的误差
# 通过后一个循环体(因为是反向传播)的隐藏层误差和当前循环体的输出层误差,计算当前循环体的隐藏层误差
layer_hidden_delta = (future_layer_hidden_delta.dot(W_hidden.T) +
layer_output_delta.dot(W_hidden_output.T)) * sigmoid_output_to_derivative(layer_hidden)
# 等到完成了所有反向传播误差计算, 才会更新权重矩阵,先暂时把更新矩阵存起来。
W_input_hidden_update += X.T.dot(layer_hidden_delta)
W_hidden_output_update += np.atleast_2d(layer_hidden).T.dot(layer_output_delta)
W_hidden_update += np.atleast_2d(prev_layer_hidden).T.dot(layer_hidden_delta)
future_layer_hidden_delta = layer_hidden_delta
# 完成所有反向传播之后,更新权重矩阵。并把矩阵变量清零
W_input_hidden += W_input_hidden_update * learning_rate
W_hidden_output += W_hidden_output_update * learning_rate
W_hidden += W_hidden_update * learning_rate
W_input_hidden_update *= 0
W_hidden_output_update *= 0
W_hidden_update *= 0
# 每800次打印一次结果
if (j % 800 == 0):
print("All Error:" + str(over_all_error))
print("Pred:" + str(d))
print("True:" + str(c))
out = 0
#将二进制形式转成十进制
for index, x in enumerate(reversed(d)):
out += x * pow(2, index)
print(str(a_int) + " - " + str(b_int) + " = " + str(out))
print("------------")
总结:
循环神经网络的结构虽然和前面所学的全连接神经网络和卷积神经网络有区别,但是总的训练思路还是相似的,都是通过前向传播得到误差,在经过反向传播修改权重等数据。上面这个例子没有用到tensorflow封装好的优化器训练,所以的自己写反向传播的过程,之前没有具体研究过反向传播的原理和过程,所以下一节我想花点时间去研究研究这个反向传播的原理和过程,并且对比一下全连接神经网络和循环神经网络的反向传播有什么异同。