#
#作者:韦访
#博客:https://blog.csdn.net/rookie_wei
#微信:1007895847
#添加微信的备注一下是CSDN的
#欢迎大家一起学习
#
1、概述
我们前面学习的全连接神经网络或者卷积神经网络,网络结构都是从输入层,到隐含层,最后到输出层,层与层之间是全连接或者部分连接,但是,每层之间的节点是没有连接的。这样就无法处理和预测序列数据,或者说是没有“记忆”的。而循环神经网络的主要用途就是处理和预测序列数据的。我们这一讲就来学习一个有“记忆”的神经网络---循环神经网络RNN。
环境配置:
操作系统:Win10 64位
Python:Python3.7
3、前向传播过程
为了更直观的了解这个过程,下面给出一个简单的循环神经网络前向传播的具体计算过程。
4、用循环神经网络模仿二进制减法
下面用一个网上比较流行的RNN模仿二进制减法的例子用来说明RNN工作原理,该例子用python写的,没用到tensorflow框架。
定义sigmoid函数及其导数
首先导入numpy库,我们这里使用sigmoid函数作为激活函数,所以要定义sigmoid函数和sigmoid函数的导数,sigmoid函数的公式如下,
所以代码如下,
#coding: utf-8
import copy
import numpy as np
#定义sigmoid激活函数
def sigmoid(x):
s = 1/(1+np.exp(-x))
return s
#定义sigmoid激活函数的导数,用于计算梯度下降
#sigmoid(x)函数的额导数为 s` = s * (1 - s),其中s指的是sigmoid函数
#所以s传入的是sigmoid函数的输出
def sigmoid_d(s):
return s*(1-s)
定义十进制数转二进制的映射
因为我们这里要计算的是二进制的减法,所以,要将十进制的数表示成二进制,首先要定义二进制的位数,我们这里只计算8位,即十进制数0~255。然后,再将十进制数0~255表示成二进制以后,放到列表int2binary中,这样就可以用int2binary[n]表示数字n(n为0~255之间的整数)了,代码如下,
#二进制的位数,这里只计算8位的
binary_dim = 8
#8位二进制的最大数,即2的8次方
largest_number = pow(2,binary_dim)
#用于整数0~255转成二进制表示的映射
def int2bin():
#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]
return int2binary
定义循环体的输入、内隐藏层、输出维度
接着,就要定义循环体的输入维度了,以计算11-6=5为例,11和6的二进制形式分别表示如下,
00001011 #对应11
00000110 #对应6
我们的输入数据是被减数和减数的二进制同一下标对应的数,如下图所示,
如上图所示,下标为7的输入数据为[1,0],下标为6的输入数据为[1,1],以此类推。所以输入数据的维度应该为2。
针对二进制加减法,对应于每一个下标的输出结果不是0就是1,只有1位,所以输出维度应该为1,如下图所示,
循环体内隐藏层的维度就由我们自己定义了,这里我们设置为16。
所以,代码如下,
#循环体的输入的维度,比如计算 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
定义权重并初始化
上面定义了循环体的输入、内隐藏层、输出维度,接下来我们就要使用它们了。我们要满足输入层乘以隐藏层乘以输出层符合输入两个数据,经过循环体后输出一个数据的规则。由于是矩阵的乘法,所以输入层的形状应该是(2,16),隐藏层的形状应该是(16,16),输出层的形状应该是(16,1),所以这三个权重的定义如下,
# 定义神经网络的权重的形状并初始化,
# W_input_hidden 是输入层到隐藏层的权重
# W_hidden 是隐藏层的权重
# W_hidden_output 是隐藏层到输出层的权重
# W_input_hidden 的形状为(2, 16), W_hidden 的形状为(16, 16), W_hidden_output 形状为(16, 1)
# 训练过程就是更新这三个权重的过程
self.W_input_hidden = (2 * np.random.random((input_dim, hidden_dim)) - 1) * 0.05
self.W_hidden = (2 * np.random.random((hidden_dim, hidden_dim)) - 1) * 0.05
self.W_hidden_output = (2 * np.random.random((hidden_dim, output_dim)) - 1) * 0.05
定义存放反向传播的权重梯度的变量
接着定义三个跟上面权重形状一样的变量,用于更新权重,
# 用于存放反向传播的权重梯度值
self.W_input_hidden_update = np.zeros_like(self.W_input_hidden)
self.W_hidden_update = np.zeros_like(self.W_hidden)
self.W_hidden_output_update = np.zeros_like(self.W_hidden_output)
随机生成被减数和减数,并计算差
首先随机生成一个被减数和减数,
# 生成一个被减数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)
保存总误差
因为循环神经网络的总损失为所有时刻(或部分时刻)损失函数的和,所以我们要将每一次前向转播中的每一个循环体的误差都保存下来,
# 存储每个循环体输出层的误差导数
self.layer_output_deltas = list()
# 存储每个循环体隐藏层的值
self.layer_hidden_values = list()
前向传播
计算前向传播,我们要遍历每一个二进制位,
如上图所示,因为有8位,所以我们有8个循环体。
def forward_propagation(self, a, b, c, d):
# 前向传播, 循环遍历每一个二进制位
# 算法如下:
# 隐藏层 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))
# 一开始没有隐藏层,所以初始化一下原始值为0.1
self.layer_hidden_values.append(np.ones(self.hidden_dim) * 0.1)
# 初始化总误差
self.over_all_error = 0
for position in range(self.binary_dim):
# 从低位开始,每次取被减数a和减数b的一个bit位作为循环体的输入
X = np.array([[a[self.binary_dim - position - 1], b[self.binary_dim - position - 1]]])
# 这里则取差值c相应的bit位,作为与预测值的对比,以取得预测误差
y = np.array([[c[self.binary_dim - position - 1]]]).T
# 计算隐藏层,新的隐藏层 = 输入层 + 旧隐藏层
# 这里 X是循环体的输入,layer_hidden_values[-1]是上一个循环体的隐藏层的值,
# 从这里就能看到,循环体的输出不止跟本次的输入有关,还跟上一个循环体也有关
layer_hidden = sigmoid(np.dot(X, self.W_input_hidden) + np.dot(self.layer_hidden_values[-1], self.W_hidden))
# 输出层,这个就是本次循环体的预测值
layer_output = sigmoid(np.dot(layer_hidden, self.W_hidden_output))
# 预测值与实际值的误差
layer_output_error = y - layer_output
# 把每一个循环体的误差导数都保存下来
self.layer_output_deltas.append((layer_output_error) * sigmoid_d(layer_output))
# 计算总误差
self.over_all_error += np.abs(layer_output_error[0])
# 保存预测bit位,这里对预测值进行四舍五入,即保存的bit值要么是0,要么是1
d[self.binary_dim - position - 1] = np.round(layer_output[0][0])
# 保存本次循环体的隐藏层,供下个循环体使用
self.layer_hidden_values.append(copy.deepcopy(layer_hidden))
反向传播
def back_propagation(self, a, b):
# 反向传播,从最后一个循环体到第一个循环体
future_layer_hidden_delta = np.zeros(self.hidden_dim)
for position in range(self.binary_dim):
# 获取循环体的输入
X = np.array([[a[position], b[position]]])
# 当前循环体的隐藏层
layer_hidden = self.layer_hidden_values[-position - 1]
# 上一个循环体的隐藏层
prev_layer_hidden = self.layer_hidden_values[-position - 2]
# 获取当前循环体的输出误差导数
layer_output_delta = self.layer_output_deltas[-position - 1]
# 计算当前隐藏层的误差
# 通过后一个循环体(因为是反向传播)的隐藏层误差和当前循环体的输出层误差,计算当前循环体的隐藏层误差
layer_hidden_delta = (future_layer_hidden_delta.dot(self.W_hidden.T) +
layer_output_delta.dot(self.W_hidden_output.T)) * sigmoid_d(layer_hidden)
# 等到完成了所有反向传播误差计算, 才会更新权重矩阵,先暂时把更新矩阵存起来。
self.W_input_hidden_update += X.T.dot(layer_hidden_delta)
self.W_hidden_output_update += np.atleast_2d(layer_hidden).T.dot(layer_output_delta)
self.W_hidden_update += np.atleast_2d(prev_layer_hidden).T.dot(layer_hidden_delta)
future_layer_hidden_delta = layer_hidden_delta
更新权重参数
def update_weight(self):
# 完成所有反向传播之后,更新权重矩阵。并把矩阵变量清零
self.W_input_hidden += self.W_input_hidden_update * self.learning_rate
self.W_hidden_output += self.W_hidden_output_update * self.learning_rate
self.W_hidden += self.W_hidden_update * self.learning_rate
self.W_input_hidden_update *= 0
self.W_hidden_output_update *= 0
self.W_hidden_update *= 0
run函数
Run函数主要就是调用上面的前向传播和反向传播,再更新参数的,代码如下,
def run(self, a, b, c, d):
# 存储每个循环体输出层的误差导数
self.layer_output_deltas = list()
# 存储每个循环体隐藏层的值
self.layer_hidden_values = list()
self.forward_propagation(a, b, c, d)
self.back_propagation(a, b)
self.update_weight()
return self.over_all_error
开始训练
定义完上面个各种功能以后就可以开始训练了,随机生成两个数相减,然后送入RNN训练,代码如下,
# 整数到二进制表示的映射
int2binary = int2bin()
rnn_o = Rnn(input_dim, hidden_dim, output_dim, learning_rate)
# 开始训练
for j in range(10000):
a, b, c, d, a_int, b_int = generate_random_number_and_difference(int2binary)
over_all_error = rnn_o.run(a, b, c, d)
# 每800次打印一次结果
if (j % 100 == 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、完整代码
完整代码链接如下,
https://mianbaoduo.com/o/bread/Y5eXkpo=
下一讲,我们用TensorFlow封装的RNN来识别MNIST数据集。