TensorFlow官方教程《Neural Networks and Deep Learning》译(第一章)

来源:互联网 发布:nginx rtmp 点播配置 编辑:程序博客网 时间:2024/04/29 07:58

– 更新中

译自:Neural Networks and Deep Learning

成果预展示

如果你能坚持阅读完本章, 你可以获得如下的成果:
这里写图片描述

上图中的命令行窗口输出为:

Epoch 0: 9095 / 10000Epoch 1: 9213 / 10000Epoch 2: 9299 / 10000Epoch 3: 9358 / 10000...Epoch 27: 9535 / 10000Epoch 28: 9515/ 10000Epoch 29: 9536 / 10000
  • 其中每一个 epoch 表示一个神经网络的训练小周期。 后面的数字表示从 10000 个测试图片,该神经网络成功识别了多少个手写数字的图片。

  • 令人兴奋的是, 上面的这个 pyhon 程序并没有使用任何特殊的神经网络库, 且它只有短短 74 行代码(除去注释和空格排版)。 但是这个简短的程序可以在没有人工干预的情况下, 自行从训练数据中学习, 以 96% 的准确度识别手写数字。 而且, 在后面的章节中, 我们会将这个程序的识别准确度改进到 99%

这意味通过阅读本文和理解上面的程序, 你能真正理解神经网络及深度学习背后的原理, 相信我, 这绝对是一个通俗易懂的教程!

前言

Neural Networks and Deep Learning 是一本免费的网络书籍,它在 Tensor Flow Playground 的首页中被推荐为学习神经网络和深度学习的入门级教材。 个人阅读后发现该教程极其清晰易懂。 故特作翻译, 以作知识分享和自我激励。 翻译中可能会加入一些个人的理解和略去一些个人认为无关紧要的修饰性内容。

该教程主要内容为

  • 神经网络, 一种优美的受生物学启发而设计出的编程模式,使得计算机可以从观测到的数据中学习
  • 深度学习, 一套利用神经网络进行学习的强大技术集合。

神经网络和深度学习目前为图像识别, 语音识别, 自然语言处理方面的很多问题提供了最佳的解决方案。 这本书会教你很多神经网络和深度学习背后的核心概念。

第一章 使用神经网络识别手写数字

人类的视觉系统是世界上的众多奇迹之一。 考虑如下的手写数字序列。

这里写图片描述

大部分人可以轻而易举的识别出图片中的数字序列为 504192 。但是这个任务的复杂度显然被这个“轻而易举” 所掩盖。 在我们每一个脑半球, 都有一个初级视觉皮层, 被称为 V1, 包含了 1亿4千万神经元, 以及神经元之间数以十亿的连接。 但是, 人类的视觉系统不仅涉及到 V1, 还有一系列的视觉皮层 V2, V3,V4 和 V5 , 渐进式地负责更加复杂的图像处理。 我们可以把我们的大脑当做一个超级计算机, 经过了上成百上千万年的演化, 完成了对视觉世界的理解。

识别视觉图像的难度当你在尝试写一个程序来识别上述图片中的数字时显现出来, 你会发现之前显而易见的直觉变得难以描述:“ 数字 9 在上方有一个圈, 在右下方有一个竖直的一笔, 可以弯曲也可以不弯曲 ”, 这在算法上是很难表达的。 如果你尝试更加精确地描述这个规则, 你会很快迷失在大量混乱的异常和特例中。 这种方法看上去毫无希望。

这里写图片描述

神经网络以一种完全不同的方式来解决这个问题, 其思想是, 获取大量的手写数字的样例, 被称作训练样例, 然后开发出一个可以从训练样例中学习的系统。 换句话说,神经网络使用样例来自动推理识别手写数字的规则。 进而, 通过增加训练的样例, 神经网络可以从手写数字中学习更多, 提高识别的准确度。 因此尽管我们在上图中只展示了 100 个数字, 但我们可以利用更多的数据样例构建一个更好的的手写数字识别系统。

在本章节中, 我们会写一个程序来实现一个能识别手写数字的神经网络。 这个程序只有 73 行, 没有使用任何特殊的神经网络库。 但是这个简短的程序可以在没有人工干预的情况下以 96% 的准确度识别手写数字。 而且, 在后面的章节中, 我们会将这个程序的识别准确度改进到 99%。 事实上, 最好的商用神经网络已经很成熟, 被商业银行用来处理支票, 被邮局用来识别地址。

我们专注于手写图像的识别, 是因为这是一个用于学习神经网络非常杰出的原型问题。 作为原型问题, 它有如下几个特点:

  • 具有挑战性。 想写出识别手写数字的程序并不是小菜一碟
  • 没有过度困难。 并不需要非常复杂的解决方案, 或者大量的算力。
  • 容易在此基础上衍生出更加高级的技术, 例如深度学习。

在这正本书中, 我们会反复地来解决手写图像识别的问题。 在本教程的后面部分, 我们会讨论这些思想如何被应用到计算机视觉, 语音识别, 自然语言处理以及其他领域的问题中。

当然, 如果本章的核心点仅仅是写一个能够识别手写数字的程序, 那应该会很短, 但是我们会沿着这个问题, 去理解神经网络的很多核心概念与思想, 具体包括

  • 两种人工神经元: 感知器(perceptron)和 S形神经元(sigmoid neuron)
  • 神经网络中标准的学习算法: 随机梯度下降(stochastic gradient descent),

自始至终, 我都会专注于解释为什么事情是以这种方式做, 并且搭建起你对神经网络的直觉性理解。 这比简单地展示出基本的机理要花费更多的篇幅, 但是这一切都是值得的, 因为你可以通过这个过程对神经网络获得深刻的理解。 在获得了所有的学习成果后, 在本章节的末尾, 我们将能够理解什么是深度学习以及为什么深度学习非常重要。

感知器(Perceptrons)


什么是神经网络? 作为开始, 我会先来解释一种称作 感知器(perceptron) 的人工神经元。 感知器是19世纪50年代-60年代, 由科学家 Frank Rosenblatt 受 Warren McCulloch 和 Walter Pitts 的早期工作启发而开发出的。 今天, 通常会使用其他类型的人工神经元。 在这本书中, 以及在大部分现代的神经网络工作中, 主要被使用的神经元模型是被称作 S形神经元(sigmoid neuron)。 我们会很快地学习到 S形神经元。 但是为了理解为什么S形神经元会以那种方式定义, 我们首先得花一些时间来理解感知器。

所以, 感知器是如何工作的? 一个感知器会获取多个二进制的输入,x1,x2,....,然后产生一个单个的二进制输出。
这里写图片描述

在这个李忠, 感知器有三个输入 x1,x2,x3 。 一般来说, 感知器可以拥有更多或更少的输入,Rosenblatt 提出了一种计算输出的简单规则。 他引入了权重(weights) , w1,w2,...., 等多个实数来表示不同输入的重要程度。 神经元的输出 0 或 1 是由输入的加权求和是否超过某个阈值 而决定的 。 就像权重一样, 阈值也是一个实数, 是该神经元的一个参数。 可以用代数更精确地描述为:

output=0ifi=1wjxjthreshold1ifi=1wjxj>threshold(1)

以上就是所有感知器工作的原理了!

这是一个非常基础的数学模型。一种理解感知器的方法是, 你可以将其看做一个能够评估不同因素, 做出决策的装置。 让我们举个栗子,一个不是非常逼真的例子,但是很容易理解。 假设周末即将到来, 你听说你所在的城市里将要有一个奶酪嘉年华。 你喜欢奶酪, 想要决定是不是要去参加这个活动。你可能会根据以下三个因素做出决策:

  1. 天气是否好?
  2. 你的男/女朋友是否会陪你一起去?
  3. 这个活动乘坐公交是否容易到达(你没有车)?

我们可以用三个二进制变量 x1,x2,x3来表示这三个因素。 例如, x1=1 表示天气好, x1=0 表示天气不好。 类似的, x2=1 表示你的男/女朋友会陪你一起去, x2=0 表示不会陪你一起去。 x3 同理。

现在假设你极度喜奶酪, 以致于即使你的男/女朋友不能够陪你而且公共交通不易到达活动地点, 你也想去参加这个活动, 但是你非常厌恶坏天气,如果天气不好的话, 你是无论如何都不会去参加这个活动。 你可以使用感知器来为这个决策过程建模。 一种建模的方法是, 设置权重 w1=6,w2=2,w3=2w1设置的越大表示天气因素对你来说越重要。 最终,假设你将这个感知器的threshhold 设置成 5。通过这些设置, 该感知器实现了一个你所期望的决策模型, 输出 1 表示你要去参加活动(天气好), 输出 0 表示你不去参加活动(天气不好)。

通过调整权重和阈值, 我们可以获得不同的决策模型。 例如, 假设我们选择 3 作为阈值, 那么当天气好时或容易乘坐公交到达活动地点且男/女朋友愿意陪伴你两个条件都达成时,决策你应该去参加活动。 调低阈值表示你更愿意去参加这个活动。

显然, 感知器不是一个完整的人类决策模型!但是这个例子展示了一个感知器是如何把不同的因素进行考量进而做出决策。 那么, 利用下面这个多感知器组成的一个复杂网络为复杂而微妙的决策过程建模似乎是可行的:
这里写图片描述

在这个网络中, 第一列感知器 - 我们称之为第一层感知器- 会通过衡量输入 inputs 的内容 做出三个非常简单的决策。 那么第二层感知器起什么作用? 第二层的每一个感知器会进一步衡量第一层感知器输出的结果再次做出各自的决策。 和第一层相比, 可以做出更为复杂,更为抽象的决策。 继而, 第三层感知器可以作出更加复杂的决策。 通过这种方式, 一个多层感知器可以参与非常复杂的决策过程。

当我在定义感知器时, 顺表说了一句一个感知器只有一个输出 。 在上面的这个网络中, 感知器似乎有多个输出。 事实上, 每个感知器输出依旧只有一个, 多个输出的箭头仅仅是单个输出被用于多个感知器的输入的简便表示。

