1-1 神经网络和深度学习

深度学习是人工智能的一种具有代表性的实现方法,下面就让我们来考察一下它究竟是什么样的技术。

备受瞩目的深度学习

在有关深度学习的热门话题中,有几个被媒体大肆报道的事件,如下表所示。

年份 事件
2012 年 在世界性的图像识别大赛ILSVRC中,使用深度学习技术的Supervision方法取得了完胜
2012 年 利用谷歌公司开发的深度学习技术,人工智能从 YouTube 的视频中识别出了猫
2014 年 苹果公司将Siri 的语音识别系统变更为使用深度学习技术的系统
2016 年 利用谷歌公司开发的深度学习技术,AlphaGo 与世界顶级棋手对决,取得了胜利
2016 年 奥迪、宝马等公司将深度学习技术运用到汽车的自动驾驶中

如上表所示,深度学习在人工智能领域取得了很大的成功。那么,深度学习究竟是什么技术呢?深度学习里的“深度”是什么意思呢?为了解答这个疑问,首先我们来考察一下神经网络,这是因为深度学习是以神经网络为出发点的。

神经网络

谈到神经网络的想法,需要从生物学上的神经元( neuron)开始说起。
从生物学的扎实的研究成果中,我们可以得到以下关于构成大脑的神经元的知识(1-2 节)。
(i) 神经元形成网络。
(ii) 对于从其他多个神经元传递过来的信号,如果它们的和不超过某个固定大小的值(阈值),则神经元不做出任何反应。
(iii) 对于从其他多个神经元传递过来的信号,如果它们的和超过某个固定大小的值(阈值),则神经元做出反应(称为点火),向另外的神经元传递固定强度的信号。
(iv) 在 (ii) 和 (iii) 中,从多个神经元传递过来的信号之和中,每个信号对应的权重不一样。
神经网络的思想 - 图1
将神经元的工作在数学上抽象化,并以其为单位人工地形成网络,这样的人工网络就是神经网络。将构成大脑的神经元的集合体抽象为数学模型,这就是神经网络的出发点。

用神经网络实现的人工智能

看过以往的科幻电影、动画片就知道,人工智能是人们很早就有的想法。那么,早期研究的人工智能和用神经网络实现的人工智能有哪些不同呢?答案就是用神经网络实现的人工智能能够自己学习过去的数据。
以往的人工智能需要人们事先将各种各样的知识教给机器,这在工业机器人等方面取得了很大成功。
神经网络的思想 - 图2
而对于用神经网络实现的人工智能,人们只需要简单地提供数据即可。神经网络接收数据后,会从网络的关系中自己学习并理解。

“人教导机器”类型的人工智能的问题

20 世纪的“人教导机器”类型的人工智能,现在仍然活跃在各种领域,然而也有一些领域是它不能胜任的,其中之一就是模式识别。让我们来看一个简单的例子。
例题 有一个用 8×8 像素读取的手写数字的图像,考虑如何让计算机判断图像中的数字是否为 0。
读取的手写数字的图像如下图所示。
神经网络的思想 - 图3
这些图像虽然大小和形状各异,但都可以认为正解是数字 0。可是,如何将这些图像中的数字是 0 这个事实教给计算机呢?
要用计算机进行处理,就需要用数学式来表示。然而,像例题这样的情况,如果使用 20 世纪的常规手段,将“0 具有这样的形状”教给计算机,处理起来会十分困难。况且,如下所示,对于写得很难看的字、读取时受到噪声影响的字,虽然人能够设法辨认出来是 0,但要将这种辨认的条件用数学式表达,并教给计算机,应该是无法做到的。
神经网络的思想 - 图4
从这个简单的例题中可以看出,“人教导机器”类型的人工智能无法胜任图像、语音的模式识别,因为要把所有东西都教给计算机是不现实的。
不过,在 20 世纪后期,对于这样的问题,人们找到了简单的解决方法,那就是神经网络以及由其发展而来的深度学习。如前所述,具体来说就是由人提供数据,然后由神经网络自己进行学习。
如此看来,神经网络似乎有一些不可思议的逻辑。然而,从数学上来说,其原理十分容易。本书的目的就是阐明它的原理。

1-2 神经元工作的数学表示

就像我们在 1-1 节看到的那样,神经网络是以从神经元抽象出来的数学模型为出发点的。下面,我们将更详细地考察神经元的工作,并将其在数学上抽象化。

整理神经元的工作

人的大脑是由多个神经元互相连接形成网络而构成的。也就是说,一个神经元从其他神经元接收信号,也向其他神经元发出信号。大脑就是根据这个网络上的信号的流动来处理各种各样的信息的。
神经网络的思想 - 图5
让我们来更详细地看一下神经元传递信息的结构。如上图所示,神经元是由细胞体、树突、轴突三个主要部分构成的。其他神经元的信号(输入信号)通过树突传递到细胞体(也就是神经元本体)中,细胞体把从其他多个神经元传递进来的输入信号进行合并加工,然后再通过轴突前端的突触传递给别的神经元。
那么,神经元究竟是怎样对输入信号进行合并加工的呢?让我们来看看它的构造。
假设一个神经元从其他多个神经元接收了输入信号,这时如果所接收的信号之和比较小,没有超过这个神经元固有的边界值(称为阈值),这个神经元的细胞体就会忽略接收到的信号,不做任何反应。
神经网络的思想 - 图6
注:对于生命来说,神经元忽略微小的输入信号,这是十分重要的。反之,如果神经元对于任何微小的信号都变得兴奋,神经系统就将“情绪不稳定”。
不过,如果输入信号之和超过神经元固有的边界值(也就是阈值),细胞体就会做出反应,向与轴突连接的其他神经元传递信号,这称为点火
神经网络的思想 - 图7
那么,点火时神经元的输出信号是什么样的呢?有趣的是,信号的大小是固定的。即便从邻近的神经元接收到很大的刺激,或者轴突连接着其他多个神经元,这个神经元也只输出固定大小的信号。点火的输出信号是由 0 或 1 表示的数字信息。

神经元工作的数学表示

