文章目录
环境介绍
Ubuntu 18.04 + PyCharm 2018 + Anaconda3(Python3是大势所趋)
Anaconda = 集成了常用包的Python,这里不做过多介绍。
上述环境中,最好保持Python版本一致(Python3),其余的关系不大。
定义神经网络的框架
考虑一个神经网络,很容易可以抽象出三种操作:
- 初始化函数:指定神经网络的层数,每一层的节点个数等,即指定神经网络的结构;
- 训练函数:通过训练数据集优化权重;
- 查询函数:通过测试数据集测试训练后的神经网络。
为此,给出如下神经网络的类定义(神经网络的框架),文件名为neural_network.py
:
# coding=utf-8
# author: BebDong
# 10/23/18
# neural network definition
class NeuralNetwork:
# initialise the neural network
def __init__(self):
pass
# train the network using training data set
def training(self):
pass
# query the network using test data set
def query(self):
pass
初始化
根据分析,编写初始化函数__init__()
,指定神经网络的结构。
# initialise the neural network
def __init__(self, numInputNodes, numHiddenNodes, numOutputNodes, learningRate):
# 单隐藏层示例,设置各层的节点个数
self.numInputNodes = numInputNodes
self.numHiddenNodes = numHiddenNodes
self.numOutputNodes = numOutputNodes
# 权重更新时的学习率
self.learningRate = learningRate
pass
创建网络节点和链接
简单均匀分布随机初始权重
网络中最重要的部分就算链接权重,我们使用权重来得到输出、反向传播误差、并优化权重本身来得到更加优化的结果。
- 示例使用单隐藏层(即一共2层,输入层不做任何操作),故而需要两个矩阵来存储权重
- 输入层和隐藏层权重矩阵大小为
numHiddenNodes*numInputNodes
,隐藏层和输出层权重矩阵大小为numOutputNodes*numHiddenNodes
- 初始权重应该较小,随机且不为0(理解这一点,需要理解神经网络的本质思想)
- 使用numpy包来生成随机权重矩阵
- 在
__init__()
函数中定义
# 初始化权重: 加上偏移-0.5是为了使权重分布在(-0.5,0.5)
self.weightInputHidden = (numpy.random.rand(self.numHiddenNodes, self.numInputNodes) - 0.5)
self.weightHiddenOutput = (numpy.random.rand(self.numOutputNodes, self.numHiddenNodes) - 0.5)
正态分布初始权重
对于设置链接的初始权重有一个经验规则:在一个节点传入链接数量平方根倒数的范围内随机采样,即从均值为0、标准方差等于节点传入链接数量平方根倒数的正态分布中进行采样。
后文中,我们将采用这种方式。
# 正态分布初始化权重
self.weightInputHidden = numpy.random.normal(0.0, pow(self.numHiddenNodes, -0.5),
(self.numHiddenNodes, self.numInputNodes))
self.weightHiddenOutput = numpy.random.normal(0.0, pow(self.numOutputNodes, -0.5),
(self.numOutputNodes, self.numHiddenNodes))
编写查询函数
查询函数query()
用于从训练好的神经网络处获取输出集进行预测。
- 网络使用sigmoid激活函数,
y
=
1
1
+
e
−
x
y=\frac{1}{1+e^{-x}}
y=1+e−x1,在SciPy中定义为
expit()
- 在
__init__()
中定义激活函数,这样可以方便地扩展激活函数或者改变激活函数 - 使用numpy进行矩阵运算
# 激活函数(lambda创建匿名函数)
self.activation_function = lambda x: scipy.special.expit(x)
至今的所有代码如下:
# coding=utf-8
# author: BebDong
# 10/23/18
import numpy
import scipy.special
# neural network definition
class NeuralNetwork:
# initialise the neural network
def __init__(self, numInputNodes, numHiddenNodes, numOutputNodes, learningRate):
# 单隐藏层示例,设置各层的节点个数
self.numInputNodes = numInputNodes
self.numHiddenNodes = numHiddenNodes
self.numOutputNodes = numOutputNodes
# 权重更新时的学习率
self.learningRate = learningRate
# 正态分布初始化权重
self.weightInputHidden = numpy.random.normal(0.0, pow(self.numHiddenNodes, -0.5),
(self.numHiddenNodes, self.numInputNodes))
self.weightHiddenOutput = numpy.random.normal(0.0, pow(self.numOutputNodes, -0.5),
(self.numOutputNodes, self.numHiddenNodes))
# 激活函数(lambda创建匿名函数)
self.activation_function = lambda x: scipy.special.expit(x)
pass
# train the network using training data set
def training(self):
pass
# query the network using test data set
def query(self, inputs_list):
# 将输入一维数组转化成二维,并转置
inputs = numpy.array(inputs_list, ndmin=2).T
# 计算到达隐藏层的信号,即隐藏层输入
hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
# 计算隐藏层输出,即经过sigmoid函数的输出
hidden_outputs = self.activation_function(hidden_inputs)
# 计算到达输出层的信号,即输出层的输入
final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
# 计算最终的输出
final_outputs = self.activation_function(final_inputs)
return final_outputs
阶段性测试
到目前为止,已经完成了神经网络的初始化和query()
的功能,按理说可以通过一个输入得到一个输出了。下面在编写训练函数之前先测试目前的所有代码。
编写一个测试文件test.py
:
# coding=utf-8
# author: BebDong
# 10/23/18
import neural_network
input_nodes = 3
hidden_nodes = 3
output_nodes = 3
learning_rate = 0.3
n = neural_network.NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
print(n.query([1.0, 0.5, -1.5]))
程序运行正常并输出类似如下结果:
程序运行的结果取决于:
- 随机产生的初始权重;
- 网络的大小和结构
编写训练函数
训练函数training()
完成两件事情:
- 第一阶段,同
query()
根据输入得到输出 - 第二阶段,反向传播误差更新链接权重
第一阶段,同query()
函数:
# 第一,同query()函数
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(targets_list, ndmin=2).T
hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
hidden_outputs = self.activation_function(hidden_inputs)
final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
final_outputs = self.activation_function(final_inputs)
第二阶段,误差反向传播并更新权重。
首先计算各层的误差:
# 计算误差
output_errors = targets - final_outputs
# 反向传播误差到隐藏层
hidden_errors = numpy.dot(self.weightHiddenOutput.T, output_errors)
对于输入层和隐藏层之间的链接权重,使用hidden_errors
来更新,对于隐藏层和输出层之间的权重,使用output_errors
来进行更新。
接着使用梯度下降的方法来更新权重(公式此处不进行推导):
# 更新隐藏层和输出层之间的权重
self.weightHiddenOutput += self.learningRate * numpy.dot((output_errors * final_outputs *
(1.0 - final_outputs)),
numpy.transpose(hidden_outputs))
# 更新输入层和隐藏层之间的权重
self.weightInputHidden += self.learningRate * numpy.dot((hidden_errors * hidden_outputs *
(1.0 - hidden_outputs)),
numpy.transpose(inputs))
神经网络的所有代码
到现在为止,我们从0开始封装了一个单隐藏层的简单神经网络。如下为完整代码:
# coding=utf-8
# author: BebDong
# 10/23/18
import numpy
import scipy.special
# neural network definition
class NeuralNetwork:
# initialise the neural network
def __init__(self, numInputNodes, numHiddenNodes, numOutputNodes, learningRate):
# 单隐藏层示例,设置各层的节点个数
self.numInputNodes = numInputNodes
self.numHiddenNodes = numHiddenNodes
self.numOutputNodes = numOutputNodes
# 权重更新时的学习率
self.learningRate = learningRate
# 正态分布初始化权重
self.weightInputHidden = numpy.random.normal(0.0, pow(self.numHiddenNodes, -0.5),
(self.numHiddenNodes, self.numInputNodes))
self.weightHiddenOutput = numpy.random.normal(0.0, pow(self.numOutputNodes, -0.5),
(self.numOutputNodes, self.numHiddenNodes))
# 激活函数(lambda创建匿名函数)
self.activation_function = lambda x: scipy.special.expit(x)
pass
# train the network using training data set
def training(self, inputs_list, targets_list):
# 第一,同query()函数
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(targets_list, ndmin=2).T
hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
hidden_outputs = self.activation_function(hidden_inputs)
final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
final_outputs = self.activation_function(final_inputs)
# 计算误差
output_errors = targets - final_outputs
# 反向传播误差到隐藏层
hidden_errors = numpy.dot(self.weightHiddenOutput.T, output_errors)
# 更新隐藏层和输出层之间的权重
self.weightHiddenOutput += self.learningRate * numpy.dot((output_errors * final_outputs *
(1.0 - final_outputs)),
numpy.transpose(hidden_outputs))
# 更新输入层和隐藏层之间的权重
self.weightInputHidden += self.learningRate * numpy.dot((hidden_errors * hidden_outputs *
(1.0 - hidden_outputs)),
numpy.transpose(inputs))
pass
# query the network using test data set
def query(self, inputs_list):
# 将输入一维数组转化成二维,并转置
inputs = numpy.array(inputs_list, ndmin=2).T
# 计算到达隐藏层的信号,即隐藏层输入
hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
# 计算隐藏层输出,即经过sigmoid函数的输出
hidden_outputs = self.activation_function(hidden_inputs)
# 计算到达输出层的信号,即输出层的输入
final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
# 计算最终的输出
final_outputs = self.activation_function(final_inputs)
return final_outputs
识别手写数字数据集MNIST
数据集介绍
完整数据集选择和获取
数据集网站:http://yann.lecun.com/exdb/mnist/
易用的数据格式:https://pjreddie.com/projects/mnist-in-csv/
原始数据网站提供的数据格式不易使用,为此我们在实验中使用他人提供的.csv格式的数据集,包含一个训练数据集(60000样本)和一个测试数据集(10000样本)。
数据集解释
打开数据集文件,可以得到如下格式:
第一列表示label列,即正确的答案,表示这张图片代表的数字。后面的784列表示一个28*28像素的图片,每个值表示每个像素点的像素值。
一种数据子集的选择
使用较小的数据子集来提高计算机的执行时间效率,当确定算法和代码有效之后,可以使用完整的数据集。
这里我们选择训练子集(100样本)和测试子集(10样本)。
直观的展示数据
编写test.py
,选择数据集中的一条记录,将这张手写数字图片绘制出来:
# coding=utf-8
# author: BebDong
# 10/23/18
import numpy
import matplotlib.pyplot as plt
# 直接使用plt.imshow无法显示图片,需要导入pylab包
import pylab
# 打开并读取文件
data_file = open("mnist_dataset/mnist_train_100.csv")
data_list = data_file.readlines()
data_file.close()
# 拆分绘制28*28图形
all_pixels = data_list[0].split(',')
image_array = numpy.asfarray(all_pixels[1:]).reshape((28, 28))
plt.figure("Image")
plt.imshow(image_array, cmap='gray', interpolation='None')
pylab.show()
我们选择了训练数据集的第一条记录,绘制结果如下:
对输入数据做必要的变换
目前,已经有了数据集和定义好的神经网络,好像可以直接将数据丢给神经网路开始训练了!?真的是这样吗?
思考:像素点的值取值范围为
[
0
,
255
]
[0,255]
[0,255],观察sigmoid函数的图像,如下图所示。当sigmoid的输入过大或者过小时,激活函数的梯度极小,这将限制神经网络的学习能力。所以需要将输入颜色的值进行缩放,使得其分布在激活函数梯度较大的舒适区域内,从而使神经网络更好的工作。
这里,将输入颜色值从 [ 0 , 255 ] [0,255] [0,255]范围缩放至 [ 0.01 , 1.0 ] [0.01,1.0] [0.01,1.0],选择0.01作为起点,是为了避免0值输入会造成权重更新失败的问题(梯度下降进行权重更新的时候,有一项是乘以输入矩阵,有兴趣的读者可以自行推导公式)。
# 缩放输入数据。0.01的偏移量避免0值输入
scaled_inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
考虑输出数据
sigmoid函数的输出范围为
(
0
,
1
)
(0,1)
(0,1),如果我们想让神经网络输出图片的像素数组的话,其值在
[
0
,
255
]
[0,255]
[0,255]之间,看起来需要调整目标值以适应激活函数的范围?
考虑另外一种方案,我们需要神经网络判断一个输入图片代表的是数字几,即输出一个
[
0
,
9
]
[0,9]
[0,9]区间的数字,共10个数字。所以可以设置输出层节点个数为10:如果答案是"0",则输出层第一个节点激发,如果答案是"7",则输出层的第8个节点激发。激发的意思是此节点数值明显大于0。
使用这种方法,需要对训练数据集做一定的调整,比如当label列为"5"的时候,对应的目标输出应该类似:[0.01, 0.01, 0.01, 0.01, 0.01, 0.99, 0.01, 0.01, 0.01, 0.01],即第6个节点被激发。
# 构建目标矩阵。sigmoid函数无法取端点值0或者1,使用0.01代替0,0.99代替1
output_nodes = 10
# 产生0值输出矩阵
targets = numpy.zeros(output_nodes) + 0.01
# 将字符串转换为整数,并设置激发节点
targets[int(all_pixels[0])] = 0.99
编写代码,进行试验
新建experiment.py
,编写试验代码:
- 隐藏层节点个数不唯一,可以多次实验进行调整
- 这里训练数据集仅100条记录,故一次性读入内存。当数据集很大时,这样的方法不可取
# coding=utf-8
# author: BebDong
# 2018.10.23
import neural_network as nn
import numpy
# 指定神经网络的结构。隐藏层节点个数不唯一
input_nodes, hidden_nodes, output_nodes = 784, 100, 10
# 指定权重更新的学习率
learning_rate = 0.3
# 创建神经网络的实例
network = nn.NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# 读取训练数据,只读方式
training_data_file = open("mnist_dataset/mnist_train_100.csv", 'r')
# 当数据集很大时,应当分批读入内存。这里仅100条记录,则一次性全部读入内存
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
for record in training_data_list:
# 缩放输入
all_pixels = record.split(',')
scaled_inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
# 创建目标输出
targets = numpy.zeros(output_nodes) + 0.01
targets[int(all_pixels[0])] = 0.99
network.training(scaled_inputs, targets)
pass
# 读取测试数据集
test_data_file = open("mnist_dataset/mnist_test_10.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试训练好的神经网络
# 初始化一个数据结构用于记录神经网络的表现
scorecard = []
# 遍历测试数据集
for record in test_data_list:
# 打印预期输出
all_pixels = record.split(',')
correct_label = int(all_pixels[0])
print("correct label: ", correct_label)
# 查询神经网络
inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
outputs = network.query(inputs)
answer = numpy.argmax(outputs)
print("network's answer: ", answer)
# 更新神经网络的表现
if answer == correct_label:
scorecard.append(1)
else:
scorecard.append(0)
pass
pass
# 打印得分
print(scorecard)
print("performance: ", sum(scorecard) / len(scorecard))
运行可以得到如下结果:
可以发现,在本次实验中神经网络的准确率达到了70%。在训练样本仅100的情况下,已经是一个很好的实验结果。
另外,experiment.py
中的代码可以通过函数进行封装,这样可以使其更简洁并且方便维护,有兴趣的同学可以自己尝试着封装,这里不再重复。
总结
本文通过实现一个简单的三层神经网络,介绍了神经网络的基本实现过程,并使用Python和MNIST手写数据集进行了实验,结果可以说令人兴奋。
- 要理解神经网络中信号传播的矩阵表示,利用矩阵运算可以极大的简化代码量;
- 要了解误差反向传播的原理,及sigmoid函数梯度下降更新权重的函数。(即误差其实是权重的函数,这里不再推导);
- 完整数据集的测试留给读者自行完成;
- 神经网络的可调节参数:学习率、网络的结构(各层的节点数量),以及使用数据集进行多次训练等等。这里不再展示相关结果,感兴趣的同学可以自行实验;
- 神经网络中的可调节参数可以单独封装为
.yml
或者.xml
配置文件(其他类型也可),便于维护和调整; - 完整的项目代码可以在github上下载:https://github.com/BebDong/NumRecognition
完整数据集测试及性能评估
- 下面的完整数据集测试同样是一次性将数据读入内存;
- 使用训练样本训练两次;
- 训练次数不可太多,防止过度拟合。
# coding=utf-8
# author: BebDong
# 2018.10.23
import neural_network as nn
import numpy
import time
# 便于计算执行时间
start = time.process_time()
# 指定神经网络的结构。隐藏层节点个数不唯一
input_nodes, hidden_nodes, output_nodes = 784, 100, 10
# 指定权重更新的学习率
learning_rate = 0.3
# 创建神经网络的实例
network = nn.NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# 读取训练数据,只读方式
training_data_file = open("mnist_dataset/mnist_train.csv", 'r')
# 当数据集很大时,应当分批读入内存。这里仅100条记录,则一次性全部读入内存
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络,epochs次
epochs = 2
for e in range(epochs):
for record in training_data_list:
# 缩放输入
all_pixels = record.split(',')
scaled_inputs = (numpy.asfarray(all_pixels[1:]) / 255.0 * 0.99) + 0.01
# 创建目标输出
targets = numpy.zeros(output_nodes) + 0.01
targets[int(all_pixels[0])] = 0.99
network.training(scaled_inputs, targets)
pass
pass
# 读取测试数据集
test_data_file = open("mnist_dataset/mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试训练好的神经网络
# 初始化一个数据结构用于记录神经网络的表现
scorecard = []
# 遍历测试数据集
for record in test_data_list:
# 打印预期输出
all_pixels = record.split(',')
correct_label = int(all_pixels[0])
# 查询神经网络
inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
outputs = network.query(inputs)
answer = numpy.argmax(outputs)
# 更新神经网络的表现
if answer == correct_label:
scorecard.append(1)
else:
scorecard.append(0)
pass
pass
# 打印得分及运行时间
print("time: ", time.process_time()-start)
print("performance: ", sum(scorecard) / len(scorecard))
我的运行结果,准确率大概在95%左右:
一个有趣的想法
我们都知道神经网络就像一个黑盒子,我们无法知道它的内部如何工作,我们往往只关注答案是否准确本身。
神经网络学习到的知识通过链接中权重来反映。跟人脑不同的是,这种反映实质上不能称之为对于这个问题的理解或者智慧。个人认为,仅仅只能将这种学习到的知识看做对样本空间特征的一种固化。
如果我们将信号的传播方向反过来,我们从输出层输入一个标签,看看输入层会输出一个什么样的图像呢?
这里需要将sigmoid激活函数换成它的反函数,作为信号反向传播时的“激活函数”。很容易根据
y
=
1
1
+
e
−
x
y=\frac{1}{1+e^{-x}}
y=1+e−x1得到反函数
x
=
l
n
[
y
1
−
y
]
x=ln[\frac{y}{1-y}]
x=ln[1−yy],这个函数由scipy.special.logit()
提供。
有兴趣的同学可以做做这个实验看看会发生什么!?