译者注:这个系列是我从Michael Nielsen 的在线书 Neural Networks and Deep Learning 翻译而来,由于个人英语水平限制,翻译不当和有误的地方还请各位指出或忽略。我的翻译并不是忠实于原文的,有公式和代码的地方我总觉得作者的废话太多,难道公式和代码还不足表达所有吗?如果你真的要读还是建议去读原文,因为这或许只是我的读书笔记而已。
这是一本关于神经网络和深度学习的在线书籍,将介绍下面两个方面的知识:
神经网络,是一个由生物神经系统激发产生的一个非常美的编程范式,它能让计算机从观测数据中学习
深度学习, 是一组强大的神经网络技术的集合
神经网络和深度学习现在给很多领域如图片识别,语言识别,自然语言处理提供了很好的解决方案。这本书将介绍神经网络和深度学习背后的核心概念
关于本书的详细信息,请点 此处 .或者直接跳转到第一章
人类视觉系统是这个世界上的奇迹之一,考虑下面这组手写数字 大部分人都会毫不费力的认出这组数字是504129。这件简单的事就是一个魔法。在人类大脑的每个半球,都有一个初级视觉皮层被成为V1 ,它包含1.4亿多神经原,这些神经原之间有数百亿的连接。当然人类视觉系统不仅仅是V1,还包括V2,V3,V4 和V5,它们做着越来越复杂的图像处理。如果我们把人的大脑当做一台超级计算机,经过了数百万年的进化才能很好的适应这个视觉世界。所以识别这组数字并不是很简单的事情。人类能很容易的理解我们做看到的,通常这个过程都是在无意之间发生的,我们不会意识到我们的视觉系统是如何帮助我们解决这个问题的。
如果你试图写一个计算机程序来识别像上面这些数字,识别的难度变得明显。为什么看似很容易,当我们做自己突然变得非常困难。一个凭直觉的方法是我们通过形状识别如:"一个9在上面有一个圈,和垂直右下的笔画“,事实证明并不是这么简单。当你试着去建立这样精确的规则时,很快你就会陷入很多不一样的情况,很难做到规则的穷举。这是行不通的.
神经网络通过不同的思路解决这个问题。通过利用大量手写数字图片,被称作训练样本, 然后建立一个系统,这个系统能从这训练样本中学习。换句话说,神经网络从用样例自动学习规则来识别手写数字。此外,通过增加训练样本的数量,神经网络会学到更多规则来提高准确率。虽然上面的图片中只有100张训练样本,我们可以建立一个利用上百万或者几十亿的样本来建立一个很好的识别系统。
在这一章我们将写一个神经网络来识别这些手写数字。程序只有74行,没用使用特别的神经网络的包。但是这段程序在没有人介入的情况下识别手写数字的准确率是96%。此外,在后面的章节,我们将改进算法,让准确率提升到99%.事时上现在的神经网络已经很好地运用在银行的欺诈识别,和邮局识别地址。
我们把精力放在手写数字识别上,是因为这个问题是学习神经网络经典的例子。原因是这个问题不简单,但又至于复杂到需要很复杂的解决方案,和强大的计算能力。另外,这是一个很好去理解更多技术的例子,像神经网络。所以,在本书中我们会反复讨论这个问题。在后面的章节,我们会讨论这些方法如何很好地运用在其他计算机视觉,自然语言处理等其他领域。
当然,如果目的只是写一段程序去识别手写数字,这样章节就会很短。所以,我们会由浅入深介绍神经网络的各个知识点,包括两类重要的神经元((the perceptron and the sigmoid neuron) ,和梯度下降算法,这是学习神经网络的标准算法。从始至终,我将讲解背后的原理,建立你对神经网络的认识。这将需要更长的篇幅去谈论,但这是值得的,在章节结束时,将会在比较深入的位置了解到神经网络,以及它为何如此重要。
什么是神经网络?在回答之前,我将介绍一种人工神经元被称作感知机(perceptron)。Perceptrons 是Frank Rosenblatt 在1950到1960年间,他受到了Warren McCulloch 和 Walter Pitts 的启发后发明的。今天,更普遍的是使用其他类型的神经元 - 在这本书中,和在神经网络的很多实际的工作,使用的主要神经元模型是一个叫Sigmoid neuron.我们稍后讨论Sigmoid 。为了更好的理解,学习感知机的原理是很有必要的。
感知机如何工作呢? 一个感知机由多个二值输入 $$x1, x2 $$....,产生一个二值输出 在上面的例子中展示了感知机有三个输入, $$x1, x2,x3 $$ 通常,可以更多或更少的输入。Rosenblatt 提出了一个简单的规则来计算输出。他采用权重$$w1,w2$$...等实数来衡量输入的重要性。感知机的输出是0,或1.这由$$/sum(wj*xj)$$是否小于某个指定的阈值,和权重一样,阈值也是感知机的参数,用更精确的公式表达如下。 这就是感知机如何起作用的。
这就是最基础的数学模型,你可以认为感知机是通过衡量证据的重要性来做出决定的。让我来举个例子。这是个很现实的例子,很好理解,也能让我们很快联系到其他的例子。假设周末到了,你听说在你的城市将有一个奶酪节,你喜欢奶酪,你尝试去做决定去还是不去,你可以通过衡量下面的三个因素:
1.天气好吗 2.你的男朋友或女朋友愿意陪你一起去吗 3.举办地的交通方便吗?(你没有车)
我们可以使用二元变量$$x1,x2,x3$$来代替上面的因素。例如,$$x1=1$$表示天气很好,$$x1=0$$表示天气不好。类似的$$x2=1$$代表你男朋友或女朋友会陪你一起去,相反$$x2=0$$,同样$$x3$$可以类似的定义交通状况。
现在,假设你是如此的喜欢奶酪以至于你男朋友或女朋友去不去你都去。但是你非常在乎天气情况,如果天气不好你就不去。你可以通过感知机去做类似的决定。一种方式就是选择$$w1=6$$作为天气的权重,$$w2=2$$和$$w3=2$$分别作为其他两个因素的权重。$$w1$$更大表明你关心天气比其他两个因素更多。最后假设你确定的感知机阈值是5。感知机将产生一个决策模型,模型只要在天气好的时候输出1,不好的时候输出0,这个输出不会受到你男朋友或女朋友去不去,或交通的影响。
通过调整权重和阈值,我们会得到不同的决策模型。举个例子,我们选择阈值为3,在天气好,或者你男朋友或女朋友陪你或交通好你都会去。换句话说,这是一个不同的决策模型。降低阈值,代表你更加愿意去参加这个节日。
很显然,感知机并没有完全模拟人类的决策机制。但是上面的例子展示了感知机是如何在做决策的时候起作用的。而且启发我们更加复杂的感知机网络会处理更加复杂的网络。 在上面这个图中,第一列由三个简单的感知机组成,它们通过衡量输入的重要性做出非常简单的决策。第二层感知机通过衡量第一层感知机输出来做出决定。这样第二层就在更加复杂的情况下和更加抽象的情况下做决策。这样更加复杂的决策可以在第三层做出。通过这种方式多次感知机能做出很复杂的决策。 顺便说下,我们定义感知机的时候说感知机只有一个输出,但是上面的图好像有多个。事实上,它们还是只有一个输出,上面的连线只是表明输出作为多个下层神经原的输入。 让我们来简化做决策的方式。条件是$$/sum(wjxj)$$非常笨重的,我们可以 通过两个方式来简化。第一个:将$$/sum(wj*xj)$$ 转化成向量运算。$$wx ==/sum(wjxj)$$ 这里$$w,x$$都是向量。第二个就是移动阈值(threshold)到公式的另一测。这样公式可以写成下面这种格式: 你可以把偏置作为感知机输出1的容易度,或者从生物的角度解释为感知机被激活的容易度。一个大有大偏置的感知机,更容易输出1.(偏置值的是上面公式中的b).如果偏置是负数,就很难输出1.很显然,很显然,引入偏置只是我们介绍感知机的很小一部分变化,在后面会看到更进一步的简化。所以这部说我们不使用阈值(threshold)我们使用偏置(bias).
我已经介绍了感知机是通过衡量输入权重来做决策的方法。感知机还可以做逻辑运算,如AND(与),OR(或),和NAND(与非)。例如,假设我们有一个感知机,有两个输入,都有-2的权重,偏置为3.如下图: 我们会看到输入为00的时候输出为1.(-2) 9-2 0+3=3是正数。相似的如果输入为01或10输出为1.但输入为11时输出为0.同样适用与NAND.
这个个NAND 的例子将展示我们是如何使用感知机来实现简单逻辑运算的。实时上我们可以用实现任何复杂的逻辑。原因是NAND对于计算机是通用的,我们可以实现任何的逻辑运算。如我们可以建立一个有两个电位组成的电路(这里比较模糊),看下面的图吧: 为了获得感知的等效网络,我们通过感知具有两个输入,每个重量-2-2替换逻辑运算,以及33的整体偏置 需要注意的是最左边的感知机的输出被两次作为输入到最下面的感知机。当我定义一个感知机的时候我并没有说这样同时作为两个输入是否被允许。其实这没关系。如果我们不想这样,可以简单的合并这两条线,将权重换成-4就好了。改变之后的就如下图: 值到现在我一直画的是输入在感知机外,实时上传统的感知机会把输入作为感知机网络的一层: 下面这个符号输入感知器,在此我们有一个输出,但没有输入 这是一个shorthand.并不是一个没有输入的感知机。为了搞清楚,假设有一个没有输入的感知机,这样$$/sum(wj*xj)$$将永远是0.这样感知机就会输出一个固定值,并不是期望的x1.不把输入算作感知机会比较合适。 上面的例子展示了感知机可以使用普遍的运算。 感知机对运算的 普遍适用是令人欣慰同时又失望的。欣慰是因为感知机可以非常常强大就像其他的计算设备一样。失望是因为感知机看起来只是一种新类型的NAND gate. 然而,有个更好的观点。事实证明我们可以设计学习算法自动调节人工神经原的权重和偏置。这种调整不需要人工干预。这种算法使人工神经网络完全不同与产通的NAND。人工神经网络能通过简单的学习来解决问题,包括一些通过传统方式很难解决的问题。
学习算法听起来很棒,但是我们怎样才能设计出这样的算法?假设我们有一个神经网络去解决某些问题。比如输入是手写数字图片的像素值,我们希望神经网络能学习权重和偏置来自动识别图片中的数字。为了理解如何学习,假设我们对权重或者偏置做一个微小的变化输出也会有相应变化。这使得学习成为可能。下面的示意图展示了我们的意图: 如果权重和偏置的微小变化会导致输出微小变化的假设成立,那么我们可以利用这个事实来修改权重和偏置让我们的什么网络的输出越来越接近我们想要的结果。举个例子,假设神经网络错误的把一个是"9"的图片识别成了"8".我们可以设定如何修改权重和偏置让神经网络的输出更接近9.不断的重复这个过程,修改权重和偏置的值,这样神经网络 就在不断学习。
问题是当我们的神经网络包含感知机的时候这样的假设是不成立的,事实上任何一个感知机的权重或者偏置微信的变化都可导致结果完全不一样,如从0变为1,这种变化组合起来就会产生相当复杂的情况。如果“9”可以正确识别了,但是神经网络对其他数字的识别又发生了很难控制的变化。如何不断修改权重和偏置来优化神经网络的行为是一个很难的问题。
我们可以通过另一种叫sigmoid neuron 的神经元来克服这个问题。sigmoid 神经元和感知机类似,但是权重的微小变化只会给结果带来很小的变化。这个特性能让sigmoid 神经元能很好的学习。
那就让我们来认识sigmoid神经元。我们用通样的方式描绘sigmoid.
和感知机一样有输入,$$x1,x2,,$$ 但输入不再只是0和1可以是任何0,1之间的值。这样0.638就是一个合法的输入。和感知机相同sigmod也有权重$$w1,w2,,,$$和全局偏置,输出也不再只是0,1.而是 $$/sigma(wx+b)$$,这里$$/sigma$$是sigmod 函数。定义如下: 更具体一点,当输入为$$x1,x2,,$$权重为$$w1,w1,,$$ 偏置为b,输出为下面的公式 一眼就能看出sigmoid神经元和感知机有很大的不同。但是你不熟悉S型函数似乎还是有很多不清楚。事实时上,和感知机有很多类似的地方,S型函数带来的只是技术细节上的变化,对原理的理解还是相同的。 为了理解和感知机的相似性。假设$$z/equiv wx+b$$ 是一个很大的整数,这样$$/sigma^-z/approx0$$,$$/sigma(z)/approx1$$换句话说$$z=wx+b$$是一个很大的正数,sigmod神经元的输出就会接近1,就像感知机一样。当$$z=wx+b$$是负数的时候sigmod 的输出是0,这也和感知机类似。 S型函数是怎么形成的呢?我们怎么理解它?实际上$$/sigma$$的具体形式并不重要,重要的是它的形状,如下图: 这是阶梯函数的平滑版本, 当$$/sigma$$是阶梯函数的时候,sigmoid神经元就是感知机,输出是1,还是0,完全由$$wx+b$$的值是正是负来决定。sigmoid 的平滑度是至关重要的性质。平滑就意味着 权重和偏置的变化会导致输出的变化相对较小。可以用下面的公式计算,权重和偏置变化对输出带来的影响: (公式里面的具体符号我就不翻译了),这是对sigmoid求导,看起来复杂,其实很简单。输出的变化量$$/Delta output$$是$$/Delta w_{j}$$和$$/Delta bj$$的线性函数。这就很容易根据偏置和权重的变化计算输出的变化。虽然sigmod神经元和感知机有很多相似性但是它让权重和偏置的变化对输出的影响更加明显。 $$/sigma$$的形状比形式更加重要,这也是公式(3)要使用它的原因。在后面我们将看到神经网络的输出可以是$$ f(wx+b)$$.$$f()$$是其他的激活函数。当用不同的激活函数的时候,主要的变化是公式(5)中偏导的计算。 我们要怎样理解一个sigmod 神经元的输出?很明显sigmod和感知机很多一个不同是可以输出0,1之间的值而不只是0,1.这是有用的,例如我们想让神经远的输出来代表图像上像素的平均强度的时候。但又有麻烦,比如我们想让神经元去判断一张图片是否是9的时候,显然当输出是0,1的时候比较好农。在实际应用中,我们定力一个规则来解决这个问题,如当输出大于0.5的时候为9小于的时候不是。
这部分是对上面的两个问题,方便理解,省略
在下一部分我将介绍一个能很好识别手写数字的神经网络的结构。为了很好的理解,我们先介绍一些概念。 前面提到过,最左边那层是输入层,最右边是输出层。在上面的途中只有一个输出。中间那层被称作hidden layer(隐藏层),因为该层神经元不是输出也不是输入。隐藏的意思有点迷惑,它没有什么更加深奥的含义,就只是表明不是输出也不是输入。上面的网络只有一个隐藏层,但是在其他情况下可以有很多,例如下面这个图; 有点乱,由于历史原因,多层神经网络有时候被称作multilayer perceptrons 或者MLPS,尽管这是由sigmoid神经元组成的神经网络。我们不会在这本书使用MLP这个词,因为很让人迷惑,但是你要知道它的存在。 输入层和输出层的定义通常很直接。比如,我们尝试去判断一张手写数字图片上是不是9.一种自然的方法就是设计一个网络,图片中的像素作为神经网络的输入。假设图片是64*64的灰度图,那么将有4096个在0和1之间的输入神经元。输出层只会有一个神经 元,输出小于0.5表明输入图片不是9,大于0.5则是。 输入层输出层的定义很简单,但是隐含层的就相当高明。特别是,不可能简单的几个规则总结隐含层设计的所有设计,神经网络的研究者已经设计了很多种隐藏层为了满足不同的需求。例如,通过试探法来确定隐含层的数量,已节省训练时间。我们将在这本书的后面遇到这样的设计。 到目前为止,我们讨论的只是一层的输出被用到下层这样的网络,这样的网络被成为前向神经网络(feedforward neural networks) 意思是这样的网络没有回路,信息会不停的往前传递。如果有回路,我们最终得出的结论不仅依赖于输入,还被输出影响,这很难理解,所以我们这里不允许这样的回路。 然而有其他人工神经网络可以有回路,这样的被称作递归神经网络( recurrent neural networks),在这样的网络里,神经元可以被激活一段时间,同时会刺激其他神经元,自己在一段时间内也可能被再次激活。这导致更多的神经元被激活。在这样的模型里回路不会有问题,神经元的输出只会在一定时间内影响他的输入,但不是永远。
回馈神经网络目前并没有前馈神经网络那样影响大,因为现在的学习算法还不是很强大.但是回馈神经网络还是非常的有意思,和我们大脑的实际思考过程更相似.它能更加复杂的问题,为了限制本书的讨论范围,我们值重点介绍广泛使用的前馈神经网络
如何设计一个神经网络,让我们回到手写数字识别问题,我们可以把这个问题分解成两个,第一,我们将把连续的图片切割成单个的数字.例如可以把下面这张图片 切成下面六张独立的图片
这种问题对人来说很容易,但是对于计算机还是有难度的.如果图片已经被分隔好了,第二个问题就是对分隔后的图片进行分类,例如把识别上图第一张图片 是5. .我们的程序将专注解决第二个问题,因为如果能识别单个的图片第一个问题并不是那么难.因为我们可以不停的尝试很多分割的方法,并对每种方式一个分数.使用分类器的准确性来衡量分隔方法的好坏.一个分隔方法分隔的图片有比较好的识别率的时候就是一个比较好的方法.反之亦然.因为当分隔的图片有误的时候分类有会有影响.所以我们先不担心分割的问题(译者:觉得分割反而是准确率的关键),我们先集中经历解决更难更棘手的单个数字的识别。 为了识别单个的图片,我们将使用三层的神经网络: 神经网络的输入是图片的像素值,我们的训练数据是28×28 的手写数字图片,输入层包含784=28*28个神经元.为了简单上图省略了很多输入。输入的像素值是灰度值,0.0代表白色1.0代表黑色。 第二层是隐藏层,隐藏层的个数为n.我们将使用不同的n值,图片中展示的是15个神经元。 输出层包含10个神经元。如果第一个神经元被激活也就是神经元的输出值接近1,那么我们认为图片的值是0.以此类推。更具体点说,我们把输出神经元编号为0到9,找出哪个神经元最接近1.比如是第6个那么我们就认为结果为6. 也许你会问为什么输出是10个,我们的目的是找到是0到9中的哪一个。一种看似更加合理的方式是只要四个输出。因为每个神经元有两种结果,0或者1.这样足够编码所有答案了,因为$$2^4=16$$ 已经10大了。那为什么 我们的输出层是10个神经元。这样是不是很更复杂了。实时证明10个的输出比4个的输出更加准确。那为什么10个的输出会比4个的输出要好呢?我们能从中得到什么启发吗? 为了理解这个问题,我们考虑10个输出的情况。有10个输出的时候,我们用第一个神经元的结果来判断是不是0.是通过衡量隐藏层的输出来做的决定的。那隐藏层做了什么呢?假设隐藏层的第一个神经元只是判断下面这样的图片是否存在: 它可以通过输入像素来判断。类似的方式,让我们假设还有三个隐藏层判断下面三张图片的样式是否存在: 或许你已经猜到了,上面四张图组合起来就是0. 所以当四个隐藏层的神经元都激活的时候我们认为是0.这并不是唯一的方法去判断数字是不是0.但是通过这种方式我们确实可以判断输入是不是0 假设神经网络按照上面说的方式运行。我们可以给一个不太严谨的解释,为什么是10个输出而不是4个。如果我们只有4个输出。第一个输出层的神经元,要尝试去决定这这张 图片最显著的特性,但是这可能比上面举的例更简单的方式了。最强的只直观解释就是数字都是和图片的形状有关系的。 上面说的都是直觉的东西,没法证明上面的三层神经网络就是按照这样的方式运作的。或许有更好的算法来使用4个神经元的输出。我想我已经尽力把这个问题解释清楚了,应该能节省你很多时间。
译者注:这部分省略省略
我们已经设计好了神经网络的结构,那它怎么学习呢?第一件事是我们需要训练数据。我们将使用 MNST data set ,包含了成千上万的已经标注好的手写数字图片。MNIST的名字是因为这些数据是NIST(United States' National Institute of Standards and Technology)收集的。下面是其中的样例:
你可以看到这些图片和我上面展示的是一样的,当然,我们将使用不在训练样本中的数据来测试。 MNIST 数据包含两部分,第一部分包括60000张图片他们是从250个人的手写数据中收集的,一半是 US Census Bureau 的员工。一半是大学雪上。图片是28*28的灰度图。第二部分包含10000张图片。前者用于训练后者用于测试。测试数据是收集自不同的250人的手写数字。所以这能让我们的测试有比较好的说服力。 我们将使用$$x$$来代表输入:784个元素组成的向量,每个元素是灰度值。我们将定义输出为$$y=y(x)$$ 这里$$y$$是10维的向量。例如输入的x实际是6的图片,输出$$y(x)=(0,0,0,0,0,1,0,0,0)^t$$ 。 我们希望有一种算法能帮助我们寻找能准确的$$y(x)$$权重和偏置,为了衡量算法的好坏我们定义了一个误差函数: 这里,w代表权重的集合,b代表所有偏置。n是输入样本的数量,a是输入为x时的输出。当然a是依赖与输入,x,w,b的但是为了简单我们有指明这种依赖关系。其实这个方程就是我们熟悉的误差平方和(MSE)。特别的我们发现方程的左边$$C(w,b)$$是非负的。因为是对向量的模求和。当所有样本都能比较准确预存的时候$$C(w,b)/approx0$$ .所以我们训练算法的目标就是让$$C(w,b)$$的值最小化。换句话说我们的目标是寻找一个权重和偏置的集合能使得$$C(w,b)$$的值总和最小。我们将使用知名的梯度下降算法。 为什么要使用误差函数而不是直接使用预测的正确数量来判断。为什么不是使用分类错误的数量来判断,而是通过其他方式比如误差函数。这是因为分类正确的数量并不是权重和偏置的变化的平滑函数,很多时候权重和偏置的调整都不会对分类正确数量有什么变化。这让调整权重和偏置变得困难。这就是我们 为什么要使用误差函数的原因。 就算我们想使用一个平滑的误差函数,那么我们为什么选择使用公式(6)中的二次函数。这是一个固定的选择吗?或许我们选择一个不同的误差函数,会得到完全不同的权重和偏置呢。这是一个值得担心的问题,再后面我们将看到我们将重新审视误差函数,并做些修改。不管怎样,公式(6)在神经网络中效果很好,我们现在就来使用它。 再重复一次,我们的目标是训练神经网络去找到合适的权重和偏置是的误差函数值最小。这是一个目标明确的问题,但是w,b的含义,还有背后的$$/sigma$$函数,还有公式的结构都很容易使人分心。我们可以忽略这些东西,只把重点击中在目标最小化上。现在我们忘掉所有的神经网络和其他的东西,假设我们有一个函数,我们希望 寻找函数的最小值。我们将介绍一种梯度下降算法来解决这种问题,然后再回到神经网络的问题。 好,假设我们尝试去最小化函数$$C(x)$$它可以是任何实值函数,v=v1,v2...是变量。为了更好的理解我们假设变量只有两个即使:v1和v2
在上面这个图我们能用眼睛看到最小值的位置。一个任意的函数或许有更加复杂的变量,不能很直观的找到最小值。 一种方式是尝试使用微积分来找到最小值。我们可以计算导数,来找到函数的极值,如果运气好,这种方法或许可行,当变量很多的时候是绝对行不通的。对于神经网络通常会有很多变量。权重和偏置通常都很复杂。所以导数是不行的。 () 好了只计算导数是行不同的,幸运的是,有一个美丽类比的算法而且这表明它非常有效。我们通过像上图山谷的图开始思考。假设在山谷的上方有一个石头从山顶滚到山底,经验告诉我们石头最终会滚到山谷。或许我们可以运用这个想找到灵感。我们随机选择一个点,然后计算偏导数,或者是多阶偏导,偏导将告诉我们这个点山的地势,然后告诉我们球应该往哪边滚。 为了更精确的理解这个问题,当v1 的值变化为$$/Delta v1$$ v2的变化为$$/Delta v2$$的时候函数的值的变化为: 我们将找到一个方法选择$$/Delta v1$$ 和$$/Delta v_{2}$$ 让$$/Delta C$$的值为负。这样我们就能不断降低,我们把上面的公式用向量表示如下: 上面的公式还可以写成下面的样子 $$/Delta C$$就是梯度向量。那么怎么知道该如何修改v的值让$$/Delta C$$的值是负数呢?特别的我们假设 这里$$/eta$$是非常小的正实数,被称作学习速率。公式(9)可以知道$$/Delta C /approx -/eta /Delta C /bullet /Delta C = -/eta ||/Delta C||^2$$,很明显$$/Delta C$$会小于0也就是会不断减小。这就是我们想要的我们可以将公式(10)作为石头的运动轨迹。 v的值可以使用上面的公式做不断的调整。不停的调整知道找到C的最小值。总的来说这个算法是在不断的计算梯度。然后修改下降的方向。像下面这个图所表示的一样: 为了能让梯度下降算法有效我们需要选择足够小的$$/eta$$让方程(9)很好的近似,可能有可能$$/Delta C>0 $$的情况,这就会让梯度下降算法不会足够好。同时我们不像$$/eta$$太小,因为太小算方法会收敛的很慢。
解释了变量只有两个的时候梯度算方法,当变量有多个的时候算方法还是相似的方式运作。假设函数C 有m个变量$$v {1},.. .v {m}$$ v变化时 ,有下面的公式: 梯度向量为: 和只有两个变量的情况一样我们可以选择 然后我们家ing保证$$/Delta$$始终为负数,和两个变量情况一样,我们可以使用相同的更新规则: 这个更新规则并不是总是有效,有些情况我们并不能找到函数C的全局最优,这一点我们将在后面的章节看到。但是在实际运中梯度下降通常会有比较好的效果,在神经网络中你将发现这是一个很有力的方法来寻找误差函数的最小值。
让我们来写一个程序来识别手写数字,使用梯度下降算法和MNIST 训练数据。我们使用PYthon(2.7)只用74行代码。第一件事情是我们需要获得数据。如果你使用git你可以使用下面的方法获得数据:
` git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
你也可以点[这里下载](https://github.com/mnielsen/neural-networks-and-deep-learning/archive/master.zip) 当我介绍MNIST 数据的时候就说过它是已经分割好的图包括6万张训练数据集,1万张测试数据。这是MNISt官网的描述。事实上我们将使用不同的方式,我们将把6万张训练数据分割成两部分,5万作为训练,1万用于检验。我们在这章不使用MNIST提供的测试数据。在后面的章节我们将用它来解释超参数的数,如学习速率。 我们会用到Python 的一个包[Numpy](http://numpy.org/),你需要先安装 下面我将讲解核心的代码,最后面会给出完整的代码。代码的核心是Network类,我们用来代表神经网络 ``
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:])]
` 参数sizes 是神经网络个层的数量。例如我们想创建一个2个输入层,3个中间层,1个输出层将用下面的方式:
net = Network([2, 3, 1]) ``
偏置和权重在类Network 中被随机初始化,使用了Numpy 的np.random.randn 函数,产生的满足均值为0方差为1的高斯分布的随机数。这个随机就是选定了梯度下降算方法的一个随机初始。后面的章节我们可以发现有更好的寻找初始值的方法,但是现在我们先这样用。 可以看到偏置和权重都是保存在Numpy的矩阵中的。 下面这是sigmoid 函数的实现,Numpy 会自动使用矩阵计算
` def sigmoid(z): return 1.0/(1.0+np.exp(-z))
然后我们增加一个前向算法的代码: ``
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) ``
贴完整的代码吧,我觉得作者的废话实在太多: ``` """ 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. """
import random
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)
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_loader ~~~~~~~~~~~~ A library to load the MNIST image data. For details of the data structures that are returned, see the doc strings forload data
andloaddata wrapper
. In practice,load
data_wrapper`` is the function usually called by our neural network code. """
import cPickle import gzip
import numpy as np
def load data(): """Return the MNIST data as a tuple containing the training data, the validation data, and the test data. The
training_data
is returned as a tuple with two entries. The first entry contains the actual training images. This is a numpy ndarray with 50,000 entries. Each entry is, in turn, a numpy ndarray with 784 values, representing the 28 * 28 = 784 pixels in a single MNIST image. The second entry in the
training_data
tuple is a numpy ndarray containing 50,000 entries. Those entries are just the digit values (0...9) for the corresponding images contained in the first entry of the tuple. The
validation_data
and
test_data
are similar, except each contains only 10,000 images. This is a nice data format, but for use in neural networks it's helpful to modify the format of the
training_data
a little. That's done in the wrapper function
load_data_wrapper(), see below. """ f = gzip.open('../data/mnist.pkl.gz', 'rb') trainingdata, validation data, test data = cPickle.load(f) f.close() return (training data, validation
data, test_data)
def load data wrapper(): """Return a tuple containing
(training_data, validation_data, test_data)
. Based on
load_data
, but the format is more convenient for use in our implementation of neural networks. In particular,
training_data
is a list containing 50,000 2-tuples
(x, y)
.
x
is a 784-dimensional numpy.ndarray containing the input image.
y
is a 10-dimensional numpy.ndarray representing the unit vector corresponding to the correct digit for
x
.
validation_data
and
test_data
are lists containing 10,000 2-tuples
(x, y)
. In each case,
x
is a 784-dimensional numpy.ndarry containing the input image, and
y
is the corresponding classification, i.e., the digit values (integers) corresponding to
x. Obviously, this means we're using slightly different formats for the training data and the validation / test data. These formats turn out to be the most convenient for use in our neural network code.""" tr d, va d, te d = load data() training inputs = [np.reshape(x, (784, 1)) for x in tr d[0]] training results = [vectorized result(y) for y in tr d[1]] training data = zip(training inputs, training results) validation inputs = [np.reshape(x, (784, 1)) for x in va d[0]] validation data = zip(validation inputs, va d[1]) test inputs = [np.reshape(x, (784, 1)) for x in te d[0]] test data = zip(test inputs, te d[1]) return (training data, validation
data, test_data)
def vectorized_result(j): """Return a 10-dimensional unit vector with a 1.0 in the jth position and zeroes elsewhere. This is used to convert a digit (0...9) into a corresponding desired output from the neural network.""" e = np.zeros((10, 1)) e[j] = 1.0 return e ``` 找数据读代码去吧,比用任何语言的解释力都强。