TensorFlow 实战(一)

原文:zh.annas-archive.org/md5/63f1015b3af62117a4a51b25a6d19428

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

前言

如今,很难找到一个不受机器学习驱动或至少不受其影响的真实世界系统或产品。机器学习在提升用户体验、削减成本和增加公司收益方面发挥着至关重要的作用。TensorFlow 是一个机器学习框架,使开发人员能够快速为各种定制用例开发机器学习解决方案。如果您是一名机器学习实践者,甚至是一名触及机器学习系统的软件工程师,了解 TensorFlow 是值得的,因为它被数百万开发人员用于构建机器学习解决方案。

本书将带领您进行一次信息丰富的旅程,涵盖了大多数流行的机器学习任务以及最先进的模型。您将了解图像分类和分割,以及各种自然语言处理任务,例如语言建模和情感分析。在此过程中,我们将努力保持我们的代码生产质量。这意味着我们将探索我们可以标准化代码和模型的方法,例如构建强大的端到端数据管道,可以处理常见的数据类型,例如图像和文本。我们还将注意其他重要的方面,例如模型的可解释性、类似任务的当前最先进性能等等。我们以 TensorFlow 如何用于构建生产级机器学习管道以为开发人员提供流畅的操作体验结束了本书。

TensorFlow 有良好的文档覆盖范围(尽管某些主题可能需要更好的文档),并且免费提供。也许你会想知道,为什么你还需要这本书。TensorFlow 已经发展成一个复杂的生态系统,其中有许多组成部分。对于初学者来说,很容易在文档中迷失方向,并浪费时间(如果不是天数)。新功能和新版本发布的速度加剧了这个问题。因此,有一个资源能够整合 TensorFlow 的最新和最重要的信息以及最佳实践,并将其呈现为一篇易于理解、解释清晰的文本,对于解决这个问题是有帮助的。

阅读完本书后,您将了解如何构建大多数常见的机器学习模型,例如卷积神经网络、循环神经网络和 Transformer。您将了解通用的机器学习生命周期以及如何将其应用于许多不同的任务。此外,您还将熟悉构建数据管道,可以在几行代码中执行复杂的转换。

我希望读者在他们的机器学习职业生涯中取得成功,并真诚地希望他们能从本书涵盖的各种主题中获益匪浅。

致谢

首先,我要感谢我的父母和我的妻子 Thushani,在整个旅程中一直支持我,并始终站在我的身边。我还要感谢 Manning 的编辑们为我提供的所有支持和鼓励。也感谢 Manning 的制作人员提供的宝贵指导。

致所有抽出时间阅读我的手稿并提供反馈的审稿人:Alessandro Buggin、Amaresh Rajasekharan、Biswanath Chowdhury、Brian Griner、David Cronkite、Dhinakaran Venkat、Eduardo Paluzo Hidalgo、Francisco Rivas、Ganesh Swaminathan、Geoff Clark、Gherghe Georgios、Giri S、Jason Hales、José Antonio Quiles、Joshua A McAdams、Kaniskha Tyagi、Kelvin D. Meeks、Kim Falk Jørgensen、Krzysztof Je˛drzejewski、Lawrence Nderu、Levi McClenny、Nguyen Cao、Nikos Kanakaris、Peter Morgan、Ryan Markoff、Sergio Govoni、Sriram Macharla、Tiklu Ganguly、Todd Cook、Tony Holdroyd、Vidhya Vinay、Vincent Ngo、Vipul Gupta、Vishwesh Ravi Shrimali 和 Wei Luo,感谢您的建议,帮助使这本书变得更好。

关于本书

在本节中,我们将讨论这本书适合谁,不同章节及其内容,以及您可以找到代码的位置。

谁应该阅读这本书?

你必须确定这本书适合你。这本书是为机器学习社区中的广泛受众编写的,以提供低门槛的入门机会,因此初学者以及具有基本到中级知识和经验的机器学习从业者可以进一步提高他们的 TensorFlow 技能。要从本书中获得最大收益,您需要以下内容:

  • 在模型开发生命周期中有经验(通过研究/行业项目)。

  • 在 Python 和面向对象编程(OOP)方面具有适度的知识(例如,类、生成器、列表推导)。

  • 对 NumPy/pandas 库有基本的了解(例如,计算汇总统计信息,pandas series DataFrame 对象是什么)。

  • 对线性代数有基本的了解(例如,基本数学、向量、矩阵、n 维张量、张量运算等)。

  • 对不同深度神经网络的基本了解。

但是,如果您具有以下任何经验,您也将从本书中受益匪浅:

  • 至少有几个月的机器学习研究员、数据科学家或机器学习工程师的经验,甚至是使用机器学习进行大学或学校项目的学生。

  • 有与其他机器学习库(例如,scikit-learn)密切合作的经验,听说过深度学习的惊人成就,并且渴望学习更多有关如何实现它们的知识。

  • 有基本的 TensorFlow 功能使用经验,但希望提高自己的水平以编写更好的 TensorFlow 代码。

本书的组织结构:一份路线图。

《TensorFlow 实战》分为三个部分和 15 章,第一部分从基础知识开始,第二部分进入中等复杂的主题,机器学习从业者应该对此感到舒适,第三部分涵盖了高级机器学习模型、库和工具。

第一部分重点介绍了 TensorFlow 的基本原理,以及如何实现简单的、精简的机器学习模型,如卷积神经网络、循环神经网络和 Transformer:

  • 第一章介绍了 TensorFlow,介绍了 ML 中使用的不同类型的硬件及其权衡,以及何时以及何时使用 TensorFlow。

  • 第二章详细介绍了 TensorFlow 在内部的工作原理,TensorFlow 中找到的不同构建模块,以及如何实现 TensorFlow 中使用的一些常见操作,如卷积。

  • 第三章讨论了 Keras,这是 TensorFlow 中用于轻松构建 ML 模型的子库,以及如何将数据加载到 TensorFlow 中。

  • 第四章首次介绍了建模。在这一章中,我们构建了一个全连接网络、一个卷积神经网络和一个循环神经网络。

  • 第五章将我们带入了深度学习的皇冠明珠:Transformer 模型以及它们的运作原理。

第二部分介绍了几种流行的机器学习任务以及这些任务上性能最佳的模型:

  • 第六章讨论了第一个用例:图像分类。在这一章中,我们使用了一个复杂的 CNN 模型,并对其进行了图像分类数据集的训练。

  • 在第七章,我们深入探讨了更高级的主题,如正则化、更复杂的模型以及模型解释技术。

  • 第八章向我们介绍了图像分割,这是一种重要的技术,能够赋予自动驾驶汽车更强大的能力。我们将训练一个模型,根据像素所属的对象类别来分割图像像素。

  • 第九章是我们深入研究自然语言处理任务的第一步。在这里,我们将训练一个模型,对电影评论中表达的情感进行分类。

  • 在第十章,我们更仔细地研究了语言建模任务,这是当今成功的 Transformer 模型的核心。在这里,我们利用语言建模任务构建了一个能够生成故事的模型。

第三部分深入探讨了更高级的主题,如在 TensorFlow 中使用 Transformer 模型和 TensorBoard 监视和将 ML 工作流投入生产:

  • 第十一章讨论了序列到序列模型,这是 Transformer 模型的前身,在机器翻译等任务中取得了成功。在这里,我们将训练一个序列到序列模型,将英语翻译成德语。

  • 在第十二章,我们继续讨论序列到序列模型,并向读者介绍了一个非常重要的概念:注意力机制。我们将学习如何将注意力机制纳入我们的模型中,这将有助于提高性能并产生富有洞见的可视化效果。

  • 第十三章延续了我们在第五章关于 Transformer 的讨论。在这一章中,我们使用 Transformer 模型解决了两个自然语言处理任务:垃圾邮件分类和问题回答。您还将介绍 Hugging Face 的 Transformers 库。

  • 第十四章重点介绍了 TensorFlow 随附的一个方便工具:TensorBoard。TensorBoard 对于监视和跟踪模型性能至关重要。它还可以用于可视化数据和性能分析。

  • 第十五章,即最后一章,重点介绍了构建生产质量的机器学习流水线。TensorFlow 提供了一个名为 TFX 的库,它提供了一个 API 来将复杂的机器学习工作流标准化为一系列步骤。

您可以根据自己的技能水平选择不同的方法来充分利用本书。例如,如果您是一名在使用 TensorFlow 领域工作了几年(例如 1-3 年)的从业者,您可能会发现第三部分比前面的部分更有用。如果您是初学者,按照时间顺序阅读所有章节是最合理的做法。

有关代码

本书包含许多源代码示例,包括编号列表和与普通文本一起排列的示例。在这两种情况下,源代码都以固定宽度的字体格式化,以将其与普通文本分开。有时,代码也会以粗体显示,以突出显示与章节中先前步骤中已更改的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已经被重新格式化;我们已经添加了换行符并重新排列了缩进以适应书中的页面空间。在极少数情况下,即使这样做也不够,列表中还包括了行延续标记(➥)。此外,在文本中描述代码时,源代码中的注释通常会被从列表中删除。代码注释会伴随许多列表出现,突出显示重要概念。

您可以在本书的在线版本 liveBook(在线版)中获取可执行代码片段,网址为 livebook.manning.com/book/tensorflow-in-action/。本书除第一章外的所有章节都附带了代码。完整的代码可在 Manning 网站 (www.manning.com) 和 GitHub 上找到,网址为 github.com/thushv89/manning_tf2_in_action

liveBook 讨论论坛

购买《TensorFlow 实战》包括免费访问 Manning 的在线阅读平台 liveBook。使用 liveBook 的独家讨论功能,您可以在全书范围或具体章节或段落中附上评论。您可以轻松地为自己做笔记,提出技术问题,并获得作者和其他用户的帮助。要访问论坛,请访问livebook.manning.com/book/tensorflow-in-action/discussion。您还可以了解有关 Manning 论坛以及守则的更多信息,请访问livebook.manning.com/discussion

Manning 致力于为读者提供一个有意义的对话场所,使读者与作者之间、读者之间的交流得以进行。这不是要求作者必须参与的承诺,作者对论坛的贡献是自愿的(也没有报酬)。我们建议您向作者提出一些具有挑战性的问题,以保持他的兴趣!论坛和以前的讨论档案将可通过出版商的网站访问,只要书还在印刷。

关于作者

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Thushan Ganegedara 是一位经验丰富的机器学习实践者,拥有四年以上的行业经验。目前,他是澳大利亚创业公司 Canva 的高级机器学习工程师,Canva 创立了在线视觉设计软件 Canva,服务着数百万客户。他的工作重点集中在搜索和推荐组上。在加入 Canva 之前,Thushan 是澳大利亚保险公司 QBE Insurance 的高级数据科学家,在该公司开发与保险索赔相关的机器学习解决方案。他还带领团队开发了一个 Speech2Text 管道。Thushan 在悉尼大学获得了机器学习专业的博士学位。

关于封面插图

《TensorFlow 实战》封面上的图像题注为“贝尔恩周围的牛奶女仆”,取自 Jacques Grasset de Saint-Sauveur 于 1797 年出版的一套手绘插图集。每幅图都是精美且手工着色。

在那个时代,通过服装即可轻松辨识居住地和职业。Manning 基于历史上丰富多样的地区文化制作书籍封面,庆祝计算机业的创新和进取精神,援用了像这样的插图集将古老的文化重现于人们眼前。

第一部分:TensorFlow 2 和深度学习的基础

很难找到一个没有将机器学习纳入其工作流程的公司。像谷歌、Airbnb 和 Twitter 这样的科技巨头,甚至小型初创公司都在以微妙和显而易见的方式使用机器学习来推动他们的系统和产品。如果你在谷歌上看到广告或在 Airbnb 上看到引人注目的列表,机器学习就是推动这些决策的核心。而 TensorFlow 是开发这些机器学习用例解决方案的一个推动者。换句话说,TensorFlow 是一个深度学习框架,几乎管理了模型生命周期的所有阶段,从开发和部署到监控性能。

在第一部分,您将被介绍 TensorFlow 框架。我们将对这个多才多艺的框架进行一个轻松的介绍。我们将首先讨论一些高层次的主题,比如机器学习是什么,TensorFlow 如何工作,Keras 库以及如何处理 TensorFlow 中的数据。我们将通过简单的场景来解释在讨论期间获得的知识。我们将看一下流行的深度学习模型的基本版本,如全连接网络、卷积神经网络、循环神经网络和 Transformer 模型。

第一章:TensorFlow 的惊人世界

本章涵盖的内容

  • TensorFlow 是什么

  • 机器学习中的硬件:GPU 和 CPU

  • 何时以及何时不使用 TensorFlow

  • 本书教授的内容

  • 本书适合谁

  • 为什么我们应该关注 TensorFlow

到 2025 年,每秒预计将产生超过 5 百万吉字节的数据(www.weforum.org)。我们通过 Google 搜索查询、推特、Facebook 照片和对 Alexa 的语音命令所做出的微小贡献将积累成空前数量的数据。因此,现在正是在人工智能前沿进行斗争、理解并利用不断增长的数字数据宇宙的最佳时机。毫无疑问,数据本身在我们从中提取信息之前并不是非常有用。例如,如果机器知道图像中有什么,图像就会更有用;如果机器能够表达/转录出说过的话,语音命令就会更有用。机器学习是让你从数据世界跨入信息领域(例如,可操作的见解、有用的模式)的门卫,通过允许机器从数据中学习。机器学习,特别是深度学习方法,在充足的数据存在的情况下提供了无与伦比的性能。随着数据的爆炸式增长,越来越多的用例将出现,可以应用深度学习。当然,我们不能忽视更好的技术淹没了流行的深度学习方法的可能性。然而,无可辩驳的现实是,迄今为止,深度学习一直在不断地胜过其他算法,特别是在充足的数据存在时。

什么是机器学习?

机器学习是一个过程,我们在给定数据作为输入的情况下训练和部署一个计算模型来预测一些输出。机器学习问题通常包括以下步骤:

  1. 理解/探索性数据分析——这是您将要探索的提供给您的数据的地方(例如,了解因变量/自变量)。

  2. 清理数据——现实世界的数据通常是混乱的,因此数据清理对确保模型看到高质量数据至关重要。

  3. 特征工程——需要从现有特征或原始数据中构建新特征。

  4. 建模——在这个阶段,您使用选定的特征和相应的目标来训练模型。

  5. 评估——在训练模型之后,您必须确保它是可靠的,并且可以在未见过的数据(例如,测试数据)上表现良好。

  6. 为利益相关者创建用户界面以使用该模型——在大多数情况下,您需要为用户提供一个仪表板/用户界面,让他们与模型进行交互。

尽管看起来像是一套明确定义的步骤,但典型的机器学习问题并不是从 A 到 B 的直线路径,而是由重复的循环或迭代组成的错综复杂的路径。例如,在特征工程阶段,您可能会意识到您尚未探索某些数据方面,这需要更多的数据探索。

深度学习模型很容易超过数百万(最近甚至数十亿)的参数(即权重和偏差),它们对数据有很大的需求。这意味着我们需要框架来有效地训练和推断深度学习模型,同时利用优化的硬件,如图形处理单元(GPU)或张量处理单元(TPU) (mng.bz/4j0g)。实现这一目标的一方面是开发高度可扩展的数据管道,可以高效地读取和处理数据。

1.1 什么是 TensorFlow?

TensorFlow 是一个机器学习框架,在机器学习社区中已经留下了近五年的烙印。它是一个端到端的机器学习框架,旨在在优化的硬件上(例如 GPU 和 TPU)运行得更快。一个机器学习框架提供了实现机器学习解决方案所需的工具和操作。尽管 TensorFlow 不局限于实现深度神经网络,但这一直是它的主要用途。TensorFlow 还支持以下内容:

TensorFlow 是最早进入繁荣的机器学习市场的框架之一。由 Google 开发和维护,TensorFlow 已发布了 100 多个版本,拥有约 2500 名贡献者,使产品日益壮大和改进。它已经发展成为一个从早期原型制作阶段到模型产业化阶段的整体生态系统。在这些阶段之间,TensorFlow 支持一系列功能:

  • 模型开发 —— 通过堆叠预定义的层或创建自定义层来轻松构建深度学习模型

  • 性能监控 —— 在模型训练时监控模型的性能

  • 模型调试 —— 调试模型训练/预测过程中出现的任何问题,如数值错误

  • 模型服务 —— 模型训练完成后,将模型部署到更广泛的公众中,以便在真实世界中使用

正如您所看到的,TensorFlow 几乎支持构建机器学习解决方案的所有阶段,并最终将其提供给实际用户。所有这些服务都被制作成一个单一便捷的包,并通过一条安装说明即可随时使用。

其他深度学习框架

市场上有几个竞争激烈的深度学习框架,它们使您能够轻松实现和生产化深度学习模型:

  • PyTorch (pytorch.org)—PyTorch 是一个框架,主要是使用一个名为 Torch 的机器库实现的,该机器库是基于 Lua 编程语言构建的。PyTorch 和 TensorFlow 具有类似的功能。

  • MXNet (mxnet.apache.org)—MXNet 是由 Apache 软件基金会维护的另一个机器学习框架。

  • DeepLearning4J (deeplearning4j.konduit.ai/)—DeepLearning4J 是一个基于 Java 的深度学习框架。

解决 ML 问题所需的各种组件将在接下来的章节中详细讨论。

接下来,我们将讨论 TensorFlow 的不同组件。这些组件将从原始数据一直到部署模型供客户访问。

1.1.1 TensorFlow 热门组件概览

