Chapter1:使用神经网络识别手写数字

  • 前言
    • Perceptrons(感知机)
    • Sigmoid neurons(sigmoid 神经元)
    • The architecture of neural networks(神经网络的架构)
    • A simple network to classify handwritten digits(一个分类手写数字的例子)
    • Learning with gradient descent(学习梯度下降)
    • Implementing our network to classify digits(实现我们的网络去分类数字)
    • Toward deep learning(朝着深度学习的方向前进)

前言

人类视觉系统是世界奇迹之一。以下一串手写数字:

大多数人毫不费力地认出这些数字是504192。这种轻松识别其实是带有“欺骗性”的。其实在我们大脑的每个半球,人类都有一个初级视觉皮层,也被称为V1,包含1.4亿个神经元,它们之间有数百亿个连接。然而,人类的视觉不仅包括V1,还包括一系列的视觉皮层——V2、V3、V4和V5——它们在不断地进行更复杂的图像处理。我们的大脑里装着一台超级计算机,经过数亿年的进化调整,非常适合理解视觉世界。识别手写数字并不容易。相反,我们人类是很厉害的,非常善于理解我们的眼睛所看到的东西。但几乎这一切都是无意识的。所以我们通常不会意识到我们的视觉系统解决了一个多么棘手的问题。
如果你试图编写一个计算机程序来识别像上面这样的数字,那么视觉模式识别的困难就显而易见了。当我们自己去做的时候,看似简单的事情突然变得非常困难。关于我们如何识别形状的简单直觉——“9在顶部有一个环,在右下角有一个垂直的笔画”——在算法上表达起来并不那么简单。当您试图使这些规则精确时,您很快就会迷失在异常、警告和特殊情况的泥沼中。看起来似乎无望。
神经网络以另一种方式处理这个问题。这个想法是用大量的手写数字作为训练样本,

然后开发一个可以从这些训练例子中学习的系统。换句话说,神经网络使用这些例子来自动推断出识别手写数字的规则。此外,通过增加训练样本的数量,该网络可以了解更多的笔迹信息,从而提高其准确性。因此,虽然我在上面只展示了100个训练数字,也许我们可以通过使用数千甚至数百万甚至数十亿的训练样本来构建一个更好的手写识别器。
在本章中,我们将编写一个计算机程序来实现一个学习识别手写数字的神经网络。这个程序只有74行,没有使用特殊的神经网络库。但是这个短程序可以识别准确率超过96%的数字,无需人工干预。此外,在后面的章节中,我们将提出可以将准确率提高到99%以上的想法。事实上,现在最好的商业神经网络已经好到足够可以被银行用来处理支票,被邮局用来识别地址这些业务。
我们之所以关注手写识别,是因为它是学习神经网络的一个很好的原型问题。作为一个原型,它达到了一个最佳点:它具有挑战性——识别手写数字不是件容易的事——但它并不难到需要一个极其复杂的解决方案,或巨大的计算能力。此外,这是一个发展更先进的技术的好方法,比如深度学习。所以在整本书中我们会不断地回到手写识别的问题上。在本书的后面部分,我们将讨论如何将这些思想应用到计算机视觉的其他问题,以及语音、自然语言处理和其他领域。
当然,如果这一章的重点只是写一个识别手写数字的计算机程序,那么这一章就会短得多!但在此过程中,我们将发展许多关于神经网络的关键思想,包括两种重要的人工神经元(the perceptron and the sigmoid neuron),以及被称为随机梯度下降的神经网络标准学习算法。在整个过程中,我专注于解释为什么事情是这样做的,以及建立你的神经网络直觉。这需要一个更长的讨论,而不是我只是介绍了基本的机制,但这些都是值得的,你将获得更深的理解。在本章的最后,我们将会理解什么是深度学习,以及它为什么重要。

Perceptrons(感知机)

什么是神经网络?首先,我将解释一种叫做感知器的人工神经元。感知机是在20世纪50年代和60年代由科学家Frank Rosenblatt受到Warren McCulloch和Walter Pitts早期工作的启发而发展起来的。今天,使用人工神经元的其他模型更为常见——在这本书中,以及在许多关于神经网络的现代工作中,使用的主要神经元模型被称为sigmoid neurons。我们很快就会讲到sigmoid neurons。但是要理解为什么sigmoid neurons是这样定义的,首先花点时间去理解感知器是值得的。
那么感知机是如何工作的呢?一个感知器接受几个二进制输入,x1,x2,…,并产生一个单一的二进制输出:

在这个例子中,感知机有三个输入,x1 x2 x3。一般来说,它可以有更多或更少的输入。Rosenblatt提出了一个简单的规则来计算输出。他引入了权值w1、w2、…,表示各个输入对输出的重要性的实数。神经元的输出,0或1,是由∑jwjxj的加权和是否小于或大于某个阈值所决定的。就像权值一样,阈值是一个实数,它是神经元的一个参数。用更精确的代数术语来说:

这就是感知机的工作原理!
这是基本的数学模型。你可以这样理解:感知器是一个通过weighing up evidence (通过与真实的情况比较来调节权重)来做决定的设计。让我举个例子。这不是一个很现实的例子,但是很容易理解,我们很快就会看到更现实的例子。假设周末快到了,你听说你所在的城市将举办奶酪节。你喜欢奶酪,正在决定是否去参加这个节日。你可以通过权衡以下三个因素来做出决定:

  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。然后感知机会决定你应该在天气好的时候去参加这个节日,或者当节日临近公共交通的时候,你的男朋友或女朋友愿意和你一起去。换句话说,这将是一种不同的决策模式。降低门槛意味着你更愿意去参加节日。
    显然,感知器并不是人类决策的完整模型!但是这个例子说明了感知机是如何权衡不同的证据来做出决定的。一个复杂的感知器网络可以做出非常微妙的决定
    在这个网络中,感知机的第一列——我们称之为感知机的第一层——通过权衡输入的数据,做出三个非常简单的决定。那么第二层的感知器呢?每一个感知器都是通过权衡第一层决策的结果来做决定的。这样,第二层的感知器可以在比第一层感知器更复杂、更抽象的层次上做出决定。第三层的感知器甚至可以做出更复杂的决定。这样,一个多层感知器网络就可以进行复杂的决策。
    顺便说一下,当我定义感知机的时候我说过感知机只有一个输出。在感知器上面的网络中,它们看起来有多个输出。实际上,它们仍然是单个输出。多个输出箭头仅仅是一种有用的方式来指示一个感知器的输出被用作其他几个感知器的输入。它比只画一条输出线然后分割更方便。
    我们来简化一下描述感知机的方式:∑jwjxj>threshold 这种写法看起来很笨重,我们可以做两个改变来简化它。1:将∑jwjxj写作点积的形式, w⋅x≡∑jwjxj。其中w和x是向量,它们的分量分别代表权重和输入。2:把阈值移到不等式的另一边,用偏置bias代替, b≡−threshold。使用偏置而不是阈值,感知器规则可以重写:


