神经网络初试(二)

引入

今天我们就继续上次的那篇文章,谈一谈神经网络中比较基础也比较核心的部分。按顺序分别是,前向传播、反向传播、权重更新以及代码实现。

在进入正题之前,还是有必要介绍一些事情,方便对于下文的理解。

第一、本文,乃至本系列的文章,其主体都是全连接神经网络。全连接神经网络应该是神经网络中最简单的一种了。顾名思义,全连接指的是对于任意一层的某个神经元来说,它与前后两层的所有神经元之间都存在连接。如图所示:

这个神经网络有三层,从左往右分别是之前提到过的输入层、隐藏层、输出层。每一层都有三个神经元,一般情况下,这代表着它的输入应该是一个三维数据,也就是三维向量,例如三维空间中任意一个坐标。隐藏层的三个神经元,把它当成处理数据的黑盒就好了。输出层的三个神经元,则代表神经网络的输出也是一个三维的数据,例如它的输出也是一个坐标。

之前我们也提到了偏置项,使用偏置项来表示输入层之外的神经元的阈值,并且提到了,可以在每一层神经元(除了输出层)上方添加一个偏置神经元,形式更加统一的表示神经网络。上方的图片是不包含偏置神经元的神经网络结构图,我们可以再看一个包含偏置神经元的神经网络。


诺,对于这个神经网络而言,它的前两层最上方的神经元都是偏置神经元,其输入恒为1,它与下一层的神经元连接的权重,则表示着相应神经元的阈值。

当然啦,倘若我们不把最上面两个神经元看作偏置神经元,这就是一个非全连接的神经网络。这不是我们研究的对象~

第二、本文提到的神经网络都是前馈的,也就是输入层接受外界输入, 隐含层与输出层神经元对信号进行加工, 最终结果由输出层神经元输出。数据在其中是从输入到输出单向传递的,不会从输出层又向输入层传回来。实际的应用中,自然也是存在这种可以反馈的,也就是信号回传的神经网络,但这也不是我们的研究对象。

所以,我们的研究对象就是全连接的,前馈的神经网络,也就是最简单的一种神经网络。它是之后神经网络各种变形的基础,所以还是很有必要了解掌握的。

接下来进入正题。

前向传播

前向传播其实我们在上一篇文章中提过,所谓的前向传播,其实就是神经网络进行预测输出时,所进行的操作。我们以一个简单的神经网络为例再次说明这一过程,作为接下来的铺垫。

上图的神经网络就是我们用于介绍前向传播与反向传播的主体啦。它的输入层是一个二维向量,输出也是一个二维向量,隐藏层有三个神经元。一般来说,前向传播过程中一个神经元对应一个一维数据。

对于权重,我们使用来表示,代表这一层第个神经元与下一层第个神经元之间的权重,是一个具体的数值。这里为了简化,我们暂时不把偏置项加进去,换句话说,我们将所有神经元的阈值都置为0。因此,这是一个只有权重,阈值为0的全连接前馈神经网络。

除了权重和阈值,激活函数也是神经网络的核心之一。这里我们采用较为传统的sigmoid函数作为激活函数,记为。因此,激活函数

接下来我们来叙述一下前向传播的过程:

给定一个输入向量,我们可以计算出隐藏层神经元的输入向量,记为

是一个列向量(本文以分号为分隔符的向量均为列向量),其中每一个分量的计算公式为

这个过程在上一篇文章中已经提到了,为了方便表示以及计算,我们可以使用矩阵来表示这一过程。(没学过线性代数的自己去学一下哦)

这个其实很简单,我们将输入向量(列向量)记为

这其实也是一个的矩阵,两行一列。类似的,隐藏层的输入向量是一个的矩阵。之后,我们记输入层与隐藏层之间的矩阵为,所以

这是一个的矩阵。

所以

不过仅仅是隐藏层的输入,也就是隐藏层每一个神经元接收到的数据。由于我们并没有考虑阈值,所以这里直接对隐藏层的输入数据进行激活,就得到了隐藏层每一个神经元的输出,即

这样,数据就由最原始的输入,先转化为隐藏层接收的输入数据,又转化为了隐藏层的输出。