让我们整理一下已经考察过的神经元点火的结构。
(i) 来自其他多个神经元的信号之和成为神经元的输入。
(ii) 如果这个信号之和超过神经元固有的阈值,则点火。
(iii) 神经元的输出信号可以用数字信号 0 和 1 来表示。即使有多个输出端,其值也是同一个。
下面让我们用数学方式表示神经元点火的结构。
首先,我们用数学式表示输入信号。由于输入信号是来自相邻神经元的输出信号,所以根据 (iii),输入信号也可以用“有”“无”两种信息表示。因此,用变量 神经网络的思想 - 图8 表示输入信号时,如下所示。
神经网络的思想 - 图9
注:与视细胞直接连接的神经元等个别神经元并不一定如此,因为视细胞的输入是模拟信号。
接下来,我们用数学式表示输出信号。根据 (iii),输出信号可以用表示点火与否的“有”“无”两种信息来表示。因此,用变量 神经网络的思想 - 图10 表示输出信号时,如下所示。
神经网络的思想 - 图11
神经网络的思想 - 图12
最后,我们用数学方式来表示点火的判定条件。
从 (i) 和 (ii) 可知,神经元点火与否是根据来自其他神经元的输入信号的和来判定的,但这个求和的方式应该不是简单的求和。例如在网球比赛中,对于来自视觉神经的信号和来自听觉神经的信号,大脑是通过改变权重来处理的。因此,神经元的输入信号应该是考虑了权重的信号之和。用数学语言来表示的话,例如,来自相邻神经元 1、2、3 的输入信号分别为 神经网络的思想 - 图13神经网络的思想 - 图14神经网络的思想 - 图15,则神经元的输入信号之和可以如下表示。
神经网络的思想 - 图16
式中的 神经网络的思想 - 图17神经网络的思想 - 图18神经网络的思想 - 图19 是输入信号 神经网络的思想 - 图20神经网络的思想 - 图21神经网络的思想 - 图22 对应的权重(weight)。
神经网络的思想 - 图23
根据 (ii),神经元在信号之和超过阈值时点火,不超过阈值时不点火。于是,利用式 (1),点火条件可以如下表示。
神经网络的思想 - 图24
这里,神经网络的思想 - 图25 是该神经元固有的阈值。
例 1 来自两个神经元 1、2 的输入信号分别为变量 神经网络的思想 - 图26神经网络的思想 - 图27,权重为 神经网络的思想 - 图28神经网络的思想 - 图29,神经元的阈值为 神经网络的思想 - 图30。当 神经网络的思想 - 图31神经网络的思想 - 图32神经网络的思想 - 图33 时,考察信号之和 神经网络的思想 - 图34 的值与表示点火与否的输出信号 神经网络的思想 - 图35 的值。

输入 神经网络的思想 - 图36 输入 神经网络的思想 - 图37 神经网络的思想 - 图38 点火 输出信号 神经网络的思想 - 图39
0 0 神经网络的思想 - 图40 0
0 1 神经网络的思想 - 图41 0
1 0 神经网络的思想 - 图42 1
1 1 神经网络的思想 - 图43 1

点火条件的图形表示

下面我们将表示点火条件的式 (2) 图形化。以神经元的输入信号之和为横轴,神经元的输出信号 神经网络的思想 - 图44 为纵轴,将式 (2) 用图形表示出来。如下图所示,当信号之和小于 神经网络的思想 - 图45 时,神经网络的思想 - 图46 取值 0,反之 神经网络的思想 - 图47 取值 1。
神经网络的思想 - 图48
如果用函数式来表示这个图形,就需要用到下面的单位阶跃函数
神经网络的思想 - 图49
单位阶跃函数的图形如下所示。
神经网络的思想 - 图50
利用单位阶跃函数 神经网络的思想 - 图51,式 (2) 可以用一个式子表示如下。
点火的式子:神经网络的思想 - 图52
通过下表可以确认式 (3) 和式 (2) 是一样的。

神经网络的思想 - 图53 神经网络的思想 - 图54 神经网络的思想 - 图55 神经网络的思想 - 图56
0(无点火) 小于 神经网络的思想 - 图57 神经网络的思想 - 图58 0
1(点火) 大于等于 神经网络的思想 - 图59 神经网络的思想 - 图60 1

此外,该表中的 神经网络的思想 - 图61(式 (3) 的阶跃函数的参数)的表达式
神经网络的思想 - 图62
称为该神经元的加权输入
备注 神经网络的思想 - 图63 的处理
有的文献会像下面这样处理式 (2) 的不等号。
神经网络的思想 - 图64
在生物上这也许是很大的差异,不过对于接下来的讨论而言是没有问题的。因为我们的主角是 Sigmoid 函数,所以不会发生这样的问题。

1-3 激活函数:将神经元的工作一般化

1-2 节中用数学式表示了神经元的工作。本节我们试着将其在数学上一般化。

简化神经元的图形

为了更接近神经元的形象,1-2 节中将神经元表示为了下图的样子。
神经网络的思想 - 图65
然而,为了画出网络,需要画很多的神经元,在这种情况下上面那样的图就不合适了。因此,我们使用如下所示的简化图,这样很容易就能画出大量的神经元。
神经网络的思想 - 图66
为了与生物学的神经元区分开来,我们把经过这样简化、抽象化的神经元称为神经单元(unit)。
注:很多文献直接称为“神经元”。本书为了与生物学术语“神经元”区分,使用“神经单元”这个称呼。另外,也有文献将“神经单元”称为“人工神经元”,但是由于现在也存在生物上的人工神经元,所以本书中也不使用“人工神经元”这个称呼。

激活函数

将神经元的示意图抽象化之后,对于输出信号,我们也对其生物上的限制进行一般化。
根据点火与否,生物学上的神经元的输出 神经网络的思想 - 图67 分别取值 1 和 0(下图)。
神经网络的思想 - 图68
然而,如果除去“生物”这个条件,这个“0 和 1 的限制”也应该是可以解除的。这时表示点火与否的下式(1 - 2 节式 (3))就需要修正。
点火的式子:神经网络的思想 - 图69
这里,神经网络的思想 - 图70 是单位阶跃函数。我们将该式一般化,如下所示。
神经网络的思想 - 图71
这里的函数 神经网络的思想 - 图72 是建模者定义的函数,称为激活函数(activation function)。神经网络的思想 - 图73神经网络的思想 - 图74神经网络的思想 - 图75 是模型允许的任意数值,神经网络的思想 - 图76 是函数 神经网络的思想 - 图77 能取到的任意数值。这个式 (2) 就是今后所讲的神经网络的出发点。
注:虽然式 (2) 只考虑了 3 个输入,但这是很容易推广的。另外,式 (1) 使用的单位阶跃函数 神经网络的思想 - 图78 在数学上也是激活函数的一种。
请注意,式 (2) 的输出 神经网络的思想 - 图79 的取值并不限于 0 和 1,对此并没有简单的解释。一定要用生物学来比喻的话,可以考虑神经单元的“兴奋度”“反应度”“活性度”。
我们来总结一下神经元和神经单元的不同点,如下表所示。


