去年Alaph GO击败李世石九段,社会掀起了机器学习技术讨论的热潮,不过很多人对机器学习并不了解,本文借由手写数字识别问题阐述了机器学习原理,并给出用Python 2.7 编写的源码,帮助大家搞清楚什么是机器学习。不过本文介绍的只是机器学习的核心思想,离工程应用还有一定距离。
本文翻译自Michael Nielsen的著作《Neural Networks and Deep Learning》感兴趣的同学可以直接看原版。(翻译时略作删减)
以下是原文
人类视觉系统是世界奇迹之一。考虑到以下手写数字系列:
很多人可以毫不费力地直接识别出这是504192。在 在我们大脑的每个半球,人类有一个主视觉皮层,也称为V1,包含1.4亿个神经元,在众多神经元之间有数百亿的连接。 然而,人类视觉系统不仅仅包含V1,还涉及一系列视觉皮层 :V2,V3,V4和V5 ,这些视觉皮层逐渐进行更复杂的图像处理。 我们的大脑进过数亿年进化而来的超级计算机,进化的结果是我们的大脑可以精准的理解我们身边的世界。 识别手写数字并不容易。 然而人类却非常擅长识别眼睛看到的事物并且,几乎所有的识别工作都是无意识完成的。 因此,我们通常不会察觉我们的视觉系统解决的问题有多难。
如果尝试用计算机程序去完成手写数字识别问题是非常困难的。举个简单的例子——识别数字“9”,我们人脑会意识到在9的顶部有一个循环,在右下方有一个垂直的笔画,然而用计算机语言却不好实现这样的分析逻辑。当你试图枚举这些规则,使之精确的时候,你很快就会迷失在一个无数例外和边界条件的混乱之中。
那么神经网络会怎么做呢?神经网络会用不同的方式处理该问题——使用大量标识好的手写数字作为训练集,然后开发一个可以从这些训练数据中学习而来的系统。换句话说,神经网络使用训练数据自己推断识别手写数字的规则。此外,通过增加训练示例的数量,网络可以更多地了解手写,从而提高其准确性。 因此,虽然我只展示了100个训练数字,也许我们可以通过使用数千甚至数百万或数十亿的训练样本来构建一个更好的手写识别器。
在本文中,我们将编写一个计算机程序,实现一个学习识别手写数字的神经网络。 该程序只有74行,并且不使用特殊的神经网络库。 但这个短程序可以识别数字的精度超过96%,无需人为干预。
为了更好的理解神经网络的原理,在介绍手写数字识别神经网络之前,我会介绍两种重要的人工神经元类型(the perceptron 和 the sigmoid neuron),以及用于神经网络学习标准学习算法——随机梯度差分(stochastic gradient descent)
Perceptron
为了解释什么是神经网络,首先介绍第一类人工神经元:Perceptron。感知器需要几个二进制输入,x1,x2,...并产生一个二进制输出:
在上图中,该Perceptron有三个输入:x1,x2,x3。一般来说,它可以有更多或更少的输入。Perceptron的输出(0或者1)由各输入的加权和是否小于或者大于每个阈值来确定。阈值和权值一样是一个Perceptron的参数。
式中,xj代表输入变量,wj表示相应输入变量的权值。
这是基本的数学模型。 你可以想象Perceptron是一个通过权衡输入做出决策的装置。 让我举个例子。 这不是一个很现实的例子,但它很容易理解,假设周末即将到来,你已经听说你的城市将会有一个奶酪节。 你喜欢奶酪,并试图决定是否去参加节日。 你可以通过权衡三个因素来做出决定:
- 当天的天气是否好?
- 你的男朋友或者女朋友是否想陪同你?
- 因为你没有车,你想知道奶酪节附近是否有公交站?
我们可以通过相应的二进制变量x1,x2和x3表示这三个因子。 例如,如果天气好,x1 = 1,如果天气不好,x1 = 0。 类似地,如果你的男朋友或女朋友想要去,x2 = 1,如果不是,x2 = 0。 相同的规则也适用于变量x3和是否有公交站。
假设你非常喜欢奶酪,所以你很想去参加奶酪节,不管你的男女朋友是否感兴趣或者是否难道到达奶酪节。但是坏天气会让奶酪节扫兴,所以你不想在坏天气的时候参加奶酪节。这时你可以用Perceptron模拟这种决策行为。一种方法是为天气选择权重w1 = 6,对于其他条件,分别为w2 = 2,w3 = 2。 w1的值越大,表示天气对你来说很重要,远远超过你的男朋友或女朋友是否陪同你一起去,以及附近是否有公交站。 最后,假设您为Perceptron选择阈值5。 Perceptron通过计算输入和权值乘机和并同阈值进行比较,每当天气好时输出1,而每当天气坏时输出0。 这个Perceptron的输出同我们的假设相符,即天气坏的时候不过男女朋友是否陪同或者是否有公交站,都不会去奶酪节。
通过改变权重和阈值,我们可以得到不同的决策模型。 例如,假设我们选择了一个阈值3.然后Perceptron会决定你应该在天气好的时候或者同时男女朋友同意陪同以及附近有公交站同时成立的时候前往奶酪节。 换句话说,这将是一个不同的决策模式。 降低门槛意味着你更愿意去参加奶酪节。
以上介绍的Perceptron显然并不是人类决策的完整模型。但是这个例子说明了Perceptron如果权衡不同类型的输入最终做出决定。并且说明一个复杂的Perceptron网络可以做出相当微妙的决定:
在上图的网络中,我们称第一列Perceptron为第一层Perceptron,第一列Perceptron通过权值同输入相乘做了三个简单的决定。第二层中的Perceptron通过权衡来自第一层决策的结果来做出决定。以这种方式第二层中Perceptron可以在比第一层中Perceptron更复杂更抽象的水平上做出决定。并且甚至可以通过第三层中的Perceptron来做出更复杂的决定。以这种方式,Perceptron的多层网络可以参与复杂的决策。
我们可以用一种更简单的方式表示式1,首先我们可以把权值同输入的乘机和用点乘的方式表示,并且可以把阈值移到等式的左边这样就可以写成下面的样子。使用b表示阈值的负数,同时我们称b为偏差(bias)。
你可以认为偏差为衡量激活Perceptron难易程度的标准。对于一个拥有很大偏差值的Perceptron,它是很容易激活的,对于一个拥有绝对值很大的负偏差值来说,激活Perceptron变成一个艰难的任务。
同时,在本文以下的内容中,我们会将神经网络的输入看做为一个没有输入只有输出的特殊Perceptron。
Sigmoid Neurons
学习算法听起来很棒。 但是我们如何能为神经网络设计这样的算法呢? 假设我们有一个Perceptron网络,我们想用它来学习解决一些问题。 例如,网络的输入可以是手写数字扫描得来的数字图像。 我们希望网络学习权值和偏差,以便神经网络可以输出对手写数字正确的分类。 那么学习行为到底是怎样进行的呢,假设我们在网络中的一些权值(或偏差)做一个小的改变,这些微小的改动会导致网络的输出产生一个微小的相应变化。 正如我们将看到的,这个属性将使学习成为可能。如下图所示:
如果在权值或者偏差上的微小改动会告知网络输出产生微小改动成立的话,我们就可以运用这个事实来修改权值和偏差,让我们的网络以我们想要的方式变得更好。
问题是,以上的描述不会发生在Perceptron网络中,因为权值或者偏差的微小改动会导致单个Perceptron输出的翻转(例如从0到1或者从1到0)。这样的翻转导致网络中其他Perceptron的行为变得不可控。所以,虽然你的“9”现在可以正确分类,但是某个微小变动导致对其他数字的分类行为变得不可控制。这使得不可能通过逐渐修改权重和偏差最终使网络输出朝着我们期望的方式变化。
不过我们可以通过以下方式解决这个问题——使用另一种人工神经元Sigmoid Neurons。Sigmoid Neurons类似于Perceptron,但是它的权重和偏差出现微小变动的时候,神经元输出的变动也是微小的。这是使用Sigmoid Neurons的神经网络可以实现学习行为的关键。
一个Sigmoid Neurons可以用下图表示,同Perceptron并没有什么不同。
像Perceptron一样,Sigmoid Neurons同样接受输入,但是这些输入值不再是离散的0或者1,输入值可以使用0到1之间的任何值,比如说0.638。同Perceptron一样,Sigmoid Neurons每个输入都有相应的权值,w1,w2,以及偏差b。但是Sigmoid Neurons的输出不再是0或者1这样的离散值了,它变成了σ(w⋅x+ b),σ的定义如下:
如果我们把神经元的输入写进上面的表达式:
其实上面的表达式并不难理解,如果σ函数的变量是一个极大的值,那么σ函数趋向于1,相反的话,σ函数趋向于0。σ函数的曲线如下图所示。
而代表Perceptron的阶梯函数如下图所示。
实际上,如果σ函数的形式如果是阶梯函数的话,那么Sigmoid Neurons就是一个Perceptron——输出是1还是0取决于输入w⋅x+b的正负。通过使用实际的σ函数,我们可以得到平滑的决策函数曲线。事实上,σ函数的平滑性是关键。σ函数的平滑性决定了权值的微分Δwj和偏差的微分将在神经元输出中产生小变化Δoutput。通过微积分我们可以得到Δoutput可近似于下式:
上式告诉我们,Sigmoid Neurons的输出变化Δoutput是权值变化Δwj和偏差变化Δb的线性函数,这种线性使得容易选择权值和偏差中的小变化以实现输出中期望的小变化。虽然Sigmoid Neurons具有许多Perceptron相同的定性行为,但是它更容易找到如何通过改变权值偏差从而改变神经网络输出的方法。
神经网络的架构
为了实现识别手写数字的任务,在了解识别手写数字神经网络实现原理之前,我们先介绍几个概念。
如前所述,该网络中最左边的神经元层称为输入层,输入层内的神经元称为输入神经元。 最右边或者叫输出层包含输出神经元,还可以叫做单个输出神经元。 中间层被称为隐藏层,因为该层中的神经元既不是输入也不是输出。 上面的网络只有一个隐藏层,但一些网络有多个隐藏层。 例如,以下四层网络有两个隐藏层:
网络中的输入和输出层的设计通常是比较简单的。 例如,假设我们试图识别手写数字图像是否为“9”。 设计网络的自然方式是将图像像素的强度编码到输入神经元中。 如果图像是64乘64灰度图像,则我们将具有4,096 = 64×64个输入神经元,其强度在0和1之间适当地缩放。输出层将仅包含单个神经元,当输出值小于0.5时表示该图像不是9,如果输出值大于0.5时,则表示该图像为9。
虽然神经网络的输入和输出层的设计通常是直接的,但是隐藏层的设计则非常具有技术性。特别的,目前隐藏层的设计并没有一些可以遵循的基本原则。 然而,神经网络研究人员已经为隐藏层开发了许多设计启发式,这有助于人们从他们的网络中获得他们想要的行为。 例如,这种启发法可以用于帮助确定如何根据训练网络所需的时间折衷隐藏层的数量。
如果设计一个可以识别手写数字的简单神经网络
我们已经定义了神经网络,让我们回到手写识别问题。 我们可以把识别手写数字的问题分成两个子问题。 首先,我们想要一种将包含许多数字的图像分割成单独图像序列的方法,每个图像包含一个数字。 例如,我们想将下面的图像分割成6个独立的图像。
我们人类可以轻松解决这个问题,但是这对计算机程序来说就是挑战性的。一旦我们成功的分割出单独的数字,我们的程序就可以来对每个单独数字进行分类了。例如,我们的程序就可以识别出下面是个5。
我们将专注于编写一个程序来解决第二个问题,即解决独立手写数字图像的识别问题。因为一旦解决了独立手写数字图像识别问题,数字图像分割问题就不难解决了。一种方法是可以用多种图像分割方法去分割图像,然后用手写数字图像识别算法对每个分割出来的图像打分,如果某个分割出来的图像得分比较高,说明分割出来的图像是正确的,如果得分很低,则说明分割不正确。这个方法以及不同的分割算法的选择,可以相当好的解决分割问题。这里我们将专注于开发一个神经网络。
我们使用一个三层神经网络来解决数字识别问题。第一层神经元输入手写数字图像每个像素的编码。由于我们使用的训练集中手写数字图像是28*28像素图片,所以我们的输入层包含728=28*28个神经元。为了简单起见,我省略了下面图像中大部分输入神经元。输入神经元输入的值是图像的灰度值,0.0表示白色,1.0表示黑色,在两个值中间的值表示灰色阴影。
网络的第二层是隐藏层,我们使用n表示隐藏层中神经元的数量,在该神经网络中,我们使用15个隐藏层神经元。
网络的输出层包含10个神经元,如果第一个神经元被激发,则它的输出output≈1,则表示神经网络认为输入的数字为0。如果第二个输出神经元被激发,则我们认为神经网络的输入为1,以此类推。
你可能想知道为什么我们使用10个输出神经元。毕竟,神经网络的目标是告诉我们输入是什么数字,一个看似自然的方法是使用4个输出神经元,将每个神经元视为一个二进制位,二进制位取值取决于输出值是靠近0还是靠近1。四个神经元已经足以编码10答案。因为2的四次方为16,而我们分类的结果个数只有10个。然而最终证明,对于这个特定问题,具有10个输出神经元的神经网络比具有4个输出神经元的神经网络可以更好的书别数字。但是这也只是对于这个特定问题,并不是说10个输出神经元的结果要好于4个输出神经元。
现在让我们来讨论下,神经网络为什么可以工作。首先考虑使用10个输出神经元的情况。其实每个输出神经元得到的结论是加权隐藏层输出的结果。我们可以这样认为:隐藏层中第一个神经元检测是否存在如下图像:
它可以通过对与图像重叠的输入像素进行大量加权,并且只对其他输入进行轻微加权来做到这一点。 以类似的方式,让我们假设为了论证的目的,隐藏层中的第二,第三和第四神经元检测是否存在以下图像:
正如你可能已经猜到的,这四个图像一起组成我们在前面显示的数字行中看到的0图像:
所以如果四个隐藏神经网络都激活了,则说明网络输入的数字是0。现在,以上所说都是一些启发式,没有说三层神经网络必须以我所描述隐藏的神经元检测简单的组件形状这种形式运行。不过以上的描述可以帮助我们理解一个神经网络到底是怎么工作的。
梯度差分法
现在我们设计了一个解决手写数字识别问题的神经网络,那么它怎么学会识别数字的呢?首先我们需要一个训练数据集。我们将使用MNIST数据集,它包含数万个手写数字扫描图像以及其正确的分类。下面是MNIST数据集数据的图片:
正如你所见到的,这正是上文中我们展示过的数字图片。当我们用训练集数据放到神经网络里面,神经网络经过学习后,我们还会用训练集的数据去检测神经网络的识别准确度。
MNIST数据分为两部分。 第一部分包含60,000张图像用作训练数据。 这些图像是扫描250人的手写样本,其中一半是美国人口普查局的员工,其中一半是高中生。 图像是灰度的,尺寸为28×28像素。 MNIST数据集的第二部分是10,000个要用作测试数据的图像。 同样,这些是28乘28的灰度图像。 我们将使用测试数据来评估我们的神经网络学习识别数字的程度。
我们将使用符号x来表示训练输入。 我们将将每个训练输入x视为28×28 = 784维向量。 向量中的每个元素表示图像中单个像素的灰度值。 我们将通过y = y(x)来表示相应的期望输出,其中y是10维向量。 例如,如果特定训练图像x描绘6,则y(x)=(0,0,0,0,0,0,1,0,0,0)T是来自网络的期望输出 。 注意,这里的T是转置操作,将行向量转换为普通(列)向量。
经过训练后,我们期望神经网络找到一个算法,这个算法里面有合适的权值和偏差,使得神经网络输出近似于训练数据集相应的输入。为了量化这一目标,我们定义了一个目标函数(cast function):
我们称上式中函数C为二次成本函数(quadratic cost function),有时候我们称之为均方误差。上式中,w表示网络中所有权重的集合,b是所有偏差,n是训练输入的总数,a是当输入为x时,输入x所对应的真实输出值。最后,求和计算是对所以的输入x进行的求和计算。符号∥v∥是表示向量v长度的函数。仔细分析上式可以发现,目标函数C(w,b)是非负的,因为等式右边所加的每一项都是非负的。此外,当 y(x)正好近似等于所有训练输入x的输出a时,目标函数C(w,b)趋向于0。这证明神经网络训练的结果很好。如果相反的,对于每一项输入x,它的y(x)和真实输出a差距很多,那么目标函数C(w,b)将会变得非常大。这说明神经网络训练的不好。因此,我们训练神经网络就是为了使目标函数C(w,b)最小化。换句话说,我们想要找到一组权重和偏差,这使得成本尽可能小。我们将使用称为梯度差分的算法。
我们为什么要引入目标函数的概念呢? 毕竟,我们不是主要对由网络是否能过正确分类的手写数字图像感兴趣吗? 为什么不尝试直接最大化识别手写数字图像正确的数量,而是要最小化目标函数呢? 其问题是正确分类的图像的数量不是网络中的权重和偏差的平滑函数。 在大多数情况下,对权重和偏差进行小的改变不会对正确分类的训练图像的数量造成任何改变。 这使得很难找出改变权重和偏差的方法来改进神经网络的性能。 如果我们使用像目标函数这样的平滑函数,那么很容易找出权重和偏差改进的方法,从而进一步改进神经网络的分类效果。 这就是为什么我们首先专注于最小化目标函数。
即使我们想使用平滑的目标函数,你可能仍然想知道为什么我们选择方程(6)中的形式。方程(6)不是一个特别的选择吗? 也许如果我们选择不同的目标函数,我们会得到一个完全不同的使得目标函数最小化的权重和偏差集合? 这是一个很好的问题,以后我们会介绍其他目标函数形式,不过对于这个问题,方程(6)中的形式是合适的。
那好,让我们假设我们试图最小化一个函数C(v)。这可以是很多变量的任何实值函数,v = v1,v2,...。注意,我已经用v替换了w和b符号,以强调这可以是任何函数 。并且,假设C作为只有两个变量v1和v2的函数有助于我们解决C(v)最小化问题。
我们的目的是找到C(v)的最小值,如果C(v)只有两个变量v1和v2,则我们可以画出C(v)的函数图像,如上图所示,在上图中我们很容易找到该函数的最小值,因为很容看出来:)。
那么我们如何从数学上找到解决问题的方法呢?首先第一种方法是微积分,我们可以计算C(v)的导数,然后尝试用使用它们找到极值点。当C(v)只有一个或者两个变量的时候,这个问题是容易解决的,但是一旦随着变量数的增加,求解函数最小值将变成一个噩梦。对于我们的神经网络,我们通常需要数十亿个权值和偏差,所以使用微积分没有办法解决这个问题。
那么第二种方法的思想就很奇妙了。我们可以把C(v)函数图像比作一个山谷,我们想象一个球从山谷的坡上滚下来。我们日常经验告诉我们,球最终会滚到谷底。我们可以使用这个想法作为我们找到函数最小值的方法。我们随机选择一个球的起点,然后模拟球向下滚动到山谷底部的运动。
为了使这个问题更精确,让我们考虑当我们将球在v1方向上移动一段距离Δv1和在v2方向上移动一段距离Δv2时会发生什么。 微积分告诉我们C的变化如下:
然后,为了让小球滚落到山谷,我们必须让 ΔC一直保持负值。为了找出让ΔC一直保持负值的方法,我们做如下定义:
其中Δv表示变量v1和v2的微小变化,而∇C表示C(v)函数的梯度向量。
这样我们就可以将函数C的变化写成Δv和∇C的形式,我们将方程(7)重写为:
这个方程有助于解释为什么∇C被称为梯度向量:∇C将v中的变化与C中的变化相关联,正如我们期望的一种称为梯度的东西。 但是这个方程真正令人兴奋的是,它让我们知道如何选择Δv使ΔC为负。 特别是,假设我们选择
其中η是小的正参数(称为学习速率)。然后公式(9)告诉我们:
因为∥∇C||的平方大于等于0,所以保证了C将总是减小,从不增加。而这正是我们所需要的。因此,我们将使用等式(10)来定义我们的梯度下降算法中球的“运动定律”。 也就是说,我们将使用等式(10)计算Δv的值,然后将球的位置v移动该量:
总之,梯度差分算法的工作方式就是重复计算梯度∇C,然后在相反的方向上移动,从而保持一直向山谷滚下去。如下图所示:
以上,我解释了两个变量的C函数如何求得最小值,但是事实上C函数通常有很多参数。不过我们只要做一些小的修改就可以运用以上说明的方法了。
并且我们保证用于ΔC的(近似)表达式(12)将是负的。 这使我们能够通过重复应用更新规则,使梯度达到最小,即使C是许多变量的函数
现在我们来看一下,梯度差分法如何应用在神经网络学习中。其实是使用梯度差分算法不断去找到使得方程(6)中目标函数最小化的权值wk和偏差bl:
通过重复应用此更新规则,我们可以“下山”,并找到目标函数的最小值。 换句话说,这是可以用于在神经网络中学习的规则。
在实践中,为了计算梯度∇C,我们需要分别为每个训练输入x计算梯度∇Cx,然后对它们求平均。 不幸的是,当训练输入的数量非常大时,这可能需要很长时间,并且因此学习过程非常的缓慢。所以在实际应用中,我们通过随机选择小训练样本来估计梯度∇C。通过对这个小样本进行平均,我们可以快速得到真实梯度∇C的估计。这有助于加速学习过程。
识别手写数字算法的实现
现在让我们来写一个程序来实现识别手写数字功能。我们将使用到上文中介绍的梯度差分算法和MNIST训练集。我们将使用一个74行的简短python 2.7程序。首先我们需要获取MNIST数据,如果你是一个git用户,你可以通过复制本文代码来获取数据:
git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
如果你不使用git,你可以从这里下载到数据和代码。
在本文中,我已经介绍过MNIST数据,我说它被分为60,000个训练图像和10,000个测试图像。在实际应用中,我们将60,000个训练图像分为两部分,一组50,000个图像用于训练我们的神经网络,另一组10,000图像用于验证神经网络的效果。
除了用到MNIST数据集,我们还用到一个叫做Numpy的python库,你可以从这里下载。
让我来解释一下神经网络代码的核心特性,下面给出完整的列表。代码的核心是一个网络类,我们用它来表示一个神经网络。下面是我们用来初始化的代码:
class Network(object):
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
list size 包含相应层中神经元的数量,例如,如果我们要创建一个Network对象,在第一层中有2个神经元,在第二层中有3个神经元,最后一层有1个神经元,我们将使用代码:
net = Network([2, 3, 1])
Network对象中的biases和weights都是随机初始化的。使用Numpy中np.random.randn函数生成平均值为0标准差为1的高斯分布。
还要注意的是,权值和偏差被储存为Numpy矩阵的列表。因此,net.weights [1]储存的是链接第二层和第三层神经元的权值的Numpy矩阵。(这不是第一和第二层,因为Python的列表索引从0开始。)其中,wjk是第二层中的第k个神经元和第三层中的第j个神经元之间的连接的权重。 j和k索引的这种排序可能看起来很奇怪 - 确切地说,交换j和k索引有意义吗?其实,这样做的目的是运算单个神经元结果的方便,单个神经元结果我们就可以写成:
这个公式我们分开来看,a是第二层神经元激活状态的向量,为了获取a′ ,我们将a乘以权值矩阵w,并且同偏差的向量b相加。然后我们对向量wa + b中的每个条目元素应用函数σ。 (这被称为矢量化函数σ)。很容易验证等式(22)给出与我们早先的规则(等式(4))相同的结果,用于计算Sigmoid Neurons的输出。
这样我们就可以很简单的写出计算输出的函数:
def sigmoid(z):
return 1.0/(1.0+np.exp(-z))
请注意,当输入z是一个向量或Numpy数组时,Numpy自动以元素方式应用函数Sigmoid,即以向量化形式。
然后,我们向Network类添加一个feedforward方法,给定网络的输入a,返回相应的输出。
def feedforward(self, a):
"""Return the output of the network if "a" is input."""
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a
当然,我们希望我们的网络对象做的主要事情是学习。 为此,我们给他们一个实现随机梯度下降的SGD方法。
def SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None):
"""Train the neural network using mini-batch stochastic
gradient descent. The "training_data" is a list of tuples
"(x, y)" representing the training inputs and the desired
outputs. The other non-optional parameters are
self-explanatory. If "test_data" is provided then the
network will be evaluated against the test data after each
epoch, and partial progress printed out. This is useful for
tracking progress, but slows things down substantially."""
if test_data: n_test = len(test_data)
n = len(training_data)
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [training_data[k:k+mini_batch_size]
for k in xrange(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print "Epoch {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test)
else:
print "Epoch {0} complete".format(j)
training_data是表示训练输入和对应的期望输出的元组(x,y)的列表。变量epochs和mini_batch_size是你期望的 - 要训练的时期的数量,以及在抽样时使用的迷你批量的大小。 eta是学习率η。如果提供了可选参数test_data,则程序将在每个训练时期之后评估网络,并打印出部分进度。这对于跟踪进度很有用,但会大大降低效率。
代码工作如下。在每个迭代期,它开始随机对训练数据洗牌,然后将其分割成适当大小的小批量训练数据。然后对于每个mini_batch,我们应用梯度差分的单步。这通过代码self.update_mini_batch(mini_batch,eta)来完成,它使用mini_batch中的训练数据根据梯度差分的单次迭代更新网络权值和偏差。以下是update_mini_batch方法的代码:
def update_mini_batch(self, mini_batch, eta):
"""Update the network's weights and biases by applying
gradient descent using backpropagation to a single mini batch.
The "mini_batch" is a list of tuples "(x, y)", and "eta"
is the learning rate."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
绝大部分工作是通过下面这行代码执行的
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
下面是完整的代码:
"""
network.py
~~~~~~~~~~
A module to implement the stochastic gradient descent learning
algorithm for a feedforward neural network. Gradients are calculated
using backpropagation. Note that I have focused on making the code
simple, easily readable, and easily modifiable. It is not optimized,
and omits many desirable features.
"""
#### Libraries
# Standard library
import random
# Third-party libraries
import numpy as np
class Network(object):
def __init__(self, sizes):
"""The list ``sizes`` contains the number of neurons in the
respective layers of the network. For example, if the list
was [2, 3, 1] then it would be a three-layer network, with the
first layer containing 2 neurons, the second layer 3 neurons,
and the third layer 1 neuron. The biases and weights for the
network are initialized randomly, using a Gaussian
distribution with mean 0, and variance 1. Note that the first
layer is assumed to be an input layer, and by convention we
won't set any biases for those neurons, since biases are only
ever used in computing the outputs from later layers."""
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
def feedforward(self, a):
"""Return the output of the network if ``a`` is input."""
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a
def SGD(self, training_data, epochs, mini_batch_size, eta,
test_data=None):
"""Train the neural network using mini-batch stochastic
gradient descent. The ``training_data`` is a list of tuples
``(x, y)`` representing the training inputs and the desired
outputs. The other non-optional parameters are
self-explanatory. If ``test_data`` is provided then the
network will be evaluated against the test data after each
epoch, and partial progress printed out. This is useful for
tracking progress, but slows things down substantially."""
if test_data: n_test = len(test_data)
n = len(training_data)
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in xrange(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print "Epoch {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test)
else:
print "Epoch {0} complete".format(j)
def update_mini_batch(self, mini_batch, eta):
"""Update the network's weights and biases by applying
gradient descent using backpropagation to a single mini batch.
The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``
is the learning rate."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
def backprop(self, x, y):
"""Return a tuple ``(nabla_b, nabla_w)`` representing the
gradient for the cost function C_x. ``nabla_b`` and
``nabla_w`` are layer-by-layer lists of numpy arrays, similar
to ``self.biases`` and ``self.weights``."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# Note that the variable l in the loop below is used a little
# differently to the notation in Chapter 2 of the book. Here,
# l = 1 means the last layer of neurons, l = 2 is the
# second-last layer, and so on. It's a renumbering of the
# scheme in the book, used here to take advantage of the fact
# that Python can use negative indices in lists.
for l in xrange(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)
def evaluate(self, test_data):
"""Return the number of test inputs for which the neural
network outputs the correct result. Note that the neural
network's output is assumed to be the index of whichever
neuron in the final layer has the highest activation."""
test_results = [(np.argmax(self.feedforward(x)), y)
for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)
def cost_derivative(self, output_activations, y):
"""Return the vector of partial derivatives \partial C_x /
\partial a for the output activations."""
return (output_activations-y)
#### Miscellaneous functions
def sigmoid(z):
"""The sigmoid function."""
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
return sigmoid(z)*(1-sigmoid(z))
那么如何让这个程序识别手写数字图像呢?首先我们载入MNIST数据。这一步骤我们使用mnist_loader.py来完成。
>>> import mnist_loader
>>> training_data, validation_data, test_data = \
... mnist_loader.load_data_wrapper()
然后我们建立一个有30个隐藏神经元的网络。
>>> import network
>>> net = network.Network([784, 30, 10])
最后,我们将使用随机梯度下降学习从MNIST训练数据超过30个迭代,mini-batch大小为10,学习率η= 3.0
>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
最后我们将训练出一个可以识别手写数字图像的神经网络。
Epoch 0: 9129 / 10000
Epoch 1: 9295 / 10000
Epoch 2: 9348 / 10000
...
Epoch 27: 9528 / 10000
Epoch 28: 9542 / 10000
Epoch 29: 9534 / 10000
网友评论