Files
ailearning/src/py3.x/dl/bp.py
2019-10-11 16:48:36 +08:00

870 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import random
from functools import reduce
from numpy import *
# sigmoid 函数
def sigmoid(inX):
'''
Desc:
sigmoid 函数实现
Args:
inX --- 输入向量
Returns:
对输入向量作用 sigmoid 函数之后得到的输出
'''
return 1.0 / (1 + exp(-inX))
# 定义神经网络的节点类
class Node(object):
'''
Desc:
神经网络的节点类
'''
def __init__(self, layer_index, node_index):
'''
Desc:
初始化一个节点
Args:
layer_index --- 层的索引,也就是表示第几层
node_index --- 节点的索引,也就是表示节点的索引
Returns:
None
'''
# 设置节点所在的层的位置
self.layer_index = layer_index
# 设置层中的节点的索引
self.node_index = node_index
# 设置此节点的下游节点,也就是这个节点与下一层的哪个节点相连
self.downstream = []
# 设置此节点的上游节点,也就是哪几个节点的下游节点与此节点相连
self.upstream = []
# 此节点的输出
self.output = 0
# 此节点真实值与计算值之间的差值
self.delta = 0
def set_output(self, output):
'''
Desc:
设置节点的 output
Args:
output --- 节点的 output
Returns:
None
'''
self.output = output
def append_downstream_connection(self, conn):
'''
Desc:
添加此节点的下游节点的连接
Args:
conn --- 当前节点的下游节点的连接的 list
Returns:
None
'''
# 使用 list 的 append 方法来将 conn 中的节点添加到 downstream 中
self.downstream.append(conn)
def append_upstream_connection(self, conn):
'''
Desc:
添加此节点的上游节点的连接
Args:
conn ---- 当前节点的上游节点的连接的 list
Returns:
None
'''
# 使用 list 的 append 方法来将 conn 中的节点添加到 upstream 中
self.upstream.append(conn)
def calc_output(self):
'''
Desc:
计算节点的输出,依据 output = sigmoid(wTx)
Args:
None
Returns:
None
'''
# 使用 reduce() 函数对其中的因素求和
output = reduce(lambda ret, conn: ret + conn.upstream_node.output * conn.weight, self.upstream, 0)
# 对上游节点的 output 乘 weights 之后求和得到的结果应用 sigmoid 函数,得到当前节点的 output
self.output = sigmoid(output)
def calc_hidden_layer_delta(self):
'''
Desc:
计算隐藏层的节点的 delta
Args:
output --- 节点的 output
Returns:
None
'''
# 根据 https://www.zybuluo.com/hanbingtao/note/476663 的 式4 计算隐藏层的delta
downstream_delta = reduce(lambda ret, conn: ret + conn.downstream_node.delta * conn.weight, self.downstream, 0.0)
# 计算此节点的 delta
self.delta = self.output * (1 - self.output) * downstream_delta
def calc_output_layer_delta(self, label):
'''
Desc:
计算输出层的 delta
Args:
label --- 输入向量对应的真实标签,不是计算得到的结果
Returns:
None
'''
# 就是那输出层的 delta
self.delta = self.output * (1 - self.output) * (label - self.output)
def __str__(self):
'''
Desc:
将节点的信息打印出来
Args:
None
Returns:
None
'''
# 打印格式:第几层 - 第几个节点output 是多少delta 是多少
node_str = '%u-%u: output: %f delta: %f' % (self.layer_index, self.node_index, self.output, self.delta)
# 下游节点
downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')
# 上游节点
upstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.upstream, '')
# 将本节点 + 下游节点 + 上游节点 的信息打印出来
return node_str + '\n\tdownstream:' + downstream_str + '\n\tupstream:' + upstream_str
# ConstNode 对象,为了实现一个输出恒为 1 的节点(计算偏置项 wb 时需要)
class ConstNode(object):
'''
Desc:
常数项对象,即相当于计算的时候的偏置项
'''
def __init__(self, layer_index, node_index):
'''
Desc:
初始化节点对象
Args:
layer_index --- 节点所属的层的编号
node_index --- 节点的编号
Returns:
None
'''
self.layer_index = layer_index
self.node_index = node_index
self.downstream = []
self.output = 1
def append_downstream_connection(self, conn):
'''
Desc:
添加一个到下游节点的连接
Args:
conn --- 到下游节点的连接
Returns:
None
'''
# 使用 list 的 append 方法将包含下游节点的 conn 添加到 downstream 中
self.downstream.append(conn)
def calc_hidden_layer_delta(self):
'''
Desc:
计算隐藏层的 delta
Args:
None
Returns:
None
'''
# 使用我们的 公式 4 来计算下游节点的 delta求和
downstream_delta = reduce(lambda ret, conn: ret + conn.downstream_node.delta * conn.weight, self.downstream, 0.0)
# 计算隐藏层的本节点的 delta
self.delta = self.output * (1 - self.output) * downstream_delta
def __str__(self):
'''
Desc:
将节点信息打印出来
Args:
None
Returns:
None
'''
# 将节点的信息打印出来
# 格式 第几层-第几个节点的 output
node_str = '%u-%u: output: 1' % (self.layer_index, self.node_index)
# 此节点的下游节点的信息
downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')
# 将此节点与下游节点的信息组合,一起打印出来
return node_str + '\n\tdownstream:' + downstream_str
# 神经网络的层对象,负责初始化一层。此外,作为 Node 的集合对象,提供对 Node 集合的操作
class Layer(object):
'''
Desc:
神经网络的 Layer 类
'''
def __init__(self, layer_index, node_count):
'''
Desc:
神经网络的层对象的初始化
Args:
layer_index --- 层的索引
node_count --- 节点的个数
Returns:
None
'''
# 设置 层的索引
self.layer_index = layer_index
# 设置层中的节点的 list
self.nodes = []
# 将 Node 节点添加到 nodes 中
for i in range(node_count):
self.nodes.append(Node(layer_index, i))
# 将 ConstNode 节点也添加到 nodes 中
self.nodes.append(ConstNode(layer_index, node_count))
def set_output(self, data):
'''
Desc:
设置层的输出,当层是输入层时会用到
Args:
data --- 输出的值的 list
Returns:
None
'''
# 设置输入层中各个节点的 output
for i in range(len(data)):
self.nodes[i].set_output(data[i])
def calc_output(self):
'''
Desc:
计算层的输出向量
Args:
None
Returns:
None
'''
# 遍历本层的所有节点除去最后一个节点因为它是恒为常数的偏置项b
# 调用节点的 calc_output 方法来计算输出向量
for node in self.nodes[:-1]:
node.calc_output()
def dump(self):
'''
Desc:
将层信息打印出来
Args:
None
Returns:
None
'''
# 遍历层的所有的节点 nodes将节点信息打印出来
for node in self.nodes:
print(node)
# Connection 对象类,主要负责记录连接的权重,以及这个连接所关联的上下游的节点
class Connection(object):
'''
Desc:
Connection 对象,记录连接权重和连接所关联的上下游节点,注意,这里的 connection 没有 s ,不是复数
'''
def __init__(self, upstream_node, downstream_node):
'''
Desc:
初始化 Connection 对象
Args:
upstream_node --- 上游节点
downstream_node --- 下游节点
Returns:
None
'''
# 设置上游节点
self.upstream_node = upstream_node
# 设置下游节点
self.downstream_node = downstream_node
# 设置权重,这里设置的权重是 -0.1 到 0.1 之间的任何数
self.weight = random.uniform(-0.1, 0.1)
# 设置梯度 为 0.0
self.gradient = 0.0
def calc_gradient(self):
'''
Desc:
计算梯度
Args:
None
Returns:
None
'''
# 下游节点的 delta * 上游节点的 output 计算得到梯度
self.gradient = self.downstream_node.delta * self.upstream_node.output
def update_weight(self, rate):
'''
Desc:
根据梯度下降算法更新权重
Args:
rate --- 学习率 / 或者成为步长
Returns:
None
'''
# 调用计算梯度的函数来将梯度计算出来
self.calc_gradient()
# 使用梯度下降算法来更新权重
self.weight += rate * self.gradient
def get_gradient(self):
'''
Desc:
获取当前的梯度
Args:
None
Returns:
当前的梯度 gradient
'''
return self.gradient
def __str__(self):
'''
Desc:
将连接信息打印出来
Args:
None
Returns:
连接信息进行返回
'''
# 格式为:上游节点的层的索引+上游节点的节点索引 ---> 下游节点的层的索引+下游节点的节点索引,最后一个数是权重
return '(%u-%u) -> (%u-%u) = %f' % (
self.upstream_node.layer_index,
self.upstream_node.node_index,
self.downstream_node.layer_index,
self.downstream_node.node_index,
self.weight)
# Connections 对象,提供 Connection 集合操作。
class Connections(object):
'''
Desc:
Connections 对象,提供 Connection 集合的操作,看清楚后面有没有 s ,不要看错
'''
def __init__(self):
'''
Desc:
初始化 Connections 对象
Args:
None
Returns:
None
'''
# 初始化一个列表 list
self.connections = []
def add_connection(self, connection):
'''
Desc:
将 connection 中的节点信息 append 到 connections 中
Args:
None
Returns:
None
'''
self.connections.append(connection)
def dump(self):
'''
Desc:
将 Connections 的节点信息打印出来
Args:
None
Returns:
None
'''
for conn in self.connections:
print(conn)
# Network 对象,提供相应 API
class Network(object):
'''
Desc:
Network 类
'''
def __init__(self, layers):
'''
Desc:
初始化一个全连接神经网络
Args:
layers --- 二维数组,描述神经网络的每层节点数
Returns:
None
'''
# 初始化 connections使用的是 Connections 对象
self.connections = Connections()
# 初始化 layers
self.layers = []
# 我们的神经网络的层数
layer_count = len(layers)
# 节点数
node_count = 0
# 遍历所有的层,将每层信息添加到 layers 中去
for i in range(layer_count):
self.layers.append(Layer(i, layers[i]))
# 遍历除去输出层之外的所有层,将连接信息添加到 connections 对象中
for layer in range(layer_count - 1):
connections = [Connection(upstream_node, downstream_node) for upstream_node in self.layers[layer].nodes for downstream_node in self.layers[layer + 1].nodes[:-1]]
# 遍历 connections将 conn 添加到 connections 中
for conn in connections:
self.connections.add_connection(conn)
# 为下游节点添加上游节点为 conn
conn.downstream_node.append_upstream_connection(conn)
# 为上游节点添加下游节点为 conn
conn.upstream_node.append_downstream_connection(conn)
def train(self, labels, data_set, rate, epoch):
'''
Desc:
训练神经网络
Args:
labels --- 数组,训练样本标签,每个元素是一个样本的标签
data_set --- 二维数组,训练样本的特征数据。每行数据是一个样本的特征
rate --- 学习率
epoch --- 迭代次数
Returns:
None
'''
# 循环迭代 epoch 次
for i in range(epoch):
# 遍历每个训练样本
for d in range(len(data_set)):
# 使用此样本进行训练(一条样本进行训练)
self.train_one_sample(labels[d], data_set[d], rate)
# print 'sample %d training finished' % d
def train_one_sample(self, label, sample, rate):
'''
Desc:
内部函数,使用一个样本对网络进行训练
Args:
label --- 样本的标签
sample --- 样本的特征
rate --- 学习率
Returns:
None
'''
# 调用 Network 的 predict 方法,对这个样本进行预测
self.predict(sample)
# 计算根据此样本得到的结果的 delta
self.calc_delta(label)
# 更新权重
self.update_weight(rate)
def calc_delta(self, label):
'''
Desc:
计算每个节点的 delta
Args:
label --- 样本的真实值,也就是样本的标签
Returns:
None
'''
# 获取输出层的所有节点
output_nodes = self.layers[-1].nodes
# 遍历所有的 label
for i in range(len(label)):
# 计算输出层节点的 delta
output_nodes[i].calc_output_layer_delta(label[i])
# 这个用法就是切片的用法, [-2::-1] 就是将 layers 这个数组倒过来从没倒过来的时候的倒数第二个元素开始到翻转过来的倒数第一个数比如这样aaa = [1,2,3,4,5,6,7,8,9],bbb = aaa[-2::-1] ==> bbb = [8, 7, 6, 5, 4, 3, 2, 1]
# 实际上就是除掉输出层之外的所有层按照相反的顺序进行遍历
for layer in self.layers[-2::-1]:
# 遍历每层的所有节点
for node in layer.nodes:
# 计算隐藏层的 delta
node.calc_hidden_layer_delta()
def update_weight(self, rate):
'''
Desc:
更新每个连接的权重
Args:
rate --- 学习率
Returns:
None
'''
# 按照正常顺序遍历除了输出层的层
for layer in self.layers[:-1]:
# 遍历每层的所有节点
for node in layer.nodes:
# 遍历节点的下游节点
for conn in node.downstream:
# 根据下游节点来更新连接的权重
conn.update_weight(rate)
def calc_gradient(self):
'''
Desc:
计算每个连接的梯度
Args:
None
Returns:
None
'''
# 按照正常顺序遍历除了输出层之外的层
for layer in self.layers[:-1]:
# 遍历层中的所有节点
for node in layer.nodes:
# 遍历节点的下游节点
for conn in node.downstream:
# 计算梯度
conn.calc_gradient()
def get_gradient(self, label, sample):
'''
Desc:
获得网络在一个样本下,每个连接上的梯度
Args:
label --- 样本标签
sample --- 样本特征
Returns:
None
'''
# 调用 predict() 方法,利用样本的特征数据对样本进行预测
self.predict(sample)
# 计算 delta
self.calc_delta(label)
# 计算梯度
self.calc_gradient()
def predict(self, sample):
'''
Desc:
根据输入的样本预测输出值
Args:
sample --- 数组,样本的特征,也就是网络的输入向量
Returns:
使用我们的感知器规则计算网络的输出
'''
# 首先为输入层设置输出值output为样本的输入向量即不发生任何变化
self.layers[0].set_output(sample)
# 遍历除去输入层开始到最后一层
for i in range(1, len(self.layers)):
# 计算 output
self.layers[i].calc_output()
# 将计算得到的输出,也就是我们的预测值返回
return list(map(lambda node: node.output, self.layers[-1].nodes[:-1]))
def dump(self):
'''
Desc:
打印出我们的网络信息
Args:
None
Returns:
None
'''
# 遍历所有的 layers
for layer in self.layers:
# 将所有的层的信息打印出来
layer.dump()
# # ------------------------- 至此,基本上我们把 我们的神经网络实现完成,下面还会介绍一下对应的梯度检查相关的算法,现在我们首先回顾一下我们上面写道的类及他们的作用 ------------------------
'''
1、节点类的实现 Node :负责记录和维护节点自身信息以及这个节点相关的上下游连接,实现输出值和误差项的计算。如下:
layer_index --- 节点所属的层的编号
node_index --- 节点的编号
downstream --- 下游节点
upstream ---- 上游节点
output ---- 节点的输出值
delta ------ 节点的误差项
2、ConstNode 类,偏置项类的实现:实现一个输出恒为 1 的节点(计算偏置项的时候会用到),如下:
layer_index --- 节点所属层的编号
node_index ---- 节点的编号
downstream ---- 下游节点
没有记录上游节点,因为一个偏置项的输出与上游节点的输出无关
output ----- 偏置项的输出
3、layer 类,负责初始化一层。作为的是 Node 节点的集合对象,提供对 Node 集合的操作。也就是说layer 包含的是 Node 的集合。
layer_index ---- 层的编号
node_count ----- 层所包含的节点的个数
def set_ouput() -- 设置层的输出,当层是输入层时会用到
def calc_output -- 计算层的输出向量,调用的 Node 类的 计算输出 方法
4、Connection 类:负责记录连接的权重,以及这个连接所关联的上下游节点,如下:
upstream_node --- 连接的上游节点
downstream_node -- 连接的下游节点
weight -------- random.uniform(-0.1, 0.1) 初始化为一个很小的随机数
gradient -------- 0.0 梯度,初始化为 0.0
def calc_gradient() --- 计算梯度,使用的是下游节点的 delta 与上游节点的 output 相乘计算得到
def get_gradient() ---- 获取当前的梯度
def update_weight() --- 根据梯度下降算法更新权重
5、Connections 类:提供对 Connection 集合操作,如下:
def add_connection() --- 添加一个 connection
6、Network 类:提供相应的 API如下
connections --- Connections 对象
layers -------- 神经网络的层
layer_count --- 神经网络的层数
node_count --- 节点个数
def train() --- 训练神经网络
def train_one_sample() --- 用一个样本训练网络
def calc_delta() --- 计算误差项
def update_weight() --- 更新每个连接权重
def calc_gradient() --- 计算每个连接的梯度
def get_gradient() --- 获得网络在一个样本下,每个连接上的梯度
def predict() --- 根据输入的样本预测输出值
'''
# #--------------------------------------回顾完成了,有些问题可能还是没有弄懂,没事,我们接着看下面---------------------------------------------
class Normalizer(object):
'''
Desc:
归一化工具类
Args:
object --- 对象
Returns:
None
'''
def __init__(self):
'''
Desc:
初始化
Args:
None
Returns:
None
'''
# 初始化 16 进制的数,用来判断位的,分别是
# 0x1 ---- 00000001
# 0x2 ---- 00000010
# 0x4 ---- 00000100
# 0x8 ---- 00001000
# 0x10 --- 00010000
# 0x20 --- 00100000
# 0x40 --- 01000000
# 0x80 --- 10000000
self.mask = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80]
def norm(self, number):
'''
Desc:
对 number 进行规范化
Args:
number --- 要规范化的数据
Returns:
规范化之后的数据
'''
# 此方法就相当于判断一个 8 位的向量,哪一位上有数字,如果有就将这个数设置为 0.9 ,否则,设置为 0.1,通俗比较来说,就是我们这里用 0.9 表示 1用 0.1 表示 0
return list(map(lambda m: 0.9 if number & m else 0.1, self.mask))
def denorm(self, vec):
'''
Desc:
对我们得到的向量进行反规范化
Args:
vec --- 得到的向量
Returns:
最终的预测结果
'''
# 进行二分类,大于 0.5 就设置为 1小于 0.5 就设置为 0
binary = list(map(lambda i: 1 if i > 0.5 else 0, vec))
# 遍历 mask
for i in range(len(self.mask)):
binary[i] = binary[i] * self.mask[i]
# 将结果相加得到最终的预测结果
return reduce(lambda x,y: x + y, binary)
def mean_square_error(vec1, vec2):
'''
Desc:
计算平均平方误差
Args:
vec1 --- 第一个数
vec2 --- 第二个数
Returns:
返回 1/2 * (x-y)^2 计算得到的值
'''
return 0.5 * reduce(lambda a, b: a + b, map(lambda v: (v[0] - v[1]) * (v[0] - v[1]), zip(vec1, vec2)))
def gradient_check(network, sample_feature, sample_label):
'''
Desc:
梯度检查
Args:
network --- 神经网络对象
sample_feature --- 样本的特征
sample_label --- 样本的标签
Returns:
None
'''
# 计算网络误差
network_error = lambda vec1, vec2: 0.5 * reduce(lambda a, b: a + b, map(lambda v: (v[0] - v[1]) * (v[0] - v[1]), zip(vec1, vec2)))
# 获取网络在当前样本下每个连接的梯度
network.get_gradient(sample_feature, sample_label)
# 对每个权重做梯度检查
for conn in network.connections.connections:
# 获取指定连接的梯度
actual_gradient = conn.get_gradient()
# 增加一个很小的值,计算网络的误差
epsilon = 0.0001
conn.weight += epsilon
error1 = network_error(network.predict(sample_feature), sample_label)
# 减去一个很小的值,计算网络的误差
conn.weight -= 2 * epsilon # 刚才加过了一次因此这里需要减去2倍
error2 = network_error(network.predict(sample_feature), sample_label)
# 根据式6计算期望的梯度值
expected_gradient = (error2 - error1) / (2 * epsilon)
# 打印
print('expected gradient: \t%f\nactual gradient: \t%f' % (expected_gradient, actual_gradient))
def train_data_set():
'''
Desc:
获取训练数据集
Args:
None
Returns:
labels --- 训练数据集每条数据对应的标签
'''
# 调用 Normalizer() 类
normalizer = Normalizer()
# 初始化一个 list用来存储后面的数据
data_set = []
labels = []
# 0 到 256 ,其中以 8 为步长
for i in range(0, 256, 8):
# 调用 normalizer 对象的 norm 方法
n = normalizer.norm(int(random.uniform(0, 256)))
# 在 data_set 中 append n
data_set.append(n)
# 在 labels 中 append n
labels.append(n)
# 将它们返回
return labels, data_set
def train(network):
'''
Desc:
使用我们的神经网络进行训练
Args:
network --- 神经网络对象
Returns:
None
'''
# 获取训练数据集
labels, data_set = train_data_set()
labels = list(labels)
data_set = list(labels)
# 调用 network 中的 train方法来训练我们的神经网络
network.train(labels, data_set, 0.3, 50)
def test(net,data):
#此函数不明觉厉,但是传参就有问题,如果跑不通就把这段代码注释掉吧。。。
'''
Desc:
对我们的全连接神经网络进行测试
Args:
network --- 神经网络对象
data ------ 测试数据集
Returns:
None
'''
# 调用 Normalizer() 类
normalizer = Normalizer()
# 调用 norm 方法,对数据进行规范化
norm_data = normalizer.norm(data)
norm_data = list(norm_data)
# 对测试数据进行预测
predict_data = net.predict(norm_data)
# 将结果打印出来
print('\ttestdata(%u)\tpredict(%u)' % (data, normalizer.denorm(predict_data)))
def correct_ratio(network):
'''
Desc:
计算我们的神经网络的正确率
Args:
network --- 神经网络对象
Returns:
None
'''
normalizer = Normalizer()
correct = 0.0
for i in range(256):
if normalizer.denorm(network.predict(normalizer.norm(i))) == i:
correct += 1.0
print('correct_ratio: %.2f%%' % (correct / 256 * 100))
def gradient_check_test():
'''
Desc:
梯度检查测试
Args:
None
Returns:
None
'''
# 创建一个有 3 层的网络,每层有 2 个节点
net = Network([2, 2, 2])
# 样本的特征
sample_feature = [0.9, 0.1]
# 样本对应的标签
sample_label = [0.9, 0.1]
# 使用梯度检查来查看是否正确
gradient_check(net, sample_feature, sample_label)
if __name__ == '__main__':
'''
Desc:
主函数
Args:
None
Returns:
None
'''
# 初始化一个神经网络,输入层 8 个节点,隐藏层 3 个节点,输出层 8 个节点
net = Network([8, 3, 8])
# 训练我们的神经网络
train(net)
# 将我们的神经网络的信息打印出来
net.dump()
# 打印出神经网络的正确率
correct_ratio(net)