同理,我们将隐藏层的输出再传递到输出层去。记隐藏层与输出层之间的权重矩阵为

其中表示隐藏层第个神经元与输出层第个神经元之间的权重。

接着,我们计算出输出层接收到的二维向量为

最后,再经过激活函数,得到最终的输出

就这样,我们实现了上述神经网络前向传播的过程,及其矩阵表示,实现了从输入到输出的转化。即
\begin{cases} X_{hidden}=W_{input\_hidden} \cdot I =W_{input\_hidden} \cdot [x_1;x_2]\\ O_{hidden}=h(X_{hidden})=sigmoid(X_{hidden}) \\ X_{output}=W_{hidden\_output} \cdot O_{hidden} \\ O_{output}=sigmoid(X_{output})=[y_1;y_2] \end{cases}

神经网络的前向传播,就是这么一个从的过程。当然,这里的都可以是任一维度的数据。

误差反向传播

前向传播的过程,是我们已知权重矩阵,实现从输入到输出的过程。而误差的反向传播,解决的则是已知输入和输出,求解合适的权重矩阵的过程。

之前提到过,机器学习三要素是模型、策略和算法。放在当前语境下,模型就是上文中那个简单的神经网络。策略,则是我们衡量误差的手段,这里我们使用最经典的平方损失函数作为损失函数,即我们的策略。记对于某个样本,该神经网络模型进行预测时的误差为

其中,表示样本的实际输出值的第个分量,表示的是将样本的实际输入值输入神经网络后,输出层第个神经元的输出值,也就是的第个分量的预测值。对于上述的神经网络,显然,。

问题来了,我们现在表示出了误差,也知道了我们希望通过调整每一层的权重矩阵来缩小这个误差,那如何进行调整呢?

这里就不再引入了,直说吧,我们可以使用梯度下降法进行权重的调整。如果对于梯度下降法不太了解,可以看看我之前写的那篇关于梯度下降的文章,或者自行搜索一下。

我们之前使用梯度下降法,是基于

这里就是损失函数,就是参数,而则是关于进行的移动。对于一般的问题,比如一元线性回归,可以直接构造出这样一个等式,并且发现误差下降的方向,就是负梯度的方向。之后一步步进行迭代,对于参数进行调整。

但是对于神经网络,问题就没有这么简单了。神经网络是一层层叠加的,每两层之间都有一个权重矩阵。但是我们可以观察到的误差,仅仅是最后输出的误差,也就是预测值与实际值之间的误差。虽然这一层的误差是由之前的误差层层传递而来,但是我们很难直接通过最后一层的误差,对最前几层的权重矩阵进行调整。假设一个神经网络有10层,那么我们或许可以使用梯度下降,基于最后一层的误差,对最后一层的权重矩阵进行调整,但是对于第一层和第二层之间的权重矩阵,又要怎么构建“误差与权重”之间的关系呢?

当然,肯定是可以一步步构建的,但一定会很麻烦……因为中间涉及到的每一个权重矩阵,对于最后的误差都有影响,所以如果想直接一步步推导回来,那么误差与第一个权重矩阵的关系等式中,必然包含着。感兴趣的,可以以一个三层的神经网络,自己尝试一下。

因此,我们的想法就是,将最终可以观察到的误差,一步步往回进行反向传播,得到每一层的输入值与输出值之间的误差,并基于此,使用梯度下降的方式对于相应的权重矩阵进行调整。可以理解为,每两层之间的权重矩阵都对应着一个误差,这个误差与相应的权重矩阵是直接相关的。这样就不用一层层跋山涉水将距离很远的误差与权重直接构建关系啦。

如上图所示,是我们可以直接观察到的误差(=实际值-预测值),它由这两个分量组成,代表输出层两个神经元与实际值的误差。与有着直接的关系,因此我们可以通过对进行修正。同理,我们通过分别对进行调整。所以问题就转化为,我们如何通过,求解得到呢?

这就涉及到误差的反向传播啦,回到我们的那个模型上


对于这个三层的神经网络,我们可以观察到的误差是,并且,我们知道这两层之间的权重,那如何求解第二层的误差呢?