正如先前提到的,TensorFlow 是一个端到端的机器学习框架。这意味着 TensorFlow 需要支持机器学习项目的许多不同能力和阶段。在确定了业务问题之后,任何机器学习项目都是从数据开始的。一个重要的步骤是进行探索性数据分析。通常情况下,这是通过使用 TensorFlow 和其他数据操作库(例如,pandas、NumPy)的组合来完成的。在这一步中,我们试图理解我们的数据,因为这将决定我们能够多好地使用它来解决问题。通过对数据有扎实的理解(例如,数据类型、数据特定属性、在将数据提供给模型之前需要进行的各种清理/处理),下一步是找到一种有效的方式来使用数据。TensorFlow 提供了一个全面的 API(应用程序编程接口),称为 tf.data API(或 tensorflow.data API)(www.tensorflow.org/guide/data),它使您能够利用野外发现的数据。具体来说,这个 API 提供了各种对象和函数来开发高度灵活的自定义输入数据管道。根据您的需求,您在 TensorFlow 中有几种其他检索数据的选项:

  • tensorflow-datasets—提供访问一系列流行的机器学习数据集的方法,只需一行代码即可下载。

  • Keras 数据生成器—Keras 是 TensorFlow 的一个子模块,并提供了基于 TensorFlow 低级 API 构建的各种高级功能。数据生成器提供了从各种来源(例如磁盘)加载特定类型的数据(例如图像或时间序列数据)的方法。

Keras 简史

Keras 最初由 François Chollet 创建,作为一个与平台无关的高级 API,可以同时使用两种流行的低级符号数学库之一:TensorFlow 或 Theano。具体来说,Keras 提供了层(例如全连接层、卷积层等),这些层封装了神经网络的核心计算。

此外,Keras 提供了可下载并方便使用的预训练模型。由于 Theano 在 2017 年退出,TensorFlow 成为 Keras 的首选后端。在 2017 年(TensorFlow v1.4 及以上版本),Keras 被整合到 TensorFlow 中,现在是 TensorFlow 的一个子模块,提供了各种可重复使用的层,可用于构建深度学习模型以及预训练模型。

使用这些元素中的任何一个(或它们的组合),您可以编写一个数据处理流水线(例如一个 Python 脚本)。数据会根据您试图解决的问题而变化。例如,在图像识别任务中,数据将是图像及其相应的类别(例如,狗/猫)。对于情感分析任务,数据将是电影评论及其相应的情感(例如,积极/消极/中性)。该流水线的目的是从这些数据集中产生一批数据。通常馈送给深度学习模型的数据集可能有数万(甚至更多)个数据点,并且永远不会完全适合有限的计算机内存,因此我们一次馈送一小批数据(例如,几百个数据点),并以批次方式遍历整个数据集。

接下来是模型构建阶段。深度学习模型有许多不同的类型和规模。有四种主要类型的深度网络:全连接、卷积神经、循环神经和 Transformer。正如您将在后续章节中看到的,这些模型具有不同的能力、优势和劣势。TensorFlow 还提供了不同的 API,用于构建模型的控制程度各不相同。首先,在其最原始的形式中,TensorFlow 提供了各种基本操作(例如矩阵乘法)和用于存储模型输入和输出的数据结构(例如 n 维张量)。这些可以用作构建块,从零开始实现任何深度学习模型。

然而,使用低级 TensorFlow API 构建模型可能会相当麻烦,因为您需要反复使用 TensorFlow 中的各种低级操作,并确保模型中正在进行的计算的正确性。这就是 Keras 的用武之地。Keras(现在是 TensorFlow 的一个子模块)相比 TensorFlow API 提供了几个优势:

  • 它提供 封装了神经网络中经常发生的各种常见功能的层对象。我们将在接下来的章节中更详细地了解可用的层。

  • 它提供了几种高级模型构建 API(例如,Sequential、functional 和 subclassing)。例如,Sequential API 适用于构建从输入到输出经过一系列层的简单模型,而 functional API 更适用于处理更复杂的模型。我们将在第三章中更详细地讨论这些 API。

正如您可以想象的那样,这些功能大大降低了使用 TensorFlow 的障碍。例如,如果您需要实现一个标准的神经网络,您只需要堆叠几个标准的 Keras 层,而如果您要使用低级 TensorFlow API 做同样的事情,那将会花费您数百行代码。但是,如果您需要灵活性来实现复杂的模型,您仍然有自由去这样做。

最后,TensorFlow 提供了其最抽象的 API,称为 Estimator API(www.tensorflow.org/guide/estimator)。这个 API 的设计非常健壮,能够抵御任何用户引起的错误。这种健壮性是通过一个非常受限的 API 来保证的,向用户公开了训练、预测和评估模型的最低功能。

当您构建模型时,TensorFlow 将创建所谓的数据流图。这个图是您的模型的表示以及它执行的操作。然后,如果您有优化的硬件(例如,GPU),TensorFlow 将识别出这些设备,并将此图的部分放置在该特殊硬件上,以便您对模型执行的任何操作尽可能快地执行。附录 A 提供了设置 TensorFlow 和其他所需依赖项以运行代码的详细说明。

1.1.2 构建和部署机器学习模型

在构建模型之后,你可以使用准备好的数据通过 tf.data API 对其进行训练。模型的训练过程非常重要,对于深度学习模型来说,它非常耗时,所以你需要一种方式来定期监视模型的进展,并确保在训练过程中性能保持在合理水平。为此,我们会记录 loss 值,这是对训练和验证数据性能的评估指标,因此如果出现问题,你可以尽快介入。TensorFlow 中有更高级的工具,可以让你以更多选项和便利的方式监视模型的性能和健康状况。TensorBoard(www.tensorflow.org/tensorboard)是一个随 TensorFlow 一起提供的可视化工具,可以用于可视化模型的各种指标(例如准确率、精确度等)在训练过程中的变化。你只需要将你想要可视化的指标记录到一个目录中,然后启动 TensorBoard 服务器,并提供该目录作为参数。TensorBoard 将自动在一个仪表盘上可视化记录的指标。这样,如果出现问题,你将很快注意到,并且记录的指标将帮助你定位模型中的问题。

在训练过程中(甚至在训练过程期间),你需要保存模型,否则在退出 Python 程序后模型将被销毁。此外,如果训练过程在训练中断时被中断,你可以恢复模型并继续训练(如果你已经保存了它)。在 TensorFlow 中,可以以几种方式保存模型。你可以简单地将模型保存为 HDF5 格式(即用于大型文件存储的格式)。另一种推荐的方法是将其保存为 SavedModel(www.tensorflow.org/guide/saved_model),这是 TensorFlow 采用的保存模型的标准方式。在接下来的章节中,我们将看到如何保存不同的格式。

你所完成的所有出色工作都已经得到了回报。现在,你想要欢快地向世界展示你构建的非常聪明的机器学习模型。你希望用户使用这个模型并对其感到惊叹,并且希望它能够成为关于人工智能的新闻标题。为了将模型介绍给用户,你需要提供一个 API。为此,TensorFlow 提供了称为 TensorFlow Serving 的功能(www.tensorflow.org/tfx/guide/serving)。TensorFlow Serving 帮助你部署训练好的模型并为用户和客户提供 API。这是一个复杂的主题,涉及许多不同的子主题,我们将在另一章中讨论它。

我们已经从单纯的数据出发,进行了一次漫长的旅程,最终将模型部署和提供给客户使用。接下来,我们将比较在机器学习中使用的几种流行硬件选择。

1.2 GPU vs. CPU

如果你实现过简单的计算机程序(例如商业网站)或者使用过标准数据科学工具如 NumPy、pandas 或者 scikit-learn,你应该听过 GPU 这个术语。为了获得真正的好处,TensorFlow 依赖于特殊的硬件,比如 GPU。事实上,我们在深度神经网络方面取得的进展很大程度上归功于过去几年 GPU 的进步。GPU 有何特殊之处?它们与计算机的大脑、中央处理单元(CPU)有何不同?

让我们通过类比来理解这一点。想想你通勤上班的方式。如果你早早准备好并有些时间可以浪费,你可能会坐公交车。但是如果你只有 10 分钟的时间参加早上 9 点的重要会议,你可能会决定开车。这两种交通方式有什么不同?它们分别有什么不同的用途?汽车的设计是为了快速将少数人(例如四个)送到目的地(即低延迟)。另一方面,公共汽车慢但可以在一次行程中运载更多人(例如 60 人)(即高吞吐量)。此外,汽车配备了各种传感器和设备,使您的驾驶/乘车更加舒适(例如停车传感器、车道检测、座椅加热器等)。但公共汽车的设计更注重为大量乘客提供基本需求(例如座位、停车按钮等),选项有限使您的乘车愉快(见图 1.1)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.1 比较 CPU、GPU 和 TPU。CPU 就像一辆汽车,设计用于快速运输少数人。GPU 就像一辆公共汽车,慢慢地运输许多人。TPU 也像一辆公共汽车,但只在特定场景下运行良好。

CPU 就像一辆汽车,GPU 就像一辆公共汽车。一个典型的 CPU 有少数核心(例如,八个)。CPU 核心快速地执行多种任务(例如 I/O 操作,协调不同设备之间的通信等),但规模较小。为了支持各种操作,CPU 需要支持大量指令。为了使这些指令运行得快,CPU 依赖昂贵的基础设施(例如更多的晶体管、不同级别的缓存等)。总之,CPU 在小规模上快速执行大量指令。相反,一个典型的 GPU 有许多核心(例如,一千多个)。但是 GPU 核心支持有限的指令集,不太注重快速执行它们。

在机器学习的背景下,特别是在深度学习中,我们大多需要重复执行大量的矩阵乘法来训练和推断模型。矩阵乘法是 GPU 高度优化的功能,这使得 GPU 成为理想选择。

我们不应忘记我们的朋友 TPU,它们是优化硬件清单的最新知名添加。TPU 由 Google 发明,可以被视为简化的 GPU。它们是专门针对机器学习和人工智能应用的应用特定集成电路(ASIC)。它们被设计用于低精度高容量运算。例如,GPU 通常使用 32 位精度,而 TPU 使用一种称为 bfloat16 的特殊数据类型(使用 16 位)(mng.bz/QWAe)。此外,TPU 缺乏图形处理功能,如光栅化/纹理映射。TPU 的另一个区别特征是它们比 GPU 要小得多,意味着可以在更小的物理空间内容纳更多的 TPU。

将我们的汽车-公交车类比扩展到 TPU,你可以将 TPU 视为经济型公交车,设计用于在偏远地区短距离旅行。它不能像普通公交车那样舒适地长途旅行或适应各种道路/天气条件,但它可以将你从 A 点运送到 B 点,因此可以完成任务。

1.3 TensorFlow 的使用时机

了解或学习 TensorFlow 的关键组成部分是知道何时以及何时不应该使用 TensorFlow。让我们通过深度学习的视角来看一下这一点。

1.3.1 TensorFlow 的使用时机

TensorFlow 绝不是任何机器学习问题的万能解决方案。只有了解 TensorFlow 的适用范围,才能获得最佳效果。

深度学习模型的原型设计

TensorFlow 是原型设计模型的绝佳工具(例如,全连接网络、卷积神经网络、长短期记忆网络),因为它提供了层对象(在 Keras 中),例如以下内容:

  • 全连接网络的密集层

  • 卷积神经网络的卷积层

  • 用于顺序模型的 RNN(循环神经网络)/ LSTM(长短期记忆)/ GRU(门控循环单元)层

(你不需要了解这些层的底层机制,因为它们将在后面的章节中深入讨论。)TensorFlow 甚至提供了一套预训练模型,因此你可以用更少的代码开发一个简单的模型,包括几个层,或者一个由许多模型组成的复杂集成模型。

实现可以在优化硬件上更快运行的模型

TensorFlow 包含核心(各种低级操作的实现;例如,矩阵乘法)进行了优化,以便在 GPU 和 TPU 上更快地运行。因此,如果你的模型可以利用这些优化的操作(例如,线性回归),并且需要重复运行大量数据的模型,TensorFlow 将有助于更快地运行模型。

控制 TensorFlow 代码在硬件上的运行

尽管利用 GPU/TPU 运行 TensorFlow 代码非常重要,但同样重要的是我们可以在运行代码时控制资源利用(例如,内存)。以下是运行 TensorFlow 代码时可以控制的主要方面:

  • 特定 TensorFlow 操作的运行位置 —— 通常情况下你不需要这样做,但是你可以指定某个操作应在 CPU/GPU/TPU 上运行,或者指定使用哪个 GPU/TPU,特别是当你拥有多个 GPU/TPU 时。

  • GPU 中的内存使用量 —— 你可以告诉 TensorFlow 只分配总 GPU 内存的一定百分比。这对确保 GPU 内存中有一个用于任何涉及图形处理的进程(例如操作系统使用)非常方便。

在云上运行模型/服务化

机器学习模型的最常见目标是为解决现实世界的问题服务;因此,模型需要通过仪表板或 API 向感兴趣的利益相关者提供预测。TensorFlow 的一个独特优势是,当模型达到这个阶段时,你不需要离开它。换句话说,你可以通过 TensorFlow 开发你的模型服务 API。此外,如果你有豪华的硬件(例如 GPU/TPU),TensorFlow 在进行预测时会利用它。

监控模型的训练过程中的模型性能。

在模型训练期间,关注模型性能以防止过度拟合或欠拟合非常关键。即使有 GPU 的帮助,训练深度学习模型仍然可能很繁琐,因为它们的计算需求很高。这使得监控这些模型比运行几分钟的简单模型更加困难。如果要监视运行几分钟的模型,可以将指标打印到控制台并记录到文件中以供参考。

但是,由于深度学习模型经历了大量的训练迭代,当这些指标以图形方式可视化时更容易吸收信息。TensorBoard 正是提供这种功能。你只需要在 TensorFlow 中记录和保持你的性能指标,并将 TensorBoard 指向该记录目录。TensorBoard 将通过自动将此信息转换为图形来处理此操作目录中的信息,我们可以用来分析模型的质量。

创建重型数据管道

我们已经多次指出,深度学习模型对数据有很大的需求量。通常,深度学习模型所依赖的数据集不适合内存。这意味着我们需要以更小、更易处理的数据批次,以低延迟的方式提供大量的数据。正如我们已经看到的,TensorFlow 提供了丰富的 API 来向深度学习模型流式传输数据。我们所需要做的就是理解所提供函数的语法并适当地使用它们。此类数据管道的一些示例情景包括以下内容:

  • 一个消费大量图像并对其进行预处理的管道

  • 一个消费大量以标准格式(例如 CSV [逗号分隔值])呈现的结构化数据并执行标准预处理(例如归一化)的管道。

  • 一个处理大量文本数据并执行简单预处理(例如,文本小写化,去除标点符号)的流水线

1.3.2 不适用 TensorFlow 的情况

掌握工具或框架时了解不应该做什么同样重要。在这一部分,我们将讨论其他工具可能比 TensorFlow 更高效的一些领域。

实现传统的机器学习模型

机器学习拥有大量的模型(例如,线性/逻辑回归、支持向量机、决策树、K 均值),这些模型属于不同类别(例如,监督与非监督学习),并且具有不同的动机、方法、优势和劣势。有许多模型被使用,您不会看到太多性能提升使用优化的硬件(例如,决策树、K 均值等),因为这些模型不具有固有的可并行性。有时您需要运行这些算法作为您开发的新算法的基准,或者以了解机器学习问题的难易程度。

使用 TensorFlow 实现这些方法将会花费比应该更多的时间。在这种情况下,scikit-learn (scikit-learn.org/stable/) 是一个更好的选择,因为该库提供了大量已实现的模型。TensorFlow 确实支持一些算法,如基于提升树的模型 (mng.bz/KxPn)。但根据我的经验,使用 XGBoost (极端梯度提升) (xgboost.readthedocs.io/en/latest/) 实现提升树更加方便,因为它受到其他库的更广泛支持。此外,如果您需要 GPU 优化版本的 scikit-learn 算法,NVIDIA 也提供了一些适用于 GPU 的算法 (rapids.ai/)。

操纵和分析小规模结构化数据

有时我们将使用相对较小结构的数据集(例如,10,000 个样本),这些数据集可以轻松放入内存。如果数据可以完全加载到内存中,pandas 和 NumPy 是探索和分析数据的更好选择。这些是配备有高度优化的 C/C++ 实现的各种数据操作(例如,索引、过滤、分组)和统计相关操作(例如,平均值、总和)的库。对于小数据集,TensorFlow 可能会造成显著的开销(在 CPU 和 GPU 之间传输数据,在 GPU 上启动计算内核),特别是如果运行大量较小、成本较低的操作。此外,pandas/NumPy 在如何操作数据方面更具表现力,因为这是它们的主要关注点。

创建复杂的自然语言处理流水线

如果您正在开发自然语言处理(NLP)模型,则很少会将数据传递给模型而不对数据进行至少简单的预处理(例如,文本小写化、去除标点符号)。但指导您的预处理流水线的实际步骤将取决于您的用例和您的模型。例如,有时会有一些简单步骤(例如,小写化、去除标点符号),或者您可能有一个完整的预处理流水线,需要进行复杂的任务(例如,词干提取、词形还原、拼写纠正)。在前一种情况下,TensorFlow 是一个不错的选择,因为它提供了一些简单的文本预处理功能(例如,小写化、替换文本、字符串分割等)。然而,在后一种情况下,如果诸如词形还原、词干提取、拼写纠正等昂贵步骤主导着预处理流水线,TensorFlow 将阻碍您的进展。对此,spaCy (spacy.io/) 是一个更强大的选择,因为它提供了直观的界面和可用的模型,用于执行标准的 NLP 处理任务。

spaCy 在定义流水线时支持包含 TensorFlow 模型(通过一个特殊包装器)。但作为一个经验法则,在可能的情况下尽量避免这样做。不同库之间的集成通常耗时,并且在复杂设置中甚至可能出错。

表 1.1 总结了 TensorFlow 的各种优点和缺点。

表 1.1 TensorFlow 优缺点总结

任务
原型化深度学习模型X
实现在优化硬件上运行更快的模型(包括非深度学习)X
在云端将模型投入生产/服务X
在模型训练期间监控模型X
创建重型数据流水线X
实现传统机器学习模型X
操纵和分析小规模结构化数据X
创建复杂的自然语言处理流水线X

1.4 本书将教授您什么?

在接下来的章节中,本书将教授您一些至关重要的技能,这些技能将帮助您主要且有效地解决研究问题。

1.4.1 TensorFlow 基础知识