神经元 神经单元
输出值 神经网络的思想 - 图80 0或1 模型允许的任意数值
激活函数 单位阶跃函数 由分析者给出,其中著名的是 Sigmoid 函数(后述)
输出的解释 点火与否 神经单元的兴奋度、反应度、活性度

神经网络的思想 - 图81
将神经元点火的式 (1) 一般化为神经单元的激活函数式 (2),要确认这样做是否有效,就要看实际做出的模型能否很好地解释现实的数据。实际上,式 (2) 表示的模型在很多模式识别问题中取得了很好的效果。

Sigmoid 函数

激活函数的代表性例子是 Sigmoid 函数 神经网络的思想 - 图82,其定义如下所示。
神经网络的思想 - 图83
关于这个函数,我们会在后面详细讨论(2-1 节)。这里先来看看它的图形,Sigmoid 函数 神经网络的思想 - 图84 的输出值是大于 0 小于 1 的任意值。此外,该函数连续、光滑,也就是说可导。这两种性质使得 Sigmoid 函数很容易处理。
神经网络的思想 - 图85
单位阶跃函数的输出值为 1 或 0,表示点火与否。然而,Sigmoid 函数的输出值大于 0 小于 1,这就有点难以解释了。如果用生物学术语来解释的话,如上文中的表格所示,可以认为输出值表示神经单元的兴奋度等。输出值接近 1 表示兴奋度高,接近 0 则表示兴奋度低。
神经网络的思想 - 图86
本书中将 Sigmoid 函数作为标准激活函数使用,因为它具有容易计算的漂亮性质。如果用数学上单调递增的可导函数来代替,其原理也是一样的。

练习:实现sigmoid函数

  1. import numpy as np
  2. import matplotlib.pylab as plt
  3. def sigmoid_function(x):
  4. return 1/(1+np.exp(-x))
  5. x = np.linspace(-5, 5)
  6. y = sigmoid_function(x)
  7. plt.plot(x, y)
  8. plt.show()

偏置

再来看一下激活函数的式 (2)。
神经网络的思想 - 图87
这里的 神经网络的思想 - 图88 称为阈值,在生物学上是表现神经元特性的值。从直观上讲,神经网络的思想 - 图89 表示神经元的感受能力,如果 神经网络的思想 - 图90 值较大,则神经元不容易兴奋(感觉迟钝),而如果值较小,则神经元容易兴奋(敏感)。
然而,式 (2) 中只有 神经网络的思想 - 图91 带有负号,这看起来不漂亮。数学不喜欢不漂亮的东西。另外,负号具有容易导致计算错误的缺点,因此,我们将 神经网络的思想 - 图92 替换为 神经网络的思想 - 图93
神经网络的思想 - 图94
经过这样处理,式子变漂亮了,也不容易发生计算错误。这个 神经网络的思想 - 图95 称为偏置(bias)。
神经网络的思想 - 图96
本书将式 (4) 作为标准使用。另外,此时的加权输入 神经网络的思想 - 图97(1-2 节)如下所示。
神经网络的思想 - 图98
式 (4) 和式 (5) 是今后所讲的神经网络的出发点,非常重要。
另外,生物上的权重 神经网络的思想 - 图99神经网络的思想 - 图100神经网络的思想 - 图101 和阈值 神经网络的思想 - 图102神经网络的思想 - 图103)都不是负数,因为负数在自然现象中实际上是不会出现的。然而,在将神经元一般化的神经单元中,是允许出现负数的。
神经网络的思想 - 图104
问题 右图是一个神经单元。如图所示,输入 神经网络的思想 - 图105 的对应权重是 2,输入 神经网络的思想 - 图106的对应权重是 3,偏置是 -1。根据下表给出的输入,求出加权输入 神经网络的思想 - 图107 和输出 神经网络的思想 - 图108。注意这里的激活函数是 Sigmoid 函数。
神经网络的思想 - 图109

输入 神经网络的思想 - 图110 输入 神经网络的思想 - 图111 加权输入 神经网络的思想 - 图112 输出 神经网络的思想 - 图113
0.2 0.1

0.6 0.5

结果如下表所示(式 (3) 中的 e 取 e = 2.7 进行计算)

输入 神经网络的思想 - 图114 输入 神经网络的思想 - 图115 加权输入 神经网络的思想 - 图116 输出 神经网络的思想 - 图117
0.2 0.1 2×0.2 + 3×0.1 - 1 = -0.3 0.43
0.6 0.5 2×0.6 + 3×0.5 - 1 = 1.7 0.84

备注 改写式 (5)
我们将式 (5) 像下面这样整理一下。
神经网络的思想 - 图118
这里增加了一个虚拟的输入,可以理解为以常数 1 作为输入值(下图)。
神经网络的思想 - 图119
于是,加权输入 神经网络的思想 - 图120 可以看作下面两个向量的内积。
神经网络的思想 - 图121
计算机擅长内积的计算,因此按照这种解释,计算就变容易了。

1-4 什么是神经网络

神经网络作为本书的主题,它究竟是什么样的呢?下面让我们来看一下其概要。

神经网络

上一节我们考察了神经单元,它是神经元的模型化。那么,既然大脑是由神经元构成的网络,如果我们模仿着创建神经单元的网络,是不是也能产生某种“智能”呢?这自然是让人期待的。众所周知,人们的期待没有被辜负,由神经单元组成的网络在人工智能领域硕果累累。
在进入神经网络的话题之前,我们先来回顾一下上一节考察过的神经单元的功能。

  • 将神经单元的多个输入 神经网络的思想 - 图122 整理为加权输入 神经网络的思想 - 图123神经网络的思想 - 图124其中 神经网络的思想 - 图125 为权重,神经网络的思想 - 图126 为偏置,神经网络的思想 - 图127 为输入的个数。
  • 神经单元通过激活函数 神经网络的思想 - 图128,根据加权输入 神经网络的思想 - 图129 输出 神经网络的思想 - 图130神经网络的思想 - 图131

神经网络的思想 - 图132
将这样的神经单元连接为网络状,就形成了神经网络
网络的连接方法多种多样,本书将主要考察作为基础的阶层型神经网络以及由其发展而来的卷积神经网络
注:为了与生物学上表示神经系统的神经网络区分开来,有的文献使用“人工神经网络”这个称呼。本书中为了简便,省略了“人工”二字。

