循环神经网络实现退位减法器 - TensorFlow

半兽人 发表于: 2019-04-05   最后更新时间: 2019-04-20 15:38:08  
{{totalSubscript}} 订阅, 3,225 游览

简单循环神经网络实现一个退位减法器

本例将把前面所讲述的内容用代码实现一遍。如果前面的描述您还不明白,可以通过这个例子,加深对前面内容的理解。

使用Python搭建一个简单的RNN网络,让它来拟合一个退位减法。退位减法也具有RNN的特性,即输入的两个数相减时,一旦发生退位运算,需要将中间状态保存起来,当高位的数传入时将退位标志一并传入参与运算。

下面就来用代码实现RNN拟合减法,具体步骤如下。

实例描述

使用Ptyhon编写简单循环神经网络拟合一个退位减法的操作,观察其反向传播过程。

1.定义基本函数

首先手动写一个Sigmoid函数及其导数(导数用于反向传播)。

import copy, numpy as np
np.random.seed(0) #固定随机数生成器的种子,可以每次得到一样的值
def sigmoid(x):  # 激活函数
    output = 1 / (1 + np.exp(-x))
    return output

def sigmoid_output_to_derivative(output):  # 激活函数的导数
    return output * (1 - output)

2.建立二进制映射

定义的减法最大值限制在256之内,即8位二进制的减法,定义int与二进制之间的映射数组int2binary

int2binary = {}  # 整数到其二进制表示的映射
binary_dim = 8  # 暂时制作256以内的减法
## 计算0-256的二进制表示
largest_number = pow(2, binary_dim)
binary = np.unpackbits(
    np.array([range(largest_number)], dtype=np.uint8).T, axis=1)
for i in range(largest_number):
    int2binary[i] = binary[i]

3.定义参数

定义学习参数:隐藏层的权重synapse_0、循环节点的权重synapse_h(输入节点16、输出节点16)、输出层的权重synapse_1(输入16节点,输出1节点)。为了减小复杂度,这里只设置w权重,b被忽略。

# 参数设置
alpha = 0.9  # 学习速率
input_dim = 2  # 输入的维度是2
hidden_dim = 16
output_dim = 1  # 输出维度为1

# 初始化网络
synapse_0 = (2 * np.random.random((input_dim, hidden_dim)) - 1) * 0.05  # 维度为2*16, 2是输入维度,16是隐藏层维度
synapse_1 = (2 * np.random.random((hidden_dim, output_dim)) - 1) * 0.05
synapse_h = (2 * np.random.random((hidden_dim, hidden_dim)) - 1) * 0.05
# => [-0.05, 0.05),

# 用于存放反向传播的权重更新值
synapse_0_update = np.zeros_like(synapse_0)
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)

synapse_0_update在前面很少见到,是因为它被隐含在优化器里了。这里全部“裸写”(不使用TensorFlow库函数),需要定义一组变量,用于反向优化参数时存放参数需要调整的调整值,对应于前面的3个权重synapse_0synapse_1synapse_h

4.准备样本数据

大致是这样的过程:

  1. 建立循环生成样本数据,先生成两个数a和b。如果a小于b,就交换位置,保证被减数大。
  2. 计算出相减的结果c。
  3. 将3个数转换成二进制,为模型计算做准备。

将上面过程一一实现,代码如下。

# 开始训练
for j in range(10000):
    # 生成一个数字a
    a_int = np.random.randint(largest_number)
    # 生成一个数字b,b的最大值取的是largest_number/2,作为被减数,让它小一点
    b_int = np.random.randint(largest_number / 2)
    # 如果生成的b大了,那么交换一下
    if a_int < b_int:
        tt = a_int
        b_int = a_int
        a_int = tt

    a = int2binary[a_int]  # 二进制编码
    b = int2binary[b_int]  # 二进制编码
    # 正确的答案
    c_int = a_int - b_int
    c = int2binary[c_int]

5.模型初始化

# 存储神经网络的预测值
d = np.zeros_like(c)
overallError = 0  # 每次把总误差清零

layer_2_deltas = list()  # 存储每个时间点输出层的误差
layer_1_values = list()  # 存储每个时间点隐藏层的值

layer_1_values.append(np.ones(hidden_dim) * 0.1)  # 一开始没有隐藏层,所以初始化一下原始值为0.1
  • 初始化输出值为0
  • 初始化总误差为0
  • layer_2_deltas存储反向传播过程中的循环层的误差
  • layer_1_values为隐藏层的输出值,

由于第一个数据传入时,没有前面的隐藏层输出值来作为本次的输入,所以需要为其定义一个初始值,这里定义为0.1。

6.正向传播

循环遍历每个二进制位,从个位开始依次相减,并将中间隐藏层的输出传入下一位的计算(退位减法),把每一个时间点的误差导数都记录下来,同时统计总误差,为输出准备。

