面向云边端的深度学习实践指南-全-
面向云边端的深度学习实践指南(全)
原文:Practical Deep Learning for Cloud, Mobile, and Edge
译者:飞龙
前言
我们正在经历人工智能的复兴,每个人和他们的邻居都希望成为这一运动的一部分。这很可能是您正在浏览这本书的原因。关于深度学习的书籍有很多。因此,您可能会问我们,非常合理地问,为什么这本书存在?我们马上就会解释。
自 2013 年以来,我们在微软、英伟达、亚马逊和 Square 等公司构建产品的深度学习之旅中,我们目睹了这一领域的巨大变化。不断发展的研究是一个事实,而成熟工具的缺乏是生活的现实。
在与社区的交流和学习过程中,我们注意到缺乏明确的指导,说明如何将研究转化为面向日常用户的最终产品。毕竟,最终用户可能在网络浏览器、智能手机或边缘设备的前面。这通常涉及无数小时的黑客和实验,通过博客、GitHub 问题线程和 Stack Overflow 答案进行广泛搜索,给包的作者发送电子邮件以获取深奥的知识,以及偶尔的“恍然大悟”时刻。即使市场上的书籍往往更注重理论或如何使用特定工具。我们能希望从现有的书籍中学到的最好的东西就是构建一个玩具示例。
为了填补理论和实践之间的鸿沟,我们最初开始进行关于将人工智能从研究转化为最终用户的演讲,特别关注实际应用。这些演讲旨在展示激励性的例子,以及根据技能水平(从业余爱好者到 Google 规模的工程师)和在生产中部署深度学习所涉及的努力不同的复杂性水平。我们发现,无论是初学者还是专家,都会从这些演讲中获益。
随着时间的推移,这个领域幸运地变得更加容易接近初学者,并且有更多的工具可用。像 Fast.ai 和 DeepLearning.ai 这样的优秀在线材料使得理解如何训练 AI 模型变得比以往任何时候都更容易。书籍也占据了使用深度学习框架(如 TensorFlow 和 PyTorch)教授基础知识的市场。但即使有了这一切,理论和实践之间的巨大鸿沟仍然大多未被解决。我们希望弥合这一鸿沟。因此,您现在正在阅读的就是这本书。
本书使用平易近人的语言以及计算机视觉中的即时趣味项目,从简单的分类器开始,假设没有机器学习和人工智能的知识,逐渐增加复杂性,提高准确性和速度,扩展到数百万用户,部署在各种硬件和软件上,并最终通过强化学习构建微型自动驾驶汽车。
几乎每一章都以一个激励性的例子开始,提前确定在构建解决方案的过程中可能会问到的问题,并讨论解决问题的多种方法,每种方法都涉及不同的复杂性水平和努力。如果您正在寻找一个快速解决方案,您可能只需要阅读一章的几页就可以了。想要更深入了解主题的人应该阅读整章。当然,每个人都应该浏览这些章节中包含的案例研究,有两个原因——它们很有趣,展示了行业中的人们如何使用章节中讨论的概念来构建真实产品。
我们还讨论了深度学习从业者和行业专业人士在使用云端、浏览器、移动设备和边缘设备构建真实应用程序时面临的许多实际问题。我们在这本书中编制了许多实用的“技巧和窍门”,以及生活经验,鼓励我们的读者构建可以让某人的一天变得更好一点的应用程序。
给后端/前端/移动软件开发人员
您很可能已经是一位熟练的程序员。即使 Python 对您来说是一种陌生的语言,我们相信您将能够轻松掌握并立即开始。最重要的是,我们不指望您具有机器学习和人工智能方面的任何背景;这就是我们在这里的原因!我们相信您将从本书关注以下领域中获益:
-
如何构建面向用户的 AI 产品
-
如何快速训练模型
-
如何最小化原型设计中所需的代码和工作量
-
如何使模型更具性能和能源效率
-
如何操作和扩展,并估算涉及的成本
-
发现 AI 如何在行业中应用,包括 40 多个案例研究和实际示例
-
深度学习的广泛知识发展
-
开发一套通用的技能,可应用于新的框架(例如 PyTorch)、领域(例如医疗保健、机器人学)、输入模态(例如视频、音频、文本)和任务(例如图像分割、一次性学习)
对于数据科学家
您可能已经精通机器学习,可能知道如何训练深度学习模型。好消息!您可以进一步丰富您的技能,并加深您在该领域的知识,以便构建真实产品。本书将通过以下内容帮助您的日常工作和更多内容:
-
加快您的训练速度,包括在多节点集群上
-
建立对开发和调试模型的直觉,包括超参数调整,从而显著提高模型准确性
-
了解您的模型如何工作,发现数据中的偏见,并自动确定最佳的超参数以及模型架构,使用 AutoML
-
学习其他数据科学家使用的技巧,包括快速收集数据、以有组织的方式跟踪您的实验、与世界分享您的模型,并了解您任务的最佳可用模型
-
使用工具将您最佳的模型部署和扩展到真实用户,甚至自动化(无需涉及 DevOps 团队)
对于学生
现在是考虑从事人工智能职业的绝佳时机——它正在成为继互联网和智能手机之后的下一个技术革命。已经取得了很多进展,还有很多待发现的地方。我们希望本书可以作为您迈向人工智能职业的第一步,并且更好地发展更深入的理论知识。最好的部分是您不必花费大量资金购买昂贵的硬件。事实上,您可以完全免费在网络浏览器上使用强大的硬件进行训练(感谢 Google Colab!)。通过本书,我们希望您能够:
-
通过开发一系列有趣的项目,立志从事人工智能职业
-
从行业实践中学习,以帮助准备实习和工作机会
-
通过构建像自动驾驶汽车这样有趣的应用来释放您的创造力
-
通过利用您的创造力解决人类面临的最紧迫问题,成为 AI 的倡导者
对于教师
我们相信本书可以很好地补充您的课程内容,提供有趣的、实际的项目。我们详细介绍了深度学习流程的每一步,以及如何有效和高效地执行每一步的技术。我们在书中提出的每个项目都可以成为学期中课堂上的优秀合作或个人作品。最终,我们将在http://PracticalDeepLearning.ai上发布 PowerPoint 演示幻灯片,可用于课程辅助教学。
对于机器人爱好者
机器人技术令人兴奋。如果您是机器人爱好者,我们无需说服您向机器人添加智能是正确的途径。像树莓派、NVIDIA Jetson Nano、Google Coral、Intel Movidius、PYNQ-Z2 等功能日益强大的硬件平台正在推动机器人领域的创新。随着我们向工业 4.0 发展,其中一些平台将变得越来越相关和普遍。通过本书,您将:
-
学习如何构建和训练人工智能,然后将其应用到边缘
-
基准测试和比较边缘设备的性能、大小、功耗、电池和成本
-
了解如何为特定场景选择最佳的人工智能算法和设备
-
了解其他制作者如何构建创意机器人和机器
-
学习如何在该领域取得进一步进展并展示您的工作
每章内容预期
第一章,探索人工智能的领域
我们从 1950 年代到今天对这个不断发展的领域进行了一次探索,分析了构成完美深度学习配方的要素,熟悉常见的人工智能术语和数据集,并窥探了负责任人工智能的世界。
第二章,图片中有什么:使用 Keras 进行图像分类
我们只需用五行 Keras 代码就深入探讨了图像分类的世界。然后,我们通过在视频上叠加热图来了解神经网络在进行预测时关注的内容。奖励:我们听到了 Keras 的创始人弗朗索瓦·肖莱的激励个人旅程,展示了单个个体可以产生的影响。
第三章,猫与狗:使用 Keras 进行 30 行迁移学习
我们使用迁移学习在新的自定义分类任务上重复使用先前训练过的网络,以在几分钟内获得接近最先进的准确性。然后,我们对结果进行分析,以了解其分类情况。在此过程中,我们构建了一个通用的机器学习流水线,本书中多次重复使用。奖励:我们听到了 fast.ai 的联合创始人 Jeremy Howard 的声音,他讲述了成千上万的学生如何利用迁移学习来启动他们的人工智能之旅。
第四章,构建反向图像搜索引擎:理解嵌入
像谷歌反向图像搜索一样,我们探索如何使用嵌入——图像的上下文表示来在不到十行的代码中找到相似的图像。然后,当我们探索不同的策略和算法以加速规模化时,乐趣开始了,从数千到数百万张图像,并使它们在微秒内可搜索。
第五章,从新手到大师预测者:最大化卷积神经网络准确性
我们探索策略,以最大化我们的分类器可以实现的准确性,借助一系列工具,包括 TensorBoard、What-If Tool、tf-explain、TensorFlow Datasets、AutoKeras 和 AutoAugment。在此过程中,我们进行实验,以发展对哪些参数可能适用或不适用于您的人工智能任务的直觉。
第六章,最大化 TensorFlow 的速度和性能:一个方便的清单
我们通过一个包含 30 个技巧的清单,将训练和推理的速度提升到极致,以减少尽可能多的低效,并最大化当前硬件的价值。
第七章,实用工具、技巧和窍门
我们在各种主题和工具中拓展我们的实用技能,从安装、数据收集、实验管理、可视化,一直到探索建立深度学习理论基础的更多途径。
第八章,云计算机视觉 API:15 分钟上手
工作要聪明,不要辛苦。我们在不到 15 分钟内利用谷歌、微软、亚马逊、IBM 和 Clarifai 的云 AI 平台的强大功能。对于现有 API 无法解决的任务,我们使用定制分类服务来训练分类器,无需编码。然后我们将它们放在一个开放的基准测试中进行比较——你可能会惊讶于谁赢了。
第九章,使用 TensorFlow Serving 和 KubeFlow 在云端进行可扩展推理服务
我们将我们定制训练的模型部署到云端/本地,以便从数百万到数百万的请求中进行可扩展服务。我们探索 Flask、Google Cloud ML Engine、TensorFlow Serving 和 KubeFlow,展示了工作量、场景和成本效益分析。
第十章,在浏览器中使用 TensorFlow.js 和 ml5.js 的 AI
每个使用计算机或智能手机的个人都统一拥有一个软件程序——他们的浏览器。通过基于浏览器的深度学习库,包括 TensorFlow.js 和 ml5.js,可以接触到所有这些用户。客座作者 Zaid Alyafeai 向我们介绍了在浏览器中运行的技术和任务,如身体姿势估计、生成对抗网络(GANs)、图像到图像的转换与 Pix2Pix 等。额外福利:听听 TensorFlow.js 和 ml5.js 的关键贡献者关于这些项目的孵化过程。
第十一章,iOS 上的实时物体分类:使用 Core ML
我们探索了移动端深度学习的领域,重点关注苹果生态系统中的 Core ML。我们在不同的 iPhone 上对模型进行基准测试,研究减小应用程序大小和能源影响的策略,并探讨动态模型部署、设备上的训练以及专业应用程序的构建方式。
第十二章,iOS 上的 Not Hotdog:使用 Core ML 和 Create ML
硅谷的 Not Hotdog 应用(来自 HBO)被认为是移动 AI 的“Hello World”,因此我们通过三种不同的方式来构建实时版本,以示敬意。
第十三章,食物识别:使用 TensorFlow Lite 和 ML Kit 开发 Android 应用
我们通过 TensorFlow Lite 将 AI 带到 Android。然后我们使用 ML Kit(建立在 TensorFlow Lite 之上)和 Fritz 来探索构建自我改进 AI 应用的端到端开发生命周期。在这个过程中,我们关注模型版本控制、A/B 测试、成功度量、动态更新、模型优化等主题。额外福利:我们可以听到 Pete Warden(移动和嵌入式 TensorFlow 技术负责人)在将 AI 带到边缘设备方面的丰富经验。
第十四章,使用 TensorFlow 目标检测 API 构建完美的猫定位器应用
我们探索了四种不同的方法来定位图像中物体的位置。我们回顾了多年来物体检测的发展,并分析了速度和准确性之间的权衡。这为案例研究如人群计数、人脸检测和自动驾驶奠定了基础。
第十五章,成为制造者:探索边缘嵌入式 AI
特邀作者 Sam Sterckval 将深度学习带到低功耗设备,展示了一系列具有不同处理能力和成本的 AI 能力边缘设备,包括树莓派,NVIDIA Jetson Nano,Google Coral,Intel Movidius 和 PYNQ-Z2 FPGA,为机器人和创客项目打开了大门。奖励:听取 NVIDIA Jetson Nano 团队的意见,了解人们如何从他们的开源食谱书快速构建创意机器人。
第十六章,使用 Keras 进行端到端深度学习模拟自动驾驶汽车
通过使用微软 AirSim 的逼真模拟环境,特邀作者 Aditya Sharma 和 Mitchell Spryn 指导我们在训练虚拟汽车时,首先在环境中驾驶汽车,然后教授 AI 模型复制其行为。在这个过程中,本章涵盖了许多在自动驾驶汽车行业中适用的概念。
第十七章,在不到一小时内构建自动驾驶汽车:使用 AWS DeepRacer 进行强化学习
从虚拟世界到物理世界,特邀作者 Sunil Mallya 展示了如何在不到一小时内组装、训练和比赛 AWS DeepRacer,一辆微型汽车。通过强化学习的帮助,汽车学会了自动驾驶,惩罚自己的错误并最大化成功。我们学习如何将这些知识应用到从 AI 驾驶的奥林匹克到 RoboRace(使用全尺寸自动驾驶汽车)的比赛中。奖励:听取来自 Anima Anandkumar(NVIDIA)和 Chris Anderson(DIY Robocars 创始人)的意见,了解自动驾驶汽车行业的发展方向。
本书中使用的约定
本书中使用以下排版约定:
斜体
表示新术语,URL,电子邮件地址,文件名和文件扩展名。
等宽
用于程序清单,以及在段落中引用程序元素,如变量或函数名称,数据库,数据类型,环境变量,语句和关键字。
等宽粗体
显示用户应按照字面意义输入的命令或其他文本。
等宽斜体
显示应替换为用户提供的值或由上下文确定的值的文本。
提示
这个元素表示提示或建议。
注意
这个元素表示一般说明。
警告
这个元素表示警告或注意。
使用代码示例
补充材料(代码示例,练习等)可在http://PracticalDeepLearning.ai下载。如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至PracticalDLBook@gmail.com。
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分,否则无需联系我们以获得许可。例如,编写一个使用本书中几个代码块的程序不需要许可。出售或分发 O’Reilly 图书中的示例需要许可。通过引用本书并引用示例代码回答问题不需要许可。将本书中大量示例代码合并到产品文档中需要许可。
我们感谢,但通常不需要署名。署名通常包括标题,作者,出版商和 ISBN。例如:“Practical Deep Learning for Cloud, Mobile, and Edge by Anirudh Koul, Siddha Ganju, and Meher Kasam (O’Reilly). Copyright 2020 Anirudh Koul, Siddha Ganju, Meher Kasam, 978-1-492-03486-5.”
如果您觉得您使用的代码示例超出了合理使用范围或上述给出的许可,请随时通过permissions@oreilly.com与我们联系。
致谢
团体致谢
我们要感谢以下人员在我们写作这本书的旅程中给予的巨大帮助。没有他们,这本书将不可能完成。
这本书得以问世,要归功于我们的开发编辑 Nicole Taché的努力。她在我们的旅程中一直支持我们,并在每个步骤中提供重要的指导。她帮助我们优先考虑了正确的材料(信不信由你,这本书本来会更大!),并确保我们在正确的轨道上。她是我们写的每一份草稿的第一个读者,因此我们的首要目标是确保她能够理解内容,尽管她对 AI 还很陌生。我们对她的支持深表感激。
我们还要感谢 O'Reilly 团队的其他成员,包括我们的制作编辑 Christopher Faucher,他在紧张的时间表下辛勤工作,确保这本书按时印刷出版。我们也感谢我们的副本编辑 Bob Russell,他以其迅速的编辑和对细节的关注给我们留下了深刻印象。他让我们意识到在学校要注意英语语法的重要性(尽管有些晚了几年,我们很抱歉)。我们还要感谢 Rachel Roumeliotis(内容战略副总裁)和 Olivia MacDonald(开发编辑总监)对这个项目的信任和持续支持。
我们要特别感谢那些为我们的读者带来他们的技术专长,与我们分享他们对这一领域的热情的客座作者。来自微软的阿迪蒂亚·夏尔马和米切尔·斯普林向我们展示了,我们对玩视频赛车游戏的热爱可以被用来训练自动驾驶汽车,在模拟环境中驾驶汽车(使用 AirSim)。来自亚马逊的苏尼尔·马利亚通过演示表明,只需一个小时就可以组装并让一个微型自动驾驶汽车(AWS DeepRacer)在赛道上使用强化学习找到自己的路。来自 Edgise 的山姆·斯特克瓦尔总结了市场上可用的各种嵌入式人工智能硬件,这样我们就可以在下一个机器人项目上占得先机。最后,来自法赫德国王大学的扎伊德·阿利亚菲向我们展示了,浏览器同样能够运行严肃的交互式人工智能模型(借助 TensorFlow.js 和 ml5js)。
这本书之所以能够达到现在的形式,要归功于我们出色的技术审阅人员及时的反馈,他们不知疲倦地在我们的草稿上工作,指出他们发现的任何技术错误,并给出了更好传达我们想法的建议。由于他们的反馈(以及 TensorFlow 不断变化的 API),我们最终对原始预发布的大部分内容进行了重写。我们感谢玛格丽特·梅纳德-雷德(机器学习谷歌开发专家,您可能在阅读 TensorFlow 文档时读过她的作品),帕科·内森(Derwin Inc. 35 年行业资深人士,向 Anirudh 介绍了公开演讲的世界),安迪·佩特雷拉(Kensu 首席执行官和创始人,SparkNotebook 的创造者,他的技术见解符合他的声誉),尼基塔·库尔(Adobe 高级数据科学家,每次迭代后阅读并提出改进建议,有效地阅读了几千页内容,从而使内容更易接近)对每一章的详细审阅。此外,我们还得到了许多在特定主题方面具有专业知识的审阅人员的帮助,无论是浏览器中的人工智能,移动开发还是自动驾驶汽车。按章节排序的审阅人员名单如下:
-
第一章:达林尼·钱德拉塞卡兰,谢林·托马斯
-
第二章:阿努杰·夏尔马,查尔斯·科兹罗克,马诺杰·帕里哈尔,潘克什·巴莫特拉,普拉纳夫·坎特
-
第三章:阿努杰·夏尔马,查尔斯·科兹罗克,马诺杰·帕里哈尔,潘克什·巴莫特拉,普拉纳夫·坎特
-
第四章:阿努杰·夏尔马,马诺杰·帕里哈尔,潘克什·巴莫特拉,普拉纳夫·坎特
-
第六章:加布里埃尔·伊巴贡,吉里·西姆萨,马克斯·卡茨,潘克什·巴莫特拉
-
第七章:潘克什·巴莫特拉
-
第八章:迪佩什·阿加瓜尔
-
第九章:潘克什·巴莫特拉
-
第十章:布雷特·伯利,洛朗·德努,曼拉吉·辛格
-
第十一章:大卫·阿普加,詹姆斯·韦伯
-
第十二章:大卫·阿普加
-
第十三章:杰西·威尔逊,萨尔曼·加迪特
-
第十四章:阿克希特·阿罗拉,普拉纳夫·坎特,罗希特·塔内贾,罗纳伊·阿克
-
第十五章:格尔特鲁伊·范洛梅尔,乔克·德库伯,乔利恩·德科克,玛丽安娜·范洛梅尔,山姆·亨德里克斯
-
第十六章:达里奥·萨利斯奇克,库尔特·尼布尔,马修·陈,普拉维恩·帕拉尼萨米
-
第十七章:基尔特什·加格,拉里·皮泽特,皮埃尔·迪马斯,里卡多·苏埃拉斯,塞戈莱恩·德塞尔丁-潘哈德,斯里·埃拉普罗卢,新井辰也
我们在整本书中引用了一些创作者的摘录,他们让我们一窥他们的世界,以及他们建立最为著名的项目的如何以及为什么。我们感谢弗朗索瓦·肖莱,杰里米·霍华德,皮特·沃登,阿尼玛·阿南德库马尔,克里斯·安德森,蔡善庆,丹尼尔·斯米尔科夫,克里斯托瓦尔·瓦伦兹埃拉,丹尼尔·希夫曼,哈特·伍勒里,丹·阿布迪诺尔,千德德·雅斗,约翰·威尔士和丹尼·阿特斯莫。
个人致谢
我要感谢我的家人 Arbind、Saroj 和 Nikhita,他们给予我支持、资源、时间和自由去追求我的激情。感谢微软、Aira 和雅虎的所有黑客和研究人员,他们与我一起将想法变成原型产品,我们的旅程中不是成功,而是教会我们很多的挫折。我们的尝试和困难为这本书提供了丰富的素材,超出了我们最初的估计,多出了 250 页!感谢卡内基梅隆大学、达豪斯大学和塔帕大学的学术家庭,你们教会了我不仅仅是学术(不像我的 GPA 可能暗示的那样)。还要感谢盲人和低视力社区,你们每天都激励我在人工智能领域工作,向我们展示了只要拥有正确的工具,人们是无限的。
Anirudh
我的祖父是一位作家,他曾告诉我:“写一本书比我想象的更难,也比我能想象的更有回报。”我永远感激我的祖父母、家人、妈妈、爸爸和 Shriya,他们倡导追求知识,帮助我成为今天的自己。感谢卡内基梅隆大学、欧洲核子研究中心、NASA FDL、Deep Vision、NITH 和 NVIDIA 的合作伙伴和导师们,他们在我整个旅程中与我同行,我感激他们教导我,帮助我培养了科学素养。感谢我的朋友们,希望他们还记得我,因为我最近一直很低调,非常感谢他们的耐心。希望能再见到你们。感谢那些无私地审阅书籍章节并充当听众的朋友们,非常感谢——没有你们,这本书就不会成形。
Siddha
我感激我的父母 Rajagopal 和 Lakshmi,他们从一开始就给予我无尽的爱和支持,以及提供我良好生活和教育的坚定意愿。感谢 UF 和 VNIT 的教授们,他们教导我,让我庆幸自己主修计算机科学。感谢我的极具支持性的伴侣 Julia Tanner,近两年来,她不得不忍受与我的合著者无休止的 Skype 通话,以及我那些糟糕的笑话(其中一些不幸地出现在这本书中)的夜晚和周末。我还要感谢我的出色经理 Joel Kustka,在我写作这本书的过程中支持我。向那些在我不能像他们希望的那样经常和他们一起玩的朋友们致以问候。
Meher
最后,感谢 Grammarly 的制作者,他们让那些英语成绩平平的人成为了出版作者!
第一章:探索人工智能的领域
以下是 Dr. May Carson(图 1-1)关于人工智能(AI)在 21 世纪人类生活中角色变化的重要论文中的话:
人工智能经常被称为 21 世纪的电力。今天,人工智能程序将有能力推动各种行业(包括医疗),设计医疗设备并构建新型产品和服务,包括机器人和汽车。随着 AI 的发展,组织已经在努力确保这些人工智能程序能够胜任工作,并且重要的是,避免错误或危险事故。组织需要 AI,但他们也意识到并非所有可以用 AI 做的事情都是一个好主意。
我们对使用这些技术和政策操作人工智能所需的资金进行了广泛研究。主要结论是,每年每人在 AI 项目上花费的资金与用于研究、构建和生产这些项目的资金大致相等。这似乎是显而易见的,但并非完全如此。
首先,AI 系统需要支持和维护来帮助它们的功能。为了真正可靠,他们需要人们具备运行它们和帮助他们执行一些任务的技能。AI 组织提供工作人员来执行这些服务所需的复杂任务是至关重要的。了解从事这些工作的人员也很重要,尤其是当 AI 比人类更复杂时。例如,人们通常会从事需要高级知识的工作,但不一定擅长处理需要构建和维护的系统。

图 1-1. Dr. May Carson
致歉
现在我们必须坦白承认,本章直到现在为止的所有内容都是完全虚假的。真的是一切!所有的文本(除了第一个斜体句子,这是我们写的种子)都是使用 GPT-2 模型(由 Adam King 构建)在网站TalkToTransformer.com上生成的。作者的名字是使用网站Onitools.moe上的“Nado Name Generator”生成的。至少作者的照片必须是真实的,对吧?不,这张照片是从网站ThisPersonDoesNotExist.com生成的,该网站利用生成对抗网络(GANs)的魔法每次重新加载页面时向我们展示不存在的人的新图片。
尽管我们对以不诚实的方式开始整本书感到矛盾,但我们认为展示 AI 的最新技术是重要的,当您,我们的读者,最不期望时。看到 AI 已经能够创造出比一些世界领导人更聪明和雄辩的句子,这实在是令人难以置信、惊人和可怕的。它能够凭空创造出句子,这让我们不禁要说,这是大联盟。
话虽如此,AI 目前还无法从我们这里获得的一件事是有趣的能力。我们希望这本书中前三段虚假的段落将是整本书中最枯燥的部分。毕竟,我们不想被称为“比机器更无聊的作者”。
真正的介绍
还记得你看过的一个魔术表演,其中一个把戏让你眼花缭乱,让你想,“他们到底是怎么做到的?!”你是否曾经对一则 AI 应用程序登上新闻感到好奇?在这本书中,我们希望为您提供知识和工具,不仅可以解构,还可以构建类似的应用程序。
通过易于理解的逐步解释,我们剖析了使用人工智能的真实应用程序,并展示了您如何在各种平台上创建这些应用程序——从云端到浏览器,再到智能手机,再到边缘人工智能设备,最终着眼于当前人工智能领域的最终挑战:自动驾驶汽车。
在大多数章节中,我们从一个激励性问题开始,然后一步一步地构建一个端到端的解决方案。在本书的前部分,我们培养了构建人工智能大脑所需的技能。但这只是一半的战斗。构建人工智能的真正价值在于创建可用的应用程序。我们不是在谈论玩具原型。我们希望您构建的软件可以在现实世界中被真实的人们使用,以改善他们的生活。因此,书名中有“实用”一词。为此,我们讨论了我们可以选择的各种选项,并根据性能、能耗、可扩展性、可靠性和隐私权的权衡选择适当的选项。
在这第一章中,我们退后一步,欣赏人工智能历史上的这一时刻。我们探讨人工智能的含义,特别是在深度学习的背景下以及导致深度学习成为 21 世纪初最具突破性技术进步领域之一的事件序列。我们还研究了构成完整深度学习解决方案的核心组件,为我们做好准备,以便在随后的章节中实际动手。
因此,我们的旅程从这里开始,从一个非常基本的问题开始。
什么是人工智能?
在本书中,我们经常使用“人工智能”、“机器学习”和“深度学习”这些术语,有时可以互换使用。但从严格的技术角度来看,它们有不同的含义。以下是每个术语的概要(也可参见图 1-2)。
人工智能
这使得机器具备模仿人类行为的能力。IBM 的深蓝是人工智能的一个著名例子。
机器学习
这是人工智能的一个分支,其中机器使用统计技术从以前的信息和经验中学习。目标是让机器根据过去的学习观察来未来采取行动。如果您看过 IBM 的沃森在危险边缘上与肯·詹宁斯和布拉德·拉特对决,您就看到了机器学习的实际应用。更具相关性的是,下次垃圾邮件没有进入您的收件箱时,您可以感谢机器学习。
深度学习
这是机器学习的一个子领域,其中使用深度、多层神经网络进行预测,特别擅长计算机视觉、语音识别、自然语言理解等领域。

图 1-2。人工智能、机器学习和深度学习之间的关系
在本书中,我们主要关注深度学习。
激励性例子
让我们直奔主题。是什么促使我们写这本书?为什么您要花自己辛苦挣来的钱购买这本书?我们的动机很简单:让更多的人参与到人工智能的世界中。您正在阅读这本书意味着我们的工作已经完成了一半。
然而,为了真正激起您的兴趣,让我们看一些出色的例子,展示人工智能已经能够做到什么:
-
“DeepMind 的 AI 代理在《星际争霸 II》中征服人类专家”:The Verge,2019
-
“由 AI 生成的艺术品在佳士得拍卖会上以近 50 万美元售出”:AdWeek,2018
-
“AI 在检测肺癌方面击败放射科医生”:美国管理护理杂志,2019
-
“波士顿动力的 Atlas 机器人可以做跑酷”:ExtremeTech,2018
-
“Facebook,卡内基梅隆大学首次打败 6 名扑克专家的 AI”:ai.facebook.com,2019
-
“盲人现在可以通过微软的 Seeing AI 触摸探索照片”:TechCrunch,2019
-
“IBM 的沃森超级计算机在最后的《危险边缘》比赛中击败人类”:VentureBeat,2011
-
“谷歌的 ML-Jam 挑战音乐家即兴演奏并与人工智能合作”:VentureBeat,2019
-
“无需人类知识即可掌握围棋游戏”:Nature,2017
-
“中国人工智能在诊断脑瘤方面击败医生”:Popular Mechanics,2018
-
“使用人工智能发现了两颗新行星”:Phys.org,2019
-
“英伟达最新的人工智能软件将粗糙的涂鸦转化为逼真的风景”:The Verge,2019
这些人工智能的应用是我们的北极星。这些成就的水平相当于获得金牌的奥运表现。然而,在现实世界中解决一系列问题的应用相当于完成一场 5K 比赛。开发这些应用并不需要多年的训练,但当开发者跨过终点线时,会获得巨大的满足感。我们在这里指导你完成这场 5K 比赛。
在本书中,我们有意地优先考虑广度。人工智能领域的变化如此之快,以至于我们只能希望为您提供正确的思维方式和一系列工具。除了解决个别问题外,我们还将探讨不同的、看似无关的问题之间的基本重叠,这些重叠可以为我们所用。例如,声音识别使用卷积神经网络(CNNs),这也是现代计算机视觉的基础。我们涉及多个领域的实际方面,因此您将能够迅速从 0 到 80,解决现实世界中的问题。如果我们引起了足够的兴趣,让您决定从 80 到 95,我们将认为我们的目标已经实现。正如常用的短语所说,我们希望“民主化人工智能”。
重要的是要注意,人工智能领域的许多进展都是在过去几年中取得的——这一点很难言之过早。为了说明我们已经取得了多大的进步,举个例子:五年前,你需要一个博士学位才能进入这个行业。五年后,你甚至不需要博士学位就能写一本关于这个主题的整本书。(真的,查看我们的个人资料吧!)
尽管现代深度学习的应用似乎非常惊人,但它们并不是靠自己就能达到这一点的。它们站在许多行业巨头的肩膀上,这些人已经在推动行业的极限数十年了。事实上,我们不能完全欣赏这个时代的重要性,而不看整个历史。
人工智能简史
让我们回到过去一点:我们整个宇宙都处于一个炽热而密集的状态。然后大约在 140 亿年前开始膨胀,等等...好吧,我们不必回溯那么久(但现在你一整天都会被这首歌困扰,对吧?)。其实,70 年前才种下了人工智能的第一颗种子。艾伦·图灵在他 1950 年的论文《计算机器械与智能》中首次提出了“机器能思考吗?”这实际上涉及到了更大的哲学辩论,即意识和成为人类的含义。拥有创作协奏曲的能力并知道你已经创作了它,这是什么意思?图灵发现这种框架相当狭隘,而提出了一个测试:如果一个人无法区分机器和另一个人,那真的重要吗?一个能够模仿人类的人工智能本质上就是人类。
令人兴奋的开端
“人工智能”这个术语是由约翰·麦卡锡在 1956 年的达特茅斯暑期研究项目中创造的。当时实际的计算机甚至并不是真正存在的东西,所以令人惊讶的是,他们能够讨论诸如语言模拟、自我改进的学习机器、对感官数据的抽象等未来领域。当然,其中很多是理论性的。这是人工智能第一次成为一个研究领域而不是一个单一项目。
1957 年,弗兰克·罗森布拉特在他的论文“感知器:一个感知和识别的自动机”中奠定了深度神经网络的基础。他假设应该可以构建一个电子或电机系统,学会识别光学、电气或音调信息的模式相似性。这个系统将类似于人类大脑。他提出使用统计模型进行预测,而不是使用基于规则的模型(当时算法的标准)。
注
在整本书中,我们反复提到神经网络这个词组。什么是神经网络?它是人类大脑的简化模型。就像大脑一样,它有神经元,当遇到熟悉的东西时会被激活。不同的神经元通过连接(对应我们大脑中的突触)相连,帮助信息从一个神经元流向另一个神经元。
在图 1-3 中,我们可以看到最简单的神经网络的一个示例:感知器。从数学上讲,感知器可以表示如下:
输出 = f(x[1], x[2], x[3]) = x[1] w[1] + x[2] w[2] + x[3] w[3] + b

图 1-3。感知器的示例
1965 年,伊瓦赫涅科和拉帕在他们的论文“数据处理的群方法——随机逼近的竞争方法”中发表了第一个工作的神经网络。在这个领域存在一些争议,但伊瓦赫涅科被一些人认为是深度学习之父。
在这个时候,人们对机器能够做到什么做出了大胆的预测。机器翻译、语音识别等将比人类表现更好。世界各国政府兴奋起来,开始敞开钱包资助这些项目。这场淘金热始于 20 世纪 50 年代末,并一直持续到 20 世纪 70 年代中期。
寒冷黑暗的日子
投入了数百万美元后,第一个系统被投入实践。结果发现,很多最初的预言是不现实的。语音识别只有在特定方式下才有效,即使如此,也只适用于有限的一组词汇。语言翻译结果错误严重,成本远远超过人类的成本。感知器(本质上是单层神经网络)很快达到了可靠预测的上限。这限制了它们在现实世界中大多数问题中的实用性。这是因为它们是线性函数,而现实世界中的问题通常需要非线性分类器进行准确预测。想象一下试图将一条直线拟合到一条曲线上!
那么当你过度承诺而未能兑现时会发生什么?你会失去资金。国防高级研究计划局,通常称为 DARPA(是的,那些人;建立了 ARPANET,后来成为互联网的人),资助了美国的许多最初项目。然而,近 20 年来缺乏结果越来越让这个机构感到沮丧。登上月球比获得可用的语音识别器更容易!
同样,在大西洋的另一边,1974 年发表了莱特希尔报告,其中说:“通用机器人是一个幻觉。”想象一下,作为 1974 年的英国人,看着计算机科学的权威在 BBC 上辩论人工智能是否是资源浪费。结果,人工智能研究在英国遭到破坏,随后在全世界范围内也受到影响,摧毁了许多职业生涯。这种对人工智能失去信心的阶段持续了大约两十年,被称为“人工智能寒冬”。如果当时能有尼德·史塔克在场警告他们就好了。
一线希望
即使在那些寒冷的日子里,这个领域也做了一些开创性的工作。当然,感知器——作为线性函数——具有有限的能力。如何解决这个问题?通过将它们链接在一个网络中,使得一个(或多个)感知器的输出是另一个(或多个)感知器的输入。换句话说,一个多层神经网络,如图 1-4 所示。层数越多,学习的非线性就越多,从而产生更好的预测。只有一个问题:如何训练它?乔治·希尔顿和他的朋友们登场了。他们在 1986 年的论文“通过反向传播错误学习表示”中发表了一种称为反向传播的技术。它是如何工作的?做出预测,看看预测与现实有多大偏差,然后将错误的大小传播回网络,以便学会修正它。重复这个过程,直到错误变得微不足道。一个简单而强大的概念。我们在本书中反复使用反向传播这个术语。

图 1-4。一个多层神经网络的示例(图片来源)
1989 年,乔治·赛本科提供了通用逼近定理的第一个证明,该定理指出具有单个隐藏层的神经网络在理论上能够模拟任何问题。这是令人瞩目的,因为这意味着神经网络可以(至少在理论上)超越任何机器学习方法。它甚至可以模仿人类大脑。但所有这些都只是纸上谈兵。这个网络的规模很快就会在现实世界中限制。通过使用多个隐藏层并使用...等待...反向传播来训练网络,这个问题可以在一定程度上得到解决!
在更实际的一面,卡内基梅隆大学的一个团队于 1986 年建造了第一辆自主车辆 NavLab 1(图 1-5)。最初,它使用单层神经网络来控制方向盘的角度。这最终导致了 1995 年的 NavLab 5。在一次演示中,一辆汽车独自驾驶了从匹兹堡到圣迭戈的 2850 英里旅程中的所有路程,只剩下 50 英里。NavLab 在许多特斯拉工程师出生之前就获得了驾驶执照!

图 1-5。1986 年的自主 NavLab 1 的全貌(图片来源)
上世纪 80 年代的另一个突出例子是美国邮政服务(USPS)。该服务需要根据邮政编码(邮政编码)自动对邮件进行分类。由于许多邮件一直是手写的,因此无法使用光学字符识别(OCR)。为了解决这个问题,杨立昆等人使用了来自国家标准技术研究所(NIST)的手写数据,以证明神经网络能够识别这些手写数字,他们在论文“应用于手写邮政编码识别的反向传播”中使用了这些手写数据。该机构的网络 LeNet 成为 USPS 几十年来用于自动扫描和分类邮件的工具。这是令人瞩目的,因为这是第一个真正在野外工作的卷积神经网络。最终,在 1990 年代,银行将使用该模型的演变版本 LeNet-5 来读取支票上的手写数字。这为现代计算机视觉奠定了基础。
注意
那些已经了解 MNIST 数据集的人可能已经注意到我们刚刚提到的 NIST 的联系。这是因为 MNIST 数据集本质上包含了原始 NIST 数据集中的一部分图像,对它们进行了一些修改(“MNIST”中的“M”),以便为神经网络的训练和测试过程提供便利。修改包括将它们调整为 28 x 28 像素,将数字居中在该区域,抗锯齿等。

图 1-6。MNIST 数据集中手写数字的样本
一些人继续他们的研究,包括尤尔根·施密德胡伯,他提出了像长短期记忆(LSTM)这样的网络,具有文本和语音的有前途的应用。
在那时,尽管理论已经足够先进,但实际上无法证明结果。主要原因是当时的硬件计算成本太高,将它们扩展到更大的任务是一个挑战。即使通过某种奇迹硬件是可用的,要实现其全部潜力的数据肯定不容易获得。毕竟,互联网仍处于拨号阶段。支持向量机(SVM),一种用于分类问题的机器学习技术在 1995 年引入,速度更快,在较小量的数据上提供了相当不错的结果,因此已经成为常态。
因此,人工智能和深度学习的声誉很差。研究生被警告不要进行深度学习研究,因为这是“聪明的科学家的职业终结的领域”。在该领域工作的人和公司会使用替代词,如信息学、认知系统、智能代理、机器学习等,以与人工智能名称区分开来。这有点像美国战争部被重新品牌为国防部,以使人们更容易接受。
深度学习如何成为一种事物
幸运的是,2000 年代带来了高速互联网、智能手机摄像头、视频游戏以及像 Flickr 和知识共享这样的照片分享网站(带来了合法重用他人照片的能力)。大量的人们能够迅速用口袋里的设备拍照,然后立即上传。数据湖正在填满,逐渐有了足够的机会去尝试。这个快乐的交汇产生了 1400 万张图像的 ImageNet 数据集,并得益于(当时的普林斯顿大学)李飞飞等人的一些巨大工作。
在同一个十年里,个人电脑和游戏机游戏变得非常严肃。玩家们要求他们的视频游戏拥有越来越好的图形。这反过来促使了图形处理单元(GPU)制造商,如 NVIDIA 不断改进他们的硬件。这里要记住的关键一点是,GPU 在矩阵运算方面非常出色。为什么会这样?因为数学需要!在计算机图形学中,常见的任务,如移动对象、旋转对象、改变形状、调整光照等都使用矩阵运算。而 GPU 专门擅长执行这些任务。你知道还有什么需要大量矩阵计算吗?神经网络。这是一个巧合。
有了 ImageNet 准备好后,2010 年设立了年度 ImageNet 大规模视觉识别挑战赛(ILSVRC),公开挑战研究人员提出更好的分类这些数据的技术。提供了一个包含大约 120 万张图像的 1,000 个类别的子集,以推动研究的边界。像尺度不变特征变换(SIFT)+ SVM 这样的最新计算机视觉技术在 2010 年产生了 28%(2010 年)和 25%(2011 年)的前五错误率(即,如果按概率排名的前五个猜测之一匹配,则被认为是准确的)。
然后到了 2012 年,排行榜上的一个条目将错误率几乎减少到 16%。来自多伦多大学的 Alex Krizhevsky,Ilya Sutskever(最终创立了 OpenAI)和 Geoffrey Hinton 提交了该条目。AlexNet 恰如其名,受到 LeNet-5 的启发。即使只有八层,AlexNet 拥有庞大的 6000 万参数和 65 万个神经元,导致一个 240 MB 的模型。它使用两个 NVIDIA GPU 在一周内训练。这一事件让所有人都感到惊讶,证明了 CNN 的潜力,这导致了现代深度学习时代的发展。
图 1-7 量化了 CNN 在过去十年中取得的进展。自 2012 年深度学习出现以来,我们看到 ImageNet LSVRC 获奖作品的分类错误率每年减少 40%。随着 CNN 变得更深,错误率继续下降。

图 1-7. ImageNet LSVRC 获奖作品的演变
请记住,我们正在大大简化 AI 的历史,并且肯定忽略了一些细节。基本上,这是数据、GPU 和更好技术的融合导致了这个现代深度学习时代。进展不断扩展到新的领域。正如表 1-1 所强调的,科幻小说中的东西已经成为现实。
表 1-1. 现代深度学习时代的精华片段
| 2012 | 来自 Google Brain 团队的神经网络开始观看 YouTube 视频后识别猫 |
|---|---|
| 2013 |
-
研究人员开始在各种任务上尝试深度学习
-
word2vec 为单词和短语带来上下文,使理解含义更接近一步
-
语音识别的错误率下降了 25%
|
| 2014 |
|---|
-
GANs 被发明
-
Skype 实时翻译语音
-
聊天机器人 Eugene Goostman 通过图灵测试
-
使用神经网络进行序列到序列学习
-
图像字幕将图像翻译成句子
|
| 2015 |
|---|
-
微软 ResNet 在图像准确性上击败人类,训练 1000 层网络
-
百度的 Deep Speech 2 进行端到端语音识别
-
Gmail 推出智能回复
-
YOLO(You Only Look Once)实时进行目标检测
-
视觉问答允许根据图像提出问题
|
| 2016 |
|---|
-
AlphaGo 击败专业人类围棋选手
-
Google WaveNets 帮助生成逼真的音频
-
微软在对话语音识别中实现了与人类的平等
|
| 2017 |
|---|
-
AlphaGo Zero 在 3 天内学会自己下围棋
-
胶囊网络修复 CNN 中的缺陷
-
引入张量处理单元(TPUs)
-
加利福尼亚州允许出售自动驾驶汽车
-
Pix2Pix 允许从草图生成图像
|
| 2018 |
|---|
-
AI 设计比人类更好的 AI,使用神经结构搜索
-
Google Duplex 演示代表我们预订餐厅
-
Deepfakes 在视频中交换一个面孔
-
Google 的 BERT 在语言理解任务中成功超越人类
-
DawnBench 和 MLPerf 建立用于基准测试 AI 训练
|
| 2019 |
|---|
-
OpenAI Five 击败 Dota2 世界冠军
-
StyleGan 生成逼真的图像
-
OpenAI GPT-2 生成逼真的文本段落
-
富士通在 75 秒内训练 ImageNet
-
微软向 OpenAI 投资 10 亿美元
-
艾伦研究所的 AI 通过 12 年级科学考试,得分 80%
|
希望现在您对 AI 和深度学习有了历史背景,并了解为什么这一时刻如此重要。重要的是要认识到在这一领域进展迅速的速度。但正如我们迄今所见,情况并非总是如此。
根据该领域的两位先驱,实现真实世界的计算机视觉的最初估计是在 20 世纪 60 年代的“一个夏天”。他们只差了半个世纪!成为未来学家并不容易。亚历山大·维斯纳-格罗斯的一项研究发现,算法提出和取得突破之间的平均时间约为 18 年。另一方面,数据集提供和帮助实现突破之间的平均时间仅为三年!看看过去十年的任何突破。实现该突破的数据集很可能在几年前才被提供。
数据显然是限制因素。这显示了一个好数据集对深度学习可以发挥的关键作用。然而,数据并不是唯一的因素。让我们看看构成完美深度学习解决方案基础的其他支柱。
完美深度学习解决方案的配方
在 Gordon Ramsay 开始烹饪之前,他确保所有的配料都准备就绪。解决问题时使用深度学习也是如此(图 1-8)。

图 1-8。完美深度学习解决方案的成分
这就是你的深度学习*准备好了!
*Dataset + Model + Framework + Hardware = Deep Learning Solution*
让我们更详细地看看这些内容。
数据集
就像吃豆子的吃豆人渴望数据一样,深度学习也渴望数据——大量的数据。它需要这么多数据来发现有意义的模式,从而帮助做出稳健的预测。传统机器学习在 20 世纪 80 年代和 90 年代是主流,因为即使只有几百到几千个示例,它也可以正常运行。相比之下,从头开始构建的深度神经网络(DNNs)通常需要更多的数据来完成典型的预测任务。这里的好处是预测更准确。
在本世纪,我们每天都会产生数量巨大的数据——图像、文本、视频、传感器数据等等,但要有效利用这些数据,我们需要标签。要构建一个情感分类器,以了解亚马逊评论是积极的还是消极的,我们需要成千上万的句子,并为每个句子分配一个情感。要训练一个用于 Snapchat 镜头的面部分割系统,我们需要在成千上万的图像上准确标记眼睛、嘴唇、鼻子等位置。要训练一辆自动驾驶汽车,我们需要用人类驾驶员对控制设备的反应标记视频片段,例如刹车、油门、方向盘等。这些标签对我们的人工智能起着教师的作用,比仅有未标记数据更有价值。
获取标签可能会很昂贵。难怪有成千上万的工人在众包标注任务周围形成了一个整个行业。每个标签的成本可能从几分钱到几美元不等,取决于工人分配标签所花费的时间。例如,在开发微软 COCO(上下文中的常见对象)数据集期间,大约需要三秒来标记图像中每个对象的名称,大约需要 30 秒来在每个对象周围放置一个边界框,以及 79 秒来为每个对象绘制轮廓。重复这个过程数十万次,你就能开始估算一些更大数据集的成本。一些标注公司,如 Appen 和 Scale AI,已经价值超过十亿美元。
我们的银行账户可能没有一百万美元。但幸运的是,在这次深度学习革命中发生了两件好事:
-
大型标记数据集已经被主要公司和大学慷慨地公开。
-
一种称为 迁移学习 的技术,允许我们将模型调整到具有数百个示例的数据集上,只要我们的模型最初是在类似于我们当前集合的更大数据集上进行训练的。我们在本书中反复使用这种技术,包括在第五章中,我们进行实验并证明即使有几十个示例,也可以通过这种技术获得良好的性能。迁移学习打破了训练良好模型需要大数据的神话。欢迎来到 微小数据 的世界!
表格 1-2 展示了当今一些流行的数据集,用于各种深度学习任务。
表格 1-2. 多样的公共数据集
| 数据类型 | 名称 | 详情 |
|---|---|---|
| 图像 | 开放图像 V4(来自谷歌) |
-
19700 个类别中的九百万张图像
-
拥有 600 个类别的 174 万张图像(带有边界框)
|
| Microsoft COCO |
|---|
-
拥有 80 个对象类别的 33 万张图像
-
包含边界框、分割和每张图像五个标题
|
| 视频 | YouTube-8M |
|---|
-
610 万个视频,3862 个类别,26 亿个视听特征
-
3.0 标签/视频
-
1.53 TB 的随机抽样视频
|
| 视频、图像 | BDD100K(来自加州大学伯克利分校) |
|---|
-
超过 1100 小时的 10 万个驾驶视频
-
拥有 10 个类别的边界框的 10 万张图像
-
拥有车道标记的 10 万张图像
-
拥有可驾驶区域分割的 10 万张图像
-
拥有像素级实例分割的 1 万张图像
|
| Waymo 开放数据集 | 总计 3,000 个驾驶场景,16.7 小时的视频数据,60 万帧,约 2500 万个 3D 边界框和 2200 万个 2D 边界框 |
|---|---|
| 文本 | SQuAD |
| Yelp 评论 | |
| 卫星数据 | Landsat 数据 |
| 音频 | Google AudioSet |
| LibriSpeech | 1000 小时的英语朗读语音 |
模型架构
在高层次上,模型只是一个函数。它接受一个或多个输入并给出一个输出。输入可以是文本、图像、音频、视频等形式。输出是一个预测。一个好的模型是那些预测可靠地匹配预期现实的模型。模型在数据集上的准确性是决定其是否适用于实际应用的主要因素。对于许多人来说,这就是他们真正需要了解的关于深度学习模型的全部。但当我们窥探模型的内部工作时,它变得真正有趣(图 1-9)。

图 1-9. 一个深度学习模型的黑盒视图
模型内部是一个由节点和边组成的图。节点代表数学运算,而边代表数据如何从一个节点流向另一个节点。换句话说,如果一个节点的输出可以成为一个或多个节点的输入,那么这些节点之间的连接由边表示。这个图的结构决定了准确性的潜力、速度、它消耗的资源(内存、计算和能量)以及它能够处理的输入类型。
节点和边的布局被称为模型的架构。本质上,它是一个蓝图。现在,蓝图只是一部分。我们仍然需要实际的建筑。训练是利用这个蓝图来构建那座建筑的过程。我们通过反复进行以下步骤来训练模型:1)输入数据,2)从中获取输出,3)监视这些预测与预期现实(即与数据相关联的标签)之间的差距,然后,4)将错误的大小传播回模型,以便它逐渐学会自我纠正。这个训练过程是迭代进行的,直到我们对预测的准确性感到满意。
这种训练的结果是一组数字(也称为权重),分配给每个节点。这些权重是图中的节点在给定的输入上操作所必需的参数。在训练开始之前,我们通常将随机数分配为权重。训练过程的目标基本上是逐渐调整每组这些权重的值,直到它们与相应的节点一起产生令人满意的预测。
为了更好地理解权重,让我们来看下面的数据集,其中有两个输入和一个输出:
表 1-3.示例数据集
| input[1] | input[2] | output |
|---|---|---|
| --- | --- | --- |
| 1 | 6 | 20 |
| 2 | 5 | 19 |
| 3 | 4 | 18 |
| 4 | 3 | 17 |
| 5 | 2 | 16 |
| 6 | 1 | 15 |
使用线性代数(或我们头脑中的猜测),我们可以推断控制这个数据集的方程是:
output = f(input[1], input[2]) = 2 x input[1] + 3 x input[2]
在这种情况下,这个数学运算的权重是 2 和 3。一个深度神经网络有数百万个这样的权重参数。
根据使用的节点类型不同,不同主题的模型架构将更适合不同类型的输入数据。例如,CNNs 用于图像和音频,而循环神经网络(RNNs)和 LSTM 通常用于文本处理。
一般来说,从头开始训练这些模型可能需要相当长的时间,可能需要几周。幸运的是,许多研究人员已经完成了在通用数据集(如 ImageNet)上训练它们的艰苦工作,并使它们可供所有人使用。更好的是,我们可以拿这些可用的模型并将它们调整到我们的特定数据集。这个过程称为迁移学习,占了从业者绝大多数需求。
与从头开始训练相比,迁移学习提供了双重优势:显著减少的训练时间(几分钟到几小时,而不是几周),并且可以使用大大较小的数据集(数百到数千个数据样本,而不是数百万个)。表 1-4 显示了一些著名的模型架构示例。
表 1-4.多年来的示例模型架构
| 任务 | 示例模型架构 |
|---|---|
| --- | --- |
| 图像分类 | ResNet-152(2015 年),MobileNet(2017 年) |
| 文本分类 | BERT(2018 年),XLNet(2019 年) |
| 图像分割 | U-Net(2015 年),DeepLabV3(2018 年) |
| 图像翻译 | Pix2Pix(2017 年) |
| 目标检测 | YOLO9000(2016 年),Mask R-CNN(2017 年) |
| 语音生成 | WaveNet(2016 年) |
表 1-4 中的每个模型都在参考数据集(例如,分类的 ImageNet,检测的 MS COCO)上有一个已发布的准确度指标。此外,这些架构有它们自己的特征资源需求(以兆字节为单位的模型大小,以浮点运算为单位的计算需求,或 FLOPS)。
我们将在接下来的章节深入探讨迁移学习。现在,让我们看看我们可以使用的深度学习框架和服务。
注
当 Kaiming He 等人在 2015 年提出了 152 层的 ResNet 架构时——考虑到之前最大的 GoogLeNet 模型由 22 层组成,这是当时的壮举——每个人心中只有一个问题:“为什么不是 153 层?”原来,原因是 Kaiming 的 GPU 内存用完了!
框架
有几个深度学习库可以帮助我们训练模型。此外,还有专门用于使用这些训练模型进行预测(或推理)的框架,优化应用程序所在的位置。
从历史上看,就像通常的软件一样,许多库已经出现并消失了——Torch(2002 年)、Theano(2007 年)、Caffe(2013 年)、Microsoft Cognitive Toolkit(2015 年)、Caffe2(2017 年)——并且这个领域一直在迅速发展。从每个库中学到的东西使其他库更容易上手,引起了兴趣,并提高了初学者和专家的生产力。表 1-5 看一些流行的框架。
表 1-5. 流行的深度学习框架
| 框架 | 最适用于 | 典型目标平台 |
|---|---|---|
| TensorFlow(包括 Keras) | 训练 | 台式机、服务器 |
| PyTorch | 训练 | 台式机、服务器 |
| MXNet | 训练 | 台式机、服务器 |
| TensorFlow Serving | 推理 | 服务器 |
| TensorFlow Lite | 推理 | 移动和嵌入式设备 |
| TensorFlow.js | 推理 | 浏览器 |
| ml5.js | 推理 | 浏览器 |
| Core ML | 推理 | 苹果设备 |
| Xnor AI2GO | 推理 | 嵌入式设备 |
TensorFlow
2011 年,Google Brain 开发了用于内部研究和工程的 DNN 库 DistBelief。它帮助训练了 Inception(2014 年 ImageNet 大规模视觉识别挑战的获奖作品),并帮助提高了 Google 产品中语音识别的质量。与 Google 的基础设施紧密联系,它不容易配置和与外部机器学习爱好者共享代码。意识到这些限制,Google 开始研发第二代分布式机器学习框架,承诺是通用、可扩展、高性能且可移植到许多硬件平台。而且最重要的是,它是开源的。Google 称之为 TensorFlow,并于 2015 年 11 月宣布发布。
TensorFlow 实现了许多前述承诺,从开发到部署形成了一个端到端的生态系统,并在这个过程中获得了大量的追随者。在 GitHub 上拥有超过 10 万颗星星,显示出没有停止的迹象。然而,随着采用的增加,该库的用户 rightly 批评它使用起来不够简单。正如笑话所说,TensorFlow 是由 Google 工程师制作的库,为 Google 工程师制作的库,如果你足够聪明使用 TensorFlow,你就足够聪明被雇佣在那里。
但 Google 并不孤单。说实话,即使到 2015 年,使用深度学习库仍然是一种令人不愉快的经历。甚至忘记使用这些库,安装其中一些框架就让人想拔头发。(Caffe 的用户们,这是不是让你们想起了什么?)
Keras
作为深度学习从业者面临困难的答案,François Chollet 于 2015 年 3 月发布了开源框架 Keras,自那以后世界就变了。这个解决方案突然使深度学习对初学者变得可访问。Keras 提供了一个直观且易于使用的编码界面,然后使用其他深度学习库作为后端计算框架。从其第一个后端 Theano 开始,Keras 鼓励快速原型设计并减少代码行数。最终,这种抽象扩展到其他框架,包括 Cognitive Toolkit、MXNet、PlaidML,以及 TensorFlow。
PyTorch
同时,PyTorch 在 2016 年初在 Facebook 开始,工程师们有幸观察到 TensorFlow 的局限性。PyTorch 从一开始就支持本地 Python 构造和 Python 调试,使其灵活且易于使用,很快成为 AI 研究人员的最爱。它是第二大端到端深度学习系统。Facebook 另外构建了 Caffe2,用于将 PyTorch 模型部署到生产环境,为超过十亿用户提供服务。PyTorch 推动了研究,而 Caffe2 主要用于生产。2018 年,Caffe2 被吸收到 PyTorch 中,形成一个完整的框架。
一个不断发展的领域
如果这个故事以 Keras 和 PyTorch 的便利结束,这本书的副标题就不会有“TensorFlow”这个词了。TensorFlow 团队意识到,如果真的想要扩大工具的影响力并使 AI 民主化,就需要让工具更容易使用。因此,当 Keras 正式作为 TensorFlow 的一部分包含在内时,这是一个好消息,提供了两全其美的选择。这使开发人员可以使用 Keras 定义模型和训练模型,使用核心 TensorFlow 进行高性能数据管道,包括分布式训练和部署生态系统。这是天作之合!最重要的是,TensorFlow 2.0(2019 年发布)包括对本地 Python 构造和急切执行的支持,正如我们在 PyTorch 中看到的那样。
有了这么多竞争框架可用,可移植性的问题不可避免地出现。想象一下,一篇新的研究论文以 PyTorch 公开发布的最先进模型。如果我们不在 PyTorch 中工作,我们将无法参与研究,必须重新实现和训练。开发人员喜欢能够自由共享模型,而不受限于特定的生态系统。许多开发人员自然地编写了库,将模型格式从一个库转换为另一个库。这是一个简单的解决方案,但由于转换工具的数量庞大,缺乏官方支持和足够的质量,导致了组合爆炸。为了解决这个问题,微软和 Facebook 等行业主要参与者发起了开放神经网络交换(ONNX)。ONNX 提供了一个通用模型格式的规范,可以被多个流行库官方读写。此外,它为不支持此格式的库提供了转换器。这使开发人员可以在一个框架中进行训练,然后在另一个框架中进行推断。
除了这些框架外,还有几个图形用户界面(GUI)系统,可以实现无代码训练。使用迁移学习,它们可以快速生成多种格式的训练模型,用于推断。即使是您的祖母也可以通过点按界面快速训练神经网络!
表 1-6.流行的基于 GUI 的模型训练工具
| 服务 | 平台 |
|---|---|
| 微软 CustomVision.AI | 基于 Web |
| Google AutoML | 基于 Web |
| Clarifai | 基于 Web |
| IBM 视觉识别 | 基于 Web |
| 苹果 Create ML | macOS |
| NVIDIA DIGITS | 桌面 |
| Runway ML | 桌面 |
那么为什么我们选择 TensorFlow 和 Keras 作为本书的主要框架?考虑到可用的材料数量,包括文档、Stack Overflow 答案、在线课程、庞大的贡献者社区、平台和设备支持、行业采用以及可用的工作岗位(在美国,与 PyTorch 相比,大约有三倍的 TensorFlow 相关角色),当涉及到框架时,TensorFlow 和 Keras 目前主导着这个领域。对我们来说选择这种组合是有道理的。也就是说,本书讨论的技术也适用于其他库。学习一个新的框架不应该花费太长时间。因此,如果您真的想要加入一个专门使用 PyTorch 的公司,不要犹豫去申请。
硬件
1848 年,当詹姆斯·W·马歇尔在加利福尼亚州发现金矿时,这一消息迅速传遍美国。成千上万的人涌向该州开始挖掘财富。这被称为加利福尼亚淘金热。早期行动者能够挖掘出一大笔财富,但后来者则没有那么幸运。但淘金热多年来并未停止。你能猜到在这段时间内谁赚了最多钱吗?铲子制造商!
云和硬件公司是 21 世纪的铲子制造商。不相信?看看过去十年微软和英伟达的股票表现。1849 年和现在唯一的区别是我们可供选择的铲子数量之多令人难以置信。
鉴于可用的硬件种类繁多,重要的是根据应用程序的资源、延迟、预算、隐私和法律要求来做出正确的选择。
根据您的应用程序与用户的交互方式,推理阶段通常有用户在另一端等待响应。这对可以使用的硬件类型以及硬件的位置施加了限制。例如,由于网络延迟问题,Snapchat 镜头无法在云端运行。此外,它需要在接近实时的情况下运行,以提供良好的用户体验(UX),因此对每秒处理的帧数设置了最低要求(通常>15 fps)。另一方面,上传到图像库(如 Google 照片)的照片不需要立即进行图像分类。几秒钟或几分钟的延迟是可以接受的。
走向另一个极端,培训需要更多的时间;从几分钟到几小时到几天不等。根据我们的培训场景,更好的硬件的真正价值在于能够加快实验速度和增加迭代次数。对于比基本神经网络更严肃的事情,更好的硬件可以产生巨大的差异。通常情况下,与 CPU 相比,GPU 的速度会提高 10 到 15 倍,并且性能每瓦更高,将实验完成的等待时间从一周减少到几个小时。这可能是观看大峡谷纪录片(两小时)与实际前往大峡谷旅行(四天)之间的区别。
以下是几个基本的硬件类别可供选择以及它们通常的特征(另请参见图 1-10):
中央处理器(CPU)
便宜、灵活、慢。例如,英特尔酷睿 i9-9900K。
图形处理器(GPU)
高吞吐量,非常适合批处理以利用并行处理,价格昂贵。例如,英伟达 GeForce RTX 2080 Ti。
现场可编程门阵列(FPGA)
快速、低功耗、可重新编程以适应定制解决方案,价格昂贵。知名公司包括赛灵思、Lattice Semiconductor、Altera(英特尔)。由于能够在几秒钟内运行并可配置到任何 AI 模型,微软必应将大部分 AI 运行在 FPGA 上。
专用集成电路(ASIC)
定制芯片。设计成本极高,但在大规模生产时成本低廉。就像在制药行业一样,第一件物品的成本最高,因为设计和制造所需的研发工作。大规模生产相对廉价。具体示例包括以下内容:
张量处理单元(TPU)
专门用于神经网络操作的 ASIC,仅在 Google Cloud 上提供。
边缘 TPU
小于美国一分硬币大小,加速边缘推理。
神经处理单元(NPU)
通常由智能手机制造商使用,这是一种专用芯片,用于加速神经网络推理。

图 1-10。相对于灵活性、性能和成本的不同类型硬件的比较
让我们看看每种情况下会使用哪些方案:
-
开始训练→CPU
-
训练大型网络→GPU 和 TPU
-
智能手机推断→移动 CPU、GPU、数字信号处理器(DSP)、NPU
-
可穿戴设备(例如智能眼镜、智能手表)→Edge TPU、NPU
-
嵌入式 AI 项目(例如洪水调查无人机、自动轮椅)→加速器如谷歌 Coral、英特尔 Movidius 与树莓派,或像 NVIDIA Jetson Nano 这样的 GPU,一直到用于智能音箱唤醒词检测的 15 美元微控制器(MCU)
随着我们阅读本书,我们将仔细探讨其中许多内容。
负责任的 AI
到目前为止,我们已经探讨了 AI 的力量和潜力。它展现了极大的潜力,可以增强我们的能力,使我们更加高效,赋予我们超能力。
但伟大的力量伴随着伟大的责任。
尽管 AI 可以帮助人类,但当设计得不经过深思熟虑时(无论是有意还是无意),它也有同等的潜力伤害我们。AI 本身不应受责备;而是 AI 的设计者。
考虑一些过去几年在新闻中曝光的真实事件。
-
“____ 据称可以通过分析你的面部判断你是否是恐怖分子”(图 1-11):《计算机世界》,2016 年
-
“AI 正在错误地将人们送进监狱”:《麻省理工科技评论》,2019 年
-
“____ 超级计算机推荐‘不安全和不正确’的癌症治疗方法,内部文件显示”:《STAT 新闻》,2018 年
-
“____ 开发了一个用于招聘人才的 AI 工具,但不得不关闭它,因为它歧视女性”:《商业内幕》,2018 年
-
“____AI 研究:主要物体识别系统偏向于拥有更多金钱的人”:《风险投资者》,2019 年
-
“____ 将黑人标记为‘大猩猩’”:《今日美国》,2015 年。“两年后,____ 通过从图像分类器中清除‘大猩猩’标签解决了‘种族主义算法’问题”:《Boing Boing》,2018 年
-
“____ 在 Twitter 用户教会它种族主义后,让其新的 AI 机器人 Tay 保持沉默”:《科技博客》,2016 年
-
“AI 误将公交车侧面广告误认为著名 CEO,指控其违章横穿马路”:《财新全球》,2018 年
-
“____ 因员工反对‘战争生意’而决定放弃五角大楼的 AI 合同”:《华盛顿邮报》,2018 年
-
“自动驾驶 ____ 致命车辆‘在撞倒并杀死行人前六秒发现了她’”:《太阳报》,2018 年

图 1-11。声称根据面部结构对人进行分类的初创公司
你能在这里填上空白吗?我们给你一些选项——亚马逊、微软、谷歌、IBM 和优步。继续填写。我们会等待。
我们将它们留空是有原因的。这是为了认识到这不是属于特定个人或公司的问题。这是每个人的问题。尽管这些事情发生在过去,可能不反映当前状态,但我们可以从中学习,尽量避免犯同样的错误。这里的一线希望是每个人都从这些错误中学到了东西。
作为 AI 的开发者、设计师、架构师和领导者,我们有责任超越表面上的技术问题。以下是一些与我们解决的任何问题(无论是 AI 还是其他问题)相关的主题。它们不应该被忽视。
偏见
在我们日常工作中,我们常常带入自己的偏见,有意或无意。这是多种因素的结果,包括我们的环境、成长背景、文化规范,甚至我们固有的天性。毕竟,AI 和驱动它们的数据集并非在真空中创造出来的——它们是由带有自己偏见的人类创造的。计算机不会自己神奇地产生偏见,它们反映和放大现有的偏见。
以 YouTube 应用程序早期的一个例子为例,开发人员注意到大约 10%的上传视频是颠倒的。也许如果这个数字更低,比如 1%,可能会被视为用户错误。但 10%的数字太高了,不能被忽视。你知道谁恰好占人口的 10%吗?左撇子!这些用户将手机保持在与右撇子同向相反的方向。但 YouTube 的工程师在开发和测试移动应用程序时没有考虑到这种情况,因此 YouTube 将上传的视频以相同的方向上传到服务器,供左撇子和右撇子用户使用。
如果开发人员团队中有一个左撇子,这个问题本可以更早被发现。这个简单的例子展示了多样性的重要性。左右利手只是定义个体的一个小属性。许多其他因素,通常超出他们的控制,经常起作用。诸如性别、肤色、经济地位、残疾、出生国家、语音模式,甚至像头发长度这样微不足道的事情,都可能决定某人的改变生活结果,包括算法如何对待他们。
谷歌的机器学习词汇表列出了可能影响机器学习流程的几种偏见形式。以下只是其中一些:
选择偏见
数据集不代表真实世界问题的分布,并偏向于某些类别的子集。例如,在许多虚拟助手和智能家居扬声器中,一些口音被过度代表,而其他口音在训练数据集中根本没有数据,导致世界大部分人口的用户体验不佳。
选择偏见也可能是因为概念的共同出现。例如,当使用谷歌翻译将句子“她是医生。他是护士”翻译成土耳其等性别中立语言,然后再翻译回来时,性别会发生变化,如图 1-12 所示。这可能是因为数据集包含了大量的男性代词和“医生”一词的共同出现,以及女性代词和“护士”一词的共同出现。

图 1-12。谷歌翻译反映了数据中的潜在偏见(截至 2019 年 9 月)
隐性偏见
这种偏见是因为我们在看到某事时都会做出的隐性假设。考虑图 1-13 中的突出部分。任何看到它的人都可能非常肯定地认为那些条纹属于斑马。事实上,考虑到 ImageNet 训练的网络对纹理有多么偏向,大多数网络都会将整个图像分类为斑马。除了我们知道这张图片是用斑马样式的织物装饰的沙发。

图 1-13。Glen Edelson 的斑马沙发(图片来源)
报告偏见
有时,房间里最响亮的声音是最极端的声音,并主导了对话。看一眼 Twitter 可能会让人觉得世界正在末日,而大多数人都在忙于过着平凡的生活。不幸的是,无聊是卖不动的。
内外群体偏见
来自东亚的注释员可能会看到自由女神像的照片,并给予“美国”或“美利坚合众国”等标签,而来自美国的人可能会看同一张照片,并分配更细致的标签,如“纽约”或“自由岛”。人类天性是看待自己的群体时会有细微差别,而看待其他群体时会更加同质化,这也反映在我们的数据集中。
问责和可解释性
想象一下,在 19 世纪末,卡尔·本茨先生告诉你,他发明了这种四轮设备,可以比任何其他东西更快地将你运送。除了他不知道它是如何工作的。他只知道它消耗了一种高度易燃的液体,在内部爆炸了几次以推动它前进。是什么让它移动?是什么让它停下来?是什么阻止它燃烧坐在里面的人?他没有答案。如果这是汽车的起源故事,你可能不想进入那个装置。
这正是目前人工智能正在发生的事情。以前,传统机器学习中,数据科学家必须手动从数据中选择特征(预测变量),然后机器学习模型会学习这些特征。尽管这种手动选择过程虽然繁琐和受限,但给了他们更多的控制和洞察力,以了解预测是如何产生的。然而,使用深度学习,这些特征是自动选择的。数据科学家可以通过提供大量数据来构建模型,这些模型在大多数情况下会可靠地进行预测。但数据科学家并不知道模型是如何工作的,它学习了哪些特征,在什么情况下模型有效,更重要的是,在什么情况下模型无法工作。当 Netflix 基于我们已经观看的内容向我们推荐电视节目时,这种方法可能是可以接受的(尽管我们相当肯定他们的代码中的某处有recommendations.append("Stranger Things"))。但是如今人工智能所做的远不止推荐电影。警察和司法系统开始依赖算法来决定某人是否对社会构成风险,以及他们是否应该在审判前被拘留。许多人的生命和自由岌岌可危。我们绝对不能将重要的决策外包给一个不负责任的黑匣子。幸运的是,有改变的势头,投资于可解释人工智能,其中模型不仅能够提供预测,还能解释导致其做出某种预测的因素,并揭示限制的领域。
此外,一些城市(如纽约)开始让他们的算法对公众负责,承认公众有权知道他们用于重要决策的算法以及它们的工作原理,允许专家进行审查和审计,提高政府机构的专业知识以更好地评估他们添加的每个系统,并提供争议决定的机制,这些决定是由算法做出的。
可重复性
科学领域的研究只有在可重复时才会得到社区的广泛认可;也就是说,任何研究者都应该能够复制测试条件并获得相同的结果。除非我们能够重现模型的过去结果,否则在将来使用它时无法追究责任。在没有可重复性的情况下,研究容易受到p-hacking的影响——调整实验参数直到获得期望的结果。研究人员有必要广泛记录他们的实验条件,包括数据集、基准和算法,并在进行实验之前声明他们将要测试的假设。对机构的信任达到历史最低点,而那些不基于现实但被媒体夸大的研究可能会进一步侵蚀这种信任。传统上,复制研究论文被认为是一种黑暗的艺术,因为许多实现细节被省略。令人振奋的消息是,研究人员现在逐渐开始使用公开可用的基准(而不是他们私下构建的数据集)并开源他们用于研究的代码。社区成员可以借助这些代码,证明它有效,并使其更好,从而迅速导致新的创新。
稳健性
有一个关于对 CNN 进行一像素攻击的研究领域。基本上,目标是找到并修改图像中的一个像素,使 CNN 预测完全不同的东西。例如,在一个苹果图片中改变一个像素可能导致 CNN 将其分类为狗。许多其他因素可以影响预测,如噪音、光照条件、摄像头角度等,这些因素不会影响人类做出类似判断的能力。这对于自动驾驶汽车尤为重要,因为街上的坏人可能会修改汽车看到的输入,以操纵它做坏事。事实上,腾讯的 Keen Security Lab 能够利用特斯拉 AutoPilot 的漏洞,通过在路上策略性地放置小贴纸,导致汽车变道并驶入对向车道。如果我们要信任 AI,那么必须具备能够抵御噪音、轻微偏差和故意操纵的强大 AI。
隐私
在追求构建更好的人工智能的过程中,企业需要收集大量数据。不幸的是,有时它们会越过界限,过度热衷于收集超出当前任务所需的信息。一个企业可能认为它只是为了善良目的使用收集的数据。但如果被一家对数据使用没有相同道德底线的公司收购呢?消费者的信息可能被用于超出最初目标的目的。此外,所有这些数据集中在一个地方使其成为黑客的目标,他们窃取个人信息并将其出售给犯罪企业。此外,政府已经在试图追踪每个个人时越界。
所有这些都与普遍认可的隐私人权相悖。消费者希望能够透明地了解有关他们的数据收集情况,谁可以访问这些数据,数据如何被使用,以及退出数据收集过程的机制,以及删除已经收集的数据。
作为开发者,我们希望意识到我们正在收集的所有数据,并问自己是否有必要收集某个数据。为了最小化我们收集的数据,我们可以实施隐私感知的机器学习技术,如联邦学习(在谷歌键盘中使用),这使我们能够在用户设备上训练网络,而无需将任何个人身份信息发送到服务器。
事实证明,在本节开头提到的许多标题中,是糟糕的公关后果引起了这些话题的主流意识,引入了问责制,并导致了整个行业思维的转变,以防止未来的重复发生。我们必须继续要求自己、学者、行业领袖和政客在每一次失误时负责,并迅速采取措施纠正错误。我们做出的每个决定和采取的每个行动都有可能为未来几十年树立先例。随着人工智能的普及,我们需要共同努力提出艰难的问题,并为这些问题找到答案,以便在最大程度上减少潜在的危害。
总结
本章探讨了令人兴奋的人工智能和深度学习世界的格局。我们追溯了人工智能的时间线,从其谦逊的起源,伟大的希望时期,到黑暗的人工智能冬季,再到如今的复苏。在这个过程中,我们回答了为什么这一次不同的问题。然后,我们看了构建深度学习解决方案所需的必要要素,包括数据集、模型架构、框架和硬件。这为我们在接下来的章节中进一步探索做好了准备。希望您喜欢本书的其余部分。现在是深入研究的时候了!
常见问题
-
我刚刚开始。我需要花很多钱购买强大的硬件吗?
幸运的是,您甚至可以通过网络浏览器开始。我们所有的脚本都可以在线获取,并且可以在 Google Colab 提供的免费 GPU 上运行(感谢慷慨的 Google Colab 团队,他们免费提供强大的 GPU(每次最多 12 小时)。这应该让您开始。随着您通过执行更多实验(尤其是在专业环境或大型数据集上)变得更加熟练,您可能希望通过在云上租用(Microsoft Azure、Amazon Web Services(AWS)、Google Cloud Platform(GCP)等)或购买硬件来获得 GPU。不过要注意那些电费账单!
![在 Colab 中运行的 GitHub 笔记本的屏幕截图]()
图 1-14。在 Colab 中运行的 GitHub 笔记本的屏幕截图
-
Colab 很棒,但我已经有一台为玩<插入视频游戏名称>而购买的强大计算机。我应该如何设置我的环境?
理想的设置涉及 Linux,但 Windows 和 macOS 也可以。对于大多数章节,您需要以下内容:
-
Python 3 和 PIP
-
tensorflow或tensorflow-gpuPIP 包(版本 2 或更高) -
Pillow
我们喜欢保持事情整洁和自包含,因此我们建议使用 Python 虚拟环境。每当您安装软件包或运行脚本或笔记本时,都应该使用虚拟环境。
如果您没有 GPU,您的设置就完成了。
如果您有 NVIDIA GPU,您需要安装适当的驱动程序,然后安装 CUDA,然后安装 cuDNN,然后安装
tensorflow-gpu软件包。如果您使用 Ubuntu,有一个比手动安装这些软件包更简单的解决方案,即使用Lambda Stack一行代码安装整个环境。或者,您可以使用 Anaconda Distribution 安装所有软件包,这对 Windows、Mac 和 Linux 同样有效。
-
-
我在哪里可以找到本书中使用的代码?
您可以在http://PracticalDeepLearning.ai找到可直接运行的示例。
-
阅读本书的最低先决条件是什么?
博士学位包括微积分、统计分析、变分自动编码器、运筹学等领域...绝对不是必需的,才能阅读这本书(让您有点紧张,是吧?)。一些基本的编码技能、熟悉 Python、健康的好奇心和幽默感应该会在吸收材料的过程中大有裨益。尽管初级水平的移动开发理解(使用 Swift 和/或 Kotlin)会有所帮助,但我们设计的示例是自给自足且足够简单,可以由以前从未编写过移动应用的人部署。
-
我们将使用哪些框架?
Keras + TensorFlow 用于训练。逐章探讨不同的推理框架。
-
我读完这本书后会成为专家吗?
如果您跟着学习,您将掌握从训练到推理再到性能最大化等各种主题的知识。尽管这本书主要关注计算机视觉,您也可以将相同的知识应用到其他领域,如文本、音频等,并且能够迅速上手。
-
本章前面的猫是谁?
那是 Meher 的猫,Vader。他将在本书中多次客串。不用担心,他已经签署了模特释放表。
-
我可以联系您吗?
当然。如果有任何问题、更正或其他问题,请给我们发送电子邮件至 PracticalDLBook@gmail.com,或在 Twitter 上@PracticalDLBook 发推文。
如果您正在阅读盗版副本,请考虑我们对您感到失望。
第二章:图片中有什么:使用 Keras 进行图像分类
如果您浏览过深度学习文献,可能会看到一大堆充斥着令人生畏的数学的学术解释。不用担心。我们将通过一个简单的例子来引导您进入实际的深度学习,只需几行代码就可以对图像进行分类。
在本章中,我们将更仔细地研究 Keras 框架,讨论它在深度学习领域的地位,然后使用它来使用现有的最先进分类器对一些图像进行分类。我们通过使用热图来直观地研究这些分类器的运作方式。通过这些热图,我们将在视频中对对象进行分类,做一个有趣的项目。
回想一下“完美深度学习解决方案的配方”,我们需要四个要素来创建我们的深度学习配方:硬件、数据集、框架和模型。让我们看看这些在本章中是如何发挥作用的:
-
我们从简单的硬件开始。即使是一台廉价的笔记本电脑也足以满足本章的需求。或者,您可以通过在 Colab 中打开 GitHub 笔记本(参见http://PracticalDeepLearning.ai)来运行本章中的代码。这只是几次鼠标点击的事情。
-
因为我们暂时不会训练神经网络,所以我们不需要一个数据集(除了一些样本照片用于测试)。
-
接下来,我们来看框架。本章的标题中包含了 Keras,所以我们暂时会使用它。事实上,在本书的很大一部分中,我们都会使用 Keras 来满足我们的训练需求。
-
解决深度学习问题的一种方法是获取一个数据集,编写训练代码,花费大量时间和精力(包括人力和电力)来训练模型,然后用它进行预测。但我们不是自寻烦恼的人。因此,我们将使用一个预训练模型。毕竟,研究界已经花费了大量心血和泪水来训练和发布许多现在公开可用的标准模型。我们将重复使用其中一个更著名的模型,名为 ResNet-50,这是 ResNet-152 的小兄弟,后者在 2015 年赢得了 ILSVRC 比赛。
在本章中,您将亲自动手编写一些代码。众所周知,学习的最佳方式是通过实践。不过,您可能会想知道,这背后的理论是什么?这将在后续章节中介绍,我们将通过本章作为基础,更深入地探讨 CNN 的细节。
介绍 Keras
正如第一章所讨论的,Keras 于 2015 年作为一个易于使用的抽象层出现,使快速原型设计成为可能。这使得深度学习的初学者的学习曲线陡峭度降低了很多。同时,它通过帮助他们快速迭代实验,使深度学习专家更加高效。事实上,Kaggle.com上的大多数获胜团队都使用了 Keras。最终,在 2017 年,Keras 的完整实现直接集成到了 TensorFlow 中,将 TensorFlow 的高可扩展性、性能和庞大的生态系统与 Keras 的易用性结合在一起。在网络上,我们经常看到将 TensorFlow 版本的 Keras 称为tf.keras。
在本章和第三章中,我们完全使用 Keras 编写所有代码。这包括文件读取、图像处理(增强)等样板函数。我们主要出于学习的便利性。从第五章开始,我们逐渐开始直接使用更多原生高性能的 TensorFlow 函数,以获得更多的可配置性和控制。
预测图像的类别
用通俗的语言来说,图像分类回答了一个问题:“这张图像包含什么对象?”更具体地说,“这张图像包含* X 对象的概率是多少”,其中 X 来自预定义的对象类别列表。如果概率高于最小阈值,则图像很可能包含一个或多个 X *实例。
一个简单的图像分类流程包括以下步骤:
-
加载一张图像。
-
将其调整为预定义大小,如 224 x 224 像素。
-
将像素值缩放到范围[0,1]或[–1,1],也就是归一化。
-
选择一个预训练模型。
-
在图像上运行预训练模型,以获取类别预测列表及其相应的概率。
-
显示几个最高概率类别。
提示
GitHub 链接在网站http://PracticalDeepLearning.ai上提供。导航到code/chapter-2,您将找到详细步骤的 Jupyter 笔记本1-predict-class.ipynb。
我们首先从 Keras 和 Python 包中导入所有必要的模块:
import tensorflow as tf
from tf.keras.applications.resnet50 import preprocess_input, decode_predictions
from tf.keras.preprocessing import image
import numpy as np
import matplotlib.pyplot as plt
接下来,我们加载并显示要分类的图像(参见图 2-1):
img_path = "../../sample-images/cat.jpg"
img = image.load_img(img_path, target_size=(224, 224))
plt.imshow(img)
plt.show()

图 2-1。显示输入文件内容的图表
是的,这是一只猫(尽管文件名有点暴露了)。这就是我们的模型理想情况下应该预测的内容。
在将任何图像传递给 Keras 之前,我们希望将其转换为标准格式。这是因为预训练模型期望输入具有特定大小。在我们的情况下,标准化涉及将图像调整为 224 x 224 像素。
大多数深度学习模型期望输入一批图像。但是当我们只有一张图像时该怎么办?当然,我们创建一个包含一张图像的批次!这实质上涉及制作一个由该对象组成的数组。另一种看待这个问题的方式是将维度的数量从三(表示图像的三个通道)扩展到四(额外的一个用于数组本身长度)。
如果这不清楚,考虑这种情况:对于一个包含 64 张尺寸为 224 x 224 像素的图像的批次,每张图像包含三个通道(RGB),表示该批次的对象将具有形状 64 x 224 x 224 x 3。在接下来的代码中,我们将只使用一张尺寸为 224 x 224 x 3 的图像,我们将通过将维度从三扩展到四来创建一个只包含该图像的批次。这个新创建的批次的形状将是 1 x 224 x 224 x 3:
img_array = image.img_to_array(img)
img_batch = np.expand_dims(img_array, axis=0) # Increase the number of dimensions
在机器学习中,模型在接收到一致范围内的数据时表现最佳。范围通常包括[0,1]和[–1,1]。鉴于图像像素值在 0 到 255 之间,运行 Keras 的preprocess_input函数对输入图像进行归一化,将每个像素归一化到一个标准范围。归一化或特征缩放是图像预处理的核心步骤之一,使其适用于深度学习。
现在是模型的时间。我们将使用一个名为 ResNet-50 的卷积神经网络(CNN)。我们应该问的第一个问题是:“我在哪里找到这个模型?”当然,我们可以在互联网上搜索,找到与我们的深度学习框架(Keras)兼容的内容。但是没人有时间这样做!幸运的是,Keras 喜欢简化事情,并通过一个函数调用将其提供给我们。在第一次调用此函数后,模型将从远程服务器下载并在本地缓存:
model = tf.keras.applications.resnet50.ResNet50()
当使用这个模型进行预测时,结果包括每个类别的概率预测。Keras 还提供了decode_predictions函数,告诉我们图像中包含的每个对象类别的概率。
现在,让我们看看一个方便的函数中的整个代码:
def classify(img_path):
img = image.load_img(img_path, target_size=(224, 224))
model = tf.keras.applications.resnet50.ResNet50()
img_array = image.img_to_array(img)
img_batch = np.expand_dims(img_array, axis=0)
img_preprocessed = preprocess_input(img_batch)
prediction = model.predict(img_preprocessed)
print(decode_predictions(prediction, top=3)[0])
classify("../../sample-images/cat.jpg")
[('n02123045', 'tabby', 0.50009364),
('n02124075', 'Egyptian_cat', 0.21690978),
('n02123159', 'tiger_cat', 0.2061722)]
这幅图像的预测类别是各种类型的猫科动物。为什么它不简单地预测“猫”这个词呢?简短的答案是,ResNet-50 模型是在一个包含许多类别的细粒度数据集上训练的,不包括更一般的“猫”。我们稍后会更详细地调查这个数据集,但首先让我们加载另一张样本图像(参见图 2-2):
img_path = '../../sample-images/dog.jpg'
img = image.load_img(img_path, target_size=(224, 224))
plt.imshow(img)
plt.show()

图 2-2. 显示文件 dog.jpg 内容的图
然后,我们再次运行之前的便捷函数:
classify("../../sample-images/dog.jpg")
[(u'n02113186', u'Cardigan', 0.809839),
(u'n02113023', u'Pembroke', 0.17665945),
(u'n02110806', u'basenji', 0.0042166105)]
正如预期的那样,我们得到了不同品种的犬类(不仅仅是“狗”类别)。如果你对柯基品种的狗不熟悉,那么“corgi”这个词在威尔士语中的意思就是“侏儒犬”。卡迪根和彭布罗克是柯基家族的亚品种,它们看起来非常相似。我们的模型也认为是这样,这一点也不奇怪。
注意每个类别的预测概率。通常,具有最高概率的预测被认为是答案。或者,任何高于预定义阈值的值也可以被视为答案。在狗的例子中,如果我们设置阈值为 0.5,那么卡迪根将是我们的答案。

图 2-3. 在浏览器中使用 Google Colab 运行笔记本
提示
您可以在本章中跟随代码并在浏览器中交互式地执行,而无需进行任何安装,只需使用 Google Colab。只需在 GitHub 上每个您想要尝试的笔记本顶部找到“在 Colab 上运行”链接。然后,点击“运行单元格”按钮;这应该执行该单元格中的代码,如图 2-3 所示。
调查模型
我们从模型中得到了预测,太棒了!但是是什么因素导致了这些预测?这里有一些问题我们需要问:
-
模型是在哪个数据集上训练的?
-
我可以使用其他模型吗?它们有多好?我可以在哪里获取它们?
-
为什么我的模型会做出这样的预测?
我们将在本节中探讨这些问题的答案。
ImageNet 数据集
让我们调查 ResNet-50 训练的 ImageNet 数据集。正如其名称所示,ImageNet是一个图像网络;也就是说,这是一个以网络形式组织的图像数据集,如图 2-4 所示。它以分层方式排列(类似于 WordNet 层次结构),使得父节点包含该父节点内所有可能的各种图像的集合。例如,在“动物”父节点内,有鱼类、鸟类、哺乳动物、无脊椎动物等。每个类别都有多个子类别,这些子类别又有子子类别,依此类推。例如,“美国水猎犬”类别距离根节点有八个级别。狗类别包含了总共五个层次中的 189 个子类别。
从视觉上看,我们制作了图 2-5 中显示的树状图,以帮助您了解 ImageNet 数据集包含的各种高级实体。这个树状图还显示了构成 ImageNet 数据集的不同类别的相对百分比。

图 2-4. ImageNet 数据集中的类别和子类别

图 2-5. ImageNet 及其类别的树状图
ImageNet 数据集是著名的 ILSVRC 的基础,该比赛始于 2010 年,旨在评估计算机视觉的进展并挑战研究人员在包括对象分类在内的任务上进行创新。回想一下第一章中提到的,ImageNet 挑战看到每年提交的结果在准确性上有了显著提高。当它刚开始时,错误率接近 30%。现在,它是 2.2%,已经优于普通人在这项任务上的表现。这个数据集和挑战被认为是计算机视觉最近进展的最重要原因。
等等,人工智能的准确率比人类还高?如果数据集是由人类创建的,那么人类不应该有 100%的准确率吗?事实上,数据集是由专家创建的,每个图像都经过多人验证。然后,斯坦福研究人员(现在是特斯拉的著名人物)Andrej Karpathy 尝试了解一个普通人在 ImageNet-1000 上的表现。结果他取得了 94.9%的准确率,远低于我们所有人期望的 100%。Andrej 费了一周的时间查看了 1500 张图像,每张图像花费大约一分钟的时间进行标记。他是如何将 5.1%的图像分类错误的呢?原因有点微妙:
细粒度识别
对于许多人来说,很难区分西伯利亚哈士奇和阿拉斯加雪橇犬。真正熟悉狗品种的人能够区分它们,因为他们寻找区分这两种品种的更细节的细节。事实证明,神经网络能够更容易地学习这些更细节的细节,而不是人类。
类别无知
并非每个人都知道所有 120 种狗的品种,当然也不会知道其中的每一种。但人工智能知道。毕竟,它是经过训练的。
注
与 ImageNet 类似,像 Switchboard 这样的语音数据集报告了 5.1%的语音转录错误率(巧合地与 ImageNet 相同)。很明显,人类有极限,而人工智能正在逐渐超越我们。
这种快速改进的另一个关键原因之一是研究人员公开分享了在像 ImageNet 这样的数据集上训练的模型。在下一节中,我们将更详细地了解模型重用。
模型动物园
模型动物园是一个组织或个人可以公开上传他们构建的模型供他人重用和改进的地方。这些模型可以使用任何框架(例如 Keras、TensorFlow、MXNet),用于任何任务(分类、检测等),或者在任何数据集上进行训练(例如 ImageNet、街景房屋号码(SVHN))。
模型动物园的传统始于 Caffe,这是第一个深度学习框架之一,由加州大学伯克利分校开发。从头开始在一个拥有数百万图像的数据库上训练深度学习模型需要数周的训练时间和大量的 GPU 计算能量,这使得这项任务变得困难。研究界认识到这是一个瓶颈,参加 ImageNet 比赛的组织在 Caffe 的网站上开源了他们训练过的模型。其他框架很快也效仿。
在开始新的深度学习项目时,首先探索是否已经有一个执行类似任务并在类似数据集上训练的模型是一个好主意。
Keras 中的模型动物园是使用 Keras 框架在 ImageNet 数据集上训练的各种架构的集合。我们在表 2-1 中列出它们的详细信息。
表 2-1. 选择预训练的 ImageNet 模型的架构细节
| 模型 | 大小 | Top-1 准确率 | Top-5 准确率 | 参数 | 深度 |
|---|---|---|---|---|---|
| VGG16 | 528 MB | 0.713 | 0.901 | 138,357,544 | 23 |
| VGG19 | 549 MB | 0.713 | 0.9 | 143,667,240 | 26 |
| ResNet-50 | 98 MB | 0.749 | 0.921 | 25,636,712 | 50 |
| ResNet-101 | 171 MB | 0.764 | 0.928 | 44,707,176 | 101 |
| ResNet-152 | 232 MB | 0.766 | 0.931 | 60,419,944 | 152 |
| InceptionV3 | 92 MB | 0.779 | 0.937 | 23,851,784 | 159 |
| InceptionResNetV2 | 215 MB | 0.803 | 0.953 | 55,873,736 | 572 |
| NASNetMobile | 23 MB | 0.744 | 0.919 | 5,326,716 | — |
| NASNetLarge | 343 MB | 0.825 | 0.96 | 88,949,818 | — |
| MobileNet | 16 MB | 0.704 | 0.895 | 4,253,864 | 88 |
| MobileNetV2 | 14 MB | 0.713 | 0.901 | 3,538,984 | 88 |
“Top-1 准确率”列指示最佳猜测正确答案的次数,“Top-5 准确率”列指示五次猜测中至少有一次正确的次数。网络的“深度”指示网络中存在多少层。“参数”列指示模型的大小;也就是说,模型有多少个独立的权重:参数越多,模型越“重”,预测速度越慢。在本书中,我们经常使用 ResNet-50(在研究论文中引用最多的常见架构,以获得高准确性)和 MobileNet(在速度、大小和准确性之间取得良好平衡)。
类激活图
图像显著性,在 UX 研究中通常很有名,试图回答“用户注意力集中在图像的哪个部分?”这是通过眼动研究来实现的,并以热图表示。例如,大号、粗体字或人脸通常比背景更受关注。可以猜想这些热图对设计师和广告商会有多有用,他们可以根据最大化用户注意力来调整内容。受到这种人类显著性版本的启发,了解神经网络关注图像的哪个部分会很有趣,这正是我们将要进行的实验。
在我们的实验中,我们将在视频上叠加一个类激活图(或俗称的热图),以便了解网络关注的内容。热图告诉我们类似于“在这张图片中,这些像素负责预测类别dog,其中dog是概率最高的类别。 “热”像素用红色、橙色和黄色等暖色表示,而“冷”像素则用蓝色表示。像素越“热”,提供的信号越高,指向预测的方向。图 2-6 给我们一个更清晰的图像。(如果您正在阅读印刷版本,请参考书的 GitHub 获取原始彩色图片。)

图 2-6. 狗的原始图像及其生成的热图
在 GitHub 存储库(参见http://PracticalDeepLearning.ai),导航至code/chapter-2。在那里,您会找到一个方便的 Jupyter 笔记本,2-class-activation-map-on-video.ipynb,描述以下步骤:
首先,我们需要使用pip安装keras-vis:
$ pip install keras-vis --user
然后,我们在单个图像上运行可视化脚本,生成其热图:
$ python visualization.py --process image --path ../sample-images/dog.jpg
我们应该看到一个名为dog-output.jpg的新创建的文件,显示原始图像及其热图的并排视图。正如我们从图 2-6 中看到的,图像的右半部分指示了“热区”,以及对“Cardigan”(即威尔士柯基)的正确预测。
接下来,我们想要可视化视频中帧的热图。为此,我们需要FFmpeg,这是一个开源的多媒体框架。您可以在https://www.ffmpeg.org找到下载二进制文件以及您操作系统的安装说明。
我们使用ffmpeg将视频拆分为单独的帧(每秒 25 帧),然后在每个帧上运行我们的可视化脚本。我们必须首先创建一个目录来存储这些帧,并将其名称作为ffmpeg命令的一部分传递:
$ mkdir kitchen
$ ffmpeg -i video/kitchen-input.mov -vf fps=25 kitchen/thumb%04d.jpg -hide_banner
然后我们使用包含上一步帧的目录路径运行可视化脚本:
$ python visualization.py --process video --path kitchen/
我们应该看到一个新创建的kitchen-output目录,其中包含来自输入目录的所有帧的热图。
最后,使用ffmpeg从这些帧编译一个视频:
$ ffmpeg -framerate 25 -i kitchen-output/result-%04d.jpg kitchen-output.mp4
完美!结果是原始视频与覆盖在其上的热图的副本并排显示。这是一个有用的工具,特别是用来发现模型是否学习了正确的特征,或者在训练过程中是否捕捉到了杂散的人工制品。
想象一下生成热图来分析我们训练模型或预训练模型的优点和不足。
您可以通过使用智能手机摄像头拍摄视频并在文件上运行上述脚本来自己尝试这个实验。别忘了在 Twitter 上发布您的视频,标记@PracticalDLBook!
提示
热图是一种在数据中可视化检测偏见的好方法。模型预测的质量严重依赖于其训练的数据。如果数据存在偏见,那将反映在预测中。一个很好的例子是(尽管可能是一个都市传说),美国军方想要使用神经网络来检测伪装在树木中的敌方坦克。¹ 构建模型的研究人员拍摄了照片——50%包含伪装的坦克,50%只有树木。模型训练得到了 100%的准确率。值得庆祝吗?遗憾的是,当美国军方进行测试时情况并非如此。该模型表现得非常糟糕——不比随机猜测好。调查发现,带有坦克的照片是在多云(阴天)拍摄的,而没有坦克的照片是在晴朗的天气拍摄的。神经网络模型开始寻找天空而不是坦克。如果研究人员使用热图来可视化模型,他们会很早就发现这个问题。
在收集数据时,我们必须警惕潜在的偏见,这可能会影响我们模型的学习。例如,当收集图像来构建食物分类器时,我们应该验证其他人工制品(如盘子和餐具)是否被学习为食物。否则,筷子的存在可能会导致我们的食物被分类为炒面。另一个术语来定义这个问题是共现性。食物经常与餐具共同出现。因此要注意这些人工制品是否渗入到分类器的训练中。
总结
在本章中,我们通过 Keras 对深度学习宇宙有了一瞥。这是一个易于使用但功能强大的框架,我们将在接下来的几章中使用。我们观察到通常不需要收集数百万张图片并使用强大的 GPU 来训练自定义模型,因为我们可以使用预训练模型来预测图像的类别。通过深入研究像 ImageNet 这样的数据集,我们了解了这些预训练模型可以预测的类别。我们还了解到在大多数框架中存在的模型动物园中可以找到这些模型。
在第三章中,我们探讨了如何调整现有的预训练模型,以便对其原始意图之外的输入类别进行预测。与当前章节一样,我们的方法旨在获得输出,而无需数百万张图片和大量硬件资源来训练分类器。
¹ “人工智能作为全球风险中的积极和消极因素” 作者 Eliezer Yudkowsky 在《全球灾难性风险》(牛津大学出版社)中。
第三章:猫与狗:使用 Keras 中的 30 行进行迁移学习
想象一下,我们想学习如何演奏口琴,这是一种手持键盘形式的吹奏乐器。如果没有音乐背景,口琴是我们的第一件乐器,可能需要我们几个月的时间才能熟练演奏。相比之下,如果我们已经擅长演奏另一种乐器,比如钢琴,可能只需要几天的时间,因为这两种乐器非常相似。将一个任务的经验应用到另一个类似任务上进行微调是我们在现实生活中经常做的事情(如图 3-1 所示)。两个任务越相似,将一个任务的经验应用到另一个任务上就越容易。
我们可以将现实生活中的这种现象应用到深度学习的世界中。使用预训练模型开始一个深度学习项目可能会相对快速,因为它重新利用了在训练过程中学到的知识,并将其适应到手头的任务中。这个过程被称为迁移学习。
在这一章中,我们使用迁移学习来通过在几分钟内使用 Keras 训练我们自己的分类器来修改现有模型。到本章结束时,我们将拥有几种工具来创建任何任务的高准确度图像分类器。

图 3-1。现实生活中的迁移学习
将预训练模型适应新任务
在讨论迁移学习的过程之前,让我们快速回顾一下深度学习蓬勃发展的主要原因:
-
像 ImageNet 这样更大更高质量的数据集的可用性
-
更好的计算资源可用;即更快速和更便宜的 GPU
-
更好的算法(模型架构、优化器和训练过程)
-
可重复使用的预训练模型,它们经过数月的训练,但可以快速重复使用
最后一点可能是普及深度学习的最重要原因之一。如果每个训练任务都需要一个月的时间,只有少数资金雄厚的研究人员才会在这个领域工作。由于迁移学习,训练模型的被低估的英雄,我们现在可以在几分钟内修改现有模型以适应我们的任务。
例如,我们在第二章中看到,预训练的 ResNet-50 模型,它在 ImageNet 上训练,可以预测猫和狗的品种,以及其他成千上万个类别。因此,如果我们只想在高级别的“猫”和“狗”类别之间进行分类(而不是低级别的品种),我们可以从 ResNet-50 模型开始,快速重新训练此模型以分类猫和狗。我们只需要在训练期间向其展示包含这两个类别的数据集,这应该需要几分钟到几小时不等。相比之下,如果我们不使用预训练模型来训练猫与狗的模型,可能需要几个小时到几天的时间。
对卷积神经网络的浅层探讨
我们一直使用术语“模型”来指代 AI 中用于做出预测的部分。在计算机视觉的深度学习中,该模型通常是一种称为 CNN 的特殊类型的神经网络。尽管我们稍后会更详细地探讨 CNN,但在这里我们简要地看一下如何通过迁移学习训练它们。
在机器学习中,我们需要将数据转换为一组可识别的特征,然后添加一个分类算法对它们进行分类。CNN 也是如此。它们由两部分组成:卷积层和全连接层。卷积层的工作是将图像的大量像素转换为一个更小的表示;即特征。全连接层将这些特征转换为概率。全连接层实际上是一个具有隐藏层的神经网络,正如我们在第一章中看到的那样。总之,卷积层充当特征提取器,而全连接层充当分类器。图 3-2 显示了 CNN 的高级概述。

图 3-2. CNN 的高级概述
想象一下,我们想要检测一个人脸。我们可能想要使用 CNN 对图像进行分类,并确定其中是否包含人脸。这样的 CNN 由几个层连接在一起组成。这些层代表数学运算。一个层的输出是下一个层的输入。第一个(或最底层)是输入层,输入图像被馈送到这里。最后一个(或最顶层)是输出层,给出预测。
它的工作方式是将图像馈送到 CNN 中,并通过一系列层,每个层执行数学运算并将结果传递给下一个层。最终的输出是一个对象类别列表及其概率。例如,类别如球—65%,草—20%,等等。如果图像的输出包含一个“人脸”类别,概率为 70%,我们可以得出结论,图像中包含人脸的可能性为 70%。
注意
看待 CNN 的一种直观(和过于简化的)方式是将它们视为一系列滤波器。正如“滤波器”一词所暗示的,每个层都充当信息的筛子,只有在识别到信息时才“通过”。(如果你听说过电子学中的高通和低通滤波器,这可能会很熟悉。)我们说该层对该信息“激活”。每个层对类似猫、狗、汽车等部分的视觉模式被激活。如果一个层没有识别信息(由于训练时学到的内容),其输出接近于零。CNN 是深度学习世界的“保安”!
在人脸检测示例中,较低级别的层(图 3-3 a; 靠近输入图像的层)被“激活”以获取更简单的形状;例如,边缘和曲线。因为这些层仅对基本形状激活,所以它们可以很容易地被重新用于不同于人脸识别的目的,比如检测汽车(毕竟每个图像都由边缘和曲线组成)。中级别的层(图 3-3 b)被激活以获取更复杂的形状,比如眼睛、鼻子和嘴唇。这些层不像较低级别的层那样容易被重复使用。它们可能不太适用于检测汽车,但可能仍然适用于检测动物。更高级别的层(图 3-3 c)被激活以获取更复杂的形状,例如大部分人脸。这些层往往更具任务特定性,因此在其他图像分类问题中最不可重复使用。
(a)较低级别的激活,接着是(b)中级别的激活和(c)上层的激活(图片来源:Lee 等人的《用于可扩展无监督学习的分层表示的卷积深度信念网络》,ICML 2009)
图 3-3. (a) 低层激活,接着是(b) 中层激活和(c) 上层激活(图片来源:Convolutional Deep Belief Networks for Scalable Unsupervised Learning of Hierarchical Representations, Lee et al., ICML 2009)
随着我们接近最后的层,一层可以识别的复杂性和能力增加。相反,随着我们接近输出,一层的可重用性减少。当我们看到这些层学习的内容时,这很快就会变得明显。
迁移学习
如果我们想要从一个模型转移知识到另一个模型,我们希望重复使用更多通用层(靠近输入)和更少任务特定层(靠近输出)。换句话说,我们想要移除最后几层(通常是全连接层),以便我们可以利用更通用的层,并添加针对我们特定分类任务的层。一旦训练开始,通用层(构成我们新模型的大部分)将保持冻结(即,它们是不可修改的),而新添加的任务特定层将被允许修改。这就是迁移学习如何帮助快速训练新模型的方式。图 3-4 说明了这个过程,即针对任务 X 训练的预训练模型如何适应任务 Y。

图 3-4. 迁移学习概述
微调
基本的迁移学习只能带我们走这么远。我们通常在通用层之后只添加两到三个全连接层来构建新的分类器模型。如果我们想要更高的准确性,我们必须允许更多的层被训练。这意味着解冻一些在迁移学习中本来会被冻结的层。这被称为微调。图 3-5 展示了一个例子,其中一些接近头部/顶部的卷积层被解冻并针对手头的任务进行训练。

图 3-5. 微调卷积神经网络
显然,与基本的迁移学习相比,在微调过程中会调整更多的层到我们的数据集中。因为与迁移学习相比,更多的层已经适应了我们的任务,我们可以为我们的任务实现更高的准确性。微调多少层的决定取决于手头的数据量以及目标任务与预训练模型训练的原始数据集的相似性。
我们经常听到数据科学家说,“我微调了模型”,这意味着他们拿了一个预训练模型,移除了任务特定层并添加了新的层,冻结了较低层,然后在新数据集上训练网络的上部分。
注意
在日常用语中,迁移学习和微调是可以互换使用的。在口语中,迁移学习更多地被用作一个概念,而微调则被称为其实施。
微调多少
我们应该微调卷积神经网络的多少层?这可以由以下两个因素来指导:
我们有多少数据?
如果我们有几百张标记的图像,从头开始训练和测试一个全新定义的模型(即,定义一个具有随机种子权重的模型架构)将会很困难,因为我们需要更多的数据。用这么少的数据进行训练的危险是这些强大的网络可能会潜在地记住它,导致不良的过拟合(我们将在本章后面探讨)。相反,我们将借用一个预训练的网络并微调最后几层。但如果我们有一百万张标记的图像,微调网络的所有层是可行的,如果必要,可以从头开始训练。因此,任务特定数据的数量决定了我们是否可以微调,以及微调多少。
数据有多相似?
如果任务特定数据与预训练网络使用的数据相似,我们可以调整最后几层。但是,如果我们的任务是在 X 射线图像中识别不同的骨骼,并且我们想要从 ImageNet 训练的网络开始,那么常规 ImageNet 图像和 X 射线图像之间的高差异将要求几乎所有层都进行训练。
总之,表 3-1 提供了一个易于遵循的备忘单。
表 3-1。调整的时间和方式的备忘单
| 数据集之间相似度高 | 数据集之间相似度低 | |
|---|---|---|
| 大量训练数据 | 调整所有层 | 从头开始训练,或者调整所有层 |
| 少量训练数据 | 调整最后几层 | 运气不佳!使用较小的网络进行训练,进行大量数据增强,或以某种方式获取更多数据 |
足够的理论,让我们看看实际操作。
在 Keras 中使用迁移学习构建自定义分类器
正如承诺的那样,现在是用 30 行或更少行构建我们的最先进分类器的时候了。在高层次上,我们将使用以下步骤:
-
组织数据。下载标记的猫和狗的图像,然后将图像分成训练和验证文件夹。
-
构建数据管道。定义一个用于读取数据的管道,包括对图像进行预处理(例如,调整大小)和将多个图像组合成批次。
-
增加数据。在缺少大量训练图像的情况下,进行小的更改(增强),如旋转、缩放等,以增加训练数据的变化。
-
定义模型。采用预训练模型,删除最后几个任务特定层,并附加一个新的分类器层。冻结原始层的权重(即,使它们不可修改)。选择一个优化算法和一个要跟踪的指标(如准确性)。
-
训练和测试。训练几次迭代,直到我们的验证准确率很高。保存模型,以便最终加载到任何应用程序中进行预测。
这一切很快就会变得清晰起来。让我们详细探讨这个过程。
组织数据
理解训练、验证和测试数据之间的区别至关重要。让我们看一个学生为标准化考试(例如美国的 SAT、中国的高考、印度的 JEE、韩国的 CSAT 等)做准备的现实类比。课堂教学和家庭作业类比于训练过程。学校中的小测验、期中考试和其他测试相当于验证,学生可以经常参加这些测试,评估表现,并在学习计划中做出改进。他们最终是为了在只有一次机会的最终标准化考试中表现最佳。期末考试相当于测试集,学生在这里没有机会改进(忽略重考的能力)。这是他们展示所学内容的唯一机会。
同样,我们的目标是在现实世界中提供最佳预测。为此,我们将数据分为三部分:训练、验证和测试。典型的分布为 80%用于训练,10%用于验证,10%用于测试。请注意,我们随机将数据分成这三组,以确保可能潜入的最少偏见。模型的最终准确性由测试集上的准确性确定,就像学生的分数仅由他们在标准化考试中的表现确定一样。
模型从训练数据中学习,并使用验证集来评估其性能。机器学习从业者将这种性能作为反馈,以找到持续改进模型的机会,类似于学生如何通过小测验改进他们的准备工作。我们可以调整几个旋钮来提高性能;例如,要训练的层数。
在许多研究竞赛中(包括Kaggle.com),参赛者会收到一个与用于构建模型的数据分开的测试集。这确保了在报告准确性时竞赛中的一致性。参赛者需要将可用数据划分为训练和验证集。同样,在本书的实验中,我们将继续将数据划分为这两个集合,记住测试数据集仍然是报告现实世界数字的必要条件。
那么为什么要使用验证集呢?有时数据很难获取,为什么不使用所有可用样本进行训练,然后在其上报告准确性呢?当模型开始学习时,它将逐渐在训练数据集上给出更高准确性的预测(称为训练准确性)。但由于它们非常强大,深度神经网络有可能记住训练数据,有时甚至在训练数据上达到 100%的准确性。然而,其在现实世界中的表现将非常糟糕。这就好像学生在考试之前就知道会出现的问题一样。这就是为什么验证集,不用于训练模型,可以给出模型性能的真实评估。即使我们可能将 10-15%的数据分配为验证集,它将在很大程度上指导我们了解我们的模型到底有多好。
对于训练过程,我们需要将数据集存储在正确的文件夹结构中。我们将图像分为两组:训练和验证。对于图像文件,Keras 将根据其父文件夹名称自动分配类(类别)的名称。图 3-6 展示了重新创建的理想结构。

图 3-6. 不同类别的训练和验证数据的示例目录结构
以下一系列命令可以帮助下载数据并实现这个目录结构:
$ wget https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/
download/train.zip
$ unzip train.zip
$ mv train data
$ cd data
$ mkdir train val
$ mkdir train/cat train/dog
$ mkdir val/cat val/dog
数据文件夹中的 25,000 个文件以“cat”和“dog”为前缀。现在,将文件移动到它们各自的目录中。为了保持我们最初的实验简短,我们每类选择 250 个随机文件,并将它们放入训练和验证文件夹中。我们可以随时增加/减少这个数字,以尝试在准确性和速度之间取得平衡:
$ ls | grep cat | sort -R | head -250 | xargs -I {} mv {} train/cat/
$ ls | grep dog | sort -R | head -250 | xargs -I {} mv {} train/dog/
$ ls | grep cat | sort -R | head -250 | xargs -I {} mv {} val/cat/
$ ls | grep dog | sort -R | head -250 | xargs -I {} mv {} val/dog/
构建数据管道
要开始我们的 Python 程序,我们首先导入必要的包:
import tensorflow as tf
from tf.keras.preprocessing.image import ImageDataGenerator
from tf.keras.models import Model
from tf.keras.layers import Input, Flatten, Dense, Dropout,
GlobalAveragePooling2D
from tf.keras.applications.mobilenet import MobileNet, preprocess_input
import math
将以下配置行放在导入语句之后,我们可以根据我们的数据集进行修改:
TRAIN_DATA_DIR = 'data/train_data/'
VALIDATION_DATA_DIR = 'data/val_data/'
TRAIN_SAMPLES = 500
VALIDATION_SAMPLES = 500
NUM_CLASSES = 2
IMG_WIDTH, IMG_HEIGHT = 224, 224
BATCH_SIZE = 64
类别数
有两个类别需要区分,我们可以将这个问题视为以下之一:
-
一个二元分类任务
-
多类分类任务
二元分类
作为二元分类任务,重要的是要注意,“猫与狗”实际上是“猫与非猫”。狗会被分类为“非猫”,就像桌子或球一样。对于给定的图像,模型将给出一个与“cat”类对应的单个概率值,因此“非猫”的概率为 1 - P(cat)。如果概率高于 0.5,我们将预测为“cat”;否则为“非猫”。为了简化问题,我们假设测试集中只包含猫或狗的图像。因为“猫与非猫”是一个二元分类任务,我们将类别数设置为 1;即“cat”。任何无法分类为“cat”的内容将被分类为“非猫”。
提示
Keras 按照文件夹名称的字母顺序处理输入数据。因为按字母顺序,“cat”在“dog”之前,我们的第一个预测类别是“cat”。对于多类任务,我们可以应用相同的概念,并根据文件夹排序顺序推断每个类别标识符(索引)。请注意,类别索引从第一个类别开始为 0。
多类分类
在一个假设的世界中,只有猫和狗,没有其他东西,一个“非猫”总是狗。因此,“非猫”标签可以简单地替换为“狗”标签。然而,在现实世界中,我们有超过两种类型的物体。如前所述,球或沙发也会被分类为“狗”,这是不正确的。因此,在真实场景中,将其视为多类别分类任务而不是二元分类任务要更有用。作为多类别分类任务,我们为每个类别预测单独的概率值,最高的概率值是我们的赢家。在“猫与狗”案例中,我们将类别数设置为两。为了使我们的代码可重用于未来的任务,我们将把这视为多类别任务。
批量大小
在高层次上,训练过程包括以下步骤:
-
对图像进行预测(前向传递)。
-
确定哪些预测是错误的,并将预测与真实值之间的差异传播回去(反向传播)。
-
反复进行,直到预测变得足够准确。
初始迭代很可能准确率接近 0%。然而,重复这个过程几次可能会产生一个高度准确的模型(>90%)。
批量大小定义了模型一次看到多少张图片。重要的是,每个批次中有来自不同类别的各种图片,以防止在迭代之间的准确度指标出现大幅波动。为此,需要一个足够大的批量大小。然而,重要的是不要设置批量大小过大;太大的批量可能无法适应 GPU 内存,导致“内存不足”崩溃。通常,批量大小设置为 2 的幂。对于大多数问题,一个好的起点是 64,我们可以通过增加或减少数量来调整。
数据增强
通常,当我们听到深度学习时,我们会将其与数百万张图片联系在一起。因此,我们拥有的 500 张图片可能对于真实世界的训练来说是一个较低的数字。虽然这些深度神经网络非常强大,但对于少量数据来说可能过于强大,训练图像集数量有限的危险在于神经网络可能会记住我们的训练数据,并在训练集上表现出色,但在验证集上准确度较低。换句话说,模型已经过度训练,无法泛化到以前未见过的图像。我们绝对不希望出现这种情况。
提示
通常,当我们尝试在少量数据上训练神经网络时,结果是模型在训练数据上表现非常好,但在之前未见过的数据上做出相当糟糕的预测。这样的模型将被描述为过度拟合模型,问题本身被称为过度拟合。
图 3-7 说明了这种现象,即接近正弦曲线的点的分布(几乎没有噪音)。点代表我们的网络可见的训练数据,叉代表在训练期间未见过的测试数据。在一个极端情况下(欠拟合),一个简单的模型,如线性预测器,将无法很好地表示基础分布,导致训练数据和测试数据上的高错误率。在另一个极端情况下(过拟合),一个强大的模型(如深度神经网络)可能有能力记住训练数据,这将导致训练数据上的错误率非常低,但在测试数据上仍然有很高的错误率。我们希望的是在训练错误和测试错误都相对较低的愉快中间位置,这理想情况下确保我们的模型在真实世界中的表现与训练期间一样好。

图 3-7。欠拟合、过拟合和理想拟合对于接近正弦曲线的点
伟大的力量伴随着伟大的责任。我们有责任确保我们强大的深度神经网络不会在我们的数据上过拟合。当我们有少量训练数据时,过拟合是常见的。我们可以通过几种不同的方式减少这种可能性:
-
以某种方式获取更多数据
-
大幅度增强现有数据
-
微调更少的层
通常存在数据不足的情况。也许我们正在处理一个小众问题,数据很难获得。但我们可以通过一些方式人为地增加我们的分类数据集:
旋转
在我们的例子中,我们可能希望随机将这 500 张图像旋转 20 度,向任一方向,从而产生多达 20,000 个可能的独特图像。
随机移动
将图像稍微向左或向右移动。
缩放
稍微放大或缩小图像。
通过结合旋转、移动和缩放,程序可以生成几乎无限数量的独特图像。这一重要步骤称为数据增强。数据增强不仅有助于添加更多数据,还有助于为真实场景训练更健壮的模型。例如,并非所有图像都将猫正确地放在中间或以完美的 0 度角。Keras 提供了ImageDataGenerator函数,该函数在从目录加载数据时增强数据。为了说明图像增强的效果,图 3-8 展示了由imgaug库为一个示例图像生成的增强示例。(请注意,我们实际训练时不会使用 imgaug。)

图 3-8。从单个图像生成的可能的图像增强
彩色图像通常有三个通道:红色、绿色和蓝色。每个通道的强度值范围从 0 到 255。为了将其归一化(即将值缩小到 0 到 1 之间),我们使用preprocess_input函数(其中,除了其他操作外,还将每个像素除以 255):
train_datagen = ImageDataGenerator(preprocessing_function=preprocess_input,
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
zoom_range=0.2)
val_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)
提示
有时,知道训练图像的标签可以帮助确定适当的增强方式。例如,在训练数字识别器时,您可能会同意对数字“8”的图像进行垂直翻转增强,但不适用于“6”和“9”。
与我们的训练集不同,我们不希望增强我们的验证集。原因是,使用动态增强,验证集将在每次迭代中保持变化,导致的准确度指标将不一致且难以在其他迭代中进行比较。
现在是从目录加载数据的时候了。一次训练一张图像可能效率不高,所以我们可以将它们分成组。为了在训练过程中引入更多的随机性,我们将在每个批次中保持图像的随机顺序。为了在同一程序的多次运行中保持可重现性,我们将为随机数生成器提供一个种子值:
train_generator = train_datagen.flow_from_directory(
TRAIN_DATA_DIR,
target_size=(IMG_WIDTH, IMG_HEIGHT),
batch_size=BATCH_SIZE,
shuffle=True,
seed=12345,
class_mode='categorical')
validation_generator = val_datagen.flow_from_directory(
VALIDATION_DATA_DIR,
target_size=(IMG_WIDTH, IMG_HEIGHT),
batch_size=BATCH_SIZE,
shuffle=False,
class_mode='categorical')
模型定义
现在数据已经处理好了,我们来到我们训练过程中最关键的组件:模型。在接下来的代码中,我们重复使用了之前在 ImageNet 数据集上训练过的 CNN(在我们的案例中是 MobileNet),丢弃了最后几层,称为全连接层(即 ImageNet 特定的分类器层),并用适合当前任务的自己的分类器替换它们。
对于迁移学习,我们“冻结”原始模型的权重;也就是说,将这些层设置为不可修改,因此只有新分类器的层(我们将添加的)可以被修改。我们在这里使用 MobileNet 来保持速度,但这种方法对于任何神经网络都同样有效。以下几行包括一些术语,如Dense、Dropout等。虽然我们在本章不会探讨它们,但你可以在附录 A 中找到解释。
def model_maker():
base_model = MobileNet(include_top=False, input_shape =
(IMG_WIDTH,IMG_HEIGHT,3))
for layer in base_model.layers[:]:
layer.trainable = False # Freeze the layers
input = Input(shape=(IMG_WIDTH, IMG_HEIGHT, 3))
custom_model = base_model(input)
custom_model = GlobalAveragePooling2D()(custom_model)
custom_model = Dense(64, activation='relu')(custom_model)
custom_model = Dropout(0.5)(custom_model)
predictions = Dense(NUM_CLASSES, activation='softmax')(custom_model)
return Model(inputs=input, outputs=predictions)
训练模型
设置训练参数
数据和模型都准备好了,我们唯一需要做的就是训练模型。这也被称为将模型拟合到数据。为了训练一个模型,我们需要选择和修改一些不同的训练参数。
损失函数
loss函数是我们在训练过程中对模型进行惩罚的方式。我们希望最小化这个函数的值。例如,在预测房价的任务中,loss函数可以是均方根误差。
优化器
这是一个帮助最小化loss函数的算法。我们使用 Adam,这是目前最快的优化器之一。
学习率
学习是渐进的。学习率告诉优化器向解决方案迈出多大的步伐;换句话说,最小化损失的位置。迈出太大的步伐,我们最终会过度摆动并超过目标。迈出太小的步伐,可能需要很长时间才能最终到达目标损失值。设置一个最佳学习率是很重要的,以确保我们在合理的时间内达到学习目标。在我们的例子中,我们将学习率设置为 0.001。
度量
选择一个度量标准来评估训练模型的性能。准确率是一个很好的可解释度量标准,特别是当类别不平衡时(即每个类别的数据量大致相等)。请注意,这个度量标准与loss函数无关,主要用于报告,而不是作为模型的反馈。
在下面的代码片段中,我们使用之前编写的model_maker函数创建自定义模型。我们使用这里描述的参数进一步定制这个模型,以适应我们的猫狗任务:
model = model_maker()
model.compile(loss='categorical_crossentropy',
optimizer= tf.train.Adam(lr=0.001),
metrics=['acc'])
num_steps = math.ceil(float(TRAIN_SAMPLES)/BATCH_SIZE)
model.fit_generator(train_generator,
steps_per_epoch = num_steps,
epochs=10,
validation_data = validation_generator,
validation_steps = num_steps)
注意
你可能已经注意到了前面代码中的时代这个术语。一个时代代表了一个完整的训练步骤,网络已经遍历整个数据集。一个时代可能包含几个小批次。
开始训练
运行这个程序,让魔法开始吧。如果你没有 GPU,在等待时可以煮杯咖啡——可能需要 5 到 10 分钟。或者为什么要等待,当你可以在 Colab 上免费使用 GPU 运行本章的笔记本时呢?
完成后,请注意有四个统计数据:训练数据和验证数据上的loss和acc。我们期待val_acc:
> Epoch 1/100 7/7 [====] - 5s -
loss: 0.6888 - acc: 0.6756 - val_loss: 0.2786 - val_acc: 0.9018
> Epoch 2/100 7/7 [====] - 5s -
loss: 0.2915 - acc: 0.9019 - val_loss: 0.2022 - val_acc: 0.9220
> Epoch 3/100 7/7 [====] - 4s -
loss: 0.1851 - acc: 0.9158 - val_loss: 0.1356 - val_acc: 0.9427
> Epoch 4/100 7/7 [====] - 4s -
loss: 0.1509 - acc: 0.9341 - val_loss: 0.1451 - val_acc: 0.9404
> Epoch 5/100 7/7 [====] - 4s -
loss: 0.1455 - acc: 0.9464 - val_loss: 0.1637 - val_acc: 0.9381
> Epoch 6/100 7/7 [====] - 4s -
loss: 0.1366 - acc: 0.9431 - val_loss: 0.2319 - val_acc: 0.9151
> Epoch 7/100 7/7 [====] - 4s -
loss: 0.0983 - acc: 0.9606 - val_loss: 0.1420 - val_acc: 0.9495
> Epoch 8/100 7/7 [====] - 4s -
loss: 0.0841 - acc: 0.9731 - val_loss: 0.1423 - val_acc: 0.9518
> Epoch 9/100 7/7 [====] - 4s -
loss: 0.0714 - acc: 0.9839 - val_loss: 0.1564 - val_acc: 0.9509
> Epoch 10/100 7/7 [====] - 5s -
loss: 0.0848 - acc: 0.9677 - val_loss: 0.0882 - val_acc: 0.9702
在第一个时代仅用了 5 秒就在验证集上达到了 90%的准确率,仅用了 500 张训练图片。不错!到第 10 步时,我们观察到约 97%的验证准确率。这就是迁移学习的力量。
让我们花点时间欣赏这里发生的事情。仅仅用 500 张图片,我们就能在几秒钟内达到高水平的准确性,而且代码量很少。相比之下,如果我们没有一个在 ImageNet 上预先训练过的模型,要获得一个准确的模型可能需要几个小时到几天的训练时间,以及更多的数据。
这就是我们需要在任何问题上训练一流分类器的所有代码。将数据放入以类名命名的文件夹中,并更改配置变量中的相应值。如果我们的任务有两个以上的类别,我们应该使用categorical_crossentropy作为loss函数,并将最后一层的activation函数替换为softmax。表 3-2 说明了这一点。
表 3-2. 根据任务决定损失和激活类型
| 分类类型 | 类模式 | 损失 | 最后一层的激活 |
|---|---|---|---|
| 1 或 2 个类 | 二进制 | 二元交叉熵 | sigmoid |
| 多类别,单标签 | 分类 | 分类交叉熵 | softmax |
| 多类别,多标签 | 分类 | 二元交叉熵 | sigmoid |
在我们忘记之前,保存刚刚训练的模型,以便稍后使用:
model.save('model.h5')
测试模型
现在我们有了一个经过训练的模型,我们可能最终想要稍后在我们的应用程序中使用它。我们现在可以随时加载这个模型并对图像进行分类。load_model,顾名思义,加载模型:
from tf.keras.models import load_model
model = load_model('model.h5')
现在让我们尝试加载我们的原始样本图像,看看我们得到什么结果:
img_path = '../../sample_images/dog.jpg'
img = image.load_img(img_path, target_size=(224,224))
img_array = image.img_to_array(img)
expanded_img_array = np.expand_dims(img_array, axis=0)
preprocessed_img = preprocess_input(expanded_img_array) # Preprocess the image
prediction = model.predict(preprocessed_img)
print(prediction)
print(validation_generator.class_indices)
[[0.9967706]]
{'dog': 1, 'cat': 0}
打印概率值,我们看到它是 0.996。这是给定图像属于类别“1”(狗)的概率。因为概率大于 0.5,所以图像被预测为狗。
这就是我们训练自己分类器所需要的全部内容。在本书中,您可以期望在进行最少修改的情况下重复使用此代码进行训练。您也可以在自己的项目中重复使用此代码。尝试调整时代和图像的数量,并观察它如何影响准确性。此外,我们应该尝试使用我们可以在网上找到的任何其他数据。没有比这更容易的了!
分析结果
通过我们训练过的模型,我们可以分析它在验证数据集上的表现。除了更直接的准确性指标之外,查看误判的实际图像应该能让我们直观地了解这个例子是否真正具有挑战性,或者我们的模型还不够复杂。
有三个问题我们想要为每个类别(猫、狗)回答:
-
哪些图像我们最有信心是猫/狗?
-
哪些图像我们最不确定是猫/狗?
-
哪些图像尽管非常自信,但预测错误?
在进行这之前,让我们对整个验证数据集进行预测。首先,我们正确设置管道配置:
# VARIABLES
IMG_WIDTH, IMG_HEIGHT = 224, 224
VALIDATION_DATA_DIR = 'data/val_data/'
VALIDATION_BATCH_SIZE = 64
# DATA GENERATORS
validation_datagen = ImageDataGenerator(
preprocessing_function=preprocess_input)
validation_generator = validation_datagen.flow_from_directory(
VALIDATION_DATA_DIR,
target_size=(IMG_WIDTH, IMG_HEIGHT),
batch_size=VALIDATION_BATCH_SIZE,
shuffle=False,
class_mode='categorical')
ground_truth = validation_generator.classes
然后,我们进行预测:
predictions = model.predict_generator(validation_generator)
为了使我们的分析更容易,我们创建一个字典,存储每个图像的索引到预测和实际值(预期预测):
# prediction_table is a dict with index, prediction, ground truth
prediction_table = {}
for index, val in enumerate(predictions):
# get argmax index
index_of_highest_probability = np.argmax(val)
value_of_highest_probability = val[index_of_highest_probability]
prediction_table[index] = [value_of_highest_probability,
index_of_highest_probability, ground_truth[index]]
assert len(predictions) == len(ground_truth) == len(prediction_table)
对于接下来的两个代码块,我们提供了常用的样板代码,我们在整本书中经常重复使用。
以下是我们将使用的辅助函数的签名,用于查找具有给定类别的最高/最低概率值的图像。此外,我们将使用另一个辅助函数,- display(),将图像以网格形式输出到屏幕上:
def display(sorted_indices, message):
similar_image_paths = []
distances = []
for name, value in sorted_indices:
[probability, predicted_index, gt] = value
similar_image_paths.append(VALIDATION_DATA_DIR + fnames[name])
distances.append(probability)
plot_images(similar_image_paths, distances, message)
此函数在本书的 Github 网站上定义(请参阅http://PracticalDeepLearning.ai,位于code/chapter-3)。
现在开始有趣的部分!哪些图像我们最有信心包含狗?让我们找到预测概率最高的图像(即最接近 1.0;参见图 3-9 中概率最高的图像)与预测类别狗(即 1):
# Most confident predictions of 'dog'
indices = get_images_with_sorted_probabilities(prediction_table,
get_highest_probability=True, label=1, number_of_items=10,
only_false_predictions=False)
message = 'Images with the highest probability of containing dogs'
display(indices[:10], message)

图 3-9. 概率最高的包含狗的图像
这些图像确实非常像狗。概率如此之高的原因之一可能是图像中包含了多只狗,以及清晰、明确的视图。现在让我们尝试找出我们最不确定包含狗的图像(请参见图 3-10):
# Least confident predictions of 'dog'
indices = get_images_with_sorted_probabilities(prediction_table,
get_highest_probability=False, label=1, number_of_items=10,
only_false_predictions=False)
message = 'Images with the lowest probability of containing dogs'
display(indices[:10], message)

图 3-10. 概率最低的包含狗的图像
重申一下,这些是我们的分类器最不确定包含狗的图像。这些预测大多处于临界点(即 0.5 概率)成为主要预测。请记住,作为猫的概率只是略小一些,大约在 0.49 左右。与之前一组图像相比,这些图像中出现的动物通常更小,更不清晰。这些图像通常导致错误预测——只有 10 张图像中的 2 张被正确预测。在这里做得更好的一种可能方法是使用更大的图像集进行训练。
如果您担心这些错误分类,不用担心。提高分类准确性的一个简单技巧是对接受分类器结果的阈值设定更高,比如 0.75。如果分类器对图像类别不确定,其结果将被保留。在第五章中,我们将看看如何找到最佳阈值。
说到错误预测,当分类器信心较低时(即两类问题的概率接近 0.5 时),显然会出现错误预测。但我们不希望的是在我们的分类器对其预测非常确信时出现错误预测。让我们看看分类器确信包含狗的图像,尽管它们实际上是猫(参见图 3-11):
# Incorrect predictions of 'dog'
indices = get_images_with_sorted_probabilities(prediction_table,
get_highest_probability=True, label=1, number_of_items=10,
only_false_predictions=True)
message = 'Images of cats with the highest probability of containing dogs'
display(indices[:10], message)

图 3-11。具有最高概率包含狗的猫的图像
嗯...结果是这些图像中有一半包含猫和狗,我们的分类器正确地预测了狗类别,因为在这些图像中它们的大小更大。因此,这里不是分类器有问题,而是数据有问题。这在大型数据集中经常发生。另一半通常包含不清晰和相对较小的对象(但理想情况下,我们希望对这些难以识别的图像具有较低的置信度)。
对猫类重复相同一组问题,哪些图像更像猫(参见图 3-12)?
# Most confident predictions of 'cat'
indices = get_images_with_sorted_probabilities(prediction_table,
get_highest_probability=True, label=0, number_of_items=10,
only_false_predictions=False)
message = 'Images with the highest probability of containing cats'
display(indices[:10], message)

图 3-12。具有最高概率包含猫的图像
有趣的是,其中许多图像中有多只猫。这证实了我们之前的假设,即多个清晰、明确的猫的视图可以给出更高的概率。另一方面,哪些图像我们对包含猫最不确定(参见图 3-13)?
# Least confident predictions of 'cat'
indices = get_images_with_sorted_probabilities(prediction_table,
get_highest_probability=False, label=0, number_of_items=10,
only_false_predictions=False)
message = 'Images with the lowest probability of containing cats'
display(indices[:10], message)

图 3-13。具有最低概率包含猫的图像
正如之前所见,关键对象的大小较小,有些图像相当不清晰,这意味着在某些情况下对比度太高,或者对象太亮,这与大多数训练图像不符。例如,第八张(dog.6680)和第十张(dog.1625)图像中的相机闪光灯使得狗难以识别。第六张图像包含一只狗站在同色的沙发前。两张图像包含笼子。
最后,我们的分类器错误地确信包含猫的图像是哪些(参见图 3-14)?
# Incorrect predictions of 'cat'
indices = get_images_with_sorted_probabilities(prediction_table,
get_highest_probability=True, label=0, number_of_items=10,
only_false_predictions=True)
message = 'Images of dogs with the highest probability of containing cats'
display(indices[:10], message)

图 3-14。具有最高概率包含猫的狗的图像
这些错误预测是我们想要减少的。其中一些显然是错误的,而另一些则是令人困惑的图像。在图 3-14 中的第六张图(dog.4334)似乎被错误标记为狗。第七和第十张图在背景上难以区分。第一和第十张图内部缺乏足够的纹理,无法给分类器足够的识别能力。而一些狗太小,比如第二和第四张。
通过各种分析,我们可以总结出,错误预测可能是由于低照明、不清晰、难以区分的背景、缺乏纹理以及相对于图像的较小占用区域而引起的。
分析我们的预测是了解我们的模型学到了什么以及它擅长什么的好方法,并突出了增强其预测能力的机会。增加训练示例的大小和更强大的数据增强将有助于提高分类的准确性。还要注意,向我们的模型展示真实世界的图像(看起来类似于我们的应用程序将要使用的场景)将极大地提高其准确性。在第五章中,我们使分类器更加健壮。
进一步阅读
为了更好地理解神经网络和 CNN,我们的网站提供了一个学习指南,其中包括推荐的资源,如视频讲座、博客,以及更有趣的是,交互式可视化工具,让您可以在浏览器中玩不同的场景,而无需安装任何软件包。如果您是深度学习的初学者,我们强烈推荐这个指南,以加强您的基础知识。它涵盖了您需要建立直觉以解决未来问题的理论。我们使用 Google 的 TensorFlow Playground(图 3-15)进行神经网络和 Andrej Karpathy 的 ConvNetJS(图 3-16)进行 CNN。

图 3-15. 在 TensorFlow Playground 中构建神经网络

图 3-16. 定义 CNN 并在 ConvNetJS 中训练期间可视化每个层的输出
此外,我们在附录 A 中还有一个简短的指南,总结了卷积神经网络,作为一个方便的参考。
总结
在本章中,我们介绍了迁移学习的概念。我们重用了一个预训练模型,在不到 30 行代码和几乎 500 张图片的情况下构建了自己的猫狗分类器,在几分钟内达到了最先进的准确性。通过编写这段代码,我们也揭穿了一个神话,即我们需要数百万张图片和强大的 GPU 来训练我们的分类器(尽管它们有帮助)。
希望通过这些技能,您可能最终能够回答一个古老的问题,即谁放出了狗。
在接下来的几章中,我们将利用这些知识更深入地理解 CNN,并将模型准确性提升到更高水平。
第四章:构建反向图像搜索引擎:理解嵌入
鲍勃刚刚买了一套新房子,正在寻找一些时尚现代的家具来填充它。他不停地翻阅家具目录,参观家具展厅,但还没有找到自己喜欢的东西。然后有一天,他看到了他梦寐以求的沙发——一张独特的 L 形白色现代沙发在一个办公室接待处。好消息是他知道自己想要什么。坏消息是他不知道从哪里购买。沙发上没有写品牌和型号号码。询问办公室经理也没有帮助。所以,他从不同角度拍了几张照片,想在当地的家具店里打听,但运气不佳:没有人知道这个特定品牌。在互联网上使用“白色 L 形”、“现代沙发”等关键词搜索给他带来了成千上万的结果,但没有他在找的那个。
爱丽丝听到鲍勃的沮丧,问道:“为什么不试试反向图像搜索?”鲍勃将他的图像上传到谷歌和必应的反向图像搜索,很快在一个在线购物网站上发现了一张看起来相似的图像。从网站上找到这张更完美的图像后,他进行了更多的反向图像搜索,发现其他网站以更便宜的价格提供相同的沙发。在上网几分钟后,鲍勃正式订购了他梦寐以求的沙发!
反向图像搜索(或更专业地称为实例检索)使开发人员和研究人员能够构建超越简单关键字搜索的场景。从在 Pinterest 上发现视觉上相似的对象到在 Spotify 上推荐相似的歌曲,再到亚马逊基于相机的产品搜索,底层使用的是一类类似的技术。像 TinEye 这样的网站在摄影师的照片未经同意发布在互联网上时会发出侵权警告。甚至在几个安全系统中的人脸识别也使用类似的概念来确定人的身份。
最好的部分是,有了正确的知识,你可以在几个小时内构建许多这些产品的工作副本。所以让我们开始吧!
这是我们在本章中要做的事情:
-
在 Caltech101 和 Caltech256 数据集上执行特征提取和相似性搜索
-
学习如何扩展到大型数据集(多达数十亿张图像)
-
使系统更准确和优化
-
分析案例研究,看看这些概念在主流产品中如何使用
图像相似度
首要问题是:给定两幅图像,它们是否相似?
解决这个问题有几种方法。一种方法是比较两幅图像之间的区域块。尽管这可以帮助找到精确或接近精确的图像(可能已经被裁剪),但即使轻微旋转也会导致不相似。通过存储区块的哈希值,可以找到图像的重复。这种方法的一个用例是在照片中识别抄袭行为。
另一种天真的方法是计算 RGB 值的直方图并比较它们的相似性。这可能有助于找到在相同环境中拍摄的内容没有太多变化的近似图像。例如,在图 4-1 中,这种技术用于图像去重软件,旨在找到硬盘上的照片爆发,这样您就可以选择最好的照片并删除其余的照片。当数据集增长时,误报的可能性会增加。这种方法的另一个缺点是,对颜色、色调或白平衡进行小的更改会使识别变得更加困难。

图 4-1。基于 RGB 直方图的“相似图像检测器”程序
更加健壮的传统计算机视觉方法是利用诸如尺度不变特征变换(SIFT)、加速稳健特征(SURF)和定向 FAST 和旋转 BRIEF(ORB)等算法,在边缘附近找到视觉特征,然后比较两张照片之间共同的相似特征数量。这有助于从通用的图像级理解转向相对健壮的对象级理解。尽管这对于具有较少变化的刚性对象的图像非常有效,比如谷物盒的印刷侧几乎总是相同的,但对于比较可变形的对象如人类和动物则不太有帮助,因为它们可能展示不同的姿势。例如,您可以在亚马逊应用程序的基于相机的产品搜索体验中看到显示的特征。该应用程序以蓝色点的形式显示这些特征(图 4-2)。当它看到足够数量的特征时,它会将它们发送到亚马逊服务器以检索产品信息。

图 4-2. 亚马逊应用程序中带有视觉特征的产品扫描仪
更深入地探讨,另一种方法是使用深度学习找到图像的类别(例如,沙发),然后找到同一类别内的其他图像。这相当于从图像中提取元数据,以便随后可以对其进行索引并在基于典型文本查询的搜索中使用。通过在 ElasticSearch 等开源搜索引擎中使用元数据,可以轻松扩展此功能。许多电子商务网站在内部进行基于标签的查询搜索时,会根据从图像中提取的标签显示推荐内容。正如您所期望的那样,通过提取标签,我们会丢失某些信息,如颜色、姿势、场景中物体之间的关系等。此方法的一个主要缺点是需要大量标记数据来训练用于在新图像上提取这些标签的分类器。每次需要添加新类别时,都需要重新训练模型。
因为我们的目标是在数百万张图像中进行搜索,理想情况下,我们需要一种方法将图像中数百万像素中包含的信息总结为一个较小的表示(比如几千维),并且使得相似对象的总结表示彼此靠近,而不同对象之间则相距较远。
幸运的是,深度神经网络来拯救。正如我们在第二章和第三章中看到的,CNN 将图像输入转换为一千维的特征向量,然后将其作为分类器的输入,输出图像可能属于的顶级身份(比如狗或猫)。特征向量(也称为嵌入或瓶颈特征)本质上是几千个浮点值的集合。通过 CNN 中的卷积和池化层,基本上是一种减少操作,以过滤图像中包含的信息,提取最重要和显著的成分,这些成分形成瓶颈特征。训练 CNN 会以这种方式塑造这些值,使得属于同一类别的项目之间的欧氏距离很小(或者简单地说是对应值之间差的平方和的平方根),而不同类别的项目之间的距离较大。这是一个重要的特性,有助于解决许多无法使用分类器的问题,特别是在无监督问题中,因为缺乏足够的标记数据。
提示
找到相似图像的理想方法是使用迁移学习。例如,通过预训练的卷积神经网络(如 ResNet-50)传递图像,提取特征,然后使用度量来计算错误率,如欧几里德距离。
说了这么多,让我们写代码吧!
特征提取
一张图像胜过千言万语的特征。
在这一部分中,我们将主要使用 Caltech 101 数据集(131 MB,约 9,000 张图像)来玩耍和理解特征提取的概念,然后最终使用 Caltech 256(1.2 GB,约 30,000 张图像)。Caltech 101,顾名思义,包含大约 9,000 张图像,分为 101 个类别,每个类别大约有 40 到 800 张图像。需要注意的是,有一个第 102 个类别称为“BACKGROUND_Google”,其中包含随机图像,不包含在前 101 个类别中,需要在我们开始实验之前删除。请记住,我们编写的所有代码也可以在GitHub 存储库中找到。
让我们下载数据集:
$ wget
http://www.vision.caltech.edu/Image_Datasets/Caltech101/
101_ObjectCategories.tar.gz
$ tar -xvf 101_ObjectCategories.tar.gz
$ mv 101_ObjectCategories caltech101
$ rm -rf caltech101/BACKGROUND_Google
现在,导入所有必要的模块:
import numpy as np
from numpy.linalg import norm
import pickle
from tqdm import tqdm, tqdm_notebook
import os
import time
from tf.keras.preprocessing import image
from tf.keras.applications.resnet50 import ResNet50, preprocess_input
加载不带顶部分类层的 ResNet-50 模型,以便只获取瓶颈特征。然后定义一个函数,该函数接受图像路径,加载图像,将其调整为 ResNet-50 支持的适当尺寸,提取特征,然后对其进行归一化:
model = ResNet50(weights='imagenet', include_top=False,
input_shape=(224, 224, 3))
def extract_features(img_path, model):
input_shape = (224, 224, 3)
img = image.load_img(img_path, target_size=(
input_shape[0], input_shape[1]))
img_array = image.img_to_array(img)
expanded_img_array = np.expand_dims(img_array, axis=0)
preprocessed_img = preprocess_input(expanded_img_array)
features = model.predict(preprocessed_img)
flattened_features = features.flatten()
normalized_features = flattened_features / norm(flattened_features)
return normalized_features
提示
在前面示例中定义的函数是我们在 Keras 中几乎每个特征提取需求中使用的key函数。
就是这样!让我们看看模型生成的特征长度:
features = extract_features('../../sample_images/cat.jpg', model)
print(len(features))
annoy
> 2048
ResNet-50 模型从提供的图像生成了 2,048 个特征。每个特征都是介于 0 和 1 之间的浮点值。
提示
如果您的模型是在与 ImageNet 不相似的数据集上训练或微调的,请相应重新定义“preprocess_input(img)”步骤。该函数中使用的均值是特定于 ImageNet 数据集的。Keras 中的每个模型都有自己的预处理函数,因此请确保您使用正确的函数。
现在是时候为整个数据集提取特征了。首先,我们使用这个方便的函数获取所有文件名,该函数会递归查找目录下所有图像文件(由其扩展名定义):
extensions = ['.jpg', '.JPG', '.jpeg', '.JPEG', '.png', '.PNG']
def get_file_list(root_dir):
file_list = []
counter = 1
for root, directories, filenames in os.walk(root_dir):
for filename in filenames:
if any(ext in filename for ext in extensions):
file_list.append(os.path.join(root, filename))
counter += 1
return file_list
然后,我们提供数据集的路径并调用该函数:
# path to the datasets
root_dir = '../../datasets/caltech101'
filenames = sorted(get_file_list(root_dir))
我们现在定义一个变量,将存储所有特征,遍历数据集中的所有文件名,提取它们的特征,并将它们附加到先前定义的变量中:
feature_list = []
for i in tqdm_notebook(range(len(filenames))):
feature_list.append(extract_features(filenames[i], model))
在 CPU 上,这应该在一个小时内完成。在 GPU 上,只需要几分钟。
提示
为了更好地了解时间,使用超级方便的工具tqdm,它显示一个进度条(图 4-3)以及每次迭代的速度,已经过去的时间和预计的完成时间。在 Python 中,使用tqdm包装一个可迭代对象;例如,tqdm(range(10))。其 Jupyter Notebook 变体是tqdm_notebook。

图 4-3. 使用tqdm_notebook显示的进度条
最后,将这些特征写入 pickle 文件,以便我们将来可以在不必重新计算的情况下使用它们:
pickle.dump(feature_list, open('data/features-caltech101-resnet.pickle', 'wb'))
pickle.dump(filenames, open('data/filenames-caltech101.pickle','wb'))
这就是全部!我们已经完成了特征提取部分。
相似性搜索
给定一张照片,我们的目标是在我们的数据集中找到一张与当前照片相似的照片。我们首先加载预先计算的特征:
filenames = pickle.load(open('data/filenames-caltech101.pickle', 'rb'))
feature_list = pickle.load(open('data/features-caltech101-resnet.pickle', 'rb'))
我们将使用 Python 的机器学习库scikit-learn来查找查询特征的最近邻居;也就是代表查询图像的特征。我们使用暴力算法训练一个最近邻模型,以根据欧几里德距离找到最近的五个邻居(要在系统上安装scikit-learn,请使用pip3 install sklearn):
from sklearn.neighbors import NearestNeighbors
neighbors = NearestNeighbors(n_neighbors=5, algorithm='brute',
metric='euclidean').fit(feature_list)
distances, indices = neighbors.kneighbors([feature_list[0]])
现在,您已经知道了最接近第一个查询特征(代表第一张图像)的五个最近邻的索引和距离。请注意第一步——训练步骤的快速执行。与训练大多数机器学习模型不同,这些模型可能需要几分钟到几小时在大型数据集上训练,实例化最近邻模型是瞬时的,因为在训练时没有太多处理。这也被称为惰性学习,因为所有处理都推迟到分类或推理时间。
现在我们知道了索引,让我们看看那个特征背后的实际图像。首先,我们选择一个图像进行查询,比如说,索引=0:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
%matplotlib inline # Show the plots as a cell within the Jupyter Notebooks
plt.imshow(mpimg.imread(filenames[0]))
图 4-4 展示了这个结果。

图 4-4。Caltech-101 数据集中的查询图像
现在,让我们通过绘制第一个结果来检查最近邻。
plt.imshow(mpimg.imread(filenames[indices[0]]))
图 4-5 展示了这个结果。

图 4-5。我们查询图像的最近邻
等等,这不是重复的吗?实际上,最近的索引将是图像本身,因为这就是被查询的内容:
for i in range(5):
print(distances[0][i])
0.0
0.8285478
0.849847
0.8529018
这也得到了第一个结果距离为零的事实的证实。现在让我们绘制真正的第一个最近邻:
plt.imshow(mpimg.imread(filenames[indices[1]]))
这次看看图 4-6 中的结果。

图 4-6。查询图像的第二近邻
这绝对看起来像是一张相似的图像。它捕捉到了一个相似的概念,具有相同的图像类别(人脸),相同的性别,以及与柱子和植被相似的背景。事实上,这是同一个人!
我们可能会经常使用这个功能,所以我们已经构建了一个名为plot_images()的辅助函数,用于可视化几个查询图像及其最近邻。现在让我们调用这个函数来可视化六个随机图像的最近邻。另外,请注意,每次运行以下代码片段时,显示的图像将不同(图 4-7),因为显示的图像是由一个随机整数索引的。
for i in range(6):
random_image_index = random.randint(0,num_images)
distances, indices = neighbors.kneighbors([featureList[random_image_index]])
# don't take the first closest image as it will be the same image
similar_image_paths = [filenames[random_image_index]] +
[filenames[indices[0][i]] for i in range(1,4)]
plot_images(similar_image_paths, distances[0])

图 4-7。不同图像的最近邻返回相似的图像
用 t-SNE 可视化图像聚类
让我们通过可视化整个数据集来提升游戏!
为了做到这一点,我们需要降低特征向量的维度,因为不可能在两个维度(纸张)中绘制一个 2,048 维向量(特征长度)。t-分布随机邻居嵌入(t-SNE)算法将高维特征向量降至 2D,提供数据集的鸟瞰视图,有助于识别聚类和附近图像。t-SNE 难以扩展到大型数据集,因此通过主成分分析(PCA)降低维度然后调用 t-SNE 是一个好主意:
# Perform PCA over the features
num_feature_dimensions=100 # Set the number of features
pca = PCA(n_components = num_feature_dimensions)
pca.fit(featureList)
feature_list_compressed = pca.transform(featureList)
# For speed and clarity, we'll analyze about first half of the dataset.
selected_features = feature_list_compressed[:4000]
selected_class_ids = class_ids[:4000]
selected_filenames = filenames[:4000]
tsne_results =
TSNE(n_components=2,verbose=1,metric='euclidean')
.fit_transform(selected_features)
# Plot a scatter plot from the generated t-SNE results
colormap = plt.cm.get_cmap('coolwarm')
scatter_plot = plt.scatter(tsne_results[:,0],tsne_results[:,1], c =
selected_class_ids, cmap=colormap)
plt.colorbar(scatter_plot)
plt.show()
我们将在后面的部分更详细地讨论 PCA。为了扩展到更大的维度,使用均匀流形逼近和投影(UMAP)。
图 4-8 展示了相似类别的聚类,以及它们是如何靠近彼此的。

图 4-8。t-SNE 可视化图像特征聚类,每个聚类用相同颜色表示一个对象类别
图 4-8 中的每种颜色表示不同的类别。为了使其更加清晰,我们可以使用另一个辅助函数plot_images_in_2d()来绘制这些集群中的图像,就像图 4-9 中所演示的那样。

图 4-9. t-SNE 可视化显示图像集群;相似的图像在同一集群中
很棒!有一个明显划分的人脸、花朵、老式汽车、船只、自行车的集群,以及一个稍微分散的陆地和海洋动物集群。有很多图像重叠在一起,这使得图 4-9 有点令人困惑,所以让我们尝试使用辅助函数tsne_to_grid_plotter_manual()将 t-SNE 绘制为清晰的瓷砖,其结果可以在图 4-10 中看到。
tsne_to_grid_plotter_manual(tsne_results[:,0], tsne_results[:,1],
selected_filenames)

图 4-10. t-SNE 可视化与瓷砖图像;相似的图像彼此靠近
这绝对更清晰了。我们可以看到相似的图像在人脸、椅子、自行车、飞机、船只、笔记本电脑、动物、手表、花朵、倾斜的尖塔、老式汽车、锚标志和相机的集群中共同定位,都靠近自己的同类。物以类聚!
提示
2D 集群很棒,但在 3D 中可视化它们会看起来更加出色。如果它们可以旋转、缩放,并且可以使用鼠标进行操作而无需编码,那将更好。如果数据可以以交互方式搜索,显示其邻居,那就更加分数。TensorFlow Embedding projector在基于浏览器的 GUI 工具中实现了所有这些功能以及更多。来自图像和文本数据集的预加载嵌入对于更好地理解嵌入的强大性能是有帮助的。正如图 4-11 所示,看到深度学习发现约翰·列侬、齐柏林飞艇和埃里克·克莱普顿恰好在英语中与披头士乐队在类似的语境中使用是令人欣慰的。

图 4-11. TensorFlow Embedding projector 显示了 10,000 个常见英语单词的 3D 表示,并突出显示了与“披头士”相关的单词
提高相似性搜索的速度
有几个机会可以提高相似性搜索步骤的速度。对于相似性搜索,我们可以利用两种策略:要么减少特征长度,要么使用更好的算法在特征之间进行搜索。让我们分别检查这两种策略。
特征向量长度
理想情况下,我们期望搜索的数据量越小,搜索速度就应该越快。回想一下,ResNet-50 模型提供了 2048 个特征。每个特征都是一个 32 位浮点数,每个图像由一个 8 KB 的特征向量表示。对于一百万张图像,这相当于将近 8 GB。想象一下在 8 GB 的特征中进行搜索会有多慢。为了更好地了解我们的情况,表 4-1 给出了我们从不同模型中获得的特征长度。
表 4-1. 不同 CNN 模型的 Top 1%准确率和特征长度
| 模型 | 瓶颈特征长度 | 在 ImageNet 上的 Top-1%准确率 |
|---|---|---|
| VGG16 | 512 | 71.5% |
| VGG19 | 512 | 72.7% |
| MobileNet | 1024 | 66.5% |
| InceptionV3 | 2048 | 78.8% |
| ResNet-50 | 2048 | 75.9% |
| Xception | 2048 | 79.0% |
注意
在幕后,tf.keras.applications中提供的许多模型产生数千个特征。例如,InceptionV3 产生形状为 1 x 5 x 5 x 2048 的特征,这相当于 2048 个 5 x 5 卷积的特征图,总共有 51200 个特征。因此,通过使用平均或最大池化层来减少这个大向量变得至关重要。池化层将每个卷积(例如 5 x 5 层)压缩为一个值。这可以在模型实例化期间定义如下:
model = InceptionV3(weights='imagenet', include_top=False,
input_shape = (224,224,3), pooling='max')
对于产生大量特征的模型,通常会发现所有代码示例都使用这个池化选项。表 4-2 显示了不同模型中最大池化对特征数量的影响前后效果。
表 4-2。不同模型在池化前后的特征数量
| 模型 | 池化前特征数量 | 池化后特征数量 |
|---|---|---|
| ResNet-50 | [1,1,1,2048] = 2048 | 2048 |
| InceptionV3 | [1,5,5,2048] = 51200 | 2048 |
| MobileNet | [1,7,7,1024] = 50176 | 1024 |
正如我们所看到的,几乎所有模型都生成大量特征。想象一下,如果我们能将特征减少到仅 100 个(减少了 10 到 20 倍!),而不会影响结果的质量,搜索速度会有多快。除了尺寸之外,这对于大数据场景来说是一个更大的改进,因为数据可以一次性加载到 RAM 中,而不是定期加载部分数据,从而提供更大的加速。PCA 将帮助我们实现这一点。
使用 PCA 减少特征长度
PCA 是一个统计过程,质疑代表数据的特征是否同等重要。一些特征是否足够冗余,以至于即使删除这些特征,我们仍然可以获得类似的分类结果?PCA 被认为是降维的常用技术之一。请注意,它并不消除冗余特征;相反,它生成一组新的特征,这些特征是输入特征的线性组合。这些线性特征彼此正交,这就是为什么所有冗余特征都不存在。这些特征被称为主成分。
执行 PCA 非常简单。使用scikit-learn库,执行以下操作:
import sklearn.decomposition.PCA as PCA
num_feature_dimensions=100
pca = PCA(n_components = num_feature_dimensions)
pca.fit(feature_list)
feature_list_compressed = pca.transform(feature_list)
PCA 还可以告诉我们每个特征的相对重要性。第一个维度具有最大的方差,随着维度的增加,方差不断减小:
# Explain the importance of first 20 features
print(pca.explained_variance_ratio_[0:20])
[ 0.07320023 0.05273142 0.04310822 0.03494248 0.02166119 0.0205037
0.01974325 0.01739547 0.01611573 0.01548918 0.01450421 0.01311005
0.01200541 0.0113084 0.01103872 0.00990405 0.00973481 0.00929487
0.00915592 0.0089256 ]
嗯,为什么我们从原始的 2048 个维度中选择了 100 个维度?为什么不是 200 个?PCA 代表我们的原始特征向量,但是在减少维度。每个新维度在表示原始向量方面的回报递减(即,新维度可能不太能解释数据),并占用宝贵的空间。我们可以在原始数据解释得有多好与我们想要减少多少之间取得平衡。让我们可视化前 200 个维度的重要性。
pca = PCA(200)
pca.fit(feature_list)
matplotlib.style.use('seaborn')
plt.plot(range(1,201),pca.explained_variance_ratio_,'o--', markersize=4)
plt.title ('Variance for each PCA dimension')
plt.xlabel('PCA Dimensions')
plt.ylabel('Variance')
plt.grid(True)
plt.show()
图 4-12 展示了结果。

图 4-12。每个 PCA 维度的方差
个体方差将告诉我们新增特征的重要性。例如,在前 100 个维度之后,额外的维度并不增加太多方差(几乎等于 0),可以忽略不计。甚至在检查准确性之前,可以安全地假设具有 100 个维度的 PCA 将是一个强大的模型。另一种看待这个问题的方式是通过找到累积方差来可视化原始数据有多少是由有限数量的特征解释的(参见图 4-13)。
plt.plot(range(1,201),pca.explained_variance_ratio_.cumsum(),'o--', markersize=4)
plt.title ('Cumulative Variance with each PCA dimension')
plt.xlabel('PCA Dimensions')
plt.ylabel('Variance')
plt.grid(True)
plt.show()

图 4-13。每个 PCA 维度的累积方差
如预期的那样,添加 100 个维度(从 100 到 200)仅增加了 0.1 的方差,并开始逐渐趋于平稳。作为参考,使用完整的 2,048 个特征将导致累积方差为 1。
PCA 中的维度数量是一个重要的参数,我们可以根据手头的问题进行调整。直接证明一个好的阈值的一种方法是找到特征数量与其对准确性和速度的影响之间的良好平衡:
pca_dimensions = [1,2,3,4,5,10,20,50,75,100,150,200]
pca_accuracy = []
pca_time = []
for dimensions in pca_dimensions:
# Perform PCA
pca = PCA(n_components = dimensions)
pca.fit(feature_list)
feature_list_compressed = pca.transform(feature_list[:])
# Calculate accuracy over the compressed features
accuracy, time_taken = accuracy_calculator(feature_list_compressed[:])
pca_time.append(time_taken)
pca_accuracy.append(accuracy)
print("For PCA Dimensions = ", dimensions, ",\tAccuracy = ",accuracy,"%",
",\tTime = ", pca_time[-1])
我们使用图表图 4-14 来可视化这些结果,并看到在一定数量的维度之后,增加维度并不会导致更高的准确性:
plt.plot(pca_time, pca_accuracy,'o--', markersize=4)
for label, x, y in zip(pca_dimensions, pca_time,pca_accuracy):
plt.annotate(label, xy=(x, y), ha='right', va='bottom')
plt.title ('Test Time vs Accuracy for each PCA dimension')
plt.xlabel('Test Time')
plt.ylabel('Accuracy')
plt.grid(True)
plt.show()

图 4-14。每个 PCA 维度的测试时间与准确性
正如图中所示,在超过 100 个维度的特征长度之后,准确性几乎没有提高。与原始数据(2,048)相比,几乎少了 20 倍的维度(100),这在几乎任何搜索算法中都提供了极高的速度和更少的时间,同时实现了类似(有时略有更好)的准确性。因此,100 将是这个数据集的理想特征长度。这也意味着前 100 个维度包含了关于数据集的大部分信息。
使用这种减少的表示有许多好处,如有效利用计算资源、去除噪音、由于维度较少而实现更好的泛化,以及对在这些数据上学习的机器学习算法的性能改进。通过将距离计算减少到最重要的特征,我们还可以稍微提高结果的准确性。这是因为以前所有的 2,048 个特征在距离计算中都是平等贡献的,而现在,只有最重要的 100 个特征发挥作用。但更重要的是,它使我们摆脱了“维度灾难”。观察到,随着维度数量的增加,两个最接近点和两个最远点之间的欧氏距离比 tend to become 1。在非常高维的空间中,来自真实世界数据集的大多数点似乎相互之间的距离相似,欧氏距离度量开始无法区分相似和不相似的项目。PCA 有助于恢复理智。
您还可以尝试不同的距离,如闵可夫斯基距离、曼哈顿距离、杰卡德距离和加权欧氏距离(其中权重是每个特征的贡献,如pca.explained_variance_ratio_中所解释的)。
现在,让我们将注意力转向使用这个减少后的特征集来使我们的搜索更快。
使用近似最近邻扩展相似性搜索
我们想要什么?最近邻。我们的基准是什么?暴力搜索。虽然在两行中实现起来很方便,但它会遍历每个元素,因此随着数据大小(项目数量以及维度数量)的增加而呈线性增长。将我们的特征向量从 2,048 的长度缩减到 100,不仅会使数据大小减少 20 倍,还会使使用暴力搜索时的速度增加 20 倍。PCA 确实是值得的!
假设相似性搜索一个包含 10,000 张图像的小集合,现在用 100 个特征长度向量表示,大约需要 1 毫秒。即使对于 10,000 个项目来说这看起来很快,但在一个真实的生产系统中,也许有更大的数据,比如 10 百万个项目,这将需要超过一秒的时间来搜索。我们的系统可能无法每秒每个 CPU 核心处理超过一个查询。如果您从用户那里每秒收到 100 个请求,即使在机器的多个 CPU 核心上运行(并为每个线程加载搜索索引),您也需要多台机器才能处理流量。换句话说,低效的算法意味着花费大量的硬件成本。
蛮力搜索是我们进行每次比较的基准。在大多数算法方法中,蛮力搜索是最慢的方法。现在我们已经设定了基准,我们将探索近似最近邻算法。与蛮力搜索方法保证正确结果不同,近似算法通常能够得到正确结果,因为它们是...嗯,近似值。大多数算法提供某种形式的调整来平衡正确性和速度。可以通过与蛮力基准结果进行比较来评估结果的质量。
近似最近邻基准
有几个近似最近邻(ANN)库,包括知名的 Spotify 的 Annoy、FLANN、Facebook 的 Faiss、Yahoo 的 NGT 和 NMSLIB。对它们进行基准测试将是一项繁琐的任务(假设您能够安装其中一些)。幸运的是,ann-benchmarks.com的热心人(Martin Aumueller、Erik Bernhardsson 和 Alec Faitfull)已经为我们做了大量工作,以可重现的方式在 19 个库上进行了大型公共数据集的基准测试。我们将在一个代表单词的特征嵌入数据集上进行比较(而不是图像),该数据集大小为 350 MB,包含 400,000 个表示 100 维单词的特征向量。图 4-15 展示了它们在正确性调整时的原始性能。性能是以库每秒响应查询的能力来衡量的。请记住,正确性的度量是返回的前 n 个最接近项与真实前 n 个最接近项的比例。这个基准是通过蛮力搜索来衡量的真实情况。

图 4-15. ANN 库比较(数据来自ann-benchmarks.com)
在这个数据集上表现最强的库在可接受的 0.8 召回率下每秒返回接近数千个查询。为了让大家有个概念,我们的蛮力搜索每秒执行不到 1 个查询。在最快的情况下,一些库(如 NGT)每秒可以返回超过 15,000 个结果(尽管召回率较低,使其在使用上不切实际)。
我应该使用哪个库?
毫无疑问,您使用的库将在很大程度上取决于您的场景。每个库在搜索速度、准确性、索引大小、内存消耗、硬件使用(CPU/GPU)和设置便捷性之间存在权衡。表 4-3 提供了不同场景和推荐的库的摘要。
表 4-3. ANN 库推荐
| 场景 | 推荐 |
|---|---|
| 我想在 Python 中快速进行实验,而不需要太多设置,但我也关心速度。 | 使用 Annoy 或 NMSLIB |
| 我有一个大型数据集(多达 1000 万条记录或数千个维度),并且非常关心速度。 | 使用 NGT |
| 我有一个庞大的数据集(超过 1 亿条记录)并且有一组 GPU。 | 使用 Faiss |
| 我想建立一个 100%正确性的基准,然后立即转向更快的库,用数量级的加速打动我的老板,并获得奖金。 | 使用蛮力搜索方法 |
我们在书的 GitHub 网站上提供了几个库的代码详细示例(请参阅http://PracticalDeepLearning.ai),但是在这里,我们将详细展示我们常用的库 Annoy,并将其与合成数据集上的蛮力搜索进行比较。此外,我们还简要介绍了 Faiss 和 NGT。
创建合成数据集
为了进行不同库之间的苹果对苹果比较,我们首先创建一个由随机浮点值组成的百万项目数据集,均值为 0,方差为 1。此外,我们选择一个随机特征向量作为我们的查询以找到最近的邻居:
num_items = 1000000
num_dimensions = 100
dataset = np.random.randn(num_items, num_dimensions)
dataset /= np.linalg.norm(dataset, axis=1).reshape(-1, 1)
random_index = random.randint(0,num_items)
query = dataset[random_index]
蛮力算法
首先,我们计算使用蛮力算法进行搜索所需的时间。它会逐个遍历整个数据,计算查询和当前项目之间的距离。我们使用timeit命令来计算时间。首先,我们创建搜索索引以检索五个最近的邻居,然后使用查询进行搜索:
neighbors = NearestNeighbors(n_neighbors=5, algorithm='brute',
metric='euclidean').fit(dataset)
%timeit distances, indices = neighbors.kneighbors([query])
> 177 ms ± 136 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
提示
timeit命令是一个方便的工具。要对单个操作的时间进行基准测试,请在其前面加上此命令。与运行一次语句的 time 命令相比,timeit会多次运行后续行以提供更精确的聚合统计数据以及标准偏差。默认情况下,它关闭垃圾收集,使独立的计时更具可比性。也就是说,这可能不反映在实际生产负载中打开垃圾收集时的计时情况。
Annoy
Annoy(近似最近邻居)是一个带有 Python 绑定的 C++库,用于搜索最近邻居。以速度闻名,由 Spotify 发布,并用于生产中提供其音乐推荐。与其名字相反,实际上使用起来很有趣且简单。
要使用 Annoy,我们使用pip进行安装:
$ pip install annoy
使用起来相当简单。首先,我们使用两个超参数构建一个搜索索引:数据集的维度数量和树的数量:
from annoy import AnnoyIndex
annoy_index = AnnoyIndex(num_dimensions) *# Length of item vector that will be
`indexed`*
for i in range(num_items):
annoy_index.add_item(i, dataset[i])
annoy_index.build(40) # 40 trees
现在让我们看看搜索一个图像的五个最近邻居需要多长时间:
%timeit indexes=t.get_nns_by_vector(query, 5, include_distances=True)
> 34.9 μs ± 165 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
现在这真是飞快!换个角度来看,即使对于我们的百万项目数据集,这在单个 CPU 核心上可以处理近 28,000 个请求。考虑到大多数 CPU 都有多个核心,它应该能够在单个系统上处理超过 100,000 个请求。最好的部分是它允许您在多个进程之间共享相同的内存中的索引。因此,最大的索引可以等同于您的整体 RAM 大小,使得在单个系统上处理多个请求成为可能。
其他好处包括生成一个适度大小的索引。此外,它将创建索引与加载索引分离,因此您可以在一台机器上创建索引,传递它,然后在您的服务机器上将其加载到内存中并提供服务。
提示
想知道要使用多少棵树吗?更多的树会提供更高的精度,但会增加索引的大小。通常,不需要超过 50 棵树来获得最高精度。
NGT
雅虎日本的邻域图和树(NGT)库目前在大多数基准测试中处于领先地位,最适合具有大维度(数千个)的大型数据集(数百万个项目)。尽管该库自 2016 年以来就存在,但真正进入行业基准测试场景的时间是在 2018 年,当时实现了 ONNG 算法(k最近邻图索引优化),考虑到可能有多个线程在服务器上运行 NGT,它可以将索引放在共享内存中,借助内存映射文件来帮助减少内存使用量以及增加加载时间。
Faiss
Faiss 是 Facebook 的高效相似性搜索库。通过存储向量的压缩表示(紧凑的量化代码)而不是原始值,它可以在单个服务器上扩展到数十亿个向量的 RAM。它特别适用于密集向量。通过在 GPU 内存(VRAM)上存储索引,特别适用于具有 GPU 的机器。这适用于单 GPU 和多 GPU 设置。它提供了根据搜索时间、准确性、内存使用和索引时间配置性能的能力。它是已知的在 GPU 上最快的 ANN 搜索实现之一。嘿,如果对 Facebook 来说足够好,那对我们大多数人来说也足够好(只要我们有足够的数据)。
虽然展示整个过程超出了本书的范围,但我们建议使用 Anaconda 安装 Faiss 或使用其 Docker 容器快速入门。
通过微调提高准确性
许多预训练模型是在 ImageNet 数据集上训练的。因此,在大多数情况下,它们为相似性计算提供了一个令人难以置信的起点。也就是说,如果你调整这些模型以适应你的特定问题,它们将更准确地找到相似的图像。
在本章的这一部分,我们识别了表现最差的类别,用 t-SNE 进行可视化,微调,然后看看它们的 t-SNE 图表如何变化。
什么是一个好的度量标准,用来检查是否确实获得了相似的图像?
痛苦的选择 1
逐个图像浏览整个数据集,并手动评分返回的图像是否确实看起来相似。
更快乐的选择 2
简单地计算准确性。也就是说,对于属于类别X的图像,相似的图像是否属于相同的类别?我们将称之为相似性准确性。
那么,我们表现最差的类别是什么?为什么它们表现最差?为了回答这个问题,我们预先定义了一个辅助函数worst_classes。对于数据集中的每个图像,它使用蛮力算法找到最近邻,然后返回准确性最低的六个类别。为了查看微调的效果,我们在一个更困难的数据集上运行我们的分析:Caltech-256。调用这个函数揭示了准确性最低的类别:
names_of_worst_classes_before_finetuning, accuracy_per_class_before_finetuning =
worst_classes(feature_list[:])
Accuracy is 56.54
Top 6 incorrect classifications
059.drinking-straw Accuracy: 11.76%
135.mailbox Accuracy: 16.03%
108.hot-dog Accuracy: 16.72%
163.playing-card Accuracy: 17.29%
195.soda-can Accuracy: 19.68%
125.knife Accuracy: 20.53%
为了了解它们在某些类别上表现如此糟糕的原因,我们绘制了一个 t-SNE 图表,以在 2D 空间中可视化嵌入,你可以在图 4-16 中看到。为了防止图表过度拥挤,我们只使用了每个 6 个类别中的 50 个项目。
提示
为了增强图表的可见性,我们可以为每个类别定义不同的标记和不同的颜色。Matplotlib 提供了各种标记和颜色。
markers = [ "^", ".","s", "o","x", "P" ]
colors = ['red', 'blue', 'fuchsia', 'green',
'purple', 'orange']

图 4-16. 在微调之前对最不准确类别的特征向量进行 t-SNE 可视化
啊,这些特征向量到处都是,互相重叠。在其他应用程序中使用这些特征向量,如分类,可能不是一个好主意,因为很难找到它们之间的清晰分隔平面。难怪它们在这个基于最近邻的分类测试中表现如此糟糕。
如果我们使用微调模型重复这些步骤,你认为结果会是什么?我们认为会有一些有趣的事情;让我们看看图 4-17,看看。

图 4-17. 在微调后对最不准确类别的特征向量进行 t-SNE 可视化
这样就清爽多了。就像在第三章中展示的那样,只需轻微微调,嵌入就开始聚集在一起。将预训练模型的嵌入与微调模型的嵌入进行比较。机器学习分类器将能够更轻松地在这些类别之间找到一个分隔平面,从而提高分类准确性,以及在不使用分类器时更相似的图像。请记住,这些是最高误分类的类别;想象一下在微调后原本准确率更高的类别会有多么好。
以前,预训练的嵌入实现了 56%的准确率。微调后的新嵌入提供了惊人的 87%准确率!一点点魔法就能产生巨大的影响。
微调的一个限制是需要有标记的数据,这并不总是存在。因此,根据您的用例,您可能需要标记一些数据。
不过,还有一个小小的非传统训练技巧,我们将在下一节中讨论。
没有全连接层的微调
正如我们已经知道的,神经网络由三部分组成:
-
最终生成特征向量的卷积层
-
全连接层
-
最终的分类器层
微调,顾名思义,涉及轻微调整神经网络以适应新的数据集。通常涉及剥离全连接层(顶层),用新的层替换它们,然后使用这个数据集训练这个新组合的神经网络。以这种方式训练会导致两件事情:
-
所有新添加的全连接层中的权重将受到显著影响。
-
卷积层中的权重只会略微改变。
全连接层在获得最大分类准确率方面起着很大作用。因此,生成特征向量的网络的大部分部分将变化微不足道。因此,尽管微调,特征向量将显示很少的变化。
我们的目标是让外观相似的对象具有更接近的特征向量,而之前描述的微调未能实现这一目标。通过强制所有特定任务的学习发生在卷积层中,我们可以看到更好的结果。我们如何实现这一点呢?通过移除所有全连接层,并在卷积层之后直接放置一个分类器层(生成特征向量的卷积层)。这个模型是为相似性搜索而优化的,而不是为分类。
为了比较微调模型优化的过程,用于分类任务与相似性搜索,让我们回顾一下我们在第三章中如何微调我们的模型以进行分类:
from tf.keras.applications.resnet50 import ResNet50
model = ResNet50(weights='imagenet', include_top=False,
input_shape = (224,224,3))
input = Input(shape=(224, 224, 3))
x = model(input)
x = GlobalAveragePooling2D()(x)
x = Dense(64, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(NUM_CLASSES, activation='softmax')(x)
model_classification_optimized = Model(inputs=input, outputs=x)
以下是我们如何为相似性搜索微调我们的模型。请注意中间缺少的隐藏密集层:
from tf.keras.applications.resnet50 import ResNet50
model = ResNet50(weights='imagenet', include_top=False,
input_shape = (224,224,3))
input = Input(shape=(224, 224, 3))
x = model(input)
x = GlobalAveragePooling2D()(x)
# No dense or dropout layers
x = Dense(NUM_CLASSES, activation='softmax')(x)
model_similarity_optimized = Model(inputs=input, outputs=x)
在微调之后,为了使用model_similarity_optimized来提取特征而不是为类别给出概率,只需pop(即移除)最后一层:
model_similarity_optimized.layers.pop()
model = Model(model_similarity_optimized.input,
model_similarity_optimized.layers[-1].output)
这里需要注意的关键一点是,如果您使用常规的微调过程,我们将获得比model_similarity_optimized更低的相似性准确率。显然,我们希望在分类场景下使用model_classification_optimized,在提取嵌入以进行相似性搜索时使用model_similarity_optimized。
有了所有这些知识,现在你可以为你正在处理的任何场景制作一个快速准确的相似性系统。是时候看看人工智能行业的巨头是如何构建他们的产品的了。
用于一次性人脸验证的连体网络
人脸验证系统通常试图确定——给定两张人脸图像——这两张图像是否属于同一个人。这是一个高精度的二元分类器,需要能够稳健地处理不同的光照、服装、发型、背景和面部表情。为了增加挑战,尽管在员工数据库中可能有许多人的图像,但可能只有少数同一个人的图像可用。同样,在银行的签名识别和亚马逊的产品识别中也存在相同的限制每个项目图像数量的挑战。
你会如何训练这样的分类器?从 ImageNet 预训练的 ResNet 模型中挑选嵌入可能无法区分这些细微的面部特征。一种方法是将每个人作为一个单独的类别,然后像通常训练常规网络一样进行训练。出现了两个关键问题:
-
如果我们有一百万个个体,训练一百万个类别是不可行的。
-
每类图像训练数量较少会导致过度训练。
另一个想法:我们可以教授网络直接比较并决定一对图像是否相似或不相似,通过在训练期间对它们的相似性提供指导。这就是连体网络背后的关键思想。拿一个模型,输入两张图像,提取两个嵌入,然后计算两个嵌入之间的距离。如果距离低于阈值,则认为它们相似,否则不相似。通过输入一对图像和相关标签,相似或不相似,并将网络端到端地训练,嵌入开始捕捉输入的细粒度表示。这种直接优化距离度量的方法,如图 4-18 所示,被称为度量学习。

图 4-18。用于签名验证的连体网络;请注意相同的 CNN 用于两个输入图像
我们可以扩展这个想法,甚至输入三张图像。选择一个锚定图像,选择另一个正样本(相同类别),和另一个负样本(不同类别)。现在让我们训练这个网络,直接优化相似项之间的距离最小化,不相似项之间的距离最大化。帮助我们实现这一目标的损失函数称为三元损失函数。在前面的一对图像的情况下,损失函数称为对比损失函数。三元损失函数往往会产生更好的结果。
网络训练完成后,在测试时我们只需要一张脸部的参考图像来判断这个人是否相同。这种方法为一次性学习打开了大门。其他常见用途包括签名和标志识别。Saket Maheshwary 和 Hemant Misra 提出了一个非常有创意的应用,即使用连体网络通过计算两者之间的语义相似性来匹配简历和求职者。
案例研究
让我们看一些有趣的例子,展示我们迄今所学知识在工业中的应用。
Flickr
Flickr 是最大的照片分享网站之一,尤其受到专业摄影师的欢迎。为了帮助摄影师找到灵感并展示用户可能感兴趣的内容,Flickr 推出了一个基于相同语义意义的相似性搜索功能。正如在图 4-19 中所示,探索沙漠图案会导致几个类似图案的结果。在幕后,Flickr 采用了一种名为局部优化产品量化(LOPQ)的 ANN 算法,该算法已在 Python 和 Spark 实现中开源。

图 4-19。沙漠照片的相似模式(图片来源)
Pinterest 是一个广泛使用的应用程序,以其视觉搜索功能而闻名,更具体地说是其称为相似图钉和相关图钉的功能。百度和阿里巴巴等其他公司也推出了类似的视觉搜索系统。此外,Zappos、Google Shopping 和like.com正在使用计算机视觉进行推荐。
在 Pinterest 中,“女性时尚”是最受欢迎的图钉主题之一,相似外观功能(图 4-20)帮助人们发现相似的产品。此外,Pinterest 还报告称其相关图钉功能增加了转发率。Pinterest 上并非每个图钉都有关联的元数据,这使得推荐成为一个困难的冷启动问题,因为缺乏上下文。Pinterest 的开发人员通过使用视觉特征来生成相关图钉来解决这个冷启动问题。此外,Pinterest 实现了一个增量指纹服务,如果上传了新图像或者特征发生演变(由于工程师对底层模型的改进或修改),则生成新的数字签名。

图 4-20。Pinterest 应用程序的相似外观功能(图片来源:Pinterest 博客)
名人模仿者
像Celebslike.me这样的网站应用程序在 2015 年爆红,寻找名人中的最近邻居,如图 4-21 所示。2018 年,Google Arts & Culture 应用程序采取了类似的病毒式方法,显示与您的脸最接近的现有肖像。双胞胎与否是另一个具有类似目的的应用程序。

图 4-21。在 celebslike.me 网站上测试我们的朋友 Pete Warden 的照片(Google 移动和嵌入式 TensorFlow 技术主管)
Spotify
Spotify 使用最近邻居来推荐音乐,并根据当前播放的歌曲集创建自动播放列表和电台。通常,用于在 Netflix 上推荐电影等内容的协同过滤技术是内容不可知的;也就是说,推荐发生是因为具有相似口味的大量用户正在观看相似的电影或听相似的歌曲。这对于新的尚未流行的内容构成问题,因为用户将继续获得现有流行内容的推荐。这也被称为前面提到的冷启动问题。解决方案是使用对内容的潜在理解。与图像类似,我们可以使用 MFCC 特征(Mel 频率倒谱系数)从音乐中创建特征向量,从而生成可以被视为图像并用于生成特征的 2D 频谱图。歌曲被分成三秒片段,它们的频谱图用于生成特征。然后将这些特征平均在一起以代表完整的歌曲。图 4-22 显示了歌手的歌曲被投影到特定区域。我们可以区分嘻哈(左上)、摇滚(右上)、流行(左下)和电子音乐(右下)。正如前面讨论的,Spotify 在后台使用 Annoy。

图 4-22。使用从音频预测的潜在因素进行预测使用模式分布的 t-SNE 可视化(图片来源:“深度基于内容的音乐推荐”由 Aaron van den Oord,Sander Dieleman,Benjamin Schrauwen,NIPS 2013)
图像字幕
图像字幕是将图像翻译成句子的科学(如图 4-23 所示)。这不仅仅是物体标记,还需要对整个图像和物体之间的关系有更深入的视觉理解。为了训练这些模型,一个名为 MS COCO 的开源数据集于 2014 年发布,其中包括超过 30 万张图像,以及物体类别、句子描述、视觉问答对和物体分割。它作为每年竞赛的基准,用于观察图像字幕、物体检测和分割的进展。

图 4-23。Seeing AI 中的图像字幕功能:盲人社区的 Talking Camera App
在挑战的第一年(2015 年)中应用的一种常见策略是将语言模型(LSTM/RNN)与 CNN 结合起来,以使 CNN 特征向量的输出作为语言模型(LSTM/RNN)的输入。这种组合模型以端到端的方式联合训练,取得了令人印象深刻的结果,震惊了世界。尽管每个研究实验室都在努力超越对方,但后来发现进行简单的最近邻搜索可以产生最先进的结果。对于给定的图像,根据嵌入的相似性找到相似的图像。然后,注意相似图像字幕中的共同词,并打印包含最常见词的字幕。简而言之,懒惰的方法仍然能击败最先进的方法,这暴露了数据集中的一个关键偏见。
这种偏见被 Larry Zitnick 称为长颈鹿-树问题。在搜索引擎上搜索“长颈鹿”进行图像搜索。仔细观察:除了长颈鹿,几乎每张图像中都有草吗?很有可能你可以将这些图像的大多数描述为“一只长颈鹿站在草地上”。同样,如果查询图像(如图 4-24 中最左边的照片)包含一只长颈鹿和一棵树,几乎所有相似的图像(右边)都可以描述为“一只长颈鹿站在草地上,旁边有一棵树”。即使没有对图像有更深入的理解,也可以通过简单的最近邻搜索得出正确的字幕。这表明为了衡量系统的真正智能,我们需要在测试集中加入更多语义上新颖/原创的图像。

图 4-24。长颈鹿-树问题(图片来源:通过视觉问答测量机器智能,C.劳伦斯·齐特尼克,艾丝瓦里亚·阿格拉瓦尔,斯坦尼斯洛夫·安托尔,玛格丽特·米切尔,德鲁夫·巴特拉,黛薇·帕里克)
简而言之,不要低估简单的最近邻方法!
总结
现在我们已经完成了一次成功的探险,我们在其中探索了如何利用嵌入来定位相似的图像。我们通过探索如何利用 ANN 算法和库(包括 Annoy、NGT 和 Faiss)将搜索从几千个文档扩展到几十亿个文档。我们还了解到,通过对模型进行微调,可以提高在监督设置中嵌入的准确性和代表性能力。最后,我们看了如何使用 Siamese 网络,利用嵌入的力量进行一次性学习,比如用于人脸验证系统。最后,我们研究了在行业中各种用例中如何使用最近邻方法。最近邻是您工具包中简单但强大的工具。
第五章:从新手到大师预测者:最大化卷积神经网络准确性
在第一章中,我们探讨了负责任的人工智能开发的重要性。我们讨论的一个方面是我们模型的稳健性的重要性。用户只有在能够确信他们在日常生活中遇到的人工智能是准确可靠的情况下,才能信任我们构建的内容。显然,应用的背景非常重要。食物分类器偶尔将意大利面误分类为面包可能没问题。但是对于自动驾驶汽车将行人误认为街道车道就很危险。因此,本章的主要目标是构建更准确的模型。
在本章中,您将培养一种直觉,以识别下次开始训练时改进模型准确性的机会。我们首先看一下确保您不会盲目进行的工具。之后,在本章的大部分时间里,我们采取了一种非常实验性的方法,建立基线,隔离要调整的单个参数,并观察它们对模型性能和训练速度的影响。本章中使用的许多代码都汇总在一个 Jupyter Notebook 中,还附有一个可交互的示例清单。如果您选择将其纳入下次训练脚本中,它应该是非常可重用的。
我们探讨了在模型训练过程中经常出现的几个问题:
-
我不确定是使用迁移学习还是从头开始构建来训练自己的网络。对于我的情况,哪种方法更好?
-
我可以提供的最少数据量是多少,以获得可接受的结果?
-
我想确保模型正在学习正确的内容,而不是获取虚假相关性。我如何能够看到这一点?
-
如何确保我(或其他人)每次运行实验时都能获得相同的结果?换句话说,如何确保我的实验可重复性?
-
改变输入图像的长宽比是否会影响预测结果?
-
减少输入图像大小是否会对预测结果产生显著影响?
-
如果我使用迁移学习,应该微调多少层才能实现我偏好的训练时间与准确性的平衡?
-
或者,如果我从头开始训练,我的模型应该有多少层?
-
在模型训练期间提供适当的“学习率”是多少?
-
有太多事情需要记住。有没有一种方法可以自动化所有这些工作?
我们将尝试通过对几个数据集进行实验来逐一回答这些问题。理想情况下,您应该能够查看结果,阅读要点,并对实验所测试的概念有所了解。如果您感到更有冒险精神,您可以选择使用 Jupyter Notebook 自行进行实验。
行业工具
本章的主要重点之一是在试图获得高准确性的同时,减少实验过程中涉及的代码和工作量。存在一系列工具可以帮助我们使这个过程更加愉快:
TensorFlow 数据集
快速便捷地访问大约 100 个数据集,性能良好。所有知名数据集都可用,从最小的 MNIST(几兆字节)到最大的 MS COCO、ImageNet 和 Open Images(数百吉字节)。此外,还提供医学数据集,如结直肠组织学和糖尿病视网膜病变。
TensorBoard
近 20 种易于使用的方法,可视化训练的许多方面,包括可视化图形、跟踪实验以及检查通过网络的图像、文本和音频数据。
What-If 工具
在单独的模型上并行运行实验,并通过比较它们在特定数据点上的性能来揭示它们之间的差异。编辑单个数据点以查看它如何影响模型训练。
tf-explain
分析网络所做的决策,以识别数据集中的偏见和不准确性。此外,使用热图可视化网络在图像的哪些部分激活。
Keras 调谐器
一个为tf.keras构建的库,可以在 TensorFlow 2.0 中自动调整超参数。
AutoKeras
自动化神经架构搜索(NAS),跨不同任务进行图像、文本和音频分类以及图像检测。
自动增强
利用强化学习来改进现有训练数据集中的数据量和多样性,从而提高准确性。
现在让我们更详细地探索这些工具。
TensorFlow 数据集
TensorFlow 数据集是一个近 100 个准备就绪数据集的集合,可以快速帮助构建用于训练 TensorFlow 模型的高性能输入数据管道。TensorFlow 数据集标准化了数据格式,使得很容易用另一个数据集替换一个数据集,通常只需更改一行代码。正如您将在后面看到的,将数据集分解为训练、验证和测试也只需一行代码。我们还将在下一章从性能的角度探索 TensorFlow 数据集。
您可以使用以下命令列出所有可用数据集(为了节省空间,此示例中仅显示了完整输出的一小部分):
import tensorflow_datasets as tfds
print(tfds.list_builders())
['amazon_us_reviews', 'bair_robot_pushing_small', 'bigearthnet', 'caltech101',
'cats_vs_dogs', 'celeb_a', 'imagenet2012', … , 'open_images_v4',
'oxford_flowers102', 'stanford_dogs','voc2007', 'wikipedia', 'wmt_translate',
'xnli']
让我们看看加载数据集有多简单。稍后我们将把这个插入到一个完整的工作流程中:
# Import necessary packages
import tensorflow_datasets as tfds
# Downloading and loading the dataset
dataset = tfds.load(name="cats_vs_dogs", split=tfds.Split.TRAIN)
# Building a performance data pipeline
dataset = dataset.map(preprocess).cache().repeat().shuffle(1024).batch(32).
prefetch(tf.data.experimental.AUTOTUNE)
model.fit(dataset, ...)
提示
tfds生成了很多进度条,它们占用了很多屏幕空间——使用tfds.disable_progress_bar()可能是一个好主意。
TensorBoard
TensorBoard 是您可视化需求的一站式服务,提供近 20 种工具来理解、检查和改进模型的训练。
传统上,为了跟踪实验进展,我们保存每个时代的损失和准确性值,然后在完成时使用matplotlib绘制。这种方法的缺点是它不是实时的。我们通常的选择是观察文本中的训练进度。此外,在训练完成后,我们需要编写额外的代码来在matplotlib中制作图表。TensorBoard 通过提供实时仪表板(图 5-1)来解决这些问题以及更多紧迫问题,帮助我们可视化所有日志(如训练/验证准确性和损失),以帮助理解训练的进展。它提供的另一个好处是能够比较当前实验的进展与上一个实验,这样我们就可以看到参数的变化如何影响我们的整体准确性。

图 5-1。TensorBoard 默认视图展示实时训练指标(浅色线表示上一次运行的准确性)
为了使 TensorBoard 能够可视化我们的训练和模型,我们需要使用摘要写入器记录有关我们的训练的信息:
summary_writer = tf.summary.FileWriter('./logs')
要实时跟踪我们的训练,我们需要在模型训练开始之前加载 TensorBoard。我们可以使用以下命令加载 TensorBoard:
# Get TensorBoard to run
%load_ext tensorboard
# Start TensorBoard
%tensorboard --logdir ./log
随着更多 TensorFlow 组件需要可视化用户界面,它们通过成为可嵌入插件在 TensorBoard 中重用 TensorBoard。您会注意到 TensorBoard 上的非活动下拉菜单;那里您可以看到 TensorFlow 提供的所有不同配置文件或工具。表 5-1 展示了各种可用工具中的一小部分。
表 5-1. TensorBoard 的插件
| TensorBoard 插件名称 | 描述 |
|---|---|
| 默认标量 | 可视化标量值,如分类准确度。 |
| 自定义标量 | 可视化用户定义的自定义指标。例如,不同类别的不同权重,这可能不是一个现成的指标。 |
| 图像 | 通过点击图像选项卡查看每个层的输出。 |
| 音频 | 可视化音频数据。 |
| 调试工具 | 允许可视化调试并设置条件断点(例如,张量包含 NaN 或无穷大)。 |
| 图表 | 以图形方式显示模型架构。 |
| 直方图 | 显示模型各层的权重分布随训练进展的变化。这对于检查使用量化压缩模型的效果特别有用。 |
| Projector | 使用 t-SNE、PCA 等可视化投影。 |
| 文本 | 可视化文本数据。 |
| PR 曲线 | 绘制精确率-召回率曲线。 |
| 概要 | 对模型中所有操作和层的速度进行基准测试。 |
| Beholder | 实时训练过程中可视化模型的梯度和激活。它允许按滤波器查看它们,并允许将它们导出为图像甚至视频。 |
| What-If 工具 | 通过切片和切块数据以及检查其性能来调查模型。特别有助于发现偏见。 |
| HParams | 查找哪些参数以及以什么值最重要,允许记录整个参数服务器(在本章中详细讨论)。 |
| 网格 | 可视化 3D 数据(包括点云)。 |
值得注意的是,TensorBoard 并不是特定于 TensorFlow 的,可以与其他框架如 PyTorch、scikit-learn 等一起使用,具体取决于所使用的插件。要使插件工作,我们需要编写要可视化的特定元数据。例如,TensorBoard 将 TensorFlow Projector 工具嵌入其中,以使用 t-SNE 对图像、文本或音频进行聚类(我们在第四章中详细讨论过)。除了调用 TensorBoard 外,我们还需要编写像图像的特征嵌入这样的元数据,以便 TensorFlow Projector 可以使用它来进行聚类,如图 5-2 中所示。

图 5-2. TensorFlow 嵌入项目展示数据聚类(可以作为 TensorBoard 插件运行)
What-If 工具
如果我们能够通过可视化来检查我们的 AI 模型的预测结果会怎么样?如果我们能够找到最佳阈值来最大化精确度和召回率会怎么样?如果我们能够通过切片和切块数据以及我们的模型所做的预测来看到它擅长的地方以及有哪些改进机会会怎么样?如果我们能够比较两个模型以找出哪个确实更好会怎么样?如果我们能够通过在浏览器中点击几下就能做到所有这些以及更多呢?听起来肯定很吸引人!Google 的 People + AI Research(PAIR)倡议中的 What-If 工具(图 5-3 和图 5-4)帮助打开 AI 模型的黑匣子,实现模型和数据的可解释性。

图 5-3. What-If 工具的数据点编辑器使根据数据集的注释和分类器的标签对数据进行过滤和可视化成为可能

图 5-4。在 What-If 工具的性能和公平性部分中的 PR 曲线帮助交互式地选择最佳阈值以最大化精度和召回率
要使用 What-If 工具,我们需要数据集和一个模型。正如我们刚才看到的,TensorFlow Datasets 使得下载和加载数据(以tfrecord格式)相对容易。我们只需要定位数据文件即可。此外,我们希望将模型保存在同一目录中:
# Save model for What If Tool
tf.saved_model.save(model, "/tmp/model/1/")
最好在本地系统中执行以下代码行,而不是在 Colab 笔记本中,因为 Colab 和 What-If 工具之间的集成仍在不断发展。
让我们开始 TensorBoard:
$ mkdir tensorboard
$ tensorboard --logdir ./log --alsologtostderr
现在,在一个新的终端中,让我们为所有的 What-If 工具实验创建一个目录:
$ mkdir what-if-stuff
将训练好的模型和 TFRecord 数据移动到这里。整体目录结构看起来像这样:
$ tree .
├── colo
│ └── model
│ └── 1
│ ├── assets
│ ├── saved_model.pb
│ └── variables
我们将在新创建的目录中使用 Docker 来提供模型:
$ sudo docker run -p 8500:8500 \
--mount type=bind,source=/home/{your_username}/what-if-stuff/colo/model/,
target=/models/colo \
-e MODEL_NAME=colo -t tensorflow/serving
一句警告:端口必须是8500,所有参数必须与前面的示例中显示的完全相同。
接下来,在最右侧,点击设置按钮(灰色的齿轮图标),并添加表 5-2 中列出的值。
表 5-2。What-If 工具的配置
| 参数 | 值 |
|---|---|
| 推断地址 | ip_addr:8500 |
| 模型名称 | /models/colo |
| 模型类型 | 分类 |
| 示例路径 | /home/{your_username}/what_if_stuff/colo/models/colo.tfrec(注意:这必须是绝对路径) |
我们现在可以在 TensorBoard 中的浏览器中打开 What-If 工具,如图 5-5 所示。

图 5-5。What-If 工具的设置窗口
What-If 工具还可以根据不同的分组对数据集进行可视化,如图 5-6 所示。我们还可以使用该工具通过set_compare_estimator_and_feature_spec函数确定在同一数据集上多个模型中表现更好的模型。
from witwidget.notebook.visualization import WitConfigBuilder
# features are the test examples that we want to load into the tool
models = [model2, model3, model4]
config_builder =
WitConfigBuilder(test_examples).set_estimator_and_feature_spec(model1, features)
for each_model in models:
config_builder =
config_builder.set_compare_estimator_and_feature_spec(each_model, features)

图 5-6。What-If 工具可以使用多个指标、数据可视化等等
现在,我们可以加载 TensorBoard,然后在可视化部分选择我们想要比较的模型,如图 5-7 所示。这个工具有很多功能可以探索!

图 5-7。使用 What-If 工具选择要比较的模型
tf-explain
传统上,深度学习模型一直是黑匣子,直到现在,我们通常通过观察类别概率和验证准确性来了解它们的性能。为了使这些模型更具可解释性和可解释性,热图应运而生。通过显示导致预测的图像区域的强度更高,热图可以帮助可视化它们的学习过程。例如,经常在雪地中看到的动物可能会得到高准确度的预测,但如果数据集中只有那种动物和雪作为背景,模型可能只会关注雪作为与动物不同的模式,而不是动物本身。这样的数据集展示了偏见,使得当分类器置于现实世界中时,预测不够稳健(甚至可能危险!)。热图可以特别有用,以探索这种偏见,因为如果数据集没有经过仔细筛选,往往会渗入虚假相关性。
tf-explain(由 Raphael Meudec 开发)通过这样的可视化帮助理解神经网络的结果和内部工作,揭示数据集中的偏见。我们可以在训练时添加多种类型的回调,或者使用其核心 API 生成后续可以加载到 TensorBoard 中的 TensorFlow 事件。对于推断,我们只需要传递一张图像、其 ImageNet 对象 ID 以及一个模型到 tf-explain 的函数中。您必须提供对象 ID,因为tf.explain需要知道为该特定类别激活了什么。tf.explain提供了几种不同的可视化方法:
Grad CAM
梯度加权类激活映射(Grad CAM)通过查看激活图来可视化图像的部分如何影响神经网络的输出。基于最后一个卷积层的对象 ID 的梯度生成热图(在图 5-8 中有示例)。Grad CAM 在很大程度上是一个广谱热图生成器,因为它对噪声具有鲁棒性,并且可以用于各种 CNN 模型。
遮挡敏感性
通过遮挡图像的一部分(使用随机放置的小方块补丁)来确定网络的稳健性。如果预测仍然正确,那么网络平均上是稳健的。图像中最温暖(即红色)的区域在遮挡时对预测的影响最大。
激活
可视化卷积层的激活。

图 5-8。使用 MobileNet 和 tf-explain 在图像上进行可视化
如下面的代码示例所示,这样的可视化可以用很少的代码构建。通过拍摄视频,生成单独的帧,并使用 Grad CAM 运行 tf-explain 并将它们组合在一起,我们可以详细了解这些神经网络如何对移动摄像机角度做出反应。
from tf_explain.core.grad_cam import GradCAM
From tf.keras.applications.MobileNet import MobileNet
model = MobileNet(weights='imagenet', include_top=True)
# Set Grad CAM System
explainer = GradCAM()
# Image Processing
IMAGE_PATH = 'dog.jpg'
dog_index = 263
img = tf.keras.preprocessing.image.load_img(IMAGE_PATH, target_size=(224, 224))
img = tf.keras.preprocessing.image.img_to_array(img)
data = ([img], None)
# Passing the image through Grad CAM
grid = explainer.explain(data, model, 'conv1', index)
name = IMAGE_PATH.split(".jpg")[0]
explainer.save(grid, '/tmp', name + '_grad_cam.png')
机器学习实验的常见技术
前几章着重于训练模型。然而,接下来的几节包含一些在运行训练实验时要牢记的事项。
数据检查
数据检查的第一个最大障碍是确定数据的结构。TensorFlow Datasets 使得这一步相对容易,因为所有可用的数据集都具有相同的格式和结构,并且可以以高效的方式使用。我们只需要将数据集加载到 What-If 工具中,并使用已经存在的各种选项来检查数据。例如,在 SMILE 数据集上,我们可以根据其注释可视化数据集,例如戴眼镜和不戴眼镜的人的图像,如图 5-9 所示。我们观察到数据集中有更广泛的分布是没有戴眼镜的人的图像,从而揭示了由于数据不平衡而导致的数据偏见。这可以通过通过工具修改指标的权重来解决。

图 5-9。根据预测和真实类别切分和分割数据
数据分割:训练、验证、测试
将数据集分割为训练、验证和测试集非常重要,因为我们希望在分类器(即测试数据集)上报告结果。TensorFlow Datasets 使得下载、加载和将数据集分割为这三部分变得容易。一些数据集已经带有三个默认的分割。另外,数据可以按百分比进行分割。以下代码展示了使用默认分割:
dataset_name = "cats_vs_dogs"
train, info_train = tfds.load(dataset_name, split=tfds.Split.TRAIN,
with_info=True)
在tfds中的猫狗数据集只有预定义的训练集分割。与此类似,TensorFlow 数据集中的一些数据集没有validation分割。对于这些数据集,我们从预定义的training集中取一小部分样本,并将其视为validation集。总而言之,使用weighted_splits来拆分数据集可以处理在拆分之间随机化和洗牌数据:
# Load the dataset
dataset_name = "cats_vs_dogs"
# Dividing data into train (80), val (10) and test (10)
split_train, split_val, split_test = tfds.Split.TRAIN.subsplit(weighted=[80, 10,
10])
train, info_train = tfds.load(dataset_name, split=split_train , with_info=True)
val, info_val = tfds.load(dataset_name, split=split_val, with_info=True)
test, info_test = tfds.load(dataset_name, split=split_test, with_info=True)
早停
早停有助于避免网络过度训练,通过监视显示有限改进的时期的数量。假设一个模型被设置为训练 1,000 个时期,在第 10 个时期达到 90%的准确率,并在接下来的 10 个时期内不再有进一步的改进,那么继续训练可能是一种资源浪费。如果时期数超过了一个名为patience的预定义阈值,即使可能还有更多的时期可以训练,训练也会停止。换句话说,早停决定了训练不再有用的时刻,并停止训练。我们可以使用monitor参数更改指标,并将早停添加到模型的回调列表中:
# Define Early Stopping callback
earlystop_callback = tf.keras.callbacks.EarlyStopping(monitor='val_acc',
min_delta=0.0001, patience=10)
# Add to the training model
model.fit_generator(... callbacks=[earlystop_callback])
可重现的实验
训练一次网络。然后,再次训练,而不更改任何代码或参数。您可能会注意到,即使在代码中没有进行任何更改,两次连续运行的准确性也略有不同。这是由于随机变量造成的。为了使实验在不同运行中可重现,我们希望控制这种随机化。模型权重的初始化、数据的随机洗牌等都利用了随机化算法。我们知道,通过初始化种子,可以使随机数生成器可重现,这正是我们要做的。各种框架都有设置随机种子的方法,其中一些如下所示:
# Seed for Tensorflow
tf.random.set_seed(1234)
# Seed for Numpy
import numpy as np
np.random.seed(1234)
# Seed for Keras
seed = 1234
fit(train_data, augment=True, seed=seed)
flow_from_dataframe(train_dataframe, shuffle=True, seed=seed)
注意
在所有正在使用的框架和子框架中设置种子是必要的,因为种子在框架之间不可转移。
端到端深度学习示例管道
让我们结合几个工具,构建一个骨干框架,这将作为我们的管道,在其中我们将添加和删除参数、层、功能和各种其他附加组件,以真正理解发生了什么。按照书籍 GitHub 网站上的代码(参见http://PracticalDeepLearning.ai),您可以在 Colab 中的浏览器中交互式运行此代码,针对 100 多个数据集进行修改。此外,您可以将其修改为大多数分类任务。
基本迁移学习管道
首先,让我们为迁移学习构建这个端到端示例。
# Import necessary packages
import tensorflow as tf
import tensorflow_datasets as tfds
# tfds makes a lot of progress bars, which takes up a lot of screen space, hence
# disabling them
tfds.disable_progress_bar()
tf.random.set_seed(1234)
# Variables
BATCH_SIZE = 32
NUM_EPOCHS= 20
IMG_H = IMG_W = 224
IMG_SIZE = 224
LOG_DIR = './log'
SHUFFLE_BUFFER_SIZE = 1024
IMG_CHANNELS = 3
dataset_name = "oxford_flowers102"
def preprocess(ds):
x = tf.image.resize_with_pad(ds['image'], IMG_SIZE, IMG_SIZE)
x = tf.cast(x, tf.float32)
x = (x/127.5) - 1
return x, ds['label']
def augmentation(image,label):
image = tf.image.random_brightness(image, .1)
image = tf.image.random_contrast(image, lower=0.0, upper=1.0)
image = tf.image.random_flip_left_right(image)
return image, label
def get_dataset(dataset_name):
split_train, split_val = tfds.Split.TRAIN.subsplit(weighted=[9,1])
train, info_train = tfds.load(dataset_name, split=split_train , with_info=True)
val, info_val = tfds.load(dataset_name, split=split_val, with_info=True)
NUM_CLASSES = info_train.features['label'].num_classes
assert NUM_CLASSES >= info_val.features['label'].num_classes
NUM_EXAMPLES = info_train.splits['train'].num_examples * 0.9
IMG_H, IMG_W, IMG_CHANNELS = info_train.features['image'].shape
train = train.map(preprocess).cache().
repeat().shuffle(SHUFFLE_BUFFER_SIZE).batch(BATCH_SIZE)
train = train.map(augmentation)
train = train.prefetch(tf.data.experimental.AUTOTUNE)
val = val.map(preprocess).cache().repeat().batch(BATCH_SIZE)
val = val.prefetch(tf.data.experimental.AUTOTUNE)
return train, info_train, val, info_val, IMG_H, IMG_W, IMG_CHANNELS,
NUM_CLASSES, NUM_EXAMPLES
train, info_train, val, info_val, IMG_H, IMG_W, IMG_CHANNELS, NUM_CLASSES,
NUM_EXAMPLES = get_dataset(dataset_name)
# Allow TensorBoard callbacks
tensorboard_callback = tf.keras.callbacks.TensorBoard(LOG_DIR,
histogram_freq=1,
write_graph=True,
write_grads=True,
batch_size=BATCH_SIZE,
write_images=True)
def transfer_learn(train, val, unfreeze_percentage, learning_rate):
mobile_net = tf.keras.applications.ResNet50(input_shape=(IMG_SIZE, IMG_SIZE,
IMG_CHANNELS), include_top=False)
mobile_net.trainable=False
# Unfreeze some of the layers according to the dataset being used
num_layers = len(mobile_net.layers)
for layer_index in range(int(num_layers - unfreeze_percentage*num_layers),
num_layers ):
mobile_net.layers[layer_index].trainable = True
model_with_transfer_learning = tf.keras.Sequential([mobile_net,
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(64),
tf.keras.layers.Dropout(0.3),
tf.keras.layers.Dense(NUM_CLASSES,
activation='softmax')],)
model_with_transfer_learning.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
loss='sparse_categorical_crossentropy',
metrics=["accuracy"])
model_with_transfer_learning.summary()
earlystop_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
min_delta=0.0001,
patience=5)
model_with_transfer_learning.fit(train,
epochs=NUM_EPOCHS,
steps_per_epoch=int(NUM_EXAMPLES/BATCH_SIZE),
validation_data=val,
validation_steps=1,
validation_freq=1,
callbacks=[tensorboard_callback,
earlystop_callback])
return model_with_transfer_learning
# Start TensorBoard
%tensorboard --logdir ./log
# Select the last % layers to be trained while using the transfer learning
# technique. These layers are the closest to the output layers.
unfreeze_percentage = .33
learning_rate = 0.001
model = transfer_learn(train, val, unfreeze_percentage, learning_rate)
基本自定义网络管道
除了在最先进的模型上进行迁移学习外,我们还可以通过构建自己的自定义网络来进行实验和开发更好的直觉。只需在先前定义的迁移学习代码中交换模型即可:
def create_model():
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(IMG_SIZE, IMG_SIZE, IMG_CHANNELS)),
tf.keras.layers.MaxPool2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=(2, 2)),
tf.keras.layers.Dropout(rate=0.3),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(rate=0.3),
tf.keras.layers.Dense(NUM_CLASSES, activation='softmax')
])
return model
def scratch(train, val, learning_rate):
model = create_model()
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
earlystop_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
min_delta=0.0001,
patience=5)
model.fit(train,
epochs=NUM_EPOCHS,
steps_per_epoch=int(NUM_EXAMPLES/BATCH_SIZE),
validation_data=val,
validation_steps=1,
validation_freq=1,
callbacks=[tensorboard_callback, earlystop_callback])
return model
现在,是时候利用我们的管道进行各种实验了。
超参数如何影响准确性
在本节中,我们旨在逐一修改深度学习管道的各种参数——从微调的层数到使用的激活函数的选择——主要看其对验证准确性的影响。此外,当相关时,我们还观察其对训练速度和达到最佳准确性的时间(即收敛)的影响。
我们的实验设置如下:
-
为了减少实验时间,本章中我们使用了一个更快的架构——MobileNet。
-
我们将输入图像分辨率降低到 128 x 128 像素以进一步加快训练速度。一般来说,我们建议在生产系统中使用更高的分辨率(至少 224 x 224)。
-
如果实验连续 10 个时期准确率不增加,将应用早停。
-
对于使用迁移学习进行训练,通常解冻最后 33%的层。
-
学习率设置为 0.001,使用 Adam 优化器。
-
除非另有说明,我们主要使用牛津花卉 102 数据集进行测试。我们选择这个数据集是因为它相对难以训练,包含了大量类别(102 个)以及许多类别之间的相似之处,这迫使网络对特征进行细粒度理解以取得良好的效果。
-
为了进行苹果与苹果的比较,我们取特定实验中的最大准确性值,并将该实验中的所有其他准确性值相对于该最大值进行归一化。
基于这些和其他实验,我们总结了一份可操作的提示清单,可在下一个模型训练冒险中实施。这些内容可以在本书的 GitHub(参见http://PracticalDeepLearning.ai)上找到,还有交互式可视化。如果您有更多提示,请随时在推特上发表@PracticalDLBook或提交拉取请求。
迁移学习与从头开始训练
实验设置
训练两个模型:一个使用迁移学习,一个从头开始在相同数据集上训练。
使用的数据集
牛津花卉 102,结肠组织学
使用的架构
预训练的 MobileNet,自定义模型
图 5-10 显示了结果。

图 5-10。比较在不同数据集上进行迁移学习和训练自定义模型
以下是关键要点:
-
通过重复使用先前学习的特征,迁移学习可以使训练期间的准确性迅速提高。
-
尽管预计基于 ImageNet 上预训练模型的迁移学习在目标数据集也是自然图像时会起作用,但网络在早期层学习的模式对超出 ImageNet 范围的数据集也能奇妙地奏效。这并不一定意味着它会产生最佳结果,但可以接近。当图像与模型预训练的更多真实世界图像匹配时,我们可以相对快速地提高准确性。
迁移学习中微调层数的影响
实验设置
将可训练层的百分比从 0 变化到 100%
使用的数据集
牛津花卉 102
使用的架构
预训练的 MobileNet
图 5-11 显示了结果。

图 5-11。微调的层数对模型准确性的影响
以下是关键要点:
-
微调的层数越多,达到收敛所需的纪元越少,准确性越高。
-
微调的层数越多,每个纪元的训练时间就越长,因为涉及更多的计算和更新。
-
对于需要对图像进行细粒度理解的数据集,通过解冻更多层使其更具任务特定性是获得更好模型的关键。
数据大小对迁移学习的影响
实验设置
每次添加一个类别的图像
使用的数据集
猫与狗
使用的架构
预训练的 MobileNet
图 5-12 显示了结果。

图 5-12。每个类别数据量对模型准确性的影响
以下是关键要点:
-
即使每个类别只有三张图像,模型也能够以接近 90%的准确性进行预测。这显示了迁移学习在减少数据需求方面的强大作用。
-
由于 ImageNet 有几个猫和狗,所以在 ImageNet 上预训练的网络更容易适应我们的数据集。像牛津花卉 102 这样更困难的数据集可能需要更多的图像才能达到类似的准确性。
学习率的影响
实验设置
在 0.1、0.01、0.001 和 0.0001 之间变化学习率
使用的数据集
牛津花卉 102
使用的架构
预训练的 MobileNet
图 5-13 显示了结果。

图 5-13. 学习率对模型准确性和收敛速度的影响
以下是关键要点:
-
学习率过高,模型可能永远无法收敛。
-
学习率过低会导致收敛所需时间过长。
-
在快速训练中找到合适的平衡至关重要。
优化器的影响
实验设置
尝试可用的优化器,包括 AdaDelta、AdaGrad、Adam、梯度下降、动量和 RMSProp
使用的数据集
牛津花卉 102
使用的架构
预训练的 MobileNet
图 5-14 显示了结果。

图 5-14. 不同优化器对收敛速度的影响
以下是关键要点:
-
Adam 是更快收敛到高准确性的不错选择。
-
RMSProp 通常更适用于 RNN 任务。
批量大小的影响
实验设置
以 2 的幂变化批量大小
使用的数据集
牛津花卉 102
使用的架构
预训练
图 5-15 显示了结果。

图 5-15. 批量大小对准确性和收敛速度的影响
以下是关键要点:
-
批量大小越大,结果从一个时期到另一个时期的不稳定性就越大,波动也越大。但更高的准确性也会导致更高效的 GPU 利用率,因此每个时期的速度更快。
-
批量大小过低会减缓准确性的提升。
-
16/32/64 是很好的起始批量大小。
调整大小的影响
实验设置
将图像大小改为 128x128、224x224
使用的数据集
牛津花卉 102
使用的架构
预训练
图 5-16 显示了结果。

图 5-16. 图像大小对准确性的影响
以下是关键要点:
- 即使像素只有三分之一,验证准确性也没有显著差异。这一方面显示了 CNN 的稳健性。这可能部分是因为牛津花卉 102 数据集中有花朵的特写可见。对于对象在图像中占比较小的数据集,结果可能较低。
宽高比变化对迁移学习的影响
实验设置
拍摄具有不同宽高比(宽:高比)的图像,并将它们调整为正方形(1:1 宽高比)。
使用的数据集
猫与狗
使用的架构
预训练
图 5-17 显示了结果。

图 5-17. 图像中宽高比和对应准确性的分布
以下是关键要点:
-
最常见的宽高比是 4:3,即 1.33,而我们的神经网络通常在 1:1 的比例下进行训练。
-
神经网络对由调整为正方形形状引起的宽高比的轻微修改相对稳健。即使达到 2.0 的比例也能得到不错的结果。
自动调整工具以获得最大准确性
正如我们自 19 世纪以来所看到的,自动化总是导致生产力的提高。在本节中,我们研究可以帮助我们自动搜索最佳模型的工具。
Keras 调谐器
由于有许多潜在的超参数组合需要调整,找到最佳模型可能是一个繁琐的过程。通常,两个或更多参数可能会对收敛速度和验证准确性产生相关影响,因此逐个调整可能不会导致最佳模型。如果好奇心占了上风,我们可能想要同时对所有超参数进行实验。
Keras Tuner 用于自动化超参数搜索。我们定义了一个搜索算法,每个参数可以取的潜在值(例如,离散值或范围),我们要最大化的目标对象(例如,验证准确性),然后坐下来看程序开始训练。Keras Tuner 代表我们进行多次实验,改变参数,存储最佳模型的元数据。以下代码示例改编自 Keras Tuner 文档,展示了通过不同的模型架构进行搜索(在 2 到 10 层之间变化),以及调整学习率(在 0.1 到 0.001 之间):
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
from kerastuner.engine.hypermodel import HyperModel
from kerastuner.engine.hyperparameters import HyperParameters
# Input data
(x, y), (val_x, val_y) = keras.datasets.mnist.load_data()
x = x.astype('float32') / 255.
val_x = val_x.astype('float32') / 255.
# Defining hyper parameters
hp = HyperParameters()
hp.Choice('learning_rate', [0.1, 0.001])
hp.Int('num_layers', 2, 10)
# Defining model with expandable number of layers
def build_model(hp):
model = keras.Sequential()
model.add(layers.Flatten(input_shape=(28, 28)))
for _ in range(hp.get('num_layers')):
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
model.compile(
optimizer=keras.optimizers.Adam(hp.get('learning_rate')),
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
return model
hypermodel = RandomSearch(
build_model,
max_trials=20, # Number of combinations allowed
hyperparameters=hp,
allow_new_entries=False,
objective='val_accuracy')
hypermodel.search(x=x,
y=y,
epochs=5,
validation_data=(val_x, val_y))
# Show summary of overall best model
hypermodel.results_summary()
每次实验都会显示类似这样的数值:
> Hp values:
|-learning_rate: 0.001
|-num_layers: 6
┌──────────────┬────────────┬───────────────┐
│ Name │ Best model │ Current model │
├──────────────┼────────────┼───────────────┤
│ accuracy │ 0.9911 │ 0.9911 │
│ loss │ 0.0292 │ 0.0292 │
│ val_loss │ 0.227 │ 0.227 │
│ val_accuracy │ 0.9406 │ 0.9406 │
└──────────────┴────────────┴───────────────┘
在实验结束时,结果摘要提供了迄今为止进行的实验的快照,并保存了更多元数据。
Hypertuning complete - results in ./untitled_project
[Results summary]
|-Results in ./untitled_project
|-Ran 20 trials
|-Ran 20 executions (1 per trial)
|-Best val_accuracy: 0.9406
另一个重要的好处是能够实时在线跟踪实验并通过访问http://keras-tuner.appspot.com获取进展通知,获取 API 密钥(来自 Google App Engine),并在我们的 Python 程序中输入以下行以及真实的 API 密钥:
tuner.enable_cloud(api_key=api_key)
由于潜在的组合空间可能很大,随机搜索比网格搜索更受青睐,因为这是一种更实际的方式来在有限的实验预算上找到一个好的解决方案。但也有更快的方法,包括 Hyperband(Lisha Li 等人)的实现也在 Keras Tuner 中可用。
对于计算机视觉问题,Keras Tuner 包括可调整的应用程序,如 HyperResNet。
AutoAugment
另一个示例超参数是增强。要使用哪些增强?增强的幅度有多大?组合太多会使情况变得更糟吗?我们可以让人工智能来决定,而不是将这些决定留给人类。AutoAugment 利用强化学习来找到最佳的增强组合(如平移、旋转、剪切)以及应用的概率和幅度,以最大化验证准确性。(该方法由 Ekin D. Cubuk 等人应用,以得出新的 ImageNet 验证数据的最新技术成果。)通过在 ImageNet 上学习最佳的增强参数组合,我们可以轻松地将其应用到我们的问题上。
应用从 ImageNet 预先学习的增强策略非常简单:
from PIL import Image
from autoaugment import ImageNetPolicy
img = Image.open("cat.jpg")
policy = ImageNetPolicy()
imgs = [policy(img) for _ in range(8) ]
图 5-18 显示了结果。

图 5-18。通过在 ImageNet 数据集上学习的增强策略的输出
AutoKeras
随着人工智能自动化越来越多的工作,不足为奇它最终也可以自动设计人工智能架构。NAS 方法利用强化学习将小型架构块连接在一起,直到它们能够最大化目标函数;换句话说,我们的验证准确性。当前最先进的网络都基于 NAS,使人类设计的架构相形见绌。这一领域的研究始于 2017 年,2018 年更加注重使训练更快。现在有了 AutoKeras(Haifeng Jin 等人),我们也可以以相对容易的方式在我们的特定数据集上应用这种最先进的技术。
使用 AutoKeras 生成新的模型架构只需提供我们的图像和相关标签,以及一个完成作业的时间限制。在内部,它实现了几种优化算法,包括贝叶斯优化方法来搜索最佳架构:
!pip3 install autokeras
!pip3 install graphviz
from keras.datasets import mnist
from autokeras.image.image_supervised import ImageClassifier
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.reshape(x_test.shape + (1,))
clf = ImageClassifier(path=".",verbose=True, augment=False)
clf.fit(x_train, y_train, time_limit= 30 * 60) # 30 minutes
clf.final_fit(x_train, y_train, x_test, y_test, retrain=True)
y = clf.evaluate(x_test, y_test)
print(y)
# Save the model as a pickle file
clf.export_autokeras_model("model.pkl")
visualize('.')
训练后,我们都渴望了解新模型架构的样子。与我们通常看到的大多数干净的图像不同,这个看起来相当难以理解或打印出来。但我们相信的是,它能产生高准确性。
总结
在本章中,我们看到了一系列工具和技术,帮助探索改进 CNN 准确性的机会。通过建立迭代实验的案例,您了解到调整超参数如何带来最佳性能。由于有这么多超参数可供选择,我们随后看了自动化方法,包括 AutoKeras、AutoAugment 和 Keras Tuner。最重要的是,本章的核心代码结合了多个工具,存储在书的 GitHub 上(请参阅http://PracticalDeepLearning.ai),可以通过一行更改轻松调整到 100 多个数据集,并在浏览器中在线运行。此外,我们编制了一份可操作提示清单,以及在线托管的交互式实验,帮助您的模型获得一点额外优势。我们希望本章涵盖的内容能够使您的模型更加健壮,减少偏见,使其更易解释,并最终有助于负责任地发展人工智能。
第六章:最大化 TensorFlow 的速度和性能:一个便捷清单
生活就是要用手头的资源做到最好,优化就是游戏的名字。
关键不在于拥有一切,而在于明智地利用你的资源。也许我们真的想买那辆法拉利,但我们的预算只够买一辆丰田。不过你知道吗?通过正确的性能调优,我们可以让那家伙在纳斯卡比赛中飞驰!
让我们从深度学习世界来看这个问题。谷歌凭借其工程实力和能够煮沸海洋的 TPU 机架,在约 30 分钟内训练 ImageNet 创下了速度纪录!然而,仅仅几个月后,三名研究人员(Andrew Shaw、Yaroslav Bulatov 和 Jeremy Howard)带着口袋里的 40 美元,在公共云上只用了 18 分钟就训练完了 ImageNet!
我们可以从这些例子中得出的教训是,你拥有的资源量并不像你充分利用它们那样重要。关键在于用最大潜力来充分利用资源。这一章旨在作为一个潜在性能优化的便捷清单,我们在构建深度学习流水线的所有阶段都可以使用,并且在整本书中都会很有用。具体来说,我们将讨论与数据准备、数据读取、数据增强、训练以及最终推断相关的优化。
而这个故事始于两个词,也终于两个词...
GPU 饥饿
人工智能从业者经常问的一个问题是,“为什么我的训练速度这么慢?”答案往往是 GPU 饥饿。
GPU 是深度学习的生命线。它们也可能是计算机系统中最昂贵的组件。鉴于此,我们希望充分利用它们。这意味着 GPU 不应该等待来自其他组件的数据以进行处理。相反,当 GPU 准备好处理时,预处理的数据应该已经准备就绪并等待使用。然而,现实是 CPU、内存和存储通常是性能瓶颈,导致 GPU 的利用率不佳。换句话说,我们希望 GPU 成为瓶颈,而不是反过来。
为数千美元购买昂贵的 GPU 可能是值得的,但前提是 GPU 本身就是瓶颈。否则,我们可能还不如把钱烧了。
为了更好地说明这一点,考虑图 6-1。在深度学习流水线中,CPU 和 GPU 协作工作,彼此传递数据。CPU 读取数据,执行包括增强在内的预处理步骤,然后将其传递给 GPU 进行训练。它们的合作就像接力比赛,只不过其中一个接力选手是奥运会运动员,等待着一个高中田径选手传递接力棒。GPU 空闲的时间越长,资源浪费就越多。

图 6-1. GPU 饥饿,等待 CPU 完成数据准备
本章的大部分内容致力于减少 GPU 和 CPU 的空闲时间。
一个合理的问题是:我们如何知道 GPU 是否饥饿?两个方便的工具可以帮助我们回答这个问题:
nvidia-smi
这个命令显示 GPU 的统计信息,包括利用率。
TensorFlow Profiler + TensorBoard
这在 TensorBoard 中以时间线的形式交互式地可视化程序执行。
nvidia-smi
nvidia-smi的全称是 NVIDIA 系统管理接口程序,提供了关于我们珍贵 GPU 的详细统计信息,包括内存、利用率、温度、功耗等。这对于极客来说是一个梦想成真。
让我们来试一试:
$ nvidia-smi
图 6-2 展示了结果。

图 6-2。nvidia-smi的终端输出,突出显示 GPU 利用率
在训练网络时,我们感兴趣的关键数字是 GPU 利用率,文档中定义为过去一秒钟内 GPU 上执行一个或多个内核的时间百分比。51%实际上并不那么好。但这是在调用nvidia-smi时的瞬间利用率。我们如何持续监控这些数字?为了更好地了解 GPU 使用情况,我们可以使用watch命令每半秒刷新一次利用率指标(值得记住这个命令):
$ watch -n .5 nvidia-smi
注意
尽管 GPU 利用率是衡量我们流水线效率的一个很好的代理,但它本身并不能衡量我们如何充分利用 GPU,因为工作仍可能只使用 GPU 资源的一小部分。
因为盯着终端屏幕看数字跳动并不是分析的最佳方式,我们可以每秒轮询一次 GPU 利用率并将其转储到文件中。在我们的系统上运行任何与 GPU 相关的进程时运行大约 30 秒,然后通过按 Ctrl+C 停止它:
$ nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits -f
gpu_utilization.csv -l 1
现在,从生成的文件中计算中位数 GPU 利用率:
$ sort -n gpu_utilization.csv | grep -v '⁰$' | datamash median 1
提示
Datamash 是一个方便的命令行工具,可对文本数据文件执行基本的数字、文本和统计操作。您可以在https://www.gnu.org/software/datamash/找到安装说明。
nvidia-smi是在命令行上检查 GPU 利用率的最便捷方式。我们能否进行更深入的分析?原来,对于高级用户,TensorFlow 提供了一套强大的工具。
TensorFlow 分析器 + TensorBoard
TensorFlow 附带了tfprof(图 6-3),TensorFlow 分析器,帮助分析和理解训练过程,例如为模型中的每个操作生成详细的模型分析报告。但是命令行可能有点难以导航。幸运的是,TensorBoard 是一个基于浏览器的 TensorFlow 可视化工具套件,包括一个用于分析器的插件,让我们可以通过几次鼠标点击与网络进行交互式调试。其中包括 Trace Viewer,一个显示时间轴上事件的功能。它有助于调查资源在特定时间段内的精确使用情况并发现效率低下的地方。
注意
截至目前为止,TensorBoard 仅在 Google Chrome 中得到完全支持,可能不会在其他浏览器(如 Firefox)中显示分析视图。

图 6-3。TensorBoard 中分析器的时间轴显示 GPU 处于空闲状态,而 CPU 正在处理,以及 CPU 处于空闲状态而 GPU 正在处理
默认情况下,TensorBoard 启用了分析器。激活 TensorBoard 涉及一个简单的回调函数:
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="/tmp",
profile_batch=7)
model.fit(train_data,
steps_per_epoch=10,
epochs=2,
callbacks=[`tensorboard_callback`])
在初始化回调时,除非显式指定profile_batch,否则会对第二批进行分析。为什么是第二批?因为第一批通常比其余批次慢,这是由于一些初始化开销。
注意
需要重申的是,使用 TensorBoard 进行分析最适合 TensorFlow 的高级用户。如果您刚开始使用,最好使用nvidia-smi。(尽管nvidia-smi不仅提供 GPU 利用率信息,而且通常是大多数实践者使用的方式。)对于希望更深入了解硬件利用率指标的用户,NVIDIA Nsight 是一个很好的工具。
好了。有了这些工具,我们知道我们的程序需要一些调整,并有提高效率的空间。我们将在接下来的几节中逐个查看这些领域。
如何使用此检查表
在商业中,一个经常引用的建议是“无法衡量的东西无法改进”。这也适用于深度学习流水线。调整性能就像进行科学实验。您设置一个基准运行,调整一个旋钮,测量效果,并朝着改进的方向迭代。以下清单上的项目是我们的旋钮——有些快速简单,而其他一些更复杂。
要有效使用此清单,请执行以下操作:
-
隔离要改进的流水线部分。
-
在清单上找到相关的要点。
-
实施、实验,并观察运行时间是否减少。如果没有减少,则忽略更改。
-
重复步骤 1 至 3,直到清单耗尽。
一些改进可能微小,一些可能更为重大。但所有这些变化的累积效应希望能够实现更快、更高效的执行,最重要的是,为您的硬件带来更大的回报。让我们逐步查看深度学习流水线的每个领域,包括数据准备、数据读取、数据增强、训练,最后是推理。
性能清单
数据准备
-
“存储为 TFRecords”
-
“减少输入数据的大小”
-
“使用 TensorFlow 数据集”
数据读取
-
“使用 tf.data”
-
“预取数据”
-
“并行化 CPU 处理”
-
“并行化 I/O 和处理”
-
“启用非确定性排序”
-
“缓存数据”
-
“打开实验性优化”
-
“自动调整参数值”
数据增强
- “使用 GPU 进行增强”
训练
-
“使用自动混合精度”
-
“使用更大的批量大小”
-
“使用八的倍数”
-
“找到最佳学习率”
-
“使用 tf.function”
-
“过度训练,然后泛化”
-
“使用渐进采样”
-
“使用渐进增强”
-
“使用渐进调整大小”
-
-
“为硬件安装优化堆栈”
-
“优化并行 CPU 线程数量”
-
“使用更好的硬件”
-
“分布式训练”
-
“检查行业基准”
推理
-
“使用高效模型”
-
“量化模型”
-
“修剪模型”
-
“使用融合操作”
-
“启用 GPU 持久性”
注意
此清单的可打印版本可在PracticalDeepLearning.ai上找到。下次训练或部署模型时,可以将其用作参考。或者更好的是,通过与朋友、同事以及更重要的是您的经理分享,传播快乐。
数据准备
在进行任何训练之前,我们可以进行一些优化,这些优化与我们如何准备数据有关。
存储为 TFRecords
图像数据集通常由成千上万个小文件组成,每个文件大小几千字节。我们的训练管道必须逐个读取每个文件。这样做成千上万次会产生显著的开销,导致训练过程变慢。在使用旋转硬盘时,这个问题更加严重,因为磁头需要寻找每个文件的开头。当文件存储在像云这样的远程存储服务上时,这个问题会进一步恶化。这就是我们的第一个障碍所在!
为了加快读取速度,一个想法是将成千上万个文件合并成少数几个较大的文件。这正是 TFRecord 所做的。它将数据存储在高效的 Protocol Buffer(protobuf)对象中,使其更快速读取。让我们看看如何创建 TFRecord 文件:
# Create TFRecord files
import tensorflow as tf
from PIL import Image
import numpy as np
import io
cat = "cat.jpg"
img_name_to_labels = {'cat' : 0}
img_in_string = open(cat, 'rb').read()
label_for_img = img_name_to_labels['cat']
def getTFRecord(img, label):
feature = {
'label': _int64_feature(label),
'image_raw': _bytes_feature(img),
}
return tf.train.Example(features=tf.train.Features(feature=feature))
with tf.compat.v1.python_io.TFRecordWriter('img.tfrecord') as writer:
for filename, label in img_name_to_labels.items():
image_string = open(filename, 'rb').read()
tf_example = getTFRecord(image_string, label)
writer.write(tf_example.SerializeToString())
现在,让我们看看如何读取这些 TFRecord 文件:
# Reading TFRecord files
dataset = tf.data.TFRecordDataset('img.tfrecord')
ground_truth_info = {
'label': tf.compat.v1.FixedLenFeature([], tf.int64),
'image_raw': tf.compat.v1.FixedLenFeature([], tf.string),
}
def map_operation(read_data):
return tf.compat.v1.parse_single_example(read_data, ground_truth_info)
imgs = dataset.map(map_operation)
for image_features in imgs:
image_raw = image_features['image_raw'].numpy()
label = image_features['label'].numpy()
image = Image.open(io.BytesIO(image_raw))
image.show()
print(label)
那么,为什么不将所有数据合并到一个文件中,比如说 ImageNet?尽管读取成千上万个小文件会因涉及的开销而影响性能,但读取巨大文件同样不是一个好主意。它们降低了我们进行并行读取和并行网络调用的能力。将大型数据集分片(划分)为 TFRecord 文件的甜蜜点在大约 100 MB 左右。
减小输入数据的大小
具有大图像的图像数据集在传递到 GPU 之前需要调整大小。这意味着以下内容:
-
在每次迭代中重复使用 CPU 周期
-
我们的数据管道中消耗的 I/O 带宽以比所需更大的速率被重复使用
节省计算周期的一个好策略是在整个数据集上执行常见的预处理步骤一次(如调整大小),然后将结果保存在 TFRecord 文件中,以供所有未来运行使用。
使用 TensorFlow 数据集
对于常用的公共数据集,从 MNIST(11 MB)到 CIFAR-100(160 MB),再到 MS COCO(38 GB)和 Google Open Images(565 GB),下载数据是相当费力的(通常分布在多个压缩文件中)。想象一下,如果在慢慢下载文件的过程中,下载了 95%,然后连接变得不稳定并中断,你会感到多么沮丧。这并不罕见,因为这些文件通常托管在大学服务器上,或者从各种来源(如 Flickr)下载(就像 ImageNet 2012 一样,它提供了下载 150 GB 以上图像的 URL)。连接中断可能意味着需要重新开始。
如果你认为这很繁琐,那么真正的挑战实际上只有在成功下载数据之后才开始。对于每个新数据集,我们现在需要查阅文档,确定数据的格式和组织方式,以便适当地开始读取和处理。然后,我们需要将数据分割为训练、验证和测试集(最好转换为 TFRecords)。当数据太大而无法放入内存时,我们需要做一些手动操作来高效地读取并将其有效地提供给训练管道。我们从未说过这很容易。
或者,我们可以通过使用高性能、即用即用的 TensorFlow 数据集包来避免所有痛苦。有几个著名的数据集可用,它会下载、拆分并使用最佳实践来喂养我们的训练管道,只需几行代码。
让我们看看有哪些数据集可用。
import tensorflow_datasets as tfds
# See available datasets
print(tfds.list_builders())
===== Output =====
'abstract_reasoning', 'bair_robot_pushing_small', 'caltech101', 'cats_vs_dogs',
'celeb_a', 'celeb_a_hq', 'chexpert', 'cifar10', 'cifar100', 'cifar10_corrupted',
'cnn_dailymail', 'coco2014', 'colorectal_histology',
'colorectal_histology_large', 'cycle_gan' ...
截至目前,已有 100 多个数据集,这个数字还在稳步增长。现在,让我们下载、提取并使用 CIFAR-10 的训练集创建一个高效的管道:
train_dataset = tfds.load(name="cifar100", split=tfds.Split.TRAIN)
train_dataset = train_dataset.shuffle(2048).batch(64)
就是这样!第一次执行代码时,它将在我们的机器上下载并缓存数据集。对于以后的每次运行,它将跳过网络下载,直接从缓存中读取。
数据读取
现在数据准备好了,让我们寻找最大化数据读取管道吞吐量的机会。
使用 tf.data
我们可以选择使用 Python 的内置 I/O 库手动读取数据集中的每个文件。我们只需为每个文件调用open,然后就可以开始了,对吧?这种方法的主要缺点是我们的 GPU 将受到文件读取的限制。每次读取一个文件时,GPU 都需要等待。每次 GPU 开始处理其输入时,我们都需要等待下一个文件从磁盘中读取。看起来相当浪费,不是吗?
如果你只能从本章中学到一件事,那就是:tf.data是构建高性能训练管道的方法。在接下来的几节中,我们将探讨几个可以利用来提高训练速度的tf.data方面。
让我们为读取数据设置一个基本管道:
files = tf.data.Dataset.list_files("./training_data/*.tfrecord")
dataset = tf.data.TFRecordDataset(files)
dataset = dataset.shuffle(2048)
.repeat()
.map(lambda item: tf.io.parse_single_example(item, features))
.map(_resize_image)
.batch(64)
预取数据
在我们之前讨论的管道中,GPU 等待 CPU 生成数据,然后 CPU 等待 GPU 完成计算,然后再生成下一个周期的数据。这种循环依赖会导致 CPU 和 GPU 的空闲时间,这是低效的。
prefetch函数通过将数据的生成(由 CPU)与数据的消耗(由 GPU)分离,帮助我们。使用一个后台线程,它允许数据被异步传递到一个中间缓冲区,其中数据可以立即供 GPU 消耗。CPU 现在继续进行下一个计算,而不是等待 GPU。同样,一旦 GPU 完成了其先前的计算,并且缓冲区中有数据可用,它就开始处理。
要使用它,我们只需在管道的最后调用prefetch,并附加一个buffer_size参数(即可以存储的最大数据量)。通常buffer_size是一个小数字;在许多情况下,1就足够了:
dataset = dataset.prefetch(buffer_size=16)
在短短几页中,我们将向您展示如何找到这个参数的最佳值。
总之,如果有机会重叠 CPU 和 GPU 计算,prefetch将自动利用它。
并行化 CPU 处理
如果我们有多个核心的 CPU,但只使用其中一个核心进行所有处理,那将是一种浪费。为什么不利用其他核心呢?这正是map函数中的num_parallel_calls参数派上用场的地方:
dataset = dataset.map(lambda item: tf.io.parse_single_example(item, features),
`num_parallel_calls``=``4`)
这将启动多个线程来并行处理map()函数。假设后台没有运行重型应用程序,我们将希望将num_parallel_calls设置为系统上的 CPU 核心数。任何更多的设置可能会由于上下文切换的开销而降低性能。
并行化 I/O 和处理
从磁盘或更糟的是从网络中读取文件是瓶颈的主要原因。我们可能拥有世界上最好的 CPU 和 GPU,但如果我们不优化文件读取,那一切都将是徒劳的。解决这个问题的一个方法是并行化 I/O 和后续处理(也称为*交错处理)。
dataset = files.interleave(map_func, num_parallel_calls=4)
在这个命令中,发生了两件事:
-
输入数据是并行获取的(默认情况下等于系统上的核心数)。
-
在获取的数据上,设置
num_parallel_calls参数允许map_func函数在多个并行线程上执行,并异步从传入的数据中读取。
如果没有指定num_parallel_calls,即使数据是并行读取的,map_func也会在单个线程上同步运行。只要map_func的运行速度快于输入数据到达的速度,就不会有问题。如果map_func成为瓶颈,我们肯定希望将num_parallel_calls设置得更高。
启用非确定性排序
对于许多数据集,读取顺序并不重要。毕竟,我们可能会随机排列它们的顺序。默认情况下,在并行读取文件时,tf.data仍然尝试以固定的轮流顺序产生它们的输出。缺点是我们可能会在途中遇到“拖延者”(即,一个操作比其他操作花费更长时间,例如慢速文件读取,并阻止所有其他操作)。这就像在杂货店排队时,我们前面的人坚持使用现金并找零,而其他人都使用信用卡。因此,我们跳过拖延者,直到他们完成处理,而不是阻塞所有准备输出的后续操作。这打破了顺序,同时减少了等待少数较慢操作的浪费周期:
options = tf.data.Options()
options.experimental_deterministic = False
dataset = tf.data.Dataset.list_files("./training_data/")
dataset = dataset.with_options(options)
dataset = dataset.interleave(tf.data.TFRecordDataset, num_parallel_calls=4)
缓存数据
Dataset.cache()函数允许我们将数据复制到内存或磁盘文件中。有两个原因可能需要缓存数据集:
-
为了避免在第一个 epoch 之后重复从磁盘读取。这显然只在缓存在内存中且可以适应可用 RAM 时有效。
-
为了避免重复执行昂贵的 CPU 操作(例如,将大图像调整为较小尺寸)。
提示
缓存最适用于不会更改的数据。建议在任何随机增强和洗牌之前放置cache();否则,在最后进行缓存将导致每次运行中数据和顺序完全相同。
根据我们的情况,我们可以使用以下两行中的一行:
dataset = dataset.cache() # in-memory
dataset = dataset.cache(filename='tmp.cache') # on-disk
值得注意的是,内存中的缓存是易失性的,因此只在每次运行的第二个 epoch 中显示性能改进。另一方面,基于文件的缓存将使每次运行更快(超出第一次运行的第一个 epoch)。
提示
在“减少输入数据的大小”中,我们提到预处理数据并将其保存为 TFRecord 文件作为未来数据流水线的输入。在流水线中的预处理步骤之后直接使用cache()函数,只需在代码中进行一个单词的更改,就可以获得类似的性能。
打开实验性优化
TensorFlow 有许多内置的优化,通常最初是实验性的,并默认关闭。根据您的用例,您可能希望打开其中一些以从流水线中挤出更多性能。这些优化中的许多细节在tf.data.experimental.OptimizationOptions的文档中有详细说明。
注意
这里是关于过滤和映射操作的快速复习:
过滤
过滤操作逐个元素遍历列表,并抓取符合给定条件的元素。条件以 lambda 操作的形式提供,返回布尔值。
映射
映射操作只是接受一个元素,执行计算,并返回一个输出。例如,调整图像大小。
让我们看看一些可用的实验性优化,包括两个连续操作的示例,这些操作合并在一起作为一个单一操作可能会受益。
过滤融合
有时,我们可能想根据多个属性进行过滤。也许我们只想使用同时有狗和猫的图像。或者在人口普查数据集中,只查看收入超过一定门槛且距离市中心一定距离的家庭。filter_fusion可以帮助加快这种情况的速度。考虑以下示例:
dataset = dataset.filter(lambda x: x < 1000).filter(lambda x: x % 3 == 0)
第一个过滤器对整个数据集执行完整传递,并返回小于 1,000 的元素。在此输出上,第二个过滤器执行另一个传递以进一步删除不能被三整除的元素。我们可以将两个过滤操作合并为一个传递,而不是对许多相同元素执行两次传递,方法是使用AND操作。这正是filter_fusion选项所能实现的——将多个过滤操作合并为一个传递。默认情况下,它是关闭的。您可以使用以下语句启用它:
options = tf.data.Options()
options.experimental_optimization.filter_fusion = True
dataset = dataset.with_options(options)
映射和过滤融合
考虑以下示例:
dataset = dataset.map(lambda x: x * x).filter(lambda x: x % 2 == 0)
在这个示例中,map函数对整个数据集执行完整传递,计算每个元素的平方。然后,filter函数丢弃奇数元素。与执行两次传递(尤其是在这个特别浪费的示例中)相比,我们可以通过打开map_and_filter_fusion选项将 map 和 filter 操作合并在一起,使它们作为一个单元操作:
options.experimental_optimization.map_and_filter_fusion = True
映射融合
与前面两个示例类似,合并两个或多个映射操作可以防止对相同数据执行多次传递,而是将它们合并为单次传递:
options.experimental_optimization.map_fusion = True
自动调整参数值
您可能已经注意到,本节中的许多代码示例对一些参数具有硬编码值。针对手头问题和硬件的组合,您可以调整它们以实现最大效率。如何调整它们?一个明显的方法是逐个手动调整参数并隔离并观察每个参数对整体性能的影响,直到我们获得精确的参数集。但由于组合爆炸,要调整的旋钮数量很快就会失控。如果这还不够,我们精心调整的脚本在另一台机器上不一定会像在另一台机器上那样高效,因为硬件的差异,如 CPU 核心数量、GPU 可用性等。甚至在同一系统上,根据其他程序的资源使用情况,这些旋钮可能需要在不同运行中进行调整。
我们如何解决这个问题?我们做与手动调整相反的事情:自动调整。使用爬山优化算法(这是一种启发式搜索算法),此选项会自动找到许多tf.data函数参数的理想参数组合。只需使用tf.data.experimental.AUTOTUNE而不是手动分配数字。这是一个参数来统治它们所有。考虑以下示例:
dataset = dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
这不是一个优雅的解决方案吗?我们可以对tf.data管道中的几个其他函数调用执行相同的操作。以下是一个示例,结合了“数据读取”部分中的几个优化,以创建高性能数据管道:
options = tf.data.Options()
options.experimental_deterministic = False
dataset = tf.data.Dataset.list_files("/path/*.tfrecord")
dataset = dataset.with_options(options)
dataset = files.interleave(tf.data.TFRecordDataset,
num_parallel_calls=tf.data.experimental.AUTOTUNE)
dataset = dataset.map(preprocess,
num_parallel_calls=tf.data.experimental.AUTOTUNE)
dataset = dataset.cache()
dataset = dataset.repeat()
dataset = dataset.shuffle(2048)
dataset = dataset.batch(batch_size=64)
dataset = dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
数据增强
有时,我们可能没有足够的数据来运行我们的训练管道。即使有,我们可能仍然希望操纵图像以提高模型的鲁棒性—借助数据增强。让我们看看是否可以使这一步更快。
使用 GPU 进行增强
数据预处理管道可能足够复杂,以至于你可以写一本关于它们的整本书。图像转换操作,如调整大小、裁剪、颜色转换、模糊等通常在数据从磁盘读入内存后立即执行。鉴于这些都是矩阵转换操作,它们可能在 GPU 上表现良好。
OpenCV、Pillow 和内置的 Keras 增强功能是计算机视觉中最常用的用于处理图像的库。然而,这里有一个主要限制。它们的图像处理主要基于 CPU(尽管你可以编译 OpenCV 以与 CUDA 一起使用),这意味着管道可能没有充分利用底层硬件的真正潜力。
注意
截至 2019 年 8 月,正在努力将 Keras 图像增强转换为 GPU 加速。
我们可以探索一些不同的受 GPU 限制的选项。
tf.image 内置增强
tf.image提供了一些方便的增强函数,我们可以轻松地将其插入到tf.data流水线中。其中一些方法包括图像翻转、颜色增强(色调、饱和度、亮度、对比度)、缩放和旋转。考虑以下示例,改变图像的色调:
updated_image = tf.image.adjust_hue(image, delta = 0.2)
依赖tf.image的缺点是,与 OpenCV、Pillow 甚至 Keras 相比,功能要受限得多。例如,在tf.image中,用于图像旋转的内置函数只支持逆时针旋转 90 度的图像。如果我们需要能够按任意角度旋转,比如 10 度,我们就需要手动构建这个功能。另一方面,Keras 提供了这个功能。
作为tf.data流水线的另一种选择,NVIDIA 数据加载库(DALI)提供了一个由 GPU 处理加速的快速数据加载和预处理流水线。如图 6-4 所示,DALI 实现了包括在训练之前在 GPU 中调整图像大小和增强图像等几个常见步骤。DALI 与多个深度学习框架一起工作,包括 TensorFlow、PyTorch、MXNet 等,提供了预处理流水线的可移植性。
NVIDIA DALI
![NVIDIA DALI 流水线
图 6-4. NVIDIA DALI 流水线
此外,即使 JPEG 解码(一个相对繁重的任务)也可以部分利用 GPU,从而获得额外的提升。这是通过使用 nvJPEG 实现的,nvJPEG 是一个用于 JPEG 解码的 GPU 加速库。对于多 GPU 任务,随着 GPU 数量的增加,性能几乎呈线性增长。
NVIDIA 的努力在 MLPerf 中取得了创纪录的成绩(对机器学习硬件、软件和服务进行基准测试),在 80 秒内训练了一个 ResNet-50 模型。
训练
对于那些刚开始进行性能优化的人来说,最快的收获来自于改进数据流水线,这相对容易。对于已经快速提供数据的训练流水线,让我们来研究一下实际训练步骤的优化。
使用自动混合精度
“一行代码让你的训练速度提高两到三倍!”
深度学习模型中的权重通常以单精度存储,即 32 位浮点,或者更常见的称为 FP32。将这些模型放在内存受限的设备上,如手机,可能会很具挑战性。使模型变小的一个简单技巧是将其从单精度(FP32)转换为半精度(FP16)。当然,这些权重的代表能力会下降,但正如我们在本章后面展示的(“量化模型”)中所示,神经网络对于小的变化是有弹性的,就像它们对图像中的噪声有弹性一样。因此,我们可以获得更高效的模型,而几乎不损失准确性。事实上,我们甚至可以将表示减少到 8 位整数(INT8),而不会显著损失准确性,正如我们将在接下来的一些章节中看到的。
因此,如果我们可以在推断期间使用降低精度表示,那么在训练期间也可以这样做吗?从 32 位到 16 位表示实际上意味着可以提供双倍的内存带宽,双倍的模型大小,或者可以容纳双倍的批量大小。不幸的是,结果表明在训练期间天真地使用 FP16 可能会导致模型准确性显著下降,甚至可能无法收敛到最佳解决方案。这是因为 FP16 对于表示数字的范围有限。由于精度不足,如果训练期间对模型的任何更新足够小,将导致更新甚至不被注册。想象一下将 0.00006 添加到权重值为 1.1 的情况。使用 FP32,权重将正确更新为 1.10006。然而,使用 FP16,权重将保持为 1.1。相反,诸如修正线性单元(ReLU)之类的层的任何激活可能足够高,以至于 FP16 会溢出并达到无穷大(Python 中的NaN)。
应对这些挑战的简单答案是使用自动混合精度训练。在这种方法中,我们将模型存储为 FP32 作为主副本,并在 FP16 中执行训练的前向/后向传递。在执行每个训练步骤后,该步骤的最终更新然后被缩放回 FP32,然后应用于主副本。这有助于避免 FP16 算术的缺陷,并导致更低的内存占用和更快的训练(实验证明速度增加了两到三倍),同时实现与仅在 FP32 中训练相似的准确性水平。值得注意的是,像 NVIDIA Volta 和 Turing 这样的新型 GPU 架构特别优化 FP16 操作。
要在训练期间启用混合精度,我们只需在 Python 脚本的开头添加以下行:
os.environ['TF_ENABLE_AUTO_MIXED_PRECISION'] = '1'
使用更大的批量大小
与在一个批次中使用整个数据集进行训练不同,我们使用几个数据的小批量进行训练。这是出于两个原因:
-
我们的完整数据(单批次)可能无法适应 GPU 内存。
-
通过提供许多较小的批次,我们可以实现类似的训练准确性,就像通过提供较少的较大批次一样。
使用较小的小批量可能无法充分利用可用的 GPU 内存,因此对于这个参数进行实验,查看其对 GPU 利用率的影响(使用nvidia-smi命令),并选择最大化利用率的批量大小是至关重要的。像 NVIDIA 2080 Ti 这样的消费级 GPU 配备了 11 GB 的 GPU 内存,对于像 MobileNet 家族这样的高效模型来说是足够的。
例如,在具有 2080 Ti 显卡的硬件上,使用 224 x 224 分辨率图像和 MobileNetV2 模型,GPU 可以容纳高达 864 的批量大小。图 6-5 显示了从 4 到 864 不同批量大小对 GPU 利用率(实线)以及每个 epoch 的时间(虚线)的影响。正如我们在图中看到的那样,批量大小越大,GPU 利用率越高,导致每个 epoch 的训练时间更短。
即使在我们的最大批量大小为 864(在内存分配耗尽之前),GPU 利用率也不会超过 85%。这意味着 GPU 足够快,可以处理我们非常高效的数据管道的计算。将 MobileNetV2 替换为更重的 ResNet-50 模型立即将 GPU 利用率提高到 95%。

图 6-5。不同批量大小对每个 epoch 的时间(秒)以及 GPU 利用率的影响(X 轴和 Y 轴均采用对数刻度)。
提示
尽管我们展示了高达几百的批量大小,但大型工业训练负载通常使用更大的批量大小,借助一种称为层自适应速率缩放(LARS)的技术。例如,富士通研究在短短 75 秒内使用 2048 个 Tesla V100 GPU 和庞大的批量大小 81920 在 ImageNet 上训练了一个 ResNet-50 网络,使其 Top-1 准确率达到 75%!
使用 8 的倍数
深度学习中的大多数计算都是“矩阵乘法和加法”的形式。尽管这是一项昂贵的操作,但在过去几年中已经建立了专门的硬件来优化其性能。例如,谷歌的 TPU 和英伟达的张量核心(可以在图灵和伏尔塔架构中找到)。图灵 GPU 提供张量核心(用于 FP16 和 INT8 操作)以及 CUDA 核心(用于 FP32 操作),张量核心提供了显著更高的吞吐量。由于它们的专门性质,张量核心要求提供给它们的数据中的某些参数必须是 8 的倍数。以下是三个这样的参数:
-
卷积滤波器中的通道数
-
完全连接层中的神经元数量和该层的输入
-
小批量的大小
如果这些参数不能被 8 整除,GPU CUDA 核心将被用作备用加速器。根据英伟达报告的一个实验,将批量大小从 4,095 更改为 4,096 导致吞吐量增加了五倍。请记住,使用 8 的倍数(或在 INT8 操作中使用 16 的倍数),以及使用自动混合精度,是激活张量核心的最低要求。为了更高的效率,推荐值实际上是 64 或 256 的倍数。同样,谷歌建议在使用 TPU 时使用 128 的倍数以获得最大效率。
找到最佳学习率
一个极大影响我们收敛速度(和准确性)的超参数是学习率。训练的理想结果是全局最小值;也就是说,最小损失点。学习率过高可能会导致我们的模型超过全局最小值(就像一个疯狂摆动的钟摆),可能永远不会收敛。学习率过低可能会导致收敛时间过长,因为学习算法将朝着最小值迈出非常小的步骤。找到正确的初始学习率可以产生巨大的差异。
找到理想的初始学习率的朴素方法是尝试几种不同的学习率(例如 0.00001、0.0001、0.001、0.01、0.1)并找到一个比其他更快收敛的学习率。或者,更好的是,在一系列值上执行网格搜索。这种方法有两个问题:1)根据粒度的不同,可能会找到一个不错的值,但可能不是最优值;2)我们需要多次训练,这可能会耗费时间。
在 Leslie N. Smith 的 2015 年论文“用于训练神经网络的循环学习率”中,他描述了一种更好的策略来找到这个最佳学习率。总结如下:
-
从一个非常低的学习率开始,逐渐增加直到达到预定的最大值。
-
在每个学习率下观察损失——首先它会停滞,然后开始下降,最后会再次上升。
-
计算每个学习率下损失的减少率(一阶导数)。
-
选择具有最高损失减少率的点。
听起来好像有很多步骤,但幸运的是我们不需要为此编写代码。Pavel Surmenok 的keras_lr_finder库为我们提供了一个方便的函数来找到它:
lr_finder = LRFinder(model)
lr_finder.find(x_train, y_train, start_lr=0.0001, end_lr=10, batch_size=512,
epochs=5)
lr_finder.plot_loss(n_skip_beginning=20, n_skip_end=5)
图 6-6 显示了损失与学习率之间的关系图。很明显,学习率为 10^(-4)或 10^(-3)可能太低(因为损失几乎没有下降),同样,大于 1 可能太高(因为损失急剧增加)。

图 6-6。显示损失随学习率增加而变化的图表
我们最感兴趣的是损失减少最多的点。毕竟,我们希望尽量减少在训练过程中达到最小损失所花费的时间。在图 6-7 中,我们绘制了损失的变化速率 - 损失相对于学习率的导数:
# Show Simple Moving Average over 20 points to smoothen the graph
lr_finder.plot_loss_change(sma=20, n_skip_beginning=20, n_skip_end=5,
y_lim=(-0.01, 0.01))

图 6-7。显示损失随学习率增加而变化的图表
这些图表显示,值在 0.1 左右会导致损失最快下降,因此我们会选择它作为我们的最佳学习率。
使用 tf.function
默认情况下在 TensorFlow 2.0 中启用的急切执行模式允许用户逐行执行代码并立即看到结果。这在开发和调试中非常有帮助。这与 TensorFlow 1.x 形成对比,对于 TensorFlow 1.x,用户必须将所有操作构建为图形,然后一次执行它们以查看结果。这使得调试成为一场噩梦!
急切执行的灵活性是否会带来成本?是的,一个微小的成本,通常在微秒级别,对于像训练 ResNet-50 这样的大型计算密集型操作基本可以忽略不计。但是在有许多小操作的情况下,急切执行可能会产生较大的影响。
我们可以通过两种方法克服这个问题:
禁用急切执行
对于 TensorFlow 1.x,不启用急切执行将让系统优化程序流程作为图形并运行得更快。
使用 tf.function
在 TensorFlow 2.x 中,您无法禁用急切执行(有一个兼容性 API,但我们不应该将其用于除了从 TensorFlow 1.x 迁移之外的任何其他用途)。相反,任何可以通过在图模式下执行来加速的函数可以简单地用@tf.function进行注释。值得注意的是,在注释函数内调用的任何函数也将在图模式下运行。这使我们能够从基于图形的执行中获得加速的优势,而不会牺牲急切执行的调试能力。通常,最佳加速是在短时间的计算密集型任务中观察到的:
conv_layer = tf.keras.layers.Conv2D(224, 3)
def non_tf_func(image):
for _ in range(1,3):
conv_layer(image)
return
@tf.function
def tf_func(image):
for _ in range(1,3):
conv_layer(image)
return
mat = tf.zeros([1, 100, 100, 100])
# Warm up
non_tf_func(mat)
tf_func(mat)
print("Without @tf.function:", timeit.timeit(lambda: non_tf_func(mat),
number=10000), " seconds")
print("With @tf.function:", timeit.timeit(lambda: tf_func(mat), number=10000),
"seconds")
=====Output=====
Without @tf.function: 7.234016112051904 seconds
With @tf.function: 0.7510978290811181 seconds
正如我们在编造的例子中所看到的,简单地使用@tf.function给我们带来了 10 倍的加速,从 7.2 秒到 0.7 秒。
过度训练,然后泛化
在机器学习中,对数据集进行过度训练被认为是有害的。然而,我们将演示我们可以以受控的方式利用过度训练来使训练更快。
俗话说,“完美是好的敌人。”我们不希望我们的网络一开始就完美。事实上,我们甚至不希望它一开始就很好。我们真正想要的是它能够快速学习一些东西,即使不完美。因为这样我们就有了一个良好的基线,可以将其调整到最高潜力。实验证明,我们可以比传统训练更快地到达目的地。
注意
为了进一步澄清过度训练然后泛化的概念,让我们看一个关于语言学习的不完美类比。假设您想学习法语。一种方法是向您抛出一本词汇和语法书,希望您能记住所有内容。当然,您可能每天都会翻阅这本书,也许几年后,您可能能说一些法语。但这不是学习的最佳方式。
或者,我们可以看看语言学习程序如何处理这个过程。这些程序最初只向您介绍一小部分单词和语法规则。学会它们后,您将能够说一些断断续续的法语。也许您可以在餐厅要一杯咖啡,或者在公交车站问路。此时,您将不断地接触到更多的单词和规则,这将帮助您随着时间的推移不断提高。
这个过程类似于我们的模型如何逐渐学习更多数据。
我们如何迫使网络快速而不完美地学习?让它在我们的数据上过度训练。以下三种策略可以帮助。
使用渐进采样
一种过度训练然后泛化的方法是逐渐向模型展示越来越多原始训练集的内容。以下是一个简单的实现:
-
从数据集中取样(比如大约 10%)。
-
训练网络直到收敛;换句话说,直到在训练集上表现良好。
-
在更大的样本上进行训练(甚至整个训练集)。
通过反复显示数据集的较小样本,网络将更快地学习特征,但只与显示的样本相关。因此,它往往会过度训练,通常在训练集上表现比测试集更好。当发生这种情况时,将训练过程暴露给整个数据集将有助于泛化学习,最终测试集的性能会提高。
使用渐进增强
另一种方法是首先在整个数据集上进行训练,几乎没有数据增强,然后逐渐增加增强程度。
通过反复显示未增强的图像,网络会更快地学习模式,逐渐增加增强程度,使其更加稳健。
使用渐进调整大小
另一种方法,由 fast.ai 的 Jeremy Howard 提出(该网站提供免费的人工智能课程),是渐进式调整大小。这种方法背后的关键思想是首先在缩小像素尺寸的图像上进行训练,然后逐渐在越来越大的尺寸上进行微调,直到达到原始图像尺寸。
宽度和高度都减半的图像像素减少了 75%,理论上可以使训练速度比原始图像提高四倍。类似地,将原始高度和宽度缩小到四分之一在最好的情况下可以导致 16 倍的减少(精度较低)。较小的图像显示的细节较少,迫使网络学习更高级的特征,包括广泛的形状和颜色。然后,使用较大的图像进行训练将有助于网络学习更精细的细节,逐渐提高测试精度。就像一个孩子首先学习高级概念,然后逐渐在后来的岁月中暴露于更多细节一样,这个概念也适用于 CNN。
提示
您可以尝试结合任何这些方法,甚至构建自己的创造性方法,比如首先在一部分类别上进行训练,然后逐渐推广到所有类别。
为硬件安装优化的堆栈。
托管的开源软件包二进制文件通常是为了在各种硬件和软件配置上运行而构建的。这些软件包试图迎合最普遍的需求。当我们在软件包上执行 pip install 时,我们最终会下载并安装这个通用的、适用于所有人的二进制文件。这种便利是以无法利用特定硬件堆栈提供的特定功能为代价的。这个问题是避免安装预构建二进制文件的一个重要原因,而是选择从源代码构建软件包。
例如,Google 在pip上有一个单独的 TensorFlow 包,可以在旧的 Sandy Bridge(第二代 Core i3)笔记本电脑上运行,也可以在强大的 16 核 Intel Xeon 服务器上运行。尽管方便,但这种方法的缺点是该软件包无法充分利用 Xeon 服务器的强大硬件。因此,对于基于 CPU 的训练和推断,Google 建议从源代码编译 TensorFlow 以最佳优化手头的硬件。
手动执行此操作的一种方法是在构建源代码之前设置硬件的配置标志。例如,要启用 AVX2 和 SSE 4.2 指令集的支持,我们可以简单地执行以下构建命令(请注意命令中每个指令集前面的额外m字符):
$ bazel build -c opt --copt=-mavx2 --copt=-msse4.2
//tensorflow/tools/pip_package:build_pip_package
如何检查可用的 CPU 特性?使用以下命令(仅限 Linux):
$ lscpu | grep Flags
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36
clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm
constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid
aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16
xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt
tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch
cpuid_fault epb cat_l3 cdp_l3 invpcid_single pti intel_ppin ssbd ibrs ibpb stibp
tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2
erms invpcid rtm cqm rdt_a rdseed adx smap intel_pt xsaveopt cqm_llc
cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts md_clear
flush_l1d
使用适当的指令集指定构建标志从源代码构建 TensorFlow 应该会显著提高速度。这里的缺点是从源代码构建可能需要相当长的时间,至少几个小时。或者,我们可以使用 Anaconda 下载和安装由英特尔在他们的深度神经网络数学核心库(MKL-DNN)之上构建的高度优化的 TensorFlow 变体。安装过程非常简单。首先,我们安装Anaconda包管理器。然后,我们运行以下命令:
# For Linux and Mac
$ conda install tensorflow
# For Windows
$ conda install tensorflow-mkl
在 Xeon CPU 上,MKL-DNN 通常可以提供推理速度提升两倍以上。
关于 GPU 的优化怎么样?因为 NVIDIA 使用 CUDA 库来抽象化各种 GPU 内部的差异,通常不需要从源代码构建。相反,我们可以简单地从pip(tensorflow-gpu包)安装 GPU 变体的 TensorFlow。我们推荐使用Lambda Stack一键安装程序以方便安装(还包括 NVIDIA 驱动程序、CUDA 和 cuDNN)。
对于云端的训练和推断,AWS、微软 Azure 和 GCP 都提供了针对其硬件优化的 TensorFlow GPU 机器映像。快速启动多个实例并开始使用。此外,NVIDIA 还提供了用于本地和云端设置的 GPU 加速容器。
优化并行 CPU 线程的数量
比较以下两个例子:
# Example 1
X = tf.multiply(A, B)
Y = tf.multiply(C, D)
# Example 2
X = tf.multiply(A, B)
Y = tf.multiply(`X`, C)
在这些例子中有几个领域可以利用内在的并行性:
在操作之间
在例子 1 中,Y 的计算不依赖于 X 的计算。这是因为这两个操作之间没有共享数据,因此它们都可以在两个单独的线程上并行执行。
相比之下,在例子 2 中,Y 的计算取决于第一个操作(X)的结果,因此第二个语句在第一个语句完成执行之前无法执行。
用以下语句设置可用于操作间并行性的最大线程数配置:
tf.config.threading.set_inter_op_parallelism_threads(num_threads)
推荐的线程数等于机器上的 CPU(插槽)数量。可以使用lscpu命令(仅限 Linux)获取此值。
每个操作级别
我们还可以利用单个操作内的并行性。诸如矩阵乘法之类的操作本质上是可并行化的。
图 6-8 展示了一个简单的矩阵乘法操作。很明显,整体乘积可以分为四个独立的计算。毕竟,一个矩阵的一行与另一个矩阵的一列的乘积不依赖于其他行和列的计算。每个这样的分割都可能获得自己的线程,所有这四个线程可以同时执行。

图 6-8. A matrix multiplication for A x B operation with one of the multiplications highlighted
用以下语句设置用于操作内并行性的线程数配置:
tf.config.threading.set_intra_op_parallelism_threads(num_threads)
推荐的线程数等于每个 CPU 的核心数。你可以使用 Linux 上的lscpu命令获取这个值。
使用更好的硬件
如果你已经最大化了性能优化,但仍需要更快的训练速度,那么你可能已经准备好购买一些新的硬件。用固态硬盘替换旋转硬盘可以走很远,添加一个或多个更好的 GPU 也可以。还有,有时 CPU 可能是罪魁祸首。
实际上,你可能不需要花太多钱:像 AWS、Azure 和 GCP 这样的公共云都提供了租用强大配置的能力,每小时只需几美元。最重要的是,它们都预装了优化的 TensorFlow 堆栈。
当然,如果你有足够的资金或者有一个相当慷慨的费用账户,你可以直接跳过整个章节,购买 2-petaFLOPS 的 NVIDIA DGX-2。它重达 163 公斤(360 磅),其 16 个 V100 GPU(共 81920 个 CUDA 核心)消耗 10 千瓦的功率——相当于七台大型窗式空调。而且它的售价只有 40 万美元!

图 6-9. 价值 40 万美元的 NVIDIA DGX-2 深度学习系统
分发训练
“两行代码实现训练水平扩展!”
在单台只有一个 GPU 的机器上,我们只能走得这么远。即使是最强大的 GPU 在计算能力上也有上限。垂直扩展只能带我们走到这么远。相反,我们寻求水平扩展——在处理器之间分发计算。我们可以在多个 GPU、TPU 甚至多台机器之间进行这样的操作。事实上,这正是 Google Brain 的研究人员在 2012 年所做的,他们使用了 16000 个处理器来运行一个用于观看 YouTube 上猫的神经网络。
在 2010 年代初的黑暗时期,对 ImageNet 的训练通常需要几周甚至几个月的时间。多个 GPU 可以加快速度,但很少有人有技术知识来配置这样的设置。对于初学者来说,这几乎是不可能的。幸运的是,我们生活在 TensorFlow 2.0 的时代,设置分布式训练只需要引入两行代码:
mirrored_strategy = tf.distribute.MirroredStrategy()
with mirrored_strategy.scope():
model = tf.keras.applications.ResNet50()
model.compile(loss="mse", optimizer="sgd")
训练速度几乎与添加的 GPU 数量成比例增加(90-95%)。例如,如果我们添加了四个 GPU(具有相似的计算能力),理想情况下我们会注意到速度提高了超过 3.6 倍。
然而,单个系统只能支持有限数量的 GPU。那么多个节点,每个节点都有多个 GPU 呢?类似于MirroredStrategy,我们可以使用MultiWorkerMirroredStrategy。在构建云上集群时,这非常有用。表 6-1 展示了不同用例的几种分发策略。
表 6-1. 推荐的分发策略
| 策略 | 用例 |
|---|---|
MirroredStrategy |
单个节点有两个或更多 GPU |
MultiWorkerMirroredStrategy |
每个节点有一个或多个 GPU |
为了让集群节点之间能够相互通信以使用MultiWorkerMirroredStrategy,我们需要在每个主机上配置TF_CONFIG环境变量。这需要设置一个包含集群中所有其他主机的 IP 地址和端口的 JSON 对象。手动管理这个过程可能会出错,这就是 Kubernetes 等编排框架真正发挥作用的地方。
注意
来自 Uber 的开源 Horovod 库是另一个高性能且易于使用的分布框架。在下一节中看到的许多记录基准性能需要在多个节点上进行分布式训练,而 Horovod 的性能帮助它们获得了优势。值得注意的是,大多数行业使用 Horovod,特别是因为在早期版本的 TensorFlow 上进行分布式训练是一个更加复杂的过程。此外,Horovod 可以与所有主要的深度学习库一起工作,只需进行最少量的代码更改或专业知识。通常通过命令行配置,可以通过单个命令行在四个节点上运行一个分布式程序,每个节点有四个 GPU:
$ horovodrun -np 16 -H
server1:4,server2:4,server3:4,server4:4 python
train.py
检查行业基准测试
上世纪 80 年代有三样东西普遍受欢迎——长发、随身听和数据库基准测试。就像当前深度学习的热潮一样,数据库软件也在经历着一段大胆承诺的阶段,其中一些是营销炒作。为了对这些公司进行测试,引入了一些基准测试,其中最著名的是事务处理委员会(TPC)基准测试。当有人需要购买数据库软件时,他们可以依靠这个公共基准测试来决定在哪里花费公司的预算。这种竞争推动了快速创新,提高了每美元的速度和性能,使行业比预期更快地前进。
受 TPC 和其他基准测试的启发,一些系统基准测试被创建出来,以标准化机器学习性能报告。
DAWNBench
斯坦福的 DAWNBench 基准测试了在 ImageNet 上将模型训练到 93% Top-5 准确率所需的时间和成本。此外,它还对推理时间进行了时间和成本排行榜。值得赞赏的是,训练如此庞大的网络的性能改进速度之快。当 DAWNBench 最初于 2017 年 9 月开始时,参考条目在 13 天内以 2323.39 美元的成本进行了训练。从那时起仅仅一年半的时间里,尽管最便宜的训练成本低至 12 美元,最快的训练时间是 2 分钟 43 秒。最重要的是,大多数条目包含了训练源代码和优化,我们可以通过研究和复制来进一步指导。这进一步说明了超参数的影响以及我们如何利用云进行廉价和快速的训练而不会让银行破产。
表 6-2。截至 2019 年 8 月的 DAWNBench 条目,按将模型训练到 93% Top-5 准确率的最低成本排序
| 成本(美元) | 训练时间 | 模型 | 硬件 | 框架 |
|---|---|---|---|---|
| $12.60 | 2:44:31 | ResNet-50Google Cloud TPU | GCP n1-standard-2, Cloud TPU | TensorFlow 1.11 |
| $20.89 | 1:42:23 | ResNet-50Setu Chokshi(MS AI MVP) | Azure ND40s_v2 | PyTorch 1.0 |
| $42.66 | 1:44:34 | ResNet-50 v1GE Healthcare(Min Zhang) | 8*V100(单个 p3.16x 大) | TensorFlow 1.11 + Horovod |
| $48.48 | 0:29:43 | ResNet-50Andrew Shaw, Yaroslav Bulatov, Jeremy Howard | 32 * V100(4x - AWS p3.16x 大) | Ncluster + PyTorch 0.5 |
MLPerf
与 DAWNBench 类似,MLPerf 旨在对人工智能系统性能进行可重复和公平的测试。虽然比 DAWNBench 更新,但这是一个行业联盟,在硬件方面得到了更广泛的支持。它在两个分区中进行训练和推断的挑战:开放和闭合。闭合分区使用相同的模型和优化器进行训练,因此可以将原始硬件性能进行苹果对苹果的比较。另一方面,开放分区允许使用更快的模型和优化器,以实现更快的进展。与 DAWNBench 中更具成本效益的条目相比,在 MLPerf 中表现最佳的参与者可能对我们大多数人来说有些难以企及。性能最佳的 NVIDIA DGX SuperPod,由 96 个 DGX-2H 组成,共计 1,536 个 V100 GPU,价格在 3500 万至 4000 万美元的范围内。尽管 1024 个 Google TPU 本身可能价值数百万美元,但它们每个都可以按需在云上租用,价格为每小时 8 美元(截至 2019 年 8 月),导致不到 275 美元的净成本用于不到两分钟的训练时间。
表 6-3. 截至 2019 年 8 月 DAWNBench 上的关键闭式分区条目,显示 ResNet-50 模型达到 75.9% Top-1 准确率的训练时间
| 时间(分钟) | 提交者 | 硬件 | 加速器 | 加速器数量 |
|---|---|---|---|---|
| 1.28 | TPUv3 | TPUv3 | 1,024 | |
| 1.33 | NVIDIA | 96x DGX-2H | Tesla V100 | 1,536 |
| 8,831.3 | 参考 | Pascal P100 | Pascal P100 | 1 |
尽管上述两个基准测试都强调训练和推断(通常在更强大的设备上),但还有其他针对低功耗设备的推断特定竞赛,旨在最大化准确性和速度,同时降低功耗。在年度会议上举行,以下是其中一些比赛:
-
LPIRC:低功耗图像识别挑战
-
EDLDC:嵌入式深度学习设计竞赛
-
设计自动化会议(DAC)的系统设计比赛
推断
训练我们的模型只是游戏的一半。我们最终需要向用户提供预测。以下几点指导您使服务端更具性能。
使用高效的模型
深度学习竞赛传统上是为了提出最高准确性模型,登上排行榜并获得炫耀权。但从业者生活在一个不同的世界——为用户快速高效地提供服务的世界。在智能手机、边缘设备和每秒数千次调用的服务器等设备上,全面高效(模型大小和计算)至关重要。毕竟,许多设备可能无法提供半个千兆字节的 VGG-16 模型,该模型需要执行 300 亿次操作,甚至没有那么高的准确性。在众多预训练架构中,有些准确性较高但较大且资源密集,而其他一些提供适度准确性但更轻。我们的目标是选择可以在推断设备的可用计算能力和内存预算下提供最高准确性的架构。在图 6-10 中,我们希望选择位于左上角区域的模型。

图 6-10. 比较不同模型的大小、准确性和每秒操作次数(改编自 Alfredo Canziani、Adam Paszke 和 Eugenio Culurciello 的“深度神经网络模型在实际应用中的分析”)
通常,约 15 MB 的 MobileNet 系列是高效智能手机运行时的首选模型,更近期的版本如 MobileNetV2 和 MobileNetV3 比它们的前身更好。此外,通过改变 MobileNet 模型的超参数,如深度乘数,可以进一步减少计算量,使其成为实时应用的理想选择。自 2017 年以来,生成最优架构以最大化准确性的任务也已经通过 NAS 自动化。它帮助发现了多次打破 ImageNet 准确性指标的新(看起来相当晦涩的)架构。例如,基于 829 MB 的 PNASNet 架构的 FixResNeXt 在 ImageNet 上达到了惊人的 86.4%的 Top-1 准确性。因此,研究界自然会问 NAS 是否有助于找到针对移动设备调整的架构,最大化准确性同时最小化计算量。答案是肯定的——导致更快更好的模型,优化了手头的硬件。例如,MixNet(2019 年 7 月)胜过许多最先进的模型。请注意,我们从数十亿的浮点运算转变为数百万次(图 6-10 和图 6-11)。

图 6-11。由 Mingxing Tan 和 Quoc V. Le 撰写的论文“MixNet:混合深度卷积核”中的几个移动友好模型的比较
作为从业者,我们在哪里可以找到当前最先进的模型?PapersWithCode.com/SOTA展示了几个 AI 问题的排行榜,随着时间的推移比较了论文结果,以及模型代码。特别感兴趣的是那些参数数量少但准确率高的模型。例如,EfficientNet 以 6600 万参数获得了惊人的 Top-1 84.4%的准确率,因此它可能是在服务器上运行的理想选择。此外,ImageNet 测试指标是在 1,000 个类别上,而我们的情况可能只需要对少数类别进行分类。对于这些情况,一个更小的模型就足够了。列在 Keras Application(tf.keras.applications)、TensorFlow Hub 和 TensorFlow Models 中的模型通常有许多变体(输入图像尺寸、深度乘数、量化等)。
提示
谷歌 AI 研究人员发表论文后不久,他们会在TensorFlow Models存储库上发布论文中使用的模型。
量化模型
“将 32 位权重表示为 8 位整数,获得 2 倍更快,4 倍更小的模型”
神经网络主要由矩阵-矩阵乘法驱动。所涉及的算术通常相当宽容,即数值上的小偏差不会导致输出的显著波动。这使得神经网络对噪声相当稳健。毕竟,我们希望能够在图片中识别出一个苹果,即使在不太完美的光线下也能做到。当我们进行量化时,实质上是利用了神经网络的这种“宽容”特性。
在我们看不同的量化技术之前,让我们先试着建立对它的直觉。为了用一个简单的例子说明量化表示,我们将把 32 位浮点权重转换为 INT8(8 位整数)使用线性量化。显然,FP32 表示 2³²个值(因此需要 4 个字节来存储),而 INT8 表示 2⁸ = 256 个值(1 个字节)。进行量化:
-
找出神经网络中 FP32 权重所代表的最小值和最大值。
-
将此范围分为 256 个间隔,每个间隔对应一个 INT8 值。
-
计算一个将 INT8(整数)转换回 FP32 的缩放因子。例如,如果我们的原始范围是从 0 到 1,而 INT8 数字是 0 到 255,则缩放因子将为 1/256。
-
在每个区间中用 INT8 值替换 FP32 数字。此外,在推理阶段存储缩放因子,用于将 INT8 值转换回 FP32 值。这个缩放因子只需要为整个量化值组存储一次。
-
在推理计算期间,将 INT8 值乘以缩放因子以将其转换回浮点表示。图 6-12 展示了线性量化的一个例子,区间为[0, 1]。

图 6-12。将从 0 到 1 的 32 位浮点范围量化为 8 位整数范围,以减少存储空间
有几种不同的方法可以量化我们的模型,最简单的方法是将权重的位表示从 32 位减少到 16 位或更低。显而易见,将 32 位转换为 16 位意味着需要一半的内存大小来存储模型。同样,转换为 8 位将需要四分之一的大小。那么为什么不将其转换为 1 位并节省 32 倍的大小呢?嗯,尽管模型在一定程度上是宽容的,但随着每次减少,我们会注意到精度下降。在某个阈值之下,精度的降低呈指数增长(特别是在 8 位以下)。要在下面并仍然拥有一个有用的工作模型(如 1 位表示),我们需要遵循一个特殊的转换过程将它们转换为二值化神经网络。深度学习初创公司 XNOR.ai 已经成功将这种技术引入生产。微软嵌入式学习库(ELL)同样提供了这样的工具,对于像树莓派这样的边缘设备具有很大的价值。
量化有许多好处:
改进的内存使用
通过将模型量化为 8 位整数表示(INT8),我们通常可以减少 75%的模型大小。这使得在内存中存储和加载模型更加方便。
性能改善
整数运算比浮点运算更快。此外,内存使用的节省减少了在执行期间从 RAM 卸载模型的可能性,这也附带减少了功耗消耗的好处。
可移植性
边缘设备,如物联网设备,可能不支持浮点运算,因此在这种情况下将模型保持为浮点是不可行的。
大多数推理框架提供了量化的方法,包括苹果的 Core ML 工具,NVIDIA 的 TensorRT(用于服务器),以及谷歌的 TensorFlow Lite,以及谷歌的 TensorFlow 模型优化工具包。使用 TensorFlow Lite,模型可以在训练后转换期间进行量化(称为后训练量化)。为了进一步减少精度损失,我们可以在训练期间使用 TensorFlow 模型优化工具包。这个过程称为量化感知训练。
衡量量化带来的好处是很有用的。来自TensorFlow Lite 模型优化基准测试的指标(在表 6-4 中显示)给了我们一个提示,比较了 1)未量化,2)后训练量化,和 3)量化感知训练模型。性能是在 Google Pixel 2 设备上测量的。
表 6-4。不同量化策略(8 位)对模型的影响(来源:TensorFlow Lite 模型优化文档)
| 模型 | MobileNet | MobileNetV2 | InceptionV3 |
|---|---|---|---|
| Top-1 准确率 | 原始 | 0.709 | 0.719 |
| 后训练量化 | 0.657 | 0.637 | 0.772 |
| 量化感知训练 | 0.7 | 0.709 | 0.775 |
| 延迟(毫秒) | 原始 | 124 | 89 |
| 后训练量化 | 112 | 98 | 845 |
| 量化感知训练 | 64 | 54 | 543 |
| 尺寸(MB) | 原始 | 16.9 | 14 |
| 优化 | 4.3 | 3.6 | 23.9 |
那么,这些数字表示什么?在使用 TensorFlow Lite 进行 INT8 量化后,我们看到尺寸大约减小了四倍,运行时间大约加快了两倍,准确性变化小于 1%。不错!
更极端的量化形式,如 1 位二值化神经网络(如 XNOR-Net),在 AlexNet 上测试时声称速度提高了 58 倍,尺寸大约减小了 32 倍,准确性损失了 22%。
修剪模型
选一个数字。将它乘以 0。我们得到什么?零。再将你选择的数字乘以一个接近 0 的小值,比如 10^-6,我们仍然会得到一个微不足道的值。如果我们用 0 替换这样微小的权重(→ 0)在一个模型中,它对模型的预测应该几乎没有影响。这被称为基于幅度的权重修剪,或简称修剪,是一种模型压缩形式。从逻辑上讲,在全连接层中在两个节点之间放置一个权重为 0 等同于删除它们之间的边。这使得具有密集连接的模型更加稀疏。
事实上,模型中大部分权重接近 0。修剪模型将导致许多这些权重被设置为 0。这对准确性几乎没有影响。虽然这本身并不节省任何空间,但在将模型保存到像 ZIP 这样的压缩格式的磁盘时,它引入了大量冗余,可以被利用。 (值得注意的是,压缩算法擅长重复模式。重复次数越多,可压缩性就越高。)最终结果是我们的模型通常可以压缩四倍。当我们最终需要使用模型时,需要在加载到内存进行推断之前对其进行解压缩。
TensorFlow 团队在修剪模型时观察到了表 6-5 中显示的准确性损失。如预期的那样,像 MobileNet 这样更高效的模型与相对较大的模型(如 InceptionV3)相比,观察到更高(尽管仍然很小)的准确性损失。
表 6-5。模型准确性损失与修剪百分比
| 模型 | 稀疏度 | 相对于原始准确性的准确性损失 |
|---|---|---|
| InceptionV3 | 50% | 0.1% |
| InceptionV3 | 75% | 2.5% |
| InceptionV3 | 87.5% | 4.5% |
| MobileNet | 50% | 2% |
Keras 提供了 API 来修剪我们的模型。这个过程可以在训练过程中进行迭代。正常训练一个模型或选择一个预训练模型。然后,定期修剪模型并继续训练。在定期修剪之间有足够的时代允许模型从引入如此多的稀疏性造成的任何损害中恢复过来。稀疏度和修剪之间的时代数量可以被视为要调整的超参数。
另一种实现这一点的方法是使用Tencent 的 PocketFlow工具,这是一个一行命令,提供了最近研究论文中实现的几种其他修剪策略。
使用融合操作
在任何严肃的 CNN 中,卷积层和批量归一化层经常一起出现。它们有点像 CNN 层的劳雷尔和哈迪。从根本上讲,它们都是线性操作。基本线性代数告诉我们,组合两个或更多线性操作也将导致一个线性操作。通过组合卷积和批量归一化层,我们不仅减少了计算量,还减少了在数据传输中花费的时间,包括主存储器和 GPU 之间,以及主存储器和 CPU 寄存器/缓存之间。将它们合并为一个操作可以防止额外的往返。幸运的是,对于推断目的,大多数推断框架要么自动执行这个融合步骤,要么提供模型转换器(如 TensorFlow Lite)在将模型转换为推断格式时进行这种优化。
启用 GPU 持久性
加载和初始化 GPU 驱动程序需要时间。您可能已经注意到,每次启动训练或推理作业时都会有延迟。对于频繁且短暂的作业来说,开销可能会迅速变得相对昂贵。想象一下一个图像分类程序,分类需要 10 秒,其中有 9.9 秒用于加载驱动程序。我们需要的是 GPU 驱动程序在后台保持预初始化,并在我们的训练作业启动时随时准备好。这就是 NVIDIA GPU Persistence Daemon 发挥作用的地方:
$ nvidia-persistenced --user {YOUR_USERNAME}
我们的 GPU 在空闲时间会消耗更多的瓦特,但它们将在下次启动程序时准备好并可用。
总结
在本章中,我们探讨了改进深度学习流程速度和性能的不同途径,从存储和读取数据到推理。慢速数据流通常导致 GPU 缺乏数据,导致空闲周期。通过我们讨论的几种简单优化方法,我们的硬件可以发挥最大效率。这个方便的清单可以作为一个方便的参考。请随意为您的桌子(或冰箱)制作一份副本。通过这些学习,我们希望看到您的名字出现在 MLPerf 基准测试列表的前列。
第七章:实用工具、技巧和窍门
本章包含我们作为作者在专业工作中以及在撰写本书过程中遇到的材料,主要是在实验过程中。这里涵盖的材料不一定适用于任何单独的章节;相反,这些材料是深度学习从业者在日常工作中可能会发现有用的,涵盖了各种任务的实用指南,包括设置环境、训练、模型互操作性、数据收集和标记、代码质量、管理实验、团队协作实践、隐私以及进一步探索主题。
由于人工智能领域变化迅速,本章是书籍 Github 存储库(请参见http://PracticalDeepLearning.ai)中的“活动”文档的一个小子集,该文档位于code/chapter-9,正在不断发展。如果您有更多问题,或者更好的答案可以帮助其他读者,请随时在 Twitter 上发送推文@PracticalDLBook或提交拉取请求。
安装
Q: 我在 GitHub 上发现了一个有趣且有用的 Jupyter Notebook。要使代码运行,需要克隆存储库、安装软件包、设置环境等多个步骤。有没有一种即时的方法可以交互式运行它?
只需将 Git 存储库 URL 输入到 Binder(mybinder.org),它将把它转换为一组交互式笔记本。在幕后,它会搜索存储库根目录中的requirements.txt或environment.yml等依赖文件。这将用于构建一个 Docker 镜像,以帮助在浏览器中交互式运行笔记本。
Q: 在新的 Ubuntu 机器上使用 NVIDIA GPU,快速搭建深度学习环境的最快方法是什么?
如果pip install tensorflow-gpu能解决所有问题,那该多好啊。然而,这离现实还很远。在新安装的 Ubuntu 机器上,列出所有安装步骤至少需要三页以上的时间来跟随,包括安装 NVIDIA GPU 驱动程序、CUDA、cuDNN、Python、TensorFlow 和其他软件包。然后需要仔细检查 CUDA、cuDNN 和 TensorFlow 之间的版本互操作性。很多时候,这会导致系统崩溃。可以说是一种痛苦的世界!
如果两行代码就能轻松解决所有问题,那不是很棒吗?有求必应:
$ sudo apt update && sudo ubuntu-drivers autoinstall && sudo reboot
$ export LAMBDA_REPO=$(mktemp) \
&& wget -O${LAMBDA_REPO} \
https://lambdalabs.com/static/misc/lambda-stack-repo.deb \
&& sudo dpkg -i ${LAMBDA_REPO} && rm -f ${LAMBDA_REPO} \
&& sudo apt-get update && sudo apt-get install -y lambda-stack-cuda \
&& sudo reboot
第一行确保所有驱动程序都已更新。第二行是由总部位于旧金山的 Lambda Labs 提供的。该命令设置 Lambda Stack,安装了 TensorFlow、Keras、PyTorch、Caffe、Caffe2、Theano、CUDA、cuDNN 和 NVIDIA GPU 驱动程序。因为公司需要在成千上万台机器上安装相同的深度学习软件包,所以它使用了一行命令自动化了这个过程,然后开源了它,以便其他人也可以使用。
Q: 在 Windows PC 上安装 TensorFlow 的最快方法是什么?
-
安装 Anaconda Python 3.7。
-
在命令行上运行
conda install tensorflow-gpu。 -
如果您没有 GPU,运行
conda install tensorflow。
基于 CPU 的 Conda 安装的一个额外好处是它安装了经过优化的 Intel MKL TensorFlow,比使用pip install tensorflow得到的版本运行更快。
Q: 我有一块 AMD GPU。在现有系统上,我能从 TensorFlow 的 GPU 加速中受益吗?
尽管大多数深度学习领域使用 NVIDIA GPU,但有越来越多的人群在 AMD 硬件上运行,并借助 ROCm 堆栈。使用命令行进行安装很简单:
-
sudo apt install rocm-libs miopen-hip cxlactivitylogger -
sudo apt install wget python3-pip -
pip3 install --user tensorflow-rocm
Q: 忘记安装,我在哪里可以获得预安装的深度学习容器?
Docker 与设置环境是同义词。Docker 帮助运行与工具、库和配置文件捆绑在一起的隔离容器。在选择来自主要云提供商(如 AWS、Microsoft Azure、GCP、阿里巴巴等)的虚拟机(VM)时,有几个深度学习 Docker 容器可用于立即开始工作。NVIDIA 还免费提供 NVIDIA GPU 云容器,这些容器与用于在 MLPerf 基准测试中打破训练速度记录的高性能容器相同。您甚至可以在桌面机器上运行这些容器。
训练
Q: 我不喜欢不断盯着屏幕检查我的训练是否已完成。我能否在手机上收到通知警报?
使用Knock Knock,这是一个 Python 库,正如其名称所示,当您的训练结束(或程序崩溃)时,通过电子邮件、Slack 甚至 Telegram 发送警报通知您!最重要的是,只需向您的训练脚本添加两行代码即可。不再需要打开程序一千次来检查训练是否已完成。
Q: 我更喜欢图形和可视化而不是纯文本。我能否获得我的训练过程的实时可视化?
FastProgress 进度条(最初由 Sylvain Gugger 为 fast.ai 开发)来拯救。
Q: 我进行大量的实验迭代,经常忘记每次实验之间的变化以及变化的影响。如何以更有组织的方式管理我的实验?
软件开发通过版本控制具有保留历史更改记录的能力。不幸的是,机器学习并没有同样的奢侈。现在通过 Weights and Biases 和 Comet.ml 等工具,情况正在改变。它们允许您跟踪多次运行,并记录训练曲线、超参数、输出、模型、注释等,只需向您的 Python 脚本添加两行代码。最重要的是,通过云的力量,即使您远离机器,也可以方便地跟踪实验并与他人分享结果。
Q: 如何检查 TensorFlow 是否在我的机器上使用 GPU?
使用以下方便的命令:
tf.test.is_gpu_available()
Q: 我的机器上有多个 GPU。我不希望我的训练脚本占用所有 GPU。如何限制我的脚本仅在特定 GPU 上运行?
使用 CUDA_VISIBLE_DEVICES=GPU_ID。只需在训练脚本命令前加上以下前缀:
$ CUDA_VISIBLE_DEVICES=GPU_ID python train.py
或者,在您的培训脚本中的早期写入以下行:
import os
os.environ["CUDA_VISIBLE_DEVICES"]="GPU_ID"
GPU_ID可以具有值如 0、1、2 等。您可以使用nvidia-smi命令查看这些 ID(以及 GPU 使用情况)。要分配给多个 GPU,使用逗号分隔的 ID 列表。
Q: 有时候感觉在训练时有太多旋钮要调整。是否可以自动完成,以获得最佳准确性?
有许多自动化超参数调整的选项,包括针对 Keras 的 Hyperas 和 Keras Tuner,以及更通用的框架,如 Hyperopt 和贝叶斯优化,执行广泛的实验以更智能地最大化我们的目标(即在我们的情况下最大化准确性)比简单的网格搜索更智能。
Q: ResNet 和 MobileNet 对我的用例已经足够好了。是否可能构建一个可以在我的情况下实现更高准确度的模型架构?
三个词:神经架构搜索(NAS)。让算法为您找到最佳架构。NAS 可以通过 Auto-Keras 和 AdaNet 等软件包实现。
Q: 如何调试我的 TensorFlow 脚本?
答案就在问题中:TensorFlow 调试器(`tfdbg)。
模型
Q: 我想快速了解我的模型的输入和输出层,而不必编写代码。我该如何实现?
使用 Netron。它以图形方式显示您的模型,并在点击任何层时提供有关架构的详细信息。
Q: 我需要发表一篇研究论文。我应该使用哪个工具来绘制我的有机、自由范围、无麸质模型架构?
当然是 MS Paint!不,我们只是开玩笑。我们也喜欢 NN-SVG 和 PlotNeuralNet,用于创建高质量的 CNN 图表。
Q: 是否有一个一站式的所有模型的平台?
确实!探索PapersWithCode.com、ModelZoo.co 和 ModelDepot.io,获取一些灵感。
Q: 我已经训练好了我的模型。如何让其他人可以使用它?
你可以开始让模型可以从 GitHub 下载。然后将其列在前面答案中提到的模型动物园中。为了更广泛地采用,将其上传到 TensorFlow Hub(tfhub.dev)。
除了模型之外,您应该发布一个“模型卡片”,它本质上就像模型的简历。这是一个详细介绍作者信息、准确度指标和基准数据集的简短报告。此外,它提供了关于潜在偏见和超出范围用途的指导。
Q: 我之前在 X 框架中训练过一个模型,但我需要在 Y 框架中使用它。我需要浪费时间在 Y 框架中重新训练吗?
不需要。你只需要 ONNX 的力量。对于不在 TensorFlow 生态系统中的模型,大多数主要的深度学习库都支持将它们保存为 ONNX 格式,然后可以将其转换为 TensorFlow 格式。微软的 MMdnn 可以帮助进行这种转换。
数据
Q: 我能在几分钟内收集关于一个主题的数百张图像吗?
是的,你可以使用一个名为 Fatkun Batch Download Image 的 Chrome 扩展,在三分钟或更短的时间内收集数百张图像。只需在你喜欢的图像搜索引擎中搜索一个关键词,按正确的使用权限(例如,公共领域)过滤图像,然后按下 Fatkun 扩展下载所有图像。参见第十二章,在那里我们使用它构建了一个 Not Hotdog 应用程序。
额外提示:要从单个网站下载,请搜索一个关键词,后面跟着 site:网站地址。例如,“马 site:flickr.com”。
Q: 忘掉浏览器。如何使用命令行从 Google 爬取图像?
$ pip install google_images_download
$ googleimagesdownload -k=horse -l=50 -r=labeled-for-reuse
-k、-l和-r分别是keyword、limit(图像数量)和usage_rights的缩写。这是一个功能强大的工具,有许多选项可以控制和过滤从 Google 搜索中下载的图像。此外,它保存了搜索引擎链接的原始图像,而不仅仅是加载 Google Images 显示的缩略图。要保存超过 100 张图像,请安装selenium库以及chromedriver。
Q: 这些工具不足以收集图像。我需要更多控制。还有哪些工具可以帮助我以更自定义的方式下载数据,超越搜索引擎?
带有 GUI(无需编程):
易于 GUI 识别要提取的元素规则
基于 Chrome 的爬虫扩展,特别用于从单个网站提取结构化输出
基于云的可扩展爬虫,用于并行、大型任务
基于 Python 的编程工具:
对于更可编程的爬取控制,这是最著名的爬虫之一。与构建自己的天真爬虫来探索网站相比,它提供了按域名、代理和 IP 进行节流速率的功能;可以处理 robots.txt;提供了在向 Web 服务器显示的浏览器标头方面的灵活性;并处理了几种可能的边缘情况。
InstaLooter
一个用于爬取 Instagram 的基于 Python 的工具。
Q: 我有目标类别的图像,但现在需要负类别(非项目/背景)的图像。有什么快速方法可以构建一个负类别的大数据集?
ImageN提供了 1,000 张图片——200 个 ImageNet 类别的 5 张随机图片——您可以将其用作负类。如果需要更多,请从 ImageNet 中以编程方式下载随机样本。
Q: 我如何搜索适合我的需求的预构建数据集?
尝试 Google 数据集搜索、VisualData.io和DatasetList.com。
Q: 对于像 ImageNet 这样的数据集,下载、弄清楚格式,然后加载它们进行训练需要太多时间。有没有一种简单的方法来读取流行的数据集?
TensorFlow 数据集是一个不断增长的数据集集合,可以与 TensorFlow 一起使用。其中包括 ImageNet、COCO(37 GB)和 Open Images(565 GB)等。这些数据集被公开为tf.data.Datasets,并提供了高性能的代码来将它们输入到您的训练流程中。
Q: 在数百万张 ImageNet 图片上进行训练将需要很长时间。有没有一个更小的代表性数据集,我可以尝试进行训练,以便快速进行实验和迭代?
尝试Imagenette。由 fast.ai 的 Jeremy Howard 创建,这个 1.4 GB 的数据集只包含 10 个类别,而不是 1,000 个。
Q: 有哪些可用于训练的最大数据集?
-
腾讯 ML 图片:1,770 万张图片,带有 11,000 个类别标签
-
Open Images V4(来自 Google):19.7 K 类别中的 9 百万张图片
-
BDD100K(来自加州大学伯克利分校):来自 100,000 个驾驶视频的图像,超过 1,100 小时
-
YFCC100M(来自雅虎):99.2 百万张图片
Q: 有哪些现成的大型视频数据集可以使用?
| 名称 | 详情 |
|---|---|
| YouTube-8M | 6.1 百万个视频,3,862 个类别,26 亿个视听特征每个视频 3.0 个标签 1.53 TB 的随机抽样视频 |
| Something Something(来自 Twenty Billion Neurons) | 174 个动作类别中的 221,000 个视频,例如“将水倒入酒杯但错过了,所以洒在旁边”人们用日常物品执行预定义的动作 |
| Jester(来自 Twenty Billion Neurons) | 27 个类别中的 148,000 个视频,例如“用两根手指放大”在网络摄像头前预定义的手势 |
Q: 这些是有史以来组装的最大标记数据集吗?
没有!像 Facebook 和 Google 这样的公司会精心策划自己的私人数据集,这些数据集比我们可以使用的公共数据集要大得多:
-
Facebook:35 亿张带有嘈杂标签的 Instagram 图片(首次报道于 2018 年)
-
谷歌 - JFT-300M:30 亿张带有嘈杂标签的图片(首次报道于 2017 年)
遗憾的是,除非你是这些公司的员工,否则你实际上无法访问这些数据集。我们必须说,这是一个不错的招聘策略。
Q: 我如何获得帮助注释数据?
有几家公司可以帮助标记不同类型的注释。值得一提的几家公司包括 SamaSource、Digital Data Divide 和 iMerit,他们雇用那些机会有限的人,最终通过在贫困社区就业来创造积极的社会经济变革。
Q: 是否有用于数据集的版本控制工具,就像 Git 用于代码一样?
Qri 和 Quilt 可以帮助版本控制我们的数据集,有助于实验的可重复性。
Q: 如果我没有一个大型数据集来解决我的独特问题怎么办?
尝试为训练开发一个合成数据集!例如,找到感兴趣对象的逼真 3D 模型,并使用 Unity 等 3D 框架将其放置在逼真的环境中。调整光照和相机位置、缩放和旋转,从多个角度拍摄这个对象的快照,生成无限的训练数据。此外,像 AI.Reverie、CVEDIA、Neuromation、Cognata、Mostly.ai 和 DataGen Tech 这样的公司提供了用于训练需求的逼真模拟。合成训练数据的一个重要好处是标记过程内置于合成过程中。毕竟,您会知道自己在创造什么。与手动标记相比,这种自动标记可以节省大量金钱和精力。
隐私
Q: 如何开发一个更注重隐私的模型而不深入研究密码学?
TensorFlow Encrypted 可能是您正在寻找的解决方案。它可以使用加密数据进行开发,这在云端尤为重要。内部,大量的安全多方计算和同态加密实现了隐私保护的机器学习。
Q: 我可以防止别人窥探我的模型吗?
嗯,除非你在云端,权重是可见的并且可以被反向工程。在智能手机上部署时,使用 Fritz 库来保护您模型的知识产权。
教育和探索
Q: 我想成为人工智能专家。除了这本书,我应该在哪里投入更多时间学习?
在互联网上有几个资源可以深入学习深度学习。我们强烈推荐这些来自一些最好的老师的视频讲座,涵盖了从计算机视觉到自然语言处理等各种应用领域。
-
Fast.ai(由 Jeremy Howard 和 Rachel Thomas 创建)提供了一个免费的 14 集视频讲座系列,采用更多的 PyTorch 实践学习方法。除了课程,还有一整套工具和一个活跃的社区,已经导致了许多研究论文和可直接使用的代码的突破(比如使用 fast.ai 库训练最先进网络只需三行代码)。
-
Deeplearning.ai(由 Andrew Ng 创建)提供了一个包含五门课程的“深度学习专项课程”。它是免费的(尽管您可以支付一小笔费用以获得证书),将进一步巩固您的理论基础。Ng 博士在 Coursera 上的第一门机器学习课程已经教授了超过两百万名学生,这个系列延续了深受初学者和专家喜爱的高度易懂的内容传统。
-
如果我们没有在这个列表中鼓励您注意O'Reilly 的在线学习平台,我们将感到遗憾。这个平台帮助超过两百万用户提升他们的职业,包含数百本书籍、视频、在线培训和由 O'Reilly 的人工智能和数据会议上的领先思想家和实践者发表的主题演讲。
Q: 我在哪里可以找到有趣的笔记本进行学习?
Google Seedbank 是一个互动机器学习示例集合。基于 Google Colaboratory 构建,这些 Jupyter 笔记本可以立即运行,无需任何安装。一些有趣的示例包括:
-
使用 GANs 生成音频
-
视频动作识别
-
生成莎士比亚风格的文本
-
音频风格转移
Q: 我在哪里可以了解特定主题的最新技术?
考虑到人工智能领域技术更新迅速,SOTAWHAT 是一个方便的命令行工具,可以搜索最新模型、数据集、任务等研究论文。例如,要查找 ImageNet 的最新结果,请在命令行上使用sotawhat imagenet。此外,paperswithcode.com/sota还提供了论文、源代码和发布模型的存储库,以及一个交互式的基准时间轴。
Q: 我正在阅读一篇 Arxiv 上的论文,我非常喜欢。我需要从头开始编写代码吗?
一点也不!ResearchCode Chrome 扩展程序使得在浏览arxiv.org或 Google Scholar 时轻松找到代码。只需按一下扩展按钮即可。您也可以在ResearchCode.com网站上查找代码而无需安装扩展程序。
Q: *我不想写任何代码,但我仍然想使用我的摄像头进行交互式实验模型。我该怎么做?
Runway ML是一个易于使用但功能强大的 GUI 工具,允许您下载模型(来自互联网或您自己的模型)并使用网络摄像头或其他输入,如视频文件,以交互方式查看输出。这使得进一步组合和混合模型的输出以创建新作品成为可能。所有这些只需点击几下鼠标就能完成;因此,它吸引了大量的艺术家社区!
Q: 如果我可以在没有代码的情况下进行测试,那么我也可以在没有代码的情况下进行训练吗?
我们在第八章(基于网络)和第十二章(基于桌面)中详细讨论了这个问题。简而言之,诸如微软的CustomVision.ai、谷歌的 Cloud AutoML Vision、Clarifai、百度 EZDL 和苹果的 Create ML 等工具提供了拖放式训练功能。其中一些工具只需几秒钟就能完成训练。
最后一个问题
Q: 告诉我一个伟大的深度学习恶作剧?
打印并挂在keras4kindergartners.com上显示的海报,靠近饮水机,看着人们的反应。

图 7-1. 来自keras4kindergartners.com的关于 AI 状态的讽刺海报
第八章:云计算机视觉 API:15 分钟内上手
由于附近核电站多次发生近乎核泄漏的事件,斯普林菲尔德市图书馆(我们不允许提及州名)决定将他们所有宝贵的档案以数字形式存储太过危险。在听说对手城市 Shelbyville 的图书馆开始数字化他们的记录后,他们也想加入这场游戏。毕竟,他们的文章收藏,如“老人对着云喊叫”和“本地人认为摔跤是真的”,以及百年历史的峡谷和城市创始人杰比达·斯普林菲尔德的标志性照片是无法替代的。除了使他们的档案对灾难具有弹性外,他们还将使他们的档案易于搜索和检索。当然,现在斯普林菲尔德的居民可以在他们的客厅沙发上轻松访问所有这些材料。
数字化文件的第一步当然是扫描。那是容易的部分。然后开始真正的挑战——处理和理解所有这些视觉图像。斯普林菲尔德的团队面前有几种不同的选择。
-
为每一页和每张照片执行手动数据输入。考虑到这座城市拥有超过 200 年的丰富历史,这将需要很长时间,而且容易出错且昂贵。转录所有这些材料将是一场痛苦的经历。
-
雇佣一支数据科学家团队来构建图像理解系统。这将是一个更好的方法,但计划中有一个小小的问题。对于依靠慈善捐款运行的图书馆来说,雇佣一支数据科学家团队将很快耗尽其预算。一个数据科学家不仅可能是图书馆中薪水最高的员工,也可能是整个斯普林菲尔德市(除了富有的实业家蒙哥马利·伯恩斯)中收入最高的工人。
-
找一个懂得足够编码的人来使用现成的视觉 API 的智能。
逻辑上,他们选择了快速且廉价的第三种选择。他们也有一点运气。斯普林菲尔德小学勤奋的四年级学生马丁·普林斯恰好懂一些编码,自愿为他们建立系统。尽管马丁并不懂太多深度学习(毕竟他只有 10 岁),但他知道如何进行一些一般编码,包括使用 Python 进行 REST API 调用。这就是他真正需要知道的。事实上,他只用了不到 15 分钟就弄清楚了如何进行第一次 API 调用。
马丁的工作方式很简单:将扫描的图像发送到云 API,获得预测结果,并将其存储在数据库中以供将来检索。显然,对于图书馆拥有的每一条记录,都要重复这个过程。他只需要选择正确的工具来完成这项工作。
所有大公司——亚马逊、谷歌、IBM、微软——都提供类似的计算机视觉 API,可以标记图像、检测和识别人脸和名人、识别相似图像、读取文本,有时甚至可以识别手写。其中一些甚至提供了训练我们自己的分类器的能力,而无需编写一行代码。听起来真的很方便!
在背景中,这些公司不断努力改进计算机视觉的最新技术。他们花费了数百万美元来获取和标记数据集,其分类法远远超出了 ImageNet 数据集。我们不妨充分利用他们研究人员的心血和汗水(以及电费)。
易用性、入职和开发速度、功能的多样性、标签的丰富性以及竞争性定价使得基于云的 API 难以忽视。所有这些都不需要雇佣昂贵的数据科学团队。第五章和第六章分别针对准确性和性能进行了优化;而本章主要针对人力资源进行了优化。
在本章中,我们探讨了几种基于云的视觉识别 API。我们在数量和质量上对它们进行了比较。这应该会让您更容易选择最适合您目标应用程序的 API。如果它们仍然不符合您的需求,我们将探讨如何通过只需几次点击来训练自定义分类器。
(为了公开透明,本书的一些作者以前曾在微软工作,这里讨论的是微软的产品。我们尝试通过构建可重现的实验和证明我们的方法论来遏制这种偏见。)
视觉识别 API 的景观
让我们探索一些不同的视觉识别 API。
Clarifai
Clarifai(图 8-1)是 2013 年 ILSVRC 分类任务的获胜者。由纽约大学的研究生 Matthew Zeiler 创立,这是最早的视觉识别 API 公司之一。
注意
有趣的事实:在研究一个分类器来检测不安全的图像时,理解和调试 CNN 所学到的内容变得很重要,以减少误报。这导致 Clarifai 发明了一种可视化技术,以暴露哪些图像在 CNN 的任何层中刺激特征映射。正如他们所说,需求是发明之母。
这个 API 有什么独特之处?
它提供超过 23 种语言的多语言标记,以及在先前上传的照片中进行视觉相似性搜索,基于面部的多元文化外观分类器,照片美学评分器,焦点评分器,以及嵌入向量生成,以帮助我们构建自己的反向图像搜索。它还提供专业领域的识别,包括服装和时尚、旅行和款待以及婚礼。通过其公共 API,图像标记器支持 11,000 个概念。

图 8-1. Clarifai 结果示例
微软认知服务
2015 年,随着 ResNet-152 的创建,微软能够在 ILSVRC 赢得七项任务,包括 COCO 图像字幕挑战以及野外情绪识别挑战,涵盖了从分类和检测(定位)到图像描述的各个方面。大部分这项研究都被转化为云 API。最初是从 2015 年微软研究的 Project Oxford 开始,最终在 2016 年更名为认知服务。这是一个包含超过 50 个 API 的综合性集合,涵盖视觉、自然语言处理、语音、搜索、知识图链接等多个领域。历史上,许多相同的库在 Xbox 和必应的部门运行,但现在它们正在向外部开发人员开放。一些展示开发人员如何创造性地使用这些 API 的病毒应用程序包括how-old.net(我看起来多大?)、Mimicker Alarm(需要做出特定的面部表情才能关闭早晨的闹钟)和CaptionBot.ai。
这个 API 有什么独特之处?
如图 8-2 所示,该 API 提供图像字幕、手写理解和头饰识别。由于有许多企业客户,认知服务不使用客户图像数据来改进其服务。

图 8-2. 微软认知服务结果示例
谷歌云视觉
谷歌在 2014 年 ILSVRC 比赛中凭借 22 层 GoogLeNet 获得了胜利,最终为现在常见的 Inception 架构铺平了道路。除了 Inception 模型,谷歌在 2015 年 12 月发布了一套视觉 API。在深度学习领域,拥有大量数据肯定是提高分类器的优势,而谷歌拥有大量的消费者数据。例如,通过从 Google 街景中学到的知识,您应该期望在现实世界的文本提取任务中表现相对良好,比如在广告牌上。
这个 API 有什么独特之处?
对于人脸,它提供了最详细的面部关键点(图 8-3),包括滚动、倾斜和平移,以准确定位面部特征。API 还会返回与给定输入相似的网络图片。尝试谷歌系统的性能的简单方法是将照片上传到 Google 照片并通过标签搜索。

图 8-3. 谷歌云视觉结果示例
亚马逊 Rekognition
不,这个标题并没有错别字。亚马逊 Rekognition API(图 8-4)主要基于 Orbeus,这是一家位于加利福尼亚州圣尼维尔的初创公司,于 2015 年底被亚马逊收购。成立于 2012 年,其首席科学家还在 2014 年 ILSVRC 检测挑战中获奖。同样的 API 被用于推动著名的照片整理应用 PhotoTime。该 API 的服务作为 AWS 产品的一部分提供。考虑到大多数公司已经提供了照片分析 API,亚马逊正在加倍努力提供视频识别服务以实现差异化。
这个 API 有什么独特之处?
车牌识别,视频识别 API 以及与 AWS 产品(如 Kinesis 视频流、Lambda 等)的更好端到端集成示例是 Rekognition API 的亮点。此外,亚马逊的 API 是唯一一个可以确定被拍摄对象的眼睛是睁着还是闭着的 API。

图 8-4. 亚马逊 Rekognition 结果示例
IBM Watson 视觉识别
在 Watson 品牌下,IBM 的视觉识别服务于 2015 年初开始。在收购了位于丹佛的初创公司 AlchemyAPI 之后,AlchemyVision 被用于推动视觉识别 API(图 8-5)。与其他公司一样,IBM 也提供自定义分类器训练。令人惊讶的是,Watson 目前还没有提供光学字符识别功能。

图 8-5. IBM Watson 视觉识别结果示例
Algorithmia
Algorithmia 是一个在云上托管算法作为 API 的市场。成立于 2013 年,这家位于西雅图的初创公司既有自己的内部算法,也有其他人创建的算法(在这种情况下,创建者根据调用次数赚取收入)。根据我们的经验,这个 API 的响应时间似乎是最慢的。
这个 API 有什么独特之处?
黑白照片的着色服务(图 8-6),图像风格化,图像相似度以及在本地或任何云提供商上运行这些服务的能力。

图 8-6. Algorithmia 风格转移结果示例
由于提供的服务太多,选择一个服务可能会让人感到不知所措。我们可能会因为各种原因选择一个服务而不是另一个。显然,对大多数开发人员来说,最重要的因素是准确性和价格。准确性是深度学习革命带来的重要承诺,许多应用程序需要它以保持一致。服务的价格可能是需要考虑的另一个因素。我们也可能选择一个服务提供商,因为我们的公司已经与其有结算账户,而与其他服务提供商集成将需要额外的努力。API 响应速度可能是另一个因素,特别是如果用户正在等待响应。由于许多这些 API 调用可以被抽象化,因此很容易在不同的提供商之间切换。
比较视觉识别 API
为了帮助我们做出决策,让我们逐个比较这些 API。在本节中,我们将检查每个服务的提供、成本和准确性。
服务提供
表 8-1 列出了每个云提供商提供的服务。
表 8-1. 视觉 API 提供商的比较(截至 2019 年 8 月)
| Algorithmia | Amazon Rekognition | Clarifai | Microsoft Cognitive Services | Google Cloud Vision | IBM Watson Visual Recognition | |
|---|---|---|---|---|---|---|
| 图像分类 | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
| 图像检测 | ✔ | ✔ | ✔ | ✔ | ||
| OCR | ✔ | ✔ | ✔ | ✔ | ||
| 人脸识别 | ✔ | ✔ | ✔ | |||
| 情绪识别 | ✔ | ✔ | ✔ | ✔ | ||
| 标志识别 | ✔ | ✔ | ✔ | |||
| 地标识别 | ✔ | ✔ | ✔ | ✔ | ||
| 名人识别 | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
| 多语言标记 | ✔ | |||||
| 图像描述 | ✔ | |||||
| 手写 | ✔ | ✔ | ||||
| 缩略图生成 | ✔ | ✔ | ✔ | |||
| 内容审核 | ✔ | ✔ | ✔ | ✔ | ✔ | |
| 自定义分类训练 | ✔ | ✔ | ✔ | ✔ | ||
| 自定义检测器训练 | ✔ | ✔ | ||||
| 移动定制模型 | ✔ | ✔ | ✔ | |||
| 免费套餐 | 每月 5000 次请求 | 每月 5000 次请求 | 每月 5000 次请求 | 每月 5000 次请求 | 每月 1000 次请求 | 7500 |
已经有很多服务在运行并准备在我们的应用程序中使用。因为数字和硬数据有助于做出决策,现在是时候在成本和准确性两个因素上分析这些服务了。
成本
金钱不是从树上长出来的(尚未),因此分析使用现成 API 的经济学是很重要的。以每秒约 1 次查询(QPS)的重型查询 API 为例,为一个完整的月份提供服务(大约每月 260 万次请求),图 8-7 按预估成本排序列出了不同提供商的比较(截至 2019 年 8 月)。

图 8-7. 不同基于云的视觉 API 的成本比较
尽管对于大多数开发人员来说,这是一个极端的情况,但对于大型公司来说,这将是一个相当现实的负载。我们最终将比较这些价格与在云中运行我们自己的服务,以确保我们在符合我们情景的情况下获得最大的性价比。
也就是说,许多开发人员可能会发现费用微不足道,因为我们在这里看到的所有云提供商都有每月 5000 次调用的免费套餐(除了 Google Vision,它每月只提供 1000 次免费调用),然后大约每 1000 次调用收取 1 美元。
准确性
在一个由市场部门主导的世界中,他们声称自己是市场领导者,我们如何判断谁才是真正的最佳呢?我们需要一些共同的指标来比较这些服务提供商在一些外部数据集上的表现。
展示构建可重复基准的过程中,我们使用 COCO-Text 数据集评估文本提取质量,该数据集是 MS COCO 数据集的子集。这个包含 63,686 张图像的数据集包含了日常生活场景中的文本,比如横幅上的文字、街道标志、公交车上的数字、杂货店的价格标签、设计师衬衫等等。这些真实世界的图像使得这个数据集相对较难测试。我们使用词错误率(WER)作为我们的基准度量。为了简化问题,我们忽略单词的位置,只关注单词是否存在(即词袋模型)。为了匹配,整个单词必须是正确的。
在 COCO-Text 验证数据集中,我们挑选所有包含一个或多个可读文本实例(没有中断的完整文本序列)的图像,并比较长度超过一个字符的文本实例。然后我们将这些图像发送到各种云视觉 API。图 8-8 呈现了结果。

图 8-8. 2019 年 8 月不同文本提取 API 的 WER
考虑到数据集的难度,这些结果是显著的。本十年早期的大多数最先进的文本提取工具都不会超过 10%的标记。这展示了深度学习的力量。在一部分手动测试的图像中,我们还注意到这些 API 中一些的性能每年都有所提高,这是云端 API 所享有的另一个好处。
和往常一样,我们用于实验的所有代码都托管在 GitHub 上(请参见http://PracticalDeepLearning.ai)。
我们分析的结果在很大程度上取决于我们选择的数据集以及我们的度量标准。根据我们的数据集(又受我们的用例影响)以及我们的最低质量度量标准,我们的结果可能会有所不同。此外,服务提供商在背景中不断改进他们的服务。因此,这些结果并非一成不变,会随着时间的推移而改善。这些结果可以在 GitHub 上的脚本上复制到任何数据集上。
偏见
在第一章中,我们探讨了偏见如何渗入数据集以及它对人们的现实生活造成的后果。本章中我们探索的 API 也不例外。麻省理工学院媒体实验室的研究员乔伊·布拉明尼发现,在微软、IBM 和 Megvii(也称为 Face++)中,没有一个能够准确检测她的面部和性别。她怀疑自己是否有独特的面部特征,使得这些 API 无法检测到她(与 Timnit Gebru 合作),编制了六个国家立法机构成员的面部图像,这些国家女性代表比例较高,建立了 Pilot Parliaments Benchmark(PPB;请参见图 8-9)。她选择了来自三个非洲国家和三个欧洲国家的成员,以测试这些 API 在不同肤色上的表现。如果你没有生活在石头下,你已经可以看出这将会发展成什么样的情况。
她观察到 API 的整体准确率在 85%至 95%之间。只有当她开始根据不同类别划分数据时,她才观察到每个类别的准确率存在巨大差异。她首先观察到男性和女性的检测准确率存在显著差异。她还观察到,根据肤色划分,检测准确率的差异更大。最后,考虑到性别和肤色,检测准确率在最差检测组(较深色女性)和最佳检测组(较浅色男性)之间的差异变得更加明显。例如,在 IBM 的情况下,非洲女性的检测准确率仅为 65.3%,而同一 API 对欧洲男性的准确率为 99.7%。高达 34.4%的差异!考虑到许多这些 API 被执法部门使用,偏见渗入可能会带来生死后果。

图 8-9。来自 Pilot Parliaments Benchmark(PPB)的不同性别和肤色的平均脸
以下是我们从这项研究中学到的一些见解:
-
算法只有在其训练的数据良好时才有效。这表明训练数据集需要多样性。
-
通常,综合数字并不能总是揭示真实情况。只有在将数据集划分到不同子组时,数据集中的偏见才会显现。
-
偏见并不属于任何特定公司;相反,这是一个整个行业的现象。
-
这些数字并非铁板钉钉,仅反映了实验进行时的时间。正如 2017 年(图 8-10)和 2018 年后续研究(图 8-11)之间数字的急剧变化所示,这些公司正在认真从数据集中消除偏见。
-
研究人员通过公共基准测试商业公司,导致整个行业的改进(即使是出于对不良公关的恐惧)。

图 8-10。2017 年 4 月和 5 月在 PPB 上进行的 API 之间的人脸检测比较

图 8-11。2018 年 8 月在 PPB 上由 Inioluwa Deborah Raji 等人进行的 API 之间的人脸检测比较。
那么图像标记 API 中的偏见呢?Facebook AI 研究在同名论文中思考了“对象识别对每个人都有效吗?”这个问题(Terrance DeVries 等人)。该团队于 2019 年 2 月在 Dollar Street 上测试了多个云 API,这是来自 50 个国家 264 个家庭的各种家庭物品图像的集合(图 8-12)。

图 8-12。来自 Dollar Street 数据集的地理多样化图像的图像标记 API 性能
以下是这次测试的一些关键发现:
-
在收入较低地区的图像中,对象分类 API 的准确性明显较低,如图 8-13 所示。
-
像 ImageNet、COCO 和 OpenImages 这样的数据集严重缺乏来自非洲、印度、中国和东南亚的图像,因此在非西方世界的图像上表现较差。
-
大多数数据集是通过英语关键词搜索收集的,省略了提及同一对象的其他语言短语的图像。

图 8-13. 六个云 API 的平均准确率(和标准差)与收集图像的家庭收入之间的关系
总之,根据我们想要使用这些云 API 的场景,我们应该建立自己的基准,并定期测试它们,以评估这些 API 是否适合用例。
开始使用云 API
调用这些云服务只需要很少的代码。在高层次上,获取一个 API 密钥,加载图像,指定意图,使用正确的编码(例如,对于图像使用 base64),发送 POST 请求并接收结果。大多数云提供商提供软件开发工具包(SDK)和展示如何调用他们服务的示例代码。他们还提供可通过 pip 安装的 Python 包,以进一步简化调用过程。如果您正在使用 Amazon Rekognition,我们强烈建议使用其pip包。
让我们重复使用我们激动人心的图像来测试这些服务。
首先,让我们在 Microsoft Cognitive Services 上尝试一下。获取一个 API 密钥,并在以下代码中替换它(前 5000 次调用是免费的——对于我们的实验来说足够了):
cognitive_services_tagimage('DogAndBaby.jpg')
Results:
{
"description": {
"tags": ["person", "indoor", "sitting", "food", "table", "little",
"small", "dog", "child", "looking", "eating", "baby", "young", "front",
"feeding", "holding", "playing", "plate", "boy", "girl", "cake", "bowl",
"woman", "kitchen", "standing", "birthday", "man", "pizza"],
"captions": [{
"text": "a little girl sitting at a table with a dog",
"confidence": 0.84265453815486435
}]
},
"requestId": "1a32c16f-fda2-4adf-99b3-9c4bf9e11a60",
"metadata": {
"height": 427,
"width": 640,
"format": "Jpeg"
}
}
“一个坐在桌子旁边的小女孩和一只狗”——非常接近!还有其他选项可以生成更详细的结果,包括每个标签的概率。
提示
尽管 ImageNet 数据集主要标记为名词,但许多这些服务超越了这一点,返回动词如“吃”、“坐”、“跳”。此外,它们可能包含形容词如“红色”。这些可能不适合我们的应用。我们可能希望过滤掉这些形容词和动词。一个选择是将它们的语言类型与普林斯顿的 WordNet 进行比较。这在 Python 中使用自然语言处理工具包(NLTK)可用。此外,我们可能希望过滤掉诸如“室内”和“室外”之类的词语(通常由 Clarifai 和 Cognitive Services 显示)。
现在,让我们使用 Google Vision API 测试相同的图像。从他们的网站获取一个 API 密钥,并在以下代码中使用它(并且高兴,因为前 1000 次调用是免费的):
google_cloud_tagimage('DogAndBaby.jpg')
Results:
{
"responses": [
{
"labelAnnotations": [
{
"mid": "/m/0bt9lr",
"description": "dog",
"score": 0.951077,
"topicality": 0.951077
},
{
"mid": "/m/06z04",
"description": "skin",
"score": 0.9230451,
"topicality": 0.9230451
},
{
"mid": "/m/01z5f",
"description": "dog like mammal",
"score": 0.88359463,
"topicality": 0.88359463
},
{
"mid": "/m/01f5gx",
"description": "eating",
"score": 0.7258142,
"topicality": 0.7258142
}
# other objects
]
}
]
}
这不是太容易了吗?这些 API 帮助我们在不需要博士学位的情况下获得最先进的结果,仅需 15 分钟!
提示
尽管这些服务返回带有概率的标签和图像标题,但开发人员需要确定一个阈值。通常,60%和 40%分别是图像标签和图像标题的良好阈值。
从用户体验的角度,将概率传达给最终用户也很重要。例如,如果结果置信度>80%,我们可能会在标签前加上“这张图片包含....”。对于<80%,我们可能希望将该前缀更改为“这张图片可能包含...”以反映结果中较低的置信度。
训练我们自己的自定义分类器
这些服务可能并不完全满足我们用例的要求。假设我们发送给其中一个服务的照片返回标签“狗”,我们可能更感兴趣的是识别狗的品种。当然,我们可以按照第三章来在 Keras 中训练自己的分类器。但如果我们不需要编写一行代码,那不是更棒吗?帮助已经到来。
其中一些云提供商通过简单的拖放界面让我们有能力训练自己的自定义分类器。漂亮的用户界面并没有表明它们在幕后使用迁移学习。因此,Cognitive Services Custom Vision、Google AutoML、Clarifai 和 IBM Watson 都为我们提供了自定义训练的选项。此外,其中一些甚至允许构建自定义检测器,可以识别带有边界框的对象的位置。其中的关键过程如下:
-
上传图片
-
标记它们
-
训练模型
-
评估模型
-
将模型发布为 REST API
-
奖励:下载一个适用于智能手机和边缘设备推理的移动友好模型
让我们看一下微软的Custom Vision的逐步示例。
-
创建一个项目 (图 8-14): 选择最能描述我们用例的领域。对于大多数情况,“通用”可能是最佳选择。对于更专业的场景,我们可能想选择一个相关的领域。
![在 Custom Vision 中创建一个新项目]()
图 8-14. 在 Custom Vision 中创建一个新项目
例如,如果我们有一个电子商务网站,网站上有产品照片背景是纯白色的,我们可能想选择“零售”领域。如果我们最终打算在手机上运行这个模型,我们应该选择模型的“紧凑”版本;它尺寸更小,只有轻微的准确度损失。
-
上传 (图 8-15): 对于每个类别,上传图片并进行标记。每个类别至少上传 30 张照片是很重要的。在我们的测试中,我们上传了 30 多张马耳他犬的图片,并进行了适当的标记。
![在 CustomVision.ai 上上传图片]()
图 8-15. 在 CustomVision.ai 上上传图片
-
训练 (图 8-16): 点击 Train 按钮,大约三分钟后,我们就有了一个全新的分类器。
![CustomVision.ai 页面右上角的 Train 按钮]()
图 8-16. CustomVision.ai 页面右上角的 Train 按钮
-
分析模型的性能:检查模型的精度和召回率。系统默认将阈值设置为 90%置信度,并给出该值下的精度和召回率指标。要获得更高的精度,增加置信度阈值。这将以减少召回率为代价。图 8-17 展示了示例输出。
-
准备就绪:我们现在有一个可以从任何应用程序调用的生产就绪的 API 端点。
为了突出数据量对模型质量的影响,让我们训练一个狗品种分类器。我们可以使用斯坦福狗数据集,这是一个包含 100 多种狗品种的集合。为简单起见,我们随机选择了 10 种品种,这些品种有超过 200 张可用图片。有了 10 个类别,一个随机分类器正确识别图像的几率是十分之一,或者 10%。我们应该很容易地超过这个数字。表 8-2 显示了在不同数据集上训练的效果。
表 8-2. 训练图片数量对精度和召回率的影响
| 每类 30 张训练图片 | 每类 200 张训练图片 | |
|---|---|---|
| 精度 | 91.2% | 93.5% |
| 召回率 | 85.3% | 89.6% |

图 8-17. 相对精度和召回率,我们的样本训练集每类有 200 张图片
因为我们还没有上传测试集,所以这里报告的性能数据是在完整数据集上使用常见的k折交叉验证技术得出的。这意味着数据被随机分成k部分,然后(*k - 1)部分用于训练,剩下的部分用于测试。这个过程进行了几次,每次使用随机子集的图像,并报告了平均结果。
令人难以置信的是,即使每类只有 30 张图片,分类器的精度也超过 90%,如图 8-18 所示。令人惊讶的是,这个训练过程只花了不到 30 秒的时间。
不仅如此,我们可以深入研究每个类别的性能。高精度的类别可能看起来更加明显,而低精度的类别可能与另一个类别相似。

图 8-18. API 返回的一些可能标签
这种简短而方便的方法并非没有缺点,正如您将在以下部分中看到的那样。在该部分中,我们还讨论了缓解策略,以帮助利用这个相当有用的工具。
我们的分类器不令人满意的主要原因
分类器表现不佳的原因有很多。以下是其中一些:
数据不足
如果我们发现准确性不够满足我们的需求,可能需要用更多数据训练系统。当然,每个类别 30 张图像只是一个开始。但对于生产质量的应用程序,更多的图像更好。通常建议每个类别使用 200 张图像。
不具代表性的训练数据
通常,互联网上的图像过于干净,设置在工作室灯光下,背景干净,并且接近画面中心。我们的应用程序可能每天看到的图像可能没有那么好地表示。用真实世界的图像训练我们的分类器对于获得最佳性能非常重要。
不相关的领域
在幕后,自定义视觉正在运行迁移学习。这使得在创建项目时选择正确的领域非常重要。举个例子,如果我们试图对 X 射线图像进行分类,从基于 ImageNet 的模型进行迁移学习可能不会产生如此准确的结果。对于这种情况,手动在 Keras 中训练我们自己的分类器会是最好的选择,就像在第三章中演示的那样(尽管这可能需要超过三分钟)。
使用它进行回归
在机器学习中,有两种常见的问题类别:分类和回归。分类是为输入预测一个或多个类别。另一方面,回归是根据输入预测一个数值;例如,预测房价。自定义视觉主要是一个分类系统。将其用于通过标记对象数量来计数对象是错误的方法,并将导致不令人满意的结果。
计数对象是一种回归问题。我们可以通过定位图像中每个对象的每个实例(也称为目标检测)并计算它们的出现次数来实现。另一个回归问题的例子是根据个人的头像预测其年龄。我们将在后面的章节中解决这两个问题。
类别太相似
如果我们的类看起来太相似,并且在区分上依赖较小级别的细节,那么模型可能表现不佳。例如,一张五美元和一张二十美元的纸币具有非常相似的高级特征。真正区别它们的是更低级别的细节。另一个例子,区分吉娃娃和西伯利亚哈士奇可能很容易,但区分阿拉斯加雪橇犬和西伯利亚哈士奇可能更困难。如在第三章中演示的完全重新训练的 CNN 应该比这个基于自定义视觉的系统表现更好。
提示
自定义视觉的一个很棒的功能是,如果模型对通过其 API 端点遇到的任何图像感到不确定,Web UI 将显示这些图像供手动审查。我们可以定期审查和手动标记新图像,并持续改进模型的质量。这些图像往往对分类器的改进最大,原因有两个:首先,它们代表了真实世界的使用。其次,更重要的是,与模型已经可以轻松分类的图像相比,它们对模型的影响更大。这被称为半监督学习。
在这一部分中,我们讨论了一些可以提高模型准确性的方法。在现实世界中,这并不是用户体验的全部。我们能够多快地响应请求也非常重要。在接下来的部分中,我们将介绍一些不牺牲质量的情况下提高性能的方法。
比较自定义分类 API
正如你可能在整本书中已经注意到的那样,我们非常坚持数据驱动。如果我们要在一个服务上花钱,我们最好能物有所值。是时候来测试一下这个炒作了。
对于许多分类问题,这些定制的基于云的分类器表现得相当不错。为了真正测试它们的极限,我们需要更具挑战性的数据集。我们需要释放最难缠的数据集,训练这个模型,并获得一些有见地的结果——使用斯坦福狗数据集。
使用整个数据集可能会使这些分类器变得太容易(毕竟,ImageNet 已经包含了很多狗品种),所以我们提高了难度。相反,我们在整个数据集上训练了我们自己的 Keras 分类器,并从表现最差的前 34 个类别中构建了一个迷你数据集(每个类别至少包含 140 张图片)。这些类别表现不佳的原因是因为它们经常与其他看起来相似的狗品种混淆。为了表现更好,它们需要对特征有细致的理解。我们将图像分为训练数据集中每个类别 100 张随机选择的图像,以及测试数据集中每个类别 40 张随机选择的图像。为了避免任何类别不平衡对预测的影响,我们为每个类别选择了相同数量的训练和测试图像。
最后,我们选择了最小置信阈值为 0.5,因为它似乎在所有服务中在精度和召回率之间取得了良好的平衡。在高置信阈值(例如 0.99)下,分类器可能非常准确,但可能只有少数图像有预测;换句话说,召回率非常低。另一方面,非常低的阈值(0.01)会导致几乎所有图像都有预测。然而,我们不应该依赖许多这样的结果。毕竟,分类器并不自信。
我们报告F1 分数(也称为F-度量),它是结合了这两个值的混合分数:
此外,我们报告了训练所需的时间,如图 8-19 所示。除了云端,我们还使用了苹果的 Create ML 工具在 MacBook Pro 上进行训练,有时使用数据增强(旋转、裁剪和翻转),有时不使用。
谷歌和微软提供了定制训练持续时间的能力。谷歌 Auto ML 允许我们在 1 到 24 小时之间进行定制。微软提供了一个免费的“快速训练”选项和一个付费的“高级训练”选项(类似于谷歌的提供),我们可以选择在 1 到 24 小时之间的任何时间段进行训练。

图 8-19。显示了自定义分类器服务的 F1 分数,截至 2019 年 8 月(分数越高越好)
以下是这个实验的一些有趣的收获:
-
Clarifai 和微软为这 3400 张训练图像提供了几乎即时的训练。
-
与“快速训练”相比,微软的“高级训练”表现稍好一些(大约增加了 1 个点)多出的一个小时的训练。因为“快速训练”只需要不到 15 秒的时间进行训练,我们可以推断它的基础特征提取器已经很擅长提取细粒度特征。
-
令人惊讶的是,尽管在添加增强功能后,苹果的 Create ML 表现更差,尽管训练时间超过两个小时,其中大部分时间用于创建增强功能。这是在一台顶级的 MacBook Pro 上完成的,并在 Activity Monitor 中显示 100%的 GPU 利用率。
此外,为了测试特征提取器的强度,我们改变了提供给服务的训练数据量(图 8-20)。由于微软的训练时间不到 15 秒,我们很容易(而且便宜!)在那里进行实验。我们在训练时每类变化了 30 到 100 张图像,同时保持测试时每类 40 张图像不变。

图 8-20. 不同类别训练数据大小对测试 F1 分数的影响(分数越高越好)
尽管微软建议每类至少使用 50 张图像,但低于这个限制并没有显著影响性能。F1 分数没有像人们预期的那样变化,这显示了迁移学习的价值(使少量数据构建分类器)以及具有能够进行细粒度分类的良好特征提取器的重要性。
值得重申的是,这个实验是故意设置为困难的,以对这些分类器进行压力测试。平均而言,它们在整个斯坦福狗数据集上的表现会更好。
云 API 的性能调优
现代手机拍摄的照片可以具有高达 4000 x 3000 像素的分辨率,并且大小可达 4 MB。根据网络质量,上传这样的图像到服务可能需要几秒钟。随着每增加一秒,对我们的用户来说可能会变得越来越令人沮丧。我们能不能让这个过程更快一些呢?
有两种方法可以减小图像的大小:
调整大小
大多数 CNN 接受大小为 224 x 224 或 448 x 448 像素的输入图像。对于 CNN 来说,手机照片的大部分分辨率都是不必要的。在将图像发送到网络之前缩小图像大小是有意义的,而不是将大图像发送到网络,然后在服务器上缩小图像。
压缩
大多数图像库在保存文件时执行有损压缩。即使稍微压缩一点,也可以大大减小图像的大小,同时对图像本身的质量影响很小。压缩确实会引入噪音,但 CNN 通常足够强大,可以处理其中的一些噪音。
调整大小对图像标记 API 的影响
我们进行了一个实验,从 iPhone 拍摄的一百多张不同的未经修改的图像,分辨率为默认分辨率(4032 x 3024),并将它们发送到 Google Cloud Vision API,为每张图像获取标签。然后我们逐步缩小原始图像的大小(5%,10%,15%…95%),并收集这些较小图像的 API 结果。然后我们使用以下公式计算每个图像的一致性率:
图 8-21 显示了这个实验的结果。在图中,实线显示了文件大小的减小,虚线表示一致性率。我们从实验中得出的主要结论是,将分辨率减小 60%会导致文件大小减小 95%,与原始图像相比准确性几乎没有变化。

图 8-21. 调整图像大小对一致性率和相对于原始图像的文件大小减小的影响
压缩对图像标记 API 的影响
我们重复了相同的实验,但是不改变分辨率,而是逐步改变每个图像的压缩因子。在图 8-22 中,实线显示了文件大小的减少,虚线代表协议率。这里的主要要点是,60%的压缩得分(或 40%的质量)导致文件大小减少 85%,与原始图像相比,准确性几乎没有变化。

图 8-22。压缩图像对协议率和文件大小相对于原始图像的影响
压缩对 OCR API 的影响
我们拿一份包含 300 多个单词的文档,分辨率为 iPhone 的默认分辨率(4032 x 3024),并将其发送到 Microsoft Cognitive Services API 进行文本识别测试。然后我们逐步以 5%的增量压缩它,然后发送每个图像并压缩它。我们将这些图像发送到同一 API,并将它们的结果与基线进行比较,以计算百分比的错误率。我们观察到,即使将压缩因子设置为 95%(即原始图像质量的 5%),也不会对结果的质量产生影响。
调整大小对 OCR API 的影响
我们重复了之前的实验,但这次是通过调整每个图像的大小而不是压缩。在某一点之后,错误率从零跳升到接近 100%,几乎所有单词都被错误分类。重新测试另一份文档,其中每个单词的字体大小都不同,结果显示特定字体大小以下的所有单词都被错误分类。为了有效识别文本,OCR 引擎需要文本的高度大于最小高度(一个很好的经验法则是大于 20 像素)。因此,分辨率越高,准确性就越高。
我们学到了什么?
-
对于文本识别,大幅压缩图像,但不要调整大小。
-
对于图像标记,适度调整大小(例如 50%)和适度压缩(例如 30%)的组合应该会导致文件大小大幅减少(并且 API 调用更快),而不会影响 API 结果的质量。
-
根据您的应用程序,您可能正在处理已经调整大小和压缩的图像。每个处理步骤都可能会对这些 API 的结果产生轻微差异,因此应该尽量减少它们。
提示
收到图像后,云 API 会在内部调整大小以适应其自身的实现。对我们来说,这意味着两个级别的调整大小:我们首先调整图像大小以减小尺寸,然后将其发送到云 API,后者会进一步调整图像大小。缩小图像会引入失真,这在较低分辨率下更为明显。我们可以通过从更高分辨率调整大小来最小化失真的影响,这个分辨率比原始分辨率大几倍。例如,将 3024x3024(原始)→ 302x302(发送到云端)→ 224x224(API 内部调整大小)相比于 3024x3024 → 896x896 → 224x224,最终图像中的失真会更多。因此,在发送图像之前最好找到一个合适的中间尺寸。此外,指定高级插值选项如 BICUBIC 和 LANCZOS 将导致更准确地表示原始图像在较小版本中的效果。
案例研究
有人说生活中最好的事情并不容易获得。我们相信这一章证明了相反。在接下来的部分中,我们将看看一些科技行业巨头如何利用云 API 进行人工智能,从而推动一些非常引人注目的场景。
纽约时报
可能看起来像是本章开头描绘的情景取自卡通,但实际上与《纽约时报》(NYT)的情况非常接近。拥有 160 多年的光辉历史,纽约时报在其档案中拥有大量的照片珍藏品。它将许多这些文物存放在地下三层的建筑物地下室中,恰当地称为“尸房”。这个收藏的价值是无价的。2015 年,由于管道泄漏,地下室的部分地方受损,包括其中一些存档记录。幸运的是,损坏很小。然而,这促使纽约时报考虑对其进行数字化存档,以防止另一场灾难。
这些照片被扫描并以高质量存储。然而,这些照片本身没有任何识别信息。许多照片的背面有手写或印刷的注释,为照片提供了背景信息。纽约时报使用 Google Vision API 扫描这些文本,并为相应的图像添加标签。此外,这个流程提供了从照片中提取更多元数据的机会,包括地标识别、名人识别等等。这些新添加的标签为其搜索功能提供动力,使公司内外的任何人都可以浏览画廊并使用关键词、日期等搜索,而无需访问地下三层的尸房。
Uber
Uber 使用 Microsoft Cognitive Services 在几毫秒内识别其 700 多万司机中的每一个。想象一下 Uber 必须以何种规模运营其名为“实时身份验证”的新功能。该功能通过提示司机随机或每次分配给新乘客时自拍来验证当前司机是否确实是注册司机。这张自拍照与文件中的司机照片进行比对,只有在脸部模型匹配时才允许司机继续。这一安全功能有助于通过确保乘客的安全和确保司机账户不受损害来建立问责制。这一安全功能能够检测自拍中的变化,包括帽子、胡须、太阳镜等,然后提示司机摘掉帽子或太阳镜自拍。

图 8-23. Uber Drivers 应用程序提示司机自拍以验证司机的身份(图片来源)
Giphy
回到 1976 年,当理查德·道金斯博士创造了“模因”一词时,他并不知道四十年后它会变得如此活跃。我们生活在一个大多数聊天应用程序建议匹配上下文的适当动画 GIF 的时代,而不是给出简单的文本回复。几个应用程序提供了专门搜索模因和 GIF 的功能,如 Tenor、Facebook Messenger、Swype 和 Swiftkey。它们大多通过 Giphy 进行搜索(图 8-24),这是世界上最大的动画模因搜索引擎,通常以 GIF 格式呈现。

图 8-24. Giphy 从动画中提取文本作为搜索的元数据
GIF 通常有叠加的文本(比如正在说的对话),有时我们想要寻找一个具有特定对话的 GIF,直接来自电影或电视节目。例如,来自 2010 年《未来派》剧集的图像图 8-24,其中“eyePhone”(错误)发布时经常被用来表达对产品或想法的兴奋。了解内容使 GIF 更易搜索。为了实现这一点,Giphy 使用 Google 的 Vision API 来提取识别文本和对象,帮助搜索完美的 GIF。
很明显,给 GIF 打标签是一项困难的任务,因为一个人必须筛选数百万个这些动画,并逐帧手动注释它们。2017 年,Giphy 找到了两种自动化这一过程的解决方案。第一种方法是从图像中检测文本。第二种方法是根据图像中的对象生成标签,以补充其搜索引擎的元数据。这些元数据存储和搜索使用 ElasticSearch 来创建一个可扩展的搜索引擎。
对于文本检测,该公司使用 Google Vision API 的 OCR 服务对 GIF 的第一帧进行确认,以确定 GIF 是否实际包含文本。如果 API 回复肯定,Giphy 将发送下一帧,接收其 OCR 检测到的文本,并找出文本的差异;例如,文本是静态的(在 gif 的整个持续时间内保持不变)还是动态的(不同帧中的不同文本)。为了生成与图像中对象对应的类标签,工程师有两个选择:标签检测或网络实体,这两者都可以在 Google Vision API 上使用。标签检测提供对象的实际类名。网络实体提供一个实体 ID(可在 Google Knowledge Graph 中引用),这是在网上看到的相同和相似图像的唯一网址。使用这些额外的注释使新系统的点击率(CTR)增加了 32%。中长尾搜索(即不太频繁的搜索)受益最大,因为提取的元数据使以前未注释的 GIF 浮出水面,否则这些 GIF 将被隐藏。此外,用户的元数据和点击行为提供了数据,以制作相似性和去重功能。
OmniEarth
OmniEarth 是一家总部位于弗吉尼亚州的公司,专门收集、分析和结合卫星和航拍图像以及其他数据集,以可扩展和高速的方式跟踪全国的用水情况。该公司能够在几小时内扫描整个美国境内 1.44 亿个土地地块。在内部,它使用 IBM Watson Visual Recognition API 对土地地块的图像进行分类,以获取有价值的信息,比如绿化程度。将这种分类与温度和降雨等其他数据点结合起来,OmniEarth 可以预测用于灌溉田地的水量。
对于房产,它从图像中推断出数据点,比如游泳池、树木或可灌溉的园艺景观的存在,以预测用水量。该公司甚至预测了由于过度浇水或漏水等不当做法而导致的水浪费情况。OmniEarth 通过分析超过 15 万个土地地块,帮助加利福尼亚州了解用水情况,然后制定了一个有效的策略来遏制水资源浪费。
Photobucket
Photobucket 是一个流行的在线图像和视频托管社区,每天上传超过两百万张图片。使用 Clarifai 的 NSFW 模型,Photobucket 自动标记不受欢迎或冒犯性的用户生成内容,并将其发送给人工审核团队进行进一步审核。此前,该公司的人工审核团队只能监控到约 1%的传入内容。大约 70%的被标记图片被发现是不可接受的内容。与以往的手动努力相比,Photobucket 识别出了 700 倍更多的不受欢迎内容,从而清理了网站并创造了更好的用户体验。这种自动化还帮助发现了两个儿童色情账户,并向 FBI 报告了这一情况。
Staples
像 Staples 这样的电子商务商店通常依赖有机搜索引擎流量来推动销售。在搜索引擎排名中高居榜首的方法之一是在图像的 ALT 文本字段中放置描述性图像标签。为了在 12 种不同语言中提供服务的 Staples Europe 发现标记产品图像和翻译关键字是一项昂贵的任务,传统上是外包给人类机构的。幸运的是,Clarifai 以更便宜的价格提供 20 种语言的标签,为 Staples 节省了数万美元的成本。使用这些相关关键字导致了流量的增加,最终通过其电子商务商店增加了销售额,因为产品页面的访问者激增。
InDro Robotics
这家加拿大无人机公司利用 Microsoft Cognitive Services 来支持搜索和救援行动,不仅在自然灾害期间,还主动检测紧急情况。该公司利用 Custom Vision 专门训练模型,用于识别水中的船只和救生衣(图 8-25),并利用这些信息通知控制站。与救生员相比,这些无人机能够独自扫描更大范围的海洋。这种自动化可以提醒救生员有紧急情况,从而提高发现速度并在过程中挽救生命。
澳大利亚已经开始使用其他公司的无人机配备充气舱,以便在帮助到达之前能够做出反应。部署后不久,这些充气舱拯救了两名被困在海洋中的青少年,如图 8-26 所示。澳大利亚还利用无人机来检测鲨鱼,以便撤离海滩。可以预见到这些自动化、定制培训服务所能带来的巨大价值。

图 8-25. InDro Robotics 进行的检测

图 8-26. 无人机识别两名被困游泳者并释放充气舱,他们抓住充气舱(图片来源)
总结
在本章中,我们探讨了各种云 API 用于计算机视觉,首先定性比较提供的服务范围,然后定量比较它们的准确性和价格。我们还看到可能出现在结果中的偏见来源。我们看到,只需一小段代码片段,我们就可以在不到 15 分钟内开始使用这些 API。因为一个模型并不适用于所有情况,我们使用拖放界面训练了一个自定义分类器,并将多家公司进行了对比测试。最后,我们讨论了压缩和调整大小的建议,以加快图像传输速度,并了解它们对不同任务的影响。最后,我们研究了各行业的公司如何利用这些云 API 构建真实应用程序。祝贺您走到了这一步!在下一章中,我们将看到如何为自定义场景部署我们自己的推理服务器。
第九章:使用 TensorFlow Serving 和 KubeFlow 在云上进行可扩展推断服务
想象一下:你刚刚建立了一个一流的分类器。你的目标,正如硅谷的座右铭所说,“让世界变得更美好”,你将通过一个出色的狗/猫分类器来实现这一目标。你有一个扎实的商业计划,迫不及待地想要在下周向风险投资公司推销你的神奇分类器。你知道投资者会问及你的云策略,你需要在他们考虑给你钱之前展示一个扎实的演示。你会怎么做?创建模型只是战斗的一半,提供服务是下一个挑战,通常是更大的挑战。事实上,很长一段时间以来,训练模型只需要几周的时间,但是试图将其提供给更多人使用是一个长达数月的战斗,通常涉及后端工程师和 DevOps 团队。
在本章中,我们回答了一些关于托管和提供定制模型的常见问题。
-
我如何在我的个人服务器上托管我的模型,以便我的同事可以使用它?
-
我不是后端/基础设施工程师,但我想让我的模型可用,以便为成千上万(甚至数百万)的用户提供服务。如何在合理的价格下做到这一点,而不必担心可扩展性和可靠性问题?
-
有一些原因(如成本、法规、隐私等)使我无法将我的模型托管在云上,而只能在内部网络(我的工作网络)上托管。在这种情况下,我能够以规模和可靠性提供预测吗?
-
我可以在 GPU 上进行推断吗?
-
我可以期待每个选项的费用是多少?
-
我能否跨多个云提供商扩展我的训练和服务?
-
要使这些运行需要多少时间和技术知识?
让我们从高层次概述可用工具开始我们的旅程。
提供 AI 预测的景观
有许多工具、库和云服务可用于训练 AI 模型以提供预测请求。图 9-1 将它们简化为四类。

图 9-1. 不同推断服务选项的高级概述和比较
根据我们的推断场景,我们可以做出适当的选择。表 9-1 深入探讨。
表 9-1。通过网络提供深度学习模型的工具
| 类别和示例 | 预期的首次预测时间 | 优缺点 |
|---|
| HTTP 服务器
-
Flask
-
Django
-
Apache OpenWhisk
-
Python
http.server
| <5 分钟 | + 运行简单+ 通常运行当前 Python 代码– 较慢– 未经 AI 优化 |
|---|
| 托管和管理的云堆栈
-
Google Cloud ML
-
Azure ML
-
亚马逊 Sage Maker
-
Algorithmia
| <15 分钟 | + 更容易的 GUI/命令行界面+ 高度可扩展+ 完全托管,减少了对 DevOps 团队的需求– 通常仅限于基于 CPU 的推断,对于大型模型可能较慢– 预热查询时间可能较慢 |
|---|
| 手动管理的服务库
-
TensorFlow Serving
-
英伟达 TensorRT
-
DeepDetect
-
MXNet 模型服务
-
Skymind Intelligence Layer with DeepLearning4J
-
Seldon
-
DeepStack AI 服务器
| <15 分钟 | + 高性能+ 允许手动控制优化、批处理等+ 可以在 GPU 上运行– 设置更复杂– 要在多个节点上扩展通常需要额外的工作 |
|---|
| 云 AI 编排框架
- KubeFlow
| ~1 小时 | + 使扩展训练和推理易于管理+ 在云提供商之间可移植+ 开发和生产环境一致+ 对于数据科学家,与熟悉的工具(如 Jupyter Notebooks)集成,用于将模型发送到生产环境+ 可以组合条件管道以自动化测试、级联模型+ 使用现有的手动管理的服务库– 仍在发展中– 对于初学者,托管和管理的云堆栈提供了更简单的学习曲线 |
|---|
在本章中,我们探讨了一系列工具和场景。其中一些选项易于使用,但功能有限。其他选项提供更精细的控制和更高的性能,但设置更复杂。我们将看一个每个类别的例子,并深入研究,以便了解何时使用其中之一是有意义的。然后,我们将对不同解决方案进行成本分析,并详细介绍一些解决方案在实践中的工作方式。
Flask:构建自己的服务器
我们从构建自己的服务器(BYOS)的最基本技术开始。从表 9-1 的第一列中选择,我们选择了 Flask。
使用 Flask 创建 REST API
Flask 是一个基于 Python 的 Web 应用程序框架。它于 2010 年发布,在 GitHub 上拥有超过 46,000 颗星,正在持续开发。它快速且易于设置,对于原型设计非常有用。当数据科学从业者想要向一组有限的用户提供他们的模型(例如,在企业网络上与同事共享)时,通常会选择这个框架,而不会有太多麻烦。
使用pip安装 Flask 相当简单:
$ pip install flask
安装后,我们应该能够运行以下简单的“Hello World”程序:
from flask import Flask
app = Flask(__name__)
@app.route("/hello")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
以下是运行“Hello World”程序的命令:
$ python hello.py
* Running on http://127.0.0.1:5000/ (Press Ctrl+C to quit)
默认情况下,Flask 在端口 5000 上运行。当我们在浏览器中打开 URL http://localhost:5000/hello时,我们应该看到“Hello World!”这几个字,如图 9-2 所示。

图 9-2。在 Web 浏览器中导航到 http://localhost:5000/hello,查看“Hello World!”网页
正如您所看到的,只需几行代码就可以让一个简单的 Web 应用程序运行起来。在该脚本中最重要的一行是@app.route("/hello")。它指定了主机名后的路径/hello将由其下方的方法提供服务。在我们的情况下,它只是返回字符串“Hello World!”在下一步中,我们将看看如何将 Keras 模型部署到 Flask 服务器,并创建一个路由来提供我们模型的预测。
将 Keras 模型部署到 Flask
我们的第一步是加载我们的 Keras 模型。以下行从.h5文件中加载模型。您可以在本书的 GitHub(请参阅http://PracticalDeepLearning.ai)的code/chapter-9目录中找到本章的脚本:
from tf.keras.models import load_model
model = load_model("dogcat.h5")
现在,我们创建路由/infer,以支持对我们的图像进行推理。自然地,我们将支持POST请求以接受图像:
@app.route('/infer', methods=[POST])
def infer():
file = request.files['file']
image = Image.open(file)
image = preprocess(image)
predictions = model.predict(image)
max_index = numpy.argmax(predictions)
# We know the labels from the model we trained previously
if max_index == 0:
return "Cat"
else:
return "Dog"
为了测试推理,让我们使用curl命令,在包含一只狗的示例图像上进行如下操作:
$ curl -X POST -F image=@dog.jpg 'http://localhost:5000/infer'
{"predictions":[{"label":"dog","probability":0.8525022864341736}]}
正如预期的那样,我们得到了“dog”的预测。到目前为止,这个方法运行得相当顺利。此时,Flask 只在本地运行;也就是说,网络上的其他人无法向该服务器发出请求。要使 Flask 对其他人可用,我们只需将app.run()更改为以下内容:
app.run(host="0.0.0.0")
在这一点上,我们可以让我们的模型对我们网络内的任何人开放。下一个问题将是——我们是否可以做同样的事情让模型对普通公众可用?对于这个问题的答案是一个坚定的否定!Flask 网站上有一个显著的警告:“警告:不要在生产环境中使用开发服务器。” Flask 确实不支持开箱即用的生产工作,需要自定义代码来启用。在接下来的部分中,我们将看看如何在适用于生产使用的系统上托管我们的模型。考虑到所有这些,让我们回顾一下使用 Flask 的一些优缺点。
使用 Flask 的优点
Flask 提供了一些优势,包括:
-
快速设置和原型设计
-
快速开发周期
-
资源占用轻
-
在 Python 社区中具有广泛吸引力
使用 Flask 的缺点
同时,出于以下原因,Flask 可能不是您的最佳选择:
-
无法扩展;默认情况下,它不适用于生产负载。Flask 一次只能处理一个请求
-
开箱即用不支持模型版本控制
-
不支持批量请求处理
生产级服务系统中的理想特质
对于任何从公共网络提供流量的云服务,我们在决定使用解决方案时希望看到某些属性。在机器学习的背景下,在构建推理服务时,我们会寻找额外的特质。如果本节中有一些特质,我们将看一下其中的一些。
高可用性
为了让我们的用户信任我们的服务,它必须几乎始终可用。对于许多严肃的参与者,他们用“九的数量”来衡量他们的可用性指标。如果一个企业声称其服务有四个九的可用性,他们的意思是系统 99.99%的时间都是正常运行和可用的。尽管 99%听起来令人印象深刻,表 9-2 将每年的停机时间放在了透视中。
表 9-2。不同可用性百分比的每年停机时间
| 可用性 % | 每年停机时间 |
|---|---|
| 99%(“两个九”) | 3.65 天 |
| 99.9%(“三个九”) | 8.77 小时 |
| 99.99%(“四个九”) | 52.6 分钟 |
| 99.999%(“五个九”) | 5.26 分钟 |
想象一下,如果像亚马逊这样的主要网站只有 99.9%的可用性,每年超过八个小时的停机时间将导致数百万用户收入损失。五个九被认为是圣杯。少于三个九的任何可用性通常不适合高质量的生产系统。
可扩展性
生产服务处理的流量在较长时间段内几乎从不是均匀的。例如,《纽约时报》在早晨经历的流量明显更多,而 Netflix 通常在晚上和深夜经历流量激增,人们在那时放松。流量还受季节因素影响。亚马逊在黑色星期五和圣诞季节经历了数量级更多的流量。
更高的需求需要更多的资源可用和在线以为他们提供服务。否则,系统的可用性将受到威胁。实现这一目标的一种天真的方法是预测系统将提供的最高流量量,确定为提供该流量水平所需的资源数量,然后永久分配该数量。这种方法有两个问题:1)如果您的规划是正确的,那么大部分时间资源将被低效利用,实际上是在浪费金钱;2)如果您的估计不足,您可能会影响服务的可用性,并最终失去客户的信任和钱包。
管理流量负载的更智能的方法是在流入时监视它们,并动态分配和释放可用于服务的资源。这确保了增加的流量在不丢失服务的情况下处理,同时在低流量时期将运营成本降至最低。
在缩减资源时,即将被释放的任何资源很可能正在处理流量。在关闭该资源之前,必须确保所有这些请求都已完成。此外,关键是资源不能处理任何新请求。这个过程被称为排空。当机器因例行维护和/或升级而关闭时,排空也至关重要。
低延迟
考虑这些事实。亚马逊在 2008 年发表了一项研究,发现其零售网站的每增加 100 毫秒的延迟会导致 1%的利润损失。网站加载延迟一秒会导致高达 16 亿美元的收入损失!谷歌发现移动网站 500 毫秒的延迟导致流量下降 20%。换句话说,广告服务的机会减少了 20%。而这不仅影响行业巨头。如果一个网页在手机上加载时间超过三秒,53%的用户会放弃它(根据谷歌 2017 年的一项研究)。显然,时间就是金钱。
报告平均延迟可能会产生误导,因为它可能比实际情况更乐观。这就好比说如果比尔·盖茨走进一个房间,那么每个人平均都是亿万富翁。相反,百分位延迟通常是报告的指标。例如,一个服务可能报告 99th 百分位的 987 毫秒。这意味着 99%的请求在 987 毫秒或更短的时间内得到响应。同一个系统的平均延迟可能是 20 毫秒。当然,随着对您的服务的流量增加,如果服务没有扩展以提供足够的资源,延迟可能会增加。因此,延迟、高可用性和可伸缩性是相互交织在一起的。
地理可用性
纽约和悉尼之间的距离接近 10,000 英里(16,000 公里)。真空中的光速大约为每秒 186,282 英里(每秒 300,000 公里)。二氧化硅玻璃(用于光纤电缆)将光速降低约 30%,降至每秒 130,487 英里(每秒 210,000 公里)。在连接这两个城市之间的一条直线上运行的光纤上,单个请求的往返传输时间仅为约 152 毫秒。请记住,这并不考虑请求在服务器上处理所需的时间,或者数据包在途中通过多个路由器进行跳转的时间。对于许多应用程序来说,这种服务水平是不可接受的。
希望在全球范围内使用的服务必须被战略性地放置,以最小化用户在这些地区的延迟。此外,资源可以根据当地流量动态扩展或缩减,从而提供更精细的控制。主要的云服务提供商至少在五个大洲设有基地(抱歉企鹅!)。
提示
想要模拟从您的计算机到世界各地特定数据中心的传入请求需要多长时间吗?表 9-3 列出了一些由云服务提供商提供的方便的基于浏览器的工具。
表 9-3. 不同云服务提供商的延迟测量工具
| 服务 | 云服务提供商 |
|---|---|
| AzureSpeed.com | 微软 Azure |
| CloudPing.info | 亚马逊网络服务 |
| GCPing.com | 谷歌云平台 |
此外,为了确定从一个位置到另一个位置的延迟的现实组合,CloudPing.co测量了 AWS 区域间的延迟,包括 16 个以上美国 AWS 数据中心之间的延迟。
故障处理
有一句古老的谚语说生活中只有两件事是确定的——死亡和税收。在 21 世纪,这句格言不仅适用于人类,也适用于计算机硬件。机器经常出现故障。问题从来不是机器会出现故障,而是何时会出现故障。生产质量服务的一个必要特性是它能够优雅地处理故障。如果一台机器出现故障,迅速启动另一台机器来替代它并继续提供流量服务。如果整个数据中心出现故障,无缝地将流量路由到另一个数据中心,以便用户甚至不会意识到一开始发生了任何不好的事情。
监控
如果你不能测量它,你就无法改进它。更糟糕的是,它甚至存在吗?监控请求数量、可用性、延迟、资源使用情况、节点数量、流量分布和用户位置对于了解服务的表现至关重要;找到改进的机会;更重要的是,要支付多少。大多数云提供商已经内置了提供这些指标的仪表板。此外,记录任务特定的分析,如模型推断、预处理等的时间,可以增加另一层理解。
模型版本控制
我们在本书中学到(并将继续学习直到最后一页)机器学习始终是迭代的。特别是在现实世界的应用中,模型可以学习的数据不断生成。此外,传入数据的分布可能随时间而变化,与训练时相比,导致预测能力降低(称为概念漂移现象)。为了为用户提供最佳体验,我们希望不断改进我们的模型。每次我们使用更新的数据训练模型以进一步提高其准确性并制作最佳版本时,我们希望尽快而无缝地为用户提供。任何良好的生产质量推断系统都应该提供提供模型的不同版本的能力,包括在一瞬间交换模型的实时版本的能力。
A/B 测试
除了支持模型的多个版本之外,我们希望根据用户的地理位置、人口统计数据或简单的随机分配等各种属性,在同一时间为模型提供不同版本的原因。
A/B 测试是改进模型时特别有用的工具。毕竟,如果我们全新的模型在某种程度上存在缺陷,我们宁愿它只部署给我们用户的一个小子集,而不是同时部署给所有用户,以便在捕捉到缺陷之前。此外,如果模型在这个小子集上符合成功标准,它为实验提供了验证,并证明最终可以提升到所有用户。
支持多个机器学习库
最后但并非最不重要的是,我们不想被锁定在单一的机器学习库中。组织中的一些数据科学家可能会在 PyTorch 中训练模型,其他人可能会在 TensorFlow 中训练,或者对于非深度学习任务,scikit-Learn 可能就足够了。支持多个库的灵活性将是一个受欢迎的奖励。
Google Cloud ML Engine:托管的云 AI 服务堆栈
考虑到我们在前一节中讨论的生产环境中所有理想的特性,使用 Flask 为用户提供服务通常不是一个好主意。如果您没有专门的基础设施团队,并且希望花更多时间制作更好的模型而不是部署它们,那么使用托管的云解决方案是正确的方法。今天市场上有几种基于云的推断即服务解决方案。我们选择探索 Google Cloud ML Engine 部分原因是因为方便的 TensorFlow 集成,部分原因是因为它与我们在第十三章中提到的 ML Kit 材料很好地结合在一起。
使用 Cloud ML Engine 的优点
-
易于部署到生产环境中,具有基于 Web 的 GUI
-
功能强大,易于扩展到数百万用户
-
提供深入的模型使用洞察
-
能够对模型进行版本控制
使用 Cloud ML Engine 的缺点
-
高延迟,仅提供 CPU 进行推断(截至 2019 年 8 月)
-
不适用于涉及法律和数据隐私问题的场景,其中数据不能离开网络
-
对复杂应用程序的架构设计施加限制
构建分类 API
以下逐步指南展示了如何在 Google Cloud ML Engine 上上传和托管我们的 Dog/Cat 分类器模型:
-
在https://console.cloud.google.com/mlengine/models上在 Google Cloud ML Engine 仪表板上创建一个模型。因为这是我们第一次使用仪表板,我们需要单击“启用 API”,如图 9-3 所示。
![Google Cloud ML Engine 仪表板上机器学习模型的列表页面]()
图 9-3. Google Cloud ML Engine 仪表板上机器学习模型的列表页面
-
为模型命名并添加描述(图 9-4)。
![Google Cloud ML Engine 上的模型创建页面]()
图 9-4. Google Cloud ML Engine 上的模型创建页面
-
模型创建完成后,我们可以在列表页面上访问模型(图 9-5)。
![Google Cloud ML Engine 上的模型列表页面]()
图 9-5. Google Cloud ML Engine 上的模型列表页面
-
单击模型以转到模型详细信息页面(图 9-6)并添加新版本。
![刚创建的 DogCat 分类器的详细页面]()
图 9-6. 刚创建的 Dog/Cat 分类器的详细页面
-
填写必要信息以创建新版本。底部的最后一个字段要求您在使用之前将模型上传到 Google Cloud Storage。单击“浏览”按钮以创建用于存储模型的新存储桶(图 9-7)。
![为机器学习模型创建新版本]()
图 9-7. 为机器学习模型创建一个新版本
-
创建一个具有唯一名称、存储类和区域的新存储桶。创建此存储桶后,转到https://console.cloud.google.com/storage/browser(在保持当前标签页打开的同时在新标签页中打开)找到这个新创建的存储桶并将模型上传到那里(图 9-8)。
![在 ML 模型版本创建页面内创建一个新的 Google Cloud Storage 存储桶]()
图 9-8. 在 ML 模型版本创建页面内创建一个新的 Google Cloud Storage 存储桶
-
我们的 Dog/Cat 分类器模型是一个.h5文件。但是,Google Cloud 期望一个 SavedModel 文件。您可以在本书的 GitHub 存储库(请参阅
PracticalDeepLearning.ai)的code/chapter-9/scripts/h5_to_tf.ipynb中找到将.h5文件转换为 SavedModel 的脚本。只需加载模型并执行笔记本的其余部分。 -
在 Google Cloud Storage 浏览器中,将新转换的模型(图 9-9)上传到您在第 6 步中创建的存储桶中。
![Google Cloud Storage 浏览器页面显示以 TensorFlow 格式上传的 DogCat 分类器模型]()
图 9-9. Google Cloud Storage 浏览器页面显示以 TensorFlow 格式上传的 Dog/Cat 分类器模型
-
在模型版本创建页面上指定您刚上传的模型的 URI(图 9-10)。
![将上传到 Google Cloud Storage 的模型的 URI 添加到其中]()
图 9-10. 将上传到 Google Cloud Storage 的模型的 URI 添加到其中
-
点击保存按钮,等待模型版本创建。一旦模型版本创建完成,您就可以开始针对它进行预测。
-
如果您的机器上还没有安装,您可以从安装网站https://cloud.google.com/sdk/install下载并安装 Google Cloud SDK。
-
您可以使用 Cloud ML Engine REST API 进行请求。但是,为了简洁起见,请使用 Cloud SDK 中的命令行工具。您首先需要使用位于code/chapter-9的image-to-json.py脚本将图像转换为request.json文件:
$ python image-to-json.py --input dog.jpg --output request.json -
接下来,使用在上一步中创建的request.json文件来执行针对我们模型的请求:
$ time cloud ai-platform predict --model DogCat --version v1 --json-instances request.json SCORES [0.14749771356, 0.8525022864] real 0m3.370s user 0m0.811s sys 0m0.182s
从输出中可以看到,我们得到了与我们的 Flask 服务器相似的结果;即“狗”的预测,置信度为 85%。
注意
如果这是您第一次使用gcloud,您需要运行以下命令将命令行工具与您的 Google 账户绑定:
$ gcloud auth login
接下来,使用以下命令选择项目:
$ gcloud config set project {project_name}
小菜一碟,不是吗?在我们的示例中,为了简洁起见,我们使用 Google Cloud SDK 请求预测。在生产场景中,您可能希望使用 Google 的 API 端点执行相同的预测请求;可以通过生成 HTTP 请求或使用他们的客户端库来执行。我们可以在 Google Cloud 文档中查看生产场景的文档。
此时,该模型已准备好通过浏览器、移动设备和边缘设备、桌面以及云环境上的应用程序提供给全球任何用户。对于希望在云提供的灵活性和可靠性的个人和组织来说,使用托管堆栈是一个相当可行的选择,同时对基础设施的设置和维护工作要求最小。
相比之下,有些情况下托管解决方案可能不是最佳选择。原因可能包括定价模型、数据隐私问题、法律问题、技术问题、信任问题或合同义务。在这种情况下,一个在本地托管和管理的解决方案将更可取。
提示
为了一次处理大量图像,您可以修改image-to-json.py以创建包含多个输入数组的request.json文件。
TensorFlow Serving
TensorFlow Serving 是 TensorFlow 生态系统中用于快速提供机器学习模型的开源库。与 Flask 不同,它专为性能而构建,具有低开销,并设计用于生产环境中使用。大公司广泛使用 TensorFlow Serving 来为他们的预测服务提供模型。它是 TensorFlow Extended (TFX)的一个重要组件,TFX 是 TensorFlow 生态系统中的端到端深度学习管道之一。
正如我们在查看生产系统所需的特性时看到的,TensorFlow Serving 提供低延迟、故障处理、高吞吐量和模型版本控制。另一个好处是能够在同一服务上同时提供多个模型。它实现了几种技术来加速服务:
-
在服务器启动期间,它启动了一批线程以快速加载模型。
-
它为加载模型和提供推理使用单独的线程池,同时给予推理池中的线程更高的优先级。这对降低请求延迟至关重要。
-
它在短时间内构建传入异步请求的小批量。正如我们所见,在训练期间在 GPU 上批处理数据的强大功能,它旨在在异步请求中带来类似的效率。例如,等待 500 毫秒来将几个推理请求分组。在最坏的情况下,这会为批处理中的第一个请求增加 500 毫秒的惩罚,但它会减少请求的平均延迟,并最大化硬件利用率。
注意
TensorFlow Serving 让你完全控制模型发布过程。你可以在同一个进程中为不同的模型或同一种模型的不同版本提供服务。你只需要确保你知道要删除或投入生产的版本的名称和位置。
安装
有几种不同的设置 TensorFlow Serving 的方法:
-
从源代码构建
-
使用 APT 下载和安装
-
部署 Docker 镜像
如果你感到冒险,从源代码构建可能是你寻求的危险。但如果你只是想快速启动并运行,我们建议使用 Docker,因为它需要最少的步骤来启动系统。你可能会问,什么是 Docker?Docker 为在其中运行的应用程序提供了 Linux 环境的虚拟化。它提供了资源隔离,基本上作为一个干净的板凳来设置一个应用程序可以运行的环境。通常,一个应用程序及其所有依赖项被打包到一个单独的 Docker 容器中,然后根据需要重复部署。因为应用程序是在一个干净的环境中设置的,它减少了配置和部署错误的可能性。这使得 Docker 非常适合在生产中运行应用程序。
Docker 为我们提供的最大好处是减轻“依赖地狱”,因为所有必要的依赖项都打包在容器中。使用 Docker 的另一个优势是,在不同平台上设置应用程序的过程基本上保持一致,无论你使用 Windows、Linux 还是 Mac。
Docker 安装说明,取决于目标平台,可在 Docker 主页上找到。这应该不会花费太多时间,因为设置非常简单。安装完 Docker 后,你可以运行以下命令来为 CPU 设置 TensorFlow Serving:
$ docker run -p 8501:8501 \
--mount type=bind,source=/path/to/dogcat/,target=/models/dogcat \
-e MODEL_NAME=dogcat -t tensorflow/serving
对于启用 GPU 的机器,运行以下命令:
$ docker run -p 8501:8501 --runtime=nvidia \
--mount type=bind,source=/path/to/dogcat/,target=/models/dogcat \
-e MODEL_NAME=dogcat -t tensorflow/serving
无论哪种情况,如果一切顺利,你应该在本地端口 8501 上运行一个 REST API,为我们的狗/猫分类器提供服务。
注意
在任何推理请求中,端到端延迟是沿着过程中多个步骤所花费的时间的总和。这包括往返网络时间,序列化/反序列化请求和响应对象的时间,当然还有执行实际推理的时间。增加开销的另一个组件是服务框架;也就是 TensorFlow Serving。谷歌声称 TensorFlow Serving 贡献的开销很小。在实验中,它观察到 TensorFlow Serving 单独能够在 16 核 Intel Xeon E5 2.6 GHz 机器上每个核心处理大约 100,000 个 QPS。因为它正在测量开销,这不包括远程过程调用(RPC)时间和 TensorFlow 推理处理时间。
尽管 TensorFlow Serving 是从单台机器提供推理的绝佳选择,但它没有用于水平扩展的内置功能。相反,它被设计用于与其他系统一起使用,这些系统可以通过动态扩展来增强 TensorFlow Serving。我们将在下一节中探讨一个这样的解决方案。
KubeFlow
在本书中,我们探讨了端到端深度学习流水线的各个步骤,从数据摄入、分析、规模化的分布式训练(包括超参数调整)、跟踪实验、部署,最终到规模化提供预测请求。每个步骤都有其自身的复杂性,具有一套工具、生态系统和专业领域。人们将毕生精力投入到这些领域中的一个。这并不是一件轻而易举的事情。当考虑到必要的后端工程、硬件工程、基础设施工程、依赖管理、DevOps、容错和其他工程挑战时,所需的专业知识的组合爆炸可能导致大多数组织的招聘过程非常昂贵。
正如我们在前一节中看到的,Docker 通过提供可移植的容器,省去了我们管理依赖关系的麻烦。它帮助我们轻松地在各个平台上提供 TensorFlow Serving,而无需从源代码构建或手动安装依赖项。太好了!但它仍然没有回答许多其他挑战。我们如何扩展容器以匹配需求的增长?我们如何有效地在容器之间分发流量?我们如何确保容器彼此可见并能够通信?
这些问题是由Kubernetes回答的。Kubernetes 是一个自动部署、扩展和管理容器(如 Docker)的编排框架。由于它利用了 Docker 提供的可移植性,我们可以使用 Kubernetes 在开发人员笔记本电脑和几千台机器集群上几乎相同的方式部署。这有助于在不同环境中保持一致性,并具有可访问的可扩展性。值得注意的是,Kubernetes 不是机器学习的专用解决方案(Docker 也不是);相反,它是解决软件开发中许多问题的通用解决方案,我们在深度学习的背景下使用它。
但让我们不要过于急躁。毕竟,如果 Kubernetes 是一切的解决方案,它就会出现在章节标题中!使用 Kubernetes 的机器学习从业者仍然需要组装所有适当的容器集合(用于训练、部署、监控、API 管理等),然后需要将它们协调在一起,以构建一个完全运作的端到端流水线。不幸的是,许多数据科学家正在尝试在他们自己的孤立环境中做这件事,重新发明轮子,构建临时的机器学习特定流水线。我们能不能省去所有人的麻烦,为机器学习场景制定一个基于 Kubernetes 的解决方案呢?
KubeFlow登场了,它承诺自动化大部分这些工程挑战,并隐藏了运行分布式、可扩展、端到端深度学习系统的复杂性背后的一个基于 Web GUI 的工具和一个强大的命令行工具。这不仅仅是一个推理服务。把它看作一个大型工具生态系统,可以无缝地互操作,更重要的是,可以随需求扩展。KubeFlow 是为云构建的。虽然不只是一个云——它建立在与所有主要云提供商兼容的基础上。这对成本有重大影响。因为我们不受限于特定的云提供商,如果竞争云提供商降低价格,我们可以随时自由地转移所有操作。毕竟,竞争有利于消费者。
KubeFlow 支持各种硬件基础设施,从开发人员的笔记本电脑和本地数据中心,一直到公共云服务。由于它是建立在 Docker 和 Kubernetes 之上的,我们可以放心,无论是在开发人员的笔记本电脑上部署还是在数据中心的大型集群上部署,环境都是相同的。开发人员设置与生产环境不同的任何方式都可能导致故障,因此在各种环境中保持一致性非常有价值。
表 9-4 展示了 KubeFlow 生态系统中现成可用的工具的简要列表。
表 9-4. KubeFlow 上可用的工具
| 工具 | 功能 |
|---|---|
| Jupyter Hub | 笔记本环境 |
| TFJob | 训练 TensorFlow 模型 |
| TensorFlow Serving | 服务 TensorFlow 模型 |
| Seldon | 服务模型 |
| NVIDIA TensorRT | 服务模型 |
| Intel OpenVINO | 服务模型 |
| KFServing | 用于服务 Tensorflow、XGBoost、scikit-learn、PyTorch 和 ONNX 模型的抽象 |
| Katib | 超参数调整和 NAS |
| Kubebench | 运行基准测试作业 |
| PyTorch | 训练 PyTorch 模型 |
| Istio | API 服务、身份验证、A/B 测试、发布、指标 |
| Locust | 负载测试 |
| 管道 | 管理实验、作业和运行,调度机器学习工作流程 |
社区中有一个笑话,有这么多预打包技术,KubeFlow 最终使我们的简历符合流行词(和招聘人员)的要求。
注
许多人认为 KubeFlow 是 Kubernetes 和 TensorFlow 的组合,但正如您所见,事实并非如此。它是那样,而且更多。
KubeFlow 的两个重要部分使其独特:管道和公平。
管道
管道使我们能够在机器学习中组合步骤,安排复杂的工作流程。图 9-11 展示了一个管道的示例。通过 GUI 工具查看管道可以帮助利益相关者理解它(不仅仅是构建它的工程师)。

图 9-11. 在 KubeFlow 中展示的端到端管道
公平
公平允许我们通过 Jupyter Notebooks 直接管理整个构建、训练和部署生命周期。图 9-12 展示了如何启动一个新的笔记本服务器,在那里我们可以托管所有的 Jupyter 笔记本,在上面运行训练,并使用几行代码将我们的模型部署到谷歌云,同时还可以在非常熟悉的 Jupyter 环境中进行操作:
from fairing.deployers.gcp.gcpserving import GCPServingDeployer
GCPServingDeployer().deploy(model_dir, model_name, version_name)

图 9-12. 在 KubeFlow 上创建一个新的 Jupyter Notebook 服务器
安装
创建一个新的 KubeFlow 部署是一个非常简单的过程,在 KubeFlow 网站上有详细的文档。您可以使用浏览器为 GCP 设置 KubeFlow。或者,您可以使用 KubeFlow 命令行工具在 GCP、AWS 和 Microsoft Azure 上设置部署。图 9-13 展示了使用 Web 浏览器进行 GCP 部署。

图 9-13. 在浏览器上创建一个在 GCP 上使用 KubeFlow 部署
截至目前,KubeFlow 正在积极开发,并没有停止的迹象。像红帽、思科、戴尔、优步和阿里巴巴这样的公司是一些积极的贡献者,云巨头如微软、谷歌和 IBM 也是如此。解决困难挑战的简易性和可访问性吸引更多人使用任何平台,而 KubeFlow 正是在做这件事。
价格与性能考虑
在第六章中,我们看了如何提高我们的模型性能,无论是在智能手机上还是在服务器上进行推理。现在让我们从另一个角度来看:硬件性能和涉及的价格。
在构建生产系统时,我们通常希望灵活选择合适的硬件,以在性能、规模和价格之间取得适当的平衡。考虑构建一个需要基于云的推理的应用程序。我们可以手动设置我们自己的堆栈(使用 Flask 或 TensorFlow Serving 或 KubeFlow),或者我们可以使用托管的推理即服务堆栈(如 Google Cloud ML Engine)。假设我们的服务爆红了,让我们看看会花费多少。
推理即服务的成本分析
对于 Google Cloud ML Engine,在 2019 年 8 月的北美地区,单核 CPU 机器的综合推理时间每小时的成本相当便宜,为 0.0401 美元。还有一个四核 CPU 机器的选项,但实际上,单核应该足够满足大多数应用程序的需求。向服务器运行几个查询,使用一个 12 KB 的小图像,平均需要大约 3.5 秒,如图 9-14 所示。这听起来很慢,部分原因是在一个中等速度的机器上进行推理,更重要的是在一个 CPU 服务器上。值得一提的是,这个基准测试是在一个已经接收到 API 请求并且已经预加载了模型的机器上进行的。相比之下,第一个查询需要 30 到 60 秒。这显示了保持服务持续运行或发送频繁的预热查询的重要性。这是因为如果 Google Cloud ML 引擎注意到长时间不使用,它会关闭模型。

图 9-14。Google Cloud ML Engine 显示传入查询和提供调用的延迟,用户端的端到端延迟约为 3.5 秒
如果每秒有一个请求持续一个月,总共将有 60 x 60 x 24 x 30 = 2,592,000 次调用。假设每个推理需要 3.5 秒,单个节点将不足以满足需求。云服务会迅速意识到这一点,并针对增加的流量,启动三台额外的机器来处理流量。总共,四台机器在每个节点每小时 0.0401 美元的情况下运行一个月,总成本将为 115.48 美元。换句话说,对于两百万次调用,这大约相当于一个月每天一杯星巴克咖啡的成本。而且我们不要忘记这还没有涉及太多的 DevOps 团队成员,他们的时间是昂贵的。如果我们考虑一个类似 Yelp 的服务的假设情景,用户平均每秒上传 64 个 QPS 的食物照片,使用分类模型对其进行推理只需要花费 7390 美元。
构建自己的堆栈的成本分析
少花钱,高可扩展性,这是一个成功的组合。但唯一的缺点是每个请求的总往返延迟。自己动手,获取一个在云上带有适度 GPU 的虚拟机,并设置我们的扩展管道(使用 KubeFlow 或使用 TensorFlow Serving 的本地云负载平衡功能),我们可以在毫秒内做出响应,或者批量处理几个传入的查询(比如每 500 毫秒)来提供服务。例如,在 Azure 的 VM 清单中,我们可以以每小时 2.07 美元的价格租用一台 ND6 机器,该机器配备了 NVIDIA P40 GPU 和 112 GiB RAM。通过每 500 毫秒到 1 秒批量处理传入请求,这台机器可以以每月 1490 美元的总成本为 64 个请求每秒提供服务,比 Google Cloud ML Engine 更快。
总之,在处理大量 QPS 场景时,自己编排云机器环境的成本节约和性能优势在图 9-15 中得到了充分展示。

图 9-15。基础设施即服务(Google Cloud ML Engine)与在虚拟机上构建自己的堆栈(Azure VM)的成本比较(截至 2019 年 8 月的成本)
提示
在基准测试中经常出现的一个问题是我的系统的极限是什么?JMeter可以帮助回答这个问题。JMeter 是一个负载测试工具,可以让您通过易于使用的图形界面对系统进行压力测试。它允许您创建可重用的配置来模拟各种使用场景。
总结
在这一章中,我们回答了大多数工程师和开发人员提出的问题:在现实世界的应用程序中如何扩展为规模化的模型预测请求?我们探讨了四种不同的方法来提供图像识别模型:使用 Flask、Google Cloud ML、TensorFlow Serving 和 KubeFlow。根据规模、延迟要求和我们的技能水平,一些解决方案可能比其他更具吸引力。最后,我们对不同堆栈的成本效益有了直观的了解。现在我们可以向世界展示我们出色的分类器模型,剩下的就是让我们的工作走向病毒式传播!
第十章:在浏览器中使用 TensorFlow.js 和 ml5.js 的人工智能
与客座作者 Zaid Alyafeai 合作撰写
你是一个有远大梦想的开发者。你有一个了不起的 AI 模型,你希望很多人尝试。多少人算是很多?一万人?一百万人?不,傻瓜。你喜欢远大梦想。一亿人怎么样?这是一个很好的圆数。现在说服一亿人下载并安装一个应用程序,并为其在他们的手机上腾出空间并不容易。但如果我们告诉你,他们已经为你安装了一个应用程序。无需下载。无需安装。无需应用商店。这是什么黑魔法!当然,这就是 Web 浏览器。而且作为一个奖励,它也可以在你的 PC 上运行。
这就是谷歌在决定向数十亿用户推出其首个 AI 涂鸦时在其主页上所做的事情。而选择的主题比 J.S. Bach 的音乐更好的是什么呢。(Bach 的父母想要叫他 J.S. Bach,比 JavaScript 诞生还早了 310 年。他们有相当远见!)
简单来说,这个涂鸦允许任何人使用鼠标点击写下两小节的随机音符的一行(声音)。当用户点击一个标有“和谐”的按钮时,输入将被处理,与 Bach 写的数百首包含两到四行(声音)音乐的音乐相比较。系统会找出哪些音符与用户的输入最搭配,以创造出更丰富的 Bach 风格的音乐作品。整个过程在浏览器中运行,因此谷歌不需要扩展其机器学习预测基础设施。

图 10-1. 谷歌的 Bach 音乐和谐器涂鸦
除了成本节省和在任何平台上运行的能力外,通过浏览器,我们可以为用户提供更丰富、更交互式的体验,因为网络延迟不是一个因素。当然,因为一旦模型被下载后,一切都可以在本地运行,最终用户可以从他们数据的隐私中受益。
考虑到 JavaScript 是 Web 浏览器的语言,深入研究可以在用户浏览器中运行我们训练好的模型的基于 JavaScript 的深度学习库对我们来说是有用的。这正是我们在本章中要做的事情。
在这里,我们专注于在浏览器中实现深度学习模型。首先,我们看一下不同基于 JavaScript 的深度学习框架的简要历史,然后转向 TensorFlow.js,最终是一个称为 ml5.js 的更高级抽象。我们还会检查一些复杂的基于浏览器的应用程序,比如检测一个人的身体姿势或将手绘涂鸦转换为照片(使用 GANs)。最后,我们会谈论一些实际考虑因素,并展示一些真实案例研究。
基于 JavaScript 的机器学习库:简要历史
近年来深度学习的突破,许多尝试都是为了让更多人以 Web 库的形式访问 AI。表 10-1 提供了不同库的简要概述,按照它们首次发布的顺序。
表 10-1. 不同基于 JavaScript 的深度学习库的历史概述(截至 2019 年 8 月的数据)
| 活跃年份 | GitHub 上的★ | 以其闻名 | |
|---|---|---|---|
| brain.js | 2015–至今 | 9,856 | 神经网络,RNNs,LSTMs 和 GRUs |
| ConvNetJS | 2014–2016 | 9,735 | 神经网络,CNNs |
| Synaptic | 2014–至今 | 6,571 | 神经网络,LSTMs |
| MXNetJS | 2015–2017 | 420 | 运行 MXNet 模型 |
| Keras.js | 2016–2017 | 4,562 | 运行 Keras 模型 |
| CaffeJS | 2016–2017 | 115 | 运行 Caffe 模型 |
| TensorFlow.js(以前称为 deeplearn.js) | 2017 至今 | 11,282 | 在 GPU 上运行 TensorFlow 模型 |
| ml5.js | 2017 至今 | 2,818 | 在 TF.js 之上易于使用。 |
| ONNX.js | 2018 至今 | 853 | 速度,运行 ONNX 模型 |
让我们更详细地了解一些这些库,并看看它们是如何发展的。
ConvNetJS
ConvNetJS是由 Andrej Karpathy 于 2014 年设计的 JavaScript 库,作为他在斯坦福大学博士期间的课程的一部分。它在浏览器中训练 CNN,这是一个令人兴奋的提议,特别是在 2014 年,考虑到 AI 热潮开始兴起,开发人员不必经历繁琐和痛苦的设置过程即可运行。ConvNetJS 通过在浏览器中进行交互式训练演示,帮助许多人第一次接触 AI。
注意
事实上,当麻省理工学院的科学家 Lex Fridman 在 2017 年教授他的热门自动驾驶课程时,他挑战全球学生使用强化学习在浏览器中使用 ConvNetJS 训练模拟自动驾驶汽车,如图 10-2 所示。

图 10-2。使用 ConvNetJS 进行强化学习训练汽车的 DeepTraffic 截图
Keras.js
Keras.js 是由 Leon Chen 于 2016 年推出的。它是一个在浏览器中使用 JavaScript 的 Keras 端口。Keras.js 使用 WebGL 在 GPU 上运行计算。它使用着色器(用于像素渲染的特殊操作)来运行推断,这使得它们比仅使用 CPU 运行要快得多。此外,Keras.js 可以在 CPU 上的 Node.js 服务器上运行以提供基于服务器的推断。Keras.js 实现了一些卷积、密集、池化、激活和 RNN 层。它已不再处于活跃开发阶段。
ONNX.js
由 Microsoft 于 2018 年创建,ONNX.js 是一个 JavaScript 库,用于在浏览器和 Node.js 中运行 ONNX 模型。ONNX 是一个代表机器学习模型的开放标准,是 Microsoft、Facebook、Amazon 等公司的合作。ONNX.js 速度惊人。事实上,在早期基准测试中,甚至比 TensorFlow.js(下一节讨论)更快,如图 10-3 和图 10-4 所示。这可能归因于以下原因:
-
ONNX.js 利用 WebAssembly(来自 Mozilla)在 CPU 上执行,利用 WebGL 在 GPU 上执行。
-
WebAssembly 允许在 Web 浏览器中运行 C/C++和 Rust 程序,同时提供接近本机性能。
-
WebGL 在浏览器中提供像图像处理这样的 GPU 加速计算。
-
尽管浏览器往往是单线程的,但 ONNX.js 使用 Web Workers 在后台提供多线程环境以并行化数据操作。

图 10-3。在不同 JavaScript 机器学习库上对 ResNet-50 进行基准测试的数据(数据来源)

图 10-4。在不同 JavaScript 机器学习库上对 ResNet-50 进行基准测试的数据(数据来源)
TensorFlow.js
一些库提供了在浏览器内训练的能力(例如 ConvNetJS),而其他库提供了极快的性能(例如现已停用的 TensorFire)。来自 Google 的 deeplearn.js 是第一个支持使用 WebGL 进行快速 GPU 加速操作的库,同时还提供了在浏览器内定义、训练和推理的能力。它提供了即时执行模型(用于推理)以及延迟执行模型进行训练(类似于 TensorFlow 1.x)。这个项目最初于 2017 年发布,成为了 TensorFlow.js(2018 年发布)的核心。它被认为是 TensorFlow 生态系统的一个重要组成部分,因此目前是最活跃开发的 JavaScript 深度学习库。考虑到这一事实,我们在本章中专注于 TensorFlow.js。为了使 TensorFlow.js 更加简单易用,我们还看看 ml5.js,它构建在 TensorFlow.js 之上,抽象出其复杂性,提供了一个简单的 API,其中包含从 GAN 到 PoseNet 的现成模型。
TensorFlow.js 架构
首先,让我们看一下 TensorFlow.js 的高级架构(请参见图 10-5)。TensorFlow.js 直接在桌面和移动设备的浏览器中运行。它利用 WebGL 进行 GPU 加速,但也可以回退到浏览器的运行时以在 CPU 上执行。
它由两个 API 组成:操作 API 和层 API。操作 API 提供对低级操作(如张量算术和其他数学运算)的访问。层 API 建立在操作 API 之上,提供卷积、ReLU 等层。

图 10-5。TensorFlow.js 和 ml5.js 生态系统的高级概述
除了浏览器,TensorFlow.js 还可以在 Node.js 服务器上运行。此外,ml5.js 使用 TensorFlow.js 提供了一个更高级别的 API 以及几个预构建的模型。在不同抽象级别上都可以访问所有这些 API,使我们能够构建 Web 应用程序,不仅可以进行简单的推理,还可以在浏览器内部训练模型。
以下是在基于浏览器的 AI 开发生命周期中经常出现的一些常见问题:
-
我如何在浏览器中运行预训练模型?我可以使用我的网络摄像头实时交互吗?
-
我如何从我的 TensorFlow 训练模型为浏览器创建模型?
-
我能在浏览器中训练模型吗?
-
不同的硬件和浏览器如何影响性能?
我们在本章中回答了这些问题,首先是 TensorFlow.js,然后是 ml5.js。我们探索了 ml5.js 社区贡献的一些丰富的内置功能,否则直接在 TensorFlow.js 上实现将需要大量的工作和专业知识。我们还看看了一些创意开发者构建的激励示例之前的基准方法。
现在让我们看看如何利用预训练模型在浏览器中进行推理。
使用 TensorFlow.js 运行预训练模型
TensorFlow.js 提供了许多预训练模型,我们可以直接在浏览器中运行。一些示例包括 MobileNet、SSD 和 PoseNet。在下面的示例中,我们加载了一个预训练的 MobileNet 模型。完整的代码位于本书的 GitHub 存储库(请参见http://PracticalDeepLearning.ai)中的code/chapter-10/mobilenet-example/。
首先,我们导入库的最新捆绑包:
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/
tfjs@latest/dist/tf.min.js"></script>
然后我们导入 MobileNet 模型:
<script src="https://cdn.jsdelivr.net/npm/
@tensorflow-models/mobilenet@1.0.0"></script>
现在我们可以使用以下代码进行预测:
<img id="image" src="cat.jpg" />
<p id="prediction_output">Loading predictions...</p>
<script>
const image = document.getElementById("image");
const predictionOutput = document.getElementById("prediction_output");
// Load the model.
mobilenet.load().then(model => {
// Classify the image. And output the predictions
model.classify(image).then(predictions => {
predictionOutput.innerText = predictions[0].className;
});
});
</script>
图 10-6 显示了一个示例输出。

图 10-6。浏览器中的类别预测输出
我们可以通过以下方式使用 JSON 文件 URL 加载模型:
const path = 'https://storage.googleapis.com/tfjs-
models/tfjs/mobilenet_v1_1.0_224/model.json';
model = tf.loadLayersModel(path).then(model => {
// Load model and output predictions here
});
JSON 文件包含模型的架构、参数名称和较小的分片权重文件的路径。分片使文件足够小,可以被 Web 浏览器缓存,这将使加载对于之后需要模型的任何时间更快。
用于浏览器的模型转换
在前一节中,我们看了如何加载一个已经以 JSON 格式存在的预训练模型。在本节中,我们将学习如何将一个预训练的 Keras 模型(.h5格式)转换为与 TensorFlow.js 兼容的 JSON 格式。为此,我们需要使用pip安装转换工具。
$ pip install tensorflowjs
假设我们训练的 Keras 模型存储在名为keras_model的文件夹中,我们可以使用以下命令进行转换:
$ tensorflowjs_converter --input_format keras keras_model/model.h5 web_model/
现在web_model目录将包含我们可以使用tf.loadLayersModel方法轻松加载的.json和.shard文件:
$ ls web_model
group1-shard1of4 group1-shard3of4 model.json group1-shard2of4 group1-shard4of4
就是这样!将我们训练好的模型带到浏览器中是一项简单的任务。对于我们没有现成训练好的模型的情况,TensorFlow.js 也允许我们直接在浏览器中训练模型。在下一节中,我们将通过创建一个使用网络摄像头视频流训练模型的端到端示例来探索这一点。
提示
在本地加载模型需要运行一个 Web 服务器。我们可以使用很多选项,从 LAMP(Linux, Apache, MySQL, PHP)堆栈到使用 npm 安装http-server,甚至在 Windows 上运行 Internet Information Services(IIS)来本地测试模型加载。甚至 Python 3 也可以运行一个简单的 Web 服务器:
$ python3 -m http.server 8080
在浏览器中训练
前面的例子使用了一个预训练模型。让我们提高一下,直接在浏览器中使用来自网络摄像头的输入训练我们自己的模型。就像在之前的一些章节中一样,我们看一个简单的例子,利用迁移学习来加快训练过程。
从 Google 的 Teachable Machine 改编,我们使用迁移学习构建一个简单的二元分类模型,将使用网络摄像头的视频流进行训练。为了构建这个模型,我们需要一个特征提取器(将输入图像转换为特征或嵌入),然后连接一个网络将这些特征转换为预测。最后,我们可以使用网络摄像头的输入进行训练。代码可以在书的 GitHub 存储库中找到(参见http://PracticalDeepLearning.ai),路径为code/chapter-10/teachable-machine。
注意
Google Creative Lab 建立了一个有趣的互动网站叫做Teachable Machine,用户可以通过简单地在网络摄像头前展示这些对象来训练任何类型的分类问题的三个类别。这三个类别被简单地标记为绿色、紫色和橙色。在预测时,Teachable Machine 不会在网页上以文本形式显示单调的类别概率(甚至更糟的是在控制台中显示),而是显示可爱动物的 GIF 或根据被预测的类别播放不同的声音。可以想象,这对于课堂上的孩子来说将是一次有趣和引人入胜的体验,也将作为一个很好的工具来介绍他们 AI。

图 10-7. 在 Teachable Machine 中在浏览器中实时训练
特征提取
正如我们在本书的一些早期章节中所探讨的,从头开始训练一个大模型是一个缓慢的过程。使用一个预训练模型并利用迁移学习来定制模型以适应我们的用例是更便宜和更快速的。我们将使用该模型从输入图像中提取高级特征(嵌入),并使用这些特征来训练我们的自定义模型。
为了提取特征,我们加载并使用一个预训练的 MobileNet 模型:
const path = 'https://storage.googleapis.com/tfjs-
models/tfjs/mobilenet_v1_1.0_224/model.json';
const mobilenet = await tf.loadLayersModel(path);
让我们检查模型的输入和输出。我们知道该模型是在 ImageNet 上训练的,最后一层预测了 1,000 个类别中的每一个的概率:
const inputLayerShape = mobilenet.inputs[0].shape; // [null, 224, 224, 3]
const outputLayerShape = mobilenet.outputs[0].shape; // [null, 1000]
const numLayers = mobilenet.layers.length; // 88
提取特征时,我们选择一个靠近输出的层。在这里,我们选择conv_pw_13_relu并将其作为模型的输出;也就是说,移除末尾的密集层。我们创建的模型称为特征提取模型:
// Get a specific layer const layer = mobilenet.getLayer('conv_pw_13_relu');
// Create a new feature extraction model featureExtractionModel = tf.model({inputs: mobilenet.inputs, outputs:
layer.output});
featureExtractionModel.layers.length; // 82
在训练过程中,我们将保持特征提取模型不变。相反,我们在其顶部添加一组可训练的层来构建我们的分类器:
const trainableModel = tf.sequential({
layers: [
tf.layers.flatten({inputShape: [7, 7, 1024]}),
tf.layers.dense({
units: 64,
activation: 'relu',
kernelInitializer: 'varianceScaling',
useBias: true
}),
tf.layers.dense({
units: 2,
kernelInitializer: 'varianceScaling',
useBias: false,
activation: 'softmax'
})]
});
数据收集
在这里,我们使用网络摄像头捕获图像并进行特征提取处理。Teachable Machine 中的capture()函数负责设置webcamImage以存储从网络摄像头捕获的图像。现在,让我们对它们进行预处理,使其适用于特征提取模型:
function capture() {
return tf.tidy(() => {
// convert to a tensor
const webcamImage = tf.fromPixels(webcam);
// crop to 224x224
const croppedImage = cropImage(webcamImage);
// create batch and normalize
const batchedImage = croppedImage.expandDims(0);
return batchedImage.toFloat().div(tf.scalar(127)).sub(tf.scalar(1));
});
}
在捕获图像后,我们可以将图像和标签添加到训练数据中:
function addTrainingExample(img, label) {
// Extract features.
const data = featureExtractionModel.predict(img);
// One-hot encode the label.
const oneHotLabel = tf.tidy(() =>
tf.oneHot(tf.tensor1d([label], 'int32'), 2));
// Add the label and data to the training set.
}
训练
接下来,我们训练模型,就像我们在第三章中训练的那样。就像在 Keras 和 TensorFlow 中一样,我们添加一个优化器并定义损失函数:
const optimizer = tf.train.adam(learningRate);
model.compile({ optimizer: optimizer, loss: 'categoricalCrossentropy' });
model.fit(data, label, {
batchSize,
epochs: 5,
callbacks: {
onBatchEnd: async (batch, logs) => {
await tf.nextFrame();
}
}
}
注意
需要记住的一件重要事情是,在 GPU 上,由 TensorFlow.js 分配的内存在tf.tensor对象超出范围时不会被释放。一个解决方案是在每个创建的对象上调用dispose()方法。然而,这会使代码更难阅读,特别是涉及到链式调用时。考虑以下示例:
const result = a.add(b).square().neg();
return result;
为了清理所有内存,我们需要将其分解为以下步骤:
const sum = a.add(b);
const square = sum.square();
const result = square.neg();
sum.dispose();
square.dispose();
return result;
相反,我们可以简单地使用tf.tidy()来进行内存管理,同时保持我们的代码整洁和可读。我们的第一行代码只需要包裹在tf.tidy()块中,如下所示:
const result = tf.tidy(() => {
return a.add(b).square().neg();
});
在使用 CPU 后端时,对象会被浏览器自动垃圾回收。在那里调用.dispose()没有任何效果。
作为一个简单的用例,让我们训练模型来检测情绪。为此,我们只需添加属于两个类别之一的训练示例:快乐或悲伤。使用这些数据,我们可以开始训练。在图 10-8 中,展示了在每个类别的 30 张图像上训练模型后的最终结果。

图 10-8. 我们模型在浏览器中的网络摄像头视频中的预测
提示
通常,在使用网络摄像头进行预测时,UI 会冻结。这是因为计算发生在与 UI 渲染相同的线程上。调用await tf.nextFrame()将释放 UI 线程,使网页响应并防止标签/浏览器冻结。
GPU 利用率
我们可以通过 Chrome 分析器查看训练和推断期间的 CPU/GPU 利用率。在前面的示例中,我们记录了 30 秒的利用率并观察了 GPU 的使用情况。在图 10-9 中,我们看到 GPU 使用了四分之一的时间。

图 10-9. 在 Google Chrome 分析器视图中显示的 GPU 利用率
到目前为止,我们已经讨论了如何从头开始做所有事情,包括加载模型、从网络摄像头捕获视频、收集训练数据、训练模型和运行推断。如果所有这些步骤都可以在幕后处理,我们只需专注于如何处理推断结果,那不是很好吗?在下一节中,我们将使用 ml5.js 讨论这一点。
ml5.js
ml5.js 是 TensorFlow.js 的一个更高级抽象,可以轻松地以统一的方式使用现有的预训练深度学习模型,只需很少的代码行数。该软件包配备了各种内置模型,从图像分割到声音分类再到文本生成,如 表 10-2 所示。此外,ml5.js 简化了与预处理、后处理等相关的步骤,让我们可以专注于使用这些模型构建我们想要的应用程序。对于这些功能,ml5js 都配备了一个 演示 和参考代码。
表 10-2. ml5.js 中的选定内置模型,显示文本、图像和声音的功能范围
| 功能 | 描述 |
|---|---|
| PoseNet | 检测人体关节的位置 |
| U-Net | 对象分割;例如,去除对象背景 |
| 风格迁移 | 将一幅图像的风格转移到另一幅图像 |
| Pix2Pix | 图像到图像的转换;例如,黑白到彩色 |
| Sketch RNN | 根据不完整的草图创建涂鸦 |
| YOLO | 目标检测;例如,定位带有边界框的人脸 |
| 声音分类器 | 识别音频;例如,口哨、拍手、“一”、“停止”等 |
| 音高检测器 | 估计声音的音高 |
| Char RNN | 基于大量文本训练生成新文本 |
| 情感分类器 | 检测句子的情感 |
| Word2Vec | 生成词嵌入以识别词之间的关系 |
| 特征提取器 | 从输入生成特征或嵌入 |
| kNN 分类器 | 使用 k-最近邻创建快速分类器 |
让我们看看它的运行情况。首先,我们导入最新的 ml5.js 捆绑包,这类似于 TensorFlow.js:
<script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"
type="text/javascript"></script>
请注意,我们不再需要导入与 TensorFlow.js 相关的任何内容,因为它已经包含在 ml5.js 中。我们创建一个简单的示例,其中使用与之前相同的 MobileNet 场景:
// Initialize the image classifier method with MobileNet
const classifier = ml5.imageClassifier('MobileNet', modelLoaded);
// Make a prediction with the selected image
classifier.predict(document.getElementById('image'), function(err, results) {
console.log(results);
});
完成!在三行代码中,一个预训练模型就在我们的浏览器中运行了。现在,让我们打开浏览器的控制台,检查 图 10-10 中呈现的输出。

图 10-10. 预测类别及其概率
提示
如果您不熟悉浏览器控制台,可以通过在浏览器窗口中的任何位置右键单击并选择“检查元素”来简单访问它。一个单独的窗口将打开,其中包含控制台。
我们可以在 code/chapter-10/ml5js 找到前面示例的完整源代码。
请注意,ml5.js 使用回调来管理模型的异步调用。回调是在相应调用完成后执行的函数。例如,在最后的代码片段中,模型加载完成后会调用 modelLoaded 函数,表示模型已加载到内存中。
注意
p5.js 是一个与 ml5.js 配合得很好的库,可以通过实时视频流轻松进行模型预测。您可以在 code/chapter-10/p5js-webcam/ 找到一个演示 p5.js 强大功能的代码片段。
ml5.js 原生支持 p5.js 元素和对象。您可以使用 p5.js 元素来绘制对象、捕获网络摄像头视频等。然后,您可以轻松地将这些元素作为输入传递给 ml5.js 回调函数。
PoseNet
到目前为止,在本书中,我们主要探讨了图像分类问题。在后面的章节中,我们将研究目标检测和分割问题。这些问题类型构成了计算机视觉文献的大部分。然而,在本节中,我们选择暂时离开常规,处理一种不同类型的问题:关键点检测。这在包括医疗保健、健身、安全、游戏、增强现实和机器人技术等各种领域都有重要应用。例如,为了通过锻炼鼓励健康生活方式,墨西哥城安装了可以检测深蹲姿势并向至少做 10 个深蹲的乘客提供免费地铁票的亭子。在本节中,我们将探讨如何在我们谦卑的网络浏览器中运行如此强大的东西。
PoseNet 模型在浏览器中提供实时姿势估计。一个“姿势”包括人体不同关键点(包括关节)的位置,如头顶、眼睛、鼻子、颈部、手腕、肘部、膝盖、脚踝、肩膀和臀部。您可以使用 PoseNet 来检测同一帧中可能存在的单个或多个姿势。
让我们使用 PoseNet 构建一个示例,ml5.js 中已经提供了它,可以用来检测和绘制关键点(借助 p5.js 的帮助)。

图 10-11. 使用 PoseNet 在前总统奥巴马参与雪仗的照片上绘制的关键点
您可以在code/chapter-10/posenet/single.html找到检测静态图像关键点的代码:
<script src="http://p5js.org/assets/js/p5.min.js"></script>
<script src="http://p5js.org/assets/js/p5.dom.min.js"></script>
<script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
<script>
function setup() {
// Set up camera here
// Call PoseNet model
const poseNet = ml5.poseNet(video, modelReady);
// PoseNet callback function
poseNet.on('pose', function (results) {
const poses = results;
});
}
</script>
我们也可以在网络摄像头上运行类似的脚本(在code/chapter-10/posenet/webcam.html)。
现在让我们看另一个由 ml5.js 支持的示例。
pix2pix
“Hasta la vista, baby!”
这是电影史上最令人难忘的台词之一。巧合的是,这是 1991 年经典电影《终结者 2:审判日》中一个 AI 机器人说的。顺便说一句,它的翻译是“再见,宝贝!”自那时以来,语言翻译技术已经取得了长足的进步。过去,语言翻译建立在短语替换规则上。现在,它被表现更好的深度学习系统所取代,这些系统能够理解句子的上下文,将其转换为目标语言中具有类似含义的句子。
想一想:如果我们可以从句子一翻译到句子二,那么我们是否可以将一幅图片从一个环境转换到另一个环境?我们可以做到以下这些吗:
-
将一幅图像从低分辨率转换为高分辨率?
-
将一幅图像从黑白转换为彩色?
-
将一幅图像从白天转换为夜晚视图?
-
将地球的卫星图像转换为地图视图?
-
将一幅手绘草图转换为照片?
嗯,图像翻译不再是科幻。2017 年,Philip Isola 等人开发了一种将一幅图片转换为另一幅图片的方法,方便地命名为 pix2pix。通过学习几对之前和之后的图片,pix2pix 模型能够基于输入图片生成高度逼真的渲染。例如,如图 10-12 所示,给定一个包的铅笔素描,它可以重新创建包的照片。其他应用包括图像分割、合成艺术形象等。

图 10-12. pix2pix 上输入和输出对的示例
注意
想象一种情景,有一个银行出纳员和一个货币伪造者。银行出纳员的工作是发现假币,而伪造者的目标是尽可能地让银行出纳员难以识别假币。他们显然处于对抗性的情况。每当警察发现假钞时,伪造者都会从中学习错误,将其视为改进的机会(毕竟是成长思维),并试图让银行出纳员下次更难阻止他。这迫使银行出纳员随着时间的推移更善于识别假币。这种反馈循环迫使他们两个在自己的工作上变得更好。这是推动 GAN 的基本原理。
正如图 10-13 所示,GAN 由两个网络组成,一个生成器和一个鉴别器,它们与伪造者和银行出纳员具有相同的对抗关系。生成器的工作是生成看起来逼真的输出,与训练数据非常相似。鉴别器的责任是识别由生成器传递给它的数据是真实还是伪造的。鉴别器的输出被反馳回生成器以开始下一个周期。每当鉴别器正确地将生成的输出识别为伪造时,它会迫使生成器在下一个周期变得更好。
值得注意的是,GAN 通常无法控制要生成的数据。然而,有一些 GAN 的变体,如条件 GAN,允许标签成为输入的一部分,从而更好地控制输出生成;也就是说,调节输出。pix2pix 是条件 GAN 的一个例子。

图 10-13。GAN 的流程图
我们使用 pix2pix 创建了一个在浏览器中运行的简单素描应用程序。输出图像非常有趣。请考虑图 10-14 和图 10-15 中显示的示例。

图 10-14。素描到图像示例

图 10-15。我们可以创建彩色蓝图(左)和 pix2pix 将其转换为逼真的人脸(右)
注意
有趣的事实:Ian Goodfellow 在酒吧时想出了 GAN 的想法。这为发明、组织和公司的想法起源于饮料的列表中又增加了一项,包括 RSA 算法、西南航空和魁地奇游戏的创造。
pix2pix 通过训练图像对来工作。在图 10-16 中,左侧的图像是输入图像或条件输入。右侧的图像是目标图像,我们想要生成的逼真输出(如果您正在阅读印刷版本,则不会看到右侧的彩色图像)。

图 10-16。pix2pix 的训练对:一幅黑白图像及其原始彩色图像
训练 pix2pix 的一个更简单的端口是由Christopher Hesse基于 TensorFlow 的实现。我们可以使用一个非常简单的脚本来训练我们自己的模型:
python pix2pix.py \
--mode train \
--output_dir facades_train \
--max_epochs 200 \
--input_dir facades/train \
--which_direction BtoA
训练完成后,我们可以使用以下命令保存模型:
python tools/export-checkpoint.py --checkpoint ../export --output_file
models/MY_MODEL_BtoA.pict
之后,我们可以使用这段简单的代码将保存的权重加载到 ml5.js 中。请注意用于在画布中检索输出的转移函数:
// Create a pix2pix model using a pre-trained network
const pix2pix = ml5.pix2pix('models/customModel.pict', modelLoaded);
// Transfer using a canvas
pix2pix.transfer(canvas, function(err, result) {
console.log(result);
});
我们还可以绘制笔画并允许实时素描。例如,图 10-17 展示了一个绘制皮卡丘的示例。

图 10-17。Pix2Pix:从边缘到皮卡丘由 Yining Shi 制作,基于 ml5.js
基准测试和实际考虑
作为非常关心我们的最终用户如何看待我们的产品的人,对待他们是很重要的。两个因素在用户体验我们的产品时起着重要作用:模型大小和基于硬件的推理时间。让我们更仔细地看看每个因素。
模型大小
典型的 MobileNet 模型为 16 MB。在标准家庭或办公网络上加载这个可能只需要几秒钟。在移动网络上加载相同的模型会花更长时间。时间在流逝,用户变得不耐烦。而这还是在模型开始推理之前。等待大型模型加载对用户体验的影响比它们的运行时间更为有害,尤其是在互联网速度不如新加坡这样的宽带天堂的地方。有一些策略可以帮助:
选择最适合工作的最小模型
在预训练网络中,EfficientNet、MobileNet 或 SqueezeNet 往往是最小的(按准确性递减顺序)。
量化模型
在导出到 TensorFlow.js 之前,使用 TensorFlow 模型优化工具包减小模型大小。
构建我们自己的微型模型架构
如果最终产品不需要重量级的 ImageNet 级别分类,我们可以构建自己的较小模型。当谷歌在谷歌主页上制作 J.S. Bach 涂鸦时,其模型仅为 400 KB,几乎瞬间加载。
推理时间
考虑到我们的模型可以在运行在 PC 或手机上的浏览器中访问,我们希望特别注意用户体验,尤其是在最慢的硬件上。在我们的基准测试过程中,我们在不同设备上的各种浏览器中运行chapter10/code/benchmark.html。图 10-18 展示了这些实验的结果。

图 10-18。在 Chrome 上不同设备上 MobileNetV1 的推理时间
图 10-18 暗示硬件越快,模型推理速度越快。苹果在 GPU 性能方面似乎胜过安卓。尽管,显然,这不是一个“不折不扣的比较”。
出于好奇,不同浏览器是否以相同的速度运行推理?让我们在 iPhone X 上找出答案;图 10-19 展示了结果。

图 10-19。iPhone X 上不同浏览器的推理时间
图 10-19 向我们展示了 iPhone 上所有浏览器的相同速度。这并不令人惊讶,因为所有这些浏览器都使用 iPhone 的基于 WebKit 的内置浏览器控件称为WKWebView。在 MacBook Pro 上呢?看看图 10-20。

图 10-20。在 i7 @ 2.6 GHz macOS 10.14 机器上不同浏览器的推理时间
结果可能会令人惊讶。在这个例子中,Chrome 的速度几乎是 Firefox 的两倍。为什么?打开 GPU 监视器显示,与 Firefox 相比,Chrome 的 GPU 利用率要高得多,稍高于 Safari。利用率越高,推理速度越快。这意味着根据操作系统的不同,浏览器可能具有不同的优化来加速 GPU 上的推理,从而导致不同的运行时间。
需要注意的一个关键点是这些测试是在顶级设备上进行的。它们不一定反映普通用户可能拥有的设备类型。这也对电池使用有影响,如果长时间运行。因此,我们需要对性能设置适当的期望,特别是对于实时用户体验。
案例研究
现在我们知道了浏览器上深度学习的所有要素,让我们看看这个行业正在做些什么。
Semi-Conductor
你是否曾梦想指挥纽约爱乐乐团?通过Semi-Conductor,你的梦想已经实现了一半。打开网站,站在网络摄像头前,挥动手臂,看着整个管弦乐团随你的意愿演奏莫扎特的《小夜曲》!正如你可能猜到的那样,它使用 PoseNet 来跟踪手臂运动,并利用这些运动来设置速度、音量和演奏音乐的乐器部分(包括小提琴、中提琴、大提琴和低音提琴)(图 10-21)。这个由谷歌悉尼创意实验室在澳大利亚悉尼建立的项目,使用了一个预先录制的音乐片段,将其分解成微小片段,每个片段根据手臂运动以得分速度和音量播放。手向上移动增加音量,移动更快增加速度。这种互动体验只有因为 PoseNet 能够在每秒几帧的速度下进行推理(在普通笔记本电脑上)。

图 10-21。通过在半导体演示器上挥动手臂来控制管弦乐团
TensorSpace
CNNs 经常感觉...嗯,复杂。通常被视为黑匣子,很难理解。滤波器是什么样的?是什么激活了它们?为什么它们做出了某种预测?它们笼罩在神秘之中。与任何复杂的事物一样,可视化可以帮助打开这个黑匣子,使其更容易理解。这就是TensorSpace的作用,这个“在空间中呈现张量的库”。
它允许我们在 3D 空间中加载模型,探索它们在浏览器中的结构,通过它们进行缩放和旋转,输入数据,并了解图像是如何逐层处理并传递到最终预测层的。滤波器最终可以被手动检查,而无需任何安装。而且,正如图 10-22 所暗示的,如果你感觉很有见识,你甚至可以加载到虚拟现实中,并与任何背景进行对比!

图 10-22。在 TensorSpace 内可视化的 LeNet 模型
Metacar
自动驾驶汽车是一个复杂的怪物。使用强化学习来训练它们可能需要很多时间、金钱和猴子扳手(不包括最初的碰撞)。如果我们能在浏览器中训练它们会怎样?Metacar通过提供一个模拟的 2D 环境来训练玩具汽车,使用强化学习,全部在浏览器中进行,如图 10-23 所示。就像玩视频游戏一样,你可以逐渐进入更难的关卡,Metacar 允许构建多个关卡来提高你的汽车性能。利用 TensorFlow.js,这旨在使强化学习更易于访问(我们将在第十七章中更详细地探讨这个问题,同时构建一个小型自动驾驶汽车)。

图 10-23。用强化学习进行训练的 Metacar 环境
Airbnb 的照片分类
在线房屋租赁公司 Airbnb 要求房主和租客上传他们的照片以完善个人资料。不幸的是,一些人试图上传他们最容易获得的照片——他们的驾照或护照。考虑到信息的机密性,Airbnb 使用在 TensorFlow.js 上运行的神经网络来检测敏感图像,并阻止它们上传到服务器。
GAN 实验室
类似于TensorFlow Playground(一个基于浏览器的神经网络可视化工具),GAN 实验室(图 10-24)是一个优雅的可视化工具,用于理解使用 TensorFlow.js 的 GAN。可视化 GAN 是一个困难的过程,因此为了简化它,GAN 实验室尝试学习简单的分布并可视化生成器和鉴别器网络的输出。例如,真实分布可以是在 2D 空间中表示一个圆的点。生成器从一个随机的高斯分布开始,并逐渐尝试生成原始分布。这个项目是乔治亚理工学院和谷歌 Brain/PAIR(People + AI Research)之间的合作。

图 10-24. GAN 实验室截图
摘要
我们首先研究了基于 JavaScript 的深度学习库的发展,并选择了 TensorFlow.js 作为重点关注的候选。我们在网络摄像头上实时运行预训练模型,甚至在浏览器中训练模型。像 Chrome 的分析器这样的工具让我们了解 GPU 的使用情况。然后,为了进一步简化开发,我们使用了 ml5.js,这使我们能够仅用几行代码构建像 PoseNet 和 pix2pix 这样的演示。最后,我们在现实世界中对这些模型和库的性能进行了基准测试,最终得出了一些有趣的案例研究。
在浏览器中运行神经网络的一个巨大好处是浏览器相比于任何智能手机平台具有更广泛的覆盖范围。再加上不必克服用户不愿安装另一个应用的优势。这也为快速原型设计提供了一个平台,使得在投入大量时间和金钱构建本地体验之前,可以廉价地验证假设。TensorFlow.js 与 ml5.js 结合使用,加速了将人工智能的力量带入浏览器的过程,并将其覆盖范围扩大到大众。
第十一章:在 iOS 上使用 Core ML 进行实时对象分类
到目前为止,我们已经看到我们的深度学习模型在桌面、云端和浏览器上运行。尽管这种设置有明显的优势,但并不适用于所有情况。在本章中,我们将探讨在移动设备上使用深度学习模型进行预测。
将计算带到用户设备附近,而不是远程服务器,对于许多原因都是有利的:
延迟和互动性
发送图像,将其在云端处理,然后返回结果可能需要几秒钟,具体取决于网络质量和传输数据的数量。这可能导致糟糕的用户体验。几十年的用户体验研究,包括 Jakob Nielsen 在 1993 年发表在他的书《可用性工程》(Elsevier)中的发现,显示如下:
-
0.1 秒是让用户感觉系统反应即时的极限。
-
1 秒是用户思维流畅保持不间断的极限。
-
10 秒是保持用户注意力集中的极限。
大约两十年后,谷歌发布了一项研究结果,发现如果移动浏览器加载一个网页超过三秒,一半的用户会放弃。甚至忘记三秒,延迟增加 100 毫秒都会导致亚马逊销售额下降 1%。这是很多损失的收入。通过立即在设备上处理来减轻这种情况可以实现丰富和互动的用户体验。实时运行深度学习模型,就像 Snapchat Lenses 所做的那样,可以增加与用户的互动。
全天候可用性和降低云成本
显然,向云端发送的数据越少,对于开发者来说意味着更少的计算成本,从而节省金钱。当应用程序获得关注并发展成大量用户群时,这也减少了扩展成本。对于用户来说,在边缘计算上进行计算也很有帮助,因为他们不需要担心数据计划成本。此外,本地处理意味着全天候可用性,无需担心失去连接的恐惧。
隐私
对于用户来说,本地计算通过不外部共享数据来保护隐私,这些数据可能被挖掘用于用户信息。对于开发者来说,这确保了处理个人可识别信息(PII)时更少的麻烦。随着欧盟的《通用数据保护条例》(GDPR)和其他全球用户数据保护法律的出台,这变得更加重要。
希望这些论点能够说明为什么移动设备上的人工智能很重要。对于构建任何严肃应用程序的人来说,在开发过程中考虑以下几个常见问题:
-
如何将我的模型转换为在智能手机上运行?
-
我的模型是否能在其他平台上运行?
-
如何快速运行我的模型?
-
如何最小化应用程序的大小?
-
如何确保我的应用程序不会耗尽电池?
-
如何在不经历大约两天的应用审核过程的情况下更新我的模型?
-
如何进行 A/B 测试我的模型?
-
我能在设备上训练模型吗?
-
如何保护我的知识产权(即模型)不被盗窃?
在接下来的三章中,我们将探讨如何使用不同的框架在智能手机上运行深度学习算法。在这个过程中,我们会回答这些问题。
在本章中,我们深入探讨了 iOS 设备的移动人工智能世界。我们首先看一下一般的端到端软件生命周期(在图 11-1 中显示),看看不同部分如何相互配合。我们探索 Core ML 生态系统,其历史和提供的功能。接下来,我们在 iOS 设备上部署一个实时对象分类应用程序,并学习性能优化和基准测试。最后,我们分析了一些基于 Core ML 构建的真实应用程序。
是时候看看全局了。
移动设备上人工智能的开发生命周期
图 11-1 描述了移动设备上 AI 的典型生命周期。

图 11-1. 移动 AI 开发生命周期
让我们更仔细地看一下图 11-1 中的阶段:
-
收集数据: 我们收集的数据应反映应用程序的使用环境。由智能手机摄像头拍摄的真实用户照片往往比专业摄影师拍摄的照片更好作为训练示例。我们可能在第一天没有这些数据,但随着使用量的增长,我们可以逐渐收集更多数据。在许多情况下,一个很好的起点是从搜索引擎下载图像。
-
标记数据: 我们需要与我们希望我们的模型预测的数据样本相关联的标签。高质量(即正确)的标签对于一个好的模型至关重要。
-
训练模型: 我们利用迄今为止拥有的数据和相关标签构建最高准确度的神经网络。
-
转换模型: 将模型从训练框架导出到移动兼容的框架。
-
优化性能: 由于移动设备资源受限,对于内存、能源和处理器的使用效率至关重要。
-
部署: 将模型添加到应用程序中并将其交付给用户。
-
监控: 跟踪应用在现实世界中的使用情况,找到进一步改进的机会。此外,收集愿意同意的用户的真实数据样本,以供这个生命周期使用,然后回到第一步。
在本书的前部分,我们主要探讨了阶段 1、2 和 3,以及一般性能改进。在本章中,我们将重点放在阶段 4、5 和 6 上。在接下来的几章中,我们将探讨所有这些阶段在移动开发的背景下的应用。
提示
在将应用交到真实用户手中之前(如果应用表现不如他们期望的那样,他们可能会讨厌它),通过一种称为“dogfooding”的过程收集反馈是常见做法。俗话说:吃自己的狗粮。这个过程涉及拥有一群忠实用户的内部圈子,他们可以测试早期版本,从而在发布给公众之前识别错误。对于 AI 开发,这个内部圈子也可能贡献数据,并评估 AI 模型在现实世界中的成功。随着模型的改进,它们可以逐渐部署到越来越多的测试用户,最终部署到公众。
让我们开始吧!
Core ML 简史
Core ML 为在苹果设备(如 iPhone 和 iPad 以及 MacBook、Apple TV 和 Apple Watch)上运行深度神经网络提供了一种最简单的方式。除了易于使用外,它还针对底层硬件架构进行了优化。在过去几年中,替代框架已经变得更好了,但很难超越 Core ML 提供的简单性和性能。
传统上,要在苹果设备上快速运行 CNN,开发人员需要使用 Metal 编写,这是一个为游戏开发人员提供的库,用于更好地利用 GPU。不幸的是,使用 Metal 进行开发就像是在汇编语言或 CUDA 代码中编写一样。这是繁琐、容易出错且难以调试的。很少有开发人员敢于踏上这条道路。Amund Tveit 于 2015 年 12 月推出的 DeepLearningKit 是为部署 CNN 构建一个对 Metal 的抽象的努力。
在 2016 年的苹果全球开发者大会(WWDC)上,该公司宣布了 Metal Performance Shaders(MPS),这是建立在 Metal 之上的一个高性能库,用于优化图形和某些计算操作。它抽象了许多底层细节,提供了基本构建模块,如卷积、池化和 ReLU。它允许开发人员通过在代码中组合这些操作来编写深度神经网络。对于来自 Keras 世界的任何人来说,这是一个熟悉而不那么令人畏惧的任务。不幸的是,在编写 MPS 代码时涉及了大量的繁琐工作,因为您需要在每个步骤中手动跟踪输入和输出维度。例如,苹果发布的用于识别 1,000 个对象类别的 InceptionV3 模型的代码示例超过 2,000 行,其中大部分定义了网络。现在想象一下在训练过程中稍微更改模型,然后不得不查看所有 2,000 行代码以在 iOS 代码中反映相同的更新。Forge(2017 年 4 月),由 Matthijs Hollemans 开发的一个库,是为了简化 MPS 开发,减少启动模型所需的样板代码。
当苹果在 2017 年的 WWDC 上宣布 Core ML 时,所有这些困难都消失了。这包括 iOS 上的推理引擎和一个名为 Core ML Tools 的开源 Python 包,用于序列化来自 Keras 和 Caffe 等其他框架的 CNN 模型。构建应用程序的一般工作流程是:在其他软件包中训练模型,将其转换为.mlmodel文件,并部署在运行在 Core ML 平台上的 iOS 应用程序中。
Core ML 支持导入使用第一方和第三方框架和文件格式构建的广泛范围的机器学习模型。图 11-2 展示了其中一些(顺时针,从左上角开始),如 TensorFlow、Keras、ONNX、scikit-learn、Caffe2、苹果的 Create ML、LIBSVM 和 TuriCreate(也来自苹果)。ONNX 本身支持各种框架,包括 PyTorch(Facebook)、MXNet(亚马逊)、Cognitive Toolkit(微软)、PaddlePaddle(百度)等,从而确保与天下任何主要框架的兼容性。

图 11-2。截至 2019 年与 Core ML 兼容的框架
Core ML 的替代方案
根据平台的不同,有几种选项可以实现实时预测。这些包括通用推理框架,如 Core ML(来自苹果)、TensorFlow Lite(来自谷歌)、ML Kit(也来自谷歌)和 Fritz,以及特定芯片加速器框架,包括骁龙神经处理引擎(来自高通)和华为 AI 移动计算平台(用于华为的神经处理单元)。表 11-1 提供了所有这些框架的高级比较。
表 11-1。移动设备 AI 框架的比较
| 框架 | 适用于 iOS | 适用于 Android | 动态更新 | A/B 测试 | 设备上的训练 | 模型加密 |
|---|---|---|---|---|---|---|
| Core ML | ✓ | — | ✓ | — | ✓ | — |
| TensorFlow Lite | ✓ | ✓ | — | — | 2019 年底发布 | — |
| ML Kit | ✓ | ✓ | ✓ | ✓ | — | — |
| Fritz | ✓ | ✓ | ✓ | ✓ | — | ✓ |
TensorFlow Lite
2017 年 11 月,谷歌宣布推出了一个名为 TensorFlow Lite 的设备上推理引擎,旨在将 TensorFlow 生态系统扩展到服务器和个人电脑之外。在此之前,TensorFlow 生态系统内的选项包括将整个 TensorFlow 库移植到 iOS(这很沉重且缓慢),以及稍后推出的其略微简化版本称为 TensorFlow Mobile(仍然相当庞大)。
TensorFlow Lite 从头开始重建,针对移动和边缘设备进行优化,优化速度、模型和解释器大小以及功耗。它增加了对 GPU 后端代理的支持,这意味着只要为硬件平台实现了 GPU 支持,TensorFlow Lite 就可以利用 GPU 的强大功能。在 iOS 上,GPU 代理使用 Metal 进行加速。我们在第十三章中更详细地讨论了 TensorFlow Lite。
ML Kit
ML Kit 是谷歌提供的一个高级库,提供了许多计算机视觉、自然语言处理和人工智能功能,包括运行 TensorFlow Lite 模型的能力。一些功能包括人脸检测、条形码扫描、智能回复、设备上的翻译和语言识别。然而,ML Kit 的主要卖点是与 Google Firebase 的集成。Firebase 提供的功能包括动态模型更新、A/B 测试和基于远程配置驱动的动态模型选择(根据客户选择使用哪个模型的花哨词)。我们在第十三章中更详细地探讨了 ML Kit。
Fritz
Fritz 是一家创立的初创公司,旨在使移动推断的端到端过程更加简单。它通过提供易于使用的命令行工具,弥合了机器学习从业者和移动工程师之间的鸿沟。一方面,它将 Keras 中的训练直接集成到部署流水线中,因此机器学习工程师可以在训练完成后添加一行 Keras 回调即可立即将模型部署给用户。另一方面,移动工程师可以在不需要部署到物理设备的情况下对模型进行基准测试,通过虚拟模拟模型的性能,评估 Keras 模型与 Core ML 的兼容性,并为每个模型获取分析数据。Fritz 的一个独特卖点是模型保护功能,通过混淆防止模型在手机越狱时进行深度检查。
苹果的机器学习架构
为了更好地了解 Core ML 生态系统,有必要看到苹果提供的所有不同 API 以及它们如何相互配合。图 11-3 让我们看到了构成苹果机器学习架构的不同组件。

图 11-3. 苹果为应用开发人员提供的不同级别的 API
基于领域的框架
为了简化机器学习中的常见任务,不需要领域专业知识,苹果提供了许多开箱即用的 API,涵盖视觉、自然语言、语音和声音分析等领域。表 11-2 详细概述了苹果操作系统上可用的功能。
表 11-2. 苹果操作系统中的开箱即用机器学习功能
| 视觉 | 自然语言 | 其他 |
|---|
|
-
面部特征点检测
-
图像相似度
-
显著性检测
-
光学字符识别
-
矩形检测
-
人脸检测
-
对象分类
-
条形码检测
-
地平线检测
-
人类和动物检测
-
对象跟踪(用于视频)
|
-
标记化
-
语言识别
-
词性识别
-
文本嵌入
|
-
语音识别(设备上和云端)
-
声音分类
|
ML 框架
Core ML 具有在深度学习和机器学习模型上运行推断的能力。
ML 性能基元
以下是苹果堆栈中的一些机器学习基元:
MPS
提供低级和高性能的基元,利用 GPU 来快速运行大多数基于 CNN 的网络。如果 Core ML 不支持某个模型,MPS 提供了所有构建块,使我们能够构建它们。此外,我们可能考虑使用 MPS 手动创建一个模型以提高性能(例如保证模型在 GPU 上运行)。
加速和基本神经网络子程序
加速是苹果对基本线性代数子程序(BLAS)库的实现。它提供了用于高性能大规模数学计算和图像计算的功能,如基本神经网络子程序(BNNS),有助于实现和运行神经网络。
既然我们已经看到了 Core ML 和特定领域 API 如何融入整体架构,让我们看看在 iOS 应用程序上运行机器学习模型所需的工作量是多么少。
提示
苹果提供了几个可下载的模型(图 11-4),用于各种计算机视觉任务,从分类到检测物体(带有边界框),分割(识别像素),深度估计等。您可以在https://developer.apple.com/machine-learning/models/找到它们。
对于分类,您可以在苹果的机器学习网站上找到许多预训练的 Core ML 模型,包括 MobileNet、SqueezeNet、ResNet-50 和 VGG16。

图 11-4. 苹果机器学习网站上的现成模型
构建实时物体识别应用程序
虽然我们不打算教授 iOS 开发,但我们想演示在 iOS 设备上实时运行一个能够在 1,000 个 ImageNet 类别中进行分类的物体识别模型。
我们将检查运行此应用程序所需的最少代码量。一般的概述如下:
-
将.mlmodel文件拖放到 Xcode 项目中。
-
将模型加载到 Vision 容器(
VNCoreMLModel)中。 -
基于该容器创建一个请求(
VNCoreMLRequest),并提供一个在请求完成时将被调用的函数。 -
创建一个请求处理程序,可以根据提供的图像处理请求。
-
执行请求并打印结果。
让我们看看如何编写代码来实现这一点:
import CoreML
import Vision
// load the model
let model = try? VNCoreMLModel(for: Resnet50().model)!
// create a request with a callback
let classificationRequest = VNCoreMLRequest(model: model) {
(request, error) in
// print the results once the request is complete
if let observations = request.results as? [VNClassificationObservation] {
let results = observations
.map{"\($0.identifier) - \($0.confidence)"}
.joined(separator: "\n")
print(results)
}
}
// create a request handler taking an image as an argument
let requestHandler = VNImageRequestHandler(cgImage: cgImage)
// execute the request
try? requestHandler.perform([classificationRequest])
在这段代码中,cgImage可以是来自各种来源的图像。它可以是来自照片库或网络的照片。
我们还可以使用摄像头实时场景,并将单个摄像头帧传递到此函数中。普通的 iPhone 摄像头可以每秒拍摄高达 60 帧。
默认情况下,Core ML 沿着较长的边裁剪图像。换句话说,如果模型需要方形尺寸,Core ML 将提取图像中心的最大方形。这可能会让开发人员感到困惑,因为他们发现图像的顶部和底部条被忽略以进行预测。根据情况,我们可能希望使用.centerCrop、.scaleFit或.scaleFill选项,如图 11-5 所示:
classificationRequest.imageCropAndScaleOption = .scaleFill

图 11-5. 不同缩放选项如何修改输入图像到 Core ML 模型
既然我们已经深入了解了这个主题,还有什么比这更有趣的呢?怎么样在手机上实际运行它!我们在本书的 GitHub 网站上提供了一个完整的应用程序(请参见http://PracticalDeepLearning.ai)位于code/chapter-11。通过 iPhone 或 iPad,我们可以相当快速地部署它并进行实验,即使没有 iOS 开发知识也可以。以下是步骤(注意:这需要 Mac):
-
从苹果开发者网站或 Mac App Store 下载 Xcode。
-
插入 iOS 设备。手机在部署过程中需要保持解锁状态。
-
将当前工作目录更改为
CameraApp:$ cd code/chapter-11/CameraApp -
从苹果网站下载 Core ML 模型,使用提供的 Bash 脚本:
$ ./download-coreml-models.sh -
打开 Xcode 项目:
$ open CameraApp.xcodeproj -
在项目层次结构导航器中,点击左上角的 CameraApp 项目,如图 11-6 所示,打开项目信息视图。
![Xcode 中的项目信息视图]()
图 11-6. Xcode 中的项目信息视图
-
因为 Xcode 保留了唯一的包标识符,所以使用一个唯一的名称来标识项目。
-
登录到苹果账户,让 Xcode 签署应用程序并将其部署到设备上。选择一个团队进行签署,如图 11-7 所示。
![选择一个团队,让 Xcode 自动管理代码签名]()
图 11-7. 选择一个团队,让 Xcode 自动管理代码签名
-
点击“构建和运行”按钮(右向三角形)将应用程序部署到设备上,如图 11-8 所示。这通常需要大约 30 到 60 秒。
![选择设备,点击“构建和运行”按钮来部署应用程序]()
图 11-8. 选择设备,点击“构建和运行”按钮来部署应用程序
-
设备不会立即运行应用程序,因为它没有受信任。前往设置 > 通用 > 配置文件与设备管理,选择包含您信息的行,然后点击“信任{your_email_id}”,如图 11-9 所示。
![配置文件和设备管理屏幕]()
图 11-9. 配置文件和设备管理屏幕
-
在主屏幕上找到 CameraApp 并运行应用程序。
![应用程序的屏幕截图]()
图 11-10. 应用程序的屏幕截图
输出显示“笔记本电脑,笔记本电脑”预测的置信度为 84%,“笔记本电脑,笔记本电脑”预测的置信度为 11%。
这一切都很有趣。现在让我们转向更严肃的业务:将不同框架的模型转换为 Core ML 模型。
提示
Xcode 的一个很棒之处是当我们将.mlmodel文件加载到 Xcode 中时,会显示模型的输入和输出参数,如图 11-11 所示。当我们没有自己训练模型并且不想编写代码(比如在 Keras 中的model.summary())来探索模型架构时,这是特别有帮助的。

图 11-11. Xcode 模型检查器显示 MobileNetV2 模型的输入和输出
转换为 Core ML
在我们构建的代码示例中,您看到了Inceptionv3.mlmodel文件。您是否想知道这个文件是如何生成的?毕竟,Inception 是由谷歌使用 TensorFlow 训练的。该文件是从.pb文件转换为 Core ML 模型的。我们可能也有需要将模型从 Keras、Caffe 或任何其他框架转换为 Core ML。以下是一些工具,可以实现模型转换为 Core ML。
-
Core ML 工具(苹果):从 Keras(.h5)、Caffe(.caffemodel)以及 LIBSVM、scikit-learn 和 XGBoost 等机器学习库。
-
tf-coreml(谷歌):从 TensorFlow(.pb)。
-
onnx-coreml(ONNX):从 ONNX(.onnx)。
我们详细查看前两个转换器。
从 Keras 转换
Core ML 工具有助于将 Keras、ONNX 和其他模型格式转换为 Core ML 格式(.mlmodel)。使用pip安装coremltools框架:
$ pip install --upgrade coremltools
现在,让我们看看如何使用现有的 Keras 模型并将其转换为 Core ML。转换只需一行命令,然后我们可以保存转换后的模型,如下所示:
from tensorflow.keras.applications.resnet50 import ResNet50
model = ResNet50()
import coremltools
coreml_model = coremltools.converters.keras.convert(model)
coreml_model.save("resnet50.mlmodel")
就是这样!它再简单不过了。我们将讨论如何转换具有不受支持层(如 MobileNet)的模型,详见第十二章。
从 TensorFlow 进行转换
苹果建议使用tf-coreml(来自 Google)将基于 TensorFlow 的模型转换为 Core ML。在以下步骤中,我们将一个预训练的 TensorFlow 模型转换为 Core ML。这个过程比我们之前看到的单行代码要复杂一些。
首先,我们使用pip执行tfcoreml的安装:
$ pip install tfcoreml --user --upgrade
要进行转换,我们需要知道模型的第一层和最后一层。我们可以通过使用模型可视化工具(如Netron)检查模型架构来确定这一点。在 Netron 中加载 MobileNet 模型(.pb文件)后,我们可以在图形界面中可视化整个模型。图 11-12 显示了 MobileNet 模型的一个小部分;特别是输出层。

图 11-12. 在 Netron 中查看的 MobileNet 的输出层
我们只需要将其作为参数插入以下 Python 代码中并运行:
import tfcoreml as tf_converter
tf_converter.convert(tf_model_path = "input_model.pb",
mlmodel_path = "output_model.mlmodel",
output_feature_names = ["MobilenetV1/Predictions/Reshape_1:0"])
当脚本运行时,我们会看到每个层的每个操作被转换为相应的 Core ML 等效操作。完成后,我们应该在目录中找到导出的.mlmodel。
我们的 Core ML 模型现在已经准备好了。将其拖入 Xcode 进行测试。
动态模型部署
作为开发人员,我们可能会希望随着时间的推移不断改进我们的模型。毕竟,我们希望我们的用户尽快获得最新和最好的模型。处理这个问题的一种方法是每次部署新模型时向 App Store 发送更新。这种方法效果不是很好,因为每次我们都需要等待大约两天才能获得苹果的批准,可能会导致显著的延迟。
另一种推荐的方法是让应用程序动态下载.mlmodel文件,并在用户设备内编译它。有几个原因我们可能想尝试这种方法:
-
我们希望定期更新模型,而不受我们 App Store 发布节奏的限制。
-
我们希望保持应用程序下载大小较小,并让用户最终仅下载与其使用场景相关的模型。这将减少存储需求和带宽成本。苹果在通过蜂窝网络下载 App Store 的应用程序上设置了 200 MB 的限制,因此保持在该限制以下至关重要,以免错失潜在的下载机会。
-
我们希望在不同用户集合上对不同模型进行 A/B 测试,以进一步提高模型的质量。
-
我们希望为不同的用户段、区域和语言环境使用不同的模型。
实现这一目标的过程相当简单:将.mlmodel托管在服务器上,并设计我们的应用程序以动态下载文件。在模型位于用户设备上之后,我们可以在该文件上运行MLModel.compileModel来生成模型的编译版本:
let compiledModelUrl = try MLModel.compileModel(at: downloadedModelUrl)
let model = try MLModel(contentsOf: compiledModelUrl)
请记住,compiledModelUrl是一个临时位置的地址。如果您希望在会话之外将模型保存在设备上更长时间,您必须将其移动到永久存储中。
注意
尽管我们可以直接使用 Core ML 手动进行模型管理,但这仍然涉及大量样板代码的编写,包括后端和设备端。我们需要手动管理每个模型的版本、文件存储和配置基础设施,以及过程中的任何错误。这就是 ML Kit 和 Fritz 真正出色的地方,它们提供了这些功能。我们将在第十三章中更详细地讨论这一点。
在设备上训练
到目前为止,我们已经看到了一些适合“一刀切”的神经网络的场景。然而,一些用例如果没有个性化模型将无法正常工作。一个例子是一个根据每张图片中人脸识别来组织用户照片库的应用。鉴于普通用户的手机大多充斥着朋友和家人的照片,一个训练有丹尼·德维托和汤姆·汉克斯脸部的通用模型对用户来说并不是很有用(除非当然他们属于德维托或汉克斯家族)。
一个现实生活中的例子是 iOS 系统键盘,它会随着时间学习用户的语言模式,并开始为该用户提供越来越相关的建议。当用户使用俚语、昵称、领域特定术语等时,这变得更加明显,这些可能不是常见语言词典的一部分。基于这些数据的个性化建议对于其他用户来说是无用的。
在这些情况下,我们希望为该用户收集数据并仅为该个人训练个性化模型。实现这一目标的一种方法是收集并发送数据到云端,在那里训练一个新模型,然后将更新后的模型发送回用户。这种方法存在可扩展性、成本和隐私问题。
相反,Core ML 提供了设备上训练的功能,因此用户的数据永远不需要离开设备。当isUpdatable属性设置为true时,Core ML 模型是可更新的。此外,需要重新训练的特定层集(通常是网络末端)也需要将相同属性设置为true。还可以在模型上设置额外的训练参数,如学习率和优化器。
尽管训练可能会消耗 GPU 和神经处理器单元(NPU;更多信息请参阅第十三章),但训练可以在后台进行调度(使用BackgroundTasks框架),即使设备处于空闲和充电状态,通常在晚上。这对用户体验的影响最小。
要进行设备上的训练,我们可以调用MLUpdateTask函数,以及我们将用于重新训练模型的新数据。我们还需要在函数调用中传入新更新模型的路径。训练完成后,该路径上的模型就准备就绪了:
let modelUrl = bundle.url(forResource: "MyClassifier",
withExtension: "mlmodelc")!
let updatedModelUrl = bundle.url(forResource: "MyClassifierUpdated",
withExtension: "mlmodelc")!
let task = try MLUpdateTask(
forModelAt: modelUrl,
trainingData: trainData,
configuration: nil,
completionHandler: { [weak self] (updateContext) in
self.model = updateContext.model
updateContext.model.write(to: updatedModelUrl)
})
task.resume()
联邦学习
设备上的训练很棒,除了一个缺点:通用全局模型没有机会改进。开发人员是否可以以某种方式利用每个用户设备上生成的数据来改进全局模型,而无需从其设备传输数据?这就是联邦学习的作用。
联邦学习是一种协作分布式训练过程。它通过将增量更新(来自用户设备的个性化模型)发送到云端,并在整个用户群体中聚合这些更新,从而为每个人丰富全局模型,将设备上的训练推进了一步。请记住,这里没有传输任何用户数据,也不可能从聚合的特征集中逆向工程用户数据。通过这种方法,我们能够尊重用户的隐私,同时通过集体参与使每个人受益。
设备上的训练是联邦学习的一个重要步骤。尽管我们还没有达到这一步,但这是行业发展的方向。我们可以期待随着时间的推移看到对联邦学习的更多支持。
TensorFlow Federated 是联邦学习的一个实现,为谷歌的 GBoard 键盘应用提供训练支持。用户设备上的训练发生在它们正在充电时的后台。
性能分析
编写原型是一回事。制作适用于生产的应用程序则完全是另一回事。有几个因素会影响用户体验,了解权衡是很重要的。其中一些因素包括支持的设备型号、最低操作系统(OS)版本、处理帧率以及深度学习模型的选择。在本节中,我们探讨了这些因素对产品质量和性能的影响。
在 iPhone 上对模型进行基准测试
与基准测试一样,最好在公开可下载的模型上进行实验,以便我们可以轻松下载并进行实践!
首先,我们在 2013 年至 2018 年生产的多款 iPhone 上运行了我们的实时对象分类应用程序。表 11-3 列出了这些实验的结果。
表 11-3。不同 iPhone 版本上不同型号的基准推理时间
| 设备型号 | iPhone 5s | iPhone 6 | iPhone 6s | iPhone 7+ | iPhone X | iPhone XS | iPhone 11 Pro |
|---|---|---|---|---|---|---|---|
| 发布年份 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| RAM | 1 GB | 1 GB | 2 GB | 2 GB | 2 GB | 4 GB | 4GB |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 处理器芯片 | A7 | A8 | A9 | A10 | A11 | A12 | A13 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 模型 | 准确率(%) | 大小(MB) | FPS | FPS | FPS | FPS | FPS |
| VGG-16 | 71 | 553 | 0.1 | 0.2 | 4.2 | 5.5 | 6.9 |
| InceptionV3 | 78 | 95 | 1.4 | 1.5 | 9 | 11.1 | 12.8 |
| ResNet-50 | 75 | 103 | 1.9 | 1.7 | 11.6 | 13.5 | 14.1 |
| MobileNet | 71 | 17 | 7.8 | 9 | 19.6 | 28.6 | 28.6 |
| SqueezeNet | 57 | 5 | 13.3 | 12.4 | 29.4 | 33.3 | 34.5 |
2014 年和 2015 年推理时间之间的差异尤为显著。2015 年发生了什么?如果你猜到了 GPU,你是对的。iPhone 6S 首次引入了专用 GPU,支持诸如“嘿 Siri”之类的功能。
另一方面,让我们看看 2018 年 9 月 iPhone XS 发布月份在美国 iPhone 中的市场份额,如表 11-4 所示。请注意,发布通常在每年的 9 月份进行。
表 11-4。截至 2018 年 9 月的美国 iPhone 设备市场份额(iPhone XS 发布月份;数据来自Flurry Analytics,排除 iPhone XS、XS Plus 和 XS Max)
| 发布年份 | iPhone 型号 | 百分比 |
|---|---|---|
| 2017 | 8 Plus | 10.8% |
| 2017 | X | 10.3% |
| 2017 | 8 | 8.1% |
| 2016 | 7 | 15.6% |
| 2016 | 7 Plus | 12.9% |
| 2016 | SE | 4.2% |
| 2015 | 6S | 12.5% |
| 2015 | 6S Plus | 6.1% |
| 2014 | 6 | 10.7% |
| 2014 | 6 Plus | 3.3% |
| 2013 | 5S | 3.4% |
| 2013 | 5C | 0.8% |
| 2012 | 5 | 0.8% |
| 2011 | 4S | 0.4% |
| 2010 | 4 | 0.2% |
从表 11-4 中推导出累积百分比,我们得到了表 11-5。该表根据我们选择支持的最旧设备来表达我们的潜在市场份额。例如,2016 年 9 月之后发布的 iPhone(2018 年 9 月之前的两年)总市场份额为 61.9%。
表 11-5。iPhone 年度累积市场份额
| 年份 | 累积百分比 |
|---|---|
| 1 | 29.2% |
| 2 | 61.9% |
| 3 | 80.5% |
| 4 | 94.5% |
| 5 | 98.7% |
| 6 | 99.5% |
结合基准和市场份额,我们有一些设计选择和优化可供选择:
使用更快的模型
在 iPhone 6 上,VGG-16 的运行速度比 MobileNet 慢大约 40 倍。在 iPhone XS 上,它仍然比 MobileNet 慢大约两倍。仅仅选择一个更高效的模型就可以对性能产生显著影响,通常无需牺牲等量的准确性。值得注意的是,MobileNetV2 和 EfficientNet 提供了更好的速度和准确性的组合。
确定支持的最低 FPS
一个在实时摄像头视频上应用滤镜的功能需要能够处理摄像头每秒传送的大量帧,以确保流畅的体验。相比之下,由于用户发起的操作,一个处理单个图像的应用并不需要太过担心性能。很多应用程序会处于两者之间的某个位置。重要的是确定最低必要的 FPS,并进行类似于表 11-5 的基准测试,以确定跨 iPhone 世代的最佳模型。
批处理
GPU 非常擅长并行处理。因此,将一批数据进行处理比对每个项目进行推断更有效并不奇怪。Core ML 利用这一事实通过暴露批处理 API 来实现。一些用户体验,特别是异步和/或内存密集型的体验,可以从这些批处理 API 中获益。例如,在照片库中进行任何批量处理照片。我们可以将一组图像一起发送到批处理 API,以便 Core ML 可以优化 GPU 上的性能,而不是逐个处理图像。
动态模型选择
根据使用情况,我们可能对高准确度并不像对流畅体验(在某个最低 FPS 阈值下)那样在意。在这些情况下,我们可能会在较慢、较旧的设备上选择一个更轻、准确性较低的模型,而在更快、现代化的设备上选择一个更大、更准确的模型。这可以与通过云端进行模型部署结合使用,这样我们就不会不必要地增加应用的大小。
利用“Sherlocking”来获益
“Sherlocking”是一个术语,用于描述第一方供应商(尤其是苹果)通过内置功能使第三方软件变得过时。一个很好的例子是,当苹果发布了 iPhone 内置的手电筒功能时,所有第三方手电筒应用程序(无论是付费还是免费)都变得过时了。举个例子,假设一个应用在 2017 年发布了一个面部跟踪功能。一年后,苹果在 iOS 12 的 Core ML 中添加了一个更准确的面部跟踪 API。由于面部跟踪的神经网络已经内置在操作系统中,该应用可以更新其代码以使用内置的 API。然而,由于 API 在 iOS 11 中仍然不可用,该应用可以采用混合方法,通过使用旧代码路径保持向后兼容性。应用开发人员可以停止将神经网络捆绑到应用中,从而减小其大小,可能减少几兆字节,并动态下载旧模型以供 iOS 11 用户使用。
优雅降级
有时,由于性能需求,较旧的手机可能无法处理较新的深度学习模型。这是一个完全合理的情况,特别是如果该功能是尖端的。在这些情况下,可以采用两种方法之一。第一种是将计算外包到云端。这显然会牺牲互动性和隐私。另一种选择是为这些用户禁用该功能,并向他们显示为什么该功能不可用的消息。
测量能源影响
在之前的章节中,我们专注于在服务器上托管分类器。尽管有许多因素会影响设计决策,但能量消耗通常不在其中。然而,在客户端,电池电量往往是有限的,最小化其消耗成为首要任务。用户对产品的感知很大程度上取决于其消耗的能量。还记得以前 GPS 是电池杀手的时代吗?许多需要位置访问的应用程序因为消耗太多电池电量而获得了大量一星评价。我们绝对不希望出现这种情况。
注意
当用户意识到应用程序比预期消耗更多电池时,他们通常会非常慷慨地给出差评,正如图 11-13 中所示的评论所示。

图 11-13. App Store 中关于 YouTube 和 Snapchat 的评论,抱怨电池消耗过大
在这里,我们利用 Xcode 的调试导航器中的能量影响选项卡(图 11-14),并生成图 11-15。

图 11-14. Xcode 调试导航器选项卡

图 11-15. Xcode 2017 年 iPad Pro 上的能量影响图表(注意:此截图是在不同时间拍摄的,所以数字略有不同)
图 11-15 显示了在 2017 年 iPad Pro 上运行该过程的能量影响很大。其中一个重要原因是,在我们的示例代码中,我们正在处理从摄像头接收到的每一帧。这意味着摄像头捕获的每一帧都会被发送到 GPU 进行处理,导致能量消耗增加。在许多实际应用中,并不需要对每一帧进行分类。即使处理每隔一帧,也可以实现显著的能量节省,而不会对用户体验产生一般性影响。在接下来的部分中,我们将探讨帧处理速率与能量影响之间的关系。
注意
CPU 与 GPU 使用率的比例受模型架构影响,具体取决于卷积操作的数量。在这里,我们对不同模型的平均 CPU 和 GPU 利用率进行了分析(图 11-16)。这些数字可以从 Xcode 的能量影响图表中提取出来。请注意,我们通常更喜欢 GPU 利用率更高的模型以提高性能效率。

图 11-16. 比较 iOS 11 上不同模型的 CPU 和 GPU 利用率
基准负载
正如您所期望的那样,在每帧实时运行 CNN 模型会导致高 GPU/CPU 利用率,从而迅速耗尽电池。用户可能还会注意到手机发热。
与其在每帧上运行分析,不如我们跳过几帧?MobileNet 在 iPad Pro 2017 上分析一帧需要 20 毫秒。因此,它每秒分类约 50 帧。相反,如果我们以 1FPS 运行它,GPU 利用率从 42%降低到仅 7%——降低了超过 83%!对于许多应用程序来说,以 1FPS 处理可能已经足够,同时仍保持设备冷却。例如,一个安全摄像头即使每隔几秒处理一帧,也可能表现得相当不错。
通过调整每秒分析的帧数并测量 GPU 利用率百分比,我们观察到了一个有趣的趋势。从图 11-17 中的图表可以明显看出,FPS 越高,GPU 利用率越高。这对能源消耗有显著影响。对于需要长时间运行的应用程序,减少每秒推断次数可能是有益的。

图 11-17。调整 FPS 并分析 2017 年 iPad Pro 上的负载
图中显示的值是从 Xcode 仪器的核心动画仪器中得出的。以下是我们用来生成结果的过程:
-
从 Xcode 中,点击产品,然后选择配置文件。
-
仪器窗口出现后,选择核心动画仪器,如图 11-18 所示。
![Xcode 仪器中的仪器窗口]()
图 11-18。Xcode 仪器中的仪器窗口
-
按下录制按钮以开始在分析模式下运行应用程序。
-
等待几秒钟开始收集仪器数据。
-
测量 GPU 硬件利用率列中的值(图 11-19)。
![在核心动画工具中实时分析的应用程序]()
图 11-19。在核心动画工具中实时分析的应用程序
到目前为止,在本章中,我们已经探讨了使用 Core ML 制作生产级应用程序的技术。在下一节中,我们将讨论现实世界中这类应用程序的示例。
减小应用程序大小
应用程序大小对于某些市场的开发人员至关重要。苹果不允许通过蜂窝网络下载大于 200MB 的应用程序。这对于那些经常在外出时使用的应用程序(如 Uber 和 Lyft 等打车应用程序)来说是一个关键的争议点。有趣的一点是:Uber 必须进行一些非常严格的优化,例如大幅减少 Swift 可选项、结构体和协议的数量(否则有助于代码可维护性),以使应用程序二进制大小低于 App Store 限制(当时为 150MB)。如果不这样做,公司将失去很多新用户。
任何我们在应用程序中引入的新 AI 功能都会导致存储使用量增加是理所当然的。我们可以采用一些策略来减少这种影响。
注意
总部位于旧金山的数据分析公司 Segment 想要确定应用程序大小对安装数量的影响有多大。为了进行实验,该公司购买了一个按揭计算器应用程序,该应用程序的应用下载量保持稳定(每天约 50 次)。他们拿到了一个原始大小为 3MB 的应用程序,然后反复地用泰勒·斯威夫特专辑的图片膨胀它(嘿,这是为了科学和研究!)。工程师们观察到,随着公司增加应用程序大小,每日安装量显著下降。当应用程序超过 100MB(当时 App Store 通过蜂窝网络下载的最大限制)时,每日安装量下降了惊人的 44%!此外,该应用程序吸引了一些负面评论,用户对应用程序大小表示怀疑。
这个故事的寓意是,应用程序大小比我们想象的更为重要,我们在发布应用程序之前应该注意我们的应用程序消耗的空间量。
避免捆绑模型
如果可能的话,我们应该避免将模型与 App Store 二进制文件捆绑在一起。无论如何都需要下载相同数量的数据,所以只要用户体验不受影响,我们应该在实际使用功能时延迟模型下载。此外,我们应该在 WiFi 环境下下载模型,以保留蜂窝带宽。Microsoft Translator 和 Google Translate 实现了一种形式,它们一开始只进行基于云的翻译。知道旅行者经常使用这些应用(可能没有良好的互联网接入),它们还提供了离线模式,在这种模式下,所需的语言模型会在用户提示的情况下在后台下载。
使用量化
正如我们在 第六章 中讨论的,量化是一种很好的策略,可以大幅减小模型大小,同时保持性能。基本上,它将 32 位浮点权重降低到 16 位浮点,将 8 位整数一直降低到 1 位。但是,我们绝对不建议低于 8 位,因为会导致准确性下降。我们可以在几行代码中使用 Core ML Tools 为 Keras 模型实现量化:
import coremltools
model_spec = coremltools.utils.load_spec("MyModel.mlmodel")
# 16-bit conversion
model_fp16_spec =
coremltools.utils.convert_neural_network_spec_weights_to_fp16(model_spec)
coremltools.utils.save_spec(model_fp16_spec, "MyModel_FP16.mlmodel")
# 8-bit or lower quantization
num_bits = 8
model_quant_spec =
coremltools.models.neural_network.quantization_utils.quantize_weights(model_spec,
num_bits, "linear")
coremltools.utils.save_spec(model_quant_spec, "MyModel_Quant.mlmodel")
展示量化对具有挑战性数据集的影响,其中小的修改可能导致 drastc 改变,我们选择使用 Keras(大约 14 MB 大小)为牛津的 102 种花卉数据集构建分类器,并将其量化为不同的位表示,同时测量其准确性并减小其大小。为了衡量预测的变化,我们比较完整精度模型和量化模型之间的匹配百分比。我们测试了三种量化模式。
-
简单的
linear量化,我们在 第六章 中描述过。在这种策略中,间隔是均匀分布的。 -
使用查找表的线性量化,或
linear_lut。在这种技术中,间隔是不均匀分布的,密集区域的间隔更小,有更多的间隔,而稀疏区域的间隔更大,有更少的间隔。由于这些间隔是不均匀的,它们需要存储在查找表中,而不是直接用简单的算术计算。 -
由 k-means 生成的查找表,或
kmeans_lut,通常用于最近邻分类器。
表 11-6 展示了我们的观察结果。
表 11-6。不同目标位大小和不同量化模式的量化结果
| 量化为 | 百分大小减少(约) | 与 32 位结果匹配的百分比 |
|---|---|---|
linear |
linear_lut |
kmeans_lut |
| --- | --- | --- |
| 16 位 | 50% | 100% |
| 8 位 | 75% | 88.37% |
| 4 位 | 88% | 0% |
| 2 位 | 94% | 0% |
| 1 位 | 97% | 0% |
这给了我们一些见解。
-
将表示降低到 16 位对准确性没有任何影响。我们可以将模型大小几乎减半,而准确性几乎没有差异。
-
当用于构建查找表时,k-means 优于简单的线性分割方法。即使在 4 位时,也只损失了 20% 的准确性,这是非常了不起的。
-
将量化模型降至 8 位可以使模型大小减小 4 倍,而准确性几乎没有损失(特别是对于
kmeans_lut模式)。 -
将量化降至低于 8 位会导致准确性急剧下降,特别是对于线性模式。
使用 Create ML
Create ML 是苹果的一个工具,通过简单地将数据拖放到 Mac 上的 GUI 应用程序中来训练模型。它提供了几个模板,包括对象分类/检测、声音分类和文本分类等,使得即使没有领域专业知识的新手也可以进行训练 AI。它使用迁移学习来调整只有少数几层对我们的任务是必要的。因此,训练过程可以在几分钟内完成。操作系统内置了大部分层(可用于多个任务),而特定任务的层可以作为应用程序的一部分进行发布。最终导出的模型非常小(仅有 17 KB,我们很快就会看到)。我们将在下一章中亲自体验 Create ML。
案例研究
让我们看一些使用 Core ML 进行移动推断的真实案例。
魔术数独
在 2017 年ARKit 在 iOS 上推出后不久,来自 Hatchlings 的游戏和移动应用初创公司的魔术数独应用成为了突破性的热门应用之一。只需将手机对准数独谜题,应用程序就会在纸上显示一个已解决的数独。正如我们此时可能已经猜到的那样,该系统使用 Core ML 来运行基于 CNN 的数字识别器。系统经过以下步骤:
-
使用 ARKit 获取相机帧。
-
使用 iOS Vision Framework 查找矩形。
-
确定它是否为数独网格。
-
从数独图像中提取 81 个方块。
-
使用 Core ML 识别每个方块中的数字。
-
使用数独求解器完成空方块。
-
使用 ARKit 在原始纸张表面上投影完成的数独,如图 11-20 所示。

图 11-20. 解决 ARKit 的逐步解决方案(图片来源)
Hatchlings 团队首先使用了 MNIST 数字识别模型,该模型主要由手写数字组成。但对于印刷字体来说并不稳健。然后,团队拍摄了数千页数独书,使用其流水线提取方块,直到获得了大量不同字体中的单个数字图像。为了标记这些数据,团队请求其粉丝将每个项目分类为 0 到 9 和空类。在 24 小时内,团队获得了 60 万个数字扫描。下一步是训练一个定制的 CNN,因为系统需要快速工作,因为需要对 81 个方块图像进行分类。使用 Core ML 部署这个模型后,应用程序推出并一夜之间成为了热门。
与野外的新用户一起运行带来了以前未预料到的新情况。因为大多数用户面前没有数独谜题,他们经常在计算机屏幕上搜索数独,这对模型总是准确识别是困难的。此外,由于 ARKit 的固定焦距限制,输入图像可能会稍微模糊。为了解决这个问题,Hatchlings 团队收集了额外的计算机屏幕上带有谜题的照片示例,稍微模糊了它们,并使用额外的数据训练了一个新的 CNN。通过 App Store 更新,整个体验变得更加稳健。总之,在推出应用程序或服务时,应用程序构建者需要不断从以前未预料到的新场景中学习。
Seeing AI
Seeing AI 是微软研究设计的一款面向盲人和低视力社区的语音相机应用程序。它使用计算机视觉通过口头语音描述人物、文本、手写、物体、货币等等。大部分图像处理在设备上本地进行,使用 Core ML。其中一个核心用户需求是识别产品——通常可以通过扫描条形码的特写来确定。但对于盲人用户来说,条形码的位置是未知的,使大多数条形码应用程序难以使用。为了解决这个问题,团队构建了一个自定义的 CNN,用包含不同角度、大小、光照和方向的条形码图像进行训练。用户现在尝试在 iPhone 前旋转物体,当 CNN 分类出条形码的存在时(实时运行,逐帧),它会发出可听的蜂鸣声。蜂鸣声的频率直接与相机可见的条形码区域相关。当盲人用户开始将条形码靠近时,蜂鸣声会变得更快。当条形码阅读库能够清晰看到条形码时,应用程序解码通用产品代码并朗读产品名称。过去,盲人用户通常需要购买并携带笨重的激光条形码扫描仪,通常价格超过 1300 美元。事实上,一个慈善机构筹集了数百万美元,捐赠这些硬件条形码阅读器给需要的人。现在,深度学习可以免费解决这个问题。这是一个将计算机视觉和用户体验结合起来解决现实问题的好例子。
HomeCourt
在生活中,无论是写作、演奏乐器还是烹饪,定期练习是必不可少的。然而,练习的质量远比数量重要。使用数据支持我们的练习并监控我们的进步,对我们获得技能的速度有奇效。这正是总部位于加利福尼亚州圣何塞的初创公司 NEX Team 为篮球练习设定的目标,借助其 HomeCourt 应用程序的帮助。运行该应用程序很简单:将其设置在地面或三脚架上,将相机对准篮球场,然后开始录制。
该应用程序在 Core ML 之上实时运行对象检测器,以跟踪球、人和篮球框。在这些人中,一个重要问题是:谁投篮了?当球靠近篮筐时,应用程序会倒带视频以识别投篮的球员。然后,它对球员进行人体姿势估计,以跟踪球员的动作。如果这还不够令人印象深刻,它使用几何变换将球场的这个 3D 场景转换为 2D 地图(如图 11-21 右上角所示),以跟踪球员投篮的位置。这里需要注意的重要一点是,多个模型同时运行。根据跟踪的对象和球员的身体位置、关节等,应用程序为球员提供每次投篮的释放高度、释放角度、释放位置、释放时间、球员速度等统计数据和可视化。这些统计数据很重要,因为它们与成功尝试有很高的相关性。球员可以在比赛进行时录制整个比赛,然后回家分析每个投篮,以确定他们的弱点,以便在下次改进。
在不到两年的时间里,这家小型初创公司吸引了数十万用户,口碑传播从业余者到专业人士。甚至美国国家篮球协会(NBA)与 NEX Team 合作,帮助改善他们球员的表现,使用 HomeCourt。所有这些都是因为他们看到了一个未满足的需求,并提出了一个创造性的解决方案,使用深度学习。

图 11-21。HomeCourt 应用程序在玩家进行比赛时实时跟踪其投篮
InstaSaber + YoPuppet
你知道《星球大战》系列中最大的利润制造者是谁吗?不是电影票房收入,绝对不是从销售 DVD 中获得的。这里有一个提示:它押韵的是商品。卢卡斯影业(现在是迪士尼)通过销售 R2D2 玩具和 Chewbacca 服装赚了很多钱。尽管永远的最爱仍然是心爱的光剑。然而,与电影中看起来很酷不同,商品光剑大多由塑料制成,实际上看起来不太科幻。此外,你挥舞几次,很可能会不小心打到别人的脸。
2020CV 的创始人哈特·伍勒利决定改变这一点,通过 InstaSaber 应用程序将好莱坞级别的视觉效果带到我们的手机上。只需卷起一张纸,握住它,将手机的摄像头对准它,看着它变成一个发光的光剑,如图 11-22 所示。挥动你的手,不仅可以实时跟踪它,还可以听到与卢克与他的父亲(剧透!)达斯维达战斗时相同的声音效果。
将追踪魔法提升到下一个级别,他开发了 YoPuppet,该应用程序实时跟踪手部关节,构建出模仿手部运动的虚拟木偶。只有当它真正实时无延迟,准确且看起来逼真时,才能产生一种悬置现实的效果。
除了逼真,这些应用程序玩起来也很有趣。难怪 InstaSaber 在网上和新闻中成为一夜爆红,获得数百万次的病毒浏览。AI 潜力甚至让亿万富翁投资者马克·库班说:“你得到了一笔交易”,并投资于 2020CV。

图 11-22。InstaSaber 和 YoPuppet 的屏幕截图
摘要
在本章中,我们快速浏览了 Core ML 的世界,它为在 iOS 设备上运行机器学习和深度学习算法提供了推理引擎。通过分析运行 CNN 所需的最小代码,我们构建了一个实时物体识别应用程序,可以对 1,000 个 ImageNet 类别进行分类。在此过程中,我们讨论了一些关于模型转换的有用信息。进入下一个级别,我们学习了关于动态模型部署和设备上训练等实用技术,同时还在各种 iOS 设备上对不同的深度学习模型进行了基准测试,从而更深入地了解了电池和资源约束。我们还看了如何使用模型量化来优化应用程序大小。最后,为了从行业中获得一些灵感,我们探索了 Core ML 在生产应用程序中的使用实例。
在下一章中,我们将通过使用 Create ML(以及其他工具)进行训练,部署我们的自定义训练分类器,并使用 Core ML 运行一个端到端的应用程序。
第十二章:在 iOS 上使用 Core ML 和 Create ML 的 Not Hotdog
“我是富人,”新晋百万富翁简阳在接受彭博社采访时说([图 12-1](part0014.html#jian_yang_being_interviewed_by_bloomberg))。他做了什么?他创建了 Not Hotdog 应用程序([图 12-2](part0014.html#the_not_hotdog_app_in_action_left_parent)),让世界“变得更美好”。

图 12-1。简阳在 Periscope 收购他的“Not Hotdog”技术后接受彭博新闻采访(图片来源:来自 HBO 的硅谷)
对于我们中的少数人可能会感到困惑(包括本书的三分之一作者),我们正在参考 HBO 的硅谷,这是一部节目,其中一个角色被要求制作 SeeFood - “食物的 Shazam”。它旨在对食物图片进行分类,并提供食谱和营养信息。令人发笑的是,该应用程序最终只能识别热狗。其他任何东西都将被分类为“Not Hotdog”。
我们选择引用这个虚构的应用程序有几个原因。它在流行文化中非常重要,许多人可以轻松地与之联系。它是一个典范:足够简单,但又足够强大,可以在现实世界的应用中看到深度学习的魔力。它也非常容易地可以推广到识别多个类别的物品。

图 12-2。Not Hotdog 应用程序的操作(图片来源:苹果应用商店中的 Not Hotdog 应用程序)
在本章中,我们通过几种不同的方法来构建一个 Not Hotdog 克隆。整个端到端过程的一般概述如下:
-
收集相关数据。
-
训练模型。
-
转换为 Core ML。
-
构建 iOS 应用程序。
[表 12-1](part0014.html#various_approaches_to_get_a_model_ready)介绍了步骤 1 到 3 的不同选项。在本章后面,我们将深入研究每个选项。
表 12-1。从头开始为移动部署准备模型的各种方法
| 数据收集 | 训练机制 | 模型转换 |
|---|
|
-
查找或收集数据集
-
Fatkun Chrome 浏览器扩展
-
使用 Bing Image Search API 的网络爬虫
|
-
基于 Web 的 GUI:CustomVision.ai,IBM Watson,Clarifai,Google AutoML
-
创建 ML
-
使用任何选择的框架,如 Keras 进行微调
|
-
创建 ML,CustomVision.ai 和其他 GUI 工具生成.mlmodel。
-
对于 Keras,请使用 Core ML 工具。
-
对于 TensorFlow 训练的模型,请使用
tf-coreml。
|
让我们开始吧!
收集数据
要开始使用深度学习解决任何计算机视觉任务,我们首先需要有一组图像数据集进行训练。在本节中,我们使用三种不同的方法来收集相关类别的图像,所需时间逐渐增加,从几分钟到几天。
方法 1:查找或收集数据集
解决问题的最快方法是手头上有现有的数据集。有大量公开可用的数据集,其中一个类别或子类别可能与我们的任务相关。例如,ETH Zurich 的 Food-101([https://www.vision.ee.ethz.ch/datasets_extra/food-101/](https://oreil.ly/dkS6X))包含一个热狗类别。另外,ImageNet 包含 1,257 张热狗的图像。我们可以使用剩余类别的随机样本作为“Not Hotdog”。
要从特定类别下载图像,可以使用[ImageNet-Utils](https://oreil.ly/ftyOU)工具:
-
在 ImageNet 网站上搜索相关类别;例如,“热狗”。
-
注意 URL 中的
wnid(WordNet ID):[http://image-net.org/synset?wnid=n07697537](http://image-net.org/synset?wnid=n07697537)。 -
克隆 ImageNet-Utils 存储库:
$ git clone --recursive https://github.com/tzutalin/ImageNet_Utils.git -
通过指定
wnid下载特定类别的图像:$ ./downloadutils.py --downloadImages --wnid n07697537
如果我们找不到数据集,我们也可以通过使用智能手机自己拍照来构建自己的数据集。我们拍摄的照片必须代表我们的应用程序在现实世界中的使用方式。另外,通过向朋友、家人和同事提问,可以生成一个多样化的数据集。大公司使用的另一种方法是雇佣承包商负责收集图像。例如,Google Allo 发布了一个功能,可以将自拍照片转换为贴纸。为了构建这个功能,他们雇佣了一组艺术家拍摄照片并创建相应的贴纸,以便他们可以对其进行训练。
注意
确保检查数据集中图像发布的许可证。最好使用根据宽松许可证发布的图像,如知识共享许可证。
方法 2:Fatkun Chrome 浏览器扩展
有几个浏览器扩展可以让我们从网站批量下载多个图像。一个例子是Fatkun 批量下载图像,这是 Chrome 浏览器上可用的浏览器扩展。
我们可以通过以下简短快速的步骤准备好整个数据集。
-
将扩展添加到我们的浏览器。
-
在 Google 或必应图像搜索中搜索关键字。
-
选择搜索设置中适合图像许可证的过滤器。
-
页面重新加载后,多次向下滚动几次,以确保页面上加载更多缩略图。
-
打开扩展并选择“此标签”选项,如图 12-3 所示。
![“热狗”的必应搜索结果]()
图 12-3. “热狗”的必应搜索结果
-
请注意,默认情况下选择了所有缩略图。在屏幕顶部,单击切换按钮以取消选择所有缩略图并仅选择我们需要的缩略图。我们可以设置最小宽度和高度为 224(大多数预训练模型将 224x224 作为输入尺寸)。
![通过 Fatkun 扩展选择图像]()
图 12-4. 通过 Fatkun 扩展选择图像
-
在右上角,单击“保存图像”以将所有选定的缩略图下载到我们的计算机上。
注意
请注意,屏幕截图中显示的图像是标志性图像(即主要对象直接聚焦在干净的背景上)。只使用这样的图像在我们的模型中可能导致其无法很好地推广到真实世界的图像。例如,在具有干净白色背景的图像中(如在电子商务网站上),神经网络可能会错误地学习到白色背景等于热狗。因此,在进行数据收集时,请确保您的训练图像代表真实世界。
提示
对于“非热狗”的负类别,我们希望收集大量可用的随机图像。此外,收集看起来类似于热狗但实际上不是的物品;例如,潜水艇三明治、面包、盘子、汉堡等等。
热狗的常见共现物品缺失,如盘子上的食物、纸巾、番茄酱瓶或包装袋,可能会误导模型认为那些是真正的热狗。因此,请确保将这些添加到负类别中。
当您安装像 Fatkun 这样的浏览器扩展时,它将请求权限读取和修改我们访问的所有网站上的数据。当您不使用它下载图像时,最好禁用该扩展。
方法 3:使用必应图像搜索 API 的网络爬虫
对于构建更大的数据集,使用 Fatkun 收集图像可能是一个繁琐的过程。此外,Fatkun 浏览器扩展返回的图像是缩略图,而不是原始大小的图像。对于大规模图像集合,我们可以使用搜索图像的 API,比如必应图像搜索 API,其中我们可以建立一定的约束,如关键字、图像大小和许可证。谷歌曾经有图像搜索 API,但在 2011 年停止了。
必应的搜索 API 是其基于 AI 的图像理解和传统信息检索方法(即使用来自“alt-text”、“metadata”和“caption”等字段的标签)的融合。由于这些字段的误导性标签,我们往往会得到一些不相关的图像。因此,我们希望手动解析收集的图像,以确保它们实际上与我们的任务相关。
当我们有一个非常庞大的图像数据集时,手动筛选出所有质量差的训练示例可能是一项艰巨的任务。以迭代的方式逐步改进训练数据集的质量会更容易。以下是高层次的步骤:
-
通过手动审查少量图像创建训练数据的子集。例如,如果我们的原始数据集中有 50k 张图像,我们可能希望手动选择大约 500 个良好的训练示例进行第一次迭代。
-
对这 500 张图像进行训练。
-
在剩余的图像上测试模型,并为每个图像获取置信度值。
-
在置信度值最低的图像中(即经常错误预测的图像),审查一个子集(比如 500 个)并丢弃不相关的图像。将此子集中剩余的图像添加到训练集中。
-
重复步骤 1 到 4 几次,直到我们对模型的质量感到满意。
这是一种半监督学习形式。
提示
您可以通过将被丢弃的图像重新用作负训练示例来进一步提高模型的准确性。
注意
对于一组没有标签的大量图像,您可能希望使用其他定义文本的共现作为标签;例如,标签、表情符号、alt 文本等。
Facebook 利用帖子文本中的标签构建了一个包含 35 亿张图像的数据集,将它们作为弱标签进行训练,并最终在 ImageNet 数据集上进行微调。这个模型比最先进的结果提高了 2%(85%的前 1%准确率)。
现在我们已经收集了图像数据集,让我们最终开始训练它们。
训练我们的模型
广义上说,有三种简单的训练方式,我们之前已经讨论过。在这里,我们提供了几种不同方法的简要概述。
方法 1:使用基于 Web UI 的工具
如第八章中讨论的,有几种工具可以通过提供带标签的图像并使用 Web UI 进行训练来构建自定义模型。微软的 CustomVision.ai、Google AutoML、IBM Watson Visual Recognition、Clarifai 和 Baidu EZDL 是几个例子。这些方法无需编码,许多提供简单的拖放 GUI 进行训练。
让我们看看如何在不到五分钟的时间内使用 CustomVision.ai 创建一个适合移动设备的模型:
-
访问http://customvision.ai,并创建一个新项目。因为我们想要将训练好的模型导出到手机上,所以选择一个紧凑型模型类型。由于我们的领域与食品相关,选择“食品(紧凑型)”,如图 12-5 所示。
![在 CustomVision.ai 上定义一个新项目]()
图 12-5. 在 CustomVision.ai 上定义一个新项目
-
上传图像并分配标签(标签),如图 12-6 所示。每个标签至少上传 30 张图像。
![在 CustomVision.ai 仪表板上上传图像。请注意,标签已填充为 Hotdog 和 Not Hotdog]()
图 12-6. 在 CustomVision.ai 仪表板上上传图片。请注意,标签已填充为 Hotdog 和 Not Hotdog
-
点击训练按钮。一个对话框会打开,如图 12-7 所示。快速训练主要训练最后几层,而高级训练可能会调整整个网络,从而获得更高的准确性(显然需要更多时间和金钱)。对于大多数情况,快速训练选项应该足够了。
![训练类型选项]()
图 12-7. 训练类型选项
-
不到一分钟,一个屏幕应该出现,显示每个类别新训练模型的精度和召回率,如图 12-8 所示。(这应该让你想起我们之前在书中讨论过的精度和召回率。)
![新训练模型的精度、召回率和平均精度]()
图 12-8. 新训练模型的精度、召回率和平均精度
-
调整概率阈值,看看它如何改变模型的性能。默认的 90%阈值可以取得相当不错的结果。阈值越高,模型变得越精确,但召回率会降低。
-
点击导出按钮,选择 iOS 平台(图 12-9)。在内部,CustomVision.ai 将模型转换为 Core ML(如果要导出到 Android,则转换为 TensorFlow Lite)。
![CustomVision.ai 中的模型导出选项]()
图 12-9. CustomVision.ai 中的模型导出选项
我们完成了,而且完全不需要编写一行代码!现在让我们看看更方便的无编码训练方式。
方法 2:使用 Create ML
2018 年,苹果推出了 Create ML,作为苹果生态系统内开发者训练计算机视觉模型的一种方式。开发者可以打开一个playground,写几行 Swift 代码来训练图像分类器。或者,他们可以使用CreateMLUI导入在 playground 内显示有限的 GUI 训练体验。这是一个让 Swift 开发者能够部署 Core ML 模型而无需太多机器学习经验的好方法。
一年后,在 2019 年的苹果全球开发者大会(WWDC)上,苹果通过在 macOS Catalina(10.15)上宣布独立的 Create ML 应用,进一步降低了门槛。它提供了一个易于使用的 GUI,可以训练神经网络而无需编写任何代码。训练神经网络只需将文件拖放到此 UI 中。除了支持图像分类器外,他们还宣布支持目标检测器、NLP、声音分类、活动分类(根据来自 Apple Watch 和 iPhone 的运动传感器数据对活动进行分类)以及表格数据(包括推荐系统)。
而且,速度很快!模型可以在不到一分钟内训练完成。这是因为它使用迁移学习,所以不需要训练网络中的所有层。它还支持各种数据增强,如旋转、模糊、噪声等,你只需要点击复选框。
在 Create ML 出现之前,通常认为任何想在合理时间内训练一个严肃的神经网络的人都必须拥有 NVIDIA GPU。Create ML 利用了内置的 Intel 和/或 Radeon 显卡,使 MacBook 上的训练速度更快,而无需购买额外的硬件。Create ML 允许我们同时训练多个模型,来自不同的数据源。它特别受益于强大的硬件,如 Mac Pro 甚至外部 GPU(eGPU)。
使用 Create ML 的一个主要动机是其输出的模型大小。完整模型可以分解为基础模型(生成特征)和更轻的特定任务分类层。苹果将基础模型内置到其每个操作系统中。因此,Create ML 只需要输出特定任务的分类器。这些模型有多小?仅几千字节(与 MobileNet 模型的 15 MB 相比,后者已经相当小了)。在越来越多的应用开发人员开始将深度学习整合到其应用中的今天,这一点至关重要。同一神经网络不需要在多个应用程序中不必要地复制,消耗宝贵的存储空间。
简而言之,Create ML 易于使用,速度快,体积小。听起来太好了。事实证明,完全垂直集成的反面是开发人员被绑定到苹果生态系统中。Create ML 只导出.mlmodel文件,这些文件只能在 iOS、iPadOS、macOS、tvOS 和 watchOS 等苹果操作系统上使用。遗憾的是,Create ML 尚未实现与 Android 的集成。
在本节中,我们使用 Create ML 构建 Not Hotdog 分类器:
-
打开 Create ML 应用程序,点击新建文档,从可用的几个选项中选择图像分类器模板(包括声音、活动、文本、表格),如图 12-10 所示。请注意,这仅适用于 Xcode 11(或更高版本),macOS 10.15(或更高版本)。
![选择新项目的模板]()
图 12-10。选择新项目的模板
-
在下一个屏幕中,输入项目名称,然后选择完成。
-
我们需要将数据分类到正确的目录结构中。如图 12-11 所示,我们将图像放在以其标签名称命名的目录中。将训练和测试数据分别放在相应的目录中是有用的。
![将训练和测试数据放在不同的目录中]()
图 12-11。将训练和测试数据放在不同的目录中
-
将 UI 指向训练和测试数据目录,如图 12-12 所示。
![Create ML 中的训练界面]()
图 12-12。Create ML 中的训练界面
-
在选择训练和测试数据目录后,图 12-12 显示了 UI。请注意,验证数据是由 Create ML 自动选择的。此外,请注意可用的增强选项。在这一点上,我们可以点击播放按钮(右向三角形;参见图 12-13)开始训练过程。
![加载训练和测试数据后打开的 Create ML 屏幕]()
图 12-13。加载训练和测试数据后打开的 Create ML 屏幕
注意
当您进行实验时,您会很快注意到每个添加的增强都会使训练变慢。为了设定一个快速的基准性能指标,我们应该避免在第一次运行中使用增强。随后,我们可以尝试添加更多增强来评估它们对模型质量的影响。
-
当训练完成时,我们可以看到模型在训练数据、(自动选择的)验证数据和测试数据上的表现,如图 12-14 所示。在屏幕底部,我们还可以看到训练过程花费的时间以及最终模型的大小。在不到两分钟内达到 97%的测试准确率。而且输出只有 17 KB。相当不错。
![训练完成后的 Create ML 屏幕]()
图 12-14。训练完成后的 Create ML 屏幕
-
我们现在非常接近了,我们只需要导出最终模型。将输出按钮(在图 12-14 中突出显示)拖到桌面上创建.mlmodel文件。
-
我们可以双击新导出的.mlmodel文件,检查输入和输出层,以及通过将图像拖放到其中来测试模型,如图 12-15 所示。
![Xcode 中的模型检查器 UI]()
图 12-15. Xcode 中的模型检查器 UI
该模型现在已准备好插入到任何苹果设备的应用程序中。
注意
Create ML 使用迁移学习,仅训练最后几层。根据您的用例,苹果提供的底层模型可能不足以进行高质量的预测。这是因为您无法训练模型的早期层,从而限制了模型可以调整的潜力。对于大多数日常问题,这不应该是一个问题。但是,对于非常特定领域的应用,如 X 射线,或者外观非常相似的对象,细微的细节很重要(如区分货币票据),训练完整的 CNN 将是一个更好的方法。我们将在下一节中探讨如何做到这一点。
方法 3:使用 Keras 进行微调
到目前为止,我们已经成为使用 Keras 的专家。如果我们愿意进行实验并愿意花更多时间训练模型,这个选项可以让我们获得更高的准确性。我们可以重用第三章中的代码,并修改参数,如目录和文件名,批量大小和图像数量。您可以在书的 GitHub 网站上找到代码(请参见http://PracticalDeepLearning.ai),位于code/chapter-12/1-keras-custom-classifier-with-transfer-learning.ipynb。
模型训练应该需要几分钟才能完成,具体取决于硬件,在训练结束时,我们应该在磁盘上准备好一个NotHotDog.h5文件。
使用 Core ML 工具进行模型转换
如第十一章中所讨论的,有几种将我们的模型转换为 Core ML 格式的方法。
从 CustomVision.ai 生成的模型直接以 Core ML 格式可用,因此无需转换。对于在 Keras 中训练的模型,Core ML 工具可以帮助进行转换。请注意,因为我们使用了一个使用名为relu6的自定义层的 MobileNet 模型,我们需要导入CustomObjectScope:
from tensorflow.keras.models import load_model
from tensorflow.keras.utils.generic_utils import CustomObjectScope
import tensorflow.keras
with CustomObjectScope({'relu6':
tensorflow.keras.applications.mobilenet.relu6,'DepthwiseConv2D':
tensorflow.keras.applications.mobilenet.DepthwiseConv2D}):
model = load_model('NotHotDog-model.h5')
`import` coremltools
coreml_model `=` coremltools`.`converters`.`keras`.`convert(model)
coreml_model`.`save('NotHotDog.mlmodel')
现在我们有一个准备好的 Core ML 模型,我们只需要构建应用程序。
构建 iOS 应用程序
我们可以使用第十一章中的代码,只需用新生成的模型文件替换.mlmodel,如图 12-16 所示。

图 12-16. 将.mlmodel加载到 Xcode 中
现在,编译并运行应用程序,完成了!图 12-17 展示了令人惊叹的结果。

图 12-17. 我们的应用程序识别热狗
进一步探索
我们可以使这个应用程序更有趣吗?我们可以通过在下一章中涵盖的 Food-101 数据集中训练所有类别来构建一个真正的“Shazam for food”。此外,我们可以改进 UI,与我们当前应用程序显示的基本百分比相比。为了使其像“Not Hotdog”一样病毒传播,提供一种将分类分享到社交媒体平台的方式。
总结
在这一章中,我们通过一个端到端的流程,收集数据、训练和转换模型,并在 iOS 设备上实际应用中使用它。对于流程的每一步,我们探索了一些不同复杂程度的选项。而且,我们将前几章涵盖的概念放在了一个真实应用的背景下。
现在,就像建阳一样,去赚取你的百万美元吧!
第十三章:食物的 Shazam:使用 TensorFlow Lite 和 ML Kit 开发 Android 应用程序
在开发了病毒性的 Not Hotdog 应用程序(我们在第十二章中看到)之后,建阳最初应该构建一个能识别所有食物的分类器。事实上,该应用最初应该被称为 SeeFood——一款可以“看到”食物并立即识别的应用程序(图 13-1)。换句话说,这是“食物的 Shazam”。然而,该应用太成功了,以至于被 Periscope 收购。他的投资者 Erlich Bachman 的最初愿景仍未实现。在本章中,我们的任务是实现这个梦想。

图 13-1。在苹果应用商店上的 Not Hotdog 应用列表
这样的功能在哪里会有用呢?对于健康狂热者,它可以查看一道菜并提供营养信息,包括卡路里数。或者,它可以扫描几种成分,并根据它们推荐食谱。或者,它甚至可以查看市场上的产品,并检查它是否含有任何被列入黑名单的成分,如特定过敏原。
这是一个有趣的问题,有几个原因需要解决,因为它代表了几个挑战:
数据收集挑战
世界上有一百多种美食,每种美食都有数百甚至数千种菜肴。
准确性挑战
它应该大部分时间都是正确的。
性能挑战
它应该几乎立即运行。
平台挑战
仅有 iPhone 应用程序是不够的。许多发展中国家的用户使用性能较弱的智能手机,特别是 Android 设备。跨平台开发是必须的。
为一个美食分类器应用程序制作一个美食已经够棘手了。想象一下要为存在的每种食物做到这一点——并且在两个平台上都要做到!一个个人或一个小团队将很快遇到尝试解决这个问题的扩展问题。在本章中,我们以此示例作为动力,探索我们在第十一章中探讨的移动 AI 开发生命周期的不同部分。
我们在这里探讨的材料不需要局限于智能手机。我们可以将我们的学习应用到移动以外的边缘设备,如 Google Coral 和 Raspberry Pi,这些我们在本书后面讨论。
食品分类器应用程序的生命周期
因此,我们希望构建一个全球多美食、多平台的食品分类器。听起来是一项艰巨的任务,但我们可以将其分解为可管理的步骤。就像生活中一样,我们首先需要爬行,然后才能行走,最后才能奔跑。以下是一个可能要考虑的潜在方法:
-
为单一美食(例如意大利美食)收集一小组初始图像。
-
用相应的菜品标识符(例如
margherita_pizza)标记这些图像。 -
训练分类器模型。
-
将模型转换为移动框架兼容格式(例如.tflite)。
-
通过将模型与出色的用户体验集成来构建移动应用程序。
-
招募α用户并与他们分享应用程序。
-
收集详细的使用指标以及来自活跃用户的反馈,包括相机帧(倾向于反映真实世界的使用)和相应的代理标签(指示分类是正确还是错误)。
-
使用新收集的图像作为额外的训练数据来改进模型。这个过程需要迭代。
-
当模型的质量达到最低质量标准时,将应用程序/功能发布给更多/所有用户。继续监控和改进该美食的模型质量。
-
为每种美食重复这些步骤。
提示
对于第 7 步,我们可以选择在用户体验中集成反馈。例如,应用程序可以显示一个按概率排名的预测列表,这些预测是给定图片的可能候选项。如果我们的模型表现良好,用户应该大部分时间选择第一个选项。选择排名较低的预测基本上被视为错误的预测。在最坏的情况下,如果没有一个选项是正确的,允许用户手动添加一个新标签。这张照片,连同标签(在所有三种情况下),可以作为训练数据纳入。
我们不需要大量数据就可以开始。尽管上述每个步骤听起来可能有些复杂,但我们可以显著地自动化这个过程。这种方法的酷之处在于,应用程序被使用得越多,它就变得越好,自动地。就好像它有自己的生命一样。我们在本章末尾探讨了这种自我进化的方法。
提示
您的应用/公司的铁杆粉丝将成为一个好的α用户。α用户理想情况下是那些对您的产品成功有着切身利益的人。对于一个食物识别应用程序,潜在的用户群可能是那些关注每一卡路里和食材的健身爱好者。这些用户明白应用程序的质量在一开始可能不尽如人意,但他们也看到了通过持续、建设性的反馈来塑造它的角色。他们自愿同意签署一个自由的数据共享协议,以提供诸如使用度量和日常使用中的图像帧等数据。我们建议您的用户清楚地知道您将收集关于他们的哪些信息,并允许他们选择退出或删除。不要显得可怕!
在本章中,我们探讨了上述生命周期的不同部分以及帮助我们完成每个步骤的工具。最后,我们将全面、端到端地查看整个移动开发生命周期,不仅仅是从本章开始,还包括之前的章节,并结合它们看看我们如何有效地在构建一个生产质量的真实应用程序中使用它们。
我们的旅程始于理解谷歌生态系统中以下工具。
TensorFlow Lite
模型转换和移动推理引擎。
ML Kit
高级软件开发工具包(SDK),具有几个内置 API,以及运行自定义 TensorFlow Lite 模型的能力,以及与 Firebase 在谷歌云上的集成。
Firebase
一个基于云的框架,为生产质量的移动应用程序提供必要的基础设施,包括分析、崩溃报告、A/B 测试、推送通知等。
TensorFlow 模型优化工具包
用于优化模型大小和性能的一组工具。
TensorFlow Lite 概述
正如第十一章中提到的,谷歌发布了一个名为 TensorFlow Lite 的设备端推理引擎,将 TensorFlow 生态系统的覆盖范围扩展到了云端和桌面之外。在此之前,TensorFlow 生态系统中的选项是将整个 TensorFlow 库移植到 iOS(这样会很沉重和缓慢),后来是其略微简化的版本 TensorFlow Mobile(有所改进,但仍然相当庞大)。
TensorFlow Lite 从头开始针对移动端进行了优化,具有以下显著特点:
小
TensorFlow Lite 附带了一个更轻量级的解释器。即使包含了所有操作符,解释器的大小也不到 300KB。在使用常见模型如 MobileNet 时,我们可以预期该占用空间小于 200KB。作为参考,上一代的 TensorFlow Mobile 曾占用 1.5MB。此外,TensorFlow Lite 使用了选择性注册——它只打包模型将使用的操作,最小化不必要的开销。
快速
TensorFlow Lite 提供了显著的加速,因为它能够利用设备上的硬件加速,如 GPU 和 NPU(如果可用)。在 Android 生态系统中,它使用 Android 神经网络 API 进行加速。类似地,在 iPhone 上,它使用 Metal API。谷歌声称在使用 GPU 时(相对于 CPU),在一系列任务中可以实现两到七倍的加速。
TensorFlow 使用 Protocol Buffers(Protobufs)进行反序列化/序列化。Protobufs 是一种表示数据的强大工具,由于其灵活性和可扩展性,但这会在低功耗设备(如移动设备)上产生性能成本。
FlatBuffers证明是解决这个问题的答案。最初为视频游戏开发构建,低开销和高性能是必须的,它们也被证明是移动设备的一个很好的解决方案,显著减少了代码占用空间、内存使用和用于模型序列化和反序列化的 CPU 周期。这也大大提高了启动时间。
在网络中,有一些层在推断时具有固定的计算;例如,批量归一化层,可以预先计算,因为它们依赖于训练期间获得的值,如均值和标准差。因此,批量归一化层的计算可以与前一层的计算提前融合(即,在模型转换期间),从而减少推断时间,使整个模型更快。这被称为预融合激活,TensorFlow Lite 支持。
解释器使用静态内存和静态执行计划。这有助于减少模型加载时间。
较少的依赖项
TensorFlow Lite 的代码库主要是标准的 C/C++,依赖项很少。这使得打包和部署更容易,同时还减小了部署包的大小。
支持自定义操作符
TensorFlow Lite 包含量化和浮点核心操作符,其中许多已经针对移动平台进行了调整,可以用于创建和运行自定义模型。如果 TensorFlow Lite 不支持我们模型中的某个操作,我们也可以编写自定义操作符来使我们的模型运行起来。
在构建我们的初始 Android 应用程序之前,检查 TensorFlow Lite 的架构会很有用。
TensorFlow Lite 架构
图 13-2 提供了 TensorFlow Lite 架构的高级视图。

图 13-2. TensorFlow Lite 生态系统的高级架构
作为应用开发者,我们将在顶层层中与 TensorFlow Lite API(或者选择与 ML Kit 交互,后者又使用 TensorFlow Lite)进行交互。TensorFlow Lite API 将所有使用较低级 API(如 Android 的神经网络 API)时涉及的复杂性抽象化。请记住,这类似于 Core ML 在 Apple 生态系统中的工作方式。
从另一个极端来看,计算可以在各种类型的硬件模块上运行。其中最常见的是 CPU,仅仅因为其普遍性和灵活性。现代智能手机越来越配备了专门的模块,包括 GPU 和新的 NPU(专门用于神经网络计算,如 iPhone X 上的)。此外,数字信号处理器(DSP)专门用于单一任务,如面部认证、指纹认证和唤醒词检测(如“嘿 Siri”)。
在物联网(IoT)世界中,微控制器(MCU)占主导地位。没有操作系统,没有处理器,内存很少(几 KB),这些产品在大量生产中成本低廉,并且易于整合到各种应用程序中。使用 TensorFlow Lite for Microcontrollers,开发人员可以在这些裸机设备上运行人工智能,而无需互联网连接。针对 MCU 的 TensorFlow Lite 解释器的精简版本(大约 20 KB)称为 TensorFlow Lite Micro Interpreter。
那么 TensorFlow Lite 如何与硬件交互呢?通过使用代理,这些代理是平台感知对象,公开一致的平台无关 API。换句话说,代理屏蔽了解释器对其运行的具体硬件的任何了解。它们承担了图执行的全部或部分责任,否则将在 CPU 上运行,而现在则在效率更高的 GPU 和 NPU 上运行。在 Android 上,GPU 代理使用 OpenGL 加速性能,而在 iOS 上则使用 Metal API。
鉴于 TensorFlow Lite 本身是平台无关的,它需要调用一个实现已知合同的特定平台库。这个合同是 TensorFlow Lite Delegate API。在 Android 中,这个合同由 Android 神经网络 API 来实现(适用于运行 Android 8.1 及以上版本的设备)。神经网络 API 旨在为更高级别的机器学习框架提供基础功能层。在苹果世界中,相当于神经网络 API 的是 Metal Performance Shaders。
根据我们目前所了解的信息,让我们动手操作。
模型转换为 TensorFlow Lite
在本书的这一部分,我们应该已经有一个模型(在 ImageNet 上预训练或在 Keras 中自定义训练)。在我们可以将该模型插入到 Android 应用程序之前,我们需要将其转换为 TensorFlow Lite 格式(一个.tflite文件)。
让我们看看如何使用 TensorFlow Lite Converter 工具转换模型,这个tflite_convert命令与我们的 TensorFlow 安装捆绑在一起:
# Keras to TensorFlow Lite
$ tflite_convert \
--output_file=my_model.tflite \
--keras_model_file=my_model.h5
# TensorFlow to TensorFlow Lite
$ tflite_convert \
--output_file=my_model.tflite \
--graph_def_file=my_model/frozen_graph.pb
这个命令的输出是新的my_model.tflite文件,然后我们可以将其插入到下一节中的 Android 应用程序中。稍后,我们将看看如何使用tflite_convert工具使该模型更具性能。此外,TensorFlow Lite 团队已经创建了许多预训练模型,这些模型以 TensorFlow Lite 格式可用,省去了这一转换步骤。
构建实时物体识别应用程序
从 TensorFlow 存储库中运行示例应用程序是玩转 TensorFlow Lite API 的简单方法。请注意,我们需要一部 Android 手机或平板电脑来运行该应用程序。以下是构建和部署应用程序的步骤:
-
克隆 TensorFlow 存储库:
git clone https://github.com/tensorflow/tensorflow.git -
从https://developer.android.com/studio下载并安装 Android Studio。
-
打开 Android Studio,然后选择“打开现有的 Android Studio 项目”(图 13-3)。
![Android Studio 的启动界面]()
图 13-3. Android Studio 的启动界面
-
转到克隆的 TensorFlow 存储库的位置,然后进一步导航至tensorflow/tensorflow/contrib/lite/java/demo/(图 13-4)。选择打开。
![TensorFlow 存储库中 Android Studio“打开现有项目”屏幕]()
图 13-4. TensorFlow 存储库中 Android Studio“打开现有项目”屏幕
-
在 Android 设备上,启用开发者选项。(请注意,我们在这里使用的是 Pixel 设备,它使用原生 Android 操作系统。对于其他制造商,说明可能会有所不同。)
-
转到设置。
-
向下滚动到“关于手机”或“关于平板电脑”选项(图 13-5),然后选择它。
![Android 手机上的系统信息屏幕;在此处选择“关于手机”选项]()
图 13-5。Android 手机上的系统信息屏幕;在此处选择“关于手机”选项
-
查找“构建号”行并点击七次。(是的,你没看错——七次!)
-
您应该看到一条消息(图 13-6),确认开发者模式已启用。
![Android 设备上的“关于手机”屏幕]()
图 13-6。Android 设备上的“关于手机”屏幕
-
如果您正在使用手机,请点击返回按钮返回到上一个菜单。
-
您应该看到一个“开发者选项”按钮,直接位于“关于手机”或“关于平板电脑”选项的上方(图 13-7)。点击此按钮以显示“开发者选项”菜单(图 13-8)。
![显示“开发者选项”已启用的系统信息屏幕]()
图 13-7。显示“开发者选项”已启用的系统信息屏幕
![启用 USB 调试的 Android 设备上的“开发者选项”屏幕]()
图 13-8。启用 USB 调试的 Android 设备上的“开发者选项”屏幕
-
-
通过 USB 电缆将 Android 设备连接到计算机。
-
Android 设备可能会显示一条消息,要求允许 USB 调试。启用“始终允许此计算机”,然后选择确定(图 13-9)。
![在显示的警报上允许 USB 调试]()
图 13-9。在显示的警报上允许 USB 调试
-
在 Android Studio 中,在调试工具栏上(图 13-10),点击“运行应用”按钮(右向三角形)。
![Android Studio 中的调试工具栏]()
图 13-10。Android Studio 中的调试工具栏
-
一个窗口打开,显示所有可用的设备和模拟器(图 13-11)。选择您的设备,然后选择确定。
![从部署目标选择屏幕中选择手机]()
图 13-11。从部署目标选择屏幕中选择手机
-
应用程序应该安装并开始在我们的手机上运行。
-
应用程序将请求您的相机权限;请继续授予权限。
-
相机的实时视图应该出现,以及实时对象分类预测,以及进行预测所需的秒数,如图 13-12 所示。
![应用程序正在运行,显示实时预测]()
图 13-12。应用程序正在运行,显示实时预测
就是这样!我们在手机上运行了一个基本的应用程序,它可以拍摄视频帧并对其进行分类。它简单而且运行得相当不错。
除了对象分类之外,TensorFlow Lite 存储库还有许多其他 AI 问题的示例应用程序(iOS 和 Android),包括以下内容:
-
对象检测
-
姿势估计
-
手势识别
-
语音识别
拥有这些示例应用程序的好处是,只要有基本的说明,没有移动开发背景的人就可以在手机上运行它们。更好的是,如果我们有一个自定义训练的模型,我们可以将其插入应用程序并看到它为我们的自定义任务运行。
这对于刚开始很棒。然而,在现实世界中情况要复杂得多。拥有成千上万甚至数百万用户的严肃现实世界应用程序的开发者需要考虑超越推断的问题,比如更新和分发模型,在用户子集中测试不同版本,保持 iOS 和 Android 之间的一致性,最终降低每个工程成本。在内部完成所有这些工作可能会昂贵、耗时,而且实际上是不必要的。自然而然,提供这些功能的平台会很吸引人。这就是 ML Kit 和 Firebase 的作用所在。
ML Kit + Firebase
ML Kit 是在 2018 年 Google I/O 大会上推出的移动 SDK。它为初学者和高级 ML 开发人员提供了方便的 API,用于执行许多常见的 ML 任务。默认情况下,ML Kit 具有视觉和语言智能的通用功能集。表 4-1 列出了我们可以在几行代码中完成的一些常见 ML 任务。
表 13-1. ML Kit 内置功能
| 视觉 | 语言 |
|---|---|
| 对象分类对象检测和跟踪流行的地标检测文本识别人脸检测条形码检测 | 语言识别设备上翻译智能回复 |
ML Kit 还使我们能够使用自定义训练的 TensorFlow Lite 模型进行推断。让我们花点时间来欣赏为什么这对开发者如此重要。想象一下,我们正在构建一个名片扫描器。我们可以引入一个自定义的名片检测模型,识别出名片可见时及其边界(以创建一个漂亮的视觉用户界面),运行内置的文本识别,并过滤掉边界之外的文本,以防止多余的字符。或者,考虑一个可以通过指向物体构建的语言学习游戏,运行一个物体分类器,然后使用设备上的翻译 API 来宣布标签的法语。使用 ML Kit 完全可以相对快速地构建这些功能。虽然许多这些功能也在 Core ML 中可用,但 ML Kit 具有跨平台的额外优势。
然而,ML Kit 只是谜题的一部分。它集成到 Google 的 Firebase 中,这是 Google Cloud 的一部分,是移动和 Web 应用程序开发平台。Firebase 提供了一系列功能,这些功能是生产质量应用程序所必需的基础设施,例如以下功能:
-
推送通知
-
认证
-
崩溃报告
-
日志
-
性能监控
-
托管设备测试
-
A/B 测试
-
模型管理
最后一点对我们非常重要。Firebase 给我们带来的最大好处之一是能够在云端托管我们的自定义模型,并根据需要在应用程序中下载它们。只需将模型复制到 Google Cloud 上的 Firebase,引用应用程序内的 ML Kit 模型,然后我们就可以开始了。A/B 测试功能使我们能够向不同用户展示同一模型的不同版本,并测量不同模型之间的性能。
注意
对于许多内置功能,ML Kit 还提供了一个在云端处理图像的功能,其中模型比设备上的模型要大得多。这些更大的模型显然需要更强大的硬件来运行它们,但可以提供一些准确性的改进,可能有更大的分类法(比如成千上万的对象类别而不是数百个)。事实上,一些功能,比如地标识别功能,只能在云端上运行。
云处理选项在我们需要一点额外准确性和/或用户手机处理能力不足以很好地运行设备上模型时特别有用。
ML Kit 中的对象分类
对于我们之前的实时对象分类任务,如果我们使用 ML Kit 而不是原始的 TensorFlow Lite,我们可以简化我们的代码到以下几行(使用 Kotlin):
val image = FirebaseVisionImage.fromBitmap(bitmap)
val detector = FirebaseVision.getInstance().visionLabelDetector
val result = detector.detectInImage(image).addOnSuccessListener { labels ->
// Print labels
}
ML Kit 中的自定义模型
除了 ML Kit 提供的预构建模型外,我们还可以运行自己的自定义模型。这些模型必须是 TensorFlow Lite 格式。以下是一个简单的代码片段,用于加载打包到应用中的自定义模型:
val customModel = FirebaseLocalModelSource.Builder("my_custom_model")
.setAssetFilePath("my_custom_model.tflite").build()
FirebaseModelManager.getInstance().registerLocalModelSource(customModel)
接下来,我们指定模型的输入和输出配置(对于一个接收尺寸为 224x224 的 RGB 图像并为 1,000 个类别名称提供预测的模型):
val IMAGE_WIDTH = 224
val IMAGE_HEIGHT = 224
val modelConfig = FirebaseModelInputOutputOptions.Builder()
.setInputFormat(0, FirebaseModelDataType.FLOAT32, intArrayOf(1,
IMAGE_WIDTH, IMAGE_HEIGHT, 3))
.setOutputFormat(0, FirebaseModelDataType.FLOAT32, intArrayOf(1, 1000))
.build()
接下来,我们创建一个单个图像数组,并将每个像素归一化到范围[-1,1]:
val bitmap = Bitmap.createScaledBitmap(image, IMAGE_WIDTH, IMAGE_HEIGHT, true)
val input = Array(1) {
Array(IMAGE_WIDTH) { Array(IMAGE_HEIGHT) { FloatArray(3) } }
}
for (x in 0..IMAGE_WIDTH) {
for (y in 0..IMAGE_HEIGHT) {
val pixel = bitmap.getPixel(x, y)
input[0][x][y][0] = (Color.red(pixel) - 127) / 128.0f
input[0][x][y][1] = (Color.green(pixel) - 127) / 128.0f
input[0][x][y][2] = (Color.blue(pixel) - 127) / 128.0f
}
}
现在,我们基于我们的自定义模型设置一个解释器:
val options = FirebaseModelOptions.Builder()
.setLocalModelName("my_custom_model").build()
val interpreter = FirebaseModelInterpreter.getInstance(options)
接下来,我们在解释器上运行我们的输入批处理:
val modelInputs = FirebaseModelInputs.Builder().add(input).build()
interpreter.run(modelInputs, modelConfig).addOnSuccessListener { result ->
// Print results
}
是的,就是这么简单!在这里,我们看到了如何将自定义模型与应用捆绑在一起。有时,我们可能希望应用动态从云端下载模型,原因如下:
-
我们希望在 Play 商店上保持默认应用大小较小,以免阻止有数据使用限制的用户下载我们的应用。
-
我们希望尝试不同种类的模型,并根据可用的指标选择最佳模型。
-
我们希望用户拥有最新和最好的模型,而无需经历整个应用发布流程。
-
需要模型的功能可能是可选的,我们希望节省用户设备上的空间。
这带我们来到了托管模型。
托管模型
ML Kit 与 Firebase 一起,使我们能够在 Google Cloud 上上传和存储我们的模型,并在需要时从应用中下载。模型下载后,其功能与将模型捆绑到应用中完全相同。此外,它还为我们提供了推送模型更新的能力,而无需对应用进行整体发布。此外,它还让我们可以在真实世界中对我们的模型进行实验,以查看哪些模型在实际中表现最佳。对于托管模型,我们需要看两个方面。
访问托管模型
以下行通知 Firebase 我们想使用名为my_remote_custom_model的模型:
val remoteModel = FirebaseCloudModelSource.Builder("my_remote_custom_model")
.enableModelUpdates(true).build()
FirebaseModelManager.getInstance().registerCloudModelSource(remoteModel)
请注意,我们将enableModelUpdates设置为使我们能够从云端向设备推送模型更新。我们还可以选择配置模型首次下载的条件与每次下载的条件——设备是否空闲,当前是否正在充电,下载是否仅限于 WiFi 网络等。
接下来,我们设置一个解释器,就像我们在本地模型中所做的那样:
val options = FirebaseModelOptions.Builder()
.setCloudModelName("my_remote_custom_model").build()
val interpreter = FirebaseModelInterpreter.getInstance(options)
在此之后,执行预测的代码看起来与本地模型的代码完全相同。
接下来,我们讨论托管模型的另一个方面——上传模型。
上传托管模型
截至撰写本文时,Firebase 仅支持托管在 GCP 上的模型。在本节中,我们将介绍创建、上传和存储托管模型的简单过程。本小节假设我们已经拥有现有的 GCP 帐户。
以下列出了我们需要采取的步骤来将模型托管在云端:
-
前往https://console.firebase.google.com。选择一个现有项目或添加一个新项目(图 13-13)。
![Google Cloud Firebase 的主页]()
图 13-13。Google Cloud Firebase 的主页
-
在项目概述屏幕上,创建一个 Android 应用(图 13-14)。
![Google Cloud Firebase 上的项目概述屏幕]()
图 13-14。Google Cloud Firebase 上的项目概述屏幕
-
在 Android Studio 中使用项目的应用 ID(图 13-15)。
![Firebase 上的应用创建屏幕]()
图 13-15。Firebase 上的应用创建屏幕
-
点击“注册应用程序”后,下载配置文件。这个配置文件为应用程序提供了访问我们云账户所需的凭据。按照应用程序创建页面上显示的方式将配置文件和 Firebase SDK 添加到 Android 应用程序中。
-
在 ML Kit 部分,选择开始,然后选择“添加自定义模型”(图 13-16)。
![ML Kit 自定义模型选项卡]()
图 13-16。ML Kit 自定义模型选项卡
-
在名称字段中,输入
my_remote_custom_model以匹配代码中的名称。 -
从计算机上传模型文件(图 13-17)。
![将 TensorFlow Lite 模型文件上传到 Firebase]()
图 13-17。将 TensorFlow Lite 模型文件上传到 Firebase
-
文件上传完成后,点击“发布”按钮。
就是这样!我们的模型现在已经准备好从应用程序动态访问和使用。接下来,我们将探讨如何使用 Firebase 在模型之间进行 A/B 测试。
A/B 测试托管模型
让我们假设我们有一个名为my_model_v1的版本 1 模型,最初部署给我们的用户。在用户使用一段时间后,我们获得了更多数据可以进行训练。这次训练的结果是my_model_v2(图 13-18)。我们想评估这个新版本是否会给我们带来更好的结果。这就是 A/B 测试的用武之地。

图 13-18。当前上传的自定义模型到 Firebase
A/B 测试被行业广泛使用,是一种统计假设检验技术,回答了“B 是否比 A 更好?”的问题。这里的 A 和 B 可以是同类的任何东西:网站上的内容、手机应用程序上的设计元素,甚至是深度学习模型。在积极开发模型并发现用户对模型不同迭代的反应时,A/B 测试是一个非常有用的功能。
用户已经使用my_model_v1一段时间了,我们想看看 v2 版本是否让我们的用户疯狂。我们想慢慢开始;也许只有 10%的用户应该得到 v2。为此,我们可以设置一个 A/B 测试实验如下:
-
在 Firebase 中,点击 A/B 测试部分,然后选择“创建实验”(图 13-19)。
![在 Firebase 中进行 A/B 测试的屏幕,我们可以创建一个实验]()
图 13-19。在 Firebase 中进行 A/B 测试的屏幕,我们可以创建一个实验
-
选择远程配置选项。
-
在基础部分中,在“实验名称”框中输入实验名称和可选描述(图 13-20),然后点击下一步。
![创建远程配置实验屏幕的基础部分]()
图 13-20。创建远程配置实验屏幕的基础部分
-
在打开的定位部分中,从“目标用户”下拉菜单中选择我们的应用程序,并输入目标用户的百分比(图 13-21)。
![远程配置屏幕的定位部分]()
图 13-21。远程配置屏幕的定位部分
-
选择一个有意义的目标指标。我们将在下一节中更详细地讨论这个问题。
-
在变体部分(图 13-22),创建一个名为
model_name的新参数,反映了特定用户将使用的模型的名称。对照组使用默认模型,即my_model_v1。我们还创建了一个名为my_model_v2的额外变体,分配给 10%的用户。![远程配置屏幕的变体部分]()
图 13-22。远程配置屏幕的变体部分
-
选择“审查”,然后选择“开始实验”。随着时间的推移,我们可以增加使用变体的用户分布。
哒哒!现在我们的实验已经开始运行。
测量实验效果
接下来我们该怎么做?我们希望给我们的实验一些时间来看它的表现。根据实验的类型,我们可能需要给它几天到几周的时间。实验的成功可以通过任意数量的标准来确定。Google 提供了一些开箱即用的指标,我们可以使用,如图 13-23 所示。

图 13-23。设置 A/B 测试实验时可用的分析
假设我们想要最大化我们估计的总收入——毕竟,我们都想像 Jian-Yang 一样变得富有。我们将衡量那些打开实验的用户与基线(即未打开实验的用户)的收入。如果我们的每位用户的收入相对于基线增加,我们将认为实验是成功的。相反,如果每位用户的收入没有增加/减少,我们将得出相反的结论。对于成功的实验,我们希望逐步将其推广到所有用户。在那一点上,它不再是一个实验,而是“毕业”成为核心产品。
在代码中使用实验
现在我们已经设置了一个实验,让我们看看如何在我们的应用程序中使用代码包含它。要在代码中使用适当的模型,我们只需访问远程配置对象(remoteConfig)并从中获取模型名称。我们从远程配置对象获取的模型名称将取决于用户是否包含在实验中。以下代码行实现了这一点:
val remoteConfig = FirebaseRemoteConfig.getInstance()
remoteConfig.fetch()
val modelName = remoteConfig.getString("current_best_model")
val remoteModel = FirebaseCloudModelSource.Builder(modelName)
.enableModelUpdates(true).build()
FirebaseModelManager.getInstance().registerCloudModelSource(remoteModel)
执行预测的其余代码与前几节完全相同。我们的应用现在已准备好使用我们实验规定的正确模型。
iOS 上的 TensorFlow Lite
前几章展示了在 iOS 上使用苹果的 Core ML 有多么容易。我们只需将模型拖放到 Xcode 中,然后只需几行代码就可以开始进行推理。相比之下,即使是查看 TensorFlow Lite 存储库中的基本 iOS 示例,也可以明显看出,需要大量样板代码才能使最基本的应用程序运行。相比之下,与 ML Kit 一起使用的 TensorFlow Lite 是一种相当愉快的体验。除了能够使用干净简洁的 API 外,我们还可以获得本章前面详细介绍的所有功能——远程下载和更新模型、模型 A/B 测试以及云回退处理。所有这些都不需要做太多额外的工作。编写同时面向 iOS 和 Android 的深度学习应用程序的开发人员可能会考虑使用 ML Kit 作为“一次构建,随处使用”的方式。
性能优化
在第六章中,我们从理论角度探讨了量化和剪枝。让我们从 TensorFlow Lite 的角度以及实现它们的工具中近距离地看看它们。
使用 TensorFlow Lite 转换器进行量化
对于 iOS,苹果在其 Core ML 工具包中提供了quantization_utils。而对于 TensorFlow Lite,等效的是已经内置的tflite_convert工具,我们在本章前面使用过。在命令行中,我们可以指定输入文件、模型图、要转换为的数据类型以及输入和输出的名称(可以使用 Netron 进行检查,如第十一章所示)。从 32 位转换为 8 位整数表示意味着模型大小减小四倍,而准确性损失相对较小。
$ tflite_convert \
--output_file=quantized-model.tflite \
--graph_def_file=/tmp/some-graph.pb \
--inference_type=QUANTIZED_UINT8 \
--input_arrays=input \
--output_arrays=MobilenetV1/Predictions/Reshape_1 \
--mean_values=128 \
--std_dev_values=127
完成后,此命令应该给我们提供quantized-model.tflite模型。
TensorFlow 模型优化工具包
TensorFlow Lite 转换器是获得我们的模型量化的最简单方法。值得注意的是,转换器在训练后对模型进行量化。由于表示能力的降低,可能会出现轻微但明显的准确率损失。我们能做得更好吗?量化感知训练,顾名思义,在训练期间考虑量化的影响,并试图补偿和减少后续训练中可能发生的损失。
尽管两种量化形式都使模型大小减少了 75%,但实验结果显示如下:
-
对于 MobileNetV2,与训练后量化导致的准确率下降 8 个点相比,量化感知训练仅导致了 1 个点的下降。
-
对于 InceptionV3,与训练后量化相比,量化感知训练使延迟减少了惊人的 52%,而后者仅减少了 25%。
注意
值得注意的是,这些准确率指标是在 1,000 类 ImageNet 测试集上的。大多数问题在类别较少时具有较低的复杂性。对于这种更简单的问题,训练后量化应该导致更小的损失。
量化感知训练可以使用 TensorFlow Model Optimization Toolkit 实现。该工具包还提供了一系列用于模型压缩的工具,包括修剪。此外,TensorFlow Lite 模型仓库已经提供了使用这种技术的预量化模型。表 13-2 列出了各种量化策略的效果。
表 13-2. 不同量化策略(8 位)对模型的影响(来源:TensorFlow Lite 模型优化文档)
| 模型 | MobileNet | MobileNetV2 | InceptionV3 |
|---|---|---|---|
| Top-1 准确率 | 原始 | 0.709 | 0.719 |
| 训练后量化 | 0.657 | 0.637 | 0.772 |
| 量化感知训练 | 0.7 | 0.709 | 0.775 |
| 延迟(毫秒) | 原始 | 124 | 89 |
| 训练后量化 | 112 | 98 | 845 |
| 量化感知训练 | 64 | 54 | 543 |
| 大小(MB) | 原始 | 16.9 | 14 |
| 优化后 | 4.3 | 3.6 | 23.9 |
Fritz
正如我们迄今所见,Core ML 和 TensorFlow Lite 的主要目的是为了提供快速的移动推断。有一个模型,将其插入应用程序中,并运行推断。然后出现了 ML Kit,除了具有内置的 AI 功能外,还使得部署模型和监视我们的自定义模型(使用 Firebase)变得更加容易。回顾整个流程,我们有训练、转换为移动格式、优化速度、部署到用户、监视性能、跟踪模型版本。这些是分布在许多工具中的几个步骤。很可能没有一个人拥有整个流程。这是因为通常这些工具需要一定程度的熟悉度(例如,将模型部署给用户可能超出数据科学家的舒适区)。总部位于波士顿的初创公司 Fritz 正试图消除这些障碍,使数据科学家和移动开发人员更加简单地完成从模型开发到部署的整个周期。
Fritz 提供了一个移动 AI 开发的端到端解决方案,包括以下值得注意的功能:
-
能够在训练完成后使用回调函数直接将模型部署到用户设备上。
-
能够在计算机上直接对模型进行基准测试,而无需将其部署到手机上。以下代码演示了这一点:
$ fritz model benchmark <path to keras model.h5> ... ------------------------ Fritz Model Grade Report ------------------------ Core ML Compatible: True Predicted Runtime (iPhone X): 31.4 ms (31.9 fps) Total MFLOPS: 686.90 Total Parameters: 1,258,580 Fritz Version ID: <Version UID> -
能够加密模型,以便我们的知识产权不被恶意行为者从设备上窃取。
-
能够用几行代码实现许多先进的计算机视觉算法。SDK 带有这些算法的移动友好实现,可以以高帧率运行。例如,图像分割、风格转移、对象检测和姿势估计。图 13-24 显示了在各种 iOS 设备上运行的对象检测的基准。
let poseModel = FritzVisionPoseModel() guard let poseResult = try? poseModel.predict(image) else { return } let imageWithPose = poseResult.drawPose() //` `O``v``e``r``l``a``y``s` `p``o``s``e` `o``n` `i``n``p``u``t``.![Fritz SDK 在不同移动设备上的对象检测功能性能,相对于 iPhone X]()
图 13-24。Fritz SDK 在不同移动设备上的对象检测功能性能,相对于 iPhone X
-
能够通过重新训练使用他们的 Jupyter 笔记本自定义预构建模型到自定义数据集。值得注意的是,这可能是一个困难的问题(即使对于专业数据科学家来说),但因为开发人员只需确保数据格式正确,这个问题就得到了很大的简化。
-
能够从命令行管理所有模型版本。
-
能够使用 Fritz 的开源应用 Heartbeat 测试我们模型的移动准备性(也可在 iOS/Android 应用商店中找到)。假设我们已经准备好一个模型,但对移动端不太了解,我们可以克隆该应用,用我们自己的模型替换现有模型,并在手机上运行。
-
一个充满活力的社区,关于移动 AI 最新信息的博客在heartbeat.fritz.ai上。
移动 AI 应用开发周期的整体视角
到目前为止,在本书中,我们已经看过许多技术和技术,使我们能够在移动 AI 开发周期中执行各种任务。在这里,我们通过探索贯穿整个生命周期的问题类型,将所有内容联系在一起。图 13-25 提供了开发周期的广泛概述。

图 13-25。移动 AI 应用开发生命周期
如何收集初始数据?
我们可以采用几种不同的策略来实现这一点:
-
找到感兴趣的对象,并手动拍摄不同角度、光照、环境、构图等照片。
-
使用浏览器扩展程序(如 Fatkun)从互联网上抓取数据(第十二章)。
-
查找现有数据集(Google 数据集搜索)。例如,Food-101 用于菜肴。
-
合成自己的数据集。
-
将对象(前景)放在绿幕(背景)前拍摄照片。将背景替换为随机图像以合成大型数据集,同时进行缩放、裁剪和旋转,以创建数百张图像。
-
如果无法使用绿幕,从现有的真实世界图像中分割(即切割)出对象并重复上一步骤,以构建一个强大、多样化的数据集。需要注意的是,在(a)和(b)点中,前景需要有足够的多样性;否则,网络可能会过度学习一个例子,而不是理解对象。
-
找到感兴趣对象的真实 3D 模型,并将其放置在使用 Unity 等 3D 框架的真实环境中。调整光照和相机位置、缩放和旋转,从多个角度拍摄这个对象的快照。我们在第七章中探讨了 AI.Reverie 和 CVEDIA 等在这一领域工作的公司。我们使用逼真的模拟器来为自动驾驶章节(第十六章和第十七章)训练模型。
-
如何为我的数据标记标签?
在前一个答案中的大多数步骤应该已经为您提供了数据的标签。对于未标记的集合,请使用 Supervisely、Labelbox 和 Diffgram 等标记工具。对于真正大型的数据集,自己注释数据可能不可行,提供收入机会给弱势群体的社会责任标记服务,如 Digital Data Divide、iMerit 和 Samasource,可能是一个不错的选择。
如何训练我的模型?
训练模型有两种广泛的方法:
-
使用代码:使用 Keras 和 TensorFlow。
-
无需代码:使用自定义分类器服务,如 Google 的 Auto ML、Microsoft 的 CustomVision.ai 和 Clarifai,或仅适用于 Apple 生态系统的 Create ML。
如何将模型转换为移动友好格式?
以下是将模型转换为移动兼容格式的几种不同方法:
-
使用 Core ML Tools(仅适用于 Apple)。
-
对于 iOS 和 Android,请使用 TensorFlow Lite Converter。
-
或者,使用 Fritz 进行端到端的流程。
如何使我的模型性能良好?
以下是一些使模型性能良好的技术:
-
从 MobileNet 系列等高效模型开始,甚至更好的是 EfficientNet。
-
通过量化和修剪模型来减小模型的大小,以改善加载和推断时间,同时保持模型准确性相对完好。预计大小会减小高达 75%,准确性损失很小。
-
对于 Apple 生态系统,请使用 Core ML Tools,对于 iOS 和 Android,请使用 TensorFlow 模型优化工具包。
如何为用户构建出色的用户体验?
显然,这将取决于您试图解决的问题的类型。但一个一般的指导原则是在互动性、性能和资源使用(内存、CPU、电池等)之间取得适当的平衡。当然,一个智能的反馈机制可以实现轻松的数据收集和反馈。将这种体验变成游戏化将使其达到一个全新的水平。
在食品分类器应用程序的情况下,用户拍照后,我们的用户界面将显示给定照片的前五个候选预测列表(按置信度降序排列)。例如,如果用户拍摄了一张披萨的照片,屏幕上显示的候选预测可能是“馅饼 - 75%”,“披萨 - 15%”,“烤饼 - 6%”,“面包 - 3%”,“砂锅 - 1%”。在这种情况下,用户将选择第二个预测。对于完美的模型,用户将始终选择第一个预测。由于我们的模型并不完美,用户选择的预测的排名将成为未来改进模型的信号。在最坏的情况下,如果没有一个预测是正确的,用户应该有一种手动标记数据的方法。在手动输入时提供自动建议功能将有助于保持数据集中的干净标签。
一个更好的体验可能是用户永远不需要拍照。相反,预测是实时可用的。
如何让模型对用户可用?
以下是将模型部署给用户的一些方法:
-
将模型捆绑到应用程序二进制文件中,并在应用商店发布。
-
或者,将模型托管在云中,并在需要时让应用程序下载模型。
-
使用 Fritz 或 Firebase 等模型管理服务(与 ML Kit 集成)。
如何衡量模型的成功?
第一步是确定成功标准。考虑以下示例:
-
我的模型应该在第 90 百分位数以下的时间内运行推断。
-
“使用这个模型的用户每天都会打开应用程序。”
-
在食品分类器应用程序中,一个成功的指标可能是“80%的用户选择了预测列表中的第一个预测”。
这些成功标准并非一成不变,应该随着时间的推移不断发展。数据驱动非常重要。当你有一个新的模型版本时,在一部分用户中运行 A/B 测试,并根据该版本评估成功标准,以确定它是否比上一个版本更好。
如何改进我的模型?
以下是改进我们模型质量的一些方法:
-
从用户那里收集关于个别预测的反馈:什么是正确的,更重要的是,什么是错误的。将这些图像与相应的标签一起作为下一个模型训练周期的输入。图 13-26 说明了这一点。
-
对于明确选择加入的用户,在预测置信度低时自动收集帧。手动标记这些帧并将其输入到下一个训练周期中。

图 13-26. 错误预测的反馈周期,生成更多训练数据,导致改进的模型
如何在用户手机上更新模型?
以下是如何在用户手机上更新模型的一些方法:
-
将新模型捆绑到下一个应用程序发布中。这是缓慢且不灵活的。
-
将新模型托管到云端,并强制应用程序在世界范围内下载新模型。最好在用户连接 WiFi 时执行此操作。
-
使用模型管理系统,如 Firebase(与 ML Kit 一起)或 Fritz 来自动化涉及的大部分繁重工作。
有了所有这些问题的答案,让我们欣赏这个应用程序如何自我改进的美丽。
自我进化模型
对于只需要成熟预训练模型的应用程序,我们的工作已经完成。只需将模型插入应用程序,我们就可以开始了。对于特别依赖稀缺训练数据的定制训练模型,我们可以让用户参与构建一个自我改进、不断发展的模型。
在最基本的层面上,每当用户使用应用程序时,他们都会提供必要的反馈(图像+标签)以进一步改进模型,如图 13-27 所示。

图 13-27. 自我进化模型周期
就像大学生必须在步入现实世界之前经历多年的毕业准备一样,模型在到达最终用户之前必须经历开发阶段。以下是软件发布阶段的流行模型,也是发布 AI 模型的有用指南。
- Dev
这是开发的初始阶段,在这个阶段,应用程序的开发人员是唯一的用户。数据积累缓慢,体验非常不稳定,模型预测可能相当不可靠。
- Alpha
在应用程序准备好由开发人员之外的少数用户测试之后,现在被认为处于 alpha 阶段。用户反馈在这里非常关键,因为它将有助于改善应用体验,并提供更大规模的数据给管道。这里的体验不像以前那样有 bug,模型预测也更可靠一些。在一些组织中,这个阶段也被称为dogfooding(即员工进行内部应用测试)。
- Beta
测试版应用的用户数量比α版要多得多。用户反馈在这里也非常重要。数据收集速度更大规模。许多不同的设备在现实世界中收集数据,以帮助快速改进模型。由于α用户的存在,体验更加稳定,模型预测更加可靠。测试版应用通常托管在苹果的 TestFlight(iOS)和谷歌的 Play Console(Android)上。第三方服务,如 HockeyApp 和 TestFairy,也很受欢迎,用于托管测试版程序。
- 产品
生产中的应用是稳定的,并且广泛可用。尽管数据以大量速率进入,但在这一点上,模型相当稳定,不会学习太多。然而,随着真实世界的使用不断增长,它能够在可能在前三个阶段之前没有见过的边缘情况上变得更好。当模型足够成熟时,可以对小版本改进进行α/β测试或在生产受众的子集上进行 A/B 测试。
尽管许多数据科学家在开始开发之前假设数据是可用的,但移动 AI 领域的人可能需要在一个小数据集上启动,并逐步改进他们的系统。
对于食物 Shazam 应用的这种自我进化系统,开发人员必须做出的最困难的选择应该是他们去哪些餐馆收集种子数据。
案例研究
让我们看一些有趣的例子,展示迄今为止学到的知识如何应用于行业中。
放手吧!
我们需要在这里承认。我们在本章一直在构建的应用已经存在。一个名为 Fit Now 的波士顿公司在一年前已经实现了 Erlich Bachman 的梦想。Lose It!(图 13-28)声称已经帮助了 3000 万用户减掉了 8500 万磅,通过跟踪他们所吃的食物。用户可以用手机的摄像头对准条形码、营养标签和他们即将吃的食物,以跟踪每一顿饭所摄入的卡路里和宏量元素。
公司首先使用基于云的算法实现了其食品扫描系统。由于资源需求和网络延迟,体验无法实时。为了改善体验,Fit Now 团队将其模型迁移到 TensorFlow Lite 以优化移动端,并使用 ML Kit 无缝部署到数百万用户。通过持续的反馈循环、模型更新和 A/B 测试,该应用基本上可以自我改进。

图 13-28。Lose It!中的 Snap It 功能显示了扫描食物项目的多个建议
Pixel 3 手机上的人像模式
将专业摄影师与业余爱好者区分开来的一个关键视觉概念是 bokeh,或者图像主题背后背景的模糊。相机距离主题越近,背景就越模糊。借助低* f *值的专业镜头(通常价格高达数千美元),可以产生壮观的模糊效果。
但如果你没有那种钱,人工智能可以帮助。谷歌的 Pixel 3 提供了一个“人像”模式,可以创建虚化效果。基本上,它使用 CNN 来估计场景中每个像素的深度,并确定哪些像素属于前景和背景。然后将背景像素模糊到不同的强度,以创建虚化效果,如图 13-29 所示。
深度估计是一个计算密集型任务,因此快速运行是至关重要的。具有 GPU 后端的 Tensorflow Lite 挺身而出,比 CPU 后端快大约 3.5 倍。
Google 通过训练一个专门用于深度估计的神经网络来实现这一点。它使用了一个带有多个手机摄像头的装置(被称为“Frankenphone”),从每个摄像头属于的略有不同的视角拍摄同一场景的照片。然后,它利用每个像素经历的 视差效应 精确确定每个像素的深度。然后将这个深度图与图像一起输入到 CNN 中。

图 13-29. Pixel 3 上的肖像效果,通过模糊实现前景和背景的分离
阿里巴巴的说话者识别
想象一下对着朋友的手机说“嘿 Siri”或“嘿 Google”,然后接着说“读取最后一条短信”的神奇词语。从隐私的角度来看,如果这个方法奏效,那将是令人恐惧的。显然,如今大多数移动操作系统都要求我们先解锁手机才能继续。这给手机的所有者带来了不太流畅的体验。
阿里巴巴机器智能实验室通过以下方法解决了这个问题。首先,将语音转换为声谱图像(使用梅尔频率倒谱算法提取音频特征),训练一个 CNN(使用迁移学习),最后使用 Tensorflow Lite 部署到设备上。没错:CNN 不仅可以用于计算机视觉!
通过识别说话者,他们能够在家庭中有多个用户的设备上个性化内容(例如,在其类似 Netflix 的电视应用程序中的“与您的电视交谈”功能)。此外,通过识别声音是否为人类声音,它能够将口头命令与背景噪音隔离开来进行转录。为了加快使用 Tensorflow Lite 的处理速度,工程师们保留了 USE_NEON 标志以加速基于 ARM 的 CPU 的指令集。团队报告称,通过这种优化,速度提高了四倍。
ML Kit 中的面部轮廓
想要快速构建类似 Snapchat 面部滤镜的功能,而不需要获得博士学位吗?ML Kit 也提供了实现这一功能所需的工具——一个用于识别面部轮廓的 API,一组点(以 x 和 y 坐标表示),这些点遵循输入图像中面部特征的形状。总共有 133 个点映射到各种面部轮廓,包括代表每只眼睛的 16 个点,而 36 个点映射到脸部周围的椭圆形状,如 图 13-30 所示。
在内部,ML Kit 正在使用 TensorFlow Lite 运行深度学习模型。在使用新的 TensorFlow Lite GPU 后端进行实验时,Google 发现与之前的 CPU 后端相比,Pixel 3 和三星 Galaxy S9 的速度提高了四倍,iPhone 7 的速度提高了六倍。这样做的效果是,我们可以实时精确地在脸上放置帽子或太阳镜。

图 13-30. ML Kit 识别的 133 个面部轮廓点(图片来源)
YouTube Stories 中的实时视频分割
绿幕是视频制作行业中任何人的基本设备。通过选择与主题不匹配的颜色,可以在后期制作过程中使用 色度键 技术更改背景。显然,这需要昂贵的软件和强大的后期制作设备。许多 YouTuber 有同样的需求,但可能没有足够的预算。现在他们在 YouTube Stories 应用程序中有了解决方案——通过 TensorFlow Lite 实现的实时视频分割选项。
这里的关键要求首先是快速运行语义分割(每秒 30 帧以上),其次是时间上的一致性;例如,在边缘实现平滑的帧与帧之间的时间连续性。如果我们尝试在多个帧上运行语义分割,我们很快就会注意到许多分割掩模的边缘在跳动。这里采用的关键技巧是将人脸的分割掩模传递给下一帧作为先验。因为我们传统上使用三个通道(RGB),技巧是添加第四个通道,本质上是前一帧的输出,如图 13-31 所示。

图 13-31。输入图像(左)被分解为其三个组件层(R、G、B)。然后将前一帧的输出掩模与这些组件连接在一起(图片来源)
通过对基础 CNN 结构的其他优化,YouTube 团队能够在 iPhone 7 上以每秒 100 帧以上的速度运行系统,并且在 Pixel 2 上以每秒 40 帧以上的速度运行。此外,比较 TensorFlow Lite 的 CPU 与 GPU 后端,选择 GPU 后端时速度增加了 18 倍(与仅针对图像的其他语义分割任务通常为 2 到 7 倍的加速相比更多)。
摘要
在本章中,我们讨论了 TensorFlow Lite 的架构,以及它与 Android 的神经网络 API 的关系。然后我们着手在 Android 设备上运行一个简单的物体识别应用程序。我们介绍了 Google 的 ML Kit,并讨论了您可能想要使用它的原因。此外,我们还讨论了如何将我们的 TensorFlow 模型转换为 TensorFlow Lite 格式,以便它们可以在 Android 设备中使用。最后,我们讨论了 TensorFlow Lite 如何被用来解决真实世界问题的一些示例。
在下一章中,我们将探讨如何利用实时深度学习开发交互式、真实世界的应用程序。
第十四章:使用 TensorFlow Object Detection API 构建完美的猫定位器应用
鲍勃经常受到附近的流浪猫的访问。这些访问导致了不太愉快的结果。你看,鲍勃有一个相当大的花园,他非常用心地照料。然而,这只毛茸茸的小家伙每天晚上都会来到他的花园,并开始咬一堆植物。几个月的辛勤工作在一夜之间被摧毁。显然对这种情况不满意,鲍勃渴望采取一些行动。
发挥他内心的宠物侦探艾斯·文图拉(Ace Ventura)的精神,他试图在夜间保持清醒,赶走猫,但显然,这在长期内是不可持续的。毕竟,红牛也有其限制。达到一个临界点后,他决定使用核选项:将 AI 与他的洒水系统结合起来,从而真正“打开水管”对付猫。
在他广阔的花园里,他设置了一个可以跟踪猫的运动并打开最近的洒水器来吓跑猫的摄像头。他周围有一部旧手机,他所需要的就是一种实时确定猫位置的方法,这样他就可以相应地控制哪个洒水器触发。
希望经过几次不受欢迎的浴,如图 14-1 所示,猫会不再去鲍勃的花园里捣乱。

图 14-1. 建立一个 AI 猫喷水系统,就像这个 Havahart Spray Away Motion Detector
提示
敏锐的观察者可能已经注意到,我们试图在二维空间中找到猫的位置,而猫本身存在于三维空间中。我们可以做出一些假设来简化在真实世界坐标中定位猫的问题,假设摄像头始终处于固定位置。为了确定猫的距离,我们可以使用一个平均猫的大小来测量它在图片上的表观大小,这些图片是在距离摄像头的固定距离处定期拍摄的。例如,一个平均猫在距离摄像头两英尺处可能看起来有一个 300 像素的边界框高度,而当它离摄像头镜头三英尺时,它可能占据 250 像素。
在这一章中,我们帮助鲍勃制作他的猫探测器。(动物权益倡导者请注意:在制作本章时没有伤害到任何猫。)我们沿途回答以下问题:
-
有哪些不同类型的计算机视觉任务?
-
如何重用为现有对象类别预训练的模型?
-
我能否在不编写任何代码的情况下训练一个物体检测器模型?
-
我想要更精细的控制,以获得更高的准确性和速度。如何训练一个自定义物体检测器?
计算机视觉任务的类型
在前面的大部分章节中,我们基本上看了一种问题:物体分类。在这种情况下,我们找出了图像中是否包含某个特定类别的对象。在鲍勃的情况下,除了知道摄像头是否看到了一只猫之外,还需要确切地知道猫的位置,以触发最近的洒水器。确定图像中对象的位置是一种不同类型的问题:物体检测。在我们深入研究物体检测之前,让我们看看频繁的计算机视觉任务的种类以及它们试图回答的问题。
分类
在本书中,我们已经看过几个分类的例子。简单来说,分类的任务是将图像分配给图像中存在的对象类别。它回答了这样一个问题:“图像中是否有 X 类对象?”分类是最基本的计算机视觉任务之一。在一张图像中,可能有多个类别的对象,称为多类别分类。在这种情况下,一张图像的标签将是图像包含的所有对象类别的列表。
定位
分类的缺点是它不告诉我们图像中特定对象的位置在哪里,它有多大或多小,或者有多少个对象。它只告诉我们特定类别的对象存在于图像中的某个地方。定位,又称带定位的分类,可以告诉我们图像中有哪些类别以及它们在图像中的位置。这里的关键词是类别,而不是个别对象,因为定位每个类别只给出一个边界框。就像分类一样,它无法回答“这个图像中有多少个 X 类对象?”的问题。
正如您马上会看到的,只有在可以保证每个类别只有一个实例时,定位才能正常工作。如果一个类别有多个实例,一个边界框可能包含该类别的一些或所有对象(取决于概率)。但每个类别只有一个边界框似乎相当受限制,不是吗?
检测
当我们在同一图像中有多个属于多个类别的对象时,定位就不够了。对象检测会为每个类的每个实例提供一个边界矩形,对于所有类别。它还会告诉我们每个检测到的边界框中的对象的类别。例如,在自动驾驶汽车的情况下,对象检测会为汽车看到的每辆车、每个人、每个路灯等返回一个边界框。由于这种能力,我们也可以将其用于需要计数的应用程序。例如,计算人群中的人数。
与定位不同,检测在图像中某个类的实例数量上没有限制。这就是为什么它在现实世界的应用中被使用的原因。
警告
通常人们在说“对象定位”时实际上是指“对象检测”。重要的是要注意这两者之间的区别。
分割
对象分割是将类标签分配给整个图像中的每个像素的任务。我们使用蜡笔给各种对象上色的儿童涂色书是一个现实世界的类比。考虑到图像中的每个像素都被分配一个类,这是一个高度计算密集的任务。虽然对象定位和检测提供边界框,分割产生像素组,也被称为掩码。与检测相比,分割在对象的边界方面显然更加精确。例如,一个可以实时改变用户头发颜色的化妆应用程序会使用分割。根据掩码的类型,分割有不同的风格。
语义分割
给定一幅图像,语义分割为每个类分配一个掩码。如果有多个相同类别的对象,则它们都分配给同一个掩码。同一类别的实例之间没有区分。就像定位一样,语义分割无法计数。
实例级分割
给定一幅图像,实例级分割识别每个类别每个实例占据的区域。同一类别的不同实例将被唯一分割。
表 14-1 列出了不同的计算机视觉任务。
表 14-1。使用图像 ID 120524 从 MS COCO 数据集说明的计算机视觉任务类型
| 任务 | 图像 | 通俗地说 | 输出 |
|---|---|---|---|
| 对象分类 | ![]() |
这个图像中有一只羊吗? | 类概率 |
| 对象定位 | ![]() |
图像中是否有“一只”羊,它在哪里? | 边界框和类概率 |
| 对象检测 | ![]() |
这个图像中的“所有”对象是什么? | 边界框,类概率,类 ID 预测 |
| 语义分割 | ![]() |
这幅图像中哪些像素属于不同的类别;例如,“羊”,“狗”,“人”? | 每个类别一个掩码 |
| 实例级别分割 | ![]() |
这幅图像中每个类别的每个实例属于哪些像素;例如,“羊”,“狗”,“人”? | 每个类别的每个实例一个掩码 |
对象检测方法
根据情景、需求和技术知识,有几种方法可以实现在应用程序中获取对象检测功能。表 14-2 介绍了一些方法。
表 14-2. 对象检测的不同方法及其权衡
| 自定义类别 | 工作量 | 云端或本地 | 优缺点 | |
|---|---|---|---|---|
| 基于云的对象检测 API | 否 | <5 分钟 | 仅限云端 | + 成千上万种类别+ 处于技术前沿+ 快速设置+ 可扩展 API– 无定制化– 由于网络而产生的延迟 |
| 预训练模型 | 否 | <15 分钟 | 本地 | + ~100 个类别+ 选择适合速度和准确性需求的模型+ 可在边缘运行– 无定制化 |
| 基于云的模型训练 | 是 | <20 分钟 | 云端和本地 | + 基于 GUI,无需编码进行训练+ 可定制性+ 选择强大模型(用于云端)和高效模型(用于边缘)+ 可扩展 API– 使用迁移学习,可能不允许完整模型重新训练 |
| 自定义训练模型 | 是 | 4 小时至 2 天 | 本地 | + 高度可定制+ 提供各种速度和准确性要求的模型– 耗时且复杂 |
值得注意的是,“云”标记的选项已经为可扩展性而设计。同时,标记为“本地”推断的选项也可以部署在云端。为了实现高规模,我们可以将它们部署在云端,类似于我们在第九章中为分类模型所做的方式。
调用预构建的基于云的对象检测 API
正如我们在第八章中已经看到的,调用基于云的 API 相对简单。该过程涉及设置账户、获取 API 密钥、阅读文档以及编写 REST API 客户端。为了简化这个过程,许多服务提供基于 Python(和其他语言)的 SDK。主要的基于云的对象检测 API 提供商是 Google 的 Vision AI(图 14-2)和微软的认知服务。
使用基于云的 API 的主要优势在于它们具有可扩展性,并支持识别成千上万种对象类别。这些云巨头使用大型专有数据集构建了他们的模型,这些数据集不断增长,导致了非常丰富的分类法。考虑到最大的公共数据集中带有对象检测标签的类别数量通常只有几百个,目前还没有非专有解决方案能够检测成千上万种类别。
基于云的 API 的主要限制当然是由于网络请求而产生的延迟。无法通过这种延迟实现实时体验。在下一节中,我们将看看如何在没有任何数据或训练的情况下实现实时体验。

图 14-2. 在 Google 的 Vision AI API 上运行熟悉的照片以获取对象检测结果
重用预训练模型
在这一部分,我们将探讨如何轻松地在预训练模型上在手机上运行目标检测器。希望您已经熟悉了在之前的一些章节中在移动设备上运行神经网络的方法。在书的 GitHub 网站上引用了在 iOS 和 Android 上运行目标检测模型的代码(请参阅http://PracticalDeepLearning.ai)位于code/chapter-14。更改功能只是简单地用新的.tflite模型文件替换现有的模型文件。
在之前的章节中,我们经常使用 MobileNet 来进行分类任务。尽管 MobileNet 系列,包括 MobileNetV2(2018)和 MobileNetV3(2019)本身只是分类网络,但它们可以作为 SSD MobileNetV2 等目标检测架构的骨干,我们在本章中使用。
获取模型
揭示幕后魔术的最佳方法是深入研究一个模型。我们需要获取我们的手上的TensorFlow Models 仓库,其中包含 50 多个深度学习任务的模型,包括音频分类、文本摘要和目标检测。这个仓库包含模型以及我们在本章中使用的实用脚本。话不多说,让我们在我们的机器上克隆这个仓库:
$ git clone https://github.com/tensorflow/models.git && cd models/research
然后,我们更新PYTHONPATH以使所有脚本对 Python 可见:
$ export PYTHONPATH="${PYTHONPATH}:`pwd`:`pwd`/slim"
接下来,我们将从我们的 GitHub 仓库(请参阅http://PracticalDeepLearning.ai)复制协议缓冲区(protobuf)编译器命令脚本到我们当前目录,以使.proto 文件对 Python 可用:
$ cp {path_to_book_github_repo}/code/chapter-14/add_protoc.sh . && \
chmod +x add_protoc.sh && \
./add_protoc.sh
最后,我们按照以下步骤运行setup.py脚本:
$ python setup.py build
$ python setup.py install
现在是下载我们预构建模型的时候了。在我们的情况下,我们使用 SSD MobileNetV2 模型。您可以在 TensorFlow 的目标检测模型动物园找到一个大型模型列表,其中包含按照它们训练的数据集、推理速度和平均精度(mAP)进行分类的模型。在那个列表中,下载名为ssd_mobilenet_v2_coco的模型,正如其名称所示,该模型是在 MS COCO 数据集上训练的。在 NVIDIA GeForce GTX TITAN X 上以 600x600 分辨率输入运行时,该模型在 22 mAP 上运行,耗时 31 毫秒。下载完该模型后,我们将其解压缩到 TensorFlow 仓库中的models/research/object_detection目录中。我们可以按照以下方式检查目录的内容:
$ cd object_detection/
$ ls ssd_mobilenet_v2_coco_2018_03_29
checkpoint model.ckpt.data-00000-of-00001 model.ckpt.meta
saved_model
frozen_inference_graph.pb model.ckpt.index pipeline.config
测试我们的模型
在将我们的模型插入移动应用程序之前,验证模型是否能够进行预测是一个好主意。TensorFlow Models 仓库包含一个 Jupyter Notebook,我们可以简单地插入一张照片进行预测。您可以在models/research/object_detection/object_detection_tutorial.ipynb找到这个笔记本。图 14-3 展示了来自该笔记本的一张熟悉照片的预测。

图 14-3。来自 TensorFlow Models 仓库的可直接使用的 Jupyter Notebook 中的目标检测预测
部署到设备
现在我们已经验证了模型的工作原理,是时候将其转换为适合移动设备的格式了。为了将其转换为 TensorFlow Lite 格式,我们使用了我们熟悉的tfite_convert工具,来自第十三章。值得注意的是,该工具操作.pb文件,而 TensorFlow 目标检测模型动物园仅提供模型检查点。因此,我们首先需要从检查点和图生成.pb模型。让我们使用 TensorFlow Models 仓库附带的方便脚本来做到这一点。您可以在models/research/object_detection中找到export_tflite_ssd_graph.py文件:
$ python export_tflite_ssd_graph.py \
--pipeline_config_path=ssd_mobilenet_v2_coco_2018_03_29/pipeline.config \
--trained_checkpoint_prefix=ssd_mobilenet_v2_coco_2018_03_29/
model.ckpt.data-00000-of-00001 \
--output_directory=tflite_model \
--add_postprocessing_op=true
如果前面的脚本执行成功,我们将在tflite_model目录中看到以下文件:
$ ls tflite_model
tflite_graph.pb
tflite_graph.pbtxt
我们将使用tflite_convert工具将其转换为 TensorFlow Lite 格式。
$ tflite_convert --graph_def_file=tflite_model/tflite_graph.pb \
--output_file=tflite_model/model.tflite
现在唯一剩下的事情就是将模型插入应用程序中。我们将使用的应用程序在书的 GitHub 网站中引用(请参阅http://PracticalDeepLearning.ai)位于code/chapter-14。我们已经看过如何在第 11、12 和 13 章中如何在应用程序中更换模型,因此我们不会在这里再次讨论。图 14-4 显示了将对象检测器模型插入 Android 应用程序时的结果。

图 14-4. 在 Android 设备上运行实时对象检测模型
我们能够立即看到“猫”预测的原因是因为“猫”是用于训练模型的 MS COCO 数据集中已经存在的 80 个类别之一。这个模型可能足以供 Bob 部署在他的设备上(请记住,尽管 Bob 可能不使用手机来跟踪他的花园,但将 TensorFlow Lite 模型部署到边缘硬件的过程是非常相似的)。然而,Bob 最好通过在花园内生成的数据对模型进行微调以提高精度。在接下来的部分中,我们将探讨如何使用迁移学习来构建一个只使用基于网络的工具的对象检测器。如果你已经阅读了第八章,接下来的内容应该感觉很熟悉。
构建一个无需任何代码的自定义检测器
我的猫的呼吸闻起来像猫粮!
拉尔夫·威格姆
让我们面对现实吧。你们的作者可以四处走动,在花园里拍摄很多猫的照片来进行这个实验。这将是乏味的,我们可能只会获得几个额外百分点的精度和召回率。或者我们可以看一个真正有趣的例子,也测试 CNN 的极限。当然,我们指的是辛普森一家。鉴于大多数模型没有使用卡通图像的特征进行训练,看看我们的模型在这里的表现将是有趣的。
首先,我们需要获取一个数据集。幸运的是,我们在Kaggle上有一个可用的数据集。让我们下载数据集并使用 CustomVision.ai(类似于第八章)来训练我们的辛普森分类器,以下是使用的步骤。太棒了!
注意
与一些人可能认为的相反,我们这些作者并没有数百万美元散落在周围。这意味着我们无法负担从福克斯购买辛普森一家的版权。版权法律的结果是阻止我们发布该节目的任何图像。因此,在这一部分中,我们使用了下一个最好的选择——来自美国国会的公共领域图像。我们使用国会议员的照片,这些议员与辛普森一家有相同的名字:霍默·霍奇、玛吉·鲁克玛、巴特·戈登和丽莎·布伦特·罗切斯特。请记住,这仅用于出版目的;我们仍然在原始卡通数据集上进行了训练。
-
前往 CustomVision.ai 并开始一个新的目标检测项目(图 14-5)。
![在 CustomVision.ai 中创建一个新的目标检测项目]()
图 14-5. 在 CustomVision.ai 中创建一个新的目标检测项目
-
为霍默、玛吉、巴特和丽莎创建标签。为每个角色上传 15 张图像,并为这些图像中的每一个绘制一个边界框。仪表板现在应该类似于图 14-6。
![带有边界框和类名的仪表板]()
图 14-6.带有边界框和类名的仪表板
-
点击“训练”按钮,让训练开始。仪表板上有滑块来控制概率和重叠阈值。我们可以尝试调整它们,使其达到我们喜欢的效果,并获得良好的精度和召回率。
-
点击“快速测试”按钮,并使用辛普森家族的任意随机图像(之前未用于训练)来测试模型的性能。结果可能还不太好,但没关系:我们可以通过使用更多数据进行训练来解决这个问题。
-
让我们尝试增加多少图像以提高精度、召回率和 mAP。让我们从为每个类别添加五张图像并重新训练模型开始。
-
重复此过程,直到我们获得可接受的结果。图 14-7 显示了我们实验的结果。
![随着每个类别图像数量的增加,百分比平均精度改善的测量]()
图 14-7.随着每个类别图像数量的增加,百分比平均精度改善的测量
有了我们的最终模型,我们能够对互联网上的随机图像进行相当好的检测,如图 14-8 所示。尽管不是预期的,但令人惊讶的是,一个在自然图像上预训练的模型能够在相对少量数据上对卡通进行微调。

图 14-8.使用最终模型检测到的辛普森角色,由同名的美国国会议员代表(请参见本节开头的注释)
正如我们现在已经知道的那样,CustomVision.ai 允许将此模型导出为各种格式,包括 Core ML、TensorFlow Lite、ONNX 等。我们可以简单地下载到我们想要的格式(在我们的情况下为.tflite),并将此文件插入到应用程序中,类似于我们在上一节中使用预训练模型的方式。有了实时的辛普森检测,我们可以在下次斯金纳校长在电视上谈论蒸汽火腿时向朋友和家人展示。
注意
CustomVision.ai 并不是唯一一个允许您在不编写任何代码的情况下在线标记、训练和部署的平台。Google 的 Cloud AutoML(截至 2019 年 10 月仍处于测试阶段)和 Apple 的 Create ML(仅适用于 Apple 生态系统)提供非常相似的功能集。此外,Matroid 允许您从视频源构建自定义对象检测器,这对于训练网络而不需要花费太多精力来构建数据集非常有用。
到目前为止,我们已经看过三种快速的方法来运行目标检测,只需很少的代码。我们估计大约 90%的用例可以通过这些选项之一来处理。如果您可以使用这些解决您的情况,您可以停止阅读本章,直接跳转到“案例研究”部分。
在极少数情况下,我们可能希望对准确性、语音、模型大小和资源使用等因素进行进一步细化控制。在接下来的章节中,我们将更详细地了解目标检测的世界,从标记数据一直到部署模型。
目标检测的演变
多年来,随着深度学习革命的发生,不仅分类,还有其他计算机视觉任务,包括目标检测,都经历了一次复兴。在这段时间里,提出了几种用于目标检测的架构,通常是在其他架构的基础上构建的。图 14-9 展示了其中一些的时间轴。

图 14-9. 不同目标检测架构的时间轴(图片来源:吴雄伟等人的“深度学习目标检测的最新进展”)
在目标分类中,我们的 CNN 架构从图像中提取特征并计算特定数量类别的概率。这些 CNN 架构(ResNet,MobileNet)作为目标检测网络的骨干。在目标检测网络中,我们的最终结果是边界框(由矩形中心,高度,宽度定义)。尽管涵盖许多这些内部细节可能需要更深入的探索(您可以在本书的 GitHub 网站上找到,链接在http://PracticalDeepLearning.ai),但我们可以广泛地将它们分为两类,如表 14-3 所示。
表 14-3. 目标检测器的类别
| 描述 | 优缺点 | |
|---|---|---|
| 两阶段检测器 | 使用区域建议网络(RPN)生成类别不可知的边界框候选区域(感兴趣区域)。在这些区域提议上运行 CNN 以为每个分配一个类别。示例:Faster R-CNN,Mask R-CNN。 | + 高精度- 较慢 |
| 一阶段检测器 | 直接在特征图的每个位置上对对象进行分类预测;可以进行端到端训练。示例:YOLO,Single-Shot Detector (SSD)。 | + 速度快,更适合实时应用- 精度较低 |
注意
在目标检测领域,Ross Girshik 是一个你在阅读论文时经常会遇到的名字。他在深度学习时代之前和之后都有作品,包括可变形部件模型(DPM),R-CNN,Fast R-CNN,Faster R-CNN,You Only Look Once(YOLO),Mask R-CNN,Feature Pyramid Network(FPN),RetinaNet 和 ResNeXt 等等,经常在公共基准上年复一年地打破自己的记录。
我参与了几次 PASCAL VOC 目标检测挑战的第一名入选,并因我在可变形部件模型上的工作而获得了“终身成就”奖。我认为这指的是 PASCAL 挑战的终身,而不是我的!
—Ross Girschik
性能考虑
从从业者的角度来看,主要关注点往往是精度和速度。这两个因素通常相互呈反比关系。我们希望找到在我们的场景中达到理想平衡的检测器。表 14-4 总结了一些在 TensorFlow 目标检测模型库中可用的预训练模型。报告的速度是在一个 600x600 像素分辨率的图像输入上,使用 NVIDIA GeForce GTX TITAN X 卡进行处理的。
表 14-4. TensorFlow 目标检测模型库网站上一些可用架构的速度与平均精度百分比
| 推理速度(毫秒) | 在 MS COCO 上的 mAP 百分比 | |
|---|---|---|
| ssd_mobilenet_v1_coco | 30 | 21 |
| ssd_mobilenet_v2_coco | 31 | 22 |
| ssdlite_mobilenet_v2_coco | 27 | 22 |
| ssd_resnet_50_fpn_coco | 76 | 35 |
| faster_rcnn_nas | 1,833 | 43 |
谷歌在 2017 年进行的一项研究(参见图 14-10)揭示了以下关键发现:
-
一阶段检测器比两阶段检测器更快,但精度较低。
-
骨干 CNN 架构在分类任务(如 ImageNet)上的准确性越高,对象检测器的精度也越高。
-
小尺寸物体对对象检测器具有挑战性,表现较差(通常在 10% mAP 以下)。这种影响在单阶段检测器上更为严重。
-
在大尺寸物体上,大多数检测器表现相似。
-
更高分辨率的图像会产生更好的结果,因为小物体在网络中看起来更大。
-
两阶段对象检测器内部生成大量建议,用作对象的潜在位置。建议数量可以进行调整。减少建议数量可以在准确性略微降低的情况下加快速度(阈值将取决于使用情况)。
对象检测架构以及骨干架构(特征提取器)对平均精度和预测时间的影响(请注意,在灰度打印中颜色可能不清晰;请参考书籍的 GitHub 获取彩色图像)(图片来源:Jonathan Huang 等人的现代卷积对象检测器速度/准确性权衡)
图 14-10。对象检测架构以及骨干架构(特征提取器)对平均精度和预测时间的影响(请注意,在灰度打印中颜色可能不清晰;请参考书籍的 GitHub 网站[参见http://PracticalDeepLearning.ai]获取彩色图像)(图片来源:Jonathan Huang 等人的现代卷积对象检测器速度/准确性权衡)
对象检测中的关键术语
对象检测中经常使用的术语包括交并比、平均精度、非极大值抑制和锚点。让我们逐个解释这些术语的含义。
交并比
在目标检测训练流程中,通常使用诸如交并比(IoU)之类的指标作为过滤特定质量检测的阈值。为了计算 IoU,我们使用预测的边界框和实际边界框来计算两个不同的数字。第一个是预测和实际情况重叠的区域——交集。第二个是预测和实际情况覆盖的范围——并集。正如名称所示,我们简单地将交集的总面积除以并集的面积来获得 IoU。图 14-11 直观地展示了两个 2x2 正方形的 IoU 概念,它们在中间的一个 1x1 正方形中相交。

图 14-11。IoU 比率的可视化表示
在理想情况下,预测的边界框与实际情况完全匹配,IoU 值将为 1。在最坏的情况下,预测与实际情况没有重叠,IoU 值将为 0。正如我们所看到的,IoU 值将在 0 和 1 之间变化,较高的数字表示较高质量的预测,如图 14-12 所示。
为了帮助我们过滤和报告结果,我们会设置一个最小 IoU 阈值。将阈值设置为非常激进的值(如 0.9)会导致丢失许多可能在后续流程中变得重要的预测。相反,将阈值设置得太低会导致太多虚假的边界框。通常用于报告目标检测模型精度的最小 IoU 为 0.5。

图 14-12。IoU 的图示;更好模型的预测往往与实际情况有更重叠,导致更高的 IoU
值得在这里重申的是,IoU 值是针对每个类别实例计算的,而不是针对每个图像。然而,为了计算检测器在更大一组图像上的质量,我们会将 IoU 与其他指标(如平均精度)结合使用。
平均精度
在研究目标检测的论文中,我们经常遇到诸如 AP@0.5 的数字。该术语表示 IoU=0.5 时的平均精度。另一个更详细的表示方法是 AP@[0.6:0.05:0.75],这是从 IoU=0.6 到 IoU=0.75 的平均精度,间隔为 0.05。平均精度(或 mAP)简单地是所有类别的平均精度。对于 COCO 挑战,使用的 MAP 指标是 AP@[0.5:0.05:0.95]。
非极大值抑制
在内部,目标检测算法对可能在图像中的对象的潜在位置提出了许多建议。对于图像中的每个对象,预期会有多个具有不同置信度值的边界框建议。我们的任务是找到最能代表对象真实位置的建议。一种天真的方法是只考虑置信度值最大的建议。如果整个图像中只有一个对象,这种方法可能有效。但如果图像中有多个类别,每个类别中有多个实例,这种方法就不起作用了。
非极大值抑制(NMS)来拯救我们(图 14-13)。NMS 背后的关键思想是,同一对象的两个实例不会有严重重叠的边界框;例如,它们的边界框的 IoU 将小于某个 IoU 阈值(比如 0.5)。实现这一点的一种贪婪方法是对每个类别重复以下步骤:
-
过滤掉所有置信度低于最小阈值的建议。
-
接受置信度值最大的提议。
-
对于所有按其置信度值降序排序的剩余提议,检查当前框与先前接受的提议之一的 IoU 是否≥0.5。如果是,则过滤掉;否则,接受它作为提议。
![使用 NMS 找到最能代表图像中对象位置的边界框]()
图 14-13。使用 NMS 找到最能代表图像中对象位置的边界框
使用 TensorFlow 目标检测 API 构建自定义模型
在本节中,我们将通过一个完整的端到端示例来构建一个对象检测器。我们将看到该过程中的几个步骤,包括收集、标记和预处理数据,训练模型,并将其导出为 TensorFlow Lite 格式。
首先,让我们看一些收集数据的策略。
数据收集
我们现在知道,在深度学习的世界中,数据至关重要。我们可以通过几种方式获取我们想要检测的对象的数据:
使用现成的数据集
有一些公共数据集可用于训练对象检测模型,如 MS COCO(80 个类别)、ImageNet(200 个类别)、Pascal VOC(20 个类别)和更新的 Open Images(600 个类别)。MS COCO 和 Pascal VOC 在大多数对象检测基准测试中使用,基于 COCO 的基准测试对于真实世界场景更具现实性,因为图像更复杂。
从网上下载
我们应该已经熟悉这个过程,因为在第十二章中,我们为“热狗”和“非热狗”类别收集了图像。浏览器扩展程序如 Fatkun 对于快速从搜索引擎结果中下载大量图像非常方便。
手动拍照
这是更耗时(但潜在有趣)的选项。为了构建一个强大的模型,重要的是用在各种不同环境中拍摄的照片来训练它。拿着要检测的对象,我们应该至少拍摄 100 到 150 张不同背景下的照片,具有各种构图和角度,并在许多不同的光照条件下。图 14-14 展示了用于训练可乐和百事对象检测模型的一些照片示例。考虑到模型可能会学习到红色代表可乐,蓝色代表百事这种虚假相关性,最好混合对象和可能会混淆它的背景,以便最终实现更强大的模型。

图 14-14。在各种不同环境中拍摄的对象照片,用于训练对象检测模型
值得在对象不太可能使用的环境中拍摄对象的照片,以使数据集多样化并提高模型的稳健性。这也有一个额外的好处,可以使原本乏味和繁琐的过程变得有趣。我们可以通过挑战自己想出独特和创新的照片来使其有趣。图 14-15 展示了在构建货币检测器过程中拍摄的一些创意照片的示例。

图 14-15。在构建多样化货币数据集过程中拍摄的一些创意照片
因为我们想要制作一个猫检测器,我们将重复使用“猫和狗”Kaggle 数据集中的猫图片,该数据集在第三章中使用过。我们随机选择了该数据集中的图片,并将它们分成训练集和测试集。
为了保持输入到我们网络的大小的统一性,我们将所有图像调整为固定大小。我们将使用相同的大小作为网络定义的一部分,并在转换为.tflite模型时使用。我们可以使用 ImageMagick 工具一次调整所有图像的大小:
$ apt-get install imagemagick
$ mogrify -resize 800x600 *.jpg
提示
如果您在当前目录中有大量图像(即数万张图像),上述命令可能无法列出所有图像。解决方法是使用find命令列出所有图像,并将输出传输到mogrify命令:
$ find . -type f | awk -F. '!a[$NF]++{print $NF}' |
xargs -I{} mogrify -resize 800x600 *.jpg
标记数据
在获得数据之后,下一步是对其进行标记。与分类不同,仅将图像放入正确的目录中就足够了,我们需要手动为感兴趣的对象绘制边界矩形。我们选择此任务的工具是 LabelImg(适用于 Windows、Linux 和 Mac),原因如下:
-
您可以将注释保存为 PASCAL VOC 格式的 XML 文件(也被 ImageNet 采用)或 YOLO 格式。
-
它支持单标签和多标签。
注意
如果您像我们一样是好奇的猫,您可能想知道 YOLO 格式和 PASCAL VOC 格式之间的区别是什么。 YOLO 恰好使用简单的.txt文件,每个图像一个文件,文件名相同,用于存储有关图像中类别和边界框的信息。以下是 YOLO 格式中.txt文件的典型外观:
class_for_box1 box1_x box1_y box1_w box1_h
class_for_box2 box2_x box2_y box2_w box2_h
请注意,此示例中的 x、y、宽度和高度是使用图像的完整宽度和高度进行归一化的。
另一方面,PASCAL VOC 格式是基于 XML 的。与 YOLO 格式类似,我们每个图像使用一个 XML 文件(最好与相同名称)。您可以在 Git 存储库的code/chapter-14/pascal-voc-sample.xml中查看示例格式。
首先,从GitHub下载应用程序到计算机上的一个目录。该目录将包含一个可执行文件和一个包含一些示例数据的数据目录。因为我们将使用自己的自定义数据,所以我们不需要提供的数据目录中的数据。您可以通过双击可执行文件来启动应用程序。
此时,我们应该有一组准备用于训练过程的图像。我们首先将数据随机分成训练集和测试集,并将它们放在不同的目录中。我们可以通过简单地将图像随机拖放到任一目录中来进行此拆分。创建训练和测试目录后,我们通过单击“打开目录”来加载训练目录,如图 14-16 所示。

图 14-16. 单击“打开目录”按钮,然后选择包含训练数据的目录
LabelImg 加载训练目录后,我们可以开始标记过程。我们必须逐个图像地为每个对象(在我们的情况下仅为猫)手动绘制边界框,如图 14-17 所示。绘制边界框后,我们将提示提供标签以配合边界框。对于此练习,请输入对象的名称“cat”。输入标签后,我们只需选择复选框,以指定再次为后续图像选择该标签。对于具有多个对象的图像,我们将制作多个边界框并添加相应的标签。如果存在不同类型的对象,我们只需为该对象类别添加新标签。

图 14-17. 从左侧面板中选择 Create RectBox 以创建覆盖猫的边界框
现在,对所有训练和测试图像重复这一步骤。我们希望为每个对象获取紧密的边界框,以便对象的所有部分都被框住。最后,在训练和测试目录中,我们看到每个图像文件都有一个.xml文件,如图 14-18 所示。我们可以在文本编辑器中打开.xml文件并检查元数据,如图像文件名、边界框坐标和标签名称。

图 14-18. 每个图像都附带一个包含标签信息和边界框信息的 XML 文件
数据预处理
到目前为止,我们有方便的 XML 数据,提供了所有对象的边界框。但是,为了使用 TensorFlow 对数据进行训练,我们必须将其预处理成 TensorFlow 理解的格式,即 TFRecords。在将数据转换为 TFRecords 之前,我们必须经历一个中间步骤,将所有 XML 文件中的数据合并到一个单独的逗号分隔值(CSV)文件中。TensorFlow 提供了帮助脚本来协助我们进行这些操作。
现在我们的环境已经设置好,是时候开始做一些真正的工作了:
-
使用来自 Dat Tran 的racoon_dataset存储库中的
xml_to_csv工具,将我们的 cats 数据集目录转换为一个单独的 CSV 文件。我们在我们的存储库中提供了这个文件的稍作编辑副本,位于code/chapter-14/xml_to_csv.py中:$ python xml_to_csv.py -i {path to cats training dataset} -o {path to output train_labels.csv} -
对测试数据做同样的操作:
$ python xml_to_csv.py -i {path to cats test dataset} -o {path to test_labels.csv} -
创建label_map.pbtxt文件。该文件包含所有类别的标签和标识符映射。我们使用这个文件将文本标签转换为整数标识符,这是 TFRecord 所期望的。因为我们只有一个类别,所以这个文件相对较小,如下所示:
item { id: 1 name: 'cat' } -
生成 TFRecord 格式的文件,其中包含以后用于训练和测试模型的数据。这个文件也来自 Dat Tran 的racoon_dataset存储库。我们在我们的存储库中提供了这个文件的稍作编辑副本,位于code/chapter-14/generate_tfrecord.py中。(值得注意的是,参数中的
image_dir路径应与 XML 文件中的路径相同;LabelImg 使用绝对路径。)$ python generate_tfrecord.py \ --csv_input={path to train_labels.csv} \ --output_path={path to train.tfrecord} \ --image_dir={path to cat training dataset} $ python generate_tfrecord.py \ --csv_input={path to test_labels.csv} \ --output_path={path to test.tfrecord} \ --image_dir={path to cat test dataset}
有了准备好的train.tfrecord和test.tfrecord文件,我们现在可以开始训练过程了。
检查模型
(此部分仅供信息,对训练过程并非必需。如果您愿意,可以直接跳转到“训练”。)
我们可以使用saved_model_cli工具检查我们的模型:
$ saved_model_cli show --dir ssd_mobilenet_v2_coco_2018_03_29/saved_model \
--tag_set serve \
--signature_def serving_default
该脚本的输出如下:
The given SavedModel SignatureDef contains the following input(s):
inputs['inputs'] tensor_info:
dtype: DT_UINT8
shape: (-1, -1, -1, 3)
name: image_tensor:0
The given SavedModel SignatureDef contains the following output(s):
outputs['detection_boxes'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 100, 4)
name: detection_boxes:0
outputs['detection_classes'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 100)
name: detection_classes:0
outputs['detection_scores'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 100)
name: detection_scores:0
outputs['num_detections'] tensor_info:
dtype: DT_FLOAT
shape: (-1)
name: num_detections:0
outputs['raw_detection_boxes'] tensor_info:
dtype: DT_FLOAT
shape: (-1, -1, 4)
name: raw_detection_boxes:0
outputs['raw_detection_scores'] tensor_info:
dtype: DT_FLOAT
shape: (-1, -1, 2)
name: raw_detection_scores:0
Method name is: tensorflow/serving/predict
以下是我们如何解释前一个命令的输出:
-
inputs的形状为(-1, -1, -1, 3)表明训练过的模型可以接受包含三个通道的任意大小的图像输入。由于输入大小的固有灵活性,转换后的模型会比具有固定大小输入的模型更大。当我们在本章后面训练自己的目标检测器时,我们将把输入大小固定为 800 x 600 像素,以使得生成的模型更紧凑。 -
在转向输出时,第一个输出是
detection_boxes (-1, 100, 4)。这告诉我们有多少个检测框可能存在,以及它们的样子。特别是,第一个数字(即-1)表示我们可以根据所有 100 个类别的检测结果拥有任意数量的框(第二个数字),每个框有四个坐标(第三个数字)——x、y、宽度和高度——定义每个框。换句话说,我们不限制每个 100 个类别的检测数量。 -
对于
detection_classes,我们有一个包含两个元素的列表:第一个定义了我们检测到多少个对象,第二个是一个独热编码向量,其中检测到的类别被启用。 -
num_detections是图像中检测到的对象数量。它是一个单一的浮点数。 -
raw_detection_boxes定义了每个对象检测到的每个框的坐标。所有这些检测都是在对所有检测应用 NMS 之前进行的。 -
raw_detection_scores是两个浮点数的列表,第一个描述了总检测数,第二个描述了总类别数,包括背景(如果将其视为单独的类别)。
训练
因为我们使用的是 SSD MobileNetV2,所以我们需要创建一个pipeline.config文件的副本(来自 TensorFlow 存储库),并根据我们的配置参数进行修改。首先,复制配置文件并在文件中搜索字符串PATH_TO_BE_CONFIGURED。该参数指示需要使用正确路径(最好是绝对路径)更新的所有位置。我们还需要编辑配置文件中的一些参数,例如类别数(num_classes)、步数(num_steps)、验证样本数(num_examples)以及训练和测试/评估部分的标签映射路径。 (您可以在本书的 GitHub 网站上找到我们版本的pipeline.config文件(请参见http://PracticalDeepLearning.ai))。
$ cp object_detection/samples/configs/ssd_mobilenet_v2_coco.config
./pipeline.config
$ vim pipeline.config
注意
在我们的示例中,我们使用 SSD MobileNetV2 模型来训练我们的目标检测器。在不同条件下,您可能希望选择不同的模型来进行训练。因为每个模型都附带自己的管道配置文件,您将修改该配置文件。配置文件与每个模型捆绑在一起。您将使用类似的过程,识别需要更改的路径并使用文本编辑器相应地更新它们。
除了修改路径,您可能还想修改配置文件中的其他参数,例如图像大小、类别数、优化器、学习率和迭代次数。
到目前为止,我们一直在 TensorFlow 存储库中的models/research目录中。现在让我们进入object_detection目录,并从 TensorFlow 运行model_main.py脚本,根据我们刚刚编辑的pipeline.config文件提供的配置来训练我们的模型:
$ cd object_detection/
$ python model_main.py \
--pipeline_config_path=../pipeline.config \
--logtostderr \
--model_dir=training/
当我们看到以下输出行时,我们就知道训练正在正确进行:
Average Precision (AP) @[ IoU=0.50:0.95 | area=all | maxDets=100 ] = 0.760
根据我们进行训练的迭代次数,训练将需要几分钟到几个小时,所以计划一下,吃点零食,洗洗衣服,清理猫砂盆,或者更好的是,提前阅读下一章。训练完成后,我们应该在 training/目录中看到最新的检查点文件。训练过程还会生成以下文件:
$ ls training/
checkpoint model.ckpt-13960.meta
eval_0 model.ckpt-16747.data-00000-of-00001
events.out.tfevents.1554684330.computername model.ckpt-16747.index
events.out.tfevents.1554684359.computername model.ckpt-16747.meta
export model.ckpt-19526.data-00000-of-00001
graph.pbtxt model.ckpt-19526.index
label_map.pbtxt model.ckpt-19526.meta
model.ckpt-11180.data-00000-of-00001 model.ckpt-20000.data-00000-of-00001
model.ckpt-11180.index model.ckpt-20000.index
model.ckpt-11180.meta model.ckpt-20000.meta
model.ckpt-13960.data-00000-of-00001 pipeline.config
model.ckpt-13960.index
我们将在下一节将最新的检查点文件转换为.TFLite格式。
提示
有时,您可能希望对模型进行更深入的检查,以进行调试、优化和/或信息目的。可以将以下命令与指向检查点文件、配置文件和输入类型的参数一起运行,以查看所有不同参数、模型分析报告和其他有用信息:
$ python export_inference_graph.py \
--input_type=image_tensor \
--pipeline_config_path=training/pipeline.config \
--output_directory=inference_graph \
--trained_checkpoint_prefix=training/model.ckpt-20000
这个命令的输出如下:
=========================Options=========================
-max_depth 10000
-step -1
...
==================Model Analysis Report==================
...
Doc:
scope: The nodes in the model graph are organized by
their names, which is hierarchical like filesystem.
param: Number of parameters (in the Variable).
Profile:
node name | # parameters
_TFProfRoot (--/3.72m params)
BoxPredictor_0 (--/93.33k params)
BoxPredictor_0/BoxEncodingPredictor
(--/62.22k params)
BoxPredictor_0/BoxEncodingPredictor/biases
(12, 12/12 params)
...
FeatureExtractor (--/2.84m params)
...
这份报告可以帮助您了解您拥有多少参数,从而帮助您找到模型优化的机会。
模型转换
现在我们有了最新的检查点文件(后缀应该与pipeline.config文件中的迭代次数匹配),我们将像之前在本章中使用我们的预训练模型一样,将其输入到export_tflite_ssd_graph脚本中:
$ python export_tflite_ssd_graph.py \
--pipeline_config_path=training/pipeline.config \
--trained_checkpoint_prefix=training/model.ckpt-20000 \
--output_directory=tflite_model \
--add_postprocessing_op=true
如果前面的脚本执行成功,我们将在tflite_model目录中看到以下文件:
$ ls tflite_model
tflite_graph.pb
tflite_graph.pbtxt
我们还剩下最后一步:将冻结的图形文件转换为.TFLite格式。我们可以使用tflite_convert工具来完成这个操作:
$ tflite_convert --graph_def_file=tflite_model/tflite_graph.pb \
--output_file=tflite_model/cats.tflite \
--input_arrays=normalized_input_image_tensor \
--output_arrays='TFLite_Detection_PostProcess','TFLite_Detection_PostProcess:1','TFLi
te_Detection_PostProcess:2','TFLite_Detection_PostProcess:3' \
--input_shape=1,800,600,3 \
--allow_custom_ops
在这个命令中,请注意我们使用了一些可能一开始不直观的不同参数:
-
对于
--input_arrays,参数简单地表示在预测期间提供的图像将是归一化的float32张量。 -
提供给
--output_arrays的参数表明每个预测中将有四种不同类型的信息:边界框的数量、检测分数、检测类别和边界框本身的坐标。这是可能的,因为我们在上一步的导出图脚本中使用了参数--add_postprocessing_op=true。 -
对于
--input_shape,我们提供与pipeline.config文件中相同的维度值。
其余的都相当琐碎。我们现在应该在tflite_model/目录中有一个全新的cats.tflite模型文件。现在它已经准备好插入到 Android、iOS 或边缘设备中,以进行猫的实时检测。这个模型正在为拯救鲍勃的花园而努力!
提示
在浮点 MobileNet 模型中,'normalized_image_tensor'的值在[-1,1)之间。这通常意味着将每个像素(线性地)映射到一个值在[–1,1]之间。输入图像的值在 0 到 255 之间被缩放为(1/128),然后将-1 的值添加到它们中以确保范围是[-1,1)。
在量化的 MobileNet 模型中,'normalized_image_tensor'的值在[0, 255]之间。
通常,在 TensorFlow Models 存储库的models/research/object_detection/models目录中定义的特征提取器类中查看preprocess函数。
图像分割
在超越目标检测的基础上,为了获得更精确的物体位置,我们可以执行物体分割。正如我们在本章前面看到的那样,这涉及对图像帧中的每个像素进行类别预测。像 U-Net、Mask R-CNN 和 DeepLabV3+这样的架构通常用于执行分割任务。与目标检测类似,有一个越来越流行的趋势是在实时运行分割网络,包括在资源受限的设备上,如智能手机上。实时性为许多消费者应用场景打开了大门,比如面部滤镜(图 14-19),以及工业场景,比如为自动驾驶汽车检测可行驶道路(图 14-20)。

图 14-19。使用 ModiFace 应用程序为头发上色,准确映射属于头发的像素

图 14-20。从行车记录仪(CamVid 数据集)的帧上执行的图像分割
正如你现在可能已经习惯在这本书中听到的,你可以在不需要太多代码的情况下完成很多工作。像 Supervisely、LabelBox 和 Diffgram 这样的标注工具不仅可以帮助标注数据,还可以加载先前注释过的数据并在预训练的对象分割模型上进一步训练。此外,这些工具提供了 AI 辅助标注,可以大大加快(约 10 倍)原本费时费力的昂贵标注过程。如果这听起来令人兴奋,那你很幸运!我们在书的 GitHub 网站上包含了一个额外的资源指南,介绍如何学习和构建分割项目(请参见http://PracticalDeepLearning.ai)。
案例研究
现在让我们看看目标检测是如何被用于推动行业中的实际应用的。
智能冰箱
智能冰箱近几年来越来越受欢迎,因为它们开始变得更加实惠。人们似乎喜欢知道冰箱里有什么,即使他们不在家也能了解。微软与瑞士制造业巨头利勃海尔合作,在其新一代 SmartDeviceBox 冰箱中使用深度学习(图 14-21)。该公司使用 Fast R-CNN 在其冰箱内执行目标检测,以检测不同的食物项目,维护库存,并让用户了解最新状态。

图 14-21。从 SmartDeviceBox 冰箱检测到的对象及其类别(图片来源)
人群计数
人群计数不仅仅是猫在窗户外凝视时才会做的事情。在许多情况下都很有用,包括管理大型体育赛事、政治集会和其他高流量区域的安全和后勤。人群计数,顾名思义,可以用于计算任何类型的对象,包括人和动物。野生动物保护是一个极具挑战性的问题,因为缺乏标记数据和强烈的数据收集策略。
野生动物保护
包括格拉斯哥大学、开普敦大学、芝加哥自然历史博物馆和坦桑尼亚野生动物研究所在内的几个组织联合起来,统计了地球上最大的陆地动物迁徙:在塞伦盖蒂和肯尼亚马赛马拉国家保护区之间的 1,300,000 只蓝色角马和 25 万只斑马(图 14-22)。他们通过 2200 名志愿公民科学家使用 Zooniverse 平台进行数据标记,并使用像 YOLO 这样的自动化目标检测算法来统计角马和斑马的数量。他们观察到志愿者和目标检测算法的计数相差不到 1%。

图 14-22。从一架小型勘测飞机拍摄的角马的航拍照片(图片来源)
昆布梅拉
另一个密集人群计数的例子来自印度的普拉亚格拉吉,大约每 12 年有 2.5 亿人参加昆布梅拉节(图 14-23)。对于如此庞大的人群进行控制一直是困难的。事实上,在 2013 年,由于糟糕的人群管理,一场踩踏事件导致 42 人不幸丧生。
2019 年,当地政府与拉森和图博签约,利用人工智能进行各种后勤任务,包括交通监控、垃圾收集、安全评估以及人群监测。当局使用了一千多个闭路电视摄像头,分析了人群密度,并设计了一个基于固定区域人群密度的警报系统。在人群密度较高的地方,监测更加密集。这使得它成为有史以来建立的最大人群管理系统,进入了吉尼斯世界纪录!

图 14-23。2013 年昆布梅拉,由一名参与者拍摄(图片来源)
Seeing AI 中的人脸检测
微软的 Seeing AI 应用程序(为盲人和低视力社区)提供了实时人脸检测功能,通过该功能,它会告知用户手机摄像头前的人员、他们的相对位置以及与摄像头的距离。一个典型的引导可能会听起来像“左上角有一个脸,距离四英尺”。此外,该应用程序使用人脸检测引导来对已知人脸列表进行人脸识别。如果脸部匹配,它还会宣布该人的姓名。一个语音引导的例子可能是“伊丽莎白靠近顶部边缘,距离三英尺”,如图 14-24 所示。
为了在摄像头视频中识别一个人的位置,系统正在运行一个快速的移动优化对象检测器。然后将这张脸的裁剪图传递给微软认知服务中的年龄、情绪、发型等识别算法进行进一步处理。为了以一种尊重隐私的方式识别人,该应用程序要求用户的朋友和家人拍摄他们脸部的三张自拍照,并生成脸部的特征表示(嵌入),该表示存储在设备上(而不是存储任何图像)。当未来在摄像头实时视频中检测到一张脸时,将计算一个嵌入并与嵌入数据库和相关名称进行比较。这是基于一次性学习的概念,类似于我们在第四章中看到的孪生网络。不存储图像的另一个好处是,即使存储了大量的人脸,应用程序的大小也不会急剧增加。

图 14-24。Seeing AI 上的人脸检测功能
自动驾驶汽车
包括 Waymo、Uber、特斯拉、Cruise、NVIDIA 等在内的几家自动驾驶汽车公司使用目标检测、分割和其他深度学习技术来构建他们的自动驾驶汽车。行人检测、车辆类型识别和速限标志识别等高级驾驶辅助系统(ADAS)是自动驾驶汽车的重要组成部分。
在自动驾驶汽车的决策中,NVIDIA 使用专门的网络来执行不同的任务。例如,WaitNet 是一个用于快速、高级检测交通灯、十字路口和交通标志的神经网络。除了快速外,它还能够抵抗噪音,并能够在雨雪等恶劣条件下可靠工作。WaitNet 检测到的边界框然后被馈送到更详细分类的专门网络中。例如,如果在一帧中检测到交通灯,那么它将被馈送到 LightNet——一个用于检测交通灯形状(圆形与箭头)和状态(红、黄、绿)的网络。此外,如果在一帧中检测到交通标志,它将被馈送到 SignNet,一个用于对美国和欧洲几百种交通标志进行分类的网络,包括停车标志、让行标志、方向信息、速限信息等。从更快的网络到专门网络的级联输出有助于提高性能并模块化不同网络的开发。

图 14-25。使用 NVIDIA Drive 平台在自动驾驶汽车上检测交通灯和标志(图片来源)
总结
在这一章中,我们探讨了计算机视觉任务的类型,包括它们之间的关系以及它们之间的区别。我们深入研究了目标检测,管道的工作原理以及它随时间的演变。然后我们收集数据,标记数据,训练模型(有或没有代码),并将模型部署到移动设备上。接着我们看了看目标检测在工业、政府和非政府组织(NGOs)中如何被使用。然后我们偷偷看了一眼目标分割的世界。目标检测是一个非常强大的工具,其巨大潜力仍在每天在我们生活的许多领域中被发现。你能想到哪些创新的目标检测用途?在 Twitter 上与我们分享@PracticalDLBook。
第十五章:成为创客:探索边缘嵌入式 AI
由客座作者 Sam Sterckval 撰写
您知道如何构建出色的 AI 应用程序,但您想要更多。您不想仅仅在某台计算机上运行 AI 软件,您想将其带到现实世界中。您想要构建设备,使事物更具互动性,使生活更轻松,为人类服务,或者仅仅是为了好玩。也许您想要构建一个互动绘画,当您看着它时会微笑。门口的摄像头在未经授权的人试图偷取包裹时发出响亮的警报。也许是一个可以分类回收物和垃圾的机械臂。在树林中防止盗猎的设备,也许?或者一架可以自主巡视大面积并在洪水期间识别处于困境中的人的无人机。甚至可能是一辆可以自行驾驶的轮椅。您需要的是一个智能电子设备,但您将如何构建它,它将花费多少,它将有多强大?在本章中,我们开始探讨这些问题。
我们将看看如何在嵌入式设备上实现 AI——这是您可能在“创客”项目中使用的设备。创客是具有 DIY 精神的人,他们利用自己的创造力构建新事物。创客通常从业余爱好者开始,他们是热爱解决问题的人、机器人专家、创新者,有时也是企业家。
本章的目的是激发您选择适当设备的能力(这意味着不要试图在微小 CPU 上运行重型 GAN,或者获取一台千核 GPU 来运行“不是热狗”分类器),并尽快轻松地为测试设置它。我们通过探索一些更为人熟知的设备,看看我们如何使用它们来执行我们模型的推理。最后,我们将看看全球的创客们如何利用 AI 来构建机器人项目。
让我们迈出第一步,看看嵌入式 AI 设备的当前情况。
探索嵌入式 AI 设备的现状
在本节中,我们探索一些知名的嵌入式 AI 设备,列在表 15-1 中。在我们进行测试之前,我们将讨论它们的内部工作原理和它们之间的区别。
表 15-1. 设备列表
| Raspberry Pi 4 | 截至目前为止最著名的单板计算机 |
|---|---|
| Intel Movidius NCS2 | 使用 16 核视觉处理单元(VPU)的 USB 加速器 |
| Google Coral USB | 使用自定义的谷歌应用特定集成电路(ASIC)的 USB 加速器 |
| NVIDIA Jetson Nano | 使用 CPU 和 128 核 CUDA GPU 组合的单板计算机 |
| PYNQ-Z2 | 使用 CPU 和 50k CLB 现场可编程门阵列(FPGA)组合的单板计算机 |
我们不想说哪个更好,我们想学习如何为特定项目选择设置。我们通常在早晨上班途中看不到法拉利。更小、更紧凑的汽车更常见,它们同样有效,甚至更好。同样,对于电池供电的无人机来说,使用功能强大的 1000 美元以上的 NVIDIA 2080 Ti GPU 可能过于夸张。以下是我们应该问自己的一些问题,以了解哪种边缘设备最适合我们的需求:
-
设备有多大(例如,与硬币相比)?
-
设备的成本是多少?考虑一下您是否预算有限。
-
设备有多快?它会处理单个 FPS 还是 100 FPS?
-
设备通常需要多少功率(瓦特)?对于电池供电项目,这可能至关重要。
在考虑这些问题的同时,让我们逐个探索图 15-1 中的一些设备。

图 15-1. 嵌入式 AI 设备的家庭照片;从顶部开始,顺时针方向:PYNQ-Z2,Arduino UNO R3,英特尔 Movidius NCS2,树莓派 4,谷歌珊瑚 USB 加速器,NVIDIA Jetson Nano,以及一个欧元硬币用于参考在中间
树莓派
因为我们将讨论面向制造商的嵌入式设备,让我们从与电子项目普遍同义的一个开始:树莓派(图 15-2)。它便宜,易于构建,并且有一个庞大的社区。

图 15-2. 树莓派 4
| 尺寸 | 85.6 毫米 x 56.5 毫米 |
|---|---|
| 价格 | 起价$35 |
| 处理单元 | ARM Cortex-A72 CPU |
| 功率评级 | 15W |
首先,什么是树莓派?它是一台“单板计算机”,这意味着所有计算都可以在一块印刷电路板(PCB)上完成。这款单板计算机的第四版搭载了一个 Broadcom SoC(片上系统),包含(最重要的)一个 ARMv8-A72 四核 CPU,一个 VideoCore VI 3D 单元,以及一些视频解码器和编码器。它配备了不同大小的 RAM,最高可达 4GB。
这个版本在性能上比树莓派 3 有了很大的提升,但效率稍微降低了一些。这是因为树莓派 3 拥有一个 ARMv8-A53 核心,这是高效版本,而 ARMv8-A72 核心(用于第 4 版)是高性能版本。我们可以看到这反映在电源供应建议中,树莓派 3 为 2.5 安培。对于树莓派 4,这变成了 3 安培。还要注意的是,树莓派 4 具有 USB 3 端口,而树莓派 3 只有 USB 2 端口。这将在未来证明是重要信息。
现在让我们来谈谈一些机器学习的内容。无论树莓派 4 变得多么强大,它仍然主要由四个顺序 CPU 核心组成(现在支持乱序执行)。它有 VideoCore VI,但截至目前还没有适用于这种架构的 TensorFlow 版本。Idein Inc.的中村浩一(Koichi Nakamura)构建了一个名为py-videocore的 Python 库,用于访问树莓派 SoC 的四核处理单元(QPUs;类似 GPU 的核心)。他以前用它加速了神经网络,但目前还不能简单加速 TensorFlow。也有用于访问这些核心的 C++库。但正如你可能怀疑的那样,这些核心并不那么强大,所以即使用于加速神经网络,也可能无法产生期望的结果。为了简单起见,我们不会深入研究这些库,因为深入算法已超出本书的范围。而且,正如我们将在接下来看到的,这可能根本不是必要的。
最后,树莓派已被证明是一个非常有用的硬件设备,可用于许多任务。它经常用于教育目的,以及由制造商使用。您会惊讶地发现有多少行业在工业环境中使用树莓派(有一种叫做 netPi 的东西,它只是一个带有坚固外壳的树莓派)。
英特尔 Movidius 神经计算棒
现在让我们直接深入探讨树莓派成为许多项目首选基础的原因之一:英特尔 Movidius 神经计算棒 2(图 15-3),这是英特尔推出的第二代 USB 加速器。

图 15-3. 英特尔神经计算棒 2
| 尺寸 | 72.5 毫米 x 27 毫米 |
|---|---|
| 价格 | $87.99 |
| 处理单元 | Myriad X VPU |
| 功率评级 | 1W |
它的功能非常简单:您提供一个神经网络和一些数据,它会执行推理所需的所有计算。它只通过 USB 连接,因此您可以将其连接到树莓派上,运行 USB 加速器上的推理,并释放树莓派的 CPU 来执行您想要执行的所有其他酷炫操作。
它基于 Myriad VPU。第一代使用了 Myriad 2 VPU,而这一代使用了 Myriad X VPU。VPU 包含 16 个 SHAVE(流式混合架构向量引擎)核心,类似于 GPU 核心,但不太适合图形相关操作。它具有片上 RAM,这在进行神经网络推理时特别有用,因为这些网络在计算时会产生大量数据,这些数据可以直接存储在核心旁边,从而大大降低访问时间。
Google Coral USB 加速器
Google Coral(图 15-4),包含 Google Edge TPU,是我们将讨论的第二个 USB 加速器。首先,解释一下为什么我们要看两种不同的 USB 加速器。如前所述,英特尔棒具有多个 SHAVE 核心,具有多个可用指令,类似于 GPU 核心,因此充当处理单元。另一方面,Google Edge TPU 是一种 ASIC(特定应用集成电路),它也进行一些处理,但只服务于一个目的(因此有“特定”关键字)。ASIC 具有一些硬件固有的属性,其中一些非常好,另一些则不太好:
速度
由于 TPU 内部的所有电子电路只服务于一个目的,因此在解码操作方面没有额外的开销。您输入输入数据和权重,它会几乎立即给出结果。
效率
所有 ASIC 只服务于一个目的,因此不需要额外的能量。ASIC 的性能/瓦特指标通常是业内最高的。
灵活性
ASIC 只能执行其设计用途;在这种情况下,那将是加速 TensorFlow Lite 神经网络。您需要使用 Google Edge TPU 编译器和 8 位.tflite模型。
复杂性
Google 本质上是一家软件公司,它知道如何使事物易于使用。这正是它在这里所做的。Google Coral 非常容易上手。
那么 Google Edge TPU 是如何工作的呢?关于 Edge TPU 本身的信息尚未公开,但有关 Cloud TPU 的信息已经公开,因此可以假设 Edge TPU 的工作方式与 Cloud TPU 大致相同。它具有专用硬件来执行乘法-加法、激活和池化操作。芯片上的所有晶体管都已连接在一起,以便接收权重和输入数据,并以高度并行的方式计算输出。芯片上最大的部分(除了片上内存外)是一个部分,确切地说就是“矩阵乘法单元”。它使用一种相当聪明但并非全新的原则,称为系统执行。这种执行原则通过将中间结果存储在处理元素中而不是存储在内存中来降低内存带宽。

图 15-4。Google Coral USB 加速器
| 尺寸 | 65 毫米 x 30 毫米 |
|---|---|
| 价格 | $74.99 |
| 处理单元 | Google Edge TPU |
| 功率评级 | 2.5W |
注意
我们讨论的两种 USB 加速器之间的主要区别是什么?Google Coral 更强大,但比英特尔 Movidius NCS2(稍微)不太灵活。也就是说,Coral 设置和使用起来要容易得多,尤其是在您训练和转换模型之后。
NVIDIA Jetson Nano
关于另一种硬件:NVIDIA Jetson Nano(图 15-5),一款单板 AI 计算机。有点像树莓派,但配备了一个 128 核 CUDA 启用的 Maxwell GPU。GPU 的添加使其有点类似于带有 Intel NCS2 的树莓派,但 NCS2 有 16 个核心,而这个 Jetson 有 128 个。它还包含什么?一个四核 A57 ARMv8 CPU,是树莓派 4 中 ARMv8 A72 的前身(它比树莓派 4 早几个月),4GB 的低功耗双数据速率 4(LPDDR4)RAM 内存,非常方便地与 CPU 和 GPU 共享,这使您可以在 GPU 上处理数据,而无需复制它。

图 15-5. NVIDIA Jetson Nano
| 尺寸 | 100 毫米 x 79 毫米 |
|---|---|
| 价格 | $99.00 |
| 处理单元 | ARM A57 + 128 核 Maxwell GPU |
| 功率评级 | 10W(在高负载下可能会更高) |
这款单板计算机的特点是,正如之前所说,这些 GPU 核心是 CUDA 启用的,因此是的,它们可以运行 TensorFlow-GPU 或任何其他框架,这将与树莓派相比产生很大的差异,特别是当您计划训练网络时。而且,这对于 GPU 来说价格便宜。目前,Jetson Nano 售价为$99,其中包括一款性能相当高的 ARM CPU 和一个 128 核 GPU。相比之下,带有 4GB 内存的树莓派 4 售价约为$55,Coral USB 加速器约为$75,Movidius NCS2 约为$90。后两者不是独立的,至少需要额外的树莓派才能真正发挥作用,而树莓派没有可以轻松加速深度学习应用的 GPU。
关于 Jetson Nano 的另一个说明:它可以加速默认的 TensorFlow 32 位浮点运算,但如果使用 16 位浮点运算,它将变得更加高效,如果使用其自己的 TensorRT 框架,效率将更高。幸运的是,该公司有一个名为 TensorFlow-TensorRT(TF-TRT)的小型开源库,它将自动加速可用的操作,并允许 TensorFlow 执行其余操作。与 TensorFlow-GPU 相比,这个库提供了大约四倍的加速。考虑到所有这些,这使得 Jetson Nano 成为最灵活的设备。
FPGA + PYNQ
现在是系好安全带的时候了,因为我们即将深入探讨电子领域的黑暗世界。基于 Xilinx Zynq 芯片系列的 PYNQ 平台(图 15-6),在很大程度上与本主题中讨论的其他设备完全不同。如果您稍微研究一下,您会发现它配备了一款双核 ARM-A9 CPU,时钟频率高达 667 MHz。您会想到的第一件事是“你是认真的吗?与树莓派的 1.5 GHz 四核 A72 相比,这太荒谬了!”您是对的,这款设备中的 CPU 在很大程度上是毫无价值的。但它还有另一项功能——一个 FPGA。

图 15-6. Xilinx PYNQ-Z2
| 尺寸 | 140 毫米 x 87 毫米 |
|---|---|
| 价格 | $119.00 |
| 处理单元 | Xilinx Zynq-7020 |
| 功率评级 | 13.8W |
FPGAs
要理解 FPGA 是什么,我们首先需要看一些熟悉的概念。让我们从 CPU 开始:一个知道一系列操作的设备,称为指令集架构(ISA)。这是一个定义 CPU 可以执行的所有操作的集合,通常包含操作,例如“加载字”(字通常是等于 CPU 数据通路大小的位数的值,通常为 32 位或 64 位),它将某个值加载到 CPU 的内部寄存器中,“加法”,可以将两个内部寄存器相加,并将结果存储在第三个寄存器中,例如。
CPU 之所以能够做到这一点,是因为它包含了一大堆晶体管(如果你愿意,可以将其视为电气开关),这些晶体管被硬连线在一起,以便 CPU 自动翻译操作并执行操作的预期目的。软件程序只是这些操作的一个非常长的列表,按照精心考虑的顺序。单核 CPU 将逐个接收操作并执行它们,速度相当惊人。
让我们来看看并行性。正如你所知,神经网络主要由卷积或线性节点组成,这些节点都可以转换为矩阵乘法。如果我们看一下这些操作背后的数学,我们会发现每个层的每个输出点可以独立于其他输出点计算,如图 15-7 所示。

图 15-7。矩阵乘法
我们现在可以看到所有这些操作实际上都可以以并行方式完成。我们的单核 CPU 无法做到这一点,因为它一次只能执行一个操作,但这就是多核 CPU 的用武之地。四核 CPU 可以同时执行四个操作,这意味着理论上的加速四倍。英特尔 Movidius NCS2 中的 SHAVE 核和 Jetson Nano 中的 CUDA 核可能不像 CPU 核心那样复杂,但它们足够好用于这些乘法和加法,而 NCS2 有 16 个,Jetson Nano 有 128 个,而像 RTX 2080 Ti 这样的大型 GPU 甚至有 4352 个 CUDA 核。现在很容易看出为什么 GPU 在执行深度学习任务时比 CPU 更好。
让我们回到 FPGAs。而 CPU 和 GPU 是巨大的晶体管集合,硬连线执行一组指令,你可以把 FPGA 想象成同样巨大的晶体管集合,但没有连线。你可以选择它们如何连线,并且随时重新连线,使它们可重构。你可以把它们连线成为 CPU。你也可以找到人们将它们连线成为 GPU 的原理图和项目。但最有趣的是,它们甚至可以被连线成为与你的深度学习神经网络完全相同的架构,这实际上使它们成为你网络的物理实现。这里故意使用了“连线”一词。通常,你会发现人们用“程序”这个词来谈论这种配置,但这可能会让人困惑。你所做的是重新配置实际的硬件,不像 CPU 或 GPU 那样下载一个硬件可以运行的程序。
PYNQ 平台
我们通常将 FPGA 的“连线”文件称为比特流或位图,因为你刷入芯片的内容基本上只是应该建立的连接的地图。正如你可以想象的那样,制作这些比特流比运行 Python 脚本要复杂得多。这就是 PYNQ 的用武之地。它的标语是“Zynq 的 Python 生产力”。该公司安装了 PYNQ 图像,自动在芯片内部的 ARM 上运行 Jupyter Notebook,并附带一些基本的比特流;然而,未来可能会有更多的比特流可用。在 PYNQ 世界中,这些比特流被称为叠加。
如果你寻找示例,你很快会找到一个名为“BNN-PYNQ”的示例,其中有一个简单的 VGG 风格、六层 CNN,可以在 PYNQ-Z1 和 Z2 上以大约 3000 FPS 运行,在 ZCU104 上接近 10000 FPS,该芯片板载了 Zynq 芯片的 Ultrascale+版本。这些数字看起来相当疯狂,但要考虑到它们是在 32x32 像素图像上运行,而不是通常的 224x224 像素图像,并且这个网络是“二值化”的,这意味着它的权重和激活只有一位,而不是 TensorFlow 的 32 位。为了更好地比较性能,我尝试在 Keras 中重新创建一个类似的网络。
我构建了 FP32 模型并在 CIFAR-10 数据集上对其进行了训练。 CIFAR-10 数据集可以在 Keras 中轻松使用keras.datasets.cifar10获得。 FP32 模型达到了大约 17%的错误率,令人惊讶的是,仅比二进制模型好了 2%。在 Intel i9 八核 CPU 上,推理速度约为 132 FPS。据我所知,在 CPU 上没有一种简单有效地使用二进制网络的方法——您需要深入研究一些特定的 Python 软件包或一些 C 代码,以充分利用硬件。您可能会实现三到五倍的加速。然而,这仍然远远不及低端 FPGA,并且 CPU 通常会在这样做时消耗更多电力。当然,一切都有两面性,对于 FPGA 来说,这必须是设计的复杂性。存在开源框架,即由 Xilinx 支持的FINN框架。其他制造商提供额外的框架,但没有一个能与 TensorFlow 等易于使用的软件包和框架相提并论。为 FPGA 设计神经网络还涉及大量的电子知识,因此超出了本书的范围。
Arduino
Arduino(图 15-8)在通过传感器和执行器与现实世界进行交互时可以让您的生活变得更加轻松。
微控制器单元(MCUs)Arduino 是围绕 8 位高级虚拟 RISC(AVR)ATMega328p 微控制器构建的微控制器开发板,运行速度为 16 MHz(所以是的,我们不要试图在上面运行一个体面的神经网络)。微控制器是每个人都接触过的东西——您几乎可以在稍微电子化的所有东西中找到微控制器,从每个屏幕/电视到您的桌面键盘,甚至自行车灯甚至可能包含微控制器以启用闪烁模式。
这些板子有各种形状,大小和性能,尽管 Arduino UNO 中的 MCU 性能相对较低,但更强大的 MCU 可用。 ARM Cortex-M 系列可能是最知名的。最近,TensorFlow 和 uTensor(一种针对 MCU 的极轻量级机器学习推理框架)联手,使得在这些 MCU 上运行 TensorFlow 模型成为可能。

图 15-8. Arduino UNO R3
| 尺寸 | 68.6 毫米 x 53.3 毫米 |
|---|---|
| 价格 | 少于 10.00 美元 |
| 处理单元 | AVR ATMega328p |
| 功率评级 | 0.3 瓦特 |
尽管 Arduino UNO 实际上不适用于执行大量计算,但它具有许多其他优点,使其成为制作者工作台上不可或缺的一部分。它便宜,简单,可靠,并且周围有一个庞大的社区。大多数传感器和执行器都有某种 Arduino 库和盾牌,这使得它成为一个非常容易与之交互的平台。AVR 架构是一种有点过时的 8 位修改的哈佛架构,但学习起来非常容易,尤其是在其顶部的 Arduino 框架上。围绕 Arduino 的庞大社区显然带来了许多教程,因此只需查找您选择的传感器的任何 Arduino 教程,您肯定会找到您项目所需的内容。
提示
使用 Python 的串行库,您可以通过 USB 电缆使用通用异步收发器(UART)轻松地与 Arduino 进行接口,以便来回发送命令或数据。
嵌入式 AI 设备的定性比较
表 15-2 总结了我们谈论过的平台,图 15-9 绘制了每个设备的性能与复杂性之间的关系。
表 15-2. 测试平台的优缺点
| 嵌入式设备 | 优点 | 缺点 |
|---|---|---|
| 树莓派 4 |
-
简单
-
庞大的社区
-
易得
-
便宜
|
- 缺乏计算能力
|
| Intel Movidius NCS2 |
|---|
-
简单优化
-
支持大多数平台
|
-
加速而言相对昂贵
-
不那么强大
|
| Google Coral USB |
|---|
-
易于入门
-
巨大的加速
-
价格与加速比
|
-
仅支持.tflite
-
需要八位量化
|
| NVIDIA Jetson Nano |
|---|
-
一体化
-
训练是可能的
-
便宜
-
实现良好性能真的很容易
-
CUDA GPU
|
-
性能可能仍然不足
-
高级优化可能会很复杂
|
| PYNQ-Z2 |
|---|
-
潜在的巨大功率效率
-
实际硬件设计
-
潜在的巨大性能
-
比 Jetson Nano 更加多功能
|
-
非常复杂
-
设计流程长
|

图 15-9。性能与使用复杂性之间的关系(圆圈的大小代表价格)
在本节中,我们尝试通过使用 MobileNetV2 对相同图像进行 250 次分类来测试一些平台,顶部平台经过 ImageNet 数据集训练。我们每次使用相同的图像,因为这将使我们能够消除在系统中可能发生的数据瓶颈的一部分,这些系统没有足够的 RAM 来存储所有权重和许多不同的图像。
好的,让我们先找一张图片。谁不喜欢猫呢?所以,让我们有一张美丽猫的图片(图 15-10),它的尺寸正好是 224 x 224 像素,这样我们就不需要调整图像。

图 15-10。我们将用于实验的猫的图像
拥有基准测试总是不错的,所以让我们先在 PC 上运行模型。第二章解释了如何做到这一点,所以让我们继续编写一个脚本,该脚本将调用预测 250 次,并测量这需要多长时间。您可以在本书的 GitHub 网站上找到完整的脚本(请参阅http://PracticalDeepLearning.ai)位于code/chapter-15:
$ python3 benchmark.py
Using TensorFlow backend.
tf version : 1.14.0
keras version : 2.2.4
input tensor shape : (1, 224, 224, 3)
warmup prediction
[('n02124075', 'Egyptian_cat', 0.6629321)]
starting now...
Time[s] : 16.704
FPS : 14.968
Model saved.
既然我们已经建立了基准测试,现在是时候开始研究如何在嵌入式设备上运行这个模型,看看我们是否以及如何将性能提升到一个有用的水平。
与树莓派一起动手
我们从先前讨论的设备中最知名和最基本的设备开始:树莓派(简称 RPi)。
我们首先安装 Raspbian,这是专为 Raspberry Pi 制作的 Linux 变体。为了简洁起见,让我们假设我们有一个已安装、更新、连接并准备就绪的 Raspberry Pi。我们可以直接在 Pi 上安装 TensorFlow,这应该可以使用pip安装程序完成:
$ pip3 install tensorflow
注意
由于最近切换到 Raspbian Buster,我们遇到了一些问题,通过使用以下piwheel解决了这些问题:
$ pip3 install
https://www.piwheels.org/simple/tensorflow/tensorflow
-1.13.1-cp37-none-linux_armv7l.whl
使用pip安装 Keras 很简单,但不要忘记安装libhdf5-dev,如果您想要将权重加载到神经网络中,则会需要它:
$ pip3 install keras
$ apt install libhdf5-dev
在 RPi 上安装 OpenCV(在代码中称为 cv2)可能是一种负担(特别是在最近切换操作系统后),我们可以使用 PIL 加载图像而不是 OpenCV。这意味着用 PIL 替换 cv2 的导入,并更改代码以使用此库加载图像:
#input_image = cv2.imread(input_image_path)
#input_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)
input_image = PIL.Image.open(input_image_path)
input_image = np.asarray(input_image)
提示
与 OpenCV 不同,PIL 以 RGB 格式加载图像,因此不再需要从 BGR 转换为 RGB。
就是这样!现在应该可以在您的 Raspberry Pi 上运行完全相同的脚本:
$ python3 benchmark.py
Using TensorFlow backend.
tf version : 1.14.0
keras version : 2.2.4
input tensor shape : (1, 224, 224, 3)
warmup prediction
[('n02124075', 'Egyptian_cat', 0.6629321)]
starting now...
Time[s] : 91.041
FPS : 2.747
正如您所看到的,它运行速度慢得多,下降到不到三帧每秒。是时候考虑如何加快速度了。
我们可以做的第一件事是看看 TensorFlow Lite。TensorFlow 内置了一个转换器,让我们可以轻松地将任何模型转换为 TensorFlow Lite(.tflite)模型。
我们继续编写一个小脚本,与原始基准测试脚本非常相似,它将设置好一切以使用 TensorFlow Lite 模型并运行 250 次预测。当然,这个脚本也可以在本书的 GitHub 网站上找到(参见http://PracticalDeepLearning.ai)。
让我们继续运行这个脚本,以便评估我们加快速度的努力。
$ python3 benchmark_tflite.py
Using TensorFlow backend.
input tensor shape : (1, 224, 224, 3)
conversion to tflite is done
INFO: Initialized TensorFlow Lite runtime.
[[('n02124075', 'Egyptian_cat', 0.68769807)]]
starting now (tflite)...
Time[ms] : 32.152
FPS : 7.775
注意
Google Coral 网站提供了可使用 TensorFlow Lite 运行的 8 位量化 CPU 模型。
对于树莓派 4,我们观察到速度增加了三倍(参见 Table 15-3)。对于一个快速尝试来说,这还不错,但我们仍然只达到了 7.8 FPS,对于许多应用来说已经足够好了。但是如果我们最初的计划是在实时视频上做一些事情呢?那么这还不够。对模型进行量化可以实现更高的性能,但鉴于树莓派的 CPU 并未针对 8 位整数运算进行优化,这只会带来一点点性能提升。
表 15-3。树莓派基准测试
| 设置 | FPS |
|---|---|
| Raspberry Pi 4 (tflite, 8-bit) | 8.6 |
| Raspberry Pi 4 (tflite) | 7.8 |
| Raspberry Pi 3B+ (tflite, 8-bit) | 4.2 |
| Raspberry Pi 4 | 2.7 |
| Raspberry Pi 3B+ | 2.1 |
那么我们如何进一步加快速度,使其对自动赛车或自动无人机等应用变得更有用呢?我们之前提到过:USB 加速器。
使用 Google Coral USB 加速器加速
没错,这些加速器现在非常方便。我们的硬件设置可以保持完全不变,只需要稍微添加一点内容。那么让我们看看:如何让 Google Coral USB 加速器在树莓派上运行,并且是否可以加快速度?
首先要做的事情。假设我们手头有一个 USB 加速器,我们应该做的第一件事是从供应商那里找到一个“入门指南”。查看https://coral.withgoogle.com/,转到Docs>USB Accelerator>Get Started,您将在几分钟内开始运行。
截至目前,Coral USB 加速器尚未完全兼容树莓派 4,但在调整install.sh脚本后,很容易让它运行起来。您只需要简单地在安装脚本中添加一部分,以便它识别 Pi 4,如图 15-11 所示。

图 15-11。Google Coral 安装脚本更改
现在install.sh脚本将正确运行。然后,我们需要重命名一个.so文件,以便它也可以在 Python 3.7 中运行。使用 Unix 的复制命令来完成这一步:
$ sudo cp /usr/local/lib/python3.7/dist-
packages/edgetpu/swig/_edgetpu_cpp_wrapper.cpython-
35m-arm-linux-gnueabihf.so
/usr/local/lib/python3.7/dist-
packages/edgetpu/swig/_edgetpu_cpp_wrapper.cpython-
37m-arm-linux-gnueabihf.so
完成这些步骤后,一切应该正常工作,Google 的演示应该可以运行。
现在我们已经让 Coral 运行起来了,让我们再次尝试对同一个 MobileNetV2 模型进行基准测试。但这次,必须使用它的量化版本,因为 Google 的 Edge TPU 仅支持 8 位整数运算。Google 提供了经过量化和转换的 MobileNetV2 模型,可以直接在 Edge TPU 上使用。我们可以从 Coral 网站下载它,路径为Resources > See pre-compiled models > MobileNetV2(ImageNet) > Edge TPU Model。
注意
如果您想为 Edge TPU 创建、训练、量化和转换自己的模型,这需要更多工作,不能使用 Keras 完成。您可以在Google Coral 网站上找到如何操作的信息。
我们有一个可用的 USB 加速器和一个可以在其上运行的模型。现在,让我们制作一个新的脚本来测试其性能。我们即将制作的文件与之前的文件非常相似,并且很多内容直接来自 Google 提供的 Coral 示例。而且,像往常一样,这个文件可以在 GitHub 网站上下载(请参见http://PracticalDeepLearning.ai):
$ python3 benchmark_edgetpu.py
INFO: Initialized TensorFlow Lite runtime.
warmup prediction
Egyptian cat
0.59375
starting now (Edge TPU)...
Time[s] : 1.042
FPS : 240.380
是的,没错。它已经完成了 250 次分类!对于带有 USB3 的 Raspberry Pi 4,只需要 1.04 秒,这相当于 240.38 FPS!现在这才是加速。正如您可以期待的那样,使用这些速度,实时视频将毫无问题。
有很多预编译的模型可用,用于各种不同的目的,所以请查看它们。您可能会找到一个适合您需求的模型,这样您就不需要经历创建、训练、量化和转换自己的流程(或挣扎)。
Raspberry Pi 3 没有 USB3 端口,只有 USB2。我们可以在表 15-4 中清楚地看到这为 Coral 创建了一个数据瓶颈。
表 15-4. Google Coral 基准测试
| 设置 | FPS |
|---|---|
| i7-7700K + Coral(tflite,8 位) | 352.1 |
| Raspberry Pi 4 + Coral(tflite,8 位) | 240.4 |
| Jetson Nano + Coral(tflite,8 位) | 223.2 |
| RPi3 + Coral(tflite,8 位) | 75.5 |
移植到 NVIDIA Jetson Nano
那个 USB 加速器很好,但是如果您的项目需要一个真正的特定模型,该模型在一些疯狂的、自制的数据集上进行了训练呢?正如我们之前所说,为 Coral Edge TPU 创建一个新模型要比创建一个 Keras 模型要复杂得多。那么,在边缘上运行具有良好性能的自定义模型是否有简单的方法?NVIDIA 来拯救!通过其 Jetson Nano,该公司已经为 Raspberry Pi 创建了一个替代品,它具有启用 CUDA 的、相当高效的 GPU,让您不仅可以加速 TensorFlow,还可以加速任何您喜欢的东西。
同样,我们必须首先安装所有所需的软件包。首先,您需要下载 NVIDIA 的 JetPack 副本,这是其针对 Jetson 平台的类似 Debian 的操作系统版本。然后,为了安装 TensorFlow 和所需的软件包,NVIDIA 指导我们如何在这里进行操作。
注意
请注意,pip3存在一个已知问题:“无法导入名称‘main’”。
这是解决方案:
$ sudo apt install nano
$ sudo nano /usr/bin/pip3
Replace : main => __main__
Replace : main() => __main__._main()
提示
更新所有pip软件包可能需要很长时间;如果您想确保 Jetson 没有冻结,您可能首先想要安装htop:
$ sudo apt install htop
$ htop
这让您可以监视 CPU 利用率。只要这个工作正常,您的 Jetson 也是如此。
注意
Jetson Nano 在编译时往往会变得很热,因此我们建议使用某种风扇。开发板有一个连接器,但请记住这会向风扇提供 5V 电压,而大多数风扇的额定电压为 12V。
一些 12V 风扇可能可以直接解决问题,有些可能需要一点推动才能启动。您将需要一个 40 毫米 x 40 毫米的风扇。也有 5V 版本可用。
不幸的是,NVIDIA 的操作指南并不像 Google 的那么简单。您可能会经历一些挣扎,偶尔会有翻桌的时刻。
但请坚持下去;您最终会成功的。
提示
在 Jetson 上安装 Keras 需要 scipy,它需要libatlas-base-dev和gfortran,因此首先安装后者,然后再前进:
$ sudo apt install libatlas-base-dev gfortran
$ sudo pip3 install scipy
$ sudo pip3 install keras
一切都完成后,我们可以选择直接运行benchmark.py文件,由于 GPU 的存在,速度比在 Raspberry Pi 上快得多:
$ python3 benchmark.py
Using TensorFlow backend.
tf version : 1.14.0
keras version : 2.2.4
input tensor shape : (1, 224, 224, 3)
warmup prediction
[('n02124075', 'Egyptian_cat', 0.6629321)]
starting now...
Time[s] : 20.520
FPS : 12.177
这立即展示了 Jetson Nano 的强大之处:它几乎与带有 GPU 的任何其他 PC 一样运行。但是,12 FPS 仍然不算很高,因此让我们看看如何优化这一点。
提示
您还可以将 Google Coral 连接到 Jetson Nano 上,这样可以同时在 Coral 上运行一个模型,在 GPU 上运行另一个模型的可能性就打开了。
Jetson 的 GPU 实际上是专门用于 16 位浮点运算的,因此,这将是精度和性能之间的最佳折衷。如前所述,NVIDIA 有一个名为 TF-TRT 的软件包,可以促进优化和转换。然而,这仍然比 Coral 示例复杂一些(请记住,谷歌为我们提供了 Coral 的预编译模型文件)。您需要冻结 Keras 模型,然后创建一个 TF-TRT 推理图。如果您需要在 GitHub 上找到所有内容,这可能需要一段时间,因此它已捆绑在本书的 GitHub 网站上(请参见http://PracticalDeepLearning.ai)。
您可以使用tftrt_helper.py文件来优化自己的模型,以便在 Jetson Nano 上进行推理。它实际上只是冻结 Keras 模型,删除训练节点,然后使用 NVIDIA 的 TF-TRT Python 软件包(包含在 TensorFlow Contrib 中)来优化模型。
$ python3 benchmark_jetson.py
FrozenGraph build.
TF-TRT model ready to rumble!
input tensor shape : (1, 224, 224, 3)
[[('n02124075', 'Egyptian_cat', 0.66293204)]]
starting now (Jetson Nano)...
Time[s] : 5.124
FPS : 48.834
48 FPS,仅几行代码就实现了四倍的加速。令人兴奋,不是吗?您可以在自己定制的模型上实现类似的加速,以满足项目特定需求;这些性能基准可以在表 15-5 中找到。
表 15-5. NVIDIA Jetson Nano 基准测试
| 设置 | FPS |
|---|---|
| Jetson Nano + Coral(tflite,8 位) | 223.2 |
| Jetson Nano(TF-TRT,16 位) | 48.8 |
| Jetson Nano 128CUDA | 12.2 |
| Jetson Nano(tflite,8 位) | 11.3 |
比较边缘设备的性能
表 15-6 显示了运行 MobileNetV2 模型的几个边缘设备的定量比较。
表 15-6. 完整的基准测试结果
| 设置 | FPS |
|---|---|
| i7-7700K + Coral(tflite,8 位) | 352.1 |
| i7-7700K + GTX1080 2560CUDA | 304.9 |
| 树莓派 4 + Coral | 240.4 |
| Jetson Nano + Coral(tflite,8 位) | 223.2 |
| RPi3 + Coral(tflite,8 位) | 75.5 |
| Jetson Nano(TF-TRT,16 位) | 48.8 |
| i7-7700K(tflite,8 位) | 32.4 |
| i9-9880HQ(2019 款 MacBook Pro) | 15.0 |
| Jetson Nano 128CUDA | 12.2 |
| Jetson Nano(tflite,8 位) | 11.3 |
| i7-4870HQ(2014 款 MacBook Pro) | 11.1 |
| Jetson Nano(tflite,8 位) | 10.9 |
| 树莓派 4(tflite,8 位) | 8.6 |
| 树莓派 4(tflite) | 7.8 |
| RPi3B+(tflite,8 位) | 4.2 |
| 树莓派 4 | 2.7 |
| RPi3B+ | 2.1 |
我们可以总结这些实验的要点如下:
-
良好的优化会产生很大的影响。计算优化领域仍在迅速发展,我们将在未来几年看到一些重大变化。
-
在谈论 AI 的计算单元时,更多总是更好的。
-
在性能/瓦特方面,ASIC > FPGA > GPU > CPU。
-
TensorFlow Lite 非常棒,尤其适用于小型 CPU。请记住,它专门针对小型 CPU,对于 x64 机器使用将不那么有趣。
案例研究
这一章是关于制造者的。没有展示他们制造的东西,这一章就是不完整的。让我们看一下通过在边缘运行 AI 所创造的一些东西。
JetBot
NVIDIA 一直处于深度学习革命的前沿,为研究人员和开发人员提供强大的硬件和软件。2019 年,他们迈出了下一步,也为制造者们提供了 Jetson Nano。我们已经了解了这款硬件的强大之处,现在是时候用它来构建一些东西了,比如 DIY 微型汽车。幸运的是,NVIDIA 为我们提供了 JetBot(图 15-12),这是一个开源机器人,可以由 Jetson Nano 控制。
JetBot 维基提供了适合初学者的逐步构建说明。以下是一个高层次的概述:
-
购买材料清单中指定的零件,如电机、转向器、摄像头和 WiFi 天线。除了 99 美元的 Jetson Nano 外,还有大约 30 个零件,总成本约为 150 美元。
-
现在是时候发挥我们内心的麦克盖尔,拿起一把钳子和一个十字螺丝刀,组装这些零件。最终结果应该类似于图 15-12。
-
接下来,我们将设置软件。这涉及将 JetBot 镜像刷入 SD 卡,启动 Jetson Nano,将其连接到 WiFi,最后通过 Web 浏览器连接到我们的 JetBot。
-
最后,我们运行提供的示例笔记本。提供的笔记本使我们能够通过少量代码不仅可以从 Web 浏览器控制机器人,还可以使用其摄像头从流视频中收集训练数据,在设备上训练深度学习模型(毕竟 Jetson Nano 是一个迷你 GPU),并将其用于避开障碍物和跟随对象(如人或球)等任务。

图 15-12. NVIDIA JetBot
制造商已经在这个基础框架上扩展了 JetBot 的功能,例如将激光雷达传感器连接到 JetBot 上,以更好地理解环境,将 JetBot 带到不同的物理形式,如坦克、Roomba 吸尘器(JetRoomba)、跟随物体的蜘蛛(JetSpider)等。因为所有道路最终都通向赛车,Jetson Nano 团队最终发布了一个类似的开源配方书,用于名为 JetRacer 的赛车。它基于更快的底盘、更高的摄像头帧率,并针对推理进行了优化(使用 TensorRT)以处理速度。最终结果:JetRacer 已经在 DIY Robocars 聚会等活动中进行比赛。
这里最慢的步骤是...等待硬件到货。
蹲下换取地铁票
当健康不能激励人们锻炼时,还有什么?答案是...免费的火车票!为了提高公众对其不断增长的肥胖危机的认识,墨西哥城在地铁站安装了带摄像头的售票机,提供免费车票,但只有在你锻炼时才能获得。十个深蹲可以获得一张免费车票(图 15-13)。莫斯科也在其地铁站实施了类似的系统,但显然,那里的官员对每个人要求 30 个深蹲。
受到这些启发,我们可以预见如何构建我们自己的“深蹲跟踪器”。我们可以通过多种方式实现这一目标,最简单的方法是用“深蹲”和“不深蹲”这两个类别训练我们自己的分类器。这显然需要构建一个包含成千上万张人们蹲下或不蹲下的图片的数据集。
更好的方法是运行 PoseNet(跟踪身体关节,正如我们在第十章中看到的)在 Google Coral USB 加速器上。借助其 10 多 FPS,我们可以跟踪髋部点下降到足够低的次数,并更准确地计数。我们所需要的只是一个树莓派、一个 Pi 摄像头、一个 Coral USB 加速器和一个公共交通运营商提供一个车票打印机和无尽的免费车票,我们就可以开始让我们的城市变得更健康。

图 15-13. 蹲下换取车票
黄瓜分类器
日本黄瓜农场主的儿子小池真琴观察到,他的父母在收获后花费了大量时间分类黄瓜。这种分类需要根据非常小的特征(如微小的纹理差异、微小的划痕和刺,以及更大的属性,如大小、厚度和曲率)将黄瓜分成九类。这个系统很复杂,雇佣兼职工人几乎是不可能的,因为培训他们需要花费很长时间。他不能袖手旁观,看着父母每天超过八个小时地进行这种劳动密集型的任务。
我们还没有提到的一个细节是,小池先生曾是丰田的嵌入式系统设计师。他意识到很多视觉检查可以利用深度学习的力量自动化。他拍摄了超过 7000 张他母亲在三个月内手动分类的不同黄瓜的照片。然后,他训练了一个分类器,可以查看黄瓜的照片,并以高度准确性预测它属于哪个类别。但一个分类器现在单独做不了什么,对吧?
利用他在嵌入式系统方面的经验,他将一个分类器部署到一个连接了摄像头、传送带系统和分类手臂的树莓派上,根据树莓派的预测将每个黄瓜推入多个箱子中的一个(图 15-14)。树莓派首先运行一个小黄瓜/非黄瓜分类器。对于被分类为黄瓜的图像,树莓派会将图像发送到一个运行更复杂的多类别黄瓜分类器的服务器。请记住,这是在 2015 年,就在 TensorFlow 发布不久之后(远在 TensorFlow Lite、MobileNet 以及当前树莓派 4 甚至 3 的强大功能之前)。有了这样一台机器,他的父母可以花更多时间在实际的农业上,而不是整天在已经收获的黄瓜中进行分类。

图 15-14. 小池真的黄瓜分类机(图片来源)
尽管关于人工智能接管工作的辩论每天都出现在新闻中,但小池先生的故事真正突出了人工智能提供的真正价值:使人类更加高效,增强我们的能力,同时让他的父母感到骄傲。
进一步探索
成为一个创客时要关注的两个同等重要的方面是硬件和软件。一个缺一不可。有更多的 DIY 项目可以探索,一个人一辈子也无法完成。但关键是找到一些你热衷的项目,从头到尾构建它们,当它们开始真正运作时感受到兴奋。逐渐地,你会积累知识,下次阅读一个项目时,你可以猜测如何自己构建它。
开始的最佳方式是动手并构建更多项目。一些优秀的灵感来源包括Hackster.io、Hackaday.io和Instructables.com,它们涵盖了各种项目主题、平台和技能水平(从初学者到高级),通常还包括逐步说明。
为了减少初学者组装硬件的开销,市场上有几种套件可用。例如,树莓派的 AI 套件包括 AIY Vision Kit(谷歌)、AIY Speech Kit(谷歌)、GoPiGo(Dexter Industries)和 DuckieTown 平台(麻省理工学院),等等。它们降低了开始电子硬件项目的门槛,使这个新领域更易接近,因此对每个人都可用,从高中生到快速原型制作者。
在软件方面,让低功耗设备上的人工智能运行更快意味着使模型更加高效。像量化和修剪这样的模型压缩技术(如第六章中提到的)可以帮助我们实现这一目标。为了更快地运行(牺牲一些准确性),BNNs 代表模型使用一位而不是正常的 32 位,是一个潜在的解决方案。像 XNOR.ai 和微软的嵌入式学习库这样的公司让我们能够制作这样的模型。对于在内存少于 100 KB 的微控制器等更低功耗设备上运行人工智能,Pete Warden 的书籍TinyML是一个优秀的学习资源。
总结
在本章中,我们介绍了一些知名的嵌入式人工智能设备,并看了看如何在其中一些设备上运行 Keras 模型。我们从高层次的视角介绍了它们的内部运作方式,以及它们如何实现运行神经网络所需的计算能力。最终,我们对它们进行基准测试,以便根据项目的大小、成本、延迟和功耗需求来开发直觉。从平台到机器人项目,我们看了看全球创客们如何利用它们来构建电子项目的一些方法。本章讨论的设备显然只是众多可用设备中的一小部分。随着边缘成为人工智能更受欢迎的地方,新设备将不断涌现。
借助边缘人工智能的力量,创客们可以想象并实现他们的梦想项目,这些项目在几年前可能被认为是科幻小说。在这个 DIY 人工智能世界中的旅程中,请记住,你是一个与许多开放和愿意分享的修补者紧密联系在一起的社区中的一员。论坛很活跃,所以如果你遇到问题,总会有乐意帮助的人可以求助。希望通过你的项目,你可以激励其他人成为创客。
第十六章:使用 Keras 进行端到端深度学习模拟自动驾驶汽车
由客座作者 Aditya Sharma 和 Mitchell Spryn 贡献
从蝙蝠车到骑士骑手,从机器人出租车到自动送披萨,自动驾驶汽车已经吸引了现代流行文化和主流媒体的想象力。为什么不呢?科幻概念何时会变为现实?更重要的是,自动驾驶汽车承诺解决当今城市社会面临的一些关键挑战:道路事故、污染水平、城市越来越拥挤、在交通中浪费的生产力小时数等等。
毫无疑问,一个完整的自动驾驶系统构建起来相当复杂,不可能在一章中解决。就像一个洋葱一样,任何复杂的问题都包含可以剥离的层。在我们的情况下,我们打算解决一个基本问题:如何转向。更好的是,我们不需要一辆真正的汽车来做到这一点。我们将训练一个神经网络在模拟环境中自主驾驶我们的汽车,使用逼真的物理和视觉效果。
但首先,简短的历史。

图 16-1。SAE 驾驶自动化级别(图片来源)
自动驾驶的简史
尽管自动驾驶似乎是一个非常近期的技术发展,但这个领域的研究起源于 30 多年前。卡内基梅隆大学的科学家们在 1984 年首次尝试了这个看似不可能的壮举,当时他们开始研究后来被称为 Navlab 1 的项目。 “Navlab”是卡内基梅隆大学导航实验室的简称,这个名字并不花哨,但它将成为人类未来非常令人印象深刻的第一步。Navlab 1 是一辆雪佛兰面包车,车上装有硬件机架,其中包括研究团队的工作站和 Sun Microsystems 超级计算机等设备。当时它被认为是尖端技术,是一个自包含的移动计算系统。当时远程无线通信几乎不存在,云计算甚至还不是一个概念。把整个研究团队放在一辆高度实验性的自动驾驶面包车后面可能看起来像是一个危险的想法,但 Navlab 的最高时速只有 20 英里,只在空旷的道路上行驶。这确保了工作在车上的科学家们的安全。但非常令人印象深刻的是,这辆车使用的技术为今天的自动驾驶汽车所用的技术奠定了基础。例如,Navlab 1 使用视频摄像头和测距仪(激光雷达的早期版本)来观察周围环境。为了导航,他们使用了单层神经网络来根据传感器数据预测转向角度。相当不错,不是吗?

图 16-2。NavLab 1 的全貌(图片来源)
深度学习、自动驾驶和数据问题
即使 35 年前科学家们就知道神经网络将在实现自动驾驶汽车方面发挥关键作用。当时,当然我们没有所需的技术(GPU、云计算、FPGA 等)来训练和部署大规模的深度神经网络,以使自动驾驶成为现实。今天的自动驾驶汽车使用深度学习来执行各种任务。表 16-1 列出了一些示例。
表 16-1。深度学习在自动驾驶汽车中的应用示例
| 任务 | 示例 |
|---|---|
| 感知 | 检测可驾驶区域、车道标线、交叉口、人行横道等 |
| 检测其他车辆、行人、动物、道路上的物体等 | |
| 检测交通标志和信号 | |
| 导航 | 根据传感器输入预测和输出转向角、油门等 |
| 进行超车、变道、掉头等操作 | |
| 进行并线、出口、穿越环岛等操作 | |
| 在隧道、桥梁等地方驾驶 | |
| 响应交通违规者、逆行驾驶者、意外环境变化等 | |
| 穿梭于交通拥堵中 |
我们知道深度学习需要数据。事实上,一个经验法则是更多的数据可以确保更好的模型性能。在之前的章节中,我们看到训练图像分类器可能需要数百万张图像。想象一下训练一辆汽车自动驾驶需要多少数据。当涉及到验证和测试所需的数据时,自动驾驶汽车的情况也非常不同。汽车不仅必须能够行驶,还必须安全行驶。因此,测试和验证模型性能是基本关键的,需要比传统深度学习问题所需的数据多得多。然而,准确预测所需数据的确切数量是困难的,估计各不相同。一项 2016 年的研究显示,一辆汽车要想达到与人类驾驶员一样的水平,需要行驶 110 亿英里。对于一队 100 辆自动驾驶汽车,每天 24 小时收集数据,以 25 英里每小时的平均速度行驶,这将需要超过 500 年!
当然,通过让车队在现实世界中行驶 110 亿英里来收集数据是非常不切实际和昂贵的。这就是为什么几乎每个在这个领域工作的人——无论是大型汽车制造商还是初创公司——都使用模拟器来收集数据和验证训练模型。例如,Waymo(谷歌的自动驾驶汽车团队)截至 2018 年底在道路上行驶了近 1000 万英里。尽管这比地球上任何其他公司都多,但它仅占我们 110 亿英里的不到 1%。另一方面,Waymo 在模拟中行驶了 70 亿英里。尽管每个人都使用模拟进行验证,但有些公司构建自己的模拟工具,而其他公司则从 Cognata、Applied Intuition 和 AVSimulation 等公司许可。还有一些很棒的开源模拟工具可用:来自微软的AirSim,来自英特尔的Carla,以及来自百度的Apollo 模拟。由于有了这些工具,我们不需要连接我们汽车的 CAN 总线来学习让它自动驾驶的科学。
在本章中,我们使用了一个专为微软的自动驾驶食谱定制的 AirSim 版本。
自动驾驶的“Hello, World!”:在模拟环境中操纵方向盘
在这一部分,我们实现了自动驾驶的“Hello, World!”问题。自动驾驶汽车是复杂的,有数十个传感器传输着大量数据,同时在行驶中做出多个决策。就像编程一样,对于自动驾驶汽车的“Hello, World!”,我们将要求简化为基本原理:
-
汽车始终保持在道路上。
-
对于外部传感器输入,汽车使用安装在前引擎盖上的单个摄像头的单个图像帧。不使用其他传感器(激光雷达、雷达等)。
-
基于这个单图像输入,以恒定速度行驶,汽车预测其输出的转向角。不会预测其他可能的输出(刹车、油门、换挡等)。
-
道路上没有其他车辆、行人、动物或其他任何东西,也没有相关的环境。
-
所行驶的道路是单车道,没有标线、交通标志和信号,也没有保持在道路左侧或右侧的规则。
-
道路主要在转弯方面变化(这将需要改变转向角来导航),而不是在高程方面变化(这将需要改变油门,刹车等)。
设置和要求
我们将在 AirSim 的景观地图中实现“Hello, World!”问题(图 16-4)。AirSim 是一个开源的逼真模拟平台,是一个用于训练数据收集和验证基于深度学习的自主系统模型开发的研究工具。

图 16-4。AirSim 中的景观地图
您可以在 GitHub 上的Autonomous Driving Cookbook中找到本章的代码,形式为 Jupyter 笔记本的端到端深度学习教程。在食谱仓库中,转到AirSimE2EDeepLearning/。我们可以通过运行以下命令来实现:
$ git clone https://github.com/Microsoft/AutonomousDrivingCookbook.git
$ cd AutonomousDrivingCookbook/AirSimE2EDeepLearning/
我们将使用 Keras 这个我们已经熟悉的库来实现我们的神经网络。在这个问题上,除了本书之前章节介绍的内容,我们不需要学习任何新的深度学习概念。
对于本章,我们通过在 AirSim 中驾驶来创建了一个数据集。数据集的未压缩大小为 3.25 GB。这比我们之前使用的数据集稍大一些,但请记住,我们正在实现一个非常复杂问题的“Hello, World!”。相比之下,汽车制造商每天在道路上收集多个 PB 的数据是相当正常的做法。您可以通过访问aka.ms/AirSimTutorialDataset来下载数据集。
我们可以在 Windows 或 Linux 机器上运行提供的代码并训练我们的模型。我们为本章创建了 AirSim 的独立构建,您可以从aka.ms/ADCookbookAirSimPackage下载。下载后,请将模拟器包提取到您选择的位置。请注意路径,因为您以后会需要它。请注意,此构建仅在 Windows 上运行,但仅需要查看我们最终训练好的模型。如果您使用 Linux,可以将模型文件拷贝到提供的模拟器包的 Windows 机器上运行。
AirSim 是一个高度逼真的环境,这意味着它能够生成逼真的环境图片供模型训练。在玩高清视频游戏时,你可能遇到过类似的图形。考虑到提供的模拟器包的数据量和图形质量,GPU 肯定是运行代码和模拟器的首选。
最后,运行提供的代码还需要一些额外的工具和 Python 依赖项。您可以在代码仓库的README 文件中的环境设置中找到这些详细信息。在高层次上,我们需要 Anaconda 与 Python 3.5(或更高版本)、TensorFlow(作为 Keras 的后端运行)和 h5py(用于存储和读取数据和模型文件)。设置好 Anaconda 环境后,我们可以通过以 root 或管理员身份运行InstallPackages.py文件来安装所需的 Python 依赖项。
表 16-2 提供了我们刚刚定义的所有要求的摘要。
表 16-2。设置和要求摘要
| 项目 | 要求/链接 | 备注/评论 |
|---|---|---|
| 代码仓库 | https://oreil.ly/_IJl9 | 可在 Windows 或 Linux 机器上运行 |
| 使用的 Jupyter 笔记本 | DataExplorationAndPreparation.ipynbTrainModel.ipynbTestModel.ipynb | |
| 数据集下载 | aka.ms/AirSimTutorialDataset | 大小为 3.25 GB;用于训练模型 |
| 模拟器下载 | aka.ms/ADCookbookAirSimPackage | 不需要用于训练模型,只用于部署和运行;仅在 Windows 上运行 |
| GPU | 推荐用于训练,运行模拟器所需 | |
| 其他工具+Python 依赖项 | 存储库中 README 文件中的环境设置部分 |
有了这些准备工作,让我们开始吧!
数据探索和准备
当我们深入探索神经网络和深度学习的世界时,您会注意到,机器学习的许多魔力并不是来自神经网络是如何构建和训练的,而是来自数据科学家对问题、数据和领域的理解。自动驾驶也不例外。正如您很快将看到的,深入了解数据和我们试图解决的问题对于教会我们的汽车如何自动驾驶至关重要。
这里的所有步骤也都在 Jupyter Notebook DataExplorationAndPreparation.ipynb中详细说明。我们首先导入这部分练习所需的所有必要模块:
import os
import random
import numpy as np
import pandas as pd
import h5py
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw
import Cooking #This module contains helper code. Please do not edit this file.
接下来,我们确保我们的程序可以找到我们解压缩的数据集的位置。我们还为其提供一个位置来存储我们处理(或“烹饪”)的数据:
# << Point this to the directory containing the raw data >>
RAW_DATA_DIR = 'data_raw/'
# << Point this to the desired output directory for the cooked (.h5) data >>
COOKED_DATA_DIR = 'data_cooked/'
# The folders to search for data under RAW_DATA_DIR
# For example, the first folder searched will be RAW_DATA_DIR/normal_1
DATA_FOLDERS = ['normal_1', 'normal_2', 'normal_3', 'normal_4', 'normal_5',
'normal_6', 'swerve_1', 'swerve_2', 'swerve_3']
# The size of the figures and illustrations used
FIGURE_SIZE = (10,10)
查看提供的数据集,我们会看到它包含九个文件夹:normal_1到normal_6和swerve_1到swerve_3。稍后我们会回到这些文件夹的命名。在每个文件夹中都有图像以及.tsv(或.txt)文件。让我们看看其中一个图像:
sample_image_path = os.path.join(RAW_DATA_DIR, 'normal_1/images/img_0.png')
sample_image = Image.open(sample_image_path)
plt.title('Sample Image')
plt.imshow(sample_image)
plt.show()
我们应该看到以下输出。我们可以看到这张图像是由放置在汽车引擎盖中央的摄像头拍摄的(在图 16-5 中底部略有可见)。我们还可以从模拟器中看到用于这个问题的景观环境。请注意,道路上没有其他车辆或物体,符合我们的要求。尽管道路看起来是多雪的,但对于我们的实验来说,雪只是视觉上的,不是实际的;也就是说,它不会对我们汽车的物理和运动产生影响。当然,在现实世界中,准确复制雪、雨等的物理特性非常重要,以便我们的模拟器可以生成高度准确的现实版本。这是一个非常复杂的任务,许多公司都投入了大量资源。幸运的是,对于我们的目的,我们可以安全地忽略所有这些,并将雪视为图像中的白色像素。

图 16-5. 显示文件 normal_1/images/img_0.png 的内容的图
让我们通过显示一个.tsv/.txt文件的内容来预览其余数据是什么样的:
sample_tsv_path = os.path.join(RAW_DATA_DIR, 'normal_1/airsim_rec.txt')
sample_tsv = pd.read_csv(sample_tsv_path, sep='\t')
sample_tsv.head()
我们应该看到以下输出:

这里有很多内容,让我们来分解一下。首先,我们可以看到表中的每一行对应一个图像帧(这里我们只显示了前五帧)。因为这是我们数据收集驱动的开始,我们可以看到汽车还没有开始移动:速度和油门都是 0,档位是空档。因为我们只是尝试预测问题中的转向角,我们不会预测速度、油门、刹车或档位的值。然而,这些值将作为输入数据的一部分(稍后会详细介绍)。
识别感兴趣区域
让我们现在回到我们的示例图像。到目前为止,我们所做的所有观察都是从人类的眼睛看到的。让我们再次看一下这幅图像,只是这一次我们换成了一辆刚开始学习自动驾驶并试图理解所看到的东西的汽车的角度(或轮胎)。
如果我是汽车,看着我的摄像头呈现给我的这张图像,我可以将其分为三个不同的部分(图 16-6)。首先是下面的第三部分,看起来更或多少一致。它主要由直线(道路、车道分隔线、道路围栏等)组成,颜色一致,白色、黑色、灰色和棕色。底部还有一个奇怪的黑色弧线(这是汽车的引擎盖)。图像的上面第三部分,与下面的第三部分一样,也是一致的。主要是灰色和白色(天空和云)。最后,中间的第三部分有很多事情发生。有大的棕色和灰色形状(山),它们完全不一致。还有四个高大的绿色图形(树),它们的形状和颜色与我看到的其他任何东西都不同,所以它们一定非常重要。随着我看到更多的图像,上面的第三部分和下面的第三部分并没有太大变化,但中间的第三部分发生了很多变化。因此,我得出结论,这是我应该最关注的部分。

图 16-6. 自动驾驶汽车所看到的图像的三个部分
你看到我们汽车面临的挑战了吗?它无法知道呈现给它的图像的哪一部分是重要的。它可能会尝试将转向角度的变化与景观的变化联系起来,而不是专注于道路的转弯,这与其他事物相比非常微妙。景观的变化不仅令人困惑,而且与我们正在解决的问题毫不相关。尽管天空大部分时间都没有变化,但它也不提供与我们汽车转向相关的任何信息。最后,每张照片中捕捉到的引擎盖部分也是不相关的。
让我们通过仅关注图像的相关部分来解决这个问题。我们通过在图像中创建一个感兴趣区域(ROI)来实现这一点。正如我们可以想象的那样,没有一个硬性规则来规定我们的 ROI 应该是什么样子的。这真的取决于我们的用例。对于我们来说,因为摄像头处于固定位置,道路上没有高度变化,ROI 是一个简单的矩形,专注于道路和道路边界。我们可以通过运行以下代码片段来查看 ROI:
sample_image_roi = sample_image.copy()
fillcolor=(255,0,0)
draw = ImageDraw.Draw(sample_image_roi)
points = [(1,76), (1,135), (255,135), (255,76)]
for i in range(0, len(points), 1):
draw.line([points[i], points[(i+1)%len(points)]], fill=fillcolor, width=3)
del draw
plt.title('Image with sample ROI')
plt.imshow(sample_image_roi)
plt.show()
我们应该看到图 16-7 中显示的输出。

图 16-7. 我们的汽车在训练期间应关注的 ROI
我们的模型现在将只关注道路,不会被环境中其他任何事情所困扰。这也将图像的大小减少了一半,这将使我们的神经网络训练更容易。
数据增强
正如前面提到的,在训练深度学习模型时,尽可能获得尽可能多的数据总是更好的。我们已经在之前的章节中看到了数据增强的重要性,以及它如何帮助我们不仅获得更多的训练数据,还可以避免过拟合。然而,先前讨论的大多数用于图像分类的数据增强技术对于我们当前的自动驾驶问题并不适用。让我们以旋转为例。随机将图像旋转 20 度在训练智能手机相机分类器时非常有帮助,但是我们汽车引擎盖上的摄像头固定在一个位置,永远不会看到旋转的图像(除非我们的汽车在翻转,那时我们有更大的问题)。随机移位也是如此;我们在驾驶时永远不会遇到这些。然而,对于我们当前的问题,我们可以做一些其他事情来增强数据。
仔细观察我们的图像,我们可以看到在 y 轴上翻转它会产生一个看起来很容易来自不同测试运行的图像(图 16-8)。如果我们使用图像的翻转版本以及它们的常规版本,我们可以有效地将数据集的大小加倍。以这种方式翻转图像将要求我们同时修改与之相关的转向角。因为我们的新图像是原始图像的反射,所以我们只需改变相应转向角的符号(例如,从 0.212 变为-0.212)。

图 16-8。在 y 轴上翻转图像
我们可以做另一件事来增加我们的数据。自动驾驶汽车不仅需要准备好应对道路上的变化,还需要准备好应对外部条件的变化,如天气、时间和光照条件。今天大多数可用的模拟器都允许我们合成这些条件。我们所有的数据都是在明亮的光照条件下收集的。我们可以在训练过程中通过调整图像亮度引入随机光照变化,而不是返回模拟器收集更多不同光照条件下的数据。图 16-9 展示了一个例子。

图 16-9。将图像亮度降低 40%
数据集不平衡和驾驶策略
深度学习模型的好坏取决于它们所训练的数据。与人类不同,今天的人工智能无法自行思考和推理。因此,当面对完全新的情况时,它会回归到以前看到的内容,并基于其训练的数据集进行预测。此外,深度学习的统计特性使其忽略那些不经常发生的实例,将其视为异常值和离群值,即使它们具有重要意义。这被称为数据集不平衡,对数据科学家来说是一个常见的困扰。
想象一下,我们正在尝试训练一个分类器,该分类器查看皮肤镜图像以检测病变是良性还是恶性。假设我们的数据集有一百万张图像,其中有 1 万张包含恶性病变。很可能我们在这些数据上训练的分类器总是预测图像是良性的,从不预测是恶性的。这是因为深度学习算法旨在在训练过程中最小化错误(或最大化准确性)。通过将所有图像分类为良性,我们的模型达到了 99%的准确性,然后结束训练,严重失败于其训练的任务:检测癌症病变。这个问题通常通过在更平衡的数据集上训练预测器来解决。
自动驾驶也无法免于数据集不平衡的问题。让我们以我们的转向角预测模型为例。从我们日常驾驶经验中,我们对转向角了解多少?在驾驶过程中,我们大多数时间都是直线行驶。如果我们的模型只是在正常驾驶数据上进行训练,它将永远不会学会如何转弯,因为在训练数据集中相对稀缺。为了解决这个问题,我们的数据集不仅需要包含正常驾驶策略的数据,还需要包含“转弯”驾驶策略的数据,数量上具有统计学意义。为了说明我们的意思,让我们回到我们的.tsv/.txt文件。在本章的前面,我们指出了数据文件夹的命名。现在应该清楚了,我们的数据集包含了使用正常驾驶策略进行的六次收集运行和使用转弯驾驶策略进行的三次收集运行的数据。
让我们将所有.tsv/.txt文件中的数据聚合到一个 DataFrame 中,以便更容易进行分析:
full_path_raw_folders = [os.path.join(RAW_DATA_DIR, f) for f in DATA_FOLDERS]
dataframes = []
for folder in full_path_raw_folders:
current_dataframe = pd.read_csv(os.path.join(folder, 'airsim_rec.txt'),
sep='\t')
current_dataframe['Folder'] = folder
dataframes.append(current_dataframe)
dataset = pd.concat(dataframes, axis=0)
让我们在散点图上绘制来自两种驾驶策略的转向角:
min_index = 100
max_index = 1500
steering_angles_normal_1 = dataset[dataset['Folder'].apply(lambda v: 'normal_1'
in v)]['Steering'][min_index:max_index]
steering_angles_swerve_1 = dataset[dataset['Folder'].apply(lambda v: 'swerve_1'
in v)]['Steering'][min_index:max_index]
plot_index = [i for i in range(min_index, max_index, 1)]
fig = plt.figure(figsize=FIGURE_SIZE)
ax1 = fig.add_subplot(111)
ax1.scatter(plot_index, steering_angles_normal_1, c='b', marker='o',
label='normal_1')
ax1.scatter(plot_index, steering_angles_swerve_1, c='r', marker='_',
label='swerve_1')
plt.legend(loc='upper left');
plt.title('Steering Angles for normal_1 and swerve_1 runs')
plt.xlabel('Time')
plt.ylabel('Steering Angle')
plt.show()
图 16-10 显示了结果。

图 16-10。显示两种驾驶策略的转向角的图
让我们也绘制一下使用每种策略收集的数据点数量:
dataset['Is Swerve'] = dataset.apply(lambda r: 'swerve' in r['Folder'], axis=1)
grouped = dataset.groupby(by=['Is Swerve']).size().reset_index()
grouped.columns = ['Is Swerve', 'Count']
def make_autopct(values):
def my_autopct(percent):
total = sum(values)
val = int(round(percent*total/100.0))
return '{0:.2f}% ({1:d})'.format(percent,val)
return my_autopct
pie_labels = ['Normal', 'Swerve']
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
ax.pie(grouped['Count'], labels=pie_labels, autopct =
make_autopct(grouped['Count']), explode=[0.1, 1], textprops={'weight': 'bold'},
colors=['lightblue', 'salmon'])
plt.title('Number of data points per driving strategy')
plt.show()
图 16-11 显示了这些结果。

图 16-11。两种驾驶策略的数据集拆分
查看图 16-10,正如我们所预期的,我们看到正常驾驶策略产生的转向角大多数是我们在日常驾驶中观察到的,大部分是直行,偶尔转弯。相比之下,突然转向驾驶策略主要集中在急转弯,因此转向角的值较高。如图 16-11 所示,这两种策略的结合给我们一个不错的、虽然在现实生活中不太现实的分布,以 75/25 的比例进行训练。这也进一步巩固了模拟对自动驾驶的重要性,因为在现实生活中我们不太可能使用实际汽车进行突然转向策略来收集数据。
在结束关于数据预处理和数据集不平衡的讨论之前,让我们最后再看一下我们两种驾驶策略的转向角分布,通过在直方图上绘制它们(图 16-12):
bins = np.arange(-1, 1.05, 0.05)
normal_labels = dataset[dataset['Is Swerve'] == False]['Steering']
swerve_labels = dataset[dataset['Is Swerve'] == True]['Steering']
def steering_histogram(hist_labels, title, color):
plt.figure(figsize=FIGURE_SIZE)
n, b, p = plt.hist(hist_labels.as_matrix(), bins, normed=1, facecolor=color)
plt.xlabel('Steering Angle')
plt.ylabel('Normalized Frequency')
plt.title(title)
plt.show()
steering_histogram(normal_labels, 'Normal driving strategy label distribution',
'g')
steering_histogram(swerve_labels, 'Swerve driving strategy label distribution',
'r')

图 16-12。两种驾驶策略的转向角分布
正如前面所述,与正常驾驶策略相比,突然转向驾驶策略给我们提供了更广泛的角度范围,如图 16-12 所示。这些角度将帮助我们的神经网络在汽车偏离道路时做出适当的反应。我们数据集中的不平衡问题在一定程度上得到解决,但并非完全解决。我们仍然有很多零,跨越两种驾驶策略。为了进一步平衡我们的数据集,我们可以在训练过程中忽略其中的一部分。尽管这给我们提供了一个非常平衡的数据集,但大大减少了我们可用的数据点总数。在构建和训练神经网络时,我们需要记住这一点。
在继续之前,让我们确保我们的数据适合训练。让我们从所有文件夹中获取原始数据,将其分割为训练、测试和验证数据集,并将它们压缩成 HDF5 文件。HDF5 格式允许我们以块的方式访问数据,而不需要一次性将整个数据集读入内存。这使其非常适合深度学习问题。它还可以与 Keras 无缝配合。以下代码将需要一些时间来运行。当运行完成后,我们将得到三个数据集文件:train.h5、eval.h5和test.h5。
train_eval_test_split = [0.7, 0.2, 0.1]
full_path_raw_folders = [os.path.join(RAW_DATA_DIR, f) for f in DATA_FOLDERS]
Cooking.cook(full_path_raw_folders, COOKED_DATA_DIR, train_eval_test_split)
每个数据集文件有四个部分:
图像
包含图像数据的 NumPy 数组。
上一个状态
包含汽车最后已知状态的 NumPy 数组。这是一个(转向、油门、刹车、速度)元组。
标签
包含我们希望预测的转向角的 NumPy 数组(在范围-1..1 上进行了标准化)。
元数据
包含有关文件的元数据的 NumPy 数组(它们来自哪个文件夹等)。
现在我们已经准备好从数据集中获取观察和经验,并开始训练我们的模型。
训练我们的自动驾驶模型
本节中的所有步骤也在 Jupyter Notebook TrainModel.ipynb中详细说明。与之前一样,我们首先导入一些库并定义路径:
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential, Model
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, Lambda,
Input, concatenate
from tensorflow.keras.optimizers import Adam, SGD, Adamax, Nadam
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint,
CSVLogger
from tensorflow.keras.callbacks import EarlyStopping
import tensorflow.keras.backend as K
from tensorflow.keras.preprocessing import image
from tensorflow.keras.models import Sequential, Model, load_model
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense,
from tensorflow.keras.layers import Lambda, Input, concatenate, BatchNormalization
from keras_tqdm import TQDMNotebookCallback
import json
import os
import numpy as np
import pandas as pd
from Generator import DriveDataGenerator
from Cooking import checkAndCreateDir
import h5py
from PIL import Image, ImageDraw
import math
import matplotlib.pyplot as plt
# << The directory containing the cooked data from the previous step >>
COOKED_DATA_DIR = 'data_cooked/'
# << The directory in which the model output will be placed >>
MODEL_OUTPUT_DIR = 'model'
让我们也设置好我们的数据集:
train_dataset = h5py.File(os.path.join(COOKED_DATA_DIR, 'train.h5'), 'r')
eval_dataset = h5py.File(os.path.join(COOKED_DATA_DIR, 'eval.h5'), 'r')
test_dataset = h5py.File(os.path.join(COOKED_DATA_DIR, 'test.h5'), 'r')
num_train_examples = train_dataset['image'].shape[0]
num_eval_examples = eval_dataset['image'].shape[0]
num_test_examples = test_dataset['image'].shape[0]
batch_size=32
驾驶数据生成器
我们在之前的章节中介绍了 Keras 数据生成器的概念。数据生成器会遍历数据集,并从磁盘中以块的形式读取数据。这使我们能够让 CPU 和 GPU 保持繁忙,提高吞吐量。为了实现前一节讨论的想法,我们创建了自己的 Keras 生成器,称为DriveDataGenerator。
让我们回顾一下前一节中的一些观察结果:
-
我们的模型应该只关注每个图像中的 ROI。
-
我们可以通过水平翻转图像并反转转向角的符号来增强我们的数据集。
-
我们可以通过在图像中引入随机亮度变化来进一步增强数据。这将模拟不同的光照条件,并使我们的模型更加健壮。
-
我们可以随机删除一定百分比的转向角为零的数据点,以便在训练时模型看到一个平衡的数据集。
-
我们平衡后可用的数据点总数将显著减少。
让我们看看DriveDataGenerator如何处理这些前四项。当我们开始设计神经网络时,我们将回到最后一项。
ROI 是一个简单的图像裁剪。[76,135,0,255](在接下来的代码块中显示)是表示 ROI 的矩形的[x1,x2,y1,y2]值。生成器从每个图像中提取此矩形。我们可以使用参数roi来修改 ROI。
水平翻转相对简单。在生成批次时,随机图像沿 y 轴翻转,并且如果参数horizontal_flip设置为True,它们的转向角值将被反转。
对于随机亮度变化,我们引入参数brighten_range。将此参数设置为0.4会随机地使任何给定批次中的图像亮度增加或减少最多 40%。我们不建议将此值增加到0.4之上。为了计算亮度,我们将图像从 RGB 转换为 HSV 空间,将“V”坐标缩放上下,然后转换回 RGB。
通过删除零来平衡数据集,我们引入了参数zero_drop_percentage。将此设置为0.9将在任何给定批次中随机删除 90%的 0 标签数据点。
让我们使用这些参数初始化我们的生成器:
data_generator = DriveDataGenerator(rescale=1./255., horizontal_flip=True,
brighten_range=0.4)
train_generator = data_generator.flow\
(train_dataset['image'], train_dataset['previous_state'],
train_dataset['label'], batch_size=batch_size, zero_drop_percentage=0.95,
roi=[76,135,0,255])
eval_generator = data_generator.flow\
(eval_dataset['image'], eval_dataset['previous_state'],
eval_dataset['label'],
batch_size=batch_size, zero_drop_percentage=0.95, roi=[76,135,0,255]
让我们通过在相应图像上绘制标签(转向角)来可视化一些样本数据点:
def draw_image_with_label(img, label, prediction=None):
theta = label * 0.69 #Steering range for the car is +- 40 degrees -> 0.69
# radians
line_length = 50
line_thickness = 3
label_line_color = (255, 0, 0)
prediction_line_color = (0, 255, 255)
pil_image = image.array_to_img(img, K.image_data_format(), scale=True)
print('Actual Steering Angle = {0}'.format(label))
draw_image = pil_image.copy()
image_draw = ImageDraw.Draw(draw_image)
first_point = (int(img.shape[1]/2), img.shape[0])
second_point = (int((img.shape[1]/2) + (line_length * math.sin(theta))),
int(img.shape[0] - (line_length * math.cos(theta))))
image_draw.line([first_point, second_point], fill=label_line_color,
width=line_thickness)
if (prediction is not None):
print('Predicted Steering Angle = {0}'.format(prediction))
print('L1 Error: {0}'.format(abs(prediction-label)))
theta = prediction * 0.69
second_point = (int((img.shape[1]/2) + ((line_length/2) *
math.sin(theta))), int(img.shape[0] - ((line_length/2) * math.cos(theta))))
image_draw.line([first_point, second_point], fill=prediction_line_color,
width=line_thickness * 3)
del image_draw
plt.imshow(draw_image)
plt.show()
[sample_batch_train_data, sample_batch_test_data] = next(train_generator)
for i in range(0, 3, 1):
draw_image_with_label(sample_batch_train_data[0][i],
sample_batch_test_data[i])
我们应该看到类似于图 16-13 的输出。请注意,我们现在在训练模型时只关注 ROI,从而忽略原始图像中存在的所有非相关信息。线表示地面真实转向角。这是汽车在摄像机拍摄图像时行驶的角度。

图 16-13。在图像上绘制转向角
模型定义
现在我们准备定义神经网络的架构。在这里,我们必须考虑到在删除零后,我们的数据集非常有限的问题。因此,我们不能构建太深的网络。由于我们处理的是图像,我们将需要一些卷积/最大池化对来提取特征。
然而,仅仅使用图像可能不足以使我们的模型收敛。仅仅使用图像进行训练也不符合现实世界中做驾驶决策的方式。在驾驶时,我们不仅感知周围环境,还意识到我们的速度、转向程度,以及油门和刹车踏板的状态。相机、激光雷达、雷达等传感器输入到神经网络中只对应驾驶员在做驾驶决策时手头上所有信息的一部分。呈现给神经网络的图像可能是从一辆静止的汽车或以 60 英里/小时行驶的汽车中拍摄的;网络无法知道是哪种情况。在以 5 英里/小时行驶时将方向盘向右转两度与以 50 英里/小时行驶时做同样动作将产生非常不同的结果。简而言之,试图预测转向角度的模型不应仅依赖感官输入。它还需要关于汽车当前状态的信息。幸运的是,我们有这些信息可用。
在上一节的结尾,我们指出我们的数据集有四个部分。对于每个图像,除了转向角标签和元数据外,我们还记录了与图像对应的汽车上次已知状态。这以 (转向、油门、刹车、速度) 元组的形式存储,我们将使用这些信息以及我们的图像作为神经网络的输入。请注意,这不违反我们的“Hello, World!”要求,因为我们仍然将单个摄像头作为唯一的外部传感器。
将我们讨论的所有内容放在一起,您可以在 图 16-14 中看到我们将用于此问题的神经网络。我们使用了三个卷积层,分别具有 16、32、32 个滤波器,并且使用了 (3,3) 的卷积窗口。我们将图像特征(从卷积层输出)与提供汽车先前状态的输入层进行合并。然后将组合特征集传递到两个分别具有 64 和 10 个隐藏神经元的全连接层中。我们网络中使用的激活函数是 ReLU。请注意,与我们在前几章中处理的分类问题不同,我们网络的最后一层是一个没有激活的单个神经元。这是因为我们要解决的问题是一个回归问题。我们网络的输出是转向角度,一个浮点数,而不是我们之前预测的离散类别。

图 16-14. 网络架构
现在让我们实现我们的网络。我们可以使用 model.summary():
image_input_shape = sample_batch_train_data[0].shape[1:]
state_input_shape = sample_batch_train_data[1].shape[1:]
activation = 'relu'
# Create the convolutional stacks
pic_input = Input(shape=image_input_shape)
img_stack = Conv2D(16, (3, 3), name="convolution0", padding='same',
activation=activation)(pic_input)
img_stack = MaxPooling2D(pool_size=(2,2))(img_stack)
img_stack = Conv2D(32, (3, 3), activation=activation, padding='same',
name='convolution1')(img_stack)
img_stack = MaxPooling2D(pool_size=(2, 2))(img_stack)
img_stack = Conv2D(32, (3, 3), activation=activation, padding='same',
name='convolution2')(img_stack)
img_stack = MaxPooling2D(pool_size=(2, 2))(img_stack)
img_stack = Flatten()(img_stack)
img_stack = Dropout(0.2)(img_stack)
# Inject the state input
state_input = Input(shape=state_input_shape)
merged = concatenate([img_stack, state_input])
# Add a few dense layers to finish the model
merged = Dense(64, activation=activation, name='dense0')(merged)
merged = Dropout(0.2)(merged)
merged = Dense(10, activation=activation, name='dense2')(merged)
merged = Dropout(0.2)(merged)
merged = Dense(1, name='output')(merged)
adam = Nadam(lr=0.0001, beta_1=0.9, beta_2=0.999, epsilon=1e-08)
model = Model(inputs=[pic_input, state_input], outputs=merged)
model.compile(optimizer=adam, loss='mse')
回调
Keras 的一个很好的特性是能够声明 回调函数。回调函数在每个训练周期结束后执行,帮助我们深入了解训练过程并在一定程度上控制超参数。它们还让我们定义在训练进行时执行某些操作的条件;例如,如果损失停止减少,则提前停止训练。我们将为我们的实验使用一些回调函数:
ReduceLROnPlateau
如果模型接近最小值且学习率过高,模型将在该最小值周围循环而无法达到它。当验证损失达到平稳期并停止改善时,此回调将允许模型减少学习率,使我们能够达到最佳点。
CSVLogger
这使我们能够将每个周期结束后模型的输出记录到一个 CSV 文件中,这样我们就可以跟踪进展而无需使用控制台。
ModelCheckpoint
通常,我们希望使用在验证集上损失最低的模型。此回调将在每次验证损失改善时保存模型。
EarlyStopping
当验证损失不再改善时,我们将停止训练。否则,我们会面临过拟合的风险。此选项将检测验证损失停止改善的时候,并在发生这种情况时停止训练过程。
现在让我们继续实现这些回调:
plateau_callback = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3,
min_lr=0.0001, verbose=1)
checkpoint_filepath = os.path.join(MODEL_OUTPUT_DIR, 'models', '{0}_model.{1}
-{2}.h5'.format('model', '{epoch:02d}', '{val_loss:.7f}'))
checkAndCreateDir(checkpoint_filepath)
checkpoint_callback = ModelCheckpoint(checkpoint_filepath, save_best_only=True,
verbose=1)
csv_callback = CSVLogger(os.path.join(MODEL_OUTPUT_DIR, 'training_log.csv'))
early_stopping_callback = EarlyStopping(monitor='val_loss', patience=10,
verbose=1)
nbcallback = TQDMNotebookCallback()
setattr(nbcallback, 'on_train_batch_begin', `lambda` x,y: `None`)
setattr(nbcallback, 'on_train_batch_end', `lambda` x,y: `None`)
setattr(nbcallback, 'on_test_begin', `lambda` x: `None`)
setattr(nbcallback, 'on_test_end', `lambda` x: `None`)
setattr(nbcallback, 'on_test_batch_begin', `lambda` x,y: `None`)
setattr(nbcallback, 'on_test_batch_end', `lambda` x,y: `None`)
callbacks=[plateau_callback, csv_callback, checkpoint_callback,
early_stopping_callback, nbcallback]
我们现在已经准备好开始训练了。模型需要一段时间来训练,所以这将是一个不错的 Netflix 休息时间。训练过程应该以约 0.0003 的验证损失终止:
history = model.fit_generator(train_generator,
steps_per_epoch=num_train_examples // batch_size, epochs=500,
callbacks=callbacks, validation_data=eval_generator,
validation_steps=num_eval_examples // batch_size, verbose=2)
Epoch 1/500
Epoch 00001: val_loss improved from inf to 0.02338, saving model to
model\models\model_model.01-0.0233783.h5
- 442s - loss: 0.0225 - val_loss: 0.0234
Epoch 2/500
Epoch 00002: val_loss improved from 0.02338 to 0.00859, saving model to
model\models\model_model.02-0.0085879.h5
- 37s - loss: 0.0184 - val_loss: 0.0086
Epoch 3/500
Epoch 00003: val_loss improved from 0.00859 to 0.00188, saving model to
model\models\model_model.03-0.0018831.h5
- 38s - loss: 0.0064 - val_loss: 0.0019
…………………………….
我们的模型现在已经训练好并准备就绪。在看到它的表现之前,让我们进行一个快速的合理性检查,并将一些预测绘制在图像上:
[sample_batch_train_data, sample_batch_test_data] = next(train_generator)
predictions = model.predict([sample_batch_train_data[0],
sample_batch_train_data[1]])
for i in range(0, 3, 1):
draw_image_with_label(sample_batch_train_data[0][i],
sample_batch_test_data[i], predictions[i])
我们应该看到类似于图 16-15 的输出。在这个图中,粗线是预测输出,细线是标签输出。看起来我们的预测相当准确(我们还可以在图像上方看到实际和预测值)。是时候部署我们的模型并看看它的表现了。

图 16-15。在图像上绘制实际和预测的转向角
部署我们的自动驾驶模型
本节中的所有步骤也在 Jupyter Notebook TestModel.ipynb中有详细说明。现在我们的模型已经训练好了,是时候启动模拟器并使用我们的模型驾驶汽车了。
与之前一样,首先导入一些库并定义路径:
from tensorflow.keras.models import load_model
import sys
import numpy as np
import glob
import os
if ('../../PythonClient/' not in sys.path):
sys.path.insert(0, '../../PythonClient/')
from AirSimClient import *
# << Set this to the path of the model >>
# If None, then the model with the lowest validation loss from training will be
# used
MODEL_PATH = None
if (MODEL_PATH == None):
models = glob.glob('model/models/*.h5')
best_model = max(models, key=os.path.getctime)
MODEL_PATH = best_model
print('Using model {0} for testing.'.format(MODEL_PATH))
接下来,加载模型并在景观环境中连接到 AirSim。要启动模拟器,在 Windows 机器上,打开一个 PowerShell 命令窗口,位于我们解压缩模拟器包的位置,并运行以下命令:
.\AD_Cookbook_Start_AirSim.ps1 landscape
在 Jupyter Notebook 中,运行以下命令将模型连接到 AirSim 客户端。确保模拟器在启动此过程之前已经运行:
model = load_model(MODEL_PATH)
client = CarClient()
client.confirmConnection()
client.enableApiControl(True)
car_controls = CarControls()
print('Connection established!')
连接建立后,让我们现在设置汽车的初始状态以及用于存储模型输出的一些缓冲区:
car_controls.steering = 0
car_controls.throttle = 0
car_controls.brake = 0
image_buf = np.zeros((1, 59, 255, 3))
state_buf = np.zeros((1,4))
下一步是设置模型期望从模拟器接收 RGB 图像作为输入。我们需要为此定义一个辅助函数:
def get_image():
image_response = client.simGetImages([ImageRequest(0, AirSimImageType.Scene,
False, False)])[0]
image1d = np.fromstring(image_response.image_data_uint8, dtype=np.uint8)
image_rgba = image1d.reshape(image_response.height, image_response.width, 4)
return image_rgba[76:135,0:255,0:3].astype(float)
最后,让我们为我们的模型设置一个无限循环,从模拟器中读取图像以及汽车的当前状态,预测转向角,并将其发送回模拟器。因为我们的模型只预测转向角,所以我们需要提供一个控制信号来自行维持速度。让我们设置这样一个控制信号,使汽车尝试以恒定的 5m/s 速度行驶:
while (True):
car_state = client.getCarState()
if (car_state.speed < 5):
car_controls.throttle = 1.0
else:
car_controls.throttle = 0.0
image_buf[0] = get_image()
state_buf[0] = np.array([car_controls.steering, car_controls.throttle,
car_controls.brake, car_state.speed])
model_output = model.predict([image_buf, state_buf])
car_controls.steering = round(0.5 * float(model_output[0][0]), 2)
print('Sending steering = {0}, throttle = {1}'.format(car_controls.steering,
car_controls.throttle))
client.setCarControls(car_controls)
我们应该看到类似于以下的输出:
Sending steering = 0.03, throttle = 1.0
Sending steering = 0.03, throttle = 1.0
Sending steering = 0.03, throttle = 1.0
Sending steering = 0.03, throttle = 1.0
Sending steering = 0.03, throttle = 1.0
Sending steering = -0.1, throttle = 1.0
Sending steering = -0.12, throttle = 1.0
Sending steering = -0.13, throttle = 1.0
Sending steering = -0.13, throttle = 1.0
Sending steering = -0.13, throttle = 1.0
Sending steering = -0.14, throttle = 1.0
Sending steering = -0.15, throttle = 1.0
我们成功了!汽车在道路上很好地行驶,大部分时间保持在右侧,小心地穿过所有急转弯和潜在的偏离道路的地方。恭喜我们训练出了我们的第一个自动驾驶模型!

图 16-16。训练模型驾驶汽车
在我们结束之前,有几件值得注意的事情。首先,请注意汽车的运动不是完全平滑的。这是因为我们正在处理一个回归问题,并为汽车看到的每个图像帧做出一个转向角预测。解决这个问题的一种方法是在一系列连续的图像上平均预测。另一个想法可能是将其转化为一个分类问题。更具体地,我们可以为转向角定义桶(...,-0.1,-0.05,0,0.05,0.1,...),将标签分桶化,并为每个图像预测正确的桶。
如果让模型运行一段时间(略长于五分钟),我们会观察到汽车最终会随机偏离道路并撞车。这发生在一个有陡峭上坡的赛道部分。还记得我们在问题设置中的最后一个要求吗?高度变化需要操作油门和刹车。因为我们的模型只能控制转向角,所以在陡峭的道路上表现不佳。
进一步探索
在“Hello, World!”场景中训练过后,我们的模型显然不是一个完美的驾驶员,但不要灰心。请记住,我们只是刚刚触及深度学习和自动驾驶汽车交汇点的可能性表面。我们能够让我们的汽车几乎完美地学会驾驶,只使用了一个非常小的数据集,这是值得骄傲的事情!
以下是一些新的想法,可以在本章学到的基础上进行扩展。您可以使用本章已经准备好的设置来实现所有这些想法。
扩展我们的数据集
一般规则是,使用更多的数据有助于提高模型性能。现在我们已经启动并运行了模拟器,通过进行更多的数据收集运行来扩展我们的数据集将是一个有用的练习。我们甚至可以尝试将来自 AirSim 中各种不同环境的数据结合起来,看看我们在这些数据上训练的模型在不同环境中的表现如何。
在本章中,我们只使用了单个摄像头的 RGB 数据。AirSim 允许我们做更多的事情。例如,我们可以收集深度视图、分割视图、表面法线视图等每个可用摄像头的图像。因此,对于每个实例,我们可能有 20 个不同的图像(对于四种模式中的所有五个摄像头)。使用所有这些额外数据能帮助我们改进我们刚刚训练的模型吗?
在序列数据上训练
我们的模型目前对每个预测使用单个图像和单个车辆状态。然而,这并不是我们在现实生活中驾驶的方式。我们的行动总是考虑到导致给定时刻的最近一系列事件。在我们的数据集中,我们有所有图像的时间戳信息可用,我们可以使用这些信息创建序列。我们可以修改我们的模型,使用前 N 个图像和状态进行预测。例如,给定过去的 10 个图像和过去的 10 个状态,预测下一个转向角度。(提示:这可能需要使用 RNNs。)
强化学习
在下一章学习强化学习之后,我们可以回来尝试自动驾驶食谱中的Distributed Deep Reinforcement Learning for Autonomous Driving教程。使用 AirSim 中包含的 Neighborhood 环境,该环境已经包含在我们为本章下载的软件包中,我们将学习如何通过迁移学习和云来扩展深度强化学习训练作业,并将训练时间从不到一周缩短到不到一个小时。
总结
本章让我们一窥深度学习如何推动自动驾驶行业。利用前几章学到的技能,我们使用 Keras 实现了自动驾驶的“Hello, World!”问题。通过探索手头的原始数据,我们学会了如何预处理数据,使其适合训练高性能模型。而且我们能够用一个非常小的数据集完成这个任务。我们还能够拿出我们训练好的模型,并将其部署到模拟世界中驾驶汽车。您不认为这其中有一些魔力吗?
第十七章:在不到一个小时内构建自动驾驶汽车:使用 AWS DeepRacer 进行强化学习
由客座作者 Sunil Mallya 撰写
如果你关注科技新闻,你可能已经看到了关于计算机何时将接管世界的辩论再次兴起。尽管这是一个有趣的思考练习,但是是什么引发了这些辩论的再次兴起呢?这种辩论再次兴起的很大一部分原因归功于计算机在决策任务中击败人类的消息——在国际象棋中获胜,在视频游戏中取得高分,如《Atari》(2013),在复杂的围棋比赛中击败人类(2016),最后,在 2017 年击败人类团队在《Defense of the Ancients》(Dota)2 中。这些成功最令人惊讶的事情是,“机器人”通过相互对抗并强化他们发现的成功策略来学习这些游戏。
如果我们更广泛地思考这个概念,这与人类教导他们的宠物没有什么不同。为了训练一只狗,每一种好行为都会通过奖励狗狗一块零食和许多拥抱来加强,而每一种不良行为都会通过断言“坏狗狗”来加以阻止。强化好行为和阻止不良行为的概念基本上构成了强化学习的核心。
计算机游戏,或者说一般的游戏,需要做出一系列决策,因此传统的监督方法并不适用,因为它们通常专注于做出单一决策(例如,这是一张猫还是狗的图片?)。强化学习社区内部的一个笑话是我们整天都在玩视频游戏(剧透:这是真的!)。目前,强化学习正在被应用于各行各业,以优化股票交易,管理大型建筑和数据中心的供暖和制冷,进行实时广告竞价,优化视频流质量,甚至优化实验室中的化学反应。鉴于这些生产系统的例子,我们强烈建议在顺序决策制定和优化问题中使用强化学习。在本章中,我们专注于学习这种机器学习范式,并将其应用于一个真实世界问题:在不到一个小时内构建一个 1/18 比例的自动驾驶汽车。
强化学习简介
与深度学习类似,强化学习在过去几年中经历了复兴,自从以前保持的人类视频游戏记录被打破以来。强化学习理论在上世纪 90 年代达到鼎盛时期,但由于计算要求和训练这些系统的困难,它没有进入大规模生产系统。传统上,强化学习被认为是计算密集型的;相比之下,神经网络是数据密集型的。但是深度神经网络的进步也使强化学习受益。神经网络现在被用来表示强化学习模型,从而诞生了深度强化学习。在本章中,我们将强化学习和深度强化学习这两个术语互换使用,但在几乎所有情况下,如果没有另有说明,当我们提到强化学习时,我们指的是深度强化学习。
尽管最近有所进展,但强化学习的领域并不友好。用于训练深度学习模型的界面逐渐变得简单,但在强化学习社区中还没有跟上。强化学习的另一个具有挑战性的方面是显著的计算要求和模型收敛所需的时间(学习完成)——要创建一个收敛的模型实际上需要几天,甚至几周的时间。现在,假设我们有耐心、神经网络知识和金钱带宽,关于强化学习的教育资源是少之又少的。大多数资源都针对高级数据科学家,有时对开发人员来说难以触及。我们之前提到的那个 1/18 比例的自动驾驶汽车?那就是 AWS 的 DeepRacer。AWS DeepRacer 背后最大的动机之一是让强化学习对开发人员更加可访问。它由亚马逊 SageMaker 强化学习提供支持,这是一个通用的强化学习平台。而且,让我们真实一点:谁不喜欢自动驾驶汽车呢?

图 17-1。AWS DeepRacer 的 1/18 比例自主汽车
为什么要通过自动驾驶汽车学习强化学习?
近年来,自动驾驶技术得到了重大投资和成功。DIY 自动驾驶和无线电控制(RC)汽车竞速社区因此变得流行。开发人员在真实硬件上构建按比例缩小的自主汽车,并在真实场景中进行测试的热情空前,这促使我们使用“车辆”(字面上)来教育开发人员学习强化学习。尽管存在其他算法来构建自动驾驶汽车,如传统计算机视觉或监督学习(行为克隆),但我们认为强化学习比这些算法更具优势。
表 17-1 总结了一些供开发人员使用的热门自动驾驶套件,以及支持它们的技术。强化学习的一个关键优势是模型可以在模拟器中进行专门训练。但强化学习系统也带来了一系列挑战,其中最大的挑战之一是从模拟到真实(sim2real)问题。在将完全在模拟中训练的模型部署到真实环境中时,总是存在挑战。DeepRacer 通过一些简单而有效的解决方案来解决这个问题,我们稍后在本章中讨论。在 2018 年 11 月推出 DeepRacer 后的前六个月内,近 9000 名开发人员在模拟器中训练了他们的模型,并成功在真实赛道上进行了测试。
表 17-1。自主自动驾驶技术的概况
| 硬件 | 组装 | 技术 | 成本 | ||
|---|---|---|---|---|---|
| AWS DeepRacer | Intel Atom with 100 GFLOPS GPU | 预装 | 强化学习 | $399 | ![]() |
| OpenMV | OpenMV H7 | DIY (两小时) | 传统计算机视觉 | $90 | ![]() |
| Duckietown | Raspberry Pi | 预装 | 强化学习,行为克隆 | $279–$350 | ![]() |
| DonkeyCar | Raspberry Pi | DIY (两到三小时) | 行为克隆 | $250 | ![]() |
| NVIDIA JetRacer | Jetson Nano | DIY (三到五小时) | 监督学习 | 约$400 | ![]() |
使用 DeepRacer 进行实用的深度强化学习
现在是本章最令人兴奋的部分:构建我们的第一个基于强化学习的自主赛车模型。在我们踏上这个旅程之前,让我们建立一个快速的术语备忘单,帮助您熟悉重要的强化学习术语:
目标
完成绕过赛道一圈而不偏离赛道。
输入
在人类驾驶的汽车中,人类通过可视化环境并利用驾驶知识做出决策并驾驶车辆。DeepRacer 也是一个视觉驱动系统,因此我们将单个摄像头图像作为系统的输入。具体来说,我们使用灰度 120x160 图像作为输入。
输出(动作)
在现实世界中,我们通过油门(油门)、刹车和方向盘来驾驶汽车。建立在遥控车上的 DeepRacer 有两个控制信号:油门和方向盘,两者都由传统的脉冲宽度调制(PWM)信号控制。将驾驶映射到 PWM 信号可能不直观,因此我们将汽车可以采取的驾驶动作离散化。还记得我们在电脑上玩的那些老式赛车游戏吗?其中最简单的使用箭头键——左、右和上——来驾驶汽车。同样,我们可以定义汽车可以采取的一组固定动作,但对油门和方向盘有更精细的控制。我们的强化学习模型在训练后将决定采取哪种动作,以便成功地驾驶赛道。在创建模型时,我们将有灵活性定义这些动作。
注意
遥控爱好车中的舵机通常由 PWM 信号控制,这是一系列脉冲信号,脉冲宽度不同。舵机需要到达的位置是通过发送特定宽度的脉冲信号来实现的。脉冲的参数是最小脉冲宽度、最大脉冲宽度和重复率。
代理
学习并做出决策的系统。在我们的情况下,是汽车学习如何驾驶环境(赛道)。
环境
代理通过与动作的交互学习。在 DeepRacer 中,环境包含一个定义代理可以前往和停留的赛道。代理探索环境以收集数据,以训练基础的深度强化学习神经网络。
状态(s)
代理在环境中的位置表示。这是代理的一个瞬时快照。对于 DeepRacer,我们使用图像作为状态。
动作(a)
代理可以做出的决策集。
步骤
从一个状态离散过渡到下一个状态。
情节
这指的是汽车为实现目标而尝试的努力;即,在赛道上完成一圈。因此,一个情节是一系列步骤或经验。不同的情节可能有不同的长度。
奖励(r)
给定输入状态,代理采取的动作的值。
策略(π)
决策策略或函数;从状态到动作的映射。
价值函数(V)
状态到值的映射,其中值代表给定状态下对动作的预期奖励。
重播或经验缓冲区
临时存储缓冲区,存储经验,这是一个元组(s,a,r,s'),其中“s”代表摄像头捕获的观察(或状态),“a”代表车辆采取的动作,“r”代表该动作产生的预期奖励,“s'”代表采取动作后的新观察(或新状态)。
奖励函数
任何强化学习系统都需要一个指导,告诉模型在学习过程中在特定情况下什么是好的或坏的动作。奖励函数充当这个指导,评估汽车采取的动作并给予奖励(标量值),指示该动作在该情况下的可取性。例如,在左转时,采取“左转”动作将被认为是最佳的(例如,奖励=1;在 0-1 范围内),但采取“右转”动作将是不好的(奖励=0)。强化学习系统最终根据奖励函数收集这些指导并训练模型。这是训练汽车的最关键部分,也是我们将重点关注的部分。
最后,当我们组装系统时,原理图流程如下:
Input (120x160 grayscale Image) → (reinforcement learning Model) →
Output (left, right, straight)
在 AWS DeepRacer 中,奖励函数是模型构建过程中的重要部分。在训练 AWS DeepRacer 模型时,我们必须提供它。
在一个 episode 中,代理与赛道互动,学习最优动作集,以最大化预期累积奖励。但是,单个 episode 并不产生足够的数据来训练代理。因此,我们最终会收集许多 episode 的数据。定期,在每个第 n 个 episode 结束时,我们启动一个训练过程,生成一个强化学习模型的迭代。我们运行许多迭代来生成我们能够的最佳模型。这个过程在下一节中详细解释。训练结束后,代理通过在模型上运行推理来执行自主驾驶,以根据图像输入采取最佳动作。模型的评估可以在模拟环境中使用虚拟代理或在物理 AWS DeepRacer 汽车的真实环境中进行。
终于是时候创建我们的第一个模型了。因为汽车的输入是固定的,即来自摄像头的单个图像,所以我们只需要关注输出(动作)和奖励函数。我们可以按照以下步骤开始训练模型。
构建我们的第一个强化学习
要进行这个练习,您需要一个 AWS 账户。使用您的账户凭据登录 AWS 控制台,如图 17-2 所示。

图 17-2。AWS 登录控制台
首先,让我们确保我们在北弗吉尼亚地区,因为该服务仅在该地区提供,并转到 DeepRacer 控制台页面:https://console.aws.amazon.com/deepracer/home?region=us-east-1#getStarted。
在选择“强化学习”后,模型页面会打开。该页面显示了所有已创建模型的列表以及每个模型的状态。要创建模型,请从这里开始该过程。

图 17-3。训练 AWS DeepRacer 模型的工作流程
步骤 1:创建模型
我们将创建一个模型,供 AWS DeepRacer 汽车在赛道上自主驾驶(采取动作)。我们需要选择特定的赛道,提供我们的模型可以选择的动作,提供一个奖励函数,用于激励我们期望的驾驶行为,并配置训练期间使用的超参数。
在 AWS DeepRacer 控制台上创建模型
图 17-4。在 AWS DeepRacer 控制台上创建模型
步骤 2:配置训练
在这一步中,我们选择我们的训练环境,配置动作空间,编写奖励函数,并在启动训练作业之前调整其他与训练相关的设置。
配置模拟环境
我们的强化学习模型训练发生在模拟赛道上,我们可以选择赛道来训练我们的模型。我们将使用 AWS RoboMaker,这是一个使构建机器人应用程序变得简单的云服务,来启动模拟环境。
在训练模型时,我们选择与我们打算在赛道上比赛的最终赛道最相似的赛道。截至 2019 年 7 月,AWS DeepRacer 提供了七个可以进行训练的赛道。虽然配置这样一个辅助环境并不是必需的,也不能保证一个好的模型,但它将最大化我们的模型在赛道上表现最好的可能性。此外,如果我们在一条直线赛道上训练,那么我们的模型很可能不会学会如何转弯。就像在监督学习的情况下,模型不太可能学会不属于训练数据的内容一样,在强化学习中,代理人不太可能从训练环境中学到超出范围的内容。对于我们的第一个练习,选择 re:Invent 2018 赛道,如图 17-5 所示。
要训练一个强化学习模型,我们必须选择一个学习算法。目前,AWS DeepRacer 控制台仅支持近端策略优化(PPO)算法。团队最终将支持更多的算法,但选择 PPO 是为了更快的训练时间和更优越的收敛性能。训练一个强化学习模型是一个迭代的过程。首先,定义一个奖励函数来覆盖代理在环境中的所有重要行为是一个挑战。其次,通常需要调整超参数以确保令人满意的训练性能。这两者都需要实验。一个谨慎的方法是从一个简单的奖励函数开始,这将是本章的方法,然后逐步增强它。AWS DeepRacer 通过允许我们克隆一个训练好的模型来促进这个迭代过程,在这个模型中,我们可以增强奖励函数以处理之前被忽略的变量,或者我们可以系统地调整超参数直到结果收敛。检测这种收敛的最简单方法是查看日志,看看汽车是否超过了终点线;换句话说,进展是否达到了 100%。或者,我们可以直观地观察汽车的行为,并确认它是否超过了终点线。

图 17-5。在 AWS DeepRacer 控制台上选择赛道
配置动作空间
接下来,我们配置我们的模型在训练期间和训练后选择的动作空间。一个动作(输出)是速度和转向角的组合。目前在 AWS DeepRacer 中,我们使用离散动作空间(固定的动作集)而不是连续动作空间(以x速度转动x度,其中x和y取实值)。这是因为更容易映射到物理汽车上的值,我们稍后会深入探讨这一点在“驾驶 AWS DeepRacer 汽车”。为了构建这个离散动作空间,我们指定了最大速度、速度级别、最大转向角和转向级别,如图 17-6 所示。

图 17-6。在 AWS DeepRacer 控制台上定义动作空间
以下是动作空间的配置参数:
最大转向角度
这是汽车前轮可以向左和向右转动的最大角度。轮子可以转动的角度是有限的,因此最大转向角度为 30 度。
转向角度粒度
指的是最大转向角两侧的转向间隔数。因此,如果我们的最大转向角为 30 度,+30 度是向左,-30 度是向右。具有 5 个转向粒度时,从左到右的转向角如图 17-6 所示,将在行动空间中:30 度,15 度,0 度,-15 度和-30 度。转向角始终围绕 0 度对称。
最大速度
指的是模拟器中车辆将以米/秒(m/s)为单位测量的最大速度驾驶的速度。
速度级别
指的是从最大速度(包括)到零(不包括)的速度级别数。因此,如果我们的最大速度是 3m/s,速度粒度为 3,那么我们的行动空间将包含 1m/s、2m/s 和 3m/s 的速度设置。简单来说,3m/s 除以 3 等于 1m/s,所以从 0m/s 到 3m/s 以 1m/s 的增量进行。0m/s 不包括在行动空间中。
根据前面的例子,最终的行动空间将包括 15 个离散行动(三种速度 x 五种转向角),这些应该在 AWS DeepRacer 服务中列出。随意尝试其他选项,只需记住较大的行动空间可能需要更长时间进行训练。
提示
根据我们的经验,以下是一些建议如何配置行动空间:
-
我们的实验表明,具有更快最大速度的模型收敛所需的时间比具有较慢最大速度的模型更长。在某些情况下(奖励函数和赛道相关),5m/s 模型收敛可能需要超过 12 小时。
-
我们的模型不会执行不在行动空间中的行动。同样,如果模型在从未需要使用此行动的赛道上进行训练,例如,在直道上不会激励转弯,那么模型将不知道如何使用此行动,因为它不会被激励转弯。在开始考虑构建强大模型时,请确保记住行动空间和训练赛道。
-
指定快速速度或大转向角是很好的,但我们仍然需要考虑我们的奖励函数,以及是否有意义全速驶入转弯,或在赛道的直线段上展示之字形行为。
-
我们还需要牢记物理学。如果我们尝试以超过 5m/s 的速度训练模型,我们可能会看到我们的车在拐弯时打滑,这可能会增加模型收敛的时间。
配置奖励函数
正如我们之前解释的那样,奖励函数评估了在给定情况下行动结果的质量,并相应地奖励该行动。在实践中,奖励是在每次行动后进行训练时计算的,并且构成了用于训练模型的经验的关键部分。然后我们将元组(状态,行动,下一个状态,奖励)存储在内存缓冲区中。我们可以使用模拟器提供的多个变量来构建奖励函数逻辑。这些变量代表了车辆的测量,如转向角和速度;车辆与赛道的关系,如(x,y)坐标;以及赛道,如路标(赛道上的里程碑标记)。我们可以使用这些测量值来在 Python 3 语法中构建我们的奖励函数逻辑。
所有参数都作为字典提供给奖励函数。它们的键,数据类型和描述在图 17-7 中有文档记录,一些更微妙的参数在图 17-8 中有进一步说明。
奖励函数参数(这些参数的更深入审查可在文档中找到)
图 17-7. 奖励函数参数(这些参数的更深入审查可在文档中找到)
图解释了一些奖励函数参数
图 17-8. 奖励函数参数的可视化解释
为了构建我们的第一个模型,让我们选择一个示例奖励函数并训练我们的模型。让我们使用默认模板,在其中汽车试图跟随中心虚线,如图 17-9 所示。这个奖励函数背后的直觉是沿着赛道采取最安全的导航路径,因为保持在中心位置可以使汽车远离赛道外。奖励函数的作用是:在赛道周围创建三个层次,使用三个标记,然后为在第二层次驾驶的汽车提供更多奖励,而不是在中心或最后一层次驾驶。还要注意奖励的大小差异。我们为保持在狭窄的中心层次提供 1 的奖励,为保持在第二(偏离中心)层次提供 0.5 的奖励,为保持在最后一层次提供 0.1 的奖励。如果我们减少中心层次的奖励,或增加第二层次的奖励,实质上我们在激励汽车使用更大的赛道表面。记得你考驾照的时候吗?考官可能也是这样做的,当你靠近路缘或车道标志时扣分。这可能会很有用,特别是在有急转弯的情况下。

图 17-9. 一个示例奖励函数
以下是设置这一切的代码:
def reward_function(params):
'''
Example of rewarding the agent to follow center line
'''
# Read input parameters
track_width = params['track_width']
distance_from_center = params['distance_from_center']
# Calculate 3 markers that are at varying distances away from the center line
marker_1 = 0.1 * track_width
marker_2 = 0.25 * track_width
marker_3 = 0.5 * track_width
# Give higher reward if the car is closer to center line and vice versa
if distance_from_center <= marker_1:
reward = 1.0
elif distance_from_center <= marker_2:
reward = 0.5
elif distance_from_center <= marker_3:
reward = 0.1
else:
reward = 1e-3 # likely crashed/ close to off track
return float(reward)
因为这是第一次训练运行,让我们专注于理解创建和评估基本模型的过程,然后专注于优化它。在这种情况下,我们跳过算法设置和超参数部分,使用默认设置。
注意
常见问题:奖励应该在某个范围内吗,我可以给负奖励吗?
没有真正的约束来决定我们可以奖励和不奖励什么,但作为一个良好的实践,更容易理解奖励的方式是将它们放在 0-1 或 0-100 的范围内。更重要的是,我们的奖励尺度应该适当地为行为提供相对奖励。例如,在右转时,我们应该用高奖励奖励正确的行为,用接近 0 的奖励奖励左转行为,也许用介于两者之间的奖励或高于左转行为的奖励奖励直行行为,因为这可能不是完全错误的行为。
配置停止条件
这是我们开始训练之前的最后一节。在这里,我们指定模型将训练的最长时间。这是一个方便的机制,让我们可以终止训练,因为我们将根据训练时间计费。
指定 60 分钟,然后选择“开始训练”。如果出现错误,我们将被带到错误位置。开始训练后,可能需要长达六分钟的时间来启动所需的服务(如 Amazon SageMaker、AWS Robomaker、AWS Lambda、AWS Step Function)来开始训练。请记住,如果我们确定模型已经收敛(如下一节所述),我们随时可以通过点击“停止”按钮提前停止训练。
第三步:模型训练
当我们的模型开始训练后,我们可以从 DeepRacer 控制台上列出的模型中选择它。然后,我们可以通过查看随时间变化的总奖励图以及从模拟器中汽车的第一人称视角来定量地了解训练的进展情况(参见图 17-10)。
起初,我们的车无法在直路上行驶,但随着它学习到更好的驾驶行为,我们应该看到它的表现提高,奖励图增加。此外,当我们的车驶离赛道时,它将被重置在赛道上。我们可能会观察到奖励图呈波动状态。
注意
常见问题:为什么奖励图呈波动状态?
代理从高探索开始,并逐渐开始利用训练模型。因为代理始终对其决策的一部分采取随机动作,所以可能会有时候完全做出错误决定并最终偏离轨道。这通常在训练开始时很高,但随着模型开始学习,这种尖锐性应该会减少。
日志始终是关于我们模型训练更细粒度信息的良好来源。在本章后面,我们将探讨如何以编程方式使用日志来更深入地了解我们模型的训练。与此同时,我们可以查看 Amazon SageMaker 和 AWS RoboMaker 的日志文件。日志输出到 Amazon CloudWatch。要查看日志,请将鼠标悬停在奖励图上,并选择刷新按钮下方出现的三个点。然后,选择“查看日志”。因为模型训练需要一个小时,这可能是一个好时机跳到下一节,了解更多关于强化学习的知识。

图 17-10。AWS DeepRacer 控制台上的训练图和模拟视频流
第 4 步:评估模型的性能
在强化学习中,评估模型能力的最佳方法是运行它,使其仅利用——也就是说,它不会采取随机动作。在我们的情况下,首先在类似于训练的赛道上测试它,看看它是否能复制训练行为。接下来,尝试在不同的赛道上测试泛化能力。在我们的模型训练完成后,我们可以开始模型评估。从我们观察到训练的模型详细信息页面中,选择“开始评估”。现在我们可以选择要评估我们模型性能的赛道以及圈数。选择“re:Invent 2018”赛道和 5 圈,然后选择开始。完成后,我们应该看到与图 17-11 中显示的类似的页面,总结了我们模型尝试绕过赛道并完成圈数的结果。

图 17-11。AWS DeepRacer 控制台上的模型评估页面
干得好!我们已经成功构建了我们的第一个强化学习启用的自动驾驶汽车。
注意
常见问题:当我运行评估时,有时只看到 100%的完成率?
当模型在模拟器中运行推理并在赛道周围导航时,由于模拟器的保真度,汽车可能会以稍微不同的位置结束相同的动作——换句话说,15 度左转动作可能只会导致 14.9 度。实际上,我们在模拟器中观察到的只是非常小的偏差,但这些小偏差会随着时间的推移而累积。训练良好的模型能够从接近越野位置恢复,但训练不足的模型可能无法从接近事故的位置恢复。
现在我们的模型已经成功评估,我们继续改进它,并学习如何实现更好的圈速。但在此之前,您需要更多地了解强化学习背后的理论,并深入了解学习过程。
注意
我们可以开始使用 AWS DeepRacer 控制台服务创建我们的第一个模型,对其进行训练(最多六个小时),评估它,并免费提交给 AWS DeepRacer 联赛。
行动中的强化学习
让我们更仔细地看看强化学习的实际应用。在这里,我们讨论一些理论、内部工作原理以及与我们自动驾驶汽车项目相关的许多实用见解。
强化学习系统是如何学习的?
首先,我们必须理解探索与利用。类似于孩子学习的方式,强化学习系统通过探索和发现什么是好的和坏的来学习。在孩子的情况下,父母指导或告诉孩子他的错误,评价所做的决定是好还是坏,或者好坏程度如何。孩子记住了他在特定情况下所做的决定,并试图在合适的时机重播,或者利用那些决定。实质上,孩子试图最大化父母的认可或欢呼。在孩子的早期生活中,它更加关注父母的建议,从而创造学习的机会。而在后来的生活中,成年人很少听从父母的建议,而是利用他们学到的概念来做决定。
如图 17-12 所示,在每个时间步“t”,DeepRacer 汽车(代理)接收到它的当前观察,状态(S[t]),并基于此选择一个行动(A[t])。作为代理所采取的行动的结果,它获得奖励 R[t+1]并移动到状态 S[t+1],这个过程在整个 episode 中持续进行。
深度强化学习理论基础要点
图 17-12. 深度强化学习理论基础要点
在 DeepRacer 的背景下,代理通过采取随机行动来进行探索,奖励函数,本质上就像父母一样,告诉汽车它在给定状态下所采取的行动是好还是不好。通常,对于给定状态的行动的“好坏”被表示为一个数字,较高的数字意味着它接近最优,较低的数字意味着它不是。系统为每一步记录所有这些信息,具体包括:当前状态,采取的行动,行动的奖励和下一个状态(s,a,r,s'),这就是我们所谓的经验回放缓冲区,本质上是一个临时内存缓冲区。整个想法在于汽车可以从哪些决定是好的以及其他决定是坏的中学习。关键点是我们从高度探索开始,并逐渐增加利用。
在 DeepRacer 模拟器中,我们以每秒 15 帧的速度采样输入图像状态。在每一步或捕获的图像帧中,汽车从一个状态转移到另一个状态。每个图像代表汽车所处的状态,最终在强化学习模型训练后,它将试图通过推断采取哪种行动来进行利用。为了做出决策,我们可以随机采取行动,也可以使用我们的模型进行推荐。随着模型的训练,训练过程平衡了探索和利用。首先,我们更多地进行探索,因为我们的模型不太可能很好,但随着它通过经验学习,我们更多地向利用方向转变,让模型更多地控制。图 17-13 描述了这个流程。这种转变可以是线性衰减、指数衰减或任何类似的策略,通常根据我们假设的学习程度进行调整。通常,在实际的强化学习训练中使用指数衰减。

图 17-13. DeepRacer 训练流程
在通过随机选择或模型预测采取行动后,汽车转移到一个新状态。使用奖励函数,计算奖励并分配给结果。这个过程将继续,对于每个状态,直到达到终端状态为止;也就是说,汽车偏离赛道或完成一圈,此时汽车将被重置,然后重复。一步是从一个状态到另一个状态的转换,每一步都会记录一个(状态,行动,新状态,奖励)元组。从重置点到终端状态的所有步骤称为episode。
为了说明一个情节,让我们看一下图 17-14 中一个微型赛道的例子,奖励函数鼓励沿着中心线行驶,因为这是从起点到终点最短、最快的路径。这个情节包括四个步骤:在第 1 和第 2 步,汽车沿着中心线行驶,然后在第 3 步左转 45 度,继续沿着这个方向,最终在第 4 步发生碰撞。

图 17-14. 一个代理在一个情节中探索的插图
我们可以将这些情节视为经验,或者是我们模型的训练数据。定期,强化学习系统会从内存缓冲区中随机选择一小部分这些记录,并训练一个 DNN(我们的深度强化学习模型),以便根据我们的奖励函数“指导”来产生一个最能够在环境中导航的模型;换句话说,获得最大的累积奖励。随着时间的推移或更多的情节,我们会看到一个随着时间变得更好的模型。当然,一个极其重要的警告是奖励函数必须被明确定义,并引导代理朝着目标前进。如果我们的奖励函数不好,我们的模型就无法学习正确的行为。
让我们稍微偏离一下,以了解一个糟糕的奖励函数。当我们最初设计系统时,汽车可以自由选择任何方向(左、右、前、后)。我们最简单的奖励函数没有区分方向,最终模型学会了通过来回摇摆来累积奖励。后来通过激励汽车向前行驶来克服了这个问题。幸运的是,现在的开发者们,DeepRacer 团队通过让汽车只能向前移动,使这一切变得更简单,因此我们甚至不需要考虑这种行为。
现在回到强化学习。我们如何知道模型是否变得更好? 让我们回到探索和利用的概念。记住,我们从高探索、低利用开始,然后逐渐增加利用率。这意味着在训练过程的任何时刻,强化学习系统会探索一定百分比的时间;也就是说,采取随机行动。随着经验缓冲区的增长,对于任何给定状态,它存储了各种决策(行动),包括导致最高奖励和最低奖励的行动。模型基本上利用这些信息来学习和预测在给定状态下采取的最佳行动。假设模型最初对状态 S 采取了“直行”行动,并获得了 0.5 的奖励,但下一次到达状态 S 时,它采取了“右转”行动,并获得了 1 的奖励。如果内存缓冲区中有几个样本的奖励为“1”,对于相同的状态 S 和行动“右转”,模型最终会学习到“右转”是该状态的最佳行动。随着时间的推移,模型会探索的时间越来越少,利用的时间越来越多,这个百分比分配会线性或指数地改变,以便实现最佳学习。
关键在于,如果模型正在采取最佳行动,系统会学会继续选择这些行动;如果没有选择最佳行动,系统将继续尝试学习给定状态的最佳行动。从实际角度来看,可能存在许多到达目标的路径,但对于图 17-13 中的小型赛道示例来说,最快的路径是沿着赛道中间直线前进,因为实际上任何转弯都会减慢汽车速度。在训练过程中,在模型开始学习的早期阶段,我们观察到它可能到达终点线(目标),但可能没有选择最佳路径,如图 17-15(左)所示,汽车来回穿梭,因此需要更长时间到达终点线,累积奖励仅为 9。但因为系统仍然继续探索一部分时间,代理给自己机会找到更好的路径。随着经验的积累,代理学会找到最佳路径并收敛到一个最佳策略,以达到总累积奖励 18,如图 17-15(右)所示。

图 17-15. 达到目标的不同路径示例
从数量上来说,对于每一集,你应该看到奖励逐渐增加的趋势。如果模型做出了最佳决策,汽车必须在赛道上并且在课程中导航,从而累积奖励。然而,你可能会看到即使在高奖励的集数之后,图表也会下降,这可能是因为汽车仍然具有较高程度的探索,正如前面提到的那样。
强化学习理论
现在我们了解了强化学习系统是如何学习和工作的,特别是在 AWS DeepRacer 的背景下,让我们来看一些正式的定义和一般的强化学习理论。当我们使用强化学习解决其他问题时,这些背景知识将会很有用。
马尔可夫决策过程
马尔可夫决策过程(MDP)是一个用于建模控制过程中决策制定的离散随机状态转移过程框架。马尔可夫性质定义了每个状态仅仅依赖于前一个状态。这是一个方便的性质,因为这意味着要进行状态转移,所有必要的信息必须在当前状态中可用。强化学习中的理论结果依赖于问题被制定为一个 MDP,因此重要的是要理解如何将问题建模为一个 MDP,以便使用强化学习来解决。
无模型与基于模型
在这个背景下,模型指的是对环境的学习表示。这是有用的,因为我们可以潜在地学习环境中的动态并使用模型训练我们的代理,而不必每次都使用真实环境。然而,在现实中,学习环境并不容易,通常更容易在模拟中拥有真实世界的表示,然后联合学习感知和动态作为代理导航的一部分,而不是按顺序一个接一个地学习。在本章中,我们只关注无模型的强化学习。
基于价值
对于代理采取的每个动作,奖励函数都会分配相应的奖励。对于任何给定的状态-动作对,了解其价值(奖励)是有帮助的。如果这样的函数存在,我们可以计算在任何状态下可以实现的最大奖励,并简单地选择相应的动作来导航环境。例如,在一个 3x3 的井字棋游戏中,游戏情况的数量是有限的,因此我们可以建立一个查找表,以便在给定情况下给出最佳移动。但是在国际象棋游戏中,考虑到棋盘的大小和游戏的复杂性,这样的查找表将是计算昂贵的,并且存储空间将会很大。因此,在复杂的环境中,很难列出状态-动作值对或定义一个可以将状态-动作对映射到值的函数。因此,我们尝试使用神经网络通过对价值函数进行参数化,并使用神经网络来近似给定状态观察下每个动作的价值。一个基于值的算法的例子是深度 Q 学习。
基于策略
策略是代理学习如何在环境中导航的一组规则。简单来说,策略函数告诉代理从当前状态中采取哪个动作是最佳动作。基于策略的强化学习算法,如 REINFORCE 和策略梯度,找到最佳策略,无需将值映射到状态。在强化学习中,我们对策略进行参数化。换句话说,我们允许神经网络学习什么是最佳策略函数。
基于策略还是基于值——为什么不两者兼而有之?
一直存在关于使用基于策略还是基于值的强化学习的争论。新的架构尝试同时学习价值函数和策略函数,而不是保持其中一个固定。这种在强化学习中的方法被称为演员评论家。
您可以将演员与策略关联起来,将评论家与价值函数关联起来。演员负责采取行动,评论家负责估计这些行动的“好坏”或价值。演员将状态映射到动作,评论家将状态-动作对映射到值。在演员评论家范式中,这两个网络(演员和评论家)使用梯度上升分别进行训练,以更新我们深度神经网络的权重。(请记住,我们的目标是最大化累积奖励;因此,我们需要找到全局最大值。)随着情节的推移,演员变得更擅长采取导致更高奖励状态的行动,评论家也变得更擅长估计这些行动的价值。演员和评论家学习的信号纯粹来自奖励函数。
给定状态-动作对的价值称为Q 值,表示为 Q(s,a)。我们可以将 Q 值分解为两部分:估计值和衡量动作优于其他动作的因素的量化度量。这个度量被称为优势函数。我们可以将优势视为给定状态-动作对的实际奖励与该状态的预期奖励之间的差异。差异越大,我们离选择最佳动作就越远。
考虑到估计状态的价值可能会成为一个困难的问题,我们可以专注于学习优势函数。这使我们能够评估动作不仅基于其有多好,还基于它可能有多好。这使我们比其他简单的基于策略梯度的方法更容易收敛到最佳策略,因为通常策略网络具有很高的方差。
延迟奖励和折扣因子(γ)
根据采取的动作,每个状态转换都会获得奖励。但这些奖励的影响可能是非线性的。对于某些问题,即时奖励可能更重要,在某些情况下,未来奖励可能更重要。例如,如果我们要构建一个股票交易算法,未来奖励可能具有更高的不确定性;因此,我们需要适当地进行折现。折现因子(γ)是介于[0,1]之间的乘法因子,接近零表示即时未来奖励更重要。对于接近 1 的高值,代理将专注于采取最大化未来奖励的行动。
AWS DeepRacer 中的强化学习算法
首先,让我们看一个最简单的策略优化强化学习算法的例子:香草策略梯度。

图 17-16. 香草策略梯度算法的训练过程
我们可以将深度强化学习模型看作由两部分组成:输入嵌入器和策略网络。输入嵌入器将从图像输入中提取特征并将其传递给策略网络,策略网络做出决策;例如,预测对于给定输入状态哪个动作是最佳的。鉴于我们的输入是图像,我们使用卷积层(CNNs)来提取特征。因为策略是我们想要学习的内容,我们对策略函数进行参数化,最简单的方法是使用全连接层进行学习。输入 CNN 层接收图像,然后策略网络使用图像特征作为输入并输出一个动作。因此,将状态映射到动作。随着模型的训练,我们变得更擅长映射输入空间和提取相关特征,同时优化策略以获得每个状态的最佳动作。我们的目标是收集最大的累积奖励。为了实现这一目标,我们更新模型权重以最大化累积未来奖励,通过这样做,我们给导致更高累积未来奖励的动作赋予更高的概率。在以前训练神经网络时,我们使用随机梯度下降或其变体;在训练强化学习系统时,我们寻求最大化累积奖励;因此,我们不是最小化,而是最大化。因此,我们使用梯度上升来将权重移动到最陡峭奖励信号的方向。
DeepRacer 使用一种高级的策略优化变体,称为 Proximal Policy Optimization(PPO),在图 17-17 中进行了总结。

图 17-17. 使用 PPO 算法进行训练
在图 17-17 的左侧,我们的模拟器使用最新的策略(模型)获取新的经验(s,a,r,s')。经验被馈送到经验重放缓冲区中,在我们完成一定数量的周期后,将经验馈送给我们的 PPO 算法。
在图 17-17 的右侧,我们使用 PPO 更新我们的模型。尽管 PPO 是一种策略优化方法,但它使用了我们之前描述的优势演员-评论家方法。我们计算 PPO 梯度并将策略移动到我们获得最高奖励的方向。盲目地朝这个方向迈大步可能会导致训练中的变化过大;如果我们迈小步,训练可能会持续很长时间。PPO 通过限制每个训练步骤中策略可以更新的程度来改善策略(演员)的稳定性。这是通过使用剪切的替代目标函数来实现的,它防止策略更新过多,从而解决了策略优化方法中常见的大方差问题。通常情况下,对于 PPO,我们保持新旧策略的比率在[0.8, 1.2]。评论家告诉演员采取的行动有多好,以及演员应该如何调整其网络。在策略更新后,新模型被发送到模拟器以获取更多经验。
以 DeepRacer 为例的深度强化学习总结
要使用强化学习解决任何问题,我们需要按照以下步骤进行:
-
定义目标。
-
选择输入状态。
-
定义动作空间。
-
构建奖励函数。
-
定义 DNN 架构。
-
选择强化学习优化算法(DQN、PPO 等)。
训练强化学习模型的基本方式在构建自动驾驶汽车或构建机器人手臂抓取物体时并没有改变。这是该范式的一个巨大优势,因为它允许我们专注于更高层次的抽象。要使用强化学习解决问题,首要任务是将问题定义为 MDP,然后定义输入状态和代理在给定环境中可以采取的一组动作,以及奖励函数。实际上,奖励函数可能是最难定义的部分之一,通常也是最重要的,因为这会影响我们的代理学习的策略。在定义了与环境相关的因素之后,我们可以专注于深度神经网络架构应该如何将输入映射到动作,然后选择强化学习算法(基于价值、基于策略、演员-评论家)进行学习。选择算法后,我们可以专注于控制算法行为的高级旋钮。当我们驾驶汽车时,我们倾向于关注控制,对内燃机的理解并不会太大程度上影响我们的驾驶方式。同样,只要我们了解每个算法暴露的旋钮,我们就可以训练强化学习模型。
现在是时候结束了。让我们制定 DeepRacer 赛车问题:
-
目标:在最短时间内绕过赛道完成一圈
-
输入:灰度 120x160 图像
-
动作:具有组合速度和转向角值的离散动作
-
奖励:奖励汽车在赛道上行驶,鼓励更快行驶,并防止进行大量校正或曲线行为
-
DNN 架构:三层 CNN + 全连接层(输入 → CNN → CNN → CNN → FC → 输出)
-
优化算法:PPO
步骤 5:改进强化学习模型
我们现在可以着手改进我们的模型,并了解我们模型训练的见解。首先,我们专注于在控制台中进行训练改进。我们可以改变强化学习算法设置和神经网络超参数。
算法设置
这一部分指定了在训练过程中强化学习算法将使用的超参数。超参数用于提高训练性能。
神经网络的超参数
表 17-2 介绍了可调整神经网络的超参数。尽管默认值在实践中被证明是不错的,但从实际角度来看,开发人员应该专注于批量大小、时代数量和学习率,因为它们被发现对生成高质量模型最有影响;也就是说,充分利用我们的奖励函数。
表 17-2. 深度神经网络可调超参数的描述和指导
| 参数 | 描述 | 提示 |
|---|---|---|
| 批量大小 | 从经验缓冲区中随机抽取的最近车辆经验数量,用于更新底层深度学习神经网络权重。如果我们在缓冲区中有 5,120 个经验,并指定批量大小为 512,那么忽略随机抽样,我们将获得 10 个经验批次。每个批次将依次用于在训练过程中更新我们的神经网络权重。 | 使用更大的批量大小可以促进神经网络权重的更稳定和平滑的更新,但要注意训练可能会变慢。 |
| 时代数量 | 一个时代代表对所有批次的一次遍历,神经网络权重在处理每个批次后更新,然后继续下一个批次。十个时代意味着我们逐个更新神经网络权重,使用所有批次,但重复这个过程 10 次。 | 使用更多的时代数量可以促进更稳定的更新,但预计训练会变慢。当批量大小较小时,可以使用较少的时代数量。 |
| 学习率 | 学习率控制神经网络权重的更新幅度。简单来说,当我们需要改变策略的权重以获得最大累积奖励时,我们应该如何调整我们的策略。 | 更大的学习率会导致更快的训练,但可能难以收敛。较小的学习率会导致稳定的收敛,但训练时间可能较长。 |
| 探索 | 这指的是确定探索和利用之间的权衡方法。换句话说,我们应该使用什么方法来确定何时停止探索(随机选择动作)以及何时利用我们积累的经验。 | 由于我们将使用离散动作空间,我们应该始终选择“CategoricalParameters”。 |
| 熵 | 添加到动作空间的概率分布中的不确定性或随机性程度。这有助于促进选择随机动作,以更广泛地探索状态/动作空间。 | |
| 折扣因子 | 一个指定未来奖励对预期累积奖励的贡献程度的因子。折扣因子越大,模型查看未来奖励以确定预期累积奖励的距离越远,训练速度越慢。使用折扣因子为 0.9 时,车辆包括来自 10 个未来步骤的奖励以进行移动。使用折扣因子为 0.999 时,车辆考虑来自 1,000 个未来步骤的奖励以进行移动。 | 推荐的折扣因子值为 0.99、0.999 和 0.9999。 |
| 损失类型 | 损失类型指定用于更新网络权重的目标函数(成本函数)的类型。对于小的更新,Huber 和均方误差损失类型的行为类似。但随着更新变大,Huber 损失相对于均方误差损失采取较小的增量。 | 当出现收敛问题时,使用 Huber 损失类型。当收敛良好且希望训练更快时,使用均方误差损失类型。 |
| 每次训练之间的剧集数量 | 此参数控制汽车在每次模型训练迭代之间应获取多少经验。对于具有更多局部最大值的更复杂问题,需要更大的经验缓冲区,以提供更多不相关的数据点。在这种情况下,训练会更慢但更稳定。 | 推荐值为 10、20 和 40。 |
模型训练见解
在模型训练完成后,从宏观角度来看,随时间变化的奖励图表,就像图 17-10 中的图表,让我们了解了训练的进展以及模型开始收敛的点。但它并没有给我们一个收敛策略的指示,也没有让我们了解我们的奖励函数的行为,或者汽车速度可以改进的地方。为了获得更多见解,我们开发了一个Jupyter Notebook,分析训练日志,并提供建议。在本节中,我们将看一些更有用的可视化工具,可以用来深入了解我们模型的训练。
日志文件记录了汽车所采取的每一步。在每一步中,它记录了汽车的 x、y 位置,偏航(旋转),转向角,油门,从起点开始的进度,采取的行动,奖励,最近的航路点等等。
热图可视化
对于复杂的奖励函数,我们可能想要了解赛道上的奖励分布;也就是说,奖励函数在赛道上给汽车奖励的位置以及大小。为了可视化这一点,我们可以生成一个热图,如图 17-18 所示。鉴于我们使用的奖励函数在赛道中心附近给出最大奖励,我们看到该区域很亮,中心线两侧的一个小带是红色的,表示奖励较少。最后,赛道的其余部分是黑暗的,表示当汽车在这些位置时没有奖励或接近 0 的奖励。我们可以按照代码生成自己的热图,并调查我们的奖励分布。

图 17-18. 示例中心线奖励函数的热图可视化
改进我们模型的速度
在我们运行评估之后,我们得到了圈速的结果。此时,我们可能会对汽车所采取的路径或失败的地方感到好奇,或者它减速的地方。为了了解所有这些,我们可以使用这个笔记本中的代码来绘制一张赛道热图。在接下来的示例中,我们可以观察汽车在赛道周围导航时所采取的路径,并可视化它通过赛道上各个点的速度。这让我们了解了我们可以优化的部分。快速查看图 17-19(左)表明汽车在赛道的直线部分并没有真正快;这为我们提供了一个机会。我们可以通过给予更多奖励来激励模型在赛道的这一部分更快地行驶。

图 17-19. 评估运行的速度热图;(左)使用基本示例奖励函数的评估圈,(右)使用修改后的奖励函数更快的圈
在图 17-19(左)中,有时汽车似乎有一个小的曲折模式,因此这里的一个改进可能是当汽车转弯过多时对其进行惩罚。在接下来的代码示例中,如果汽车转向超过阈值,我们将奖励乘以 0.8 的因子。我们还通过给予汽车 20%的额外奖励来激励汽车更快地行驶,如果汽车以 2 米/秒或更快的速度行驶。当使用这个新的奖励函数进行训练时,我们可以看到汽车比以前的奖励函数更快。图 17-19(右)显示更加稳定;汽车几乎完美地沿着中心线行驶,并且完成一圈比我们第一次尝试快大约两秒。这只是改进模型的一个简短介绍。我们可以继续迭代我们的模型,并使用这些工具记录更好的圈速。所有建议都包含在这里显示的奖励函数示例中:
def reward_function(params):
'''
Example of penalize steering, which helps mitigate zigzag behaviors and
speed incentive
'''
# Read input parameters
distance_from_center = params['distance_from_center']
track_width = params['track_width']
steering = abs(params['steering_angle']) # Only need absolute steering angle
speed = params['speed'] # in meter/sec
# Calculate 3 markers that are at varying distances away from the center line
marker_1 = 0.1 * track_width
marker_2 = 0.25 * track_width
marker_3 = 0.5 * track_width
# Give higher reward if the agent is closer to the center line and vice versa
if distance_from_center <= marker_1:
reward = 1
elif distance_from_center <= marker_2:
reward = 0.5
elif distance_from_center <= marker_3:
reward = 0.1
else:
reward = 1e-3 # likely crashed/ close to off track
# Steering penalty threshold, change the number based on your action space
setting
ABS_STEERING_THRESHOLD = 15
# Penalize reward if the agent is steering too much
if steering > ABS_STEERING_THRESHOLD:
reward *= 0.8
# Incentivize going faster
if speed >= 2:
reward *= 1.2
比赛 AWS DeepRacer 汽车
现在是时候将我们从虚拟世界学到的知识带到现实世界中,与一辆真正的自动驾驶汽车比赛。当然是玩具大小的!
如果您拥有 AWS DeepRacer 汽车,请按照提供的说明在实际汽车上测试您的模型。对于有兴趣购买汽车的人,AWS DeepRacer 可以在亚马逊上购买。
建造赛道
现在我们有了一个训练过的模型,我们可以在真实赛道上评估这个模型,并使用实际的 AWS DeepRacer 汽车。首先,让我们在家里建造一个临时赛道来比赛我们的模型。为简单起见,我们只会建造部分赛道,但提供了如何建造整个赛道的说明在这里。
要建造一条赛道,您需要以下材料:
对于赛道边界:
我们可以使用约两英寸宽的白色或米色胶带在深色赛道表面上创建一条赛道。在虚拟环境中,赛道标记的厚度设置为两英寸。对于深色表面,使用白色或米色胶带。例如,1.88 英寸宽的珍珠白色胶带或1.88 英寸(粘性较小)的粘贴胶带。
对于赛道表面:
我们可以在深色硬地板上创建一条赛道,例如硬木、地毯、混凝土或沥青毡。后者模仿了现实世界的道路表面,反射很少。
AWS DeepRacer 单圈赛道模板
这个基本的赛道模板由两个直线赛道段连接的弯曲赛道段组成,如图 17-20 所示。使用这个赛道训练的模型应该使我们的 AWS DeepRacer 车辆直线行驶或向一个方向转弯。指定的转弯角度仅供参考;在铺设赛道时,我们可以使用近似的测量值。

图 17-20。测试赛道布局
在 AWS DeepRacer 上运行模型
要启动 AWS DeepRacer 车辆的自动驾驶,我们必须将至少一个 AWS DeepRacer 模型上传到我们的 AWS DeepRacer 车辆。
要上传模型,请从 AWS DeepRacer 控制台中选择我们训练过的模型,然后将模型工件从其 Amazon S3 存储下载到可以从计算机访问的(本地或网络)驱动器上。在模型页面上提供了一个方便的下载模型按钮。
要将训练过的模型上传到车辆上,请执行以下操作:
-
从设备控制台的主导航窗格中,选择模型,如图 17-21 所示。
![AWS DeepRacer 汽车网络控制台上的模型上传菜单]()
图 17-21。AWS DeepRacer 汽车网络控制台上的模型上传菜单
-
在模型页面上,选择上传,位于模型列表上方。
-
从文件选择器中,导航到您下载模型工件的驱动器或共享位置,并选择要上传的模型。
-
成功上传模型后,它将被添加到模型列表中,并可以加载到车辆的推理引擎中。
让 AWS DeepRacer 车辆自主驾驶
要开始自动驾驶,请将车辆放在物理赛道上,并执行以下操作:
-
按照说明登录到车辆的设备控制台,然后执行以下操作进行自动驾驶:
-
在“控制车辆”页面的控制部分中,选择“自动驾驶”,如图 17-22 所示。
![AWS DeepRacer 车辆 Web 控制台上的驾驶模式选择菜单]()
图 17-22. AWS DeepRacer 车辆 Web 控制台上的驾驶模式选择菜单
-
-
在“选择模型”下拉列表中(图 17-23),选择一个已上传的模型,然后选择“加载模型”。这将开始将模型加载到推理引擎中。该过程大约需要 10 秒钟才能完成。
-
调整车辆的“最大速度”设置为训练模型中使用的最大速度的百分比。(诸如真实赛道的表面摩擦等因素可能会降低车辆的最大速度,使其低于训练中使用的最大速度。您需要进行实验以找到最佳设置。)
![AWS DeepRacer 车辆 Web 控制台上的模型选择菜单]()
图 17-23. AWS DeepRacer 车辆 Web 控制台上的模型选择菜单
-
选择“启动车辆”以使车辆自主驾驶。
-
观看车辆在物理赛道上行驶,或在设备控制台上的流媒体视频播放器上观看。
-
要停止车辆,请选择“停止车辆”。
Sim2Real 转移
模拟器可能比真实世界更方便进行训练。在模拟器中可以轻松创建某些场景;例如,汽车与人或其他汽车碰撞——我们不希望在现实世界中这样做 :)。然而,在大多数情况下,模拟器不会具有我们在现实世界中的视觉保真度。此外,它可能无法捕捉真实世界的物理特性。这些因素可能会影响模拟器中环境的建模,因此,即使在模拟中取得了巨大成功,当代理在现实世界中运行时,我们可能会遇到失败。以下是处理模拟器限制的一些常见方法:
系统识别
建立一个数学模型来模拟真实环境,并校准物理系统,使其尽可能真实。
领域适应
将模拟域映射到真实环境,或反之亦然,使用正则化、GAN 或迁移学习等技术。
领域随机化
创建各种具有随机属性的模拟环境,并在所有这些环境的数据上训练模型。
在 DeepRacer 的背景下,模拟保真度是对真实世界的近似表示,物理引擎可能需要一些改进来充分模拟汽车的所有可能的物理特性。但深度强化学习的美妙之处在于我们不需要一切都完美。为了减轻影响汽车的大幅感知变化,我们做了两件重要的事情:a)我们不使用 RGB 图像,而是将图像转换为灰度,使模拟器和真实世界之间的感知差异变窄,b)我们有意使用浅层特征嵌入器;例如,我们只使用了几个 CNN 层;这有助于网络不完全学习模拟环境。相反,它迫使网络只学习重要特征。例如,在赛道上,汽车学会专注于使用白色赛道边缘标记进行导航。查看图 17-24,它使用一种称为 GradCAM 的技术生成图像中最具影响力部分的热图,以了解汽车正在寻找导航的位置。

图 17-24。AWS DeepRacer 导航的 GradCAM 热图
进一步探索
要继续冒险,您可以参与各种虚拟和实体赛车联赛。以下是一些探索的选项。
DeepRacer 联赛
AWS DeepRacer 在 AWS 峰会和每月虚拟联赛中有一个实体联赛。要在当前虚拟赛道上比赛并赢取奖品,请访问联赛页面:https://console.aws.amazon.com/deepracer/home?region=us-east-1#leaderboards。
高级 AWS DeepRacer
我们了解到一些高级开发人员可能希望对模拟环境有更多控制,并且还具有定义不同神经网络架构的能力。为了实现这一体验,我们提供了一个基于Jupyter Notebook的设置,您可以使用它来提供训练自定义 AWS DeepRacer 模型所需的组件。
AI 驾驶奥林匹克
在 2018 年的 NeurIPS 上,AI 驾驶奥林匹克(AI-DO)以自动驾驶汽车的人工智能为重点推出。在第一届比赛中,这项全球竞赛在 Duckietown 平台上展示了车道跟随和车队管理挑战。这个平台有两个组成部分,Duckiebots(微型自动出租车)和 Duckietowns(包含道路、标志的微型城市环境)。Duckiebots 的工作是运送 Duckietown 的市民(小鸭子)。搭载了一个微型摄像头并在树莓派上运行计算,Duckiebots 配备了易于使用的软件,帮助从高中生到大学研究人员相对快速地运行他们的代码。自第一届 AI-DO 以来,这项比赛已经扩展到其他顶级人工智能学术会议,并现在包括涵盖 Duckietown 和 DeepRacer 平台的挑战。

图 17-25。AI 驾驶奥林匹克的 Duckietown
DIY Robocars
DIY Robocars Meetup最初在加利福尼亚州奥克兰开始,现在已经扩展到全球 50 多个 Meetup 小组。这些是有趣而引人入胜的社区,可以尝试与其他自动驾驶和自主无人车领域的爱好者合作。许多人每月举办比赛,是一个很好的场所来进行实体赛车比赛。
Roborace
现在是发挥我们内心的迈克尔·舒马赫的时候了。赛车运动通常被认为是马力的竞争,现在 Roborace 正在将其转变为智能的竞争。Roborace 组织了全电动、自动驾驶赛车之间的比赛,如由丹尼尔·西蒙设计的时尚外观的 Robocar(以未来设计而闻名,如《创:战纪》中的 Tron Legacy Light Cycle),如图 17-26 所示。我们不再谈论微缩比例的汽车了。这些是全尺寸的、重达 1,350 公斤、长 4.8 米,能够达到 200 英里/小时(320 公里/小时)。最好的部分?我们不需要复杂的硬件知识来比赛。
这里真正的明星是人工智能开发人员。每个团队都会得到一辆相同的汽车,因此获胜的关键在于竞争对手编写的自主人工智能软件。车载传感器的输出,如摄像头、雷达、激光雷达、声纳和超声波传感器,都是可用的。为了进行高吞吐量的计算,汽车还装载了强大的 NVIDIA DRIVE 平台,能够每秒处理数万亿次浮点运算。我们需要做的就是构建算法,让汽车保持在赛道上,避免发生事故,并当然尽可能快地领先。
为了开始,Roborace 提供了一个赛车模拟环境,其中提供了真实汽车的精确虚拟模型。随之而来的是虚拟资格赛,获胜者有机会参加包括在电动方程式赛道上的真实比赛。到 2019 年,顶级赛车队已经将差距缩小到最佳人类表现的 5-10%之内。很快,开发人员将站在领奖台上,击败专业车手。最终,这样的比赛会带来创新,希望这些经验可以转化回自动驾驶汽车行业,使它们更安全、更可靠,同时性能更出色。

图 17-26。Roborace 的 Robocar 由 Daniel Simon 设计(图片由 Roborace 提供)
摘要
在前一章中,我们看了如何通过在模拟器内手动驾驶来训练自动驾驶车辆的模型。在本章中,我们探讨了与强化学习相关的概念,并学习了如何制定每个人都在关注的最终问题:如何让自动驾驶汽车学会驾驶。我们利用强化学习的魔力,将人类从循环中移除,教车辆在模拟器中独立驾驶。但为什么要限制在虚拟世界呢?我们将这些经验带入现实世界,并驾驶了一辆真实汽车。而这一切只需要一个小时!
深度强化学习是一个相对较新但令人兴奋的领域,值得进一步探索。最近对强化学习的扩展正在开辟新的问题领域,其应用可以实现大规模自动化。例如,分层强化学习使我们能够建模细粒度的决策制定。元强化学习使我们能够在不同环境中建模广义决策制定。这些框架让我们更接近模仿类似人类行为。毫不奇怪,许多机器学习研究人员认为强化学习有潜力让我们更接近人工通用智能,并开辟了以前被认为是科幻的道路。







































































浙公网安备 33010602011771号