神经网络各层的职责

阶层型神经网络如下图所示,按照(layer)划分神经单元,通过这些神经单元处理信号,并从输出层得到结果,如下图所示。
神经网络的思想 - 图133
构成这个网络的各层称为输入层隐藏层输出层,其中隐藏层也被称为中间层
各层分别执行特定的信号处理操作。
输入层负责读取给予神经网络的信息。属于这个层的神经单元没有输入箭头,它们是简单的神经单元,只是将从数据得到的值原样输出。
隐藏层的神经单元执行前面所复习过的处理操作 (1) 和 (2)。在神经网络中,这是实际处理信息的部分。
输出层与隐藏层一样执行信息处理操作 (1) 和 (2),并显示神经网络计算出的结果,也就是整个神经网络的输出。

深度学习

深度学习,顾名思义,是叠加了很多层的神经网络。叠加层有各种各样的方法,其中著名的是卷积神经网络(第 5 章)。

考察具体的例子

从现在开始一直到第 4 章,我们都将围绕着下面这个简单的例子来考察神经网络的结构。
例题 建立一个神经网络,用来识别通过 4×3 像素的图像读取的手写数字 0 和 1。学习数据是 64 张图像,其中像素是单色二值。
我们来示范一下这个例题如何解答。
神经网络的思想 - 图134
这个解答是演示实际的神经网络如何发挥功能的最简单的神经网络示例,但对于理解本质已经足够了。该思路也同样适用于复杂的情况。
注:例题的解答有很多种,并不仅限于这一示例。
这个简单的神经网络的特征是,前一层的神经单元与下一层的所有神经单元都有箭头连接,这样的层构造称为全连接层(fully connected layer)。这种形状对于计算机的计算而言是十分容易的。
下面让我们来简单地看一下各层的含义。

解答示例中输入层的含义

输入层由 12 个神经单元构成,对此我们立刻就能够理解,因为神经网络一共需要读取 4×3 = 12 个像素信息。
神经网络的思想 - 图135
输入层的神经单元的输入与输出是相同的。一定要引入激活函数 神经网络的思想 - 图136 的话,可以用恒等函数(神经网络的思想 - 图137)来充当。

解答示例中输出层的含义

输出层由两个神经单元构成,这是因为我们的题目是识别两种手写数字 0 和 1,需要一个在读取手写数字 0 时输出较大值(即反应较大)的神经单元,以及一个在读取手写数字 1 时输出较大值的神经单元。
例如,将 Sigmoid 函数作为激活函数使用。在这种情况下,读取数字 0 的图像时,输出层上方的神经单元的输出值比下方的神经单元的输出值大;而读取数字 1 的图像时,输出层下方的神经单元的输出值比上方的神经单元的输出值大,如下图所示。像这样,根据输出层的神经单元的输出的大小,对整个神经网络进行判断。
神经网络的思想 - 图138

神经网络的思想 - 图139

MNIST数据集


