TensorFlow入门教程(11)循环神经网络RNN

#
#作者:韦访
#博客: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数据集。

 

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值