一篇读完 Python神经网络编程 make your own neuralnetwork

Python 神经网络编程 

 make your own neural network

非常适合入门神经网络编程的一本书,主要是三部分: 介绍神经网络的基本原理和知识;用Python写一个神经网络训练识别手写数字;对识别手写数字的程序的一些优化。

 

神经网络如何工作

神经网络的大的概括就是:给定输入,经过一些处理,得到输出。当不知道具体的运算处理方式时,尝试使用模型来估计其运作方式,在这个过程中可以基于模型输出和已知真实实例之间的比较来得到误差、调整参数

 

常见的神经网络模型包括分类器和预测器。通俗而言,分类器是将已有数据分开;预测是根据给定输入,给出预测的输出。本质上没有太大差别。在分类过程中其实就是要找到线分开各组数据,关键就是确定这条线,也就是确定斜率。在书中给的直线分类器的例子中,就是随机一个斜率,然后用一组组正确的数据来逐步修正这个斜率:

delt A = E/x,其中A是斜率。但是这样直线的斜率只会取决于最后一个样本而不会顾及到前面的样本。为了改进,每次只要变化delt A 的一部分,这也就是学习率的由来。调节学习率,单一的训练样本就不能主导整个学习过程,也可以减少噪声的影响。复杂的分类器也是这样的思想。

 

神经网络,正如名字所示,灵感来自动物的神经元细胞,毕竟大脑真的很神奇。神经元的结构包括树突和轴突,电信号从一端沿着轴突再传到树突,这样就从一个神经元传到另一个神经元。简单说,生物的神经元也是接受电信号输入,再输出另一个电信号。不过生物神经元与简单的线性函数不一样,不能简单的对输入做出反应。可以这样认为,在产生输出之前,输入必须达到一个阈值。直观上,神经元不希望传递微小的噪声信号,而只传递有意识的明显的信号。 我们用激活函数来表达这种方式。

 

下图所示为S函数,也就是sigmoid函数,比较平滑,更自然更接近现实。

 

S函数简单,并且实际上非常常见。S函数也称为逻辑函数:

 

 

继续考虑对神经元进行建模。生物的神经元是可以接受许多输入的,而不仅仅是一个输入。对这些输入,我们只需要 相加,得到总和,作为S函数的输入,然后输出结果。这就是神经元的工作机制。 如果组合信号不够强大,那么S函数的效果是抑制输出信号;足够大时则激发神经元。生物神经元中每个神经元都接受来自之前的多个神经元的输入,也可能同时提供信号给多个神经元。

 

那么模仿时,可以构建多层神经元,每层神经元都与其前后的神经元互相连接。如图:

 

可以不断调整节点之间的连接强度。之所以没有弄成其他奇怪的构造而采用完全的连接,是因为这样的连接方式可以相对容易的编码成计算机指令;神经网络的学习过程将会弱化那些实际上不需要的连接。

 

接下来书中用2*2 3*3的简单例子,给了一些数据,来实际的体验神经网络的计算过程。

 

随机一些权重,输入*权重之后求和,得到下一层每个节点的输入;经过激活函数,得到该层的输出;然后继续向下一层传播,直到输出。

 

这个过程可以用矩阵简洁方便的计算。比如2*2的神经网络的计算过程可以表示为:

 

三层的也是如此。

从第一层(输入层)到第二层(隐藏层):

X是结果,W是权重矩阵,I是第一层的输入;

然后经过激活函数sigmoid得到输出:

 

然后继续向下一层传播:

在经过一层sigmoid得到最后的输出

Winput_hidden的维度是 hidden_layer * input_layer;

Whidden_output的维度是output_layer*hidden_layer

记住是刚好相反的,行数是下一层的节点数目,列数是本层的节点数

 

 

以上是整个神经网络从接受输入,然后向前传播的过程。

 

那么如何进行学习呢?仍然是使用误差值,也就是前向传过来得到的答案与所知正确答案之间的差值。

那么如何根据误差来更显链接的权重呢?是将误差按链接的权重分配,反向传播回来,进行修正。比如误差是4,有两条链接(0.1 0.3)指向这个节点,那么分别传回去的误差就分别是1和3.

 

也就是说在向前传播数据和向后传播误差的过程中,都要用到权重。将误差从输出向后传播到网络中的方法就叫做反向传播

 