这里使用的数据集是MNIST手写数字图像集。 MNIST是机器学习领域最有名的数据集之一,被应用于从简单的实验到发表的论文研究等各种场合。实际上,在阅读图像识别或机器学习的论文时, MNIST数据集经常作为实
验用的数据出现。
MNIST数据集是由0到9的数字图像构成的,如图所示。训练图像有6万张,测试图像有1万张,这些图像可以用于学习和推理。 MNIST数据集的一般使用方法是,先用训练图像进行学习,再用学习到的模型度量能在多大程度上对测试图像进行正确的分类。
image.png
MNIST的图像数据是28像素 × 28像素的灰度图像(1通道),各个像素的取值在0到255之间。每个图像数据都相应地标有“7”“2”“1”等标签。提供了便利的Python脚本mnist.py,该脚本已经将MNIST数据集转换成了NumPy数组(mnist.py在dataset目录下)。使用mnist.py中的load_mnist()函数,就可以按下述方式轻松读入MNIST数据。

  1. from dataset.mnist import load_mnist
  2. (x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
  3. # 输出各个数据的形状
  4. print(x_train.shape) # (60000, 784)

load_mnist函数以“(训练图像 ,训练标签 ),(测试图像,测试标签 )”的形式返回读入的MNIST数据。此外,还可以像 load_mnist(normalize=True,flatten=True, one_hot_label=False) 这 样,设 置 3 个 参 数。第 1 个 参 数normalize设置是否将输入图像正规化为0.0~1.0的值。如果将该参数设置为 False,则输入图像的像素会保持原来的0~255。第2个参数 flatten设置是否展开输入图像(变成一维数组)。如果将该参数设置为 False,则输入图
像为1 × 28 × 28的三维数组;若设置为True,则输入图像会保存为由784个元素构成的一维数组。第3个参数one_hot_label设置是否将标签保存为onehot表示(one-hot representation)。 one-hot表示是仅正确解标签为1,其余皆为0的数组,就像[0,0,1,0,0,0,0,0,0,0]这样。当one_hot_label为False时,只是像 7、 2这样简单保存正确解标签;当 one_hot_label为 True时,标签则保存为one-hot表示。
现在,我们试着显示MNIST图像,同时也确认一下数据。图像的显示使用PIL(Python Image Library)模块。执行下述代码后,训练图像的第一张就会显示出来,如图所示(源代码在mnist_show.py中)

  1. import numpy as np
  2. from dataset.mnist import load_mnist
  3. from PIL import Image
  4. def img_show(img):
  5. pil_img = Image.fromarray(np.uint8(img))
  6. pil_img.show()
  7. (x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
  8. index = 1000 # 查看第几张图片
  9. img = x_train[index]
  10. label = t_train[index]
  11. print(label)
  12. print(img.shape) # (784,)
  13. img = img.reshape(28, 28) # 把图像的形状变为原来的尺寸
  14. print(img.shape) # (28, 28)
  15. img_show(img)

这里需要注意的是, flatten=True时读入的图像是以一列(一维) NumPy数组的形式保存的。因此,显示图像时,需要把它变为原来的28像素 × 28像素的形状。可以通过 reshape()方法的参数指定期望的形状,更改NumPy
数组的形状。此外,还需要把保存为NumPy数组的图像数据转换为PIL用的数据对象,这个转换处理由Image.fromarray()来完成。
image.png

下面,我们对这个MNIST数据集实现神经网络的推理处理。神经网络的输入层有784个神经元,输出层有10个神经元。输入层的784这个数字来源于图像大小的28 × 28 = 784,输出层的10这个数字来源于10类别分类(数
字0到9,共10类别)。此外,这个神经网络有2个隐藏层,第1个隐藏层有50个神经元,第2个隐藏层有100个神经元。这个50和100可以设置为任何值。下面我们先定义 get_data()、 init_network()、 predict()这3个函数(代码在
neuralnet_mnist.py中)。

  1. def get_data():
  2. (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
  3. return x_test, t_test
  4. def init_network():
  5. with open("sample_weight.pkl", 'rb') as f:
  6. network = pickle.load(f)
  7. return network
  8. def predict(network, x):
  9. W1, W2, W3 = network['W1'], network['W2'], network['W3']
  10. b1, b2, b3 = network['b1'], network['b2'], network['b3']
  11. a1 = np.dot(x, W1) + b1
  12. z1 = sigmoid(a1)
  13. a2 = np.dot(z1, W2) + b2
  14. z2 = sigmoid(a2)
  15. a3 = np.dot(z2, W3) + b3
  16. y = softmax(a3)
  17. return y

init_network()会读入保存在pickle文件 sample_weight.pkl中的学习到的权重参数 A。这个文件中以字典变量的形式保存了权重和偏置参数。剩余的2个函数,和前面介绍的代码实现基本相同,无需再解释。现在,我们用这3
个函数来实现神经网络的推理处理。然后,评价它的识别精度(accuracy),即能在多大程度上正确分类。

  1. x, t = get_data()
  2. network = init_network()
  3. accuracy_cnt = 0
  4. for i in range(len(x)):
  5. y = predict(network, x[i])
  6. p= np.argmax(y) # 获取概率最高的元素的索引
  7. if p == t[i]:
  8. accuracy_cnt += 1
  9. print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

首先获得MNIST数据集,生成网络。接着,用 for语句逐一取出保存在 x中的图像数据,用predict()函数进行分类。 predict()函数以NumPy数组的形式输出各个标签对应的概率。比如输出[0.1, 0.3, 0.2, …, 0.04]的
数组,该数组表示“0”的概率为0.1,“1”的概率为0.3,等等。然后,我们取出这个概率列表中的最大值的索引(第几个元素的概率最高),作为预测结果。可以用 np.argmax(x)函数取出数组中的最大值的索引, np.argmax(x)将获取被赋给参数x的数组中的最大值元素的索引。最后,比较神经网络所预测的答案和正确解标签,将回答正确的概率作为识别精度。
执行上面的代码后,会显示“Accuracy:0.9352”。这表示有93.52 %的数据被正确分类了。

在这个例子中,我们把load_mnist函数的参数normalize设置成了True。将 normalize设置成 True后,函数内部会进行转换,将图像的各个像素值除以255,使得数据的值在0.0~1.0的范围内。像这样把数据限定到某个范围内的处理称为正规化(normalization)。此外,对神经网络的输入数据进行某种既定的转换称为预处理(pre-processing)。这里,作为对输入图像的一种预处理,我们进行了正规化。

以上就是处理MNIST数据集的神经网络的实现,现在我们来关注输入数据和权重参数的“形状”。再看一下刚才的代码实现。下面我们使用Python解释器,输出刚才的神经网络的各层的权重的形状。

  1. >>> x, _ = get_data()
  2. >>> network = init_network()
  3. >>> W1, W2, W3 = network['W1'], network['W2'], network['W3']
  4. >>>
  5. >>> x.shape
  6. (10000, 784)
  7. >>> x[0].shape
  8. (784,)
  9. >>> W1.shape
  10. (784, 50)
  11. >>> W2.shape
  12. (50, 100)
  13. >>> W3.shape
  14. (100, 10)

我们通过上述结果来确认一下多维数组的对应维度的元素个数是否一致(省略了偏置)。用图表示的话,如图所示。可以发现,多维数组的对应维度的元素个数确实是一致的。此外,我们还可以确认最终的结果是输出了
元素个数为10 的一维数组。
image.png
从整体的处理流程来看,上图中,输入一个由784个元素(原本是一个28 × 28的二维数组)构成的一维数组后,输出一个有10个元素的一维数组。这是只输入一张图像数据时的处理流程。现在我们来考虑打包输入多张图像的情形。比如,我们想用 predict()函数一次性打包处理100张图像。为此,可以把x的形状改为100 × 784,将
100张图像打包作为输入数据。用图表示的话,如下图所示。
image.png
如图所示,输入数据的形状为 100 × 784,输出数据的形状为100 × 10。这表示输入的100张图像的结果被一次性输出了。比如, x[0]和 y[0]中保存了第0张图像及其推理结果, x[1]和 y[1]中保存了第1张图像及其推理结果,等等。
这种打包式的输入数据称为批(batch)。批有“捆”的意思,图像就如同纸币一样扎成一捆。

批处理对计算机的运算大有利处,可以大幅缩短每张图像的处理时间。那么为什么批处理可以缩短处理时间呢?这是因为大多数处理数值计算的库都进行了能够高效处理大型数组运算的最优化。并且,在神经网络的运算中,当数据传送成为瓶颈时,批处理可以减轻数据总线的负荷(严格地讲,相对于数据读入,可以将更多的时间用在计算上)。也就是说,批处理一次性计算大型数组要比分开逐步计算各个小型数组速度更快。

下面我们进行基于批处理的代码实现。这里用粗体显示与之前的实现的不同之处。

  1. x, t = get_data()
  2. network = init_network()
  3. batch_size = 100 # 批数量
  4. accuracy_cnt = 0
  5. for i in range(0, len(x), batch_size):
  6. x_batch = x[i:i+batch_size]
  7. y_batch = predict(network, x_batch)
  8. p = np.argmax(y_batch, axis=1)
  9. accuracy_cnt += np.sum(p == t[i:i+batch_size])
  10. print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

在 range()函数生成的列表的基础上,通过 x[i:i+batch_size]从输入数据中抽出批数据。 x[i:i+batch_n]会取出从第i个到第i+batch_n个之间的数据。本例中是像x[0:100]、 x[100:200]……这样,从头开始以100为单位将数据提取为批数据。
然后,通过argmax()获取值最大的元素的索引。不过这里需要注意的是,我们给定了参数axis=1。这指定了在100 × 10的数组中,沿着第1维方向(以第1维为轴)找到值最大的元素的索引(第0维对应第1个维度) A。这里也来看一个例子。

  1. >>> x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6],
  2. ... [0.2, 0.5, 0.3], [0.8, 0.1, 0.1]])
  3. >>> y = np.argmax(x, axis=1)
  4. >>> print(y)
  5. [1 2 1 0]

