入门篇——解析Python神经网络基本原理及编程

目录

一、神经网络基本原理

 1.生物神经网络的简单原理

2.人工神经网络的基本原理

3.神经网络正向计算输出

4.神经网络反向传播误差

 5.神经网络更新连接权重

 二、Python神经网络编程

1.三层神经网络的代码框架

2.利用手写数字的数据集MNIST训练神经网络

3.利用手写数字测试神经网络


一、神经网络基本原理

 1.生物神经网络的简单原理


 在生物神经网络中,每个神经元的树突接受来自之前多个神经元输出的电信号,将其组合成更强的信号。如果组合后的信号足够强,超过阀值,这个神经元就会被激活并且也会发射信号,信号则会沿着轴突到达这个神经元的终端,再传递给接下来更多的神经元的树突,如图1所示。

                                                                                                            图1 生物神经网络的简单原理

2.人工神经网络的基本原理

仿照生物神经网络,构建多层人工神经网络,每一层的人工神经元都与其前后层的神经元相互连接,如图2所示。在每个连接上显示了相关的连接权重,较小的权重将弱化信号,而较大的权重将放大信号。


                                                                                                                        图2 人工神经网络的基本原理


       对于神经网络中的单个神经元而言, 人工神经元的前半端(图3中红色虚线框)相当于生物神经元的树突,是输入端,用来接受多个神经元输出的信号并进行组合;人工神经元的后半端(图3中绿色虚线框)相当于生物神经元的轴突,是输出端,用来输出信号给接下来更多的神经元;前后端中间的分界线是激活函数,相当于生物神经元的阀值函数,用来对输入的组合信号判断是否达到阀值,如果达到阀值则该神经元激活,向输出端输出信号,否则抑制信号,不进行输出。

                                                                                                             图3 人工神经元的基本原理


       因此,神经网络的基本原理是将神经网络的输出值y与训练样本中标定的真实输出值进行比较,计算出输出误差,之后再使用这个误差值来指导前后两层中每两个神经元之间连接权重的调整,进而逐步改善神经网络的输出值,直至与训练样本的真实输出值之间的误差达到很小,在设定的可容忍范围内为止。可以看出,前后两层中每两个神经元之间的连接权重就是神经网络需要进行学习的内容,对这些连接权重持续进行优化,才能使神经网络的输出越来越好,达到我们满意的结果。


3.神经网络正向计算输出


        如上图2所示,输入信号从第一层(也就是输入层)进入神经网络后,不管自输入层以后到底有多少层,都可以使用以下两步来计算经过各层后的输出信号:一是利用连接权重来调节从前一层中各神经元输入的信号并进行组合;二是对组合之后的信号应用激活函数,生成该层的输出信号。而对于第一层的输入层而言,仅仅表示输入层中每个神经元的输入而已,对输入层中每个神经元不使用激活函数。因此,用强大的矩阵运算来表示以上所描述的自输入层以后每一层神经网络的正向输出值的话,则为:

                                                        $$ \vec{X}=\vec{W}\cdot\vec{I} \tag{1} $$              (1)
                                                       $$ \vec{O}=Sigmoid(\vec{X}) \tag{2} $$    (2)

其中,公式(1)就是利用连接权重来调节从前一层中各神经元输入的信号并进行组合,\vec{X}是组合后的信号矢量,\vec{W}是该层的各神经元与前一层的各神经元之间的连接权重矩阵,\vec{I}是前一层的输入信号矢量;公式(2)是对组合之后的信号应用激活函数并生成该层的输出信号,\vec{O}是该层的输出信号矢量,sigmoid是所采用的激活函数(也就是S阀值函数)。