让我们来简化一下描述感知器的方式。条件i=1wjxj>threshold 看上去非常烦琐, 我们可以做两个记号上的改变来简化它。 第一个改变是把i=1wjxj 用点乘的方式表示, 即 wxi=1wjxj , wx是权重和输入的向量表示。 第二个改变是, 把阈值挪到等式的另外一侧, 改用偏移量 (bias)来表示。 bthreshold 使用偏移量而不是阈值来表示后, 感知器的规则可以被重写为:

output=0ifi=1wjxj+b01ifi=1wjxj+b>0(2)

你可以认为, 偏移量是用于衡量一个感知器输出 1 的难易度。 显然, 引入偏移量 b 仅仅是我们描述感知器方式的一个细微改变。 但是我们之后会看到, 这个改变如何进一步带来符号表示上的简化。 在本书的其余部分, 我们将不会再使用阈值 threshold , 只使用 bias。

我已经将感知器描述为一个衡量多个因素, 做出决策的方法。 感知器的另一用途是模拟计算机底层的基础逻辑电路例如 AND, OR, NAND。(译者注: 大学学过电子电路的童鞋会对此感到非常亲切, 没有学过的童鞋这里比较困惑也不影响后续内容的理解) 例如, 假设我们有一个感知器接收两个输入, 每一个输入的权重是 -2, 总偏移 bias 是 3 , 如下:
这里写图片描述

这样, 当我们输入 00 的时候就会输出 1 , 因为(2)0+(2)0+3=1, 同理, 输入0110 会输出1。 但是输入11的时候时候会输出0。 所以, 这个感知器实现了一个与非门(NAND GATE)。

与非门的例子说明我们可以使用感知器来实现简单的逻辑功能。 事实上, 我们可以利用多感知器的网络来实现任意的逻辑。 原因是与非门对于计算来说是一个通用的功能,只要有足够多的与非门, 我们可以搭建出实现任意逻辑计算的功能 (这一点是被严谨证明过的, 计算机硬件底层就是由大量与非门实现的)。 例如我们用与非门搭建一个实现 2 bit 加法电路, 这首先需要按位求和, 还需要有一个 bit 来实现进位。
这里写图片描述

上面这个 2 bit 求和电路可以通过如下的感知网络实现:
这里写图片描述

上图中的每个感知器的两个输入权重 weight 都是 -2 , 偏移量 bias 都是3 。 注意到我们把负责输出进位结果的感知器向左移动了一些, 仅仅是为了更容易画图。

上面的感知器网络中有一个值得关注的细节是, 最左边的感知器的输出在最下面的感知器上使用了两次, 作为输入。 当我定义感知器的时候, 我并没有说过这种把一个输出两次用作另外一个感知器的输入的操作是否被允许。事实上, 这不是特别要紧,如果我们不想允许这种情形, 那么可以把两个箭头合并作一个箭头, 权重改为 -4 即可, 如下图:

这里写图片描述

现在我会把 x1x2 两个变量用额外的一层感知器 - 输入层 - 来表示。 :

这里写图片描述

输入层的感知器只有输出, 没有输入, 如下:
这里写图片描述

这种表示方法只是一种速记法, 它并不是真的意味着一个没有输入的感知器。为了理解, 我们可以假设我们确实有一个无输入的感知器。 那么加权求和结果 i=1wjxj 就总是0, 那么感知器当 b>0 输出1, 当 b0 输出 0 。 也就是说感知器仅仅是在简单的输出一个固定的值,而不是我们所期待的上图中的 x1 。 因此, 最好不要把输入层的感知器当做一个真正的感知器,而是仅仅地把他们当做一些特殊的, 用于定义输出特定变量值的单元 x1,x2,...

加法器的例子展现了如何用感知器网络来模拟包含很多与非门的逻辑电路哦, 由于与非门对于任意计算逻辑来说都是通用的, 那么感知器对于任意计算逻辑也是通用的。

感知器的这种逻辑计算通用性即是令人兴奋的, 也是让人失望的。 令人兴奋的原因是我们感知器网络可以和任意一种计算设备一样强大(目前电子计算的实现基础都是逻辑电路,量子计算除外)。 但是这个事实也是令人失望的, 因为感知器似乎也仅仅是一种新型的与非门 , 这可算不上是什么大的进步。

然而, 事实不止这么简单。 因为我们发现, 可以发明出一种学习算法 来自动的调整人工神经网络中各个神经元的的权重 weights 和 bias 。 这个调整过程可以在没有程序员直接干预的情况下,针对外界的反馈刺激做出响应。 这些学习算法使得我们能以一种完全不同于传统逻辑电路的方式来使用人工神经元 , 即用神经网络学习去解决那些很难用通过传统逻辑电路设计而解决的复杂问题。

can learn to build gate network without human intervention

S形神经元(Sigmoid Neurons)


学习算法听上去非常棒! 但是我们如何为神经网络实现这种算法呢? 假设有一个感知器网络, 我们希望用该网络来学习解决某个问题。 例如, 神经网络的输入时扫描出的手写数字图像的原始像素数据。 我们希望神经网络可以学习权重 weights 和 偏移量 bias 使得该神经网络的输出能够将数字正确区分。 为了理解这个学习过程怎样进行, 先假设我们对网络的权重 weights (或偏移量 bias) 作一些轻微的改变。 我们希望看到的是, 这些权重上的轻微改变仅仅对网络的输出产生一些轻微的影响。 这之后我们会理解, 这个性质会使得学习成为可能。 下面是这个改动的图例展现(很明显下面这个网络还太简单, 不足以进行手写数字的识别!):

这里写图片描述

如果一个权重 weight ( 或者偏移量 bias ) 的小改变只会导致最终输出的轻微变化, 那么我们就可以利用这个性质来修改权重和偏移量使得网络表现的更像我们期待的那样。 例如, 网络可能把一个应该被当做数字9 的图像错误识别成了数字8 , 我们可以搞清楚如何微调权重或偏移量的值, 使得网络最终的输出结果可以靠近一点 “9” . 然后我们可以重复这个过程, 不断地改变权重和偏移量, 使得网络输出的结果越来越好。 这样, 网络就实现了学习的过程。

问题是, 上述的过程并不会发生在由感知器构成的神经网络中。 事实上, 对于任意感知器权重或偏移量的轻微改变可能会导致那个感知器的输出完全翻转, 例如从0到1。 这个结果的翻转可能会导致其余网络的行为以一种非常复杂的方式完全改变。 所以当你把网络微调到可以正确识别9的时候, 网络对于其他图像的识别行为会被完全改变, 难以控制。 这使得你很难通过微调权重和偏移量的方法, 使得网络逐渐表现出你期待的行为。可能会有什么非常聪明的方法可以解决这个问题, 但是这里并没显而易见的方案可以使得感知器网络进行学习。

这个问题可以通过引入另外一种称作 S形神经元(sigmoid neruon)的神经元予以解决。 S形神经元和感知器非常相似,可以说是在感知器的基础上进行了一些改动使得权重和偏移量的轻微变化只会导致输出的轻微变化。 这是使得S形神经元组成的神经网络能够学习的至关重要的一个性质。

我们以相同的方式来描绘 S形神经元:

这里写图片描述

和感知器一样, S形神经元拥有输入 x1,x2,... , 不同的是, 输入的值不再是 0 或 1, 而是0至1之间的任意值, 如 0.638... 是S形神经元的一个有效输入。 和感知器一样, S形神经元的每个输入也有相应的权重w1,w2,... 和 总体偏移量 bias , b。 但是, 输出的值不再是 0 或 1, 而是 σ(wx+b), 其中 σ 被称作S形函数(sigmoid function)或 逻辑函数(logisitic function), 定义为:

σz11+ez[3]

为了使得一切更清晰一些, S形神经元的输出可以被写作:
11+exp(i=1wjxjb)[4]