你可以把偏置看作是感知器输出1的容易程度的度量。或者用更生物学的术语来说,偏置是用来衡量感知器触发的难易程度的。对于一个有很大偏置的感知器,它很容易输出1。但如果偏差非常小,感知器就很难输出1。很明显,引入偏置只是我们描述感知机的一个小变化,但是我们稍后会看到它会导致进一步的符号简化。正因为如此,在书的其余部分我们不会使用阈值,我们会一直使用偏置。
我已经将感知机描述为一种衡量事实以做出决定的方法(a method for weighing evidence to make decisions.)。另一种使用感知机的方法是计算我们通常认为是基础计算的基本逻辑函数,比如AND, OR, AND NAND。例如,假设我们有一个有两个输入的感知机,每个输入的权值为- 2 ,总体偏差为3。这是我们的感知器:

然后我们看到输入x1=0和x2=0输出1,因为(−2)∗0+(−2)∗0+3=3是正数。但是输入x1=1,x2=1输出0,因为(−2)∗1+(−2)∗1+3=−1。我们的感知器实现了一个NAND门!
NAND的例子表明我们可以使用感知器来计算简单的逻辑函数。事实上,我们可以使用感知机网络来计算任何逻辑函数。原因是NAND门对于计算来说是通用的,也就是说,我们可以用NAND门构建任何计算。例如,我们可以使用NAND门来构建一个增加两位元的电路

为了得到一个等效的感知机网络,我们将所有的NAND门替换为两个输入的感知机,每个输入的权值为- 2,总体偏置为3。这是最终的网络。注意,我已经移动了与右下角NAND门相对应的感知器,只是为了更容易在图上画出箭头:

这个感知器网络一个值得注意的方面是,最左边感知器的输出被用作最下面感知器的两次输入。当我定义感知器模型时,我并没有说这种双输出到同一个地方是否被允许。实际上,这并不重要。如果我们不希望出现这种情况,那么可以简单地将这两条线合并成一个权重为-4的连接,而不是两个权重为-2的连接。(如果你觉得这个不明显,你应该停下来,向自己证明这是等价的。)有了这个变化,网络看起来如下,所有未标记的权值等于-2,所有偏置等于3,单个权值为-4,标记如下:

到目前为止,我一直把像x1和x2这样的输入作为浮动在感知器网络左边的变量。事实上,传统的做法是绘制一个额外的感知器层——输入层——来编码输入:

Sigmoid neurons(sigmoid 神经元)

学习算法听起来很棒。但是我们如何为神经网络设计这样的算法呢?假设我们有一个感知机网络我们想用它来学习解决一些问题。例如,网络的输入可能是来自扫描的手写数字图像的原始像素数据。我们希望网络学习权重和偏差,这样网络的输出就能正确地对数字进行分类。为了了解学习是如何进行的,假设我们对网络中的某个权重(或偏差)做了一个小小的改变。我们想要的是,这个小的权重变化只会引起网络输出的一个小的对应变化。我们马上就会看到,这个特性将使学习成为可能。总的来说,这就是我们想要的(很明显,这个网络对于手写识别来说太简单了!)

如果某一权重(或偏差)的微小变化确实只会导致输出的微小变化,那么我们可以利用这一事实来修改权重和偏差,使我们的网络以我们希望的方式运行。例如,假设网络错误地将应该是“9”的图像分类为“8”。我们可以找出如何在权重和偏差上做一个小的改变,这样网络就能更接近于将图像分类为“9”。然后我们会重复这一过程,不断改变权重和偏差以产生越来越好的output。神经网络将会在这个过程中“学习”。
问题是,当我们的网络包含感知器时,就不会发生这种情况。事实上,网络中任何一个感知器的权值或偏差的一个小变化有时会导致该感知器的输出完全翻转,比如从0到1。这种翻转可能会导致网络其他部分的行为以某种非常复杂的方式完全改变。因此,虽然你的“9”现在可能被正确分类,但网络在所有其他图像上的行为可能已经以某种难以控制的方式完全改变了。这使得我们很难看到如何逐步修改权重和偏差,从而使网络更接近所期望的行为。也许有什么聪明的办法可以解决这个问题。但现在还不清楚我们如何学习感知器网络。
我们可以使用一种新型的sigmoid neuron人造神经元来解决这个问题,Sigmoid神经元与感知器类似,但经过了修改,所以它们的权重和偏差的微小变化只会导致它们的输出产生微小的变化。这是一个至关重要的事实,它将使得sigmoid neurons可以“学习”。
OK,让我来描述一下sigmoid neurons。我们将用描述感知器的方式来描述sigmoid neurons:
正如一个感知机一样,sigmoid neurons也有输入,x1,x2,…但是并不仅仅限于输入0或1,这些输入可以输入介于0到1之间的任何值,比如:0.638等。和感知器一样,sigmoid神经元也有每个输入的权值,w1、w2、……和总体偏差b。但是输出不是0或1。相反,他的函数是 σ(w⋅x+b),这里的 σ被叫做sigmoid 函数,也被定义为如下公式:


更明确地说,一个输入为x1,x2,…,权值w1,w2,…,和偏置b的sigmoid neurons输出是:

乍一看,sigmoid神经元与感知器非常不同。如果你还不熟悉sigmoid函数的代数形式,它可能看起来晦涩难懂,令人生畏。事实上,感知器和sigmoid神经元之间有很多相似之处,而sigmoid函数的代数形式更多的是一种技术细节,而不是成为你理解的一个障碍。
理解与感知器模型的相似性,假设z≡w⋅x + b是一个很大的正数。然后e−z≈0所以σ(z)≈1.换句话说,当z = w⋅x + b是非常大的正数。sigmoid neuron的输出接近1。另一方面假设z = w⋅x + b是负得很大的的负数。然后e−z→∞,σ(z)≈0。
也就是说,sigmoid函数的功能是相当于把一个实数压缩至0到1之间。当z是非常大的正数时,σ(z)会趋近于1,而z是非常小的负数时,则σ(z)会趋近于0。看了图之后或许你会明白很多:

