简单循环神经网络实现一个退位减法器
本例将把前面所讲述的内容用代码实现一遍。如果前面的描述您还不明白,可以通过这个例子,加深对前面内容的理解。
使用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_0
、synapse_1
和synapse_h
。
4.准备样本数据
大致是这样的过程:
- 建立循环生成样本数据,先生成两个数a和b。如果a小于b,就交换位置,保证被减数大。
- 计算出相减的结果c。
- 将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
可以看到,刚开始还不准,随着迭代次数增加,到后来已经可以完全拟合退位减法了。