视觉系统的深度学习-全-
视觉系统的深度学习(全)
原文:Deep Learning for Vision Systems
译者:飞龙
前置内容
前言
两年前,我决定写一本书,从直观的角度教授计算机视觉的深度学习。我的目标是开发一个全面资源,将学习者从仅了解机器学习基础知识带到构建他们可以应用于解决复杂计算机视觉问题的先进深度学习算法。
问题 : 简而言之,到目前为止,还没有任何书籍以我想要学习计算机视觉深度学习的方式教授。作为一名初学者机器学习工程师,我希望读一本书,从 A 点到 Z 点都能涵盖。我计划专门从事构建现代计算机视觉应用,并希望有一个单一的资源可以教会我完成以下两件事:1)使用神经网络构建端到端的计算机视觉应用,2)能够舒适地阅读和实施研究论文,以跟上最新的行业进展。
我发现自己需要在在线课程、博客、论文和 YouTube 视频之间跳转,以为自己创建一个全面的课程。试图在更深的层次上理解底层发生的事情是具有挑战性的:不仅是一个基本理解,还包括概念和理论在数学上的意义。很难找到一本全面资源,它(在水平方向上)涵盖了我在处理复杂计算机视觉应用时需要学习的最重要主题,同时也足够深入(在垂直方向上)以帮助我理解使魔法生效的数学原理。
作为一名初学者,我搜索了但找不到任何能满足这些需求的东西。所以现在我已经写了它。我的目标不仅是写一本在我开始时想要学习的内容的书,而且还要提高你自学的能力。
我的解决方案是一本全面深入的书:
-
水平方向 -- 这本书解释了工程师需要学习的大部分主题,以构建生产就绪的计算机视觉应用,从神经网络及其工作原理到不同类型的神经网络架构以及如何训练、评估和调整网络。
-
垂直方向 -- 这本书比代码深入一层或两层,直观(且温和)地解释了底层的数学原理,以使你能够舒适地阅读和实施研究论文,甚至发明你自己的技术。
在写作的时候,我认为这是唯一一种以这种方式教授视觉系统深度学习的资源。无论你是想找一份计算机视觉工程师的工作,想要更深入地理解计算机视觉中的高级神经网络算法,还是想要构建你的产品或初创公司,我写这本书是考虑到你的。我希望你喜欢它。
致谢
这本书工作量很大。不,这样说更准确:工作量真的很大!但我希望你会觉得它很有价值。有很多人在我写作过程中给予了我帮助。
我要感谢 Manning 出版社的同事们,是你们让这本书成为可能:出版人 Marjan Bace 以及编辑和制作团队的所有人,包括 Jennifer Stout、Tiffany Taylor、Lori Weidert、Katie Tennant 以及许多幕后工作的同事们。
我要向由 Alain Couniot 领导的同行评审团队表示衷心的感谢——Al Krinker、Albert Choy、Alessandro Campeis、Bojan Djurkovic、Burhan ul haq、David Fombella Pombal、Ishan Khurana、Ita Cirovic Donev、Jason Coleman、Juan Gabriel Bono、Juan José Durillo Barrionuevo、Michele Adduci、Millad Dagdoni、Peter Hraber、Richard Vaughan、Rohit Agarwal、Tony Holdroyd、Tymoteusz Wolodzko 和 Will Fuger——以及那些在论坛上积极提供反馈的读者们。他们的贡献包括纠正错别字、代码错误和技术错误,以及提出有价值的主题建议。每次通过评审流程和每次通过论坛主题实施的反馈都塑造和塑造了这本书的最终版本。
最后,我要感谢整个 Synapse Technology 团队。你们创造了一些非常酷的东西。感谢 Simanta Guatam、Aleksandr Patsekin、Jay Patel 以及其他人回答我的问题,并为这本书出谋划策。
关于这本书
适合阅读这本书的人
如果你熟悉基本的机器学习框架,能够在 Python 中编程,并且想要学习如何构建和训练高级、生产就绪的神经网络来解决复杂的计算机视觉问题,这本书是为你写的。这本书是为那些具有中级 Python 经验和基本机器学习理解,希望探索深度神经网络训练并学习如何将深度学习应用于解决计算机视觉问题的读者所写。
当我开始写这本书时,我的主要目标是这样的:“我想写一本书来提升读者的技能,而不是仅仅传授他们内容。”为了实现这个目标,我必须关注两个主要原则:
-
教你如何学习。我不想读一本只是罗列科学事实的书。我可以在互联网上免费获取这些信息。如果我读一本书,我希望读完之后我的技能有所增长,这样我就可以进一步研究这个主题。我想学习如何思考所提出的解决方案,并提出自己的见解。
-
深入探究。如果我成功地满足了第一个原则,那么这个原则就变得容易了。如果你学会了如何学习新概念,那么我就能够深入挖掘,而不必担心你可能落后。这本书不会回避学习的数学部分,因为理解数学方程式将赋予你在人工智能世界中最优秀的技能:阅读研究论文、比较创新以及在你自己的问题中实施新概念的正确决策能力。但我承诺只介绍你需要的数学概念,并承诺以一种不会打断你理解概念(如果你更喜欢没有数学部分)的方式呈现它们。
本书是如何组织的:路线图
本书分为三个部分。第一部分详细解释深度学习,作为剩余主题的基础。我强烈建议你不要跳过这一部分,因为它深入探讨了神经网络组件和定义,并解释了理解神经网络内部工作所需的所有概念。阅读第一部分后,你可以直接跳到剩余章节中感兴趣的主题。第二部分解释深度学习技术来解决对象分类和检测问题,第三部分解释深度学习技术来生成图像和视觉嵌入。在几个章节中,实际项目实现了所讨论的主题。
关于代码
本书的所有代码示例都使用免费下载的开源框架。我们将使用 Python、Tensorflow、Keras 和 OpenCV。附录 A 将指导你完成完整的设置。我还建议如果你想在你的机器上运行本书的项目,最好能访问到一个 GPU,因为第六章到第十章包含更复杂的项目,用于训练深度网络,这些项目在普通 CPU 上需要很长时间。另一个选择是使用像 Google Colab 这样的免费或付费的云环境。
源代码示例既出现在编号列表中,也出现在正常文本中。在两种情况下,源代码都以fixed-width font like this这样的固定宽度字体格式化,以将其与普通文本区分开来。有时代码也会被**in** **bold**加粗,以突出显示与章节中先前步骤不同的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已经被重新格式化;我们添加了换行符并重新调整了缩进,以适应书籍中可用的页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中移除。许多列表旁边都有代码注释,突出显示重要概念。
本书示例的代码可以从 Manning 网站 www.manning.com/books/deep-learning-for-vision-systems 和 GitHub github.com/moelgendy/deep_learning_for_vision_systems 下载。
liveBook 讨论论坛
购买《视觉系统深度学习》包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在论坛上对本书发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问 livebook.manning.com/#!/book/deep-learning-for-vision-systems/discussion。您还可以在 livebook.manning.com/#!/discussion 了解更多关于 Manning 论坛和行为准则的信息。
Manning 对读者的承诺是提供一个平台,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者在论坛上的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要本书有售,论坛和先前讨论的存档将可通过出版社网站访问。
关于作者
穆罕默德·埃尔根迪是 Rakuten 工程副总裁,在那里他领导着其 AI 平台和产品的开发。此前,他曾在 Synapse Technology 担任工程主管,构建专有计算机视觉应用程序,以在全球安全检查站检测威胁。在亚马逊,穆罕默德建立了中央 AI 团队,为 AWS 和 Amazon Go 等亚马逊工程团队提供深度学习智库服务。他还开发了亚马逊机器大学计算机视觉深度学习课程。穆罕默德经常在亚马逊 DevCon、O'Reilly 的 AI 会议和谷歌的 I/O 等人工智能会议上发表演讲。
关于封面插图
《视觉系统深度学习》封面上的插图描绘了伊本·海瑟姆,一位阿拉伯数学家、天文学家和物理学家,他因对光学和视觉感知原理的重大贡献而常被称为“现代光学之父”。该插图是根据约翰内斯·海维利乌斯作品《月相学》十五世纪版扉页修改的。
在他的著作《光学之书》(Kitab al-Manazir)中,伊本·海瑟姆是第一个解释视觉发生时,光线从物体反射然后进入眼睛的现象的人。他也是第一个证明视觉发生在大脑而不是眼睛中的人——许多这些概念都是现代视觉系统的核心。当你阅读这本书的第一章时,你会看到这种关联。
在这个领域工作和创新的过程中,伊本·海瑟姆对我来说是一个巨大的灵感来源。通过在这本书的封面上纪念他的记忆,我希望激励同行从业者,我们的工作可以存活并激励他人数千年。
第一部分. 深度学习基础
计算机视觉是一个技术领域,得益于过去几年在人工智能和深度学习方面取得的巨大进步而迅速发展。神经网络现在帮助自动驾驶汽车在周围导航,避开其他车辆、行人和其他障碍物;推荐代理在建议类似其他产品的产品方面变得越来越聪明。面部识别技术也在变得更加复杂,使得智能手机在解锁手机或门之前能够识别面部。像这些以及其他计算机视觉应用已经成为我们日常生活中的必备品。然而,通过超越简单对象的识别,深度学习赋予了计算机想象和创造新事物的能力,如之前不存在过的艺术、新的人类面孔和其他物体。本书的第一部分探讨了深度学习的基础、不同形式的神经网络以及通过超参数调整等概念进一步发展的结构化项目。
1 欢迎来到计算机视觉
本章涵盖
-
视觉系统组件
-
计算机视觉应用
-
理解计算机视觉流程
-
预处理图像和提取特征
-
使用分类学习算法
嘿!我非常激动你能在这里。你做出了一个伟大的决定--掌握深度学习(DL)和计算机视觉(CV)。时机再合适不过了。得益于近年来人工智能和深度学习的巨大进步,CV 领域正在快速发展。神经网络现在使得自动驾驶汽车能够确定其他车辆和行人的位置,并绕过它们。我们越来越多地在家中的智能设备中使用 CV 应用程序--从安全摄像头到门锁。CV 还使面部识别工作得比以往任何时候都要好:智能手机可以识别面部以解锁,智能锁可以解锁门。如果在未来某个时候,你的沙发或电视能够识别你家中特定的人并根据他们的个人喜好做出反应,我并不会感到惊讶。这不仅仅是识别物体--DL 已经赋予了计算机想象和创造新事物(如艺术品;新物体;甚至独特、逼真的面部)的能力。
我对深度学习在计算机视觉中的兴奋,以及是什么吸引我进入这个领域的主要原因,是 AI 研究的快速进步正在使每天都能在不同行业中构建新的应用,这在几年前是不可能的。CV 研究的无限可能性激发了我写这本书的灵感。通过学习这些工具,也许你将能够发明新的产品和应用。即使你最终没有从事 CV 工作,你也会发现这本书中的许多概念对你的某些 DL 算法和架构非常有用。那是因为虽然主要关注 CV 应用,但本书涵盖了最重要的 DL 架构,如人工神经网络(ANNs)、卷积网络(CNNs)、生成对抗网络(GANs)、迁移学习以及更多,这些可以转移到其他领域,如自然语言处理(NLP)和语音用户界面(VUIs)。
本章的高级布局如下:
-
计算机视觉直觉 -- 我们将从视觉感知直觉开始,学习人类与机器视觉系统之间的相似性。我们将探讨视觉系统有两个主要组件:一个感知设备和解释设备。每个设备都针对完成特定任务而定制。
-
计算机视觉应用 -- 在这里,我们将从宏观角度审视不同计算机视觉应用中使用的深度学习算法。然后,我们将讨论不同生物的视觉问题。
-
计算机视觉流程--最后,我们将聚焦于视觉系统的第二个组成部分:解释设备。我们将逐步讲解视觉系统在处理和解析图像数据时所采取的步骤序列。这些步骤被称为计算机视觉流程。CV 流程由四个主要步骤组成:图像输入、图像预处理、特征提取以及用于解释图像的机器学习模型。我们将讨论图像形成以及计算机是如何“看”图像的。然后,我们将快速回顾图像处理技术和特征提取。
准备好了吗?让我们开始吧!
1.1 计算机视觉
任何人工智能系统的核心概念是它能够感知其环境并根据其感知采取行动。计算机视觉关注的是视觉感知部分:它是通过构建世界的物理模型,使人工智能系统能够采取适当的行动,通过图像和视频感知和理解世界的一门科学。对于人类来说,视觉只是感知的一个方面。我们通过视觉感知世界,但也通过声音、气味以及我们的其他感官。对于人工智能系统来说也是如此--视觉只是理解世界的一种方式。根据你正在构建的应用,你选择最能捕捉世界的传感器。
1.1.1 视觉感知是什么?
视觉感知在最基本的意义上是通过视觉或视觉输入观察模式和对象的行为。例如,对于自动驾驶汽车来说,视觉感知意味着理解周围的对象及其具体细节--例如行人,或者车辆是否需要保持在特定车道中央--以及检测交通标志并理解其含义。这就是为什么“感知”这个词是定义的一部分。我们不仅仅是想要捕捉周围的环境。我们试图构建能够通过视觉输入真正理解该环境的系统。
1.1.2 视觉系统
在过去的几十年里,传统的图像处理技术被认为是计算机视觉系统,但这并不完全准确。一个处理图像的机器与理解图像中发生的事情的机器完全不同,这并不是一个简单任务。现在,图像处理只是更大、更复杂系统的一部分,该系统旨在解释图像内容。
人类视觉系统
在最高层面上,视觉系统对于人类、动物、昆虫以及大多数生物体来说几乎是相同的。它们由一个传感器或眼睛来捕捉图像,以及一个大脑来处理和解释图像。系统随后根据从图像中提取的数据输出对图像组件的预测(图 1.1)。

图 1.1 人类视觉系统利用眼睛和大脑来感知和解释图像。
让我们看看人类视觉系统是如何工作的。假设我们想要解释图 1.1 中的狗的图像。我们看它,并直接理解图像由一群狗(具体来说是三只)组成。对我们来说,在这个图像中分类和检测物体是非常自然的,因为我们多年来一直在接受识别狗的训练。
假设有人第一次给你看一张狗的图片--你肯定不知道这是什么。然后他们告诉你这是一只狗。经过几次这样的实验后,你将接受训练来识别狗。现在,在接下来的练习中,他们给你看一张马的图片。当你看这张图片时,你的大脑开始分析物体特征:嗯嗯,它有四条腿,长脸,长耳朵。它可能是一只狗吗?“错误:这是一匹马,”你被告知。然后你的大脑在其算法中调整了一些参数来学习狗和马之间的差异。恭喜!你刚刚训练了你的大脑来分类狗和马。你能把更多的动物加到等式中,比如猫、老虎、猎豹等等吗?当然可以。你可以训练你的大脑来识别几乎任何东西。对计算机来说也是如此。你可以训练机器来学习和识别物体,但人类比机器更直观。你只需要几幅图像就能学会识别大多数物体,而机器则需要成千上万,甚至在更复杂的情况下,数百万个图像样本来学习识别物体。
机器学习的视角
让我们从机器学习的角度来回顾之前的例子:
-
你通过观察几个标注为狗的图像的例子来学习识别狗。这种方法被称为监督学习。
-
标注数据是指你已经知道目标答案的数据。你被展示了一幅狗的样本图像,并被告知这是一只狗。你的大脑学会了将你看到的特征与这个标签“狗”相关联。
-
然后,你被展示了一个不同的物体,一匹马,并被要求识别它。起初,你的大脑认为它是一只狗,因为你之前没有见过马,你的大脑将马的特征与狗的特征混淆了。当你被告知你的预测是错误的时,你的大脑调整了它的参数来学习马的特征。“是的,两者都有四条腿,但马的腿更长。更长的腿意味着这是一匹马。”我们可以多次运行这个实验,直到大脑不再犯错误。这被称为通过试错进行训练。
人工智能视觉系统
科学家们受到人类视觉系统的启发,近年来在用机器复制视觉能力方面做了惊人的工作。为了模仿人类视觉系统,我们需要相同的两个主要组件:一个感知设备来模仿眼睛的功能,以及一个强大的算法来模仿大脑在解释和分类图像内容时的功能(图 1.2)。

图 1.2 计算机视觉系统的组成部分包括一个感知设备和解释设备。
1.1.3 感知设备
视觉系统旨在完成特定任务。设计的一个重要方面是选择最佳的感应设备来捕捉特定环境的周围环境,无论是摄像头、雷达、X 射线、CT 扫描、激光雷达,还是提供完整环境场景以完成任务的设备组合。
让我们再次看看自动驾驶汽车(AV)的例子。AV 视觉系统的主要目标是让汽车能够理解其周围的环境,并安全、及时地从 A 点移动到 B 点。为了实现这一目标,车辆配备了包括摄像头和传感器在内的组合设备,这些设备可以检测到 360 度的运动——行人、骑自行车的人、车辆、道路施工和其他物体——从最远三个足球场外的地方。
这里有一些通常用于自动驾驶汽车中感知周围区域的感应设备:
-
激光雷达,一种类似雷达的技术,使用不可见的光脉冲来创建周围区域的高分辨率 3D 地图。
-
照相机可以看到街牌和路面标记,但不能测量距离。
-
雷达可以测量距离和速度,但不能看到细节。
医学诊断应用使用 X 射线或 CT 扫描作为感应设备。或者,你可能需要使用其他类型的雷达来捕捉农业视觉系统的景观。有各种各样的视觉系统,每个系统都设计来执行特定的任务。设计视觉系统的第一步是确定它们是为完成什么任务而构建的。在设计端到端视觉系统时,这一点需要牢记在心。
识别图像
动物、人类和昆虫都使用眼睛作为感应设备。但并非所有眼睛的结构、输出图像质量和分辨率都相同。它们是根据生物体的特定需求定制的。例如,蜜蜂和许多其他昆虫都有复眼,由多个透镜组成(单个复眼中多达 30,000 个透镜)。复眼分辨率低,这使得它们在远距离识别物体方面不太擅长。但它们对运动非常敏感,这对于高速飞行时的生存至关重要。蜜蜂不需要高分辨率的图像。它们的视觉系统被构建成能够在快速飞行时捕捉到最小的运动。

复眼分辨率低,但对运动敏感。
1.1.4 解释设备
计算机视觉算法通常用作解释设备。解释器是视觉系统的“大脑”。其作用是从感应设备获取输出图像,学习特征和模式以识别物体。因此,我们需要构建一个大脑。简单!科学家们受到我们大脑工作方式的启发,试图逆向工程中枢神经系统,以获得有关如何构建人工大脑的见解。因此,人工神经网络(ANNs)应运而生(图 1.3)。

图 1.3 生物神经元与人工系统之间的相似性
在图 1.3 中,我们可以看到生物神经元与人工系统之间的类比。两者都包含一个主要处理元素,即神经元,它有输入信号(x[1],x[2],...,x[n])和一个输出。
生物神经元的 学习行为启发了科学家们创建一个相互连接的神经元网络。模仿人类大脑中信息处理的方式,当足够多的输入信号被激活时,每个人工神经元会向它所连接的所有神经元发送信号。因此,神经元在个体层面上具有非常简单的机制(你将在下一章中看到);但是当你有成千上万的这些神经元堆叠在层中并相互连接时,每个神经元都连接到成千上万的其它神经元,从而产生学习行为。构建多层神经网络被称为深度学习(图 1.4)。

图 1.4 深度学习涉及网络中的神经元层。
深度学习方法通过数据在神经元层中的连续变换来学习表示。在这本书中,我们将探讨不同的深度学习架构,例如人工神经网络和卷积神经网络,以及它们在计算机视觉应用中的使用。
机器学习能否比人类大脑实现更好的性能?
嗯,如果你在 10 年前问我这个问题,我可能会说不可能,机器无法超越人类的准确率。但让我们看看以下两个场景:
-
假设你被给了一本包含 10,000 张狗的照片的书,这些照片按品种分类,并要求你学习每种品种的特性。你需要在 10,000 张照片中学习 130 种品种需要多长时间?如果你被要求对 100 张狗的照片进行测试,并基于你所学的内容进行标记,那么在这 100 张照片中,你能正确标记多少张?嗯,经过几小时训练的神经网络可以达到超过 95%的准确率。
-
在创作方面,神经网络可以研究特定艺术品笔触、色彩和阴影中的模式。基于这种分析,它可以将原始艺术作品的风格转移到新图像中,并在几秒钟内创建出新的原创艺术品。
最近人工智能和深度学习的发展使得机器在许多图像分类和目标检测应用中超越了人类的视觉能力,并且这种能力正在迅速扩展到许多其他应用。但不要仅凭我的话,在下一节中,我们将讨论一些使用深度学习技术的最流行的计算机视觉应用。
1.2 计算机视觉的应用
几十年前,计算机开始能够在图像中识别人脸,但现在人工智能系统正在与计算机在照片和视频中分类对象的能力相媲美。得益于计算能力和可用数据量的显著进步,人工智能和深度学习已经在许多复杂的视觉感知任务上实现了超越人类的表现,如图像搜索和字幕、图像和视频分类以及物体检测。此外,深度神经网络不仅限于计算机视觉任务:它们在自然语言处理和语音用户界面任务中也取得了成功。在这本书中,我们将重点关注应用于计算机视觉任务的视觉应用。
深度学习被用于许多计算机视觉应用中,以识别对象及其行为。在本节中,我不会尝试列出所有现有的计算机视觉应用。那需要一本书的篇幅。相反,我将为您提供一个鸟瞰图,展示一些最受欢迎的深度学习算法及其在不同行业中的可能应用。这些行业包括自动驾驶汽车、无人机、机器人、店内摄像头和能够检测早期肺癌的医疗诊断扫描仪。
1.2.1 图像分类
图像分类是将图像分配到预定义类别集合中的标签的任务。卷积神经网络是一种在处理和分类图像方面真正发光的神经网络类型,它在许多不同的应用中表现出色:
-
肺癌诊断 -- 肺癌是一个日益严重的问题。肺癌非常危险的主要原因是在诊断时通常处于中期或晚期。在诊断肺癌时,医生通常用眼睛检查 CT 扫描图像,寻找肺中的小结节。在早期阶段,结节通常非常小,难以发现。几家计算机视觉公司决定利用深度学习技术来应对这一挑战。
几乎所有的肺癌都是从一个小结节开始的,这些结节以医生需要多年时间才能学会识别的各种形状出现。医生在识别中等大小和大结节,如 6-10 毫米的结节方面非常擅长。但当结节为 4 毫米或更小时,有时医生难以识别它们。深度学习网络,特别是卷积神经网络,现在能够自动从 X 射线和 CT 扫描图像中学习这些特征,并在它们变得致命之前早期检测到小结节(图 1.5)。
![图片]()
图 1.5 现在的视觉系统能够从 X 射线图像中学习模式,以识别早期发展阶段的肿瘤。
-
交通标志识别 -- 传统上,标准计算机视觉方法被用于检测和分类交通标志,但这种方法需要耗时的人工工作来手工制作图像中的重要特征。相反,通过将深度学习应用于这个问题,我们可以创建一个可靠地分类交通标志的模型,该模型通过自身学习识别最合适的特征(图 1.6)。
![]()
图 1.6 视觉系统可以以非常高的性能检测交通标志。
注意:越来越多的图像分类任务正在使用卷积神经网络得到解决。由于它们的高识别率和快速执行,CNNs 极大地提升了大多数计算机视觉任务,无论是现有的还是新的。就像癌症诊断和交通标志的例子一样,你可以将成千上万的图像输入到 CNN 中,将它们标注成你想要的任意多类。其他图像分类的例子包括识别人物和物体、区分不同的动物(如猫、狗和马)、不同品种的动物、适合农业的土地类型等等。简而言之,如果你有一组标注好的图像,卷积网络可以将它们分类到一组预定义的类别中。
1.2.2 物体检测与定位
图像分类问题是 CNNs 最基本的应用。在这些问题中,每张图像只包含一个物体,我们的任务是识别它。但如果我们希望达到人类水平的理解,我们必须增加这些网络的复杂性,以便它们能够识别图像中的多个物体及其位置。为此,我们可以构建 YOLO(你只需看一次)、SSD(单次检测器)和 Faster R-CNN 等目标检测系统,这些系统不仅能够对图像进行分类,还能够定位和检测包含多个物体的图像中的每个物体。这些深度学习系统可以观察一张图像,将其分解成更小的区域,并为每个区域标注一个类别,以便在给定的图像中定位和标注可变数量的物体(图 1.7)。你可以想象,这样的任务对于自动驾驶系统等应用来说是基本的前提条件。

图 1.7 深度学习系统可以在图像中分割物体。
1.2.3 生成艺术(风格迁移)
神经风格迁移是计算机视觉中最有趣的 应用之一,它用于将一种图像的风格转移到另一种图像上。风格迁移的基本思想是这样的:你取一张图像——比如说,一座城市的图像——然后为这张图像应用一种艺术风格——比如说,文森特·梵高的《星夜》——输出与原始图像相同的城市,但看起来像是梵高所绘(图 1.8)。

图 1.8 将梵高的《星夜》风格转移到原始图像上,产生了一件感觉像是原始艺术家创作的艺术品
这实际上是一个很酷的应用。如果你了解任何画家,令人惊讶的是,完成一幅画可能需要几天甚至几周的时间,然而这里有一个应用可以在几秒钟内根据现有的风格创作出新的图像。
1.2.4 创建图像
虽然早期的例子确实是 AI 在计算机视觉应用方面的真正令人印象深刻的例子,但我看到真正的魔法在这里发生:创造的魔法。2014 年,Ian Goodfellow 发明了一种新的深度学习模型,可以想象新事物,称为生成对抗网络(GANs)。这个名字听起来有点吓人,但我向你保证它们并不如此。GAN 是一种演化的 CNN 架构,被认为是深度学习中的一个重大进步。所以当你理解 CNNs 时,GANs 将对你来说更有意义。
GANs(生成对抗网络)是复杂的深度学习模型,可以生成令人惊叹的、逼真的合成图像,包括物体、人物和地点等。如果你给他们一组图像,他们可以制作出全新的、看起来非常逼真的图像。例如,StackGAN 是 GAN 架构变体之一,可以使用对象的文本描述来生成与该描述匹配的高分辨率图像。这不仅仅是数据库中的图像搜索。这些“照片”以前从未见过,完全是虚构的(图 1.9)。

图 1.9 生成对抗网络(GANs)可以从一组现有图像中创建新的、“虚构”的图像。
GAN 是近年来机器学习中最有希望的发展之一。对 GAN 的研究是新的,结果非常令人鼓舞。到目前为止,GANs 的大部分应用都是图像相关的。但它让你想:如果机器被赋予创造图片的想象力,它们还能创造什么?在未来,你最喜欢的电影、音乐,甚至书籍会不会由计算机创造?将一种数据类型(文本)合成另一种(图像)的能力最终将使我们能够仅使用详细的文本描述来创造各种娱乐。
GANs 创作艺术品
2018 年 10 月,一幅名为《Edmond Belamy 肖像》的 AI 创作画作以 43.25 万美元的价格售出。这件艺术品展示了一个名为 Edmond de Belamy 的虚构人物,可能是法国人——从他的深色大衣和平凡的白色领口来看,他可能是一位牧师。

一幅以虚构人物 Edmond de Belamy 命名的 AI 生成艺术品以 43.25 万美元的价格售出。
这件艺术品是由三位 25 岁的法国学生使用 GANs 创作的。该网络在 14 至 20 世纪之间绘制的 15,000 幅肖像画数据集上进行了训练,然后创作了自己的作品。该团队打印了这幅画,装裱并签署了 GAN 算法的一部分。
1.2.5 人脸识别
人脸识别(FR)使我们能够精确地识别或标记一个人的图像。日常应用包括在网络上搜索名人以及在图像中自动标记朋友和家人。人脸识别是一种细粒度分类。
著名的《人脸识别手册》(Li 等人,Springer,2011)将 FR 系统的两种模式进行了分类:
-
面部识别——面部识别涉及一对一的匹配,将查询面部图像与数据库中的所有模板图像进行比较,以确定查询面的身份。另一个面部识别场景涉及城市当局的观察名单检查,其中查询面与嫌疑人名单(一对一的匹配)进行匹配。
-
面部验证——面部验证涉及一对一的匹配,将查询面部图像与声称身份的模板面部图像进行比较(见图 1.10)。

图 1.10 面部验证(左)和面部识别(右)的示例
1.2.6 图像推荐系统
在这个任务中,用户试图根据给定的查询图像找到相似的图像。购物网站根据特定产品的选择提供产品建议(通过图像),例如,显示与用户选择的鞋子相似的各种鞋子。图 1.11 展示了服装搜索的一个例子。

图 1.11 服装搜索。每行最左边的图像是查询/点击的图像,后续的列显示了相似的服装。(来源:刘等,2016 年。)
1.3 计算机视觉流程:全景
好的,现在我已经吸引了你的注意,让我们深入一层了解计算机视觉系统。记住,在本章前面,我们讨论了视觉系统由两个主要组件组成:感知设备和解释设备(图 1.12 提供了一个提醒)。在本节中,我们将探讨解释设备组件用于处理和理解图像的流程。

图 1.12 关注计算机视觉系统中的解释设备
计算机视觉的应用多种多样,但典型的视觉系统使用一系列不同的步骤来处理和分析图像数据。这些步骤被称为计算机视觉流程。许多视觉应用遵循获取图像和数据、处理这些数据、执行一些分析和识别步骤,然后基于提取的信息进行预测的流程(见图 1.13)。

图 1.13 计算机视觉流程,它接收输入数据,处理它,提取信息,然后将它发送到机器学习模型进行学习
让我们将图 1.13 中的流程应用于图像分类器示例。假设我们有一张摩托车的图片,我们希望模型从以下类别中预测该对象的可能性:摩托车、汽车和狗(见图 1.14)。

图 1.14 使用机器学习模型从摩托车、汽车和狗类别预测摩托车对象的可能性
定义:图像分类器是一种算法,它接收图像作为输入,并输出一个标签或“类别”,以识别该图像。在机器学习中,类别(也称为类别)是数据的输出类别。
这是图像通过分类管道的流程:
-
计算机从像相机这样的成像设备接收视觉输入。这种输入通常以图像或形成视频的图像序列的形式捕获。
-
然后每个图像都会通过一些预处理步骤,其目的是标准化图像。常见的预处理步骤包括调整图像大小、模糊、旋转、改变其形状,或者将图像从一种颜色转换到另一种颜色,例如从彩色转换为灰度。只有通过标准化图像——例如,使它们具有相同的大小——你才能然后比较它们并进一步分析它们。
-
我们提取特征。特征是我们定义对象的东西,它们通常是关于对象形状或颜色的信息。例如,区分摩托车的一些特征是车轮的形状、车头灯、泥板等。这个过程输出的结果是特征向量,它是一系列独特的形状列表,用于识别对象。
-
特征被输入到分类模型中。这一步查看前一步的特征向量并预测图像的类别。假设你是一名分类器模型几分钟,让我们通过分类过程。你逐个查看特征向量中的特征列表,并试图确定图像中有什么:
-
首先你看到一个轮子特征;这可能是一辆汽车、一辆摩托车还是一只狗?显然它不是狗,因为狗没有轮子(至少,正常的狗,不是机器人)。然后这可能是一张汽车或摩托车的照片。
-
你继续到下一个特征,即车头灯。它更有可能是摩托车而不是汽车。
-
下一个特征是后泥板——同样,它更有可能是摩托车。
-
这个物体只有两个轮子;这更接近于摩托车。
-
你继续分析所有特征,如车身形状、踏板等,直到你到达对图像中物体的最佳猜测。
-
这个过程的输出是每个类别的概率。正如你在我们的例子中所看到的,狗的概率最低,只有 1%,而这是一个摩托车的概率是 85%。你可以看到,尽管模型能够以最高的概率预测正确的类别,但它仍然对区分汽车和摩托车有些困惑——它预测有 14%的可能性这是一张汽车的照片。既然我们知道它是一辆摩托车,我们可以说我们的机器学习分类算法的准确率是 85%。还不错!为了提高这个准确率,我们可能需要做更多步骤 1(获取更多的训练图像),或者步骤 2(更多的处理以去除噪声),或者步骤 3(提取更好的特征),或者步骤 4(更改分类器算法并调整一些超参数),或者甚至允许更多的训练时间。我们可以采取的许多不同方法来提高我们模型的表现力都包含在一个或多个的管道步骤中。
那是图像如何在 CV 管道中流动的大致情况。接下来,我们将深入探讨管道的每个步骤。
1.4 图像输入
在 CV 应用中,我们处理图像或视频数据。现在让我们谈谈灰度图像和彩色图像,在后面的章节中,我们将讨论视频,因为视频只是图像的堆叠顺序帧。
1.4.1 图像作为函数
一张图像可以被表示为两个变量 x 和 y 的函数,它们定义了一个二维区域。数字图像由像素网格组成。像素是图像的基本构建块。每个图像都由一组像素组成,这些像素的值代表图像中特定位置的光强度。让我们再次看看摩托车示例,在应用像素网格后(图 1.15)。

图 1.15 图像由称为像素的原始构建块组成。像素值代表图像中特定位置出现的光强度。
图 1.14 中的图像大小为 32 × 16。这意味着图像的尺寸是 32 像素宽和 16 像素高。x 轴从 0 到 31,y 轴从 0 到 16。总的来说,该图像有 512(32 × 16)个像素。在这张灰度图像中,每个像素包含一个值,代表该特定像素上的光强度。像素值范围从 0 到 255。由于像素值代表光强度,值 0 代表非常暗的像素(黑色),255 代表非常亮(白色),而中间的值代表灰度上的强度。
你可以看到图像坐标系与笛卡尔坐标系相似:图像是二维的,位于 x-y 平面上。原点(0, 0)位于图像的左上角。为了表示特定的像素,我们使用以下符号:F 作为函数,x,y作为像素在 x 和 y 坐标中的位置。例如,位于 x = 12 和 y = 13 的像素是白色的;这由以下函数表示:F(12, 13) = 255。同样,位于摩托车前部的像素(20, 7)是黑色的,表示为 F(20, 7) = 0。
Grayscale => F(*x, y*) gives the intensity at position (*x, y*)
那是关于灰度图像的情况。彩色图像又是如何呢?
在彩色图像中,不是只用一个数字来表示像素的值,而是用三个数字来表示像素中每种颜色的强度。例如,在 RGB 系统中,像素的值由三个数字表示:红色强度、绿色强度和蓝色强度。图像还有其他颜色系统,如 HSV 和 Lab。所有这些在表示像素值时都遵循相同的概念(关于彩色图像的更多内容将在后面讨论)。以下是 RGB 系统中表示彩色图像的函数:
Color image in RGB => F(*x, y*) = [ red (*x, y*), green (*x, y*), blue (*x, y*) ]
将图像视为一个函数在图像处理中非常有用。我们可以将图像视为 F(x, y)的函数,并在数学上对其操作以将其转换为新的图像函数 G(x, y)。让我们看看表 1.1 中的图像变换示例。
表 1.1 图像变换示例函数
| 应用 | 变换 |
|---|---|
| 使图像变暗。 | G(*x, y*) = 0.5 * F(*x, y*) |
| 使图像变亮。 | G(*x, y*) = 2 * F(*x, y*) |
| 将对象向下移动 150 个像素。 | G(*x, y*) = F(x, *y* + 150) |
| 将图像中的灰色去除以将图像转换为黑白。 | G(*x, y*) = { 0 if F(*x, y*) < 130, 255 otherwise } |
1.4.2 计算机如何识别图像
当我们看图像时,我们看到物体、风景、颜色等等。但计算机并不是这样。考虑图 1.16。你的人类大脑可以处理它,并立即知道这是一张摩托车图片。对于计算机来说,图像看起来像像素值的 2D 矩阵,这些值代表颜色光谱中的强度。这里没有上下文,只是一大堆数据。

图 1.16 计算机将图像视为值的矩阵。这些值代表像素在颜色光谱中的强度。例如,灰度图像的像素值范围在 0(黑色)到 255(白色)之间。
图 1.16 中的图像大小为 24 × 24。这个大小表示图像的宽度和高度:水平方向有 24 个像素,垂直方向也有 24 个像素。这意味着总共有 576(24 × 24)个像素。如果图像大小为 700 × 500,那么矩阵的维度将是(700,500),其中矩阵中的每个像素代表该像素的亮度强度。0 代表黑色,255 代表白色。
1.4.3 彩色图像
在灰度图像中,每个像素只代表一种颜色的强度,而在标准的 RGB 系统中,彩色图像有三个通道(红色、绿色和蓝色)。换句话说,彩色图像由三个矩阵表示:一个表示像素中红色的强度,一个表示绿色,一个表示蓝色(图 1.17)。

图 1.17 彩色图像由红色、绿色和蓝色通道表示,矩阵可以用来表示这些颜色的强度。
如图 1.17 所示,彩色图像由三个通道组成:红色、绿色和蓝色。现在的问题是,计算机是如何看到这张图像的?同样,它们看到的是一个矩阵,与只有单一通道的灰度图像不同。在这种情况下,我们将有三个矩阵堆叠在一起;这就是为什么它是一个 3D 矩阵。700×700 彩色图像的维度是(700, 700, 3)。假设第一个矩阵代表红色通道;那么该矩阵的每个元素代表该像素中红色颜色的强度,绿色和蓝色同理。彩色图像中的每个像素都与三个数字(0 到 255)相关联。这些数字代表该特定像素中红色、绿色和蓝色的强度。
以像素(0,0)为例,我们可以看到它代表的是绿色草地的左上角像素。当我们查看这个像素在彩色图像中的样子时,它看起来就像图 1.18 所示。图 1.19 中的例子展示了绿色的一些色调及其 RGB 值。
计算机是如何看到颜色的?
计算机将图像视为矩阵。灰度图像有一个通道(灰色);因此,我们可以用二维矩阵来表示灰度图像,其中每个元素代表该特定像素的亮度强度。记住,0 代表黑色,255 代表白色。灰度图像有一个通道,而彩色图像有三个通道:红色、绿色和蓝色。我们可以用三维矩阵来表示彩色图像,其中深度为三。
我们也看到了图像如何被当作空间函数来处理。这个概念使我们能够在数学上操作图像,并从中改变或提取信息。将图像视为函数是许多图像处理技术的基础,例如将彩色转换为灰度或缩放图像。这些步骤只是通过数学方程对图像像素进行逐像素的转换。
-
灰度图:f(x, y)给出位置(x, y)的强度
-
彩色图像:f(x, y) = [红色(x, y),绿色(x, y),蓝色(x, y)]

绿色草地的图像实际上是由不同强度的三种颜色组成的。

图 1.19 展示了不同深度的绿色代表三种图像颜色(红色、绿色、蓝色)的不同强度。
1.5 图像预处理
在机器学习(ML)项目中,你通常会经历一个数据预处理或清洗步骤。作为一名机器学习工程师,在构建学习模型之前,你将花费大量时间清理和准备数据。这一步骤的目标是使数据准备好,以便机器学习模型更容易分析和处理。对于图像来说,也是如此。根据你要解决的问题和手头的数据集,在将图像输入机器学习模型之前,可能需要进行一些数据整理。
图像处理可能涉及简单的任务,如图像缩放。稍后,您将了解到为了将图像数据集输入到卷积网络中,所有图像都必须具有相同的大小。其他处理任务也可以进行,如几何和颜色变换、将颜色转换为灰度等。本书的章节和项目中我们将涵盖各种图像处理技术。
获得的数据通常很杂乱,来自不同的来源。为了将其输入到机器学习模型(或神经网络)中,它需要被标准化和清理。预处理用于执行将减少算法复杂度并提高准确性的步骤。我们不能为图像拍摄的每种条件编写一个独特的算法;因此,当我们获取图像时,我们将其转换为一种形式,以便通用的算法可以解决它。以下小节描述了一些数据预处理技术。
1.5.1 将彩色图像转换为灰度图像以降低计算复杂性
有时候,您可能会发现从图像中删除不必要的信 息以减少空间或计算复杂度很有用。例如,假设您想将彩色图像转换为灰度图像,因为对于许多物体来说,颜色并不是识别和解释图像所必需的。灰度图像可能足以识别某些物体。由于彩色图像包含比黑白图像更多的信息,它们可能会增加不必要的复杂性,并在内存中占用更多空间。请记住,彩色图像以三个通道表示,这意味着将它们转换为灰度图像将减少需要处理的像素数量(图 1.20)。

图 1.20 将彩色图像转换为灰度图像会减少需要处理的像素数量。这对于不依赖于颜色信息损失的应用程序来说可能是一个好的方法。
在这个例子中,您可以看到如何使用亮度和暗度(强度)的模式来定义许多物体的形状和特征。然而,在其他应用中,颜色对于定义某些物体很重要,例如皮肤癌检测,它严重依赖于皮肤颜色(红色皮疹)。
-
标准化图像 ——正如您将在第三章中看到的那样,某些机器学习算法(如 CNN)存在的一个重要约束是,需要将数据集中的图像调整到统一的尺寸。这意味着在将图像输入到学习算法之前,您的图像必须进行预处理并缩放,以具有相同的宽度和高度。
何时颜色很重要?
将图像转换为灰度可能不是某些问题的好选择。有一些应用中颜色非常重要:例如,在医学图像中建立一个诊断系统来识别红色皮肤疹。这个应用严重依赖于皮肤中红色颜色的强度。从图像中移除颜色将使解决这个问题更加困难。一般来说,彩色图像在许多医学应用中提供了非常有帮助的信息。
图像中颜色重要性的另一个例子是自动驾驶汽车中的车道检测应用,其中汽车必须区分黄色和白色线条,因为它们被处理方式不同。灰度图像无法提供足够的信息来区分黄色和白色线条。
![图片]()
基于灰度的图像处理器无法区分彩色图像。
识别问题中颜色重要性的经验法则是用肉眼观察图像。如果你能够在灰度图像中识别你正在寻找的对象,那么你可能已经提供了足够的信息给你的模型。如果不能,那么你的模型肯定需要更多的信息(颜色)。同样的规则可以应用于我们将要讨论的大多数其他预处理技术。
-
数据增强 --另一种常见的预处理技术是将现有数据集与现有图像的修改版本相结合。缩放、旋转和其他仿射变换通常用于扩大你的数据集,使神经网络接触到你图像的广泛变化。这使得你的模型更有可能在任何形式和形状中出现时识别对象。图 1.21 展示了应用于蝴蝶图像的图像增强的一个例子。
![图片]()
图 1.21 图像增强技术创建了输入图像的修改版本,为机器学习模型提供更多学习样本。
-
其他技术 --有许多预处理技术可供选择,以便为训练机器学习模型准备你的图像。在某些项目中,你可能需要从你的图像中移除背景颜色以减少噪声。在其他项目中,可能需要你调整图像的亮度或暗度。简而言之,你需要应用到数据集上的任何调整都是预处理的一部分。你将根据手头的数据集和你要解决的问题选择合适的处理技术。在这本书中,你将看到许多图像处理技术,这有助于你建立自己在项目工作中需要哪些技术的直觉。
免费午餐定理
这是一个由 David Wolpert 和 William Macready 在“优化中的无免费午餐定理”(IEEE Transactions on Evolutionary Computation 1, 67)中提出的短语。当团队在机器学习项目中工作时,你经常会听到这句话。这意味着没有一种规定的食谱适合所有模型。在机器学习项目中工作时,你需要做出许多选择,比如构建你的神经网络架构、调整超参数以及应用适当的数据预处理技术。虽然有一些经验法则可以解决某些问题,但事实上并没有一种保证在所有情况下都能有效工作的单一食谱。
你必须对你正在尝试解决的问题的数据集和问题做出某些假设。对于某些数据集,最好将彩色图像转换为灰度图像,而对于其他数据集,你可能需要保留或调整彩色图像。
好消息是,与传统的机器学习不同,深度学习算法需要最少的数据预处理,因为正如你很快就会看到的,神经网络在处理图像和提取特征方面做了大部分繁重的工作。
1.6 特征提取
特征提取是 CV 流程的核心组件。实际上,整个深度学习模型都是围绕提取有用特征的想法展开的,这些特征可以清楚地定义图像中的对象。因此,我们将在这里花更多的时间,因为了解什么是特征、什么是特征向量以及为什么我们要提取特征是非常重要的。
定义 在机器学习中,特征是观察现象的个别可测量属性或特征。特征是你输入到你的机器学习模型中以输出预测或分类的数据。假设你想预测房屋的价格:你的输入特征(属性)可能包括square_foot(面积)、number_of_rooms(房间数)、bathrooms(浴室数量)等,模型将根据你的特征值输出预测价格。选择能够清楚地区分你的对象的良好特征可以增加机器学习算法的预测能力。
1.6.1 计算机视觉中的特征是什么?
在计算机视觉(CV)中,一个特征是图像中一个可测量的数据片段,它对该特定物体是独特的。它可能是一种独特的颜色或特定的形状,如线条、边缘或图像片段。一个好的特征被用来区分不同的物体。例如,如果我给你一个像轮子这样的特征,并让你猜测一个物体是摩托车还是狗,你会怎么猜?摩托车。正确!在这种情况下,轮子是一个强大的特征,它清楚地区分了摩托车和狗。然而,如果我给你同样的特征(一个轮子)并让你猜测一个物体是自行车还是摩托车,这个特征就不足以区分这些物体。你需要寻找更多像镜子、车牌或可能还有踏板这样的特征,这些特征共同描述了一个物体。在机器学习(ML)项目中,我们希望将原始数据(图像)转换成特征向量,以展示给我们的学习算法,这样算法就可以学习物体的特征(图 1.22)。

图 1.22 示例输入图像被输入到特征提取算法中,以在图像中找到模式并创建特征向量
在图中,我们将摩托车原始输入图像输入到特征提取算法中。现在,让我们将特征提取算法视为一个黑盒,我们稍后再来讨论它。现在,我们需要知道提取算法产生一个包含特征列表的向量。这个特征向量是一个一维数组,它对物体提供了一个稳健的表示。
特征泛化能力
需要强调的是,图 1.22 反映的是仅从一辆摩托车中提取的特征。一个特征的重要特性是可重复性。特征应该能够检测到一般的摩托车,而不仅仅是这一特定的摩托车。因此,在现实世界的问题中,一个特征并不是输入图像中某一部分的精确副本。

特征需要检测一般模式。
以轮子特征为例,这个特征并不完全像某一特定摩托车的轮子。相反,它看起来像是一个带有一些识别训练数据集中所有图像中轮子的模式的圆形形状。当特征提取器看到成千上万张摩托车的图像时,它识别出定义一般轮子的模式,而不管它们在图像中的位置和它们属于哪种类型的摩托车。
1.6.2 什么因素使一个特征(有用的)变得好?
机器学习模型的好坏取决于你提供的特征。这意味着提出好的特征是构建机器学习模型的重要工作。但什么因素使一个特征变得好?你如何判断?
让我们用一个例子来讨论这个问题。假设我们想要构建一个分类器来区分两种类型的狗:灰狗和拉布拉多。让我们选取两个特征——狗的高度和它们的眼睛颜色——并对它们进行评估(图 1.23)。

图 1.23 灰狗和拉布拉多狗的例子
让我们从高度开始。你认为这个特征有多有用?嗯,平均来说,灰狗通常比拉布拉多高几英寸,但并不总是这样。狗的世界中有很多变化。所以让我们评估这两种品种群体中不同值上的这个特征。让我们在图 1.24 中的直方图上可视化玩具例子中的高度分布。

图 1.24 玩具狗数据集上高度分布的可视化
从直方图上,我们可以看到,如果狗的高度是 20 英寸或更少,有超过 80%的概率这只狗是拉布拉多。在直方图的另一边,如果我们看那些身高超过 30 英寸的狗,我们可以相当有信心地说这只狗是灰狗。那么,关于直方图中间的数据(20 到 30 英寸的高度)呢?我们可以看到,每种类型狗的概率相当接近。在这种情况下,思考过程如下:
如果高度 ≤ 20:
返回更高的拉布拉多概率
如果高度 ≥ 30:
返回更高的灰狗概率
如果 20 < 高度 < 30:
寻找其他特征来分类对象
因此,在这种情况下,狗的高度是一个有用的特征,因为它有助于(增加信息)区分两种狗类型。我们可以保留它。但是,它并不总是能区分灰狗和拉布拉多,这是可以接受的。在机器学习项目中,通常没有一种特征可以单独对所有的对象进行分类。这就是为什么在机器学习中,我们几乎总是需要多个特征,每个特征捕捉不同类型的信息。如果只有一个特征就能完成这项工作,我们就可以直接写if-else语句,而不用费心训练分类器。
小贴士:类似于我们之前在颜色转换(颜色与灰度)中做的,为了确定你应该为特定问题使用哪些特征,进行一个思想实验。假装你是分类器。如果你想区分灰狗和拉布拉多,你需要知道哪些信息?你可能会问关于毛发长度、身体大小、颜色等等。
为了快速举例说明一个无用的特征,让我们看看狗的眼睛颜色。在这个玩具例子中,假设我们只有两种眼睛颜色,蓝色和棕色。图 1.25 显示了这种例子可能看起来像直方图。

图 1.25 玩具狗数据集中眼睛颜色分布的可视化
很明显,对于大多数值,两种类型的分布大约是 50/50。所以实际上,这个特征告诉我们 nothing,因为它与狗的类型不相关。因此,它不能区分灰狗和拉布拉多。
什么是物体识别的好特征?
一个好的特征将帮助我们以所有可能的方式识别一个对象。一个好的特征的特点如下:
-
可识别
-
容易追踪和比较
-
在不同尺度、光照条件和视角下保持一致性
-
即使在噪声图像中或只有部分对象可见时仍然可见
1.6.3 提取特征(手工提取与自动提取)
这是在机器学习中一个很大的主题,可能需要一整本书来阐述。它通常在被称为特征工程的主题背景下进行描述。在这本书中,我们只关注从图像中提取特征。因此,我将在本章中简要介绍这个想法,并在后面的章节中进一步展开。
传统机器学习使用手工特征
在传统的机器学习问题中,我们花费大量时间在手工特征选择和工程上。在这个过程中,我们依赖我们的领域知识(或与领域专家合作)来创建使机器学习算法表现更好的特征。然后,我们将生成的特征输入到支持向量机(SVM)或 AdaBoost 等分类器中,以预测输出(图 1.26)。一些手工特征集包括以下内容:
-
方向梯度直方图(HOG)
-
Haar 级联
-
尺度不变特征变换(SIFT)
-
加速鲁棒特征(SURF)

图 1.26 传统机器学习算法需要手工特征提取。
使用自动提取特征的深度学习
然而,在深度学习中,我们不需要从图像中手动提取特征。网络自动提取特征,并通过对其连接应用权重来学习它们在输出中的重要性。你只需将原始图像输入到网络中,当它通过网络层时,网络会识别图像中的模式以创建特征(图 1.27)。可以将神经网络视为特征提取器加分类器,它们是端到端可训练的,与传统机器学习模型使用的手工特征形成对比。

图 1.27 深度神经网络通过其层自动提取特征并对对象进行分类。不需要手工特征。
神经网络是如何区分有用特征和非有用特征的?
你可能会觉得神经网络只理解最有用的特征,但这并不完全正确。神经网络会收集所有可用的特征并给它们随机分配权重。在训练过程中,神经网络调整这些权重以反映它们的重要性以及它们应该如何影响输出预测。出现频率最高的模式将具有更高的权重,被认为是更有用的特征。权重最低的特征对输出的影响非常小。这个过程将在下一章中更详细地讨论。

对不同特征进行加权以反映其在识别对象中的重要性
为什么使用特征?
输入图像包含太多不必要的额外信息,这些信息对于分类来说是不必要的。因此,预处理图像后的第一步是通过提取重要信息并丢弃非必要信息来简化它。通过提取重要的颜色或图像片段,我们可以将复杂和大量的图像数据转换为更小的特征集。这使得基于特征对图像进行分类的任务更加简单和快速。
考虑以下示例。假设我们有一个包含 10,000 张摩托车图像的数据集,每张图像的宽度为 1,000 像素,高度为 1,000 像素。一些图像有均匀的背景,而其他图像则有繁忙的背景和不必要的数据。当这些数千张图像被输入到特征提取算法中时,我们丢失了所有对识别摩托车不重要的非必要数据,我们只保留了一个可以直接输入到分类器中的有用特征列表(图 1.28)。这个过程比让分类器查看 10,000 张图像的原始数据集来学习摩托车的属性要简单得多。

图 1.28 从数千张图像中提取并巩固特征,形成一个特征向量以供分类器使用
1.7 分类学习算法
到目前为止,关于分类器管道我们已经讨论了以下几点:
-
输入图像 -- 我们已经看到图像是如何表示为函数的,以及计算机将图像视为灰度图像的二维矩阵和彩色图像的三维矩阵(三个通道)。
-
图像预处理 -- 我们讨论了一些图像预处理技术,以清理我们的数据集并使其准备好作为机器学习算法的输入。
-
特征提取 -- 我们将我们的大量图像数据集转换为一个描述图像中对象的独特有用特征的向量。
现在是时候将提取的特征向量输入到分类器中,以输出图像的类别标签(例如,摩托车或其他)。
正如我们在上一节中讨论的,分类任务可以通过以下方式之一完成:传统的机器学习算法,如支持向量机(SVMs),或深度神经网络算法,如卷积神经网络(CNNs)。虽然传统的机器学习算法可能在某些问题上获得相当好的结果,但 CNNs 在处理和分类最复杂的问题中的表现尤为出色。
在这本书中,我们将详细讨论神经网络以及它们是如何工作的。现在,我想让你知道神经网络会自动从你的数据集中提取有用特征,并充当分类器,为你的图像输出类别标签。输入图像通过神经网络的层来学习它们的特征,一层一层地学习(图 1.29)。你的网络越深(层越多),它就会学习到数据集的更多特征:因此得名深度学习。更多的层伴随着一些权衡,我们将在下一章中讨论。神经网络的最外层通常充当分类器,输出类别标签。

图 1.29 输入图像通过神经网络的层,以便它可以逐层学习特征。
摘要
-
人类和机器视觉系统都包含两个基本组件:一个感知设备和解释设备。
-
解释过程包括四个步骤:输入数据,预处理,进行特征提取,并生成机器学习模型。
-
一张图像可以被表示为 x 和 y 的函数。计算机将图像视为像素值的矩阵:灰度图像有一个通道,彩色图像有三个通道。
-
图像处理技术因问题和数据集而异。这些技术中的一些是将图像转换为灰度以降低复杂性,将图像调整到统一大小以适应您的神经网络,以及数据增强。
-
特征是图像中用于分类其对象的独特属性。传统的机器学习算法使用多种特征提取方法。
2 深度学习和神经网络
本章涵盖
-
理解感知器和多层感知器
-
使用不同类型的激活函数
-
使用前向传播、误差函数和误差优化来训练网络
-
执行反向传播
在上一章中,我们讨论了计算机视觉(CV)管道组件:输入图像、预处理、提取特征和学习算法(分类器)。我们还讨论了在传统的机器学习(ML)算法中,我们手动提取特征,生成一个特征向量,由学习算法进行分类,而在深度学习(DL)中,神经网络既是特征提取器又是分类器。神经网络自动识别模式并从图像中提取特征,并将它们分类为标签(图 2.1)。

图 2.1 传统的机器学习算法需要手动提取特征。深度神经网络通过其层传递输入图像来自动提取特征。
在本章中,我们将从 CV 背景中短暂休息,打开图 2.1 中的 DL 算法框。我们将深入探讨神经网络如何学习特征和做出预测。然后,在下一章中,我们将回到 CV 应用,介绍最流行的 DL 架构之一:卷积神经网络。
本章的高级布局如下:
-
我们将从神经网络最基本的部分开始:感知器,这是一个只包含一个神经元的神经网络。
-
然后,我们将转向一个包含数百个神经元的更复杂的神经网络架构,以解决更复杂的问题。这个网络被称为多层感知器(MLP),其中神经元堆叠在隐藏层中。在这里,你将学习神经网络架构的主要组成部分:输入层、隐藏层、权重连接和输出层。
-
你将了解到网络训练过程包括三个主要步骤:
-
前向操作
-
计算误差
-
误差优化:使用反向传播和梯度下降来选择最优化参数,以最小化误差函数
-
我们将深入探讨这些步骤的每一个。你会发现构建神经网络需要做出必要的设计决策:选择优化器、损失函数和激活函数,以及设计网络架构,包括应该有多少层相互连接以及每层应该有多少个神经元。准备好了吗?让我们开始吧!

图 2.2 人工神经网络由节点层组成,节点或神经元通过边连接。
2.1 理解感知器
让我们看看第一章(图 2.2)中的人工神经网络(ANN)图。你可以看到,ANN 由许多按层结构排列的神经元组成,以执行某种计算并预测输出。这种架构也可以称为多层感知器,因为它更直观,因为它暗示网络由多层感知器组成。MLP 和 ANN 这两个术语可以互换使用,以描述这种神经网络架构。
在图 2.2 中的 MLP 图中,每个节点被称为神经元。我们很快将讨论 MLP 网络是如何工作的,但首先让我们聚焦于神经网络最基本的部分:感知器。一旦你理解了单个感知器的工作原理,理解多个感知器如何协同工作以学习数据特征就会变得更加直观。
2.1.1 什么是感知器?
最简单的神经网络是感知器,它由单个神经元组成。从概念上讲,感知器的工作方式类似于生物神经元(图 2.3)。生物神经元从其树突接收电信号,以各种程度调节电信号,然后仅在输入信号的总体强度超过某个阈值时通过其突触发出输出信号。然后输出被馈送到另一个神经元,依此类推。
为了模拟生物神经元的特性,人工神经元执行两个连续的功能:它计算输入的加权总和来表示输入信号的总体强度,然后对结果应用一个阶跃函数,以确定是否在信号超过某个阈值时输出 1,如果没有超过阈值则输出 0。

图 2.3 人工神经元是受生物神经元启发的。不同的神经元通过携带信息的突触相互连接。
正如我们在第一章中讨论的,并非所有输入特征都同等有用或重要。为了表示这一点,每个输入节点都被分配一个权重值,称为其连接权重,以反映其重要性。
连接权重
并非所有输入特征都是同等重要(或有用)的特征。每个输入特征(x[1])都被分配其自己的权重(w[1]),以反映其在决策过程中的重要性。分配了更高权重的输入对输出的影响更大。如果权重高,它会放大输入信号;如果权重低,它会减弱输入信号。在神经网络的一般表示中,权重由从输入节点到感知器的线条或边表示。
例如,如果你是根据一组特征(如大小、邻里和房间数量)预测房价,那么有三个输入特征(x[1]、x[2]和x[3])。这些输入中的每一个都将有不同的权重值,代表其对最终决策的影响。例如,如果房屋的大小对价格的影响是邻里的两倍,而邻里对房间数量的影响是两倍,那么你将看到权重值分别为 8、4 和 2。
连接值是如何分配的,以及学习是如何发生的,这是神经网络训练过程的核心。这是我们将在本章剩余部分讨论的内容。
在图 2.4 的感知器图中,你可以看到以下内容:
-
输入向量 --输入到神经元的特征向量。通常用大写x表示输入向量的输入(x[1]、x[2],……,x[n]*)。
-
权重向量 --每个x[1]被分配一个权重值w[1],它表示其在区分不同输入数据点中的重要性。
-
神经元函数 --在神经元内进行的计算,以调节输入信号:加权总和和阶跃激活函数。
-
输出 --由你为网络选择的激活函数类型控制。有不同类型的激活函数,我们将在本章详细讨论。对于阶跃函数,输出为 0 或 1。其他激活函数产生概率输出或浮点数。输出节点代表感知器的预测。

图 2.4 输入向量被输入到神经元中,并分配权重以表示其重要性。神经元内部进行的计算是加权总和和激活函数。
让我们更深入地了解神经元内部发生的加权总和和阶跃函数计算。
加权总和函数
也称为线性组合,加权总和函数是所有输入乘以其权重之和,然后加上一个偏置项。此函数产生以下方程表示的直线:
z = Σx[i] · w[i] + b(偏置)
z = x[1] · w[1] + x[2] · w[2] + x[3] · w[3] + … + x[n] · w[n] + b
这里是如何在 Python 中实现加权总和的:
z = np.dot(w.T,X) + b ❶
❶ x 是输入向量(大写 X),w 是权重向量,b 是 y 轴截距。
感知器中的偏置是什么,为什么我们要添加它?
让我们复习一下线性代数的一些概念,以帮助理解底层发生了什么。以下是直线的函数:

直线的方程
直线的函数由方程 (y = mx + b) 表示,其中 b 是 y 轴截距。要能够定义一条线,你需要两样东西:线的斜率和线上的一个点。偏差就是 y 轴上的那个点。偏差允许你将线在 y 轴上上下移动,以更好地拟合预测与数据。没有偏差(b),线总是必须通过原点(0,0),这将导致拟合较差。为了可视化偏差的重要性,请看上面的图表,并尝试使用通过原点(0,0)的线将圆圈与星号分开。这是不可能的。
输入层可以通过引入一个始终具有值为 1 的额外输入节点来给予偏差,正如你可以在下一个图中看到的那样。在神经网络中,偏差(b)的值被视为一个额外的权重,并由神经元学习并调整以最小化成本函数,正如我们将在本章的后续部分中学习的那样。

输入层可以通过引入一个始终具有值为 1 的额外输入来给予偏差。
步激活函数
在人工和生物神经网络中,一个神经元不仅仅输出它接收到的原始输入。相反,还有一个额外的步骤,称为激活函数;这是大脑的决策单元。在 ANNs 中,激活函数接受之前相同的加权求和输入(z = Σx[i] · w[i] + b)并在加权求和高于某个阈值时激活(触发)神经元。这种激活基于激活函数的计算。在本章的后面部分,我们将回顾不同类型的激活函数及其在神经网络更广泛背景中的通用目的。感知器算法使用的最简单的激活函数是步函数,它产生二进制输出(0 或 1)。它基本上说,如果求和输入 ≥ 0,它“触发”(输出 = 1);否则(求和输入 < 0),它不触发(输出 = 0)(图 2.5)。


这就是 Python 中步函数看起来像什么:
def step_function(*z*): ❶
if z <= 0:
return 0
else:
return 1
❶ z 是加权求和 = σ x[i] · w[i] + b
2.1.2 感知器是如何学习的?
感知器通过试错法从错误中学习。它通过调整权重的值上下移动,直到网络被训练(图 2.6)。

图 2.6 在学习过程中调整权重上下移动以优化损失函数的值。
感知器的学习逻辑是这样的:
-
神经元计算加权求和并应用激活函数来做出预测 ŷ。这被称为前馈过程:
- ŷ = activation(Σx[i] · w[i] + b)
-
它将输出预测与正确标签进行比较来计算错误:
- 错误 = y - ŷ
-
然后,它会更新权重。如果预测过高,它会调整权重以在下一次做出更低的预测,反之亦然。
-
重复!
这个过程会重复多次,神经元会持续更新权重以改善其预测,直到步骤 2 产生一个非常小的错误(接近零),这意味着神经元的预测非常接近正确值。此时,我们可以停止训练,并将产生最佳结果的权重值保存下来,以应用于未来结果未知的情况。
2.1.3 一个神经元足够解决复杂问题吗?
简短的回答是不,但让我们看看原因。感知器是一个线性函数。这意味着训练后的神经元将产生一条直线来分离我们的数据。
假设我们想要训练一个感知器来预测一个玩家是否会被大学队接受。我们收集了前几年的所有数据,并训练感知器根据仅有的两个特征(身高和体重)来预测玩家是否会被接受。训练后的感知器将找到最佳权重和偏差值,以产生最佳拟合的直线,将接受者与非接受者分开(最佳拟合)。这条线的方程如下:
z = 身高 · w[1] + 年龄 · w[2] + b
在训练数据上完成训练后,我们可以开始使用感知器来预测新玩家的数据。当我们得到一个身高 150 厘米、12 岁的玩家时,我们用值(150,12)计算前面的方程。当在图表(图 2.7)中绘制时,你可以看到它低于这条线:神经元预测这个玩家不会被接受。如果它高于这条线,那么这个玩家将被接受。

图 2.7 线性可分的数据可以通过一条直线分离。
在图 2.7 中,单个感知器工作得很好,因为我们的数据是线性可分的。这意味着训练数据可以通过一条直线分离。但生活并不总是那么简单。当我们有一个更复杂的、不能通过直线分离的数据集(非线性数据集)时会发生什么?
正如你在图 2.8 中看到的,一条单独的直线无法分离我们的训练数据。我们说它不适合我们的数据。我们需要一个更复杂的网络来处理这种更复杂的数据。如果我们构建一个包含两个感知器的网络会怎样?这将产生两条线。这会帮助我们更好地分离数据吗?
好吧,这确实比直线要好。但我仍然看到一些颜色预测错误。我们能否添加更多的神经元来使函数更好地拟合?现在你明白了。从概念上讲,我们添加的神经元越多,网络就越能拟合我们的训练数据。事实上,如果我们添加太多的神经元,这会使网络过度拟合训练数据(不好)。但我们会稍后讨论这个问题。这里的普遍规则是,我们的网络越复杂,它就越能学习我们数据的特点。

图 2.8 在非线性数据集中,一条直线无法分离训练数据。在这个例子中,一个具有两个感知器的网络可以产生两条直线,并帮助进一步分离数据。
2.2 多层感知器
我们看到,单个感知器在可以由直线分割的简单数据集上表现良好。但是,正如你可以想象的那样,现实世界比这复杂得多。这就是神经网络可以展示其全部潜力的地方。
线性与非线性问题
-
线性数据集--数据可以用一条直线分割。
-
非线性数据集--数据不能仅用一条直线分割。我们需要多条直线来形成一个分割数据的形状。
看看这个二维数据。在线性问题中,星号和点可以通过画一条直线轻松分类。在非线性数据中,一条直线无法将两种形状分开。

线性数据和非线性数据的示例
要分割非线性数据集,我们需要多条直线。这意味着我们需要提出一个架构,在神经网络中使用成百上千的神经元。让我们看看图 2.9 中的例子。记住,感知器是一个产生直线的线性函数。因此,为了拟合这些数据,我们试图创建一个类似三角形的形状,以分割暗点。看起来三条线就能完成这项工作。

图 2.9 感知器是一个产生直线的线性函数。因此,为了拟合这些数据,我们需要三个感知器来创建一个类似三角形的形状,以分割暗点。
图 2.9 是一个用于模拟非线性数据的小型神经网络示例。在这个网络中,我们使用了一个隐藏层,该层由三个堆叠在一起的神经元组成,之所以称为隐藏层,是因为在训练过程中我们看不到这些层的输出。
2.2.1 多层感知器架构
我们已经看到如何设计一个包含多个神经元的神经网络。让我们通过一个更复杂的数据集来扩展这个想法。图 2.10 中的图来自 Tensorflow playground 网站(playground.tensorflow.org)。我们试图模拟一个螺旋数据集以区分两个类别。为了拟合这个数据集,我们需要构建一个包含数十个神经元的神经网络。一个非常常见的神经网络架构是将神经元堆叠在彼此之上,称为隐藏层。每一层有 n 个神经元。层通过权重连接相互连接。这导致了图中的多层感知器(MLP)架构。
神经网络架构的主要组件如下:
-
输入层 --包含特征向量。
-
隐藏层 --神经元堆叠在隐藏层之上。它们被称为“隐藏”层,因为我们看不到或控制进入这些层的输入或输出。我们唯一做的就是将特征向量输入到输入层,并查看输出层输出的结果。
-
权重连接(边) --权重被分配给节点之间的每个连接,以反映它们对最终输出预测的影响的重要性。在图网络术语中,这些被称为连接节点的边。
![图片]()
图 2.10 Tensorflow playground 示例表示深度神经网络中的特征学习
-
输出层 --我们从输出层得到答案或预测。根据神经网络的结构设置,最终输出可能是一个实值输出(回归问题)或一组概率(分类问题)。这取决于我们在输出层神经元中使用的激活函数的类型。我们将在下一节讨论不同类型的激活函数。
我们讨论了输入层、权重和输出层。这个架构的下一个区域是隐藏层。
2.2.2 什么是隐藏层?
这是特征学习过程的核心所在。当你查看图 2.10 中的隐藏层节点时,你会看到早期层检测简单的模式以学习低级特征(直线)。后面的层检测模式中的模式以学习更复杂特征和形状,然后是模式中的模式,以此类推。当我们讨论卷积网络时,这个概念将很有用。现在,要知道,在神经网络中,我们堆叠隐藏层,从彼此学习复杂特征,直到我们的数据拟合。所以当你设计你的神经网络时,如果你的网络没有拟合数据,解决方案可能是添加更多的隐藏层。
2.2.3 每层有多少层,每层有多少节点?
作为机器学习工程师,你将主要设计你的网络并调整其超参数。虽然没有一种单一的推荐配方适用于所有模型,但在这本书的整个过程中,我们将尝试建立你的超参数调整直觉,并推荐一些起点。当你与神经网络一起工作时,层数和每层的神经元数量是你要设计的重要超参数之一。
一个网络可以有一个或多个隐藏层(技术上,可以是你想要的任意多个)。每个层有一个或多个神经元(同样,可以是你想要的任意多个)。作为机器学习工程师,你的主要任务是设计这些层。通常,当我们有两个或更多隐藏层时,我们称之为深度神经网络。一般规则是这样的:你的网络越深,它就越能拟合训练数据。但是,过多的深度并不是好事,因为网络可以拟合训练数据到一定程度,以至于当展示新数据时无法泛化(过拟合);同时,它也变得更加计算密集。所以你的任务是构建一个既不太简单(一个神经元)也不太复杂的数据网络。建议你阅读其他人成功实施的不同神经网络架构,以建立对问题过于简单的直觉。从这一点开始,也许三到五层(如果你在 CPU 上训练),观察网络性能。如果表现不佳(欠拟合),则添加更多层。如果你看到过拟合的迹象(稍后讨论),则减少层数。为了了解添加更多层时神经网络的表现,可以在 Tensorflow playground(playground.tensorflow.org)上尝试操作。
全连接层
需要强调的是,在经典的 MLP 网络架构中,层与下一隐藏层是完全连接的。在下面的图中,注意一个层中的每个节点都与前一层的所有节点相连。这被称为全连接网络。这些边是表示该节点对输出值重要性的权重。

一个全连接网络
在后面的章节中,我们将讨论神经网络架构的其他变体(如卷积网络和循环网络)。现在,知道这是最基本的神经网络架构,并且可以用以下任何一种名称来引用:ANN、MLP、全连接网络或前向网络。
让我们做一个快速练习,找出我们示例中有多少条边。假设我们设计了一个具有两个隐藏层的 MLP 网络,每个隐藏层有五个神经元:
-
Weights_0_1: (输入层有 4 个节点) × (层 1 有 5 个节点) + 5 个偏置[每个神经元一个偏置] = 25 条边 -
Weights_1_2: 5 × 5 个节点 + 5 个偏置 = 30 条边 -
Weights_2_output: 5 × 3 个节点 + 3 个偏置 = 18 条边 -
本网络中的总边(权重)数 = 73
在这个非常简单的网络中,我们总共有 73 个权重。这些权重的值是随机初始化的,然后网络通过前向传播和反向传播来学习最适合训练数据的权重最佳值。
要查看这个网络中的权重数量,尝试以下方式在 Keras 中构建这个简单的网络:
model = Sequential([
Dense(5, input_dim=4),
Dense(5),
])
打印模型摘要:
model.summary()
输出将如下所示:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense (Dense) (None, 5) 25
_________________________________________________________________
dense_1 (Dense) (None, 5) 30
_________________________________________________________________
dense_2 (Dense) (None, 3) 18
=================================================================
Total params: 73
Trainable params: 73
Non-trainable params: 0
2.2.4 本节的一些要点
让我们回顾一下我们迄今为止讨论的内容:
-
我们讨论了生物神经元和人工神经元之间的类比:两者都有输入和一个执行某些计算以调节输入信号并创建输出的神经元。
-
我们聚焦于人工神经元的计算,以探索其两个主要功能:加权总和和激活函数。
-
我们看到网络为所有边分配随机权重。这些权重参数反映了这些特征在输出预测中的有用性(或重要性)。
-
最后,我们看到了感知器包含单个神经元。它们是线性函数,产生一条直线来分割线性数据。为了分割更复杂的数据(非线性),我们需要在我们的网络中应用多个神经元来形成一个多层感知器。
-
MLP 架构包含输入特征、连接权重、隐藏层和输出层。
-
我们讨论了感知器学习的高级过程。学习过程是三个主要步骤的重复:前向计算以产生预测(加权总和和激活)、计算误差以及反向传播误差并更新权重以最小化误差。
我们还应该记住一些关于神经网络超参数的重要观点:
-
隐藏层数量 -- 你可以拥有你想要的任何数量的层,每层可以有任意数量的神经元。一般想法是,你拥有的神经元越多,你的网络将越好地学习训练数据。但是,如果你有太多的神经元,这可能会导致一种称为过拟合的现象:网络学习训练集如此之多,以至于它记住了它而不是学习其特征。因此,它将无法泛化。为了获得适当的层数,从一个小的网络开始,并观察网络性能。然后开始添加层,直到你得到满意的结果。
-
激活函数 -- 有许多类型的激活函数,最流行的是 ReLU 和 softmax。建议你在隐藏层中使用 ReLU 激活函数,在输出层中使用 Softmax(你将在本书中的大多数项目中看到这是如何实现的)。
-
误差函数 -- 衡量网络的预测与真实标签之间的距离。均方误差是回归问题的常见度量,交叉熵是分类问题的常见度量。
-
优化器 -- 优化算法用于找到最小化误差的最优权重值。有几种优化器类型可供选择。在本章中,我们讨论了批量梯度下降、随机梯度下降和小批量梯度下降。Adam 和 RMSprop 是两种其他流行的优化器,我们在此不讨论。
-
批处理大小 -- 小批量大小是指网络接收到的子样本数量,之后参数更新发生。较大的批处理大小学习速度更快,但需要更多的内存空间。批处理大小的良好默认值可能是 32。也可以尝试 64、128、256 等。
-
训练轮数 --在训练过程中,整个训练数据集被展示给网络的次数。增加训练轮数,直到验证准确率开始下降,即使训练准确率在增加(过拟合)。
-
学习率 --优化器的一个输入参数,我们需要调整。从理论上讲,过小的学习率保证能够达到最小误差(如果你无限期地训练)。过大的学习率会加快学习速度,但并不保证找到最小误差。大多数深度学习库中优化器的默认
lr值是一个合理的起点,以获得不错的结果。从那里开始,可以按一个数量级上下调整。我们将在第四章中详细讨论学习率。
更多关于超参数的内容
我们尚未讨论的其他超参数包括 dropout 和正则化。在第三章介绍卷积神经网络之后,我们将在第四章中详细讨论超参数调整。
通常,调整超参数的最佳方式是通过试错。通过自己动手做项目以及从其他现有的神经网络架构中学习,你将开始培养出对超参数良好起点的直觉。
学习分析你网络的性能,并了解你需要调整哪个超参数来解决每个症状。这正是本书将要讨论的内容。通过理解这些超参数背后的推理,并观察章节末尾的项目中的网络性能,你将培养出对特定效果调整哪个超参数的直觉。例如,如果你看到你的错误值没有下降并且持续振荡,那么你可能通过降低学习率来解决这个问题。或者,如果你看到网络在训练数据的学习中表现不佳,这可能意味着网络欠拟合,你需要通过添加更多神经元和隐藏层来构建一个更复杂的模型。
2.3 激活函数
当你在构建你的神经网络时,你需要做出的设计决策之一是为你神经元的计算选择哪种激活函数。激活函数也被称为转移函数或非线性,因为它们将加权求和的线性组合转换成非线性模型。激活函数被放置在每个感知器的末尾,以决定是否激活这个神经元。
为什么一定要使用激活函数呢?为什么不能只是计算我们网络的加权求和,并通过隐藏层传播以产生输出呢?
激活函数的目的是将非线性引入网络。没有它,多层感知器将表现得与单个感知器相似,无论我们添加多少层。激活函数需要将输出值限制在某个有限值内。让我们回顾一下预测玩家是否被接受(图 2.11)的例子。

图 2.11 本例重新审视了从 2.1 节预测玩家是否被接受的情况。
首先,模型计算加权总和并产生线性函数(z):
z = 身高 · w[1] + 年龄 · w[2] + b
该函数的输出没有界限。z可以是任何数字。我们使用激活函数将预测值包裹在一个有限值内。在这个例子中,我们使用一个阶跃函数,如果z > 0,则在线上(接受)以上,如果z < 0,则在线下(拒绝)。因此,如果没有激活函数,我们只有一个产生数字的线性函数,但在这种感知器中并没有做出任何决定。激活函数决定了是否触发这个感知器。
激活函数有无限种。实际上,在过去的几年里,在创建最先进的激活函数方面取得了大量进展。然而,仍然相对较少的激活函数能够满足大部分激活需求。让我们更深入地探讨一些最常见的激活函数类型。
2.3.1 线性传递函数
线性传递函数,也称为恒等函数,表示函数将信号通过而不改变。在实践中,输出将等于输入,这意味着我们实际上没有激活函数。所以无论我们的神经网络有多少层,它所做的只是计算一个线性激活函数,或者最多对输入的加权平均值进行缩放。但它不会将输入转换为非线性函数。
激活(z) = z = wx + b
两个线性函数的合成是一个线性函数,所以除非你在神经网络中添加一个非线性激活函数,否则无论你使网络有多深,你都不会计算任何有趣的函数。这里没有学习!
为了理解为什么,让我们计算激活z(x) = w · x + b的导数,其中w = 4 和b = 0。当我们绘制这个函数时,它看起来像图 2.12。然后z(x) = 4x的导数是z'(x) = 4(图 2.13)。

图 2.12 激活函数f(x) = 4x的图像
线性函数的导数是常数:它不依赖于输入值 x。这意味着每次我们进行反向传播时,梯度都将相同。这是一个大问题:因为我们实际上并没有真正改进误差,因为梯度几乎相同。这一点在我们稍后讨论反向传播时会更加清晰。

图 2.13 z(x) = 4x 的导数图像为 z'(x) = 4.
2.3.2 海维塞德步进函数(二元分类器)
步进函数产生二元输出。它基本上说,如果输入 x > 0,则触发(输出 y = 1);否则(输入 < 0),则不触发(输出 y = 0)。它主要用于二元分类问题,如真或假、垃圾邮件或非垃圾邮件、通过或失败(图 2.14)。

图 2.14 步进函数在二元分类问题中常用,因为它将输入转换为零或一。
2.3.3 Sigmoid/logistic 函数
这是最常见的激活函数之一。它通常用于二元分类器中,当你有两个类别时,预测一个类别的概率。sigmoid 函数将所有值压缩到 0 到 1 之间的概率,这减少了数据中的极端值或异常值,而没有移除它们。sigmoid 或 logistic 函数将无限连续变量(范围在 -∞ 到 +∞ 之间)转换为 0 到 1 之间的简单概率。它也被称为 S 形曲线,因为当在图表中绘制时,它会产生 S 形曲线。当步进函数用于产生离散答案(通过或失败)时,sigmoid 函数用于产生通过和失败的概率(图 2.15):
σ(z) = 1/(1 + e^(-1))

图 2.15 当步进函数用于产生离散答案(通过或失败)时,sigmoid 函数用于产生通过或失败的概率。
下面是如何在 Python 中实现 sigmoid 函数的示例:
import numpy as np ❶
def sigmoid(*x*): ❷
return 1 / (1 + np.exp(-x))
❶ 导入 numpy
❷ Sigmoid 激活函数
实时线性代数(可选)
让我们深入探讨 sigmoid 函数的数学方面,以了解它帮助解决的问题以及 sigmoid 函数方程是如何驱动的。假设我们正在尝试根据只有一个特征:年龄,来预测患者是否患有糖尿病。当我们绘制我们关于患者的数据时,我们得到图中的线性模型:
z = β0 + β1 年龄

当我们绘制关于我们患者的数据时,我们得到的线性模型
在这个图中,你可以观察到应该从 0 到 1 变化的概率平衡。注意,当患者年龄低于 25 岁时,预测的概率是负数;而当患者年龄超过 43 岁时,它们高于 1(100%)。这是一个明显的例子,说明了为什么线性函数在大多数情况下不起作用。现在,我们如何修复这个问题,以给出 0 < 概率 < 1 范围内的概率?
首先,我们需要做一些事情来消除所有负概率值。指数函数是解决这个问题的绝佳方案,因为任何东西的指数(我的意思是任何东西)总是正的。所以让我们将这个应用到我们的线性方程中,以计算概率 (p):
p = exp(z) = exp(β[0] + β[1] 年龄)
这个方程确保我们总是得到大于 0 的概率。那么,对于高于 1 的值怎么办?我们需要对它们做些什么。通过比例,任何给定的数除以一个大于它的数,都会得到一个小于 1 的数。让我们对前面的方程做同样的事情。我们将方程除以其值加上一个小值:要么是 1,要么是 a(在某些情况下非常小)的值——让我们称它为 epsilon(ε):
p = exp(z) / (exp(z) + ε)
如果你将方程除以 exp(z),你得到
p = 1/(1 + exp(-z))
当我们绘制这个方程的概率时,我们得到 sigmoid 函数的 S 形,其中概率不再低于 0 或高于 1。事实上,随着患者年龄的增长,概率渐近地接近 1;而当权重向下移动时,函数渐近地接近 0,但永远不会超出 0 < p < 1 的范围。这是 sigmoid 函数和逻辑回归的图像。

随着患者年龄的增长,概率渐近地接近 1。这是 sigmoid 函数和逻辑回归的图像。
Softmax 函数
softmax 函数是 sigmoid 函数的推广。当我们有超过两个类别时,它用于获得分类概率。它迫使神经网络的输出之和为 1(例如,0 < 输出 < 1)。在深度学习问题中,一个非常常见的用例是从许多选项(超过两个)中预测一个单一类别。
softmax 方程如下:

图 2.16 展示了 softmax 函数的一个示例。

图 2.16 softmax 函数将输入值转换为 0 到 1 之间的概率值。
TIP 当你在处理需要预测两个以上类别的问题时,softmax 函数是你在分类器的输出层经常使用的首选函数。如果你正在对两个类别进行分类,softmax 函数也能很好地工作。它基本上会像 sigmoid 函数一样工作。在本节结束时,我会告诉你关于何时使用每个激活函数的建议。
2.3.5 双曲正切函数(tanh)
双曲正切函数是 sigmoid 函数的平移版本。tanh 不是将信号值挤压在 0 到 1 之间,而是将所有值挤压到-1 到 1 的范围内。tanh 几乎总是比 sigmoid 函数在隐藏层中工作得更好,因为它有使你的数据中心化的效果,使得数据的平均值接近零而不是 0.5,这使得下一层的学习变得容易一些:

sigmoid 和 tanh 函数的一个缺点是,如果(z)非常大或非常小,那么这个函数的梯度(或导数或斜率)会变得非常小(接近零),这将减慢梯度下降(图 2.17)。这就是 ReLU 激活函数(下文将解释)提供解决方案的时候。

图 2.17 如果(z)非常大或非常小,那么这个函数的梯度(或导数或斜率)将非常小(接近零)。
2.3.6 矩形线性单元
矩形线性单元(ReLU)激活函数仅在输入大于零时激活节点。如果输入小于零,输出始终为零。但是当输入高于零时,它与输出变量之间存在线性关系。ReLU 函数表示如下:
f(x) = max (0, x)
在撰写本文时,ReLU 被认为是最先进的激活函数,因为它在许多不同的情况下都表现良好,并且它在隐藏层中训练通常比 sigmoid 和 tanh 更好(图 2.18)。

图 2.18 ReLU 函数通过将所有负值输入转换为零来消除输入的所有负值。
这是 ReLU 在 Python 中的实现方式:
def relu(*x*): ❶
if *x* < 0:
return 0
else:
return x
❶ ReLU 激活函数
2.3.7 Leaky ReLU
ReLU 激活的一个缺点是当(x)为负时,导数等于零。Leaky ReLU 是 ReLU 的一种变体,试图减轻这个问题。Leaky ReLU 在(x)为负时引入了一个小的负斜率(大约 0.01),而不是当x < 0 时函数为零。它通常比 ReLU 函数表现更好,尽管在实践中并不常用。看看图 2.19 中的 Leaky ReLU 图;你能看到泄漏吗?
f (x) = max(0.01x, x)
为什么是 0.01?有些人喜欢将其用作另一个可调的超参数,但这将是过度杀戮,因为你已经有其他更大的问题需要担心。请随意尝试在你的模型中使用不同的值(0.1,0.01,0.002)并看看它们的效果如何。

图 2.19 Leaky ReLU 在x < 0 时引入了一个小的负斜率(大约 0.01),而不是当x < 0 时函数为零。
这是 Leaky ReLU 在 Python 中的实现方式:
def leaky_relu(*x*): ❶
if *x* < 0:
return *x* * 0.01
else:
return x
❶ 带有 0.01 泄漏的 Leaky ReLU 激活函数
表 2.1 总结了本节中讨论的各种激活函数。
表 2.1 最常见激活函数速查表
| 激活函数 | 描述 | 图表 | 方程式 |
|---|---|---|---|
| 线性传递函数(恒等函数) | 信号通过它时保持不变。它仍然是一个线性函数。几乎从不使用。 | ![]() |
f(x) = x |
| 海维塞德阶跃函数(二元分类器) | 产生 0 或 1 的二进制输出。主要用于二元分类,以给出离散值。 | ![]() |
![]() |
| Sigmoid/逻辑函数 | 将所有值压缩到 0 和 1 之间的概率,这减少了数据中的极端值或异常值。通常用于分类两个类别。 | ![]() |
![]() |
| Softmax 函数 | sigmoid 函数的推广。当我们有超过两个类别时,用于获得分类概率。 | ![]() |
![]() |
| 双曲正切函数 (tanh) | 将所有值压缩到-1 到 1 的范围内。Tanh 几乎总是比 sigmoid 函数在隐藏层中表现得更好。 | ![]() |
![]() |
| 矩形线性单元 (ReLU) | 只有当输入大于零时才激活节点。总是推荐用于隐藏层。比 tanh 更好。 | ![]() |
-13 |
| Leaky ReLU | 当 x < 0 时,函数为零,而 Leaky ReLU 在 x 为负时引入了一个小的负斜率(大约 0.01)。 | ![]() |
f(x) = max(0.01x, x) |
超参数警告
由于激活函数的种类繁多,选择适合你网络的激活函数可能看起来是一项艰巨的任务。虽然选择一个好的激活函数很重要,但我保证在你设计网络时,这不会是一项具有挑战性的任务。你可以从一些经验法则开始,然后根据需要调整模型。如果你不确定该使用什么,以下是我关于选择激活函数的两分钱建议:
-
对于隐藏层——在大多数情况下,你可以在隐藏层中使用 ReLU 激活函数(或 Leaky ReLU),正如你将在本书中构建的项目中看到的那样。它正变得越来越成为默认选择,因为它比其他激活函数计算得更快。更重要的是,它减少了梯度消失的可能性,因为它不会对大输入值饱和——与 sigmoid 和 tanh 激活函数相反,它们在~ 1 处饱和。记住,梯度是斜率。当函数达到平台期时,这将导致没有斜率;因此,梯度开始消失。这使得下降到最小误差变得更加困难(我们将在后面的章节中更多地讨论这种现象,称为梯度消失/爆炸)。
-
对于输出层——当类别互斥时,softmax 激活函数通常是大多数分类问题的良好选择。当进行二分类时,sigmoid 函数也起到相同的作用。对于回归问题,你可以简单地不使用任何激活函数,因为加权求和节点会产生你需要的连续输出:例如,如果你想根据同一地区的其他房屋价格预测房价。
2.4 前馈过程
现在你已经了解了如何将感知器堆叠成层,通过权重/边连接它们,执行加权求和函数,并应用激活函数,让我们实现完整的前向传递计算以生成预测输出。计算线性组合并应用激活函数的过程称为前馈。在前面的几个部分中,我们简要讨论了前馈几次;让我们更深入地看看在这个过程中发生了什么。
术语前馈用来表示信息从输入层通过隐藏层流向输出层的正向方向。这个过程通过实现两个连续的函数:加权求和和激活函数来完成。简而言之,前向传递是通过层进行计算以做出预测的过程。
让我们看看图 2.20 中的简单三层神经网络,并探索其每个组成部分:
-
层 -- 这个网络由一个具有三个输入特征输入层和三个具有每层 3、4、1 个神经元的隐藏层组成。
-
权重和偏差 (w, b) -- 节点之间的边被分配随机权重,表示为 Wab,其中 (n) 表示层号,(ab) 表示连接第 (n) 层中第 a 个神经元和前一层 (n - 1) 中第 b 个神经元的加权边。例如,W[23]^((2)) 是连接第 2 层第二个节点和第 1 层第三个节点的权重(a[2]² 到 a[1]³)。(请注意,你可以在其他深度学习文献中看到 W[ab]^((n) 的不同表示,只要你在整个网络中遵循一个约定即可。)
偏差被处理得与权重相似,因为它们是随机初始化的,其值在训练过程中学习。因此,为了方便起见,从现在开始我们将使用与权重相同的符号来表示基(w)。在深度学习文献中,你通常会看到所有权重和偏差都表示为(w),以简化表示。
-
激活函数 σ(x) -- 在这个例子中,我们使用 sigmoid 函数 σ(x) 作为激活函数。
-
节点值 (a) -- 我们将计算加权求和,应用激活函数,并将此值分配给节点 amn,其中 n 是层号,m 是层中的节点索引。例如,a 23 表示第 3 层中的第 2 个节点。

图 2.20 一个简单的三层神经网络
2.4.1 前馈计算
我们已经拥有了开始前馈计算所需的一切:

然后我们对第 2 层进行相同的计算

直至第 3 层的输出预测:

就这样!你刚刚计算了一个两层神经网络的正向传播。让我们花点时间来反思一下我们刚刚做了什么。看看我们为这样一个小的网络需要解多少个方程。当我们有一个更复杂的问题,输入层有数百个节点,隐藏层有数百个节点时会发生什么?使用矩阵一次传递多个输入会更有效率。这样做可以大幅提高计算速度,尤其是在使用 NumPy 这样的工具时,我们可以用一行代码来实现这一点。
让我们看看矩阵计算看起来是什么样子(图 2.21)。我们在这里所做的只是简单地将输入和权重堆叠成矩阵并相乘。直观地阅读这个方程的方法是从右到左。从最右边开始,跟我一起来:
-
我们将所有输入堆叠成一个向量(行,列),在这种情况下(3,1)。
-
我们将输入向量与第 1 层的权重矩阵(W^((1)))相乘,然后应用 sigmoid 函数。
-
我们将第 2 层的输出结果(σ · W^((2)))和第 3 层的输出结果(σ · W^((3)))相乘。
-
如果我们有第 4 层,我们将步骤 3 的结果乘以σ · W^((4)),依此类推,直到我们得到最终的预测输出 ŷ!
这里是这个矩阵公式的简化表示:
ŷ = σ · W^((3)) · σ · W^((2)) · σ · W^((1)) · (x)

图 2.21 从左到右阅读,我们将输入堆叠成一个向量,将输入向量与第 1 层的权重矩阵相乘,应用 sigmoid 函数,并乘以结果。
2.4.2 特征学习
隐藏层中的节点(ai)是在每一层学习后的新特征。例如,如果你看图 2.20,你会看到我们有三个特征输入(x1,x2 和 x3)。在第一层进行正向传播计算后,网络学习到模式,这些特征被转换成具有不同值的三个新特征!。然后,在下一层,网络学习模式中的模式并产生新的特征!,依此类推)。每一层产生的特征并不完全理解,我们看不到它们,也没有太多控制它们。这是神经网络魔法的一部分。这就是为什么它们被称为隐藏层。我们所做的是:我们查看最终的输出预测,并调整一些参数,直到我们对网络的性能满意为止。
再次强调,让我们通过一个小例子来看一下。在图 2.22 中,你可以看到一个小的神经网络,它根据三个特征来估算房价:卧室数量、房屋大小以及所在的社区。你可以看到,在第一层的正向传播过程中,原始输入特征值 3、2000 和 1 被转换成了新的特征值!。然后它们再次被转换成预测输出值(ŷ)。在训练神经网络时,我们查看预测输出并与真实价格进行比较,以计算误差并重复此过程,直到我们得到最小误差。

图 2.22 一个小的神经网络,基于三个特征估算房价:卧室数量、房屋大小以及所在的社区
为了帮助可视化特征学习过程,让我们再次看看 Tensorflow playground 中的图 2.9(在此处重复为图 2.23)。你可以看到,第一层学习基本特征,如线条和边缘。第二层开始学习更复杂的特征,如角落。这个过程一直持续到网络的最后几层,学习到更复杂的特征形状,如适合数据集的圆形和螺旋形。

图 2.23 多个隐藏层中的特征学习
那就是神经网络学习新特征的方式:通过网络的隐藏层。首先,它们识别数据中的模式。然后,它们识别模式中的模式;然后模式中的模式中的模式,以此类推。网络越深,它对训练数据了解得就越多。
向量和矩阵复习
如果你理解了我们刚才在正向传播讨论中做的矩阵计算,你可以自由跳过这个边栏。如果你仍然不确信,请耐心等待:这个边栏是为你准备的。
正向传播的计算是一组矩阵乘法。虽然你不会手动进行这些计算,因为有很多优秀的深度学习库可以只用一行代码为你完成这些计算,但了解底层发生的数学原理是有价值的,这样你可以调试你的网络。特别是因为这个过程非常简单且有趣,让我们快速回顾一下矩阵计算。
让我们从矩阵的一些基本定义开始:
-
标量是一个单一的数字。
-
向量是一组数字。
-
矩阵是一个二维数组。
-
张量是一个 n 维数组,其中 n > 2。

矩阵维度:标量是一个单一的数字,向量是一组数字的数组,矩阵是一个二维数组,张量是一个 n 维数组。
我们将遵循大多数数学文献中使用的惯例:
-
标量用小写和斜体表示:例如,n。
-
向量用小写、斜体和粗体表示:例如,x。
-
矩阵用大写、斜体和粗体表示:例如,X。
-
矩阵维度表示如下:(行 × 列)。
乘法:
-
矩阵的标量乘法——简单地将标量数乘以矩阵中的所有数。注意,标量乘法不会改变矩阵的维度:
![图片]()
-
矩阵乘法——当乘以两个矩阵时,例如在 (行[1] × 列[1]) × (行[2] × 列[2]) 的情况下,列[1] 和行[2] 必须相等,其乘积将具有 (行[1] × 列[2]) 的维度。例如,

其中 x = 3 · 13 + 4 · 8 + 2 · 6 = 83,y = 63 和 z = 37 同理。
现在你已经知道了矩阵乘法规则,拿出一张纸,处理一下之前神经网络示例中的矩阵维度。以下图再次显示了矩阵方程,以供你方便参考。

主文中的矩阵方程。使用它来处理矩阵维度。
我希望你们对矩阵了解的最后一件事是转置。通过转置,你可以将行向量转换为列向量,反之亦然,其中形状 (m × n) 被反转并变为 (n × m)。转置矩阵使用上标 (A^T) 表示:

2.5 误差函数
到目前为止,你已经学会了如何在神经网络中实现前向传递以生成由加权求和加激活操作组成的预测。现在,我们如何评估网络刚刚生成的预测?更重要的是,我们如何知道这个预测离正确答案(标签)有多远?答案是:测量误差。误差函数的选择是神经网络设计的重要方面之一。误差函数也可以称为代价函数或损失函数,这些术语在深度学习文献中可以互换使用。
2.5.1 什么是误差函数?
误差函数是衡量神经网络预测相对于预期输出(标签)的“错误程度”的度量。它量化了我们离正确解有多远。例如,如果我们有一个高的损失,那么我们的模型做得不好。损失越小,模型做得越好。损失越大,我们的模型就需要更多的训练来提高其准确性。
2.5.2 为什么我们需要误差函数?
计算误差是一个优化问题,这是所有机器学习工程师都喜欢的(数学家也是如此)。优化问题侧重于定义一个误差函数并尝试优化其参数以获得最小误差(关于优化的更多内容将在下一节中介绍)。但就现在而言,要知道,在一般情况下,当我们处理优化问题时,如果我们能够为问题定义误差函数,我们就很有机会通过运行优化算法来最小化误差函数来解决它。
在优化问题中,我们的最终目标是找到最优变量(权重),以尽可能多地最小化误差函数。如果我们不知道我们离目标有多远,我们怎么知道在下一轮迭代中要改变什么?最小化这个错误的过程称为误差函数优化。在下一节中,我们将回顾几种优化方法。但就目前而言,我们需要从误差函数中了解的是我们离正确预测有多远,或者我们偏离了期望的性能程度有多远。
2.5.3 错误始终为正
考虑以下场景:假设我们有两个数据点,我们试图让我们的网络正确预测。如果第一个数据点产生 10 的错误,而第二个数据点产生-10 的错误,那么我们的平均错误为零!这具有误导性,因为“错误=0”意味着我们的网络正在产生完美的预测,而实际上它两次都偏离了 10。我们不想这样。我们希望每个预测的错误都是正数,这样当我们计算平均错误时,错误就不会相互抵消。想象一个弓箭手瞄准目标并偏离了 1 英寸。我们并不真正关心他们偏离的方向;我们只需要知道每一箭与目标的距离。
图 2.24 显示了两个独立模型随时间变化的损失函数的可视化。你可以看到模型#1 在最小化错误方面做得更好,而模型#2 在 6 个 epoch 之前表现更好,然后趋于平稳。
不同的损失函数会对相同的预测给出不同的错误,从而对模型的性能产生相当大的影响。对损失函数的详细讨论超出了本书的范围。相反,我们将专注于两种最常用的损失函数:均方误差(及其变体),通常用于回归问题,以及交叉熵,用于分类问题。

图 2.24 两个独立模型损失函数随时间变化的可视化
2.5.4 均方误差
均方误差(MSE)在需要输出为实值(如房价)的回归问题中常用。与仅比较预测输出与标签(ŷ[i] - y[i])不同,误差被平方并平均到数据点的数量,正如你在这个方程中看到的那样:

MSE 有几个优点。平方确保了错误始终为正数,并且较大的错误比较小的错误受到更多的惩罚。此外,它使数学变得简单,这始终是一个加分项。公式中的符号列在表 2.2 中。
均方误差(MSE)对异常值非常敏感,因为它平方了误差值。这可能不是你正在解决的问题的具体问题。事实上,对异常值的敏感性在某些情况下可能是有益的。例如,如果你正在预测股票价格,你会希望考虑异常值,对异常值的敏感性会是一件好事。在其他情况下,你可能不希望构建一个受异常值偏斜的模型,例如预测一个城市的房价。在这种情况下,你更感兴趣的是中位数,而不是平均值。为了这个目的,开发了一种均方误差(MSE)的变体,称为平均绝对误差(MAE),它在整个数据集上平均绝对误差,而不对误差值进行平方:
表 2.2 回归问题中使用的符号含义
| 符号 | 含义 |
|---|---|
| E(W, b) | 损失函数。在其他文献中也标注为 J(W, b)。 |
| W | 权重矩阵。在某些文献中,权重用希腊字母θ表示。 |
| b | 偏置向量。 |
| N | 训练样本数量。 |
| ŷi | 预测输出。在某些深度学习文献中也表示为 hw, b(x)。 |
| y[i] | 正确的输出(标签)。 |
| (ŷi - yi) | 通常称为残差。 |

2.5.5 交叉熵
交叉熵在分类问题中常用,因为它量化了两个概率分布之间的差异。例如,假设对于特定的训练实例,我们正在尝试从三个可能的类别(狗、猫、鱼)中分类一张狗的图片。这个训练实例的真实分布如下:
Probability(cat) P(dog) P(fish)
0.0 1.0 0.0
我们可以将这个“真实”分布理解为训练实例有 0%的概率属于类别 A,100%的概率属于类别 B,0%的概率属于类别 C。现在,假设我们的机器学习算法预测以下概率分布:
Probability(cat) P(dog) P(fish)
0.2 0.3 0.5
预测分布与真实分布有多接近?这就是交叉熵损失函数所确定的。我们可以使用以下公式:

其中 (y) 是目标概率,(p) 是预测概率,(m) 是类别数量。求和是针对三个类别:猫、狗和鱼。在这种情况下,损失是 1.2:
E = - (0.0 * log(0.2) + 1.0 * log(0.3) + 0.0 * log(0.5)) = 1.2
因此,这就是我们的预测与真实分布之间的“错误”或“远离”程度。
让我们再来一次,只是为了展示当网络做出更好的预测时损失是如何变化的。在之前的例子中,我们向网络展示了一只狗的图片,它预测这张图片有 30%的可能性是狗,这离目标预测非常远。在后续的迭代中,网络学习了一些模式,并将预测结果稍微改进,达到了 50%:
Probability(cat) P(dog) P(fish)
0.3 0.5 0.2
然后我们再次计算损失:
E = - (0.0*log(0.3) + 1.0*log(0.5) + 0.0*log(0.2)) = 0.69
你可以看到,当网络做出更好的预测(狗的概率从 30%上升到 50%)时,损失从 1.2 下降到 0.69。在理想情况下,当网络预测图像有 100%的可能性是狗时,交叉熵损失将是 0(不妨尝试一下数学计算)。
为了计算所有训练示例(n)的交叉熵误差,我们使用这个通用公式:

注意:重要的是要注意,你不会手动进行这些计算。理解底层的工作原理,当你设计神经网络时,会给你更好的直觉。在深度学习项目中,我们通常使用 Tensorflow、PyTorch 和 Keras 等库,其中误差函数通常是一个参数选择。
2.5.6 关于误差和权重的一个最后说明
如前所述,为了使神经网络学习,它需要尽可能最小化误差函数(0 是理想的)。误差越低,模型在预测值方面的准确性就越高。我们如何最小化误差?
让我们通过以下具有单个输入的感知器示例来了解权重和误差之间的关系:

假设输入x = 0.3,其标签(目标预测)y = 0.8。这个感知器的预测输出(ŷ)计算如下:
ŷ[i] = w · x = w · 0.3
误差,在其最简单形式中,是通过比较预测ŷ和标签y来计算的:
error = |ŷ - y |
= |(w · x) - y |
= |w · 0.3 - 0.8|
如果你观察这个误差函数,你会注意到输入(x)和目标预测(y)是固定值。对于这些特定的数据点,它们永远不会改变。在这个方程中,我们唯一可以改变的两个变量是误差和权重。现在,如果我们想达到最小误差,我们可以调整哪个变量?正确:权重!权重充当网络需要上下调整以获得最小误差的旋钮。这就是网络学习的方式:通过调整权重。当我们绘制误差函数相对于权重的图像时,我们得到图 2.25 中所示的图表。

图 2.25 网络通过调整权重来学习。当我们绘制误差函数相对于权重的图像时,我们得到这种类型的图表。
如前所述,我们用随机权重初始化网络。权重位于这条曲线上,我们的任务是让它沿着曲线下降到最小误差的最优值。寻找神经网络目标权重的过程是通过使用优化算法在迭代过程中调整权重值来实现的。
2.6 优化算法
训练神经网络涉及向网络展示许多示例(训练数据集);网络通过前向计算进行预测,并将预测结果与正确标签进行比较以计算误差。最后,神经网络需要调整权重(所有边上的权重)直到它得到最小误差值,这意味着最大准确度。现在,我们只需要构建能够为我们找到最优权重的算法。
2.6.1 优化是什么?
啊哈,优化!这是一个我非常喜爱,也是每个机器学习工程师(数学家也是如此)都喜爱的主题。优化是一种将问题框架化的方式,以最大化或最小化某个值。计算误差函数的最好之处在于,我们将神经网络转化为一个优化问题,我们的目标是最小化误差。
假设你想优化从家到工作的通勤。首先,你需要定义你正在优化的指标(误差函数)。也许你想优化通勤的成本、时间或距离。然后,基于这个特定的损失函数,你通过改变一些参数来工作以最小化其值。改变参数以最小化(或最大化)一个值被称为优化。如果你选择损失函数为成本,你可能选择一个需要两小时的更长通勤,或者(假设性地)你可能步行五小时以最小化成本。另一方面,如果你想优化通勤时间,你可能愿意花 50 美元乘坐出租车,将通勤时间缩短到 20 分钟。基于你定义的损失函数,你可以开始改变你的参数以获得你想要的结果。
TIP 在神经网络中,优化误差函数意味着更新权重和偏差,直到我们找到最优权重,或者产生最小误差的最佳权重值。
让我们看看我们正在尝试优化的空间:

在最简单的神经网络形式中,一个只有一个输入的感知器,我们只有一个权重。我们可以很容易地绘制出相对于这个权重的误差(我们试图最小化的误差),如图 2.26 中的 2D 曲线(之前已展示)。

图 2.26 对于单个感知器,误差函数相对于其权重的 2D 曲线。
但如果我们有两个权重呢?如果我们绘制这两个权重的所有可能值,我们得到一个包含误差的 3D 平面(图 2.27)。

图 2.27 绘制两个权重的所有可能值得到一个 3D 误差平面。
那么对于超过两个权重的情况呢?您的网络可能拥有数百或数千个权重(因为您的网络中的每条边都有自己的权重值)。由于我们人类只能理解最多 3 个维度,当我们有 10 个权重时,我们无法可视化错误图,更不用说数百或数千个权重参数了。因此,从现在开始,我们将使用错误函数的 2D 或 3D 平面来研究错误。为了优化模型,我们的目标是搜索这个空间,找到能够实现最低可能错误的最佳权重。
我们为什么需要一个优化算法?难道我们不能只是通过尝试大量的权重值(比如 1,000 个值)直到我们得到最小误差吗?
假设我们使用了一种暴力方法,只是尝试了大量的不同可能的权重(比如说 1,000 个值),并找到了产生最小误差的权重。这能行得通吗?好吧,从理论上讲,是的。这种方法在我们只有很少的输入并且网络中只有一个或两个神经元时可能有效。让我尝试说服您这种方法不会扩展。让我们看看一个非常简单的神经网络场景。假设我们只想根据四个特征(输入)和一个包含五个神经元的隐藏层来预测房价(见图 2.28)。

图 2.28 如果我们只想根据四个特征(输入)和一个包含五个神经元的隐藏层来预测房价,我们将有从输入层到隐藏层的 20 个边缘(权重),再加上从隐藏层到输出预测的 5 个权重。
如您所见,我们从输入层到隐藏层的边缘(权重)有 20 个,再加上从隐藏层到输出预测的 5 个权重,总共需要调整 25 个权重变量以获得最佳值。如果我们为每个权重尝试 1,000 个不同的值,那么我们将有总共 1,075 种组合:
1,000 × 1,000 × ... × 1,000 = 1,000²⁵ = 1,075 种组合
假设我们能够得到世界上最快的超级计算机:神威·太湖之光,其运行速度为 93 petaflops ⇒ 93 × 10¹⁵ 每秒浮点运算(FLOPs)。在最佳情况下,这台超级计算机将需要

这是一个巨大的数字:它比宇宙存在的时间还要长。谁有那么多时间等待网络训练?记住,这是一个非常简单的神经网络,通常使用智能优化算法只需要几分钟就能训练。在现实世界中,您将构建更复杂的网络,这些网络有数千个输入和数十个隐藏层,并且您需要在几小时(或几天,有时是几周)内训练它们。因此,我们必须想出一种不同的方法来找到最佳权重。
希望我已经说服你,通过暴力优化过程不是答案。现在,让我们研究神经网络中最受欢迎的优化算法:梯度下降。梯度下降有几个变体:批量梯度下降(BGD)、随机梯度下降(SGD)和迷你批量 GD(MB-GD)。
2.6.2 批量梯度下降
梯度的通用定义(也称为导数)是,它是告诉你曲线在任意给定点的切线斜率或变化率的函数。这只是曲线斜率或陡度的花哨说法(图 2.29)。

图 2.29 梯度是描述曲线在任意给定点的切线斜率变化率的函数。
梯度下降简单来说就是迭代更新权重,以下降误差曲线的斜率,直到我们到达最小误差的点。让我们看看我们之前引入的关于权重的误差函数。在初始权重点,我们计算误差函数的导数以得到下一步的斜率(方向)。我们重复这个过程,沿着曲线向下走,直到我们达到最小误差(图 2.30)。

图 2.30 梯度下降通过增量步骤下降误差函数。
梯度下降是如何工作的?
为了可视化梯度下降的工作原理,让我们在 3D 图中绘制误差函数(图 2.31),并逐步分析这个过程。随机初始权重(起始权重)位于点 A,我们的目标是下降到误差山的目标权重值 w[1] 和 w[2],这些权重值产生最小的误差值。我们这样做的方式是通过一系列沿着曲线的步骤,直到我们得到最小误差。为了下降误差山,我们需要确定每一步的两个东西:
-
步长方向(梯度)
-
步长大小(学习率)

图 2.31 随机初始权重(起始权重)位于点 A。我们下降到产生最小误差值的权重值 w[1] 和 w[2],以下降误差山。
方向(梯度)
假设你站在误差山的顶部点 A。为了到达底部,你需要确定导致最深下降(具有最陡斜率)的步长方向。那么斜率是什么呢?它是曲线的导数。所以如果你站在那座山的顶部,你需要看看你周围的各个方向,找出哪个方向会导致最深下降(例如 1、2、3 或 4)。假设是方向 3;我们选择那条路。这把我们带到了点 B,然后我们重新开始这个过程(计算前向传播和误差)并找到最深下降的方向,以此类推,直到我们到达山的底部。
这个过程被称为梯度下降。通过对权重相对于误差的导数(dE / Dw)进行计算,我们得到我们应该采取的方向。现在还有一件事。梯度只决定了方向。步长的大小应该是多少?它可能是一英尺的步长,也可能是一百英尺的跳跃。这是我们接下来需要确定的事情。
步长(学习率α)
学习率是网络在下降误差山时每一步的大小,通常用希腊字母 alpha(α)表示。它是你在训练神经网络时调整的最重要超参数之一(关于这一点稍后还会讨论)。较大的学习率意味着网络将学习得更快(因为它以更大的步长下降山),而较小的步长意味着学习较慢。听起来很简单。让我们使用大的学习率,在几分钟内完成神经网络训练,而不是等待几个小时。对吧?并不完全是这样。让我们看看如果我们设置一个非常大的学习率值可能会发生什么。
在图 2.32 中,你从点 A 开始。当你沿着箭头方向迈出大步时,你不会下降误差山,而是最终到达点 B,在另一边。然后另一个大步带你到 C,以此类推。误差将持续振荡,永远不会下降。我们稍后会更多地讨论调整学习率以及如何确定误差是否在振荡。但就目前而言,你需要知道这一点:如果你使用一个非常小的学习率,网络最终会下降到山脚下,并达到最小误差。但这种训练会花费更长的时间(可能是几周或几个月)。另一方面,如果你使用一个非常大的学习率,网络可能会持续振荡,永远不会训练。所以我们通常将学习率初始化为 0.1 或 0.01,并观察网络的性能,然后进一步调整。

图 2.32 设置一个非常大的学习率会导致误差振荡而永远不会下降。
将方向和步长结合起来
通过将方向(导数)乘以步长(学习率),我们得到每一步的权重变化:

我们添加负号是因为导数总是计算向上的斜率。由于我们需要下降山,我们就沿着斜率的反方向前进:
w[next-step] = w[current] + Δw
微积分复习:计算偏导数
导数是变化的研究。它测量了图表上特定点的曲线的陡峭程度。

我们想要找到曲线在确切权重点的陡峭程度。
看起来数学已经给了我们我们正在寻找的东西。在误差图表上,我们想要找到曲线在确切权重点的陡峭程度。谢谢,数学!
导数的其他术语是斜率和变化率。如果误差函数表示为 E(x),那么误差函数关于权重的导数表示为
d/dw•E(x)或简称为dE(x)/dw。
这个公式显示了当我们改变权重时,总误差将如何变化。
幸运的是,数学家为我们制定了一些规则来计算导数。由于这不是一本数学书,我们不会讨论这些规则的证明。相反,我们将从这一点开始应用这些规则来计算我们的梯度。以下是一些基本的导数规则:

让我们看看一个简单的函数来应用导数规则:
f(x) = 10x⁵ + 4x⁷ + 12x
我们可以应用幂、常数和求和规则来得到df/df,也称为f' (x):
然后,f' (x) = 50x⁴ + 28x⁶ + 12
为了理解这意味着什么,让我们绘制f(x)的图像:

使用简单函数应用导数规则。要得到任何点的斜率,我们可以在该点计算 f' (x)。
如果我们想在任何一点得到斜率,我们可以在该点计算 f ' (x)。所以 f ' (2)给出了左侧线的斜率,而 f ' (6)给出了第二条线的斜率。明白了吗?
对于导数的最后一个例子,让我们应用幂规则来计算 sigmoid 函数的导数:

注意,你不需要记住导数规则,也不需要自己计算函数的导数。多亏了出色的深度学习社区,我们有了伟大的库,只需一行代码就能为你计算这些函数。但了解底层发生的事情是有价值的。
批量梯度下降的陷阱
梯度下降是一个非常强大的算法,用于达到最小误差。但它有两个主要陷阱。
首先,并非所有成本函数都像我们之前看到的简单碗一样。可能会有洞、脊和各种不规则地形,这使得达到最小误差非常困难。考虑图 2.33,其中误差函数稍微复杂一些,有起伏。

图 2.33 复杂的误差函数由更复杂的曲线表示,具有许多局部最小值。我们的目标是达到全局最小值。
记住,在权重初始化期间,起点是随机选择的。如果梯度下降算法的起点如图所示,误差将开始下降右侧的小山,并确实达到一个最小值。但这个最小值,称为局部最小值,并不是这个误差函数可能的最小误差值。它是算法随机开始的地方的局部山的最小值。相反,我们想要达到最低可能的误差值,即全局最小值。
第二,批量梯度下降在每一步都使用整个训练集来计算梯度。还记得这个损失函数吗?

这意味着如果您的训练集(n)有 1 亿(1 亿)条记录,算法需要计算 1 亿条记录的总和才能迈出一步。这在计算上非常昂贵且缓慢。这就是为什么这个算法也被称为批量梯度下降——因为它在一次批次中使用了整个训练数据。
解决这两个问题的一个可能方法是随机梯度下降。我们将在下一节中探讨 SGD。
2.6.3 随机梯度下降
在随机梯度下降中,算法随机选择数据点,并逐个数据点进行梯度下降(图 2.34)。这提供了许多不同的权重起始点,并下降到所有山峰以计算它们的局部最小值。然后,所有这些局部最小值中的最小值就是全局最小值。这听起来非常直观;这就是 SGD 算法背后的概念。

图 2.34 随机梯度下降算法随机选择曲线上的数据点,并将它们全部下降以找到局部最小值。
随机只是一个花哨的词,指的是随机。随机梯度下降可能是机器学习中最常用的优化算法,尤其是在深度学习中。虽然梯度下降测量整个训练集的损失和梯度,以向最小值迈出一步,但 SGD 在每一步随机选择训练集中的单个实例,并仅基于该单个实例计算梯度。让我们看一下 GD 和 SGD 的伪代码,以更好地理解这些算法之间的差异:
| GD | 随机 GD |
|---|
|
-
取用所有数据。
-
计算梯度。
-
更新权重并向下移动一步。
-
重复执行 n 个 epoch(迭代)。
GD 沿着误差曲线的平滑路径下降 |
-
随机打乱训练集中的样本。
-
选择一个数据实例。
-
计算梯度。
-
更新权重并向下移动一步。
-
选择另一个数据实例。
-
重复执行 n 个 epoch(训练迭代)。
SGD 沿着误差曲线的振荡路径下降 |
由于我们在批量梯度下降中在整个训练数据上计算梯度后才会进行一步,因此你可以看到误差下降的路径是平滑的,几乎是一条直线。相比之下,由于随机梯度下降(SGD)的随机(随机)性质,你看到指向全局成本最小值的路径不是直接的,如果在二维空间中可视化成本表面,它可能会出现曲折。这是因为 SGD 中,每次迭代都试图更好地拟合单个训练示例,这使得它变得非常快,但并不保证每一步都会使曲线下降。它将接近全局最小值,一旦到达那里,它将继续弹跳,永远不会稳定下来。在实践中,这并不是一个问题,因为接近全局最小值对于大多数实际应用来说已经足够好了。SGD 几乎总是比批量梯度下降表现得更好、更快。
2.6.4 小批量梯度下降
小批量梯度下降(MB-GD)是批量梯度下降(BGD)和随机梯度下降(SGD)之间的折中方案。我们不是从单个样本(SGD)或所有样本(BGD)计算梯度,而是将训练样本分成小批量,从这些小批量中计算梯度(常见的批量大小是 k = 256)。由于我们更频繁地更新权重,MB-GD 比 BGD 收敛得更快;然而,MB-GD 允许我们使用向量运算,这通常会导致比 SGD 更好的计算性能提升。
2.6.5 梯度下降要点
这里有很多内容,让我们总结一下,好吗?以下是我脑海中总结的梯度下降方法:
-
三种类型:批量、随机和小批量。
-
所有这些方法都遵循相同的概念:
-
找到最陡斜率的方向:误差相对于权重的一阶导数 dE/Dw[i]
-
设置学习率(或步长)。算法将计算斜率,但你会将学习率作为一个超参数来设置,并通过试错法进行调整。
-
将学习率从 0.01 开始,然后降至 0.001、0.0001、0.00001。你设置的学习率越低,你越有保证能够下降到最小误差(如果你无限期地训练)。由于我们没有无限的时间,0.01 是一个合理的起点,然后我们从这个值开始逐渐降低。
-
-
批量梯度下降(Batch GD)在计算所有训练数据的梯度后更新权重。当数据量很大时,这可能在计算上非常昂贵。它扩展性不好。
-
随机梯度下降(Stochastic GD)在计算训练数据单个实例的梯度后更新权重。SGD 比批量梯度下降(BGD)更快,并且通常非常接近全局最小值。
-
小批量梯度下降(Mini-batch GD)是批量梯度下降和随机梯度下降之间的折中方案,既不使用所有数据,也不使用单个实例。相反,它选取一组训练实例(称为小批量),在这些实例上计算梯度并更新权重,然后重复此过程,直到处理完所有训练数据。在大多数情况下,MB-GD 是一个很好的起点。
-
batch_size是一个需要调整的超参数。这一点将在第四章的超参数调整部分再次提到。但通常,你可以从 batch_size = 32, 64, 128, 256 开始实验。 -
不要将 batch_size 与 epochs 混淆。一个 epoch 是在所有训练数据上的完整循环。批次数是在我们计算梯度的组中训练样本的数量。例如,如果我们有 1,000 个样本在训练数据中,并且将 batch_size 设置为 256,那么 epoch 1 = 256 个样本的 batch 1 加上 batch 2(256 个样本)加上 batch 3(256 个样本)加上 batch 4(232 个样本)。
-
最后,你需要知道,多年来已经使用了大量的梯度下降变体,这是一个非常活跃的研究领域。其中一些最受欢迎的改进包括
-
Nesterov 加速梯度
-
RMSprop
-
Adam
-
Adagrad
不要担心这些优化器。在第四章中,我们将更详细地讨论调整技术来改进你的优化器。
我知道这听起来很多,但请继续听。这些都是我想让你从本节记住的主要事情:
-
梯度下降是如何工作的(斜率加步长)
-
批量、随机和迷你批量的梯度下降之间的区别
-
你将调整的 GD 超参数:学习率和 batch_size
如果你已经掌握了这些,你就可以进入下一节了。而且不要过于担心超参数调整。我将在接下来的章节中更详细地介绍网络调整,并在本书中的几乎所有项目中。
2.7 反向传播
反向传播是神经网络学习的基础。到目前为止,你已经了解到训练神经网络通常通过重复以下三个步骤来完成:
-
前馈:获取线性组合(加权求和),并应用激活函数以获得输出预测(ŷ):
ŷ = σ · W^((3)) · σ · W^((2)) · σ · W^((1)) · (x)
-
将预测与标签进行比较,以计算误差或损失函数:
![]()
-
使用梯度下降优化算法来计算Δw,以优化误差函数:

通过网络反向传播Δw 以更新权重:

在本节中,我们将深入探讨最后一步:反向传播。
2.7.1 什么是反向传播?
反向传播,或反向传递,意味着传播误差相对于每个特定权重的导数
dE/dw**[i]
从最后一层(输出)回到第一层(输入)以调整权重。通过从预测节点(ŷ)向后传播Δw,穿过隐藏层并回到输入层,权重得到更新:
(w**[next-step] = w**[current] + Δw)
这将使误差沿着误差山下降一步。然后循环再次开始(步骤 1 到 3)以更新权重并将误差再下降一步,直到我们达到最小误差。
当我们只有一个权重时,反向传播可能听起来更清晰。我们只需通过添加 Δw 到旧权重 w**[new] = w- α•dE/dw**[i] 来调整权重。
但当我们有一个具有许多权重变量的多层感知器(MLP)网络时,事情就会变得复杂。为了使这一点更清晰,请考虑图 2.35 中的场景。

图 2.35 当我们有一个具有许多权重变量的多层感知器(MLP)网络时,反向传播变得复杂。
我们如何计算总误差相对于 w[13] 的变化率 dE/dw**[13]?记住,dE/dw**[13]基本上是说,“当我们改变参数 w[13] 时,总误差会有多少变化?”
我们通过在误差函数上应用导数规则学习了如何计算 dE/dw**[21]。这很简单,因为 w[21] 是直接连接到误差函数的。但为了计算总误差相对于权重直到输入的导数,我们需要一个微积分规则,称为链式法则。
微积分复习:导数的链式法则
再次回到微积分。还记得我们之前列出的导数规则吗?其中最重要的规则之一就是链式法则。让我们深入探讨它,看看它在反向传播中的实现方式:

链式法则是计算由其他函数内部函数组成的函数的导数的公式。它也被称为外-内规则。看看这个:

链式法则指出,“在函数组合中,导数只是相乘。”这在实现反向传播时对我们非常有用,因为前向传播只是组合了一组函数,而反向传播是在这个函数的每一部分上求导。
为了在反向传播中实现链式法则,我们只需要将多个偏导数相乘,以得到误差效应一直回到输入层。下面是如何工作的——但首先,记住我们的目标是反向传播误差直到输入层。所以在这个例子中,我们想要计算 dE/dx,这是总误差对输入 (x) 的影响:

我们在这里所做的只是将上游梯度乘以局部梯度,直到我们得到目标值。
图 2.36 显示了反向传播如何使用链式法则将梯度反向流动通过网络。让我们应用链式法则来计算误差相对于第一个输入的第三个权重 w1,3(1) 的导数,其中 (1) 表示第 1 层,w1,3 表示节点编号 1 和权重编号 3:


图 2.36 反向传播使用链式法则将梯度反向流动通过网络。
方程式一开始可能看起来很复杂,但我们实际上只是在从输出节点开始,将边的偏导数乘到输入节点。所有这些符号都使得这个方程看起来很复杂,但一旦你理解了如何读取 w[1,3]^((1)) ,反向传播方程看起来就像这样:
反向传播到边 w[1,3]^((1)) 的错误 = 边 4 上的错误效应 × 边 3 上的错误效应 × 边 2 上的错误效应 × 目标边的错误效应
这就是神经网络用来更新权重以最佳拟合我们问题的反向传播技术。

图 2.37 前向传播计算输出预测(左)。反向传播将误差的导数反向传播以更新其权重(右)。
2.7.2 反向传播要点
-
反向传播是神经元的学习过程。
-
反向传播反复调整网络中连接(权重)的权重,以最小化成本函数(实际输出向量与期望输出向量之间的差异)。
-
由于权重调整的结果,隐藏层开始代表除了输入层中代表的特征之外的重要特征。
-
对于每一层,目标是找到一组权重,确保对于每个输入向量,产生的输出向量与(或接近)期望的输出向量相同。产生的输出和期望输出之间的差异称为误差函数。
-
反向传播(图 2.37)从网络的末端开始,递归地应用链式法则来计算梯度,将错误反向传播或反馈,一直计算到网络的输入端,然后更新权重。
-
再次强调,典型神经网络问题的目标是发现一个最佳拟合我们数据的模型。最终,我们希望通过选择最佳的一组权重参数来最小化成本或损失函数。
摘要
-
感知器对于可以用一条直线(线性操作)分离的数据集效果良好。
-
无法用直线建模的非线性数据集需要一个包含许多神经元的更复杂的神经网络。通过层叠神经元创建多层感知器。
-
网络通过重复三个主要步骤来学习:前向传播、计算误差和优化权重。
-
参数是网络在训练过程中更新的变量,如权重和偏差。这些在训练过程中由模型自动调整。
-
超参数是你调整的变量,例如层数、激活函数、损失函数、优化器、早停和学习率。我们在训练模型之前调整这些参数。
3 卷积神经网络
本章涵盖
-
使用 MLP 对图像进行分类
-
使用 CNN 架构对图像进行分类
-
理解在彩色图像上的卷积
在之前,我们讨论了人工神经网络(ANNs),也称为多层感知器(MLPs),它们基本上是由具有可学习权重和偏差的神经元层堆叠而成的。每个神经元接收一些输入,这些输入通过它们的权重相乘,并通过激活函数应用非线性。在本章中,我们将讨论卷积神经网络(CNNs),它们被认为是 MLP 架构的一种演变,在图像处理方面表现更好。
本章的高级布局如下:
-
使用 MLP 进行图像分类 —— 我们将从一个使用 MLP 拓扑进行图像分类的小型项目开始,检查常规神经网络架构如何处理图像。你将了解 MLP 架构在处理图像时的缺点以及为什么我们需要一个新的、创新的神经网络架构来完成这项任务。
-
理解 CNN —— 我们将探索卷积网络,了解它们如何从图像中提取特征并对对象进行分类。你将了解 CNN 的三个主要组成部分:卷积层、池化层和全连接层。然后我们将应用这些知识在另一个小型项目中使用 CNN 对图像进行分类。
-
彩色图像 —— 我们将比较计算机如何看到彩色图像与灰度图像,以及卷积如何在彩色图像上实现。
-
图像分类项目 —— 我们将应用本章所学的一切,在一个端到端的图像分类项目中使用 CNN 对彩色图像进行分类。
网络如何学习和优化参数的基本概念在 MLP 和 CNN 中是相同的:
-
架构 —— MLP 和 CNN 由堆叠在一起的神经元层组成。CNN 具有不同的结构(卷积层与全连接层),正如我们将在接下来的章节中看到的。
-
权重和偏差 —— 在卷积层和全连接层中,推理工作方式相同。两者都有初始随机生成的权重和偏差,其值由网络学习。它们之间的主要区别在于,MLP 中的权重以向量形式存在,而在卷积层中,权重以卷积滤波器或核的形式存在。
-
超参数 —— 与 MLP 一样,当我们设计 CNN 时,我们总会指定误差函数、激活函数和优化器。前几章中解释的所有超参数保持不变;我们将添加一些特定于 CNN 的新超参数。
-
训练 -- 两个网络以相同的方式进行学习。首先,它们执行前向传播以获得预测;其次,它们将预测与真实标签进行比较以获得损失函数(y − ŷ);最后,它们使用梯度下降优化参数,将错误反向传播到所有权重,并更新它们的值以最小化损失函数。
准备好了吗?让我们开始吧!
3.1 使用 MLP 进行图像分类
让我们回顾一下第二章中的 MLP 架构。神经元堆叠在彼此之上,通过权重连接。MLP 架构由一个输入层、一个或多个隐藏层和一个输出层组成(图 3.1)。

图 3.1 MLP 架构由通过权重连接的神经元层组成。
本节将利用您从第二章了解的关于 MLP 的知识,使用 MNIST 数据集解决图像分类问题。这个分类器的目标将是将 0 到 9 的数字图像(10 个类别)进行分类。首先,让我们看看我们 MLP 架构的三个主要组件(输入层、隐藏层和输出层)。
3.1.1 输入层
当我们处理 2D 图像时,在将它们输入网络之前,我们需要对它们进行预处理,使其成为网络可以理解的形式。首先,让我们看看计算机是如何感知图像的。在图 3.2 中,我们有一个宽度为 28 像素、高度为 28 像素的图像。计算机将这个图像视为一个 28×28 的矩阵,像素值范围从 0 到 255(0 为黑色,255 为白色,介于两者之间的为灰度)。

图 3.2 计算机将这个图像视为一个像素值范围在 0 到 255 之间的 28×28 矩阵。
由于 MLP 只接受维度为(1,p)的 1D 向量作为输入,因此它们不能接受维度为(x, y)的原始 2D 图像矩阵。为了将图像放入输入层,我们首先需要将我们的图像转换成一个包含图像中所有像素值的大向量,其维度为(1,p)。这个过程称为图像展平。在这个例子中,这个图像的总像素数(n)为 28×28=784。然后,为了将这个图像输入到我们的网络中,我们需要将(28×28)矩阵展平成一个长向量,其维度为(1,784)。输入向量看起来如下:
x = [行1, 行2, 行3, ..., 行 28]
也就是说,在这个例子中,输入层将包含总共 784 个节点:x[1],x[2],...,x[784]。
可视化输入向量
为了帮助可视化扁平化的输入向量,让我们看看一个更小的矩阵(4,4):

输入(x)是一个维度为(1,16)的扁平向量:

因此,如果我们用 0 表示黑色,用 255 表示白色,输入向量将如下所示:
输入 = [0, 255, 255, 255, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0]
这是我们在 Keras 中展平输入图像的方法:
from keras.models import Sequential ❶
from keras.layers import Flatten ❷
model = Sequential() ❸
model.add( Flatten(input_shape = (28,28) )) ❹
❶ 如前所述,导入 Keras 库
❷ 导入一个名为 Flatten 的层,将图像矩阵转换为向量
❸ 定义模型
❹ 添加 Flatten 层,也称为输入层
Keras 中的Flatten层为我们处理这个过程。它将 2D 图像矩阵输入转换为 1D 向量。请注意,Flatten层必须提供一个参数值,即输入图像的形状。现在图像已经准备好被输入到神经网络中。
接下来是什么?隐藏层。
3.1.2 隐藏层
如前一章所述,神经网络可以有一个或多个隐藏层(技术上,可以有任意多个)。每一层有一个或多个神经元(同样,可以有任意多个)。作为神经网络工程师,你的主要任务是设计这些层。为了这个例子,让我们假设你决定任意设计网络,使其有两个隐藏层,每个隐藏层有 512 个节点--并且别忘了为每个隐藏层添加 ReLU 激活函数。
选择激活函数
在第二章中,我们详细讨论了不同类型的激活函数。作为一名深度学习工程师,当你构建你的网络时,你将经常面临许多不同的选择。选择最适合你正在解决的问题的激活函数是这些选择之一。虽然没有一种单一的答案适合所有问题,但在大多数情况下,ReLU 函数在隐藏层中表现最佳;对于大多数类别互斥的分类问题,softmax 函数通常在输出层是一个好的选择。softmax 函数为我们提供了输入图像描述(n)个类别之一的概率。
如前一章所述,让我们添加两个全连接层(也称为密集层),使用 Keras:
from keras.layers import Dense ❶
model.add(Dense(512, activation = 'relu')) ❷
model.add(Dense(512, activation = 'relu')) ❷
❶ 导入 Dense 层
❷ 添加两个每个有 512 个节点的 Dense 层
3.1.3 输出层
输出层相当直接。在分类问题中,输出层的节点数应该等于你试图检测的类别数。在这个问题中,我们正在对 10 个数字(0,1,2,3,4,5,6,7,8,9)进行分类。然后我们需要添加一个包含 10 个节点的最后一个Dense层:
model.add(Dense(10, activation = ‘softmax’))
3.1.4 整合所有内容
当我们将所有这些层组合在一起时,我们得到一个如图 3.3 所示的神经网络。

图 3.3 通过组合输入、隐藏和输出层我们创建的神经网络
下面是它在 Keras 中的样子:
from keras.models import Sequential ❶
from keras.layers import Flatten, Dense ❷
model = Sequential() ❸
model.add( Flatten(input_shape = (28,28) )) ❹
model.add(Dense(512, activation = 'relu')) ❺
model.add(Dense(512, activation = 'relu')) ❺
model.add(Dense(10, activation = 'softmax')) ❻
model.summary() ❼
❶ 导入 Keras 库
❷ 导入 Flatten 层以将图像矩阵转换为向量
❸ 定义神经网络架构
❹ 添加 Flatten 层
❺ 添加两个每个有 512 个节点的隐藏层。建议在隐藏层中使用 ReLU 激活函数。
❻ 添加一个包含 10 个节点的输出 Dense 层。对于多类分类问题,建议在输出层使用 softmax 激活函数。
❼ 打印模型架构摘要
当你运行这段代码时,你会看到如图 3.4 所示的模型摘要。
你可以看到,正如之前讨论的那样,flatten 层的输出是一个包含 784 个节点的向量,因为每个 28 × 28 的图像中都有 784 个像素。按照设计,隐藏层每个都产生 512 个节点;最后,输出层(dense_3)产生一个包含 10 个节点的层。

图 3.4 模型摘要
参数#字段表示每个层产生的参数(权重)的数量。这些是在训练过程中将被调整和学习的权重。它们的计算如下:
-
flatten 层之后的参数数 = 0,因为这个层只是将图像展平成向量以供输入层使用。权重尚未添加。
-
层 1 之后的参数数 = (输入层中的 784 个节点)×(隐藏层 1 中的 512 个节点)+(到偏置的 512 个连接)= 401,920。
-
层 2 之后的参数数 = (隐藏层 1 中的 512 个节点)×(隐藏层 2 中的 512 个节点)+(到偏置的 512 个连接)= 262,656。
-
层 3 之后的参数数=(隐藏层 2 中的 512 个节点)×(输出层中的 10 个节点)+(到偏置的 10 个连接)= 5,130。
-
网络中的总参数数 = 401,920 + 262,656 + 5,130 = 669,706。
这意味着在这个小小的网络中,我们总共有 669,706 个参数(权重和偏置)需要网络学习,并调整其值以优化误差函数。对于这样一个小的网络来说,这是一个巨大的数字。你可以看到,如果我们添加更多的节点和层或使用更大的图像,这个数字会如何失控。这是我们将在下面讨论的 MLPs 的两个主要缺点之一。
MLPs 与 CNNs
如果你将示例 MLP 在 MNIST 数据集上训练,你会得到相当好的结果(与 CNNs 的 99%相比,接近 96%的准确率)。但 MLPs 和 CNNs 通常不会产生可比较的结果。MNIST 数据集是特殊的,因为它非常干净且预处理得很好。例如,所有图像都有相同的大小,并且位于一个 28 × 28 像素网格的中心。此外,MNIST 数据集只包含灰度图像。如果图像有颜色或数字倾斜或未居中,这将是一个更困难的任务。
如果你尝试使用稍微复杂一些的数据集,比如 CIFAR-10,正如我们在本章末尾的项目中将要做的,网络的表现将非常差(大约 30-40%的准确率)。在更复杂的数据集上表现会更差。在混乱的真实世界图像数据中,CNNs 确实优于 MLPs。
3.1.5 MLPs 处理图像的缺点
我们几乎准备好讨论本章的主题:CNNs。但首先,让我们讨论 MLPs 中的两个主要问题,卷积网络旨在解决这些问题。
空间特征损失
将二维图像展平为 1D 向量输入会导致丢失图像的空间特征。正如我们在前面的迷你项目中看到的那样,在将图像输入到 MLP 的隐藏层之前,我们必须将图像矩阵展平为 1D 向量。这意味着丢弃图像中包含的所有 2D 信息。将输入视为没有特殊结构的简单数字向量可能对 1D 信号有效;但在二维图像中,这会导致信息丢失,因为当网络试图寻找模式时,它不会将像素值相互关联。MLP 没有意识到这些像素数字最初是按网格空间排列的,并且它们彼此相连。另一方面,CNN 不需要展平的图像。我们可以将原始像素图像矩阵输入到 CNN 网络中,CNN 将理解彼此靠近的像素比彼此远离的像素关系更紧密。
让我们简化一下,以便更多地了解图像中空间特征的重要性。假设我们正在尝试教一个神经网络识别正方形的形状,假设像素值 1 是白色,0 是黑色。当我们在一个黑色背景上画一个白色正方形时,矩阵将看起来像图 3.5。

图 3.5 如果像素值 1 是白色,0 是黑色,这是我们识别正方形时矩阵的样子。
由于 MLP 以 1D 向量作为输入,我们必须将 2D 图像展平为 1D 向量。图 3.5 的输入向量看起来是这样的:
输入向量 = [1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
当训练完成时,网络将学会仅在输入节点 x1、x2、x5 和 x6 被激活时识别正方形。但是,当我们有如图 3.6 所示的新图像,其中正方形形状位于图像的不同区域时,会发生什么?

图 3.6 图像不同区域的正方形形状
MLP 将无法知道这些是正方形的形状,因为网络没有将正方形形状作为特征学习。相反,它学习了当被激活时可能导致正方形形状的输入节点。如果我们想让我们的网络学习正方形,我们需要在图像的各个位置放置大量的正方形形状。你可以看到这种解决方案对于复杂问题来说不会扩展。
特征学习的另一个例子是:如果我们想教一个神经网络识别猫,那么理想情况下,我们希望网络学习猫的所有形状特征,无论它们出现在图像的哪个位置(耳朵、鼻子、眼睛等)。这只有在网络将图像视为一组像素时才会发生,当这些像素彼此靠近时,它们关系密切。
CNN 的学习机制将在本章中详细解释。但图 3.7 显示了网络如何在其层中学习特征。

图 3.7 CNN 通过其层学习图像特征。
完全连接(密集)层
多层感知器(MLPs)由密集层组成,这些层之间是完全连接的。完全连接意味着一个层中的每个节点都与前一层的所有节点以及下一层的所有节点相连。在这种情况下,每个神经元都有参数(权重)需要从前一层的每个神经元进行训练。虽然这对 MNIST 数据集来说不是大问题,因为图像尺寸真的很小(28 × 28),但当我们尝试处理更大的图像时会发生什么?例如,如果我们有一个 1,000 × 1,000 像素的图像,它将为第一隐藏层中的每个节点产生一百万个参数。所以如果第一隐藏层有 1,000 个神经元,那么即使在如此小的网络中,这也会产生十亿个参数。你可以想象在只有第一层之后优化十亿个参数的计算复杂性。当我们有数十或数百层时,这个数字将急剧增加。这可能会很快失控,并且不会按比例增长。
另一方面,如图 3.8 所示,CNN 是局部连接层:节点仅与前一层的节点的小子集相连。局部连接层使用的参数比密集连接层少得多,正如你将看到的。

图 3.8(左)所有神经元都与图像的所有像素相连的完全连接神经网络。(右)局部连接网络,其中只有一小部分像素与每个神经元相连。这些子集被称为滑动窗口。
所有这些意味着什么?
将二维图像矩阵展平为 1D 向量所造成的信息损失以及完全连接层在处理更大图像时的计算复杂性表明,我们需要一种全新的图像输入处理方式,其中二维信息不会完全丢失。这就是卷积网络发挥作用的地方。CNN 接受完整的图像矩阵作为输入,这显著帮助网络理解像素值中包含的图案。
3.2 CNN 架构
正规神经网络包含多个层,允许每一层依次找到更复杂的特征,这正是卷积神经网络(CNNs)的工作方式。卷积的第一层学习一些基本特征(边缘和线条),下一层学习稍微复杂一些的特征(如圆形、正方形等),接下来的层找到更复杂的特征(如面部的一部分、汽车轮子、狗的胡须等),依此类推。你很快就会看到这个演示。现在,要知道 CNN 架构遵循与神经网络相同的模式:我们在隐藏层中堆叠神经元;权重在网络训练期间随机初始化并学习;我们应用激活函数,计算误差(y − ŷ),并将误差反向传播以更新权重。这个过程是相同的。不同之处在于,我们在特征学习部分使用卷积层而不是常规的完全连接层。
3.2.1 整体概念
在我们详细探讨 CNN 架构之前,让我们先退一步,看看整体情况(图 3.9)。还记得我们在第一章中讨论过的图像分类流程吗?

图 3.9 图像分类流程包括四个组件:数据输入、数据预处理、特征提取和机器学习算法。
在深度学习(DL)之前,我们通常手动从图像中提取特征,然后将得到的特征向量输入到分类器(如 SVM 等常规机器学习算法)中。有了神经网络提供的魔力,我们可以用神经网络(MLP 或 CNN)来替换图 3.9 中步骤 3 的手动工作,该神经网络既能进行特征学习又能进行分类(步骤 3 和 4)。
我们在数字分类项目中看到,如何使用 MLP 来学习特征并对图像进行分类(步骤 3 和 4 合并)。结果证明,我们与全连接层的问题并不在于分类部分——全连接层在这方面做得很好。我们的问题是全连接层处理图像以学习特征的方式。让我们有点创意:我们将保留有效部分,并对无效部分进行修改。全连接层在特征提取(步骤 3)方面做得不好,所以让我们用局部连接层(卷积层)来替换它。另一方面,全连接层在分类提取的特征(步骤 4)方面做得很好,所以让我们保留它们用于分类部分。
CNN 的高级架构看起来像图 3.10:
-
输入层
-
用于特征提取的卷积层
-
用于分类的全连接层
-
输出预测

图 3.10 CNN 架构包括以下部分:输入层、卷积层、全连接层和输出预测。
记住,我们还在讨论整体情况。我们很快就会深入到每个组件。在图 3.10 中,假设我们正在构建一个 CNN 来将图像分类为两个类别:数字 3 和 7。看看图中的内容,并按照以下步骤进行:
-
将原始图像输入到卷积层中。
-
图像通过 CNN 层来检测模式和提取称为特征图的特征。这一步骤的输出随后被展平成一个包含图像学习特征的向量。请注意,图像的维度在每一层之后都会缩小,特征图的数量(层深度)增加,直到我们在特征提取部分的最后一层得到一个长数组的小特征。从概念上讲,你可以将这一步骤视为神经网络学习表示原始图像的更抽象特征。
-
将展平的特征向量输入到全连接层中,以对图像提取的特征进行分类。
-
神经网络激活代表图像正确预测的节点。注意,在这个例子中,我们正在对两个类别(3 和 7)进行分类。因此,输出层将有两个节点:一个代表数字 3,另一个代表数字 7。
定义 神经网络的基本思想是神经元从输入中学习特征。在 CNN 中,特征图是应用于前一层的某个滤波器的输出。它被称为特征图,因为它表示了图像中某种特征的位置。CNN 寻找的特征包括直线、边缘,甚至是物体。每当它们发现这些特征时,它们就会将它们报告给特征图。每个特征图都在寻找特定的事物:一个可能是在寻找直线,另一个可能是在寻找曲线。
3.2.2 深入了解特征提取
你可以将特征提取步骤想象成将大图像分割成包含特征的小块,并将它们堆叠成一个向量。例如,数字 3 的图像是一个图像(深度 = 1),它被分割成包含数字 3 的特定特征的小图像(图 3.11)。如果它被分割成四个特征,那么深度等于 4。随着图像通过 CNN 层,它在维度上缩小,层变得更深,因为它包含了更多的小特征图像。

图 3.11 图像被分割成包含独特特征的小图像。
注意,这只是一个比喻,用来帮助可视化特征提取过程。CNN 并非字面意义上将图像分割成碎片。相反,它们提取有意义的特征,这些特征将这个对象与其他训练集中的图像区分开来,并将它们堆叠在一个特征数组中。
3.2.3 深入了解分类
在特征提取完成后,我们添加全连接层(一个常规的 MLP)来查看特征向量,并说,“第一个特征(顶部)看起来像一条边缘:这可能是一个 3,或者 7,或者可能是一个难看的 2。我不确定;让我们看看第二个特征。嗯,这肯定不是一个 7,因为它有一个曲线,”等等,直到 MLP 确信图像是数字 3。
CNN 学习模式的方式
重要的是要注意,CNN 并不是直接在一层中将图像输入转换为特征向量。这通常发生在数十或数百层中,正如你将在本章后面看到的那样。特征学习过程在每个隐藏层之后逐步发生。因此,第一层通常学习非常基本的特征,如线条和边缘,第二层将这些线条组装成可识别的形状、角落和圆形。然后,在更深的层中,网络学习更复杂的形状,如人手、眼睛、耳朵等。例如,这里是一个 CNN 学习人脸的简化版本。

CNN 学习人脸的简化版本
你可以看到,早期层检测图像中的模式以学习低级特征,如边缘,而后期层检测模式中的模式以学习更复杂的特征,如面部的一部分,然后是模式中的模式中的模式,等等:
输入图像
-
层 1 ⇒ 模式
-
层 2 ⇒ 模式中的模式
-
层 3 ⇒ 模式中的模式中的模式
...等等
当我们在后面的章节中讨论更高级的 CNN 架构时,这个概念将非常有用。现在,你知道在神经网络中,我们堆叠隐藏层来相互学习模式,直到我们有一个包含识别图像的具有意义的特征数组。
3.3 CNN 的基本组件
不再拖延,让我们讨论 CNN 架构的主要组件。你几乎在每一个卷积网络(图 3.12)中都会看到三种主要类型的层:
-
卷积层(CONV)
-
池化层(POOL)
-
全连接层(FC)
CNN 文本表示
图 3.12 中架构的文本表示如下:
CNN 架构:输入 ⇒ CONV ⇒ RELU ⇒ POOL ⇒ CONV ⇒ RELU ⇒ POOL ⇒ FC ⇒ SOFTMAX
注意,ReLU 和 softmax 激活函数并不是真正的独立层——它们是前一层中使用的激活函数。它们在文本表示中这样显示的原因是为了指出 CNN 设计者正在卷积层中使用 ReLU 激活函数,在全连接层中使用 softmax 激活函数。因此,这代表了一个包含两个卷积层和一个全连接层的 CNN 架构。你可以添加你认为合适的任意数量的卷积层和全连接层。卷积层用于特征学习或提取,而全连接层用于分类。

图 3.12 卷积网络的基本组件包括卷积层和池化层用于特征提取,以及全连接层用于分类。
现在我们已经看到了卷积网络的完整架构,让我们深入探讨每种层类型的工作原理。然后在本节的最后,我们将它们全部组合起来。
3.3.1 卷积层
卷积层是卷积神经网络的核心构建块。卷积层就像一个特征查找窗口,逐像素地在图像上滑动以提取识别图像中对象的具有意义的特征。
什么是卷积?
在数学中,卷积是两个函数的操作,以产生第三个修改后的函数。在 CNN 的上下文中,第一个函数是输入图像,第二个函数是卷积滤波器。我们将执行一些数学运算以产生具有新像素值的修改后的图像。
让我们放大查看第一个卷积层,看看它是如何处理图像的(图 3.13)。通过在输入图像上滑动卷积滤波器,网络将图像分解成小块,并单独处理这些块以组装修改后的图像,即特征图。

图 3.13 一个 3 × 3 的卷积滤波器在输入图像上滑动。
记住这个图,以下是一些关于卷积滤波器的事实:
-
中间的 3 × 3 小矩阵是卷积滤波器,也称为核。
-
核在原始图像上逐像素滑动,并进行一些数学计算,以获取下一层“卷积”的新图像的值。滤波器卷积的图像区域称为感受野(见图 3.14)。

图 3.14 核在原始图像上逐像素滑动,并在下一层计算卷积图像。卷积区域称为感受野。
什么是核值?在卷积神经网络(CNNs)中,卷积矩阵是权重。这意味着它们也是随机初始化的,值是由网络学习的(所以你不必担心为其分配值)。
卷积操作
从我们对多层感知器(MLPs)的讨论中,数学应该看起来很熟悉。记得我们是如何将输入乘以权重并将它们全部相加以得到加权总和的吗?
加权总和 = x[1] · w[1] + x[2] · w[2] + x[3] · w[3] + ... + x[n] · w[n] + b
我们在这里做同样的事情,只是在 CNN 中,神经元和权重以矩阵形状结构化。因此,我们将感受野中的每个像素与卷积滤波器中的对应像素相乘,并将它们全部相加,以得到新图像中中心像素的值(图 3.15)。这与我们在第二章中看到的相同的矩阵点积:
(93 × -1) + (139 × 0) + (101 × 1) + (26 × -2) + (252 × 0) + (196 × 2) + (135 × -1) + (240 × 0) + (48 × 1) = 243
滤波器(或核)在整个图像上滑动。每次,我们逐个元素地乘以每个对应的像素,然后将它们全部相加,以创建一个具有新像素值的新图像。这个卷积后的图像称为特征图或激活图。

图 3.15 将感受野中的每个像素与卷积滤波器中的对应像素相乘,并将它们相加,得到新图像中中心像素的值。
应用滤波器以学习特征
让我们不要偏离最初的目标。我们做所有这些是为了让网络从图像中提取特征。应用滤波器是如何达到这个目标的?在图像处理中,滤波器用于过滤掉不需要的信息或放大图像中的特征。这些滤波器是数字矩阵,它们与输入图像卷积以修改它。看看这个边缘检测滤波器:

当这个核(K)与输入图像 F(x,y)进行卷积时,它创建了一个新的卷积图像(特征图),放大了边缘。

在图像上应用边缘检测核
为了理解卷积是如何发生的,让我们放大图像的一小部分。

在输入图像上应用边缘核的计算
这张图像显示了图像某一区域的卷积计算,以计算一个像素的值。我们通过将核在输入图像上逐像素滑动并应用相同的卷积过程来计算所有像素的值。
这些核通常被称为权重,因为它们决定了像素在形成新输出图像中的重要性。类似于我们在关于 MLP 和权重的讨论中提到的,这些权重代表了特征在输出中的重要性。在图像中,输入特征是像素值。
可以应用其他过滤器来检测不同类型的特征。例如,一些过滤器检测水平边缘,其他检测垂直边缘,还有一些检测更复杂的形状,如角,等等。关键是这些过滤器在卷积层中的应用会产生我们之前讨论过的特征学习行为:首先学习简单的特征,如边缘和直线,然后更深的层学习更复杂的特征。
我们基本上完成了滤波器的概念。这就是全部内容!
现在,让我们整体看看卷积层:每个卷积层包含一个或多个卷积滤波器。每个卷积层的滤波器数量决定了下一层的深度,因为每个滤波器都会产生自己的特征图(卷积图像)。让我们看看 Keras 中的卷积层,看看它们是如何工作的:
from keras.layers import Conv2D
model.add(Conv2D(filters=16, kernel_size=2, strides='1', padding='same',
activation='relu'))
就这样。一行代码就创建了一个卷积层。我们将在本章后面看到这条线在完整代码中的位置。让我们专注于卷积层。从代码中可以看出,卷积层有五个主要参数。正如第二章所述,我们建议在神经网络的隐藏层中使用 ReLU 激活函数。这样,一个参数就解决了。现在,让我们解释剩下的四个超参数,它们控制输出体积的大小和深度:
-
过滤器:每层的卷积滤波器数量。这代表了其输出的深度。
-
核大小:卷积滤波器矩阵的大小。大小各异:2 × 2,3 × 3,5 × 5。
-
步长。
-
填充。
我们将在下一节讨论步长和填充。但现在,让我们看看这四个超参数中的每一个。
注意:正如你在第二章关于深度学习的学习中了解到的,超参数是在配置神经网络以改进性能时调整(增加和减少)的旋钮。
卷积层中的滤波器数量
每个卷积层有一个或多个滤波器。为了理解这一点,让我们回顾第二章中的 MLP。记得我们是如何在隐藏层中堆叠神经元的,每个隐藏层有 n 个神经元(也称为隐藏单元)?图 3.16 展示了第二章中的 MLP 图。

图 3.16 神经元在隐藏层中堆叠,每个隐藏层有 n 个神经元(隐藏单元)。
类似地,在 CNN 中,卷积层是隐藏层。为了增加隐藏层中的神经元数量,我们增加卷积层中的核数量。每个核单元被视为一个神经元。例如,如果我们卷积层中有 3 × 3 的核,这意味着在这一层我们有 9 个隐藏单元。当我们添加另一个 3 × 3 核时,我们就有 18 个隐藏单元。再添加一个,我们就有 27 个,以此类推。因此,通过增加卷积层中的核数量,我们增加了隐藏单元的数量,这使得我们的网络更加复杂,能够检测更复杂的模式。当我们向 MLP 的隐藏层添加更多的神经元(隐藏单元)时,情况也是如此。图 3.17 展示了 CNN 层,显示了核数量的概念。

图 3.17 展示 CNN 层核数量概念的表示
核大小
记住,卷积滤波器也称为核。它是一个权重矩阵,在图像上滑动以提取特征。核大小指的是卷积滤波器的维度(宽度乘以高度;图 3.18)。

图 3.18 核大小指的是卷积滤波器的维度。
kernel_size 是你在构建卷积层时将要设置的超参数之一。像大多数神经网络超参数一样,没有单一的最好答案适用于所有问题。直观地说,较小的滤波器会捕捉到图像的非常精细的细节,而较大的滤波器会错过图像中的微小细节。
记住,滤波器包含网络将要学习的权重。因此,从理论上讲,kernel_size 越大,网络越深,这意味着它学得越好。然而,这伴随着更高的计算复杂度,可能会导致过拟合。
内核滤波器几乎总是正方形,大小从最小的 2 × 2 到最大的 5 × 5 不等。从理论上讲,你可以使用更大的滤波器,但这并不推荐,因为它会导致丢失重要的图像细节。
调整
我不希望你被所有的超参数调整所压倒。深度学习实际上是一门艺术,也是一门科学。我无法强调这一点:作为深度学习工程师,你大部分的工作将不是构建实际的算法,而是构建你的网络架构和设置,进行实验,调整你的超参数。今天,大量的研究都集中在尝试为给定类型的问题找到 CNN 的最佳拓扑结构和参数。幸运的是,调整超参数的问题不必像看起来那么困难。在整个书中,我将指出使用超参数的良好起点,并帮助你培养评估你的模型和分析其结果的本能,以了解你需要调整哪个旋钮(超参数)(增加或减少)。
步长和填充
你通常会一起考虑这两个超参数,因为它们都控制卷积层输出的形状。让我们看看如何:
-
步长 -- 滤波器在图像上滑动的量。例如,每次滑动一个像素,步长值是 1。如果我们想每次跳过两个像素,步长值就是 2。步长为 3 或更大的情况在实践中很少见。跳过像素会产生较小的空间输出体积。
步长为 1 将使输出图像大致与输入图像的宽度和高度相同,而步长为 2 将使输出图像大致是输入图像大小的一半。我说“大致”是因为这取决于你如何设置填充参数来处理图像的边缘。
-
填充 -- 通常称为零填充,因为我们会在图像的边缘添加零(如图 3.19 所示)。填充最常用于允许我们保留输入体积的空间大小,以便输入和输出的宽度和高度相同。这样,我们可以在不必要缩小体积的高度和宽度的情况下使用卷积层。这对于构建更深的网络很重要,因为否则,高度/宽度会随着我们进入更深的层而缩小。
![图像]()
图 3.19 零填充在图像的边缘添加零。填充 = 2 在边缘添加两层零。
注意:使用步长和填充超参数的目标是以下两个之一:保留图像的所有重要细节并将它们传递到下一层(当步长值为 1 且填充值为 same 时);或者忽略图像的一些空间信息,以使处理在计算上更经济。请注意,我们将添加池化层(将在下一节讨论)以减小图像的大小,以便关注提取的特征。目前,请了解步长和填充超参数的目的是控制卷积层的行为及其输出的大小:是传递所有图像细节还是忽略其中的一些。
3.3.2 池化层或子采样
添加更多的卷积层会增加输出层的深度,这会导致网络需要优化的参数(学习)数量增加。你可以看到,添加几个卷积层(通常是几十甚至几百)会产生大量的参数(权重)。这种网络维度的增加会增加学习过程中发生的数学运算的时间和空间复杂度。这就是池化层派上用场的时候。子采样或池化通过减少传递给下一层的参数数量来帮助减小网络的大小。池化操作通过应用总结统计函数(如最大值或平均值)来调整其输入的大小,从而减少传递给下一层的参数总数。
池化层的目标是将卷积层产生的特征图下采样到更少的参数数量,从而降低计算复杂度。在 CNN 架构中,在每层卷积层之后或之后每两层卷积层之后添加池化层是一种常见的做法(图 3.20)。

图 3.20 池化层通常在每个卷积层之后或之后每两个卷积层之后添加。
最大池化与平均池化比较
池化层主要有两种类型:最大池化和平均池化。我们首先讨论最大池化。
与卷积核类似,最大池化核是具有一定大小和步长值的窗口,在图像上滑动。与最大池化的不同之处在于,窗口没有权重或任何值。它们所做的只是滑动到由前一个卷积层创建的特征图上,并选择最大像素值传递到下一层,忽略其他值。在图 3.21 中,你可以看到一个大小为 2×2、步长为 2 的池化滤波器(滤波器在滑动图像时跳过 2 个像素)。这个池化层将特征图的大小从 4×4 减少到 2×2。

图 3.21 一个 2×2 的池化滤波器和步长为 2,将特征图从 4×4 减少到 2×2
当我们将此操作应用于卷积层中的所有特征图时,我们得到维度更小的图(宽度乘以高度),但层的深度保持不变,因为我们将对每个来自前一个滤波器的特征图应用池化滤波器。因此,如果卷积层有三个特征图,池化层的输出也将有三个特征图,但尺寸更小(图 3.22)。

图 3.22 如果卷积层有三个特征图,池化层的输出将包含三个更小的特征图。
全局平均池化是一种更极端的降维类型。与设置窗口大小和步长不同,全局平均池化计算特征图中所有像素的平均值(图 3.23)。在图 3.24 中,你可以看到全局平均池化层将一个 3D 数组转换成一个向量。

图 3.23 全局平均池化计算特征图中所有像素的平均值。

图 3.24 全局平均池化层将 3D 数组转换为向量。
为什么使用池化层?
如我们从所讨论的示例中可以看到,池化层降低了卷积层的维度。降低维度之所以重要,是因为在复杂的项目中,CNN 包含许多卷积层,每个都有数十或数百个卷积滤波器(核)。由于核包含网络学习的参数(权重),这可能会迅速失控,我们的卷积层维度可能会变得非常大。因此,添加池化层有助于保持重要特征并将它们传递到下一层,同时缩小图像维度。将池化层想象成图像压缩程序。它们在保持图像重要特征的同时降低图像分辨率(图 3.25)。

图 3.25 池化层降低图像分辨率并保留图像的重要特征。
池化与步长和填充
池化和步长的主要目的是减少神经网络中的参数数量。我们拥有的参数越多,训练过程就越昂贵。许多人不喜欢池化操作,认为我们可以通过调整步长和填充卷积层来避免它。例如,“追求简单:全卷积网络”a 提出丢弃池化层,转而采用仅由重复卷积层组成的架构。为了减少表示的大小,作者建议在卷积层中偶尔使用较大的步长。丢弃池化层也被发现有助于训练良好的生成模型,例如生成对抗网络(GANs),我们将在第十章中讨论。似乎未来的架构将具有非常少的池化层。但到目前为止,池化层仍然被广泛用于将图像从一层下采样到下一层。
a Jost Tobias Springenberg, Alexey Dosovitskiy, Thomas Brox, 和 Martin Riedmiller, “追求简单:全卷积网络”,arxiv.org/abs/1412.6806。
卷积和池化层回顾
让我们回顾一下到目前为止我们所做的工作。到目前为止,我们使用一系列卷积和池化层来处理图像并提取训练数据集中图像的特定有意义特征。为了总结我们是如何到达这里的:
-
原始图像被输入到卷积层,这是一个在图像上滑动以提取特征的核滤波器集合。
-
卷积层具有以下我们需要配置的属性:
from keras.layers import Conv2D model.add(Conv2D(filters=16, kernel_size=2, strides='1', padding='same', activation='relu'))-
filters是每层中核滤波器的数量(隐藏层的深度)。 -
kernel_size是过滤器的尺寸(也称为核)。通常为 2、3 或 5。 -
strides表示过滤器在图像上滑动的量。通常建议从 1 或 2 开始作为良好的起点。 -
padding在图像的边缘添加零值的列和行,以保留下一层的图像大小。 -
在隐藏层中强烈推荐使用
relu的activation。
-
-
池化层具有以下属性,我们需要进行配置:
from keras.layers import MaxPooling2D model.add(MaxPooling2D(pool_size=(2, 2), strides = 2))
我们继续添加成对的卷积和池化层,以达到我们“深度”神经网络所需的深度。
可视化每层之后发生的情况
在卷积层之后,图像保持其宽度和高度维度(通常是),但每经过一层都会变得更深。为什么?还记得我们之前提到的将图像切割成特征块的类比吗?这就是卷积层之后发生的事情。
例如,假设输入图像是 28 × 28(如 MNIST 数据集所示)。当我们添加一个 CONV_1 层(具有 4 个filters、1 个strides和same的padding)时,输出将具有相同的宽度和高度维度,但depth为 4(28 × 28 × 4)。现在我们添加一个具有相同超参数但更多过滤器的 CONV_2 层,我们得到更深的输出:28 × 28 × 12。

在池化层之后,图像保持其深度,但宽度和高度会缩小:

将卷积和池化结合在一起,我们得到如下内容:

这会一直发生,直到最后我们得到一个包含原始图像所有特征的细长图像管。
卷积和池化层的输出产生一个特征管(5 × 5 × 40),几乎可以用于分类。在这里我们使用 40 作为特征管深度的示例,即 40 个特征图。最后一步是在将其输入到全连接层进行分类之前将这个管子展平。如前所述,展平层将具有(1,m)的维度,其中 m = 5 × 5 × 40 = 1,000 个神经元。
3.3.3 全连接层
在通过卷积和池化层对图像进行特征学习过程之后,我们已经提取了所有特征并将它们放入一个长管中。现在是我们使用这些提取的特征来对图像进行分类的时候了。我们将使用第二章中讨论的常规神经网络架构,MLP。
为什么使用全连接层?
MLP 在分类问题中表现良好。我们在这章中使用卷积层的原因是,当从图像中提取特征时,MLP 会丢失大量有价值的信息——我们必须在将图像输入网络之前将其展平——而卷积层可以处理原始图像。现在我们已经提取了特征,并在将它们展平后,我们可以使用常规的 MLP 对它们进行分类。
我们在第二章中详细讨论了 MLP 架构:这里没有新的内容。为了重申,以下是全连接层(图 3.26):
-
输入展平向量——如图 3.26 所示,为了将特征管输入到 MLP 进行分类,我们需要将其展平成一个维度为(1,p)的向量。例如,如果特征管的维度为 5 × 5 × 40,则展平后的向量将是(1,1000)。
-
隐藏层——我们添加一个或多个全连接层,每个层有一个或多个神经元(类似于我们在构建常规 MLPs 时所做的)。
-
输出层——第二章建议对于涉及两个以上类别的分类问题使用 softmax 激活函数。在这个例子中,我们正在对 0 到 9 的数字进行分类:10 个类别。输出层中的神经元数量等于类别的数量;因此,输出层将有 10 个节点。

图 3.26 MLP 的全连接层
MLPs 和全连接层
记住第二章的内容,多层感知器(MLPs)也被称为全连接层,因为一层的所有节点都与前一层和后一层的所有节点相连。它们也被称为密集层。MLP、全连接、密集以及有时正向传播这些术语可以互换使用,以指代常规神经网络架构。

3.4 使用 CNN 进行图像分类
好的,你现在已经完全准备好构建自己的 CNN 模型来对图像进行分类。对于这个小型项目,这是一个简单的问题,但将有助于为后续章节中更复杂的问题打下基础,我们将使用 MNIST 数据集。(MNIST 数据集就像是深度学习的“Hello World”。)
注意:无论你决定使用哪个深度学习库,概念基本上是相同的。你首先在心中或在一张纸上设计 CNN 架构,然后开始堆叠层并设置它们的参数。Keras 和 MXNet(以及 TensorFlow、PyTorch 和其他深度学习库)都有其优缺点,我们将在后面讨论,但概念是相似的。因此,在本书的剩余部分,我们将主要使用 Keras,并在适当的地方简要介绍其他库。
3.4.1 构建模型架构
这是您项目中定义和构建 CNN 模型架构的部分。要查看包含图像预处理、训练和评估模型的完整项目代码,请访问本书的 GitHub 仓库 github.com/moelgendy/deep_learning_for_vision_systems,打开 mnist_cnn 笔记本,或访问本书的网站:www.manning.com/books/deep-learning-for-vision-systems 或 www.computerVisionBook.com。在此阶段,我们关注的是构建模型架构的代码。在本章末尾,我们将构建一个端到端图像分类器,并深入探讨其他部分:
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
model = Sequential() ❶
model.add(Conv2D(32, kernel_size=(3, 3), strides=1, padding='same',
activation='relu', input_shape=(28,28,1))) ❷
model.add(MaxPooling2D(pool_size=(2, 2))) ❸
model.add(Conv2D(64, (3, 3), strides=1, padding='same', activation='relu')) ❹
model.add(MaxPooling2D(pool_size=(2, 2))) ❺
model.add(Flatten()) ❻
model.add(Dense(64, activation='relu')) ❼
model.add(Dense(10, activation='softmax')) ❽
model.summary() ❾
❶ 构建 model 对象
❷ CONV_1: 添加一个具有 ReLU 激活和深度 = 32 个核的卷积层
❸ POOL_1: 对图像进行下采样以选择最佳特征
❹ CONV_2: 增加深度到 64
❺ POOL_2: 更多的下采样
❻ Flatten,因为维度太多;我们只想得到分类输出
❼ FC_1: 完全连接以获取所有相关数据
❽ FC_2: 输出 softmax 以将矩阵压缩为 10 个类别的输出概率
❾ 打印模型架构摘要
当您运行此代码时,您将看到如图 3.27 所示的模型摘要被打印出来。

图 3.27 打印的模型摘要
在我们查看模型摘要之前,有一些一般性的观察:
-
我们只需要将
input_shape参数传递给第一个卷积层。然后我们不需要向模型声明输入形状,因为前一个层的输出是当前层的输入——它已经为模型所知。 -
您可以看到每个卷积层和池化层的输出都是一个形状为 (
None,height,width,channels) 的 3D 张量。height和width的值非常直观:它们是图像在这一层的维度。channels值表示层的深度。这代表每个层中的特征图数量。这个元组中的第一个值设置为None,表示在这一层中处理图像的数量。Keras 将其设置为None,这意味着这个维度是可变的,可以接受任何数量的batch_size。 -
如您在输出形状列中看到的,随着您在网络中深入,图像维度缩小,深度增加,正如我们在本章前面讨论的那样。
-
注意网络需要优化的总参数(权重)数量:220,234,与我们本章早期创建的 MLP 网络的参数数量(669,706)相比。我们将其减少了近三分之一。
让我们逐行查看模型摘要:
-
CONV_1--我们知道输入形状:(28 × 28 × 1)。看看conv2d的输出形状:(28 × 28 × 32)。由于我们将strides参数设置为1,padding设置为same,输入图像的尺寸没有改变。但深度增加到 32。为什么?因为我们在本层添加了 32 个过滤器。每个过滤器产生一个特征图。 -
POOL_1--本层的输入是其前一层的输出:(28 × 28 × 32)。经过池化层后,图像尺寸缩小,但深度保持不变。由于我们使用了 2 × 2 的池化,输出形状为(14 × 14 × 32)。 -
CONV_2--与之前相同,卷积层增加深度并保持尺寸。前一层输入为(14 × 14 × 32)。由于本层的过滤器设置为 64,输出为(14 × 14 × 64)。 -
POOL_2--与之前相同,2 × 2 池化保持深度并缩小尺寸。输出为(7 × 7 × 64)。 -
Flatten--将具有(7 × 7 × 64)维度的特征管扁平化,将其转换为(1, 3136)维度的平坦向量。 -
Dense_1--我们将这个全连接层设置为 64 个神经元,因此输出为 64。 -
Dense_2--这是输出层,我们将其设置为 10 个神经元,因为我们有 10 个类别。
3.4.2 参数数量(权重)
好的,现在我们知道如何构建模型,并逐行阅读摘要以查看图像形状如何随着通过网络层而变化。还有一个重要的事情需要注意:模型摘要中右侧的Param #列。
参数是什么?
参数只是权重的一个名称。这些是网络学习的东西。正如我们在第二章中讨论的,网络的目的是在梯度下降和反向传播过程中更新权重值,直到找到最小化误差函数的最优参数值。
这些参数是如何计算的?
在 MLP 中,我们知道层之间是完全连接的,因此权重连接或边简单地通过乘以每层的神经元数量来计算。在 CNN 中,权重计算并不那么直接。幸运的是,有一个方程可以用来计算:
参数数量 = 过滤器数量 × 核大小 × 前一层深度 + 过滤器数量(对于偏差)
让我们用一个例子来应用这个方程。假设我们想要计算之前的小项目第二层的参数。这是CONV_2的代码再次:
model.add(Conv2D(64, (3, 3), strides=1, padding='same', activation='relu'))
由于我们知道前一层深度为 32,那么
⇒ 参数 = 64 × 3 × 3 × 32 + 64 = 18,496
注意,池化层不添加任何参数。因此,在模型摘要中,您将看到池化层后的Param #值为 0。对于扁平化层也是如此:没有添加额外的权重(见图 3.28)。

图 3.28 池化和扁平化层不添加参数,因此在模型摘要中池化和扁平化层后的Param #值为 0。
当我们将参数编号列中的所有参数相加时,我们得到这个网络需要优化的参数总数:220,234。
可训练和不可训练参数
在模型摘要中,您将看到参数总数,以及其下方可训练和不可训练参数的数量。可训练参数是神经网络在训练过程中需要优化的权重。在本例中,我们所有的参数都是可训练的(图 3.29)。

图 3.29 我们所有的参数都是可训练的,需要在训练过程中进行优化。
在后面的章节中,我们将讨论使用预训练网络并将其与您自己的网络结合以获得更快、更准确的结果:在这种情况下,您可能决定冻结一些层,因为它们是预训练的。因此,并非所有网络参数都将进行训练。这在开始训练过程之前了解您模型的内存和空间复杂度很有用;但更多内容将在后面讨论。据我们所知,我们所有的参数都是可训练的。
3.5 添加 dropout 层以避免过拟合
到目前为止,您已经介绍了 CNN 的三个主要层:卷积、池化和全连接。您几乎可以在每个 CNN 架构中找到这三种层类型。但这还不是全部——还有额外的层可以添加以避免过拟合。
3.5.1 什么是过拟合?
机器学习性能不佳的主要原因要么是过拟合,要么是欠拟合数据。欠拟合正如其名:模型无法拟合训练数据。这种情况发生在模型过于简单,无法拟合数据:例如,使用一个感知器对一个非线性数据集进行分类。
相反,过拟合意味着过度拟合数据:记住训练数据而没有真正学习特征。这种情况发生在我们构建一个超级网络,可以完美地拟合训练数据集(训练时的错误率非常低),但无法推广到它之前未见过的新数据样本。您将看到,在过拟合的情况下,网络在训练数据集上表现良好,但在测试数据集上表现不佳(图 3.30)。

图 3.30 欠拟合(左):模型无法很好地表示数据。恰到好处(中):模型很好地拟合了数据。过拟合(右):模型过度拟合数据,因此它无法推广到未见过的新例子。
在机器学习中,我们不希望构建过于简单而欠拟合数据或过于复杂而过拟合数据的模型。我们希望使用其他技术构建一个适合我们问题的神经网络。为了解决这个问题,我们将在下一节讨论 dropout 层。
3.5.2 什么是 dropout 层?
Dropout 层是防止过拟合最常用的层之一。Dropout 关闭了构成您网络一层的一定比例的神经元(节点)(如图 3.31 所示)。这个比例被识别为一个超参数,当您构建网络时对其进行调整。通过“关闭”,我的意思是这些神经元不包括在特定的正向或反向传播中。在网络中丢弃连接可能看起来有些反直觉,但随着网络的训练,一些节点可能会支配其他节点或最终犯下大错误。Dropout 为您提供了一种平衡网络的方法,使每个节点都能平等地朝着同一个目标努力,如果其中一个节点犯了错误,它不会支配您模型的行为。您可以将 dropout 视为一种使网络具有弹性的技术;通过确保没有节点太弱或太强,它使所有节点作为一个团队良好地工作。

图 3.31 Dropout 关闭了构成网络层的一定比例的神经元。
3.5.3 为什么我们需要 dropout 层?
在训练过程中,神经元之间会形成相互依赖,这控制了每个神经元的个体能力,导致训练数据的过拟合。为了真正理解为什么 dropout 是有效的,让我们更仔细地看看图 3.31 中的 MLP,并思考每一层的节点真正代表什么。第一层(最左边)是输入层,包含输入特征。第二层包含从上一层的模式中学习到的特征,当乘以权重时。然后是下一层,它学习的是模式中的模式,依此类推。每个神经元代表一个特定的特征,当乘以权重时,会转换成另一个特征。当我们随机关闭一些这些节点时,我们迫使其他节点在没有仅依赖一个或两个特征的情况下学习模式,因为任何特征在任何时候都可能被随机丢弃。这导致权重在所有特征之间分散,导致更多训练神经元。
Dropout 有助于减少神经元之间的相互依赖学习。从这个意义上讲,它有助于将 dropout 视为一种集成学习方法。在集成学习中,我们分别训练多个较弱的分类器,然后在测试时通过平均所有集成成员的响应来使用它们。由于每个分类器都是分别训练的,因此它们已经学习了数据的不同方面,它们的错误(误差)也不同。将它们结合起来有助于产生一个更强的分类器,这种分类器不太可能过拟合。
直觉
一个有助于我理解 dropout 的类比是使用杠铃训练二头肌。当我们用双臂举起杠铃时,我们倾向于依赖我们的更强臂举起比我们的弱臂更多的重量。我们的更强臂最终会得到比其他部位更多的训练并发展出更大的肌肉:

Dropout 意味着稍微打乱我们的训练(训练)过程。我们绑住我们的右手,只训练左手。然后我们绑住左手,只训练右手。然后我们打乱它,带着两只手臂回到杠铃,以此类推。过了一段时间,你会发现你的两个二头肌都得到了锻炼:

这正是我们在训练神经网络时发生的情况。有时网络的一部分具有非常大的权重并主导所有训练,而网络的另一部分则没有得到很多训练。dropout 的作用是关闭一些神经元,让其余的神经元进行训练。然后,在下一个 epoch 中,它关闭其他神经元,这个过程持续进行。
3.5.4 dropout 层在 CNN 架构中的位置在哪里?
正如您在本章中学到的,一个标准的 CNN 由交替的卷积层和池化层组成,以全连接层结束。为了防止过拟合,在将图像展平后,在架构末尾的全连接层之间注入几个 dropout 层已成为标准做法。为什么?因为 dropout 在卷积神经网络的完全连接层中已知效果良好。然而,它在卷积和池化层中的效果尚未得到充分研究:
CNN 架构:... 卷积 ⇒ 池化 ⇒ 展平 ⇒ DO ⇒ FC ⇒ DO ⇒ FC
让我们看看我们如何使用 Keras 将 dropout 层添加到我们之前的模型中:
# CNN and POOL layers
# ...
# ...
model.add(Flatten()) ❶
model.add(Dropout(rate=0.3)) ❷
model.add(Dense(64, activation='relu')) ❸
model.add(Dropout(rate=0.5)) ❹
model.add(Dense(10, activation='softmax')) ❺
model.summary() ❻
❶ 展平层
❷ 30%概率的 dropout 层
❸ FC_1:完全连接以获取所有相关数据
❹ 50%概率的 dropout 层
❺ FC_2:输出 softmax 将矩阵压缩成 10 个类别的输出概率
❻ 打印模型架构摘要
如您所见,dropout 层以rate作为参数。该比率表示要丢弃的输入单元的比例。例如,如果我们把rate设置为 0.3,这意味着该层中 30%的神经元将在每个 epoch 中被随机丢弃。所以如果我们有一个层中有 10 个节点,那么其中的 3 个神经元将被关闭,而 7 个将被训练。这三个神经元是随机选择的,在下一个 epoch 中,其他随机选择的神经元将被关闭,以此类推。由于我们是随机进行的,一些神经元可能被关闭的次数比其他的多,有些可能永远不会被关闭。这是可以的,因为我们这样做很多次,所以平均来看,每个神经元将得到几乎相同的治疗。请注意,这个比率是我们构建 CNN 时调整的另一个超参数。
3.6 对彩色图像的卷积(3D 图像)
记得在第一章中,计算机将灰度图像视为像素的二维矩阵(图 3.32)。对于计算机来说,图像看起来像是一个像素值的二维矩阵,这些值代表颜色光谱中的强度。这里没有上下文,只有大量数据。

图 3.32 对于计算机来说,图像看起来像是一个像素值的二维矩阵。
另一方面,计算机将彩色图像解释为具有高度、宽度和深度的 3D 矩阵。在 RGB 图像(红色、绿色和蓝色)的情况下,深度为 3:每个颜色一个通道。例如,一个 28 × 28 的彩色图像将被计算机视为一个 28 × 28 × 3 的矩阵。想象一下,这是一个由三个 2D 矩阵堆叠而成的——每个矩阵分别对应图像的红色、绿色和蓝色通道。每个矩阵代表其颜色的强度值。当它们堆叠在一起时,就构成了一个完整的彩色图像(图 3.33)。

图 3.33 彩色图像由三个矩阵表示。每个矩阵代表其颜色的强度值。将它们堆叠起来就构成了一个完整的彩色图像。
注意:为了泛化,我们用三维数组表示图像:高度 × 宽度 × 深度。对于灰度图像,深度为 1;对于彩色图像,深度为 3。
3.6.1 我们如何在彩色图像上执行卷积?
与我们对灰度图像所做的方法类似,我们将卷积核在图像上滑动并计算特征图。现在核本身是三维的:每个维度对应一个颜色通道(图 3.34)。

图 3.34 我们将卷积核在图像上滑动并计算特征图,从而得到一个 3D 核。
为了执行卷积,我们将做与之前相同的事情,只是现在我们的求和项是之前的 3 倍。让我们看看如何(图 3.35):
-
每个颜色通道都有自己的对应过滤器。
-
每个过滤器都会在其图像上滑动,逐元素相乘对应的像素元素,然后将它们全部相加以计算每个过滤器的卷积像素值。这与我们之前所做的方法类似。
-
然后我们将这三个值相加得到卷积图像或特征图中单个节点的值。别忘了加上 1 的偏置值。然后我们将过滤器滑动一个或多个像素(基于步长值)并执行相同操作。我们继续这个过程,直到计算完特征图中所有节点的像素值。

图 3.35 执行卷积
3.6.2 计算复杂度会发生什么变化?
注意,如果我们用一个 3 × 3 的过滤器在灰度图像上滑动,我们将为每个过滤器有总共 9 个参数(权重)(如前所述)。在彩色图像中,每个过滤器本身就是一个 3D 过滤器。这意味着每个过滤器都有一个参数数量:(高度 × 宽度 × 深度)=(3 × 3 × 3)= 27。你可以看到,当处理彩色图像时,网络复杂性如何增加,因为它必须优化更多的参数;彩色图像也占用更多的内存空间。
彩色图像包含比灰度图像更多的信息。这可能会增加不必要的计算复杂性和占用内存空间。然而,彩色图像对于某些分类任务也非常有用。这就是为什么在某些用例中,作为计算机视觉工程师的你将根据自己的判断来决定是否将彩色图像转换为灰度图像,因为在很多情况下,颜色并不是识别和解释图像所必需的:灰度图像可能就足够识别物体了。
在图 3.36 中,你可以看到一个物体(强度)中明暗模式的如何被用来定义其形状和特征。然而,在其他应用中,颜色对于定义某些物体很重要:例如,皮肤癌检测很大程度上依赖于皮肤颜色(红色皮疹)。一般来说,当涉及到 CV 应用,如识别汽车、人或皮肤癌时,你可以通过思考自己的视觉来决定颜色信息是否重要。如果我们人类在颜色上更容易识别问题,那么算法看到彩色图像可能也更容易。

图 3.36 在灰度图像中,一个物体(强度)的明暗模式可以用来定义其形状和特征。
注意,在图 3.36 中,我们只添加了一个过滤器(包含 3 个通道),它产生了一个特征图。与灰度图像类似,我们添加的每个过滤器都会产生它自己的特征图。在图 3.37 中的 CNN 中,我们有一个尺寸为(7 × 7 × 3)的输入图像。我们添加了两个尺寸为(3 × 3)的卷积过滤器。输出特征图的深度为 2,因为我们添加了两个过滤器,这与我们在灰度图像中所做的一样。
关于 CNN 架构的重要注意事项
我强烈建议查看现有的架构,因为很多人已经做了将事物组合在一起并看看什么有效的工作。从实际的角度来说,除非你正在研究问题,否则你应该从一个已经由其他人构建的用于解决类似你问题的 CNN 架构开始。然后进一步调整以适应你的数据。
在第四章中,我们将解释如何诊断你网络的性能,并讨论调整策略以改进它。在第五章中,我们将讨论最流行的 CNN 架构,并检查其他研究人员是如何构建它们的。我希望你从这个部分得到的是,首先,对 CNN 构建的概念理解;其次,更多的层导致更多的神经元,这导致更多的学习行为。但这伴随着计算成本。因此,你应该始终考虑你的训练数据的大小和复杂性(对于简单任务,可能不需要很多层)。

图 3.37 我们的输入图像尺寸为(7 × 7 × 3),我们添加了两个尺寸为(3 × 3)的卷积过滤器。输出特征图的深度为 2。
3.7 项目:彩色图像的分类
让我们看看一个端到端图像分类项目。在这个项目中,我们将训练一个 CNN 来对 CIFAR-10 数据集(www.cs.toronto.edu/ ~kriz/cifar.html)中的图像进行分类。CIFAR-10 是一个用于物体识别的成熟 CV 数据集,它是 8000 万小图像数据集的一个子集 1,包含 60000 张(32 × 32)彩色图像,每类有 6000 张图像。现在,启动你的笔记本,让我们开始吧。
第 1 步:加载数据集
第一步是将数据集加载到我们的训练和测试对象中。幸运的是,Keras 为我们提供了load_data()方法来加载 CIFAR 数据集。我们只需要导入keras.datasets然后加载数据:
import keras
from keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data() ❶
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure(figsize=(20,5))
for i in range(36):
ax = fig.add_subplot(3, 12, i + 1, xticks=[], yticks=[])
ax.imshow(np.squeeze(x_train[i]))
❶ 加载预洗牌的训练和测试数据
第 2 步:图像预处理
根据你的数据集和你要解决的问题,你需要进行一些数据清理和预处理,以便为你的学习模型做好准备。成本函数的形状像一个碗,但如果特征具有非常不同的尺度,它可能是一个拉长的碗。图 3.38 显示了在特征 1 和 2 具有相同尺度(左侧)的训练集上的梯度下降,以及在特征 1 的值比特征 2 小得多的训练集(右侧)。
提示:在使用梯度下降时,你应该确保所有特征具有相似的尺度;否则,收敛将需要更长的时间。

图 3.38 显示了标准化特征具有相同尺度,用一个均匀的碗表示(左侧)。未标准化特征尺度不同,用一个拉长的碗表示(右侧)。在具有相同尺度的特征上的训练集(左侧)和在特征 1 的值比特征 2 小得多的训练集(右侧)上的梯度下降。
调整图像尺度
按以下方式调整输入图像的尺度:
x_train = x_train.astype('float32')/255 ❶
x_test = x_test.astype('float32')/255
❶ 通过除以 255 来调整图像的像素值:[0,255] ⇒ [0,1]
准备标签(独热编码)
在本章以及整本书中,我们将讨论计算机如何通过将其转换为像素强度的矩阵形式来处理输入数据(图像)。那么标签呢?计算机是如何理解标签的?我们数据集中的每一张图像都有一个特定的标签,用文本解释了这张图像是如何被分类的。在这个特定的数据集中,例如,标签被分为以下 10 个类别:['飞机', '汽车', '鸟', '猫', '鹿', '狗', '青蛙', '马', '船', '卡车']。我们需要将这些文本标签转换为计算机可以处理的形式。计算机擅长处理数字,所以我们将进行一种称为独热编码的过程。独热编码是一种将分类变量转换为数值形式的过程。
假设数据集看起来如下:
| 图像 | 标签 |
|---|---|
| image_1 | 狗 |
| image_2 | 汽车 |
| image_3 | 飞机 |
| image_4 | truck |
| image_5 | bird |
独热编码后,我们得到以下内容:
| airplane | bird | cat | deer | dog | frog | horse | ship | truck | automobile | |
|---|---|---|---|---|---|---|---|---|---|---|
| image_1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| image_2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| image_3 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| image_4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| image_5 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
幸运的是,Keras 有一个方法可以为我们做到这一点:
from keras.utils import np_utils
num_classes = len(np.unique(y_train)) ❶
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
❶ 对标签进行独热编码
将数据集分为训练集和验证集
除了将我们的数据分为训练集和测试集之外,将训练数据进一步分为训练集和验证集是一种标准做法(图 3.39)。为什么?因为每个拆分都有不同的用途:
-
训练数据集 -- 用于训练模型的样本数据。
-
验证数据集 -- 使用的数据样本,用于在调整模型超参数时对训练数据集上的模型拟合进行无偏评估。当将验证数据集上的技能纳入模型配置时,评估变得更具偏差。
-
测试数据集 -- 使用的数据样本,用于在训练数据集上对最终模型拟合进行无偏评估。

图 3.39 将数据拆分为训练、验证和测试子集
这里是 Keras 代码:
(x_train, x_valid) = x_train[5000:], x_train[:5000] ❶
(y_train, y_valid) = y_train[5000:], y_train[:5000] ❶
print('x_train shape:', x_train.shape) ❷
print(x_train.shape[0], 'train samples') ❸
print(x_test.shape[0], 'test samples') ❸
print(x_valid.shape[0], 'validation samples') ❸
❶ 将训练集拆分为训练集和验证集
❷ 打印训练集的形状
❸ 打印训练集、验证集和测试集的图像数量
标签矩阵
独热编码将 (1 × n) 标签向量转换为维度为 (10 × n) 的标签矩阵,其中 n 是样本图像的数量。所以,如果我们数据集中有 1,000 张图像,标签向量将具有 (1 × 1000) 的维度。独热编码后,标签矩阵的维度将是 (1000 × 10)。这就是为什么,在下一步定义我们的网络架构时,我们将输出 softmax 层包含 10 个节点,每个节点代表我们拥有的每个类别的概率。

第 3 步:定义模型架构
你已经了解到 CNN(以及神经网络)的核心构建块是层。大多数深度学习项目都由堆叠简单层组成,这些层实现了数据蒸馏的一种形式。正如你在本章所学,主要的 CNN 层包括卷积、池化、全连接和激活函数。
你是如何决定网络架构的?
应该创建多少个卷积层?多少个池化层?在我看来,了解一些最流行的架构(AlexNet、ResNet、Inception)以及提取导致设计决策的关键思想是非常有帮助的。观察这些最先进的架构是如何构建的,并在你自己的项目中尝试,将帮助你建立对最适合你解决问题的 CNN 架构的直觉。我们将在第五章讨论最流行的 CNN 架构。在此之前,你需要了解以下内容:
-
你添加的层越多,理论上你的网络学习效果越好;但这也将带来计算和内存空间复杂度增加的代价,因为它增加了需要优化的参数数量。你还将面临网络过拟合训练集的风险。
-
当输入图像通过网络层时,其深度会增加,而维度(宽度,高度)会逐层缩小。
-
通常情况下,对于较小的数据集,可以从两到三层 3 × 3 卷积层开始,然后跟一个 2 × 2 池化层,这可以是一个不错的起点。添加更多的卷积和池化层,直到你的图像达到合理的尺寸(比如 4 × 4 或 5 × 5),然后添加几个全连接层进行分类。
-
你需要设置几个超参数(如
filter、kernel_size和padding)。记住,你不需要重新发明轮子:相反,查阅文献看看通常对其他人有效的超参数。以对其他人有效的架构作为起点,然后调整这些超参数以适应你的情况。下一章将专门探讨对其他人有效的方法。
学习如何与层和超参数一起工作
我不希望你在一开始构建 CNN 时过于纠结于超参数的设置。获得如何组合层和超参数直觉的最好方法之一是实际看到其他人如何具体操作。作为深度学习工程师,你大部分的工作将涉及构建架构和调整参数。本章的主要收获如下:
-
理解主要 CNN 层(卷积、池化、全连接、dropout)的工作原理以及它们存在的原因。
-
理解每个超参数的作用(卷积层中的滤波器数量、内核大小、步长和填充)。
-
最后,理解如何在 Keras 中实现任何给定的架构。如果你能够在你自己的数据集上复制这个项目,那么你就准备好了。
在第五章中,我们将回顾几个最先进的架构,并看看它们是如何工作的。
图 3.40 所示的架构被称为 AlexNet:它是一个流行的 CNN 架构,在 2011 年赢得了 ImageNet 挑战赛(关于 AlexNet 的更多细节请见第五章)。AlexNet CNN 架构由五个卷积和池化层以及三个全连接层组成。

图 3.40 AlexNet 架构
让我们尝试一个更小的 AlexNet 版本,看看它在我们的数据集上的表现如何(图 3.41)。根据结果,我们可能会添加更多层。我们的架构将堆叠三个卷积层和两个全连接(密集)层,如下所示:
CNN:输入 ⇒ CONV_1 ⇒ POOL_1 ⇒ CONV_2 ⇒ POOL_2 ⇒ CONV_3 ⇒ POOL_3 ⇒ DO ⇒ FC ⇒ DO ⇒ FC (softmax)

图 3.41 我们将构建一个包含三个卷积层和两个密集层的简单 CNN。
注意,我们将对所有隐藏层使用 ReLU 激活函数。在最后一个密集层,我们将使用具有 10 个节点的 softmax 激活函数,以返回一个包含 10 个概率分数的数组(总和为 1)。每个分数将是当前图像属于我们 10 个图像类别的概率:
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
model = Sequential()
model.add(Conv2D(filters=16, kernel_size=2, padding='same', ❶
activation='relu', input_shape=(32, 32, 3))) ❶
model.add(MaxPooling2D(pool_size=2))
model.add(Conv2D(filters=32, kernel_size=2, padding='same', ❷
activation='relu')) ❷
model.add(MaxPooling2D(pool_size=2))
model.add(Conv2D(filters=64, kernel_size=2, padding='same', ❸
activation='relu')) ❸
model.add(MaxPooling2D(pool_size=2))
model.add(Dropout(0.3)) ❹
model.add(Flatten()) ❺
model.add(Dense(500, activation='relu')) ❻
model.add(Dropout(0.4)) ❼
model.add(Dense(10, activation='softmax')) ❽
model.summary() ❾
❶ 第一个卷积和池化层。注意,我们只需要在第一个卷积层中定义 input_shape。
❷ 第二个卷积和池化层,带有 ReLU 激活函数
❸ 第三个卷积和池化层
❹ 30%率的 dropout 层以避免过拟合
❺ 将最后一个特征图展平成一个特征向量
❻ 添加第一个全连接层
❼ 另一个 40%率的 dropout 层
❽ 输出层是一个包含 10 个节点的全连接层,并使用 softmax 激活函数为 10 个类别提供概率。
❾ 打印模型架构摘要
当你运行这个单元格时,你会看到模型架构以及特征图维度如何随着每一层的连续变化,如图 3.42 所示。

图 3.42 模型摘要
我们之前讨论了如何理解这个摘要。正如你所见,我们的模型有 528,054 个参数(权重和偏差)需要训练。我们之前也讨论了如何计算这个数字。
第 4 步:编译模型
在训练我们的模型之前,最后一步是定义三个额外的超参数--损失函数、优化器以及在训练和测试期间监控的指标:
-
损失函数 --网络将如何衡量其在训练数据上的性能。
-
优化器 --网络将使用的机制来优化其参数(权重和偏差)以产生最小损失值。它通常是第二章中解释的随机梯度下降的变体之一。
-
指标 --模型在训练和测试期间要评估的指标列表。通常我们使用
metrics=['accuracy']。
随意回顾第二章以获取关于确切目的和不同类型的损失函数及优化器的更多细节。
这是编译模型的代码:
model.compile(loss='categorical_crossentropy', optimizer='rmsprop',
metrics=['accuracy'])
第 5 步:训练模型
我们现在准备好训练网络了。在 Keras 中,这是通过调用网络的.fit()方法(如将模型拟合到训练数据)来完成的:
from keras.callbacks import ModelCheckpoint
checkpointer = ModelCheckpoint(filepath='model.weights.best.hdf5', verbose=1,
save_best_only=True)
hist = model.fit(x_train, y_train, batch_size=32, epochs=100,
validation_data=(x_valid, y_valid), callbacks=[checkpointer],
verbose=2, shuffle=True)
当你运行这个单元格时,训练将开始,图 3.43 中显示的详细输出将每次显示一个时代。由于 100 个时代的显示不适合一页纸,截图显示了前 13 个时代。但当你在这个笔记本上运行时,显示将一直持续到 100 个时代。

图 3.43 训练的前 13 个时代
查看图 3.43 中的详细输出将帮助你分析你的网络性能,并提出哪些旋钮(超参数)需要调整。我们将在第四章中详细讨论这一点。现在,让我们看看最重要的要点:
-
loss和acc是训练数据的错误和准确率值。val_loss和val_acc是验证数据的错误和准确率值。 -
查看每个时代后的
val_loss和val_acc值。理想情况下,我们希望val_loss下降,val_acc上升,这表明网络在每个时代之后实际上是在学习的。 -
从第 1 个时代到第 6 个时代,你可以看到模型在每个时代之后都会保存权重,因为验证损失值在提高。所以每个时代结束时,我们保存被认为是迄今为止最佳权重的权重。
-
在第 7 个时代,
val_loss从 0.9445 上升到 1.1300,这意味着它没有改进。所以网络在这个时代没有保存权重。如果你现在停止训练并从第 6 个时代加载权重,你将得到训练期间达到的最佳结果。 -
对于第 8 个时代,
val_loss下降,因此网络将权重保存为最佳值。在第 9 个时代,没有改进,以此类推。 -
如果你训练 12 个时代后停止并加载最佳权重,网络将加载在第 10 个时代保存的权重(
val_loss= 0.9157)和(val_acc= 0.6936)。这意味着你可以期望在测试数据上获得接近 69%的准确率。
关注这些常见现象
-
val_loss在波动。如果val_loss上下波动,你可能想要降低学习率超参数。例如,如果你看到val_loss从 0.8 到 0.9,到 0.7,到 1.0,以此类推,这可能意味着你的学习率太高,无法下降错误山。尝试降低学习率,并让网络训练更长的时间。![]()
如果
val_loss波动,学习率可能太高。 -
val_loss没有改进(欠拟合)。如果val_loss没有下降,这可能意味着你的模型太简单,无法拟合数据(欠拟合)。那么你可能需要通过添加更多隐藏层来构建一个更复杂的模型,以帮助网络拟合数据。 -
loss正在下降,而val_loss停止了提升。这意味着你的网络开始对训练数据进行过拟合,并且未能减少验证数据的错误。在这种情况下,考虑使用防止过拟合的技术,如丢弃层。我们将在下一章讨论其他避免过拟合的技术。
第 6 步:加载具有最佳 val_acc 的模型
现在训练完成,我们使用 Keras 方法load_weights()将产生最佳验证准确率分数的权重加载到我们的模型中:
model.load_weights('model.weights.best.hdf5')
第 7 步:评估模型
最后一步是评估我们的模型,并计算准确率值作为百分比,表示我们的模型正确预测图像分类的频率:
score = model.evaluate(x_test, y_test, verbose=0)
print('\n', 'Test accuracy:', score[1])
当你运行这个单元格时,你将得到大约 70%的准确率。这还不错。但我们可以做得更好。尝试通过添加更多的卷积和池化层来调整 CNN 架构,看看你是否能提高你的模型。
在下一章中,我们将讨论设置深度学习(DL)项目策略和超参数调整以改进模型性能的方法。在第四章结束时,我们将重新审视这个项目,应用这些策略并将准确率提高到 90%以上。
摘要
-
MLPs、ANNs、密集和前馈都指的是我们在第二章中讨论的常规全连接神经网络架构。
-
MLPs 通常适用于一维输入,但它们在处理图像时表现不佳,主要有两个原因。首先,它们只接受以向量形式具有维度(1 × n)的特征输入。这需要展平图像,这将导致丢失其空间信息。其次,MLPs 由全连接层组成,在处理较大图像时会产生数百万甚至数十亿个参数。这将增加计算复杂度,并且对于许多图像问题无法扩展。
-
CNNs 在图像处理方面表现尤为出色,因为它们可以直接将原始图像矩阵作为输入,而不需要将图像展平。它们由称为卷积滤波器的局部连接层组成,这与 MLPs 的密集层形成对比。
-
卷积神经网络(CNNs)由三个主要层组成:用于特征提取的卷积层、用于降低网络维度的池化层,以及用于分类的全连接层。
-
机器学习预测性能不佳的主要原因要么是过拟合,要么是欠拟合数据。欠拟合意味着模型过于简单,无法拟合(学习)训练数据。过拟合意味着模型过于复杂,它记住了训练数据,并且无法对之前未见过的测试数据进行泛化。
-
添加一个丢弃层以防止过拟合。丢弃层会关闭网络层中一定比例的神经元(节点)。
- 安东尼奥·托拉尔巴、罗布·弗格森和威廉·T·弗里曼,“8000 万个小图像:用于非参数对象和场景识别的大数据集”,IEEE 信号处理与机器智能杂志(2008 年 11 月),
doi.org/10.1109/TPAMI.2008.128.
4 结构化 DL 项目和超参数调整
本章涵盖
-
定义性能指标
-
设计基线模型
-
准备训练数据
-
评估模型并提高其性能
本章总结了本书的第一部分,为深度学习(DL)提供了基础。在第二章中,你学习了如何构建多层感知器(MLP)。在第三章中,你了解了在计算机视觉(CV)问题中非常常用的神经网络架构拓扑:卷积神经网络(CNN)。在本章中,我们将通过讨论如何从头到尾构建你的机器学习(ML)项目来结束这个基础。你将学习策略,以快速有效地让你的 DL 系统运行,分析结果,并提高网络性能。
正如你可能已经从前面的项目中注意到的,深度学习(DL)是一个非常经验的过程。它依赖于运行实验和观察模型性能,而不是有一个适用于所有问题的成功公式。我们通常有一个初始的解决方案想法,将其编码,运行实验以查看其表现,然后使用这个实验的结果来完善我们的想法。在构建和调整神经网络时,你会发现自己在做出许多看似随机的决策:
-
从何开始选择一个好的架构?
-
你应该堆叠多少隐藏层?
-
每个层应该有多少隐藏单元或过滤器?
-
学习率是多少?
-
你应该使用哪种激活函数?
-
哪个更有利于获得更好的结果,是获取更多数据还是调整超参数?
在本章中,你将学习以下内容:
-
定义您系统的性能指标 -- 除了模型准确度之外,你还将使用其他指标,如精确度、召回率和 F 分数来评估你的网络。
-
设计基线模型 -- 你将选择一个合适的神经网络架构来运行你的第一个实验。
-
准备训练数据 -- 在现实世界的问题中,数据通常是杂乱的,不适合直接输入到神经网络中。在本节中,你将对数据进行处理,使其准备好进行学习。
-
评估你的模型并解释其性能 -- 训练完成后,你分析模型性能以识别瓶颈并缩小改进选项。这意味着诊断哪些网络组件的表现不如预期,并确定性能不佳是由于过拟合、欠拟合还是数据缺陷。
-
改进网络和调整超参数 -- 最后,我们将深入探讨最重要的超参数,以帮助你发展对需要调整哪些超参数的直觉。你将使用调整策略,根据上一步的诊断进行增量更改。
TIP 通过更多的实践和实验,深度学习工程师和研究人员会随着时间的推移逐渐建立起对最有效改进方法的直觉。我的建议是亲自动手尝试不同的架构和方法,以发展你的超参数调整技能。
准备好了吗?让我们开始吧!
4.1 定义性能指标
性能指标允许我们评估我们的系统。当我们开发模型时,我们想知道它工作得如何。衡量我们模型“好坏”的最简单方法就是衡量其准确率。准确率指标衡量我们的模型正确预测的次数。因此,如果我们用 100 个输入样本测试模型,并且它正确预测了 90 次,这意味着该模型是 90% 准确的。
这里是计算模型准确率的公式:

4.1.1 准确率是评估模型的最佳指标吗?
我们在早期项目中一直使用准确率作为评估模型的指标,在很多情况下效果良好。但让我们考虑以下问题:你正在设计一种用于罕见疾病的医学诊断测试。假设每百万分之一的人患有这种疾病。如果没有任何训练甚至根本不构建系统,如果你将输出硬编码为始终为负(未发现疾病),则你的系统将始终达到 99.999% 的准确率。这是好事吗?系统达到了 99.999% 的准确率,这听起来可能很神奇,但它永远不会捕捉到患有疾病的患者。这意味着准确率指标不适合衡量该模型的“好坏”。我们需要其他评估指标来衡量模型预测能力的不同方面。
4.1.2 混淆矩阵
为了为其他指标做准备,我们将使用混淆矩阵:一个描述分类模型性能的表格。混淆矩阵本身相对容易理解,但相关的术语一开始可能会有些令人困惑。一旦你理解了它,你会发现这个概念非常直观,并且非常有意义。让我们一步一步地来探讨它。
目标是从除预测准确率之外的不同角度描述模型性能。例如,假设我们正在构建一个分类器来预测患者是否患病。预期的分类是阳性(患者患病)或阴性(患者健康)。我们在 1,000 名患者上运行我们的模型,并将模型预测输入到表 4.1 中。
表 4.1 运行我们的模型以预测健康与患病患者
| 预测患病(阳性) | 预测健康(阴性) | |
|---|---|---|
| 患病患者(阳性) | 100 | 30 |
| 真阳性(TP) | 假阴性(FN) | |
| 健康患者(阴性) | 70 | 800 |
| 假阳性(FP) | 真阴性(TN) |
现在我们定义最基本的概念,它们是整数(不是比率):
-
真阳性(TP)--模型正确预测了是(患者患有疾病)。
-
真阴性(TN)--模型正确地预测为无(患者没有疾病)。
-
假阳性(FP)--模型错误地预测为有,但实际上患者并没有疾病(在某些文献中称为第一类错误或第一类错误)。
-
假阴性(FN)--模型错误地预测为无,但实际上患者确实患有疾病(在某些文献中称为第二类错误或第二类错误)。
模型预测为阴性(无疾病)的患者是模型认为健康的人,我们可以让他们回家,无需进一步治疗。模型预测为阳性(有疾病)的患者是我们将送他们进行进一步检查的人。我们更愿意犯哪种错误?错误地将某人诊断为阳性(有疾病)并让他们接受更多检查,不如错误地将某人诊断为阴性(健康)并让他们回家,冒着生命危险。在这里,评价指标显然是,我们更关心假阴性(FN)的数量。我们希望找到所有患病的人,即使模型错误地将一些健康人分类为患病。这个指标被称为召回率。
4.1.3 精确率和召回率
召回率(也称为灵敏度)告诉我们模型错误地将多少名患病患者诊断为正常。换句话说,模型错误地将多少名患病患者诊断为阴性(假阴性,FN)?召回率通过以下公式计算:

精确率(也称为特异性)是召回率的对立面。它告诉我们模型错误地将多少名健康患者诊断为患病。换句话说,模型错误地将多少名健康患者诊断为阳性(假阳性,FP)?精确率通过以下公式计算:

确定合适的指标
重要的一点是,尽管在健康诊断的例子中我们决定召回率是一个更好的指标,但其他用例可能需要不同的指标,如精确率。为了确定您问题的最合适的指标,请问自己两种可能的错误预测哪一种更严重:假阳性还是假阴性。如果您的答案是 FP,那么您正在寻找精确率。如果 FN 更严重,那么召回率就是您的答案。
以垃圾邮件分类器为例。您更关心哪种错误预测:错误地将非垃圾邮件分类为垃圾邮件,导致其丢失,还是错误地将垃圾邮件分类为非垃圾邮件,之后它进入收件箱文件夹?我相信您会更关心前者。您不希望收件人因为模型将其错误分类为垃圾邮件而丢失邮件。我们希望捕捉到所有垃圾邮件,但丢失非垃圾邮件是非常糟糕的。在这个例子中,精确率是一个合适的指标。
在某些应用中,你可能会同时关心精确率和召回率。这被称为 F 分数,如以下所述。
4.1.4 F 分数
在许多情况下,我们希望用一个代表召回率和精确率的单一指标来总结分类器的性能。为此,我们可以将精确率(p)和召回率(r)转换为单一的 F 分数指标。在数学上,这被称为 p 和 r 的调和平均数:

F 分数可以很好地整体反映你的模型表现如何。让我们再次看看健康诊断的例子。我们一致认为这是一个高召回率的模型。但如果模型在 FN 上表现很好,给出了高召回率分数,但在 FP 上表现不佳,给出了低精确率分数怎么办?FP 表现不佳意味着,为了不漏掉任何生病患者,它错误地将许多患者诊断为生病,以确保安全。因此,虽然召回率对于这个问题可能更重要,但查看模型时从精确率和召回率两个分数一起考虑是好的:
| 精确率 | 召回率 | F 分数 | |
|---|---|---|---|
| 分类器 A | 95% | 90% | 92.4% |
| 分类器 B | 98% | 85% | 91% |
注意:定义模型评估指标是一个必要的步骤,因为它将指导你改进系统的方法。如果没有明确定义的指标,很难判断对机器学习系统的更改是否导致进步。
4.2 设计基线模型
现在你已经选择了你将用于评估系统的指标,是时候建立一个合理的端到端系统来训练你的模型了。根据你要解决的问题,你需要设计基线以适应你的网络类型和架构。在这一步,你将想要回答以下问题:
-
我是否应该使用 MLP 或 CNN 网络(或 RNN,本书后面将解释)?
-
我是否应该使用其他目标检测技术,如 YOLO 或 SSD(在后续章节中解释)?
-
我的网络应该有多深?
-
我将使用哪种激活类型?
-
我将使用哪种优化器?
-
我是否需要添加其他正则化层,如 dropout 或批量归一化,以避免过拟合?
如果你的问题与已经被广泛研究的问题相似,你最好首先复制已知在该任务上表现最佳的模型和算法。你甚至可以使用在另一个数据集上训练的模型来解决你的问题,而无需从头开始训练。这被称为迁移学习,将在第六章中详细讨论。
例如,在上一个章节的项目中,我们使用了流行的 AlexNet 架构作为基线模型。图 4.1 显示了一个 AlexNet 深度 CNN 的架构,以及每层的尺寸。输入层后面是五个卷积层(CONV1 到 CONV5),第五个卷积层的输出被送入两个全连接层(FC6 到 FC7),输出层是一个具有 softmax 函数的全连接层(FC8):
输入 ⇒ CONV1 ⇒ POOL1 ⇒ CONV2 ⇒ POOL2 ⇒ CONV3 ⇒ CONV4 ⇒ CONV5 ⇒ POOL3 ⇒ FC6 ⇒ FC7 ⇒ SOFTMAX_8

图 4.1 AlexNet 架构由五个卷积层和三个 FC 层组成。
观察 AlexNet 架构,您将找到您开始自己的模型所需的所有网络超参数:
-
网络深度(层数):5 个卷积层加上 3 个全连接层
-
层数的深度(过滤器数量):CONV1 = 96,CONV2 = 256,CONV3 = 384,CONV4 = 385,CONV5 = 256
-
过滤器大小:11 × 11、5 × 5、3 × 3、3 × 3、3 × 3
-
ReLU 作为隐藏层(从 CONV1 到 FC7)的激活函数
-
在 CONV1、CONV2 和 CONV5 后的池化层
-
每个有 4,096 个神经元的 FC6 和 FC7
-
FC8 有 1000 个神经元,使用 softmax 激活函数
注意:在下一章中,我们将讨论一些最流行的 CNN 架构及其在 Keras 中的代码实现。我们将查看 LeNet、AlexNet、VGG、ResNet 和 Inception 等网络,这将帮助您了解针对不同问题的最佳架构,并可能激发您发明自己的 CNN 架构。
4.3 准备您的数据以进行训练
我们已经定义了我们将用于评估我们的模型的性能指标,并构建了我们的基线模型架构。让我们准备好我们的数据以进行训练。需要注意的是,这个过程在很大程度上取决于您的问题和数据。在这里,我将解释您在训练模型之前需要执行的基本数据预处理技术。我还会帮助您培养对“准备好的数据”外观的直觉,以便您确定需要哪些预处理技术。
4.3.1 将您的数据拆分为训练/验证/测试
当我们训练一个机器学习模型时,我们将数据拆分为训练和测试数据集(图 4.2)。我们使用训练数据集来训练模型并更新权重,然后我们使用模型之前未见过的测试数据集来评估模型。这里的黄金法则如下:永远不要使用测试数据来训练。我们不应该在训练过程中向模型展示测试样本的原因是确保模型没有作弊。我们向模型展示训练样本以学习它们的特征,然后我们测试它在从未见过的数据集上的泛化能力,以获得其性能的无偏评估。

图 4.2 将数据拆分为训练和测试数据集
什么是验证数据集?
在训练过程的每个 epoch 之后,我们需要评估模型的准确性和误差,以查看其表现并调整其参数。如果我们使用测试数据集在训练过程中评估模型,我们将违反我们永不使用测试数据训练的黄金法则。测试数据仅在训练完成后评估模型的最终性能。因此,我们进行了一个额外的分割,称为验证数据集,以在训练过程中评估和调整参数(图 4.3)。一旦模型完成训练,我们将在测试数据集上测试其最终性能。

图 4.3 在训练过程中评估模型的一个额外分割,称为验证数据集,以保持测试子集在训练完成后的最终测试
看一下这个用于模型训练的伪代码:
for each epoch for each training data instance
propagate error through the network
adjust the weights
calculate the accuracy and error over training data
for each validation data instance
calculate the accuracy and error over the validation data
正如我们在第三章的项目中看到的,当我们训练模型时,在每个 epoch 后,我们得到train_loss、train_acc、val_loss和val_acc(图 4.4)。我们使用这些数据来分析网络的性能并诊断过拟合和欠拟合,正如你将在 4.4 节中看到的。

图 4.4 每个 epoch 后的训练结果
什么是好的训练/验证/测试数据分割?
传统上,在机器学习项目中,训练和测试数据集之间使用 80/20 或 70/30 的分割。当我们添加验证数据集时,我们采用了 60/20/20 或 70/15/15 的分割。但那是在整个数据集只有几万个样本的时候。现在我们拥有的数据量巨大,有时验证和测试集各 1%就足够了。例如,如果我们的数据集包含 100 万个样本,每个测试和验证集有 10,000 个样本是非常合理的,因为保留几十万个样本的数据集是没有意义的。更好的做法是使用这些数据来训练模型。
因此,总结一下,如果你有一个相对较小的数据集,传统的比例可能就足够了。但是,如果你处理的是大型数据集,那么将训练和验证集设置为较小的值是完全可以的。
确保数据集来自相同的分布
在分割你的数据时,需要注意的一个重要事项是确保你的训练/验证/测试数据集来自相同的分布。假设你正在构建一个将在手机上部署的汽车分类器,用于检测汽车型号。记住,深度学习网络对数据有很强的需求,一个常见的经验法则是:你拥有的数据越多,你的模型表现越好。因此,为了获取数据,你决定从互联网上爬取所有高质量的、专业拍摄的汽车图像。你训练你的模型并调整它,你在测试数据集上取得了令人满意的结果,你准备将模型发布到世界——却发现它在手机摄像头拍摄的现实生活中的图像上表现不佳。这是因为你的模型已经被训练和调整以在高质量图像上取得好结果,因此它无法泛化到可能模糊、分辨率较低或具有不同特征的现实中图像。
用更技术性的语言来说,你的训练集和验证集由高质量图像组成,而生产图像(现实生活中的图像)则是低质量图像。因此,将低质量图像添加到你的训练和验证数据集中非常重要。因此,训练/验证/测试数据集应来自相同的分布。
4.3.2 数据预处理
在你将数据输入到神经网络之前,你需要进行一些数据清理和预处理,以便为你的学习模型做好准备。根据你的数据集状态和你要解决的问题,你可以选择几种预处理技术。关于神经网络的好消息是,它们需要最少的数据预处理。当给定大量训练数据时,它们能够从原始数据中提取和学习特征,这与其他传统机器学习技术不同。
话虽如此,预处理仍然可能需要,以提高性能或适应神经网络的具体限制,例如将图像转换为灰度、图像缩放、归一化和数据增强。在本节中,我们将介绍这些预处理概念;我们将在本章末尾的项目中看到它们的代码实现。
图像灰度化
我们在第三章中讨论了彩色图像是如何用三个矩阵表示的,而灰度图像只用一个矩阵表示;彩色图像通过其许多参数增加了计算复杂性。如果你的问题不需要颜色,你可以判断是否将所有图像转换为灰度,以节省计算复杂性。这里的一个好经验法则是使用人类水平性能规则:如果你能在灰度图像中用眼睛识别出物体,那么神经网络很可能也能做到同样的。
图像缩放
神经网络的一个限制是它们需要所有图像具有相同的形状。例如,如果您使用 MLPs,输入层的节点数必须等于图像中的像素数(记住在第三章中我们如何将图像展平以供 MLP 使用)。对于 CNNs 也是如此。您需要设置第一卷积层的输入形状。为了演示这一点,让我们看看添加第一个 CNN 层的 Keras 代码:
model.add(Conv2D(filters=16, kernel_size=2, padding='same', activation='relu', input_shape=(32, 32, 3)))
如您所见,我们必须在第一卷积层中定义图像的形状。例如,如果我们有三个尺寸为 32 × 32、28 × 28 和 64 × 64 的图像,我们必须在将它们输入模型之前将所有图像调整到同一尺寸。
数据归一化
数据归一化是将您的数据缩放的过程,以确保每个输入特征(在图像的情况下为像素)具有相似的数据分布。通常,原始图像由具有不同尺度(值范围)的像素组成。例如,一张图像的像素值范围可能从 0 到 255,而另一张图像的范围可能为 20 到 200。虽然这不是必需的,但将像素值归一化到 0 到 1 的范围以提高学习性能并使网络更快收敛是首选的。
为了使您的神经网络学习更快,您的数据应具有以下特征:
-
小值 --通常,大多数值应在[0, 1]范围内。
-
均匀性 --所有像素的值应在相同的范围内。
数据归一化是通过从每个像素中减去均值,然后将结果除以标准差来完成的。此类数据的分布类似于以零为中心的高斯曲线。为了演示归一化过程,图 4.5 展示了在散点图中的操作。

图 4.5 为了归一化数据,我们从每个像素中减去均值,然后将结果除以标准差。
TIP 确保您使用相同的均值和标准差对训练数据和测试数据进行归一化,因为您希望数据通过相同的转换并精确地以相同的方式进行缩放。您将在本章末尾的项目中看到这是如何实现的。
在未归一化的数据中,损失函数可能看起来像一个挤压、拉长的碗。在归一化特征后,您的损失函数将看起来更加对称。图 4.6 显示了两个特征 F1 和 F2 的损失函数。

图 4.6 归一化特征有助于 GD 算法直接向最小误差前进,从而快速达到(左)。使用未归一化的特征时,GD 会在最小误差的方向上振荡,并且达到最小值较慢(右)。
如你所见,对于归一化的特征,GD 算法直接向最小误差方向前进,因此快速达到它。但对于非归一化的特征,它会在最小误差方向上振荡,最终以长时间下降误差山结束。它最终会达到最小值,但收敛需要更长的时间。
TIP 为什么 GD 在非归一化特征上会振荡?如果我们不对数据进行归一化,特征值的分布范围可能会因每个特征而异,因此学习率将导致每个维度上的校正成比例地不同。这迫使 GD 振荡到最小误差方向,并最终以更长的路径下降误差。
图像增强
数据增强将在本章后面更详细地讨论,当我们介绍正则化技术时。但重要的是你要知道,这是你工具箱中另一个预处理技术,在需要时可以使用。
4.4 评估模型和解释其性能
在建立基线模型并对数据进行预处理之后,是时候训练模型并测量其性能了。训练完成后,你需要确定是否存在瓶颈,诊断哪些组件表现不佳,并确定性能不佳是由于过拟合、欠拟合还是训练数据中的缺陷。
神经网络的主要批评之一是它们是“黑盒子”。即使它们工作得非常好,也很难理解为什么它们工作得这么好。许多努力正在被做出以提高神经网络的解释性,这个领域可能在接下来的几年里快速发展。在本节中,我将向你展示如何诊断神经网络并分析其行为。
4.4.1 诊断过拟合和欠拟合
在运行你的实验后,你希望观察其性能,确定瓶颈是否影响了其性能,并寻找需要改进的区域。机器学习性能不佳的主要原因是训练数据集的过拟合或欠拟合。我们在第三章中讨论了过拟合和欠拟合,但现在我们将更深入地探讨如何检测系统过度拟合训练数据(过拟合)以及它过于简单以拟合数据(欠拟合)的情况:
-
欠拟合意味着模型过于简单:它未能学习训练数据,因此在训练数据上表现不佳。欠拟合的一个例子是使用单个感知器对数据进行分类
和
图形在图 4.7 中。如你所见,一条直线并不能准确分割数据。![图片]()
图 4.7 欠拟合的例子
-
过拟合是指模型对于当前问题过于复杂。它不是学习适合训练数据的特征,而是实际上记住了训练数据。因此,它在训练数据上表现非常好,但在测试之前未见过的新数据上无法泛化。在图 4.8 中,你可以看到模型对数据拟合得太好:它分割了训练数据,但这种拟合将无法泛化。
![]()
图 4.8 过拟合的一个例子
-
我们希望构建一个适合数据的模型:既不过于复杂导致过拟合,也不过于简单导致欠拟合。在图 4.9 中,你可以看到模型错过了一个形状为 O 的数据样本,但它看起来更有可能在新的数据上泛化。
![]()
图 4.9 一个适合数据的模型,并且能够泛化
TIP 我喜欢用来解释过拟合和欠拟合的类比是学生为考试做准备。欠拟合是当学生没有好好复习,所以考试不及格。过拟合是当学生记住了书本内容,在书本问题上的回答正确,但面对书本外的提问回答得不好。学生没有泛化。我们希望的是一个学生能够从书本(训练数据)中学到足够多的知识,以便在问到与书本材料相关的问题时能够泛化。
要诊断欠拟合和过拟合,在训练过程中需要关注的两个值是训练误差和验证误差:
-
如果模型在训练集上表现很好,但在验证集上相对较差,那么它就是过拟合。例如,如果
train_error是 1%,而val_error是 10%,看起来模型已经记住了训练数据集,但在验证集上无法泛化。在这种情况下,你可能需要调整超参数以避免过拟合,并通过迭代训练、测试和评估,直到达到可接受的性能。 -
如果模型在训练集上表现不佳,那么它就是欠拟合。例如,如果
train_error是 14%,而val_error是 15%,模型可能过于简单,无法学习训练集。你可能想要考虑添加更多的隐藏层或延长训练时间(更多的 epoch),或者尝试不同的神经网络架构。
在下一节中,我们将讨论几种避免过拟合和欠拟合的超参数调整技术。
使用人类水平的表现来识别贝叶斯误差率
我们讨论了实现令人满意的性能,但如何知道性能是好是坏?我们需要一个现实的基础线来比较训练和验证误差,以便知道我们是否在改进。理想情况下,0%的误差率是很好的,但并不是所有问题都现实,甚至可能是不可能的。这就是为什么我们需要定义贝叶斯误差率。
贝叶斯错误率表示我们的模型理论上可以达到的最佳错误率。由于人类通常在视觉任务上非常出色,我们可以用人类水平的表现作为贝叶斯错误的代理来衡量。例如,如果你正在处理一个相对简单的任务,比如分类狗和猫,人类非常准确。人类的错误率将非常低:比如说,0.5%。然后我们想比较我们模型的train_error与这个值。如果我们的模型准确率是 95%,那么这不是令人满意的表现,模型可能欠拟合。另一方面,假设我们正在处理一个对人类来说更复杂的任务,比如为放射科医生构建医学图像分类模型。这里的错误率可能会稍高一些:比如说,5%。那么一个准确率为 95%的模型实际上做得很好。
当然,这并不是说深度学习模型永远无法超越人类性能:相反。但这是一种很好的方法来设定基线,以衡量模型是否表现良好。(注意,示例中的错误百分比只是为了示例而任意选择的数字。)
4.4.2 绘制学习曲线
除了查看训练详细输出并比较错误数字外,诊断过拟合和欠拟合的一种方法是在训练过程中绘制你的训练和验证错误,如图 4.10 所示。
图 4.10A 显示,网络在训练数据上提高了损失值(即学习),但在验证数据上未能泛化。在验证数据上的学习在最初的几个 epoch 中进展,然后趋于平稳,甚至可能下降。这是一种过拟合的形式。请注意,这个图表显示网络实际上在训练数据上学习,这是训练正在进行的良好迹象。所以你不需要添加更多的隐藏单元,也不需要构建一个更复杂的模型。如果有什么的话,你的网络对你的数据来说太复杂了,因为它学习得太多,实际上是在记忆数据,未能泛化到新数据。在这种情况下,你的下一步可能是收集更多数据或应用避免过拟合的技术。
图 4.10B 显示,网络在训练和验证数据上都表现不佳。在这种情况下,你的网络没有在学习。你不需要更多的数据,因为网络太简单,无法从你已有的数据中学习。你的下一步是构建一个更复杂的模型。

图 4.10 (a) 网络在训练数据上提高了损失值(即学习),但在验证数据上未能泛化。(b) 网络在训练和验证数据上都表现不佳。(c) 网络学习了训练数据并泛化到验证数据。
图 4.10C 显示,网络在学习和泛化验证数据方面做得很好。这意味着网络在野外测试数据上可能会有很好的性能。
4.4.3 练习:构建、训练和评估网络
在我们进行超参数调整之前,让我们快速运行一个实验,看看我们如何分割数据,构建、训练和可视化模型结果。您可以在 www.manning.com/books/deep-learning-for-vision-systems 或 www.computervisionbook.com 找到这个练习的笔记本。
在这个练习中,我们将执行以下操作:
-
为我们的实验创建玩具数据
-
将数据分为 80% 的训练数据和 20% 的测试数据集
-
构建多层感知器(MLP)神经网络
-
训练模型
-
评估模型
-
可视化结果
这里是步骤:
- 导入依赖项:
from sklearn.datasets import make_blobs ❶
from keras.utils import to_categorical ❷
from keras.models import Sequential ❸
from keras.layers import Dense ❸
from matplotlib import pyplot ❹
❶ 使用 scikit-learn 库生成样本数据
❷ 将类别向量转换为二进制类别矩阵(独热编码)的 Keras 方法
❸ 神经网络和层库
❹ 可视化库
-
使用 scikit-learn 的
make_blobs生成一个只有两个特征和三个标签类别的玩具数据集:X, *y* = make_blobs(n_samples=1000, centers=3, n_features=2, cluster_std=2, random_state=2) -
使用 Keras 的
to_categorical对标签进行独热编码:y = to_categorical(*y*) -
将数据集分为 80% 的训练数据和 20% 的测试数据。注意,在这个例子中,为了简化,我们没有创建验证数据集:
n_train = 800 train_X, test_X = X[:n_train, :], X[n_train:, :] train_y, test_y = y[:n_train], y[n_train:] print(train_X.shape, test_X.shape) >> (800, 2) (200, 2) -
开发模型架构--这里,一个非常简单的、两层 MLP 网络模型(图 4.11 显示了模型摘要):
model = Sequential() model.add(Dense(25, input_dim=2, activation='relu')) ❶ model.add(Dense(3, activation='softmax')) ❷ model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) ❸ model.summary()❶ 由于我们有两个特征,因此有两个输入维度。隐藏层使用 ReLU 激活函数。
❷ 输出层使用 softmax 激活函数,因为有三类,有三个节点
❸ 交叉熵损失函数(在第二章中解释)和 Adam 优化器(将在下一节中解释)
![图 4-11]()
图 4.11 模型摘要
-
训练模型 1,000 个周期:
history = model.fit(train_X, train_y, validation_data=(test_X, test_y), epochs=1000, verbose=1) -
评估模型:
_, train_acc = model.evaluate(train_X, train_y) _, test_acc = model.evaluate(test_X, test_y) print('Train: %.3f, Test: %.3f' % (train_acc, test_acc)) >> Train: 0.825, Test: 0.819 -
绘制模型准确率的学习曲线(图 4.12):
pyplot.plot(history.history['accuracy'], label='train') pyplot.plot(history.history['val_accuracy'], label='test') pyplot.legend() pyplot.show()![图 4-12]()
图 4.12 学习曲线:训练和测试曲线都表现出类似的行为,与数据拟合。
让我们评估网络。查看图 4.12 中的学习曲线,你可以看到训练和测试曲线都表现出类似的行为。这意味着网络没有过拟合,如果训练曲线表现良好而测试曲线不佳,则表明过拟合。但是,网络可能欠拟合吗?也许:在如此简单的数据集上 82% 的性能被认为是较差的。为了提高这个神经网络的性能,我会尝试构建一个更复杂的网络,并尝试其他欠拟合技术。
4.5 改进网络和调整超参数
在你运行训练实验并诊断过拟合和欠拟合之后,你需要决定是否值得花时间调整网络、清理和加工你的数据,或者收集更多数据。你最不想做的事情就是花几个月时间只在一个方向上工作,最后发现几乎没提高网络性能。所以,在讨论不同的超参数调整之前,让我们先回答这个问题:你应该收集更多数据吗?
4.5.1 收集更多数据与调整超参数
我们知道深度神经网络在大量数据上表现良好。考虑到这一点,机器学习新手通常会尝试将更多数据投入到学习算法中,作为提高其性能的第一步。但是,收集和标注更多数据并不总是可行的选择,而且根据你的问题,可能会非常昂贵。此外,它可能甚至并不那么有效。
注意:尽管正在努力自动化数据标注的一些过程,但在撰写本文时,大多数标注仍然是手动完成的,尤其是在计算机视觉问题中。这里指的是实际的人类查看每一张图片并逐个进行标注(这被称为人机交互)。这里还有另一层复杂性:如果你正在标注肺部 X 光片以检测某种肿瘤,例如,你需要有资格的医生来诊断这些图像。这比雇佣人来分类狗和猫的成本要高得多。因此,对于某些准确性问题,收集更多数据可能是一个好的解决方案,并提高模型的鲁棒性,但这并不总是可行的选择。
在其他情况下,收集更多数据比改进学习算法要好得多。所以,如果你有快速有效的方法来确定是收集更多数据还是调整模型超参数会更好,那就太好了。
我用来做出这个决定的流程如下:
-
确定训练集上的性能是否可以接受。
-
可视化并观察这两个指标的性能:训练准确率(
train_acc)和验证准确率(val_acc)。 -
如果网络在训练数据集上表现不佳,这是欠拟合的迹象。没有理由收集更多数据,因为学习算法没有使用已经可用的训练数据。相反,尝试调整超参数或清理训练数据。
-
如果训练集上的性能可以接受,但在测试数据集上却差得多,那么网络就是过拟合了训练数据,并且未能推广到验证集。在这种情况下,收集更多数据可能是有效的。
小贴士:在评估模型性能时,目标是分类高级问题。如果是数据问题,则花更多时间在数据预处理或收集更多数据上。如果是学习算法问题,则尝试调整网络。
4.5.2 参数与超参数
我们不要将参数与超参数混淆。超参数是我们设置和调整的变量。参数是网络在没有任何直接操作的情况下更新的变量。参数是网络在训练期间学习并更新的变量,我们不会调整它们。在神经网络中,参数是在反向传播过程中自动优化以产生最小误差的权重和偏差。相比之下,超参数是网络没有学习的变量。它们在训练模型之前由机器学习工程师设置,然后进行调整。这些变量定义了网络结构并决定了网络的训练方式。超参数的例子包括学习率、批量大小、训练轮数、隐藏层数量以及其他在下一节中讨论的内容。
调整旋钮
将超参数想象成封闭盒子(神经网络)上的旋钮。我们的任务是设置和调整旋钮以获得最佳性能:

4.5.3 神经网络超参数
深度学习算法带有多个控制模型行为许多方面的超参数。一些超参数影响算法的运行时间和内存成本,而其他超参数影响模型的预测能力。
超参数调整的挑战在于没有适用于每个问题的魔法数字。这与我们在第一章中提到的无免费午餐定理相关。好的超参数值取决于数据集和任务。选择最佳超参数并了解如何调整它们需要理解每个超参数的作用。在本节中,你将建立对为什么想要调整超参数一个方向或另一个方向的理解,我将提出一些最有效超参数的良好起始值。
一般而言,我们可以将神经网络超参数分为三个主要类别:
-
网络架构
-
隐藏层数量(网络深度)
-
每一层的神经元数量(层宽度)
-
激活类型
-
学习和优化
-
学习率和衰减计划
-
小批量大小
-
优化算法
-
训练迭代次数或轮数(以及提前停止标准)
-
避免过拟合的正则化技术
-
L2 正则化
-
Dropout 层
-
数据增强
我们在第二章和第三章中讨论了所有这些超参数,除了正则化技术。接下来,我们将快速介绍它们,重点关注当我们调整每个旋钮上下时会发生什么,以及如何知道应该调整哪个超参数。
4.5.4 网络架构
首先,让我们谈谈定义神经网络架构的超参数:
-
隐藏层数量(代表网络深度)
-
每层的神经元数量,也称为隐藏单元(代表网络宽度)
-
激活函数
神经网络的深度和宽度
无论你是在设计一个 MLP、CNN 还是其他神经网络,你都需要决定你网络中的隐藏层数量(深度)以及每层的神经元数量(宽度)。隐藏层数量和单元数量描述了网络的学习能力。目标是设置足够大的数量,以便网络能够学习数据特征。一个较小的网络可能会欠拟合,而一个较大的网络可能会过拟合。要知道什么是“足够大”的网络,你需要选择一个起点,观察性能,然后调整上下。
数据集越复杂,模型学习其特征所需的学习能力就越大。看看图 4.13 中的三个数据集。
如果你给模型提供过多的学习容量(过多的隐藏单元),它可能会倾向于过拟合数据并记住训练集。如果你的模型正在过拟合,你可能想要减少隐藏单元的数量。

图 4.13 数据集越复杂,模型学习其特征所需的学习能力就越大。
通常,添加隐藏神经元直到验证错误不再提高是好的。权衡的是,训练更深层的网络在计算上很昂贵。拥有较少的单元可能会导致欠拟合,而拥有更多的单元通常不会有害,只要适当的正则化(如 dropout 和本章后面讨论的其他内容)。
尝试在 Tensorflow playground(playground.tensorflow.org)上玩一玩,以培养更多的直觉。尝试不同的架构,并在观察网络的学习行为的同时,逐渐添加更多层和隐藏层中的更多单元。
激活类型
激活函数(在第二章中广泛讨论)为我们的神经元引入了非线性。没有激活,我们的神经元只会将线性组合(加权求和)传递给彼此,而无法解决任何非线性问题。这是一个非常活跃的研究领域:每隔几周,我们就会接触到新的激活类型,而且有很多可供选择。但在撰写本文时,ReLU 及其变体(如 Leaky ReLU)在隐藏层中表现最佳。而在输出层,对于分类问题,非常常见的是使用 softmax 函数,其神经元数量等于你问题中的类别数量。
层和参数
当考虑你的神经网络架构中的隐藏层数量和单元数量时,考虑网络中的参数数量及其对计算复杂性的影响是有用的。你网络中的神经元越多,网络需要优化的参数就越多。(在第三章中,我们学习了如何打印模型摘要以查看将要训练的总参数数量。)
根据您的硬件配置(计算能力和内存)来决定您是否需要减少参数数量。为了减少训练参数数量,您可以执行以下操作之一:
-
减少网络的深度和宽度(隐藏层和单元)。这将减少训练参数数量,从而降低神经网络复杂性。
-
添加池化层,或者调整卷积层的步长和填充,以减少特征图维度。这将降低参数数量。
这些只是帮助您了解如何在真实项目中看待训练参数数量以及您需要做出的权衡的例子。复杂的网络导致大量的训练参数,这反过来又导致对计算能力和内存的高需求。
构建您的基线架构的最佳方式是查看可用于解决特定问题的流行架构,并从这里开始;评估其性能,调整其超参数,并重复。记得我们在第三章的图像分类项目中如何受到 AlexNet 的启发来设计我们的 CNN。在第五章中,我们将探讨一些最流行的 CNN 架构,如 LeNet、AlexNet、VGG、ResNet 和 Inception。
4.6 学习与优化
现在我们已经构建了我们的网络架构,是时候讨论那些决定网络如何学习和优化其参数以实现最小误差的超参数了。
4.6.1 学习率和衰减计划
学习率是单个最重要的超参数,应该始终确保它已经被调整。如果只有时间优化一个超参数,那么这个值得调整的超参数就是它。
--约书亚·本吉奥
学习率(lr 值)在第二章中已经进行了广泛讨论。作为复习,让我们思考一下梯度下降(GD)是如何工作的。GD 优化器寻找权重最优值,以产生尽可能低的误差。在设置我们的优化器时,我们需要定义它下降误差山时采取的步长。这个步长就是学习率。它代表了优化器下降误差曲线的速度快慢。当我们仅用一个权重绘制成本函数时,如图 4.14 所示,我们得到一个简化的 U 形曲线,其中权重随机初始化在曲线上的一个点上。

图 4.14 当我们仅用一个权重绘制成本函数时,得到的是一个过于简化的 U 形曲线。
梯度下降法计算梯度以找到减少错误的方向(导数)。在图 4.14 中,下降方向是向右的。梯度下降法在每个迭代(epoch)之后开始向下移动。现在,正如你在图 4.15 中可以看到的,如果我们奇迹般地正确选择了学习率值,我们就能在一步之内找到最小化错误的最佳权重值。这是一个不可能的情况,我正在用它来阐述。让我们称这个值为理想的学习率。

图 4.15 如果我们奇迹般地正确选择了学习率值,我们就能在一步之内找到最小化错误的最佳权重值。
如果学习率小于理想学习率值,那么模型可以通过沿着错误曲线采取更小的步骤继续学习,直到找到权重最优化值(图 4.16)。太小意味着它最终会收敛,但需要更长的时间。

图 4.16 理想学习率值更小的学习率:模型沿着错误曲线采取更小的步骤。
如果学习率大于理想的学习率值,优化器会在第一步中超过最优权重值,然后在下一步中再次在另一侧超过(图 4.17)。这可能会产生比我们开始时更低的错误,并收敛到一个合理的值,但不是我们试图达到的最低错误。

图 4.17 学习率大于理想学习率值:优化器超过最优权重值。
如果学习率远大于理想的学习率值(多出两倍以上),优化器不仅会超过理想的权重值,而且会越来越远离最小错误(图 4.18)。这种现象称为发散。

图 4.18 学习率远大于理想学习率值:优化器越来越远离最小错误。
过高与过低的学习率
将学习率设置得过高或过低是在优化器速度与性能之间进行权衡。太低的学习率需要很多个 epoch 才能收敛,通常太多。理论上,如果学习率太小,算法在无限时间内运行的情况下是保证最终收敛的。另一方面,过高的学习率可能会更快地将我们带到更低的错误值,因为我们沿着错误曲线采取了更大的步骤,但算法振荡并偏离最小值的可能性更大。因此,理想情况下,我们希望选择一个恰到好处的学习率(最优):它迅速达到最小点,而不会太大以至于可能发散。
当将损失值与训练迭代次数(epochs)进行绘图时,你会注意到以下情况:
-
太小的学习率--损失值持续下降,但需要更多的时间来收敛。
-
更大的学习率--损失值达到了比我们开始时更好的值,但仍然远未达到最优。
-
较大的学习率--损失可能最初会下降,但随着权重值越来越远离最佳值,它开始上升。
-
良好的学习率--损失持续下降,直到达到可能的最小值。

非常高、高、良好和低学习率之间的区别
4.6.2 寻找最佳学习率的系统方法
最佳学习率将取决于你的损失景观的拓扑结构,这反过来又取决于你的模型架构和你的数据集。无论你使用 Keras、Tensorflow、PyTorch 还是任何其他深度学习库,使用优化器的默认学习率值是一个好的开始,可以导致不错的结果。每种优化器类型都有自己的默认值。阅读你使用的深度学习库的文档,以了解你优化器的默认值。如果你的模型训练效果不佳,你可以通过使用常见的值--0.1、0.01、0.001、0.0001、0.00001 和 0.000001--来调整学习率变量,以改善性能或加快训练速度,以寻找最佳学习率。
调试此问题的方法是查看训练过程中的验证损失值:
-
如果每次步骤后
val_loss都下降,那很好。继续训练,直到它停止改进。 -
如果训练完成且
val_loss仍在下降,那么可能是因为学习率太小,还没有收敛。在这种情况下,你可以做以下两件事之一:-
使用相同的学习率,但增加更多的训练迭代(周期)来给优化器更多的时间收敛。
-
稍微增加学习率(lr)的值,然后再次进行训练。
-
-
如果
val_loss开始增加或上下波动,那么学习率太高,你需要降低其值。
4.6.3 学习率衰减和自适应学习
找到适合你问题的最佳学习率是一个迭代的过程。你从一个静态的学习率值开始,等待训练完成,评估,然后调整。调整学习率的另一种方法是设置学习率衰减:一种在训练过程中改变学习率的方法。它通常比静态值表现更好,并且可以大幅减少获得最佳结果所需的时间。
到现在为止,很明显,当我们尝试降低学习率时,我们更有可能达到更低的错误点。但训练它将需要更长的时间。在某些情况下,训练时间过长,变得不可行。一个很好的技巧是在我们的学习率中实现一个衰减率。衰减率告诉我们的网络在整个训练过程中自动降低学习率。例如,我们可以通过每个(n)步数减少一个常数(x)来降低学习率。这样,我们可以从较高的值开始,以更大的步幅向最小值迈进,然后在每个(n)个周期后逐渐降低学习率,以避免超过理想的学习率。
实现这一目标的一种方法是通过线性减少学习率(线性衰减)。例如,你可以每五个 epoch 将学习率减半,如图 4.19 所示。

图 4.19 每 5 个 epoch 将学习率 lr 减半
另一种方法是指数减少学习率(指数衰减)。例如,你可以每 8 个 epoch 将学习率乘以 0.1(见图 4.20)。显然,与线性衰减相比,网络收敛速度会慢得多,但最终会收敛。

图 4.20 每 8 个 epoch 将学习率 lr 乘以 0.1
其他聪明的学习算法具有自适应学习率(自适应学习)。这些算法使用启发式方法,在训练停止时自动更新学习率。这意味着不仅需要在必要时减少学习率,还需要在改进太慢(学习率太小)时增加它。自适应学习通常比其他学习率设置策略更有效。Adam 和 Adagrad 是自适应学习优化器的例子:本章后面将详细介绍自适应优化器。
4.6.4 小批量大小
小批量大小是优化器算法中你需要设置和调整的另一个超参数。batch_size超参数对训练过程的资源需求和速度有重大影响。
为了理解小批量,让我们回顾一下我们在第二章中解释的三个 GD 类型--批量、随机和批量:
-
批量梯度下降(BGD)--我们将整个数据集一次性喂给网络,应用前向过程,计算误差,计算梯度,并通过反向传播来更新权重。优化器通过查看所有训练数据生成的误差来计算梯度,并且每个 epoch 后只更新一次权重。因此,在这种情况下,小批量大小等于整个训练数据集。BGD 的主要优点是它具有相对较低的噪声和更大的向最小值迈进的一步(见图 4.21)。主要缺点是处理整个训练数据集可能需要太长时间,尤其是在大数据集上训练时。BGD 还需要大量的内存来训练大型数据集,这可能不可用。如果你在训练小型数据集,BGD 可能是一个不错的选择。
![]()
图 4.21 批量 GD 在向最小误差迈进的过程中具有低噪声
-
随机梯度下降(SGD)--也称为在线学习。我们每次向网络提供单个训练数据实例,并使用这个实例进行前向传播、计算误差、计算梯度,并反向传播以更新权重(图 4.22)。在 SGD 中,权重在看到每个单个实例后更新(与在每一步之前处理整个数据集的批量梯度下降(BGD)相反)。SGD 可能会非常嘈杂,因为它在向全局最小值振荡的过程中会向下迈出一步,这有时可能是错误的方向。通过使用较小的学习率可以减少这种噪声,因此,平均而言,它将你引向正确的方向,并且几乎总是比 BGD 表现更好。使用 SGD,你可以快速取得进展,通常非常接近全局最小值。主要缺点是,通过逐个实例计算 GD,你失去了训练计算中矩阵乘法带来的速度提升。
![图 4.22 随机 GD 在高噪声下振荡其路径至最小误差]()
图 4.22 随机 GD 在高噪声下振荡其路径至最小误差
为了回顾 BGD 和 SGD,在一种极端情况下,如果你将你的小批量大小设置为 1(随机训练),优化器将在计算每个单个训练数据实例的梯度后沿着误差曲线向下迈出一步。这是好的,但你失去了使用矩阵乘法带来的速度提升。在另一种极端情况下,如果你的小批量大小是整个训练数据集,那么你正在使用 BGD。处理大型数据集时,向最小误差迈出一步需要太长时间。在这两个极端之间,存在小批量梯度下降(MB-GD)。
- 小批量梯度下降(MB-GD)--在批量梯度下降和随机梯度下降之间的折衷方案。我们不是从单个样本(SGD)或所有训练样本(BGD)计算梯度,而是将训练样本分成小批量来计算梯度。这样,我们可以利用矩阵乘法加快训练速度,并开始取得进展,而不是等待训练整个训练集。
选择小批量大小的指南
首先,如果你有一个小数据集(大约少于 2,000 个),你可能更适合使用 BGD。你可以相当快地训练整个数据集。
对于大型数据集,你可以使用一系列迷你批量大小的值。迷你批量大小的典型起始值是 64 或 128。然后你可以在这个范围内调整它:32,64,128,256,512,1024,并在需要时将其翻倍以加快训练速度。但请确保你的迷你批量大大小于你的 CPU/GPU 内存。1024 和更大的迷你批量大大小于可能,但相当罕见。更大的迷你批量大大小于允许在训练计算中使用矩阵乘法来提高计算效率。但这需要更多的训练过程内存和通常更多的计算资源。以下图显示了批量大小、计算资源和神经网络训练所需的轮数之间的关系:

批量大小、计算资源和训练轮数之间的关系
4.7 优化算法
在深度学习的历史中,许多研究人员提出了优化算法,并表明它们在某些问题上的表现良好。但其中大多数随后被证明不能很好地推广到我们可能想要训练的广泛神经网络。随着时间的推移,深度学习社区逐渐认识到 GD 算法及其一些变体表现良好。到目前为止,我们已经讨论了批量、随机和迷你批量的 GD。
我们了解到,选择合适的学习率可能具有挑战性,因为学习率太小会导致收敛速度痛苦地缓慢,而学习率太大可能会阻碍收敛,并导致损失函数在最小值周围波动,甚至发散。我们需要更多创造性的解决方案来进一步优化 GD。
注意:大多数深度学习框架的文档中都有很好地解释了优化器类型。在本节中,我将解释两种最受欢迎的基于梯度下降的优化器——动量和 Adam——的概念,它们确实非常突出,并且在广泛的深度学习架构中已被证明效果良好。这将帮助你建立一个良好的基础,以便更深入地了解其他优化算法。有关优化算法的更多信息,请阅读 Sebastian Ruder 的“梯度下降优化算法概述”(arxiv.org/pdf/1609.04747.pdf)。
4.7.1 带动量的梯度下降
回想一下,SGD 最终会在垂直方向上向最小误差振荡(图 4.23)。这些振荡会减慢收敛过程,并使得使用更大的学习率更加困难,这可能导致你的算法超出并发散。

图 4.23 SGD 在垂直方向上向最小误差振荡。
为了减少这些振荡,发明了一种称为动量的技术,它让 GD 沿着相关方向导航,并减轻了无关方向中的振荡。换句话说,它使得在垂直方向上的振荡学习速度变慢,而在水平方向上的进步速度变快,这将帮助优化器更快地达到目标最小值。
这与经典物理学中的动量概念类似:当一个雪球从山上滚下来时,它会积累动量,越滚越快。同样,我们的动量项会增加梯度指向同一方向的维度,并减少梯度改变方向的维度的更新。这有助于加快收敛速度并减少振荡。
动量中的数学是如何工作的
这里的数学非常简单直接。动量是通过向更新权重的方程中添加速度项来构建的:
w[new] = w[old] - α dE/dw**[i] ❶
w[new] = w[old] - 学习率 × 梯度 + 速度项 ❷
❶ 原始更新规则
❷ 添加速度后的新规则
速度项等于过去梯度的加权平均值。
4.7.2 Adam
Adam 代表自适应动量估计。Adam 保持过去梯度的指数衰减平均值,类似于动量。而动量可以看作是一个滚下斜坡的球,Adam 则像是一个带有摩擦的重球,它会减慢动量并控制它。Adam 通常比其他优化器表现更好,因为它可以帮助我们更快地训练神经网络模型。
再次,我们有新的超参数需要调整。但好消息是,主要深度学习框架的默认值通常效果很好,所以你可能不需要调整——除了学习率,它不是 Adam 特有的超参数:
keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None,
decay=0.0)
Adam 的作者提出了这些默认值:
-
需要调整学习率。
-
对于动量项 β1,一个常见的选择是 0.9。
-
对于 RMSprop 项 β2,一个常见的选择是 0.999。
-
ε 被设置为 10^-8。
4.7.3 epochs 数量和提前停止标准
训练迭代,或称为一个 epoch,是指模型完成一个完整周期并一次性看到整个训练数据集。epoch 超参数被设置为定义我们的网络继续训练的迭代次数。训练迭代越多,我们的模型就越能学习到训练数据的特征。为了诊断你的网络是否需要更多或更少的训练 epochs,关注训练和验证错误值。
想象这个直观的方式是,我们希望只要错误值在下降,就继续训练。对吗?让我们看看图 4.24 中网络训练的样本详细输出。

图 4.24 第一五个 epochs 的样本详细输出。训练和验证错误都在改善。
你可以看到训练和验证错误都在下降。这意味着网络仍在学习。在这个时候停止训练是没有意义的。网络显然仍在朝着最小错误的方向进步。让我们再让它训练六个 epoch,并观察结果(图 4.25)。

图 4.25 训练错误仍在改善,但验证错误从第 8 个 epoch 开始开始波动。
看起来训练错误做得很好,仍在改善。这是好的。这意味着网络在训练集上正在改善。然而,如果你看第 8 个和第 9 个 epoch,你会看到val_error开始波动并增加。在train_error改善的同时val_error没有改善意味着网络开始对训练数据进行过拟合,并且无法推广到验证数据。
让我们绘制训练和验证错误(图 4.26)。你可以看到一开始训练和验证错误都在改善,但随后验证错误开始增加,导致过拟合。我们需要找到一种方法在训练开始过拟合之前停止训练。这种技术被称为提前停止。
4.7.4 提前停止
提前停止是一种广泛使用的算法,用于在过拟合发生之前确定停止训练过程的时间。它简单地监控验证错误值,并在值开始增加时停止训练。以下是 Keras 中的提前停止函数:
EarlyStopping(monitor='val_loss', min_delta=0, patience=20)
EarlyStopping函数接受以下参数:
-
monitor--训练期间监控的指标。通常我们希望关注val_loss,因为它代表我们对模型性能的内部测试。如果网络在验证数据上表现良好,它可能在测试数据和生产数据上表现也好。 -
min_delta--作为改善的最低变化量。这个变量没有标准值。为了决定min_delta的值,运行几个 epoch 并观察错误和验证精度的变化。根据变化率定义min_delta。默认值 0 在很多情况下都相当有效。 -
patience--这个变量告诉算法在错误没有改善的情况下应该等待多少个 epoch 后停止训练。例如,如果我们把patience设置为 1,训练将在错误增加的 epoch 时停止。不过,我们必须要有一点灵活性,因为错误通常会稍微波动一下然后继续改善。如果我们发现训练在最后 10 个或 20 个 epoch 内没有改善,我们可以停止训练。
TIP 提前停止的好处是它让你不必太担心 epoch 超参数。你可以设置一个较高的 epoch 数量,并让停止算法在错误停止改善时负责停止训练。
4.8 避免过拟合的正则化技术
如果你观察到你的神经网络正在过拟合训练数据,那么你的网络可能过于复杂,需要简化。你应该尝试的第一个技术之一就是正则化。在本节中,我们将讨论三种最常见的正则化技术:L2、dropout 和数据增强。

图 4.26 在不提高val_error的情况下提高train_error意味着网络开始过拟合。
4.8.1 L2 正则化
L2 正则化的基本思想是在误差函数中添加一个正则化项,从而惩罚误差函数。这反过来又降低了隐藏单元的权重值,使它们变得非常小,接近于零,以帮助简化模型。
让我们看看正则化是如何工作的。首先,我们通过添加正则化项来更新误差函数:
误差函数[new] = 误差函数[old] + 正则化项
注意,你可以使用第二章中解释的任何误差函数,如 MSE 或交叉熵。现在,让我们看看正则化项
L2 正则化项 = λ/2m * Σ || w ||²
其中 lambda (λ)是正则化参数,m是实例数量,w 是权重。更新的误差函数看起来像这样:
误差函数 [new] = 误差函数 [old] + λ/2m * Σ* || w ||²
为什么 L2 正则化可以减少过拟合?好吧,让我们看看在反向传播过程中权重是如何更新的。我们从第二章中了解到,优化器计算误差的导数,将其乘以学习率,然后从旧权重中减去这个值。以下是更新权重的反向传播方程:

由于我们向误差函数中添加了正则化项,新的误差比旧的误差大。这意味着它的导数(∂Error/∂Wx)也更大,导致 Wnew 更小。L2 正则化也称为权重衰减,因为它迫使权重向零衰减(但不是正好为零)。
减少权重导致更简单的神经网络
要了解这是如何工作的,考虑以下情况:如果正则化项非常大,以至于当乘以学习率时,它将等于 Wold,那么这将使新权重等于零。这取消了该神经元的效应,导致具有更少神经元的更简单的神经网络。
在实践中,L2 正则化不会使权重等于零。它只是使它们变得更小以减少它们的影响。大的正则化参数(ƛ)会导致权重可忽略。当权重可忽略时,模型将不会从这些单元中学到很多。这将使网络更简单,从而减少过拟合

L2 正则化减少了权重并简化了网络以减少过拟合。
这就是 L2 正则化在 Keras 中的样子:
model.add(Dense(units=16, kernel_regularizer=regularizers.l2(ƛ),
activation='relu')) ❶
❶ 当向你的网络添加隐藏层时,添加带有 L2 正则化的 kernel_regularization 参数
Lambda 值是一个你可以调整的超参数。你的深度学习库的默认值通常效果很好。如果你仍然看到过拟合的迹象,增加 lambda 超参数以降低模型复杂性。
4.8.2 Dropout 层
Dropout 是一种非常有效的正则化技术,可以简化神经网络并避免过拟合。我们在第三章中详细讨论了 dropout。dropout 算法相当简单:在每次训练迭代中,每个神经元都有概率 p 在这次训练迭代中被临时忽略(dropout)。这意味着它可能在后续迭代中是活跃的。虽然故意暂停网络中某些神经元的训练看起来有些反直觉,但这个技术效果相当惊人。概率 p 是一个超参数,被称为 dropout 率,通常设置在 0.3 到 0.5 之间。从 0.3 开始,如果你看到过拟合的迹象,就增加这个比率。
TIP 我喜欢把 dropout 想象成每天早上和你的团队扔硬币来决定谁将执行一个特定的关键任务。经过几次迭代后,所有团队成员都会学会如何完成这项任务,而不会依赖于单个成员来完成。这样,团队对变化的抵抗力会大大增强。
L2 正则化和 dropout 都旨在通过减少神经元的效率来降低网络复杂性。区别在于 dropout 在每次迭代中完全取消一些神经元的效应,而 L2 正则化只是减少权重值来降低神经元的效率。两者都导致一个更鲁棒、更有弹性的神经网络并减少过拟合。建议你在网络中使用这两种正则化技术。
4.8.3 数据增强
避免过拟合的一种方法是通过获取更多数据。由于这并不总是可行的选择,我们可以通过生成一些变换后的相同图像的新实例来增强我们的训练数据。数据增强可以是一种经济实惠的方法,为你的学习算法提供更多训练数据,从而减少过拟合。
许多图像增强技术包括翻转、旋转、缩放、缩放、光照条件以及你可以应用于数据集的许多其他变换,以提供各种图像进行训练。在图 4.27 中,你可以看到应用于数字 6 图像的一些变换技术。

图 4.27 应用于数字 6 图像的各种图像增强技术
在图 4.27 中,我们创建了 20 张新的图像,网络可以从这些图像中学习。这种合成图像的主要优势是现在你有更多的数据(20 倍)来告诉你的算法,如果一个图像是数字 6,那么即使你垂直或水平翻转它或旋转它,它仍然是数字 6。这使得模型对检测任何形式和形状的数字 6 更加鲁棒。
数据增强被认为是一种正则化技术,因为允许网络看到对象的许多变体可以减少它在特征学习过程中对对象原始形式的依赖。这使得网络在测试新数据时更加鲁棒。
Keras 中的数据增强看起来是这样的:
from keras.preprocessing.image import ImageDataGenerator ❶
datagen = ImageDataGenerator(horizontal_flip=True, vertical_flip=True) ❷
datagen.fit(training_set) ❸
❶ 从 Keras 导入 ImageDataGenerator
❷ 生成新的图像数据批次。ImageDataGenerator 接受变换类型作为参数。在这里,我们将水平和垂直翻转设置为 True。有关更多变换参数,请参阅 Keras 文档(或您的深度学习库)。
❸ 在训练集上计算数据增强
4.9 批归一化
在本章的早期,我们讨论了数据归一化以加快学习速度。我们讨论的归一化技术集中在在输入层之前对训练集进行预处理。如果输入层从归一化中受益,为什么不对隐藏单元中提取的特征做同样的事情呢?这些特征一直在变化,并且可以在训练速度和网络鲁棒性(图 4.28)上获得更多的改进。这个过程被称为批归一化(BN)。

图 4.28 批归一化是对隐藏单元中提取的特征进行归一化。
4.9.1 协变量偏移问题
在我们定义协变量偏移之前,让我们通过一个例子来说明批归一化(BN)面临的问题。假设你正在构建一个猫分类器,并且只使用白色猫的图像来训练你的算法。当你用不同颜色的猫的图像测试这个分类器时,它将不会表现良好。为什么?因为模型是在具有特定分布(白色猫)的训练集上训练的。当测试集中的分布发生变化时,它会混淆模型(图 4.29)。

图 4.29 图 A 是仅包含白色猫的训练集,图 B 是包含各种颜色猫的测试集。圆圈代表猫的图像,星星代表非猫图像。
我们不应该期望在图 A 中的数据上训练的模型会在图 B 中的新分布上表现良好。数据分布变化的概念被称为协变量偏移。
定义:如果一个模型正在学习将数据集 x 映射到标签 y,那么如果 x 的分布发生变化,就称为协变量偏移。当这种情况发生时,你可能需要重新训练你的学习算法。
4.9.2 神经网络中的协变量偏移
要了解神经网络中协变量偏移是如何发生的,可以考虑图 4.30 中的简单四层 MLP。让我们从第三层(L3)的角度来看这个网络。它的输入是 L2 层的激活值(a12、a22、a32 和 a42),这些是从前一层提取的特征。L3 正在尝试将这些输入映射到 ŷ,使其尽可能接近标签 y。当第三层这样做的时候,网络正在调整前一层参数的值。随着参数(w、b)在第一层中变化,第二层的激活值也在变化。因此,从第三隐藏层的角度来看,第二隐藏层的值一直在变化:MLP 正在遭受协变量偏移的问题。批归一化减少了隐藏单元值分布的变化程度,使得这些值变得更加稳定,这样神经网络的后续层就有更坚实的基础。

图 4.30 一个简单的四层 MLP。L1 特征输入到 L2 层。对于 2、3 和 4 层也是如此。
注意:重要的是要认识到批归一化不会取消或减少隐藏单元值的变化。它所做的是确保这种变化分布保持不变:即使单元的确切值发生变化,均值和方差不会变化。
4.9.3 批归一化是如何工作的?
在他们 2015 年的论文“Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift”(arxiv.org/abs/1502.03167)中,Sergey Ioffe 和 Christian Szegedy 提出了 BN 技术,以减少协变量偏移。批归一化在神经网络中每个层的激活函数之前添加了一个操作,以执行以下操作:
-
零中心化输入
-
归一化零中心化输入
-
缩放和移位结果
这个操作让模型学习每个层的输入的最佳尺度和均值。
批归一化中的数学原理
-
为了将输入零中心化,算法需要计算输入均值和标准差(这里的输入指的是当前的 mini-batch:因此有批归一化的说法):
❶
❷❶ 小批量均值
❷ 小批量方差
其中 m 是 mini-batch 中的实例数量,μ[B] 是当前 mini-batch 的均值,σ[B] 是标准差。
-
归一化输入:
![图片]()
其中 x̂ 是零均值和归一化的输入。注意这里我们添加了一个变量(ε)。这是一个很小的数(通常是 10^-5),以避免在 σ 在某些估计中为零时发生除以零的情况。
-
缩放和移位结果。我们将归一化输出乘以变量 γ 以缩放它,并加上 (β) 以移位它
y[i] ← γ X[i] + β
y[i] 是 BN 操作的输出,经过缩放和移位。
注意,BN 向网络引入了两个新的可学习参数:γ和β。因此,我们的优化算法将像更新权重和偏差一样更新γ和β的参数。在实践中,这意味着你可能会发现训练最初相当缓慢,因为 GD 正在寻找每个层的最佳缩放和偏移量,但一旦找到了合理的值,它就会加速。
[i]
4.9.4 Keras 中的批归一化实现
了解批归一化的工作原理非常重要,这样你可以更好地理解你的代码正在做什么。但是,当你在网络中使用 BN 时,你不必自己实现所有这些细节。实现 BN 通常只需添加一行代码,使用任何深度学习框架。在 Keras 中,你通过在隐藏层之后添加一个 BN 层来将批归一化添加到你的神经网络中,以便在将结果馈送到下一层之前对其进行归一化。
以下代码片段展示了如何在构建神经网络时添加 BN 层:
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.layers.normalization import BatchNormalization ❶
model = Sequential() ❷
model.add(Dense(hidden_units, activation='relu')) ❸
model.add(BatchNormalization()) ❹
model.add(Dropout(0.5)) ❺
model.add(Dense(units, activation='relu')) ❻
model.add(BatchNormalization()) ❼
model.add(Dense(2, activation='softmax')) ❽
❶ 从 Keras 库中导入 BatchNormalization 层
❷ 初始化模型
❸ 添加第一个隐藏层
❹ 将批归一化层添加到第 1 层的输出结果中进行归一化
❺ 如果你正在向你的网络添加 dropout,最好在批归一化层之后添加它,因为你不希望随机关闭的节点错过归一化步骤。
❻ 添加第二个隐藏层
❶ 将批归一化层添加到第 2 层的输出结果中进行归一化
❽ 输出层
4.9.5 批归一化回顾
我希望你能从这次讨论中得到的直觉是,BN 不仅应用于输入层,还应用于神经网络中隐藏层的值。这减弱了早期和后期层之间学习过程的耦合,允许网络的每一层更独立地学习。
从网络中较后层的角度来看,较前层的节点不能移动太多,因为它们被限制为具有相同的均值和方差。这使得在较后层的学习工作更容易。这种情况的发生是通过确保隐藏单元具有由两个显式参数γ和β控制的标准化分布(均值和方差),学习算法在训练期间设置这些参数。
4.10 项目:在图像分类中实现高准确率
在这个项目中,我们将回顾第三章中的 CIFAR-10 分类项目,并将本章的一些改进技术应用于提高准确率,从约 65%提高到约 90%。你可以通过访问书籍的网站www.manning.com/books/deep-learning-for-vision-systems或www.computervisionbook.com来跟随这个示例,查看代码笔记本。
我们将通过以下步骤完成项目:
-
导入依赖项。
-
准备训练数据:
-
从 Keras 库中下载数据
-
将其分割为训练、验证和测试数据集。
-
归一化数据。
-
对标签进行独热编码。
-
-
构建模型架构。除了第三章中提到的常规卷积和池化层外,我们还将以下层添加到我们的架构中:
-
更深的神经网络以增加学习容量
-
Dropout 层
-
对卷积层应用 L2 正则化
-
批标准化层
-
-
训练模型。
-
评估模型。
-
绘制学习曲线。
让我们看看这是如何实现的。
第 1 步:导入依赖项
这是导入所需依赖项的 Keras 代码:
import keras ❶
from keras.datasets import cifar10
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.utils import np_utils
from keras.layers import Dense, Activation, Flatten, Dropout, BatchNormalization,
Conv2D, MaxPooling2D
from keras.callbacks import ModelCheckpoint
from keras import regularizers, optimizers
import numpy as np ❷
from matplotlib import pyplot ❸
❶ 使用 Keras 库下载数据集,预处理图像和网络组件
❷ 导入 numpy 进行数学运算
❸ 导入 matplotlib 库以可视化结果
第 2 步:准备训练数据
Keras 为我们提供了可下载和实验的一些数据集。这些数据集通常是预处理的,几乎可以直接输入到神经网络中。在本项目中,我们使用的是 CIFAR-10 数据集,它包含 50,000 张 32 × 32 彩色训练图像,分为 10 个类别,以及 10,000 张测试图像。有关 CIFAR-100、MNIST、Fashion-MNIST 等更多数据集,请查看 Keras 文档。
Keras 已经将 CIFAR-10 数据集分割成训练集和测试集。我们将加载它们,然后将训练数据集分割成 45,000 张图像用于训练和 5,000 张图像用于验证,如本章所述:
(x_train, y_train), (x_test, y_test) = cifar10.load_data() ❶
x_train = x_train.astype('float32') ❶
x_test = x_test.astype('float32') ❶
(x_train, x_valid) = x_train[5000:], x_train[:5000] ❷
(y_train, y_valid) = y_train[5000:], y_train[:5000] ❷
❶ 下载并分割数据
❷ 将训练集分割为训练集和验证集
让我们打印x_train、x_valid和x_test的形状:
print('x_train =', x_train.shape)
print('x_valid =', x_valid.shape)
print('x_test =', x_test.shape)
>> x_train = (45000, 32, 32, 3)
>> x_valid = (5000, 32, 32, 3)
>> x_test = (1000, 32, 32, 3)
形状元组的格式如下:(实例数量,宽度,高度,通道)。
归一化数据
通过从每个像素中减去平均值然后除以标准差来对图像的像素值进行归一化:
mean = np.mean(x_train,axis=(0,1,2,3))
std = np.std(x_train,axis=(0,1,2,3))
x_train = (x_train-mean)/(std+1e-7)
x_valid = (x_valid-mean)/(std+1e-7)
x_test = (x_test-mean)/(std+1e-7)
对标签进行独热编码
为了在训练、验证和测试数据集中对标签进行独热编码,我们使用 Keras 中的to_categorical函数:
num_classes = 10
y_train = np_utils.to_categorical(y_train,num_classes)
y_valid = np_utils.to_categorical(y_valid,num_classes)
y_test = np_utils.to_categorical(y_test,num_classes)
数据增强
对于增强技术,我们将任意选择以下转换:旋转、宽度和高度偏移以及水平翻转。当你处理问题时,查看网络未能识别或提供较差检测的图像,并尝试理解为什么它在这些图像上表现不佳。然后创建你的假设并对其进行实验。例如,如果遗漏的图像是旋转的形状,你可能想尝试旋转增强。你会应用它,实验,评估,并重复。你将完全通过分析你的数据和了解网络性能来做出决定:
datagen = ImageDataGenerator( ❶
rotation_range=15,
width_shift_range=0.1,
height_shift_range=0.1,
horizontal_flip=True,
vertical_flip=False
)
datagen.fit(x_train) ❷
❶ 数据增强
❷ 在训练集上计算数据增强
第 3 步:构建模型架构
在第三章中,我们构建了一个受 AlexNet 启发的架构(3 个卷积层 + 2 个全连接层)。在本项目中,我们将构建一个更深层的网络以增加学习容量(6 个卷积层 + 1 个全连接层)。
网络具有以下配置:
-
我们不会在每个卷积层之后添加池化层,而是每两个卷积层之后添加一个。这个想法是受 VGGNet 的启发,VGGNet 是由牛津大学视觉几何组开发的一个流行的神经网络架构。VGGNet 将在第五章中解释。
-
受 VGGNet 的启发,我们将卷积层的
kernel_size设置为 3 × 3,并将池化层的pool_size设置为 2 × 2。 -
我们将在每隔一个卷积层后添加 Dropout 层,(p)的范围从 0.2 到 0.4。
-
在每个卷积层之后将添加一个批量归一化层,以归一化下一层的输入。
-
在 Keras 中,L2 正则化被添加到卷积层代码中。
下面是代码:
base_hidden_units = 32 ❶
weight_decay = 1e-4 ❷
model = Sequential() ❸
# CONV1
model.add(Conv2D(base_hidden_units, kernel_size= 3, padding='same', ❹
kernel_regularizer=regularizers.l2(weight_decay), ❺
input_shape=x_train.shape[1:]))
model.add(Activation('relu')) ❻
model.add(BatchNormalization()) ❼
# CONV2
model.add(Conv2D(base_hidden_units, kernel_size= 3, padding='same',
kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# POOL + Dropout
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2)) ❽
# CONV3
model.add(Conv2D(base_hidden_units * 2, kernel_size= 3, padding='same', ❾
kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# CONV4
model.add(Conv2D(base_hidden_units * 2, kernel_size= 3, padding='same',
kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# POOL + Dropout
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3))
# CONV5
model.add(Conv2D(base_hidden_units * 4, kernel_size= 3, padding='same',
kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# CONV6
model.add(Conv2D(base_hidden_units * 4, kernel_size= 3, padding='same',
kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# POOL + Dropout
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4))
# FC7
model.add(Flatten()) ❿
model.add(Dense(10, activation='softmax')) ⓫
model.summary() ⓬
❶ 隐藏单元数量变量。我们在这里声明这个变量,并在我们的卷积层中使用它,以便更容易从一处更新。
❷ L2 正则化超参数(ƛ)
❸ 创建一个序列模型(层线性堆叠)
❹ 注意,我们在这里定义了 input_shape,因为这是第一个卷积层。对于剩余的层,我们不需要这样做。
❺ 在卷积层中添加 L2 正则化
❻ 所有隐藏层使用 ReLU 激活函数
❼ 添加一个批量归一化层
❽ 20%概率的 Dropout 层
❾ 隐藏单元数量 = 64
❿ 将特征图展平为 1D 特征向量(在第三章中解释)
⓫ 10 个隐藏单元,因为数据集有 10 个类别标签。输出层使用 Softmax 激活函数(在第二章中解释)
⓬ 打印模型摘要
模型摘要如图 4.31 所示。
第 4 步:训练模型
在我们进入训练代码之前,让我们讨论一下一些超参数设置背后的策略:
-
batch_size--这是本章中我们讨论的迷你批大小超参数。batch_size越高,你的算法学习得越快。你可以从 64 个迷你批开始,并将此值加倍以加快训练速度。我在我的机器上尝试了 256,得到了以下错误,这意味着我的机器内存不足。然后我将它降低到 128:Resource exhausted: OOM when allocating tensor with shape[256,128,4,4] -
epochs--我开始使用 50 次训练迭代,发现网络仍在改进。所以我继续添加更多的 epochs 并观察训练结果。在这个项目中,我在 125 个 epochs 后达到了>90%的准确率。正如你很快就会看到的,如果你让它训练更长的时间,仍然有改进的空间。![]()
图 4.31 模型摘要
-
优化器--我使用了 Adam 优化器。参见第 4.7 节了解更多关于优化算法的信息。
注意:请注意,我在这个实验中使用了 GPU。训练大约花费了 3 个小时。建议您使用自己的 GPU 或任何云计算服务以获得最佳结果。如果您无法访问 GPU,我建议您尝试更少的 epoch 数量,或者计划让您的机器在夜间或甚至几天内进行训练,具体取决于您的 CPU 规格。
让我们看看训练代码:
batch_size = 128 ❶
epochs = 125 ❷
checkpointer = ModelCheckpoint(filepath='model.100epochs.hdf5', verbose=1,
save_best_only=True ) ❸
optimizer = keras.optimizers.adam(lr=0.0001,decay=1e-6) ❹
model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy']) ❺
history = model.fit_generator(datagen.flow(x_train, y_train, batch_size=batch_size), callbacks=[checkpointer], steps_per_epoch=x_train.shape[0] // batch_size, epochs=epochs, verbose=2, validation_data=(x_valid, y_valid)) ❻
❶ 小批量大小
❷ 训练迭代次数
❸ 指定保存最佳权重的文件路径,以及一个布尔值 True,表示只有当权重有所改进时才保存权重。
❹ 学习率=0.0001 的 Adam 优化器
❺ 交叉熵损失函数(在第二章中解释)
❻ 允许您在 CPU 上并行对图像进行实时数据增强,同时训练您的模型在 GPU 上。检查点回调保存模型权重;您可以添加其他回调,如早停功能。
当您运行此代码时,您将看到每个 epoch 的网络训练的详细输出。关注loss和val_loss值以分析网络并诊断瓶颈。图 4.32 显示了第 121 至 125 个 epoch 的详细输出。

图 4.32 第 121 至 125 个 epoch 的详细输出
第 5 步:评估模型
为了评估模型,我们使用 Keras 函数evaluate并打印结果:
scores = model.evaluate(x_test, y_test, batch_size=128, verbose=1)
print('\nTest result: %.3f loss: %.3f' % (scores[1]*100,scores[0]))
>> Test result: 90.260 loss: 0.398
绘制学习曲线
绘制学习曲线以分析训练性能并诊断过拟合和欠拟合(图 4.33):
pyplot.plot(history.history['acc'], label='train')
pyplot.plot(history.history['val_acc'], label='test')
pyplot.legend()
pyplot.show()

图 4.33 学习曲线
进一步改进
90%的准确率相当不错,但您仍然可以进一步提高。以下是一些您可以尝试的想法:
-
更多训练 epoch--请注意,网络直到第 123 个 epoch 都在改进。您可以增加 epoch 数量到 150 或 200,让网络训练更长的时间。
-
深度网络--尝试添加更多层以增加模型复杂度,这会增加学习容量。
-
降低学习率--降低 lr(如果您这样做,应该训练更长的时间)。
-
不同的 CNN 架构--尝试使用 Inception 或 ResNet(在下一章中详细解释)。经过 200 个 epoch 的训练后,ResNet 神经网络可以达到高达 95%的准确率。
-
迁移学习--在第六章中,我们将探讨使用预训练网络在您的数据集上以更少的学习时间获得更高结果的技术。
摘要
-
一般规则是,您的网络越深,它学得越好。
-
在撰写本文时,ReLU 在隐藏层中表现最佳,softmax 在输出层中表现最佳。
-
随机梯度下降通常能成功找到最小值。但如果你需要快速收敛并且正在训练一个复杂的神经网络,使用 Adam 是安全的。
-
通常,您训练得越多,效果越好。
-
L2 正则化和 dropout 结合使用可以很好地减少网络复杂性和过拟合。
第二部分:图像分类和检测
人工智能研究领域的快速发展正使得每天都能在各个行业中构建出之前几年不可能实现的新应用。通过学习这些工具,你将能够自己发明新的产品和应用。即使你最终没有从事计算机视觉工作,这里许多概念对深度学习算法和架构也是非常有用的。
在第一部分中学习了深度学习的基础之后,现在是时候构建一个机器学习项目来检验你所学的知识了。在这里,我们将介绍快速有效地使深度学习系统运行、分析结果以及提高网络性能的策略,特别是通过深入研究高级卷积神经网络、迁移学习和目标检测。
5 个高级 CNN 架构
本章涵盖
-
与 CNN 设计模式合作
-
理解 LeNet、AlexNet、VGGNet、Inception 和 ResNet 网络架构
欢迎来到本书的第二部分。第一部分介绍了神经网络架构的基础,涵盖了多层感知器(MLPs)和卷积神经网络(CNNs)。我们在第一部分结束时总结了构建你的深度神经网络项目并调整其超参数以改进网络性能的策略。在第二部分,我们将在此基础上开发计算机视觉(CV)系统,以解决复杂图像分类和目标检测问题。
在第三章和第四章中,我们讨论了 CNN 的主要组件以及设置超参数,如隐藏层数量、学习率、优化器等。我们还讨论了其他提高网络性能的技术,如正则化、增强和 dropout。在本章中,你将看到这些元素是如何结合在一起构建卷积网络的。我将带你了解当时处于前沿的五种最流行的 CNN,你将看到它们的开发者是如何考虑构建、训练和改进网络的。我们将从 1998 年开发的 LeNet 开始,它在识别手写字符方面表现相当不错。你将看到自那时起 CNN 架构是如何发展到更深的 CNN,如 AlexNet 和 VGGNet,以及更高级的超级深度网络,如 2014 年和 2015 年开发的 Inception 和 ResNet。
对于每个 CNN 架构,你将学习以下内容:
-
新特性--我们将探索这些网络区别于其他网络的新特性以及它们的创造者试图解决的具体问题。
-
网络架构--我们将涵盖每个网络的架构和组件,并查看它们是如何结合在一起形成端到端网络的。
-
网络代码实现--我们将逐步通过使用 Keras 深度学习(DL)库的网络实现进行讲解。本节的目标是让你学会如何阅读研究论文,并在需要时实现新的架构。
-
设置学习超参数--在实现网络架构之后,你需要设置你在第四章中学到的学习算法的超参数(优化器、学习率、权重衰减等)。我们将按照每个网络的原研究论文中呈现的方式实现学习超参数。在本节中,你将看到性能是如何从一种网络演变到另一种网络,以及这些演变是如何随时间发展的。
-
网络性能--最后,你将看到每个网络在基准数据集(如 MNIST 和 ImageNet)上的表现,正如它们的研究论文中所展示的那样。
本章的三个主要目标是:
-
理解高级 CNN 的架构和超参数的学习。你将实现像 AlexNet 和 VGGNet 这样的简单到中等复杂性的 CNN。对于非常复杂的问题,你可能想使用更深层的网络,如 Inception 和 ResNet。
-
理解每个网络的独特特性和它们被开发的原因。每个后续的 CNN 架构都解决了前一个架构中的特定限制。在阅读本章中关于五个网络(及其研究论文)的内容后,你将为阅读和理解新出现的网络打下坚实的基础。
-
学习 CNN 如何演变以及其设计者的思维过程。这将帮助你培养对什么有效以及在你构建自己的网络时可能出现的什么问题的直觉。
在第三章中,你学习了 CNN 的基本构建块,包括卷积层、池化层和全连接层。正如你将在本章中看到的,近年来,许多计算机视觉研究都集中在如何将这些基本构建块组合起来形成有效的 CNN。让你培养直觉的最好方法之一是检查并学习这些架构(类似于我们大多数人可能通过阅读他人的代码来学习编写代码)。
为了充分利用本章,我们鼓励你在阅读我的解释之前先阅读每个部分中链接的研究论文。本书第一部分的内容已经完全装备你开始阅读 AI 领域的先驱者所写的研究论文。阅读和实施研究论文是迄今为止你将从这个书中建立的最有价值的技能之一。
小贴士:我个人认为,阅读研究论文、解释其背后的核心,并实现代码是一项非常重要的技能,每个深度学习爱好者和实践者都应该掌握。实际实施研究想法可以揭示作者的思维过程,并帮助将这些想法转化为现实世界的工业应用。我希望通过阅读本章,你将能够轻松阅读研究论文并在自己的工作中实施其发现。这个领域的快速发展要求我们始终跟上最新的研究。现在你在本书(或在其他出版物)中学到的知识在三年或四年内可能不再是最新和最好的——甚至可能更早。我希望你从本书中带走的最有价值的资产是一个强大的深度学习基础,它能够让你走出现实世界,能够阅读最新的研究并自己实现它。
你准备好了吗?让我们开始吧!
5.1 CNN 设计模式
在我们深入探讨常见的 CNN 架构的细节之前,我们将探讨一些关于 CNN 的常见设计选择。一开始可能会觉得有太多的选择要做。每次我们学习到深度学习中的新内容,都会给我们带来更多的超参数来设计。因此,通过查看由该领域的先驱研究者创建的一些常见模式,我们能够理解他们的动机,并从他们结束的地方开始,而不是完全随机地做事,这很好:
-
模式 1:特征提取和分类 -- 卷积网络通常由两部分组成:特征提取部分,由一系列卷积层组成;以及分类部分,由一系列全连接层组成(图 5.1)。这种情况在 ConvNets 中几乎是始终如一的,从 LeNet 和 AlexNet 到最近几年出现的 Inception 和 ResNet 等最新的 CNN。
![图片]()
图 5.1 卷积网络通常包括特征提取和分类。
-
模式 2:图像深度增加,维度减少 -- 每一层的输入数据是一个图像。随着每一层的应用,我们会在新的图像上应用一个新的卷积层。这促使我们以更通用的方式思考图像。首先,您会看到每个图像都是一个具有高度、宽度和深度的 3D 对象。深度指的是颜色通道,对于灰度图像,深度为 1,对于彩色图像,深度为 3。在后续的层中,图像仍然具有深度,但它们本身不是颜色:它们是特征图,代表从上一层提取的特征。这就是为什么随着我们深入网络层,深度会增加。在图 5.2 中,图像的深度等于 96;这表示该层中的特征图数量。所以,这是一个您总会看到的模式:图像深度增加,维度减少。
![图片]()
图 5.2 图像深度增加,维度减少。
-
模式 3:全连接层 -- 这通常不像前两种模式那么严格,但了解这一点非常有帮助。通常,网络中的所有全连接层要么具有相同数量的隐藏单元,要么在每一层中递减。很难找到一个网络,其全连接层中的单元数量在每一层中都在增加。研究发现,保持单元数量不变并不会损害神经网络,因此,如果您想限制在设计网络时必须做出的选择数量,这可能是一个好方法。这样,您只需选择每一层的单元数量,并将其应用于所有全连接层。
现在你已经了解了基本的 CNN 模式,让我们来看看一些实现了这些模式的架构。其中大多数架构之所以闻名,是因为它们在 ImageNet 竞赛中表现出色。ImageNet 是一个包含数百万图像的著名基准,DL 和 CV 研究人员使用 ImageNet 数据集来比较算法。关于这一点,我们稍后再谈。
备注:本章中的代码片段并非旨在可运行。目的是向你展示如何实现研究论文中定义的规范。请访问本书的网站 (www.manning.com/books/deep-learning-for-vision-systems) 或 Github 仓库 (github.com/moelgendy/deep_learning_for_vision_systems) 以获取完整的可执行代码。
现在,让我们开始本章将要讨论的第一个网络:LeNet。
5.2 LeNet-5
1998 年,Lecun 等人引入了一种开创性的 CNN,称为 LeNet-5。1 LeNet-5 架构简单明了,其中的组件对你来说并不陌生(它们在 1998 年时是新的);你在第三章中学习了卷积、池化和全连接层。该架构由五个权重层组成,因此得名 LeNet-5:三个卷积层和两个全连接层。
定义:我们称卷积层和全连接层为权重层,因为它们包含可训练的权重,而池化层则不包含任何权重。常见的做法是使用权重层的数量来描述网络的深度。例如,AlexNet(将在下一节中解释)被称为八层深度,因为它包含五个卷积层和三个全连接层。我们更关注权重层的原因主要是它们反映了模型的计算复杂度。
5.2.1 LeNet 架构
LeNet-5 的架构如图 5.3 所示:
输入图像 ⇒ C1 ⇒ TANH ⇒ S2 ⇒ C3 ⇒ TANH ⇒ S4 ⇒ C5 ⇒ TANH ⇒ FC6 ⇒ SOFTMAX7
其中 C 代表卷积层,S 代表下采样或池化层,FC 代表全连接层。
注意到 Yann LeCun 和他的团队使用了 tanh 作为激活函数,而不是目前最先进的 ReLU。这是因为 1998 年,ReLU 还没有在 DL 的背景下使用,而在隐藏层中更常见的是使用 tanh 或 sigmoid 作为激活函数。不再赘述,让我们在 Keras 中实现 LeNet-5。

图 5.3 LeNet 架构
5.2.2 Keras 中的 LeNet-5 实现
要在 Keras 中实现 LeNet-5,请阅读原始论文并遵循第 6-8 页的架构信息。以下是构建 LeNet-5 网络的主要要点:
-
每个卷积层的滤波器数量--正如你在图 5.3 中所见(以及论文中定义的),每个卷积层的深度(滤波器数量)如下:C1 有 6 个,C3 有 16 个,C5 有 120 个。
-
每个卷积层的核大小--论文中指定
kernel_size为 5 × 5。 -
子采样(池化)层--在每个卷积层之后添加一个子采样(池化)层。每个单元的感受野是一个 2 × 2 的区域(例如,
pool_size为 2)。请注意,LeNet-5 的创造者使用了平均池化,它计算其输入的平均值,而不是我们在早期项目中使用的最大池化层,后者传递其输入的最大值。如果您感兴趣,可以尝试两者,看看区别。对于这个实验,我们将遵循论文的架构。 -
激活函数--如前所述,LeNet-5 的创造者为了隐藏层使用了 tanh 激活函数,因为对称函数被认为比 sigmoid 函数收敛更快(图 5.4)。

图 5.4 LeNet 架构由大小为 5 × 5 的卷积核、池化层、激活函数(tanh)以及分别有 120、84 和 10 个神经元的三个全连接层组成。
现在我们将其放入代码中,以构建 LeNet-5 架构:
from keras.models import Sequential ❶
from keras.layers import Conv2D, AveragePooling2D, Flatten, Dense ❶
model = Sequential() ❷
# C1 Convolutional Layer
model.add(Conv2D(filters = 6, kernel_size = 5, strides = 1, activation = 'tanh',
input_shape = (28,28,1), padding = 'same'))
# S2 Pooling Layer
model.add(AveragePooling2D(pool_size = 2, strides = 2, padding = 'valid'))
# C3 Convolutional Layer
model.add(Conv2D(filters = 16, kernel_size = 5, strides = 1,activation = 'tanh',
padding = 'valid'))
# S4 Pooling Layer
model.add(AveragePooling2D(pool_size = 2, strides = 2, padding = 'valid'))
# C5 Convolutional Layer
model.add(Conv2D(filters = 120, kernel_size = 5, strides = 1,activation = 'tanh',
padding = 'valid'))
model.add(Flatten()) ❸
# FC6 Fully Connected Layer
model.add(Dense(units = 84, activation = 'tanh'))
# FC7 Output layer with softmax activation
model.add(Dense(units = 10, activation = 'softmax'))
model.summary() ❹
❶ 导入 Keras 模型和层
❷ 实例化一个空的序列模型
❸ 将 CNN 输出展平以供全连接层完全连接
❹ 打印模型摘要(图 5.5)
LeNet-5 按照今天的标准来说是一个小型的神经网络。它有 61,706 个参数,与本章后面将要看到的更现代网络的数百万个参数相比。
阅读本章讨论的论文时的注意事项
当你阅读 LeNet-5 论文时,只需知道它比本章我们将要讨论的其他论文更难读。本节中提到的大多数想法都在论文的第 2 和第三部分。论文的后期部分讨论了被称为图变换网络的东西,这在今天并不广泛使用。所以如果你真的尝试去读这篇论文,我建议你专注于第二部分,它讨论了 LeNet 架构和学习细节;然后可以快速浏览第三部分,其中包含一些相当有趣的实验和结果。
我建议先从 AlexNet 论文(在第 5.3 节中讨论)开始,然后是 VGGNet 论文(第 5.4 节),最后是 LeNet 论文。一旦你阅读了其他论文,它是一个很好的经典论文去研究。

图 5.5 LeNet-5 模型摘要
5.2.3 设置学习超参数
LeCun 及其团队使用了计划衰减学习,其中学习率的值按照以下计划降低:前两个 epoch 为 0.0005,接下来的三个 epoch 为 0.0002,接下来的四个 epoch 为 0.00005,然后之后为 0.00001。在论文中,作者训练了他们的网络 20 个 epoch。
让我们根据这个计划构建一个lr_schedule函数。该方法接受一个整数 epoch 号作为参数,并返回学习率(lr):
def lr_schedule(epoch):
if epoch <= 2: ❶
lr = 5e-4
elif epoch > 2 and epoch <= 5:
lr = 2e-4
elif epoch > 5 and epoch <= 9:
lr = 5e-5
else:
lr = 1e-5
return lr
❶前两个 epoch 的学习率 lr 为 0.0005,接下来的三个 epoch(3 到 5)为 0.0002,接下来的四个 epoch(6 到 9)为 0.00005,之后为 0.00001(超过 9)。
我们在以下代码片段中使用lr_schedule函数来编译模型:
from keras.callbacks import ModelCheckpoint, LearningRateScheduler
lr_scheduler = LearningRateScheduler(lr_schedule)
checkpoint = ModelCheckpoint(filepath='path_to_save_file/file.hdf5',
monitor='val_acc',
verbose=1,
save_best_only=True)
callbacks = [checkpoint, lr_reducer]
model.compile(loss='categorical_crossentropy', optimizer='sgd',
metrics=['accuracy'])
现在开始网络训练 20 个 epoch,正如论文中提到的:
hist = model.fit(X_train, y_train, batch_size=32, epochs=20,
validation_data=(X_test, y_test), callbacks=callbacks,
verbose=2, shuffle=True)
如果你想看到完整的代码实现,请参阅书中代码附带的可下载笔记本。
5.2.4 LeNet 在 MNIST 数据集上的性能
当你在 MNIST 数据集上训练 LeNet-5 时,你将获得超过 99%的准确率(参见书中代码的代码笔记本)。尝试使用隐藏层中的 ReLU 激活函数重新运行此实验,并观察网络性能的差异。
5.3 AlexNet
LeNet 在 MNIST 数据集上表现非常好。但结果证明,MNIST 数据集非常简单,因为它包含灰度图像(1 个通道)并且只分为 10 个类别,这使得它是一个更容易的挑战。AlexNet 背后的主要动机是构建一个更深层的网络,能够学习更复杂的函数。
AlexNet(图 5.6)是 2012 年 ILSVRC 图像分类竞赛的获胜者。Krizhevsky 等人创建了神经网络架构,并在 1.2 百万张高分辨率图像上训练,将其分为 ImageNet 数据集的 1000 个不同的类别。2 AlexNet 在其时代是顶尖的,因为它是最早的真正“深度”网络,为 CV 社区打开了认真考虑卷积网络在应用中的大门。我们将在本章后面解释更深的网络,如 VGGNet 和 ResNet,但了解卷积网络是如何演变的以及 AlexNet 的主要缺点是很好的,这些缺点是后来网络的主要动机。

图 5.6 AlexNet 架构
如图 5.6 所示,AlexNet 与 LeNet 有很多相似之处,但更深(更多隐藏层)且更大(每层更多过滤器)。它们有相似的构建模块:一系列堆叠在一起的卷积和池化层,随后是全连接层和 softmax。我们了解到 LeNet 大约有 61,000 个参数,而 AlexNet 有大约 6000 万个参数和 650 万个神经元,这使得它具有更大的学习容量来理解更复杂的功能。这允许 AlexNet 在 2012 年的 ILSVRC 图像分类竞赛中取得显著的成绩。
ImageNet 和 ILSVRC
ImageNet (image-net.org/index)是一个大型视觉数据库,旨在用于视觉物体识别软件研究。它旨在根据一组定义的单词和短语将图像标记和分类到近 22,000 个类别。这些图像来自网络,并由人类使用亚马逊的 Mechanical Turk 众包工具进行标记。在撰写本文时,ImageNet 项目中已有超过 1400 万张图像。为了组织如此大量的数据,ImageNet 的创建者遵循了 WordNet 层次结构,其中 WordNet 中的每个有意义的单词/短语被称为同义词集(简称 synset)。在 ImageNet 项目中,图像根据这些 synset 组织,目标是每个 synset 有 1,000+张图像。
ImageNet 项目每年举办一次名为 ImageNet 大规模视觉识别挑战(ILSVRC,www.image-net.org/challenges/LSVRC)的软件竞赛,软件程序在此竞赛中竞争正确分类和检测对象和场景。我们将使用 ILSVRC 挑战作为基准来比较不同网络的性能。
5.3.1 AlexNet 架构
您在第三章末的项目中看到了 AlexNet 架构的一个版本。该架构相当直接。它包括:
-
具有以下核大小的卷积层:11 × 11, 5 × 5, 和 3 × 3
-
图像下采样使用的最大池化层
-
使用 Dropout 层来避免过拟合
-
与 LeNet 不同,隐藏层使用 ReLU 激活函数,输出层使用 softmax 激活函数
AlexNet 由五个卷积层组成,其中一些后面跟着最大池化层,以及三个全连接层,最终有 1000 个类别的 softmax。该架构可以用以下文本表示:
输入图像 ⇒ CONV1 ⇒ POOL2 ⇒ CONV3 ⇒ POOL4 ⇒ CONV5 ⇒ CONV6 ⇒ CONV7 ⇒ POOL8 ⇒ FC9 ⇒ FC10 ⇒ SOFTMAX7
5.3.2 AlexNet 的新特性
在 AlexNet 之前,深度学习(DL)开始在语音识别和其他一些领域开始受到关注。但 AlexNet 是里程碑式的,它说服了 CV 社区中的许多人认真看待深度学习,并证明它确实在 CV 中有效。AlexNet 提出了一些在以前的 CNN(如 LeNet)中没有使用的新特性。您已经从前面的章节中熟悉了它们,所以我们将在这里快速浏览它们。
ReLU 激活函数
AlexNet 在非线性部分使用 ReLU 而不是 tanh 和 sigmoid 函数,这些函数是传统神经网络(如 LeNet)早期标准。ReLU 在 AlexNet 架构的隐藏层中使用,因为它训练得更快。这是因为 sigmoid 函数的导数在饱和区域变得非常小,因此应用于权重的更新几乎消失。这种现象称为梯度消失问题。ReLU 由以下方程表示:
f (x) = max(0,x)
在第二章中对此进行了详细讨论。
消失梯度问题
某些激活函数,如 sigmoid 函数,将大的输入空间压缩到 0 到 1(对于 tanh 激活为-1 到 1)之间的小输入空间。因此,sigmoid 函数输入的大变化导致输出的小变化。结果,导数变得非常小:

消失梯度问题:sigmoid 函数输入的大变化导致输出变化微乎其微。
当我们查看 ResNet 架构时,在本章的后面我们将更详细地讨论消失梯度现象。
Dropout 层
如第三章所述,dropout 层被用来防止神经网络过拟合。被“丢弃”的神经元不会对前向传递做出贡献,也不参与反向传播。这意味着每次输入时,神经网络都会采样一个不同的架构,但所有这些架构都共享相同的权重。这种技术减少了神经元之间的复杂共适应,因为一个神经元不能依赖于特定其他神经元的出现。因此,神经元被迫学习更多在与其他许多随机子集的神经元结合时有用的鲁棒特征。Krizhevsky 等人在这两个全连接层中使用了概率为 0.5 的 dropout。
数据增强
避免过拟合的一种流行且非常有效的方法是通过使用标签保持变换来人工扩大数据集。这通过使用图像旋转、翻转、缩放等变换生成训练图像的新实例来实现。数据增强在第四章中有详细解释。
局部响应归一化
AlexNet 使用了局部响应归一化。这与第四章中解释的批量归一化技术不同。归一化有助于加快收敛速度。如今,批量归一化已取代局部响应归一化;在本章的实现中,我们将使用 BN。
权重正则化
Krizhevsky 等人使用了 0.0005 的权重衰减。权重衰减是第四章中解释的 L2 正则化技术的另一个术语。这种方法减少了深度学习神经网络模型在训练数据上的过拟合,从而使网络在新的数据上更好地泛化:
model.add(Conv2D(32, (3,3), kernel_regularizer=l2(ƛ)))
Lambda (λ) 值是一个权重衰减超参数,你可以调整它。如果你仍然看到过拟合,你可以通过增加λ值来减少它。在这种情况下,Krizhevsky 和他的团队发现,一个小的衰减值 0.0005 对于模型学习来说已经足够好了。
在多个 GPU 上训练
Krizhevsky 等人使用了一个配备 3 GB 内存的 GTX 580 GPU。在当时,它是最先进的,但不足以训练数据集中的 1.2 百万个训练示例。因此,该团队开发了一种复杂的方法,将网络分散到两个 GPU 上。基本思想是许多层被分割到两个不同的 GPU 上,这些 GPU 之间相互通信。你今天不需要担心这些细节:在本书后面的部分,我们将讨论在分布式 GPU 上训练深度网络的更先进的方法。
5.3.3 Keras 中的 AlexNet 实现
现在你已经了解了 AlexNet 及其新颖特性的基本组成部分,让我们应用它们来构建 AlexNet 神经网络。我建议你阅读原始论文的第 4 页上的架构描述并跟随。
如图 5.7 所示,该网络包含八个权重层:前五个是卷积层,剩下的三个是全连接层。最后一个全连接层的输出被送入一个 1000 路 softmax,产生对 1000 个类别标签的分布。
备注:AlexNet 输入从 227 × 227 × 3 图像开始。如果你阅读了论文,你会注意到它提到了输入图像的维度体积为 224 × 224 × 3。但数字只对 227 × 227 × 3 图像(图 5.7)有意义。我建议这可能是在论文中的打字错误。

图 5.7 AlexNet 包含八个权重层:五个卷积层和三个全连接层。其中两个包含 4,096 个神经元,输出被送入一个 1,000 个神经元的 softmax。
层叠方式如下:
-
CONV1--作者使用了一个大的内核大小(11)。他们还使用了一个大的步长(4),这使得输入维度大约缩小了 4 倍(从 227 × 227 到 55 × 55)。我们如下计算输出维度:
(227 - 11)/4 + 1 = 55
深度是卷积层中的滤波器数量(96)。输出维度是 55 × 55 × 96。
-
POOL 尺寸为 3 × 3--这降低了维度从 55 × 55 到 27 × 27:
(55 - 3)/2 + 1 = 27
池化层不会改变体积的深度。输出维度是 27 × 27 × 96。
同样,我们可以计算剩余层的输出维度:
-
CONV2--内核大小= 5,深度= 256,步长= 1
-
POOL--大小= 3 × 3,它将输入维度从 27 × 27 下采样到 13 × 13
-
CONV3--内核大小= 3,深度= 384,步长= 1
-
CONV4--内核大小= 3,深度= 384,步长= 1
-
CONV5--内核大小= 3,深度= 256,步长= 1
-
POOL--大小= 3 × 3,它将输入从 13 × 13 下采样到 6 × 6
-
Flatten 层--将维度体积 6 × 6 × 256 展平为 1 × 9,216
-
FC 层包含 4,096 个神经元
-
FC 层包含 4,096 个神经元
-
Softmax 层包含 1,000 个神经元
注意:你可能想知道 Krizhevsky 和他的团队是如何决定实施这种配置的。设置网络超参数的正确值,如核大小、深度、步长、池化大小等,是繁琐的,需要大量的试错。想法保持不变:我们希望应用许多权重层来增加模型学习更复杂函数的能力。我们还需要在之间添加池化层以降采样输入维度,正如第二章所讨论的。因此,设置确切的超参数是 CNN 的一个挑战。VGGNet(下文将解释)通过实现统一的层配置来解决设计网络时试错量的问题。
注意,所有卷积层之后都跟着一个批量归一化层,所有隐藏层之后都跟着 ReLU 激活。现在,让我们将其放入代码中,以构建 AlexNet 架构:
from keras.models import Sequential ❶
from keras.regularizers import l2 ❶
from keras.layers import Conv2D, AveragePooling2D, Flatten, Dense, ❶
Activation,MaxPool2D, BatchNormalization, Dropout ❶
model = Sequential() ❷
# 1st layer (CONV + pool + batchnorm)
model.add(Conv2D(filters= 96, kernel_size= (11,11), strides=(4,4), padding='valid',
input_shape = (227,227,3)))
model.add(Activation('relu')) ❸
model.add(MaxPool2D(pool_size=(3,3), strides=(2,2)))
model.add(BatchNormalization())
# 2nd layer (CONV + pool + batchnorm)
model.add(Conv2D(filters=256, kernel_size=(5,5), strides=(1,1), padding='same',
kernel_regularizer=l2(0.0005)))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(3,3), strides=(2,2), padding='valid'))
model.add(BatchNormalization())
# layer 3 (CONV + batchnorm) ❹
model.add(Conv2D(filters=384, kernel_size=(3,3), strides=(1,1), padding='same', kernel_regularizer=l2(0.0005)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# layer 4 (CONV + batchnorm) ❺
model.add(Conv2D(filters=384, kernel_size=(3,3), strides=(1,1), padding='same',
kernel_regularizer=l2(0.0005)))
model.add(Activation('relu'))
model.add(BatchNormalization())
# layer 5 (CONV + batchnorm)
model.add(Conv2D(filters=256, kernel_size=(3,3), strides=(1,1), padding='same',
kernel_regularizer=l2(0.0005)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(3,3), strides=(2,2), padding='valid'))
model.add(Flatten()) ❻
# layer 6 (Dense layer + dropout)
model.add(Dense(units = 4096, activation = 'relu'))
model.add(Dropout(0.5))
# layer 7 (Dense layers)
model.add(Dense(units = 4096, activation = 'relu'))
model.add(Dropout(0.5))
# layer 8 (softmax output layer)
model.add(Dense(units = 1000, activation = 'softmax'))
model.summary() ❼
❶ 导入 Keras 模型、层和正则化器
❷ 实例化一个空的序列模型
❸ 激活函数可以添加到自己的层中,或者像我们在之前的实现中那样在 Conv2D 函数内添加。
❹ 注意,AlexNet 的作者在这里没有添加池化层。
❺ 与第 3 层类似
❻ 将 CNN 输出展平以供全连接层使用
❼ 打印模型摘要
当你打印模型摘要时,你会看到总参数数是 6200 万:
____________________________________________
Total params: 62,383, 848
Trainable params: 62,381, 096
Non-trainable params: 2,752
注意:LeNet 和 AlexNet 都有许多超参数需要调整。那些网络的作者不得不进行许多实验来设置每层的核大小、步长和填充,这使得网络更难以理解和管理。VGGNet(下文将解释)通过一个非常简单、统一的架构解决了这个问题。
5.3.4 设置学习超参数
AlexNet 训练了 90 个 epoch,在两个 Nvidia Geforce GTX 580 GPU 上同时进行,耗时 6 天。这就是为什么你会在原始论文中看到网络被分成两个管道的原因。Krizhevsky 等人以 0.01 的初始学习率和 0.9 的动量开始。当验证误差停止改进时,lr被除以 10:
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=np.sqrt(0.1)) ❶
optimizer = keras.optimizers.sgd(lr = 0.01, momentum = 0.9) ❷
model.compile(loss='categorical_crossentropy', optimizer=optimizer,
metrics=['accuracy']) ❸
model.fit(X_train, y_train, batch_size=128, epochs=90,
validation_data=(X_test, y_test), verbose=2, callbacks=[reduce_lr]) ❹
❶ 当验证误差停滞时,将学习率降低 0.1
❷ 设置 SGD 优化器,学习率为 0.01,动量为 0.9
❸ 编译模型
❹ 在训练方法中使用回调函数训练模型并调用 reduce_lr 值
5.3.5 AlexNet 性能
AlexNet 在 2012 年 ILSVRC 挑战中显著优于所有之前的竞争对手。它实现了 15.3%的获胜 top-5 测试错误率,而当年第二好的参赛者使用了其他传统分类器,其错误率为 26.2%。这种巨大的性能提升吸引了 CV 社区对卷积网络解决复杂视觉问题潜力的关注,并导致了更先进的 CNN 架构,你将在本章的后续部分看到。
Top-1 和 top-5 错误率是多少?
Top-1 和 top-5 是主要在研究论文中使用的术语,用来描述算法在特定分类任务上的准确性。Top-1 错误率是指分类器没有给出正确类别最高分数的百分比,而 top-5 错误率是指分类器在其前五次猜测中没有包含正确类别的百分比。
让我们用一个例子来应用这个概念。假设有 100 个类别,我们向网络展示一张猫的图片。分类器为每个类别输出一个分数或置信度值,如下所示:
-
猫:70%
-
狗:20%
-
马:5%
-
摩托车:4%
-
汽车:0.6%
-
飞机:0.4%
这意味着分类器能够在 top-1 中正确预测图像的真实类别。尝试对 100 张图像进行相同的实验,并观察分类器错过真实标签的次数,这就是你的 top-1 错误率。
同样的概念也适用于 top-5 错误率。在例子中,如果真实标签是马,那么分类器在 top-1 中错过了真实标签,但在前五个预测类别中捕捉到了它(例如,top-5)。计算分类器在 top 五预测中错过真实标签的次数,这就是你的 top-5。
理想情况下,我们希望模型始终在 top-1 中预测正确的类别。但 top-5 通过定义模型对错过类别的正确预测有多接近,提供了对模型性能的更全面评估。
5.4 VGGNet
VGGNet 于 2014 年由牛津大学视觉几何组开发(因此得名 VGG)。3 其构建组件与 LeNet 和 AlexNet 中的完全相同,只是 VGGNet 是一个更深层的网络,具有更多的卷积、池化和密集层。除此之外,这里没有引入新的组件。
VGGNet,也称为 VGG16,由 16 个权重层组成:13 个卷积层和 3 个全连接层。其均匀架构使其在深度学习社区中受到欢迎,因为它非常容易理解。
5.4.1 VGGNet 的新特性
我们已经看到,设置 CNN 超参数(如核大小、填充、步长等)可能具有挑战性。VGGNet 的创新概念是它有一个简单的架构,包含均匀的组件(卷积和池化层)。它通过用多个 3 × 3 池大小滤波器依次替换 AlexNet 中的大核大小滤波器(第一和第二卷积层分别为 11 和 5)来改进 AlexNet。
架构由一系列均匀的卷积构建块组成,随后是一个统一的池化层,其中:
-
所有卷积层都使用 3 × 3 核大小的滤波器,
strides值为1,padding值为same。 -
所有池化层都有 2 × 2 的池大小和
strides值为2。
Simonyan 和 Zisserman 决定使用一个更小的 3 × 3 核,以便网络能够提取比 AlexNet 的大核(11 × 11 和 5 × 5)更细粒度的图像特征。其理念是,在给定的卷积感受野中,多个堆叠的小核比一个大核更好,因为多个非线性层增加了网络的深度;这使得它能够在较低的成本下学习更复杂的特征,因为它具有更少的学习参数。
例如,在他们的实验中,作者注意到两个 3 × 3 卷积层的堆叠(中间没有空间池化)具有 5 × 5 的有效感受野,而三个 3 × 3 卷积层的效果相当于 7 × 7 的感受野。因此,通过使用具有更高深度的 3 × 3 卷积,你可以获得使用更多非线性整流层(ReLU)的好处,这使得决策函数更具判别性。其次,这减少了训练参数的数量,因为当你使用具有 C 通道的三层 3 × 3 卷积时,堆叠由 32C2 = 27C2 权重参数化,而单个 7 × 7 卷积层需要 72C2 = 49C2 权重,这比 81%更多的参数。
感受野
如第三章所述,感受野是输出所依赖的有效输入图像区域:

这种卷积和池化组件的统一配置简化了神经网络架构,这使得它非常容易理解和实现。
VGGNet 架构是通过堆叠 3 × 3 卷积层并在几个卷积层之后插入 2 × 2 池化层来开发的。这之后是传统的分类器,它由全连接层和 softmax 组成,如图 5.8 所示。

图 5.8 VGGNet-16 架构
5.4.2 VGGNet 配置
Simonyan 和 Zisserman 为 VGGNet 架构创建了几个配置,如图 5.9 所示。所有配置都遵循相同的设计。配置 D 和 E 是最常用的,被称为 VGG16 和 VGG19,指的是权重层的数量。每个块包含一系列具有类似超参数配置的 3 × 3 卷积层,之后跟一个 2 × 2 池化层。

图 5.9 VGGNet 架构配置
表 5.1 列出了每个配置的学习参数数量(以百万为单位)。VGG16 产生约 138 百万个参数;VGG19,这是 VGGNet 的更深层版本,有超过 144 百万个参数。VGG16 更常用,因为它几乎与 VGG19 的表现一样好,但参数更少。
表 5.1 VGGNet 架构参数(以百万为单位)
| 网络 | A, A-LRN | B | C | D | E |
|---|---|---|---|---|---|
| 参数数量 | 133 | 133 | 134 | 138 | 144 |
Keras 中的 VGG16
配置 D(VGG16)和 E(VGG19)是最常用的配置,因为它们是更深层的网络,可以学习更复杂的函数。因此,在本章中,我们将实现配置 D,它有 16 个权重层。VGG19(配置 E)可以通过在第三、第四和第五块中添加一个第四卷积层来实现,如图 5.9 所示。本章下载的代码包括了 VGG16 和 VGG19 的完整实现。
注意,Simonyan 和 Zisserman 使用了以下正则化技术来避免过拟合:
-
L2 正则化,权重衰减为 5 × 10^-4。为了简化,这个没有添加到下面的实现中。
-
对前两个全连接层使用 Dropout 正则化,Dropout 比率为 0.5。
Keras 代码如下:
model = Sequential() ❶
# block #1
model.add(Conv2D(filters=64, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same', input_shape=(224,224, 3)))
model.add(Conv2D(filters=64, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(MaxPool2D((2,2), strides=(2,2)))
# block #2
model.add(Conv2D(filters=128, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=128, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(MaxPool2D((2,2), strides=(2,2)))
# block #3
model.add(Conv2D(filters=256, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=256, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=256, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(MaxPool2D((2,2), strides=(2,2)))
# block #4
model.add(Conv2D(filters=512, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=512, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=512, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(MaxPool2D((2,2), strides=(2,2)))
# block #5
model.add(Conv2D(filters=512, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=512, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(Conv2D(filters=512, kernel_size=(3,3), strides=(1,1), activation='relu',
padding='same'))
model.add(MaxPool2D((2,2), strides=(2,2)))
# block #6 (classifier)
model.add(Flatten())
model.add(Dense(4096, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(4096, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1000, activation='softmax'))
model.summary() ❷
❶ 实例化一个空的序列模型
❷ 打印模型摘要
当你打印模型摘要时,你会看到总参数数量约为 138 百万:
____________________________________________
Total params: 138,357, 544
Trainable params: 138,357, 544
Non-trainable params: 0
5.4.3 学习超参数
Simonyan 和 Zisserman 遵循了与 AlexNet 类似的训练程序:使用带有动量 0.9 的 mini-batch 梯度下降进行训练。初始学习率设置为 0.01,当验证集准确率停止提高时,学习率减少 10 倍。
5.4.4 VGGNet 性能
VGG16 在 ImageNet 数据集上实现了 8.1%的 top-5 错误率,而 AlexNet 实现了 15.3%。VGG19 表现得更好:它能够实现大约 7.4%的 top-5 错误率。值得注意的是,尽管与 AlexNet 相比,VGGNet 的参数数量更多,深度更大,但由于深度更大和卷积滤波器尺寸更小,VGGNet 需要更少的 epoch 来收敛。
5.5 Inception 和 GoogLeNet
Inception 网络在 2014 年问世,当时谷歌的一组研究人员发表了他们的论文,“通过卷积加深网络。”4 这种架构的主要特点是构建一个更深的神经网络,同时提高网络内部计算资源的利用率。Inception 网络的一个特定实现被称为 GoogLeNet,并被用于团队在 2014 年 ILSVRC 的提交中。它使用了一个 22 层的网络(比 VGGNet 更深),通过将参数数量减少 12 倍(从约 138 百万减少到约 1300 万)并实现了显著更准确的结果。该网络使用了一个受经典网络(AlexNet 和 VGGNet)启发的 CNN,但实现了一个被称为 inception 模块的新元素。
5.5.1 Inception 的新特性
Szegedy 等人设计网络架构时采取了不同的方法。正如我们在前面的网络中看到的,在设计网络时,你需要为每一层做出一些架构决策,例如这些:
-
卷积层的核大小——我们在之前的架构中看到,核大小是变化的:1 × 1、3 × 3、5 × 5,在某些情况下,11 × 11(如 AlexNet)。在设计卷积层时,我们发现自己试图挑选和调整适合我们数据集的每一层的核大小。回想第三章,较小的核可以捕捉图像的更细微的细节,而较大的滤波器会忽略这些细节。
-
何时使用池化层——AlexNet 在每个或每两个卷积层后使用池化层来缩小空间特征。VGGNet 在网络更深时,在每个两个、三个或四个卷积层后应用池化。
配置核大小和定位池化层是我们主要通过试错和实验来做出决定,以获得最佳结果。Inception 说:“与其在卷积层中选择一个期望的滤波器大小并决定池化层的位置,不如将它们全部应用在一个块中,并称之为 Inception 模块。”
也就是说,与经典架构中层层堆叠的方式不同,Szegedy 和他的团队建议我们创建一个由多个不同核大小的卷积层组成的 Inception 模块。然后,通过堆叠 Inception 模块来发展架构。图 5.10 展示了经典卷积网络与 Inception 网络的架构对比。

图 5.10 经典卷积网络与 Inception 网络对比
从图中,你可以观察到以下内容:
-
在 LeNet、AlexNet 和 VGGNet 等经典架构中,我们堆叠卷积层和池化层来构建特征提取器。最后,我们添加密集的全连接层来构建分类器。
-
在 Inception 架构中,我们从一个卷积层和一个池化层开始,堆叠 Inception 模块和池化层来构建特征提取器,然后添加常规的密集分类层。
我们一直将 Inception 模块视为黑盒,以理解 Inception 架构的全貌。现在,我们将拆解 Inception 模块,以了解其工作原理。
5.5.2 Inception 模块:朴素版本
Inception 模块由四个层组成:
-
1 × 1 卷积层
-
3 × 3 卷积层
-
5 × 5 卷积层
-
3 × 3 最大池化层
这些层的输出被连接成一个单一的输出体积,形成下一阶段的输入。朴素版本的 Inception 模块在图 5.11 中展示。
图表可能看起来有点令人眼花缭乱,但理念简单易懂。让我们通过以下示例来理解:
-
假设我们有一个来自前一层的输入维度体积,大小为 32 × 32 × 200。
-
我们同时将这个输入馈送到四个卷积中:
-
depth=64和padding=same的 1 × 1 卷积层。这个核的输出 = 32 × 32 × 64。 -
depth=128和padding=same的 3 × 3 卷积层。输出 = 32 × 32 × 128。 -
depth=32和padding=same的 5 × 5 卷积层。输出 = 32 × 32 × 32。 -
padding=same和strides=1的 3 × 3 最大池化层。输出 = 32 × 32 × 32。
-
-
我们将四个输出的深度连接起来,创建一个维度为 32 × 32 × 256 的一维输出体积。

图 5.11 Naive representation of an inception module
现在我们有一个 inception 模块,它接受一个 32 × 32 × 200 的输入体积,并输出一个 32 × 32 × 256 的体积。
注意:在前一个例子中,我们使用了一个 padding 值为 same。在 Keras 中,padding 可以设置为 same 或 valid,正如我们在第三章中看到的。same 值会导致填充输入,使得输出长度与原始输入相同。我们这样做是因为我们希望输出具有与输入相似的宽度和高度维度。我们希望在 inception 模块中输出相似的维度以简化深度连接过程。现在我们只需将所有输出的深度相加,将它们连接成一个输出体积,然后将其送入我们网络中的下一层。
5.5.3 具有维度缩减的 Inception 模块
我们刚才看到的 inception 模块的朴素表示法在处理像 5 × 5 卷积层这样的大尺寸滤波器时存在一个大的计算成本问题。为了更好地理解朴素表示法中的计算问题,让我们计算一下前一个例子中 5 × 5 卷积层将要执行的操作数量。
维度为 32 × 32 × 200 的输入体积将被送入 32 个滤波器的 5 × 5 卷积层,滤波器维度 = 5 × 5 × 32。这意味着计算机需要计算的总乘法次数是 32 × 32 × 200 乘以 5 × 5 × 32,这超过了 1.63 亿次操作。虽然我们可以用现代计算机执行这么多操作,但这仍然相当昂贵。这就是维度缩减层可以非常有用的时候。
维度缩减层(1 × 1 卷积层)
1 × 1 卷积层可以将 1.63 亿次操作的操作成本降低到十分之一左右。这就是为什么它被称为缩减层。这里的想法是在 3 × 3 和 5 × 5 卷积层等更大的核之前添加一个 1 × 1 卷积层,以减少它们的深度,从而减少操作的数量。
让我们来看一个例子。假设我们有一个输入维度体积为 32 × 32 × 200。然后我们添加一个深度为 16 的 1 × 1 卷积层。这将从 200 个通道减少维度体积到 16 个通道。然后我们可以在输出上应用 5 × 5 卷积层,其深度要小得多(图 5.12)。

图 5.12 使用降维通过减少层的深度来降低计算成本。
注意,32 × 32 × 200 的输入通过两个卷积层处理后,输出一个维度为 32 × 32 × 32 的体积,这与未应用降维层时产生的体积相同。但在这里,我们不是在整个输入体积的 200 个通道上处理 5 × 5 卷积层,而是将这个巨大的体积缩小到只有 16 个通道的更小的中间体积。
现在,让我们看看这个操作涉及的计算成本,并将其与我们之前应用降维层时得到的 1.63 亿次乘法进行比较:
计算量
= 1 × 1 卷积层中的操作 + 5 × 5 卷积层中的操作
= (32 × 32 × 200) 乘以 (1 × 1 × 16 + 32 × 32 × 16) 乘以 (5 × 5 × 32)
= 320 万 + 1310 万
这个操作中的总乘法次数为 1630 万次,是未使用降维层时计算的 1.63 亿次的十分之一。
1 × 1 卷积层
1 × 1 卷积层的思想是它保留了输入体积的空间维度(高度和宽度),但改变了体积的通道数(深度):

1 × 1 卷积层保留了空间维度,但改变了深度。
1 × 1 卷积层也被称为瓶颈层,因为瓶颈是瓶子中最小的一部分,降维层减少了网络的维度,使其看起来像瓶颈:

1 × 1 卷积层被称为瓶颈层。
降维对网络性能的影响
你可能会想知道,如此大幅度地缩小表示大小是否会损害神经网络的性能。Szegedy 等人进行了实验,发现只要适度地实现降维层,就可以显著缩小表示大小而不会损害性能——并且节省大量计算。
现在,让我们将降维层付诸实践,并构建一个新的具有降维功能的 inception 模块。为此,我们将保持从原始表示中连接四个层的相同概念。我们将在 3 × 3 和 5 × 5 卷积层之前添加一个 1 × 1 卷积降维层,以降低它们的计算成本。我们还将添加一个 1 × 1 卷积层在 3 × 3 最大池化层之后,因为池化层不会减少其输入的深度。因此,在我们进行连接之前,我们需要将降维层应用于它们的输出(图 5.13)。

图 5.13 使用降维构建 inception 模块
我们在更大的卷积层之前添加了降维,以便在后续阶段计算复杂度不受控制地激增之前,显著增加每个阶段的单元数量。此外,设计遵循了实用的直觉,即视觉信息应该在各种尺度上处理,然后汇总,以便下一阶段可以同时从不同尺度抽象特征。
Inception 模块的回顾
总结来说,如果您正在构建神经网络的一层,并且不想决定在卷积层中使用什么滤波器大小或何时添加池化层,Inception 模块允许您使用所有这些,并将所有输出的深度连接起来。这被称为 Inception 模块的朴素表示。
然后,我们遇到了使用大滤波器带来的计算成本问题。在这里,我们使用了一个称为 reduce 层的 1 × 1 卷积层,它显著降低了计算成本。我们在 3 × 3 和 5 × 5 卷积层之前以及最大池化层之后添加 reduce 层,以创建一个具有降维的 Inception 模块。
5.5.4 Inception 架构
现在我们已经了解了 Inception 模块的组成部分,我们准备构建 Inception 网络架构。我们使用 Inception 模块的降维表示,将 Inception 模块堆叠在一起,并在它们之间添加一个 3 × 3 池化层以进行下采样,如图 5.14 所示。

图 5.14 我们通过将 Inception 模块堆叠在一起来构建 Inception 网络。
我们可以堆叠任意数量的 Inception 模块来构建一个非常深的卷积网络。在原始论文中,该团队构建了一个特定的 Inception 模块实例,并将其称为 GoogLeNet。他们在 2014 年 ILSVRC 竞赛的提交中使用了这个网络。GoogLeNet 架构如图 5.15 所示。

图 5.15 完整的 GoogLeNet 模型由三部分组成:第一部分具有类似于 AlexNet 和 LeNet 的经典 CNN 架构,第二部分是一堆 Inception 模块和池化层,第三部分是传统的全连接分类器。
如您所见,GoogLeNet 使用了一堆总共九个 Inception 模块,并在每隔几个块中使用最大池化层来降低维度。为了简化这个实现,我们将 GoogLeNet 架构分解为三个部分:
-
A 部分--与 AlexNet 和 LeNet 架构相同;包含一系列卷积和池化层。
-
B 部分 --包含九个 Inception 模块堆叠如下:两个 Inception 模块 + 池化层 + 五个 Inception 模块 + 池化层 + 五个 Inception 模块。
-
C 部分 --网络的分类器部分,包括全连接和 softmax 层。
5.5.5 Keras 中的 GoogLeNet
现在,让我们在 Keras 中实现 GoogLeNet 架构(图 5.16)。注意,inception 模块将前一个模块的特征作为输入,通过四个路径传递,将所有四个路径的输出深度连接起来,然后将连接后的输出传递到下一个模块。这四个路径如下:
-
1 × 1 卷积层
-
1 × 1 卷积层 + 3 × 3 卷积层
-
1 × 1 卷积层 + 5 × 5 卷积层
-
3 × 3 池化层 + 1 × 1 卷积层

图 5.16 GoogLeNet 的 inception 模块
首先,我们将构建inception_module函数。它接受每个卷积层的滤波器数量作为参数,并返回连接后的输出:
def inception_module(x, filters_1 × 1, filters_3x3_reduce, filters_3x3, filters_5x5_reduce,
filters_5x5, filters_pool_proj, name=None):
conv_1x1 = Conv2D(filters_1x1, kernel_size=(1, 1), padding='same', activation='relu',
kernel_initializer=kernel_init, bias_initializer=bias_init)(*x*) ❶
# 3 × 3 route = 1 × 1 CONV + 3 × 3 CONV
pre_conv_3x3 = Conv2D(filters_3x3_reduce, kernel_size=(1, 1), padding='same',
activation='relu', kernel_initializer=kernel_init,
bias_initializer=bias_init)(*x*)
conv_3x3 = Conv2D(filters_3x3, kernel_size=(3, 3), padding='same', activation='relu',
kernel_initializer=kernel_init,
bias_initializer=bias_init)(pre_conv_3x3)
# 5 × 5 route = 1 × 1 CONV + 5 × 5 CONV
pre_conv_5x5 = Conv2D(filters_5x5_reduce, kernel_size=(1, 1), padding='same',
activation='relu', kernel_initializer=kernel_init,
bias_initializer=bias_init)(*x*)
conv_5x5 = Conv2D(filters_5x5, kernel_size=(5, 5), padding='same', activation='relu',
kernel_initializer=kernel_init,
bias_initializer=bias_init)(pre_conv_5x5)
# pool route = POOL + 1 × 1 CONV
pool_proj = MaxPool2D((3, 3), strides=(1, 1), padding='same')(*x*)
pool_proj = Conv2D(filters_pool_proj, (1, 1), padding='same', activation='relu',
kernel_initializer=kernel_init, bias_initializer=bias_init)(pool_proj)
output = concatenate([conv_1x1, conv_3x3, conv_5x5, pool_proj], axis=3, name=name) ❷
return output
❶ 创建一个 1 × 1 卷积层,其输入直接来自前一个层
❷ 将三个滤波器的深度连接在一起
GoogLeNet 架构
现在inception_module函数已经准备好了,让我们根据图 5.16 构建 GoogLeNet 架构。为了获取inception_module函数参数的值,我们将查看图 5.17,它代表了 Szegedy 等人在原始论文中实现的超参数设置。(注意,图中的“#3 × 3 reduce”和“#5 × 5 reduce”代表在 3 × 3 和 5 × 5 卷积层之前使用的 1 × 1 滤波器层。)

图 5.17 Szegedy 等人原 Inception 论文中实现的超参数
现在,让我们逐一查看部分 A、B 和 C 的实现。
Part A: 构建网络的底部部分
让我们构建网络的底部部分。这部分包括一个 7 × 7 卷积层⇒ 3 × 3 池化层⇒ 1 × 1 卷积层⇒ 3 × 3 卷积层⇒ 3 × 3 池化层,如图 5.18 所示。

图 5.18 网络的底部部分
在LocalResponseNorm层中,类似于 AlexNet,使用局部响应归一化来帮助加速收敛。如今,批量归一化被用来代替。
这里是部分 A 的 Keras 代码:
# input layer with size = 24 × 24 × 3
input_layer = Input(shape=(224, 224, 3))
kernel_init = keras.initializers.glorot_uniform()
bias_init = keras.initializers.Constant(value=0.2)
x = Conv2D(64, (7, 7), padding='same', strides=(2, 2), activation='relu', name='conv_1_7x7/2',
kernel_initializer=kernel_init, bias_initializer=bias_init)(input_layer)
x = MaxPool2D((3, 3), padding='same', strides=(2, 2), name='max_pool_1_3x3/2')(*x*)
x = BatchNormalization()(*x*)
x = Conv2D(64, (1, 1), padding='same', strides=(1, 1), activation='relu')(*x*)
x = Conv2D(192, (3, 3), padding='same', strides=(1, 1), activation='relu')(*x*)
x = BatchNormalization()(*x*)
x = MaxPool2D((3, 3), padding='same', strides=(2, 2))(*x*)
Part B: 构建 inception 模块和最大池化层
要构建 inception 模块 3a 和 3b 以及第一个最大池化层,我们使用表 5.2 开始。代码如下:
表 5.2 Inception 模块 3a 和 3b
| 类型 | #1 × 1 | #3 × 3 reduce | #3 × 3 | #5 × 5 reduce | #5 × 5 | Pool proj |
|---|---|---|---|---|---|---|
| Inception (3a) | 064 | 096 | 128 | 16 | 32 | 32 |
| Inception (3b) | 128 | 128 | 192 | 32 | 96 | 64 |
x = inception_module(x, filters_1x1=64, filters_3x3_reduce=96, filters_3x3=128,
filters_5x5_reduce=16, filters_5x5=32, filters_pool_proj=32,
name='inception_3a')
x = inception_module(x, filters_1x1=128, filters_3x3_reduce=128, filters_3x3=192,
filters_5x5_reduce=32, filters_5x5=96, filters_pool_proj=64,
name='inception_3b')
x = MaxPool2D((3, 3), padding='same', strides=(2, 2))(*x*)
类似地,让我们创建 inception 模块 4a、4b、4c、4d 和 4e 以及最大池化层:
x = inception_module(x, filters_1x1=192, filters_3x3_reduce=96, filters_3x3=208,
filters_5x5_reduce=16, filters_5x5=48, filters_pool_proj=64,
name='inception_4a')
x = inception_module(x, filters_1x1=160, filters_3x3_reduce=112, filters_3x3=224,
filters_5x5_reduce=24, filters_5x5=64, filters_pool_proj=64,
name='inception_4b')
x = inception_module(x, filters_1x1=128, filters_3x3_reduce=128, filters_3x3=256,
filters_5x5_reduce=24, filters_5x5=64, filters_pool_proj=64,
name='inception_4c')
x = inception_module(x, filters_1x1=112, filters_3x3_reduce=144, filters_3x3=288,
filters_5x5_reduce=32, filters_5x5=64, filters_pool_proj=64,
name='inception_4d')
x = inception_module(x, filters_1x1=256, filters_3x3_reduce=160, filters_3x3=320,
filters_5x5_reduce=32, filters_5x5=128, filters_pool_proj=128,
name='inception_4e')
x = MaxPool2D((3, 3), padding='same', strides=(2, 2), name='max_pool_4_3x3/2')(*x*)
现在,让我们创建模块 5a 和 5b:
x = inception_module(x, filters_1x1=256, filters_3x3_reduce=160, filters_3x3=320,
filters_5x5_reduce=32, filters_5x5=128, filters_pool_proj=128,
name='inception_5a')
x = inception_module(x, filters_1x1=384, filters_3x3_reduce=192, filters_3x3=384,
filters_5x5_reduce=48, filters_5x5=128, filters_pool_proj=128,
name='inception_5b')
Part C: 构建分类器部分
在他们的实验中,Szegedy 等人发现添加一个 7 × 7 平均池化层将 top-1 准确率提高了约 0.6%。然后他们添加了一个 40%概率的 dropout 层来减少过拟合:
x = AveragePooling2D(pool_size=(7,7), strides=1, padding='valid')(*x*)
x = Dropout(0.4)(*x*)
x = Dense(10, activation='softmax', name='output')(*x*)
5.5.6 学习超参数
该团队使用了一个动量为 0.9 的 SGD 梯度下降优化器。他们还实施了一个每 8 个周期固定学习率衰减计划,衰减率为 4%。以下是如何实现与论文中类似的训练规范的示例:
epochs = 25
initial_lrate = 0.01
def decay(epoch, steps=100): ❶
initial_lrate = 0.01 ❶
drop = 0.96 ❶
epochs_drop = 8 ❶
lrate = initial_lrate * math.pow(drop, math.floor((1+epoch)/epochs_drop)) ❶
return lrate ❶
lr_schedule = LearningRateScheduler(decay, verbose=1)
sgd = SGD(lr=initial_lrate, momentum=0.9, nesterov=False)
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=256, epochs=epochs, validation_data=(X_test, y_test), callbacks=[lr_schedule], verbose=2, shuffle=True)
❶ 实现学习率衰减函数
5.5.7 Inception 在 CIFAR 数据集上的性能
GoogLeNet 在 2014 年的 ILSVRC 竞赛中获胜。它实现了 6.67% 的顶级错误率,这非常接近人类水平的表现,并且比之前的 CNN,如 AlexNet 和 VGGNet,要好得多。
5.6 ResNet
Residual Neural Network (ResNet) 是由微软研究团队在 2015 年开发的。5 他们引入了一种具有跳过连接的新颖残差模块架构。该网络还针对隐藏层进行了大量的批量归一化。这项技术使得团队能够训练具有 50、101 和 152 个权重层的非常深的神经网络,同时其复杂度仍低于 VGGNet(19 层)等较小的网络。ResNet 在 2015 年的 ILSVRC 竞赛中实现了 3.57% 的顶级错误率,超过了所有先前卷积神经网络的表现。
5.6.1 ResNet 的新特性
观察神经网络架构从 LeNet、AlexNet、VGGNet 和 Inception 的发展,你可能会注意到,网络越深,其学习容量越大,并且从图像中提取特征的能力越强。这主要是因为非常深的网络能够表示非常复杂的函数,这使得网络能够在许多不同层次的抽象中学习特征,从边缘(在底层)到非常复杂的特征(在深层)。
在本章的早期,我们看到了像 VGGNet-19(19 层)和 GoogLeNet(22 层)这样的深度神经网络。它们在 ImageNet 挑战中都表现出色。但我们能否构建更深层的网络呢?我们从第四章中了解到,添加太多层的缺点是这样做会使网络更容易过拟合训练数据。这不是一个主要问题,因为我们可以使用正则化技术,如 dropout、L2 正则化和批量归一化来避免过拟合。因此,如果我们能够解决过拟合问题,我们难道不想构建深度为 50、100 或甚至 150 层的网络吗?答案是肯定的。我们确实应该尝试构建非常深的神经网络。我们只需要解决另一个问题,即解开构建超深层网络的能力:一种称为梯度消失的现象。
梯度消失和梯度爆炸
非常深网络的难题在于,用于改变权重的信号在早期层变得非常小。为了理解原因,让我们考虑第二章中解释的梯度下降过程。当网络反向传播从最终层到第一层的误差梯度时,它在每一步都会乘以权重矩阵;因此梯度可以迅速指数级下降到零,导致梯度消失现象,阻止早期层学习。结果,网络的性能变得饱和,甚至开始迅速退化。
在其他情况下,梯度会迅速指数级增长并“爆炸”到非常大的值。这种现象称为梯度爆炸。
为了解决梯度消失问题,He 等人创建了一个捷径,允许梯度直接反向传播到早期层。这些捷径被称为跳跃连接:它们用于将网络早期层的信息流向后期层,为梯度流动创建一个替代的捷径路径。跳跃连接的另一个重要好处是,它们允许模型学习一个恒等函数,这确保该层至少与前一层的性能一样好(图 5.19)。

图 5.19 无跳跃连接的传统网络(左侧);有跳跃连接的网络(右侧)。
在图 5.19 的左侧是传统的逐层堆叠的卷积层。在右侧,我们仍然像以前一样堆叠卷积层,但同时也将原始输入添加到卷积块的输出中。这是一个跳跃连接。然后我们添加两个信号:跳跃连接 + 主路径。
注意,跳跃箭头指向第二个卷积层的末端——而不是之后。原因是我们在应用该层的 ReLU 激活函数之前添加了两个路径。如图 5.20 所示,x信号沿着捷径路径传递,然后添加到主路径,f(x)。然后,我们对f(x) + x应用 ReLU 激活,以产生输出信号:relu( f(x) + x )。

图 5.20 添加路径并应用 ReLU 激活函数以解决通常伴随非常深网络的梯度消失问题
跳跃连接的代码实现很简单:
X_shortcut = *x* ❶
X = Conv2D(filters = F1, kernel_size = (3, 3), strides = (1,1))(*x*) ❷
X = Activation('relu')(*x*) ❷
X = Conv2D(filters = F1, kernel_size = (3, 3), strides = (1,1))(*x*) ❷
X = Add()([X, X_shortcut]) ❸
X = Activation('relu')(*x*) ❹
❶ 将捷径的值存储为等于输入 x
❷ 执行主路径操作:CONV + ReLU + CONV
❸ 将两个路径合并
❹ 应用 ReLU 激活函数
这种跳跃连接和卷积层的组合被称为残差块。类似于 Inception 网络,ResNet 由一系列这些残差块构建块组成,它们堆叠在一起(图 5.21)。

图 5.21 经典 CNN 架构(左)。Inception 网络由一系列 Inception 模块组成(中间)。残差网络由一系列残差块组成(右)。
从图中,你可以观察到以下内容:
-
特征提取器 --为了构建 ResNet 的特征提取器部分,我们从一个卷积层和一个池化层开始,然后在每个残差块上堆叠以构建网络。当我们设计我们的 ResNet 网络时,我们可以添加尽可能多的残差块来构建甚至更深的网络。
-
分类器 --分类部分与其他网络中我们学习的内容相同:全连接层后跟 softmax。
现在你已经了解了什么是跳跃连接,并且熟悉了 ResNet 的高级架构,让我们来拆解残差块以了解它们是如何工作的。
5.6.2 残差块
一个残差模块由两个分支组成:
-
捷径路径(图 5.22)--将输入连接到第二分支的加法。
-
主路径 --一系列卷积和激活。主路径由三个具有 ReLU 激活的卷积层组成。我们还在每个卷积层中添加批量归一化以减少过拟合并加快训练速度。主路径架构如下:[CONV ⇒ BN ⇒ ReLU] × 3。

图 5.22 主路径的输出通过捷径在输入值被馈送到 ReLU 函数之前被添加。
与我们之前解释的类似,捷径路径在最后一个卷积层的激活函数之前被添加到主路径上。然后我们在添加两条路径后应用 ReLU 函数。
注意,残差块中没有池化层。相反,He 等人决定使用瓶颈 1 × 1 卷积层进行维度下采样,类似于 Inception 网络。因此,每个残差块从 1 × 1 卷积层开始,以下采样输入维度体积,然后是 3 × 3 卷积层和另一个 1 × 1 卷积层以下采样输出。这是一种在多层中保持体积维度的良好技术。这种配置被称为瓶颈残差块。
当我们将残差块堆叠在一起时,体积维度从一个块变化到另一个块。并且你可能还记得第二章中矩阵介绍的内容,为了能够执行矩阵加法操作,矩阵应该具有相似的维度。为了解决这个问题,我们需要在下采样两条路径之前对捷径路径进行下采样。我们通过在捷径路径中添加一个瓶颈层(1 × 1 卷积层 + 批量归一化)来实现这一点,如图 5.23 所示。这被称为减少捷径。

图 5.23 为了减少输入维度,我们在捷径路径中添加了一个瓶颈层(1 × 1 卷积层 + 批量归一化)。这被称为减少捷径。
在我们深入代码实现之前,让我们回顾一下关于残差块的讨论:
-
残差块包含两个路径:捷径路径和主路径。
-
主路径由三个卷积层组成,我们向它们添加批归一化层:
-
1 × 1 卷积层
-
3 × 3 卷积层
-
1 × 1 卷积层
-
-
实现捷径路径有两种方式:
-
常规捷径 --将输入维度添加到主路径。
-
减少捷径 --在合并到主路径之前,在捷径路径中添加一个卷积层。
-
当我们实现 ResNet 网络时,我们将使用常规和缩减捷径。当你看到完整的实现时,这会变得更加清晰。但就目前而言,我们将实现bottleneck_residual_block函数,它接受一个reduce布尔参数。当reduce为True时,这意味着我们想要使用缩减捷径;否则,它将实现常规捷径。bottleneck_residual_block函数接受以下参数:
-
X--形状为(样本数,高度,宽度,通道)的输入张量 -
f--整数,指定主路径中间卷积层窗口的形状 -
filters--定义主路径卷积层中滤波器数量的 Python 整数列表 -
reduce--布尔值:True标识缩减层 -
s--整数(步长)
函数返回X:残差块的输出,它是一个形状为(高度,宽度,通道)的张量。
函数如下:
def bottleneck_residual_block(X, kernel_size, filters, reduce=False, s=2):
F1, F2, F3 = filters ❶
X_shortcut = *x* ❷
if reduce: ❸
X_shortcut = Conv2D(filters = F3, kernel_size = (1, 1), strides = (s,s))(X_shortcut) ❹
X_shortcut = BatchNormalization(axis = 3)(X_shortcut) ❹
*x* = Conv2D(filters = F1, kernel_size = (1, 1), strides = (s,s), padding = 'valid')(*x*) ❺
*x* = BatchNormalization(axis = 3)(*x*)
*x* = Activation('relu')(*x*)
else:
# First component of main path
*x* = Conv2D(filters = F1, kernel_size = (1, 1), strides = (1,1), padding = 'valid')(*x*)
*x* = BatchNormalization(axis = 3)(*x*)
*x* = Activation('relu')(*x*)
# Second component of main path
*x* = Conv2D(filters = F2, kernel_size = kernel_size, strides = (1,1), padding = 'same')(*x*)
*x* = BatchNormalization(axis = 3)(*x*)
*x* = Activation('relu')(*x*)
# Third component of main path
*x* = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1,1), padding = 'valid')(*x*)
*x* = BatchNormalization(axis = 3)(*x*)
# Final step
*x* = Add()([X, X_shortcut]) ❻
*x* = Activation('relu')(*x*) ❻
return X
❶ 解包元组以检索每个卷积层的滤波器
❷ 将输入值保存起来,稍后用于将其添加到主路径
❸ 当 reduce 为 True 时的条件
❹ 为了减少空间尺寸,将一个 1 × 1 卷积层应用于捷径路径。为此,我们需要两个卷积层具有相似的步长。
❺ 如果 reduce,将第一个卷积层的步长设置为与捷径步长相似。
❻ 将捷径值添加到主路径,并通过 ReLU 激活传递
5.6.3 Keras 中的 ResNet 实现
到目前为止,你已经学到了很多关于残差块的知识。让我们将这些块叠加在一起来构建完整的 ResNet 架构。在这里,我们将实现 ResNet50:ResNet 架构的一个版本,包含 50 个权重层(因此得名)。你可以通过遵循原始论文中的图 5.24 的架构来开发具有 18、34、101 和 152 层的 ResNet。

图 5.24 原始论文中几个 ResNet 变体的架构
从上一节我们知道每个残差模块包含 3 × 3 卷积层,现在我们可以计算 ResNet50 网络内部的权重层总数如下:
-
阶段 1:7 × 7 卷积层
-
阶段 2:3 个残差块,每个块包含[1 × 1 卷积层 + 3 × 3 卷积层 + 1 × 1 卷积层] = 9 个卷积层
-
第三阶段:4 个残差块 = 总共 12 个卷积层
-
第四阶段:6 个残差块 = 总共 18 个卷积层
-
第五阶段:3 个残差块 = 总共 9 个卷积层
-
完全连接的 softmax 层
当我们将所有这些层加在一起时,我们得到总共 50 个权重层,这些层描述了 ResNet50 的架构。同样,你也可以计算其他 ResNet 版本中的权重层数量。
注意:在以下实现中,我们使用在每个阶段的开始处具有减少快捷支路的残差块来减少前一层输出的空间尺寸。然后我们使用该阶段的其余层的常规快捷支路。回想一下我们在bottleneck_ residual_block函数中的实现,我们将设置reduce参数为True以应用减少快捷支路。
现在,让我们根据图 5.24 中的 50 层架构来构建 ResNet50 网络。我们构建一个 ResNet50 函数,该函数接受input_shape和classes作为参数,并输出模型:
def ResNet50(input_shape, classes):
X_input = Input(input_shape) ❶
# Stage 1
*x* = Conv2D(64, (7, 7), strides=(2, 2), name='conv1')(X_input)
*x* = BatchNormalization(axis=3, name='bn_conv1')(*x*)
*x* = Activation('relu')(*x*)
*x* = MaxPooling2D((3, 3), strides=(2, 2))(*x*)
# Stage 2
*x* = bottleneck_residual_block(X, 3, [64, 64, 256], reduce=True, s=1)
*x* = bottleneck_residual_block(X, 3, [64, 64, 256])
*x* = bottleneck_residual_block(X, 3, [64, 64, 256])
# Stage 3
*x* = bottleneck_residual_block(X, 3, [128, 128, 512], reduce=True, s=2)
*x* = bottleneck_residual_block(X, 3, [128, 128, 512])
*x* = bottleneck_residual_block(X, 3, [128, 128, 512])
*x* = bottleneck_residual_block(X, 3, [128, 128, 512])
# Stage 4
*x* = bottleneck_residual_block(X, 3, [256, 256, 1024], reduce=True, s=2)
*x* = bottleneck_residual_block(X, 3, [256, 256, 1024])
*x* = bottleneck_residual_block(X, 3, [256, 256, 1024])
*x* = bottleneck_residual_block(X, 3, [256, 256, 1024])
*x* = bottleneck_residual_block(X, 3, [256, 256, 1024])
*x* = bottleneck_residual_block(X, 3, [256, 256, 1024])
# Stage 5
*x* = bottleneck_residual_block(X, 3, [512, 512, 2048], reduce=True, s=2)
*x* = bottleneck_residual_block(X, 3, [512, 512, 2048])
*x* = bottleneck_residual_block(X, 3, [512, 512, 2048])
# AVGPOOL
*x* = AveragePooling2D((1,1))(*x*)
# output layer
*x* = Flatten()(*x*)
*x* = Dense(classes, activation='softmax', name='fc' + str(classes))(*x*)
model = Model(inputs = X_input, outputs = X, name='ResNet50') ❷
return model
❶ 将输入定义为具有 input_shape 形状的张量
❷ 创建模型
5.6.4 学习超参数
He 等人遵循了类似于 AlexNet 的训练程序:使用带有动量 0.9 的 mini-batch GD 进行训练。团队将学习率初始值设为 0.1,当验证误差停止改善时,将其减少 10 倍。他们还使用了 L2 正则化,权重衰减为 0.0001(为了简化,本章未实现)。正如你在前面的实现中看到的,他们在每个卷积操作之后和激活之前使用了批量归一化来加速训练:
from keras.callbacks import ReduceLROnPlateau
epochs = 200 ❶
batch_size = 256 ❶
reduce_lr= ReduceLROnPlateau(monitor='val_loss',factor=np.sqrt(0.1),
patience=5, min_lr=0.5e-6) ❷
model.compile(loss='categorical_crossentropy', optimizer=SGD, metrics=['accuracy']) ❸
model.fit(X_train, Y_train, batch_size=batch_size, validation_data=(X_test, Y_test),
epochs=epochs, callbacks=[reduce_lr]) ❹
❶ 设置训练参数
❷ min_lr 是学习率的下限,factor 是学习率减少的倍数。
❸ 编译模型
❹ 使用训练方法中的回调函数调用 reduce_lr 值来训练模型
5.6.5 ResNet 在 CIFAR 数据集上的性能
与本章中解释的其他网络类似,ResNet 模型的性能是基于他们在 ILSVRC 比赛中的结果进行基准测试的。ResNet-152 在 2015 年分类比赛中以 4.49%的 top-5 错误率获得第一名,使用单个模型时为 3.57%,使用模型集成时为 3.57%。这比其他所有网络都要好,例如 GoogLeNet(Inception),其 top-5 错误率为 6.67%。ResNet 还在许多目标检测和图像定位挑战中获得了第一名,我们将在第七章中看到。更重要的是,ResNet 中的残差块概念为高效训练具有数百层的超深层神经网络打开了新的可能性。
使用开源实现
现在您已经学习了最流行的 CNN 架构中的一些,我想分享一些关于如何使用它们的实用建议。事实证明,由于学习衰减等超参数的调整细节以及其他影响性能的因素,许多这些神经网络难以复制或难以调整。深度学习研究人员甚至可能很难根据阅读他们的论文来复制他人的精炼工作。
幸运的是,许多深度学习研究人员通常会将其工作开源。在 GitHub 上简单搜索网络实现,就会指向几个深度学习库中的实现,您可以克隆并训练。如果您能找到作者的实现,通常会比尝试从头开始重新实现网络要快得多——尽管有时从头开始重新实现可能是一项很好的练习,就像我们之前所做的那样。
摘要
-
经典的 CNN 架构具有相同的经典架构,即通过不同的配置堆叠卷积和池化层。
-
LeNet 由五个权重层组成:三个卷积层和两个全连接层,在第一和第二个卷积层之后有一个池化层。
-
AlexNet 比 LeNet 更深,包含八个权重层:五个卷积层和三个全连接层。
-
VGGNet 通过为整个网络创建统一的配置来解决设置卷积和池化层超参数的问题。
-
Inception 试图解决与 VGGNet 相同的问题:不需要决定使用哪种滤波器大小以及在哪里添加池化层,Inception 说,“让我们都用上。”
-
ResNet 遵循与 Inception 相同的方法,创建了残差块,当它们堆叠在一起时,形成了网络架构。ResNet 试图解决在训练非常深的神经网络时出现的梯度消失问题,这会导致学习停滞或退化。ResNet 团队引入了跳跃连接,允许信息从网络中的早期层流向后期层,为梯度流动创建了一条替代的快捷路径。ResNet 的基本突破在于它使我们能够训练具有数百层的极其深的神经网络。
1.Y. Lecun, L. Bottou, Y. Bengio, and P. Haffner, “Gradient-Based Learning Applied to Document Recognition,” Proceedings of the IEEE 86 (11): 2278-2324, yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf.
2.Alex Krizhevsky, Ilya Sutskever, and Geoffrey E. Hinton, “ImageNet Classification with Deep Convolutional Neural Networks,” Communications of the ACM 60 (6): 84-90, dl.acm.org/doi/10.1145/3065386.
-
卡伦·西莫尼亚(Karen Simonyan)和安德鲁·齐塞拉曼(Andrew Zisserman),“用于大规模图像识别的超深卷积神经网络”,2014 年,
arxiv.org/pdf/1409.1556v6.pdf. -
克里斯蒂安·塞格迪(Christian Szegedy)、克里斯蒂安·刘(Wei Liu)、杨庆佳(Yangqing Jia)、皮埃尔·塞尔曼内特(Pierre Sermanet)、斯科特·里德(Scott Reed)、德拉戈米尔·安古洛夫(Dragomir Anguelov)、杜米特鲁·埃尔汉(Dumitru Erhan)、文森特·范霍克(Vincent Vanhoucke)和安德鲁·拉宾诺维奇(Andrew Rabinovich),“通过卷积加深学习”,载于 IEEE 计算机视觉与模式识别会议论文集,第 1-9 页,2015 年,
mng.bz/YryB. -
凯明·赫(Kaiming He)、张祥宇(Xiangyu Zhang)、邵庆庆(Shaoqing Ren)和孙剑(Jian Sun),“用于图像识别的深度残差学习”,2015 年,
arxiv.org/abs/1512.03385.
6 迁移学习
本章涵盖
-
理解迁移学习技术
-
使用预训练网络解决你的问题
-
理解网络微调
-
探索开源图像数据集以训练模型
-
构建两个端到端迁移学习项目
迁移学习是深度学习中最重要的技术之一。当构建一个用于解决特定问题的视觉系统时,你通常需要收集和标记大量数据来训练你的网络。你可以构建卷积神经网络(convnets),正如你在第三章中学到的,并从头开始训练;这是一个可接受的方法。但如果你能下载一个其他人已经调整和训练好的现有神经网络,并将其作为你新任务的起点呢?迁移学习允许你做到这一点。你可以下载一个其他人已经训练和调整好的开源模型,并使用他们的优化参数(权重)作为起点,在给定任务的小数据集上训练你的模型。这样,你可以更快地训练你的网络并取得更高的结果。
DL 研究人员和从业者已经发布了他们花费数周甚至数月时间在 GPU 上训练的算法的研究论文和开源项目,这些算法在各种问题上取得了最先进的结果。通常,其他人已经完成这项工作并经历了痛苦的高性能研究过程的事实意味着你可以下载一个开源架构和权重,并将它们作为你自己的神经网络的良好起点。这就是迁移学习:将一个领域预训练网络的知识迁移到不同领域的自己的问题上。
在本章中,我将解释迁移学习并概述使用它的原因。我还会详细说明不同的迁移学习场景以及如何使用它们。最后,我们将看到使用迁移学习解决现实世界问题的例子。准备好了吗?让我们开始吧!
6.1 迁移学习解决了哪些问题?
如其名所示,迁移学习意味着将神经网络从特定数据集的训练中学到的知识迁移到另一个相关问题上(图 6.1)。迁移学习目前在深度学习领域非常流行,因为它允许你在较短的时间内用相对较少的数据训练深度神经网络。迁移学习的重要性在于,在大多数现实世界的问题中,我们通常没有数百万个标记的图像来训练如此复杂的模型。

图 6.1 迁移学习是将网络从一项任务中获取的知识迁移到新任务的过程。在神经网络的情况下,获取的知识是提取的特征。
这个想法非常直接。首先,我们在大量数据上训练一个深度神经网络。在训练过程中,网络提取了大量有用的特征,这些特征可以用来检测这个数据集中的对象。然后,我们将这些提取的特征(特征图)转移到新的网络上,并在我们的新数据集上训练这个新的网络来解决不同的问题。迁移学习是一种通过重用为标准 CV 基准数据集(如 ImageNet 图像识别任务)开发的预训练模型的模型权重来简化收集和训练大量数据过程的好方法。表现最好的模型可以直接下载并使用,或者集成到新的模型中,用于解决你自己的 CV 问题。
问题是,我们为什么要使用迁移学习?为什么我们不可以直接在我们的新数据集上训练一个神经网络来解决问题呢?为了回答这个问题,我们首先需要了解迁移学习解决的主要问题。我们现在就来讨论这些问题;然后我会详细介绍迁移学习的工作原理以及应用它的不同方法。
深度神经网络对数据的需求极大,需要大量的标记数据才能实现高性能。在实践中,很少有人从头开始训练整个卷积网络。这主要是由于两个主要问题:
-
数据问题——从头开始训练一个网络需要大量的数据才能得到令人满意的结果,这在大多数情况下是不切实际的。拥有足够大以解决你问题的数据集相对较少。获取和标记数据也非常昂贵:这主要是一个由人类完成的繁琐过程,他们捕获图像并逐个标记它们,这使得它成为一个非平凡的任务。
-
计算问题——即使你能为你的问题获取数十万张图像,在数百万张图像上训练深度神经网络在计算上也非常昂贵,因为这通常需要数周在多个 GPU 上的训练。此外,请记住,训练神经网络是一个迭代过程。所以,即使你碰巧有训练复杂神经网络所需的计算能力,花费数周时间在每次训练迭代中尝试不同的超参数,直到最终达到令人满意的结果,也会使项目成本高昂。
此外,使用迁移学习的一个重要好处是它有助于模型泛化其学习并避免过拟合。当你在实际环境中应用深度学习模型时,它面临着无数它可能从未见过的条件,并且不知道如何处理;每个客户端都有自己的偏好,并生成与用于训练的数据不同的数据。模型被要求在许多与训练任务相关但不完全相似的任务上表现良好。
例如,当您将汽车分类器模型部署到生产环境中时,人们通常使用不同类型的摄像头,每种摄像头都有其自身的图像质量和分辨率。此外,图像可能在不同天气条件下拍摄。这些图像细微差别因用户而异。为了训练模型以涵盖所有这些不同情况,您要么必须考虑每一个案例并获取大量图像来训练网络,要么尝试构建一个更鲁棒的模型,使其更好地泛化到新的用例中。这就是迁移学习所做的事情。由于在野外考虑模型可能遇到的所有情况并不现实,迁移学习可以帮助我们应对新场景。对于超出大量标记数据任务和领域的深度学习生产规模使用,这是必要的。从已经看过数百万图像的另一个网络中迁移提取的特征将使我们的模型更不容易过拟合,并在面对新场景时更好地泛化。当我们解释以下章节中迁移学习是如何工作时,您将能够完全理解这个概念。
6.2 什么是迁移学习?
在理解了迁移学习解决的问题之后,让我们来看看它的正式定义。迁移学习是将网络从一个任务中获取的知识(特征图)转移到另一个任务中,在第一个任务中我们有大量数据,而在第二个任务中数据并不丰富。它通常用于在神经网络模型首先在一个与正在解决的问题相似的问题上训练的情况下。然后,从训练模型中使用的层在一个新的模型上训练感兴趣的问题。
正如我们之前讨论的,为了训练一个图像分类器,使其图像分类精度接近或超过人类水平,我们需要大量的数据、强大的计算能力和大量的时间。我相信我们大多数人都没有这些。知道这将是资源有限的人的问题,研究人员构建了在大型图像数据集(如 ImageNet、MS COCO、Open Images 等)上训练的最先进模型,并将他们的模型与公众共享以供重用。这意味着您永远不必从头开始训练图像分类器,除非您有一个异常大的数据集和非常大的计算预算来从头开始自己训练一切。即使是这样,您可能还是最好使用迁移学习来微调预训练网络以适应您的大数据集。在本章的后面部分,我们将讨论不同的迁移学习方法,您将了解微调的含义以及为什么即使您有大量数据集,使用迁移学习也是更好的选择。我们还将简要讨论这里提到的某些流行数据集。
注意:当我们谈论从头开始训练一个模型时,我们的意思是模型从对世界的零知识开始,模型的结构和参数开始时是随机猜测。从实际的角度来说,这意味着模型的权重是随机初始化的,并且它们需要通过训练过程来优化。
转移学习的直觉在于,如果一个模型在大而足够通用的数据集上进行了训练,那么这个模型将有效地作为视觉世界的通用表示。然后我们可以使用它所学习的特征图,而无需在大型数据集上训练,通过将所学知识转移到我们的模型中,并以此作为我们自己的任务的基础起始模型。
在转移学习中,我们首先在一个基础数据集和任务上训练一个基础网络,然后重新利用学到的特征,或者将它们转移到第二个目标网络,以在目标数据集和任务上进行训练。如果特征是通用的,即适合基础和目标任务,而不是仅针对基础任务,那么这个过程通常会有效。
--Jason Yosinski 等人 1
让我们直接跳到一个例子,以更好地理解如何使用转移学习。假设我们想要训练一个能够对狗和猫的图像进行分类的模型,而我们的问题中只有两个类别:狗和猫。我们需要为每个类别收集数十万张图像,对它们进行标记,并从头开始训练我们的网络。另一个选择是使用另一个预训练网络的知识。
首先,我们需要找到一个具有与我们当前问题相似特征的数据库。这涉及到花一些时间去探索不同的开源数据库,以找到与我们问题最接近的一个。为了这个例子,让我们使用 ImageNet,因为我们已经在前一章中熟悉它,并且它有很多狗和猫的图像。因此,预训练网络熟悉狗和猫的特征,并且将需要最少的训练。(在本章的后面,我们将探索其他数据库。)接下来,我们需要选择一个在 ImageNet 上训练并取得良好结果的网络。在第五章中,我们学习了像 VGGNet、GoogLeNet 和 ResNet 这样的最先进架构。它们中的任何一个都可以工作。对于这个例子,我们将选择一个在 ImageNet 数据集上训练的 VGG16 网络。
为了将 VGG16 网络适应我们的问题,我们将下载它并带有预训练的权重,移除分类器部分,添加我们自己的分类器,然后重新训练新的网络(图 6.2)。这被称为使用预训练网络作为特征提取器。我们将在本章后面讨论不同类型的转移学习。
定义:预训练模型是一个在大型数据集上预先训练过的网络,通常是在大规模图像分类任务上。我们可以直接使用预训练模型来运行我们的预测,或者使用网络的预训练特征提取部分并添加我们自己的分类器。这里的分类器可以是一个或多个密集层,甚至是传统的机器学习算法,如支持向量机(SVMs)。

图 6.2 应用迁移学习到 VGG16 网络的示例。我们冻结了网络的特征提取部分并移除了分类器部分。然后我们添加了新的分类器 softmax 层,包含两个隐藏单元。
为了完全理解如何使用迁移学习,让我们在 Keras 中实现这个示例。(幸运的是,Keras 有一系列预训练网络,我们可以直接下载和使用:模型完整列表见keras.io/api/applications。)以下是步骤:
-
下载 VGG16 网络及其权重的开源代码来创建我们的基础模型,并从 VGG 网络中移除分类层(
FC_4096>FC_4096>Softmax_1000):from keras.applications.vgg16 import VGG16 ❶ base_model = VGG16(weights = "imagenet", include_top=False, input_shape = (224,224, 3)) ❷ base_model.summary()❶ 从 Keras 导入 VGG16 模型
❷ 下载模型的预训练权重,并将它们保存在变量
base_model中。我们指定 Keras 下载 ImageNet 权重。include_top设置为False以忽略模型顶部的全连接分类器部分。 -
当你打印基础模型的摘要时,你会注意到我们下载了与第五章中实现的完全相同的 VGG16 架构。这是下载受你使用的深度学习库支持的流行网络的一种快速方法。或者,你也可以像我们在第五章中做的那样自己构建网络,并单独下载权重。我将在本章末尾的项目中向你展示如何操作。但就目前而言,让我们看看我们刚刚下载的
base_model摘要:Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 ================================================================= Total params: 14,714,688 Trainable params: 14,714,688 Non-trainable params: 0 _________________________________________________________________注意,这个下载的架构不包含网络顶部的分类器部分(三个全连接层),因为我们设置了
include_top参数为False。更重要的是,注意摘要中可训练和非可训练参数的数量。下载的网络本身使所有网络参数可训练。正如你所见,我们的base_model有超过 1400 万个可训练参数。接下来,我们想要冻结所有下载的层并添加我们自己的分类器。 -
冻结在 ImageNet 数据集上训练的特征提取层。冻结层意味着冻结它们的训练权重,以防止我们在运行训练时重新训练:
for layer in base_model.layers: ❶ layer.trainable = False base_model.summary()❶ 通过此代码遍历层并将它们锁定,使它们不可训练
在此情况下省略模型摘要以节省空间,因为它与之前的一个类似。不同之处在于所有权重都已冻结,可训练参数现在等于零,所有冻结层的参数都是不可训练的:
Total params: 14,714,688 Trainable params: 0 Non-trainable params: 14,714,688 -
添加我们自己的分类密集层。在这里,我们将添加一个具有两个单元的 softmax 层,因为我们的问题中只有两个类别(见图 6.3):
from keras.layers import Dense, Flatten ❶ from keras.models import Model last_layer = base_model.get_layer('block5_pool') ❷ last_output = last_layer.output ❸ x = Flatten()(last_output) ❹ x = Dense(2, activation='softmax', name='softmax')(*x*) ❺❶ 导入 Keras 模块
❷ 使用 get_layer 方法保存网络的最后一层
❸ 将最后一层的输出保存为下一层的输入
❹ 将分类器输入(VGG16 模型的最后一层输出)展平
❺ 添加我们的新 softmax 层,包含两个隐藏单元
![图片]()
图 6.3 移除网络的分类器部分,并添加一个具有两个隐藏节点的 softmax 层。
-
构建一个
new_model,它以基础模型的输入作为其输入,以最后一个 softmax 层的输出作为输出。新的模型由 VGGNet 中的所有特征提取层组成,并带有预训练的权重,再加上我们新的、未训练的 softmax 层。换句话说,当我们训练模型时,我们只会在本例中训练 softmax 层以检测我们新问题的特定特征(德国牧羊犬、比格犬、两者都不是):new_model = Model(inputs=base_model.input, outputs=x) ❶ new_model.summary() ❷ _________________________________________________________________ Layer (type) Output Shape Param # =================================================== input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ flatten_layer (Flatten) (None, 25088) 0 _________________________________________________________________ softmax (Dense) (None, 2) 50178 =================================================== Total params: 14,789,955 Trainable params: 50,178 Non-trainable params: 14,714,688 _________________________________________________________________❶ 使用 Keras 的 Model 类实例化一个 new_model
❷ 打印 new_model 摘要
训练新的模型比从头开始训练网络快得多。为了验证这一点,看看这个模型中的可训练参数数量(约 50,000)与网络中不可训练参数数量(约 1400 万)的对比。这些“不可训练”的参数已经在大型数据集上进行了训练,并且我们冻结了它们以使用我们问题中提取的特征。使用这个新模型,我们不必从头开始训练整个 VGGNet,因为我们只需要处理新添加的 softmax 层。
此外,由于新模型在数百万张图像(ImageNet 数据集+我们的小型数据集)上进行了训练,我们通过迁移学习获得了更好的性能。这允许网络理解物体细微差别的更详细信息,从而使其在新、以前未见过的图像上更好地泛化。
注意,在本例中,我们只探讨了构建模型的部分,以展示迁移学习是如何被使用的。在本章的结尾,我将带您通过两个端到端项目来展示如何在您的数据集上训练新的网络。但现在,让我们看看迁移学习是如何工作的。
6.3 迁移学习是如何工作的
到目前为止,我们学习了迁移学习技术是什么以及它解决的主要问题。我们还看到了一个例子,说明如何将训练在 ImageNet 上的预训练网络的学习迁移到我们的特定任务。现在,让我们看看为什么迁移学习有效,真正从一个问题转移到另一个问题的内容是什么,以及一个在某个数据集上训练的网络如何在不同的、可能无关的数据集上表现良好。
以下快速问题是来自前几章的提醒,以帮助我们了解迁移学习中的核心内容:
-
网络在训练过程中真正学习的是什么?简短的答案是:特征图。
-
这些特征是如何学习的?在反向传播过程中,权重被更新,直到我们得到最小化误差函数的优化权重。
-
特征和权重之间的关系是什么?特征图是在卷积过程中将权重滤波器应用于输入图像的结果(图 6.4)。
![图片]()
图 6.4 通过将卷积核应用于输入图像生成特征图的示例
-
从一个网络到另一个网络真正传递的是什么?为了传递特征,我们下载预训练网络的优化权重。然后,这些权重被用作训练过程的起点,并重新训练以适应新的问题。
好的,让我们深入了解,了解当我们说预训练网络时我们指的是什么。当我们训练卷积神经网络时,网络以特征图的形式从图像中提取特征:神经网络中每个层在应用权重滤波器后的输出。它们是训练集中存在的特征的表示。它们被称为特征图,因为它们映射了图像中某种特征的位置。CNNs 寻找直线、边缘甚至物体等特征。每当它们发现这些特征时,它们就会将它们报告给特征图。每个权重滤波器都在寻找不同的东西,这在特征图中得到反映:一个滤波器可能正在寻找直线,另一个可能寻找曲线,等等(图 6.5)。

图 6.5 网络以特征图的形式从图像中提取特征。它们是在应用权重滤波器后,训练集中存在的特征的表示。
现在,回想一下,神经网络在正向传播和反向传播的训练周期中会迭代更新它们的权重。当我们经历一系列的训练迭代和超参数调整,直到网络产生令人满意的结果时,我们说网络已经被训练了。训练完成后,我们输出两个主要项目:网络架构和训练好的权重。因此,当我们说我们要使用一个预训练网络时,我们的意思是我们将下载网络架构以及权重。
在训练过程中,模型只学习存在于这个训练数据集中的特征。但是,当我们下载在大量数据集(如 ImageNet)上训练的大型模型(如 Inception)时,从这些大型数据集中已经提取的所有特征现在都可供我们使用。我发现这真的很令人兴奋,因为这些预训练模型已经发现了我们数据集中没有的其他特征,这将帮助我们构建更好的卷积网络。
在视觉问题中,神经网络需要学习大量的关于训练数据集的知识。有低级特征,如边缘、角、圆形形状、曲线形状和块状物;还有中级和高级特征,如眼睛、圆形、方形和轮子。图像中有许多 CNN 可以捕捉到的细节——但如果我们训练数据集中只有 1,000 张图像,甚至 25,000 张图像,这可能不足以让模型学习所有这些内容。通过使用预训练网络,我们基本上可以将所有这些知识下载到我们的神经网络中,给它一个巨大且更快的起点,并实现更高的性能水平。
6.3.1 神经网络是如何学习特征的?
神经网络通过逐步增加复杂度,一层层地在数据集中学习特征。这些被称为特征图。你越深入网络层,学习的图像特定特征就越多。在图 6.6 中,第一层检测低级特征,如边缘和曲线。第一层的输出成为第二层的输入,产生更高层次的半圆和方形等特征。下一层将前一层输出组装成熟悉物体的部分,随后的一层检测物体。随着我们通过更多层,网络产生一个表示更复杂特征的激活图。随着我们深入网络,过滤器开始对像素空间更大区域做出更敏感的反应。高级层放大对区分重要方面有重要作用的输入,并抑制无关的变异。

图 6.6 CNN 在网络的早期层检测低级通用特征的示例。你越深入网络层,学习的图像特定特征就越多。
考虑图 6.6 中的示例。假设我们正在构建一个检测人脸的模型。我们注意到网络在第一层学习了低级特征,如线条、边缘和块状物。这些低级特征似乎并不特定于某个数据集或任务;它们是通用的特征,适用于许多数据集和任务。中级层将这些线条组装起来,以便能够识别形状、角和圆。请注意,提取的特征开始变得更加具体于我们的任务(人脸):中级特征包含形成人脸中眼睛和鼻子等物体的形状组合。随着我们深入网络,我们注意到特征最终从通用过渡到具体,并且到网络的最后一层,形成了非常具体于我们任务的顶级特征。我们开始看到区分不同人的面部特征。
现在,让我们以这个例子为例,比较从四个模型中提取的特征图,这些模型被训练来分类人脸、汽车、大象和椅子(见图 6.7)。注意,早期层的特征在所有模型中都非常相似。它们代表低级特征,如边缘、线条和块。这意味着在单一任务上训练的模型在网络的早期层中捕获了相似的数据类型关系,并且可以很容易地用于其他域的不同问题。我们越深入网络,特征就越具体,直到网络过度拟合其训练数据,使其更难泛化到不同的任务。较低级的特征几乎总是可以从一个任务迁移到另一个任务,因为它们包含通用的信息,如图像的结构和性质。将线条、点、曲线和物体的小部分信息迁移对于网络更快地学习以及在新任务上使用更少的数据是非常有价值的。

图 6.7 从四个模型中提取的特征图,这些模型被训练来分类人脸、汽车、大象和椅子
6.3.2 后层提取特征的可迁移性
在较深层提取的特征的可迁移性取决于原始数据集和新数据集的相似性。其理念是所有图像都必须有形状和边缘,因此早期层通常可以在不同域之间迁移。只有当我们开始提取更高级的特征时,我们才能识别物体之间的差异:比如,脸上的鼻子或汽车上的轮胎。只有在这种情况下,我们才能说,“好吧,这是一个人物,因为它有鼻子。这是汽车,因为它有轮胎。”基于源域和目标域的相似性,我们可以决定是否只从源域迁移低级特征,或者迁移高级特征,或者介于两者之间。这源于观察,随着我们讨论的下一段,网络的深层变得越来越具体于原始数据集中包含的类别的细节。
定义:源域是预训练网络所训练的原始数据集。目标域是我们希望训练网络的新数据集。
6.4 迁移学习方法
主要的迁移学习方法有三种:将预训练网络作为分类器、将预训练网络作为特征提取器以及微调。每种方法都可能有效,并且在开发和训练深度 CNN 模型时可以节省大量时间。可能不清楚哪种预训练模型的使用能在你的新计算机视觉任务上产生最佳结果,因此可能需要进行一些实验。在本节中,我们将解释这三种场景,并给出如何实现它们的示例。
6.4.1 使用预训练网络作为分类器
使用预训练网络作为分类器不需要冻结任何层或进行额外的模型训练。相反,我们只需取一个在类似问题上训练过的网络,并将其直接部署到我们的任务中。预训练模型直接用于对新图像进行分类,没有对其进行更改,也没有进行额外训练。我们所做的只是下载网络架构及其预训练权重,然后直接在我们的新数据上运行预测。在这种情况下,我们说我们新问题的领域与预训练网络训练的领域非常相似,并且它已经准备好部署。
在狗品种示例中,我们可以直接使用在 ImageNet 数据集上训练的 VGG16 网络来运行预测。ImageNet 已经包含了很多狗的图片,因此预训练网络的大部分表示能力可能被用于区分不同狗品种的特征。
让我们看看如何使用预训练网络作为分类器。在这个例子中,我们将使用在 ImageNet 数据集上预训练的 VGG16 网络来对图 6.8 中的德国牧羊犬图像进行分类。

图 6.8 我们将用于运行预测的德国牧羊犬样本图像
步骤如下:
-
导入必要的库:
from keras.preprocessing.image import load_img from keras.preprocessing.image import img_to_array from keras.applications.vgg16 import preprocess_input from keras.applications.vgg16 import decode_predictions from keras.applications.vgg16 import VGG16 -
下载 VGG16 的预训练模型及其 ImageNet 权重。我们将
include_top设置为True,因为我们想使用整个网络作为分类器:model = VGG16(weights = "imagenet", include_top=True, input_shape = (224,224, 3)) -
加载并预处理输入图像:
image = load_img('path/to/image.jpg', target_size=(224, 224)) ❶ image = img_to_array(image) ❷ image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) ❸ image = preprocess_input(image) ❹❶ 从文件中加载图像
❷ 将图像像素转换为 NumPy 数组
❸ 重新塑形数据以适应模型
❹ 准备图像以供 VGG 模型使用
-
现在输入图像已经准备好供我们运行预测:
yhat = model.predict(image) ❶ label = decode_predictions(yhat) ❷ label = label[0][0] ❸ print('%s (%.2f%%)' % (label[1], label[2]*100)) ❹❶ 预测所有输出类别的概率
❷ 将概率转换为类别标签
❸ 获取概率最高的最可能结果
❹ 打印分类
当你运行此代码时,你将得到以下输出:
>> German_shepherd (99.72%)
你可以看到,该模型已经被训练来以高置信度分数(99.72%)预测正确的狗品种。这是因为 ImageNet 数据集包含超过 20,000 个标记的狗图片,分为 120 个类别。前往本书的网站,用你自己的图片亲自尝试代码:www.manning.com/books/deep-learning-for-vision-systems 或 www.computervisionbook.com。请随意探索 ImageNet 中可用的类别,并在你自己的图片上运行此实验。
6.4.2 使用预训练网络作为特征提取器
这种方法与我们本章早期实现的狗品种示例类似:我们从一个在 ImageNet 上预训练的 CNN 中提取,冻结其特征提取部分,移除分类器部分,并添加我们自己的新、密集分类器层。在图 6.9 中,我们使用预训练的 VGG16 网络,冻结所有 13 个卷积层的权重,并用一个新的分类器替换旧的分类器,以便从头开始训练。
当我们的新任务与预训练网络训练的原数据集相似时,我们通常采用这种场景。由于 ImageNet 数据集包含大量的狗和猫的示例,网络学习到的特征图包含许多适用于我们新任务的狗和猫特征。这意味着我们可以使用从 ImageNet 数据集中提取的高级特征来完成这项新任务。
要做到这一点,我们冻结预训练网络的所有层,并在新数据集上仅训练我们刚刚添加的分类器部分。这种方法被称为使用预训练网络作为特征提取器,因为我们冻结了特征提取器部分,以便将所有学习到的特征图转移到我们的新问题上。我们仅在预训练模型之上添加一个新的分类器,该分类器将从零开始训练,这样我们就可以重新利用之前学习到的特征图来处理我们的数据集。
我们移除了预训练网络的分类部分,因为它通常非常特定于原始分类任务,并且随后它对模型训练的类别集合也是特定的。例如,ImageNet 有 1,000 个类别。分类器部分已经被训练来过度拟合训练数据,将它们分类到 1,000 个类别中。但在我们的新问题中,比如猫与狗的区别,我们只有两个类别。因此,从头开始训练一个新的分类器来过度拟合这两个类别要有效得多。

图 6.9 加载预训练的 VGG16 网络,移除分类器,并添加自己的分类器。
6.4.3 微调
到目前为止,我们已经看到了两种使用预训练网络进行迁移学习的基本方法:将预训练网络用作分类器或特征提取器。我们通常在目标域与源域相似时使用这些方法。但如果目标域与源域不同呢?如果它非常不同呢?我们还能使用迁移学习吗?是的。即使域非常不同,迁移学习仍然效果很好。我们只需要从源域提取正确的特征图,并微调它们以适应目标域。
在图 6.10 中,我们展示了从预训练网络中转移知识的不同方法。如果你下载整个网络且没有进行任何更改,只是运行预测,那么你是在使用该网络作为分类器。如果你只冻结卷积层,那么你是在使用预训练网络作为特征提取器,并将所有高级特征图转移到你的领域。微调的正式定义是冻结用于特征提取的一些网络层,并联合训练非冻结层和预训练模型中新添加的分类器层。它被称为微调,因为我们重新训练特征提取层时,我们微调高阶特征表示,使其对新任务数据集更加相关。
在更实际的层面上,如果我们冻结图 6.10 中的特征图 1 和 2,新的网络将使用特征图 2 作为其输入,并从这一点开始学习以适应后续层的特征到新的数据集。这节省了网络学习特征图 1 和 2 所需的时间。

图 6.10 网络通过其层学习特征。在迁移学习中,我们决定冻结预训练网络中的特定层以保留学习到的特征。例如,如果我们冻结网络在层 3 的特征图上,我们保留了它在层 1、2 和 3 中学到的内容。
如我们之前讨论的,网络早期提取的特征图是通用的。随着我们深入网络,特征图变得越来越具体。这意味着图 6.10 中的特征图 4 对源领域非常具体。基于两个领域的相似性,我们可以决定在适当的特征图级别冻结网络:
-
如果领域相似,我们可能希望冻结网络直到最后一个特征图级别(例如,特征图 4)。
-
如果领域非常不同,我们可能决定在特征图 1 之后冻结预训练网络,并重新训练所有剩余的层。
在这两种可能性之间,有一系列我们可以应用的微调选项。我们可以重新训练整个网络,或者冻结预训练网络在特征图 1、2、3 或 4 的任何级别,并重新训练剩余的网络。我们通常通过试错来决定适当的微调级别。但有一些指导原则,我们可以遵循以直观地决定预训练网络的微调级别。这个决定是两个因素的结果:我们拥有的数据量以及源领域和目标领域之间的相似程度。我们将在第 6.5 节中解释这些因素和四种可能的场景,以选择适当的微调级别。
为什么微调比从头开始训练更好?
当我们从零开始训练一个网络时,我们通常随机初始化权重并应用梯度下降优化器来找到最佳权重集,以优化我们的误差函数(如第二章所述)。由于这些权重从随机值开始,没有保证它们会以接近期望的最优值开始。如果初始化值远离最优值,优化器将需要很长时间才能收敛。这时微调可以非常有用。预训练网络的权重已经优化以从其数据集中学习。因此,当我们使用这个网络来解决问题时,我们以它结束时的权重值开始。所以,网络收敛得比如果它必须从随机初始化的权重从头开始训练要快得多。我们基本上是在微调已经优化的权重以适应我们的新问题,而不是用随机权重从头开始训练整个网络。即使我们决定重新训练整个预训练网络,从训练好的权重开始也会比从头开始用随机初始化的权重训练网络收敛得更快。
微调时使用较小的学习率
与新数据集的类分数计算的新线性分类器(随机初始化)的权重相比,通常在微调时使用较小的学习率。这是因为我们预计卷积网络的权重相对较好,所以我们不希望太快太多地扭曲它们(尤其是在上面的新分类器从随机初始化中进行训练时)。
6.5 选择适当的迁移学习级别
回想一下,早期的卷积层提取的是通用特征,并且随着我们深入网络,它们对训练数据的特定性会越来越高。换句话说,我们可以从现有的预训练模型中选择特征提取的详细程度。例如,如果新的任务与预训练网络的源域(例如,不同于 ImageNet)相当不同,那么预训练模型在第一层之后的输出可能就合适了。如果新的任务与源域相似,那么可能可以使用模型中更深层的输出,甚至可以使用在 softmax 层之前的全连接层的输出。
如前所述,选择适当的迁移学习级别是两个重要因素的函数:
-
目标数据集的大小(小或大)--当我们有一个小数据集时,网络可能不会从训练更多层中学习到很多,因此它可能会过度拟合新数据。在这种情况下,我们可能希望进行较少的微调,并更多地依赖于源数据集。
-
源数据集和目标数据集的领域相似性——我们的新问题与原始数据集的领域相似到什么程度?例如,如果你的问题是分类汽车和船只,ImageNet 可能是一个不错的选择,因为它包含许多具有相似特征的图像。另一方面,如果你的问题是根据 X 射线图像对肺癌进行分类,这是一个完全不同的领域,可能需要大量的微调。
这两个因素导致了四种主要场景:
-
目标数据集很小,并且与源数据集相似。
-
目标数据集很大且与源数据集相似。
-
目标数据集很小,并且与源数据集非常不同。
-
目标数据集很大,并且与源数据集非常不同。
让我们逐一讨论这些场景,以了解导航我们选项的常见经验法则。
6.5.1 场景 1:目标数据集很小且与源数据集相似
由于原始数据集与我们的新数据集相似,我们可以预期预训练的卷积神经网络中的高级特征也与我们的数据集相关。因此,最好冻结网络的特征提取部分,只重新训练分类器。
另一个可能不是很好对网络进行微调的原因是我们的新数据集很小。如果我们在一个小数据集上微调特征提取层,这将迫使网络对我们的数据进行过度拟合。这并不好,因为根据定义,小数据集没有足够的信息来覆盖其对象的所有可能特征,这使得它无法泛化到新的、之前未见过的数据。因此,在这种情况下,我们进行的微调越多,网络就越容易过度拟合新数据。
例如,假设我们新数据集中的所有图像都包含在特定天气环境下的狗——例如雪。如果我们在这个数据集上微调,我们将迫使新的网络选择像雪和白色背景这样的特征作为狗的特定特征,并使其无法在其他天气条件下对狗进行分类。因此,一般经验法则是:如果你有少量数据,在微调预训练网络时要小心过度拟合。
6.5.2 场景 2:目标数据集很大且与源数据集相似
由于这两个领域相似,我们可以冻结特征提取部分并重新训练分类器,类似于我们在场景 1 中做的。但由于我们新领域中的数据更多,我们可以通过微调整个预训练网络或其部分来获得性能提升,并且更有信心不会过度拟合。由于高级特征相关(因为数据集相似),因此不需要通过整个网络进行微调。所以一个好的开始是冻结大约 60-80%的预训练网络,并在新数据上重新训练剩余的部分。
6.5.3 场景 3:目标数据集很小且与源数据集不同
由于数据集不同,可能最好不冻结预训练网络的更高层特征,因为它们包含更多数据集特定的特征。相反,从网络中较早的部分重新训练层会更好——或者不冻结任何层,对整个网络进行微调。然而,由于你的数据集较小,在整个数据集上微调整个网络可能不是一个好主意,因为这样做会使它容易过拟合。在这种情况下,一个折中的方案会更好。一个好的开始是冻结大约预训练网络的前三分之一或一半。毕竟,早期层包含非常通用的特征图,即使数据集非常不同,这些特征图对你的数据集也将是有用的。
6.5.4 场景 4:目标数据集很大且与源数据集不同
由于新数据集很大,你可能想从头开始训练整个网络,根本不使用迁移学习。然而,在实践中,正如我们之前讨论的那样,从预训练模型初始化权重通常仍然非常有益。这样做可以使模型更快收敛。在这种情况下,我们有一个大型的数据集,这使我们能够有信心在整个网络上进行微调,而不必担心过拟合。
6.5.5 迁移学习场景回顾
我们已经探讨了帮助我们定义使用哪种迁移学习方法的两个主要因素(我们数据的大小和源数据集与目标数据集之间的相似性)。这两个因素为我们提供了表 6.1 中定义的四个主要场景。图 6.11 总结了在每个场景中应使用适当微调级别的指南。

图 6.11 在四种场景中适当微调级别的指南
表 6.1 迁移学习场景
| 场景 | 目标数据集的大小 | 原始数据集和新数据集的相似性 | 方法 |
|---|---|---|---|
| 1 | 小 | 相似 | 使用预训练网络作为特征提取器 |
| 2 | 大 | 相似 | 在整个网络中进行微调 |
| 3 | 小 | 非常不同 | 从网络早期激活中进行微调 |
| 4 | 大 | 非常不同 | 在整个网络中进行微调 |
6.6 开源数据集
CV 研究社区在互联网上发布数据集方面做得相当不错。所以,当你听到像 ImageNet、MS COCO、Open Images、MNIST、CIFAR 等名字时,这些是人们已经发布到网上,并且许多计算机研究人员已经将它们用作基准来训练他们的算法并获得最先进结果的数据集。
在本节中,我们将回顾一些流行的开源数据集,以帮助您在寻找最适合您问题的数据集时提供指导。请注意,本章中列出的数据集是撰写时 CV 研究社区中最流行的数据集;我们并不打算提供所有开源数据集的完整列表。许多图像数据集可供使用,而且数量每天都在增长。在开始您的项目之前,我鼓励您进行自己的研究,以探索可用的数据集。
6.6.1 MNIST
MNIST (yann.lecun.com/exdb/mnist) 代表修改后的国家标准与技术研究院。它包含从 0 到 9 的手写数字的标记图像。该数据集的目标是对手写数字进行分类。MNIST 在研究社区中因其作为分类算法的基准而被广泛使用。实际上,它被认为是图像数据集的“hello, world!”。但如今,MNIST 数据集相对比较简单,一个基本的卷积神经网络就能达到超过 99%的准确率,因此 MNIST 不再被视为 CNN 性能的基准。我们在第三章中实现了使用 MNIST 数据集的 CNN 分类项目;请随意回顾。

图 6.12 MNIST 数据集的样本
MNIST 包含 60,000 个训练图像和 10,000 个测试图像。所有图像都是灰度图(单通道),每个图像高 28 像素,宽 28 像素。图 6.12 展示了 MNIST 数据集的一些样本图像。
6.6.2 Fashion-MNIST
Fashion-MNIST 是为了取代原始的 MNIST 数据集而创建的,因为对于现代卷积神经网络来说,它已经变得过于简单。数据存储的格式与 MNIST 相同,但不是手写数字,而是包含 10 个时尚服装类别的 60,000 个训练图像和 10,000 个测试图像:T 恤/上衣、裤子、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包和踝靴。访问github.com/zalandoresearch/fashion-mnist以探索和下载数据集。图 6.13 展示了所代表类别的样本。

图 6.13 Fashion-MNIST 数据集的样本图像
6.6.3 CIFAR
CIFAR-10 (www.cs.toronto.edu/~kriz/cifar.html) 被认为是 CV 和 ML 文献中图像分类的另一个基准数据集。与 MNIST 中的图像相比,CIFAR 图像更为复杂,因为 MNIST 图像都是灰度图,且物体居中,而 CIFAR 图像是彩色(三个通道)的,物体外观变化很大。CIFAR-10 数据集包含 10 个类别的 32×32 彩色图像,每个类别有 6,000 个图像。共有 50,000 个训练图像和 10,000 个测试图像。图 6.14 展示了数据集中的类别。

图 6.14 CIFAR-10 数据集的样本图像
CIFAR-100 是 CIFAR-10 的“大哥”:它包含 100 个类别,每个类别有 600 个图像。这 100 个类别被分为 20 个超级类别。每个图像都附有精细标签(它所属的类别)和粗略标签(它所属的超级类别)。
6.6.4 ImageNet
我们在之前的章节中多次讨论了 ImageNet 数据集,并在第五章和本章中广泛使用了它。但为了完整性,我们在这里也进行讨论。在撰写本文时,ImageNet 被认为是当前的基准,并被 CV 研究人员广泛用于评估他们的分类算法。
ImageNet 是一个大型视觉数据库,旨在用于视觉对象识别软件研究。它旨在根据一组定义的单词和短语将图像标记和分类到近 22,000 个类别中。这些图像是从网络收集的,并由人类通过亚马逊的 Mechanical Turk 众包工具进行标记。在撰写本文时,ImageNet 项目中已有超过 1400 万张图像。为了组织如此大量的数据,ImageNet 的创造者遵循了 WordNet 层次结构:WordNet 中的每个有意义的单词/短语被称为同义词集(简称 synset)。在 ImageNet 项目中,图像根据这些 synset 组织,目标是每个 synset 有 1,000+张图像。图 6.15 显示了由斯坦福大学汇编的 ImageNet 示例拼贴。

图 6.15 斯坦福大学汇编的 ImageNet 示例拼贴
当 CV 社区谈论 ImageNet 时,通常指的是 ImageNet 大规模视觉识别挑战(ILSVRC)。在这个挑战中,软件程序竞争正确分类和检测对象和场景。我们将使用 ILSVRC 挑战作为基准来比较不同网络的性能。
6.6.5 MS COCO
MS COCO (cocodataset.org)代表 Microsoft Common Objects in Context。它是一个开源数据库,旨在使未来的研究能够进行对象检测、实例分割、图像标题和定位人体关键点。它包含 328,000 张图像。其中超过 200,000 张被标记,包括 1.5 百万个对象实例和 80 个对象类别,这些类别对于一个 4 岁的孩子来说很容易识别。数据集创造者的原始研究论文描述了该数据集的动机和内容。2 图 6.16 显示了 MS COCO 网站上提供的数据集样本。

图 6.16 MS COCO 数据集的样本(图片版权©2015,COCO 联盟,经 Creative Commons Attribution 4.0 许可使用。)
6.6.6 Google Open Images
Open Images (storage.googleapis.com/openimages/web/index.html) 是由谷歌创建的一个开源图像数据库。截至本文撰写时,它包含超过 900 万张图像。使其脱颖而出的原因是这些图像大多是复杂场景,跨越了成千上万的物体类别。此外,其中超过 200 万张图像被人工标注了边界框,使得 Open Images 成为迄今为止最大的具有物体位置标注的数据集(见图 6.17)。在这个图像子集中,有大约 1540 万个 600 个类别物体的边界框。类似于 ImageNet 和 ILSVRC,Open Images 有一个名为 Open Images Challenge (mng.bz/aRQz) 的挑战。
6.6.7 Kaggle
除了本节中列出的数据集之外,Kaggle (www.kaggle.com) 也是数据集的另一个优秀来源。Kaggle 是一个网站,它主办了机器学习和深度学习挑战,来自世界各地的人们可以参与并提交算法以供评估。
我们强烈建议你探索这些数据集,并寻找每天出现的许多其他开源数据集,以更好地理解它们支持的类别和使用案例。在本章的项目中,我们主要使用 ImageNet;在整个书中,我们将使用 MS COCO,尤其是在第七章。

图 6.17 来自 Open Images 数据集的标注图像,摘自谷歌 AI 博客(Vittorio Ferrari,“Open Images 更新——现在包含边界框”,2017 年 7 月,mng.bz/yyVG)。
6.7 项目 1:作为特征提取器的预训练网络
在这个项目中,我们使用非常少量的数据来训练一个检测狗和猫图像的分类器。这是一个相当简单的项目,但这个练习的目标是了解如何在数据非常少且目标域与源域相似(场景 1)的情况下实现迁移学习。正如本章所解释的,在这种情况下,我们将使用预训练的卷积网络作为特征提取器。这意味着我们将冻结网络的特征提取部分,添加我们自己的分类器,然后在我们的新小型数据集上重新训练网络。
从这个项目中,另一个重要的收获是学习如何预处理自定义数据并将其准备好以训练你的神经网络。在以前的项目中,我们使用了 CIFAR 和 MNIST 数据集:它们已经被 Keras 预处理,所以我们只需要从 Keras 库中下载它们,并直接用于训练网络。本项目提供了一个教程,说明如何构建你的数据存储库并使用 Keras 库来准备你的数据。
访问书籍网站www.manning.com/books/deep-learning-for-vision-systems或www.computervisionbook.com下载用于此项目的代码笔记本和数据集。由于我们使用迁移学习,训练不需要高计算能力,因此你可以在个人电脑上运行这个笔记本;你不需要 GPU。
对于这个实现,我们将使用 VGG16。尽管它没有在 ILSVRC 中记录最低的错误率,但我发现它对这项任务效果很好,并且比其他模型训练得更快。我得到了大约 96%的准确率,但你完全可以自由地使用 GoogLeNet 或 ResNet 进行实验并比较结果。
使用预训练模型作为特征提取器的过程已经确立:
-
导入必要的库。
-
预处理数据,使其准备好用于神经网络。
-
从在大数据集上训练的 VGG16 网络中加载预训练的权重。
-
冻结卷积层(特征提取部分)中的所有权重。记住,要冻结的层会根据新任务与原始数据集的相似性进行调整。在我们的案例中,我们观察到 ImageNet 有很多狗和猫的图片,因此网络已经训练好了提取我们目标对象的详细特征。
-
用自定义分类器替换网络的完全连接层。你可以添加你认为合适的完全连接层,每个层可以有任意数量的隐藏单元。对于像这样的简单问题,我们将只添加一个包含 64 个单元的隐藏层。你可以观察结果,如果模型欠拟合就调整,如果模型过拟合就下调。对于 softmax 层,单元的数量必须设置为类别数(在我们的案例中是两个单元)。
-
编译网络,并在新的猫狗数据上运行训练过程以优化模型,使其适用于较小的数据集。
-
评估模型。
现在,让我们逐步进行这些步骤并实现这个项目:
-
导入必要的库:
from keras.preprocessing.image import ImageDataGenerator from keras.preprocessing import image from keras.applications import imagenet_utils from keras.applications import vgg16 from keras.applications import mobilenet from keras.optimizers import Adam, SGD from keras.metrics import categorical_crossentropy from keras.layers import Dense, Flatten, Dropout, BatchNormalization from keras.models import Model from sklearn.metrics import confusion_matrix import itertools import matplotlib.pyplot as plt %matplotlib inline -
预处理数据,使其准备好用于神经网络。Keras 有一个
ImageDataGenerator类,允许我们轻松地即时执行图像增强;你可以在keras.io/api/preprocessing/image上了解更多信息。在这个例子中,我们使用ImageDataGenerator生成我们的图像张量,但为了简单起见,我们不会实现图像增强。ImageDataGenerator类有一个名为flow_from_directory()的方法,用于从包含图像的文件夹中读取图像。此方法期望你的数据目录结构如图 6.18 所示。![]()
图 6.18 使用 Keras 的
.flow_from_directory()方法所需的目录结构我已经将数据结构在书的代码中,所以它已经准备好供你使用
flow_from_directory()。现在,将数据加载到train_path、valid_path和test_path变量中,然后生成训练、验证和测试批次:train_path = 'data/train' valid_path = 'data/valid' test_path = 'data/test' train_batches = ImageDataGenerator().flow_from_directory(train_path, ❶ target_size=(224,224), batch_size=10) valid_batches = ImageDataGenerator().flow_from_directory(valid_path, target_size=(224,224), batch_size=30) test_batches = ImageDataGenerator().flow_from_directory(test_path, target_size=(224,224), batch_size=50, shuffle=False)❶ ImageDataGenerator 生成具有实时数据增强的批处理张量图像数据。数据将循环(以批处理形式)。在这个例子中,我们不会进行任何图像增强。
-
从在大数据集上训练的 VGG16 网络中加载预训练的权重。类似于本章中的示例,我们从 Keras 下载 VGG16 网络,并在 ImageNet 数据集上预训练后下载其权重。请记住,我们想要从这个网络中移除分类器部分,因此我们将参数
include_top=False设置为:base_model = vgg16.VGG16(weights = "imagenet", include_top=False, input_shape = (224,224, 3)) -
冻结卷积层(特征提取部分)中的所有权重。我们冻结了之前步骤中创建的
base_model中的卷积层,并将其用作特征提取器,然后在下一步中在其顶部添加分类器:for layer in base_model.layers: ❶ layer.trainable = False❶ 通过此代码遍历层并将它们锁定,以使它们不可训练
-
添加新的分类器,并构建新的模型。我们在基础模型之上添加了一些层。在这个例子中,我们添加了一个具有 64 个隐藏单元的全连接层和一个具有 2 个隐藏单元的 softmax 层。我们还添加了批归一化和 dropout 层以避免过拟合:
last_layer = base_model.get_layer('block5_pool') ❶ last_output = last_layer.output x = Flatten()(last_output) ❷ x = Dense(64, activation='relu', name='FC_2')(*x*) ❸ x = BatchNormalization()(*x*) ❸ x = Dropout(0.5)(*x*) ❸ x = Dense(2, activation='softmax', name='softmax')(*x*) ❸ new_model = Model(inputs=base_model.input, outputs=x) ❹ new_model.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 25088) 0 _________________________________________________________________ FC_2 (Dense) (None, 64) 1605696 _________________________________________________________________ batch_normalization_1 (Batch (None, 64) 256 _________________________________________________________________ dropout_1 (Dropout) (None, 64) 0 _________________________________________________________________ softmax (Dense) (None, 2) 130 ================================================================= Total params: 16,320,770 Trainable params: 1,605,954 Non-trainable params: 14,714,816 _________________________________________________________________❶ 使用 get_layer 方法保存网络的最后一层。然后将最后一层的输出保存为下一层的输入。
❷ 将分类器输入(VGG16 模型的最后一层的输出)展平
❸ 添加一个具有 64 个单元和批归一化、dropout 和 softmax 层的全连接层
❹ 使用 Keras 的 Model 类实例化一个新模型
-
编译模型并运行训练过程:
new_model.compile(Adam(lr=0.0001), loss='categorical_crossentropy', metrics=['accuracy']) new_model.fit_generator(train_batches, steps_per_epoch=4, validation_data=valid_batches, validation_steps=2, epochs=20, verbose=2)当你运行前面的代码片段时,每个 epoch 之后都会打印出详细的训练信息,如下所示:
Epoch 1/20 - 28s - loss: 1.0070 - acc: 0.6083 - val_loss: 0.5944 - val_acc: 0.6833 Epoch 2/20 - 25s - loss: 0.4728 - acc: 0.7754 - val_loss: 0.3313 - val_acc: 0.8605 Epoch 3/20 - 30s - loss: 0.1177 - acc: 0.9750 - val_loss: 0.2449 - val_acc: 0.8167 Epoch 4/20 - 25s - loss: 0.1640 - acc: 0.9444 - val_loss: 0.3354 - val_acc: 0.8372 Epoch 5/20 - 29s - loss: 0.0545 - acc: 1.0000 - val_loss: 0.2392 - val_acc: 0.8333 Epoch 6/20 - 25s - loss: 0.0941 - acc: 0.9505 - val_loss: 0.2019 - val_acc: 0.9070 Epoch 7/20 - 28s - loss: 0.0269 - acc: 1.0000 - val_loss: 0.1707 - val_acc: 0.9000 Epoch 8/20 - 26s - loss: 0.0349 - acc: 0.9917 - val_loss: 0.2489 - val_acc: 0.8140 Epoch 9/20 - 28s - loss: 0.0435 - acc: 0.9891 - val_loss: 0.1634 - val_acc: 0.9000 Epoch 10/20 - 26s - loss: 0.0349 - acc: 0.9833 - val_loss: 0.2375 - val_acc: 0.8140 Epoch 11/20 - 28s - loss: 0.0288 - acc: 1.0000 - val_loss: 0.1859 - val_acc: 0.9000 Epoch 12/20 - 29s - loss: 0.0234 - acc: 0.9917 - val_loss: 0.1879 - val_acc: 0.8372 Epoch 13/20 - 32s - loss: 0.0241 - acc: 1.0000 - val_loss: 0.2513 - val_acc: 0.8500 Epoch 14/20 - 29s - loss: 0.0120 - acc: 1.0000 - val_loss: 0.0900 - val_acc: 0.9302 Epoch 15/20 - 36s - loss: 0.0189 - acc: 1.0000 - val_loss: 0.1888 - val_acc: 0.9000 Epoch 16/20 - 30s - loss: 0.0142 - acc: 1.0000 - val_loss: 0.1672 - val_acc: 0.8605 Epoch 17/20 - 29s - loss: 0.0160 - acc: 0.9917 - val_loss: 0.1752 - val_acc: 0.8667 Epoch 18/20 - 25s - loss: 0.0126 - acc: 1.0000 - val_loss: 0.1823 - val_acc: 0.9070 Epoch 19/20 - 29s - loss: 0.0165 - acc: 1.0000 - val_loss: 0.1789 - val_acc: 0.8833 Epoch 20/20 - 25s - loss: 0.0112 - acc: 1.0000 - val_loss: 0.1743 - val_acc: 0.8837注意,该模型使用常规 CPU 计算能力训练得非常快。每个 epoch 大约需要 25 到 29 秒,这意味着模型在 20 个 epoch 的训练中花费不到 10 分钟。
-
评估模型。首先,让我们定义
load_dataset()方法,我们将使用它将我们的数据集转换为张量:from sklearn.datasets import load_files from keras.utils import np_utils import numpy as np def load_dataset(path): data = load_files(path) paths = np.array(data['filenames']) targets = np_utils.to_categorical(np.array(data['target'])) return paths, targets test_files, test_targets = load_dataset('small_data/test')然后,我们创建 test_tensors 以在它们上评估模型:
from keras.preprocessing import image from keras.applications.vgg16 import preprocess_input from tqdm import tqdm def path_to_tensor(img_path): img = image.load_img(img_path, target_size=(224, 224)) ❶ *x* = image.img_to_array(img) ❷ return np.expand_dims(x, axis=0) ❸ def paths_to_tensor(img_paths): list_of_tensors = [path_to_tensor(img_path) for img_path in tqdm(img_paths)] return np.vstack(list_of_tensors) test_tensors = preprocess_input(paths_to_tensor(test_files))❶ 以 PIL.Image.Image 类型加载 RGB 图像
❷ 将 PIL.Image.Image 类型转换为形状为(224, 224, 3)的 3D 张量
❸ 将 3D 张量转换为形状为(1, 224, 224, 3)的 4D 张量,并返回 4D 张量
现在我们可以运行 Keras 的
evaluate()方法来计算模型准确率:print('\nTesting loss: {:.4f}\nTesting accuracy: {:.4f}'.format(*new_model.evaluate(test_tensors, test_targets))) Testing loss: 0.1042 Testing accuracy: 0.9579
该模型在不到 10 分钟的训练时间内达到了 95.79%的准确率。考虑到我们的数据集非常小,这是一个非常好的结果。
6.8 项目 2:微调
在这个项目中,我们将探讨本章前面讨论过的场景 3,其中目标数据集很小,并且与源数据集非常不同。这个项目的目标是构建一个能够区分 10 个类别的手势语言分类器:从 0 到 9 的手势语言数字。图 6.19 展示了我们数据集的一个样本。
以下是我们的数据集详细信息:
-
类别数量 = 10(数字 0,1,2,3,4,5,6,7,8 和 9)
-
图像大小 = 100 × 100
-
色彩空间 = RGB
-
训练集包含 1,712 个图像
-
验证集包含 300 个图像
-
测试集包含 50 个图像

图 6.19 标准手势数据集的一个样本
我们的数据集非常小这一点非常明显。如果你尝试在这个非常小的数据集上从头开始训练网络,你将不会取得好的结果。另一方面,尽管源域和目标域非常不同,我们仍然能够通过使用迁移学习达到超过 98%的准确率。
注意:请带着批判的眼光看待这次评估,因为网络还没有用大量数据彻底测试。在这个数据集中我们只有 50 个测试图像。尽管如此,预期迁移学习仍然能够取得良好的结果,但我还是想强调这一点。
访问本书的网站www.manning.com/books/deep-learning-for-vision-systems或www.computervisionbook.com以下载用于此项目的源代码笔记本和数据集。与项目 1 类似,训练不需要高计算能力,因此你可以在个人电脑上运行这个笔记本;你不需要 GPU。
为了方便与前面的项目进行比较,我们将使用在 ImageNet 数据集上训练的 VGG16 网络。微调预训练网络的步骤如下:
-
导入必要的库。
-
预处理数据以使其准备好供神经网络使用。
-
从在大数据集(ImageNet)上训练的 VGG16 网络中加载预训练的权重。
-
冻结特征提取器部分的一部分。
-
添加新的分类器层。
-
编译网络,并运行训练过程以优化模型以适应较小的数据集。
-
评估模型。
现在我们来实现这个项目:
-
导入必要的库:
from keras.preprocessing.image import ImageDataGenerator from keras.preprocessing import image from keras.applications import imagenet_utils from keras.applications import vgg16 from keras.optimizers import Adam, SGD from keras.metrics import categorical_crossentropy from keras.layers import Dense, Flatten, Dropout, BatchNormalization from keras.models import Model from sklearn.metrics import confusion_matrix import itertools import matplotlib.pyplot as plt %matplotlib inline -
预处理数据以使其准备好供神经网络使用。与项目 1 类似,我们使用 Keras 中的
ImageDataGenerator类和flow_from_directory()方法来预处理我们的数据。数据已经为你结构化,可以直接创建张量:train_path = 'dataset/train' valid_path = 'dataset/valid' test_path = 'dataset/test' train_batches = ImageDataGenerator().flow_from_directory(train_path, ❶ target_size=(224,224), batch_size=10) valid_batches = ImageDataGenerator().flow_from_directory(valid_path, target_size=(224,224), batch_size=30) test_batches = ImageDataGenerator().flow_from_directory(test_path, target_size=(224,224), batch_size=50, shuffle=False) Found 1712 images belonging to 10 classes. Found 300 images belonging to 10 classes. Found 50 images belonging to 10 classes.❶
ImageDataGenerator生成具有实时数据增强的批处理张量图像数据。数据将会循环(以批处理形式)。在这个例子中,我们不会进行任何图像增强。 -
从在大型数据集(ImageNet)上训练的 VGG16 网络中加载预训练的权重。我们从 Keras 库中下载带有 ImageNet 权重的 VGG16 架构。注意,我们在这里使用参数
pooling='avg':这基本上意味着将对最后一个卷积层的输出应用全局平均池化,因此模型的输出将是一个 2D 张量。我们将其用作在添加全连接层之前Flatten层的替代方案:base_model = vgg16.VGG16(weights = "imagenet", include_top=False, input_shape = (224,224, 3), pooling='avg') -
冻结特征提取器部分的一些层,并在我们的新训练数据上微调其余部分。微调的程度通常是通过试错来确定的。VGG16 有 13 个卷积层:你可以冻结所有层,也可以根据你的数据与源数据的相似程度冻结其中一些层。在手语案例中,新领域与我们的领域非常不同,因此我们将从仅微调最后五层开始;如果我们没有得到令人满意的结果,我们可以进一步微调。结果是我们训练了新模型后,达到了 98%的准确率,所以这是一个很好的微调水平。但在其他情况下,如果你发现你的网络没有收敛,尝试微调更多层。
for layer in base_model.layers[:-5]: ❶ layer.trainable = False base_model.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ global_average_pooling2d_1 ( (None, 512) 0 ================================================================= Total params: 14,714,688 Trainable params: 7,079,424 Non-trainable params: 7,635,264 _________________________________________________________________❶ 遍历层并锁定它们,除了最后五层
-
添加新的分类器层,并构建新的模型:
last_output = base_model.output ❶ x = Dense(10, activation='softmax', name='softmax')(last_output) ❷ new_model = Model(inputs=base_model.input, outputs=x) ❸ new_model.summary() ❹ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ global_average_pooling2d_1 ( (None, 512) 0 _________________________________________________________________ softmax (Dense) (None, 10) 5130 ================================================================= Total params: 14,719,818 Trainable params: 7,084,554 Non-trainable params: 7,635,264❶ 将 base_model 的输出保存为下一层的输入
❷ 添加我们的新 softmax 层,包含 10 个隐藏单元
❸ 使用 Keras 的 Model 类实例化一个新模型
❹ 打印新模型的摘要
-
编译网络,并运行训练过程以优化模型以适应较小的数据集:
new_model.compile(Adam(lr=0.0001), loss='categorical_crossentropy', metrics=['accuracy']) from keras.callbacks import ModelCheckpoint checkpointer = ModelCheckpoint(filepath='signlanguage.model.hdf5', save_best_only=True) history = new_model.fit_generator(train_batches, steps_per_epoch=18, validation_data=valid_batches, validation_steps=3, epochs=20, verbose=1, callbacks=[checkpointer]) Epoch 1/150 18/18 [==============================] - 40s 2s/step - loss: 3.2263 - acc: 0.1833 - val_loss: 2.0674 - val_acc: 0.1667 Epoch 2/150 18/18 [==============================] - 41s 2s/step - loss: 2.0311 - acc: 0.1833 - val_loss: 1.7330 - val_acc: 0.3000 Epoch 3/150 18/18 [==============================] - 42s 2s/step - loss: 1.5741 - acc: 0.4500 - val_loss: 1.5577 - val_acc: 0.4000 Epoch 4/150 18/18 [==============================] - 42s 2s/step - loss: 1.3068 - acc: 0.5111 - val_loss: 0.9856 - val_acc: 0.7333 Epoch 5/150 18/18 [==============================] - 43s 2s/step - loss: 1.1563 - acc: 0.6389 - val_loss: 0.7637 - val_acc: 0.7333 Epoch 6/150 18/18 [==============================] - 41s 2s/step - loss: 0.8414 - acc: 0.6722 - val_loss: 0.7550 - val_acc: 0.8000 Epoch 7/150 18/18 [==============================] - 41s 2s/step - loss: 0.5982 - acc: 0.8444 - val_loss: 0.7910 - val_acc: 0.6667 Epoch 8/150 18/18 [==============================] - 41s 2s/step - loss: 0.3804 - acc: 0.8722 - val_loss: 0.7376 - val_acc: 0.8667 Epoch 9/150 18/18 [==============================] - 41s 2s/step - loss: 0.5048 - acc: 0.8222 - val_loss: 0.2677 - val_acc: 0.9000 Epoch 10/150 18/18 [==============================] - 39s 2s/step - loss: 0.2383 - acc: 0.9276 - val_loss: 0.2844 - val_acc: 0.9000 Epoch 11/150 18/18 [==============================] - 41s 2s/step - loss: 0.1163 - acc: 0.9778 - val_loss: 0.0775 - val_acc: 1.0000 Epoch 12/150 18/18 [==============================] - 41s 2s/step - loss: 0.1377 - acc: 0.9667 - val_loss: 0.5140 - val_acc: 0.9333 Epoch 13/150 18/18 [==============================] - 41s 2s/step - loss: 0.0955 - acc: 0.9556 - val_loss: 0.1783 - val_acc: 0.9333 Epoch 14/150 18/18 [==============================] - 41s 2s/step - loss: 0.1785 - acc: 0.9611 - val_loss: 0.0704 - val_acc: 0.9333 Epoch 15/150 18/18 [==============================] - 41s 2s/step - loss: 0.0533 - acc: 0.9778 - val_loss: 0.4692 - val_acc: 0.8667 Epoch 16/150 18/18 [==============================] - 41s 2s/step - loss: 0.0809 - acc: 0.9778 - val_loss: 0.0447 - val_acc: 1.0000 Epoch 17/150 18/18 [==============================] - 41s 2s/step - loss: 0.0834 - acc: 0.9722 - val_loss: 0.0284 - val_acc: 1.0000 Epoch 18/150 18/18 [==============================] - 41s 2s/step - loss: 0.1022 - acc: 0.9611 - val_loss: 0.0177 - val_acc: 1.0000 Epoch 19/150 18/18 [==============================] - 41s 2s/step - loss: 0.1134 - acc: 0.9667 - val_loss: 0.0595 - val_acc: 1.0000 Epoch 20/150 18/18 [==============================] - 39s 2s/step - loss: 0.0676 - acc: 0.9777 - val_loss: 0.0862 - val_acc: 0.9667注意从详细输出中查看每个 epoch 的训练时间。该模型使用常规 CPU 计算能力训练得非常快。每个 epoch 大约需要 40 秒,这意味着模型在 15 分钟内训练了 20 个 epoch。
-
评估模型的准确率。与之前的项目类似,我们创建了一个
load_dataset()方法来创建test_targets和test_tensors,然后使用 Keras 的evaluate()方法在测试图像上运行推理并获取模型准确率:print('\nTesting loss: {:.4f}\nTesting accuracy: {:.4f}'.format(*new_model.evaluate(test_tensors, test_targets))) Testing loss: 0.0574 Testing accuracy: 0.9800评估模型更深层次的方法是创建一个混淆矩阵。我们在第四章中解释了混淆矩阵:它是一个常用来描述分类模型性能的表格,以提供对模型在测试数据集上表现更深入的理解。有关不同模型评估指标的具体细节,请参阅第四章。现在,让我们为我们的模型构建一个混淆矩阵(见图 6.20):
from sklearn.metrics import confusion_matrix import numpy as np cm_labels = ['0','1','2','3','4','5','6','7','8','9'] cm = confusion_matrix(np.argmax(test_targets, axis=1), np.argmax(new_model.predict(test_tensors), axis=1)) plt.imshow(cm, cmap=plt.cm.Blues) plt.colorbar() indexes = np.arange(len(cm_labels)) for i in indexes: for j in indexes: plt.text(j, i, cm[i, j]) plt.xticks(indexes, cm_labels, rotation=90) plt.xlabel('Predicted label') plt.yticks(indexes, cm_labels) plt.ylabel('True label') plt.title('Confusion matrix') plt.show()要阅读这个混淆矩阵,请查看预测标签轴上的数字,并检查它是否在真实标签轴上正确分类。例如,查看预测标签轴上的数字 0:所有五张图片都被分类为 0,没有图片被错误地分类为其他任何数字。同样,查看预测标签轴上的其余数字。你会注意到,模型成功地对所有测试图像进行了正确预测,除了真实标签为 8 的图像。在这种情况下,模型错误地将数字 8 的图像分类为数字 7。
摘要
-
迁移学习通常是在开始分类和目标检测项目时首选的方法,尤其是在你没有大量训练数据时。
-
迁移学习将源数据集学习到的知识迁移到目标数据集,以节省训练时间和计算成本。
-
神经网络逐步以递增的复杂度层次学习数据集中的特征。你越深入网络层,学习的特征就越具有图像特异性。
-
网络的早期层学习低级特征,如线条、块和边缘。第一层的输出成为第二层的输入,产生更高级的特征。下一层将前一层输出组装成熟悉物体的部分,后续层检测物体。
-
迁移学习的主要方法有:使用预训练网络作为分类器、使用预训练网络作为特征提取器以及微调。
-
使用预训练网络作为分类器意味着直接使用网络对新图像进行分类,而不冻结层或应用模型训练。
-
使用预训练网络作为特征提取器意味着冻结网络的分类器部分,并重新训练新的分类器。
-
微调意味着冻结用于特征提取的一些网络层,并联合训练非冻结层和预训练模型中新添加的分类器层。
![]()
图 6.20 手语分类器的混淆矩阵
-
一个网络到另一个网络的特征可迁移性是目标数据大小和源数据与目标数据域相似性的函数。
-
通常,微调参数使用较小的学习率,而从头开始训练输出层可以使用较大的学习率。
1.Jason Yosinski, Jeff Clune, Yoshua Bengio, 和 Hod Lipson, “How Transferable Are Features in Deep Neural Networks?” Advances in Neural Information Processing Systems 27 (Dec. 2014): 3320-3328, arxiv.org/abs/1411.1792.
- Tsung-Yi Lin, Michael Maire, Serge Belongie, 等,“Microsoft COCO: Common Objects in Context” (February 2015),
arxiv.org/pdf/1405.0312.pdf.
7 使用 R-CNN、SSD 和 YOLO 进行目标检测
本章涵盖
-
理解图像分类与目标检测
-
理解目标检测项目的一般框架
-
使用 R-CNN、SSD 和 YOLO 等目标检测算法
在前几章中,我们解释了如何使用深度神经网络进行图像分类任务。在图像分类中,我们假设图像中只有一个主要的目标对象,模型的主要焦点是识别目标类别。然而,在许多情况下,我们对图像中的多个目标感兴趣。我们不仅想要对它们进行分类,还想要获取它们在图像中的具体位置。在计算机视觉中,我们将此类任务称为目标检测。图 7.1 解释了图像分类和目标检测任务之间的区别。

图 7.1 图像分类与目标检测任务。在分类任务中,分类器输出类别概率(猫),而在目标检测任务中,检测器输出定位检测到的对象的边界框坐标(本例中的四个框)及其预测类别(两只猫、一只鸭和一只狗)。
目标检测是一个涉及两个主要任务的计算机视觉任务:在图像中定位一个或多个对象,并对图像中的每个对象进行分类(见表 7.1)。这是通过在识别的对象周围绘制一个包含其预测类别的边界框来完成的。这意味着系统不仅预测图像的类别,就像在图像分类任务中那样;它还预测适合检测到的对象的边界框坐标。这是一个具有挑战性的计算机视觉任务,因为它需要成功的目标定位,以便在图像中定位并绘制每个对象的边界框,以及目标分类来预测定位到的对象的正确类别。
表 7.1 图像分类与目标检测
| 图像分类 | 目标检测 |
|---|
| 目标是预测图像中对象的类型或类别。
-
输入:包含单个对象的图像
-
输出:类别标签(猫、狗等)
-
示例输出:类别概率(例如,84%猫)
| 目标是通过边界框预测图像中对象的位置,并预测定位到的对象的类别。|
-
输入:包含一个或多个对象的图像
-
输出:一个或多个边界框(由坐标定义)以及每个边界框的类别标签
-
包含两个对象的图像的示例输出:
-
box1 坐标(x, y, w, h)和类别概率
-
box2 坐标和类别概率
-
注意,图像坐标(x, y, w, h)如下:(x 和 y)是边界框中心点的坐标,而(w 和 h)是框的宽度和高度。|
| |
| |
| |
| |
目标检测在许多领域中得到广泛应用。例如,在自动驾驶技术中,我们需要通过识别捕获的视频图像中车辆、行人、道路和障碍物的位置来规划路线。机器人通常执行此类任务以检测感兴趣的目标。安全领域的系统需要检测异常目标,如入侵者或炸弹。
本章的结构如下:
-
我们将探讨目标检测算法的通用框架。
-
我们将深入研究三种最受欢迎的检测算法:R-CNN 系列网络、SSD 和 YOLO 系列网络。
-
我们将利用在现实世界项目中学到的知识来训练一个端到端的目标检测器。
到本章结束时,我们将了解深度学习在目标检测中的应用,以及不同的目标检测模型如何相互启发和分化。让我们开始吧!
7.1 通用目标检测框架
在我们深入探讨 R-CNN、SSD 和 YOLO 等目标检测系统之前,让我们先讨论这些系统的通用框架,以便理解基于深度学习(DL)的系统在检测对象时遵循的高级工作流程以及它们用于评估检测性能的指标。目前不必担心目标检测器的代码实现细节。本节的目标是向您概述不同的目标检测系统如何处理这一任务,并介绍一种新的思考方式以及一系列新概念,以便您能够理解我们在第 7.2 节、第 7.3 节和第 7.4 节中将要解释的深度学习架构。
通常,一个目标检测框架包含四个组件:
-
区域提议——使用算法或深度学习模型生成感兴趣区域(RoIs),以便系统进一步处理。这些是网络认为可能包含对象的区域;输出是大量边界框,每个边界框都有一个对象性分数。具有高对象性分数的边界框随后被传递到网络层进行进一步处理。
-
特征提取和网络预测——为每个边界框提取视觉特征。根据视觉特征(例如,一个对象分类组件)评估这些特征,并确定在提议中是否存在以及哪些对象存在。
-
非极大值抑制(NMS)——在这个步骤中,模型可能已经找到了同一对象的多个边界框。NMS 通过将重叠的边界框合并为每个对象的单个边界框来帮助避免对同一实例的重复检测。
-
评估指标——类似于图像分类任务中的准确率、精确率和召回率指标(见第四章),目标检测系统有自己的指标来评估它们的检测性能。在本节中,我们将解释最流行的指标,如平均精度均值(mAP)、精确率-召回率曲线(PR 曲线)和交并比(IoU)。
现在,让我们深入一层,了解这些组件的目标,以建立对这些组件的直观认识。
7.1.1 区域提议
在这一步,系统查看图像并提议进行进一步分析的 RoIs(感兴趣区域)。RoIs 是系统认为有很高可能性包含对象的区域,称为对象性分数(图 7.2)。具有高对象性分数的区域将传递到下一步;分数低的区域将被放弃。

图 7.2 系统提出的感兴趣区域(RoIs)。具有高对象性分数的区域代表有很高可能性包含对象(前景),而具有低对象性分数的区域被忽略,因为它们有很低的可能性包含对象(背景)。
生成区域提议有几种方法。最初,选择性搜索算法被用来生成对象提议;当我们讨论 R-CNN 网络时,我们将更详细地介绍这个算法。其他方法使用从图像中提取的更复杂的视觉特征,这些特征由深度神经网络生成区域(例如,基于深度学习模型的特征)。
我们将更详细地讨论不同的目标检测系统如何处理这个任务。需要注意的是,这一步产生了大量(数千个)边界框,这些边界框将由网络进一步分析和分类。在这一步中,网络分析图像中的这些区域,并根据其对象性分数将每个区域分类为前景(对象)或背景(无对象)。如果对象性分数高于某个阈值,则该区域被认为是前景,并在网络中推进。请注意,这个阈值可以根据你的问题进行配置。如果阈值太低,你的网络将生成所有可能的提议,你将有更好的机会检测到图像中的所有对象。另一方面,这非常计算密集,会减慢检测速度。因此,生成区域提议的权衡是区域数量与计算复杂度——正确的做法是使用特定于问题的信息来减少 RoIs 的数量。
7.1.2 网络预测
该组件包括用于特征提取的预训练 CNN 网络,从输入图像中提取具有代表性的特征,并使用这些特征来确定图像的类别。在目标检测框架中,人们通常使用预训练的图像分类模型来提取视觉特征,因为这些模型通常具有很好的泛化能力。例如,在 MS COCO 或 ImageNet 数据集上训练的模型能够提取相当通用的特征。
在这一步,网络分析所有被识别为有很高可能性包含对象的区域,并对每个区域做出两个预测:
-
边界框预测--定位围绕对象的框的坐标。边界框坐标表示为元组(x, y, w, h),其中x和y是边界框中心点的坐标,w 和 h 是框的宽度和高度。
-
类别预测:预测每个对象类别概率的经典 softmax 函数。
由于提出了数千个区域,每个对象周围都会有多个边界框,这些边界框具有正确的分类。例如,看看图 7.3 中的狗的图像。网络显然能够找到对象(狗)并成功对其进行分类。但是检测总共触发了五次,因为狗出现在之前步骤中产生的五个 RoIs 中:因此图中的狗周围有五个边界框。尽管检测器能够成功地在图像中定位狗并正确分类它,但这并不是我们需要的。对于大多数问题,我们只需要每个对象一个边界框。在某些问题中,我们只想得到最适合对象的那个框。如果我们正在构建一个用于在图像中计数狗的系统呢?我们当前的系统将计数五只狗。我们不想这样。这就是非极大值抑制技术派上用场的时候了。

图 7.3 边界框检测器对一个对象产生多个边界框。我们希望将这些框合并成一个最适合对象的边界框。
7.1.3 非极大值抑制(NMS)
如图 7.4 所示,对象检测算法的一个问题是它可能会找到同一对象的多个检测。因此,它不是只围绕对象创建一个边界框,而是为同一对象绘制多个框。NMS 是一种确保检测算法只检测每个对象一次的技术。正如其名所示,NMS 检查围绕一个对象的全部框,以找到具有最大预测概率的框,并抑制或消除其他框(因此得名)。

图 7.4 对于同一个对象提出了多个区域。经过 NMS 处理后,只剩下最适合对象的框;其余的都被忽略,因为它们与选定的框有较大的重叠。
NMS 的一般思想是将候选框的数量减少到每个对象只有一个边界框。例如,如果帧中的对象相当大,并且已经生成了超过 2,000 个对象提议,那么其中一些很可能彼此之间有显著的重叠,并且与对象本身重叠。
让我们看看 NMS 算法的工作步骤:
-
抛弃所有预测值低于某个特定阈值(称为置信度阈值)的边界框。这个阈值是可调整的,这意味着如果预测概率低于设定的阈值,则该框将被抑制。
-
查看所有剩余的框,并选择概率最高的边界框。
-
计算具有相同类别预测的剩余框的重叠。具有高度重叠并预测相同类别的边界框被平均在一起。这个重叠指标称为交并比(IoU)。IoU 将在下一节中详细解释。
-
抑制任何 IoU 值小于某个特定阈值(称为 NMS 阈值)的框。通常 NMS 阈值等于 0.5,但如果你想输出更少或更多的边界框,它也是可调整的。
NMS 技术通常在不同检测框架中是标准的,但它是一个重要的步骤,可能需要根据场景调整超参数,如置信度阈值和 NMS 阈值。
7.1.4 对象检测器评估指标
当评估对象检测器的性能时,我们使用两个主要的评估指标:每秒帧数和平均精度均值。
每秒帧数(FPS)用于衡量检测速度
用于衡量检测速度的最常用指标是每秒帧数(FPS)。例如,Faster R-CNN 以仅 7 FPS 的速度运行,而 SSD 以 59 FPS 的速度运行。在基准测试实验中,你将看到论文的作者将他们的网络结果表述为:“网络x在 Z FPS 下实现了 Y%的 mAP,”其中x是网络名称,y是 mAP 百分比,Z 是 FPS。
平均精度均值(mAP)用于衡量网络精度
在对象识别任务中最常用的评估指标是平均精度均值(mAP)。它是一个从 0 到 100 的百分比,通常值越高越好,但它的值与分类中使用的准确度指标不同。
要理解 mAP 是如何计算的,你首先需要理解交并比(IoU)和精确度-召回率曲线(PR 曲线)。让我们先解释 IoU 和 PR 曲线,然后再回到 mAP。
交并比(IoU)
这个指标评估两个边界框的重叠:真实边界框(Bground truth)和预测边界框(Bpredicted)。通过应用 IoU,我们可以判断一个检测是否有效(真阳性)或不是(假阳性)。图 7.5 说明了真实边界框和预测边界框之间的 IoU。

图 7.5 IoU 分数是真实边界框和预测边界框之间的重叠。
交并比(IoU)的值从 0(完全没有重叠)到 1(两个边界框重叠 100%)不等。两个边界框之间的重叠程度(IoU 值)越高,越好(见图 7.6)。

图 7.6 IoU 分数范围从 0(无重叠)到 1(100%重叠)。两个边界框之间的重叠程度(IoU)越高,越好。
要计算预测的 IoU,我们需要以下信息:
-
真实边界框(Bground truth):在标注过程中创建的手动标注边界框
-
我们模型预测的边界框(Bpredicted)
我们通过将重叠面积除以并集面积来计算 IoU,如下方程所示:

IoU 用于定义一个正确的预测,意味着一个 IoU 值大于某个阈值的预测(真阳性)。这个阈值取决于挑战,但 0.5 是一个标准值。例如,一些挑战,如 Microsoft COCO,使用 mAP@0.5(IoU 阈值为 0.5)或 mAP@0.75(IoU 阈值为 0.75)。如果 IoU 值高于这个阈值,预测被认为是真阳性(TP);如果低于阈值,则被认为是假阳性(FP)。
精度-召回曲线(PR 曲线)
定义了 TP 和 FP 后,我们现在可以计算在测试数据集上针对给定类别的检测的精度和召回率。如第四章所述,我们计算精度和召回率如下(FN 代表假阴性):

在计算了所有类别的精度和召回率后,PR 曲线如图 7.7 所示。

图 7.7 使用精度-召回曲线来评估目标检测器的性能。
PR 曲线是评估目标检测器性能的好方法,因为通过为每个对象类别绘制曲线来改变置信度。如果一个检测器的精度在召回率增加时保持较高,则认为它是一个好的检测器,这意味着如果你改变置信度阈值,精度和召回率仍然很高。另一方面,一个差的检测器需要增加 FP 的数量(降低精度)以达到高的召回率。这就是为什么 PR 曲线通常以高精度值开始,随着召回率的增加而降低。
现在我们有了 PR 曲线,我们可以通过计算曲线下的面积(AUC)来计算平均精度(AP)。最后,目标检测的 mAP 是所有类别计算出的 AP 的平均值。值得注意的是,一些研究论文将 AP 和 mAP 互换使用。
概述
总结一下,mAP 的计算如下:
-
获取每个边界框关联的对象性分数(该框包含对象的概率)。
-
计算精度和召回率。
-
通过改变分数阈值,为每个类别计算 PR 曲线。
-
计算平均精度(AP):PR 曲线下的面积。在这一步,为每个类别计算 AP。
-
计算平均精度(mAP):所有不同类别的平均 AP。
关于 mAP 的最后一点是,它比其他传统指标(如准确率)更复杂。好消息是您不需要自己计算 mAP 值:大多数深度学习目标检测实现都为您处理 mAP 的计算,您将在本章后面看到。
现在我们已经了解了目标检测算法的一般框架,让我们更深入地探讨其中三个最受欢迎的。在本章中,我们将详细讨论 R-CNN 系列网络、SSD 和 YOLO 网络,以了解目标检测器是如何随着时间的推移而演变的。我们还将检查每个网络的优缺点,以便您可以选择最适合您问题的算法。
7.2 基于区域的卷积神经网络(R-CNNs)
R-CNN 系列的目标检测技术通常被称为 R-CNNs,即基于区域的卷积神经网络,由 Ross Girshick 等人于 2014 年开发。1 R-CNN 系列在 2015 年和 2016 年分别扩展到包括 Fast-RCNN2 和 Faster-RCN3。在本节中,我将快速为您介绍 R-CNN 系列从 R-CNN 到 Fast R-CNN 再到 Faster R-CNN 的演变过程,然后我们将更深入地探讨 Faster R-CNN 架构和代码实现。
7.2.1 R-CNN
R-CNN 是其家族中最简单的基于区域的架构,但它是理解所有多个目标识别算法如何工作的基础。它是卷积神经网络在目标检测和定位问题上的第一个大型、成功应用之一,并为其他高级检测算法铺平了道路。该方法在基准数据集上进行了演示,在 PASCAL VOC-2012 数据集和 ILSVRC 2013 目标检测挑战赛上实现了当时最先进的结果。图 7.8 展示了 R-CNN 模型架构的总结。

图 7.8 R-CNN 模型架构总结。(改编自 Girshick 等人,“用于准确目标检测和语义分割的丰富特征层次。”)
R-CNN 模型由四个部分组成:
-
提取感兴趣区域 --也称为提取区域提议。这些区域有很高的概率包含一个对象。一种称为选择性搜索的算法扫描输入图像以找到包含块的区域,并将它们作为 RoIs 提议给管道中的下一个模块进行处理。提议的 RoIs 随后被变形为固定大小;它们通常大小不一,但正如我们在前面的章节中学到的,CNN 需要固定大小的输入图像。
-
特征提取模块 --我们在区域提议上运行一个预训练的卷积网络,以从每个候选区域中提取特征。这是我们之前章节中学到的典型的 CNN 特征提取器。
-
分类模块 --我们训练一个分类器,如支持向量机(SVM),一种传统的机器学习算法,根据上一步提取的特征对候选检测进行分类。
-
定位模块 --也称为边界框回归器。让我们回顾一下回归的概念。机器学习问题被分类为分类或回归问题。分类算法输出离散的、预定义的类别(狗、猫、大象),而回归算法输出连续的值预测。在这个模块中,我们想要预测围绕对象的边界框的位置和大小。边界框通过识别四个值来表示:框原点的 x 和 y 坐标(x, y)、框的宽度和高度(w, h)。将这些值组合起来,回归器预测定义边界框的四个实数值,如下面的元组所示:(x, y, w, h)。
选择性搜索
选择性搜索是一种贪婪搜索算法,用于提供可能包含物体的区域建议。它通过将相似的像素和纹理组合成矩形框来尝试找到可能包含物体的区域。选择性搜索结合了穷举搜索算法(检查图像中所有可能的位置)和从下而上的分割算法(将相似区域分层组合)的优点,以捕捉所有可能的对象位置。
选择性搜索算法通过在图像中应用分割算法来寻找块,以便确定可能是什么物体(参见以下图中右侧的图像)。

选择性搜索算法在图像中寻找类似块状的区域以提取区域。在右侧,分割算法定义了可能是物体的块。然后选择性搜索算法选择这些区域以供进一步调查。
从下而上的分割递归地将这些区域组合成更大的区域,以创建大约 2,000 个需要调查的区域,如下所示:
-
计算所有相邻区域之间的相似性。
-
将两个最相似的区域组合在一起,并计算结果区域与其邻居之间的新相似性。
-
此过程会重复进行,直到整个物体被单个区域覆盖。
注意,选择性搜索算法及其如何计算区域相似性的综述超出了本书的范围。如果你对此感兴趣,可以进一步学习。
关于这项技术,你可以参考原始论文。[a] 为了理解 R-CNN,你可以将选择性搜索算法视为一个智能扫描图像并为我们提出 RoI 位置的“黑盒”。

使用选择性搜索算法的从下而上的分割示例。它通过在每次迭代中组合相似区域,直到整个物体被单个区域覆盖。
J.R.R. Uijlings, K.E.A. van de Sande, T. Gevers, 和 A.W.M. Smeulders, “选择性搜索用于物体识别,” 2012, www.huppelen.nl/publications/selectiveSearchDraft.pdf.
图 7.9 以直观的方式展示了 R-CNN 架构。如图所示,网络首先提出 RoIs,然后提取特征,接着根据这些特征对这些区域进行分类。本质上,我们将物体检测转化为一个图像分类问题。

图 7.9 R-CNN 架构的示意图。每个提出的 RoI 都会通过 CNN 提取特征,然后通过边界框回归器和 SVM 分类器产生网络输出预测。
训练 R-CNN
在上一节中,我们了解到 R-CNN 由四个模块组成:选择性搜索区域提议、特征提取器、分类器和边界框回归器。除了选择性搜索算法外,所有 R-CNN 模块都需要进行训练。因此,为了训练 R-CNN,我们需要做以下几步:
-
训练特征提取器 CNN。这是一个典型的 CNN 训练过程。我们或者从头开始训练一个网络,这种情况很少发生,或者微调一个预训练的网络,正如我们在第六章中学到的那样。
-
训练 SVM 分类器。SVM 算法在本书中没有涉及,但它是一种传统的机器学习分类器,在需要训练标记数据的意义上与深度学习分类器没有区别。
-
训练边界框回归器。该模型为每个 K 个物体类别输出四个实数值,以缩小区域边界框。
通过查看 R-CNN 的学习步骤,你可以很容易地发现训练 R-CNN 模型既昂贵又缓慢。训练过程涉及训练三个独立的模块,共享计算很少。这种多阶段管道训练是 R-CNN 的一个缺点,我们将在下一节中看到。
R-CNN 的缺点
R-CNN 非常易于理解,并且它在首次推出时取得了最先进的结果,尤其是在使用深度卷积神经网络提取特征时。然而,它实际上不是一个通过深度神经网络学习定位的单个端到端系统。相反,它是由多个独立算法组合而成的,共同执行物体检测。因此,它有以下显著的缺点:
-
物体检测非常慢。对于每张图像,选择性搜索算法会提出大约 2,000 个 RoI 供整个管道(CNN 特征提取器和分类器)检查。由于它对每个物体提议执行卷积网络前向传递而不共享计算,这导致计算量非常大,因此非常慢。这种高计算需求意味着 R-CNN 不适合许多应用,尤其是需要快速推理的应用,如自动驾驶汽车等。
-
训练是一个多阶段管道。如前所述,R-CNN 需要训练三个模块:CNN 特征提取器、SVM 分类器和边界框回归器。因此,训练过程非常复杂,不是一个端到端训练。
-
训练在空间和时间上都很昂贵。当训练 SVM 分类器和边界框回归器时,从每张图像中的每个对象提议中提取特征并写入磁盘。对于像 VGG16 这样的非常深的网络,使用 GPU 对几千张图像的训练过程需要几天时间。从空间上讲,训练过程也很昂贵,因为提取的特征需要数百 GB 的存储空间。
我们需要的是一个端到端深度学习系统,它修正了 R-CNN 的缺点,同时提高了其速度和准确性。
7.2.2 Fast R-CNN
Fast R-CNN 是 R-CNN 的直接后裔,由 Ross Girshick 于 2015 年开发。Fast R-CNN 在许多方面与 R-CNN 技术相似,但在检测速度上有所改进,同时通过两个主要变化提高了检测准确性:
-
与 R-CNN 不同,它不是从区域提议模块开始,然后使用特征提取模块,Fast-RCNN 建议我们首先将 CNN 特征提取器应用于整个输入图像,然后提出区域。这样,我们只需在整个图像上运行一个 ConvNet,而不是在 2,000 个重叠区域上运行 2,000 个 ConvNet。
-
它将 ConvNet 的任务扩展到分类部分,通过用 softmax 层替换传统的 SVM 机器学习算法来实现。这样,我们有一个单一模型来执行两个任务:特征提取和对象分类。
Fast R-CNN 架构
如图 7.10 所示,Fast R-CNN 基于网络的最后一个特征图生成区域提议,而不是像 R-CNN 那样从原始图像中生成。因此,我们只需为整个图像训练一个 ConvNet。此外,我们不再需要训练许多不同的 SVM 算法来分类每个对象类别,而是单个 softmax 层直接输出类别概率。现在我们只有一个神经网络需要训练,而不是一个神经网络和多个 SVM。
Fast R-CNN 的架构由以下模块组成:
-
特征提取模块 -- 网络从全图像开始,使用 ConvNet 提取特征。
-
RoI 提取器 -- 选择性搜索算法为每张图像提出大约 2,000 个区域候选。
-
RoI 池化层 -- 这是一个新组件,用于在将 RoIs 馈送到全连接层之前从特征图中提取一个固定大小的窗口。它使用最大池化将任何有效 RoI 内的特征转换为具有固定空间范围高度×宽度(H×W)的小特征图。RoI 池化层将在 Faster R-CNN 部分中更详细地解释;现在,了解它应用于从 CNN 提取的最后一个特征图层,其目标是提取固定大小的 RoIs 以馈送到全连接层和输出层。
-
双头输出层 --模型分为两个头部:
-
一个 softmax 分类层,输出每个 RoI 的离散概率分布
-
一个边界框回归层,用于预测相对于原始 RoI 的偏移量
-

图 7.10 快速 R-CNN 架构由特征提取器 ConvNet、RoI 提取器、RoI 池化层、全连接层和双头输出层组成。注意,与 R-CNN 不同,快速 R-CNN 在应用区域建议模块之前,将特征提取器应用于整个输入图像。
快速 R-CNN 中的多任务损失函数
由于快速 R-CNN 是一个端到端学习架构,用于学习对象的类别以及相关的边界框位置和大小,因此损失是多任务损失。在多任务损失的情况下,输出有 softmax 分类器和边界框回归器,如图 7.10 所示。
在任何优化问题中,我们需要定义一个损失函数,我们的优化器算法试图最小化它。(第二章提供了更多关于优化和损失函数的细节。)在目标检测问题中,我们的目标是优化两个目标:对象分类和对象定位。因此,在这个问题中,我们有两个损失函数:Lcls 用于分类损失,Lloc 用于定义对象位置的边界框预测。
快速 R-CNN 网络有两个兄弟输出层,带有两个损失函数:
-
分类 --第一个输出一个离散概率分布(每个 RoI),覆盖 K + 1 个类别(我们为背景添加一个类别)。概率 P 是通过全连接层的 K + 1 个输出的 softmax 计算得到的。分类损失函数是对真实类别 u 的对数损失。
L[cls](p,u) = −logp[u]
其中 u 是真实标签,u ∈ 0, 1, 2, . . . (K + 1);其中 u = 0 表示背景;而 p 是每个 RoI 在 K + 1 个类别上的离散概率分布。
-
回归 --第二个兄弟层输出每个 K 个对象类别的边界框回归偏移 v = (x, y, w, h)。损失函数是类别 u 的边界框损失。
Lloc = σ L1smooth
其中:
-
v 是真实边界框,v = (x, y, w, h)。
-
t u 是预测边界框校正:
t^u = (t[x]^u, t[y]^u, t[w]^u, t[h]^u)
-
L1[smooth] 是使用平滑 L1 损失函数来衡量 tiu 和 vi 之间差异的边界框损失。它是一个鲁棒函数,据称比其他回归损失(如 L2)对异常值更不敏感。
-
总体损失函数是
L = L[cls] + L[loc]
L(p, u, t^u, v) = L[cls](p, u) + [u ≥ 1] l[box](t^u, v)
注意,[u ≥ 1] 在回归损失之前添加,表示当检查的区域不包含任何对象且包含背景时为 0。这是一种在分类器将区域标记为背景时忽略边界框回归的方法。指示函数 [u ≥ 1] 定义为

Fast R-CNN 的缺点
Fast R-CNN 在测试时间上要快得多,因为我们不需要为每张图像将 2,000 个区域提议输入到卷积神经网络中。相反,每个图像只进行一次卷积操作,并从中生成一个特征图。训练也更快,因为所有组件都在一个 CNN 网络中:特征提取器、物体分类器和边界框回归器。然而,仍然存在一个大的瓶颈:用于生成区域提议的选择搜索算法非常慢,并且由另一个模型单独生成。使用深度学习实现完整端到端物体检测系统的最后一步是找到一种方法将区域提议算法结合到我们的端到端深度学习网络中。这就是 Faster R-CNN 所做的事情,我们将在下面看到。
7.2.3 Faster R-CNN
Faster R-CNN 是 R-CNN 家族的第三次迭代,由 Shaoqing Ren 等人于 2016 年开发。与 Fast R-CNN 类似,图像被提供给一个卷积网络,该网络提供卷积特征图。而不是在特征图上使用选择搜索算法来识别区域提议,使用区域提议网络(RPN)来预测作为训练过程一部分的区域提议。然后使用 RoI 池化层重塑预测的区域提议,并在提议的区域中对图像进行分类,并预测边界框的偏移值。这些改进既减少了区域提议的数量,又加速了模型的测试时间操作,接近实时,并具有当时最先进的性能。
Faster R-CNN 架构
Faster R-CNN 的架构可以用两个主要网络来描述:
-
区域提议网络(RPN)--选择搜索被一个卷积网络所取代,该网络从特征提取器的最后特征图中提出 RoIs 以供调查。RPN 有两个输出:物体分数(物体或无物体)和边界框位置。
-
Fast R-CNN --它由 Fast R-CNN 的典型组件组成:
-
特征提取器的基网络:一个典型的预训练 CNN 模型,用于从输入图像中提取特征。
-
RoI 池化层用于提取固定大小的 RoIs
-
输出层包含两个全连接层:一个 softmax 分类器用于输出类别概率,一个边界框回归 CNN 用于预测边界框。
-
如图 7.11 所示,输入图像被提供给网络,并通过预训练的 CNN 提取其特征。这些特征并行发送到 Faster R-CNN 架构的两个不同组件:
-
RPN 用于确定图像中可能存在物体的位置。在此阶段,我们并不知道物体是什么,只知道图像的某个位置可能存在一个潜在的物体。
-
RoI 池化用于提取固定大小的特征窗口。

图 7.11 Faster R-CNN 架构有两个主要组件:一个 RPN,用于识别可能包含感兴趣对象及其大致位置的区域,以及一个 Fast R-CNN 网络,用于分类对象并细化使用边界框定义的位置。这两个组件共享预训练的 VGG16 的卷积层。
然后将输出传递到两个全连接层:一个用于物体分类器,一个用于边界框坐标预测,以获得我们的最终定位。
该架构实现了一个端到端可训练的完整目标检测流程,其中所有必需的组件都在网络内部:
-
基础网络特征提取器
-
区域建议
-
RoI 池化
-
物体分类
-
边界框回归器
从基础网络提取特征
与 Fast R-CNN 类似,第一步是使用预训练的 CNN 并切掉其分类部分。基础网络用于从输入图像中提取特征。我们在第六章详细介绍了这一点。在这个组件中,你可以根据你试图解决的问题使用任何流行的 CNN 架构。原始 Faster R-CNN 论文使用了在 ImageNet 上预训练的 ZF4 和 VGG5 网络;但自那时起,已经出现了许多具有不同数量权重的不同网络。例如,MobileNet6,一个更小、更高效的针对速度优化的网络架构,大约有 330 万个参数,而 ResNet-152(152 层)--曾是 ImageNet 分类竞赛的顶尖技术--大约有 6000 万个。最近,新的架构如 DenseNet7 在提高结果的同时,也在减少参数数量。
VGGNet 与 ResNet
现在,ResNet 架构大多已取代 VGG 作为提取特征的基础网络。ResNet 相对于 VGG 的明显优势是它有更多的层(更深),这使它能够学习非常复杂的功能。这在分类任务中是正确的,在目标检测的情况下也应该同样正确。此外,ResNet 通过使用残差连接和批量归一化(这是在 VGG 首次发布时没有发明的)来训练深度模型变得容易。请回顾第五章,以获得不同 CNN 架构的更详细审查。
正如我们在前面的章节中学到的,每个卷积层都是基于之前的信息创建抽象。第一层通常学习边缘,第二层在边缘中寻找激活以形成更复杂形状的模式,依此类推。最终我们得到一个卷积特征图,可以输入到 RPN 中以提取包含对象的区域。
区域建议网络(RPN)
RPN 根据预训练卷积神经网络的最后一个特征图识别可能包含感兴趣对象的区域。RPN 也被称为注意力网络,因为它引导网络将注意力集中在图像中的有趣区域。Faster R-CNN 使用 RPN 将区域提议直接嵌入到 R-CNN 架构中,而不是运行选择性搜索算法来提取 RoIs。
RPN 的架构由两层组成(图 7.12):
-
一个具有 512 个通道的 3 × 3 全卷积层
-
两个平行的 1 × 1 卷积层:一个分类层,用于预测区域是否包含对象(其分数为背景或前景),以及一个用于回归或边界框预测的层。
全卷积网络(FCNs)
物体检测网络的一个重要方面是它们应该是全卷积的。全卷积神经网络意味着网络不包含任何全连接层,通常在网络末尾用于输出预测之前。
在图像分类的背景下,通过在整个体积上应用平均池化来移除全连接层,然后在使用单个密集 softmax 分类器输出最终预测之前。FCN 有两个主要优点:
-
它更快,因为它只包含卷积操作,没有全连接层。
-
它可以接受任何空间分辨率的图像(宽度和高度),只要图像和网络可以适应可用的内存。
作为 FCN,使网络对输入图像的大小不变。然而,在实践中,我们可能希望坚持一个固定的输入大小,因为只有在我们实现算法时才会出现的问题才会变得明显。这样一个重大问题是,如果我们想批量处理图像(因为批量的图像可以通过 GPU 并行处理,从而提高速度),所有图像都必须具有固定的高度和宽度。

图 7.12 RPN 架构的卷积实现,其中 k 是锚的数量
3 × 3 卷积层应用于基础网络的最后一个特征图,其中 3 × 3 大小的滑动窗口在特征图上移动。然后,输出被传递到两个 1 × 1 卷积层:一个分类器和边界框回归器。请注意,RPN 的分类器和回归器并不是试图预测对象及其边界框的类别;这将在 RPN 之后进行,即在 RPN 之后。记住,RPN 的目标是确定该区域是否有对象需要由全连接层之后进一步研究。在 RPN 中,我们使用二进制分类器来预测区域的物体得分,以确定该区域是前景(包含对象)还是背景(不包含对象)的概率。它基本上是查看该区域并问,“这个区域包含对象吗?”如果答案是肯定的,那么该区域将通过 RoI 池化和最终输出层进行进一步研究(见图 7.13)。

图 7.13 RPN 分类器预测目标得分,即图像包含对象(前景)或背景的概率。
回归器是如何预测边界框的?
要回答这个问题,我们首先定义边界框。它是围绕对象的框,由元组(x, y, w, h)标识,其中x和y是图像中的坐标,描述边界框的中心,h 和 w 是边界框的高度和宽度。研究人员发现,定义中心点的(x, y)坐标可能具有挑战性,因为我们必须强制执行一些规则以确保网络预测的值在图像边界内。相反,我们可以在图像中创建称为锚框的参考框,并使回归层预测从这些框的偏移量,称为 delta (Δx, Δy, Δw, Δh),以调整锚框以更好地适应对象并获得最终提议(见图 7.14)。

图 7.14 展示了从锚框预测 delta 偏移量和边界框坐标的说明
锚框
使用滑动窗口方法,RPN 为特征图中的每个位置生成 k 个区域。这些区域表示为锚框。锚位于其对应滑动窗口的中间,在尺寸和宽高比方面有所不同,以覆盖广泛的各种对象。它们是固定边界框,放置在整个图像中,用于在首次预测对象位置时作为参考。在其论文中,Ren 等人生成了九个锚框,它们都具有相同的中心,但具有三种不同的宽高比和三种不同的尺寸。
图 7.15 展示了锚框应用的一个示例。锚位于滑动窗口的中心;每个窗口有 k 个锚框,锚位于其中心。

图 7.15 锚点位于每个滑动窗口的中心。IoU 被计算出来以选择与真实值重叠最多的边界框。
训练 RPN
RPN 被训练来对锚框进行分类,输出物体存在分数,并近似物体的四个坐标(位置参数)。它使用人工标注员来标注边界框进行训练。标注的框被称为真实值。
对于每个锚框,计算重叠概率值(p),这表示这些锚框与真实值边界框重叠的程度:

如果一个锚框与真实值边界框的重叠度很高,那么它很可能包含感兴趣的对象,并且在与无对象分类任务中标记为正。同样,如果一个锚框与真实值边界框的重叠度很小,它将被标记为负。在训练过程中,正负锚框作为输入传递到两个全连接层,分别对应于锚框是否包含对象的分类以及位置参数(四个坐标)的回归。对应于位置的一个锚框数量(k),RPN 网络输出 2k 个分数和 4k 个坐标。例如,如果每个滑动窗口的锚框数量(k)是 9,那么 RPN 输出 18 个物体存在分数和 36 个位置坐标(图 7.16)。
RPN 作为一个独立的应用程序
RPN 可以用作独立的应用程序。例如,在只有一个对象类别的問題中,可以使用物体存在概率作为最终的类别概率。这是因为在这种情况下,前景意味着单个类别,而背景意味着不是单个类别。
你想要使用 RPN 处理单类检测等案例的原因是它在训练和预测过程中的速度提升。由于 RPN 是一个非常简单的网络,仅使用卷积层,其预测时间可以比使用分类基础网络更快。

图 7.16 区域建议网络
全连接层
输出全连接层接收两个输入:来自基础 ConvNet 的特征图和来自 RPN 的 RoIs。然后它对选定的区域进行分类,并输出它们的预测类别和边界框参数。Faster R-CNN 中的物体分类层使用 softmax 激活,而位置回归层使用线性回归来定义边界框的位置坐标。所有网络参数都使用多任务损失一起进行训练。
多任务损失函数
与 Fast R-CNN 类似,Faster R-CNN 针对一个多任务损失函数进行了优化,该函数结合了分类和边界框回归的损失:

初始时,损失方程可能看起来有些令人眼花缭乱,但实际上它比看起来要简单。理解它并不是运行和训练 Faster R-CNN 的必要条件,所以您可以自由地跳过这一部分。但我鼓励您努力理解这一解释,因为它将大大加深您对优化过程内部工作原理的理解。让我们首先看看符号;参见表 7.2。
多任务损失函数符号
| 符号 | 说明 |
|---|---|
| p[i] 和 p[i]^* | pi 是预测的锚点 (i) 是对象的概率和背景的概率,p*i 是锚点是否为对象的二元真实值(0 或 1)。 |
| t[i] 和 t[i]^* | t[i] 是定义边界框的四个预测参数,而 t[i]^* 是真实参数。 |
| N[cls] | 分类损失的归一化项。Ren 等人将其设置为约 256 个 mini-batch 的大小。 |
| N[loc] | 边界框回归的归一化项。Ren 等人将其设置为锚点位置的数量,约 2400。 |
| L[cls](*p[i], p[i]^**) | 两个类别的对数损失函数。我们可以通过预测样本是否为目标对象将多类分类轻松转换为二分类:L[cls](p[i], p[i]^**) = −p[i]^* log p[i] - (1 - p[i]^) log (1 − p[i]) |
| L1[smooth] | 如 7.2.2 节所述,边界框损失使用平滑 L1 损失函数来衡量预测和真实位置参数 (t[i], *t[i]^**) 之间的差异。它是一个鲁棒函数,据称比其他回归损失(如 L2)对异常值更不敏感。 |
| λ | 一个平衡参数,Ren 等人将其设置为约 10(因此 Lcls 和 Lloc 项大致同等权重)。 |
现在您已经知道了符号的定义,让我们再次尝试阅读多任务损失函数。为了帮助理解这个方程,暂时忽略归一化项和 (i) 项。以下是每个实例 (i) 的简化损失函数:
损失 = L[cls](p, p^*) + p^ · L1[smooth](t - t^**)
这个简化的函数是两个损失函数的和:分类损失和位置损失(边界框)。让我们逐一看看它们:
-
任何损失函数的想法都是从预测值中减去真实值以找到误差量。分类损失是第二章中解释的交叉熵函数。没有什么新的。它是一个对数损失函数,用于计算预测概率 (p) 和真实值 (*p, p^**) 之间的误差:
L[cls](p[i], p[i]^**) = −p[i]^** log p[i] − (1 − *p[i]^**) log (1 - p[i])
-
位置损失是使用平滑 L1 损失函数预测的位置参数(t[i],t[i]**)与真实位置参数之间的差异。然后将差异乘以包含对象的区域的地面真实概率*p**。如果不是对象,p^**为 0,以消除非对象区域的整个位置损失。
最后,我们将两个损失的值相加以创建多损失函数:
L = L[cls] + L[loc]
你看,这就是每个实例(i)的多损失函数。将(i)和σ符号放回以计算每个实例的损失总和。
7.2.4 R-CNN 家族回顾
表 7.3 总结了 R-CNN 架构的演变:
-
R-CNN -- 边界框由选择性搜索算法提出。每个边界框都被扭曲,并通过如 AlexNet 之类的深度卷积神经网络提取特征,然后使用线性 SVM 和线性回归器进行最终的一组对象分类和边界框预测。
-
Fast R-CNN -- 一种具有单个模型的单一代计设计。在 CNN 之后使用 RoI 池化层来巩固区域。该模型直接预测类别标签和 RoIs。
-
Faster R-CNN -- 一个完全端到端的深度学习目标检测器。它用区域提议网络替换选择性搜索算法来提议 RoIs,该网络解释从深度卷积神经网络中提取的特征,并学习直接提议 RoIs。
从 R-CNN 到 Fast R-CNN 再到 Faster R-CNN 的 CNN 网络家族的演变
| R-CNN | Fast R-CNN | Faster R-CNN | |
|---|---|---|---|
![]() |
![]() |
![]() |
|
| PASCAL 视觉对象类别挑战赛 2007 的 mAP | 66.0% | 66.9% | 66.9% |
| 特征 |
-
对每个图像应用选择性搜索以提取 RoIs(约 2,000 个)。
-
使用卷积神经网络从提取的约 2,000 个区域中提取特征。
-
使用分类和边界框预测。
| 每个图像只通过 CNN 一次,并提取特征图。
-
卷积神经网络(ConvNet)用于从输入图像中提取特征图。
-
在这些图上使用选择性搜索来生成预测。
这样,我们只需在整个图像上运行一个卷积神经网络,而不是在 2000 个重叠区域上运行约 2000 个卷积神经网络。 | 用区域提议网络替换选择性搜索方法,这使得算法速度更快。一个端到端的深度学习网络。|
| 局限性 | 计算时间高,因为每个区域都单独通过 CNN。此外,使用三个不同的模型进行预测。 | 选择性搜索慢,因此计算时间仍然很高。 | 对象提议需要时间。由于有不同系统依次工作,系统的性能取决于前一个系统的表现。 |
|---|---|---|---|
| 每个图像的测试时间 | 50 秒 | 2 秒 | 0.2 秒 |
| R-CNN 加速 | 1x | 25x | 250x |
R-CNN 局限性
如您可能已经注意到的,每一篇论文都提出了对 R-CNN 早期工作的改进,旨在开发一个更快的网络,目标是实现实时目标检测。通过这一系列工作所展示的成就确实令人惊叹,然而,这些架构中的任何一个都没有成功创建一个真正的实时目标检测器。不深入细节,以下这些问题已经与这些网络相关联:
-
训练数据难以处理,耗时过长。
-
训练发生在多个阶段(例如,训练区域提议与分类器)。
-
网络在推理时的速度太慢。
幸运的是,在过去的几年里,已经创建了新的架构来解决 R-CNN 及其后续产品的瓶颈,从而实现了实时目标检测。最著名的是单次检测器(SSD)和 YOLO(你只看一次),我们将在第 7.3 节和第 7.4 节中解释。
多阶段检测器与单阶段检测器
R-CNN 家族中的所有模型都是基于区域的。检测分为两个阶段,因此这些模型被称为两阶段检测器:
-
该模型提出了一组 RoIs,使用选择性搜索或 RPN。提出的区域是稀疏的,因为潜在的边界框候选者可能是无限的。
-
分类器只处理区域候选者。
单阶段检测器采取不同的方法。它们跳过了区域提议阶段,并在可能的密集采样位置上直接运行检测。这种方法更快、更简单,但可能会略微降低性能。在接下来的两个部分中,我们将检查 SSD 和 YOLO 单阶段目标检测器。一般来说,单阶段检测器通常比两阶段检测器精度低,但速度要快得多。
7.3 单次检测器(SSD)
SSD 论文由 Wei Liu 等人于 2016 年发布 8。SSD 网络在目标检测任务中的性能和精度方面达到了新的记录,在标准数据集如 PASCAL VOC 和 Microsoft COCO 上得分超过 74% mAP,在 59 FPS 的速度下运行。
测量检测器速度(FPS:每秒帧数)
如本章开头所讨论的,测量检测速度最常用的指标是每秒帧数。例如,Faster R-CNN 每秒只能运行 7 帧(FPS)。已经有很多尝试通过攻击检测管道的每个阶段来构建更快的检测器,但到目前为止,显著提高速度的代价是显著降低检测精度。在本节中,您将了解为什么像 SSD 这样的单阶段网络可以实现更快的检测,这更适合实时检测。
对于基准测试,SSD300 在 59 FPS 时实现了 74.3% mAP,而 SSD512 在 22 FPS 时实现了 76.8% mAP,这超过了 Faster R-CNN(在 7 FPS 时实现 73.2% mAP)。SSD300 指的是 300 × 300 大小的输入图像,而 SSD512 指的是 512 × 512 大小的输入图像。
我们之前了解到,R-CNN 系列是多阶段检测器:网络首先预测边界框的对象分数,然后将这个框通过分类器传递以预测类别概率。在单阶段检测器如 SSD 和 YOLO(在第 7.4 节中讨论)中,卷积层直接在一次操作中做出两种预测:因此得名单次检测器。图像只通过一次网络,每个边界框的对象分数使用逻辑回归预测,以指示与真实值的重叠程度。如果边界框与真实值重叠 100%,则对象分数为 1;如果没有重叠,则对象分数为 0。然后我们设置一个阈值值(0.5),即“如果对象分数高于 50%,则这个边界框很可能包含感兴趣的对象,我们得到预测。如果低于 50%,则忽略预测。”
7.3.1 高级 SSD 架构
SSD 方法基于一个前馈卷积网络,该网络生成固定大小的边界框和分数集合,用于预测这些框中对象类实例的存在,然后通过 NMS 步骤生成最终检测。SSD 模型的架构由三个主要部分组成:
-
基础网络提取特征图——用于高质量图像分类的标准预训练网络,在分类层之前被截断。在他们的论文中,Liu 等人使用了 VGG16 网络。其他网络如 VGG19 和 ResNet 也可以使用,并且应该产生良好的结果。
-
多尺度特征层——在基础网络之后添加了一系列卷积滤波器。这些层的大小逐渐减小,以允许预测多个尺度的检测。
-
非最大值抑制——NMS 用于消除重叠的框,并为每个检测到的对象保留一个框。
正如你在图 7.17 中可以看到,层 4_3、7、8_2、9_2、10_2 和 11_2 直接对 NMS 层做出预测。我们将在第 7.3.3 节中讨论为什么这些层的大小逐渐减小。现在,让我们继续了解 SSD 中的数据端到端流程。

图 7.17 SSD 架构由基础网络(VGG16)、用于对象检测的额外卷积层以及用于最终检测的非最大值抑制(NMS)层组成。注意,卷积层 7、8、9、10 和 11 直接做出预测,并将其直接输入到 NMS 层。来源:Liu 等人,2016 年。
你可以在图 7.17 中看到,网络对每个类别进行了总共 8,732 次检测,然后将其输入到 NMS 层以减少到每个对象一次检测。这个数字 8,732 是从哪里来的?
为了实现更精确的检测,特征图的各个层也通过一个小型的 3 × 3 卷积进行物体检测。例如,Conv4_3 的大小为 38 × 38 × 512,并应用了一个 3 × 3 的卷积。有四个边界框,每个边界框有(类别数量 + 4 个边界框值)个输出。假设有 20 个物体类别加上 1 个背景类别;那么输出边界框的数量是 38 × 38 × 4 = 5,776 个边界框。同样,我们计算其他卷积层的边界框数量:
-
Conv7: 19 × 19 × 6 = 2,166 个边界框(每个位置 6 个边界框)
-
Conv8_2: 10 × 10 × 6 = 600 个边界框(每个位置 6 个边界框)
-
Conv9_2: 5 × 5 × 6 = 150 个边界框(每个位置 6 个边界框)
-
Conv10_2: 3 × 3 × 4 = 36 个边界框(每个位置 4 个边界框)
-
Conv11_2: 1 × 1 × 4 = 4 个边界框(每个位置 4 个边界框)
如果我们将它们加起来,我们得到 5,776 + 2,166 + 600 + 150 + 36 + 4 = 8,732 个边界框产生。这对于我们的检测器来说是一个巨大的数字。这就是为什么我们应用 NMS 来减少输出边界框的数量。正如你将在 7.4 节中看到的,在 YOLO 中,最后有 7 × 7 个位置,每个位置有两个边界框:7 × 7 × 2 = 98 个边界框。
输出预测看起来是什么样子?
对于每个特征,网络预测以下内容:
-
描述边界框的 4 个值(x, y, w, h)
-
物体得分的 1 个值
-
表示每个类别概率的 C 值
这总共是 5 + C 个预测值。假设我们的问题中有四个物体类别。那么每个预测将是一个看起来像这样的向量:[x, y, w, h, 物体得分, C1, C2, C3, C4]。

当我们在问题中有四个类别时,输出预测的示例可视化。卷积层预测边界框坐标、物体得分和四个类别的概率:C1、C2、C3 和 C4。
现在,让我们更深入地探讨 SSD 架构的每个组件。
7.3.2 基础网络
如你在图 7.17 中看到的,SSD 架构在切除了全连接分类层(VGG16 在第五章中有详细解释)之后建立在 VGG16 架构之上。VGG16 被用作基础网络,因为它在高质量图像分类任务中表现出色,并且因其对迁移学习有帮助提高结果的问题而受到欢迎。与原始 VGG 全连接层不同,增加了一系列支持卷积层(从 Conv6 开始),使我们能够提取多个尺度的特征,并逐步减小每个后续层的输入大小。
下面是使用 Keras 在 SSD 中使用的 VGG16 网络的简化代码实现。你不需要从头开始实现这个;我包括这个代码片段的目标是向你展示这是一个典型的 VGG16 网络,就像第五章中实现的那样:
conv1_1 = Conv2D(64, (3, 3), activation='relu', padding='same')
conv1_2 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv1_1)
pool1 = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), padding='same')(conv1_2)
conv2_1 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool1)
conv2_2 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv2_1)
pool2 = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), padding='same')(conv2_2)
conv3_1 = Conv2D(256, (3, 3), activation='relu', padding='same')(pool2)
conv3_2 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv3_1)
conv3_3 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv3_2)
pool3 = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), padding='same')(conv3_3)
conv4_1 = Conv2D(512, (3, 3), activation='relu', padding='same')(pool3)
conv4_2 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv4_1)
conv4_3 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv4_2)
pool4 = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), padding='same')(conv4_3)
conv5_1 = Conv2D(512, (3, 3), activation='relu', padding='same')(pool4)
conv5_2 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv5_1)
conv5_3 = Conv2D(512, (3, 3), activation='relu', padding='same')(conv5_2)
pool5 = MaxPooling2D(pool_size=(3, 3), strides=(1, 1), padding='same')(conv5_3)
你在第五章中看到了 VGG16 在 Keras 中的实现。在这里添加这个的主要收获如下:
-
层 conv4_3 将再次用于直接预测。
-
层 pool5 将被输入到下一层(conv6),这是多尺度特征层的第一个层。
基础网络如何进行预测
考虑以下示例。假设你有图 7.18 中的图像,网络的任务是在图像中围绕所有船只绘制边界框。过程如下:
-
与 R-CNN 中的锚点概念类似,SSD 在图像周围叠加了一个锚点网格。对于每个锚点,网络在其中心创建一组边界框。在 SSD 中,锚点被称为先验。
-
基础网络将每个边界框视为一个单独的图像。对于每个边界框,网络会问:“这个框里有没有船?”或者换句话说,“我在这个框里提取了任何船只特征吗?”
-
当网络找到一个包含船只特征的边界框时,它会将其坐标预测和对象分类发送到 NMS 层。
-
NMS 消除了所有与真实边界框重叠最少的框。
注意:Liu 等人使用 VGG16 是因为它在复杂图像分类任务中表现出色。您可以使用其他网络,如更深的 VGG19 或 ResNet 作为基础网络,如果不在准确性上,应该表现得更好;但如果您选择实现更深的网络,它可能会更慢。如果您想要在复杂、高性能的深度网络和快速之间取得平衡,MobileNet 是一个不错的选择。
现在,让我们继续 SSD 架构的下一个组件:多尺度特征层。

图 7.18 SSD 基础网络查看锚点框以找到船只特征。实线框表示网络已找到船只特征。虚线框表示没有船只特征。
7.3.3 多尺度特征层
这些是添加到截断基础网络末尾的卷积特征层。这些层的大小逐渐减小,以便能够在多个尺度上进行检测预测。
多尺度检测
为了了解多尺度特征层的目标以及它们为什么大小不同,让我们看看图 7.19 中的马匹图像。如图所示,基础网络可能能够检测到背景中的马匹特征,但它可能无法检测到离相机最近的马匹。为了理解原因,仔细看看虚线边界框,并尝试想象这个框在完整图像之外(见图 7.20)。

图 7.19 图像中不同尺度的马匹。离相机远的马匹更容易检测,因为它们体积小,可以适应先验(锚点框)。基础网络可能无法检测到离相机最近的马匹,因为它需要不同尺度的锚点来创建能够覆盖更多可识别特征的先验。

图 7.20 一个孤立的马特征
你能在图 7.20 的边界框中看到马的特征吗?不。为了处理图像中不同尺度的对象,一些方法建议在不同大小上预处理图像,并在之后合并结果(图 7.21)。然而,通过使用不同大小的卷积层,我们可以在单个网络中使用来自几个不同层的特征图;对于预测,我们可以模仿相同的效果,同时在整个对象尺度上共享参数。随着 CNN 逐渐减少空间维度,特征图的分辨率也降低。SSD 使用低分辨率层来检测较大尺度的对象。例如,4 × 4 特征图用于较大尺度的对象。

图 7.21 低分辨率特征图检测较大尺度的对象(右侧);高分辨率特征图检测较小尺度的对象(左侧)。
为了可视化这一点,想象一下网络将图像维度减小,以便所有马都能在其边界框内(图 7.22)。多尺度特征层调整图像维度并保持边界框大小,以便它们可以适应较大的马。实际上,卷积层并不是字面上减小图像的大小;这只是为了说明,帮助我们直观地理解这个概念。图像不仅被调整大小,它实际上经过了卷积过程,因此看起来不再像自己。它将是一个完全随机的图像,但它将保留其特征。卷积过程在第三章中详细解释。

图 7.22 多尺度特征层减小输入图像的空间维度以检测不同尺度的对象。在这张图像中,你可以看到新的先验值有点放大,以覆盖靠近摄像头的马的可识别特征。
使用多尺度特征图可以显著提高网络精度。刘等人进行了一项实验,以衡量通过添加多尺度特征层所获得的优势。图 7.23 显示了随着层数减少,精度下降;你可以看到使用不同数量的特征图层进行对象检测时的精度。

图 7.23 使用原始论文中多个输出层的效果。当作者添加多尺度特征时,检测器的精度(mAP)提高。(来源:刘等人,2016。)
注意,当预测源来自所有六个层时,网络精度从 74.3%下降到 62.4%,对于单一源层。当仅使用 conv7 层进行预测时,性能最差,这强化了这样一个信息:在不同层上分布不同尺度的边界框是至关重要的。
多尺度层的架构
刘等人决定添加六个逐渐变小的卷积层。他们通过大量的调整和试错,直到产生最佳结果。正如你在图 7.17 中看到的,卷积层 6 和 7 非常简单。Conv6 的内核大小为 3 × 3,而 conv7 的内核大小为 1 × 1。另一方面,8 到 11 层更像是一系列块,其中每个块由两个内核大小为 1 × 1 和 3 × 3 的卷积层组成。
这是 Keras 中 6 到 11 层的代码实现(你可以在书中可下载的代码中看到完整的实现):
# conv6 and conv7
conv6 = Conv2D(1024, (3, 3), dilation_rate=(6, 6), activation='relu',
padding='same')(pool5)
conv7 = Conv2D(1024, (1, 1), activation='relu', padding='same')(conv6)
# conv8 block
conv8_1 = Conv2D(256, (1, 1), activation='relu', padding='same')(conv7)
conv8_2 = Conv2D(512, (3, 3), strides=(2, 2), activation='relu',
padding='valid')(conv8_1)
# conv9 block
conv9_1 = Conv2D(128, (1, 1), activation='relu', padding='same')(conv8_2)
conv9_2 = Conv2D(256, (3, 3), strides=(2, 2), activation='relu',
padding='valid')(conv9_1)
# conv10 block
conv10_1 = Conv2D(128, (1, 1), activation='relu', padding='same')(conv9_2)
conv10_2 = Conv2D(256, (3, 3), strides=(1, 1), activation='relu',
padding='valid')(conv10_1)
# conv11 block
conv11_1 = Conv2D(128, (1, 1), activation='relu', padding='same')(conv10_2)
conv11_2 = Conv2D(256, (3, 3), strides=(1, 1), activation='relu',
padding='valid')(conv11_1)
如前所述,如果你不在研究或学术界工作,你很可能不需要自己实现目标检测架构。在大多数情况下,你会下载一个开源实现,并在其基础上工作以解决你的问题。我只是添加了这些代码片段,帮助你内化关于不同层架构讨论的信息。
膨胀(或膨胀)卷积
膨胀卷积为卷积层引入了另一个参数:膨胀率。这定义了内核中值的间隔。具有膨胀率 2 的 3 × 3 内核具有与 5 × 5 内核相同的视野,而只使用九个参数。想象一下,取一个 5 × 5 内核,并删除每行的第二列和每列的第二行。
这在相同的计算成本下提供了更宽的视野。

具有膨胀率 2 的 3 × 3 内核具有与 5 × 5 内核相同的视野,而只使用九个参数。
膨胀卷积在实时分割领域特别受欢迎。如果你需要一个宽视野且无法承担多个卷积或更大的内核,请使用它们。
以下代码使用 Keras 构建了一个具有 2 倍膨胀率的 3 × 3 膨胀卷积层:
Conv2D(1024, (3, 3), dilation_rate=(2,2), activation='relu', padding='same')
接下来,我们讨论 SSD 架构的第三和最后一个组件:NMS。
7.3.4 非极大值抑制
在 SSD 在推理时每类生成的框数量很大,因此在使用 NMS 技术剪枝(本章前面已解释)时,剪枝大部分边界框至关重要。置信度损失和 IoU 小于某个阈值的框被丢弃,只保留前 N 个预测(图 7.24)。这确保了网络只保留最可能的预测,而噪声预测被移除。
SSD 是如何使用 NMS 来剪枝边界框的?SSD 按置信度分数对预测框进行排序。从置信度最高的预测开始,SSD 通过计算它们的 IoU 来评估是否存在任何先前预测的与同一类别的边界框重叠超过某个阈值。 (IoU 阈值是可调的。Liu 等人在他们的论文中选择了 0.45。) IoU 高于阈值的框被忽略,因为它们与置信度更高的另一个框重叠太多,因此它们最有可能检测到同一对象。每个图像最多保留 200 个预测。

图 7.24 非极大值抑制将每个对象的边界框数量减少到仅一个。
7.4 只看一次 (YOLO)
与 R-CNN 系列类似,YOLO 是由 Joseph Redmon 等人开发的目标检测网络系列,并通过以下版本逐年改进:
-
YOLOv1,于 2016 年发布 9--被称为“统一、实时目标检测”,因为它是一个统一的检测网络,统一了检测器的两个组件:目标检测器和类别预测器。
-
YOLOv2(也称为 YOLO9000),稍后于 2016 年发布 10--能够检测超过 9,000 个对象;因此得名。它已在 ImageNet 和 COCO 数据集上训练,并实现了 16%的 mAP,这并不好;但它在测试时非常快。
-
YOLOv3,于 2018 年发布 11--比先前模型大得多,并实现了 57.9%的 mAP,这是 YOLO 系列目标检测器中的最佳结果。
YOLO 系列是一系列端到端深度学习模型,专为快速目标检测而设计,并且是构建快速实时目标检测器的首次尝试之一。它是现有最快的目标检测算法之一。尽管模型的精度接近但不如 R-CNNs,但由于它们的检测速度,它们在目标检测中很受欢迎,通常在实时视频或相机输入中演示。
YOLO 的创造者采用了与先前网络不同的方法。YOLO 不经过与 R-CNNs 类似的区域提议步骤。相反,它通过将输入分割成单元格网格来仅预测有限数量的边界框;每个单元格直接预测一个边界框和对象分类。结果是大量候选边界框,这些边界框通过 NMS(图 7.25)合并成最终的预测。

图 7.25 YOLO 将图像分割成网格,预测每个网格的对象,然后使用 NMS 最终确定预测。
YOLOv1 提出了通用架构,YOLOv2 优化了设计并利用预定义的锚框来改进边界框建议,YOLOv3 进一步优化了模型架构和训练过程。在本节中,我们将重点关注 YOLOv3,因为它目前在 YOLO 家族中是最先进的架构。
7.4.1 YOLOv3 的工作原理
YOLO 网络将输入图像分割成 S × S 的网格。如果真实值框的中心落在单元格中,则该单元格负责检测该物体的存在。每个网格单元格预测 B 个边界框及其物体得分和类别预测,如下所示:
-
B 个边界框的坐标--与之前的检测器类似,YOLO 为每个边界框(b[x] , b[y] , b[w] , b[h])预测四个坐标,其中x和y被设置为单元格位置的偏移量。
-
物体得分 (P[0])--表示该单元格包含物体的概率。物体得分通过 sigmoid 函数转换为介于 0 和 1 之间的概率。物体得分的计算如下:
P[0] = P[r] (包含物体) × IoU (预测,真实)
-
类别预测--如果边界框包含物体,网络预测 K 个类别的概率,其中 K 是您问题中的类别总数。
需要注意的是,在 v3 之前,YOLO 使用 softmax 函数对类别得分进行计算。在 v3 中,Redmon 等人决定使用 sigmoid。原因是 softmax 假设每个框恰好有一个类别,这通常并不成立。换句话说,如果一个物体属于一个类别,那么它肯定不属于另一个类别。虽然这个假设对于某些数据集是正确的,但在我们处理像“女性”和“人”这样的类别时可能不起作用。多标签方法可以更准确地建模数据。

图 7.26 展示了将 13 × 13 网格应用于输入图像时的 YOLOv3 工作流程示例。输入图像被分割成 169 个单元格。每个单元格预测 B 个边界框及其物体得分,以及它们的类别预测。在这个例子中,我们展示了中心真实值单元格对 3 个边界框进行预测(B = 3)。每个预测具有以下属性:边界框坐标、物体得分和类别预测。
如图 7.26 所示,对于每个边界框 (b),预测结果如下:[(边界框坐标),(物体得分),(类别预测)]。我们已经了解到,边界框坐标是四个值加上一个物体得分值和 K 个类别预测值。因此,所有边界框预测的总值是 5B + K 乘以网格 S × S 中的单元格数:
总预测值 = S × S × (5B + K)
不同尺度的预测
仔细观察图 7.26。注意预测特征图有三个框。你可能想知道为什么有三个框。与 SSD 中的锚框概念类似,YOLOv3 有九个锚框,允许每个细胞在三个不同尺度上进行预测。检测层在具有步长 32、16 和 8 的三个不同大小的特征图上进行检测。这意味着对于大小为 416 × 416 的输入图像,我们在 13 × 13、26 × 26 和 52 × 52 的尺度上进行检测(图 7.27)。13 × 13 层负责检测大对象,26 × 26 层负责检测中等对象,52 × 52 层检测小对象。

图 7.27 不同尺度的预测特征图
这导致每个细胞预测三个边界框(B = 3)。这就是为什么在图 7.26 中,预测特征图正在预测框 1、框 2 和框 3。负责检测狗的边界框将是与真实框具有最高 IoU 的锚框。
注意:在不同层上的检测有助于解决 YOLOv2 中常见的检测小对象的问题。上采样层可以帮助网络保留和学会细粒度特征,这对于检测小对象至关重要。
网络通过下采样输入图像直到第一个检测层来实现这一点,在该层使用步长为 32 的特征图进行检测。进一步,层通过 2 倍上采样并与具有相同特征图大小的先前层的特征图连接。现在在步长为 16 的层进行另一个检测。相同的上采样过程被重复,并在步长为 8 的层进行最终检测。
YOLOv3 输出边界框
对于大小为 416 × 416 的输入图像,YOLO 预测 ((52 × 52) + (26 × 26) + 13 × 13)) × 3 = 10,647 个边界框。对于一个输出来说,这是一个巨大的框数量。在我们的狗示例中,我们只有一个对象。我们只想在这个对象周围有一个边界框。我们如何将框从 10,647 个减少到 1 个?
首先,我们根据它们的对象性分数过滤框。通常,分数低于阈值的框被忽略。其次,我们使用 NMS 来解决同一图像的多次检测问题。例如,图像中心突出网格细胞的三个边界框都可能检测到一个框,或者相邻的单元格可能检测到同一个对象。
7.4.2 YOLOv3 架构
现在你已经了解了 YOLO 的工作原理,那么通过其架构将会非常简单直接。YOLO 是一个将目标检测和分类统一到一个端到端网络的单一神经网络。神经网络架构受到了 GoogLeNet 模型(Inception)在特征提取方面的启发。YOLO 不是使用 Inception 模块,而是使用 1 × 1 降维层后跟 3 × 3 卷积层。Redmon 和 Farhadi 将这个称为 DarkNet(图 7.28)。

图 7.28 YOLO 的高级架构
YOLOv2 使用了一个定制的深度架构 darknet-19,一个原本 19 层的网络,增加了 11 个额外的层用于目标检测。具有 30 层架构的 YOLOv2 常常在小型目标检测上遇到困难。这归因于随着层对输入进行下采样而丢失了细粒度特征。然而,YOLOv2 的架构仍然缺少现在大多数最先进算法中稳定的一些最重要的元素:没有残差块,没有跳跃连接,没有上采样。YOLOv3 包含了所有这些更新。
YOLOv3 使用了名为 Darknet-53 的 DarkNet 变体(如图 7.29)。它有一个在 ImageNet 上训练的 53 层网络。为了检测任务,在其上又堆叠了 53 个额外的层,为 YOLOv3 提供了一个 106 层的全卷积底层架构。这就是 YOLOv3 相比 YOLOv2 慢的原因——但这也带来了检测精度的显著提升。

图 7.29 DarkNet-53 特征提取器架构。(来源:Redmon 和 Farhadi,2018。)
YOLOv3 的完整架构
我们刚刚了解到 YOLOv3 在三个不同的尺度上进行预测。当您看到完整的架构时,这会变得更加清晰,如图 7.30 所示。

图 7.30 YOLOv3 网络架构。(灵感来源于 Ayoosh Kathuria 在 Medium 上的帖子“YOLO v3 的新特性是什么?”2018 年,mng.bz/lGN2。)
输入图像通过 DarkNet-53 特征提取器,然后网络将图像下采样到第 79 层。网络分支并继续下采样图像,直到在第 82 层进行第一次预测。这种检测是在 13 × 13 的网格尺度上进行的,正如我们之前解释的那样,负责检测大型物体。
接下来,第 79 层的特征图上采样 2 倍到 26 × 26 的尺寸,并与第 61 层的特征图连接。然后,第 94 层在第 26 × 26 的网格尺度上进行第二次检测,负责检测中等物体。
最后,再次遵循类似的程序,第 91 层的特征图在经过少量上采样卷积层后,与第 36 层的特征图进行深度连接。第 106 层在第 52 × 52 的网格尺度上进行第三次预测,负责检测小型物体。
7.5 项目:在自动驾驶汽车应用中训练 SSD 网络
本项目的代码由 Pierluigi Ferrari 在他的 GitHub 仓库 (github.com/pierluigiferrari/ssd_keras) 创建。该项目已为本章改编;您可以在本书的可下载代码中找到此实现。
注意,对于这个项目,我们将构建一个名为 SSD7 的小型 SSD 网络。SSD7 是 SSD300 网络的七层版本。需要注意的是,虽然 SSD7 网络会产生一些可接受的结果,但这并不是一个优化的网络架构。目标是构建一个低复杂度的网络,足够快,以便您可以在个人计算机上训练。我在道路交通数据集上训练这个网络大约花费了 20 个小时;在 GPU 上训练可能需要的时间少得多。
注意 Pierluigi Ferrari 创建的原始仓库包含 SSD7、SSD300 和 SSD512 网络的实现教程。我鼓励您查看。
在这个项目中,我们将使用由 Udacity 创建的玩具数据集。您可以访问 Udacity 的 GitHub 仓库以获取有关数据集的更多信息(github.com/udacity/self-driving-car/tree/master/annotations)。该数据集包含超过 22,000 个标记的图像和 5 个对象类别:汽车、卡车、行人、骑自行车的人和交通灯。所有图像都已调整大小,高度为 300 像素,宽度为 480 像素。您可以将数据集作为本书代码的一部分下载。
注意 GitHub 数据仓库归 Udacity 所有,并且在此写作之后可能会更新。为了避免任何混淆,我下载了用于创建此项目的数据集,并将其与本书的代码一起提供,以便您可以在项目中复制这些结果。
使这个数据集非常有趣的是,这些是在加利福尼亚州山景城及其邻近城市白天驾驶时拍摄的真实时间图像。没有进行图像清理。请查看图 7.31 中的图像示例。

图 7.31 Udacity 自动驾驶数据集的示例图像(图像版权©2016 Udacity,在 MIT 许可证下发布。)
如 Udacity 页面所述,数据集由 CrowdAI 和 Autti 标记。您可以在文件夹中找到 CSV 格式的标签,分为三个文件:训练集、验证集和测试集。标记格式简单,如下所示:
| frame | xmin | xmax | ymin | ymax | class_id |
|---|---|---|---|---|---|
| 1478019952686311006.jpg | 237 | 251 | 143 | 155 | 1 |
Xmin、xmax、ymin 和 ymax 是边界框坐标。Class_id 是正确的标签,frame 是图像名称。
使用 LabelImg 进行数据标注
如果您正在标注自己的数据,有几个开源标注应用程序可供使用,例如 LabelImg (pypi.org/project/labelImg)。它们非常容易设置和使用。

使用 labelImg 应用程序标注图像的示例
7.5.1 步骤 1:构建模型
在开始模型训练之前,仔细查看keras_ssd7.py文件中的build_model方法。此文件使用 SSD 架构构建 Keras 模型。正如我们在本章前面所学,该模型由卷积特征层和多个从不同特征层获取输入的卷积预测层组成。
下面是build_model方法的样子。请阅读keras_ssd7.py文件中的注释以了解传递的参数:
def build_model(image_size,
mode='training',
l2_regularization=0.0,
min_scale=0.1,
max_scale=0.9,
scales=None,
aspect_ratios_global=[0.5, 1.0, 2.0],
aspect_ratios_per_layer=None,
two_boxes_for_ar1=True,
clip_boxes=False,
variances=[1.0, 1.0, 1.0, 1.0],
coords='centroids',
normalize_coords=False,
subtract_mean=None,
divide_by_stddev=None,
swap_channels=False,
confidence_thresh=0.01,
iou_threshold=0.45,
top_k=200,
nms_max_output_size=400,
return_predictor_sizes=False)
7.5.2 步骤 2:模型配置
在本节中,我们设置模型配置参数。首先,我们将高度、宽度和颜色通道数设置为模型接受的图像输入。如果您的输入图像的大小与这里定义的不同,或者如果您的图像大小不均匀,您必须使用数据生成器的图像变换(调整大小和/或裁剪),以确保在输入模型之前,您的图像达到所需的输入大小:
img_height = 300 ❶
img_width = 480 ❶
img_channels = 3 ❶
intensity_mean = 127.5 ❷
intensity_range = 127.5 ❷
❶ 输入图像的高度、宽度和通道数
❷ 设置为您的偏好(可能是 None)。当前设置将输入像素值转换为区间[-1,1]。
类别数量是您数据集中正类别的数量:例如,PASCAL VOC 为 20,COCO 为 80。类别 ID 0 必须始终保留为背景类别:
n_classes = 5 ❶
scales = [0.08, 0.16, 0.32, 0.64, 0.96] ❷
aspect_ratios = [0.5, 1.0, 2.0] ❸
steps = None ❹
offsets = None ❺
two_boxes_for_ar1 = True ❻
clip_boxes = False ❼
variances = [1.0, 1.0, 1.0, 1.0] ❽
normalize_coords = True ❾
❶ 我们数据集中的类别数量
❷ 明确的锚框缩放因子列表。如果传递了此参数,它将覆盖 min_scale 和 max_scale 参数。
❸ 锚框的纵横比列表
❹ 如果您想手动设置锚框网格的步长大小;不推荐
❺ 如果您想手动设置锚框网格的偏移量;不推荐
❻ 指定是否为纵横比 1 生成两个锚框
❼ 指定是否将锚框裁剪到图像边界内
❽ 通过编码目标坐标缩放的方差列表
❾ 指定模型是否应使用相对于图像大小的坐标
7.5.3 步骤 3:创建模型
现在我们调用build_model()函数来构建我们的模型:
model = build_model(image_size=(img_height, img_width, img_channels),
n_classes=n_classes,
mode='training',
l2_regularization=0.0005,
scales=scales,
aspect_ratios_global=aspect_ratios,
aspect_ratios_per_layer=None,
two_boxes_for_ar1=two_boxes_for_ar1,
steps=steps,
offsets=offsets,
clip_boxes=clip_boxes,
variances=variances,
normalize_coords=normalize_coords,
subtract_mean=intensity_mean,
divide_by_stddev=intensity_range)
您可以可选地加载保存的权重。如果您不想加载权重,请跳过以下代码片段:
model.load_weights('<path/to/model.h5>', by_name=True)
实例化一个 Adam 优化器和 SSD 损失函数,并编译模型。在这里,我们将使用一个名为SSDLoss的自定义 Keras 函数。它实现了多任务对数损失用于分类和光滑 L1 损失用于定位。neg_pos_ratio和alpha设置为 SSD 论文(Liu et al., 2016)中所述:
adam = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
ssd_loss = SSDLoss(neg_pos_ratio=3, alpha=1.0)
model.compile(optimizer=adam, loss=ssd_loss.compute_loss)
7.5.4 步骤 4:加载数据
要加载数据,请按照以下步骤操作:
-
实例化两个
DataGenerator对象——一个用于训练,一个用于验证:train_dataset = DataGenerator(load_images_into_memory=False, hdf5_dataset_path=None) val_dataset = DataGenerator(load_images_into_memory=False, hdf5_dataset_path=None) -
解析训练和验证数据集的图像和标签列表:
images_dir = 'path_to_downloaded_directory' train_labels_filename = 'path_to_dataset/labels_train.csv' ❶ val_labels_filename = 'path_to_dataset/labels_val.csv' train_dataset.parse_csv(images_dir=images_dir, labels_filename=train_labels_filename, input_format=['image_name', 'xmin', 'xmax', 'ymin', 'ymax', 'class_id'], include_classes='all') val_dataset.parse_csv(images_dir=images_dir, labels_filename=val_labels_filename, input_format=['image_name', 'xmin', 'xmax', 'ymin', 'ymax', 'class_id'], include_classes='all') train_dataset_size = train_dataset.get_dataset_size() ❷ val_dataset_size = val_dataset.get_dataset_size() ❷ print("Number of images in the training dataset:\t{:>6}".format(train_dataset_size)) print("Number of images in the validation dataset:\t{:>6}".format(val_dataset_size))❶ 真实值
❷ 获取训练和验证数据集的样本数量
此单元格应按如下方式打印出您的训练和验证数据集的大小:
Number of images in the training dataset: 18000 Number of images in the validation dataset: 4241 -
设置批量大小:
batch_size = 16如您在第四章所学,您可以根据用于此训练的硬件增加批处理大小以获得计算速度的提升。
-
定义数据增强过程:
data_augmentation_chain = DataAugmentationConstantInputSize( random_brightness=(-48, 48, 0.5), random_contrast=(0.5, 1.8, 0.5), random_saturation=(0.5, 1.8, 0.5), random_hue=(18, 0.5), random_flip=0.5, random_translate=((0.03,0.5), (0.03,0.5), 0.5), random_scale=(0.5, 2.0, 0.5), n_trials_max=3, clip_boxes=True, overlap_criterion='area', bounds_box_filter=(0.3, 1.0), bounds_validator=(0.5, 1.0), n_boxes_min=1, background=(0,0,0)) -
实例化一个编码器,可以将真实标签编码成 SSD 损失函数所需的格式。在这里,编码器构造函数需要模型预测层的空间维度来创建锚框:
predictor_sizes = [model.get_layer('classes4').output_shape[1:3], model.get_layer('classes5').output_shape[1:3], model.get_layer('classes6').output_shape[1:3], model.get_layer('classes7').output_shape[1:3]] ssd_input_encoder = SSDInputEncoder(img_height=img_height, img_width=img_width, n_classes=n_classes, predictor_sizes=predictor_sizes, scales=scales, aspect_ratios_global=aspect_ratios, two_boxes_for_ar1=two_boxes_for_ar1, steps=steps, offsets=offsets, clip_boxes=clip_boxes, variances=variances, matching_type='multi', pos_iou_threshold=0.5, neg_iou_limit=0.3, normalize_coords=normalize_coords) -
创建将传递给 Keras 的
fit_generator()函数的生成器处理程序:train_generator = train_dataset.generate(batch_size=batch_size, shuffle=True, transformations=[ data_augmentation_chain], label_encoder=ssd_input_encoder, returns={'processed_images', 'encoded_labels'}, keep_images_without_gt=False) val_generator = val_dataset.generate(batch_size=batch_size, shuffle=False, transformations=[], label_encoder=ssd_input_encoder, returns={'processed_images', 'encoded_labels'}, keep_images_without_gt=False)
7.5.5 步骤 5:训练模型
一切准备就绪,我们现在可以训练我们的 SSD7 网络了。我们已经选择了一个优化器和学习率,并设置了批处理大小;现在让我们设置剩余的训练参数并训练网络。这里没有新的参数是您之前没有学过的。我们将设置模型检查点、提前停止和学习率降低率:
model_checkpoint =
ModelCheckpoint(filepath='ssd7_epoch-{epoch:02d}_loss-{loss:.4f}_val_loss-{val_loss:.4f}.h5',
monitor='val_loss',
verbose=1,
save_best_only=True,
save_weights_only=False,
mode='auto',
period=1)
csv_logger = CSVLogger(filename='ssd7_training_log.csv',
separator=',',
append=True)
early_stopping = EarlyStopping(monitor='val_loss', ❶
min_delta=0.0,
patience=10,
verbose=1)
reduce_learning_rate = ReduceLROnPlateau(monitor='val_loss', ❷
factor=0.2,
patience=8,
verbose=1,
epsilon=0.001,
cooldown=0,
min_lr=0.00001)
callbacks = [model_checkpoint, csv_logger, early_stopping, reduce_learning_rate]
❶ 如果验证损失在 10 个连续的 epoch 中没有提高,则提前停止
❷ 当学习率达到平台期时的学习率降低率
将一个 epoch 设置为包含 1,000 个训练步骤。我在这里任意地将 epoch 数设置为 20。这并不一定意味着 20,000 个训练步骤是最佳数量。根据模型、数据集、学习率等因素,您可能需要更长时间(或更短)的训练才能达到收敛:
initial_epoch = 0 ❶
final_epoch = 20 ❶
steps_per_epoch = 1000
history = model.fit_generator(generator=train_generator, ❷
steps_per_epoch=steps_per_epoch,
epochs=final_epoch,
callbacks=callbacks,
validation_data=val_generator,
validation_steps=ceil( val_dataset_size/batch_size),
initial_epoch=initial_epoch)
❶ 如果您正在继续之前的训练,请相应地设置 initial_epoch 和 final_epoch。
❷ 开始训练
7.5.6 步骤 6:可视化损失
让我们可视化loss和val_loss值,看看训练和验证损失是如何演变的,并检查我们的训练是否朝着正确的方向进行(图 7.32):
plt.figure(figsize=(20,12))
plt.plot(history.history['loss'], label='loss')
plt.plot(history.history['val_loss'], label='val_loss')
plt.legend(loc='upper right', prop={'size': 24})

图 7.32 SSD7 训练 20 个 epoch 期间可视化的loss和val_loss值
7.5.7 步骤 7:进行预测
现在让我们使用训练好的模型在验证数据集上进行一些预测。为了方便,我们将使用我们已设置的验证生成器。您可以随意更改批处理大小:
predict_generator = val_dataset.generate(batch_size=1, ❶
shuffle=True,
transformations=[],
label_encoder=None,
returns={'processed_images',
'processed_labels',
'filenames'},
keep_images_without_gt=False)
batch_images, batch_labels, batch_filenames = next(predict_generator) ❷
y_pred = model.predict(batch_images) ❸
y_pred_decoded = decode_detections(y_pred, ❹
confidence_thresh=0.5,
iou_threshold=0.45,
top_k=200,
normalize_coords=normalize_coords,
img_height=img_height,
img_width=img_width)
np.set_printoptions(precision=2, suppress=True, linewidth=90)
print("Predicted boxes:\n")
print(' class conf xmin ymin xmax ymax')
print(y_pred_decoded[i])
❶ 1. 设置预测的生成器。
❷ 2. 生成样本。
❸ 3. 进行预测。
❹ 4. 解码原始预测 y_pred。
此代码片段打印出预测的边界框及其类别和每个边界框的置信度,如图 7.33 所示。

图 7.33 预测的边界框、置信度和类别
当我们将这些预测框绘制到图像上时,如图 7.34 所示,每个预测框旁边都有其置信度以及类别名称。真实框也被绘制到图像上进行比较。

图 7.34 将预测框绘制到图像上
摘要
-
图像分类是预测图像中对象类型或类别的任务。
-
目标检测是通过边界框预测图像中对象的位置以及定位对象的类别。
-
目标检测系统的一般框架包括四个主要组件:区域提议,特征提取和预测,非极大值抑制,以及评估指标。
-
目标检测算法使用两个主要指标进行评估:每秒帧数(FPS)来衡量网络的速率,以及平均精度均值(mAP)来衡量网络的精度。
-
最受欢迎的三种目标检测系统是 R-CNN 系列网络,SSD 和 YOLO 系列网络。
-
R-CNN 系列网络有三个主要变体:R-CNN,Fast R-CNN 和 Faster R-CNN。R-CNN 和 Fast R-CNN 使用选择性搜索算法来提议 RoIs,而 Faster R-CNN 是一个端到端深度学习系统,它使用区域提议网络来提议 RoIs。
-
YOLO 网络家族包括 YOLOv1,YOLOv2(或 YOLO9000)和 YOLOv3。
-
R-CNN 是一个多阶段检测器:它将预测边界框对象分数和对象类别的过程分为两个不同的阶段。
-
SSD 和 YOLO 是单阶段检测器:图像通过网络一次来预测对象分数和对象类别。
-
通常,单阶段检测器比双阶段检测器精度低,但速度要快得多。
-
罗斯·吉什克,杰夫·多纳休,特雷弗·达尔,和吉滕德拉·马利克, “用于准确目标检测和语义分割的丰富特征层次结构,” 2014,
arxiv.org/abs/1311.2524. -
罗斯·吉什克, “Fast R-CNN,” 2015,
arxiv.org/abs/1504.08083. -
邵庆庆,何凯明,罗斯·吉什克,和孙剑, “Faster R-CNN: 使用区域提议网络的实时目标检测,” 2016,
arxiv.org/abs/1506.01497. -
马修·D·泽勒和罗布·弗格森, “可视化与理解卷积网络,” 2013,
arxiv.org/abs/1311.2901. -
卡伦·西蒙扬和安德鲁·齐塞曼, “用于大规模图像识别的非常深的卷积网络,” 2014,
arxiv.org/abs/1409.1556. -
安德鲁·G·豪厄德,朱孟龙,陈波,卡列宁琴科·德米特里,王伟军,韦安德·托比亚斯,安德烈托·马可,亚当·哈特维格, “MobileNets: 用于移动视觉应用的效率卷积神经网络,” 2017,
arxiv.org/abs/1704.04861. -
高翔,刘壮,范德马滕·劳伦斯,和奎因伯格·基利安, “密集连接卷积网络,” 2016,
arxiv.org/abs/1608.06993. -
刘伟,安格洛夫·德拉戈米尔,厄尔汉·杜米特鲁,塞格迪·克里斯蒂安,里德·斯科特,傅成阳,亚历山大·C·伯格, “SSD: 单阶段多框检测器,” 2016,
arxiv.org/abs/1512.02325.
9.约瑟夫·雷德蒙、桑托什·迪瓦拉、罗斯·吉什克和阿里·法哈迪,“你只需看一次:统一、实时目标检测”,2016 年,arxiv.org/abs/1506.02640。
10.约瑟夫·雷德蒙和阿里·法哈迪,“YOLO9000:更好、更快、更强”,2016 年,arxiv.org/abs/1612.08242。
11.约瑟夫·雷德蒙和阿里·法哈迪,“YOLOv3:渐进式改进”,2018 年,arxiv.org/abs/1804.02767。
第三部分:生成模型和视觉嵌入
到目前为止,我们已经讨论了很多关于深度神经网络如何帮助我们理解图像特征并在其上执行确定性任务的内容,例如对象分类和检测。现在,我们将注意力转向计算机视觉和深度学习的一个不同且稍微高级的领域:生成模型。这些神经网络模型实际上创造了之前不存在的新内容——新人物、新物体、新的现实,就像魔法一样!我们使用特定领域的数据集来训练这些模型,然后它们会创建出看起来接近真实数据的、来自同一领域的新图像。在本部分书中,我们将涵盖训练和图像生成,同时还将探讨神经迁移和视觉嵌入领域的最新进展。
8 生成对抗网络 (GANs)
本章涵盖
-
理解 GANs 的基本组件:生成模型和判别模型
-
评估生成模型
-
了解 GANs 在流行视觉应用中的使用
-
构建 GAN 模型
生成对抗网络 (GANs) 是由蒙特利尔大学的 Ian Goodfellow 及其他研究人员,包括 Yoshua Bengio,于 2014 年引入的一种新型神经网络架构。1 Facebook 的 AI 研究总监 Yann LeCun 将 GANs 称为“过去 10 年机器学习中最有趣的想法”。这种兴奋是有充分理由的。GANs 最显著的特点是它们能够创建超逼真的图像、视频、音乐和文本。例如,除了最右侧的列外,图 8.1 右侧显示的所有面部都不是真实人类的;它们都是伪造的。图左侧的手写数字也是如此。这表明 GAN 能够从训练图像中学习特征,并使用其学到的模式想象出新的图像。

图 8.1 Goodfellow 及其合作者展示的 GANs 能力的说明。这些是在对两个数据集(MNIST 和多伦多面部数据集 [TFD])进行训练后由 GANs 生成的样本。在两种情况下,最右侧的列包含真实数据。这表明生成数据确实是生成的,而不仅仅是网络记忆的。 (来源:Goodfellow 等,2014 年。)
在过去的章节中,我们学习了如何使用深度神经网络来理解图像特征并在其上执行确定性任务,如对象分类和检测。在这本书的这一部分,我们将讨论计算机视觉世界中深度学习的另一种类型的应用:生成模型。这些是能够想象并产生以前未曾创造过的新内容的神经网络模型。它们能够以看似神奇的方式想象出新的世界、新的人和新的现实。我们通过提供特定领域的训练数据集来训练生成模型;它们的任务是创建看起来像真实数据的新对象图像。
很长一段时间以来,人类在计算机上拥有优势:想象和创造的能力。计算机在解决回归、分类和聚类等问题上表现出色。但随着生成网络的引入,研究人员可以让计算机生成与人类对手相同或更高质量的内容。通过学习模仿任何数据分布,计算机可以被教会创造与我们自己的世界相似的内容,无论在哪个领域:图像、音乐、语音、散文。从某种意义上说,它们是机器人艺术家,它们的产出令人印象深刻。GAN 也被视为实现通用人工智能(AGI)的重要垫脚石,这是一种能够匹配人类认知能力,在几乎任何领域获得专业知识的人工系统——从图像到语言,再到创作十四行诗所需的创造性技能。
自然地,这种生成新内容的能力使得 GAN 看起来有点像魔法,至少一开始是这样。在本章中,我们只尝试揭开 GAN 可能性的冰山一角。我们将克服 GAN 的表面魔法,深入探讨这些模型背后的架构思想和数学,以便提供必要的理论知识与实践技能,继续探索这个领域中最吸引你的任何方面。我们不仅将讨论 GAN 所依赖的基本概念,还将实现和训练一个端到端 GAN,并逐步进行。让我们开始吧!
8.1 GAN 架构
GAN 基于对抗性训练的理念。GAN 架构基本上由两个相互竞争的神经网络组成:
-
生成器试图将随机噪声转换为看起来像是来自原始数据集的观察值。
-
判别器试图预测一个观察值是否来自原始数据集或生成器的伪造品。
这种竞争性帮助他们模仿任何数据分布。我喜欢将 GAN 架构想象成两个拳击手在打架(图 8.2):在争取赢得比赛的过程中,他们都在学习对方的动作和技术。他们开始时对对手的了解较少,随着比赛的进行,他们不断学习和变得更好。

图 8.2 两个对抗性网络之间的战斗:生成性和判别性
另一个类比将有助于阐明这个观点:将 GAN 想象成猫捉老鼠游戏中伪造者和警察的对立面,其中伪造者正在学习传递假钞,而警察正在学习检测它们(图 8.3)。双方都是动态的:随着伪造者学会完美地制作假钞,警察正在训练并提高检测假币的能力。双方都在不断学习对方的方法,形成一种持续的升级。

图 8.3 GAN 的生成器和判别器模型就像是一个伪造者和一个警察。
如你在图 8.4 的架构图中所见,GAN 执行以下步骤:
-
生成器接收随机数字并返回一个图像。
-
这个生成的图像被输入到判别器中,同时还有从实际的真实数据集中提取的图像流。
-
判别器接收真实和虚假的图像,并返回概率:介于 0 和 1 之间的数字,其中 1 代表对真实性的预测,0 代表对虚假的预测。

图 8.4 GAN 架构由生成器和判别器网络组成。请注意,判别器网络是一个典型的卷积神经网络(CNN),其中卷积层的大小逐渐减小,直到达到展平层。另一方面,生成器网络是一个倒置的 CNN,它从展平的向量开始:卷积层的大小逐渐增加,直到形成输入图像的维度。
如果你仔细观察生成器和判别器网络,你会注意到生成器网络是一个倒置的卷积神经网络(ConvNet),它从展平的向量开始。图像被上采样,直到它们的大小与训练数据集中的图像相似。我们将在本章后面更深入地探讨生成器架构——我只是想让你现在注意到这个现象。
8.1.1 深度卷积 GANs (DCGANs)
在 2014 年的原始 GAN 论文中,多层感知器(MLP)网络被用来构建生成器和判别器网络。然而,从那时起,已经证明卷积层为判别器提供了更大的预测能力,这反过来又提高了生成器和整体模型的准确性。这种类型的 GAN 被称为深度卷积 GAN(DCGAN),由 Alec Radford 等人于 2016 年开发。2 现在,所有 GAN 架构都包含卷积层,因此当我们谈论 GAN 时,“DC”是隐含的;因此,在本章的其余部分,我们将 DCGAN 称为 GAN 和 DCGAN。你还可以回顾第二章和第三章,了解更多关于 MLP 和 CNN 网络之间的差异以及为什么 CNN 更适合图像问题。接下来,让我们更深入地探讨判别器和生成器网络的架构。
8.1.2 判别器模型
如前所述,判别器的目标是预测图像是真实的还是虚假的。这是一个典型的监督分类问题,因此我们可以使用我们在前几章中学到的传统分类器网络。该网络由堆叠的卷积层组成,后面跟着一个具有 sigmoid 激活函数的密集输出层。我们使用 sigmoid 激活函数,因为这是一个二元分类问题:网络的目的是输出介于 0 和 1 之间的预测概率值,其中 0 表示生成器生成的图像是虚假的,1 表示它是 100%真实的。
权衡器是一个正常、易于理解的分类模型。如图 8.5 所示,训练权衡器相当直接。我们向权衡器提供标记图像:伪造(或生成)的图像和真实图像。真实图像来自训练数据集,伪造图像是生成器模型的输出。

图 8.5 GAN 的权衡器
现在,让我们在 Keras 中实现权衡器网络。在本章末尾,我们将把所有代码片段组合起来构建一个端到端的 GAN。我们首先实现一个discriminator_model函数。在这个代码片段中,图像输入的形状是 28 × 28;你可以根据你的问题需要更改它:
def discriminator_model():
discriminator = Sequential() ❶
discriminator.add(Conv2D(32, kernel_size=3, strides=2,
input_shape=(28,28,1),padding="same")) ❷
discriminator.add(LeakyReLU(alpha=0.2)) ❸
discriminator.add(Dropout(0.25)) ❹
discriminator.add(Conv2D(64, kernel_size=3, strides=2, padding="same")) ❺
discriminator.add(ZeroPadding2D(padding=((0,1),(0,1)))) ❺
discriminator.add(BatchNormalization(momentum=0.8)) ❻
discriminator.add(LeakyReLU(alpha=0.2)) ❻
discriminator.add(Dropout(0.25)) ❻
discriminator.add(Conv2D(128, kernel_size=3, strides=2, padding="same")) ❼
discriminator.add(BatchNormalization(momentum=0.8)) ❼
discriminator.add(LeakyReLU(alpha=0.2)) ❼
discriminator.add(Dropout(0.25)) ❼
discriminator.add(Conv2D(256, kernel_size=3, strides=1, padding="same")) ❽
discriminator.add(BatchNormalization(momentum=0.8)) ❽
discriminator.add(LeakyReLU(alpha=0.2)) ❽
discriminator.add(Dropout(0.25)) ❽
discriminator.add(Flatten()) ❾
discriminator.add(Dense(1, activation='sigmoid')) ❾
discriminator.summary() ❿
img_shape = (28,28,1) ⓫
img = Input(shape=img_shape)
probability = discriminator(img) ⓬
return Model(img, probability) ⓭
❶ 实例化了一个序列模型,并将其命名为 discriminator
❷ 向权衡器模型添加了一个卷积层
❸ 添加了漏斗 ReLU 激活函数
❹ 添加了一个 dropout 层,dropout 概率为 25%
❺ 添加了一个带有零填充的第二卷积层
❻ 添加了一个批量归一化层以实现更快的学习和更高的精度
❼ 添加了第三个卷积层,包含批量归一化、漏斗 ReLU 和 dropout
❽ 添加了第四个卷积层,包含批量归一化、漏斗 ReLU 和 dropout
❾ 将网络展平,并添加了具有 sigmoid 激活函数的输出密集层
❿ 打印模型摘要
⓫ 设置了输入图像的形状
⓬ 运行权衡器模型以获取输出概率
⓭ 返回一个以图像为输入并产生概率输出的模型
权衡器模型的输出摘要如图 8.6 所示。正如你可能已经注意到的,没有什么新东西:权衡器模型遵循我们在第 3、4 和 5 章中学到的传统 CNN 网络的常规模式。我们堆叠卷积、批量归一化、激活和 dropout 层来创建我们的模型。所有这些层都有超参数,我们在训练网络时调整这些超参数。对于你的实现,你可以调整这些超参数,根据需要添加或删除层。CNN 超参数的调整在第 3 和第四章中有详细解释。

图 8.6 权衡器模型的输出摘要
在图 8.6 的输出摘要中,请注意输出特征图的宽度和高度在减小,而深度在增加。这是我们之前章节中看到的传统 CNN 网络的预期行为。让我们在下一节中看看生成器网络中的特征图大小会发生什么变化。
8.1.3 生成器模型
生成器接收一些随机数据,并试图模仿训练数据集以生成假图像。它的目标是试图生成与训练数据集完美匹配的图像来欺骗判别器。随着训练的进行,它在每次迭代后都会变得越来越好。但是判别器也在同时进行训练,因此生成器必须随着判别器学习其技巧而不断改进。

图 8.7 GAN 的生成器模型
如图 8.7 所示,生成器模型看起来像一个倒置的卷积神经网络。生成器接收一个包含一些随机噪声数据的向量输入,并将其重塑为一个具有宽度、高度和深度的立方体体积。这个体积被用来作为特征图,将被输入到几个卷积层中,以创建最终的图像。
上采样以缩放特征图
传统卷积神经网络使用池化层对输入图像进行下采样。为了缩放特征图,我们使用上采样层,通过重复输入像素的每一行和每一列来缩放图像维度。
Keras 有一个上采样层(Upsampling2D),它通过将一个缩放因子(size)作为参数来缩放图像维度:
keras.layers.UpSampling2D(size=(2, 2))
这行代码重复图像矩阵的每一行和每一列两次,因为缩放因子的尺寸设置为(2, 2);参见图 8.8。如果缩放因子是(3, 3),上采样层将输入矩阵的每一行和每一列重复三次,如图 8.9 所示。

图 8.8 当缩放大小为(2, 2)时的上采样示例

图 8.9 当缩放大小为(3, 3)时的上采样示例
当我们构建生成器模型时,我们会不断添加上采样层,直到特征图的大小与训练数据集相似。你将在下一节中看到如何在 Keras 中实现这一点。
现在,让我们构建generator_model函数,该函数构建生成器网络:
def generator_model():
generator = Sequential() ❶
generator.add(Dense(128 * 7 * 7, activation="relu", input_dim=100)) ❷
generator.add(Reshape((7, 7, 128))) ❸
generator.add(UpSampling2D(size=(2,2))) ❹
generator.add(Conv2D(128, kernel_size=3, padding="same")) ❺
generator.add(BatchNormalization(momentum=0.8)) ❺
generator.add(Activation("relu"))
generator.add(UpSampling2D(size=(2,2))) ❻
# convolutional + batch normalization layers
generator.add(Conv2D(64, kernel_size=3, padding="same")) ❼
generator.add(BatchNormalization(momentum=0.8)) ❼
generator.add(Activation("relu"))
# convolutional layer with filters = 1
generator.add(Conv2D(1, kernel_size=3, padding="same"))
generator.add(Activation("tanh"))
generator.summary() ❽
noise = Input(shape=(100,)) ❾
fake_image = generator(noise) ❿
return Model(noise, fake_image) ⓫
❶ 实例化一个序列模型并将其命名为 generator
❷ 添加一个具有 128 × 7 × 7 个神经元的密集层
❸ 将图像维度重塑为 7 × 7 × 128
❹ 上采样层将图像维度的大小加倍到 14 × 14
❺ 添加一个卷积层来运行卷积过程和批量归一化
❻ 将图像维度上采样到 28 × 28
❼ 我们在这里不添加上采样,因为 28 × 28 的图像大小与 MNIST 数据集中的图像大小相同。你可以根据你自己的问题进行调整。
❽ 打印模型摘要
❾ 生成长度为 100 的输入噪声向量。我们在这里使用 100 来创建一个简单的网络。
❿ 运行生成器模型以创建假图像
⓫ 返回一个模型,该模型以噪声向量为输入,并输出假图像
生成器模型的输出总结如图 8.10 所示。在代码片段中,唯一的新组件是 Upsampling 层,通过重复像素来加倍其输入维度。与判别器类似,我们在每一层卷积层之上堆叠其他优化层,如 BatchNormalization。生成器模型的关键区别在于它从扁平化的向量开始;图像被上采样,直到它们的尺寸与训练数据集相似。所有这些层都有超参数,我们在训练网络时调整这些超参数。对于你的实现,你可以根据需要调整这些超参数,添加或删除层。

图 8.10 生成器模型的输出总结
注意每一层输出形状的变化。它从一个包含 6,272 个神经元的 1D 向量开始。我们将其重塑为一个 7 × 7 × 128 的体积,然后宽度高度被上采样两次,变为 14 × 14,接着是 28 × 28。深度从 128 减少到 64,再减少到 1,因为此网络是为了处理我们将在本章后面实现的灰度 MNIST 数据集项目而构建的。如果你正在构建一个用于生成彩色图像的生成器模型,那么你应该将最后一层卷积层的过滤器设置为 3。
8.1.4 训练 GAN
现在我们已经分别学习了判别器和生成器模型,让我们将它们组合起来训练一个端到端的生成对抗网络。判别器正在被训练成为一个更好的分类器,以最大化将正确标签分配给训练示例(真实)和生成器生成的图像(伪造)的概率:例如,警察将更好地区分假币和真币。另一方面,生成器正在被训练成为一个更好的伪造者,以最大化欺骗判别器的机会。这两个网络都在它们所做的事情上变得更擅长。
训练 GAN 模型的过程涉及两个过程:
-
训练判别器。这是一个直接的监督训练过程。网络被提供了来自生成器(伪造)和训练数据(真实)的标记图像,并学习通过 sigmoid 预测输出区分真实和伪造图像。这里没有什么新内容。
-
训练生成器。这个过程有点棘手。生成器模型不能像判别器那样单独训练。它需要判别器模型来告诉它是否成功地伪造了图像。因此,我们创建了一个由判别器和生成器模型组成的组合网络来训练生成器。
将训练过程想象成两条平行的车道。一条车道单独训练判别器,另一条车道是训练生成器的组合模型。GAN 训练过程如图 8.11 所示。

图 8.11 训练 GAN 的流程图
正如你在图 8.11 中看到的,在训练组合模型时,我们冻结了判别器的权重,因为这个模型只专注于训练生成器。当我们解释生成器训练过程时,我们将讨论这个想法背后的直觉。现在,只需知道我们需要构建和训练两个模型:一个用于单独的判别器,另一个用于判别器和生成器模型。
这两个过程都遵循第二章中解释的传统神经网络训练过程。它从正向传播过程开始,然后进行预测,计算并反向传播错误。在训练判别器时,错误被反向传播回判别器模型以更新其权重;在组合模型中,错误被反向传播回生成器以更新其权重。
在训练迭代过程中,我们遵循与第二章中解释的传统神经网络训练过程相同的步骤,以观察网络的性能并调整其超参数,直到我们看到生成器在我们的问题上达到了令人满意的结果。这时,我们可以停止训练并部署生成器模型。现在,让我们看看我们如何编译判别器和组合网络来训练 GAN 模型。
训练判别器
如我们之前所述,这是一个简单直接的过程。首先,我们从本章早期创建的discriminator_model方法构建模型。然后我们编译模型,并使用你选择的optimizer(在这个例子中我们使用Adam)以及binary_crossentropy损失函数。
让我们看看 Keras 实现构建和编译生成器的代码。请注意,这个代码片段本身并不是为了独立编译而设计的——它在这里是为了说明。在本章末尾,你可以找到这个项目的完整代码:
discriminator = discriminator_model()
discriminator.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
我们可以通过使用 Keras 的train_on_batch方法创建随机训练批次来训练模型,对单个数据批次运行单个梯度更新:
noise = np.random.normal(0, 1, (batch_size, 100)) ❶
gen_imgs = generator.predict(noise) ❷
# Train the discriminator (real classified as ones and generated as zeros)
d_loss_real = discriminator.train_on_batch(imgs, valid)
d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
❶ 采样噪声
❷ 生成一批新的图像
训练生成器(组合模型)
在训练 GANs 时,有一个棘手的部分:训练生成器。虽然判别器可以在不与生成器模型分离的情况下进行训练,但生成器需要判别器才能进行训练。为此,我们构建了一个包含生成器和判别器的组合模型,如图 8.12 所示。

图 8.12 展示了包含生成器和判别器模型的组合模型示意图
当我们想要训练生成器时,我们冻结判别器模型的权重,因为生成器和判别器有不同的损失函数,将它们拉向不同的方向。如果我们不冻结判别器权重,它将被拉向生成器学习的同一方向,因此它更有可能将生成的图像预测为真实,这不是我们想要的结果。冻结判别器模型的权重不会影响我们在训练判别器时编译的现有判别器模型。将其想象为有两个判别器模型——这并不是实际情况,但这样更容易想象。
现在,让我们构建组合模型:
generator = generator_model() ❶
z = Input(shape=(100,)) ❷
image = generator(*z*) ❷
discriminator.trainable = False ❸
valid = discriminator(img) ❹
combined = Model(z, valid) ❺
❶ 构建生成器
❷ 生成器将噪声作为输入并生成一个图像。
❸ 冻结判别器模型的权重
❹ 判别器将生成的图像作为输入并确定其有效性。
❺ 组合模型(堆叠的生成器和判别器)训练生成器以欺骗判别器。
现在我们已经构建了组合模型,我们可以像往常一样进行训练过程。我们使用binary_crossentropy损失函数和 Adam 优化器编译组合模型:
combined.compile(loss='binary_crossentropy', optimizer=optimizer)
g_loss = self.combined.train_on_batch(noise, valid) ❶
❶ 训练生成器(希望判别器将图像误认为是真实的)
训练 epoch
在本章末尾的项目中,您将看到之前的代码片段被放入循环函数中,以执行一定数量的 epoch 的训练。对于每个 epoch,两个编译好的模型(判别器和组合)同时训练。在训练过程中,生成器和判别器都会得到提升。您可以通过在每个 epoch(或一组 epoch)后打印出结果来观察您的 GAN 的性能,看看生成器在生成合成图像方面的表现。图 8.13 展示了生成器在其训练过程中在 MNIST 数据集上性能演变的一个示例。

图 8.13 在其训练过程中,从第 0 个 epoch 到第 9,500 个 epoch,生成器在模仿 MNIST 数据集的手写数字方面变得越来越擅长。
在示例中,第 0 个 epoch 以随机噪声数据开始,这些数据尚未代表训练数据集中的特征。随着 GAN 模型经过训练,其生成器在创建高质量的训练数据集仿制品方面变得越来越擅长,这些仿制品可以欺骗判别器。手动观察生成器的性能是评估系统性能、决定 epoch 数量和何时停止训练的好方法。我们将在第 8.2 节中更详细地探讨 GAN 评估技术。
8.1.5 GAN 最小-最大函数
GAN 训练更像是一场零和游戏,而不是一个优化问题。在零和游戏中,总效用分数在玩家之间分配。一个玩家分数的增加会导致另一个玩家分数的减少。在人工智能中,这被称为最小-最大博弈论。最小-最大是一种决策算法,通常用于回合制、两人游戏。该算法的目标是找到最佳下一步。一个被称为最大化者的玩家努力获得最高分数;另一个被称为最小化者的玩家通过对抗最大化者来尝试获得最低分数。
GAN 在以下方程中玩一个最小-最大游戏,其中整个网络试图优化 V(D,G) 函数:

判别器(D)的目标是最大化获取图像正确标签的概率。另一方面,生成器(G)的目标是尽量减少被抓住的机会。因此,我们训练D来最大化将正确标签分配给训练示例和来自G的样本的概率。我们同时训练G来最小化 log(1 - D(G(z))).换句话说,D和G通过价值函数 V(D,G) 进行两人最小-最大游戏。
最小-最大博弈论
在两人零和游戏中,一个人只有在另一个玩家失败的情况下才能获胜。没有合作的可能性。这种博弈论在诸如井字棋、国际象棋、曼卡拉、象棋等游戏中被广泛使用。最大化玩家试图获得尽可能高的分数,而最小化玩家则试图做相反的事情,以获得尽可能低的分数。
在给定的游戏状态下,如果最大化者处于优势,那么分数将倾向于正值。如果最小化者在该状态下处于优势,那么分数将倾向于负值。这些值是通过为每种游戏类型独特的启发式算法计算得出的。
就像任何其他数学方程一样,前面的方程对于不熟悉其背后数学的人来说可能看起来令人恐惧,但它所代表的思想简单而强大。它只是判别器和生成器模型两个竞争目标的数学表示。让我们首先通过(表 8.1)了解符号,然后进行解释。
表 8.1 最小-最大方程中使用的符号
| 符号 | 说明 |
|---|---|
| G | 生成器。 |
| D | 判别器。 |
| z | 供给生成器(G)的随机噪声。 |
| G(z) | 生成器接收随机噪声数据(z)并尝试重建真实图像。 |
| D(G(z)) | 来自生成器的判别器(D)输出。 |
| logD(x) | 判别器对真实数据的概率输出。 |
判别器从两个来源获取输入:
-
生成器的数据,G(z)--这是假数据(z)。生成器输出的判别器表示为 D(G(z)).
-
来自真实训练数据(x)的真实输入--来自真实数据的判别器输出表示为 log D(x).
为了简化最小-最大方程,最好的方法是将其分解为两个部分:判别器训练函数和生成器训练(联合模型)函数。在训练过程中,我们创建了两个训练流程,每个流程都有自己的错误函数:
-
一个是单独的判别器,以下函数旨在通过使预测尽可能接近 1 来最大化最小-最大函数:
E[x ~p[data]] [logD(x)]
-
另一个是用于训练生成器的联合模型,以下函数旨在通过使预测尽可能接近 0 来最小化最小-最大函数:
E[z ~P z](z) [log(1 - D(G(z)))]
现在我们已经理解了方程符号,并对最小-最大函数的工作原理有了更好的理解,让我们再次看看这个函数:

最小-最大目标函数 V(D, G) 的目标是最大化从真实数据分布中的 D(x),并最小化从伪造数据分布中的 D(G(z))。为了实现这一点,我们在目标函数中使用 D(x) 的对数似然和 1 - D(z)。一个值的对数只是确保我们越接近一个错误值,我们受到的惩罚就越大。
在 GAN 训练过程的早期,判别器会以高置信度拒绝生成器产生的伪造数据,因为伪造的图像与真实训练数据非常不同——生成器还没有学会。随着我们训练判别器以最大化将正确标签分配给真实示例和生成器产生的伪造图像的概率,我们同时训练生成器以最小化生成伪造数据时的判别器分类错误。判别器希望最大化目标,使得对于真实数据 D(x) 接近 1,对于伪造数据 D(G(z)) 接近 0。另一方面,生成器希望最小化目标,使得 D(G(z)) 接近 1,这样判别器就会上当,认为生成的 G(z) 是真实的。当生成器产生的伪造数据被识别为真实数据时,我们停止训练。
8.2 评估 GAN 模型
用于分类和检测问题的深度学习神经网络模型通过损失函数进行训练,直到收敛。另一方面,GAN 生成器模型使用一个学习将图像分类为真实或生成的判别器进行训练。正如我们在上一节中学到的,生成器和判别器模型一起训练以保持平衡。因此,没有使用目标损失函数来训练 GAN 生成器模型,也没有办法仅从损失中客观地评估训练的进度和模型的相对或绝对质量。这意味着必须使用生成的合成图像的质量以及通过手动检查生成的图像来评估模型。
识别评估技术的一个好方法是回顾研究论文和作者用来评估他们 GAN 的技术。Tim Salimans 等人(2016)通过让人类标注员手动判断合成样本的视觉质量来评估他们的 GAN 性能。3 他们创建了一个网络界面,并在 Amazon Mechanical Turk (MTurk) 上雇佣标注员来区分生成数据和真实数据。
使用人类标注员的一个缺点是,该指标取决于任务的设置和标注员的动机。该团队还发现,当他们对标注员关于其错误的反馈时,结果发生了巨大变化:通过从这种反馈中学习,标注员能够更好地指出生成图像中的缺陷,从而给出更悲观的品质评估。
Salimans 等人和本节中我们将讨论的其他研究人员使用了其他非手动方法。一般来说,关于如何评估给定的 GAN 生成器模型还没有共识。这使得研究人员和实践者难以进行以下操作:
-
在训练运行中选择最佳的 GAN 生成器模型--换句话说,决定何时停止训练。
-
选择生成的图像来展示 GAN 生成器模型的性能。
-
比较和基准测试 GAN 模型架构。
-
调整模型超参数和配置,并比较结果。
寻找可量化的方法来理解 GAN 的进度和输出质量仍然是研究的一个活跃领域。已经开发了一套定性和定量技术,用于根据生成的合成图像的质量和多样性来评估 GAN 模型的性能。用于图像质量和多样性的两个常用评估指标是 Inception 分数和 Fréchet Inception 距离 (FID)。在本节中,你将了解基于生成的合成图像评估 GAN 模型的技术。
8.2.1 Inception 分数
Inception 分数基于一个启发式方法,即现实样本应该能够在通过预训练网络(如 ImageNet 上的 Inception)时被分类(因此得名 Inception 分数)。这个想法真的很简单。启发式依赖于两个值:
-
生成的图像高可预测性 -- 我们将预训练的 Inception 分类器模型应用于每个生成的图像,并获取其 softmax 预测。如果生成的图像足够好,那么它应该给出一个高可预测性分数。
-
生成样本的多样性 -- 不应有任何类别主导生成的图像分布。
使用模型对大量生成的图像进行分类。具体来说,预测图像属于每个类的概率。然后将这些概率总结到分数中,以捕捉每个图像看起来像已知类别的程度以及图像集合在已知类别之间的多样性。如果这两个特性都满足,应该有一个较大的 inception score。较高的 inception score 表示生成的图像质量更好。
8.2.2 Fréchet inception 距离 (FID)
FID 分数是由 Martin Heusel 等人在 2017 年提出并使用的。4 该分数被提出作为对现有 inception score 的改进。
与 inception score 类似,FID 分数使用 Inception 模型来捕捉输入图像的特定特征。这些激活值是针对一组真实和生成的图像计算的。每个真实和生成的图像的激活值被总结为一个多元高斯分布,然后使用 Fréchet 距离(也称为 Wasserstein-2 距离)来计算这两个分布之间的距离。
重要的一点是,FID 需要一个合理的样本量才能给出良好的结果(建议的大小是 50,000 个样本)。如果你使用太多的样本,你最终会高估你的实际 FID,并且估计值会有很大的方差。较低的 FID 分数表示更符合真实图像统计特性的真实图像。
8.2.3 要使用哪种评估方案
这两种度量(inception score 和 FID)都很容易在生成的图像批次上实现和计算。因此,在训练过程中系统地生成图像并保存模型的做法可以,并且应该继续使用,以便进行事后模型选择。深入探讨 inception score 和 FID 超出了本书的范围。如前所述,这是一个活跃的研究领域,截至写作时,行业内还没有关于评估 GAN 性能的最佳方法的共识。不同的分数评估图像生成过程的各个方面,而且不太可能有一个单一的分数可以涵盖所有方面。本节的目标是让你了解近年来开发的一些自动化 GAN 评估过程的技巧,但手动评估仍然被广泛使用。
当你刚开始时,从手动检查生成的图像开始以评估和选择生成器模型是一个好主意。对于初学者和专家来说,开发 GAN 模型本身就足够复杂了;手动检查可以在改进你的模型实现和测试模型配置方面让你走得很远。
其他研究人员通过使用特定领域的评估指标采取了不同的方法。例如,Konstantin Shmelkov 及其团队(2018 年)使用了基于图像分类的两个度量,GAN-train 和 GAN-test,分别近似 GAN 的召回率(多样性)和精确度(图像质量)。5
8.3 流行 GAN 应用
在过去的五年里,生成模型已经取得了长足的进步。该领域已经发展到预期下一代生成模型在创作艺术方面将比人类更加得心应手。现在,GANs(生成对抗网络)已经拥有了解决医疗保健、汽车、美术等多个行业问题的能力。在本节中,我们将了解一些对抗网络的用例以及用于该应用的 GAN 架构。本节的目标不是实现 GAN 网络的变体,而是提供一些关于 GAN 模型潜在应用的曝光以及进一步阅读的资源。
8.3.1 文本到照片合成
从文本描述合成高质量图像是计算机视觉中的一个挑战性问题。现有文本到图像方法生成的样本可以大致反映给定描述的意义,但它们缺乏必要的细节和生动的物体部分。
为此应用构建的 GAN 网络是堆叠生成对抗网络(StackGAN)。6 张等人能够根据文本描述生成 256 × 256 的逼真图像。
StackGANs 在两个阶段工作(图 8.14):
-
第一阶段:StackGAN 根据给定的文本描述绘制对象的原始形状和颜色,生成低分辨率图像。
-
第二阶段:StackGAN 以第一阶段的结果和文本描述作为输入,生成具有逼真细节的高分辨率图像。它能够纠正第一阶段创建的图像中的缺陷,并通过细化过程添加引人注目的细节。

图 8.14 (a) 第一阶段:给定文本描述,StackGAN 绘制对象的粗糙形状和基本颜色,生成低分辨率图像。(b) 第二阶段以第一阶段的结果和文本描述作为输入,生成具有逼真细节的高分辨率图像。(来源:张等,2016 年。)
8.3.2 图像到图像翻译(Pix2Pix GAN)
图像到图像的转换被定义为在足够的训练数据下将场景的一种表示转换为另一种表示。它受到语言翻译类比的影响:正如一个想法可以用许多不同的语言表达一样,一个场景可以通过灰度图像、RGB 图像、语义标签图、边缘草图等来呈现。在图 8.15 中,图像到图像的转换任务在一系列应用中得到了演示,例如将街景分割标签转换为真实图像、灰度转换为彩色图像、产品草图转换为产品照片以及白天照片转换为夜晚照片。
Pix2Pix 是由 Phillip Isola 等人于 2016 年设计的 GAN 家族成员,用于通用图像到图像的转换。7 Pix2Pix 网络架构类似于 GAN 的概念:它包括一个生成器模型,用于输出看起来逼真的新合成图像,以及一个判别器模型,用于将图像分类为真实(来自数据集)或伪造(生成)。训练过程也与 GAN 类似:判别器模型直接更新,而生成器模型通过判别器模型更新。因此,这两个模型在对抗过程中同时训练,其中生成器试图更好地欺骗判别器,而判别器试图更好地识别伪造图像。

图 8.15 从原始论文中摘取的 Pix2Pix 应用示例。
Pix2Pix 网络的新颖之处在于,它们学习一个适应当前任务和数据的损失函数,这使得它们可以在各种环境中应用。它们是一种条件生成对抗网络(cGAN),其中输出图像的生成取决于输入源图像。判别器被提供了源图像和目标图像,并必须确定目标是否是源图像的合理变换。
Pix2Pix 网络在许多图像到图像的转换任务中取得了非常令人鼓舞的结果。访问affinelayer.com/pixsrv来更多体验 Pix2Pix 网络;该网站有一个由 Isola 及其团队创建的交互式演示,您可以在其中将猫或产品的草图边缘转换为照片和立面转换为真实图像。
8.3.3 图像超分辨率 GAN(SRGAN)
一种特定的 GAN 模型可以用来将低分辨率图像转换为高分辨率图像。这种类型被称为超分辨率生成对抗网络(SRGAN),由 Christian Ledig 等人于 2016 年引入。8 图 8.16 展示了 SRGAN 如何创建一个非常高分辨率的图像。

图 8.16 SRGAN 将低分辨率图像转换为高分辨率图像。(来源:Ledig 等人,2016 年。)
8.3.4 准备好动手实践了吗?
GAN 模型在创造和想象从未存在过的新现实方面具有巨大的潜力。本章中提到的应用只是几个例子,以给你一个关于 GAN 今天能做什么的印象。这样的应用每隔几周就会出现,值得一试。如果你对探索更多 GAN 应用感兴趣,请访问由 Erik Linder-Norén 维护的惊人的 Keras-GAN 存储库github.com/eriklindernoren/Keras-GAN,它包括许多使用 Keras 创建的 GAN 模型,是 Keras 示例的极好资源。本章中的大部分代码都受到了这个存储库的启发和改编。
8.4 项目:构建自己的 GAN
在这个项目中,你将使用生成器和判别器中的卷积层构建一个 GAN。这被称为深度卷积 GAN(DCGAN)。DCGAN 架构最初由 Alec Radford 等人(2016 年)探索,如第 8.1.1 节所述,并在生成新图像方面取得了令人印象深刻的成果。你可以跟随本章中的实现或运行本书可下载代码中的项目笔记本中的代码。
在这个项目中,你将在 Fashion-MNIST 数据集上训练 DCGAN(github.com/zalandoresearch/fashion-mnist)。Fashion-MNIST 包含 60,000 个用于训练的灰度图像和 10,000 个图像的测试集(图 8.17)。每个 28 × 28 的灰度图像都与 10 个类别中的一个标签相关联。Fashion-MNIST 旨在作为原始 MNIST 数据集的直接替代品,用于基准测试机器学习算法。我选择灰度图像进行这个项目,因为与三通道彩色图像相比,在单通道灰度图像上训练卷积网络所需的计算能力更少,这使得在没有 GPU 的个人计算机上训练更容易。

图 8.17 Fashion-MNIST 数据集示例
数据集被划分为 10 个时尚类别。类别标签如下:
| 标签 | 描述 |
|---|---|
| 0 | T 恤/上衣 |
| 1 | 裤子 |
| 2 | 针织衫 |
| 3 | 晚礼服 |
| 4 | 外套 |
| 5 | 凉鞋 |
| 6 | 衬衫 |
| 7 | 运动鞋 |
| 8 | 背包 |
| 9 | 踝靴 |
第 1 步:导入库
和往常一样,首先要做的是导入我们在项目中使用的所有库:
from __future__ import print_function, division
from keras.datasets import fashion_mnist ❶
from keras.layers import Input, Dense, Reshape, Flatten, Dropout ❷
from keras.layers import BatchNormalization, Activation, ZeroPadding2D ❷
from keras.layers.advanced_activations import LeakyReLU ❷
from keras.layers.convolutional import UpSampling2D, Conv2D ❷
from keras.models import Sequential, Model ❷
from keras.optimizers import Adam ❷
import numpy as np ❸
import matplotlib.pyplot as plt ❸
❶ 从 Keras 导入 fashion_mnist 数据集
❷ 导入 Keras 层和模型
❸ 导入 numpy 和 matplotlib
第 2 步:下载并可视化数据集
Keras 使我们能够通过一条命令:fashion_mnist.load_data()下载 Fashion-MNIST 数据集。在这里,我们下载了数据集并将训练集重新缩放到-1 到 1 的范围,以便模型更快地收敛(有关图像缩放的更多详细信息,请参阅第四章中的“数据归一化”部分):
(training_data, _), (_, _) = fashion_mnist.load_data() ❶
X_train = training_data / 127.5 - 1\. ❷
X_train = np.expand_dims(X_train, axis=3) ❷
❶ 加载数据集
❷ 将训练数据重新缩放到-1 到 1 的范围
为了乐趣,让我们可视化图像矩阵(图 8.18):
def visualize_input(img, ax):
ax.imshow(img, cmap='gray')
width, height = img.shape
thresh = img.max()/2.5
for *x* in range(width):
for *y* in range(height):
ax.annotate(str(round(img[x][y],2)), xy=(y,x),
horizontalalignment='center',
verticalalignment='center',
color='white' if img[x][y]<thresh else 'black')
fig = plt.figure(figsize = (12,12))
ax = fig.add_subplot(111)
visualize_input(training_data[3343], ax)

图 8.18 Fashion-MNIST 数据集的可视化示例
第 3 步:构建生成器
现在,让我们构建生成器模型。输入将是我们在第 8.1.5 节中解释的噪声向量(z)。生成器架构如图 8.19 所示。

图 8.19 生成器模型的架构
第一层是一个全连接层,然后将其重塑为一个深而窄的层,类似于 7 × 7 × 128(在原始 DCGAN 论文中,团队将输入重塑为 4 × 4 × 1024)。然后我们使用上采样层将特征图维度从 7 × 7 加倍到 14 × 14,然后再加倍到 28 × 28。在这个网络中,我们使用三个卷积层。我们还使用了批量归一化和 ReLU 激活。对于这些层中的每一层,一般的方案是卷积⇒批量归一化⇒ReLU。现在,让我们堆叠这些层,直到我们得到形状为 28 × 28 × 1 的最终转置卷积层:
def build_generator():
generator = Sequential() ❶
generator.add(Dense(128 * 7 * 7, activation="relu", input_dim=100)) ❷
generator.add(Reshape((7, 7, 128))) ❸
generator.add(UpSampling2D()) ❹
generator.add(Conv2D(128, kernel_size=3, padding="same", ❺
activation="relu")) ❺
generator.add(BatchNormalization(momentum=0.8)) ❺
generator.add(UpSampling2D()) ❻
# convolutional + batch normalization layers
generator.add(Conv2D(64, kernel_size=3, padding="same", ❼
activation="relu")) ❼
generator.add(BatchNormalization(momentum=0.8)) ❼
# convolutional layer with filters = 1
generator.add(Conv2D(1, kernel_size=3, padding="same",
activation="relu"))
generator.summary() ❽
noise = Input(shape=(100,)) ❾
fake_image = generator(noise) ❿
return Model(inputs=noise, outputs=fake_image) ⓫
❶ 实例化一个序列模型并将其命名为生成器
❷ 添加一个密集层,其神经元数量为 128 × 7 × 7
❸ 将图像尺寸重塑为 7 × 7 × 128
❹ 上采样层将图像尺寸加倍到 14 × 14
❺ 添加一个卷积层以运行卷积过程和批量归一化
❻ 将图像尺寸上采样到 28 × 28
❼ 我们在这里不添加上采样,因为 28 × 28 的图像大小与 MNIST 数据集中的图像大小相同。你可以根据你自己的问题进行调整。
❽ 打印模型摘要
❾ 生成长度为 100 的输入噪声向量。我们在这里选择 100 是为了创建一个简单的网络。
❿ 运行生成器模型以创建假图像
⓫ 返回一个模型,该模型以噪声向量作为输入并输出假图像
第 4 步:构建判别器
判别器只是一个卷积分类器,就像我们之前构建的那样(图 8.20)。判别器的输入是 28 × 28 × 1 的图像。我们想要几个卷积层,然后是一个全连接层作为输出。和之前一样,我们想要一个 sigmoid 输出,并且需要返回 logits。对于卷积层的深度,我建议从第一层的 32 或 64 个过滤器开始,然后随着层数的增加,深度加倍。在这个实现中,我们开始使用 64 层,然后是 128 层,然后是 256 层。对于下采样,我们不使用池化层。相反,我们只使用步长卷积层进行下采样,类似于 Radford 等人实现的方式。

图 8.20 判别器模型的架构
我们还使用了批量归一化和 dropout 来优化训练,正如我们在第四章中学到的。对于四个卷积层中的每一个,一般的方案是卷积⇒批量归一化⇒漏 ReLU。现在,让我们构建build_discriminator函数:
def build_discriminator():
discriminator = Sequential() ❶
discriminator.add(Conv2D(32, kernel_size=3, strides=2,
input_shape=(28,28,1), padding="same")) ❷
discriminator.add(LeakyReLU(alpha=0.2)) ❸
discriminator.add(Dropout(0.25)) ❹
discriminator.add(Conv2D(64, kernel_size=3, strides=2,
padding="same")) ❺
discriminator.add(ZeroPadding2D(padding=((0,1),(0,1)))) ❻
discriminator.add(BatchNormalization(momentum=0.8)) ❼
discriminator.add(LeakyReLU(alpha=0.2))
discriminator.add(Dropout(0.25))
discriminator.add(Conv2D(128, kernel_size=3, strides=2, padding="same")) ❽
discriminator.add(BatchNormalization(momentum=0.8)) ❽
discriminator.add(LeakyReLU(alpha=0.2)) ❽
discriminator.add(Dropout(0.25)) ❽
discriminator.add(Conv2D(256, kernel_size=3, strides=1, padding="same")) ❾
discriminator.add(BatchNormalization(momentum=0.8)) ❾
discriminator.add(LeakyReLU(alpha=0.2)) ❾
discriminator.add(Dropout(0.25)) ❾
discriminator.add(Flatten()) ❿
discriminator.add(Dense(1, activation='sigmoid')) ❿
img = Input(shape=(28,28,1)) ⓫
probability = discriminator(img) ⓬
return Model(inputs=img, outputs=probability) ⓭
❶ 实例化一个序列模型并将其命名为判别器
❷ 将卷积层添加到判别器模型中
❸ 添加一个带有泄漏 ReLU 激活函数的泄漏 ReLU 激活函数
❹ 添加一个具有 25% dropout 概率的 dropout 层
❺ 添加一个具有零填充的第二卷积层
❻ 添加一个零填充层以将维度从 7 × 7 更改为 8 × 8
❼ 添加一个批量归一化层以加快学习速度并提高准确性
❽ 添加具有批量归一化、泄漏 ReLU 和 dropout 的第三卷积层
❾ 添加具有批量归一化、泄漏 ReLU 和 dropout 的第四卷积层
❿ 将网络展平并添加具有 sigmoid 激活函数的输出密集层
⓫ 设置输入图像形状
⓬ 运行判别器模型以获取输出概率
⓭ 返回一个以图像作为输入并产生概率输出的模型
第 5 步:构建联合模型
如第 8.1.3 节所述,为了训练生成器,我们需要构建一个包含生成器和判别器的联合网络(图 8.21)。联合模型将噪声信号(z)作为输入并输出判别器的预测输出,作为伪造或真实。

图 8.21 联合模型架构
记住,我们希望禁用联合模型中的判别器训练,如第 8.1.3 节中详细解释的那样。在训练生成器时,我们不希望判别器更新权重,但仍然希望将判别器模型包含在生成器训练中。因此,我们创建了一个包含两个模型的联合网络,但在联合网络中冻结了判别器模型的权重:
optimizer = Adam(learning_rate=0.0002, beta_1=0.5) ❶
discriminator = build_discriminator() ❷
discriminator.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
discriminator.trainable = False ❸
# Build the generator
generator = build_generator() ❹
z = Input(shape=(100,)) ❺
img = generator(*z*) ❺
valid = discriminator(img) ❻
combined = Model(inputs=z, outputs=valid) ❼
combined.compile(loss='binary_crossentropy', optimizer=optimizer) ❼
❶ 定义优化器
❷ 构建并编译判别器
❸ 冻结判别器权重,因为我们不想在生成器训练期间训练它
❹ 构建生成器
❺ 生成器以 latent_dim = 100 的噪声作为输入并生成图像。
❻ 判别器将生成的图像作为输入并确定其有效性。
❼ 联合模型(堆叠的生成器和判别器)训练生成器以欺骗判别器。
第 6 步:构建训练函数
在训练 GAN 模型时,我们训练两个网络:判别器和我们在上一节中创建的联合网络。让我们构建一个 train 函数,它接受以下参数:
-
训练的轮数
-
批量大小
-
save_interval来指定我们希望保存结果的频率
def train(epochs, batch_size=128, save_interval=50):
valid = np.ones((batch_size, 1)) ❶
fake = np.zeros((batch_size, 1)) ❶
for epoch in range(epochs):
## Train Discriminator network
idx = np.random.randint(0, X_train.shape[0], batch_size) ❷
imgs = X_train[idx] ❷
noise = np.random.normal(0, 1, (batch_size, 100)) ❸
gen_imgs = generator.predict(noise) ❸
d_loss_real = discriminator.train_on_batch(imgs, valid) ❹
d_loss_fake = discriminator.train_on_batch(gen_imgs, fake) ❹
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake) ❹
## Train the combined network (Generator)
g_loss = combined.train_on_batch(noise, valid) ❺
print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" %
(epoch, d_loss[0], 100*d_loss[1], g_loss)) ❻
if epoch % save_interval == 0: ❼
plot_generated_images(epoch, generator) ❼
❶ 对抗性真实值
❷ 选择随机的一半图像
❸ 样本噪声,并生成一批新图像
❹ 训练判别器(将真实分类为 1s,将生成分类为 0s)
❺ 训练生成器(希望判别器将图像误认为是真实的)
❻ 打印进度
❼ 如果在 save_interval 时保存生成的图像样本
在运行 train() 函数之前,您需要定义以下 plot_generated _images() 函数:
def plot_generated_images(epoch, generator, examples=100, dim=(10, 10),
figsize=(10, 10)):
noise = np.random.normal(0, 1, size=[examples, latent_dim])
generated_images = generator.predict(noise)
generated_images = generated_images.reshape(examples, 28, 28)
plt.figure(figsize=figsize)
for i in range(generated_images.shape[0]):
plt.subplot(dim[0], dim[1], i+1)
plt.imshow(generated_images[i], interpolation='nearest', cmap='gray_r')
plt.axis('off')
plt.tight_layout()
plt.savefig('gan_generated_image_epoch_%d.png' % epoch)
第 7 步:训练并观察结果
现在代码实现已经完成,我们准备开始 DCGAN 的训练。要训练模型,运行以下代码片段:
train(epochs=1000, batch_size=32, save_interval=50)
这将运行 1,000 个时代的训练,并且每 50 个时代保存一次图像。当您运行train()函数时,训练进度会像图 8.22 所示那样打印出来。

图 8.22 前 16 个时代的训练进度
我自己运行了 10,000 个时代的训练。图 8.23 显示了 0、50、1,000 和 10,000 个时代后的结果。

图 8.23 GAN 生成器在 0、50、1,000 和 10,000 个时代后的输出
如您在图 8.23 中看到的,在 0 个时代,图像只是随机噪声——没有模式或有意义的数据。在 50 个时代,模式开始形成。一个非常明显的模式是图像中心开始形成的亮像素,以及周围的较暗像素。这是因为训练数据中,所有形状都位于图像的中心。在训练过程的后期,在 1,000 个时代,您可以清楚地看到形状,并且可以猜测出提供给 GAN 模型的训练数据类型。快进到 10,000 个时代,您可以看到生成器已经非常擅长重新创建训练数据集中不存在的新的图像。例如,选择在这个时代创建的任何对象:比如说左上角的图像(连衣裙)。这是一个完全新的连衣裙设计,在训练数据集中不存在。GAN 模型在从训练集中学习连衣裙模式后,创建了一个全新的连衣裙设计。您可以运行更长时间的训练,或者使生成器网络更深,以获得更精细的结果。
在结束之前
对于这个项目,我使用了 Fashion-MNIST 数据集,因为图像非常小,是灰度图(单通道),这使得您在本地计算机上没有 GPU 的情况下进行训练时计算成本很低。Fashion-MNIST 数据集也非常干净:所有图像都居中,噪声较少,因此在启动 GAN 训练之前不需要太多预处理。这使得它成为一个很好的玩具数据集,可以用来启动您的第一个 GAN 项目。
如果您想尝试更高级的数据集,您可以将 CIFAR 作为您的下一步(www.cs.toronto.edu/~kriz/cifar.html)或 Google 的 Quick, Draw!数据集(quickdraw.withgoogle.com),这是在撰写本文时被认为是世界上最大的涂鸦数据集。另一个更严肃的数据集是斯坦福的 Cars 数据集(ai.stanford.edu/~jkrause/cars/car_dataset.html),它包含超过 16,000 张 196 类汽车的图像。您可以尝试训练您的 GAN 模型为您的梦想汽车设计一个全新的设计!
摘要
-
GAN 从训练数据集中学习模式,并创建具有与训练集相似分布的新图像。
-
GAN 架构由两个相互竞争的深度神经网络组成。
-
生成器试图将随机噪声转换为看起来像是从原始数据集中采样的观察值。
-
判别器试图预测一个观察值是否来自原始数据集或生成器伪造的之一。
-
判别器的模型是一个典型的分类神经网络,旨在将生成器生成的图像分类为真实或伪造。
-
生成器的架构看起来像是一个倒置的 CNN,它以狭窄的输入开始,经过几次上采样,直到达到所需的大小。
-
上采样层通过重复其输入像素的每一行和每一列来调整图像尺寸。
-
为了训练 GAN,我们通过两个并行网络批量训练网络:判别器和冻结判别器权重并仅更新生成器权重的组合网络。
-
为了评估 GAN,我们主要依赖于我们对生成器创建的图像质量的观察。其他评估指标包括 Inception 得分和 Fréchet Inception 距离(FID)。
-
除了生成新图像外,GAN 还可以用于文本到照片合成、图像到图像翻译、图像超分辨率和其他许多应用。
1.伊恩·J·古德费尔、让·普吉特-阿巴迪、梅赫迪·米尔扎、丁旭、大卫·沃德-法雷利、谢吉尔·奥齐尔、阿隆·库维尔和约书亚·本吉奥,“生成对抗网络,”2014 年,arxiv.org/abs/1406.2661.
2.亚历克·拉德福德、卢克·梅茨和索乌米特·钦塔拉,“使用深度卷积生成对抗网络的无监督表示学习,”2016 年,arxiv.org/abs/1511.06434.
3.蒂姆·萨利曼斯、伊恩·古德费尔、沃伊切赫·扎伦巴、维基·张、亚历克·拉德福德和x[i]陈。“改进 GAN 的训练技术,”2016 年,arxiv.org/abs/1606.03498.
4.马丁·海塞尔、胡伯特·拉姆萨乌尔、托马斯·乌特纳、伯纳德·内斯勒和塞普·霍克赖特,“通过双时间尺度更新规则训练的 GAN 收敛到局部纳什均衡,”2017 年,arxiv.org/abs/1706.08500.
5.康斯坦丁·施梅尔科夫、科德利亚·施密德和卡尔特克·阿拉哈里,“我的 GAN 有多好?”2018 年,arxiv.org/abs/1807.09499.
6.韩张、徐涛、李洪升、张少亭、王晓刚、黄晓雷和迪米特里斯·梅塔克萨斯,“StackGAN:使用堆叠生成对抗网络进行文本到逼真图像合成,”2016 年,arxiv.org/abs/1612.03242.
7.菲利普·伊索拉、朱俊彦、周廷辉和亚历克谢·A·埃夫罗,“条件对抗网络进行图像到图像翻译,”2016 年,arxiv.org/abs/1611.07004.
- 克里斯蒂安·莱迪格,卢卡斯·泰斯,费伦茨·胡萨,何塞·卡巴列罗,安德鲁·坎宁安,亚历杭德罗·阿科斯塔,安德鲁·艾肯等,《使用生成对抗网络进行逼真单图像超分辨率》,2016 年,
arxiv.org/abs/1609.04802.
9 DeepDream 和神经风格迁移
本章涵盖了
-
可视化 CNN 特征图
-
理解 DeepDream 算法并实现自己的梦想
-
使用神经风格迁移算法创建艺术图像
在纯艺术,尤其是绘画中,人类已经掌握了通过组合图像的内容和风格之间的复杂相互作用来创造独特视觉体验的技能。到目前为止,这个过程算法基础尚不清楚,并且不存在具有类似能力的人工系统。如今,深度神经网络在视觉感知的许多领域,如对象分类和检测,已经显示出巨大的潜力。为什么不用深度神经网络来创造艺术呢?在本章中,我们介绍了一个基于深度神经网络的人工系统,该系统能够创建具有高感知质量的艺术图像。该系统使用神经网络表示来分离和重新组合任意图像的内容和风格,为艺术图像的创建提供了一种神经网络算法。
在本章中,我们探索了两种使用神经网络创建艺术图像的新技术:DeepDream 和神经风格迁移。首先,我们检查卷积神经网络如何看世界。我们已经学习了 CNN 如何用于对象分类和检测问题中的特征提取;在这里,我们学习如何可视化提取的特征图。一个原因是我们需要这种可视化技术来理解 DeepDream 算法。此外,这将帮助我们更好地理解网络在训练期间学到了什么;我们可以利用这一点来提高网络在解决分类和检测问题时表现。
接下来,我们讨论 DeepDream 算法。这种技术的关键思想是将我们在某一层可视化的特征打印到我们的输入图像上,以创建一种梦幻般的幻觉图像。最后,我们探索神经风格迁移技术,它接受两个图像作为输入——一个风格图像和一个内容图像——并创建一个包含内容图像的布局和风格图像的纹理、颜色和模式的新组合图像。
为什么这次讨论很重要?因为这些技术帮助我们理解和可视化神经网络如何执行困难的分类和检测任务,并检查网络在训练期间学到了什么。能够看到网络的想法,在区分物体时是一个重要的特征,这将帮助你了解你的训练集中缺少什么,从而提高网络的表现。
这些技术也让我们思考神经网络是否可能成为艺术家的工具,给我们提供一种新的方式来结合视觉概念,或者甚至可能对一般创造性过程的根源投下一些光。此外,这些算法为算法理解人类如何创造和感知艺术图像提供了一条前进的道路。
9.1 卷积神经网络如何观察世界
我们在这本书中已经讨论了很多深度神经网络所能做到的令人惊叹的事情。但尽管关于深度学习的新闻都十分激动人心,神经网络确切地是如何观察和解释世界的仍然是一个黑箱。是的,我们尝试解释了训练过程是如何工作的,并且我们通过直观和数学的方式解释了网络通过多次迭代更新权重以优化损失函数的反向传播过程。这一切听起来都很好,在科学方面也很有道理。但是,CNN 是如何观察世界的?它们是如何在所有层之间观察提取的特征的?
更好地理解它们是如何识别特定模式或对象以及为什么它们工作得如此之好,可能会让我们进一步提高它们的性能。此外,在商业方面,这也会解决“AI 可解释性”问题。在许多情况下,商业领导者感到无法根据模型预测做出决策,因为没有人真正理解黑箱内部发生了什么。这就是我们在本节中要做的:我们打开黑箱,可视化网络通过其层所看到的内容,以帮助使神经网络决策对人类可解释。
在计算机视觉问题中,我们可以可视化卷积网络内部的特征图,以了解它们是如何观察世界的,以及它们认为哪些特征在区分不同类别时是独特的。可视化卷积层这一想法是由 Erhan 等人于 2009 年提出的。1 在本节中,我们将解释这一概念并在 Keras 中实现它。
9.1.1 重新审视神经网络的工作原理
在我们跳入如何可视化 CNN 中的激活图(或特征图)的解释之前,让我们重新审视神经网络的工作原理。我们通过展示数百万个训练示例来训练深度神经网络。然后,网络逐渐更新其参数,直到它给出我们想要的分类。网络通常由 10-30 层堆叠的人工神经元组成。每个图像被输入到输入层,然后与下一层通信,直到最终达到“输出”层。网络的预测由其最终输出层产生。
神经网络的一个挑战是理解每个层究竟发生了什么。我们知道在训练之后,每个层逐渐提取更高层次上的图像特征,直到最终层基本上做出关于图像包含内容的决定。例如,第一层可能寻找边缘或角落,中间层将基本特征解释为寻找整体形状或组件,而最后几层将这些组合成完整的解释。这些神经元对非常复杂的图像,如汽车或自行车,做出反应。
为了理解网络通过训练学到了什么,我们想要打开这个黑盒并可视化其特征图。可视化提取特征的一种方法是将网络颠倒过来,并要求它以某种方式增强输入图像,以引发特定的解释。比如说,你想知道什么样的图像会产生“鸟”的输出。从一个充满随机噪声的图像开始,然后逐渐调整图像,使其逐渐接近神经网络认为的鸟的重要特征(图 9.1)。

图 9.1 从一个由随机噪声组成的图像开始,调整它直到我们可视化网络认为的鸟的重要特征。
我们将更深入地探讨鸟的例子,看看如何可视化网络滤波器。从这个介绍中我们可以得出结论,神经网络足够聪明,能够理解哪些是重要的特征,并通过其层传递给全连接层进行分类。非重要特征在过程中被丢弃。简单来说,神经网络学习训练数据集中对象的特征。如果我们能够在网络的深层中可视化这些特征图,我们就可以找出神经网络关注的地方,并看到它用来做出预测的确切特征。
注意:这个过程在 François Chollet 的《Python 深度学习》(Manning,2017;www.manning.com/books/deep-learning-with-python)一书中描述得最好:“你可以将深度网络视为一个多阶段信息蒸馏操作,其中信息通过连续的滤波器,并逐渐变得更加纯净。”
9.1.2 可视化 CNN 特征
可视化卷积网络学习到的特征的一个简单方法是通过显示每个滤波器旨在响应的视觉模式。这可以通过输入空间中的梯度上升来实现。通过对 ConvNet 的输入图像的值应用梯度上升,我们可以最大化特定滤波器的响应,从一个空白输入图像开始。结果输入图像将是那个滤波器响应最大的图像。
梯度上升与梯度下降
作为提醒,梯度的通用定义是:它是定义曲线在任意给定点的切线斜率或变化率的函数。用简单的话说,梯度是那个点的线的斜率。以下是一些曲线上的特定点的梯度示例。

曲线上不同点的梯度
我们是想要下降还是上升曲线,这取决于我们的项目。我们在第二章中了解到,梯度下降法是寻找局部最小值(例如,最小化损失函数)的算法,它通过朝着梯度的负方向迈步来下降误差函数。
为了可视化特征图,我们希望最大化这些特征,使它们在输出图像上显示出来。为了最大化损失函数,我们希望通过使用梯度上升算法来反转 GD 过程。它通过正梯度的比例来接近该函数的局部最大值。
现在是本节最有意思的部分。在这个练习中,我们将看到 VGG16 网络开始、中间和末尾的一些示例的可视化特征图。实现方法是直接的,我们很快就会看到。在我们进行代码实现之前,让我们看看这些可视化滤波器看起来像什么。
从图 9.1 中我们看到的 VGG16 图中,让我们如下可视化第一层、中间层和深层层的输出特征图:block1_conv1、block3_conv2和block5_conv3。图 9.2、9.3 和 9.4 显示了特征在整个网络层中的演变过程。

图 9.2 block1_conv1 滤波器生成的特征图可视化
如图 9.2 所示,你可以看到早期层基本上只是编码低级、通用的特征,如方向和颜色。这些方向和颜色滤波器然后在后续层中组合成基本的网格和点状纹理。这些纹理逐渐组合成越来越复杂的图案(图 9.3):网络开始看到一些创建基本形状的图案。这些形状目前还不太容易识别,但比早期的那些要清晰得多。

图 9.3 block3_conv2 滤波器生成的特征图可视化
现在是最激动人心的部分。在图 9.4 中,你可以看到网络能够在模式中找到模式。这些特征包含可识别的形状。虽然网络依赖于多个特征图来做出预测,但我们可以查看这些图,并对这些图像的内容做出合理的猜测。在左边的图像中,我可以看到眼睛,也许还有喙,我会猜测这是一种鸟或鱼。即使我们的猜测不正确,我们也可以轻松排除大多数其他类别,如汽车、船只、建筑、自行车等,因为我们可以清楚地看到眼睛,而这些类别中没有一个有眼睛。同样,观察中间的图像,我们可以从模式中猜测这是一条链。右边的图像更像是食物或水果。

图 9.4 block5_conv3 滤波器生成的特征图可视化
这在分类和检测问题中有什么帮助呢?让我们以图 9.4 中的左侧特征图为例。观察可见的特征,如眼睛和喙,我可以解释说网络依赖于这两个特征来识别一只鸟。有了关于网络学习到的关于鸟的知识,我将猜测它可以在图 9.5 中检测到鸟,因为鸟的眼睛和喙是可见的。

图 9.5 具有可见眼睛和喙特征的鸟的图像示例
现在,让我们考虑一个更具对抗性的情况,其中我们可以看到鸟的身体,但眼睛和喙被树叶覆盖(图 9.6)。鉴于网络在眼睛和喙特征上添加了高权重以识别鸟类,它可能会因为鸟类的主要特征被隐藏而错过这个鸟类。另一方面,一个普通的人可以很容易地在图像中检测到鸟类。解决这个问题的一个方法是使用几种数据增强技术之一,并在你的训练数据集中收集更多的对抗性案例,以迫使网络在鸟类的其他特征(如形状和颜色)上添加更高的权重。

图 9.6 鸟类对抗性图像的示例,其中眼睛和喙不可见,但人体可以通过人类识别
9.1.3 实现特征可视化器
现在你已经看到了可视化的示例,是时候动手编写代码来自己可视化这些激活过滤器了。本节将介绍从官方 Keras 文档中实现的 CNN 可视化代码,略有调整。2 你将学习如何生成最大化所选特征图平均激活的模式。你可以在 Keras 的 GitHub 仓库中看到完整的代码(mng.bz/Md8n)。
注意:如果你尝试运行本节中的代码片段,你可能会遇到错误。这些片段只是为了说明主题。我们鼓励你查看与本书一起可下载的完整可执行代码。
首先,我们从 Keras 库中加载 VGG16 模型。为此,我们首先从 Keras 导入 VGG16,然后加载模型,该模型在 ImageNet 数据集上预训练,不包括网络分类全连接层(顶部部分):
from keras.applications.vgg16 import VGG16 ❶
model = VGG16(weights='imagenet', include_top=False) ❷
❶ 从 Keras 导入 VGG 模型
❷ 加载模型
现在,让我们查看所有 VGG16 层的名称和输出形状。我们这样做是为了选择我们想要可视化的特定层的过滤器:
for layer in model.layers: ❶
if 'conv' not in layer.name: ❷
continue
filters, biases = layer.get_weights() ❸
print(layer.name, layer.output.shape)
❶ 遍历模型层
❷ 检查卷积层
❸ 获取过滤器权重
当你运行这个代码单元时,你会得到图 9.7 所示的输出。这些都是 VGG16 网络中包含的所有卷积层。你可以通过简单地按名称引用每个层来可视化它们的任何输出,正如你将在下一个代码片段中看到的那样。

图 9.7 显示下载的 VGG16 网络中卷积层的输出
假设我们想要可视化第一个卷积层:block1_conv1。请注意,这个层有 64 个过滤器,每个过滤器都有一个从 0 到 63 的索引,称为filter_index。现在让我们定义一个损失函数,该函数试图最大化特定层(layer_name)中特定过滤器(filter_index)的激活。我们还想使用 Keras 的后端函数gradients来计算梯度,并将梯度归一化以避免非常小和非常大的值,以确保平滑的梯度上升过程。
在这个代码片段中,我们为梯度上升设置了场景。我们定义了一个损失函数,计算了梯度,并归一化了梯度:
from keras import backend as K
layer_name = 'block1_conv1'
filter_index = 0 ❶
layer_dict = dict([(layer.name, layer) for layer in model.layers[1:]]) ❷
layer_output = layer_dict[layer_name].output ❸
loss = K.mean(layer_output[:, :, :, filter_index]) ❸
grads = K.gradients(loss, input_img)[0] ❹
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5) ❺
iterate = K.function([input_img], [loss, grads]) ❻
❶ 识别我们想要可视化的过滤器。这可以是该层中从 0 到 63 的任何整数,因为该层有 64 个过滤器。
❷ 获取每个关键层的符号输出(我们给它们起了独特的名字)。
❸ 构建一个损失函数,该函数最大化考虑的层的第 n 个过滤器的激活
❹ 计算输入图片相对于该损失的梯度
❺ 归一化梯度
❻ 此函数根据输入图片返回损失和梯度。
我们可以使用我们刚刚定义的 Keras 函数来进行梯度上升,针对我们的过滤器激活损失:
import numpy as np
input_img_data = np.random.random((1, 3, img_width, img_height)) * 20 + 128 ❶
for i in range(20): ❷
loss_value, grads_value = iterate([input_img_data]) ❷
input_img_data += grads_value * step ❷
❶ 从一个带有一些噪声的灰色图像开始
❷ 进行 20 步的梯度上升
现在我们已经实现了梯度上升,我们需要构建一个将张量转换为有效图像的函数。我们将称之为deprocess_image(x)。然后我们将图像保存到磁盘上以查看它:
from keras.preprocessing.image import save_img
def deprocess_image(*x*):
*x* -= x.mean() ❶
*x* /= (x.std() + 1e-5) ❶
*x* *= 0.1 ❶
*x* += 0.5 ❷
*x* = np.clip(x, 0, 1) ❷
*x* *= 255 ❸
*x* = x.transpose((1, 2, 0)) ❸
*x* = np.clip(x, 0, 255).astype('uint8') ❸
return *x* ❸
img = input_img_data[0]
img = deprocess_image(img)
imsave('%s_filter_%d.png' % (layer_name, filter_index), img)
❶ 归一化张量:以 0 为中心,并确保 std 为 0.1
❷ 截断到[0, 1]
❸ 转换为 RGB 数组
结果应该类似于图 9.8。

图 9.8 VGG16 层block1_conv1的可视化
你可以尝试将可视化的过滤器更改为后续块中的深层,例如block2和block3,以查看网络通过其层识别模式中的模式提取的更定义的特征。在最高层(block5_conv2,block5_conv3)中,你将开始识别与网络训练分类的对象中发现的纹理相似的纹理,例如羽毛、眼睛等。
9.2 DeepDream
DeepDream 是由 Google 研究人员 Alexander Mordvintsev 等人于 2015 年开发的。3 它是一种艺术图像修改技术,使用 CNN 创建梦幻般的、致幻的图像,如图 9.9 所示的示例所示。

图 9.9 DeepDream 输出图像
为了比较,原始输入图像显示在图 9.10 中。原始图像是来自海洋的风景图像,包含两只海豚和其他生物。DeepDream 将两只海豚合并成一个物体,并将其中一个面孔替换成看起来像狗脸的东西。其他物体也被以艺术的方式变形,海洋背景具有边缘状的纹理。

图 9.10 DeepDream 输入图像
由于它生成的充满算法痕迹、鸟羽、狗脸和眼睛的奇异图片,DeepDream 迅速成为互联网现象。这些痕迹是 DeepDream ConvNet 在 ImageNet 上训练的结果,在 ImageNet 中,狗的品种和鸟的种类被大量过度代表。如果你尝试另一个在具有多数分布的其他对象的数据集上预训练的网络,例如汽车,你将在输出图像中看到汽车特征。
该项目最初是一个有趣的实验,旨在反向运行 CNN 并使用第 9.1 节中解释的相同的卷积滤波器可视化技术来可视化其激活图:反向运行卷积神经网络,对输入进行梯度上升,以最大化卷积神经网络上层特定滤波器的激活。DeepDream 使用了这个相同的思想,但做了一些修改:
-
输入图像--在滤波器可视化中,我们不使用输入图像。我们从一张空白图像(或稍微有点噪声的图像)开始,然后最大化卷积层的滤波器激活,以查看其特征。在 DeepDream 中,我们使用输入图像到网络,因为目标是将这些可视化的特征打印到图像上。
-
最大化滤波器与层--在滤波器可视化中,正如其名所示,我们只最大化层内特定滤波器的激活。但在 DeepDream 中,我们的目标是最大化整个层的激活,以一次混合大量特征。
-
八度音阶--在 DeepDream 中,输入图像以不同的尺度称为八度音阶进行处理,以提高可视化的特征质量。这个过程将在下一节中解释。
9.2.1 DeepDream 算法的工作原理
与过滤可视化技术类似,DeepDream 使用在大数据集上预训练的网络。Keras 库提供了许多可用的预训练卷积神经网络:VGG16、VGG19、Inception、ResNet 等等。我们可以在 DeepDream 实现中使用这些网络中的任何一个;我们甚至可以在自己的数据集上训练一个自定义网络,并在 DeepDream 算法中使用它。直观地说,网络的选择以及它预训练的数据将影响我们的可视化,因为不同的卷积神经网络架构会导致不同的学习特征;当然,不同的训练数据集也会创建不同的特征。
DeepDream 的创造者使用了 Inception 模型,因为他们发现实际上它产生的梦境看起来很美。所以在本章中,我们将使用 Inception v3 模型。我们鼓励你尝试不同的模型来观察差异。
DeepDream 的整体思想是,我们将输入图像通过一个预训练的神经网络,如 Inception v3 模型。在某个层,我们计算梯度,它告诉我们如何改变输入图像以最大化该层的值。我们继续这样做,直到 10、20 或 40 次迭代,最终,输入图像中开始出现模式(图 9.11)。

图 9.11 DeepDream 算法
这工作得很好,但是,如果预训练的网络是在相当小的图像尺寸上训练的,比如 ImageNet,那么当我们的输入图像很大(比如 1000 × 1000)时,DeepDream 算法会在图像中打印出很多小的模式,看起来很嘈杂而不是艺术。这是因为所有提取的特征都很小。为了解决这个问题,DeepDream 算法以不同的尺度称为八度音阶处理输入图像。
八度只是一个表示区间的时髦词。想法是通过区间应用 DeepDream 算法于输入图像。我们首先将图像降级为几个不同的尺度。尺度数量是可配置的,你很快就会看到。对于每个区间,我们执行以下操作:
-
注入细节:为了避免在每次连续放大后丢失大量图像细节,我们在每次放大过程之后将丢失的细节重新注入到图像中,以创建混合图像。
-
应用 DeepDream 算法:将混合图像通过 DeepDream 算法。
-
放大到下一个区间。
如图 9.12 所示,我们从一个大输入图像开始,然后将其降级两次,以在 3 次方频域中得到一个小图像。在应用 DeepDream 的第一个区间,我们不需要进行细节注入,因为输入图像是尚未放大过的源图像。我们将其通过 DeepDream 算法,然后放大输出。放大后,细节丢失,导致图像越来越模糊或像素化。这就是为什么在 2 次方频域中重新注入输入图像的细节,然后将混合图像通过 DeepDream 算法非常有价值。我们再次应用放大、细节注入和 DeepDream 的过程,以获得最终的结果图像。这个过程以递归方式运行,直到我们对输出艺术满意为止。

图 9.12 DeepDream 过程:连续图像降级称为八度,细节重新注入,然后放大到下一个八度
我们将 DeepDream 参数设置为以下内容:
num_octave = 3 ❶
octave_scale = 1.4 ❷
iterations = 20 ❸
❶ 尺度数量
❷ 尺度之间的尺寸比。每个连续的尺度比前一个大 1.4 倍(即 40%更大)。
❸ 迭代次数
现在你已经了解了 DeepDream 算法的工作原理,让我们看看如何使用 Keras 实现 DeepDream。
9.2.2 Keras 中的 DeepDream 实现
我们将要实现的 DeepDream 实现是基于 François Chollet 从官方 Keras 文档([keras.io/examples/generative/deep_dream/](https://keras.io/examples/generative/deep_dream/))和他的书《Python 深度学习》中的代码。我们将在此代码适应 Jupyter Notebooks 后解释此代码:
import numpy as np
from keras.applications import inception_v3
from keras import backend as K
from keras.preprocessing.image import save_img
K.set_learning_phase(0) ❶
model = inception_v3.InceptionV3(weights='imagenet', include_top=False) ❷
❶ 禁用所有训练操作,因为我们不会用该模型进行任何训练
❷ 下载预训练的 Inception v3 模型,不包括其顶部部分
现在,我们需要定义一个字典,指定用于生成梦境的层。为此,让我们打印出模型摘要以查看所有层并选择层名:
model.summary()
Inception v3 非常深,摘要打印很长。为了简单起见,图 9.13 显示了网络的一些层。
你选择的精确层及其对最终损失的贡献对你在梦境图像中产生的视觉效果有重要影响,因此你希望这些参数易于配置。为了定义我们想要贡献于梦境创建的层,我们创建一个包含层名称及其相应权重的字典。层的权重越大,其对梦境的贡献级别越高:
layer_contributions = {
'mixed2': 0.4,
'mixed3': 2.,
'mixed4': 1.5,
'mixed5': 2.3,
}

图 9.13 Inception v3 模型摘要的一部分
这些是我们尝试最大化激活的层名称。请注意,当你更改此字典中的层时,你将产生不同的梦境,因此我们鼓励你尝试不同的层及其相应的权重。对于这个项目,我们将通过向我们的字典添加四个层及其权重(mixed2、mixed3、mixed4和mixed5)来从一个相对任意的配置开始。作为一个指导,记得从本章前面的内容中,较低层可以用来生成边缘和几何图案,而高层则导致注入令人着迷的视觉图案,包括狗、猫和鸟的幻觉。
现在,让我们定义一个包含损失的张量:各层激活的 L2 范数的加权总和:
layer_dict = dict([(layer.name, layer) for layer in model.layers]) ❶
loss = K.variable(0.) ❷
for layer_name in layer_contributions:
coeff = layer_contributions[layer_name]
activation = layer_dict[layer_name].output
scaling = K.prod(K.cast(K.shape(activation), 'float32'))
loss = loss + coeff *
K.sum(K.square(activation[:, 2: -2, 2: -2, :])) / scaling ❸
❶ 将层名称映射到层实例的字典
❷ 通过添加层贡献到这个标量变量来定义损失
❸ 将层的特征 L2 范数添加到损失中。我们通过只涉及非边界像素来避免边界伪影。
接下来,我们计算损失,这是我们将在梯度上升过程中尝试最大化的量。在滤波器可视化中,我们希望最大化特定层中特定滤波器的值。在这里,我们将同时最大化多个层中所有滤波器的激活。具体来说,我们将最大化一组高级层激活的 L2 范数的加权总和:
dream = model.input ❶
grads = K.gradients(loss, dream)[0] ❷
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7) ❸
outputs = [loss, grads] ❹
fetch_loss_and_grads = K.function([dream], outputs) ❹
def eval_loss_and_grads(*x*):
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1]
return loss_value, grad_values
def gradient_ascent(x, iterations, step, max_loss=None): ❺
for i in range(iterations):
loss_value, grad_values = eval_loss_and_grads(*x*)
if max_loss is not None and loss_value > max_loss:
break
print('...Loss value at', i, ':', loss_value)
*x* += step * grad_values
return x
❶ 包含生成图像的张量
❷ 计算梦境图像相对于损失的梯度
❸ 归一化梯度
❹ 设置一个 Keras 函数以检索给定输入图像的损失和梯度值
❺ 运行梯度上升过程多次迭代
现在我们已经准备好开发我们的 DeepDream 算法。过程如下:
-
加载输入图像。
-
定义从最小到最大的尺度数量。
-
将输入图像调整到最小尺寸。
-
对于每个尺度,从最小开始,应用以下:
-
梯度上升函数
-
上采样到下一个尺度
-
在上采样过程中重新注入丢失的细节
-
当我们回到原始大小时停止该过程。
-
首先,我们设置算法参数:
step = 0.01 ❶
num_octave = 3 ❷
octave_scale = 1.4 ❸
iterations = 20 ❹
max_loss = 10.
❶ 梯度上升步长
❷ 运行梯度上升的尺度数量
❸ 尺度之间的尺寸比
❹ 迭代次数
注意,调整这些超参数将允许你实现新的效果。
让我们定义我们想要用来创建梦境的输入图像。在这个例子中,我下载了一张旧金山金门大桥的图片(见图 9.14);你也可以使用你自己的图片。图 9.15 显示了 DeepDream 的输出图像。

图 9.14 示例输入图像

图 9.15 DeepDream 输出
这里是 Keras 代码:
base_image_path = 'input.jpg' ❶
img = preprocess_image(base_image_path)
original_shape = img.shape[1:3]
successive_shapes = [original_shape]
for i in range(1, num_octave):
shape = tuple([int(dim / (octave_scale ** i)) for dim in original_shape])
successive_shapes.append(shape)
successive_shapes = successive_shapes[::-1]
original_img = np.copy(img)
shrunk_original_img = resize_img(img, successive_shapes[0])
for shape in successive_shapes:
print('Processing image shape', shape)
img = resize_img(img, shape)
img = gradient_ascent(img, iterations=iterations, step=step,
max_loss=max_loss)
upscaled_shrunk_original_img = resize_img(shrunk_original_img, shape)
same_size_original = resize_img(original_img, shape)
lost_detail = same_size_original - upscaled_shrunk_original_img
img += lost_detail
shrunk_original_img = resize_img(original_img, shape)
phil_img = deprocess_image(np.copy(img))
save_img('deepdream_output/dream_at_scale_' + str(shape) + '.png', phil_img)
final_img = deprocess_image(np.copy(img))
save_img('final_dream.png', final_img) ❷
❶ 定义输入图像的路径
❷ 将结果保存到磁盘
9.3 神经风格迁移
到目前为止,我们已经学习了如何在网络中可视化特定的过滤器。我们还学习了如何使用 DeepDream 算法操纵输入图像的特征来创建梦幻般的幻觉图像。在本节中,我们将探索一种新的艺术图像类型,即卷积神经网络可以通过神经风格迁移来创建的:将一种图像的风格转移到另一种图像的技术。
神经风格迁移算法的目标是将一张图像的风格(风格图像)应用到另一张图像的内容(内容图像)上。这里的风格指的是图像中的纹理、颜色和其他视觉模式。而内容则是图像的高级宏观结构。结果是包含内容图像的内容和风格图像的风格的综合图像。
例如,让我们看看图 9.16。内容图像中的对象(如海豚、鱼和植物)在综合图像中保持不变,但具有风格图像的特定纹理(蓝色和黄色的笔触)。

图 9.16 神经风格迁移示例
神经风格迁移的想法是由 Leon A. Gatys 等人于 2015 年提出的。4 在此之前,与纹理生成紧密相关的风格迁移概念在图像处理社区中有着悠久的历史;但事实证明,基于深度学习的风格迁移实现提供了传统计算机视觉技术所无法比拟的结果,并引发了一场在创造性计算机视觉应用中的惊人复兴。
在创建艺术的不同神经网络技术(如 DeepDream)中,风格迁移是我最喜欢的一种。DeepDream 可以创建酷炫的幻觉图像,但有时可能会让人感到不安。此外,作为一个深度学习工程师,有意创造你心中所想的特定艺术品并不容易。另一方面,风格迁移可以使用艺术工程师将你从图像中想要的内容与你最喜欢的画作混合,创造出你想象中的东西。这是一个非常酷的技术,如果由艺术工程师使用,可以创造出与专业画家作品相媲美的美丽艺术。
实现风格迁移背后的主要思想与第二章中解释的所有深度学习算法的核心思想相同:我们首先定义一个损失函数来定义我们想要实现的目标,然后我们致力于优化这个函数。在风格迁移问题中,我们知道我们想要实现的目标是:在保留原始图像内容的同时,采用参考图像的风格。现在我们所需做的就是以数学形式定义内容和风格,然后定义一个合适的损失函数来最小化它。
定义损失函数的关键思想是要记住,我们想要保留一张图像的内容和另一张图像的风格:
-
内容损失--计算内容图像和组合图像之间的损失。最小化这个损失意味着组合图像将包含更多来自原始图像的内容。
-
风格损失--计算风格图像和组合图像之间的风格损失。最小化这个损失意味着组合图像将具有与风格图像相似的风格。
-
噪声损失--这被称为总变差损失。它衡量了组合图像中的噪声。最小化这个损失会创建一个具有更高空间平滑性的图像。
这里是总损失的方程:
total_loss = [style(style_image) - style(combined_image)] +
[content(original_image) - content(combined_image)] + total_variation_loss
注意:Gatys 等人(2015)在迁移学习的研究中不包括总变差损失。经过实验,研究人员发现,当他们在输出图像中鼓励空间平滑性时,生成的网络在风格迁移方面表现更好,更具有审美价值。
现在我们已经对神经网络风格迁移算法的大致工作原理有了了解,我们将更深入地探讨每种类型的损失,以了解它是如何推导和编码在 Keras 中的。然后我们将了解如何训练一个神经网络风格迁移网络,以最小化我们刚刚定义的total_loss函数。
9.3.1 内容损失
内容损失衡量了两个图像在主题和内容整体布局方面的差异。换句话说,包含相似场景的两个图像应该具有较小的损失值,而包含完全不同场景的两个图像应该具有较大的损失值。图像的主题和内容布局是通过根据 ConvNet 中的高级特征表示对图像进行评分来衡量的,例如海豚、植物和水。识别这些特征是深度神经网络背后的整个前提:这些网络被训练来提取图像的内容,并通过识别前一层简单特征中的模式来学习更深层的更高层次特征。因此,我们需要一个经过训练以提取内容图像特征的深度神经网络,以便我们可以挖掘网络的深层来提取高级特征。
为了计算内容损失,我们测量内容图像和组合图像输出的均方误差。通过尝试最小化这个误差,网络试图向组合图像添加更多内容,使其越来越接近原始内容图像:
内容损失 = 1/2 Σ[内容(原始图像) - 内容(组合图像)]²
最小化内容损失函数确保我们在组合图像中保留了原始图像的内容。
为了计算内容损失,我们将内容和风格图像输入到一个预训练的网络中,并从其中选择一个深层层来提取高级特征。然后我们计算两个图像之间的均方误差。让我们看看如何在 Keras 中计算两个图像之间的内容损失。
注意:本节中的代码片段是从官方 Keras 文档中的神经风格迁移示例改编的(keras.io/examples/generative/neural_style_transfer/)。如果你想要重新创建这个项目并尝试不同的参数,我建议你从 Keras 的 Github 仓库作为起点(mng.bz/GVzv)或运行与此书一起提供的可下载的改编代码。
首先,我们定义两个 Keras 变量来保存内容图像和风格图像,并创建一个占位符张量,它将包含生成的组合图像:
content_image_path = '/path_to_images/content_image.jpg' ❶
style_image_path = '/path_to_images/style_image.jpg' ❶
content_image = K.variable(preprocess_image(content_image_path)) ❷
style_image = K.variable(preprocess_image(style_image_path)) ❷
combined_image = K.placeholder((1, img_nrows, img_ncols, 3)) ❷
❶ 内容和风格图像的路径
❷ 获取我们图像的张量表示
现在,我们将三张图像连接成一个输入张量,并将其输入到 VGG19 神经网络中。请注意,当我们加载 VGG19 模型时,我们将include_top参数设置为False,因为我们不需要包括用于此任务的分类全连接层。这是因为我们只对网络的特征提取部分感兴趣:
input_tensor = K.concatenate([content_image, style_image,
combined_image], axis=0) ❶
model = vgg19.VGG19(input_tensor=input_tensor,
weights='imagenet', include_top=False) ❷
❶ 将三张图像合并成一个单一的 Keras 张量
❷ 使用我们的三张图像作为输入构建 VGG19 网络。该模型将加载预训练的 ImageNet 权重。
与我们在第 9.1 节中所做的一样,我们现在选择我们想要用来计算内容损失的神经网络层。我们想要选择一个深层层以确保它包含内容图像的高级特征。如果你选择网络的早期层(如块 1 或块 2),网络将无法从原始图像中完全传递内容,因为早期层提取的是低级特征,如线条、边缘和块。在这个例子中,我们选择了块 5 中的第二个卷积层(block5_conv2):
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers]) ❶
layer_features = outputs_dict['block5_conv2'] ❶
❶ 获取每个关键层的符号输出(我们给了它们独特的名称)
现在我们可以从输入张量中提取我们选择的层的特征:
content_image_features = layer_features[0, :, :, :]
combined_features = layer_features[2, :, :, :]
最后,我们创建content_loss函数,该函数计算内容图像和合成图像之间的均方误差。我们创建一个辅助损失函数,旨在保留content_image的特征并将其转移到combined-image:
def content_loss(content_image, combined_image): ❶
return K.sum(K.square(combined - base))
content_loss = content_weight * content_loss(content_image_features,
combined_features) ❷
❶ 内容图像输出与合成图像之间的均方误差函数
❷ 内容损失通过权重参数进行缩放。
权重参数
在这个代码实现中,您将看到以下权重参数:content_weight、style_weight 和 total_variation_weight。这些是我们作为网络输入设置的缩放参数,如下所示:
content_weight = content_weight
style_weight = style_weight
这些权重参数描述了内容、风格和噪声在我们输出图像中的重要性。例如,如果我们设置style_weight = 100 和 content_weight = 1,这意味着我们愿意为了更艺术化的风格迁移而牺牲一点内容。此外,更高的total_variation_weight意味着更高的空间平滑度。
9.3.2 风格损失
正如我们之前提到的,在这个上下文中,风格指的是图像中的纹理、颜色和其他视觉模式。
多层表示风格特征
定义风格损失比我们之前处理的内容损失更具挑战性。在内容损失中,我们只关心在更深层次提取的高级特征,所以我们只需要从 VGG19 网络中选择一层来保留其特征。而在风格损失中,另一方面,我们想要选择多个层,因为我们想要获得图像风格的多个尺度表示。我们想要捕捉低级层、中级层和高级层的图像风格。这使我们能够捕捉风格图像的纹理和风格,并排除内容图像中对象的全球排列。
语法矩阵用于测量联合激活的特征图
语法矩阵是一种用于数值测量两个特征图联合激活程度的方法。我们的目标是构建一个损失函数,以捕捉 CNN 中多个层的风格和纹理。为此,我们需要计算我们 CNN 中激活层之间的相关性。这种相关性可以通过计算激活之间的语法矩阵——特征外积——来捕捉。
为了计算特征图的语法矩阵,我们需要将特征图展平并计算点积:
def gram_matrix(*x*):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram
让我们构建style_loss函数。它计算网络中一系列层的风格和合成图像的语法矩阵。然后,通过计算平方误差之和来比较它们之间风格和纹理的相似性:
def style_loss(style, combined):
S = gram_matrix(style)
C = gram_matrix(combined)
channels = 3
size = img_nrows * img_ncols
return K.sum(K.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))
在这个例子中,我们将计算五个层的风格损失:VGG19 网络中每个块的第一个卷积层(注意,如果您更改特征层,网络将保留不同的风格):
feature_layers = ['block1_conv1', 'block2_conv1',
'block3_conv1', 'block4_conv1',
'Block5_conv1']
最后,我们遍历这些 feature_layers 来计算风格损失:
for layer_name in feature_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
style_loss += (style_weight / len(feature_layers)) * sl ❶
❶ 将风格损失乘以加权参数和计算风格损失所涉及的层数
在训练过程中,网络通过最小化输出图像(组合图像)的风格与输入风格图像的风格之间的损失来工作。这迫使组合图像的风格与风格图像相关联。
9.3.3 总方差损失
总方差损失是组合图像中噪声的度量。网络的目标是最小化这个损失函数,以最小化输出图像中的噪声。
让我们创建一个 total_variation_loss 函数来计算图像的噪声程度。这是我们将要做的:
-
将图像向右移动一个像素,并计算转移图像与原始图像之间的平方误差之和。
-
重复步骤 1,这次将图像向下移动一个像素。
这两个项(a 和 b)的和是总方差损失:
def total_variation_loss(*x*):
a = K.square(
x[:, :img_nrows - 1, :img_ncols - 1, :] - x[:, 1:, :img_ncols - 1, :])
b = K.square(
x[:, :img_nrows - 1, :img_ncols - 1, :] - x[:, :img_nrows - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
tv_loss = total_variation_weight * total_variation_loss(combined_image) ❶
❶ 将总方差损失乘以加权参数
最后,我们计算我们问题的整体损失,这是内容、风格和总方差损失的加和:
total_loss = content_loss + style_loss + tv_loss
9.3.4 网络训练
现在我们已经定义了我们问题的总损失函数,我们可以运行 GD 优化器来最小化这个损失函数。首先我们创建一个对象类 Evaluator,它包含计算整体损失的方法,如前所述,以及相对于输入图像的损失梯度:
class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None
def loss(self, x):
assert self.loss_value is None
loss_value, grad_values = eval_loss_and_grads(*x*)
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
接下来,我们在训练过程中使用我们的评估器类中的方法。为了最小化总损失函数,我们使用基于 SciPy 的优化方法 scipy.optimize.fmin_l_bfgs_b:
from scipy.optimize import fmin_l_bfgs_b
Iterations = 1000 ❶
x = preprocess_image(content_image_path) ❷
for i in range(iterations): ❸
x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x.flatten(),
fprime=evaluator.grads, maxfun=20)
img = deprocess_image(x.copy()) ❹
fname = result_prefix + '_at_iteration_%d.png' % i ❹
save_img(fname, img) ❹
❶ 训练 1,000 次迭代
❷ 将内容图像初始化为组合图像的第一迭代。
❸ 在生成的图像的像素上运行基于 SciPy 的优化(L-BFGS)以最小化总损失。
❹ 保存当前生成的图像
提示:当训练自己的神经风格迁移网络时,请注意,不需要高细节水平的内容图像效果更好,并且已知可以创建视觉上吸引人或有辨识度的艺术图像。此外,包含大量纹理的风格图像比平面风格图像更好:平面图像(如白色背景)不会产生美观的结果,因为没有多少纹理可以转移。
摘要
-
CNN 通过连续的过滤器学习训练集中的信息。网络的每一层处理不同抽象级别的特征,因此生成的特征复杂度取决于层在网络中的位置。早期层学习低级特征;层在网络中的深度越深,提取的特征越容易识别。
-
一旦网络被训练,我们可以反向运行它来稍微调整原始图像,以便给定的输出神经元(如面部或某些动物的面部)产生更高的置信度分数。这种技术可用于可视化,以更好地理解神经网络的涌现结构,并且是 DeepDream 概念的基础。
-
DeepDream 在称为八度的不同尺度上处理输入图像。我们传递每个尺度,重新注入图像细节,通过 DeepDream 算法处理,然后将图像放大以供下一个八度使用。
-
DeepDream 算法与滤波器可视化算法类似。它反向运行 ConvNet,根据网络提取的表示生成输出。
-
DeepDream 与滤波器可视化不同,因为它需要一个输入图像,并最大化整个层,而不是层内的特定滤波器。这使得 DeepDream 能够同时混合大量特征。
-
DeepDream 不仅限于图像——它还可以用于语音、音乐等。
-
神经风格迁移是一种技术,它训练网络保留风格图像的风格(纹理、颜色、图案)并保留内容图像的内容。然后,网络创建一个新的组合图像,该图像结合了风格图像的风格和内容图像的内容。
-
直观地说,如果我们最小化内容、风格和变化损失,我们将得到一个新的图像,其中包含来自内容图像和风格图像的内容和风格的低方差,以及低噪声。
-
内容权重、风格权重和总变化权重的不同值将给出不同的结果。
1.杜米特鲁·埃尔汗,约书亚·本吉奥,阿隆·库维尔,和帕斯卡尔·文森特。“可视化深度网络的更高层特征。”蒙特利尔大学 1341(3):1。2009 年。mng.bz/yyMq。
2.弗朗索瓦·肖莱特,“卷积神经网络如何看世界”,Keras 博客,2016 年,blog.keras.io/category/demo.html。
3.亚历山大·莫尔维茨夫,克里斯托弗·奥拉,和迈克·泰卡,“Deepdream——可视化神经网络的代码示例”,谷歌 AI 博客,2015 年,mng.bz/aROB。
4.莱昂·A·加蒂斯,亚历山大·S·埃克,和马蒂亚斯·贝特格,“艺术风格的神经网络算法”,2015 年,arxiv.org/abs/1508.06576。
10 视觉嵌入
本章节涵盖
-
通过损失函数表达图像之间的相似性
-
训练 CNN 以实现高精度的期望嵌入函数
-
在实际应用中使用视觉嵌入
由拉特内什·库马尔编写
获取图像之间有意义的关联是许多与我们日常生活息息相关的应用的关键构建块,例如人脸识别和图像搜索算法。为了解决这类问题,我们需要构建一个算法,可以从图像中提取相关特征,并随后使用相应的特征进行比较。
拉特内什·库马尔于 2014 年在法国 Inria 的 STARS 团队获得博士学位。在攻读博士学位期间,他专注于视频理解问题:视频分割和多目标跟踪。他还拥有印度马普尔大学的工程学士学位和佛罗里达大学盖恩斯维尔分校的科学硕士学位。他共同撰写了多篇关于在相机网络中重新识别物体时学习视觉嵌入的科学出版物。
在前面的章节中,我们了解到我们可以使用卷积神经网络(CNN)来提取图像的有意义特征。本章将利用我们对 CNN 的理解来共同训练(联合)一个视觉嵌入层。在本章的上下文中,视觉嵌入指的是附加到 CNN 的最后一个完全连接层(在损失层之前)。联合训练指的是共同训练嵌入层和 CNN 参数。
本章探讨了训练和使用视觉嵌入进行大规模基于图像的查询检索系统(如图 10.1 所示的应用)的细节。为了执行这项任务,我们首先需要将我们的图像数据库投影(嵌入)到向量空间(嵌入)。这样,图像之间的比较可以通过测量它们在这个嵌入空间中的成对距离来完成。这是视觉嵌入系统的高级概念。

图 10.1 在处理图像时,我们在日常生活中遇到的示例应用:一台机器比较两张图像(左);查询数据库以找到与输入图像相似的图像(右)。比较两张图像是一个非平凡的任务,并且是许多与有意义图像搜索相关的应用的关键。
定义嵌入是一个向量空间,通常比输入空间维度低,它保留了相对不相似性(在输入空间中)。我们使用向量空间和嵌入空间这两个术语可以互换。在本章的上下文中,训练好的 CNN 的最后完全连接层就是这个向量(嵌入)空间。例如,一个有 128 个神经元的完全连接层对应于一个 128 维的向量空间。
为了在图像之间进行可靠的比较,嵌入函数需要捕捉到期望的输入相似度度量。可以使用各种方法学习嵌入函数;其中一种流行的方法是使用深度卷积神经网络。图 10.2 说明了使用卷积神经网络创建嵌入的高级过程。

图 10.2 使用卷积神经网络从输入图像中获取嵌入
在下一节中,我们将探讨一些使用视觉嵌入进行大规模查询检索系统的示例应用。然后我们将深入探讨视觉嵌入系统不同组件的不同方面:损失函数、挖掘信息数据以及嵌入网络的训练和测试。随后,我们将使用这些概念来解决我们关于构建基于视觉嵌入的查询检索系统的章节项目。之后,我们将探索推动项目网络准确性的边界的方法。到本章结束时,你将能够训练一个卷积神经网络以获得可靠且有意义的嵌入,并将其用于实际应用中。
10.1 视觉嵌入的应用
让我们看看一些实际日常信息检索算法,这些算法使用了视觉嵌入的概念。在给定输入查询的情况下,检索相似图像的一些突出应用包括面部识别(FR)、图像推荐和物体重识别系统。
10.1.1 面部识别
FR 是关于自动识别或标记图像,以精确地标识图像中的人物身份。日常应用包括在网络上搜索名人、自动标记照片中的朋友和家人等。识别是一种细粒度分类。人脸识别手册[1]将 FR 系统的两种模式进行了分类(图 10.3 进行了比较):
-
面部识别——一种将查询人脸图像与数据库中所有模板图像进行一对一匹配,以确定查询人脸身份的方法。例如,城市当局可以检查观察名单,将查询与嫌疑人名单进行匹配(一对一匹配)。另一个有趣的例子是自动标记出现在照片中的用户,这是由主要社交网络平台实现的功能。
-
面部验证——一种将查询人脸图像与声称身份的模板人脸图像进行一对一匹配的方法。

图 10.3 面部验证和面部识别系统:一个面部验证系统的例子,通过一对一匹配来识别图像是否为 Sundar(左);一个面部识别系统的例子,通过一对多匹配来识别所有图像(右)。尽管识别和识别在目标层面上存在差异,但它们都依赖于一个良好的嵌入函数,该函数能够捕捉到人脸之间的有意义差异。(该图受到了[2]的启发。)
10.1.2 图像推荐系统
在这个任务中,用户试图根据给定的查询图像找到相似图像。购物网站根据特定产品的选择提供产品建议(通过图像),例如显示与用户所选鞋子相似的所有类型的鞋子。图 10.4 展示了服装搜索的示例。

图 10.4 服装搜索。每行的最左边的图像是查询图像,随后的列显示了与它相似的多种服装。(此图中的图像来自[3]。)
注意,两个图像之间的相似性取决于选择相似性度量的上下文。图像的嵌入根据选择的相似性度量类型而有所不同。一些相似性度量的例子包括颜色相似性和语义相似性:
-
颜色相似性 --检索到的图像具有相似的颜色,如图 10.5 所示。这个度量在检索类似颜色的画作、类似颜色的鞋子(不一定确定风格)等应用中使用。
![图片]()
图 10.5 相似性示例,其中汽车通过颜色进行区分。注意,在这个说明性的二维嵌入空间中,颜色相似的汽车更接近。
-
语义相似性 --检索到的图像具有相同的语义属性,如图 10.6 所示。在我们之前的鞋类检索示例中,用户期望看到与高跟鞋具有相同语义的鞋类建议。你可以发挥创意,决定将颜色相似性与语义结合,以提供更有意义的建议。
![图片]()
图 10.6 示例:身份嵌入。具有相似特征的汽车在嵌入空间中投影得更近。
10.1.3 物体重识别
物体重识别的一个例子是安全摄像头网络(CCTV 监控),如图 10.7 所示。安全操作员可能希望查询特定人员并找出他们在所有摄像头中的位置。系统需要在一个摄像头中识别移动对象,然后跨摄像头重新识别对象以建立一致的标识。

图 10.7 多摄像头数据集显示了人员在摄像头之间的存在。(来源:[4]。)
这个问题通常被称为人员重识别。请注意,它与面部验证系统类似,我们感兴趣的是捕捉在单独摄像头中的任何两个人是否相同,而不需要确切知道一个人的身份。
在所有这些应用中,一个核心方面是依赖于一个嵌入函数,该函数能够捕捉并保留输入与输出嵌入空间之间的相似性(和差异性)。在接下来的章节中,我们将深入探讨设计适当的损失函数和采样(挖掘)信息数据点,以指导 CNN 的训练,以实现高质量的嵌入函数。
但在我们深入探讨创建嵌入的细节之前,让我们回答这个问题:为什么我们需要嵌入——我们难道不能直接使用图像吗?让我们回顾一下直接使用图像像素值作为嵌入的这种天真方法的瓶颈。在这种方法中,嵌入的维度(假设所有图像都是高分辨率的)将是 1920 × 1080,以双精度在计算机内存中表示,这给存储和检索带来了计算上的限制,考虑到任何有意义的时要求。此外,大多数嵌入都需要在监督设置中学习,因为事先的语义比较是未知的(这就是我们释放 CNN 提取有意义和相关性语义的力量的时候)。任何在这种高维嵌入空间上的学习算法都将受到维度灾难的影响:随着维度的增加,空间的体积增加得如此之快,以至于可用的数据变得稀疏。
自然数据的几何和数据分布是非均匀的,围绕低维结构拼接。因此,使用图像大小作为数据维度是过度的(更不用说巨大的计算复杂性和冗余了)。因此,我们在学习嵌入时的目标是双重的:学习所需的语义以进行比较,并实现嵌入空间低(或更低)的维度。
10.2 学习嵌入
学习嵌入函数涉及定义一个期望的准则来衡量相似性;它可以基于颜色、图像中存在的对象的语义,或者纯粹在监督形式中数据驱动。由于事先知道正确的语义(用于比较图像)很困难,因此监督学习更受欢迎。我们不会手动制作相似性准则特征,在本章中,我们将专注于监督数据驱动的嵌入学习,其中我们假设我们被给定一个训练集。图 10.8 描述了使用深度 CNN 学习嵌入的高级架构。

图 10.8 学习机器的示意图(顶部);(测试)过程概述(底部)。
学习嵌入的过程很简单:
-
选择一个卷积神经网络(CNN)架构。任何合适的 CNN 架构都可以使用。在实践中,最后一层全连接层用于确定嵌入。因此,这个全连接层的大小决定了嵌入向量空间的大小。根据训练数据集的大小,使用例如 ImageNet 数据集进行预训练可能是明智的。
-
选择一个损失函数。流行的损失函数是对比损失和三元组损失。(这些在 10.3 节中解释。)
-
选择数据集采样(挖掘)方法。天真地提供数据集中所有可能的样本是浪费且不可行的。因此,我们需要求助于采样(挖掘)信息数据点来训练我们的 CNN。我们将在第 10.4 节学习各种采样技术。
-
在测试时,最后一层全连接层充当相应图像的嵌入。
现在我们已经回顾了学习嵌入的训练和推理过程的大致情况,我们将深入探讨定义有用的损失函数以表达我们期望的嵌入目标。
10.3 损失函数
我们在第二章中了解到,优化问题需要定义一个损失函数来最小化。学习嵌入与其他深度学习问题没有不同:我们首先定义一个需要最小化的损失函数,然后训练一个神经网络来选择参数(权重)值,以产生最小的误差值。在本节中,我们将更深入地探讨关键的嵌入损失函数:交叉熵、对比和三元组。
首先,我们将形式化问题设置。然后,我们将探讨不同的损失函数及其数学公式。
10.3.1 问题设置和形式化
为了理解学习嵌入的损失函数并最终训练一个 CNN(为此损失),我们首先形式化输入成分和期望的输出特性。这种形式化将在后面的章节中以简洁的方式理解和分类各种损失函数。为了这次对话的目的,我们的数据集可以表示如下:
χ = {(x[i] , y[i])}[i]^N =1
N是训练图像的数量,x[i]是输入图像,y[i]是它的对应标签。我们的目标是创建一个嵌入
f (x ; θ): ℝ^D → ℝ^F
将图像从ℝD*映射到ℝ*D的特征(嵌入)空间,使得具有相似身份的图像在这个特征空间中是度量上接近的(反之亦然,对于具有不同身份的图像而言)。
θ* = arg [θ] minℒ( f (θ; χ ))
其中θ是学习函数的参数集。
让
D(x[i], x[j]) : ℝ^F X ℝ^F → ℝ
作为嵌入空间中图像x[i]和x[j]之间的距离度量。为了简单起见,我们省略了输入标签,并将D(x[i], x[j])表示为D[ij] · y[ij] = 1。这两个样本(i)和(j)属于同一类,且y[ij] = 0 表示不同类别的样本。
一旦我们训练了一个嵌入网络的优化参数,我们希望学习到的函数具有以下特性:
-
嵌入应该对视图、光照和形状变化不敏感。
-
从实际应用部署的角度来看,嵌入和排名的计算应该是高效的。这要求一个低维向量空间(嵌入)。这个空间越大,比较任意两个图像所需的计算就越多,这反过来又影响了时间复杂度。
学习嵌入的流行选择是交叉熵损失、对比损失和三元组损失。接下来的章节将介绍和形式化这些损失。
10.3.2 交叉熵损失
学习嵌入也可以被表述为一个细粒度的分类问题,相应的卷积神经网络可以使用流行的交叉熵损失(在第二章中详细解释)。以下方程表达了交叉熵损失,其中 p(y[ij] | f (x; θ )) 表示后验类别概率。在 CNN 文献中,softmax 损失意味着使用交叉熵损失在判别性区域训练的 softmax 层:

在训练过程中,在损失层之前添加了一个全连接(嵌入)层。每个身份被视为一个单独的类别,类别的数量等于训练集中身份的数量。一旦使用分类损失训练了网络,最终的分类层就被移除,并从网络的新最终层中获得嵌入(图 10.9)。

图 10.9 展示了如何使用交叉熵损失来训练嵌入层(全连接)。右侧展示了推理过程,并概述了在直接使用交叉熵损失学习嵌入时的训练和推理之间的不连续性。(此图改编自[5])
通过最小化交叉熵损失,CNN 的参数(θ)被选择,使得对于正确类别,估计概率接近 1,而对于所有其他类别,估计概率接近 0。由于交叉熵损失的目标是将特征分类到预定义的类别中,因此,与在训练过程中直接在嵌入空间中包含相似性(和差异性)约束的损失相比,这种网络的性能通常较差。此外,当考虑例如 1 百万个身份的数据集时,学习变得计算上不可行。(想象一个有 1 百万个神经元的损失层!)尽管如此,使用交叉熵损失(在数据集的可操作子集上,例如 1,000 个身份的子集)预训练网络是一种流行的策略,这反过来又使得嵌入损失更快地收敛。我们将在第 10.4 节中进一步探讨这一点,在训练过程中挖掘信息样本。
注意:交叉熵损失的一个缺点是训练和推理之间的不连续性。因此,与嵌入学习损失(对比和三元组)相比,它通常表现较差。这些损失明确尝试将输入图像空间到嵌入空间的相对距离保持下来。
10.3.3 对比损失
对比损失通过鼓励所有相似类实例无限接近彼此,同时强制其他类别的实例在输出嵌入空间中远离彼此来优化训练目标(我们在这里说无限接近,因为 CNN 不能使用精确为零的损失进行训练)。使用我们的问题形式化,这种损失定义为
是衡量嵌入空间中图像 x[i] 和 x[j] 距离的度量。为了简单起见,我们省略了输入标签,并将 D(x[i], x[j]) 表示为 D[ij] · y[ij] = 1。两个样本 ( i ) 和 ( j ) 属于同一类别,且 y[ij] = 0 的值表示不同类别的样本。
对比损失 (i, j) = y[ij] D²[ij] + (1 - y[ij])[α - D²[ij]][+]
注意,在损失函数中的 [.]+ = max(0,.) 表示 hinge 损失,α是一个预定的阈值(margin),用于确定当两个样本 i 和 j 属于不同类别时的最大损失。从几何上讲,这意味着只有当两个样本在嵌入空间中的距离小于这个 margin 时,不同类别的两个样本才会对损失做出贡献。Dij,如公式中所述,指的是两个样本 i 和 j 在嵌入空间中的距离。
这种损失也被称为 Siamese 损失,因为我们可以将其可视化为具有共享参数的孪生网络;两个 CNN 各自输入一张图像。对比损失在 Chopra 等人[6]的开创性工作中被用于人脸验证问题,其目标是验证两个展示的面孔是否属于同一身份。这种损失的示例在图 10.10 中提供了人脸识别的上下文。

图 10.10 计算对比损失需要两张图像。当两张图像属于同一类别时,优化尝试在嵌入空间中将它们放得更近,反之亦然,当图像属于不同类别时。
注意,所有不同类别的 margin α 的选择是相同的。Manmatha 等人[7]分析了其影响:这种α的选择意味着对于不同的身份,视觉上不同的类别被嵌入到与视觉上相似的类别相同的特征空间中。与三元组损失(下文将解释)相比,这种假设更为严格,并限制了嵌入流形的结构,这随后使得学习变得更加困难。对于包含 N 个样本的数据集,每个 epoch 的训练复杂度为 O(N²),因为这种损失需要遍历一对样本来计算对比损失。
10.3.4 三元组损失
受 Weinberger 等人[8]关于最近邻分类的度量学习开创性工作的启发,FaceNet(Schroff 等人[9])提出了一种适用于查询检索任务的修改版,称为三元组损失。三元组损失通过考虑来自同一点的正负对距离,为损失函数添加了上下文。从数学上讲,与之前的问题形式化相关,三元组损失可以表示如下:
三元组 (a, p, n) = [D[ap] − D[an] + α][+]
注意,Dap 代表锚点和正样本之间的距离,而 Dan 是锚点和负样本之间的距离。图 10.11 展示了使用锚点、正样本和负样本计算损失项的过程。在成功训练后,我们希望所有同一类别的对比都会比不同类别的对对比得更近。

图 10.11 计算三元组损失需要三个样本。学习的目标是使同一类别的样本嵌入比不同类别的样本更近。
由于计算三元组损失需要三个项,每个 epoch 的训练复杂度是O(N³),这在实际数据集上计算上是不可行的。三元组和对比损失中的高计算复杂度激发了许多用于高效优化和收敛的采样方法。让我们回顾一下以天真和直接的方式实现这些损失函数的复杂度。
10.3.5 损失函数的简单实现和运行时分析
考虑以下规格的玩具示例:
-
身份数(N):100
-
每个身份的样本数(S):10
如果我们以天真方式实现损失函数(参见图 10.12),会导致每个 epoch(图 10.12 中的内部for循环 1)的训练复杂度:
-
交叉熵损失 --这是一个相对简单的损失。在一个 epoch 中,它只需要遍历所有样本。因此,这里的复杂度是O(N × S) = O(10³)。
-
对比损失 --这个损失会访问所有成对距离,所以从样本数量来看,复杂度是二次的(N × S):即O(100 × 10 × 100 × 10) = O(10⁶)。
-
三元组损失 --对于每次损失计算,我们需要访问三个样本,所以最坏情况下的复杂度是立方。从样本总数来看,那就是O(10⁹)。

图 10.12 算法 1,对于简单实现
尽管计算交叉熵损失很容易,但与其他嵌入损失相比,其性能相对较低。一些直观的解释在 10.3.2 节中提出。在最近的学术作品中(如[10, 11, 13]),三元组损失在提供适当的硬数据挖掘时,通常比对比损失给出更好的结果,我们将在下一节中解释。
注意:在以下章节中,我们提到三元组损失,因为它在多个学术作品中对比损失具有更高的性能。
一个需要注意的重要点是,在 O(10⁹)的三元组中,并不是很多对损失有显著的贡献。在实践中,在一个训练周期内,大多数的三元组都是微不足道的:也就是说,当前网络在这些数据上的损失已经很低,因此这些微不足道三元组的锚正对(在嵌入空间中)比锚负对更接近。这些微不足道的三元组不会为更新网络参数提供有意义的信息,从而阻碍了收敛。此外,信息三元组比微不足道三元组要少得多,这反过来又导致了信息三元组贡献的稀释。
为了提高三元组枚举的计算复杂度和收敛性,我们需要提出一种有效的策略来枚举三元组,并在训练过程中向 CNN(无微不足道三元组)提供信息三元组样本。这个过程选择信息三元组被称为挖掘。信息数据点是本章的核心,将在以下几节中讨论。
一种处理这种立方复杂度的流行策略是以下方式枚举三元组:
-
使用数据加载器构建的当前批次仅构建三元组集合。
-
从这个集合中挖掘一个信息三元组子集。
下一个部分将详细探讨这种策略。
10.4 挖掘信息数据
到目前为止,我们已经探讨了三元组和对比损失在计算上对实际数据集大小的不切实际。在本节中,我们将深入了解训练 CNN 进行三元损失时的关键步骤,并学习如何提高训练收敛性和计算复杂度。
图 10.12 中的直接实现被归类为离线训练,因为三元组的选取必须考虑整个数据集,因此在训练 CNN 时不能即时完成。正如我们之前提到的,这种计算有效三元组的方法效率低下,对于深度学习数据集来说在计算上是不可行的。
为了处理这种复杂性,FaceNet [9] 提出使用基于在线批次的三元组挖掘。作者动态构建一个批次,并对该批次进行三元组的挖掘,忽略该批次之外的数据集。这种策略被证明是有效的,并导致了人脸识别中的最先进准确率。
让我们总结一下训练周期中的信息流(见图 10.13)。在训练过程中,从数据集中构建小批量,然后为小批量中的每个样本识别有效三元组。然后使用这些三元组来更新损失,这个过程迭代进行,直到所有批次耗尽,从而完成一个周期。

图 10.13 在线训练过程中的信息流。数据加载器从训练数据中采样一个随机子集到 GPU。随后,计算三元组以更新损失。
与 FaceNet 类似,OpenFace [37] 提出了一种训练方案,其中数据加载器构建一个具有预定义统计信息的训练批次,并在 GPU 上计算批次的嵌入。随后,在 CPU 上生成有效三元组以计算损失。
在下一小节中,我们将探讨一个改进的数据加载器,它可以为我们提供良好的批次统计信息以挖掘三元组。随后,我们将探讨如何有效地挖掘好的、信息丰富的三元组以改善训练收敛性。
10.4.1 数据加载器
让我们检查数据加载器的设置及其在三元组损失训练中的作用。数据加载器从数据集中选择一个随机子集,对于挖掘信息三元组至关重要。如果我们依赖于一个平凡的数据加载器来选择数据集的随机子集(迷你批次),则可能不会导致良好的类别多样性,从而无法找到许多三元组。例如,随机选择仅包含一个类别的批次将没有任何有效三元组,因此会导致无效的批次迭代。我们必须在数据加载器级别上确保批次分布良好,以便挖掘三元组。
注意:在数据加载器级别上实现更好收敛的要求是形成一个具有足够类别多样性的批次,以促进图 10.11 中的三元组挖掘步骤。
训练的一般有效方法是首先挖掘一组大小为 B 的三元组,以便 B 个项对三元组损失做出贡献。一旦选择了 B,它们的图像将被堆叠以形成一个 3B 图像的批次大小(B 个锚点,B 个正样本和负样本),然后计算 3B 个嵌入以更新损失。
Hermans 等人[11]在他们对重新审视三元组损失所做的令人印象深刻的工作中,意识到在前一节中在线生成中有效三元组的低利用率。在一组 3B 张图像(B 个锚点,B 个正样本,B 个负样本)中,我们总共有 6B 个 2 - 4B 个有效三元组,因此仅使用 B 个三元组是低效的。
计算堆叠的 3B 张 B 三元组图像中的有效三元组数量
为了理解在 3B 图像堆叠中计算有效三元组数量(即 B 个锚点,B 个正样本和负样本),让我们假设我们恰好有一对相同的类别。这意味着我们可以为(锚点,正样本)对选择 3B - 2 个负样本。在这个集合中有 2B 个可能的锚点-正样本对,导致总共 2B (3B - 2)个有效三元组。以下图示了一个示例。

B = 3 的示例。具有相同模式的圆圈属于同一类别。由于只有前两列有可能的正样本,总共有 2B(六个)锚点。选择一个锚点后,我们剩下 3B - 2(七个)负样本,这意味着总共有 2B (3B - 2)个三元组。
基于之前的讨论,为了更有效地使用三元组,Hermans 等人提出在数据加载器级别进行一项关键的组织性修改:通过从数据集 x 中随机采样 P 个身份,然后为每个身份采样 K 张图像(随机),从而得到一个大小为 PK 的批次。使用这个数据加载器(适当的三元组挖掘),作者展示了在人员重识别任务上达到最先进的准确率。我们将在接下来的子节中更详细地探讨[11]中引入的挖掘技术。使用这种组织性修改,Kumar 等人[10, 12]在许多不同的数据集上展示了车辆重识别任务的最先进结果。
由于在重识别任务上取得了优越的结果,[11]已成为识别文献中的主要支柱,批次构建(数据加载器)现在已成为实践中的标准。默认推荐批次大小为 P = 18,K = 4,导致 42 个样本。
计算有效三元组的数量
让我们通过一个计算批次中有效三元组数量的工作示例来使这个概念更清晰。假设我们随机选择了一个大小为 PK 的批次:
-
P = 10 个不同的类别
-
每个类别的样本数 = 4 个样本
使用这些值,我们得到以下批次统计信息:
-
锚点的总数 = 40 = (PK)
-
每个锚点的正样本数量 = 3 = (K - 1)
-
每个锚点的负样本数量 = 9 × 4 = (K(P - 1))
-
有效三元组的总数 = 前述结果的乘积 = 40 × 3 × (9 × 4)
在挖掘信息三元组的前瞻概念中窥视,注意到对于每个锚点,我们都有一个正样本集和一个负样本集。我们之前已经论证了许多三元组是非信息性的,因此在后续章节中,我们将探讨各种过滤重要三元组的方法。更确切地说,我们检查帮助过滤正负样本集中信息子集的技术(对于一个锚点)。
现在我们已经构建了一个用于挖掘三元组的有效数据加载器,我们准备探索在训练 CNN 时挖掘信息三元组的各种技术。在接下来的章节中,我们首先概述硬数据挖掘,然后专注于在线生成(挖掘)信息三元组,这遵循了[11]中的批次构建方法。
10.4.2 信息性数据挖掘:寻找有用的三元组
在训练机器学习模型时挖掘信息样本是一个重要问题,学术文献中存在许多解决方案。我们在这里简要地看一下它们。
一种流行的采样方法来寻找信息样本是困难数据挖掘,它在许多计算机视觉应用中如目标检测和动作定位中被使用。困难数据挖掘是一种用于模型迭代训练的自举技术:在每次迭代中,当前模型应用于验证集以挖掘模型表现不佳的困难数据。然后只将这组困难数据呈现给优化器,这增加了模型有效学习和更快收敛到最优的能力。另一方面,如果模型只接触到困难数据,这些数据可能包含异常值,那么它对正常数据的异常值判别能力会受到影响,从而阻碍训练进度。数据集中的异常值可能是标签错误或图像质量差的样本捕获的结果。
在三元损失的上下文中,一个困难负样本是离锚点更近的样本(因为这个样本会导致高损失)。同样,一个困难正样本是在嵌入空间中远离锚点的样本。
为了在困难数据采样期间处理异常值,FaceNet [9] 提出了半困难采样,它挖掘既不太困难也不太平凡的三元组,以便在训练期间获得有意义的梯度。这是通过使用边界参数来实现的:只有位于边界并且比选定的正样本对锚点更远的负样本被考虑(见图 10.14),从而忽略了太容易和太困难的负样本。然而,这反过来又增加了调整额外超参数的训练负担。这种半困难负样本的临时策略在大批量的 1,800 张图像中得到了实践,从而在 CPU 上枚举三元组。请注意,在[11]中的默认批量大小(42 张图像)中,可以在 GPU 上有效地枚举有效三元组的集合。

图 10.14 边界:将三元组分为困难、半困难和容易。这个插图(在人脸识别的上下文中)是一个锚点和相应的负样本。因此,靠近锚点的负样本是困难的。
图 10.15 说明了三元组的困难程度。记住,如果一个正样本在训练时间步的网络中离其锚点在嵌入空间中很远,那么这个正样本就更加困难。同样,在从锚点到负数据的距离图中,靠近(距离更短)锚点的样本更困难。作为提醒,以下是锚点(a)、正样本(p)和负样本(n)的三元损失函数:
l[triplet] (a, p, n) = [D[ap] − D[an] + α][+]

图 10.15 困难正样本和困难负样本数据。图显示了正样本(顶部)和负样本(底部)相对于锚点(在特定时间步)的距离。样本的困难程度随着我们在两个图上从左到右移动而增加。
在探讨了硬数据及其缺陷的概念之后,我们现在将探讨针对我们的批次的多种在线三元组挖掘技术。一旦数据加载器构建了一个批次(大小为 PK),就有 PK 个可能的锚点。如何为这些锚点找到正负数据是挖掘技术的关键。首先,我们来看两种简单而有效的在线三元组挖掘技术:批全(BA)和批硬(BH)。
10.4.3 批全(BA)
在批次的上下文中,批全(BA)指的是使用所有可能的和有效的三元组;也就是说,我们不会对三元组进行任何排序或选择。在实现方面,对于一个锚点,这个损失是通过对所有可能的有效三元组求和来计算的。对于一个包含 PK 个图像的批次,由于 BA 选择了所有三元组,因此更新三元组损失的项数是 PK(K - 1)(K(P - 1))。
使用这种方法,所有样本(三元组)都同等重要;因此,这很容易实现。另一方面,BA 可能导致信息平均化。一般来说,许多有效的三元组是平凡的(损失低或非信息性),而只有少数是有信息的。对所有有效的三元组使用相同的权重求和会导致有信息三元组的贡献平均化。Hermans 等人[11]经历了这种平均化,并在人重识别的背景下进行了报告。
10.4.4 批硬(BH)
与 BA 不同,批硬(BH)只考虑锚点的最硬数据。对于批次中的每个可能的锚点,BH 使用一个最硬的正数据项和一个最硬的负数据项来计算损失。请注意,在这里,数据点的硬度相对于锚点是相对的。对于一个包含 PK 个图像的批次,由于 BH 对每个锚点只选择一个正样本和一个负样本,因此更新三元组损失的项数是 PK(可能的锚点总数)。
BH 对信息平均化具有鲁棒性,因为简单的(较容易的)样本被忽略了。然而,它可能难以区分异常值:异常值可能由于错误的标注而渗入,并且模型会努力收敛到它们,从而危及训练质量。此外,在使用 BH 之前使用未预训练的网络时,样本的硬度(相对于锚点)无法可靠地确定。在训练过程中无法获得此类信息,因为最硬的样本现在是任何随机样本,这可能导致训练停滞。这已在[9]中报告,并且在[10]中将 BH 应用于从零开始训练车辆重识别网络时也出现了这种情况。
为了直观地理解 BA 和 BH,让我们再次查看我们用来表示锚点与所有正负数据距离的图(图 10.16)。BA 不进行选择,使用所有五个样本来计算最终的损失,而 BH 只使用最硬的数据(忽略所有其他数据)。图 10.17 显示了计算 BH 和 BA 的算法概述。
三重损失函数的另一种形式化
Ristani 等人[13]在他们关于多摄像头重识别特征的著名论文中,将各种批量采样技术统一在一个表达式中。在一个批次中,设 a 为一个锚点样本,N(a)和 P(a)代表对应锚点 a 的负样本和正样本的子集。然后三重损失可以写成

对于锚点样本 a,wp 代表正样本 p 的权重(重要性);同样,wn 表示负样本 n 的重要性。在一个时间步的总损失通过以下方式获得
![图 10-17_Eb.png]
在这个公式中,BA 和 BH 可以像以下图所示那样集成(也参见下一节中的表 10.1)。这个图中的 Y 轴表示选择权重。

展示了正样本相对于锚点的选择权重。对于 BA,所有样本同等重要,而 BH 只重视最困难的样本(其余的都被忽略)。

图 10.16 困难数据的说明:正样本与锚点(在特定时间步)的距离(左);负样本与锚点的距离(右)。BA 考虑了所有样本,而 BH 只考虑最右侧的条形(这个批次的最难正数据)。

图 10.17 计算 BA 和 BH 的算法
10.4.5 批量加权(BW)
BA 是一种简单的采样,对所有样本进行均匀加权。这种均匀的权重分布可能会忽略重要困难样本的贡献,因为这些样本通常被琐碎的简单样本所超过。为了缓解 BA 中的这个问题,Ristani 等人[13]采用了一种批量加权(BW)的加权方案:一个样本的权重基于其与相应锚点的距离,从而比简单样本赋予信息性(更困难)样本更多的重视。正负数据的相应权重在表 10.1 中显示。图 10.18 展示了这种技术中样本的加权情况。
表 10.1 矿掘良好正 xp 和负 xn 的各种方式的快照[10]。BS 和 BW 将在下一节中通过示例进行探讨。
| 矿掘 | 正样本权重:wp | 负样本权重:wn | 评论 |
|---|---|---|---|
| 所有(BA) | 1 | 1 | 所有样本都进行均匀加权。 |
| 困难(BH) | ![]() |
![]() |
选择一个最困难的样本。 |
| 样本(BS) | ![]() |
![]() |
从多项式分布中选择一个。 |
| 加权(BW) | ![]() |
![]() |
权重基于其与锚点的距离进行采样。 |

图 10.18 BW 选择左图中锚点正数据的 BW 示意图。在这种情况下,使用了所有五个正样本(如 BA),但每个样本都被分配了权重。与 BA 不同,BA 对每个样本的权重相同,而右边的图按与锚点相应的距离比例对每个样本进行加权。这意味着我们更加关注远离锚点的正样本(因此更难且更有信息量)。对于这个锚点的负数据也是以同样的方式选择的,但权重相反。
10.4.6 批量样本(BS)
另一种采样技术是批量样本(BS);它在 Hermans 等人[11]的实现页面上被积极讨论,并且已经被 Kumar 等人[10]用于最先进的车辆再识别。BS 使用锚点到样本的距离分布来挖掘 2 个正负数据样本用于锚点(见图 10.19)。与 BH 相比,这种技术避免了采样异常值,并且它还希望确定最相关的样本,因为采样是使用距离到锚点的分布进行的。

图 10.19 BS 选择锚点正数据的示意图。与 BH 类似,目标是找到一个信息丰富且不是异常值的数据项(对于左图中的锚点)。BH 会选择最困难的数据项,这可能导致找到异常值。BS 使用距离作为分布来以分类方式挖掘样本,从而选择一个信息丰富且可能不是异常值的样本。(注意,这是一个随机多项式选择;我们在这里选择第三个样本只是为了说明这个概念。)
现在,让我们通过一个项目和深入了解训练和测试 CNN 进行嵌入所需的机制来解开这些想法。
10.5 项目:训练嵌入网络
在这个项目中,我们通过构建一个基于图像的查询检索系统将我们的概念付诸实践。我们选择了在视觉嵌入文献中流行的两个问题,并且已经积极研究以找到更好的解决方案:
-
购物困境--找到与我查询项目相似的服装。
-
再识别--在数据库中找到相似车辆;即从不同视角(摄像头)识别一辆车。
不论是哪些任务,训练、推理和评估过程都是相同的。以下是成功训练嵌入网络的一些关键要素:
-
训练集--我们遵循一个带有注释的监督学习方法,这些注释强调了固有的相似性度量。数据集可以被组织成一系列文件夹,每个文件夹确定图像的标识/类别。目标是使属于同一类别的图像在嵌入空间中彼此更接近,反之亦然。
-
测试集--测试集通常分为两个集合:查询集和图库集(通常,学术论文将图库集称为测试集)。查询集由用作查询的图像组成。图库集中的每张图像都与每张查询图像进行排名(检索)。如果嵌入被完美学习,则查询的最高排名(检索)物品都属于同一类。
-
距离度量--为了在嵌入空间中表达两张图像之间的相似性,我们使用相应的嵌入之间的欧几里得(L2)距离。
-
评估--为了定量评估训练好的模型,我们使用第四章和第七章中解释的 top-k 准确率和平均精度(mAP)指标。对于查询集中的每个对象,目标是检索测试集(图库集)中相似的身份。对于查询图像 q 的 AP(q) 定义为
![图片]()
其中 P(k) 表示排名 k 的精确度,Ngt(q) 是查询 q 的总真实检索数,δk 是一个布尔指示函数。所以,当查询图像 q 与测试图像的正确匹配发生在排名 k 时,其值为 1。正确的检索意味着查询和测试的地面真实标签是相同的。
然后计算 mAP 作为所有查询图像的平均值
![图片]()
其中 Q 是查询图像的总数。以下几节将更详细地探讨这两个任务。
10.5.1 时尚:给我找类似这个的物品
第一个任务是确定在商店拍摄的两张图片是否属于同一服装物品。与时尚相关的购物对象(衣服、鞋子)是工业应用中视觉搜索的关键领域,例如图像推荐引擎,这些引擎推荐与购物者所寻找的产品相似的产品。刘等人 [3] 为购物图像检索任务引入了最大的数据集之一(DeepFashion)。该基准包含来自流行的 Forever 21 目录的 11,735 件服装的 54,642 张图片。该数据集包括 25,000 张训练图像和大约 26,000 张测试图像,分为查询集和图库集;图 10.20 显示了样本图像。

图 10.20 每一行表示一个特定的类别及其相应的相似图像。一个完美学习的嵌入将使每行中图像的嵌入彼此更接近,而不是任何两列图像(属于不同的服装类别)之间的图像。(此图中的图像取自 DeepFashion 数据集 [3]。)
10.5.2 车辆重新识别
重新识别是匹配相机网络中及跨相机网络中物体外观的任务。通常的流程涉及用户在网络的全部相机中寻找查询物体出现的所有实例。例如,交通管理员可能在全市范围的相机网络中寻找一辆特定的汽车。其他例子包括人和面部重新识别,这些在安全和生物识别领域是主流。
本任务使用来自 Liu 等人的著名 VeRi 数据集 [14, 36]。该数据集包含 776 辆车(身份)在交通监控场景中 20 个摄像头下的 40,000 个边界框标注;图 10.21 展示了样本图像。每辆车由 2 到 18 个摄像头从不同的视角和不同的光照条件下捕捉。值得注意的是,视角不仅限于前/后视图,还包括侧面视图,从而使这个数据集更具挑战性。标注包括车辆的品牌和型号、颜色以及相机间关系和轨迹信息。

图 10.21 每一行表示一个车辆类别。与服装任务类似,这里的目的是(训练嵌入 CNN)使同一类别的嵌入比不同类别的嵌入更接近。(此图中的图像来自 VeRi 数据集 [14]。)
我们将仅使用类别(或身份)级别的标注;我们不会使用品牌、型号和时空位置等属性。在训练过程中加入更多信息可能有助于提高准确性,但这超出了本章的范围。然而,本章的最后部分引用了一些关于在嵌入学习中结合多源信息的新发展。
10.5.3 实现方法
本项目使用与 [11] 相关的 GitHub 代码库的 triplet learning(github.com/VisualComputingInstitute/triplet-reid/tree/sampling)。数据预处理和步骤总结可在本书的可下载代码中找到;前往项目的 Jupyter notebook,可以跟随项目实现的逐步教程。鼓励 TensorFlow 用户查看 Olivier Moindrot 的博客文章“TensorFlow 中的 Triplet Loss 和在线 Triplet Mining”(omoindrot.github.io/triplet-loss),以了解实现 triplet loss 的各种方法。
训练深度卷积神经网络(CNN)涉及几个关键的超参数,我们在此简要讨论它们。以下是本项目设置的超参数总结:
-
预训练在 ImageNet 数据集 [15] 上进行。
-
输入图像大小为 224 × 224。
-
元架构:我们使用 Mobilenet-v1 [16],它有 569 百万 MACs,并测量融合的乘法和加法操作的数量。这个架构有 424 万个参数,在 ImageNet 的图像分类基准测试中实现了 70.9% 的 top-1 准确率,输入图像大小为 224 × 224。
-
优化器:我们使用默认超参数的 Adam 优化器 [17](ε = 10^-3,β1 = 0.9,β2 = 0.999)。初始学习率设置为 0.0003。
-
数据增强通过在线方式使用标准的图像翻转操作进行。
-
批量大小为 18(P)随机采样的身份,每个身份 4(K)个样本,因此每个批次共有 18 × 4 个样本。
-
边距:作者将 hinge 损失 [.]+ 替换为一种称为 softplus 的平滑变化:ln(1 + .)。我们的实验也应用 softplus 而不是使用硬边距。
-
嵌入维度对应于最后一个全连接层的维度。我们将其固定为所有实验的 128 个单位。使用较小的嵌入大小有助于提高计算效率。
定义:在计算机科学中,乘累加操作是一个常见的步骤,它计算两个数字的乘积并将该乘积加到累加器中。执行此操作的硬件单元称为乘累加器(MAC,或 MAC 单元);该操作本身也常被称为 MAC 或 MAC 操作。
关于与最先进方法的比较的说明
在深入比较之前,请记住,训练一个深度神经网络需要调整几个超参数。这反过来可能导致比较几个算法时的陷阱:例如,如果底层 CNN 在相同的预训练数据集上表现良好,则一种方法可能会表现得更好。其他类似的超参数包括训练算法的选择(例如 vanilla SGD 或更复杂的 Adam)以及我们在本书中看到的许多其他参数。你必须深入了解算法的机制,才能看到完整的图景。
10.5.4 测试训练好的模型
要测试一个训练好的模型,每个数据集都包含两个文件:一个查询集和一个画廊集。这些集合可以用来计算前面提到的评估指标:mAP 和 top-k 准确率。虽然评估指标是一个很好的总结,但我们也会从视觉上查看结果。为此,我们从查询集中随机选取图像,并从画廊集中找到(绘制)top-k 检索结果。以下小节展示了使用本章中各种挖掘技术的定量和定性结果。
任务 1:店内检索
让我们看看图 10.22 中的学习嵌入的样本检索。结果看起来视觉上很吸引人:top 检索来自与查询相同的类别。网络在推断排名靠前的相同查询的不同视图方面表现合理。

图 10.22 使用各种嵌入方法从时尚数据集中检索的样本。每一行表示查询图像及其查询图像的 top-5 检索。一个 x 表示一个错误的检索。
表 10.2 概述了在各种采样场景下 triplet 损失的性能。BW 在所有采样方法中表现最佳。在这种情况下,top-1 准确率相当好:我们能够在第一次检索中检索到相同类别的时尚物品,准确率超过 87%。请注意,在评估设置中,k > 1 的 top-k 准确率更高(单调递增)。
表 10.2 各种采样方法在店内检索任务上的性能
| 方法 | top-1 | top-2 | top-5 | top-10 | top-20 |
|---|---|---|---|---|---|
| 批量所有 | 83.79 | 89.81 | 94.40 | 96.38 | 97.55 |
| 批量硬匹配 | 86.40 | 91.22 | 95.43 | 96.85 | 97.83 |
| 批量采样 | 86.62 | 91.36 | 95.36 | 96.72 | 97.84 |
| 批量加权 | 87.70 | 92.26 | 95.77 | 97.22 | 98.09 |
| 胶囊嵌入 | 33.90 | - | - | 75.20 | 84.60 |
| ABE [18] | 87.30 | - | - | 96.70 | 97.90 |
| BIER [19] | 76.90 | - | - | 92.80 | 95.20 |
我们的结果与最先进的结果相比表现良好。使用基于注意力的集成(ABE)[18],训练了多种关注图像不同部分的集成。通过增强独立嵌入的鲁棒性(BIER)[19],将具有共享特征表示的度量 CNN 集成训练为一个在线梯度提升问题。值得注意的是,这个集成框架不引入任何额外的参数(并且与任何差异损失一起工作)。
任务 2:车辆重识别
Kumar 等人 [12] 最近对优化三元组损失的采样变体进行了彻底评估。结果总结在表 10.3 中,并与几种最先进的方法进行了比较。值得注意的是,作者在没有使用任何其他信息源(如时空距离和属性)的情况下,与最先进的方法相比表现良好。定性结果如图 10.23 所示,展示了嵌入对视角的鲁棒性。请注意,检索具有所需的视角不变性属性,因为同一辆车的不同视角被检索到 top-5 排名中。
表 10.3 在 VeRi 数据集上比较各种提出的方法。星号 (*) 表示使用了时空信息。
| 方法 | mAP | top-1 | top-5 |
|---|---|---|---|
| 批量采样 | 67.55 | 90.23 | 96.42 |
| 批量硬匹配 | 65.10 | 87.25 | 94.76 |
| 批量全部 | 66.91 | 90.11 | 96.01 |
| 批量加权 | 67.02 | 89.99 | 96.54 |
| GSTE [20] | 59.47 | 96.24 | 98.97 |
| VAMI [21] | 50.13 | 77.03 | 90.82 |
| VAMI+ST * [21] | 61.32 | 85.92 | 91.84 |
| Path-LSTM * [22] | 58.27 | 83.49 | 90.04 |
| PAMTRI (RS) [23] | 63.76 | 90.70 | 94.40 |
| PAMTRI (All) [23] | 71.88 | 92.86 | 96.97 |
| MSVR [24] | 49.30 | 88.56 | - |
| AAVER [25] | 61.18 | 88.97 | 94.70 |

图 10.23 使用各种嵌入方法在 VeRi 数据集上的样本检索。每一行表示一个查询图像及其 top-5 检索结果。星号 (*) 表示一个错误的检索。
为了评估文献中各种方法的优缺点,让我们从概念上考察车辆重识别中的竞争方法:
-
Kanaci 等人 [26] 基于使用模型标签的损失函数(见图 10.24)提出了基于交叉级别的车辆重识别(CLVR)。这种设置与我们第 10.3.2 节和图 10.9 中看到的是相似的。作者没有在 VeRi 数据集上进行评估。鼓励您参考这篇论文以了解在其他车辆重识别数据集上的性能。
![]()
图 10.24 跨级车辆重识别(CLVR)。(来源:[24]。)
-
Bai 等人[20]提出的组敏感三元组嵌入(GSTE)是一种新颖的训练过程,它使用 K-Means 聚类来聚类类内变化。这以增加一个额外参数 K-Means 聚类为代价,帮助进行更有指导性的训练。
-
Zheng 等人[23]提出的姿态感知多任务学习(PAMTRI)通过结合合成数据(从而解决关键点标注需求)和关键点标注来训练一个多任务环境下的嵌入网络。PAMTRI(All)在此数据集上取得了最佳结果。PAMTRI(RS)使用真实和合成数据的混合来学习嵌入,而 PAMTRI(All)还额外在多任务学习框架中使用车辆关键点和属性。
-
由 Khorramshahi 等人[25]提出的自适应注意力车辆重识别(AAVER)是一种近期的研究成果,其中作者构建了一个双路径网络以提取全局和局部特征。这些特征随后被连接起来形成一个最终的嵌入。所提出的嵌入损失是通过身份和关键点方向注释来最小化的。
-
周等人[21]提出了一种用于视角注意力多视角推理(VAMI)的训练过程,包括生成对抗网络(GAN)和多视角注意力学习。作者推测,能够合成(使用 GAN 生成)多个视角视图将有助于学习更好的最终嵌入。
-
使用 Path-LSTM,Shen 等人[22]为他们的时空正则化生成多个路径建议,并需要一个额外的 LSTM 来对这些建议进行排序。
-
Kanaci 等人[24]提出了基于金字塔的深度学习方法的多尺度车辆表示(MSVR)用于重识别。MSVR 通过具有多个分支的网络架构从图像金字塔中学习车辆重识别敏感的特征表示,所有这些分支都是同时优化的。
这些方法关于关键超参数的快照总结见表 10.4。
表 10.4 总结了训练过程中使用的一些重要超参数和标签。
| 方法 | ED | 标注 |
|---|---|---|
| 我们的方法 | 0128 | ID |
| GSTE [20] | 1024 | ID |
| VAMI [21] | 2048 | ID + A |
| PAMTRI (All) [23] | 1024 | ID + K + A |
| MSVR [24] | 2048 | ID |
| AAVER [25] | 2048 | ID + K |
| 备注:ED = 嵌入维度;K = 关键点;A = 属性。 |
通常,车牌是一个全局唯一标识符。然而,由于标准交通摄像机的安装,车牌难以提取;因此,车辆再识别需要基于视觉的特征。如果两辆车是同一品牌、型号和颜色,那么视觉特征无法区分它们(除非有一些独特的标记,如文字或划痕)。在这些困难场景中,只有时空信息(如 GPS 信息)才能有所帮助。要了解更多信息,鼓励大家查阅 Tang 等人[27]最近提出的提议数据集。
10.6 推动当前准确性的边界
深度学习是一个不断发展的领域,每天都有新的训练方法被引入。本节提供了提高当前嵌入水平的一些想法,以及一些最近提出的用于训练深度 CNN 的技巧和窍门:
-
重新排序 -- 在获得画廊图像的初始排名(针对输入查询图像)后,重新排序使用后处理步骤,目的是提高相关图像的排名。这是许多再识别和信息检索系统中广泛使用的一个强大步骤。
再识别中一个流行的方法是 Zhong 等人[28]提出的(见图 10.25)。给定一个探测图像 p 和一个画廊集,为每个人提取外观特征(嵌入)和 k-互反特征。计算每个探测图像和画廊图像对的原始距离 d 和 Jaccard 距离 Jd。最终距离是 d 和 Jd 的组合,并用于获得提出的排名列表。
![]()
图 10.25 Zhong 等人提出的重新排序建议(来源:[28])
最近在车辆再识别领域的一项工作,AAVER [25] 通过后处理使用重新排序提升了 5%的 mAP 准确率。
定义 Jaccard 距离是在两组数据之间计算的,表示两个集合的交集与并集的比例。
-
技巧和窍门 -- Luo 等人[29]在人员再识别任务上展示了强大的基线性能。作者遵循了 Hermans 等人[11](本章研究)相同的批量构建方法,并使用了数据增强、预热学习率和标签平滑等技巧,仅举几例。值得注意的是,作者的表现优于许多最先进的方法。鼓励大家将这些通用技巧应用于训练任何与识别相关的 CNN 任务。
定义 预热学习率是指一个学习率调度策略,该策略将学习率线性地与预定义的初始训练 epoch 数相关联。标签平滑调整交叉熵损失,使得结果损失对训练集的置信度较低,从而有助于模型泛化并防止过拟合。这在小规模数据集中特别有用。
-
注意 --在本章中,我们专注于以全局方式学习嵌入:也就是说,我们没有明确引导网络关注,例如,对象的判别部分。一些采用注意力的突出工作包括 Liu 等人 [30] 和 Chen 等人 [31]。采用注意力还可以帮助提高重识别网络的跨域性能,如 [32] 中所示。
-
使用更多信息指导训练 --表 10.3 中的最先进比较简要地提到了结合来自多个来源的信息的工作:身份、属性(例如车辆的制造商和型号)以及时空信息(每个查询图像和图库图像的 GPS 位置)。理想情况下,包括更多信息有助于获得更高的准确性。然而,这需要为数据添加标注。在多属性设置中进行训练的合理方法是使用多任务学习(MTL)。通常,损失会变得冲突;这通过适当的任务权重(使用交叉验证)来解决。通过多目标优化解决这种冲突损失场景的多任务学习框架是由 Sener 等人 [32] 提出的。
在人脸、人员和车辆分类的上下文中,MTL 的一些流行作品由 Ranjan 等人 [34]、Ling 等人 [35] 和 Tang [23] 完成。
摘要
-
图像检索系统需要学习视觉嵌入(一个向量空间)。在这个嵌入空间中,任何一对图像都可以通过它们的几何距离进行比较。
-
要使用 CNN 学习嵌入,有三种流行的损失函数:交叉熵、三重损失和对比损失。
-
三重损失的三种简单训练方法在计算上难以承受。因此,我们使用基于批次的基于信息的数据挖掘:全部批次、硬批次、样本批次和加权批次。
参考文献
-
S.Z. Li 和 A.K. Jain. 2011. 《人脸识别手册》. Springer Science & Business Media.
www.springer.com/gp/book/9780857299314. -
V. Gupta 和 S. Mallick. 2019. “面向初学者的面部识别介绍。” Learn OpenCV. 2019 年 4 月 16 日。
www.learnopencv.com/face-recognition-an-introduction-for-beginners. -
Z. Liu, P. Luo, S. Qiu, X. Wang, 和 X. Tang. 2016. “Deepfashion: 利用丰富注释实现鲁棒的服装识别和检索。” IEEE 计算机视觉与模式识别会议 (CVPR)。
mmlab.ie.cuhk.edu.hk/projects/DeepFashion.html. -
T. Xiao, S. Li, B. Wang, L. Lin, 和 X. Wang. 2016. “人员搜索的联合检测和识别特征学习。”
arxiv.org/abs/1604.01850. -
Y. Zhai, X. Guo, Y. Lu, 和 H. Li. 2018. “为行人重识别的分类损失辩护。”
arxiv.org/abs/1809.05864. -
S. Chopra, R. Hadsell, and Y. LeCun. 2005. “Learning a Similarity Metric Discriminatively, with Application to Face Verification.” In 2005 IEEE Computer Society Conference on Computer Vision and Pattern Recognition (CVPR’05), 1: 539-46 vol. 1.
doi.org/10.1109/CVPR.2005.202. -
C-Y. Wu, R. Manmatha, A.J. Smola, and P. Krähenbühl. 2017. “Sampling Matters in Deep Embedding Learning.”
arxiv.org/abs/1706.07567. -
Q. Weinberger and L.K. Saul. 2009. “Distance Metric Learning for Large Margin Nearest Neighbor Classification.” The Journal of Machine Learning Research 10: 207-244.
papers.nips.cc/paper/2795-distance-metric-learning-for-large-margin-nearest-neighbor-classification.pdf. -
F. Schroff, D. Kalenichenko, and J. Philbin. 2015. “FaceNet: A Unified Embedding for Face Recognition and Clustering.” In 2015 IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 815-23.
ieeexplore.ieee.org/document/7298682. -
R. Kumar, E. Weill, F. Aghdasi, and P. Sriram. 2019. “Vehicle Re-Identification: An Efficient Baseline Using Triplet Embedding.”
arxiv.org/pdf/1901.01015.pdf. -
A. Hermans, L. Beyer, and B. Leibe. 2017. “In Defense of the Triplet Loss for Person Re-Identification.”
arxiv.org/abs/1703.07737. -
R. Kumar, E. Weill, F. Aghdasi, and P. Sriram. 2020. “A Strong and Efficient Baseline for Vehicle Re-Identification Using Deep Triplet Embedding.” Journal of Artificial Intelligence and Soft Computing Research 10 (1): 27-45.
content.sciendo.com/view/journals/jaiscr/10/1/article-p27.xml. -
E. Ristani and C. Tomasi. 2018. “Features for Multi-Target Multi-Camera Tracking and Re-Identification.”
arxiv.org/abs/1803.10859. -
X. Liu, W. Liu, T. Mei, and H. Ma. 2018. “PROVID: Progressive and Multimodal Vehicle Reidentification for Large-Scale Urban Surveillance.” IEEE Transactions on Multimedia 20 (3): 645-58.
doi.org/10.1109/TMM.2017.2751966. -
J. Deng, W. Dong, R. Socher, L. Li, Kai Li, and Li Fei-Fei. 2009. “ImageNet: A Large-Scale Hierarchical Image Database.” In 2009 IEEE Conference on Computer Vision and Pattern Recognition, 248-55.
ieeexplore.ieee.org/lpdocs/epic03/wrapper.htm?arnumber=5206848. -
A.G. Howard, M. Zhu, B. Chen, D. Kalenichenko, W. Wang, T. Weyand, M. Andreetto, and H. Adam. 2017. “MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications.”
arxiv.org/abs/1704.04861. -
D.P. Kingma 和 J. Ba. 2014. “Adam: 随机优化的方法。”
arxiv.org/abs/1412.6980. -
W. Kim, B. Goyal, K. Chawla, J. Lee, 和 K. Kwon. 2018. “基于注意力的深度度量学习集成。” 在 2018 IEEE 计算机视觉与模式识别会议 (CVPR), 760-777,
arxiv.org/abs/1804.00382. -
M. Opitz, G. Waltner, H. Possegger, 和 H. Bischof. 2017. “BIER--稳健提升独立嵌入。” 在 2017 IEEE 国际计算机视觉会议 (ICCV), 5199-5208.
ieeexplore.ieee.org/document/8237817. -
Y. Bai, Y. Lou, F. Gao, S. Wang, Y. Wu, 和 L. Duan. 2018. “针对车辆重识别的组敏感三元组嵌入。” IEEE 传输多媒体 20 (9): 2385-99.
ieeexplore.ieee.org/document/8265213. -
Y. Zhouy 和 L. Shao. 2018. “针对车辆重识别的视角感知多视图推理。” 在 2018 IEEE/CVF 计算机视觉与模式识别会议,6489-98.
ieeexplore.ieee.org/document/8578777. -
Y. Shen, T. Xiao, H. Li, S. Yi, 和 X. Wang. 2017. “使用视觉时空路径提议学习车辆重识别的深度神经网络。” 在 2017 IEEE 国际计算机视觉会议 (ICCV), 1918-27.
ieeexplore.ieee.org/document/8237472. -
Z. Tang, M. Naphade, S. Birchfield, J. Tremblay, W. Hodge, R. Kumar, S. Wang, 和 X. Yang. 2019. “PAMTRI: 基于姿态的多任务学习,用于使用高度随机化合成数据的车辆重识别。” 在 IEEE 国际计算机视觉会议论文集,211-20.
openaccess.thecvf.com/content_ICCV_2019/html/Tang_PAMTRI_Pose-Aware_Multi-Task_Learning_for_Vehicle_Re-Identification_Using_Highly_Randomized_ICCV_2019_paper.html. -
A. Kanacı, X. Zhu, 和 S. Gong. 2017. “通过细粒度跨层深度学习进行车辆重识别。” 在 BMVC AMMDS Workshop, 2:772-88.
arxiv.org/abs/1809.09409. -
P. Khorramshahi, A. Kumar, N. Peri, S.S. Rambhatla, J.-C. Chen, 和 R. Chellappa. 2019. “具有自适应注意力的双路径模型,用于车辆重识别。”
arxiv.org/abs/1905.03397. -
A. Kanacı, X. Zhu, 和 S. Gong. 2017. “通过细粒度跨层深度学习进行车辆重识别。” 在 BMVC AMMDS Workshop, 2:772-88.
www.eecs.qmul.ac.uk/~xiatian/papers. -
Z. Tang, M. Naphade, M.-Y. Liu, X. Yang, S. Birchfield, S. Wang, R. Kumar, D. Anastasiu, and J.-N. Hwang. 2019. “CityFlow:用于多目标多摄像头车辆跟踪和重识别的城市规模基准.” 在 2019 年 IEEE 计算机视觉和模式识别会议(CVPR)中.
arxiv.org/abs/1903.09254. -
Z. Zhong, L. Zheng, D. Cao, and S. Li. 2017. “使用 K-互反编码对人员重识别进行重新排序.” 在 2017 年 IEEE 计算机视觉和模式识别会议(CVPR)中, 3652-3661,
arxiv.org/abs/1701.08398. -
H. Luo, Y. Gu, X. Liao, S. Lai, and W. Jiang. 2019. “深度人员重识别的技巧集合和强大基线.” 在 2019 年 IEEE 计算机视觉和模式识别会议(CVPR)研讨会中.
arxiv.org/abs/1903.07071. -
H. Liu, J. Feng, M. Qi, J. Jiang, and S. Yan. 2016. “用于人员重识别的端到端比较注意力网络.” IEEE 图像处理杂志 26 (7): 3492-3506.
arxiv.org/abs/1606.04404. -
G. Chen, C. Lin, L. Ren, J. Lu, and J. Zhou. 2019. “用于人员重识别的自我批判注意力学习.” 在 IEEE 国际计算机视觉会议论文集中, 9637-46.
openaccess.thecvf.com/content_ICCV_2019/html/Chen_Self-Critical_Attention_Learning_for_Person_Re-Identification_ICCV_2019_paper.html. -
H. Liu, J. Cheng, S. Wang, and W. Wang. 2019. “注意力:跨域人员重识别的一个大惊喜.”
arxiv.org/abs/1905.12830. -
O. Sener and V. Koltun. 2018. “多任务学习作为多目标优化.” 在第 32 届国际神经网络信息处理系统会议论文集中, 525-36.
dl.acm.org/citation.cfm?id=3326943.3326992. -
R. Ranjan, S. Sankaranarayanan, C. D. Castillo, and R. Chellappa. 2017. “用于面部分析的全能卷积神经网络.” 在 2017 年第 12 届 IEEE 自动面部和手势识别会议(FG 2017)中, 17-24.
arxiv.org/abs/1611.00851. -
H. Ling, Z. Wang, P. Li, Y. Shi, J. Chen, and F. Zou. 2019. “通过多任务学习提高人员重识别.” Neurocomputing 347: 109-118.
doi.org/10.1016/j.neucom.2019.01.027. -
X. Liu, W. Liu, T. Mei, and H. Ma. 2016. “基于深度学习的城市监控车辆渐进式重识别方法.” 在计算机视觉 - ECCV 2016, 869-84.
doi.org/10.1007/978-3-319-46475-6_53. -
B. Amos, B. Ludwiczuk, M. Satyanarayanan, 等人. 2016. “Openface: 一个具有移动应用的通用人脸识别库.” 卡内基梅隆大学计算机科学学院 6: 2.
elijah.cs.cmu.edu/DOCS/CMU-CS-16-118.pdf.
-
在实践中,由于主机内存限制,这一步被分解为两个
for循环。 -
坚定地。例如,在 Tensorflow 中,请参阅
mng.bz/zjvQ.
附录 A. 设置环境
本书中的所有代码都是用 Python 3、Open CV、Keras 和 TensorFlow 编写的。在你的计算机上设置深度学习环境的过程相当复杂,包括以下步骤,本附录将详细说明这些步骤:
-
下载代码仓库。
-
安装 Anaconda。
-
设置你的深度学习环境:安装本书项目中需要的所有包(NumPy、OpenCV、Keras、TensorFlow 等)。
-
[可选] 设置 AWS EC2 环境。如果你想在 GPU 上训练网络,这一步是可选的。
A.1 下载代码仓库
本书展示的所有代码都可以从本书的网站 (www.manning.com/books/deep-learning-for-vision-systems) 和 GitHub (github.com/moelgendy/deep_learning_for_vision_systems) 下载,以 Git 仓库的形式。GitHub 仓库包含每个章节的目录。如果你不熟悉使用 Git 和 GitHub 进行版本控制,你可以查看入门文章 (help.github.com/categories/bootcamp) 和/或初学者资源 (help.github.com/articles/good-resources-for-learning-git-and-github) 以学习这些工具。
A.2 安装 Anaconda
Anaconda (anaconda.org) 是为数据科学和机器学习项目构建的包发行版。它包含 conda,一个包和环境管理器。你将使用 conda 为使用不同版本库的项目创建隔离的环境。你还将使用它来在你的环境中安装、卸载和更新包。
注意,Anaconda 是一个相当大的下载文件(约 600 MB),因为它包含了 Python 中最常用的机器学习包。如果你不需要所有这些包,或者需要节省带宽或存储空间,也可以选择 Miniconda,这是一个更小的发行版,仅包含 conda 和 Python。你仍然可以使用 conda 安装任何可用的包;只是它们不会随安装包一起提供。
按以下步骤在你的计算机上安装 Anaconda:
-
Anaconda 可用于 Windows、macOS 和 Linux。你可以在 www.anaconda.com/distribution 找到安装程序和安装说明。选择 Python 3 版本,因为自 2020 年 1 月起 Python 2 已被弃用。如果你有 64 位操作系统,请选择 64 位安装程序;否则,请选择 32 位安装程序。请下载适当的版本。
-
按照图 A.1 中显示的图形界面安装程序进行安装。
![Anaconda 安装器在 macOS 上]()
图 A.1 macOS 上的 Anaconda 安装器
-
安装完成后,你将自动进入默认的 conda 环境,其中已安装所有软件包。你可以通过在终端中输入
condalist来检查自己的安装情况,以查看你的 conda 环境。
A.3 设置你的深度学习环境
现在,你将创建一个新的环境并安装你将为项目使用的软件包。你将使用 conda 作为包管理器来安装你需要的库。你可能已经熟悉 pip;它是 Python 库的默认包管理器。conda 与 pip 类似,但可用的软件包主要集中在数据科学领域,而 pip 用于通用用途。
Conda 也是一个虚拟环境管理器。它与像 virtualenv (virtualenv.pypa.io/en/stable) 和 pyenv (github.com/pyenv/pyenv) 这样的其他流行环境管理器类似。然而,conda 并不像 pip 那样特定于 Python:它也可以安装非 Python 软件包。它是一个适用于任何软件栈的包管理器。也就是说,并非所有 Python 库都可通过 Anaconda 发行版和 conda 获取。你可以(并且将会)继续使用 pip 与 conda 一起安装软件包。
A.3.1 手动设置你的开发环境
按照以下步骤手动安装本书项目中需要的所有库。否则,跳到下一节以安装书中 GitHub 仓库中为你创建的环境。
-
在你的终端中,创建一个新的 conda 环境,使用 Python 3,并将其命名为
deep_learning_for_vision_systems:conda create -n deep_learning_for_vision_systems python=3注意,要删除 conda 环境,你使用
condaenvremove-n<env_name>。 -
激活你的环境。在安装你的软件包之前,你必须激活该环境。这样,所有软件包都只为此环境安装:
conda activate deep_learning_for_vision_systems注意,要停用环境,你使用
condadeactivate<env_name>。现在,你已经进入了你的新环境。要查看此环境中安装的默认软件包,请输入以下命令:
condalist。接下来,你将安装本书项目中使用的软件包。 -
安装 NumPy、pandas 和 Matplotlib。这些是非常常见的机器学习包,你几乎总是在你的项目中使用它们进行数学运算、数据处理和可视化任务:
conda install numpy pandas matplotlib注意,在整个安装过程中,你将收到提示以确认继续(Proceed ([y]/n)?)。输入 y 并按 Enter 键继续安装。
-
安装 Jupyter 笔记本。在这本书中,我们使用 Jupyter 笔记本来简化开发:
conda install jupyter notebook -
安装 OpenCV(最受欢迎的开源计算机视觉库):
conda install -c conda-forge opencv -
安装 Keras:
pip install keras -
安装 TensorFlow:
pip install tensorflow现在一切准备就绪,你的环境已准备好开始开发。如果你想查看你环境中安装的所有库,请输入以下命令:
conda list
这些软件包与其他环境是分开的。这样,你可以避免任何版本冲突问题。
A.3.2 使用本书仓库中的 conda 环境
-
从
github.com/moelgendy/deep_learning_for_vision_systems克隆本书的 GitHub 仓库。环境位于安装器/应用程序.yaml 文件中:cd installer -
创建 conda
deep_learning环境:conda env create -f my_environment.yaml -
激活 conda 环境:
-
启动你的 Jupyter 笔记本(确保你位于
deep_learning_for_vision_systems仓库的根目录):jupyter notebook
现在你已经准备好运行与本书相关的笔记本了。
A.3.3 保存和加载环境
如果你想与他人共享环境,以便他们可以使用正确的版本安装你代码中使用的所有包,那么保存环境是一个最佳实践。为此,你可以使用以下命令将包保存到 YAML 文件中:
conda env export > my_environment.yaml
这样,其他人可以使用以下命令在他们的机器上使用此 YAML 文件来复制你的环境:
conda env create -f my_environment.yaml
你还可以将环境中的包列表导出到一个.txt 文件中,然后将该文件包含在你的代码中。这允许其他人轻松地加载你代码的所有依赖项。Pip 具有与此类似的命令功能:
pip freeze > requirements.txt
你可以在安装目录中下载的代码中找到本书项目使用的环境详情。你可以使用它来在你的机器上复制我的环境。
A.4 设置你的 AWS EC2 环境
训练和评估深度神经网络是一项计算密集型任务,这取决于你的数据集大小和神经网络的大小。本书中的所有项目都是专门设计的,具有适当大小的问题和数据集,以便你可以在本地机器的 CPU 上训练网络。但其中一些项目可能需要长达 20 小时才能训练——或者更多,这取决于你的计算机规格和其他参数,如迭代次数、神经网络大小和其他因素。
一种更快的替代方案是在图形处理单元(GPU)上训练,这是一种支持更大并行性的处理器。你可以自己搭建深度学习设备,或者使用像 Amazon AWS EC2 这样的云服务。许多云服务提供商提供类似的功能,但 EC2 是一个合理的默认选择,大多数初学者都可以使用。在接下来的几节中,我们将介绍从无到有,在 Amazon 服务器上运行神经网络的步骤。
A.4.1 创建 AWS 账户
按照以下步骤操作:
-
访问aws.amazon.com,并点击创建 AWS 账户按钮。你还需要选择一个支持计划。你可以选择免费的 Basic Support Plan。你可能需要提供信用卡信息,但你现在不会收取任何费用。
-
启动 EC2 实例:
-
前往 EC2 管理控制台
console.aws.amazon.com/ec2/v2/home,并点击启动实例按钮。 -
点击 AWS Marketplace。
-
搜索“深度学习 AMI”,并选择适合您环境的 AMI。Amazon Machine Images (AMI) 包含了您在 GPU 上训练所需的所有环境文件和驱动程序。它包含 cuDNN 和许多其他用于本书项目中所需的软件包。对于特定项目所需的其他软件包,在相应的项目说明中详细说明。
-
选择实例类型:
-
筛选实例列表,仅显示 GPU 实例。
-
选择 p2.xlarge 实例类型。这个实例足够强大,可以用于我们的项目,而且并不昂贵。如果您有兴趣尝试更强大的实例,请随意选择。
-
点击“审查和启动”按钮。
-
-
编辑安全组。您将在本书中使用 Jupyter 笔记本,它们默认使用端口 8888。要访问此端口,您需要通过编辑安全组在 AWS 上打开它:
-
选择“创建新的安全组”。
-
将安全组名称设置为 Jupyter。
-
点击“添加规则”,并设置自定义 TCP 规则。
-
设置端口号为 8888。
-
将源设置为“任何地方”。
-
点击“审查和启动”。
-
-
点击“启动”按钮以启动您的 GPU 实例。您需要指定一个身份验证密钥对才能访问您的实例。因此,当您启动实例时,请确保选择“创建新的密钥对”并点击“下载密钥对”按钮。这将下载一个.pem 文件,您需要它才能访问您的实例。将.pem 文件移动到您计算机上安全且易于记住的位置;您将通过您选择的位置访问您的实例。在.pem 文件下载后,点击“启动实例”按钮。
-
警告:从现在开始,AWS 将向您收取运行此 EC2 实例的费用。您可以在 EC2 按需定价页面找到详细信息(aws.amazon.com/ec2/pricing/on-demand)。最重要的是,始终记得在不使用实例时停止实例。否则,它们可能会继续运行,您可能会收到一大笔账单!AWS 主要按运行实例收费,因此一旦您停止实例,大多数费用就会停止。然而,较小的存储费用将继续累积,直到您终止(删除)实例。
A.4.2 远程连接到您的实例
现在您已经创建了您的 EC2 实例,请转到您的 EC2 仪表板,选择实例,并启动它,如图 A.2 所示。请允许一分钟左右的时间让 EC2 实例启动。当实例状态检查显示“检查通过”时,您就会知道它已经准备好了。滚动到描述部分,并记下 EC2 仪表板上的 IPv4 公共 IP 地址(格式为 X.X.X.X);您将在下一步中需要它来远程访问您的实例。

图 A.2 如何远程连接到您的实例
在您的终端上,按照以下步骤连接到您的 EC2 服务器:
-
导航到上一节中存储您的.pem 文件的位置。
-
输入以下内容:
ssh -i YourKeyName.pem user@X.X.X.Xuser可以是ubuntu@或ec2-user@。X.X.X.X是您从 EC2 实例描述中保存的 IPv4 公共 IP。YourKeyName.pem是您的.pem文件名。
提示:如果您看到有关您的 .pem 文件的“权限错误”或“权限被拒绝”错误消息,请尝试执行 chmod 400 path/to/YourKeyName.pem,然后再次运行 ssh 命令。
A.4.3 运行您的 Jupyter 笔记本
最后一步是在 EC2 服务器上运行您的 Jupyter 笔记本。在您从终端远程访问实例后,按照以下步骤操作:
-
在您的终端中输入以下命令:
jupyter notebook --ip=0.0.0.0 --no-browser当您按下 Enter 键时,您将获得一个访问令牌,如图 A.3 所示。复制此令牌值,因为您将在下一步中使用它。
![]()
图 A.3 将令牌复制以运行笔记本。
-
在您的浏览器中,访问此 URL:http://<IPv4 公共 IP>:8888。请注意,IPv4 公共 IP 是您从 EC2 实例描述中保存的那个。例如,如果公共 IP 是 25.153.17.47,那么 URL 将是 http://25.153.17.47:8888。
-
将您在步骤 1 中复制的令牌密钥输入到令牌字段中,然后点击登录(图 A.4)。
![]()
图 A.4 登录
-
安装您项目所需的库,类似于您在 A.3.1 节中做的操作。但这次,使用
pipinstall而不是condainstall。例如,要安装 Keras,您需要输入pipinstallkeras。
那就这样了。现在您已经准备好开始编码了!





















和
图形在图 4.7 中。如你所见,一条直线并不能准确分割数据。






❶
❷
























浙公网安备 33010602011771号