首先,我们将学习 TensorFlow 的基础知识。我们将学习它提供的不同执行方式,用于实现任何 TensorFlow 解决方案的主要构建模块(例如,tf.Variable、tf.Operation),以及各种低级操作的功能。然后我们将探索由 Keras(TensorFlow 的一个子模块)向用户公开的各种模型构建 API,以及它们的优点和局限性,这将有助于做出何时使用特定模型构建 API 的决定。我们还将研究我们可以为 TensorFlow 模型获取数据的各种方法。与传统方法不同,深度学习模型消耗大量数据,因此拥有高效且可扩展的数据摄入管道(即输入管道)至关重要。

1.4.2 深度学习算法

实现高效的深度学习模型是 TensorFlow 的主要目的之一。因此,我们将讨论各种深度学习算法的架构细节,如全连接神经网络、卷积神经网络(CNN)和循环神经网络(RNN)。请注意,研究这些模型的理论不是本书的目标。我们将只讨论这些模型,以便帮助我们理解如何在 TensorFlow/Keras 中舒适地实现它们。

通过实施和应用这些模型到流行的计算机视觉和 NLP 应用程序,如图像分类、图像分割、情感分析和机器翻译,我们将进一步磨练我们对这些模型的理解。看到这些模型在这些任务上的表现如何,没有人工设计的特征将会很有趣。

接着,我们将讨论一类新的模型,称为 Transformers。Transformers 与卷积神经网络和循环神经网络非常不同。与 CNN 和 RNN 不同,它们每次可以看到完整的时间序列数据,从而导致更好的性能。事实上,Transformers 在许多 NLP 任务上已经超过了以前记录的最先进模型。我们将学习如何在 TensorFlow 中引入这些模型,以提高各种下游任务的性能。

1.4.3 监控和优化

知道如何在 TensorFlow 中实现模型是不够的。仔细检查和监视模型性能是创建可靠机器学习模型的重要步骤。使用可视化工具,如 TensorBoard 来可视化性能指标和特征表示是必备的技能。模型可解释性也已经成为一个重要的话题,因为像神经网络这样的黑盒模型正在成为机器学习中的常见商品。TensorBoard 有一些工具来解释模型或解释为什么模型做出了某个决定。

接下来,我们将探讨如何使模型训练速度更快。训练时间是使用深度学习模型中最突出的瓶颈之一,因此我们将讨论一些使模型训练更快的技术!

1.5 这本书是为谁写的?

本书是为机器学习社区中更广泛的读者群写的,旨在为初学者提供一个相对容易的入门,以及具有基本到中等知识/经验的机器学习从业者,以进一步推动他们的 TensorFlow 技能。为了充分利用本书,您需要以下内容:

  • 通过研究/行业项目在模型开发生命周期中的经验

  • 对 Python 和面向对象编程(OOP)的中等知识(例如,类/生成器/列表推导式)

  • NumPy/pandas 库的基本知识(例如,计算摘要统计信息,pandas series DataFrame 对象是什么)

  • 对线性代数有基本的了解(例如,基本数学,向量,矩阵,n 维张量,张量操作等)

  • 对不同的深度神经网络有基本的熟悉

如果你是以下的人,那么你将会从这本书中受益匪浅

  • 至少有几个月的机器学习研究员,数据科学家,机器学习工程师,甚至是在大学/学校项目中作为学生拥有使用机器学习的经验

  • 与其他机器学习库密切合作(例如,scikit-learn),并听说过深度学习的惊人成绩,并渴望学习如何实现它们

  • 对基本的 TensorFlow 功能有所了解,但希望写出更好的 TensorFlow 代码

你可能在想,在有着大量资源可用的情况下(例如 TensorFlow 文档,StackOverFlow.com 等),学习 TensorFlow 不是很容易(且免费)吗?是和不。如果你只是需要针对问题工作的“一些”解决方案,你可能能够使用现有的资源进行 hack。但很可能这将是一个次优解,因为为了提出一个有效的解决方案,你需要建立对 TensorFlow 执行代码的强大心理形象,理解 API 中提供的功能,理解限制等。同时,逐渐有序地了解 TensorFlow 并理解它也非常困难,而仅仅是随机阅读免费资料是无法做到的。坚实的心理形象和牢固的知识来自于多年的经验(并密切关注新功能,GitHub 问题和 stackoverflow.com 问题),或者来自于一位具有多年经验的作者编写的书籍。这里的重要问题不是“我该如何使用 TensorFlow 解决我的问题?”,而是“我该如何有效地使用 TensorFlow 解决我的问题?”提出一个有效的解决方案需要对 TensorFlow 有扎实的理解。在我看来,一个有效的解决方案可以做到(但不限于)以下几点:

  • 保持相对简洁的代码,同时又不牺牲可读性太多(例如,避免冗余操作,在可能的情况下聚合操作)

  • 使用 API 中最新最棒的特性,避免重复发明轮子,节省时间

  • 尽可能利用优化(例如,避免循环,使用矢量化操作)

如果你让我用几个词来概括这本书,我会说“让读者能够编写有效的 TensorFlow 解决方案”。

1.6 我们真的应该关心 Python 和 TensorFlow 2 吗?

这里我们将了解到你将会大量学习的两项最重要的技术:Python 和 TensorFlow。Python 是我们将使用来实现各种 TensorFlow 解决方案的基础编程语言。但重要的是要知道,TensorFlow 支持许多不同的语言,比如 C++,Go,JavaScript 等等。

我们应该试图回答的第一个问题是:“为什么我们选择 Python 作为我们的编程语言?” Python 的流行度近年来有所增加,特别是在科学界,这是因为大量的库加强了 Python(例如 pandas、NumPy、scikit-learn),这使得进行科学实验/模拟以及记录/可视化/报告结果变得更加容易。在图 1.2 中,您可以看到 Python 如何成为最受欢迎的搜索词(至少在 Google 搜索引擎中是如此)。如果将结果仅限于机器学习社区,您将看到更高的差距。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.2 不同编程语言的流行程度(2015-2020)

下一个要回答的问题是:“我们为什么选择 TensorFlow?” TensorFlow 几乎从深度学习开始流行就一直存在(mng.bz/95P8)。 TensorFlow 在大约五年的时间里不断改进和修订,随着时间的推移变得越来越稳定。此外,与其他类似的库不同,TensorFlow 提供了一个生态系统的工具,以满足您的机器学习需求,从原型设计到模型训练再到模型。在图 1.3 中,您可以看到 TensorFlow 与其一个流行竞争对手 PyTorch 的比较。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.3 TensorFlow 和 PyTorch 的流行程度(2015-2020)

我们也值得检查随着数据量的增长,我们所获得的性能增长有多大。图 1.4 比较了一个流行的科学计算库(NumPy)与 TensorFlow 在矩阵乘法任务中的表现。这是在 Intel i5 第九代处理器和 NVIDIA 2070 RTX 8 GB GPU 上测试的。在这里,我们正在乘以两个随机初始化的矩阵(每个矩阵大小为 n × n)。我们记录了 n = 100、1000、5000、7500、1000 时所花费的时间。在图的左侧,您可以看到时间增长的差异。NumPy 显示随着矩阵大小的增长,所花费时间呈指数增长。但是,TensorFlow 显示出大致线性的增长。在图的右侧,您可以看到如果 TensorFlow 操作需要一秒钟需要多少秒。这一信息很清楚:随着数据量的增长,TensorFlow 比 NumPy 做得更好。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.4 在矩阵乘法任务中比较 NumPy 和 TensorFlow 计算库

摘要

  • 由于提供了大量数据时提供的卓越性能,深度学习已成为一个热门话题。

  • TensorFlow 是一个端到端的机器学习框架,提供生态系统支持的模型原型设计、模型构建、模型监控、模型服务等。

  • TensorFlow 和任何其他工具一样,都有优势和劣势。因此,用户需要权衡这些因素,以解决他们试图解决的问题。

  • TensorFlow 是一个非常好的工具,可以快速原型设计各种复杂度的深度学习模型。

  • TensorFlow 并不适合分析/操作小型数据集或开发复杂的文本处理数据管道。

  • 本书不仅教读者如何实现某些 TensorFlow 解决方案,更教授读者如何在最小化工作量的情况下实现有效的解决方案,同时减少错误的可能性。

第二章:TensorFlow 2

本章介绍了

  • TensorFlow 2 是什么

  • TensorFlow 中的重要数据结构和操作

  • TensorFlow 中常见的与神经网络相关的操作

在上一章中,我们了解到 TensorFlow 是一种端到端的机器学习框架,主要用于实现深度神经网络。TensorFlow 擅长将这些深度神经网络转换为在优化硬件上(例如 GPU 和 TPU)运行更快的计算图。但请记住,这不是 TensorFlow 的唯一用途。表 2.1 概述了 TensorFlow 支持的其他领域。

表 2.1 TensorFlow 提供的各种功能

概率机器学习TensorFlow 支持实现概率机器学习模型。例如,可以使用 TensorFlow API 实现贝叶斯神经网络等模型(www.tensorflow.org/probability)。
与计算机图形有关的计算计算机图形计算大部分可以使用 GPU 实现(例如模拟各种光照效果、光线追踪;www.tensorflow.org/graphics)。
TensorFlow Hub:可重用(预训练的)模型在深度学习中,我们通常试图利用已经在大量数据上训练过的模型来解决我们感兴趣的下游任务。TensorFlow Hub 是一个存放这种用 TensorFlow 实现的模型的仓库(www.tensorflow.org/hub)。
可视化/调试 TensorFlow 模型TensorFlow 提供了一个仪表板,用于可视化和监控模型性能,甚至可视化数据(www.tensorflow.org/tensorboard)。

在接下来的章节中,我们将展开一次充满惊喜的旅程,探索 TensorFlow 中的花里胡哨的东西,并学习如何在 TensorFlow 擅长的领域中表现出色。换句话说,我们将学习如何使用 TensorFlow 解决现实世界的问题,例如图像分类(即在图像中识别对象)、情感分析(即识别评论/意见中的正面/负面情绪)等等。在解决这些任务的同时,您将学习如何克服过拟合和类别不平衡等现实世界中可能会出现的挑战,这些问题很容易妨碍我们的进展。本章将特别关注在我们进入可以使用深度网络解决的复杂问题之前,为 TensorFlow 提供扎实的基础知识。

首先,我们将在 TensorFlow 2 和 TensorFlow 1 中实现一个神经网络,看看 TensorFlow 在用户友好性方面发展了多少。然后,我们将了解 TensorFlow 提供的基本单元(例如变量、张量和操作),我们必须对此有很好的理解才能开发解决方案。最后,我们将通过一系列有趣的计算机视觉练习来理解几个复杂的数学操作的细节。

2.1 TensorFlow 2 初步

假设你正在参加一门机器学习课程,并被要求使用 TensorFlow 实现一个多层感知机(MLP)(即一种神经网络类型),并针对给定的数据点计算最终输出。你对 TensorFlow 还不熟悉,所以你去图书馆开始研究 TensorFlow 是什么。在研究过程中,你意识到 TensorFlow 有两个主要版本(1 和 2),决定使用最新最好的:TensorFlow 2. 你已经按附录 A 中的要求安装了所需的库。

在继续之前,让我们了解一下 MLP。MLP(图 2.1)是一个简单的神经网络,它有一个输入层,一个或多个隐藏层和一个输出层。这些网络也被称为全连接网络

注 Some research only uses the term MLP to refer to a network made of multiple perceptrons (mng.bz/y4lE) organized in a hierarchical structure. However, in this book, we will use the terms MLP and fully connected network interchangeably.

在每一层中,我们有权重和偏置,用于计算该层的输出。在我们的例子中,我们有一个大小为 4 的输入,一个具有三个节点的隐藏层和一个大小为 2 的输出层。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.1 多层感知机(MLP)或全连接网络的示意图。有三层:一个输入层,一个隐藏层(带有权重和偏置),一个输出层。输出层使用 softmax 激活产生归一化的概率作为输出。

输入值(x)经过以下计算转换为隐藏值(h):

h = σ(x W[1] + b[1])

其中σ是 sigmoid 函数。Sigmoid 函数是一个简单的非线性逐元素变换,如图 2.2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.2 sigmoid 激活函数对不同输入的可视化

x是一个大小为 1 × 4 的矩阵(即一行四列),W[1]是一个大小为 4 × 3 的矩阵(即四行三列),b[1]是 1 × 4 的矩阵(即一行四列)。这给出了一个大小为 1 × 3 的h。最后,输出计算为

y = softmax(h W[2] + b[2])

这里,W[2]是一个 3 × 2 的矩阵,b[2]是一个 1 × 2 的矩阵。Softmax 激活将最后一层的线性分数(即h W[2] + b[2])归一化为实际概率(即沿列求和的值等于 1)。假设输入向量x的长度为K,softmax 激活产生一个K长的向量yy的第i个元素计算如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中y[i]是第i个输出元素,x[i]是第i个输入元素。作为一个具体的例子,假设最终层没有 softmax 激活产生,

[16, 4]

应用 softmax 归一化将这些值转换为

[16/(16+4), 4/(16+4)] = [0.8, 0.2]

让我们看看如何在 TensorFlow 2 中实现这一点。您可以在 Jupyter 笔记本(Ch02-Fundamentals-of-TensorFlow-2/2.1.Tensorflow_Fundamentals.ipynb)中找到代码。如何安装必要的库和设置开发环境在附录 A 中描述。最初,我们需要使用导入语句导入所需的库:

import numpy as np
import tensorflow as tf

然后,我们定义网络的输入(x)和变量(或参数)(即w[1]、b[1]、w[2]和b[2]):

x = np.random.normal(size=[1,4]).astype('float32')

init = tf.keras.initializers.RandomNormal()

w1 = tf.Variable(init(shape=[4,3])) 
b1 = tf.Variable(init(shape=[1,3])) 

w2 = tf.Variable(init(shape=[3,2])) 
b2 = tf.Variable(init(shape=[1,2])) 

在这里,x是一个大小为 1×4(即一行四列)的简单 NumPy 数组,其值来自正常分布。然后,我们将网络的参数(即权重和偏差)定义为 TensorFlow 变量。tf.Variable 的行为类似于典型的 Python 变量。在定义时会附加一些值,并且随着时间的推移可能会发生变化。tf.Variable 用于表示神经网络的权重和偏差,在优化或训练过程中会更改这些参数。定义 TensorFlow 变量时,需要为变量提供一个初始化器和一个形状。在这里,我们使用从正态分布随机抽样值的初始化器。请记住,W[1]大小为 4×3,b[1]大小为 1×3,W[2]大小为 3×2,b[2]大小为 1×2,每个参数的形状参数都相应地进行了设置。接下来,我们将多层感知器网络的核心计算定义为一个漂亮的模块化函数。这样,我们可以轻松地重用该函数来计算多层的隐藏层输出:

@tf.function
def forward(x, W, b, act):
    return act(tf.matmul(x,W)+b)

