目录
1 什么是神经网络
1.1 神经元模型
1.2 感知机
1.3 多层神经网络
1.4 为什么要使用神经网络
2 神经网络目标函数
2.1 交叉熵代价函数(二分类交叉熵解决多分类问题)
2.2 交叉熵代价函数(多分类交叉熵)+ softmax函数
2.3 均方误差
3 神经网络优化算法(误差逆传播算法,BP)
3.1 前向传播
3.2 使用交叉熵代价函数(二分类交叉熵形式)反向传播
3.2.1 反向传播
3.2.2 ’误差’的计算
3.2.3 反向传播的推导
3.3 使用交叉熵代价函数(多分类交叉熵形式)+softmax函数进行反向传播
3.3.1 算法步骤
3.3.2 反向传播的推导
3.4 使用均方误差函数反向传播
3.4.1 反向传播步骤
3.4.2 反向传播的推导
4 神经网络算法步骤
5 神经网络算法分析
6 对比均方误差和交叉熵(二分类)
7 实例(均方误差函数)
7.1 前馈的实质
7.1.1 初始化权重和输入信号
7.1.2 输入层到隐藏层
7.1.3 隐藏层到输出层
7.2 反向传播的实质
7.2.1 计算总误差(使用均方误差)
7.2.2 隐藏层和输出层的权重更新
7.2.3 输入层和隐藏层的权重更新
8 python实现
人工神经网络(artificial neural network,缩写ANN),简称神经网络(neural network,缩写NN)或类神经网络,是一种模仿生物神经网络(动物的中枢神经系统,特别是大脑)的结构和功能的数学模型或计算模型,用于对函数进行估计或近似。
神经网络主要由:输入层,隐藏层,输出层构成。当隐藏层只有一层时,该网络为两层神经网络,由于输入层未做任何变换,可以不看做单独的一层。实际中,网络输入层的每个神经元代表了一个特征,输出层个数代表了分类标签的个数(在做二分类时,如果采用sigmoid分类器,输出层的神经元个数为1个;如果采用softmax分类器,输出层神经元个数为2个;如果是多分类问题,即输出类别>=3时,输出层神经元为类别个数),而隐藏层层数以及隐藏层神经元是由人工设定。一个基本的两层神经网络可见下图(注意:说神经网络多少层数的时候一般不包括输入层。 在神经网络中的激活主要讲的是梯度的更新的激活):
图1 两层神经网络
神经网络的最基本的构成元素是神经元(Neuron),也就是Kohonen的定义中的简单单元。学过生物的都知道,人的大脑中有上亿个神经元构成神经网络,生物神经网络中各个网络之间相互连接,通过神经递质相互传递信息。如果某个神经元接收了足够多的神经递质(乙酰胆碱),那么其点位变会积累地足够高,从而超过某个阈值(Threshold)。超过这个阈值之后,这个神经元变会被激活,达到兴奋的状态,而后发送神经递质给其他的神经元。
1943年,McCulloch和Pitts将生物神经网络工作的原理抽象成了一个简单的机器学习模型——MP神经元模型。
图2 MP神经元模型
MP神经元模型接收来自n个其他神经元传递过来的输入信号(x1~xn),这些输入信号通过带权重(θ或ω来表示权重,上图采用θ)的连接(Connection)进行传递,然后神经元(图示阈值为b)收到的总输入(所有输入和权重的乘积的和)与神经元的阈值b比较,并经由激活函数(Activation Function,又称响应函数)处理之后产生神经元的输出。
理想情况下,激活函数的形式应为阶跃函数(也就是修正线性单元ReLU)。如下图所示:
图3 阶跃函数
即输出有0或1两种,0 表示神经元不兴奋,1 则表示神经元兴奋。但阶跃函数不光滑且不连续,因此对于激活函数的选择通常会选择S函数(Sigmoid Function)。
S函数的相关内容,详见机器学习算法(一):逻辑回归模型(Logistic Regression, LR)_意念回复的博客-CSDN博客_逻辑回归模型。
S函数的定义如下所示:
其函数曲线图像如下:
图4 S函数曲线
从函数图像可以明显看出,函数的值域是(0,1)。即函数值落在0到1之间。S函数的性质有可以将较大范围内变化的输入值压缩到(0,1)区间内,因此也被成为挤压函数(Squashing Function)。
通过把很多个神经单元按照一定的层次连接起来,便得到了一个神经网络。
感知机(Perceptron)是由两层神经元所构成。
图5 由两层神经元构成的感知机
如图所示,输入层接收输入信号之后传输给输出层,输出层即阈值逻辑单元(MP神经元)。通过感知机可以轻易地实现逻辑与、或、非运算。
通过感知机实现逻辑运算,在MP神经元模型中,有输出:
假设激活函数为阶跃函数sgn(x):
我们通过制定权重和阈值得到了可以进行逻辑运算的感知机,那么如果给定训练数据集,同样我们可以通过学习得到相应地权重和阈值。
阈值b可以看做是一个输入固定为-1.0对应连接权重ωn+1的哑结点。通过这样定义阈值,则可以将学习权重和阈值简化为只学习权重。
感知机的学习规则很简单,对于训练样本(x,y),若感知机当前的输出为y',则感知机的权重调整如下:
其中,η为学习速率。若感知机对样本的预测正确的话,即y'=y,则感知机不发生任何变化。若其预测错误,则根据错误的程度进行相应权重的调整。
感知机中,只有输出层神经元进行激活函数的处理,也就是说感知机只有一层功能神经元(Functional Neuron),因此感知机的学习能力有限。实际上,通过感知机所实现的逻辑运算都是线性可分问题。
若两类模式是线性可分的,那么存在一个线性超平面能将其分开,则感知机的学习过程一定会收敛从而得到合适的权向量:
否则,感知机在学习的过程中会产生震荡,从而无法得到稳定的权向量,从而使得感知机无法求解。也就是说,感知机无法解决像是“异或XOR”(异或是非线性可分的问题)这样的非线性问题。
图6 线性可分的“与”、“或”、“非”问题与非线性可分的“异或”问题
如上图中,左面三个是通过线性分割实现,而最右侧的异或则无法通过线性分割实现。
图7 基本的两层神经网络
其中 为输入层的值, ,表示第 k 层中,第 i 个神经元的激活值(该神经元的输出), 表示第 k 层的神经元个数。当 k=1时即为输入层,即 ,而 为偏置项。
为了求最后的输出值 ,我们需要计算隐藏层中每个神经元的激活值 。而隐藏层/输出层的每一个神经元,都是由上一层神经元经过类似逻辑回归计算而来。我们可以使用下图进行理解:
图7 基本的两层神经网络(带权重)
我们使用 来表示第 k 层的参数(边权),其中下标 j 表示第 k+1 层的第 j 个神经元,i 表示第 k 层的第 i 个神经元。于是我们可以计算出隐藏层的三个激活值:
再将隐藏层的三个激活值以及偏置项 用来计算出输出层神经元的激活值即为该神经网络的输出:
其中 g(z) 为非线性变换函数(激活函数)。
相关矩阵的维度:
首先,神经网络应用在分类问题中效果很好。 工业界中分类问题居多。LR 或者 linear SVM 更适用线性分类。如果数据非线性可分(现实生活中多是非线性的),LR 通常需要靠特征工程做特征映射,增加高斯项或者组合项;SVM需要选择核。 而增加高斯项、组合项会产生很多没有用的维度,增加计算量。GBDT 可以使用弱的线性分类器组合成强分类器,但维度很高时效果可能并不好。而神经网络在三层及以上时,能够很好地进行非线性可分。现在我们使用下面的例子进行一下解释。
有这样一组样本,如下图:
若我们需要对上图中的样本进行分类,直观来看,很难找到一条线性分类边界对其进行分类,而观察上表中的输入输出值,我们可以看出分类结果与输入值是异或关系。而逻辑回归可以通过改变参数,来实现“与”、“或”、“非”简单操作。
(1)我们先来观察一下逻辑回归实现逻辑“与”操作,假设模型函数如下:
对应结构与结果为:
(2)逻辑回归实现逻辑“或非”操作,假设模型函数如下:
对应结果为:
(3)逻辑回归实现逻辑“或”操作,假设模型函数如下:
对应结果为:
观察(1)(2)中的 与 的值,通过“或”操作,便能够得到“异或”操作的结果。
也就是说,若将三个逻辑回归操作进行叠加,便能够对上述例子进行非线性分类。大致结构图可理解为下:
而对线性分类器的逻辑与和逻辑或的组合可以完美的对平面样本进行分类。
隐层决定了最终的分类效果 :
由上图可以看出,随着隐层层数的增多,凸域将可以形成任意的形状,因此可以解决任何复杂的分类问题。实际上,Kolmogorov理论指出:双隐层感知器就足以解决任何复杂的分类问题。
于是我们可以得出这样的结论:神经网络通过将线性分类器进行组合叠加,能够较好地进行非线性分类。
同样的,对于神经网络我们也需要知道其目标函数,才能够对目标函数进行优化从而学习到参数。
假设神经网络的输出层只有一个神经元,该网络有 K 层,则其目标函数为(若不止一个神经元,每个输出神经元的目标函数类似,仅仅是参数矩阵的不同):
其中 倒数第2层的激活值,作为输出层的输入值。而其值为 , 为实际分类结果 0/1 , m 为样本数, 为第 k 层的神经元个数。
代价函数参考逻辑回归:机器学习算法(一):逻辑回归模型(Logistic Regression, LR)_意念回复的博客-CSDN博客_逻辑回归模型
注:
在吴恩达的机器学习视频中,当输出只有两种可能,即输出层神经元只有一个节点时,代价函数如下:
当输出有三种及以上种可能时,即输出层神经元有三个及以上节点时,(多分类问题),代价函数如下:
此处的K与上文的K含义不同,此处的K为输出的类别数,即输出层神经元的个数。
softmax函数:
SoftMax函数:SoftMax函数_意念回复的博客-CSDN博客_softmax函数图像
SoftMax函数的推导(广义线性模型):指数分布族函数与广义线性模型(Generalized Linear Models,GLM)_意念回复的博客-CSDN博客_广义线性模型连接函数
多分类交叉熵:
熵:信息熵、相对熵、交叉熵_意念回复的博客-CSDN博客
K为输出的类别数,即输出层神经元的个数。m 为样本数
神经网络与普通的分类器不同,其是一个巨大的网络,最后一层的输出与每一层的神经元都有关系。而神经网络的每一层,与下一层之间,都存在一个参数矩阵。我们需要通过优化算法求出每一层的参数矩阵,对于一个有 K 层的神经网络,我们共需要求解出 K−1 个参数矩阵。因此我们无法直接对目标函数进行梯度的计算来求解参数矩阵。
对于神经网络的优化算法,主要需要两步:前向传播(Forward Propagation)与反向传播(Back Propagation)。
前向传播就是从输入层到输出层,计算每一层每一个神经元的激活值。也就是先随机初始化每一层的参数矩阵,然后从输入层开始,依次计算下一层每个神经元的激活值,一直到最后计算输出层神经元的激活值。
以下面这个例子来看:
前向传播计算激活值的过程:
(1)随机初始化参数矩阵:
(2)计算隐藏层的每个神经元激活值:
即:
(3)计算输出层的每个神经元激活值:
即:
反向传播总的来说就是根据前向传播计算出来的激活值,来计算每一层参数的梯度,并从后往前进行参数的更新。
在介绍反向传播的计算步骤之前,我们先引入一个概念---除输入层外每个神经元节点的“损失” , 表示第 k 层第 j 个神经元的损失。
于是我们可以计算求得(除输入层)每一层神经元的损失(以上一个例子来解释):
其中 为实际值。向量化表示如下:
其中 ⋅∗ 表示两个矩阵对应位置上元素相乘, 是对函数求导。而
由上可看出,第二层的损失 是基于第三层的损失 计算而来。也就是说,我们可以先计算第三层的损失并对第二层的参数矩阵进行更新,再利用第三层的损失计算第二层的损失以及更新第一层的参数矩阵(至于为何可以这样进行,将在后面进行证明)。
于是,基于反向传播算法的梯度更新步骤如下:
其中,为正则化的参数。
参考吴恩达机器学习视频(4层神经网络时):
算法步骤:
在完成一次前向传播后,已知 J(Θ) ,也就是已知 (y^,y) 为了实现梯度下降:,我们需要求。
(1)解题思路:
δ 在很多书中翻译为‘误差’,但这与模型预测值与样本y之间的误差不是一个概念。实际上δ是一个微分值。我们定义。有了这个中间值设定,可以简化我们的计算,不用对每一层θ的偏导计算都从网络的输出层开始重新计算一遍。至于为什么将这个中间值设定在,大概是两个原因:1、这是离我们要求导的 θ(l) 最靠近的一个元素。2、你也可以把这个中间值设定在∂C∂a(l),但这样在推导后的公式表达上比较繁琐,不易直观理解。事实上没有这个中间值你也能完整表达整个网络的梯度求导,只不过那个公式充满了∑会让你看着头晕。
再进一步,如果我们找到与之间的关系,在反向传播中,我们刚好可以从输出层往输入层方向一层层的计算下去。
(2)寻找本层’误差’与下一层’误差’之间的关系
寻找与之间的关系 。
要寻找与之间的关系之前,就得明确一点是如何把‘误差’传给的。
先不说那么多,还是直接进行微分入手,已知:,其中,a = sigmod(z)
很简单吧。不过这里的微分都是直接对矩阵的操作,∇是更恰当的计算标识。从另一方面说,如果你把网络简化成1->1->1->1的结构,以上推导就直接成立了。
通过上面的公式我们可以看到,怎么感觉和正向传播差不多啊,可以看出来参数矩阵作用前向传播,而它的转置就能作用于梯度的反向传播。
(3)确定输出层’误差’的值
确定的值 。
完成了上面的计算流程,我们已经完成了反向求导计算工作的70%了。我们知道了与之间的关系,就能从输出层一层层的往输入层推。但是在输出层因为没有下一层节点,所以的值需要用另一种方式确定。回顾一下神经网络的损失函数:
在本文中都表达损失函数,考虑单条训练样本省略m,将输出值作为整体计算,即将表达为y,同时省略正则项可以表示为:
(4)最后一步
为什么求梯度时,要先对后一层进行计算,并利用其结果来求前一层的梯度?我们将针对如下例子进行推导证明:
第一层的参数为:
第二层的参数为:
(1)对第二层的参数求梯度
我们先来对第二层的参数求梯度:
交叉熵误差函数为:
其中 为实际值, 。
求导推导公式参考2.2:机器学习算法(一):逻辑回归模型(Logistic Regression, LR)_意念回复的博客-CSDN博客_逻辑回归模型
(2)对第一层的参数求梯度
先对中括号内的求导:
其中, = , = (a,一个神经元的激活值,即输出值)
故:
其中,
对比着3.2中的公式,我们可以看出,第 k 层的梯度可以根据第 k+1 层的损失来计算(上式是用第 2 层的损失来推导第 1 层的梯度)。
链式推导(以三层神经网络为例):
首先计算误差函数对,即的导数:
(1)对第二层参数求梯度
a = g(z)
(2)对第一层参数求梯度
没有1,因为不对输入层求误差。使用进行各参数更新时,不需要加负号。
使用该方法时,应先将真实输出经过softmax函数进行转换,再带入代价函数中进行计算。即:
softmax函数:
多分类交叉熵:
链式推导(以三层神经网络为例):
一、计算误差函数对求导的导数
当我们对分类的Loss进行改进的时候,我们要通过梯度下降,每次优化一个step大小的梯度,这个时候我们就要求Loss对每个权重矩阵的偏导,然后应用链式法则。那么这个过程的第一步,就是对softmax求导。
计算误差函数对未经过softmax函数转化的输出,即求导的导数,根据链式法则,可将其转化为误差函数对经过softmax函数转化后的输出求导的导数,再乘以经过softmax函数转化后的输出对求导的导数:
忽略样本数量,即:
其中,
(1)损失函数对softmax层求导:
我们对Si求导,其实是对Si中的求导,从公式中可以看出,由于每个Si的分母中都包含,因此对所有的Si都要求导。
其中,为第k个神经元的期望输出值,为第k个神经元经过softmax函数转化后的输出,即y_out。
(2)softmax层对输出层求导
分两种情况:
① q = k
② q ≠ k
(3)输出层对隐藏层输出神经元求导
此时我们分别求出了两个偏导数,将两个偏导数合并起来(相乘),因为softmax层对输出层的偏导数有两种情况,因此链式求导的结果也分 q = k 和 q ≠ k 两种情况,加以合并:
由于:
因此:
二、对第二层参数求梯度
三、对第一层参数求梯度
链式推导:
首先计算误差函数对,即的导数:
(1)对第二层参数求梯度
(2)对第一层参数求梯度
没有1,因为不对输入层求误差。使用进行各参数更新时,不需要加负号。
神经网络训练过程中每一次需要迭代的部分为:前向传播和计算损失、反向传播和权值更新。
(1)理论上,单隐层神经网络可以逼近任何连续函数(只要隐层的神经元个数足够)
(2)对于一些分类数据(比如CTR预估),3层神经网络效果优于2层神经网络,但如果把层数不断增加(4,5,6层),对最后的结果的帮助没有那么大的跳变。
(3)提升隐层数量或者隐层神经元个数,神经网络的“容量”会变大,空间表达能力会变强。
(4)过多的隐层和神经元结点会带来过拟合问题。
(5)不要试图降低神经网络参数量来减缓过拟合,用正则化或者dropout层。
(6)隐藏层一般为一层,如果为多层时,每一层的神经元个数相同。
注:在代码中对参数的初始化并不是使用0来初始化,还是在范围 [−ϵ,ϵ] 间随机初始化。对应代码为:
Theta = np.random.rand(nextUnit, Unit+1) * 2 * epsilon - epsilon
epsilon 即ϵ,nextUnit为隐藏层神经元的个数。
此处说的交叉熵,应该是将多分类问题看做多个二分类问题的交叉熵。
二分类中交叉熵损失函数:
此时,交叉熵损失函数一般用来代替均方差损失函数,并与sigmoid激活函数组合。
sigmoid激活函数表达式:
sigmoid激活函数表达式:
从图中可以看出,对于sigmoid函数,当 的取值越大或越小,函数曲线变得越平缓,意味着导数 越趋近于0。
以单个样本的一次梯度下降为例:
前两个公式分别是前向传播的线性和非线性部分,第三个公式是均方差损失函数,第四个公式是交叉熵损失函数。梯度下降的目的,直白地说:是减小真实值和预测值的距离,而损失函数用来度量真实值和预测值之间距离,所以梯度下降目的也就是减小损失函数的值。怎么减小损失函数的值呢?变量只有 和 ,所以我们要做的就是不断修改 和 的值以使损失函数越来越小。(这里例子只有一步,只修改一次)
和 的更新:
其中 表示学习率,用来控制步长,即向下走一步的长度
随机梯度下降:最优化方法一:梯度下降法_意念回复的博客-CSDN博客_梯度下降法求最优解
关键点来了,为什么用交叉熵而不是均方差呢?
均方差对参数的偏导:
交叉熵对参数的偏导:
注:为了简洁,以上公式中用 代替了 。
从以上公式可以看出:均方差对参数的偏导的结果都乘了sigmoid的导数 ,而之前看图发现sigmoid导数在其变量值很大或很小时趋近于0,所以偏导数很有可能接近于0。
由参数更新公式:参数=参数-学习率×损失函数对参数的偏导
可知,其变量值很大或很小时,即偏导很小时,参数更新速度会变得很慢,而当偏导接近于0时,参数几乎就不更新了。
反观交叉熵对参数的偏导就没有sigmoid导数,所以不存在这个问题。这就是选择交叉熵而不选择均方差的原因。
当输入向量X输入感知器时,第一次初始化权重向量W是随机组成的,也可以理解成我们任意设置了初始值,并和输入做点积运算,然后模型通过权重更新公式来计算新的权重值,更新后的权重值又接着和输入相互作用,如此迭代多次,得到最终的权重。
信号向前传播,权重的更新反向传播。
反向传播这个术语经常被误解为用于多层神经网络的整个学习算法。实际上,反向传播仅指用于计算梯度的方法,而另一种算法,例如随机梯度下降,使用该梯度来进行学习。此外,反向传播经常被误解为仅适用于多层神经网络,但是原则上它可以计算任何函数的导数(对于一些函数,正确的响应是报告函数的导数是未定义的)。
假设有如下三层网络,输入层、隐藏层、输出层,现有一组信号X输入网络,输入层和隐藏层的链接权重Winput−hidden和隐藏层与输出层之间的权重Whidden−ouput,我们随机初始化。为了清晰效果,我们仅标注了几个权重,第一个输入节点和中间隐藏层第一个节点之间的权重为,正如上图中的神经网络所示。同样,你可以看到输入的第二节点和隐藏层的第二节点之间的链接的权重为,隐藏层第三个节点和输出层第二个节点之间链接的权重为......,此处的下标与1.3中所定义的相反,使用 来表示隐藏层的第 3个神经元到输出层的第 2 个神经元之间链接的权重。
输入矩阵:
输入层和隐藏层之间的连接权重:
隐藏层和输出层之间的连接权重:
初始值定义好以后,开始计算输入到隐藏层的组合调节输入值。
此处的矩阵乘法计算出的答案如下:
让我们来整理一下网络的信号流动情况,作为第一层的输出,即第二层的输入已经正确求解,现在它准备进入隐藏层。
一进入隐藏层,我们就对的这些节点使用S激活函数,使其变得更加自然,并且我们把经过S函数(激活函数)处理后的这组输出信号命名为。
让我们再次可视化这些输入到第二层隐藏层的组合调节输入。现在信号已经向前流动到了第二层,下一步当然是计算第三层的输入信号(还未经过S函数的输入信号),计算的方法和前面一样,没有什么区别,不管我们的网络是几层,这种方法都适用。
于是,我们有:
现在,更新示意图展示我们的进展,从初始输入信号开始,一层层往前流动的前馈信号,最后得到了最终层的组合输入信号。
最后一步使用S函数得到最后一层的输出,用表示:
前馈信号的流动到此为止,任务完成!通过可视化的图形,前馈神经网络中信号流入方向,变化等情况我们用网络图最后形展示出来。
整个过程就是前馈的意思,信号一直向前流动,最后输出,中间任意层没有信号反回上一级网络。
下一步我们会将神经网络的输出值与训练样本中的输出值进行比较,计算出误差,并使用这个误差值来反向调节权重值。
上一步我们得到了前向传播的输出值为[0.726, 0.708],这个值与真实值[0.01,0.99]还存在一定差距,不过没关系,反向传播误差会帮助我们更新权值,缩小这些误差,让我们来实验一下。
因为总误差为:
由于我们的实验网络有两个输出,因此总误差为两个输出误差之和。
第一个误差:
第二个误差:
总误差:
对于隐藏层和输出层之间的权重来说,如果我们想知道对整体误差产生了多少影响,可以用总误差对求偏导,该偏导可以使用链式法则表示。
如图所示的反向传播示意图,并结合求导表达式,可以帮助我们更清楚的了解误差是怎么反向传播的。
下面我们对以上求导式子中的每个小式子分别求值:
(2)再来计算
(3)最后计算
所以:
我们取学习率η=0.5,利用公式
得到更新后的为:
因此,改变上述式子的变量可以更新等权重值。
计算输入层和隐藏层之间的权重和上面的方法一样,但使用误差对权重进行求导时,该误差应使用两个输出口的总误差,而不是一个输入口的误差。我们仍然用图形化的方式来展示:
如上图所示,对于输入层和隐藏层之间的权重来说,如果我们想知道对整体误差产生了多少影响,可以用总误差对求偏导,该偏导可以使用链式法则表示。
我们还是一个一个的计算上面的式子。
对于隐藏层的输出,它会接受来自两个输出传来的误差,所以
下面的为隐藏层和输出层的链接权重:
最后得到
= 0.08545775 − 0.0233198208 = 0.062137929
= 0.062137929 × 0.181879 × 0.9 = 0.010168071
我们取学习率η=0.5,利用公式
得到更新后的为:
= 0.9 - 0.5 × 0.010168071 = 0.89491596
同样的方法可以更新其他权重的值。这样我们就完成了误差反向传播算法的介绍,在实际训练中我们通过这种方法不停的迭代,直到总误差接近0为止,得到的最优权重保留下来,训练完成。
# -*- coding: utf-8 -*-
"""
mse:使用均方误差
ce:将多分类问题转化为多个二分类问题
soft:多分类交叉熵,最后一层输出后再使用softmax函数
该算法先将所有的样本进行一个前向传播和一次反向传播,进行权重更新后,再进行小批量进行权重更新。
每一次的输出均使用sigmoid函数。
(最终文件)
"""
import copy
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelBinarizer
mse_list = []
ce_list = []
soft_list = []
def sigmoid(x):
"""
激活函数
:param x: 激活函数输入值
:return:
"""
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
"""
激活函数求导
:param x: 激活函数输入值
:return:
"""
return x * (1 - x)
def mse_function(y, pred_y):
"""
计算均方误差
:param y: 期望的y
:param pred_y: 预测得到的y
"""
mse = (np.sum(pow((pred_y - y), 2)) / len(pred_y)) / 2
return mse
def ce_function(y, pred_y):
"""
转化为多个二分类问题时,计算交叉熵
:param y: 期望的y
:param pred_y: 预测得到的y
:return:
"""
cross_entropy = -np.sum(y * np.log(pred_y) + (1 - y) * np.log(1 - pred_y))
return cross_entropy
def soft_function(y, pred_y):
"""
使用softmax函数转化输出后,使用多分类计算交叉熵误差
:param y: 期望的y
:param pred_y: 预测得到的y
:return:
"""
pred_y = softmax(pred_y)
soft_y = -np.sum(y * np.log(pred_y))
return soft_y
def softmax(pred_y):
denominator = np.sum(np.exp(pred_y))
pred_y = np.exp(pred_y) / denominator
return pred_y
class NeuralNetwork:
def __init__(self, layer, times, alpha, epsilon):
"""
初始化神经网络
:param layer: 每一层的神经元个数
:param times: 循环迭代的次数
:param alpha: 迭代步长
"""
self.layer = layer
self.times = times
self.alpha = alpha
# 初始化隐藏层和输出层的权重
self.ce_weights = []
# (100, 65) (10, 101)
for tier in range(len(layer) - 1):
self.ce_weights.append(np.random.rand(
layer[tier + 1], layer[tier] + 1) * 2 * epsilon - epsilon)
# self.ce_weights.append(
# np.random.uniform(
# -epsilon, epsilon, (layer[tier + 1], layer[tier] + 1)))
self.mse_weights = copy.deepcopy(self.ce_weights)
self.soft_weights = copy.deepcopy(self.ce_weights)
def fit(self, feature, target, gamma, batch_num):
feature_x0 = np.ones((np.shape(feature)[0], 1))
feature_x = np.hstack((feature_x0, feature)) # 维度(1257, 65)
m = len(feature_x)
iters = 0
# 第一次前向传播和反向传播,使用全部样本,更新参数
mse_error = 0
ce_error = 0
soft_error = 0
mse_deltas_error_list = np.array(
[0 for _ in range(len(self.mse_weights))])
ce_deltas_error_list = np.array(
[0 for _ in range(len(self.ce_weights))])
soft_deltas_error_list = np.array(
[0 for _ in range(len(self.soft_weights))])
for i in range(m):
# 前向传播,得到每一层神经元的激活值,即输出,并得到第一次前向传播的mse和ce值
activators = self.forward_propagation(
feature_x[i], self.mse_weights)
mse = mse_function(target[i], activators[-1])
ce = ce_function(target[i], activators[-1])
soft = soft_function(target[i], activators[-1])
mse_error = mse_error + mse
ce_error = ce_error + ce
soft_error = soft_error + soft
# 反向传播
# 使用均方误差时的反向传播,得到deltas_error △
mse_deltas_error = self.mse_back_propagation(activators, target[i],
self.mse_weights)
mse_deltas_error_list = mse_deltas_error_list + mse_deltas_error
# 使用交叉熵误差时的反向传播,得到deltas_error △,并更新
ce_deltas_error = self.ce_back_propagation(
activators, target[i], self.ce_weights)
ce_deltas_error_list = ce_deltas_error_list + ce_deltas_error
# 使用soft和交叉熵的反向传播,得到deltas_error △
soft_deltas_error = self.soft_back_propagation(
activators, target[i], self.soft_weights)
soft_deltas_error_list = soft_deltas_error_list + soft_deltas_error
# if i % 100 == 0:
# print(i)
# 记录第一次前向传播后,权重未更新时,加上正则项后的mse、ce、使用soft和交叉熵的误差
mse_regu = []
ce_regu = []
soft_regu = []
for w in range(len(self.ce_weights)):
mse_weights_re = np.sum(np.power(self.mse_weights[w], 2))
mse_regu.append(mse_weights_re)
ce_weights_re = np.sum(np.power(self.ce_weights[w], 2))
ce_regu.append(ce_weights_re)
soft_weights_re = np.sum(np.power(self.soft_weights[w], 2))
soft_regu.append(soft_weights_re)
mse_regular = (mse_error + gamma * np.sum(mse_regu) / 2) / m
ce_regular = (ce_error + gamma * np.sum(ce_regu) / 2) / m
soft_regular = (soft_error + gamma * np.sum(soft_regu) / 2) / m
mse_list.append(mse_regular)
ce_list.append(ce_regular)
soft_list.append(soft_regular)
# 根据反向传播的结果,更新各层参数
mse_weights = self.update_parameters(
mse_deltas_error_list, self.mse_weights, m, gamma)
ce_weights = self.update_parameters(
ce_deltas_error_list, self.ce_weights, m, gamma)
soft_weights = self.update_parameters(
soft_deltas_error_list, self.soft_weights, m, gamma)
self.mse_weights = mse_weights
self.ce_weights = ce_weights
self.soft_weights = soft_weights
# 记录第一次反向传播,权重更新后,加上正则项后,所有样本的mse、ce、使用soft和交叉熵的误差
self.compute_error(feature_x, target, m, gamma)
# 小批量更新
print("batch----------------------------------------------------------")
while iters < self.times:
rand_index = np.random.randint(0, m, size=(1, batch_num))[0]
feature_batch = feature_x[rand_index]
target_batch = target[rand_index]
mse_error = 0
ce_error = 0
soft_error = 0
mse_deltas_error_list = np.array(
[0 for _ in range(len(self.mse_weights))])
ce_deltas_error_list = np.array(
[0 for _ in range(len(self.ce_weights))])
soft_deltas_error_list = np.array(
[0 for _ in range(len(self.soft_weights))])
for i in range(batch_num):
# 前向传播,得到每一层神经元的激活值,即输出,并得到前向传播的mse和ce值
mse_activators = self.forward_propagation(
feature_batch[i], self.mse_weights)
ce_activators = self.forward_propagation(
feature_batch[i], self.ce_weights)
soft_activators = self.forward_propagation(
feature_batch[i], self.soft_weights)
mse = mse_function(target[i], mse_activators[-1])
ce = ce_function(target[i], ce_activators[-1])
soft = soft_function(target[i], soft_activators[-1])
mse_error = mse_error + mse
ce_error = ce_error + ce
soft_error = soft_error + soft
# print(iters)
# print("mse_activators:", mse_activators)
# print("ce_activators:", ce_activators)
# print("soft_activators:", soft_activators)
# 反向传播
# 使用均方误差时的反向传播,得到deltas_error △,并更新
mse_deltas_error = self.mse_back_propagation(
mse_activators, target_batch[i], self.mse_weights)
mse_deltas_error_list = mse_deltas_error_list + \
mse_deltas_error
# 使用交叉熵误差时的反向传播,得到deltas_error △,并更新
ce_deltas_error = self.ce_back_propagation(
ce_activators, target_batch[i], self.ce_weights)
ce_deltas_error_list = ce_deltas_error_list + ce_deltas_error
# 使用soft和交叉熵的反向传播,得到deltas_error △,并更新
soft_deltas_error = self.soft_back_propagation(
soft_activators, target_batch[i], self.soft_weights)
soft_deltas_error_list = soft_deltas_error_list + \
soft_deltas_error
# 根据反向传播的结果,更新各层参数
mse_weights = self.update_parameters(
mse_deltas_error_list, self.mse_weights, batch_num, gamma)
ce_weights = self.update_parameters(
ce_deltas_error_list, self.ce_weights, batch_num, gamma)
soft_weights = self.update_parameters(
soft_deltas_error_list, self.soft_weights, batch_num, gamma)
self.mse_weights = mse_weights
self.ce_weights = ce_weights
self.soft_weights = soft_weights
# 记录此次反向传播,权重更新后,加上正则项后,所有样本的mse、ce、使用soft和交叉熵的误差
self.compute_error(feature_x, target, m, gamma)
iters += 1
if iters % 500 == 0:
print(iters)
return self.mse_weights, self.ce_weights, self.soft_weights
# 前向传播,得到每层神经元的输出
def forward_propagation(self, feature_one, for_weights):
activators = [feature_one.reshape(1, -1)] # 激活项,输入层的激活项即为X (1*65)
for forward_layer in range(len(for_weights)):
activator = sigmoid(
np.dot(activators[forward_layer], for_weights[forward_layer].T))
if forward_layer < len(for_weights) - 1:
activator = np.append(np.array([1]), activator) # 1*101
# activators:(1, 65) (1, 101) (1, 10)
activators.append(activator.reshape(1, -1))
return activators
# 使用均方误差函数进行反向传播
def mse_back_propagation(self, activators, target_one, back_weights):
# 反向计算该样本各层神经元误差delta
deltas_error = [0 for _ in range(len(back_weights))]
error = (target_one - activators[-1]) * \
sigmoid_derivative(activators[-1])
deltas = [error]
for j in range(len(back_weights) - 1, 0, -1):
delta = np.dot(back_weights[j].T, deltas[-1].T).T \
* sigmoid_derivative(activators[j])
deltas.append(delta)
deltas.reverse() # deltas:(1, 101) (1, 10)
# 计算deltas_error △
for j in range(len(back_weights)):
de_error = np.dot(deltas[j].reshape(-1, 1), activators[j])
if j < len(back_weights) - 1:
de_err = de_error[1:]
else:
de_err = de_error
deltas_error[j] = de_err
return deltas_error
# 使用交叉熵函数进行反向传播
def ce_back_propagation(self, activators, target_one, back_weights):
# 反向计算该样本各层神经元误差deltas
deltas_error = [0 for _ in range(len(back_weights))]
error = target_one - activators[-1]
deltas = [error]
for j in range(len(back_weights) - 1, 0, -1):
delta = np.dot(back_weights[j].T, deltas[-1].T).T \
* sigmoid_derivative(activators[j])
deltas.append(delta)
deltas.reverse() # deltas:(1, 101) (1, 10)
# 计算deltas_error △
for j in range(len(back_weights)):
de_error = np.dot(deltas[j].reshape(-1, 1), activators[j])
if j < len(back_weights) - 1:
de_err = de_error[1:]
else:
de_err = de_error
deltas_error[j] = de_err
return deltas_error
# 使用soft和交叉熵进行反向传播
def soft_back_propagation(self, activators, target_one, back_weights):
# 反向传播计算该样本各层神经元误差deltas
deltas_error = [0 for _ in range(len(back_weights))]
# print("activators[-1]:", activators[-1])
soft_activators = softmax(activators[-1])
# print("soft_activators: ", soft_activators)
error = (target_one - soft_activators) * sigmoid_derivative(
activators[-1])
deltas = [error]
for j in range(len(back_weights) - 1, 0, -1):
delta = np.dot(back_weights[j].T, deltas[-1].T).T \
* sigmoid_derivative(activators[j])
deltas.append(delta)
deltas.reverse() # deltas:(1, 101) (1, 10)
# 计算deltas_error △
for j in range(len(back_weights)):
de_error = np.dot(deltas[j].reshape(-1, 1), activators[j])
if j < len(back_weights) - 1:
de_err = de_error[1:]
else:
de_err = de_error
deltas_error[j] = de_err
return deltas_error
# 更新参数
def update_parameters(self, deltas_error_list, up_weights, data_num, gamma):
for ly in range(len(up_weights)):
# 除偏置神经元外,其余神经元加上正则项
deltas_error_list_regular = deltas_error_list[ly][:, 1:]
weights_regular = up_weights[ly][:, 1:]
d_part = deltas_error_list_regular / data_num \
+ gamma * weights_regular
# test_der = deltas_error_list[ly][:, 0]/data_num
der = np.hstack(
(((deltas_error_list[ly][:, 0].reshape(-1, 1)) / data_num),
d_part))
up_weights[ly] = up_weights[ly] + self.alpha * der
return up_weights
# 计算所有输入样本的误差和,加正则项
def compute_error(self, feature_all, target_all, data_num, gamma):
mse_error = 0
ce_error = 0
soft_error = 0
for i in range(data_num):
# 前向传播,得到每一层神经元的激活值,即输出,并计算误差
mse_activators = self.forward_propagation(
feature_all[i], self.mse_weights)
ce_activators = self.forward_propagation(
feature_all[i], self.ce_weights)
soft_activators = self.forward_propagation(
feature_all[i], self.soft_weights)
mse = mse_function(target_all[i], mse_activators[-1])
ce = ce_function(target_all[i], ce_activators[-1])
soft = soft_function(target_all[i], soft_activators[-1])
mse_error = mse_error + mse
ce_error = ce_error + ce
soft_error = soft_error + soft
# 计算加上正则项后的mse、ce、使用soft和交叉熵的误差
mse_regu = []
ce_regu = []
soft_regu = []
for w in range(len(self.ce_weights)):
mse_weights_re = np.sum(np.power(self.mse_weights[w], 2))
mse_regu.append(mse_weights_re)
ce_weights_re = np.sum(np.power(self.ce_weights[w], 2))
ce_regu.append(ce_weights_re)
soft_weights_re = np.sum(np.power(self.soft_weights[w], 2))
soft_regu.append(soft_weights_re)
mse_regular = (mse_error + gamma * np.sum(mse_regu) / 2) / data_num
ce_regular = (ce_error + gamma * np.sum(ce_regu) / 2) / data_num
soft_regular = (soft_error + gamma * np.sum(soft_regu) / 2) / data_num
mse_list.append(mse_regular)
ce_list.append(ce_regular)
soft_list.append(soft_regular)
# 进行预测
def predict(feature, target, target_lb, mse_w, ce_w, soft_w, gamma):
feature_x0 = np.ones((np.shape(feature)[0], 1))
feature_x = np.hstack((feature_x0, feature)) # 维度(540, 65)
data_num = len(feature_x)
mse_error = 0
ce_error = 0
soft_error = 0
mse_predict_value_list = []
ce_predict_value_list = []
soft_predict_value_list = []
for i in range(data_num):
mse_activators = nn.forward_propagation(feature_x[i], mse_w)
ce_activators = nn.forward_propagation(feature_x[i], ce_w)
soft_activators = nn.forward_propagation(feature_x[i], soft_w)
# 计算数字形式的预测输出,用于之后计算准确率
mse_pred = mse_activators[-1]
ce_pred = ce_activators[-1]
soft_pred = soft_activators[-1]
mse_predict_value = np.argmax(mse_pred)
ce_index_value = np.argmax(ce_pred)
soft_index_value = np.argmax(soft_pred)
mse_predict_value_list.append(mse_predict_value)
ce_predict_value_list.append(ce_index_value)
soft_predict_value_list.append(soft_index_value)
# 计算均方误差和交叉熵
mse = mse_function(target_lb[i], mse_activators[-1])
ce = ce_function(target_lb[i], ce_activators[-1])
soft = soft_function(target_lb[i], soft_activators[-1])
mse_error = mse_error + mse
ce_error = ce_error + ce
soft_error = soft_error + soft
# 计算加上正则项后的mse、ce、使用soft和交叉熵的误差
mse_regu = []
ce_regu = []
soft_regu = []
for w in range(len(ce_weights)):
mse_weights_re = np.sum(np.power(mse_w[w], 2))
mse_regu.append(mse_weights_re)
ce_weights_re = np.sum(np.power(ce_w[w], 2))
ce_regu.append(ce_weights_re)
soft_weights_re = np.sum(np.power(soft_w[w], 2))
soft_regu.append(soft_weights_re)
mse_regular = (mse_error + gamma * np.sum(mse_regu) / 2) / data_num
ce_regular = (ce_error + gamma * np.sum(ce_regu) / 2) / data_num
soft_regular = (soft_error + gamma * np.sum(soft_regu) / 2) / data_num
# 计算准确率
mse_judge = np.array(mse_predict_value_list) == np.array(target)
mse_prec = np.sum(mse_judge) / len(mse_judge)
ce_judge = np.array(ce_predict_value_list) == np.array(target)
ce_prec = np.sum(ce_judge) / len(ce_judge)
soft_judge = np.array(soft_predict_value_list) == np.array(target)
soft_prec = np.sum(soft_judge) / len(soft_judge)
print(mse_judge, ce_judge, soft_prec, sep="\n")
return mse_regular, ce_regular, soft_regular, \
mse_prec, ce_prec, soft_prec
def plot(mse, ce, soft):
# 设置matplotlib 支持中文显示
mpl.rcParams['font.family'] = 'SimHei' # 设置字体为黑体
mpl.rcParams['axes.unicode_minus'] = False # 设置在中文字体是能够正常显示负号(“-”)
# plt.figure(figsize=(20, 20))
# plt.plot(mse_re, lw=1, c='red', marker='s', ms=4, label="均方误差")
# plt.plot(ce_re, lw=1, c='green', marker='o', ms=4, label="二分类交叉熵")
# plt.plot(soft_re, lw=1, c='yellow', marker='^', ms=4, label="多分类交叉熵")
plt.figure()
# 绘制误差值
mse_re_part = [mse[i - 1] for i in range(1, len(mse)) if i % 50 == 0]
mse_re_part.insert(0, mse[0])
ce_re_part = [ce[i - 1] for i in range(1, len(ce)) if i % 50 == 0]
ce_re_part.insert(0, ce[0])
soft_re_part = [soft[i - 1]
for i in range(1, len(soft)) if i % 50 == 0]
soft_re_part.insert(0, soft[0])
x_data = [i for i in range(len(soft)) if i % 50 == 0]
plt.plot(x_data, mse_re_part, lw=1, c='red',
marker='s', ms=4, label="均方误差")
plt.plot(x_data, ce_re_part, lw=1, c='green',
marker='o', ms=4, label="二分类交叉熵")
plt.plot(x_data, soft_re_part, lw=1, c='yellow',
marker='^', ms=4, label="多分类交叉熵")
plt.xlabel("迭代次数")
plt.ylabel("误差")
plt.title("手写字预测-神经网络")
plt.legend()
plt.show()
if __name__ == "__main__":
digits = datasets.load_digits()
range_value = np.max(digits.data) - np.min(digits.data)
data = (digits.data - np.min(digits.data)) / range_value
train_feature, test_feature, train_target, test_target = train_test_split(
data, digits.target, test_size=0.3)
train_target_lb = LabelBinarizer().fit_transform(train_target)
test_target_lb = LabelBinarizer().fit_transform(test_target)
layer = [64, 100, 10]
times = 8000 # 迭代次数
alphas = 0.02 # 迭代步长
epsilon = 1 # 初始化权重的范围[-epsilon, epsilon]
nn = NeuralNetwork(layer, times, alphas, epsilon) # 初始化一个三层的神经网络
gamma = 0.0001 # 正则化系数
batch_num = 20
mse_weights, ce_weights, soft_weights = nn.fit(
train_feature, train_target_lb, gamma, batch_num)
# print(mse_list, ce_list, soft_list, sep="\n")
mse_re, ce_re, soft_re, mse_precision, ce_precision, soft_precision = \
predict(test_feature, test_target, test_target_lb,
mse_weights, ce_weights, soft_weights, gamma)
print("mse_re:{0}".format(mse_re),
"ce_re:{0}".format(ce_re),
"soft_re:{0}".format(soft_re), sep="\n")
print("mse_precision:{0}".format(mse_precision),
"ce_precision:{0}".format(ce_precision),
"soft_precision:{0}".format(soft_precision), sep="\n")
plot(mse_list, ce_list, soft_list)
交叉熵:https://www.jianshu.com/p/8a0ad237b0ed
从零开始机器学习-16 初探神经网络(Neural Network) - 简书
神经网络的理解与实现 - EEEEEcho - 博客园
神经网络和深度学习之——误差反向传播算法 - 帅虫哥 - 博客园
深度学习笔记1:利用numpy从零搭建一个神经网络 - 知乎