4.神经网络反向传播误差


       如上图2所示,在神经网络中,我们将信号从输入层正向传播到输出层时用到了连接权重。此外,将输出层得到的误差反向传播到中间每一层时,我们也需要使用连接权重,与输入信号正向传播一样,我们同样会为具有较大连接权重的连接分配较大的误差。因此,在误差反向传播过程中,中间层(也称为隐藏层)节点的误差的计算思路是,中间层中每一个节点的误差是与该节点正向连接中所有连接权重分割的误差之和。例如,在图4所示的神经网络中,中间层第一个神经元(也称为节点)的误差既得到了在权重w_{1,1}的连接上第一个输出节点的输出误差e_{1}的一部分,同时也得到了在权重w_{1,2}的连接上第二个输出节点的输出误差e_{2}的一部分,其误差为这两部分误差之和;同理,中间层第二个节点的误差也是通过连接权重分割的误差之和得到。因此,通过反向传播误差的过程计算出中间层节点的误差,写成矩阵形式为:
                                                                                                                $$ \left[ \begin{matrix} e_{hidden,1} \\ e_{hidden,2} \end{matrix} \right] = \left[ \begin{matrix} \frac{w_{1,1}}{w_{1,1}+w_{2,1}} &\frac{w_{1,2}}{w_{1,2}+w_{2,2}} \\ \frac{w_{2,1}}{w_{1,1}+w_{2,1}} &\frac{w_{2,2}}{w_{1,2}+w_{2,2}} \end{matrix} \right] \left[ \begin{matrix} e_{1} \\ e_{2} \end{matrix} \right] $$

再观察上式,中间层节点的误差可以看成是由权重矩阵和输出层误差通过矩阵的乘法运算得到。其中,这个公式的权重矩阵中每个元素都是一个分数,其分母是一种归一化因子,如果我们忽略每一个分数的这些归一化因子,仅仅影响的是计算出的中间层节点误差的大小,而仍然符合我们前面所提出的较大的连接权重就意味着要携带较大的输出误差给中间层的原则。此时,上式就简化为:
                                                                                                                       $$ \left[ \begin{matrix} e_{hidden,1} \\ e_{hidden,2} \end{matrix} \right] = \left[ \begin{matrix} w_{1,1} & w_{1,2} \\ w_{2,1} & w_{2,2} \end{matrix} \right] \left[ \begin{matrix} e_{1} \\ e_{2} \end{matrix} \right] $$

此时,这个式子中的权重矩阵就是神经网络正向计算输出中公式(1)中的连接权重矩阵$\vec{W}$。因此,继续用强大的矩阵运算来表示以上所描述的自输出层反向传播误差过程中计算得到的中间层误差值的话,则为:
                                                                                                                       $$ \vec{E}_{hidden}=\vec{W}_{hidden-output}\cdot\vec{E}_{output} \tag{3} $$
        其中,公式(3)中\vec{E}_{hidden}是中间层节点误差矢量,\vec{W}_{hidden-output}是输出层与中间层之间的连接权重矩阵(如果中间有多层的话,就是后一层与该中间层之间的连接权重矩阵),\vec{E}_{output}是输出层的误差矢量(如果中间有多层的话,就是后一层的误差矢量)。

                                                                                                                        图4 反向传播误差示意图


 5.神经网络更新连接权重


        如何更新输入层和中间层之间的连接权重以及中间层和输出层之间的连接权重是神经网络学习过程中的核心问题。通过公式(3)我们知道,神经网络的误差是连接权重的函数,因此,改进神经网络就意味着通过改变连接权重来减少这种误差。那么,在最小化误差的过程中,我们就需要使用梯度下降法来计算出误差函数相对于连接权重的斜率。如图5所示,在梯度下降过程中,(1)和(2)是采用了两种不同起始点的情况,其中情况(1)容易陷入局部最优点,而没有找到能使误差最小的连接权重,因此,为避免这一问题的发生,在神经网络中会使用不同的起始连接权重来进行梯度下降从而求得全局最优点。

                                                                                                                  图5 梯度下降法示意图


       因此,求得误差函数相对于连接权重的斜率显得尤为关键,因为这个斜率就是使用梯度下降法找到能使输出误差值达到最小的方向。所以,对于第k个节点来说,与之连接的前一层中第j个节点的连接权重w_{j,k}的改变方向与斜率方向相反,其更新公式为:
                                                                                                               $$ {w}_{j,k}( new )={w}_{j,k}( old )-\alpha\times \frac{\partial E}{\partial w_{j,k}} $$    (4)