这个形状是一个平滑版本的阶梯函数:

实际上如果σ是一个阶跃函数,那么sigmoid神经元是一个感知器,因为输出1或0取决于w⋅x + b是正数还是负数。σ函数的平滑性是一件非常重要的事情,σ函数的额平滑度意味着 Δwj和Δb的微小变化将会引起Δoutput的微小变化。事实上,微分可以让我们将Δoutput近似表示为:

我们应该如何解释sigmoid神经元的输出呢?很明显,感知器和sigmoid神经元之间的一个很大的区别是sigmoid神经元不仅仅输出0或1。它们可以输出0到1之间的任何实数,因此像0.173…和0.689…这样的值是合法的输出。这可能很有用,例如,如果我们想要使用输出值来表示输入到神经网络的图像像素的平均强度。但有时也会令人讨厌。假设我们希望网络的输出表明“输入图像是9”或“输入图像不是9”。显然,如果输出是0或1,这是最简单的方法,就像感知机一样。但是在实践中,我们可以建立一个惯例来处理这个问题,例如,决定将任何大于0.5的输出解释为“9”,而将任何小于0.5的输出解释为“不是9”。当我们使用这样的约定时,我总是明确地声明,所以它不应该引起任何混淆。

The architecture of neural networks(神经网络的架构)

在下一节中,我将介绍一种能够很好地对手写数字进行分类的神经网络。在此之前,解释一些术语有助于我们为网络的不同部分命名。假设我们有这个网络:

如前所述,这个网络中最左边的一层称为输入层,这层中的神经元称为输入神经元。最右边的输出层包含输出神经元,或者,在本例中,包含单个输出神经元。中间层称为隐层,因为这层神经元既不是输入也不是输出。“隐藏”这个词可能听起来有点神秘——我第一次听到这个词时,我以为它一定有某种深刻的哲学或数学意义——但它实际上只是“不是输入或输出”的意思。上面的网络只有一个隐藏层,但是有些网络有多个隐藏层。例如,下面的四层网络有两个隐藏层:

出于历史原因,这种多层网络有时被称为多层感知器或mlp,尽管它是由sigmoid神经元组成的,而不是感知器。我不打算在这本书中使用MLP术语,因为我认为它令人困惑,但是我想提醒您它的存在。
网络中输入层和输出层的设计通常很简单。例如,假设我们试图确定一个手写图像是否描绘了一个“9”。设计网络的一种自然方法是将图像像素的强度编码到输入神经元中。如果图像是一个64x64灰度图像,那么我们将有4,096=64×64个输入神经元,其强度适当地缩放在0和1之间。输出层只有一个神经元,输出值小于0.5表示“输入图像不是9”,大于0.5表示“输入图像是9”。
虽然神经网络的输入层和输出层的设计通常很简单,但是隐藏层的设计却很复杂。特别是,用一些简单的经验法则来总结隐藏层的设计过程是不可能的。相反,神经网络研究人员已经开发出许多针对隐藏层的设计启发法,帮助人们从他们的网络中获得他们想要的行为。例如,可以使用这种启发式方法来帮助确定如何平衡隐藏层的数量与训练网络所需的时间。在本书的后面部分,我们将会遇到一些这样的设计启发。
到目前为止,我们一直在讨论神经网络,其中一层的输出用作下一层的输入。这种网络称为前馈神经网络。这意味着在网络中没有循环——信息总是向前反馈,而不是向后反馈。如果我们做的有循环,我们最终得到的情况输入σ函数依赖于输出。这很难理解,所以我们不允许这样的循环。
然而,在人工神经网络的其他模型中,反馈回路是可能的。这些模型被称为RNN(递归神经网络)。这些模型的想法是让神经元在一段有限的时间内fire。这种fire可以刺激其他神经元,这些神经元可能在稍后的一段时间内也会短暂fire。这会导致更多的神经元fire,所以随着时间的推移,我们会有一连串的神经元fire。在这样的模型中,循环不会引起问题,因为神经元的输出只会在以后的某个时间影响它的输入,而不是在瞬间。
递归神经网络的影响力不如前馈网络,部分原因是递归网络的学习算法(至少到目前为止)不够强大。但是 recurrent nets仍然非常有趣。它们比前馈网络更接近我们大脑的工作方式。而递归网络有可能解决一些重要的问题,而这些问题只有通过前馈网络才能解决。然而,在这本书中,我们将集中在更广泛使用的前馈网络。

A simple network to classify handwritten digits(一个分类手写数字的例子)

定义了神经网络之后,让我们回到手写识别。我们可以把识别手写数字的问题分成两个子问题。首先,我们希望有一种方法可以将包含多个数字的图像分解成一系列独立的图像,每个图像包含一个数字。例如,我们想要将图像

拆分成6个分开的图像
我们人类可以很容易地解决这个分割问题,但是对于一个计算机程序来说,正确地分割图像是一个挑战。一旦图像被分割,程序就需要对每个单独的数字进行分类。例如,我们想让程序识别上面的第一个数字,

是一个数字5.
我们将重点编写一个程序来解决第二个问题,即对单个数字进行分类。我们这样做,因为事实证明,分割问题并不那么难解决,一旦你有一个很好的方法来分类个别数字。解决细分问题的方法有很多。一种方法是尝试多种不同的图像分割方法,使用单个数字分类器对每个尝试分割进行评分。如果单个数字分类器对其在所有段中的分类都分得很好,那么trial segmentation就会得到高分;如果分类器在一个或多个段中遇到很多问题,那么试trial segmentation就会得到低分。其思想是,如果分类器在某个地方有问题,那么它可能有问题。因此,与其担心这些,我们不如专注于开发一个神经网络,它可以解决更有趣和更困难的问题,即识别单个手写数字。
为了识别单个数字,我们将使用一个三层神经网络:

网络的输入层包含编码输入像素值的神经元。正如下一节所讨论的,我们的网络训练数据将包含许多28×28像素的手写数字扫描图像,因此输入层包含784=28×28个神经元。为了简单起见,我在上面的图表中省略了784个输入神经元中的大部分。输入像素为灰度,值为0表示白色,值为1表示黑色,0-1之间的值为逐渐变暗的灰色阴影。
网络的第二层是一个隐藏层。我们用神经网络来表示这个隐含层中的神经元数量,我们将用不同的神经网络值来进行实验。上面的例子展示了一个小的隐含层,包含了n=15个神经元。
网络的输出层包含10个神经元。如果第一个神经元被激活,,输出≈1,则表示网络认为数字是0。如果第二个神经元被激活,则表明网络认为这个数字是1。等等。更精确地说,我们给输出神经元编号,从0到9,然后计算出哪个神经元的激活值最高。如果这个神经元是,比如说,神经元编号6,那么我们的网络就会猜测输入的数字是6。其他输出神经元也是如此。
你可能想知道为什么我们使用10个输出神经元。毕竟,网络的目标是告诉我们哪个数字(0、1、2、……、9)对应于输入图像。一种看似自然的方法是只使用4个输出神经元,根据神经元的输出是接近0还是接近1,将每个神经元视为一个二进制值。4个神经元就足够对答案进行编码,因为2的4次方等于16大于输入数字的10个可能值。为什么我们的网络应该使用10个神经元呢?这不是效率低下吗?最终的证明是经验主义的:我们可以尝试这两种网络设计,结果证明,对于这个特殊的问题,有10个输出神经元的网络比有4个输出神经元的网络更好地学习识别数字。但这让我们疑惑,为什么使用10个输出神经元效果更好。有没有什么启发式的方法可以预先告诉我们应该使用10-output编码而不是4-output编码?
为了理解我们为什么这样做,从基本原理出发,思考一下神经网络在做什么是有帮助的。首先考虑我们使用10个输出神经元的情况。让我们把注意力集中在第一个输出神经元上,这个神经元要判断数字是不是0。它通过权衡来自隐藏的神经元层的evidence来做到这一点。那些隐藏的神经元在做什么?为了方便讨论,假设隐层中的第一个神经元检测到的图像是否如下图所示:

它可以通过对与图像重叠的输入像素进行加大权重,而只对其他输入进行很小的加权来做到这一点。同理,为了论证,我们假设隐层中的第二、第三、第四神经元检测到是否存在以下图像:

你可能已经猜到了,这四张图片合在一起构成了我们之前看到的数字为0的图片:

所以如果这四个隐藏的神经元都在活动那么我们就可以得出数字是0的结论。当然,这并不是我们可以用来得出图像是0的唯一证据——我们可以通过许多其他方式合法地得到0(例如,通过上面图像的翻译,或轻微的扭曲)。但似乎可以有把握地说,至少在这种情况下,我们可以得出输入是0的结论。
假设神经网络以这种方式运行,我们可以给出一个合理的解释,为什么从网络中输出10个输出比4个输出更好。如果我们有4个输出,那么第一个输出神经元将试图决定数字的最有效位是多少。没有简单的方法把最重要的部分与上面所示的简单形状联系起来。很难想象有什么好的原因使得数字的组成形状与输出中最重要的位密切相关。
综上所述,这只是一个启发。没有人说三层神经网络必须以我所描述的方式运作,隐藏的神经元检测简单的组成形状。也许一个聪明的学习算法会找到一些权重的分配,让我们只使用4个输出神经元。但作为一种启发式的思维方式,在设计好的神经网络架构中可以为您节省很多时间。

Learning with gradient descent(学习梯度下降)

既然我们已经设计了我们的神经网络,它如何学习识别数字呢?我们首先需要学习的是一个数据集,即所谓的训练数据集。我们将使用MNIST数据集,该数据集包含成千上万张手写数字的扫描图像,以及它们的正确分类。MNIST的名字来源于它是美国国家标准与技术研究院NIST收集的两个数据集的修改过后的子集。以下是来自MNIST的一些图片:

正如您所看到的,这些数字实际上与本章开头所示的数字一样,都是一个难以识别的数字。当然,在测试我们的网络时,我们会要求它识别不在训练集中的图像!
MNIST的数据分为两部分。第一部分包含60000张图像作为训练数据。这些图像是来自250个人的扫描笔迹样本,其中一半是美国人口普查局的雇员,一半是高中生。图像是灰度和28×28像素的大小。MNIST数据集的第二部分是10,000张用作测试数据的图像。同样,这些是28×28的灰度图像。我们将使用测试数据来评估我们的神经网络学习识别数字的能力。为了更好地测试员工的表现,测试数据取自250人,与原始的training数据不同(尽管仍是一个由人口普查局雇员和高中生组成的群体)。这有助于让我们相信,我们的系统能够识别那些在训练中没有看到书写内容的人的数字。
我们将使用符号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是一个转置操作,将一个行向量变成一个普通的(列)向量。
我们想要的是一种算法,它能让我们找到权重和偏差,当使用输入为x的数据训练神经网络,使得输出近似于y(x)。为了量化我们在多大程度上实现了这个目标,我们定义了一个损失函数:

这里,w表示所有权重的集合,b是整个网络的偏置,n是所有训练数据的数量,当x作为输入时网络的输出向量 a。网络的输出取决于x, w和b符号‖v‖只是表示向量v的一般长度函数。我们称C为二次损失函数;它有时也被称为均方误差或MSE。考察二次损失函数的形式,我们发现C(w,b)是非负的,因为总和中的每一项都是非负的。此外,成本C(w,b)变小,即对于所有的输入,当y(x)近似等于输出时,C(w,b)≈0。所以我们的训练算法做得很好,如果它能找到权值和偏差,使得C(w,b)≈0。相比之下,当C(w,b)较大时,情况就不那么好了——这意味着对于大量输入,y(x)不接近输出a。所以我们训练算法的目标是使C(w,b)作为权重和偏差的函数的代价最小化。换句话说,我们希望找到一组使损失尽可能小的权重和偏差。我们将使用一种叫做梯度下降的算法。
为什么要引入二次损失?毕竟,我们主要关心的不是网络正确分类的图像的数量吗?为什么不尝试直接最大化这个数字,而不是最小化像二次损失这样的代理度量呢?问题是,正确分类的图像数量并不是网络中权重和偏差的平滑函数。在大多数情况下,对权重和偏差做一些小的改变根本不会导致正确分类的训练图像的数量发生任何变化。这使得很难找出如何改变权重和偏差来提高性能。如果我们使用一个平滑的损失函数,比如二次损失,就很容易找出如何对权重和偏差进行小的改变,从而提高成本。这就是为什么我们首先关注最小化二次损失,然后才会检查分类的准确性。
即使我们想要使用一个平滑的cost函数,你可能仍然想知道为什么我们选择方程(6)中的二次函数,这不是一个特别的选择吗?也许如果我们选择一个不同的cost函数我们会得到一个完全不同的最小化权重和偏差的集合?这是一个有效的关注点,稍后我们将重新讨论cost函数,并进行一些修改。然而,方程(6)的二次cost函数对于理解神经网络学习的基础知识非常有用,所以我们暂时只讨论它。
总结一下,我们训练神经网络的目标是找到使二次cost函数C(w,b)最小化的权值和偏差。这是一个适定性的问题,但目前有很多分散结构的构成——怎样更好的解释w,b,以及网络体系结构的选择等等。结果是,我们可以通过忽略大部分的结构,而只关注损失函数最小化。现在我们要忘记所有关于cost函数的具体形式,以及与神经网络的联系,等等。相反,我们假设我们只是得到了一个有很多变量的函数我们想要最小化这个函数。我们要开发一种叫做梯度下降的技术,它可以用来解决这样的最小化问题。然后我们会回到我们想要最小化的神经网络的特定函数。
假设我们要最小化某个函数C(v)这可以是任何多变量的实值函数,v =v1,v2,…注意,我已经用v代替了w和b的符号来强调这可以是任何函数-我们不再在神经网络的背景下具体思考。为了最小化C(v)我们可以把C想象成一个只有两个变量的函数,我们称它为v1和v2:

我们要做的是找出C在哪里达到全局最小值。当然,对于上面所画的函数,我们可以仔细观察它的图像并找出它的最小值。在这个意义上,我可能展示了一个稍微过于简单的函数!一个通用的函数。但C可能是一个包含很多变量的复杂函数,通常不可能仅仅通过观察图形来找出最小值。
解决这个问题的一种方法是用微积分来分析求最小值。我们可以计算导数然后试着用它们找出C是极值的地方。如果幸运的话,当C是一个或几个变量的函数时,这种方法可能会奏效。但是当我们有更多的变量时,它会变成一个噩梦。对于神经网络,我们通常需要更多的变量——最大的神经网络具有成本函数,它以极其复杂的方式依赖于数十亿的权重和偏差。用微积分来最小化是行不通的!
(在断言我们将通过把C想象成只有两个变量的函数来深入了解它之后,我在两段话中回过头来两次说:“嘿,但是如果它是一个不止两个变量的函数呢?”很抱歉。请相信我,把C想象成两个变量的函数确实很有帮助。有时这幅图会崩溃,最后两段就是关于这种崩溃的。对数学的良好思考通常包括处理多个直觉图像,学习何时使用每个图像合适,何时不合适。
好吧,微积分不管用。幸运的是,这里有一个很好的类比,它说明了一种非常有效的算法。我们首先把我们的功能想象成一个山谷。如果你斜着看上面的布局,那应该不会太难。我们想象一个球沿着斜坡滚下山谷。我们的日常经验告诉我们,这个球最终会滚到谷底。也许我们可以用这个方法来求函数的最小值?我们会随机选择一个(假想的)球的起点,然后模拟球滚到山谷底部时的运动。我们可以简单地通过计算C的导数(或许还有一些二阶导数)来进行这个模拟——这些导数将告诉我们我们需要知道的关于山谷的局部“形状”的一切,因此我们的球应该如何滚动。
根据我刚写的,你可能会认为我们要写出球的牛顿运动方程,考虑摩擦力和重力的影响,等等。实际上,我们不会把滚动球的类比太当真——我们是在设计一个算法来最小化C,而不是开发一个精确的物理定律模拟!俯视是为了激发我们的想象力,而不是限制我们的思维。因此而不是进入所有混乱的物理细节。
更准确地说,让我们想想当我们在 v1的方向轻微的移动Δv1,v2的方向轻微的移动Δv2会发生什么?微积分告诉我们C的变化如下:

我们要找到一种方法选择Δv1和Δv2使ΔC为负;也就是说,这样球就会滚进山谷。找到一种能够定义向量V变化的 Δv的选择:Δv≡(Δv1,Δv2)T,这里T是转置运算,把行向量变成列向量。我们也将C的梯度定义为偏导的向量(∂C∂v1,∂C∂v2)我们通过∇C表示梯度向量,即:

我想澄清一些有时会让人对梯度感到困惑的东西。当会议首次∇C符号,人们有时不知道他们应该如何看待∇符号。那么,确切地说,∇意味着什么?事实上,这是完全可以把∇C作为一个数学对象——上面定义的向量,它是用两个符号。在这个角度看,∇只是一个符号,告诉你“嘿,∇C是一个梯度向量”。有更先进的观点,∇可以被视为一个独立的数学实体本身(例如,作为微分算子),但是我们不需要这样的观点。
有了这些定义,表达式(7)可以写成:

这个方程可以解释为什么∇C称为梯度向量:∇C变化的变化与c和v有关,正如我们期望一个叫做梯度。但真正令人兴奋的方程是,它让我们看到如何选择Δv使ΔC为负数的。特别地,假设我们选择

η是一个较小的,正的参数(即学习速率)。方程(9)可以看出来 ΔC≈−η∇C⋅∇C=−η∥∇C∥2,又 ∥∇C∥2≥0,这使得ΔC≤0,,因此如果我们按照(10)中的公式改变v,则C始终会降低,不会增加。这正是我们想要的!所以我们用方程(10)来定义梯度下降算法中球的“运动定律”。我们将使用方程(10)为Δv计算一个值,然后通过这个数值来移动球的位置:

然后我们将再次使用这个更新规则,以进行另一个操作。如果我们继续这样做,一遍又一遍,我们将继续降低C,直到——我们希望——我们达到一个全局的最小值。
总结,梯度下降算法的工作方式是反复计算梯度∇C,然后向相反的方向移动,沿着斜率滚到山谷。我们可以这样可视化:


请注意,使用这个规则,梯度下降并不能重现真实的物理运动。在现实生活中,球有动量,这种动量可以使它滚过斜坡,甚至(暂时)滚上山坡。只有在摩擦力的作用下,球才能保证滚进山谷。相比之下,我们的规则选择Δv只是说“立刻下降”。这仍然是求最小值的一个很好的规则!
为了让梯度下降法正常工作,我们需要选择学习率η是足够小的,方程(9)是一个很好的近似。如果我们不是这样的,我们可能最终ΔC > 0,这显然不是好!与此同时,我们不希望η太小,因为这将使Δv微小变化,因此梯度下降算法将非常缓慢。在实际实现中,η往往不同,方程(9)仍然是一个好的近似。稍后我们将看到这是如何工作的。
我已经解释了当C是一个只有两个变量的函数时的梯度下降。但是,事实上,即使C是一个包含更多变量的函数,一切都是一样的。特别假设C是m个变量的函数,v1,…,vm。然后一个小变化产生的变化 CΔv =(Δv1,…,Δvm) T。

和我们保证(近似)ΔC表达式(12)将是负的。这为我们提供了一种最小化梯度的方法,即使C是一个包含多个变量的函数,也可以通过反复应用更新规则来实现:

在实践中梯度下降法通常非常有效,在神经网络中,我们会发现它是一种最小化成本函数的强大方法,从而帮助神经网络“学习”。

Implementing our network to classify digits(实现我们的网络去分类数字)

好吧!那就让我们写一个程序,学习如何识别手写数字,使用随机梯度下降和MNIST训练数据。我们将使用一个简短的Python(2.7)程序来实现这一点,只需74行代码!我们需要做的第一件事是获取MNIST
数据。如果您是git用户,那么您可以通过克隆本书的代码库来获取数据代码实现
顺便说一句,当我在前面描述MNIST数据时,我说它被分成6万张训练图像和1万张测试图像。这是官方的描述。实际上,我们将以稍微不同的方式分割数据。我们将离开测试图像,但是60000 -图像MNIST训练集分割成两个部分:一组50000张图片,我们将使用训练神经网络,和一个单独的10000图像验证集。我们不会使用验证的数据在这一章,但后来在书中我们会发现它有用的在搞清楚如何设置神经网络的某些hyper-parameters——诸如学习速率,等等,这不是直接由我们的学习算法。虽然验证数据不是原始MNIST规范的一部分,但许多人以这种方式使用MNIST,而且在神经网络中使用验证数据是很常见的。从现在开始,当我提到“MNIST训练数据”时,我指的是我们的50,000张图像数据集,而不是最初的60,000张图像数据集。
除了MNIST数据之外,我们还需要一个名为Numpy的Python库来进行快速的线性代数运算。
在给出完整的清单之前,让我先解释一下神经网络代码的核心特性。核心是一个网络类,我们用它来表示一个神经网络。下面是我们用来初始化网络对象的代码:

class Network(object):def __init__(self, sizes):self.num_layers = len(sizes)self.sizes = sizesself.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:])]