在这里,act 是您选择的任何非线性激活函数(例如 tf.nn.sigmoid)。(您可以在此处查看各种激活函数:www.tensorflow.org/api_docs/python/tf/nn。要注意的是,并非所有函数都是激活函数。表达式 tf.matmul(x,W)+b 优雅地封装了我们之前看到的核心计算(即x W[1]+ b[1]和 h W[2]+b[2])到可重用的表达式中。在这里,tf.matmul 执行矩阵乘法运算。该计算在图 2.3 中说明。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.3 矩阵乘法和偏置加法的示例输入、权重和偏差说明

将@tf.function 放在函数的顶部是 TensorFlow 知道该函数包含 TensorFlow 代码的一种方式。我们将在下一部分更详细地讨论@tf.function 的目的。这将带我们进入代码的最后部分。由于我们已经定义了输入、所有参数和核心计算,因此可以计算网络的最终输出。

# Computing h
h = forward(x, w1, b1, tf.nn.sigmoid)

# Computing y
y = forward(h, w2, b2, tf.nn.softmax)

print(y)

输出将会是:

tf.Tensor([[0.4912673 0.5087327]], shape=(1, 2), dtype=float32)

这里,h 和 y 是各种 TensorFlow 操作(例如 tf.matmul)的结果张量(类型为 tf.Tensor)。输出中的确切值可能会略有不同(请见下面的列表)。

列表 2.1 使用 TensorFlow 2 的多层感知器网络

import numpy as np                                  ❶
import tensorflow as tf                             ❶

x = np.random.normal(size=[1,4]).astype('float32')  ❷

init = tf.keras.initializers.RandomNormal()         ❸

w1 = tf.Variable(init(shape=[4,3]))                 ❹
b1 = tf.Variable(init(shape=[1,3]))                 ❹

w2 = tf.Variable(init(shape=[3,2]))                 ❹
b2 = tf.Variable(init(shape=[1,2]))@tf.functiondef forward(x, W, b, act):return act(tf.matmul(x,W)+b)                    ❻

h = forward(x, w1, b1, tf.nn.sigmoid)               ❼

y = forward(h, w2, b2, tf.nn.softmax)print(y)

❶导入 NumPy 和 TensorFlow 库

❷ MLP 的输入(一个 NumPy 数组)

❸ 用于初始化变量的初始化器

❹ 第一层(w1 和 b2)和第二层(w2 和 b2)的参数

❺ 这行告诉 TensorFlow 的 AutoGraph 构建图形。

❻ MLP 层计算,它接受输入、权重、偏置和非线性激活

❼ 计算第一个隐藏层的输出 h

❽ 计算最终输出 y

接下来,我们将看看 TensorFlow 运行代码时背后发生了什么。

2.1.1 TensorFlow 在底层是如何运行的?

在典型的 TensorFlow 程序中,有两个主要步骤:

  1. 定义一个涵盖输入、操作和输出的数据流图。在我们的练习中,数据流图将表示 x、w1、b1、w2、b2、h 和 y 之间的关系。

  2. 通过为输入提供值并计算输出来执行图形。例如,如果我们需要计算 h,则将一个值(例如 NumPy 数组)馈送到 x 并获取 h 的值。

TensorFlow 2 使用一种称为命令式执行的执行样式。在命令式执行中,声明(定义图形)和执行同时发生。这也被称为急切执行代码。

您可能想知道数据流图是什么样的。这是 TensorFlow 用来描述您定义的计算流程的术语,并表示为有向无环图(DAG):箭头表示数据,节点表示操作。换句话说,tf.Variable 和 tf.Tensor 对象表示图中的边,而操作(例如 tf.matmul)表示节点。例如,对于

h = x W[1] + b[1]

将看起来像图 2.4。然后,在运行时,您可以通过向 x 提供值来获取 y 的值,因为 y 依赖于输入 x。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.4 一个示例计算图。这里的各个元素将在 2.2 节中更详细地讨论。

TensorFlow 如何知道创建数据流图?您可能已经注意到以@符号开头的行悬挂在 forward(…) 函数的顶部。这在 Python 语言中称为装饰器。@tf.function 装饰器接受执行各种 TensorFlow 操作的函数,跟踪所有步骤,并将其转换为数据流图。这是多么酷?这鼓励用户编写模块化代码,同时实现数据流图的计算优势。TensorFlow 2 中这个功能被称为 AutoGraph(www.tensorflow.org/guide/function)。

什么是装饰器?

装饰器通过包装函数来修改函数的行为,这发生在函数被调用之前/之后。一个很好的装饰器示例是在每次调用函数时记录输入和输出。下面是如何使用装饰器的示例:

def log_io(func):
    def wrapper(*args, **kwargs):
        print("args: ", args)
        print(“kwargs:, kwargs)
        out = func(*args, **kwargs)
        print("return: ", out)
    return wrapper

@log_io
def easy_math(x, y):
    return x + y + ( x * y)

res = easy_math(2,3)

这将输出

args:  (2, 3)
kwargs:  {}
return:  11

预期的。因此,当您添加 @tf.function 装饰器时,它实际上修改了调用函数的行为,通过构建给定函数内发生的计算的计算图。

图 2.5 中的图解描述了 TensorFlow 2 程序的执行路径。第一次调用函数 a(…) 和 b(…) 时,将创建数据流图。然后,将输入传递给函数,以将输入传递给图并获取您感兴趣的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.5 TensorFlow 2 程序的典型执行。在第一次运行时,TensorFlow 会跟踪所有使用 @tf.function 注释的函数,并构建数据流图。在后续运行中,根据函数调用传递相应的值给图,并检索结果。

AutoGraph

AutoGraph 是 TensorFlow 中的一个很棒的功能,通过在幕后努力工作,减轻了开发者的工作量。要真正欣赏这个功能,请阅读更多内容请访问www.tensorflow.org/guide/function。虽然它相当令人惊叹,但 AutoGraph 不是万能药。因此,了解其优点以及限制和注意事项非常重要:

  • 如果您的代码包含大量重复操作(例如,多次迭代训练神经网络),AutoGraph 将提供性能提升。

  • 如果您运行多个仅运行一次的不同操作,则 AutoGraph 可能会减慢您的速度;因为您仅运行一次操作,构建图仅是一种开销。

  • 要注意将什么包含在您向 AutoGraph 公开的函数内。例如

    • NumPy 数组和 Python 列表将被转换为 tf.constant 对象。

    • 在函数跟踪期间将展开 for 循环,这可能导致大型图最终耗尽内存。

TensorFlow 1,TensorFlow 2 的前身,使用了一种称为声明式基于图的执行的执行风格,它包含两个步骤:

  1. 明确定义一个数据流图,使用各种符号元素(例如占位符输入、变量和操作),以实现你所需的功能。与 TensorFlow 2 不同,这些在声明时不会保存值。

  2. 明确编写代码来运行定义的图,并获取或评估结果。您可以在运行时向先前定义的符号元素提供实际值,并执行图。

这与 TensorFlow 2 非常不同,后者隐藏了数据流图的所有复杂性,通过自动在后台构建它。在 TensorFlow 1 中,您必须显式构建图,然后执行它,导致代码更加复杂且难以阅读。表 2.2 总结了 TensorFlow 1 和 TensorFlow 2 之间的区别。

表 2.2 TensorFlow 1 和 TensorFlow 2 之间的区别

TensorFlow 1TensorFlow 2
默认情况下不使用急切执行默认情况下使用急切执行
使用符号占位符表示图形的输入直接将实际数据(例如,NumPy 数组)提供给数据流图
由于结果不是按命令式评估,因此难以调试由于操作是按命令式评估的,因此易于调试
需要显式手动创建数据流图具有 AutoGraph 功能,可以跟踪 TensorFlow 操作并自动创建图形
不鼓励面向对象编程,因为它强制您提前定义计算图鼓励面向对象编程
由于具有单独的图形定义和运行时代码,代码的可读性较差具有更好的代码可读性

在下一节中,我们将讨论 TensorFlow 的基本构建模块,为编写 TensorFlow 程序奠定基础。

练习 1

给定以下代码,

# A
import tensorflow as tf
# B
def f1(x, y, z):
    return tf.math.add(tf.matmul(x, y) , z)
#C
w = f1(x, y, z)

tf.function 装饰器应该放在哪里?

  1. A

  2. B

  3. C

  4. 以上任何一项

2.2 TensorFlow 构建模块

我们已经看到了 TensorFlow 1 和 TensorFlow 2 之间的核心差异。在此过程中,您接触到了 TensorFlow API 公开的各种数据结构(例如,tf.Variable)和操作(例如,tf.matmul)。现在让我们看看在哪里以及如何使用这些数据结构和操作。

在 TensorFlow 2 中,我们需要了解三个主要的基本元素:

  • tf.Variable

  • tf.Tensor

  • tf.Operation

你已经看到所有这些被使用了。例如,从前面的 MLP 示例中,我们有这些元素,如表 2.3 所示。了解这些基本组件有助于理解更抽象的组件,例如 Keras 层和模型对象,稍后将进行讨论。

表 2.3 MLP 示例中的 tf.Variable、tf.Tensor 和 tf.Operation 实体

元素示例
tf.Variablew1*,* b1*,* w2 和 b2
tf.Tensorh 和 y
tf.Operationtf.matmul

牢牢掌握 TensorFlow 的这些基本元素非常重要,原因有几个。主要原因是,从现在开始,您在本书中看到的所有内容都是基于这些元素构建的。例如,如果您使用像 Keras 这样的高级 API 构建模型,它仍然使用 tf.Variable、tf.Tensor 和 tf.Operation 实体来进行计算。因此,了解如何使用这些元素以及您可以实现什么和不能实现什么非常重要。另一个好处是,TensorFlow 返回的错误通常使用这些元素呈现给您。因此,这些知识还将帮助我们理解错误并在开发更复杂的模型时迅速解决它们。

2.2.1 理解 tf.Variable

构建典型的机器学习模型时,您有两种类型的数据:

  • 模型参数随时间变化(可变),因为模型针对所选损失函数进行了优化。

  • 模型输出是给定数据和模型参数的静态值(不可变)

tf.Variable 是定义模型参数的理想选择,因为它们被初始化为某个值,并且可以随着时间改变其值。一个 TensorFlow 变量必须具有以下内容:

  • 形状(变量的每个维度的大小)

  • 初始值(例如,从正态分布中抽样的随机初始化)

  • 数据类型(例如 int32、float32)

你可以如下定义一个 TensorFlow 变量

tf.Variable(initial_value=None, trainable=None, dtype=None)

其中

  • 初始值包含提供给模型的初始值。通常使用 tf.keras.initializers 子模块中提供的变量初始化器提供(完整的初始化器列表可以在 mng.bz/M2Nm 找到)。例如,如果你想使用均匀分布随机初始化一个包含四行三列的二维矩阵的变量,你可以传递 tf.keras.initializers.RandomUniform()([4,3])。你必须为 initial_value 参数提供一个值。

  • trainable 参数接受布尔值(即 True 或 False)作为输入。将 trainable 参数设置为 True 允许通过梯度下降更改模型参数。将 trainable 参数设置为 False 将冻结层,以使值不能使用梯度下降进行更改。

  • dtype 指定变量中包含的数据的数据类型。如果未指定,这将默认为提供给 initial_value 参数的数据类型(通常为 float32)。

让我们看看如何定义 TensorFlow 变量。首先,请确保已导入以下库:

import tensorflow as tf
import numpy as np

你可以如下定义一个大小为 4 的一维 TensorFlow 变量,其常量值为 2:

v1 = tf.Variable(tf.constant(2.0, shape=[4]), dtype='float32')
print(v1)

>>> <tf.Variable 'Variable:0' shape=(4,) dtype=float32, numpy=array([2., 2., 2., 2.], dtype=float32)>

在这里,tf.constant(2.0, shape=[4]) 生成一个有四个元素且值为 2.0 的向量,然后将其用作 tf.Variable 的初始值。你也可以使用 NumPy 数组定义一个 TensorFlow 变量:

v2 = tf.Variable(np.ones(shape=[4,3]), dtype='float32')
print(v2)

>>> <tf.Variable 'Variable:0' shape=(4, 3) dtype=float32, numpy=
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]], dtype=float32)>

在这里,np.ones(shape=[4,3]) 生成一个形状为 [4,3] 的矩阵,所有元素的值都为 1。下一个代码片段定义了一个具有随机正态初始化的三维(3×4×5) TensorFlow 变量:

v3 = tf.Variable(tf.keras.initializers.RandomNormal()(shape=[3,4,5]), dtype='float32')
print(v3)

>>> <tf.Variable 'Variable:0' shape=(3, 4, 5) dtype=float32, numpy=
array([[[-0.00599647, -0.04389469, -0.03364765, -0.0044175 ,
          0.01199682],
        [ 0.05423453, -0.02812728, -0.00572744, -0.08236874,
         -0.07564012],
        [ 0.0283042 , -0.05198685,  0.04385028,  0.02636188,
          0.02409425],
        [-0.04051876,  0.03284673, -0.00593955,  0.04204708,
         -0.05000611]],

       ...

       [[-0.00781542, -0.03068716,  0.04313354, -0.08717368,
          0.07951441],
        [ 0.00467467,  0.00154883, -0.03209472, -0.00158945,
          0.03176221],
        [ 0.0317267 ,  0.00167555,  0.02544901, -0.06183815,
          0.01649506],
        [ 0.06924769,  0.02057942,  0.01060928, -0.00929202,
          0.04461157]]], dtype=float32)>

在这里,你可以看到如果我们打印一个 tf.Variable,可以看到它的属性,如下所示:

  • 变量的名称

  • 变量的形状

  • 变量的数据类型

  • 变量的初始值

你还可以使用一行代码将你的 tf.Variable 转换为 NumPy 数组

arr = v1.numpy()

然后,你可以通过打印 Python 变量 arr 来验证结果

print(arr) 

将返回

>>> [2\. 2\. 2\. 2.]

tf.Variable 的一个关键特点是,即使在初始化后,你也可以根据需要更改其元素的值。例如,要操作 tf.Variable 的单个元素或片段,你可以使用 assign() 操作如下。

为了本练习的目的,让我们假设以下 TensorFlow 变量,它是一个由零初始化的矩阵,有四行三列:

v = tf.Variable(np.zeros(shape=[4,3]), dtype='float32')

你可以如下更改第一行(即索引为 0)和第三列(即索引为 2)中的元素:

v = v[0,2].assign(1)

这会产生下列数组:

>>> [[0\. 0\. 1.]
     [0\. 0\. 0.]
     [0\. 0\. 0.]
     [0\. 0\. 0.]]

请注意,Python 使用以零为基数的索引。这意味着索引从零开始(而不是从一开始)。例如,如果你要获取向量 vec 的第二个元素,你应该使用 vec[1]。

你也可以使用切片改变数值。例如,下面我们就将最后两行和前两列的数值改为另外一些数:

v = v[2:, :2].assign([[3,3],[3,3]])

结果如下:

>>> [[0\. 0\. 1.]
     [0\. 0\. 0.]
     [3\. 3\. 0.]
     [3\. 3\. 0.]]

练习 2

请编写代码创建一个 tf.Variable,其数值为下面的数值,并且类型为 int16。你可以使用 np.array() 来完成该任务。

1 2 3
4 3 2

2.2.2 理解 tf.Tensor

正如我们所见,tf.Tensor 是对某些数据进行 TensorFlow 操作后得到的输出(例如,对 tf.Variable 或者 tf.Tensor 进行操作)。在定义机器学习模型时,tf.Tensor 对象广泛应用于存储输入、层的中间输出、以及模型的最终输出。到目前为止,我们主要看了向量(一维)和矩阵(二维)。但是,我们也可以创建 n 维数据结构。这样的一个 n 维数据结构被称为一个 张量。表 2.4 展示了一些张量的示例。

表 2.4 张量的示例

描述示例
一个 2 × 4 的二维张量
[
 [1,3,5,7],
 [2,4,6,8]
]

|

一个大小为 2 × 3 × 2 × 1 的四维张量
[
  [
    [[1],[2]],
    [[2],[3]],
    [[3],[4]]
  ],
  [
    [[1],[2]],
    [[2],[3]],
    [[3],[4]]
  ]
]

|

张量也有轴,张量的每个维度都被认为是一个轴。图 2.6 描述了一个 3D 张量的轴。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.6 一个 2 × 4 × 3 张量,包含三个轴。第一个轴(axis 0)是行维度,第二个轴(axis 1)是列维度,最后一个轴(axis 2)是深度维度。

严格来说,张量也可以只有一个维度(即向量)或者只是一个标量。但是需要区分 tensor 和 tf.Tensor。在讨论模型的数学方面时我们会使用 tensor/vector/scalar,而我们在提到 TensorFlow 代码所输出的任何数据相关输出时都会使用 tf.Tensor。

下面我们将讨论一些会产生 tf.Tensor 的情况。例如,你可以通过一个 tf.Variable 和一个常数相乘来产生一个 tf.Tensor:

v = tf.Variable(np.ones(shape=[4,3]), dtype='float32')
b = v * 3.0

如果你使用 print(type(b).name) 分析前面操作生成的对象类型,你会看到下面的输出:

>>> EagerTensor

EagerTensor 是从 tf.Tensor 继承而来的一个类。它是一种特殊类型的 tf.Tensor,其值在定义后会立即得到计算。你可以通过执行下列命令验证 EagerTensor 实际上是 tf.Tensor:

assert isinstance(b, tf.Tensor)

也可以通过将一个 tf.Tensor 加上另一个 tf.Tensor 来创建一个 tf.Tensor。

a = tf.constant(2, shape=[4], dtype='float32')
b = tf.constant(3, shape=[4], dtype='float32')
c = tf.add(a,b)

print© 将打印出下列结果:

>>> [5\. 5\. 5\. 5]

在这个例子中,tf.constant() 用于创建 tf.Tensor 对象 a 和 b。通过将 a 和 b 加在一起,你将得到一个类型为 tf.Tensor 的张量 c。如之前所述,可以通过运行如下代码验证该张量:

assert isinstance(c, tf.Tensor)

tf.Variable 和 tf.Tensor 之间的关键区别在于,tf.Variable 允许其值在变量初始化后发生更改(称为可变结构)。然而,一旦您初始化了一个 tf.Tensor,在执行的生命周期中您就无法更改它(称为不可变数据结构)。tf.Variable 是一种可变数据结构,而 tf.Tensor 是一种不可变数据结构。

让我们看看如果尝试在初始化后更改 tf.Tensor 的值会发生什么:

a = tf.constant(2, shape=[4], dtype='float32')
a = a[0].assign(2.0)

您将收到以下错误:

---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-19-6e4e6e519741> in <module>()
      1 a = tf.constant(2, shape=[4], dtype='float32')
----> 2 a = a[0].assign(2.0)

AttributeError: 'tensorflow.python.framework.ops.EagerTensor' object has no attribute 'assign'

显然,TensorFlow 对我们尝试修改 tf.Tensor 对象的叛逆行为并不感兴趣。

张量动物园

TensorFlow 有各种不同的张量类型,用于解决各种问题。以下是 TensorFlow 中可用的一些不同的张量类型:

RaggedTensor——一种用于不能有效表示为矩阵的可变序列长度数据集的数据类型

TensorArray——一种动态大小的数据结构,可以从小开始,并随着添加更多数据而伸展(类似于 Python 列表)

SparseTensor——一种用于表示稀疏数据的数据类型(例如,用户-电影评分矩阵)

在下一小节中,我们将讨论一些流行的 TensorFlow 操作。

练习 3

你能写出创建初始化为从正态分布中抽样的值并且形状为 4 × 1 × 5 的 tf.Tensor 的代码吗?您可以使用 np.random.normal()来实现这个目的。

2.2.3 理解 tf.Operation

TensorFlow 的骨干是操作,它允许您对数据进行有用的操作。例如,深度网络中的核心操作之一是矩阵乘法,这使得 TensorFlow 成为实现核心操作的强大工具。就像矩阵乘法一样,TensorFlow 提供了许多低级操作,可用于 TensorFlow。可以在TensorFlow API中找到可用操作的完整列表。

让我们讨论一些您可以使用的流行算术操作。首先,您有基本的算术操作,如加法、减法、乘法和除法。您可以像对待普通 Python 变量一样执行这些操作。为了演示这一点,让我们假设以下向量:

import tensorflow as tf
import numpy as np

a = tf.constant(4, shape=[4], dtype='float32')
b = tf.constant(2, shape=[4], dtype='float32')

我们可以通过执行以下操作来查看 a 和 b 的样子

print(a)
print(b)

这给出

>>> tf.Tensor([4\. 4\. 4\. 4.], shape=(4,), dtype=float32)
>>> tf.Tensor([2\. 2\. 2\. 2.], shape=(4,), dtype=float32)

对 a 和 b 执行加法

c = a+b
print(c)

提供

>>> tf.Tensor([6\. 6\. 6\. 6.], shape=(4,), dtype=float32)

对 a 和 b 执行乘法

e = a*b
print(e)

提供

>>> tf.Tensor([8\. 8\. 8\. 8.], shape=(4,), dtype=float32)

您还可以在张量之间进行逻辑比较。假设

a = tf.constant([[1,2,3],[4,5,6]])
b = tf.constant([[5,4,3],[3,2,1]])

并检查逐元素相等性

equal_check = (a==b)
print(equal_check)

提供

>>> tf.Tensor(
    [[False False  True]
     [False False False]], shape=(2, 3), dtype=bool) 

检查小于或等于元素

leq_check = (a<=b)
print(leq_check)

提供

>>> tf.Tensor(
    [[ True  True  True]
     [False False False]], shape=(2, 3), dtype=bool)

