深度学习入门:基于Python的理论与实现
深度学习入门:基于Python的理论与实现
端,您可以在任意设备上,用自己喜
欢的浏览器和PDF阅读器进行阅读。
但您购买的电子书仅供您个人使用,
未经授权,不得进行传播。
我们愿意相信读者具有这样的良知和
觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对
该用户实施包括但不限于关闭该帐号
等维权措施,并可能追究法律责任。
图灵程序设计丛书
深度学习入门
基于 Python 的理论与实现
Beijing・Boston・Farnham・Sebastopol・Tokyo
O’Reilly Japan, Inc. 授权人民邮电出版社出版
人民邮电出版社
北 京
图书在版编目(CIP)数据
中国版本图书馆CIP数据核字(2018)第112509号
内 容 提 要
本书是深度学习真正意义上的入门书,深入浅出地剖析了深度学习的原理和相关技术。
书中使用 Python 3,尽量不依赖外部库或工具,带领读者从零创建一个经典的深度学习网
络,使读者在此过程中逐步理解深度学习。书中不仅介绍了深度学习和神经网络的概念、
特征等基础知识,对误差反向传播法、卷积神经网络等也有深入讲解,此外还介绍了学
习相关的实用技巧,自动驾驶、图像生成、强化学习等方面的应用,以及为什么加深层
可以提高识别精度等疑难问题。
本书适合深度学习初学者阅读,也可作为高校教材使用。
◆ 著 [日]斋藤康毅
译 陆宇杰
责任编辑 杜晓静
执行编辑 刘香娣
责任印制 周昇亮
◆ 人民邮电出版社出版发行 北京市丰台区成寿寺路 11 号
邮编 100164 电子邮件 [email protected]
网址 http://www.ptpress.com.cn
北京 印刷
◆ 开本:880×1230 1/32
印张:9.625
字数:300 千字 2018 年 7 月第 1 版
印数:27 001 - 29 000 册 2019 年 5 月北京第 9 次印刷
著作权合同登记号 图字:01-2017-0526 号
定价:59.00 元
读者服务热线:(010)51095183转 600 印装质量热线:(010)81055316
反盗版热线:(010)81055315
广告经营许可证:京东工商广登字 20170147 号
版权声明
Copyright © 2016 Koki Saitoh, O’Reilly Japan, Inc.
Posts and Telecommunications Press, 2018.
Authorized translation of the Japanese edition of “Deep Learning from
Scratch” © 2016 O’ Reilly Japan, Inc. This translation is published and sold by
permission of O’ Reilly Japan, Inc., the owner of all rights to publish and sell the
same.
O’Reilly 为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组
织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了 Make 杂志,
从而成为 DIY 革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。
O’Reilly 的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创
新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly 现在还将先锋专家的
知识传递给普通的计算机用户。无论是通过书籍出版、在线服务或者面授课程,每一
项 O’Reilly 的产品都反映了公司不可动摇的理念——信息是激发创新的力量。
业界评论
“O’Reilly Radar 博客有口皆碑。”
——Wired
“O’Reilly 凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”
——Business 2.0
“Tim 是位特立独行的商人,他不光放眼于最长远、最广阔的视野,并且切实地按照
Yogi Berra 的建议去做了:
‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去,
Tim 似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”
——Linux Journal
目录
译者序······················································· xiii
前言························································· xv
1.4.1 保存为文件······································· 9
1.4.2 类· ············································ 10
1.5 NumPy · ·············································· 11
1.5.1 导入 NumPy· ···································· 11
1.5.2 生成 NumPy 数组· ································ 12
1.5.3 NumPy 的算术运算······························· 12
1.5.4 NumPy 的 N 维数组· ······························ 13
1.5.5 广播············································ 14
1.5.6 访问元素········································ 15
1.6 Matplotlib············································· 16
1.6.1 绘制简单图形· ··································· 16
1.6.2 pyplot 的功能· ··································· 17
1.6.3 显示图像········································ 18
1.7 小结·················································· 19
第 2 章 感知机················································ 21
2.1 感知机是什么· ········································· 21
2.2 简单逻辑电路· ········································· 23
2.2.1 与门············································ 23
2.2.2 与非门和或门· ··································· 23
2.3 感知机的实现· ········································· 25
2.3.1 简单的实现······································ 25
2.3.2 导入权重和偏置· ································· 26
2.3.3 使用权重和偏置的实现· ··························· 26
2.4 感知机的局限性· ······································· 28
2.4.1 异或门·········································· 28
2.4.2 线性和非线性· ··································· 30
2.5 多层感知机· ··········································· 31
2.5.1 已有门电路的组合· ······························· 31
2.5.2 异或门的实现· ··································· 33
2.6 从与非门到计算机· ····································· 35
2.7 小结·················································· 36
第 3 章 神经网络·············································· 37
3.1 从感知机到神经网络· ··································· 37
3.1.1 神经网络的例子· ································· 37
3.1.2 复习感知机······································ 38
3.1.3 激活函数登场· ··································· 40
3.2 激活函数·············································· 42
3.2.1 sigmoid 函数· ···································· 42
3.2.2 阶跃函数的实现· ································· 43
3.2.3 阶跃函数的图形· ································· 44
3.2.4 sigmoid 函数的实现· ······························ 45
3.2.5 sigmoid 函数和阶跃函数的比较······················ 46
3.2.6 非线性函数······································ 48
3.2.7 ReLU 函数· ····································· 49
3.3 多维数组的运算· ······································· 50
3.3.1 多维数组········································ 50
3.3.2 矩阵乘法········································ 51
3.3.3 神经网络的内积· ································· 55
3.4 3 层神经网络的实现· ···································· 56
3.4.1 符号确认········································ 57
3.4.2 各层间信号传递的实现· ··························· 58
3.4.3 代码实现小结· ··································· 62
3.5 输出层的设计· ········································· 63
3.5.1 恒等函数和 softmax 函数· ·························· 64
3.5.2 实现 softmax 函数时的注意事项· ···················· 66
3.5.3 softmax 函数的特征· ······························ 67
viii 目录
3.5.4 输出层的神经元数量· ····························· 68
3.6 手写数字识别· ········································· 69
3.6.1 MNIST 数据集· ·································· 70
3.6.2 神经网络的推理处理· ····························· 73
3.6.3 批处理·········································· 75
3.7 小结·················································· 79
第 4 章 神经网络的学习· ······································· 81
4.1 从数据中学习· ········································· 81
4.1.1 数据驱动········································ 82
4.1.2 训练数据和测试数据· ····························· 84
4.2 损失函数·············································· 85
4.2.1 均方误差········································ 85
4.2.2 交叉熵误差······································ 87
4.2.3 mini-batch 学习· ································· 88
4.2.4 mini-batch 版交叉熵误差的实现· ···················· 91
4.2.5 为何要设定损失函数· ····························· 92
4.3 数值微分·············································· 94
4.3.1 导数············································ 94
4.3.2 数值微分的例子· ································· 96
4.3.3 偏导数·········································· 98
4.4 梯度··················································100
4.4.1 梯度法··········································102
4.4.2 神经网络的梯度· ·································106
4.5 学习算法的实现· ·······································109
4.5.1 2 层神经网络的类·································110
4.5.2 mini-batch 的实现· ·······························114
4.5.3 基于测试数据的评价· ·····························116
4.6 小结··················································118
目录 ix
第 5 章 误差反向传播法· ·······································121
5.1 计算图················································121
5.1.1 用计算图求解· ···································122
5.1.2 局部计算········································124
5.1.3 为何用计算图解题· ·······························125
5.2 链式法则··············································126
5.2.1 计算图的反向传播· ·······························127
5.2.2 什么是链式法则· ·································127
5.2.3 链式法则和计算图· ·······························129
5.3 反向传播··············································130
5.3.1 加法节点的反向传播· ·····························130
5.3.2 乘法节点的反向传播· ·····························132
5.3.3 苹果的例子······································133
5.4 简单层的实现· ·········································135
5.4.1 乘法层的实现· ···································135
5.4.2 加法层的实现· ···································137
5.5 激活函数层的实现· ·····································139
5.5.1 ReLU 层· ······································· 139
5.5.2 Sigmoid 层·······································141
5.6 Affine/Softmax 层的实现·································144
5.6.1 Affine 层· ·······································144
5.6.2 批版本的 Affine 层· ······························· 148
5.6.3 Softmax-with-Loss 层· ····························150
5.7 误差反向传播法的实现· ································· 154
5.7.1 神经网络学习的全貌图· ···························154
5.7.2 对应误差反向传播法的神经网络的实现 155
· ··············
5.7.3 误差反向传播法的梯度确认 ·························158
5.7.4 使用误差反向传播法的学习·························159
5.8 小结··················································161
x 目录
第 6 章 与学习相关的技巧· ·····································163
6.1 参数的更新· ···········································163
6.1.1 探险家的故事· ···································164
6.1.2 SGD· ·········································· 164
6.1.3 SGD 的缺点· ···································· 166
6.1.4 Momentum······································168
6.1.5 AdaGrad········································170
6.1.6 Adam· ·········································172
6.1.7 使用哪种更新方法呢· ·····························174
6.1.8 基于 MNIST 数据集的更新方法的比较· ···············175
6.2 权重的初始值· ·········································176
6.2.1 可以将权重初始值设为 0 吗· ························176
6.2.2 隐藏层的激活值的分布· ··························· 177
6.2.3 ReLU 的权重初始值 ·······························181
6.2.4 基于 MNIST 数据集的权重初始值的比较 183
· ·············
第 7 章 卷积神经网络· ·········································201
7.1 整体结构··············································201
7.2 卷积层················································202
7.2.1 全连接层存在的问题· ·····························203
7.2.2 卷积运算········································203
7.2.3 填充············································206
7.2.4 步幅············································207
7.2.5 3 维数据的卷积运算· ······························209
7.2.6 结合方块思考· ···································211
7.2.7 批处理··········································213
7.3 池化层················································214
7.4 卷积层和池化层的实现· ································· 216
7.4.1 4 维数组· ·······································216
7.4.2 基于 im2col 的展开 217
· ·······························
7.4.3 卷积层的实现 219
· ···································
7.4.4 池化层的实现· ···································222
7.5 CNN 的实现· ··········································224
7.6 CNN 的可视化· ········································228
7.6.1 第 1 层权重的可视化·······························228
7.6.2 基于分层结构的信息提取· ························· 230
7.7 具有代表性的 CNN ····································· 231
7.7.1 LeNet· ·········································231
7.7.2 AlexNet·········································232
7.8 小结··················································233
第 8 章 深度学习··············································235
8.1 加深网络··············································235
8.1.1 向更深的网络出发· ·······························235
8.1.2 进一步提高识别精度· ·····························238
xii 目录
8.1.3 加深层的动机· ···································240
8.2 深度学习的小历史· ·····································242
8.2.1 ImageNet· ······································243
8.2.2 VGG· ··········································244
8.2.3 GoogLeNet· ·····································245
8.2.4 ResNet· 246
········································
参考文献· ····················································279
译者序
深度学习的浪潮已经汹涌澎湃了一段时间了,市面上相关的图书也已经
出版了很多。其中,既有知名学者伊恩·古德费洛(Ian Goodfellow)等人撰
写的系统介绍深度学习基本理论的《深度学习》,也有各种介绍深度学习框
架的使用方法的入门书。你可能会问,现在再出一本关于深度学习的书,是
不是“为时已晚”
?其实并非如此,因为本书考察深度学习的角度非常独特,
它的出版可以说是“千呼万唤始出来”。
本书最大的特点是“剖解”了深度学习的底层技术。正如美国物理学家
理 查 德·费 曼(Richard Phillips Feynman)所 说:
“What I cannot create, I
do not understand.”只有创造一个东西,才算真正弄懂了一个问题。本书就
是教你如何创建深度学习模型的一本书。并且,本书不使用任何现有的深度
学习框架,尽可能仅使用最基本的数学知识和 Python 库,从零讲解深度学
习核心问题的数学原理,从零创建一个经典的深度学习网络。
本书的日文版曾一度占据了东京大学校内书店(本乡校区)理工类图书
的畅销书榜首。各类读者阅读本书,均可有所受益。对于非 AI 方向的技术
人员,本书将大大降低入门深度学习的门槛;对于在校的大学生、研究生,
本书不失为学习深度学习的一本好教材;即便是对于在工作中已经熟练使用
框架开发各类深度学习模型的读者,也可以从本书中获得新的体会。
本书从开始翻译到出版,前前后后历时一年之久。译者翻译时力求忠于
原文,表达简练。为了保证翻译质量,每翻译完一章后,译者都会放置一段
xiv 译者序
时间,再重新检查一遍。图灵公司的专业编辑们又进一步对译稿进行了全面
细致的校对,提出了许多宝贵意见,在此表示感谢。但是,由于译者才疏学浅,
书中难免存在一些错误或疏漏,恳请读者批评指正,以便我们在重印时改正。
最后,希望本书的出版能为国内的 AI 技术社区添砖加瓦!
陆宇杰
2018 年 2 月 上海
前言
科幻电影般的世界已经变成了现实—人工智能战胜过日本将棋、国际
象棋的冠军,最近甚至又打败了围棋冠军;智能手机不仅可以理解人们说的话,
还能在视频通话中进行实时的“机器翻译”;配备了摄像头的“自动防撞的车”
保护着人们的生命安全,自动驾驶技术的实用化也为期不远。环顾我们的四
周,原来被认为只有人类才能做到的事情,现在人工智能都能毫无差错地完
成,甚至试图超越人类。因为人工智能的发展,我们所处的世界正在逐渐变
成一个崭新的世界。
在这个发展速度惊人的世界背后,深度学习技术在发挥着重要作用。对
于深度学习,世界各地的研究人员不吝褒奖之辞,称赞其为革新性技术,甚
至有人认为它是几十年才有一次的突破。实际上,深度学习这个词经常出现
在报纸和杂志中,备受关注,就连一般大众也都有所耳闻。
本书就是一本以深度学习为主题的书,目的是让读者尽可能深入地理解
深度学习的技术。因此,本书提出了“从零开始”这个概念。
本书的特点是通过实现深度学习的过程,来逼近深度学习的本质。通过
实现深度学习的程序,尽可能无遗漏地介绍深度学习相关的技术。另外,本
书还提供了实际可运行的程序,供读者自己进行各种各样的实验。
为了实现深度学习,我们需要经历很多考验,花费很长时间,但是相应
地也能学到和发现很多东西。而且,实现深度学习的过程是一个有趣的、令
人兴奋的过程。希望读者通过这一过程可以熟悉深度学习中使用的技术,并
能从中感受到快乐。
目前,深度学习活跃在世界上各个地方。在几乎人手一部的智能手机中、
开启自动驾驶的汽车中、为 Web 服务提供动力的服务器中,深度学习都在
发挥着作用。此时此刻,就在很多人没有注意到的地方,深度学习正在默默
地发挥着其功能。今后,深度学习势必将更加活跃。为了让读者理解深度学
习的相关技术,感受到深度学习的魅力,笔者写下了本书。
本书的理念
本书是一本讲解深度学习的书,将从最基础的内容开始讲起,逐一介绍
理解深度学习所需的知识。书中尽可能用平实的语言来介绍深度学习的概念、
特征、工作原理等内容。不过,本书并不是只介绍技术的概要,而是旨在让
读者更深入地理解深度学习。这是本书的特色之一。
那么,怎么才能更深入地理解深度学习呢?在笔者看来,最好的办法就
是亲自实现。从零开始编写可实际运行的程序,一边看源代码,一边思考。
笔者坚信,这种做法对正确理解深度学习(以及那些看上去很高级的技术)
是很重要的。这里用了“从零开始”一词,表示我们将尽可能地不依赖外部
的现成品(库、工具等)。也就是说,本书的目标是,尽量不使用内容不明的
黑盒,而是从自己能理解的最基础的知识出发,一步一步地实现最先进的深
度学习技术。并通过这一实现过程,使读者加深对深度学习的理解。
如果把本书比作一本关于汽车的书,那么本书并不会教你怎么开车,其
着眼点不是汽车的驾驶方法,而是要让读者理解汽车的原理。为了让读者理
解汽车的结构,必须打开汽车的引擎盖,把零件一个一个地拿在手里观察,
并尝试操作它们。之后,用尽可能简单的形式提取汽车的本质,并组装汽车
模型。本书的目标是,通过制造汽车模型的过程,让读者感受到自己可以实
际制造出汽车,并在这一过程中熟悉汽车相关的技术。
为了实现深度学习,本书使用了 Python 这一编程语言。Python 非常受
欢迎,初学者也能轻松使用。Python 尤其适合用来制作样品(原型),使用
前言 xvii
Python 可以立刻尝试突然想到的东西,一边观察结果,一边进行各种各样
的实验。本书将在讲解深度学习理论的同时,使用 Python 实现程序,进行
各种实验。
在光看数学式和理论说明无法理解的情况下,可以尝试阅读源代码
并运行,很多时候思路都会变得清晰起来。对数学式感到困惑时,
就阅读源代码来理解技术的流程,这样的事情相信很多人都经历过。
本书通过实际实现(落实到代码)来理解深度学习,是一本强调“工程”
的书。书中会出现很多数学式,但同时也会有很多程序员视角的源代码。
本书面向的读者
本书旨在让读者通过实际动手操作来深入理解深度学习。为了明确本书
的读者对象,这里将本书涉及的内容列举如下。
• 使用 Python,尽可能少地使用外部库,从零开始实现深度学习的程序。
• 为了让 Python 的初学者也能理解,介绍 Python 的使用方法。
• 提供实际可运行的 Python 源代码,同时提供可以让读者亲自实验的
学习环境。
• 从简单的机器学习问题开始,最终实现一个能高精度地识别图像的系统。
• 以简明易懂的方式讲解深度学习和神经网络的理论。
• 对于误差反向传播法、卷积运算等乍一看很复杂的技术,使读者能够
在实现层面上理解。
• 介绍一些学习深度学习时有用的实践技巧,如确定学习率的方法、权
重的初始值等。
• 介绍最近流行的 Batch Normalization、Dropout、Adam 等,并进行
实现。
• 讨论为什么深度学习表现优异、为什么加深层能提高识别精度、为什
么隐藏层很重要等问题。
• 介绍自动驾驶、图像生成、强化学习等深度学习的应用案例。
xviii 前言
本书不面向的读者
明确本书不适合什么样的读者也很重要。为此,这里将本书不会涉及的
内容列举如下。
• 不介绍深度学习相关的最新研究进展。
• 不介绍 Caffe、TensorFlow、Chainer 等深度学习框架的使用方法。
• 不介绍深度学习的详细理论,特别是神经网络相关的详细理论。
• 不详细介绍用于提高识别精度的参数调优相关的内容。
• 不会为了实现深度学习的高速化而进行 GPU 相关的实现。
• 本书以图像识别为主题,不涉及自然语言处理或者语音识别的例子。
综上,本书不涉及最新研究和理论细节。但是,读完本书之后,读者
应该有能力进一步去阅读最新的论文或者神经网络相关的理论方面的技
术书。
本书以图像识别为主题,主要学习使用深度学习进行图像识别时
所需的技术。自然语言处理或者语音识别等不是本书的讨论对象。
本书的阅读方法
学习新知识时,只听别人讲解的话,有时会无法理解,或者会立刻忘记。
正如“不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之”A ,在
学习新东西时,没有什么比实践更重要了。本书在介绍某个主题时,都细心
地准备了一个可以实践的场所——能够作为程序运行的源代码。
本书会提供 Python 源代码,读者可以自己动手实际运行这些源代码。
在阅读源代码的同时,可以尝试去实现一些自己想到的东西,以确保真正
A 出自荀子《儒效篇》。
前言 xix
理解了。另外,读者也可以使用本书的源代码,尝试进行各种实验,反复
试错。
本书将沿着“理论说明”和“Python 实现”两个路线前进。因此,建议
读者准备好编程环境。本书可以使用 Windows、Mac、Linux 中的任何一个
系统。关于 Python 的安装和使用方法将在第 1 章介绍。另外,本书中用到
的程序可以从以下网址下载。
http://www.ituring.com.cn/book/1921
让我们开始吧
通过前面的介绍,希望读者了解本书大概要讲的内容,产生继续阅读的
兴趣。
最近出现了很多深度学习相关的库,任何人都可以方便地使用。实际上,
使用这些库的话,可以轻松地运行深度学习的程序。那么,为什么我们还要
特意花时间从零开始实现深度学习呢?一个理由就是,在制作东西的过程中
可以学到很多。
在制作东西的过程中,会进行各种各样的实验,有时也会卡住,抱着脑
袋想为什么会这样。这种费时的工作对深刻理解技术而言是宝贵的财富。像
这样认真花费时间获得的知识在使用现有的库、阅读最新的文章、创建原创
的系统时都大有用处。而且最重要的是,制作本身就是一件快乐的事情。(还
需要快乐以外的其他什么理由吗?)
既然一切都准备好了,下面就让我们踏上实现深度学习的旅途吧!
表述规则
本书在表述上采用如下规则。
粗体字(Bold)
用来表示新引入的术语、强调的要点以及关键短语。
xx 前言
等宽字(Constant Width)
用来表示下面这些信息:程序代码、命令、序列、组成元素、语句选项、
分支、变量、属性、键值、函数、类型、类、命名空间、方法、模块、属性、
参数、值、对象、事件、事件处理器、XML 标签、HTML 标签、宏、文件
的内容、来自命令行的输出等。若在其他地方引用了以上这些内容(如变量、
函数、关键字等),也会使用该格式标记。
用来表示提示、启发以及某些值得深究的内容的补充信息。
读者意见与咨询
虽然笔者已经尽最大努力对本书的内容进行了验证与确认,但仍不免在
某些地方出现错误或者容易引起误解的表达等,给读者的理解带来困扰。如
果读者遇到这些问题,请及时告知,我们在本书重印时会将其改正,在此先
表示不胜感激。与此同时,也希望读者能够为本书将来的修订提出中肯的建
议。本书编辑部的联系方式如下。
前言 xxi
本书的主页地址如下。
http://www.ituring.com.cn/book/1921
http://www.oreilly.co.jp/books/9784873117584(日语)
https://github.com/oreilly-japan/deep-learning-from-scratch
http://www.oreilly.com/(英语)
http://www.oreilly.co.jp/(日语)
致谢
首先,笔者要感谢推动了深度学习相关技术(机器学习、计算机科学等)
发展的研究人员和工程师。本书的完成离不开他们的研究工作。其次,笔者
还要感谢在图书或网站上公开有用信息的各位同仁。其中,斯坦福大学的
CS231n [5] 公开课慷慨提供了很多有用的技术和信息,笔者从中学到了很多东西。
在本书执笔过程中,曾受到下列人士的帮助:teamLab 公司的加藤哲朗、
喜多慎弥、飞永由夏、中野皓太、中村将达、林辉大、山本辽;Top Studio
公司的武藤健志、增子萌;Flickfit 公司的野村宪司;得克萨斯大学奥斯汀
分校 JSPS 海外特别研究员丹野秀崇。他们阅读了本书原稿,提出了很多宝
贵的建议,在此深表谢意。另外,需要说明的是,本书中存在的不足或错误
均是笔者的责任。
最后,还要感谢 O’ Reilly Japan 的宮川直树,在从本书的构想到完成的
大约一年半的时间里,宫川先生一直支持着笔者。非常感谢!
2016 年 9 月 1 日
斋藤康毅
第1章
Python 入门
Python 是一个简单、易读、易记的编程语言,而且是开源的,可以免
费地自由使用。Python 可以用类似英语的语法编写程序,编译起来也不费
力,因此我们可以很轻松地使用 Python。特别是对首次接触编程的人士来说,
Python 是最合适不过的语言。事实上,很多高校和大专院校的计算机课程
均采用 Python 作为入门语言。
此外,使用 Python 不仅可以写出可读性高的代码,还可以写出性能高(处
理速度快)的代码。在需要处理大规模数据或者要求快速响应的情况下,使
用 Python 可以稳妥地完成。因此,Python 不仅受到初学者的喜爱,同时也
受到专业人士的喜爱。实际上,Google、Microsoft、Facebook 等战斗在 IT
行业最前沿的企业也经常使用 Python。
2 第 1 章 Python 入门
再者,在科学领域,特别是在机器学习、数据科学领域,Python 也被
大 量 使 用。Python 除 了 高 性 能 之 外,凭 借 着 NumPy、SciPy 等 优 秀 的 数
值计算、统计分析库,在数据科学领域占有不可动摇的地位。深度学习的
框架中也有很多使用 Python 的场景,比如 Caffe、TensorFlow、Chainer、
Theano 等著名的深度学习框架都提供了 Python 接口。因此,学习 Python
对使用深度学习框架大有益处。
综上,Python 是最适合数据科学领域的编程语言。而且,Python 具有
受众广的优秀品质,从初学者到专业人士都在使用。因此,为了完成本书的
从零开始实现深度学习的目标,Python 可以说是最合适的工具。
1.2.1 Python 版本
1.2.2 使用的外部库
本书的目标是从零开始实现深度学习。因此,除了NumPy库和Matplotlib
库之外,我们极力避免使用外部库。之所以使用这两个库,是因为它们可以
有效地促进深度学习的实现。
1.2 Python 的安装 3
NumPy 是用于数值计算的库,提供了很多高级的数学算法和便利的数
组(矩阵)操作方法。本书中将使用这些便利的方法来有效地促进深度学习
的实现。
Matplotlib 是用来画图的库。使用 Matplotlib 能将实验结果可视化,并
在视觉上确认深度学习运行期间的数据。
本书将使用下列编程语言和库。
• Python 3.x(2016 年 8 月时的最新版本是 3.5)
• NumPy
• Matplotlib
1.2.3 Anaconda 发行版
A Anaconda 作为一个针对数据分析的发行版,包含了许多有用的库,而本书中实际上只会使用其中的
NumPy 库和 Matplotlib 库。因此,如果想保持轻量级的开发环境,单独安装这两个库也是可以的。
——译者注
Python 的版本信息。
$ python --version
Python 3.4.1 :: Anaconda 2.1.0 (x86_64)
$ python
Python 3.4.1 |Anaconda 2.1.0 (x86_64)| (default, Sep 10 2014, 17:24:09)
[GCC 4.2.1 (Apple Inc. build 5577)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> 1 + 2
3
Python 解释器可以像这样进行对话式(交互式)的编程。下面,我们使
用这个对话模式,来看几个简单的 Python 编程的例子。
1.3.1 算术计算
加法或乘法等算术计算,可按如下方式进行。
>>> 1 - 2
-1
>>> 4 * 5
20
1.3 Python 解释器 5
>>> 7 / 5
1.4
>>> 3 ** 2
9
1.3.2 数据类型
编程中有数据类型(data type)这一概念。数据类型表示数据的性质,
有整数、小数、字符串等类型。Python 中的 type() 函数可以用来查看数据
类型。
>>> type(10)
<class 'int'>
>>> type(2.718)
<class 'float'>
>>> type("hello")
<class 'str'>
型)”。
1.3.3 变量
可以使用 x 或 y 等字母定义变量(variable)。此外,可以使用变量进行计算,
也可以对变量赋值。
>>> x = 10 # 初始化
>>> print(x) # 输出 x
10
>>> x = 100 # 赋值
>>> print(x)
100
>>> y = 3.14
>>> x * y
314.0
>>> type(x * y)
<class 'float'>
Python 是属于“动态类型语言”的编程语言,所谓动态,是指变量的类
型是根据情况自动决定的。在上面的例子中,用户并没有明确指出“x 的类
型是 int(整型)”,是 Python 根据 x 被初始化为 10,从而判断出 x 的类型为
int 的。此外,我们也可以看到,整数和小数相乘的结果是小数(数据类型的
1.3.4 列表
除了单一的数值,还可以用列表(数组)汇总数据。
>>> print(a)
[1, 2, 3, 4, 99]
>>> a[0:2] # 获取索引为 0 到 2(不包括 2 !)的元素
[1, 2]
>>> a[1:] # 获取从索引为 1 的元素到最后一个元素
[2, 3, 4, 99]
1.3 Python 解释器 7
1.3.5 字典
列表根据索引,按照 0, 1, 2, . . . 的顺序存储值,而字典则以键值对的形
式存储数据。字典就像《新华字典》那样,将单词和它的含义对应着存储起来。
1.3.6 布尔型
1.3.7 if 语句
的语句开头有4个空白字符。它是缩进的意思,表示当前面的条件(if hungry)
Python 使用空白字符表示缩进。一般而言,每缩进一次,使用 4
个空白字符。
1.3.8 for 语句
可以按顺序访问列表等数据集合中的各个元素。
1.3.9 函数
可以将一连串的处理定义成函数(function)。
此外,函数可以取参数。
另外,字符串的拼接可以使用 +。
关闭 Python 解释器时,Linux 或 Mac OS X 的情况下输入 Ctrl-D(按住
Ctrl,再按 D 键);Windows 的情况下输入 Ctrl-Z,然后按 Enter 键。
1.4.1 保存为文件
print("I'm hungry!")
$ cd ~/deep-learning-from-scratch/ch01 # 移动目录
$ python hungry.py
I'm hungry!
1.4.2 类
class 类名:
def __init__(self, 参数 , …): # 构造函数
...
def 方法名 1(self, 参数 , …): # 方法 1
...
def 方法名 2(self, 参数 , …): # 方法 2
...
class Man:
def __init__(self, name):
self.name = name
print("Initialized!")
def hello(self):
print("Hello " + self.name + "!")
def goodbye(self):
print("Good-bye " + self.name + "!")
m = Man("David")
m.hello()
m.goodbye()
从终端运行 man.py。
$ python man.py
Initialized!
Hello David!
Good-bye David!
1.5 NumPy
在深度学习的实现中,经常出现数组和矩阵的计算。NumPy 的数组类
(numpy.array)中提供了很多便捷的方法,在实现深度学习时,我们将使用这
些方法。本节我们来简单介绍一下后面会用到的 NumPy。
1.5.1 导入 NumPy
1.5.2 生成 NumPy 数组
1.5.3 NumPy 的算术运算
1.5.4 NumPy 的 N 维数组
NumPy 不仅可以生成一维数组(排成一列的数组),也可以生成多维数组。
比如,可以生成如下的二维数组(矩阵)。
和数组的算术运算一样,矩阵的算术运算也可以在相同形状的矩阵间以
对应元素的方式进行。并且,也可以通过标量(单一数值)对矩阵进行算术运算。
这也是基于广播的功能。
>>> print(A)
[[1 2]
[3 4]]
14 第 1 章 Python 入门
>>> A * 10
array([[ 10, 20],
[ 30, 40]])
1.5.5 广播
NumPy 中,形状不同的数组之间也可以进行运算。之前的例子中,在
2×2 的矩阵 A 和标量 10 之间进行了乘法运算。在这个过程中,如图 1-1 所示,
标量 10 被扩展成了 2 × 2 的形状,然后再与矩阵 A 进行乘法运算。这个巧妙
的功能称为广播(broadcast)。
1 2
*
10
= 1 2
*
10 10
= 10 20
3 4 3 4 10 10 30 40
我们通过下面这个运算再来看一个广播的例子。
1 2
*
10 20
= 1 2
*
10 20
= 10 40
3 4 3 4 10 20 30 80
图 1-2 广播的例子 2
1.5.6 访问元素
元素的索引从 0 开始。对各个元素的访问可按如下方式进行。
除了前面介绍的索引操作,NumPy 还可以使用数组访问各个元素。
运用这个标记法,可以获取满足一定条件的元素。例如,要从 X 中抽出
大于 15 的元素,可以写成如下形式。
16 第 1 章 Python 入门
>>> X > 15
array([ True, True, False, True, False, False], dtype=bool)
>>> X[X>15]
array([51, 55, 19])
布尔型的数组。上例中就是使用这个布尔型数组取出了数组的各个元素(取
出 True 对应的元素)。
1.6 Matplotlib
在深度学习的实验中,图形的绘制和数据的可视化非常重要。Matplotlib
是用于绘制图形的库,使用 Matplotlib 可以轻松地绘制图形和实现数据的可
视化。这里,我们来介绍一下图形的绘制方法和图像的显示方法。
1.6.1 绘制简单图形
import numpy as np
import matplotlib.pyplot as plt
# 生成数据
x = np.arange(0, 6, 0.1) # 以 0.1 为单位,生成 0 到 6 的数据
y = np.sin(x)
# 绘制图形
1.6 Matplotlib 17
plt.plot(x, y)
plt.show()
这里使用 NumPy 的 arange 方法生成了 [0, 0.1, 0.2, ���, 5.8, 5.9] 的
1.0
0.5
0.0
−0.5
−1.0
0 1 2 3 4 5 6
图 1-3 sin 函数的图形
1.6.2 pyplot 的功能
import numpy as np
import matplotlib.pyplot as plt
# 生成数据
x = np.arange(0, 6, 0.1) # 以 0.1 为单位,生成 0 到 6 的数据
y1 = np.sin(x)
18 第 1 章 Python 入门
y2 = np.cos(x)
# 绘制图形
plt.plot(x, y1, label="sin")
plt.plot(x, y2, linestyle = "--", label="cos") # 用虚线绘制
plt.xlabel("x") # x 轴标签
plt.ylabel("y") # y 轴标签
plt.title('sin & cos') # 标题
plt.legend()
plt.show()
0.5
0.0
y
−0.5
−1.0
0 1 2 3 4 5 6
x
1.6.3 显示图像
plt.show()
50
100
150
200
250
0 50 100 150 200 250
图 1-5 显示图像
1.7 小结
本章重点介绍了实现深度学习(神经网络)所需的编程知识,以为学习
深度学习做好准备。从下一章开始,我们将通过使用 Python 实际运行代码,
逐步了解深度学习。
20 第 1 章 Python 入门
本章所学的内容
• Python 是一种简单易记的编程语言。
• Python 是开源的,可以自由使用。
• 本书中将使用 Python 3.x 实现深度学习。
• 本书中将使用 NumPy 和 Matplotlib 这两种外部库。
• Python 有“解释器”和“脚本文件”两种运行模式。
• Python 能够将一系列处理集成为函数或类等模块。
• NumPy 中有很多用于操作多维数组的便捷方法。
第2章
感知机
2.1 感知机是什么
感知机接收多个输入信号,输出一个信号。这里所说的“信号”可以想
象成电流或河流那样具备“流动性”的东西。像电流流过导线,向前方输送
电子一样,感知机的信号也会形成流,向前方输送信息。但是,和实际的电
流不同的是,感知机的信号只有“流 / 不流”
(1/0)两种取值。在本书中,0
对应“不传递信号”,1 对应“传递信号”。
图 2-1 是一个接收两个输入信号的感知机的例子。x1、x2 是输入信号,
y 是 输 出 信 号,w1、w2 是 权 重(w 是 weight 的 首 字 母)。图 中 的 ○ 称 为“神
经元”或者“节点”。输入信号被送往神经元时,会被分别乘以固定的权重
A 严格地讲,本章中所说的感知机应该称为“人工神经元”或“朴素感知机”,但是因为很多基本的处
理都是共通的,所以这里就简单地称为“感知机”。
22 第 2 章 感知机
(w1x1、w2x2)
。神经元会计算传送过来的信号的总和,只有当这个总和超过
了某个界限值时,才会输出 1。这也称为“神经元被激活”。这里将这个界
限值称为阈值,用符号 θ 表示。
x1
w1
y
w2
x2
图 2-1 有两个输入的感知机
感知机的运行原理只有这些!把上述内容用数学式来表示,就是式(2.1)。
(2.1)
感知机的多个输入信号都有各自固有的权重,这些权重发挥着控制各个
信号的重要性的作用。也就是说,权重越大,对应该权重的信号的重要性就
越高。
权重相当于电流里所说的电阻。电阻是决定电流流动难度的参数,
电阻越低,通过的电流就越大。而感知机的权重则是值越大,通过
的信号就越大。不管是电阻还是权重,在控制信号流动难度(或者流
动容易度)这一点上的作用都是一样的。
2.2 简单逻辑电路
2.2.1 与门
现在让我们考虑用感知机来解决简单的问题。这里首先以逻辑电路为题
材来思考一下与门(AND gate)。与门是有两个输入和一个输出的门电路。图 2-2
这种输入信号和输出信号的对应表称为“真值表”。如图 2-2 所示,与门仅在
两个输入均为 1 时输出 1,其他时候则输出 0。
x1 x2 y
0 0 0
1 0 0
0 1 0
1 1 1
图 2-2 与门的真值表
下面考虑用感知机来表示这个与门。需要做的就是确定能满足图 2-2 的
真值表的 w1、w2、θ 的值。那么,设定什么样的值才能制作出满足图 2-2 的
条件的感知机呢?
实际上,满足图 2-2 的条件的参数的选择方法有无数多个。比如,当
(w1, w2, θ) = (0.5, 0.5, 0.7) 时,可 以 满 足 图 2-2 的 条 件。此 外,当 (w1, w2, θ)
为 (0.5, 0.5, 0.8) 或者 (1.0, 1.0, 1.0) 时,同样也满足与门的条件。设定这样的
参数后,仅当 x1 和 x2 同时为 1 时,信号的加权总和才会超过给定的阈值 θ。
2.2.2 与非门和或门
x1 x2 y
0 0 1
1 0 1
0 1 1
1 1 0
图 2-3 与非门的真值表
x1 x2 y
0 0 0
1 0 1
0 1 1
1 1 1
图 2-4 或门的真值表
2.3 感知机的实现 25
这里决定感知机参数的并不是计算机,而是我们人。我们看着真值
表这种“训练数据”,人工考虑(想到)了参数的值。而机器学习的课
题就是将这个决定参数值的工作交由计算机自动进行。学习是确定
合适的参数的过程,而人要做的是思考感知机的构造(模型),并把
训练数据交给计算机。
如上所示,我们已经知道使用感知机可以表示与门、与非门、或门的逻
辑电路。这里重要的一点是:与门、与非门、或门的感知机构造是一样的。
实际上,3 个门电路只有参数的值(权重和阈值)不同。也就是说,相同构造
的感知机,只需通过适当地调整参数的值,就可以像“变色龙演员”表演不
同的角色一样,变身为与门、与非门、或门。
2.3 感知机的实现
2.3.1 简单的实现
在函数内初始化参数 w1、w2、theta,当输入的加权总和超过阈值时返回 1,
否则返回 0。我们来确认一下输出结果是否如图 2-2 所示。
AND(0, 0) # 输出 0
AND(1, 0) # 输出 0
AND(0, 1) # 输出 0
AND(1, 1) # 输出 1
果然和我们预想的输出一样!这样我们就实现了与门。按照同样的步骤,
也可以实现与非门和或门,不过让我们来对它们的实现稍作修改。
2.3.2 导入权重和偏置
刚才的与门的实现比较直接、容易理解,但是考虑到以后的事情,我们
将其修改为另外一种实现形式。在此之前,首先把式(2.1)的 θ 换成 −b,于
是就可以用式(2.2)来表示感知机的行为。
(2.2)
式(2.1)和式(2.2)虽然有一个符号不同,但表达的内容是完全相同的。
此处,b 称为偏置,w1 和 w2 称为权重。如式(2.2)所示,感知机会计算输入
信号和权重的乘积,然后加上偏置,如果这个值大于 0 则输出 1,否则输出 0。
下面,我们使用 NumPy,按式(2.2)的方式实现感知机。在这个过程中,我
们用 Python 的解释器逐一确认结果。
2.3.3 使用权重和偏置的实现
使用权重和偏置,可以像下面这样实现与门。
A
偏置这个术语,有“穿木屐” 的效果,即在没有任何输入时(输入为
0 时),给输出穿上多高的木屐(加上多大的值)的意思。实际上,在
式 (2.2) 的 b + w1x1 + w2x2 的计算中,当输入 x1 和 x2 为 0 时,只输出
偏置的值。
A
接着,我们继续实现与非门和或门。
A 因为木屐的底比较厚,穿上它后,整个人也会显得更高。——译者注
x = np.array([x1, x2])
w = np.array([0.5, 0.5]) # 仅权重和偏置与 AND 不同!
b = -0.2
tmp = np.sum(w*x) + b
if tmp <= 0:
return 0
else:
return 1
2.4 感知机的局限性
到这里我们已经知道,使用感知机可以实现与门、与非门、或门三种逻
辑电路。现在我们来考虑一下异或门(XOR gate)。
2.4.1 异或门
x1 x2 y
0 0 0
1 0 1
0 1 1
1 1 0
图 2-5 异或门的真值表
2.4 感知机的局限性 29
实际上,用前面介绍的感知机是无法实现这个异或门的。为什么用感知
机可以实现与门、或门,却无法实现异或门呢?下面我们尝试通过画图来思
考其中的原因。
首先,我们试着将或门的动作形象化。或门的情况下,当权重参数
(b, w1, w2) = (−0.5, 1.0, 1.0) 时,可满足图 2-4 的真值表条件。此时,感知机
可用下面的式(2.3)表示。
(2.3)
x2
x1
0 1
图 2-6 感知机的可视化:灰色区域是感知机输出 0 的区域,这个区域与或门的性质一致
或门在 (x1, x2) = (0, 0) 时输出 0,在 (x1, x2) 为 (0, 1)、(1, 0)、(1, 1) 时输
出 1。图 2-6 中,○表示 0,△表示 1。如果想制作或门,需要用直线将图 2-6
30 第 2 章 感知机
中的○和△分开。实际上,刚才的那条直线就将这 4 个点正确地分开了。
那么,换成异或门的话会如何呢?能否像或门那样,用一条直线作出分
割图 2-7 中的○和△的空间呢?
x2
x1
0 1
图 2-7 ○和△表示异或门的输出。可否通过一条直线作出分割○和△的空间呢?
2.4.2 线性和非线性
图 2-7 中的○和△无法用一条直线分开,但是如果将“直线”这个限制条
件去掉,就可以实现了。比如,我们可以像图 2-8 那样,作出分开○和△的空间。
感知机的局限性就在于它只能表示由一条直线分割的空间。图2-8 这样弯
曲的曲线无法用感知机表示。另外,由图 2-8 这样的曲线分割而成的空间称为
非线性空间,由直线分割而成的空间称为线性空间。线性、非线性这两个术
语在机器学习领域很常见,可以将其想象成图 2-6 和图2-8所示的直线和曲线。
2.5 多层感知机 31
x2
0
x1
1
图 2-8 使用曲线可以分开○和△
2.5 多层感知机
感知机不能表示异或门让人深感遗憾,但也无需悲观。实际上,感知机
的绝妙之处在于它可以“叠加层”
(通过叠加层来表示异或门是本节的要点)
。
这里,我们暂且不考虑叠加层具体是指什么,先从其他视角来思考一下异或
门的问题。
2.5.1 已有门电路的组合
异或门的制作方法有很多,其中之一就是组合我们前面做好的与门、与
非门、或门进行配置。这里,与门、与非门、或门用图 2-9 中的符号表示。另外,
图 2-9 中与非门前端的○表示反转输出的意思。
那么,请思考一下,要实现异或门的话,需要如何配置与门、与非门和
或门呢?这里给大家一个提示,用与门、与非门、或门代替图 2-10 中的各个
“?”,就可以实现异或门。
32 第 2 章 感知机
AND NAND OR
图 2-9 与门、与非门、或门的符号
x1
x2
图 2-10 将与门、与非门、或门代入到“?”中,就可以实现异或门!
2.4 节讲到的感知机的局限性,严格地讲,应该是“单层感知机无法
表示异或门”或者“单层感知机无法分离非线性空间”。接下来,我
们将看到通过组合感知机(叠加层)就可以实现异或门。
x1
s1
y
s2
x2
图 2-11 通过组合与门、与非门、或门实现异或门
x1 x2 s1 s2 y
0 0 1 0 0
1 0 1 1 1
0 1 1 1 1
1 1 0 1 0
图 2-12 异或门的真值表
2.5.2 异或门的实现
这个 XOR 函数会输出预期的结果。
XOR(0, 0) # 输出 0
XOR(1, 0) # 输出 1
XOR(0, 1) # 输出 1
XOR(1, 1) # 输出 0
这样,异或门的实现就完成了。下面我们试着用感知机的表示方法(明
确地显示神经元)来表示这个异或门,结果如图 2-13 所示。
如图 2-13 所示,异或门是一种多层结构的神经网络。这里,将最左边的
一列称为第 0 层,中间的一列称为第 1 层,最右边的一列称为第 2 层。
图 2-13 所示的感知机与前面介绍的与门、或门的感知机(图 2-1)形状不
同。实际上,与门、或门是单层感知机,而异或门是 2 层感知机。叠加了多
层的感知机也称为多层感知机(multi-layered perceptron)。
34 第 2 章 感知机
x1 s1
x2 s2
图 2-13 用感知机表示异或门
1. 第 0 层的两个神经元接收输入信号,并将信号发送至第 1 层的神经元。
2. 第 1 层的神经元将信号发送至第 2 层的神经元,第 2 层的神经元输出 y。
这种 2 层感知机的运行过程可以比作流水线的组装作业。第 1 段(第 1 层)
的工人对传送过来的零件进行加工,完成后再传送给第 2 段(第 2 层)的工人。
第 2 层的工人对第 1 层的工人传过来的零件进行加工,完成这个零件后出货
(输出)。
像这样,在异或门的感知机中,工人之间不断进行零件的传送。通过这
样的结构(2 层结构),感知机得以实现异或门。这可以解释为“单层感知机
无法表示的东西,通过增加一层就可以解决”。也就是说,通过叠加层(加深
层),感知机能进行更加灵活的表示。
2.6 从与非门到计算机 35
2.6 从与非门到计算机
多层感知机可以实现比之前见到的电路更复杂的电路。比如,进行加法
运算的加法器也可以用感知机实现。此外,将二进制转换为十进制的编码器、
满足某些条件就输出 1 的电路(用于等价检验的电路)等也可以用感知机表示。
实际上,使用感知机甚至可以表示计算机!
计算机是处理信息的机器。向计算机中输入一些信息后,它会按照某种
既定的方法进行处理,然后输出结果。所谓“按照某种既定的方法进行处理”
是指,计算机和感知机一样,也有输入和输出,会按照某个既定的规则进行
计算。
人们一般会认为计算机内部进行的处理非常复杂,而令人惊讶的是,实
际上只需要通过与非门的组合,就能再现计算机进行的处理。这一令人吃惊
的事实说明了什么呢?说明使用感知机也可以表示计算机。前面也介绍了,
与非门可以使用感知机实现。也就是说,如果通过组合与非门可以实现计算
机的话,那么通过组合感知机也可以表示计算机(感知机的组合可以通过叠
加了多层的单层感知机来表示)。
说到仅通过与非门的组合就能实现计算机,大家也许一下子很难相信。
建议有兴趣的读者看一下《计算机系统要素:从零开始构建现代计
算机》。这本书以深入理解计算机为主题,论述了通过 NAND 构建可
运行俄罗斯方块的计算机的过程。此书能让读者真实体会到,通过
简单的 NAND 元件就可以实现计算机这样复杂的系统。
综上,多层感知机能够进行复杂的表示,甚至可以构建计算机。那么,
什么构造的感知机才能表示计算机呢?层级多深才可以构建计算机呢?
理论上可以说 2 层感知机就能构建计算机。这是因为,已有研究证明,
2 层感知机(严格地说是激活函数使用了非线性的 sigmoid 函数的感知机,具
体请参照下一章)可以表示任意函数。但是,使用 2 层感知机的构造,通过
设定合适的权重来构建计算机是一件非常累人的事情。实际上,在用与非门
等低层的元件构建计算机的情况下,分阶段地制作所需的零件(模块)会比
较自然,即先实现与门和或门,然后实现半加器和全加器,接着实现算数逻
辑单元(ALU),然后实现 CPU。因此,通过感知机表示计算机时,使用叠
加了多层的构造来实现是比较自然的流程。
本书中不会实际来实现计算机,但是希望读者能够记住,感知机通过叠
加层能够进行非线性的表示,理论上还可以表示计算机进行的处理。
2.7 小结
本章我们学习了感知机。感知机是一种非常简单的算法,大家应该很快
就能理解它的构造。感知机是下一章要学习的神经网络的基础,因此本章的
内容非常重要。
本章所学的内容
• 感知机是具有输入和输出的算法。给定一个输入后,将输出一个既
定的值。
• 感知机将权重和偏置设定为参数。
• 使用感知机可以表示与门和或门等逻辑电路。
• 异或门无法通过单层感知机来表示。
• 使用 2 层感知机可以表示异或门。
• 单层感知机只能表示线性空间,而多层感知机可以表示非线性空间。
• 多层感知机(在理论上)可以表示计算机。
上一章我们学习了感知机。关于感知机,既有好消息,也有坏消息。好
消息是,即便对于复杂的函数,感知机也隐含着能够表示它的可能性。上一
章已经介绍过,即便是计算机进行的复杂处理,感知机(理论上)也可以将
其表示出来。坏消息是,设定权重的工作,即确定合适的、能符合预期的输
入与输出的权重,现在还是由人工进行的。上一章中,我们结合与门、或门
的真值表人工决定了合适的权重。
神经网络的出现就是为了解决刚才的坏消息。具体地讲,神经网络的一
个重要性质是它可以自动地从数据中学习到合适的权重参数。本章中,我们
会先介绍神经网络的概要,然后重点关注神经网络进行识别时的处理。在下
一章中,我们将了解如何从数据中学习权重参数。
3.1 从感知机到神经网络
神经网络和上一章介绍的感知机有很多共同点。这里,我们主要以两者
的差异为中心,来介绍神经网络的结构。
3.1.1 神经网络的例子
也称为隐藏层。“隐藏”一词的意思是,隐藏层的神经元(和输入层、输出
层不同)肉眼看不见。另外,本书中把输入层到输出层依次称为第 0 层、第
1 层、第 2 层(层号之所以从 0 开始,是为了方便后面基于 Python 进行实现)。
图 3-1 中,第 0 层对应输入层,第 1 层对应中间层,第 2 层对应输出层。
中间层
输入层 输出层
图 3-1 神经网络的例子
3.1.2 复习感知机
在观察神经网络中信号的传递方法之前,我们先复习一下感知机。现在
3.1 从感知机到神经网络 39
x1
w1
w2
x2
图 3-2 复习感知机
(3.1)
b 是被称为偏置的参数,用于控制神经元被激活的容易程度;而 w1 和 w2
是表示各个信号的权重的参数,用于控制各个信号的重要性。
顺便提一下,在图 3-2 的网络中,偏置 b 并没有被画出来。如果要明确
地表示出 b,可以像图 3-3 那样做。图 3-3 中添加了权重为 b 的输入信号 1。这
个感知机将 x1、x2、1 三个信号作为神经元的输入,将其和各自的权重相乘后,
传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。如果
这个总和超过 0,则输出 1,否则输出 0。另外,由于偏置的输入信号一直是 1,
所以为了区别于其他神经元,我们在图中把这个神经元整个涂成灰色。
现在将式(3.1)改写成更加简洁的形式。为了简化式(3.1),我们用一个
函数来表示这种分情况的动作(超过 0 则输出 1,否则输出 0)。引入新函数
h(x),将式(3.1)改写成下面的式(3.2)和式(3.3)。
1
b
x1 w1
y
w2
x2
图 3-3 明确表示出偏置
(3.3)
3.1.3 激活函数登场
刚才登场的 h(x)函数会将输入信号的总和转换为输出信号,这种函数
一般称为激活函数(activation function)。如“激活”一词所示,激活函数的
作用在于决定如何来激活输入信号的总和。
现在来进一步改写式(3.2)。式(3.2)分两个阶段进行处理,先计算输入
信号的加权总和,然后用激活函数转换这一总和。因此,如果将式(3.2)写
得详细一点,则可以分成下面两个式子。
y = h(a) (3.5)
首先,式(3.4)计算加权输入信号和偏置的总和,记为 a。然后,式(3.5)
用 h() 函数将 a 转换为输出 y。
3.1 从感知机到神经网络 41
之前的神经元都是用一个○表示的,如果要在图中明确表示出式(3.4)
和式(3.5),则可以像图 3-4 这样做。
1
b
w1 h()
x1 a y
w2
x2
图 3-4 明确显示激活函数的计算过程
如图 3-4 所示,表示神经元的○中明确显示了激活函数的计算过程,即
信号的加权总和为节点 a,然后节点 a 被激活函数 h() 转换成节点 y。本书中,
“神
经元”和“节点”两个术语的含义相同。这里,我们称 a 和 y 为“节点”,其实
它和之前所说的“神经元”含义相同。
通常如图 3-5 的左图所示,神经元用一个○表示。本书中,在可以明确
神经网络的动作的情况下,将在图中明确显示激活函数的计算过程,如图 3-5
的右图所示。
h()
a y
图 3-5 左图是一般的神经元的图,右图是在神经元内部明确显示激活函数的计算过程的图(a
表示输入信号的总和,h() 表示激活函数,y 表示输出)
42 第 3 章 神经网络
下面,我们将仔细介绍激活函数。激活函数是连接感知机和神经网络的
桥梁。A
本书在使用“感知机”一词时,没有严格统一它所指的算法。一
般而言,“朴素感知机”是指单层网络,指的是激活函数使用了阶
跃函数 A 的模型。“多层感知机”是指神经网络,即使用 sigmoid
函数(后述)等平滑的激活函数的多层网络。
3.2 激活函数
式(3.3)表示的激活函数以阈值为界,一旦输入超过阈值,就切换输出。
这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为
激活函数。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。
那么,如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如
果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。下
面我们就来介绍一下神经网络使用的激活函数。
3.2.1 sigmoid 函数
神经网络中经常使用的一个激活函数就是式(3.6)表示的 sigmoid 函数
(sigmoid function)。
(3.6)
A 阶跃函数是指一旦输入超过阈值,就切换输出的函数。
3.2 激活函数 43
信号被传送给下一个神经元。实际上,上一章介绍的感知机和接下来要介绍
的神经网络的主要区别就在于这个激活函数。其他方面,比如神经元的多层
连接的构造、信号的传递方法等,基本上和感知机是一样的。下面,让我们
通过和阶跃函数的比较来详细学习作为激活函数的 sigmoid 函数。
3.2.2 阶跃函数的实现
def step_function(x):
if x > 0:
return 1
else:
return 0
这个实现简单、易于理解,但是参数 x 只能接受实数(浮点数)。也就是
说,允许形如 step_function(3.0) 的调用,但不允许参数取 NumPy 数组,例
如 step_function(np.array([1.0, 2.0]))。为了便于后面的操作,我们把它修
def step_function(x):
y = x > 0
return y.astype(np.int)
>>> y
array([False, True, True], dtype=bool)
对 NumPy 数组进行不等号运算后,数组的各个元素都会进行不等号运算,
生成一个布尔型数组。这里,数组 x 中大于 0 的元素被转换为 True,小于等
于 0 的元素被转换为 False,从而生成一个新的数组 y。
数组 y 是一个布尔型数组,但是我们想要的阶跃函数是会输出 int 型的 0
或 1 的函数。因此,需要把数组 y 的元素类型从布尔型转换为 int 型。
>>> y = y.astype(np.int)
>>> y
array([0, 1, 1])
3.2.3 阶跃函数的图形
下面我们就用图来表示上面定义的阶跃函数,为此需要使用 matplotlib 库。
import numpy as np
import matplotlib.pylab as plt
def step_function(x):
return np.array(x > 0, dtype=np.int)
1.0
0.8
0.6
0.4
0.2
0.0
−6 −4 −2 0 2 4 6
图 3-6 阶跃函数的图形
3.2.4 sigmoid 函数的实现
def sigmoid(x):
return 1 / (1 + np.exp(-x))
这里,np.exp(-x) 对应 exp(−x)。这个实现没有什么特别难的地方,但
是要注意参数 x 为 NumPy 数组时,结果也能被正确计算。实际上,如果在
这个 sigmoid 函数中输入一个 NumPy 数组,则结果如下所示。
NumPy 数组的各个元素间进行。
下面我们把 sigmoid 函数画在图上。画图的代码和刚才的阶跃函数的代
码几乎是一样的,唯一不同的地方是把输出 y 的函数换成了 sigmoid 函数。
运行上面的代码,可以得到图 3-7。
3.2.5 sigmoid 函数和阶跃函数的比较
1.0
0.8
0.6
0.4
0.2
0.0
−6 −4 −2 0 2 4 6
图 3-7 sigmoid 函数的图形
1.0
0.8
0.6
0.4
0.2
0.0
−6 −4 −2 0 2 4 6
3.2.6 非线性函数
在介绍激活函数时,经常会看到“非线性函数”和“线性函数”等术语。
函数本来是输入某个值后会返回一个值的转换器。向这个转换器输
入某个值后,输出值是输入值的常数倍的函数称为线性函数(用数学
式表示为 h(x) = cx。c 为常数)
。因此,线性函数是一条笔直的直线。
而非线性函数,顾名思义,指的是不像线性函数那样呈现出一条直
线的函数。
A 竹筒敲石是日本的一种庭院设施。支点架起竹筒,一端下方置石,另一端切口上翘。在切口上滴水,
水积多后该端下垂,水流出,另一端翘起,之后又因重力而落下,击石发出响声。——译者注
3.2 激活函数 49
神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使
用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神
经网络的层数就没有意义了。
线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无
隐 藏 层 的 神 经 网 络”。为 了 具 体 地(稍 微 直 观 地)理 解 这 一 点,我 们 来 思
考 下 面 这 个 简 单 的 例 子。这 里 我 们 考 虑 把 线 性 函 数 h(x) = cx 作 为 激 活
函 数,把 y(x) = h(h(h(x))) 的 运 算 对 应 3 层 神 经 网 络 A。这 个 运 算 会 进 行
y(x) = c × c × c × x 的乘法运算,但是同样的处理可以由 y(x) = ax(注意,
a = c 3)这一次乘法运算(即没有隐藏层的神经网络)来表示。如本例所示,
使用线性函数时,无法发挥多层网络带来的优势。因此,为了发挥叠加层所
带来的优势,激活函数必须使用非线性函数。
3.2.7 ReLU 函数
(3.7)
def relu(x):
return np.maximum(0, x)
A 该对应只是一个近似,实际的神经网络运算比这个例子要复杂,但不影响后面的结论成立。
——译者注
−1
−6 −4 −2 0 2 4 6
图 3-9 ReLU 函数
3.3 多维数组的运算
3.3.1 多维数组
简单地讲,多维数组就是“数字的集合”,数字排成一列的集合、排成
长方形的集合、排成三维状或者(更加一般化的)N 维状的集合都称为多维数
组。下面我们就用 NumPy 来生成多维数组,先从前面介绍过的一维数组开始。
3.3 多维数组的运算 51
3.3.2 矩阵乘法
下面,我们来介绍矩阵(二维数组)的乘积。比如 2 × 2 的矩阵,其乘积
可以像图 3-11 这样进行计算(按图中顺序进行计算是规定好了的)。
1 2 行
3 4
5 6
图 3-10 横向排列称为行,纵向排列称为列
1×5+2×7
1 2 5 6 19 22
=
3 4 7 8 43 50
A B
3×5+4×7
图 3-11 矩阵的乘积的计算方法
如本例所示,矩阵的乘积是通过左边矩阵的行(横向)和右边矩阵的列(纵
向)以对应元素的方式相乘后再求和而得到的。并且,运算的结果保存为新
的多维数组的元素。比如,A 的第 1 行和 B 的第 1 列的乘积结果是新数组的
第 1 行第 1 列的元素,A 的第 2 行和 B 的第 1 列的结果是新数组的第 2 行第 1
列的元素。另外,在本书的数学标记中,矩阵将用黑斜体表示(比如,矩阵
A),以区别于单个元素的标量(比如,a 或 b)。这个运算在 Python 中可以用
如下代码实现。
>>> A.shape
(2, 2)
>>> B = np.array([[5,6], [7,8]])
>>> B.shape
(2, 2)
>>> np.dot(A, B)
array([[19, 22],
[43, 50]])
值可能不一样。和一般的运算(+ 或 * 等)不同,矩阵的乘积运算中,操作数(A、
B)的顺序不同,结果也会不同。
这里介绍的是计算 2 × 2 形状的矩阵的乘积的例子,其他形状的矩阵的
乘积也可以用相同的方法来计算。比如,2 × 3 的矩阵和 3 × 2 的矩阵的乘积
可按如下形式用 Python 来实现。
(2, 2)
>>> A.shape
(2, 3)
>>> np.dot(A, C)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: shapes (2,3) and (2,2) not aligned: 3 (dim 1) != 2 (dim 0)
A B = C
形状: 3×2 2×4 3×4
保持一致
图 3-12 在矩阵的乘积运算中,对应维度的元素个数要保持一致
A B = C
形状: 3×2 2 3
保持一致
3.3.3 神经网络的内积
1
y1 ( 1 3 5
2 4 6 )
2
x1 X W = Y
3
4
y2 2 2×3 3
x2 5 一致
6 y3
图 3-14 通过矩阵的乘积进行神经网络的运算
(2, 3)
>>> Y = np.dot(X, W)
>>> print(Y)
[ 5 11 17]
3.4 3 层神经网络的实现
x1 y1
x2 y2
3.4.1 符号确认
在介绍神经网络中的处理之前,我们先导入 、 等符号。这些符
号可能看上去有些复杂,不过因为只在本节使用,稍微读一下就跳过去也问
题不大。
本节的重点是神经网络的运算可以作为矩阵运算打包进行。因为
神经网络各层的运算是通过矩阵的乘法运算打包进行的(从宏观
视角来考虑),所以即便忘了(未记忆)具体的符号规则,也不影
响理解后面的内容。
第1层的权重
(1)
x1
w12
前一层的第2个神经元
后一层的第1个神经元
x2
图 3-16 权重的符号
58 第 3 章 神经网络
3.4.2 各层间信号传递的实现
x1 y1
x2 y2
图 3-17 从输入层到第 1 层的信号传递
图 3-17 中增加了表示偏置的神经元“1”。请注意,偏置的右下角的索引
号只有一个。这是因为前一层的偏置神经元(神经元“1”)只有一个 A 。
为了确认前面的内容,现在用数学式表示 。 通过加权信号和偏
置的和按如下方式进行计算。
(3.8)
A 任何前一层的偏置神经元“1”都只有一个。偏置权重的数量取决于后一层的神经元的数量(不包括
后一层的偏置神经元“1”)。——译者注
3.4 3 层神经网络的实现 59
此外,如果使用矩阵的乘法运算,则可以将第 1 层的加权和表示成下面
的式(3.9)。
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)
A1 = np.dot(X, W1) + B1
Z1 = sigmoid(A1)
1
1
h()
y1
x1
h()
y2
x2
h()
图 3-18 从输入层到第 1 层的信号传递
print(Z1.shape) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
1 1
y1
h()
x1
y2
h()
x2
def identity_function(x):
return x
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3) # 或者 Y = A3
1 1
σ()
y1
x1
σ()
y2
x2
图 3-20 从第 2 层到输出层的信号传递
输出层所用的激活函数,要根据求解问题的性质决定。一般地,回
归问题可以使用恒等函数,二元分类问题可以使用 sigmoid 函数,
多元分类问题可以使用 softmax 函数。关于输出层的激活函数,我
们将在下一节详细介绍。
3.4.3 代码实现小结
至此,我们已经介绍完了 3 层神经网络的实现。现在我们把之前的代码
实现全部整理一下。这里,我们按照神经网络的实现惯例,只把权重记为大
写字母 W1,其他的(偏置或中间结果等)都用小写字母表示。
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
3.5 输出层的设计 63
return network
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y) # [ 0.31682708 0.69627909]
3.5 输出层的设计
神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出
层的激活函数。一般而言,回归问题用恒等函数,分类问题用 softmax 函数。
64 第 3 章 神经网络
机器学习的问题大致可以分为分类问题和回归问题。分类问题是数
据属于哪一个类别的问题。比如,区分图像中的人是男性还是女性
的问题就是分类问题。而回归问题是根据某个输入预测一个(连续的)
数值的问题。比如,根据一个人的图像预测这个人的体重的问题就
是回归问题(类似“57.4kg”这样的预测)。
3.5.1 恒等函数和 softmax 函数
恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直
接输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出。
另外,将恒等函数的处理过程用之前的神经网络图来表示的话,则如图 3-21
所示。和前面介绍的隐藏层的激活函数一样,恒等函数进行的转换处理可以
用一根箭头来表示。
σ()
a1 y1
σ()
a2 y2
σ()
a3 y3
图 3-21 恒等函数
(3.10)
σ()
a1 y1
a2 y2
a3 y3
图 3-22 softmax 函数
这个 Python 实现是完全依照式(3.10)进行的,所以不需要特别的解释。
考虑到后面还要使用 softmax 函数,这里我们把它定义成如下的 Python 函数。
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
softmax 函数的实现可以像式(3.11)这样进行改进。
(3.11)
首先,式(3.11)在分子和分母上都乘上 C 这个任意的常数(因为同时对
分母和分子乘以相同的常数,所以计算结果不变)。然后,把这个 C 移动到
指数函数(exp)中,记为 log C。最后,把 log C 替换为另一个符号 C 。
式(3.11)说明,在进行 softmax 的指数函数的运算时,加上(或者减去)
某个常数并不会改变运算的结果。这里的 C 可以使用任何值,但是为了防
止溢出,一般会使用输入信号中的最大值。我们来看一个具体的例子。
3.5 输出层的设计 67
如该例所示,通过减去输入信号中的最大值(上例中的 c),我们发现原
本为 nan(not a number,不确定)的地方,现在被正确计算了。综上,我们
可以像下面这样实现 softmax 函数。
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
3.5.3 softmax 函数的特征
使用 softmax() 函数,可以按如下方式计算神经网络的输出。
求解机器学习问题的步骤可以分为“学习”A 和“推理”两个阶段。首
先,在学习阶段进行模型的学习 B,然后,在推理阶段,用学到的
模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省
略输出层的 softmax 函数。在输出层使用 softmax 函数是因为它和
神经网络的学习有关系(详细内容请参考下一章)。
3.5.4 输出层的神经元数量
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输
出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测
是图中的数字 0 到 9 中的哪一个的问题(10 类别分类问题),可以像图 3-23 这样,
将输出层的神经元设定为 10 个。
如图 3-23 所示,在这个例子中,输出层的神经元从上往下依次对应数字
0, 1, . . ., 9。此外,图中输出层的神经元的值用不同的灰度表示。这个例子
A “学习”也称为“训练”,为了强调算法从数据中学习模型,本书使用“学习”一词。——译者注
B 这里的“学习”是指使用训练数据、自动调整参数的过程,具体请参考第 4 章。——译者注
3.6 手写数字识别 69
中神经元 y2 颜色最深,输出的值最大。这表明这个神经网络预测的是 y2 对应
的类别,也就是“2”。
y0 = “0”
y1 = “1”
某种运算 y2 = “2”
y9 = “9”
输入层 输出层
图 3-23 输出层的神经元对应各个数字
3.6 手写数字识别
介绍完神经网络的结构之后,现在我们来试着解决实际问题。这里我们
来进行手写数字图像的分类。假设学习已经全部结束,我们使用学习到的参
数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向
传播(forward propagation)。
和求解机器学习问题的步骤(分成学习和推理两个阶段进行)一样,
使用神经网络解决问题时,也需要首先使用训练数据(学习数据)进
行权重参数的学习;进行推理时,使用刚才学习到的参数,对输入
数据进行分类。
70 第 3 章 神经网络
3.6.1 MNIST 数据集
图 3-24 MNIST 图像数据集的例子
import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
from dataset.mnist import load_mnist
# 第一次调用会花费几分钟 ……
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True,
normalize=False)
# 输出各个数据的形状
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
print(img.shape) # (784,)
img = img.reshape(28, 28) # 把图像的形状变成原来的尺寸
print(img.shape) # (28, 28)
img_show(img)
这里需要注意的是,flatten=True 时读入的图像是以一列(一维)NumPy
数组的形式保存的。因此,显示图像时,需要把它变为原来的 28 像素 × 28
像素的形状。可以通过 reshape() 方法的参数指定期望的形状,更改 NumPy
数组的形状。此外,还需要把保存为 NumPy 数组的图像数据转换为 PIL 用
的数据对象,这个转换处理由 Image.fromarray() 来完成。
3.6 手写数字识别 73
图 3-25 显示 MNIST 图像
3.6.2 神经网络的推理处理
def get_data():
(x_train, t_train), (x_test, t_test) = \
load_mnist(normalize=True, flatten=True, one_hot_label=False)
return x_test, t_test
def init_network():
with open("sample_weight.pkl", 'rb') as f:
network = pickle.load(f)
return network
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = softmax(a3)
return y
权重参数 A。这个文件中以字典变量的形式保存了权重和偏置参数。剩余的 2
个函数,和前面介绍的代码实现基本相同,无需再解释。现在,我们用这 3
个函数来实现神经网络的推理处理。然后,评价它的识别精度(accuracy),
即能在多大程度上正确分类。
x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
y = predict(network, x[i])
p = np.argmax(y) # 获取概率最高的元素的索引
if p == t[i]:
accuracy_cnt += 1
A 因为之前我们假设学习已经完成,所以学习到的参数被保存下来。假设保存在 sample_weight.pkl
文件中,在推理阶段,我们直接加载这些已经学习到的参数。——译者注
执行上面的代码后,会显示“Accuracy:0.9352”。这表示有 93.52 % 的数
据被正确分类了。目前我们的目标是运行学习到的神经网络,所以不讨论识
别精度本身,不过以后我们会花精力在神经网络的结构和学习方法上,思考
如何进一步提高这个精度。实际上,我们打算把精度提高到 99 % 以上。
另外,在这个例子中,我们把 load_mnist 函数的参数 normalize 设置成了
True。将 normalize 设置成 True 后,函数内部会进行转换,将图像的各个像
预处理在神经网络(深度学习)中非常实用,其有效性已在提高识别
性能和学习的效率等众多实验中得到证明。在刚才的例子中,作为
一种预处理,我们将各个像素值除以 255,进行了简单的正规化。
实际上,很多预处理都会考虑到数据的整体分布。比如,利用数据
整体的均值或标准差,移动数据,使数据整体以 0 为中心分布,或
者进行正规化,把数据的延展控制在一定范围内。除此之外,还有
将数据整体的分布形状均匀化的方法,即数据白化(whitening)等。
3.6.3 批处理
>>> x, _ = get_data()
>>> network = init_network()
>>> W1, W2, W3 = network['W1'], network['W2'], network['W3']
>>>
>>> x.shape
(10000, 784)
>>> x[0].shape
(784,)
>>> W1.shape
76 第 3 章 神经网络
(784, 50)
>>> W2.shape
(50, 100)
>>> W3.shape
(100, 10)
我们通过上述结果来确认一下多维数组的对应维度的元素个数是否一致
(省略了偏置)。用图表示的话,如图 3-26 所示。可以发现,多维数组的对应
维度的元素个数确实是一致的。此外,我们还可以确认最终的结果是输出了
元素个数为 10 的一维数组。
X W1 W2 W3 → Y
形状: 784 784 × 50 50 × 100 100 × 10 10
一致 一致 一致
图 3-26 数组形状的变化
X W1 W2 W3 → Y
形状:100 × 784 784 × 50 50 × 100 100 × 10 100 × 10
图 3-27 批处理中数组形状的变化
其推理结果,等等。
这种打包式的输入数据称为批(batch)。批有“捆”的意思,图像就如同
纸币一样扎成一捆。
批处理对计算机的运算大有利处,可以大幅缩短每张图像的处理时
间。那么为什么批处理可以缩短处理时间呢?这是因为大多数处理
数值计算的库都进行了能够高效处理大型数组运算的最优化。并且,
在神经网络的运算中,当数据传送成为瓶颈时,批处理可以减轻数
据总线的负荷(严格地讲,相对于数据读入,可以将更多的时间用在
计算上)。也就是说,批处理一次性计算大型数组要比分开逐步计算
各个小型数组速度更快。
下面我们进行基于批处理的代码实现。这里用粗体显示与之前的实现的
不同之处。
x, t = get_data()
network = init_network()
最后,我们比较一下以批为单位进行分类的结果和实际的答案。为此,
需要在 NumPy 数组之间使用比较运算符(==)生成由 True/False 构成的布尔
型数组,并计算 True 的个数。我们通过下面的例子进行确认。
至此,基于批处理的代码实现就介绍完了。使用批处理,可以实现高速
且高效的运算。下一章介绍神经网络的学习时,我们将把图像数据作为打包
的批数据进行学习,届时也将进行和这里的批处理一样的代码实现。
3.7 小结
本章介绍了神经网络的前向传播。本章介绍的神经网络和上一章的感知
机在信号的按层传递这一点上是相同的,但是,向下一个神经元发送信号时,
改变信号的激活函数有很大差异。神经网络中使用的是平滑变化的 sigmoid
函数,而感知机中使用的是信号急剧变化的阶跃函数。这个差异对于神经网
络的学习非常重要,我们将在下一章介绍。
本章所学的内容
本章的主题是神经网络的学习。这里所说的“学习”是指从训练数据中
自动获取最优权重参数的过程。本章中,为了使神经网络能进行学习,将导
入损失函数这一指标。而学习的目的就是以该损失函数为基准,找出能使它
的值达到最小的权重参数。为了找出尽可能小的损失函数的值,本章我们将
介绍利用了函数斜率的梯度法。
4.1 从数据中学习
神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指
可以由数据自动决定权重参数的值。这是非常了不起的事情!因为如果所有
的参数都需要人工决定的话,工作量就太大了。在第 2 章介绍的感知机的例
子中,我们对照着真值表,人工设定了参数的值,但是那时的参数只有 3 个。
而在实际的神经网络中,参数的数量成千上万,在层数更深的深度学习中,
参数的数量甚至可以上亿,想要人工决定这些参数的值是不可能的。本章将
介绍神经网络的学习,即利用数据决定参数值的方法,并用 Python 实现对
MNIST 手写数字数据集的学习。
对于线性可分问题,第 2 章的感知机是可以利用数据自动学习的。
根据“感知机收敛定理”,通过有限次数的学习,线性可分问题是可
解的。但是,非线性可分问题则无法通过(自动)学习来解决。
82 第 4 章 神经网络的学习
4.1.1 数据驱动
数据是机器学习的命根子。从数据中寻找答案、从数据中发现模式、根
据数据讲故事……这些机器学习所做的事情,如果没有数据的话,就无从谈
起。因此,数据是机器学习的核心。这种数据驱动的方法,也可以说脱离了
过往以人为中心的方法。
通常要解决某个问题,特别是需要发现某种模式时,人们一般会综合考
虑各种因素后再给出回答。“这个问题好像有这样的规律性?”
“不对,可能
原因在别的地方。”——类似这样,人们以自己的经验和直觉为线索,通过反
复试验推进工作。而机器学习的方法则极力避免人为介入,尝试从收集到的
数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能
避免人为介入。
现在我们来思考一个具体的问题,比如如何实现数字“5”的识别。数字
5 是图 4-1 所示的手写图像,我们的目标是实现能区别是否是 5 的程序。这个
问题看起来很简单,大家能想到什么样的算法呢?
图 4-1 手写数字 5 的例子:写法因人而异,五花八门
如果让我们自己来设计一个能将 5 正确分类的程序,就会意外地发现这
是一个很难的问题。人可以简单地识别出 5,但却很难明确说出是基于何种
规律而识别出了 5。此外,从图 4-1 中也可以看到,每个人都有不同的写字习惯,
要发现其中的规律是一件非常难的工作。
因此,与其绞尽脑汁,从零开始想出一个可以识别 5 的算法,不如考虑
通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量,
4.1 从数据中学习 83
再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以
从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图
像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括
SIFT、SURF 和 HOG 等。使用这些特征量将图像数据转换为向量,然后对
转换后的向量使用机器学习中的 SVM、KNN 等分类器进行学习。
机器学习的方法中,由机器从收集到的数据中找出规律性。与从零开始
想出算法相比,这种方法可以更高效地解决问题,也能减轻人的负担。但是
需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。对于不
同的问题,必须使用合适的特征量(必须设计专门的特征量),才能得到好的
结果。比如,为了区分狗的脸部,人们需要考虑与用于识别 5 的特征量不同
的其他特征量。也就是说,即使使用特征量和机器学习的方法,也需要针对
不同的问题人工考虑合适的特征量。
到这里,我们介绍了两种针对机器学习任务的方法。将这两种方法用图
来表示,如图 4-2 所示。图中还展示了神经网络(深度学习)的方法,可以看
出该方法不存在人为介入。
如图 4-2 所示,神经网络直接学习图像本身。在第 2 个方法,即利用特
征量和机器学习的方法中,特征量仍是由人工设计的,而在神经网络中,连
图像中包含的重要特征量也都是由机器来学习的。
人想到的算法 答案
人想到的特征量 机器学习
答案
(SIFT、HOG等) (SVM、KNN等)
神经网络
答案
(深度学习)
图 4-2 从人工设计规则转变为由机器从数据中学习:没有人为介入的方块用灰色表示
深 度 学 习 有 时 也 称 为 端 到 端 机 器 学 习(end-to-end machine
learning)。这里所说的端到端是指从一端到另一端的意思,也就是
从原始数据(输入)中获得目标结果(输出)的意思。
神经网络的优点是对所有的问题都可以用同样的流程来解决。比如,不
管要求解的问题是识别 5,还是识别狗,抑或是识别人脸,神经网络都是通
过不断地学习所提供的数据,尝试发现待求解的问题的模式。也就是说,与
待处理的问题无关,神经网络可以将数据直接作为原始数据,进行“端对端”
的学习。
4.1.2 训练数据和测试数据
本章主要介绍神经网络的学习,不过在这之前,我们先来介绍一下机器
学习中有关数据处理的一些注意事项。
机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和
实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试
数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测
试数据呢?因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能
力,就必须划分训练数据和测试数据。另外,训练数据也可以称为监督数据。
泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的
能力。获得泛化能力是机器学习的最终目标。比如,在识别手写数字的问题
中,泛化能力可能会被用在自动读取明信片的邮政编码的系统上。此时,手
写数字识别就必须具备较高的识别“某个人”写的字的能力。注意这里不是“特
定的某个人写的特定的文字”,而是“任意一个人写的任意文字”。如果系统
只能正确识别已有的训练数据,那有可能是只学习到了训练数据中的个人的
习惯写法。
因此,仅仅用一个数据集去学习和评价参数,是无法进行正确评价的。
这样会导致可以顺利地处理某个数据集,但无法处理其他数据集的情况。顺
便说一下,只对某个数据集过度拟合的状态称为过拟合(over fitting)。避免
过拟合也是机器学习的一个重要课题。
4.2 损失函数 85
4.2 损失函数
如果有人问你现在有多幸福,你会如何回答呢?一般的人可能会给出诸
如“还可以吧”或者“不是那么幸福”等笼统的回答。如果有人回答“我现在
的幸福指数是 10.23”的话,可能会把人吓一跳吧。因为他用一个数值指标来
评判自己的幸福程度。
这里的幸福指数只是打个比方,实际上神经网络的学习也在做同样的事
情。神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基
准,寻找最优权重参数。和刚刚那位以幸福指数为指引寻找“最优人生”的
人一样,神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中
所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,
但一般用均方误差和交叉熵误差等。
损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的
神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。
以“性能的恶劣程度”为指标可能会使人感到不太自然,但是如
果给损失函数乘上一个负值,就可以解释为“在多大程度上不坏”,
即“性能有多好”。并且,“使性能的恶劣程度达到最小”和“使性
能的优良程度达到最大”是等价的,不管是用“恶劣程度”还是“优
良程度”,做的事情本质上都是一样的。
4.2.1 均方误差
可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squared
error)。均方误差如下式所示。
(4.1)
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> 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 表示。
如式(4.1)所示,均方误差会计算神经网络的输出和正确解监督数据的
各个元素之差的平方,再求总和。现在,我们用 Python 来实现这个均方误差,
实现方式如下所示。
这里举了两个例子。第一个例子中,正确解是“2”
,神经网络的输出的最大
值是“2”;第二个例子中,正确解是“2”
,神经网络的输出的最大值是“7”
。如
实验结果所示,我们发现第一个例子的损失函数的值更小,和监督数据之间的
误差较小。也就是说,均方误差显示第一个例子的输出结果与监督数据更加吻合。
4.2.2 交叉熵误差
(4.2)
进行一些简单的计算。
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
0.51082545709933802
>>>
>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
2.3025840929945458
第一个例子中,正确解标签对应的输出为 0.6,此时的交叉熵误差大约
为 0.51。第二个例子中,正确解标签对应的输出为 0.1 的低值,此时的交叉
熵误差大约为 2.3。由此可以看出,这些结果与我们前面讨论的内容是一致的。
4.2.3 mini-batch 学习
机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,
就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,
计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据
有 100 个的话,我们就要把这 100 个损失函数的总和作为学习的指标。
前面介绍的损失函数的例子中考虑的都是针对单个数据的损失函数。如
4.2 损失函数 89
果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面
的式(4.3)。
(4.3)
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
那么,如何从这个训练数据中随机抽取 10 笔数据呢?我们可以使用
NumPy 的 np.random.choice(),写成如下形式。
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
使用 np.random.choice() 可以从指定的数字中随机选择想要的数字。比如,
np.random.choice(60000, 10) 会从 0 到 59999 之间随机选择 10 个数字。如下
面的实际代码所示,我们可以得到一个包含被选数据的索引的数组。
之后,我们只需指定这些随机选出的索引,取出 mini-batch,然后使用
这个 mini-batch 计算损失函数即可。
计算电视收视率时,并不会统计所有家庭的电视机,而是仅以那些
被选中的家庭为统计对象。比如,通过从关东地区随机选择 1000 个
家庭计算收视率,可以近似地求得关东地区整体的收视率。这 1000
个家庭的收视率,虽然严格上不等于整体的收视率,但可以作为整
体的一个近似值。和收视率一样,mini-batch 的损失函数也是利用
一部分样本数据来近似地计算整体。也就是说,用随机选择的小批
量数据(mini-batch)作为全体训练数据的近似值。
4.2 损失函数 91
4.2.4 mini-batch 版交叉熵误差的实现
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
t] 能抽出各个数据的正确解标签对应的神经网络的输出(在这个例子中,
y[3,9], y[4,4]])。
4.2.5 为何要设定损失函数
上面我们讨论了损失函数,可能有人要问:
“为什么要导入损失函数呢?”
以数字识别任务为例,我们想获得的是能提高识别精度的参数,特意再导入
一个损失函数不是有些重复劳动吗?也就是说,既然我们的目标是获得使识
别精度尽可能高的神经网络,那不是应该把识别精度作为指标吗?
对于这一疑问,我们可以根据“导数”在神经网络学习中的作用来回答。
下一节中会详细说到,在神经网络的学习中,寻找最优参数(权重和偏置)时,
要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小
的地方,需要计算参数的导数(确切地讲是梯度),然后以这个导数为指引,
逐步更新参数的值。
假设有一个神经网络,现在我们来关注这个神经网络中的某一个权重参
数。此时,对该权重参数的损失函数求导,表示的是“如果稍微改变这个权
重参数的值,损失函数的值会如何变化”。如果导数的值为负,通过使该权
重参数向正方向改变,可以减小损失函数的值;反过来,如果导数的值为正,
则通过使该权重参数向负方向改变,可以减小损失函数的值。不过,当导数
的值为 0 时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此
时该权重参数的更新会停在此处。
之所以不能用识别精度作为指标,是因为这样一来绝大多数地方的导数
都会变为 0,导致参数无法更新。话说得有点多了,我们来总结一下上面的内容。
在进行神经网络的学习时,不能将识别精度作为指标。因为如果以
识别精度为指标,则参数的导数在绝大多数地方都会变为 0。
为什么用识别精度作为指标时,参数的导数在绝大多数地方都会变成 0
呢?为了回答这个问题,我们来思考另一个具体例子。假设某个神经网络正
确识别出了 100 笔训练数据中的 32 笔,此时识别精度为 32 %。如果以识别精
度为指标,即使稍微改变权重参数的值,识别精度也仍将保持在 32 %,不会
出现变化。也就是说,仅仅微调参数,是无法改善识别精度的。即便识别精
度有所改善,它的值也不会像 32.0123 . . . % 这样连续变化,而是变为 33 %、
34 % 这样的不连续的、离散的值。而如果把损失函数作为指标,则当前损
失函数的值可以表示为 0.92543 . . . 这样的值。并且,如果稍微改变一下参数
的值,对应的损失函数也会像 0.93432 . . . 这样发生连续性的变化。
识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值
也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出
于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。
如图 4-4 所示,阶跃函数的导数在绝大多数地方(除了 0 以外的地方)均为 0。
也就是说,如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微
小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化。
阶跃函数就像“竹筒敲石”一样,只在某个瞬间产生变化。而 sigmoid 函数,
如图 4-4 所示,不仅函数的输出(竖轴的值)是连续变化的,曲线的斜率(导数)
也是连续变化的。也就是说,sigmoid 函数的导数在任何地方都不为 0。这对
神经网络的学习非常重要。得益于这个斜率不会为 0 的性质,神经网络的学
习得以正确进行。
阶跃函数 sigmoid函数
4.3 数值微分
梯度法使用梯度的信息决定前进的方向。本节将介绍梯度是什么、有什
么性质等内容。在这之前,我们先来介绍一下导数。
4.3.1 导数
(4.4)
式(4.4)表示的是函数的导数。左边的符号 表示 f(x)关于 x 的导
数,即 f(x)相对于 x 的变化程度。式(4.4)表示的导数的含义是,x 的“微小
变化”将导致函数 f(x)的值在多大程度上发生变化。其中,表示微小变化的
h 无限趋近 0,表示为 。
接下来,我们参考式(4.4),来实现求函数的导数的程序。如果直接实
现式(4.4)的话,向 h 中赋入一个微小值,就可以计算出来了。比如,下面
的实现如何?
# 不好的实现示例
def numerical_diff(f, x):
h = 10e-50
return (f(x+h) - f(x)) / h
A
函数 numerical_diff(f, x) 的名称来源于数值微分 的英文 numerical
differentiation。这个函数有两个参数,即“函数 f”和“传给函数 f 的参数 x”。
乍一看这个实现没有问题,但是实际上这段代码有两处需要改进的地方。
在上面的实现中,因为想把尽可能小的值赋给 h(可以话,想让 h 无限
接近 0),所以 h 使用了 10e-50 这个微小值。但是,这样反而产生了舍入误差
(rounding error)。所谓舍入误差,是指因省略小数的精细部分的数值(比如,
小数点后第 8 位以后的数值)而造成最终的计算结果上的误差。比如,在
Python 中,舍入误差可如下表示。
>>> np.float32(1e-50)
0.0
A 所谓数值微分就是用数值方法近似求解函数的导数的过程。——译者注
真的切线
近似切线
y = f(x)
x x+h
图 4-5 真的导数(真的切线)和数值微分(近似切线)的值不同
如上所示,利用微小的差分求导数的过程称为数值微分(numerical
differentiation)。而基于数学式的推导求导数的过程,则用“解析
性”
(analytic)一词,称为“解析性求解”或者“解析性求导”。比如,
y = x2 的导数,可以通过 解析性地求解出来。因此,当 x = 2 时,
y 的导数为 4。解析性求导得到的导数是不含误差的“真的导数”
。
4.3.2 数值微分的例子
现在我们试着用上述的数值微分对简单函数进行求导。先来看一个由下
式表示的 2 次函数。
用 Python 来实现式(4.5),如下所示。
def function_1(x):
return 0.01*x**2 + 0.1*x
接下来,我们来绘制这个函数的图像。画图所用的代码如下,生成的图
像如图 4-6 所示。
import numpy as np
import matplotlib.pylab as plt
我们来计算一下这个函数在 x = 5 和 x = 10 处的导数。
98 第 4 章 神经网络的学习
>>> numerical_diff(function_1, 5)
0.1999999999990898
>>> numerical_diff(function_1, 10)
0.2999999999986347
f(x )
4.3.3 偏导数
(4.6)
def function_2(x):
4.3 数值微分 99
f(x)
x1 3
2
1
0
−1
−2 x0
−3
图 4-8 的图像
现在我们来求式(4.6)的导数。这里需要注意的是,式(4.6)有两个变量,
所以有必要区分对哪个变量求导数,即对 x0 和 x1 两个变量中的哪一个求导数。
另外,我们把这里讨论的有多个变量的函数的导数称为偏导数。用数学式表
示的话,可以写成 、 。
怎么求偏导数呢?我们先试着解一下下面两个关于偏导数的问题。
100 第 4 章 神经网络的学习
在这些问题中,我们定义了一个只有一个变量的函数,并对这个函数进
行了求导。例如,问题 1 中,我们定义了一个固定 x1 = 4 的新函数,然后对
只有变量 x0 的函数应用了求数值微分的函数。从上面的计算结果可知,问题
1 的答案是 6.00000000000378,问题 2 的答案是 7.999999999999119,和解析
解的导数基本一致。
像这样,偏导数和单变量的导数一样,都是求某个地方的斜率。不过,
偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为
某个值。在上例的代码中,为了将目标变量以外的变量固定到某些特定的值
上,我们定义了新函数。然后,对新定义的函数应用了之前的求数值微分的
函数,得到偏导数。
4.4 梯度
在刚才的例子中,我们按变量分别计算了 x0 和 x1 的偏导数。现在,我
们希望一起计算 x0 和 x1 的偏导数。比如,我们来考虑求 x0 = 3, x1 = 4 时 (x0, x1)
的偏导数 。另外,像 这样的由全部变量的偏导数汇总
而成的向量称为梯度(gradient)。梯度可以像下面这样来实现。
# f(x-h) 的计算
x[idx] = tmp_val - h
fxh2 = f(x)
return grad
函数 numerical_gradient(f, x) 的实现看上去有些复杂,但它执行的处
理和求单变量的数值微分基本没有区别。需要补充说明一下的是,np.zeros_
like(x) 会生成一个形状和 x 相同、所有元素都为 0 的数组。
图 4-9 的梯度
4.4.1 梯度法
机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必
须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数
取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我
们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值
(或者尽可能小的值)的方法就是梯度法。
这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,
无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际
上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。
函数的极小值、最小值以及被称为鞍点(saddle point)的地方,
梯度为 0。极小值是局部最小值,也就是限定在某个范围内的最
小值。鞍点是从某个方向上看是极大值,从另一个方向上看则是
极小值的点。虽然梯度法是要寻找梯度为 0 的地方,但是那个地
方不一定就是最小值(也有可能是极小值或者鞍点)。此外,当函
数很复杂且呈扁平状时,学习可能会进入一个(几乎)平坦的地区,
陷入被称为“学习高原”的无法前进的停滞期。
虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地
减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的
任务中,要以梯度的信息为线索,决定前进的方向。
此时梯度法就派上用场了。在梯度法中,函数的取值从当前位置沿着梯
度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,
如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,
逐渐减小函数值的过程就是梯度法(gradient method)。梯度法是解决机器
学习中最优化问题的常用方法,特别是在神经网络的学习中经常被使用。
根据目的是寻找最小值还是最大值,梯度法的叫法有所不同。严格地讲,
寻找最小值的梯度法称为梯度下降法(gradient descent method),
寻找最大值的梯度法称为梯度上升法(gradient ascent method)。但
是通过反转损失函数的符号,求最小值的问题和求最大值的问题会
变成相同的问题,因此“下降”还是“上升”的差异本质上并不重要。
一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法。
104 第 4 章 神经网络的学习
现在,我们尝试用数学式来表示梯度法,如式(4.7)所示。
(4.7)
式(4.7)的 η 表示更新量,在神经网络的学习中,称为学习率(learning
rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
式(4.7)是表示更新一次的式子,这个步骤会反复执行。也就是说,每
一步都按式(4.7)更新变量的值,通过反复执行此步骤,逐渐减小函数值。
虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也
可以通过类似的式子(各个变量的偏导数)进行更新。
学习率需要事先确定为某个值,比如 0.01 或 0.001。一般而言,这个值
过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会
一边改变学习率的值,一边确认学习是否正确进行了。
下面,我们用 Python 来实现梯度下降法。如下所示,这个实现很简单。
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
问题:请用梯度法求 的最小值。
所以说通过梯度法我们基本得到了正确结果。如果用图来表示梯度法的更新
过程,则如图 4-10 所示。可以发现,原点处是最低的地方,函数的取值一
点点在向其靠近。这个图的源代码在 ch04/gradient_method.py 中(但 ch04/
gradient_method.py 不显示表示等高线的虚线)。
图 4-10 的梯度法的更新过程:虚线是函数的等高线
106 第 4 章 神经网络的学习
前面说过,学习率过大或者过小都无法得到好的结果。我们来做个实验
验证一下。
# 学习率过大的例子:lr=10.0
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
array([ -2.58983747e+13, -1.29524862e+12])
# 学习率过小的例子:lr=1e-10
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
array([-2.99999994, 3.99999992])
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学
习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率
是一个很重要的问题。
像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重
和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练
数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。
一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利
进行的设定。
4.4.2 神经网络的梯度
神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参
数的梯度。比如,有一个只有一个形状为 2 × 3 的权重 W 的神经网络,损失
(4.8)
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient
class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3) # 用高斯分布进行初始化
return loss
接下来求梯度。和前面一样,我们使用 numerical_gradient(f, x) 求梯
求出神经网络的梯度后,接下来只需根据梯度法,更新权重参数即可。
在下一节中,我们会以 2 层神经网络为例,实现整个学习过程。
数组,所以改动并不大。这里省略了对代码的说明,想知道细节的
读者请参考源代码(common/gradient.py)。
4.5 学习算法的实现
关 于 神 经 网 络 学 习 的 基 础 知 识,到 这 里 就 全 部 介 绍 完 了。“损 失 函
数”
“mini-batch”
“梯度”
“梯度下降法”等关键词已经陆续登场,这里我们
来确认一下神经网络的学习步骤,顺便复习一下这些内容。神经网络的学习
步骤如下所示。
前提
神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的
过程称为“学习”。神经网络的学习分成下面 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 数据集进行学习。
4.5.1 2 层神经网络的类
import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient
class TwoLayerNet:
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
# x: 输入数据 , t: 监督数据
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
# x: 输入数据 , t: 监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
虽然这个类的实现稍微有点长,但是因为和上一章的神经网络的前向处
理的实现有许多共通之处,所以并没有太多新东西。我们先把这个类中用到
的变量和方法整理一下。表 4-1 中只罗列了重要的变量,表 4-2 中则罗列了所
有的方法。
表 4-1 TwolayerNet 类中使用的变量
变量 说明
params 保存神经网络的参数的字典型变量(实例变量)。
params['W1'] 是第 1 层的权重,params['b1'] 是第 1 层的偏置。
params['W2'] 是第 2 层的权重,params['b2'] 是第 2 层的偏置
表 4-2 TwoLayerNet 类的方法
方法 说明
如上所示,params 变量中保存了该神经网络所需的全部参数。并且,
params 变量中保存的权重参数会用在推理处理(前向处理)中。顺便说一下,
推理处理的实现如下所示。
accuracy(self, x, t) 的实现和上一章的神经网络的推理处理基本一样。如
果仍有不明白的地方,请再回顾一下上一章的内容。另外,loss(self, x, t)
据数值微分,计算各个参数相对于损失函数的梯度。另外,gradient(self, x, t)
是下一章要实现的方法,该方法使用误差反向传播法高效地计算梯度。
numerical_gradient(self, x, t) 基于数值微分计算参数的梯度。下
一章,我们会介绍一个高速计算梯度的方法,称为误差反向传播法。
用误差反向传播法求到的梯度和数值微分的结果基本一致,但可以
高速地进行处理。使用误差反向传播法计算梯度的 gradient(self,
x, t) 方法会在下一章实现,不过考虑到神经网络的学习比较花时间,
想节约学习时间的读者可以替换掉这里的 numerical_gradient(self,
x, t),抢先使用 gradient(self, x, t) !
4.5.2 mini-batch 的实现
neuralnet.py 中)。
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
train_loss_list = []
# 超参数
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
4.5 学习算法的实现 115
for i in range(iters_num):
# 获取 mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# grad = network.gradient(x_batch, t_batch) # 高速版 !
# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
# 记录学习过程
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
放大显示
观察图 4-11,可以发现随着学习的进行,损失函数的值在不断减小。这
是学习正常进行的信号,表示神经网络的权重参数在逐渐拟合数据。也就是
说,神经网络的确在学习!通过反复地向它浇灌(输入)数据,神经网络正
在逐渐向最优参数靠近。
4.5.3 基于测试数据的评价
为了正确进行评价,我们来稍稍修改一下前面的代码。与前面的代码不
同的地方,我们用粗体来表示。
A 实际上,一般做法是事先将所有训练数据随机打乱,然后按指定的批次大小,按序生成 mini-batch。
这样每个 mini-batch 均有一个索引号,比如此例可以是 0, 1, 2, . . . , 99,然后用索引号可以遍历所有
的 mini-batch。遍历一次所有数据,就称为一个 epoch。请注意,本节中的 mini-batch 每次都是随机
选择的,所以不一定每个数据都会被看到。——译者注
4.5 学习算法的实现 117
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
train_loss_list = []
train_acc_list = []
test_acc_list = []
# 平均每个 epoch 的重复次数
iter_per_epoch = max(train_size / batch_size, 1)
# 超参数
iters_num = 10000
batch_size = 100
learning_rate = 0.1
for i in range(iters_num):
# 获取 mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# grad = network.gradient(x_batch, t_batch) # 高速版 !
# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
在上面的例子中,每经过一个 epoch,就对所有的训练数据和测试数据
计算识别精度,并记录结果。之所以要计算每一个 epoch 的识别精度,是因
为如果在 for 语句的循环中一直计算识别精度,会花费太多时间。并且,也
没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推
移就可以了)。因此,我们才会每经过一个 epoch 就记录一次训练数据的识别
精度。
把从上面的代码中得到的结果用图表示的话,如图 4-12 所示。
图 4-12 训练数据和测试数据的识别精度的推移(横轴的单位是 epoch)
图 4-12 中,实线表示训练数据的识别精度,虚线表示测试数据的识别精
度。如图所示,随着 epoch 的前进(学习的进行),我们发现使用训练数据和
测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两
条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。
4.6 小结
本章中,我们介绍了神经网络的学习。首先,为了能顺利进行神经网络
的学习,我们导入了损失函数这个指标。以这个损失函数为基准,找出使它
4.6 小结 119
的值达到最小的权重参数,就是神经网络学习的目标。为了找到尽可能小的
损失函数值,我们介绍了使用函数斜率的梯度法。
本章所学的内容
• 机器学习中使用的数据集分为训练数据和测试数据。
• 神经网络用训练数据进行学习,并用测试数据评价学习到的模型的
泛化能力。
• 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数
的值减小。
• 利用某个给定的微小值的差分求导数的过程,称为数值微分。
• 利用数值微分,可以计算权重参数的梯度。
• 数值微分虽然费时间,但是实现起来很简单。下一章中要实现的稍
微复杂一些的误差反向传播法可以高速地计算梯度。
上一章中,我们介绍了神经网络的学习,并通过数值微分计算了神经网
络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度)。数值微
分虽然简单,也容易实现,但缺点是计算上比较费时间。本章我们将学习一
个能够高效计算权重参数的梯度的方法——误差反向传播法。
要正确理解误差反向传播法,我个人认为有两种方法:一种是基于数学式;
另一种是基于计算图(computational graph)。前者是比较常见的方法,机器
学习相关的图书中多数都是以数学式为中心展开论述的。因为这种方法严密
且简洁,所以确实非常合理,但如果一上来就围绕数学式进行探讨,会忽略
一些根本的东西,止步于式子的罗列。因此,本章希望大家通过计算图,直
观地理解误差反向传播法。然后,再结合实际的代码加深理解,相信大家一
定会有种“原来如此!”的感觉。
此外,通过计算图来理解误差反向传播法这个想法,参考了 Andrej
Karpathy 的博客 [4] 和他与 Fei-Fei Li 教授负责的斯坦福大学的深度学习课程
CS231n [5]。
5.1 计算图
计算图将计算过程用图形表示出来。这里说的图形是数据结构图,通
过多个节点和边表示(连接节点的直线称为“边”)。为了让大家熟悉计算图,
122 第 5 章 误差反向传播法
本节先用计算图解一些简单的问题。从这些简单的问题开始,逐步深入,最
终抵达误差反向传播法。
5.1.1 用计算图求解
现在,我们尝试用计算图解简单的问题。下面我们要看的几个问题都是
用心算就能解开的简单问题,这里的目的只是通过它们让大家熟悉计算图。
掌握了计算图的使用方法之后,在后面即将看到的复杂计算中它将发挥巨大
威力,所以本节请一定学会计算图的使用方法。
计算图通过节点和箭头表示计算过程。节点用○表示,○中是计算的内
容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右
传递。用计算图解问题 1,求解过程如图 5-1 所示。
图 5-1 基于计算图求解的问题 1 的答案
2
苹果的个数
1.1
消费税
图 5-2 基于计算图求解的问题 1 的答案:
“苹果的个数”和“消费税”作为变量标在○外面
再看下一题。
2
苹果的个数
100 200
×
650 715
+ ×
150 450
×
3
橘子的个数
1.1
消费税
图 5-3 基于计算图求解的问题 2 的答案
这个问题中新增了加法节点“+”,用来合计苹果和橘子的金额。构建了
计算图后,从左向右进行计算。就像电路中的电流流动一样,计算结果从左
向右传递。到达最右边的计算结果后,计算过程就结束了。从图 5-3 中可知,
问题 2 的答案为 715 日元。
124 第 5 章 误差反向传播法
综上,用计算图解题的情况下,需要按如下流程进行。
1. 构建计算图。
2. 在计算图上,从左向右进行计算。
这里的第 2 歩“从左向右进行计算”是一种正方向上的传播,简称为正
向传播(forward propagation)。正向传播是从计算图出发点到结束点的传播。
既然有正向传播这个名称,当然也可以考虑反向(从图上看的话,就是从右向左)
的传播。实际上,这种传播称为反向传播(backward propagation)。反向传
播将在接下来的导数计算中发挥重要作用。
5.1.2 局部计算
计算图的特征是可以通过传递“局部计算”获得最终结果。“局部”这个
词的意思是“与自己相关的某个小范围”。局部计算是指,无论全局发生了什么,
都能只根据与自己相关的信息输出接下来的结果。
我们用一个具体的例子来说明局部计算。比如,在超市买了 2 个苹果和
其他很多东西。此时,可以画出如图 5-4 所示的计算图。
其他很多东西
复杂的计算
…
4000
4200 4620
2 + ×
苹果的个数
100 200
×
1.1
消费税
图 5-4 买了 2 个苹果和其他很多东西的例子
如图 5-4 所示,假设(经过复杂的计算)购买的其他很多东西总共花费
4000 日元。这里的重点是,各个节点处的计算都是局部计算。这意味着,例
如苹果和其他很多东西的求和运算(4000 + 200 → 4200)并不关心 4000 这个
数字是如何计算而来的,只要把两个数字相加就可以了。换言之,各个节点
处只需进行与自己有关的计算(在这个例子中是对输入的两个数字进行加法
运算),不用考虑全局。
综上,计算图可以集中精力于局部计算。无论全局的计算有多么复杂,
各个步骤所要做的就是对象节点的局部计算。虽然局部计算非常简单,但是
通过传递它的计算结果,可以获得全局的复杂计算的结果。
比如,组装汽车是一个复杂的工作,通常需要进行“流水线”作业。
每个工人(机器)所承担的都是被简化了的工作,这个工作的成果会
传递给下一个工人,直至汽车组装完成。计算图将复杂的计算分割
成简单的局部计算,和流水线作业一样,将局部计算的结果传递给
下一个节点。在将复杂的计算分解成简单的计算这一点上与汽车的
组装有相似之处。
5.1.3 为何用计算图解题
前面我们用计算图解答了两个问题,那么计算图到底有什么优点呢?一
个优点就在于前面所说的局部计算。无论全局是多么复杂的计算,都可以通
过局部计算使各个节点致力于简单的计算,从而简化问题。另一个优点是,
利用计算图可以将中间的计算结果全部保存起来(比如,计算进行到 2 个苹
果时的金额是 200 日元、加上消费税之前的金额 650 日元等)。但是只有这些
理由可能还无法令人信服。实际上,使用计算图最大的原因是,可以通过反
向传播高效计算导数。
在介绍计算图的反向传播时,我们再来思考一下问题 1。问题 1 中,我
们计算了购买 2 个苹果时加上消费税最终需要支付的金额。这里,假设我们
想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金
额关于苹果的价格的导数”。设苹果的价格为 x,支付金额为 L,则相当于求
。这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。
126 第 5 章 误差反向传播法
如前所述,“支付金额关于苹果的价格的导数”的值可以通过计算图的
反向传播求出来。先来看一下结果,如图 5-5 所示,可以通过计算图的反向
传播求导数(关于如何进行反向传播,接下来马上会介绍)。
2
苹果的个数
1.1
消费税
图 5-5 基于反向传播的导数的传递
如图 5-5 所示,反向传播使用与正方向相反的箭头(粗线)表示。反向传
播传递“局部导数”,将导数的值写在箭头的下方。在这个例子中,反向传
播从右向左传递导数的值(1 → 1.1 → 2.2)。从这个结果中可知,“支付金额
关于苹果的价格的导数”的值是 2.2。这意味着,如果苹果的价格上涨 1 日元,
最终的支付金额会增加 2.2 日元(严格地讲,如果苹果的价格增加某个微小值,
则最终的支付金额将增加那个微小值的 2.2 倍)。
这里只求了关于苹果的价格的导数,不过“支付金额关于消费税的导
数”
“支付金额关于苹果的个数的导数”等也都可以用同样的方式算出来。并
且,计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以
高效地计算多个导数。综上,计算图的优点是,可以通过正向传播和反向传
播高效地计算各个变量的导数值。
5.2 链式法则
前面介绍的计算图的正向传播将计算结果正向(从左到右)传递,其计
算过程是我们日常接触的计算过程,所以感觉上可能比较自然。而反向传播
将局部导数向正方向的反方向(从右到左)传递,一开始可能会让人感到困惑。
传递这个局部导数的原理,是基于链式法则(chain rule)的。本节将介绍链
式法则,并阐明它是如何对应计算图上的反向传播的。
5.2.1 计算图的反向传播
话不多说,让我们先来看一个使用计算图的反向传播的例子。假设存在
y = f (x) 的计算,这个计算的反向传播如图 5-6 所示。
x y
f
E E
图 5-6 计算图的反向传播:沿着与正方向相反的方向,乘上局部导数
如图所示,反向传播的计算顺序是,将信号 E 乘以节点的局部导数
( ),然后将结果传递给下一个节点。这里所说的局部导数是指正向传播
中 y = f (x) 的导数,也就是 y 关于 x 的导数( )。比如,假设 y = f(x) = x2,
则局部导数为 = 2x。把这个局部导数乘以上游传过来的值(本例中为 E),
然后传递给前面的节点。
这就是反向传播的计算顺序。通过这样的计算,可以高效地求出导数的
值,这是反向传播的要点。那么这是如何实现的呢?我们可以从链式法则的
原理进行解释。下面我们就来介绍链式法则。
5.2.2 什么是链式法则
介绍链式法则时,我们需要先从复合函数说起。复合函数是由多个函数
构成的函数。比如,z = (x + y)2 是由式(5.1)所示的两个式子构成的。
z = t2
(5.1)
t=x + y
链式法则是关于复合函数的导数的性质,定义如下。
如果某个函数由复合函数表示,则该复合函数的导数可以用构成复
合函数的各个函数的导数的乘积表示。
这就是链式法则的原理,乍一看可能比较难理解,但实际上它是一个
非常简单的性质。以式(5.1)为例, (z 关于 x 的导数)可以用 (z 关于 t
的导数)和 (t 关于 x 的导数)的乘积表示。用数学式表示的话,可以写成
式(5.2)。
(5.2)
式(5.2)中的 ∂ t 正好可以像下面这样“互相抵消”,所以记起来很简单。
现在我们使用链式法则,试着求式(5.2)的导数 。为此,我们要先求
式(5.1)中的局部导数(偏导数)。
(5.3)
(5.4)
5.2 链式法则 129
5.2.3 链式法则和计算图
现在我们尝试将式(5.4)的链式法则的计算用计算图表示出来。如果用
“**2”节点表示平方运算的话,则计算图如图 5-7 所示。
t z
+ **2
图 5-7 式(5.4)的计算图:沿着与正方向相反的方向,乘上局部导数后传递
如图所示,计算图的反向传播从右到左传播信号。反向传播的计算顺序是,
先将节点的输入信号乘以节点的局部导数(偏导数),然后再传递给下一个节
点。比如,反向传播时,“**2”节点的输入是 ,将其乘以局部导数 (因
为正向传播时输入是 t、输出是 z,所以这个节点的局部导数是 ),然后传
递给下一个节点。另外,图 5-7 中反向传播最开始的信号 在前面的数学
式中没有出现,这是因为 ,所以在刚才的式子中被省略了。
图 5-7 中 需 要 注 意 的 是 最 左 边 的 反 向 传 播 的 结 果。根 据 链 式 法 则,
成立,对应“z 关于 x 的导数”。也就是说,反向传播
是基于链式法则的。
把 式(5.3)的 结 果 代 入 到 图 5-7 中,结 果 如 图 5-8 所 示, 的结果为
2(x + y)。
130 第 5 章 误差反向传播法
t z
+ **2
1
y
图 5-8 根据计算图的反向传播的结果, 等于 2(x + y)
5.3 反向传播
上一节介绍了计算图的反向传播是基于链式法则成立的。本节将以“+”
和“×”等运算为例,介绍反向传播的结构。
5.3.1 加法节点的反向传播
首先来考虑加法节点的反向传播。这里以 z = x + y 为对象,观察它的
反向传播。z = x + y 的导数可由下式(解析性地)计算出来。
(5.5)
z
+ +
图 5-9 加法节点的反向传播:左图是正向传播,右图是反向传播。如右图的反向传播所示,
加法节点的反向传播将上游的值原封不动地输出到下游
某种计算
z
+ 某种计算 L
某种计算
图 5-10 加法节点存在于某个最后输出的计算的一部分中。反向传播时,从最右边的输
出出发,局部导数从节点向节点反方向传播
132 第 5 章 误差反向传播法
现在来看一个加法的反向传播的具体例子。假设有“10 + 5=15”这一计
算,反向传播时,从上游会传来值 1.3。用计算图表示的话,如图 5-11 所示。
10
1.3
15 +
+
1.3
5
1.3
图 5-11 加法节点的反向传播的具体例子
因为加法节点的反向传播只是将输入信号输出到下一个节点,所以如图
5-11 所示,反向传播将 1.3 向下一个节点传递。
5.3.2 乘法节点的反向传播
接下来,我们看一下乘法节点的反向传播。这里我们考虑 z = xy。这个
式子的导数用式(5.6)表示。
(5.6)
z ×
×
图 5-12 乘法的反向传播:左图是正向传播,右图是反向传播
10
6.5
50 ×
×
1.3
5
13
图 5-13 乘法节点的反向传播的具体例子
因为乘法的反向传播会乘以输入信号的翻转值,所以各自可按 1.3 × 5 =
6.5、1.3 × 10 = 13 计算。另外,加法的反向传播只是将上游的值传给下游,
并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输
入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。
5.3.3 苹果的例子
再来思考一下本章最开始举的购买苹果的例子(2 个苹果和消费税)。这
里要解的问题是苹果的价格、苹果的个数、消费税这 3 个变量各自如何影响
最终支付的金额。这个问题相当于求“支付金额关于苹果的价格的导数”
“支
134 第 5 章 误差反向传播法
付金额关于苹果的个数的导数”
“支付金额关于消费税的导数”。用计算图的
反向传播来解的话,求解过程如图 5-14 所示。
2
苹果的个数
110
1.1
消费税
200
图 5-14 购买苹果的反向传播的例子
如前所述,乘法节点的反向传播会将输入信号翻转后传给下游。从图 5-14
的结果可知,苹果的价格的导数是 2.2,苹果的个数的导数是 110,消费税的
导数是 200。这可以解释为,如果消费税和苹果的价格增加相同的值,则消
费税将对最终价格产生 200 倍大小的影响,苹果的价格将产生 2.2 倍大小的影响。
不过,因为这个例子中消费税和苹果的价格的量纲不同,所以才形成了这样
的结果(消费税的 1 是 100%,苹果的价格的 1 是 1 日元)。
最后作为练习,请大家来试着解一下“购买苹果和橘子”的反向传播。
在图 5-15 中的方块中填入数字,求各个变量的导数(答案在若干页后)。
2
苹果的个数
100 200
×
650 715
+ ×
150 450
×
3
橘子的个数
1.1
消费税
图 5-15 购买苹果和橘子的反向传播的例子:在方块中填入数字,完成反向传播
5.4 简单层的实现 135
5.4 简单层的实现
这里也以层为单位来实现乘法节点和加法节点。
5.4.1 乘法层的实现
层的实现中有两个共通的方法(接口)forward() 和 backward()。forward()
对应正向传播,backward() 对应反向传播。
现在来实现乘法层。乘法层作为 MulLayer 类,其实现过程如下所示(源
代码在 ch05/layer_naive.py 中)。
class MulLayer:
def __init__(self):
self.x = None
self.y = None
return out
return dx, dy
136 第 5 章 误差反向传播法
来的导数(dout)乘以正向传播的翻转值,然后传给下游。
上面就是 MulLayer 的实现。现在我们使用 MulLayer 实现前面的购买苹果
的例子(2 个苹果和消费税)。上一节中我们使用计算图的正向传播和反向传播,
像图 5-16 这样进行了计算。
2
苹果的个数
110
1.1
消费税
200
图 5-16 购买 2 个苹果
apple = 100
apple_num = 2
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price) # 220
# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
5.4.2 加法层的实现
接下来,我们实现加法节点的加法层,如下所示。
class AddLayer:
def __init__(self):
pass
2
苹果的个数
110
200
100 ×
1.1 650 715
2.2
+ ×
150 1.1 1
450
×
3.3
1.1
3
橘子的个数
165 1.1
消费税
650
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num) #(1)
orange_price = mul_orange_layer.forward(orange, orange_num) #(2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price) #(3)
price = mul_tax_layer.forward(all_price, tax) #(4)
# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice) #(4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) #(3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price) #(2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price) #(1)
print(price) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
这个实现稍微有一点长,但是每一条命令都很简单。首先,生成必要的
层,以合适的顺序调用正向传播的 forward() 方法。然后,用与正向传播相
反的顺序调用反向传播的 backward() 方法,就可以求出想要的导数。
综上,计算图中层的实现(这里是加法层和乘法层)非常简单,使用这
些层可以进行复杂的导数计算。下面,我们来实现神经网络中使用的层。
5.5 激活函数层的实现
现在,我们将计算图的思路应用到神经网络中。这里,我们把构成神经
网络的层实现为一个类。先来实现激活函数的 ReLU 层和 Sigmoid 层。
5.5.1 ReLU 层
(5.7)
通过式(5.7),可以求出 y 关于 x 的导数,如式(5.8)所示。
(5.8)
在式(5.8)中,如果正向传播时的输入 x 大于 0,则反向传播会将上游的
值原封不动地传给下游。反过来,如果正向传播时的 x 小于等于 0,则反向
传播中传给下游的信号将停在此处。用计算图表示的话,如图 5-18 所示。
现在我们来实现 ReLU 层。在神经网络的层的实现中,一般假定 forward()
和 backward() 的参数是 NumPy 数组。另外,实现 ReLU 层的源代码在 common/
layers.py 中。
140 第 5 章 误差反向传播法
x>0时 x0时
x y x y
relu relu
图 5-18 ReLU 层的计算图
class Relu:
def __init__(self):
self.mask = None
return out
return dx
ReLU 层的作用就像电路中的开关一样。正向传播时,有电流通过
的话,就将开关设为 ON;没有电流通过的话,就将开关设为 OFF。
反向传播时,开关为 ON 的话,电流会直接通过;开关为 OFF 的话,
则不会有电流通过。
5.5.2 Sigmoid 层
(5.9)
1
x −x exp(−x) 1+exp(−x) 1+exp(−x)
× exp + /
−1 1
图 5-19 sigmoid 层的计算图(仅正向传播)
图 5-19 中,除了“×”和“+”节点外,还出现了新的“exp”和“/”节点。
“exp”节点会进行 y = exp(x) 的计算,“/”节点会进行 的计算。
如图 5-19 所示,式(5.9)的计算由局部计算的传播构成。下面我们就来
进行图 5-19 的计算图的反向传播。这里,作为总结,我们来依次看一下反向
传播的流程。
142 第 5 章 误差反向传播法
步骤 1
“/”节点表示 ,它的导数可以解析性地表示为下式。
(5.10)
根据式(5.10),反向传播时,会将上游的值乘以 −y2(正向传播的输出的
平方乘以 −1 后的值)后,再传给下游。计算图如下所示。
x −x exp(−x) 1+exp(−x) y
× exp + /
−1 1
步骤 2
“+”节点将上游的值原封不动地传给下游。计算图如下所示。
x −x exp(−x) 1+exp(−x) y
× exp + /
−1 1
步骤 3
“exp”节点表示 y = exp(x),它的导数由下式表示。
(5.11)
计算图中,上游的值乘以正向传播时的输出(这个例子中是 exp(−x))后,
再传给下游。
x −x exp(−x) 1+exp(−x) y
× exp + /
exp(−x)
−1 1
5.5 激活函数层的实现 143
步骤 4
“×”节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以 −1。
x −x exp(−x) 1+exp(−x) y
× exp + /
exp(−x) exp(−x)
−1 1
图 5-20 Sigmoid 层的计算图
x y
sigmoid
exp(−x)
图 5-21 Sigmoid 层的计算图(简洁版)
(5.12)
144 第 5 章 误差反向传播法
x y
sigmoid
(1 − y)
class Sigmoid:
def __init__(self):
self.out = None
return out
return dx
5.6.1 Affine 层
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘
积运算(NumPy 中是 np.dot(),具体请参照 3.3 节)。比如,还记得我们用
Python 进行了下面的实现吗?
5.6 Affine/Softmax 层的实现 145
>>> X = np.random.rand(2) # 输入
>>> W = np.random.rand(2,3) # 权重
>>> B = np.random.rand(3) # 偏置
>>>
>>> X.shape # (2,)
>>> W.shape # (2, 3)
>>> B.shape # (3,)
>>>
>>> Y = np.dot(X, W) + B
X · W = O
(3,)
(2,) (2, 3)
保持一致
图 5-23 矩阵的乘积运算中对应维度的元素个数要保持一致
神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿
射变换”A。因此,这里将进行仿射变换的处理实现为“Affine 层”。
现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。
将乘积运算用“dot”节点表示的话,则 np.dot(X, W) + B 的运算可用图 5-24
所示的计算图表示出来。另外,在各个变量的上方标记了它们的形状(比如,
计算图上显示了 X 的形状为 (2,),X·W 的形状为 (3,) 等)。
A 几何中,仿射变换包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算。
——译者注
146 第 5 章 误差反向传播法
(2,)
X
(3,) (3,)
X W Y
dot +
(2, 3)
W
(3,)
B
图 5-24 Affine 层的计算图(注意变量是矩阵,各个变量的上方标记了该变量的形状)
(5.13)
(5.14)
5.6 Affine/Softmax 层的实现 147
(2,)
X (3,) (3,)
1 1 X・W Y
dot +
(2,) (3,) (3, 2) (2, 3)
W
(3,) (3,)
2 2
(3,)
图 5-25 Affine 层的反向传播:注意变量是多维数组。反向传播时各个变量的下方标记了
该变量的形状
(5.15)
为什么要注意矩阵的形状呢?因为矩阵的乘积运算要求对应维度的元素
个数保持一致,通过确认一致性,就可以导出式(5.13)
。比如, 的形状是
T
(3,),W 的形状是 (2, 3) 时,思考 和 W 的乘积,使得 的形状为 (2,)
(图 5-26)。这样一来,就会自然而然地推导出式(5.13)。
(2,)
X
(3,)
1 1 X・W
dot
(3,) (3, 2) (2,) (2, 3)
W
(3,)
图 5-26 矩阵的乘积(“dot”节点)的反向传播可以通过组建使矩阵对应维度的元素个数一
致的乘积运算而推导出来
5.6.2 批版本的 Affine 层
(N, 2)
X (N, 3) (N, 3)
1 1 X・W Y
dot +
(N, 2) (N, 3) (3, 2) (2, 3)
W
(N, 3) (N, 3)
2 2
(2, 3) (2, N) (N, 3) (3,)
B
3
3 的第一个轴(第 0 轴)方向上的和
(3) (N, 3)
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
return out
return dx
5.6.3 Softmax-with-Loss 层
得分 概率
5.3 0.008
0.3 0.00005
・・・・・・
・・・・・・
10.1 0.991
・・・
・・・
・・・
・・・
・・・
・・・
0.01 0.00004
神经网络中进行的处理有推理(inference)和学习两个阶段。神经网
络的推理通常不使用 Softmax 层。比如,用图 5-28 的网络进行推理时,
会将最后一个 Affine 层的输出作为识别结果。神经网络中未被正规
化的输出结果(图 5-28 中 Softmax 层前面的 Affine 层的输出)有时
被称为“得分”。也就是说,当神经网络的推理只需要给出一个答案
的情况下,因为此时只对得分最大值感兴趣,所以不需要 Softmax 层。
不过,神经网络的学习阶段则需要 Softmax 层。
S
a1 exp(a 1 ) y1 log y 1
1
S
exp × log ×
y 1−t 1
1 − t1
1 − yt 1 t −t 1
t 1 log y 1
S exp(a 1 ) S 1 2
exp(a 2 ) −1
− t 2S
1
a2 S exp(a 2 ) y2 log y 2 t 2 log y 2 t 1 log y 1 + t 2 log y 2 + t 3 log y 3 L
exp × log × + ×
y 2−t 2 t2 − yt 2 t 3 −t 2 −1 −1 1
−
exp(a 3 ) exp(a 2 ) −t 3S 1
S
2 t 3 log y 3 −1
y3 −1
a3 exp(a 3 ) log y 3
exp × log ×
y 3−t 3 − t3 − yt 3 −t 3
3
exp(a 3 )
图 5-29 Softmax-with-Loss 层的计算图
可以看到,Softmax-with-Loss 层有些复杂。这里只给出了最终结果,
对 Softmax-with-Loss 层的导出过程感兴趣的读者,请参照附录 A。
图 5-29 的计算图可以简化成图 5-30。
图 5-30 的 计 算 图 中,softmax 函 数 记 为 Softmax 层,交 叉 熵 误 差 记 为
Cross Entropy Error 层。这里假设要进行 3 类分类,从前面的层接收 3 个输
入(得分)。如图 5-30 所示,Softmax 层将输入(a1, a2, a3)正规化,输出(y1,
y2, y3)。Cross Entropy Error 层接收 Softmax 的输出(y1, y2, y3)和监督标签(t1,
t2, t3),从这些数据中输出损失 L。
152 第 5 章 误差反向传播法
t1
a1 y1
y 1− t 1
t2
a2 y2 Cross L
Softmax Entropy
Error
y 2− t 2 1
t3
a3 y3
y 3 −t 3
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax 的输出
self.t = None # 监督数据(one-hot vector)
return self.loss
return dx
的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。
154 第 5 章 误差反向传播法
5.7 误差反向传播法的实现
通过像组装乐高积木一样组装上一节中实现的层,可以构建神经网络。
本节我们将通过组装已经实现的层来构建神经网络。
5.7.1 神经网络学习的全貌图
在进行具体的实现之前,我们再来确认一下神经网络学习的全貌图。神
经网络学习的步骤如下所示。
前提
神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的
过程称为学习。神经网络的学习分为下面 4 个步骤。
步骤 1(mini-batch)
从训练数据中随机选择一部分数据。
步骤 2(计算梯度)
计算损失函数关于各个权重参数的梯度。
步骤 3(更新参数)
将权重参数沿梯度方向进行微小的更新。
步骤 4(重复)
重复步骤 1、步骤 2、步骤 3。
之前介绍的误差反向传播法会在步骤 2 中出现。上一章中,我们利用数
值微分求得了这个梯度。数值微分虽然实现简单,但是计算要耗费较多的时
间。和需要花费较多时间的数值微分不同,误差反向传播法可以快速高效地
计算梯度。
5.7 误差反向传播法的实现 155
5.7.2 对应误差反向传播法的神经网络的实现
现在来进行神经网络的实现。这里我们要把2层神经网络实现为 TwoLayerNet。
首先,将这个类的实例变量和方法整理成表 5-1 和表 5-2。
表 5-1 TwoLayerNet 类的实例变量
实例变量 说明
params 保存神经网络的参数的字典型变量。
params['W1'] 是第 1 层的权重,params['b1'] 是第 1 层的偏置。
params['W2'] 是第 2 层的权重,params['b2'] 是第 2 层的偏置
layers 保存神经网络的层的有序字典型变量。
以 layers['Affine1']、layers['ReLu1']、layers['Affine2'] 的形式,
通过有序字典保存各个层
lastLayer 神经网络的最后一层。
本例中为 SoftmaxWithLoss 层
表 5-2 TwoLayerNet 类的方法
方法 说明
__init__(self, input_size, 进行初始化。
hidden_size, output_size, 参数从头开始依次是输入层的神经元数、隐藏层的
weight_init_std) 神经元数、输出层的神经元数、初始化权重时的高
斯分布的规模
predict(self, x) 进行识别(推理)。
参数 x 是图像数据
loss(self, x, t) 计算损失函数的值。
参数 X 是图像数据、t 是正确解标签
accuracy(self, x, t) 计算识别精度
numerical_gradient(self, x, t) 通过数值微分计算关于权重参数的梯度(同上一章)
gradient(self, x, t) 通过误差反向传播法计算关于权重参数的梯度
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict
class TwoLayerNet:
# 生成层
self.layers = OrderedDict()
self.layers['Affine1'] = \
Affine(self.params['W1'], self.params['b1'])
self.layers['Relu1'] = Relu()
self.layers['Affine2'] = \
Affine(self.params['W2'], self.params['b2'])
self.lastLayer = SoftmaxWithLoss()
return x
# x: 输入数据 , t: 监督数据
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
# x: 输入数据 , t: 监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'] = self.layers['Affine1'].dW
grads['b1'] = self.layers['Affine1'].db
grads['W2'] = self.layers['Affine2'].dW
grads['b2'] = self.layers['Affine2'].db
return grads
请注意这个实现中的粗体字代码部分,尤其是将神经网络的层保存为
OrderedDict 这一点非常重要。OrderedDict 是有序字典,“有序”是指它可以
记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元
素的顺序调用各层的 forward() 方法就可以完成处理,而反向传播只需要按
照相反的顺序调用各层即可。因为 Affine 层和 ReLU 层的内部会正确处理正
向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再
按顺序(或者逆序)调用各层。
158 第 5 章 误差反向传播法
像这样通过将神经网络的组成元素以层的方式实现,可以轻松地构建神
经网络。这个用层进行模块化的实现具有很大优点。因为想另外构建一个神
经网络(比如 5 层、10 层、20 层……的大的神经网络)时,只需像组装乐高
积木那样添加必要的层就可以了。之后,通过各个层内部实现的正向传播和
反向传播,就可以正确计算进行识别处理或学习所需的梯度。
5.7.3 误差反向传播法的梯度确认
到目前为止,我们介绍了两种求梯度的方法。一种是基于数值微分的方
法,另一种是解析性地求解数学式的方法。后一种方法通过使用误差反向传
播法,即使存在大量的参数,也可以高效地计算梯度。因此,后文将不再使
用耗费时间的数值微分,而是使用误差反向传播法求梯度。
数值微分的计算很耗费时间,而且如果有误差反向传播法的(正确的)
实现的话,就没有必要使用数值微分的实现了。那么数值微分有什么用呢?
实际上,在确认误差反向传播法的实现是否正确时,是需要用到数值微分的。
数值微分的优点是实现简单,因此,一般情况下不太容易出错。而误差
反向传播法的实现很复杂,容易出错。所以,经常会比较数值微分的结果和
误差反向传播法的结果,以确认误差反向传播法的实现是否正确。确认数值
微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是
非常相近)的操作称为梯度确认(gradient check)。梯度确认的代码实现如下
所示(源代码在 ch05/gradient_check.py 中)。
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 读入数据
(x_train, t_train), (x_test, t_test) = \ load_mnist(normalize=True, one_
hot_label = True)
x_batch = x_train[:3]
t_batch = t_train[:3]
# 求各个权重的绝对误差的平均值
for key in grad_numerical.keys():
diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
print(key + ":" + str(diff))
b1:9.70418809871e-13
W2:8.41139039497e-13
b2:1.1945999745e-10
W1:2.2232446644e-13
从这个结果可以看出,通过数值微分和误差反向传播法求出的梯度的差
非常小。比如,第 1 层的偏置的误差是 9.7e-13(0.00000000000097)。这样一来,
我们就知道了通过误差反向传播法求出的梯度是正确的,误差反向传播法的
实现没有错误。
数值微分和误差反向传播法的计算结果之间的误差为 0 是很少见的。
这是因为计算机的计算精度有限(比如,32 位浮点数)。受到数值精
度的限制,刚才的误差一般不会为 0,但是如果实现正确的话,可
以期待这个误差是一个接近 0 的很小的值。如果这个值很大,就说
明误差反向传播法的实现存在错误。
5.7.4 使用误差反向传播法的学习
最后,我们来看一下使用了误差反向传播法的神经网络的学习的实现。
和之前的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点。这
里只列出了代码,省略了说明(源代码在 ch05/train_neuralnet.py 中)。
160 第 5 章 误差反向传播法
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 读入数据
(x_train, t_train), (x_test, t_test) = \
load_mnist(normalize=True, one_hot_label=True)
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []
for i in range(iters_num):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 通过误差反向传播法求梯度
grad = network.gradient(x_batch, t_batch)
# 更新
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print(train_acc, test_acc)
5.8 小结 161
5.8 小结
本章我们介绍了将计算过程可视化的计算图,并使用计算图,介绍了神
经网络中的误差反向传播法,并以层为单位实现了神经网络中的处理。我们
学过的层有 ReLU 层、Softmax-with-Loss 层、Affine 层、Softmax 层等,这
些层中实现了 forward 和 backward 方法,通过将数据正向和反向地传播,可
以高效地计算权重参数的梯度。通过使用层进行模块化,神经网络中可以自
由地组装层,轻松构建出自己喜欢的网络。
本章所学的内容
• 通过使用计算图,可以直观地把握计算过程。
• 计算图的节点是由局部计算构成的。局部计算构成全局计算。
• 计算图的正向传播进行一般的计算。通过计算图的反向传播,可以
计算各个节点的导数。
• 通过将神经网络的组成元素实现为层,可以高效地计算梯度(反向传
播法)。
• 通过比较数值微分和误差反向传播法的结果,可以确认误差反向传
播法的实现是否正确(梯度确认)。
本章将介绍神经网络的学习中的一些重要观点,主题涉及寻找最优权重
参数的最优化方法、权重参数的初始值、超参数的设定方法等。此外,为了
应对过拟合,本章还将介绍权值衰减、Dropout 等正则化方法,并进行实现。
最后将对近年来众多研究中使用的 Batch Normalization 方法进行简单的介绍。
使用本章介绍的方法,可以高效地进行神经网络(深度学习)的学习,提高
识别精度。让我们一起往下看吧!
6.1 参数的更新
神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻
找最优参数的问题,解决这个问题的过程称为最优化(optimization)。遗憾的是,
神经网络的最优化问题非常难。这是因为参数空间非常复杂,无法轻易找到
最优解(无法使用那种通过解数学式一下子就求得最小值的方法)。而且,在
深度神经网络中,参数的数量非常庞大,导致最优化问题更加复杂。
在前几章中,为了找到最优参数,我们将参数的梯度(导数)作为了线索。
使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠
近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent),
简称 SGD。SGD 是一个简单的方法,不过比起胡乱地搜索参数空间,也算是“聪
明”的方法。但是,根据不同的问题,也存在比 SGD 更加聪明的方法。本节
6.1.1 探险家的故事
进入正题前,我们先打一个比方,来说明关于最优化我们所处的状况。
有一个性情古怪的探险家。他在广袤的干旱地带旅行,坚持寻找幽
深的山谷。他的目标是要到达最深的谷底(他称之为“至深之地”)。这
也是他旅行的目的。并且,他给自己制定了两个严格的“规定”:一个
是不看地图;另一个是把眼睛蒙上。因此,他并不知道最深的谷底在这
个广袤的大地的何处,而且什么也看不见。在这么严苛的条件下,这位
探险家如何前往“至深之地”呢?他要如何迈步,才能迅速找到“至深
之地”呢?
寻找最优参数时,我们所处的状况和这位探险家一样,是一个漆黑的世
界。我们必须在没有地图、不能睁眼的情况下,在广袤、复杂的地形中寻找
“至深之地”。大家可以想象这是一个多么难的问题。
在这么困难的状况下,地面的坡度显得尤为重要。探险家虽然看不到周
围的情况,但是能够知道当前所在位置的坡度(通过脚底感受地面的倾斜状况)
。
于是,朝着当前所在位置的坡度最大的方向前进,就是 SGD 的策略。勇敢
的探险家心里可能想着只要重复这一策略,总有一天可以到达“至深之地”。
6.1.2 SGD
让大家感受了最优化问题的难度之后,我们再来复习一下 SGD。用数
学式可以将 SGD 写成如下的式(6.1)。
(6.1)
这里把需要更新的权重参数记为W,把损失函数关于W的梯度记为 。
η 表示学习率,实际上会取 0.01 或 0.001 这些事先决定好的值。式子中的←
6.1 参数的更新 165
表示用右边的值更新左边的值。如式(6.1)所示,SGD 是朝着梯度方向只前
进一定距离的简单方法。现在,我们将 SGD 实现为一个 Python 类(为方便
后面使用,我们将其实现为一个名为 SGD 的类)。
class SGD:
def __init__(self, lr=0.01):
self.lr = lr
network = TwoLayerNet(...)
optimizer = SGD()
for i in range(10000):
...
x_batch, t_batch = get_mini_batch(...) # mini-batch
grads = network.gradient(x_batch, t_batch)
params = network.params
optimizer.update(params, grads)
...
换为 Momentum。
很多深度学习框架都实现了各种最优化方法,并且提供了可以简单
切换这些方法的构造。比如 Lasagne 深度学习框架,在 updates.py
这个文件中以函数的形式集中实现了最优化方法。用户可以从中选
择自己想用的最优化方法。
6.1.3 SGD 的缺点
虽然 SGD 简单,并且容易实现,但是在解决某些问题时可能没有效率。
这里,在指出 SGD 的缺点之际,我们来思考一下求下面这个函数的最小值
的问题。
(6.2)
10
120
100
5
80
60 z
0
y
40
20
−5
0
10
5 −10
−10 0 y −10 −5 0 5 10
−5 0 −5 x
x 5 10 −10
图 6-1 的图形(左图)和它的等高线(右图)
6.1 参数的更新 167
现在看一下式(6.2)表示的函数的梯度。如果用图表示梯度的话,则如
图 6-2 所示。这个梯度的特征是,y 轴方向上大,x 轴方向上小。换句话说,
就是 y 轴方向的坡度大,而 x 轴方向的坡度小。这里需要注意的是,虽然式
(6.2)的最小值在 (x, y) = (0, 0) 处,但是图 6-2 中的梯度在很多地方并没有指
向 (0, 0)。
0
y
−2
−4
−10 −5 0 5 10
x
图 6-2 的梯度
SGD
10
0
y
−5
−10
−10 −5 0 5 10
x
6.1.4 Momentum
(6.3)
(6.4)
图 6-4 Momentum:小球在斜面上滚动
式(6.3)中有 αv 这一项。在物体不受任何力时,该项承担使物体逐渐减
速的任务(α 设定为 0.9 之类的值),对应物理上的地面摩擦或空气阻力。下
面是 Momentum 的代码实现(源代码在 common/optimizer.py 中)。
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
Momentum
10
0
y
−5
−10
−10 −5 0 5 10
x
6.1.5 AdaGrad
在神经网络的学习中,学习率(数学式中记为 η)的值很重要。学习率过小,
会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能
正确进行。
在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate
decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”
学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。
而 AdaGrad [6] 进一步发展了这个想法,针对“一个一个”的参数,赋予其“定
制”的值。
AdaGrad 会为参数的每个元素适当地调整学习率,与此同时进行学习
(AdaGrad 的 Ada 来自英文单词 Adaptive,即“适当的”的意思)。下面,让
我们用数学式表示 AdaGrad 的更新方法。
6.1 参数的更新 171
(6.5)
(6.6)
AdaGrad 会记录过去所有梯度的平方和。因此,学习越深入,更新
的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0,
完 全 不 再 更 新。为 了 改 善 这 个 问 题,可 以 使 用 RMSProp [7] 方 法。
RMSProp 方法并不是将过去所有的梯度一视同仁地相加,而是逐渐
地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。
这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小
过去的梯度的尺度。
现 在 来 实 现 AdaGrad。AdaGrad 的 实 现 过 程 如 下 所 示(源 代 码 在
common/optimizer.py 中)。
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
这里需要注意的是,最后一行加上了微小值 1e-7。这是为了防止当
self.h[key] 中有 0 时,将 0 用作除数的情况。在很多深度学习的框架中,这
AdaGrad
10
0
y
−5
−10
−10 −5 0 5 10
x
由图 6-6 的结果可知,函数的取值高效地向着最小值移动。由于 y 轴方
向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按
比例进行调整,减小更新的步伐。因此,y 轴方向上的更新程度被减弱,
“之”
字形的变动程度有所衰减。
6.1.6 Adam
Momentum 参照小球在碗中滚动的物理规则进行移动,AdaGrad 为参
数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起会怎么样
6.1 参数的更新 173
Adam
10
0
y
−5
−10
−10 −5 0 5 10
x
6.1.7 使用哪种更新方法呢
到目前为止,我们已经学习了 4 种更新参数的方法。这里我们来比较一
下这 4 种方法(源代码在 ch06/optimizer_compare_naive.py 中)。
如图 6-8 所示,根据使用的方法不同,参数更新的路径也不同。只看这
个图的话,AdaGrad 似乎是最好的,不过也要注意,结果会根据要解决的问
题而变。并且,很显然,超参数(学习率等)的设定值不同,结果也会发生变化。
SGD Momentum
10 10
5 5
y 0 y 0
−5 −5
−10 −10
−10 −5 0 5 10 −10 −5 0 5 10
x x
AdaGrad Adam
10 10
5 5
y 0 y 0
−5 −5
−10 −10
−10 −5 0 5 10 −10 −5 0 5 10
x x
图 6-8 最优化方法的比较:SGD、Momentum、AdaGrad、Adam
我 们 以 手 写 数 字 识 别 为 例,比 较 前 面 介 绍 的 SGD、Momentum、
AdaGrad、Adam 这 4 种方法,并确认不同的方法在学习进展上有多大程度
的差异。先来看一下结果,如图 6-9 所示(源代码在 ch06/optimizer_compare_
mnist.py 中)。
1.0
Adam
SGD
AdaGrad
0.8
Momentum
0.6
loss
0.4
0.2
0.0
0 500 1000 1500 2000
iterations
6.2 权重的初始值
在神经网络的学习中,权重的初始值特别重要。实际上,设定什么样的
权重初始值,经常关系到神经网络的学习能否成功。本节将介绍权重初始值
的推荐值,并通过实验确认神经网络的学习是否会快速进行。
6.2.1 可以将权重初始值设为 0 吗
后面我们会介绍抑制过拟合、提高泛化能力的技巧——权值衰减(weight
decay)。简单地说,权值衰减就是一种以减小权重参数的值为目的进行学习
的方法。通过减小权重参数的值来抑制过拟合的发生。
如果想减小权重的值,一开始就将初始值设为较小的值才是正途。实际上,
在这之前的权重初始值都是像 0.01 * np.random.randn(10, 100) 这样,使用
的内容)。因此,权重被更新为相同的值,并拥有了对称的值(重复的值)。
这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”
(严格地讲,是为了瓦解权重的对称结构),必须随机生成初始值。
6.2.2 隐藏层的激活值的分布
观察隐藏层的激活值 A(激活函数的输出数据)的分布,可以获得很多启
发。这里,我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的
激活值的分布的。这里要做的实验是,向一个 5 层神经网络(激活函数使用
sigmoid 函数)传入随机生成的输入数据,用直方图绘制各层激活值的数据分
布。这个实验参考了斯坦福大学的课程 CS231n [5]。
进行实验的源代码在 ch06/weight_init_activation_histogram.py 中,下
面展示部分代码。
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
w = np.random.randn(node_num, node_num) * 1
z = np.dot(x, w)
a = sigmoid(z) # sigmoid 函数
activations[i] = a
A 这里我们将激活函数的输出数据称为“激活值”,但是有的文献中会将在层之间流动的数据也称为“激
活值”。
178 第 6 章 与学习相关的技巧
# 绘制直方图
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
图 6-10 使用标准差为 1 的高斯分布作为权重初始值时的各层激活值的分布
# w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01
各层的激活值的分布都要求有适当的广度。为什么呢?因为通过
在各层间传递多样性的数据,神经网络可以进行高效的学习。反
过来,如果传递的是有所偏向的数据,就会出现梯度消失或者“表
现力受限”的问题,导致学习可能无法顺利进行。
导了合适的权重尺度。推导出的结论是,如果前一层的节点数为 n,则初始
值使用标准差为 的分布 A(图 6-12)。
n 个节点
使用标准差为 的高斯分布进行初始化
使用 Xavier 初始值后,前一层的节点数越多,要设定为目标节点的初始
值的权重尺度就越小。现在,我们使用 Xavier 初始值进行实验。进行实验的
代码只需要将设定权重初始值的地方换成如下内容即可(因为此处所有层的
节点数都是 100,所以简化了实现)。
A Xavier 的论文中提出的设定值,不仅考虑了前一层的输入节点数量,还考虑了下一层的输出节点数量。
但是,Caffe 等框架的实现中进行了简化,只使用了这里所说的前一层的输入节点进行计算。
5000
4000
3000
2000
1000
0
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
6.2.3 ReLU 的权重初始值
Xavier 初始值是以激活函数是线性函数为前提而推导出来的。因为
sigmoid 函数和 tanh 函数左右对称,且中央附近可以视作线性函数,所以适
6000
5000
4000
3000
2000
1000
0
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
6000
5000
4000
3000
2000
1000
0
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
6000
5000
4000
3000
2000
1000
0
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
权重初始值为 He 初始值时
下面通过实际的数据,观察不同的权重初始值的赋值方法会在多大程度
上影响神经网络的学习。这里,我们基于 std = 0.01、Xavier 初始值、He 初
始值进行实验(源代码在 ch06/weight_init_compare.py 中)。先来看一下结果,
如图 6-15 所示。
2.5
He
std = 0.01
Xavier
2.0
1.5
loss
1.0
0.5
0.0
0 500 1000 1500 2000
iterations
在上一节,我们观察了各层的激活值分布,并从中了解到如果设定了合
适的权重初始值,则各层的激活值分布会有适当的广度,从而可以顺利地进
行学习。那么,为了使各层拥有适当的广度,
“强制性”地调整激活值的分布
会怎样呢?实际上,Batch Normalization[11] 方法就是基于这个想法而产生的。
• 可以使学习快速进行(可以增大学习率)。
• 不那么依赖初始值(对于初始值不用那么神经质)。
• 抑制过拟合(降低 Dropout 等的必要性)。
考虑到深度学习要花费很多时间,第一个优点令人非常开心。另外,后
两点也可以帮我们消除深度学习的学习中的很多烦恼。
如前所述,Batch Norm 的思路是调整各层的激活值分布使其拥有适当
的广度。为此,要向神经网络中插入对数据分布进行正规化的层,即 Batch
Normalization 层(下文简称 Batch Norm 层),如图 6-16 所示。
Batch Batch
Affine ReLU Affine ReLU Affine Softmax
Norm Norm
(6.7)
(6.8)
(N,D) (N,D)
out
x x − +
dx * * dout
ˆ2 x−ε 1
(D,)
x
r
dr r
(D,)
β
β
dβ
Training Accuracy
0.9
0.8
0.7
0.6
accuracy
0.5
0.4
0.3
1.0
w:1.0 w:0.541169526546 w:0.292864456463 w:0.158483319246
0.8
accuracy
0.6
0.4
0.2
0.0
1.0
w:0.0857695898591 w:0.0464158883361 w:0.0251188643151 w:0.0125935639088
0.8
accuracy
0.6
0.4
0.2
0.0
1.0
w:0.0073564225446 w:0.00398107170553 w:0.00215443469003 w:0.00116591440118
0.8
accuracy
0.6
0.4
0.2
0.0
1.0
w:0.00063095734448 w:0.000341454887383 w:0.000184784979742 w:0.0001
0.8
accuracy
0.6
0.4
0.2
0.0
0 5 10 15 20 0 5 10 15 20 0 5 10 15 20 0 5 10 15 20
epochs epochs epochs epochs
6.4 正则化
机器学习的问题中,过拟合是一个很常见的问题。过拟合指的是只能拟
合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。机
器学习的目标是提高泛化能力,即便是没有包含在训练数据里的未观测数据,
也希望模型可以进行正确的识别。我们可以制作复杂的、表现力强的模型,
6.4 正则化 189
但是相应地,抑制过拟合的技巧也很重要。
6.4.1 过拟合
发生过拟合的原因,主要有以下两个。
• 模型拥有大量参数、表现力强。
• 训练数据少。
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100
train_loss_list = []
train_acc_list = []
test_acc_list = []
for i in range(1000000000):
batch_mask = np.random.choice(train_size, batch_size)
190 第 6 章 与学习相关的技巧
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
的单位)保存识别精度。现在,我们将这些列表(train_acc_list、test_acc_
list)绘成图,结果如图 6-20 所示。
1.0
0.8
0.6
accuracy
0.4
0.2
train
test
0.0
0 50 100 150 200
epochs
图 6-20 训练数据(train)和测试数据(test)的识别精度的变化
6.4 正则化 191
6.4.2 权值衰减
权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过
在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是
因为权重参数取值过大才发生的。
复习一下,神经网络的学习目的是减小损失函数的值。这时,例如为
损失函数加上权重的平方范数(L2 范数)。这样一来,就可以抑制权重变大。
用符号表示的话,如果将权重记为 W,L2 范数的权值衰减就是 ,然
后将这个 加到损失函数上。这里,λ 是控制正则化强度的超参数。λ
设置得越大,对大的权重施加的惩罚就越重。此外, 开头的 是用于
将 的求导结果变成 λW 的调整用常量。
对于所有权重,权值衰减方法都会为损失函数加上 。因此,在求权
重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数λW。
L2 范数相当于各个元素的平方和。用数学式表示的话,假设有权重
W = (w1, w2, . . . , wn),则 L2 范数可用 计算
出来。除了 L2 范数,还有 L1 范数、L ∞范数等。L1 范数是各个元
素的绝对值之和,相当于 |w1| + |w2| + . . . + |wn|。L∞范数也称为
Max 范数,相当于各个元素的绝对值中最大的那一个。L2 范数、L1
范数、L∞范数都可以用作正则化项,它们各有各的特点,不过这里
我们要实现的是比较常用的 L2 范数。
1.0
0.8
0.6
accuracy
0.4
0.2
train
test
0.0
0 50 100 150 200
epochs
图 6-21 使用了权值衰减的训练数据(train)和测试数据(test)的识别精度的变化
如图 6-21 所示,虽然训练数据的识别精度和测试数据的识别精度之间有
差距,但是与没有使用权值衰减的图 6-20 的结果相比,差距变小了。这说明
过拟合受到了抑制。此外,还要注意,训练数据的识别精度没有达到 100%(1.0)。
6.4.3 Dropout
作为抑制过拟合的方法,前面我们介绍了为损失函数加上权重的 L2 范
数的权值衰减方法。该方法可以简单地实现,在某种程度上能够抑制过拟合。
但是,如果网络的模型变得很复杂,只用权值衰减就难以应对了。在这种情
况下,我们经常会使用 Dropout [14] 方法。
Dropout 是一种在学习的过程中随机删除神经元的方法。训练时,随机
选出隐藏层的神经元,然后将其删除。被删除的神经元不再进行信号的传递,
如图 6-22 所示。训练时,每传递一次数据,就会随机选择要删除的神经元。
然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,
要乘上训练时的删除比例后再输出。
6.4 正则化 193
下面我们来实现 Dropout。这里的实现重视易理解性。不过,因为训练
时如果进行恰当的计算的话,正向传播时单纯地传递数据就可以了(不用乘
以删除比例),所以深度学习的框架中进行了这样的实现。关于高效的实现,
可以参考 Chainer 中实现的 Dropout。
class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
正向传播时传递了信号的神经元,反向传播时按原样传递信号;正向传播时
194 第 6 章 与学习相关的技巧
没有传递信号的神经元,反向传播时信号将停在那里。
现在,我们使用 MNIST 数据集进行验证,以确认 Dropout 的效果。源代
码在 ch06/overfit_dropout.py 中。另外,源代码中使用了 Trainer 类来简化实现。
1.0 1.0
0.8 0.8
0.6 0.6
accuracy
accuracy
0.4 0.4
0.2 0.2
train train
test test
0.0 0.0
0 50 100 150 200 250 300 0 50 100 150 200 250 300
epochs epochs
机器学习中经常使用集成学习。所谓集成学习,就是让多个模型单
独进行学习,推理时再取多个模型的输出的平均值。用神经网络的
语境来说,比如,准备 5 个结构相同(或者类似)的网络,分别进行
学习,测试时,以这 5 个网络的输出的平均值作为答案。实验告诉我们,
6.5 超参数的验证 195
通过进行集成学习,神经网络的识别精度可以提高好几个百分点。
这个集成学习与 Dropout 有密切的关系。这是因为可以将 Dropout
理解为,通过在学习过程中随机删除神经元,从而每一次都让不同
的模型进行学习。并且,推理时,通过对神经元的输出乘以删除比
例(比如,0.5 等),可以取得模型的平均值。也就是说,可以理解成,
Dropout 将集成学习的效果(模拟地)通过一个网络实现了。
6.5 超参数的验证
神经网络中,除了权重和偏置等参数,超参数(hyper-parameter)也经
常出现。这里所说的超参数是指,比如各层的神经元数量、batch 大小、参
数更新时的学习率或权值衰减等。如果这些超参数没有设置合适的值,模型
的性能就会很差。虽然超参数的取值非常重要,但是在决定超参数的过程中
一般会伴随很多的试错。本节将介绍尽可能高效地寻找超参数的值的方法。
6.5.1 验证数据
之前我们使用的数据集分成了训练数据和测试数据,训练数据用于学习,
测试数据用于评估泛化能力。由此,就可以评估是否过度拟合了训练数据(是
否发生了过拟合),以及泛化能力如何等。
下面我们要对超参数设置各种各样的值以进行验证。这里要注意的是,
不能使用测试数据评估超参数的性能。这一点非常重要,但也容易被忽视。
为什么不能用测试数据评估超参数的性能呢?这是因为如果使用测试数
据调整超参数,超参数的值会对测试数据发生过拟合。换句话说,用测试数
据确认超参数的值的“好坏”,就会导致超参数的值被调整为只拟合测试数据。
这样的话,可能就会得到不能拟合其他数据、泛化能力低的模型。
因此,调整超参数时,必须使用超参数专用的确认数据。用于调整超参
数的数据,一般称为验证数据(validation data)。我们使用这个验证数据来
评估超参数的好坏。
196 第 6 章 与学习相关的技巧
训练数据用于参数(权重和偏置)的学习,验证数据用于超参数的性
能评估。为了确认泛化能力,要在最后使用(比较理想的是只用一次)
测试数据。
根据不同的数据集,有的会事先分成训练数据、验证数据、测试数据三
部分,有的只分成训练数据和测试数据两部分,有的则不进行分割。在这种
情况下,用户需要自行进行分割。如果是 MNIST 数据集,获得验证数据的
最简单的方法就是从训练数据中事先分割 20% 作为验证数据,代码如下所示。
# 打乱训练数据
x_train, t_train = shuffle_dataset(x_train, t_train)
# 分割验证数据
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]
这里,分割训练数据前,先打乱了输入数据和监督标签。这是因为数据
集的数据可能存在偏向(比如,数据从“0”到“10”按顺序排列等)。这里使
用的 shuffle_dataset 函数利用了 np.random.shuffle,在 common/util.py 中有
它的实现。
接下来,我们使用验证数据观察超参数的最优化方法。
6.5.2 超参数的最优化
进行超参数的最优化时,逐渐缩小超参数的“好值”的存在范围非常重要。
所谓逐渐缩小范围,是指一开始先大致设定一个范围,从这个范围中随机选
出一个超参数(采样),用这个采样到的值进行识别精度的评估;然后,多次
重复该操作,观察识别精度的结果,根据这个结果缩小超参数的“好值”的范围。
通过重复这一操作,就可以逐渐确定超参数的合适范围。
超参数的范围只要“大致地指定”就可以了。所谓“大致地指定”,是指
像 0.001(10−3)到 1000(103)这样,以“10 的阶乘”的尺度指定范围(也表述
为“用对数尺度(log scale)指定”)。
在超参数的最优化中,要注意的是深度学习需要很长时间(比如,几天
或几周)。因此,在超参数的搜索中,需要尽早放弃那些不符合逻辑的超参数。
于是,在超参数的最优化中,减少学习的 epoch,缩短一次评估所需的时间
是一个不错的办法。
以上就是超参数的最优化的内容,简单归纳一下,如下所示。
步骤 0
设定超参数的范围。
步骤 1
从设定的超参数范围中随机采样。
步骤 2
使用步骤 1 中采样到的超参数的值进行学习,通过验证数据评估识别精
度(但是要将 epoch 设置得很小)。
步骤 3
重复步骤 1 和步骤 2(100 次等),根据它们的识别精度的结果,缩小超参
数的范围。
反复进行上述操作,不断缩小超参数的范围,在缩小到一定程度时,从
该范围中选出一个超参数的值。这就是进行超参数的最优化的一种方法。
198 第 6 章 与学习相关的技巧
这里介绍的超参数的最优化方法是实践性的方法。不过,这个方
法与其说是科学方法,倒不如说有些实践者的经验的感觉。在超
参数的最优化中,如果需要更精炼的方法,可以使用贝叶斯最优
化(Bayesian optimization)
。贝叶斯最优化运用以贝叶斯定理为中
心的数学理论,能够更加严密、高效地进行最优化。详细内容请
参 考 论 文“Practical Bayesian Optimization of Machine Learning
[16]
Algorithms” 等。
6.5.3 超参数最优化的实现
像这样进行随机采样后,再使用那些值进行学习。之后,多次使用各种
超参数的值重复进行学习,观察合乎逻辑的超参数在哪里。这里省略了具体
实现,只列出了结果。进行超参数最优化的源代码在 ch06/hyperparameter_
optimization.py 中,请大家自由参考。
图 6-24 实线是验证数据的识别精度,虚线是训练数据的识别精度
图 6-24 中,按识别精度从高到低的顺序排列了验证数据的学习的变化。
从图中可知,直到“Best-5”左右,学习进行得都很顺利。因此,我们来
观察一下“Best-5”之前的超参数的值(学习率和权值衰减系数),结果如下
所示。
这样就能缩小到合适的超参数的存在范围,然后在某个阶段,选择一个最终
的超参数的值。
6.6 小结
本章我们介绍了神经网络的学习中的几个重要技巧。参数的更新方法、
权重初始值的赋值方法、Batch Normalization、Dropout 等,这些都是现代
神经网络中不可或缺的技术。另外,这里介绍的技巧,在最先进的深度学习
中也被频繁使用。
本章所学的内容
7.1 整体结构
Affine ReLU Affine ReLU Affine ReLU Affine ReLU Affine Softmax
图 7-1 基于全连接层(Affine 层)的网络的例子
Conv ReLU Pooling Conv ReLU Pooling Conv ReLU Affine ReLU Affine Softmax
7.2 卷积层
CNN 中出现了一些特有的术语,比如填充、步幅等。此外,各层中传
递的数据是有形状的数据(比如,3 维数据),这与之前的全连接网络不同,
因此刚开始学习 CNN 时可能会感到难以理解。本节我们将花点时间,认真
学习一下 CNN 中使用的卷积层的结构。
7.2 卷积层 203
7.2.1 全连接层存在的问题
之前介绍的全连接的神经网络中使用了全连接层(Affine 层)。在全连接
层中,相邻层的神经元全部连接在一起,输出的数量可以任意决定。
全连接层存在什么问题呢?那就是数据的形状被“忽视”了。比如,输
入数据是图像时,图像通常是高、长、通道方向上的 3 维形状。但是,向全
连接层输入时,需要将 3 维数据拉平为 1 维数据。实际上,前面提到的使用
了 MNIST 数据集的例子中,输入图像就是 1 通道、高 28 像素、长 28 像素
的(1, 28, 28)形状,但却被排成 1 列,以 784 个数据的形式输入到最开始的
Affine 层。
图像是 3 维形状,这个形状中应该含有重要的空间信息。比如,空间上
邻近的像素为相似的值、RGB 的各个通道之间分别有密切的关联性、相距
较远的像素之间没有什么关联等,3 维形状中可能隐藏有值得提取的本质模
式。但是,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元
(同一维度的神经元)处理,所以无法利用与形状相关的信息。
而卷积层可以保持形状不变。当输入数据是图像时,卷积层会以 3 维
数据的形式接收输入数据,并同样以 3 维数据的形式输出至下一层。因此,
在 CNN 中,可以(有可能)正确理解图像等具有形状的数据。
另 外,CNN 中,有 时 将 卷 积 层 的 输 入 输 出 数 据 称 为 特 征 图(feature
map)。其中,卷积层的输入数据称为输入特征图(input feature map),输出
数据称为输出特征图(output feature map)。本书中将“输入输出数据”和“特
征图”作为含义相同的词使用。
7.2.2 卷积运算
卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的“滤波
器运算”。在介绍卷积运算时,我们来看一个具体的例子(图 7-3)。
204 第 7 章 卷积神经网络
1 2 3 0
2 0 1
0 1 2 3 15 16
0 1 2
3 0 1 2 6 15
1 0 2
2 3 0 1
输入数据 滤波器
图 7-3 卷积运算的例子:用“”符号表示卷积运算
如图 7-3 所示,卷积运算对输入数据应用滤波器。在这个例子中,输入
数据是有高长方向的形状的数据,滤波器也一样,有高长方向上的维度。假
设用(height, width)表示数据和滤波器的形状,则在本例中,输入大小是
(4, 4),滤波器大小是 (3, 3),输出大小是 (2, 2)。另外,有的文献中也会用“核”
这个词来表示这里所说的“滤波器”。
现在来解释一下图 7-3 的卷积运算的例子中都进行了什么样的计算。图 7-4
中展示了卷积运算的计算顺序。
对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用。这里所
说的窗口是指图 7-4 中灰色的 3 × 3 的部分。如图 7-4 所示,将各个位置上滤
波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积
累加运算)。然后,将这个结果保存到输出的对应位置。将这个过程在所有
位置都进行一遍,就可以得到卷积运算的输出。
在全连接的神经网络中,除了权重参数,还存在偏置。CNN 中,滤波
器的参数就对应之前的权重。并且,CNN 中也存在偏置。图 7-3 的卷积运算
的例子只展示到了应用滤波器的阶段。包含偏置的卷积运算的处理流如图 7-5
所示。
如图 7-5 所示,向应用了滤波器的数据加上了偏置。偏置通常只有 1 个
(1 × 1)
(本例中,相对于应用了滤波器的 4 个数据,偏置只有 1 个),这个值
会被加到应用了滤波器的所有元素上。
7.2 卷积层 205
11 2
2 3
3 0
2 0 1
00 11 22 3 15
15
0 1 2
3
3 0
0 1
1 2
1 0 2
2 3 0 1
1 2
2 3
3 0
0
2 0 1
0 1
1 2
2 3
3 15 16
16
0 1 2
3 0
0 1
1 2
2
1 0 2
2 3 0 1
1 2 3 0
2 0 1
0
0 1
1 2
2 3 15 16
0 1 2
3
3 0
0 1
1 2 6
6
1 0 2
2
2 3
3 0
0 1
1 2 3 0
2 0 1
0 1
1 2
2 3
3 15 16
0 1 2
3 0
0 1
1 2
2 6 15
15
1 0 2
2 3
3 0
0 1
1
图 7-4 卷积运算的计算顺序
206 第 7 章 卷积神经网络
1 2 3 0
2 0 1
0 1 2 3 15 16 18 19
0 1 2 + 3
3 0 1 2 6 15 9 18
1 0 2
2 3 0 1
输入数据 滤波器(权重) 偏置 输出数据
图 7-5 卷积运算的偏置:向应用了滤波器的元素加上某个固定值(偏置)
7.2.3 填充
在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比
如 0 等),这称为填充(padding)
,是卷积运算中经常会用到的处理。比如,
在图 7-6 的例子中,对大小为 (4, 4) 的输入数据应用了幅度为 1 的填充。“幅
度为 1 的填充”是指用幅度为 1 像素的 0 填充周围。
1 2 3 0 7 12 10 2
2 0 1
0 1 2 3 4 15 16 10
0 1 2
3 0 1 2 10 6 15 6
1 0 2
2 3 0 1 8 10 4 3
图 7-6 卷积运算的填充处理:向输入数据的周围填入 0(图中用虚线表示填充,并省略了
填充的内容“0”)
7.2.4 步幅
应用滤波器的位置间隔称为步幅(stride)。之前的例子中步幅都是 1,如
果将步幅设为 2,则如图 7-7 所示,应用滤波器的窗口的间隔变为 2 个元素。
1 2 3 0 1 2 3
0 1 2 3 0 1 2
3 0 1 2 3 0 1
2 0 1 15
2 3 0 1 2 3 0
0 1 2
1 2 3 0 1 2 3
1 0 2
0 1 2 3 0 1 2
3 0 1 2 3 0 1
步幅: 2
1 2 3 0 1 2 3
0 1 2 3 0 1 2
3 0 1 2 3 0 1
2 0 1 15 17
2 3 0 1 2 3 0
0 1 2
1 2 3 0 1 2 3
1 0 2
0 1 2 3 0 1 2
3 0 1 2 3 0 1
图 7-7 步幅为 2 的卷积运算的例子
208 第 7 章 卷积神经网络
(7.1)
现在,我们使用这个算式,试着做几个计算。
例3
输入大小:(28, 31);填充:2;步幅:3;滤波器大小:(5, 5)
7.2 卷积层 209
如这些例子所示,通过在式(7.1)中代入值,就可以计算输出大小。这
里需要注意的是,虽然只要代入值就可以计算输出大小,但是所设定的值必
须使式(7.1)中的 和 分别可以除尽。当输出大小无法
除尽时(结果是小数时),需要采取报错等对策。顺便说一下,根据深度学习
的框架的不同,当值无法除尽时,有时会向最接近的整数四舍五入,不进行
报错而继续运行。
7.2.5 3 维数据的卷积运算
之前的卷积运算的例子都是以有高、长方向的 2 维形状为对象的。但是,
图像是 3 维数据,除了高、长方向之外,还需要处理通道方向。这里,我们按
照与之前相同的顺序,看一下对加上了通道方向的3 维数据进行卷积运算的例子。
图 7-8 是卷积运算的例子,图 7-9 是计算顺序。这里以 3 通道的数据为例,
展示了卷积运算的结果。和 2 维数据时(图 7-3 的例子)相比,可以发现纵深
方向(通道方向)上特征图增加了。通道方向上有多个特征图时,会按通道
进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出。
4 2 1 2
3 50 26 25 4 0 2
4
0 01 13 0
1 2 262 312 013 2 2 10 11 2 63 55
0 1 1*1 231 300 5 2 0 2 18 51
0 31 02 0
3 00 31 02 1 1 0 2
2 3 0 1
输入数据 滤波器 输出数据
图 7-8 对 3 维数据进行卷积运算的例子
210 第 7 章 卷积神经网络
4 2 1 2
3 50 26 25 4 4 0 2
1 22 23 20 3 0 01 13 0
6 1 1 2 2 10 11 2 63
0 11 12 13 0 2 0 2
* 3 0 5 0 31 02 0
3 00 31 02 1
2 3 0 1 1 0 2
4 2 1 2
3 50 26 25 4 0 2
4
1 0 01 13 0
22 623 120 13 63 55
2 2 10 11 2
0 11 *12 313 00 2 0 2
5 0 31 02 0
3 00 31 02 1
1 0 2
2 3 0 1
4 2 1 2
3 50 26 25 4 0 2
4
1 22 623 120 13 0 01 13 0
2 2 10 11 2 63 55
0 11 *12 313 00 2 0 2
5 0 31 02 0 18
3 00 31 02 1
2 3 0 1 1 0 2
4 2 1 2
3 50 26 25 4 0 2
4
1 22 23 20 3 0 01 13 0
6 1 1 2 63 55
2 10 11 2
0 11 12 13 0 2 0 2 18 51
* 3 0 5 0 31 02 0
3 00 31 02 1
2 3 0 1 1 0 2
图 7-9 对 3 维数据进行卷积运算的计算顺序
7.2 卷积层 211
需要注意的是,在 3 维数据的卷积运算中,输入数据和滤波器的通道数
要设为相同的值。在这个例子中,输入数据和滤波器的通道数一致,均为 3。
滤波器大小可以设定为任意值(不过,每个通道的滤波器大小要全部相同)。
这个例子中滤波器大小为 (3, 3),但也可以设定为 (2, 2)、(1, 1)、(5, 5) 等任
意值。再强调一下,通道数只能设定为和输入数据的通道数相同的值(本例
中为 3)。
7.2.6 结合方块思考
将数据和滤波器结合长方体的方块来考虑,3 维数据的卷积运算会很
容易理解。方块是如图 7-10 所示的 3 维长方体。把 3 维数据表示为多维数组
时,书 写 顺 序 为(channel, height, width)。比 如,通 道 数 为 C、高 度 为 H、
长度为 W 的数据的形状可以写成(C, H, W)。滤波器也一样,要按(channel,
height, width)的顺序书写。比如,通道数为 C、滤波器高度为 FH(Filter
Height)、长度为 FW(Filter Width)时,可以写成(C, FH, FW)。
OH
H FH
FW
OW
W
图 7-10 结合方块思考卷积运算。请注意方块的形状
的输出,该怎么做呢?为此,就需要用到多个滤波器(权重)。用图表示的话,
如图 7-11 所示。
FW
C
FN个
FH
FN
C
OH
H
OW
W
图 7-11 基于多个滤波器的卷积运算的例子
FW
C
FN个
FH FN
FN
C
FN
OH + OH
H 1
1
OW OW
W
(C, H, W) (FN, C, FH, FW) (FN, OH, OW) + (FN, 1, 1) (FN, OH, OW)
输入数据 滤波器 偏置 输出数据
图 7-12 卷积运算的处理流(追加了偏置项)
7.2.7 批处理
神经网络的处理中进行了将输入数据打包的批处理。之前的全连接神经
网络的实现也对应了批处理,通过批处理,能够实现处理的高效化和学习时
对 mini-batch 的对应。
我们希望卷积运算也同样对应批处理。为此,需要将在各层间传递的数
据保存为 4 维数据。具体地讲,就是按 (batch_num, channel, height, width)
的顺序保存数据。比如,将图 7-12 中的处理改成对 N 个数据进行批处理时,
数据的形状如图 7-13 所示。
图 7-13 的批处理版的数据流中,在各个数据的开头添加了批用的维度。
像这样,数据作为 4 维的形状在各层间传递。这里需要注意的是,网络间传
递的是 4 维数据,对这 N 个数据进行了卷积运算。也就是说,批处理将 N 次
的处理汇总成了 1 次进行。
FW
C
FN 个
FH FN FN
C
FN
H OH + OH
* 1
1
W OW OW
N 个数据 N 个数据 N 个数据
(N, C, H, W)
输入数据 * (FN, 滤波器
C, FH, FW) (N, FN, OH, OW) + (FN, 1, 1)
偏置
(N, FN, OH, OW)
输出数据
图 7-13 卷积运算的处理流(批处理)
214 第 7 章 卷积神经网络
7.3 池化层
1 2 1 0 1 2 1 0
0 1 2 3 2 0 1 2 3 2 3
3 0 1 2 3 0 1 2 4’
2 4 0 1 2 3
4 0 1
1 2 13 0 1 2 1 0
0 1 2 3 2 1’
3 0 1 2 3 2 3
3 0 1 2 3 0 1 2 4 2
2 4 0 1 2 4 0 1
图 7-14 Max 池化的处理顺序
池化层的特征
池化层有以下特征。
没有要学习的参数
池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最
大值(或者平均值),所以不存在要学习的参数。
通道数不发生变化
经过池化运算,输入数据和输出数据的通道数不会发生变化。如图 7-15
所示,计算是按通道独立进行的。
4 2 1 2
3 0 6 5
5 2 2 4 4 4
1 2 1 0
2 2 2 3 3
6 1 1 2 06 5
0 1 2 3 2 3
1 1 1 0 0 2
( 3 0 5
3 00 31 02 1 4 2
2 4 0 1
输入数据 输出数据
图 7-15 池化中通道数不变
对微小的位置变化具有鲁棒性(健壮)
输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对
输入数据的微小偏差具有鲁棒性。比如,3 × 3 的池化的情况下,如图
7-16 所示,池化会吸收输入数据的偏差(根据数据的不同,结果有可
能不一致)。
216 第 7 章 卷积神经网络
11 22 0 7 1 0 1’ 11 22 0 7 1
00 91 22 3 2 3 33 00 91 22 3 2
33 00 11 2 1 2 1’
9 7 22 33 00 11 2 1 1’
9 7
2 4 0 1 0 1 6 8 3 2 4 0 1 0 6 8
6 0 1 2 1 2 2 6 0 1 2 1
2 4 0 1 8 1 1 2 4 0 1 8
图 7-16 输入数据在宽度方向上只偏离 1 个元素时,输出仍为相同的结果(根据数据的不同,
有时结果也不相同)
7.4 卷积层和池化层的实现
7.4.1 4 维数组
im2col
输入数据
图 7-17 im2col 的示意图
图 7-18 将滤波器的应用区域从头开始依次横向展开为 1 列
在图 7-18 中,为了便于观察,将步幅设置得很大,以使滤波器的应用区
域不重叠。而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在
滤波器的应用区域重叠的情况下,使用 im2col 展开后,展开后的元素个数会
多于原方块的元素个数。因此,使用 im2col 的实现存在比普通的实现消耗更
多内存的缺点。但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有
益处。比如,在矩阵计算的库(线性代数库)等中,矩阵计算的实现已被高
度最优化,可以高速地进行大矩阵的乘法运算。因此,通过归结到矩阵计算
上,可以有效地利用线性代数库。
im2col 这个名称是“image to column”的缩写,翻译过来就是“从
图 像 到 矩 阵”的 意 思。Caffe、Chainer 等 深 度 学 习 框 架 中 有 名 为
im2col 的函数,并且在卷积层的实现中,都使用了 im2col。
使用 im2col 展开输入数据后,之后就只需将卷积层的滤波器(权重)纵
向展开为 1 列,并计算 2 个矩阵的乘积即可(参照图 7-19)。这和全连接层的
Affine 层进行的处理基本相同。
如图 7-19 所示,基于 im2col 方式的输出结果是 2 维矩阵。因为 CNN 中
数据会保存为 4 维数组,所以要将 2 维输出数据转换为合适的形状。以上就
是卷积层的实现流程。
7.4 卷积层和池化层的实现 219
输入数据
滤波器
im2col
reshape
矩阵的乘积
输出数据
输出数据(2 维)
7.4.3 卷积层的实现
• filter_h ―滤波器的高
• filter_w ―滤波器的长
• stride ―步幅
• pad ―填充
220 第 7 章 卷积神经网络
我们来实际使用一下这个 im2col。
import sys, os
sys.path.append(os.pardir)
from common.util import im2col
x1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9, 75)
x2 = np.random.rand(10, 3, 7, 7) # 10 个数据
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90, 75)
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
return out
7.4 卷积层和池化层的实现 221
卷积层的初始化方法将滤波器(权重)
、偏置、步幅、填充作为参数接收。
滤 波 器 是 (FN, C, FH, FW) 的 4 维 形 状。另 外,FN、C、FH、FW 分 别 是 Filter
动计算 -1 维度上的元素个数,以使多维数组的元素个数前后一致。比如,
(10, 3, 5, 5) 形状的数组的元素个数共有 750 个,指定 reshape(10,-1) 后,就
会转换成 (10, 75) 形状的数组。
forward 的实现中,最后会将输出大小转换为合适的形状。转换时使用了
索引 0, 1, 2, 3 0, 3, 1, 2
7.4.4 池化层的实现
1 2 0 1
3 0 2 4
1 通道
1 0 3 2
4 2 1 2
4 2 0 1
3 0 6 5
5 2 2 4
1 22 3 0 4 2
23 20 3
6 1 1 2
0 11 2 4 6 5 4 3
(1 31 00 5 2 通道
1 00 3 0 2 3
34 02 1
3 2 0 1 1 0 3 1
输入数据
4 2 0 1
1 2 0 4
3 通道
3 0 4 2
6 2 4 5
图 7-21 对输入数据展开池化的应用区域(2×2 的池化的例子)
像这样展开之后,只需对展开的矩阵求各行的最大值,并转换为合适的
形状即可(图 7-22)。
7.4 卷积层和池化层的实现 223
1 2 0 1 2
3 0 2 4 4
1 0 3 2 3
4 2 1 2 4 2 0 1 4
3 50 26 25 4 4
4 3 0 4 2 4
1 22 623 120 13 展开 max reshape 4 46 6
2 6 5 4 3 6 2 34 3
0 11 (12 314 00 5 3 0 2 3 3 3 4
1 00 34 02 1
3 2 0 1 1 0 3 1 3
输出数据
4 2 0 1 4
输入数据
1 2 0 4 4
3 0 4 2 4
6 2 4 5 6
图 7-22 池化层的实现流程:池化的应用区域内的最大值元素用灰色表示
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
# 展开 (1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
# 最大值 (2)
out = np.max(col, axis=1)
# 转换 (3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
1. 展开输入数据。
2. 求各行的最大值。
3. 转换为合适的输出大小。
各阶段的实现都很简单,只有一两行代码。
我们已经实现了卷积层和池化层,现在来组合这些层,搭建进行手写数
字识别的 CNN。这里要实现如图 7-23 所示的 CNN。
参数
• input_dim ―输入数据的维度:
(通道,高,长)
• conv_param ―卷积层的超参数(字典)。字典的关键字如下:
filter_num ―滤波器的数量
filter_size ―滤波器的大小
stride ―步幅
pad ―填充
• hidden_size ―隐藏层(全连接)的神经元数量
• output_size ―输出层(全连接)的神经元数量
• weitght_int_std ―初始化时权重的标准差
的超参数值。
SimpleConvNet 的初始化的实现稍长,我们分成 3 部分来说明,首先是初
始化的最开始部分。
class SimpleConvNet:
def __init__(self, input_dim=(1, 28, 28),
conv_param={'filter_num':30, 'filter_size':5,
'pad':0, 'stride':1},
hidden_size=100, output_size=10, weight_init_std=0.01):
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / \
filter_stride + 1
pool_output_size = int(filter_num * (conv_output_size/2) *
(conv_output_size/2))
226 第 7 章 卷积神经网络
这里将由初始化参数传入的卷积层的超参数从字典中取了出来(以方便
后面使用),然后,计算卷积层的输出大小。接下来是权重参数的初始化部分。
self.params = {}
self.params['W1'] = weight_init_std * \
np.random.randn(filter_num, input_dim[0],
filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_init_std * \
np.random.randn(pool_output_size,
hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_init_std * \
np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
学习所需的参数是第 1 层的卷积层和剩余两个全连接层的权重和偏置。
将这些参数保存在实例变量的 params 字典中。将第 1 层的卷积层的权重设为
关键字 W1,偏置设为关键字 b1。同样,分别用关键字 W2、b2 和关键字 W3、b3
来保存第 2 个和第 3 个全连接层的权重和偏置。
最后,生成必要的层。
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'],
self.params['b1'],
conv_param['stride'],
conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'],
self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'],
self.params['b3'])
self.last_layer = SoftmaxWithLoss()
接下来是基于误差反向传播法求梯度的代码实现。
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'] = self.layers['Conv1'].dW
grads['b1'] = self.layers['Conv1'].db
grads['W2'] = self.layers['Affine1'].dW
grads['b2'] = self.layers['Affine1'].db
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].db
return grads
参数的梯度通过误差反向传播法(反向传播)求出,通过把正向传播和
反向传播组装在一起来完成。因为已经在各层正确实现了正向传播和反向传
播的功能,所以这里只需要以合适的顺序调用即可。最后,把各个权重参数
的梯度保存到 grads 字典中。这就是 SimpleConvNet 的实现。
CNN 中用到的卷积层在“观察”什么呢?本节将通过卷积层的可视化,
探索 CNN 中到底进行了什么处理。
7.6.1 第 1 层权重的可视化
学习前 学习后
图 7-24 学习前和学习后的第 1 层的卷积层的权重:虽然权重的元素是实数,但是在图像
的显示上,统一将最小值显示为黑色(0),最大值显示为白色(255)
滤波器 1 对垂直方向上
的边缘有响应
输出图像 1
输入图像
对水平方向上
滤波器 2 的边缘有响应
输出图像 2
图 7-25 对水平方向上和垂直方向上的边缘有响应的滤波器:输出图像 1 中,垂直方向的
边缘上出现白色像素,输出图像 2 中,水平方向的边缘上出现很多白色像素
230 第 7 章 卷积神经网络
图 7-25 中显示了选择两个学习完的滤波器对输入图像进行卷积处理时的
结果。我们发现“滤波器 1”对垂直方向上的边缘有响应,“滤波器 2”对水平
方向上的边缘有响应。
由此可知,卷积层的滤波器会提取边缘或斑块等原始信息。而刚才实现
的 CNN 会将这些原始信息传递给后面的层。
7.6.2 基于分层结构的信息提取
55
27
13 13 13
11
5 3
3 3
11
27
13
3
13 13 dense dense
224 5 3 3
384 384 256 1000
55
256 Max
pooling 4096 4096
Max Max
pooling pooling
Stride 96
224 of 4
Numerical Data-driven
dinning table
cock
groccry storc
ship
Conv 1: Edge+Blob Conv 3: Texture Conv 5: Object Parts Fc8: Object Classes
如图 7-26 所示,如果堆叠了多层卷积层,则随着层次加深,提取的信息
也愈加复杂、抽象,这是深度学习中很有意思的一个地方。最开始的层对简
单的边缘有响应,接下来的层对纹理有响应,再后面的层对更加复杂的物体
部件有响应。也就是说,随着层次加深,神经元从简单的形状向“高级”信息
变化。换句话说,就像我们理解东西的“含义”一样,响应的对象在逐渐变化。
关于 CNN,迄今为止已经提出了各种网络结构。这里,我们介绍其中
特别重要的两个网络,一个是在 1998 年首次被提出的 CNN 元祖 LeNet[20],
另一个是在深度学习受到关注的 2012 年被提出的 AlexNet[21]。
7.7.1 LeNet
7.7.2 AlexNet
fully
55 connected
27
13 13 13
11
5 3 3 3
13 13 13
224 11 27
3 3 3
5 1000
384 384 256 4096 4096
55 max
256 pooling
224 max
96 pooling
3
AlexNet 叠有多个卷积层和池化层,最后经由全连接层输出结果。虽然
结构上 AlexNet 和 LeNet 没有大的不同,但有以下几点差异。
• 激活函数使用 ReLU。
• 使用进行局部正规化的 LRN(Local Response Normalization)层。
• 使用 Dropout(6.4.3 节)。
大多数情况下,深度学习(加深了层次的网络)存在大量的参数。因此,
学习需要大量的计算,并且需要使那些参数“满意”的大量数据。可
以说是 GPU 和大数据给这些课题带来了希望。
7.8 小结
本章所学的内容
• CNN 在此前的全连接层的网络中新增了卷积层和池化层。
• 使用 im2col 函数可以简单、高效地实现卷积层和池化层。
• 通过 CNN 的可视化,可知随着层次变深,提取的信息愈加高级。
• LeNet 和 AlexNet 是 CNN 的代表性网络。
• 在深度学习的发展中,大数据和 GPU 做出了很大的贡献。
图灵社区会员 溯光(18123771246) 专享 尊重版权
第8章
深度学习
深度学习是加深了层的深度神经网络。基于之前介绍的网络,只需通过
叠加层,就可以创建深度网络。本章我们将看一下深度学习的性质、课题和
可能性,然后对当前的深度学习进行概括性的说明。
8.1 加深网络
关于神经网络,我们已经学了很多东西,比如构成神经网络的各种层、
学习时的有效技巧、对图像特别有效的 CNN、参数的最优化方法等,这些
都是深度学习中的重要技术。本节我们将这些已经学过的技术汇总起来,创
建一个深度网络,挑战 MNIST 数据集的手写数字识别。
8.1.1 向更深的网络出发
Conv ReLU Conv ReLU Pool Conv ReLU Conv ReLU Pool
Conv ReLU Conv ReLU Pool Affine ReLU Dropout Affine Dropout Softmax
图 8-1 进行手写数字识别的深度 CNN
• 基于 3×3 的小型滤波器的卷积层。
• 激活函数是 ReLU。
• 全连接层的后面使用 Dropout 层。
• 基于 Adam 的最优化。
• 使用 He 初始值作为权重初始值。
A 最终的识别精度有少许偏差,不过在这个网络中,识别精度大体上都会超过 99%。
8.1 加深网络 237
3 6 3 3 8 3 7 8 7
5 0 5 5 3 5 3 3 3
8 1 6 8 7 6 9 7 9
9 3 0 9 2 0 4 2 4
5 7 1 5 0 1 9 0 9
3 1 3 3 6 3 4 6 4
6 7 9 6 4 9 1 4 1
0 9 8 0 9 8 7 9 7
图 8-2 识别错误的图像的例子:各个图像的左上角显示了正确解标签,右下角显示了本
网络的推理结果
8.1.2 进一步提高识别精度
对于 MNIST 数据集,层不用特别深就获得了(目前)最高的识别精
度。一般认为,这是因为对于手写数字识别这样一个比较简单的任
务,没有必要将网络的表现力提高到那么高的程度。因此,可以说
加深层的好处并不大。而之后要介绍的大规模的一般物体识别的情况,
因为问题复杂,所以加深层对提高识别精度大有裨益。
参考刚才排行榜中前几名的方法,可以发现进一步提高识别精度的技术和
线索。比如,集成学习、学习率衰减、Data Augmentation(数据扩充)等都有
助于提高识别精度。尤其是Data Augmentation,虽然方法很简单,但在提高
识别精度上效果显著。
Data Augmentation 基于算法“人为地”扩充输入图像(训练图像)。具
体地说,如图 8-4 所示,对于输入图像,通过施加旋转、垂直或水平方向上
的移动等微小变化,增加图像的数量。这在数据集的图像数量有限时尤其有效。
基于旋转的变形
原图像
基于平移的变形
A flip 处理只在不需要考虑图像对称性的情况下有效。
过这个技巧的实现比较简单,有兴趣的读者请自己试一下。
8.1.3 加深层的动机
关于加深层的重要性,现状是理论研究还不够透彻。尽管目前相关理论
还比较贫乏,但是有几点可以从过往的研究和实验中得以解释(虽然有一些
直观)。本节就加深层的重要性,给出一些增补性的数据和说明。
首先,从以 ILSVRC 为代表的大规模图像识别的比赛结果中可以看出加
深层的重要性(详细内容请参考下一节)。这种比赛的结果显示,最近前几名
的方法多是基于深度学习的,并且有逐渐加深网络的层的趋势。也就是说,
可以看到层越深,识别性能也越高。
下面我们说一下加深层的好处。其中一个好处就是可以减少网络的参数
数量。说得详细一点,就是与没有加深层的网络相比,加深了层的网络可以
用更少的参数达到同等水平(或者更强)的表现力。这一点结合卷积运算中
的滤波器大小来思考就好理解了。比如,图 8-5 展示了由 5 × 5 的滤波器构成
的卷积层。
输入数据 输出数据
5×5
图 8-5 5×5 的卷积运算的例子
这里希望大家考虑一下输出数据的各个节点是从输入数据的哪个区域计
算出来的。显然,在图 8-5 的例子中,每个输出节点都是从输入数据的某个
5 × 5 的区域算出来的。接下来我们思考一下图 8-6 中重复两次 3 × 3 的卷积
运算的情形。此时,每个输出节点将由中间数据的某个 3 × 3 的区域计算出来。
那么,中间数据的 3 × 3 的区域又是由前一个输入数据的哪个区域计算出来
的呢?仔细观察图 8-6,可知它对应一个 5 × 5 的区域。也就是说,图 8-6 的
输出数据是“观察”了输入数据的某个 5 × 5 的区域后计算出来的。
3×3 3×3
一次 5 × 5 的卷积运算的区域可以由两次 3 × 3 的卷积运算抵充。并且,
相对于前者的参数数量 25(5 × 5),后者一共是 18(2 × 3 × 3),通过叠加卷
积层,参数数量减少了。而且,这个参数数量之差会随着层的加深而变大。
比如,重复三次 3 × 3 的卷积运算时,参数的数量总共是 27。而为了用一次
卷积运算“观察”与之相同的区域,需要一个 7 × 7 的滤波器,此时的参数数
量是 49。
叠加小型滤波器来加深网络的好处是可以减少参数的数量,扩大感
受野(receptive field,给神经元施加变化的某个局部空间区域)。并且,
通过叠加层,将 ReLU 等激活函数夹在卷积层的中间,进一步提高
了网络的表现力。这是因为向网络添加了基于激活函数的“非线性”
表现力,通过非线性函数的叠加,可以表现更加复杂的东西。
242 第 8 章 深度学习
加深层的另一个好处就是使学习更加高效。与没有加深层的网络相比,
通过加深层,可以减少学习数据,从而高效地进行学习。为了直观地理解这
一点,大家可以回忆一下 7.6 节的内容。7.6 节中介绍了 CNN 的卷积层会分
层次地提取信息。具体地说,在前面的卷积层中,神经元会对边缘等简单的
形状有响应,随着层的加深,开始对纹理、物体部件等更加复杂的东西有响应。
我们先牢记这个网络的分层结构,然后考虑一下“狗”的识别问题。要
用浅层网络解决这个问题的话,卷积层需要一下子理解很多“狗”的特征。“狗”
有各种各样的种类,根据拍摄环境的不同,外观变化也很大。因此,要理解“狗”
的特征,需要大量富有差异性的学习数据,而这会导致学习需要花费很多时间。
不过,通过加深网络,就可以分层次地分解需要学习的问题。因此,各
层需要学习的问题就变成了更简单的问题。比如,最开始的层只要专注于学
习边缘就好,这样一来,只需用较少的学习数据就可以高效地进行学习。这
是为什么呢?因为和印有“狗”的照片相比,包含边缘的图像数量众多,并
且边缘的模式比“狗”的模式结构更简单。
通过加深层,可以分层次地传递信息,这一点也很重要。比如,因为提
取了边缘的层的下一层能够使用边缘的信息,所以应该能够高效地学习更加
高级的模式。也就是说,通过加深层,可以将各层要学习的问题分解成容易
解决的简单问题,从而可以进行高效的学习。
以上就是对加深层的重要性的増补性说明。不过,这里需要注意的是,
近几年的深层化是由大数据、计算能力等即便加深层也能正确地进行学习的
新技术和环境支撑的。
8.2 深度学习的小历史
袭成为一个转折点,在之后的比赛中,深度学习一直活跃在舞台中央。本节
我们以 ILSVRC 这个大规模图像识别比赛为轴,看一下深度学习最近的发展
趋势。
8.2.1 ImageNet
ILSVRC 大赛有多个测试项目,其中之一是“类别分类”
(classification),
在该项目中,会进行 1000 个类别的分类,比试识别精度。我们来看一下最
近几年的 ILSVRC 大赛的类别分类项目的结果。图 8-8 中展示了从 2010 年到
2015 年的优胜队伍的成绩。这里,将前 5 类中出现正确解的情况视为“正确”,
此时的错误识别率用柱形图来表示。
图 8-8 中需要注意的是,以 2012 年为界,之后基于深度学习的方法一直
居于首位。实际上,我们发现 2012 年的 AlexNet 大幅降低了错误识别率。并且,
此后基于深度学习的方法不断在提升识别精度。特别是 2015 年的 ResNet(一
个超过 150 层的深度网络)将错误识别率降低到了 3.5%。据说这个结果甚至
超过了普通人的识别能力。
这些年深度学习取得了不斐的成绩,其中 VGG、GoogLeNet、ResNet
244 第 8 章 深度学习
已广为人知,在与深度学习有关的各种场合都会遇到这些网络。下面我们就
来简单地介绍一下这 3 个有名的网络。
图 8-8 ILSCRV 优胜队伍的成绩演变:竖轴是错误识别率,横轴是年份。横轴的括号内
是队伍名或者方法名
8.2.2 VGG
224 × 224
fully
connected
112 × 112
56 × 56
28 × 28
14 × 14
7×7
3
3
1000
4096 4096
8.2.3 GoogLeNet
图 8-10 GoogLeNet(引用自文献 [23])
只看图的话,这似乎是一个看上去非常复杂的网络结构,但实际上它基
本上和之前介绍的 CNN 结构相同。不过,GoogLeNet 的特征是,网络不仅
在纵向上有深度,在横向上也有深度(广度)。
GoogLeNet 在横向上有“宽度”,这称为“Inception 结构”,以图 8-11 所
示的结构为基础。
如图 8-11 所示,Inception 结构使用了多个大小不同的滤波器(和池化),
最后再合并它们的结果。GoogLeNet 的特征就是将这个 Inception 结构用作
一个构件(构成元素)。此外,在 GoogLeNet 中,很多地方都使用了大小为
246 第 8 章 深度学习
1 × 1 的滤波器的卷积层。这个 1 × 1 的卷积运算通过在通道方向上减小大小,
有助于减少参数和实现高速化处理(具体请参考原始论文 [23])。
8.2.4 ResNet
ResNet[24] 是微软团队开发的网络。它的特征在于具有比以前的网络更
深的结构。
我们已经知道加深层对于提升性能很重要。但是,在深度学习中,过度
加深层的话,很多情况下学习将不能顺利进行,导致最终性能不佳。ResNet 中,
为了解决这类问题,导入了“快捷结构”
(也称为“捷径”或“小路”)。导入这
个快捷结构后,就可以随着层的加深而不断提高性能了(当然,层的加深也
是有限度的)。
如图 8-12 所示,快捷结构横跨(跳过)了输入数据的卷积层,将输入 x 合
计到输出。
图 8-12 中,在连续 2 层的卷积层中,将输入 x 跳着连接至 2 层后的输出。
这里的重点是,通过快捷结构,原来的 2 层卷积层的输出 F(x) 变成了 F(x) + x。
通过引入这种快捷结构,即使加深层,也能高效地学习。这是因为,通过快
捷结构,反向传播时信号可以无衰减地传递。
8.2 深度学习的小历史 247
weight layer
F(x) relu
x
weight layer
identity
F(x) + x
relu
因为快捷结构只是原封不动地传递输入数据,所以反向传播时会将
来自上游的梯度原封不动地传向下游。这里的重点是不对来自上游
的梯度进行任何处理,将其原封不动地传向下游。因此,基于快捷
结构,不用担心梯度会变小(或变大),能够向前一层传递“有意义
的梯度”。通过这个快捷结构,之前因为加深层而导致的梯度变小的
梯度消失问题就有望得到缓解。
8.3 深度学习的高速化
随着大数据和网络的大规模化,深度学习需要进行大量的运算。虽然到
目前为止,我们都是使用 CPU 进行计算的,但现实是只用 CPU 来应对深度
学习无法令人放心。实际上,环视一下周围,大多数深度学习的框架都支持
GPU(Graphics Processing Unit),可以高速地处理大量的运算。另外,最
近的框架也开始支持多个 GPU 或多台机器上的分布式学习。本节我们将焦
点放在深度学习的计算的高速化上,然后逐步展开。深度学习的实现在 8.1
节就结束了,本节要讨论的高速化(支持 GPU 等)并不进行实现。
8.3.1 需要努力解决的问题
在介绍深度学习的高速化之前,我们先来看一下深度学习中什么样的处
理比较耗时。图 8-14 中以 AlexNet 的 forward 处理为对象,用饼图展示了各
层所耗费的时间。
从图中可知,AlexNet 中,大多数时间都被耗费在卷积层上。实际上,
卷积层的处理时间加起来占 GPU 整体的 95%,占 CPU 整体的 89% !因此,
如何高速、高效地进行卷积层中的运算是深度学习的一大课题。虽然图 8-14
是推理时的结果,不过学习时也一样,卷积层中会耗费大量时间。
正如 7.2 节介绍的那样,卷积层中进行的运算可以追溯至乘积累加运算。
因此,深度学习的高速化的主要课题就变成了如何高速、高效地进
行大量的乘积累加运算。
8.3 深度学习的高速化 249
GPU 原本是作为图像专用的显卡使用的,但最近不仅用于图像处理,
也用于通用的数值计算。由于 GPU 可以高速地进行并行数值计算,因此
GPU 计算的目标就是将这种压倒性的计算能力用于各种用途。所谓 GPU 计算,
是指基于 GPU 进行通用的数值计算的操作。
深度学习中需要进行大量的乘积累加运算(或者大型矩阵的乘积运算)
。
这种大量的并行运算正是 GPU 所擅长的(反过来说,CPU 比较擅长连续的、
复杂的计算)
。因此,与使用单个 CPU 相比,使用 GPU 进行深度学习的运算
可以达到惊人的高速化。下面我们就来看一下基于 GPU 可以实现多大程度的
高速化。图 8-15 是基于 CPU 和 GPU 进行 AlexNet 的学习时分别所需的时间。
从图中可知,使用 CPU 要花 40 天以上的时间,而使用 GPU 则可以将时
间缩短至 6 天。此外,还可以看出,通过使用 cuDNN 这个最优化的库,可
以进一步实现高速化。
GPU 主要由 NVIDIA 和 AMD 两家公司提供。虽然两家的 GPU 都可以
用于通用的数值计算,但与深度学习比较“亲近”的是 NVIDIA 的 GPU。实
际上,大多数深度学习框架只受益于 NVIDIA 的 GPU。这是因为深度学习
的框架中使用了 NVIDIA 提供的 CUDA 这个面向 GPU 计算的综合开发环境。
通过 im2col 可以将卷积层进行的运算转换为大型矩阵的乘积。这个
im2col 方式的实现对 GPU 来说是非常方便的实现方式。这是因为,
相比按小规模的单位进行计算,GPU 更擅长计算大规模的汇总好的
数据。也就是说,通过基于 im2col 以大型矩阵的乘积的方式汇总计算,
更容易发挥出 GPU 的能力。
8.3.3 分布式学习
者多台机器上进行分布式计算。现在的深度学习框架中,出现了好几个支持
多 GPU 或者多机器的分布式学习的框架。其中,Google 的 TensorFlow、微
软的 CNTK(Computational Network Toolki)在开发过程中高度重视分布式
学习。以大型数据中心的低延迟·高吞吐网络作为支撑,基于这些框架的分
布式学习呈现出惊人的效果。
基于分布式学习,可以达到何种程度的高速化呢?图 8-16 中显示了基于
TensorFlow 的分布式学习的效果。
8.3.4 运算精度的位数缩减
在深度学习的高速化中,除了计算量之外,内存容量、总线带宽等也有
可能成为瓶颈。关于内存容量,需要考虑将大量的权重参数或中间数据放在
内存中。关于总线带宽,当流经 GPU(或者 CPU)总线的数据超过某个限制时,
就会成为瓶颈。考虑到这些情况,我们希望尽可能减少流经网络的数据的位数。
计算机中为了表示实数,主要使用 64 位或者 32 位的浮点数。通过使用
较多的位来表示数字,虽然数值计算时的误差造成的影响变小了,但计算的
处理成本、内存使用量却相应地增加了,还给总线带宽带来了负荷。
关于数值精度(用几位数据表示数值),我们已经知道深度学习并不那么
需要数值精度的位数。这是神经网络的一个重要性质。这个性质是基于神经
网络的健壮性而产生的。这里所说的健壮性是指,比如,即便输入图像附有
一些小的噪声,输出结果也仍然保持不变。可以认为,正是因为有了这个健
壮性,流经网络的数据即便有所“劣化”,对输出结果的影响也较小。
计算机中表示小数时,有 32 位的单精度浮点数和 64 位的双精度浮点数
等格式。根据以往的实验结果,在深度学习中,即便是 16 位的半精度浮点
数(half float),也可以顺利地进行学习 [30]。实际上,NVIDIA 的下一代 GPU
框架 Pascal 也支持半精度浮点数的运算,由此可以认为今后半精度浮点数将
被作为标准使用。
关于深度学习的位数缩减,到目前为止已有若干研究。最近有人提出了
用 1 位来表示权重和中间数据的 Binarized Neural Networks 方法 [31]。为了实
现深度学习的高速化,位数缩减是今后必须关注的一个课题,特别是在面向
嵌入式应用程序中使用深度学习时,位数缩减非常重要。
8.4 深度学习的应用案例
前面,作为使用深度学习的例子,我们主要讨论了手写数字识别的图
像类别分类问题(称为“物体识别”)。不过,深度学习并不局限于物体识别,
还可以应用于各种各样的问题。此外,在图像、语音、自然语言等各个不同
的领域,深度学习都展现了优异的性能。本节将以计算机视觉这个领域为中
心,介绍几个深度学习能做的事情(应用)。
8.4.1 物体检测
物体检测是从图像中确定物体的位置,并进行分类的问题。如图 8-17 所
示,要从图像中确定物体的种类和物体的位置。
图 8-17 物体检测的例子(引用自文献 [34])
254 第 8 章 深度学习
8.4.2 图像分割
图 8-19 图像分割的例子(引用自文献 [34]):左边是输入图像,右边是监督用的带标签图像
之前实现的神经网络是对图像整体进行了分类,要将它落实到像素水平
的话,该怎么做呢?
要基于神经网络进行图像分割,最简单的方法是以所有像素为对象,对
每个像素执行推理处理。比如,准备一个对某个矩形区域中心的像素进行分
类的网络,以所有像素为对象执行推理处理。正如大家能想到的,这样的
方法需要按照像素数量进行相应次 forward 处理,因而需要耗费大量的时间
(正确地说,卷积运算中会发生重复计算很多区域的无意义的计算)。为了解
决这个无意义的计算问题,有人提出了一个名为 FCN(Fully Convolutional
Network)[37] 的方法。该方法通过一次 forward 处理,对所有像素进行分类(图
8-20)。
FCN 的字面意思是“全部由卷积层构成的网络”。相对于一般的 CNN 包
含全连接层,FCN 将全连接层替换成发挥相同作用的卷积层。在物体识别
中使用的网络的全连接层中,中间数据的空间容量被作为排成一列的节点进
256 第 8 章 深度学习
行处理,而只由卷积层构成的网络中,空间容量可以保持原样直到最后的输出。
如图 8-20 所示,FCN 的特征在于最后导入了扩大空间大小的处理。基
于这个处理,变小了的中间数据可以一下子扩大到和输入图像一样的大小。
FCN 最后进行的扩大处理是基于双线性插值法的扩大(双线性插值扩大)。
FCN 中,这个双线性插值扩大是通过去卷积(逆卷积运算)来实现的(细节请
参考 FCN 的论文 [37])。
全连接层中,输出和全部的输入相连。使用卷积层也可以实现与此
结构完全相同的连接。比如,针对输入大小是 32×10×10(通道
数 32、高 10、长 10)的数据的全连接层可以替换成滤波器大小为
32×10×10 的卷积层。如果全连接层的输出节点数是 100,那么在
卷积层准备 100 个 32×10×10 的滤波器就可以实现完全相同的处理。
像这样,全连接层可以替换成进行相同处理的卷积层。
8.4.3 图像标题的生成
有一项融合了计算机视觉和自然语言的有趣的研究,该研究如图 8-21 所
示,给出一个图像后,会自动生成介绍这个图像的文字(图像的标题)。
给出一个图像后,会像图 8-21 一样自动生成表示该图像内容的文本。比
如,左 上 角 的 第 一 幅 图 像 生 成 了 文 本“A person riding a motorcycle on a
8.4 深度学习的应用案例 257
图 8-21 基于深度学习的图像标题生成的例子(引用自文献 [38])
(在没有铺装的道路上骑摩托车的人),而且这个文本只从该图像
dirt road.”
自动生成。文本的内容和图像确实是一致的。并且,令人惊讶的是,除了“骑
摩托车”之外,连“没有铺装的道路”都被正确理解了。
一个基于深度学习生成图像标题的代表性方法是被称为 NIC(Neural
Image Caption)的模型。如图 8-22 所示,NIC 由深层的 CNN 和处理自然语
言的 RNN(Recurrent Neural Network)构成。RNN 是具有循环连接的网络,
经常被用于自然语言、时间序列数据等连续性的数据上。
NIC 基于 CNN 从图像中提取特征,并将这个特征传给 RNN。RNN 以
CNN 提取出的特征为初始值,循环地生成文本。这里,我们不深入讨论技
术上的细节,不过基本上 NIC 是组合了两个神经网络(CNN 和 RNN)的简单
结构。基于 NIC,可以生成惊人的高精度的图像标题。我们将组合图像和自
然语言等多种信息进行的处理称为多模态处理。多模态处理是近年来备受关
注的一个领域。
258 第 8 章 深度学习
RNN 的 R 表示 Recurrent(循环的)。这个循环指的是神经网络的循环
的网络结构。根据这个循环结构,神经网络会受到之前生成的信息
的影响(换句话说,会记忆过去的信息),这是 RNN 的特征。比如,
生成“我”这个词之后,下一个要生成的词受到“我”这个词的影响,
生成了“要”;然后,再受到前面生成的“我要”的影响,生成了“睡觉”
这个词。对于自然语言、时间序列数据等连续性的数据,RNN 以记
忆过去的信息的方式运行。
8.5 深度学习的未来
深度学习已经不再局限于以往的领域,开始逐渐应用于各个领域。本节
将介绍几个揭示了深度学习的可能性和未来的研究。
8.5.1 图像风格变换
Artistic Style”[39],一经发表就受到全世界的广泛关注。
这里我们不会介绍这项研究的详细内容,只是叙述一下这个技术的大致
框架,即刚才的方法是在学习过程中使网络的中间数据近似内容图像的中间
数据。这样一来,就可以使输入图像近似内容图像的形状。此外,为了从风
格图像中吸收风格,导入了风格矩阵的概念。通过在学习过程中减小风格矩
阵的偏差,就可以使输入图像接近梵高的风格。
8.5.2 图像的生成
刚才的图像风格变换的例子在生成新的图像时输入了两个图像。不同于
这种研究,现在有一种研究是生成新的图像时不需要任何图像(虽然需要事
260 第 8 章 深度学习
先使用大量的图像进行学习,但在“画”新图像时不需要任何图像)。比如,
基于深度学习,可以实现从零生成“卧室”的图像。图 8-24 中展示的图像是
基 于 DCGAN(Deep Convolutional Generative Adversarial Network)[41] 方
法生成的卧室图像的例子。
图 8-24 的图像可能看上去像是真的照片,但其实这些图像都是基于
DCGAN 新生成的图像。也就是说,DCGAN 生成的图像是谁都没有见过的
图像(学习数据中没有的图像),是从零生成的新图像。
能画出以假乱真的图像的 DCGAN 会将图像的生成过程模型化。使用大
量图像(比如,印有卧室的大量图像)训练这个模型,学习结束后,使用这
个模型,就可以生成新的图像。
DCGAN 中使用了深度学习,其技术要点是使用了 Generator(生成者)
和 Discriminator(识别者)这两个神经网络。Generator 生成近似真品的图
像,Discriminator 判别它是不是真图像(是 Generator 生成的图像还是实际
拍摄的图像)。像这样,通过让两者以竞争的方式学习,Generator 会学习到
更加精妙的图像作假技术,Discriminator 则会成长为能以更高精度辨别真假
的鉴定师。两者互相切磋、共同成长,这是 GAN(Generative Adversarial
8.5 深度学习的未来 261
Network)这个技术的有趣之处。在这样的切磋中成长起来的 Generator 最终
会掌握画出足以以假乱真的图像的能力(或者说有这样的可能)。
之 前 我 们 见 到 的 机 器 学 习 问 题 都 是 被 称 为 监 督 学 习(supervised
learning)的问题。这类问题就像手写数字识别一样,使用的是图像
数据和监督标签成对给出的数据集。不过这里讨论的问题,并没有
给出监督数据,只给了大量的图像(图像的集合),这样的问题称为
无监督学习(unsupervised learning)。无监督学习虽然是很早之前就
开始研究的领域(Deep Belief Network、Deep Boltzmann Machine
等很有名),但最近似乎并不是很活跃。今后,随着使用深度学习的
DCGAN 等方法受到关注,无监督学习有望得到进一步发展。
8.5.3 自动驾驶
计算机代替人类驾驶汽车的自动驾驶技术有望得到实现。除了汽车制造
商之外,IT 企业、大学、研究机构等也都在为实现自动驾驶而进行着激烈
的竞争。自动驾驶需要结合各种技术的力量来实现,比如决定行驶路线的路
线计划(path plan)技术、照相机或激光等传感技术等,在这些技术中,正
确识别周围环境的技术据说尤其重要。这是因为要正确识别时刻变化的环境、
自由来往的车辆和行人是非常困难的。
如果可以在各种环境中稳健地正确识别行驶区域的话,实现自动驾驶可
能也就没那么遥远了。最近,在识别周围环境的技术中,深度学习的力量备
受期待。比如,基于 CNN 的神经网络 SegNet[42],可以像图 8-25 那样高精度
地识别行驶环境。
图 8-25 中对输入图像进行了分割(像素水平的判别)。观察结果可知,在
某种程度上正确地识别了道路、建筑物、人行道、树木、车辆等。今后若能
基于深度学习使这种技术进一步实现高精度化、高速化的话,自动驾驶的实
用化可能也就没那么遥远了。
262 第 8 章 深度学习
图 8-25 基于深度学习的图像分割的例子:道路、车辆、建筑物、人行道等被高精度地识
别了出来(引用自文献 [43])
8.5.4 Deep Q-Network(强化学习)
就像人类通过摸索试验来学习一样(比如骑自行车),让计算机也在摸索
试验的过程中自主学习,这称为强化学习(reinforcement learning)。强化学
习和有“教师”在身边教的“监督学习”有所不同。
强化学习的基本框架是,代理(Agent)根据环境选择行动,然后通过这
个行动改变环境。根据环境的变化,代理获得某种报酬。强化学习的目的是
决定代理的行动方针,以获得更好的报酬(图 8-26)。
图 8-26 中展示了强化学习的基本框架。这里需要注意的是,报酬并不是
确定的,只是“预期报酬”。比如,在《超级马里奥兄弟》这款电子游戏中,
让马里奥向右移动能获得多少报酬不一定是明确的。这时需要从游戏得分(获
得的硬币、消灭的敌人等)或者游戏结束等明确的指标来反向计算,决定“预
期报酬”。如果是监督学习的话,每个行动都可以从“教师”那里获得正确的
评价。
在使用了深度学习的强化学习方法中,有一个叫作 Deep Q-Network(通
称 DQN)[44] 的方法。该方法基于被称为 Q 学习的强化学习算法。这里省略
8.5 深度学习的未来 263
环境
行动 报酬(观测)
代理
图 8-26 强化学习的基本框架:代理自主地进行学习,以获得更好的报酬
Q 学习的细节,不过在 Q 学习中,为了确定最合适的行动,需要确定一个被
称为最优行动价值函数的函数。为了近似这个函数,DQN 使用了深度学习
(CNN)。
在 DQN 的研究中,有让电子游戏自动学习,并实现了超过人类水平的
操作的例子。如图 8-27 所示,DQN 中使用的 CNN 把游戏图像的帧(连续 4 帧)
作为输入,最终输出游戏手柄的各个动作(控制杆的移动量、按钮操作的有
无等)的“价值”。
之前在学习电子游戏时,一般是把游戏的状态(人物的地点等)事先提
取出来,作为数据给模型。但是,在 DQN 中,如图 8-27 所示,输入数据
只有电子游戏的图像。这是 DQN 值得大书特书的地方,可以说大幅提高了
DQN 的实用性。为什么呢?因为这样就无需根据每个游戏改变设置,只要
给 DQN 游戏图像就可以了。实际上,DQN 可以用相同的结构学习《吃豆人》、
Atari 等很多游戏,甚至在很多游戏中取得了超过人类的成绩。
8.6 小结
本章我们实现了一个(稍微)深层的 CNN,并在手写数字识别上获得了
超过 99% 的高识别精度。此外,还讲解了加深网络的动机,指出了深度学习
在朝更深的方向前进。之后,又介绍了深度学习的趋势和应用案例,以及对
高速化的研究和代表深度学习未来的研究案例。
深度学习领域还有很多尚未揭晓的东西,新的研究正一个接一个地出现。
今后,全世界的研究者和技术专家也将继续积极从事这方面的研究,一定能
实现目前无法想象的技术。
感谢读者一直读到本书的最后一章。如果读者能通过本书加深对深度学
习的理解,体会到深度学习的有趣之处,笔者将深感荣幸。
8.6 小结 265
本章所学的内容
• 对于大多数的问题,都可以期待通过加深网络来提高性能。
• 在最近的图像识别大赛 ILSVRC 中,基于深度学习的方法独占鳌头,
使用的网络也在深化。
• VGG、GoogLeNet、ResNet 等是几个著名的网络。
• 基于 GPU、分布式学习、位数精度的缩减,可以实现深度学习的高速化。
• 深度学习(神经网络)不仅可以用于物体识别,还可以用于物体检测、
图像分割。
• 深度学习的应用包括图像标题的生成、图像的生成、强化学习等。最近,
深度学习在自动驾驶上的应用也备受期待。
t1
a1 y1
y1− t1
t2
a2 y2 Cross L
Softmax Entropy
Error
y2− t2 1
t3
a3 y3
y3− t3
图 A-1 Softmax-with-Loss 层的计算图
268 附录 A Softmax-with-Loss 层的计算图
A.1 正向传播
(A.1)
(A.2)
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1
exp(a 1 ) S y1
=
exp(a 1 )
a1 exp(a 1 )
S
exp
1
exp(a 2 ) S y2
=
exp(a 2 )
a2 exp(a 2 ) S
exp
1 y3
exp(a 3 ) S
=
exp(a 3 )
a3 exp(a 3 )
S
exp
图 A-2 Softmax 层的计算图(仅正向传播)
t1
y1 log y 1
log
t 1 log y 1
t2
t3
t 3 log y 3 −1
y3 log y 3
log
A.2 反向传播
t1
y1 log y 1
log
t
− y11 −t 1 t 1 log y 1
t2
−1
y2 log y 2 t 2 log y 2 t 1 log y 1 + t 2 log y 2 + t 3 log y 3 L
log
t −1 −1 1
− y22 −t 2
t3
t 3 log y 3 −1
−1
y3 log y 3
log
t
− y33 −t 3
图 A-4 交叉熵误差的反向传播
求这个计算图的反向传播时,要注意下面几点。
步骤 1
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1
exp(a 1 ) S y1
=
exp(a 1 )
a1 exp(a 1 )
S
exp
t
− y1
1 1
exp(a 2 ) S
exp(a 2 )
a2 exp(a 2 ) S
exp
t
− y2
2
1
exp(a 3 ) S
exp(a 3 )
a3 exp(a 3 )
S
exp
t
− y3
3
步骤 2
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1
exp(a 1 ) S y1
=
−t
exp(a 1 )
exp(a 1 )
1
S
a1 S
exp
- yt 1
−t 2
1 1
exp(a 2 ) S
S
exp(a 2 )
a2 exp(a 2 ) S
exp
- yt 2
2
1
exp(a 3 ) −t 3 S S
exp(a 3 )
a3 exp(a 3 )
S
exp
- yt 3
3
“×”节点将正向传播的值翻转后相乘。这个过程中会进行下面的计算。
(A.3)
A.2 反向传播 273
步骤 3
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1 (t + t + t )
S 1 2 3 1
exp(a 1 ) y1
=
S
1
=
−t
S
exp(a 1 )
exp(a 1 )
1
S
a1 S
exp
t
− y11
−t 2
1
exp(a 2 ) S
S
exp(a 2 )
a2 exp(a 2 ) S
exp
t
− y22
1
exp(a 3 ) −t 3 S S
exp(a 3 )
a3 exp(a 3 ) S
exp
t
− y33
正向传播时若有分支流出,则反向传播时它们的反向传播的值会相加。
因此,这里分成了三支的反向传播的值 (−t1S, −t2S, −t3S) 会被求和。然后,
还要对这个相加后的值进行“/”节点的反向传播,结果为 。
这里,(t1, t2, t3) 是监督标签,也是 one-hot 向量。one-hot 向量意味着 (t1, t2, t3)
中只有一个元素是 1,其余都是 0。因此,(t1, t2, t3) 的和为 1。
274 附录 A Softmax-with-Loss 层的计算图
步骤 4
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1
S 1
exp(a 1 ) S y1
1
=
−t
S exp(a 1 )
exp(a 1 )
1
S
a1 S
exp
t
− y1
−t 2
1 1
exp(a 2 ) S
S
exp(a 2 )
a2 exp(a 2 ) S
exp
t
− y2
2
1
exp(a 3 ) −t 3 S S
exp(a 3 )
a3 exp(a 3 ) S
exp
t
− y3
3
“+”节点原封不动地传递上游的值。
A.2 反向传播 275
步骤 5
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1
S 1
exp(a 1 ) S y1
1
=
−t
S exp(a 1 )
exp(a 1 )
1
S
a1 S
exp
t 1 t1 t
− y1 − y1
−t 2
=−
1 S exp(a 1 ) 1 1
exp(a 2 ) S
S
exp(a 2 )
a2 exp(a 2 ) S
exp
t
− y2
2
1
exp(a 3 ) −t 3 S S
exp(a 3 )
a3 exp(a 3 ) S
exp
t
− y3
3
“×”节点将值翻转后相乘。这里,式子变形时使用了 。
步骤 6
=
exp(a 1 ) + exp(a 2 ) + exp(a 3 )
1
S 1
exp(a 1 ) S y1
1
=
−t
S exp(a 1 )
exp(a 1 )
1
S
a1 S
exp
exp(a 1 ) t1 t
− y1
−t 2
− t 1 =y 1 − t 1 − 1 1
S exp(a 1 )
S
S
exp(a 2 )
exp(a 2 )
a2 exp(a 2 ) S
exp
t
− y2
2
1
exp(a 3 ) −t 3 S S
exp(a 3 )
a3 exp(a 3 ) S
exp
t
− y3
3
“exp”节点中有下面的关系式成立。
(A.4)
A.3 小结
S
a1 exp(a 1 ) y1 log y 1
1
× ×
S
exp log
y1 − t1
1 − t1
1 −yt 1 t − t1
t 1 log y 1
S exp(a 1 ) S 1 2
exp(a 2 )
−t 2S
1 −1
a2 S exp(a 2 ) y2 log y 2 t 2 log y 2 t 1 log y 1 + t 2 log y 2 + t 3 log y 3 L
exp × log × + ×
y2 − t2 t2 −yt 2 t 3 − t2 −1 −1 1
−
exp(a 3 ) exp(a 2 ) −t 3S 1
S
2 t 3 log y 3 −1
y3 −1
a3 exp(a 3 ) log y 3
exp × log ×
y3 − t3 − t3 −yt 3 − t3
3
exp(a 3 )
图 A-5 Softmax-with-Loss 层的计算图
图 A-5 的计算图看上去很复杂,但是使用计算图逐个确认的话,求导(反
向传播的步骤)也并没有那么复杂。除了这里介绍的 Softmax-with-Loss 层,
遇到其他看上去很难的层(如 Batch Normalization 层)时,请一定按照这里
的步骤思考一下。相信会比只看数学式更容易理解。
参考文献
Python / NumPy
计算图(误差反向传播法)
深度学习的在线课程(资料)
参数的更新方法
权重参数的初始值
超参数的最优化
[15] James Bergstra and Yoshua Bengio(2012): Random Search for Hyper-
Parameter Optimization. Journal of Machine Learning Research 13,
Feb (2012), 281 – 305.
[16] Jasper Snoek, Hugo Larochelle, and Ryan P. Adams(2012): Practical
Bayesian Optimization of Machine Learning Algorithms. In F. Pereira,
C. J. C. Burges, L. Bottou, & K. Q. Weinberger, eds. Advances in
Neural Information Processing Systems 25. Curran Associates, Inc.,
2951 – 2959.
CNN 的可视化
具有代表性的网格
数据集
[25] J. Deng, W. Dong, R. Socher, L.J. Li, Kai Li, and Li Fei-Fei(2009):
ImageNet: A large-scale hierarchical image database. In IEEE
Conference on Computer Vision and Pattern Recognition, 2009. CVPR
2009. 248 – 255.
参考文献 283
计算的高速化
MNIST 数据集识别精度排行榜及最高精度的方法
深度学习的应用