推广到多个节点的误差反向传播时,每个节点的误差都按比例向后传播,然后在上一层中重新组合(指向一个节点的所有链接传回来的误差的和就是传到这个节点的误差)。依据同样的方式就可以继续向上一层传播。用图来阐述更加清楚:

 

 

总的来说,神经网络是根据调整链接权重进行学习,这种方法由误差主导。在输出节点中的误差等于所需值与实际值间的差;而内部节点的误差是按照链接权重来分配误差,然后在内部节点中重组这些误差。

 

同样用矩阵来进行计算。严格按照上面的思路将是这样的:

但是这样并不好处理。观察上图,最重要的其实是wij*e,分子越大,分到的权重就应该越大,而这些分数的分母是归一化的分母,忽略分母之后,比例仍然在。

于是:

就可以写成:

注意这里,是原来的链接矩阵的转置!!

 

 

下面就是一个重要问题,到底如何更新权重。为什么要反向传播误差呢?因为我们要用误差来更新权重。神经网络要处理的问题往往过于复杂,无法像简单的线性分类器一样用数学的方式求得参数,暴力的解决不实际,所以最后提出的是梯度下降(gradient descent)的方法。

 

书中对梯度下降的比喻是摸黑下山,打着手电筒只能看到最近的一小片地方,你就选择看起来是下坡的方向走。逐步逐步走,不需要知道整个山的地貌,也可以下山。

进一步将其推广到一个复杂的函数,梯度下降的思想在于,我们不需要知道整个函数的面貌,也可以一点点朝着最低点前进。

如果将复杂的函数换成神经网络的误差函数,下山找到最小值就是要最小化误差,这样才可以改进神经网络。这就要用到上面的梯度下降算法了。

 

可以用二维的函数帮助理解:

随机一个起点,斜率为负的话向右走(x增大),斜率为正的话向左走(x减小)——往相反的方向增加x的值,这样可以慢慢的向最小值靠近,直到达到附近的位置。

在接近真正的最小值时,斜率会变小,需要调节步长比较小,才不会在最小值附近来回震荡。

 

当函数有很多参数的时候,这种方法才真正显出优势。

 

不过这种方法也有缺点,可能会终止在某个局部的最小值。为了避免这种情况,可以进行多次随机的初始化操作。

 

总之,梯度下降算法是求解函数最小值的一种很好的办法,当函数非常复杂困难,并且不能轻易使用数学代数求解时,这种方法很实用;当函数有很多参数时,这种方法依然适用,还可以容忍不完善数据,偶尔走错一步也不影响大局。

 

 

神经网络本身的输出函数不是一个误差函数,但由于误差是目标训练值与实际输出值之间的差值,因此可以很容易的把输出函数变成误差函数。

 

直接相减可能会出现正负误差相抵消的问题;而绝对值函数的斜率在最小值附近不连续,会让梯度下降法在v型山谷附近跳来跳去。因此最终选的是差的平方:斜率好计算,误差函数平滑连续,且越接近最小值,梯度越小。

 

确定了误差函数后,需要计算出误差函数相对于权重的斜率。也就是神经网络的误差E对网络链接权重wij导数。

右边只是将E表达了出来,t是真实值,o是神经网络的输出,所有节点的误差的平方和。

但是注意到,在节点n的输出on只取决于连接到这个节点的链接,也就是节点k的输出ok只取决于权重wjk,即连接到k的链接。换句话说,节点k的输出不依赖链接wjb(wjb表示由前一层第j个节点链接到该层第b个节点的权重)。也就是说,对wjk而言,我们只需要考虑节点Ok。

所以上式可以改成:

结合链式法则:

而前面那个就是个二次函数对ok的求导。

将Ok的表达式列出来,就是sigmoid函数之后得到的输出

其中oj是上一层的输出,结合了这之间的链接矩阵后,在本层进行组合(即求和),然后过一个激活函数。对sigmoid函数进行求导,可以得到:

所以继续之前的求导过程:

可以去掉前面的2,因为我们只对误差函数的方向感兴趣(想想那个二维的函数)。

所以我们一直努力要得到的答案就是:

这是从输出层到隐藏层的的误差的斜率。

套进去我们可以得到从隐藏层到输入层之间的斜率:

oi其实就是输入的值。

 

根据相反方向来修改链接矩阵:

同样的,我们要转化成矩阵的运算:

即:

 

wjk是连接当前层节点j与下一层节点k的链接矩阵。ɑ是学习率,Ok是下一层节点的值,最后一项OjT 是上一层节点的输出的转置。

这是正确的点乘顺序。

 

 

以上是神经网络的原理。接下来有关准备数据要知道的事情。

观察sigmoid函数:

其输出值在0-1之间,而x过大时,激活函数会变得很平缓,也就是斜率很小,这样不利于学习新的权重。

观察上面计算更新权重值的表达式,权重的更新取决于激活函数的梯度,梯度太小限制神经网络的学习能力,也就是所谓的饱和神经网络。

因此我们不应该让输入值太大;而计算机处理太小的信号时,可能会丧失精度。因此也不能过小。

 

好的建议是调整输入值在0.0-1.0之间,而输入0会使输出也是0(oj),权重更新值也会变为0(上面那个复杂的式子),所以也要避开0;因此最好加上一个小的偏移。

 

因为我们的激活函数不能产生大于1的值,因此将训练目标值设的比较大是不合理的。而且逻辑函数的输出也无法达到1,只能是接近1;如果目标设置的过大,训练网络将会驱使更大的权重,以获得越来越大的输出,这将使得网络饱和。因此,我们应当调整输出值符合激活函数的输出范围,一般在0.0-1.0,由于0.0和 1.0也不能,也可以使用0.01-0.99.

 

除了输入输出,同样的道理也适用于初始权重的设置。同样要避免大的权重使得网络饱和,可以用-1.0~1.0之间随机均匀的选择。另外,如果很多信号进入一个节点,并且这些信号已经表现的不错了,那么对这些信号进行组合并应用激活函数时,应保持这些表现良好的信号。可以在一个节点传入链接数量平方根倒数的大致范围内随机采样,初始化权重。

直观上理解,如果一个节点传入的链接越多,就有越多的信号被叠加在一起,因此如果链接越多,而减小链接的范围是有意义的。实际上就是从均值为0、保准方差等于节点传入链接数量的平方根倒数的正态分布中进行采样。

另外,不管你怎么做,禁止将权重设置为相同的恒定值,特别是不要设置为0.这样,网络中每个节点都将收到相同的信号值,输出也是一样的,反向传播误差时误差也是平均分的,这将导致同等量的权重更新,又会出现相同的权重。而0 的话,输入信号归0,取决于输入信号的权重更新函数也因此为0.完全丧失更新权重的能力。

Python写一个神经网络训练识别手写数字

具体参考github上的代码:

https://github.com/JoJoJun/MNTST_Record_HandWritten_Numbers

主要是构建了一个神经网络类:neuralNetwok

这个类有初始化函数__init__,设定输入层节点、隐藏层节点和输出层节点数,设定学习率,设定激活函数,用随机值初始化输入层-隐藏层和隐藏层-输出层间的链接矩阵。

 

查询部分(query),是输入输入值,用训练好的模型(权重参数),计算得到输出值,计算过程就是前面神经网络前向传播的过程,要用到pyhon库中numpy.dot运算,以及激活函数。

 

训练部分(train)这是整个神经网络类的核心部分。输入是输入值和目标值。计算前项传播的部分和查询部分是相同的。得到输出后,与真实的目标值相减得到误差E,然后用第一部分推出的那个很长的式子来计算delt wij并更新权重。

整个神经网络类的结构就是如此,想要使用这个类,就在main函数中,实例化这个类,然后准备训练数据和测试数据,用一定的迭代次数来训练模型,之后用训练过的这些参数,调用query输入测试数据就可以进行测试了。

 

这里对原始的输入数据的处理是,将,分隔的像素值分开,处理成合适的输入范围,目标值则是一个vector,正确的数字大,其他的数字接近0.

测试数据时对输出用argmax选出得分最高的那个,就是神经网络判断的数字。

优化和尝试

可以通过改变学习率、神经网络各层节点数、修改epoch次数、多次随机初始化等方式来找到更合适的参数。

 

这部分还指出了自己手写数字,然后转化成同样的28*28的像素,用自己训练得到的模型进行识别。

由于数据集有限,可以通过适当旋转已有的图片来得到新的数据。

猜你喜欢

转载自blog.csdn.net/BeforeEasy/article/details/83932623