- 注解: exp(x 表示指数函数 ex

乍一看, S形神经元似乎和感知器非常不一样。 S形神经元的代数形式在你还未熟悉它时, 似乎非常晦涩且令人生畏。 但事实上, 感知器和S形神经元有很多相似之处, S形函数的代数形式更多的只是一些技术细节, 而不是一个难于理解的障碍。

为了理解S形神经元和感知器模型的相似性, 假设zwx+b是一个很大的正数, 那么ez0σ(z)1 。 换句话说, 当 z=wx+b 为一个很大的正值时, 输出就趋近于1, 其行为就像感知器一样。 另一方面, 假设z=wx+b 为一个很大的负数时,则 ez 那么, σ(z)0 。 所以当z=wx+b为一个非常大的负数时, S形神经元的行为也趋近于感知器。 只有当z=wx+b为中间值时, S形神经元的行为才会与感知器有所区别。

σ 函数的代数形式的具体含义到底是什么? 我们应该如何理解它? 事实上, σ 函数的具体形式并不是那么重要, 真正重要的是这个函数的形状:

这里写图片描述

上图的这个形状是一个阶梯函数( step function )的平滑版。

这里写图片描述

如果σ 函数变成了一个阶梯函数, 那么 S形神经元就会变成一个感知器, 因为它只能输出0或1, 且取决于w \cdot x + b 的正负。因此, 通过利用σ 函数, 我们获得了一个“平滑版”的感知器。
确切来说, σ函数的平滑性质才是关键的性质,而不是它具体的代数形式。 σ函数的平滑性意味着权重的细微改变Δwj 和偏移量的细微改变Δb 仅仅会产生输出的一个轻微变化Δoutput. 事实上, 微积分告诉我们Δoutput约等于:

ΔoutputjoutputwjΔwj+outputbΔb

其中 output/wjoutput/b 表示输出 output 关于 wjb 的偏微分。 不要因为你不熟悉偏微分而感到焦虑。 尽管这个表达式看上去很复杂, 充满了偏微分符号, 但它事实只是再阐述一些简单的事实:Δoutput 是一个关于ΔwjΔb 的线性函数。 这种线性性质使得很容易在权重 w 和 偏移量 j 上进行一些微调以达到输出上期待的变化。 所以 , S形神经元在定性方面,拥有了和感知器相同的行为, 在定量方面, 使得微调权重和偏移量来改变输出的操作变得可行。

回到之前的话题, 如果σ 函数的形状是真正重要的部分, 那么为什么一定要使用公式3 ? 事实上, 在本书的靠后部分, 我们会偶尔考量输出结果为 f(wx+b) 的神经元, 其中 f() 称为激活函数(activation function)。 使用不同的激活函数产生的变化主要就是公式5中的偏微分值发生了改变, 使用 σ 函数会简化代数运算, 因为指数在微分的时候有一些非常可爱的性质。 无论如何, σ 函数在神经网络领域中是一个被广泛使用的函数, 也是我们在这本书中将会用得最多的激活函数。

那么, 如何解读S形神经元的输出呢? 显然, 感知器和S形神经元的一个大的区别是 S形神经元并不是只输出0或1。 它可以输出0到1之间的任意实数, 如 0.173...,0.689都是有效的输出。这个性质可以在一些场景发挥作用,例如, 我们可能会希望用输出值来表示一个经过神经网络处理的像素数据的平均亮度(average intensity)时。 但是, 这个性质有时又会让人感到讨厌, 例如我们希望用输出来表示被识别的图像是否为“9”时, 显然感知器直接输出0或1会更加清晰明了。 然而, 实际应用中, 我们可以设置, 大于 0.5 的输出被认为表示图像被识别为 “是 9”, 小于等于 0.5 的输出表示识别结果“不是 9”。 在使用这种设置时, 我总是会明确的指出来, 避免任何可能产生的困惑。

神经网络的架构

在下一小节, 我会介绍一个可以出色完成手写数字分类工作的神经网络。 在介绍它之前, 解释一些有关神经网络架构的术语会很有助于我们进一步学习神经网络。 假设我们有如下的网络:
这里写图片描述
正如之前提及的, 神经网络最左侧的一层被称为输入层, 其中的神经元被称为输入神经元(input neurons)。 最右侧的一层被称为输出层 (output layer), 在本例中, 包含一个单一的输出神经元。 中间层被称为隐藏层(hidden layer), 因为在这一层的神经元既不是输入也不是输出。 术语“隐藏(hidden)”听起来好像有点神秘 - 当我第一次听到这个术语的术语时, 我以为它一定一位置更深层次的哲学或数学含义 - 但事实上, 它真的只包含“既不是输出, 也不是输出”的含义。 上面的网络仅有一层隐藏层, 但是一些神经网络中有多层隐藏层, 例如, 如下的四层网络结构有两层隐藏层:

这里写图片描述

出于历史原因, 这种多层的网络尽管由S形神经元组成, 但有时仍会被称为多层感知器(multilayer perceptrons or MLPs), 我不打算在本书中使用 MLP 这个术语, 因为我觉得它很容易让人误解, 在这里提出来只是希望提醒你它的确存在。

神经网络输入层和输出层的设计通常非常简单, 例如, 我们希望识别1个数字是否为9 ,一个很自然的方法是将像素的亮度信息编码作为输入神经元的输入。 如果图像是一个 64*64 的灰度图像, 那么我们就有 64*64=4096 个输入神经元, 每一个像素的亮度分布在 0 到 1 之间。 输出层就仅仅包含一个神经元, 输出的值如果小于 0.5 表示 “输入图像不是9”, 输出值大于0.5 表示“输入图像是9”。

景观神经网络输入层和输出层的设计通常很直观, 但是隐藏层的设计就很需要讲究了。 具体来说, 很难把隐藏层的设计用一些简单的经验法则来总结。 相反, 神经网络的研究者开发出了很多针对隐藏层的启发式设计方法, 以此来帮助人们获得能按照他们期待运行的神经网络。 例如, 一些启发式方法可以用来帮助决策如何在隐藏仓的数量和训练神经网络所需要的时间之间作取舍。 我们在本书的后面部分会见到一些这样的启发式设计方法。

到目前为止, 我们已经讨论了会把一层神经元的输出用作另一层神经元输入的神经网络。 这种网络被称为前馈(feedforward)神经网络。 这意味着, 网络中是不存在回路的 - 信息总是向前反馈, 不会向后反馈。 如果网络中有了回路,最终 σ函数的输入就会受到网络最终输出结果的影响。 那样会很难弄懂网络的工作机理, 所以我们并不允许这种回路的存在。

然而, 低于有一些其他的人工神经网络模型允许反向回馈传播回路的存在 。这些模型被称为 递归神经网络。 递归神经网络的思想是使得神经元的激活态只能维持一段时间,在神经元被激活时, 可以进一步激活其他的神经元, 这些被进一步激活的神经元同样也只能维持一段时间的激活态。 这样的过程可以使得神经元一层层地被激活, 像瀑布一样。 在这种情况下, 反向传播回路的存在并不会引发问题, 因为神经元的输出对其输入的影响仅仅会在晚一些的时刻发生, 而不是立刻发生。

递归神经网络的影响力比前向反馈网络的影响力要小, 一部分原因是尚未发现非常强有效的递归神经网络的学习算法(到目前为止)。 但是递归神经网络依旧极度有趣且值得研究。因为从生物神经领域的角度来看, 和前向反馈网络相比,递归神经网络更接近我们大脑的工作机理。 然而,为了不扩大我们学习的范围, 本书我们将专注于更加广泛使用的前向反馈网络。

一个简单的用于区分手写数字的网络

定义了神经网络之后, 让我们回到手写数字识别的问题。 我们可以把这个问题分解成两个子问题。 第一个子问题是, 我们需要设计一种方式来吧一个包含数字图像序列的图片分解成多个独立的图片。 如下:

这里写图片描述

分割成
这里写图片描述

我们人类可以很容易地解决这个切分问题。 但是要使得电脑程序来正确地切分图片则很有挑战。 一旦图片可以被正确地切分, 程序只需要识别单个的数字就行了。 例如, 我们希望我们的程序识别上述图片中的第一个数字

这里写图片描述

我们将专注于写一个程序来解决第二个子问题, 也就是, 识别单个的数字。 这样做的原因是, 一旦你有了单数字识别器以后, 分解数字序列的问题就不是那么困难了。 有很多的方法可以解决数字序列分割问题。 一种方法是, 尝试用不同的方式分割图片, 然后使用单个数字识别器为分割出的每个图片打分。如果单数字识别器可以顺利识别所有的分割结果, 则这个分割方案评分较高, 如果单数字识别器在识别分割结果上,出现了很多的困难,无法识别, 那么这个分割方案得分就低。 这种方法的背后思想是, 如果单数字识别器在识别过程中遇到了困难, 那很有可能是因为数字序列没有被正确分割导致的。 这种思想和其相应的变种可以被用来很好地解决图片切分问题。 所以, 与其担心数字分割如何解决, 不如专注于开发一个神经网络来解决更有意思且更有挑战的问题, 也就是, 识别单个的手写数字。

为了识别单个的数字, 我们会使用一个三层神经网络如下:

这里写图片描述

网络的输入层包含了用于编码像素输入的神经元。 正如上一小节所提到的, 网络的训练数据将会包含很多 28 * 28 像素的手写数字扫描图像, 因此输入层包含了 784=2828 个神经元。 输入的像素信息是灰度, 取值0代表白, 取值1代表黑。 中间值代表深度不同的灰色。

第二层是网络的隐藏层, 我们用 n表示隐藏层的神经元数量, 我们会对 n 实验不同的取值。 上图中表示了一个仅仅包含 n=15 神经元的隐藏层。

输出层里的神经网络包含 10 个神经元。 如果第一个神经元激活, 即输出 output1 , 那么就表示神经网络认为输入的数字是 “0”, 如果第二个神经元的输出为 1 , 则表示神经网络认为输入的图像为 “1”, 依次类推。 准确来说, 我们把输出层的神经元按0-9编号, 然后判断哪一个神经元输出的值最大, 输出值最大的那个神经元的编号会被当做网络对于输入图像的识别结果。

你可能会想, 为什么我们要用 10 个输出神经元。 毕竟我们的目标是让网络输出一个 0-9 的结果来告诉我们输入的图像是什么, 一个看上去很自然的方法是仅仅使用 4 个输出神经元, 把每一个神经元的输出当做二进制的一个值。 四个神经元就足够编码 0 - 9 了, 因为 24=16 , 完全足够表示 10 个值。 那么, 为什么我们还要用 10 个神经元来表示最终的结果呢? 这样设计的依据来源于经验: 我们可以将两种方案都予以尝试, 有 10 个输出神经元的网络比 4 个输出神经元的网络有更好的训练结果。 但是这会引发我们思考, 为什么 10 个输出神经元的网络工作得更好。 有没有什么直觉性的原因可以提前告诉我们为什么应该使用一个 10输出神经元的网络而非 4 输出神经元的网络。

为了理解其中的原因,从神经网络工作的最初原理来思考会很有帮助。 以 10 个神经元输出的例子来思考,首先让我们专注于第一个输出神经元, 那个用于判断输入数字是否为 0 的神经元。 它的输出结果是通过衡量隐藏层神经元输出结果而得到的。 那么, 隐藏层神经元在做什么呢? 让我们来假设隐藏层的第一个神经元在检测一个输入的图像是否像如下的结果 。

这里写图片描述

该神经元可以通过增加这种情形的像素输入的权重, 减轻其他情形输入的权重来识别该类型图案。 类似的, 让我们假设隐藏层的第二个, 第三个神经元分别在识别图像是不是如下的情形。

这里写图片描述

正如你所猜测到的, 这四个部分构成图像 0 如下:

这里写图片描述

所以当隐藏层的这4 个神经元都被激活时, 我们可以判断输入的图像是 “0”, 当然, 这不是我们用来识别数字为 0 的唯一证据, 我们可以用很多其他的方式来判断一个数字是不是 0 (例如, 上述图像的变形, 或者添加一些干扰后的情形). 但是, 在上图的这种情形下, 把输入判断为一个 “0” 是一种比较稳妥的做法。

假设神经网络是按照我们上述的假设来工作的, 那么我们可以为 10 输出神经网络比 4输出神经网络工作得更好给出一个可行的解释: 如果我们有 4 个神经元作为输出, 那么第一个神经元就是用于判断输入数字的二进制表示最高位的结果是什么。 然而, 并没有非常容易的方法可以把二进制数字的最高位与上述图像的形状关联起来。 所以很难想象, 有什么历史原因使得数字不同部分的形状和其二进制表示的最高位有什么紧密的关系。

到目前为止, 上述这些内容都仅仅是一种启发式的推断。 并没有什么明确的证据表明这个3层神经网络必须以我刚刚描述的方式运作, 即由隐藏层神经元来检测简单的图形形状。 可能一种更为精巧的学习算法可以发现一种权重分布使得我们仅仅使用4个输出神经元也可以得到很好的识别结果。 但是, 作为一种启发式的思路, 我刚刚所描述的机理是可以说的通的, 而且可以帮助你在设计好的神经网络时, 节约很多时间。

利用梯度下降算法进行学习

现在我们已经有了一个自己的神经网络设计,怎样使得“学习” 识别手写数字呢? 我们首先需要一个可供学习的数据集, 即所谓的训练数据集。 我们会使用 MNIST 数据集, 其中包含了成千上万的手写数字扫描图片, 以及他们的正确识别结果。 之所以叫 MNIST 是因为这些数据其实是 NIST 美国国家标准与技术研究院 收集的两个数据集合的经过修改(Modified)后的子集. 下面这些数字就是其中的几个图片。

这里写图片描述

正如你说看到的, 这些数字和我们在本章开头所展现的, 作为挑战等待识别的数字一样。 当然, 在测试我们的神经网络时, 我们会要求识别不再训练数据集内的图片。

MNIST 数据来源于两个部分。 第一个部分包含 60,000 个图片, 用来作为训练数据 。 这些图片是从250个人的手写数字扫描而来, 其中的半数人是人口统计局员工,另一半则是高中生。 这些图片 28 * 28 像素的灰度图片。 第二个部分是 10,000个用于作为测试数据的图片, 同样, 这些图片也是 28 * 28 像素的灰度图片。 我们将使用测试数据来评估我们的神经网络“学习”的效果是否足够好, 这些测试用的数据来源于另外250 个人(依旧是一半人口统计局员工和一半高中生)。 这有助于让我们相信训练出的神经网络可以识别不在训练数据源之内的人书写出的数字。

我们会使用记号 x 表示训练输入。 把训练的输入当做一个 28 * 28 = 784 的空间向量。 向量中的每一项代表其表示的像素位置的灰度。 我们将期望的输出表示为 y=y(x) , 其中 y 是一个 10维向量, 例如, 对于一个特定的训练图片 x , 识别结果为 6 则会表示为 y(x)=(0000001000)T 。 注意到这里的 T 表示矩阵的转置操作, 把一个行向量转置为列向量。

这里我们希望拥有一个算法可以帮我们找到合适的权重 weights和 便宜了 biases 使得神经网络的输出针对所有训练输入的输出都接近于我们训练输入的实际结果 y(x) ( 译者提示: 这里的 y(x) 很容易被误当做神经网络的输出 , 但是它其实用来表示我们期待的正确输出)。 为了量化我们是否达到了目标, 这里定义一个 成本函数(有时也被称为损失函数或目标函数):

C(w,b)12nxy(x)a2[6]

这里, w 表示神经网络中所有的权重组成的集合, b 表示所有的偏移值组成的集合, n 表示训练输入的数量。 求和的计算是针对所有的输入 x 进行的。当然, 输出 a 取决于 x, w, 和 b , 但是为了使表达式比较简单, 我并没有明确表达这种依赖关系。 记号 v 表示的是常见的求矢量长度的函数。我们将会把 C称为二次成本函数( quadratic cost function)。 有时它也被称为均方差(mean squared error)或 MSE。 观察这个二次成本函数的形式, 我们会发现 C(w,b) 是非负的。 进一步来说, 成本函数 C(w,b) 尽可能变小时, 也就是, C(w,b)0 时, 也就是对于所有的输入x, 都有 y(x) 约等于输出 a 。所以我们的训练算法如果能够找到一组权重值 w 和偏移量 b 使得 C(w,b)0 , 就可以认为是大功告成了。 所以我们的训练算法的目标就是寻找 w 和 b 的值, 使得 C(w,b) 取最小值。 我们将通过一个被称之为 梯度下降 的算法完成这个目标

为什么要引入二次成本函数呢? 我们所感兴趣的不是怎样使得正确识别的图片数量最大化吗, 为什么不直接试着寻找能正确识别图片数量的最大值呢, 而是间接的来寻找成本函数的最小值呢?

这里的问题在于, 正确识别的图片数量并不是一个关于 w 和 b 的平滑函数。 大部分情况下, 对于权重和偏移量的细微改变, 并不会导致正确识别图片数量的变化。 这会使得我们很难搞清楚怎么样调整权重值和偏移量, 才能改善我们的神经网络识别效果。 如果我们使用一个平滑的函数, 例如二次成本函数, 那么就更容易搞清该如何调整权重和偏移量以改善神经网络的效果。 这就是为什么我们要专注于如何最小化成本函数, 以及为什么通过成本函数, 我们才能检验神经网络识别准确度。

即便知道了我们想使用一个平滑的成本函数, 但你依旧可能好奇, 为什么我们使用 等式【6】形式的二次函数而不是一些其他的函数。 这难道是一种特别的选择吗? 是不是我们选择了另一种不同的成本函数就会得到完全不同的结果呢?

这是一个合理的假设,之后我们会重新思考成本函数, 对它做一些改变。 然而, 等式【6】描述的二次成本函数足以帮助我们理解神经网络的工作基本原理, 所以我们目前会继续使用这个二次成本函数。

再强调一下,我们的目标是为我们的神经网络寻找到一组权重和偏移量, 使得二次成本函数 C(w,b) 的值最小化。 这已经是一个被转化的,非常明确的目标了, 但是它还是有者让人心生畏惧的复杂结构: w 和 b 都是矢量, σ 函数还需要被代入到这个等式中, 神经网络的结构也没有在函数体现, 我们的输入数据集 MNIST 还很复杂等等一系列问题。 但事实上, 我们只要忽略这里面大部分结构, 仅仅关注如何最小化成本函数这一件事, 我们可以理解大量的内容。 所以, 目前, 我们将暂时忘记成本函数的具体形式, 神经网络错综复杂的连接等等一系列问题。 相反, 让我们想象我们仅仅拥有了一个变量很多的函数, 而我们想要将这个函数最小化。 我们会开发出一种名为梯度下降的技巧来解决这个最小化问题。 然后我们会重新回来思考我们真正想要最小化的那个具体的, 神经网络的成本函数。

好的, 让我们来想象一下, 我们试图最小化某个函数 C(v), 这个可能是一个拥有很多变量的实数函数, v=v1,v2,.... 注意到, 我把 w 和 b 的记号替换成了 v ,以此来强调这里的 C(v) 可以是任意的一个函数。重要的事情再说一遍, 我们现在并不是在思考公式【6】 所描述的二次成本函数, 而是在思考如何最小化一个一般的, 可以是任意形式的函数。 为了找到 C(v) 的最小值, 先将 C 想象成一个只有两个变量v1,v2的函数会容易很多。

这里写图片描述

我们所希望找到的是, Cv1,v2 的值在什么位置才能全局最小。 现在, 假如函数的形状是上图这样, 我们就可以通过肉眼直接找出取使得Cv1,v2 取最小值的位置(v1,v2)坐标。 这样的话, 我可能给出了一个太简单的函数。 但是对于一个一般的函数 C, 它可能是一个拥有众多变量的,非常复杂的函数,通常是不可能通过画出函数形状来直接寻找最小值的。

一种解决这个问题的方法是利用微积分学来分析性地寻找最小值。 我们可以计算出函数的导数来寻找函数取极值的位置。 如果我们运气好的话, 函数仅仅包含一个或几个变量时, 这样是可行的,但是当变量特别多时, 这种方法就会变成噩梦。 而对于神经网络而言, 我们常常会需要更多的变量,最大的神经网络拥有依赖于上亿的权重值和偏移量的成本函数, 且形式极为复杂。 使用微积分的方式没有办法寻找其最小值。

好的, 所以微积分并不起作用。 幸运的是, 有一个漂亮的类推帮我们推荐了一种可以很好起作用的算法。 我们现在把函数的形状想象成一个山谷, 就像上一张图片一样。 想象一个球从山谷上滚到了谷底。 或许我们可以用这种想法来寻找到一个函数的最小值? 我们可以随机的为这个球寻找一个起始点, 然后模拟一下球滚向谷底的运动方向。 我们可通过计算 C 的导数来完成这个模拟过程。 导数值会告诉我们球所在“山谷”位置的形状, 继而让我们知道球会向哪个方向滚动。

基于我刚才所说的, 你可能以为我们在试图写出球的牛顿运动方程, 需要考虑摩擦力和重力等等的。 但其实, 我们并不会真的去模拟球的下滚, 我们是在发明一种算法来寻找函数 C 的最小值, 并不是发明一种精确模拟球物理学上的下滚。 这种举例只是用来激发我们的想象,而不是束缚我们的思考。 所以与其被物理学的复杂细节所困扰, 不如让我们简单的问自己一个问题: 如果我们自己做一天上帝, 可以编写我们自己的物理学法则, 来指示球应该向哪里滚动, 我们会选择哪种运动法则使得球总是滚向谷底呢?
为了使得这个问题更为明确, 让我思考一下,当我们将球轻微地移动很少的量 , 即在v1 方向移动很小的量 Δv1, 在v2 方向移动很小的量 Δv2。 微积分(不熟悉的童鞋可以百度一下全微分的定理和法则)告诉我们函数 C 会这样变化:

ΔCCv1Δv1+Cv2Δv2[7]

我们会寻找到一种选择 Δv1Δv2 的方法, 来使得ΔC 是负值, 这样也就相当于找到了一种方法让“球总是向谷底滚”。, 为了搞清楚该如何选择 Δv1Δv2 , , 我们定义矢量 Δv(Δv1,Δv2)T。 这里 T 还是用来表示转置, 使得行向量变为列向量。 我们还会将函数 C 的“梯度”定义为偏微分矢量, (Cv1,Cv2)T 。 我们用记号 C 表示梯度向量:
C(Cv1,Cv2)T[8]

一瞬间, 我们把改变量记号 ΔC 改写为了梯度 C 。 在这里, 我想澄清一下梯度符号有时会让人产生困惑的地方。当第一次见到梯度符号时, 人们可能会思考我们该如何理解 这个符号呢? 到底意味着什么。 事实上, 单纯地将 看做一个单一的数学对象, 也就是公式【8】定义的向量是完全ok的。从这个角度而言, 仅仅是一个小旗子一样的记号, 告诉你 “嘿,C 是一个梯度向量哦 ” 。 不过, C 符号还有一些更高级的角度使得它可以被看做一个独立的数学实体, 但是目前我们并不需要那种角度。

有了以上的定义后, ΔC 的表达式 【7】 就可以被重写为:

ΔCCΔv[9]

  • 译者注: 对两个向量执行点乘运算,就是对这两个向量对应位置的元素一一相乘之后求和的操作,点乘的结果是一个标量。

这个等式可以帮助我们解释梯度向量 C , Cv 的变化量和 C 的变化量关联了起来, 就像我们所期待的“梯度”(斜率)所能做到的一样。 但是这个等式真正令人兴奋的地方在于, 它让我们看到了如何选择 Δv 才能使得 ΔC 变成负值。 假设我们选择:

Δv=ηC[10]

这里, η 是一个很小的正值(被称之我学习率 learning rate), 那么等式 【9】则告诉我们 ΔCηCC=ηC2 。 因为 C20 , 所以这就保证了 ΔC0 , 也就是说,如果我们根据表达式 10 的指示来改变 vC 总是减小的, 而不会增加。 这正是我们想要的性质! 所以我们将会用等式 【10】 来定义我们梯度下降算法中的 “小球运动法则”。 我们会用等式 【10】 来计算 Δv , 然后按照计算出的 Δv 移动“小球“:

vv=vηC[11]

然后我们会再次使用这个规则, 进行下一次移动。 只要一直重复这个动作, 我们就能够一直减小 C 直到我们达到一个全局最小值。

总结一下, 梯度下降算法工作的方式就是, 不断地重复计算梯度 C , 然后向梯度正值相反的方向移动, “沿着坡的下方向滚动”。 正如下图所示:
这里写图片描述

注意到这个梯度下降的规则并不是重新了物理上的移动。 在现实世界中, 一个球在下滚的过程中会有动能, 那个动能会使得球可以从谷底冲上坡, 只有在由摩擦力造成动能转化的情况下, 才能使得球总是会停在谷底。 对比而言, 我们的规则在选择变量 Δv 时, 仅仅是在说, “向下走, 立刻” , 是一个寻找最小值的优秀法则。

为了使得梯度下降算法正确的工作, 我们需要恰当的选择学习率 (learning rate) η 为一个足够小的量, 才能尽可能的模拟等式 9 。 如果我们不这样做, 我们可能最后会得到 ΔC>0 的结果。 同时我们又不希望 η 过于小, 以至于 Δv 过于小,进而使得梯度下降算法工作的十分缓慢。 在实际的算法实现中, η 通常会变化, 使得等式 9 有一个好的近似值,且使得算法不会太缓慢。 之后我们会看到它具体是如何实现的。

我已经解释了梯度下降算法在 C 是一个二元函数时, 是如何工作的。 但事实上, 在 C 拥有更多的变量时, 所有的东西依旧成立, 假设 C 是一个包含 m 个变量,v1,v2,...,vm的函数。 那么 ΔCΔv=(Δv1,...,Δvm)T 的关系就是(译者注: 这里用到的还是全微分定理和相关法则, 但是此处没有提到判断 C 函数是否可微的问题。

ΔCCΔv[12]

其中, 梯度 就是:

C(Cv1,...,Cvm)T[13]

正如两个变量的情况一样,我们可以选择:

Δv=ηC[14]

然后,我们就可以保证等式 12 中的 ΔC 是负值。 然后继续按照规则更新变量 v:

vv=vηC[15]

你可以将认为这个 v 的更新规则就是梯度下降算法的定义。 它给了我们一种方法, 通过重复地改变变量 v 的位置去寻找函数 CC 的最小值。 这个规则并不总是能起作用, 其中有一些环节可能会出问题, 导致梯度下降算法没办法找到 CC 全局最小值, 这种情况我们会在后续章节里面探讨。 但是, 实践中, 梯度下降算法的效果通常都是极好的, 在神经网络中,我们会发现它是一种最小化成本函数极度强大的工具, 进而可以帮助网络进行学习。

事实上, 从某种角度而言, 梯度下降算法甚至是寻找最小值的最优策略。 假设我们试图进行一个 \Delta vΔv 的位移, 来使得 C 减少的尽可能多。 这就等同于在最小化 \Delta C \approx \nabla C \cdot \Delta v, 我们会限制移动量的大小使得 \left\|\Delta v\right\| = \epsilon , 其中 \epsilon 是某个固定的很小的正值 \epsilon > 0。 换句话说, 我们的移动距离是固定的,我们希望找到一个方向能使得这个移动的效果尽可能明显,也就是移动以后, C 减少的尽可能多。 可以证明,给定限制 \left\|v\right\| = \epsilon 时, 当 \Delta v = -\eta\nabla C , 其中 \eta = \epsilon/\left\|\nabla C\right\| 时, \Delta C = \nabla C \cdot \Delta v 可以取到最小值。

译者证明: 根据柯西施瓦兹不等式, 两个向量 u, v 有如下不等式关系
\left\|u\right\|\left\|v\right\| \ge \left\| u\cdot v\right\|

所以当我们已知 \Delta C = \nabla C \cdot \Delta v\left\|v\right\| = \epsilon 时,

如果令 \Delta v = -\eta\nabla C = ( -\epsilon/\left\|\nabla C\right\| ) \nabla C,


\begin{align*} \Delta C &= \nabla C \cdot \Delta v \\&= \nabla C \cdot ( -\epsilon/\left\|\nabla C\right\| ) \nabla C \\&= \nabla C ^2 ( -\epsilon/\left\|\nabla C\right\| ) \\&= \left\|\nabla C\right\| ^2 ( -\epsilon/\left\|\nabla C\right\| ) \\&= -\epsilon \left\|\nabla C\right\| \\&= -\left\|\Delta v\right\|\left\|\nabla C\right\|\end{align*}

根据柯西不等式知 \left\|\Delta v\right\|\left\|\nabla C\right\|\ge \left\| \Delta v \cdot \nabla C \right\|, 所以, -\left\|\Delta v\right\|\left\|\nabla C\right\| 必然为 \nabla C \cdot \Delta v 的最小值

人们已经研究调查过很多梯度下降的变种, 包括更接近物理学球下滚模拟的变种算法, 这些球下滚模拟的变种有一些优势, 但是也有很明显的缺点: 不可避免地需要计算二阶偏微分, 这个计算代价很高。为了搞清楚为什么说他们计算代价很高, 现假设我们要计算二阶偏微分 \partial ^2 C / \partial v_j \partial v_k 。 如果有一千万个 v_j 这种的变量, 那么我们就需要进行一万亿 (一千万的平方)次这样的微分计算, 这样会产生极大的计算负担! 虽然这样说, 但是还是有一些技巧来避免这种问题, 而且寻找梯度下降算法的替代方案也是研究的热点领域。 但是在这本书中, 我们将使用梯度算法(和其变种)作为我们进行神经网络学习的主要方法。

那么, 到底我们应该该如何应用梯度下降算法来进行神经网络学习呢。 核心思想是用梯度下降算法来寻找能够最小化等式【6】 所描述的成本函数的权重 w_kb_l . 为了弄清具体如何使用, 让我们将 w_kb_l 代入公式后, 重新审视一下梯度下降的更新变量的规则, 换句话说, 我们之前所说的球的“位置”对应的变量现在由 w_kb_l 构成, 梯度向量 \nabla C 由两个部分 \frac{\partial C}{\partial w_k} \frac{\partial C}{\partial b_l} 两个部分构成。 针对这两个部分写出梯度下降算法的变量(“球的位置”)更新规则如下:

\begin{eqnarray} w_k & \rightarrow & w_k' = w_k-\eta \frac{\partial C}{\partial w_k} \tag{16}\\ b_l & \rightarrow & b_l' = b_l-\eta \frac{\partial C}{\partial b_l}.\tag{17}\end{eqnarray}

  • 译者注: 这里的代入过程,可能会让人困惑, 因为神经网络中的在给定一个输入 x 以后, 输出其实是一个的变量为所有的 w 和 所有的 b 的复杂形式的函数, 其中可能有 k 个权重值 w_{i=1, 2,3,..., k}l 个偏移量的值 b_{j=1,2,3,..., l } 。 所以其最终的成本函数 C 其实是一个拥有 k+l 个变量的函数。 虽然变量众多, 但是正如之前所说, 梯度下降的变量更新法则不仅适用于两个变量的函数, 对于多变量的函数同样适用。 所以这里就得到了如上的两条变量更新规则【16】【17】。

通过重复引用上面的更新规则, 我们可以“滚下坡”, 有望找到找到一个成本函数的最小值, 换句话说, 这是一个可以用来进行神经网络“学习”的规则。

在应用这个规则时, 会遇到很有挑战性的问题,我们会在后面的章节里面探究这些问题。 但是目前, 我只想提到其中的一个问题,为了理解这个问题是什么, 让我们重新回顾一下公式【6】中的二次成本函数。注意到这个成本函数的形式是 C = \frac{1}{n} \sum_x C_x , 它其实是在求成本 C_x 在 x 取不同值时的平均值。 注意到对于单个的训练输入 x , 输出 C_x \equiv \frac{\|y(x)-a\|^2}{2}

译者再次提醒: y(x) 这里不是神经网络会随权重 w_k 和 偏移量 b_l 调整而变化的输出, 而是给定一个 x 后, 我们期望的输出。 a 才是会随权重 w_k 和 偏移量 b_l 调整而变化的输出, 这里的 y(x) 和 a 都是会随着 x 的变化而改变的

在实践中, 为了计算梯度 \nabla C ,我们需要为每个 x 分别计算 \nabla C_x , 然后再对他们求平均值, \nabla C = \frac{1}{n} \sum_x \nabla C_x, 不幸的是, 当训练输入数量非常庞大时, 这可能会花费很长时间, 导致“学习”速度非常缓慢。

有一个被称为随机梯度下降的想法可以被用来加速学习过程,这个想法是将一小部门随机选择的输入作为样本集, 然后针对这个样本集计算 \nabla C_x , 然后计算这些来自小样本集的结果的平均值, 我们可以很快地得到一个良好的, 关于真正 \nabla C 的估计值。 这可以帮助我们加速梯度下降, 进而加速学习的过程。

为了使得这个想法更为清晰准确, 随机梯度下降算法的具体是这样工作的:
1. 首先随机选择少量 m 个 训练输入 X_1, X_2, ...,X_m , 将这些输入称为 “迷你批”mini-batch, 就是一小批数据)。 “迷你批” 的大小 m 需要足够大, 至少大到满足如下条件:

\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}