其中,$\alpha$是学习率,用来调节这个权重改变的强度,确保不会发生超调现象,这其实是一种有节制的更新方式。

接下来,我们把重心放在求解斜率这件事情上。对于第k个节点,其输出值为$o_{k}$,而真实的输出值为t_{k}(它是一个真实存在的常数),所以,斜率公式为:
                                                                     $$ \frac{\partial E}{\partial w_{j,k}} =\frac{\partial }{\partial w_{j,k}} (t_{k}-o_{k})^2 =-2(t_{k}-o_{k})\times \frac{\partial o_{k}}{\partial w_{j,k}} =-2(t_{k}-o_{k})\times \frac{\partial}{\partial w_{j,k}}sigmoid(\sum\limits_{j}w_{j,k}\times o_{j}) $$          (5)

而S函数sigmoid的求导公式为:\frac{\partial sigmoid(x)}{\partial x}=sigmoid(x)(1-sigmoid(x))。因此,公式(5)可写为:
                                                                                   $$ \frac{\partial E}{\partial w_{j,k}} = =-(t_{k}-o_{k})\times sigmoid(\sum\limits_{j}w_{j,k}\times o_{j}) (1- sigmoid(\sum\limits_{j}w_{j,k}\times o_{j}))\cdot o_{j} $$     (6)

观察上式,我们发现求得的斜率公式由三个部分的乘积项构成,第一部分就是误差值e_{k},只不过我们由公式(5)求得该式的时候省去了因数2,因为我们只对误差函数的斜率感兴趣,所以无需管它的数值大小;第二部分中sigmoid(\sum\limits_{j}w_{j,k}\times o_{j})就是联立公式(1)和(2),神经网络正向计算得到的节点k的输出值o_{k};第三部分o_{j}就是与第k个节点连接的前一层中第j个节点的输出。

因此,不论是输入层和中间层之间的误差函数相对于连接权重的斜率以及中间层和输出层之间的误差函数相对于连接权重的斜率都可以采用公式(6)计算得到,只不过在输入层和中间层之间的误差函数相对于连接权重的斜率求解过程中,需要先利用公式(3)表示的神经网络反向传播误差来求得中间层的误差,才能得到公式(6)中第一部分所表示的误差值,而在中间层和输出层之间的误差函数相对于连接权重的斜率求解过程中,直接由(真实值-输出值)求得了公式(6)中第一部分所表示的误差值。所以,继续用强大的矩阵运算来表示公式(4)所描述的由误差函数的斜率矩阵来更新连接权重矩阵的话,则为:
                                                                                            $$ \vec{W}_{j,k}( new ) - \vec{W}_{j,k}( old )=\alpha\times \vec{E}_{k}\cdot \vec{O}_{k}(1- \vec{O}_{k})\cdot \vec{O}_{j}^{T}\tag{7} $$      (7)

 二、Python神经网络编程


1.三层神经网络的代码框架

在以上描述的神经网络的基本原理以及相关计算公式的基础上,可以使用Python语言制作三层但不限每层中节点数目的神经网络。因此,一个神经网络类至少应该包括以下三个函数:

  1. 初始化函数——设定输入层、中间层和输出层节点的数目,设置学习率的大小, 随机初始化输入层和中间层以及中间层和输出层之间的连接权重矩阵。
  2. 训练函数——给定训练集样本后,正向计算输出值并根据样本标定的真实值算出误差值,再反向传播误差算出中间层的误差值,最后计算出误差函数相对于连接权重的斜率并利用梯度下降法更新输入层和中间层以及中间层和输出层之间的连接权重矩阵。
  3. 查询函数——给定输入后,计算出自输入层以后每一层神经网络的正向输出值并输出神经网络的最终值。

依据上述代码框架,下面给出神经网络类的具体代码:

import numpy
import scipy.special

