第 1 节:深度学习的元素
在本节中,您将概述使用 Python 进行的深度学习,还将了解深度前馈网络,玻尔兹曼机和自编码器的架构。 我们还将练习基于 DFN 的示例以及玻尔兹曼机和自编码器的应用,以及基于带 Python 的 DL 框架/库的具体示例及其基准。
本节包括以下章节:
- “第 1 章”,“深度学习入门”
- “第 2 章”,“深度前馈网络”
- “第 3 章”,“受限制的玻尔兹曼机和自编码器”
一、深度学习入门
人工智能可能会起作用,如果能起作用,它将是技术上最大的发展。
-山姆·奥特曼
欢迎使用《Python 深度学习架构实践指南》!如果您完全不熟悉深度学习,则可以从这本书开始您的旅程。 对于有想法的读者,我们几乎涵盖了深度学习的各个方面。 因此,您肯定会从本书中学到更多有关深度学习的知识。*
这本书是按累积方式布置的。 也就是说,它从基础开始,一遍又一遍地构建,直至达到高级。 在本章中,我们将讨论人类如何开始在机器中创建智能,以及人工智能如何逐渐演变为机器学习以及最终的深度学习。 然后,我们看到了深度学习的一些不错的应用。 回到基本原理,我们将学习人工神经元的工作原理,并最终为通过深度学习模型编码的方式建立环境。 完成本章后,您将了解以下内容。
- 什么是人工智能以及机器学习,深度学习与人工智能的关系
- 机器学习任务的类型
- 有关一些有趣的深度学习应用的信息
- 什么是人工神经网络及其工作方式
- 使用 Python 设置 TensorFlow 和 Keras
让我们从关于人工智能以及人工智能,机器学习和深度学习之间的关系的简短讨论开始。
人工智能
自计算机时代开始以来,人类一直在尝试将大脑模仿到机器中。 研究人员一直在研究使机器不仅可以计算,而且可以像人类一样做出决定的方法。 我们的这一追求在 1960 年代左右催生了人工智能。 根据定义,人工智能意味着开发能够完成任务而无需人工对每个决策进行明确编程的系统。 1956 年,亚瑟·塞缪尔(Arthur Samuel)编写了第一个玩跳棋的程序。 从那以后,研究人员试图通过定义不涉及任何学习的手写规则集来模仿人类的智力。 玩象棋这样的游戏的人工智能程序不过是一组手动定义的动作和策略。 1959 年,亚瑟·塞缪尔(Arthur Samuel)创造了术语机器学习。 机器学习开始使用各种概率和贝叶斯统计概念来执行模式识别,特征提取,分类等。 在 1980 年代,受人脑神经结构的启发,引入了人工神经网络(ANN)。 2000 年代的 ANN 演变成如今所谓的深度学习! 以下是通过机器学习和深度学习发展人工智能的时间表:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h4jaxJCV-1681704767239)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/404e4fe6-c4a4-4b37-95af-12e309977203.png)]
机器学习
机器学习之前的人工智能就是编写机器用来处理提供的数据的规则。 机器学习实现了过渡。 现在,仅通过将数据和预期输出提供给机器学习算法,计算机就可以为任务返回一组优化的规则。 机器学习使用历史数据来训练系统并在未知但相似的数据上对其进行测试,从而开始机器学习如何进行决策而无需进行硬编码的旅程。 在 90 年代初,机器学习已成为人工智能的新面孔。 开发了更大的数据集并将其公开,以允许更多的人建立和训练机器学习模型。 很快,庞大的机器学习科学家/工程师社区诞生了。 尽管机器学习算法从统计数据中得出推论,但使错误分析最小化的方法使其功能强大。 它试图最小化数据集提供的预期输出与预测算法输出之间的误差,以发现优化规则。 这是机器学习的学习部分。 我们不会在本书中介绍机器学习算法,但它们基本上分为三类:有监督,无监督和增强。 由于深度学习还是机器学习的子集,因此这些类别也适用于深度学习。
监督学习
在监督学习中,数据集既包含输入数据点又包含预期输出,通常称为标签。 该算法的工作是学习从输入到预期输出的映射函数。 该函数可以是线性函数,例如y = mx c
或非线性函数,例如y = ax3 bx2 cx d
,其中y
为目标输出,x
为输入。 所有监督学习任务都可以归类为回归和分类。
回归
回归处理学习连续映射函数,该函数可以预测各种输入特征提供的值。 该函数可以是线性的或非线性的。 如果函数是线性的,则称为线性回归;如果函数是非线性的,则通常称为多项式回归。 当存在多个输入特征(变量)时预测值,我们称为多元回归。 回归的一个非常典型的例子是房屋预测问题。 提供房屋的各种参数,例如建筑面积,位置,房间数量等,可以使用历史数据预测房屋的准确售价。
分类
当对目标输出值而不是原始值进行分类时(如回归),这是一项分类任务。 例如,我们可以根据输入特征,花瓣长度,花瓣宽度,萼片长度和萼片宽度对不同种类的花进行分类。 输出类别为杂色,山和弗吉尼亚。 逻辑回归,决策树,朴素贝叶斯等算法是分类算法。 我们将在第 2 章中介绍分类的详细信息。
无监督学习
当我们没有输入的相应目标输出值时,将使用无监督学习。 它用于了解数据分布并发现数据点之间某些种类的相似性。 由于没有可供学习的目标输出,因此无监督算法依靠初始化器来生成初始决策边界,并在它们遍历数据时对其进行更新。 在多次处理数据之后,算法将更新为优化的决策边界,该边界将基于相似性对数据点进行分组。 这种方法称为聚类,并且使用了诸如 K 均值的算法。
强化学习
还记得童年时代如何学会骑自行车吗? 那是一个反复试验的过程,对吗? 您试图平衡自己,每次做错事时,都会踩下自行车。 但是,您从错误中学到了东西,最终,您能够骑行而不会摔倒。 同样,强化学习的作用也一样! 智能体暴露于一种环境中,在该环境中,它会从一系列可能的操作中采取措施,从而导致智能体状态发生变化。 状态是智能体所处环境的当前状况。对于每个动作,智能体都会获得奖励。 每当收到的报酬为正时,表示智能体已采取正确的步骤;而当报酬为负数时,则表示错误。 智能体遵循策略,即强化学习算法,通过该策略智能体可以考虑当前状态确定下一步操作。 强化学习是人工智能的真正形式,其灵感来自人类通过反复试验而学习的方式。 想想自己是智能体,还是自行车环境! 这里讨论强化学习算法不在本书的讨论范围之内,因此让我们将重点转移到深度学习上吧!
深度学习
尽管机器学习为计算机提供了学习决策边界的能力,但它错过了这样做的鲁棒性。 机器学习模型必须针对每个特定应用进行特别设计。 人们花费了数小时来决定选择哪些特征以实现最佳学习。 随着数据交叉折叠和数据非线性的增加,机器学习模型难以产生准确的结果。 科学家们很快意识到,需要一个更强大的工具来实现这一增长。 在 1980 年代,人工神经网络的概念得到了重生,并且凭借更快的计算能力,开发了更深版本的人工神经网络,从而为我们提供了我们一直在寻找的强大工具-深度学习!
深度学习的应用
技术的优势取决于其应用的健壮性。 由于深度学习的大量应用,它在技术以及非技术市场中引起了巨大的轰动。 因此,在本节中,我们将讨论深度学习的一些惊人应用,这些将使您全神贯注。
自动驾驶汽车
这可能是深度学习中最酷,最有前途的应用。 自动驾驶汽车上装有许多摄像头。 输出的视频流被馈入深度学习网络,该网络可以识别并分割汽车周围存在的不同对象。 NVIDIA 推出了一种用于无人驾驶汽车的端到端学习,这是一个卷积神经网络,可以从摄像机中获取输入图像,并以转向角或加速度的形式预测应采取的动作。 为了训练网络,当人行驶时,将存储转向角,油门和摄像机视图,并记录该人针对周围发生的变化所采取的动作。 然后通过反向传播(在“第 2 章”,“深度前馈网络”中详细讨论了反向传播)来更新网络参数,这些误差来自人为输入和网络的预测。
如果您想了解有关 NVIDIA 的无人驾驶汽车学习的更多信息,可以参考以下 NVIDIA 的论文。
图片翻译
生成对抗网络(GAN)是最臭名昭著的深度学习架构。 这是由于它们具有从随机噪声输入向量生成输出的能力。 GAN 有两个网络:生成器和判别器。 生成器的工作是将随机向量作为输入并生成样本输出数据。 判别器从生成器创建的真实数据和伪数据中获取输入。 判别器的工作是确定输入是来自真实数据还是来自生成器的伪造数据。 您可以将场景可视化,想象辨别器是一家试图区分真假货币的银行。 同时,产生者是试图将假币传递给伪造银行的欺诈行为; 生成器和判别器都从错误中吸取教训,生成器最终产生的结果非常精确地模仿了真实数据。
GAN 有趣的应用之一是图像到图像的翻译。 它基于条件 GAN(我们将在第 7 章中详细讨论 GAN)。 给定一对具有某种关系的图像(例如 I1 和 I2),条件 GAN 将学习如何将 I1 转换为 I2。 创建了一个名为 pix2pix 的专用软件来演示此概念的应用。 它可用于为黑白图像填充颜色,从卫星图像创建地图,仅通过草图生成对象图像,而不能!
以下是 Phillip Isola 发表的《图像到图像翻译》的实际论文的链接,以及来自 pix2pix 的示例图像,描述了图像到图像翻译的各种应用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1hvQRstM-1681704767241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/1ff8273d-4497-4109-98d9-c4987801332c.png)]
源自 pix2pix
机器翻译
这个世界上有 4,000 多种语言,数十亿人通过它们进行交流。 您可以想象需要进行语言翻译的规模。 大多数翻译都是由人工翻译完成的,因为机器进行的基于规则的经典翻译通常是毫无意义的。 深度学习提出了解决方案。 它可以像我们一样学习语言,并生成更自然的翻译。 这通常称为神经机器翻译(NMT)。
编解码结构
神经机器翻译模型是循环神经网络(RNN),以编码器/解码器的方式排列。 编码器网络通过 RNN 接收可变长度的输入序列,并将这些序列编码为固定大小的向量。 解码器从此编码向量开始,并开始逐字生成翻译,直到它预测句子的结尾。 整个架构通过输入语句和正确的输出翻译进行了端到端的训练。 这些系统的主要优点(除了具有处理可变输入大小的功能之外)是,它们学习句子的上下文并据此进行预测,而不是进行逐词翻译。 在以下屏幕截图中,可以最好地看到神经机器翻译在 Google 翻译上的作用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tVXm0GEV-1681704767241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/13f8d5f2-cc43-4d5f-94e2-19d200c8ace5.png)]
源自 Google 翻译
聊天机器人
您可能会发现这是最酷的应用! 像人类一样与我们交谈的计算机一直是一种令人着迷的愿望。 它给我们一种计算机变得智能的感觉。 但是,大多数先前构建的聊天机器人系统都是基于知识库和规则的,这些规则和规则定义了从中选择哪个响应。 这使得聊天机器人成为一个非常封闭的领域,而且听起来很不自然。 但是对编码器-解码器架构进行了一些调整,我们看到了机器翻译实际上可以使聊天机器人自己生成响应。 编码学习输入句子的上下文,并且,如果整个架构都经过样本查询和响应训练,则每当系统看到新查询时,系统都可以基于学习结果生成响应。 IBM Watson,Bottr 和 rasa 等许多平台都在构建深度学习驱动的工具,以构建用于业务目的的聊天机器人。
建立基础
在本部分中,您将开始成为深度学习架构师的旅程。 深度学习是人工神经网络的支柱。 我们的第一步应该是了解它们如何工作。 在本节中,我们描述了人工神经元背后的生物学灵感和创建 ANN 的数学模型。 我们尝试将数学降至最低,并更多地关注概念。 但是,我们假设您熟悉基本的代数和微积分。
生物灵感
如前所述,深度学习受到人脑的启发。 这确实是个好主意。 要开发机器内部的大脑智能,您需要机器来模仿大脑! 现在,如果您略微意识到人脑如何如此快地学习和记忆事物,那么您必须知道,这是有可能的,因为数百万个神经元形成了一个相互连接的网络,相互之间发送信号,从而构成了记忆。 神经元具有两个主要成分:树突和轴突。 树突充当受体,并结合神经元正在接收的所有信号。 轴突通过突触与其他神经元末端的树突相连。 一旦传入的信号超过阈值,它们就会流过轴突并突触,将信号传递到连接的神经元。 神经元相互连接的结构决定了网络的功能。 以下是生物神经元的外观示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-caKqHgJK-1681704767241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/a7616492-ab9d-4bf2-923a-61579c1c5d20.png)]
生物神经元(来自 Wikimedia)
因此,神经网络的人工模型应该是一个由相互连接的节点组成的并行网络,该网络可以接收来自其他各个节点的输入,并在激活时传递输出。 这种激活现象必须通过某种数学运算来控制。 接下来让我们看一下运算和方程式!
人工神经网络
ANN 由两个组件构建:节点和权重。 节点扮演神经元的角色,权重是可学习的参数,权重将神经元彼此连接并控制其激活路径。
那么,我们如何制作一个人工神经元或节点呢? 认为x
是神经元的标量输入,w
是神经元的标量权重。 如果您不知道标量和向量是什么,则标量仅仅是一个实数元素,而向量是此类元素的列表。 人工神经元可以表示为以下方程式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-THWxk5mu-1681704767241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/15573422-803d-4dbd-8538-1fe680abd79f.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yv4rcY3U-1681704767242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/8860b8bb-bd8f-4789-b77d-d6fdfa4915e5.png)]
圆圈表示将标量x
作为输入并在将其与权重w
相乘后输出a
的神经元。 在此,将b
称为偏置。 在方程式中增加了偏置,以提供针对特定输入范围移动输出的功能。 一旦我们通过激活函数,偏置的作用将变得更加清晰。
现在,假设神经元不仅接受单个标量输入,还接受多个输入。 输入可以称为向量(例如P
)。 然后,P
可以写成一组标量输入p[1], [2], ..., p[n]
和每个输入也将具有权重向量(例如W = w[1], w[2], ..., w[n]
),这将用于激活神经元。 以下矩阵代表P
和W
向量:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWvq2B1M-1681704767242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/1f5224a5-58e7-4b78-a07c-2db3e3dab03e.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g9b2qBHc-1681704767243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9bdc844e-2c5f-4fcf-a8b2-0ae365befe92.png)]
那么,我们需要对方程式进行哪些更改以适合这些多个输入? 简单地总结一下! 这会将基本公式a = w.x b
更改为以下公式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAzhXSIK-1681704767243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/b5209007-54ef-4798-acc5-b71bd9942351.png)]
接受多个输入的人工神经元如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHSgsBoU-1681704767243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d79b3126-9b43-41b4-bf87-740584ac06e2.png)]
但是一个神经元会独自做什么? 尽管它仍然可以输出可用于进行二元判断(零或一)的值,但是我们需要大量相似的神经元以并行方式排列并相互连接,就像在大脑中一样,以便超越二元决策。 那么,它会是什么样? 如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g8aVl8Lt-1681704767243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/b5b45e90-cda3-4ce3-a2fc-666b68168925.png)]
现在需要对方程式进行哪些更改? 只是权重向量和输出向量的尺寸。 现在我们将得到n
xm
权重数,其中n
是输入数,m
是神经元数。 同样,我们将从每个神经元获得单独的输出。 因此,输出也变成一个向量:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ka4m2nWn-1681704767244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/cf07f117-b871-4f88-b16e-9f6b7aefda0e.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8s9T3Q1u-1681704767244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/70d177e4-5a49-4343-b149-caafb4161e59.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTtAemsX-1681704767244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/e054af64-fac8-4c6d-b547-769ae4c37d2e.png)]
到目前为止,我们已经学习了对人工神经网络建模的基本结构和数学方程。 接下来,我们将看到另一个重要的概念,称为激活函数。
激活函数
激活函数是任何深度学习模型的组成部分。 激活函数是将输入值压缩到一定范围内的数学函数。 假设您输入具有实数输入的神经网络,并使用随机数初始化权重矩阵,并希望使用输出进行分类; 也就是说,您需要输出值介于零到一之间,但是您的神经元可以输出-2.2453
或17854.763
之类的任何值。 因此,需要将输出缩放到特定范围。 这是激活函数的作用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUtcTzr8-1681704767245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/91fd79f6-db4a-43ea-a1a7-4838b3a9e2ba.png)]
根据要求,有很多激活函数。 我们将讨论深度学习中经常使用的一些激活函数。
线性激活
该激活与输入成比例。 它仅用于按一定常数c
缩放输出。 以下是线性激活的输出f(x)
与输入x
的关系图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9efS7FWf-1681704767245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/80acf8f6-3f18-4281-aa6a-12ea8273b0e4.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSN5xC8y-1681704767245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/59562508-475e-4b5f-8b4e-8ac17d1187d5.png)]
Sigmoid 激活
对于所有实数输入,此函数的输出范围从零到一。 这对于从神经元生成概率分数非常重要。 该函数也是连续且非线性的,有助于保持输出的非线性。 同样,曲线的梯度在原点附近陡峭,并且随着我们开始沿x
轴移动而饱和。 这意味着,围绕原点的输入会发生微小变化,从而导致输出发生重大变化。 此特性有助于分类任务,因为它试图使输出保持接近零或一。 以下是针对输入x
进行 Sigmoid 激活的方程式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VOtQpEHv-1681704767246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9a4c25fc-874f-4b01-974a-9706c0615bb5.png)]
以下是 Sigmoid 激活函数的图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZmRgH6Bw-1681704767246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/236e7085-e234-4c6d-9c6d-11b01287ea14.png)]
Tanh 激活
Tanh 或 tan 双曲函数类似于 Sigmoid 函数,但输出范围从-1
到1
而不是从0
到1
。 通常在输出的符号对我们也很重要的情况下使用。 以下是 tanh 激活函数的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BquSbb1j-1681704767246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/56dee9ca-4953-4a1f-a101-587a1cc5a98d.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pys9FXg6-1681704767246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/34d35406-4169-4eed-a185-9fa19af7b809.png)]
ReLU 激活
整流线性或众所周知的 ReLU 函数是深度学习模型中使用最广泛的激活函数。 将负值抑制为零。 ReLU 之所以被广泛使用的原因是,它使产生负值的神经元失活。 在包含数千个神经元的大多数网络中都需要这种行为。 以下是 ReLU 激活函数的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3NopSdnu-1681704767247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d6b15842-c410-4e91-a27e-2bb09fd93809.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2mJed3jy-1681704767247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/fd40709e-8eb2-45eb-84d4-c53e8463e4ad.png)]
ReLU 的一种修改形式是泄漏的 ReLU。 ReLU 以负值完全使神经元失活。 泄漏的 ReLU 并没有完全使神经元失活,而是通过c
降低了这些神经元的作用。 以下等式定义了泄漏的 ReLU 激活函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gM0dHVk5-1681704767247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/a1be47b9-c063-4825-af40-a579d613ae62.png)]
以下是 ReLU 激活函数的输出值图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-05qTEPis-1681704767247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/dc631b42-0008-41cb-a6bc-c4a814bfe035.png)]
Softmax 激活
这是大多数分类任务中要使用的激活函数。 大多数时候,网络必须预测属于该特定类别的输入的概率。 Softmax 激活输出此概率分数,表示模型对预测类别的信心程度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ir6m4pfS-1681704767248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/596aa852-f4d5-41fd-a02f-0985bb6fe2a5.png)]
TensorFlow 和 Keras
在继续进行之前,让我们快速设置编码环境。 本书在所有章节中都使用 Python 编程语言。 因此,我们希望您具有 Python 的先验知识。 我们将使用两个最受欢迎的深度学习开源框架-TensorFlow 和 Keras。 首先开始设置 Python(以防您尚未安装)。
我们强烈建议使用 Linux(最好是 Ubuntu)或 macOS 操作系统。 原因是大多数用于深度学习的库都构建为与 Linux/Unix 操作系统最兼容。 这些操作系统将涵盖所有设置说明。
在安装 Python 时,建议安装版本 3.6,而不是最新的 3.7 或更高版本。 这是为了避免由于 TensorFlow 中用作变量名称的 Python 中的新关键字而导致 TensorFlow 和 Python 之间发生不可预料的冲突。
搭建环境
在单独的环境中处理项目始终是一个好习惯。 环境是一个空间,它使安装在其中的库和依赖项与操作系统的全局空间保持隔离。 假设您必须从事两个项目; 一个需要较旧版本的库,另一个需要较新版本。 在这种情况下,全局安装新版本将覆盖旧版本,并使第一个项目无法使用。 但是,您可以为两个项目创建两个单独的环境,并分别安装所需的版本。 希望您现在有了在环境中工作的想法。
我们将使用 Miniconda,它是开源 Python 包管理和分发 Anaconda 的一小部分。 Conda 是 Miniconda 中的包管理器,可帮助安装和管理 Python 包。
我们将按照以下分步过程设置 Conda 的工作环境:
- 根据您的操作系统从这里下载适用于 Python 3.7 的 Miniconda。 只需运行下载的文件即可安装 Miniconda。
- 您可能想创建一个单独的目录来存储我们将在本书中介绍的代码。 我们称它为
deep_learning
目录。 如果您希望升级到最新版本的 Conda 并升级包,请拉起终端并转到以下目录:
conda upgrade conda
conda upgrade --all
- 现在,我们将使用 Conda 来创建我们的工作环境。 在“终端”窗口中发出以下命令。 命名环境为您想要的; 我们在这里将其命名为
test_env
:
conda create -n test_env
- 要激活环境,请发出以下命令:
conda activate test_env
- 要在完成后停用环境,请在“终端”窗口中发出以下命令:
conda deactivate
在使用 Conda 创建的环境中,可以同时使用pip
(Python 的默认包管理器)和 Conda(Anaconda 的包管理器)来安装库。
要查看您的环境中安装的包,可以使用以下命令:
代码语言:javascript复制conda list
无论包是conda
还是pip
都将显示包。
TensorFlow 简介
TensorFlow 是 Google Brain 团队开发的开源库,专门用于训练和运行深度学习模型。 TensorFlow 有两种版本:仅 CPU 版本和 GPU 支持版本。
安装 TensorFlow CPU
我们之前提到过,TensorFlow 目前在 Python 3.7 中不稳定。 因此,我们将使用 Conda 来安装 TensorFlow 而不是使用pip
。 康达以独特的方式处理它。 一旦发出以下命令来安装 TensorFlow,Conda 也会下载并安装其他必需的包。 它还将 Python 恢复到 TensorFlow 稳定的版本,例如 Python 3.6.8! 在“终端”窗口中键入以下命令(提及的注释除外)以安装 TensorFlow CPU:
# go to the deep_learning directory
cd deep_learning
# activate the environment
conda activate test_env
# install TensorFlow CPU
conda install tensorflow
安装后,您可以运行 Python 检查 Conda 是否已还原该版本。
所提到的命令与本书出版时的时间相同。 您必须知道库和存储库在不断变化。 如果命令无法正常运行,建议检查源中的最新更新。
安装 TensorFlow GPU
如果您具有 TensorFlow 支持的 GPU,则可以安装 TensorFlow GPU 版本以加快训练过程。 TensorFlow 为支持 NVIDIA CUDA 的 GPU 卡提供支持。 您可以参考以下链接来检查是否支持您的 GPU 卡。
要通过本地pip
安装 TensorFlow GPU 版本,必须经过一系列繁琐的过程:
- 下载并安装适用于您的操作系统的 CUDA 工具包
- 下载并安装 cuDNN 库(以支持 GPU 中的深度学习计算)
- 为
CUDA_HOME
和 CUDA 工具包添加路径变量 - 通过
pip
安装 TensorFlow GPU
值得庆幸的是,Anaconda 只需一个命令即可编译所有内容,从兼容的 CUDA 工具包,cuDNN 库到 TensorFlow-GPU。 如果当前环境中已经安装了 TensorFlow CPU,则可以停用该环境并为 TensorFlow GPU 创建新的环境。 您只需在 Conda 环境中运行以下命令,它将为您下载并安装所有内容:
代码语言:javascript复制# deactivate the environment
conda deactivate
# create new environment
conda create -n tf_gpu
#activate the environment
conda activate tf_gpu
# let conda install everything!
conda install tensorflow-gpu
安装完成后,就该测试安装了!
测试安装
要测试是否已成功安装,可以在 Python 中运行以下代码段。 如果您已安装 GPU 版本,请在 Python 中导入 TensorFlow 并运行以下命令:
代码语言:javascript复制>>>import tensorflow as tf
>>>sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
如果您已成功安装 GPU 版本,这将返回有关 GPU 卡的详细信息以及 TensorFlow 编译使用的其他详细信息。
为了正确检查安装(与 CPU 或 GPU 版本无关),我们将执行以下简单的张量乘法运算:
代码语言:javascript复制>>>t1 = tf.constant([8.0, 4.0, 3.0, 10.0, 9.0, 2.0], shape = [2,3],name='tensor1')
>>>t2 = tf.constant([12.0, 6.0, 4.0, 5.0, 9.0, 1.0], shape = [3,2],name='tensor2')
>>>out = tf.matmul(t1, t2)
>>>sess = tf.Session()
>>>print(session.run(out))
此代码必须打印两个张量的按元素相乘的输出。
认识 TensorFlow
与传统的 Python 库不同,TensorFlow 首先构建一个包含模型结构的空图,然后使用Session
通过输入数据来运行该图。 为了输入数据,TensorFlow 使用张量。 张量不过是多维数组。 张量是 TensorFlow 中数据的基本单元。 张量的维数由其秩表示,形状的表示像numpy
矩阵(例如[2, 3]
)。
建立图
TensorFlow 图是在图中组织的一系列操作。 首先以 TensorFlow 图的形式构建模型架构。 您需要记住三个基本操作:
tf.constant
:像 Python 中的常量一样,拥有一个常量张量,但是与 Python 不同,它仅在 TensorFlow 会话期间被激活。tf.Variable
:持有在训练期间可学习的可变张量并更新值。tf.Placeholder
:这是 TensorFlow 的有趣功能。 在构建图时,我们不提供输入数据。 但是,需要布置图将要接收的输入的形状和数据类型。 因此,占位符充当一个容器,当会话被激活时,它将允许输入张量的流动。
让我们尝试在 TensorFlow 中添加两个常量,如下所示:
代码语言:javascript复制>>>import tensorflow as tf
>>>t1 = tf.constant('hey')
>>>t2 = tf.constant('there')
>>sum = t1 t2
>>>print(sum)
这将输出如下内容:add:0
,shape=()
,dtype=string
。 您期待heythere
吗? 这不会发生,因为 TensorFlow 仅在激活会话后才运行图。 通过定义常量,我们只制作了一个图,这就是为什么打印试图告诉运行该图的总和的原因。 因此,让我们创建一个会话。
建立会话
tf.Session
对象用于创建会话。 默认情况下,它采用当前图,或者您可以指定要使用的图:
>>>sess = tf.Session()
>>>print(sess.run(sum))
这将按预期打印heythere
。
Keras 简介
Keras 还是一个流行的深度学习开源库。 Keras 更像是包装器,可以简化构建和训练模型。 它使用 TensorFlow 作为其后端。 由于 TensorFlow 的结构复杂,在 TensorFlow 中构建复杂的深度学习架构可能会变得非常棘手。 Keras 提供了非常人性化的编码体验以及模型的快速原型制作。 您可以使用 Conda 安装 Keras:
代码语言:javascript复制conda install keras
顺序 API
Keras 中的模型架构可以简单地通过一层又一层地堆叠来构建。 这在 Keras 中称为顺序方法,是最常见的一种方法:
代码语言:javascript复制from keras.models import Sequential. # importing the Sequential class
from keras.layers import Dense. #importing the Deep Learning layers
model = Sequential() #making an object of Sequential class
#adding the first Dense layer. You have to mention input dimensions to the first
#layer of model.
model.add(Dense(units=128, input_dims = 100, activation = 'relu))
model.add(Dense(units = 4, activation = 'softmax'))
完成模型架构后,Keras 将使用model.compile
方法构建具有所需损失函数和优化器的图,并使用model.fit
使用输入来训练模型。 如果您没有获得什么损失函数,请不要担心! 我们将在随后的章节中讨论所有这些。
函数式 API
这只是用于编码模型图的另一种布局。 如果您更熟悉 Python 样式代码编写,则可以选择以下布局:
代码语言:javascript复制from keras.models import Model
from keras.layers import Dense, Input
#defining input placeholder with input shape
inp = Input(shape = 100)
# layers
x = Dense(units = 128, activation = 'relu')
x = Dense(units = 64, activation = 'relu')
# taking output
predict = Dense(units = 4, activation = 'softmax')(x)
# defining model
model = Model(inputs = inp, outputs = predict)
总结
让我们快速看一下本章中学到的内容。 我们首先简要讨论了人工智能及其通过机器学习和深度学习的演变。 然后,我们看到了有关深度学习的一些有趣应用的详细信息,例如机器翻译,聊天机器人和光学字符识别。 这是本书的第一章,我们专注于学习深度学习的基础知识。
我们了解了 ANN 在某些数学的帮助下如何工作。 此外,我们看到了 ANN 和深度学习中使用的不同类型的激活函数。 最后,我们开始使用 TensorFlow 和 Keras 设置我们的编码环境,以构建深度学习模型。
在下一章中,我们将看到神经网络如何演变成深度前馈网络和深度学习。 我们还将使用 TensorFlow 和 Keras 编写我们的第一个深度学习模型!
二、深度前馈网络
在本章中,您将构建我们的第一个深度学习网络-深度前馈网络(DFN)。 我们将首先讨论深度前馈网络的演进历史,然后讨论 DFN 的架构。 在任何分类任务中,DFN 都扮演着不可或缺的角色。 除了支持分类任务外,DFN 独立版还可以用于回归和分类。 任何深度学习网络都具有很多元素,例如损失函数,梯度,优化器等,它们一起来训练网络。 在本章中,我们将详细讨论这些基本元素。 这些元素对于我们将在本书中看到的各种深度学习网络都是通用的。 我们还将演示如何提出和预处理数据以训练深度学习网络。 刚开始您可能会觉得有些难以理解,但是最终您会明白的。 所以,只要坚持下去! 完成本章后,您将了解以下内容:
- DFN 的架构
- 损失函数
- 梯度下降
- 反向传播
- 过拟合和正则化
- 编码您的第一个 DFN
让我们从 DFN 的演进历史开始!
通往 DFN 的进化之路
沃伦·麦卡洛克(Warren McCulloch)和沃尔特·皮茨(Walter Pitts)最早在 1943 年创建了人工神经网络模型。他们基于阈值逻辑建立了该模型。 通过对输入求和来计算阈值,根据阈值,输出为二进制,零或一。 1958 年,Rosenblatt 创建了另一个神经元模型,称为感知器。 Perceptron 是人工神经元的最简单模型,可以将输入分为两类(我们在“第 1 章”,“深度学习入门”中讨论了该神经元)。 亨利·凯利(Henry J.Kelley)大约在 1960 年代初提出了使用链规则通过反向传播误差来训练神经网络的概念。 然而,反向传播作为一种算法是非结构化的,并且感知器模型无法解决那个著名的 XOR 问题。 1986 年,Geoff Hinton,David Rumelhart 和 Ronald Williams 证明了具有隐藏层的神经网络可以通过反向传播学习非线性函数。 此外,还强调了神经网络能够通过通用逼近定理学习任何函数。 但是,神经网络无法扩展以解决较大的问题,到 90 年代,其他机器学习算法(例如支持向量机(SVM))占据了空间。 在 2006 年左右,Hinton 再次提出了在彼此之间添加层并为新层训练参数的想法。 使用该策略对更深层的网络进行了训练,这些网络被称为深度前馈网络。 从这里开始,神经网络有了一个新名称-深度学习!
接下来,我们将讨论 DFN 的架构设计。 我们将了解如何构建,训练分层结构,以及深度和前馈这两个术语的重要性。
DFN 的架构
在上一章中,我们看到了多神经元人工神经网络的架构。 但是,该架构仅由单层神经元组成。 现在考虑一下大脑:它是单层神经元还是多层? 是的,大脑有多层神经元,神经元层之间是一层又一层地连接在一起的。 进入大脑的输入经过初始层以提取低级特征,并经过连续层以提取高阶特征。 DFN 的架构受到多个神经元的分层结构的启发。 该网络具有连续堆叠的各个层,其中前一层的神经元输出被前馈作为下一层的输入(这就是为什么该网络称为前馈网络)。 架构输入层,隐藏层和输出层中存在三种类型的层。 下图中可以看到这些层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SdsKhDT4-1681704767248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/02008526-2814-4900-ad36-70931e768749.png)]
根据通用逼近定理,具有单个隐藏层的前馈神经网络能够用有限数量的神经元建模任何实函数。
您可以通过以下链接阅读有关通用逼近定理的更多信息。
但是,神经元的数量增长到如此之快,以至于几乎不可能建立这样的网络。 相反,增加层数可以增加神经元的数量,并有助于更好地学习特征。 网络越深(具有更多的隐藏层),学习就会越好。 接下来是显示典型 DFN 的示意图。 为了简单起见,图中没有显示偏差和标签,但是它们的作用类似于前馈网络:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rY15JRJw-1681704767248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/88a672fd-be84-4cbb-b84a-f4955a7d7a53.png)]
希望您现在了解深度和前馈在 DFN 中的含义。 接下来,我们将看到如何训练这些网络以学习损失函数和反向传播算法。
训练
DFN 中存在的权重值负责进行预测。 任何深度网络都具有如此众多的权重,以至于不可能找到理想的权重值。 因此,我们尝试搜索一组权重值,这些值将给我们足够好的预测结果。 因此,训练网络意味着从一组初始权重开始学习最佳权重值。 假设我们有一个 DFN,最初,我们不知道哪种权重会表现良好。 因此,我们用随机实数初始化权重值。 现在,我们必须从初始化的权重值转到最佳权重值。 我们可以将该任务分为以下三个部分:
- 首先,我们需要知道初始化的权重是否合适。 如果不是,那么预测输出与预期输出有多少不同? 这由损失函数计算。
- 其次,提出了最大似然估计(MLE)的概念,该概念指出,为了找到一组优化的参数(在我们的情况下为权重),我们需要最大化获得期望值的可能性(概率)。 简而言之,如果我们尝试通过更改权重(参数)来使损失函数最小化,那么我们会将预测输出分布的概率最大化,使其与预期分布尽可能接近。
- 从以上两点可以得出结论,我们需要通过更新权重来最小化损失函数。 当达到损失的最小可能值时,可以说网络已经学习(训练)了映射函数,以预测接近预期值的值。 在 DFN 中,结合梯度下降和反向传播算法来完成权重的更新和损失函数的最小化。
在随后的部分中,我们将进一步讨论损失函数,以及如何将其最小化以训练具有梯度下降和反向传播的网络。
损失函数
将y
视为来自 DFN 的预测输出,并将y_bar
视为预期的输出(或标签)。 损失函数可以衡量您的预测的正确性! 根据期望输出和预测输出之间的差异定义最简单的损失函数,如下所示: L(w)
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPktB9Va-1681704767249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/a0a5e697-c96b-4441-a1e3-6175086f9d89.png)]
但是,这种简单的方程式被认为是一种不好的措施,因为这种损失函数倾向于给出可能为负也可能为正的值。 已经为回归和分类任务开发了各种损失函数,我们将在以下小节中介绍它们。
回归损失
回归任务需要直接预测值,并且预期输出也是直接值。 这使我们能够基于两个值之间的差异来确定损失。 我们将看到的第一个也是最常见的回归损失是均方误差损失。
均方误差(MSE)
让我们假设我们的数据集中有n
个样本。 这意味着我们将具有n
个预测值(y[1], y[2], ..., y[n]
)和n
对应的期望值(y_bar[1], ..., y_bar[n]
)。 均方由以下公式定义:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lsi84nWY-1681704767249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/2adae1ac-d004-4f9c-8400-7ca5f53fbdb2.png)]
顾名思义,该误差函数将预测值和期望值(误差)之间的差平方,然后取平均值。 根据值的不同,误差可能是正值,也可能是负值,将它们相加会导致具有相反符号的值被抵消,从而导致不正确的损失值。 因此,在求和之前将它们平方以使所有值均为正。 这样,每个误差仅会影响其大小。 采取另一种手段来使损失正常化并避免突然的高损失值。 由于存在平方值,因此数据中的任何异常值(输出值与预期值相距甚远的样本)将对损失造成的影响超出其应有的范围。
因此,如果数据集中有很多离群值,那么 MSE 并不是一个合适的选择! 但是,平方的易计算性和函数是可微的,MSE 是回归中最常见的损失函数。 解决具有不同符号的错误问题的另一种方法是 MSE。
平均绝对误差
平均绝对误差而不是平方误差,而是采用误差的绝对值使所有结果为正。 与 MSE 相比,采用绝对值而不是平方会使损失函数对异常值的敏感性降低。 但是,计算绝对值需要通过线性编程进行计算,并使损失函数不可微,这可能会导致重大问题,同时通过反向传播更新权重值。 (我们将在接下来的部分中看到反向传播。)以下等式描述了平均绝对误差函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AeCaknrz-1681704767249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/08a653d5-be50-4c48-8459-2476b4efa45d.png)]
分类损失
分类任务没有直接值可以预测。 相反,它们通常具有表示输入所属类别的正整数标签。 因此,期望值和预测值都只是整数表示。 因此,直接采用差异对分类将不起作用,对于回归也是如此。 为了解决这个问题,在概率分布上开发了损失函数。 分类的最常见损失函数之一是交叉熵。
交叉熵
可以将分类问题视为预测属于每个类别的输入的概率。 训练数据将由每个类的零或一的标签组成。 标签也可以被视为概率值:一个代表输入属于该类的肯定可能性,而零表示该输入不属于该类。
为了比较预测输出和预期输出,我们因此需要一种方法来比较两个概率分布(预测和预期标签的概率分布)。 交叉熵的作用完全一样! 认为y_bar[i]
是第i
个训练样本的预期标签,而y[i]
是我们模型中的预测标签。 然后,通过以下等式定义交叉熵损失:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ocjspm2X-1681704767249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/022fa948-6031-4e52-8b8a-2b14cad9f4f3.png)]
由于使用了对数,因此交叉熵也称为对数损失。 当两个概率分布相似时,交叉熵损失将减少,并且当分布彼此不同时,交叉熵损失将增加。
我们已经说过,我们需要最小化损失函数。 我们将看到梯度下降是如何实现的。
梯度下降
让我们考虑成本函数J(w)
,它是网络权重w
的函数。 成本函数仅是损失函数与其他与正则化相关的参数的组合。 (我们稍后将讨论过拟合和正则化。暂时,考虑成本函数与损失函数相同。)我们可以假设由下图表示的凸成本函数J(w)
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hZln4wZa-1681704767250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/96daf0f6-9785-474e-8dbe-db5b911afb0b.png)]
前面我们提到,最初,我们选择一个随机值作为权重。 假设初始权重由上图中的A
表示。 我们的目标是达到成本函数的最小值,例如J_min(w)
。 假设先前示例中的成本函数在点B
处具有最小值。 您可能会注意到,成本函数的最小值(点B
)的斜率为零,而对于其他点(例如点A
(我们的初始点))则不为零。 。 因此,我们可以利用成本函数的梯度(斜率)来达到最小值。
梯度或斜率是y
轴值相对于x
轴的变化率。 在函数的任何一点上,通过计算该函数相对于x
的导数即可获得梯度。
这个想法是在初始点计算成本函数的梯度,然后以减小梯度直到其变为零(这将是最小值)的方式更新权重。 这就是为什么将该方法称为梯度下降的原因。 那么,权重如何更新? 梯度下降的不同变化使用不同的策略来更新权重。 梯度下降主要有三种类型:批量梯度下降(原始梯度下降),随机梯度下降和小批量梯度下降。
梯度下降的类型
在讨论梯度下降的类型之前,我们应该了解一个重要的参数,称为学习率(η
)。 梯度下降逐步达到成本函数的最小值。 学习率决定了步骤的持续时间。 随着权重的更新,决定合适的学习率至关重要。 如果学习率太大,权重更新可能会超出最小值,这将导致最小值周围的永无止境的振荡。 如果学习率太小,该算法将采取非常小的步骤,这可能需要大量时间才能收敛到最小值。
为了使视图更清晰,下图中描述了这两种方案:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pv45nfBs-1681704767250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d26610ce-dd93-4f3f-b63b-22f2eb7e4381.png)]
现在,我们将看到梯度下降的第一个基本变体-批梯度下降。
批量梯度下降
让我们说w_new
是一批梯度下降之后经过更新的权重集合,而w
是旧的权重集合。 权重根据以下规则进行更新:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tSh7IlwP-1681704767250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/1bcc1e26-889f-410e-a2c0-2eba6ca9e647.png)]
您可以通过考虑我们的成本函数J(w)
上的一个随机点来验证自己,即新的权重将始终朝向最小值。 批梯度下降需要计算整个数据集的梯度以执行一组更新。 因此,对于大型数据集,批量梯度下降的速度非常慢。 对于零件中处理的数据集,几乎不可能进行批量梯度下降。 为了克服这一主要缺点,引入了梯度下降的另一种形式-随机梯度下降。
随机梯度下降
经过训练集中的每个样本后,随机梯度下降会更新权重。 让我们考虑第i
个训练样本x^(i)
及其对应的标签y^(i)
。 随机梯度下降下的权重更新规则如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmqwp2zu-1681704767250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/781917bf-0e5c-4161-8a00-7914ddacfbb5.png)]
在每个样本之后更新权重可显着提高梯度下降的收敛速度。 但是,这会使更新过于频繁,从而导致成本函数的波动。 但是,波动解决了一个有趣的问题,即权重卡在局部最小值上。 到现在为止,我们已经看到只有一个全局最小值的简单成本函数。 想象一个具有多个局部最小值的复杂成本函数。 全局最小值不必与局部最小值相同。 通常,批量梯度下降倾向于卡在最接近的初始化最小值,而该最小值可能不是全局最小值。 另一方面,随机梯度下降的波动允许跳到另一个可能更好的局部最小值。 下图显示了具有多个最小值的复杂成本函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Poe5VXLg-1681704767251)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/330d3157-d203-4229-a91a-58560acddf8d.png)]
小批量梯度下降
在每个样本之后更新权重会创建很多冗余更新,而在通过完整的数据集之后更新权重实际上也是不可能的。 微型批量梯度下降法通过从数据中进行微型批量后更新权重来解决这两个问题。 微型批量是来自训练数据的一小部分样本; 例如,如果一个数据集有 10,000 个训练样本,我们可以将数据分为多个小批,例如 64 个。在每次微型批量之后,权重都会更新。 假设n
为微型批量的大小,则权重更新规则如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPWbgFsk-1681704767251)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/8a837b07-df52-4264-882b-655093f68941.png)]
反向传播
我们已经看到了如何使用梯度下降来更新权重,但是要执行梯度下降,我们需要计算成本函数相对于权重的梯度(导数)。 假设要计算成本函数相对于权重w
的梯度,我们可以使用以下导数方程,其中h
是一个小的正数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RAJOriKs-1681704767252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/666cc82b-7395-48ff-9ee5-95b91a4c03d5.png)]
在这里,要计算权重的梯度,我们需要计算两次成本函数,即执行两次完整的前向传递。 任何深度学习网络都具有数百万的权重,并且通过上述公式计算梯度将在计算上非常昂贵。 那么,对此有什么解决方案? 反向传播! 1986 年,当 David Rumelhart,Geoffrey Hilton 和 Ronald Williams 表明使用反向传播在神经网络中可以更快地计算梯度时,该算法获得了发展。 反向传播算法的核心是链式规则。 链式规则允许您仅通过一次向前和一次向后通过来计算所有所需的梯度。
链规则用于计算复合函数的导数。 考虑两个函数z = f(y)
和y = g(x)
,则函数z = f(y)
相对x
的导数可以作为z = f(y)
相对于y
以及y
相对于x
的导数。 以下等式表示相同:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8bVfhBkY-1681704767252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/5595976d-d1db-418d-9737-a2572deb4f32.png)]
类似地,链规则可以扩展为n
具有互变量的不同函数。
在转到该算法之前,我们将看到将要使用的符号。 w[jk]^l
将用来表示第l - 1
层的神经元j
与第l
层的神经元k
的连接权重。 b[j]^l
将用于第l
层的神经元j
的偏差。 对于激活函数, a[j]^l
用于表示第l
层的神经元j
的激活。 该符号易于理解。 上标表示层数,而下标表示层的神经元。
对于第l
层,输出可以用以下等式表示,其中第l
层的输入是第l-1
层的激活输出, a^(l-1) = σ(z^(l-1))
,σ
代表激活函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ov9jwf12-1681704767252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d2b66ac6-9341-40cf-a251-6f04f327d7fe.png)]
接下来,考虑第l
层中第j
个神经元,误差δ
由以下给出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-llpAkdRH-1681704767253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/470c6a97-d719-4db6-9229-2aa5d418c7d3.png)]
使用链式规则,我们可以编写以下公式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L4mChyeB-1681704767253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/8e33a066-6de3-4a82-9b81-63a86da6cfc0.png)]
同样,我们计算每一层的误差。 通过第l 1
层和第l
层之间的误差之间的以下关系,计算最后一层的误差并将其反向传播到前一层。其中⊙
表示逐元素乘法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fe8KcgXd-1681704767253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9906a086-ba66-4846-a41b-ce2b0b13cd7a.png)]
最后,根据以下等式,使用这些误差来计算成本函数相对于各层权重的梯度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvdNeULa-1681704767253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/fe3df3cc-a2c1-4504-832d-1f8cd30eccf1.png)]
我们将不对方程进行数学证明,但是您可以了解算法如何根据方程从最后一层计算误差并将误差反向传播到先前的层。 例如第l
层第j
个神经元的权重因此被计算为该层误差(δ[j]^l
),与从前一层第k
个神经元到第l
层第i
个神经元的激活输出的乘积。
现在,我们已经看到了如何通过单次向后传递来计算权重更新所需的梯度。 我们已经提到了由于局部最小值和复杂的成本函数,梯度下降无法正确收敛的挑战。 接下来,我们将看到优化器如何通过调整学习率η
解决该问题。
优化器
当沿着成本函数的斜率下降时,我们需要朝着所需的最小值靠拢一些。 否则,我们将超调,然后围绕最小值波动。 这可以通过在训练期间保持学习率可变来实现。 优化器提供的算法可在整个训练过程中调整学习率,以提供更好的收敛性。 TensorFlow 中的优化器执行使损失函数(成本函数)最小化的任务。 最常用的两个优化器是 Adam 优化器和 RMSProp 优化器。 我们将不在此处讨论优化器涉及的数学。
训练,测试和验证
通常,我们不会使用可用于训练模型的全部数据。 这样做将使我们没有任何余地来判断我们训练后的模型的表现,也无法检查训练是否朝着正确的方向进行。 我们将数据分为以下三组:
- 训练集
- 验证集
- 测试集
训练集
训练集包含将用于训练模型的数据。 该集合是三个集合中最大的集合,并且包含大多数数据(因为我们不想丢失很多训练数据)。 该模型通过迭代训练集来学习特征和模式。 尽管我们在训练期间测量训练的准确率,但实际上并没有评估模型。 由于模型已从训练集中学习并熟悉数据,因此训练精度可以达到很高的值。 因此,高训练精度并不一定意味着一个好的模型。 当模型对看不见的数据进行预测时,将对模型进行实际评估。
验证集
在训练模型时,验证集为模型提供了频繁且公正的评估。 我们不在训练集中包含验证集,但在训练过程中进行某些迭代后,将继续检查验证集的准确率。 验证集有助于在训练过程中微调参数。
最初,验证集似乎并不那么重要,但是当我们讨论过拟合时,您会看到它扮演的重要角色。
测试集
训练我们的模型的目的是对未知数据(模型未看到的数据)做出良好的预测。 因此,必须使用模型从未见过的数据来进行真正的评估。 测试集通常是从全部可用数据中保留的,在训练过程中永远不会显示给模型。 测试集可让您使用未知数据评估我们的模型。 这里要了解的重要事项是对于受监管的任务,测试集 还包含数据和相应的预期标签,例如训练集。 仅当我们将模型预测与标签进行比较以计算准确率时,才使用标签。 因此,应为验证集和测试集保留多少数据?
通常,最好将 80% 的可用数据保留为训练集,将 10% 作为验证集,将最后 10% 作为测试集,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6tp7tn5-1681704767254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/1355e2b4-f233-459e-9906-1f47ebc71e77.png)]
过拟合和正则化
任何深度学习网络(例如 DFN)都具有很多可学习的参数(以百万计)。 具有大量参数的网络可以适合任何数据分布。 但是,训练的重点不是完全适合训练数据,而是学习表征数据的一般特征和模式。 模型可能会学会正确预测训练集中的每个样本,但无法在测试集中执行。 这通常发生在具有大量参数的网络中。 免费的可学习参数可用于学习训练集的每一个复杂性。 但是,这样做时,网络会过度学习,并且变得非常特定于训练数据,无法对不熟悉的数据执行操作。 这种过度学习现象被称为过拟合。 下图可以使您对现象有更好的了解。 过拟合的模型学习非常特定于训练数据的决策边界(红色),这通常不是一个好的决策边界:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGwMxGGZ-1681704767254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/6fdcbaa2-9350-4f94-8487-033a4f76c5c1.png)]
黑线表示拟合良好,绿线表示过拟合-图片来自 Wikipedia
为了避免过拟合,在深度学习中使用了正则化技术。 正则化到底是什么意思? 正则化通过惩罚或带来参数的随机性来限制参数的过度学习。 使用三种主要的正则化技术来避免过拟合:
- L1 和 L2 正则化
- 丢弃
- 早期停止
数据扩充也被考虑在正则化下。 它涉及操纵输入数据以创建同一数据的更多变体。 例如,图像被移动和缩放以产生更多的变体。 数据扩充方法针对不同类型的数据而有所不同。
L1 和 L2 正则化
记得我们之前提到过,成本函数是损失函数与正则化参数的组合。 L1 和 L2 是添加到损失函数以构成成本函数的参数。 L1 和 L2 通过使某些权重可以忽略不计,从而损失了损失函数,从而减少了可能导致过度学习的自由参数。
L1 正则化将权重的绝对值作为惩罚系数添加到损失函数中,如下式所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-12kAmr6t-1681704767254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/6dbbcf94-1256-4531-bcf2-94f49cab5bc6.png)]
公式中的第一项表示权重的平方损失函数,第二项是 L1 正则化系数。 λ
是一个超参数,可以手动调整以达到良好的匹配度。 L1 正则化被认为是健壮的,因为它对数据的异常值不那么敏感。 但是 L1 正则化有时可能会在计算上变得昂贵,并可能在我们的损失函数中引入不连续性。 因此,由于其复杂性,通常不使用 L1 正则化。
另一方面,L2 正则化将权重的平滑平方值作为损失因子加到损失函数,如以下公式所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XsuEJ2id-1681704767254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/153afc32-3bc4-4c01-8ae4-9be654ffc499.png)]
使用 L2 正则化的优点是易于计算平方并通过损失函数确保连续性。 但这是以敏感性为代价的。 由于平方项,L2 正则化会放大离群值的损失函数,因此使成本函数非常敏感。 在下一小节中,我们将看到什么是丢弃以及如何避免过拟合。
丢弃
丢弃是处理神经网络过拟合(或深度学习)的独特方法。 为了引入网络中的随机性,丢弃操作通过删除与该节点之间的权重连接来随机删除几个节点(神经元)。 丢弃在每个迭代中随机发生。 下图显示了 DFN 中丢弃的工作方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TUg4q665-1681704767255)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/92ad7f67-8956-4811-8344-8537f848d734.png)]
早期停止
在讨论验证集时,我们稍后将讨论验证集在过拟合中的作用。 在训练过程中,我们实际上不知道有多少次迭代训练模型。 这通常是过拟合的原因。 知道何时停止是重要的事情! 训练时设置验证集有助于我们决定何时停止。 由于训练算法将训练损失降到最低,因此不管模型是否过拟合,损失将始终保持减少。 因此,除了训练损失,我们还监视验证集上的损失。 训练和验证损失都将持续减少,直到达到最佳点为止。 此后,由于过拟合,验证损失将再次开始增加。 验证和训练损失开始出现分歧的点是我们需要停止过度训练并避免过拟合的点。 您可以在下图中观察到相同的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FrAMw0HA-1681704767255)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/a66bfde1-ae69-4bad-bdf6-c27f968ae94a.png)]
到目前为止,我们已经收集了有关深度学习基本方面的足够信息。 因此,让我们开始深入研究使用 TensorFlow 编码我们的第一个 DFN!
建立我们的第一个 DFN
到目前为止,我们已经了解了 DFN 的工作原理,以及有关网络训练的架构和方面。 在本节中,我们将使用 TensorFlow 构建我们的第一个 DFN。 建立任何深度学习将或多或少涉及以下步骤:
- 读取输入数据和预期输出数据(标签)
- 准备所需格式的数据(预处理)
- 将数据分为训练集,验证集和测试集(验证集有时是可选的)
- 建立模型架构图以及损失函数和优化器以更新权重
- 运行 TensorFlow 会话以遍历数据并训练网络
- 通过测试数据测试模型的准确率
MNIST 时尚数据
MNIST 是手写数字的数据集,其中包含 60,000 个训练样本和 10,000 个测试样本(从零到九的手写数字)。 每个样本都是28 x 28
单通道(灰度)图像。 数据集被广泛用作深度学习的起点。 但是,对于深度学习模型,数据集非常简单且易于学习。 而且,数据集并不是现实计算机视觉任务中图像的很好示例。
因此,我们将在 MNIST 时尚数据集上建立第一个模型,该数据集的开发方式与原始 MNIST 类似。 它有 10 个时尚商品(T 恤,裤子,套头衫,衣服,外套,凉鞋,衬衫,运动鞋,包和脚踝靴)的 60,000 个训练和 10,000 个测试样本。 与原始 MNIST 一样,时尚 MNIST 也具有28 x 28
灰度图像,但是图像学习起来更为复杂。 有关数据集的更多信息,请参见这里。
获取数据
TensorFlow 已经具有内置的keras
类,用于下载和管理 MNIST 方式提供的数据。 因此,您不必手动下载数据,TensorFlow 会为您完成! 我们将从以下步骤开始:
- 让我们编写一个 Python 脚本来构建我们的第一个 DFN。 首先,导入所需的依赖项,如下所示:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow import keras
Scikit-learn 或 sklearn 是一个重要的 Python 库,它提供了很多机器学习模型以及数据准备方法的帮助。 您可以使用以下命令安装scikit
学习:
$ conda install scikit-learn
- 使 TensorFlow 的
keras
类的对象具有如下数据加载函数:
# making object of fashion_mnist class
fashionObj = keras.datasets.fashion_mnist
- 如果尚未在系统上下载
load_data
方法,它将下载 MNIST 时尚数据集:
# trainX contains input images and trainY contains corresponding labels.
# Similarly for test
(trainX, trainY), (testX, testY) = fashionObj.load_data()
print('train data x shape: ', trainX.shape)
print('test data x shape: ', testX.shape)
print('train data y shape: ', trainY.shape)
print('test data y shape: ', testY.shape)
- 加载数据后,我们将打印训练数据以及测试数据的形状。 输出形状应如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ahc6Vi5J-1681704767255)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/e074ce74-2b7d-43d7-8f7d-30bf225e71a9.png)]
数据中包含的标签是数字,从0
到9
,其中每个整数代表一个类。 我们将制作一个字典,将这些整数映射到 MNIST 时尚数据中提到的类。 您可以在以下屏幕截图中看到整数及其对应的类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7FYfSah6-1681704767255)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/7730111d-1f18-4919-bd3a-4b0c5796b8f4.png)]
前面输出的代码如下:
代码语言:javascript复制# make a label dictionary to map integer labels to classes
classesDict = {0:'T-shirt/top', 1:'Trouser', 2:'Pullover', 3:'Dress', 4:'Coat',5:'Sandal', 6:'Shirt', 7:'Sneaker', 8:'Bag', 9:'Ankle boot'}
可视化数据
让我们看看数据集中的一些图像及其对应的标签。 我们使用matplotlib
库从数据绘制四个样本图像,如以下代码所示:
rows = 2
columns = 2
fig = plt.figure(figsize = (5,5))
for i in range(1, rows*columns 1):
image = trainX[i]
label = trainY[i]
sub = fig.add_subplot(rows, columns, i)
sub.set_title('Label: ' classesDict[label])
plt.imshow(image)
plt.show()
图像图将类似于以下屏幕截图。 每个子图的标题传达了图像所属的类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fum8itML-1681704767256)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/ae0a1783-4a52-438c-b415-6c34e4ea2b94.png)]
标准化和分割数据
图像中像素的值范围为 0 到 255。始终建议在计算上将输入值保持在0
至1
之间。 因此,我们将通过将数据除以最大可能值 255 来归一化我们的数据,以使0
至1
之间的所有内容都变为正常。 同样,输入图像的尺寸为28 x 28
,但我们的 DFN 并不采用二维输入。 因此,我们将输入训练图像从(60000, 28, 28)
展平到(60000, 784)
并测试从(10000, 28, 28)
到(10000, 784)
的输入图像:
trainX = trainX.reshape(trainX.shape[0], 784) / 255.0
testX = testX.reshape(testX.shape[0], 784) / 255.0
数据集已经分为两部分-训练数据和测试数据。 因此,我们只需要将训练数据分为验证和训练数据即可。 我们将使用 sklearn 的train_test_split
方法进行此操作。 该方法还会在拆分之前对数据进行混洗,以确保拆分后的数据不会偏向某个类:
trainX, valX, trainY, valY = train_test_split(trainX, trainY, test_size = 0.1, random_state =2)
# random_state is used for randomly shuffling the data.
模型参数
与准备数据有关的大多数任务现已完成。 现在,我们将重点介绍深层前馈模型。 在构建模型图之前,我们将确定模型的以下参数:
- 类的数量(
CLASS_NUM
):在 MNIST 时尚数据集中,有 10 个类。 因此,我们将分类的类数为 10。 - 输入层神经元的数量(
INPUT_UNITS
):我们将输入层中的一个神经元附加到图像的一个像素值。 输入层中有 784(28 x 28
)个神经元。 - 第一层神经元的数量(
HIDDEN_LAYER_1_UNITS
):我们决定在网络的第一隐藏层中保留 256 个神经元。 您可以尝试将此数字更改为您的选择。 - 第二层神经元的数量(
HIDDEN_LAYER_2_UNITS
):在第二层隐藏层中,保留了 128 个神经元。 同样,您可以将此数字更改为选择之一。 - 输出层中神经元的数量(
OUTPUT_LAYER_UNITS
):由于我们将使用 softmax 激活,因此输出层中的每个神经元将输出属于该类别的输入的概率。 因此,我们需要使神经元的数量等于类的数量。 - 优化器的学习率(
LEARNING_RATE
):我们使用的优化器的默认学习率是0.001
。 您可以更改此设置并查看对训练的影响。 - 批量大小(
BATCH_SIZE
):我们使用小批量数据进行训练。 将整个训练数据划分为大小等于批量大小的块。 对于每个批量,执行权重更新。 - 迭代(
EPOCHS
):我们遍历整个数据的次数。
让我们看下面的代码:
代码语言:javascript复制CLASS_NUM = 10
#number of classes we need to classify
INPUT_UNITS = 784
# no. of neurons in input layer 784, as we have 28x28 = 784 pixels in each image.
# we connect each pixel to a neuron.
HIDDEN_LAYER_1_UNITS = 256
# no of neurons in first hidden layer
HIDDEN_LAYER_2_UNITS = 128
#no. of neurons in second hidden layer
OUTPUT_LAYER_UNITS = CLASS_NUM
# no. of neurons in output layer = no. of classes we have to classify.
# each neuron will output the probability of input belonging to the class it represents
LEARNING_RATE = 0.001
# learning rate for gradient descent. Default value is 0.001
BATCH_SIZE = 64
# we will take input data in sets of 64 images at once instead of using whole data
# for every iteration. Each set is called a batch and batch function is used to generate
# batches of data.
NUM_BATCHES = int(trainX.shape[0] / BATCH_SIZE)
# number of mini-batches required to cover the train data
EPOCHS = 10
# number of iterations we will perform to train
单热编码
单热编码是一个向量,其大小等于仅包含二进制值(0 和 1)的类的数量。 用于表示标签。 向量在类的索引处包含 1,其余均为 0。 在我们的案例中,我们有 10 个类; 因此,用于表示第三类(套衫)的标签的单热编码向量将如下所示:
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
使用单热编码而不是整数值的原因是因为输出层中的每个神经元都归为一类。 也就是说,1 表示属于该类,0 表示不属于该类。 因此,每个神经元在期望的输出向量中需要 0 或 1 的值,而不是整数。 要将标签转换为一键向量,我们使用numpy
库,如下所示:
trainY = np.eye(CLASS_NUM)[trainY]
valY = np.eye(CLASS_NUM)[valY]
testY = np.eye(CLASS_NUM)[testY]
建立模型图
我们使用不同的名称范围构建 TensorFlow 图。 TensorFlow 中的名称范围允许对图中的变量进行分组和区分。 它还允许对不同的变量重用相同的名称。 例如,所有与输入层有关的变量都放在input_layer
范围内。
添加占位符
将保留输入数据x
和标签y
的占位符放置在名称placeholders
作用域下,如下所示:
with tf.name_scope('placeholders') as scope:
# making placeholders for inputs (x) and labels (y)
# the first dimension 'BATCH_SIZE' represents the number of samples
# in a batch. It can also be kept 'None'. Tensorflow will automatically
# detect the shape from incoming data.
x = tf.placeholder(shape = [BATCH_SIZE, 784], dtype = tf.float32, name = 'inp_x')
y = tf.placeholder(shape = [BATCH_SIZE, CLASS_NUM], dtype = tf.float32, name= 'true_y')
添加层
接下来,我们将定义包含第一组权重和偏差的输入层,如下所示:
代码语言:javascript复制with tf.name_scope('inp_layer') as scope:
# the first set of weights will be connecting the inputs layer to first hidden layer
# Hence, it will essentially be a matrix of shape [INPUT_UNITS, #HIDDEN_LAYER_1_UNITS]
weights1 = tf.get_variable(shape = [INPUT_UNITS, HIDDEN_LAYER_1_UNITS], dtype = tf.float32, name = 'weights_1')
biases1 = tf.get_variable(shape = [HIDDEN_LAYER_1_UNITS], dtype = tf.float32, name = 'bias_1')
# performing W.x b, we rather multiply x to W in due to matrix shape #constraints.
# otherwise you can also take transpose of W and multiply it to x
layer1 = tf.nn.relu(tf.add(tf.matmul(x, weights1), biases1), name = 'layer_1')
# we use the relu activations in the 2 hidden layers
同样,我们还将定义第一个隐藏层,如以下代码所示:
代码语言:javascript复制with tf.name_scope('hidden_1') as scope:
# second set of weights between hidden layer 1 and hidden layer 2
weights2 = tf.get_variable(shape = [HIDDEN_LAYER_1_UNITS, HIDDEN_LAYER_2_UNITS], dtype = tf.float32, name = 'weights_2')
biases2 = tf.get_variable(shape = [HIDDEN_LAYER_2_UNITS], dtype = tf.float32,
name = 'bias_2')
# the output of layer 1 will be fed to layer 2 (as this is Feedforward Network)
layer2 = tf.nn.relu(tf.add(tf.matmul(layer1, weights2), biases2), name ='layer_2')
最后,我们添加输出层,如下所示:
代码语言:javascript复制with tf.name_scope('out_layer') as scope:
#third set of weights will be from second hidden layer to final output layer
weights3 = tf.get_variable(shape = [HIDDEN_LAYER_2_UNITS, OUTPUT_LAYER_UNITS], dtype = tf.float32, name = 'weights_3')
biases3 = tf.get_variable(shape = [OUTPUT_LAYER_UNITS], dtype = tf.float32, name = 'biases_3')
# In the last layer, we should use the 'softmax' activation function to get the
# probabilities. But we won't do so here because we will use the cross entropy #loss with softmax which first converts the output to probabilty with softmax
layer3 = tf.add(tf.matmul(layer2, weights3), biases3, name = 'out_layer')
添加损失函数
我们之前谈到了损失。 由于这是分类任务,因此交叉熵损失将是最合适的。 要使用交叉熵,预测和预期输出必须是概率分布。 这是通过 softmax 激活来完成的。 TensorFlow 中的交叉熵损失函数在单个函数中结合了 softmax 激活和交叉熵损失,我们无需在网络的最后一层单独应用 softmax 激活。
不要在输出层的最后和最后的 softmax 交叉熵损失中一起使用显式 softmax 激活。 这样做将导致模型的训练准确率突然下降。
代码语言:javascript复制# now we shall add the loss function to graph
with tf.name_scope('loss') as scope:
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = layer3, labels = y))
添加优化器
我们使用 Adam 优化器来最大程度地减少损失,如下所示:
代码语言:javascript复制# adding optimizer
with tf.name_scope('optimizer') as scope:
# we will use Adam Optimizer. It is the most widely used optimizer
optimizer = tf.train.AdamOptimizer(learning_rate = LEARNING_RATE)
# we will use this optimizer to minimize loss, that is, to train the network
train = optimizer.minimize(loss)
计算精度
有很多指标可以衡量模型的表现。 我们会持续监控批量的训练损失值和训练准确率。 准确率以百分比(多少个预测正确)度量。 随着精度的提高,损失值不断减小。 对于每次迭代,我们还检查验证损失和准确率。 由于验证损失在达到最小值后又开始增加,因此我们需要停止训练,否则我们将过拟合模型:
代码语言:javascript复制with tf.name_scope('accuracy') as scope:
# here we will check how many predictions our model is making correct by comparing # the labels
# tf.equal compares the two tensors element wise, where tf.argmax returns the #index of class which the prediction and label belong to.
correctPredictions = tf.equal(tf.argmax(layer3, axis=1), tf.argmax(y, axis = 1))
# calculating average accuracy
avgAccuracy = tf.reduce_mean(tf.cast(correctPredictions, tf.float32))
进行训练
到目前为止,我们已经创建了包含层,损失和优化器的模型图。 但是,要使图变得活动,我们需要运行该会话! 对于每次迭代,都会生成一批输入图像和标签。 每次调用run
函数时,都需要将数据馈入占位符。 生成的批量数据通过feed_dict
参数输入到占位符。 每次运行train
操作时,权重都会根据损失进行更新。 张量仅在活动会话中保持其值:
# beginning Tensorflow Session to start training
with tf.Session() as sess:
# initializing Tensorflow variables under session
sess.run(tf.global_variables_initializer())
for epoch in range(EPOCHS):
for i in range(NUM_BATCHES):
# creating batch of inputs
batchX = trainX[i*BATCH_SIZE : (i 1)*BATCH_SIZE , :]
batchY = trainY[i*BATCH_SIZE : (i 1)*BATCH_SIZE , :]
# running the train operation for updating weights after every mini-batch
_, miniBatchLoss, acc = sess.run([train, loss, avgAccuracy], feed_dict = {x: batchX, y: batchY})
# printing accuracy and loss for every 4th training batch
if i % 10 == 0:
print('Epoch: ' str(epoch) ' Minibatch_Loss: ' "{:.6f}".format(miniBatchLoss) ' Train_acc: ' "{:.5f}".format(acc) "n")
# calculating loss for validation batches
for i in range(int(valX.shape[0] / BATCH_SIZE)):
valBatchX = valX[i*BATCH_SIZE : (i 1)*BATCH_SIZE, :]
valBatchY = valY[i*BATCH_SIZE: (i 1)*BATCH_SIZE, :]
valLoss, valAcc = sess.run([loss, avgAccuracy], feed_dict = {x: valBatchX, y: valBatchY})
if i % 5 ==0:
print('Validation Batch: ', i,' Val Loss: ', valLoss, 'val Acc: ', valAcc)
我们每八个训练批量打印一次精度指标,如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wqS2cM2A-1681704767256)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/629f0c32-5f9f-4ac6-96a8-167b20aa3bf5.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5umD047t-1681704767256)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/c016a528-53fe-479d-bef3-0af383441680.png)]
另外,我们每隔Validation
个批量打印验证准确率,如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dWLCB13-1681704767256)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/0c9f78a4-4d44-46df-8ed9-42b3c457ac91.png)]
您可能会注意到,在第 8 个阶段之后,验证准确率开始下降。 这表明该模型已充分学习,超出此点将使模型过拟合。
训练完成后,我们将根据测试数据测试表现。 将使用用于评估训练和验证准确率的相同操作,但是数据将更改为测试数据,如以下代码所示:
代码语言:javascript复制 # after training, testing performance on test batch
for i in range(int(testX.shape[0] / BATCH_SIZE)):
testBatchX = testX[i*BATCH_SIZE : (i 1)*BATCH_SIZE, :]
testBatchY = testY[i*BATCH_SIZE: (i 1)*BATCH_SIZE, :]
testLoss, testAcc = sess.run([loss, avgAccuracy], feed_dict = {x: testBatchX, y: testBatchY})
if i % 5 ==0:
print('Test Batch: ', i,' Test Loss: ', testLoss, 'Test Acc: ', testAcc)
我们每隔五分Test Batch
打印一次测试准确率,如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WimqhdZP-1681704767263)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/212768e4-9149-4afd-b4f5-6af2f3d55dab.png)]
如果发现很难跟踪代码片段的结构和流程,我们在这里提供该模型的完整代码:
代码语言:javascript复制'''
MNIST Fashion Deep Feedforward example
'''
import os
# use following command if you are getting error with MacOS
os.environ['KMP_DUPLICATE_LIB_OK']='True'
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow import keras
fashionObj = keras.datasets.fashion_mnist
(trainX, trainY), (testX, testY) = fashionObj.load_data()
print('train data x shape: ', trainX.shape)
print('test data x shape:', testX.shape)
print('train data y shape: ', trainY.shape)
print('test data y shape: ', testY.shape)
classesDict = {0:'T-shirt/top', 1:'Trouser', 2:'Pullover', 3:'Dress', 4:'Coat', 5:'Sandal', 6:'Shirt', 7:'Sneaker', 8:'Bag', 9:'Ankle boot'}
rows = 2
columns = 2
fig = plt.figure(figsize = (5,5))
for i in range(1, rows*columns 1):
image = trainX[i]
label = trainY[i]
sub = fig.add_subplot(rows, columns, i)
sub.set_title('Label: ' classesDict[label])
plt.imshow(image)
plt.show()
trainX = trainX.reshape(trainX.shape[0], 784) / 255.0
testX = testX.reshape(testX.shape[0], 784) / 255.0
trainX, valX, trainY, valY = train_test_split(trainX, trainY, test_size = 0.1, random_state =2)
CLASS_NUM = 10
#number of classes we need to classify
INPUT_UNITS = 784
# no. of neurons in input layer 784, as we have 28x28 = 784 pixels in each image.
# we connect each pixel to a neuron.
HIDDEN_LAYER_1_UNITS = 256
HIDDEN_LAYER_2_UNITS = 128
OUTPUT_LAYER_UNITS = CLASS_NUM
LEARNING_RATE = 0.001
BATCH_SIZE = 64
NUM_BATCHES = int(trainX.shape[0] / BATCH_SIZE)
EPOCHS = 20
trainY = np.eye(CLASS_NUM)[trainY]
valY = np.eye(CLASS_NUM)[valY]
testY = np.eye(CLASS_NUM)[testY]
with tf.name_scope('placeholders') as scope:
# making placeholders for inputs (x) and labels (y)
x = tf.placeholder(shape = [BATCH_SIZE, 784], dtype = tf.float32, name = 'inp_x')
y = tf.placeholder(shape = [BATCH_SIZE, CLASS_NUM], dtype = tf.float32, name = 'true_y')
with tf.name_scope('inp_layer') as scope:
weights1 = tf.get_variable(shape = [INPUT_UNITS, HIDDEN_LAYER_1_UNITS], dtype = tf.float32, name = 'weights_1')
biases1 = tf.get_variable(shape = [HIDDEN_LAYER_1_UNITS], dtype = tf.float32,
name = 'bias_1')
with tf.name_scope('hidden_1') as scope:
weights2 = tf.get_variable(shape = [HIDDEN_LAYER_1_UNITS, HIDDEN_LAYER_2_UNITS], dtype = tf.float32, name = 'weights_2')
biases2 = tf.get_variable(shape = [HIDDEN_LAYER_2_UNITS], dtype = tf.float32,
name = 'bias_2')
layer2 = tf.nn.relu(tf.add(tf.matmul(layer1, weights2), biases2), name = 'layer_2')
with tf.name_scope('out_layer') as scope:
weights3 = tf.get_variable(shape = [HIDDEN_LAYER_2_UNITS, OUTPUT_LAYER_UNITS], dtype = tf.float32, name = 'weights_3')
biases3 = tf.get_variable(shape = [OUTPUT_LAYER_UNITS], dtype = tf.float32,
name = 'biases_3')
layer3 = tf.add(tf.matmul(layer2, weights3), biases3, name = 'out_layer')
with tf.name_scope('loss') as scope:
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = layer3, labels = y))
with tf.name_scope('optimizer') as scope:
optimizer = tf.train.AdamOptimizer(learning_rate = LEARNING_RATE)
train = optimizer.minimize(loss)
with tf.name_scope('accuracy') as scope:
correctPredictions = tf.equal(tf.argmax(layer3, axis=1), tf.argmax(y, axis = 1))
avgAccuracy = tf.reduce_mean(tf.cast(correctPredictions, tf.float32))
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for epoch in range(EPOCHS):
for i in range(NUM_BATCHES):
batchX = trainX[i*BATCH_SIZE : (i 1)*BATCH_SIZE , :]
batchY = trainY[i*BATCH_SIZE : (i 1)*BATCH_SIZE , :]
_, miniBatchLoss, acc = sess.run([train, loss, avgAccuracy], feed_dict = {x: batchX, y: batchY})
if i % 10 == 0:
print('Epoch: ' str(epoch) ' Minibatch_Loss: ' "{:.6f}".format(miniBatchLoss) ' Train_acc: ' "{:.5f}".format(acc) "n")
for i in range(int(valX.shape[0] / BATCH_SIZE)):
valBatchX = valX[i*BATCH_SIZE : (i 1)*BATCH_SIZE, :]
valBatchY = valY[i*BATCH_SIZE: (i 1)*BATCH_SIZE, :]
valLoss, valAcc = sess.run([loss, avgAccuracy], feed_dict = {x: valBatchX, y: valBatchY})
if i % 5 ==0:
print('Validation Batch: ', i,' Val Loss: ', valLoss, 'val Acc: ', valAcc)
for i in range(int(testX.shape[0] / BATCH_SIZE)):
testBatchX = testX[i*BATCH_SIZE : (i 1)*BATCH_SIZE, :]
testBatchY = testY[i*BATCH_SIZE: (i 1)*BATCH_SIZE, :]
testLoss, testAcc = sess.run([loss, avgAccuracy], feed_dict = {x: testBatchX, y: testBatchY})
if i % 5 ==0:
print('Test Batch: ', i,' Test Loss: ', testLoss, 'Test Acc: ', testAcc)
简单的方法
仅为一个简单的 DFN 编写所有上述代码似乎很乏味。 因此,TensorFlow 具有高级模块,使我们可以更轻松地构建模型。 Keras 通过提供构建层的函数来处理主要的编码结构,使我们能够专注于模型架构。 让我们使用 Keras 构建一个小型 DFN,如下所示:
代码语言:javascript复制import keras
# importing the sequential method in Keras
from keras.models import Sequential
# Importing the dense layer which creates a layer of deep feedforward network
from keras.layers import Dense, Activation, Flatten, Dropout
# getting the data as we did earlier
fashionObj = keras.datasets.fashion_mnist
(trainX, trainY), (testX, testY) = fashionObj.load_data()
print('train data x shape: ', trainX.shape)
print('test data x shape:', testX.shape)
print('train data y shape: ', trainY.shape)
print('test data y shape: ', testY.shape)
# Now we can directly jump to building model, we build in Sequential manner as discussed in Chapter 1
model = Sequential()
# the first layer we will use is to flatten the 2-d image input from (28,28) to 784
model.add(Flatten(input_shape = (28, 28)))
# adding first hidden layer with 512 units
model.add(Dense(512))
#adding activation to the output
model.add(Activation('relu'))
#using Dropout for Regularization
model.add(Dropout(0.2))
# adding our final output layer
model.add(Dense(10))
#softmax activation at the end
model.add(Activation('softmax'))
# normalizing input data before feeding
trainX = trainX / 255
testX = testX / 255
# compiling model with optimizer and loss
model.compile(optimizer= 'Adam', loss = 'sparse_categorical_crossentropy', metrics = ['accuracy'])
# training the model
model.fit(trainX, trainY, epochs = 5, batch_size = 64)
# evaluating the model on test data
model.evaluate(testX, testY)
print('Test Set average Accuracy: ', evalu[1])
上面的代码将输出以下内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bkpRZzPc-1681704767264)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/b9e72d48-b033-48c5-9860-d6df05237e86.png)]
总结
我们从 DFN 和深度学习的演变历史开始本章。 我们了解了 DFN 的分层架构以及训练中涉及的各个方面,例如损失函数,梯度下降,反向传播,优化器和正则化。 然后,我们使用 TensorFlow 和 Keras 通过第一个 DFN 进行编码。 我们从开源时尚 MNIST 数据开始,并逐步学习了建立网络的逐步过程,从处理数据到训练模型。
在下一章中,我们将看到玻尔兹曼机和自编码器的架构。
三、受限玻尔兹曼机和自编码器
当您在线购物或浏览电影时,您可能会想知道“您可能也喜欢的电影”产品如何工作。 在本章中,我们将说明幕后算法,称为受限玻尔兹曼机(RBM)。 我们将首先回顾 RBM 及其发展路径。 然后,我们将更深入地研究其背后的逻辑,并在 TensorFlow 中实现 RBM。 我们还将应用它们来构建电影推荐器。 除了浅层架构,我们还将继续使用称为深度信念网络(DBN)的 RBM 堆叠版本,并使用它对图像进行分类,当然,我们在 TensorFlow 中实现。
RBM 通过尝试重建输入数据来找到输入的潜在表示。 在本章中,我们还将讨论自编码器,这是另一种具有类似想法的网络。 在本章的后半部分,我们将继续介绍自编码器,并简要介绍它们的发展路径。 我们将说明按照其架构或形式化形式分类的各种自编码器。 我们还将采用不同类型的自编码器来检测信用卡欺诈。 这将是一个有趣的项目,更令人着迷的是,您将看到这些自编码器种类如何努力学习使用某些架构或强加约束形式的更健壮的表示形式。
我们将深入探讨以下主题:
- 什么是 RBM?
- RBM 的发展路径
- 在 TensorFlow 中实现 RBM
- 用于电影推荐的 RBM
- 数据库
- TensorFlow 中 DBN 的实现
- 用于图像分类的 DBN
- 什么是自编码器?
- 自编码器的发展路径
- 原始自编码器
- 深度自编码器
- 稀疏自编码器
- 去噪自编码器
- 压缩自编码器
- 用于信用卡欺诈检测的自编码器
什么是 RBM?
RBM 是一种生成型随机神经网络。 通过说生成式,它表明网络对输入集上的概率分布进行建模。 随机意味着神经元在被激活时具有随机行为。 RBM 的一般图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cY0pPDbt-1681704767264)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/7e117abf-04cc-407c-b57b-1683118b2ea8.png)]
通常,RBM 由一个输入层组成,该输入层通常称为可见层(v[1], v[2], v[3], v[4]
)和一个隐藏层(h[1], h[2], h[3], h[4]
)。 RBM 模型由与可见层和隐藏层之间的连接相关的权重W = {w[ij]}, 1 <= i <= |V|, 1 <= j <= |H|
以及偏差a = {a[i]}, 1 <= i <= |V|
用于可见层,偏置b = {b[j]}, 1 <= j <= |H|
用于隐藏层。
RBM 中显然没有输出层,因此学习与前馈网络中的学习有很大不同,如下所示:
- 与其减少描述地面实况与输出层之间差异的损失函数,不如尝试减少能量函数,该函数定义如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0mBuGClS-1681704767264)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d06354bc-4963-4fef-b7aa-f100ae1ce33e.png)]
对于不熟悉能量函数的人,术语能量来自物理学,用于量化大型物体对另一个物体的重力。 物理学中的能量函数测量两个对象或机器学习中两个变量的兼容性。 能量越低,变量之间的兼容性越好,模型的质量越高。
- 与产生输出相反,它在其可见和隐藏单元集上分配概率,并且每个单元在每个时间点处于
0
(关闭)或1
(激活)的二进制状态下。 给定可见层v
,隐藏单元被激活的概率计算如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lPEwILzr-1681704767264)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/33a47fba-518e-4f08-9a61-fafb69291569.png)]
类似地,给定一个隐藏层h
,我们可以如下计算可见单元被激活的概率:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VoF5zr2l-1681704767264)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/40046dd7-01ec-41c0-93d3-aa5df24b71c5.png)]
由于h
和v
的状态基于彼此随机分配给0
或1
,因此可以通过重复少量采样过程实现收敛。 下图演示了此过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bcJxwiiK-1681704767265)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/188cc358-b184-4998-aa60-b5de2c02b12a.png)]
从可见层v^(0)
的初始状态开始,计算P(h | v^(0))
; 隐藏层h^(0)
用P(h | v^(0))
采样,然后计算P(v | h^(0))
。 接下来,基于P(v | h^(0))
采样状态v^(1)
,h^(1)
基于P(h | v^(1))
采样,依此类推。 此过程称为吉布斯采样。 也可以将其视为重建可见层。
- 根据初始状态
v^(0)
和k
个吉布斯步骤之后的状态v^(k)
计算梯度,其中⊗
表示外部乘积:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A3dFxvGA-1681704767265)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d4a21cdd-b149-43ee-aa01-cb823662c83d.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OR4M2Qxh-1681704767265)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/4fb3b1ff-408f-4af7-bda0-4769dcf638c3.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wpu2SdSs-1681704767265)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/3c3310a9-f432-4dc1-b562-942e0fa94f88.png)]
这些梯度称为对比散度。
我希望您现在已经掌握了 RBM 背后的理论。 在简要介绍了 RBM 的演变路径之后,您将在动手部分中增强对 RBM 的理解,我们将在下一节中进行介绍。
RBM 的发展路径
顾名思义,RBM 源自玻尔兹曼机。玻尔兹曼机由 Geoffrey Hinton 和 Paul Smolensky 于 1983 年发明,是一种网络类型,其中所有单元(可见和隐藏)都处于二进制状态并连接在一起。 尽管他们具有学习有趣的表示形式的理论能力,但对他们来说还是有许多实际问题,包括训练时间,训练时间随模型大小呈指数增长(因为所有单元都已连接)。 玻尔兹曼机的总体示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i8prejmQ-1681704767266)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/34c9802d-3a2e-46c7-a490-5bc637bd517f.png)]
为了使学习玻尔兹曼机模型更容易,Paul Smolensky 于 1986 年首次发明了一种称为 Harmonium 的连接受限的版本。 在 2000 年中期,Geoffrey Hinton 和其他研究人员发明了一种效率更高的架构,该架构仅包含一个隐藏层,并且不允许隐藏单元之间进行任何内部连接。 从那时起,RBM 被应用于各种有监督的学习任务中,包括:
- 图像分类(《使用判别受限的玻尔兹曼机进行分类》)
- 语音识别(《使用受限的玻尔兹曼机学习语音声波的更好表示》)
它们也已应用于无监督的学习任务,包括以下内容:
- 降维(《使用神经网络降低数据的维数》)
- 特征学习(《无监督特征学习中的单层网络分析》),当然还有协同过滤和推荐系统 ,我们将在本节之后进行处理
您可能会注意到,RBM 有点浅,只有一个隐藏层。 Geoffrey Hinton 在 2006 年推出了称为 DBN 的深版本的 RBM。DBN 可以看作是堆叠在一起的一组 RBM,其中一个 RBM 的隐藏层是下一个 RBM 的可见层。 隐藏层充当分层特征检测器。 DBN 的一般图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dRBuKpG0-1681704767266)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/51a6bfe7-88bd-47ad-b74b-3bd214cce072.png)]
DBN 也有许多有趣的应用,例如:
- 电话识别(《用于电话识别的深度信念网络》)
- 脑电信号(《脑电图的深层信念网络:对近期贡献的回顾和未来展望》)
- 自然语言理解(《深度信念网络在自然语言理解上的应用》)
按照承诺,我们现在将详细研究 RBM 及其深版本 DBN,然后将其应用于实际问题。
RBM 架构和应用
我们将首先介绍 RBM 及其实现,以及它们在推荐系统中的应用,然后再转到 DBN 并利用它们对图像进行分类。
RBM 及其在 TensorFlow 中的实现
让我们从初始化 RBM 模型的参数开始。 回想一下,RMB 模型由与可见层和隐藏层之间的连接关联的权重W
,可见层的偏置a
和偏置b
组成。 用于隐藏层。 RBM 对象由权重W
,偏差a
和b
,可见单元数和隐藏单元数,吉布斯步骤数构成。 常规神经网络超参数,包括批量大小,学习率和周期数:
>>> import numpy as np
>>> import tensorflow as tf
>>> class RBM(object):
... def __init__(self, num_v, num_h, batch_size, learning_rate,
num_epoch, k=2):
... self.num_v = num_v
... self.num_h = num_h
... self.batch_size = batch_size
... self.learning_rate = learning_rate
... self.num_epoch = num_epoch
... self.k = k
... self.W, self.a, self.b = self._init_parameter()
在属性初始化之后,我们定义_init_
参数方法如下:
>>> def _init_parameter(self):
... """ Initializing the model parameters including weights
and bias
"""
... abs_val = np.sqrt(2.0 / (self.num_h self.num_v))
... W = tf.get_variable('weights', shape=(self.num_v, self.num_h),
initializer=tf.random_uniform_initializer(
minval=-abs_val, maxval=abs_val))
... a = tf.get_variable('visible_bias', shape=(self.num_v),
initializer=tf.zeros_initializer())
... b = tf.get_variable('hidden_bias', shape=(self.num_h),
initializer=tf.zeros_initializer())
... return W, a, b
直观地,我们可以安全地将所有偏差初始化为 0。对于权重,最好使用启发式方法将其初始化。 常用的启发式方法包括:
√(2 / 上一层大小)
√(1 / 上一层大小)
,也称为 xavier 初始化√(2 / 上一层大小 这一层大小)
这些启发式方法有助于防止收敛缓慢,并且通常是权重初始化的良好起点。
正如我们前面提到的,训练 RBM 模型是一个搜索参数的过程,该参数可以通过吉布斯采样最好地重构输入向量。 让我们实现吉布斯采样方法,如下所示:
代码语言:javascript复制>>> def _gibbs_sampling(self, v):
... """
... Gibbs sampling
... @param v: visible layer
... @return: visible vector before Gibbs sampling,
conditional probability P(h|v) before Gibbs sampling,
visible vector after Gibbs sampling,
conditional probability P(h|v) after Gibbs sampling
... """
... v0 = v
... prob_h_v0 = self._prob_h_given_v(v0)
... vk = v
... prob_h_vk = prob_h_v0
... for _ in range(self.k):
... hk = self._bernoulli_sampling(prob_h_vk)
... prob_v_hk = self._prob_v_given_h(hk)
... vk = self._bernoulli_sampling(prob_v_hk)
... prob_h_vk = self._prob_h_given_v(vk)
... return v0, prob_h_v0, vk, prob_h_vk
给定输入向量vk
,吉布斯采样开始于计算P(h | v)
。 然后执行吉布斯步骤。 在每个吉布斯步骤中,隐藏层h
是根据P(h | v)
通过伯努利采样获得的; 计算条件概率P(v | h)
并用于生成可见向量v
的重建版本; 并根据最新的可见向量更新条件概率P(h | v)
。 最后,它返回吉布斯采样之前和之后的可见向量,以及吉布斯采样之前和之后的条件概率P(h | v)
。
现在,我们实现了条件概率P(v | h)
和P(h | v)
的计算,以及伯努利采样:
- 计算
P(v[i] = 1 | h) = sigmoid(a[i] Σ[j] h[j]w[ij])
如下:
>>> def _prob_v_given_h(self, h):
... """
... Computing conditional probability P(v|h)
... @param h: hidden layer
... @return: P(v|h)
... """
... return tf.sigmoid(
tf.add(self.a, tf.matmul(h, tf.transpose(self.W))))
- 计算
P(h[j] = 1 | v) = sigmoid(b[j] Σ[i] v[i]w[ij])
如下:
>>> def _prob_h_given_v(self, v):
... """
... Computing conditional probability P(h|v)
... @param v: visible layer
... @return: P(h|v)
... """
... return tf.sigmoid(tf.add(self.b, tf.matmul(v, self.W)))
- 现在,我们将计算伯努利抽样,如下所示:
>>> def _bernoulli_sampling(self, prob):
... """ Bernoulli sampling based on input probability """
... distribution = tf.distributions.Bernoulli(
probs=prob, dtype=tf.float32)
... return tf.cast(distribution.sample(), tf.float32)
现在我们能够计算吉布斯采样前后的可见输入和条件概率P(h | v)
,我们可以计算梯度,包括ΔW = v[o] ⊗ P(h | v^(o)) - P(h | v^(k))
,Δa = v[0] - v[k]
和Δb = P(h | v^(k)) - P(h | v^(k))
,如下所示:
>>> def _compute_gradients(self, v0, prob_h_v0, vk, prob_h_vk):
... """
... Computing gradients of weights and bias
... @param v0: visible vector before Gibbs sampling
... @param prob_h_v0: conditional probability P(h|v)
before Gibbs sampling
... @param vk: visible vector after Gibbs sampling
... @param prob_h_vk: conditional probability P(h|v)
after Gibbs sampling
... @return: gradients of weights, gradients of visible bias,
gradients of hidden bias
... """
... outer_product0 = tf.matmul(tf.transpose(v0), prob_h_v0)
... outer_productk = tf.matmul(tf.transpose(vk), prob_h_vk)
... W_grad = tf.reduce_mean(outer_product0 - outer_productk, axis=0)
... a_grad = tf.reduce_mean(v0 - vk, axis=0)
... b_grad = tf.reduce_mean(prob_h_v0 - prob_h_vk, axis=0)
... return W_grad, a_grad, b_grad
使用吉布斯采样和梯度,我们可以组合一个以时间为单位的参数更新,如下所示:
代码语言:javascript复制>>> def _optimize(self, v):
... """
... Optimizing RBM model parameters
... @param v: input visible layer
... @return: updated parameters, mean squared error of reconstructing v
... """
... v0, prob_h_v0, vk, prob_h_vk = self._gibbs_sampling(v)
... W_grad, a_grad, b_grad = self._compute_gradients(v0, prob_h_v0, vk,
prob_h_vk)
... para_update=[tf.assign(self.W,
tf.add(self.W, self.learning_rate*W_grad)),
... tf.assign(self.a,
tf.add(self.a, self.learning_rate*a_grad)),
... tf.assign(self.b,
tf.add(self.b, self.learning_rate*b_grad))]
... error = tf.metrics.mean_squared_error(v0, vk)[1]
... return para_update, error
除了更新权重W := W lr * ΔW
,偏差a := a lr * Δa
和偏差b := b lr * Δb
之外,我们还计算了重建可见层的均方误差。
到目前为止,我们已经准备好用于训练 RBM 模型的必要组件,因此下一步是将它们放在一起以形成train
方法,如以下代码所示:
>>> def train(self, X_train):
... """
... Model training
... @param X_train: input data for training
... """
... X_train_plac = tf.placeholder(tf.float32, [None, self.num_v])
... para_update, error = self._optimize(X_train_plac)
... init = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())
... with tf.Session() as sess:
... sess.run(init)
... epochs_err = []
... n_batch = int(X_train.shape[0] / self.batch_size)
... for epoch in range(1, self.num_epoch 1):
... epoch_err_sum = 0
... for batch_number in range(n_batch):
... batch = X_train[batch_number * self.batch_size:
(batch_number 1) * self.batch_size]
... _, batch_err = sess.run((para_update, error),
feed_dict={X_train_plac: batch})
... epoch_err_sum = batch_err
... epochs_err.append(epoch_err_sum / n_batch)
... if epoch % 10 == 0:
... print("Training error at epoch %s: %s" %
(epoch,epochs_err[-1]))
请注意,我们在训练中采用小批量梯度下降,并记录每个周期的训练误差。 整个训练过程都依赖于_optimize
方法,该方法适合每个数据批量上的模型。 它还会每隔10
个时间段输出训练误差,以确保质量。
我们刚刚完成了 RBM 算法的实现。 在下一节中,我们将其应用于电影推荐。
用于电影推荐的 RBM
众所周知,电子商务网站会根据用户的购买和浏览历史向他们推荐产品。 相同的逻辑适用于电影推荐。 例如,Netflix 根据用户在观看的电影上提供的反馈(例如评分)来预测用户喜欢的电影。 RBM 是推荐系统最受欢迎的解决方案之一。 让我们看一下推荐的 RBM 的工作原理。
给定经过训练的 RBM 模型,由用户喜欢,不喜欢和未观看的一组电影组成的输入从可见层转到隐藏层,然后又回到可见层,从而生成输入的重构版本 。 除了与用户进行交互的电影外,重构的输入还包含以前未评级的信息。 它可以预测是否会喜欢这些电影。 一般图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RCr5NTkl-1681704767266)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/166067d5-6d17-45ad-9449-6dbb6edad311.png)]
在此示例中,输入内容包括六部电影,其中三部被点赞(用1
表示),两部不喜欢(用0
表示),而另一部未分级(用?
表示)。 该模型接受输入并对其进行重构,包括缺少的电影。
因此,模型如何知道缺少的单元应为0
(或1
)? 回忆每个隐藏的单元都连接到所有可见的单元。 在训练过程中,一个隐藏的单元试图发现一个潜在因素,该潜在因素可以解释数据中的一个属性,或本例中的所有电影。 例如,一个隐藏的二元单元可以了解电影类型是否是喜剧电影,是否属于正义电影,主题是否为复仇电影或其他任何捕捉到的东西。 在重构阶段,将为输入分配一个新值,该值是根据代表所有这些潜在因素的隐藏单元计算得出的。
听起来神奇吗? 让我们开始构建基于 RBM 的电影推荐器。
我们将使用 MovieLens 中的电影评分数据集。 这是一个非商业性网站,用于收集用户的移动收视率并提供个性化建议,由明尼苏达大学的研究实验室 GroupLens 运营。
首先,我们将在这个页面中查看 1M 基准数据集。 它包含来自 6,040 位用户的 3,706 部电影的大约一百万个收视率。 我们可以通过这里下载数据集并解压缩下载的文件。 等级包含在ratings.dat
文件中,每一行是一个等级,格式如下:
UserID::MovieID::Rating::Timestamp
评分记录如下所示:
代码语言:javascript复制1::1193::5::978300760
2::1357::5::978298709
10::1022::5::979775689
有几件事要注意:
- 用户 ID 的范围是 1 到 6,040
- MovieID 的范围是 1 到 3,952,但并非每部电影都经过分级
- 评分是
{1, 2, 3, 4, 5}
之一 - 每个用户给几部电影评分
我们可以建立 RBM 模型,根据用户和其他人的电影评分推荐用户尚未观看的电影。
您可能会注意到输入额定值不是二进制的。 我们如何将它们提供给 RBM 模型? 最简单的解决方法是二进制化,例如,将大于三的等级转换为1
(类似),否则将其转换为0
(不喜欢)。 但是,这可能会导致信息丢失。 或者,在我们的解决方案中,我们将原始评级缩放到[0,1]
范围,并将每个重新缩放的评级视为获得1
的概率。 也就是说,v = P(v = 1 | h)
,不需要伯努利采样。
现在,让我们加载数据集并构建训练数据集。 不要忘了跟踪已分级的电影,因为并非所有电影都已分级:
代码语言:javascript复制>>> import numpy as np
>>> data_path = 'ml-1m/ratings.dat'
>>> num_users = 6040
>>> num_movies = 3706
>>> data = np.zeros([num_users, num_movies], dtype=np.float32)
>>> movie_dict = {}
>>> with open(data_path, 'r') as file:
... for line in file.readlines()[1:]:
... user_id, movie_id, rating, _ = line.split("::")
... user_id = int(user_id) - 1
... if movie_id not in movie_dict:
... movie_dict[movie_id] = len(movie_dict)
... rating = float(rating) / 5
... data[user_id, movie_dict[movie_id]] = rating
>>> data = np.reshape(data, [data.shape[0], -1])
>>> print(data.shape)(6040, 3706)
训练数据集的大小为6,040 x 3,706
,每行包含3706
缩放等级,包括0.0
,表示未分级。 可以将其显示为下表(虚拟)以获得更直观的视图:
movie_0 | movie_1 | movie_2 | … | … | movie_n | |
---|---|---|---|---|---|---|
user_0 | 0.0 | 0.2 | 0.8 | 0.0 | 1.0 | 0.0 |
user_1 | 0.8 | 0.0 | 0.6 | 1.0 | 0.0 | 0.0 |
user_2 | 0.0 | 0.0 | 1.0 | 1.0 | 0.8 | 0.0 |
… | … | … | … | … | … | … |
user_m | 0.0 | 0.6 | 0.0 | 0.8 | 0.0 | 0.8 |
看一下它们的分布,如下所示:
代码语言:javascript复制>>> values, counts = np.unique(data, return_counts=True)
>>> for value, count in zip(values, counts):
... print('Number of {:2.1f} ratings: {}'.format(value, count))
Number of 0.0 ratings: 21384032
Number of 0.2 ratings: 56174
Number of 0.4 ratings: 107557
Number of 0.6 ratings: 261197
Number of 0.8 ratings: 348971
Number of 1.0 ratings: 226309
我们可以看到矩阵非常稀疏。 同样,那些0
代表未被相应用户评级的电影,而不是获得1
的可能性为零。 因此,在整个训练过程中,我们应将未分级电影的分级保持为零,这意味着我们应在每个吉布斯步骤之后将其还原为0
。 否则,它们的重构值将包含在隐藏层和梯度的计算中,结果,该模型将在很大程度上未优化。
因此,我们修改了_gibbs_sampling
和_optimize
方法,如下所示:
>>> def _gibbs_sampling(self, v):
... """
... Gibbs sampling (visible units with value 0 are unchanged)
... @param v: visible layer
... @return: visible vector before Gibbs sampling,
conditional probability P(h|v) before Gibbs sampling,
... visible vector after Gibbs sampling,
conditional probability P(h|v) after Gibbs sampling
... """
... v0 = v
... prob_h_v0 = self._prob_h_given_v(v0)
... vk = v
... prob_h_vk = prob_h_v0
... for _ in range(self.k):
... hk = self._bernoulli_sampling(prob_h_vk)
... prob_v_hk = self._prob_v_given_h(hk)
... vk_tmp = prob_v_hk
... vk = tf.where(tf.equal(v0, 0.0), v0, vk_tmp)
... prob_h_vk = self._prob_h_given_v(vk)
... return v0, prob_h_v0, vk, prob_h_vk
我们采用v = P(v = 1 | h)
并使用0
恢复等级,如下所示:
>>> def _optimize(self, v):
... """
... Optimizing RBM model parameters
... @param v: input visible layer
... @return: updated parameters, mean squared error of reconstructing v
... """
... v0, prob_h_v0, vk, prob_h_vk = self._gibbs_sampling(v)
... W_grad, a_grad, b_grad = self._compute_gradients(
v0, prob_h_v0, vk, prob_h_vk)
... para_update=[tf.assign(self.W,
tf.add(self.W, self.learning_rate*W_grad)),
... tf.assign(self.a,
tf.add(self.a, self.learning_rate*a_grad)),
... tf.assign(self.b, tf.add(self.b,
self.learning_rate*b_grad))]
... bool_mask = tf.cast(tf.where(tf.equal(v0, 0.0),
x=tf.zeros_like(v0), y=tf.ones_like(v0)),
dtype=tf.bool)
... v0_mask = tf.boolean_mask(v0, bool_mask)
... vk_mask = tf.boolean_mask(vk, bool_mask)
... error = tf.metrics.mean_squared_error(v0_mask, vk_mask)[1]
... return para_update, error
在计算训练误差时,我们只考虑那些额定的电影,否则它将变得非常小。 通过这些更改,我们现在可以安全地将 RBM 模型拟合到训练集上,如以下代码所示:
代码语言:javascript复制>>> rbm = RBM(num_v=num_movies, num_h=80, batch_size=64,
num_epoch=100, learning_rate=0.1, k=5)
我们以80
隐藏单元,64,100
周期的批量大小,0.1
的学习率和5
吉布斯步骤初始化模型,如下所示:
>>> rbm.train(data)
Training error at epoch 10: 0.043496965727907545
Training error at epoch 20: 0.041566036522705505
Training error at epoch 30: 0.040718327296224044
Training error at epoch 40: 0.04024859795227964
Training error at epoch 50: 0.03992816338196714
Training error at epoch 60: 0.039701666445174116
Training error at epoch 70: 0.03954154300562879
Training error at epoch 80: 0.03940619274656823
Training error at epoch 90: 0.03930238915726225
Training error at epoch 100: 0.03921664716239939
训练误差减少到0.039
,我们可以使用训练后的模型推荐电影。 为此,我们需要返回优化的参数并使用这些参数添加预测方法。
在我们之前定义的训练方法中,我们通过更改以下行来保留更新的参数:
代码语言:javascript复制... _, batch_err = sess.run(
(para_update, error),feed_dict={X_train_plac: batch})
我们需要将以下几行替换为:
代码语言:javascript复制... parameters, batch_err = sess.run((para_update, error),
feed_dict={X_train_plac: batch})
然后,我们需要在方法末尾返回最后更新的参数,如下所示:
代码语言:javascript复制... return parameters
引入训练后的模型并重建输入数据的预测方法定义如下:
代码语言:javascript复制>>> def predict(self, v, parameters):
... W, a, b = parameters
... prob_h_v = 1 / (1 np.exp(-(b np.matmul(v, W))))
... h = np.random.binomial(1, p=prob_h_v)
... prob_v_h = 1 /
(1 np.exp(-(a np.matmul(h, np.transpose(W)))))
... return prob_v_h
现在,我们可以获得输入数据的预测,如下所示:
代码语言:javascript复制>>> parameters_trained = rbm.train(data)
>>> prediction = rbm.predict(data, parameters_trained)
以一个用户为例,我们将五星级的电影与未评级的电影进行比较,但预计其评级将高于0.9
。 以下代码均显示了这些内容:
>>> sample, sample_pred = data[0], prediction[0]
>>> five_star_index = np.where(sample == 1.0)[0]
>>> high_index = np.where(sample_pred >= 0.9)[0]
>>> index_movie = {value: key for key, value in movie_dict.items()}
>>> print('Movies with five-star rating:', ',
'.join(index_movie[index] for index in five_star_index))
Movies with five-star rating: 2918, 1035, 3105, 1097, 1022, 1246, 3257, 265, 1957, 1968, 1079, 39, 1704, 1923, 1101, 597, 1088, 1380, 300, 1777, 1307, 62, 543, 249, 440, 2145, 3526, 2248, 1013, 2671, 2059, 381, 3429, 1172, 2690
>>> print('Movies with high prediction:',
', '.join(index_movie[index] for index in high_index if index not
in five_star_index))
Movies with high prediction: 527, 745, 318, 50, 1148, 858, 2019, 922, 787, 2905, 3245, 2503, 53
我们可以在movies.dat
文件中查找相应的电影。 例如,该用户喜欢3257::The Bodyguard
和1101::Top Gun
是有道理的,因此他/她也将喜欢50::The Usual Suspects
,858::The Godfather
和527::Schindler's List
。 但是,由于 RBM 的不受监督的性质,除非我们咨询每个用户,否则很难评估模型的表现。 我们需要开发一种模拟方法来测量预测精度。
我们为每个用户随机选择 20% 的现有评分,并在将其输入经过训练的 RBM 模型中时暂时使它们未知。 然后,我们比较所选模拟等级的预测值和实际值。
首先,让我们将用户分成 90% 的训练集和 10% 的测试集,它们的等级将分别用于训练模型和执行仿真。 如下代码所示:
代码语言:javascript复制>>> np.random.seed(1)
>>> np.random.shuffle(data)
>>> data_train, data_test = data[:num_train, :], data[num_train:, :]
其次,在测试集上,我们从每个用户中随机选择现有评级的 20% 进行模拟,如下所示:
代码语言:javascript复制>>> sim_index = np.zeros_like(data_test, dtype=bool)
>>> perc_sim = 0.2
>>> for i, user_test in enumerate(data_test):
... exist_index = np.where(user_test > 0.0)[0]
... sim_index[i, np.random.choice(exist_index,
int(len(exist_index)*perc_sim))] = True
所选等级暂时变为未知,如下所示:
代码语言:javascript复制>>> data_test_sim = np.copy(data_test)
>>> data_test_sim[sim_index] = 0.0
接下来,我们在训练集上训练模型,并在模拟测试集上进行预测,如下所示:
代码语言:javascript复制>>> rbm = RBM(num_v=num_movies, num_h=80, batch_size=64,
num_epoch=100, learning_rate=1, k=5)
>>> parameters_trained = rbm.train(data_train)
Training error at epoch 10: 0.039383551327600366
Training error at epoch 20: 0.03883369417772407
Training error at epoch 30: 0.038669846597171965
Training error at epoch 40: 0.038585483273934754
Training error at epoch 50: 0.03852854181258451
Training error at epoch 60: 0.03849853335746697
Training error at epoch 70: 0.03846755987476735
Training error at epoch 80: 0.03844876645044202
Training error at epoch 90: 0.03843735127399365
Training error at epoch 100: 0.038423490045326095
>>> prediction = rbm.predict(data_test_sim, parameters_trained)
最后,我们可以通过计算预测值与所选等级的实际值之间的 MSE 来评估预测准确率,如下所示:
代码语言:javascript复制>>> from sklearn.metrics import mean_squared_error
>>> print(mean_squared_error(
data_test[sim_index],prediction[sim_index]))
0.037987366148405505
我们基于 RBM 的电影推荐器可实现0.038
的 MSE。 如果您有兴趣,可以使用更大的数据集,例如位于这里,以及位于这里的 1000 万个收视率数据集。
通过其实现和应用,我们已经获得了更多有关 RBM 的知识。 按照承诺,在下一节中,我们将介绍 RBM 的堆叠架构-DBN。
DBN 及其在 TensorFlow 中的实现
DBN 就是一组堆叠在一起的 RBM,其中一个 RBM 的隐藏层是下一个 RBM 的可见层。 在训练层的参数期间,前一层的参数保持不变。 换句话说,以顺序方式逐层训练 DBN 模型。 通过将每个层添加到顶部,我们可以从先前提取的特征中提取特征。 这就是深度架构的来源,也是 DBN 分层特征检测器的成因。
要实现 DBN,我们需要重用 RBM 类中的大多数代码,因为 DBN 由一系列 RBM 组成。 因此,我们应该为每个 RBM 模型的参数明确定义变量范围。 否则,我们将为多个 RBM 类引用同一组变量,这在 TensorFlow 中是不允许的。 因此,我们添加了一个属性 ID,并使用它来区分不同 RBM 模型的参数:
代码语言:javascript复制>>> class RBM(object):
... def __init__(self, num_v, id, num_h, batch_size,
learning_rate, num_epoch, k=2):
... self.num_v = num_v
... self.num_h = num_h
... self.batch_size = batch_size
... self.learning_rate = learning_rate
... self.num_epoch = num_epoch
... self.k = k
... self.W, self.a, self.b = self._init_parameter(id)
...
>>> def _init_parameter(self, id):
... """ Initializing parameters the the id-th model
including weights and bias """
... abs_val = np.sqrt(2.0 / (self.num_h self.num_v))
... with tf.variable_scope('rbm{}_parameter'.format(id)):
... W = tf.get_variable('weights', shape=(self.num_v,
self.num_h), initializer=tf.random_uniform_initializer(
minval=-abs_val, maxval=abs_val))
... a = tf.get_variable('visible_bias', shape=(self.num_v),
initializer=tf.zeros_initializer())
... b = tf.get_variable('hidden_bias', shape=(self.num_h),
initializer=tf.zeros_initializer())
... return W, a, b
而且,训练的 RBM 的隐藏向量被用作下一个 RBM 的输入向量。 因此,我们定义了一种额外的方法来简化此操作,如下所示:
代码语言:javascript复制>>> def hidden_layer(self, v, parameters):
... """
... Computing hidden vectors
... @param v: input vectors
... @param parameters: trained RBM parameters
... """
... W, a, b = parameters
... h = 1 / (1 np.exp(-(b np.matmul(v, W))))
... return h
RBM 类的其余部分与我们先前实现的类相同。 现在,我们可以处理 DBN,如下所示:
代码语言:javascript复制>>> class DBN(object):
... def __init__(self, layer_sizes, batch_size,
learning_rates, num_epoch, k=2):
... self.rbms = []
... for i in range(1, len(layer_sizes)):
... rbm = RBM(num_v=layer_sizes[i-1], id=i,
num_h=layer_sizes[i], batch_size=batch_size,
learning_rate=learning_rates[i-1],
num_epoch=num_epoch, k=k)
... self.rbms.append(rbm)
DBN 类接受的参数包括layer_sizes
(每层的单元数,从第一个输入层开始),batch_size
,learning_rates
(每个 RBM 单元的学习率列表),num_epoch
和吉布斯步骤k
。
训练方法定义如下,其中在原始输入数据或先前隐藏层的输出上训练隐藏层的参数:
代码语言:javascript复制... def train(self, X_train):
... """
... Model training
... @param X_train: input data for training
... """
... self.rbms_para = []
... input_data = None
... for rbm in self.rbms:
... if input_data is None:
... input_data = X_train.copy()
... parameters = rbm.train(input_data)
... self.rbms_para.append(parameters)
... input_data = rbm.hidden_layer(input_data, parameters)
使用训练过的参数,predict
方法将计算最后一层的输出,如下所示:
... def predict(self, X):
... """
... Computing the output of the last layer
... @param X: input data for training
... """
... data = None
... for rbm, parameters in zip(self.rbms, self.rbms_para):
... if data is None:
... data = X.copy()
... data = rbm.hidden_layer(data, parameters)
... return data
最后一层的输出是提取的特征,这些特征用于下游任务,例如分类,回归或聚类。 在下一节中,我们将说明如何将 DBN 应用于图像分类。
用于图像分类的 DBN
我们将使用的数据集由1797
10 类手写数字图像组成。 每个图像的尺寸为8 x 8
,每个像素值的范围为 0 到 16。让我们读取数据集并将数据缩放到0
到1
的范围,然后将其分为训练和测试集,如下所示 :
>>> from sklearn import datasets
>>> data = datasets.load_digits()
>>> X = data.data
>>> Y = data.target
>>> print(X.shape)
(1797, 64)
>>> X = X / 16.0
>>> np.random.seed(1)
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, Y_train, Y_test =
train_test_split(X, Y, test_size = 0.2)
我们使用一个分别具有两个256
和512
隐藏单元隐藏层的 DBN,并在训练集上对其进行训练,如下所示:
>>> dbn = DBN([X_train.shape[1], 256, 512], 10, [0.05, 0.05], 20, k=2)
>>> dbn.train(X_train)
Training error at epoch 10: 0.0816881338824759
Training error at epoch 20: 0.07888000140656957
Training error at epoch 10: 0.005190357937106303
Training error at epoch 20: 0.003952089745968164
使用训练有素的 DBN,我们为训练和测试集生成最后一个隐藏层的输出向量,如以下代码所示:
代码语言:javascript复制>>> feature_train = dbn.predict(X_train)
>>> feature_test = dbn.predict(X_test)
>>> print(feature_train.shape)
(1437, 512)
>>> print(feature_test.shape)
(360, 512)
然后,我们将提取的 512 维特征输入到逻辑回归模型中以完成数字分类任务,如下所示:
代码语言:javascript复制>>> from sklearn.linear_model import LogisticRegression
>>> lr = LogisticRegression(C=10000)
>>> lr.fit(feature_train, Y_train)
整个算法的流程如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pSAGx8uN-1681704767267)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/4b2953f5-915b-47f0-a1b1-b773f48ece30.png)]
最后,我们使用经过训练的逻辑回归模型来预测从测试集中提取的特征,如下所示:
代码语言:javascript复制>>> print(lr.score(feature_test, Y_test))
0.9777777777777777
用这种方法可以达到 97.8% 的分类精度。
什么是自编码器?
在上一部分中,我们刚刚学习了 RBM 及其变体 DBN,并获得了实践经验。 回想一下,RBM 由输入层和隐藏层组成,后者试图通过查找输入的潜在表示来重建输入数据。 从本节开始,我们将学习的神经网络模型自编码器(AE)具有相似的想法。 基本 AE 由三层组成:输入层,隐藏层和输出层。 输出层是通过隐藏层的输入的重建。 AE 的一般图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wfzXHVFo-1681704767267)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/3c4a7a71-d7ee-4437-8535-ea5452e3f8a7.png)]
可以看到,当自编码器接收数据时,它首先对其进行编码以适合隐藏层,然后尝试将其重新构造回原始输入数据。 同时,隐藏层可以提取输入数据的潜在表示。 由于这种结构,网络的前半部分称为编码器,该编码器将输入数据压缩为潜在表示。 相反,后半部分是解码器,用于对提取的表示进行解压缩。
AE 和 RBM 都旨在最小化重构误差,但是 AE 与 RBM 在以下方面有所不同:
- AE 以判别性方式了解隐藏表示,而无需考虑输入数据的概率分布
- RBM 通过从隐藏层和输入层中进行采样以随机方式找到隐藏表示
现在,让我们快速了解 AE 的发展历程,然后再将其应用于实际问题。
自编码器的发展路径
《无监督预训练的一种方法》首次引入自编码器作为神经网络中模块化学习。 然后《通过多层感知器进行的自动关联和奇异值分解》将它们用于降维,《自编码器,最小描述长度和亥姆霍兹 F 能量》将其用于线性特征学习。
自编码器随着时间的推移而发展,在过去的十年中提出了几种变体。 在 2008 年,P.Vincent 等人。 《使用降噪自编码器提取和构成稳健特征》介绍了去噪自编码器(DAE), 网络被迫从损坏的版本中重建输入数据,以便他们可以学习更强大的特征。
I.Goodfellow 等开发了稀疏自编码器,它通过引入稀疏约束来扩大隐藏表示。 可以在《测量深度网络中的不变性》中找到详细信息。
压缩自编码器由 S. Rifai 在《压缩自编码器:特征提取期间的显式不变性》中提出。 将惩罚项添加到成本函数,以便网络能够提取对输入数据周围的微小变化不太敏感的表示形式。
2013 年,在《自编码变分贝叶斯》中,提出了一种称为变分自编码器(VAE)的特殊类型,其中考虑了潜在变量的概率分布。
我们将在 Keras 中实现 AE 的几种变体,并使用它们来解决信用卡欺诈检测问题。
自编码器架构和应用
我们将从基本的原始 AE 开始,然后是深度版本,然后是稀疏自编码器,去噪自编码器,然后使用收缩自编码器结束。
在整个部分中,我们将以信用卡欺诈数据集为例,演示如何应用各种架构的自编码器。
原始自编码器
这是最基本的三层架构,非常适合于开始实现自编码器。 让我们准备数据集。 我们正在使用的数据集来自 Kaggle 竞赛,可以从这个页面中的Data
页面下载。 每行包含 31 个字段,如下所示:
Time
:自数据集中第一行以来的秒数V1, V2, ..., V28
:通过 PCA 获得的原始特征的主要成分Amount
:交易金额Class
:1
用于欺诈性交易,0
否则
我们将数据加载到 pandas 数据框中,并删除Time
字段,因为它提供的信息很少,如下所示:
>>> import pandas as pd
>>> data = pd.read_csv("creditcard.csv").drop(['Time'], axis=1)
>>> print(data.shape)
(284807, 30)
数据集包含 284,000 个样本,但高度不平衡,几乎没有欺诈性样本,如下所示:
代码语言:javascript复制>>> print('Number of fraud samples: ', sum(data.Class == 1))
Number of fraud samples: 492
>>> print('Number of normal samples: ', sum(data.Class == 0))
Number of normal samples: 284315
从这里的Data
页面中的函数可视化面板中可以看出,V1 至 V28 是高斯标准分布,而Amount
不是。 因此,我们需要标准化Amount
函数,如以下代码所示:
>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> data['Amount'] =
scaler.fit_transform(data['Amount'].values.reshape(-1, 1))
经过预处理后,我们将数据分为 80% 的训练和 20% 的测试,如下所示:
代码语言:javascript复制>>> import numpy as np
>>> np.random.seed(1)
>>> data_train, data_test = train_test_split(data, test_size=0.2)
正如我们所估计的那样,欺诈类仅占总人口的 0.17%,因此传统的监督学习算法可能很难从少数民族中选择足够的模式。 因此,我们求助于基于 AE 的无监督学习解决方案。 训练有素的自编码器可以完美地重建输入数据。 如果我们仅在正常样本上安装自编码器,则该模型将成为仅擅于再现非异常数据的正常数据重构器。 但是,如果我们将此模型输入异常输入,则重构输出和输入之间会有相对较大的差异。 因此,我们可以通过使用 AE 测量重建误差来检测异常。
因此,我们重组了训练和测试集,因为仅需要正常样本即可拟合模型,如下所示:
代码语言:javascript复制>>> data_test = data_test.append(data_train[data_train.Class == 1],
ignore_index=True)
>>> data_train = data_train[data_train.Class == 0]
由于我们的方法不受监督,因此我们不需要训练目标。 因此,我们仅采用训练集中的特征,如下所示:
代码语言:javascript复制>>> X_train = data_train.drop(['Class'], axis=1).values
>>> X_test = data_test.drop(['Class'], axis=1).values
>>> Y_test = data_test['Class']
现在可以使用这些数据了。 现在是时候在 Keras 中构建原始自编码器了。 现在,让我们开始导入必要的模块,如下所示:
代码语言:javascript复制>>> from keras.models import Model
>>> from keras.layers import Input, Dense
>>> from keras.callbacks import ModelCheckpoint, TensorBoard
>>> from keras import optimizers
第一层是输入层,单元为29
(输入数据为29
-维度),如下所示:
>>> input_size = 29
>>> input_layer = Input(shape=(input_size,))
第二层是具有40
单元的隐藏层,对输入数据进行编码,如下所示:
>>> hidden_size = 40
>>> encoder = Dense(hidden_size, activation="relu")(input_layer)
最后,还有最后一层,即输出层,其大小与输入层相同,它对隐藏的表示进行解码,如下所示:
代码语言:javascript复制>>> decoder = Dense(input_size)(encoder)
使用以下代码将它们连接在一起:
代码语言:javascript复制>>> ae = Model(inputs=input_layer, outputs=decoder)
>>> print(ae.summary())
_______________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 29) 0
_______________________________________________________________
dense_1 (Dense) (None, 40) 1200
_______________________________________________________________
dense_2 (Dense) (None, 29) 1189
=================================================================
Total params: 2,389
Trainable params: 2,389
Non-trainable params: 0
_________________________________________________________________
然后,我们使用 Adam(学习率0.0001
)作为优化器来编译模型,如下所示:
>>> optimizer = optimizers.Adam(lr=0.0001)
>>> ae.compile(optimizer=optimizer, loss='mean_squared_error')
除了模型检查点之外,我们还使用 TensorBoard 作为回调函数。 TensorBoard 是 TensorFlow 的表现可视化工具,可提供训练和验证指标的动态图,例如:
代码语言:javascript复制>>> tensorboard = TensorBoard(log_dir='./logs/run1/',
write_graph=True, write_images=False)
>>> model_file = "model_ae.h5"
>>> checkpoint = ModelCheckpoint(model_file, monitor='loss',
verbose=1, save_best_only=True, mode='min')
最后,我们使用数据(X_train, X_train
)对模型进行拟合,并使用数据(X_test, X_test
)作为自编码器进行验证,并尝试产生与输入相同的输出:
>>> num_epoch = 30
>>> batch_size = 64
>>> ae.fit(X_train, X_train, epochs=num_epoch, batch_size=batch_size,
shuffle=True, validation_data=(X_test, X_test),
verbose=1, callbacks=[checkpoint, tensorboard])
以下是第一个和最后一个3
周期的结果:
Train on 227440 samples, validate on 57367 samples
Epoch 1/30
227440/227440 [==============================] - 4s 17us/step - loss: 0.6690 - val_loss: 0.4297
Epoch 00001: loss improved from inf to 0.66903, saving model to model_ae.h5
Epoch 2/30
227440/227440 [==============================] - 4s 18us/step - loss: 0.1667 - val_loss: 0.2057
Epoch 00002: loss improved from 0.66903 to 0.16668, saving model to model_ae.h5
Epoch 3/30
227440/227440 [==============================] - 4s 17us/step - loss: 0.0582 - val_loss: 0.1124
......
......
Epoch 28/30
227440/227440 [==============================] - 3s 15us/step - loss: 1.4541e-05 - val_loss: 0.0011
Epoch 00028: loss improved from 0.00001 to 0.00001, saving model to model_ae.h5
Epoch 29/30
227440/227440 [==============================] - 4s 15us/step - loss: 1.2951e-05 - val_loss: 0.0011
Epoch 00029: loss improved from 0.00001 to 0.00001, saving model to model_ae.h5
Epoch 30/30
227440/227440 [==============================] - 4s 16us/step - loss: 1.9115e-05 - val_loss: 0.0010
Epoch 00030: loss did not improve from 0.00001
我们可以在终端中输入以下命令来检出 TensorBoard:
代码语言:javascript复制tensorboard --logdir=logs
它返回以下内容:
代码语言:javascript复制Starting TensorBoard b'41' on port 6006
(You can navigate to http://192.168.0.12:6006)
通过转到http://192.168.0.12:6006
(主机可能有所不同,具体取决于您的环境),我们可以看到随着时间的推移训练损失和验证损失。
下图显示了平滑为 0(无指数平滑)时的训练损失:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHeaSbfM-1681704767267)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/7a055f42-2a52-4db7-824e-8a4cc5ab5a1e.png)]
此处显示了平滑为 0(无指数平滑)时的验证损失:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MxfesJqb-1681704767267)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/a441f9d5-c897-4251-abb4-0647eb5536be.png)]
现在,我们可以将测试集提供给训练有素的模型,并计算由均方误差测量的重构误差,如下所示:
代码语言:javascript复制>>> recon = ae.predict(X_test)
>>> recon_error = np.mean(np.power(X_test - recon, 2), axis=1)
通常,我们将计算 ROC 曲线下的面积,以评估不平衡数据的二分类表现,如下所示:
代码语言:javascript复制>>> from sklearn.metrics import (roc_auc_score,
precision_recall_curve, auc, confusion_matrix)
>>> roc_auc = roc_auc_score(Y_test, recon_error)
>>> print('Area under ROC curve:', roc_auc)
Area under ROC curve: 0.9548928080050032
实现了 ROC 0.95
的 AUC。 但是,由于少数类别很少发生(在测试集中约为 0.87%),因此在这种情况下并不一定表示表现良好。 ROC 的 AUC 可以轻松达到0.9
以上,而无需任何智能模型。 相反,我们应该通过精确调用曲线下的面积来衡量表现,该曲线绘制如下:
>>> import matplotlib.pyplot as plt
>>> precision, recall, th =
precision_recall_curve(Y_test, recon_error)
>>> plt.plot(recall, precision, 'b')
>>> plt.title('Precision-Recall Curve')
>>> plt.xlabel('Recall')
>>> plt.ylabel('Precision')
>>> plt.show()
请参考以下曲线图,以得到精确的召回曲线:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zNUNKU4N-1681704767267)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/a81f053f-76a4-4f5f-9901-f625bf18162d.png)]
精确调用曲线下的面积计算如下:
代码语言:javascript复制>>> area = auc(recall, precision)
>>> print('Area under precision-recall curve:', area)
Area under precision-recall curve: 0.8217824584439969
精确召回曲线下的面积为 0.82。 我们还可以绘制各种决策阈值下的精度和召回率,如下所示:
代码语言:javascript复制>>> plt.plot(th, precision[1:], 'k')
>>> plt.plot(th, recall[1:], 'b', label='Threshold-Recall curve')
>>> plt.title('Precision (black) and recall (blue) for different
threshold values')
>>> plt.xlabel('Threshold of reconstruction error')
>>> plt.ylabel('Precision or recall')
>>> plt.show()
请参考以下图表以获得预期结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MevFi6xz-1681704767270)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/3c455bf0-56c6-4d87-87de-8fdf6dbd4094.png)]
可以看出,我们设置的阈值越高,精度越高,但是召回率却越低。 我们将选择0.000001
作为决策阈值并计算混淆矩阵,如下所示:
>>> threshold = 0.000001
>>> Y_pred = [1 if e > threshold else 0 for e in recon_error]
>>> conf_matrix = confusion_matrix(Y_test, Y_pred)
>>> print(conf_matrix)
[[55078 1797]
[ 73 419]]
基于 AE 的异常检测器成功捕获了大多数欺诈交易,并且仅错误地拒绝了一些正常交易。 您可以根据特定的折衷考虑其他决策阈值。
深度自编码器
除了一个隐藏层,输出层可以是通过几个隐藏层对输入的重构。 例如,以下是分别具有80
,40
和80
单元的三个隐藏层的模型:
>>> hidden_sizes = [80, 40, 80]
>>> input_layer = Input(shape=(input_size,))
>>> encoder = Dense(hidden_sizes[0], activation="relu")(input_layer)
>>> encoder = Dense(hidden_sizes[1], activation="relu")(encoder)
>>> decoder = Dense(hidden_sizes[2], activation='relu')(encoder)
>>> decoder = Dense(input_size)(decoder)
>>> deep_ae = Model(inputs=input_layer, outputs=decoder)
>>> print(deep_ae.summary())
_______________________________________________________________
Layer (type) Output Shape Param #
===============================================================
input_1 (InputLayer) (None, 29) 0
_______________________________________________________________
dense_1 (Dense) (None, 80) 2400
_______________________________________________________________
dense_2 (Dense) (None, 40) 3240
_______________________________________________________________
dense_3 (Dense) (None, 80) 3280
_______________________________________________________________
dense_4 (Dense) (None, 29) 2349
===============================================================
Total params: 11,269
Trainable params: 11,269
Non-trainable params: 0
_________________________________________________________________
由于要训练的参数更多,我们将学习率降低到0.00005
,并增加了周期数,如下所示:
>>> optimizer = optimizers.Adam(lr=0.00005)
>>> num_epoch = 50
其余代码与普通解决方案相同,在此不再赘述。 但是,这是前两个周期的结果:
代码语言:javascript复制Epoch 1/50
227440/227440 [==============================] - 6s 25us/step - loss: 0.5392 - val_loss: 0.3506
Epoch 00001: loss improved from inf to 0.53922, saving model to model_deep_ae.h5
......
......
Epoch 49/50
227440/227440 [==============================] - 6s 26us/step - loss: 3.3581e-05 - val_loss: 0.0045
Epoch 00049: loss improved from 0.00004 to 0.00003, saving model to model_deep_ae.h5
Epoch 50/50
227440/227440 [==============================] - 6s 25us/step - loss: 3.4013e-05 - val_loss: 0.0047
Epoch 00050: loss did not improve from 0.00003
同样,我们通过精确调用曲线下的面积来测量表现,这次完成了0.83
,这比原始版本略好:
>>> print('Area under precision-recall curve:', area)
Area under precision-recall curve: 0.8279249913991501
稀疏自编码器
在训练神经网络时,我们通常会在损失目标函数中施加约束,以控制网络的容量并防止过拟合。 自编码器也不例外。 我们可以在自编码器的损失函数中添加 L1 范数正则化项,从而引入稀疏约束。 这种自编码器称为稀疏自编码器。
当训练样本很多时,例如我们的案例超过 220,000,很难说出稀疏性的影响。 因此,我们仅将 5% 的数据用于训练,如下所示:
代码语言:javascript复制>>> data_train, data_test = train_test_split(data, test_size=0.95)
我们将快速通过常规自编码器进行基准测试,如下所示:
代码语言:javascript复制>>> hidden_sizes = [80, 40, 80]
>>> input_layer = Input(shape=(input_size,))
>>> encoder = Dense(hidden_sizes[0], activation="relu")(input_layer)
>>> encoder = Dense(hidden_sizes[1], activation="relu")(encoder)
>>> decoder = Dense(hidden_sizes[2], activation='relu')(encoder)
>>> decoder = Dense(input_size)(decoder)
>>> ae = Model(inputs=input_layer, outputs=decoder)
除了0.0008
和30
周期的学习率外,其余代码与上一节相同:
>>> optimizer = optimizers.Adam(lr=0.0008)
>>> num_epoch = 30
以下是前两个周期的结果:
代码语言:javascript复制Train on 14222 samples, validate on 270585 samples
Epoch 1/30
14222/14222 [==============================] - 3s 204us/step - loss: 0.5800 - val_loss: 0.2497
Epoch 00001: loss improved from inf to 0.57999, saving model to model_ae.h5
Epoch 2/30
14222/14222 [==============================] - 3s 194us/step - loss: 0.1422 - val_loss: 0.1175
Epoch 00002: loss improved from 0.57999 to 0.14224, saving model to model_ae.h5
......
......
Epoch 29/30
14222/14222 [==============================] - 3s 196us/step - loss: 0.0016 - val_loss: 0.0054
Epoch 00029: loss did not improve from 0.00148
Epoch 30/30
14222/14222 [==============================] - 3s 195us/step - loss: 0.0013 - val_loss: 0.0079
Epoch 00030: loss improved from 0.00148 to 0.00132, saving model to model_ae.h5
>>> print('Area under precision-recall curve:', area)
Area under precision-recall curve: 0.6628715223813105
我们在0.66
的精确调用曲线下获得了面积。
现在,让我们使用 L1 正则化因子0.00003
的稀疏版本,如下所示:
>>> from keras import regularizers
>>> input_layer = Input(shape=(input_size,))
>>> encoder = Dense(hidden_sizes[0], activation="relu",
activity_regularizer=regularizers.l1(3e-5))(input_layer)
>>> encoder = Dense(hidden_sizes[1], activation="relu")(encoder)
>>> decoder = Dense(hidden_sizes[2], activation='relu')(encoder)
>>> decoder = Dense(input_size)(decoder)
>>> sparse_ae = Model(inputs=input_layer, outputs=decoder)
前两个周期的结果如下:
代码语言:javascript复制Epoch 1/30
14222/14222 [==============================] - 3s 208us/step - loss: 0.6295 - val_loss: 0.3061
Epoch 00001: loss improved from inf to 0.62952, saving model to model_sparse_ae.h5
Epoch 2/30
14222/14222 [==============================] - 3s 197us/step - loss: 0.1959 - val_loss: 0.1697
Epoch 00002: loss improved from 0.62952 to 0.19588, saving model to model_sparse_ae.h5
......
......
Epoch 29/30
14222/14222 [==============================] - 3s 209us/step - loss: 0.0168 - val_loss: 0.0277
Epoch 00029: loss improved from 0.01801 to 0.01681, saving model to model_sparse_ae.h5
Epoch 30/30
14222/14222 [==============================] - 3s 213us/step - loss: 0.0220 - val_loss: 0.0496
Epoch 00030: loss did not improve from 0.01681
使用稀疏自编码器可以实现精确调用曲线0.70
下更大的区域,该稀疏自编码器学习输入数据的稀疏表示和放大表示:
>>> print('Area under precision-recall curve:', area)
Area under precision-recall curve: 0.6955808468297678
去噪自编码器
去噪自编码器(DAE)是自编码器的另一种规范化版本,但是该规范化是在输入数据上添加的,而不是损失函数。 自编码器被迫从损坏的输入数据中重建原始数据,以期希望学习到更强大的特征。
对于每个输入样本,将随机选择一组特征进行更改。 建议将腐败率定为 30% 至 50%。 通常,训练样本越多,腐败率越低; 样本越少,腐败率越高。
有两种典型的方法来生成损坏的数据:
- 为所选数据分配零
- 将高斯噪声添加到所选数据
下图演示了 DAE 的工作方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JoVyN7FE-1681704767270)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/bb8c5846-e12f-43d0-84c8-6990c4688891.png)]
DAE 通常用于神经网络预训练,其中提取的鲁棒表示形式用作下游监督学习的输入特征。 因此,它们不适用于我们的无监督解决方案。 您可以通过这个页面中的图像分类示例进行进一步研究。
压缩自编码器
我们将学习的最后一种自编码器是压缩自编码器。 它们与稀疏兄弟相似,因为它们增加了惩罚项以学习更强大的表示形式。 但是,惩罚项更为复杂,可以如下推导,其中h[j]
是隐藏层第j
个单元的输出,W
是编码器的权重,W[ij]
是连接第i
个输入单元,以及第j
个隐藏单元的权重:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0F8mndfD-1681704767271)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/3819616b-4468-43f3-b392-72d7523ae417.png)]
我们在上一节中定义的原始自编码器的顶部添加压缩项,如下所示:
代码语言:javascript复制>>> hidden_size = 40
>>> input_layer = Input(shape=(input_size,))
>>> encoder = Dense(hidden_size, activation="relu")(input_layer)
>>> decoder = Dense(input_size)(encoder)
>>> contractive_ae = Model(inputs=input_layer, outputs=decoder)
损失函数现在变为:
代码语言:javascript复制>>> factor = 1e-5
>>> def contractive_loss(y_pred, y_true):
... mse = K.mean(K.square(y_true - y_pred), axis=1)
... W = K.variable(
value=contractive_ae.layers[1].get_weights()[0])
... W_T = K.transpose(W)
... W_T_sq_sum = K.sum(W_T ** 2, axis=1)
... h = contractive_ae.layers[1].output
... contractive = factor *
K.sum((h * (1 - h)) ** 2 * W_T_sq_sum, axis=1)
... return mse contractive
我们使用这种收缩损失来编译模型,如下所示:
代码语言:javascript复制>>> contractive_ae.compile(optimizer=optimizer, loss=contractive_loss)
其余代码保持不变,但是这次使用0.0003
(optimizer = optimizers.Adam(lr=0.0003)
)的学习率。
我们在此介绍前两个周期的结果:
代码语言:javascript复制Train on 227440 samples, validate on 57367 samples
Epoch 1/30
227440/227440 [==============================] - 6s 27us/step - loss: 0.3298 - val_loss: 0.1680
Epoch 00001: loss improved from inf to 0.32978, saving model to model_contractive_ae.h5
Epoch 2/30
227440/227440 [==============================] - 5s 24us/step - loss: 0.0421 - val_loss: 0.0465
Epoch 00002: loss improved from 0.32978 to 0.04207, saving model to model_contractive_ae.h5
......
......
Epoch 29/30
227440/227440 [==============================] - 5s 23us/step - loss: 3.8961e-04 - val_loss: 0.0045
Epoch 00029: loss did not improve from 0.00037
Epoch 30/30
227440/227440 [==============================] - 5s 22us/step - loss: 4.7208e-04 - val_loss: 0.0057
Epoch 00030: loss did not improve from 0.00037
该模型以0.83
的精确召回曲线下的面积胜过原始模型:
>>> print('Area under precision-recall curve:', area)
Area under precision-recall curve: 0.8311662962345293
到目前为止,我们已经研究了五种不同类型的自编码器,包括基本的原始编码,深度编码,稀疏编码,去噪编码和收缩编码。 每种类型的自编码器的特长来自某些架构或不同形式的强制约束。 尽管架构或处罚有所不同,但它们具有相同的目标,即学习更强大的表示形式。
总结
我们刚刚使用受限的玻尔兹曼机和自编码器完成了 DL 架构的重要学习旅程! 在本章中,我们更加熟悉 RBM 及其变体。 我们从 RBM 是什么,RBM 的演变路径以及它们如何成为推荐系统的最新解决方案入手。 我们从零开始在 TensorFlow 中实现了 RBM,并构建了基于 RBM 的电影推荐器。 除了浅层架构之外,我们还探索了称为深度信念网络的 RBM 的堆叠版本,并将其用于图像分类,该分类从零开始在 TensorFlow 中实现。
学习自编码器是旅程的后半部分,因为它们具有相似的想法,即通过输入数据重建来寻找潜在的输入表示形式。 在讨论了什么是自编码器并讨论了它们的发展路径之后,我们说明了各种自编码器,这些编码器按其架构或正则化形式进行了分类。 我们还在信用卡欺诈检测中应用了不同类型的自编码器。 每种类型的自编码器都打算提取某些结构或强制形式的鲁棒表示。
练习
您可以使用自编码器构建电影推荐器吗?
致谢
感谢 Shyong Lam 和 Jon Herlocker 清理并生成了 MovieLens 数据集:
F. Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4, Article 19 (December 2015), 19 pages. DOI=http://dx.doi.org/10.1145/2827872
第 2 节:卷积神经网络
在本节中,我们将学习一类用于图像的深度学习网络,称为卷积神经网络(CNN),以及为什么 CNN 比深度前馈网络更好 。 然后,我们将研究如何减少深度学习网络所需的计算成本,并看到移动神经网络不过是经过修改以具有更少参数和更少内存的 CNN。
本节将介绍以下章节:
- “第 4 章”,“CNN 架构”
- “第 5 章”,“移动神经网络和 CNN”
四、CNN 架构
在本章中,我们将讨论一类重要的图像深度学习网络,称为卷积神经网络(CNN)。 针对图像相关任务(例如图像识别,分类,对象检测等)而构建的大多数深度学习模型都将 CNN 作为其主要网络。 CNN 允许我们以三维体积而不是单维向量处理传入的数据。 尽管 CNN 是一类神经网络(由权重,层和损失函数组成),但深层前馈网络在结构上有很多差异,我们将在本章中进行解释。 为了让您了解 CNN 的功能强大,ResNet CNN 架构在世界著名的图像分类挑战 ILSVRC 上实现了 3.57% 的最高错误率。 这种表现优于 ImageNet 强大数据集上的人类视觉感知。 我们将在本章后面讨论 ImageNet 和 ILSVRC。 以下是本章将要学习的主题:
- 深度前馈网络的问题以及对 CNN 的需求
- CNN 的演进之路
- CNN 的架构
- CNN 的不同层及其作用
- 使用 CNN 的图像分类
- 一些著名的图像分类 CNN 架构
- 您的第一个具有 CIFAR-10 数据集的 CNN 图像分类器
- 使用 CNN 的对象检测
- 使用 CNN 的著名物体检测器
- 您的第一个 TensorFlow 对象检测器
深度前馈网络存在的问题
在“第 2 章”,“深度前馈网络”中,我们学习了使用深度前馈网络识别(分类)时尚商品的图像。 每个图像的大小为28 x 28
,我们将一个神经元连接到每个像素。 这样,我们在第一层本身就有28 x 28 = 784
个神经元。 但是在现实世界中,图像几乎没有这么小。 让我们考虑一个大小为500 x 500
的中型图像。因此,现在,在第一层中,我们将需要 250,000 个神经元。 对于这样大小的图像,第一层中有大量的神经元。 因此,网络对于该任务而言在计算上变得过于昂贵。 那么,我们如何解决这个问题呢? 同样,生物学的灵感来了! 在下一节中,我们将详细介绍 CNN 的发展。
CNN 的演进之路
在 1960 年代,人们发现动物的视觉皮层并没有像深度前馈网络那样处理图像。 而是,视觉皮层中的单个神经元连接到一个小的区域(而不是单个像素),这称为感受野。 感受野中的任何活动都会触发相应的神经元。
受视觉皮层感受野的启发,科学家提出了局部连接的想法,以减少处理图像所需的人工神经元数量。 深度前馈网络的这种修改版本称为 CNN(在本书中,CNN 均指卷积神经网络)。 1989 年,Yann LeCun 开发了可训练的 CNN,能够识别手写数字。 1998 年,Yann LeCun 的 LeNet-5 模型再次成功地使用了七个堆叠的卷积层(例如深前馈网络中的层)对大小为32 x 32
的数字进行分类。输入图像尺寸的增加由于缺乏处理能力而受到限制。 当时可用。 但是在 2000 年代初,GPU 能够执行并行计算,因此大大减少了深度学习网络所需的处理时间。 更深的 CNN 的开发始于 GPU。 在深入研究细节之前,我们将向您介绍 ImageNet。 它是一个开放源代码数据集,其中包含 1500 万张贴有大约 22,000 个不同对象的图像。 ImageNet 的建立旨在帮助在对象识别领域下开发具有手动标记图像的模型,以进行模型训练。 每年都会举办一场名为 ImageNet 大规模视觉识别挑战赛(ILSVRC)的竞赛,该竞赛使用 ImageNet 数据集的一个子集,以挑战为对象提供更准确的识别方法,俗称图像分类。 有关更多详细信息,请参考 ImageNet 网站。
CNN 中有很多新方面,例如权重共享,批量操作和本地连接。 在下一节中,我们将讨论所有这些以及架构。
CNN 的架构
CNN 当然是像深度前馈网络一样的神经网络。 CNN 以可学习的权重逐层构建,并且像任何典型的深度学习网络一样受到训练:通过最小化成本函数和反向传播误差。 区别在于神经元的连接方式。 CNN 旨在处理图像。 图像数据具有 CNN 用来减少神经元数量并获得更好学习的两个独特特点:
- 图像是三维体积-宽度,高度和通道(通道有时称为深度)。 因此,卷积层以三维体积而不是单维向量进行输入和输出。
- 邻域中的像素具有彼此相关的值。 这称为空间关系。 CNN 通过过滤器使用此功能,以提供与附近像素的神经元的本地连接。
在以下小节中,我们将研究 CNN 涉及的各层以及每一层的独特功能。
输入层
输入层由 3D 数组而不是一维向量组成。 该层以像素在图像中的方式保存像素。 因此,输入层具有形状(批大小,宽度,高度和通道)。 例如,如果我们具有尺寸为32 x 32
的图像和三个通道的 RGB,且批大小为 64,则输入层的形状将为(64, 32, 32, 3)
。 在下图中可以看到这一点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dJ605vV-1681704767271)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/19387290-7362-4924-9ce7-791916ed6e8a.png)]
输入层的形状由粉红色的立方体表示(图像来自 Wikipedia,CS231n 课程的一部分)
这里要注意的重要一点是,输入层用于将结构保持在三维空间中。 在下一节中,我们将看到卷积层如何利用此三维空间。
卷积层
我们将在这里讨论的第一件事是过滤器。 可以将过滤器视为由可学习的权重值组成的图像的较小版本。 就像我们在深层前馈网络中从一个神经元到另一个神经元具有权重连接一样,权重也存在于卷积层中,不同之处在于权重是以过滤器的形式将连接器覆盖的空间区域连接到神经元。 让我们考虑一个大小为5 x 5
(宽度和高度)的过滤器的示例。 过滤器也将延伸到图像的第三维(通道)。 对于三通道图像,过滤器尺寸为5 x 5 x 3
,对于单通道图像,过滤器尺寸为5 x 5 x 1
。下图显示了5 x 5 x 1
过滤器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAijzA1Z-1681704767271)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/3e990b97-ec1f-423b-8c5b-c84ab3dec1ae.png)]
那么,过滤器在卷积层中做什么? 过滤器在卷积层中执行两项非常重要的任务-本地连接和参数共享。 之前,我们讨论了 CNN 中的感受野,这意味着仅将神经元连接到其邻域图像像素。 该邻域由过滤器定义。 我们在图像上滑动过滤器,过滤器中的每个权重都连接到特定幻灯片的特定神经元。 然后,神经元使用该位置处过滤器覆盖的图像像素的权重和值来计算卷积输出。 换句话说,我们可以说卷积层中的每个神经元都局部地连接到由过滤器定义的图像的局部区域。 这称为本地连接。 下图显示了本地连接:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5JskLJ4-1681704767271)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9e061d11-a5a7-409f-9d2d-34ad6722b6ae.png)]
观察感受野(输入层中的深粉红色斑块)如何连接到下一层的单个神经元
神经元如何通过卷积计算过滤器的输出? 为了理解这一点,让我们考虑在6 x 6 x 1
图像上放置3 x 3 x 1
过滤器的第一张幻灯片的场景。 计算过滤器中每个权重值与对应位置的像素值的点积。 对一个位置上所有权重值的点积求和,此计算出的总和就是卷积的输出。 激活函数(例如 ReLU)用于神经元的输出中。
接下来,我们将看到过滤器如何在图像上滑动以生成卷积输出。 对于过滤器的每张幻灯片,都会将新的神经元连接到过滤器输出。 因此,滑动所涉及的参数也趋向于控制卷积层的输出尺寸。 过滤器的滑动涉及三个重要参数-跨步,零填充和深度:
- 跨步决定了过滤器从一个位置滑动到另一位置时跳跃的像素数。 通常,跨步值保持为 1。过滤器在每张幻灯片中跳一个像素。 跨度也可以大于 1,但通常不使用。
- 通常,如果过滤器从图像的左上角开始滑动,则所有幻灯片的最终生成输出趋向于具有较小的尺寸。 但是,通常,我们希望卷积层的输出具有与输入图像相同的宽度和高度。 零填充在图像的边界上添加了 0 的额外填充,从而为过滤器提供了额外的滑动空间,使得最终输出的尺寸与输入的尺寸相同。 当我们加 0 时,这不会影响卷积运算的值。
- 通常,CNN 在层中不使用单个过滤器。 我们使用一组过滤器(例如 12 个过滤器)。 这样做是因为具有不同权重集的每个过滤器趋向于捕获图像的不同特征。 来自每个过滤器的响应被一个接一个地堆叠,并且每个响应被称为激活映射。 例如,如果我们使用
32 x 32 x 1
的图像和四个大小为3 x 3 x 1
的过滤器,且跨步为 2,填充为 1,则卷积层的输出尺寸为(16 x 16 x 4
)。 在这里,最后一个维度将等于激活图的数量,该数量将等于过滤器的数量。 可以使用以下公式计算输出的宽度和高度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UssxEeuF-1681704767271)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/4a40c8d8-8ea1-4317-890e-70a3b19d3297.png)]
在这里,W
是输入大小(W = 32
),F
是过滤器大小(F = 3
),P
为填充(P = 1
),而S
为跨步(S = 1
)。
您可能已经观察到,我们正在整个图像上制作相同的过滤器幻灯片。 这意味着在幻灯片中使用相同的权重,而不是为每个幻灯片创建不同的权重集。 由于图像中不同位置的像素值高度相关,因此使卷积幻灯片共享权重会产生良好的效果。 如果证明过滤器在图像中的某个位置有用,则该过滤器也将在不同位置有用。 整个图像上的过滤器权重共享称为参数共享,大大减少了网络中所需参数的数量。
下一层称为最大池化层。 最大池化层用于减小激活图的大小。
最大池化层
CNN 的总体思想是保持通过过滤器提取特征并增加激活图的深度,同时减小宽度和高度尺寸,以便最终剩下高度压缩的特征向量。 为了减小激活图的尺寸,CNN 使用连续卷积层之间的最大池化层。
最大池化层具有两个主要参数-核大小和跨步。 最大池化还在其连接到的上一层的激活图上滑动一个窗口。 该窗口通常称为核。 核在任何幻灯片上的工作都是比较核所覆盖的值,并且仅保留最大值作为该位置的输出。 最常用的核大小是2 x 2
。使用超出此大小的核大小会导致各层之间的信息大量丢失。 同样,跨步是决定核在幻灯片中跳转多少像素的参数。 下图演示了核在大小为2 x 2
,跨步为 2 的4 x 4
激活图上执行最大池化的过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PyFUDU9-1681704767272)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/1083fec4-6e27-4b6f-9133-bab4b3af0908.png)]
来自 CS231n 的图像
下图显示了如何使用最大池化层来减小图像和特征映射的大小:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SdHRXJGw-1681704767272)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/78bd04c3-e196-4683-a117-02a039840a43.png)]
图片来自该课程 CS231n 课程
全连接层
在卷积网络的末端,我们需要对图像进行分类并训练网络。 这必须使用 softmax 概率和交叉熵损失来完成。 到现在为止,特征已与卷积层一起提取。 这里的想法是将最后一个卷积或最大池化层的 4D 张量输出压缩到 2D 张量中,其中第一个维度仍将代表批量大小,第二个维度将包含来自最后一层的所有输出值(像数组一样被压缩) 。 此压缩操作通常称为展开操作。 展平的目的是我们现在可以在前添加前馈层,展平后将它们连接到所有值,然后像在深前馈网络中那样使用 softmax 概率和交叉熵损失训练网络。 该层与前馈层相同,但称为全连接层,因为与卷积层不同,因为它仅具有本地连接性,所以该层连接到来自最后一层的每个值。 通常,如果展平后的参数数量很大,我们可以添加一系列全连接层。
现在我们已经了解了卷积网络中的架构和层,在下一节中,我们将使用卷积网络进行图像分类。
使用 CNN 的图像分类
在本节中,我们将介绍一些用于图像分类任务的最成功的 CNN 架构,例如 VGGNet,InceptionNet 和 ResNet。 这些网络由于其强大的特征提取功能,还被用作对象检测模型中的特征提取器。 我们将在以下小节中简要讨论网络。
VGGNet
VGGNet 由牛津大学的 K. Simonyan 和 A. Zisserman 开发。 该网络在 ILSVRC 2014 上获得亚军。VGGNet 是 AlexNet 的改进,用较小的3 x 3
卷积代替了 11 和 5 的较高卷积,在多个堆叠层上保持一致。 尽管 VGGNet 并非 ILSVRC 的赢家,但其简单,易于实现的架构及其强大的特征提取功能使 VGGNet 成为对象检测或分割任务中基础网络的明智选择。
VGGNet 具有许多基于堆叠层数的变体。 分别具有 16 层和 19 层的 VGG16 和 VGG19 是最常用的架构。 下图演示了具有3 x 3
卷积层,maxpooling 和全连接层的 VGG16 架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGdUj7To-1681704767272)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/8e22521e-6e42-4d7a-a142-27d8ccec557e.png)]
如果您希望参考 VGGNet 的原始论文,请访问以下链接。
接下来,我们将讨论 ILSVRC 2014 的获奖架构-InceptionNet。
GoogLeNet
GoogLeNet(通常称为 InceptionNet)是 2014 年 ILSVRC 竞赛的获胜者。 让我们在以下几点进行讨论:
- 在卷积神经网络中,选择合适的核大小进行卷积总是一件大事。 同一对象在不同的图像中可以具有各种大小。 为了捕获不同大小的特征,我们当然需要相应地设置核大小。 当感兴趣的对象覆盖大部分区域时,较大的核通常是好的,而较小的核则适合于本地放置的对象。
- 网络越深,越好! 但是,堆叠很多层会使梯度流变得困难,并导致过拟合。 简而言之,网络的深度在一定程度上受到限制。 超出此限制,网络不再训练。 它只是过拟合。
- 建立网络时,我们需要检查其大小。 建立非常大的网络需要巨大的计算能力,这是非常昂贵的。 建立网络的大量费用可能无法满足成本与效用之间的折衷。
Google 的研究人员为了解决这些问题,设计了一个复杂的层,他们将其称为 Inception 模块。
这个想法是对卷积核并行使用不同大小,而不是在层中使用单个核大小。 这样,网络现在可以选择核大小,并且网络现在可以通过核学习最适合该工作的特征。 相互并行排列核也使架构稀疏,这有助于简化对更深层网络的训练。
典型的 InceptionNet 使用三个大小分别为1 x 1
、3 x 3
和5 x 5
的卷积核。 将所有这三个核的结果连接起来,形成一个输出向量,该向量充当下一层的输入。 初始层还在5 x 5
和5 x 5
核之前添加了5 x 5
卷积,以减小大小。 下图显示了 Inception 模块:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nOXcqj5L-1681704767272)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/7ce8e06b-b573-4d4f-adba-f78d4ac2dbea.png)]
图片取自原始论文《随着卷积一起深入》
可以在这个页面上找到原始论文《随着卷积一起深入》的链接。
接下来,我们将研究一种称为 ResNet 的架构,该架构在对图像进行分类方面声称比人类的感知甚至更好。
ResNet
ResNet 是 ILSVRC 2015 的获奖架构。关于 ResNet 的最令人惊讶的事实是,它在 ILSVRC 上实现了 3.57% 的前五位错误率,超过了人类的视觉感知!
ResNet 暴露了一个问题,该问题一直限制着非常深层网络的训练。 在训练深度网络时,精度达到一定的极限,然后迅速下降。 无论架构如何深入,这种现象都将准确率限制在一定的阈值内。 微软研究公司在名为《深度残差学习的图像识别》论文中介绍了 ResNet,该论文可在这个页面中找到。
在整篇论文中,研究人员声称,与其让网络直接通过函数(例如H(x)
)学习从x
到y
的映射, 它使用残差函数F(x) = H(x) - x
。 可以将函数F(x)
视为代表网络的层,并且可以将其重写为H(x) = F(x) x
。 作者声称优化间接残差函数F(x)
比获得x
到y
的直接优化映射H(x)
容易。
在此,将x
作为该层的输入,将H(x)
作为该层的输出,并将F(x)
作为该层的函数, 我们可以很容易地观察到输入x
将添加到层的输出中,以使最终输出H(x) = F(x) x
。 这种创建从该层的输入到输出的连接,称为残差连接或跳跃连接。 下图显示了具有跳过连接的 ResNet 的构建块:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jGnc2i5t-1681704767272)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/117181dc-715d-412a-8c93-1279b908aac8.png)]
图片来源:深度残差学习,用于图像识别
跳过连接的添加解决了深度网络中的饱和和准确率降低的问题,使架构可以具有更多的层而不饱和。 该架构由 34 层组成,大部分包含3 x 3
卷积过滤器。 为了减小特征映射的宽度和高度,使用了第 2 步卷积。 最后,使用全局平均池,然后使用 1,000 个单元的全连接层。 在下图中,与典型的 VGG-19 和没有残留连接的架构相比,您可以观察到 ResNet 架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5jrs6MeD-1681704767273)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/52e9c62f-3040-47b7-a838-4e0c19477c8c.png)]
图片来源:深度残差学习,用于图像识别
现在是时候建立我们自己的 CNN 网络进行图像分类了。
建立我们的第一个 CNN
在这里,我们将使用著名的 CIFAR-10 数据集来演示使用 CNN 的分类。 如果您不了解 CIFAR 数据集,则以下小节将提供简要说明。
CIFAR
CIFAR 数据集包含近 8000 万张图像。 该数据集是开源的,并由 Alex Krizhevsky,Vinod Nair 和 Geoffrey Hinton 进行整理。 数据集分为两个子集-CIFFAR-10 和 CIFAR-100。 CIFAR-10 数据集具有属于 10 类的图像-飞机,汽车,鸟,马,猫,狗,鹿,青蛙,船和卡车。 CIFAR-10 每个类别中有 6,000 张图像。 这意味着它总共有 60,000 张图像。 50,000 张图像用于训练,10,000 张图像用于测试。 每个图像的尺寸为 32 x 32 x 3,并且每个图像都是 RGB 颜色。 CIFAR-100 数据集与 CIFAR-10 相似,除了有 100 个类别而不是 10 个类别。我们将在这里使用 CIFAR-10 数据集,因为它具有较少的类别。 您可以从 CIFAR 网站下载 CIFAR-10 Python 版本。
数据下载并提取后,您将在提取的文件夹中找到以下文件:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s6WGnjOU-1681704767273)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/fe70c84a-8ab4-422a-811f-f26d1d005a10.png)]
由于数据集很大,因此分为五个批量-data_batch_1
,data_batch_2
,data_batch_3
,data_batch_4
和data_batch_5
,因此我们不需要将完整的数据集加载到内存中。 每个文件中的数据都使用 Python 中的pickle
模块转储。 要从文件中检索数据,我们可以使用 Python 的pickle
模块中的load
方法。 每个批量文件包含 10,000 张图像。 因此,转储向量的尺寸为(10,000 x 3,072
)。 RGB 通道图像被展平为单个尺寸。 (32 x 32 x 3 = 3,072
)。 我们将需要以所需的形式重塑数据。 接下来,我们将看到如何加载和预处理数据以供使用。
数据加载和预处理
让我们首先编写函数来加载数据批并将其重塑为三维图像。 我们还将使用在原始 CIFAR 网站中也提到过的pickle
操作加载数据。 让我们创建一个名为data
的类,该类将包含与数据加载和预处理有关的函数。 我们还将定义一个名为load_data_batch
的函数,用于将数据批量加载到内存中。 在类属性中,我们将创建一个名为labelsDicti
的字典,该字典会将数字标签映射到其实际类。 还创建了逆字典inverseLabelsDicti
,以将实际的类映射到数字标签。 这将有助于我们进行预测:
# first import some essential modules
import numpy as np
import pickle
import matplotlib.pyplot as plt
import os
import sys
import tensorflow as tf
from sklearn.utils import shuffle
# define the path to the directory where you have extracted the zipped data
DATA_DIR = 'cifar-10-batches-py'
#hyper-parameters for the model
BATCH_SIZE = 128
CLASS_NUM = 10
EPOCHS = 20
DROPOUT = 0.5
LEARNING_RATE = 0.001
IMAGE_SIZE = (32, 32)
SEED = 2
class data:
def __init__(self, dataDir, fileName, batchSize, seed, classNum = 10):
self.dataDir = dataDir
self.fileName = fileName
self.classNum = classNum
self.batchSize = batchSize
self.seed = seed
self.labelsDicti = {0:'airplane',1:'automobile',2:'bird',3:'cat',4:'deer',5:'dog',6:'frog',7:'horse',8:'ship',9:'truck'}
self.inverseLabelsDicti = {v:k for k,v in self.labelsDicti.items()}
def load_data_batch(self):
with open(os.path.join(self.dataDir, self.fileName), 'rb') as f:
dataBatch = pickle.load(f, encoding = 'latin1')
#print(dataBatch['data'].shape)
# latin1 encoding has been used to dump the data.
# we don't need filename and other details,
# we will keep only labels and images
self.images = dataBatch['data']
self.labels = dataBatch['labels']
在这里,dataBatch
将是一个包含以下键的字典:
batch_label
:表示文件在5
批量中的哪个批量labels
:从0
到9
的图像的数字标签data
:numpy
形状的数组(10,000 x 3,072
),表示数据filenames
:包含相应图像的名称
我们仅将data
和labels
保留在两个单独的命名属性中,而忽略其他所有内容。 接下来,我们需要将图像重塑为原始形式。 为此,我们首先需要从 10,000 张图像中分离出三个通道。 除了将它们分为三个通道外,我们还将图像重塑为宽度和高度尺寸。 也就是32 x 32
。这里要注意的重要一点是,我们需要先将图像分成通道,然后再分为宽度和高度。 因此,一旦将图像重塑为(1e4, 3, 32, 32)
,我们将需要交换轴。 可以通过numpy
数组上的transpose
函数完成交换:
def reshape_data(self):
# function to reshape and transpose
self.images = self.images.reshape(len(self.images), 3, 32, 32).transpose(0, 2, 3, 1)
现在,我们可以可视化一些图像并查看它们的相应标签。 我们将向data
类添加visualise_data
函数,该函数将获取4
索引列表,并将图像绘制在子图中的这些索引处,并将图像的类别显示为标题:
由于数据集中图像的尺寸较小,绘制的图像将非常模糊。
代码语言:javascript复制def visualise_data(self, indices):
plt.figure(figsize = (5, 5))
for i in range(len(indices)):
# take out the ith image in indices
img = self.images[indices[i]]
# it's corresponding label
label =self.labels[indices[i]]
plt.subplot(2,2,i 1)
plt.imshow(img)
plt.title(self.labelsDicti[label])
plt.show()
您可以创建data
类的对象,并调用我们在该对象上构建的函数以以下方式将其可视化:
dataObj = data(DATA_DIR, 'data_batch_1')
dataObj.load_data_batch()
dataObj.reshape_data()
dataObj.visualise_data([100, 4000, 2, 8000])
# here we have chosen indices 100, 4000, 2, 8000
下面的屏幕快照显示了运行上述代码的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7s015RJy-1681704767273)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/069ef92a-0fec-4796-b1f3-178201d59131.png)]
接下来,我们将标签转换为单一编码形式。 我们已经在“第 2 章”,“深度前馈网络”中讨论了单热编码。 如果您不记得它,可以返回参考“第 2 章”,“深度前馈网络”下的单热编码。 CIFAR-10 的类数为 10,并且类属性classNum
的默认值为10
。 以下函数将标签转换为单热编码:
def one_hot_encoder(self):
# this function will convert the labels into one-hot vectors
# initially the label vector is a list, we will convert it to numpy array,
self.labels = np.array(self.labels, dtype = np.int32)
#converting to one-hot
self.labels = np.eye(self.classNum)[self.labels]
#print(self.labels.shape)
我们对图像进行归一化。 在这里,通过归一化,我们意味着将像素值设置在 0 到 1 之间。这很有用,因为激活函数在 0 到 1 之间时很敏感。每个通道中的像素值在 0 到 255 之间。 因此,我们将图像数组除以 255(这是可能的最大值),以使所有内容介于 0 和 1 之间:
代码语言:javascript复制def normalize_images(self):
# just simply dividing by 255
self.images = self.images / 255
为了促进适当的训练,我们需要调出随机样本。 因此,我们将使用 sklearn 的shuffle
函数对数据进行混洗:
def shuffle_data(self):
# shuffle the data so that training is better
self.images, self.labels = shuffle(self.images, self.labels, random_state = self.seed)
下一个函数将是data
类的重要函数。 该函数将从加载的文件中生成一批数据和标签。 我们知道我们是分批训练模型的,并且我们已经声明了一个超参数BATCH_SIZE
,它决定了一批图像的数量。 因此,该函数将继续循环遍历从文件加载的数据,并每次产生一批大小BATCH_SIZE
。 在这里,使用yield
代替return
,因为yield
保留了函数控制,并且我们创建了生成器对象,而不是一旦使用它们便被销毁的列表,从而节省了我们的内存:
def generate_batches(self):
# function to yield out batches of batchSize from the loaded file
for i in range(0, len(self.images), self.batchSize):
last = min(i self.batchSize, len(self.images))
yield (self.images[i: last], self.labels[i: last])
现在,我们将专注于构建 CNN 模型。 让我们定义另一个类model
,它将包含我们的模型图。 我们还将超参数定义为类的属性:
class model:
def __init__(self, batchSize, classNum, dropOut, learningRate, epochs, imageSize, savePath):
self.batchSize = batchSize
self.classNum = classNum
self.dropOut = dropOut
self.imageSize = imageSize
self.learningRate = learningRate
self.epochs = epochs
self.savePath = savePath
首先,我们将使占位符保留数据和标签。 在这里,请注意,占位符的尺寸将为 4。第一个尺寸将代表批量大小,并且正如我们前面所讨论的,输入层将数据保存为 3D 体积。 其余三个尺寸分别是宽度,高度和图像通道。
这里要做的另一件事是为dropOut
值创建另一个占位符。 由于 TensorFlow 将所有内容都视为张量,因此dropOut
的值也必须是张量。 因此,通过keepProb
,我们将添加dropOut
占位符值:
with tf.name_scope('placeholders') as scope:
self.x = tf.placeholder(shape = [None, self.imageSize[0], self.imageSize[1], 3], dtype = tf.float32, name = 'inp_x')
self.y = tf.placeholder(shape = [None, self.classNum], dtype = tf.float32, name = 'true_y')
self.keepProb = tf.placeholder(tf.float32)
您可以使用不同的过滤器号,核大小和网络中不同数量的层来试用网络架构。 让我们定义模型的第一层。 我们将在第一层中使用 64 个过滤器,核大小为3 x 3
:
#first conv layer with 64 filters
with tf.name_scope('conv_1') as scope:
#tensorflow takes the kernel as a 4D tensor. We can initialize the values with tf.zeros
filter1 = tf.Variable(tf.zeros([3, 3, 3, 64], dtype=tf.float32), name='filter_1')
conv1 = tf.nn.relu(tf.nn.conv2d(self.x, filter1, [1, 1, 1, 1], padding='SAME', name = 'convo_1'))
在 TensorFlow 中,我们需要将过滤器定义为可变 4D 张量。 前三个维度代表过滤器的宽度,高度和深度,第四个维度是我们想要的过滤器的输出数量。 在这里,第三维必须是当前深度,而第四维必须是我们想要的过滤器数量(此处为 64)。
接下来,我们将在网络中添加一个最大池化层:
代码语言:javascript复制 with tf.name_scope('pool_1') as scope:
pool1 = tf.nn.max_pool(conv1, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1],padding='SAME', name = 'maxPool_1')
在此,第二维和第三维表示池化核的宽度和高度。 同样,我们将定义网络的其他层。 我们将逐渐增加深度,并减小宽度和高度:
代码语言:javascript复制with tf.name_scope('conv_2') as scope:
filter2 = tf.Variable(tf.zeros([2, 2, 64, 128], dtype=tf.float32), name='filter_2')
conv2 = tf.nn.relu(tf.nn.conv2d(pool1, filter2, [1, 1, 1, 1], padding='SAME', name = 'convo_2'))
with tf.name_scope('conv_3') as scope:
filter3 = tf.Variable(tf.zeros([2, 2, 128, 128], dtype=tf.float32), name='filter_3')
conv3 = tf.nn.relu(tf.nn.conv2d(conv2, filter3, [1, 1, 1, 1], padding='SAME', name = 'convo_3'))
with tf.name_scope('pool_2') as scope:
pool2 = tf.nn.max_pool(conv3, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1],
padding='SAME', name = 'maxPool_2')
with tf.name_scope('conv_4') as scope:
filter4 = tf.Variable(tf.zeros([1, 1, 128, 256], dtype=tf.float32), name='filter_4')
conv4 = tf.nn.relu(tf.nn.conv2d(pool2, filter4, [1, 1, 1, 1], padding='SAME', name = 'convo_4'))
with tf.name_scope('pool_3') as scope:
pool3 = tf.nn.max_pool(conv4, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1],
padding='SAME', name = 'maxPool_3')
with tf.name_scope('conv_5') as scope:
filter5 = tf.Variable(tf.zeros([1, 1, 256, 512], dtype=tf.float32), name='filter_5')
conv5 = tf.nn.relu(tf.nn.conv2d(pool3, filter5, [1, 1, 1, 1], padding='SAME', name = 'convo_5'))
现在,是时候添加全连接层了。 要添加全连接层,我们首先需要将来自上一层的输出展平。 您可以在 TensorFlow 中使用Flatten()
函数,也可以重塑上一层的输出:
with tf.name_scope('flatten') as scope:
flatt = tf.layers.Flatten()(conv5)
#shape = conv5.get_shape().as_list()
#flatt = tf.reshape(conv5, [-1, shape[1]*shape[2]*shape[3]])
我们将添加三个全连接层,其单元分别为 1,024、512 和 256。这些层将使用前面定义的dropOut
和rely
激活函数。 全连接层也称为密集层,因为它们创建具有全局连接的密集结构:
with tf.name_scope('dense_1') as scope:
dense1 = tf.layers.dense(flatt, units = 1024, activation = 'relu',name='fc_1')
dropOut1 = tf.nn.dropout(dense1, self.keepProb)
with tf.name_scope('dense_2') as scope:
dense2 = tf.layers.dense(dropOut1, units = 512, activation = 'relu',name='fc_2')
dropOut2 = tf.nn.dropout(dense2, self.keepProb)
with tf.name_scope('dense_3') as scope:
dense3 = tf.layers.dense(dropOut2, units = 256, activation = 'relu',name='fc_3')
dropOut3 = tf.nn.dropout(dense3, self.keepProb)
输出层也将是一个全连接层,不同之处在于我们将在此层中不使用任何激活函数:
代码语言:javascript复制with tf.name_scope('out') as scope:
outLayer = tf.layers.dense(dropOut3, units = self.classNum, activation = None, name='out_layer')
当我们为深度前馈网络定义损失函数和优化器时,我们将在此处类似地对其进行定义:
代码语言:javascript复制 with tf.name_scope('loss') as scope:
self.loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = outLayer, labels = self.y))
with tf.name_scope('optimizer') as scope:
optimizer = tf.train.AdamOptimizer(learning_rate = self.learningRate)
self.train = optimizer.minimize(self.loss)
with tf.name_scope('accuracy') as scope:
correctPredictions = tf.equal(tf.argmax(outLayer, axis=1), tf.argmax(self.y, axis = 1))
# calculating average accuracy
self.avgAccuracy = tf.reduce_mean(tf.cast(correctPredictions, tf.float32))
现在,让我们创建model
类的对象以启动模型图:
modelGraph = model(batchSize = BATCH_SIZE, classNum = CLASS_NUM, dropOut = DROPOUT,
learningRate = LEARNING_RATE, epochs = EPOCHS, imageSize = IMAGE_SIZE, savePath = 'model')
接下来,我们将创建一个 TensorFlow 会话并在批量文件周围循环。 对于从 1 到 5 的每个批量文件,我们将创建一个数据类的对象,并调用我们创建的函数来加载和预处理数据。 此外,generate_batches
函数不断生成用于训练的批量。 您可以保存模型,例如,每隔 10 个时间段:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
for epoch in range(modelGraph.epochs):
for iBatch in range(1, 6):
dataObj = data(DATA_DIR, 'data_batch_' str(iBatch), BATCH_SIZE, SEED)
dataObj.load_data_batch()
dataObj.reshape_data()
#dataObj.visualise_data([100, 4000, 2, 8000])
dataObj.one_hot_encoder()
dataObj.normalize_images()
dataObj.shuffle_data()
#print(dataObj.generate_batches()[0])
for batchX, batchY in dataObj.generate_batches():
#print(batchX[0])
#print(batchY[0])
_, lossT, accT = sess.run([modelGraph.train, modelGraph.loss, modelGraph.avgAccuracy],
feed_dict = {modelGraph.x: batchX, modelGraph.y: batchY, modelGraph.keepProb: modelGraph.dropOut})
print('Epoch: ' str(epoch) ' Minibatch_Loss: ' "{:.6f}".format(lossT) ' Train_acc: ' "{:.5f}".format(accT) "n")
if epoch % 10 == 0:
saver.save(sess, modelGraph.savePath)
下一部分将讨论使用 CNN 进行对象检测的任务。 我们将学习一些成功的对象检测架构,并使用 TensorFlow 实现对象检测。
使用 CNN 的对象检测
我们在日常生活中遇到的大多数自然图像都不是由覆盖整个图像的单个对象组成的。 通常,它是位于不同位置的不同对象的混合体。 在这种情况下,简单的对象识别将行不通。 因此,检测图像中存在的各种物体及其位置变得具有挑战性。 这是深度学习大放异彩的地方!
因此,对象检测可以分为两部分:
- 对象定位:确定图像中对象的
x
,y
坐标 - 对象识别:确定位置是否有对象,如果有,则确定是什么对象
因此,对象检测网络具有两个单独的子网来执行这两个任务。 第一个网络在图像中生成不同的兴趣区域,而第二个网络对它们进行分类。
R-CNN
这是用于对象检测的深度学习方法的早期阶段之一。 它利用选择性搜索算法生成区域提议。 区域提议是图像中任何包含对象的可能性很大的长宽比的有界框。 选择性搜索是一种基于图的算法,该算法首先使用像素强度划分区域,然后根据颜色,纹理和大小对它们进行分层分组以生成区域。 该算法的问题在于,即使对于低分辨率图像,它也会产生太多区域提议。 因此,R-CNN 将区域提议限制为 2,000。
将这些建议调整为相同的形状,然后输入到 CNN 网络中,该网络将从区域中提取特征并输出特征向量。 每个类别的对象都有一个 SVM 分类器,该分类器被馈入该特征向量以预测包含该对象的区域的概率。 同样,相同的特征向量被馈送到线性回归器中以预测对象边界框中的偏移。 尽管区域提议包含对象,但可能不会涵盖整个对象。 因此,预测偏移量有助于校正边界框的坐标。 下图显示了 R-CNN 架构中涉及的步骤:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnKFQIGh-1681704767273)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/0ecedb2a-76fe-43c4-bd37-6aed5ca377ba.png)]
可以在这个页面上找到《用于准确的对象检测和语义分割的丰富的特征层次结构》本文的链接。
接下来,我们将研究通过将选择性搜索替换为单独的区域提议网络来改进 R-CNN 的架构。
更快的 R-CNN
由于为每个图像生成了 2,000 个区域提议,因此 R-CNN 速度相当慢。 选择性搜索算法也不总是产生好的候选区域提议。 更快的 R-CNN 引入了区域提议网络以生成区域提议,从而取代了选择性搜索算法。 它具有以下功能:
- 最初从输入图像中提取特征映射的 CNN
- 九个锚点(三个比例和三个比例),以覆盖特征映射中不同大小和比例的对象
- 区域提议网络(RPN),以生成感兴趣的区域并对其进行排名
- 兴趣区域(ROI)合并以将不同形状的提案重塑为固定大小
首先,将输入图像输入到 CNN 中以生成特征映射。 特征映射进入 RPN,RPN 使用3 x 3
卷积核将特征映射调整为固定大小。 对于特征映射上的每个点,将预测九个锚框以及它们的无对象(是否存在对象)和边界框坐标(中心 x,中心 y,宽度和高度)。 RPN 生成的许多建议彼此重叠。 通过非最大抑制来消除重叠的边界框,该非最大抑制将计算边界框的交并比(Iou),并消除具有大于设置阈值分数的框。 RPN 为我们提供了建议的区域,但大小不同。 为了通过 R-CNN 对它们进行分类,我们需要获得相同大小的建议。 ROI 池通过将建议区域划分为相等数量的部分,然后应用最大池化来执行工作。 这样,无论初始大小如何,输出将始终为固定大小。 然后将这些 ROI 合并的输出馈送到 R-CNN 模块中进行分类。 在下图中,您可以观察到架构的完整管道:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0MU3h50v-1681704767273)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/90d06ae2-4f5f-4f1f-90b1-4c35fdb5f78c.png)]
图片来自纸本,更快的 R-CNN:使用区域提议网络实现实时目标检测
可以在这个页面上找到 Faster-RCNN 原始论文的链接。
接下来,我们将研究另一类对象检测器,即基于回归的对象检测器,它大大简化了对象检测的任务。
你只看一次(YOLO)
到目前为止,我们已经讨论过的对象检测架构依赖于区域提议(通过选择性搜索或单独的区域提议网络)。 这些类型的架构的问题在于,由于内部存在多个网络的集成,它们的实现非常复杂。 这些架构涉及大量参数,这使它们在计算上过于昂贵。 而且,网络首先提出了许多感兴趣的区域,这使得不可能实时执行检测。
为了应对这些挑战,Joseph Redmon,Santosh Divvala,Ross Girshick 和 Ali Farhadi 在 2015-16 年开发了一种新的基于预测(基于回归)的架构,该架构能够进行实时检测。 该架构称为 YOLO ,它是您只看一次的缩写。 YOLO 是一种端到端可训练的架构,仅使用单个 CNN 来检测对象。
YOLO 将图像划分为S x S
网格。 为每个网格预测两个边界框,以及对象属于特定类别的概率。 边界框大小不限于在网格内部。 每个边界框具有五个预测值-(x
,y
,w
,h
)。x
和y
表示边界框相对于网格的中心,而w
和h
表示网格的宽度和高度。 相对于图像尺寸的边界框。 因此,网络进行S x S x (B x 5 C)
预测,其中B
是每个单元格(例如两个)和C
是C
类的类概率。 您现在会注意到,该网络依赖于预测值,因此是基于回归的对象检测网络。 在下图中,您可以观察如何将图像划分为网格以预测边界框和类分数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQDdSlby-1681704767274)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/477e049a-49c7-40b3-8a65-4615bd81cd4f.png)]
图片取自本文,您只看一次:统一的实时对象检测
论文的链接《只看一次:统一的实时对象检测》,可在这里找到。
YOLO 使用 24 个卷积层。 这些层遵循简单的结构,一个接一个地重复使用1 x 1
和1 x 1
卷积。 最后,存在两个全连接层以输出预测的张量。 下图中可以看到该架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ttcWokm-1681704767274)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/e88ddb72-9c5a-4de7-926d-0f5798a6e5c3.png)]
图片来自本文,《只看一次:统一的实时对象检测》
YOLO 中使用的损失函数可分为四个部分:
- 边界框的位置
x
和y
的预测的平方和损失 - 在边界框的宽度和高度中进行预测的平方根损失
- 边界框置信度得分的损失
- 分类损失
以下公式包含 YOLO 的组合损失函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fbNtiKHZ-1681704767274)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/6154dd4a-bbd5-474c-8c2c-4931ad15fda3.png)]
图片出自原始论文,“您只看一次”:统一的实时对象检测
损失函数中的第一项采用所有B
边界框预测变量的边界框位置差的平方和。 第二项使用宽度和高度做相同的事情。 您会注意到额外的平方根。 这组作者说,大边界框中的小偏差比小边界框中的小偏差要小。 对项进行平方根运算有助于我们降低对较大值的敏感度。 我们还预测了置信度分数C[i]
,以及边界框(预测边界框时模型的置信度)。 损失函数中的第三项与置信度得分有关。 损失函数中的最后一项是关于将对象分类为不同类别的。
尽管 YOLO 极大地简化了对象检测架构并能够实时进行预测,但是也存在某些缺点。 该模型不会提取不同比例的特征,因此对于不同大小和比例的对象不具有鲁棒性。 该模型还难以检测组合在一起的较小尺寸的对象。 接下来,我们将研究另一种基于回归的对象检测架构,即单发多框检测器(SSD),该架构可弥补 YOLO 的缺点。
单发多框探测器
与 YOLO 一样,SSD 也是基于回归的对象检测器,但是 SSD 的创建者声称 SSD 比 YOLO 更快,更准确。 我们可以将 SSD 分为四个主要部分:
- 基本网络 - VGG16
- 多个比例的特征映射
- 用于边界框预测的卷积
- 用于预测的默认边界框
任何卷积网络的首要任务是减小输入的尺寸并增加特征映射的深度,以便提取特征。 然后,可以将特征映射中提取的特征用于不同任务,无论是分类还是检测。 SSD 也一样! SSD 使用著名的 VGG16 架构作为模型的初始层(基础网络)进行特征提取(请记住,这与 YOLO 不同,因为图像本身首先被划分为网格,然后将卷积应用于预测)。 VGG16 架构末尾的全连接层已被删除,因为使用 VGG16 的目的只是为了提供丰富的特征来学习而非分类。 在改良的 VGG16 网络的末端,SSD 引入了六层以上的卷积。 这些额外的六层的大小逐渐减小。 添加额外层的目的是使网络能够从不同大小和不同比例的对象中提取特征。 这就是为什么这些层中的特征映射的大小不断减小(多比例缩放的特征映射)的原因。 下图显示了 SSD 的总体架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUFfPshG-1681704767274)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/f30e4535-41dc-4b52-94e1-562e8fe946b9.png)]
图片来自纸张,SSD:单发多框检测器
《SSD:单发多框检测器》原始文件的链接可以在以下网址找到。
第一个特征映射集是从 VGG 16 架构的第 23 层提取的,大小为38 x 38 x 512
(此处 512 是过滤器的深度或数量)。 第二组特征映射的大小为19 x 19 x 1,024
,适用于捕获稍大的对象。 进一步的特征映射集将尺寸减小到10 x 10 x 512
、5 x 5 x 256
、3 x 3 x 256
,最后减小到1 x 1 x 256
。
为了进行预测,SSD 在提取的特征映射上使用3 x 3 x d
(d
表示过滤器的深度)卷积核。 对于特征映射上的每个点,3 x 3
核输出边界框偏移量和类分数。 SSD 为特征映射中的每个点分配了默认框。 3 x 3
卷积的工作是从覆盖对象的默认边界框预测四个偏移值。 除偏移量外,它还可以预测类别的c
类别分数。 如果我们在每个位置都有m x n
尺寸特征映射,并且在每个位置都有k
默认边界框,则从该层做出的预测总数将为(c 4) x k x m x n
。 每个位置的默认框数通常为 4 到 6。 这些默认边界框的比例和大小由网络中最低和最高特征映射的比例决定。 假设我们有m
个特征映射; 然后,默认边界框的比例(s[k]
)由以下公式给出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ViMwWEMg-1681704767275)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/93c36209-1288-4964-b35e-b1a8b03d31fa.png)]
在此, s_min
是最低特征映射的比例, s_max
是最高特征映射的比例。 然后,默认框的高度和宽度由以下关系定义:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kmBcW432-1681704767275)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d3f41649-c78b-4cd0-8e69-d67a555b0e87.png)]
以下屏幕快照说明了使用8 x 8
函数图和8 x 8
函数图进行 SSD 预测。 边界框偏移,Δ(cx, cy, w, h)
和p
类的类分数(c[1], c[2], ..., c[p]
)的预测:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CWGwYLls-1681704767275)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/5b9fce5d-d17d-4e37-9c44-665f6047ddf6.png)]
图片来自纸张,SSD:单发多框检测器
SSD 中使用的损失函数是定位损失和分类损失的组合。 本地化损失是按以下方式定义的平滑 L1 损失:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NiUA0SQ6-1681704767275)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/ddd6a2ca-05a3-45d2-8f1b-a0cb254f967d.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wvvouAkg-1681704767275)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/873d282c-c59c-4417-936a-a5c6e707c5e9.png)]
在此,为预测边界框(l
)和地面真实边界框(g
)之间的所有N
默认边界框计算平滑损失。 。 分类损失是针对类别的 softmax 计算类别分数之上的简单交叉熵损失。 分类损失由以下公式给出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TdW5As0c-1681704767276)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9ce711e0-639d-427e-8c9a-b7dd247ec465.png)]
在这里,第一项是边界框是否包含对象,第二项是没有对象时。 c_hat
代表该类别的 softmax 得分。
到目前为止,我们已经了解了 SSD 的工作原理。 现在,让我们使用 SSD 使用 TensorFlow 来检测对象!
TensorFlow 对象检测动物园
目标检测模型很难训练。 这是由于其复杂的架构以及进行大量预测的要求。 要训练诸如 Faster RCNN,YOLO 或 SSD 的对象检测模型,需要大量的并行处理能力,这并不是每个人都可以使用的。 即使您可以进行这种计算,也要花费数小时和数小时的时间,并要进行仔细的监视以训练端到端的对象检测模型。 尽管非常准确,但这可能会限制这些模型的易于使用。
为了克服这个普遍面临的问题,研究人员提出了预训练网络的想法。 使用可在公共大型数据集(例如 COCO 数据集,PASCAL VOC 数据集,Kitti 数据集等)上获得的最新资源来训练模型。 这些数据集的链接可以在这个页面,和这个页面中找到。
然后将模型的权重和图公开。 对对象检测深度学习模型感兴趣的任何人都可以下载这些权重和图以将其实现以用于对象检测。
TensorFlow 凭借其 TensorFlow 对象检测 API 和 TensorFlow 模型库向前迈出了一步,以开源各种预先训练的模型权重和 TensorFlow 冻结图来帮助深度学习开发人员。 您可以查看以下 TensorFlow 模型动物园的链接,并比较不同对象检测模型的运行时间和平均精度均值(MAP)。
接下来,我们将研究如何使用 TensorFlow 模型库进行对象检测。 让我们基于前面讨论的 SSD 建立模型。 我们的第一步是为我们要实现的模型下载预训练的权重。 在这里,我们将考虑模型ssd_mobilenet_v2_coco
。 MobileNet 背后的想法将在下一章中进行讨论。 现在,将其视为在 COCO 数据集上训练的 SSD 对象检测网络。 您可以通过单击模型名称以压缩形式下载包含所有相关文件的目录,如以下屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qVGGUgI-1681704767276)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/2f5bd5e7-6903-4b80-ac6e-02d6aab1a418.png)]
下载.zip
文件后,可以将其解压缩到deep_learning
文件夹中。 接下来,我们将看一个脚本,该脚本将从冻结的图中加载模型和权重,并检测输入图像中的对象。 首先,导入所需的依赖项:
请记住要激活用于深度学习的 Python 库和 TensorFlow 的安装环境。 我们在“第 1 章”,“深度学习”中创建了一个名为test_env
的环境。 您可以使用它! 如果您缺少任何依赖项,则可以在终端中(在激活的环境下)简单地执行conda install <dependency_name>
命令。
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import cv2
import os
import argparse
我们将使用argparse
模块传递需要执行检测的图像的路径。 使用argparse
,可以提供运行脚本时要检测到的图像的存储目录的路径:
parser.add_argument('--im_path', type=str, help='path to input image')
#parser.add_argument('--save_path', type=str, help='save path to output image')
args = parser.parse_args()
IM_PATH = args.im_path
接下来,我们将使用opencv
定义一个简单的函数来读取图像:
def read_image(imPath):
img = cv2.imread(imPath)
return img
提取的文件夹必须包含protobuf
格式(带有.pb
扩展名)的模型的冻结图。 我们需要读取此文件以加载冻结的图:
FROZEN_GRAPH_FILE = 'frozen_inference_graph.pb' #path to frozen graph
我们在 Python 中使用 TensorFlow,但是 TensorFlow 的实际库是用 C 编写的。 TensorFlow 使用名为protobuf
的模块将图转换为不同的语言。 因此,在读取由protobuf
(通常带有.pb
扩展名)存储的图时,我们需要首先使用tf.GraphDef
定义一个序列图,然后将其放入要创建的空图内。 以下代码执行相同的操作:
# making an empty graph
graph = tf.Graph()
with graph.as_default():
# making a serial graph
serialGraph = tf.GraphDef()
# reading from saved frozen graph
with tf.gfile.GFile(FROZEN_GRAPH_FILE, 'rb') as f:
serialRead = f.read()
serialGraph.ParseFromString(serialRead)
tf.import_graph_def(serialGraph, name = '')
接下来,我们使用加载的图初始化会话:
代码语言:javascript复制sess = tf.Session(graph = graph)
现在,我们将读取指定目录路径中的图像。 在这里,我们仅考虑.jpeg
图片,但您可以根据需要将其更改为其他格式:
for dirs in os.listdir(IM_PATH):
if not dirs.startswith('.'):
for im in os.listdir(os.path.join(IM_PATH, dirs)):
if im.endswith('.jpeg'):
image = read_image(os.path.join(IM_PATH, dirs, im))
if image is None:
print('image read as None')
print('image name: ', im)
TensorFlow 图由张量变量和占位符组成,它们用于在会话期间流动和馈送数据。 为了获取输出并将输入输入模型,我们需要取出负责输入和输出的张量。 我们可以通过图中的名称来获取张量。 我们使用以下代码为图像的输出检测到的边界框,类和输入占位符获取张量:
代码语言:javascript复制imageTensor = graph.get_tensor_by_name('image_tensor:0')
bboxs = graph.get_tensor_by_name('detection_boxes:0')
classes = graph.get_tensor_by_name('detection_classes:0')
现在,我们准备对图像执行对象检测。 在这里,我们需要使用np.expand_dims()
在图像中添加一个额外的尺寸,因为 TensorFlow 会保留批量尺寸的第一个尺寸:
(outBoxes, classes) = sess.run([bboxs, classes],feed_dict = {imageTensor:np.expand_dims(image, axis=0)})
我们可以使用简单的np.squeeze()
操作将结果提取为可视的,以通过使用以下代码来消除多余的尺寸:
cnt = 0
imageHeight, imageWidth = image.shape[:2]
boxes = np.squeeze(outBoxes)
classes = np.squeeze(classes)
boxes = np.stack((boxes[:,1] * imageWidth, boxes[:,0] * imageHeight,
boxes[:,3] * imageWidth, boxes[:,2] * imageHeight),axis=1).astype(np.int)
一旦有了预测的边界框,我们将使用opencv
在其周围绘制一个矩形框。 您也可以选择打印类值。 将打印数字类值; 您可以参考 COCO 数据集并将数字标签转换为实际标签。 我们会将其留给您作为练习:
for i, bb in enumerate(boxes):
print(classes[i])
cv2.rectangle(image, (bb[0], bb[1]), (bb[2], bb[3]), (100,100,255), thickness = 1)
此后,我们只需要绘制最终图像以查看图像上的边界框:
代码语言:javascript复制plt.figure(figsize = (10, 10))
plt.imshow(image)
plt.show()
就是这样了! 如果您陷入前面的片段的缩进或流程中,则以下是完整的代码供您参考:
代码语言:javascript复制import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import cv2
import os
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--im_path', type=str, help='path to input image')
args = parser.parse_args()
IM_PATH = args.im_path
def read_image(imPath):
img = cv2.imread(imPath)
return img
FROZEN_GRAPH_FILE = 'frozen_inference_graph.pb' #path to frozen graph
# making an empty graph
graph = tf.Graph()
with graph.as_default():
serialGraph = tf.GraphDef()
with tf.gfile.GFile(FROZEN_GRAPH_FILE, 'rb') as f:
serialRead = f.read()
serialGraph.ParseFromString(serialRead)
tf.import_graph_def(serialGraph, name = '')
sess = tf.Session(graph = graph)
for dirs in os.listdir(IM_PATH):
if not dirs.startswith('.'):
for im in os.listdir(os.path.join(IM_PATH, dirs)):
if im.endswith('.jpeg'):
image = read_image(os.path.join(IM_PATH, dirs, im))
if image is None:
print('image read as None')
print('image name: ', im)
# here we will bring in the tensors from the frozen graph we loaded,
# which will take the input through feed_dict and output the bounding boxes
imageTensor = graph.get_tensor_by_name('image_tensor:0')
bboxs = graph.get_tensor_by_name('detection_boxes:0')
classes = graph.get_tensor_by_name('detection_classes:0')
(outBoxes, classes) = sess.run([bboxs, classes],feed_dict={imageTensor: np.expand_dims(image, axis=0)})
# visualize
cnt = 0
imageHeight, imageWidth = image.shape[:2]
boxes = np.squeeze(outBoxes)
classes = np.squeeze(classes)
boxes = np.stack((boxes[:,1] * imageWidth, boxes[:,0] * imageHeight,
boxes[:,3] * imageWidth, boxes[:,2] * imageHeight),axis=1).astype(np.int)
for i, bb in enumerate(boxes):
print(classes[i])
cv2.rectangle(image, (bb[0], bb[1]), (bb[2], bb[3]), (255,255,0), thickness = 1)
plt.figure(figsize = (10, 10))
plt.imshow(image)
plt.show()
让我们拿起一张图片向您展示它的外观。 我们拍摄了以下两个人类站立的图像(图像来自 Wikipedia):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98mPLSq4-1681704767276)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/3c18033c-5bf7-43b6-9d01-44969dedba53.png)]
这是检测到的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOJMxMDi-1681704767276)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/6633cb52-a38d-4d5d-9919-5ee13e9ec169.png)]
总结
在本章中,我们首先讨论了深度前馈网络的缺点以及 CNN 如何克服这些缺点而发展。 接下来,我们深入研究了 CNN 的架构,了解了 CNN 的不同层-输入层,卷积层,最大池化层和全连接层。 我们研究了一些著名的图像分类 CNN 的架构,然后在 CIFAR-10 数据集上构建了我们的第一个 CNN 图像分类器。 然后,我们继续使用 CNN 进行对象检测。 我们讨论了各种对象检测器,例如 RCNN,Faster-RCNN,YOLO 和 SSD。 最后,我们使用 TensorFlow 检测模型 Zoo 通过 SSD 实现了我们的第一个对象检测器。
在下一章中,我们将介绍 CNN 架构,这些架构需要较少的计算能力,并且轻便,可以在移动设备上运行。 它们被称为 MobileNets!
五、移动神经网络和 CNN
深度学习网络所需的计算成本一直是扩展的关注点。 进行推理需要数百万个乘法运算。 这限制了已开发的卷积神经网络(CNN)模型的实际使用。 移动神经网络为这一问题提供了突破。 它们是超小型且计算量较小的深度学习网络,并具有与原始同类产品相当的表现。 移动神经网络只是经过修改以具有更少参数的 CNN,这意味着它们消耗的内存更少。 这样,它们就可以在内存和处理能力有限的移动设备上工作。 因此,移动神经网络在使 CNN 用于实时应用中起着至关重要的作用。 在本章中,我们将介绍 Google 引入的两种基准移动 CNN 架构-MobileNet 和 MobileNetV2。 完成本章后,您将了解以下主题:
- MobileNet 如何发展
- MobileNet 的架构
- 用 Keras 实现 MobileNet
- MobileNetV2
- MobileNetV2 的动机
- MobileNetV2 的架构
- 比较两个 MobileNet
- SSD
MobileNets 的演进之路
CNN 为计算机视觉带来了光明的未来。 CNN 凭借连续几年在 ILSVRC 竞赛中的出色表现,为复杂的计算机视觉任务(例如检测和识别)奠定了基准。 但是这些 CNN 模型所需的计算能力一直很高。 这可能导致 CNN 的商业使用受到重大挫折。 现实世界中几乎所有与对象检测有关的任务都是通过便携式设备执行的,例如移动电话,监控摄像头或任何其他嵌入式设备。 这些设备的计算能力和内存有限。 为了使任何深度学习网络都在便携式设备上运行,网络权重和网络中发生的计算数量(即网络中的参数数量)必须非常小。 CNN 具有数百万个参数和权重,因此似乎不可能在任何移动设备上打包和运行 CNN!
2017 年初,Google 的一组研究人员取得了突破,并推出了一种称为 MobileNets 的新型 CNN,用于移动和嵌入式视觉。 MobileNet 具有深度可分离卷积的概念,在保持相同模型深度的同时,显着减少了卷积网络的参数数量。 MobileNets 取得了巨大的成功! 许多公司开始使用 MobileNets 在移动设备上进行实时检测。
Google 在 2018 年推出了第二版 MobileNets,称为 MobileNetV2。 较新的 MobileNetV2 具有反向残差块。 各种成功的对象检测架构(例如 SSD)也与 MobileNetV2 结合在一起,以创建用于对象检测的高效移动架构。 因此,让我们在下一部分中了解 MobileNets 的架构。
MobileNets 的架构
MobileNets 架构的核心是深度可分离卷积的概念。 CNN 的标准卷积操作由深度卷积和点卷积代替。 因此,我们首先来看下一部分中的深度可分离卷积。
深度可分离卷积
顾名思义,深度可分离卷积必须与特征映射的深度有关,而不是其宽度和高度。 请记住,当我们在 CNN 中的输入图像上使用过滤器时,该过滤器覆盖了图像的所有通道(例如彩色图像的三个 RGB 通道)。 无论输入中存在多少个通道,卷积核总是覆盖所有通道并在单个通道特征映射中生成输出。
在任何层中,如果我们想要n
个数量的特征映射,则在上一层上运行n
个数量的核,因为每个核输出一个通道。 下图显示了标准卷积的输出响应:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMrZJr5A-1681704767276)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/26400924-f3b0-4f17-9cb1-4e868d209c2d.png)]
但是与标准卷积不同,深度卷积并未将输入中的所有通道都考虑在内以输出单个通道。 而是,它分别在每个通道上执行卷积。 因此,对n
通道图像执行深度卷积将在输出中产生n
通道。 深度方向可分卷积有两个部分-深度方向卷积(我们刚刚讨论过)和点方向卷积。 深度卷积之后是点状卷积,这只是一个具有1 x 1
核的常规卷积运算。 需要逐点卷积来组合深度卷积的结果。
下图显示了深度可分离卷积的输出响应:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n4nJQW4D-1681704767277)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/b7027c07-c5d8-47e5-8f0f-1eff9e9720ee.png)]
那么,使用n
卷积过滤器生成深度n
的特征映射有什么问题? 为什么我们需要用深度方向可分离卷积代替标准卷积? 在下一节中,我们将探讨原因。
深度可分离卷积的需求
在 MobileNets 中引入深度可分离卷积的目的是减少所需的计算费用。 因此,让我们将深度卷积可分离卷积与标准卷积的计算成本进行比较。 由于乘法运算而导致主要的计算成本(诸如加法的运算在计算上很简单)。 乘法次数越多,所需的计算就越高。 让我们考虑一个M x M x N
图像的情况。 这是 RGB 图像的典型尺寸。 假设我们正在使用K x K x N
大小的标准卷积核,并且我们希望尺寸特征映射为G x G x D
。 为此,我们必须使用D
个过滤器:
- 因此,在一个位置上一个卷积过滤器所需的乘法运算次数为
K . K . N = K^2 N
。 - 该过滤器会滑动
G x G
次,以生成一个过滤器的完整输出。 这使得乘法的次数G^2 K^2 N
。 - 我们有
D
个这样的核。 因此,这使得我们需要卷积G^2 K^2 ND
的总成本。
现在,让我们计算使用深度方向可分离卷积生成相同结果所需的乘法运算次数。 我们知道深度方向可分卷积有两个部分-深度方向卷积和点方向卷积。 标准卷积和深度卷积之间的主要区别在于,深度卷积在卷积核中使用 1 的深度。 让我们考虑一下前面提到的相同场景。 我们有一个M
xM
xN
图片:
- 此处的核大小为
K x K x 1
。我们需要N
个核,以适应完整的图像,这将为我们提供一个G x G x N
的输出尺寸。 因此,此处所需的乘法数为G^2 K^2 N
。 - 现在,该进行逐点卷积了。 这涉及组合深度方向卷积的输出。 点式卷积的核是
1 x 1 x N
。 如果该核在深度卷积的整个输出中滑动,则一个核所需的乘法运算次数将为G^2 N
。 - 如果我们要在最终输出特征映射中使用深度
D
,则使用D
个点状核的最终输出为G
xG
xD
。 因此,乘法数变为G^2 ND
。 - 深度方向可分离卷积所需的乘法总数是深度方向卷积和点方向卷积所需的乘法总和,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZnfnqVOq-1681704767277)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/4686e008-9c3f-4021-bd27-85cd318f15d0.png)]
我们可以通过以下方式比较标准卷积和深度可分离卷积所需的乘法次数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdN2haad-1681704767277)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9b23f1db-5fa5-43a3-9c3b-d96b685696c0.png)]
通常,如果我们将D = 256
,K = 3
,则比率为 0.115。 这意味着深度方向可分离卷积的参数是标准卷积的九倍。
希望您现在对 MobileNet 如何通过深度方向可分离卷积减少参数数量有所了解。 现在,让我们在下一个小节中查看 MobileNet 的结构。
MobileNet 的结构
MobileNet 的结构由 30 层组成。 它以3 x 3
的标准卷积作为第一层开始。 此后,继续进行深度卷积和点卷积。 深度可拆分卷积块是深度可拆分卷积和点式卷积的连续组合,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BHlsQ7GV-1681704767277)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9ca9c54e-df3e-4dbf-9f52-4f2d7a72786c.png)]
图片来自《MobileNets:用于移动视觉应用的高效卷积神经网络》。 BN 代表批量归一化。
在该结构中,前面的块重复 13 次。 为了减少特征映射的宽度和高度,MobileNet 在深度卷积中使用了第二步。 是的,它不使用 maxpooling! 为了增加特征映射的深度,逐点卷积将通道数量加倍。 通道的加倍发生在相应的逐点层中,其中在深度卷积中使用跨步 2。
可以在这个页面上找到 MobileNets 研究论文的链接。
MobileNet 经过 ImageNet 数据的训练,图像的输入尺寸为224 x 224 x 3
。根据 ImageNet 图像的输入尺寸,从卷积层出来的最终输出尺寸为7 x 7 x 1,024
。 卷积结束后,将应用全局平均池(GAP)层,以使尺寸为1 x 1 x 1,024
。 假设尺寸为H x W x D
的特征映射,GAP 层会计算HW
值的平均值,并使用单个平均值替换H x W
值,因此输出尺寸始终为1 x 1 x D
。
由于 MobileNet 主要用于分类,因此结束层是全连接层。 MobileNets 中使用的激活函数是 ReLU6。 我们已经了解了 ReLU,但是 ReLU6 是什么? ReLU6 与 ReLU 函数相同,但上限限制为六个。 作者声称 ReLU6 可帮助模型更早地学习稀疏特征。 以下等式定义了 ReLU6 激活函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Pq7Nc5S-1681704767277)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/9b45c6c3-fae7-4c2a-9d9c-b395303ff1e3.png)]
让我们在下表中查看 MobileNet 的完整架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QsHETPns-1681704767278)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/6a59c5a9-4edb-4e15-a71f-6b2ef601ea7e.png)]
图片来自《MobileNets:针对移动视觉应用的高效卷积神经网络》
既然我们已经了解了 MobileNet 的架构以及如何通过深度方向上可分离的卷积减少参数的数量,那么让我们看一下 MobileNet 的实现。
MobileNet 与 Keras
MobileNet 经过 ImageNet 数据训练。 通过使用 Keras 应用类,我们可以使用模型的预训练权重来实现 MobileNet。 在 Keras 应用中,您可以找到许多预先训练的模型供使用。 您可以通过这里浏览 Keras 应用的文档。
所以,让我们开始吧! 首先,显然,我们将导入所需的依赖项:
代码语言:javascript复制import keras
from keras.preprocessing import image
from keras.applications import imagenet_utils
from keras.models import Model
from keras.applications.mobilenet import preprocess_input
import numpy as np
import argparse
import matplotlib.pyplot as plt
Keras preprocessing
提供了一个类,例如ImageDataGenerator
类,该类有助于从数据集中绘制成批图像。 我们的下一个工作是获取模型权重和图。 在我们的脚本中添加以下步骤后,下载将仅在您的系统上进行一次:
model = keras.applications.mobilenet.MobileNet(weights = 'imagenet')
下载可能需要一些时间,具体取决于您的互联网连接。 Keras 将继续更新状态,完成后将如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LRjlTg6h-1681704767278)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/aae353e4-6a4d-4882-8ad1-dc65e337676a.png)]
我们将使用argparse
模块来帮助将图像路径传递到我们希望 MobileNet 分类的图像的脚本:
parser = argparse.ArgumentParser()
parser.add_argument('--im_path', type = str, help = 'path to the image')
args = parser.parse_args()
# adding the path to image
IM_PATH = args.im_path
我们将使用 Keras 提供的load_img
函数来加载此图像,并使用img_to_array
将其转换为数组:
img = image.load_img(IM_PATH, target_size = (224, 224))
img = image.img_to_array(img)
ImageNet 中的图像的宽度和高度为224
。 因此,默认情况下将目标大小设置为(224, 224)
。 正如我们前面所看到的,第一维始终保持批量大小。 我们将扩展图像的尺寸,以将批量大小作为第一个尺寸(因为我们使用的是单个图像,因此可以假定其为批量大小 1):
img = np.expand_dims(img, axis = 0)
最后,我们将通过mobilenet
的preprocess_input()
函数传递img
,该函数执行基本的预处理操作,例如重新塑形和标准化图像的像素值:
img = preprocess_input(img)
现在,是时候让 MobileNet 对我们提供的图像做出预测了:
代码语言:javascript复制prediction = model.predict(img)
当模型根据 ImageNet 数据集预测类别时,我们将使用decode_predictions
函数以人类可读的形式带回前五项预测:
output = imagenet_utils.decode_predictions(prediction)
print(output)
让我们使用以下鹈鹕鸟的图像并检查 MobileNet 是否能够对其进行分类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EmEwqaoU-1681704767278)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d2a57e22-fd53-4083-9327-c861985f5444.png)]
图片来自维基百科
您需要按照以下方式从正在运行的环境下的终端运行脚本以及图像的路径:
代码语言:javascript复制$python mobilenet_keras.py --im_path=pelican.jpg
以下是我们脚本的输出。 您可以看到 MobileNet 将该图像预测为鹈鹕,概率为 0.99! 在预测的前五类中,还有其他一些看起来像鹈鹕的鸟,但是由于 softmax 激活,它们的概率被抑制了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4UUSYMwv-1681704767278)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/941bcc42-9998-4ab2-835a-7fef5862d7e3.png)]
您可以使用与 ImageNet 数据类有关的不同图像来探索 MobileNet 的表现。 在 MobileNet 成功之后,Google 研究团队于 2018 年 4 月发布了 MobileNet 的更新版本。我们将在下一部分中了解 MobileNet 的第二版本。
MobileNetV2
第二个版本的 MobileNet,称为 MobileNetV2,甚至比 MobileNet 还要快。 第二个版本也具有较少的参数。 自发布以来,MobileNetV2 已广泛用于最新的对象检测和分段架构中,以使资源有限的设备上的对象检测或分段成为可能。 让我们看看创建 MobileNetV2 的动机。
MobileNetV2 的动机
Google 的研究人员希望 MobileNet 更轻巧。 如何使 MobileNet 具有更少的参数? 所有基于 CNN 的模型都增加了特征映射(深度通道)的数量,同时减小了宽度和高度。 减小网络大小的一种简单方法是减小特征映射的深度。 通道数越少,参数越少。 但这会削弱 CNN! 卷积过滤器将无法从浅层特征映射中提取特征。 那我们现在怎么办?
Google 研究人员找到了解决该问题的方法,并介绍了现有 MobileNet 架构的两个主要变化:扩展线性瓶颈层和倒置残差块。我们将在下一部分中介绍 MobileNetV2 的详细结构。
MobileNetV2 的结构
MobileNetV2 的核心架构仍然依赖于深度方向上可分离的卷积层。 还记得 MobileNet 的基石吗? 它具有3 x 3
的深度卷积层,然后是3 x 3
的逐点卷积和批量归一化,中间是 ReLU6。 MobileNetV2 遵循相同的块,不同之处在于顶部有一个额外的扩展层和一个线性瓶颈层来代替3 x 3
点向卷积。 首先让我们看一下线性瓶颈层的作用。
线性瓶颈层
在 MobileNet 中,1 x 1
点向卷积负责增加通过网络的特征映射的深度。 MobileNetV2 中的线性瓶颈层执行相反的工作。 它实际上减少了特征映射的深度。 为了保留层中的非线性,ReLU 激活函数会降低负值。 这导致信道中的信息丢失。 为了解决这个问题,特征映射中使用了许多通道,因此很有可能一个通道中的信息丢失会保留在任何其他通道中。
但是,MobileNetV2 的作者证明,如果将输入通道投影到低维空间而不是高维空间,则 ReLU 激活能够保留来自输入通道的所有信息。 这是一个重大突破! 作者还提供原始文件中的补充材料来证明这一点。
可以在这个页面上找到 MobileNetV2 研究论文的链接。
因此,作者在卷积之后引入了所谓的线性瓶颈层,这降低了尺寸。 例如,如果中间层具有 384 个通道,则它将减少为 128 个通道。 但是减小尺寸并不是我们所需要的! 为了执行深度卷积,我们需要更高的维数。 因此,在深度卷积之前使用扩展层以增加通道的数量。 让我们看一下扩展层的功能。
扩展层
扩展层是1 x 1
卷积层,始终具有比输入维更大的输出通道维。 扩展量由称为扩展因子的超参数控制。 整个 MobileNetV2 的扩展因子都设置为 6。例如,如果输入具有 64 个通道,它将被扩展为64 * 6 = 384
个通道。 在其上进行深度卷积,然后瓶颈层将其带回到 128 个通道。
MobileNetV2 的架构首先扩展通道,执行卷积,然后减小它。 这使架构的端到端维数较低,从而减少了所需参数的数量。
下图显示了 MobileNetV2 的总体构建块:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJc9AlIo-1681704767279)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/27394f16-3c33-45fe-9c6f-14d757432e20.png)]
现在,架构还剩下一件事:剩余层。 尽管它类似于 ResNet 模型中的跳过连接,但我们将查看有关它的一些详细信息。
反向残差块
由于通过将许多层堆叠在一起,深度学习模型变得太深,训练网络变得非常困难。 这不仅是由于需要巨大的计算能力,而且还因为梯度逐渐消失在层中。 我们知道深度学习模型中的所有学习都取决于通过反向传播的梯度流。 在大型网络中,梯度随着每一步而变小,并在穿过所有层之前消失。 这种现象限制了我们使网络变得太深。 ResNet 架构中引入的剩余连接或跳过连接可帮助我们克服此问题。 来自上一层的连接将添加到一层,以便梯度获得易于流动的路径。 这些跳过连接使 ResNet 模型比通常的更深。
受跳过连接的启发,MobileNetV2 的作者声称有用的信息仅位于瓶颈层,因此,为了使梯度易于通过多个瓶颈块流动,他们增加了从一个瓶颈层到另一个瓶颈层的剩余连接。 由于 ResNet 中原始残差连接和 MobileNetV2 中残差连接之间的设计差异,作者选择将此称为反向残差。
下图可以看出差异:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQwLMwYa-1681704767279)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/2778f97c-d627-4359-9458-b39cfdeaf5cc.png)]
图片来自研究论文《MobileNetV2:残差和线性瓶颈》
既然我们已经涵盖了 MobileNetV2 的所有元素,我们将研究 MobileNetV2 的整体结构。
整体架构
MobileNetV2 首先对图像执行标准卷积,以创建 32 个过滤器特征映射。 此后,有 19 个残存的瓶颈层块(我们在扩展层子部分中看到的块)。 所有卷积核的大小均为3 x 3
。整个网络中一直使用 6 的恒定扩展因子。 下表列出了 MobileNetV2 的总体架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Or2aQTsK-1681704767279)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/d335fb34-5b5e-43ba-8a2e-e83db7eb88a9.png)]
图片来自研究论文《MobileNetV2:残差和线性瓶颈》
在上表中,n
列表示重复特定层的次数。 s
列代表用于该层的跨步。 列c
和t
分别表示层中使用的通道数和扩展因子。
与 MobileNet 相似,我们也可以使用 Keras 来实现 MobileNetV2。
实现 MobileNetV2
我们将遵循与 MobileNet 相似的过程。 您可以在 Keras 应用中找到 MobileNetV2。 我们将使用与 MobileNet 相同的代码,除了这次将使用 MobileNetV2。 供参考,代码如下:
代码语言:javascript复制import keras
from keras.preprocessing import image
from keras.applications import imagenet_utils
from keras.applications.mobilenet import preprocess_input
from keras.models import Model
import numpy as np
import argparse
import matplotlib.pyplot as plt
model = keras.applications.mobilenet_v2.MobileNetV2(weights = 'imagenet')
parser = argparse.ArgumentParser()
parser.add_argument('--im_path', type = str, help = 'path to the image')
args = parser.parse_args()
# adding the path to image
IM_PATH = args.im_path
img = image.load_img(IM_PATH, target_size = (224, 224))
img = image.img_to_array(img)
img = np.expand_dims(img, axis = 0)
img = preprocess_input(img)
prediction = model.predict(img)
output = imagenet_utils.decode_predictions(prediction)
print(output)
该脚本将首先下载 MobileNetV2 的权重,这可能需要一些时间,具体取决于您的互联网连接。 它看起来像这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rTiSCIbR-1681704767279)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/c0f39c88-43b3-45ed-9438-fce2223cf5ee.png)]
让我们使用以下火烈鸟图像来检查输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dWLxYVuZ-1681704767279)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/03938748-1aca-4107-a9a3-bab2a30a48bd.png)]
这是输出的样子。 我们可以看到该网络大约有 86% 的人确定该图像是火烈鸟。 您可以观察到其他类别的概率由于 softmax 而受到抑制:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iO8nJYw-1681704767280)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/17770212-865d-48cc-adbc-4a34a751818a.png)]
一年内推出了两个版本的 MobileNet。 第二个版本包含重大更改,我们已经讨论过。 现在,让我们比较两个网络的一些标准参数。
比较两个 MobileNet
MobileNetV2 对 MobileNet 的架构进行了重大更改。 这些更改值得吗? 在表现方面,MobileNetV2 比 MobileNet 好多少? 我们可以根据一次推理所需的乘法运算数量来比较模型,这通常称为 MAC(乘法累加数)。 MAC 值越高,网络越重。 我们还可以根据模型中的参数数量来比较模型。 下表显示了 MobileNet 和 MobileNetV2 的 MAC 和参数数:
网络 | 参数数 | MAC |
---|---|---|
MobileNet V1 | 420 万 | 575M |
MobileNet V2 | 340 万 | 300M |
我们还可以根据不同通道数和分辨率所需的内存来比较模型。 下表提供了比较数据。 测量的内存为千字节(Kb):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tmnnl2oH-1681704767280)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/5042d3d3-2d3b-478d-8952-ec150a8b3c2a.png)]
TensorFlow 还提供了在像素 1 移动电话上运行的两个 MobileNet 之间的准确率与延迟比较。 延迟基本上表示运行模型需要多少时间。 下图显示了比较:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WR6tNauZ-1681704767280)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/e78595f8-4f7c-4cd9-b4a4-49d9aeced026.png)]
您可以在这个页面上找到有关比较的更多详细信息。
MobileNetV2 不仅仅是分类。 该架构的作者提出了将对象检测和分段架构相结合的想法。 在下一部分中,我们将介绍用于对象检测的 MobileNetV2 和 SSD 的非常成功的组合。
SSD MobileNetV2
MobileNetV2 的制造商还使移动设备的实时对象检测成为可能。 他们介绍了 SSD 对象检测器和 MobileNetV2(称为 SSDLite)的组合。 请记住,在“第 4 章”,“CNN 架构”中,我们将ssd_mobilenetv2
用于对象检测。 与 SSDLite 相同。 选择 SSD 的原因很简单。 SSD 的构建独立于基础网络,因此卷积被深度可分离卷积替代。 SSDLite 的第一层连接到 MobileNetV2 的第 15 层的扩展。 用深度可分离卷积替换标准卷积可以显着减少网络进行对象检测所需的参数数量。
下表显示了原始 SSD 网络和 SSDLite 所需的参数数量和乘法运算的比较:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CV0l7eqM-1681704767280)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-dl-arch-py/img/8ee9bbcf-e883-4886-ad12-da9bea86256f.png)]
图片来自研究论文《MobileNetV2:残差和线性瓶颈》
总结
在本章的开头,我们讨论了使神经网络在实时应用中运行所需的移动神经网络。 我们讨论了 Google 推出的两种基准 MobileNet 架构-MobileNet 和 MobileNetV2。 我们研究了深度可分离卷积之类的修改如何工作,并取代了标准卷积,从而使网络能够以更少的参数获得相同的结果。 通过 MobileNetV2,我们研究了通过扩展层和瓶颈层进一步缩小网络的可能性。 我们还研究了 Keras 中这两个网络的实现,并根据参数数量,MAC 和所需的内存比较了这两个网络。 最后,我们讨论了 MobileNets 与对象检测网络(例如 SSD)的成功组合,以在移动设备上实现对象检测。
在下一章中,我们将介绍另一种成功的深度学习架构,称为循环神经网络(RNN)。 这些网络旨在捕获序列中的时间信息,例如句子或任何其他文本。