for position in range(binary_dim):  # 循环遍历每一个二进制位

    # 生成输入和输出
    X = np.array([[a[binary_dim - position - 1], b[binary_dim - position - 1]]])  # 从右到左,每次取两个输入数字的一个bit位
    y = np.array([[c[binary_dim - position - 1]]]).T  # 正确答案
    # hidden layer (input ~+ prev_hidden)
    layer_1 = sigmoid(
        np.dot(X, synapse_0) + np.dot(layer_1_values[-1], synapse_h))  # (输入层 + 之前的隐藏层) -> 新的隐藏层,这是体现循环神经网络的最核心的地方
    # output layer (new binary representation)
    layer_2 = sigmoid(np.dot(layer_1, synapse_1))  # 隐藏层 * 隐藏层到输出层的转化矩阵synapse_1 -> 输出层

    layer_2_error = y - layer_2  # 预测误差
    layer_2_deltas.append((layer_2_error) * sigmoid_output_to_derivative(layer_2))  # 把每一个时间点的误差导数都记录下来
    overallError += np.abs(layer_2_error[0])  # 总误差

    d[binary_dim - position - 1] = np.round(layer_2[0][0])  # 记录下每一个预测bit位

    # 将隐藏层保存起来。下个时间序列便可以使用
    layer_1_values.append(copy.deepcopy(layer_1))  # 记录下隐藏层的值,在下一个时间点用

    future_layer_1_delta = np.zeros(hidden_dim)

最后一行代码是为了反向传播准备的初始化。同正向传播一样,反向传播是从最后一次往前反向计算误差,对于每一个当前的计算都需要有它的下一次结果参与。反向计算是从最后一次开始的,它没有后一次的输出,所以需要初始化一个值作为其后一次的输入,这里初始化为0。

7.反向训练

初始化之后,开始从高位往回遍历,一次对每一位的所有层计算误差,并根据每层误差对权重求偏导,得到其调整值,最终将每一位算出的各层权重的调整值加在一起乘以学习率,来更新各层的权重,完成一次优化训练。

    # 反向传播,从最后一个时间点到第一个时间点
    for position in range(binary_dim):
        X = np.array([[a[position], b[position]]])  # 最后一次的两个输入
        layer_1 = layer_1_values[-position - 1]  # 当前时间点的隐藏层
        prev_layer_1 = layer_1_values[-position - 2]  # 前一个时间点的隐藏层

        layer_2_delta = layer_2_deltas[-position - 1]  # 当前时间点输出层导数
        # 通过后一个时间点(因为是反向传播)的隐藏层误差和当前时间点的输出层误差,计算当前时间点的隐藏层误差
        layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(
            synapse_1.T)) * sigmoid_output_to_derivative(layer_1)

        # 等完成了所有反向传播误差计算,才会更新权重矩阵,先暂时把更新矩阵存起来
        synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
        synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
        synapse_0_update += X.T.dot(layer_1_delta)

        future_layer_1_delta = layer_1_delta

        # 完成所有反向传播之后,更新权重矩阵,并把矩阵变量清零
        synapse_0 += synapse_0_update * alpha
        synapse_1 += synapse_1_update * alpha
        synapse_h += synapse_h_update * alpha
        synapse_0_update *= 0
        synapse_1_update *= 0
        synapse_h_update *= 0

更新完后会将中间变量值清零。

8.输出结果

每运行800次将结果输出,代码如下。

        # 打印输出过程
        if (j % 800 == 0):
            print("总误差:" + str(overallError))
            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("------------")

运行代码,输出结果如下:

总误差:[ 3.97242498]
Pred:[0 0 0 0 0 0 0 0]
True:[0 0 0 0 0 0 0 0]
9 - 9 = 0
------------
总误差:[ 2.1721182]
Pred:[0 0 0 0 0 0 0 0]
True:[0 0 0 1 0 0 0 1]
17 - 0 = 0
------------
……
------------
总误差:[ 0.04588656]
Pred:[1 0 0 1 0 1 1 0]
True:[1 0 0 1 0 1 1 0]
167 - 17 = 150
------------
总误差:[ 0.08098026]
Pred:[1 0 0 1 1 0 0 0]
True:[1 0 0 1 1 0 0 0]
204 - 52 = 152
------------
总误差:[ 0.03262333]
Pred:[1 1 0 0 0 0 0 0]
True:[1 1 0 0 0 0 0 0]
209 - 17 = 192

可以看到,刚开始还不准,随着迭代次数增加,到后来已经可以完全拟合退位减法了。

完整代码如下:

更新于 2019-04-20
在线,9小时前登录

查看TensorFlow更多相关的文章或提一个关于TensorFlow的问题,也可以与我们一起分享文章