最后,我们比较一下以批为单位进行分类的结果和实际的答案。为此,需要在NumPy数组之间使用比较运算符(==)生成由 True/False构成的布尔型数组,并计算True的个数。我们通过下面的例子进行确认。

  1. >>> y = np.array([1, 2, 1, 0])
  2. >>> t = np.array([1, 2, 0, 0])
  3. >>> print(y==t)
  4. [True True False True]
  5. >>> np.sum(y==t)
  6. 3

至此,基于批处理的代码实现就介绍完了。使用批处理,可以实现高速且高效的运算。

损失函数

如果有人问你现在有多幸福,你会如何回答呢?一般的人可能会给出诸如“还可以吧”或者“不是那么幸福”等笼统的回答。如果有人回答“我现在的幸福指数是10.23”的话,可能会把人吓一跳吧。因为他用一个数值指标来
评判自己的幸福程度。这里的幸福指数只是打个比方,实际上神经网络的学习也在做同样的事情。神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。和刚刚那位以幸福指数为指引寻找“最优人生”的人一样,神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。
可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squarederror)。均方误差如下式所示。
image.png
这里, yk是表示神经网络的输出, tk表示监督数据, k表示数据的维数。
比如,在手写数字识别的例子中, yk、 tk是由如下10个元素构成的数据。

  1. >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
  2. >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]


数组元素的索引从第一个开始依次对应数字“0”“1”“2”…… 这里,神经网络的输出y是softmax函数的输出。由于softmax函数的输出可以理解为概率,因此上例表示“0”的概率是0.1,“1”的概率是0.05,“2”的概率是0.6等。 t是监督数据,将正确解标签设为1,其他均设为0。这里,标签“2”为1,表示正确解是“2”。将正确解标签表示为1,其他标签表示为0的表示方法称为one-hot表示。
均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。现在,我们用Python来实现这个均方误差,实现方式如下所示。

  1. def mean_squared_error(y, t):
  2. return 0.5 * np.sum((y-t)**2)

这里,参数 y和 t是NumPy数组。代码实现完全遵照式,因此不再具体说明。现在,我们使用这个函数,来实际地计算一下。

  1. >>> # 设“2”为正确解
  2. >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
  3. >>>
  4. >>> # 例1: “2”的概率最高的情况(0.6)
  5. >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
  6. >>> mean_squared_error(np.array(y), np.array(t))
  7. 0.097500000000000031
  8. >>>
  9. >>> # 例2: “7”的概率最高的情况(0.6)
  10. >>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
  11. >>> mean_squared_error(np.array(y), np.array(t))
  12. 0.59750000000000003

这里举了两个例子。第一个例子中,正确解是“2”,神经网络的输出的最大值是“2”;第二个例子中,正确解是“2”,神经网络的输出的最大值是“7”。如实验结果所示,我们发现第一个例子的损失函数的值更小,和监督数据之间的误差较小。也就是说,均方误差显示第一个例子的输出结果与监督数据更加吻合。

除了均方误差之外, 交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示
image.png
这里, log表示以e为底数的自然对数(log e)。 yk是神经网络的输出, tk是正确解标签。并且, tk中只有正确解标签的索引为1,其他均为0(one-hot表示)。因此,式(4.2)实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是-log 0.6 = 0.51;若“2”对应的输出是0.1,则交叉熵误差为-log 0.1 = 2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
自然对数的图像如图所示 。
image.png
x等于1时, y为0;随着x向0靠近, y逐渐变小。因此,正确解标签对应的输出越大,式的值越接近0;当输出为1时,交叉熵误差为0。此外,如果正确解标签对应的输出较小,则式的值较大。
下面,我们来用代码实现交叉熵误差。

  1. def cross_entropy_error(y, t):
  2. delta = 1e-7
  3. return -np.sum(t * np.log(y + delta))

这里,参数y和t是NumPy数组。函数内部在计算np.log时,加上了一个微小值delta。这是因为,当出现np.log(0)时, np.log(0)会变为负无限大的-inf,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。下面,我们使用cross_entropy_error(y, t)进行一些简单的计算。

  1. >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
  2. >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
  3. >>> cross_entropy_error(np.array(y), np.array(t))
  4. 0.51082545709933802
  5. >>>
  6. >>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
  7. >>> cross_entropy_error(np.array(y), np.array(t))
  8. 2.3025840929945458

第一个例子中,正确解标签对应的输出为0.6,此时的交叉熵误差大约为0.51。第二个例子中,正确解标签对应的输出为0.1的低值,此时的交叉熵误差大约为2.3。由此可以看出,这些结果与我们前面讨论的内容是一致的。

学习

机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据
有100个的话,我们就要把这100个损失函数的总和作为学习的指标。前面介绍的损失函数的例子中考虑的都是针对单个数据的损失函数。如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式。
image.png
这里,假设数据有N个, tnk表示第n个数据的第k个元素的值(ynk是神经网络的输出, tnk是监督数据)。式子虽然看起来有一些复杂,其实只是把求单个数据的损失函数的式扩大到了N份数据,不过最后还要除以N进行正规化。通过除以N,可以求单个数据的“平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。比如,即便训练数据有1000个或10000个,也可以求得单个数据的平均损失函数。
另外, MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函
数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。
下面我们来编写从训练数据中随机选择指定个数的数据的代码,以进行mini-batch学习。在这之前,先来看一下用于读入MNIST数据集的代码。

  1. import numpy as np
  2. from dataset.mnist import load_mnist
  3. (x_train, t_train), (x_test, t_test) = \
  4. load_mnist(normalize=True, one_hot_label=True)
  5. print(x_train.shape) # (60000, 784)
  6. print(t_train.shape) # (60000, 10)

load_mnist函数是用于读入MNIST数据集的函数。这个函数在本书提供的脚本dataset/mnist.py中,它会读入训练数据和测试数据。 读入数据时,通过设定参数 one_hot_label=True,可以得到one-hot表示(即
仅正确解标签为1,其余为0的数据结构)。
读入上面的MNIST数据后,训练数据有60000个,输入数据是784维(28 × 28)的图像数据,监督数据是10维的数据。因此,上面的 x_train、 t_train的形状分别是(60000, 784)和(60000, 10)。
那么,如何从这个训练数据中随机抽取10笔数据呢?我们可以使用NumPy的np.random.choice(),写成如下形式。

  1. train_size = x_train.shape[0]
  2. batch_size = 10
  3. batch_mask = np.random.choice(train_size, batch_size)
  4. x_batch = x_train[batch_mask]
  5. t_batch = t_train[batch_mask]