在此代码中,列表大小包含相应层中的神经元数量。例如,如果我们想创建一个网络对象,在第一层有2个神经元,在第二层有3个神经元,在最后一层有1个神经元,我们可以通过代码来实现:

net = Network([2, 3, 1])

网络对象中的偏差和权重都是使用np.random.randn随机初始化的。生成均值为0,标准差为1的高斯分布。这个随机初始化为我们的随机梯度下降算法提供了一个起点。在后面的章节中,我们将会找到初始化权重和偏差的更好的方法,但是现在先这样做。请注意,网络初始化代码假定神经元的第一层是输入层,并省略了设置这些神经元的任何偏差,因为偏差只用于计算后面几层的输出。
还要注意,偏差和权值存储为Numpy矩阵列表。比如net.weights[1]是一个Numpy矩阵,存储连接第二层和第三层神经元的权值。(它不是第一层和第二层,因为Python的列表索引从0开始。)自净。net.weights[1]相当冗长,我们制用矩阵w表示。wjk是第二层的第k个神经元和第三层的第j个神经元之间连接的权值。j和k的下标顺序可能看起来很奇怪——交换下标j和k肯定更有意义吧?使用这种顺序的最大好处是,它意味着第三层神经元的激活向量为:

在这个等式中有很多东西,所以让我们一块一块地展开它。a是第二层神经元激活的载体。为了得到a '我们用a乘以权重矩阵w,加上向量b的偏差。然后应用函数σ激活(这称为矢量化 σ函数。)我们很容易验证方程(22)给出的结果与我们之前的规则(4)相同,用于计算sigmoid神经元的输出。
定义sigmoid函数:

def sigmoid(z):return 1.0/(1.0+np.exp(-z))##注意,当输入z是向量或Numpy数组时,Numpy会自动应用函数sigmoid

然后,我们将前馈方法添加到网络类中,该方法在给定网络的输入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 stochasticgradient descent.  The "training_data" is a list of tuples"(x, y)" representing the training inputs and the desiredoutputs.  The other non-optional parameters areself-explanatory.  If "test_data" is provided then thenetwork will be evaluated against the test data after eachepoch, and partial progress printed out.  This is useful fortracking 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是您所期望的—训练的epochs的数量,以及采样时使用的批次的大小。eta是学习速率η。如果提供了可选参数test_data,则程序将在每个训练周期之后评估,并打印出部分进度。这对于跟踪进度很有用,但是会大大降低速度。
代码如下所示。在每个epoch中,它首先随机打乱训练数据,然后将其划分为适当大小的批量。这是一种从训练数据中随机抽样的简单方法。然后,对于每个mini_batch,我们应用一个单一的梯度下降步骤。这是由代码本身完成的。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 applyinggradient 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)

这调用了所谓的反向传播算法,这是计算成本函数梯度的一种快速方法。因此,update_mini_batch只需为mini_batch中的每个训练示例计算这些梯度,然后更新self.weights and self.biases 即可。