简单理解的话,我们可以把误差的反向传播过程,看成误差的分配过程。对于误差,与其直接有关的误差分别是。在神经网络前向传播的过程中,也通过权重传递了过来,最终汇总形成了。

我们用一个简单的方式理解一下:不妨假设激活函数为,则,那么由全微分公式



如果我们将看成相应的误差,那么的误差其实就是误差的加权和。事实上误差的传播公式也是这么回事,感兴趣的可以自行搜索一下。

因此,如果我们现在知道了,想要求解,很自然的一个想法,就是根据权重再将分解下去。即
\begin{cases} \Delta a_1=\frac{w_{11}}{w_{11}+w_{21}+w_{31}}\cdot \Delta y_1 \\ \Delta a_2=\frac{w_{21}}{w_{11}+w_{21}+w_{31}}\cdot \Delta y_1 \\ \Delta a_3=\frac{w_{31}}{w_{11}+w_{21}+w_{31}}\cdot \Delta y_1 \end{cases}
其实这样的分解应该不是正确的,因为此时

并不一定成立。但这是一个很合理的分配方式,相信你也是这么认为的。

让我们回到上述模型,则从那里分到的误差为

同理,它从那里分到的误差为

由于和本身并不存在直接关系,因此这两者分给的误差直接相加,就可以得到

以此类推
\begin{cases} e_{11}=\frac{w_{11}}{w_{11}+w_{21}+w_{31}}\cdot e_{21}+\frac{w_{12}}{w_{12}+w_{22}+w_{32}}\cdot e_{22} \\ e_{12}=\frac{w_{21}}{w_{11}+w_{21}+w_{31}}\cdot e_{21}+\frac{w_{22}}{w_{12}+w_{22}+w_{32}}\cdot e_{22} \\ e_{13}=\frac{w_{31}}{w_{11}+w_{21}+w_{31}}\cdot e_{21}+\frac{w_{32}}{w_{12}+w_{22}+w_{32}}\cdot e_{22} \end{cases}

举个例子看一下


事实上,我们更加关心的不是误差本身,而是对于误差的分配,即将后一层的误差按照权重向前一层进行传递。所以,为了进一步的简化计算,我们直接将上式中的分子去掉,即

有些同学会对此感到迷惑,因为他们的分母并不完全一样啊,怎么能说删就删呢?可以这么想,来自于两个部分,分别是和,这两者首先不是直接相关的,因此我们可以对于误差直接相加,得到。而其中来自于的部分,其分母是对于的归一化处理,我们可以将其转化为,这并不改变按权重分配的本质。同样的,可以将转化为。最后如之前一般再将二者直接相加,就得到了

明白了吗?其实应该是似懂非懂,因为整个过程在数学上有些缺乏严谨性,更多的是从直观理解的角度进行处理,比如误差直接按权分配,去除归一化的因子等等。如果觉得不太能接受,那可以去看看更加数学化的误差反向传播方法的推导。本文关于误差反向传播的说明,好像确实有种邪·教的感觉hhh。但是本质上的思想应该是一样的。

最后,为了方便表示,我们记

则由上文,我们知道


权重更新

以上我们(或许?)知道了误差是如何进行反向传播的,并进行了矩阵的表示,接下来就可以根据误差对权重进行调整啦。

首先,对于神经网络任意一层的误差

所以对于相应的权重矩阵中的任一个参数

再看一眼下图,我们会发现,中与有关的部分,仅仅由后一层的第个神经元有关,相应的误差部分,也仅仅与第个神经元的误差有关。(例如下图中只与有关)所以



根据链式法则(微积分中的求导法则都忘了?)
\begin{alignat}{2} \frac{\partial E}{\partial w_{j,k}} & =\frac{\partial E}{\partial o_k}\cdot \frac{\partial o_k}{\partial w_{j,k}} \\ & = -2(y_k-o_k)\cdot \frac{\partial o_k}{\partial w_{j,k}} \\ & =-2(y_k-o_k)\cdot \frac{\partial }{\partial w_{j,k}} sigmoid(\sum_{j}w_{jk}\cdot o_{j}) \end{alignat}
解释一下 这部分。首先, 是后一层第 个输出, 则是前一层第 个节点的输出, 中 则是固定的。例如上图,对于 ,它仅与 有关,而 相应神经元的输出 ,则与前一层的三个神经元都有关系,就是前文提到的前向传播啦。(说明: , 表示神经网络第 层)