接下来,您有减少运算符,允许您在特定轴或所有轴上减少张量(例如,最小值/最大值/和/乘积):

a = tf.constant(np.random.normal(size=[5,4,3]), dtype='float32')

这里,a 是一个看起来像这样的 tf.Tensor:

>>> tf.Tensor(
    [[[-0.7665215   0.9611947   1.456347  ]
      [-0.52979267 -0.2647674  -0.57217133]
      [-0.7511135   2.2282166   0.6573406 ]
      [-1.1323775   0.3301812   0.1310132 ]]
     ...
     [[ 0.42760614  0.17308706 -0.90879506]
      [ 0.5347165   2.569637    1.3013649 ]
      [ 0.95198756 -0.74183583 -1.2316796 ]
      [-0.03830088  1.1367576  -1.2704859 ]]], shape=(5, 4, 3), dtype=float32)

让我们首先获取此张量的所有元素的总和。换句话说,在所有轴上减少张量:

red_a1 = tf.reduce_sum(a)

这产生

>>> -4.504758

接下来,让我们在轴 0 上获取产品(即,对 a 的每一行进行逐元素乘积):

red_a2 = tf.reduce_prod(a, axis=0)

这产生

>>> [[-0.04612858  0.45068324  0.02033644]
     [-0.27674386 -0.03757533 -0.33719817]
     [-1.4913832  -2.1016302  -0.39335614]
     [-0.00213956  0.14960718  0.01671476]]

现在我们将在多个轴(即 0 和 1)上获取最小值:

red_a3 = tf.reduce_min(a, axis=[0,1])

这产生

>>> [-1.6531237 -1.6245098 -1.4723392]

你可以看到,无论何时在某个维度上执行缩减操作,你都会失去该维度。例如,如果你有一个大小为[6,4,2]的张量,并且在轴 1 上缩减该张量(即第二个轴),你将得到一个大小为[6,2]的张量。在某些情况下,你需要在缩减张量的同时保留该维度(导致一个[6,1,2]形状的张量)。一个这样的情况是使你的张量广播兼容另一个张量(mng.bz/g4Zn)。广播是一个术语,用来描述科学计算工具(例如 NumPy/TensorFlow)在算术操作期间如何处理张量。在这种情况下,你可以将 keepdims 参数设置为 True(默认为 False)。你可以看到最终输出的形状的差异

# Reducing with keepdims=False
red_a1 = tf.reduce_min(a, axis=1)
print(red_a1.shape)

这产生

>>> [5,3]

# Reducing with keepdims=True
red_a2 = tf.reduce_min(a, axis=1, keepdims=True)
print(red_a2.shape)

这产生

>>> red_a2.shape = [5,1,3]

表 2.5 中概述了其他几个重要的函数。

表 2.5 TensorFlow 提供的数学函数

tf.argmax描述计算给定轴上最大值的索引。例如,以下示例显示了如何在轴 0 上计算 tf.argmax。
用法d = tf.constant([[1,2,3],[3,4,5],[6,5,4]])d_max1 = tf.argmax(d, axis=0)
结果tf.Tensor ([2,2,0])
tf.argmin描述计算给定轴上最小值的索引。例如,以下示例显示了如何在轴 1 上计算 tf.argmin。
用法d = tf.constant([[1,2,3],[3,4,5],[6,5,4]])d_min1 = tf.argmin(d, axis=1)
结果tf.Tensor([[0],[0],[0]])
tf.cumsum描述计算给定轴上向量或张量的累积和
用法e = tf.constant([1,2,3,4,5])e_cumsum = tf.cumsum(e)
结果tf.Tensor([1,3,6,10,15])

我们在这里结束了对 TensorFlow 基本原语的讨论。接下来我们将讨论在神经网络模型中常用的一些计算。

练习 4

还有另一个计算平均值的函数叫做 tf.reduce_mean()。给定包含以下值的 tf.Tensor 对象 a,你能计算每列的平均值吗?

0.5 0.2 0.7
0.2 0.3 0.4
0.9 0.1 0.1

2.3 TensorFlow 中与神经网络相关的计算

这里我们将讨论一些支撑深度神经网络的关键低级操作。假设你在学校学习计算机视觉课程。对于你的作业,你必须使用各种数学运算来操作图像,以实现各种效果。我们将使用一张著名的狒狒图像(图 2.7),这是计算机视觉问题的常见选择。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.7 狒狒的图像

2.3.1 矩阵乘法

你的第一个任务是将图像从 RGB 转换为灰度。为此,你必须使用矩阵乘法。让我们首先了解什么是矩阵乘法。

Lena 的故事

尽管我们在练习中使用了狒狒的图像,但长期以来,一直有一个传统,即使用 Lena(一位瑞典模特)的照片来演示各种计算机视觉算法。关于这是如何成为计算机视觉问题的规范的背后有一个非常有趣的故事,您可以在mng.bz/enrZ上阅读。

使用 tf.matmul()函数在两个张量之间执行矩阵乘法。对于两个矩阵,tf.matmul()执行矩阵乘法(例如,如果您有大小为[4,3]和大小为[3,2]的矩阵,矩阵乘法将得到一个[4,2]张量)。图 2.8 说明了矩阵乘法操作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.8 在一个 4×3 矩阵和一个 3×2 矩阵之间进行矩阵乘法,得到一个 4×2 矩阵。

更一般地说,如果您有一个 n×m 矩阵(a)和一个 m×p 矩阵(b),则矩阵乘法 c 的结果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是,如果您有高维张量 a 和 b,则将在 a 的最后一个轴上和 b 的倒数第二个轴上执行总乘积。a 和 b 张量的维度除了最后两个轴外都需要相同。例如,如果您有一个大小为[3,5,7]的张量 a 和大小为[3,7,8]的张量 b,则结果将是一个大小为[3,5,8]的张量。

回到我们的问题,给定三个 RGB 像素,您可以使用以下方法将其转换为灰度像素。

0.3 * R + 0.59 * G + 0.11 * B

这是将任何 RGB 图像转换为灰度图像的常见操作(mng.bz/p2M0),这取决于手头的问题是否重要。例如,要从图像中识别数字,颜色并不那么重要。通过将图像转换为灰度,您实质上通过减少输入的大小(一个通道而不是三个)并去除噪声特征(即,颜色信息),从而帮助模型。

给定一个 512×512×3 的图像,如果您将其与代表所提供权重的 3×1 数组相乘,您将得到大小为 512×512×1 的灰度图像。然后,我们需要删除灰度图像的最后一个维度(因为它是一个),最终得到大小为 512×512 的矩阵。对此,您可以使用 tf.squeeze()函数,该函数删除大小为一的任何维度(请参阅下一个列表)。

列表 2.2 使用矩阵乘法将 RGB 图像转换为灰度图像

from PIL import Image                                          ❶
import tensorflow as tf
import numpy as np

x_rgb = np.array(Image.open("baboon.jpg")).astype('float32')   ❷
x_rgb = tf.constant(x_rgb)                                     ❸

grays = tf.constant([[0.3], [0.59] ,[0.11]])                   ❹

x = tf.matmul(x_rgb, grays)                                    ❺
x = tf.squeeze(x)

❶ PIL 是用于基本图像处理的 Python 库。

❷ 大小为 512×512×3 的 RGB 图像被加载为 NumPy 数组。

❸ 将 NumPy 数组转换为 tf.Tensor。

❹ 作为一个 3×1 数组的 RGB 权重

❺ 执行矩阵乘法以获得黑白图像

❻ 去掉最后一个维度,即 1

矩阵乘法在全连接网络中也是一项重要操作。为了从输入层到隐藏层,我们需要使用矩阵乘法和加法。暂时忽略非线性激活,因为它只是一个逐元素的转换。图 2.9 可视化了您之前构建的 MLP 的隐藏层计算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.9 计算发生在隐藏层的插图。x 是输入(1×4),W 是权重矩阵(4×3),b 是偏差(1×3),最终,h 是输出(1×3)。

2.3.2 卷积操作

接下来的任务是实现边缘检测算法。知道可以使用卷积操作检测边缘后,您还想使用 TensorFlow 展示自己的技能。好消息是,您可以做到!

卷积操作在卷积神经网络中非常重要,卷积神经网络是用于图像相关的机器学习任务(例如图像分类、物体检测)的深度网络。卷积操作将 窗口(也称为filterkernel)移动到数据上,同时在每个位置产生单个值。卷积窗口在每个位置都有一些值。对于给定位置,卷积窗口中的值是元素乘积并与数据中与该窗口重叠的部分相加,以产生该位置的最终值。卷积操作如图 2.10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.10 卷积操作的计算步骤

根据您选择的卷积窗口的值,您可以产生一些独特的效果。您可以尝试在setosa.io/ev/image-kernels/上尝试一些流行的核。边缘检测也是一种流行的计算机视觉技术,可以使用卷积操作来实现。TensorFlow 提供了 tf.nn.convolution()函数来执行卷积。

首先,我们将黑白狒狒图片存储在 tf.Tensor 中,并将其输入变量命名为 x,x 是一个大小为 512×512 的矩阵。现在,让我们从中创建一个名为 y 的新变量:

y = tf.constant(x)

接下来,让我们定义我们的边缘检测 filter。我们将使用一种名为 近似拉普拉斯滤波器 的边缘检测滤波器,它是一个填有 -1 值的 3 × 3 矩阵,除了最中间的值是 8 外。请注意,内核的总和为零:

filter = tf.Variable(np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]]).astype('float32'))

接下来我们需要对 y 和 filter 进行 reshape,因为 tf.nn.convolution() 函数接受具有非常特定形状的输入和 filter。第一个约束是 y 和 filter 应具有相同的 rank。在这里,rank 指数据中的维数个数。我们这里有 rank2 的张量,将进行二维卷积。要执行 2D 卷积,输入和 kernel 都需要是 rank 4。因此我们需要对输入和 kernel 进行几步重塑:

  1. 在输入的开始和结尾添加两个更多的维度。开始的维度表示批量维度,最后的维度表示通道维度(例如,图像的 RGB 通道)。虽然在我们的示例中值为 1,但我们仍然需要这些维度存在(例如,一个大小为[512,512]的图像将重塑为[1,512,512,1])。

  2. 在 filter 的末尾添加两个大小为 1 的额外维度。这些新维度表示输入和输出通道。我们有一个单通道(即灰度)输入,我们也希望产生一个单通道(即灰度)的输出(例如,一个大小为[3,3]的核将被重塑为[3,3,1,1])。

注意 张量的阶数是指该张量的维数。这与矩阵的秩不同。

如果你不完全明白为什么我们添加了这些额外的维度,不要担心。当我们在后面的章节中讨论卷积神经网络中的卷积操作时,这将更有意义。现在,你只需要理解卷积操作的高级行为就可以了。在 TensorFlow 中,你可以按如下方式重塑 y 和 filter:

y_reshaped = tf.reshape(y, [1,512,512,1])
filter_reshaped = tf.reshape(filter, [3,3,1,1])

这里,y 是一个 512×512 的张量。表达式 tf.reshape(y, [1,512,512,1])将 y(即一个 2D 张量)重塑为一个 4D 张量,大小为 1×512×512×1。同样,filter(即一个大小为 3×3 的 2D 张量)被重塑为一个 4D 张量,大小为 3×3×1×1。请注意,在重塑过程中元素的总数保持不变。现在你可以计算卷积输出如下:

y_conv = tf.nn.convolution(y_reshaped, filter_reshaped)

你可以将边缘检测的结果可视化并将其与原始图像进行比较,如图 2.11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.11 原始黑白图像与边缘检测结果的比较

在下一节中,我们将讨论另一种操作,即 pooling 操作。

2.3.3 Pooling 操作

我们接下来的任务是将经过边缘检测后的图像的宽度和高度减半。例如,如果我们有一个 512×512 的图像,并且需要将其调整为 256×256,pooling 操作是实现这一目标的最佳方式。出于这个原因,pooling(或子采样)操作在卷积神经网络中常被用于减小输出的尺寸,以便可以使用更少的参数从数据中学习。

为什么它被称为 pooling 操作?

子采样操作之所以也被称为“pooling”可能是因为这个词的含义以及统计学的原因。pooling一词用于描述将事物合并为一个单一实体,这也正是此操作所做的(例如通过平均值或取最大值)。在统计学中,你会发现术语pooled variance,它是两个群体之间方差的加权平均值(mng.bz/OGdO),本质上将两个方差合并为一个方差。

在 TensorFlow 中,您可以调用 tf.nn.max_pool()函数进行最大池化,调用 tf.nn.avg_pool()函数进行平均池化:

z_avg = tf.nn.avg_pool(y_conv, (1,2,2,1), strides=(1,2,2,1), padding='VALID')
z_max = tf.nn.max_pool(y_conv, (1,2,2,1), strides=(1,2,2,1), padding='VALID')

池化操作是卷积神经网络中常见的另一种操作,它的工作原理与卷积操作类似。但与卷积操作不同的是,池化操作的内核中没有值。在给定位置,池化操作取得与数据中内核重叠的部分的平均值或最大值。在给定位置产生平均值的操作称为平均池化,而产生最大值的操作称为最大池化。图 2.12 说明了最大池化操作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.12:最大池化操作。池化窗口在图像上从一个位置移动到另一个位置,同时一次产生一个值(即与池化窗口重叠的图像中的最大值)。

我们有一个形状为[1,510,510,1]的 4D 张量 y_conv。你可能会注意到,这些维度略小于原始图像的大小(即 512)。这是因为,在对具有 h 高度和 w 宽度的图像进行没有额外填充的大小为 c×c 的窗口卷积时,得到的图像的维度为 h-c+1 和 w-c+1。我们可以进行如下所示的池化操作。您可以使用以下函数进行平均池化或最大池化:

z_avg = tf.nn.avg_pool(y_conv, (1,2,2,1), strides=(1,2,2,1), padding='VALID')
z_max = tf.nn.max_pool(y_conv, (1,2,2,1), strides=(1,2,2,1), padding='VALID')

这将得到两个图像,z_avg 和 z_max;它们的形状都是[1,255,255,1]。为了仅保留高度和宽度维度并移除大小为 1 的冗余维度,我们使用 tf.squeeze()函数:

z_avg = np.squeeze(z_avg.numpy())
z_max = np.squeeze(z_max.numpy())

您可以使用 Python 的绘图库 matplotlib 绘制 z_avg 和 z_max 并获得图 2.13 中所示的结果。代码已在笔记本中提供。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.13:边缘检测后的结果与平均或最大池化后的结果

图 2.13 显示了不同类型池化的效果。仔细观察,您会看到平均池化结果更加一致和连续,而最大池化结果则更加嘈杂。

请注意,与卷积操作不同,我们没有提供滤波器(或内核),因为池化操作没有滤波器。但是我们需要传递窗口的维度。这些维度表示输入的相应维度(即它是一个[batch 维度、高度、宽度、通道]的窗口)。除此之外,我们还传递了两个参数:stride 和 padding。我们将在后面的章节中详细讨论这些参数。

练习 5

给定一个大小为 256×256 的灰度图像 img 和一个大小为 5×5 的卷积滤波器 f。你能编写 tf.reshape()函数调用和 tf.nn.convolution()操作吗?输出的大小会是多少?

很好!现在你已经了解了深度学习网络中最常用的操作。我们将在此结束关于 TensorFlow 基础知识的讨论。在下一章中,我们将讨论 TensorFlow 中提供的一个高级 API,称为 Keras,它对于模型构建特别有用。

摘要

  • TensorFlow 是一个端到端的机器学习框架。

  • TensorFlow 提供了一个生态系统,便于模型原型设计、模型构建、模型监控和模型提供。

  • TensorFlow 1 使用声明式图执行风格(先定义,然后运行),而 TensorFlow 2 使用命令式图执行风格(运行时定义)。

  • TensorFlow 提供了三个主要构建模块:tf.Variable(用于随时间变化的值)、tf.Tensor(随时间固定的值)和 tf.Operation(对 tf.Variable 和 tf.Tensor 对象执行的转换)。

  • TensorFlow 提供了几个用于构建神经网络的操作,如 tf.matmul、tf.nn.convolution 和 tf.nn.max_pool。

  • 您可以使用 tf.matmul 将 RGB 图像转换为灰度图像。

  • 您可以使用 tf.nn.convolution 来检测图像中的边缘。

  • 您可以使用 tf.nn.max_pool 来调整图像大小。

练习答案

练习 1: 2