"""
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 npclass Network(object):def __init__(self, sizes):"""The list ``sizes`` contains the number of neurons in therespective layers of the network.  For example, if the listwas [2, 3, 1] then it would be a three-layer network, with thefirst layer containing 2 neurons, the second layer 3 neurons,and the third layer 1 neuron.  The biases and weights for thenetwork are initialized randomly, using a Gaussiandistribution with mean 0, and variance 1.  Note that the firstlayer is assumed to be an input layer, and by convention wewon't set any biases for those neurons, since biases are onlyever used in computing the outputs from later layers."""self.num_layers = len(sizes)self.sizes = sizesself.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 adef SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None):"""Train the neural network using mini-batch stochasticgradient descent.  The ``training_data`` is a list of tuples``(x, y)`` representing the training inputs and the desiredoutputs.  The other non-optional parameters areself-explanatory.  If ``test_data`` is provided then thenetwork will be evaluated against the test data after eachepoch, and partial progress printed out.  This is useful fortracking 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 applyinggradient 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))*nwfor w, nw in zip(self.weights, nabla_w)]self.biases = [b-(eta/len(mini_batch))*nbfor b, nb in zip(self.biases, nabla_b)]def backprop(self, x, y):"""Return a tuple ``(nabla_b, nabla_w)`` representing thegradient for the cost function C_x.  ``nabla_b`` and``nabla_w`` are layer-by-layer lists of numpy arrays, similarto ``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]# feedforwardactivation = xactivations = [x] # list to store all the activations, layer by layerzs = [] # list to store all the z vectors, layer by layerfor b, w in zip(self.biases, self.weights):z = np.dot(w, activation)+bzs.append(z)activation = sigmoid(z)activations.append(activation)# backward passdelta = self.cost_derivative(activations[-1], y) * \sigmoid_prime(zs[-1])nabla_b[-1] = deltanabla_w[-1] = np.dot(delta, activations[-2].transpose())for l in xrange(2, self.num_layers):z = zs[-l]sp = sigmoid_prime(z)delta = np.dot(self.weights[-l+1].transpose(), delta) * spnabla_b[-l] = deltanabla_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 neuralnetwork outputs the correct result. Note that the neuralnetwork's output is assumed to be the index of whicheverneuron 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数据开始。我将使用一个小助手程序mnist_loader来完成此任务。我们在Python shell中执行以下命令:

>>> import mnist_loader
>>> training_data, validation_data, test_data = \
... mnist_loader.load_data_wrapper(

当然,这也可以在一个单独的Python程序中完成.
加载MNIST数据后,我们将建立一个包含30个隐藏神经元的网络。我们在导入上面列出的Python程序(名为network)之后,

>>> import network
>>> net = network.Network([784, 30, 10])

训练数据集采用30个 epochs, mini-batch size 为10, 学习率 η=3.0:

>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

请注意,如果在阅读过程中运行代码,则需要一些时间来执行—对于典型的机器(从2015年开始),可能需要几分钟来运行。我建议您设置运行状态,继续读取,并定期检查代码的输出。如果你很着急,你可以通过减少epoch的数量,减少隐藏神经元的数量,或者只使用部分训练数据来加速。请注意,生产代码要快得多:这些Python脚本旨在帮助您理解神经网络如何工作,而不是高性能代码!当然,一旦我们训练了一个网络,它就可以在几乎任何计算平台上快速运行。例如,一旦我们了解了网络的一组良好的权重和偏差,就可以很容易地将其移植到web浏览器的Javascript中运行,或作为移动设备上的本机应用程序运行。无论如何,这是神经网络一次训练输出的部分记录。该文本显示了训练后神经网络正确识别的测试图像的个数。

Toward deep learning(朝着深度学习的方向前进)

虽然我们的神经网络提供了令人印象深刻的表现,但这种表现有些神秘。网络中的权值和偏差是自动发现的。这意味着我们不能马上解释网络是如何运作的。我们能找到一些方法来理解我们的网络对手写数字进行分类的原理吗?有了这些原则,我们能做得更好吗?
更直白地说,假设几十年后,神经网络将导致人工智能(AI)的出现。我们能理解这样的智能网络是如何工作的吗?也许这些网络对我们来说是不透明的,它们的权重和偏差我们无法理解,因为它们是自动习得的。在人工智能研究的早期,人们希望构建人工智能的努力也能帮助我们理解智能背后的原理,或许还能帮助我们理解人类大脑的功能。但结果可能是,我们最终既不了解大脑,也不了解人工智能是如何工作的!
为了解决这些问题,让我们回想一下我在本章开始时对人工神经元的解释,作为衡量evidence的一种方式。假设我们想要确定一幅图像是否显示人脸:

我们可以用解决手写识别问题的方法来解决这个问题——使用图像中的像素作为神经网络的输入,而神经网络的输出是单个神经元,表示“是的,这是一张脸”或“不,这不是一张脸”。
假设我们这样做,但是我们没有使用学习算法。相反,我们将尝试手工设计一个网络,选择适当的权重和偏差。我们该怎么做呢?暂时把神经网络完全忘掉,我们可以使用的一种启发式方法是把问题分解成子问题:图像左上角有一只眼睛吗?它的右上角有一只眼睛吗?它中间有个鼻子吗?它的底部中间有嘴吗?上面有头发吗?等等。
如果其中几个问题的答案是“是”,或者仅仅是“可能是”,那么我们就可以得出结论,图像很可能是一张脸。相反,如果大多数问题的答案是“不”,那么图像可能不是脸。
当然,这只是一个粗略的启发,它有许多不足之处。也许这个人是秃头,所以他们没有头发。也许我们只能看到脸的一部分,或者脸是有角度的,所以一些面部特征是模糊的。尽管如此,启发式的建议是,如果我们可以用神经网络来解决子问题,那么也许我们可以通过组合用于子问题的神经网络来构建一个用于人脸检测的神经网络。这是一个可能的架构,用矩形表示子网络。注意,这并不是解决人脸检测问题的现实方法;而是帮助我们建立关于网络如何运作的直觉。体系结构:

子网络也有可能被分解。假设我们在考虑这个问题:“左上角有一只眼睛吗?”这个问题可以分解为:“有眉毛吗?”“有睫毛吗?”;“有鸢尾吗?”等等。当然,这些问题也应该包括位置信息——“眉毛在左上方,在虹膜上方吗?”但是,让我们保持简单。回答“左上方有眼睛吗?”的问题,现在可以分解:

这些问题也可以通过多个层次进一步分解。最后,我们将使用子网络来回答问题,这些问题非常简单,很容易在单个像素级别上回答。例如,这些问题可能是关于图像中特定点上存在或不存在非常简单的形状。这些问题可以由连接到图像原始像素的单个神经元来回答。
最终的结果是一个网络将一个非常复杂的问题——这张图片是否显示了人脸——分解成非常简单的问题,这些问题可以在单个像素的水平上回答。它通过一系列的多层来实现这一点,早期的层回答关于输入图像的非常简单和具体的问题,而后期的层则建立一个更加复杂和抽象概念的层次结构。具有这种多层结构(两个或两个以上的隐层)的网络称为深度神经网络。
当然,我还没有说过如何将这种递归分解成子网络。手工设计网络中的权重和偏差当然是不现实的。相反,我们希望使用学习算法,以便网络能够自动地从训练数据中学习权重和偏差,从而了解概念的层次结构。20世纪80年代和90年代的研究人员尝试使用随机梯度下降和反向传播来训练深度网络。不幸的是,除了一些特殊的建筑,他们没有太多的运气。网络会学习,但非常缓慢,而且在实践中往往过于缓慢,无法发挥作用。
自2006年以来,已经开发了一套技术,使学习在深度神经网络中成为可能。这些深度学习技术基于随机梯度下降和反向传播,但也引入了新的思想。这些技术使更深层(和更大)的网络得以训练——人们现在通常训练5到10个隐藏层的网络。而且,事实证明,在许多问题上,这些神经网络要比浅层的神经网络表现得好得多。当然,原因是深层网络能够建立一个复杂的概念层次结构。这有点像传统编程语言使用模块化设计和抽象思想来创建复杂计算机程序的方式。将深度网络与浅层网络进行比较,有点类似于将编程语言与无法进行此类调用的简化语言进行函数调用的能力进行比较。抽象在神经网络中的形式与传统编程中的不同,但它同样重要。

Deep Learning之手写数字识别相关推荐

  1. Java软件研发工程师转行之深度学习(Deep Learning)进阶:手写数字识别+人脸识别+图像中物体分类+视频分类+图像与文字特征+猫狗分类

    本文适合于对机器学习和数据挖掘有所了解,想深入研究深度学习的读者 1.对概率基本概率有所了解 2.具有微积分和线性代数的基本知识 3.有一定的编程基础(Python) Java软件研发工程师转行之深度 ...

  2. Neural Networks and Deep Learning读书笔记--神经网络应用:手写数字识别

    神经网络应用于MNIST数据手写数字识别 加载MNIST数据,用下面所描述的一小段辅助程序mnist_loader.py来完成.用于存储MNIST数据的数据结构在文档字符串中进行了描述-它是简单的内容 ...

  3. 深度学习(3)手写数字识别问题

    深度学习(3)手写数字识别问题 1. 问题归类 2. 数据集 3. Image 4. Input and Output 5. Regression VS Classification 6. Compu ...

  4. 《神经网络和深度学习》系列文章五:用简单的网络结构解决手写数字识别

    出处: Michael Nielsen的<Neural Network and Deep Learning>,点击末尾"阅读原文"即可查看英文原文. 本节译者:哈工大S ...

  5. 基于TensorFlow深度学习框架,运用python搭建LeNet-5卷积神经网络模型和mnist手写数字识别数据集,设计一个手写数字识别软件。

    本软件是基于TensorFlow深度学习框架,运用LeNet-5卷积神经网络模型和mnist手写数字识别数据集所设计的手写数字识别软件. 具体实现如下: 1.读入数据:运用TensorFlow深度学习 ...

  6. 使用Dl4j训练的一个手写数字识别软件

    DL4J使用之手写数字识别 最近一直在学习深度学习,由于我是Java程序员出身,就选择了一个面向Java的深度学习库-DL4J.为了更加熟练的掌握这个库的使用,我使用该库,以MNIST(http:// ...

  7. 【学习日记】手写数字识别及神经网络基本模型

    2021.10.7 [学习日记]手写数字识别及神经网络基本模型 1 概述 张量(tensor)是数字的容器,是矩阵向任意维度的推广,其维度称为轴(axis).深度学习的本质是对张量做各种运算处理,其分 ...

  8. 手写数字识别系统 基于python

    环境基于Python3.6和Tensorflow框架 实现手写数字识别系统 本文使用python基于TensorFlow设计手写数字识别算法,并编程实现GUI界面,构建手写数字识别系统.文中首先对如何 ...

  9. 基于CNN的手写数字识别

    基于CNN的手写数字识别 文章目录 基于CNN的手写数字识别 零. 写在之前 壹. 聊聊CNN 01. 什么是CNN 02. 为什么要有CNN 03. CNN模型 3.1 卷积层 3.2 池化层 3. ...

最新文章

  1. 前端一HTML:九:css中颜色的表示
  2. Android style 继承
  3. 计算机个性化定制服务课题,服务网络的构建与面向增量式需求的动态定制方法-计算机科学与技术专业论文.docx...
  4. 如何在Chrome浏览器中创建账户?
  5. rtt面向对象oopc——0.类、对象及派生
  6. 持续集成部署Jenkins工作笔记0015---编辑SVN钩子程序
  7. WTL的CBitmapButton在MFC下完美使用
  8. win10系统服务器怎样设置密码,win10系统的电脑如何给普通文件夹设置密码
  9. 美8家最具潜力新公司:在线旅游和新媒体居多
  10. [Excel]如何取得多項式擬合的R平方值(R-squared)?
  11. 谈笑间学会大数据-Hive数据定义
  12. 检查python是否安装成功
  13. [寒江孤叶丶的Cocos2d-x之旅_17]Cocos2d-x 3.2版本以上LUA脚本热更新(动态更新)解决方案
  14. 定义符号常量pi.const float pi= 3.1415926f; 这句话是否正确
  15. 人生无根蒂,飘如陌上尘.
  16. 80C51单片机:5.独立键盘、矩阵键盘检测
  17. 汽车CAN、LIN汇总
  18. Python基础知识汇总和应用示例
  19. 我的.Subtext二次开发之路系列:兵马未动,粮草先行
  20. windows系统怎么开启/禁用驱动程序强制签名

热门文章

  1. 批量下载数据——以TRMM数据为例
  2. mathcad matlab,[讨论] (转载)我为什么特别推MathCAD?
  3. 大数据相关概念-什么是探针
  4. 如何在苹果电脑上装软件
  5. oracle添加分区语句_oracle增加分区的方法
  6. android 直播评论动画,Android自定义View实现直播点赞特效
  7. 宝塔搭建TY博客附好看模板
  8. 计算机网络如何促进幼儿教师专业发展,新学期计算机网络实训报告范文与新幼儿教师的述职报告合集.doc...
  9. opencv Day1
  10. NOI 2005 聪聪可可