Python-深度学习架构实用指南-全-
Python 深度学习架构实用指南(全)
零、前言
深度学习架构由代表高级抽象的多级非线性操作组成。 这使您可以从数据中学习有用的特征表示。 《使用 Python 的动手深度学习架构》为您简要介绍了用于深度和浅层架构的基本学习算法。 本书包含构建高效人工智能系统的实际实现和思想,将帮助您学习神经网络在构建深度架构中如何发挥重要作用。
您将通过易于理解的代码和图表,了解各种深度学习架构,例如 AlexNet,VGG Net,GoogleNet 等。 除此之外,这本书还将指导您构建和训练各种深度架构,例如玻尔兹曼机制,自编码器,卷积神经网络(CNN),循环神经网络(RNN),自然语言处理(NLP),生成对抗网络(GAN)和其他内容,以及实际的实现方式。 本书介绍了用于深层和浅层架构的基本学习算法。
到本书结尾,您将能够使用流行的框架和数据集以及每种架构所需的设计模式来构建深度模型。 您将准备探索当今世界中深层架构的可能性。
这本书是给谁的
如果您是数据科学家,机器学习开发人员/工程师,深度学习从业人员,或者对 AI 领域感到好奇,并且想升级各种深度学习架构的知识,那么本书将对您有吸引力。 您应该具有一些统计知识和机器学习算法知识,才能从本书中获得最大收益。
本书涵盖的内容
“第 1 章”,“深度学习入门”涵盖了机器和人工智能中的智能进化,以及最终的深度学习。 然后,我们将研究深度学习的一些应用,并建立用于通过深度学习模型进行编码的环境。 完成本章后,您将学习以下内容。
“第 2 章”,“深度前馈网络”涵盖了深度前馈网络及其架构的发展历史。 我们还将演示如何提供和预处理数据以训练深度学习网络。
“第 3 章”,“受限玻尔兹曼机和自编码器”解释了幕后的算法,称为受限玻尔兹曼机(RBM)及其演化路径。 然后,我们将更深入地研究其背后的逻辑,并在 TensorFlow 中实现 RBM。 我们还将应用它们来构建电影推荐器。 然后,我们将学习自编码器,并简要介绍它们的发展路径。 我们还将说明各种自编码器,按其架构或形式化形式进行分类。
“第 4 章”,“CNN 架构”涵盖了一类重要的图像深度学习网络,称为卷积神经网络(CNN)。 我们还将讨论 CNN 在深度前馈网络上的好处。 然后,我们将详细了解一些著名的图像分类 CNN,然后在 CIFAR-10 数据集上构建第一个 CNN 图像分类器。 然后,我们将继续使用 CNN 和 TensorFlow 检测模型 zoo 进行对象检测。
“第 5 章”,“移动神经网络和 CNN”讨论了用于在实时应用中进行 CNN 工作的移动神经网络的需求。 我们还将讨论 Google 推出的两种基准 MobileNet 架构-MobileNet 和 MobileNetV2。 稍后,我们将讨论 MobileNet 与对象检测网络(例如 SSD)的成功组合,以在移动设备上实现对象检测。
“第 6 章”,“循环神经网络”解释了最重要的深度学习模型之一,循环神经网络(RNN),其架构以及 RNN 的进化路径。 稍后,我们将讨论按循环层分类的各种架构,包括原始 RNN,LSTM,GRU 和双向 RNN,并应用原始架构来编写我们自己的《战争与和平》(毫无意义)。 我们还将介绍双向架构,该架构允许模型保留序列的过去和将来上下文中的信息。
“第 7 章”,“生成对抗网络”解释了最有趣的深度学习模型之一,生成对抗网络(GANs)及其演化路径。 我们还将以图像生成为例来说明各种 GAN 架构。 我们还将探索四种 GAN 架构,包括原始 GAN,深度卷积 GAN,条件 GAN 和信息最大化 GAN。
“第 8 章”,“深度学习的新趋势”讨论了一些我们已经发现今年有影响力并且在未来会更加突出的深度学习思想。 我们还将学习贝叶斯深度学习结合了贝叶斯学习和深度学习的优点。
充分利用这本书
读者需要具备 Python,TensorFlow 和 Keras 的先验知识。
下载示例代码文件
您可以从 www.packt.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packt.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或注册 www.packt.com 。
- 选择“支持”选项卡。
- 单击“代码下载和勘误”。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 版 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 中。 如果代码有更新,它将在现有的 GitHub 存储库中进行更新。
我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件安装为系统中的另一个磁盘。”
代码块设置如下:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
任何命令行输入或输出的编写方式如下:
conda activate test_env
conda install tensorflow
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“从管理面板中选择系统信息。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、深度学习入门
人工智能可能会起作用,如果能起作用,它将是技术上最大的发展。
-山姆·奥特曼
欢迎使用《Python 深度学习架构实践指南》!如果您完全不熟悉深度学习,则可以从这本书开始您的旅程。 对于有想法的读者,我们几乎涵盖了深度学习的各个方面。 因此,您肯定会从本书中学到更多有关深度学习的知识。*
这本书是按累积方式布置的。 也就是说,它从基础开始,一遍又一遍地构建,直至达到高级。 在本章中,我们将讨论人类如何开始在机器中创建智能,以及人工智能如何逐渐演变为机器学习以及最终的深度学习。 然后,我们看到了深度学习的一些不错的应用。 回到基本原理,我们将学习人工神经元的工作原理,并最终为通过深度学习模型编码的方式建立环境。 完成本章后,您将了解以下内容。
- 什么是人工智能以及机器学习,深度学习与人工智能的关系
- 机器学习任务的类型
- 有关一些有趣的深度学习应用的信息
- 什么是人工神经网络及其工作方式
- 使用 Python 设置 TensorFlow 和 Keras
让我们从关于人工智能以及人工智能,机器学习和深度学习之间的关系的简短讨论开始。
人工智能
自计算机时代开始以来,人类一直在尝试将大脑模仿到机器中。 研究人员一直在研究使机器不仅可以计算,而且可以像人类一样做出决定的方法。 我们的这一追求在 1960 年代左右催生了人工智能。 根据定义,人工智能意味着开发能够完成任务而无需人工对每个决策进行明确编程的系统。 1956 年,亚瑟·塞缪尔(Arthur Samuel)编写了第一个玩跳棋的程序。 从那以后,研究人员试图通过定义不涉及任何学习的手写规则集来模仿人类的智力。 玩象棋这样的游戏的人工智能程序不过是一组手动定义的动作和策略。 1959 年,亚瑟·塞缪尔(Arthur Samuel)创造了术语机器学习。 机器学习开始使用各种概率和贝叶斯统计概念来执行模式识别,特征提取,分类等。 在 1980 年代,受人脑神经结构的启发,引入了人工神经网络(ANN)。 2000 年代的 ANN 演变成如今所谓的深度学习! 以下是通过机器学习和深度学习发展人工智能的时间表:

机器学习
机器学习之前的人工智能就是编写机器用来处理提供的数据的规则。 机器学习实现了过渡。 现在,仅通过将数据和预期输出提供给机器学习算法,计算机就可以为任务返回一组优化的规则。 机器学习使用历史数据来训练系统并在未知但相似的数据上对其进行测试,从而开始机器学习如何进行决策而无需进行硬编码的旅程。 在 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 的示例图像,描述了图像到图像翻译的各种应用:

源自 pix2pix
机器翻译
这个世界上有 4,000 多种语言,数十亿人通过它们进行交流。 您可以想象需要进行语言翻译的规模。 大多数翻译都是由人工翻译完成的,因为机器进行的基于规则的经典翻译通常是毫无意义的。 深度学习提出了解决方案。 它可以像我们一样学习语言,并生成更自然的翻译。 这通常称为神经机器翻译(NMT)。
编解码结构
神经机器翻译模型是循环神经网络(RNN),以编码器/解码器的方式排列。 编码器网络通过 RNN 接收可变长度的输入序列,并将这些序列编码为固定大小的向量。 解码器从此编码向量开始,并开始逐字生成翻译,直到它预测句子的结尾。 整个架构通过输入语句和正确的输出翻译进行了端到端的训练。 这些系统的主要优点(除了具有处理可变输入大小的功能之外)是,它们学习句子的上下文并据此进行预测,而不是进行逐词翻译。 在以下屏幕截图中,可以最好地看到神经机器翻译在 Google 翻译上的作用:

源自 Google 翻译
聊天机器人
您可能会发现这是最酷的应用! 像人类一样与我们交谈的计算机一直是一种令人着迷的愿望。 它给我们一种计算机变得智能的感觉。 但是,大多数先前构建的聊天机器人系统都是基于知识库和规则的,这些规则和规则定义了从中选择哪个响应。 这使得聊天机器人成为一个非常封闭的领域,而且听起来很不自然。 但是对编码器-解码器架构进行了一些调整,我们看到了机器翻译实际上可以使聊天机器人自己生成响应。 编码学习输入句子的上下文,并且,如果整个架构都经过样本查询和响应训练,则每当系统看到新查询时,系统都可以基于学习结果生成响应。 IBM Watson,Bottr 和 rasa 等许多平台都在构建深度学习驱动的工具,以构建用于业务目的的聊天机器人。
建立基础
在本部分中,您将开始成为深度学习架构师的旅程。 深度学习是人工神经网络的支柱。 我们的第一步应该是了解它们如何工作。 在本节中,我们描述了人工神经元背后的生物学灵感和创建 ANN 的数学模型。 我们尝试将数学降至最低,并更多地关注概念。 但是,我们假设您熟悉基本的代数和微积分。
生物灵感
如前所述,深度学习受到人脑的启发。 这确实是个好主意。 要开发机器内部的大脑智能,您需要机器来模仿大脑! 现在,如果您略微意识到人脑如何如此快地学习和记忆事物,那么您必须知道,这是有可能的,因为数百万个神经元形成了一个相互连接的网络,相互之间发送信号,从而构成了记忆。 神经元具有两个主要成分:树突和轴突。 树突充当受体,并结合神经元正在接收的所有信号。 轴突通过突触与其他神经元末端的树突相连。 一旦传入的信号超过阈值,它们就会流过轴突并突触,将信号传递到连接的神经元。 神经元相互连接的结构决定了网络的功能。 以下是生物神经元的外观示意图:

生物神经元(来自 Wikimedia)
因此,神经网络的人工模型应该是一个由相互连接的节点组成的并行网络,该网络可以接收来自其他各个节点的输入,并在激活时传递输出。 这种激活现象必须通过某种数学运算来控制。 接下来让我们看一下运算和方程式!
人工神经网络
ANN 由两个组件构建:节点和权重。 节点扮演神经元的角色,权重是可学习的参数,权重将神经元彼此连接并控制其激活路径。
那么,我们如何制作一个人工神经元或节点呢? 认为x是神经元的标量输入,w是神经元的标量权重。 如果您不知道标量和向量是什么,则标量仅仅是一个实数元素,而向量是此类元素的列表。 人工神经元可以表示为以下方程式:


圆圈表示将标量x作为输入并在将其与权重w相乘后输出a的神经元。 在此,将b称为偏置。 在方程式中增加了偏置,以提供针对特定输入范围移动输出的功能。 一旦我们通过激活函数,偏置的作用将变得更加清晰。
现在,假设神经元不仅接受单个标量输入,还接受多个输入。 输入可以称为向量(例如P)。 然后,P可以写成一组标量输入p[1], [2], ..., p[n]和每个输入也将具有权重向量(例如W = w[1], w[2], ..., w[n]),这将用于激活神经元。 以下矩阵代表P和W向量:


那么,我们需要对方程式进行哪些更改以适合这些多个输入? 简单地总结一下! 这会将基本公式a = w.x + b更改为以下公式:

接受多个输入的人工神经元如下图所示:

但是一个神经元会独自做什么? 尽管它仍然可以输出可用于进行二元判断(零或一)的值,但是我们需要大量相似的神经元以并行方式排列并相互连接,就像在大脑中一样,以便超越二元决策。 那么,它会是什么样? 如下图:

现在需要对方程式进行哪些更改? 只是权重向量和输出向量的尺寸。 现在我们将得到nxm权重数,其中n是输入数,m是神经元数。 同样,我们将从每个神经元获得单独的输出。 因此,输出也变成一个向量:



到目前为止,我们已经学习了对人工神经网络建模的基本结构和数学方程。 接下来,我们将看到另一个重要的概念,称为激活函数。
激活函数
激活函数是任何深度学习模型的组成部分。 激活函数是将输入值压缩到一定范围内的数学函数。 假设您输入具有实数输入的神经网络,并使用随机数初始化权重矩阵,并希望使用输出进行分类; 也就是说,您需要输出值介于零到一之间,但是您的神经元可以输出-2.2453或17854.763之类的任何值。 因此,需要将输出缩放到特定范围。 这是激活函数的作用:

根据要求,有很多激活函数。 我们将讨论深度学习中经常使用的一些激活函数。
线性激活
该激活与输入成比例。 它仅用于按一定常数c缩放输出。 以下是线性激活的输出f(x)与输入x的关系图:


Sigmoid 激活
对于所有实数输入,此函数的输出范围从零到一。 这对于从神经元生成概率分数非常重要。 该函数也是连续且非线性的,有助于保持输出的非线性。 同样,曲线的梯度在原点附近陡峭,并且随着我们开始沿x轴移动而饱和。 这意味着,围绕原点的输入会发生微小变化,从而导致输出发生重大变化。 此特性有助于分类任务,因为它试图使输出保持接近零或一。 以下是针对输入x进行 Sigmoid 激活的方程式:

以下是 Sigmoid 激活函数的图解:

Tanh 激活
Tanh 或 tan 双曲函数类似于 Sigmoid 函数,但输出范围从-1到1而不是从0到1。 通常在输出的符号对我们也很重要的情况下使用。 以下是 tanh 激活函数的图:


ReLU 激活
整流线性或众所周知的 ReLU 函数是深度学习模型中使用最广泛的激活函数。 将负值抑制为零。 ReLU 之所以被广泛使用的原因是,它使产生负值的神经元失活。 在包含数千个神经元的大多数网络中都需要这种行为。 以下是 ReLU 激活函数的图:


ReLU 的一种修改形式是泄漏的 ReLU。 ReLU 以负值完全使神经元失活。 泄漏的 ReLU 并没有完全使神经元失活,而是通过c降低了这些神经元的作用。 以下等式定义了泄漏的 ReLU 激活函数:

以下是 ReLU 激活函数的输出值图:

Softmax 激活
这是大多数分类任务中要使用的激活函数。 大多数时候,网络必须预测属于该特定类别的输入的概率。 Softmax 激活输出此概率分数,表示模型对预测类别的信心程度:

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 的包管理器)来安装库。
要查看您的环境中安装的包,可以使用以下命令:
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 环境中运行以下命令,它将为您下载并安装所有内容:
# 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 并运行以下命令:
>>>import tensorflow as tf
>>>sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
如果您已成功安装 GPU 版本,这将返回有关 GPU 卡的详细信息以及 TensorFlow 编译使用的其他详细信息。
为了正确检查安装(与 CPU 或 GPU 版本无关),我们将执行以下简单的张量乘法运算:
>>>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 中添加两个常量,如下所示:
>>>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:
conda install keras
顺序 API
Keras 中的模型架构可以简单地通过一层又一层地堆叠来构建。 这在 Keras 中称为顺序方法,是最常见的一种方法:
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 样式代码编写,则可以选择以下布局:
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 的架构受到多个神经元的分层结构的启发。 该网络具有连续堆叠的各个层,其中前一层的神经元输出被前馈作为下一层的输入(这就是为什么该网络称为前馈网络)。 架构输入层,隐藏层和输出层中存在三种类型的层。 下图中可以看到这些层:

根据通用逼近定理,具有单个隐藏层的前馈神经网络能够用有限数量的神经元建模任何实函数。
但是,神经元的数量增长到如此之快,以至于几乎不可能建立这样的网络。 相反,增加层数可以增加神经元的数量,并有助于更好地学习特征。 网络越深(具有更多的隐藏层),学习就会越好。 接下来是显示典型 DFN 的示意图。 为了简单起见,图中没有显示偏差和标签,但是它们的作用类似于前馈网络:

希望您现在了解深度和前馈在 DFN 中的含义。 接下来,我们将看到如何训练这些网络以学习损失函数和反向传播算法。
训练
DFN 中存在的权重值负责进行预测。 任何深度网络都具有如此众多的权重,以至于不可能找到理想的权重值。 因此,我们尝试搜索一组权重值,这些值将给我们足够好的预测结果。 因此,训练网络意味着从一组初始权重开始学习最佳权重值。 假设我们有一个 DFN,最初,我们不知道哪种权重会表现良好。 因此,我们用随机实数初始化权重值。 现在,我们必须从初始化的权重值转到最佳权重值。 我们可以将该任务分为以下三个部分:
- 首先,我们需要知道初始化的权重是否合适。 如果不是,那么预测输出与预期输出有多少不同? 这由损失函数计算。
- 其次,提出了最大似然估计(MLE)的概念,该概念指出,为了找到一组优化的参数(在我们的情况下为权重),我们需要最大化获得期望值的可能性(概率)。 简而言之,如果我们尝试通过更改权重(参数)来使损失函数最小化,那么我们会将预测输出分布的概率最大化,使其与预期分布尽可能接近。
- 从以上两点可以得出结论,我们需要通过更新权重来最小化损失函数。 当达到损失的最小可能值时,可以说网络已经学习(训练)了映射函数,以预测接近预期值的值。 在 DFN 中,结合梯度下降和反向传播算法来完成权重的更新和损失函数的最小化。
在随后的部分中,我们将进一步讨论损失函数,以及如何将其最小化以训练具有梯度下降和反向传播的网络。
损失函数
将y视为来自 DFN 的预测输出,并将y_bar视为预期的输出(或标签)。 损失函数可以衡量您的预测的正确性! 根据期望输出和预测输出之间的差异定义最简单的损失函数,如下所示: L(w):

但是,这种简单的方程式被认为是一种不好的措施,因为这种损失函数倾向于给出可能为负也可能为正的值。 已经为回归和分类任务开发了各种损失函数,我们将在以下小节中介绍它们。
回归损失
回归任务需要直接预测值,并且预期输出也是直接值。 这使我们能够基于两个值之间的差异来确定损失。 我们将看到的第一个也是最常见的回归损失是均方误差损失。
均方误差(MSE)
让我们假设我们的数据集中有n个样本。 这意味着我们将具有n个预测值(y[1], y[2], ..., y[n])和n对应的期望值(y_bar[1], ..., y_bar[n])。 均方由以下公式定义:

顾名思义,该误差函数将预测值和期望值(误差)之间的差平方,然后取平均值。 根据值的不同,误差可能是正值,也可能是负值,将它们相加会导致具有相反符号的值被抵消,从而导致不正确的损失值。 因此,在求和之前将它们平方以使所有值均为正。 这样,每个误差仅会影响其大小。 采取另一种手段来使损失正常化并避免突然的高损失值。 由于存在平方值,因此数据中的任何异常值(输出值与预期值相距甚远的样本)将对损失造成的影响超出其应有的范围。
因此,如果数据集中有很多离群值,那么 MSE 并不是一个合适的选择! 但是,平方的易计算性和函数是可微的,MSE 是回归中最常见的损失函数。 解决具有不同符号的错误问题的另一种方法是 MSE。
平均绝对误差
平均绝对误差而不是平方误差,而是采用误差的绝对值使所有结果为正。 与 MSE 相比,采用绝对值而不是平方会使损失函数对异常值的敏感性降低。 但是,计算绝对值需要通过线性编程进行计算,并使损失函数不可微,这可能会导致重大问题,同时通过反向传播更新权重值。 (我们将在接下来的部分中看到反向传播。)以下等式描述了平均绝对误差函数:

分类损失
分类任务没有直接值可以预测。 相反,它们通常具有表示输入所属类别的正整数标签。 因此,期望值和预测值都只是整数表示。 因此,直接采用差异对分类将不起作用,对于回归也是如此。 为了解决这个问题,在概率分布上开发了损失函数。 分类的最常见损失函数之一是交叉熵。
交叉熵
可以将分类问题视为预测属于每个类别的输入的概率。 训练数据将由每个类的零或一的标签组成。 标签也可以被视为概率值:一个代表输入属于该类的肯定可能性,而零表示该输入不属于该类。
为了比较预测输出和预期输出,我们因此需要一种方法来比较两个概率分布(预测和预期标签的概率分布)。 交叉熵的作用完全一样! 认为y_bar[i]是第i个训练样本的预期标签,而yᵢ是我们模型中的预测标签。 然后,通过以下等式定义交叉熵损失:

由于使用了对数,因此交叉熵也称为对数损失。 当两个概率分布相似时,交叉熵损失将减少,并且当分布彼此不同时,交叉熵损失将增加。
我们已经说过,我们需要最小化损失函数。 我们将看到梯度下降是如何实现的。
梯度下降
让我们考虑成本函数J(w),它是网络权重w的函数。 成本函数仅是损失函数与其他与正则化相关的参数的组合。 (我们稍后将讨论过拟合和正则化。暂时,考虑成本函数与损失函数相同。)我们可以假设由下图表示的凸成本函数J(w):

前面我们提到,最初,我们选择一个随机值作为权重。 假设初始权重由上图中的A表示。 我们的目标是达到成本函数的最小值,例如J_min(w)。 假设先前示例中的成本函数在点B处具有最小值。 您可能会注意到,成本函数的最小值(点B)的斜率为零,而对于其他点(例如点A(我们的初始点))则不为零。 。 因此,我们可以利用成本函数的梯度(斜率)来达到最小值。
梯度或斜率是y轴值相对于x轴的变化率。 在函数的任何一点上,通过计算该函数相对于x的导数即可获得梯度。
这个想法是在初始点计算成本函数的梯度,然后以减小梯度直到其变为零(这将是最小值)的方式更新权重。 这就是为什么将该方法称为梯度下降的原因。 那么,权重如何更新? 梯度下降的不同变化使用不同的策略来更新权重。 梯度下降主要有三种类型:批量梯度下降(原始梯度下降),随机梯度下降和小批量梯度下降。
梯度下降的类型
在讨论梯度下降的类型之前,我们应该了解一个重要的参数,称为学习率(η)。 梯度下降逐步达到成本函数的最小值。 学习率决定了步骤的持续时间。 随着权重的更新,决定合适的学习率至关重要。 如果学习率太大,权重更新可能会超出最小值,这将导致最小值周围的永无止境的振荡。 如果学习率太小,该算法将采取非常小的步骤,这可能需要大量时间才能收敛到最小值。
为了使视图更清晰,下图中描述了这两种方案:

现在,我们将看到梯度下降的第一个基本变体-批梯度下降。
批量梯度下降
让我们说w_new是一批梯度下降之后经过更新的权重集合,而w是旧的权重集合。 权重根据以下规则进行更新:

您可以通过考虑我们的成本函数J(w)上的一个随机点来验证自己,即新的权重将始终朝向最小值。 批梯度下降需要计算整个数据集的梯度以执行一组更新。 因此,对于大型数据集,批量梯度下降的速度非常慢。 对于零件中处理的数据集,几乎不可能进行批量梯度下降。 为了克服这一主要缺点,引入了梯度下降的另一种形式-随机梯度下降。
随机梯度下降
经过训练集中的每个样本后,随机梯度下降会更新权重。 让我们考虑第i个训练样本x^(i)及其对应的标签y^(i)。 随机梯度下降下的权重更新规则如下:

在每个样本之后更新权重可显着提高梯度下降的收敛速度。 但是,这会使更新过于频繁,从而导致成本函数的波动。 但是,波动解决了一个有趣的问题,即权重卡在局部最小值上。 到现在为止,我们已经看到只有一个全局最小值的简单成本函数。 想象一个具有多个局部最小值的复杂成本函数。 全局最小值不必与局部最小值相同。 通常,批量梯度下降倾向于卡在最接近的初始化最小值,而该最小值可能不是全局最小值。 另一方面,随机梯度下降的波动允许跳到另一个可能更好的局部最小值。 下图显示了具有多个最小值的复杂成本函数:

小批量梯度下降
在每个样本之后更新权重会创建很多冗余更新,而在通过完整的数据集之后更新权重实际上也是不可能的。 微型批量梯度下降法通过从数据中进行微型批量后更新权重来解决这两个问题。 微型批量是来自训练数据的一小部分样本; 例如,如果一个数据集有 10,000 个训练样本,我们可以将数据分为多个小批,例如 64 个。在每次微型批量之后,权重都会更新。 假设n为微型批量的大小,则权重更新规则如下:

反向传播
我们已经看到了如何使用梯度下降来更新权重,但是要执行梯度下降,我们需要计算成本函数相对于权重的梯度(导数)。 假设要计算成本函数相对于权重w的梯度,我们可以使用以下导数方程,其中h是一个小的正数:

在这里,要计算权重的梯度,我们需要计算两次成本函数,即执行两次完整的前向传递。 任何深度学习网络都具有数百万的权重,并且通过上述公式计算梯度将在计算上非常昂贵。 那么,对此有什么解决方案? 反向传播! 1986 年,当 David Rumelhart,Geoffrey Hilton 和 Ronald Williams 表明使用反向传播在神经网络中可以更快地计算梯度时,该算法获得了发展。 反向传播算法的核心是链式规则。 链式规则允许您仅通过一次向前和一次向后通过来计算所有所需的梯度。
链规则用于计算复合函数的导数。 考虑两个函数z = f(y)和y = g(x),则函数z = f(y)相对x的导数可以作为z = f(y)相对于y以及y相对于x的导数。 以下等式表示相同:

类似地,链规则可以扩展为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)),σ代表激活函数:

接下来,考虑第l层中第j个神经元,误差δ由以下给出:

使用链式规则,我们可以编写以下公式:

同样,我们计算每一层的误差。 通过第l + 1层和第l层之间的误差之间的以下关系,计算最后一层的误差并将其反向传播到前一层。其中⊙表示逐元素乘法:

最后,根据以下等式,使用这些误差来计算成本函数相对于各层权重的梯度:

我们将不对方程进行数学证明,但是您可以了解算法如何根据方程从最后一层计算误差并将误差反向传播到先前的层。 例如第l层第j个神经元的权重因此被计算为该层误差(δ[j]^l),与从前一层第k个神经元到第l层第i个神经元的激活输出的乘积。
现在,我们已经看到了如何通过单次向后传递来计算权重更新所需的梯度。 我们已经提到了由于局部最小值和复杂的成本函数,梯度下降无法正确收敛的挑战。 接下来,我们将看到优化器如何通过调整学习率η解决该问题。
优化器
当沿着成本函数的斜率下降时,我们需要朝着所需的最小值靠拢一些。 否则,我们将超调,然后围绕最小值波动。 这可以通过在训练期间保持学习率可变来实现。 优化器提供的算法可在整个训练过程中调整学习率,以提供更好的收敛性。 TensorFlow 中的优化器执行使损失函数(成本函数)最小化的任务。 最常用的两个优化器是 Adam 优化器和 RMSProp 优化器。 我们将不在此处讨论优化器涉及的数学。
训练,测试和验证
通常,我们不会使用可用于训练模型的全部数据。 这样做将使我们没有任何余地来判断我们训练后的模型的表现,也无法检查训练是否朝着正确的方向进行。 我们将数据分为以下三组:
- 训练集
- 验证集
- 测试集
训练集
训练集包含将用于训练模型的数据。 该集合是三个集合中最大的集合,并且包含大多数数据(因为我们不想丢失很多训练数据)。 该模型通过迭代训练集来学习特征和模式。 尽管我们在训练期间测量训练的准确率,但实际上并没有评估模型。 由于模型已从训练集中学习并熟悉数据,因此训练精度可以达到很高的值。 因此,高训练精度并不一定意味着一个好的模型。 当模型对看不见的数据进行预测时,将对模型进行实际评估。
验证集
在训练模型时,验证集为模型提供了频繁且公正的评估。 我们不在训练集中包含验证集,但在训练过程中进行某些迭代后,将继续检查验证集的准确率。 验证集有助于在训练过程中微调参数。
最初,验证集似乎并不那么重要,但是当我们讨论过拟合时,您会看到它扮演的重要角色。
测试集
训练我们的模型的目的是对未知数据(模型未看到的数据)做出良好的预测。 因此,必须使用模型从未见过的数据来进行真正的评估。 测试集通常是从全部可用数据中保留的,在训练过程中永远不会显示给模型。 测试集可让您使用未知数据评估我们的模型。 这里要了解的重要事项是对于受监管的任务,测试集 还包含数据和相应的预期标签,例如训练集。 仅当我们将模型预测与标签进行比较以计算准确率时,才使用标签。 因此,应为验证集和测试集保留多少数据?
通常,最好将 80% 的可用数据保留为训练集,将 10% 作为验证集,将最后 10% 作为测试集,如下图所示:

过拟合和正则化
任何深度学习网络(例如 DFN)都具有很多可学习的参数(以百万计)。 具有大量参数的网络可以适合任何数据分布。 但是,训练的重点不是完全适合训练数据,而是学习表征数据的一般特征和模式。 模型可能会学会正确预测训练集中的每个样本,但无法在测试集中执行。 这通常发生在具有大量参数的网络中。 免费的可学习参数可用于学习训练集的每一个复杂性。 但是,这样做时,网络会过度学习,并且变得非常特定于训练数据,无法对不熟悉的数据执行操作。 这种过度学习现象被称为过拟合。 下图可以使您对现象有更好的了解。 过拟合的模型学习非常特定于训练数据的决策边界(红色),这通常不是一个好的决策边界:

黑线表示拟合良好,绿线表示过拟合-图片来自 Wikipedia
为了避免过拟合,在深度学习中使用了正则化技术。 正则化到底是什么意思? 正则化通过惩罚或带来参数的随机性来限制参数的过度学习。 使用三种主要的正则化技术来避免过拟合:
- L1 和 L2 正则化
- 丢弃
- 早期停止
数据扩充也被考虑在正则化下。 它涉及操纵输入数据以创建同一数据的更多变体。 例如,图像被移动和缩放以产生更多的变体。 数据扩充方法针对不同类型的数据而有所不同。
L1 和 L2 正则化
记得我们之前提到过,成本函数是损失函数与正则化参数的组合。 L1 和 L2 是添加到损失函数以构成成本函数的参数。 L1 和 L2 通过使某些权重可以忽略不计,从而损失了损失函数,从而减少了可能导致过度学习的自由参数。
L1 正则化将权重的绝对值作为惩罚系数添加到损失函数中,如下式所示:

公式中的第一项表示权重的平方损失函数,第二项是 L1 正则化系数。 λ是一个超参数,可以手动调整以达到良好的匹配度。 L1 正则化被认为是健壮的,因为它对数据的异常值不那么敏感。 但是 L1 正则化有时可能会在计算上变得昂贵,并可能在我们的损失函数中引入不连续性。 因此,由于其复杂性,通常不使用 L1 正则化。
另一方面,L2 正则化将权重的平滑平方值作为损失因子加到损失函数,如以下公式所示:

使用 L2 正则化的优点是易于计算平方并通过损失函数确保连续性。 但这是以敏感性为代价的。 由于平方项,L2 正则化会放大离群值的损失函数,因此使成本函数非常敏感。 在下一小节中,我们将看到什么是丢弃以及如何避免过拟合。
丢弃
丢弃是处理神经网络过拟合(或深度学习)的独特方法。 为了引入网络中的随机性,丢弃操作通过删除与该节点之间的权重连接来随机删除几个节点(神经元)。 丢弃在每个迭代中随机发生。 下图显示了 DFN 中丢弃的工作方式:

早期停止
在讨论验证集时,我们稍后将讨论验证集在过拟合中的作用。 在训练过程中,我们实际上不知道有多少次迭代训练模型。 这通常是过拟合的原因。 知道何时停止是重要的事情! 训练时设置验证集有助于我们决定何时停止。 由于训练算法将训练损失降到最低,因此不管模型是否过拟合,损失将始终保持减少。 因此,除了训练损失,我们还监视验证集上的损失。 训练和验证损失都将持续减少,直到达到最佳点为止。 此后,由于过拟合,验证损失将再次开始增加。 验证和训练损失开始出现分歧的点是我们需要停止过度训练并避免过拟合的点。 您可以在下图中观察到相同的结果:

到目前为止,我们已经收集了有关深度学习基本方面的足够信息。 因此,让我们开始深入研究使用 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)
- 加载数据后,我们将打印训练数据以及测试数据的形状。 输出形状应如以下屏幕截图所示:

数据中包含的标签是数字,从0到9,其中每个整数代表一个类。 我们将制作一个字典,将这些整数映射到 MNIST 时尚数据中提到的类。 您可以在以下屏幕截图中看到整数及其对应的类:

前面输出的代码如下:
# 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()
图像图将类似于以下屏幕截图。 每个子图的标题传达了图像所属的类:

标准化和分割数据
图像中像素的值范围为 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):我们遍历整个数据的次数。
让我们看下面的代码:
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')
添加层
接下来,我们将定义包含第一组权重和偏差的输入层,如下所示:
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
同样,我们还将定义第一个隐藏层,如以下代码所示:
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')
最后,我们添加输出层,如下所示:
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 激活。 这样做将导致模型的训练准确率突然下降。
# 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 优化器来最大程度地减少损失,如下所示:
# 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)
计算精度
有很多指标可以衡量模型的表现。 我们会持续监控批量的训练损失值和训练准确率。 准确率以百分比(多少个预测正确)度量。 随着精度的提高,损失值不断减小。 对于每次迭代,我们还检查验证损失和准确率。 由于验证损失在达到最小值后又开始增加,因此我们需要停止训练,否则我们将过拟合模型:
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)
我们每八个训练批量打印一次精度指标,如以下屏幕截图所示:


另外,我们每隔Validation个批量打印验证准确率,如以下屏幕截图所示:

您可能会注意到,在第 8 个阶段之后,验证准确率开始下降。 这表明该模型已充分学习,超出此点将使模型过拟合。
训练完成后,我们将根据测试数据测试表现。 将使用用于评估训练和验证准确率的相同操作,但是数据将更改为测试数据,如以下代码所示:
# 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打印一次测试准确率,如以下屏幕截图所示:

如果发现很难跟踪代码片段的结构和流程,我们在这里提供该模型的完整代码:
'''
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,如下所示:
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])
上面的代码将输出以下内容:

总结
我们从 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 的一般图如下所示:

通常,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 中显然没有输出层,因此学习与前馈网络中的学习有很大不同,如下所示:
- 与其减少描述地面实况与输出层之间差异的损失函数,不如尝试减少能量函数,该函数定义如下:

对于不熟悉能量函数的人,术语能量来自物理学,用于量化大型物体对另一个物体的重力。 物理学中的能量函数测量两个对象或机器学习中两个变量的兼容性。 能量越低,变量之间的兼容性越好,模型的质量越高。
- 与产生输出相反,它在其可见和隐藏单元集上分配概率,并且每个单元在每个时间点处于
0(关闭)或1(激活)的二进制状态下。 给定可见层v,隐藏单元被激活的概率计算如下:

类似地,给定一个隐藏层h,我们可以如下计算可见单元被激活的概率:

由于h和v的状态基于彼此随机分配给0或1,因此可以通过重复少量采样过程实现收敛。 下图演示了此过程:

从可见层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)计算梯度,其中⊗表示外部乘积:



这些梯度称为对比散度。
我希望您现在已经掌握了 RBM 背后的理论。 在简要介绍了 RBM 的演变路径之后,您将在动手部分中增强对 RBM 的理解,我们将在下一节中进行介绍。
RBM 的发展路径
顾名思义,RBM 源自玻尔兹曼机。玻尔兹曼机由 Geoffrey Hinton 和 Paul Smolensky 于 1983 年发明,是一种网络类型,其中所有单元(可见和隐藏)都处于二进制状态并连接在一起。 尽管他们具有学习有趣的表示形式的理论能力,但对他们来说还是有许多实际问题,包括训练时间,训练时间随模型大小呈指数增长(因为所有单元都已连接)。 玻尔兹曼机的总体示意图如下:

为了使学习玻尔兹曼机模型更容易,Paul Smolensky 于 1986 年首次发明了一种称为 Harmonium 的连接受限的版本。 在 2000 年中期,Geoffrey Hinton 和其他研究人员发明了一种效率更高的架构,该架构仅包含一个隐藏层,并且不允许隐藏单元之间进行任何内部连接。 从那时起,RBM 被应用于各种有监督的学习任务中,包括:
- 图像分类(《使用判别受限的玻尔兹曼机进行分类》)
- 语音识别(《使用受限的玻尔兹曼机学习语音声波的更好表示》)
它们也已应用于无监督的学习任务,包括以下内容:
- 降维(《使用神经网络降低数据的维数》)
- 特征学习(《无监督特征学习中的单层网络分析》),当然还有协同过滤和推荐系统 ,我们将在本节之后进行处理
您可能会注意到,RBM 有点浅,只有一个隐藏层。 Geoffrey Hinton 在 2006 年推出了称为 DBN 的深版本的 RBM。DBN 可以看作是堆叠在一起的一组 RBM,其中一个 RBM 的隐藏层是下一个 RBM 的可见层。 隐藏层充当分层特征检测器。 DBN 的一般图如下所示:

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 模型是一个搜索参数的过程,该参数可以通过吉布斯采样最好地重构输入向量。 让我们实现吉布斯采样方法,如下所示:
>>> 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
使用吉布斯采样和梯度,我们可以组合一个以时间为单位的参数更新,如下所示:
>>> 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 模型,由用户喜欢,不喜欢和未观看的一组电影组成的输入从可见层转到隐藏层,然后又回到可见层,从而生成输入的重构版本 。 除了与用户进行交互的电影外,重构的输入还包含以前未评级的信息。 它可以预测是否会喜欢这些电影。 一般图如下所示:

在此示例中,输入内容包括六部电影,其中三部被点赞(用1表示),两部不喜欢(用0表示),而另一部未分级(用?表示)。 该模型接受输入并对其进行重构,包括缺少的电影。
因此,模型如何知道缺少的单元应为0(或1)? 回忆每个隐藏的单元都连接到所有可见的单元。 在训练过程中,一个隐藏的单元试图发现一个潜在因素,该潜在因素可以解释数据中的一个属性,或本例中的所有电影。 例如,一个隐藏的二元单元可以了解电影类型是否是喜剧电影,是否属于正义电影,主题是否为复仇电影或其他任何捕捉到的东西。 在重构阶段,将为输入分配一个新值,该值是根据代表所有这些潜在因素的隐藏单元计算得出的。
听起来神奇吗? 让我们开始构建基于 RBM 的电影推荐器。
我们将使用 MovieLens 中的电影评分数据集。 这是一个非商业性网站,用于收集用户的移动收视率并提供个性化建议,由明尼苏达大学的研究实验室 GroupLens 运营。
首先,我们将在这个页面中查看 1M 基准数据集。 它包含来自 6,040 位用户的 3,706 部电影的大约一百万个收视率。 我们可以通过这里下载数据集并解压缩下载的文件。 等级包含在ratings.dat文件中,每一行是一个等级,格式如下:
UserID::MovieID::Rating::Timestamp
评分记录如下所示:
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),不需要伯努利采样。
现在,让我们加载数据集并构建训练数据集。 不要忘了跟踪已分级的电影,因为并非所有电影都已分级:
>>> 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 |
看一下它们的分布,如下所示:
>>> 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 模型拟合到训练集上,如以下代码所示:
>>> 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,我们可以使用训练后的模型推荐电影。 为此,我们需要返回优化的参数并使用这些参数添加预测方法。
在我们之前定义的训练方法中,我们通过更改以下行来保留更新的参数:
... _, batch_err = sess.run(
(para_update, error),feed_dict={X_train_plac: batch})
我们需要将以下几行替换为:
... parameters, batch_err = sess.run((para_update, error),
feed_dict={X_train_plac: batch})
然后,我们需要在方法末尾返回最后更新的参数,如下所示:
... return parameters
引入训练后的模型并重建输入数据的预测方法定义如下:
>>> 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
现在,我们可以获得输入数据的预测,如下所示:
>>> 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% 的测试集,它们的等级将分别用于训练模型和执行仿真。 如下代码所示:
>>> np.random.seed(1)
>>> np.random.shuffle(data)
>>> data_train, data_test = data[:num_train, :], data[num_train:, :]
其次,在测试集上,我们从每个用户中随机选择现有评级的 20% 进行模拟,如下所示:
>>> 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
所选等级暂时变为未知,如下所示:
>>> data_test_sim = np.copy(data_test)
>>> data_test_sim[sim_index] = 0.0
接下来,我们在训练集上训练模型,并在模拟测试集上进行预测,如下所示:
>>> 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 来评估预测准确率,如下所示:
>>> 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 模型的参数:
>>> 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 的输入向量。 因此,我们定义了一种额外的方法来简化此操作,如下所示:
>>> 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,如下所示:
>>> 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。
训练方法定义如下,其中在原始输入数据或先前隐藏层的输出上训练隐藏层的参数:
... 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,我们为训练和测试集生成最后一个隐藏层的输出向量,如以下代码所示:
>>> 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 维特征输入到逻辑回归模型中以完成数字分类任务,如下所示:
>>> from sklearn.linear_model import LogisticRegression
>>> lr = LogisticRegression(C=10000)
>>> lr.fit(feature_train, Y_train)
整个算法的流程如下所示:

最后,我们使用经过训练的逻辑回归模型来预测从测试集中提取的特征,如下所示:
>>> print(lr.score(feature_test, Y_test))
0.9777777777777777
用这种方法可以达到 97.8% 的分类精度。
什么是自编码器?
在上一部分中,我们刚刚学习了 RBM 及其变体 DBN,并获得了实践经验。 回想一下,RBM 由输入层和隐藏层组成,后者试图通过查找输入的潜在表示来重建输入数据。 从本节开始,我们将学习的神经网络模型自编码器(AE)具有相似的想法。 基本 AE 由三层组成:输入层,隐藏层和输出层。 输出层是通过隐藏层的输入的重建。 AE 的一般图如下所示:

可以看到,当自编码器接收数据时,它首先对其进行编码以适合隐藏层,然后尝试将其重新构造回原始输入数据。 同时,隐藏层可以提取输入数据的潜在表示。 由于这种结构,网络的前半部分称为编码器,该编码器将输入数据压缩为潜在表示。 相反,后半部分是解码器,用于对提取的表示进行解压缩。
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 个样本,但高度不平衡,几乎没有欺诈性样本,如下所示:
>>> 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% 的测试,如下所示:
>>> import numpy as np
>>> np.random.seed(1)
>>> data_train, data_test = train_test_split(data, test_size=0.2)
正如我们所估计的那样,欺诈类仅占总人口的 0.17%,因此传统的监督学习算法可能很难从少数民族中选择足够的模式。 因此,我们求助于基于 AE 的无监督学习解决方案。 训练有素的自编码器可以完美地重建输入数据。 如果我们仅在正常样本上安装自编码器,则该模型将成为仅擅于再现非异常数据的正常数据重构器。 但是,如果我们将此模型输入异常输入,则重构输出和输入之间会有相对较大的差异。 因此,我们可以通过使用 AE 测量重建误差来检测异常。
因此,我们重组了训练和测试集,因为仅需要正常样本即可拟合模型,如下所示:
>>> data_test = data_test.append(data_train[data_train.Class == 1],
ignore_index=True)
>>> data_train = data_train[data_train.Class == 0]
由于我们的方法不受监督,因此我们不需要训练目标。 因此,我们仅采用训练集中的特征,如下所示:
>>> X_train = data_train.drop(['Class'], axis=1).values
>>> X_test = data_test.drop(['Class'], axis=1).values
>>> Y_test = data_test['Class']
现在可以使用这些数据了。 现在是时候在 Keras 中构建原始自编码器了。 现在,让我们开始导入必要的模块,如下所示:
>>> 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)
最后,还有最后一层,即输出层,其大小与输入层相同,它对隐藏的表示进行解码,如下所示:
>>> decoder = Dense(input_size)(encoder)
使用以下代码将它们连接在一起:
>>> 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 的表现可视化工具,可提供训练和验证指标的动态图,例如:
>>> 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:
tensorboard --logdir=logs
它返回以下内容:
Starting TensorBoard b'41' on port 6006
(You can navigate to http://192.168.0.12:6006)
通过转到http://192.168.0.12:6006(主机可能有所不同,具体取决于您的环境),我们可以看到随着时间的推移训练损失和验证损失。
下图显示了平滑为 0(无指数平滑)时的训练损失:

此处显示了平滑为 0(无指数平滑)时的验证损失:

现在,我们可以将测试集提供给训练有素的模型,并计算由均方误差测量的重构误差,如下所示:
>>> recon = ae.predict(X_test)
>>> recon_error = np.mean(np.power(X_test - recon, 2), axis=1)
通常,我们将计算 ROC 曲线下的面积,以评估不平衡数据的二分类表现,如下所示:
>>> 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()
请参考以下曲线图,以得到精确的召回曲线:

精确调用曲线下的面积计算如下:
>>> area = auc(recall, precision)
>>> print('Area under precision-recall curve:', area)
Area under precision-recall curve: 0.8217824584439969
精确召回曲线下的面积为 0.82。 我们还可以绘制各种决策阈值下的精度和召回率,如下所示:
>>> 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()
请参考以下图表以获得预期结果:

可以看出,我们设置的阈值越高,精度越高,但是召回率却越低。 我们将选择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
其余代码与普通解决方案相同,在此不再赘述。 但是,这是前两个周期的结果:
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% 的数据用于训练,如下所示:
>>> data_train, data_test = train_test_split(data, test_size=0.95)
我们将快速通过常规自编码器进行基准测试,如下所示:
>>> 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
以下是前两个周期的结果:
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)
前两个周期的结果如下:
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 的工作方式:

DAE 通常用于神经网络预训练,其中提取的鲁棒表示形式用作下游监督学习的输入特征。 因此,它们不适用于我们的无监督解决方案。 您可以通过这个页面中的图像分类示例进行进一步研究。
压缩自编码器
我们将学习的最后一种自编码器是压缩自编码器。 它们与稀疏兄弟相似,因为它们增加了惩罚项以学习更强大的表示形式。 但是,惩罚项更为复杂,可以如下推导,其中hⱼ是隐藏层第j个单元的输出,W是编码器的权重,W[ij]是连接第i个输入单元,以及第j个隐藏单元的权重:

我们在上一节中定义的原始自编码器的顶部添加压缩项,如下所示:
>>> 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)
损失函数现在变为:
>>> 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
我们使用这种收缩损失来编译模型,如下所示:
>>> contractive_ae.compile(optimizer=optimizer, loss=contractive_loss)
其余代码保持不变,但是这次使用0.0003(optimizer = optimizers.Adam(lr=0.0003))的学习率。
我们在此介绍前两个周期的结果:
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
四、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)。 在下图中可以看到这一点:

输入层的形状由粉红色的立方体表示(图像来自 Wikipedia,CS231n 课程的一部分)
这里要注意的重要一点是,输入层用于将结构保持在三维空间中。 在下一节中,我们将看到卷积层如何利用此三维空间。
卷积层
我们将在这里讨论的第一件事是过滤器。 可以将过滤器视为由可学习的权重值组成的图像的较小版本。 就像我们在深层前馈网络中从一个神经元到另一个神经元具有权重连接一样,权重也存在于卷积层中,不同之处在于权重是以过滤器的形式将连接器覆盖的空间区域连接到神经元。 让我们考虑一个大小为5 x 5(宽度和高度)的过滤器的示例。 过滤器也将延伸到图像的第三维(通道)。 对于三通道图像,过滤器尺寸为5 x 5 x 3,对于单通道图像,过滤器尺寸为5 x 5 x 1。下图显示了5 x 5 x 1过滤器:

那么,过滤器在卷积层中做什么? 过滤器在卷积层中执行两项非常重要的任务-本地连接和参数共享。 之前,我们讨论了 CNN 中的感受野,这意味着仅将神经元连接到其邻域图像像素。 该邻域由过滤器定义。 我们在图像上滑动过滤器,过滤器中的每个权重都连接到特定幻灯片的特定神经元。 然后,神经元使用该位置处过滤器覆盖的图像像素的权重和值来计算卷积输出。 换句话说,我们可以说卷积层中的每个神经元都局部地连接到由过滤器定义的图像的局部区域。 这称为本地连接。 下图显示了本地连接:

观察感受野(输入层中的深粉红色斑块)如何连接到下一层的单个神经元
神经元如何通过卷积计算过滤器的输出? 为了理解这一点,让我们考虑在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)。 在这里,最后一个维度将等于激活图的数量,该数量将等于过滤器的数量。 可以使用以下公式计算输出的宽度和高度:

在这里,W是输入大小(W = 32),F是过滤器大小(F = 3),P为填充(P = 1),而S为跨步(S = 1)。
您可能已经观察到,我们正在整个图像上制作相同的过滤器幻灯片。 这意味着在幻灯片中使用相同的权重,而不是为每个幻灯片创建不同的权重集。 由于图像中不同位置的像素值高度相关,因此使卷积幻灯片共享权重会产生良好的效果。 如果证明过滤器在图像中的某个位置有用,则该过滤器也将在不同位置有用。 整个图像上的过滤器权重共享称为参数共享,大大减少了网络中所需参数的数量。
下一层称为最大池化层。 最大池化层用于减小激活图的大小。
最大池化层
CNN 的总体思想是保持通过过滤器提取特征并增加激活图的深度,同时减小宽度和高度尺寸,以便最终剩下高度压缩的特征向量。 为了减小激活图的尺寸,CNN 使用连续卷积层之间的最大池化层。
最大池化层具有两个主要参数-核大小和跨步。 最大池化还在其连接到的上一层的激活图上滑动一个窗口。 该窗口通常称为核。 核在任何幻灯片上的工作都是比较核所覆盖的值,并且仅保留最大值作为该位置的输出。 最常用的核大小是2 x 2。使用超出此大小的核大小会导致各层之间的信息大量丢失。 同样,跨步是决定核在幻灯片中跳转多少像素的参数。 下图演示了核在大小为2 x 2,跨步为 2 的4 x 4激活图上执行最大池化的过程:

来自 CS231n 的图像
下图显示了如何使用最大池化层来减小图像和特征映射的大小:

图片来自该课程 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 架构:

如果您希望参考 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 模块:

图片取自原始论文《随着卷积一起深入》
可以在这个页面上找到原始论文《随着卷积一起深入》的链接。
接下来,我们将研究一种称为 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 的构建块:

图片来源:深度残差学习,用于图像识别
跳过连接的添加解决了深度网络中的饱和和准确率降低的问题,使架构可以具有更多的层而不饱和。 该架构由 34 层组成,大部分包含3 x 3卷积过滤器。 为了减小特征映射的宽度和高度,使用了第 2 步卷积。 最后,使用全局平均池,然后使用 1,000 个单元的全连接层。 在下图中,与典型的 VGG-19 和没有残留连接的架构相比,您可以观察到 ResNet 架构:

图片来源:深度残差学习,用于图像识别
现在是时候建立我们自己的 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 版本。
数据下载并提取后,您将在提取的文件夹中找到以下文件:

由于数据集很大,因此分为五个批量-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索引列表,并将图像绘制在子图中的这些索引处,并将图像的类别显示为标题:
由于数据集中图像的尺寸较小,绘制的图像将非常模糊。
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
下面的屏幕快照显示了运行上述代码的输出:

接下来,我们将标签转换为单一编码形式。 我们已经在“第 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 之间:
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)。
接下来,我们将在网络中添加一个最大池化层:
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')
在此,第二维和第三维表示池化核的宽度和高度。 同样,我们将定义网络的其他层。 我们将逐渐增加深度,并减小宽度和高度:
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)
输出层也将是一个全连接层,不同之处在于我们将在此层中不使用任何激活函数:
with tf.name_scope('out') as scope:
outLayer = tf.layers.dense(dropOut3, units = self.classNum, activation = None, name='out_layer')
当我们为深度前馈网络定义损失函数和优化器时,我们将在此处类似地对其进行定义:
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 架构中涉及的步骤:

可以在这个页面上找到《用于准确的对象检测和语义分割的丰富的特征层次结构》本文的链接。
接下来,我们将研究通过将选择性搜索替换为单独的区域提议网络来改进 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 模块中进行分类。 在下图中,您可以观察到架构的完整管道:

图片来自纸本,更快的 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类的类概率。 您现在会注意到,该网络依赖于预测值,因此是基于回归的对象检测网络。 在下图中,您可以观察如何将图像划分为网格以预测边界框和类分数:

图片取自本文,您只看一次:统一的实时对象检测
论文的链接《只看一次:统一的实时对象检测》,可在这里找到。
YOLO 使用 24 个卷积层。 这些层遵循简单的结构,一个接一个地重复使用1 x 1和1 x 1卷积。 最后,存在两个全连接层以输出预测的张量。 下图中可以看到该架构:

图片来自本文,《只看一次:统一的实时对象检测》
YOLO 中使用的损失函数可分为四个部分:
- 边界框的位置
x和y的预测的平方和损失 - 在边界框的宽度和高度中进行预测的平方根损失
- 边界框置信度得分的损失
- 分类损失
以下公式包含 YOLO 的组合损失函数:

图片出自原始论文,“您只看一次”:统一的实时对象检测
损失函数中的第一项采用所有B边界框预测变量的边界框位置差的平方和。 第二项使用宽度和高度做相同的事情。 您会注意到额外的平方根。 这组作者说,大边界框中的小偏差比小边界框中的小偏差要小。 对项进行平方根运算有助于我们降低对较大值的敏感度。 我们还预测了置信度分数Cᵢ,以及边界框(预测边界框时模型的置信度)。 损失函数中的第三项与置信度得分有关。 损失函数中的最后一项是关于将对象分类为不同类别的。
尽管 YOLO 极大地简化了对象检测架构并能够实时进行预测,但是也存在某些缺点。 该模型不会提取不同比例的特征,因此对于不同大小和比例的对象不具有鲁棒性。 该模型还难以检测组合在一起的较小尺寸的对象。 接下来,我们将研究另一种基于回归的对象检测架构,即单发多框检测器(SSD),该架构可弥补 YOLO 的缺点。
单发多框探测器
与 YOLO 一样,SSD 也是基于回归的对象检测器,但是 SSD 的创建者声称 SSD 比 YOLO 更快,更准确。 我们可以将 SSD 分为四个主要部分:
- 基本网络 - VGG16
- 多个比例的特征映射
- 用于边界框预测的卷积
- 用于预测的默认边界框
任何卷积网络的首要任务是减小输入的尺寸并增加特征映射的深度,以便提取特征。 然后,可以将特征映射中提取的特征用于不同任务,无论是分类还是检测。 SSD 也一样! SSD 使用著名的 VGG16 架构作为模型的初始层(基础网络)进行特征提取(请记住,这与 YOLO 不同,因为图像本身首先被划分为网格,然后将卷积应用于预测)。 VGG16 架构末尾的全连接层已被删除,因为使用 VGG16 的目的只是为了提供丰富的特征来学习而非分类。 在改良的 VGG16 网络的末端,SSD 引入了六层以上的卷积。 这些额外的六层的大小逐渐减小。 添加额外层的目的是使网络能够从不同大小和不同比例的对象中提取特征。 这就是为什么这些层中的特征映射的大小不断减小(多比例缩放的特征映射)的原因。 下图显示了 SSD 的总体架构:

图片来自纸张,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])由以下公式给出:

在此, s_min是最低特征映射的比例, s_max是最高特征映射的比例。 然后,默认框的高度和宽度由以下关系定义:

以下屏幕快照说明了使用8 x 8函数图和8 x 8函数图进行 SSD 预测。 边界框偏移,Δ(cx, cy, w, h)和p类的类分数(c[1], c[2], ..., c[p])的预测:

图片来自纸张,SSD:单发多框检测器
SSD 中使用的损失函数是定位损失和分类损失的组合。 本地化损失是按以下方式定义的平滑 L1 损失:


在此,为预测边界框(l)和地面真实边界框(g)之间的所有N默认边界框计算平滑损失。 。 分类损失是针对类别的 softmax 计算类别分数之上的简单交叉熵损失。 分类损失由以下公式给出:

在这里,第一项是边界框是否包含对象,第二项是没有对象时。 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 对象检测网络。 您可以通过单击模型名称以压缩形式下载包含所有相关文件的目录,如以下屏幕截图所示:

下载.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 = '')
接下来,我们使用加载的图初始化会话:
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 图由张量变量和占位符组成,它们用于在会话期间流动和馈送数据。 为了获取输出并将输入输入模型,我们需要取出负责输入和输出的张量。 我们可以通过图中的名称来获取张量。 我们使用以下代码为图像的输出检测到的边界框,类和输入占位符获取张量:
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)
此后,我们只需要绘制最终图像以查看图像上的边界框:
plt.figure(figsize = (10, 10))
plt.imshow(image)
plt.show()
就是这样了! 如果您陷入前面的片段的缩进或流程中,则以下是完整的代码供您参考:
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):

这是检测到的结果:

总结
在本章中,我们首先讨论了深度前馈网络的缺点以及 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个数量的核,因为每个核输出一个通道。 下图显示了标准卷积的输出响应:

但是与标准卷积不同,深度卷积并未将输入中的所有通道都考虑在内以输出单个通道。 而是,它分别在每个通道上执行卷积。 因此,对n通道图像执行深度卷积将在输出中产生n通道。 深度方向可分卷积有两个部分-深度方向卷积(我们刚刚讨论过)和点方向卷积。 深度卷积之后是点状卷积,这只是一个具有1 x 1核的常规卷积运算。 需要逐点卷积来组合深度卷积的结果。
下图显示了深度可分离卷积的输出响应:

那么,使用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 的深度。 让我们考虑一下前面提到的相同场景。 我们有一个MxMxN图片:
- 此处的核大小为
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个点状核的最终输出为GxGxD。 因此,乘法数变为G^2 ND。 - 深度方向可分离卷积所需的乘法总数是深度方向卷积和点方向卷积所需的乘法总和,如下所示:

我们可以通过以下方式比较标准卷积和深度可分离卷积所需的乘法次数:

通常,如果我们将D = 256,K = 3,则比率为 0.115。 这意味着深度方向可分离卷积的参数是标准卷积的九倍。
希望您现在对 MobileNet 如何通过深度方向可分离卷积减少参数数量有所了解。 现在,让我们在下一个小节中查看 MobileNet 的结构。
MobileNet 的结构
MobileNet 的结构由 30 层组成。 它以3 x 3的标准卷积作为第一层开始。 此后,继续进行深度卷积和点卷积。 深度可拆分卷积块是深度可拆分卷积和点式卷积的连续组合,如下图所示:

图片来自《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 激活函数:

让我们在下表中查看 MobileNet 的完整架构:

图片来自《MobileNets:针对移动视觉应用的高效卷积神经网络》
既然我们已经了解了 MobileNet 的架构以及如何通过深度方向上可分离的卷积减少参数的数量,那么让我们看一下 MobileNet 的实现。
MobileNet 与 Keras
MobileNet 经过 ImageNet 数据训练。 通过使用 Keras 应用类,我们可以使用模型的预训练权重来实现 MobileNet。 在 Keras 应用中,您可以找到许多预先训练的模型供使用。 您可以通过这里浏览 Keras 应用的文档。
所以,让我们开始吧! 首先,显然,我们将导入所需的依赖项:
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 将继续更新状态,完成后将如下图所示:

我们将使用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 对我们提供的图像做出预测了:
prediction = model.predict(img)
当模型根据 ImageNet 数据集预测类别时,我们将使用decode_predictions函数以人类可读的形式带回前五项预测:
output = imagenet_utils.decode_predictions(prediction)
print(output)
让我们使用以下鹈鹕鸟的图像并检查 MobileNet 是否能够对其进行分类:

图片来自维基百科
您需要按照以下方式从正在运行的环境下的终端运行脚本以及图像的路径:
$python mobilenet_keras.py --im_path=pelican.jpg
以下是我们脚本的输出。 您可以看到 MobileNet 将该图像预测为鹈鹕,概率为 0.99! 在预测的前五类中,还有其他一些看起来像鹈鹕的鸟,但是由于 softmax 激活,它们的概率被抑制了:

您可以使用与 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 的总体构建块:

现在,架构还剩下一件事:剩余层。 尽管它类似于 ResNet 模型中的跳过连接,但我们将查看有关它的一些详细信息。
反向残差块
由于通过将许多层堆叠在一起,深度学习模型变得太深,训练网络变得非常困难。 这不仅是由于需要巨大的计算能力,而且还因为梯度逐渐消失在层中。 我们知道深度学习模型中的所有学习都取决于通过反向传播的梯度流。 在大型网络中,梯度随着每一步而变小,并在穿过所有层之前消失。 这种现象限制了我们使网络变得太深。 ResNet 架构中引入的剩余连接或跳过连接可帮助我们克服此问题。 来自上一层的连接将添加到一层,以便梯度获得易于流动的路径。 这些跳过连接使 ResNet 模型比通常的更深。
受跳过连接的启发,MobileNetV2 的作者声称有用的信息仅位于瓶颈层,因此,为了使梯度易于通过多个瓶颈块流动,他们增加了从一个瓶颈层到另一个瓶颈层的剩余连接。 由于 ResNet 中原始残差连接和 MobileNetV2 中残差连接之间的设计差异,作者选择将此称为反向残差。
下图可以看出差异:

图片来自研究论文《MobileNetV2:残差和线性瓶颈》
既然我们已经涵盖了 MobileNetV2 的所有元素,我们将研究 MobileNetV2 的整体结构。
整体架构
MobileNetV2 首先对图像执行标准卷积,以创建 32 个过滤器特征映射。 此后,有 19 个残存的瓶颈层块(我们在扩展层子部分中看到的块)。 所有卷积核的大小均为3 x 3。整个网络中一直使用 6 的恒定扩展因子。 下表列出了 MobileNetV2 的总体架构:

图片来自研究论文《MobileNetV2:残差和线性瓶颈》
在上表中,n列表示重复特定层的次数。 s列代表用于该层的跨步。 列c和t分别表示层中使用的通道数和扩展因子。
与 MobileNet 相似,我们也可以使用 Keras 来实现 MobileNetV2。
实现 MobileNetV2
我们将遵循与 MobileNet 相似的过程。 您可以在 Keras 应用中找到 MobileNetV2。 我们将使用与 MobileNet 相同的代码,除了这次将使用 MobileNetV2。 供参考,代码如下:
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 的权重,这可能需要一些时间,具体取决于您的互联网连接。 它看起来像这样:

让我们使用以下火烈鸟图像来检查输出:

这是输出的样子。 我们可以看到该网络大约有 86% 的人确定该图像是火烈鸟。 您可以观察到其他类别的概率由于 softmax 而受到抑制:

一年内推出了两个版本的 MobileNet。 第二个版本包含重大更改,我们已经讨论过。 现在,让我们比较两个网络的一些标准参数。
比较两个 MobileNet
MobileNetV2 对 MobileNet 的架构进行了重大更改。 这些更改值得吗? 在表现方面,MobileNetV2 比 MobileNet 好多少? 我们可以根据一次推理所需的乘法运算数量来比较模型,这通常称为 MAC(乘法累加数)。 MAC 值越高,网络越重。 我们还可以根据模型中的参数数量来比较模型。 下表显示了 MobileNet 和 MobileNetV2 的 MAC 和参数数:
| 网络 | 参数数 | MAC |
|---|---|---|
| MobileNet V1 | 420 万 | 575M |
| MobileNet V2 | 340 万 | 300M |
我们还可以根据不同通道数和分辨率所需的内存来比较模型。 下表提供了比较数据。 测量的内存为千字节(Kb):

TensorFlow 还提供了在像素 1 移动电话上运行的两个 MobileNet 之间的准确率与延迟比较。 延迟基本上表示运行模型需要多少时间。 下图显示了比较:

您可以在这个页面上找到有关比较的更多详细信息。
MobileNetV2 不仅仅是分类。 该架构的作者提出了将对象检测和分段架构相结合的想法。 在下一部分中,我们将介绍用于对象检测的 MobileNetV2 和 SSD 的非常成功的组合。
SSD MobileNetV2
MobileNetV2 的制造商还使移动设备的实时对象检测成为可能。 他们介绍了 SSD 对象检测器和 MobileNetV2(称为 SSDLite)的组合。 请记住,在“第 4 章”,“CNN 架构”中,我们将ssd_mobilenetv2用于对象检测。 与 SSDLite 相同。 选择 SSD 的原因很简单。 SSD 的构建独立于基础网络,因此卷积被深度可分离卷积替代。 SSDLite 的第一层连接到 MobileNetV2 的第 15 层的扩展。 用深度可分离卷积替换标准卷积可以显着减少网络进行对象检测所需的参数数量。
下表显示了原始 SSD 网络和 SSDLite 所需的参数数量和乘法运算的比较:

图片来自研究论文《MobileNetV2:残差和线性瓶颈》
总结
在本章的开头,我们讨论了使神经网络在实时应用中运行所需的移动神经网络。 我们讨论了 Google 推出的两种基准 MobileNet 架构-MobileNet 和 MobileNetV2。 我们研究了深度可分离卷积之类的修改如何工作,并取代了标准卷积,从而使网络能够以更少的参数获得相同的结果。 通过 MobileNetV2,我们研究了通过扩展层和瓶颈层进一步缩小网络的可能性。 我们还研究了 Keras 中这两个网络的实现,并根据参数数量,MAC 和所需的内存比较了这两个网络。 最后,我们讨论了 MobileNets 与对象检测网络(例如 SSD)的成功组合,以在移动设备上实现对象检测。
在下一章中,我们将介绍另一种成功的深度学习架构,称为循环神经网络(RNN)。 这些网络旨在捕获序列中的时间信息,例如句子或任何其他文本。
六、循环神经网络
在本章中,我们将解释最重要的深度学习模型之一,即循环神经网络(RNNs)。 我们将首先回顾什么是 RNN,以及为什么它们非常适合处理顺序数据。 在简要介绍了 RNN 模型的发展路径之后,我们将说明根据不同形式的输入和输出数据以及工业示例进行分类的各种 RNN 架构。 我们将找出问题的答案,例如“我们如何仅生成一个输出?”,“输出可以是序列吗?”,和“仅对一个输入单元有效吗?”。
接下来,我们将讨论按循环层分类的几种架构。 首先,我们将应用基本的 RNN 架构来编写我们自己的《战争与和平》。 具有原始架构的 RNN 不能很好地保存长期依赖的信息。 为了解决这个问题,我们将学习内存增强型架构,包括长短期内存和门控循环单元。 我们还将在股票价格预测中采用门控架构。 最后,由于对捕获过去的信息不满意,我们将引入一种双向架构,该架构允许该模型从序列的过去和将来上下文中保留信息。 具有 LSTM 的双向模型将用于对电影评论的情感进行分类。
在本章中,我们将介绍以下主题:
- 什么是 RNN?
- RNN 的演进路径
- RNN 架构按输入和输出(一对多,多对一,同步和不同步的多对多)
- 原始 RNN 架构
- 用于文本生成的原始 RNN
- 长期记忆
- 用于文本生成的 LSTM RNN
- 门控循环单元
- 用于股价预测的 GRU RNN
- 双向 RNN
- 用于情感分类的 BRNN
什么是 RNN?
回想一下,在前几章中讨论的深度前馈网络,自编码器神经网络和 CNN 中,数据从输入层到输出层是单向流动的。 但是,深度学习模型允许数据沿任何方向进行,甚至可以循环回到输入层,并且不仅限于前馈架构。 从上一个输出循环返回的数据成为下一个输入数据的一部分。 RNN 就是很好的例子。 下图描述了 RNN 的一般形式,在本章中,我们将研究 RNN 的几种变体:

如上图所示,来自先前时间点的数据进入了当前时间点的训练。 循环架构使模型可以很好地与时间序列(例如产品销售,股票价格)或顺序输入(例如文章中的单词-DNA 序列)配合使用。
假设我们在传统的神经网络中有一些输入xₜ(其中t代表时间步长或顺序顺序),如下图所示。 假设不同t处的输入彼此独立。 可以将任何时间的网络输出写为h[t] = f(x[t]),如下所示:

在 RNN 中,反馈回路将当前状态的信息传递到下一个状态,如下图的网络展开版本所示。 RNN 网络在任何时间的输出都可以写成h[t] = f(h[t-1], x[t])。 对序列的每个元素执行相同的任f,并且输出hₜ取决于先前计算的输出h[t-1]。 得益于这种类似链的架构,或到目前为止所计算出的额外的存储器捕获,在将 RNN 应用于时间序列和序列数据方面取得了巨大的成功。 在将 RNN 的变体应用于各种问题之前,首先,我们将了解 RNN 的历史及其演变路径:

RNN 的演进路径
RNN 实际上具有悠久的历史,最早是在 1980 年代开发的。 霍普菲尔德网络是第一个具有循环链接的神经网络,它是约翰·霍普菲尔德(John Hopfield)在《Neurons with graded response have collective computational properties like those of two-state neurons》中发明的。
受 Hopfield 网络的启发,在《及时发现结构》中引入了全连接神经网络 -- Elman 网络。 Elman 网络具有一个隐藏层和一组连接到该隐藏层的上下文单元。 在每个时间步,上下文单元都会跟踪隐藏单元的先前值。
1992 年,Schmidhuber 由于记住了长期依赖性而发现了梯度消失的问题。 五年后,Schmidhuber 和 Hochreiter 在《LSTM》中提出了长短期记忆(LSTM)。 遗忘门单元增强了 LSTM 的功能,它可以删除旧的和无关的内存,从而避免梯度消失。
在 1997 年,RNN 扩展为双向版本(在《双向循环神经网络》中发表,该模型在正向(从头到尾)和负向(从头到尾)时间方向上训练。
《分层控制如何在人工自适应系统中进行自组织》中介绍的分层 RNN 同时具有水平和垂直循环连接,从而分解复杂和自适应信息。
自 2007 年以来,LSTM 开始盛行:在《循环神经网络的判别性关键词发现应用》中,它们在某些语音识别任务中的表现优于传统模型。 在 2009 年,通过连通性时间分类(CTC)训练的 LSTM 模型用于语音音频中的连接手写识别和音素识别等任务。 LSTM 模型也成为机器翻译和语言建模的最新解决方案。 如《Show and Tell:神经图像字幕生成器》中所述,LSTM 甚至与 CNN 结合以自动进行图像字幕。
早在 2014 年,GRU RNN 被引入,是对常规 RNN 的另一项改进,类似于 LSTM。 GRU RNN 在许多任务上的表现与 LSTM 相似。 但是,它们在较小的数据集上表现出更好的表现,部分原因是它们需要调整的参数较少,而架构中的门却较少。
按照承诺,我们将详细研究 RNN 的变体,然后将其应用于实际问题。
RNN 架构和应用
RNN 可以分为多对一,一对多,多对多(同步)和多对多(基于它们的输入和输出)。 从隐藏层的角度来看,最常用的 RNN 架构包括基本的原始 RNN 和双向的 LSTM 和 GRU。 我们将专注于 RNN 的这四种架构,并将首先通过输入和输出简要提及这四个类别。
不同输入和输出的架构
多对一:多对一架构可能是最直观的。 我们可以在序列中输入尽可能多的元素或时间步长,但是模型在经历整个序列后仅产生一个输出。 下图显示了其一般结构,其中f表示一个或多个循环层:

多对一架构可用于情感分析,其中该模型读取整个客户评论(例如)并输出情感分数。 类似地,它可以用于在遍历整个音频流之后识别歌曲的流派。
一对多:与多对一 RNN 相反,一对多架构仅接受一个输入并产生一个输出序列。 一对多架构可以表示如下:

像这样的 RNN 通常用于生成序列。 例如,我们可以使用该模型来生成带有起始音符或流派作为唯一输入的音乐。 以类似的方式,我们甚至可以用指定的起始词以莎士比亚风格写诗。
多对多(同步):第三种架构多对多(同步)使每个输入需要一个输出。 正如我们在以下网络流中看到的那样,每个输出都取决于所有先前的输出和当前输入:

多对多(同步)架构的常见用例之一是时间序列预测,在这种情况下,我们希望在给定当前和所有先前输入的情况下,在每个时间步长执行滚动预测。 此架构还广泛用于自然语言处理(NLP)问题,例如命名实体识别(NER),词性(PoS)标记和语音识别。
多对多(不同步):对于多对多架构的不同步版本,该模型在完成读取整个输入序列之前不会生成输出。 结果,输出的数量可以与输入的数量不同。 如下图所示,输出序列Ty的长度不必等于输入序列Tx的长度:

在机器翻译中最常见的是不同步的多对多架构。 例如,模型读取英语的整个句子,然后开始生成法语的翻译句子。 另一个流行的用例是提前进行多步预测,要求我们根据所有先前的输入来预测提前几个时间步。
到目前为止,我们已经通过模型输入和输出了解了四种 RNN 架构,我们将在本章的其余部分的实际示例中结合其中的一些,我们将主要讨论隐藏层中的架构,更具体地说,是循环层。
让我们从原始 RNN 开始,这是循环架构的最基本形式。
原始神经网络
标注了权重且展开后的版本的基本 RNN 模型如下所示:

这里,U是连接输入层和隐藏层的权重,V是隐藏层和输出层之间的权重,W是循环层,即反馈层的权重; sₜ是时间步t的隐藏状态,xₜ和hₜ分别是时间步t的输入和输出。
请注意,为简便起见,我们从现在开始仅使用一个循环层,但是我们可以将多个循环层堆叠在一起,我们将很快看到。
层之间的关系可以描述如下:
- 基于当前输入
xₜ和通过s[t] = a(U x[t] + W s[t-1])的先前隐藏状态s[t-1]计算时间步长t和sₜ的隐藏状态,其中a是激活函数。 RNN 中隐藏层的激活函数的典型选择包括 tanh 和 ReLU。 - 同样,
s[t-1]取决于s[t-2]: s[t-1] = a(U x[t] + W s[t-2]),依此类推。s₁也依赖于s₀,按照惯例,该s₀设置为全零。 - 由于对时间步长具有这种依赖性,因此可以将隐藏状态视为网络的内存,从以前的时间步长中捕获信息。
- 将时间步长
t的输出计算为h[t] = g(V s[t]),其中g是激活函数。 根据执行的任务,它可以是用于二分类的 Sigmoid 函数,用于多类分类的 softmax 函数以及用于回归的简单线性函数。
与传统的神经网络类似,所有的权重U,V和W均使用反向传播算法进行训练。 但是不同之处在于,在当前时间t上,我们需要计算除当前时间之外的所有先前t-1个时间步的损失。 这是因为权重由所有时间步共享,并且一个时间步的输出间接依赖于所有先前的时间步,例如权重的梯度。 例如,如果要计算时间步t = 5的梯度,则需要向后传播前四个时间步,并对五个时间步的梯度求和。 这种特殊的反向传播算法称为时间上的反向传播(BPTT)。
从理论上讲,RNN 可以从输入序列的开头捕获信息,从而增强了时间序列或序列建模的预测能力。 但是,由于梯度梯度消失,原始 RNN 并非如此。 我们将在后面对此进行详细说明,并将了解专门设计用于解决此问题的其他架构,例如 LSTM 和 GRU。 但是现在,让我们假设原始 RNN 在许多情况下都能正常工作,并且获得了一些实践经验,因为它们是任何 RNN 的基本组成部分。
用于文本生成的原始 RNN
如前所述,RNN 通常在 NLP 域中用作语言模型,它在单词序列上分配概率分布,例如机器翻译,PoS 标记和语音识别。 我们将使用一种相当有趣的语言来对问题文本生成进行建模,其中 RNN 模型用于学习指定域的文本序列,然后在所需域中生成全新且合理的文本序列。
基于 RNN 的文本生成器可以接受任何输入文本,例如《哈利波特》等小说,莎士比亚的诗歌以及《星球大战》的电影剧本,并可以生成自己的《哈利波特》,莎士比亚的诗歌和《星球大战》电影剧本。 如果对模型进行了很好的训练,那么人工文本应该是合理的,并且阅读起来应与原始文本相似。 在本部分中,我们将以《战争与和平》和俄罗斯作家列夫·托尔斯泰的小说为例。 随意使用您喜欢的任何书籍进行训练。 我们建议从没有版权保护的书中下载文本数据。 古腾堡计划是一个很好的资源,拥有超过 5.7 万本版权已过期的免费优秀书籍。
首先,我们需要直接从这里下载《战争与和平》的.txt文件。 或者,我们可以从 Gutenberg 项目下载该文件,但是我们将需要进行一些清理,例如,从文件以及目录中删除开头部分Project Gutenberg EBook,以及结尾的End of the Project。
然后,我们读取文件,将文本转换为小写,并通过打印出前 100 个字符来快速查看它:
>>> training_file = 'warpeace_input.txt'
>>> raw_text = open(training_file, 'r').read()
>>> raw_text = raw_text.lower()
>>> raw_text[:100]
'ufeff"well, prince, so genoa and lucca are now just family estates of thenbuonapartes. but i warn you, i'
现在,我们需要计算总共有多少个字符:
>>> n_chars = len(raw_text)
>>> print('Total characters: {}'.format(n_chars))
Total characters: 3196213
然后,我们可以获得唯一的字符和词汇量:
>>> chars = sorted(list(set(raw_text)))
>>> n_vocab = len(chars)
>>> print('Total vocabulary (unique characters): {}'.format(n_vocab))
Total vocabulary (unique characters): 57
>>> print(chars)
['n', ' ', '!', '"', "'", '(', ')', '*', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '=', '?', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!!CDP!E.agrave!!', '!!CDP!E.auml!!', '!!CDP!E.eacute!!', '!!CDP!E.ecirc!!', 'ufeff']
现在,我们有了一个原始的训练数据集,其中包含超过 300 万个字符和 57 个唯一字符。 但是我们如何将其提供给 RNN 模型呢?
回想一下,在同步多对多架构中,该模型接受序列并同时生成序列。 在我们的例子中,我们可以给模型提供固定长度的字符序列。 输出序列的长度与输入序列的长度相同,并且一个字符从其输入序列偏移。 假设我们从learning的单词设置序列长度为5。 现在,我们可以使用输入learn和输出earni来构造训练样本。 我们可以在网络中对此进行可视化,如下所示:

我们只是构造了一个训练样本。 对于整个训练集,我们可以将原始文本数据分成相等长度的序列,例如 100。每个字符序列都是训练样本的输入。
接下来,我们以相同的方式将原始文本数据拆分为序列,但是这次从第二个字符开始。 每个结果序列都是训练样本的输出。 例如,给定原始文本deep learning architectures和5作为序列长度,我们可以创建五个训练样本,如下所示:
| 输入 | 输出 |
|---|---|
deep_ |
eep_l |
learn |
earni |
ing_a |
ng_ar |
rchit |
chite |
ectur |
cture |
在此,_表示空间。
请注意,最后一个子序列es不够长,因此我们可以简单地忽略它。
由于神经网络模型仅吸收数字数据,因此字符的输入和输出序列由单热编码的向量表示。 我们通过将 57 个字符映射到从0到56的索引以及另一个相反的索引来创建字典:
>>> index_to_char = dict((i, c) for i, c in enumerate(chars))
>>> char_to_index = dict((c, i) for i, c in enumerate(chars))
>>> print(char_to_index)
{'n': 0, ' ': 1, '!': 2, '"': 3, "'": 4, '(': 5, ')': 6, '*': 7, ',': 8, '-': 9, '.': 10, '/': 11, '0': 12, '1': 13, '2': 14, '3': 15, '4': 16, '5': 17, '6': 18, '7': 19, '8': 20, '9': 21, ':': 22, ';': 23, '=': 24, '?': 25, 'a': 26, 'b': 27, 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34, 'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41, 'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48, 'x': 49, 'y': 50, 'z': 51, '!!CDP!E.agrave!!': 52, '!!CDP!E.auml!!': 53, '!!CDP!E.eacute!!': 54, '!!CDP!E.ecirc!!': 55, 'ufeff': 56}
例如,字符e成为长度为 57 的向量,索引为30的为1,所有其他索引的值为 0s。 准备好角色查找表后,我们可以如下构建训练数据集:
>>> import numpy as np
>>> seq_length = 100
>>> n_seq = int(n_chars / seq_length)
将序列长度设置为 100,我们将获得n_seq训练样本。 接下来,我们初始化训练输入和输出:
请注意,序列长度具有形状(样本数,序列长度,特征维数)。 由于我们将使用 Keras 进行 RNN 模型训练,因此需要这种形式。
>>> X = np.zeros((n_seq, seq_length, n_vocab))
>>> Y = np.zeros((n_seq, seq_length, n_vocab))
组装每个n_seq样本:
>>> for i in range(n_seq):
... x_sequence = raw_text[i * seq_length : (i + 1) * seq_length]
... x_sequence_ohe = np.zeros((seq_length, n_vocab))
... for j in range(seq_length):
... char = x_sequence[j]
... index = char_to_index[char]
... x_sequence_ohe[j][index] = 1.
... X[i] = x_sequence_ohe
... y_sequence =
raw_text[i * seq_length + 1 : (i + 1) * seq_length + 1]
... y_sequence_ohe = np.zeros((seq_length, n_vocab))
... for j in range(seq_length):
... char = y_sequence[j]
... index = char_to_index[char]
... y_sequence_ohe[j][index] = 1.
... Y[i] = y_sequence_ohe
>>> X.shape
(31962, 100, 57)
>>> Y.shape
(31962, 100, 57)
同样,每个样本都由单热编码字符的100元素组成。 我们终于准备好了训练数据集,现在是时候构建我们的原始 RNN 模型了。 让我们训练一个具有两个循环层的模型,如下所示:

每层包含 800 个单元,其中0.3和 ReLU 的丢弃率作为激活函数。 首先,导入所有必需的模块:
>>> from keras.models import Sequential
>>> from keras.layers.core import Dense, Activation, Dropout
>>> from keras.layers.recurrent import SimpleRNN
>>> from keras.layers.wrappers import TimeDistributed
>>> from keras import optimizers
现在,指定其他超参数,包括批量大小和周期数,以及隐藏层和单元数以及丢弃率:
>>> batch_size = 100
>>> n_layer = 2
>>> hidden_units = 800
>>> n_epoch= 300
>>> dropout = 0.3
现在,创建并编译网络:
>>> model = Sequential()
>>> model.add(SimpleRNN(hidden_units, input_shape=
(None, n_vocab),return_sequences=True, activation='relu'))
>>> model.add(Dropout(dropout))
>>> for i in range(n_layer - 1):
... model.add(SimpleRNN(hidden_units, return_sequences=True,
activation='relu'))
... model.add(Dropout(dropout))
>>> model.add(TimeDistributed(Dense(n_vocab)))
>>> model.add(Activation('softmax'))
关于我们刚刚建立的模型,需要注意以下几点:
return_sequences=True:循环层的输出变成一个序列,从而实现了我们想要的多对多架构。 否则,它将变成多对一,最后一个元素作为输出。TimeDistributed:由于循环层的输出是一个序列,而下一层-密集层-不接受顺序输入,因此TimeDistributed换行用作适配器来解决此问题。Softmax:之所以使用这种激活,是因为该模型生成了一个单编码字符向量。
至于优化器,我们将选择 RMSprop,其学习率为0.001:
>>> optimizer = optimizers.RMSprop(lr=0.001, rho=0.9,
epsilon=1e-08, decay=0.0)
>>> model.compile(loss="categorical_crossentropy",optimizer=optimizer)
添加了多类交叉熵的损失度量之后,我们就完成了模型的构建。 我们可以通过使用以下代码来查看模型的摘要:
>>> print(model.summary()) _________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_1 (SimpleRNN) (None, None, 800) 686400
_________________________________________________________________
dropout_1 (Dropout) (None, None, 800) 0
_________________________________________________________________
simple_rnn_2 (SimpleRNN) (None, None, 800) 1280800
_________________________________________________________________
dropout_2 (Dropout) (None, None, 800) 0
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 57) 45657
_________________________________________________________________
activation_1 (Activation) (None, None, 57) 0
=================================================================
Total params: 2,012,857
Trainable params: 2,012,857
Non-trainable params: 0
_________________________________________________________________
我们有超过 200 万个参数需要训练。 但是,在开始漫长的训练过程之前,最好建立一些回调,以便在训练过程中跟踪模型的统计信息和内部状态。 我们将采用的回调函数包括:
- 模型检查点,它在每个周期后保存模型,以便我们加载最新保存的模型,并在模型意外停止时从那里继续训练。
- 尽早停止,当失去的表现不再改善时,停止训练。
- 定期检查文本生成结果。 我们想看看生成的文本是多么合理,并且训练损失不够明显。
这些函数的定义或初始化如下:
>>> from keras.callbacks import Callback, ModelCheckpoint, EarlyStopping
>>> file_path =file_path =
"weights/weights_epoch_{epoch:03d}_loss_{loss:.4f}.hdf5"
>>> checkpoint = ModelCheckpoint(file_path, monitor='loss',
verbose=1, save_best_only=True, mode='min')
模型检查点将与周期号一起保存,而训练损失则保存在文件名中。 我们还同时监视验证损失,并查看其是否在50个连续的周期内停止下降:
>>> early_stop = EarlyStopping(monitor='loss', min_delta=0,
patience=50, verbose=1, mode='min')
接下来,我们有用于质量监控的回调。 首先,我们编写一个辅助函数,该函数在给定 RNN 模型的情况下生成任意长度的文本:
>>> def generate_text(model, gen_length, n_vocab, index_to_char):
... """
... Generating text using the RNN model
... @param model: current RNN model
... @param gen_length: number of characters we want to generate
... @param n_vocab: number of unique characters
... @param index_to_char: index to character mapping
... @return: string of text generated
... """
... # Start with a randomly picked character
... index = np.random.randint(n_vocab)
... y_char = [index_to_char[index]]
... X = np.zeros((1, gen_length, n_vocab))
... for i in range(gen_length):
... X[0, i, index] = 1.
... indices = np.argmax(model.predict(
X[:, max(0, i - seq_length -1):i + 1, :])[0], 1)
... index = indices[-1]
... y_char.append(index_to_char[index])
... return ('').join(y_char)
它以随机选择的字符开头。 然后,输入模型根据过去生成的字符来预测剩余的每个gen_length-1字符,这些字符的长度最大为100(序列长度)。
现在,我们可以定义callback类,该类为每个N个周期生成文本:
>>> class ResultChecker(Callback):
... def __init__(self, model, N, gen_length):
... self.model = model
... self.N = N
... self.gen_length = gen_length
...
... def on_epoch_end(self, epoch, logs={}):
... if epoch % self.N == 0:
... result = generate_text(self.model, self.gen_length,
n_vocab, index_to_char)
... print('nMy War and Peace:n' + result)
现在所有组件都准备就绪,让我们开始训练模型:
>>> model.fit(X, Y, batch_size=batch_size, verbose=1, epochs=n_epoch,
callbacks=[ResultChecker(model, 10, 200), checkpoint, early_stop])
生成器为每个10周期写入200字符。 我们可以看到每个周期的进度条,其详细设置为1(0是静默模式,2没有进度条)。
以下是周期1,11,51和101的结果:
Epoch 1:
Epoch 1/300
8000/31962 [======>.......................] - ETA: 51s - loss: 2.8891 31962/31962 [==============================] - 67s 2ms/step - loss: 2.1955 My War and Peace:
5 the count of the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the stord and the
Epoch 00001: loss improved from inf to 2.19552, saving model to weights/weights_epoch_001_loss_2.19552.hdf5
Epoch 11:
Epoch 11/300
100/31962 [..............................] - ETA: 1:26 - loss: 1.2321 31962/31962 [==============================] - 66s 2ms/step - loss: 1.2493 My War and Peace:
?" said the countess was a strange the same time the countess was already been and said that he was so strange to the countess was already been and the same time the countess was already been and said Epoch 00011: loss improved from 1.26144 to 1.24933, saving model to weights/weights_epoch_011_loss_1.2493.hdf5
Epoch 51:
Epoch 51/300
31962/31962 [==============================] - 66s 2ms/step - loss: 1.1562 My War and Peace:
!!CDP!E.agrave!! to see him and the same thing is the same thing to him and the same thing the same thing is the same thing to him and the same thing the same thing is the same thing to him and the same thing the sam Epoch 00051: loss did not improve from 1.14279
Epoch 101:
Epoch 101/300
31962/31962 [==============================] - 67s 2ms/step - loss: 1.1736 My War and Peace:
= the same thing is to be a soldier in the same way to the soldiers and the same thing is the same to me to see him and the same thing is the same to me to see him and the same thing is the same to me Epoch 00101: loss did not improve from 1.11891
训练在周期203提前停止:
Epoch 00203: loss did not improve from 1.10864
Epoch 00203: early stopping
在 Tesla K80 GPU 上,每个周期大约需要 1 分钟。 经过约 3.5 小时的训练,损失从2.19552减少到1.10864。
它在周期151生成以下文本:
which was a strange and serious expression of his face and shouting and said that the countess was standing beside him.
"what a battle is a strange and serious and strange and so that the countess was
我们的《战争与和平》读起来不错,尽管有点荒谬。 通过调整此原始 RNN 模型的超参数,我们可以做得更好吗? 绝对可以,但这是不值得的。 正如我们前面提到的,训练一个普通的 RNN 模型来解决需要学习长期依赖关系的问题非常困难-距离较远的步骤之间的依赖关系通常对于预测至关重要。 但是,由于梯度消失的问题,原始 RNN 仅能够捕获序列中几个早期步骤之间的时间依赖性。 LSTM 和 GRU 之类的架构是专门为解决此问题而设计的。 我们将在以下两个部分中说明它们如何随着时间的推移在内存中维护信息。
LSTM RNN
LSTM 的架构的神奇之处在于:在普通循环单元的顶部,增加了一个存储单元和三个信息门以处理长期依赖关系。 LSTM 的循环单元如下所示(我们还提出了一种原始用于比较):

在上图中从左到右,主要组成部分说明如下:
cₜ是存储单元,它从输入序列的最开始就存储上下文。f表示遗忘门,它控制来自前一存储状态c[t-1]的多少信息可以向前传递。 与遗忘门相关的权重包括W[f],它与先前的隐藏状态S[t-1]连接,和u[f],它与当前输入xₜ连接。i代表输入门,它确定当前输入可以通过多少信息。 权重Wᵢ和Uᵢ分别将其与先前的隐藏状态和当前输入相连。- tanh 只是隐藏状态的激活函数,并且基于当前输入
xₜ和先前的隐藏状态s[t-1]及其相应的权重W[c]和U[c]进行计算。 它与原始 RNN 中的a完全相同。 o用作输出门,它定义了将内部存储器中的多少信息用作整个循环单元的输出。 同样,W[o]和U[o]是关联的权重。
因此,这些组件之间的关系可以概括如下:
- 在时间步
t处的遗忘门f的输出被计算为f = sigmoid(U[f] x[t] + W[f] s[t-1])。 - 将在时间步
t处输入门i的输出计算为i = sigmoid(U[i] x[t] + W[i] s[t-1])。 - 将在时间步
t处的 tanh 激活c'的输出计算为c' = sigmoid(U[c] x[t] + W[c] s[t-1])。 - 在时间步
t处的输出门o的输出被计算为o = sigmoid(U[o] x[t] + W[o] s[t-1])。 - 在时间步
t处的存储单元cₜ由c[t] = f .* c[t-1] + i .* c'更新,其中.*表示逐元素乘法。 值得注意的是,f和i中的 Sigmoid 函数将其输出转换为从0到1的范围,从而控制分别通过的先前存储器c[t - 1]和当前存储器输入c'的数据比例。 - 最后,在时间步
t的隐藏状态sₜ被更新为s[t] = o .* c[t]。 同样,o确定用作整个单元输出的更新存储单元cₜ的比例。
使用随时间的反向传播训练所有四组权重U和W,这与原始 RNN 相同。 通过学习三个信息门的权重,网络显式地对长期依赖关系进行建模。 接下来,我们将利用 LSTM 架构并发明更强大的文本生成器。
用于文本生成的 LSTM RNN
在基于 LSTM 的文本生成器中,我们将序列长度增加到 160 个字符,因为它可以更好地处理长序列。 记住要用新的seq_length = 160 重新生成训练集X和Y。
为了轻松地将该模型的表现与以前的原始模型进行比较,我们将保留一个相似的结构-两个循环层,两个循环层均包含800单元,0.4的丢弃率和 tanh (默认情况下)作为激活函数:
>>> from keras.layers.recurrent import LSTM
>>> batch_size = 100
>>> n_layer = 2
>>> hidden_units = 800
>>> n_epoch= 300
>>> dropout = 0.4
现在,创建并编译网络:
>>> model = Sequential() >>> model.add(LSTM(hidden_units, input_shape=(None, n_vocab),
return_sequences=True)) >>> model.add(Dropout(dropout)) >>> for i in range(n_layer - 1): ... model.add(LSTM(hidden_units, return_sequences=True)) ... model.add(Dropout(dropout)) >>> model.add(TimeDistributed(Dense(n_vocab))) >>> model.add(Activation('softmax'))
优化器RMSprop的学习速度为0.001:
>>> optimizer = optimizers.RMSprop(lr=0.001, rho=0.9,
epsilon=1e-08, decay=0.0) >>> model.compile(loss="categorical_crossentropy", optimizer=optimizer)
让我们总结一下我们刚刚组装的 LSTM 模型:
>>> print(model.summary())
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_1 (LSTM) (None, None, 800) 2745600
_________________________________________________________________
dropout_1 (Dropout) (None, None, 800) 0
_________________________________________________________________
lstm_2 (LSTM) (None, None, 800) 5123200
_________________________________________________________________
dropout_2 (Dropout) (None, None, 800) 0
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 57) 45657
_________________________________________________________________
activation_1 (Activation) (None, None, 57) 0
=================================================================
Total params: 7,914,457
Trainable params: 7,914,457
Non-trainable params: 0
_________________________________________________________________
有 800 万个参数需要训练,几乎是原始模型中训练参数的四倍。 让我们开始训练他们:
>>> model.fit(X, Y, batch_size=batch_size, verbose=1, epochs=n_epoch,
callbacks=[ResultChecker(model, 10, 500), checkpoint, early_stop])
生成器为每个10周期写入500个字符长的文本。
以下是周期151,201和251的结果:
Epoch 151:
Epoch 151/300
19976/19976 [==============================] - 250s 12ms/step - loss: 0.7300
My War and Peace:
ing to the countess. "i have nothing to do with him and i have nothing to do with the general," said prince andrew.
"i am so sorry for the princess, i am so since he will not be able to say anything. i saw him long ago. i am so sincerely that i am not to
blame for it. i am sure that something is so much talk about the emperor alexander's personal attention."
"why do you say that?" and she recognized in his son's presence.
"well, and how is she?" asked pierre.
"the prince is very good to make
Epoch 00151: loss improved from 0.73175 to 0.73003, saving model to weights/weights_epoch_151_loss_0.7300.hdf5
Epoch 201:
Epoch 201/300
19976/19976 [==============================] - 248s 12ms/step - loss: 0.6794 My War and Peace:
was all the same to him. he received a story proved that the count had not yet seen the countess and the other and asked to be able to start a tender man than the world. she was not a family affair and was at the same time as in the same way. a few minutes later the count had been at home with his smile and said: "i am so glad! well, what does that mean? you will see that you are always the same." "you know i have not come to the conclusion that i should like to
send my private result. the prin Epoch 00201: loss improved from 0.68000 to 0.67937, saving model to weights/weights_epoch_151_loss_0.6793.hdf5
Epoch 251:
Epoch 251/300
19976/19976 [==============================] - 249s 12ms/step - loss: 0.6369 My War and Peace:
nd the countess was sitting in a single look on
her face. "why should you be ashamed?" "why do you say that?" said princess mary. "why didn't you say a word of this?" said prince andrew with a smile. "you would not like that for my sake, prince vasili's son, have you seen the rest of the two?" "well, i am suffering," replied the princess with a sigh. "well, what a delightful norse?" he shouted. the convoy and driving away the flames of the battalions of the first
day of the orthodox russian Epoch 00251: loss improved from 0.63715 to 0.63689, saving model to weights/weights_epoch_251_loss_0.6368.hdf5
最后,在周期300,训练因0.6001损失而停止。
每个周期大约需要四到五分钟,才能在 Tesla K80 GPU 上完成。 借助大约 22 个小时的训练,文本生成器借助 LSTM 架构能够编写出更逼真有趣的《战争与和平》脚本。
此外,用于字符生成的 LSTM RNN 不限于文本。 他们可以从任何字符数据中学习,例如源代码,HTML,LaTex,并希望自动编写软件程序,网页和科学论文。
GRU RNN
在 LSTM 之后的十多年中,发明了具有门控机制的替代架构 GRU。 GRU 和 LSTM 的表现相当,在某些情况下一个要优于另一个。 但是,GRU 只有两个信息门,这比 LSTM 稍微复杂一些。 GRU 的循环单元描述如下:

上图中从左到右的关键组件说明如下:
r表示复位门,它控制要忘记前一存储器s[t-1]的多少信息。 给定连接到先前隐藏状态s[t-1]的权重W[r]和连接到当前输入xₜ的U[r],复位门r的输出在时间步t计算为r = sigmoid(U[r] x[t] + W[r] s[t-1])。p代表更新门,它确定可以从前一个内存中传递多少信息。 将权重W[p]和U[p]分别连接到先前的存储状态和当前输入,将时间t的更新门p的输出计算为p = sigmoid(U[p] x[t] + W[p] s[t-1])。- tanh 是隐藏状态的激活函数,并基于当前输入
xₜ和先前存储状态的重置进行计算。 给定它们的相应权重W[c]和U[c],将当前存储器c'在时间步t的输出计算为c = tanh(U[c] x[t] + W[c] (r .* s[t-1]))。 - 最后,在时间步
t的隐藏状态sₜ被更新为s[t] = (1 - p) .* c' + p .* s[t-1]。 同样,p决定用于更新当前内存的先前内存的比例–越接近1,则保留的内存就越多; 距离0越近,发生的当前存储器越多。 - 有趣的是,如果
p是全零向量,而r是全一向量(例如我们没有明确保留任何先前的内存),则该网络只是一个普通的 RNN。
总体而言,GRU 与 LSTM 非常相似。 它们都使用光栅机制进行长期建模,并且与门相关的参数通过 BPTT 进行训练。 但是,有一些区别值得注意:
- LSTM 中有三个信息门,而 GRU 中只有两个。
- GRU 中的更新门负责输入门和 LSTM 中的遗忘门。
- GRU 中的重置门直接应用于先前的隐藏状态。
- LSTM 显式地对存储单元
cₜ进行建模,而 GRU 则不。 - 附加的非线性将应用于 LSTM 中更新的隐藏状态。
- LSTM 于 1997 年推出,近年来已得到研究和广泛使用。 GRU 于 2014 年发明,至今尚未得到充分的探索。 这就是为什么 LSTM 比 GRU 更受欢迎的原因,即使不能保证一个 LSTM 胜过另一个。
- 通常认为,与 GSTM 相比,训练 GRU RNN 相对更快并且需要更少的数据,因为 GRU RNN 具有较少的参数。 因此,有人认为 GRU 在训练量较小的情况下效果更好。
尽管还有许多谜团,我们还是将 GRU RNN 应用于十亿(或万亿)美元的问题:股价预测。
用于股价预测的 GRU RNN
预测股票会使许多人感兴趣。 多年以来,已经开发出了大量使用机器学习技术预测股票价格的方法。 例如,在《Python 机器学习示例的》的“第 7 章”中,线性回归,随机森林和支持向量机被用于预测股票价格。 在像这样的传统机器学习解决方案中,特征工程可能是最费力的阶段。 这是手动创建特定于域的特征或信号的过程,这些特征或信号对于定向预测比原始输入更为重要。 典型的发明特征包括x天移动平均线,一段时间内的波动率和x天回报率。 相反,基于 RNN 的深度学习解决方案不涉及以手工为特色的手工制作,而是自己找出及时或顺序的关系。 我们将通过使用 GRU RNN 预测道琼斯工业平均指数(DJIA)来展示循环架构的强大功能。
尽管我们强调了深度学习的优势,但我们并未断言深度学习方法优于传统的机器学习方法。 在机器学习中,没有一种适合所有的。
DJIA 由 30 只大型和重要股票(例如 Apple,IBM,GE 和 Goldman Sachs)组成,是全球投资者最常关注的市场指数之一。 它代表了整个美国市场价值的四分之一,这使该项目更加令人兴奋。
我们可以在这个页面上查看其历史每日数据。 其中一些数据的屏幕截图如下:

五个值说明了交易日内股票在一段时间内的走势:开盘价和收盘价,即交易日的起始价和最终价,低点和高位,即股票交易价格范围和交易量,即在一个交易日交易的股票总数。 例如,我们将重点关注使用历史收盘价来预测未来收盘价。 但是,合并其他四个指标也是可行的。
让我们从数据获取和探索开始。 首先,我们将数据从 2001-01-01 下载到 2017-12-31:在同一网页上,将时间段更改为 2001 年 1 月 1 日至 2017 年 12 月 31 日。单击“应用”按钮,最后单击“下载数据”。 然后,我们可以加载并查看数据:
>>> import numpy as np >>> import matplotlib.pyplot as plt >>> import pandas as pd >>> raw_data = pd.read_csv('^DJI.csv') >>> raw_data.head()
Date Open High Low Close 0 2001-01-02 10790.919922 10797.019531 10585.360352 10646.150391 1 2001-01-03 10637.419922 11019.049805 10581.089844 10945.750000 2 2001-01-04 10944.940430 11028.000000 10888.419922 10912.410156 3 2001-01-05 10912.809570 10919.419922 10627.750000 10662.009766 4 2001-01-08 10658.730469 10700.849609 10516.019531 10621.349609
Adj Close Volume 0 10646.150391 253300000 1 10945.750000 420720000 2 10912.410156 382800000 3 10662.009766 272650000 4 10621.349609 225780000
使用以下代码行绘制收盘价数据:
>>> data = raw_data.Close.values >>> len(data) 4276 >>> plt.plot(data) >>> plt.xlabel('Time period') >>> plt.ylabel('Price') >>> plt.show()
前面的代码将创建以下图形:

一年平均有 252 个交易日。 这就是为什么在 17 年中仅选择了 4,276 个数据点的原因。
接下来,我们需要从原始时间序列构造顺序输入,以便提供 RNN 模型,这与我们在文本生成中所做的类似。 回想一下,在“多对一”架构中,该模型采用一个序列,并经过序列中的所有时间步长后产生一个输出。 在我们的案例中,我们可以将过去T天的价格序列提供给 RNN 模型,并输出第二天的价格。
将价格时间序列表示为x₁,x₂,...,xₙ(N = 4276),并以T = 5为例。 通过这样做,我们可以创建训练样本,如下所示:
| 输入 | 输出 |
|---|---|
{x[1], x[2], x[3], x[4], x[5]} |
x₆ |
{x[2], x[3], x[4], x[5], x[6]} |
x₇ |
{x[3], x[4], x[5], x[6], x[7]} |
x₈ |
| ...... | ... |
{x[n - 1], x[n - 2], x[n - 3], x[n - 4], x[n - 5]} |
xₙ |
在这里,我们通过回顾前 5 个交易日(一周)来预测第二天的价格。 我们还在网络中对其进行了描述:

因此,我们实现了序列生成函数:
>>> def generate_seq(data, window_size): ... """ ... Transform input series into input sequences and outputs based
on a specified window size ... @param data: input series ... @param window_size: int ... @return: numpy array of input sequences, numpy array of outputs ... """ ... X, Y = [], [] ... for i in range(window_size, len(data)): ... X.append(data[i - window_size:i]) ... Y.append(data[i]) ... return np.array(X),np.array(Y)
然后,我们以T = 10构造输入和输出数据集(回溯 2 周):
>>> window_size = 10 >>> X, Y = generate_seq(data, window_size) >>> X.shape (4266, 10) >>> Y.shape (4266,)
接下来,我们将数据分为 70% 的训练和 30% 的测试:
>>> train_ratio = 0.7
>>> train_n = int(len(Y) * train_ratio)
>>> X_train = X[:train_n]
>>> Y_train = Y[:train_n]
>>> X_test = X[train_n:]
>>> Y_test = Y[train_n:]
我们现在可以开始对训练数据进行建模吗? 当然不需要-需要数据缩放或预处理。 从上图可以看出,测试数据与训练数据不成比例,更不用说将来的数据了。 回归模型无法预测超出范围的值。 为了解决这个问题,我们通常使用最大最小缩放x_scaled = (x - x_min) / (x_max / x_min)将数据缩放到给定范围,例如 0 到 1。 但是,没有可靠的方法来预测股票的x_max(或x_min。 这与已知最小值和最大值(例如,图像预测中的 0 和 255)的情况不同。 为了解决这个问题,我们将每个窗口内的价格标准化。 我们只需将时间窗口中的每个价格除以最近的已知价格即可。 再次使用前面的T = 5示例,我们可以如下预处理训练样本:
| 输入 | 输出 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| ...... | ... |
![]() |
![]() |
我们基本上将绝对值转换为相对值。 预处理函数实现如下:
>>> def scale(X, Y): ... """ ... Scaling the prices within each window ... @param X: input series ... @param Y: outputs ... @return: scaled input series and outputs ... """ ... X_processed, Y_processed = np.copy(X), np.copy(Y) ... for i in range(len(X)): ... x = X[i, -1] ... X_processed[i] /= x ... Y_processed[i] /= x ... return X_processed, Y_processed
扩展训练和测试数据:
>>> X_train_scaled, Y_train_scaled = scale(X_train, Y_train) >>> X_test_scaled, Y_test_scaled = scale(X_test, Y_test)
终于到了构建 GRU RNN 模型的时候了:
>>> from keras.models import Sequential >>> from keras.layers import Dense, GRU >>> from keras import optimizers >>> model = Sequential() >>> model.add(GRU(256, input_shape=(window_size, 1))) >>> model.add(Dense(1))
在这里,由于我们只有 2986 个训练样本,因此我们正在设计一个相对简单的模型,该模型具有一个 256 个单元的循环层。 对于优化器,使用 RMSprop,学习率为 0.006,以最小化均方误差:
>>> optimizer = optimizers.RMSprop(lr=0.0006, rho=0.9,
epsilon=1e-08, decay=0.0) >>> model.compile(loss='mean_squared_error', optimizer=optimizer)
除了早期停止和模型检查点之外,我们还使用 TensorBoard 作为回调函数。 TensorBoard 是 TensorFlow 的表现可视化工具,可提供用于训练和验证指标的动态图:
>>> from keras.callbacks import TensorBoard, EarlyStopping, ModelCheckpoint >>> tensorboard = TensorBoard(log_dir='./logs/run1/', write_graph=True, write_images=False)
验证损失是在提前停止和模型检查点中使用的度量标准:
>>> early_stop = EarlyStopping(monitor='val_loss', min_delta=0, patience=100, verbose=1, mode='min') >>> model_file = "weights/best_model.hdf5" >>> checkpoint = ModelCheckpoint(model_file, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
与往常一样,重塑输入数据以提供 Keras RNN 模型:
>>> X_train_reshaped = X_train_scaled.reshape( (X_train_scaled.shape[0], X_train_scaled.shape[1], 1)) >>> X_test_reshaped = X_test_scaled.reshape( (X_test_scaled.shape[0], X_test_scaled.shape[1], 1))
该模型适合训练集并通过测试集进行了验证,最大历时为300,批量为100:
>>> model.fit(X_train_reshaped, Y_train_scaled, validation_data=
(X_test_reshaped, Y_test_scaled), epochs=300, batch_size=100,
verbose=1, callbacks=[tensorboard, early_stop, checkpoint])
以下是周期1,11,52和99的结果:
Epoch 1:
Epoch 1/300 2986/2986 [==============================] - 1s 386us/step - loss: 0.0641 - val_loss: 0.0038 Epoch 00001: val_loss improved from inf to 0.00383, saving model to weights/best_model.hdf5
Epoch 11:
Epoch 11/300 2986/2986 [==============================] - 1s 353us/step - loss: 0.0014 - val_loss: 9.0839e-04 Epoch 00011: val_loss improved from 0.00128 to 0.00091, saving model to weights/best_model.hdf5
Epoch 52:
Epoch 52/300 2986/2986 [==============================] - 1s 415us/step - loss: 4.2122e-04 - val_loss: 6.0911e-05 Epoch 00052: val_loss improved from 0.00010 to 0.00006, saving model to weights/best_model.hdf5
Epoch 99:
Epoch 99/300 2986/2986 [==============================] - 1s 391us/step - loss: 2.1644e-04 - val_loss: 5.2291e-05 Epoch 00099: val_loss improved from 0.00005 to 0.00005, saving model to weights/best_model.hdf5
每个周期大约需要 1 秒才能在 CPU(Core i7)上完成。 训练在周期 242 停止,因为验证损失不再减少:
Epoch 241/300 2986/2986 [==============================] - 1s 370us/step - loss: 1.9895e-04 - val_loss: 7.5277e-05 Epoch 00241: val_loss did not improve Epoch 242/300 2986/2986 [==============================] - 1s 368us/step - loss: 1.9372e-04 - val_loss: 9.1636e-05 Epoch 00242: val_loss did not improve Epoch 00242: early stopping
同时,我们可以在终端中输入以下命令行来检出 TensorBoard:
tensorboard --logdir=logs
它返回以下输出:
Starting TensorBoard b'41' on port 6006 (You can navigate to http://192.168.0.12:6006)
如果转到http://192.168.0.12:6006,您将能够查看一段时间内的训练损失和验证损失。
平滑为 0(无指数平滑)时的训练损失:

平滑为 0(无指数平滑)时的验证损失:

学习进展顺利,两种损失都随着时间而减少。 通过将其与基本事实进行比较,我们可以进一步将预测可视化。 首先,加载我们刚刚获得的最佳模型,并为训练数据和测试数据计算预测:
>>> from keras.models import load_model >>> model = load_model(model_file) >>> pred_train_scaled = model.predict(X_train_reshaped) >>> pred_test_scaled = model.predict(X_test_reshaped)
我们还需要将按比例缩放的预测转换回其原始比例。 我们可以编写一个函数来简化此操作:
>>> def reverse_scale(X, Y_scaled): ... """ ... Convert the scaled outputs to the original scale ... @param X: original input series ... @param Y_scaled: scaled outputs ... @return: outputs in original scale ... """ ... Y_original = np.copy(Y_scaled) ... for i in range(len(X)): ... x = X[i, -1] ... Y_original[i] *= x ... return Y_original
将反向缩放应用于缩放的预测:
>>> pred_train = reverse_scale(X_train, pred_train_scaled) >>> pred_test = reverse_scale(X_test, pred_test_scaled)
最后,绘制预测以及基本事实:
>>> plt.plot(Y) >>> plt.plot(np.concatenate([pred_train, pred_test])) >>> plt.xlabel('Time period') >>> plt.ylabel('Price') >>> plt.legend(['original series','prediction'],loc='center left') >>> plt.show()
前面的代码将创建以下图形:

结果图表明预测是非常准确的。 为了进行比较,我们还使用了仅经过 10 个周期训练的模型,得出了效果不佳的结果:

应该注意的是,该项目主要是为了演示 GRU RNN 的应用,而不是用于实际的股票交易。 实际上,它要复杂得多,应考虑许多外部和内部因素,例如基本面,技术模式,利率,波动性,周期,新闻和情感。
双向 RNN
到目前为止,在 RNN 架构中,输入序列的信息是从过去到当前状态再到未来的一个方向学习的。 它限制了当前状态以利用将来的输入信息。 让我们看一个缺失单词生成的简单示例:
He said, "Machine __ combines computer science and statistics."
对于仅学习前三个单词的 RNN 模型来说,很难生成适合整个句子的下一个单词。 但是,如果给出了剩余单词,则该模型将更好地捕获上下文,并且更有可能预测下一个单词,即learning。 为了克服单向 RNN 的局限性,引入了双向 RNN(BRNN)。
在 BRNN 中,隐藏层由两个独立的循环层组成。 这两层是相反的方向:一层为正时间方向,也称为正向,其中输入信息从过去流向当前状态。 另一个为负时间方向,也称为反向,其中从将来到当前状态处理输入信息。 下图描述了 BRNN 的一般结构:

在此,f ->和f <-分别表示正向和反向循环层。 它们连接在一起,形成隐藏层f,并保留来自过去和将来状态的信息。
当需要并提供完整的上下文(包括过去和将来的信息)时,BRNN 特别有用。 例如,在词性标记,实体识别或手写识别中,可以通过了解当前单词或字母之后的单词或字母来提高性能。 其他出色的用例包括语音识别,机器翻译和图像字幕。
接下来,我们将 BRNN 和 LSTM 结合起来用于情感分类。 我们将看到捕获移动评论的完整上下文信息是否有助于增强其情感极性。
用于情感分类的双向 RNN
Keras 包含来自 IMDb 的 50,000 条电影评论的数据集,并用情感极性(1为正,0为负)标记。 评论已经过预处理,每个词都由字典中的相应索引表示。 字典中的单词将根据整个数据集中的频率进行排序。 例如,编码为4的单词是数据中第 4 个最常见的单词。 您可以猜测1代表the,2代表and,并且最高索引用于停用词。
可以通过以下代码获得单词索引字典:
>>> from keras.datasets import imdb >>> word_to_id = imdb.get_word_index()
我们可以使用load_data函数加载数据,该数据返回两个元组,即训练数据集和测试数据集:
>>> max_words = 5000 >>> (x_train, y_train), (x_test, y_test) =
imdb.load_data(num_words=max_words, skip_top=10, seed=42)
在这里,我们仅考虑前 5,000 个最常用的词,但排除了前 10 个最常用的词。 种子用于复制目的:
>>> print(len(y_train), 'training samples') 25000 training samples >>> print(len(y_test), 'testing samples') 25000 testing samples
您会在某些输入样本中找到许多 2。 这是因为2用于表示由于我们应用了频率过滤器而被切掉的单词,包括前 10 个最常用的单词和第 5,000 个最常用的单词。 实际单词从索引3开始。
您可能会注意到输入样本的长度不同; 例如,第一个训练样本中有467个单词,第二个训练样本中有138个单词。 但是,训练 RNN 模型需要输入相同长度的样本。 我们需要将序列填充到相同的长度:短样本的末尾用 0 填充,而长样本则被截断。 我们使用pad_sequences函数,将序列长度指定为200:
>>> from keras.preprocessing import sequence >>> maxlen = 200 >>> x_train = sequence.pad_sequences(x_train, maxlen=maxlen) >>> x_test = sequence.pad_sequences(x_test, maxlen=maxlen) >>> print('x_train shape:', x_train.shape) x_train shape: (25000, 200) >>> print('x_test shape:', x_test.shape) x_test shape: (25000, 200)
现在,我们有 200 个单词的输入序列,每个单词由0至4999的整数表示。 我们可以进行传统的单热编码,但是所生成的 3,000 维稀疏输出将使训练相应的 RNN 模型过慢。 取而代之的是,我们执行单词嵌入以将单词索引转换为较低维的密集向量。 在 Keras 中,我们将嵌入层用作模型的第一层:
>>> from keras.models import Sequential >>> from keras.layers import Embedding >>> model = Sequential() >>> model.add(Embedding(max_words, 128, input_length=maxlen))
嵌入层将5000(max_words)值的索引输入转换为 128 维向量。 接下来,我们将双向 RNN 与 LSTM 相结合:
>>> from keras.layers import Dense, Embedding, LSTM, Bidirectional >>> model.add(Bidirectional(LSTM(128, dropout=0.2, recurrent_dropout=0.2)))
我们仅在 LSTM 层上应用Bidirectional包装器。 LSTM 层具有 128 个隐藏的单元,其中输入单元的丢弃率为 20%,循环连接的丢弃率为 20%。
最后一层生成逻辑输出:
>>> model.add(Dense(1, activation='sigmoid'))
对于优化器,RMSprop与0.001的学习率一起使用,以最小化二分类的交叉熵:
>>> optimizer = optimizers.RMSprop(0.001) >>> model.compile(optimizer=optimizer,loss='binary_crossentropy', metrics=['accuracy'])
最后,我们将测试集作为验证数据训练模型,并根据验证损失提前停止训练:
>>> from keras.callbacks import EarlyStopping >>> early_stop = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1, mode='min') >>> model.fit(x_train, y_train, batch_size=32, epochs=100, validation_data=[x_test, y_test], callbacks=[early_stop])
让我们看一下周期1,5,8和15的日志:
Epoch 1:
Train on 25000 samples, validate on 25000 samples
Epoch 1/100
5504/25000 [=====>........................] - ETA: 15:04 - loss: 0.6111 - acc: 0.6672 25000/25000 [==============================] - 1411s 56ms/step - loss: 0.4730 - acc: 0.7750 - val_loss: 0.3765 - val_acc: 0.8436
Epoch 5:
Epoch 5/100
5088/25000 [=====>........................] - ETA: 15:22 - loss: 0.2395 - acc: 0.9025 25000/25000 [==============================] - 1407s 56ms/step - loss: 0.2367 - acc: 0.9070 - val_loss: 0.2869 - val_acc: 0.8848 Epoch 00005: val_loss did not improve from 0.27994
Epoch 8:
Epoch 8/100
5088/25000 [=====>........................] - ETA: 15:16 - loss: 0.1760 - acc: 0.9347 25000/25000 [==============================] - 1404s 56ms/step - loss: 0.1815 - acc: 0.9314 - val_loss: 0.2703 - val_acc: 0.8960
Epoch 15:
Epoch 15/100
5408/25000 [=====>........................] - ETA: 15:08 - loss: 0.0936 - acc: 0.9680 25000/25000 [==============================] - 1413s 57ms/step - loss: 0.0975 - acc: 0.9656 - val_loss: 0.3588 - val_acc: 0.8816 Epoch 00015: val_loss did not improve from 0.27034
训练在周期18停止,因为提前停止触发:
Epoch 00018: val_loss did not improve from 0.27034
Epoch 00018: early stopping
使用 BRNN 和 LSTM 可以达到 89.6% 的测试精度。
总结
我们刚刚完成了关于 DL 架构-RNN 的学习旅程的重要部分! 在本章中,我们更加熟悉了 RNN 及其变体。 我们从 RNN 是什么,RNN 的发展路径以及它们如何成为顺序建模的最新解决方案入手。 我们还研究了四种 RNN 架构,并按输入和输出数据的形式进行了分类,并提供了一些工业示例。
接下来,我们讨论按循环层分类的各种架构,包括原始 RNN,LSTM,GRU 和双向 RNN。 首先,我们应用了原始架构来编写我们自己的《战争与和平》 ,尽管有点荒谬。 我们通过使用 LSTM 架构 RNN 生成了更好的版本。 股票价格预测中采用了另一种内存增强型架构 GRU。
最后,除了过去的信息外,我们还引入了双向架构,该模型允许模型保留序列的过去和将来上下文中的信息。 我们还使用双向 RNN 和 LSTM 进行电影评论情感分类。 在下一章中,我们将探讨 DL 模型的另一项伟大发明:生成对抗网络。
七、生成对抗网络
在本章中,我们将解释最有趣的深度学习模型之一,即生成对抗网络(GAN)。 我们将首先回顾 GAN 是什么以及它们的用途。 在简要介绍了 GAN 模型的演化路径之后,我们将说明各种 GAN 架构以及图像生成示例。
想象一下,您正在模仿一个最初并不了解的艺术品(例如梵高的《星空》)。 您可以随意参加多次。 每次您提交作品时,评委都会向您反馈真实艺术品的外观以及复制品的接近程度。 在最初的几次尝试中,由于您对原始作品的了解非常有限,因此您的作品得分不高。 经过几番尝试,由于法官提供了有用的提示,您的作品越来越接近真实的艺术品。 您会不断尝试和改进,并将法官的反馈纳入您的尝试中,而在最近的几次尝试中,您的工作看起来与原始工作非常接近。 希望您最终能赢得比赛。
GAN 在合成图像,声波,视频和任何其他信号方面几乎具有相同的作用,我们将在本章中对此进行深入探讨。
在本章中,我们将介绍以下主题:
- 什么是 GAN?
- 生成模型
- 对抗训练
- GAN 的演进路径
- 原始 GAN 架构
- 实现原始 GAN
- 生成图像
- 深度卷积 GAN 架构
- 实现深度卷积 GAN
- 条件 GAN 架构
- 实现条件 GAN
- 信息最大化 GAN 架构
- 实现信息最大化 GAN
什么是 GAN?
GAN 可能是深度神经网络最有趣的类型之一。 自从 Goodfellow 等人首次引入 GAN 以来。 2014 年,围绕它们开展的研究项目和应用越来越多,其中一些确实很有趣。 以下是我们挑选的有趣的东西:
- 图像生成,例如猫图片,假名人面孔,甚至是现代艺术品
- 音频或视频合成,例如 DeepMind 的 WaveNets,它们能够为天文图像生成人类语音和 3D 重建
- 时间序列生成,例如用于股票市场预测的医学数据和高频数据
- 统计推断,例如通过分析一堆图片来设计服装的亚马逊算法
那么,为什么 GAN 如此强大? 对于 GAN,最好先讨论生成模型,因为 GAN 基本上是生成模型。
生成模型
机器学习中有两种主要类型的模型,即生成模型和判别模型。 顾名思义,判别模型试图在两个(或多个)类之间区分数据。 例如,我们在“第 4 章”,“CNN 架构”中讨论过的 CNN 模型,学会告诉我们一个图像是猫还是狗,给定其中一个图像, 以及 “第 6 章”,“循环神经网络”中的 RNN 模型经过训练,可以输出给定段落的正面或负面情感。 判别模型着重于根据数据的特征预测数据类别。 相反,生成模型不尝试将特征映射到类,而是在给定特定类的情况下生成特征。 例如,训练高斯混合模型(GMM)以生成适合训练集分布的新数据。 生成模型对给定单个类的特征分布进行建模。 也就是说,可以使用生成模型对数据进行分类,例如朴素贝叶斯和玻尔兹曼机,我们在“第 3 章”,“受限玻尔兹曼机和自编码器”中进行了讨论。 但是,他们的首要任务是弄清楚某些特征的可能性,而不是识别标签。 然后将学习到的特征分布用于分类。 如果仍然感到困惑,这是区分生成模型和判别模型的简单方法:
- 判别模型对寻找边界或规则来分离数据感兴趣
- 生成模型侧重于对数据分布进行建模
GAN 模型由两个网络组成。 一个称为生成器的网络负责生成新的数据样本,从而生成 GAN 生成模型。 另一个网络称为判别器,对生成的数据进行真实性评估。 具体而言,它确定单独生成的样本是否属于真实训练数据集。 再次,GAN 仍然是生成模型,因为它们专注于生成特别感兴趣的数据分布,并且添加了判别器以提供反馈以更好地生成数据。 让我们看看它是如何工作的。
对抗 - 以对抗方式进行训练
如果我们还记得梵高(Van Gogh)的例子,那么 GAN 模型就在做类似的事情。 我们模仿艺术品,而 GAN 中的生成器生成候选对象,例如感兴趣的特定数据分布; 法官评估我们的副本,而 GAN 中的判别器则通过从真实数据分布中区分生成的候选对象和实例来评估生成的候选对象。
GAN 中的生成器和判别器以对抗性方式进行训练,也就是说,它们在零和框架中相互竞争,以便生成模拟真实数据分布的数据。 就像在美术比赛中一样,我们不断改进复制品,以期获得法官的高分,并且法官不断评估我们的工作并提供反馈。 GAN 中的生成器旨在生成被视为来自真实数据分布的合成实例,即使它们是伪造的,而判别器的目标是识别单个实例是否是伪造的(已合成) 或真实的。 从优化的角度来看,生成器的训练目标是增加判别器的错误-判别器犯的错误越多,生成器的表现就越好。 判别器的目的是减少其误差,这是显而易见的。 在每次迭代中,两个网络都使用梯度下降来实现其目标,这并不是什么新鲜事。 有趣的是,每个网络都在试图击败另一个网络,即生成器试图欺骗判别器,而判别器则试图不被欺骗。 最终,来自生成器的合成数据(例如图像,音频,视频,时间序列)(希望)能够欺骗最复杂的判别器,类似于艺术品复制品竞赛,我们能够从最严格的标准中获得最高分但最有帮助的裁判。
实际上,生成器从具有高斯分布的多元高斯分布的预定义分布中抽取随机样本作为最受欢迎的输入,并生成看起来好像可能来自目标分布的数据。 这与复制品竞赛非常相似,在复制品竞赛中,我们最初对艺术品一无所知,并一直朝着像样的复制品努力。
以图像生成为例,下图可以表示 GAN 模型:

GAN 采取的步骤如下:
-
生成器网络从高斯分布中获取随机样本并输出图像。
-
这些生成的图像然后被馈送到判别器网络。
-
判别器网络接收生成的图像和从实际数据集中获取的图像。
-
判别器输出概率,其中上限 1 表示输入图像被认为是真实的,下限 0 表示输入图像被认为是假的。
-
生成器的损失(成本函数)是基于判别器认为真实的伪图像的交叉熵来计算的。
-
判别器的损失(成本函数)是根据被认为是伪造的伪图像的交叉熵加上被认为是真实的真实图像的交叉熵来计算的。
-
对于每个周期,两个网络都经过优化,分别将其各自的损失降至最低。
-
在某个时候,当融合判别器将融合生成器生成的图像视为真实图像时,该模型将得到很好的训练。
-
最终,训练有素的生成器会生成图像作为最终输出,从而模仿真实图像的输入。
GAN 的演进路径
对抗训练的思想可以追溯到 1990 年代早期的工作,例如 Schmidhuber 的《通过可预测性最小化学习阶乘代码》。 2013 年,在《通过受控相互作用学习动物行为的协同进化方法》中提出了无模型推断的对抗模型。 2014 年,Goodfellow 等人在《生成对抗网络》中首次引入了 GAN。
Li 等人(提出动物行为推断的同一作者)在《图灵学习:一种无度量的行为推断方法及其在群体中的应用》中于 2016 年提出了图灵学习 一词。 图灵学习与图灵测试相关,并且是 GAN 的概括,如《概括 GAN:图灵的视角》中所总结。 在图灵学习中,模型不仅限于 GAN 或神经网络; 判别器会影响生成器的输入,在图灵测试中充当询问器。
提出的第一个 GAN 由用于生成器和判别器的全连接层组成。 自从原始架构以来,已经开发了许多新的创新方法,例如,深度卷积 GAN,条件 GAN 和信息最大化的 GAN。 在下一节中,我们将详细研究这些架构,并从头开始实现每个架构。
GAN 架构和实现
如所承诺的,我们将仔细研究我们在前几节中详细提到的 GAN 的变体,并将其应用于实际问题。 最常用的 GAN 包括深度卷积 GAN,条件 GAN 和信息最大化 GAN。 让我们从最基本的架构开始。
原始 GAN
在最基本的 GAN 模型中,生成器和判别器都是全连接神经网络。 原始 GAN 的架构可以描述如下:

生成器的输入是来自特定分布的随机样本,我们通常将其称为噪声或潜在变量。 第二层和后面的几层是隐藏层,在这种情况下,它们是全连接层。 隐藏层通常比其先前的隐藏层具有更多的单元。 输出层的大小与预期的生成大小相同,与实际数据的大小相同。 对于判别器,其输入是真实或生成的数据,其后是一个或多个隐藏层,以及一个单元的输出层。 每个隐藏层通常比其先前的隐藏层具有更少的单元。 通常,生成器和判别器具有相同数量的隐藏层。 而且,两组隐藏层通常是对称的。 例如,原始 GAN 的生成器和判别器如下所示:

现在我们已经了解了什么是普通 GAN,我们可以开始在 TensorFlow 中从头开始实现它们了。 从这里开始,我们将以 MNIST 手写数字数据集为例,以便我们可以应用 GAN 生成我们自己的 MNIST。
我们将从加载用于模型训练的 MNIST 数据集开始:
>>> import numpy as np
>>> import tensorflow as tf
>>> def load_dataset():
... (x_train, y_train), (x_test, y_test) =
tf.keras.datasets.mnist.load_data('./mnist_data')
... train_data = np.concatenate((x_train, x_test), axis=0)
... train_data = train_data / 255.
... train_data = train_data * 2\. - 1
... train_data = train_data.reshape([-1, 28 * 28])
... return train_data
该函数读取并合并原始训练和测试集(不包括标签),因为在原始模型中不需要它们。 它还将数据从[0, 255]到[-1, 1]的范围重新缩放,这是神经网络模型预处理的非常重要的一部分,并且还将单个样本重塑为一维样本。
调用此函数并检查已加载数据的大小:
>>> data = load_dataset()
>>> print("Training dataset shape:", data.shape)
Training dataset shape: (70000, 784)
总共有 70,000 个训练样本,每个样本有 784 个尺寸(28 x 28)。 如果您忘记了 MNIST 数据的外观,我们将使用定义如下的函数显示一些示例:
>>> import matplotlib.pyplot as plt
>>> def display_images(data, image_size=28):
... fig, axes = plt.subplots(4, 10, figsize=(10, 4))
... for i, ax in enumerate(axes.flatten()):
... img = data[i, :]
... img = (img - img.min()) / (img.max() - img.min())
... ax.imshow(img.reshape(image_size, image_size), cmap='gray')
... ax.xaxis.set_visible(False)
... ax.yaxis.set_visible(False)
... plt.subplots_adjust(wspace=0, hspace=0)
... plt.show()
此函数在 4 行 10 列中显示 40 张图像。 稍后将重新使用它以显示生成的图像。 看一下前 40 个真实样本:
>>> display_images(data)
请参考以下屏幕截图以获取最终结果:

在评估我们生成的图像的真实性时,您将需要稍后返回这些图像。
现在,让我们开始构建 GAN 模型。 首先,我们为全连接层定义包装函数,因为它在原始 GAN 中最常使用:
>>> def dense(x, n_outputs, activation=None):
... return tf.layers.dense(x, n_outputs, activation=activation,
kernel_initializer=
tf.random_normal_initializer(mean=0.0,stddev=0.02))
通过放置一些密集层,我们构建了生成器:
>>> def generator(z, alpha=0.2):
... """
... Generator network
... @param z: input of random samples
... @param alpha: leaky relu factor
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... fc1 = dense(z, 256)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 1024)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 28 * 28)
... out = tf.tanh(out)
... return out
生成器将输入的随机噪声依次馈入三个隐藏层,分别具有 256、512 和 1,024 个隐藏单元。 请注意,每个隐藏层的激活函数是泄漏的 ReLU,这是 ReLU 的变体。 发明它是为了解决即将死去的 ReLU 问题,即对于该函数的任何负输入,其输出变为零。 它定义为f(x) = max(x, ax),其中a是介于 0 到 1 之间的斜率因子(但较小的值更常见)。 下图显示了 ReLU 和泄漏版本之间的比较(例如leak = 0.2):

在三个隐藏层之后,输出层随后进行 tanh 激活,将数据映射到期望图像的相同大小和范围。 类似地,我们用四个密集层构建判别器,其中三个是隐藏层,其大小与生成器中隐藏层的顺序相反:
>>> def discriminator(x, alpha=0.2):
... """
... Discriminator network
... @param x: input samples, can be real or generated samples
... @param alpha: leaky relu factor
... @return: output logits
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... fc1 = dense(x, 1024)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 256)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 1)
... return out
输出层将数据映射到单个单元对率。 现在,我们可以为大小为 784 的实际输入数据和大小为 100 的噪声输入数据定义占位符:
>>> noise_size = 100
>>> tf.reset_default_graph()
>>> X_real = tf.placeholder(tf.float32, (None, 28 * 28), name='input_real')
>>> z = tf.placeholder(tf.float32, (None, noise_size), name='input_noise')
将生成器应用于输入噪声,并将判别器应用于生成的图像以及真实图像数据:
>>> g_sample = generator(z)
>>> d_real_out = discriminator(X_real)
>>> d_fake_out = discriminator(g_sample)
利用网络的所有这些输出,我们为生成器开发了loss计算,该计算基于被认为是真实的伪造图像:
>>> g_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(logits=d_fake_out,
labels=tf.ones_like(d_fake_out)))
>>> tf.summary.scalar('generator_loss', g_loss)
我们还记录了损失,以便使用 TensorBoard 可视化学习进度。
接下来是用于判别器的loss计算,该计算基于两个组件:真实图像被视为伪造的图像和伪图像被视为真实的图像:
>>> d_real_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(logits=d_real_out,
labels=tf.ones_like(d_real_out)))
>>> d_fake_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(logits=d_fake_out,
labels=tf.zeros_like(d_fake_out)))
>>> d_loss = d_real_loss + d_fake_loss
>>> tf.summary.scalar('discriminator_loss', d_loss)
同样,我们记录了判别器损失。 然后,我们为两个网络定义优化器,如下所示:
>>> train_vars = tf.trainable_variables()
>>> d_vars = [var for var in train_vars
if var.name.startswith('discriminator')]
>>> g_vars = [var for var in train_vars
if var.name.startswith('generator')]
>>> learning_rate = 0.0002
>>> beta1 = 0.5
>>> with tf.control_dependencies(
tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
... d_opt = tf.train.AdamOptimizer(learning_rate,
beta1=beta1).minimize(d_loss, var_list=d_vars)
... g_opt = tf.train.AdamOptimizer(learning_rate,
beta1=beta1).minimize(g_loss, var_list=g_vars)
优化器实现 Adam 算法,学习率为 0.0002,第一矩衰减率为 0.5。 在进行模型优化之前,不要忘记定义一个函数,该函数返回用于训练的批量数据:
>>> def gen_batches(data, batch_size, shuffle=True):
... """
... Generate batches for training
... @param data: training data
... @param batch_size: batch size
... @param shuffle: shuffle the data or not
... @return: batches generator
... """
... n_data = data.shape[0]
... if shuffle:
... idx = np.arange(n_data)
... np.random.shuffle(idx)
... data = data[idx]
... for i in range(0, n_data, batch_size):
... batch = data[i:i + batch_size]
... yield batch
准备好所有组件之后,我们就可以开始训练 GAN 模型了。 对于每 100 步,我们记录生成器损失和判别器损失。 为了进行表现检查,我们创建了一组噪声输入,并显示了当前生成器针对每个周期生成的图像:
>>> epochs = 100
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter(
'./logdir/vanilla', sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for batch_x in gen_batches(data, batch_size):
... batch_z = np.random.uniform(
-1, 1, size=(batch_size,noise_size))
... _, summary, d_loss_batch = sess.run(
[d_opt, merged, d_loss],
feed_dict={z: batch_z, X_real: batch_x})
... sess.run(g_opt, feed_dict={z: batch_z})
... _, g_loss_batch = sess.run(
[g_opt, g_loss], feed_dict={z: batch_z})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss:
{:.4f}, generator Loss: {:.4f}".format(
... epoch + 1, epochs, d_loss_batch,g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z), feed_dict={z:sample_z})
... display_images(gen_samples)
注意,在每个周期,生成器更新两次,而判别器仅更新一次。 这是因为优化判别器比生成器容易得多,这很直观。 任意图像只是毫不费力地被认为是伪造的。 如果判别器在早期出现收敛,则不完整的生成器将产生垃圾。 您还可以为两个网络指定不同的学习率,例如,生成器的学习率稍高一些,0.001,判别器的学习率则为 0.0002。
请参阅以下屏幕截图以获取第 25 阶段的最终结果:

以下屏幕截图显示了周期 50 的输出:

以下屏幕截图显示了周期 75 的输出:

以下屏幕截图显示了周期 100 的输出:

我们的第一个 GAN 模型能够合成手写数字,并且大多数看起来都是合法的! 我们还来看看 TensorBoard 中的学习图。 要运行 TensorBoard,请在终端中输入以下命令:
tensorboard --logdir=logdir/
然后,在浏览器中转到http://localhost:6006/; 我们将看到判别器的图表:

以下是生成器的示意图:

您可能会注意到,有些数字看起来很奇怪。 在全连接原始模型的基础上,我们可以做哪些改进? 对于计算机视觉,使用卷积层可能是最直观的方法。
深度卷积 GAN
卷积层已成为解决图像问题的必备条件。 使用 GAN 生成图像也不例外。 因此,Radford 等人在《使用深度卷积生成对抗网络进行无监督表示学习》提出了深度卷积生成对抗网络(DCGAN)。
很容易理解 DCGAN 中的判别器。 它也非常类似于用于分类的标准 CNN,其中使用一个或多个卷积层,每个卷积层之后是一个非线性层,最后是一个全连接层。 例如,我们可以具有以下架构:

卷积层共有三个,分别由 64 个,128 个和 256 个5 x 5过滤器组成。
如前所述,生成器通常与判别器对称。 DCGAN 中的判别器通过卷积层解释输入图像并生成数字输出。 因此,生成器需要使用转置的卷积层将数字噪声输入转换为图像,这与卷积层完全相反。 卷积层执行下采样,而转置卷积层执行上采样。 例如,我们可以为生成器使用以下架构:

考虑到所有这些概念,让我们实现 DCGAN。 同样,我们从定义卷积和转置卷积层的包装函数开始,因为它们在 DCGAN 中使用最频繁:
>>> def conv2d(x, n_filters, kernel_size=5):
... return tf.layers.conv2d(inputs=x, filters=n_filters,
kernel_size=kernel_size, strides=2, padding="same",
kernel_initializer=tf.random_normal_initializer(
mean=0.0, stddev=0.02))
>>> def transpose_conv2d(x, n_filters, kernel_size=5):
... return tf.layers.conv2d_transpose(inputs=x,
filters=n_filters, kernel_size=kernel_size, strides=2,
padding='same', kernel_initializer=
tf.random_normal_initializer(mean=0.0, stddev=0.02))
我们定义的密集函数可用于全连接层。 同样,我们在卷积层的输出上应用批量归一化。 批量规范化背后的思想类似于输入数据规范化,可加快学习速度。 通过从激活层的输出中减去批量平均值,然后将其除以批量标准差来执行批量标准化。 我们为批量标准化定义了一个包装器函数,如下所示:
>>> def batch_norm(x, training, epsilon=1e-5, momentum=0.9):
... return tf.layers.batch_normalization(x, training=training,
epsilon=epsilon, momentum=momentum)
现在,我们可以使用刚刚定义的组件来构造判别器:
>>> def discriminator(x, alpha=0.2, training=True):
... """
... Discriminator network for DCGAN
... @param x: input samples, can be real or generated samples
... @param alpha: leaky relu factor
... @param training: whether to return the output in training mode
(normalized with statistics of the current batch)
... @return: output logits
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... conv1 = conv2d(x, 64)
... conv1 = tf.nn.leaky_relu(conv1, alpha)
... conv2 = conv2d(conv1, 128)
... conv2 = batch_norm(conv2, training=training)
... conv2 = tf.nn.leaky_relu(conv2, alpha)
... conv3 = conv2d(conv2, 256)
... conv3 = batch_norm(conv3, training=training)
... conv3 = tf.nn.leaky_relu(conv3, alpha)
... fc = tf.layers.flatten(conv3)
... out = dense(fc, 1)
... return out
这很容易。 三个卷积层分别包含 64、128 和 256 个5 x 5过滤器。
开发生成器有点棘手。 回想一下,我们需要首先将输入的一维噪声整形为三维图像,以启用转置卷积。 我们知道,由于两个网络的对称性,第三维是 256。 那么,前两个维度是什么? 它们是2 x 2,在第一个转置的卷积层之后变为4 x 4,第二个之后的为8 x 8,第三个之后的为16 x 16,如果它是3 x 3,则与我们的28 x 28的目标相去甚远。 同样,它在第三个转置的卷积层之后变为24 x 24,这又不够大。 如果它是4 x 4,则在第三个转置的卷积层之后变为32 x 32。 因此,将线性输入重塑为4 x 4图像就足够了。 请注意,现在生成的图像输出的大小为32 x 32,这与我们的真实图像的大小4 x 4不同。要确保对判别器的输入恒定,我们只需要在真实图像上填充零即可。 在load_dataset函数的顶部实现了实图像的零填充:
>>> def load_dataset_pad():
... (x_train, y_train), (x_test, y_test)=
tf.keras.datasets.mnist.load_data('./mnist_data')
... train_data = np.concatenate((x_train, x_test), axis=0)
... train_data = train_data / 255.
... train_data = train_data * 2\. - 1
... train_data = train_data.reshape([-1, 28, 28, 1])
... train_data = np.pad(train_data, ((0,0),(2,2),(2,2),(0,0)),
'constant', constant_values=0.)
... return train_data
由于 DCGAN 中的判别器接受三维图像输入,因此训练数据也被重塑为(70000, 32, 32, 1):
>>> data = load_dataset_pad()
>>> print("Training dataset shape:", data.shape)
Training dataset shape: (70000, 32, 32, 1)
加载数据后,我们可以继续定义生成器:
>>> def generator(z, n_channel, training=True):
... """
... Generator network for DCGAN
... @param z: input of random samples
... @param n_channel: number of output channels
... @param training: whether to return the output in training mode (normalized with statistics of the current batch)
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... fc = dense(z, 256 * 4 * 4, activation=tf.nn.relu)
... fc = tf.reshape(fc, (-1, 4, 4, 256))
... trans_conv1 = transpose_conv2d(fc, 128)
... trans_conv1 = batch_norm(trans_conv1, training=training)
... trans_conv1 = tf.nn.relu(trans_conv1)
... trans_conv2 = transpose_conv2d(trans_conv1, 64)
... trans_conv2 = batch_norm(trans_conv2, training=training)
... trans_conv2 = tf.nn.relu(trans_conv2)
... trans_conv3 = transpose_conv2d(trans_conv2, n_channel)
... out = tf.tanh(trans_conv3)
... return out
首先,它将噪声输入映射到具有 4,096 个单元的全连接层,以便它可以重塑大小为4 x 4 x 256的三维数据,然后由三个转置的卷积层消耗。
现在,我们可以为尺寸为28 x 28 x 1的实际输入数据定义占位符:
>>> image_size = data.shape[1:]
>>> tf.reset_default_graph()
>>> X_real = tf.placeholder(
tf.float32, (None,) + image_size, name='input_real')
噪声输入数据和其余参数与上一节中的相同,因此我们跳过重复相同的代码。 接下来,我们将生成器应用于输入噪声:
>>> g_sample = generator(z, image_size[2])
其余部分,包括图像判别器,损失计算和优化器,将重用上一节中的内容。
准备好所有组件之后,我们现在就可以开始训练我们的 DCGAN 模型了。 同样,我们记录每 100 步的损失,并显示每个周期(此时总共 50 个周期)的合成图像:
>>> epochs = 50
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter(
'./logdir/dcgan', sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for batch_x in gen_batches(data, batch_size):
... batch_z = np.random.uniform(
-1, 1, size=(batch_size, noise_size))
... _, summary, d_loss_batch = sess.run(
[d_opt, merged,d_loss], feed_dict=
{z: batch_z, X_real: batch_x})
... sess.run(g_opt, feed_dict={z: batch_z, X_real:batch_x})
... _, g_loss_batch = sess.run([g_opt, g_loss], feed_dict=
{z: batch_z, X_real: batch_x})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss: {:.4f},
generator Loss: {:.4f}".format(epoch + 1, epochs,
d_loss_batch, g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z, image_size[2],
training=False), feed_dict={z: sample_z})
... display_images(gen_samples, 32)
请参阅以下屏幕截图以获取第 25 阶段的最终结果:

最后,请参考以下屏幕截图,以获取第 50 阶段的最终结果:

TensorBoard 中显示的学习图如下所示:

下图显示了生成器损失:

从我们的 DCGAN 模型生成的图像看起来比从普通 GAN 生成的图像更真实。 我们还将它们与真实图像一起放置; 没有提示,您能告诉哪个集是真集还是假集?

到目前为止,就我们无法控制要产生的 0 到 9 而言,我们生成的数字是相当随机的。 这是因为原始 GAN 和 DCGAN 中的生成器仅吸收随机噪声,只要结果看起来是真实的,就不再限制生成什么。 我们将看到条件 GAN 和 infoGAN 如何启用此功能。
条件 GAN
条件 GAN(CGAN)通过将标签信息馈送到生成器和判别器,从而希望生成特定标签的数据,从而使我们可以控制要生成的内容。 下图显示了 CGAN 的架构:

如我们所见,标签数据是 CGAN 中生成器和判别器的输入空间的扩展。 注意,标签数据表示为一热向量。 例如,MNIST 数据集中的数字 2 变为[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]。 CGAN 的其他部分,例如cost函数,与常规 GAN 相似。 因此,实现 CGAN 应该很容易。 我们可以开发一个全连接 CGAN,但是 CGAN 中的隐藏层不限于全连接层。 您可以尝试实现卷积版本作为练习。
首先,我们需要修改data load函数以包含标签:
>>> def load_dataset_label():
... from keras.utils import np_utils
... (x_train, y_train), (x_test, y_test)
=tf.keras.datasets.mnist.load_data('./mnist_data')
... x_data = np.concatenate((x_train, x_test), axis=0)
... y_train = np_utils.to_categorical(y_train)
... y_test = np_utils.to_categorical(y_test)
... y_data = np.concatenate((y_train, y_test), axis=0)
... x_data = x_data / 255.
... x_data = x_data * 2\. - 1
... x_data = x_data.reshape([-1, 28 * 28])
... return x_data, y_data
该函数还将标签数据从一维转换为一热编码的十维:
>>> x_data, y_data = load_dataset_label()
>>> print("Training dataset shape:", x_data.shape) Training dataset shape: (70000, 784)
因此,batch生成函数也需要更新,以便它返回一批图像和标签:
>>> def gen_batches_label(x_data, y_data, batch_size, shuffle=True):
... """
... Generate batches including label for training
... @param x_data: training data
... @param y_data: training label
... @param batch_size: batch size
... @param shuffle: shuffle the data or not
... @return: batches generator
... """
... n_data = x_data.shape[0]
... if shuffle:
... idx = np.arange(n_data)
... np.random.shuffle(idx)
... x_data = x_data[idx]
... y_data = y_data[idx]
... for i in range(0, n_data - batch_size, batch_size):
... x_batch = x_data[i:i + batch_size]
... y_batch = y_data[i:i + batch_size]
... yield x_batch, y_batch
稍后,我们将标签数据的占位符定义为新输入:
>>> n_classes = 10
>>> y = tf.placeholder(tf.float32, shape=[None, n_classes],name='y_classes')
生成器获取标签数据,并将其与输入噪声连接起来:
>>> def generator(z, y, alpha=0.2):
... """
... Generator network for CGAN
... @param z: input of random samples
... @param y: labels of the input samples
... @param alpha: leaky relu factor
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... z_y = tf.concat([z, y], axis=1)
... fc1 = dense(z_y, 256)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 1024)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 28 * 28)
... out = tf.tanh(out)
... return out
判别器做同样的事情:
>>> def discriminator(x, y, alpha=0.2):
... """
... Discriminator network for CGAN
... @param x: input samples, can be real or generated samples
... @param y: labels of the input samples
... @param alpha: leaky relu factor
... @return: output logits
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... x_y = tf.concat([x, y], axis=1)
... fc1 = dense(x_y, 1024)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 512)
... fc2 = tf.nn.leaky_relu(fc2, alpha)
... fc3 = dense(fc2, 256)
... fc3 = tf.nn.leaky_relu(fc3, alpha)
... out = dense(fc3, 1)
... return out
现在,我们将y标签提供给generator和discriminator:
>>> g_sample = generator(z, y)
>>> d_real_out = discriminator(X_real, y)
>>> d_fake_out = discriminator(g_sample, y)
为了进行质量检查,我们在给定噪声输入的每个周期合成图像,并带有 10 个类别的集合的标签。 样本标签定义如下:
>>> n_sample_display = 40
>>> sample_y = np.zeros(shape=(n_sample_display, n_classes))
>>> for i in range(n_sample_display):
... j = i % 10
... sample_y[i, j] = 1
训练部分之前的其余代码与原始 GAN 模型中的代码相同。
准备好所有组件之后,我们就可以开始训练 CGAN 模型了:
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter('./logdir/cgan',sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for batch_x, batch_y in gen_batches_label(
x_data, y_data,batch_size):
... batch_z = np.random.uniform(-1, 1,
size=(batch_size, noise_size))
... _, summary, d_loss_batch = sess.run([d_opt, merged,d_loss],
feed_dict={z: batch_z,
X_real: batch_x, y: batch_y})
... sess.run(g_opt, feed_dict={z: batch_z, y: batch_y})
... _, g_loss_batch = sess.run([g_opt, g_loss], feed_dict=
{z: batch_z, y: batch_y})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss: {:.4f},
generator Loss: {:.4f}".format(
... epoch + 1, epochs, d_loss_batch, g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z, y),
feed_dict={z:sample_z, y: sample_y})
... display_images(gen_samples)
请参阅以下屏幕截图,以获取第 50 阶段的最终结果:

并参考以下屏幕截图以获取第 100 个周期的最终结果:

TensorBoard 中显示的学习图如下所示:

下图显示了生成器损失:

使用 CGAN,我们可以完全控制要生成的数字,并且还可以使用 InfoGAN 来控制其他属性,例如宽度或旋转度。
InfoGAN
InfoGANs(最大化生成对抗网络信息的缩写)在某种意义上类似于 CGAN,因为两个生成器网络都接受一个附加参数,并且条件变量c,例如标签信息。 他们都试图学习相同的条件分布P(X | z, c。 InfoGAN 与 CGAN 的区别在于它们处理条件变量的方式。
CGAN 认为条件变量是已知的。 因此,在训练期间将条件变量显式馈送到判别器。 相反,InfoGAN 假设条件变量是未知的且是潜在的,我们需要根据训练数据来推断条件变量。 InfoGAN 中的判别器负责推导后验P(c | X)。 下图显示了 InfoGAN 的架构:

由于我们不需要将条件变量提供给将自动推断出条件的判别器,因此我们可以将其基本分配给任何与数据相关的事物。 它不仅限于标签。 它可以是边缘的宽度,旋转角度和特定样式。 此外,它不仅限于一个变量,也不限于诸如标签之类的分类值。 我们可以有多个变量或一个标签变量,以及一个或多个连续变量作为潜在特征。
那么,InfoGAN 如何学习潜在特征? 顾名思义,它们是通过最大化信息来实现的,信息是指信息论中的互信息。 我们希望最大化c与生成器生成的输出之间的相互信息。 InfoGAN 的loss函数可以概括如下:
L_InfoGAN = (D, G) = L(D, G) - I(c, G(z, c))
此处,L(D, G)是常规 GAN 中的loss函数,I(c | G(z, c))是c和生成的输出之间的互信息。 更准确地,将P(c | G(z, c))预测为更高的I(c | G(z, c))。
互信息I(a, b)衡量如果我们知道b,我们对a的了解程度。P(a | b)(或P(b | a))预测得越准确,I(a, b)越高。 I(a, b) = 0表示a和b完全无关。
取决于潜在变量是什么,可以不同地计算I(c | G(z, c))。 对于分类变量,它是通过cross熵来衡量的。 对于连续变量,可以将其计算为其分布(例如高斯分布)之间的方差。
考虑到所有这些概念,我们可以开始开发 InfoGAN 模型。 回想一下,我们不需要在 InfoGAN 中提供标签数据。 因此,我们可以将load_dataset_pad和gen_batches函数用作 DCGAN。 让我们像往常一样先加载数据:
>>> data = load_dataset_pad()
>>> print("Training dataset shape:", data.shape)
>>> Training dataset shape: (70000, 32, 32, 1)
现在,我们为条件变量定义一个占位符,作为生成器的额外输入:
>>> n_classes = 10
>>> n_cont = 1
>>> c = tf.placeholder(tf.float32, shape=[None, n_classes + n_cont],
name='conditional_variable')
此示例中的潜在特征包括 10 维一热编码特征和一维连续热特征。 现在,我们定义 InfoGAN 的生成器,它生成条件变量并将其与输入噪声连接起来:
>>> def generator(z, c, n_channel, training=True):
... """
... Generator network for InfoGAN
... @param z: input of random samples
... @param c: latent features for the input samples
... @param n_channel: number of output channels
... @param training: whether to return the output in training mode
(normalized with statistics of the current batch)
... @return: output of the generator network
... """
... with tf.variable_scope('generator', reuse=tf.AUTO_REUSE):
... z_c = tf.concat([z, c], axis=1)
... fc = dense(z_c, 256 * 4 * 4, activation=tf.nn.relu)
... fc = tf.reshape(fc, (-1, 4, 4, 256))
... trans_conv1 = transpose_conv2d(fc, 128)
... trans_conv1 = batch_norm(trans_conv1, training=training)
... trans_conv1 = tf.nn.relu(trans_conv1)
... trans_conv2 = transpose_conv2d(trans_conv1, 64)
... trans_conv2 = batch_norm(trans_conv2, training=training)
... trans_conv2 = tf.nn.relu(trans_conv2)
... trans_conv3 = transpose_conv2d(trans_conv2, n_channel)
... out = tf.tanh(trans_conv3)
... return out
至于判别器,其前半部分与 DCGAN 的判别器相同,后者由三组卷积层组成。 它的后半部分由两个全连接层组成,后面是三组输出:判别器对率(用于确定图像是真实的还是伪造的),连续变量的后验和分类变量的后验:
>>> def discriminator(x, n_classes, n_cont=1, alpha=0.2, training=True):
... """
... Discriminator network for InfoGAN
... @param x: input samples, can be real or generated samples
... @param n_classes: number of categorical latent variables
... @param n_cont: number of continuous latent variables
... @param alpha: leaky relu factor
... @param training: whether to return the output in training mode
(normalized with statistics of the current batch)
... @return: discriminator logits, posterior for the continuous
variable, posterior for the categorical variable
... """
... with tf.variable_scope('discriminator', reuse=tf.AUTO_REUSE):
... conv1 = conv2d(x, 64)
... conv1 = tf.nn.leaky_relu(conv1, alpha)
... conv2 = conv2d(conv1, 128)
... conv2 = batch_norm(conv2, training=training)
... conv2 = tf.nn.leaky_relu(conv2, alpha)
... conv3 = conv2d(conv2, 256)
... conv3 = batch_norm(conv3, training=training)
... conv3 = tf.nn.leaky_relu(conv3, alpha)
... fc1 = tf.layers.flatten(conv3)
... fc1 = dense(fc1, 1024)
... fc1 = batch_norm(fc1, training=training)
... fc1 = tf.nn.leaky_relu(fc1, alpha)
... fc2 = dense(fc1, 128)
... d_logits = dense(fc2, 1)
... cont = dense(fc2, n_cont)
... classes = dense(fc2, n_classes)
... return d_logits, cont, classes
现在,我们将生成器和判别器应用于输入数据以及条件变量:
>>> g_sample = generator(z, c, image_size[2])
>>> d_real_logits, d_real_cont, d_real_cat = discriminator(
X_real, n_classes, n_cont)
>>> d_fake_logits, d_fake_cont, d_fake_cat = discriminator(
g_sample, n_classes, n_cont)
回想一下,InfoGAN 中的损失函数由两部分组成。 第一部分与标准 GAN 相同:
>>> g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
logits=d_fake_logits, labels=tf.ones_like(d_fake_logits)))
>>> d_real_loss = tf.reduce_mean(
... tf.nn.sigmoid_cross_entropy_with_logits(
logits=d_real_logits, labels=tf.ones_like(d_real_logits)))
>>> d_fake_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
logits=d_fake_logits,
labels=tf.zeros_like(d_fake_logits)))
>>> d_loss = d_real_loss + d_fake_loss
第二部分是相互信息。 对于单热编码的分类变量,通过交叉熵来度量:
>>> cat = c[:, n_cont:]
>>> d_cat_loss = tf.reduce_mean(
... tf.nn.softmax_cross_entropy_with_logits(
logits=d_fake_cat, labels=cat))
对于连续变量,可以将其计算为其分布(例如高斯分布)之间的方差:
>>> d_cont_loss = tf.reduce_sum(tf.square(d_fake_cont))
信息损失(与互信息相反)是这两种损失的加权和:
>>> lambda_cont = 0.1
>>> lambda_cat = 1.0
>>> d_info_loss = lambda_cont * d_cont_loss + lambda_cat * cat_loss
生成器和判别器的最终损失如下:
>>> g_loss += d_info_loss
>>> tf.summary.scalar('generator_loss', g_loss)
>>> d_loss += d_info_loss
>>> tf.summary.scalar('discriminator_loss', d_loss)
我们还需要开发一个函数来生成用于训练的随机条件变量:
>>> def gen_condition_variable(n_size, n_classes, n_cont):
... cont = np.random.randn(n_size, n_cont)
... cat = np.zeros((n_size, n_classes))
... cat[range(n_size), np.random.randint(0, n_classes, n_size)] = 1
... return np.concatenate((cont, cat), axis=1)
为了进行质量检查,我们在给定噪声输入的每个周期合成图像,并与一组 10 类的条件变量和一组恒定连续变量进行合成。 样本条件变量的定义如下:
>>> n_sample_display = 40
>>> sample_c = np.zeros((n_sample_display, n_cont + n_classes))
>>> for i in range(n_sample_display):
... j = i % 10
... sample_c[i, j + 1] = 1
... sample_c[i, 0] = -3 + int(i / 10) * 2
第 1,第 11 ,第 21 和第 31 个样本被赋予标签0,而第 2,第 12,第 22 和第 32 个样本被赋予标签1,依此类推。 前 10 个样本被赋予连续值-3,接下来的 10 个样本被赋予-1,然后被赋予1,最后被赋予最后的 10 个样本3。 训练超过 50 个周期如下:
>>> steps = 0
>>> with tf.Session() as sess:
... merged = tf.summary.merge_all()
... train_writer = tf.summary.FileWriter('./logdir/infogan',sess.graph)
... sess.run(tf.global_variables_initializer())
... for epoch in range(epochs):
... for x in gen_batches(data, batch_size):
... batch_z = np.random.uniform(
-1, 1, size=(batch_size, noise_size))
... batch_c = gen_condition_variable(
batch_size, n_classes, n_cont)
... _, summary, d_loss_batch = sess.run([d_opt, merged,
d_loss], feed_dict=
{z: batch_z, X_real: x, c: batch_c})
... sess.run(g_opt, feed_dict=
{z: batch_z, X_real: x, c: batch_c})
... _, g_loss_batch = sess.run([g_opt, g_loss],
feed_dict={z: batch_z, X_real: x, c: batch_c})
... if steps % 100 == 0:
... train_writer.add_summary(summary, steps)
... print("Epoch {}/{} - discriminator loss: {:.4f},
generator Loss: {:.4f}".format(
... epoch + 1, epochs, d_loss_batch, g_loss_batch))
... steps += 1
... gen_samples = sess.run(generator(z, c,image_size[2],
training=False),
feed_dict={z: sample_z, c: sample_c})
... display_images(gen_samples, 32)
请参阅以下屏幕截图,以获取第 20 阶段的最终结果:

有关周期 40 的最终结果,请参考以下屏幕截图:

有关最后一个周期的最终结果,请参考以下屏幕截图:

您可能已经注意到,生成的图像不是 0 到 9 的顺序。我们做错了吗? 幸运的是,没有。 回想一下,条件变量c在模型之前是未知的,并且是在训练期间推断出来的。 标签 0 不一定表示模型的数字 0。 但是模型获得的知识是类别 0 与任何其他类别都不同。 因此,从 0 到 9 的标签仅代表 10 个不同的类别。 事实证明,在我们的案例中,类别 0 到 9 代表数字 3、0、2、6、4、7、5、8、9、1。连续变量呢? 我们可以看到生成的图像的旋转行为逐行不同,尤其是 0、7、8、9 和 1。第一行(输入连续值-3的前 10 张图像)显示从垂直轴开始逆时针旋转 20 度。 最后一行(具有输入连续值3的最后 10 张图像)显示从垂直轴顺时针旋转 20 度。
使用 InfoGAN,除了生成的图像类别之外,我们还扩展了对宽度或旋转等属性的控制。
总结
我们刚刚完成了有关深度学习架构 GAN 的学习旅程的重要部分! 在本章中,我们更加熟悉 GAN 及其变体。 我们从 GAN 入手。 GAN 的演进路径; 以及它们如何在数据合成(例如图像生成,音频和视频生成)中如此流行。 我们还研究了四种 GAN 架构,即原始 GAN,深度卷积 GAN,条件 GAN 和信息最大化的 GAN。 我们从头开始实现了每个 GAN 模型,并使用它们来生成看起来是真实的数字图像。
GAN 是近几年来深度学习的一项伟大发明。 在下一章中,我们将讨论深度学习的其他最新进展,包括贝叶斯神经网络,胶囊网络和元学习。
八、深度学习的新趋势
在本书的前七章中,具有各种架构的深度神经网络展示了它们从图像,文本和事务数据中学习的能力。 尽管近年来深度学习发展迅速,但它的发展似乎并不会很快减速。 我们看到几乎每个月都会提出新的深度学习架构,并且新的解决方案时不时地成为最新技术。 因此,在最后一章中,我们想谈一谈深度学习中的一些想法,这些想法今年对我们很有影响,并且在将来会更加突出。
在本章中,我们将研究以下主题:
- 贝叶斯神经网络
- 深度学习模型的局限性
- 贝叶斯神经网络的实现
- 胶囊网络
- 卷积神经网络(CNN)的局限性
- 元学习
- 深度学习中的挑战
- 元学习模型的实现
深度学习的新趋势
除了我们在前几章中提到的以外,还有许多其他有趣的深度学习模型和架构难以分类,同时,它们是深度学习的新趋势,并将在未来几年中产生巨大影响 。 在 NLP 中, BERT(代表转换器的双向编码器表示)成为了最新的语言模型(有关更多详细信息,请参阅以下论文,由 Google 发布)。 在计算机视觉方面,GAN 不断获得普及和改进。 他们的发明者 Ian Goodfellow 提出了注意力生成对抗网络,用于更精细地生成图像,其中包括以下三个新趋势:
- 贝叶斯神经网络
- 胶囊网络
- 元学习
贝叶斯神经网络
贝叶斯深度学习结合了贝叶斯学习和深度学习的优点。 它提供了一个深度学习框架,可以实现最先进的表现,同时捕获和建模不确定性。 首先让我们开始理解不确定性的含义,然后我们将继续探讨贝叶斯深度学习如何从不确定性的角度看待事物。
我们的深度学习模型不知道的东西 - 不确定性
不确定性是由于知识有限而无法准确描述未来结果的状态。 在机器学习或深度学习的上下文中,这与预测结果的含糊性有关,或者与人的主观定义和概念有关,而不是自然的客观事实。 不确定性很重要,因为它为我们提供了有关预测的自信程度的信息-如果预测不足或过分虚假,我们可能会拒绝我们的预测。
我们的深度学习模型通常无法估计这种不确定性。 他们产生预测,并盲目接受它们的准确率。 您可能会争辩说,最后一个 Sigmoid 或 softmax 层提供了预测结果的可能性。 这不是信心吗? 概率越高,置信度越高? 不幸的是,这不是信心,因为概率描述了一个结果相对于另一个的结果,但并不能解释整体的信心。
在许多情况下,不知道不确定性可能会带来问题。 例如,2016 年 5 月,一辆 AI 辅助车辆在未能识别出明亮的天空下的白色拖拉机拖车时坠毁。 早在 2016 年,Google 相册中的图像分类系统将两名非洲人标记为大猩猩。 如果将不确定性纳入算法,则可能会避免错误的决策,因为低信度的预测将被拒绝。
我们如何获取不确定性信息 – 贝叶斯神经网络
贝叶斯深度学习将预测与不确定性信息相关联。 让我们看看它是如何工作的。
在我们的常规深度学习模型中,包括权重w和偏差b的参数是通过最大似然估计优化的 (MLE):
w = argmax[w] logP(x, y | w)
训练模型后,参数的每个系数(例如w[1], w[2], ..., b[1], b[2], ...)都是标量,例如w[1] = 1,w[2] = 3。
相反,在贝叶斯学习中,每个系数都与一个分布相关联。 例如,它们可以处于高斯分布w[1] ~ N(1, 1), w[2] ~ N(3, 2),如下图所示:

关于单个值进行分布的好处是,它可以衡量预测的可信度。 如果使用从分布中采样的不同参数集生成一致的预测,则可以说这些预测具有很高的置信度。 相反,如果在各种样本上的预测不一致,则可以说预测的置信度较低。
估计参数的分布等效于参数的最大后验估计(MAP):
w = argmax[w] logP(w | x, y)
根据贝叶斯规则,可以如下计算P(w | x, y):

困难的部分是计算分母,这就是证据。 这需要对w的所有可能值进行积分。 幸运的是,可以使用蒙特卡洛或变分推断之类的技术对其进行近似。 我们不会在此处提供有关这些技术的详细信息,因为它们不在本书的讨论范围之内。 取而代之的是,我们将使用 TensorFlow 实现贝叶斯神经网络,并演示拥有不确定性信息的力量。
Edward 是我们将用来实现贝叶斯推理的库。 它建立在 TensorFlow 之上,专为概率建模和推断而设计,包括变分推断和蒙特卡洛。 在撰写本文时(2018 年底),Edward 仅与 TensorFlow 1.7 兼容。 因此,在安装 Edward 之前,我们将必须卸载当前版本的 TensorFlow 并安装 1.7 版:
pip uninstall tensorflow
pip install tensorflow==1.7.0.
pip install edward
不要忘了在此部分之后安装最新版本的 TensorFlow。 现在,我们可以执行以下步骤来实现贝叶斯神经网络:
- 让我们导入必要的包:
>>> import numpy as np
>>> import tensorflow as tf
>>> from edward.models import Categorical, Normal
>>> import edward as ed
- 加载训练和测试数据。 在示例中,我们将使用 MNIST 数据集:
>>> def load_dataset():
... (x_train, y_train), (x_test, y_test) =
tf.keras.datasets.mnist.load_data('./mnist_data')
... x_train = x_train / 255.
... x_train = x_train.reshape([-1, 28 * 28])
... x_test = x_test / 255.
... x_test = x_test.reshape([-1, 28 * 28])
... return (x_train, y_train), (x_test, y_test)
>>> (x_train, y_train), (x_test, y_test) = load_dataset()
- 定义一些占位符和变量:
>>> batch_size = 100
>>> n_features = 28 * 28
>>> n_classes = 10
>>> x = tf.placeholder(tf.float32, [None, n_features])
>>> y_ph = tf.placeholder(tf.int32, [batch_size])
- 为简单起见,我们将使用仅具有一个隐藏层的神经网络,以及
w和b。 我们分别使用高斯分布为权重和偏差设置先验:
>>> w = Normal(loc=tf.zeros([n_features, n_classes]),
scale=tf.ones([n_features, n_classes]))
>>> b = Normal(loc=tf.zeros(n_classes), scale=tf.ones(n_classes))
loc参数指定分布的平均值,而scale参数指定标准差。 然后,我们可以计算预测输出:
>>> y = Categorical(tf.matmul(x, w) + b)
由于模型的参数是与单个值相反的分布,因此输出也应具有分布-特别是分类分布。
- 如前所述,我们使用变分推断来近似
w和b的后验。 我们建立了近似分布Q(w)和Q(b),它们将被优化以匹配w和b。 两组分布之间的差异通过 Kullback-Leibler(KL)散度来衡量,我们尝试将其最小化。
同样,近似分布Q(w)和Q(b)也是高斯分布,但初始位置随机:
>>> qw = Normal(loc=tf.Variable(tf.random_normal(
[n_features, n_classes])), scale=tf.nn.softplus(
tf.Variable(tf.random_normal([n_features, n_classes]))))
>>> qb = Normal(loc=tf.Variable(tf.random_normal([n_classes])),
scale=tf.nn.softplus(tf.Variable(
tf.random_normal([n_classes]))))
- 接下来,我们定义并初始化与 KL 散度的变分推论:
>>> inference = ed.KLqp({w: qw, b: qb}, data={y: y_ph})
>>> inference.initialize(n_iter=100, scale=
{y: float(x_train.shape[0]) / batch_size})
变分推断带有 100 次迭代。
- 不要忘记启动 TensorFlow 会话并初始化该会话的所有变量:
>>> sess = tf.InteractiveSession()
>>> tf.global_variables_initializer().run()
- 现在,我们可以以小批量的方式开始训练贝叶斯网络模型,在此我们可以重用我们在“第 7 章”,“生成对抗网络”中定义的批量生成函数:
>>> def gen_batches_label(x_data, y_data, batch_size, shuffle=True):
... """
... Generate batches including label for training
... @param x_data: training data
... @param y_data: training label
... @param batch_size: batch size
... @param shuffle: shuffle the data or not
... @return: batches generator
... """
... n_data = x_data.shape[0]
... if shuffle:
... idx = np.arange(n_data)
... np.random.shuffle(idx)
... x_data = x_data[idx]
... y_data = y_data[idx]
... for i in range(0, n_data - batch_size, batch_size):
... x_batch = x_data[i:i + batch_size]
... y_batch = y_data[i:i + batch_size]
... yield x_batch, y_batch
>>> for _ in range(inference.n_iter):
... for X_batch, Y_batch in gen_batches_label( x_train, y_train, batch_size):
... inference.update(feed_dict={x: X_batch, y_ph: Y_batch})
- 训练完成后,我们在测试集上评估模型(优化的近似分布
Q(w)和Q(b))。 因为它们是分布,所以我们可以对w和b的各种集合进行采样,并计算相应的预测。 我们训练样本 30 次,但总的来说,训练越多越好:
>>> n_samples = 30
>>> pred_samples = []
>>> for _ in range(n_samples):
... w_sample = qw.sample()
... b_sample = qb.sample()
... prob = tf.nn.softmax(
tf.matmul(x_test.astype(np.float32), w_sample)
+ b_sample)
... pred = np.argmax(prob.eval(), axis=1).astype(np.float32)
... pred_samples.append(pred)
- 来自每个采样参数的预测记录在
pred_samples中。 现在,我们可以计算每组预测的准确率:
>>> acc_samples = []
>>> for pred in pred_samples:
... acc = (pred == y_test).mean() * 100
... acc_samples.append(acc)
30 组预测的准确率如下:
>>> print('The classification accuracy for each sample of w and b:',
acc_samples)
The classification accuracy for each sample of w and b: [90.86999999999999, 90.86, 91.84, 90.88000000000001, 91.33, 91.14999999999999, 90.42, 90.59, 91.36, 91.18, 90.25, 91.22, 89.36, 90.99000000000001, 90.99000000000001, 91.33, 91.2, 91.38, 90.56, 90.75, 90.75, 91.01, 90.96, 91.17, 91.29, 91.03, 91.12, 91.64, 91.44, 90.71000000000001]
单个样本的准确率非常一致,约为 91%。 这表明该模型具有很高的置信度。 我们还可以仔细看一个图像示例来验证这一点。 我们需要拍摄第一个测试图像(即标签7)并显示它:
>>> image_test_ind = 0
>>> image_test = x_test[image_test_ind]
>>> label_test = y_test[image_test_ind]
>>> print('The label of the image is:', label_test)
The label of the image is: 7
>>> import matplotlib.pyplot as plt
>>> plt.imshow(image_test.reshape((28, 28)),cmap='Blues')
>>> plt.show()
请参考以下屏幕截图以获取最终结果:

此示例的预测如下:
>>> pred_samples_test = [pred[image_test_ind] for pred in pred_samples]
>>> print('The predictions for the example are:', pred_samples_test)
The predictions for the example are: [7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0]
我们还制作了预测的直方图,以更好地可视化它们:
>>> plt.hist(pred_samples_test, bins=range(10))
>>> plt.xticks(np.arange(0,10))
>>> plt.xlim(0, 10)
>>> plt.xlabel("Predictions for the example")
>>> plt.ylabel("Frequency")
>>> plt.show()
请参考以下屏幕截图以获取最终结果:

这些预测是一致且充满信心的。 如果预测不一致,将会是什么样? 我们可以使用 notMNIST 数据集看到这一点。 notMNIST 数据集包含从A到J的 529,119 张28 x 28字母的灰度图像。 请参阅以下屏幕快照以获取数据集:

我们可以从这里下载数据集。 单击MNIST_small.tar.gz的链接,该链接包含数据集的一小部分,并解压缩该文件。 举一个例子并显示它:
>>> from scipy import ndimage
>>> image_file = 'notMNIST_small/A/MDRiXzA4LnR0Zg==.png'
>>> image_not = ndimage.imread(image_file).astype(float)
>>> plt.imshow(image_not, cmap='Blues')
>>> plt.show()
请参考以下屏幕截图以获取最终结果:

像之前一样对图像进行预处理:
>>> image_not = image_not / 255.
>>> image_not = image_not.reshape([-1, 28 * 28])
同样,我们对经过训练的模型进行采样,并使用每组采样参数进行预测:
>>> pred_samples_not = []
>>> for _ in range(n_samples):
... w_sample = qw.sample()
... b_sample = qb.sample()
... prob = tf.nn.softmax(tf.matmul(
image_not.astype(np.float32),w_sample) + b_sample)
... pred = np.argmax(prob.eval(), axis=1).astype(np.float32)
... pred_samples_not.append(pred[0])
该 notMNIST 示例(A)的预测如下:
>>> print('The predictions for the notMNIST example are:',
pred_samples_not)
The predictions for the notMNIST example are: [2.0, 5.0, 2.0, 2.0, 2.0, 2.0, 2.0, 3.0, 5.0, 5.0, 8.0, 2.0, 5.0, 5.0, 5.0, 3.0, 2.0, 5.0, 6.0, 2.0, 2.0, 5.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 8.0, 2.0]
再次,我们对预测进行直方图,以更好地可视化它们:
>>> plt.hist(pred_samples_not, bins=range(10))
>>> plt.xticks(np.arange(0,10))
>>> plt.xlim(0,10)
>>> plt.xlabel("Predictions for the notMNIST example")
>>> plt.ylabel("Frequency")
>>> plt.show()
请参考以下屏幕截图以获取最终结果:

这些预测是非常不一致的。 如我们所见,2,3,5,6和8到处都是 。 这是我们所期望的,因为基本事实是模型知道的A,不是 10 类(0至9)中的任何一个。 在贝叶斯学习中,预测具有很高的不确定性,因此我们将拒绝我们的预测。 在常规学习中,模型将给出不提供置信度或确定性信息的预测。
胶囊网络
胶囊网络是一种新型的深度神经网络。 它们以类似于 3D 计算机图形的方式处理视觉信息。 它们保留了对象之间的层次关系,而 CNN 无法做到。 尽管 CNN 仍然是大多数计算机视觉任务的首选解决方案,但它们仍面临一些挑战。 让我们谈谈胶囊网络是如何解救的。
卷积神经网络无法做什么
不要误会我的意思-CNN 非常棒,仍然是当今大多数计算机视觉问题的主要解决方案。 通过回顾我们在前几章中研究的内容,CNN 擅长从卷积过滤器激活的像素中检测有效特征,从而模拟人类视觉系统过程图像。 它们在较早的层中检测低级特征(例如边缘和线条),在较后的层中检测高水平特征(例如轮廓和形状)。 但是,它具有根本的局限性-缺乏空间层次结构。
假设我们正在使用 CNN 模型进行人脸检测项目。 训练有素的网络的某些卷积可能识别眼睛,但有些可能是由鼻子,一些耳朵或嘴巴触发的。 只要我们具有所有组件,包括两只眼睛和耳朵,一只鼻子和一只嘴,无论它们的相对位置如何,全连接层都可以识别一张脸。 那就对了! 因此,以下两个图像 A 和 B 都将通过 CNN 人脸检测器:
图片 A:

图片 B:

显然,图像 B 不是脸,因为它的眼睛,鼻子,嘴巴和耳朵遍布各处。 但是,由于 CNN 仅关心某些组件的存在,而不关心这些组件的相对位置及其方向,因此将其分类为人脸。 胶囊网络是由 Geoffrey Hinton 发明的(《胶囊之间的动态路由》)来合并有关组件之间的相对空间关系及其方向的信息。 。
胶囊网络 – 融合方向和相对的空间关系
胶囊网络的发明受到计算机图形学的启发,在计算机图形学中,图像是根据对象的内部几何信息构造的。 此数据存储为代表这些对象的层次结构和相对位置及其方向的几何对象和矩阵的数组。
在 CNN 中,神经元输出标量。 在胶囊网络中,神经元输出一个向量,该向量以下列方式编码所有重要的几何信息:
- 向量的长度表示检测到物体(例如,眼睛,耳朵和鼻子或某种形状)的可能性
- 向量的方向编码对象的方向
以下检测摩天大楼的示例可以帮助您更好地了解向量:

在此简化示例中,两个向量(黑色和蓝色)代表水平矩形和三角形。 我们可以在生成的向量表示图中观察到这一点:存在摩天大楼的左上区域具有最长的向量,并且其方向与摩天大楼的方向匹配; 没有摩天大楼的区域的向量较短,其方向与摩天大楼的方向不同。
这种表示法的优点在于,该模型能够从所有不同的视角检测对象。 假设我们有一个向量,用于检测长度为 0.8 的摩天大楼的水平矩形。 如果我们更改图像中摩天大楼的视点,则向量将随着检测到的对象状态的变化而相应地旋转。 但是,向量的长度保持为 0.8,因为仍然可以检测到对象。 胶囊网络的这种特性称为活动等价性或不变性,而 CNN 不能通过合并来实现。
使用向量而不是比例作为神经元的输出,胶囊网络通过编码从不同层提取的特征之间的层次和几何关系来扩展 CNN。 特征不再是在二维平面上,而是在三维空间内,类似于计算机图形图像。 现在,您可能想知道胶囊如何消耗和产生向量。 让我们比较和总结胶囊中的计算以及下表中传统神经元中的计算:

胶囊不是直接进行加权和,而是首先运行矩阵乘法以获得更高级别的特征。 让我们重用我们的摩天大楼检测示例,并假设我们有一个输入向量u₁用于检测矩形,u₂用于检测三角形。 然后将这两个向量乘以仿射变换矩阵W的相应权重向量。 权重向量可能会编码矩形和摩天大楼之间的相对位置,而另一个权重向量可能会尝试了解三角形和摩天大楼之间关系的方向。 进行仿射变换后,我们可以获得较高级别的特征u_hat[1]和u_hat[2],它们根据三角形和矩形的位置和方向表示摩天大楼的位置和方式。
此后,下一步是执行加权和,这听起来很熟悉。 在传统的神经元中,权重是通过反向传播进行优化和计算的。 但是,在捕获中使用动态路由学习权重。 动态路由的主要思想是使用多数表决来确定最可能的对象应该喜欢什么。 接近真实预测的向量的权重c应具有较高的值,与远离正确预测的向量相对应的权重c应具有较低的值。
最后,胶囊中的激活函数也是新的东西。 回想一下,在传统的神经元中,典型的激活函数包括 Sigmoid,tanh 和 ReLU,其主要目的是增加非线性并重新调整输入。 同样,对于胶囊,激活函数的后半部分将输入向量的长度转换为 1,但不改变其方向; 激活函数的前半部分(称为压缩)进一步缩放长度,使其不等于 1,同时增加了一些非线性。
这就是胶囊中计算的工作方式。 总而言之,胶囊网络采用功能强大的表示形式-向量-对不同层上重要的特征层次信息进行编码,这在使用标量的传统 CNN 中不可用。 最后但并非最不重要的一点是,如果您对自己实现胶囊网络感兴趣,请随时查看有关 Keras 和 CapsLayer 的以下链接:
元学习
元学习是深度学习研究中另一个令人兴奋的趋势。 就像传统学习一样,它不仅仅针对特定任务在庞大的数据集上进行训练。 它试图利用从任务分配中学到的过去经验来模仿人类的学习过程。 即使只有少量训练样本,它也可以实现良好的表现。 但是,传统的深度学习方法无法做到这一点。
深度学习的一大挑战 – 训练数据
您可能已经看到了以下图表,在给定各种训练数据的情况下,将深度学习与传统机器学习算法之间的表现进行了比较:

仅有少量训练数据可用,深度学习算法通常不会比传统机器学习算法起作用或表现更差。 有了足够的数据,深度学习就开始击败传统学习。 随着数据量的增加,它对表现差距的影响也越来越大。 这里的明确信息是,深度学习通常需要足够数量的训练数据才能获得良好的表现。
深度学习之所以需要巨大的数据集,是因为它使用该数据集从头开始训练模型(或模型的某些关键部分)。 您在这里看到问题了吗? 显然,与我们人类不同,传统的深度学习算法无法从少量数据样本中快速学习,而人类则可以利用过去的经验来快速学习。 我们可以从以下两个方面总结一下机器学习(ML)/ 深度学习(DL)与人类:
- 样本效率:深度学习的样本效率低,因为它需要来自各个类别的足够的样本用于分类任务,而对于回归任务则需要足够的变化。 例如,我们通常在每位数数千个样本上训练一个手写数字识别器。
- 可移植性:对于特定任务,深度学习不会从以前的经验中学习,也不会利用从同一任务中学到的知识。 您可能会争辩说,迁移学习就像利用以前的经验一样,但是请记住,迁移的知识是从其他任务中获得的,我们仍然需要使用庞大的数据集来微调模型的关键部分。
那么,我们如何才能使深度学习接近人类的学习方式-学习呢? 当我们没有足够的训练数据时,如何还能有效地学习? 答案是元学习。
元学习 – 学习如何学习
元学习是机器学习中的现代子领域。 它使用有关机器学习实验的元数据来自动解决具有类似属性的新学习问题。 在元学习中,训练仅限于来自单个分布的样本,并且不会因训练规模小而受到损害。 就像孩子们只看过几次之后如何分辨猫和狗一样。 另一方面,元学习超出了一项任务。 它了解有关任务的分布情况,因此它仍可用于从未介绍过的任务。 就像孩子一样,有了从猫和狗那里学到的知识,他们就可以区分老虎和狼,而以前却看不到任何老虎或狼。 简而言之,元学习学习如何学习。 它试图建立在如何解决任务的知识(元数据)上。
在元学习中,模型经过各种任务训练。 每个学习任务都与包含输入特征和目标变量的数据集相关联。 学习的目的是优化模型,以使这些任务的分配损失最小。 请注意,此处的模型表示高级优化器,可更新低级模型的权重。 即,训练高级模型以学习低级模型。 一项任务的一个数据集在整个过程中被视为一个数据样本。
元学习有许多不同的方法,因为元学习仍然是一个宽松术语,是一个相对较新的概念。 最受欢迎的包括:
- 基于模型学习的元学习:这是一个顺序模型,只需几个步骤即可更新其参数。 可以在记忆增强神经网络(MANN)中找到典型的模型学习架构,如下图所示,其外观与 RNN 相似。 在 MANN 中,模型的最后一步将提取要预测的样本。 在此之前,每个步骤都需要获取训练样本,并且样本的标签会在上一步中输入。 有了这种显式的存储缓冲区(也称为一步偏移),网络就可以记住学习经验,而不仅仅是从特征到标签的简单映射。 而且,由于我们可以通过随机选择不同的训练数据点
(X, Y)并按顺序排列它们来轻松生成大量的训练案例,因此不必担心这种训练规模的限制。 下图描述了一个基于模型的元学习示例:

- 基于度量的学习,我们将在下一节中详细讨论。
基于指标的元学习
在有限的训练数据的情况下,我们应该做的是提取最重要的信息,而不要过度这样做。 基于度量的元学习试图实现相同的目标。 它利用度量或距离函数在正确的级别上提取特征。
典型的基于度量的学习模型是连体神经网络。 如下图的架构所示,它由两个相同的网络组成,它们共享相同的权重和参数以分别从两个输入中提取特征:

然后,从连体网络中提取的特征将输入判别器,以确定两个输入数据样本是否属于同一类。 判别器首先计算特征对之间的距离(例如 L1 距离,L2 距离,余弦相似度)。 然后,该距离将通过 Sigmoid 激活传递到全连接层,以产生两个输入来自同一类别的概率。
简而言之,连体网络试图学习有效的特征,以便揭示两个输入之间的关系。 为了帮助更好地了解这个基于度量的网络,我们将实现它,并将其应用于数据量有限的人脸识别项目。
标准的面部识别系统应该能够仅使用系统中该人的几张照片来识别该人的身份。 显然,为了积累足够的训练数据,不能强迫他拍摄数百张照片。 尽管这是一个多类别的分类问题,但在这种情况下,CNN 不能在一个很小的训练集上真正起作用,因为每个类别的样本要多得多。 而且,如果我们采用典型的多类分类路线,则每次新用户加入系统时都必须对模型进行重新训练,这是不切实际的。 幸运的是,连体神经网络擅长处理少样本学习问题。
我们将使用 AT&T 的人脸数据库(也称为 ORL 人脸数据库)的一个例子。 从这里下载数据集并解压缩。 提取的文件夹有 40 个子文件夹,从s1到s40,它们代表 40 个主题。 每个子文件夹包含 10 个图像文件1.pgm,2.pgm,...,10.pgm。 因此,总共只有来自 40 个个体的 400 个图像样本。 同样,要使典型的 CNN 分类器仅使用 400 个训练样本几乎是不可能的。
每个图像的尺寸为92 * 112,并具有 256 个灰度等级。 图像文件为 PGM 格式。 让我们开始使用PIL包阅读并显示一个样本。 PIL代表 Python Imaging Library,如果尚未安装,则可以使用以下命令进行安装:
pip install Pillow
现在,阅读并显示一个图像样本:
>>> from PIL import Image
>>> img = Image.open('./orl_faces/s1/1.pgm')
>>> print(img.size)
(92, 112)
>>> img.show()

首先,我们编写一个函数以将所有图像及其主题 ID 加载到字典中,其中键是主题 ID,值是 10 张图像的列表:
>>> image_size = [92, 112, 1]
>>> def load_images_ids(path='./orl_faces'):
... id_image = {}
... for id in range(1, 41):
... id_image[id] = []
... for image_id in range(1, 11):
... img = Image.open('{}/s{}/{}.pgm'.format(
path, id, image_id))
... img = np.array(img).reshape(image_size)
... id_image[id].append(img)
... return id_image
注意,每个图像矩阵都被重塑为尺寸[92, 112, 1]。
现在,加载所有图像及其主题 ID:
>>> id_image = load_images_ids()
我们仅用 400 个样本处理该项目的方法是将其从多类分类转换为二分类问题。 我们不是直接预测图像属于 40 个对象中的哪个,而是预测图像属于各个对象的概率,而概率最高的那个将成为最终结果。 图像属于对象的概率是根据图像与对象样本之间的距离得出的。 下图说明了预测过程:

给定来自未知主题的图像,我们将其分别与来自三个主题的每个图像进行比较(假设系统中只有三个主题)。 我们将每对图像馈送到连体神经网络并获得概率分数。 具有对象 1 的图像的图像对获得最高概率。 因此,查询图像很可能来自主题 1。使用这种策略,我们将能够从 400 张图像中生成更多的训练样本(或者具体来说,是成对的),以适合连体神经网络。 。 例如,我们最多可以为每个主题创建10 * 9/2 = 45个唯一阳性对,或者对于所有 40 个主题总共创建45 * 40 = 1800个唯一阳性对。 类似地,我们可以为每对主题形成10 * 10 = 100个唯一的阴性对,或者对于所有可能的主题对总共形成100 * (40 * 39/2) = 78,000个唯一的阴性对。 最后,我们可以获得数百个训练样本,这确实提高了人脸识别表现。
现在,让我们通过执行以下步骤在 Keras 中构建连体神经网络:
- 首先,导入所需的所有模块:
>>> from keras import backend as K
>>> from keras.layers import Activation
>>> from keras.layers import Input, Lambda, Dense, Dropout,
Convolution2D, MaxPooling2D, Flatten
>>> from keras.models import Sequential, Model
- 我们的连体网络由卷积层组成,因为我们正在处理图像,最大池化层和全连接层。 这种架构的连体网络被称为卷积连体神经网络:
>>> def siamese_network():
... seq = Sequential()
... nb_filter = 16
... kernel_size = 6
... # Convolution layer
... seq.add(Convolution2D(nb_filter, (kernel_size, kernel_size), input_shape=image_size, border_mode='valid'))
... seq.add(Activation('relu'))
... seq.add(MaxPooling2D(pool_size=(2, 2)))
... seq.add(Dropout(.25))
... # flatten
... seq.add(Flatten())
... seq.add(Dense(50, activation='relu'))
... seq.add(Dropout(0.1))
... return seq
- 然后,我们可以定义输入占位符,两个图像,并将它们传递给连体网络:
>>> img_1 = Input(shape=image_size)
>>> img_2 = Input(shape=image_size)
>>> base_network = siamese_network()
>>> feature_1 = base_network(img_1)
>>> feature_2 = base_network(img_2)
- 然后,我们需要计算这两个结果特征向量之间的距离(我们使用 L1 距离),并将结果映射到概率输出:
>>> distance_function = lambda x: K.abs(x[0] - x[1])
>>> distance = Lambda(distance_function, output_shape=
lambda x: x[0])([feature_1, feature_2])
>>> prediction = Dense(1, activation='sigmoid')(distance)
- 最后,我们需要用输入和输出占位符包装 Keras 模型,并使用基于 Adam 的优化器和交叉熵作为损失函数对其进行编译:
>>> model = Model(input=[img_1, img_2], output=prediction)
>>> from keras.losses import binary_crossentropy
>>> from keras.optimizers import Adam
>>> optimizer = Adam(lr=0.001)
>>> model.compile(loss=binary_crossentropy, optimizer=optimizer)
- 现在该模型已准备好进行训练,我们需要使用以下函数构建训练集,其中从各个受试者中随机选择正对,从两个不同受试者中随机选择负对。 正负比率保持在 50:50:
>>> np.random.seed(42)
>>> def gen_train_data(n, id_image):
... X_1, X_2 = [], []
... Y = [1] * (n // 2) + [0] * (n // 2)
... # generate positive samples
... ids = np.random.choice(range(1, 41), n // 2)
... for id in ids:
... two_image_ids = np.random.choice(range(10), 2, False)
... X_1.append(id_image[id][two_image_ids[0]])
... X_2.append(id_image[id][two_image_ids[1]])
... # generate negative samples, by randomly selecting two
images from two ids
... for _ in range(n // 2):
... two_ids = np.random.choice(range(1, 41), 2, False)
... two_image_ids = np.random.randint(0, 10, 2)
... X_1.append(id_image[two_ids[0]][two_image_ids[0]])
... X_2.append(id_image[two_ids[1]][two_image_ids[1]])
... X_1 = np.array(X_1).reshape([n] + image_size) / 255
... X_2 = np.array(X_2).reshape([n] + image_size) / 255
... Y = np.array(Y)
... return [X_1, X_2], Y
我们获得 8,000 个训练样本:
>>> X_train, Y_train = gen_train_data(8000, id_image)
- 现在,我们可以将模型拟合到训练集上,具有 10 个周期和 10% 的数据拆分以进行验证:
>>> epochs = 10
>>> model.fit(X_train, Y_train, validation_split=0.1, batch_size=64, verbose=1, epochs=epochs)
Epoch 1/10
7200/7200 [==============================] - 71s 10ms/step - loss: 0.5168 - val_loss: 0.3305
Epoch 2/10
7200/7200 [==============================] - 62s 9ms/step - loss: 0.3259 - val_loss: 0.2210
Epoch 3/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.2467 - val_loss: 0.2219
Epoch 4/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.2089 - val_loss: 0.1669
Epoch 5/10
7200/7200 [==============================] - 60s 8ms/step - loss: 0.1920 - val_loss: 0.1521
Epoch 6/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.1534 - val_loss: 0.1441
Epoch 7/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.1509 - val_loss: 0.1124
Epoch 8/10
7200/7200 [==============================] - 58s 8ms/step - loss: 0.1408 - val_loss: 0.1323
Epoch 9/10
7200/7200 [==============================] - 59s 8ms/step - loss: 0.1281 - val_loss: 0.1360
Epoch 10/10
7200/7200 [==============================] - 58s 8ms/step - loss: 0.1215 - val_loss: 0.1736
训练和验证损失看起来都不错,但是我们仍然需要在真实环境中进行测试。 假设有n个受试者,则在一个测试用例中将有n个样本。 在这些n个样本中,一个样本是地面真实对,由查询图像和同一主题的图像组成; 其他n-1个样本由查询图像和其他主题的图像组成。 例如,假设我们有四个主题,A,B,C和D,及其图像a,b,c和d。 给定未知图像x,一个测试案例可以是[x, a],[x, b],[x, c]和[x, d]。 在元学习中,给定n唯一类,这样的任务称为四路学习或 N 路学习。
首先,让我们定义一个生成测试用例的函数:
>>> def gen_test_case(n_way):
... ids = np.random.choice(range(1, 41), n_way)
... id_1 = ids[0]
... image_1 = np.random.randint(0, 10, 1)[0]
... image_2 = np.random.randint(image_1 + 1, 9 + image_1, 1)[0] % 10
... X_1 = [id_image[id_1][image_1]]
... X_2 = [id_image[id_1][image_2]]
... for id_2 in ids[1:]:
... image_2 = np.random.randint(0, 10, 1)[0]
... X_1.append(id_image[id_1][image_1])
... X_2.append(id_image[id_2][image_2])
... X_1 = np.array(X_1).reshape([n_way] + image_size) / 255
... X_2 = np.array(X_2).reshape([n_way] + image_size) / 255
... return [X_1, X_2]
请注意,为方便起见,我们将地面真值样本放在第一位,而错误的样本放在其余部分。
我们将以 4、9、16、25、36 和 40 方式的学习方式评估模型。 在每种学习方式中,我们重复实验 1,000 次并计算准确率。 为了进行比较,我们使用 K 最近邻(KNN)作为基准模型来计算识别表现,我们定义如下:
>>> def knn(X):
... distances = [np.linalg.norm(x_1 - x_2)
for x_1, x_2 in zip(X[0], X[1])]
... pred = np.argmin(distances)
... return pred
最后,我们开始测试:
>>> n_experiment = 1000
>>> for n_way in [4, 9, 16, 25, 36, 40]:
... n_correct_snn = 0
... n_correct_knn = 0
... for _ in range(n_experiment):
... X_test = gen_test_case(n_way)
... pred = model.predict(X_test)
... pred_id = np.argmax(pred)
... if pred_id == 0:
... n_correct_snn += 1
... if knn(X_test) == 0:
... n_correct_knn += 1
... print('{}-way few shot learning accuracy: {}'.format(
n_way, n_correct_snn / n_experiment))
... print('Baseline accuracy with knn: {}\n'.format(
n_correct_knn / n_experiment))
4-way few shot learning accuracy: 0.963
Baseline accuracy with knn: 0.876
9-way few shot learning accuracy: 0.931
Baseline accuracy with knn: 0.752
16-way few shot learning accuracy: 0.845
Baseline accuracy with knn: 0.663
25-way few shot learning accuracy: 0.767
Baseline accuracy with knn: 0.55
36-way few shot learning accuracy: 0.679
Baseline accuracy with knn: 0.497
40-way few shot learning accuracy: 0.659
Baseline accuracy with knn: 0.478
我们的连体元学习模型大大优于基线。 我们还可以观察到,n越高,要考虑的类别越多,因此预测正确的类别越困难。
总结
这是我们 DL 架构的最后一站,也是 DL 历程中的新趋势。 在本章中,我们了解到贝叶斯深度学习结合了贝叶斯学习和深度学习的优点。 它对不确定性进行建模,以某种方式告诉我们我们对预测的信任程度。 胶囊网络捕获对象之间的东方和相对空间关系。 我们相信,它们将来会变得更加成熟和流行。
元学习(即学习如何学习)是 DL 研究社区中令人兴奋的主题。 我们已经实现了元学习模型,即带有 Keras 的连体神经网络,并将其应用于人脸识别问题。 实际上,DL 中还有许多其他有趣的事情值得研究,例如深度强化学习,主动学习和自动机器学习。 在阅读本书时,您还发现其他趋势吗?
第 1 节:深度学习的元素
在本节中,您将概述使用 Python 进行的深度学习,还将了解深度前馈网络,玻尔兹曼机和自编码器的架构。 我们还将练习基于 DFN 的示例以及玻尔兹曼机和自编码器的应用,以及基于带 Python 的 DL 框架/库的具体示例及其基准。
本节包括以下章节:
- “第 1 章”,“深度学习入门”
- “第 2 章”,“深度前馈网络”
- “第 3 章”,“受限制的玻尔兹曼机和自编码器”
第 2 节:卷积神经网络
在本节中,我们将学习一类用于图像的深度学习网络,称为卷积神经网络(CNN),以及为什么 CNN 比深度前馈网络更好 。 然后,我们将研究如何减少深度学习网络所需的计算成本,并看到移动神经网络不过是经过修改以具有更少参数和更少内存的 CNN。
本节将介绍以下章节:
- “第 4 章”,“CNN 架构”
- “第 5 章”,“移动神经网络和 CNN”
第 3 节:序列建模
在本节中,我们将学习两个重要的 DL 模型以及这些模型的演化路径。 我们将通过一些示例探索它们的架构和各种工程最佳实践。
本节将涵盖以下章节:
- “第 6 章”,“循环神经网络”
第 4 节:生成对抗网络(GAN)
在本章中,您将学习 GAN 及其基准。 连同 GAN 模型的演变路径,将通过工业示例来说明架构和工程最佳实践。 我们还将探讨如何仅生成一个输出,一个输出是否可以是一个序列以及它是否仅适用于一个输入元素。
本节将介绍以下章节:
- “第 7 章”,“生成对抗网络”
第 5 节:深度学习和高级人工智能的未来
在本节中,我们想谈谈一些深度学习方面的想法,这些想法我们今年很有影响力,并且将来会更加突出。
本节将介绍以下章节:
- “第 8 章”,“深度学习的新趋势”









浙公网安备 33010602011771号