也就是说, m 个输入的平均值要接近于全体输入的平均值才行。

上述等式的两边互换一下我们就得到:

\begin{eqnarray} \nabla C \approx \frac{1}{m} \sum_{j=1}^m \nabla C_{X_{j}},\tag{19}\end{eqnarray}

这个公式表示了我们可以通过计算 “迷你批”输入的梯度而估计所有输入的“梯度”。

为了将上述的结论和我们神经网络的“学习”连接在一起, 让我来代入结论, 重新看一下梯度下降的规则:

假设 w_kb_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}


这里的 m 个 X 就是从所有输入中随机选出的一个“迷你批”数据集。然后我们会再随机选出另一个“迷你批”数据集, 再以他们作为训练输入。然后, 继续进行这个过程, 当我们耗尽了所有的训练输入后, 就算完成了一个“纪元”(epoch) 的训练, 然后会开始新纪元的训练。
1
上述的这种规则偶尔会随着成本函数输入的规模和迷你批对权重和偏移量的更新次数的规模增长而变化。 在等式【6】中, 我们将总体的成本函数用因子 \frac{1}{n} 均摊开来。 但是人们有时会忽略这个因子\frac{1}{n},将每一个输入的成本都加起来求总和而不是求平均。 这在训练输入的量不能提前确定时是尤其有用的,例如, 训练的过程中可能会有更多实时产生的新的训练输入被生成出来。