# neural network class of 3 layer
class neuralNetwork :
    
    # initialiation
    def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate) :
        # 输入、隐藏和输出节点的个数(本类只适用3层神经网络)
        self.inodes = inputnodes
        self.hnodes = hiddennodes
        self.onodes = outputnodes
        # 学习率
        self.lr = learningrate
        # 输入和隐藏以及隐藏和输出的两层连接权重矩阵
        #self.wih = numpy.random.rand(self.hnodes, self.inodes) - 0.5 # 保证权重取值范围为-1.0~+1.0,而现在为-0.5~+0.5
        #self.who = numpy.random.rand(self.onodes, self.hnodes) - 0.5
        self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes)) # 正态分布方式初始化权重
        self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
        # 激活函数sigmoid()
        self.activation_function = lambda x: scipy.special.expit(x)
        pass

                                                                                                          图6 初始化函数的python代码

# training
    def train(self, input_list, target_list) :
        # <1> 正向计算输出
        inputs = numpy.array(input_list, ndmin = 2).T
        targets = numpy.array(target_list, ndmin = 2).T
        # 计算隐藏层节点的输入和输出
        hidden_inputs = numpy.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        # 计算输出层节点的输入和输出
        final_inputs = numpy.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)
        
        # <2> 反向计算误差
        # 计算输出层的误差
        output_errors = targets - final_outputs
        # 计算隐藏层的误差
        hidden_errors = numpy.dot(self.who.T, output_errors)
        
        # <3> 利用误差更新权重
        # 更新隐藏层和输出层之间的权重
        self.who = self.who + self.lr *  numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), 
                                                   numpy.transpose(hidden_outputs))
        # 更新输入层和隐藏层之间的权重
        self.wih = self.wih + self.lr *  numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),
                                                   numpy.transpose(inputs))
        pass
    

                                                                                                            图7 训练函数的python代码

# quering
    def query(self, input_list) :
        # 将输入的一行数据转化为二维数组,并且将行转置成列
        inputs = numpy.array(input_list, ndmin = 2).T
        # 计算隐藏层节点的输入和输出
        hidden_inputs = numpy.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        # 计算输出层节点的输入和输出
        final_inputs = numpy.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)
        # 返回
        return final_outputs

                                                                                                                    图8 查询函数的python代码


2.利用手写数字的数据集MNIST训练神经网络

由于MNIST数据集中每个手写数字的像素均统一为28x28=784个颜色灰度值,因此输入层节点个数需要与此对应,设置为784。而对于输出层来说,其需要识别并输出0到9共10个数字中的一个,这就意味着神经网络应该有10个输出层节点,每个节点对应一个数字标签。例如,在图9中,如果神经网络识别出的手写数字为“5”,则输出层中第6个节点被激活,而其余的输出节点保持抑制状态;如果神经网络识别出的手写数字为“0”,则输出层中第1个节点被激活,而其余的输出节点保持抑制状态;如果神经网络识别出的手写数字为“9”,则输出层中第10个节点被激活,而其余的输出节点保持抑制状态。当然,处于抑制状态的节点并不是一定会输出特别小的信号,只是相对于处于激活状态的节点来说,它的信号值相对较小而已,因为我们通常只会使用信号值最大的节点作为处于激活状态的节点。

                                                                                                                             图9 输出层节点工作示意图

此外,我们使用同样的训练数据集,重复多次进行训练,这样能提高神经网络的性能。我们把训练一次称为一个世代,那么具有5个世代的训练,就意味着使用整个训练数据集运行程序5次。这个做法虽然会增加计算机运行的时间,但却是值得的。因为在前面的神经网络基本原理里讲过连接权重矩阵是随机初始化的,这也就意味着在梯度下降过程中每次选择的起始点不同,会提供更多的爬下坡的机会,不容易陷入错误的局部最优点,更有助于在梯度下降过程中进行连接权重的更新。所以,利用手写数字的数据集MNIST训练神经网络的代码如下:

import numpy

# 创建神经网络
input_nodes = 784
hidden_nodes = 200
output_nodes = 10
learning_rate = 0.09
N = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)