使用np.random.choice()可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)会从0到59999之间随机选择10个数字。如下面的实际代码所示,我们可以得到一个包含被选数据的索引的数组。

  1. >>> np.random.choice(60000, 10)
  2. array([ 8013, 14666, 58210, 23832, 52091, 10153, 8107, 19410, 27260,
  3. 21411])

之后,我们只需指定这些随机选出的索引,取出mini-batch,然后使用这个mini-batch计算损失函数即可。
如何实现对应mini-batch的交叉熵误差呢?只要改良一下之前实现的对应单个数据的交叉熵误差就可以了。这里,我们来实现一个可以同时处理单个数据和批量数据(数据作为batch集中输入)两种情况的函数。

  1. def cross_entropy_error(y, t):
  2. if y.ndim == 1:
  3. t = t.reshape(1, t.size)
  4. y = y.reshape(1, y.size)
  5. batch_size = y.shape[0]
  6. return -np.sum(t * np.log(y + 1e-7)) / batch_size

这里, y是神经网络的输出, t是监督数据。 y的维度为1时,即求单个数据的交叉熵误差时,需要改变数据的形状。并且,当输入为mini-batch时,要用batch的个数进行正规化,计算单个数据的平均交叉熵误差。
此外,当监督数据是标签形式(非one-hot表示,而是像“2”“7”这样的标签)时,交叉熵误差可通过如下代码实现。

  1. def cross_entropy_error(y, t):
  2. if y.ndim == 1:
  3. t = t.reshape(1, t.size)
  4. y = y.reshape(1, y.size)
  5. batch_size = y.shape[0]
  6. return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

实现的要点是,由于one-hot表示中t为0的元素的交叉熵误差也为0,因此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。因此, t为 one-hot表示时通过
t * np.log(y)计算的地方,在 t为标签形式时,可用 np.log( y[np.arange(batch_size), t] )实现相同的处理(为了便于观察,这里省略了微小值1e-7)。作为参考,简单介绍一下np.log( y[np.arange(batch_size), t] )。 np.arange
(batch_size)会生成一个从0到 batch_size-1的数组。比如当 batch_size为5时, np.arange(batch_size)会生成一个NumPy 数组 [0, 1, 2, 3, 4]。因为t中标签是以 [2, 7, 0, 9, 4]的形式存储的,所以 y[np.arange(batch_size),
t]能抽出各个数据的正确解标签对应的神经网络的输出(在这个例子中,y[np.arange(batch_size), t] 会 生 成 NumPy 数 组 [y[0,2], y[1,7], y[2,0],y[3,9], y[4,4]])。

神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面4个步骤。
步骤1( mini-batch)
从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们
的目标是减小mini-batch的损失函数的值。
步骤2(计算梯度)
为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
步骤3(更新参数)
将权重参数沿梯度方向进行微小更新。
步骤4(重复)
重复步骤1、步骤2、步骤3。

神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini batch数据,所以又称为随机梯度下降法(stochastic gradient descent)。“随机”指的是“随机选择的”的意思,因此,随机梯度下降法是“对随机选择的数据进行的梯度下降法”。深度学习的很多框架中,随机梯度下降法一般由一个名为SGD的函数来实现。SGD来源于随机梯度下降法的英文名称的首字母。下面,我们来实现手写数字识别的神经网络。这里以2层神经网络(隐藏层为1层的网络)为对象,使用MNIST数据集进行学习。

首先,我们将这个2层神经网络实现为一个名为TwoLayerNet的类,实现过程如下所示 。源代码two_layer_net.py中。

  1. # coding: utf-8
  2. from common.functions import *
  3. class TwoLayerNet:
  4. def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
  5. # 初始化权重
  6. self.params = {}
  7. self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
  8. self.params['b1'] = np.zeros(hidden_size)
  9. self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
  10. self.params['b2'] = np.zeros(output_size)
  11. def predict(self, x):
  12. W1, W2 = self.params['W1'], self.params['W2']
  13. b1, b2 = self.params['b1'], self.params['b2']
  14. a1 = np.dot(x, W1) + b1
  15. z1 = sigmoid(a1)
  16. a2 = np.dot(z1, W2) + b2
  17. y = softmax(a2)
  18. return y
  19. # x:输入数据, t:监督数据
  20. def loss(self, x, t):
  21. y = self.predict(x)
  22. return cross_entropy_error(y, t)
  23. def accuracy(self, x, t):
  24. y = self.predict(x)
  25. y = np.argmax(y, axis=1)
  26. t = np.argmax(t, axis=1)
  27. accuracy = np.sum(y == t) / float(x.shape[0])
  28. return accuracy
  29. def gradient(self, x, t):
  30. W1, W2 = self.params['W1'], self.params['W2']
  31. b1, b2 = self.params['b1'], self.params['b2']
  32. grads = {}
  33. batch_num = x.shape[0]
  34. # forward
  35. a1 = np.dot(x, W1) + b1
  36. z1 = sigmoid(a1)
  37. a2 = np.dot(z1, W2) + b2
  38. y = softmax(a2)
  39. # backward
  40. dy = (y - t) / batch_num
  41. grads['W2'] = np.dot(z1.T, dy)
  42. grads['b2'] = np.sum(dy, axis=0)
  43. da1 = np.dot(dy, W2.T)
  44. dz1 = sigmoid_grad(a1) * da1
  45. grads['W1'] = np.dot(x.T, dz1)
  46. grads['b1'] = np.sum(dz1, axis=0)
  47. return grads
变量 说明
params

保存神经网络的参数的字典型变量(实例变量)。
params[‘W1’]是第1层的权重, params[‘b1’]是第1层的偏置。
params[‘W2’]是第2层的权重, params[‘b2’]是第2层的偏置
grads

保存梯度的字典型变量(gradient()方法的返回值)。
grads[‘W1’]是第1层权重的梯度, grads[‘b1’]是第1层偏置的梯度。
grads[‘W2’]是第2层权重的梯度, grads[‘b2’]是第2层偏置的梯度



