【深度学习】Python实现简单神经网络

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_28869927/article/details/83281190

环境介绍

Ubuntu 18.04 + PyCharm 2018 + Anaconda3(Python3是大势所趋)
Anaconda = 集成了常用包的Python,这里不做过多介绍。
上述环境中,最好保持Python版本一致(Python3),其余的关系不大。

定义神经网络的框架

考虑一个神经网络,很容易可以抽象出三种操作:

  1. 初始化函数:指定神经网络的层数,每一层的节点个数等,即指定神经网络的结构;
  2. 训练函数:通过训练数据集优化权重;
  3. 查询函数:通过测试数据集测试训练后的神经网络。

为此,给出如下神经网络的类定义(神经网络的框架),文件名为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

创建网络节点和链接

简单均匀分布随机初始权重

网络中最重要的部分就算链接权重,我们使用权重来得到输出、反向传播误差、并优化权重本身来得到更加优化的结果。

  1. 示例使用单隐藏层(即一共3层),故而需要两个矩阵来存储权重
  2. 输入层和隐藏层权重矩阵大小为numHiddenNodes*numInputNodes,隐藏层和输出层权重矩阵大小为numOutputNodes*numHiddenNodes
  3. 初始权重应该较小,随机且不为0(理解这一点,需要理解神经网络的本质思想)
  4. 使用numpy包来生成随机权重矩阵
  5. __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()用于从训练好的神经网络处获取输出集进行预测。

  1. 网络使用sigmoid激活函数, y = 1 1 + e x y=\frac{1}{1+e^{-x}} ,在SciPy中定义为expit()
  2. __init__()中定义激活函数,这样可以方便地扩展激活函数或者改变激活函数
  3. 使用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]))

程序运行正常并输出类似如下结果:
在这里插入图片描述
程序运行的结果取决于:

  1. 随机产生的初始权重;
  2. 网络的大小和结构

编写训练函数

训练函数training()完成两件事情:

  1. 第一阶段,同query()根据输入得到输出
  2. 第二阶段,反向传播误差更新链接权重

第一阶段,同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开始封装了一个单隐藏层(即一共3层)的简单神经网络。如下为完整代码:

# 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] ,观察sigmoid函数的图像,如下图所示。当sigmoid的输入过大或者过小时,激活函数的梯度极小,这将限制神经网络的学习能力。所以需要将输入颜色的值进行缩放,使得其分布在激活函数梯度较大的舒适区域内,从而使神经网络更好的工作。

sigmoid函数图像

这里,将输入颜色值从 [ 0 , 255 ] [0,255] 范围缩放至 [ 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 , 255 ] [0,255] 之间,看起来需要调整目标值以适应激活函数的范围?
考虑另外一种方案,我们需要神经网络判断一个输入图片代表的是数字几,即输出一个 [ 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,编写试验代码:

  1. 隐藏层节点个数不唯一,可以多次实验进行调整
  2. 这里训练数据集仅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手写数据集进行了实验,结果可以说令人兴奋。

  1. 要理解神经网络中信号传播的矩阵表示,利用矩阵运算可以极大的简化代码量;
  2. 要了解误差反向传播的原理,及sigmoid函数梯度下降更新权重的函数。(即误差其实是权重的函数,这里不再推导);
  3. 完整数据集的测试留给读者自行完成;
  4. 神经网络的可调节参数:学习率、网络的结构(各层的节点数量),以及使用数据集进行多次训练等等。这里不再展示相关结果,感兴趣的同学可以自行实验;
  5. 神经网络中的可调节参数可以单独封装为.yml或者.xml配置文件(其他类型也可),便于维护和调整;
  6. 完整的项目代码可以在github上下载:https://github.com/BebDong/NumRecognition

完整数据集测试及性能评估

  1. 下面的完整数据集测试同样是一次性将数据读入内存;
  2. 使用训练样本训练两次;
  3. 训练次数不可太多,防止过度拟合。
# 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}} 得到反函数 x = l n [ y 1 y ] x=ln[\frac{y}{1-y}] ,这个函数由scipy.special.logit()提供。

有兴趣的同学可以做做这个实验看看会发生什么!?

猜你喜欢

转载自blog.csdn.net/qq_28869927/article/details/83281190