# 训练神经网络
training_data_file = open("C:/Users/Administrator/mnist_dataset/mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
epochs = 5 # 训练5个世代
for e in range(epochs):
    for record in training_data_list:
        all_values = record.split(',')
        inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01 # 输入数据取值范围为0.01~1.0
        targets = numpy.zeros(output_nodes) + 0.01 # 输出数据为0.01和0.99
        targets[int(all_values[0])] = 0.99
        N.train(inputs, targets)
        pass
    pass

                                                                        图10 手写数字的数据集MNIST训练神经网络的python代码


3.利用手写数字测试神经网络

当神经网络完成训练以后,我们还可以利用MNIST测试数据集来测试神经网络的性能,看看训练好的神经网络在它从未见过的手写数字所构成的测试数据集上的识别正确率如何。同时,也可以自己动手写几个数字,制作成图片并导入神经网络程序中观察该训练好的神经网络是否能进行正确识别。所以,利用MNIST测试数据集和自己手写数字来测试已训练好的神经网络的代码如下图11所示:

# 用mnist测试数据集来测试神经网络
test_data_file = open("C:/Users/Administrator/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_values = record.split(',')
    correct_label = int(all_values[0]) # 正确的数字
    inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01 
    outputs = N.query(inputs)
    label = numpy.argmax(outputs) # 找出一列中最大的数的位置,从0开始编号,也就是输出的数字
    if (correct_label == label):
        scorecard.append(1)
    else:
        scorecard.append(0)
    pass
scoredcard_array = numpy.asarray(scorecard)
print("mnist测试数据集中10000条手写数据的识别正确率为:", scoredcard_array.sum() / scoredcard_array.size)

# 用自己手写数字来测试神经网络
from PIL import Image #导入图像处理工具
image = Image.open("E:/MLP/9.png").convert('F') #打开图像
#image = image.resize((28,28)) #调整图像大小
arr=[] #将图像中的像素作为预测数据点的特征
for i in range(28):
     for j in range(28):
        pixel = 255.0 - float(image.getpixel((j,i)))
        pixel2 = (pixel / 255.0 * 0.99) + 0.01
        arr.append(pixel2)
#arr1 = numpy.array(arr).reshape(1,-1) #只有一个样本,需要进行reshape操作
# 画出将要识别的手写数字    
import matplotlib.pyplot
image_array = numpy.asfarray(arr1).reshape((28,28))
matplotlib.pyplot.imshow(image_array, cmap='Greys', interpolation = 'None')
# 神经网络识别出手写数字 
label2 = numpy.argmax(N.query(arr1)) # 找出一列中最大的数的位置,从0开始编号,也就是输出的数字
print("识别的数字为:", label2)
#N.query(arr1)

# 画出mnist数据集中任何一行的数字
import matplotlib.pyplot
all_values = test_data_list[12].split(',')
image_array = numpy.asfarray(all_values[1:]).reshape((28,28))
matplotlib.pyplot.imshow(image_array, cmap='Greys', interpolation = 'None')
N.query((numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01)

                                                                           图11 利用MNIST测试数据集和自己手写数字来测试神经网络的python代码

测试结果的输出,如下图12所示:

                                                                                                                    图12 测试结果的输出

从上图可以看出,在MNIST测试数据集上,所构建的神经网络表现相当优异,其对MNIST测试数据集中手写数字的正确识别率达到了97%左右。此外,该神经网络也能够正确识别出自己动手写的数字“9”,在输出层的10个节点中,我们能看到第10个输出节点的信号值是最大的,因此神经网络判定它就是激活的节点,因此该节点对应的数字标签“9”就是神经网络最后给出的手写数字的识别结果。

至此,本文章将神经网络的基本原理、核心计算公式以及相应的python代码均介绍完毕,并且还通过训练好的神经网络模型正确识别出了自己手写的数字,是对神经网络知识进行入门学习的一个比较好的开端。

 

欢迎喜欢文史和科技类的朋友们关注公众号:

CodingFarmer2019

我们一起学史悟道,格物致知,再引领科技发展!实现人生辉煌!

 


 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值