练习 2: tf.Variable(np.array([[1,2,3],[4,3,2]], dtype=”int16”)

练习 3: tf.constant(np.random.normal(size=[4,1,5]))

练习 4: tf.reduce_mean(a, axis=1)

练习 5:

img_reshaped = tf.reshape(img, [1,256,256,1])
f_reshaped = tf.reshape(f, [5,5,1,1])
y = tf.nn.convolution(img_reshaped, f_reshaped)

最终输出的形状将是 [1,252,252,1]。卷积操作的结果大小为图像大小 - 卷积窗口大小 + 1。

第三章:Keras 和 TensorFlow 2 中的数据检索

本章涵盖的内容

  • Keras 中用于构建模型的不同 API

  • 检索和操作持久化数据

我们已经探讨了低级 TensorFlow API 的细节,比如定义 tf.Variable 对象和 tf.Tensor 对象,这些对象可以用来存储数字和字符串等。我们还查看了 TensorFlow 提供的一些常用功能,如 tf.Operation。最后,我们详细讨论了一些复杂的操作,比如矩阵乘法和卷积。如果你分析任何标准的深度神经网络,你会发现它由矩阵乘法和卷积等标准数学操作构成。

然而,如果你要使用低级别的 TensorFlow API 实现这些网络,你会发现自己在代码中多次复制这些操作,这将耗费宝贵的时间,并使代码难以维护。但好消息是你不需要这样做。TensorFlow 提供了一个名为 Keras 的子模块,它解决了这个问题,这也是本章的重点。Keras 是 TensorFlow 中的一个子库,它隐藏了构建模块,并为开发机器学习模型提供了高级 API。在本章中,我们将看到 Keras 提供了几种不同的 API,可以根据解决方案的复杂性来选择使用。

我们将通过讨论机器学习的另一个重要方面来结束本章:向模型提供数据。通常,我们需要从磁盘(或网络)中检索数据,并在将其提供给模型之前清理和处理数据。我们将讨论 TensorFlow 中几种不同的数据检索工具,如 tf.data 和 tensorflow-datasets API,以及它们如何简化读取和操作最终输入模型的数据。

3.1 Keras 模型构建 API

作为黑客马拉松的一部分,您正在开发一个花卉物种分类器。您的团队将创建几个不同的多层感知器变体,以便将它们的性能与花卉物种识别数据集进行比较。目标是训练能够在给定花卉的多个测量值的情况下输出花卉物种的模型。您需要开发的模型如下:

  • 模型 A——仅从提供的特征中学习的模型(基线)

  • 模型 B——除了使用特征本身外,还使用特征的主成分(详见 3.1.3 节)

  • 模型 C——一个使用非常规隐藏层计算的模型,它使用了一个乘法偏差,除了传统的加法偏差之外,这在神经网络中通常是找不到的(详见 3.1.4 节)

您计划使用 Keras,并且知道它提供了多个模型构建 API。为了快速提供结果,您需要知道在哪个模型中使用哪个 Keras API。

Keras (keras.io/) 最初是作为一个高级 API 启动的,可以使用多个低级后端(例如,TensorFlow、Theano),并允许开发人员轻松构建机器学习模型。换句话说,Keras 隐藏了低级操作的细节,并提供了一个直观的 API,您可以用几行代码构建模型。自从 TensorFlow 1.4 以来,Keras 已经集成到了 TensorFlow 中(www.tensorflow.org/guide/keras/overview)。您可以使用import tensorflow.keras导入 Keras。Keras 有三个主要的 API:

  • 顺序式

  • 函数式

  • 子类化

顺序式 API 是最容易使用的。然而,它是一个非常受限制的 API,只允许您创建一个以一个输入开始,经过一系列层,以及以一个输出结束的网络。接下来,函数式 API 需要更多的工作才能使用。但它也提供了更多的灵活性,例如具有多个输入、并行层和多个输出。最后,子类化 API 可以被认为是最难驾驭的。其思想是创建一个代表您的模型或模型中的一层的 Python 对象,同时使用 TensorFlow 提供的低级功能来实现所需的功能。让我们简要地介绍一下如何使用这些 API。但我们不会止步于此;在接下来的章节中,我们将更详细地了解这些 API。图 3.1 突出显示了 API 之间的主要区别。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.1 顺序式、函数式和子类化 API 的比较。

在这里,对于模型 A,我们将使用顺序式 API,因为它是最简单的。要实现模型 B,其中将有两个输入层,我们将使用函数式 API。最后,要实现模型 C,其中我们需要实现一个自定义层,我们将使用子类化 API。

3.1.1 引入数据集

假设您决定使用一个名为鸢尾花数据集(archive.ics.uci.edu/ml/datasets/Iris)的流行机器学习数据集。这个数据集记录了几种不同种类的鸢尾花(Iris-setosa、Iris-versicolor 和 Iris-virginica)的萼片长度、萼片宽度、花瓣长度和花瓣宽度。对于每朵花,我们都有萼片长度/宽度和花瓣长度/宽度。正如您所看到的,每个输入都有四个特征,每个输入都可以属于三个类别之一。首先,让我们下载数据,对其进行快速分析,并将其格式化为我们可以方便地用于模型训练的格式。

首先,您需要确保环境设置正确,并且已安装所需的库,如附录 A 中所述。接下来,打开位于 Ch03-Keras-and-Data-Retrieval/3.1.Keras_APIs.ipynb 的 Jupyter 笔记本。现在,如笔记本中的代码所示,我们需要导入 requests 库来下载数据,导入 pandas 来操作该数据,当然,还有 TensorFlow:

import requests
import pandas as pd
import tensorflow as tf

现在我们将下载数据并将数据保存到文件中:

url = "https:/ /archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"
r = requests.get(url)

# Writing data to a file
with open('iris.data', 'wb') as f:
  f.write(r.content)

然后,我们使用 pandas 库的 read_csv() 函数读取数据 (mng.bz/j2Op):

iris_df = pd.read_csv('iris.data', header=None)

这里,iris_df 是一个 pandas DataFrame (mng.bz/Wxaw)。在最简单的形式下,数据帧可以被认为是一个按行和列组织的信息矩阵。您可以使用 iris_df.head() 命令检查数据的前几行,其结果如下:

0     1       2       3       4
0     5.1     3.5     1.4     0.2     Iris-setosa
1     4.9     3.0     1.4     0.2     Iris-setosa
2     4.7     3.2     1.3     0.2     Iris-setosa

然后,我们将对数据进行一些装饰性修改,使其看起来更好。我们将提供适当的列名称(可从数据集的网页中获取)

iris_df.columns = ['sepal_length', 'sepal_width', 'petal_width', 'petal_length', 'label']

并将字符串标签映射为整数:

iris_df["label"] = iris_df["label"].map({'Iris-setosa':0, 'Iris-versicolor':1, 'Iris-virginica':2})

我们得到了以下改进后的 pandas DataFrame:

      sepal_length   sepal_width   petal_width   petal_length   label
0     5.1             3.5           1.4           0.2            0
1     4.9             3.0           1.4           0.2            0
2     4.7             3.2           1.3           0.2            0

作为最后一步,我们将通过从每列中减去均值来将数据居中,因为这通常会导致更好的性能:

iris_df = iris_df.sample(frac=1.0, random_state=4321)
x = iris_df[["sepal_length", "sepal_width", "petal_width", "petal_length"]]
x = x - x.mean(axis=0)
y = tf.one_hot(iris_df["label"], depth=3)

在这里,print(x) 将输出

        sepal_length  sepal_width  petal_width  petal_length
31      -0.443333        0.346    -2.258667     -0.798667
23      -0.743333        0.246    -2.058667     -0.698667
70       0.056667        0.146     1.041333      0.601333
100      0.456667        0.246     2.241333      1.301333
44      -0.743333        0.746    -1.858667     -0.798667
..            ...          ...          ...           ...

注意 在对数据进行洗牌后,索引不再按顺序排列。print(y) 将输出

tf.Tensor(
    [[1\. 0\. 0.]
     [1\. 0\. 0.]
     [0\. 1\. 0.]
     ...
     [0\. 0\. 1.]
     [0\. 0\. 1.]
     [0\. 1\. 0.]], 
shape=(150, 3), dtype=float32)

对数据进行洗牌是一个重要的步骤:数据是按特定顺序排列的,每个类别都紧跟在另一个后面。但是当数据被洗牌时,每个被呈现给网络的批次都有所有类别的良好混合,这样可以获得最佳结果。您还可以看到我们对 y(或标签)使用了一种称为 独热编码 的转换。独热编码将每个标签转换为唯一的零向量,其中一个元素为一。例如,标签 0、1 和 2 被转换为以下独热编码向量:

0 → [1, 0, 0]

1 → [0, 1, 0]

2 → [0, 0, 1]

3.1.2 顺序 API

数据准备就绪后,是时候实现模型 A 了,这是第一个神经网络。第一个模型非常简单,只需要提供的特征并预测花的种类。您可以使用 Keras 顺序 API,因为它是最简单的,我们所需要做的就是将几个层顺序堆叠在一起。图 3.2 描述了顺序 API 与其他 API 的比较。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.2 顺序 API 与其他 API 的比较(被标记为灰色)

让我们创建一个具有以下特点的网络:

  • 一个具有 4 个节点的输入层

  • 一个具有 32 个节点的隐藏层

  • 一个具有 16 个节点的隐藏层

  • 一个 3 节点输出层

注意 每层节点数是模型的超参数。在这种情况下,我们任意选择了这些值。但为了获得最佳结果,我们应该使用一个超参数优化算法 (mng.bz/8MJB) 来找到给定问题的最佳超参数。

在定义模型之前,我们需要导入 TensorFlow 中的某些层和顺序模型。然后,您可以使用一行代码即可实现该模型(见下一个清单)。

利用 Sequential API 实现的模型 A,如 3.1 清单所示。

from tensorflow.keras.layers import Dense            ❶
from tensorflow.keras.models import Sequential       ❶
import tensorflow.keras.backend as K                 ❶

K.clear_session()                                    ❷
model = Sequential([                                 ❸
    Dense(32, activation='relu', input_shape=(4,)),  ❸
    Dense(16, activation='relu'),                    ❸
    Dense(3, activation='softmax')])

❶导入必要的模块和类。

❷在创建模型之前清除 TensorFlow 计算图。

❸使用 Sequential API 定义模型。

让我们分析一下我们刚刚做的。您可以使用 Sequential 对象创建一个顺序模型,然后传递一个层的序列,例如 Dense 层。一个层封装了神经网络中可以找到的典型的可重复使用的计算(例如隐藏层计算,卷积操作)。

Dense 层提供了全连接网络中所发生的核心计算(即通过 h = activation(xW + b)从输入(x)到隐藏输出(h))。Dense 层有两个重要参数:隐藏单元的数量和非线性激活函数。通过堆叠一组 Dense 层,您可以构建一个多层的全连接网络。我们正在使用以下层构建网络:

  • Dense(32, activation=‘relu’, input_shape=(4,))

  • Dense(16, activation=‘relu’)

  • Dense(3, activation=‘softmax’)

在第一个 Dense 层中,可以看到传递了一个额外的参数 input_shape。input_shape 是使用 TensorFlow 创建的任何模型的关键属性。您必须确切地知道要传递给模型的输入的形状,因为随后的所有层的输出都取决于输入的形状。实际上,某些层只能处理某些特定的输入形状。

在这个例子中,我们说输入的形状将是[None, 4]。虽然我们只在形状中指定了 4,但 Keras 会自动添加一个未指定的(即 None)维度到 input_shape 中,它表示输入的批次维度。正如您可能已经知道的,深度神经网络以批次的方式处理数据(即一次处理多个示例)。另一个尺寸(大小为 4)是特征维度,意味着网络可以接受具有四个特征的输入。将批次维度设为 None 将批次维度未指定,允许您在模型训练/推断时传递任意数量的示例。

一个层的另一个重要方面是层中使用的非线性激活函数。在这里,我们可以看到前两个层使用了 ReLU(修正线性单元)激活函数。它是前馈模型中非常简单但功能强大的激活函数。ReLU 具有以下功能:

y = max (0, x)

最后一层使用了 softmax 激活函数。正如之前讨论的,softmax 激活函数将最后一层(即 logits)的得分归一化为一个有效的概率分布。具体来说,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以一个示例为例,假设最后一层没有使用 softmax 激活函数产生了

[15, 30, 5]

应用 softmax 归一化将这些值转换为

[15/(15+30+5), 30/(15+30+5), 5/(15+30+5)]
= [0.3, 0.6, 0.1]

现在模型已经定义好了,我们需要执行一个关键步骤,称为模型编译,如果我们要成功地使用它的话。对于我们的模型,我们将使用

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

在这里,我们设置了模型的损失函数、优化器和度量标准。损失函数表示模型在给定数据上的表现如何(例如,分类交叉熵)。损失越低,模型就越好。除了损失函数之外,我们还使用了一个优化器,它知道如何改变模型的权重和偏差,以使损失减少。在这里,我们选择了损失函数 categorical_crossentropy(mng.bz/EWej),这通常在多类别分类问题中效果良好,以及优化器 adam(arxiv.org/pdf/1412.6980.pdf),由于其在各种问题中的出色表现,是一个常见的选择。我们还可以选择性地定义度量标准来关注模型(例如,模型准确率)。最后,我们可以使用以下方法检查您刚刚创建的模型

model.summary()

输出

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_3 (Dense)              (None, 32)                160       
_________________________________________________________________
dense_4 (Dense)              (None, 16)                528       
_________________________________________________________________
dense_5 (Dense)              (None, 3)                 51        
=================================================================
Total params: 739
Trainable params: 739
Non-trainable params: 0
_________________________________________________________________

模型摘要清晰地显示了层数、每个层的类型、每个层的输出形状以及每个层的参数数量。让我们使用之前准备好的数据集来训练这个模型,以对各种鸢尾花进行分类。我们使用方便的 fit()函数来训练一个 Keras 模型:

model.fit(x, y, batch_size=64, epochs=25)

fit()函数接受许多不同的参数:

  • X—数据特征

  • Y—数据标签(独热编码)

  • 批处理大小(可选)—单个批次中的数据点数量

  • epochs(可选)—模型训练期间重复数据集的次数

像 batch_size 和 epochs 这样的值是经验性地选择的。如果你运行前面的代码,你将得到以下结果:

Train on 150 samples
Epoch 1/25
150/150 [==============================] - 0s 2ms/sample - loss: 1.1773 - acc: 0.2667
Epoch 2/25
150/150 [==============================] - 0s 148us/sample - loss: 1.1388 - acc: 0.2933
...
Epoch 24/25
150/150 [==============================] - 0s 104us/sample - loss: 0.6254 - acc: 0.7400
Epoch 25/25
150/150 [==============================] - 0s 208us/sample - loss: 0.6078 - acc: 0.7400

看起来我们的小型项目相当成功,因为我们观察到训练准确率(“acc”)在只有 25 个 epochs 的情况下稳步增长到了 74%。然而,仅仅依靠训练准确率来决定一个模型是否表现更好是不明智的。有各种技术可以做到这一点,我们将在接下来的章节中进行回顾。

机器学习中的可重现性

可重现性是机器学习中的一个重要概念。可重现性意味着你可以运行一个实验,发布结果,并确保对你的研究感兴趣的人可以复现结果。它还意味着你将在多次试验中得到相同的结果。如果你看一下笔记本 ch02/1.Tensorflow_ Fundamentals.ipynb,你会看到我们已经采取的一项措施,以确保结果在多次试验中保持一致。你将在“Library imports and some setups”部分看到以下代码:

def fix_random_seed(seed):
    try:
        np.random.seed(seed)
    except NameError:
        print("Warning: Numpy is not imported. Setting the seed for Numpy failed.")
    try:
        tf.random.set_seed(seed)
    except NameError:
        print("Warning: TensorFlow is not imported. Setting the seed for TensorFlow failed.")
    try:
        random.seed(seed)
    except NameError:
        print("Warning: random module is not imported. Setting the seed for random failed.")

# Fixing the random seed
fix_random_seed(4321)

随机种子是影响研究可重复性的一个常见因素,因为神经网络普遍使用随机初始化。通过固定种子,你可以确保每次运行代码时都会得到相同的随机数序列。这意味着在多次试验中,模型的权重和偏置初始化是相同的,前提是其他条件没有改变。

为了确保你的代码能够产生一致的结果,请在尝试代码练习时调用 fix_random_seed 函数(通过运行第一个代码单元格)。

3.1.3 函数式 API

现在是时候实现第二个模型(即,模型 B)了,该模型使用主成分作为额外的输入。希望这个额外输入(主成分)能为模型提供额外的特征,从而提高模型的性能。主成分是使用一种称为主成分分析(PCA)的算法提取出来的。PCA 是一种降维技术,它会将高维数据投影到一个较低维度的空间中,同时努力保留数据中存在的方差。现在你需要创建一个模型,该模型接受两个不同的输入特征集。

你不能再使用 Sequential API,因为它只能处理顺序模型(即,单输入层通过一系列层产生单输出)。在这里,我们有两个不同的输入:花卉的原始特征和 PCA 特征。这意味着两个层以并行方式工作,产生两个不同的隐藏表示,并将它们连接起来,最后为输入产生类别概率,如图 3.3 所示。函数式 API 对于这种类型的模型是一个很好的选择,因为它可以用于定义具有多个输入或多个输出的模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.3 函数式 API 与其他 API 的对比(灰色块为无法使用的功能)

让我们开始吧。首先,我们需要导入以下层和模型对象,因为它们将成为我们模型的核心:

from tensorflow.keras.layers import Input, Dense, Concatenate
from tensorflow.keras.models import Model

接下来,我们需要创建两个 Input 层(用于原始输入特征和 PCA 特征):

inp1 = Input(shape=(4,))
inp2 = Input(shape=(2,))

原始输入特征的 Input 层将有四个特征列,而 PCA 特征的 Input 层将只有两个特征列(因为我们只保留了前两个主成分)。如果回顾一下我们如何使用 Sequential API 定义模型,你会注意到我们没有使用 Input 层。但在使用函数式 API 时,我们需要明确指定我们需要包含在模型中的 Input 层。

定义了两个 Input 层后,我们现在可以计算这些层的单独隐藏表示:

out1 = Dense(16, activation='relu')(inp1)
out2 = Dense(16, activation='relu')(inp2)

这里,out1 表示 inp1 的隐藏表示(即原始特征),out2 是 inp2 的隐藏表示(即 PCA 特征)。然后我们连接这两个隐藏表示:

out = Concatenate(axis=1)([out1,out2])

让我们更详细地了解在使用 Concatenate 层时会发生什么。Concatenate 层只是沿着给定的轴连接两个或多个输入。在此示例中,我们有两个输入传递给 Concatenate 层(即 [None, 16] 和 [None, 16]),并希望沿着第二个轴(即 axis=1)进行连接。请记住,当您指定 shape 参数时,Keras 会向输入/输出张量添加一个额外的批次维度。这个操作的结果是一个大小为 [None, 32] 的张量。从这一点开始,您只有一个序列的层。我们将定义一个具有 relu 激活函数的 16 节点 Dense 层,最后是一个具有 softmax 归一化的三节点输出层:

out = Dense(16, activation='relu')(out)
out = Dense(3, activation='softmax')(out)

我们需要做一步额外的工作:创建一个 Model 对象,说明输入和输出是什么。现在,我们有一堆层和没有 Model 对象。最后,我们像之前一样编译模型。我们选择 categorical_crossentropy 作为损失函数,adam 作为优化器,像之前一样。我们还将监控训练准确率:

model = Model(inputs=[inp1, inp2], outputs=out)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

这个模型的完整代码在以下清单中提供。

列表 3.2 使用 Keras 函数式 API 实现的模型 B

from tensorflow.keras.layers import Input, Dense, Concatenate
from tensorflow.keras.models import Model
import tensorflow.keras.backend as K

K.clear_session()                                                                ❶

inp1 = Input(shape=(4,))                                                         ❷
inp2 = Input(shape=(2,))                                                         ❷

out1 = Dense(16, activation='relu')(inp1)                                        ❸
out2 = Dense(16, activation='relu')(inp2)                                        ❸

out = Concatenate(axis=1)([out1,out2])                                           ❹

out = Dense(16, activation='relu')(out)
out = Dense(3, activation='softmax')(out) 

model = Model(inputs=[inp1, inp2], outputs=out)                                  ❺
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

❶ 确保清除 TensorFlow 图

❷ 两个输入层。一个输入层具有四个特征,另一个输入层具有两个特征。

❸ 两个并行隐藏层

❹ 负责将两个并行输出 out1 和 out2 进行拼接的连接层

❺ 模型定义

❻ 使用损失函数、优化器和评估指标编译模型

现在你可以打印模型的摘要了

model.summary()

得到的结果为

Model: "model"
_____________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to 
=====================================================================================
input_1 (InputLayer)            [(None, 4)]          0 
_____________________________________________________________________________________
input_2 (InputLayer)            [(None, 2)]          0 
_____________________________________________________________________________________
dense (Dense)                   (None, 16)           80          input_1[0][0]  
_____________________________________________________________________________________
dense_1 (Dense)                 (None, 16)           48          input_2[0][0]  
_____________________________________________________________________________________
concatenate (Concatenate)       (None, 32)           0           dense[0][0]        
                                                                 dense_1[0][0]      
_____________________________________________________________________________________
dense_2 (Dense)                 (None, 16)           528         concatenate[0][0]  
_____________________________________________________________________________________
dense_3 (Dense)                 (None, 3)            51          dense_2[0][0] 
=====================================================================================
Total params: 707
Trainable params: 707
Non-trainable params: 0
_____________________________________________________________________________________

对于这个摘要表示,你觉得怎么样?你能从这个摘要中推断出它是什么样的模型吗?很遗憾,不能。虽然我们的模型有并行层,但是摘要看起来似乎我们有一系列按顺序处理输入和输出的层。我们有没有办法获得比这更好的表示呢?是的,我们有!

Keras 还提供了以网络图的形式可视化模型的能力。您可以使用下面的代码实现:

tf.keras.utils.plot_model(model)

如果您在 Jupyter 笔记本上运行此命令,您将获得以下图形的内联输出(图 3.4)。现在我们的模型的运行情况更加清晰了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.4 使用函数式 API 创建的模型示例。可以在顶部看到并行的输入层和隐藏层。最终的输出层位于底部。

如果您需要将此图保存到文件中,只需执行以下操作:

tf.keras.utils.plot_model(model, to_file='model.png’)

如果您需要在层的名称和类型之外查看输入/输出大小,可以通过将 show_shapes 参数设置为 True 来实现

tf.keras.utils.plot_model(model, show_shapes=True)

这将返回图 3.5。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.5 使用 show_shapes=True 绘制的 Keras 模型图

请记住我们有两个输入,原始特征(x)和 x 的前两个主成分(我们称之为 x_pca)。您可以如下计算前两个主成分(使用 scikit-learn 库):

from sklearn.decomposition import PCA

pca_model = PCA(n_components=2, random_state=4321)

x_pca = pca_model.fit_transform(x)

PCA 已经在 scikit-learn 中实现。你定义一个 PCA 对象,并将值 2 传递给 n_components 参数。你也固定了随机种子,以确保在各个试验中保持一致性。然后你可以调用 fit_transform(x) 方法来获得最终的 PCA 特征。你可以像之前一样训练这个模型,调用

model.fit([x, x_pca], y, batch_size=64, epochs=10)

遗憾的是,你不会看到很大的准确率提升。结果将与您之前达到的相当。在给定的代码示例中,使用这个模型时你会有大约 6% 的准确率提升。然而,你会发现,如果增加迭代次数,这个差距会变得越来越小。这主要是因为添加 PCA 特征并没有真正增加多少价值。我们将四个维度减少到两个,这不太可能产生比我们已经拥有的更好的特征。让我们在下一个练习中试试运气。

3.1.4 子类化 API

回到研究实验室,看到添加主成分并没有改善结果有点令人沮丧。然而,团队对于你对于在给定模型中使用哪个 API 的了解却印象深刻。一位团队成员建议了一个最终模型。当前,密集层是通过以下方式计算其输出的

h = α(xW + b)

其中 α 是某种非线性。你想看看是否通过添加另一个偏差(即,除了加性偏差外,我们添加了一个乘法偏差)可以改善结果,使得方程变为

h = α([xW + b] × b[mul])

这就是层子类化会拯救一切的地方,因为在 Keras 中没有预先构建的层能够提供这种功能。Keras 提供的最终 API 是子类化 API(见图 3.6),它将允许我们将所需的计算定义为一个计算单元(即,一个层),并在定义模型时轻松重用它。子类化来自软件工程概念中的继承。其思想是你有一个提供某种对象一般功能的超类(例如,一个 Layer 类),然后你从该层中派生(或继承),创建一个更具体的层,实现特定功能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.6 子类化 API 与其他 API 的比较(已灰显)

子类化 API 与顺序 API 和函数 API 有着截然不同的风格。在这里,你正在创建一个 Python 类,该类定义了层或模型的基本操作。在本书中,我们将专注于子类化层(即不包括模型)。在我看来,更多的情况下你会对层进行子类化而不是模型,因为层的子类化更加方便,可能在你只有一个模型或多个模型的情况下需要。然而,只有当你创建由许多较小模型组成的更大的复合模型时,才需要模型的子类化。值得注意的是,一旦你学会了层的子类化,扩展到模型的子类化相对容易。

当子类化层时,有三个重要的函数需要从你继承的 Layer 基类中重写:

  • init() — 使用任何它接受的参数初始化层

  • build() — 模型的参数将在这里创建

  • call() — 定义了正向传播期间需要进行的计算

这是你会写的新层。我们将适当地称呼我们的自定义层为 MulBiasDense。注意这一层是如何继承自位于 tensorflow.keras.layers 子模块中的基础层 Layer 的。

列表 3.3 使用 Keras 子类化新层

from tensorflow.keras import layers

class MulBiasDense(layers.Layer):

    def __init__(self, units=32, input_dim=32, activation=None):super(MulBiasDense, self).__init__()                                  ❶
        self.units = units                                                    ❶
        self.activation = activation                                          ❶

    def build(self, input_shape):                                             ❷
        self.w = self.add_weight(shape=(input_shape[-1], self.units),         ❷
                                 initializer='glorot_uniform', trainable=True)❷
        self.b = self.add_weight(shape=(self.units,),                         ❷
                                 initializer='glorot_uniform', trainable=True)❷
        self.b_mul = self.add_weight(shape=(self.units,),                     ❷
                                 initializer='glorot_uniform', trainable=True)def call(self, inputs):                                                   ❸
        out = (tf.matmul(inputs, self.w) + self.b) * self.b_mul               ❸
        return layers.Activation(self.activation)(out)

❶ 定义了定义层所需的各种超参数

❷ 将层中的所有参数定义为 tf.Variable 对象。self.b_mul 代表了乘法偏置。

❸ 定义了在向层馈送数据时需要进行的计算

首先,我们有 init() 函数。层有两个参数:隐藏单元的数量和激活类型。激活默认为 None,意味着如果未指定,则没有非线性激活(即仅进行线性转换):

def __init__(self, units=32, activation=None):
    super(MulBiasDense, self).__init__()
    self.units = units
    self.activation = activation

接下来,我们实现 build() 函数,这是子类化中的一个重要的拼图。所有参数(例如权重和偏置)都是在这个函数内创建的:

def build(self, input_shape):
    self.w = self.add_weight(shape=(input_shape[-1], self.units),
                             initializer='glorot_uniform', trainable=True)
    self.b = self.add_weight(shape=(self.units,),
                             initializer='glorot_uniform', trainable=True)
    self.b_mul = self.add_weight(shape=(self.units,),
                                 initializer='glorot_uniform', trainable=True)

在这里,参数 w、b 和 b_mul 分别指代方程中的 Wbb[mul]。对于每个参数,我们提供了形状、初始化器和一个布尔值以指示可训练性。此处使用的初始化器 ‘glorot_uniform’(mng.bz/N6A7)是一种流行的神经网络初始化器。最后,我们需要编写 call() 函数,它定义了输入将如何转换为输出:

def call(self, inputs):
    out = (tf.matmul(inputs, self.w) + self.b) * self.b_mul
    return layers.Activation(self.activation)(out)

就是这样:我们的第一个子类化层。值得注意的是,在子类化层时,你需要了解的其他几个函数还有:

  • compute_output_shape() — 通常,Keras 会自动推断出层的输出形状。但是,如果你进行了太多复杂的转换,Keras 可能会迷失方向,你将需要使用这个函数明确地定义输出形状。

  • get_config() - 如果您计划在训练后将模型保存到磁盘,则需要实现此函数,该函数返回由图层使用的参数的字典。

定义了新的层后,可以像以下清单展示的那样使用函数式 API 创建模型。

清单 3.4 使用 Keras 子类化 API 实现的模型 C

from tensorflow.keras.layers import Input, Dense, Concatenate                    ❶
from tensorflow.keras.models import Model                                        ❶
import tensorflow.keras.backend as K                                             ❶
import tensorflow as tf                                                          ❶

K.clear_session()                                                                ❷

inp = Input(shape=(4,))                                                          ❸
out = MulBiasDense(units=32, activation='relu')(inp)                             ❹
out = MulBiasDense(units=16, activation='relu')(out)                             ❹
out = Dense(3, activation='softmax')(out)                                        ❺

model = Model(inputs=inp, outputs=out)                                           ❻
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

❶ 导入必要的模块和类

❷ 确保我们清除 TensorFlow 图

❸ 定义输入层

❹ 使用新的子类化层 MulBiasDense 定义两个层

❺ 定义 softmax 输出层

❻ 定义最终模型

❼ 使用损失函数、优化器和准确率作为指标编译模型

不幸的是,在我们的实验中,我们尝试的所有架构改进都没有带来明显的改进。但是,您通过知道针对哪个模型使用哪个 API 使同事们感到印象深刻,使小组能够在提交论文的截止日期前准备好结果。表 3.1 进一步总结了我们讨论的 API 的主要优缺点。

表 3.1 使用不同 Keras APIs 的优缺点

Sequential APIPros使用顺序 API 实现的模型易于理解、简洁。
Cons无法实现具有多输入/输出等复杂架构特性的模型。
Functional APIPros可用于实现具有多输入/输出等复杂架构元素的模型。
Cons开发人员需要手动正确连接各种层并创建模型。
Sub-classing APIPros可以创建不作为标准层提供的自定义层和模型。
Cons需要深入理解 TensorFlow 提供的底层功能。
由于用户定义的性质,可能会导致不稳定性和调试困难。

在下一节中,我们将讨论您可以在 TensorFlow 中导入和摄入数据的不同方式。

练习 1

假设您需要创建一个具有单个输入层和两个输出层的全连接神经网络。您认为哪个 API 最适合这项任务?

3.2 获取 TensorFlow/Keras 模型的数据

到目前为止,我们已经看过了如何使用不同的 Keras APIs 实现各种模型。此时,您应该已经知道何时使用哪种 API(有时甚至知道不该使用哪种 API)来查看模型的架构。接下来,我们将学习如何使用 TensorFlow/Keras 读取数据来训练这些模型。

假设您最近加入了一家初创公司,作为一名数据科学家,正在尝试使用包含机器学习模型的软件来识别花的品种(使用图像)。他们已经有一个可以接受一批图像和一批标签并训练模型的自定义数据管道。然而,这个数据管道相当隐晦且难以维护。您的任务是实现一个易于理解和维护的替代数据管道。这是一个通过使用 TensorFlow 快速原型设计数据管道来给您的老板留下深刻印象的绝佳机会。

除非经过数据训练,否则模型没有任何价值。更多(高质量)的数据意味着更好的性能,因此将数据以可伸缩和高效的方式提供给模型非常重要。现在是时候探索 TensorFlow 的特性,以创建实现此目的的输入管道。有两种流行的获取数据的替代方法:

  • tf.data API

  • Keras 数据生成器

您将要处理的数据集(从 mng.bz/DgVa 下载)包含一个包含文件名和标签的 CSV(逗号分隔值)文件以及一个包含 210 个花卉图像(.png 格式)的集合。

注意 还有第三种方法,那就是使用 Python 包访问流行的机器学习数据集。这个包被称为 tensorflow-datasets。这意味着只有当您想要使用包已支持的数据集时,此方法才有效。

现在是时候伸展一下手指,开始实现数据管道了。

3.2.1 tf.data API

让我们看看输入管道可能是什么样子。例如,用于您的图像分类任务的输入管道可能看起来像图 3.7。首先,从文本文件中读取整数标签(存储为 [文件名、标签] 记录)。接下来,读取与文件名对应的图像并将其调整为固定的高度和宽度。然后,将标签转换为 one-hot 编码表示。One-hot 编码表示将整数转换为由零和一组成的向量。然后,将图像和 one-hot 编码标签压缩在一起,以保持图像与其相应标签之间的正确对应关系。现在,这些数据可以直接馈送到 Keras 模型中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.7 您将使用 tf.data API 开发的输入管道

在我们的数据集中,我们有一组花卉图像和一个包含文件名及其对应标签的 CSV 文件。我们将按照以下步骤创建数据管道:

  • 将 CSV 文件读取为 tf.data.Dataset。

  • 将文件名和标签作为单独的数据集提取出来。

  • 读取与文件名数据集中的文件名对应的图像文件。

  • 解码图像数据并将其转换为 float32 张量。

  • 将图像调整为 64 × 64 像素。

  • 将标签转换为 one-hot 编码向量。

  • 将图像数据集和 one-hot 向量数据集压缩在一起。

  • 将数据集分批为五个样本的批次。

为了将 CSV 文件读取为一个数据集实体,我们将使用方便的 tf.data.experimental.CsvDataset 对象。您可能会发现,实际上,这是一个实验性的对象。这意味着它的测试程度没有 tf.data API 中的其他功能那么多,并且在某些情况下可能会出现问题。但对于我们的小而简单的示例,不会出现任何问题:

import os # Provides various os related functions

data_dir = os.path.join('data','flower_images') + os.path.sep
csv_ds = tf.data.experimental.CsvDataset(
    os.path.join(data_dir,'flower_labels.csv') , record_defaults=("",-1), header=True
)

tf.data.experimental.CsvDataset 对象需要两个强制参数:一个或多个文件名和一个默认记录,如果记录损坏或不可读,将使用默认记录。在我们的案例中,默认记录是一个空文件名(“”)和标签 -1。您可以通过调用 tf.data.Dataset 打印一些记录

for item in csv_ds.take(5):
    print(item)

在这里,take() 是一个函数,它以数字作为参数,并从数据集中返回那么多的记录。这将输出以下内容:

(<tf.Tensor: shape=(), dtype=string, numpy=b'0001.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'0002.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'0003.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=2>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'0004.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'0005.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)

如果你还记得,flower_labels.csv 文件包含两列:文件名和相应的标签。您可以在数据集输出中看到,每个元组都包含两个元素:文件名和标签。接下来,我们将这两列拆分为两个单独的数据集。这可以很容易地通过使用 map() 函数来完成,该函数将一个给定的函数应用于数据集中的所有记录:

fname_ds = csv_ds.map(lambda a,b: a)
label_ds = csv_ds.map(lambda a,b: b)

Lambda 表达式

Lambda 表达式是一个很棒的工具,它使您可以在代码中使用匿名函数。就像普通函数一样,它们接受参数并返回一些输出。例如,以下函数将添加两个给定值(x 和 y):

lambda x, y : x + y

Lambda 表达式是一种很好的写函数的方式,如果它们只被使用一次,因此不需要名称。学会有效使用 lambda 表达式将使您的代码清晰而简洁。

在这里,我们使用简洁的 lambda 表达式告诉 map() 函数我们想要实现什么。现在,我们可以专注于获取图像数据。为了做到这一点,我们将再次使用 map() 函数。但这一次,我们将编写一个单独的函数来定义需要发生的事情:

import tensorflow as tf

def get_image(file_path):

    # loading the image from disk as a byte string
    img = tf.io.read_file(data_dir + file_path)
    # convert the compressed string to a 3D uint8 tensor
    img = tf.image.decode_png(img, channels=3)
    # Use `convert_image_dtype` to convert to floats in the [0,1] range.
    img = tf.image.convert_image_dtype(img, tf.float32)
    # resize the image to the desired size.
    return tf.image.resize(img, [64, 64])

要从文件名中获取图像张量,我们所需要做的就是将该函数应用于 fname_ds 中的所有文件名:

image_ds = fname_ds.map(get_image)

随着图像数据集的读取,让我们将标签数据转换为独热编码向量:

label_ds = label_ds.map(lambda x: tf.one_hot(x, depth=10))

为了训练图像分类器,我们需要两个项目:一个图像和一个标签。我们确实有这两个作为两个单独的数据集。但是,我们需要将它们合并为一个数据集,以确保一致性。例如,如果我们需要对数据进行洗牌,将数据集合并成一个非常重要,以避免不同的随机洗牌状态,这将破坏数据中的图像到标签的对应关系。tf.data.Dataset.zip() 函数让您可以轻松地做到这一点:

data_ds = tf.data.Dataset.zip((image_ds, label_ds))

我们已经做了大量工作。让我们回顾一下:

  • 读取一个包含文件名和标签的 CSV 文件作为 tf.data.Dataset

  • 将文件名(fname_ds)和标签(label_ds)分开为两个单独的数据集

  • 从文件名加载图像作为数据集(images_ds)同时进行一些预处理

  • 将标签转换为独热编码向量

  • 使用 zip() 函数创建了一个组合数据集

让我们花点时间看看我们创建了什么。tf.data.Dataset 的行为类似于普通的 python 迭代器。这意味着你可以使用循环(例如 for/while)轻松地迭代项,也可以使用 next() 等函数获取项。让我们看看如何在 for 循环中迭代数据:

for item in data_ds:
    print(item)

这会返回以下内容:

>>> (<tf.Tensor: shape=(64, 64, 3), dtype=float32, numpy=
array([[[0.05490196, 0.0872549 , 0.0372549 ],
        [0.06764706, 0.09705883, 0.04411765],
        [0.06862745, 0.09901962, 0.04509804],
        ...,
        [0.3362745 , 0.25686276, 0.21274512],
        [0.26568627, 0.18823531, 0.16176471],
        [0.2627451 , 0.18627453, 0.16960786]]], dtype=float32)>, <tf.Tensor: shape=(10,), dtype=float32, numpy=array([1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)>)

如你所见,item 是一个元组,第一个元素是图像张量(大小为 64 × 64 × 3),第二个元素是一个独热编码向量(大小为 10)。还有一些工作要做。首先,让我们对数据集进行洗牌,以确保在馈送给模型之前不引入任何有序数据:

data_ds = data_ds.shuffle(buffer_size= 20)

buffer_size 参数起着重要作用。它在运行时指定了加载到内存中用于洗牌的元素数量。在本例中,输入管道将加载 20 条记录到内存中,并在迭代数据时从中随机抽样。较大的 buffer_size 可以提供更好的随机化,但会增加内存需求。接下来,我们将讨论如何从数据集中创建数据批次。

请记住,我们说过 Keras 在创建模型时,如果指定了 input_shape(Sequential API)或 shape(functional API),会自动添加批次维度。这就是深度网络处理数据的方式:作为数据批次(即,不是单个样本)。因此,在将数据馈送到模型之前进行批处理非常重要。例如,如果使用批次大小为 5,如果迭代之前的数据集,你将得到一个大小为 5 × 64 × 64 × 3 的图像张量和一个大小为 5 × 10 的标签张量。使用 tf.data.Dataset API 对数据进行批处理非常简单:

data_ds = data_ds.batch(5)

你可以使用以下方式打印其中一个元素:

for item in data_ds:
    print(item)
    break

运行这个命令后,你将得到以下结果:

(
    <tf.Tensor: shape=(5, 64, 64, 3), dtype=float32, numpy=
    array(
        [
            [
                [
                    [0.5852941 , 0.5088236 , 0.39411768],
                    [0.5852941 , 0.50980395, 0.4009804 ],
                    [0.5862745 , 0.51176476, 0.40490198],
                    ...,
                    [0.82156867, 0.7294118 , 0.62352943],
                    [0.82745105, 0.74509805, 0.6392157 ],
                    [0.8284314 , 0.75098044, 0.64509803]
                ],  

                [
                    [0.07647059, 0.10784315, 0.05882353],
                    [0.07843138, 0.11078432, 0.05882353],
                    [0.11862746, 0.16078432, 0.0892157 ],
                    ...,
                    [0.17745098, 0.23529413, 0.12450981],
                    [0.2019608 , 0.27549022, 0.14509805],
                    [0.22450982, 0.28921568, 0.16470589]
                ]
            ]
        ], 
        dtype=float32
    )>, 
    <tf.Tensor: shape=(5, 10), dtype=float32, numpy=
    array(
        [
            [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
            [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
            [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
            [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
            [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.]
        ], 
        dtype=float32
    )>
)

这就是本练习的结束。下面的代码展示了最终的代码的样子。

代码清单 3.5 tf.data 用于花朵图像数据集的输入管道

import tensorflow as tf
import os

data_dir = os.path.join('data','flower_images', 'flower_images') + os.path.sep 
csv_ds = tf.data.experimental.CsvDataset(                               ❶
    os.path.join(data_dir,'flower_labels.csv') , ("",-1), header=True)                                                                       ❶
fname_ds = csv_ds.map(lambda a,b: a)                                    ❷
label_ds = csv_ds.map(lambda a,b: b)def get_image(file_path):

    img = tf.io.read_file(data_dir + file_path)
    # convert the compressed string to a 3D uint8 tensor
    img = tf.image.decode_png(img, channels=3)
    # Use `convert_image_dtype` to convert to floats in the [0,1] range.
    img = tf.image.convert_image_dtype(img, tf.float32)
    # resize the image to the desired size.
    return tf.image.resize(img, [64, 64])

image_ds = fname_ds.map(get_image)                                      ❸
label_ds = label_ds.map(lambda x: tf.one_hot(x, depth=10))              ❹
data_ds = tf.data.Dataset.zip((image_ds, label_ds))                     ❺

data_ds = data_ds.shuffle(buffer_size= 20)                              ❻
data_ds = data_ds.batch(5)

❶ 使用 TensorFlow 从 CSV 文件中读取数据。

❷ 将文件名和整数标签分开为两个数据集对象

❸ 从文件名中读取图像

❹ 将整数标签转换为独热编码标签

❺ 将图像和标签合并为一个数据集

❻ 对数据进行洗牌和分批处理,为模型做准备。

注意,你无法使用我们在鸢尾花数据集练习中创建的模型,因为那些是全连接网络。我们需要使用卷积神经网络来处理图像数据。为了让你有所了解,练习笔记本 3.2.Creating_Input_ Pipelines.ipynb 中提供了一个非常简单的卷积神经网络模型。不用担心这里使用的各种层和它们的参数,我们将在下一章详细讨论卷积神经网络。

model = Sequential([
    Conv2D(64,(5,5), activation='relu', input_shape=(64,64,3)),
    Flatten(),
    Dense(10, activation='softmax')
])

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

使用此输入管道,你可以方便地使用适当的模型馈送数据:

model.fit(data_ds, epochs=10)

运行此命令后,你将获得以下结果:

Epoch 1/10
42/42 [==============================] - 1s 24ms/step - loss: 3.1604 - acc: 0.2571
Epoch 2/10
42/42 [==============================] - 1s 14ms/step - loss: 1.4359 - acc: 0.5190
...
Epoch 9/10
42/42 [==============================] - 1s 14ms/step - loss: 0.0126 - acc: 1.0000
Epoch 10/10
42/42 [==============================] - 1s 15ms/step - loss: 0.0019 - acc: 1.0000

在你上任的第一个星期里迅速取得了一些很好的成果,你自豪地走到老板面前展示你所做的工作。他对你建立的流程的清晰性和高效性感到非常印象深刻。然而,你开始思考,我能用 Keras 数据生成器做得更好吗?

练习 2

想象一下你有一个标签数据集叫 labels_ds(即一个整数标签序列),并且有一些值为 -1 的损坏标签。你能写一个 lambda 函数并将其与 tf.Dataset.map() 函数一起使用来删除这些标签吗?

3.2.2 Keras 数据生成器

另一个获取图像数据的途径是使用 Keras 提供的数据生成器。目前,Keras 提供了两个数据生成器:

tf.keras.preprocessing.image.ImageDataGenerator
tf.keras.preprocessing.sequence.TimeSeriesDataGenerator

虽然不像 tf.data API 那样可定制,但这些生成器仍然提供了一种快速简便的方式将数据输入模型。我们来看看如何使用 ImageDataGenerator 将这些数据提供给模型。ImageDataGenerator (mng.bz/lxpB) 有一个非常长的允许参数列表。在这里,我们只关注如何使 ImageDataGenerator 适应我们所拥有的数据。

然后,为了获取数据,Keras ImageDataGenerator 提供了 flow_from_dataframe() 函数。这个函数对我们来说非常理想,因为我们有一个包含文件名和它们关联标签的 CSV 文件,可以表示为一个 pandas DataFrame。让我们从一些变量定义开始:

data_dir = os.path.join('data','flower_images', 'flower_images')

接下来,我们将使用默认参数定义一个 ImageDataGenerator:

img_gen = ImageDataGenerator()

现在我们可以使用 flow_from_dataframe() 函数:

labels_df = pd.read_csv(os.path.join(data_dir, 'flower_labels.csv'), header=0)
gen_iter = img_gen.flow_from_dataframe(
    dataframe=labels_df, 
    directory=data_dir, 
    x_col='file', 
    y_col='label', 
    class_mode='raw', 
    batch_size=5, 
    target_size=(64,64)
)

我们首先加载包含两列的 CSV 文件:file(文件名)和 label(整数标签)。接下来,我们调用 flow_from_dataframe() 函数,同时还有以下重要参数:

  • dataframe—包含标签信息的数据框

  • directory—定位图像的目录

  • x_col—数据框中包含文件名的列的名称

  • y_col—包含标签的列的名称

  • class_mode—标签的性质(由于我们有原始标签,class_mode 设置为原始)

你可以通过运行下面的代码来查看第一个样本是什么样子的

for item in gen_iter:
    print(item)
    break

这将输出

(
    array([[[[ 10.,  11.,  11.],
             [ 51.,  74.,  46.],
             [ 36.,  56.,  32.],
             ...,
             [  4.,   4.,   3.],
             [ 16.,  25.,  11.],
             [ 17.,  18.,  13.]],
            ...

            [[197., 199., 174.],
             [162., 160., 137.],
             [227., 222., 207.],
             ...,
             [ 57.,  58.,  50.],
             [ 33.,  34.,  27.],
             [ 55.,  54.,  43.]]]], dtype=float32
    ), 
    array([5, 6], dtype=int64)
)

再次,使用批量大小为 5,你会看到一个图像批(即大小为 5 × 64 × 64 × 3)和一个 one-hot 编码的标签批(大小为 5 × 6)生成为一个元组。完整的代码如下所示。

图 3.6 Keras ImageDataGenerator 用于花卉图像数据集

from tensorflow.keras.preprocessing.image import ImageDataGenerator           ❶
import os                                                                     ❶
import pandas as pd                                                           ❶

data_dir = os.path.join('data','flower_images', 'flower_images')              ❷

img_gen = ImageDataGenerator()print(os.path.join(data_dir, 'flower_labels.csv'))
labels_df = pd.read_csv(os.path.join(data_dir, 'flower_labels.csv'), header=0)❹

gen_iter = img_gen.flow_from_dataframe(                                       ❺
    dataframe=labels_df, directory=data_dir, x_col='file', y_col='label',     ❺
    class_mode='raw', batch_size=2, target_size=(64,64))

❶ 导入必要的模块

❷ 定义数据目录

❸ 定义 ImageDataGenerator 来处理图像和标签

❹ 通过读取 CSV 文件作为数据框来定义标签

❺ 从数据框中的文件名和标签读取图像和标签

这看起来比之前的流程更好。你仅用三行代码就创建了一个数据流程。你的知识肯定让你的老板印象深刻,你正在走上快速晋升的道路。

我们将在后面的章节详细讨论 ImageDataGenerator 的参数以及它支持的其他数据检索函数。

然而,要记住简洁并不总是好的。通常,简洁意味着你可以通过这种方法实现的功能有限。对于 tf.data API 和 Keras 数据生成器来说也是如此。tf.data API 尽管需要比 Keras 数据生成器更多的工作,但比 Keras 数据生成器更灵活(并且可以提高效率)。

3.2.3 tensorflow-datasets 包

在 TensorFlow 中检索数据的最简单方法是使用 tensorflow-datasets (www.tensorflow.org/datasets/overview) 包。然而,一个关键的限制是 tensorflow-datasets 只支持一组定义好的数据集,而不像 tf.data API 或 Keras 数据生成器可以用于从自定义数据集中获取数据。这是一个单独的包,不是官方 TensorFlow 包的一部分。如果你按照说明设置了 Python 环境,你已经在你的环境中安装了这个包。如果没有,你可以通过执行以下命令轻松安装它:

pip install tensorflow-datasets

在你的虚拟 Python 环境的终端(例如,Anaconda 命令提示符)中执行上述命令。为了确保软件包安装正确,运行以下行在你的 Jupyter 笔记本中,确保没有出现任何错误:

import tensorflow_datasets as tfds

tensorflow-datasets 提供了许多不同类别的数据集。你可以在www.tensorflow.org/datasets/catalog找到一个全面的可用列表。表 3.2 还概述了一些在 tensorflow-datasets 中可用的热门数据集。

表 3.2 tensorflow-datasets 中可用的几个数据集

数据类型数据集名称任务
Audiolibrispeech语音识别
ljspeech语音识别
Imagescaltech101图像分类
cifar10 和 cifar100图像分类
imagenet2012图像分类
Textimdb_reviews情感分析
tiny_shakespeare语言模型
wmt14_translate机器翻译

让我们使用 tensorflow-datasets 来检索 cifar10 数据集,这是一个广泛使用的图像分类数据集,其中包含属于 10 个类别(例如汽车、船、猫、马等)的 32×32 大小的 RGB 图像。首先,让我们确保它作为一个数据集可用。在 Jupyter 笔记本上执行以下操作:

tfds.list_builders()

我们可以看到 cifar10 是其中一个数据集,正如我们所期望的那样。让我们使用 tfds.load()函数加载数据集。当你首次调用这个方法时,TensorFlow 会先下载数据集,然后为你加载它:

data, info = tfds.load("cifar10", with_info=True)

当它成功下载后,查看(info)变量中可用的信息:

print(info)

>>> tfds.core.DatasetInfo(
    name='cifar10',
    version=3.0.0,
    description='The CIFAR-10 dataset consists of 60000 32x32 colour images in 10 classes, with 6000 images per class. There are 50000 training images and 10000 test images.',
    homepage='https:/ /www.cs.toronto.edu/~kriz/cifar.xhtml',
    features=FeaturesDict({
        'image': Image(shape=(32, 32, 3), dtype=tf.uint8),
        'label': ClassLabel(shape=(), dtype=tf.int64, num_classes=10),
    }),
    total_num_examples=60000,
    splits={
        'test': 10000,
        'train': 50000,
    },
    supervised_keys=('image', 'label'),
    citation="""@TECHREPORT{Krizhevsky09learningmultiple,
        author = {Alex Krizhevsky},
        title = {Learning multiple layers of features from tiny images},
        institution = {},
        year = {2009}
    }""",
    redistribution_info=,
)

这非常有信息量。我们现在知道有 60,000 个 32 × 32 的彩色图像属于 10 个类别。数据集分为 50,000(训练)和 10,000(测试)。现在让我们看看数据变量:

print(data)

>>> {'test': <DatasetV1Adapter 
        shapes: {image: (32, 32, 3), label: ()}, 
        types: {image: tf.uint8, label: tf.int64}>, 
     'train': <DatasetV1Adapter 
        shapes: {image: (32, 32, 3), label: ()}, 
        types: {image: tf.uint8, label: tf.int64}>
    }

我们可以看到它是一个包含键“train”和“test”的字典,每个键都有一个 tf.data.Dataset。幸运的是,我们已经学习了 tf.data.Dataset 的工作原理,所以我们可以快速了解如何准备数据。让我们看一下训练数据。你可以通过以下方式访问这个训练数据集。

train_ds = data["train"]

然而,如果你尝试迭代这个数据集,你会注意到数据并没有被分批。换句话说,数据是一次检索一个样本。但是,正如我们已经说过很多次的那样,我们需要批量数据。修复方法很简单:

train_ds = data["train"].batch(16)

现在,为了看一下 train_ds 中的一批数据是什么样子,你可以执行以下操作:

for item in train_ds:
    print(item)
    break

这将输出

{
    'id': <tf.Tensor: shape=(16,), dtype=string, numpy=
          array(
              [
                  b'train_16399', b'train_01680', b'train_47917', b'train_17307',
                  b'train_27051', b'train_48736', b'train_26263', b'train_01456',
                  b'train_19135', b'train_31598', b'train_12970', b'train_04223',
                  b'train_27152', b'train_49635', b'train_04093', b'train_17537'
              ], 
              dtype=object
          )>, 
    'image': <tf.Tensor: shape=(16, 32, 32, 3), dtype=uint8, numpy=
          array(
              [
                  [
                      [
                          [143,  96,  70],
                          [141,  96,  72],
                          [135,  93,  72],
                          ...,         
                          [128,  93,  60],
                          [129,  94,  61],
                          [123,  91,  58]
                      ]
                  ]
              ], 
              dtype=uint8
          )>, 
    'label': <tf.Tensor: shape=(16,), dtype=int64, numpy=
          array(
              [7, 8, 4, 4, 6, 5, 2, 9, 6, 6, 9, 9, 3, 0, 8, 7], 
              dtype=int64
          )>
}

它将是一个包含三个键的字典:id、image 和 label。id 是每个训练记录的唯一标识。image 将有一个大小为 16 × 32 × 32 × 3 的张量,而 label 将有一个大小为 16 的张量(即整数标签)。当将 tf.data.Dataset 传递给 Keras 模型时,模型期望数据集对象产生一个元组 (x,y),其中 x 是一批图像,y 是标签(例如,one-hot 编码)。因此,我们需要编写一个额外的函数,将数据放入正确的格式:

def format_data(x):
    return (x["image"], tf.one_hot(x["label"], depth=10))

train_ds = train_ds.map(format_data)

通过这个简单的转换,你可以将这个数据集馈送给一个模型,方法如下:

model.fit(train_ds, epochs=25)

这是令人惊讶的工作。现在你知道了为模型检索数据的三种不同方法:tf.data API、Keras 数据生成器和 tensorflow-datasets 包。我们将在这里结束对 Keras API 和不同数据导入 API 的讨论。

练习 3

你能写一行代码导入 caltech101 数据集吗?在你这样做之后,探索这个数据集。

摘要

  • Keras,现在已经集成到 TensorFlow 中,提供了几种高级模型构建 API:串行 API、功能 API 和子类化 API。这些 API 有不同的优缺点。

  • 串行 API 是使用最简单的,但只能用于实现简单的模型。

  • 功能和子类化 API 可能难以使用,但允许开发人员实现复杂的模型。

  • TensorFlow 包含几种获取数据的方法:tf.data API、Keras 数据生成器和 tensorflow-datasets。tf.data。

  • API 提供了向模型提供数据的最可定制方式,但需要更多的工作来获取数据。

  • tensorflow-datasets 是使用最简单的,但是它有限,因为它只支持有限的数据集。

练习答案

练习 1: 功能 API。由于有两个输出层,我们不能使用串行 API。没有必要使用子类化 API,因为我们需要的一切都可以使用 Keras 层完成。

练习 2: labels_ds.map(lambda x: x if x != -1). 你也可以使用 tf.Dataset .filter() 方法(即 labels_ds.filter(lambda x: x != -1))。

练习 3: tfds.load(“caltech101”, with_info=True)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值