对于激活函数

所以
\begin{alignat}{2} \frac{\partial E}{\partial w_{j,k}} & =-2(y_k-o_k)\cdot \frac{\partial }{\partial w_{j,k}} sigmoid(\sum_{j}w_{jk}\cdot o_{j}) \\ & = -2(y_k-o_k)\cdot \frac{\partial\ sigmoid(\sum_{j}w_{jk}\cdot o_{j})}{\sum_{j}w_{jk}\cdot o_{j}} \cdot \frac{\partial \sum_{j}w_{jk}\cdot o_{j}}{\partial w_{j,k}} \\ & = -2(y_k-o_k)\cdot sigmoid(\sum_{j}w_{jk}\cdot o_{j})\cdot (1-sigmoid(\sum_{j}w_{jk}\cdot o_{j}))\cdot o_j \end{alignat}
最后的,自然是上一层的第个输出啦,刚刚讲过了。(说明:若,表示神经网络第层)

考虑到在进行迭代时,导数前面的系数意义不大(还有学习率支撑嘛),所以我们可以把式子中的删掉,另外,就得到了误差关于权重的导数

相应的参数更新公式则为

其中为学习率。

但是是一个权重矩阵,所以我们有必要把上式也转化成矩阵的形式,便于计算。以上文的那个三层神经网络为例,第二层的权重更新公式为

其中均是上文提到过的前向传播过程中的列向量。

相信你可以自己写出的迭代公式了…

以上,参数的更新部分就全部结束了

MNIST数据集

接下来介绍一下MNIST手写数字图像集。MNIST是机器学习领域最有名的数据集之一,被应用于从简单的实验到发表的论文研究等各种场合。MNIST数据集是由0到9的数字图像构成的。训练图像有6万张,测试图像有1万张,主要分别用于训练和评价。

MNIST数据集的一般使用方法是,先用训练图像进行学习,再用学习到的模型度量能在多大程度上对测试图像进行正确的分类。

MNIST的图像数据是28像素 × 28像素的灰度图像(1通道),各个像素的取值在0到255之间。每个图像数据都相应地标有“2”、“3”、“9”等标签。因此,这些图像在进行分类时的输入特征就是28*28=784个位置的像素灰度取值,输出特征就是相应的最可能的数字标签。

由于这类图像是比较简单的灰度图,所以完全可以自己手写一些数字,将其转化成28×28的灰度图像,进而进行验证。更加直接地看到神经网络的力量。

原始数据集网址:http://yann.lecun.com/exdb/mnist/

代码实现

原始的数据集应该是无法直接使用的……这里提供一个网址:https://pjreddie.com/projects/mnist-in-csv/

该网站提供了将原始数据集转化为csv格式的代码,以及csv的文件。同时也是接下来的代码使用到的数据集文件,如果需要的话自行下载吧。

代码也放在下面了,没有依赖现成的第三方库,完全是把上文的过程用代码重述了一遍。如果你已经对于本文的内容很理解了,那么看这段代码应该没有任何问题。如果你看代码还是有问题,不如再把文章读一读?

