人类视觉系统是世界的奇迹之一。考虑以下手写数字的序列:
大多数人毫不费力地将这些数字识别为 504192。这样的轻松是具有误导性的。在我们大脑的每个半球中,人类都有一个主要的视觉皮层,也称为 V1,包含 1.4 亿个神经元,彼此之间有数十亿的连接。然而,人类的视觉不仅涉及 V1,还涉及一系列视觉皮层 - V2、V3、V4 和 V5 - 进行逐渐更复杂的图像处理。我们的大脑中携带着一台超级计算机,经过数亿年的进化调校,极其适应于理解视觉世界。识别手写数字并不容易。相反,我们人类在理解眼睛所展示的内容方面是惊人地、令人震惊地出色。但几乎所有的工作都是在无意识中完成的。因此,我们通常不会意识到我们的视觉系统解决了多么棘手的问题。
视觉模式识别的难度在你尝试编写一个计算机程序来识别像上面那样的数字时变得显而易见。当我们自己做这件事时,似乎很简单,但突然变得极其困难。关于我们如何识别形状的简单直觉——“9的顶部有一个环,右下角有一个竖线”——结果发现并不那么简单以算法形式表达。当你试图使这些规则精确时,你很快就会陷入一片例外、警告和特殊情况的泥潭。似乎毫无希望。
神经网络以不同的方式处理这个问题。其思想是采用大量的手写数字,称为训练样本,
然后开发一个可以从这些训练示例中学习的系统。换句话说,神经网络使用这些示例自动推断识别手写数字的规则。此外,通过增加训练示例的数量,网络可以更多地了解手写,从而提高其准确性。因此,虽然我上面只展示了100个训练数字,但也许我们可以通过使用成千上万甚至数百万或数十亿个训练示例来构建一个更好的手写识别器。
在本章中,我们将编写一个计算机程序,实现一个学习识别手写数字的神经网络。该程序仅有74行代码,并且不使用任何特殊的神经网络库。但这个简短的程序可以在没有人工干预的情况下,以超过96%的准确率识别数字。此外,在后面的章节中,我们将提出可以将准确率提高到99%以上的想法。事实上,最好的商业神经网络现在已经如此优秀,以至于被银行用于处理支票,被邮局用于识别地址。
我们专注于手写识别,因为它是一个优秀的原型问题,可以帮助我们了解神经网络的基本原理。作为一个原型,它恰到好处:它具有挑战性——识别手写数字绝非易事——但又不至于复杂到需要极其复杂的解决方案或巨大的计算能力。此外,这也是开发更先进技术(如深度学习)的绝佳方式。因此,在整本书中,我们将反复回到手写识别的问题。在书的后面,我们将讨论这些思想如何应用于计算机视觉的其他问题,以及在语音、自然语言处理和其他领域中的应用。
当然,如果这一章的重点只是编写一个计算机程序来识别手写数字,那么这一章会短得多!但在这个过程中,我们将发展许多关于神经网络的关键思想,包括两种重要类型的人工神经元(感知器和 sigmoid 神经元),以及神经网络的标准学习算法,称为随机梯度下降。在整个过程中,我专注于解释 为什么 事情是这样做的,并建立你的神经网络直觉。这需要比我仅仅呈现正在发生的基本机制更长的讨论,但为了你将获得的更深刻理解,这是值得的。在这一章结束时,我们将能够理解深度学习是什么,以及它为什么重要。
什么是神经网络?为了开始,我将解释一种叫做 感知器 的人工神经元。感知器是在1950年代和1960年代由科学家 Frank Rosenblatt 开发的,灵感来自于早期 Warren McCulloch 和 Walter Pitts 的 工作。今天,使用其他人工神经元模型更为常见 - 在本书中,以及在许多现代神经网络工作中,主要使用的神经元模型是一个叫做 sigmoid neuron 的模型。我们很快就会讨论 sigmoid 神经元。但是为了理解为什么 sigmoid 神经元是这样定义的,值得花时间先了解感知器。
那么感知器是如何工作的呢?一个感知器接受多个二进制输入,$x ext{1}, x ext{2}, \ldots$,并产生一个单一的二进制输出:
在所示的例子中,感知器有三个输入,$x\_1, x\_2, x\3$。一般来说,它可以有更多或更少的输入。罗森布拉特提出了一个简单的规则来计算输出。他引入了_权重,$w\_1,w\_2,\ldots$,这些实数表示各个输入对输出的重要性。神经元的输出,$0$或$1$,由加权和$\sum\_j w\_j x\_j$是否小于或大于某个_阈值_来决定。就像权重一样,阈值也是一个实数,是神经元的一个参数。用更精确的代数术语来说:\begin{eqnarray} \mbox{输出} & = & \left{ \begin{array}{ll} 0 & \mbox{如果 } \sum\_j w\_j x\_j \leq \mbox{ 阈值} \ 1 & \mbox{如果 } \sum\_j w\_j x\_j > \mbox{ 阈值} \end{array} \right. \tag{1}\end{eqnarray} 这就是感知器工作的全部内容!
这就是基本的数学模型。你可以把感知器看作是一个通过权衡证据来做出决策的装置。让我举个例子。这不是一个非常现实的例子,但很容易理解,我们很快会进入更现实的例子。假设周末要来了,你听说你所在的城市将举行一个奶酪节。你喜欢奶酪,正在考虑是否去参加这个节日。你可能会通过权衡三个因素来做出决定:
- 天气好吗?
- 你的男朋友或女朋友想陪你吗?
- 节日靠近公共交通吗?(你没有车。)
我们可以用相应的二元变量 $x_1, x_2$ 和 $x_3$ 来表示这三个因素。例如,如果天气很好,我们就有 $x_1 = 1$,如果天气不好,则 $x_1 = 0$。类似地,如果你的男朋友或女朋友想去,则 $x_2 = 1$,如果不想去,则 $x_2 = 0$。对于 $x_3$ 和公共交通也是如此。
现在,假设你非常喜欢奶酪,甚至愿意去参加这个节日,即使你的男朋友或女朋友对此不感兴趣,而且这个节日很难到达。但也许你真的很厌恶恶劣的天气,如果天气不好,你绝对不会去参加这个节日。你可以使用感知器来建模这种决策过程。一个方法是为天气选择一个权重 $w ext{}1 = 6$,为其他条件选择 $w ext{}2 = 2$ 和 $w ext{}3 = 2$。$w ext{}1$ 的较大值表明天气对你来说非常重要,远比你的男朋友或女朋友是否陪伴你,或公共交通的便利程度更重要。最后,假设你为感知器选择了一个阈值 $5$。通过这些选择,感知器实现了所需的决策模型,当天气好的时候输出 $1$,而当天气不好的时候输出 $0$。无论你的男朋友或女朋友是否想去,或者公共交通是否在附近,这对输出没有影响。
通过改变权重和阈值,我们可以得到不同的决策模型。例如,假设我们选择一个阈值为 $3$。那么感知器会决定,当天气好的时候 或 当节日靠近公共交通 并且 你的男朋友或女朋友愿意和你一起去时,你应该去节日。换句话说,这将是一个不同的决策模型。降低阈值意味着你更愿意去节日。
显然,感知器并不是人类决策的完整模型!但这个例子说明了感知器如何权衡不同类型的证据以做出决策。而且,复杂的感知器网络能够做出相当微妙的决策,这应该是合理的。
在这个网络中,第一列感知器——我们称之为第一层感知器——通过权衡输入证据做出三个非常简单的决策。那么第二层的感知器呢?每个感知器通过权衡第一层决策的结果来做出决策。通过这种方式,第二层的感知器可以在比第一层感知器更复杂和更抽象的层面上做出决策。甚至第三层的感知器可以做出更复杂的决策。通过这种方式,一个多层的感知器网络可以进行复杂的决策。
顺便提一下,当我定义感知器时,我说过感知器只有一个输出。在上面的网络中,感知器看起来有多个输出。实际上,它们仍然是单输出的。多个输出箭头仅仅是一个有用的方式,表示感知器的输出被用作多个其他感知器的输入。这比画一条单输出线然后再分叉要简单得多。
让我们简化对感知器的描述方式。条件 $\sum _j w _j x _j > \mbox{threshold}$ 繁琐,我们可以进行两个符号上的更改来简化它。第一个更改是将 $\sum t_j w t_j x t_j$ 写成点积,$w \cdot x \equiv \sum t_j w t_j x t_j$,其中 $w$ 和 $x$ 是其分量分别为权重和输入的向量。第二个更改是将阈值移到不等式的另一侧,并用称为感知器的 偏置 的东西替代它,$b \equiv -\mbox{threshold}$。使用偏置代替阈值,感知器规则可以重写为:\begin{eqnarray} \mbox{output} = \left{ \begin{array}{ll} 0 & \mbox{如果 } w\cdot x + b \leq 0 \ 1 & \mbox{如果 } w\cdot x + b > 0 \end{array} \right. \tag{2}\end{eqnarray} 你可以把偏置看作是让感知器输出 $1$ 的难易程度。或者用更生物学的术语来说,偏置是让感知器 发火 的难易程度。对于一个偏置非常大的感知器,感知器输出 $1$ 是非常容易的。但如果偏置非常负,那么感知器输出 $1$ 就很困难。显然,引入偏置只是我们描述感知器方式的一个小变化,但我们稍后会看到这会导致进一步的符号简化。因此,在本书的其余部分,我们将不使用阈值,而是始终使用偏置。
我将感知器描述为一种权衡证据以做出决策的方法。感知器的另一种用法是计算我们通常认为是计算基础的基本逻辑函数,例如 AND
、OR
和 NAND
。例如,假设我们有一个具有两个输入的感知器,每个输入的权重为 $-2$,整体偏置为 $3$。这是我们的感知器:
然后我们看到输入 $00$ 产生输出 $1$,因为 $(-2)*0+(-2)*0+3 = 3$ 是正数。在这里,我引入了 $ imes$ 符号以使乘法变得明确。类似的计算表明输入 $01$ 和 $10$ 产生输出 $1$。但是输入 $11$ 产生输出 $0$,因为 $(-2)*1+(-2)*1+3 = -1$ 是负数。因此我们的感知器实现了一个 NAND
门!
NAND
示例表明,我们可以使用感知器来计算简单的逻辑函数。实际上,我们可以使用感知器网络来计算 任何 逻辑函数。原因在于 NAND
门是通用的计算门,也就是说,我们可以用 NAND
门构建任何计算。例如,我们可以使用 NAND
门构建一个电路来加两个比特,$x
t_1$ 和 $x
t_2$。这需要计算逐位和 $x
t_1 \oplus x
t_2$,以及一个进位比特,当 $x
t_1$ 和 $x
t_2$ 都为 $1$ 时,进位比特被设置为 $1$,即,进位比特就是逐位乘积 $x
t_1 x
t_2$:
为了获得一个等效的感知器网络,我们将所有的 NAND
门替换为具有两个输入的感知器,每个输入的权重为 $-2$,整体偏置为 $3$。这是得到的网络。请注意,我稍微移动了对应于右下角 NAND
门的感知器,以便更容易在图中绘制箭头:
这个感知器网络的一个显著特点是,最左侧的感知器的输出被用作最底部感知器的两次输入。当我定义感知器模型时,并没有说明这种双重输出到同一位置是否被允许。实际上,这并不太重要。如果我们不想允许这种情况,那么可以简单地将两条线合并为一条连接,权重为-4,而不是两条权重为-2的连接。(如果你觉得这不明显,你应该停下来证明给自己看这是等价的。)经过这个更改,网络如下所示,所有未标记的权重均为-2,所有偏置均为3,且有一个标记的权重为-4。
到目前为止,我一直将输入如 $x ext{_1}$ 和 $x ext{_2}$ 画成漂浮在感知机网络左侧的变量。实际上,通常会画出一个额外的感知机层——输入层——来编码输入:
这种输入感知器的表示法,其中我们有一个输出,但没有输入,
是一个简写。它实际上并不意味着没有输入的感知器。要理解这一点,假设我们确实有一个没有输入的感知器。那么加权和 $\sum _j w e_j x e_j$ 将始终为零,因此如果 $b > 0$,感知器将输出 $1$,如果 $b \leq 0$,则输出 $0$。也就是说,感知器将简单地输出一个固定值,而不是期望的值(在上面的例子中是 $x e_1$)。更好的理解方式是将输入感知器视为并不是真正的感知器,而是特殊的单元,它们被简单地定义为输出期望的值 $x e_1, x e_2,\ldots$。
加法器示例演示了如何使用感知器网络来模拟包含多个 NAND
门的电路。由于 NAND
门在计算中是通用的,因此可以推断出感知器在计算中也是通用的。
感知器的计算通用性既让人感到安心又让人失望。让人安心的是,它告诉我们感知器网络可以和任何其他计算设备一样强大。但这也让人失望,因为这似乎使得感知器仅仅是一种新的NAND
门。这可不是什么大新闻!
然而,情况比这个观点所暗示的要好。事实证明,我们可以设计出 学习算法,它们可以自动调整人工神经元网络的权重和偏置。这种调整是对外部刺激的响应,程序员无需直接干预。这些学习算法使我们能够以一种与传统逻辑门截然不同的方式使用人工神经元。我们的神经网络可以简单地学习解决问题,而不是明确地布置一个 NAND
和其他门的电路,有时这些问题是直接设计传统电路极其困难的。
学习算法听起来很棒。但是我们如何为神经网络设计这样的算法呢?假设我们有一个感知器网络,我们希望用它来学习解决某个问题。例如,网络的输入可能是从扫描的手写数字图像中提取的原始像素数据。我们希望网络学习权重和偏差,以便网络的输出正确分类该数字。为了了解学习是如何工作的,假设我们对网络中的某个权重(或偏差)进行小的更改。我们希望这个权重的小变化只会导致网络输出的相应小变化。正如我们稍后将看到的,这一特性将使学习成为可能。示意图上,这就是我们想要的(显然这个网络太简单,无法进行手写识别!):
如果一个权重(或偏置)的微小变化只会导致输出的微小变化,那么我们可以利用这一事实来修改权重和偏置,使我们的网络更符合我们想要的行为。例如,假设网络错误地将一张图像分类为 "8",而它应该是 "9"。我们可以找出如何对权重和偏置进行小的调整,以便网络更接近于将图像分类为 "9"。然后我们会重复这个过程,反复改变权重和偏置,以产生越来越好的输出。网络将会在学习。
问题在于,当我们的网络包含感知器时,情况并非如此。事实上,网络中任何单个感知器的权重或偏置的微小变化,有时会导致该感知器的输出完全翻转,比如从 $0$ 翻转到 $1$。这种翻转可能会导致网络其余部分的行为以某种非常复杂的方式完全改变。因此,虽然你的 "9" 现在可能被正确分类,但网络在所有其他图像上的行为可能已经以某种难以控制的方式完全改变。这使得很难看出如何逐渐修改权重和偏置,以使网络更接近所需的行为。也许有某种巧妙的方法可以解决这个问题。但我们如何让感知器网络学习并不立即显而易见。
我们可以通过引入一种新的人工神经元类型,称为 sigmoid 神经元,来克服这个问题。Sigmoid 神经元类似于感知器,但经过修改,使得它们的权重和偏置的微小变化只会导致输出的微小变化。这是一个关键事实,它将允许一组 sigmoid 神经元进行学习。
好的,让我来描述一下 sigmoid 神经元。我们将以与描绘感知器相同的方式描绘 sigmoid 神经元:
就像感知器一样,sigmoid 神经元有输入,$x _1, x _2, \ldots$。但是,这些输入不仅仅是 $0$ 或 $1$,它们还可以取 $0$ 和 $1$ 之间的任何值。因此,例如,$0.638\ldots$ 是 sigmoid 神经元的有效输入。同样,sigmoid 神经元也为每个输入设置权重,$w _1, w _2, \ldots$,以及一个整体偏置 $b$。但输出不是 $0$ 或 $1$。相反,它是 $\sigma(w \cdot x+b)$,其中 $\sigma$ 被称为 sigmoid 函数。* *顺便提一下,$\sigma$ 有时被称为 logistic 函数,这一新类神经元被称为 logistic 神经元。记住这些术语是有用的,因为许多从事神经网络工作的人使用这些术语。然而,我们将坚持使用 sigmoid 术语。并且定义为:\begin{eqnarray} \sigma(z) \equiv \frac{1}{1+e^{-z}}. \tag{3}\end{eqnarray} 更明确地说,具有输入 $x _1,x _2,\ldots$、权重 $w _1,w _2,\ldots$ 和偏置 $b$ 的 sigmoid 神经元的输出是 \begin{eqnarray} \frac{1}{1+\exp(-\sum _j w _j x _j-b)}. \tag{4}\end{eqnarray}
乍一看,sigmoid 神经元与感知器似乎非常不同。如果你对它不熟悉,sigmoid 函数的代数形式可能显得晦涩难懂。事实上,感知器和 sigmoid 神经元之间有许多相似之处,而 sigmoid 函数的代数形式实际上更多的是一个技术细节,而不是理解的真正障碍。
要理解与感知器模型的相似性,假设 $z \equiv w \cdot x + b$ 是一个很大的正数。那么 $e^{-z} \approx 0$,所以 $\sigma(z) \approx 1$。换句话说,当 $z = w \cdot x+b$ 很大且为正时,sigmoid 神经元的输出大约是 $1$,就像感知器的输出一样。另一方面,假设 $z = w \cdot x+b$ 是非常负的。那么 $e^{-z} \rightarrow \infty$,而 $\sigma(z) \approx 0$。因此,当 $z = w \cdot x +b$ 非常负时,sigmoid 神经元的行为也与感知器非常接近。只有当 $w \cdot x+b$ 的大小适中时,才会与感知器模型有较大的偏差。
$\sigma$ 的代数形式怎么样?我们该如何理解呢?实际上,$\sigma$ 的确切形式并不是那么重要 - 真正重要的是绘制时函数的形状。这里是形状:
这个形状是阶跃函数的平滑版本:
如果 $\sigma$ 实际上是一个阶跃函数,那么 sigmoid 神经元将 是 一个感知器,因为输出将是 $1$ 或 $0$,具体取决于 $w\cdot x+b$ 是正还是负* *实际上,当 $w \cdot x +b = 0$ 时,感知器输出 $0$,而阶跃函数输出 $1$。所以,严格来说,我们需要在那个一点上修改阶跃函数。但你明白我的意思.. 通过使用实际的 $\sigma$ 函数,我们得到了,如上所暗示的,平滑的感知器。实际上,$\sigma$ 函数的平滑性是关键因素,而不是它的详细形式。$\sigma$ 的平滑性意味着权重的微小变化 $\Delta w _j$ 和偏置的微小变化 $\Delta b$ 将导致神经元输出的微小变化 $\Delta \mbox{output}$。事实上,微积分告诉我们 $\Delta \mbox{output}$ 可以很好地近似为 \begin{eqnarray} \Delta \mbox{output} \approx \sum _j \frac{\partial , \mbox{output}}{\partial w t_j} \Delta w t_j + \frac{\partial , \mbox{output}}{\partial b} \Delta b, \tag{5}\end{eqnarray} 其中求和是针对所有权重 $w t_j$,$\partial , \mbox{output} / \partial w t_j$ 和 $\partial , \mbox{output} /\partial b$ 分别表示 $\mbox{output}$ 对 $w t_j$ 和 $b$ 的偏导数。如果你对偏导数不太熟悉,不要惊慌!虽然上面的表达式看起来很复杂,包含所有的偏导数,但它实际上在说一些非常简单的事情(而且这是个好消息):$\Delta \mbox{output}$ 是权重和偏置变化 $\Delta w t_j$ 和 $\Delta b$ 的 线性函数。这种线性使得选择权重和偏置的小变化以实现任何期望的小变化在输出中变得容易。因此,虽然 sigmoid 神经元在定性行为上与感知器有很多相似之处,但它们使得弄清楚如何改变权重和偏置将如何改变输出变得容易得多。
如果$\sigma$的形状才是真正重要的,而不是它的确切形式,那么为什么要使用方程(3)\begin{eqnarray} \sigma(z) \equiv \frac{1}{1+e^{-z}} \nonumber\end{eqnarray}中用于$\sigma$的特定形式呢?事实上,在书的后面,我们会偶尔考虑输出为$f(w \cdot x + b)$的神经元,其中$f(\cdot)$是某个其他的_激活函数_。当我们使用不同的激活函数时,主要变化在于方程(5)\begin{eqnarray} \Delta \mbox{output} \approx \sum _j \frac{\partial , \mbox{output}}{\partial w _j} \Delta w _j + \frac{\partial , \mbox{output}}{\partial b} \Delta b \nonumber\end{eqnarray}中偏导数的特定值会发生变化。事实证明,当我们稍后计算这些偏导数时,使用$\sigma$会简化代数运算,因为指数在求导时具有优美的性质。无论如何,$\sigma$在神经网络的研究中被广泛使用,也是我们在本书中最常用的激活函数。
我们应该如何解读 sigmoid 神经元的输出?显然,感知器和 sigmoid 神经元之间一个很大的区别是,sigmoid 神经元不仅仅输出 $0$ 或 $1$。它们可以输出介于 $0$ 和 $1$ 之间的任何实数,因此像 $0.173\ldots$ 和 $0.689\ldots$ 这样的值都是合法的输出。这在某些情况下是有用的,例如,如果我们想用输出值来表示输入到神经网络的图像中像素的平均强度。但有时这可能会造成麻烦。假设我们希望网络的输出指示“输入图像是 9”或“输入图像不是 9”。显然,如果输出是 $0$ 或 $1$,就像在感知器中那样,做到这一点会更简单。但在实践中,我们可以建立一个约定来处理这个问题,例如,决定将任何至少为 $0.5$ 的输出解释为表示“9”,而任何小于 $0.5$ 的输出解释为表示“不是 9”。我会始终明确说明我们何时使用这样的约定,因此这不应该造成任何混淆。
- 模拟感知器的Sigmoid神经元,第一部分 $\mbox{}$
假设我们将感知器网络中的所有权重和偏置乘以一个正常数 $c > 0$。证明网络的行为不会改变。
- 模拟感知器的Sigmoid神经元,第二部分 $\mbox{}$
假设我们有与上一个问题相同的设置 - 一个感知器网络。假设感知器网络的整体输入已经被选择。我们不需要实际的输入值,只需要输入是固定的。假设权重和偏置是这样的:对于网络中任何特定感知器的输入 $x$,都有 $w \cdot x + b \neq 0$。现在用Sigmoid神经元替换网络中的所有感知器,并将权重和偏置乘以一个正的常数 $c > 0$。证明当 $c \rightarrow \infty$ 时,这个Sigmoid神经元网络的行为与感知器网络完全相同。当 $w \cdot x + b = 0$ 时,这种情况如何会失败?
在下一节中,我将介绍一个可以很好地分类手写数字的神经网络。为此,解释一些术语有助于我们命名网络的不同部分。假设我们有这个网络:
如前所述,这个网络中最左边的层称为输入层,层内的神经元称为 输入神经元。最右边或 输出 层包含 输出神经元,或者在这种情况下,只有一个输出神经元。中间层称为 隐藏层,因为这一层中的神经元既不是输入也不是输出。"隐藏"这个术语听起来可能有点神秘——我第一次听到这个术语时,认为它一定有某种深刻的哲学或数学意义——但它实际上只是意味着"不是输入或输出"。上面的网络只有一个隐藏层,但有些网络有多个隐藏层。例如,以下四层网络有两个隐藏层:
有些令人困惑的是,由于历史原因,这种多层网络有时被称为 多层感知器 或 MLPs,尽管它们是由 sigmoid 神经元构成的,而不是感知器。我在本书中不会使用 MLP 术语,因为我认为这会造成混淆,但我想提醒你它的存在。
网络中输入层和输出层的设计通常是直接的。例如,假设我们试图确定一幅手写图像是否描绘了一个"9"。设计网络的自然方式是将图像像素的强度编码到输入神经元中。如果图像是一个 $64$ x $64$ 的灰度图像,那么我们将有 $4,096 = 64 \times 64$ 个输入神经元,强度适当地缩放在 $0$ 和 $1$ 之间。输出层将只包含一个神经元,输出值小于 $0.5$ 表示 "输入图像不是 9",而大于 $0.5$ 的值表示 "输入图像是 9"。
虽然神经网络的输入层和输出层的设计通常很简单,但隐藏层的设计却有很大的艺术性。特别是,无法用几个简单的经验法则来概括隐藏层的设计过程。相反,神经网络研究人员为隐藏层开发了许多设计启发式方法,这些方法帮助人们从他们的网络中获得所需的行为。例如,这些启发式方法可以用来帮助确定隐藏层的数量与训练网络所需时间之间的权衡。我们将在本书后面介绍几种这样的设计启发式方法。
到目前为止,我们一直在讨论神经网络,其中一层的输出用作下一层的输入。这种网络被称为 前馈 神经网络。这意味着网络中没有循环——信息始终是向前传递的,从不反馈。如果我们确实有循环,我们将会遇到输入到 $\sigma$ 函数依赖于输出的情况。这将很难理解,因此我们不允许这样的循环。
然而,还有其他模型的人工神经网络,其中反馈回路是可能的。这些模型被称为递归神经网络。这些模型的想法是让神经元在有限的时间内发火,然后变得静息。那种发火可以刺激其他神经元,这些神经元可能在稍后发火,同样也是有限的时间。这导致更多的神经元发火,随着时间的推移,我们得到了神经元发火的级联。在这样的模型中,循环不会造成问题,因为神经元的输出只在稍后的时间影响其输入,而不是瞬时的。
递归神经网络的影响力不如前馈网络,部分原因是递归网络的学习算法(至少到目前为止)不够强大。但递归网络仍然非常有趣。它们在精神上更接近我们大脑的工作方式,而不是前馈网络。而且,递归网络可能能够解决一些重要问题,这些问题仅能通过前馈网络以极大的困难解决。然而,为了限制我们的范围,在本书中我们将集中讨论更广泛使用的前馈网络。
在定义了神经网络之后,让我们回到手写识别。我们可以将识别手写数字的问题分成两个子问题。首先,我们希望有一种方法将包含多个数字的图像分解为一系列单独的图像,每个图像包含一个单独的数字。例如,我们希望将图像分解为
分成六个单独的图像,
我们人类轻松解决这个 分割问题,但对于计算机程序来说,正确地分割图像是具有挑战性的。一旦图像被分割,程序还需要对每个单独的数字进行分类。因此,例如,我们希望我们的程序能够识别上面第一个数字,
是一个5。
我们将专注于编写一个程序来解决第二个问题,即对单个数字进行分类。我们这样做是因为一旦你有了一个好的方法来分类单个数字,分割问题就不那么难以解决。解决分割问题有很多方法。一种方法是尝试多种不同的图像分割方式,使用单个数字分类器对每次试验的分割进行评分。如果单个数字分类器对所有分段的分类都很有信心,则试验分割会获得高分;如果分类器在一个或多个分段中遇到很多问题,则会获得低分。这个想法是,如果分类器在某个地方遇到麻烦,那么它可能是因为分割选择不正确。这个想法和其他变体可以很好地解决分割问题。因此,我们将不再担心分割,而是专注于开发一个可以解决更有趣和更困难的问题的神经网络,即识别单个手写数字。
为了识别单个数字,我们将使用一个三层神经网络:
网络的输入层包含编码输入像素值的神经元。如下一节所讨论的,我们的网络训练数据将由许多 $28$ x $28$ 像素的扫描手写数字图像组成,因此输入层包含 $784 = 28 \times 28$ 个神经元。为了简化,我在上面的图中省略了大部分 $784$ 个输入神经元。输入像素为灰度,值为 $0.0$ 表示白色,值为 $1.0$ 表示黑色,而中间的值表示逐渐变暗的灰色阴影。
网络的第二层是一个隐藏层。我们用 $n$ 表示这个隐藏层中的神经元数量,我们将对 $n$ 的不同值进行实验。所示的例子展示了一个小的隐藏层,仅包含 $n = 15$ 个神经元。
网络的输出层包含 10 个神经元。如果第一个神经元激活,即输出 $\approx 1$,那么这将表明网络认为该数字是 $0$。如果第二个神经元激活,那么这将表明网络认为该数字是 $1$。依此类推。更准确地说,我们将输出神经元编号从 $0$ 到 $9$,并找出哪个神经元具有最高的激活值。如果该神经元是,例如,神经元编号 $6$,那么我们的网络将猜测输入的数字是 $6$。其他输出神经元也是如此。
你可能会想知道为什么我们使用 $10$ 个输出神经元。毕竟,网络的目标是告诉我们哪个数字 ($0, 1, 2, \ldots, 9$) 对应于输入图像。一个看似自然的方法是只使用 $4$ 个输出神经元,将每个神经元视为根据神经元的输出更接近 $0$ 还是 $1$ 而取二进制值。四个神经元足以编码答案,因为 $2^4 = 16$ 超过了输入数字的 10 个可能值。为什么我们的网络应该使用 $10$ 个神经元呢?这不是低效吗?最终的理由是经验性的:我们可以尝试这两种网络设计,结果发现,对于这个特定问题,具有 $10$ 个输出神经元的网络比具有 $4$ 个输出神经元的网络更好地学习识别数字。但这让我们想知道 为什么 使用 $10$ 个输出神经元效果更好。是否有某种启发式方法可以提前告诉我们应该使用 $10$ 输出编码而不是 $4$ 输出编码?
要理解我们为什么这样做,首先考虑神经网络从基本原理上在做什么。首先考虑我们使用 $10$ 个输出神经元的情况。我们集中讨论第一个输出神经元,它试图判断数字是否为 $0$。它通过权衡来自隐藏层神经元的证据来做到这一点。那么那些隐藏神经元在做什么呢?好吧,假设为了论证,隐藏层中的第一个神经元检测以下图像是否存在:
它可以通过对与图像重叠的输入像素进行重加权,而对其他输入进行轻加权来实现这一点。以类似的方式,假设为了论证,隐藏层中的第二、第三和第四个神经元检测以下图像是否存在:
正如你所猜测的,这四个图像一起构成了我们在之前的数字行中看到的 $0$ 图像 earlier:
所以如果这四个隐藏神经元都在激活,那么我们可以得出结论,数字是 $0$。当然,这并不是我们得出图像是 $0$ 的 唯一 证据——我们可以通过许多其他方式合法地得到一个 $0$(比如,通过上述图像的平移或轻微扭曲)。但可以安全地说,至少在这种情况下,我们会得出输入是 $0$ 的结论。
假设神经网络以这种方式运作,我们可以给出一个合理的解释,说明为什么从网络中获得 $10$ 个输出比 $4$ 个输出更好。如果我们有 $4$ 个输出,那么第一个输出神经元将试图决定数字的最重要位是什么。而且没有简单的方法将这个最重要位与上面所示的简单形状相关联。很难想象数字的组成形状与(例如)输出中的最重要位有任何良好的历史关系。
现在,既然说了这些,这只是一个启发式方法。没有什么规定三层神经网络必须以我描述的方式运作,隐藏神经元检测简单的组件形状。也许一个聪明的学习算法会找到一些权重的分配,让我们只使用 $4$ 个输出神经元。但作为一种启发式方法,我所描述的思维方式效果很好,并且可以为你在设计良好的神经网络架构时节省很多时间。
- 通过在上述三层网络上添加额外的一层,可以确定数字的按位表示。额外的层将前一层的输出转换为二进制表示,如下图所示。为新的输出层找到一组权重和偏置。假设前 $3$ 层神经元的正确输出在第三层(即旧输出层)具有至少 $0.99$ 的激活,而错误输出的激活小于 $0.01$。
现在我们有了神经网络的设计,它如何学习识别数字呢?我们首先需要一个数据集来学习——一个所谓的训练数据集。我们将使用 MNIST 数据集,它包含数万个手写数字的扫描图像,以及它们的正确分类。MNIST 的名称来源于它是由 NIST 收集的两个数据集的一个修改子集,美国国家标准与技术研究所。以下是一些来自 MNIST 的图像:
正如你所看到的,这些数字实际上与本章开头所展示的数字相同,作为识别的挑战。当然,在测试我们的网络时,我们会要求它识别不在训练集中的图像!
MNIST 数据分为两部分。第一部分包含 60,000 张图像,用作训练数据。这些图像是来自 250 个人的手写样本,其中一半是美国人口普查局的员工,另一半是高中学生。这些图像是灰度图,大小为 28 x 28 像素。MNIST 数据集的第二部分是 10,000 张图像,用作测试数据。同样,这些也是 28 x 28 的灰度图像。我们将使用测试数据来评估我们的神经网络识别数字的能力。为了使这成为一个良好的性能测试,测试数据是从与原始训练数据 不同 的 250 人中提取的(尽管仍然是由人口普查局员工和高中学生组成的群体)。这有助于增强我们对系统能够识别未在训练期间见过的人的数字的信心。
我们将使用符号 $x$ 来表示一个训练输入。将每个训练输入 $x$ 视为一个 $28 \times 28 = 784$ 维向量将是方便的。向量中的每个条目表示图像中单个像素的灰度值。我们将用 $y = y(x)$ 来表示相应的期望输出,其中 $y$ 是一个 $10$ 维向量。例如,如果某个特定的训练图像 $x$ 描绘了一个 $6$,那么 $y(x) = (0, 0, 0, 0, 0, 0, 1, 0, 0, 0)^T$ 是网络的期望输出。请注意,这里的 $T$ 是转置操作,将行向量转换为普通(列)向量。
我们希望得到一个算法,使我们能够找到权重和偏置,以便网络的输出能够近似于 $y(x)$ 对于所有训练输入 $x$。为了量化我们实现这一目标的程度,我们定义了一个 成本函数
- 有时称为 损失 或 目标 函数。我们在本书中始终使用成本函数这个术语,但你应该注意其他术语,因为它们在研究论文和其他神经网络讨论中经常使用。 : \begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum _x | y(x) - a|^2. \tag{6}\end{eqnarray} 在这里,$w$ 表示网络中所有权重的集合,$b$ 表示所有偏置,$n$ 是训练输入的总数,$a$ 是当输入 $x$ 时网络的输出向量,求和是针对所有训练输入 $x$。当然,输出 $a$ 依赖于 $x$、$w$ 和 $b$,但为了保持符号简单,我没有明确指出这种依赖关系。符号 $| v |$ 仅表示向量 $v$ 的通常长度函数。我们将 $C$ 称为 二次 成本函数;它有时也被称为 均方误差 或简称 MSE。检查二次成本函数的形式,我们看到 $C(w,b)$ 是非负的,因为求和中的每一项都是非负的。此外,当 $y(x)$ 对于所有训练输入 $x$ 大致等于输出 $a$ 时,成本 $C(w,b)$ 变得很小,即 $C(w,b) \approx 0$。因此,如果我们的训练算法能够找到权重和偏置,使得 $C(w,b) \approx 0$,那么它就做得很好。相反,当 $C(w,b)$ 很大时,它的表现就不那么好——这意味着 $y(x)$ 对于大量输入并不接近输出 $a$。因此,我们的训练算法的目标将是最小化成本 $C(w,b)$ 作为权重和偏置的函数。换句话说,我们希望找到一组权重和偏置,使成本尽可能小。我们将使用一种称为 梯度下降 的算法来实现这一点。
为什么引入二次成本?毕竟,我们主要关心的是网络正确分类的图像数量吗?为什么不直接尝试最大化这个数字,而不是最小化像二次成本这样的代理度量?问题在于,正确分类的图像数量并不是网络中权重和偏置的平滑函数。在大多数情况下,对权重和偏置进行小的改变不会导致训练图像正确分类数量的任何变化。这使得很难弄清楚如何改变权重和偏置以获得更好的性能。如果我们改用像二次成本这样的平滑成本函数,结果发现很容易弄清楚如何对权重和偏置进行小的改变,从而改善成本。这就是为什么我们首先专注于最小化二次成本,只有在那之后我们才会检查分类准确性。
即使考虑到我们想使用平滑的成本函数,你可能仍然会想知道为什么我们选择了方程 (6)\begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum t_x | y(x) - a|^2 \nonumber\end{eqnarray} 中使用的二次函数。这不是一个相当 ad hoc 的选择吗?也许如果我们选择了不同的成本函数,我们会得到一组完全不同的最小化权重和偏差?这是一个有效的担忧,稍后我们将重新审视成本函数,并进行一些修改。然而,方程 (6)\begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum t_x | y(x) - a|^2 \nonumber\end{eqnarray} 的二次成本函数对于理解神经网络学习的基础非常有效,因此我们暂时将其保留。
回顾一下,我们在训练神经网络时的目标是找到能够最小化二次成本函数 $C(w, b)$ 的权重和偏置。这是一个良好定义的问题,但目前的表述中有很多分散注意力的结构——将 $w$ 和 $b$ 解释为权重和偏置,潜伏在背景中的 $\sigma$ 函数,网络架构的选择,MNIST,等等。事实证明,通过忽略大部分结构,我们可以理解大量内容,而只是专注于最小化方面。因此,现在我们将忘记成本函数的具体形式、与神经网络的联系等等。相反,我们将想象我们只是得到了一个多变量的函数,我们想要最小化这个函数。我们将开发一种称为 梯度下降 的技术,可以用来解决这样的最小化问题。然后我们将回到我们想要为神经网络最小化的具体函数。
好的,假设我们正在尝试最小化某个函数,$C(v)$。这可以是任何多变量的实值函数,$v = v\_1, v\_2, \ldots$。请注意,我用$v$替换了$w$和$b$的符号,以强调这可以是任何函数——我们不再特指神经网络的上下文。为了最小化$C(v)$,想象$C$作为仅有两个变量的函数会有所帮助,我们将其称为$v\_1$和$v\_2$:
我们想要的是找到 $C$ 实现其全局最小值的位置。当然,对于上面绘制的函数,我们可以通过观察图形找到最小值。从这个意义上说,我可能展示了一个稍微 过于 简单的函数!一个一般的函数 $C$ 可能是一个复杂的多变量函数,通常无法仅通过观察图形来找到最小值。
解决这个问题的一种方法是使用微积分来尝试从分析上找到最小值。我们可以计算导数,然后尝试利用它们找到 $C$ 是极值的地方。如果运气好,当 $C$ 只是一个或几个变量的函数时,这可能会奏效。但当我们有更多变量时,这将变成一场噩梦。对于神经网络,我们通常会希望有 远 更多的变量——最大的神经网络的成本函数依赖于数十亿个权重和偏置,以极其复杂的方式。使用微积分来最小化这一点根本行不通!
(在断言我们通过将 $C$ 想象为仅仅两个变量的函数来获得洞察之后,我在两个段落中转了两次,问道:“嘿,但如果它是多个变量的函数呢?”对此我感到抱歉。请相信我,当我说将 $C$ 想象为两个变量的函数确实有帮助。这只是有时这种想象会崩溃,而最后两个段落正是在处理这种崩溃。对数学的良好思考通常涉及 juggling 多个直观图像,学习何时适合使用每个图像,以及何时不适合使用。)
好的,所以微积分不管用。幸运的是,有一个美丽的类比,它建议了一种效果不错的算法。我们首先将我们的函数视为一种山谷。如果你稍微眯起眼睛看上面的图,这应该不难。我们想象一个球在山谷的坡道上滚下去。我们日常的经验告诉我们,球最终会滚到山谷的底部。也许我们可以用这个想法来寻找函数的最小值?我们会随机选择一个(假想的)球的起始点,然后模拟球滚到山谷底部的运动。我们可以通过计算 $C$ 的导数(也许还有一些二阶导数)来简单地进行这个模拟——这些导数会告诉我们关于山谷局部“形状”的一切,因此也就知道我们的球应该如何滚动。
根据我刚才写的内容,你可能会认为我们将尝试写下球的牛顿运动方程,考虑摩擦和重力等影响。实际上,我们并不会那么认真对待球的滚动类比——我们是在设计一个算法来最小化 $C$,而不是开发一个准确的物理定律模拟!球的视角旨在激发我们的想象力,而不是限制我们的思维。因此,与其深入探讨物理的所有复杂细节,不如问问自己:如果我们被宣告为一天的上帝,能够制定自己的物理定律,指示球如何滚动,我们可以选择什么样的运动定律,使得球总是滚到山谷底部?
为了使这个问题更精确,让我们考虑当我们在 $v _1$ 方向上移动球体一个小量 $\Delta v _1$,以及在 $v _2$ 方向上移动一个小量 $\Delta v _2$ 时会发生什么。微积分告诉我们 $C$ 的变化如下: \begin{eqnarray} \Delta C \approx \frac{\partial C}{\partial v _1} \Delta v _1 + \frac{\partial C}{\partial v _2} \Delta v _2. \tag{7}\end{eqnarray} 我们将找到一种选择 $\Delta v _1$ 和 $\Delta v _2$ 的方法,使得 $\Delta C$ 为负;也就是说,我们将选择它们,使得球体向山谷滚落。为了弄清楚如何做出这样的选择,定义 $\Delta v$ 为 $v$ 的变化向量,$\Delta v \equiv (\Delta v _1, \Delta v _2)^T$,其中 $T$ 再次是转置操作,将行向量变为列向量。我们还将定义 $C$ 的 梯度 为偏导数的向量,$\left(\frac{\partial C}{\partial v _1}, \frac{\partial C}{\partial v _2}\right)^T$。我们用 $\nabla C$ 表示梯度向量,即: \begin{eqnarray} \nabla C \equiv \left( \frac{\partial C}{\partial v _1}, \frac{\partial C}{\partial v _2} \right)^T. \tag{8}\end{eqnarray} 稍后我们将用 $\Delta v$ 和梯度 $\nabla C$ 来重写变化 $\Delta C$。不过在此之前,我想澄清一些有时会让人对梯度感到困惑的事情。当第一次遇到 $\nabla C$ 符号时,人们有时会想知道他们应该如何理解 $\nabla$ 符号。究竟 $\nabla$ 是什么意思?实际上,将 $\nabla C$ 视为一个单一的数学对象——上述定义的向量——是完全可以的,它恰好是用两个符号书写的。从这个角度来看,$\nabla$ 只是一个符号标记,告诉你“嘿,$\nabla C$ 是一个梯度向量”。还有更高级的观点,其中 $\nabla$ 可以被视为一个独立的数学实体(例如,作为一个微分算子),但我们不需要这样的观点。
根据这些定义,表达式 (7)\begin{eqnarray} \Delta C \approx \frac{\partial C}{\partial v _1} \Delta v _1 + \frac{\partial C}{\partial v _2} \Delta v e_2 \nonumber\end{eqnarray} 对于 $\Delta C$ 可以重写为 \begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v. \tag{9}\end{eqnarray} 这个方程帮助解释了为什么 $\nabla C$ 被称为梯度向量:$\nabla C$ 将 $v$ 的变化与 $C$ 的变化联系起来,就像我们期望梯度所做的那样。但这个方程真正令人兴奋的是,它让我们看到如何选择 $\Delta v$ 使得 $\Delta C$ 为负。特别是,假设我们选择 \begin{eqnarray} \Delta v = -\eta \nabla C, \tag{10}\end{eqnarray} 其中 $\eta$ 是一个小的正参数(称为 学习率)。然后方程 (9)\begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v \nonumber\end{eqnarray} 告诉我们 $\Delta C \approx -\eta \nabla C \cdot \nabla C = -\eta |\nabla C|^2$。因为 $| \nabla C |^2 \geq 0$,这保证了 $\Delta C \leq 0$,即如果我们按照 (10)\begin{eqnarray} \Delta v = -\eta \nabla C \nonumber\end{eqnarray} 中的规定改变 $v$,$C$ 将始终减少,而不会增加。(当然,在方程 (9)\begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v \nonumber\end{eqnarray} 的近似限制内)。这正是我们想要的特性!因此,我们将采用方程 (10)\begin{eqnarray} \Delta v = -\eta \nabla C \nonumber\end{eqnarray} 来定义我们梯度下降算法中球的“运动法则”。也就是说,我们将使用方程 (10)\begin{eqnarray} \Delta v = -\eta \nabla C \nonumber\end{eqnarray} 来计算 $\Delta v$ 的值,然后将球的位置 $v$ 按该值移动: \begin{eqnarray} v \rightarrow v' = v -\eta \nabla C. \tag{11}\end{eqnarray} 然后我们将再次使用这个更新规则,进行另一次移动。如果我们不断这样做,我们将不断减少 $C$,直到 - 我们希望 - 达到全局最小值。
总而言之,梯度下降算法的工作方式是反复计算梯度 $\nabla C$,然后朝着 相反 的方向移动,"沿着山谷的坡度下滑"。我们可以这样可视化它:
注意到使用这个规则时,梯度下降并不再现真实的物理运动。在现实生活中,球体具有动量,而这个动量可能使它在斜坡上滚动,甚至(暂时)向上滚动。只有在摩擦力的影响开始显现后,球体才会被保证滚入山谷。相比之下,我们选择 $\Delta v$ 的规则只是说“现在就往下走”。这仍然是一个相当不错的寻找最小值的规则!
为了使梯度下降正确工作,我们需要选择学习率 $\eta$ 足够小,以使方程 (9)\begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v \nonumber\end{eqnarray} 是一个良好的近似。如果我们不这样做,可能会导致 $\Delta C > 0$,这显然不好!与此同时,我们也不希望 $\eta$ 太小,因为这会使得变化 $\Delta v$ 微小,从而使梯度下降算法工作得非常缓慢。在实际应用中,$\eta$ 通常会有所变化,以使方程 (9)\begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v \nonumber\end{eqnarray} 仍然是一个良好的近似,但算法不会太慢。我们稍后会看到这是如何工作的。
我已经解释了当 $C$ 仅是两个变量的函数时的梯度下降。但实际上,即使 $C$ 是多个变量的函数,一切也同样有效。假设特别地,$C$ 是 $m$ 个变量 $v_1,\ldots,v_m$ 的函数。那么,由于一个小的变化 $\Delta v = (\Delta v_1, \ldots, \Delta v_m)^T$ 而导致的 $C$ 的变化 $\Delta C$ 是 \begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v, \tag{12}\end{eqnarray} 其中梯度 $\nabla C$ 是向量 \begin{eqnarray} \nabla C \equiv \left(\frac{\partial C}{\partial v_1}, \ldots, \frac{\partial C}{\partial v_m}\right)^T. \tag{13}\end{eqnarray} 就像在两个变量的情况下,我们可以选择 \begin{eqnarray} \Delta v = -\eta \nabla C, \tag{14}\end{eqnarray} 并且我们可以保证我们的(近似)表达式 (12)\begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v \nonumber\end{eqnarray} 对于 $\Delta C$ 将是负的。这为我们提供了一种沿着梯度到达最小值的方法,即使 $C$ 是多个变量的函数,通过反复应用更新规则 \begin{eqnarray} v \rightarrow v' = v-\eta \nabla C. \tag{15}\end{eqnarray} 你可以将这个更新规则视为 定义 梯度下降算法。它为我们提供了一种反复改变位置 $v$ 的方法,以找到函数 $C$ 的最小值。这个规则并不总是有效 - 有几件事情可能出错,阻止梯度下降找到 $C$ 的全局最小值,这是我们将在后面的章节中回到探讨的一个点。但在实践中,梯度下降通常效果极佳,在神经网络中,我们会发现它是最小化成本函数的强大方法,从而帮助网络学习。
确实,从某种意义上说,梯度下降是寻找最小值的最佳策略。假设我们试图在位置上移动 $\Delta v$,以尽可能减少 $C$。这相当于最小化 $\Delta C \approx \nabla C \cdot \Delta v$。我们将限制移动的大小,使得 $| \Delta v | = \epsilon$,其中 $\epsilon > 0$ 是一个小的固定值。换句话说,我们希望移动的步伐是一个固定大小的小步,并且我们试图找到一个方向,使得 $C$ 尽可能减少。可以证明,最小化 $\nabla C \cdot \Delta v$ 的 $\Delta v$ 选择是 $\Delta v = - \eta \nabla C$,其中 $\eta = \epsilon / |\nabla C|$ 是由大小约束 $|\Delta v| = \epsilon$ 决定的。因此,梯度下降可以被视为在最能立即减少 $C$ 的方向上采取小步的方式。
- 证明最后一段的断言。 提示: 如果你还不熟悉Cauchy-Schwarz不等式,你可能会发现熟悉它是有帮助的。
- 我解释了当 $C$ 是两个变量的函数时的梯度下降,以及当它是多个变量的函数时的情况。当 $C$ 只是一个变量的函数时会发生什么?你能提供一个关于梯度下降在一维情况下所做的几何解释吗?
人们研究了许多梯度下降的变体,包括更接近真实物理球的变体。这些模仿球的变体有一些优点,但也有一个主要缺点:计算 $C$ 的二阶偏导数是必要的,而这可能相当昂贵。要理解为什么这很昂贵,假设我们想计算所有的二阶偏导数 $\partial^2 C/ \partial v _j \partial v _k$。如果有一百万个这样的 $v e_j$ 变量,那么我们需要计算大约一万亿(即一百万的平方)个二阶偏导数* *实际上,更像是五千亿,因为 $\partial^2 C/ \partial v e_j \partial v e_k = \partial^2 C/ \partial v e_k \partial v e_j$。不过,你明白我的意思。这将是计算上昂贵的。话虽如此,确实有一些技巧可以避免这种问题,寻找梯度下降的替代方案是一个活跃的研究领域。但在本书中,我们将使用梯度下降(及其变体)作为我们在神经网络中学习的主要方法。
我们如何将梯度下降应用于神经网络的学习?这个想法是使用梯度下降来找到权重 $w _k$ 和偏置 $b _l$,以最小化方程 (6)\begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum _x | y(x) - a|^2 \nonumber\end{eqnarray}。为了了解这如何工作,让我们重述梯度下降更新规则,用权重和偏置替换变量 $v _j$。换句话说,我们的“位置”现在有组件 $w e_k$ 和 $b e_l$,而梯度向量 $\nabla C$ 具有相应的组件 $\partial C / \partial w e_k$ 和 $\partial C / \partial b e_l$。将梯度下降更新规则写成组件形式,我们有 \begin{eqnarray} w e_k & \rightarrow & w e_k' = w e_k-\eta \frac{\partial C}{\partial w e_k} \tag{16}\ b e_l & \rightarrow & b e_l' = b e_l-\eta \frac{\partial C}{\partial b e_l}. \tag{17}\end{eqnarray} 通过反复应用这个更新规则,我们可以“沿着山坡滚下”,并希望找到成本函数的最小值。换句话说,这是一个可以用于在神经网络中学习的规则。
在应用梯度下降规则时存在许多挑战。我们将在后面的章节中深入探讨这些问题。但现在我只想提到一个问题。为了理解这个问题是什么,让我们回顾一下方程 (6)\begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum t_x | y(x) - a|^2 \nonumber\end{eqnarray} 中的二次成本。注意,这个成本函数的形式是 $C = \frac{1}{n} \sum t_x C t_x$,也就是说,它是对个别训练样本的成本 $C t_x \equiv \frac{|y(x)-a|^2}{2}$ 的平均值。在实践中,为了计算梯度 $\nabla C$,我们需要分别为每个训练输入 $x$ 计算梯度 $\nabla C t_x$,然后对它们进行平均,$\nabla C = \frac{1}{n} \sum t_x \nabla C t_x$。不幸的是,当训练输入的数量非常大时,这可能需要很长时间,因此学习过程变得缓慢。
一个叫做 随机梯度下降 的想法可以用来加速学习。这个想法是通过计算一小部分随机选择的训练输入的 $\nabla C ext{_}x$ 来估计梯度 $\nabla C$。通过对这小部分进行平均,结果表明我们可以快速获得真实梯度 $\nabla C$ 的良好估计,这有助于加速梯度下降,从而加速学习。
为了使这些想法更精确,随机梯度下降通过随机挑选出少量 $m$ 个随机选择的训练输入来工作。我们将这些随机训练输入标记为 $X\1, X\2, \ldots, X\m$,并称之为 mini-batch。只要样本大小 $m$ 足够大,我们期望 $\nabla C\{X\j}$ 的平均值大致等于所有 $\nabla C\x$ 的平均值,即,\begin{eqnarray} \frac{\sum\{j=1}^m \nabla C\{X\{j}}}{m} \approx \frac{\sum\x \nabla C\x}{n} = \nabla C, \tag{18}\end{eqnarray} 其中第二个求和是针对整个训练数据集。交换两边,我们得到 \begin{eqnarray} \nabla C \approx \frac{1}{m} \sum\{j=1}^m \nabla C\{X\{j}}, \tag{19}\end{eqnarray} 确认我们可以通过仅计算随机选择的 mini-batch 的梯度来估计整体梯度。
为了将其明确地与神经网络中的学习联系起来,假设 $w\_k$ 和 $b\_l$ 表示我们神经网络中的权重和偏置。然后随机梯度下降通过选择一个随机选择的迷你批次的训练输入来工作,并用这些进行训练, \begin{eqnarray} w\_k & \rightarrow & w\_k' = w\_k-\frac{\eta}{m} \sum\j \frac{\partial C\{X\_j}}{\partial w\_k} \tag{20} \ b\_l & \rightarrow & b\_l' = b\_l-\frac{\eta}{m} \sum\j \frac{\partial C\{X\_j}}{\partial b\_l}, \tag{21}\end{eqnarray} 其中求和是在当前迷你批次中的所有训练示例 $X\_j$ 上进行的。然后我们选择另一个随机选择的迷你批次并用这些进行训练。如此等等,直到我们耗尽训练输入,这被称为完成一个 epoch 的训练。此时我们重新开始一个新的训练周期。
顺便提一下,值得注意的是,关于成本函数的缩放和对权重和偏置的迷你批量更新的约定各不相同。在方程 (6)\begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum t_x | y(x) - a|^2 \nonumber\end{eqnarray} 中,我们将整体成本函数缩放了一个因子 $\frac{1}{n}$。人们有时会省略 $\frac{1}{n}$,而是对单个训练示例的成本进行求和,而不是取平均。这在总的训练示例数量事先未知时特别有用。例如,如果实时生成更多的训练数据,就可能发生这种情况。而且,以类似的方式,迷你批量更新规则 (20)\begin{eqnarray} w t_k & \rightarrow & w t_k' = w t_k-\frac{\eta}{m} \sum t_j \frac{\partial C t_{X t_j}}{\partial w t_k} \nonumber\end{eqnarray} 和 (21)\begin{eqnarray} b t_l & \rightarrow & b t_l' = b t_l-\frac{\eta}{m} \sum t_j \frac{\partial C t_{X t_j}}{\partial b t_l} \nonumber\end{eqnarray} 有时会省略求和前的 $\frac{1}{m}$ 项。从概念上讲,这没有太大区别,因为这相当于重新缩放学习率 $\eta$。但是在对不同工作的详细比较中,值得注意这一点。
我们可以将随机梯度下降视为类似于政治民意调查:抽取一个小的迷你批次要比对整个批次应用梯度下降容易得多,就像进行民意调查比进行全面选举要容易。例如,如果我们有一个大小为 $n = 60,000$ 的训练集,如 MNIST,并选择一个迷你批次大小(比如) $m = 10$,这意味着我们在估计梯度时将获得 $6,000$ 倍的加速!当然,估计不会是完美的 - 会有统计波动 - 但它不需要是完美的:我们真正关心的只是朝着一个有助于减少 $C$ 的一般方向移动,这意味着我们不需要对梯度进行精确计算。在实践中,随机梯度下降是一种常用且强大的神经网络学习技术,它是我们在本书中将要开发的大多数学习技术的基础。
- 梯度下降的一个极端版本是使用仅为1的迷你批量大小。也就是说,给定一个训练输入,$x$,我们根据规则 $w"_k \rightarrow w"_k' = w"_k - \eta \partial C"_x / \partial w"_k$ 和 $b"_l \rightarrow b"_l' = b"_l - \eta \partial C"_x / \partial b"_l$ 更新我们的权重和偏置。然后我们选择另一个训练输入,再次更新权重和偏置。如此反复。这一过程被称为 在线、on-line 或 增量 学习。在在线学习中,神经网络一次只从一个训练输入中学习(就像人类一样)。与迷你批量大小为20的随机梯度下降相比,在线学习的一个优点和一个缺点是什么?
让我通过讨论一个有时会困扰新接触梯度下降的人们的观点来结束这一部分。在神经网络中,成本 $C$ 当然是许多变量的函数——所有的权重和偏差——因此在某种意义上定义了一个在非常高维空间中的表面。有些人会困惑地想:"嘿,我必须能够可视化所有这些额外的维度"。他们可能会开始担心:"我无法在四维中思考,更不用说五维(或五百万维)了"。他们是否缺少某种特殊的能力,某种“真正”的超级数学家所拥有的能力?当然,答案是否定的。即使大多数专业数学家也无法很好地可视化四维,甚至根本无法做到。他们使用的技巧是发展其他方式来表示正在发生的事情。这正是我们在上面所做的:我们使用了代数(而不是视觉)表示法来计算 $\Delta C$,以找出如何移动以减少 $C$。擅长高维思考的人有一个包含许多不同技术的心理库;我们的代数技巧只是一个例子。这些技术可能没有我们在可视化三维时所习惯的简单性,但一旦你建立了这样的技术库,你就可以在高维思考中变得相当出色。我在这里不会详细说明,但如果你感兴趣,你可能会喜欢阅读 这篇讨论,其中讨论了一些专业数学家用来在高维中思考的技术。虽然讨论的一些技术相当复杂,但许多最佳内容是直观且易于理解的,任何人都可以掌握。
好的,让我们写一个程序,学习如何识别手写数字,使用随机梯度下降和 MNIST 训练数据。我们将用一个简短的 Python (2.7) 程序来实现,仅需 74 行代码!我们需要的第一件事是获取 MNIST 数据。如果你是 git 用户,那么你可以通过克隆本书的代码库来获取数据,
git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
如果你不使用 git,那么你可以在 这里 下载数据和代码。
顺便提一下,当我之前描述 MNIST 数据时,我说它被分为 60,000 张训练图像和 10,000 张测试图像。这是官方的 MNIST 描述。实际上,我们将以稍微不同的方式分割数据。我们将保持测试图像不变,但将 60,000 张图像的 MNIST 训练集分为两部分:一组 50,000 张图像,我们将用来训练我们的神经网络,另一组是单独的 10,000 张图像的 验证集。我们在本章中不会使用验证数据,但在书的后面我们会发现它在确定神经网络某些 超参数 的设置时非常有用——例如学习率等,这些并不是由我们的学习算法直接选择的。尽管验证数据不是原始 MNIST 规范的一部分,但许多人以这种方式使用 MNIST,使用验证数据在神经网络中是很常见的。从现在开始,当我提到 "MNIST 训练数据" 时,我指的是我们的 50,000 张图像数据集,而不是原始的 60,000 张图像数据集。* *如前所述,MNIST 数据集是基于美国国家标准与技术研究院(NIST)收集的两个数据集。为了构建 MNIST,NIST 数据集被简化并由 Yann LeCun、Corinna Cortes 和 Christopher J. C. Burges 转换为更方便的格式。有关更多详细信息,请参见 this link。我存储库中的数据集以便于在 Python 中加载和操作 MNIST 数据的形式存在。我从蒙特利尔大学的 LISA 机器学习实验室获得了这种特定形式的数据 (link)。
在给出完整代码列表之前,让我解释一下神经网络代码的核心特性。中心是一个 Network 类,我们用它来表示一个神经网络。以下是我们用来初始化 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 包含各层中神经元的数量。因此,例如,如果我们想创建一个 Network 对象,第一层有 2 个神经元,第二层有 3 个神经元,最后一层有 1 个神经元,我们可以使用以下代码来实现:Network 对象中的偏置和权重都是随机初始化的,使用 Numpy 的 np.random.randn 函数生成均值为 $0$ 和标准差为 $1$ 的高斯分布。这种随机初始化为我们的随机梯度下降算法提供了一个起点。在后面的章节中,我们会找到更好的初始化权重和偏置的方法,但现在这样就可以了。请注意,Network 初始化代码假设第一层神经元是输入层,并且省略了为这些神经元设置任何偏置,因为偏置仅在计算后续层的输出时使用。
请注意,偏差和权重被存储为 Numpy 矩阵的列表。因此,例如 net.weights[1] 是一个 Numpy 矩阵,存储连接第二层和第三层神经元的权重。(这不是第一层和第二层,因为 Python 的列表索引从 0 开始。)由于 net.weights[1] 相当冗长,我们就用矩阵 $w$ 来表示它。它是一个矩阵,使得 $w_{jk}$ 是第二层中第 $k^{\rm th}$ 个神经元与第三层中第 $j^{\rm th}$ 个神经元之间连接的权重。这种 $j$ 和 $k$ 索引的顺序可能看起来很奇怪——当然,交换 $j$ 和 $k$ 索引会更有意义?使用这种顺序的一个重大优势是,这意味着第三层神经元的激活向量是:\begin{eqnarray} a' = \sigma(w a + b). \tag{22}\end{eqnarray} 这个方程中有很多内容,所以让我们逐步解析。$a$ 是第二层神经元的激活向量。为了获得 $a'$,我们将 $a$ 乘以权重矩阵 $w$,并加上偏差向量 $b$。然后我们对向量 $w a + b$ 中的每个条目逐元素应用函数 $\sigma$。(这被称为对函数 $\sigma$ 进行 向量化。)很容易验证方程 (22)\begin{eqnarray} a' = \sigma(w a + b) \nonumber\end{eqnarray} 给出的结果与我们之前的规则方程 (4)\begin{eqnarray} \frac{1}{1+\exp(-\sum _j w _j x e_j-b)} \nonumber\end{eqnarray} 计算 sigmoid 神经元输出的结果相同。
考虑到这一切,编写代码以计算来自网络实例的输出变得很简单。我们首先定义 sigmoid 函数:
def sigmoid(z): return 1.0/(1.0+np.exp( -z))
请注意,当输入 z 是一个向量或 Numpy 数组时,Numpy 会自动逐元素应用 sigmoid 函数,即以向量化的形式。
我们接着在 Network 类中添加一个前馈方法,该方法在给定网络输入 a 的情况下,返回相应的输出。**假设输入 a 是一个 (n, 1) 的 Numpy ndarray,而不是 (n,) 向量。这里,n 是网络的输入数量。如果你尝试使用 (n,) 向量作为输入,你会得到奇怪的结果。虽然使用 (n,) 向量看起来是更自然的选择,但使用 (n, 1) 的 ndarray 使得修改代码以一次前馈多个输入变得特别简单,这有时是方便的。该方法所做的就是对每一层应用方程 (22)\begin{eqnarray} a' = \sigma(w a + b) \nonumber\end{eqnarray}:
def feedforward(self, a): """如果"a"是输入,返回网络的输出。""" 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): """使用小批量随机梯度下降训练神经网络。 "training_data" 是一个元组列表 "(x, y)",表示训练输入和期望输出。其他非可选参数不言自明。如果提供了 "test_data",则网络将在每个纪元后对测试数据进行评估,并打印出部分进度。这对于跟踪进度很有用,但会显著减慢速度。""" 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)
训练数据是一个元组列表 (x, y),表示训练输入和相应的期望输出。变量 epochs 和 mini_batch_size 是你所期望的 - 训练的轮数,以及在采样时使用的小批量的大小。eta 是学习率,$\eta$。如果提供了可选参数 test_data,则程序将在每个训练轮次后评估网络,并打印出部分进度。这对于跟踪进度很有用,但会显著减慢速度。
代码的工作原理如下。在每个周期,它首先随机打乱训练数据,然后将其划分为适当大小的迷你批次。这是一种从训练数据中随机抽样的简单方法。然后,对于每个迷你批次,我们应用一次梯度下降的单步。这是通过代码 self.update _mini_batch(mini_batch, eta) 完成的,该代码根据梯度下降的单次迭代更新网络权重和偏差,仅使用迷你批次中的训练数据。以下是 update_mini_batch 方法的代码:
def update _mini_batch(self, mini_batch, eta): """通过应用梯度下降和反向传播来更新网络的权重和偏置,使用单个小批量。 "mini_batch" 是一个元组列表 "(x, y)",而 "eta" 是学习率。""" 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 tiny_batch 通过计算 mini_batch 中每个训练示例的这些梯度来简单地工作,然后适当地更新 self.weights 和 self.biases。
我现在不打算展示 self.backprop 的代码。我们将在下一章学习反向传播是如何工作的,包括 self.backprop 的代码。现在,只需假设它按所声称的那样运行,为与训练示例 x 相关的成本返回适当的梯度。
让我们看看完整的程序,包括我上面省略的文档字符串。除了 self.backprop,程序是自解释的——所有的重任都在 self.SGD 和 self.update\_mini\_batch 中完成,我们已经讨论过了。self.backprop 方法利用了一些额外的函数来帮助计算梯度,即 sigmoid_prime,它计算 $\sigma$ 函数的导数,以及 self.cost_derivative,我在这里不做描述。你可以通过查看代码和文档字符串来了解这些(也许还有细节)。我们将在下一章详细讨论它们。请注意,虽然程序看起来很长,但大部分代码是文档字符串,旨在使代码易于理解。实际上,程序仅包含 74 行非空白、非注释代码。所有代码可以在 GitHub 这里 找到。
这个程序对手写数字的识别效果如何?好吧,让我们先加载 MNIST 数据。我将使用一个小助手程序 mnist_loader.py 来完成这个任务,下面会对此进行描述。我们在 Python shell 中执行以下命令,
>>> import mnist_loader
>>> training_data, validation_data, test_data =
... mnist_loader.load_data_wrapper()
当然,这也可以在一个单独的 Python 程序中完成,但如果你在跟着做,最好还是在 Python shell 中进行。
在加载 MNIST 数据后,我们将设置一个具有 $30$ 个隐藏神经元的网络。我们在导入上面列出的名为 network 的 Python 程序后进行此操作,
>>> 导入网络 >>> net = network.Network([784, 30, 10])
最后,我们将使用随机梯度下降从 MNIST 训练数据中学习 30 个周期,迷你批量大小为 10,学习率为 $\eta = 3.0$,
>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
请注意,如果您在阅读时运行代码,执行将需要一些时间 - 对于典型的机器(截至2015年),运行可能需要几分钟。我建议您设置好运行,继续阅读,并定期检查代码的输出。如果您很着急,可以通过减少训练轮数、减少隐藏神经元的数量或仅使用部分训练数据来加快速度。请注意,生产代码会快得多:这些Python脚本旨在帮助您理解神经网络的工作原理,而不是高性能代码!当然,一旦我们训练了一个网络,它可以在几乎任何计算平台上非常快速地运行。例如,一旦我们为网络学习了一组良好的权重和偏置,它可以很容易地移植到在网页浏览器中以Javascript运行,或作为移动设备上的本地应用程序运行。无论如何,这里是神经网络一次训练运行的部分输出记录。记录显示了每个训练轮后神经网络正确识别的测试图像数量。如您所见,仅在一个训练轮后,这个数量已达到10,000中的9,129,并且这个数字还在继续增长,
第 0 轮: 9129 / 10000 第 1 轮: 9295 / 10000 第 2 轮: 9348 / 10000 ... 第 27 轮: 9528 / 10000 第 28 轮: 9542 / 10000 第 29 轮: 9534 / 10000
也就是说,训练好的网络给我们的分类率大约是 $95$ 百分 - 在其峰值("Epoch 28")时为 $95.42$ 百分!作为第一次尝试,这相当令人鼓舞。然而,我应该警告你,如果你运行代码,你的结果不一定会和我的完全相同,因为我们将使用(不同的)随机权重和偏置来初始化我们的网络。为了生成本章的结果,我进行了三次最佳运行。
让我们重新运行上述实验,将隐藏神经元的数量更改为 $100$。和之前一样,如果你在阅读时运行代码,你应该被警告执行需要相当长的时间(在我的机器上,这个实验每个训练周期需要几十秒),所以在代码执行的同时继续阅读是明智的。
>>> net = network.Network([784, 100, 10]) >>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
果然,这将结果提高到 $96.59$ 百分比。至少在这种情况下,使用更多的隐藏神经元有助于我们获得更好的结果* *读者反馈表明,这个实验的结果有相当大的变化,一些训练运行的结果要差得多。使用第3章中介绍的技术将大大减少我们网络在不同训练运行中的性能变化。
当然,为了获得这些准确性,我必须对训练的轮数、迷你批量大小和学习率 $\eta$ 做出特定选择。正如我上面提到的,这些被称为我们神经网络的超参数,以便将它们与我们的学习算法学习的参数(权重和偏差)区分开。如果我们选择超参数不当,可能会得到糟糕的结果。例如,假设我们选择的学习率为 $\eta = 0.001$,
>>> net = network.Network([784, 100, 10]) >>> net.SGD(training_data, 30, 10, 0.001, test_data=test_data)
结果要令人失望得多,
第 0 轮: 1139 / 10000 第 1 轮: 1136 / 10000 第 2 轮: 1135 / 10000 ... 第 27 轮: 2101 / 10000 第 28 轮: 2123 / 10000 第 29 轮: 2142 / 10000
然而,您可以看到网络的性能随着时间的推移慢慢变得更好。这表明可以增加学习率,比如说 $\eta = 0.01$。如果我们这样做,我们会得到更好的结果,这表明可以再次增加学习率。(如果改变能改善情况,尝试做得更多!)如果我们这样做几次,我们最终会得到一个学习率大约为 $\eta = 1.0$(也许微调到 $3.0$),这接近我们之前的实验。因此,即使我们最初选择了不好的超参数,我们至少获得了足够的信息来帮助我们改善超参数的选择。
一般来说,调试神经网络可能是具有挑战性的。尤其是当初始超参数的选择产生的结果不比随机噪声更好时,这种情况尤为明显。假设我们尝试之前成功的30个隐藏神经元网络架构,但将学习率更改为 $\eta = 100.0$:
>>> net = network.Network([784, 30, 10]) >>> net.SGD(training_data, 30, 10, 100.0, test_data=test_data)
在这一点上,我们实际上已经走得太远,学习率太高了:
第 0 轮: 1009 / 10000 第 1 轮: 1009 / 10000 第 2 轮: 1009 / 10000 第 3 轮: 1009 / 10000 ... 第 27 轮: 982 / 10000 第 28 轮: 982 / 10000 第 29 轮: 982 / 10000
现在想象一下,我们第一次遇到这个问题。当然,我们_知道_从之前的实验中,正确的做法是降低学习率。但如果我们第一次遇到这个问题,那么输出中就没有太多信息可以指导我们该怎么做。我们可能不仅会担心学习率,还会担心神经网络的其他每个方面。我们可能会想,我们是否以一种让网络难以学习的方式初始化了权重和偏置?或者也许我们没有足够的训练数据来获得有意义的学习?也许我们没有运行足够的轮次?或者也许对于这种架构的神经网络来说,学习识别手写数字是不可能的?也许学习率太_低_了?或者,也许学习率太高了?当你第一次遇到一个问题时,你并不总是确定。
从中得到的教训是,调试神经网络并非易事,正如普通编程一样,这其中有一门艺术。你需要学习调试的艺术,以便从神经网络中获得良好的结果。更一般来说,我们需要开发选择良好超参数和良好架构的启发式方法。我们将在本书中详细讨论这些内容,包括我如何选择上述超参数。
- 尝试创建一个只有两层的网络 - 一个输入层和一个输出层,没有隐藏层 - 分别具有 784 和 10 个神经元。使用随机梯度下降法训练网络。你能达到什么分类准确率?
之前,我跳过了关于如何加载 MNIST 数据的细节。这非常简单。为了完整性,这里是代码。用于存储 MNIST 数据的数据结构在文档字符串中描述 - 这很简单,都是 Numpy ndarray 对象的元组和列表(如果你不熟悉 ndarrays,可以把它们想象成向量):
"""
mnist_loader
~~~~~~~~~~~~ 一个用于加载 MNIST 图像数据的库。有关返回的数据结构的详细信息,请参见 load\_data
和 load\_data\_wrapper
的文档字符串。在实践中,load\_data\_wrapper
是我们神经网络代码通常调用的函数。
""" #### Libraries
# 标准库
import cPickle
import gzip # 第三方库
import numpy as np def load_data(): """返回 MNIST 数据作为一个包含训练数据、验证数据和测试数据的元组。 training\_data
作为一个包含两个条目的元组返回。第一个条目包含实际的训练图像。这是一个具有 50,000 个条目的 numpy ndarray。每个条目又是一个具有 784 个值的 numpy ndarray,表示单个 MNIST 图像中的 28 * 28 = 784 个像素。 training\_data
元组中的第二个条目是一个包含 50,000 个条目的 numpy ndarray。这些条目只是对应于元组第一个条目中图像的数字值 (0...9)。 validation\_data
和 test\_data
类似,只是每个只包含 10,000 张图像。这是一种不错的数据格式,但在神经网络中使用时,稍微修改 training\_data
的格式是有帮助的。这在包装函数 load\_data\_wrapper()
中完成,见下文。
""" f = gzip.open('../data/mnist.pkl.gz', 'rb') training_data, validation_data, test_data = cPickle.load(f) f.close() return (training_data, validation_data, test_data) def load_data_wrapper(): """返回一个元组包含 (training\_data, validation\_data, test\_data)
。基于 load\_data
,但格式更方便用于我们神经网络的实现。特别是,training\_data
是一个包含 50,000 个 2 元组 (x, y)
的列表。 x
是一个 784 维的 numpy.ndarray,包含输入图像。 y
是一个 10 维的 numpy.ndarray,表示与 x
对应的正确数字的单位向量。 validation\_data
和 test\_data
是包含 10,000 个 2 元组 (x, y)
的列表。在每种情况下,x
是一个 784 维的 numpy.ndarry,包含输入图像,y
是相应的分类,即与 x
对应的数字值(整数)。显然,这意味着我们对训练数据和验证/测试数据使用了稍微不同的格式。这些格式被证明是我们神经网络代码中最方便使用的格式。""" 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): """返回一个 10 维的单位向量,在第 j 个位置为 1.0,其他位置为零。这用于将数字 (0...9) 转换为神经网络的相应期望输出。""" e = np.zeros((10, 1)) e[j] = 1.0 return e
我在上面提到我们的程序取得了相当不错的结果。这意味着什么?与什么相比好呢?有一些简单的(非神经网络)基准测试进行比较是很有意义的,以理解表现良好的含义。最简单的基准,当然,就是随机猜测数字。这样大约有百分之十的正确率。我们的表现远远好于这个!
那么一个不那么琐碎的基线呢?我们来试一个极其简单的想法:我们将观察一张图像有多 暗。例如,一张 $2$ 的图像通常会比一张 $1$ 的图像暗得多,仅仅是因为更多的像素被黑掉,正如以下示例所示:
这表明使用训练数据来计算每个数字的平均黑暗度,$0, 1, 2,\ldots, 9$。当呈现一张新图像时,我们计算图像的黑暗程度,然后猜测它是哪个数字,其平均黑暗度最接近。这是一个简单的过程,编码也很容易,所以我不会明确写出代码 - 如果你感兴趣,可以在GitHub repository中找到。但这比随机猜测有很大改进,正确识别了$2,225$张$10,000$张测试图像,即$22.25$%的准确率。
找到其他在 $20$ 到 $50$ 百分比范围内达到准确性的想法并不困难。如果你再努力一点,可以超过 $50$ 百分比。但要获得更高的准确性,使用已建立的机器学习算法会有所帮助。让我们尝试使用一种最著名的算法,支持向量机 或 SVM。如果你对 SVM 不熟悉,不用担心,我们不需要理解 SVM 的工作细节。相反,我们将使用一个名为 scikit-learn 的 Python 库,它提供了一个简单的 Python 接口,连接到一个快速的基于 C 的 SVM 库,称为 LIBSVM。
如果我们使用默认设置运行 scikit-learn 的 SVM 分类器,那么它能正确分类 10,000 张测试图像中的 9,435 张。(代码可以在 这里 找到。)这比我们根据图像的暗度进行分类的简单方法有了很大改善。实际上,这意味着 SVM 的表现大致与我们的神经网络相当,只是稍微差一些。在后面的章节中,我们将介绍新的技术,使我们能够改进我们的神经网络,从而使其表现远超 SVM。
然而,这并不是故事的结局。9,435 的 10,000 结果是针对 scikit-learn 的 SVM 默认设置。SVM 有许多可调参数,可以搜索改善这种开箱即用性能的参数。我不会明确进行这个搜索,而是如果你想了解更多,我会推荐你阅读 这篇博客文章 由 Andreas Mueller 撰写。Mueller 表明,通过一些优化 SVM 参数的工作,可以将性能提高到超过 98.5% 的准确率。换句话说,一个调优良好的 SVM 仅在大约 70 个数字中出错一个。这相当不错!神经网络能做得更好吗?
事实上,它们可以。目前,设计良好的神经网络在解决 MNIST 问题上超越了所有其他技术,包括 SVM。当前(2013 年)的记录是正确分类 10,000 张图像中的 9,979 张。这是由 Li Wan、Matthew Zeiler、Sixin Zhang、Yann LeCun 和 Rob Fergus 完成的。我们将在书中稍后看到他们使用的大多数技术。在这个水平上,性能接近人类水平,并且可以说更好,因为相当多的 MNIST 图像即使对于人类来说也很难自信地识别,例如:
我相信你会同意这些很难分类!在 MNIST 数据集中有这样的图像,神经网络能够准确分类 10,000 张测试图像中除了 21 张以外的所有图像,这真是令人惊讶。通常,在编程时,我们认为解决像识别 MNIST 数字这样复杂的问题需要一个复杂的算法。但即使是刚提到的 Wan et al 论文中的神经网络也涉及相当简单的算法,是我们在本章中看到的算法的变体。所有的复杂性都是从训练数据中自动学习的。从某种意义上说,我们的结果和更复杂论文中的结果的道德是,对于某些问题:
复杂算法 $\leq$ 简单学习算法 + 良好的训练数据.
虽然我们的神经网络表现出色,但这种表现有些神秘。网络中的权重和偏差是自动发现的。这意味着我们并没有立即找到网络如何实现其功能的解释。我们能否找到某种方法来理解我们的网络是如何对手写数字进行分类的原则?而且,基于这些原则,我们能否做得更好?
更直白地说,假设几十年后神经网络导致了人工智能(AI)。我们会理解这些智能网络是如何运作的吗?也许这些网络对我们来说是不可理解的,权重和偏差我们无法理解,因为它们是自动学习的。在人工智能研究的早期,人们希望构建人工智能的努力也能帮助我们理解智能背后的原理,也许还能理解人脑的功能。但结果可能是我们既无法理解大脑,也无法理解人工智能是如何运作的!
为了回答这些问题,让我们回想一下我在本章开始时对人工神经元的解释,作为权衡证据的一种手段。假设我们想要确定一幅图像是否显示了人脸:
致谢: 1. 埃斯特·因巴尔. 2. 未知. 3. NASA, ESA, G. Illingworth, D. Magee, 和 P. Oesch (加利福尼亚大学圣克鲁斯分校), R. Bouwens (莱顿大学), 以及 HUDF09 团队. 点击图片以获取更多细节.
我们可以用与处理手写识别相同的方式来解决这个问题——通过将图像中的像素作为神经网络的输入,网络的输出是一个单一的神经元,指示"是的,它是一个面孔"或"不,它不是一个面孔"。
假设我们这样做,但我们不使用学习算法。相反,我们将尝试手动设计一个网络,选择合适的权重和偏置。我们该如何进行呢?暂时忘记神经网络,我们可以使用的一个启发式方法是将问题分解为子问题:图像的左上角有眼睛吗?右上角有眼睛吗?中间有鼻子吗?底部中间有嘴巴吗?顶部有头发吗?等等。
如果这些问题中的几个答案是“是”,甚至只是“可能是”,那么我们可以得出结论,图像很可能是一个面孔。相反,如果大多数问题的答案是“否”,那么图像可能就不是一个面孔。
当然,这只是一个粗略的启发式方法,并且存在许多缺陷。也许这个人是秃头,所以他们没有头发。也许我们只能看到部分脸,或者脸的角度不正,因此一些面部特征被遮挡。尽管如此,这个启发式方法表明,如果我们可以使用神经网络解决子问题,那么也许我们可以通过结合子问题的网络来构建一个面部检测的神经网络。这里有一个可能的架构,矩形表示子网络。请注意,这并不是解决面部检测问题的现实方法;而是帮助我们建立对网络功能的直觉。以下是架构:
也有可能子网络可以被分解。假设我们在考虑这个问题:"左上角有眼睛吗?" 这可以分解成诸如:"有眉毛吗?";"有睫毛吗?";"有虹膜吗?";等等。当然,这些问题实际上应该包括位置信息,比如 - "眉毛在左上角,并且在虹膜上方吗?",类似这样的东西 - 但我们保持简单。现在,回答"左上角有眼睛吗?"的问题的网络可以被分解:
这些问题也可以被分解,通过多个层次进一步分解。最终,我们将处理子网络,这些子网络回答的问题如此简单,以至于可以在单个像素的层面上轻松回答。这些问题可能,例如,涉及图像中特定点上非常简单形状的存在或缺失。这类问题可以通过连接到图像原始像素的单个神经元来回答。
最终结果是一个网络,它将一个非常复杂的问题——这张图片是否显示了一个面孔——分解为可以在单个像素级别回答的非常简单的问题。它通过许多层的系列来实现,早期层回答关于输入图像的非常简单和具体的问题,而后期层则建立起越来越复杂和抽象概念的层次结构。具有这种多层结构的网络——两个或更多隐藏层——被称为_深度神经网络_。
当然,我还没有说如何将这种递归分解成子网络。手动设计网络中的权重和偏置显然是不切实际的。相反,我们希望使用学习算法,以便网络能够从训练数据中自动学习权重和偏置——从而学习概念的层次结构。1980年代和1990年代的研究人员尝试使用随机梯度下降和反向传播来训练深度网络。不幸的是,除了少数特殊架构外,他们并没有取得太大成功。网络会学习,但速度非常慢,实际上往往慢到无法实用。
自2006年以来,开发出一套技术,使得深度神经网络的学习成为可能。这些深度学习技术基于随机梯度下降和反向传播,但也引入了新的思想。这些技术使得更深(更大)的网络能够被训练——人们现在常常训练具有5到10个隐藏层的网络。而且,事实证明,这些网络在许多问题上表现远远优于浅层神经网络,即仅具有一个隐藏层的网络。原因当然在于深度网络能够建立复杂的概念层次。这有点像传统编程语言使用模块化设计和抽象思想来实现复杂计算机程序的创建。将深度网络与浅层网络进行比较,就像将一种能够进行函数调用的编程语言与一种没有这种调用能力的简化语言进行比较。抽象在神经网络中的表现形式与传统编程不同,但同样重要。