TwoLayerNet类有params和grads两个字典型实例变量。 params变量中保存了权重参数,比如params[‘W1’]以NumPy数组的形式保存了第1层的权重参数。此外,第1层的偏置可以通过param[‘b1’]进行访问。这里来看一个例子。

  1. net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)
  2. net.params['W1'].shape # (784, 100)
  3. net.params['b1'].shape # (100,)
  4. net.params['W2'].shape # (100, 10)
  5. net.params['b2'].shape # (10,)

如上所示, params变量中保存了该神经网络所需的全部参数。并且,params变量中保存的权重参数会用在推理处理(前向处理)中。顺便说一下,推理处理的实现如下所示。

  1. x = np.random.rand(100, 784) # 伪输入数据(100笔)
  2. y = net.predict(x)

此外,与params变量对应, grads变量中保存了各个参数的梯度。如下所示,使用 numerical_gradient()方法计算梯度后,梯度的信息将保存在 grads变量中。

  1. x = np.random.rand(100, 784) # 伪输入数据(100笔)
  2. t = np.random.rand(100, 10) # 伪正确解标签(100笔)
  3. grads = net.gradient(x, t) # 计算梯度
  4. grads['W1'].shape # (784, 100)
  5. grads['b1'].shape # (100,)
  6. grads['W2'].shape # (100, 10)
  7. grads['b2'].shape # (10,)

接着,我们来看一下 TwoLayerNet的方法的实现。首先是 init(self,input_size, hidden_size, output_size)方法,它是类的初始化方法(所谓初始化方法,就是生成TwoLayerNet实例时被调用的方法)。从第1个参数开始,
依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。另外,因为进行手写数字识别时,输入图像的大小是784(28 × 28),输出为10个类别,所以指定参数input_size=784、 output_size=10,将隐藏层的个数hidden_size设置为一个合适的值即可。

此外,这个初始化方法会对权重参数进行初始化。如何设置权重参数的初始值这个问题是关系到神经网络能否成功学习的重要问题。这里只需要知道,权重使用符合高斯分布的随机数进行初始化,偏置使用 0进行初始化。 predict(self, x)和accuracy(self, x, t)的实现和之前神经网络的推理处理基本一样。如果仍有不明白的地方,请再回顾一下上一章的内容。另外, loss(self, x, t) 是计算损失函数值的方法。这个方法会基于predict()的结果和正确解标签,计算交叉熵误差。
剩下的 numerical_gradient(self, x, t)方法会计算各个参数的梯度。根据数值微分,计算各个参数相对于损失函数的梯度。

神经网络的学习的实现使用的是前面介绍过的mini-batch学习。所谓mini-batch学习,就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。下面,我们就以
TwoLayerNet类为对象,使用MNIST数据集进行学习(源代码在 train_neuralnet.py中)。

  1. # coding: utf-8
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. from dataset.mnist import load_mnist
  5. from two_layer_net import TwoLayerNet
  6. import pickle
  7. # 读入数据
  8. (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
  9. network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
  10. iters_num = 10000 # 适当设定循环的次数
  11. train_size = x_train.shape[0]
  12. batch_size = 100
  13. learning_rate = 0.1
  14. train_loss_list = []
  15. for i in range(iters_num):
  16. batch_mask = np.random.choice(train_size, batch_size)
  17. x_batch = x_train[batch_mask]
  18. t_batch = t_train[batch_mask]
  19. # 计算梯度
  20. grad = network.gradient(x_batch, t_batch)
  21. # 更新参数
  22. for key in ('W1', 'b1', 'W2', 'b2'):
  23. network.params[key] -= learning_rate * grad[key]
  24. loss = network.loss(x_batch, t_batch)
  25. train_loss_list.append(loss)

这里, mini-batch的大小为100,需要每次从60000个训练数据中随机取出100个数据(图像数据和正确解标签数据)。然后,对这个包含100笔数据的mini-batch求梯度,使用随机梯度下降法(SGD)更新参数。这里,梯
度法的更新次数(循环的次数)为10000。每更新一次,都对训练数据计算损失函数的值,并把该值添加到数组中。用图像来表示这个损失函数的值的推移,如图所示。
image.png
可以发现随着学习的进行,损失函数的值在不断减小。这是学习正常进行的信号,表示神经网络的权重参数在逐渐拟合数据。也就是说,神经网络的确在学习!通过反复地向它浇灌(输入)数据,神经网络正在逐渐向最优参数靠近。

我们确认了通过反复学习可以使损失函数的值逐渐减小这一事实。不过这个损失函数的值,严格地讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经网络在其他数据集上也一定能有同等程度的表现。
神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。
神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个
epoch,我们都会记录下训练数据和测试数据的识别精度。

epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000笔训练数据,用大小为 100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了 A。此时,100次就是一个epoch。

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from dataset.mnist import load_mnist
  4. from two_layer_net import TwoLayerNet
  5. import pickle
  6. # 读入数据
  7. (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
  8. network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
  9. iters_num = 10000 # 适当设定循环的次数
  10. train_size = x_train.shape[0]
  11. batch_size = 100
  12. learning_rate = 0.1
  13. train_loss_list = []
  14. train_acc_list = []
  15. test_acc_list = []
  16. iter_per_epoch = max(train_size / batch_size, 1)
  17. for i in range(iters_num):
  18. batch_mask = np.random.choice(train_size, batch_size)
  19. x_batch = x_train[batch_mask]
  20. t_batch = t_train[batch_mask]
  21. # 计算梯度
  22. grad = network.gradient(x_batch, t_batch)
  23. # 更新参数
  24. for key in ('W1', 'b1', 'W2', 'b2'):
  25. network.params[key] -= learning_rate * grad[key]
  26. loss = network.loss(x_batch, t_batch)
  27. train_loss_list.append(loss)
  28. if i % iter_per_epoch == 0:
  29. train_acc = network.accuracy(x_train, t_train)
  30. test_acc = network.accuracy(x_test, t_test)
  31. train_acc_list.append(train_acc)
  32. test_acc_list.append(test_acc)
  33. print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

在上面的例子中,每经过一个epoch,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每一个epoch的识别精度,是因为如果在 for语句的循环中一直计算识别精度,会花费太多时间。并且,也没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推移就可以了)。因此,我们才会每经过一个epoch就记录一次训练数据的识别精度。
把从上面的代码中得到的结果用图表示的话,如图所示。
image.png
实线表示训练数据的识别精度,虚线表示测试数据的识别精度。如图所示,随着epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两
条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。