# -*- coding: utf-8 -*-
import numpy as np
import scipy.special
import matplotlib.pyplot as plt
class neuralNetwork:
    def __init__(self,inputnodes,hiddennodes,outputnodes,learningRate):
        #定义一个三层的神经网络
        #初始化输入层节点数量,隐藏层节点数量,输出层节点数量
        self.inodes=inputnodes
        self.hnodes=hiddennodes
        self.onodes=outputnodes
        
        #初始化两层之间的权重矩阵
        self.wih=(np.random.rand(self.hnodes,self.inodes)-0.5)
        self.who=(np.random.rand(self.onodes,self.hnodes)-0.5)
        
        #初始化激活函数
        self.activation_function=lambda x:scipy.special.expit(x)
        
        #初始化学习率
        self.lr=learningRate
        pass
    
    #训练函数
    def train(self,inputs_list,targets_list):
        #将输入的列表转化为list便于计算
        inputs = np.array(inputs_list,ndmin=2).T
        targets=np.array(targets_list,ndmin=2).T
        
        #前向传播一次,计算隐藏层的输入和输出
        hidden_inputs=np.dot(self.wih,inputs)
        hidden_outputs=self.activation_function(hidden_inputs)
        
        #再前向传播一次,计算输出层的输入和输出
        final_inputs=np.dot(self.who,hidden_outputs)
        final_outputs=self.activation_function(final_inputs)
        
        #确定输出层的误差与隐藏层的误差
        output_errors=targets-final_outputs
        hidden_errors=np.dot(self.who.T,output_errors)
        
        #根据误差,使用梯度下降法调整权重
        self.who+=self.lr*np.dot((output_errors*final_outputs*(1.0-final_outputs)),np.transpose(hidden_outputs))
        self.wih+=self.lr*np.dot((hidden_errors*hidden_outputs*(1.0-hidden_outputs)),np.transpose(inputs))
        
        pass
    
    #查询函数
    def query(self,inputs_list):
        #将列表转化成矩阵
        inputs=np.array(inputs_list,ndmin=2).T
        
        #进行两次前向传播得到最终解
        hidden_inputs=np.dot(self.wih,inputs)
        hidden_outputs=self.activation_function(hidden_inputs)
        
        final_inputs=np.dot(self.who,hidden_outputs)
        final_outputs=self.activation_function(final_inputs)
        
        return final_outputs
    
input_nodes=784
hidden_nodes=100
output_nodes=10
lr=0.25
n=neuralNetwork(input_nodes,hidden_nodes,output_nodes,lr)
training_data_file=open("mnist_train.csv","r")
training_data_list=training_data_file.readlines()
training_data_file.close()
'''
for record in training_data_list:
    all_values=record.split(",")
    #ia=np.asfarray(all_values[1:]).reshape((28,28))
    #plt.imshow(ia,cmap="Greys",interpolation="None")  画出图像

    inputs=(np.asfarray(all_values[1:])/255.0*0.99)+0.01 #标准化处理
    targets=np.zeros(output_nodes)+0.01
    targets[int(all_values[0])]=0.99 #处理输出向量
    n.train(inputs,targets)
    pass
'''   

epochs=4
for e in range(epochs):
    for record in training_data_list:
        all_values=record.split(",")
        #ia=np.asfarray(all_values[1:]).reshape((28,28))
        #plt.imshow(ia,cmap="Greys",interpolation="None")  画出图像
    
        inputs=(np.asfarray(all_values[1:])/255.0*0.99)+0.01 #标准化处理
        targets=np.zeros(output_nodes)+0.01
        targets[int(all_values[0])]=0.99 #处理输出向量
        n.train(inputs,targets)
        pass


test_data_file=open("mnist_test.csv","r")
test_data_list=test_data_file.readlines()
test_data_file.close()

score=[]
for record in test_data_list:
    test_value=record.split(",")
    correct_label=int(test_value[0])
    #print(correct_label," correct_label")
    inputs=(np.asfarray(test_value[1:])/255.0*0.99)+0.01
    outputs=n.query(inputs)
    label=np.argmax(outputs)
    #print(label," network's answer")
    if(label==correct_label):
        score.append(1)
    else:
        score.append(0)
        pass
    pass
print("performance = ",sum(score)/len(score))

写得累死我了……其实除了反向传播那里有些邪·教外,其他部分都还好,当然我的叙述可能是有一些啰嗦。这篇文章比较适合完全不了解神经网络的朋友了解一下神经网络,其实并没有想象的那么困难,几十行代码就可以实现一个简单的神经网络了。

如果深入学习的话,不妨去听一听吴恩达的深度学习,会更专业一些……不,会专业很多。毕竟这个神经网络连偏置都没有。

最后,不管怎样,感谢你看到这里,写的不容易,看的就更不容易了……是不是应该把它拆成两篇?

不管了,就酱,bye~

你可能感兴趣的:(神经网络初试(二))