类似的, “迷你批更新规则” 【20】和 【21】 有时也会省略 \frac{1}{m}。 概念上, 这好像不会产生什么影响, 因为这等同于重新重新调整了 \eta 的大小, 但是在做一些细节性的对比时, 这种改变是值得小心对待的。

我们可以将随机梯度下降想成民意调查: 从一个小样本集中计算梯度要比从所有数据集中计算梯度容易地多, 这就像是做民意调查要比做一轮完整的全民选举要容易地多一样。 例如, 如果我们有一个大小 n = 60,000 的训练集, 正如 MNIST 数据集一样, 选择一个迷你批的大小 m=10 就意味着我们可以在估计梯度时, 可以提速 6000倍!当然,估计并不是完美的, 会有概率性的抖动, 但是它并不需要是完美的: 我们真正关心的是, 整体上朝着帮助函数 C 减小的方向移动, 这意味着我们并不需要梯度的精确值。 在实践中, 随机梯度下降算法是一个被广泛采用且强有力的神经网络“学习”技巧, 它也是我们这本书将要开发的大部分“学习”技巧的基础。
让我们通过讨论一个常常令刚刚接触梯度下降的人感到困惑的点来结束本节内容。 在神经网络中, 成本函数 C 是一个众多变量的函数- 其中包括所有的权重和偏移量- 所以, 在某种意义上, 它定义了一个很高维度的空间。 人们有的时候会卡在这里思考: “嘿, 我必须能够把这些额外的维度具象化(在脑海中形成图像概念)才行”。 然后他们会开始担心: “我没办法思考四维的空间是什么样, 更别说五维或者五千万维空间”。 我们是不是缺少什么特别的能力, 一些“真正” 的数学家才具有的能力。 当然, 答案是NO。 即便是最专业的数学家也没有办法很好地具象化。 他们所使用的技巧是, 与其去具象化四维空间, 不如尝试开发出其他的表示四维空间的方式。 这也正是我们之前所做的: 使用线性代数的形式来表示 \Delta C, 进而来搞清楚应该如何移动才能减小 C 。 善于在多维空间思考的人头脑中包含了一个技巧库, 其中有很多不同的这种类型的技巧。 那些技巧可能不具备和我们所习惯的可视化的三维空间的简单性, 但是一旦你也搭建形成了这种技巧库, 你也可以很好地在高维空间思考。 我不会在这里赘述太多细节, 但是如果你有兴趣的话, 你可能会对这里有关专业数学家思考高维空间的技巧很感兴趣。 尽管这些技巧可能会很复杂,但是其中最好的技巧都是很符合人的直觉且可以理解的, 也可以被任何一个人掌握

python 实现数字识别神经网络

好的, 让我们动手写一个程序利用 MNIST 数据集和随机梯度下降算法 来“学习”识别手写数字。 我们将会写一个短的简短的 python(版本2.7) 程序, 仅仅包含 74 行代码!我们第一件要做的事是获得 MNIST 数据。 如果你是一个 git 用户, 那你可以通过 clone 下面的本书代码的 repository 。

git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git

如果你不使用 git, 那么你可以从这里下载代码和数据。

我之前描述 MNIST 数据的时候, 我说过, 它们被枫成了 60,000 个训练图片和 10,000 个测试图片。 这是 MNIST 的官方描述。 但事实上, 我们以一种略微不同的方式划分数据: 我们不会动测试图片, 但把 60,000 个训练图片划分为两部分。 一部分包含 50,000 张图片, 我们将用它们来训练我们的神经网络, 另外的 10,000 张图片作为校验集。 在这一章里, 我们不会使用校验集数据, 但是本书的后面, 我们会发现校验集的数据在我们试图搞清该如何设置 超参数 (hyper-parameters)- 例如学习率等等的参数 时会很有用, 这些参数不会被我们的学习算法直接选择。尽管校验集数据不是原来 MNIST 规范数据的一部分, 但是很多人都会以这种方式来使用 MNIST 数据, 而且校验集数据在神经网络中是很常见的。 从现在开始, 当我提到 “MNIST 训练数据”, 我就是在指代那 50,000 个图片。 而不是 MNIST 原来包含的那 60,000 个图片。

正如我之前标注的, MNIST 数据集是基于两个由 NIST(美国国家标准委员会) 收集的数据集。 为了构建 MNIST 数据集 ,NIST 数据集被 Yann LeCun , Corinna Cortes, and Christopher J. C. Burges 这几个人拆分处理成更加便捷易用的格式。你可以从这里获得更多细节 。 这些数据集在我的代码库里被处理成了更容易被 python 加载和操作的形式。 这种形式的数据是我从蒙特利尔大学LIST机器学习实验室获得的。

除了 MNIST 数据, 我们还需要一个 python 库 Numpy, 用于进行更快的线性代数计算。 如果你还没有安装 Numpy, 你可以从这里获得它

让我们在给出完整的 python 代码前先解释一些神经网络代码的核心特性。 这里的核心类是一个 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:])]

在上面这段代码中, list 类型的数据 sizes 存储了神经网络各层的的神经元个数。 例如, 如果我们要创建一个 在第一层拥有两个神经元, 在第二层拥有 3 个神经元, 在最后一层拥有 1 个神经元的 Network 对象, 我们会写出如下的代码:

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

Network 对象的权重值偏移量都是用 Numpy np.random.randn 函数随机初始化的,该函数会生成位置参数 \mu = 0 和尺度函数 \sigma = 1 的正态分布。 这样的随机初始化给我们的随机梯度下降算法提供了一个“起点”。 在后面的章节中, 我们会找出更好的方式来初始化权重 weights 和偏移量 biases, 现在我们暂且先采用这种初始化方案。 注意, Network 的初始化代码假设第一层神经元是输入层, 然后省略了为这一层神经元赋偏移量 biases 值的操作, 因为偏移量只会在非输入层的神经元计算输出的时候用到。

还需要注意, 权重 weights 和 偏移量 biases 的值存储为 Numpy 矩阵的 list 对象。所以, 样例代码中的 net.weights[1] 是一个 Numpy 矩阵, 存储了连接第二层和第三层神经元的权重值(不是第一层和第二层, 因为 python 的list 索引是从 0 开始的 )。 因为 net.weights[1] 的表示太冗长, 我们索性用 w 表示这个矩阵。这个矩阵中, w_{jk}是连接第二层第 k 个神经元和第三层第 j 个神经元的权重。 这个 j k 的顺序看上去有些奇怪,把 j k 的位置互换一下不是更符合人的习惯吗。 使用这种顺序的一个很大好处是第三层神经元的激活向量就是:
\begin{eqnarray} a' = \sigma(w a + b).\tag{22}\end{eqnarray}

这个等式中包含了不少的内容, 让我们把它一一分解开来。 a 是第二层神经元的激活向量。 为了获得第三层神经元的激活向量 a' , 我们用权重矩阵 w乘以 a, 并且加上偏移量向量 b 。 然后对计算出的向量结果 w a + b 的每一个元素应用 \sigma 函数计算。 很容易验证等式 【22】 和我们之前的等式 【4】的计算规则是一样的。

搞清楚了以上的内容, 就容易写出计算一个网络输出的代码了 。 我们从定义 S型函数(sigmoid function)开始:

def sigmoid(z):    return 1.0/(1.0+np.exp(-z))

注意到, 当输入 z 是一个向量或者 Numpy 数组时, Numpy 会自动的对每个元素依次计算。也就是以向量形式进行计算。

然后, 我们向 Network 类里加一个前向反馈传播的方法, 该方法在给定一个神经网络的输入后, 会返回相应的输出*。 该方法的全部功能就是对每一层计算等式【22】

  • *注解: 该方法假设输入是一个 (n,1) Numpy ndarray, 而不是一个 (n,) 向量。 这里, n 是神经网络的输入数量。 如果你试着使用 (n,) 向量作为输入, 你就会得到奇怪的结果。尽管使用(n,) 向量似乎是一种更加自然的选择,但使用(n,1) ndarray 使得我们更容易更改代码来向前一次性传播多个输入, 这在有些情况下是非常方便的。

当然, 我们想让 Network 对象们做的最主要的事情是“学习”。 为了实现这个目标, 我们需要给他们一个 SGD 方法, 该方法会实现随机梯度下降算法( Stocastic Gradient Descent)。 下面是代码, 这段代码有几处会比较难懂, 但是我会把他们拆解开来进行讲解:

def SGD(self, training_data, epochs, mini_batch_size, eta,            test_data=None):        """利用迷你集随机梯度下降算法(mini-batch stochastic gradient descent)训练神经网络。 “训练数据” 是一个由多个 tuple (x,y) 组成的list(tuple 和 list 都是 python 中的非常基础的数据结构) . (x, y) 代表着训练输入和输入对应的期望输出。   其他必选参数的含义都可以根据名字看出来。 如果 test_data 非空, 那么 network 对象就会在每一个世代 epoch 训练后, 被测试数据评估一遍, 然后, 当前的进度会被输出。 在需呀追踪神经网络的训练进度时, 这是很有用的, 但是这会极大的减缓网络的训练速度。 """        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 是由 tuples (x, y) 构成的 list , 代表训练的输入和与之相对应的期望输出。 变量 epochsmini_batch_size 的含义和你期待的一致 - 分别表示要训练多少个世代(将世代epoch 理解成一个最小粒度的训练周期即可)以及迷你集(mini-batch) 的大小。eta 就是学习率 \eta ( 该符号的英文表示就是 eta) 如果可选的参数 如果可选参数 test_data 被提供了, 那么程序就会对 Network 对象在每一个世代的训练结束后, 使用测试数据进行评估, 然后输出结果作为当前的训练进度, 但是这会很大幅度的降低训练速度。

这段代码是以如下的方式工作的 :

  1. 在每个 epoch 中, 它首先随机打乱训练数据, 然后把他们划分成很多份合适尺寸的 mini-batches 。 这是一种从训练数据中随机取样的简单方法。

  2. 然后, 对于每一个 mini-batch 我们完成梯度下降的一小步。 这是通过代码 self.update_mini_batch(mini_batch, eta) 完成的。 该方法仅仅使用 mini_batch 中的数据, 根据单次梯度下降迭代规则来更新神经网络的权重 weights 和 偏移量 biases. 下面是 update_mini_batch 方法的代码。

 def update_mini_batch(self, mini_batch, eta):        """ 通过对单个迷你数据集 mini-batch 应用梯度下降以及后向扩散, 更新神经网络的权重 weights 和偏移量 biases.  mini_batch 是多个 tuple (x,y) 组成的 list。 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)

这行代码调用了一种名为 反向传播( backpropogation) 的算法, 该算法是一种快速计算成本函数梯度的算法。 所以 update_mini_batch 可以仅仅通过计算迷你数据集中的每一训练样例的梯度来, 然后利用计算出来的梯度来更新 self.weightsself.biases

现在我暂时不打算展示 backprop方法的代码。 目前, 让我们就暂且将其当做一个现成可用的黑盒, 返回训练输入样例 x 所对应的成本函数的梯度值。(译者注: 下一章节会对这里的实现细节做详细的说明)

让我们来看一下整个程序, 包括我之前省略的注释内容。 除了 self.backprop , 整个程序都是清晰易读的 - 所有的复杂工作都是在 self.SGDself.update_mini_batch 中完成的, 这两个方法我们已经解释过了。 self.backprop 方法利用了一些其他二外的函数来辅助计算梯度, 例如 sigmoid_prime 函数, 该函数计算了 \sigma 函数的导数。 另外还有 self.cost_derivative方法, 在这里我暂且先不解释该方法。 你可以仅仅通过阅读代码和文档注释来理解这些函数的主要功能。 我们会在下一个章节中来详细解释其中的细节。 注意, 虽然代码看上去很长, 但是大部分代码都是用来增加代码可读性的文档注释。 事实上, 整个程序仅仅包含 74 行不含空格,不含注释的代码。 所有的代码可以在GitHub 的这个链接 上找到。

""" network.py~~~~~~~~~~A module to implement the stochastic gradient descent learningalgorithm for a feedforward neural network.  Gradients are calculatedusing backpropagation.  Note that I have focused on making the codesimple, easily readable, and easily modifiable.  It is not optimized,and omits many desirable features."""#### Libraries# Standard libraryimport random# Third-party librariesimport numpy as npclass 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 functionsdef 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 , 来进行。 我们可以在 python shell 中通过执行一些命令来完成。

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

当然, 这可以在另外一个单独 python 程序中完成, 但是如果你一直沿着这条路走下去, 你就会发现在 Python shell 里面做可能是最简单的一种方法。

在我们加载了 MNIST 数据后,我么会搭建一个拥有 30 个隐藏神经元的 Network。 我们会在 import 之前列出的 74 行 Python 程序(程序名为 network)后进行这项工作。

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

终于, 我们要用梯度下降算法来从 MNIST 数据中进行学习30 个世代 (epoch)的学习了, 我们的迷你数据集 mini-batch 大小设为 10, 学习率 \eta=3.0

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

注意到, 如果你一边阅读, 一遍执行这个代码, 你会发现它要花费一段时间才能执行完, 对于一台典型的(2015年水平)的机器而言, 它可能会花费几分钟才能执行完。 我建议你让代码跑起来, 然后继续阅读, 然后间隙性地去查看代码的输出结果。如果你很着急, 你可以通过减少 epochs 的值或减少隐藏神经元的数量或仅仅使用一部分数据集来加速运行。 注意, 真正用于生产环境的代码会快得多, 这些 python 脚本仅仅是用来帮助你理解神经网络的工作机理, 而不是一些高性能的代码。 而且, 一旦我们训练好了一个网络, 他就能很快地运行在任意的计算平台上了。 例如, 一旦我们获得神经网络的一组良好的权重值和偏移量值, 他们可以很容易地被迁移到浏览器中的 Javascript 中或是作为一个手机 App 中。

总而言之, 这里有一份神经网络一次训练的部分输出。 下面的输出显示了测试图片中有多少图片在一个世代的训练后, 被神经网络正确地识别了出来。 正如你看到的, 在仅仅一个世代的训练后, 该网络就能从 10,000 个图片中正确识别 9,129 个图片了, 然后正确识别的数量还会随着训练继续增长。

Epoch 0: 9129 / 10000Epoch 1: 9295 / 10000Epoch 2: 9348 / 10000...Epoch 27: 9528 / 10000Epoch 28: 9542 / 10000Epoch 29: 9534 / 10000

也就是说, 训练出来的神经网络的识别最大正确率约为 95% - 95.42( Epoch 28) 是它的峰值 ! 这个结果对于首次尝试可是令人振奋的。 不过, 我要提醒你的是, 你在运行代码的时候, 你的结果并不一定和我一样, 因为我们随机使用不同的权重值和偏移量初始化神经网络的。为了产生上述的结果, 我取了三次运行中的最好的一次结果。

让我们将隐藏神经元的数量修改为 100 后, 重新做如上的实验。 正如之前所说的, 如果你一边阅读, 一边运行代码, 你会发现它要花一些时间才能执行完( 在我的机器上, 每一个 training epoch 要花费数 10 秒), 所以一边阅读,一边等待代码执行是一个很好的选择。

>>> net = network.Network([784, 100, 10])>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

当然, 上面的改变将结果改善到了 96.59% 。 至少在当前的案例中, 使用更多的神经元可以帮我们得到更好的结果。

*注: 读者反馈说, 这个实验的结果有各种各样的, 有些情况下, 调整后输出的结果变得更差了一点。 通过使用第三章的技巧,我们可以很大程度的减少在不同次运行时神经网络性能表现的差异。

当然, 为了获得这种准确率, 我不得不对训练的 epoch 数量, mini-batch 的大小, 学习率 \eta 的值做一些特殊的选择。 正如我之前提到的, 这些值被称为神经网络 “超参数 (hyper-parameters)”, 这样叫是为了把他们和其他由神经网络学习出来的参数(权重 weights 和偏移量 biases) 区分开。 如果我们选的超参数很差, 那么我们得到的结果也会相应变差, 例如, 如果我们将学习率设为 \eta = 0.001

>>> net = network.Network([784, 100, 10])>>> net.SGD(training_data, 30, 10, 0.001, test_data=test_data)

结果就不那么好了

Epoch 0: 1139 / 10000Epoch 1: 1136 / 10000Epoch 2: 1135 / 10000...Epoch 27: 2101 / 10000Epoch 28: 2123 / 10000Epoch 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)

这样我们就会离我们想要的结果偏离比较远了, 学习率也过高:

Epoch 0: 1009 / 10000Epoch 1: 1009 / 10000Epoch 2: 1009 / 10000Epoch 3: 1009 / 10000...Epoch 27: 982 / 10000Epoch 28: 982 / 10000Epoch 29: 982 / 10000

现在, 让我们想象我们第一次尝试遇到的是这种结果。 当然,从先前的试验中我们知道, 正确的做法是减小学习率。 但是如果我们第一次尝试就遇到这种结果, 输出的结果中并没有什么信息指示我们应该如何调整参数。 我们可能不仅仅会担心学习率是不是设置的不合适, 还有可能担心神经网络其他的任意一个方面。我们可能回想, 是不是权重和偏移量被初始化成了一种很难训练的值。 又或者我们的训练数据不够多, 没办法完成有效的训练? 又或者我们训练的 epoch 不够多? 又或者, 这种神经网络结构根本无法识别手写数字?又或许是学习率太低或是太高了?

当你第一次遇到这种问题是, 你总是无法确定的。 这里想要告诉你的是, 调试一个神经网络并不是一件轻而易举的事, 而且, 就像正常的编程一样, 其中是由诸多技巧甚至说艺术的。你需要学会调试的艺术才能从神经网络中获得好的结果。 更一般的说, 我们需要建立选择良好的超参数和好的神经网络架构的直觉。我们会在本书的后面部分花费大篇幅来讨论这些问题。 包括我们如何选择上面的超参数。

之前, 我跳过了 MNIST 数据是如何加载的细节。 这个过程是非常清晰直白的。 为了完整性, 这里展示其代码。 用来村塾 MNIST 数据的数据结果在注释中有描述, 都很显而易见,由 Numpy ndarray 对象构成的 tuple, list对象(如果你不熟悉 ndarray的话, 就把它当做向量即可)

"""mnist_loader~~~~~~~~~~~~A library to load the MNIST image data.  For details of the datastructures that are returned, see the doc strings for ``load_data``and ``load_data_wrapper``.  In practice, ``load_data_wrapper`` is thefunction usually called by our neural network code."""#### Libraries# Standard libraryimport cPickleimport gzip# Third-party librariesimport numpy as npdef 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')    training_data, 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

我在上面说过, 我们的程序的输出结果很理想。 这意味着什么? 我是在和什么作对比? 如果有一些(非神经网络)比较的基准线测试会更好地理解怎样才算是表现理想。 最简单的比较基线当然是随机猜数字。 每次猜对的概率只有 10% 。我们比随机猜做的好多了!

那么, 如果我们提高我们比较的标准呢, 让我们尝试一个极其简单的想法: 根据一个图像的“有多黑”来识别它是什么数字。 例如, 一个 2 肯定是比 1 要暗的, 仅仅因为有更多的像素被黑色占据, 如下图所示。

这里写图片描述

这种想法暗示我们为每个数字 0,1,2,3,…,9 计算平均灰度。 当一个新的数字被展示出来时, 我们计算一下这个图片有多黑, 然后选择最接近的平均灰度所对应的数字作为其识别结果。 这是一个很简单的过程, 很容易用代码写出来, 所以我不会写出这个过程的代码。 如果你有兴趣的话, 它就在这个 Github repository里。 但是, 它比随机猜还是要好多了, 它可以从 10,000 个图片中正确猜出 2,225 个 ,也就是说有 22.25% 的准确率。

要想找到一些其他的办法使得准确率达到 20% - 50% 并不困难。 如果你努力尝试的话, 你可以得到超过 50% 正确率的结果。 但是要得到更好的结果, 使用现有的成熟的机器学习算法就会很有帮助。 让我们试着使用目前已知的最好的算法 支持向量机 (support vector machine, SVM). 如果你不熟悉支持向量机, 不要担心, 我们并不需要理解 SVM 的工作原理。 相反, 我们会使用现成的 python 库 scikit-learn , 这个库会提供一个简单的封装了 C 语言库 LIBSVM Python 接口 .

如果我们运行了 sckikit-learn 的默认参数设置的 SVM 分类器,那么它会从 10,000 个测试图片中正确识别 9,435 个图片。 代码可以从这里得到。这和我们非常初级的通过图片的黑度来识别图片的方法相比有很大的提升。 事实上, SVM 的表现和我们的神经网络表现几乎差不多, 仅仅差了一点。 在后面的章节,我会介绍一些新的技巧, 使得我们的神经网络表现得比 SVM 好的多。

然而, 这里并不是故事的结尾。 10,000 个图片中正确识别中 9,435 个图片的结果用的是 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(中国人( ̄▽ ̄)) 和 Rob Fergus 创造的。 我们会在本书的后面看到他们使用的大部分技巧。 在那种水平下, 识别效果就接近于人类了, 甚至可以说是更好, 因为 MNIST 中的一些图片对人来来说都很难识别, 例如:

这里写图片描述

我详细你也同意这些图片很难辨认! 所以,在 MNIST 数据集中存在这种数据的情况下, 能够在 10,000 个图片中错误分类 21 个是非常出色的成绩。 通常, 在编程时, 我们认为要解决一个复杂的问题, 像识别 MNIST 数字这种问题需要非常复杂的程序。 但是, 即便是刚才提到的 Li Wan 等人所写的程序也仅仅只包含很简单的算法, 这个算法的变种我们会在后续的章节中看到。 所有的复杂度都会从训练的数据中自动学习到。 在某种意义上来说, 我们的结果和那些更成熟的论文中的结果所展现出的寓意是:

对一些问题而言,
复杂的算法\leq 简单的学习算法 + 良好的训练数据

走向深度学习

尽管神经网络展现了非常出色的表现, 但是这个表现某种程度上是很神秘的。 神经网络中的权重和偏移量是被自动发现的。 这意味着我们并不能立刻对神经网络是如何工作进行解释。 我们能否找到一种方法解释我们的神经网络工作在识别手写数字时所依据的原则吗。 而且, 知道了那些原则以后, 我们能做得更好吗?

为了使得这个问题更严峻, 假设几十年后, 神经网络最终产生了人工智能(AI). 我们能否理解智能网络是如何工作的? 这些智能网络对我们来说可能是不透明的,拥有我们不能理解的权重和偏移量, 因为这些权重和偏移量的值都是自动学习出来的。 在人工智能的早期研究中, 人们希望构建人工智能所花费的努力也能帮助我们理解智能背后的工作原理, 也有可能是, 人类大脑工作的原理。 但是结果有可能是我们既不理解大脑是怎么运转的,也不理解人工智能是如何工作的!

为了解决这些问题, 让我们重新思考本章一开始给出人工神经元, 这些神经元是一种衡量因素的方法。 假设我们想决定一个图片是否为一张人脸:

这里写图片描述

我们可以用解决手写数字识别的相同方法来解决这个问题, 将图片的像素作为神经网络的输入, 把输出设为一个单独的神经元,要么输出“没错, 这是一个脸”, 要么输出“不, 这不是一个脸”

假设我们这样做了, 但是我们不使用学习算法,而是试着设计手动设计一个网络, 用于选择参数和权重。 我们应当如何进行, 现在暂时忘却所有关于神经网络的内容, 一个直觉性的方法是把问题分解为小的子问题: 这个图片是不是左上角有个眼镜, 它是不是中间有鼻子, 是不是下部有个嘴巴, 是不是上面有头发等等。

如果上面的问题答案都是 “是”, 或者 “可能是”, 那么我们就判断这张图片可能是一个人脸。 相反,如果大部分问题的答案都是否, 那么图片有可能就不是一个人脸。

当然, 这只是一种粗略的直觉, 它有很多站不住脚的地方。 可能这个人秃头, 所以没有头发。 可能仅仅我们只能看到脸的一部分, 或者只是脸的某一个角度, 或是其他的脸的特征被忽略了。 不过, 这个直觉告诉我们我们可以利用神经网络来解决这些子问题。 然后我们可以通过合并众多网络来构建一个实现人脸检测的神经网络。 如下是一个可能的架构, 正方形表示子神经网络。 注意, 这不是要作为一个真实解决人脸检测问题的方法, 而是用来帮我们构建网络工作机理的直觉的。 架构如下:

这里写图片描述

将子网络进一步分解也是可以的。 假设我们在解决问题“是否左上角有一个眼睛” , 那么这个问题可以被分解成 :“是不是有一个眉毛”, “是不是有睫毛”, “是不是有虹膜”等, 但是让我们保持其简单性, 这个网络的问题“是否左上角有眼睛”被分解成:

这里写图片描述

这些问题依旧可以进一步被分解。 最终, 我们会得到一个回答很简单一个问题的子网络,以至于它可以仅仅用一个像素来回答。 这些问题, 例如, 可能是关于是否在图片的某些位置有某种简单形状的出现。 这种问题可以被一个直接连接到原始图片像素的神经元所回答。

最终的结果是一个将复杂问题“这个图片是否为一个人脸”拆解开来的神经网络。 它是通过很多层完成, 前面的层仅仅回答一些简单而具体的问题, 后面的层构建起一种更加复杂和抽象的概念。 拥有这种多层次结构的神经网络 - 两个或更多隐藏层- 被称为深度神经网络。

当然, 我还没有说如何完成这种递归式地神经网络分解。 很显然这要手工设计神经网络的权重和偏移量是不现实的。 相反, 我们相拥学习算法来从数据中自动学习权重和偏移量, 进而, 学习到层级性的概念。 19世纪80-90 年代的研究者们试着使用随机梯度下降算法和后向传播来训练深度网络。 不幸的是, 除了较少的特殊架构神经网络, 别的都没能获得成功。 这些神经网络的确能进行学习,但是非常缓慢, 在实践当中, 因为太过于缓慢没法发挥作用。

从 2006 年以后, 一系列深度网络学习成为可能的技巧被开发出来。 这些深度学习的技巧是基于随机梯度下降算法和后向传播算法的, 但是也引进了一些新的想法。 这些技巧使得更深(也更大)的神经网络有可能被成功训练。 人们现在会例行公事一般地训练至少拥有 5-10 个隐藏层的神经网络。 而且, 这些网络在很多问题的表现上比浅层次的神经网络表现要好得多。 原因当然是深度网络有能力构建起很复杂的层级概念。 这就有点像传统的编程语言使用模块化的设计和抽象的思想, 以构建复杂的计算机程序。 把深度网络和浅层网络比较就像在比较一门能够调用底层语言编写函数的高级语言与底层语言一样。 传统编程语言的抽象在神经网络中以另外一种形式体现了出来, 但是它依旧无可替代的重要。

阅读全文
1 0
原创粉丝点击