TensorFlow-机器学习-全-
TensorFlow 机器学习(全)
原文:machine learning ten sorflow
译者:飞龙
前言
前言
自第一版《TensorFlow 机器学习》出版以来已有两年。在人工智能领域,两年是一段很长的时间。
现在,我们对一个拥有超过 800 亿个人工神经元、学习超过 1700 亿个参数的人类语言模型感到着迷。训练这样一个模型的成本以百万美元计。麻省理工学院的 Lex Fridman 预测,随着计算和算法设计的改进,我们很快就能以不到几千美元的成本训练出与人类大脑大小相当的模型。想想看——在我们不久的将来,我们将以不到一辆 Peloton 固定自行车的成本来训练一个具有人类大脑原始容量的 AI 模型。
撰写一本捕捉这个快速发展的技术的书充满了风险。当克里斯写了几章时,研究人员可能已经产生了更新、更优雅的解决同样问题的方法。然而,今天可能只有大约 1 万人真正深刻理解人工智能。你想跳进来,学习,并开始在工作中使用人工智能。该怎么办呢?
购买这本书——即使你有第一版。特别关注以下七个新章节,它们将引导你了解人工智能的基本技术:
-
第六章,“情感分类:大型电影评论数据集”
-
第八章,“从 Android 加速度计数据推断用户活动”
-
第十章,“词性标注和词义消歧”
-
第十二章,“应用自编码器:CIFAR-10 图像数据集”
-
第十五章,“构建真实世界的 CNN:VGG-Face 和 VGG-Face Lite”
-
第十七章,“LSTMs 和自动语音识别”
-
第十八章,“用于聊天机器人的序列到序列模型”
克里斯帮助你学习机器在我们的世界中如何看、听、说、写和感受。他展示了机器如何像人眼一样,利用自编码器瞬间发现挡风玻璃上的灰尘。
克里斯用令人沮丧的、实用的细节描述建模技术,这些技术将随着时间的推移而持续存在。它们对于将问题作为张量输入、张量输出、通过图流动是基本的。正确地构建问题比描述解决问题的个别细节更为重要。预期这些细节会迅速变化和改进。
带着对 AI 建模的欣赏,你将准备好享受人工智能快速、指数级的前进旅程。欢迎来到我们的世界!跳进来,享受乐趣,开启那些 GPU,并尽你所能帮助人类解决智能问题。用智能机器重新构想我们的世界——然后用 TensorFlow 实现它。
克里斯,感谢你抽出时间做我们的向导,穿插着那种我非常喜欢的糟糕的父亲幽默。
Scott Penberthy,谷歌应用 AI 总监
旧金山
加利福尼亚州,2020 年 8 月
前言
大约 15 个月前,就在今天,我坐下来,手拿这本书的第一版全新副本,打开它,一头扎了进去。我现在在位于美丽加利福尼亚州帕萨迪纳的 NASA 喷气推进实验室管理人工智能、分析和创新发展部门。然而,当时我是 IT 部门的副首席技术官(CTO),在数据科学、信息检索和软件方面有深厚的背景,但对热门话题机器学习只有表面的了解。我尝试过它,但从未深入钻研,就像人们说的那样。鉴于 Manning 对实用性、深入示例以及最重要的是幽默(我在一切事物中都迫切寻求它;幽默使事物变得更好)的覆盖,我对这本书抱有好感。当时,自从我有时间阅读技术书籍以来,几乎已经过去了一年,更不用说坐下来尝试代码和练习了。
我决定,有了这本书,我必须运行代码,拿出铅笔和纸,绘制矩阵,并写下东西——你知道的,学习我正在阅读的内容,而不是阅读但不学习。哇,这本书真是个大挑战。它很有趣——可能是我读过的最简单的机器学习入门书籍——而且我确实理解了它。我记得有一天晚上对妻子说,“这就是为什么所有亿万富翁 CEO,比如[埃隆]马斯克,都害怕 AI 的原因。”我可以看到它在各种格式中的应用,如文本、声音、视觉和语音。它使用了一个我听说很多的令人惊叹的框架,叫做 TensorFlow。
然而,有一个问题。这本书的第一版有一个习惯,那就是在每章的结尾抛出一个项目符号——大致意思是“好吧,你刚刚覆盖了 AI 或 ML 主题 X;你可以尝试构建一个像这个最先进的 X 模型一样为 X 构建模型并测试它。”我很好奇,愿意投入时间。大约九周后,我训练并重建了视觉几何组(VGG)的标志性面部模型,并发现了一大堆改进和重建了不再存在的数据集。我编写了代码,从 Android 手机数据中推断用户的活动,如跑步或行走。我构建了一个强大的情感分类器,在 Kaggle 的爆米花电影挑战赛(现已关闭)中可能会获得前 100 名结果。
最终,我为这本书的第二版积累了足够的代码、笔记和材料。我收集了数据,编写了 Jupyter 笔记本并对其进行了文档记录,修复了代码中的错误;甚至在 Nishant Shukla 这本书的第一版和这一版之间两年时间里,TensorFlow 在 1.x 分支上发布了大约 20 个版本,2.x 版本也即将推出(在我写这本书的时候已经推出了)。
所有代码、示例、错误修复、数据下载、辅助软件库安装和 Docker 化都是这本书中你得到的内容。不要把它想成又一本*TensorFlow 书;我本可以轻易地称之为《使用 TensorFlow 和朋友的机器学习,第 2 版:NumPy、SciPy、Matplotlib、Jupyter、Pandas、Tika、SKLearn、TQDM 以及更多》。你需要所有这些元素来做数据科学和机器学习。为了清楚起见,这本书不仅仅关于 TensorFlow;这是一本关于机器学习以及如何做的书:如何清理数据、构建模型并训练它,以及(最重要的是)如何评估它。
我希望你现在和将来都像我一样喜欢机器学习。这段旅程并不容易,包括在写作时被全球大流行病所困扰,但我从未如此乐观,因为我看到了人工智能和机器学习的光明和力量。我希望在阅读这本书后,你也会这样。
致谢
如果没有感谢尼尚特·舒克拉,我将感到失职,他写了这本书的第一版。他聪明、风趣的讨论激发了我踏上这段旅程,最终使我创作了这本书。
我衷心感谢我的收购编辑迈克尔·斯蒂普斯,他相信我的书稿,并坚持我的坚持、热情和愿景。这本书因你的强烈反馈和批评而变得更好。感谢 Manning 出版社的出版商 Marjan Bace,他批准了这本书的想法,并在他第一本书 10 年后给了这位作者另一次机会。
我的开发编辑托尼·阿里托拉一直是我最大的支持者和确保这本书成功的最坚定的倡导者。她对我的信任、对愿景和过程的信念,以及我们之间的信任,使这本书变得非常出色。托尼对我的 AI 和数据科学术语的挑战以及她将我的 50000 英尺概念编辑和重新构思为实用解决方案和解决问题的能力,使无论你对编码、AI 还是 ML 的了解如何,使用 TensorFlow 进行机器学习都成为一种可口的享受。感谢她的冷静、智慧和心。
感谢我的技术发展编辑阿尔·克林克,他的技术编辑和建议无疑提高了这本书的质量。
向所有审稿人致谢:Alain Couniot、Alain Lompo、Ariel Gamino、Bhagvan Kommadi、David Jacobs、Dinesh Ghanta、Edward Hartley、Eriks Zelenka、Francisco José Lacueva、Hilde Van Gysel、Jeon Kang、Johnny L. Hopkins、Ken W. Alger、Lawrence Nderu、Marius Kreis、Michael Bright、Teresa Fontanella de Santis、Vishwesh Ravi Shrimali 和 Vittal Damaraju——你们的建议帮助使这本书变得更好。此外,我还要感谢那些提供了宝贵反馈和建议的匿名审稿人,他们鼓励我努力追求更好的安装,这导致了仓库中的全面 Docker 安装和分支,以及代码的整体更好组织。
我要感谢 Candace Gillhoolley 组织了数十次播客、推广和联系活动,以推广这本书并传播信息。
感谢我的行业同事和队友抽出他们自己的时间阅读这本书的早期草稿章节,并提供了宝贵的反馈。我特别想感谢 Philip Southam 对我的信任,以及他在 Docker 安装方面的早期工作,还有 Rob Royce 在 TensorFlow2 分支上的工作和对代码的兴趣。我还深深地感谢赵张在 CNN 章节想法上的帮助,以及 Thamme Gowda 提供的指导和讨论。
最后,我要感谢我惊人的妻子,Lisa Mattmann,在将近十年后,她让我再次做我承诺过不会再做的事情(在我上一本书和我的博士论文之前)。我在远离写作方面有着糟糕的记录,但这一次不同,在将近 20 年的相处中,她了解我,知道写作是我的激情。谢谢你,亲爱的。
我将这本书献给我的孩子们。我的大儿子,Christian John (CJ) Mattmann,对情感分析章节和文本处理表现出兴趣。他真是继承了家族的基因。我希望有一天,他会有勇气运行代码,并执行他自己的甚至更好的情感分析和机器学习。我怀疑他会这样做。感谢 Heath 和 Hailey Mattmann 理解当爸爸深夜熬夜完成这本书和编写章节以及编码时的情况。这本书献给你们!
关于这本书
为了从这本书中获得最大益处,您应该将其视为两部分:一部分是数学和理论的基础,随后是使用 Python、TensorFlow 及其相关工具的实际应用。当我特别提到在特定领域使用机器学习技术,如回归或分类,并引用示例数据集或问题时,请思考您如何利用这些数据和/或问题领域来测试您正在使用的新的机器学习技术。这正是我在第一版读者时的做法,仔细研读章节,然后应用“如果...会怎样”的想法和指向数据集的指针,基于我制作的笔记本和我所做的工作,为这一版创建了新的章节。
整个过程需要花费每个章节数周或数月的时间来完成。我在第二版中记录了所有更新。随着您阅读这本书的前几部分,您会发现章节的顺序与第一版相似。然而,在回归之后,现在有一个章节专门介绍将回归应用于第一版建议的 311 个服务练习。同样,第二版还有一个完整的章节介绍如何使用分类对 Netflix 电影评论数据执行情感分析。
在本书的其余部分,你将探索包括无监督聚类、隐藏马尔可夫模型(HMMs)、自动编码器、深度强化学习、卷积神经网络(CNNs)和 CNN 分类器等主题。我还增加了一章,介绍如何从 Android 手机中提取位置数据,并推断用户正在进行的活动类型,以及一章关于重新创建 VGG-Face 面部识别 CNN 模型的内容。为了执行一些后续练习,你可能需要访问 GPU,无论是本地在您的笔记本电脑上,还是通过 Google、Amazon 或其他大型提供商的云访问。我会在这个过程中帮助你。
请确保在 liveBook 讨论论坛(livebook.manning.com/book/machine-learning-with-tensorflow-second-edition/discussion)中发布你关于本书的任何问题、评论或建议。你的反馈对于保持本书的时效性和确保它是最好的书籍至关重要。我期待着在您的机器学习之旅中帮助你!
本书是如何组织的:一个路线图
本书分为三个部分。
第一部分,“你的机器学习配置”,解释了机器学习的一般理论,并给出了一些关于其在当今世界大规模增长和使用的动机,讨论基于最广泛使用的机器学习实现框架之一:TensorFlow。
-
第一章介绍了机器学习,并解释了它是如何教授计算机根据输入图像、文本、声音和其他格式进行分类、预测、聚合和识别的。
-
第二章涵盖了 TensorFlow 基础知识,并向读者介绍了 TensorFlow 框架;张量的概念;基于图的执行;以及创建、训练和保存模型的过程。
第二部分,“核心学习算法”,为你提供了机器学习工具箱:回归用于学习连续值预测或分类用于离散分类预测和推理。本部分中的章节是配对的;一章专注于工具和一般理论,下一章则提供了一个涉及数据清洗、准备、训练、推理和评估的详细示例问题。教授的技术包括回归、分类、无监督聚类和 HMMs。所有这些技术都是可解释的,即你可以解释机器学习过程的步骤,并直接使用数学和统计学来评估它们的值。
-
第三章涵盖了回归,这是一个涉及连续输入和可能离散或连续输出的建模问题。
-
第四章将回归应用于来自纽约市 311 服务机构的真实世界呼叫中心数据,该机构为市民提供帮助。你将收集每周呼叫量的数据集,并使用回归来预测每周预期的呼叫数量。
-
第五章涵盖了分类,这是一个建模问题,它接受离散或连续数据作为输入,并输出单个或多个分类类别标签。
-
第六章使用 Netflix 和 IMDb 电影评论数据对电影进行分类,构建了一个基于评论识别电影为正面或负面的电影情感分类器。
-
第七章演示了无监督聚类,展示了在没有标签的情况下自动将输入数据分组到离散类别中。
-
第八章将自动聚类应用于输入的 Android 手机位置数据,向你展示如何根据手机加速度计的位置数据推断用户活动。
-
第九章使你轻松进入隐马尔可夫模型(HMMs)的主题,并展示了间接证据如何导致可解释的决策。
-
第十章将隐马尔可夫模型(HMMs)应用于文本输入,以在难以判断 engineer 是名词还是动词时,对文本中的词性进行区分。(什么时候不是这样呢,对吧?)
书的最后一部分涵盖了正在席卷社区的神经网络范式:帮助汽车自动驾驶、医生诊断癌症以及手机使用生物识别技术(如你的面部)来决定你是否可以登录。神经网络是一种受人类大脑及其结构启发的特定机器学习模型,其结构是一个基于输入、发出预测、置信度、信念、结构和形状的神经元图。神经元很好地映射到张量的概念,张量是图中允许标量值、矩阵和向量等信息通过它们流动、被操作、转换等的节点——因此,谷歌框架的名称为 TensorFlow。本书的这一部分涵盖了用于压缩和表示输入的自动编码器、用于自动分类图像中对象和面孔的卷积神经网络(CNNs)以及用于时间序列数据或转换为文本的语音数据的循环神经网络(RNNs)。第三部分还涵盖了 seq2seq RNN 架构,它可以用来将输入文本和语句与智能数字助手(如聊天机器人)的响应关联起来。本书的最后一章将神经网络应用于根据输入视频和图像评估机器人折叠布料的效用。
-
第十一章涵盖了自动编码器,它们通过使用神经网络中的隐藏层将输入数据压缩成更小的表示。
-
第十二章探讨了多种类型的自动编码器,包括堆叠和去噪自动编码器,并演示了网络如何从 CIFAR-10 数据集中学习图像的紧凑表示。
-
第十三章向读者介绍了一种不同类型的网络:深度强化学习网络,它学习投资股票组合的最佳策略。
-
第十四章全部关于卷积神经网络(CNN),这是一种受视觉皮层启发的神经网络架构。CNN 使用多个卷积滤波器来开发输入图像及其更高和更低阶特征的紧凑表示。
-
第十五章教你如何构建两个真实世界的 CNN:一个用于 CIFAR-10 数据集的对象识别,另一个用于名为 VGG-Face 的面部识别系统。
-
第十六章涵盖了时间序列数据的 RNN 范式,并代表了神经网络随时间做出的决策,而不仅仅是特定实例中的决策。
-
第十七章向你展示了如何构建一个名为长短期记忆(LSTM)的真实世界 RNN 模型类型,用于自动语音到文本识别,重建了百度使其闻名的深度语音模型架构。
-
第十八章重用 RNN 并演示了 seq2seq 架构,它可以用来构建一个智能聊天机器人,该机器人可以针对用户聊天进行现实反应,这些反应是在之前的问题和答案上训练的。
-
第十九章通过探索效用景观来结束本书,使用神经网络架构从布折叠的视频中创建图像嵌入,然后使用这些嵌入来推断任务每个步骤随时间的效用。
关于代码
本书包含许多源代码示例,无论是编号列表还是与普通文本内联。在这两种情况下,源代码都使用固定宽度字体如这样来格式化,以将其与普通文本区分开来。有时代码也会加粗以突出显示与章节中先前步骤相比有所改变的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已经被重新格式化;我们添加了换行并重新调整了缩进,以适应书籍中可用的页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中移除。许多列表旁边都有代码注释,突出显示重要概念。
本书中的许多图形都包含颜色,可以在电子书版本中查看。要获取免费的电子书(PDF、ePub 或 Kindle 格式),请访问mng.bz/JxPo注册您的印刷版书籍。
书中的代码按章节组织,作为一系列 Jupyter 笔记本。你可以从 Docker Hub 拉取或自己构建相关的 Docker 容器,它会自动安装 Python 3 和 Python 2.7 以及 TensorFlow 1.15 和 1.14,这样你就可以运行书中的所有示例。书中的列表清晰划分并编号;它们对应于 GitHub 仓库中mng.bz/MoJn的章节和列表编号.ipynb 文件,以及 Manning 网站www.manning.com/books/machine-learning-with-tensorflow-second-edition。
Docker 文件会自动从远程 Dropbox 链接下载并安装第三方库(TensorFlow 及其相关库的部分,如书中所述)和必要的数据集,以便您运行所有代码。如果您在自己的 Python 环境中本地安装了库和数据,也可以在 Docker 容器外运行库和数据的下载脚本。
作者将乐意接收在 GitHub 上报告的代码问题,甚至更乐意接收您发现的任何问题的拉取请求。还有一项积极的工作正在将本书中的列表迁移到 TensorFlow2。您可以在 tensorflow2 分支中找到当前的工作 github.com/chrismattmann/MLwithTensorFlow2ed/tree/tensorflow2。
liveBook 讨论论坛
购买《TensorFlow 机器学习》包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在论坛中就本书发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问 livebook.manning.com/book/machine-learning-with-tensorflow-second-edition/discussion。您还可以在 livebook.manning.com/#!/discussion 了解更多关于 Manning 论坛和行为准则的信息。
Manning 对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未支付报酬)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要本书仍在印刷中,论坛和以前讨论的存档将可通过出版社的网站访问。
关于作者
Chris Mattmann 是美国宇航局喷气推进实验室人工智能、分析和创新发展组织部门的部门经理,在那里他被认定为 JPL 数据科学领域的首位首席科学家。Chris 将 TensorFlow 应用于他在 NASA 面临的挑战,包括使用 TensorFlow 构建谷歌 Show & Tell 算法的图像标题实现。作为 Apache 软件基金会的前任总监,Chris 为开源项目做出了贡献,并在南加州大学教授内容检测和分析、搜索引擎和信息检索的硕士研究生课程。
关于封面插图
《TensorFlow 机器学习》的封面上的插图被标注为“来自克罗地亚达尔马提亚的帕格岛的人”。这幅插图取自 2006 年出版的 19 世纪服装和民族志描述的复制品,名为《达尔马提亚》,由考古学家和历史学家弗拉内·卡拉拉教授(1812-1854)所著,他是克罗地亚斯普利特古物博物馆的第一任馆长。插图是从斯普利特中世纪中心的罗马核心,即公元 304 年左右皇帝戴克里先退休宫殿的废墟中,位于民族志博物馆(原古物博物馆)的一位 helpful librarian 那里获得的。书中包含了来自达尔马提亚不同地区的精美彩色插图,并附有服装和日常生活的描述。自 19 世纪以来,着装规范已经改变,当时丰富的地区多样性已经消失。现在很难区分不同大陆的居民,更不用说不同的城镇或地区了。也许我们用更丰富多彩的个人生活——当然,是更丰富多彩、节奏更快的技术生活——来换取了文化多样性。
在难以区分一本计算机书籍与另一本的时候,曼宁通过书籍封面上的设计,庆祝了计算机行业的创新精神和主动性。这些设计基于两百年前丰富多样的地区生活,通过此类收藏中的插图被重新呈现出来。
第一部分 你的机器学习工具
机器学习的第一规则是看到它在日常生活中的应用。从根据特征而不是图片来决定哪辆车更好,到根据图片判断机器人是否正确折叠衣物,再到通过模拟大脑的听觉功能将声波转换为大脑可以理解的文字表示来学习听,机器学习无处不在!
要进行机器学习,你需要数据——很多时候,需要大量的数据,但并非总是如此。这些数据通常没有以正确的方式准备,需要清理。你还需要一些关于这些数据的假设或假设,你希望对其进行测试和评估。说到这里,你还需要工具来评估你的机器学习在预测、分组、排名和评分方面的表现如何。所有这些组件都是你机器学习工具的一部分,你将使用它以有意义的方式接收输入数据并回答关于这些数据的问题,这样你可以进行评估。
书的第一部分专注于机器学习工具的组件,展示了如何使用 Google 的 TensorFlow 框架,以及 Python 编程语言中一系列相关的机器学习工具和实用程序,帮助你应用和使用机器学习工具来解决书中剩余部分将要探讨的现实世界问题。
1 机器学习之旅
本章涵盖
-
机器学习基础
-
数据表示、特征和向量范数
-
为什么选择 TensorFlow?
你是否曾想过计算机程序能解决的问题是否有极限?如今,计算机似乎能做很多事情,不仅仅是解开数学方程。在过去半个世纪里,编程已经成为自动化任务和节省时间的终极工具。但我们能自动化多少,我们如何去做?
计算机能否观察一张照片并说,“啊哈——我看到一对可爱的情侣在雨中走过一座桥下”?软件能否像训练有素的专家一样准确做出医疗决策?软件能否比人类做出更好的关于股市表现的预测?过去十年的成就暗示,所有这些问题的答案都是响亮的“是”,并且这些实现似乎有一个共同的策略。
近期的理论进步加上新技术的可用性,使得任何能够访问计算机的人都可以尝试自己解决这些极其困难的问题。(好吧,不仅仅是任何人,这就是你为什么在阅读这本书,对吧?)
程序员不再需要了解问题的复杂细节就能解决问题。考虑将语音转换为文本。传统的方法可能涉及理解人类声带的生物结构,通过许多手设计的、特定领域的、不可通用的代码片段来解码语音。如今,人们可以编写代码,通过观察许多示例,并在足够的时间和示例的帮助下找出解决问题的方法。
再举一个例子:识别一本书或推文中文本的情感是正面还是负面。或者你可能想更细致地识别文本,比如暗示作者喜欢或热爱的事物,他们讨厌的事物,或者他们愤怒或悲伤的事物。过去执行这项任务的方法仅限于扫描相关的文本,寻找像丑陋、愚蠢和悲惨这样的严厉词汇来表示愤怒或悲伤,或者像感叹号这样的标点符号,这可能意味着快乐或愤怒,但并不完全介于两者之间。
算法从数据中学习,类似于人类从经验中学习的方式。人类通过阅读书籍、观察情况、在学校学习、交流对话和浏览网站等多种方式学习。一台机器怎么可能发展出具有学习能力的“大脑”?这没有确切的答案,但世界级的学者们从不同的角度开发了智能程序。在这些实现中,学者们注意到了解决这类问题时的一些重复出现的模式,这导致了今天我们称之为机器学习(ML)的标准化领域。
随着机器学习研究的成熟,执行机器学习的工具变得更加标准化、稳健、高性能和可扩展。这就是 TensorFlow 的作用所在。这个软件库具有直观的界面,让程序员能够深入使用复杂的 ML 概念。
跟随版本更新:TensorFlow 2 及以上版本
本书基于 TensorFlow 1.x 系列的两个版本进行标准化。版本 1.15,是 1.x 系列中的最新发布版本,与 Python 3 兼容良好。在第七章和第十九章中,你会读到一些需要 Python 2 的例子;因此,需要 TensorFlow 1.14。
此外,在本书开发期间,还发布了针对 TensorFlow 2 的完整代码列表和代码的移植。(详情见附录。)你会发现,85-90% 的在 TensorFlow 2 中运行的代码列表代码是相同的。主要原因在于数据清洗、收集、准备和评估代码完全可重用,因为它使用了伴随的 ML 库,如 Scikit 和 Matplotlib。
TensorFlow 2 版本的代码列表包含了新特性,包括始终启用急切执行和优化器及训练更新的包名。新的代码列表在 Python 3 中运行良好;如果你尝试了它们,欢迎你对它们提出反馈。你可以在 github.com/chrismattmann/MLwithTensorFlow2ed/tree/master/TFv2 找到 TensorFlow 2 的代码列表。
第二章介绍了这个库的方方面面,之后的每一章都解释了如何使用 TensorFlow 进行各种机器学习应用。
1.1 机器学习基础
你有没有尝试向某人解释如何游泳?描述节奏性的关节运动和流体模式在复杂性上令人难以置信。同样,一些软件问题对我们来说过于复杂,难以轻易理解。对于这个任务,机器学习可能是我们使用的工具。
全速前进!
机器学习是一项相对较新的技术,所以想象一下,你是欧几里得时代的几何学家,正在开辟一个新领域的道路。或者,设想一下,你是牛顿时代的物理学家,可能正在思考机器学习领域的广义相对论等价物。
精心手工调整算法以完成任务曾经是构建软件的唯一方式。从简单观点来看,传统编程假设每个输入都有一个确定性的输出。另一方面,机器学习可以解决一类输入输出对应关系不明确的难题。
机器学习的特点是软件能够从以往的经验中学习。这样的计算机程序随着更多例子的可用而提高性能。希望如果你向这个机器投入足够的数据,它将学会模式并为新输入产生智能结果。
信任和解释机器学习输出
模式检测不再是人类独有的特性。计算机时钟速度和内存的爆炸性增长导致了一种异常情况:现在可以使用计算机进行预测、捕捉异常、排序项目和自动标记图像。这一套新工具为不明确的问题提供了智能答案,但代价是信任的微妙损失。你会信任一个计算机算法提供诸如是否进行心脏手术等至关重要的医疗建议吗?更重要的是,它会解释为什么给你提供了这样重要的医疗建议吗?
没有平庸的机器学习解决方案的余地。人类的信任太脆弱了,我们的算法必须能够抵御怀疑。请仔细跟随本章的指导。
机器学习的另一个名称是归纳学习,因为代码试图仅从数据中推断结构。这个过程就像在外国度假时阅读当地时尚杂志来了解如何着装一样。你可以从穿着当地服装的人的图片中发展出对文化的看法。你是在归纳性地学习。
你在编程时可能从未使用过这种方法,因为归纳学习并不总是必要的。考虑一下确定两个任意数之和是偶数还是奇数的任务。当然,你可以想象训练一个机器学习算法,使用数百万个训练示例(如图 1.1 所示),但你当然知道这种方法会过度。一个更直接的方法可以轻松地完成这个任务。

图 1.1 每对整数相加的结果是偶数或奇数。列出的输入和输出对应关系称为真实数据集。
两个奇数之和总是偶数。请你自己验证一下:取任意两个奇数,将它们相加,检查和是否为偶数。以下是直接证明这一事实的方法:
-
对于任何整数n,公式 2n + 1 会产生一个奇数。此外,任何奇数都可以写成 2n + 1 的形式,其中n是某个值。数字 3 可以写成 2(1) + 1。数字 5 可以写成 2(2) + 1。
-
假设我们有两个奇数,2n + 1 和 2m + 1,其中n和m是整数。两个奇数相加得到(2n + 1) + (2m + 1) = 2n + 2m + 2 = 2(n + m + 1)。这个数是偶数,因为任何数的两倍都是偶数。
同样,我们看到两个偶数之和也是偶数:2m + 2n = 2(m + n)。最后,我们还推断出偶数与奇数之和是奇数:2m + (2n + 1) = 2(m + n) + 1。图 1.2 更清楚地展示了这一逻辑。

图 1.2 输出响应与输入对之间内在逻辑的示意图
就这样!在完全不使用机器学习的情况下,你可以在任何一对整数上解决这个问题。直接应用数学规则可以解决这个问题。但在机器学习算法中,我们可以将内部逻辑视为黑盒,这意味着内部发生的逻辑可能不明显,如图 1.3 所示。

图 1.3 将机器学习解决问题的方法视为调整黑盒的参数,直到它产生令人满意的结果。
1.1.1 参数
有时候,设计一个将输入转换为相应输出的算法可能过于复杂。例如,如果输入是一系列代表灰度图像的数字,你可以想象编写一个算法来标记图像中每个对象的难度。当内部工作原理不为人所知时,机器学习就派上用场了。它为我们提供了一个工具包,可以编写软件而无需定义算法的每个细节。程序员可以留一些值未定,让机器学习系统自行找出最佳值。
未确定的值被称为参数,描述被称为模型。你的任务是编写一个算法,通过观察现有示例来找出如何最佳调整参数以实现最佳模型。哇,这话说得有点多!但别担心;这个概念将会反复出现。
机器学习可能在没有太多洞察力的情况下解决问题
通过掌握归纳问题解决的艺术,我们掌握了一把双刃剑。虽然机器学习算法在解决特定任务时可能表现良好,但追溯推理步骤以了解为什么产生结果可能并不那么清晰。一个复杂的机器学习系统学习成千上万的参数,但解开每个参数背后的含义有时并不是首要任务。考虑到这一点,我向你保证,有一个充满魔法的世界等待我们去探索。
练习 1.1
假设你已经收集了三个月的股票市场价格。你希望预测未来的趋势,以获得经济收益。在不使用机器学习的情况下,你将如何解决这个问题?(正如你将在第十三章中看到的,这个问题可以通过机器学习技术来解决。)
答案
信不信由你,硬编码的规则是定义股票市场交易策略的常见方式。像“如果价格下跌 5%,就买一些股票”这样简单的算法经常被使用。请注意,这里没有涉及机器学习——只有传统逻辑。
练习 1.2
美国国家航空航天局(NASA)将卫星发射到太空,卫星收集的数据我们称之为遥测数据。有时,收集到的数据中的异常表明仪器或收集数据时的条件出现了问题。为了简化,假设遥测数据是基于时间的数字序列。为了检测今天的异常,大多数方法使用简单的阈值,或者这些数字的最大或最小值来触发警报。使用机器学习来触发警报和检测异常,有什么更好的方法吗?
答案
你可以在每个时间步记录一系列名义上的 NASA 遥测数据——比如说,5 秒。然后取数据值,每当它们触发警报时,记录 1(异常);否则,记录 0(正常)。恭喜你——你已经建立了一个可以喂入你将在本书后面学习到的任何预测模型的真实数据集,例如回归或分类。你甚至可以构建一个深度学习模型。看,机器学习不是很有趣吗?
1.1.2 学习和推理
假设你正在尝试在烤箱里烘焙甜点。如果你对厨房不熟悉,可能需要几天时间才能找到正确的组合和完美的配料比例,以制作出美味的甜点。通过记录食谱,你可以记住如何重复制作甜点。
机器学习分享了食谱的想法。通常,我们分两个阶段来检查一个算法:学习和推理。学习阶段的目标是描述数据,这被称为特征向量,并在模型中总结它。模型是我们的食谱。实际上,模型是一个具有几个开放解释的程序,数据有助于消除歧义。
注意:特征向量是数据的实际简化。你可以将其视为一个属性列表中现实世界对象的充分总结。学习和推理步骤依赖于特征向量而不是直接的数据。
与食谱可以被其他人分享和使用的方式类似,学习到的模型被其他软件重用。学习阶段是最耗时的。运行一个算法可能需要数小时,甚至数天或数周才能收敛到一个有用的模型,正如你在第三章开始构建自己的模型时将会看到的。图 1.4 概述了学习流程。

图 1.4 学习方法通常遵循一个结构化的食谱。首先,数据集需要被转换成一个表示形式——通常是特征列表——学习算法可以使用。然后,学习算法选择一个模型并高效地搜索该模型的参数。
推理阶段使用模型对从未见过的新数据进行智能评论。这个阶段就像使用你在网上找到的食谱一样。推理过程通常比学习快得多;推理可以快到足以处理实时数据。推理完全是关于在新的数据上测试模型并在过程中观察性能,如图 1.5 所示。

图 1.5 推理方法通常使用已经学习或给出的模型。在将数据转换为可用的表示形式,例如特征向量之后,这种方法使用模型来产生预期的输出。
1.2 数据表示和特征
数据是机器学习的第一公民。计算机不过是非常复杂的计算器,所以我们提供给机器学习系统的数据必须是数学对象,如标量、向量、矩阵和图。
所有表示形式的基本主题是 特征,这是对象的可观察属性:
-
向量 具有扁平且简单的结构,是大多数现实世界机器学习应用中数据的典型体现。标量 是向量中的一个单个元素。向量有两个属性:一个表示向量维度的自然数,以及一个类型(例如实数、整数等)。整数的二维向量示例有 (1, 2) 和 (-6, 0);同样,标量可以是 1 或字符 a。实数的三维向量示例有 (1.1, 2.0, 3.9) 和 (∏, ∏/2, ∏/3)。你明白了:同一类型的数字集合。在一个使用机器学习的程序中,向量衡量数据的某个属性,如颜色、密度、响度或邻近性——任何可以用一系列数字描述的东西,每个数字对应于被测量的一个事物。
-
此外,向量的向量是一个 矩阵**. 如果每个特征向量描述了数据集中一个对象的特征,那么矩阵描述了所有对象;外部向量中的每个项目都是一个节点,它是一个对象的特征列表。
-
另一方面,图(graphs)更具表现力。图 是一组可以与 边 相连的对象(节点),用以表示网络。图形结构能够表示对象之间的关系,例如在社交网络或地铁系统的导航路线中。因此,在机器学习应用中,它们的管理要困难得多。在这本书中,我们的输入数据很少会涉及图形结构。
特征向量是现实世界数据的实用简化,这些数据可能过于复杂而难以处理。使用特征向量而不是关注数据项的每一个细节是一种实用的简化。例如,现实世界中的汽车远不止用来描述它的文本。汽车销售员试图卖给你的是汽车,而不是无形的话语或文字。这些话语是抽象概念,类似于特征向量是数据的摘要。
以下场景进一步解释了这一概念。当你打算购买一辆新车时,密切关注不同品牌和型号的每一个细节是至关重要的。毕竟,如果你即将花费数千美元,你最好勤奋地去做。你可能会记录下每辆车的特征列表并进行比较。这个有序的特征列表就是特征向量。
当你在购买汽车时,你可能发现比较油耗比比较一些不那么相关的因素(如重量)更有利可图。要跟踪的特征数量也必须恰到好处——既不能太少,否则你会失去你关心的信息,也不能太多,否则它们将难以管理且耗时。这种选择测量数量和比较哪些测量的巨大努力被称为特征工程或特征选择。根据你检查的特征,你系统的性能可能会大幅波动。选择正确的特征进行跟踪可以弥补一个弱学习算法的不足。
例如,当训练一个模型来检测图像中的汽车时,如果你首先将图像转换为灰度,你将获得巨大的性能和速度提升。通过在预处理数据时提供一些自己的偏见,你最终帮助了算法,因为它不需要学习颜色在检测汽车时并不重要。算法可以专注于识别形状和纹理,这将导致比尝试处理颜色更快的学习。
在机器学习(ML)中的一般经验法则是,更多的数据会产生更好的结果。但拥有更多特征并不总是如此。也许出人意料的是,如果你跟踪的特征数量过高,性能可能会受到影响。随着特征向量维度的增加,用代表性样本填充所有数据的空间需要指数级更多的数据。因此,如图 1.6 所示的特征工程是机器学习中最重要的问题之一。
维度诅咒
为了准确地对现实世界数据进行建模,我们显然需要不止一个或两个数据点。但数据量取决于许多因素,包括特征向量的维度数。添加过多的特征会导致描述空间所需的数据点数量呈指数增长。这就是为什么我们不能设计一个包含一百万维度的特征向量来耗尽所有可能的因素,然后期望算法学习到一个模型。这种现象被称为维度诅咒**。
你可能一开始不会意识到这一点,但当你决定哪些特征值得观察时,会发生一些重要的事情。几个世纪以来,哲学家们一直在思考同一性的含义;你可能不会立刻意识到,通过选择特定的特征,你已经对同一性给出了一种定义。

图 1.6 特征工程是选择与任务相关的特征的过程。
想象一下编写一个机器学习系统来检测图像中的面部。假设面部的一个必要特征是存在两只眼睛。隐含地,面部现在被定义为有眼睛的东西。你是否意识到这种定义可能会给你带来什么样的麻烦?如果一张照片显示某人正在眨眼,你的检测器将找不到面部,因为它找不到两只眼睛。当一个人眨眼时,算法将无法检测到面部。面部的定义从一开始就不准确,从糟糕的检测结果中可以明显看出。
现在,尤其是在智能车辆和自主无人机等能力以惊人的速度发展的今天,机器学习中的身份偏差,或者简单地说偏差,已经成为一个重大的问题,因为这些能力如果出错可能会导致人员伤亡。考虑一辆从未见过轮椅中的人的智能车辆,因为训练数据中从未包含这些例子,所以当轮椅进入人行横道时,智能汽车不会停车。如果一家公司用于递送包裹的无人机训练数据从未见过戴帽子的女性,而所有其他看起来像帽子的训练实例都是着陆点,会怎样呢?帽子和,更重要的是,戴帽子的人可能会处于极大的危险中!
物体的身份被分解为其组成的特征。如果你跟踪的一辆车的特征与另一辆车的对应特征相匹配,那么从你的角度来看,它们可能无法区分。你需要向系统中添加另一个特征来区分这些车辆;否则,你会认为它们是同一件物品(比如无人机落在那位不幸女士的帽子上)。在手工制作特征时,你必须非常小心,不要陷入这种关于身份的哲学困境。
练习 1.3
假设你正在教机器人如何叠衣服。感知系统看到衬衫躺在桌子上,如图所示。你希望将衬衫表示为特征向量,以便你可以将其与不同的衣服进行比较。决定哪些特征最有用进行追踪。(提示:零售商在线描述他们的服装时使用哪些类型的词语?)

一台机器人正在尝试叠衬衫。衬衫有哪些好的特征可以追踪?
答案
在叠衣服时,观察宽度、高度、x 对称得分、y 对称得分和平坦度是很好的特征。颜色、布料纹理和材料大多无关紧要。
练习 1.4
现在,你雄心勃勃地决定检测任意物体;以下图显示了几个示例。有哪些显著的特征可以轻松区分物体?

这里展示了三个物体的图像:一盏灯、一条裤子和一只狗。你应记录哪些好的特征来比较和区分物体?
答案
观察亮度和反射可能有助于区分灯和其他两个物体。裤子的形状通常遵循一个可预测的模板,因此形状将是另一个很好的追踪特征。最后,纹理可能是区分狗的图像与其他两个类别的显著特征。
特征工程是一项令人耳目一新的哲学追求。对于那些喜欢思考自我意义的探险者,我邀请你们沉思于特征选择,因为它仍然是一个未解之谜。幸运的是,对于你们其他人来说,为了缓解广泛的争论,最近的研究进展使得自动确定要追踪哪些特征成为可能。你们将在第七章中亲自尝试这个过程。
现在考虑这样一个问题:一位医生正在查看一组 N 张 244×244(宽度×高度)的鳞状细胞图像,就像图 1.7 中所示的那样,并试图确定它们是否表明患者存在癌症。一些图像明确表明有癌症;而另一些则没有。医生可能有一组历史患者图像,他可以随着时间的推移进行检查和学习,这样当他看到新的图像时,他就能发展出自己关于癌症外观的表征模型。

图 1.7 机器学习过程。从左到右,医生试图确定代表细胞活检的图像是否表明他们的患者有癌症。
特征向量在学习和推理中都得到应用
学习和推理之间的相互作用为机器学习系统提供了一个完整的图景,如下所示。第一步是将现实世界数据表示为特征向量。例如,我们可以通过像素强度对应的数字向量来表示图像。(我们将在未来的章节中更详细地探讨如何表示图像。)我们可以向学习算法展示与每个特征向量相关的真实标签(如鸟或狗)。有了足够的数据,算法就会生成一个学习模型。我们可以使用这个模型来处理其他现实世界数据,以揭示之前未知的标签。
特征向量是机器学习的学习和推理组件使用的现实世界数据的表示。算法的输入不是直接的现实世界图像,而是其特征向量。

特征向量是机器学习的学习和推理组件使用的现实世界数据的表示。算法的输入不是直接的现实世界图像,而是其特征向量。
在机器学习中,我们试图模拟这个模型构建过程。首先,我们从历史患者数据中获取 N 张 244 × 244 的鳞状癌细胞图像,并通过将图像与其关联的标签(癌症或无癌症)排列起来来准备问题。我们称这个阶段为机器学习的数据清洗和准备阶段。接下来是识别重要特征的过程。特征包括图像像素强度,或每个 x, y, 和 c 的早期值,或 (244, 244, 3),代表图像的高度、宽度和三个通道的红/绿/蓝 (RGB) 颜色。模型创建这些特征值与所需标签输出(癌症或无癌症)之间的映射。
1.3 距离度量
如果你有一系列你想要购买的特征向量,你可以在特征向量上定义一个距离函数,以确定哪两辆车最相似。比较对象之间的相似性是机器学习的一个基本组成部分。特征向量使我们能够以各种方式表示对象,以便进行比较。一种标准的方法是使用 欧几里得距离,这是你在思考空间中的点时可能发现最直观的几何解释。
假设我们有两个特征向量,x = (x[1], x[2], ..., x[n]) 和 y = (y[1], y[2], ..., y[n])。欧几里得距离 ||x - y || 使用以下公式计算,学者们称之为 L2 范数:

点 (0, 1) 和 (1, 0) 之间的欧几里得距离是

然而,这个函数只是许多可能的距离函数之一。L0、L1 和 L-infinity 范数也存在。所有这些范数都是测量距离的有效方式。以下是更详细的说明:
-
L0 范数计算向量的总非零元素数。例如,原点(0, 0)和向量(0, 5)之间的距离是 1,因为只有一个非零元素。(1, 1)和(2, 2)之间的 L0 距离是 2,因为两个维度都不匹配。想象一下,第一和第二维度分别代表用户名和密码。如果登录尝试和真实凭证之间的 L0 距离是 0,则登录成功。如果距离是 1,则用户名或密码错误,但不是两者都错误。最后,如果距离是 2,则数据库中找不到用户名或密码。
-
L1 范数,如图 1.8 所示,定义为Σx[n]。在 L1 范数下,两个向量之间的距离也被称为曼哈顿距离。想象一下生活在像曼哈顿这样的市中心地区,街道形成了一个网格。从一个交叉口到另一个交叉口的最短距离是沿着街区。同样,两个向量之间的 L1 距离是沿着正交方向。在 L1 范数下,(0, 1)和(1, 0)之间的距离是 2。计算两个向量之间的 L1 距离是每个维度绝对差分的总和,这是一个有用的相似度度量。

图 1.8 L1 距离被称为曼哈顿距离(也称为出租车距离),因为它类似于在像曼哈顿这样的网格状社区中汽车行驶的路线。如果一辆车从点(0, 1)行驶到点(1, 0),最短路线需要 2 个单位的长度。
- L2 范数,如图 1.9 所示,是向量的欧几里得长度,(Σ(x[n])²)^(1/2)。这是在几何平面上从一点到另一点可以采取的最直接路线。对于数学爱好者来说,这个范数实现了高斯-马尔可夫定理预测的最小二乘估计。对于其他人来说,它是空间中两点之间的最短距离。

图 1.9 点(0, 1)和(1, 0)之间的 L2 范数是两点之间单一直线段的长度。
-
L-N 范数将这种模式推广,结果为(Σ(|x[n]|)N)(1/N)。我们很少使用 L2 以上的有限范数,但这里列出是为了完整性。
-
L 无穷范数是(Σ(|x[n]|)∞)(1/∞)。更自然地说,它是每个元素中的最大幅度。如果向量是(-1, -2, -3),则 L 无穷范数是 3。如果一个特征向量代表各种物品的成本,最小化向量的 L 无穷范数是尝试减少最昂贵物品的成本。
1.4 学习类型
现在您可以使用特征向量进行比较,您有了使用数据为实际算法提供工具的必要条件。机器学习通常分为三个视角:监督学习、无监督学习和强化学习。一个新兴的新领域是元学习,有时称为 AutoML。以下几节将检查所有四种类型。
在现实生活中,我什么时候会使用 L2 范数以外的度量标准?
假设你正在为一家搜索引擎初创公司工作,试图与谷歌竞争。你的老板分配给你一个任务,就是使用机器学习来为每个用户个性化搜索结果。
一个好的目标可能是用户每月不应看到五个或更多的错误搜索结果。一年的用户数据是一个 12 维向量(每年的每个月份是一个维度),表示每月显示的错误结果的数目。你试图满足这个条件,即这个向量的 L-无穷范数必须小于 5。
假设你的老板改变了要求,说整个年度内允许的错误搜索结果少于五个。在这种情况下,你试图实现 L1 范数低于 5,因为整个空间中所有错误的和应该小于 5。
现在老板再次改变了要求:错误搜索结果的月份数量应少于 5。在这种情况下,你试图实现 L0 范数小于 5,因为非零错误的月份数量应少于 5。
1.4.1 监督学习
根据定义,一个监督者是命令链中的高级人员。当我们犹豫不决时,我们的监督者会指示我们做什么。同样,监督学习完全是关于从监督者(如教师)提供的示例中学习。
一个监督机器学习系统需要标记数据来发展有用的理解,我们称之为其模型。例如,给定许多人的照片和记录的相应种族,我们可以训练一个模型来对任意照片中从未见过的人的种族进行分类。简单来说,模型是一个函数,通过使用一组称为训练数据集的先前示例作为参考,为数据分配标签。
通过数学符号来谈论模型是一种方便的方式。设x为数据的一个实例,例如一个特征向量。与x相关联的标签是f(x),通常被称为x的真实值。通常,我们使用变量y = f(x),因为它写起来更快。在通过照片对人的种族进行分类的例子中,x可以是一个包含各种相关特征的 100 维向量,而y是代表各种种族的一组值之一。因为y是离散的,值很少,所以该模型被称为分类器。如果y可以产生许多值,并且这些值有自然顺序,则该模型被称为回归器。
让我们用g(x)表示模型对x的预测。有时,你可以调整模型以显著改变其性能。模型有参数,可以由人类或自动调整。我们用向量来表示参数。将所有这些放在一起,g(x|)更完整地表示了模型,读作“g of x given。”
注意 模型也可能有超参数,这是模型的一些额外临时属性。超参数中的超字眼一开始可能显得有些奇怪。更好的名字可能是元参数,因为参数类似于关于模型的元数据。
模型预测 g(x|) 的成功取决于它与真实值 y 的吻合程度。我们需要一种方法来衡量这两个向量之间的距离。例如,L2 范数可以用来衡量两个向量之间的接近程度。真实值与预测值之间的距离称为成本。
监督机器学习算法的本质是找出导致最小成本的模型参数。从数学上讲,我们正在寻找一个 θ*(读作 theta star),它在所有数据点 x ∈ X 中最小化成本。将这个优化问题形式化的一个方法如下方程:
θ^(* )= argmin[θ]Cost(θ|X)
其中

显然,穷举所有可能的 x 组合(也称为参数空间)最终会找到最优解,但运行时间将无法接受。机器学习的一个主要研究领域是编写高效搜索这个参数空间的算法。一些早期的算法包括梯度下降、模拟退火和*遗传算法.* TensorFlow 自动处理这些算法的低级实现细节,因此我不会过多地深入这些细节。
无论通过何种方式学习参数后,你最终可以评估模型以了解系统从数据中捕获模式的效果如何。一个经验法则是不要在用于训练的数据上评估你的模型,因为你已经知道它对训练数据有效;你需要判断模型是否对未包含在训练集中的数据进行有效处理,以确保你的模型是通用型的,而不是对训练数据有偏见。使用大部分数据用于训练,其余的用于测试。例如,如果你有 100 个标记的数据点,随机选择其中的 70 个来训练一个模型,并保留其余的 30 个来测试它,这样就创建了一个 70-30 的分割。
为什么需要分割数据?
如果 70-30 的分割看起来很奇怪,可以这样思考。假设你的物理老师给你一份练习考试,并告诉你真正的考试将与这次没有区别。你不妨记住答案,而不理解概念就能得到满分。同样,如果你在训练数据集上测试你的模型,你并没有给自己带来任何好处。你可能会产生一种虚假的安全感,因为模型可能只是记住结果。那么,这种所谓的“智慧”在哪里呢?
与使用 70-30 的分割不同,机器学习从业者通常将他们的数据集分成 60-20-20。训练消耗 60%的数据集,测试使用 20%,剩下的 20%用于验证,这在第二章中有解释。
1.4.2 无监督学习
无监督学习 是关于对没有相应标签或响应的数据进行建模。我们能够在原始数据上得出任何结论都感觉像是魔法。有了足够的数据,可能可以发现模式和结构。机器学习从业者从数据本身学习时使用的两个最强大的工具是聚类和维度降低。
聚类 是将数据分割成相似项的单独桶的过程。从某种意义上说,聚类就像在没有知道任何对应标签的情况下对数据进行分类。例如,当你将你的书组织在三个书架上时,你可能会将相似类型的书籍放在一起,或者可能按作者姓氏分组。你可能有一个斯蒂芬·金区域,另一个用于教科书,第三个用于其他任何东西。你不在乎所有书籍是否由相同的特征分开,只在乎每本书都有一些独特的东西,这让你能够将其组织到几个大致相等、易于识别的组中。最流行的聚类算法之一是 k-means,它是称为 E-M 算法 的更强大技术的一个特定实例。
维度降低 是关于操纵数据以便从更简单的角度来观察它——机器学习中的“保持简单,愚蠢”这一短语的等价物。例如,通过去除冗余特征,我们可以用更低维度的空间来解释相同的数据,并看到哪些特征是重要的。这种简化也有助于数据可视化或预处理以提高性能效率。最早的算法之一是 主成分分析 (PCA),而较新的一个是 自编码器,这些内容在第七章中有介绍。
1.4.3 强化学习
监督学习和无监督学习似乎暗示了教师的存在要么全部要么没有。但在机器学习的一个研究得很好的分支中,环境充当教师,提供提示而不是明确的答案。学习系统对其行为获得反馈,没有具体的承诺表明它在正确的方向上进步,这可能是指解决迷宫或完成一个明确的目标。
探索与利用:强化学习的核心
想象一下玩一个你从未见过的视频游戏。你在控制器上点击按钮,发现某个特定的按键组合逐渐提高你的分数。太棒了!现在你反复利用这个发现,希望能打破高分。然而,在你的潜意识里,你却在想是否错过了更好的按键组合。你应该利用你当前的最佳策略,还是冒险探索新的选项?
与监督学习不同,在监督学习中,训练数据由“教师”方便地标注,强化学习则通过观察环境对动作的反应来收集信息进行训练。强化学习是一种与环境交互以学习哪种动作组合能产生最理想结果的机器学习方法。因为我们已经通过使用“环境”和“动作”这些词将算法拟人化了,学者们通常将这个系统称为自主的代理。因此,这种机器学习方法自然地体现在机器人领域。
为了在环境中推理代理,我们引入了两个新概念:状态和动作。在特定时间冻结的世界状态称为状态。代理可以通过执行许多动作之一来改变当前状态。为了驱动代理执行动作,每个状态都会产生相应的奖励。代理最终会发现每个状态的预期总奖励,称为该状态的价值。
与任何其他机器学习系统一样,性能随着数据的增加而提高。在这种情况下,数据是经验的历史。在强化学习中,我们不知道一系列动作的最终成本或奖励,直到这一系列动作被执行。这些情况使得传统的监督学习变得无效,因为我们不知道在动作序列的历史中,哪个动作导致了最终的低价值状态。代理所知道的确切信息只是它已经采取的一系列动作的成本,这是不完整的。代理的目标是找到一系列动作,以最大化奖励。如果你对这个主题更感兴趣,你可能想查看 Manning Publications 家族的另一本相关书籍:Miguel Morales 所著的Grokking Deep Reinforcement Learning(Manning,2020;www.manning.com/books/grokking-deep-reinforcement-learning)。
1.4.4 元学习
相对较近,一个新的机器学习领域——元学习——出现了。这个想法很简单。数据科学家和机器学习专家花费大量时间执行机器学习的步骤,如图 1.7 所示。如果这些步骤——定义和表示问题、选择模型、测试模型和评估模型——本身可以被自动化,那会怎样?为什么不限制于只探索一个或一小组模型,而是让程序本身尝试所有模型呢?
许多企业将领域专家(如图 1.7 中的医生)、数据科学家(负责建模数据和可能提取或选择重要特征的人员,例如图像的 RGB 像素)以及机器学习工程师(负责调整、测试和部署模型)的角色分开,如图 1.10a 所示。正如你从本章前面的内容中记得的那样,这些角色在三个基本领域进行互动:数据清洗和准备,这两个领域领域专家和数据科学家都可能提供帮助;特征和模型选择,主要是数据科学家的工作,ML 工程师提供一些帮助;然后是训练、测试和评估,主要是 ML 工程师的工作,数据科学家提供一些帮助。我们还增加了一个新的复杂因素:将我们的模型部署出去,这在现实世界中会发生,并且带来了一组自己的挑战。这就是你正在阅读本书第二版的原因之一;它将在第二章中讨论,我将讨论部署和使用 TensorFlow。
如果我们能够让系统自动在可能模型的范围内搜索,并尝试所有模型,而不是让数据科学家和 ML 工程师选择模型、训练、评估和调整它们,会怎么样呢?这种方法克服了将你的整体 ML 经验限制在少量可能解决方案中的限制,你可能会选择第一个表现合理的解决方案。但如果系统能够找出哪些模型最好以及如何自动调整模型呢?这正是你在图 1.10b 中看到的内容:元学习的过程,或自动机器学习(AutoML)。

图 1.10 传统机器学习及其演变为元学习,其中系统自行进行模型选择、训练、调整和评估,以从众多候选模型中选择最佳机器学习模型
数据科学家,你们被取消了!
今天的取消文化非常适合元学习这一概念,其根源源于数据科学本身——创建和实验多种类型的机器学习管道的过程,包括数据清洗、模型构建和测试——可以自动化的想法。与之相关的一个高级研究计划局(DARPA)项目,数据驱动模型发现(D3M),声称目标是废除数据科学家,而是自动化他们的活动。尽管那个 DARPA 项目以及元学习领域的成果是有希望的,但我们还没有准备好完全取消数据科学家……至少现在还没有。别担心;你很安全!
练习 1.5
你会使用监督学习、无监督学习、强化学习还是元学习来解决以下问题?(a)找到最佳机器学习算法,该算法使用棒球统计数据并预测一名球员是否会进入名人堂。(b)根据没有其他信息的情况,将各种水果组织到三个篮子里。(c)根据传感器数据预测天气。(d)在多次尝试和错误后学会下棋。
答案
(a)元学习;(b)无监督学习;(c)监督学习;(d)强化学习。
1.5 TensorFlow
谷歌在 2015 年底以 Apache 2.0 许可证开源了其机器学习框架 TensorFlow。在此之前,它被谷歌在语音识别、搜索、照片和 Gmail 等应用中私有使用。
一点历史
一个名为 DistBelief 的早期可扩展分布式训练和学习系统是 TensorFlow 当前实现的主要影响。你有没有写过一团糟的代码,希望可以重新开始?这就是 DistBelief 和 TensorFlow 之间的动态。TensorFlow 不是谷歌基于内部项目开源的第一个系统。谷歌著名的 Map-Reduce 系统和 Google File System(GFS)是现代 Apache 数据处理、网络爬虫和大数据系统(包括 Hadoop、Nutch 和 Spark)的基础。此外,谷歌的大表系统是 Apache Hbase 项目的基础。
该库是用 C++实现的,具有方便的 Python API,以及一个不太受欢迎的 C++ API。由于依赖关系更简单,TensorFlow 可以快速部署到各种架构。
与 Theano——一个你可能熟悉的流行的 Python 数值计算库——类似——计算被描述为流程图,将设计从实现中分离出来。几乎无需麻烦,这种二分法使得相同的设计可以在移动设备以及拥有数千个处理器的庞大训练系统中实现。单一系统覆盖了广泛的平台。TensorFlow 也与各种新开发的类似机器学习库兼容得很好,包括 Keras(TensorFlow 2.0 完全集成了 Keras),以及 PyTorch(pytorch.org)等库,这些库最初由 Facebook 开发,以及更丰富的机器学习应用程序编程接口,如 Fast.Ai。你可以使用许多工具包来做机器学习,但你正在读一本关于 TensorFlow 的书,对吧?让我们专注于它!
TensorFlow 最令人印象深刻的特性之一是其自动微分功能。你可以在不重新定义许多关键计算的情况下实验新的网络。
注意自动微分使得实现反向传播变得容易得多,这是一种在机器学习的一个分支——神经网络中使用的计算密集型计算。TensorFlow 隐藏了反向传播的细节,这样你可以专注于更大的图景。第十一章介绍了 TensorFlow 中的神经网络简介。
所有数学都被抽象出来,在幕后展开。使用 TensorFlow 就像使用 WolframAlpha 来解决微积分问题集。
该库的另一个特点是它的交互式可视化环境,称为TensorBoard。这个工具显示了数据转换的流程图,随着时间的推移显示总结日志,并追踪性能。图 1.11 显示了 TensorBoard 的外观;第二章介绍了如何使用它。

图 1.11 TensorBoard 的实际应用示例
在 TensorFlow 中进行原型设计比在 Theano 中快得多(代码启动只需几秒钟,而不是几分钟),因为许多操作都是预编译的。由于子图执行,调试代码变得容易;整个计算段可以重用而无需重新计算。
因为 TensorFlow 不仅仅关于神经网络,它还提供了现成的矩阵计算和操作工具。大多数库,如 PyTorch、Fast.Ai 和 Caffe,都是专门为深度神经网络设计的,但 TensorFlow 更加灵活和可扩展。
该库得到了良好的文档记录,并得到了 Google 的官方支持。机器学习是一个复杂的话题,因此有一个非常值得信赖的公司支持 TensorFlow 是令人欣慰的。
1.6 未来章节概述
第二章演示了如何使用 TensorFlow 的各种组件(见图 1.12)。第 3-10 章展示了如何在 TensorFlow 中实现经典机器学习算法,第 11-19 章涵盖了基于神经网络的算法。这些算法解决了广泛的问题,如预测、分类、聚类、降维和规划。

图 1.12 本章介绍了基本的机器学习概念,第二章开始了你在 TensorFlow 中的旅程。其他工具可以应用机器学习算法(如 Caffe、Theano 和 Torch),但你在第二章中会看到为什么 TensorFlow 是最佳选择。
许多算法可以解决同一个实际世界问题,许多实际世界问题也可以由同一个算法解决。表 1.1 涵盖了本书中阐述的内容。
表 1.1 许多实际世界问题可以通过在其相应章节中找到的相应算法来解决。
| 实际世界问题 | 算法 | 章节 |
|---|---|---|
| 预测趋势,将曲线拟合到数据点,描述变量之间的关系 | 线性回归 | 3, 4 |
| 将数据分类为两个类别,找到分割数据集的最佳方式 | 逻辑回归 | 5, 6 |
| 将数据分类到多个类别 | Softmax 回归 | 5, 6 |
| 揭示观察到的隐藏原因,找到一系列结果最可能的隐藏原因 | 隐藏马尔可夫模型(维特比) | 9, 10 |
| 将数据聚类到固定数量的类别,自动将数据点划分到不同的类别中 | k-means | 7, 8 |
| 将数据聚类到任意类别,在低维嵌入中可视化高维数据 | 自组织映射 | 7, 8 |
| 降低数据的维度,学习导致高维数据的潜在变量 | 自动编码器 | 11, 12 |
| 使用神经网络在环境中规划动作(强化学习) | Q 策略神经网络 | 13 |
| 使用监督神经网络对数据进行分类 | 感知器 | 14, 15 |
| 使用监督神经网络对现实世界图像进行分类 | 卷积神经网络 | 14, 15 |
| 使用神经网络产生与观察结果匹配的模型 | 循环神经网络 | 16, 17 |
| 预测对自然语言查询的自然语言响应 | Seq2seq 模型 | 18 |
| 通过学习它们的效用对项目进行排序 | 排序 | 19 |
TIP 如果你对 TensorFlow 的复杂架构细节感兴趣,最佳资源是官方文档中的www.tensorflow.org/tutorials/customization/basics。本书迅速前进,在调整低级性能的同时,不减速地使用 TensorFlow。如果你对云服务感兴趣,你可能想考虑谷歌针对专业级规模和速度的解决方案(cloud.google.com/products/ai)。
摘要
-
TensorFlow 已经成为专业人士和研究人员实现机器学习解决方案的首选工具。
-
机器学习使用示例来开发一个专家系统,该系统能对新的输入做出有用的陈述。
-
机器学习的一个关键特性是,性能往往随着更多训练数据的增加而提高。
-
几年来,学者们已经构建了三个主要原型,大多数问题都符合这些原型:监督学习、无监督学习和强化学习。元学习是机器学习的一个新领域,它专注于自动探索整个模型、解决方案和调整技巧的空间。
-
在从机器学习角度对现实世界问题进行表述后,会出现几种算法。在众多可以完成实现的软件库和框架中,我们选择了 TensorFlow 作为我们的银弹。由谷歌开发并得到其繁荣社区的支撑,TensorFlow 为我们提供了一种轻松实现行业标准代码的方法。
2 TensorFlow 的基本要素
本章涵盖
-
理解 TensorFlow 的工作流程
-
使用 Jupyter 创建交互式笔记本
-
使用 TensorBoard 可视化算法
在实现机器学习算法之前,让我们熟悉如何使用 TensorFlow。你将立即动手编写简单的代码!本章将介绍 TensorFlow 的一些基本优势,以说服你它是最受欢迎的机器学习库。在你继续之前,请按照附录中的步骤进行安装说明,然后返回此处。
作为一种思想实验,让我们看看当我们不使用便捷的计算库时使用 Python 代码会发生什么。这就像使用一部没有安装任何额外应用程序的新智能手机一样。功能是有的,但如果你有了正确的工具,你会更加高效。
假设你是一位私人企业主,正在跟踪你产品的销售流向。你的库存由 100 件商品组成,你将每件商品的价格表示为一个名为 prices 的向量。另一个 100 维向量 amounts 表示每种商品的库存数量。你可以编写如列表 2.1 所示的 Python 代码块来计算所有产品的收入。请注意,此代码没有导入任何库。
列表 2.1 不使用库计算两个向量的内积
revenue = 0
for price, amount in zip(prices, amounts):
revenue += price * amount
计算两个向量的内积(也称为点积)的代码已经很多了。想象一下,如果你仍然缺乏 TensorFlow 及其朋友(如数值 Python(NumPy)库),解决线性方程或计算两个向量之间距离的代码将需要多少。
在安装 TensorFlow 库时,你还会安装一个知名且稳健的 Python 库,名为 NumPy,它简化了 Python 中的数学操作。在不使用其库(NumPy 和 TensorFlow)的情况下使用 Python,就像使用没有自动对焦模式的相机一样;当然,你获得了更多的灵活性,但你很容易犯粗心大意的错误。(记录在案,我们并不反对那些精心管理光圈、快门和 ISO(用于准备相机拍摄图像的所谓“手动”旋钮)的摄影师。)在机器学习中很容易犯错误,所以让我们保持相机在自动对焦模式下,并使用 TensorFlow 来帮助自动化繁琐的软件开发。
以下代码片段展示了如何使用 NumPy 简洁地编写相同的内积:
import numpy as np
revenue = np.dot(prices, amounts)
Python 是一种简洁的语言。幸运的是,这本书没有充斥着难以理解的代码。另一方面,Python 语言的简洁性也意味着每一行代码背后都发生了很多事情,你在跟随本章学习时应该仔细研究。你会发现,这将是 TensorFlow 的核心主题,它是作为 Python 的附加库优雅地平衡这一点的。TensorFlow 隐藏了足够的复杂性(比如自动对焦),但同时也允许你在想要深入时调整那些神奇的配置旋钮。
机器学习算法需要许多数学运算。通常,一个算法可以归结为一系列简单函数的迭代,直到收敛。当然,你可以使用任何标准的编程语言来执行这些计算,但既可管理又高性能的代码的秘密在于使用一个编写良好的库,比如 TensorFlow(它官方支持 Python、C++、JavaScript、Go 和 Swift)。
TIP 关于 API 的各种函数的详细文档可在www.tensorflow.org/api_docs找到。
本章中你将学习的技能是针对使用 TensorFlow 进行计算,因为机器学习依赖于数学公式。在浏览了示例和代码列表之后,你将能够使用 TensorFlow 执行任意任务,例如在大数据上计算统计数据,并使用 TensorFlow 的朋友如 NumPy 和 Matplotlib(用于可视化)来理解为什么你的机器学习算法会做出那些决策,并提供可解释的结果。这里的重点完全在于如何使用 TensorFlow,而不是通用的机器学习,我们将在后面的章节中涉及。这听起来像是一个温和的开始,对吧?
在本章的后面部分,你将使用对机器学习至关重要的 TensorFlow 旗舰功能。这些功能包括将计算表示为数据流图、设计执行分离、部分子图计算和自动微分。无需多言,让我们编写我们的第一个 TensorFlow 代码!
2.1 确保 TensorFlow 正常运行
首先,你应该确保一切运行正常。检查你的汽车油位,修复地下室烧毁的保险丝,并确保你的信用余额为零。我开玩笑的;我们说的是 TensorFlow。
为你的第一段代码创建一个名为 test.py 的新文件。通过运行以下脚本导入 TensorFlow:
import tensorflow as tf
遇到技术难题?
如果你安装了 GPU 版本且库未能搜索 CUDA 驱动程序,在这个步骤中通常会出错。记住,如果你用 CUDA 编译了库,你需要更新你的环境变量以包含 CUDA 的路径。查看 TensorFlow 上的 CUDA 说明。(有关更多信息,请参阅www.tensorflow.org/install/gpu。)
这个单独的导入使 TensorFlow 准备好为你服务。如果 Python 解释器没有抱怨,你就准备好开始使用 TensorFlow 了。
遵循 TensorFlow 约定
TensorFlow 库通常使用tf别名导入。通常,使用tf来限定 TensorFlow 是一个好主意,因为它能让你与其他开发者和开源 TensorFlow 项目保持一致性。当然,你也可以使用另一个别名(或没有别名),但这样在项目中成功重用其他人的 TensorFlow 代码片段将是一个复杂的过程。同样,NumPy 作为np,Matplotlib 作为plt,你将在整本书中看到它们作为惯例的使用。
2.2 张量的表示
现在你已经知道如何将 TensorFlow 导入 Python 源文件,让我们开始使用它!如第一章所述,描述现实世界中的对象的一个方便方法是列出其属性或特征。例如,你可以通过颜色、型号、引擎类型、里程数等来描述一辆车。特征的一个有序列表称为特征向量,这正是你将在TensorFlow代码中表示的内容。
特征向量是机器学习中最有用的工具之一,因为它们的简单性;它们是数字的列表。每个数据项通常由一个特征向量组成,一个好的数据集有数百个,甚至数千个特征向量。毫无疑问,你经常会同时处理多个向量。一个矩阵简洁地表示了一组向量,其中矩阵的每一列都是一个特征向量。
在 TensorFlow 中表示矩阵的语法是一个向量,由长度相同的向量组成。图 2.1 是一个具有两行三列的矩阵示例,例如[[1, 2, 3], [4, 5, 6]]。请注意,这个向量包含两个元素,每个元素对应矩阵的一行。

图 2.1 图表下半部分的矩阵是其上半部分紧凑代码表示的视觉化。这种表示法是科学计算库中的一种常见范式。
我们通过指定矩阵的行和列索引来访问矩阵中的一个元素。第一行和第一列表示左上角的第一元素,例如。有时,使用超过两个索引会更方便,例如在引用彩色图像中的一个像素时,不仅通过其行和列,还通过其红/绿/蓝通道。张量是矩阵的推广,它通过任意数量的索引来指定一个元素。
张量示例
假设一所小学为所有学生安排了固定的座位。你是校长,你记不住名字。幸运的是,每个教室都有一个座位网格,你可以通过学生的行和列索引轻松地给他们起绰号。
学校有多个教室,所以你不能简单地说,“早上好 4,10!继续保持!”你还需要指定教室:“嗨,2 号教室的 4,10。”与只需要两个索引来指定元素的矩阵不同,这个学校的学生需要三个数字。他们都是三秩张量的一部分。
张量的语法甚至更嵌套的向量。如图 2.2 所示,一个 2×3×2 的张量是 [[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]],这可以想象成两个大小为 3×2 的矩阵。因此,我们说这个张量的秩为 3。一般来说,张量的秩是指定一个元素所需的索引数。TensorFlow 中的机器学习算法作用于张量,因此理解如何使用它们是很重要的。

图 2.2 你可以将这个张量想象成多个矩阵叠加在一起。要指定一个元素,你必须指明行和列,以及访问的是哪个矩阵。因此,这个张量的秩为 3。
在众多表示张量的方式中很容易迷失方向。直观地看,列表 2.2 中的三行代码试图表示同一个 2×2 矩阵。这个矩阵代表两个各二维的特征向量。例如,它可以代表两个人对两部电影的评价。每个人,通过矩阵的行进行索引,分配一个数字来描述他们对电影的评论,通过列进行索引。运行代码以查看如何在 TensorFlow 中生成矩阵。
列表 2.2 表示张量的不同方式
import tensorflow as tf
import numpy as np ❶
m1 = [[1.0, 2.0],
[3.0, 4.0]] ❷
m2 = np.array([[1.0, 2.0], ❷
[3.0, 4.0]], dtype=np.float32) ❷
m3 = tf.constant([[1.0, 2.0], ❷
[3.0, 4.0]]) ❷
print(type(m1)) ❸
print(type(m2)) ❸
print(type(m3)) ❸
t1 = tf.convert_to_tensor(m1, dtype=tf.float32) ❹
t2 = tf.convert_to_tensor(m2, dtype=tf.float32) ❹
t3 = tf.convert_to_tensor(m3, dtype=tf.float32) ❹
print(type(t1)) ❺
print(type(t2)) ❺
print(type(t3)) ❺
❶ 你将在 TensorFlow 中使用 NumPy 矩阵。
❷ 以三种方式定义一个 2×2 矩阵
❸ 打印每个矩阵的类型
❹ 从各种类型创建张量对象
❺ 注意现在类型将是相同的。
第一个变量(m1)是一个列表,第二个变量(m2)是来自 NumPy 库的ndarray,最后一个变量(m3)是 TensorFlow 的常量Tensor对象,你通过使用tf.constant来初始化它。指定矩阵的三种方式中,没有一种方式必然比另一种更好,但每种方式都给你一组原始的列表值(m1)、一个类型化的 NumPy 对象(m2)或一个初始化的数据流操作:张量(m3)。
TensorFlow 中的所有操作符,如negative,都是设计来对张量对象进行操作的。一个方便的函数,你可以在任何地方使用它来确保你正在处理张量而不是其他类型,是tf.convert_to_tensor``(...)。TensorFlow 库中的大多数函数已经执行了这个功能(冗余),即使你忘记了这样做。使用tf.convert_to_tensor(...)是可选的,但我们在这里展示它,因为它有助于阐明库和整个 Python 编程语言中正在处理的隐式类型系统。列表 2.3 输出以下三次:
<class 'tensorflow.python.framework.ops.Tensor'>
提示:为了使复制和粘贴更容易,你可以在本书的 GitHub 网站上找到代码列表:github.com/chrismattmann/MLwithTensorFlow2ed。你还可以找到一个完全可用的 Docker 镜像,你可以使用所有数据、代码和库来运行本书中的示例。使用 docker pull chrismattmann/mltf2 安装它,并查看附录以获取更多详细信息。
让我们再次看看如何在代码中定义张量。在导入 TensorFlow 库之后,你可以使用 tf.constant 操作符,如下所示。列表 2.3 展示了一些不同维度的张量。
列表 2.3 创建张量
import tensorflow as tf
m1 = tf.constant([[1., 2.]]) ❶
m2 = tf.constant([[1],
[2]]) ❷
m3 = tf.constant([ [[1,2],
[3,4],
[5,6]],
[[7,8],
[9,10],
[11,12]] ]) ❸
print(m1) ❹
print(m2) ❹
print(m3) ❹
❶ 定义一个 2 × 1 的 2 阶矩阵
❷ 定义一个 1 × 2 的 2 阶矩阵
❸ 定义一个 3 阶张量
❹ 尝试打印张量。
运行列表 2.3 生成以下输出:
Tensor( "Const:0",
shape=TensorShape([Dimension(1), Dimension(2)]),
dtype=float32 )
Tensor( "Const_1:0",
shape=TensorShape([Dimension(2), Dimension(1)]),
dtype=int32 )
Tensor( "Const_2:0",
shape=TensorShape([Dimension(2), Dimension(3), Dimension(2)]),
dtype=int32 )
如你所见,每个张量都由一个恰如其分的 Tensor 对象表示。每个 Tensor 对象都有一个唯一的标签(name),一个定义其结构的维度(shape),以及一个数据类型(dtype),用于指定你将操作的价值类型。因为你没有明确提供名称,库自动生成了名称:Const:0、Const_1:0 和 Const_2:0。
张量类型
注意到 m1 的每个元素都以小数点结尾。小数点告诉 Python,元素的类型不是整数,而是浮点数。你可以传递显式的 dtype 值。与 NumPy 数组类似,张量采用一个数据类型,该数据类型指定了你将在该张量中操作的价值类型。
TensorFlow 还提供了一些方便的构造函数来创建一些简单的张量。例如,构造函数 tf.zeros(shape) 会创建一个所有值初始化为 0 的特定形状的张量,例如 [2, 3] 或 [1, 2]。同样,tf.ones(shape) 会创建一个所有值初始化为 1 的特定形状的张量。shape 参数是一个一维(1D)的 int32 类型(整数列表)张量,描述了张量的维度。
练习 2.1
你会如何初始化一个所有元素都等于 0.5 的 500 × 500 张量?
答案
tf.ones([500,500]) * 0.5
2.3 创建操作符
现在你已经准备好了一些起始张量,可以应用更多有趣的操作符,例如加法和乘法。考虑矩阵的每一行代表向另一个人(正数)或从另一个人(负数)转账的交易。取反矩阵是表示另一个人资金流动交易历史的一种方式。让我们从简单的操作开始,对列表 2.3 中的 m1 张量执行取反操作(简称 operation)。取反矩阵会将正数转换为相同大小的负数,反之亦然。
取反是最简单的操作之一。如列表 2.4 所示,取反只接受一个张量作为输入,并产生每个元素都被取反的张量。尝试运行代码。如果你掌握了定义取反的技能,你可以将这项技能推广到所有其他 TensorFlow 操作中。
note 定义操作,例如取反,与运行它是不同的。到目前为止,你已经定义了操作应该如何表现。在第 2.4 节中,你将评估(或运行)它们以计算它们的值。
列表 2.4 使用取反操作符
import tensorflow as tf
x = tf.constant([[1, 2]]) ❶
negMatrix = tf.negative(x) ❷
print(negMatrix) ❸
❶ 定义一个任意张量
❷ 取反张量
❸ 打印对象
列表 2.4 生成以下输出:
Tensor("Neg:0", shape=TensorShape([Dimension(1), Dimension(2)]), dtype=int32)
注意,输出不是 [[-1, -2]],因为你打印的是取反操作的定义,而不是操作的实际评估。打印的输出显示取反操作是一个具有名称、形状和数据类型的 Tensor 类。名称是自动分配的,但你在使用列表 2.4 中的 tf.negative 操作时也可以明确提供它。同样,形状和数据类型是从你传递的 [[1, 2]] 推断出来的。
有用的 TensorFlow 操作符
在 github.com/tensorflow/docs/tree/r1.15/site/en/api_docs/python/tf/math 的官方文档中,仔细列出了所有可用的数学操作符。常用操作符的特定示例包括以下内容:
tf.add(x, y)—将相同类型的两个张量相加,x + y
tf.subtract(x, y)—从相同类型的张量中减去,x - y
tf.multiply(x, y)—逐元素乘以两个张量
tf.pow(x, y)—取元素 x 的 y 次幂tf.exp(x)—相当于 pow(e, x),其中 e 是欧拉数(2.718 ...)`
tf.sqrt(x)—相当于 pow(x, 0.5)
tf.div(x, y)—取 x 和 y 的逐元素除法
tf.truediv(x, y)—与 tf.div 相同,但将参数转换为浮点数
tf.floordiv(x, y)—与 truediv 相同,但将最终答案向下舍入为整数
tf.mod(x, y)—取除法的逐元素余数
练习 2.2
使用你迄今为止学到的 TensorFlow 操作符来生成高斯分布(也称为正态分布)。参见图 2.3 以获取提示。为了参考,你可以在网上找到正态分布的概率密度:en.wikipedia.org/wiki/Normal_distribution。
答案
大多数数学表达式——如 ×、-、+ 等等——都是其 TensorFlow 等价的快捷方式,用于简洁。高斯函数包含许多操作,因此使用以下简写符号更干净:
from math import pi
mean = 0.0
sigma = 1.0
(tf.exp(tf.negative(tf.pow(x - mean, 2.0) /
(2.0 * tf.pow(sigma, 2.0) ))) *
(1.0 / (sigma * tf.sqrt(2.0 * pi) )))
2.4 在会话中执行操作符
会话是软件系统的一个环境,它描述了代码应该如何运行。在 TensorFlow 中,会话设置硬件设备(如 CPU 和 GPU)之间如何通信。这样,您可以在不担心微管理运行其上的硬件的情况下设计您的机器学习算法。稍后,您可以配置会话以更改其行为,而无需更改机器学习代码中的一行。
要执行一个操作并检索其计算值,TensorFlow 需要一个会话。只有注册的会话才能填充 Tensor 对象的值。为此,您必须使用 tf.Session() 创建会话类,并告诉它运行一个操作,如列表 2.5 所示。结果将是一个您可以用作后续计算的值。
列表 2.5 使用会话
import tensorflow as tf
x = tf.constant([[1., 2.]]) ❶
neg_op = tf.negative(x) ❷
with tf.Session() as sess: ❸
result = sess.run(negMatrix) ❹
print(result) ❺
❶ 定义一个任意矩阵
❷ 在其上运行否定操作
❸ 启动会话以便能够运行操作
❹ 告诉会话评估 negMatrix
❺ 打印出结果矩阵
恭喜!您已经编写了您的第一个完整的 TensorFlow 代码。尽管这段代码所做的只是将矩阵取反以产生 [[−1, −2]],但其核心开销和框架与 TensorFlow 中的其他所有内容相同。会话不仅配置了代码将在您的机器上计算的位置,而且还构建了如何将计算布局以并行化计算。
代码性能似乎有点慢
您可能已经注意到运行代码比您预期的多花了几秒钟。TensorFlow 对一个小矩阵取反需要几秒钟看起来可能不太自然。但为了优化库以适应更大、更复杂的计算,实际上进行了大量的预处理。
每个 Tensor 对象都有一个 eval() 函数来评估定义其值的数学操作。但 eval() 函数需要定义一个会话对象,以便库了解如何最好地使用底层硬件。在列表 2.5 中,我们使用了 sess.run(...),这在会话的上下文中相当于调用 Tensor 的 eval() 函数。
当您通过交互式环境(用于调试或演示目的或使用 Jupyter,如本章后面所述)运行 TensorFlow 代码时,在交互模式下创建会话通常更容易,在这种情况下,会话隐式地成为对 eval() 的任何调用的部分。这样,会话变量就不需要在代码中传递,这使得更容易关注算法的相关部分,如列表 2.6 所示。
列表 2.6 使用交互式会话模式
import tensorflow as tf
sess = tf.InteractiveSession() ❶
x = tf.constant([[1., 2.]]) ❷
negMatrix = tf.negative(x) ❷
result = negMatrix.eval() ❸
print(result) ❹
sess.close() ❺
❶ 启动一个交互式会话,这样 sess 变量就不再需要传递
❷ 定义一个任意矩阵并对其取反
❸ 现在您可以在不明确指定会话的情况下评估 negMatrix。
❹ 打印出否定后的矩阵
❺ 记得关闭会话以释放资源。
2.5 理解代码作为图
考虑一位医生预测新生儿的预期体重为 7.5 磅。你可能想弄清楚这个预测与实际测量体重的差异。作为一个过于分析的工程师,你设计了一个函数来描述新生儿所有可能体重的可能性。例如,8 磅的体重比 10 磅更有可能。
你可以选择使用高斯(也称为正态)概率分布函数。该函数接受一个数字作为输入,并输出一个非负数,描述观察输入的概率。这个函数在机器学习中经常出现,并且在 TensorFlow 中很容易定义。它使用乘法、除法、否定以及其他一些基本算子。
将每个算子想象成图中的一个节点。每当看到加号(+)或任何数学概念时,想象它是众多节点之一。这些节点之间的边代表数学函数的组合。具体来说,我们一直在研究的negative(否定)算子是一个节点,该节点的输入/输出边表示Tensor的转换。张量在图中流动,这就是为什么这个库被称为 TensorFlow。
这里有一个想法:每个算子都是一个强类型函数,它接受一个具有特定维度的输入张量,并产生相同维度的输出。图 2.3 展示了如何使用 TensorFlow 设计高斯函数。该函数以图的形式表示,其中算子是节点,边表示节点之间的交互。整个图代表一个复杂的数学函数(具体来说,是高斯函数)。图的小部分代表简单的数学概念,例如否定和加倍。

图 2.3 该图表示生成高斯分布所需的操作。节点之间的链接表示数据如何从一个操作流向下一个操作。操作本身很简单;复杂性来自于它们交织的方式。
TensorFlow 算法易于可视化。它们可以通过流程图简单地描述。这种流程图的术语是数据流图。数据流图中的每条箭头称为边。此外,数据流图中的每个状态都称为节点。会话的目的是将你的 Python 代码解释为数据流图,并将图中每个节点的计算与 CPU 或 GPU 关联起来。
2.5.1 设置会话配置
你还可以向tf.Session传递选项。TensorFlow 会自动确定将 GPU 或 CPU 设备分配给操作的最佳方式,例如,根据可用性。在创建会话时,你可以传递一个额外的选项,log_device_placement=True。列表 2.7 显示了计算在硬件上的确切位置。
列表 2.7 记录会话
import tensorflow as tf
x = tf.constant([[1., 2.]]) ❶
negMatrix = tf.negative(x) ❶
with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as sess: ❷
options = tf.RunOptions(output_partition_graphs=True)
metadata = tf.RunMetadata()
result = sess.run(negMatrix,options=options, run_metadata=metadata) ❸
print(result) ❹
print(metadata.partition_graphs) ❺
❶ 定义一个矩阵并取其相反数
❷ 使用特殊配置启动会话,该配置通过构造函数传入以启用日志记录
❸ 评估 negMatrix
❹ 打印出结果值
❺ 打印出结果图
此代码输出有关每个操作在会话中使用的 CPU/GPU 设备的信息。运行列表 2.7 会产生如下输出跟踪,以显示用于运行否定操作的设备:
Neg: /job:localhost/replica:0/task:0/cpu:0
在 TensorFlow 代码中,会话是必不可少的。你需要调用会话来“运行”数学运算。图 2.4 概述了 TensorFlow 组件如何与机器学习流程交互。会话不仅运行图操作,还可以接受占位符、变量和常量作为输入。到目前为止,我们已使用常量,但在后面的章节中,我们将开始使用变量和占位符。以下是这三种类型值的快速概述:
-
占位符 —一个未分配但将在会话中初始化的值。通常,占位符是模型的输入和输出。
-
变量 —一个可以改变的值,例如机器学习模型的参数。变量在使用之前必须由会话初始化。
-
常量 —一个不会改变的值,例如超参数或设置。
TensorFlow 中机器学习的整个流程遵循图 2.4 的流程。TensorFlow 中的大部分代码都是设置图和会话。在你设计好图并将会话连接以执行它之后,你的代码就可以使用了。

图 2.4 会话决定了如何最有效地使用硬件来处理图。当会话开始时,它会为每个节点分配 CPU 和 GPU 设备。处理完毕后,会话以可用的格式输出数据,例如 NumPy 数组。会话可以选择性地提供占位符、变量和常量。
2.6 在 Jupyter 中编写代码
由于 TensorFlow 主要是一个 Python 库,你应该充分利用 Python 的解释器。Jupyter是一个成熟的交互式环境。它是一个网络应用程序,可以优雅地显示计算结果,这样你就可以与他人分享带有注释的交互式算法,以教授一种技术或演示代码。Jupyter 还可以轻松地与可视化库如 Python 的 Matplotlib 集成,并可用于分享关于你的算法的优雅数据故事,评估其准确性,并展示结果。
你可以将你的 Jupyter 笔记本与他人分享以交换想法,你也可以下载他们的笔记本来了解他们的代码。请参阅附录以开始安装 Jupyter Notebook 应用程序。
从一个新的终端开始,将目录更改为你想要练习 TensorFlow 代码的位置,并启动笔记本服务器:
$ cd ~/MyTensorFlowStuff
$ jupyter notebook
运行此命令应该会打开一个新浏览器窗口,显示 Jupyter 笔记本的仪表板。如果没有窗口自动打开,你可以从任何浏览器导航到 http://localhost:8888。你将看到一个类似于图 2.5 的网页。

图 2.5 运行 Jupyter 笔记本将在 http://localhost:8888 上启动一个交互式笔记本。
提示:如果 jupyter notebook 命令没有工作?请确保你的 PYTHONPATH 环境变量包含了安装库时创建的 jupyter 脚本的路径。此外,本书使用了 Python 3.7(推荐)和 Python 2.7 的示例(由于 BregmanToolkit,你将在第七章中了解到)。因此,你将需要安装带有 Python 内核的 Jupyter。更多信息,请参阅 ipython.readthedocs.io/en/stable/install/kernel_install.html。
通过点击右上角的“新建”下拉菜单创建一个新的笔记本;然后选择 Notebooks > Python 3。新的 Python3 内核被启用,并且默认情况下是唯一选项,因为 Python 2 自 2020 年 1 月 1 日起已弃用。此命令创建了一个名为 Untitled.ipynb 的新文件,你可以通过浏览器界面立即开始编辑。你可以通过点击当前 Untitled 名称并输入一些更有记忆性的名称来更改笔记本的名称,例如 TensorFlow 示例笔记本。当你查看代码列表时使用的约定是简单地给笔记本命名 Listing
Jupyter 笔记本中的每一项都是一个独立的代码块或文本块,称为 cell。单元格有助于将长块代码分成可管理的代码片段和文档。你可以单独运行单元格,或者选择按顺序一次性运行所有单元格。有三种常见的评估单元格的方法:
-
在单元格中按下 Shift-Enter 将执行单元格并突出显示其下方的单元格。
-
按下 Ctrl-Enter 将光标保持在当前单元格执行后。
-
按下 Alt-Enter 将执行单元格并在其下方插入一个新空单元格。
你可以通过点击工具栏中的下拉菜单更改单元格类型,如图 2.6 所示。或者,你可以按 Esc 离开编辑模式,使用箭头键突出显示一个单元格,然后按 Y(用于代码模式)或 M(用于 Markdown 模式)。

图 2.6 下拉菜单更改笔记本中的单元格类型。代码单元格用于 Python 代码,而 Markdown 代码用于文本描述。
最后,你可以创建一个 Jupyter 笔记本,优雅地展示 TensorFlow 代码,如图 2.7 所示,通过交织代码和文本单元格。
练习 2.3
如果你仔细观察图 2.7,你会注意到它使用 tf.neg 而不是 tf .negative。这很奇怪。你能解释一下我们为什么可能那样做吗?
答案
你应该意识到 TensorFlow 库改变了命名约定,你可能会在遵循在线旧 TensorFlow 教程时遇到这些遗留下来的问题。

图 2.7 一个交互式 Python 笔记本展示了为了可读性而分组排列的代码和注释。
然而,人们在使用 Jupyter 时常常犯的一个错误是过度依赖它来进行一些可以使用 TensorFlow 完成的更复杂的机器学习任务。Jupyter 使得与 Python 代码和 TensorFlow 的交互变得愉快,但它不能替代你需要编写代码并长时间运行(数小时、数天甚至数周)“点火后忘记”的训练。在这些情况下,我们建议将你的笔记本保存;使用 Jupyter 的“另存为 Python 文件”功能(可在文件菜单中找到);然后从命令行运行保存的 Python 文件,使用 Python 解释器,在 tmux 或 screen 中运行。这些命令行实用程序允许你的当前交互会话在您注销后继续运行,并允许您稍后回来检查命令的状态,仿佛您从未离开过会话。这些工具是 UNIX 工具,但通过 Cygwin 和虚拟机,它们在 Windows 上也能工作。正如你将在后面的章节中学到的那样,特别是当使用 TensorFlow 的会话 API 优雅地执行分布式、多 GPU 训练时,如果你的代码仅存在于 Jupyter 笔记本中,你会陷入困境。笔记本环境将你绑定到特定的运行时间,可能无意中关闭(尤其是在超级计算机上),或者可能在使用数天后冻结或锁定,因为如果让 Jupyter 运行一段时间,它可能会消耗大量内存。
TIP 定期访问 Jupyter 的主屏幕,寻找那些正在运行(绿色)但不再需要的笔记本,选择这些笔记本,然后点击顶部附近的关机按钮以释放内存。你的电子邮件、网页浏览和其他活动都会感谢你!
2.7 使用变量
使用 TensorFlow 常量是一个好的开始,但大多数有趣的应用都需要数据发生变化。例如,神经科学家可能对从传感器测量中检测神经活动感兴趣。神经活动的峰值可能是一个随时间变化的布尔变量。为了在 TensorFlow 中捕捉这种活动,你可以使用 Variable 类来表示一个随时间变化的节点值。
机器学习中变量对象的使用示例
找到最佳拟合多个点的直线方程是机器学习中的一个经典问题,在第三章中进行了更详细的讨论。算法从一个初始猜测开始,这个猜测是一个由几个数字(如斜率或 y 截距)特征化的方程。随着时间的推移,算法会生成越来越好的这些数字的猜测,这些数字也被称为参数。
到目前为止,我们只操作了常量。只有常量的程序对于现实世界的应用并不那么有趣,因此 TensorFlow 允许更丰富的工具,如变量,它们是可能随时间变化的值的容器。机器学习算法更新模型的参数,直到找到每个变量的最优值。在机器学习的世界里,参数通常会波动,直到最终稳定下来,这使得变量成为它们的优秀数据结构。
列表 2.8 中的代码是一个简单的 TensorFlow 程序,演示了如何使用变量。它会在序列数据突然增加时更新变量。想象一下记录神经元活动随时间的变化。这段代码可以检测神经元活动突然增加的情况。当然,这个算法是为了教学目的而简化的。
首先导入 TensorFlow。TensorFlow 允许你使用tf.InteractiveSession()声明一个会话。当你声明了一个交互式会话后,TensorFlow 函数不需要它们通常需要的会话属性,这使得在 Jupyter 笔记本中编码更加容易。
列表 2.8 使用变量
import tensorflow as tf
sess = tf.InteractiveSession() ❶
raw_data = [1., 2., 8., -1., 0., 5.5, 6., 13] ❷
spike = tf.Variable(False) ❸
spike.initializer.run() ❹
for i in range(1, len(raw_data)): ❺
if raw_data[i] - raw_data[i-1] > 5:
updater = tf.assign(spike, True) ❻
updater.eval() ❻
else:
tf.assign(spike, False).eval()
print("Spike", spike.eval())
sess.close() ❼
❶ 以交互模式启动会话,这样你就不需要传递 sess
❸ 假设你有一些原始数据如下。
❸ 创建一个名为 spike 的布尔变量来检测一系列数字的突然增加
❹ 因为所有变量都必须初始化,所以通过在其初始化器上调用 run()来初始化变量。
❺ 遍历数据(跳过第一个元素),并在发生显著增加时更新 spike 变量
❻ 要更新一个变量,给它分配一个新的值,使用 tf.assign(,
❷ 记得在不再使用会话后关闭它。
列表 2.8 的预期输出是随时间变化的 spike 值列表:
('Spike', False)
('Spike', True)
('Spike', False)
('Spike', False)
('Spike', True)
('Spike', False)
('Spike', True)
2.8 保存和加载变量
想象一下编写一个庞大的代码块,你希望单独测试其中的一小段。在复杂的机器学习情况下,在已知检查点保存和加载数据可以使调试代码变得更加容易。TensorFlow 提供了一个优雅的接口来保存和加载变量值到磁盘;让我们看看如何使用它来达到这个目的。
你将修改在列表 2.8 中创建的代码,以便将尖峰数据保存到磁盘上,这样你就可以在其他地方加载它。你将把 spike 变量从简单的布尔值改为布尔向量,以捕获尖峰的历史(列表 2.9)。请注意,你将明确命名变量,以便以后可以用相同的名称加载它们。命名变量是可选的,但强烈建议这样做以组织代码。在本书的后面部分,特别是在第十四章和第十五章中,你还将使用 tf.identity 函数来命名变量,以便在恢复保存的模型图时可以引用它。
尝试运行此代码以查看结果。
列表 2.9 保存变量
import tensorflow as tf ❶
sess = tf.InteractiveSession() ❶
raw_data = [1., 2., 8., -1., 0., 5.5, 6., 13] ❷
spikes = tf.Variable([False] * len(raw_data), name='spikes') ❸
spikes.initializer.run() ❹
saver = tf.train.Saver() ❺
for i in range(1, len(raw_data)): ❻
if raw_data[i] - raw_data[i-1] > 5: ❻
spikes_val = spikes.eval() ❼
spikes_val[i] = True ❼
updater = tf.assign(spikes, spikes_val) ❼
updater.eval() ❽
save_path = saver.save(sess, "spikes.ckpt") ❾
print("spikes data saved in file: %s" % save_path) ❿
sess.close()
❶ 导入 TensorFlow 并启用交互式会话
❷ 假设你有一系列这样的数据。
❸ 定义一个名为 spikes 的布尔向量,用于定位原始数据中的突然尖峰
❹ 不要忘记初始化变量。
❺ 保存器 op 将启用保存和恢复变量。如果构造函数中没有传递字典,则保存当前程序中的所有变量。
❻ 遍历数据,并在发生显著增加时更新 spikes 变量。
❼ 使用 tf.assign 函数更新 spikes 的值
❽ 不要忘记评估更新器;否则,spikes 不会更新。
❾ 将变量保存到磁盘
❿ 打印保存变量的相对文件路径
你会注意到在源代码相同的目录中生成了几个文件——其中一个是 spikes.ckpt——这是一个紧凑存储的二进制文件,因此你无法用文本编辑器轻松修改它。要检索这些数据,你可以使用 saver op 中的 restore 函数,如列表 2.10 所示。
列表 2.10 加载变量
import tensorflow as tf
sess = tf.InteractiveSession()
spikes = tf.Variable([False]*8, name='spikes') ❶
spikes.initializer.run() ❷
saver = tf.train.Saver() ❸
saver.restore(sess, "./spikes.ckpt") ❹
print(spikes.eval()) ❺
sess.close()
❶ 创建一个与保存数据大小和名称相同的变量
❷ 你不再需要初始化这个变量,因为它将直接加载。
❸ 创建保存器 op 以恢复保存的数据
❹ 从 spikes.ckpt 文件中恢复数据
❺ 打印加载的数据
列表 2.10 的预期输出是包含你数据中尖峰的 Python 列表,如下所示。第一条消息只是 TensorFlow 告诉你它正在从检查点文件 spikes.ckpt 加载模型图和相关参数(在本书的后面,我们将这些称为权重):
INFO:tensorflow:Restoring parameters from ./spikes.ckpt
[False False True False False True False True]
2.9 使用 TensorBoard 可视化数据
在机器学习中,最耗时的部分不是编程;而是等待代码运行完成,除非你使用提前停止或看到过拟合并希望终止你的模型训练过程。例如,一个著名的数据集 ImageNet 包含了超过 1400 万张图像,这些图像准备用于机器学习环境。有时,使用大型数据集训练一个算法可能需要几天或几周的时间。TensorFlow 的便捷仪表板 TensorBoard 允许你快速查看图中每个节点值的改变情况,从而让你对代码的执行情况有一个大致的了解。
让我们看看如何在现实世界的例子中可视化变量随时间的变化趋势。在本节中,你将在 TensorFlow 中实现移动平均算法;然后你将仔细跟踪你关心的变量,以便在 TensorBoard 中进行可视化。
你想知道这本书为什么会有第二版吗?
这本书存在的一个关键原因是前面的斜体文本(有时,使用大数据集训练算法可能需要几天或几周的时间)。在这本书中为这种体验做好准备。本书后面,我将重新创建一些著名的(读:大型)模型,包括用于面部识别的 VGG-Face 模型,以及使用自然语言处理中的所有 Netflix 评论数据生成情感分析模型。在这种情况下,为 TensorFlow 在你的笔记本电脑上运行一整夜或在你可访问的超级计算机上运行几天做好准备。别担心;我在这里指导你,并成为你的情感支持者!
2.9.1 实现移动平均
在本节中,你将使用 TensorBoard 来可视化数据的变化。假设你对计算一家公司的平均股价感兴趣。通常,计算平均值是添加所有值然后除以总数的问题:mean = (x[1] + x[2] + ... + x[n]) / n。当值的总数未知时,你可以使用称为 指数平均 的技术来估计未知数量数据点的平均值。指数平均算法将当前估计的平均值计算为先前估计的平均值和当前值的一个函数。
更简洁地说,Avg[t] = f (Avg[t - 1], x[t]) = (1 - α) Avg[t - 1] + α x[t]。Alpha (α) 是一个将被调整的参数,表示在平均计算中最近值应该有多大的偏差。α的值越高,计算的平均值与先前估计的平均值之间的差异就越大。图 2.8(在本书后面的列表 2.15 之后显示)展示了 TensorBoard 如何随时间可视化值和相应的运行平均值。
当你编写这个移动平均的代码时,考虑每个迭代中发生的主要计算部分是个好主意。在这种情况下,每个迭代将计算 Avg[t] = (1 - α) Avg[t - 1] + α x[t]。因此,你可以设计一个 TensorFlow 操作符(列表 2.11),它正好按照公式执行。要运行此代码,你最终必须定义 alpha、curr_value 和 prev_avg。
列表 2.11 定义平均更新操作符
update_avg = alpha * curr_value + (1 - alpha) * prev_avg ❶
❶ alpha 是一个 tf.constant,curr_value 是一个占位符,prev_avg 是一个变量。
你将在稍后定义未定义的变量。你之所以以这种方式编写代码,是因为首先定义接口迫使你必须实现外围设置代码以满足接口。跳过前面的部分,让我们直接跳到会话部分,看看你的算法应该如何表现。列表 2.12 设置了主要循环并在每次迭代中调用 update_avg 操作符。运行 update_avg 操作符取决于 curr_value,它通过 feed_dict 参数提供。
列表 2.12 运行指数平均算法的迭代
raw_data = np.random.normal(10, 1, 100)
with tf.Session() as sess:
for i in range(len(raw_data)):
curr_avg = sess.run(update_avg, feed_dict={curr_value:raw_data[i]}
sess.run(tf.assign(prev_avg, curr_avg))
很好。整体图景很清晰,剩下要做的就是写出未定义的变量。让我们填补空白并实现一段可工作的 TensorFlow 代码。复制列表 2.13 以便你可以运行它。
列表 2.13 填充缺失的代码以完成指数平均算法
import tensorflow as tf
import numpy as np
raw_data = np.random.normal(10, 1, 100) ❶
alpha = tf.constant(0.05) ❷
curr_value = tf.placeholder(tf.float32) ❸
prev_avg = tf.Variable(0.) ❹
update_avg = alpha * curr_value + (1 - alpha) * prev_avg
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for i in range(len(raw_data)): ❺
curr_avg = sess.run(update_avg, feed_dict={curr_value: raw_data[i]})
sess.run(tf.assign(prev_avg, curr_avg))
print(raw_data[i], curr_avg)
❶ 创建一个具有 100 个数字的向量,均值为 10,标准差为 1
❷ 将 alpha 定义为一个常数
❸ 占位符就像一个变量,但其值是从会话中注入的。
❹ 将前一个平均值初始化为零
❺ 逐个遍历数据以更新平均值
2.9.2 可视化移动平均
现在你已经实现了一个移动平均算法的工作版本,让我们使用 TensorBoard 来可视化结果。使用 TensorBoard 的可视化通常是一个两步过程:
-
通过使用 总结操作 注释你关心的节点来挑选出你想要测量的节点。
-
在它们上调用
add_summary以将数据排队到磁盘上。
假设你有一个 img 占位符和一个 cost 操作符,如列表 2.14 所示。你可以注释它们(通过给每个一个名称,如 img 或 cost),这样它们就可以在 TensorBoard 中进行可视化。你将在你的移动平均示例中做类似的事情。
列表 2.14 使用总结操作进行注释
img = tf.placeholder(tf.float32, [None, None, None, 3])
cost = tf.reduce_sum(...)
my_img_summary = tf.summary.image("img", img)
my_cost_summary = tf.summary.scalar("cost", cost)
更普遍地,为了与 TensorBoard 通信,你必须使用总结操作,它产生由 SummaryWriter 用来保存更新到目录的序列化字符串。每次你从 SummaryWriter 调用 add_summary 方法时,TensorFlow 都会将数据保存到磁盘上供 TensorBoard 使用。
警告 请注意不要频繁调用 add_summary 函数!虽然这样做会产生更高分辨率的变量可视化,但代价是更多的计算和略微缓慢的学习。
运行以下命令在与此源代码相同的文件夹中创建一个名为 logs 的目录:
$ mkdir logs
使用传递给参数的日志目录位置运行 TensorBoard:
$ tensorboard —logdir=./logs
打开浏览器,导航到 http://localhost:6006,这是 TensorBoard 的默认 URL。列表 2.15 展示了如何将 SummaryWriter 连接到你的代码。运行它,并刷新 TensorBoard 以查看可视化。
列表 2.15 将总结写入以在 TensorBoard 中查看
import tensorflow as tf
import numpy as np
raw_data = np.random.normal(10, 1, 100)
alpha = tf.constant(0.05)
curr_value = tf.placeholder(tf.float32)
prev_avg = tf.Variable(0.)
update_avg = alpha * curr_value + (1 - alpha) * prev_avg
avg_hist = tf.summary.scalar("running_average", update_avg) ❶
value_hist = tf.summary.scalar("incoming_values", curr_value) ❷
merged = tf.summary.merge_all() ❸
writer = tf.summary.FileWriter("./logs") ❹
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
sess.add_graph(sess.graph) ❺
for i in range(len(raw_data)):
summary_str, curr_avg = sess.run([merged, update_avg],
➥ feed_dict={curr_value: raw_data[i]}) ❻
sess.run(tf.assign(prev_avg, curr_avg))
print(raw_data[i], curr_avg)
writer.add_summary(summary_str, i) ❼
❶ 为平均值创建一个总结节点
❷ 为值创建一个总结节点
❸ 合并总结以使它们更容易同时运行
❹ 将日志目录的位置传递给 writer
❺ 可选,但允许您在 TensorBoard 中可视化计算图
❻ 同时运行合并操作和更新平均操作
❼ 将摘要添加到 writer 中
小贴士:在启动 TensorBoard 之前,您可能需要确保 TensorFlow 会话已经结束。如果您重新运行列表 2.15,您需要记得清除日志目录。

图 2.8 列表 2.15 中创建的 TensorBoard 的摘要显示。TensorBoard 为可视化 TensorFlow 生成数据提供了一个用户友好的界面。
2.10 将所有内容整合:TensorFlow 系统架构和 API
我并没有深入探讨 TensorFlow 能做的一切——本书的其余部分将涉及这些主题——但我已经展示了其核心组件及其之间的接口。这些组件和接口的集合构成了 TensorFlow 架构,如图 2.9 所示。
TensorFlow 2 版本的列表包含了新特性,包括始终急切执行和优化器及训练的更新包名。新的列表在 Python 3 中运行良好;如果您尝试了它们,欢迎您对这些列表提供反馈。您可以在github.com/chrismattmann/MLwithTensorFlow2ed/tree/master/TFv2找到 TensorFlow 2 的列表代码。

图 2.9 TensorFlow 系统架构和 API。迄今为止,我们已经花费了大量时间在会话、仪器、模型表示和训练上。本书的后续部分将逐步探讨数学、用于人脸识别的复杂模型——卷积神经网络(CNNs)、优化以及系统的其他部分。
跟随版本:TF2 及以后
本书以 TensorFlow 1.x 系列的两个版本为标准。1.x 系列中的最新版本 1.15 与 Python 3 兼容良好。在第七章和第十九章中,您将阅读到一些需要 Python 2 的示例;因此,需要 TensorFlow 1.14。
此外,在本书开发期间,还发布了针对 TensorFlow 2 的完整列表和代码的移植。(详见附录中的所有详细信息。)您会注意到,在 TensorFlow 2 中运行的列表代码中有 85-90%是相同的。主要原因在于数据清洗、收集、准备和评估代码可以完全重用,因为它使用了伴随的 ML 库,如 Scikit 和 Matplotlib。
选择一个框架版本并坚持使用它之所以如此困难,一个原因就是软件变化如此之快。本书的作者在尝试运行第一版中的某些列表时发现了这一点。尽管概念、架构和系统本身保持不变,并且接近实际运行的版本,即使在两年后,TensorFlow 也发生了很大变化,自 TensorFlow 1.0 以来已发布了 20 多个版本,目前是 1.15.2 版本——值得注意的是,最后的 1.x 版本。这些变化的部分原因与系统 1.x 版本的破坏性变化有关,但其他部分则与通过执行每章末尾建议的示例获得的一些更根本的架构理解有关,摸索前行,然后意识到 TensorFlow 有代码和接口来处理问题。正如谷歌应用人工智能负责人、TensorFlow 大师 Scott Penberthy 在前言中所言,追逐 TensorFlow 版本并不是重点;细节、架构、清理步骤、处理和评估技术将经受时间的考验,而伟大的软件工程师将不断改进围绕张量的框架。
今天,TensorFlow 2.0 正在吸引众多关注,但与其追逐最新的版本,这个版本与 1.x 版本相比有一些根本性的(破坏性)变化,我更希望传达一种超越(破坏性)变化的核心理解,并确立机器学习的基础以及使 TensorFlow 如此特别的概念,不受版本的影响。这本书的版本在很大程度上是由于许多不眠之夜,在机器学习概念(如模型构建、估计和张量数学)中摸索前行,这些是 TensorFlow 架构的核心元素。
其中一些概念在图 2.9 中展示。我们在本章中花费了大量时间讨论会话的力量,如何表示张量和图,以及如何训练模型、将其保存到磁盘并恢复。但我还没有涵盖如何进行预测,如何构建更复杂的图(如回归、分类、CNN 和 RNN),以及如何在千兆和太字节的数据上运行 TensorFlow。所有这些任务都是可能的,因为谷歌和开发者花费了大量时间思考你在进行大规模机器学习时可能遇到的挑战。
这些挑战中的每一个都是 TensorFlow 架构中的灰色盒子,这意味着它是一个具有易于使用的编程语言 APIs 和大量文档和支持的组件。当某些事情不清楚时,这就是我在这里的原因。本书的其余部分将解决所有这些灰色盒子。这难道不是客户服务吗?
摘要
-
你应该用计算流程图的方式来思考数学算法。当你把节点看作操作,把边看作数据流时,编写 TensorFlow 代码就变得非常简单。定义好你的图之后,在会话中评估它,你就能得到结果。
-
毫无疑问,TensorFlow 不仅仅能将计算表示为图。正如你将在接下来的章节中看到的,一些内置函数是为机器学习领域量身定制的。实际上,TensorFlow 在 CNNs 方面提供了最好的支持,CNNs 是一种流行的图像处理模型(在音频和文本处理方面也取得了有希望的结果)。
-
TensorBoard 提供了一种简单的方式来可视化 TensorFlow 代码中数据的变化方式,以及通过检查数据趋势来调试错误。
-
TensorFlow 与 Jupyter Notebook 应用配合得非常好,这是一个优雅的交互式媒介,用于分享和记录 Python 代码。
-
本书以 TensorFlow 1.15 和 1.14 为标准,分别针对 Python 3 和 2,我致力于通过讨论系统中的所有组件和接口来展示架构的强大功能。书中还努力将代码示例移植到 TensorFlow 2,在本书的 GitHub 代码仓库的一个分支中。尽可能的情况下,本书以能够抵御 API 级别变化的方式映射概念。详情请见附录。
第二部分 核心学习算法
学习归根结底是查看过去数据并以有意义的方式预测其未来的值。当数据是连续的,如股价或呼叫中心的呼叫量时,我们称之为回归预测。如果我们预测的是特定离散类别的事物,比如图像是狗、鸟还是猫,我们称之为分类预测。分类不仅适用于图像;你可以对各种事物进行分类,包括决定文本是积极的还是消极的。
有时候,你只是希望数据中出现自然模式,这些模式允许你对其进行分组,例如具有相关属性(如一大块音频文件中的所有咳嗽声)的聚类数据,甚至可以提供有关其所有者正在做什么类型活动的手机数据——走路、交谈等等。
你并不总能观察到事件发生的直接原因,这使得预测一切变得具有挑战性。以天气为例。即使我们有先进的建模能力,预测某一天是雨天、晴天还是多云的概率仍然是 50/50。原因是,有时雨、太阳或云是观察到的输出,但真正的因果关系是隐藏的,无法直接观察到。我们称那些可以根据隐藏因果关系预测结果的模型为马尔可夫模型。这些模型是一种出色的可解释技术,可以是您机器学习工具包的一部分,用于天气以及所有其他事情,例如自动从书籍中读取大量文本,然后区分一个词是名词还是形容词。
这些技术是您的核心学习技术。在本书的这一部分,您将学习如何通过使用 TensorFlow 和其他机器学习工具来部署和应用这些技术,以及如何评估它们的性能。
3 线性回归及其扩展
本章涵盖了
-
将线拟合到数据点上
-
将任意曲线拟合到数据点上
-
测试回归算法的性能
-
将回归应用于实际数据
还记得高中时的科学课程吗?可能已经过去很久了,或者谁知道呢——也许你现在还在高中,早早地开始了机器学习的旅程。无论如何,无论你学习的是生物学、化学还是物理学,分析数据的一个常见技术是绘制一个变量如何影响另一个变量的变化。
想象一下绘制降雨频率与农业生产之间的相关性。你可能观察到降雨量的增加会导致农业生产率的增加。将这些数据点拟合到一条线上,使你能够预测在不同降雨条件下的生产率:稍微少一点雨,稍微多一点雨,等等。如果你从几个数据点中发现了潜在的函数,那么这个学到的函数将使你能够预测未见数据的价值。
回归 是研究如何最佳拟合曲线以总结数据的一种方法,是监督学习算法中最强大、研究最深入的类型之一。在回归中,我们试图通过发现可能生成这些数据点的曲线来理解数据点。在这个过程中,我们寻求解释为什么给定的数据会以这种方式分散。最佳拟合曲线为我们提供了一个模型,解释了数据集可能是如何产生的。
本章将向你展示如何将实际问题用回归来解决。正如你将看到的,TensorFlow 是正确的工具,提供了最强大的预测器之一。
3.1 正式符号
如果你有一把锤子,每个问题看起来都像钉子。本章展示了第一个主要的机器学习工具,回归,并使用精确的数学符号正式定义它。首先学习回归是一个很好的主意,因为你在未来章节中遇到的其他类型的问题中,许多技能都会得到应用。到本章结束时,回归将成为你机器学习工具箱中的“锤子”。
假设你有一些关于人们购买啤酒瓶花费的数据。爱丽丝购买了 2 瓶啤酒,花费了 4 美元,鲍勃购买了 3 瓶啤酒,花费了 6 美元,克莱尔购买了 4 瓶啤酒,花费了 8 美元。你想要找到一个方程来描述瓶数对总成本的影响。如果线性方程 y = 2x 描述了购买特定数量瓶子的成本,例如,你可以计算出每瓶啤酒的成本。
当一条线看起来很好地拟合了一些数据点时,你可能会声称你的线性模型表现良好。但你可以尝试许多可能的斜率,而不仅仅是选择 2 这个值。斜率的选择是 参数,包含参数的方程是 模型。用机器学习的术语来说,最佳拟合曲线的方程来自于学习模型的参数。
作为另一个例子,方程 y = 3x 也是一个直线,只是斜率更陡。你可以用任何实数(让我们称它为 w)替换那个系数,方程仍然会产生一条直线:y = wx。图 3.1 展示了改变参数 w 如何影响模型。以这种方式生成的所有方程的集合表示为 M = {y = wx | w ∈ ℝ},这读作“所有 y = wx 的方程,其中 w 是一个实数。”
M 是所有可能模型的集合。选择一个 w 的值生成候选模型 M(w): y = wx。你将在 TensorFlow 中编写的回归算法将迭代收敛到模型参数 w 的更好值。最优参数,我们称之为 w* (发音为 w star),是最拟合方程 M(w) : y = wx。Best-fit 意味着模型产生的误差或预测值与实际值之间的差异最小,通常称为 ground truth。我们将在本章中更多地讨论这一点。
在最一般的意义上,回归算法试图设计一个函数,我们可以称之为 f,它将输入映射到输出。该函数的定义域是实值向量 ℝ^d,其值域是实数集 ℝ。

图 3.1 参数 w 的不同值导致不同的线性方程。这些所有线性方程的集合构成了线性模型 M。
note 回归也可以有多个输出,而不是一个实数。在这种情况下,我们称之为 多元回归**.
函数的输入可以是连续的或离散的。但输出必须是连续的,如图 3.2 所示。

图 3.2 回归算法旨在产生连续输出。输入可以是离散的或连续的。这种区别很重要,因为离散值输出更适合由分类处理,这在第五章和第六章中讨论。
note 回归预测连续输出,但有时这过于冗余。有时,我们想要预测一个离散输出,例如 0 或 1,中间没有其他值。分类是一种更适合此类任务的技巧,将在第五章中讨论。
我们希望找到一个函数 f,它与给定的数据点(本质上是一组输入/输出对)很好地吻合。不幸的是,可能函数的数量是无限的,所以逐个尝试它们是没有希望的。有太多选择通常是一个坏主意。我们有必要缩小我们想要处理的函数的范围。例如,如果我们只考虑直线来拟合一组数据点,搜索就会变得容易得多。
练习 3.1
有多少种可能的函数可以将 10 个整数映射到 10 个整数?设 f(x)是一个可以接受从 0 到 9 的数字并产生从 0 到 9 的数字的函数。一个例子是恒等函数,它模仿其输入——例如,f(0) = 0,f(1) = 1,以此类推。还有多少其他函数存在?
答案
10¹⁰ = 10,000,000,000
3.1.1 你如何知道回归算法是有效的?
假设你正在尝试向一家房地产公司销售一个房价预测算法。该算法根据诸如卧室数量和地块大小等属性预测房价。房地产公司可以轻易地利用此类信息赚取数百万,但在从你那里购买之前,他们需要一些证明该算法有效的证据。
要衡量学习算法的成功,你需要了解两个重要的概念:
-
方差表示预测对所使用的训练集的敏感度。理想情况下,你选择训练集的方式不应该很重要,这意味着希望方差更低。
-
偏差表示对训练数据集所做假设的强度。做出太多的假设可能会使模型无法泛化,因此你应该偏好低偏差。
如果一个模型过于灵活,它可能会意外地记住训练数据而不是解决有用的模式。你可以想象一个曲线函数穿过数据集的每一个点,看起来没有产生错误。如果发生这种情况,我们说学习算法过拟合了数据。在这种情况下,最佳拟合曲线将与训练数据很好地吻合,但在测试数据上的表现可能非常糟糕(见图 3.3)。

图 3.3 理想情况下,最佳拟合曲线在训练数据和测试数据上都拟合得很好。如果我们看到它与测试数据拟合得不好,而与训练数据拟合得很好,那么我们的模型可能欠拟合。另一方面,如果它在测试数据上表现不佳,但在训练数据上表现良好,我们知道该模型是过拟合的。
迁移学习和过拟合
今天,一个大的过拟合挑战来自于迁移学习的过程:将模型在一个领域学到的知识应用到另一个领域。令人惊讶的是,这个过程在计算机视觉、语音识别和其他领域工作得非常好。但许多迁移学习模型都存在过拟合问题。例如,考虑著名的 MNIST(*修改后的国家标准与技术研究院)数据集和问题,用于识别 1 到 10 的黑白数字。从 MNIST 学到的模型可以应用于其他非黑白数字(如街牌),但需要微调,因为即使是最好的 MNIST 模型通常也会表现出一些过拟合。
在光谱的另一端,一个不太灵活的模型可能对未见过的测试数据泛化得更好,但在训练数据上的得分相对较低。这种情况被称为欠拟合。过于灵活的模型具有高方差和低偏差,而过于严格的模型具有低方差和高偏差。理想情况下,你希望模型同时具有低方差误差和低偏差误差。这样,模型既能泛化到未见过的数据,又能捕捉数据的规律。参见图 3.4,了解模型在二维空间中对数据欠拟合和过拟合的示例。

图 3.4 欠拟合和过拟合数据的示例
具体来说,模型的方差是衡量响应波动程度的一个指标,而偏差是衡量响应偏离真实值程度的一个指标,正如本章前面所讨论的。你希望你的模型实现准确(低偏差)以及可重复(低方差)的结果。
练习 3.2
假设你的模型是 M(w):y = wx。如果权重参数 w 的值必须是介于 0 和 9(包含)之间的整数,你能生成多少个可能的功能?
答案
只有 10 个:{y = 0, y = x, y = 2x, ..., y = 9x}
总结来说,衡量你的模型在训练数据上的表现并不是衡量其泛化能力的一个很好的指标。相反,你应该在单独的一批测试数据上评估你的模型。你可能会发现,你的模型在你训练的数据上表现良好,但在测试数据上表现糟糕,在这种情况下,你的模型很可能是对训练数据过拟合。如果测试错误与训练错误大致相同,并且两个错误相似,那么你的模型可能拟合得很好,或者(如果那个错误很高)欠拟合。
这就是为什么,为了衡量机器学习中的成功,你需要将数据集分成两组:训练数据集和测试数据集。模型使用训练数据集学习,性能在测试数据集上评估。(第 3.2 节描述了如何评估性能。)在你可以生成的许多可能的权重参数中,目标是找到最适合数据的那个。你通过定义一个成本函数来衡量最佳拟合,该函数在第 3.2 节中进行了更详细的讨论。成本函数还可以驱动你将测试数据分成另一个调整成本的参数和一个评估数据集(这是真正的未见数据)。我们将在接下来的章节中进一步解释。
3.2 线性回归
让我们先创建一些假数据,以便深入理解线性回归。创建一个名为 regression.py 的 Python 源文件,并按照列表 3.1 初始化数据。该代码将生成类似于图 3.5 的输出。
列表 3.1 可视化原始输入
import numpy as np ❶
import matplotlib.pyplot as plt ❷
x_train = np.linspace(-1, 1, 101) ❸
y_train = 2 * x_train + np.random.randn(*x_train.shape) * 0.33 ❹
plt.scatter(x_train, y_train) ❺
plt.show() ❺
❶ 导入 NumPy 以帮助生成初始原始数据
❷ 使用 Matplotlib 可视化数据
❸ 输入值是-1 和 1 之间 101 个等间距的数字。
❹ 输出值与输入成正比,但增加了噪声。
❺ 使用 Matplotlib 的函数生成数据的散点图

图 3.5 y = x + (噪声)的散点图
现在你有一些数据点可用,你可以尝试拟合一条线。至少,你需要为 TensorFlow 提供的每个候选参数提供一个分数。这种评分分配通常称为成本函数。成本越高,模型参数就越差。如果最佳拟合线是y = 2x,参数选择 2.01 应该有低成本,但选择-1 应该有更高的成本。
在将情况定义为成本最小化问题后,如图 3.6 所示,TensorFlow 负责内部工作,尝试以高效的方式更新参数,最终达到最佳可能值。遍历所有数据以更新参数的每个步骤称为一个epoch。

图 3.6 任何参数 w 最小化,成本就是最优的。成本定义为理想值与模型响应之间的误差范数。最后,响应值是从模型集中的函数计算得出的。
在这个例子中,你通过误差的总和定义成本。预测x的误差通常通过实际值f(x)与预测值M(w, x)之间的平方差来计算。因此,成本是实际值和预测值之间平方差的和,如图 3.7 所示。

图 3.7 成本是模型响应与真实值之间逐点差异的范数。
更新你的代码,使其看起来像列表 3.2。此代码定义了成本函数并请求 TensorFlow 运行一个优化器来找到模型参数的最佳解决方案。
列表 3.2 解决线性回归
import tensorflow as tf ❶
import numpy as np ❶
import matplotlib.pyplot as plt ❶
learning_rate = 0.01 ❷
training_epochs = 100 ❷
x_train = np.linspace(-1, 1, 101) ❸
y_train = 2 * x_train + np.random.randn(*x_train.shape) * 0.33 ❸
X = tf.placeholder(tf.float32) ❹
Y = tf.placeholder(tf.float32) ❹
def model(X, w): ❺
return tf.multiply(X, w)
w = tf.Variable(0.0, name="weights") ❻
y_model = model(X, w) ❼
cost = tf.square(Y-y_model) ❼
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❽
sess = tf.Session() ❾
init = tf.global_variables_initializer() ❾
sess.run(init) ❾
for epoch in range(training_epochs): ❿
for (x, y) in zip(x_train, y_train): ⓫
sess.run(train_op, feed_dict={X: x, Y: y}) ⓬
w_val = sess.run(w) ⓭
sess.close() ⓮
plt.scatter(x_train, y_train) ⓯
y_learned = x_train*w_val ⓰
plt.plot(x_train, y_learned, 'r') ⓰
plt.show() ⓰
❶ 导入 TensorFlow 用于学习算法。你需要 NumPy 来设置初始数据,并使用 Matplotlib 来可视化你的数据。
❷ 定义学习算法使用的常数。这些常数称为超参数。
❸ 设置用于找到最佳拟合线的假数据
❹ 将输入和输出节点设置为占位符,因为值将由 x_train 和 y_train 注入
❺ 定义模型为 y = w*X
❻ 设置权重变量
❼ 定义成本函数
❽ 定义学习算法每次迭代将调用的操作
❾ 设置会话并初始化所有变量
❿ 每个 epoch 多次遍历数据集
⓫ 遍历数据集中的每个项目
⓬ 更新模型参数以尝试最小化成本函数
⓭ 获得最终参数值
⓮ 关闭会话
⓯ 绘制原始数据
⓰ 绘制最佳拟合线
如图 3.8 所示,你刚刚使用 TensorFlow 解决了线性回归问题!方便的是,回归中的其余主题都是列表 3.2 的微小修改。整个流程涉及使用 TensorFlow 更新模型参数,如图 3.9 总结所示。

图 3.8 通过运行列表 3.2 显示的线性回归估计

图 3.9 学习算法通过更新模型参数来最小化给定的成本函数。
你已经学会了如何在 TensorFlow 中实现一个简单的回归模型。进一步改进是一个简单的问题,只需通过前面讨论的正确混合方差和偏差来增强模型即可。你迄今为止设计的线性回归模型负担着强烈的偏差;它只表达了一组有限的函数,例如线性函数。在第 3.3 节中,你将尝试一个更灵活的模型。你会注意到,只需要重新布线 TensorFlow 图;其他所有事情(如预处理、训练和评估)都保持不变。
3.3 多项式模型
线性模型可能是一个直观的第一选择,但现实世界的相关性很少如此简单。例如,导弹在空间中的轨迹相对于地球上的观察者来说是弯曲的。Wi-Fi 信号强度随着平方反比定律而减弱。一朵花在其一生中的高度变化当然不是线性的。
当数据点似乎形成平滑曲线而不是直线时,你需要将你的回归模型从直线改为其他东西。一种方法就是使用多项式模型。多项式是线性函数的推广。n 次多项式看起来如下所示:
f (x) = w[n] x^n + ... + w[1] x + w[0]
注意:当 n = 1 时,多项式只是一个线性方程 f (x) = w[1] x + w[0]。
考虑图 3.10 中的散点图,其中 x 轴表示输入,y 轴表示输出。正如你所看到的,一条直线不足以描述所有数据。多项式函数是线性函数的更灵活的推广。

图 3.10 这样的数据点不适合线性模型。
让我们尝试将多项式拟合到这类数据。创建一个名为 polynomial.py 的新文件,并按照列表 3.3 进行操作。
列表 3.3 使用多项式模型
import tensorflow as tf ❶
import numpy as np ❶
import matplotlib.pyplot as plt ❶
learning_rate = 0.01 ❷
training_epochs = 40 ❷
trX = np.linspace(-1, 1, 101) ❷
num_coeffs = 6 ❸
trY_coeffs = [1, 2, 3, 4, 5, 6] ❸
trY = 0 ❸
for i in range(num_coeffs): ❸
trY += trY_coeffs[i] * np.power(trX, i) ❸
trY += np.random.randn(*trX.shape) * 1.5 ❹
plt.scatter(trX, trY) ❺
plt.show() ❺
X = tf.placeholder(tf.float32) ❻
Y = tf.placeholder(tf.float32) ❻
def model(X, w): ❼
terms = [] ❼
for i in range(num_coeffs): ❼
term = tf.multiply(w[i], tf.pow(X, i)) ❼
terms.append(term) ❼
return tf.add_n(terms) ❼
w = tf.Variable([0.] * num_coeffs, name="parameters") ❽
y_model = model(X, w) ❽
cost = (tf.pow(Y-y_model, 2)) ❾
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❾
sess = tf.Session() ❿
init = tf.global_variables_initializer() ❿
sess.run(init) ❿
Jr epoch in range(training_epochs): ⓫
for (x, y) in zip(trX, trY): ⓫
sess.run(train_op, feed_dict={X: x, Y: y}) ⓫
w_val = sess.run(w) ⓫
print(w_val) ⓫
sess.close() ⓫
plt.scatter(trX, trY) ⓬
trY2 = 0 ⓬
for i in range(num_coeffs): ⓬
trY2 += w_val[i] * np.power(trX, i) ⓬
plt.plot(trX, trY2, 'r') ⓬
plt.show() ⓬
❶ 导入相关库并初始化超参数
❷ 设置伪造的原始输入数据
❸ 基于五次多项式设置原始输出数据
❹ 添加噪声
❺ 显示原始数据的散点图
❻ 定义节点以保存输入/输出对的值
❼ 定义了你的多项式模型
❽ 将参数向量设置为全零
❾ 定义了与之前相同的成本函数
❿ 按照之前的方式设置会话并运行学习算法
⓫ 完成后关闭会话
⓬ 绘制结果图
代码的最终输出是一个五次多项式,如图 3.11 所示,它拟合了数据。

图 3.11 最佳拟合曲线与非线性数据平滑对齐。
3.4 正则化
不要被多项式的美妙灵活性所迷惑,如第 3.3 节所示。仅仅因为高阶多项式是低阶多项式的扩展,并不意味着你应该总是偏好更灵活的模型。
在现实世界中,原始数据很少形成类似于多项式的平滑曲线。假设你正在绘制随着时间的推移的房价。数据可能包含波动。回归的目标是用一个简单的数学方程表示复杂性。如果你的模型过于灵活,模型可能会过度复杂化对输入的解释。
以图 3.12 中展示的数据为例。你试图将八次多项式拟合到似乎遵循方程 y = x²的点。这个过程失败得很惨,因为算法尽力更新多项式的九个系数。

图 3.12 当模型过于灵活时,最佳拟合曲线可能看起来复杂或难以直观理解。我们需要使用正则化来改善拟合,以便学习到的模型在测试数据上表现良好。
正则化是一种技术,可以将参数结构化为你偏好的形式,通常用于解决过拟合问题(见图 3.13)。在这种情况下,你预计学习到的系数除了第二项外,其他地方都为 0,从而产生曲线 y = x²。回归算法可能会产生得分较高的曲线,但看起来过于复杂。

图 3.13 正则化的概述。建模过程以数据(X)作为输入,并试图学习模型参数(W),这些参数最小化成本函数或模型预测与真实值之间的距离。顶部黄色象限显示一个二维模型参数空间,用于选择权重以简化问题。正则化确保训练算法不会选择低于理想(黄色)区域的权重,而是保持在理想权重值的白色圆圈内部。
为了影响学习算法产生一个更小的系数向量(让我们称它为 w),你将这个惩罚项加到损失项中。为了控制你想要多显著地权衡惩罚项,你将惩罚乘以一个常数非负数,λ,如下所示:
成本(X, Y ) = 损失(X, Y ) + λ
如果λ设置为 0,则正则化不起作用。随着你将λ设置为更大的值,具有较大范数的参数将受到严重惩罚。范数的选择因情况而异,但参数通常通过它们的 L1 或 L2 范数来衡量。简单来说,正则化减少了模型本应容易纠缠的灵活性。
为了找出正则化参数λ的最佳值,你必须将数据集分割成两个不相交的集合。大约 70%的随机选择的输入/输出对将组成训练数据集;剩余的 30%将用于测试。你将使用列表 3.4 中提供的函数来分割数据集。
列表 3.4 将数据集分割为测试集和训练集
def split_dataset(x_dataset, y_dataset, ratio): ❶
arr = np.arange(x_dataset.size) ❷
np.random.shuffle(arr) ❷
num_train = int(ratio * x_dataset.size) ❸
x_train = x_dataset[arr[0:num_train]] ❹
x_test = x_dataset[arr[num_train:x_dataset.size]] ❹
y_train = y_dataset[arr[0:num_train]] ❺
y_test = y_dataset[arr[num_train:x_dataset.size]] ❺
return x_train, x_test, y_train, y_test ❻
❶ 以输入输出数据集以及期望的分割比率为输入
❷ 打乱数字列表
❸ 计算训练示例的数量
❹ 使用打乱后的列表来分割 x_dataset
❺ 同样,分割 y_dataset
❻ 返回分割后的 x 和 y 数据集
练习 3.3
一个名为 SK-learn 的 Python 库支持许多有用的数据处理算法。你可以调用 SK-learn 中的一个函数来完成列表 3.4 所实现的功能。你能在库的文档中找到这个函数吗?(提示:见mng.bz/7Grm。)
答案
它被称为sklearn.model_selection.train_test_split。
使用这个便捷的工具,你可以开始测试哪个值在你的数据上表现最佳。打开一个新的 Python 文件,并按照列表 3.5 进行操作。
列表 3.5 评估正则化参数
import tensorflow as tf ❶
import numpy as np ❶
import matplotlib.pyplot as plt ❶
learning_rate = 0.001 ❶
training_epochs = 1000 ❶
reg_lambda = 0\. ❶
x_dataset = np.linspace(-1, 1, 100) ❷
num_coeffs = 9 ❷
y_dataset_params = [0.] * num_coeffs ❷
y_dataset_params[2] = 1 ❷
y_dataset = 0 ❷
for i in range(num_coeffs): ❷
y_dataset += y_dataset_params[i] * np.power(x_dataset, i) ❷
y_dataset += np.random.randn(*x_dataset.shape) * 0.3 ❷
(x_train, x_test, y_train, y_test) = split_dataset(x_dataset, y_dataset, 0.7)❸
X = tf.placeholder(tf.float32) ❸
Y = tf.placeholder(tf.float32) ❸
def model(X, w): ❹
terms = [] ❹
for i in range(num_coeffs): ❹
term = tf.multiply(w[i], tf.pow(X, i)) ❹
terms.append(term) ❹
return tf.add_n(terms) ❹
w = tf.Variable([0.] * num_coeffs, name="parameters") ❺
y_model = model(X, w) ❺
cost = tf.div(tf.add(tf.reduce_sum(tf.square(Y-y_model)), ❺
tf.multiply(reg_lambda, tf.reduce_sum(tf.square(w)))), ❻
2*x_train.size) ❻
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❻
sess = tf.Session() ❼
init = tf.global_variables_initializer() ❼
sess.run(init) ❼
for reg_lambda in np.linspace(0,1,100): ❽
for epoch in range(training_epochs): ❽
sess.run(train_op, feed_dict={X: x_train, Y: y_train}) ❽
final_cost = sess.run(cost, feed_dict={X: x_test, Y:y_test}) ❽
print('reg lambda', reg_lambda) ❽
print('final cost', final_cost) ❽
sess.close() ❾
❶ 导入相关库并初始化超参数
❷ 创建一个假数据集,y = x²
❸ 使用列表 3.4 将数据集分割为 70%训练和 30%测试
❹ 设置输入/输出占位符
❺ 定义你的模型
❻ 定义正则化成本函数
❼ 设置会话
❽ 尝试各种正则化参数
❾ 关闭会话
如果你绘制列表 3.5 中每个正则化参数对应的输出,你可以看到曲线如何随着λ的增加而变化。当λ为 0 时,算法倾向于使用高阶项来拟合数据。当你开始惩罚具有高 L2 范数的参数时,成本降低,表明你正在从过拟合中恢复,如图 3.14 所示。

图 3.14 随着正则化参数的增加,成本降低。这一结果意味着模型最初过度拟合了数据,而正则化有助于增加结构
TensorFlow 库对正则化的支持
TensorFlow 是一个完全能够支持机器学习的库,尽管本节的重点是如何自己实现正则化,但该库提供了自己的函数来计算 L2 正则化。你可以使用函数tf.nn.l2_loss(weights)通过将正则化损失添加到每个权重的成本函数中,来产生等效的结果。
3.5 线性回归的应用
在假数据上运行线性回归就像买了一辆新车却从未驾驶过。这神奇的机械渴望在现实世界中展现自己!幸运的是,许多数据集都可在网上找到,以测试你对回归的新发现知识:
-
马萨诸塞大学阿默斯特分校在
scholarworks.umass.edu/data提供了各种类型的小型数据集。 -
Kaggle 在
www.kaggle.com/datasets提供了所有类型的大规模数据,用于机器学习竞赛。 -
Data.gov (
catalog.data.gov) 是美国政府的一项开放数据倡议,其中包含许多有趣且实用的数据集。
许多数据集包含日期。例如,你可以在 www.dropbox.com/s/naw774olqkve7sc/311.csv?dl=0 找到加利福尼亚州洛杉矶所有 311 非紧急电话的数据集。一个很好的跟踪特征可能是每天、每周或每月的通话频率。为了方便起见,列表 3.6 允许你获得数据项的每周频率计数。
列表 3.6 解析原始 CSV 数据集
import csv ❶
import time ❷
def read(filename, date_idx, date_parse, year, bucket=7):
days_in_year = 365
freq = {} ❸
for period in range(0, int(days_in_year / bucket)):
freq[period] = 0
with open(filename, 'rb') as csvfile: ❹
csvreader = csv.reader(csvfile)
csvreader.next()
for row in csvreader:
if row[date_idx] == '':
continue
t = time.strptime(row[date_idx], date_parse)
if t.tm_year == year and t.tm_yday < (days_in_year-1):
freq[int(t.tm_yday / bucket)] += 1
return freq
freq = read('311.csv', 0, '%m/%d/%Y', 2014 ❺
❶ 为了轻松读取 CSV 文件
❷ 为了使用有用的日期函数
❸ 设置初始频率映射
❹ 读取数据并按周期汇总计数
❺ 获取 2014 年 311 电话通话的每周频率计数
这段代码为你提供了线性回归的训练数据。freq 变量是一个字典,将周期(如一周)映射到频率计数。一年有 52 周,所以如果你将 bucket=7 保持不变,你将会有 52 个数据点。
现在你有了数据点,你正好有输入和输出,可以使用本章介绍的技术来拟合回归模型。更实际地说,你可以使用学习到的模型来插值或外推频率计数。
摘要
-
回归是一种监督机器学习方法,用于预测连续值输出。
-
通过定义一组模型,你大大减少了可能函数的搜索空间。此外,TensorFlow 通过运行其高效的梯度下降优化器来利用函数的可微性,从而学习参数。
-
你可以轻松地将线性回归修改为学习多项式和其他更复杂的曲线。
-
为了避免数据过拟合,通过惩罚较大值的参数来正则化成本函数。
-
如果函数的输出不连续,则使用分类算法代替(参见第四章)。
-
TensorFlow 使你能够有效地解决线性回归机器学习问题,并因此对诸如农业生产、心脏病和房价等重要问题做出有用的预测。
4 使用回归进行呼叫中心量预测
本章涵盖
-
将线性回归应用于现实世界数据
-
清洗数据以适应你之前未见过的曲线和模型
-
使用高斯分布并预测沿其的点
-
评估你的线性回归如何预测预期值的效果
借助基于回归的预测和 TensorFlow 的力量,你可以开始处理涉及机器学习过程中更多步骤的现实世界问题,例如数据清洗、将模型拟合到未见过的数据,以及识别那些不一定是容易发现的最佳拟合线或多项式曲线的模型。在第三章中,我向你展示了如何在控制机器学习过程的每个步骤时使用回归,从使用 NumPy 生成适合线性函数(一条线)或多项式函数(一条曲线)的假数据点开始。但在现实生活中,当数据点不适应你之前见过的任何模式时,比如图 4.1 中的点集,会发生什么呢?仔细看看图 4.1。线性回归模型在这里是一个好的预测器吗?

图 4.1 一组数据点,对应于年份的周数(x 轴上的 0-51)和归一化呼叫量(特定一周的呼叫次数/所有周的最大呼叫次数)
图 4.2 给出了两个最佳拟合线,使用线性回归模型对呼叫数据进行拟合,这似乎非常不合适。你能想象图 4.2 左右两侧图中预测值和实际值之间的误差吗?多项式模型也会同样糟糕,因为数据没有整齐地适合曲线,y 值在 x 轴上看似随机的时刻同时增加和减少。通过绘制二阶或三阶多项式来证明自己。你能让它拟合数据吗?如果你做不到,那么计算机程序可能也会遇到类似的困难。在这种情况下,问自己这些问题是完全正常的:
-
回归还能帮助我预测下一组点吗?
-
除了直线和多项式曲线之外,还存在哪些回归模型?
-
因为现实生活中的数据通常不是以容易绘制和用回归等模型表示的漂亮的(x, y)点形式出现,我该如何准备数据,以便它能够干净地适合特定的模型,或者找到更适合混乱数据的模型?

图 4.2 对图 4.1 中所示数据点的两个线性最佳拟合模型
在第三章和整本书中,有时为了说明,我生成了一些假数据,例如 NumPy np.random.normal函数的输出,但必须强调的是,真实数据很少看起来是这样的。
注意:我知道有几位作者正在撰写关于机器学习数据清洗主题的书籍。尽管血腥的细节超出了本书的范围,但我不会向你隐藏数据清洗步骤;你将有机会使用使此过程更简单的 TensorFlow 技术来获得经验。
在将回归应用于现实世界问题时,另一个出现的问题是,如何通过计算偏差,即模型预测值与实际值之间的差异来评估预测的准确性。图 4.2 中回归模型生成的线似乎有些偏离,但你可以通过定量测量和评估模型预测与真实值之间的误差来做出更强烈的陈述。TensorFlow 通过其数据流图提供易于使用的结构,可以在几行代码中计算误差。Matplotlib 同样提供了能力,让你可以通过几行代码直观地检查和评估模型误差。
线性回归在许多现实世界问题中是一个好的预测器,因此很难选择一个。一般来说,具有时间性质的问题为历史数据训练提供了数据在 x 轴上的自然排序,以及一个未来状态(例如未来 N 小时或几周),这形成了一个良好的预测目标,可以用来测试回归。因此,选择一个涉及时间作为因变量的问题非常适合回归模型。
对于机器学习,存在大量的基于时间的数据集,许多使用这些数据集的开放数据挑战在 Kaggle(www.kaggle.com)上免费提供,Kaggle 是一个广泛使用的开放机器学习平台。Kaggle 提供了数据集、文档、可分享的代码以及执行机器学习的平台,包括对基于 TensorFlow 的笔记本和代码的一流支持。Kaggle 拥有大量的时间数据集,可以尝试回归机器学习模型,例如房地产价格挑战(mng.bz/6Ady)和纽约市(NYC)311 开放数据集(mng.bz/1gPX)。NYC 311 数据集很有趣,因为它基于时间,需要一些清洗,并且不适合线性或多项式曲线回归模型。
纽约市开放数据平台
纽约市有一个开放数据计划,它提供了易于使用的应用程序编程接口(API),可以下载用于机器学习和其他用途的数据。您可以通过opendata.cityofnewyork.us访问开放数据门户。
纽约市的 311 数据是收集居民向城市客服中心打电话请求关于城市和其他政府非紧急服务信息的一系列信息,例如废物处理、法规执行和建筑维护。想象一下,像这样的客服中心每天会接到很多电话,因为它们能帮助人们及时获取所需信息。但是,客服中心一周会接到多少电话?一年呢?有没有某些月份或周,客服中心可以预期接到更多或更少的电话?
假设你负责此类服务的员工配置,尤其是在假日期间。你应该有更多还是更少的客服代表接听电话?电话激增是否与季节有关?你应该允许额外的季节性员工,还是全职员工就足够了?回归分析和 TensorFlow 可以帮助你找到这些问题的答案。
4.1 什么是 311?
311 是美国和加拿大的一项全国性服务,提供有关非紧急市政服务的信息。该服务允许您报告垃圾收集员没有来取垃圾,或者了解如何清除您住宅前公共区域内的树叶和灌木。311 的电话量各不相同,但在大城市和市政区,每月的电话量可以从几千到几万不等。
这些客服中心及其相关信息服务必须处理的一个关键问题是,在特定月份会有多少电话进来。这些信息可能有助于服务在假日期间规划人员配备水平,或者决定为他们的资源和服务目录存储和计算成本。此外,它还可能帮助 311 倡导者提供关于它预期在任意一年为多少人提供服务的信息,这将有助于证明对关键服务的持续支持是合理的。
第一个 311
第一个 311 服务于 1996 年在马里兰州巴尔的摩开业。该服务的两个主要目标是建立政府与其公民之间更紧密的联系,并创建客户关系管理(CRM)能力,以确保更好地为社区提供服务。CRM 功能今天适合我们将在本章中探讨的数据驱动预测。
能够预测任意月份的电话量对于任何 311 服务来说都将是一项极其有用的能力。你可以通过查看一年的电话及其相关日期和时间来完成这项预测,然后将这些电话按周汇总,构建一系列点,其中 x 值是周数(1-52,或每周 7 天除以 365 天),y 值是特定周的电话计数。然后你会做以下事情:
-
在 y 轴上绘制电话数量,在 x 轴上绘制周数(1-52)。
-
检查趋势,看它是否类似于一条线、一个曲线或其他东西。
-
选择并训练一个最适合数据点(周数和调用次数)的回归模型。
-
通过计算和可视化其错误来评估你的模型表现如何。
-
使用您的新模型来预测 311 在任何给定周、季节和年份可以期待多少次调用。
这个预测看起来像是线性回归模型和 TensorFlow 可以帮助你完成的,这正是你将在本章中要做的。
4.2 为回归清理数据
首先,从mng.bz/P16w下载这些数据——2014 年夏纽约市 311 服务的一组电话调用。Kaggle 有其他 311 数据集,但你会使用这个特定的数据,因为它具有有趣的特征。这些调用以逗号分隔值(CSV)文件格式化,具有几个有趣的特征,包括以下内容:
-
一个唯一的调用标识符,显示调用创建的日期
-
报告事件或信息请求的位置和 ZIP 代码
-
代理在通话中采取的具体行动以解决问题
-
调用是从哪个区(如布朗克斯或皇后区)打来的
-
调用的状态
这个数据集包含大量对机器学习有用的信息,但在这个练习中,你只关心调用创建日期。创建一个名为 311.py 的新文件。然后编写一个函数来读取 CSV 文件中的每一行,检测周数,并按周汇总调用次数。
你的代码需要处理这个数据文件中的一些混乱。首先,将单个调用(有时一天内多达数百次)聚合到一个七天或每周的桶中,如列表 4.1 中的bucket变量所标识。freq(频率的简称)变量包含每周和每年的调用次数。如果 311 CSV 包含超过一年的数据(如你可以在 Kaggle 上找到的其他 311 CSV 文件),编写代码以允许按年份选择用于训练的调用。列表 4.1 中代码的结果是一个freq字典,其值是按年份和周数通过period变量索引的调用次数。t.tm_year变量包含从调用创建时间值(在 CSV 中索引为date_idx,一个定义日期字段所在列号的整数)和date_parse格式字符串传递给 Python 的time库的strptime(或字符串解析时间)函数后解析出的年份。date_parse格式字符串是一个模式,定义了日期在 CSV 中作为文本出现的方式,以便 Python 知道如何将其转换为 datetime 表示。
列表 4.1 从 311 CSV 中读取和汇总按周计算的调用次数
def read(filename, date_idx, date_parse, year=None, bucket=7): ❶
days_in_year = 365 ❶
freq = {}
if year != None: ❷
for period in range(0, int(days_in_year / bucket)): ❷
freq[period] = 0 ❷
with open(filename, 'r') as csvfile:
csvreader = csv.reader(csvfile)
next(csvreader)
for row in csvreader:
if row[date_idx] == '': ❸
continue
t = time.strptime(row[date_idx], date_parse)
if year == None:
if not t.tm_year in freq:
freq[t.tm_year] = {} ❹
for period in range(0, int(days_in_year / bucket)): ❹
freq[t.tm_year][period] = 0 ❹
if t.tm_yday < (days_in_year - 1):
freq[t.tm_year][int(t.tm_yday / bucket)] += 1 ❺
else:
if t.tm_year == year and t.tm_yday < (days_in_year-1):
freq[int(t.tm_yday / bucket)] += 1 ❺
return freq
❶ 一周有 7 天,一年有 365 天,因此有 52 周。
❷ 如果指定了年份,则仅选择该年的调用。
❸ 如果调用数据中不存在年份列,则跳过该行。
❹ 如果 CSV 文件包含超过一年的数据,则将(年,周)单元格的值初始化为 0。
❺ 对于每一行,将 1 次调用添加到(年,周)或(周)索引单元格的前一个计数(从 0 开始)。
列表 4.1 中的大部分代码处理的是现实世界中的数据,而不是由 NumPy 调用生成的随机(x, y)数据点,这些数据点沿正态分布——这是本书中的一个主题。机器学习期望干净的数据来执行其所有黑魔法,而现实世界中的数据并不干净。从纽约市开放数据门户(data.cityofnewyork.us/browse?q=311)随机选取 311 个 CSV 文件,您会发现很多差异。有些文件包含来自多个年份的调用,因此您的代码需要处理这种情况;有些文件有缺失的行,或者特定单元格缺失年份和日期值,而您的代码仍然需要保持稳定。编写健壮的数据清理代码是机器学习的基本原则之一,因此编写此代码将是本书许多示例中的第一步。
调用列表 4.1 中定义的 read 函数,您将得到一个以(年,周数)或简单地以(周数)为索引的 Python 字典,具体取决于您是否将年份作为最后一个功能参数传递。在您的 311.py 代码中调用此函数。该函数接受列索引(第二列,索引为 0)和看起来像的字符串作为输入。
freq = read('311.csv', 1, '%m/%d/%Y %H:%M:%S %p', 2014)
告诉函数日期字段位于索引 1(或基于 0 索引的第二列),您的日期格式为类似 'month/day/year hour:minutes:seconds AM/PM' 或 '12/10/2014 00:00:30 AM' 的字符串(对应于 2014 年 12 月 10 日午夜过后的 30 秒),并且您希望从 CSV 中获取 2014 年的日期。
如果您使用 Jupyter,可以通过检查 freq 字典来打印频率桶中的值。结果是每周调用次数的 52 周直方图(从 0 索引,因此为 0-51)。如您从输出中可以看出,数据从第 22 周到第 35 周聚集,即 2014 年 5 月 26 日至 8 月 25 日:`
freq
{0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
10: 0,
11: 0,
12: 0,
13: 0,
14: 0,
15: 0,
16: 0,
17: 0,
18: 0,
19: 0,
20: 0,
21: 10889,
22: 40240,
23: 42125,
24: 42673,
25: 41721,
26: 38446,
27: 41915,
28: 41008,
29: 39011,
30: 36069,
31: 38821,
32: 37050,
33: 36967,
34: 26834,
35: 0,
36: 0,
37: 0,
38: 0,
39: 0,
40: 0,
41: 0,
42: 0,
43: 0,
44: 0,
45: 0,
46: 0,
47: 0,
48: 0,
49: 0,
50: 0,
51: 0}
国际标准化组织(ISO)机构,该机构定义了计算机代码、数据和软件的标准,发布了常用的 ISO-8601 标准,用于将日期和时间作为字符串表示。Python 的 time 和 iso8601 库实现了此标准,该标准包括与以星期一为开始日期的周数相关的日期和时间的规范(www.epochconverter.com/weeknumbers),这对于 311 数据似乎是一个有用的表示。尽管其他周数表示方法也可用,但它们大多数都有不同的周起始日,例如星期日。
通过将基于时间的日期转换为 x 轴上的周数,你得到一个表示时间的整数值,它可以很容易地进行排序和可视化。这个值与回归函数将预测的调用频率 y 轴值相匹配。
将周数转换为日期
你将要处理的大部分基于时间的数据有时用周数表示会更好。处理一个从 1 到 52 的整数,比如字符串值'周三,2014 年 9 月 5 日',要好得多。EpochConverter 网站可以轻松地告诉你某年某周的周数。要查看列表 4.1 中 2014 年输出映射到周数的日期列表,请访问www.epochconverter.com/weeks/2014。
你可以通过使用datetime库在纯 Python 中获取相同的信息:
import datetime
datetime.date(2010, 6, 16).isocalendar()[1]
这段代码输出 24,因为 2010 年 6 月 16 日是该年的第 24 周。
不用谢!
打开一个 Jupyter 笔记本,并将列表 4.2 粘贴进去,以可视化从你的freq字典(由read函数返回)中按周计算的调用频率的分组直方图,并生成图 4.3,这是本章开头讨论的点分布。通过检查点分布,你可以决定在 TensorFlow 中编码的回归预测器的模型。列表 4.2 设置了你的输入训练值为第 1-52 周,你的预测输出为该周预期的调用次数。X_train变量包含频率字典的键(0-51 的整数,代表 52 周),而Y_train变量包含每周的 311 次调用次数。nY_train包含标准化后的 Y 值(除以maxY),介于 0 到 1 之间。我将在本章后面解释原因,但预览是它在训练过程中简化了学习过程。代码的最后几行使用 Matplotlib 创建了一个(周数,调用频率)点的散点图。
列表 4.2 可视化和设置输入数据
X_train = np.asarray(list(freq.keys())) ❶
Y_train = np.asarray(list(freq.values())) ❷
print("Num samples", str(len(X_train)))
maxY = np.max(Y_train) ❸
nY_train = Y_train / np.max(Y_train) ❸
plt.scatter(X_train, nY_train) ❹
plt.show()
❶ 定义输入训练数据 X 为周数,这是 Python freq 字典的键
❷ 定义输入训练数据 Y 为特定周的调用次数
❸ 将调用值之间的数量标准化到 0 到 1 之间,以简化学习过程
❹ 绘制数据以学习
列表 4.2 的输出显示在图 4.3 中。它与我们在第三章中尝试拟合模型的线条和曲线相当不同。记得我说过现实世界的数据并不总是那么完美吗?直观上看,数据告诉我们,在年初的前两个季度的大部分时间里,没有调用;春季出现的大峰值一直延续到夏天;在秋冬季节,没有调用。也许夏天是纽约许多人在 2014 年使用 311 的季节。更有可能的是,这些数据只是可用实际信息的一个子集,但让我们看看我们是否可以从中构建一个好的模型。

图 4.3 在 y 轴上显示呼叫频率计数,与 x 轴上的年份周数(0-51)进行比较。活动在 2014 年 5 月期间增加,在 2014 年 8 月下旬减少。
如果你曾经查看过测试分数的分布,描述从 0 到 100 排序的分数分布的常见模型是曲线或钟形曲线。结果证明,我们将在本章的剩余部分教会我们的 TensorFlow 预测器模仿这种类型的模型。
4.3 钟形曲线中有什么?预测高斯分布
钟形曲线或正态曲线是描述我们所说的符合正态分布的数据的常用术语。数据中最大的 Y 值出现在中间或统计上的均值 X 值,而较小的 Y 值出现在分布的早期和尾部 X 值。我们也将这种分布称为高斯分布,以纪念著名的德国数学家卡尔·弗里德里希·高斯,他负责描述正态分布的高斯函数。
我们可以使用 NumPy 方法np.random.normal在 Python 中生成从正态分布中抽取的随机点。以下方程显示了该分布背后的高斯函数:

方程包括参数μ(发音为 mu)和σ(发音为 sigma),其中 mu 是分布的均值,sigma 是分布的标准差。Mu 和 sigma 是模型的参数,正如你所看到的,TensorFlow 将在训练模型的过程中学习这些参数的适当值。
为了让你相信你可以使用这些参数来生成钟形曲线,你可以将列表 4.3 中的代码片段输入到名为 gaussian.py 的文件中,然后运行它以生成随后的图表。列表 4.3 中的代码生成了图 4.4 中所示的高斯曲线可视化。请注意,我选择了 mu 在-1 和 2 之间的值。你应该在图 4.4 中看到曲线的中心点,以及标准差(sigma)在 1 和 3 之间,因此曲线的宽度应该对应这些值。代码绘制了 120 个线性间隔的点,X 值在-3 和 3 之间,Y 值在 0 和 1 之间,这些点根据 mu 和 sigma 符合正态分布,输出应该类似于图 4.4。

图 4.4 三个均值在-1 和-2 之间(它们的中心点应该接近这些点)且标准差在 1 和 3 之间的钟形曲线。曲线是由在-3 和 3 之间线性分布的 120 个点生成的。
列表 4.3 生成一些高斯分布并可视化它们
def gaussian(x, mu, sig):
return np.exp(-np.power(x - mu, 2.) / (2 * np.power(sig, 2.))) ❶
x_values = np.linspace(-3, 3, 120) ❷
for mu, sig in [(-1, 1), (0, 2), (2, 3)]:
plt.plot(x_values, gaussian(x_values, mu, sig))
plt.show()
❶ 使用 mu 和 sigma 实现高斯曲线。
❷ 随机选择 X 在-3 和 3 之间的 120 个线性间隔样本。
4.4 训练你的呼叫预测回归器
现在,你已准备好使用 TensorFlow 将你的纽约市 311 数据拟合到这个模型。通过观察曲线,它们似乎自然地与 311 数据相符,特别是如果 TensorFlow 能够找出 mu 的值,使曲线的中心点接近春季和夏季,并且具有相当大的调用量,以及近似最佳标准差的 sigma 值。
列表 4.4 设置了 TensorFlow 训练会话、相关超参数、学习率和训练时代数。我使用了一个相当大的学习率步长,以便 TensorFlow 在稳定下来之前能够通过足够大的步长适当地扫描 mu 和 sig 的值。时代数——5,000——为算法提供了足够的训练步骤以确定最佳值。在我的笔记本电脑上的本地测试中,这些超参数达到了强准确性(99%),并且用时不到一分钟。但我可以选择其他超参数,例如学习率为 0.5,并给训练过程更多的步骤(时代)。机器学习的乐趣之一是超参数训练,这与其说是科学不如说是艺术,尽管元学习和 HyperOpt 等算法可能会在未来简化这一过程。超参数调整的全面讨论超出了本章的范围,但在线搜索应该会找到成千上万的相关介绍。
当超参数设置好之后,定义占位符 X 和 Y,它们将分别用于输入周数和相关的调用次数(归一化)。之前我提到过对 Y 值进行归一化并在列表 4.2 中创建 nY_train 变量以简化学习。原因是由于指数 e,我们试图学习的模型 Gaussian 函数的 Y 值仅在 0 和 1 之间。model 函数定义了要学习的 Gaussian 模型,相关的变量 mu 和 sig 随机初始化为 1。cost 函数定义为 L2 范数,训练使用梯度下降。在训练回归器 5,000 个时代后,列表 4.4 中的最后几步打印出 mu 和 sig 的学习值。
列表 4.4 设置和训练 TensorFlow 模型以拟合你的 Gaussian 曲线
learning_rate = 1.5 ❶
training_epochs = 5000 ❷
X = tf.placeholder(tf.float32) ❸
Y = tf.placeholder(tf.float32) ❸
def model(X, mu, sig):
return tf.exp(tf.div(tf.negative(tf.pow(tf.subtract(X, mu), 2.)),
➥ tf.multiply(2., tf.pow(sig, 2.))))
mu = tf.Variable(1., name="mu") ❹
sig = tf.Variable(1., name="sig") ❹
y_model = model(X, mu, sig) ❺
cost = tf.square(Y-y_model) ❻
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❻
sess = tf.Session() ❼
init = tf.global_variables_initializer() ❼
sess.run(init) ❼
for epoch in range(training_epochs):
for(x, y) in zip(X_train, nY_train):
sess.run(train_op, feed_dict={X:x, Y:y}) ❽
mu_val = sess.run(mu)
sig_val = sess.run(sig)
print(mu_val) ❾
print(sig_val) ❾
sess.close() ❿
❶ 为每个时代设置学习率
❷ 训练 5,000 个时代
❸ 设置输入(X)和预测值(Y)
❹ 定义模型的学习参数 mu 和 sig
❺ 基于 TensorFlow 图创建模型
❻ 定义成本函数为 L2 范数并设置训练操作
❼ 初始化 TensorFlow 会话
❽ 执行训练并学习 mu 和 sig 的值
❾ 打印 mu 的学习值
❿ 关闭会话
列表 4.4 的输出应类似于以下内容,它对应于 mu 和 sig 的学习值:
27.23236
4.9030166
当你完成打印值并将它们保存在局部变量 mu_val 和 sig_val 中后,不要忘记关闭你的 TensorFlow 会话,以便你可以释放它用于其 5,000 次训练迭代的资源。你可以通过调用 sess.close() 来处理这个任务。
提示:列表 4.4 展示了如何使用 TensorFlow 1.x 构建呼叫中心体积预测算法。如果你想知道算法在 TensorFlow 2.x 中的样子,请跳转到附录,我在那里用 TensorFlow 2.x 的语言讨论并重新工作了模型。有一些细微的差异,但它们值得一看。其他你迄今为止看到的数据清理和准备代码保持不变,即将看到的验证和错误计算也是如此。
4.5 可视化结果和绘制误差图
你的线性回归在预测每周 311 呼叫频率方面做得怎么样?从 mu_val 和 sig_val 中获取均值和标准差值,并使用它们在列表 4.5 和图 4.5 中绘制学习到的模型。Matplotlib 会处理初始的周数(X 轴)与呼叫频率(Y 轴)归一化的散点图。然后,你将 mu_val 和 sig_val 的学习参数插入到列表 4.4 中的模型方程的一行展开中。这样做而不是重用模型函数并将 mu_val 和 sig_val 作为参数传递的原因是,当 TensorFlow 学习到最佳参数值时,你不需要再次设置 TensorFlow 图或传递会话来评估模型值。相反,你可以使用 NumPy 及其类似函数,如 np.exp(指数)和 np.power。这些函数与 TensorFlow 的同名函数等效,但 NumPy 不需要 TensorFlow 会话及其相关资源来评估每个点的值。trY2 包含了结果学习到的呼叫频率预测;因为它们被归一化到 0 和 1 之间以学习高斯函数,所以你必须将学习值乘以 maxY 来区分每周的实际未归一化呼叫频率预测。结果是红色绘制在原始训练数据旁边,并且打印出任意周的呼叫数量预测并与原始训练数据值进行比较,以测试模型。

图 4.5 学习到的 TensorFlow 模型(以红色显示),预测每年每周(0-51)的呼叫量,对于第 1-52 周(索引为 0)。每周的实际呼叫数据量以蓝色显示。
列表 4.5 可视化学习到的模型
plt.scatter(X_train, Y_train) ❶
trY2 = maxY * (np.exp(-np.power(X_train - mu_val, 2.) / (2 * np.power(sig_val, 2.)))) ❷
plt.plot(X_train, trY2, 'r') ❸
plt.show()
print("Prediction of week 35", trY2[33])
print("Actual week 35", Y_train[33])
❶ 以蓝色散点点绘制周数(X)按呼叫数量(Y)的训练数据点
❷ 使用学习到的均值和标准差参数拟合高斯函数模型
❸ 以红色绘制学习到的模型
列表 4.5 的结果应类似于以下内容:
Prediction of week 35 21363.278811768592
Actual week 35 36967
虽然你的模型预测的呼叫量可能比实际值低 15,000 个呼叫,看起来可能有些疯狂,但请记住第三章中关于偏差和方差的讨论。你并不是在寻找一个过度拟合数据并且扭曲各种方式以确保它穿过每个点的模型。这些模型表现出高偏差和低方差,因此它们过度拟合了训练数据。相反,你想要的是表现出低偏差和低方差,并且在未见数据上表现良好的模型。
提供新的未见呼叫数据,只要纽约市 311 人口在春季和夏季似乎具有季节性,你的模型就拟合得很好,并且会表现良好。它甚至可以处理分布变化(季节间呼叫量的变化),只要输入在标准差内和/或特定的中心均值对应于最大呼叫数。你可以更新你的模型,让它学习新的mu和sig值;然后你就准备好了。
尽管你可能对这个讨论有一个疑问。你通过讨论预测值与实际值之间的差异来评估你的模型性能。你可以使用你已经看到的一些工具——Jupyter 和 Matplotlib——来定量地测量误差。运行列表 4.6 来计算模型的误差、平均误差和准确性,然后如图 4.6 所示可视化模型。error变量定义为预测数据与训练数据(如你可能在第一章中回忆的那样,是 L2 范数)之间平方差的平方根。这个误差变量为每周的每个预测呼叫频率提供了距离,最好以条形图的形式可视化。avg_error是通过将trY2中的每个预测点与Y_train之间的方差除以总周数来计算的,以获得每周预测呼叫的平均误差。算法的整体准确性为 1(每周呼叫预测的平均误差除以每周的最大呼叫数)。结果存储在acc变量中,并在列表末尾打印出来。
列表 4.6 计算误差并可视化它
error = np.power(np.power(trY2 - Y_train, 2), 0.5) ❶
plt.bar(X_train, error) ❷
plt.show() ❷
avg_error = functools.reduce(lambda a,b: a+b, (trY2-Y_train)) ❸
avg_error = np.abs(avg_error) / len(X_train) ❸
print("Average Error", avg_error)
acc = 1\. - (avg_error / maxY) ❹
print("Accuracy", acc) ❹
❶ 误差是模型预测值与实际值之间平方差的平方根。
❷ 以每周值误差的直方图形式显示误差
❸ 通过使用 Map Reduce 来求和差异并除以总周数来计算整体平均误差(每周呼叫差异数)
❹ 通过从 1 中减去平均误差除以整个数据集的最大呼叫数量来计算准确性
生成的图表,显示了每周电话预测的误差,也类似于预测曲线。

图 4.6 展示了每周预测呼叫数量的误差可视化。模型在呼叫量高的周表现相当好。
虽然整体误差可能看起来很高——它确实在分布的尾部——但平均误差仅为每周 205 个呼叫的方差,这是由于模型在关键时刻(高呼叫量周)的预测非常准确。你可以在列表 4.6 的输出中看到模型的平均误差和准确率(99%):
Average Error 205.4554733031897
Accuracy 0.9951853520187662
你在第三章中学习了正则化和训练测试分割。我在第 4.6 节中简要介绍了它们。
4.6 正则化和训练测试分割
正则化的概念可能最好在图 4.7 中描述。在训练过程中,一个机器学习模型(M),给定一些用于成本函数的输入数据(X),训练过程会探索参数(W)的空间,以最小化模型响应(预测)与实际训练输入值(X)之间的差异。这种成本被捕获在函数M(X, W)中。挑战在于,在训练和参数探索过程中,算法有时会选择局部最优但全局较差的参数值W。正则化可以通过惩罚较大的W值来影响这种选择,并试图将权重探索保持在图 4.7 中标记的优化区域(白色带红色轮廓的圆形区域)。
当模型对输入数据欠拟合或过拟合,或者在训练过程中权重探索过程需要帮助惩罚参数空间W中的较高或黄色区域时,你应该考虑使用正则化。你为 311 创建的新高斯钟形曲线模型具有相当高的准确度,尽管它在呼叫量分布的尾部存在一些误差。正则化能帮助消除这些误差吗?

图 4.7 回顾正则化的概念。在训练过程中提供的输入数据(X)通过学习参数(W)的训练过程与模型(M)相匹配,通过测量这些参数的预测与成本函数M(X, W)进行比较。在训练过程中,你希望参数空间W(左上角)的探索集中在白色区域,并避开黄色区域。正则化使得这种结果成为可能。
虽然这可能看起来有些反直觉,但答案是:不,这是由于四个概念:我们训练过程的超参数、学习率、训练步数(周期)以及列表 4.4 中 mu 和 sig 的初始值。为了方便起见,我将它们复制在这里:
learning_rate = 1.5
training_epochs = 5000
mu = tf.Variable(1., name="mu")
sig = tf.Variable(1., name="sig")
这些是模型的超参数和学习的参数。初始值非常重要,并不是随意选择的。提前知道mu(均值)和sig(标准差)的最佳值(分别为~27和~4.9)会很好,但你没有那个知识。相反,你将它们设置为默认值(每个为1),并允许学习率 1.5 控制探索的步骤。将学习率设置得太低(如0.1或0.001),算法可能需要数十万个训练周期,却永远无法达到学习参数的最佳值。将学习率设置超过 1.5(比如 3.5),算法将跳过特定训练步骤中的最佳值。
小贴士:尽管事先不知道mu(均值)和sigma(标准差),你可以通过观察数据并部分地推导出它们。接近第 27 周是分布的峰值或均值。至于标准差,将其估计为~5 很困难但并非不可能。标准差是衡量每个单位中数字从均值扩散开来的度量。低标准差意味着分布的尾部接近均值。能够使用你的眼睛探索输入和预期值并调整初始模型参数将为你节省大量的时间和训练周期。
在这个特定情况下,不需要正则化,因为直观上,你已经探索了你的训练数据,可视化了它,并将呼叫频率归一化到0到1之间以简化学习,因为你的模型适合数据并且没有过拟合或欠拟合。惩罚参数探索步骤在这种情况下会产生净负面影响,可能阻止你更精确地拟合模型。
此外,鉴于数据的稀疏性——这是你在可视化和执行探索性数据分析过程中才发现的——将数据分为训练/测试集是不可行的,因为你将失去模型所需的大量信息。假设你的 70/30 分割去除了分布的尾部以外的所有内容。信息损失可能导致你的回归模型看到的不是钟形曲线,而是直线或小的多项式曲线,并学习到错误的模型——或者更糟糕的是,学习到错误数据的正确模型,例如图 4.2 中的图表。在这里,训练/测试分割没有意义。
欢呼!你已经训练了一个真实世界的回归器,它在可用数据上达到了 99%的准确率和合理的误差。你帮助 311 开发了一个季节性准确的呼叫量预测器,帮助它预测需要多少呼叫代理来接听电话,并通过每周处理的通话数量证明了其对社区的价值。这个任务是你认为机器学习和 TensorFlow 可以帮助你完成的吗?它们可以,并且做到了。
在第五章中,我们深入探讨如何通过使用 TensorFlow 构建分类器来开发对离散输出的强大预测能力。继续前进!
摘要
-
在 TensorFlow 中将线性回归应用于直线和多项式假设所有数据都是干净且整齐地适合直线和点。本章向您展示了看起来不像第三章中看到的那样真实世界的数据,并解释了如何使用 TensorFlow 来拟合模型。
-
可视化你的输入数据点有助于你选择适合回归的模型——在这种情况下,高斯模型。
-
学习如何通过可视化来评估你的偏差和误差是使用 TensorFlow 和调整你的机器学习模型的关键部分。
-
在这个模型上应用正则化并不能帮助更好地拟合数据。当训练步骤产生超出学习步骤边界的参数时,使用正则化。
5 对分类的温和介绍
本章涵盖
-
编写正式符号
-
使用逻辑回归
-
使用混淆矩阵
-
理解多类别分类
想象一个广告机构收集有关用户互动的信息,以决定显示哪种类型的广告。这不是不常见的事情。谷歌、推特、Facebook 和其他依赖广告的大科技公司拥有令人毛骨悚然的优秀用户个人资料,以帮助提供个性化广告。最近搜索过游戏键盘或显卡的用户可能更有可能点击关于最新和最伟大的视频游戏的广告。
向每个人发送专门定制的广告可能很困难,因此将用户分组到类别中是一种常见的技术。例如,一个用户可能被归类为游戏玩家,以接收相关的视频游戏相关广告。
机器学习是实现此类任务的常用工具。在最基本层面上,机器学习从业者希望构建一个工具来帮助他们理解数据。将数据项标记为属于不同的类别是针对特定需求表征数据的一种绝佳方式。
第四章讨论了回归,这是关于将曲线拟合到数据上。正如你所回忆的,最佳拟合曲线是一个函数,它接受一个数据项作为输入,并给它分配一个来自连续分布的数字。创建一个将离散标签分配给其输入的机器学习模型称为分类。分类是处理离散输出的监督学习算法。(每个离散值称为一个类别。)输入通常是特征向量,输出是一个类别。如果只有两个类别标签(True/False,On/Off,Yes/No),我们称这种学习算法为二元分类器;否则,它被称为多类别分类器。
有许多种类的分类器,但本章重点介绍表 5.1 中概述的类别。每个都有其优点和缺点,我们将在开始在每个 TensorFlow 中实现每个类别之后更深入地探讨。
线性回归是最容易实现的类型,因为我们已经在第三章和第四章中做了大部分艰苦的工作,但正如你将看到的,它是一个糟糕的分类器。一个更好的分类器是逻辑回归算法。正如其名所示,它使用对数属性来定义更好的成本函数。最后,softmax 回归是解决多类别分类的直接方法。它是逻辑回归的自然推广,被称为 softmax 回归,因为最后一步应用了一个名为softmax的函数。
表 5.1 分类器
| 类型 | 优点 | 缺点 |
|---|---|---|
| 线性回归 | 实现简单 | 不保证有效支持只支持二元标签 |
| 逻辑回归 | 高度准确灵活的方式对模型进行正则化以进行自定义调整模型响应是概率的度量。易于用新数据更新模型 | 只支持二元标签 |
| 软最大化回归 | 支持多类分类模型响应是概率的度量。 | 实现起来更复杂 |
5.1 形式化表示
在数学符号中,分类器是一个函数 y = f(x),其中 x 是输入数据项,而 y 是输出类别(图 5.1)。从传统的科学文献中借鉴,我们通常将输入向量 x 称为 自变量,将输出 y 称为 因变量。

图 5.1 分类器产生离散输出,但可能接受连续或离散输入。
形式上,类别标签被限制在可能的值范围内。你可以将二元标签视为 Python 中的布尔变量。当输入特征只有一组可能的值时,你需要确保你的模型能够理解如何处理这些值。因为模型中的函数通常处理连续的实数,你需要预处理数据集以考虑离散变量,这些变量可以是序数或名义的(图 5.2)。

图 5.2 存在两种类型的离散集合:那些可以排序的值(序数)和那些不能排序的值(名义)。
如其名所示,序数类型的值可以排序。例如,从 1 到 10 的偶数集合中的值是序数,因为整数可以相互比较。另一方面,来自 {banana, apple, orange} 水果集合的元素可能没有自然的排序。我们称此类集合的值为 名义值,因为它们只能通过其名称来描述。
在数据集中表示名义变量的一个简单方法是为每个标签分配一个数字。集合 {banana, apple, orange} 可以被处理为 {0, 1, 2}。但是,某些分类模型可能对数据的行为有强烈的偏见。例如,线性回归会将我们的苹果解释为香蕉和橙子的中间值,这没有任何自然的意义。
为了表示因变量的名义类别,一个简单的解决方案是为名义变量的每个值添加虚拟变量。在这个例子中,fruit 变量将被移除,并替换为三个单独的变量:banana、apple 和 orange。每个变量持有 0 或 1 的值(图 5.3),具体取决于该水果的类别是否为真。这个过程通常被称为 独热编码。
正如第三章和第四章中的线性回归一样,学习算法必须遍历底层模型支持的可能的函数,该模型称为M。在线性回归中,模型由w参数化。因此,函数y = M(w)可以尝试来衡量其成本。最终,我们选择具有最小成本的w值。回归和分类之间的唯一区别是输出不再是连续的谱,而是一组离散的类别标签。

图 5.3 如果变量的值是名义的,可能需要进行预处理。一个解决方案是将每个名义值视为一个布尔变量,如图中所示;“香蕉”、“苹果”和“橙子”是三个新添加的变量,每个变量都有0或1的值。原始的fruit变量被移除。
练习 5.1
将以下每个任务视为回归或分类任务,哪个更好?
(a) 预测股票价格
(b) 决定你应该买入、卖出或持有的股票
(c) 在 1-10 的尺度上评估计算机的质量
答案
(a) 回归
(b) 分类
(c) 或者
由于回归的输入/输出类型比分类更通用,没有任何东西阻止你在分类任务上运行线性回归算法。实际上,这正是你将在第 5.3 节中做的事情。
在开始实现 TensorFlow 代码之前,然而,重要的是要评估分类器的强度。第 5.2 节涵盖了衡量分类器成功度的最先进方法。
5.2 衡量性能
在开始编写分类算法之前,你应该能够检查你结果的成功率。本节涵盖了在分类问题中衡量性能的基本技术。
5.2.1 准确度
你还记得高中或大学时的那些多项选择题吗?机器学习中的分类问题与此类似。给定一个陈述,你的任务是将其分类为给定的多项选择题“答案”之一。如果你只有两个选择,就像在是非考试中一样,我们称之为二元分类器。在学校里的评分考试中,衡量你的分数的典型方式是计算正确答案的数量,并将其除以总问题数。
机器学习采用相同的评分策略,称之为准确度。准确度通过以下公式来衡量:

这个公式提供了一个粗略的性能总结,如果你只关心算法的整体正确性,这可能就足够了。但准确度度量并没有揭示每个标签的正确和错误结果的细分。
为了弥补这一限制,混淆矩阵为分类器的成功提供了更详细的报告。描述分类器表现好坏的一个有用方法是检查它在每个类别上的表现。
考虑一个具有正负标签的二分类器,例如。如图 5.4 所示,混淆矩阵是一个表格,它比较了预测响应与实际响应的比较。被正确预测为正的数据项称为真阳性(TP)。那些被错误预测为正的数据项称为假阳性(FP)。如果算法错误地将一个元素预测为负,而实际上它是正的,我们称这种情况为假阴性(FN)。最后,当预测和现实都认为一个数据项是负标签时,我们称之为真阴性(TN)。正如你所看到的,混淆矩阵使你能够轻松地看到模型在区分两个类别时混淆的频率。

图 5.4 你可以通过使用正(绿色勾号)和负(红色禁止)标签的矩阵来比较预测结果与实际结果。
注意:本书中包含许多彩色图形,这些图形可以在电子书版本中查看。要获取免费电子书(PDF、ePub 或 Kindle 格式),请访问mng.bz/JxPo注册您的印刷版书籍。
5.2.2 精确度和召回率
虽然真阳性(TP)、假阳性(FP)、真阴性(TN)和假阴性(FN)的定义都是单独有用的,但它们的相互作用才是力量的源泉。
真阳性与总正例的比率是精确度——一个衡量正预测可能正确的分数。图 5.4 中的左侧列是总正预测数(TP + FP),因此精确度的方程是

真阳性与所有可能正性的比率是召回率,它衡量了找到真阳性的比率。这是一个衡量成功预测(即召回)多少真阳性的分数。图 5.4 中的顶部行是正性的总数(TP + FN),因此召回率的方程是

简而言之,精确度是算法预测正确的预测的度量,而召回率是算法在最终集中识别出的正确事物的度量。如果精确度高于召回率,则模型在成功识别正确项目方面比未识别某些错误项目方面做得更好,反之亦然。
这里有一个快速示例。假设你正在尝试识别一组 100 张图片中的猫,其中 40 张是猫,60 张是狗。当你运行你的分类器时,10 只猫被识别为狗,20 只狗被识别为猫。你的混淆矩阵看起来像图 5.5。

图 5.5 用于评估分类算法性能的混淆矩阵示例
你可以在预测列的左侧看到猫的总数:30 个被正确识别,10 个未被识别,总共 40 个。
练习 5.2
猫的精确度和召回率是多少?系统的准确率是多少?
答案
对于猫,精确度是 30 / (30 + 20) 或 3/5,或 60%。召回率是 30 / (30 + 10) 或 3/4,或 75%。准确率是(30 + 40) / 100,或 70%。
5.2.3 受试者工作特征曲线
因为二元分类器是最受欢迎的工具之一,所以存在许多成熟的测量它们性能的技术,例如受试者工作特征(ROC)曲线,这是一种让你比较假正例和真正例之间权衡的图表。x 轴是假正例值的度量,y 轴是真正例值的度量。
二元分类器将输入特征向量减少到一个数字,然后根据这个数字是否大于或小于指定的阈值来决定类别。当你调整机器学习分类器的阈值时,你会在图表上绘制各种假正例和真正例率的值。
比较各种分类器的一种稳健方法是比较它们的 ROC 曲线。当两条曲线不相交时,一种方法肯定比另一种方法好。好的算法远高于基线。对于两种选择,它们比随机选择或 50/50 的猜测要好。比较分类器的一种定量方法是测量 ROC 曲线下的面积。如果一个模型的曲线下面积(AUC)值高于 0.9,它是一个优秀的分类器。随机猜测输出的模型将有一个大约 0.5 的 AUC 值。见图 5.6 的示例。

图 5.6 比较算法的原理方法是通过检查它们的 ROC 曲线。当每个情况下的真正率都大于假正率时,可以简单地声明一个算法在性能方面占主导地位。如果真正率小于假正率,图表会低于由虚线表示的基线。
练习 5.3
100%正确率(所有真正例,没有假正例)在 ROC 曲线上看起来会是什么样子?
答案
100%正确率的点将位于 ROC 曲线的正 y 轴上,如图 5.7 所示。

图 5.7 具有蓝色 ROC 曲线的 100%正确分类器,与垂直的真正例(y)轴并排
5.3 使用线性回归进行分类
实现分类器的一种最简单的方法是调整线性回归算法,如第三章所述。作为提醒,线性回归模型是一组看起来线性的函数:f(x) = wx。函数f(x)接受连续的实数作为输入,并产生连续的实数作为输出。记住,分类全都是关于离散输出的。因此,强制回归模型产生一个双值(二元)输出的方法之一是将高于某个阈值的值设置为数字(例如1),将低于该阈值的值设置为不同的数字(例如0)。
我们将使用以下激励性示例继续进行。假设 Alice 是一位热衷的国际象棋玩家,你拥有她胜负历史的记录。此外,每场比赛都有一个从 1 到 10 分钟的时间限制。你可以将每场比赛的结果绘制成如图 5.8 所示。横轴代表比赛的时间限制,纵轴表示她是否获胜(y = 1)或失败(y = 0)。

图 5.8 二元分类训练数据集的可视化。值被分为两类:所有 y = 1 的点以及所有 y = 0 的点。
从数据中可以看出,Alice 是一位思维敏捷的人:她总是赢得短局比赛。但她通常会在时间限制较长的比赛中失败。从图中,你可能会想要预测决定她是否会赢的关键比赛时间限制。
你想挑战她进行一场你确信能赢的比赛。如果你选择一个明显很长的比赛,比如 10 分钟,她会拒绝比赛。让我们将比赛时间设定得尽可能短,这样她就会愿意和你比赛,同时将平衡倾斜到你这边。
数据上的线性拟合为你提供了一些可以工作的东西。图 5.9 显示了使用列表 5.1(本节稍后出现)中的线性回归计算出的最佳拟合线。对于 Alice 可能会赢的比赛,这条线的值更接近1而不是0。看起来如果你选择一个当线的值小于0.5的时间(即 Alice 更有可能输而不是赢的时候),你就有很大的赢的机会。

图 5.9 在分类数据集上的最佳拟合线。显然,这条线并不很好地拟合数据,但它为分类新数据提供了一个不精确的方法。
这条线试图尽可能好地拟合数据。由于训练数据的性质,模型将对正例响应值为接近 1,对负例响应值为接近 0。由于你使用线来模拟这些数据,某些输入可能产生介于 0 和 1 之间的值。正如你可能想象的那样,太靠近某一类别的值将导致值大于 1 或小于 0。你需要一种方法来决定一个项目属于某一类别更多。通常,你选择中点 0.5 作为决定边界(也称为 阈值)。正如你所见,此过程使用线性回归进行分类。
练习 5.4
使用线性回归作为分类工具的缺点是什么?(参见列表 5.4 以获取提示。)
答案
线性回归对数据中的异常值敏感,因此它不是一个准确的分类器。
让我们编写你的第一个分类器!打开一个新的 Python 源文件,并将其命名为 linear.py。使用列表 5.1 编写代码。在 TensorFlow 代码中,你首先需要定义占位符节点,然后从 session.run() 语句中注入值。
列表 5.1 使用线性回归进行分类
import tensorflow as tf ❶
import numpy as np ❶
import matplotlib.pyplot as plt ❶
x_label0 = np.random.normal(5, 1, 10) ❷
x_label1 = np.random.normal(2, 1, 10) ❷
xs = np.append(x_label0, x_label1) ❷
labels = [0.] * len(x_label0) + [1.] * len(x_label1) ❸
plt.scatter(xs, labels) ❹
learning_rate = 0.001 ❺
training_epochs = 1000 ❺
X = tf.placeholder("float") ❻
Y = tf.placeholder("float") ❻
def model(X, w): ❼
return tf.add(tf.multiply(w[1], tf.pow(X, 1)), ❼
tf.multiply(w[0], tf.pow(X, 0))) ❼
w = tf.Variable([0., 0.], name="parameters") ❽
y_model = model(X, w) ❾
cost = tf.reduce_sum(tf.square(Y-y_model)) ❿
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ⓫
❶ 导入 TensorFlow 用于核心学习算法,NumPy 用于数据处理,以及 Matplotlib 用于可视化
❷ 初始化假数据,每个标签有 10 个实例,分别以 5 和 2 为中心,stddev 为 1
❸ 初始化相应的标签
❹ 绘制数据
❺ 声明超参数
❻ 设置输入/输出对的占位符节点
❼ 定义线性模型 y = w1 * x + w0
❽ 设置参数变量
❾ 定义一个辅助变量,因为你将多次引用它
❿ 定义成本函数
⓫ 定义学习参数的规则
在设计 TensorFlow 图之后,参见列表 5.2 了解如何打开新会话并执行图。train_op 更新模型的参数以更好地猜测。你通过循环多次运行 train_op,因为每一步都会迭代地改进参数估计。列表 5.2 生成与图 5.8 类似的图表。
列表 5.2 执行图
sess = tf.Session() ❶
init = tf.global_variables_initializer() ❶
sess.run(init) ❶
for epoch in range(training_epochs): ❷
sess.run(train_op, feed_dict={X: xs, Y: labels}) ❷
current_cost = sess.run(cost, feed_dict={X: xs, Y: labels}) ❸
if epoch % 100 == 0:
print(epoch, current_cost) ❹
w_val = sess.run(w) ❺
print('learned parameters', w_val) ❺
sess.close() ❻
all_xs = np.linspace(0, 10, 100) ❼
plt.plot(all_xs, all_xs*w_val[1] + w_val[0]) ❼
plt.show() ❼
❶ 打开一个新的会话并初始化变量
❷ 多次运行学习操作
❸ 记录使用当前参数计算的成本
❹ 在代码运行时打印日志信息
❺ 打印学习到的参数
❻ 当不再使用时关闭会话
❼ 显示最佳拟合线
要衡量成功,你可以计算正确预测的数量并计算成功率。在列表 5.3 中,你向之前的 linear.py 代码中添加了两个节点:correct_prediction 和 accuracy。然后你可以打印 accuracy 的值来查看成功率。代码可以在关闭会话之前执行。
列表 5.3 测量准确率
correct_prediction = tf.equal(Y, tf.to_float(tf.greater(y_model, 0.5))) ❶
accuracy = tf.reduce_mean(tf.to_float(correct_prediction)) ❷
print('accuracy', sess.run(accuracy, feed_dict={X: xs, Y: labels})) ❸
print('correct_prediction', predict_val) ❹
❶ 当模型的响应大于 0.5 时,它应该是一个正标签,反之亦然。
❷ 计算成功率
❸ 打印从提供的输入中得出的成功度量
❹ 打印正确的预测
列表 5.3 中的代码产生了以下输出:
('learned parameters', array([ 1.2816, -0.2171], dtype=float32))
('accuracy', 0.95)
correct_prediction [ True False True True True True True True True
➥ True True True
True True True True True True True False]
如果分类那么简单,这一章现在就已经结束了。不幸的是,如果你在更极端的数据(也称为 异常值)上训练,线性回归方法会彻底失败。
假设 Alice 失去了一场耗时 20 分钟的游戏。你在一个包含这个新异常数据点的数据集上训练分类器。列表 5.4 将游戏时间中的一个值替换为 20。让我们看看引入异常值如何影响分类器的性能。
列表 5.4 线性回归在分类中彻底失败
x_label0 = np.append(np.random.normal(5, 1, 9), 20)
当你使用这些更改重新运行代码时,你会看到一个类似于图 5.10 的结果。

图 5.10 新的训练元素值 20 对最佳拟合线有重大影响。该线对异常值数据过于敏感;因此,线性回归是一个粗略的分类器。
原始分类器建议你可以在三分钟的游戏中打败 Alice。她可能会同意玩这样短的游戏。但修订后的分类器,如果你坚持相同的 0.5 阈值,则暗示她最短会输掉的游戏是五分钟的。她可能会拒绝玩这么长时间的游戏!
5.4 使用逻辑回归
逻辑回归提供了一个具有关于准确性和性能的理论保证的分析函数。它类似于线性回归,只是你使用不同的代价函数并对模型响应函数进行轻微变换。
让我们重新审视这里显示的线性函数:
y (x) = wx
在线性回归中,具有非零斜率的线可能从负无穷大到无穷大。如果分类的唯一合理结果是 0 或 1,那么拟合具有该特性的函数将是直观的。幸运的是,图 5.11 中描绘的 Sigmoid 函数迅速收敛到 0 或 1。

图 5.11 Sigmoid 函数的可视化
当 x 为 0 时,Sigmoid 函数的结果为 0.5。随着 x 的增加,函数收敛到 1。而当 x 减少到负无穷大时,函数收敛到 0。
在逻辑回归中,我们的模型是 sig(linear(``x``))。结果证明,该函数的最佳拟合参数暗示了两个类别之间的线性分离。这条分离线也称为 线性决策边界。
5.4.1 解决一维逻辑回归
在逻辑回归中使用的成本函数与你在线性回归中使用的不同。虽然你可以使用之前相同的成本函数,但它不会那么快,也不能保证得到最优解。这里的罪魁祸首是 sigmoid 函数,因为它使得成本函数有很多“峰值”。TensorFlow 和大多数其他机器学习库与简单的成本函数配合得最好。学者们已经找到了一种巧妙的方法来修改成本函数,以便在逻辑回归中使用 sigmoid 函数。
实际值(y)和模型响应(h)之间新的成本函数将是一个两部分方程:

你可以将这两个方程压缩成一个长方程:
Cost(y, h) = –ylog(h) – (1 – y)log(1 – h)
这个函数恰好具有高效和最优学习的所需品质。具体来说,它是凸的,但不必过于担心这意味着什么。你试图最小化成本:把成本想成高度,把成本函数想成地形。你试图找到地形中的最低点。如果没有任何地方可以向上爬,那么在地形中找到最低点会容易得多。这样的地方被称为凸性。这里没有山丘。
你可以把这个函数想象成一个球从山上滚下来。最终,球会落在底部,这是最优点。非凸函数可能有一片崎岖的地形,这使得很难预测球会滚到哪里。它甚至可能不会落在最低点。你的函数是凸的,所以算法会很容易地找出如何最小化这个成本并将“球滚下山”。
凸性很好,但在选择成本函数时,正确性也是一个重要的标准。你怎么知道这个成本函数确实做了你想要它做的事情呢?为了直观地回答这个问题,请看图 5.12。当你希望你的期望值为1时,你使用-log(x)来计算成本(注意:-log(1) = 0)。算法不会将值设置为 0,因为成本会趋向于无穷大。将这些函数相加得到一个曲线,在0和1处都趋向于无穷大,负的部分相互抵消。

图 5.12 这里展示了两个成本函数如何惩罚0和1处的值。注意,左边的函数对0的惩罚很重,但在1处没有成本。右边的成本函数显示了相反的现象。
当然,图表是一种非正式的方式来说服你选择成本函数时凸性的重要性,但关于为什么成本函数是最优的技术的讨论超出了本书的范围。如果你对数学感兴趣,你可能会对学习到成本函数是从最大熵原理推导出来的感到兴趣,你可以在网上任何地方查找相关信息。
请参阅图 5.13,以查看一维数据集上逻辑回归的最佳拟合结果。你生成的 sigmoid 曲线将提供比线性回归更好的线性决策边界。

图 5.13 这里是一个二元分类数据集的最佳拟合 sigmoid 曲线。注意曲线位于y = 0和y = 1之间。这样,这条曲线对异常值不太敏感。
你会开始注意到代码列表中的模式。在 TensorFlow 的简单/典型使用中,你生成一个假数据集,定义占位符,定义变量,定义模型,在该模型上定义一个成本函数(通常是均方误差或均方对数误差),通过使用梯度下降创建train_op,迭代地提供示例数据(可能带有标签或输出),并最终收集优化值。创建一个新的源文件名为 logistic_1d.py,并将列表 5.5 复制到其中,它生成了图 5.13。
列表 5.5 使用一维逻辑回归
import numpy as np ❶
import tensorflow as tf ❶
import matplotlib.pyplot as plt ❶
learning_rate = 0.01 ❷
training_epochs = 1000 ❷
def sigmoid(x): ❸
return 1\. / (1\. + np.exp(-x)) ❸
x1 = np.random.normal(-4, 2, 1000) ❹
x2 = np.random.normal(4, 2, 1000) ❹
xs = np.append(x1, x2) ❹
ys = np.asarray([0.] * len(x1) + [1.] * len(x2)) ❹
plt.scatter(xs, ys) ❺
X = tf.placeholder(tf.float32, shape=(None,), name="x") ❻
Y = tf.placeholder(tf.float32, shape=(None,), name="y") ❻
w = tf.Variable([0., 0.], name="parameter", trainable=True) ❼
y_model = tf.sigmoid(w[1] * X + w[0]) ❽
cost = tf.reduce_mean(-Y * tf.log(y_model) - (1 - Y) * tf.log(1 - y_model))❾
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❿
with tf.Session() as sess: ⓫
sess.run(tf.global_variables_initializer()) ⓫
prev_err = 0 ⓬
for epoch in range(training_epochs): ⓭
err, _ = sess.run([cost, train_op], {X: xs, Y: ys}) ⓮
print(epoch, err)
if abs(prev_err - err) < 0.0001: ⓯
break
prev_err = err ⓰
w_val = sess.run(w, {X: xs, Y: ys}) ⓱
all_xs = np.linspace(-10, 10, 100) ⓲
plt.plot(all_xs, sigmoid((all_xs * w_val[1] + w_val[0]))) ⓲
plt.show() ⓲
❶ 导入相关库
❷ 设置超参数
❸ 定义一个辅助函数来计算 sigmoid 函数
❹ 初始化假数据
❺ 可视化数据
❻ 定义输入/输出占位符
❼ 定义参数节点
❽ 使用 TensorFlow 的 sigmoid 函数定义模型
❾ 定义交叉熵损失函数
❿ 定义要使用的最小化器
⓫ 打开一个会话并定义所有变量
⓬ 定义一个变量来跟踪前一个错误
⓭ 迭代直到收敛或达到最大 epoch 数
⓮ 计算成本并更新学习参数
⓯ 检查收敛性。如果你每次迭代的改变小于< .01%,则完成。
⓰ 更新前一个错误值
⓱ 获取学习到的参数值
⓲ 绘制学习到的 sigmoid 函数
就这样!如果你在与 Alice 下棋,你现在将有一个二元分类器来决定何时棋局可能赢得或输掉。
TensorFlow 中的交叉熵损失
如列表 5.5 所示,使用tf.reduce_mean操作对每个输入/输出对进行交叉熵损失的求平均值。TensorFlow 库还提供了一个方便且更通用的函数,称为tf.nn.softmax_cross_entropy_with_logits。你可以在官方文档中了解更多信息,链接为 http://mng.bz/8mEk。
5.4.2 解决二维回归
现在我们将探索如何使用多个独立变量进行逻辑回归。独立变量的数量对应于维度数。在我们的情况下,二维逻辑回归问题将尝试标记一对独立变量。本节中你学到的概念可以推广到任意维度。
注意:假设你在考虑购买一部新手机。你唯一关心的属性是(1)操作系统,(2)尺寸和(3)成本。目标是决定一部手机是否值得购买。在这种情况下,有三个独立变量(手机的属性)和一个因变量(是否值得购买)。因此,我们将这个问题视为一个分类问题,其中输入向量是 3D。
考虑图 5.14 所示的数据集,它代表了一个城市中两个帮派的犯罪活动。第一维是 x 轴,可以认为是纬度,第二维是 y 轴,代表经度。在 (3, 2) 附近有一个簇,在 (7, 6) 附近还有一个簇。你的任务是决定哪个帮派最有可能对发生在位置 (6, 4) 的新犯罪负责。

图 5.14 x 轴和 y 轴代表两个独立变量。因变量持有两个可能的标签,由绘制点的形状和颜色表示。
另一种可视化图 5.14 的方法是,将独立变量 x=latitude 和 y=longitude 投影为 2D 平面,然后绘制垂直轴作为 sigmoid 函数的结果,该函数代表了一热编码的类别。你可以将函数可视化如图 5.15 所示。

图 5.15 另一种可视化两个独立变量的方法,这次考虑了由 sigmoid 函数定义的因变量,该函数代表帮派 1(绿色)和帮派 2(红色)的一热编码类别。
创建一个名为 logistic_2d.py 的新源文件,并按照列表 5.6 进行操作。
列表 5.6 设置 2D 逻辑回归数据
import numpy as np ❶
import tensorflow as tf ❶
import matplotlib.pyplot as plt ❶
learning_rate = 0.1 ❷
training_epochs = 2000 ❷
def sigmoid(x): ❸
return 1\. / (1\. + np.exp(-x)) ❸
x1_label1 = np.random.normal(3, 1, 1000) ❹
x2_label1 = np.random.normal(2, 1, 1000) ❹
x1_label2 = np.random.normal(7, 1, 1000) ❹
x2_label2 = np.random.normal(6, 1, 1000) ❹
x1s = np.append(x1_label1, x1_label2) ❹
x2s = np.append(x2_label1, x2_label2) ❹
ys = np.asarray([0.] * len(x1_label1) + [1.] * len(x1_label2)) ❹
❶ 导入相关库
❷ 设置超参数
❸ 定义一个辅助 sigmoid 函数
❹ 初始化假数据
你有两个独立变量 (x[1] 和 x[2])。一个简单的方式来建模输入 x 和输出 M(x) 之间的映射是以下方程,其中 w 是 TensorFlow 要找到的参数:
M(x, v) = sig(w[2]x[2] + w[1]x[1] + w[0])
在列表 5.7 中,你将实现方程及其相应的成本函数来学习参数。
列表 5.7 使用 TensorFlow 进行多维逻辑回归
X1 = tf.placeholder(tf.float32, shape=(None,), name="x1") ❶
X2 = tf.placeholder(tf.float32, shape=(None,), name="x2") ❶
Y = tf.placeholder(tf.float32, shape=(None,), name="y") ❶
w = tf.Variable([0., 0., 0.], name="w", trainable=True) ❷
y_model = tf.sigmoid(w[2] * X2 + w[1] * X1 + w[0]) ❸
cost = tf.reduce_mean(-tf.log(y_model * Y + (1 - y_model) * (1 - Y))) ❹
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❹
with tf.Session() as sess: ❺
sess.run(tf.global_variables_initializer()) ❺
prev_err = 0 ❺
for epoch in range(training_epochs): ❺
err, _ = sess.run([cost, train_op], {X1: x1s, X2: x2s, Y: ys}) ❺
print(epoch, err) ❺
if abs(prev_err - err) < 0.0001: ❺
break ❺
prev_err = err ❺
w_val = sess.run(w, {X1: x1s, X2: x2s, Y: ys}) ❻
x1_boundary, x2_boundary = [], [] ❼
for x1_test in np.linspace(0, 10, 100): ❽
for x2_test in np.linspace(0, 10, 100): ❽
z = sigmoid(-x2_test*w_val[2] - x1_test*w_val[1] - w_val[0]) ❾
if abs(z - 0.5) < 0.01: ❾
x1_boundary.append(x1_test) ❾
x2_boundary.append(x2_test) ❾
plt.scatter(x1_boundary, x2_boundary, c='b', marker='o', s=20) ❿
plt.scatter(x1_label1, x2_label1, c='r', marker='x', s=20) ❿
plt.scatter(x1_label2, x2_label2, c='g', marker='1', s=20) ❿
plt.show() ❿
❶ 定义输入/输出占位符节点
❷ 定义参数节点
❸ 定义 sigmoid 模型,使用两个输入变量
❹ 定义学习步长
❺ 创建一个新的会话,初始化变量,并学习参数直到收敛
❻ 在关闭会话之前获取学习到的参数值
❼ 定义用于存储边界点的数组
❽ 遍历点窗口
❾ 如果模型响应接近 0.5,则更新边界点
❿ 显示边界线以及数据
图 5.16 描述了从训练数据中学习到的线性边界线。发生在这条线上的犯罪事件,被两个帮派犯下的可能性是相等的。

图 5.16 对角虚线表示两个决策之间的概率平均分割。当数据点远离该线时,决策的信心增加。
5.5 多类分类器
到目前为止,你已经处理了多维输入,但没有处理多变量输出,如图 5.17 所示。在数据上不是二进制标签,而是有 3、4 或 100 个类别怎么办?逻辑回归需要两个标签——不再多了。

图 5.17 独立变量是二维的,由 x 轴和 y 轴表示。因变量可以是三个标签之一,由数据点的颜色和形状表示。
例如,图像分类是一个流行的多变量分类问题,因为目标是决定图像的类别。一张照片可能被归类到数百个类别之一。
要处理超过两个标签,你可以巧妙地重复使用逻辑回归(使用一对多或一对一方法)或开发一种新的方法(softmax 回归)。我们将在以下各节中查看每种方法。逻辑回归方法需要相当多的临时工程,所以让我们专注于 softmax 回归。
5.5.1 一对多
首先,为每个标签训练一个分类器,如图 5.18 所示。如果有三个标签,你就有三个可用的分类器:f1、f2 和 f3。要在新数据上进行测试,运行每个分类器以查看哪个分类器产生了最自信的响应。直观地说,通过响应最自信的分类器的标签来标记新的点。

图 5.18 一对多是一种多类分类方法,它需要每个类别的检测器。
5.5.2 一对一
然后,为每对标签训练一个分类器(见图 5.19)。如果有三个标签,那就是三个独特的对。但对于 k 个标签,那就是 k(k - 1)/2 对标签。在新数据上,运行所有分类器并选择获胜最多的类别。

图 5.19 在一对一多类分类中,每个类别对都有一个检测器。
5.5.3 Softmax 回归
Softmax 回归是以传统的 max 函数命名的,它接受一个向量并返回最大值。但 softmax 并不是 max 函数,因为它具有连续性和可微分的额外优势。因此,它具有对随机梯度下降有效工作的有益特性。
在这种多类分类设置中,每个类别对每个输入向量都有一个置信度(或概率)分数。softmax 步骤选择得分最高的输出。
打开一个名为 softmax.py 的新文件,并按照列表 5.8 进行操作。首先,你将可视化假数据以重现图 5.17(也在此处重现为图 5.20)

图 5.20 多输出分类的 2D 训练数据
列表 5.8 可视化多类数据
import numpy as np ❶
import matplotlib.pyplot as plt ❶
x1_label0 = np.random.normal(1, 1, (100, 1)) ❷
x2_label0 = np.random.normal(1, 1, (100, 1)) ❷
x1_label1 = np.random.normal(5, 1, (100, 1)) ❸
x2_label1 = np.random.normal(4, 1, (100, 1)) ❸
x1_label2 = np.random.normal(8, 1, (100, 1)) ❹
x2_label2 = np.random.normal(0, 1, (100, 1)) ❹
plt.scatter(x1_label0, x2_label0, c='r', marker='o', s=60) ❺
plt.scatter(x1_label1, x2_label1, c='g', marker='x', s=60) ❺
plt.scatter(x1_label2, x2_label2, c='b', marker='_', s=60) ❺
plt.show() ❺
❶ 导入 NumPy 和 Matplotlib
❷ 生成靠近(1, 1)的点
❸ 生成靠近(5, 4)的点
❹ 生成靠近(8, 0)的点
❺ 在散点图上可视化三个标签
接下来,在列表 5.9 中,你设置了训练和测试数据,为 softmax 回归步骤做准备。标签必须表示为一个向量,其中只有一个元素是 1,其余都是 0。这种表示称为one-hot 编码。如果有三个标签,它们将表示为以下向量:[1, 0, 0],[0, 1, 0],和[0, 0, 1]。
练习 5.5
One-hot 编码可能看起来是一个不必要的步骤。为什么不使用 1D 输出,其中值为1、2和3来表示三个类别呢?
答案
回归可能在输出中诱导出语义结构。如果输出相似,回归意味着它们的输入也相似。如果你使用一个维度,你就是在暗示标签 2 和 3 比 1 和 3 更相似。你必须小心不要做出不必要的或错误的假设,因此使用 one-hot 编码是一个安全的赌注。
列表 5.9 设置多类分类的训练和测试数据
xs_label0 = np.hstack((x1_label0, x2_label0)) ❶
xs_label1 = np.hstack((x1_label1, x2_label1)) ❶
xs_label2 = np.hstack((x1_label2, x2_label2)) ❶
xs = np.vstack((xs_label0, xs_label1, xs_label2)) ❶
labels = np.matrix([[1., 0., 0.]] * len(x1_label0) + [[0., 1., 0.]] *
➥ len(x1_label1) + [[0., 0., 1.]] * len(x1_label2)) ❷
arr = np.arange(xs.shape[0]) ❸
np.random.shuffle(arr) ❸
xs = xs[arr, :] ❸
labels = labels[arr, :] ❸
test_x1_label0 = np.random.normal(1, 1, (10, 1)) ❹
test_x2_label0 = np.random.normal(1, 1, (10, 1)) ❹
test_x1_label1 = np.random.normal(5, 1, (10, 1)) ❹
test_x2_label1 = np.random.normal(4, 1, (10, 1)) ❹
test_x1_label2 = np.random.normal(8, 1, (10, 1)) ❹
test_x2_label2 = np.random.normal(0, 1, (10, 1)) ❹
test_xs_label0 = np.hstack((test_x1_label0, test_x2_label0)) ❹
test_xs_label1 = np.hstack((test_x1_label1, test_x2_label1)) ❹
test_xs_label2 = np.hstack((test_x1_label2, test_x2_label2)) ❹
test_xs = np.vstack((test_xs_label0, test_xs_label1, test_xs_label2)) ❹
test_labels = np.matrix([[1., 0., 0.]] * 10 + [[0., 1., 0.]] * 10 + [[0., 0.,❹
➥ 1.]] * 10)
train_size, num_features = xs.shape ❺
❶ 将所有输入数据合并到一个大矩阵中
❷ 创建相应的 one-hot 标签
❸ 打乱数据集
❹ 构建测试数据集和标签
❺ 数据集的形状告诉你每个示例的示例数量和特征数量。
你在列表 5.9 中看到了hstack和vstack方法的用法,分别对应水平堆叠和垂直堆叠——这两个函数来自 NumPy 库。hstack函数接收数组并将它们按顺序水平(列向)堆叠,而vstack函数接收数组并将它们按顺序垂直(行向)堆叠。例如,列表 5.8 中的x1_label0和x2_label0打印出来时看起来像这样:
print(x1_label0)
[[ 1.48175716]
[ 0.34867807]
[-0.35358866]
...
[ 0.77637156]
[ 0.9731792 ]]
print(x2_label0)
[[ 2.02688 ]
[ 2.37936835]
[ 0.24260849]
...
[ 1.58274368]
[-1.55880602]]
xs_label0变量的结果值可能看起来像这样:
array([[ 1.48175716, 2.02688 ],
[ 0.34867807, 2.37936835],
[-0.35358866, 0.24260849],
[ 0.60081539, -0.97048316],
[ 2.61426058, 1.8768225 ],
...
[ 0.77637156, 1.58274368],
[ 0.9731792 , -1.55880602]])
最后,在列表 5.10 中,你使用了 softmax 回归。与逻辑回归中的 sigmoid 函数不同,这里你使用了 TensorFlow 库提供的softmax函数。softmax函数类似于max函数,它从一系列数字中输出最大值。它被称为softmax,因为它是对max函数的“软”或“平滑”近似,而max函数是不平滑或不连续的(这是不好的)。连续且平滑的函数通过反向传播促进了神经网络正确权重的学习。
练习 5.6
以下哪个函数是连续的?
f(x) = x2
f(x) = min(x, 0)
f(x) = tan(x)
答案
前两个是连续的。最后一个 tan(x) 有周期性渐近线,因此对于某些值没有有效的结果。
列表 5.10 使用 softmax 回归
import tensorflow as tf
learning_rate = 0.01 ❶
training_epochs = 1000 ❶
num_labels = 3 ❶
batch_size = 100 ❶
X = tf.placeholder("float", shape=[None, num_features]) ❷
Y = tf.placeholder("float", shape=[None, num_labels]) ❷
W = tf.Variable(tf.zeros([num_features, num_labels])) ❸
b = tf.Variable(tf.zeros([num_labels])) ❸
y_model = tf.nn.softmax(tf.matmul(X, W) + b) ❹
cost = -tf.reduce_sum(Y * tf.log(y_model)) ❺
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❺
correct_prediction = tf.equal(tf.argmax(y_model, 1), tf.argmax(Y, 1)) ❻
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) ❻
❶ 定义超参数
❷ 定义输入/输出占位符节点
❸ 定义模型参数
❹ 设计 softmax 模型
❺ 设置学习算法
❻ 定义一个操作来测量成功率
现在你已经定义了 TensorFlow 计算图,从会话中执行它。这次你将尝试一种新的迭代更新参数的形式,称为 批量学习。你不会一次只传递一个数据片段,而是会对数据批次运行优化器。这种技术可以加快速度,但引入了收敛到局部最优解而不是全局最优解的风险。使用列表 5.11 以批量运行优化器。
列表 5.11 执行图
with tf.Session() as sess: ❶
tf.global_variables_initializer().run() ❶
for step in range(training_epochs * train_size // batch_size): ❷
offset = (step * batch_size) % train_size ❸
batch_xs = xs[offset:(offset + batch_size), :] ❸
batch_labels = labels[offset:(offset + batch_size)]
err, _ = sess.run([cost, train_op], feed_dict={X: batch_xs, Y:
➥ batch_labels}) ❹
print (step, err) ❺
W_val = sess.run(W) ❻
print('w', W_val) ❻
b_val = sess.run(b) ❻
print('b', b_val) ❻
print("accuracy", accuracy.eval(feed_dict={X: test_xs, Y:
➥ test_labels})) ❼
❶ 打开一个新的会话并初始化所有变量
❷ 仅循环足够次数以完成对数据集的单次遍历
❸ 获取与当前批次对应的数据集子集
❹ 在这个批次上运行优化器
❺ 打印持续结果
❻ 打印最终的学到的参数
❷ 打印成功率
在数据集上运行 softmax 回归算法的最终输出是
('w', array([[-2.101, -0.021, 2.122],
[-0.371, 2.229, -1.858]], dtype=float32))
('b', array([10.305, -2.612, -7.693], dtype=float32))
Accuracy 1.0
你已经学会了模型的权重和偏差。你可以使用这些学习到的参数来对测试数据进行推断。这样做的一个简单方法是使用 TensorFlow 的 Saver 对象(见 www.tensorflow.org/guide/saved_model)保存和加载变量。你可以运行模型(在我们的代码中称为 y_model)以获得测试数据上的模型响应。
5.6 分类应用
情感是一个难以量化的概念。快乐、悲伤、愤怒、兴奋和恐惧是主观性情感的例子。对某个人来说可能令人兴奋的事情,对另一个人来说可能显得讽刺。对一些人来说似乎传达愤怒的文本,对其他人来说可能传达恐惧。如果人类都有这么多麻烦,那么计算机又能有什么运气呢?
至少,机器学习研究人员已经找到了在文本中分类正面和负面情感的方法。假设你正在构建一个类似亚马逊的网站,每个商品都有用户评论。你希望你的智能搜索引擎优先选择正面评论的商品。你可能拥有的最佳指标是平均星级评分或点赞数量。但如果你有很多没有明确评分的重文本评论怎么办呢?
情感分析可以被视为一个二元分类问题。输入是自然语言文本,输出是一个二元决策,推断出正面或负面情感。以下是一些你可以在线找到的用于解决这个确切问题的数据集:
-
大型电影评论数据集—
mng.bz/60nj -
情感标注句子数据集—
mng.bz/CzSM -
Twitter 情感分析数据集—
mng.bz/2M4d
最大的挑战是如何将原始文本表示为分类算法的输入。在整个这一章中,分类的输入始终是一个特征向量。将原始文本转换为特征向量的最古老的方法之一被称为词袋模型。你可以在mng.bz/K8yz找到关于它的良好教程和代码实现。
摘要
-
解决分类问题有许多方法,但在准确性和性能方面,逻辑回归和 softmax 回归是最稳健的两种。
-
在运行分类之前预处理数据是很重要的。例如,离散独立变量可以被调整为二元变量。
-
到目前为止,你都是从回归的角度来处理分类的。在后面的章节中,你将通过使用神经网络重新审视分类。
-
有多种方法可以处理多类分类。没有明确的答案说明你应该先尝试哪一种:一对多、多对一,还是 softmax 回归。但 softmax 方法稍微更少需要手动操作,并允许你调整超参数。
6 情感分类:大型电影评论数据集
本章涵盖
-
使用文本和词频(词袋模型)来表示情感
-
使用逻辑回归和 softmax 构建情感分类器
-
衡量分类精度
-
计算 ROC 曲线并衡量分类器的有效性
-
将你的结果提交到 Kaggle 的电影评论挑战赛
现在,机器学习的神奇用途之一,让每个人都印象深刻的是教会计算机从文本中学习。随着社交媒体、短信、Facebook Messenger、WhatsApp、Twitter 和其他来源每天产生数百亿条文本消息,可供学习的文本资源是充足的。
TIP 查看这个著名的信息图表,展示了每天从各种媒体平台到达的大量文本数据:mng.bz/yrXq。
社交媒体公司、电话提供商和应用程序制造商都在尝试使用你发送的消息来做出决策和分类你。你是否曾给你的另一半发送了一条关于你午餐吃的泰国菜的短信,然后后来在你的社交媒体上看到了弹出广告,推荐你去新的泰国餐厅?虽然听起来很可怕,好像大哥在试图识别和理解你的饮食习惯,但在线流媒体服务公司也在使用实际应用来尝试确定你是否喜欢他们的产品。
在观看一部电影后,你是否曾花时间发表一个简单的评论,比如“哇,那是一部很棒的电影!我喜欢比尔的表现!”或者“那部电影非常不合适,超过了三个小时,一开始对血腥场面感到厌恶,后来因为没有任何情节而睡着了!”(好吧,诚实地讲,我可能在上一个在线平台上写了最后那条评论。)YouTube 以其用户不仅来观看视频和病毒性内容,还参与阅读评论——查看电影、视频和其他数字媒体的内容的书面评论而闻名。这些评论很简单,因为你可以说一句话或两句话,表达你的感受,然后继续你的生活。有时,评论非常有趣,或愤怒,或极端积极;最终,它们涵盖了在线参与者观看内容时可能体验到的所有情绪范围。
这些情感对在线媒体服务公司非常有用。如果有一个简单的分类情感的方法,公司可以确定某个名人视频是否引发了极端的悲伤或极端积极的反应。反过来,如果公司能够首先分类,然后将这些情感与你接下来所做的事情相关联——如果你在看完电影后提供了一些积极的评论并点击了一个链接购买主演的电影,那么他们就有了一个完整的因果关系流程。媒体公司可以生成更多这样的内容或向你展示更多你感兴趣的内容类型。这样做可能会增加收入,比如如果你的积极反应导致你在之后购买了与该名人相关的东西。
你正在学习一种使用机器学习对输入数据进行分类的方法,并通过分类为该输入生成一些标签。情感可以有两种思考方式:首先作为二元情感(如正面/负面反应),然后作为多类情感(如仇恨、悲伤、中性、喜欢或爱)。你学习了以下两种处理这些情况的技术,你将在本章中尝试这些技术:
-
用于二元情感的逻辑回归
-
用于多类分类的 Softmax 回归
在这种情况下,输入的挑战在于它是文本,而不是像我们在第五章中由我们信任的 NumPy 库为我们生成的那些随机数据点那样的美好输入向量。幸运的是,文本和信息检索社区已经开发了一种将文本映射到数值特征向量的技术——非常适合机器学习。这种技术被称为词袋模型。
6.1 使用词袋模型
词袋模型是自然语言处理(NLP)中的一个方法,它将句子形式的文本作为输入,通过考虑提取的词汇和它们出现的频率,将其转换为一个特征向量。之所以命名为“词袋”,是因为每个单词的频率计数就像一个“袋子”,其中每个单词的出现都是一个袋子中的项目。词袋模型是一个最先进的模型,它允许你将电影评论转换为特征向量,这将用于对其情感进行分类。考虑以下关于迈克尔·杰克逊电影的评论片段文本:
With all this stuff going down at the moment with MJ i've started listening to his music, watching the odd documentary here and there, watched The Wiz and watched Moonwalker again.
将词袋模型应用于处理此评论的第一步是预处理文本,并仅提取具有实际意义的单词。通常,这个过程涉及删除任何非字母字符——如数字、HTML 标签等注释和标点符号——并将文本简化为其基本单词。之后,该方法将剩余的单词子集减少到名词、动词或形容词,并去除冠词、连词和其他停用词——这些词不是文本本身的区分性特征。
注意:许多现成的停用词列表可供使用。Python 自然语言工具包(NLTK)中使用的列表是一个很好的起点;您可以在gist.github.com/sebleier/554280找到它们。停用词通常是语言特定的,因此您需要确保您使用的列表适合您正在处理的语言。幸运的是,NLTK 目前可以处理 21 种语言的停用词。您可以在mng.bz/MoPn了解更多信息。
当这一步完成时,词袋模型生成剩余词汇词的计数直方图,该直方图成为输入文本的指纹。通常,通过将计数除以最大计数来归一化指纹,从而得到介于 0 和 1 之间的值特征向量。整个过程如图 6.1 所示。

图 6.1 词袋模型的视觉表示。文本被分析和清理,然后计数形成直方图,然后归一化以获得输入文本的特征向量表示。
6.1.1 将词袋模型应用于电影评论
要开始使用词袋模型,您需要一些评论文本。Kaggle 词袋模型与爆米花袋挑战是一个优秀的、已经完成的比赛,该比赛分析了来自互联网电影数据库(IMDb.com)的 50,000 部电影评论,以生成这些电影评论的情感分类。您可以在mng.bz/aw0B了解更多关于这个挑战的信息。您将在本章中使用这些评论来构建情感分类器。
要开始,从mng.bz/ggEE获取 labeledTrainData.tsv 文件,并将其保存到您的本地驱动器上。您还想要下载 testData.tsv 文件,从 mng.bz/emWv;您稍后会使用该文件。这些文件格式为制表符分隔值(TSV)文件,列对应于唯一标识符(id)、情感(1 表示正面或 0 表示负面)以及每行的 HTML 格式评论。
让我们尝试我们的词袋模型,并创建一个函数来处理从输入 labeledTrainData.tsv 文件创建机器学习准备好的输入特征。打开一个名为 sentiment_classifier.ipynb 的新笔记本,并创建一个 review_to_words 函数。该函数首先通过调用 Tika Python 库将 IMDb 的 HTML 评论转换为评论文本。Tika Python 是一个内容分析库,其主要功能包括文件类型识别、从 1400 多种格式中提取文本和元数据以及语言识别。
TIP Tika 的完整解释是我写的另一本 Manning 书籍的主题。认真地说,查看 Tika in Action (www.manning.com/books/tika-in-action)。在这里,你使用它来移除文本中的所有 HTML 标签,使用parser接口和其from_buffer方法,该方法接受一个字符串缓冲区作为输入,并输出 HTML 解析器关联的提取文本。
拥有提取的评论文本后,使用 Python 的re(用于正则表达式)模块,使用一个常见的模式[^a-zA-z],这意味着从字符串的开始(^符号)扫描并识别仅大写和小写字母a到z,并将其他所有内容替换为空白字符。
下一步是将文本全部转换为小写。单词的大小写对于解释句子或语言有含义,但对于独立于结构的单词出现次数计数则意义不大。接下来,通过 Python 的 NLTK 库移除停用词,包括连词和冠词。该库支持 21 种语言的停用词,因此你会使用英语的停用词,因为你正在处理 IMDb 的英语评论。最后一步是将剩余的单词作为一个字符串连接起来。列表 6.1 的输出是原始列表的简化版本,只包含有意义的单词和没有 HTML——换句话说,是干净的文本。这个干净的文本将是 Bag of Words 模型的实际输入。
列表 6.1 从评论的输入文本创建特征
from tika import parser
from nltk.corpus import stopwords
import re
def review_to_words( raw_review ):
review_text = parser.from_buffer( "<html>" + raw_review + "</html>"
➥ )["content"] ❶
letters_only = re.sub("[^a-zA-Z]", " ", review_text) ❷
words = letters_only.lower().split() ❸
stops = set(stopwords.words("english")) ❹
meaningful_words = [w for w in words if not w in stops] ❺
return( " ".join( meaningful_words )) ❻
❶ 函数通过使用 Apache Tika 将原始评论转换为单词字符串
❷ 移除非字母字符
❸ 转换为小写,分割成单个单词
❹ 将停用词转换为集合,这比在列表中搜索要快得多
❺ 移除停用词
❻ 将单词重新组合成一个由空格分隔的字符串
有了我们生成干净评论文本的函数,你就可以开始在该标签化训练数据.tsv 中的 25,000 条评论上运行该函数。但首先,你需要将这些评论加载到 Python 中。
6.1.2 清理所有电影评论
一个方便的库是 Pandas 库,它可以高效地将 TSV 加载到 Python 中,用于创建、操作和保存数据框。你可以将数据框想象成一个机器学习就绪的表格。表格中的每一列都是一个可以用于机器学习的特征,而每一行是用于训练或测试的输入。Pandas 提供了添加和删除特征列的功能,以及以复杂方式增强和替换行值的功能。Pandas 是许多书籍的主题(我没有写它们!),Google 提供了关于此主题的数万个结果,但就你的目的而言,你可以使用 Pandas 从输入 TSV 文件创建一个机器学习就绪的数据框。然后 Pandas 可以帮助你检查输入中的特征数、行数和列数。
使用那个数据框,你运行你的文本清理代码以生成干净的评论,然后你可以应用词袋模型。首先,调用 Pandas 的 read_csv 函数,并告诉它你正在读取一个没有标题行的 TSV 文件,使用制表符(\t)作为分隔符,并且你不想引用特征值。当训练数据被加载时,打印其形状和列值,展示了使用 Pandas 检查数据框的便捷性。
由于清理 25,000 部电影评论可能需要一段时间,你将使用 Python 的 TQDM 辅助库来跟踪进度。TQDM 是一个可扩展的进度条库,它将状态打印到命令行或 Jupyter 笔记本。你将迭代步骤——列表 6.2 中的 range 函数——包装成一个 tqdm 对象。然后每个迭代步骤都会使进度条的增加对用户可见,无论是通过命令行还是在笔记本中。TQDM 是一种很好的方式,可以在长时间运行的机器学习操作中“发射并忘记”,同时当你回来检查时仍然知道有事情在进行。
列表 6.2 打印了训练数据的形状 (25000, 3),对应于 25,000 条评论和 3 列(id、sentiment 和 review),以及输出 array(['id', 'sentiment', 'review'], dtype=object),对应于那些列值。将列表 6.2 中的代码添加到你的 sentiment_classifier.ipynb 笔记本中,以生成 25,000 条干净的文本评论并跟踪进度。
列表 6.2 使用 Pandas 读取电影评论并应用你的清理函数
import pandas as pd
from tqdm import tqdm_notebook as tqdm
train = pd.read_csv("labeledTrainData.tsv", header=0,
delimiter="\t", quoting=3) ❶
print(train.shape) ❷
print(train.columns.values) ❷
num_reviews = train["review"].size ❸
clean_train_reviews = [] ❹
for i in tqdm(range( 0, num_reviews )): ❺
clean_train_reviews.append( review_to_words( train["review"][i] ) )
❶ 从输入 TSV 文件中读取 25,000 条评论
❷ 打印训练数据的形状和值数量
❸ 根据数据框列的大小获取评论数量
❹ 初始化一个空列表来保存干净的评论
❺ 遍历每个评论并使用你的函数清理它
现在你有了干净的评论,是时候应用词袋模型了。Python 的 SK-learn 库 (scikit-learn.org) 是一个可扩展的机器学习库,它提供了许多与 TensorFlow 相互补的功能。尽管一些功能有重叠,但我在这本书中相当多地使用了 SK-learn 的数据清理函数。你不必是纯粹主义者。SK-learn 随带了一个名为 CountVectorizer 的出色实现,例如;你将在列表 6.3 中使用它来应用词袋模型。
首先,使用一些初始超参数创建 CountVectorizer。这些超参数告诉 SK-learn 是否要执行任何文本分析,例如分词、预处理或移除停用词。我在这里省略了它,因为你已经在列表 6.1 中编写了自己的文本清理函数,并在 6.2 中将其应用于输入文本。
一个值得注意的参数是max_features,它控制从文本中学习到的词汇的大小。选择5000的大小可以确保你构建的 TensorFlow 模型具有足够的丰富性,并且每个评论的 Bag of Words 指纹可以在不耗尽你机器上的 RAM 的情况下学习。显然,你可以根据更大的机器和更多的时间在这个参数调整示例上玩耍。一个一般性的规则是,一个大约有数千个词汇的词汇表应该为英语电影的足够学习性提供支持。然而,对于新闻、科学文献和其他领域,你可能需要实验以找到最佳值。
调用fit_transform以提供你在列表 6.2 中生成的干净评论,并获取向量化的 Bag of Words,每行一个评论,行内容为每个评论中每个词汇的计数。然后将向量转换为 NumPy 数组;打印其形状;并确保你看到(25000,5000),对应于 25,000 个输入行,每行有 5,000 个特征。将列表 6.3 中的代码添加到你的笔记本中。
列表 6.3 将 Bag of Words 模型应用于获取训练数据
from sklearn.feature_extraction.text import CountVectorizer ❶
vectorizer = CountVectorizer(analyzer = "word", \ ❶
tokenizer = None, \
preprocessor = None, \
stop_words = None, \
max_features = 5000)
train_data_features = vectorizer.fit_transform(clean_train_reviews) ❷
train_data_features = train_data_features.toarray() ❸
print(train_data_features.shape) ❹
❶ 导入 CountVectorizer 并实例化 Bag of Words 模型
❷ 调整模型,学习词汇,并将训练数据转换为向量
❸ 将结果转换为 NumPy 数组
❹ 打印结果输入特征形状(25000,5000)
6.1.3 对你的 Bag of Words 进行探索性数据分析
进行一些探索性数据分析始终是一件好事,你可能想检查CountVectorizer返回的词汇值,以了解所有评论中存在的单词。你将想要确信这里确实有东西可以学习。你想要寻找的是单词之间的某种统计分布和相关的模式,分类器将从这个分布中学习以识别。如果每个评论中的计数都相同,并且你无法通过肉眼区分它们,那么机器学习算法将面临相同的困难。
SK-learn 和CountVectorizer的伟大之处在于,它们不仅提供了一个简单的一行或两行 API 调用以创建 Bag of Words 输出,而且允许轻松检查结果。你可以获取已学习的词汇并打印它们,通过使用快速的 NumPy 求和方法按单词进行分箱,然后查看前 100 个单词及其在所有评论中的总和。执行这些任务的代码在列表 6.4 中。
列表 6.4 对返回的 Bag of Words 进行探索性数据分析
vocab = vectorizer.get_feature_names() ❶
print("size %d %s " % (len(vocab), vocab)) ❶
dist = np.sum(train_data_features, axis=0) ❷
for tag, count in zip(vocab, dist): ❸
print("%d, %s" % (count, tag)) ❸
plt.scatter(vocab[0:99], dist[0:99]) ❹
plt.xticks(vocab[0:99], rotation='vertical') ❹
plt.show()
❶ 获取已学习的词汇并打印其大小和已学习的单词
❷ 求每个词汇的词频总和
❸ 打印词汇词及其在训练集中出现的次数
❹ 绘制前 100 个单词的词频图
图 6.2 显示了所有 25000 条评论中前 100 个单词的输出词集。我本可以随机选择词汇表中的任意 100 个单词,但为了使示例简单,我选择了前 100 个。即使在第一个 100 个单词中,这些单词在评论中的计数似乎也具有统计学意义;计数并不相同,也没有一致性。有些单词比其他单词使用得更频繁,还有一些明显的异常值,所以看起来似乎有一个信号供分类器学习。你可以在第 6.2 节开始构建逻辑回归分类器。

图 6.2 显示了所有 25000 条评论中提取的 5000 词词汇表中前 100 个单词的词汇计数总和
6.2 使用逻辑回归构建情感分类器
在第五章中,当处理逻辑回归时,你确定了因变量和自变量。在情感分析中,你的因变量是每条评论的 5000D 特征向量词袋,你有 25000 条数据用于训练。你的自变量是情感值:一个1对应 IMDb 中的正面评论,一个0对应用户对电影的负面情感。
那电影标题呢?
你有没有注意到你使用的 IMDb 数据是评论和情感,但没有标题?标题词在哪里?如果这些词包含映射到电影观众在评论中使用的单词的触发词,它们可能会影响情感。但总体来说,你不需要标题——只需要情感(要学习的东西)和评论。
尝试想象你的分类器将在给定的训练数据和特征空间中探索的解决方案空间。你可以想象一个矢量平面——称之为地面——将垂直轴称为从地面上方站立并向上看天空的垂直距离——情感。在地面上,有一个从你站立的原点开始的矢量,向每个方向延伸,对应于你的词汇表中的特定单词——如果你愿意,有 5000 个轴——射出与该矢量描述的特定单词的计数相对应的距离。这个平面上的数据点是每个单词轴上的特定计数,y 值是特定点的平面上的计数集合是否意味着情感为1或0。你的想象应该类似于图 6.3。

图 6.3 通过使用逻辑回归构建分类器的想象。你的特征空间是按三维排列的单词计数,其中值是发生次数。y 轴对应于情感结果(0或1)。
给定这种结构,我们可以使用以下方程来表示与这个分类器相对应的逻辑回归方程。目标是拥有一个线性函数,其中包含所有依赖变量及其相关权重(1 到 5000)作为 sigmoid (sig) 函数的参数,这将产生一个在 0 和 1 之间波动的平滑曲线,对应于情感独立变量:
M(x, v) = sig(wx + w)
情感 = sig(w[1]x[1] + w[2]x[2] + ⋅⋅⋅ + w[5000]x[5000] + w[0])
6.2.1 为你的模型设置训练
你已经准备好设置你的 TensorFlow 逻辑回归分类器了。开始时,可以设置一个任意的学习率 0.1,并训练 2,000 个周期(在我的笔记本电脑上效果很好),尤其是因为你将执行早期停止。早期停止 是一种技术,它测量前一个周期和当前周期之间损失(或错误率)的差异。如果错误率在周期之间通过某个小的阈值 epsilon 发生变化,则认为模型是稳定的,你可以在训练中提前终止。
你将设置 sigmoid 函数,这对于模型也是必需的。如第五章所述,这个函数确保在应用成本函数后,在每次训练步骤中学习适当的模型权重时,反向传播过程有一个在 0 和 1 之间波动的平滑梯度步骤。sigmoid 函数恰好具有这些属性。
在 TensorFlow 中创建用于你将学习的 Y 值的占位符,情感标签,以及你的 X 输入占位符 5,000 × 25,000 维度特征向量:每个电影评论一个词袋向量,共有 25,000 个电影评论。在列表 6.5 中,你使用一个 Python 字典来存储每个词袋向量,索引为 X0-X4999。w 变量(权重)对应于每个依赖变量 X,并在线性方程的末尾添加一个常数 w。
cost 函数与第五章中使用的相同凸交叉熵损失函数,你将使用梯度下降作为你的优化器。设置模型的完整代码在列表 6.5 中展示。
列表 6.5 设置逻辑回归情感分类器的训练
learning_rate = 0.1 ❶
training_epochs = 2000 ❶
def sigmoid(x):
return 1\. / (1\. + np.exp(-x)) ❷
Y = tf.placeholder(tf.float32, shape=(None,), name="y") ❸
w = tf.Variable([0.] * (len(train_data_features)+1), name="w", trainable=True)❸
ys = train['sentiment'].values ❹
Xs = {}
for i in range(train_data_features.shape[1]):
Xs["X"+str(i)] = tf.placeholder(tf.float32, shape=(None,),
➥ name="x"+str(i))
linear = w[0]
for i in range(0, train_data_features.shape[1]):
linear = linear + (w[i+1] * Xs["X"+str(i)])
y_model = tf.sigmoid(linear) ❺
cost = tf.reduce_mean(-tf.log(y_model * Y + (1 - y_model) * (1 - Y))) ❻
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❻
❶ 设置学习率和周期数的初始模型超参数
❷ 设置逻辑回归模型
❸ 定义 TensorFlow 占位符以注入实际的输入和标签值
❹ 从 Pandas 数据框中提取学习用的标签
❺ 构建要学习的逻辑回归模型
❻ 定义每个学习步骤的交叉熵成本函数和训练操作
在设置好你的模型后,你可以使用 TensorFlow 进行训练。如我之前提到的,你将执行早期停止以节省在 loss 函数和模型响应成本稳定时的无用周期。
6.2.2 对你的模型进行训练
创建一个tf.train.Saver来保存模型图和训练权重,以便你可以在以后重新加载它们,并使用训练好的模型进行分类预测。训练步骤与之前看到的大致相同:你初始化 TensorFlow,这次使用 TQDM 来跟踪和逐步打印训练进度,以便你有一些指标。训练可能需要 30 到 45 分钟,并且将消耗数 GB 的内存——至少,在我的相当强大的 Mac 笔记本电脑上是这样——所以 TQDM 是必不可少的,让你知道训练过程正在进行。
训练步骤将 5000 维的特征向量注入你创建的 X 占位符字典中,并将相关的情感标签注入到 TensorFlow 的 Y 占位符变量中。你将使用你的凸loss函数作为模型成本,并将前一个 epoch 的成本值与当前 epoch 的成本值进行比较,以确定你的代码是否应该在训练中执行早期停止以节省宝贵的周期。0.0001的阈值是任意选择的,但考虑到额外的周期和时间,它可能是一个可以探索的超参数。列表 6.6 显示了逻辑回归情感分类器的完整训练过程。
列表 6.6 执行逻辑回归情感分类器的训练步骤
saver = tf.train.Saver() ❶
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
prev_err = 0\. ❷
for epoch in tqdm(range(training_epochs)):
feed_dict = {}
for i in range(train_data_features.shape[1]):
feed_dict[Xs["X"+str(i)]] = train_data_features[:, i,
➥ None].reshape(len(train_data_features)) ❸
feed_dict[Y] = ys ❸
err, _ = sess.run([cost, train_op], feed_dict=feed_dict)
print(epoch, err)
if abs(prev_err - err) < 0.0001: ❹
break ❹
prev_err = err
w_val = sess.run(w, feed_dict) ❺
save_path = saver.save(sess, "./en-netflix-binary-sentiment.ckpt") ❻
print(w_val)
print(np.max(w_val))
❶ 创建保存器以捕获你的模型图和相关训练权重
❷ 捕获先前的损失函数值以测试早期停止
❸ 提供了 25,000 条评论,5,000 维的特征向量和情感标签
❹ 测试先前的损失值是否与当前损失值有微小差异,并在有差异时中断
❺ 在模型图加载时获取训练权重
❻ 保存模型图和相关的训练权重
你已经通过使用逻辑回归训练了你的第一个文本情感分类器!
接下来,我将向你展示如何使用这个情感分类器对新未见的数据进行预测。你还将学习如何评估分类器的准确性和精确度,并通过运行 Kaggle 竞赛的测试数据来了解其性能,然后将你的结果提交到 Kaggle。
6.3 使用你的情感分类器进行预测
现在你已经构建了你的分类器,你该如何使用它来进行预测呢?当你调用tf.train.Saver来保存你的检查点文件时,会存储两个关键信息:
-
检查点包含你到达的模型权重——在这种情况下,对应于你的词袋模型中每个词汇的
sigmoid线性部分的权重。 -
检查点包含模型图及其当前状态,以防你想从上次停止的地方继续训练并继续下一个 epoch。
进行预测就像加载检查点文件并将这些权重应用到模型上一样简单。正如我在第五章中向您展示的那样,您不需要重新使用列表 6.5 中的y_model函数的 TensorFlow 版本——tf.sigmoid函数——因为这样做会加载模型图,并需要额外的资源来准备 TensorFlow 以继续训练。相反,您可以将学习到的权重应用到模型的 NumPy 版本——列表 6.5 中的内联sigmoid函数——因为您不会对它进行任何进一步的训练。
看起来很简单,对吧?但我省略了一个需要首先解决的问题。这将在本书的其余部分得到推广,其中您执行机器学习、训练模型并使用它来做出辅助自动化决策的预测。考虑您为模型训练执行的步骤:
-
对 25,000 条电影评论进行数据清洗。
-
去除 HTML 标签。
-
移除标点符号,并仅考虑
a-zA-Z.。 -
移除停用词。
-
-
应用词袋模型,将词汇限制在 5,000 个单词的特征向量中。
-
使用 25,000 个大小为 5,000 的向量和相关的 25,000 个标签(情感 1、0 和逻辑回归)来构建分类模型。
现在假设您想使用您的模型对新文本进行预测,例如以下两个句子。第一个句子显然是负面评论,第二个是正面评论:
new_neg_review = "Man, this movie really sucked. It was terrible. I could not possibly watch this movie again!"
new_pos_review = "I think that this is a fantastic movie, it really "
您如何将这些句子提供给模型以进行情感预测?您需要在预测过程中应用您在训练期间使用的相同数据预处理步骤,这样您考虑文本的方式与您训练的方式相同。您是在 5,000 维特征向量上进行训练的,因此您需要做同样的事情来准备输入文本以进行预测。此外,您还需要注意一个额外的步骤。您在训练期间生成的权重是在由CountVectorizer生成的 5,000 个单词的共同共享词汇下产生的。您正在对其进行的预测的未见过输入文本可能具有与您的训练文本词汇不同的词汇。换句话说,那个未见过文本可能使用了您训练时没有使用的其他单词,可能更多或更少。然而,您花费了近 45 分钟来训练您的逻辑回归情感分类器,也许甚至更长的时间来准备训练的输入和标签。这项工作被无效化了吗?您是否必须从头开始进行训练?
记得我之前提到过,在CountVectorizer中将词汇大小设置为5000可以提供足够的丰富性,但你可能需要调整或探索以获得最佳匹配。所以词汇大小确实很重要,你用训练模型预测的内容也同样重要。在预处理步骤和数据清理后,词汇表中剩下 5,000 个单词可以在训练和未见数据上实现高准确率——在我的训练中高达 87%,你将在本章后面使用接收者操作特征(ROC)曲线时重现这一点。但谁能说 10,000 个单词不会达到更高的准确率呢?不是我!
事实上,使用更多词汇可能达到更高的准确率。结果取决于你打算进行预测的数据以及该数据的普遍性。它还会对你的整体内存和 CPU 以及 GPU 训练需求产生重大影响,因为为每个输入向量使用更多特征无疑会消耗更多资源。请注意,如果你的未见数据词汇与训练数据中的代表性词汇有足够的重叠,就没有必要增加其大小。
小贴士:确定最佳词汇大小是最好留给一个学期的统计学或 NLP 研究生课程的任务,但简单来说,你可能需要进一步探索这个超参数。这篇帖子有一些关于词汇大小的良好建议:mng.bz/pzA8。
为了向前推进并实现本章重点关注的情感预测功能,你需要确定你新文本的词汇向量与训练中现有词汇的交集。然后,你将只考虑指纹中重叠术语在 Bag of Words 模型中的计数,并将这些计数与基于训练模型预测情感的新文本的计数进行比较。整个预测流程及其与训练流程的关系在图 6.4 中展示。

图 6.4 使用机器学习进行预测。在训练过程中(顶部),你通过清理文本、将其转换为 5,000 维特征向量,并使用它来学习 25,000 篇电影评论的情感标签(1或0)。要使用学习到的模型进行预测(右侧),你需要执行相同的数据清理步骤,并且还需要确定下一个文本及其词汇与你的训练集的交集。
让我们开始编写predict函数,该函数应接受未修改的评论文本作为输入,以及训练过程中的训练词汇和学到的权重。你需要应用相同的数据清理过程,因此你通过以下方式清理文本:
-
标记化
-
移除标点符号和非字符
-
移除停用词
-
重新组合标记
之后,再次应用词袋模型,并生成一个函数,该函数可以用于对未见过的输入文本进行情感预测。该函数应专注于确定从输入中学习的新词汇与训练词汇的重叠部分。对于每个重叠的单词,考虑单词计数;特征向量中的其他元素将为零。结果特征向量应输入到 sigmoid 函数中,用于你的逻辑回归模型,使用最优的学习权重。结果——情感的概率介于0和1之间——与阈值值0.5进行比较,以确定情感是1还是0。列表 6.7 显示了predict函数。
列表 6.7 使用逻辑回归情感分类器进行预测
def predict(test_review, vocab, weights, threshold=0.5): ❶
test_review_c = review_to_words(test_review) ❷
n_vectorizer = CountVectorizer(analyzer = "word", \ ❸
tokenizer = None, \
preprocessor = None, \
stop_words = None, \
max_features = 5000)
ex_data_features = n_vectorizer.fit_transform([test_review_c])
ex_data_features = ex_data_features.toarray() ❹
test_vocab = n_vectorizer.get_feature_names() ❹
test_vocab_counts = ex_data_features.reshape(ex_data_features.shape[1])❹
ind_dict = dict((k, i) for i, k in enumerate(vocab)) ❺
test_ind_dict = dict((k, i) for i, k in enumerate(test_vocab)) ❺
inter = set(ind_dict).intersection(test_vocab) ❺
indices = [ ind_dict[x] for x in inter ] ❺
test_indices = [test_ind_dict[x] for x in inter] ❺
test_feature_vec = np.zeros(train_data_features.shape[1]) ❻
for i in range(len(indices)): ❻
test_feature_vec[indices[i]] = test_vocab_counts[test_indices[i]] ❻
test_linear = weights[0] ❼
for i in range(0, train_data_features.shape[1]): ❼
test_linear = test_linear + (weights[i+1] * test_feature_vec[i]) ❼
y_test = sigmoid(test_linear) ❼
return np.greater(y_test, threshold).astype(float) ❽
❶ 以参数形式接受用于测试的评论文本、训练词汇、学习权重和正负预测的阈值截止值
❷ 使用与训练相同的函数清理评论
❸ 创建测试词汇和计数
❹ 将词汇和计数转换为 NumPy 数组
❺ 确定评论词汇与实际完整词汇的交集
❻ 除了我们计数重叠索引的 5,000 特征向量外,其余都是零
❼ 使用学习到的权重应用你的逻辑回归模型
❽ 如果预测概率大于 0.5,则情感为 1;否则,为 0。
在以下测试评论new_neg_review和new_pos_review上尝试该函数。该函数正确地将负面评论预测为0,将正面评论预测为1。酷,对吧?
new_neg_review = "Man, this movie really sucked. It was terrible. I could not possibly watch this movie again!"
new_pos_review = "I think that this is a fantastic movie, it really "
predict(new_neg_review, vocab, w_val)
predict(new_pos_review, vocab, w_val)
现在你有了predict函数,你可以使用它来计算混淆矩阵(第三章)。创建真实阳性、假阳性、真实阴性和假阴性的混淆矩阵,这允许你衡量分类器预测每个类别的能力,并计算精确率和召回率。此外,你可以生成 ROC 曲线,并测试你的分类器比基线好多少。
6.4 测量你的分类器的有效性
现在你可以使用你的逻辑回归分类器预测未见过的文本的情感了,一个衡量其总体有效性的好方法是将其大规模应用于大量未见过的文本。你使用了 25,000 条 IMDb 电影评论来训练分类器,所以你会使用另外 25,000 条你保留用于测试的。当你使用 Kaggle 的 TSV 文件训练分类器时,你使用的是原始 IMDb 评论数据的合并版本。你并不总是有这个好处;有时,你需要预处理原始数据。为了确保你可以处理这两种数据准备和清理方法,使用原始的 aclImdb_v1.tar.gz 文件(mng.bz/Ov5R)并为其准备测试。
解压 aclImdb_v1.tar.gz 文件。你现在有一个看起来像这样的文件夹结构,其中每个条目下面要么是文件(如README),要么是目录(如test和train):
README imdb.vocab imdbEr.txt test/ train/
打开测试目录。里面包含更多文件(.txt 和.feat)和文件夹(neg 和 pos):
labeledBow.feat neg/ pos/ urls_neg.txt urls_pos.txt
文件夹 pos(表示正面)和 neg(表示负面)包含 12,500 个文本文件,其中包含电影评论,每个文件对应于未见过的正面和负面评论,因此你将创建两个变量——only_pos_file_contents和only_neg_file_contents——来对应它们。通过使用两个循环将评论读取到这两个变量中。Python 的内置os.isfile函数确保在代码遍历和评估目录列表对象时,执行一个测试以确定该对象是否为文件(而不是目录)。os.listdir方法列出目录中的文件。列表 6.8 中的代码加载了测试 IMDb 评论。
列表 6.8:加载测试 IMDb 评论
from os import listdir
from os.path import isfile, join
pos_test_path = "aclImdb/test/pos/" ❶
neg_test_path = "aclImdb/test/neg/" ❶
only_pos_files = [f for f in listdir(pos_test_path) if ❶
➥ isfile(join(pos_test_path, f))] ❶
only_neg_files = [f for f in listdir(neg_test_path) if ❶
➥ isfile(join(neg_test_path, f))] ❶
only_pos_file_contents = []
for i in range(0, len(only_pos_files)): ❷
with open(pos_test_path + only_pos_files[i], 'r') as file:
r_data = file.read()
only_pos_file_contents.append(r_data)
only_neg_file_contents = [] ❸
for i in range(0, len(only_neg_files)):
with open(neg_test_path + only_neg_files[i], 'r') as file:
r_data = file.read()
only_neg_file_contents.append(r_data)
predictions_test = np.zeros(len(only_pos_file_contents) * 2) ❹
❶ 遍历并识别正面和负面评论文本文件的路径
❷ 将正面评论读取到包含 12,500 个文本对象的列表中
❸ 将负面评论读取到包含 12,500 个文本对象的列表中
❹ 为 25,000 个情感值创建占位符
在将测试评论加载到内存中的only_pos_file_contents和only_neg_file_contents以及标签占位符predictions_test变量中后,你可以使用predict函数来计数真实和虚假正例以及真实和虚假负例,然后计算分类器的精确度和召回率。精确度定义为

召回率定义为

列表 6.9 中的代码遍历正面情感文件,调用你的predict函数,将结果存储在predictions_test变量中。随后,它通过在负面文件内容上调用predict函数来继续操作。由于调用predict函数可能每次需要几秒钟,这取决于你笔记本电脑的处理能力,你将再次使用tqdm库来跟踪每次迭代循环的进度。列表的最后部分打印出你的分类器的精确度和召回率,然后是真实和虚假正例以及真实和虚假负例的总数。真实和虚假正例以及负例是通过将你的分类器应用于未见过的测试评论来衡量的。运行列表 6.9 输出精确度 0.859793 召回率 0.875200,这对于你的第一个分类器来说是一个出色的结果!
列表 6.9:计算混淆矩阵、精确度和召回率
TP = 0\. ❶
TN = 0\. ❶
FP = 0\. ❶
FN = 0\. ❶
for i in tqdm(range(0, len(only_pos_file_contents))): ❷
sent = predict(only_pos_file_contents[i], vocab, w_val) ❷
predictions_test[i] = sent
if sent == 1.:
TP += 1
elif sent == 0.:
FN += 1
for i in tqdm(range(0, len(only_neg_file_contents))): ❸
sent = predict(only_neg_file_contents[i], vocab, w_val) ❸
predictions_test[len(only_neg_file_contents)+i] = sent
if sent == 0.:
TN += 1
elif sent == 1.:
FP += 1
precision = (TP) / (TP + FP) ❹
recall = (TP) / (TP + FN) ❹
print("precision %f recall %f" % (precision, recall))
print(TP)
print(TN)
print(FP)
print(FN)
❶ 初始化真实和虚假正例以及真实和虚假负例的计数
❷ 遍历正面情感文本文件并调用预测函数,并计算 TP 和 FN
❸ 遍历负面情感文本文件并调用预测函数,并计算 TN 和 FP
❹ 计算并打印精确度和召回率
给定生成的预测结果,您可以进行下一步:检查曲线下面积 (AUC) 并创建 ROC 曲线,以确定您的分类器比基线好多少。您不必自己实现这个过程,可以使用 SK-learn 的 roc_curve 函数,然后使用一些 Matplotlib 来绘制结果。
要使用 roc_curve 函数,您需要列表 6.9 中的 predictions_test 变量,这是在所有真正例上运行 predict 函数的结果,然后是在真正例上运行的结果。然后您需要一个您将称为 outcomes_test 的变量,这是真实情况。因为真实情况包括 12,500 个正面情感示例后面跟着 12,500 个负面情感示例,您可以通过初始化一个调用 np.ones(NumPy 函数,创建指定大小的数组,包含 12,500 个 1)的调用来创建 outcomes_test 变量,然后附加一个调用 np.zeros(NumPy 函数,创建指定大小的数组,包含 12,500 个 0)。
当这些变量生成后,您调用 roc_curve 函数;获取真正率 (tpr) 和假正率 (fpr);然后将它们传递给 auc 函数,该函数将 AUC 存储在 roc_auc 变量中。列表 6.10 的剩余部分设置了带有基线分类器在 x 轴从 0 到 1、y 轴从 0 到 1 的虚线,然后使用 tpr 和 fpr 值在虚线上方的实线表示您的分类器实际结果(图 6.5)。
列表 6.10 使用 ROC 测量您的分类器性能与基线
from sklearn.metrics import roc_curve, auc
outcome_test = np.ones(len(only_pos_files)) ❶
outcome_test = np.append(outcome_test, np.zeros(len(only_neg_files))) ❶
fpr, tpr, thresholds = roc_curve(predictions_test, outcome_test) ❷
roc_auc = auc(fpr, tpr) ❷
plt.figure() ❸
plt.plot(fpr, tpr, color='darkorange', lw=1, label='ROC curve ❸
➥ (area = %0.2f)' % roc_auc) ❸
plt.plot([0, 1], [0, 1], color='navy', lw=1, linestyle='—') ❸
plt.xlim([0.0, 1.0]) ❸
plt.ylim([0.0, 1.05]) ❸
plt.xlabel('False Positive Rate') ❹
plt.ylabel('True Positive Rate') ❹
plt.title('Receiver operating characteristic') ❹
plt.legend(loc="lower right") ❹
plt.show() ❺
❶ 创建一个大小与正面和负面文件数量成比例的标签数组(1 表示正面,0 表示负面)
❷ 计算假正率 (fpr)、真正率 (tpr) 和曲线下面积 (roc_auc)
❸ 初始化 Matplotlib 并设置基线 ROC 和分类器结果的线型和标签
❹ 创建图例和图表标题
❺ 显示图表

图 6.5 逻辑回归情感分类器的 ROC 曲线。性能比基线好得多,ROC 曲线/AUC 为 0.87,或 87%。
您已经评估了您的逻辑回归分类器的准确性,计算了其精确度、召回率和生成 ROC 和 AUC 曲线,并将其性能与基线进行了比较,基线表现相当好(几乎 90%)。正如我在第五章中提到的,任何具有这种性能的分类器很可能在实际应用中表现良好,因为它是在平衡数据集上训练并在具有相等平衡的未见数据上评估的。
在第五章中讨论的另一种技术是 softmax 回归,它具有自然地将预测扩展到两个类别以上的好处(或二分类)。尽管我们在这里没有 N>2 个类别进行训练,但探索如何创建情感分类器的 softmax 版本仍然值得,这样你可以获得一些实际构建它的经验。你可以重用本章中完成的大部分工作,让我们开始吧!
6.5 创建 softmax-regression 情感分类器
softmax-regression 方法的好处是它将分类扩展到预测超过两个类别的预测。分类方法的结构类似于逻辑回归。你有以下方程,其中你取一个线性相关变量集(你学习的权重 w)并通过 sigmoid 函数运行,该函数将值迅速解析为平滑曲线的 0 或 1。回想一下逻辑回归的结构:
Y = sig(linear)
Y = sig(w[1]x[1] + w[2]x[2] + ⋅⋅⋅ + w[0])
使用 softmax 回归,你有一个类似的结构,其中你学习 num_features × num_labels 大小的权重。在这种情况下,权重对应于乘以相关变量的权重,类似于逻辑回归。你还学习一个大小为 num_labels 的偏差矩阵,这同样类似于标准逻辑回归。与 softmax 回归的关键区别在于,你应用 softmax 函数而不是 sigmoid 函数来学习你试图预测的 N 个类别的概率分布。softmax 的方程式是
Y = WX + B
现在考虑在情感分类问题的背景下 softmax 分类器。你试图学习一组文本表示一个标签的概率,Y,这个标签可以是正标签或负标签,对于你将要测试的每一条 25,000 条评论。每条评论的文本集被转换为一个 5,000 维的特征向量,每个特征向量对应于 25,000 条评论中的一个 (X )。权重是与两个类别(正类别或负类别)相关联的权重,用于乘以 5,000 个相关变量,我们将其相关矩阵称为 W。最后,B 是两个类别(正类别和负类别)的偏差,用于添加,形成用于回归的线性方程。图 6.6 展示了这种结构。

图 6.6 softmax-regression 方法。输入 X 字段是 25,000 条评论和一个 5,000 维的词袋特征向量。模型学习 W,它是一个大小为 num features, 5000, by num labels 2 的矩阵,用于正负类别。
给定这种结构,您可以从一些代码开始,将这些矩阵组合起来,为您的分类器做准备。您将从创建标签矩阵的地面真实值开始。地面真实值中的每个条目都需要使用 one-hot 编码(第五章)。one-hot 编码是将类别标签如 [0, 1] 和 [1,0] 生成的过程,以表示类别 A 和 B——在这种情况下,表示积极情感和消极情感。因为 softmax 回归根据有序矩阵预测特定类别,如果您希望 1 对应于积极,0 对应于消极,那么该矩阵的列顺序应该是 0th 索引表示消极,1th 索引表示积极,因此您的标签编码应该是消极情感的 [1,0] 和积极情感的 [0,1]。
您将重用列表 6.2 中的 Pandas 数据框,并使用 Pandas 库的一些强大功能。数据框就像内存中的 Python 关系表,因此您可以使用类似 SQL 的结构查询它们。例如,要选择数据框中情感为积极(或 1)的所有行,您可以遍历数据框的长度,然后单独选择情感列值为 1.0 的行。您将使用此功能生成 one-hot 编码,然后从结果中创建一个大小为 25,000 × 2 的 NumPy 矩阵。
您的训练输入是来自列表 6.11 的 CountVectorizer 的输出,转换为一个大小为 (25000 × 5000) 的特征 NumPy 浮点矩阵。在列表 6.11 中,您创建了用于您的 softmax-regression 情感分类器的输入。列表的输出是 X 输入的形状 xs.shape,或 (25000,5000),以及标签矩阵的形状 labels.shape,或 (25000,2)。
列表 6.11 创建 softmax-regression 分类器的输入
lab_mat = []
for i in range(len(train['sentiment'])):
if train['sentiment'][i] == 1.0: ❶
lab_mat = lab_mat + [[0., 1.]]
elif train['sentiment'][i] == 0.0: ❷
lab_mat = lab_mat + [[1., 0.]]
labels = np.matrix(lab_mat) ❸
xs = train_data_features.astype(float) ❹
train_size, num_features = xs.shape
print(xs.shape) ❺
print(labels.shape) ❺
❶ One-hot encodes the positive sentiment examples
❷ One-hot encodes the negative sentiment examples
❸ Converts the label matrix to a NumPy matrix for training
❹ Extracts the NumPy array of 25000 × 5000 Bag of Words vectors for the training reviews
❺ Prints the shape of the X input matrix (25000,5000) and of the label (25000,2) matrix
在创建 TensorFlow softmax-regression 分类器之前,下一步是对输入数据进行洗牌。这样做的一个原因是为了防止分类器记住输入和标签的顺序,而是学习输入 Bag of Words 向量到情感标签的映射才是重要的。您可以使用 NumPy 的 arange 方法,该方法根据给定的形状生成一系列数字索引,来处理这个任务。
你可以用训练样本的数量(xs.shape[0],或25000)调用arange,然后使用 NumPy 的random模块及其shuffle方法np.random.shuffle来随机化这些索引。随机化的索引数组arr可以用来索引xs和labels数组,以随机打乱它们。你可以应用列表 6.12 中防止分类器记住数据顺序的代码,并使用它来设置你的训练过程。
列表 6.12 防止分类器记住输入的顺序
arr = np.arange(xs.shape[0]) ❶
np.random.shuffle(arr) ❷
xs = xs[arr, :] ❸
labels = labels[arr, :]
❶ 生成一个大小为 25000 的索引数组
❷ 打乱索引并将结果存储在 arr 中
❸ 使用打乱的索引数组 arr 来打乱 X 和标签
你几乎准备好编写你的 softmax 分类器了。在模型构建和训练之前,最后一步是准备你的测试数据以进行准确度评估,你希望在训练后进行这项操作,并且你需要它来为你的 softmax-predict 函数提供支持,该函数看起来略有不同。因此,你不需要再次对你的评论进行整个清理过程(你在列表 6.2 中已经处理了这个任务),你需要创建一个函数,该函数假设你已经有了干净的评论,但会针对这些干净的评论单独运行 Bag of Words 模型以生成 25000 × 5000 大小的测试特征向量。
此外,在测试 softmax 分类器对未见数据时,你需要使用为 25000 条训练评论生成的训练词汇,因此,现在你有了训练好的情感分类器,你可以准备测试评论和标签以进行评估。如果你使用列表 6.7 中的predict函数,你可以将review_to_words的调用分开,然后执行相同的步骤。列表 6.13 执行此任务并为 25000 条测试评论生成测试特征向量。你将使用这些向量在训练后测量准确度,并在不久之后测量你新 softmax 分类器的性能。
列表 6.13 在训练后准备测试评论和标签以进行评估
def softmax_feat_vec_from_review(test_review, vocab):
n_vectorizer = CountVectorizer(analyzer = "word", \ ❶
tokenizer = None, \
preprocessor = None, \
stop_words = None, \
max_features = 5000)
ex_data_features = n_vectorizer.fit_transform([test_review]) ❷
ex_data_features = ex_data_features.toarray() ❷
test_vocab = n_vectorizer.get_feature_names() ❸
test_vocab_counts = ex_data_features.reshape(ex_data_features.shape[1]).❸
ind_dict = dict((k, i) for i, k in enumerate(vocab)) ❹
test_ind_dict = dict((k, i) for i, k in enumerate(test_vocab)) ❹
inter = set(ind_dict).intersection(test_vocab) ❹
indices = [ ind_dict[x] for x in inter ] ❹
test_indices = [test_ind_dict[x] for x in inter] ❹
test_feature_vec = np.zeros(train_data_features.shape[1]) ❺
for i in range(len(indices)): ❺
test_feature_vec[indices[i]] = test_vocab_counts[test_indices[i]] ❺
return test_feature_vec ❻
test_reviews = []
clean_test_reviews = []
test_reviews.extend(only_pos_file_contents) ❼
test_reviews.extend(only_neg_file_contents) ❼
for i in tqdm(range(len(test_reviews))):
test_review_c = review_to_words(test_reviews[i])
clean_test_reviews.append(test_review_c) ❽
test_xs = np.zeros((len(clean_test_reviews), num_features)) ❾
for i in tqdm(range(len(clean_test_reviews))): ❾
test_xs[i] = softmax_feat_vec_from_review(clean_test_reviews[i], vocab) ❾
❶ 假设评论是干净的,因此创建测试词汇表和计数。
❷ 运行 CountVectorizer 并生成 25000 × 5000 的特征矩阵
❸ 获取提供的测试评论集的词汇表以进行评估
❹ 找出评论的测试词汇与实际完整词汇的交集
❺ 对于 5000 个特征向量,除了我们计数的重叠索引外,所有值都是零
❻ 返回提供的单个评论和训练词汇的特征向量
❻ 创建一个包含 12500 条正面评论的新数组,然后追加 12500 条负面评论的文本
❽ 清理测试评论中的文本
❾ 创建一个(25000,5000)的特征向量来评估你的分类器
凭借用于评估你的分类器的数据,你准备好开始训练过程了。与之前的 TensorFlow 训练一样,你首先定义超参数。任意训练 1,000 个周期,使用学习率为 0.01,批量大小为 100。同样,通过一些实验和超参数调整,你可能找到更好的起始值,但列表 6.14 中的参数已经足够用于实验。训练过程在你的笔记本电脑上可能需要长达 30 分钟,所以你将再次使用你的朋友 TQDM 来确保你在离开电脑时可以跟踪进度。在模型训练完成后,你将使用 tf.train.Saver 来保存文件,并打印测试准确率,最终结果是 0.81064,或 82%。还不错!
列表 6.14 使用批量训练训练 softmax 回归分类器
learning_rate = 0.01 ❶
training_epochs = 1000 ❶
num_labels = 2 ❶
batch_size = 100 ❶
X = tf.placeholder("float", shape=[None, num_features]) ❷
Y = tf.placeholder("float", shape=[None, num_labels]) ❷
W = tf.Variable(tf.zeros([num_features, num_labels])) ❷
b = tf.Variable(tf.zeros([num_labels])) ❷
y_model = tf.nn.softmax(tf.matmul(X, W) + b) ❸
cost = -tf.reduce_sum(Y * tf.log(tf.maximum(y_model, 1e-15))) ❹
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❹
saver = tf.train.Saver() ❺
with tf.Session() as sess:
tf.global_variables_initializer().run()
for step in tqdm(range(training_epochs * train_size // batch_size)): ❻
offset = (step * batch_size) % train_size ❻
batch_xs = xs[offset:(offset + batch_size), :] ❻
batch_labels = labels[offset:(offset + batch_size)] ❻
err, _ = sess.run([cost, train_op], feed_dict={X: batch_xs, Y: ❻
➥ batch_labels}) ❻
print (step, err) ❼
W_val = sess.run(W) ❽
print('w', W_val) ❽
b_val = sess.run(b) ❽
print('b', b_val) ❽
print("accuracy", accuracy.eval(feed_dict={X: test_xs, Y: test_labels}))❾
save_path = saver.save(sess, "./softmax-sentiment.ckpt") ❿
print("Model saved in path: %s" % save_path) ❿
❶ 定义超参数
❷ 定义 TensorFlow 占位符,用于输入向量、情感标签、权重和偏差
❸ 使用 TensorFlow softmax 和矩阵乘法 Y = WX+b 创建模型
❹ 定义对数损失成本和训练操作,并考虑当损失为零时防止 NaN 问题
❺ 创建一个保存模型图和权重的保存器
❻ 在 100 个输入 Bag of Words 向量和情感标签的批次上训练
❼ 计算并打印每个 250,000 步的批次损失
❽ 打印学习到的权重和偏差
❾ 通过评估未见过的 25,000 条评论和情感标签来打印准确率
❿ 保存 softmax 回归模型
除了相当不错的准确率之外,你还可以看到 softmax 分类器在二进制逻辑回归分类器上表现略差。但别担心;你没有进行任何参数调整。不过,通过生成 ROC 曲线和计算 AUC 来评估它,你可以确信你的分类器的强大。我将在下一部分带你完成这个过程。不过,在开始 ROC 之前,你需要一个新函数来进行预测。
此函数与列表 6.7 中显示的函数只有细微差别,而这个细微差别是你在未来机器学习任务中选择使用逻辑回归或 softmax 回归时需要掌握的关键要点之一。在列表 6.7 中,predict 函数的最后一步使用阈值来确定你的 sigmoid 输出是否应该映射到 0 或 1。正如你所回忆的,sigmoid 在其边缘在 0 和 1 之间振荡。你需要定义一个阈值——通常是中位数,0.5——来确定中间的点应该落在 0 或 1 的哪一侧。因此,二元逻辑回归的输出是 0 或 1,与之相伴的是实际值与定义的阈值之间的对应距离。你可以将这个距离视为显示算法对输入分类为 1 或 0 的决策的信心。
Softmax 逻辑回归略有不同。输出是一个大小为 (num_samples, num_classes) 或 (行, 列) 的矩阵。当你给算法一个评论或行并尝试将输入分类到两个类别或列时,你会得到一个如 [[0.02 98.4]] 的矩阵。这个矩阵表明算法有 0.02% 的信心认为输入是负面情绪(第 0 列)和 98.4% 的信心认为它是正面(第 1 列)。对于 25,000 条评论,你会得到一个包含每个类别的两个列置信值的 25,000 行矩阵。Softmax 输出不是 0 或 1,就像在二进制逻辑回归中那样。predict_softmax 函数需要考虑这个事实,并找出列维度上的最大值。
NumPy 提供了 np.argmax 函数来精确地做到这一点。你提供 NumPy 数组作为第一个参数;第二个参数标识要测试的维度轴。该函数返回具有最大值的轴索引。对于 np.argmax([[0.02 98.4]],1),该函数将返回 1. 列表 6.15 与列表 6.7 类似;唯一的区别是你如何使用 np.argmax 解释输出。
列表 6.15 为你的 softmax 回归分类器创建 predict 函数
def predict_softmax(test_review, vocab):
test_review_c = review_to_words(test_review) ❶
n_vectorizer = CountVectorizer(analyzer = "word", \ ❷
tokenizer = None, \ ❷
preprocessor = None, \ ❷
stop_words = None, \ ❷
max_features = 5000)
ex_data_features = n_vectorizer.fit_transform([test_review_c]) ❸
ex_data_features = ex_data_features.toarray() ❸
test_vocab = n_vectorizer.get_feature_names() ❸
test_vocab_counts = ex_data_features.reshape(ex_data_features.shape[1])❸
ind_dict = dict((k, i) for i, k in enumerate(vocab)) ❹
test_ind_dict = dict((k, i) for i, k in enumerate(test_vocab)) ❹
inter = set(ind_dict).intersection(test_vocab) ❹
indices = [ ind_dict[x] for x in inter ] ❹
test_indices = [test_ind_dict[x] for x in inter] ❹
test_feature_vec = np.zeros(train_data_features.shape[1]) ❺
for i in range(len(indices)): ❺
test_feature_vec[indices[i]] = test_vocab_counts[test_indices[i]] ❺
predict = y_model.eval(feed_dict={X: [test_feature_vec], W: W_val, ❻
➥ b: b_val}) ❻
return np.argmax(predict, 1) ❼
❶ 清理评论
❷ 创建测试词汇表并计数
❸ 应用词袋模型并为测试评论生成词汇表
❹ 确定评论中的测试词汇与实际完整词汇的交集
❺ 对于 5,000 个特征的向量,除了我们计数重叠索引之外,所有值都是零
❻ 进行预测并获取 softmax 矩阵
❷ 使用 np.argmax 获取测试情感级别
有了你新的 predict_softmax 函数,你可以生成 ROC 曲线来评估你的分类器,这与列表 6.10 类似。不是对每个评论调用 predict,而是加载保存的 softmax 回归模型;将其预测应用于整个测试评论数据集,使用学习到的权重和偏差;然后使用 np.argmax 同时获取所有 25,000 条评论的预测集。输出的 ROC 曲线如列表 6.16 所示,当使用 softmax 分类器时,对测试数据的准确率为 81%。如果你有一些额外的时间和循环,可以尝试调整这些超参数;看看你是否可以将它们调整到比你的逻辑回归分类器更好的结果。不是很有趣吗?
列表 6.16 生成 ROC 曲线和评估你的 softmax 分类器
saver = tf.train.Saver() ❶
with tf.Session() as sess: ❶
saver.restore(sess, save_path) ❶
print("Model restored.") ❶
predict_vals = np.argmax(y_model.eval(feed_dict={X: test_xs, W: W_val, ❷
➥ b: b_val}), 1) ❷
outcome_test = np.argmax(test_labels, 1) ❸
predictions_test = predict_vals ❹
fpr, tpr, thresholds = roc_curve(predictions_test, outcome_test) ❺
roc_auc = auc(fpr, tpr) ❺
plt.figure()
plt.plot(fpr, tpr, color='darkorange', lw=1,
➥ label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=1, linestyle='—')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="lower right")
plt.show() ❻
❶ 加载并恢复 softmax 回归模型
❷ 同时预测所有 25,000 条评论的情感,并使用 np.argmax 生成所有情感
❸ 使用 np.argmax 获取测试情感级别
❹ 设置预测情感以进行测试
❺ 创建 ROC 曲线和 AUC,使用结果中的假阳性率和真阳性率
❻ 生成基线分类器和你的 softmax 分类器的图表
你可以在图 6.7 中看到运行列表 6.16 的结果,它描绘了评估你的分类器的 ROC 曲线。本着竞争的精神,为什么不将你的结果提交到机器学习竞赛平台 Kaggle 上的原始“单词袋遇见爆米花袋”挑战赛呢?这个过程非常简单,就像我在第 6.6 节中要展示的那样。

图 6.7 softmax 回归情感分类器的 ROC 曲线和 AUC 曲线,其表现略逊于你的逻辑回归分类器,准确率为 81%
6.6 将你的结果提交到 Kaggle
“单词袋遇见爆米花袋”挑战赛几年前就结束了,但 Kaggle 仍然允许你上传你的机器学习算法,看看它在排行榜上的位置。这个简单的流程展示了你的机器学习表现如何。你之前已经完成的所有工作的 Python 代码相当简单。
从原始的二进制逻辑回归情感分类器中调用你的predict函数。因为它比 softmax 分类器表现更好——87%对 81%——你将想要提交你的最佳作品。如果你使用原始的 Kaggle 测试 CSV 文件,你可以生成结果 Pandas 数据框 test,并在其上运行你的predict函数。然后你可以使用 Pandas 的另一个出色功能来添加一个完整的列,将其映射到相关的行。只要新列有相同数量的列,你就可以使用一个单行函数创建一个新的数据框,并添加额外的列。有了这个新的数据框,你可以使用 Pandas 的内置函数将数据框输出到 CSV,并获取你的Bag_of_Words_model.csv输出以上传到 Kaggle。列表 6.17 中的代码生成了那个 CSV 文件。
列表 6.17 生成 Kaggle 提交的情感分类结果
num_reviews = len(test["review"]) ❶
result = [] ❶
for i in tqdm(range(0, num_reviews)): ❶
r = predict(test["review"][i], vocab, w_val) ❷
result.append(r) ❷
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} ) ❸
output.to_csv( "Bag_of_Words_model.csv", index=False, quoting=3 ) ❹
❶ 生成 Kaggle 提交的空列表并逐个添加评论
❷ 调用 predict 函数以获取 1 或 0 的情感
❸ 将结果复制到包含 id 列和情感列的 Pandas 数据框中
❹ 使用 Pandas 写入逗号分隔的输出文件
现在你已经生成了 Kaggle 提交,你可以上传结果 CSV 文件到 Kaggle。假设你已经创建了你的账户,你可以访问www.kaggle.com/c/word2vec-nlp-tutorial来提交你的作品。使用以下说明,你可以为未来的 Kaggle 比赛重复使用:
-
点击蓝色的“加入比赛”功能。
-
点击蓝色的“提交迟交作业”按钮。
-
点击窗口区域,在运行列表 6.17 并生成 Bag_of_Words_model.csv 文件后进行文件选择,然后选择该文件。
-
点击蓝色的“提交预测”按钮以提交你的预测并查看你在排行榜上的位置。
我不是告诉你很容易吗?你可以在图 6.8 中看到你提交 Kaggle 的结果。

图 6.8 提交你的 Kaggle 结果
就这样!你已经将你的结果提交到了 Kaggle,并且让你的结果进入了机器学习专家的世界,供他们查看。这些合理的成果应该让你在竞赛中稳居中上水平。(遗憾的是,比赛已经结束了。)
你在将逻辑回归和 softmax 回归应用于文本分析方面做得非常出色。在接下来的几章中,你将了解到如何使用无监督方法从无标签的数据中学习。
摘要
-
你可以通过创建词汇表并计算这些词的出现次数,将文本转换为 n 维特征。
-
通过文本和词频,你可以将自然语言处理中的著名“词袋”模型应用于代表 IMDb 电影评论文本语料库中的情感。
-
使用 Pandas 数据框,你可以使用 Python 机器学习库将矩阵和向量作为内存中的表来表示,以存储分类器输出和相关文本。
-
你使用逻辑回归和相关的流程构建了一个基于 TensorFlow 的文本电影评论情感分类器。你还使用逻辑回归和 softmax 构建了一个基于 TensorFlow 的情感分类器。它在数据准备步骤以及如何解释模型响应方面都有所不同。
-
测量分类准确度通常是通过识别和计数真正的阳性、假阳性、真正的阴性和假阴性来完成的。
-
计算 ROC 曲线可以让你衡量你训练的两个分类器的有效性及其有效性。
-
你将你的结果提交到了 Kaggle 的“电影评论”挑战,以查看你的得分与其他试图从文本中自动预测情感的机器学习研究人员相比如何。
7 自动聚类数据
本章涵盖
-
使用 k-means 进行基本聚类
-
音频表示
-
音频分割
-
使用自组织图进行聚类
假设你在硬盘上有一个完全合法、非盗版的 MP3 音乐集合。所有歌曲都挤在一个巨大的文件夹中。也许自动将相似的歌曲分组到如乡村、说唱和摇滚等类别中,可以帮助组织它们。这种以无监督方式将项目分配到组(如 MP3 到播放列表)的行为被称为聚类。
第六章假设你有一个正确标记的训练数据集。不幸的是,当你收集现实世界中的数据时,你并不总是享有这种便利。假设你想要将大量音乐分割成有趣的播放列表。如果你无法直接访问它们的元数据,你如何可能将歌曲分组?
Spotify、SoundCloud、Google Music、Pandora 以及许多其他音乐流媒体服务试图解决这个问题,向客户推荐相似的歌曲。他们的方法包括各种机器学习技术的混合,但聚类通常是解决方案的核心。
聚类是将你的数据集中的项目智能分类的过程。总体思路是,同一聚类中的两个项目比属于不同聚类的项目“更接近”。这是一般定义,将接近的解释留给了读者。也许当以生物分类体系(家族、属和物种)中两种物种的相似性来衡量接近度时,猎豹和豹属于同一聚类,而大象属于另一个聚类。
你可以想象存在许多聚类算法。本章重点介绍两种类型:k-means和自组织图。这些方法是无监督的,意味着它们在没有地面实况示例的情况下拟合模型。
首先,你将学习如何将音频文件加载到 TensorFlow 中,并将它们表示为特征向量。然后,你将实现各种聚类技术来解决实际问题。
7.1 在 TensorFlow 中遍历文件
机器学习算法中的一些常见输入类型是音频和图像文件。这并不令人惊讶,因为录音和照片是语义概念的原始、冗余、通常嘈杂的表示。机器学习是一种帮助处理这些复杂性的工具。
这些数据文件有多种实现方式。例如,一张图片可以编码为 PNG 或 JPEG 文件,而音频文件可以是 MP3 或 WAV 格式。在本章中,你将研究如何将音频文件作为聚类算法的输入,以便自动将听起来相似的音乐分组。
练习 7.1
MP3 和 WAV 的优点和缺点是什么?PNG 与 JPEG 又如何?
答案
MP3 和 JPEG 显著压缩了数据,因此此类文件易于存储或传输。但因为这些文件是有损的,WAV 和 PNG 更接近原始内容。
从磁盘读取文件并不完全是机器学习特有的能力。你可以使用各种 Python 库,如 NumPy 或 SciPy,将文件加载到内存中,正如我在前面的章节中向你展示的那样。一些开发者喜欢将数据预处理步骤与机器学习步骤分开处理。管理管道没有绝对的对错之分,但你会尝试使用 TensorFlow 进行数据预处理和学习。
TensorFlow 提供了一个名为 tf.train.match_filenames_once 的操作符,用于列出目录中的文件。你可以将此信息传递给队列操作符 tf.train.string_input_producer。这样,你可以一次访问一个文件名,而不必同时加载所有内容。给定一个文件名,你可以解码文件以检索可用的数据。图 7.1 概述了使用队列的过程。

图 7.1 你可以使用 TensorFlow 中的队列来读取文件。队列是 TensorFlow 框架的一部分,你可以使用 reader.read(...) 函数来访问(并出队)它。
列表 7.1 展示了在 TensorFlow 中从磁盘读取文件的实现。
列表 7.1 遍历目录以获取数据
import tensorflow as tf
filenames = tf.train.match_filenames_once('./audio_dataset/*.wav') ❶
count_num_files = tf.size(filenames) ❷
filename_queue = tf.train.string_input_producer(filenames)
reader = tf.WholeFileReader() ❸
filename, file_contents = reader.read(filename_queue)
with tf.Session() as sess: ❹
sess.run(tf.local_variables_initializer())
num_files = sess.run(count_num_files) ❺
coord = tf.train.Coordinator() ❻
threads = tf.train.start_queue_runners(coord=coord) ❻
for i in range(num_files): ❼
audio_file = sess.run(filename) ❼
print(audio_file) ❼
❶ 存储匹配模式的文件名
❷ 运行读取器以提取文件数据
❸ 在 TensorFlow 中原生读取文件
❹ 设置用于随机检索文件名的管道
❺ 计算文件数量
❻ 初始化文件名队列的线程
❼ 逐个遍历数据
7.2 从音频中提取特征
机器学习算法通常设计为使用特征向量作为输入,但声音文件使用不同的格式。你需要一种从声音文件中提取特征以创建特征向量的方法。
这有助于理解这些文件是如何表示的。如果你曾经见过黑胶唱片,你可能已经注意到音频在磁盘上以凹槽的形式表示。我们的耳朵通过空气中的振动来解释音频。通过记录振动特性,算法可以将声音存储在数据格式中。
真实世界是连续的,但计算机以离散值存储数据。声音通过模拟-数字转换器(ADC)数字化为离散表示。你可以将声音视为随时间波动的波动,但那些数据太嘈杂且难以理解。
表示波的另一种等效方式是检查每个时间间隔的频率。这种观点称为 频域。通过使用称为 离散傅里叶变换 的数学运算(通常通过称为 快速傅里叶变换 的算法实现),很容易在时域和频域之间进行转换(你将使用它从声音中提取特征向量)。
一个方便的 Python 库可以帮助你在频域中查看音频。从 mng.bz/X0J6 下载它,解压,然后运行以下命令来设置它:
$ python setup.py install
需要 Python 2
Bregman Toolkit 在 Python 2 中官方支持。如果你使用 Jupyter Notebooks,你可以按照官方 Jupyter 文档中的说明(mng.bz/ebvw)访问两个版本的 Python。
尤其是你可以使用以下命令包含 Python 2:
$ python2 -m pip install ipykernel
$ python2 -m -ipykernel install —user
一个声音可以产生 12 种音高。在音乐术语中,这 12 种音高是 C、C#、D、D#、E、F、F#、G、G#、A、A# 和 B。列表 7.2 展示了如何检索 0.1 秒间隔内每个音高的贡献,从而得到一个有 12 行的矩阵。列数随着音频文件长度的增加而增加。具体来说,对于 t 秒的音频,将有 10 × t 列。这个矩阵也称为音频的 chromagram。
列表 7.2 在 Python 中表示音频
from bregman.suite import *
def get_chromagram(audio_file): ❶
F = Chromagram(audio_file, nfft=16384, wfft=8192, nhop=2205) ❷
return F.X ❸
❶ 传入文件名
❷ 每 0.1 秒使用这些参数描述 12 个音阶
❸ 每秒 10 次表示一个 12 维向量的值
谱图输出是一个矩阵,如图 7.2 所示。音频片段可以读作谱图,谱图是生成音频片段的配方。现在你有了在音频和矩阵之间转换的方法。正如你所学的,大多数机器学习算法接受特征向量作为有效的数据形式。因此,你将要查看的第一个机器学习算法是 k-means 聚类。

图 7.2 谱图矩阵,其中 x 轴代表时间,y 轴代表音阶类别。绿色平行四边形表示在该时间存在该音阶。
要在您的 chromagram 上运行机器学习算法,首先您需要决定您将如何表示特征向量。一个想法是通过只查看每个时间间隔中最显著的音阶类来简化音频,如图 7.3 所示。

图 7.3 在每个时间间隔中突出的最有影响力的音高。你可以将其视为每个时间间隔中最响亮的音高。
然后你计算每个音高在音频文件中出现的次数。图 7.4 将这些数据作为直方图显示,形成一个 12 维向量。如果你将向量归一化,使得所有计数之和为 1,你可以轻松比较不同长度的音频。注意这种方法与你在第六章中使用的“词袋”方法类似,用于从任意长度的文本中生成词计数的直方图。

图 7.4 你计算在每个时间间隔内听到的最响亮音高的频率,以生成这个直方图,它作为你的特征向量。
练习 7.2
有哪些其他方法可以将音频片段表示为特征向量?
答案
你可以将音频剪辑可视化为图像(例如频谱图),并使用图像分析技术提取图像特征。
查看列表 7.3 以从图 7.4 生成直方图,这是你的特征向量。
列表 7.3 获取 k-means 的数据集
import tensorflow as tf
import numpy as np
from bregman.suite import *
filenames = tf.train.match_filenames_once('./audio_dataset/*.wav')
count_num_files = tf.size(filenames)
filename_queue = tf.train.string_input_producer(filenames)
reader = tf.WholeFileReader()
filename, file_contents = reader.read(filename_queue)
chroma = tf.placeholder(tf.float32) ❶
max_freqs = tf.argmax(chroma, 0) ❶
def get_next_chromagram(sess):
audio_file = sess.run(filename)
F = Chromagram(audio_file, nfft=16384, wfft=8192, nhop=2205)
return F.X
def extract_feature_vector(sess, chroma_data): ❷
num_features, num_samples = np.shape(chroma_data)
freq_vals = sess.run(max_freqs, feed_dict={chroma: chroma_data})
hist, bins = np.histogram(freq_vals, bins=range(num_features + 1))
return hist.astype(float) / num_samples
def get_dataset(sess): ❸
num_files = sess.run(count_num_files)
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(coord=coord)
xs = []
for _ in range(num_files):
chroma_data = get_next_chromagram(sess)
x = [extract_feature_vector(sess, chroma_data)]
x = np.matrix(x)
if len(xs) == 0:
xs = x
else:
xs = np.vstack((xs, x))
return xs
❶ 创建一个操作来识别贡献最大的音调
❷ 将音程图转换为特征向量
❸ 构建一个矩阵,其中每一行是一个数据项
注意:所有代码列表均可在本书的网站上找到,网址为 mng.bz/yrEq,以及 GitHub 上 mng.bz/MoJn。
按照我在其他章节中向您展示的内容,在准备您的数据集后,说服自己,列表 7.1 帮助您从磁盘读取并从列表 7.3 生成数据集的音程图中存在某种相关性。数据集由五个声音组成,对应两种不同的咳嗽声和三种不同的尖叫声。您可以使用您友好的 Matplotlib 库来可视化数据并检查列表 7.4 中的可学习性(底层数据的独特属性)。如果您能在声音文件中找到某种相关性,那么机器学习算法也有很大机会能找到。
你将为列表 7.4 中的 12 个音调创建一组标签 P1-P12。然后,因为你已经将数据转换为列表 7.3 中的五个 1 × 12 大小的矩阵,你将把这些矩阵展平成 12 个数据点,以便为每个五个音程图可视化。列表 7.4 的结果如图 7.5 所示。

图 7.5 探索五个声音——两个咳嗽声和三个尖叫声——及其在 12 个音调上的关系。如果你的眼睛能发现模式,那么计算机也有很大机会能发现。
列表 7.4 探索你的声音文件音程图
labels=[]
for i in np.arange(12):
labels.append("P"+str(i+1)) ❶
fig, ax = plt.subplots()
ind = np.arange(len(labels))
width = 0.15
colors = ['r', 'g', 'y', 'b', 'black'] ❷
plots = []
for i in range(X.shape[0]):
Xs = np.asarray(X[i]).reshape(-1) ❸
p = ax.bar(ind + i*width, Xs, width, color=colors[i])
plots.append(p[0])
xticks = ind + width / (X.shape[0])
print(xticks)
ax.legend(tuple(plots), ('Cough1', 'Cough2', 'Scream1', 'Scream2', 'Scream3'))❹
ax.yaxis.set_units(inch)
ax.autoscale_view()
ax.set_xticks(xticks)
ax.set_xticklabels(labels)
ax.set_ylabel('Normalized freq coumt')
ax.set_xlabel('Pitch')
ax.set_title('Normalized frequency counts for Various Sounds')
plt.show()
❶ 为每个 12 个音调频率生成标签。
❷ 为五个声音中的每一个选择不同的颜色。
❸ 将 1 × 12 矩阵展平成 12 个点,每个音调一个。
❹ 为五个声音创建一个图例:两个咳嗽声和三个尖叫声。
通过视觉探索图 7.5 来说服自己,这些声音之间存在音调相似性。咳嗽声的垂直条与音调 P1 和 P5 有关联,而尖叫声与音调 P5、P6 和 P7 的垂直条有很强的关联。其他不太明显的相似性是咳嗽声和尖叫声在 P5 和 P6 之间的关系。你会发现,如果你一次可视化一个声音,比如图 7.6,那么看到这些相关性有时会更容易,但这个例子是一个很好的开始。这里肯定有可以学习的东西,所以让我们看看计算机能告诉我们关于如何聚类这些文件的信息。
7.3 使用 k-means 聚类
k-means 算法是数据聚类中最古老且最稳健的方法之一。在k-means中的k代表一个自然数变量,因此你可以想象 3-means 聚类、4-means 聚类或任何其他k的值。因此,k-means 聚类的第一步是选择一个k的值。具体来说,让我们选择k = 3。考虑到这一点,3-means 聚类的目标是把数据集划分为三个类别(也称为簇)。
选择簇的数量
选择合适的簇数量通常取决于任务。假设你正在为数百人举办活动,既有年轻人也有老年人。如果你只有两个娱乐选项的预算,你可以使用 k-means 聚类,k = 2,将客人分成两个年龄组。在其他时候,确定k的值可能并不明显。自动确定k的值要复杂一些,所以在这个部分我们不会过多涉及。简单来说,确定最佳k值的一种直接方法是迭代一系列 k-means 模拟,并应用一个成本函数来确定哪个k值在最低k值时产生了最佳的簇间差异。
k-means 算法将数据点视为空间中的点。如果你的数据集是活动的客人集合,你可以通过他们的年龄来表示每个客人。因此,你的数据集是一组特征向量。在这种情况下,每个特征向量是 1 维的,因为你只考虑人的年龄。
对于通过音频数据进行音乐聚类,数据点是音频文件的特征向量。如果两个点彼此靠近,它们的音频特征相似。你希望发现哪些音频文件属于同一个“邻域”,因为这些簇可能是组织你的音乐文件的好方法。
一个簇中所有点的中点称为其质心。根据你选择的音频特征,质心可以捕捉到诸如响亮的声音、高音调的声音或类似萨克斯管的声音等概念。需要注意的是,k-means 算法分配了非描述性的标签,如簇 1、簇 2 和簇 3。图 7.6 展示了声音数据的示例。

图 7.6 展示了四个音频文件的示例。右侧的两个似乎具有相似的直方图,左侧的两个也具有相似的直方图。你的聚类算法将能够将这些声音分组。
k-means 算法通过选择与它最近的质心的簇来将特征向量分配给一个簇。k-means 算法首先猜测簇的位置,并随着时间的推移迭代地改进其猜测。算法要么在不再改进猜测时收敛,要么在尝试达到最大次数后停止。
算法的核心包括两个任务:
-
任务 —将每个数据项(特征向量)分配到最近的质心类别。
-
重新定位 —计算新更新的簇的中间点。
这两个步骤重复进行,以提供越来越好的聚类结果,算法在重复了期望的次数或分配不再改变时停止。图 7.7 说明了该算法。

图 7.7 k-means 算法的一次迭代。假设你正在将颜色聚类到三个桶(一种非正式地说成是类别的说法)。你可以从猜测红色、绿色和蓝色开始,以开始分配步骤。然后通过平均每个桶属于的颜色来更新桶的颜色。重复直到桶的颜色不再显著变化,达到每个簇的质心所代表的颜色。
列表 7.5 展示了如何通过使用列表 7.3 生成的数据集来实现 k-means 算法。为了简单起见,选择k = 2,这样你可以轻松验证你的算法将音频文件分割成两个不同的类别。你将使用前k个向量作为质心的初始猜测。
列表 7.5 实现 k-means
k = 2 ❶
max_iterations = 100 ❷
def initial_cluster_centroids(X, k): ❸
return X[0:k, :]
def assign_cluster(X, centroids): ❹
expanded_vectors = tf.expand_dims(X, 0)
expanded_centroids = tf.expand_dims(centroids, 1)
distances = tf.reduce_sum(tf.square(tf.subtract(expanded_vectors, expanded_centroids)), 2)
mins = tf.argmin(distances, 0)
return mins
def recompute_centroids(X, Y): ❺
sums = tf.unsorted_segment_sum(X, Y, k)
counts = tf.unsorted_segment_sum(tf.ones_like(X), Y, k)
return sums / counts
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
X = get_dataset(sess)
centroids = initial_cluster_centroids(X, k)
i, converged = 0, False
while not converged and i < max_iterations: ❻
i += 1
Y = assign_cluster(X, centroids)
centroids = sess.run(recompute_centroids(X, Y))
print(centroids)
❶ 决定簇的数量
❷ 声明运行 k-means 的最大迭代次数
❸ 选择簇质心的初始猜测
❹ 将每个数据项分配到最近的簇
❺ 更新簇质心到它们的中间点
❻ 迭代以找到最佳的簇位置
就这些了!如果你知道簇的数量和特征向量表示,你可以使用 7.5 列表来聚类任何东西。在 7.4 节中,你将应用聚类到音频文件内的音频片段。
7.4 分割音频
在 7.3 节中,您将各种音频文件进行了聚类以自动分组。本节是关于在一个音频文件内使用聚类算法。前一个过程被称为聚类,而后一个过程被称为分割。分割是聚类的另一种说法,但当我们把单个图像或音频文件分割成单独的组件时,我们通常说分割而不是聚类。分割与将句子分割成单词的方式不同,与将单词分割成字母的方式不同。尽管分割和聚类共享将更大的块分割成更小块的一般思想,但单词与字母是不同的。
假设你有一个长的音频文件,可能是播客或脱口秀。想象一下编写一个机器学习算法来识别音频访谈中哪两个人在说话。分割音频文件的目标是将音频剪辑的哪些部分关联到同一类别。在这种情况下,你将为每个人有一个类别,每个人的话语应该收敛到他们适当的类别,如图 7.8 所示。

图 7.8 音频分割是自动标记段的过程。
打开一个新的源文件,并按照列表 7.6 进行操作,这将通过组织音频数据以进行分割来开始。该代码将音频文件分割成大小为segment_size的多个段。一个长的音频文件可能包含数百甚至数千个段。
列表 7.6 为分割组织数据
import tensorflow as tf
import numpy as np
from bregman.suite import *
k = 2 ❶
segment_size = 50 ❷
max_iterations = 100 ❸
chroma = tf.placeholder(tf.float32)
max_freqs = tf.argmax(chroma, 0)
def get_chromagram(audio_file):
F = Chromagram(audio_file, nfft=16384, wfft=8192, nhop=2205)
return F.X
def get_dataset(sess, audio_file): ❹
chroma_data = get_chromagram(audio_file)
print('chroma_data', np.shape(chroma_data))
chroma_length = np.shape(chroma_data)[1]
xs = []
for i in range(chroma_length / segment_size):
chroma_segment = chroma_data[:, i*segment_size:(i+1)*segment_size]
x = extract_feature_vector(sess, chroma_segment)
if len(xs) == 0:
xs = x
else:
xs = np.vstack((xs, x))
return xs
❶ 决定簇的数量
❷ 段落大小越小,结果越好(但性能越慢)。
❸ 决定何时停止迭代
❹ 通过提取音频片段作为单独的数据项来获取数据集
现在运行 k-means 聚类算法于这个数据集,以确定何时段相似。目的是让 k-means 将听起来相似的段归类为相同的标签。如果两个人的声音听起来显著不同,他们的声音片段将属于不同的标签。列表 7.7 说明了如何将分割应用于音频剪辑。
列表 7.7 分割音频剪辑
with tf.Session() as sess:
X = get_dataset(sess, 'TalkingMachinesPodcast.wav')
print(np.shape(X))
centroids = initial_cluster_centroids(X, k)
i, converged = 0, False
while not converged and i < max_iterations: ❶
i += 1
Y = assign_cluster(X, centroids)
centroids = sess.run(recompute_centroids(X, Y))
if i % 50 == 0:
print('iteration', i)
segments = sess.run(Y)
for i in range(len(segments)): ❷
seconds = (i * segment_size) / float(10)
min, sec = divmod(seconds, 60)
time_str = '{}m {}s'.format(min, sec)
print(time_str, segments[i])
❶ 运行 k-means 算法
❷ 打印每个时间间隔的标签
运行列表 7.7 的输出是一个时间戳和簇 ID 的列表,对应于播客中谁在说话:
('0.0m 0.0s', 0)
('0.0m 2.5s', 1)
('0.0m 5.0s', 0)
('0.0m 7.5s', 1)
('0.0m 10.0s', 1)
('0.0m 12.5s', 1)
('0.0m 15.0s', 1)
('0.0m 17.5s', 0)
('0.0m 20.0s', 1)
('0.0m 22.5s', 1)
('0.0m 25.0s', 0)
('0.0m 27.5s', 0)
练习 7.3
你如何检测聚类算法是否收敛(以便你可以提前停止算法)?
答案
一种方法是通过监控簇中心的变化,并在不再需要更新时宣布收敛(例如,当迭代之间误差大小的差异没有显著变化时)。为此,你需要计算误差的大小并决定什么构成了显著变化。
7.5 使用自组织映射进行聚类
自组织映射(SOM)是一种将数据表示为低维空间中的模型。在这个过程中,SOM 会自动将相似的数据项移动得更近。假设你正在为一大群人点披萨。你不想为每个人点同一种类型的披萨,因为可能有人喜欢加有凤梨、蘑菇和辣椒的奢华披萨,而你可能更喜欢加有金枪鱼、芝麻菜和洋葱的披萨。
每个人的配料偏好可以用一个 3D 向量表示。SOM 让你将这些 3D 向量嵌入到二维空间中(只要你定义披萨之间的距离度量)。然后 2D 图的可视化揭示了簇数量的良好候选者。
虽然可能比 k-means 算法收敛得慢,但 SOM 方法对簇的数量没有假设。在现实世界中,很难选择簇的数量。考虑一个随着时间的推移簇会改变的聚会,如图 7.9 所示。

图 7.9 在现实世界中,我们经常看到人群在簇中聚集。应用 k-means 需要事先知道簇的数量。一个更灵活的工具是 SOM,它对簇的数量没有先入为主的观念。
SOM 仅将数据重新解释为有利于聚类的结构。算法工作原理如下:
-
设计一个节点网格。每个节点持有与数据项相同维度的权重向量。每个节点的权重初始化为随机数,通常是标准正态分布。
-
逐个向网络展示数据项。对于每个数据项,网络都会识别出权重向量与其最接近的节点。这个节点被称为 最佳匹配单元 (BMU)。
在网络识别出 BMU 后,所有 BMU 的邻居都会更新,以便它们的权重向量更接近 BMU 的值。距离 BMU 更近的节点受到的影响比距离更远的节点更强。此外,BMU 周围邻居的数量会随着时间的推移以通常通过试错确定的速率减少。图 7.10 阐述了该算法。

图 7.10 SOM 算法的一次迭代。第一步是识别 BMU,第二步是更新相邻节点。您会继续使用训练数据迭代这两个步骤,直到达到某些收敛标准。
列表 7.8 展示了如何在 TensorFlow 中开始实现 SOM。通过打开一个新的源文件来跟随操作。
列表 7.8 设置 SOM 算法
import tensorflow as tf
import numpy as np
class SOM:
def __init__(self, width, height, dim):
self.num_iters = 100
self.width = width
self.height = height
self.dim = dim
self.node_locs = self.get_locs()
nodes = tf.Variable(tf.random_normal([width*height, dim])) ❶
self.nodes = nodes
x = tf.placeholder(tf.float32, [dim]) ❷
iter = tf.placeholder(tf.float32) ❷
self.x = x ❸
self.iter = iter ❸
bmu_loc = self.get_bmu_loc(x) ❹
self.propagate_nodes = self.get_propagation(bmu_loc, x, iter) ❺
❶ 每个节点是一个维度为 dim 的向量。对于 2D 网格,有 width × height 个节点;get_locs 在列表 7.11 中定义。
❷ 这两个操作在每个迭代中都是输入。
❸ 您需要从另一个方法中访问它们。
❹ 找到与输入最接近的节点(在列表 7.10 中)
❺ 更新邻居的值(在列表 7.9 中)
在列表 7.9 中,您定义了如何根据当前时间间隔和 BMU 位置更新相邻权重。随着时间的推移,BMU 的相邻权重受到的影响逐渐减小。这样,权重就会随着时间的推移逐渐稳定。
列表 7.9 定义如何更新邻居的值
def get_propagation(self, bmu_loc, x, iter):
num_nodes = self.width * self.height
rate = 1.0 - tf.div(iter, self.num_iters) ❶
alpha = rate * 0.5
sigma = rate * tf.to_float(tf.maximum(self.width, self.height)) / 2.
expanded_bmu_loc = tf.expand_dims(tf.to_float(bmu_loc), 0) ❷
sqr_dists_from_bmu = tf.reduce_sum(
tf.square(tf.subtract(expanded_bmu_loc, self.node_locs)), 1)
neigh_factor = ❸
tf.exp(-tf.div(sqr_dists_from_bmu, 2 * tf.square(sigma)))
rate = tf.multiply(alpha, neigh_factor)
rate_factor =
tf.stack([tf.tile(tf.slice(rate, [i], [1]),
[self.dim]) for i in range(num_nodes)])
nodes_diff = tf.multiply(
rate_factor,
tf.subtract(tf.stack([x for i in range(num_nodes)]), self.nodes))
update_nodes = tf.add(self.nodes, nodes_diff) ❹
return tf.assign(self.nodes, update_nodes) ❺
❶ 随着迭代的增加,该速率降低。此值影响 alpha 和 sigma 参数。
❷ 扩展 bmu_loc 以便您能够高效地将其成对地与 node_locs 中的每个元素进行比较
❸ 确保靠近 BMU 的节点变化更为显著
❹ 定义更新操作
❺ 返回一个操作以执行更新
列表 7.10 展示了如何根据输入数据项找到 BMU 位置。它搜索节点网格以找到最接近匹配的节点。这一步骤类似于 k-means 聚类中的分配步骤,其中网格中的每个节点都是一个潜在的聚类质心。
列表 7.10 获取最接近匹配的节点位置
def get_bmu_loc(self, x):
expanded_x = tf.expand_dims(x, 0)
sqr_diff = tf.square(tf.subtract(expanded_x, self.nodes))
dists = tf.reduce_sum(sqr_diff, 1)
bmu_idx = tf.argmin(dists, 0)
bmu_loc = tf.stack([tf.mod(bmu_idx, self.width), tf.div(bmu_idx,
å self.width)])
return bmu_loc
在列表 7.11 中,您创建了一个辅助方法来生成网格中所有节点的 (x, y) 位置列表。
列表 7.11 生成点矩阵
def get_locs(self):
locs = [[x, y]
for y in range(self.height)
for x in range(self.width)]
return tf.to_float(locs)
最后,让我们定义一个名为 train 的方法来运行算法,如列表 7.12 所示。首先,你必须设置会话并运行 global_variables_initializer 操作。接下来,你循环 num_iters 次数来使用输入数据逐个更新权重。当循环结束时,你记录最终的节点权重及其位置。
列表 7.12 运行 SOM 算法
def train(self, data):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(self.num_iters):
for data_x in data:
sess.run(self.propagate_nodes, feed_dict={self.x: data_x,
å self.iter: i})
centroid_grid = [[] for i in range(self.width)]
self.nodes_val = list(sess.run(self.nodes))
self.locs_val = list(sess.run(self.node_locs))
for i, l in enumerate(self.locs_val):
centroid_grid[int(l[0])].append(self.nodes_val[i])
self.centroid_grid = centroid_grid
就这样!现在让我们看看算法的实际应用。通过向 SOM 展示一些输入来测试实现。在列表 7.13 中,输入是一个 3D 特征向量的列表。通过训练 SOM,你将在数据中发现簇。你将使用一个 4 × 4 的网格,但最好尝试不同的值以进行交叉验证以找到最佳网格大小。图 7.11 显示了代码的输出。

图 7.11 SOM 将所有 3D 数据点放置在二维网格中。从图中,你可以选择簇中心(自动或手动)并在直观的低维空间中实现聚类。
列表 7.13 测试实现并可视化结果
from matplotlib import pyplot as plt
import numpy as np
from som import SOM
colors = np.array(
[[0., 0., 1.],
[0., 0., 0.95],
[0., 0.05, 1.],
[0., 1., 0.],
[0., 0.95, 0.],
[0., 1, 0.05],
[1., 0., 0.],
[1., 0.05, 0.],
[1., 0., 0.05],
[1., 1., 0.]])
som = SOM(4, 4, 3) ❶
som.train(colors)
plt.imshow(som.centroid_grid)
plt.show()
❶ 网格大小为 4 × 4,输入维度为 3。
自组织映射(SOM)将高维数据嵌入到二维空间中,以便于聚类。这个过程充当了一个方便的预处理步骤。你可以通过观察 SOM 的输出手动指示簇中心,但也可以通过观察权重的梯度自动找到好的中心候选者。如果你喜欢冒险,我建议阅读 Juha Vesanto 和 Esa Alhoniemi 撰写的著名论文“自组织映射的聚类”,链接为mng.bz/XzyS。
7.6 应用聚类
你已经看到了两个聚类的实际应用:组织音乐和分割音频片段以标记相似的声音。当训练数据集不包含相应的标签时,聚类特别有帮助。正如你所知,这种情况代表了无监督学习。有时,数据不便进行标注。
假设你想了解来自手机或智能手表加速度计的传感器数据。在每个时间步,加速度计提供一个 3D 向量,但你不知道人类是在行走、站立、坐着、跳舞、慢跑还是其他活动。你可以在mng.bz/rTMe获得这样的数据集。
要对时间序列数据进行聚类,你需要将加速度计向量的列表总结成一个简洁的特征向量。一种方法是通过生成加速度连续幅度的差异直方图。加速度的导数称为 jerk,你可以应用相同的操作来获得描述 jerk 幅度差异的直方图。
从数据生成直方图的过程与本章中解释的音频数据预处理步骤非常相似。在你将直方图转换为特征向量之后,你可以使用本章中的代码列表(例如 TensorFlow 中的 k-means)。
注意:虽然前几章讨论了监督学习,但本章重点介绍无监督学习。在第九章中,您将看到一个既不属于监督学习也不属于无监督学习的机器学习算法。这个算法是一个建模框架,虽然程序员对其关注不多,但它是统计学家揭示数据中隐藏因素的必备工具。
摘要
-
聚类是一种用于在数据中寻找结构的无监督机器学习算法。
-
其中最容易实现和理解的算法之一是 k-means 聚类,它在速度和准确性方面也表现良好。
-
如果没有指定聚类数量,您可以使用 SOM 算法以简化的视角查看数据。
从安卓加速度计数据中推断用户活动
本章涵盖
-
在三维空间以及时间维度上可视化您的手机位置数据
-
对安卓手机用户进行探索性数据分析并识别模式
-
使用聚类自动将安卓手机用户根据其位置数据进行分组
-
可视化 K-means 聚类
现在,我们几乎无法与一个小巧、轻薄、通常为黑色的设备分开,这个设备将我们彼此以及与世界连接起来:我们的移动电话。这些设备是计算奇迹,微型芯片,拥有强大的微处理器,比十年前的台式计算机更加强大。再加上对 Wi-Fi 网络的广泛连接,以及允许与边缘设备进行窄带和近距离安全连接的蓝牙。不久,Wi-Fi 5G 和蓝牙 6 将使这些连接扩展到地理上分散的网络,到千兆字节的数据,以及构成物联网的数百万个互联设备。
这些强大的手机还附带了各种传感器,包括摄像头、温度传感器、光敏屏幕,以及在本章之前可能对您来说不太为人所知的:加速度计。加速度计可以检测和监控滚动机器的振动,并可用于在三维空间中随时间获取位置数据,这是四维数据。所有手机都配备了这些传感器,它们以下列形式提供位置数据:
(X, Y, Z, t )
当您行走、交谈、爬楼梯和开车时,加速度计会在手机的内存中收集和记录这些数据,通常是一个具有数百 GB 信息的大容量固态设备,很快将变成千兆字节(TB)。这些数据——您在执行各种活动(如行走、交谈和爬楼梯)时的时空位置——被记录下来,可以根据您在时空中的位置以无监督的方式推断这些活动。您可以想象手机捕获的位置数据如图 8.1 所示。一个人在平面上沿正负 X、Z 方向移动,而垂直位置由 Y 轴捕获。

图 8.1 在真实世界中,您的手机捕获的(X, Y, Z)位置数据看起来是怎样的。一个人在空间中沿着 x 轴或 z 轴正负移动,当这个人跳跃或弯腰时,y 轴上发生移动。
正如我在第七章中讨论的那样,机器学习不需要标签来做出预测。你可以通过应用无监督方法,如 k-means 聚类和自组织映射(SOM)到自然适合一起的数据点来做很多事情。就像你的孩子们把蓝光光盘扔得到处都是,在你家里到处都是(等等——这不会发生在你身上?),需要组织和仔细放置回你的媒体架子里一样,你为了组织目的而聚类的物品是有自然顺序的。对于这些蓝光光盘,顺序可能是按类型或演员来分类。
但是你如何在手机上对位置数据进行分类或组织呢?这些数据很可能具有自然组织性;请自己确信这一点。当你跑步、跳跃或驾驶时,所有位置数据很可能符合某种趋势或模式。也许从点 ti 到点 ti+1 的差异涉及速度或加速度的急剧变化。速度和加速度是位置(X,Y,Z)相对于时间(t)的 8.1 图中的第一和第二导数。许多研究人员发现,冲量——位置相对于时间的第三导数——代表了作用于物体上的力的变化率。这个冲量最终被证明是你转换活动时运动趋势和变化的适当趋势,正如你的位置数据所捕捉的那样。冲量的幅度差异为无监督地聚类基于时间的位置数据提供了很好的分类;这些聚类代表你的运动活动,如跑步、攀爬和行走。
在本章中,你将基于你已经学到的 k-means 和 SOM 技术,并将它们应用于实际的位置数据。这些数据来自加州大学欧文分校(UCI)的机器学习库,这是一个开放数据集的大杂烩,你可以应用机器学习技术来生成有用的见解。用户活动从行走数据集正是你可以应用 TensorFlow 和无监督聚类技术的数据类型。让我们开始吧!
8.1 用户活动从行走数据集
用户活动从行走数据集包含 22 名参与者和他们相关的基于 Android 的位置数据。数据是在 2014 年 3 月捐赠的,并且已经被访问超过 69,000 次。当参与者的 Android 手机放在胸前的口袋中时,加速度计收集了(X,Y,Z,t)位置和时间数据。参与者沿着预定义的路径在野外行走并执行各种活动。这些数据可以用于无监督地推断活动类型。这个数据集在机器学习领域是一个常用的基准,因为它为通过使用运动模式识别和验证人员提供了挑战。
你可以从mng.bz/aw5B下载数据集。数据集解压后,文件夹结构如下所示:
1.csv 10.csv 11.csv 12.csv 13.csv 14.csv 15.csv 16.csv 17.csv
18.csv 19.csv 2.csv 20.csv 21.csv 22.csv 3.csv 4.csv
5.csv 6.csv 7.csv 8.csv 9.csv README
每个 CSV 文件对应 22 个参与者中的一个,CSV 文件的内容看起来像图 8.2 中所示的(t, X, Y, Z)形式的位置数据。

图 8.2 展示了参与者数据在 CSV 文件中以一系列数据点的形式。
在对这个 CSV 数据进行操作的第一步是将它转换为 TensorFlow 可以用于机器学习的格式:一个 NumPy 数据集。为此,你可以使用 TensorFlow 的FileReader API。你在第七章中使用了这个 API 来从音频文件的色谱图中提取特征向量,并将它们组合成一个 NumPy 数据集用于聚类。
在本章中,我们不是通过将声音转换为数字(使用快速傅里叶变换)来创建特征向量,而是将你已有的数字——在(X, Y, Z)空间中的位置——转换为加速度大小。你可以使用 Pandas 及其巧妙的 dataframe API 将参与者 CSV 文件读入内存中的一个大表格结构;然后你可以从这个结构中获取 NumPy 数组。使用我在前面章节中展示的 NumPy 数组处理 TensorFlow 和机器学习会更容易,因为你可以将 22 个参与者和他们的位置数据组合成一个 TensorFlow 兼容的 NumPy 矩阵。工作流程可以概括为以下形式(图 8.3):
-
使用 TensorFlow 的
FileReaderAPI 获取文件内容和相关的文件名。 -
使用诸如色谱图快速傅里叶变换或通过计算其N阶导数在位置数据上计算加速度大小的方法,为每个文件提取一个特征向量作为 NumPy 数组。
-
将 NumPy 数组垂直堆叠成一个 NumPy 矩阵,创建一个 N × M 的数组,其中N是样本数量,M是特征数量。
-
使用 k-means、SOM 或其他技术对 N × M 的 NumPy 矩阵进行聚类。

图 8.3 展示了为 TensorFlow 聚类准备数据的一般方法,从左到右依次为聚类过程的输入。
列表 8.1 执行了这个工作流程的第一步,使用 TensorFlow 遍历 CSV 文件,并为文件名及其内容创建成对的集合。
列表 8.1 获取 22 个参与者 CSV 文件的位置内容和文件名
import tensorflow as tf
filenames = tf.train.match_filenames_once('./User Identification From Walking
➥ Activity/*.csv') ❶
count_num_files = tf.size(filenames) ❷
filename_queue = tf.train.string_input_producer(filenames) ❸
reader = tf.WholeFileReader() ❸
filename, file_contents = reader.read(filename_queue) ❹
❶ 创建参与者 CSV 文件名的列表
❷ 计算 CSV 文件名的数量
❸ 使用 TensorFlow WholeFileReader API 读取文件内容
❹ 为所有参与者创建文件名/内容对
要开始对参与者的位置数据进行聚类,你需要一个 TensorFlow 数据集来工作。让我们使用 TensorFlow 的FileReader API 创建一个。
8.1.1 创建数据集
按照我在第七章中向你展示的,你可以通过创建一个get_dataset函数来组织你的代码,该函数依赖于一个get_feature_vector函数(图 8.3 中显示的工作流程中的步骤 2)将你的原始 CSV 位置数据转换为聚类特征向量。此函数的实现计算加速度并将其用作特征向量;加速度是位置对时间的三阶导数。当你计算了所有参与者的加速度后,你需要将这些特征向量垂直堆叠到一个大矩阵中,以便进行聚类(图 8.3 中工作流程的步骤 3),这正是get_dataset函数的目的。
在开始提取特征向量和计算加速度之前,一般良好的机器学习实践是进行一些数据探索分析,通过视觉检查来了解你正在处理的信息类型。为了获取一些你可以查看的样本位置数据,你应该首先尝试绘制出get_dataset函数。如你在图 8.2 中看到的,CSV 文件包含四个列(时间,X 位置,Y 位置和 Z 位置),因此你可以让 Pandas 根据其数据框命名列。不要担心你还没有编写你的extract_feature_vector函数。查看列表 8.2 中的代码以开始。
列表 8.2 将文件名和内容转换为 NumPy 矩阵数据集
def get_dataset(sess):
sess.run(tf.local_variables_initializer()) ❶
num_files = sess.run(count_num_files) ❶
coord = tf.train.Coordinator() ❶
threads = tf.train.start_queue_runners(coord=coord)
accel_files = []
xs = []
for i in range(num_files):
accel_file = sess.run(filename) ❷
accel_file_frame = pd.read_csv(accel_file, header=None, sep=',',
names = ["Time", "XPos", "YPos", "ZPos"]) ❸
accel_files.append(accel_file_frame) ❸
print(accel_file)
x = [extract_feature_vector(sess, accel_file_frame.values)] ❹
x = np.matrix(x)
if len(xs) == 0:
xs = x
else:
xs = np.vstack((xs, x)) ❺
return xs
❶ 创建一个 TensorFlow 会话并计算需要遍历的文件数量
❷ 使用 TensorFlow 获取文件名
❸ 将文件内容读取到具有命名列的 Pandas 数据框中,并将它们收集到一个列表中
❹ 提取特征向量
❺ 将向量垂直堆叠在 NumPy 矩阵中
在深入计算特征向量之前,你可以检查accel_files变量中创建的 Pandas 数据框中的数据。例如,前两个参与者的位置数据中的 X 值可以很容易地用 Matplotlib 绘制如下:
accel_files[0]["XPos"].plot.line()
accel_files[1]["XPos"].plot.line()
图 8.4 的结果,即使只沿 X 维度,也显示了重叠的模式,至少在 x 轴的前 5,000 个或更多时间步内。这为机器学习算法提供了学习的东西。

图 8.4 使用 Pandas 和 Matplotlib 显示前两个参与者的 X 位置数据。在最初的约 5,000 个时间步内(x 轴),点重叠现象很普遍。
如果你能够发现一个模式,机器学习算法应该能够在所有维度上发现类似的模式。算法应该能够识别出参与者 1 和 2 在前 5,000 个时间步内具有相似的 X 位置,例如。你可以通过使用 TensorFlow 利用这一信息,并轻松地将其扩展到处理所有三个位置维度,正如我将在下一节中向你展示的那样。
8.1.2 计算加速度并提取特征向量
让我们从计算所有数据的急动量开始。你可以使用 NumPy 的 np.diff 函数,该函数计算 out = a[n+1] - a[n] 对于你的数据中的所有 n,留下一个大小为 n-1 的数组。如果你有 foo = [1., 2., 3., 4., 5.] 并执行 np.diff(foo),结果将是 [1., 1., 1., 1.]。计算一次 np.diff 给你输入位置数据的第一导数(速度);计算第二次给你第二导数(加速度),依此类推。因为急动量是位置对时间的三阶导数,你需要调用 np.diff 三次,将每个前一个调用的结果传递给后续的调用。因为 np.diff 与多维输入一起工作,它计算输入矩阵中每个维度 M 的差异,而你的输入有三个维度,一个由 X、Y 和 Z 样本组成的位置。
探索你的 CSV 数据的下一个重要步骤是了解每个参与者的样本数量,这对应于你的 N × 3 参与者矩阵中的 N 参数。你可以通过取列表 8.1(一对 22 个参与者文件名及其内容)的结果,然后绘制每个参与者收集到的位置点数量的快速直方图,如图 8.3 所示,来找出每个参与者数据的样本数量。结果,如图 8.5 所示,显示了为 22 个参与者收集的位置数据的非均匀分布,这意味着你的 N 对于所有参与者及其输入来说并不相等,至少一开始是这样的。别担心,我会告诉你如何归一化样本。
列表 8.3 解码每个参与者的位置点
import matplotlib.pyplot as plt ❶
num_pts = [len(a_f) for a_f in accel_files] ❷
plt.hist(num_pts) ❸
❶ 导入 Matplotlib
❷ 统计 accel_files 中每个文件的位置点数量
❸ 绘制每个参与者文件位置点数量的均匀宽度直方图

图 8.5 所有 22 个参与者 CSV 文件中参与者样本的分布。有 9 个参与者的位置样本大约有 5,000 个,1 个有大约 17,000 个,另一个有 20,000 个,等等。
这个结果是一个问题,因为它在第七章中也是如此,因为聚类的目标是让一个 1 × M 特征向量代表你想要聚类的每个参与者。你可以观察到,在位置数据的所有时间步长中,X、Y、Z 位置维度中每个维度都有一个最大值或主导的急动量。回到急动量的定义——作用在物体上的力的变化率——在每个三个位置维度中选择最大值或主导值,可以得到三个轴上最高的力,以及最负责影响从一种活动切换到另一种活动的力。选择每个样本每个维度的最大急动量幅度,可以使你将矩阵减少到 N × 1 的大小,从而为每个参与者提供一个单一的 1D 点集。
要将这个矩阵转换回 3D 1 × 3 矩阵,你可以对 N 个样本的最大冲量大小进行直方图计数,然后在样本数量 N 上归一化,以产生每个参与者的特征向量作为 1 × 3 矩阵。这样做将为每个参与者产生一个有效的运动特征,用于独立于每个参与者样本数量的位置数据的聚类表示。执行此转换的代码是您的get_feature_vector函数。列表 8.4 有详细的说明。
列表 8.4 选择和计算每个维度的最大冲量大小
jerk = tf.placeholder(tf.float32) ❶
max_jerk = tf.reduce_max(jerk, keepdims=True, axis=1) ❶
def extract_feature_vector(sess, x):
x_s = x[:, 1:] ❷
v_X = np.diff(x_s, axis=0) ❸
a_X = np.diff(v_X, axis=0) ❸
j_X = np.diff(a_X, axis=0) ❸
X = j_X
mJerk = sess.run(max_jerk, feed_dict = {jerk: X}) ❹
num_samples, num_features = np.shape(X) ❺
hist, bins = np.histogram(mJerk, bins=range(num_features + 1))
return hist.astype(float) / num_samples ❻
❶ 初始化冲量矩阵和 TensorFlow 运算符,用于计算每个维度的最大冲量
❷ 我们不需要时间变量,因此考虑(t, X, Y, Z)中的(X, Y, Z),通过截断 t 来考虑。
❸ 计算速度(v_X)、加速度(a_X)和冲量(j_X)
❹ 通过运行 TensorFlow 计算最大冲量
❺ 样本数量为 1;特征数量为 3。
❻ 返回按样本数量(总点数)归一化的最大冲量大小
您可以通过使用 Matplotlib 绘制结果归一化冲量大小并检查每个参与者的样本来确信,在进行此数据操作时,仍然可以从绘制归一化冲量大小和检查每个 22 名参与者的样本中学习到一些东西。列表 8.5 设置了绘图以执行此探索性数据分析。第一步是为每个参与者和他们的 3D X、Y、Z 归一化冲量大小创建子图,每个参与者的三个条形使用不同的随机颜色。正如您可以从图 8.6 中看出,存在一个明显的模式。某些参与者在某些维度上的条形紧密对齐,这意味着参与者要么改变了动作以执行相同的活动,要么从他们共同执行的活动中偏离。换句话说,您可以找到趋势,机器学习算法也可以找到聚类。现在您已经准备好对相似参与者进行聚类。
列表 8.5 可视化标准化冲量大小
labels=['X', 'Y', 'Z'] ❶
fig, ax = plt.subplots()
ind = np.arange(len(labels))
width = 0.015
plots = []
colors = [np.random.rand(3,1).flatten() for i in range(num_samples)] ❷
for i in range(num_samples): ❸
Xs = np.asarray(X[i]).reshape(-1)
p = ax.bar(ind + i*width, Xs, width, color=colors[i])
plots.append(p[0])
xticks = ind + width / (num_samples)
print(xticks)
ax.legend(tuple(plots), tuple(['P'+str(i+1) for i in range(num_samples)]),
➥ ncol=4)
ax.yaxis.set_units(inch)
ax.autoscale_view()
ax.set_xticks(xticks)
ax.set_xticklabels(labels)
ax.set_ylabel('Normalized jerk count')
ax.set_xlabel('Position (X, Y, Z)')
ax.set_title('Normalized jerk magnitude counts for Various Participants')
plt.show()
❶ X、Y、Z 中的位置的三维
❷ 为 22 名参与者的每个条形选择一个随机颜色
❸ 为所有 22 名参与者的每个 3 条柱子添加子图

图 8.6 所有 22 名参与者在 X、Y、Z 三维空间中的标准化冲量大小。如图所示,存在一个明显的模式:参与者的动作取决于他们在 X、Y 和 Z 方向上的活动。
8.2 基于冲量大小对相似参与者进行聚类
正如你在第七章中学到的,k-means 聚类是一种无监督聚类方法,它有一个单超参数 K,你可以用它来设置所需聚类的数量。通常,你可以选择不同的 K 值来观察结果,以及生成的聚类与提供的数据的拟合程度。由于我们对每个参与者在时间上成千上万的数据点的最大加速度大小进行了归一化,因此我们得到了该时间段内位置变化(加速度)的一种总结。
对于 K 的一个容易选择的值是 2,以查看是否存在参与者之间的自然分割线。也许有些参与者有极端的位置变化;他们跑得快然后停下来,或者从什么也不做到跳得很高。与那些逐渐改变运动的其他参与者相比,这些随时间变化的运动突然变化应该会产生较高的加速度大小,例如,在停下来之前减速,或者整个时间都在走路或轻快慢跑。
为了这个目的,选择 K = 2 意味着我们的参与者可能存在两个随时间变化的自然运动区间。让我们用 TensorFlow 来试一试。在第七章中,我向你展示了如何使用 TensorFlow 实现 K-means 算法,并在你的输入 NumPy 矩阵特征向量上运行它。你可以将相同的代码向前推进,并将其应用于调用列表 8.2 中的 get_dataset 函数得到的数据集。
列表 8.6 添加了在第七章中定义的函数,以实现 k-means,并在你的输入参与者数据上运行这些函数,对输入数据进行聚类。为了刷新你的记忆,首先定义以下内容:
-
超参数
k和max_iterations -
聚类的数量
-
你将更新质心并将点分配到新聚类的最大次数,基于它们与质心的距离
initial_cluster_centroids 函数将数据集中的前 K 个点作为初始聚类猜测,这些猜测可能是随机的。然后,assign_cluster 函数使用 TensorFlow 的 expand_dims 函数为计算 X 数据集中点与最初猜测的质心之间的差异创建一个额外的维度。距离质心较远的点可能不在与距离相似的点相同的聚类中。
创建另一个维度允许你将这些距离分割成 expanded_centroids 变量中最初选择的 K 个聚类,该变量存储了通过算法的每次迭代,X 点与聚类质心之间的平方距离之和。
然后应用 TensorFlow 的argmin函数来识别哪个维度(0 到 K)是最小距离,并使用该掩码来识别特定的数据点和其组。数据点和组掩码相加并除以数据点计数以获得新的质心,这些质心是组中所有点平均距离。TensorFlow 的unsorted_segment_sum将质心组掩码应用于该聚类的特定点。然后 TensorFlow 的ones_like在每个数据点簇中创建一个计数元素掩码,并将每个数据点从质心到距离的总和除以计数以获得新的质心。
列表 8.6 在所有参与者的加速度大小上运行 k-means
k = 2 ❶
max_iterations = 100 ❶
def initial_cluster_centroids(X, k): ❷
return X[0:k, :]
def assign_cluster(X, centroids):
expanded_vectors = tf.expand_dims(X, 0) ❸
expanded_centroids = tf.expand_dims(centroids, 1) ❹
distances = tf.reduce_sum(tf.square(tf.subtract(expanded_vectors,
➥ expanded_centroids)), 2) ❺
mins = tf.argmin(distances, 0) ❻
return mins
def recompute_centroids(X, Y):
sums = tf.unsorted_segment_sum(X, Y, k) ❼
counts = tf.unsorted_segment_sum(tf.ones_like(X), Y, k) ❽
return sums / counts ❽
groups = None
with tf.Session() as sess: ❾
X = get_dataset(sess) ❿
centroids = initial_cluster_centroids(X, k)
i, converged = 0, False
while not converged and i < max_iterations: ⓫
i += 1
Y = assign_cluster(X, centroids)
centroids = sess.run(recompute_centroids(X, Y))
print(centroids) ⓬
groups = Y.eval()
print(groups) ⓬
❶ 定义超参数
❷ 初始时从 X 中取出前 K 个元素作为每个质心
❸ 创建一个扩展维度来处理计算每个点与质心的距离
❹ 创建一个扩展维度,将距离分为从每个质心开始的 K 组
❺ 计算距离
❻ 通过选择每个质心的最小距离来创建组掩码
❼ 通过使用掩码对每个数据点的最小距离求和
❽ 将总距离除以数据点的数量并重新计算新的质心
❾ 初始化 TensorFlow
❿ 获取参与者标准化加速度大小的初始数据集 22 × 3
⓫ 保持聚类 100 次迭代并计算新的中心点
⓬ 打印质心和组
在运行 k-means 和列表 8.6 中的代码后,你获得了两个质心点和聚类掩码:0 表示第一组;否则,标签为 1。你也可以使用一些简单的 Matplotlib 散点图功能来可视化参与者和两个聚类。你可以使用你的组掩码通过使用 0 和 1 索引到 X 数据集的点,并且你可以绘制质心点作为 X,表示每个组的中心。列表 8.7 有代码,图 8.7 显示了结果。虽然你不能确定每个聚类的标签,但很明显,参与者的运动清楚地符合两个不同的类别,因为每个组中的参与者数据有很好的分离。
[[0.40521887 0.23630463 0.11820933]
[0.59834667 0.14981037 0.05972407]]
[0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1 0 1 1 0 1]
列表 8.7 可视化两个参与者运动聚类
plt.scatter([X[:, 0]], [X[:, 1]], c=groups, s=50, alpha=0.5) ❶
plt.plot([centroids[:, 0]], [centroids[:, 1]], 'kx', markersize=15) ❷
plt.show()
❶ 使用你的组掩码索引 X 数据
❷ 使用质心点绘制 X 中心点

图 8.7 运动的两个参与者聚类。虽然你不知道每个聚类的精确标签,但你可以看到在时间上具有相似运动的参与者之间的清晰划分。
你已经使用了无监督聚类和表示参与者运动的加速度大小来将运动分为两类。你不确定哪一类涉及短促的运动,哪一类是渐进的运动,但你可以将这个确定留给未来的分类活动。
接下来,而不是查看所有参与者的动作,我将向您展示如何使用 k-means 将单个参与者的动作划分为动作类别。继续前进!
8.3 单个参与者的不同用户活动类别
在第七章中,我向您展示了如何对一个单独的声音文件进行分割,并在特定时间段内分离该声音文件中的说话者,并将他们分类到不同的组中。您可以通过使用 TensorFlow 和 k-means 聚类来对每个参与者的位置点数据进行类似操作。
UCI 的机器学习存储库,其中包含你在本章中使用的数据集“步行用户活动”,指向一篇题为“使用生物特征步行模式在可穿戴系统中进行个性化用户验证”的源论文。这篇论文指出,研究中的参与者执行了五类活动:爬楼梯、站立、行走、谈话和工作。(您可以在mng.bz/ggQE获取源论文。)
从逻辑上讲,将单个参与者的 N 个样本位置数据段分组,然后尝试自动分割和聚类代表特定时间加速度读数的点,并将它们聚类到野外活动中发生的不同动作中是有意义的。您可以从数据集中提取一个 CSV 文件,并使用超参数segment_size对其进行位置分割。当文件被分割后,您可以使用列表 8.4 中的extract_feature_vector方法来计算每个大小为 50 个位置点的段的加速度幅度及其直方图,以实现沿 5 个可能的动作类别的表示位置加速度幅度聚类。列表 8.8 创建了一个使用segment_size修改后的get_dataset版本,因此命名为get_dataset_segemented。
列表 8.8 分割单个参与者 CSV
segment_size = 50 ❶
def get_accel_data(accel_file):
accel_file_frame = pd.read_csv(accel_file, header=None, sep=',',
names = ["Time", "XPos", "YPos", "ZPos"])
return accel_file_frame.values
def get_dataset_segmented(sess, accel_file):
accel_data = get_accel_data(accel_file) ❷
print('accel_data', np.shape(accel_data))
accel_length = np.shape(accel_data)[0] ❸
print('accel_length', accel_length)
xs = []
for i in range(accel_length / segment_size):
accel_segment = accel_data[i*segment_size:(i+1)*segment_size, :] ❹
x = extract_feature_vector(sess, accel_segment)
x = np.matrix(x) ❺
if len(xs) == 0:
xs = x
else:
xs = np.vstack((xs, x))
return accel_data, xs
❶ 定义超参数
❷ 获取 CSV 位置数据
❸ 样本数量
❹ 对于每个segment_size,切掉相应数量的点并提取加速度的幅度。
❺ 将加速度幅度堆叠成一个 N × 3 矩阵,其中 N 是样本数/分割大小
在准备好你的分割数据集后,你就可以再次运行 TensorFlow 了,这次使用一个 N × 3 的矩阵,其中N是样本数 of segment_size。当你这次运行k = 5的 k-means 时,你是在要求 TensorFlow 根据参与者可能执行的不同动作将 50 个位置点的时间表示进行聚类:爬楼梯、站立、行走、谈话或工作。代码看起来与列表 8.6 相似,但在列表 8.9 中略有修改。
列表 8.9 在不同的活动组中聚类单个参与者文件
k = 5 ❶
with tf.Session() as sess:
tf.global_variables_initializer()
accel_data, X1 = get_dataset_segmented(sess, "./User Identification From
➥ Walking Activity/11.csv") ❷
centroids = initial_cluster_centroids(X1, k)
i, converged = 0, False
while not converged and i < max_iterations:
i += 1
Y1 = assign_cluster(X1, centroids)
centroids = sess.run(recompute_centroids(X1, Y1))
if i % 50 == 0:
print('iteration', i)
segments = sess.run(Y1)
print('Num segments ', str(len(segments))) ❸
for i in range(len(segments)):
seconds = (i * segment_size) / float(10)
seconds = accel_data[(i * segment_size)][0]
min, sec = divmod(seconds, 60)
time_str = '{}m {}s'.format(min, sec)
print(time_str, segments[i]) ❹
❶ 从论文中提取的爬楼梯、站立、行走、谈话和工作
❷ 只需要一个参与者。
❸ 打印分割数量——在这种情况下,112
❹ 遍历段并打印活动标签,0-4
你会想可视化运行列表 8.9 的结果。看吧,那些 112 个段似乎在位置 50 时间步段上有有趣的分布。左下角的 X 组似乎只有几个段。中间的三个组代表各种活动,看起来密度最大,而远在右下角的组比左下角的组有更多的位置段。你可以通过运行生成图 8.8 的代码来查看结果:
plt.scatter([X1[:, 0]], [X1[:, 1]], c=segments, s=50, alpha=0.5)
plt.plot([centroids[:, 0]], [centroids[:, 1]], 'kx', markersize=15)
plt.show()

图 8.8 基于提供的位置数据,单个参与者的不同活动
当然,挑战在于确切知道每个 X 对应于哪个组标签,但这项活动最好留到你可以检查每个运动组的属性,并可能尝试根据真实情况对它们进行特征化的时候。也许最接近左下角组的加速度变化量对应于逐渐加速,并且很可能是行走活动。只有通过单独分析,你才能弄清楚每个标签(0-4)对应于哪些类别——行走、跳跃、跑步等等——但机器学习和 TensorFlow 能够自动将这些位置数据划分为那些组,而不需要任何真实情况,这确实非常酷!
摘要
-
从你的手机位置数据中,一个使用无监督机器学习和 k-means 聚类的机器学习算法可以推断出你执行的活动类型。
-
TensorFlow 可以轻松计算 k-means,你可以在转换数据后应用第七章中的代码。
-
对位置数据的指纹识别类似于对文本的指纹识别。
-
准备和清理数据是机器学习中的关键步骤。
-
使用 k-means 聚类和分割现有参与者的数据,可以轻松地发现活动模式,这在更大的群体中要困难一些。
9 隐马尔可夫模型
本章涵盖
-
定义可解释模型
-
使用马尔可夫链建模数据
-
使用隐马尔可夫模型推断隐藏状态
如果火箭爆炸,可能有人会被解雇,因此火箭科学家和工程师必须能够对所有组件和配置做出自信的决定。他们通过物理模拟和从第一原理进行数学推导来实现这一点。您也通过纯粹的逻辑思维解决了科学问题。考虑波义耳定律:在固定温度下,气体的压力和体积成反比。您可以从这些简单的定律中得出关于已发现世界的深刻推论。最近,机器学习开始扮演演绎推理的重要配角。
火箭科学和机器学习并不是通常一起出现的短语,除非你真的走过我的一周。 (查看我的作者简介!)但如今,在航空航天行业中,使用智能数据驱动算法对现实世界的传感器读数进行建模更加可行。此外,机器学习技术在医疗保健和汽车行业中也得到了蓬勃发展。但为什么?
这种涌入部分可以归因于对可解释模型更好的理解,这些模型是机器学习模型,其中学习的参数具有明确的解释。例如,如果火箭爆炸,一个可解释的模型可能有助于追踪根本原因。
练习 9.1
使模型可解释的因素可能有些主观。您对可解释模型的评判标准是什么?
答案
我们喜欢将数学证明视为事实上的解释技术。如果有人要说服另一个人一个数学定理的真实性,一个无可辩驳地追踪推理步骤的证明就足够了。
本章是关于揭示观察背后的隐藏解释。考虑一个木偶大师拉动绳子使木偶看起来像是有生命的。仅分析木偶的动作可能会导致过于复杂的结论,关于一个非生命物体如何移动。在你注意到连接的绳子后,你会意识到木偶大师是解释逼真动作的最佳解释。
在这个话题上,本章介绍了隐马尔可夫模型(HMMs),它揭示了关于研究问题的直观属性。HMM 是“木偶大师”,它解释了观察结果。您通过使用第 9.2 节中描述的马尔可夫链来建模观察结果。
在详细阅读马尔可夫链和 HMMs 之前,考虑替代模型。在第 9.1 节中,你会看到可能不可解释的模型。
9.1 一个不太可解释的模型示例
黑盒机器学习算法的一个经典例子是图像分类,这种算法难以解释。在图像分类任务中,目标是给每个输入图像分配一个标签。更简单地说,图像分类通常被表述为一个多项选择题:列出的类别中哪一个最能描述图像?机器学习从业者在这方面的进步巨大,以至于今天的最佳图像分类器在某些数据集上与人类的表现相当。
在第十四章中,你将学习如何使用卷积神经网络(CNNs)来解决问题,这是一种学习大量参数的机器学习模型。但那些参数正是 CNN 的问题:如果不说成千上万,至少是数百万个参数分别代表什么意思?很难询问图像分类器它为什么会做出这样的决定。我们所能拥有的只是学习到的参数,这些参数可能无法轻易解释分类背后的推理。
机器学习有时会获得一个声誉,即它是一个黑盒工具,可以解决特定问题而不透露其结论是如何得出的。本章的目的是揭示机器学习中的一个具有可解释模型的领域。具体来说,你将了解 HMM(隐马尔可夫模型)并使用 TensorFlow 来实现它。
9.2 马尔可夫模型
安德烈·马尔可夫是一位俄罗斯数学家,他研究了在随机性的存在下系统随时间变化的方式。想象一下气体粒子在空气中弹跳。用牛顿物理学跟踪每个粒子的位置会变得非常复杂,因此引入随机性有助于简化物理模型。
马尔可夫意识到,进一步简化随机系统的是只考虑气体粒子周围的有限区域来对其进行建模。也许欧洲的气体粒子对美国的粒子几乎没有影响。那么为什么不去忽略它呢?当你只观察附近的邻域而不是整个系统时,数学就简化了。这种概念被称为马尔可夫性质。
考虑对天气进行建模。气象学家使用温度计、气压计和风速计等工具评估各种条件,以帮助预测天气。他们凭借卓越的洞察力和多年的经验来完成他们的工作。
让我们利用马尔可夫性质从一个简单的模型开始。首先,你确定你想要研究的可能情况,或者说状态。图 9.1 显示了三个天气状态作为图中的节点:Cloudy(多云)、Rainy(雨天)和 Sunny(晴天)。

图 9.1 以节点形式表示的天气条件(状态)
现在你有了状态,你想要定义一个状态如何转换到另一个状态。将天气建模为确定性系统是困难的。如果今天是晴天,明天肯定也是晴天的结论并不明显。相反,你可以引入随机性,并说如果今天是晴天,有 90%的可能性明天也是晴天,有 10%的可能性是多云。当你只使用今天的天气条件来预测明天的天气(而不是使用历史数据)时,马尔可夫性质就发挥作用了。
练习 9.2
一个仅基于其当前状态来决定采取何种行动的机器人被认为是遵循马尔可夫性质。这种决策过程的优点和缺点是什么?
答案
马尔可夫性质在计算上很容易处理,但马尔可夫模型无法推广到需要积累知识历史的情况。例如,在时间趋势很重要或了解多个过去状态能更好地预测下一个期望的情况中的模型。
图 9.2 展示了节点之间绘制的有向边,箭头指向下一个未来状态。每条边都有一个权重,表示概率(例如,如果今天是雨天,明天多云的概率为 30%)。两个节点之间没有边是一种优雅地表示那种转换概率几乎为零的方式。转移概率可以从历史数据中学习,但在此我们假设它们是给定的。

图 9.2 展示了不同天气条件之间的转移概率,用有向边表示。
如果你有三个状态,你可以用 3×3 矩阵来表示转移。矩阵中的每个元素(在第i行和第j列)对应于从节点i到节点j的边的概率。一般来说,如果你有N个状态,转移矩阵的大小将是N×N;参见图 9.4 的示例。
我们称这个系统为马尔可夫模型。随着时间的推移,状态会根据图 9.2 中定义的转移概率发生变化。在我们的例子中,晴天有 90%的可能性再次成为晴天,所以我们显示一个概率为 0.9 的边,回到自身。有 10%的可能性是晴天之后是多云,在图中表示为从晴天到多云的边 0.1。
图 9.3 是另一种可视化状态变化的方法,给出了状态转移概率。这种表示通常被称为梯形图,它最终证明是一个重要的工具,正如你将在我们实现 TensorFlow 算法时看到的。

图 9.3 展示了马尔可夫系统随时间变化的梯形表示
你已经看到了如何使用 TensorFlow 代码构建图来表示计算。你可能想将马尔可夫模型中的每个节点视为 TensorFlow 中的一个节点。但尽管图 9.2 和 9.3 很好地说明了状态转换,图 9.4 展示了在代码中实现它们的更有效方法。

图 9.4 一个转换矩阵传达了从左侧(行)到顶部(列)状态转换的概率。
记住,TensorFlow 图中的节点是张量,因此你可以将转换矩阵(让我们称其为 T)表示为 TensorFlow 中的一个节点。然后,你可以在 TensorFlow 节点上应用数学运算以实现有趣的结果。
假设你更喜欢晴天而不是雨天,因此你为每一天都有一个分数。你将每个状态的分数表示在一个名为 s 的 3 × 1 矩阵中。然后,使用 tf.matmul(T*s) 在 TensorFlow 中将这两个矩阵相乘,可以得到从每个状态转换的预期偏好。
在马尔可夫模型中表示一个场景可以极大地简化你对世界的看法。但通常,直接测量世界的状态很困难。通常,你必须使用来自多个观测的证据来弄清楚隐藏的意义。这正是 9.3 节旨在解决的问题。
9.3 隐藏马尔可夫模型
在 9.2 节中定义的马尔可夫模型在所有状态都是可观测时很方便,但情况并不总是如此。考虑只有获取一个城镇的温度读数。温度不是天气,但它与之相关。那么,你如何从这些间接的测量数据中推断出天气呢?
雨天天气很可能会造成温度读数较低,而晴天则很可能会造成温度读数较高。仅凭温度知识和转换概率,你仍然可以做出关于最可能天气的智能推断。
这种类型的问题在现实世界中很常见。一个状态可能会留下一些线索,而这些线索就是你所能拥有的全部。
这样的模型是 HMM,因为世界的真实状态(例如是否在下雨或晴天)是不可直接观测的。这些隐藏状态遵循马尔可夫模型,并且每个状态都会以一定的可能性发出可测量的观测值。例如,晴朗的隐藏状态通常可能会发出高温读数,但偶尔也可能因为某种原因发出低温读数。
在 HMM 中,你必须定义发射概率,这通常用一个称为 发射矩阵 的矩阵表示。矩阵的行数是状态的数量(晴朗、多云、雨天),列数是观测类型的数量(热、温和、冷)。矩阵中的每个元素都与发射相关的概率。
可视化 HMM 的典型方法是将观测值附加到梯形图上,如图 9.5 所示。

图 9.5 一个 HMM 框图,展示了天气条件可能产生的温度读数
所以,几乎就是这样。HMM 是对转移概率、发射概率以及另一件事的描述:初始概率。初始概率 是在模型没有先前知识的情况下每个状态发生的概率。如果你正在模拟洛杉矶的天气,那么晴朗的初始概率可能要大得多。或者让我们假设你正在模拟西雅图的天气;你知道可以将雨天的初始概率设置得更高。
HMM 允许你理解一系列观察结果。在这个天气建模场景中,你可能想知道观察特定温度读数序列的概率。我将使用前向算法来回答这个问题。
9.4 前向算法
前向算法计算观察到的概率。许多排列可能导致特定的观察结果,所以以天真方式枚举所有可能性将需要指数级的时间来计算。
相反,你可以通过使用 动态规划 来解决这个问题,这是一种将复杂问题分解成简单的小问题并使用查找表来缓存结果的策略。在你的代码中,你将查找表保存为 NumPy 数组,并将其馈送到 TensorFlow 操作以保持更新。
如列表 9.1 所示,创建一个 HMM 类来捕获 HMM 参数,包括初始概率向量、转移概率矩阵和发射概率矩阵。
列表 9.1 定义 HMM 类
import numpy as np ❶
import tensorflow as tf ❶
class HMM(object):
def __init__(self, initial_prob, trans_prob, obs_prob):
self.N = np.size(initial_prob) ❷
self.initial_prob = initial_prob ❷
self.trans_prob = trans_prob ❷
self.emission = tf.constant(obs_prob) ❷
assert self.initial_prob.shape == (self.N, 1) ❸
assert self.trans_prob.shape == (self.N, self.N) ❸
assert obs_prob.shape[0] == self.N ❸
self.obs_idx = tf.placeholder(tf.int32) ❹
self.fwd = tf.placeholder(tf.float64) ❹
❶ 导入所需的库
❷ 将参数存储为方法变量
❸ 双重检查所有矩阵的形状是否合理
❹ 定义前向算法使用的占位符
接下来,你将定义一个快速辅助函数来访问发射矩阵中的一行。列表 9.2 中的代码是一个辅助函数,它能够有效地从任意矩阵中获取数据。slice 函数从原始张量中提取一部分。此函数需要输入相关的张量、由张量指定的起始位置以及由张量指定的切片大小。
列表 9.2 创建一个辅助函数以访问观察的发射概率
def get_emission(self, obs_idx):
slice_location = [0, obs_idx] ❶
num_rows = tf.shape(self.emission)[0]
slice_shape = [num_rows, 1] ❷
return tf.slice(self.emission, slice_location, slice_shape) ❸
❶ 如何切割发射矩阵
❷ 切片的形状
❸ 执行切片操作
你需要定义两个 TensorFlow 操作。第一个,在列表 9.3 中,将只运行一次以初始化前向算法的缓存。
列表 9.3 初始化缓存
def forward_init_op(self):
obs_prob = self.get_emission(self.obs_idx)
fwd = tf.multiply(self.initial_prob, obs_prob)
return fwd
下一个操作在每次观察时更新缓存,如列表 9.4 所示。运行此代码通常被称为 执行前向步骤。尽管看起来这个 forward_op 函数没有输入,但它依赖于需要馈送到会话中的占位符变量。具体来说,self.fwd 和 self.obs_idx 是此函数的输入。
列表 9.4 更新缓存
def forward_op(self):
transitions = tf.matmul(self.fwd, tf.transpose(self.get_emission(self.obs_idx)))
weighted_transitions = transitions * self.trans_prob
fwd = tf.reduce_sum(weighted_transitions, 0)
return tf.reshape(fwd, tf.shape(self.fwd))
在HMM类之外,让我们定义一个函数来运行前向算法,如图 9.5 所示。前向算法为每个观察运行前向步骤。最后,它输出观察的概率。
列表 9.5 定义前向算法,给定HMM
def forward_algorithm(sess, hmm, observations):
fwd = sess.run(hmm.forward_init_op(), feed_dict={hmm.obs_idx:
➥ observations[0]})
for t in range(1, len(observations)):
fwd = sess.run(hmm.forward_op(), feed_dict={hmm.obs_idx:
➥ observations[t], hmm.fwd: fwd})
prob = sess.run(tf.reduce_sum(fwd))
return prob
在主函数中,让我们通过提供初始概率向量、转移概率矩阵和发射概率矩阵来设置HMM类。为了保持一致性,列表 9.6 中的示例直接从维基百科上的 HMM 文章mng.bz/8ztL中提取,如图 9.6 所示。

图 9.6 HMM 示例场景截图
通常,这三个概念定义如下:
-
初始概率向量 —状态开始的概率
-
转移概率矩阵 —给定当前状态,到达下一个状态的概率
-
发射概率矩阵 —观察到的状态暗示你感兴趣的状态已经发生的可能性
给定这些矩阵,您将调用您定义的前向算法(列表 9.6)。
列表 9.6 定义HMM和调用前向算法
if __name__ == '__main__':
initial_prob = np.array([[0.6],
[0.4]])
trans_prob = np.array([[0.7, 0.3],
[0.4, 0.6]])
obs_prob = np.array([[0.1, 0.4, 0.5],
[0.6, 0.3, 0.1]])
hmm = HMM(initial_prob=initial_prob, trans_prob=trans_prob,
➥ obs_prob=obs_prob)
observations = [0, 1, 1, 2, 1]
with tf.Session() as sess:
prob = forward_algorithm(sess, hmm, observations)
print('Probability of observing {} is {}'.format(observations, prob))
当您运行列表 9.6 时,算法输出以下内容:
Probability of observing [0, 1, 1, 2, 1] is 0.0045403
9.5 维特比解码
维特比解码算法在给定观察序列的情况下找到最可能的隐藏状态序列。它需要一个类似于前向算法的缓存方案。您将命名缓存为viterbi。在 HMM 构造函数中,添加列表 9.7 中显示的行。
列表 9.7 将维特比缓存作为成员变量添加
def __init__(self, initial_prob, trans_prob, obs_prob):
...
...
...
self.viterbi = tf.placeholder(tf.float64)
在列表 9.8 中,您将定义一个 TensorFlow 操作来更新viterbi缓存。这个操作将是HMM类中的一个方法。
列表 9.8 定义一个操作来更新前向缓存
def decode_op(self):
transitions = tf.matmul(self.viterbi, tf.transpose(self.get_emission(self.obs_idx)))
weighted_transitions = transitions * self.trans_prob
viterbi = tf.reduce_max(weighted_transitions, 0)
return tf.reshape(viterbi, tf.shape(self.viterbi))
您还需要一个操作来更新回溯指针(列表 9.9)。
列表 9.9 定义一个操作来更新回溯指针
def backpt_op(self):
back_transitions = tf.matmul(self.viterbi, np.ones((1, self.N)))
weighted_back_transitions = back_transitions * self.trans_prob
return tf.argmax(weighted_back_transitions, 0)
最后,在列表 9.10 中,定义维特比解码函数,位于 HMM 之外。
列表 9.10 定义维特比解码算法
def viterbi_decode(sess, hmm, observations):
viterbi = sess.run(hmm.forward_init_op(), feed_dict={hmm.obs: observations[0]})
backpts = np.ones((hmm.N, len(observations)), 'int32') * -1
for t in range(1, len(observations)):
viterbi, backpt = sess.run([hmm.decode_op(), hmm.backpt_op()],
feed_dict={hmm.obs: observations[t],
hmm.viterbi: viterbi})
backpts[:, t] = backpt
tokens = [viterbi[:, -1].argmax()]
for i in range(len(observations) - 1, 0, -1):
tokens.append(backpts[tokens[-1], i])
return tokens[::-1]
您可以在主函数中运行列表 9.11 中的代码来评估观察的维特比解码。
列表 9.11 运行维特比解码
seq = viterbi_decode(sess, hmm, observations)
print('Most likely hidden states are {}'.format(seq))
9.6 HMM 的应用
现在您已经实现了前向算法和维特比算法,让我们来看看您新获得的力量的一些有趣用途。
9.6.1 模型视频
想象一下仅根据一个人走路的方式(不是字面上的意思)来识别这个人。根据人的步态来识别人是一个相当酷的想法,但首先,你需要一个模型来识别步态。考虑一个 HMM,其中步态的隐藏状态序列为(1)休息位置,(2)右脚向前,(3)休息位置,(4)左脚向前,以及(5)休息位置。观察到的状态是从视频剪辑中提取的人行走/慢跑/跑步的轮廓。(此类示例的数据集可在mng.bz/Tqfx.找到)
9.6.2 模型 DNA
DNA 是一系列核苷酸,我们正在逐渐了解其结构。理解长 DNA 字符串的一个巧妙方法是对区域进行建模,如果你知道它们出现的概率顺序。就像雨天之后通常是多云的一天,也许 DNA 序列上的某个区域(起始密码子)在另一个区域(终止密码子)之前更常见。
9.6.3 建模图像
在手写识别中,我们的目标是从一个手写单词的图像中检索明文。一种方法是一次性解析字符,然后将结果连接起来。你可以利用字符是按序列书写的——单词——来构建一个 HMM。知道前一个字符可能有助于你排除下一个字符的可能性。隐藏状态是明文,观察结果是包含单个字符的裁剪图像。
9.7 HMM 的应用
当你知道隐藏状态是什么以及它们如何随时间变化时,HMM 工作得最好。幸运的是,在自然语言处理领域,使用 HMM 来标记句子的词性是可以解决的:
-
句子中的单词序列对应于 HMM 的观察结果。例如,句子“打开 pod 舱门,HAL”有六个观察到的单词。
-
隐藏状态是词性:动词、名词、形容词等等。前例中观察到的单词open应该对应于隐藏状态verb。
-
转换概率可以由程序员设计或通过数据获得。这些概率代表了词性的规则。例如,两个动词连续出现的概率应该是低的。通过设置转换概率,你可以避免算法穷举所有可能性。
-
每个单词的发射概率可以从数据中获得。一个传统的词性标注数据集被称为 Moby;你可以在www.gutenberg.org/ebooks/3203找到它。
注意:你现在已经有能力设计自己的 HMM 实验了。这些模型是强大的工具,我敦促你尝试在自己的数据上使用它们。预先定义一些转换和发射,看看你是否能恢复隐藏状态。我希望这一章能帮助你开始。
摘要
-
一个复杂、纠缠的系统可以用马尔可夫模型简化。
-
HMM 在现实世界应用中特别有用,因为大多数观察都是对隐藏状态的测量。
-
前向算法和维特比算法是用于 HMM 的最常见算法之一。
第十部分:词性标注和词义消歧
本章涵盖
-
通过从过去的数据中预测名词、动词和形容词来消歧语言
-
使用隐马尔可夫模型(HMMs)做出决策并解释它们
-
使用 TensorFlow 来建模可解释的问题并收集证据
-
从现有数据计算 HMM 的初始、转移和发射概率
-
从你自己的数据和更大的语料库中创建词性(PoS)标注器
你每天都在使用语言与他人沟通,如果你像我一样,有时你会挠头,尤其是如果你在使用英语。众所周知,英语有很多例外,这使得非母语者难以教授,包括那些正在努力自学的小家伙们。上下文很重要。在对话中,你可以使用诸如手势、面部表情和长暂停等工具来传达额外的上下文或意义,但当你阅读书面语言时,大部分上下文都缺失了,而且有很多歧义。词性(PoS)可以帮助填补缺失的上下文,以消歧义并使文本中的词语有意义。词性(PoS)告诉你一个词是否被用作动作词(动词),是否指代一个对象(名词),是否描述一个名词(形容词),等等。
考虑图 10.1 中的两个句子。在第一个句子——“我希望能设计一个未来的火星漫游车!”中——有人表示他们想要帮助设计(建造)下一个火星漫游车。现实生活中说这句话的人肯定对火星和行星科学之外的事情不感兴趣。第二个句子——“我喜欢成为一名工程师,并在 NASA JPL 的地球科学部门工作!”是由另一个人说的,关于享受在喷气推进实验室(NASA 的一部分)工作,我在那里工作。第二个句子是由一个喜欢在 NASA 工作并参与地球科学项目的研究工程师说的。

图 10.1 两个需要消歧的句子
这里的问题是:两个句子都以不同的方式使用了单词engineer。第一个句子将engineer用作动词——一个动作——而第二个句子将engineer用作名词来指代他的角色。动词和名词在英语中是不同的词性,而决定是名词还是动词并不局限于英语;词性存在于许多语言中,并在文本中的单词和字符之间划分意义。但正如你所看到的,问题是单词及其(意义)——简称为词义——通常需要去歧义以提供你通常通过口头交谈或视觉线索所理解的上下文线索。记住,当你阅读文本时,你无法听到说话者声音的语调。你看不到视觉线索来判断这个人是在说他们想要建造一个去火星的东西(使用锤子的手势)还是说他们喜欢从事地球科学项目(使用手势指向他们周围的世界)。
好消息是,语言学者们长期以来一直在人文科学领域研究词性,主要通过阅读文学作品并产生有用的指导和规则,这些规则传达了单词可以采取的特定词性。多年来,这些学者们检查了许多文本并记录了他们所看到的内容。这些努力的例子之一是古腾堡计划(mng.bz/5pBB),它包含超过 200,000 个单词和数十种可能的词性,这些词性可以在英语的多种书面文本中出现。你可以把古腾堡语料库想象成一个包含 200,000 个英语单词的表格;每个单词是一行,并作为主键,列值是该单词可能采取的不同词性(如形容词或名词)。
尝试将古腾堡计划应用于图 10.1 中的工程师句子,歧义就会跃然纸上。我已经用古腾堡的词性标注来注释这些句子以供说明(为了简洁,省略了第一句中的地球科学部分):
I<Noun/> love<Adverb/> being<Verb/> an engineer<Noun/><Verb/>
and<Conjunction/> working at<Preposition/> NASA<Noun/> JPL<Noun/>.
I<Noun/> am<Adverb/> hoping<Verb/> to engineer<Noun/><Verb/> a future
Mars<Noun/> rover<Noun/> vehicle<Noun/>.
如您所清晰看到的,古腾堡告诉我们engineer可以是一个名词或动词。你如何通过观察文本并利用周围语境或阅读大量类似文本,自动地有信心地判断一个特定单词的词性,而不是猜测呢?
机器学习和概率模型,如第九章中你所学到的隐藏马尔可夫模型(HMMs),可以通过将词性标注(PoS)的过程建模为 HMM 问题来帮助你填补一些上下文。作为一个极其初步的草图,初始概率是基于研究一组输入句子而得到的,一个特定 PoS 类发生的概率。转移概率是在特定顺序中看到某个 PoS 类在另一个或一系列类之后发生的概率。最后,发射或可观察属性是基于所有其他句子程序所看到的,一个模糊类如名词/动词是名词还是名词/动词是动词的概率。
好消息是,第九章中所有的代码都可以在这里重用。你在这章中付出的巨大努力将为机器学习准备数据,计算模型,并设置 TensorFlow 以执行其任务!让我们开始吧。在我们跳入词性标注之前,我会快速回顾一下 HMM 正在做什么,使用第九章中的 Rainy 或 Sunny 示例。跟我来!
10.1 HMM 示例回顾:雨天或晴天
第九章中的 Rainy/Sunny 示例使用 HMM 来模拟天气的隐藏状态,如果你只能观察到间接活动,如走路、购物或清洁。要将代码作为 HMM 建模,你需要天气是雨天或晴天的初始概率——一个 2×1 矩阵。你需要天气是雨天然后是晴天的转移概率(反之亦然)——一个 2×2 矩阵。你还需要看到间接动作或发射概率的概率,即走路、购物或清洁对于隐藏状态 Rainy 和 Sunny 的概率——一个 3×2 矩阵。图 10.2 的左侧展示了这个问题的构建。

图 10.2 的左侧是 Rainy/Sunny 示例的 HMM 构建。右侧展示了 TensorFlow 和我们的 HMM 类如何接收一组观察到的状态并累积这些观察状态发生的概率。
在第九章中,我向你展示了如何在 Python 和 TensorFlow 中创建一个 HMM 类来表示初始、转移和发射概率的初始化,然后运行前向模型来计算事件序列发生的整体概率(例如走路、购物、购物、清洁),然后下一个事件是购物。这个计算在图 10.2 的右侧展示。TensorFlow 操作在 HMM 类的开发中表现出色,特别是 TensorFlow 的延迟评估和图构建过程。以下是方法:
-
HMM 类充当构建
tf.slice操作图的代理,这些操作根据提供的观察结果提取特定的发射概率,以及tf.matmul操作,这些操作累积前向模型概率(如图 10.2 中的步骤 1-2、2-3 和 3-4)。 -
算法从初始概率开始,根据图 10.2 右侧的行事件右侧的第一个观测索引乘以发射概率。
-
接下来,算法累积前向模型概率并准备迭代剩余的观测(图 10.2 中的步骤 4-6)。
对于每个观测,HMM 类运行前向模型以重复执行以下步骤:
-
使用
tf.slice根据观测索引切片出特定的发射概率集,并将其与前向模型相乘(图 10.2 中的步骤 1-3)。 -
根据乘以前向模型和转换概率,累积每个隐藏状态的前向模型概率(图 10.2 中的步骤 4)。
-
使用
tf.reduce_sum累积每个选择(雨天和晴天)基于观测和tf.transpose将 Rainy/Sunny 矩阵返回到原始前向模型结构(图 10.2 中的步骤 5-6)。
列表 10.1 创建了 HMM 类并运行前向模型。此列表对应于视觉步骤 1-6(图 10.2),你可以看到图 10.2 右侧的一系列变换展示了雨天和晴天的第一次迭代。
列表 10.1 HMM 类和前向模型
class HMM(object):
def __init__(self, initial_prob, trans_prob, obs_prob): ❶
self.N = np.size(initial_prob)
self.initial_prob = initial_prob
self.trans_prob = trans_prob
self.emission = tf.constant(obs_prob)
assert self.initial_prob.shape == (self.N, 1)
assert self.trans_prob.shape == (self.N, self.N)
assert obs_prob.shape[0] == self.N
self.obs_idx = tf.placeholder(tf.int32)
self.fwd = tf.placeholder(tf.float64)
def get_emission(self, obs_idx): ❷
slice_location = [0, obs_idx]
num_rows = tf.shape(self.emission)[0]
slice_shape = [num_rows, 1]
return tf.slice(self.emission, slice_location, slice_shape)
def forward_init_op(self): ❸
obs_prob = self.get_emission(self.obs_idx)
fwd = tf.multiply(self.initial_prob, obs_prob)
return fwd
def forward_op(self):
transitions = tf.matmul(self.fwd,
tf.transpose(self.get_emission(self.obs_idx)))
weighted_transitions = transitions * self.trans_prob ❹
fwd = tf.reduce_sum(weighted_transitions, 0)
return tf.reshape(fwd, tf.shape(self.fwd))
def forward_algorithm(sess, hmm, observations): ❺
fwd = sess.run(hmm.forward_init_op(), feed_dict={hmm.obs_idx:observations[0]})
for t in range(1, len(observations)):
fwd = sess.run(hmm.forward_op(), feed_dict={hmm.obs_idx:observations[t], hmm.fwd: fwd})
prob = sess.run(tf.reduce_sum(fwd))
return prob
❶ 使用初始概率、发射概率和转换概率初始化模型
❷ 使用 tf.slice 提取与观测索引对应的发射概率
在第一个观测上运行第一个前向操作并累积前向模型
在观测上累积发射概率和转换概率
在观测 2 及以后运行前向模型并累积概率
要使用前向模型,你创建初始概率、转换概率和发射概率,然后将它们输入到 HMM 类中,如列表 10.2 所示。程序输出是看到那些特定间接观测(或发射)以特定顺序出现的累积概率。
列表 10.2 使用 Rainy/Sunny 示例运行 HMM 类
initial_prob = np.array([[0.6],[0.4]]) ❶
trans_prob = np.array([[0.7, 0.3],
[0.4, 0.6]])
obs_prob = np.array([[0.1, 0.4, 0.5],
[0.6, 0.3, 0.1]])
hmm = HMM(initial_prob=initial_prob, trans_prob=trans_prob,
➥ obs_prob=obs_prob)
observations = [0, 1, 1, 2, 1] ❷
with tf.Session() as sess:
prob = forward_algorithm(sess, hmm, observations)
print('Probability of observing {} is {}'.format(observations, prob)) ❸
创建表示初始、转换和发射概率的初始 NumPy 数组
❷ 行走、购物、购物、清洁和购物观测
❸ 运行前向模型并打印这些事件发生的累积概率
回想第九章,维特比算法是 HMM 模型和类的一个简单特化,它通过回溯指针跟踪状态之间的转换,并累积给定一组观察到的状态之间的概率转换。维特比算法不是提供一组状态发生的累积概率,而是提供每个间接观察到的隐藏状态的最可能状态。换句话说,它为每个间接观察到的行走、购物、购物、清洁和购物的状态提供最可能的状态(雨天或晴天)。您将在本章后面直接使用维特比算法,因为您将想要预测给定歧义类的 PoS。具体来说,您将想知道如果给定歧义类名词/动词,PoS 是名词还是动词。
现在我们已经完成了 HMM 的复习,是时候组装我们的初始歧义 PoS 标签器了。
10.2 PoS 标注
PoS 标注可以推广到将输入句子作为输入,然后尝试区分词性的歧义问题。Moby 项目是最大的免费音标数据库之一,它是作为 Project Gutenberg 的一部分进行镜像的。Moby 项目包含文件 mobypos.txt,这是一个包含英语单词及其可以接受的 PoS 的数据库。该文件(可在mng.bz/6Ado获取)需要解析,因为它是一组以数据库中已知的单词开头的行,后面跟着一组标签(如\A\N),这些标签对应于词性——在这种情况下是形容词(\A)和名词(\N)。PoS 标签到 mobypos.txt 注释的映射在列表 10.3 中显示,您将在本章的其余部分重用此映射。单词在 mobypos.txt 中的形式如下:
word\PoSTag1\PoSTag2
下面是一个具体的例子:
abdominous\A
根据 mobypos.txt,英语单词abdominous作为形容词有一个单一的 PoS 用法。
列表 10.3 mobypos.txt 的 PoS 标签映射
pos_tags = { ❶
"N" : "Noun",
"p" : "Plural",
"h" : "Noun Phrase",
"V" : "Verb (usu participle)",
"t" : "Verb (transitive)",
"i" : "Verb (intransitive)",
"A" : "Adjective",
"v" : "Adverb",
"C" : "Conjunction",
"P" : "Preposition",
"!" : "Interjection",
"r" : "Pronoun",
"D" : "Definite Article",
"I" : "Indefinite Article",
"o" : "Nominative"
}
pos_headers = pos_tags.keys() ❷
pt_vals = [*pos_tags.values()] ❸
pt_vals += ["sent"] ❹
❶ Project Gutenberg 中定义的 PoS 标签作为 mobypos.txt 的一部分
❷ PoS 标签键
❸ PoS 标签值
❹ 添加句尾标记,因为您稍后会用它来计算初始概率。
您可以在 Project Gutenberg 的 PoS 数据库的“数据库图例”标题下方找到完整的文档,链接为mng.bz/oROd。
要开始使用 TensorFlow 和 HMM 来区分词义,您将需要一些单词。幸运的是,我有一群充满活力的小伙子和一位妻子,他们有很多需要区分的谚语和短语。在编写这本书的过程中,我听了他们说的某些话,并记下了一些供您参考。我希望那些有青少年孩子的读者会喜欢这些:
-
“浴室里有纸巾!”
-
“你用它来做什么?”
-
“我真的喜欢玩《堡垒之夜》,这是一款令人惊叹的游戏!”
-
“纸巾出来了,妈妈,我应该用它来做什么?”
-
“我们真的很感兴趣于生酮饮食,你知道如何最好地开始它吗?”
现在,我将向您展示如何开始使用 Project Gutenberg 和 mobypos.txt 来标记词性。第一步是创建一个函数来解析完整的 mobypos.txt 文件并将其加载到 Python 变量中。因为解析文件相当于逐行读取,构建单词及其词性类别的表格,所以列表 10.4 中的简单函数就足够了。该函数返回 pos_words,这是英语单词及其词性类别的数据库,以及 pos_tag_counts——词性标签及其总出现次数的摘要。
列表 10.4 将 mobypos.txt 文件解析到词性数据库中
import tqdm
def parse_pos_file(pos_file):
with open(pos_file, 'r') as pf:
ftext = pf.readlines() ❶
for line in tqdm(ftext): ❷
l_split = line.split('\\') ❸
word = l_split[0]
classes=[]
u_classes = l_split[1].strip()
for i in range(0, len(u_classes)):
if not u_classes[i] in pos_tags: ❹
print("Unknown pos tag: "+u_classes[i]+" from line "+line)
continue
classes.append(u_classes[i])
pos_words[word] = classes ❺
for c in classes: ❻
if c in pos_tag_counts: ❻
cnt = pos_tag_counts[c] ❻
cnt = cnt + 1 ❻
else: ❻
cnt = 1 ❻
pos_tag_counts[c] = cnt ❻
❶ 逐行读取 mobypos.txt
❷ 使用 TQDM 在解析每一行时打印进度
❸ 按照单词及其后面的类别之后的 '\ ' 符号分割行
❹ 有时,您可能希望忽略某些词性类别或它们是未知的,因此跳过它们。
❺ 将英语单词与其词性类别集合相连接
❻ 累积语料库中词性标签的总数
当您执行 parse_pos_file 函数并获得 pos_words 和 pos_tag_counts 时,您可以分析 Project Gutenberg 语料库中词性标签的分布,看看是否有任何让您印象深刻的内容。特别是,您会看到一些频繁出现的词性标签和许多不太频繁出现的词性标签,这些标签在标记句子文本时可能不会出现。列表 10.5 中的简单 Matplotlib 列表揭示了词性标签的分布(图 10.3)。
列表 10.5 Project Gutenberg 中词性标签的分布
pos_lists = sorted(pos_tag_counts.items()) ❶
plists = []
for i in range(0, len(pos_lists)):
t = pos_lists[i]
plists.append((pos_tags[t[0]], t[1])) ❷
x, y = zip(*plists) ❸
plt.xticks(rotation=90)
plt.xlabel("PoS Tag")
plt.ylabel("Count")
plt.title("Distribution of PoS tags in Gutenberg corpus")
plt.bar(x, y)
plt.show() ❹
❶ 按键排序,返回一个列表
❷ 创建一个包含(标签,计数)元组的列表
❸ 将一对列表解包成两个元组
❹ 可视化图表
注意:形容词、名词、名词短语、动词(通常简称为 usually —分词)、复数、动词(及物)和副词是语料库中最频繁出现的词性标签。

图 10.3 Project Gutenberg 语料库中词性标签的分布
您可能还想知道有多少百分比英语单词被分配了多个词性类别,这样您就可以大致了解需要消歧义的内容,至少根据 Project Gutenberg 的数据。列表 10.6 提供了一个可能让您感到惊讶的答案:大约 6.5%。
列表 10.6 计算具有多个词性类别的英语单词的百分比
pc_mc = 0
for w in pos_words:
if len(pos_words[w]) > 1: ❶
pc_mc = pc_mc +1 ❶
pct = (pc_mc * 1.) / (len(pos_words.keys()) * 1.) ❷
print("Percentage of words assigned to multiple classes: {:.0%}".format(pct))❸
❶ 计算具有多个词性类别的单词数量
❷ 将具有多个词性类别的单词除以总单词数
❸ 打印结果百分比
重要的是要注意,在这 6.5% 被分配了多个词性类别的单词中,许多类别包括语言中最频繁出现的单词。总的来说,需要词义消歧。您很快就会看到需要多少。不过,在我们到达这个主题之前,我想让您先看到整体情况。
10.2.1 整体情况:使用 HMM 训练和预测词性
值得退一步考虑 HMM 和 TensorFlow 为 PoS 标签器提供了什么。如我之前提到的,PoS 标签可以推广到试图在 PoS 标签器无法确定一个单词应该有名词、动词或其他 PoS 标签时,尝试消除歧义性词性的问题。
与你在本书中遇到的其它机器学习过程类似,创建 HMM 以预测无歧义 PoS 标签的过程涉及一系列训练步骤。在这些步骤中,你需要创建一个基于 TensorFlow 的 HMM 模型;反过来,你需要一种学习初始概率、转移概率和发射概率的方法。你为 HMM 提供一些文本作为输入(例如一组句子)。训练的第一部分涉及运行一个 PoS 标签器,该标签器将以模糊的方式对句子中的单词进行标记,就像我之前在 Project Gutenberg 中展示的那样。在运行 PoS 标签器并获得标注的模糊文本后,你可以请求人类反馈以消除标签器的输出歧义。这样做会产生一个包含三个并行语料库数据的训练集,你可以用它来构建你的 HMM 模型:
-
输入文本,如句子
-
带有模糊 PoS 标签的标注句子
-
基于人类反馈消除歧义的标注句子
给定这些语料库,HMM 模型的概率可以按以下方式计算:
-
构建转移计数的二元矩阵。
-
计算每个标签的转移概率。
-
计算发射概率。
你构建一个按 PoS 标签构建的转移计数的二元矩阵,或者一个计数成对 PoS 标签相互跟随次数的矩阵,反之亦然。矩阵的行是 PoS 标签列表,列也是 PoS 标签列表——因此称为二元(连续单元、单词或短语的成对)。表格可能看起来像图 10.4。

图 10.4 PoS 的转移计数示例二元矩阵
然后,你计算一个 PoS 标签在另一个标签之后出现的次数并将它们相加。转移概率是单元格值除以该特定行的值之和。然后,对于初始概率,你取图 10.4 中对应于句子结束标签(sent)的行的计算转移概率。这行的概率是完美的初始概率集合,因为该行中 PoS 标签出现的概率是它在句子开头或句子结束标签之后出现的概率,例如图 10.5 中突出显示的最后一行。

图 10.5 初始概率是句子结束/句子开头标签(sent)中 PoS 标签的计算转移概率。
最后,您通过计算歧义词性标签及其在歧义语料库中的出现次数与用户提供的消除歧义语料库中的标签之间的比率来计算发射概率。如果,在用户提供的消除歧义语料库中,一个单词应该被标注为动词七次,而词性歧义语料库将其标注为Noun/Verb三次,Adjective两次,Noun两次,那么当隐藏状态真正是Noun时,Noun/Verb的发射概率是 3 除以 7,即 43%,依此类推。
在此结构下,您可以构建您的初始、转换和发射矩阵,然后运行您的隐马尔可夫模型(HMM),使用列表 10.1 中的 TensorFlow 代码和维特比算法,该算法可以告诉您给定观察状态的实际隐藏状态。在这种情况下,隐藏状态是真实的词性标注(PoS),而观察到的标签是歧义类词性标注。图 10.6 总结了这次讨论,并指出了 TensorFlow 和 HMM 如何帮助您消除文本歧义。

图 10.6 基于 HMM 的词性标注器的训练和预测步骤。这两个步骤都需要输入文本(句子),词性标注器对它们进行歧义标注。在训练部分,人类为输入句子提供无歧义的词性以训练 HMM。在预测阶段,HMM 预测无歧义的词性。输入文本、带有歧义词性的标注文本和带有消除歧义词性的标注文本构成了构建 HMM 所需的三个语料库。
现在我已经介绍了机器学习问题的设置,您可以准备好编写函数来执行初始词性标注并生成歧义语料库。
10.2.2 生成歧义词性标注数据集
在自然语言处理领域,将文本输入并对其词性进行分析的过程称为形态分析。如果您考虑我孩子们之前说的句子,并对它们进行形态分析,您将得到开始词性标注所需的三个语料库中的前两个。请记住,形态分析的结果是歧义词性文本。
要进行此分析,您可以使用 Python 自然语言工具包(NLTK)库,它提供了方便的文本分析代码以供重用。word_tokenize函数将句子分解成单个单词。然后您可以将每个单词与列表 10.4 中计算的pos_words字典进行对比。对于每个歧义,您会得到一个具有多个词性标注的单词,因此您需要收集并输出它们作为输入单词的一部分。在列表 10.7 中,您将创建一个analyse函数,该函数执行形态分析步骤并返回歧义标注的句子。
列表 10.7 对输入句子进行形态分析
import nltk
from nltk.tokenize import word_tokenize ❶
def analyse(txt):
words = word_tokenize(txt) ❶
words_and_tags = []
for i in range(0, len(words)): ❷
w = words[i]
w_and_tag = w ❸
if w in pos_words: ❹
for c in pos_words[w]: ❹
w_and_tag = w_and_tag + "<" + pos_tags[c] + "/>" ❹
elif w in end_of_sent_punc:
w_and_tag = w_and_tag + "<sent/>" ❺
words_and_tags.append(w_and_tag)
return " ".join(words_and_tags) ❻
❶ 导入 NLTK 并对输入句子进行分词
❷ 遍历每个单词
❸ 创建单词及其词性标签的组合字符串
❹ 如果单词已知来自 Project Gutenberg 的词性标签,检查其可能的词性。
❺ 如果单词是句子结束标签,添加那个特殊的词性。
❻ 返回由空格连接的分析单词及其词性
运行列表 10.7 中的代码会产生一个 Python 字符串列表。您可以对每个字符串运行analyse来生成平行语料库的前两部分。对于语料库的第三部分,您需要一个专家来为您消歧词性标签并告诉您正确答案。对于少数句子,几乎可以轻易地识别出某人是否将单词作为名词、动词或其他词性类别。因为我为您收集了数据,所以我提供了您可以教授 TensorFlow 的消歧词性标签(在tagged_sample_sentences列表中)。稍后您将看到,拥有更多知识和标记数据是有帮助的,但现在您会做得很好。瞧,列表 10.8 将三个平行语料库合并在一起,并为您准备好了。在构建三个语料库之后,您可以使用 Python Pandas 库创建一个数据框,然后您将使用它来提取一个概率的 NumPy 数组。然后,在提取概率之后,列表 10.9 中的代码将帮助从每个语料库中提取初始、转移和发射概率,并将结果存储在数据框中。
列表 10.8 创建句子、分析和标记的平行语料库
sample_sentences = [
"There is tissue in the bathroom!",
"What do you want it for?",
"I really enjoy playing Fortnite, it's an amazing game!",
"The tissue is coming out mommy, what should I use it for?",
"We are really interested in the Keto diet, do you know the best way to start it?"
] ❶
sample_sentences_a = [analyse(s) for s in sample_sentences] ❷
tagged_sample_sentences = [
"There is<Verb (usu participle)/> tissue<Noun/> in<Preposition/> the<Definite Article/> bathroom<Noun/> !<sent/>",
"What do<Verb (transitive)/> you<Pronoun/> want<Verb (transitive)/> it<Noun/> for<Preposition/> ?<sent/>",
"I<Pronoun/> really<Adverb/> enjoy<Verb (transitive)/> playing Fortnite , it<Pronoun/> 's an<Definite Article/> amazing<Adjective/> game<Noun/> !<sent/>",
"The tissue<Noun/> is<Verb (usu participle)/> coming<Adjective/> out<Adverb/> mommy<Noun/> , what<Definite Article/> should<Verb (usu participle)/> I<Pronoun/> use<Verb (usu participle)/> it<Pronoun/> for<Preposition/> ?<sent/>",
"We are<Verb (usu participle)/> really<Adverb/> interested<Adjective/> in<Preposition/> the<Definite Article/> Keto diet<Noun/> , do<Verb (usu participle)/> you<Pronoun/> know<Verb (usu participle)/> the<Definite Article/> best<Adjective/> way<Noun/> to<Preposition/> start<Verb (usu participle)/> it<Pronoun/> ?<sent/>"
] ❸
❶ 我收集的输入句子
❷ 在句子上运行 analyse,并将分析数据存储在列表中
❸ 我提供的消歧词性(PoS)标签
列表 10.9 构建平行语料库的 Pandas 数据框
import pandas as pd
def build_pos_df(untagged, analyzed, tagged): ❶
pos_df = pd.DataFrame(columns=['Untagged', 'Analyzed', 'Tagged'])
for i in range(0, len(untagged)):
pos_df = pos_df.append({"Untagged":untagged[i], "Analyzed":analyzed[i], "Tagged": tagged[i]}, ignore_index=True) ❷
return pos_df
pos_df = build_pos_df(sample_sentences, sample_sentences_a,
➥ tagged_sample_sentences) ❸
❶ 创建一个用于构建数据框的函数
❷ 遍历五个句子中的每一个,并将三个平行语料库中的每个元素添加进去
❸ 返回数据框
使用 Pandas 数据框结构,您可以调用df.head并交互式地检查句子、它们的形态学分析标签和人类提供的标签,以跟踪您正在处理的数据。您可以轻松地切片出列或行来处理;对于您的目的来说,更好的是,您可以获取一个 NumPy 数组。您可以在图 10.7 中看到一个调用df.head的示例。

图 10.7 使用 Pandas 轻松查看您的三个平行语料库:未标记的原始句子、(形态学)分析的模糊语料库,以及用户提供的无歧义的标记语料库。
接下来,我将向您展示如何处理数据框并创建初始概率、转移概率和发射概率。然后您将应用 HMM 类来预测消歧的词性(PoS)类别。
10.3 用于构建 PoS 消歧的 HMM 算法
在 Pandas 数据框的条件下,计算发射、转换和初始概率矩阵证明是一项相当直接的活动。你可以从图 10.7 中的“分析”列等对应的列中切出,然后从中提取所有标签。实际上,从分析和标注的句子中提取词性标注是一个需要的实用函数。列表 10.10 中的compute_tags函数枚举了数据框分析或标注列中的所有句子,并按位置提取词性标注。
列表 10.10 从句子数据框中提取标签
def compute_tags(df_col):
all_tags = [] ❶
for row in df_col:
tags = []
tag = None
tag_list = None
for i in range(0, len(row)): ❷
if row[i] == "<":
tag = ""
elif i+1 < len(row) and row[i] == "/" and row[i+1] == ">": ❸
if i+2 < len(row) and row[i+2] == "<": ❹
if tag_list == None: ❹
tag_list = [] ❹
tag_list.append(tag) ❹
tag = None ❺
else:
if tag_list != None and len(tag_list) > 0:
tag_list.append(tag)
tags.append(tag_list)
tag_list = None
tag = None
else:
tags.append(tag)
tag = None
else:
if tag != None: ❻
tag = tag + row[i] ❻
all_tags.append(tags)
tags = None
tag = None
tag_list = None
return all_tags
a_all_tags = compute_tags(pos_df['Analyzed']) ❼
t_all_tags = compute_tags(pos_df['Tagged']) ❼
❶ 初始化要返回的标签列表——按遇到顺序排列的标签列表
❷ 枚举数据框中的句子,并对每个句子枚举其字符
❸ 捕获标签结束值并决定是否存在另一个标签或是否添加此标签
❹ 如果存在两个 <名词><动词> 标签(模糊),则添加两个标签。
❺ 捕获标签结束值并决定是否存在另一个标签或是否添加此标签
❻ 收集标签值
❼ 提取所有平行句子中的分析标签(a_all_tags)和人工标注标签(t_all_tags)
拥有 compute_tags 以及提取的分析标签(a_all_tags)和提取的人工标注标签(t_all_tags),你就可以开始计算转换概率矩阵了。你将使用 t_all_tags 来构建一个双词性标注句子出现次数的矩阵,如图 10.4 所示。Pandas 可以帮助你构建这样的出现矩阵。你已经从 Project Gutenberg(列表 10.3)获得了一组有效的词性标注标签(pt_vals变量)。如果你要打印该变量的值,你会看到类似以下的内容:
print([*pt_vals])
['Noun',
'Plural',
'Noun Phrase',
'Verb (usu participle)',
'Verb (transitive)',
'Verb (intransitive)',
'Adjective',
'Adverb',
'Conjunction',
'Preposition',
'Interjection',
'Pronoun',
'Definite Article',
'Indefinite Article',
'Nominative',
'sent']
你的词性标注出现次数矩阵是一个二元矩阵,其中列是一个标签。列FirstPOS,后跟pt_vals中的词性标注,构成了列,行与图 10.4 中显示的相同值。如果你使数据框按FirstPoS的值索引,你可以轻松地使用词性标注作为切片数据框的键,例如通过除以列的总和值。列表 10.11 构建了一个未初始化的(所有单元格的值为0)转换概率矩阵。
列表 10.11 计算转换概率矩阵
def build_trans(pt_vals):
trans_df = pd.DataFrame(columns=["FirstPoS"] + pt_vals) ❶
trans_df.set_index('FirstPoS') ❷
for i in range(0, len(pt_vals)):
pt_data = {}
pt_data["FirstPoS"] = pt_vals[i] ❷
for j in range(0, len(pt_vals)):
pt = pt_vals[j]
pt_data[pt] = 0 ❸
trans_df = trans_df.append(pt_data, ignore_index=True) ❸
return trans_df ❹
❶ 构建包含“FirstPoS”列的数据框,代表两个词性标注中的第一个
❷ 在词性标注(PoS)标签上创建数据框索引
❸ 初始化计数为零
❹ 返回转换概率矩阵数据框
当你拥有未初始化的数据框时,你需要统计你标记的句子语料库中 PoS 标签的出现次数。幸运的是,这些数据已经被捕获在t_all_tags中,这是你在第 10.10 列表中计算的。一个简单的算法可以从标记语料库中捕获 PoS 标签的计数,用于转换计数矩阵。你需要遍历所有句子中捕获的 PoS 标签,捕获共现的标签,然后计算它们的出现次数。两个特殊情况是第一个句子之后的句子开头(由一个句子结束标签 precede)和最后一个句子(以snt PoS 标签结束)。因为数据框是通过引用传递的,所以你可以更新其计数值(在指定数组 [row_idx, col_idx] 中的cnt变量)来填充单元格计数。第 10.12 列表中的compute_trans_matrix函数为你做了繁重的工作。
列表 10.12 计算标记语料库中 PoS 标签的出现次数
def compute_trans_matrix(t_df, tags):
for j in range(0, len(pos_df['Tagged'])): ❶
s = pos_df['Tagged'][j]
tt_idx = [] ❷
for i in range(0, len(tags[j])): ❸
tt_idx.append(tags[j][i]) ❹
if j > 0 and i == 0: ❺
row_idx = "sent" ❺
col_idx = tags[j][i]
cnt = t_df.loc[t_df.FirstPoS==row_idx, col_idx]
cnt = cnt + 1 ❻
t_df.loc[t_df.FirstPoS==row_idx, col_idx] = cnt
if len(tt_idx) == 2: ❼
row_idx = tt_idx[0]
col_idx = tt_idx[1]
cnt = t_df.loc[t_df.FirstPoS==row_idx, col_idx]
cnt = cnt + 1 ❼
t_df.loc[t_df.FirstPoS==row_idx, col_idx] = cnt
tt_idx.clear()
elif len(tt_idx) == 1 and tags[j][i] == "sent": ❽
row_idx = tags[j][i-1]
col_idx = tags[j][i]
cnt = t_df.loc[t_df.FirstPoS==row_idx, col_idx]
cnt = cnt + 1
t_df.loc[t_df.FirstPoS==row_idx, col_idx] = cnt
tt_idx.clear()
❶ 遍历 PoS 标记语料库数据框中的每个句子
❷ 将行和列索引(tt_idx)初始化为空列表。列索引将只有两个 PoS 标签。
❸ 遍历这个句子从标记语料库中的标签
❹ 将行和列索引的第一元素作为句子的当前 PoS 标签
❺ 对于第一个句子之后的任何句子和第一个标签,第一个元素总是前一个句子的句子结束标签。
❻ 在行索引和列索引处增加计数
❼ 如果我们有行和列索引的两个元素,我们就准备好在那里增加计数。
❽ 如果我们达到了列的句子结束 PoS 标签,我们就获取我们行索引的前一个标签并增加计数。
当你在矩阵中有共现 PoS 标签的计数时,你需要进行一些后处理,将它们转换为概率。你首先对 PoS 二元组计数矩阵中的每一行的计数进行求和,然后在该行的每个单元格计数上除以总和。这个过程计算了转换概率和初始概率。因为涉及很多步骤,我在图 10.8 中捕捉了关键部分,这是一个查看列表 10.13 的有用参考。
列表 10.13 后处理转换概率矩阵
compute_trans_matrix(trans_df, t_all_tags) ❶
just_trans_df = trans_df.drop(columns='FirstPoS') ❷
just_trans_df['sum'] = just_trans_df.sum(axis=1) ❸
just_trans_df.loc[just_trans_df['sum']==0., 'sum'] = .001 ❹
trans_prob_df = just_trans_df.loc[:,"Noun":"sent"].div(just_trans_df['sum'],
➥ axis=0) ❺
❶ 计算初始零计数转换概率矩阵
❷ 删除第一个 PoS 列,因为它不再需要(对应于图 10.8 步骤 3)
❸ 添加总和列——图 10.8 步骤 2——并对每行的计数进行水平求和
❹ 如果有零计数,避免除以零,而是除以一个非常小的数。
❺ 创建一个新的数据框,不包含总和列,并对所有有效的 PoS 标签(在名词和snt之间)在总和列的值上进行就地除法。
呼吁!这真是做了很多工作。现在你将处理发射概率。除了几个更多的算法之外,这个过程相当直接,这是你在运行 HMM 之前需要做的最后一件事。

图 10.8 计算操作步骤 1-4,在 Pandas 数据框上操作并将转换计数转换为转换概率和初始概率。
10.3.1 生成发射概率。
你可以通过创建一个发射概率的大矩阵来生成发射概率,类似于你在第 10.3 节中创建的矩阵。主要区别在于,你不仅列出每行的有效词性标记,还列出歧义类别——例如“名词/动词”、“名词/形容词/动词”和“动词/副词”这样的类别。看,HMM 的全部目的就是处理歧义。你不能直接观察到“名词”或“动词”的隐藏状态,所以你通过一些发射概率观察到模糊的类别,这些概率可以从构造中学习。发射概率计数的矩阵如下构建:
行是歧义类别,后面跟着来自标记语料库的有效词性标记。这些行是你观察到的发射变量,而不是隐藏状态。列是来自标记语料库的有效词性标记,对应于如果你能直接观察到它,隐藏的无歧义词性类别会是什么。
第一和第二标记的单元格值是歧义类别在形态分析句子语料库中出现的次数(对于第一标记)除以实际词性标记在标记语料库中出现的总次数。
因此,如果“名词/动词”在形态分析语料库中出现了四次,并在标记语料库中被明确标记为“动词”,那么你将标记语料库中“动词”出现的总次数,例如六次,得到 4/6,或 0.66。在标记语料库中,有六个“动词”的词性标记,其中四次在形态分析中词性标注器模糊地认为该词是“名词/动词”。
剩余的歧义类别可以以相同的方式进行计算。你将在列表 10.14 中创建一个build_emission函数,用0作为占位符创建初始发射矩阵;然后,像之前一样,你将创建一个函数来处理分析和标记语料库,并填写发射概率。
列表 10.14 构建初始发射计数矩阵。
def build_emission(pt_vals, a_all_tags):
emission_df = pd.DataFrame(columns=["FirstPoS"] + pt_vals) ❶
emission_df.set_index('FirstPoS') ❶
amb_classes = {} ❷
for r in a_all_tags: ❷
for t in r: ❷
if type(t) == list: ❷
am_class = str(t) ❷
if not am_class in amb_classes: ❷
amb_classes[am_class] = "yes" ❷
amb_classes_k = sorted(amb_classes.keys()) ❸
for ambck in amb_classes_k: ❹
em_data = {} ❹
em_data["FirstPoS"] = ambck ❹
for j in range(0, len(pt_vals)):
em = pt_vals[j]
em_data[em] = 0
emission_df = emission_df.append(em_data, ignore_index=True)
for i in range(0, len(pt_vals)): ❺
em_data = {}
em_data["FirstPoS"] = pt_vals[i]
for j in range(0, len(pt_vals)):
em = pt_vals[j]
em_data[em] = 0
emission_df = emission_df.append(em_data, ignore_index=True)
return (emission_df, amb_classes_k)
❶ 为数据框添加第一个词性(PoS)索引和对应于标记语料库有效词性标记的列值。
❷ 提取所有标记后,将它们收集为字典的键。
❸ 对收集到的歧义类别进行排序,这些是字典 amb_classes 中的键。
❹ 将歧义类别作为行添加到发射概率矩阵中。
❺ 添加剩余的词性类别作为行,尽管你只需要标记歧义类别,但你需要标记为 1。
给定build_emission的输出,即emission_df或发射数据框,以及确定的歧义类别,你可以执行前面讨论的简单算法,以填充列表 10.15 中看到的发射概率矩阵作为开始。为了回顾,你将计算在标记语料库中某个标签(如Verb)在形态分析语料库中的歧义次数,并将词性标签识别为Noun/Verb,因为模型不确定。这种情况发生的次数与标记语料库中Verb出现的所有次数的比例是看到歧义类别Noun/Verb的发射概率。列表 10.15 在词性歧义类别的矩阵中创建初始计数。
列表 10.15 构建发射计数矩阵
def compute_emission(e_df, t_tags, a_tags):
for j in range(0, len(t_tags)):
for i in range(0, len(t_tags[j])):
a_tag = a_tags[j][i] ❶
t_tag = t_tags[j][i] ❷
if type(a_tag) == list: ❸
a_tag_str = str(a_tag) ❸
row_idx = a_tag_str ❸
col_idx = t_tag ❸
cnt = e_df.loc[e_df.FirstPoS==row_idx, col_idx] ❸
cnt = cnt + 1 ❸
e_df.loc[e_df.FirstPoS==row_idx, col_idx] = cnt
else:
if a_tag != t_tag:
continue ❹
else:
row_idx = a_tag
col_idx = t_tag
cnt = e_df.loc[e_df.FirstPoS==row_idx, col_idx]
if (cnt < 1).bool():
cnt = cnt + 1
e_df.loc[e_df.FirstPoS==row_idx, col_idx] = cnt
else:
continue ❺
❶ 分析的歧义词性标签
❷ 人工标记语料库的词性标签
❸ 歧义类别将是列表类型,因为它们将具有多个可能的标签。将其转换为字符串,并使用它来索引 row_idx/col_idx 以更新其计数。
❹ 永远不会发生;标签在分析和标记语料库中不一致。
❺ 仅更新一次歧义词性类别的计数;否则,跳过更新。
将发射计数矩阵转换为发射概率矩阵,你再次进行一些最小化后处理。因为你需要确定标记语料库中标签的总数,所以编写一个辅助函数来计算它并将结果保存为标签名称:计数的字典是有意义的。列表 10.16 中的count_tagged函数执行此任务。该函数是通用的,可以从标记语料库或分析的语料库(稍后会有用)中进行计数。
列表 10.16 计算标记语料库中标签的计数
def count_tagged(tags):
tag_counts = {}
cnt = 0
for i in range(0, len(tags)):
row = tags[i] ❶
for t in row: ❷
if type(t) == list: ❷
for tt in t:
if tt in tag_counts:
cnt = tag_counts[tt]
cnt = cnt + 1
tag_counts[tt] = cnt
else:
tag_counts[tt] = 1
else: ❸
if t in tag_counts:
cnt = tag_counts[t]
cnt = cnt + 1
tag_counts[t] = cnt
else:
tag_counts[t] = 1
return tag_counts
❶ 获取一个句子的所有标签
❷ 遍历标签,如果是一个列表;那么它是一个歧义类别,并捕获其子标签计数
❸ 否则,汇总计数
经过另一项后处理,你就可以完成在数据框中计算发射概率。此处理(列表 10.17)在每个单元格中执行除法步骤,使用计算出的标签计数。
列表 10.17 将每个发射歧义类计数除以标记语料库的词性标签计数
def emission_div_by_tag_counts(emission_df, amb_classes_k, tag_counts):
for pt in pt_vals:
for ambck in amb_classes_k: ❶
row_idx = str(ambck)
col_idx = pt
if pt in tag_counts:
tcnt = tag_counts[pt] ❷
if tcnt > 0:
emission_df.loc[emission_df.FirstPoS==row_idx, col_idx] =
➥ emission_df.loc[emission_df.FirstPoS==row_idx,
➥ col_idx] / tcnt ❸
❶ 收集行索引(歧义类别)和列索引(词性标签)
❷ 获取特定词性标签的计数
❸ 将歧义类计数除以词性标记语料库的词性标签计数
现在你已经准备好完全计算发射概率数据框。在这个阶段,这个过程相当简单;你将你的处理步骤链接起来,如列表 10.18 所示。使用标记语料库中的词性标签作为列和从分析语料库中作为行的歧义类别构建初始零值发射数据框。然后你计算与标记语料库中类似歧义类别的发生次数,并计算这些计数的比例与在标记语料库中出现的无歧义词性类别的总次数。代码显示在列表 10.18 中。
列表 10.18 计算发射概率数据框
(emission_df, amb_classes_k) = build_emission(pt_vals, a_all_tags) ❶
compute_emission(emission_df, t_all_tags, a_all_tags) ❷
tag_counts = count_tagged(t_all_tags) ❸
emission_div_by_tag_counts(emission_df, amb_classes_k, tag_counts) ❹
just_emission_df = emission_df.drop(columns='FirstPoS') ❺
❶ 构建零值发射计数数据框
❷ 计算模糊类计数
❸ 计算标记语料库中 PoS 标签的总计数
❹ 将模糊类计数除以无歧义 PoS 类总计数并删除求和列
❺ 删除 FirstPoS 索引列
现在你已经计算了所有三个概率,你最终可以运行 HMM 了!我将在第 10.4 节中展示如何操作。
10.4 运行 HMM 并评估其输出
你已经编写了所有必要的代码(结果证明相当多)来准备你的三个并行语料库,并使用这些语料库作为 TensorFlow 中 HMM 的训练输入。现在是时候看到你劳动的成果了。回想一下,你现在有两个 Pandas 数据框:just_trans_df和just_emission_df。just_trans_df中与句子(sent)标签对应的行是初始概率,如前所述,因此你有了 HMM 模型所需的所有三个数据片段。
但正如你所记得的,TensorFlow 要发挥其魔力,你需要 NumPy 数组。好消息是,你可以通过方便的辅助函数.values轻松地从 Pandas 数据框中获取这些数组,该函数返回矩阵内部的值构成的 NumPy 数组。结合.astype('float64')函数,你就有了一个简单的方法来获取所需的三个 NumPy 数组。列表 10.19 中的代码为你处理了这个任务。唯一棘手的部分是将发射概率的值转置,以确保它是按 PoS 标签而不是模糊类索引的。(简而言之,你需要交换行和列。)
列表 10.19 获取 HMM 的 NumPy 数组
initial_prob = trans_prob_df.loc[15].values.astype('float64') ❶
initial_prob = initial_prob.reshape((len(initial_prob), 1)) ❶
trans_prob = trans_prob_df.values.astype('float64') ❷
obs_prob = just_emission_df.T.values.astype('float64') ❸
❶ 获取句子行值并返回一个(16, 1)大小的 NumPy 数组
❷ 获取转移概率作为一个(16, 16)大小的 NumPy 数组
❸ 通过将(36, 16)的 PoS 模糊类数组转置为(16, 36)的数组,并按 PoS 类索引来获取发射概率
列表 10.19 中的代码为你提供了三个大小为(16, 1)、(16, 16)和(16, 36)的 NumPy 数组,分别对应初始概率、转移概率和发射概率。有了这些数组,你可以使用你的 TensorFlow HMM 类并运行 Viterbi 算法来揭示隐藏状态,即实际的 PoS 标签,考虑到模糊性。
你需要编写的一个辅助函数是将句子简单地转换为它的 PoS 模糊类观察值的一种方法。你可以使用你制作的原始发射数据框来查找从形态学分析的语料库中特定观察到的 PoS 标签/模糊类索引,然后收集这些索引到一个列表中。你还需要一个反向函数,将预测的索引转换为相应的 PoS 标签。列表 10.20 为你处理这些任务。
列表 10.20 将句子转换为其 PoS 模糊类观察值
def sent_to_obs(emission_df, pos_df, a_all_tags, sent_num):
obs = []
sent = pos_df['Untagged'][sent_num] ❶
tags = a_all_tags[sent_num] ❶
for t in tags:
idx = str(t) ❷
obs.append(int(emission_df.loc[emission_df.FirstPoS==idx].index[0]))❷
return obs
def seq_to_pos(seq): ❸
tags = []
for s in seq:
tags.append(pt_vals[s]) ❹
return tags ❹
❶ 获取句子及其 PoS 标签/模糊类
❷ 从发射数据框中获取观察值的索引
❸ 接受一组预测的词性索引,并返回词性标签名称
❹ 计算词性标签名称,并将它们作为列表返回所有预测的观察结果
现在,你可以得到你的 HMM 并在你为我精心挑选的前五个随机句子上运行它。我选择了句子索引 3,首先以它的歧义分析形式展示,然后以它的标记消除歧义形式展示:
The tissue<Noun/><Verb (transitive)/> is<Verb (usu participle)/> coming<Adjective/><Noun/> out<Adverb/><Preposition/><Interjection/><Noun/><Verb (transitive)/><Verb (intransitive)/> mommy<Noun/> , what<Definite Article/><Adverb/><Pronoun/><Interjection/> should<Verb (usu participle)/> I<Pronoun/> use<Verb (usu participle)/><Verb (transitive)/><Noun/> it<Pronoun/><Noun/> for<Preposition/><Conjunction/> ?<sent/>
The tissue<Noun/> is<Verb (usu participle)/> coming<Adjective/> out<Adverb/> mommy<Noun/> , what<Definite Article/> should<Verb (usu participle)/> I<Pronoun/> use<Verb (usu participle)/> it<Pronoun/> for<Preposition/> ?<sent/>
不再拖延,你可以运行你的 TensorFlow HMM 类。在列表 10.21 中试一试。
列表 10.21 在你的平行语料库上运行 HMM
sent_index = 3 ❶
observations = sent_to_obs(emission_df, pos_df, a_all_tags, sent_index) ❷
hmm = HMM(initial_prob=initial_prob, trans_prob=trans_prob, obs_prob=obs_prob)❸
with tf.Session() as sess: ❸
seq = viterbi_decode(sess, hmm, observations) ❹
print('Most likely hidden states are {}'.format(seq)) ❹
print(seq_to_pos(seq)) ❺
❶ 形态分析平行语料库中第三句话的索引
❷ 将句子的词性歧义类别转换为从发射矩阵到观察索引,或[9, 23, 1, 3, 20, 4, 23, 31, 18, 14, 13, 35]
❸ 使用计算出的初始、发射和转移概率初始化 TensorFlow HMM 模型
❹ 运行维特比算法并预测最可能的隐藏状态
❺ 将预测的内部状态索引转换为词性标签
当你运行列表 10.21 时,你会得到一些有趣的结果:
Most likely hidden states are [0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
['Noun', 'Verb (usu participle)', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun']
如果你比较预测的输出,你会发现前两个标签被正确预测了,但之后,每个其他的隐藏状态都被预测为名词,这显然是错误的。代码为什么做出了那些错误的预测?答案归结为数据不足和算法做出自信预测的能力。这是机器学习的一个老问题:没有足够的数据,模型无法进行精细调整。模型可以做出预测,但还没有看到足够的例子来正确划分必要的词性标签类别。你该如何解决这个问题?
一种方法是将更多句子写下来,然后用像 Gutenberg 这样的词性标注器逐句处理。之后,我可以自己消除歧义类别。
激励数据收集
过去十年中,激励收集注释的过程已经展开。蒂姆·伯纳斯-李在他的著名 2001 年《科学美国人》关于语义网的科学文章中预测了这一点(www.scientificamerican.com/article/the-semantic-web),组织机构一直在尝试从用户那里众包有价值的注释。伯纳斯-李认为,拥有一个智能代理来处理你的日历,就像今天的 Siri 一样,将足以让普通网络用户为网页编写精心制作的 XML 注释,这个期望彻底失败了。后来,社交媒体公司通过提供一种让用户与亲戚、家庭成员和社会联系保持联系的服务来说服用户为网络内容提供注释。他们做得过火了,通过提供正确的激励,收集了一个惊人的社会语料库。在这种情况下,尽管我非常喜欢你们,但我没有时间收集超过几个词性标注。幸运的是,许多其他人已经做了这件事。继续阅读,了解如何使用他们的工作。
这种解决方案是可能的,尤其是在当前时代,孩子们在家的时间比以往任何时候都要多。但是,当有大量其他标记语料库来源时,为什么还要投入人力呢?其中一个来源是布朗语料库,它是 PNLTK 的一部分。
10.5 从布朗语料库获取更多训练数据
布朗语料库是 1961 年在布朗大学创建的第一个包含来自 500 多个信息来源(如新闻和社论)的百万词英语词汇电子语料库。语料库按体裁组织,并标注了词性标记和其他结构。您可以在www.nltk.org/book/ch02.html了解更多关于语料库的信息。
布朗语料库包含各种按体裁或章节组织的文本文章,其中包含标注的句子。例如,您可以从中提取第七章的 100 个句子及其相应的词性标记(列表 10.22)。一个注意事项是,并非所有语料库都使用相同的词性标记集。而不是使用您在本章中迄今为止看到的 Project Gutenberg 的词性标记集——16 个标记——布朗语料库使用的是通用标记集,这是一个由 Slav Petrov、Dipanjan Das 和 Ryan McDonald 在 2011 年的一篇论文中定义的 14 个词性标记集(arxiv.org/abs/1104.2086)。不过,我已经为您做了大量工作,将标记集之间的子集重叠映射并记录在列表 10.23 中。将来,您可以决定映射更多的重叠,但列表为您提供了这个过程的一个想法。
列表 10.22 从布朗语料库的 100 个句子中探索词性标记
import nltk ❶
nltk.download('brown') ❶
nltk.download('universal_tagset') ❶
from nltk.corpus import brown ❶
print(brown.tagged_sents('ch07', tagset='universal')) ❷
print(len(brown.tagged_sents('ch07', tagset='universal'))) ❸
❶ 导入布朗语料库及其在通用标记集中的词性标记
❷ 打印布朗语料库第七章的词性标记句子,以识别格式
❸ 打印第七章的句子数量(122)
列表 10.22 的输出值得一看,以了解布朗语料库的记录方式,因为您将像处理我的小例句集一样处理它并准备它在一个数据框中。输出是一系列列表;每个列表包含一个与单词及其关联的通用标记集中的词性标记相对应的元组。因为这些分配是无歧义的,您可以将其视为您从三个平行语料库集中提供的用户提供的标记语料库。通用标记集到 Gutenberg 标记集的映射在列表 10.23 中提供。
[[('Special', 'ADJ'), ('districts', 'NOUN'), ('in', 'ADP'), ('Rhode', 'NOUN'), ('island', 'NOUN'), ('.', '.')], [('It', 'PRON'), ('is', 'VERB'), ('not', 'ADV'), ('within', 'ADP'), ('the', 'DET'), ('scope', 'NOUN'), ('of', 'ADP'), ('this', 'DET'), ('report', 'NOUN'), ('to', 'PRT'), ('elaborate', 'VERB'), ('in', 'ADP'), ('any', 'DET'), ('great', 'ADJ'), ('detail', 'NOUN'), ('upon', 'ADP'), ('special', 'ADJ'), ('districts', 'NOUN'), ('in', 'ADP'), ('Rhode', 'NOUN'), ('Island', 'NOUN'), ('.', '.')], ...]
列表 10.23 通用标记集到 Project Gutenberg 标记集的映射
univ_tagset = { ❶
"ADJ" : "Adjective",
"ADP" : "Adposition",
"ADV" : "Adverb",
"CONJ" : "Conjunction",
"DET" : "Determiner",
"NOUN" : "Noun",
"NUM" : "Numeral",
"PRT" : "Particle",
"PRON" : "Pronoun",
"VERB" : "Verb",
"." : "Punctuation marks",
"X" : "Other"
}
univ_gutenberg_map = { ❷
"ADJ" : "A",
"ADV" : "v",
"CONJ" : "C",
"NOUN" : "N",
"PRON" : "r",
"VERB" : "V",
"." : "sent"
}
univ_gutenberg_map_r = {v: k for k, v in univ_gutenberg_map.items()} ❸
❶ 通用标记集中词性标记的标识符和全名集合
❷ 需要考虑从 Project Gutenberg 到通用标签集的重复标签映射,该标签集以通用标签集标识符为键
❸ 通过 Project Gutenberg 简短 PoS 标签标识符创建反向索引
通过 Project Gutenberg 和通用标签集之间的重叠映射,您有了标记语料库。但是,您需要一种方法来删除标签并返回原始句子,以便您可以在 Project Gutenberg 中运行它们并获取用于训练平行语料库的模糊句子。NLTK 提供了一个方便的untag函数,可以处理这个任务。对标记句子运行untag;它返回原始句子(不带元组)。因此,您有了原始句子和标记的注释语料库,但您需要更新您的形态分析器以处理 Project Gutenberg 和通用标签集之间的映射。
您在列表 10.7 中编写的方便的analyse函数需要在几个方面进行更新:
-
white_list变量 — 能够接收 Gutenberg 到通用标签集映射,并使用它将 Gutenberg PoS 模糊标签器映射到您的pos_words映射,以便您的pos_words映射表示相应的通用标签集。 -
tagged_sent变量 — 包含现有 PoS 标签的标记句子,来自 NLTK 的注释,用于确保您只考虑有对应 Gutenberg 标签的通用标签集真实标签。 -
map_tags变量 — 一些有效的通用标签集 PoS 标签在 Gutenberg 中没有对应项,因此我擅自为您进行了映射。例如,DET(代表限定词)在 Gutenberg 中没有很好的映射,所以我将其映射到CONJ(代表连词)。这个例子可能需要改进,但为了说明目的,它工作得很好。
列表 10.24 包含更新的analyse函数,该函数将处理并从 Brown 语料库创建所有三个平行语料库。
列表 10.24 更新 analyse 函数以从 Brown 语料库学习三个平行语料库
def analyse(txt, white_list=None, tagged_sent=None):
map_tags = { ❶
"ADP" : "ADJ",
"DET" : "CONJ",
"NUM" : "NOUN",
"PRT" : "CONJ"
}
words = word_tokenize(txt) ❷
words_and_tags = []
wl_keys = None
if white_list != None: ❸
wl_keys = white_list.keys()
white_list_r = {v: k for k, v in white_list.items()}
wlr_keys = white_list_r.keys() ❹
for i in range(0, len(words)):
w = words[i]
w_and_tag = w
if w in pos_words:
for c in pos_words[w]:
if wl_keys != None:
if c not in wl_keys: ❺
continue ❺
else:
if tagged_sent != None: ❻
if tagged_sent[i][1] in white_list_r: ❼
ttag = white_list_r[tagged_sent[i][1]]
if ttag != "sent":
if ttag in pos_words[w]:
w_and_tag += "<"+pos_tags[ttag]+"/>"
else:
w_and_tag += "<"+pos_tags[c]+"/>" ❽
else:
if tagged_sent[i][0] == ".":
w_and_tag += "<"+ttag+"/>"
break
else: ❾
mt = map_tags[tagged_sent[i][1]]
ttag = white_list_r[mt]
if ttag in pos_words[w]:
w_and_tag += "<"+pos_tags[ttag]+"/>"
else:
w_and_tag += "<"+pos_tags[c]+"/>"
break
else:
w_and_tag = w_and_tag + "<" + pos_tags[c] + "/>"
else:
w_and_tag = w_and_tag + "<" + pos_tags[c] + "/>"
elif w in end_of_sent_punc:
w_and_tag = w_and_tag + "<sent/>"
words_and_tags.append(w_and_tag)
return " ".join(words_and_tags)
❶ 将一些通用标签集中的标签重新映射到 Project Gutenberg 标签,这些标签不相等
❷ 使用 NLTK 将句子分词成单词
❸ 白名单是 Project Gutenberg 和通用标签集都认可的允许标签集。
❹ 通过标识符创建白名单的反向索引
❺ PoS 标签不在白名单中,因此跳过它。
❻ Tagged_sent 是来自此句子的真实集的实际标签集合。
❼ 如果标签在白名单中,则考虑它。
❽ 标记语料库的标签与标签集不一致,因此选择其在 Project Gutenberg 中对应标识符的 PoS 标签。
❾ 标记的注释与 Gutenberg 不对应。
现在你可以创建三个并行语料库。我从 132 个句子中抓取了前 100 个句子来自布朗语料库进行训练。这个过程可能计算量很大,因为这些句子是之前使用数据的 20 倍,并且有更多的标签用于更新你的 HMM 模型。在实践中,如果你将布朗语料库中各个章节的所有内容都输入进去,这种方法会扩展得更好,但这样你将得到一个真实世界的例子,而且不需要等待数小时才能运行。列表 10.25 设置了并行语料库并创建了新的用于训练的词性标注数据框。
列表 10.25 准备布朗语料库以进行训练
brown_train = brown.tagged_sents('ch07', tagset='universal')[0:100] first
➥ 100 sentences
brown_train_u = [" ".join(untag(brown_train[i])) for i in range(0,
➥ len(brown_train))]
brown_train_a = [analyse(" ".join(untag(brown_train[i])),
➥ white_list=univ_gutenberg_map_r) for i in range(0, len(brown_train))]
brown_train_t = [analyse(" ".join(untag(brown_train[i])),
➥ white_list=univ_gutenberg_map_r, tagged_sent=brown_train[i]) for i in
➥ range(0, len(brown_train))]
new_pos_df = build_pos_df(brown_train_u, brown_train_a, brown_train_t)
all_n_a_tags = compute_tags(new_pos_df['Analyzed'])
all_n_t_tags = compute_tags(new_pos_df['Tagged'])
为了提醒你在制作更好的词性消歧预测过程中的位置,回顾一下是值得的。你看到我的句子有限,没有足够的词性标签和标记语料库来学习。通过使用 NLTK 和像布朗语料库这样的数据集,它有数千个标记的句子和语料库,你开始从 100 个布朗句子学习并行语料库。
你不能直接使用句子;analyse函数需要更新以考虑布朗和其他语料库使用与 Project Gutenberg 语料库标签集不同的词性标注集的事实。我向你展示了如何创建一个映射来考虑这一事实,并确保 Gutenberg 词性标注器仅对句子进行形态分析,并输出存在于布朗标签中的标签。在列表 10.25 中,你使用这些信息回到你的三个并行语料库的词性标注数据框,并提取了分析和标记的语料库词性标签。你已经完成了图 10.9 的步骤 1 和 2。

图 10.9 使用真实世界数据集和词性标注(PoS)标签来创建和训练你的 TensorFlow HMM 进行词性消歧标注
要到达图 10.9 的步骤 3,你需要在对代表三个并行语料库的 Pandas 数据框上运行算法,生成转移、初始和发射矩阵。你在小的五句数据集上已经这样做过了,所以你可以使用那些现有的函数,再次在数据框上运行它们,为 HMM 做准备(列表 10.26)。
列表 10.26 生成转移和发射计数矩阵
n_trans_df = build_trans(pt_vals) ❶
compute_trans_matrix(n_trans_df, all_n_t_tags)
n_just_trans_df = n_trans_df.drop(columns='FirstPoS')
n_just_trans_df['sum'] = n_just_trans_df.sum(axis=1)
n_just_trans_df.loc[n_just_trans_df['sum']==0., 'sum'] = .001 ❷
n_trans_prob_df =
➥ n_just_trans_df.loc[:,"Noun":"sent"].div(n_just_trans_df['sum'], axis=0)
(n_emission_df, n_amb_classes_k) = build_emission(pt_vals, all_n_a_tags) ❸
compute_emission(n_emission_df, all_n_t_tags, all_n_a_tags)
n_tag_counts = count_tagged(all_n_t_tags)
emission_div_by_tag_counts(n_emission_df, n_amb_classes_k, n_tag_counts)
n_just_emission_df = n_emission_df.drop(columns='FirstPoS')
❶ 构建转移矩阵
❷ 避免除以零
❸ 构建发射矩阵
构建了转移和发射矩阵后,你提取 NumPy 数组并加载 HMM 模型。再次,你获取最后一个词性标签的位置。因为有 16 列,而数据框是通过 0 索引的,所以初始概率行是索引 15。然后从它们各自的数据框(列表 10.27)中提取转移和发射概率的值。
列表 10.27 生成 NumPy 数组
n_initial_prob = n_trans_prob_df.loc[15].values.astype('float64') ❶
n_initial_prob = n_initial_prob.reshape((len(n_initial_prob), 1)) ❶
n_trans_prob = n_trans_prob_df.values.astype('float64') ❷
n_obs_prob = n_just_emission_df.T.values.astype('float64') ❸
❶ 从形状为(16,1)的转移概率中的句子词性行中提取初始概率
❷ 提取形状为(16,16)的转移概率。
❸ 提取形状为(16, 50)的发射概率
接下来,在列表 10.28 中,我再次随机选择了一个句子。为了说明,我选择了索引为 3 的句子,但也可以从布朗语料库中任意选择。句子以原始形式、模糊分析形式和标记形式展示供您参考:
There are forty-seven special district governments in Rhode Island (excluding two regional school districts, four housing authorities, and the Kent County Water Authority).
There are<Verb (usu participle)/><Noun/> forty-seven<Noun/><Adjective/> special<Adjective/><Noun/> district<Noun/> governments in<Adverb/><Adjective/><Noun/> Rhode<Noun/> Island<Noun/> ( excluding two<Noun/> regional<Adjective/> school<Noun/> districts , four<Noun/> housing<Noun/> authorities , and<Conjunction/><Noun/> the<Adverb/> Kent<Noun/> County Water Authority ) .<sent/>
There are<Verb (usu participle)/> forty-seven<Noun/> special<Adjective/> district<Noun/> governments in<Adjective/> Rhode<Noun/> Island<Noun/> ( excluding two<Noun/> regional<Adjective/> school<Noun/> districts , four<Noun/> housing<Noun/> authorities , and<Conjunction/> the<Adverb/> Kent<Noun/> County Water Authority ) .
列表 10.28 中的代码将句子通过 TensorFlow HMM 词性歧义消除步骤运行。它看起来很熟悉,因为它基本上与列表 10.21 相同。
列表 10.28 选择一个句子并运行 HMM
sent_index = ❶
observations = sent_to_obs(n_emission_df, new_pos_df, all_n_a_tags,
➥ sent_index) ❷
hmm = HMM(initial_prob=n_initial_prob, trans_prob=n_trans_prob,
➥ obs_prob=n_obs_prob) ❸
with tf.Session() as sess: ❹
seq = viterbi_decode(sess, hmm, observations) ❹
print('Most likely hidden states are {}'.format(seq)) ❺
print(seq_to_pos(seq)) ❺
❶ 选择索引为 3 的句子
❷ 将模糊的词性标签转换为从索引[33, 23, 7, 34, 11, 34, 34, 34, 40, 34, 34, 34, 21, 41, 34, 49]的证据观察值
❸ 使用从布朗语料库学习到的初始、转移和发射概率创建 HMM
❹ 运行 TensorFlow 并做出预测以消除词性标签的歧义
❺ 输出预测的隐藏状态
这次运行 HMM 除了名词之外还生成了一些有效预测的元素,特别是两个更多的词性预测:
Most likely hidden states are [3, 0, 6, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0]
['Verb (usu participle)', 'Noun', 'Adjective', 'Noun', 'Adjective', 'Noun', 'Noun', 'Noun', 'Adjective', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun', 'Noun']
HMM 的挑战与任何机器学习模型中的挑战相同:你展示的例子越多,表示变量就能更好地代表甚至未预见的情况。因为名词往往围绕句子结构中的许多其他词性,它们将是选择最多或最可能猜测的词性。话虽如此,从布朗语料库中得到的我们的 HMM 似乎比我们的五句示例表现更好,后者代表了一小部分词性及其共现。这次有 50 个词性和歧义类别,而第一个例子中只有 36 个,所以你可以推断你已经看到了更多的歧义示例,并且训练了你的 TensorFlow 模型来识别它们。
在展示你的模型随着更多数据而改进方面,你可以做得更好。让我们来衡量一下!
10.6 定义词性标注的错误条和度量标准
计算你的词性标注器表现如何的一个简单方法是定义几个简单的度量标准。第一个度量标准是每行或每句的错误率,这归结为你标注器预测正确的标签数量。这个度量标准测量如下:

其中 TLp 是句子或行 L 的预测 PoS 标记,TLt 是句子或行 L 的实际标记 PoS 标记。该方程从可能的总标记数中取出预测正确的标记数。然后除以要预测的有效标记总数。这种类型的方程通常被称为包含方程,因为分子表示预测捕获了分母中有效标记的多少。每句错误率对于衡量算法的准确度很有用,并且你可以添加一个超参数,即每句可接受的阈值。在实验了布朗语料库之后,我确定阈值值为 .4,即 40%,是每句可接受的比率。算法认为,如果至少正确预测了 60% 的 PoS 标记,则句子是正确的。
你可以使用另一个指标来评估你的 PoS 标记器,该指标捕获所有句子的所有差异,然后计算与所有句子要预测的总 PoS 标记数相比的总差异数。这种技术可以为你的算法提供一个整体准确度评估。运行列表 10.29 会告诉你,你的 HMM 正确预测了总 100 个句子中的 73 个(73%),错误率阈值为 40%,并且总体而言,你的算法错误地识别了可能预测的 1,219 个 PoS 标记中的 254 个,从而得到总体准确度为 79%。还不错!你是对的:你的模型表现更好。
再次运行 TensorFlow 和你的 HMM,这次按照列表 10.29 中所示捕获这些指标。
列表 10.29 捕获和打印每句错误率、差异和总体准确度
with tf.Session() as sess: ❶
num_diffs_t = 0 ❷
num_tags_t = 0 ❷
line_diffs = [] ❸
for i in tqdm(range(0, len(brown_train))):
observations = sent_to_obs(n_emission_df, new_pos_df, all_n_a_tags, i)
seq = viterbi_decode(sess, hmm, observations) ❹
sq_pos = seq_to_pos(seq)
diff_list = intersection(sq_pos, all_n_t_tags[i]) ❺
line_diffs.append((min(len(diff_list),len(all_n_t_tags[i])) * 1.) /❺
➥ (len(all_n_t_tags[i]) * 1.)) ❺
num_diffs_t += min(len(diff_list), len(all_n_t_tags[i])) ❻
num_tags_t += len(all_n_t_tags[i]) ❼
p_l_error_rate = 0.4 ❽
num_right = len([df for df in line_diffs if df < p_l_error_rate]) ❾
print("Num Lines Correct(threshold=%f) %d" % (p_l_error_rate, num_right))
print("Accuracy {0:.0%}".format(num_right*1\. / 100.))
print("Total diffs", num_diffs_t) ❿
print("Num Tags Total", num_tags_t) ❿
print("Overall Accuracy {0:.0%}".format(1 - (num_diffs_t*1\. / ❿
num_tags_t*1.))) ❿
❶ 重新初始化 TensorFlow
❷ 定义你想要捕获的指标
❸ 记录每行/每句错误率结果
❹ 在布朗语料库中对 100 个句子运行 TensorFlow 和 HMM
❺ 计算每行错误率并将其添加到 line_diffs
❻ 保存 PoS 标记差异的数量
❼ 保存可能的 PoS 标记数量
❽ 定义每行的错误率阈值
❾ 根据错误率阈值计算正确预测的句子数量
❿ 打印总差异数、总可能的 PoS 标记数和总体准确度
你还可以做的一件事是可视化你的劳动成果,使用 Matplotlib 图形化地查看每行的错误率。在你开发了一个好的 PoS 标记器之后,查找某些句子上的任何特定趋势是值得的。列表 10.30 和图 10.10 显示了 Matplotlib Python 代码和可视化错误率的输出。
大约每 20 个句子评估一次图表,PoS 标签器的表现非常糟糕。但总体而言,性能很强。这种情况可能需要进一步评估。例如,您可以随机打乱布朗语料库中的句子,或者检查表现不佳的句子,以确定它们是否代表了您尚未很好地学习的 PoS 标签。但我将把那项分析留到以后。您在这章中一直跟着我,学习 HMMs 的实际应用,做得很好。继续前进!
列表 10.30 使用 Matplotlib 可视化每行错误率
l_d_lists = sorted([(i, line_diffs[i]) for i in range(0, len(line_diffs))])❶
d_lists = []
for i in range(0, len(l_d_lists)):
t = l_d_lists[i]
d_lists.append((t[0], t[1]))
x, y = zip(*d_lists) ❷
plt.xlabel("Line Number")
plt.ylabel("Error rate")
plt.title("Distribution of per sentence error rate")
plt.bar(x, y)
plt.show() ❸
❶ 按键排序,返回形式为(行号,错误率)的元组列表
❷ 将一对列表拆分为两个元组
❸ 显示图表

图 10.10 您的 PoS 歧义消解标签器的每行错误率
摘要
-
词义消歧在日常生活中发生,您可以使用机器学习和 TensorFlow 来执行它。
-
标签数据可用;您需要使用 HMMs 来设置机器学习问题的方法。
-
HMMs 是可解释的模型,它们累积概率证据,并根据证据所表示的可能状态来引导决策。
第三部分 神经网络范式
过去十年中,机器学习研究深受聪明人将注意力转向大脑及其工作方式的影响。随着庞大的地面计算能力和图形处理单元(GPU)的出现,它们通过数倍优化机器学习代码的速度和部署,使得既有的模型和计算上难以实证测试的新方法都变得广泛可用,通过云服务民主化,不再被大型的网络公司所独占。
结果表明,基于大脑如何思考、听、看和说话的建模,以及将这些模型轻松部署、共享、重新训练和适应,并使用它们,已经带来了许多进步,例如智能手机中的智能数字助手或可以基于你与它的对话来订购食物或切换到你最喜欢的节目的家庭助手设备。
这些机器学习模型被称为神经网络。神经网络是基于你的大脑建模的,它包含由连接的神经元组成的网络。最近发布的模型,如自动生成可信新闻文章、剧本、推文等,感觉就像是(r)革命的开始。
TensorFlow 优化了创建神经网络以及将它们部署和评估的过程。本书的这一部分将向你展示如何创建、训练和评估一些今天最常用的神经网络,用于触摸、看、说话和听。
11 概览自动编码器
本章涵盖
-
了解神经网络
-
设计自动编码器
-
使用自动编码器表示图像
你是否曾经听到有人哼唱一段旋律并识别出这首歌?这可能对你来说很容易,但我在音乐方面幽默地没有音感。哼唱是对歌曲的一种近似。更好的近似可能是唱歌。加入一些乐器,有时,翻唱的歌曲听起来与原版几乎无法区分。
在本章中,你将不是处理歌曲,而是近似函数。函数是输入和输出之间关系的一般概念。在机器学习中,你通常想要找到将输入与输出关联起来的函数。找到最佳可能的函数拟合是困难的,但近似函数则容易得多。
人工神经网络(ANNs)是机器学习中的一个模型,可以近似任何函数。正如你所学的,你的模型是一个函数,它根据你拥有的输入给出你想要的输出。在机器学习的术语中,给定训练数据,你想要构建一个神经网络模型,它能最好地近似可能生成数据的隐含函数——这个函数可能不会给出确切的答案,但足够好以至于有用。
到目前为止,你通过明确设计一个函数来生成模型,无论是线性的;多项式的;还是更复杂的,如 softmax 回归或隐藏马尔可夫模型(HMMs)。神经网络在选择正确的函数和相应的模型时提供了一定的灵活性。理论上,神经网络可以模拟通用类型的转换,在这种情况下,你不需要了解太多关于被模拟的函数。
在 11.1 节介绍了神经网络之后,你将学习如何使用自动编码器,它们可以将数据编码成更小、更快的表示(11.2 节)。
11.1 神经网络
如果你听说过神经网络,你可能见过节点和边缘以复杂网格连接的图表。这种可视化主要受到生物学的启发——特别是大脑中的神经元。它也是可视化函数(如 f(x) = w × x + b)的一个方便方法,如图 11.1 所示。

图 11.1 线性方程 f(x) = w × x + b 的图形表示。节点用圆圈表示,边用箭头表示。边上的值通常称为权重,它们对输入进行乘法操作。当两个箭头指向同一个节点时,它们作为输入的总和。
作为提醒,线性模型是一组线性函数,例如 f (x) = w × x + b,其中 (w, b) 是参数向量。学习算法会在 w 和 b 的值周围漂移,直到找到最佳匹配数据的组合。算法成功收敛后,它会找到最佳可能的线性函数来描述数据。
线性是一个好的起点,但现实世界并不总是那么完美。因此,我们深入探讨了 TensorFlow 的起源。本章是介绍一种称为人工神经网络(ANN)的模型,它可以近似任意函数(不仅仅是线性函数)。
练习 11.1
f (x) = |x| 是线性函数吗?
答案
不,它是两个在零点缝合在一起的线性函数,而不是一条直线。
要引入非线性概念,对每个神经元的输出应用一个非线性函数,称为激活函数,是有效的。最常用的三个激活函数是 sigmoid(sig)、双曲正切(tan)和一种称为ReLU(Rectifying Linear Unit)的斜坡函数,如图 11.2 所示。

图 11.2 使用非线性函数,如 sig、tan 和 ReLU,向你的模型引入非线性。
你不必过于担心在什么情况下哪种激活函数更好。答案仍然是一个活跃的研究课题。请随意尝试图 11.2 中显示的三个函数。通常,最佳选择是通过交叉验证来确定哪个函数在给定的数据集上给出最佳模型。你还记得第五章中的混淆矩阵吗?你测试哪个模型给出的假阳性或假阴性最少,或者最适合你需求的任何其他标准。
sigmoid 函数对你来说并不陌生。你可能记得,第五章和第六章中的逻辑回归分类器将这个 sigmoid 函数应用于线性函数 w × x + b。图 11.3 中的神经网络模型表示函数 f (x) = sig(w × x + b)。该函数是一个单输入、单输出的网络,其中 w 和 b 是该模型的参数。

图 11.3 将非线性函数,如 sigmoid,应用于节点的输出。
如果你有两个输入 (x1 和 x2),你可以修改你的神经网络,使其看起来像图 11.4 中的那样。给定训练数据和损失函数,要学习的参数是 w1、w2 和 b。当你试图建模数据时,函数有多个输入是很常见的。例如,图像分类将整个图像(像素逐像素)作为输入。

图 11.4 一个双输入网络将有三个参数(w 1、w 2 和 b)。指向同一节点的多条线表示求和。
自然地,你可以推广到任意数量的输入(x1, x2, ..., xn)。相应的神经网络表示函数 f(x1, ..., xn) = sig(wn × xn + ... + w1 × x1 + b),如图 11.5 所示。

图 11.5 输入维度可以是任意长的。例如,灰度图像中的每个像素都可以有一个相应的输入 x1。这个神经网络使用所有输入生成一个单一的输出数字,你可能用它来进行回归或分类。符号 w^T 表示你正在将 w(一个 n × 1 的向量)转置成一个 1 × n 的向量。这样,你可以正确地与 x(具有 n × 1 的维度)相乘。这种矩阵乘法也称为点积,它产生一个标量(一维)值。
到目前为止,你只处理了输入层和输出层。没有任何阻止你在中间任意添加神经元。既不作为输入也不作为输出的神经元称为隐藏神经元。这些神经元被隐藏在神经网络的输入和输出接口后面,因此没有人可以直接影响它们的值。隐藏层是任何未连接的隐藏神经元集合,如图 11.6 所示。添加更多的隐藏层可以大大提高网络的表达能力。

图 11.6 不与输入和输出都接口的节点称为隐藏神经元。隐藏层是一组未连接的隐藏单元。
只要激活函数是非线性的,至少有一个隐藏层的神经网络可以逼近任意函数。在线性模型中,无论学习到什么参数,函数都保持线性。另一方面,具有隐藏层的非线性神经网络模型足够灵活,可以近似表示任何函数。这是一个多么美好的时代!
TensorFlow 附带许多辅助函数,可以帮助你以高效的方式获得神经网络的参数。当你开始使用你的第一个神经网络架构:自动编码器时,你将在本章中看到如何调用这些工具。
11.2 自动编码器
自动编码器是一种神经网络,它试图学习使输出尽可能接近输入的参数。一个明显的方法是直接返回输入,如图 11.7 所示。

图 11.7 如果你想创建一个输入等于输出的网络,你可以连接相应的节点并将每个参数的权重设置为 1。
但自动编码器比这更有趣。它包含一个小的隐藏层!如果这个隐藏层的维度比输入小,那么隐藏层就是你的数据压缩,称为编码。
在现实世界中编码数据
有几种音频格式,但最流行的可能是 MP3,因为它相对较小的文件大小。你可能已经猜到,这种高效的存储方式是有代价的。生成 MP3 文件的算法将原始未压缩音频缩小到一个听起来与你耳朵相似但文件大小小得多的文件。但是它是有损的,这意味着你将无法从编码版本完全恢复原始未压缩音频。
类似地,在本章中,我们希望降低数据的维度,使其更易于处理,但并不一定需要创建一个完美的复制。
从隐藏层重建输入的过程称为解码。图 11.8 展示了自动编码器的夸张示例。

图 11.8 在这里,你为尝试重建其输入的网络引入了一个限制。数据将通过一个狭窄的通道,如图中的隐藏层所示。在这个例子中,隐藏层只有一个节点。这个网络试图将一个 n 维输入信号编码(和解码)为一维,这在实践中可能很困难。
编码是减少输入维度的绝佳方式。例如,如果你能将 256 × 256 的图像表示为 100 个隐藏节点,那么你已将每个数据项减少了数千倍。
练习 11.2
让 x 表示输入向量(x1, x2, ..., xn),让 y 表示输出向量(y1, y2, ..., yn)。最后,让 w 和 w’分别表示编码器和解码器的权重。训练这个神经网络的可能成本函数是什么?
答案
请参阅列表 11.3 中的损失函数。
使用面向对象编程风格实现自动编码器是有意义的。这样,你可以在其他应用中重用这个类,而不用担心紧密耦合的代码。按照列表 11.1 中的概述创建你的代码可以帮助你构建更深的架构,例如堆叠自动编码器,这在经验上已被证明表现更好。
提示:通常,在神经网络中,如果你有足够的数据来避免模型过拟合,增加更多的隐藏层似乎可以提高性能。
列表 11.1 Python 类架构
class Autoencoder:
def __init__(self, input_dim, hidden_dim): ❶
def train(self, data): ❷
def test(self, data): ❸
❶ 初始化变量
❷ 在数据集上训练
❸ 对一些新数据的测试
打开一个新的 Python 源文件,并将其命名为 autoencoder.py。这个文件将定义你将在单独的代码中使用的autoencoder类。
构造函数将设置所有 TensorFlow 变量、占位符、优化器和算子。任何不需要立即会话的东西都可以放在构造函数中。因为你正在处理两组权重和偏差(一组用于编码步骤,另一组用于解码步骤),你可以使用 TensorFlow 的tf.name作用域来区分变量的名称。
列表 11.2 展示了在命名范围内定义变量的一个示例。现在你可以无缝地保存和恢复这个变量,而不用担心名称冲突。
列表 11.2 使用名称范围
with tf.name_scope('encode'):
weights = tf.Variable(tf.random_normal([input_dim, hidden_dim],
➥ dtype=tf.float32), name='weights')
biases = tf.Variable(tf.zeros([hidden_dim]), name='biases')
接下来,实现构造函数,如列表 11.3 所示。
列表 11.3 autoencoder类
import tensorflow as tf
import numpy as np
class Autoencoder:
def __init__(self, input_dim, hidden_dim, epoch=250, learning_rate=0.001):
self.epoch = epoch ❶
self.learning_rate = learning_rate ❷
x = tf.placeholder(dtype=tf.float32, shape=[None, input_dim]) ❸
with tf.name_scope('encode'): ❹
weights = tf.Variable(tf.random_normal([input_dim, hidden_dim],
➥ dtype=tf.float32), name='weights')
biases = tf.Variable(tf.zeros([hidden_dim]), name='biases')
encoded = tf.nn.tanh(tf.matmul(x, weights) + biases)
with tf.name_scope('decode'): ❺
weights = tf.Variable(tf.random_normal([hidden_dim, input_dim],
➥ dtype=tf.float32), name='weights')
biases = tf.Variable(tf.zeros([input_dim]), name='biases')
decoded = tf.matmul(encoded, weights) + biases
self.x = x ❻
self.encoded = encoded ❻
self.decoded = decoded ❻
self.loss = tf.sqrt(tf.reduce_mean(tf.square(tf.subtract(self.x,
➥ self.decoded)))) ❼
self.train_op = tf.train.RMSPropOptimizer(self.learning_rate).minimize(self.loss) ❽
self.saver = tf.train.Saver() ❾
❶ 学习周期数
❷ 优化器的超参数
❸ 定义输入层数据集
❹ 在名称范围内定义权重和偏差,以便你能将它们与解码器的权重和偏差区分开来
❺ 解码器的权重和偏差定义在这个名称范围内。
❻ 这些将是方法变量。
❼ 定义重建成本
❽ 选择优化器
❾ 设置保存器以在学习过程中保存模型参数
现在,在列表 11.4 中,你将定义一个名为train的类方法,它将接收一个数据集并学习参数以最小化其损失。
列表 11.4 训练自动编码器
def train(self, data):
num_samples = len(data)
with tf.Session() as sess: ❶
sess.run(tf.global_variables_initializer()) ❶
for i in range(self.epoch): ❷
for j in range(num_samples): ❸
l, _ = sess.run([self.loss, self.train_op], ❸
➥ feed_dict={self.x: [data[j]]}) ❸
if i % 10 == 0: ❹
print('epoch {0}: loss = {1}'.format(i, l)) ❹
self.saver.save(sess, './model.ckpt') ❺
self.saver.save(sess, './model.ckpt') ❺
❶ 启动 TensorFlow 会话并初始化所有变量
❷ 遍历构造函数中定义的周期数
❸ 一次训练一个样本,在数据项上训练神经网络
❹ 每 10 个周期打印一次重建误差
❺ 将已学习的参数保存到文件
现在你有足够的代码来设计一个从任意数据中学习自动编码器的算法。在你开始使用这个类之前,创建一个额外的方法。如列表 11.5 所示,test方法让你可以在新数据上评估自动编码器。
列表 11.5 在数据上测试模型
def test(self, data):
with tf.Session() as sess:
self.saver.restore(sess, './model.ckpt') ❶
hidden, reconstructed = sess.run([self.encoded, self.decoded], feed_dict={self.x: data}) ❷
print('input', data)
print('compressed', hidden)
print('reconstructed', reconstructed)
return reconstructed
❶ 加载已学习的参数
❷ 重建输入
最后,创建一个新的 Python 源文件名为 main.py,并使用你的autoencoder类,如列表 11.6 所示。
列表 11.6 使用你的autoencoder类
from autoencoder import Autoencoder
from sklearn import datasets
hidden_dim = 1
data = datasets.load_iris().data
input_dim = len(data[0])
ae = Autoencoder(input_dim, hidden_dim)
ae.train(data)
ae.test([[8, 4, 6, 2]])
运行train函数将输出关于损失如何在各个时期减少的调试信息。test函数显示编码和解码过程的信息:
('input', [[8, 4, 6, 2]])
('compressed', array([[ 0.78238308]], dtype=float32))
('reconstructed', array([[ 6.87756062, 2.79838109, 6.25144577,
➥ 2.23120356]], dtype=float32))
注意,你可以将一个 4D 向量压缩成 1D,然后通过一些数据损失将其解码回 4D 向量。
11.3 批量训练
如果你没有时间压力,一次训练一个样本是训练网络最安全的方法。但如果你的网络训练时间过长,一个解决方案是同时使用多个数据输入进行训练,称为批量训练。
通常,随着批量大小的增加,算法速度会加快,但成功收敛的可能性会降低。批量大小的比较是一把双刃剑。在列表 11.7 中运用它。你稍后会使用那个辅助函数。
列表 11.7 批量辅助函数
def get_batch(X, size):
a = np.random.choice(len(X), size, replace=False)
return X[a]
要使用批量学习,你需要修改列表 11.4 中的train方法。批量版本,如列表 11.8 所示,为每个数据批次插入一个额外的内部循环。通常,批次数应该足够,以便在同一个时期内覆盖所有数据。
列表 11.8 批量学习
def train(self, data, batch_size=10):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(self.epoch):
for j in range(500): ❶
batch_data = get_batch(data, self.batch_size) ❷
l, _ = sess.run([self.loss, self.train_op],
➥ feed_dict={self.x: batch_data})
if i % 10 == 0:
print('epoch {0}: loss = {1}'.format(i, l))
self.saver.save(sess, './model.ckpt')
self.saver.save(sess, './model.ckpt')
❶ 遍历各种批次选择
❷ 在随机选择的批次上运行优化器
11.4 处理图像
大多数神经网络,如您的自动编码器,只接受 1D 输入。另一方面,图像的像素由行和列索引。此外,如果一个像素是彩色的,它具有红色、绿色和蓝色浓度的值,如图 11.9 所示。

图 11.9 一幅彩色图像由像素组成,每个像素包含红色、绿色和蓝色的值。
管理图像高维度的便捷方法涉及两个步骤:
-
将图像转换为灰度。将红色、绿色和蓝色的值合并到 像素强度 中,它是颜色值的加权平均值。
-
将图像重新排列成行主序。行主序将数组存储为更长的单维集合;您将数组的所有维度放在第一个维度的末尾,这使得您可以用一个数字而不是两个数字来索引图像。如果一个图像是 3 × 3 像素,您将其重新排列成图 11.10 所示的结构。

图 11.10 图像可以用行主序表示。这样,您可以将二维结构表示为一维结构。
您可以使用多种方式在 TensorFlow 中使用图像。如果您在硬盘上有一堆图片,您可以使用 TensorFlow 中的 SciPy 加载它们。列表 11.9 展示了如何以灰度模式加载图像,调整大小,并以行主序表示它。
列表 11.9 加载图像
from scipy.misc import imread, imresize
gray_image = imread(filepath, True) ❶
small_gray_image = imresize(gray_image, 1\. / 8.) ❷
x = small_gray_image.flatten() ❸
❶ 以灰度模式加载图像
❷ 将其调整到更小的尺寸
❸ 将其转换为 1D 结构
图像处理是一个充满活力的研究领域,因此数据集很容易获得,您可以使用这些数据集而不是使用自己有限的图像。例如,名为 CIFAR-10 的数据集包含 60,000 个标记的图像,每个图像大小为 32 × 32。
练习 11.3
您能列出其他在线图像数据集吗?在网上搜索更多。
答案
深度学习社区中最常用的数据集可能是 ImageNet (www.image-net.org)。您也可以在 deeplearning.net/datasets 找到一份很好的列表。
从 www.cs.toronto.edu/~kriz/cifar.html 下载 Python 数据集。将提取的 cifar-10-batches-py 文件夹放置在您的当前工作目录中。列表 11.10 来自 CIFAR-10 网页;将代码添加到名为 main_imgs.py 的新文件中。
列表 11.10 从提取的 CIFAR-10 数据集中读取
import pickle
def unpickle(file): ❶
fo = open(file, 'rb')
dict = pickle.load(fo, encoding='latin1')
fo.close()
return dict
❶ 读取 CIFAR-10 文件,返回加载的字典
您可以使用列表 11.10 中创建的 unpickle 函数读取数据集中的每个文件。CIFAR-10 数据集包含六个文件,每个文件以 data_batch_ 开头,后跟一个数字。每个文件包含关于图像数据和相应标签的信息。列表 11.11 展示了如何遍历所有文件并将数据集附加到内存中。
列表 11.11 将所有 CIFAR-10 文件读取到内存中
import numpy as np
names = unpickle('./cifar-10-batches-py/batches.meta')['label_names']
data, labels = [], []
for i in range(1, 6): ❶
filename = './cifar-10-batches-py/data_batch_' + str(i)
batch_data = unpickle(filename) ❷
if len(data) > 0:
data = np.vstack((data, batch_data['data'])) ❸
labels = np.hstack((labels, batch_data['labels'])) ❹
else:
data = batch_data['data']
labels = batch_data['labels']
❶ 遍历六个文件
❷ 加载文件以获取 Python 字典
❸ 数据样本的行代表每个样本,所以你垂直堆叠它。
❹ 标签是一维的,所以你水平堆叠它们。
每个图像都表示为一系列红色像素,接着是绿色像素,然后是蓝色像素。列表 11.12 创建了一个辅助函数,通过平均红色、绿色和蓝色值将图像转换为灰度。
注意:你可以用其他方法实现更逼真的灰度,但这种方法通过平均三个值来完成工作。人类对绿光的感知更敏感,所以在某些其他版本的灰度中,绿色值可能在平均中具有更高的权重。
列表 11.12 将 CIFAR-10 图像转换为灰度
def grayscale(a):
return a.reshape(a.shape[0], 3, 32, 32).mean(1).reshape(a.shape[0], -1)
data = grayscale(data)
最后,收集某一类别的所有图像,例如“马”。你将在所有马的图片上运行你的自动编码器,如列表 11.13 所示。
列表 11.13 设置自动编码器
from autoencoder import Autoencoder
x = np.matrix(data)
y = np.array(labels)
horse_indices = np.where(y == 7)[0] ❶
horse_x = x[horse_indices]
print(np.shape(horse_x)) ❷
input_dim = np.shape(horse_x)[1]
hidden_dim = 100
ae = Autoencoder(input_dim, hidden_dim)
ae.train(horse_x)
❶ 从用于索引数据数组 x 的索引集中选择马(标签 7)
❷ 大小为 (5000, 3072) 的矩阵,5,000 张图像和 32 × 32*3 个通道(R,G,B),或 3,072 个值
现在,你可以将类似于你的训练数据集的图像编码为 100 个数字。这个自动编码器模型是最简单的之一,所以很明显,编码将是损失性的。小心:运行此代码可能需要长达 10 分钟。输出将跟踪每 10 个周期的损失值:
epoch 0: loss = 99.8635025024
epoch 10: loss = 35.3869667053
epoch 20: loss = 15.9411172867
epoch 30: loss = 7.66391372681
epoch 40: loss = 1.39575612545
epoch 50: loss = 0.00389165547676
epoch 60: loss = 0.00203850422986
epoch 70: loss = 0.00186171964742
epoch 80: loss = 0.00231492402963
epoch 90: loss = 0.00166488380637
epoch 100: loss = 0.00172081717756
epoch 110: loss = 0.0018497039564
epoch 120: loss = 0.00220602494664
epoch 130: loss = 0.00179589167237
epoch 140: loss = 0.00122790911701
epoch 150: loss = 0.0027100709267
epoch 160: loss = 0.00213225837797
epoch 170: loss = 0.00215123943053
epoch 180: loss = 0.00148373935372
epoch 190: loss = 0.00171591725666
请参阅本书的网站 (mng.bz/nzpa) 或 GitHub 仓库 (mng.bz/v9m7) 以获取输出示例的完整示例。
11.5 自动编码器的应用
本章介绍了最直接的自动编码器类型,但其他变体也已被研究,每种都有其优点和应用。让我们看看几个例子:
-
一个 堆叠自动编码器 以与普通自动编码器相同的方式开始。它通过最小化重建误差来学习输入到较小隐藏层的编码。然后,将隐藏层作为新自动编码器的输入,该自动编码器试图将第一层隐藏神经元编码到一个更小的层(第二层隐藏神经元)。这个过程可以按需继续。通常,学习的编码权重被用作解决深度神经网络架构中的回归或分类问题的初始值。
-
一个 去噪自动编码器 接收的是经过噪声处理的输入而不是原始输入,并试图“去噪”它。成本函数不再用于最小化重建误差。现在你正在尝试最小化去噪图像与原始图像之间的误差。直觉是,即使照片上有划痕或标记,我们的人类大脑仍然可以理解照片。如果一台机器也能透过噪声输入来恢复原始数据,那么它可能对数据的理解更好。去噪模型已被证明能更好地捕捉图像的显著特征。
-
一种 变分自动编码器 可以直接根据隐藏变量生成新的自然图像。比如说,你将一张男人的照片编码为一个 100 维向量,然后将一张女人的照片编码为另一个 100 维向量。你可以取这两个向量的平均值,通过解码器处理,并生成一个合理的图像,代表一个介于男人和女人之间的形象。这种变分自动编码器的生成能力来源于一种称为 贝叶斯网络 的概率模型。它也是现代深度伪造和生成对抗网络中使用的某些技术之一。
摘要
-
当线性模型无法有效描述数据集时,神经网络是有用的。
-
自动编码器是一种无监督学习算法,试图重现其输入,并在这样做的同时揭示数据中的有趣结构。
-
通过展平和灰度化,图像可以轻松地作为输入馈送到神经网络中。
12 应用自动编码器:CIFAR-10 图像数据集
本章涵盖
-
导航和理解 CIFAR-10 图像数据集的结构
-
构建自动编码器模型以表示不同的 CIFAR-10 图像类别
-
将 CIFAR-10 自动编码器应用于图像分类
-
在 CIFAR-10 图像上实现堆叠和去噪自动编码器
自动编码器是学习任意函数的强大工具,这些函数可以将输入转换为输出,而不需要完整的规则集来完成。自动编码器得名于其功能:学习一个比其大小小得多的输入表示,这意味着使用更少的知识编码输入数据,然后解码内部表示以近似地回到原始输入。当输入是图像时,自动编码器有许多有用的应用。压缩是一个,例如使用隐藏层中的 100 个神经元,并以行顺序格式格式化你的 2D 图像输入(第十一章)。通过红、绿、蓝通道的平均值,自动编码器学习图像的表示,并能够将 32 × 32 × 3 高度 × 宽度 × 通道图像,或 3,072 个像素强度编码为 100 个数字,这是数据量减少了 30 倍。这压缩效果如何?尽管你在第十一章中训练了一个网络来展示这个用例,但你没有探索图像的最终学习表示,但你在本章中将会。
通过使用自动编码器的表示也可以进行分类。你可以在一个标记的训练数据集(如加拿大高级研究研究所的 CIFAR-10 数据)上训练一组马图像的自动编码器,然后比较自动编码器对马的表示——这些经过训练和加权的 100 个数字,在许多样本上训练——与自动编码器对另一个图像类别(如“青蛙”)的学习表示。表示将不同,你可以用它作为对图像类型进行分类(和聚类)的紧凑方式。此外,如果你可以用强大的表示来分类样本,你可以检测异常样本,从而在数据序列中检测到某些数据集的不同,或者执行异常检测。你将在本章中探索这些自动编码器的用途,本章重点介绍 CIFAR-10 数据集。
正如我在第十一章末尾所暗示的,存在不同类型的自动编码器,包括堆叠自动编码器,它使用多个隐藏层并支持用于分类的深度架构。此外,去噪自动编码器试图对输入(如图像)进行噪声处理,并查看网络是否仍然能够学习一个更鲁棒的表示,该表示对图像不完美具有弹性。你将在本章中探索这两个概念。
12.1 什么是 CIFAR-10?
CIFAR-10 的名字来源于其 10 个图像类别,包括automobile(汽车)、plane(飞机)和frog(青蛙)。CIFAR-10 是从大小为 32 × 32 和三通道 RGB 格式的 8000 万 Tiny Images 数据集中选出的标记图像的强大工具。每张图像代表 3,072 个全色像素和 1,024 个灰度像素。你可以在people.csail.mit.edu/torralba/publications/80millionImages.pdf了解更多关于 Tiny Images 的信息。
在深入研究自动编码器表示之前,详细探索 CIFAR-10 数据集是值得的。
CIFAR-10 被分为训练集和测试集,这是一个好的做法,正如我一直在向你宣扬的那样。遵循保留大约 80%的数据用于训练和 20%的数据用于测试的做法,该数据集由 60,000 张图像组成,分为 50,000 张训练图像和 10,000 张测试图像。每个图像类别——airplane(飞机)、automobile(汽车)、bird(鸟)、cat(猫)、deer(鹿)、dog(狗)、frog(青蛙)、horse(马)、ship(船)和truck(卡车)——在数据集中有 6,000 个代表性样本。训练集包括每个类别的 5,000 个随机样本,测试集包括每个 10 个类别的 1,000 个随机样本。数据以二进制文件格式存储在磁盘上,适用于 Python、Matlab 和 C 编程用户。50,000 张图像的训练数据集被分为五个批次,每个批次有 10,000 张图像,训练文件批次是一个包含 10,000 张图像的单个文件,图像顺序是随机的。图 12.1 显示了 CIFAR-10 及其 10 个类别的视觉表示,其中显示了随机选择的图像。
要转换为灰度还是不转换
你可能想知道为什么在第十章和第十一章中使用灰度图像而不是完整的 RGB 值。也许颜色属性是你希望你的神经网络学习的东西,那么为什么去掉 R(红色)、G(绿色)和 B(蓝色),并用灰度代替它们?答案在于降维。转换为灰度可以将所需的参数数量减少 3 倍,这有助于训练和学习,而不会牺牲太多。但这并不是说你永远不会想训练一个以颜色为特征的神经网络。当你试图在一张同时有蓝色和红色杯子的图片中找到黄色杯子时,你必须使用颜色。但在本章的工作中,自动编码器对三分之二的数据量就足够满意了,你的电脑 CPU 风扇也是如此。

图 12.1 CIFAR-10 图像类别的每个类别随机挑选的 10 个样本。
在第十一章中,你构建了一个autoencoder类,它接受一个 CIFAR-10 批次的horse类图像;然后它学习图像的表示,以便将每个 1,024 像素(灰度 32 × 32)的图像减少到 100 个数字。编码性能如何?你将在 12.1.1 节中探索这个主题,同时刷新你对autoencoder类的记忆。
12.1.1 评估你的 CIFAR-10 自动编码器
你在第十一章中构建的 CIFAR-10 autoencoder 类有一个大小为 100 个神经元的单个隐藏层,输入是一个 5000 × 1024 的训练图像向量,使用 tf.matmul 和大小为 1024 × 100 的隐藏层对输入进行编码,并使用大小为 100 × 1024 的输出层对隐藏层进行解码。autoencoder 类使用 1,000 个 epoch 和用户指定的批次大小进行训练,并且还提供了方法通过打印隐藏神经元的值或编码以及原始输入值的解码表示来测试这个过程。列表 12.1 显示了该类及其实用方法。
列表 12.1 autoencoder 类
def get_batch(X, size):
a = np.random.choice(len(X), size, replace=False) ❶
return X[a] ❶
class Autoencoder:
def __init__(self, input_dim, hidden_dim, epoch=1000, batch_size=50,
➥ learning_rate=0.001):
self.epoch = epoch
self.batch_size = batch_size
self.learning_rate = learning_rate
x = tf.placeholder(dtype=tf.float32, shape=[None, input_dim])
with tf.name_scope('encode'): ❷
weights = tf.Variable(tf.random_normal([input_dim, hidden_dim],
➥ dtype=tf.float32), name='weights')
biases = tf.Variable(tf.zeros([hidden_dim]), name='biases')
encoded = tf.nn.sigmoid(tf.matmul(x, weights) + biases) ❸
with tf.name_scope('decode'):
weights = tf.Variable(tf.random_normal([hidden_dim, input_dim],
➥ dtype=tf.float32), name='weights')
biases = tf.Variable(tf.zeros([input_dim]), name='biases')
decoded = tf.matmul(encoded, weights) + biases ❹
self.x = x
self.encoded = encoded
self.decoded = decoded
self.loss = tf.sqrt(tf.reduce_mean(tf.square(tf.subtract(self.x, ❺
➥ self.decoded)))) ❺
self.train_op = ❺
➥ tf.train.RMSPropOptimizer(self.learning_rate).minimize(self.loss)❺
self.saver = tf.train.Saver() ❻
def train(self, data):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(self.epoch):
for j in range(np.shape(data)[0] // self.batch_size): ❼
batch_data = get_batch(data, self.batch_size)
l, _ = sess.run([self.loss, self.train_op],
➥ feed_dict={self.x: batch_data})
if i % 10 == 0:
print('epoch {0}: loss = {1}'.format(i, l))
self.saver.save(sess, './model.ckpt')
self.saver.save(sess, './model.ckpt')
def test(self, data):
with tf.Session() as sess:
self.saver.restore(sess, './model.ckpt')
hidden, reconstructed = sess.run([self.encoded, self.decoded], ❽
➥ feed_dict={self.x: data}) ❽
print('input', data) ❽
print('compressed', hidden) ❽
print('reconstructed', reconstructed) ❽
return reconstructed
❶ 使用索引从 X 中获取随机选择的批次大小,由于 replace=False,只选择唯一样本
❷ 使用 tf.scope 构造重用权重和偏差进行编码步骤
❸ 创建大小为 input_dim 和 hidden_dim 的编码
❹ 使用学习到的权重将编码从 hidden_dim 解码回 input_dim
❺ 使用均方根误差(RMSE)作为损失函数和优化器
❻ 重用保存器来保存和恢复模型
❼ 通过 num_batches 或 floor(dataset size/ batch_size) 迭代每个 epoch 并进行训练
❽ 计算编码和解码层以及输出,并打印它们的值
虽然你在第十一章中运行了这个类,但你没有查看它的输入或解码步骤的输出,以检查它如何学习输入的表示。为此,你可以使用第十一章中的代码,该代码在 CIFAR-10 网站上被推荐作为一种读取存储在 Pickle 格式的 Python 格式数据的简单方法。Pickle 是一个用于紧凑二进制表示的 Python 库,它将 Python 对象序列化为字节流,然后提供方法将对象反序列化回从该字节流中激活的 Python 动态对象。你可以使用 unpickle 函数和相关数据集加载代码,例如 greyscale 函数,该函数通过取每个图像的 RGB 值的平均值,将 50,000 张训练的三通道 RGB 图像转换为单通道灰度图像。CIFAR-10 加载代码的其余部分遍历下载的五个 10,000 张图像的训练文件。这些文件是 Python pickle 格式;数据存储在具有键 data 和键 labels 的 Python 字典中,labels 的有效值(0-9)对应于 10 个图像类别。10 个图像类别的名称填充到从 Python pickle batches.meta 文件中读取的 names 变量中,包括值 automobile、bird 等等。每套 10,000 张图像和标签分别垂直和水平堆叠在两个 NumPy 数组上——data 的大小为 (50000,1024) 和 labels (5000, ),分别,并可供训练使用。列表 12.2 将 CIFAR-10 数据准备好用于 Autoencoder。
列表 12.2 为 Autoencoder 准备 CIFAR-10 数据
def unpickle(file): ❶
fo = open(file, 'rb')
dict = pickle.load(fo, encoding='latin1')
fo.close()
return dict ❷
def grayscale(a):
return a.reshape(a.shape[0], 3, 32, 32).mean(1).reshape(a.shape[0], -1)❸
names = unpickle('./cifar-10-batches-py/batches.meta')['label_names'] ❹
data, labels = [], []
for i in range(1, 6): ❺
filename = './cifar-10-batches-py/data_batch_' + str(i) ❺
batch_data = unpickle(filename) ❺
if len(data) > 0:
data = np.vstack((data, batch_data['data'])) ❻
labels = np.hstack((labels, batch_data['labels'])) ❻
else: ❻
data = batch_data['data'] ❻
labels = batch_data['labels'] ❻
data = grayscale(data) ❼
❶ 加载 CIFAR-10 批量 Pickle 文件。共有五个用于训练的文件和一个用于测试的文件,每个文件包含 10,000 张图像和标签。
❷ 使用 Pickle 加载返回的是 Python 字典,键为 data 和 labels。
❸ 将 3 通道 RGB 图像输入转换为 1 通道灰度图,通过平均 RGB 值实现
❹ 根据标签索引的每个标签索引命名的图像类别
❺ 迭代五个训练批次并反序列化数据
❻ 数据在第一次迭代后垂直堆叠为(50000,1024),标签在水平堆叠为(50000, )
❼ 在(50000,1024)的图像数据数组上应用灰度函数
在将 CIFAR-10 的图像数据加载到data数组中,相关命名数组中的labels以及names数组提供与标签值关联的图像类别后,你可以查看特定的图像类别,例如horse,就像在第十一章中做的那样。通过首先从标签数组中选择所有马标签的索引,然后你可以使用这些索引来索引数据数组中的标签ID=7(horse),然后你可以使用 Matplotlib(列表 12.3)显示从 50,000 张图像集中的一小部分马图像——每个类别训练中有 5,000 张,另外 10,000 张用于测试(每个类别 1,000 张)。
列表 12.3 选择 5,000 张马图像并显示它们
x = np.matrix(data) ❶
y = np.array(labels) ❷
horse_indices = np.where(y == 7)[0] ❸
horse_x = x[horse_indices] ❹
print('Some examples of horse images we will feed to the autoencoder for
➥ training')
plt.rcParams['figure.figsize'] = (10, 10) ❺
num_examples = 5 ❺
for i in range(num_examples): ❺
horse_img = np.reshape(horse_x[i, :], (32, 32)) ❻
plt.subplot(1, num_examples, i+1) ❻
plt.imshow(horse_img, cmap='Greys_r') ❻
plt.show() ❻
❶ 将数据转换为 NumPy 矩阵,以便稍后用于 Autoencoder
❷ 通过显式转换将标签转换为 NumPy 数组,以便可以使用 np.where
❸ 使用 np.where 选择马标签的索引(类别 ID=7)
❹ 使用马索引索引到大小为(5000,1024)的图像数据
❺ 设置 Matplotlib 以打印五张 10x10 大小的马图像
❻ 显示马图像
结果输出显示在图 12.2 中。现在你可以看到使用你的 autoencoder 学习到的 CIFAR-10 图像表示。

图 12.2 从 CIFAR-10 训练数据中的 5,000 个样本集中返回的前五张马图像
现在回顾列表 12.4,其中包含训练 100 神经元 autoencoder 以学习这些马图像表示所需的小段代码。你需要运行你的 TensorFlow 训练的 autoencoder。
列表 12.4 训练你的Autoencoder
input_dim = np.shape(horse_x)[1] ❶
hidden_dim = 100 ❷
ae = Autoencoder(input_dim, hidden_dim) ❷
ae.train(horse_x) ❷
❶ 输入维度大小为 1024(32 × 32)的灰度 CIFAR-10 图像。
❷ 使用隐藏神经元作为编码大小来训练 Autoencoder 以处理马图像。
接下来,我将向你展示如何使用你学习到的表示来评估你的编码过程做得有多好,以及它如何捕捉horse图像类别。
12.2 自编码器作为分类器
审查你完成构建 autoencoder 和准备 CIFAR-10 数据以加载的步骤可能是有益的。我将为你总结:
-
加载 CIFAR-10 的 50,000 个训练图像。每个图像类别有 5,000 个样本图像。图像及其相关标签从 Python 的 Pickle 二进制表示中读取。
-
每个 CIFAR-10 图像的大小为 32 × 32,具有三个通道,或红色、绿色和蓝色像素的一个值。您通过平均它们的三个值将三个通道转换为单通道灰度。
-
您创建了一个
autoencoder类,它有一个包含 100 个神经元的单个隐藏层,该层以 5,000 个具有 1,024 个灰度强度的像素的图像集作为输入,在编码步骤中将大小减少到 100 个值。编码步骤使用 TensorFlow,通过训练学习,并使用均方根误差(RMSE)作为损失函数及其相关的优化器来学习隐藏层编码的编码权重(We)和偏置(Be)。 -
自动编码器的解码部分也学习相关的权重(Wd)和偏置(Bd),并在
test函数中编码,您将在本节中使用它。您还将创建一个classify函数,该函数将展示您的网络管理的马图像的学得表示。
您的图像数据的学得表示是自动编码器中的编码隐藏层及其相关的学得权重。整个过程流程如图 12.3 所示。

图 12.3 整个 CIFAR-10 自动编码过程。大小为 32 × 32,具有三个通道的图像被重塑为 1,024 个灰度像素。
现在让我们看看关于图像学得编码的一些信息。您可以使用Autoencoder.test函数打印隐藏层的值及其重建输入的值。您可以在 1,000 个测试 CIFAR-10 马图像上尝试它,但首先,您必须加载图像(列表 12.5)。CIFAR-10 的测试数据以 Python Pickle 格式提供。加载它并将其转换为灰度应该很熟悉,选择horse类索引(类ID=7)和从 1,000 个图像的测试集中选择马图像也是如此。然后您将运行Autoencoder.test以了解自动编码器的性能如何。如您从列表 12.1 中可能记得的那样,test函数加载隐藏编码层,运行解码步骤,并显示原始输入值。
列表 12.5 加载测试 CIFAR-10 图像并评估Autoencoder
test_data = unpickle('./cifar-10-batches-py/test_batch') ❶
test_x = grayscale(test_data['data']) ❶
test_labels = np.array(test_data['labels']) ❶
test_horse_indices = np.where(test_labels==7)[0] ❷
test_horse = test_x[test_horse_indices] ❷
ae.test(test_horse) ❸
❶ 通过读取序列化字典中的‘data’和‘labels’键,加载了 CIFAR-10 的 10,000 个测试图像的 pickle 格式
❷ 选择马类 ID=7 的索引,并索引到图像数据数组中的 1,000 个马图像
❸ 运行自动编码器测试方法来评估自动编码器
运行列表 12.5 的输出如下:
input [[ 34\. 60.66666667 36.33333333 ... 5\. 3.66666667
5\. ]
[111.66666667 120\. 116\. ... 205.66666667 204.33333333
206\. ]
[ 48.33333333 66.66666667 86.66666667 ... 135.33333333 133.66666667
140\. ]
...
[ 29\. 43.33333333 58.66666667 ... 151\. 151.33333333
147.33333333]
[100.66666667 108.66666667 109.66666667 ... 143.33333333 128.66666667
85.33333333]
[ 75.33333333 104.66666667 106.33333333 ... 108.33333333 63.66666667
26.33333333]]
compressed [[0\. 1\. 1\. ... 1\. 1\. 1.]
[0\. 1\. 1\. ... 1\. 1\. 1.]
[1\. 1\. 1\. ... 1\. 1\. 1.]
...
[0\. 1\. 1\. ... 1\. 1\. 1.]
[0\. 1\. 1\. ... 1\. 1\. 1.]
[1\. 1\. 1\. ... 1\. 1\. 1.]]
reconstructed [[ 88.69392 93.134995 93.69954 ... 49.1538 53.48016
57.427284]
[104.61283 101.96864 102.228165 ... 159.11684 158.36711 159.06337 ]
[205.88844 204.99907 205.80833 ... 106.32716 107.85627 110.11922 ]
...
[ 77.93032 76.28443 76.910736 ... 139.74327 139.15686 138.78288 ]
[171.00897 170.47668 172.95482 ... 149.24767 148.32034 151.43066 ]
[185.52625 183.34001 182.78853 ... 125.33179 127.04839 129.3505 ]]
Python 和 NumPy 进行了可爱的总结:
-
1,000 个测试马图像中的每个图像包含 1,024 个图像像素,因此
input中的每一行是 1,024 个数字。 -
compressed层有 100 个神经元,开启(1.)或关闭(0.)。 -
reconstructed值是自动编码器解码步骤之后恢复的值。
快速扫描图像值的重建显示出了相当大的差异。原始输入图像中的前三个数字——34、60.66666667和36.33333333——被自动编码器重建为88.69392、93.134995和93.69954。在其他图像和其他像素中存在类似差异,但不要过于担心它们。自动编码器默认训练了仅 100 个 epoch,学习率为 0.001。正如我在前面的章节中解释的,超参数调整是一个活跃的研究领域。您可以调整这些超参数并获得更好的值。您甚至可以执行其他优化和技巧,这些技巧我将在卷积神经网络(CNN)和数据增强出现时教给您,但到目前为止,使用test方法是一种快速而简单的方法来评估自动编码器。一种更好的方法可能是将图像的学习表示与其原始图像进行可视化比较,并直观地比较它们匹配得有多好。我会向您展示如何做,但首先,我将演示另一种计算方法来评估自动编码器。
12.2.1 通过损失使用自动编码器作为分类器
将重建图像的值与原始输入值进行比较,显示出重建像素值与原始像素值之间的差异。这种差异可以通过在训练过程中引导自动编码器的损失函数轻松测量。您使用了 RMSE。RMSE 是模型生成数据与输入之间的差异,平方后,通过平均值降低到标量,然后取平方根。RMSE 是评估训练损失的一个很好的距离度量。更好的是,它是一个很好的分类指示,因为结果证明,属于特定图像类的图像与原始值相比,模型值产生的损失是相似的。
马的图像有特定的损失,飞机的图像有不同的损失,等等。您可以通过向autoencoder类添加一个简单的方法来测试这种直觉:classify。这个方法的工作将是计算horse类的损失函数,然后比较这些图像产生的损失与其他图像类别的损失,看看是否存在差异。列表 12.6 向autoencoder类添加了classify方法,并返回隐藏和重建层以供以后使用。
列表 12.6 Autoencoder.classify 比较不同类别的损失
def classify(self, data, labels):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer()) ❶
self.saver.restore(sess, './model.ckpt') ❶
hidden, reconstructed = sess.run([self.encoded, self.decoded],
➥ feed_dict={self.x: data}) ❷
reconstructed = reconstructed[0] ❷
loss = np.sqrt(np.mean(np.square(data - reconstructed), axis=1))❸
horse_indices = np.where(labels == 7)[0] ❹
not_horse_indices = np.where(labels != 7)[0] ❹
horse_loss = np.mean(loss[horse_indices]) ❹
not_horse_loss = np.mean(loss[not_horse_indices]) ❹
print('horse', horse_loss) ❺
print('not horse', not_horse_loss) ❺
return hidden
❶ 初始化并加载 TensorFlow 训练的模型
❷ 获取隐藏(编码)层及其重建,用于计算损失
❸ 使用 NumPy 计算所有图像的损失 RMSE
❹ 计算马的图像索引和所有其他类别的索引,并计算所有图像(马或非马)的平均损失值
❺ 打印出分类器的马损失和非马损失
下面的输出来自列表 12.6:
horse 63.19770728235271
not horse 61.771580430829474
尽管不是令人震惊的,但与其他图像类别相比,horse从图像到重构的损失值明显不同,并且具有统计学意义。所以尽管自动编码器可能没有足够的经验以少量损失重构图像,但它已经学会了足够多的马图像表示,能够从 CIFAR-10 中的其他九个图像类别中区分其结构。酷!
现在你可以查看一些图像重构的差异。一点 Matplotlib 就能走得很远。你可以将classify函数返回的隐藏层和一个小型的decode方法应用到你的自动编码器上,这将生成特定编码图像的重构,并将其从 1,024 像素重新转换为 32 × 32 的灰度图像。代码在列表 12.7 中。
列表 12.7 将编码图像转换回 CIFAR-10 的解码方法
def decode(self, encoding):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
self.saver.restore(sess, './model.ckpt') ❶
reconstructed = sess.run(self.decoded, feed_dict={self.encoded:❶
➥ encoding}) ❶
img = np.reshape(reconstructed, (32, 32)) ❷
return img ❷
❶ 加载 TensorFlow 和存储的自动编码器模型以生成图像重构
❷ 将重构的 1,024 像素数组重塑为 32 × 32 的灰度图像并返回
运行该代码后,你可以使用 Matplotlib(列表 12.8)生成 CIFAR-10 测试数据集中前 20 张马图像的一组重构,左侧是原始图像,右侧是自动编码器“看到”的图像。
列表 12.8 评估和可视化 CIFAR-10 重构马图像
plt.rcParams['figure.figsize'] = (100, 100) ❶
plt.figure() ❶
for i in range(20): ❷
plt.subplot(20, 2, i*2 + 1)
original_img = np.reshape(test_horse[i, :], (32, 32)) ❸
plt.imshow(original_img, cmap='Greys_r') ❸
plt.subplot(20, 2, i*2 + 2)
reconstructed_img = ae.decode([encodings_horse[i]]) ❹
plt.imshow(reconstructed_img, cmap='Greys_r') ❹
plt.show() ❺
❶ 设置 Matplotlib 图表为 100 × 100 的图区域(行)
❷ 遍历 CIFAR-10 的前 20 张测试马图像
❸ 对于每个 CIFAR-10 测试马图像,在列的左侧显示
❹ 对于每个 CIFAR-10 测试马图像,在列的右侧显示自动编码器重构
❺ 显示图表
图 12.4 显示了结果。

图 12.4 显示列表 12.8 输出的一部分,展示了前 20 张 CIFAR-10 测试马图像中的 3 张:左侧是原始的 CIFAR-10 测试马图像,右侧是自动编码器“看到”的图像
现在你已经有了评估你的自动编码器的数值和视觉方法,我将向你展示另一种类型的自动编码器:去噪自动编码器。
如果您仔细思考,检查图 12.4 右侧的自动编码器表示的结果是非常有趣的。基于右侧的一些表示,自动编码器显然对马的特征与背景以及物体的一般形状有一些基本理解。我通常将自动编码器表示想象成当你闭上眼睛尝试重新想象你很久以前看到的东西时你会想到的东西。有时,想象力可以让你相当好地记住最近未来,但你对遥远过去的许多表示看起来就像图 12.4 右侧的自动编码器生成的图像——基本特征、形状和与背景的区别,但绝不是完美的重建。如果一个自动编码器能够学会在记住图像时考虑不完美,它就能构建一个更健壮的模型。这就是降噪自动编码器发挥作用的地方。
12.3 降噪自动编码器
在现实生活中,我们人类用我们的思维去思考一段时间,以改善我们记住东西的方式。假设你正在试图记住和家人狗狗玩接球的样子,你努力回忆起它看起来像什么,或者它玫瑰色鼻子下的黑色毛斑。看到更多在类似区域有黑色毛斑的狗,或者有略微不同毛斑的狗,可能是不同颜色的,可能会触发你记忆中更好的重建。
其中一部分原因是我们的思维构建视觉模型,通过看到更多例子而变得更好。我将在第十四章中更详细地讨论这个话题,该章涵盖了 CNN,这是一种专注于描绘低级和高级图像特征的学习方法和网络架构。与你要回忆的那张图像极其相似的其他图像——比如一只老宠物——有助于你关注那张图像的重要特征。更多不同图像的例子也有帮助。带有噪声的图像——比如光、色调、对比度或其他可重复差异的变化——构建了一个更健壮的模型和原始图像的记忆。
从这个灵感出发,去噪自编码器通过将像素值通过高斯函数或随机屏蔽一些像素(通过关闭或打开原始图像中的像素)来引入一些噪声,从而构建一个更健壮、更鲁棒的网络学习图像的表示。你需要稍微修改自动编码器原始类来创建去噪自编码器,并在你的 CIFAR-10 数据上尝试它。列表 12.9 通过对获取批量数据的函数进行轻微修改来设置 Denoiser 类。你的 Denoiser 将维护输入的并行噪声版本,因此需要在批量函数和类的其余部分中考虑这一点。你将记住的大多数其他方法——train、test、classify 和 decode——与本章其他列表中的方法工作方式相同,除了 train,这在列表 12.11 中突出显示。
列表 12.9 去噪自编码器
def get_batch_n(X, Xn, size): ❶
a = np.random.choice(len(X), size, replace=False) ❶
return X[a], Xn[a] ❶
class Denoiser:
def __init__(self, input_dim, hidden_dim, epoch=10000, batch_size=50,
➥ learning_rate=0.001):
self.epoch = epoch
self.batch_size = batch_size
self.learning_rate = learning_rate
self.x = tf.placeholder(dtype=tf.float32, shape=[None, input_dim],
➥ name='x')
self.x_noised = tf.placeholder(dtype=tf.float32, shape=[None, ❷
➥ input_dim], name='x_noised') ❷
with tf.name_scope('encode'): ❸
self.weights1 = tf.Variable(tf.random_normal([input_dim,
➥ hidden_dim], dtype=tf.float32), name='weights')
self.biases1 = tf.Variable(tf.zeros([hidden_dim]), name='biases')
self.encoded = tf.nn.sigmoid(tf.matmul(self.x_noised,
➥ self.weights1) + self.biases1, name='encoded')
with tf.name_scope('decode'): ❹
weights = tf.Variable(tf.random_normal([hidden_dim, input_dim],
➥ dtype=tf.float32), name='weights')
biases = tf.Variable(tf.zeros([input_dim]), name='biases')
self.decoded = tf.matmul(self.encoded, weights) + biases
self.loss = tf.sqrt(tf.reduce_mean(tf.square(tf.subtract(self.x,
➥ self.decoded))))
self.train_op =
➥ tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss) ❺
self.saver = tf.train.Saver()
❶ 获取一批训练数据输入,以及其噪声版本
❷ 设置输入数据噪声版本的占位符
❸ 使用 sigmoid 函数创建权重、偏置和编码数据层
❹ 创建权重、偏置和解码数据层,用于发射学习到的表示
❺ 使用 AdamOptimizer 设置损失和训练操作
对于去噪器的设置中最大的不同之处在于存储一个并行 TensorFlow 占位符,用于存储输入数据的噪声版本,编码步骤使用这个版本而不是原始输入。你可以采取几种方法。例如,你可以使用高斯函数并采样随机像素,然后根据某些频率屏蔽输入像素。这些随机像素——正如你所猜想的——是机器学习建模过程中的另一个超参数。没有最佳方法,所以尝试不同的方法,并选择最适合你用例的方法。你的 Denoiser 将在列表 12.10 中实现两种噪声方法——高斯和随机屏蔽。
列表 12.10 为你的 Denoiser 添加噪声的方法
def add_noise(self, data, noise_type=’mask-0.2’):
if noise_type == 'gaussian': ❶
n = np.random.normal(0, 0.1, np.shape(data)) ❶
return data + n ❶
if 'mask' in noise_type: ❷
frac = float(noise_type.split('-')[1]) ❷
temp = np.copy(data) ❷
for i in temp: ❷
n = np.random.choice(len(i), round(frac * len(i)), replace=False)❷
i[n] = 0
return temp ❸
❶ 在高斯随机函数上为每个像素添加介于 0 和 0.1 之间的值
❷ 根据提供的百分比 frac 选择整体输入像素的一部分随机设置为值 0
❸ 返回噪声数据
使用噪声数据,train 方法使用 get_batch_n 方法,该方法返回用于训练的噪声和常规批量输入数据。此外,test、classify 和 decode 的其余方法没有变化,除了提供噪声数据,以便返回适当的张量。列表 12.11 通过省略 decode 函数(与列表 12.7 中的相同)来完成 Denoiser 类。
列表 12.11 Denoiser 类的其余部分
def train(self, data):
data_noised = self.add_noise(data) ❶
with tf.Session() as sess: ❷
sess.run(tf.global_variables_initializer())
for i in range(self.epoch):
for j in range(50):
batch_data, batch_data_noised = get_batch_n(data,
➥ data_noised, self.batch_size) ❸
l, _ = sess.run([self.loss, self.train_op],
➥ feed_dict={self.x: batch_data, self.x_noised:
➥ batch_data_noised})
if i % 10 == 0:
print('epoch {0}: loss = {1}'.format(i, l))
self.saver.save(sess, './model.ckpt')
epoch_time = int(time.time())
self.saver.save(sess, './model.ckpt')
def test(self, data):
with tf.Session() as sess:
self.saver.restore(sess, './model.ckpt')
data_noised = self.add_noise(data) ❹
hidden, reconstructed = sess.run([self.encoded, self.decoded],
➥ feed_dict={self.x: data, self.x_noised:data_noised})
print('input', data)
print('compressed', hidden)
print('reconstructed', reconstructed)
return reconstructed
def classify(self, data, labels):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
self.saver.restore(sess, './model.ckpt')
data_noised = self.add_noise(data)
hidden, reconstructed = sess.run([self.encoded, self.decoded],
➥ feed_dict={self.x: data, self.x_noised:data_noised})
reconstructed = reconstructed[0]
print('reconstructed', np.shape(reconstructed))
loss = np.sqrt(np.mean(np.square(data - reconstructed), axis=1))
print('loss', np.shape(loss))
horse_indices = np.where(labels == 7)[0]
not_horse_indices = np.where(labels != 7)[0]
horse_loss = np.mean(loss[horse_indices])
not_horse_loss = np.mean(loss[not_horse_indices])
print('horse', horse_loss)
print('not horse', not_horse_loss)
return hidden
❶ 使用 add_noise 函数创建数据的噪声版本
❷ 创建一个新的 TensorFlow 会话用于训练
❸ 使用噪声数据
❹ 获取用于测试和分类的数据的噪声版本
当你查看测试函数的输出时,你会注意到,由于只有 1,000 个测试样本和噪声,去噪器在分类能力和区分图像类别方面的早期收益有所下降,因为在马和非马类别之间平均损失的差异很小:
data (10000, 1024)
reconstructed (1024,)
loss (10000,)
horse 61.12571251705483
not horse 61.106683374373304
但不必担心,因为随着时间的推移,去噪器自编码器将学会一个更鲁棒的特征模型,对训练数据中的波动和不完美具有抵抗力,以发展一个更稳固的模型。对于目前你拥有的马类别,你需要比 CIFAR-10 更多的数据。图 12.5 显示了去噪器从 CIFAR-10 测试集的前 20 张马图像中学习到的图像表示(左侧)。还有另一个自编码器需要了解:堆叠或深度自编码器。向着终点前进!

图 12.5 自编码器的噪声版本及其表示(右侧)和原始 CIFAR-10 测试马图像(左侧)
12.4 堆叠深度自编码器
另一类自编码器被称为堆叠或深度自编码器。与只有一个隐藏层处理输入端的编码步骤并在输出端以一个隐藏神经元的单层结束解码不同,堆叠自编码器有多个隐藏神经元层,使用前一层的参数化和调整来学习未来层的表示。这些自编码器的未来层具有更少的神经元。目标是创建和学会对输入数据进行更压缩解释的最佳设置。
在实践中,堆叠自编码器中的每一层隐藏层都学习一组最优特征的参数化,以在相邻层中实现最优的压缩表示。你可以将每一层视为代表一组高阶特征,因为你还不知道它们具体是什么,所以这个概念是简化问题复杂性的有用方法。在第十四章中,你将探索一些可视化这些表示的方法,但就目前而言,第十一章的教训仍然适用:随着你添加更多非线性的隐藏神经元,你的自编码器可以学习和表示任何函数。现在你可以接受输入并将其变得更小了!
您的 StackedAutoencoder 类是对之前自动编码器的相当直接的改编,正如您在列表 12.12 中看到的。您将创建具有与提供的输入数量一半的神经元的隐藏层。第一个隐藏层是 input_dim /2,第二个是 input_dim /4,依此类推。默认情况下,有三个隐藏层,您的自动编码器学习到的编码大小是原始输入的四分之一,因此对于 CIFAR-10,它学习了一个包含 256 个数字的输入表示。最后一个隐藏层的大小是 input_dim /2,最终的输出层大小是 input_dim。类方法与之前的自动编码器类似,只是它们在架构的 N-1 编码层上操作。
列表 12.12 StackedAutoencoder 类
class StackedAutoencoder:
def __init__(self, input_dim, num_hidden_layers=3, epoch=100, ❶
➥ batch_size=250, learning_rate=0.01): ❶
self.epoch = epoch
self.batch_size = batch_size
self.learning_rate = learning_rate
self.idim = [None]*num_hidden_layers ❷
self.hdim = [None]*num_hidden_layers ❸
self.hidden = [None]*num_hidden_layers ❹
self.weights = [None]*num_hidden_layers ❺
self.biases = [None]*num_hidden_layers ❻
x = tf.placeholder(dtype=tf.float32, shape=[None, input_dim])
initializer=tf.variance_scaling_initializer()
output_dim = input_dim
act=tf.nn.relu ❼
for i in range(0, num_hidden_layers): ❽
self.idim[i] = int(input_dim / (2*i)) if i else input_dim ❾
self.hdim[i] = int(input_dim / (2*(i+1))) if i < ❿
➥ num_hidden_layers-1 else int(input_dim/2) ❿
print('%s, weights [%d, %d] biases %d' % ("hidden layer "
➥ +str(i+1) if i else "input to hidden layer 1", self.idim[i],
➥ self.hdim[i], self.hdim[i]))
self.weights[i] = tf.Variable(initializer([self.idim[i],
➥ self.hdim[i]]), dtype=tf.float32, name='weights'+str(i))
self.biases[i] = tf.Variable(tf.zeros([self.hdim[i]]),
➥ name='biases'+str(i))
if i == 0:
self.hidden[i] = act(tf.matmul(x, self.weights[i]) + ⓫
➥ self.biases[i]) ⓫
else:
self.hidden[i] = act(tf.matmul(self.hidden[i-1],
➥ self.weights[i]) + self.biases[i])
#output layer
print('output layer, weights [%d, %d] biases %d' %
➥ (self.hdim[num_hidden_layers-1], output_dim, output_dim))
self.output_weight =
➥ tf.Variable(initializer([self.hdim[num_hidden_layers-1],
➥ output_dim]), dtype=tf.float32, name='output_weight') ⓬
self.output_bias = tf.Variable(tf.zeros([output_dim]),
➥ name='output_bias')
self.output_layer = act(tf.matmul(self.hidden[num_hidden_layers-1],
➥ self.output_weight)+self.output_bias)
self.x = x
self.loss = tf.reduce_mean(tf.square(self.output_layer-self.x))
self.train_op =
➥ tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss)
self.saver = tf.train.Saver()
❶ 创建一个具有 3 个隐藏层、批量大小为 250 和学习率为 0.01 的 StackedAutoencoder,并训练 100 个周期
❷ 每个隐藏层的输入维度
❸ 每个隐藏层的隐藏神经元数量
❹ 由激活函数(tf.nn.relu)激活的每个隐藏层的张量
❺ 每个隐藏层的权重
❻ 每个隐藏层的偏差
❼ 使用 tf.nn.relu(整流线性单元)激活函数
❽ 执行网络从输入层到隐藏层的构建
❾ 输入维度,最初是输入大小,然后是前一个隐藏层的输出大小
❿ 每个隐藏层的隐藏神经元数量(input_dim /2)
⓫ 隐藏层是输入 × 权重加上偏差的矩阵乘积,或者是前一层编码 × 权重加上偏差。
⓬ 创建输出层,其维度为 layer N-1 × 输出大小,或原始输入大小
train 方法相当直接,尽管我简要介绍了 TensorFlow Dataset API,这是一个用于轻松操作和准备训练数据的强大系统。您不必反复重写相同的批量方法,可以使用 tf.Dataset 和其方法来批量处理和打乱数据(以随机方式),以便为训练做准备。您将在第十四章中看到其他有用的方法。tf.Dataset 的关键要点是能够提前自动设置 shuffle 和 batch 参数,然后使用迭代器获取大小为 batch_size 的批次,这些批次会自动以随机顺序打乱,这样您的网络就不会记住输入数据的顺序。当迭代器耗尽时,您就完成了这个时代,您可以捕获一个 tf.errors.OutOfRangeException 来处理这种情况。
列表 12.13 StackedAutoencoder 和 tf.Dataset 的 train 方法
def train(self, data):
features = data
features_placeholder = tf.placeholder(features.dtype, features.shape)
dataset = ❶
➥ tf.data.Dataset.from_tensor_slices((features_placeholder)) ❶
dataset = dataset.shuffle(buffer_size=100) ❷
dataset = dataset.batch(self.batch_size) ❸
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(self.epoch):
batch_num=0
iter = dataset.make_initializable_iterator() ❹
sess.run(iter.initializer, feed_dict={features_placeholder:
➥ features})
iter_op = iter.get_next() ❺
while True:
try:
batch_data = sess.run(iter_op)
l, _ = sess.run([self.loss, self.train_op],
➥ feed_dict={self.x: batch_data})
batch_num += 1
except tf.errors.OutOfRangeError: ❻
break
print('epoch {0}: loss = {1}'.format(i, l))
self.saver.save(sess, './model.ckpt')
❶ 从这个案例中的张量切片创建一个新的 tf.Dataset,即输入数据是一个 NumPy 数组
❷ 设置从输入中进行随机样本选择的打乱大小
❸ 设置每个批量训练步骤中批量大小的参数
❹ 使用一个迭代器,该迭代器可以用每个批次的输入数据初始化
❺ 从原始输入集中随机打乱的大小获取下一个批次
❻ 捕获异常,指示在此个 epoch 中数据集已被耗尽,然后在保存模式后移动到保存的 epoch
TensorFlow 的 Dataset API 精美且简化了常见的批量训练技术,正如你在列表 12.13 中的 train 方法所看到的。StackedAutoencoder 的其余部分与其它自动编码器大致相同,只是你使用 N-1 层编码进行分类和解码步骤。列表 12.14 定义了其余的类。
列表 12.14 StackedAutoencoder 类的其余方法
def test(self, data):
with tf.Session() as sess:
self.saver.restore(sess, './model.ckpt')
hidden, reconstructed = sess.run([self.hidden[num_hidden_layers-1],❶
➥ self.output_layer], feed_dict={self.x: data}) ❶
print('input', data) ❷
print('compressed', hidden) ❸
print('reconstructed', reconstructed) ❹
return reconstructed
def classify(self, data, labels):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
self.saver.restore(sess, './model.ckpt')
hidden, reconstructed = sess.run([self.hidden[num_hidden_layers-1],❺
➥ self.output_layer], feed_dict={self.x: data}) ❺
reconstructed = reconstructed[0]
print('reconstructed', np.shape(reconstructed))
loss = np.sqrt(np.mean(np.square(data - reconstructed), axis=1))
print('loss', np.shape(loss))
horse_indices = np.where(labels == 7)[0]
not_horse_indices = np.where(labels != 7)[0]
horse_loss = np.mean(loss[horse_indices])
not_horse_loss = np.mean(loss[not_horse_indices])
print('horse', horse_loss) ❻
print('not horse', not_horse_loss) ❼
return hidden
def decode(self, encoding):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
self.saver.restore(sess, './model.ckpt')
reconstructed = sess.run(self.output_layer, feed_dict={self.hidden[num_hidden_layers-1]: encoding}) ❽
img = np.reshape(reconstructed, (32, 32))
return img
❶ 根据输入计算输出前的最后一层,然后解码该层
❷ 打印输入数据
❸ 打印激活的 N-1 层神经元值
❹ 打印重建的数据
❺ 使用最后一个 N-1 层进行分类
❻ 打印来自马测试类别的损失
❼ 打印来自所有其他图像类别的损失
❽ 使用 N-1 层来获取输出层并返回重建的 32 × 32 图像
测试方法的输出显示,StackedEncoder 的 N-1 层编码足以区分马类和其他图像类别,尽管编码显示的变异性比原始自动编码器少。然而,有趣的是,重建的图像显示了特征的整体更紧凑的表示,并且在图 12.6 中原始 CIFAR-test 马图像表示的右侧的遮挡区域中存在一些粗糙的高阶特征。

图 12.6 三张 CIFAR-10 测试图像(左)的堆叠自动编码器表示(右)。高阶特征在遮挡的粗糙区域中可见。
reconstructed (1024,)
loss (10000,)
horse 65.17236194056699
not horse 64.06345316293603
现在您已经探索了不同的自动编码器以及它们在图像分类和检测方面的优势和权衡,您已经准备好将它们应用于其他真实数据集了!
摘要
-
自动编码器可以通过使用神经网络隐藏层学习图像的表示来用于分类、排序和聚类图像。
-
CIFAR-10 是一个广泛使用的图像数据集,包含 10 类图像(包括
horse、bird和automobile)。CIFAR-10 包含每类 5,000 张训练图像(总计 50,000 张)和 10,000 张测试图像(每类 1,000 张)。 -
自动编码器的特殊化可以通过处理数据特征中的噪声或使用具有更多层的更密集架构来学习输入(如图像)的更鲁棒表示。这些自动编码器被称为去噪和堆叠自动编码器。
-
您可以通过比较通过手动或使用 Matplotlib 和其他辅助库学习到的表示值与它们的原始结果来评估图像数据上的自动编码器。
13 强化学习
本章涵盖
-
定义强化学习
-
实现强化学习
人类从经验中学习(或者至少应该)。你的迷人并不是偶然的。多年的正面赞美以及负面批评都帮助你塑造了今天的你。本章是关于设计一个由批评和奖励驱动的机器学习系统。
你通过与他人、家人或甚至陌生人的互动来学习什么让人们快乐,例如,通过尝试各种肌肉运动直到骑行变得顺畅,你就能学会如何骑自行车。当你执行动作时,你有时会立即得到奖励。例如,找到附近的美食可能会带来即时的满足感。有时,奖励不会立即出现;你可能需要长途跋涉才能找到一个出色的用餐地点。强化学习是关于在任何状态下选择正确的动作——例如,在图 13.1 中,它显示了一个人在交通和意外情况下导航以到达目的地。

图 13.1 在交通和意外情况下导航以到达目的地的人是一个强化学习的问题设置。
此外,假设你在从家到工作的路上,你总是选择同一条路线。但有一天,你的好奇心占了上风,你决定尝试一条不同的路线,希望缩短通勤时间。这种困境——尝试新路线或坚持已知的最佳路线——是探索与利用的一个例子。
注意 为什么尝试新事物和坚持旧事物之间的权衡被称为探索与利用?探索是有意义的,但你可以将利用视为通过坚持你所知道的东西来利用你对现状的了解。
所有这些例子都可以统一在一个通用公式下:在某个场景中执行一个动作可以产生一个奖励。对于场景的更技术性的术语是状态。我们称所有可能状态的集合为状态空间。执行一个动作会导致状态发生变化。如果你还记得第九章和第十章,它们讨论了隐马尔可夫模型(HMMs),那么这对你来说不应该太陌生。你根据观察从状态过渡到状态。但哪一系列动作会产生最高的预期奖励呢?
13.1 形式概念
与监督学习和无监督学习出现在光谱的两端不同,强化学习(RL)存在于中间某个位置。它不是监督学习,因为训练数据来自算法在探索和利用之间做出决定。它也不是无监督学习,因为算法从环境中接收反馈。只要你在执行动作在某个状态下产生奖励的情况下,你就可以使用强化学习来发现一个好的动作序列,以最大化预期奖励。
你可能会注意到,强化学习术语涉及将算法拟人化为在情境中采取行动以获得奖励。该算法通常被称为代理,它在环境中采取行动。在机器人学中应用大量的强化学习理论并不令人惊讶。图 13.2 展示了状态、行动和奖励之间的相互作用。

图 13.2 行动由箭头表示,状态由圆圈表示。对状态执行行动会产生奖励。如果你从状态 s1 开始,你可以执行行动 a1 来获得奖励 r(s1, a1)。
人类使用强化学习吗?
强化学习似乎是最好的方式来解释如何根据当前情况执行下一步行动。也许人类在生物学上也是以同样的方式行事。但让我们不要过于乐观;考虑以下例子。
有时候,人类会无意识地采取行动。如果我渴了,我可能会本能地抓起一杯水来解渴。我不会在脑海中迭代所有可能的联合动作,并在彻底计算后选择最优的一个。
最重要的是,我们采取的行动并不仅仅由我们每个时刻的观察所决定。否则,我们就不比细菌更聪明,因为细菌会根据其环境确定性地采取行动。似乎还有更多的事情在进行中,一个简单的强化学习模型可能无法完全解释人类的行为。
机器人通过执行行动来改变状态。但它如何决定采取哪种行动?第 13.1.1 节介绍了一个新概念来回答这个问题。
13.1.1 策略
每个人打扫房间的习惯都不同。有些人从整理床铺开始。我更喜欢顺时针打扫房间,这样就不会错过任何一个角落。你见过机器人吸尘器,比如 Roomba 吗?有人编写了一个机器人可以遵循的策略来清洁任何房间。在强化学习术语中,代理决定采取哪种行动的方式是一个策略:决定下一个状态的行动集合(图 13.3)。

图 13.3 一项策略建议在给定状态下采取哪种行动。
强化学习的目标是发现一个好的策略。创建该策略的一种常见方式是观察每个状态下行动的长期后果。奖励是采取行动结果的衡量标准。最佳可能的策略被称为最优策略,这是强化学习的圣杯。最优策略告诉你给定任何状态下的最佳行动——但就像现实生活中一样,它可能不会在当下提供最高的奖励。
如果你通过观察采取动作后的直接后果——即采取动作后的状态——来衡量奖励,那么计算起来很容易。这种策略被称为贪婪策略。但“贪婪地”选择提供最佳即时奖励的动作并不总是好主意。例如,当你打扫房间时,你可能会先整理床铺,因为床铺整理好了房间看起来更整洁。但如果你还有其他目标,比如洗床单,那么先整理床铺可能不是最佳的整体策略。你需要考虑接下来几个动作的结果以及最终的状态,才能得出最佳的方法。同样,在棋类游戏中,抓住对手的皇后可能会最大化棋盘上棋子的分数——但如果这让你在五步之后被将军,那么这并不是最佳的可能走法。
(马尔可夫) 强化学习的局限性
大多数 RL 公式假设你可以从知道当前状态中找出最佳动作,而不是考虑导致你到达该状态的更长期的状态和动作历史。这种决策方法被称为马尔可夫,而通用框架通常被称为马尔可夫决策过程(MDP)。我之前已经暗示过这一点。
当状态足够捕捉到下一步要做什么时,可以使用本章讨论的 RL 算法来建模这些情况。但大多数现实世界的情况都不是马尔可夫的,因此需要更现实的方法,例如状态和动作的分层表示。在极其简化的意义上,分层模型类似于上下文无关文法,而 MDPs 则类似于有限状态机。将问题建模为 MDP 而不是更分层的模型,这种建模的飞跃可以显著提高规划算法的有效性。
你也可以任意选择一个动作,这被称为随机策略。如果你提出了一种策略来解决强化学习问题,通常一个好的做法是检查你学习到的策略是否比随机和贪婪策略表现更好,后者通常被称为基线。
13.1.2 效用
长期奖励被称为效用。如果你知道在某个状态下执行动作的效用,使用强化学习来学习策略就很容易。为了决定采取哪个动作,你选择产生最高效用的动作。正如你可能猜到的,困难的部分是揭示这些效用值。
在某个状态(s)下执行动作(a)的效用被表示为函数Q(s, a),称为效用函数,如图 13.4 所示。

图 13.4 给定一个状态和采取的动作,应用效用函数 Q 预测预期的和总奖励:即时奖励(下一个状态)加上随后通过遵循最佳策略获得的奖励。
练习 13.1
如果你被给出了效用函数 Q(s, a),你如何使用它来推导策略函数?
答案
政策(s) = argmax_a Q(s, a)
计算特定状态-行动对(s, a)的效用的一种优雅方法是递归地考虑未来行动的效用。你当前行动的效用不仅受即时奖励的影响,还受下一个最佳行动的影响,如下公式所示。在公式中,s'是下一个状态,a'表示下一个行动。在状态s中采取行动a的奖励用r(s, a)表示:
Q(s, a) = r (s, a) + γmax Q(s', a')
在这里,γ是一个你可以选择的超参数,称为折现因子。如果γ为 0,代理会选择最大化即时奖励的行动。γ的更高值会使代理更加重视考虑长期后果。你可以将公式读作“这个行动的价值是采取这个行动提供的即时奖励,加上折现因子乘以之后可能发生的最好事情。”
寻求未来奖励是你可以调整的一种超参数,但还有另一个。在一些强化学习应用中,新可获得的信息可能比历史记录更重要,反之亦然。如果期望机器人快速学习解决任务但不一定是最佳方案,你可能想要设置一个更快的学习率。或者如果允许机器人有更多时间探索和利用,你可能降低学习率。让我们称学习率为α,并按以下方式更改效用函数(注意当α = 1 时,方程式是相同的):
Q(s, a) ← Q(s, a) + α (r(s, a) + γmax Q(s', a') – Q(s, a))
如果你知道 Q 函数:Q(s, a),强化学习就可以解决。对我们来说,神经网络(第十一章和第十二章)在足够训练数据的情况下可以近似函数。TensorFlow 是处理神经网络的完美工具,因为它附带了许多简化神经网络实现的必要算法。
13.2 应用强化学习
应用强化学习需要定义一种从状态执行行动后检索奖励的方法。股票市场交易者很容易满足这些要求,因为买卖股票会改变交易者的状态(现金在手),并且每个行动都会产生奖励(或损失)。
练习 13.2
使用强化学习进行买卖股票可能有哪些可能的缺点?
答案
通过在市场上执行行动,如买卖股票,你可能会影响市场,导致它从你的训练数据中发生剧烈变化。
在这种情况下,状态是一个包含当前预算、当前股票数量和最近股票价格历史(最后 200 个股票价格)信息的向量。每个状态是一个 202 维向量。
为了简单起见,只有三种行动:买入、卖出和持有:
-
以当前股价买入股票会减少预算,同时增加当前股票数量。
-
以当前股价卖出股票,将其换成现金。
-
持有既不增加也不减少预算,这个动作等待一个时间周期,不产生任何奖励。
图 13.5 展示了一种基于股票市场数据的可能策略。

图 13.5 理想情况下,我们的算法应该低位买入,高位卖出。像这里展示的那样做一次,可能会获得大约 160 美元的回报。但真正的利润来自于你更频繁地买卖。你听说过高频交易这个术语吗?这种交易涉及尽可能频繁地低位买入,高位卖出,以在给定期间内最大化利润。
目标是学习一种策略,从股票市场的交易中获得最大净收益。这难道不是一件很酷的事情吗?让我们试试吧!
13.3 实现强化学习
为了收集股票价格,你将使用 Python 中的 ystockquote 库。你可以通过使用 pip 或遵循官方指南(github.com/cgoldberg/ystockquote)来安装它。使用 pip 安装它的命令如下:
$ pip install ystockquote
安装了该库后,让我们导入所有相关的库(列表 13.1)。
列表 13.1 导入相关库
import ystockquote ❶
from matplotlib import pyplot as plt ❷
import numpy as np ❸
import tensorflow as tf ❸
import random
❶ 用于获取股票价格原始数据
❷ 用于绘制股票价格
❸ 用于数值操作和机器学习
创建一个辅助函数,使用 ystockquote 库获取股票价格。该库需要三块信息:股票符号、起始日期和结束日期。当你选择这三个值时,你会得到一个代表该期间每日股票价格的数字列表。
如果你选择的起始日期和结束日期相隔太远,获取这些数据将花费一些时间。将数据保存到磁盘上以便下次本地加载可能是个好主意。列表 13.2 展示了如何使用库和缓存数据。
列表 13.2 获取价格的帮助函数
def get_prices(share_symbol, start_date, end_date,
cache_filename='stock_prices.npy', force=False):
try: ❶
if force:
raise IOError
else:
stock_prices = np.load(cache_filename)
except IOError:
stock_hist = ystockquote.get_historical_prices(share_symbol, ❷
➥ start_date, end_date) ❷
stock_prices = []
for day in sorted(stock_hist.keys()): ❸
stock_val = stock_hist[day]['Open']
stock_prices.append(stock_val)
stock_prices = np.asarray(stock_prices)
np.save(cache_filename, stock_prices) ❹
return stock_prices.astype(float)
❶ 尝试从文件中加载数据,如果它已经被计算过
❷ 从库中检索股票价格
❸ 从原始数据中提取相关信息
❹ 缓存结果
为了进行合理性检查,可视化股票价格数据是个好主意。创建一个图表,并将其保存到磁盘上(列表 13.3)。
列表 13.3 绘制股票价格的辅助函数
def plot_prices(prices):
plt.title('Opening stock prices')
plt.xlabel('day')
plt.ylabel('price ($)')
plt.plot(prices)
plt.savefig('prices.png')
plt.show()
你可以使用列表 13.4 获取一些数据并将其可视化。
列表 13.4 获取数据和可视化
if __name__ == '__main__':
prices = get_prices('MSFT', '1992-07-22', '2016-07-22', force=True)
plot_prices(prices)
图 13.6 展示了运行列表 13.4 生成的图表。

图 13.6 该图表总结了从 1992 年 7 月 22 日到 2016 年 7 月 22 日微软(MSFT)的开盘价。如果能在第 3,000 天左右买入,在第 5,000 天左右卖出,那岂不是很好?让我们看看我们的代码是否能够学会买入、卖出和持有以实现最佳收益。
大多数强化学习算法遵循类似的实现模式。因此,创建一个具有相关方法以供以后参考的类是一个好主意,例如一个抽象类或接口。请参阅列表 13.5 中的示例和图 13.7 中的说明。强化学习需要两个明确定义的操作:如何选择动作以及如何改进效用 Q 函数。

图 13.7 大多数强化学习算法归结为三个主要步骤:推断、执行和学习。在第一步中,算法根据到目前为止的知识,在给定状态(s)的情况下选择最佳动作(a)。接下来,它执行动作以找出奖励(r)以及下一个状态(s')。然后它通过使用新获得的知识(s, r, a, s')来提高对世界的理解。
列表 13.5 定义所有决策策略的超类
class DecisionPolicy:
def select_action(self, current_state): ❶
pass
def update_q(self, state, action, reward, next_state): ❷
pass
❶ 给定一个状态,决策策略将计算下一步要采取的动作。
❷ 通过采取动作的新经验改进 Q 函数
接下来,让我们从这个超类继承以实现一个在随机决策的策略,也称为随机决策策略。你需要定义的只有 select_action 方法,该方法随机选择一个动作,甚至不查看状态。列表 13.6 展示了如何实现它。
列表 13.6 实现随机决策策略
class RandomDecisionPolicy(DecisionPolicy): ❶
def __init__(self, actions):
self.actions = actions
def select_action(self, current_state): ❷
action = random.choice(self.actions)
return action
❶ 从 DecisionPolicy 继承以实现其函数
❷ 随机选择下一个动作
在列表 13.7 中,你假设有一个策略被提供给你(例如来自列表 13.6 的策略)并在现实世界的股票价格数据上运行它。这个函数负责在每个时间间隔处理探索和利用。图 13.8 展示了列表 13.7 中的算法。

图 13.8 某个大小的滚动窗口迭代通过股票价格,如图所示,分为状态 S1、S2 和 S3。策略建议采取的行动:你可以选择利用它或随机探索另一个行动。随着你为执行动作获得奖励,你可以随着时间的推移更新策略函数。
列表 13.7 使用给定的策略进行决策并返回性能
def run_simulation(policy, initial_budget, initial_num_stocks, prices, hist):
budget = initial_budget ❶
num_stocks = initial_num_stocks ❶
share_value = 0 ❶
transitions = list()
for i in range(len(prices) - hist - 1):
if i % 1000 == 0:
print('progress {:.2f}%'.format(float(100*i) /
➥ (len(prices) - hist - 1)))
current_state = np.asmatrix(np.hstack((prices[i:i+hist], budget,
➥ num_stocks))) ❷
current_portfolio = budget + num_stocks * share_value ❸
action = policy.select_action(current_state, i) ❹
share_value = float(prices[i + hist])
if action == 'Buy' and budget >= share_value: ❺
budget -= share_value
num_stocks += 1
elif action == 'Sell' and num_stocks > 0: ❺
budget += share_value
num_stocks -= 1
else: ❺
action = 'Hold'
new_portfolio = budget + num_stocks * share_value ❻
reward = new_portfolio - current_portfolio ❼
next_state = np.asmatrix(np.hstack((prices[i+1:i+hist+1], budget,
➥ num_stocks)))
transitions.append((current_state, action, reward, next_state))
policy.update_q(current_state, action, reward, next_state) ❽
portfolio = budget + num_stocks * share_value ❾
return portfolio
❶ 初始化依赖于计算投资组合净值的值
❷ 状态是一个 hist + 2D 向量。你将强制它成为一个 NumPy 矩阵。
❸ 计算投资组合价值
❹ 从当前策略中选择一个动作
❺ 根据动作更新投资组合价值
❻ 执行动作后计算新的投资组合价值
❼ 计算在状态采取动作的奖励
❽ 在经历新的动作后更新策略
❾ 计算最终的投资组合价值
为了获得更稳健的成功测量,运行模拟几次并平均结果(列表 13.8)。这样做可能需要一段时间(可能需要 5 分钟),但你的结果将更可靠。
列表 13.8 运行多次模拟以计算平均性能
def run_simulations(policy, budget, num_stocks, prices, hist):
num_tries = 10 ❶
final_portfolios = list() ❷
for i in range(num_tries):
final_portfolio = run_simulation(policy, budget, num_stocks, prices,
➥ hist) ❸
final_portfolios.append(final_portfolio)
print('Final portfolio: ${}'.format(final_portfolio))
plt.title('Final Portfolio Value')
plt.xlabel('Simulation #')
plt.ylabel('Net worth')
plt.plot(final_portfolios)
plt.show()
❶ 决定重新运行模拟的次数
❷ 将每次运行的组合价值存储在这个数组中
❸ 运行此模拟
在 main 函数中,将列表 13.9 中的行添加到定义决策策略,然后运行模拟以查看策略的表现。
列表 13.9 定义决策策略
if __name__ == '__main__':
prices = get_prices('MSFT', '1992-07-22', '2016-07-22')
plot_prices(prices)
actions = ['Buy', 'Sell', 'Hold'] ❶
hist = 3
policy = RandomDecisionPolicy(actions) ❷
budget = 100000.0 ❸
num_stocks = 0 ❹
run_simulations(policy, budget, num_stocks, prices, hist) ❺
❶ 定义代理可以采取的动作列表
❷ 初始化一个随机决策策略
❸ 设置可用于使用的初始金额
❹ 设置已拥有的股票数量
❺ 多次运行模拟以计算最终净值的期望值
现在你有了比较结果的基准,让我们实现一个神经网络方法来学习 Q 函数。决策策略通常被称为 Q-learning 决策策略。列表 13.10 引入了一个新的超参数 epsilon,以防止在反复应用相同动作时解决方案“卡住”。epsilon 的值越低,随机探索新动作的频率就越高。Q 函数由图 13.9 所示的函数定义。

图 13.9 的输入是状态空间向量,有三个输出:每个输出的 Q 值一个
练习 13.3
你状态空间表示中忽略的其他可能影响股票价格的因素有哪些?你如何将它们纳入模拟?
答案
股票价格取决于多种因素,包括整体市场趋势、突发新闻和特定行业趋势。一旦量化,这些因素中的每一个都可以作为额外的维度应用于模型。
列表 13.10 实现更智能的决策策略
class QLearningDecisionPolicy(DecisionPolicy):
def __init__(self, actions, input_dim):
self.epsilon = 0.95 ❶
self.gamma = 0.3 ❶
self.actions = actions
output_dim = len(actions)
h1_dim = 20 ❷
self.x = tf.placeholder(tf.float32, [None, input_dim]) ❸
self.y = tf.placeholder(tf.float32, [output_dim]) ❸
W1 = tf.Variable(tf.random_normal([input_dim, h1_dim])) ❹
b1 = tf.Variable(tf.constant(0.1, shape=[h1_dim])) ❹
h1 = tf.nn.relu(tf.matmul(self.x, W1) + b1) ❹
W2 = tf.Variable(tf.random_normal([h1_dim, output_dim])) ❹
b2 = tf.Variable(tf.constant(0.1, shape=[output_dim])) ❹
self.q = tf.nn.relu(tf.matmul(h1, W2) + b2) ❺
loss = tf.square(self.y - self.q) ❻
self.train_op = tf.train.AdagradOptimizer(0.01).minimize(loss) ❼
self.sess = tf.Session() ❽
self.sess.run(tf.global_variables_initializer()) ❽
def select_action(self, current_state, step):
threshold = min(self.epsilon, step / 1000.)
if random.random() < threshold: ❾
# Exploit best option with probability epsilon
action_q_vals = self.sess.run(self.q, feed_dict={self.x:
➥ current_state})
action_idx = np.argmax(action_q_vals)
action = self.actions[action_idx]
else: ❿
# Explore random option with probability 1 - epsilon
action = self.actions[random.randint(0, len(self.actions) - 1)]
return action
def update_q(self, state, action, reward, next_state): ⓫
action_q_vals = self.sess.run(self.q, feed_dict={self.x: state})
next_action_q_vals = self.sess.run(self.q, feed_dict={self.x:
➥ next_state})
next_action_idx = np.argmax(next_action_q_vals)
current_action_idx = self.actions.index(action)
action_q_vals[0, current_action_idx] = reward + self.gamma *
➥ next_action_q_vals[0, next_action_idx]
action_q_vals = np.squeeze(np.asarray(action_q_vals))
self.sess.run(self.train_op, feed_dict={self.x: state, self.y:
➥ action_q_vals})
❶ 从 Q 函数设置超参数
❷ 设置神经网络中的隐藏节点数量
❸ 定义输入和输出张量
❹ 设计神经网络架构
❺ 定义计算效用的操作
❻ 将损失设置为平方误差
❼ 使用优化器更新模型参数以最小化损失
❽ 设置会话并初始化变量
❾ 以概率 epsilon 利用最佳选项
❿ 以概率 1 - epsilon 探索随机选项
⓫ 通过更新其模型参数来更新 Q 函数
整个脚本的输出显示在图 13.10 中。QLearningDecisionPolicy中包含两个关键函数:update_q和select_action,这两个函数实现了随时间推移的动作学习值。五分之一的几率,这些函数会输出一个随机动作。在select_action中,大约每 1,000 个价格,函数会强制执行一个随机动作和探索,这由self.epsilon定义。在update_q中,智能体根据q策略值中那些状态的argmax定义当前状态和下一个期望动作。因为算法是用tf.random_normal初始化的 Q 函数和权重,所以智能体需要一段时间才能开始意识到真正的长期奖励,但再次强调,这种认识正是重点。智能体从随机理解开始,采取行动,并在模拟过程中学习最优策略。

图 13.10 算法学习了一个良好的交易微软股票的策略。
你可以想象 Q 策略函数学习的神经网络类似于图 13.11 所示的流程。X 是你的输入,代表三个股票价格的历史、当前余额和股票数量。第一层隐藏层(20, 1)学习特定动作的奖励随时间的历史,第二层隐藏层(3, 1)将这些动作映射到任何时间的Buy(买入)、Sell(卖出)、Hold(持有)的概率。

图 13.11 Q 策略函数的两个隐藏层的神经网络结构
13.4 探索强化学习的其他应用
强化学习比你想象的更常见。当你学习了监督学习和无监督学习方法时,很容易忘记它的存在。但以下例子将让你看到谷歌成功使用强化学习的案例:
-
游戏玩法——2015 年 2 月,谷歌开发了一个名为 Deep RL 的强化学习系统,用于学习如何从 Atari 2600 游戏机玩街机视频游戏。与大多数强化学习解决方案不同,这个算法具有高维输入;它感知视频游戏的原始帧帧图像。这样,相同的算法可以与任何视频游戏一起工作,而无需太多的重新编程或重新配置。
-
更多游戏玩法——2016 年 1 月,谷歌发布了一篇关于一个能够赢得棋盘游戏围棋的 AI 智能体的论文。由于可能的配置数量巨大(甚至比棋盘还多!),这款游戏因其不可预测性而闻名,但这个使用强化学习的算法能够击败顶级的人类围棋选手。最新版本 AlphaGo Zero 于 2017 年底发布,并在仅 40 天的训练后,以 100 比 0 的比分一致击败了早期版本。到你看这本书的时候,AlphaGo Zero 的表现将比 2017 年有显著提升。
-
机器人学与控制 ——2016 年 3 月,谷歌展示了一种让机器人通过许多示例学习如何抓取物体的方法。谷歌使用多个机器人收集了超过 80 万个抓取尝试,并开发了一个模型来抓取任意物体。令人印象深刻的是,机器人仅通过摄像头输入就能抓取物体。学习抓取物体的简单概念需要聚合许多机器人的知识,花费许多天进行暴力尝试,直到检测到足够的模式。显然,机器人要能够泛化还有很长的路要走,但这个项目仍然是一个有趣的开始。
注意:现在你已经将强化学习应用于股市,是时候你从学校辍学或辞职,开始操纵系统了——亲爱的读者,你读到这本书的这一部分,你的回报就是!我在开玩笑。实际的股市是一个更加复杂的生物,但本章中的技术可以推广到许多情况。
摘要
-
强化学习是处理那些可以通过状态来框架化的问题的自然工具,这些状态会因为代理采取的行动而改变,以发现奖励。
-
实现强化学习算法需要三个主要步骤:从当前状态推断最佳行动,执行该行动,并从结果中学习。
-
Q-learning 是一种解决强化学习的方法,通过开发一个算法来近似效用函数(Q 函数)。在找到足够好的近似之后,你可以开始从每个状态推断出最佳的行动。
14 卷积神经网络
本章涵盖
-
检查卷积神经网络组件
-
使用深度学习对自然图像进行分类
-
提高神经网络性能——技巧和窍门
在疲惫的一天之后去购物是一种负担。我的眼睛被过多的信息轰炸。促销、优惠券、颜色、幼儿、闪烁的灯光和拥挤的通道是所有信号被发送到我的视觉皮层的一些例子,无论我是否试图注意。视觉系统吸收了大量的信息。
你是否听说过“一图胜千言”这句话?这可能对你或我来说是真的,但机器能否在图像中找到意义呢?我们视网膜中的光感受细胞接收光波长度,但这些信息似乎并没有传播到我们的意识中。毕竟,我无法用言语准确描述我接收到的光波长度。同样,相机捕捉像素,但我们希望提取某种形式的高级知识,例如物体的名称或位置。我们如何从像素到人类水平的感知过渡呢?
为了通过机器学习从原始感官输入中获取智能意义,你将设计一个神经网络模型。在前面的章节中,你已经看到了几种类型的神经网络模型,例如全连接的(第十三章)和自编码器(第十一章和第十二章)。在本章中,你将遇到另一种类型的模型:一种卷积神经网络(CNN),它在图像和其他感官数据(如音频)上表现非常出色。CNN 模型可以可靠地分类图像中显示的物体,例如。
在本章中,你将实现一个 CNN 模型,该模型将从 CIFAR-10 数据集(第十一章和第十二章)中学习如何将图像分类为十个候选类别之一。实际上,“一图只值十个可能性中的一个词”。这是向人类水平感知迈出的一小步,但你必须从某个地方开始,对吧?
14.1 神经网络的缺点
机器学习构成了一场永恒的斗争:设计一个足够表达数据,但又不过于灵活以至于过度拟合和记住模式的模型。神经网络被提出作为一种提高这种表达能力的途径,但正如你可能猜到的,它们经常受到过度拟合的陷阱。
注意:当你的学习模型在训练数据集上表现异常出色,但在测试数据集上表现不佳时,会发生过度拟合。该模型可能对可用数据的灵活性过高,最终几乎只是记住训练数据。
你可以使用的一个快速而简单的启发式方法来比较两个机器学习模型的灵活性,就是计算需要学习的参数数量。如图 14.1 所示,一个完全连接的神经网络,它接受一个 256 × 256 的图像并将其映射到一个包含 10 个神经元的层,将会有 256 × 256 × 10 = 655,360 个参数!与可能只有五个参数的模型相比,完全连接的神经网络可能能够表示比五个参数的模型更复杂的数据。

图 14.1 在一个完全连接的网络中,图像的每个像素都被视为一个输入。对于一个 256 × 256 大小的灰度图像,那就有 256 × 256 个神经元。将每个神经元连接到 10 个输出会产生 256 × 256 × 10 = 655,360 个权重。
第 14.2 节介绍了卷积神经网络(CNNs),这是一种减少参数数量的巧妙方法。而不是处理一个需要学习许多参数的完全连接网络,你可以采用 CNN 方法,多次重用相同的参数来减少学习到的权重数量。
14.2 卷积神经网络
CNNs 背后的主要思想是,对图像的局部理解已经足够好。实际的好处是,拥有更少的参数可以大大提高学习所需的时间,以及减少训练模型所需的数据量。然而,这种时间上的改进有时是以准确性为代价的。
与每个像素的权重完全连接的网络不同,CNN 有足够的权重来观察图像的一小部分。想象一下,你正在用放大镜读书;最终,你读完了整个页面,但你每次只看页面的一个小块。
考虑一个 256 × 256 的图像。而不是同时处理整个图像,你的 TensorFlow 代码可以高效地分块扫描它——比如说,一个沿着图像滑动(通常是左到右和上到下)的 5 × 5 窗口,如图 14.2 所示。它滑动的“快慢”被称为其步长。例如,步长为 2 意味着 5 × 5 滑动窗口每次移动 2 个像素,直到覆盖整个图像。在 TensorFlow 中,你可以通过使用内置的库函数轻松调整步长和窗口大小,正如你很快就会看到的。

图 14.2 在图像上卷积一个 5 × 5 的块(左侧)会产生另一个图像(右侧)。在这种情况下,产生的图像与原始图像大小相同。将原始图像转换为卷积图像只需要 5 × 5 = 25 个参数。
这个 5 × 5 的窗口有一个相关的 5 × 5 权重矩阵。
注意:卷积是图像像素值的加权求和,当窗口在整个图像上滑动时。这个带有权重矩阵的卷积过程在整个图像上产生另一个大小相同的图像,具体取决于惯例。卷积是将卷积应用于图像的过程。
滑动窗口的把戏发生在神经网络的卷积层上。一个典型的 CNN 有多个卷积层。每个卷积层通常生成许多交替的卷积,所以权重矩阵是一个 5 × 5 × n的张量,其中n是卷积的数量。
假设一个图像通过一个 5 × 5 × 64 的权重矩阵的卷积层。这个层通过滑动一个 5 × 5 的窗口生成 64 个卷积。因此,这个模型有 5 × 5 × 64(= 1,600)个参数,这比全连接网络少得多,全连接网络有 256 × 256(= 65,536)个参数。
CNN 的美丽之处在于参数数量与原始图像的大小无关。你可以在一个 300 × 300 的图像上运行相同的 CNN,卷积层的参数数量不会改变!
14.3 准备图像
要开始在 TensorFlow 中实现 CNN,你需要获取一些图像来工作。本节中的代码列表帮助你设置本章剩余部分的训练数据集。
首先,从www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz下载 CIFAR-10 数据集;如果需要,回顾第十一章和第十二章以获取更多相关信息。这个数据集包含 60,000 张图像,分为 10 个类别,这使得它成为分类任务的极好资源,正如我在那些章节中向你展示的那样。将文件提取到你的工作目录中。图 14.3 显示了数据集的图像示例。

图 14.3 CIFAR-10 数据集的图像。因为它们只有 32 × 32,所以有点难以看清,但你还是可以识别出一些物体。
你在第十二章使用了 CIFAR-10 数据集,所以再次调出那段代码。列表 14.1 直接来自 CIFAR-10 文档www.cs.toronto.edu/~kriz/cifar.html。将代码放入一个名为 cifar_tools.py 的文件中。
列表 14.1 使用 Python 从 CIFAR-10 文件中加载图像
import pickle
def unpickle(file):
fo = open(file, 'rb')
dict = pickle.load(fo, encoding='latin1')
fo.close()
return dict
神经网络已经容易过拟合,所以你需要尽可能减少那个错误。因此,始终记得在处理之前清洗数据。到现在为止,你已经看到数据清洗和流程有时是大部分的工作。
清洗数据是机器学习流程中的核心过程。列表 14.2 实现了以下三个步骤来清洗图像数据集:
-
如果你有一个彩色图像,尝试将其转换为灰度图以降低输入数据的维度,从而降低参数数量。
-
考虑对图像进行中心裁剪,因为图像的边缘可能不提供任何有用的信息。
-
通过从每个数据样本中减去均值并除以标准差来归一化你的输入,这样在反向传播过程中梯度不会变化得太剧烈。
列表 14.2 展示了如何使用这些技术清理图像数据集。
列表 14.2 清理数据
import numpy as np
def clean(data):
imgs = data.reshape(data.shape[0], 3, 32, 32) ❶
grayscale_imgs = imgs.mean(1) ❷
cropped_imgs = grayscale_imgs[:, 4:28, 4:28] ❸
img_data = cropped_imgs.reshape(data.shape[0], -1)
img_size = np.shape(img_data)[1]
means = np.mean(img_data, axis=1)
meansT = means.reshape(len(means), 1)
stds = np.std(img_data, axis=1)
stdsT = stds.reshape(len(stds), 1)
adj_stds = np.maximum(stdsT, 1.0 / np.sqrt(img_size))
normalized = (img_data - meansT) / adj_stds ❹
return normalized
❶ 将数据重新组织成 32 × 32 的矩阵,具有三个通道
❷ 通过平均颜色强度将图像转换为灰度
❸ 将 32 × 32 的图像裁剪成 24 × 24 的图像以减少参数
❹ 通过减去均值并除以标准差来归一化像素值
将 CIFAR-10 中的所有图像收集到内存中,并在它们上运行清理函数。列表 14.3 设置了一个方便的方法来读取、清理和结构化你的数据,以便在 TensorFlow 中使用。请将此代码包含在 cifar_tools.py 中。
列表 14.3 预处理所有 CIFAR-10 文件
def read_data(directory):
names = unpickle('{}/batches.meta'.format(directory))['label_names']
print('names', names)
data, labels = [], []
for i in range(1, 6):
filename = '{}/data_batch_{}'.format(directory, i)
batch_data = unpickle(filename)
if len(data) > 0:
data = np.vstack((data, batch_data['data']))
labels = np.hstack((labels, batch_data['labels']))
else:
data = batch_data['data']
labels = batch_data['labels']
print(np.shape(data), np.shape(labels))
data = clean(data)
data = data.astype(np.float32)
return names, data, labels
在另一个名为 using_cifar.py 的文件中,你可以通过导入cifar_tools来使用该方法。列表 14.4 和 14.5 展示了如何从数据集中采样一些图像并可视化它们。
列表 14.4 使用cifar_tools辅助函数
import cifar_tools
names, data, labels = \
cifar_tools.read_data('your/location/to/cifar-10-batches-py')
你可以随机选择一些图像并绘制它们对应的标签。列表 14.5 正是这样做的,这有助于你更好地理解你将处理的数据类型。
列表 14.5 可视化数据集中的图像
import numpy as np
import matplotlib.pyplot as plt
import random
def show_some_examples(names, data, labels):
plt.figure()
rows, cols = 4, 4 ❶
random_idxs = random.sample(range(len(data)), rows * cols) ❷
for i in range(rows * cols):
plt.subplot(rows, cols, i + 1)
j = random_idxs[i]
plt.title(names[labels[j]])
img = np.reshape(data[j, :], (24, 24))
plt.imshow(img, cmap='Greys_r')
plt.axis('off')
plt.tight_layout()
plt.savefig('cifar_examples.png')
show_some_examples(names, data, labels)
❶ 改变到你想要的行数和列数。
❷ 从数据集中随机选择图像进行展示
运行此代码后,将生成一个名为 cifar_examples.png 的文件,其外观将与本节中较早的图 14.3 相似。
14.3.1 生成过滤器
在本节中,你将使用几个随机的 5 × 5 补丁(也称为过滤器)对图像进行卷积。这一步在 CNN 中非常重要,因此你需要仔细检查数据是如何转换的。为了理解图像处理的 CNN 模型,观察图像过滤器如何转换图像是明智的。过滤器提取有用的图像特征,如边缘和形状。你可以在这些特征上训练机器学习模型。
记住,特征向量表示了如何表示数据点。当你对一个图像应用过滤器时,转换后的图像中的对应点是一个特征——这个特征表明,“当你将这个过滤器应用到这个点时,它具有这个新的值。”你在一个图像上使用的过滤器越多,特征向量的维度就越大。整体目标是平衡减少维度的过滤器数量,同时仍然捕捉到原始图像中的重要特征。
打开一个名为 conv_visuals.py 的新文件。让我们随机初始化 32 个滤波器。你将通过定义一个名为 W 的大小为 5 × 5 × 1 × 32 的变量来完成此操作。前两个维度对应于滤波器大小;最后一个维度对应于 32 个卷积。变量大小中的 1 对应于输入维度,因为 conv2d 函数能够卷积多通道的图像。 (在这个例子中,你只关心灰度图像,所以输入通道的数量是 1。) 列表 14.6 提供了生成滤波器的代码,这些滤波器如图 14.4 所示。
列表 14.6 生成和可视化随机滤波器
W = tf.Variable(tf.random_normal([5, 5, 1, 32])) ❶
def show_weights(W, filename=None):
plt.figure()
rows, cols = 4, 8 ❷
for i in range(np.shape(W)[3]): ❸
img = W[:, :, 0, i]
plt.subplot(rows, cols, i + 1)
plt.imshow(img, cmap='Greys_r', interpolation='none')
plt.axis('off')
if filename:
plt.savefig(filename)
else:
plt.show()
❶ 定义表示随机滤波器的张量
❷ 定义足够的行和列以显示图 14.4 中的 32 个图像
❸ 可视化每个滤波器矩阵

图 14.4 这些 32 个随机初始化的矩阵大小为 5 × 5。这些矩阵代表你将用于卷积输入图像的滤波器。
练习 14.1
你需要在列表 14.6 中做哪些更改以生成大小为 3 × 3 的 64 个滤波器?
答案
W = tf.Variable(tf.random_normal([3, 3, 1, 64]))
使用列表 14.7 中所示的会话,并使用 global_variables_initializer 操作初始化一些权重。调用 show_weights 函数来可视化如图 14.4 所示的随机滤波器。
列表 14.7 使用会话初始化权重
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
W_val = sess.run(W)
show_weights(W_val, 'step0_weights.png')
14.3.2 使用滤波器进行卷积
14.3.1 节向你展示了如何准备用于卷积的滤波器。在本节中,你将使用 TensorFlow 的卷积函数对随机生成的滤波器进行卷积。列表 14.8 设置了可视化卷积输出的代码。你将在之后使用它,就像你使用了 show_weights。
列表 14.8 展示卷积结果
def show_conv_results(data, filename=None):
plt.figure()
rows, cols = 4, 8
for i in range(np.shape(data)[3]):
img = data[0, :, :, i] ❶
plt.subplot(rows, cols, i + 1)
plt.imshow(img, cmap='Greys_r', interpolation='none')
plt.axis('off')
if filename:
plt.savefig(filename)
else:
plt.show()
❶ 与列表 14.6 不同,张量形状不同;它不是权重,而是结果图像。
假设你有一个示例输入图像,例如图 14.5 中所示。你可以使用 5 × 5 滤波器对 24 × 24 图像进行卷积,从而产生许多卷积图像。所有这些卷积都是对同一图像的独特视角。这些视角共同工作,以理解图像中存在的对象。列表 14.9 展示了如何逐步执行此任务。

图 14.5 来自 CIFAR-10 数据集的一个示例 24 × 24 图像
列表 14.9 可视化卷积
raw_data = data[4, :] ❶
raw_img = np.reshape(raw_data, (24, 24)) ❶
plt.figure() ❶
plt.imshow(raw_img, cmap='Greys_r') ❶
plt.savefig('input_image.png') ❶
x = tf.reshape(raw_data, shape=[-1, 24, 24, 1]) ❷
b = tf.Variable(tf.random_normal([32])) ❸
conv = tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') ❸
conv_with_b = tf.nn.bias_add(conv, b) ❸
conv_out = tf.nn.relu(conv_with_b) ❸
with tf.Session() as sess: ❹
sess.run(tf.global_variables_initializer()) ❹
conv_val = sess.run(conv) ❹
show_conv_results(conv_val, 'step1_convs.png') ❹
print(np.shape(conv_val)) ❹
conv_out_val = sess.run(conv_out) ❹
show_conv_results(conv_out_val, 'step2_conv_outs.png') ❹
print(np.shape(conv_out_val))
❶ 从 CIFAR 数据集中获取一个图像并可视化
❷ 定义 24 × 24 图像的输入张量
❸ 定义滤波器和相应的参数
❹ 在选定的图像上运行卷积
最后,通过在 TensorFlow 中运行 conv2d 函数,你得到图 14.6 中的 32 个图像。卷积图像的想法是,每个 32 个卷积捕捉到图像的不同特征。

图 14.6 在汽车图像上应用随机滤波器后的结果图像
通过添加偏置项和激活函数(如 relu,见列表 14.12 中的示例),网络的卷积层表现出非线性,这提高了其表达能力。图 14.7 显示了每个 32 个卷积输出变成了什么。

图 14.7 在你添加偏置项和激活函数后,结果卷积可以捕捉到图像中更强大的模式。图 14.7 显示了每个 32 个卷积输出变成了什么。
14.3.3 最大池化
在卷积层提取有用特征之后,通常减少卷积输出的尺寸是一个好主意。对卷积输出进行缩放或子采样有助于减少参数数量,这反过来又可以帮助防止数据过拟合。
这个概念是 最大池化 的主要思想,它在图像上扫过一个窗口并选择具有最大值的像素。根据步长长度,结果图像是原始图像大小的分数。这项技术很有用,因为它减少了数据的维度,减少了后续步骤中的参数数量。
练习 14.2
假设你想要在一个 32 × 32 的图像上进行最大池化。如果窗口大小是 2 × 2,步长长度是 2,那么结果的最大池化图像有多大?
答案
2 × 2 窗口需要在每个方向上移动 16 次,以覆盖 32 × 32 的图像,因此图像将缩小一半:16 × 16。因为它在两个维度上都缩小了一半,所以图像是原始图像的四分之一大小(½ × ½)。
将列表 14.10 放在 Session 上下文中。
列表 14.10 运行 maxpool 函数进行卷积图像子采样
k = 2
maxpool = tf.nn.max_pool(conv_out,
ksize=[1, k, k, 1],
strides=[1, k, k, 1],
padding='SAME')
with tf.Session() as sess:
maxpool_val = sess.run(maxpool)
show_conv_results(maxpool_val, 'step3_maxpool.png')
print(np.shape(maxpool_val))
运行此代码的结果是,最大池化函数将图像尺寸减半,并产生如图 14.8 所示的低分辨率卷积输出。

图 14.8 在 maxpool 运行后,卷积输出的大小减半,这使得算法在计算上更快,同时没有丢失太多信息。
你已经有了实现完整 CNN 所需的工具。在第 14.4 节中,你将最终训练分类器。
14.4 在 TensorFlow 中实现 CNN
CNN 有多个卷积和最大池化层。卷积层提供了对图像的不同视角,最大池化层通过减少维度简化了计算,同时没有丢失太多信息。
考虑一个全尺寸 256 × 256 的图像通过一个 5 × 5 滤波器卷积成 64 个卷积。如图 14.9 所示,每个卷积通过最大池化进行子采样,产生 64 个较小的 128 × 128 尺寸的卷积图像。

图 14.9 一个输入图像通过多个 5 × 5 滤波器进行卷积。卷积层包括一个附加的偏置项和激活函数,结果有 5 × 5 + 5 = 30 个参数。接下来,一个最大池化层减少了数据的维度(不需要额外的参数)。
现在你已经知道了如何制作滤波器和使用卷积操作,让我们创建一个新的源文件。你将首先定义所有变量。在列表 14.11 中,导入所有库,加载数据集,并定义所有变量。
列表 14.11 设置 CNN 权重
import numpy as np
import matplotlib.pyplot as plt
import cifar_tools
import tensorflow as tf
names, data, labels = \
cifar_tools.read_data('/home/binroot/res/cifar-10-batches-py') ❶
x = tf.placeholder(tf.float32, [None, 24 * 24]) ❷
y = tf.placeholder(tf.float32, [None, len(names)]) ❷
W1 = tf.Variable(tf.random_normal([5, 5, 1, 64])) ❸
b1 = tf.Variable(tf.random_normal([64])) ❸
W2 = tf.Variable(tf.random_normal([5, 5, 64, 64])) ❹
b2 = tf.Variable(tf.random_normal([64])) ❹
W3 = tf.Variable(tf.random_normal([6*6*64, 1024])) ❺
b3 = tf.Variable(tf.random_normal([1024])) ❺
W_out = tf.Variable(tf.random_normal([1024, len(names)])) ❻
b_out = tf.Variable(tf.random_normal([len(names)])) ❻
❶ 加载数据集
❷ 定义输入和输出占位符
❸ 应用 64 个 5 × 5 窗口大小的卷积
❹ 应用 64 个更多 5 × 5 窗口大小的卷积
❺ 引入一个全连接层
❻ 定义全连接线性层的变量
在列表 14.12 中,你定义了一个辅助函数来执行卷积、添加偏置项,然后添加激活函数。这三个步骤共同构成了网络的一个卷积层。
列表 14.12 创建卷积层
def conv_layer(x, W, b):
conv = tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
conv_with_b = tf.nn.bias_add(conv, b)
conv_out = tf.nn.relu(conv_with_b)
return conv_out
列表 14.13 展示了如何通过指定内核和步长大小来定义最大池化层。
列表 14.13 创建最大池化层
def maxpool_layer(conv, k=2):
return tf.nn.max_pool(conv, ksize=[1, k, k, 1], strides=[1, k, k, 1],
➥ padding='SAME')
你可以将卷积层和最大池化层堆叠起来,以定义 CNN 架构。列表 14.14 定义了一个可能的 CNN 模型。最后一层通常是连接到每个 10 个输出神经元的全连接网络。
列表 14.14 完整的 CNN 模型
def model():
x_reshaped = tf.reshape(x, shape=[-1, 24, 24, 1])
conv_out1 = conv_layer(x_reshaped, W1, b1) ❶
maxpool_out1 = maxpool_layer(conv_out1) ❶
norm1 = tf.nn.lrn(maxpool_out1, 4, bias=1.0, alpha=0.001 / 9.0, ❶
➥ beta=0.75) ❶
conv_out2 = conv_layer(norm1, W2, b2) ❷
norm2 = tf.nn.lrn(conv_out2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)❷
maxpool_out2 = maxpool_layer(norm2) ❷
maxpool_reshaped = tf.reshape(maxpool_out2, [-1, ❸
➥ W3.get_shape().as_list()[0]]) ❸
local = tf.add(tf.matmul(maxpool_reshaped, W3), b3) ❸
local_out = tf.nn.relu(local) ❸
out = tf.add(tf.matmul(local_out, W_out), b_out) ❸
return out ❸
❶ 构建卷积和最大池化的第一层
❷ 构建第二层
❸ 构建最后的全连接层
14.4.1 测量性能
设计好神经网络架构后,下一步是定义一个你想要最小化的损失函数。你将使用 TensorFlow 的softmax_cross_entropy_with_logits函数,官方文档中对其有最佳描述,请参阅mng.bz/4Blw:
[函数softmax_cross_entropy_with_logits]用于测量在类别互斥的离散分类任务中的概率误差(每个条目恰好属于一个类别)。例如,每个 CIFAR-10 图像都标记了一个标签:一个图像可以是狗或卡车,但不能同时是两者。
由于一个图像可能属于十个可能的标签之一,因此你将这个选择表示为一个 10 维向量。这个向量的所有元素值都是0,除了与标签对应的元素,其值将是1。正如你在前面的章节中看到的,这种表示方法称为独热编码。
如列表 14.15 所示,你将通过我在第六章提到的交叉熵损失函数来计算成本。此代码返回你的分类的概率误差。请注意,此代码仅适用于简单的分类——那些你的类别是互斥的。例如,一个卡车不能同时是狗。你可以使用许多类型的优化器,但在这个例子中,你坚持使用 AdamOptimizer,这是一个快速、简单的优化器,在mng.bz/QxJG上有详细描述。
在实际应用中,可能需要尝试调整参数,但 AdamOptimizer 现成效果很好。
列表 14.15 定义测量成本和准确性的操作
model_op = model()
cost = tf.reduce_mean( ❶
tf.nn.softmax_cross_entropy_with_logits(logits=model_op, labels=y)
)
train_op = tf.train.AdamOptimizer(learning_rate=0.001).minimize(cost) ❷
correct_pred = tf.equal(tf.argmax(model_op, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
❶ 定义分类损失函数
❷ 定义训练操作以最小化损失函数
最后,在 14.4.2 节中,你将运行训练操作以最小化神经网络的成本。在数据集上多次执行此操作将教会最优的权重(或参数)。
14.4.2 训练分类器
在列表 14.16 中,你将通过小批量遍历图像数据集来训练神经网络。随着时间的推移,权重将逐渐收敛到一个局部最优,以准确预测训练图像。
列表 14.16 使用 CIFAR-10 数据集训练神经网络
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
onehot_labels = tf.one_hot(labels, len(names), on_value=1., off_value=0.,
➥ axis=-1)
onehot_vals = sess.run(onehot_labels)
batch_size = len(data) // 200
print('batch size', batch_size)
for j in range(0, 1000): ❶
print('EPOCH', j)
for i in range(0, len(data), batch_size): ❷
batch_data = data[i:i+batch_size, :]
batch_onehot_vals = onehot_vals[i:i+batch_size, :]
_, accuracy_val = sess.run([train_op, accuracy], feed_dict={x:
➥ batch_data, y: batch_onehot_vals})
if i % 1000 == 0:
print(i, accuracy_val)
print('DONE WITH EPOCH')
❶ 循环遍历 1,000 个 epoch
❷ 批量训练网络
就这样!你已经成功设计了一个用于图像分类的 CNN。请注意:训练 CNN 可能需要超过 10 分钟的时间。如果你在 CPU 上运行此代码,甚至可能需要数小时!你能想象在等待一天后发现代码中存在错误吗?这就是为什么深度学习研究人员使用强大的计算机和 GPU 来加速计算。
14.5 提高性能的技巧和窍门
本章中你开发的 CNN 是解决图像分类问题的一种简单方法,但在完成第一个工作原型后,许多技术可以提高性能:
-
数据增强——从一个单独的图像中,你可以轻松地生成新的训练图像。作为开始,你可以水平或垂直翻转图像,这样可以将数据集的大小增加到原来的四倍。你还可以调整图像的亮度或色调,以确保神经网络能够泛化到其他波动。你甚至可能想要向图像中添加随机噪声,使分类器对小的遮挡具有鲁棒性。调整图像的大小也可能很有帮助;在训练图像中保持完全相同大小的项目几乎可以保证过拟合。
-
提前停止——在训练神经网络的同时,跟踪训练和测试错误。最初,这两种类型的错误应该缓慢减少,因为网络正在学习。但有时,测试错误会再次上升,这表明神经网络已经开始在训练数据上过拟合,并且无法泛化到之前未见过的输入。你应该在发现这种现象时立即停止训练。
-
正则化权重——另一种对抗过拟合的方法是在损失函数中添加一个正则化项。你已经在之前的章节中看到了正则化,这里同样适用相同的概念。
-
Dropout——TensorFlow 提供了一个方便的
tf.nn.dropout函数,可以将它应用于网络的任何一层以减少过拟合。在训练过程中,该函数会关闭随机选择的一定数量的神经元,从而使网络在推断输出时具有冗余性和鲁棒性。 -
深度架构——通过向神经网络添加更多的隐藏层,可以得到更深的架构。如果你有足够的训练数据,添加更多的隐藏层已被证明可以提高性能。然而,网络将需要更多的时间来训练。
练习 14.3
在这个 CNN 架构的第一轮迭代之后,尝试应用本章中提到的几个技巧和窍门。
答案
精调是这个过程的一部分,遗憾的是。你应该从调整超参数和重新训练算法开始,直到找到最佳设置。
14.6 CNNs 的应用
当输入包含来自音频或图像的传感器数据时,CNNs 会蓬勃发展。特别是图像在工业界中非常重要。例如,当你注册社交网络时,你通常会上传个人照片,而不是你自己的“你好”音频录音。人类似乎天生喜欢照片,因此你可以尝试看看 CNNs 如何用于检测图像中的面部。
整个 CNN 架构可以像你希望的那样简单或复杂。你应该从简单开始,逐渐调整你的模型,直到你满意为止。没有一条正确的路径,因为面部识别尚未完全解决。研究人员仍在发表超越先前最先进解决方案的论文。
你的第一步应该是获取一个图像数据集。最大的任意图像数据集之一是 ImageNet (image-net.org)。在这里,你可以找到你的二元分类器的负面示例。要获取面部正面示例,你可以在以下专注于人类面部的网站上找到许多数据集:
-
VGG -Face 数据集 (
www.robots.ox.ac.uk/~vgg/data/vgg_face) -
面部检测数据集和基准(FDDB)(
vis-www.cs.umass.edu/ fddb) -
面部检测和姿态估计数据库 (
mng.bz/X0Jv) -
YouTube 面部数据库 (www.cs.tau.ac.il/~wolf/ytfaces)
摘要
-
卷积神经网络(CNNs)假设捕捉信号的局部模式足以表征该信号,从而减少神经网络的参数数量。
-
清洗数据对于大多数机器学习模型的表现至关重要。你用来编写清洗数据代码的小时与神经网络自己学习这个清洗函数所需的时间相比微不足道。
15 构建真实世界的 CNN:VGG-Face 和 VGG-Face Lite
本章涵盖
-
为训练卷积神经网络(CNN)增强数据
-
通过使用 dropout 和批量归一化调整 CNN 并评估性能
-
使用 CIFAR-10 和面部识别构建准确的 CNN
卷积神经网络(CNN)架构是分析图像和区分其特征的有用工具。线条或曲线可能表明你最喜欢的汽车,或者指示可能是一个特定的更高阶特征,例如大多数青蛙图片中存在的绿色。更复杂的指示可能是在你左鼻孔附近的一个雀斑或你家族几代人传下来的下巴曲线。
人类在多年中已经熟练地挑选出这些识别特征,并且好奇为什么是如此。人类已经习惯于从出生以来就观察数亿个展示给他们的示例图像,并接收关于他们在那些图像中看到内容的反馈。记得你妈妈在给你展示球的时候重复“球”这个词吗?你很可能记得她说过这个词的某个时刻。那么,当你看到另一个形状或颜色略有不同的球时说“球”又是怎么回事呢?
可能是你小时候,当你被 handed 一个新的带有披风的超级英雄动作人偶时,问这个人物是不是超人。它不是,但它是另一个看起来相似的英雄。你为什么认为它是超人呢?披风、深色的头发,以及胸部附近可能带有某种符号的三角形是看起来熟悉的图像特征。你的生物神经网络在输入图像时被激活,检索出当你的父母口头提供时经过时间强化的标签。当你被 handed 一个新的玩具时,你脱口而出标签,你的父母纠正了你。(“不,宝贝,它是沙赞。它看起来像超人,但我完全理解你为什么那么想!”)砰——基于略有不同的特征添加了一个标签,然后你继续学习。
这个过程类似于你训练 CNN 的方式,它允许计算机程序自动捕捉更高和更低阶的特征,并使用这些特征来区分图像。正如你在第十四章所学,这些特征由卷积滤波器表示,它们是网络的学到的参数。
由于各种因素,训练 CNN 来学习这些参数并非易事:
-
训练数据的可访问性 ——为了真实和准确,CNN 需要大量的训练数据,具有许多特征变化,以便过滤器可以捕捉它们。同样,需要大量的过滤器来表示这些特征。作为一个人类,你可能在一生中看到了数十亿张图片,其中有很多类、颜色、物体和人的重复。你的标签分配会随着时间的推移而变得更好,CNN 也是如此。
-
更深的神经网络架构 和更多特征细化 ——这些架构帮助 CNN 区分具有高度相似性的图像。如果你的 CNN 已经学会了人类与鸟类的区别,那么它是如何区分不同种类的人类,比如那些有波浪卷发或卷发,或者有白皙或棕褐色皮肤的人呢?更深的架构需要你的网络学习更多的参数,但它有更多的能力来表示特征变化,因此训练时间会更长。
-
防止记忆化 和学习更具弹性的表示 ——训练一个鲁棒的 CNN 意味着防止网络从训练数据中记住物体或人的特征。这个过程也确保了你的网络对新解释图像中物体和人的开放性,这些图像具有与它在野外可能看到的那些特征略有不同的表示,打破了它简单训练的理解。
所有这些问题都是使卷积神经网络(CNN)成为解决现实世界问题有用工具所必需的,正如你在图 15.1 中看到的。

图 15.1 CNN 架构帮助机器学习算法对图像进行标记。无论你是在尝试标记一个物体还是某人的脸,相同的 CNN 架构都可以完成这项任务。
在本章中,我将向你展示如何使用 TensorFlow 构建鲁棒的 CNN 架构,从你熟悉的内容开始:加拿大高级研究研究所(CIFAR-10)的数据集,包括汽车、飞机、船只、鸟类等。这个数据集足以代表现实世界,为了使它们准确,你需要对我在第十四章中展示的基本 CNN 架构进行优化。
此外,你将使用视觉几何组(VGG)Face 模型构建一个面部检测 CNN 系统。当给定 2,622 个可能的名人面孔之一时,该 CNN 将能够以高精度识别该面孔属于哪位名人。该 CNN 甚至可以处理不同的姿势、光照、化妆(或无化妆)、眼镜(或无眼镜)、帽子(或无帽子)以及图像中的大量其他属性。让我们开始构建现实世界的 CNN 吧!
15.1 为 CIFAR-10 构建现实世界的 CNN 架构
CIFAR-10 数据集你应该很熟悉,因为你已经在第十一章和第十二章中使用过它。它包含 50,000 张用于训练的图像,代表 10 个类别中的每个类别有 5,000 张图像——飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车——以及 10,000 张测试图像(每个类别 1,000 张)。在第十四章中,我向你展示了如何构建一个具有几层的浅层网络 CNN,以将 CIFAR-10 图像分类到 10 个类别之一。
CIFAR-10 数据集是由 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 收集的。Krizhevsky 是关于 CNNs 的开创性论文《使用深度卷积神经网络进行 ImageNet 分类》(www.cs.toronto.edu/~hinton/absps/imagenet.pdf)的作者。这篇被引用超过 40,000 次的论文提出了后来被称为 AlexNet 的概念,这是 Krizhevsky 以其名字命名的用于图像处理的著名 CNN 架构。除了用于 CIFAR-10 之外,AlexNet 还被用于赢得 2012 年 ImageNet 挑战赛。(ImageNet 是一个包含数百万张图像的语料库,这些图像被 WordNet 分类法标记,有 1,000 个物体类别;CIFAR-10 是其中包含 10 个物体类别的子集。)
AlexNet 在 CNNs 之外采用了几个重要的优化。特别是,它提出了一种更深层的架构,具有更多的卷积滤波器来捕获更高和更低阶的特征(图 15.2)。这些优化包括以下内容:
-
更深的架构和更多具有相关滤波器和容量的卷积层
-
使用数据增强(旋转图像、左右翻转或随机裁剪图像)
-
技术称为 dropout 的应用,该技术随机关闭特定层的神经元,从而使架构学习到对输入的更鲁棒的表达

图 15.2 在 CIFAR 数据上用于物体分类的著名 AlexNet CNN 架构
你可以从第十四章中创建的浅层 CIFAR-10 CNN 开始实现这些优化。首先,回顾处理加载 CIFAR-10 50,000 个训练图像的读取数据函数;然后将它们转换为灰度以减少学习参数的数量。不用担心;你将在本章稍后构建面部识别 CNN 时处理彩色图像。
15.1.1 加载和准备 CIFAR-10 图像数据
CIFAR-10 数据是 Python Pickle 格式,因此你需要函数将那种 pickle 格式读回到 Python 字典中——一个用于 32 × 32 × 50000 个图像数据,另一个用于 1 × 50000 个标签。此外,你还需要准备和清理数据,这是一个我想强烈强调的任务。你将执行以下操作:
-
创建一个函数来清理数据并通过除以平均图像值来归一化图像方差。
-
将图像裁剪到中心,并减少背景噪声。
-
将图像转换为灰度以减少网络中的维度并提高学习效率。
你将在列表 15.1 中复制这些函数,并准备好你的图像和标签以进行训练。请注意,这些优化简化了学习和训练过程。你可以省略其中的一些,但你的训练可能需要无限期地收敛,或者可能永远不会快速收敛到最优的学习参数。你进行这种清理是为了帮助学习过程。
列表 15.1 加载和准备 CIFAR-10 训练图像和标签
def unpickle(file):
fo = open(file, 'rb')
dict = pickle.load(fo, encoding='latin1') ❶
fo.close()
return dict ❶
def clean(data):
imgs = data.reshape(data.shape[0], 3, 32, 32)
grayscale_imgs = imgs.mean(1) ❷
cropped_imgs = grayscale_imgs[:, 4:28, 4:28] ❸
img_data = cropped_imgs.reshape(imgs.shape[0], -1) ❸
img_size = np.shape(img_data)[1]
means = np.mean(img_data, axis=1) ❹
meansT = means.reshape(len(means), 1) ❹
stds = np.std(img_data, axis=1) ❹
stdsT = stds.reshape(len(stds), 1) ❹
adj_stds = np.maximum(stdsT, 1.0 / np.sqrt(img_size)) ❹
normalized = (img_data - meansT) / adj_stds ❹
return normalized
❶ 加载序列化的字典
❷ 灰度图像是 R、G、B 轴的平均值
❸ 裁剪图像
❹ 通过减去图像均值并除以标准差,使得图像对高变异性不敏感,从而简化学习过程
数据读取函数使用 NumPy 的npy紧凑二进制格式存储 NumPy 数组,如果你之前已经加载了图像数据和标签,它会保存现有的图像数据和标签。这样,在你处理、清理和加载图像及其标签之后,你不必再等待这些函数完成,因为它们可能需要相当长的时间才能完成。
但我有一个 GPU!
图形处理单元(GPU)在神经网络及其深度学习任务(如预测和分类)的训练方面改变了游戏规则。经过多年的优化,GPU 支持与图形处理相关的指令,这些指令最初用于视频游戏,GPU 支持的运算——矩阵乘法——在矩阵需求量大的机器学习算法中找到了盟友。然而,GPU 并不真正帮助你在传统的以 CPU 为中心的操作中,或者在涉及磁盘输入输出(I/O)的操作中,例如从磁盘加载缓存数据。好消息是,包括 TensorFlow 在内的机器学习框架知道哪些操作需要为 GPU 优化,哪些操作适合 CPU,并且相应地分配这些操作。不过,对于数据准备部分,不要过于兴奋你的 GPU,因为你仍然受限于 I/O。
列表 15.2 加载了 CIFAR-10 的图像数据和标签。请注意,它还包括augment函数,我将在第 15.1.2 节中讨论。
列表 15.2 加载 CIFAR-10 图像数据和标签
def read_data(directory):
data_file = 'aug_data.npy' ❶
labels_file = 'aug_labels.npy' ❶
names = unpickle('{}/batches.meta'.format(directory))['label_names']
print('names', names)
data, labels = [], []
if os.path.exists(data_file) and os.path.isfile(data_file) and os.path.exists(labels_file) and os.path.isfile(labels_file):
print('Loading data from cache files {} and {}'.format(data_file,
➥ labels_file))
data = np.load(data_file)
labels = np.load(labels_file)
else:
for i in range(1, 6):
filename = '{}/data_batch_{}'.format(directory, i)
batch_data = unpickle(filename)
if len(data) > 0:
data = np.vstack((data, batch_data['data']))
labels = np.hstack((labels, batch_data['labels']))
else:
data = batch_data['data']
labels = batch_data['labels']
data, labels = augment(data, labels) ❷
data = clean(data) ❸
data = data.astype(np.float32)
np.save('aug_data.npy', data) ❹
np.save('aug_labels.npy', labels) ❹
print(np.shape(data), np.shape(labels)) ❹
return names, data, labels ❹
❶ 使用这些文件名缓存图像数据和标签的 NumPy 数组,如果文件不存在则加载它们
❷ 如第 15.2 节所示执行数据增强
❸ 通过归一化图像方差并将其转换为灰度来清理图像数据
❹ 将加载的结果数据保存到 NPY 缓存文件中,这样你就不需要重新计算它们
使用构建的数据加载管道函数,接下来我将解释如何处理 CIFAR-10 卷积神经网络(CNN)的数据增强。
15.1.2 执行数据增强
CIFAR-10 是一个静态数据集。该数据集经过极大的努力收集,并被全世界使用。目前没有人收集并向数据集中添加图像;相反,每个人都使用它原样。CIFAR-10 有 10 个物体类别,例如鸟类,对于这些物体,它捕捉了你在现实生活中预期到的许多变化,例如一只鸟在飞行中起飞,一只鸟在地上,或者可能是一只鸟啄食或做出一些传统进食动作。然而,数据集并没有包括所有鸟类及其可能做的所有变化。考虑一只鸟在飞行中上升到图像的左上角。你可以想象的一个类似图像是一只鸟在图像的右上角做同样的上升动作。或者图像可以从左到右翻转。你可以想象一只鸟在图像顶部飞行,但在底部不飞,翻转图像从上到下或旋转它。考虑看到一只远处的鸟与近距离的鸟,或者它在晴天或黄昏前有点暗。可能性是无限的!
如你所学,卷积神经网络(CNN)的任务是使用过滤器在网络上表示更高和更低阶的图像特征。这些特征受到前景、背景、物体位置、旋转等因素的严重影响。为了应对现实生活中图像中的可变特征,你使用数据增强。数据增强通过在训练过程中随机将这些变换应用于数据集中的图像,以表示图像中的变化,通过添加新的图像来增强你的静态数据。在足够的训练轮次和基于批大小(一个超参数)的情况下,你可以使用数据增强来显著增加数据集的变异性和可学习性。例如,将那只鸟的图像向上左方翻转,在部分训练轮次中将其翻转至右侧。将鸟在黄昏时分着陆的图像,在其他轮次中改变图像对比度使其更亮。这些变化使得网络能够在其学习参数中考虑图像的进一步变化,使其对现实生活中具有这些变化的不见图像更具鲁棒性——而无需收集新的图像,这可能成本高昂或可能不可能。
你将在列表 15.3 和 15.4 中实现这些增强中的一些。特别是,你将实现图像左右翻转;然后你将在图像中添加一些随机噪声,称为对比度。对比度也被称为盐和胡椒或黑白随机掩码像素,如列表 15.3 中的sp_noise函数所示。
列表 15.3 简单的盐和胡椒噪声图像
def sp_noise(image,prob): ❶
output = np.zeros(image.shape,np.float32)
thres = 1 - prob ❷
for i in range(image.shape[0]):
for j in range(image.shape[1]):
rdn = random.random()
if rdn < prob:
output[i][j] = 0.
elif rdn > thres:
output[i][j] = 255.
else:
output[i][j] = image[i][j]
return output ❸
❶ 向图像添加盐(0 值,或白色)和胡椒(255 值,或黑色)噪声。prob 是噪声的概率。
❷ 决定设置像素为黑色(255)或白色(0)的阈值
❸ 返回加盐的图像
您可以使用 NumPy 的random函数,然后为翻转图像的概率和盐和胡椒设置超参数,在列表 15.4 中将这两个参数都设置为 5%。您可以玩弄这些值(它们是超参数),因为它们控制您将生成多少额外的训练数据。更多的数据总是更好的,但它可以显著增加您的数据集。
列表 15.4 实现 CIFAR-10 图像的数据增强
def augment(img_data, img_labels):
imgs = img_data.reshape(img_data.shape[0], 3, 32, 32)
flip_pct = 0.05 ❶
salt_pct = 0.05 ❷
noise_pct = 0.15 ❸
orig_size = len(imgs)
for i in tqdm(range(0, orig_size)):
if random.random() < flip_pct: ❹
im_flip = np.expand_dims(np.flipud(imgs[i]), axis=0)
imgs = np.vstack((imgs, im_flip)) ❺
img_labels = np.hstack((img_labels, img_labels[i])) ❺
if random.random() < salt_pct: ❻
im_salt = np.expand_dims(sp_noise(imgs[i], noise_pct), axis=0)
imgs = np.vstack((imgs, im_salt))
img_labels = np.hstack((img_labels, img_labels[i]))
return imgs.reshape(imgs.shape[0], -1), img_labels ❼
❶ 每次翻转图像的概率为 5%
❷ 将盐和胡椒应用于图像的概率为 5%
❸ 图像中噪声或盐和胡椒的量(图像中的 15%)
❹ 随机翻转 UD(这将实际上执行左右翻转)
❺ 添加翻转图像和标签
❻ 为图像添加随机盐和胡椒噪声
❼ 返回增强的图像数据
您可以通过绘制训练数据中超过 50,000 张图像的随机图像来双重检查您的图像数据集是否已正确增强。我选择了 52,002,但您也可以检查其他图像。以下代码使用 Matplotlib 打印出 CIFAR-10 的 24 × 24 灰度增强图像,如图 15.3 所示。请注意,图像的计数从索引 0 开始;因此,图像 52,002 的索引是 52,001。

图 15.3 一张翻转(向右)的汽车图像,可能带有盐和胡椒增强的对比度
plt.figure()
plt.title("Image of "+str(names[labels[y]]))
img = np.reshape(data[52001, :], (24,24))
plt.imshow(img, cmap='Greys_r')
plt.axis('off')
plt.tight_layout()
现在,有了您的增强数据集,您就可以构建一个更深的 CIFAR-10 CNN 模型来捕捉这些图像特征。
15.2 为 CIFAR-10 构建更深的 CNN 架构
在第十四章中构建的 CNN 架构是一个浅层架构,仅包含两个卷积层和一个全连接层,最终连接到输出层以进行图像类别预测。CNN 是有效的,但如果您像我在前面的章节中展示的那样,通过生成接收者操作特征(ROC)曲线对其进行评估,它将不足以区分 CIFAR-10 测试图像的类别,并且在使用从互联网随机收集的未见数据评估时表现会更差。浅层架构将如何表现,我将留作您的练习。相反,在本节中,我将重点介绍如何构建更好的架构并评估其性能。
性能不佳的一个主要原因是与机器学习理论家所说的模型容量有关。在浅层架构中,网络缺乏捕获高阶和低阶图像特征之间差异的必要容量,这些特征可以正确区分图像类别。如果没有足够的权重让您的机器学习模型进行学习,模型就无法分离图像中的差异;因此,它无法正确区分不同类别的输入图像中的大小差异。
以青蛙与汽车为例。青蛙和汽车有一个相似的特征:图像底部有四个形状(图 15.4)。青蛙中的这些形状——脚——更平,并且根据坐姿,可以以不同的方式排列。在四轮汽车的四个轮胎上,这些形状是圆形的,并且更固定,不能像青蛙的脚那样重新定位。如果没有学习这些参数的能力,浅层架构的 CNN 无法区分这些特征,输入图像与汽车非常接近,输出神经元可能会在automobile类别上触发,而不是在frog类别上。通过额外的权重和神经元,网络可能会发现左上角的向外形状比汽车更能表明动物和青蛙,这是一个关键特征,将更新网络的理解,并导致它将图像标记为青蛙而不是汽车。

图 15.4 一个输入的 CIFAR-10 训练图像——青蛙——首先通过浅层网络,然后通过密集网络。浅层网络认为图像是汽车;密集网络学习到图像是青蛙。
如果你的神经网络架构缺乏学习这些变化的能力,即以权重形式根据训练数据和标签进行训练和更新,那么你的模型将无法学习这些简单的差异。你可以将模型过滤器视为可调节的参数,它们区分图像中的重大差异和较小差异。
即使是人类也可能需要一些强化
在撰写本书的过程中,我进行了一个简单的实验,得到了一些有趣的结果。我向我的家庭成员展示图 15.4 中的青蛙图片,并要求他们以无上下文的方式识别图像,没有告诉他们标签。
那是恐龙吗?尼斯湖水怪?回旋镖?
提供两个标签的选择——汽车或青蛙——产生了更好的结果。重点是,即使是拥有密集神经网络的人类,可能也需要标签重新训练或可能更多的数据增强。
AlexNet 模型为你提供了一个路线图,显示了需要多少特征来界定 CIFAR-10 数据集中的所有特征并显著提高其准确性。事实上,AlexNet 远远超出了 CIFAR-10;它具有足够的容量,如果给它足够的输入数据和训练,它可以在更大的 ImageNet 数据集上表现良好,该数据集有 1,000 个输入图像类别和数百万个训练样本。AlexNet 的架构足以作为具有所需容量的密集架构的模型,以改善 CIFAR-10 的模型准确性。
列表 15.5 并非完全等同于 AlexNet;我省略了一些滤波器,这将在不牺牲明显准确性的情况下减少训练时间和计算机内存的需求。在训练机器学习模型时,你将做出这类选择。其中一些选择将取决于你是否有高容量或云计算资源,或者你是否仅在笔记本电脑上工作。
函数 model() 以 CIFAR-10 图像、一个名为 keep_prob 的超参数以及一个名为 tf.nn.dropout 的函数作为输入,这些内容将在第 15.2.1 节中解释。模型构建使用了 TensorFlow 的二维卷积滤波器函数,它以四个卷积滤波器作为输入,如下所示:
conv1_filter = tf.Variable(tf.truncated_normal(shape=[3, 3, 1, 64], mean=0,
➥ stddev=0.08))
conv2_filter = tf.Variable(tf.truncated_normal(shape=[3, 3, 64, 128], mean=0,
➥ stddev=0.08))
conv3_filter = tf.Variable(tf.truncated_normal(shape=[5, 5, 128, 256],
➥ mean=0, stddev=0.08))
conv4_filter = tf.Variable(tf.truncated_normal(shape=[5, 5, 256, 512],
➥ mean=0, stddev=0.08))
回顾第十四章的内容,滤波器的形状为 [x, y, c, n],其中 [x, y] 是卷积块的大小,c 是图像通道数(灰度图为 1),n 是滤波器的数量。因此,你的密集模型使用了四个滤波器,前两层使用 3 × 3 的窗口块大小,后两层使用 5 × 5,灰度输入使用 1 个通道,第一层卷积使用 64 个滤波器,第二层使用 128 个,第三层使用 256 个,第四层使用 512 个。
注意:这些网络架构属性的选择在 Krizhevsky 的论文中有详细阐述。但我要指出,这是一个活跃的研究领域,并且最好留给最新的机器学习论文来构建神经网络架构。在本章中,我们将遵循规定的模型。
模型构建的后半部分使用了一些高级 TensorFlow 函数来创建神经网络层。函数 tf.contrib.layers.fully_connected 创建全连接层来解释学习到的特征参数并输出 CIFAR-10 图像标签。
首先,将输入图像从 CIFAR 的 32 × 32 × 1(1024 像素)重塑为 [24 × 24 × 1] 输入,主要是为了减少参数数量并减轻本地计算资源的负担。如果你有一台比笔记本电脑更强大的机器,你可以尝试调整这个参数,所以请随意调整大小。对每个卷积滤波器,你将应用 1 像素步长和相同的填充,并通过 ReLU 激活函数将输出转换为神经元。每一层都包括最大池化,通过平均进一步减少学习到的参数空间,并且你将使用批量归一化来使每一层的统计信息更容易学习。卷积层被展平成一个代表全连接层的 1D 参数层;然后它们被映射到最终的 10 类 softmax 输出预测,以完善模型。
列表 15.5:基于 AlexNet 的 CIFAR-10 密集 CNN 架构
def model(x, keep_prob):
x_reshaped = tf.reshape(x, shape=[-1, 24, 24, 1]) ❶
conv1 = tf.nn.conv2d(x_reshaped, conv1_filter, strides=[1,1,1,1],
➥ padding='SAME') ❷
conv1 = tf.nn.relu(conv1) ❸
conv1_pool = tf.nn.max_pool(conv1, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME') ❹
conv1_bn = tf.layers.batch_normalization(conv1_pool ❺
conv2 = tf.nn.conv2d(conv1_bn, conv2_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv2 = tf.nn.relu(conv2)
conv2_pool = tf.nn.max_pool(conv2, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME')
conv2_bn = tf.layers.batch_normalization(conv2_pool)
conv3 = tf.nn.conv2d(conv2_bn, conv3_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv3 = tf.nn.relu(conv3)
conv3_pool = tf.nn.max_pool(conv3, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME')
conv3_bn = tf.layers.batch_normalization(conv3_pool)
conv4 = tf.nn.conv2d(conv3_bn, conv4_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv4 = tf.nn.relu(conv4)
conv4_pool = tf.nn.max_pool(conv4, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME')
conv4_bn = tf.layers.batch_normalization(conv4_pool)
flat = tf.contrib.layers.flatten(conv4_bn) ❻
full1 = tf.contrib.layers.fully_connected(inputs=flat, num_outputs=128,
➥ activation_fn=tf.nn.relu)
full1 = tf.nn.dropout(full1, keep_prob)
full1 = tf.layers.batch_normalization(full1)
full2 = tf.contrib.layers.fully_connected(inputs=full1, num_outputs=256,
➥ activation_fn=tf.nn.relu)
full2 = tf.nn.dropout(full2, keep_prob)
full2 = tf.layers.batch_normalization(full2)
full3 = tf.contrib.layers.fully_connected(inputs=full2, num_outputs=512,
➥ activation_fn=tf.nn.relu)
full3 = tf.nn.dropout(full3, keep_prob)
full3 = tf.layers.batch_normalization(full3)
full4 = tf.contrib.layers.fully_connected(inputs=full3, num_outputs=1024,
➥ activation_fn=tf.nn.relu)
full4 = tf.nn.dropout(full4, keep_prob)
full4 = tf.layers.batch_normalization(full4)
out = tf.contrib.layers.fully_connected(inputs=full4, num_outputs=10,
➥ activation_fn=None) ❼
return out
❶ 重塑为 [24 × 24 × 1] 输入
❷ 应用滤波器
❸ 为神经元使用 ReLU 激活函数
❹ 应用最大池化
❺ 使用批量归一化使每层的统计信息更容易学习
❻ 将神经元展平为 1D 层
❼ 将隐藏层神经元映射到最终的 10 类 softmax 输出预测
现在你已经构建了模型,我将向你展示如何使其在训练过程中对输入的变化更具抵抗力。
15.2.1 增强学习参数弹性的 CNN 优化
模型函数还使用了两种其他优化:批量归一化和 dropout。关于批量归一化的效用和目的有大量的更详细的数学解释,我将留给你们去研究。简单来说,批量归一化是一个数学函数,确保每个层的所学参数更容易训练,并且不会过度拟合输入图像,这些图像本身已经归一化(转换为灰度,除以图像均值等)。简而言之,批量归一化简化了训练并加速了模型收敛到最佳参数。好消息是,TensorFlow 隐藏了所有的数学复杂性,并为你提供了一个易于使用的实用函数来应用这项技术。
提示:如果你想了解更多关于批量归一化的信息,请考虑这篇信息丰富的文章:mng.bz/yrxB。
对于 dropout,关键直觉与数据增强类似,即你希望使 CNN 学习到的参数对输入中的不完美(更不敏感)具有更强的抵抗力,就像在现实生活中一样。即使你眯着眼睛,扭曲你看到的图像,你仍然可以辨认出其中的物体,直到一定程度。这种现象意味着你可以扭曲图像,同时仍然加强学习到的标签。Dropout 通过在训练过程中强制网络随机忘记或掩盖内部神经元的学习值,同时在训练过程中加强输出标签,更进一步。Dropout 在训练过程中以概率(1-keep_prob)随机关闭神经元,其中keep_prob是列表 15.5 中输入到model()函数的第二个参数。
Dropout 会导致网络学习到更加坚固的权重和参数,这些权重和参数对训练过程中的不完美或变化具有抵抗力。因此,类似于在第十二章中看到的对图像进行噪声处理,尽管存在扭曲,但仍然学习到更鲁棒的权重,dropout 操作类似,并掩盖了网络的内部世界。换句话说,dropout 在训练过程中随机禁用其自身的隐藏神经元,因此它所学习到的最终结果对输入更加有弹性。这项技术在神经网络构建中已被证明是有效的,因为随着足够多的时代和训练时间的增加,网络学会处理内部故障,并仍然能够回忆正确的标签和调整参数。
你是否曾经忘记了一些后来又记得的事情?Dropout 帮了忙!
这对我来说经常发生。那天,我在和妻子和孩子讨论了我的日常散步中发生的事情后,试图回忆一个输入图像的标签,婴儿车。我不断地把婴儿车称为“推车”。最终我集中了注意力,然后就想起来了:婴儿车。我肯定妻子没有给我任何强化,她肯定在内心狂笑。多年来,我确信我的内部网络层已经设置了 dropout 以允许回忆,即使不是立即的。生物启发的计算机模型不是非常棒吗?
现在是时候训练你的优化后的卷积神经网络(CNN)了,看看你是否能比第十四章训练的初始浅层网络做得更好。
15.3 训练和应用更好的 CIFAR-10 CNN
在精心整理的 CIFAR-10 输入数据集、数据增强、清理和归一化完成后,以及有了你具有生物启发性的 CNN 模型,你就可以使用 TensorFlow 进行一些训练了。考虑图 15.5 中突出显示的构建 CNN 的优化步骤。到目前为止,我已经介绍了如何获得最佳的训练数据以及确保训练容量、内存、弹性和收敛的一些模型表示步骤。

图 15.5 展示构建 CNN 的优化步骤以及在每个阶段从数据到学习模型你可以做什么
训练步骤也有你可以在之前章节中找到的优化方法。你可能还记得,正则化是一种在训练中通过惩罚非最优参数值探索来影响更好学习参数的技术。正则化适用于输入图像以及数值和文本输入。这正是机器学习和 TensorFlow 的美丽之处:一切都是输入张量或数字矩阵。
列表 15.6 通过将 CIFAR 32 × 32 RGB 图像重新缩放为 24 × 24 灰度图来设置训练过程。训练设置如下:
-
使用 1000 个周期和 0.001 的学习率
-
以 30%的概率丢弃神经元,使用 L2 正则化
-
应用 AdamOptimizer 技术
选择这些可调训练参数的理由可以成为它们自己的章节,并且已经在其他地方详细介绍了。正如我在书中提到的,你应该通过改变这些值来实验,看看它们如何影响训练过程中的整体体验:
-
训练时间的长度
-
处理时使用的 GPU 和内存量
-
收敛到最优结果的能力
目前,这些参数将允许你在没有 GPU 的笔记本电脑上完成训练。不过,我要提醒你:训练这样一个密集的网络可能需要一天或更长时间。
列表 15.6 为你的 CIFAR-10 密集 CNN 设置训练过程
tf.reset_default_graph() ❶
names, data, labels = read_data('./cifar-10-batches-py')
x = tf.placeholder(tf.float32, [None, 24 * 24], name='input_x') ❷
y = tf.placeholder(tf.float32, [None, len(names)], name='output_y') ❸
keep_prob = tf.placeholder(tf.float32, name='keep_prob') ❹
epochs = 1000 ❺
keep_probability = 0.7 ❻
learning_rate = 0.001 ❻
model_op = model(x, keep_probability) ❻
model_ops = tf.identity(model_op, name='logits') ❻
beta = 0.1 ❼
weights = [conv1_filter, conv2_filter, conv3_filter, conv4_filter] ❼
regularizer = tf.nn.l2_loss(weights[0]) ❼
for w in range(1, len(weights)): ❼
regularizer = regularizer + tf.nn.l2_loss(weights[w]) ❼
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=model_op,❼
➥ labels=y)) ❼
cost = tf.reduce_mean(cost + beta * regularizer) ❼
train_op = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost) ❽
correct_pred = tf.equal(tf.argmax(model_op, 1), tf.argmax(y, 1)) ❾
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32), ❾
➥ name='accuracy') ❾
❶ 移除之前的权重、偏差和输入
❷ 大小为(50000,576)的输入,或 50,000 个 24 × 24 的图像
❸ 大小为(50000,10)的输入,或 10 个类别的 50,000 个类别标签
❹ 定义 dropout 的超参数。应用此操作层的神经元将以 1-keep_prob 的概率被设置为 0。
❷ 使用学习率 0.001 进行 1,000 个 epoch 的训练
❻ 定义模型并允许您在训练后从磁盘通过名称‘logits’查找它
❷ 通过将权重相加,应用超参数 beta,并将其添加到 L2 正则化的成本中来实现正则化
❽ 使用 AdamOptimizer
❾ 测量准确率和正确预测的数量
现在训练过程已经定义,您可以训练您的模型(列表 15.7)。我包括了一些选项,如果您的系统中有 GPU,可以利用 GPU。注意,如果您有 GPU,训练可以在几小时内完成,而不是几天。列表代码还保存了您的训练模型,以便您可以加载它进行预测并生成 ROC 曲线进行评估。
列表 15.7 执行 CIFAR-10 的深度 CNN 训练
config = tf.ConfigProto() ❶
config.gpu_options.allow_growth = True ❶
with tf.Session(config=config) as sess: ❶
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver() ❷
onehot_labels = tf.one_hot(labels, len(names), on_value=1., off_value=0.,❸
➥ axis=-1) ❸
onehot_vals = sess.run(onehot_labels) ❸
batch_size = len(data) // 200 ❹
print('batch size', batch_size) ❹
for j in tqdm(range(0, epochs)): ❺
for i in range(0, len(data), batch_size):
batch_data = data[i:i+batch_size, :]
batch_onehot_vals = onehot_vals[i:i+batch_size, :]
_, accuracy_val = sess.run([train_op, accuracy],
➥ feed_dict={x:batch_data, y: batch_onehot_vals})
print(j, accuracy_val)
saver.save(sess,
➥ './cifar10-cnn-tf1n-ia-dropout-reg-dense-'+str(epochs)+'epochs.ckpt') ❻
❶ 允许使用 GPU 并允许 GPU 内存增长
❷ 为您的 TensorFlow 模型创建一个保存器以将其存储到磁盘
❸ 将 10 个 CIFAR-10 标签名称转换为 one-hot 标签
❹ 将训练数据分成批次(由于数据增强,批次大小可能超过 200)
❺ 使用 TQDM 库进行训练并可视化进度
❻ 保存模型
按照科学界的说法,去拿一杯咖啡吧。这段代码运行起来会花费一些时间,因为您的网络正在执行以下操作:
-
自动生成输入数据和图像的附加示例
-
通过外部增强和内部随机关闭 30%的神经元,学习更健壮的表示
-
使用其四个卷积滤波器捕捉更多变化,并区分 CIFAR-10 图像类别
-
通过正则化和批量归一化训练更快,并且有更高的概率找到最优权重
15.4 测试和评估您的 CNN 对 CIFAR-10
好吧,几个小时或可能几天后,您回来了。我知道,我知道——为什么我要让你受这样的折磨?是的,我为在您的笔记本电脑上运行这段代码导致所有其他程序崩溃而道歉。至少你现在有了训练好的模型,是时候尝试它了。
为了做到这一点,您需要一个预测函数。使用您学习到的模型进行预测涉及一些与训练类似的步骤。首先,您需要确保您的输入是一个 24 × 24 的单通道灰度图像。如果是这样,您可以从磁盘加载您的训练模型,并通过运行输入图像通过它来获取一些关键信息:
-
输出 logits,您将运行
tf.nn.softmax以从 CIFAR-10 的 10 个图像类别中获得预测。 -
具有最高 softmax 值的维度是预测的输出类别。您可以通过
np.argmax调用获取此维度,它返回行中最高值列的索引。此预测的关联置信度是 softmax 值的输出。np.argmax调用获取并返回所选最高置信度类别(class_num)、其名称(bird、automobile等)、置信度或 softmax 值以及所有类别的完整置信度集合。列表 15.8 创建了predict函数,允许您调用分类器。
列表 15.8:从输入图像预测 CIFAR-10 类别
def predict(img_data):
class_num, class_name, confidence = None, None, 0.
with tf.Session() as sess:
loaded_graph = tf.Graph() ❶
with tf.Session(graph=loaded_graph) as sess:
loader = tf.train.import_meta_graph('./cifar10-cnn-tf1n-ia- ❷
➥ dropout-reg-dense-'+str(epochs)+'epochs.ckpt' + '.meta') ❷
loader.restore(sess, './cifar10-cnn-tf1n-ia-dropout-reg-dense- ❷
➥ '+str(epochs)+'epochs.ckpt') ❷
loaded_logits = loaded_graph.get_tensor_by_name('logits:0') ❸
logits_out = sess.run(tf.nn.softmax(loaded_logits), ❹
➥ feed_dict={'input_x:0': img_data.reshape((1, 24*24))}) ❹
class_num = np.argmax(logits_out, axis=1)[0] ❺
class_name = names[class_num] ❺
confidence = logits_out[0,class_num] ❺
all_preds = logits_out ❺
return (class_num, class_name, confidence, all_preds) ❺
❶ 获取默认 TensorFlow 图的指针
❷ 将模型加载到图中
❸ 从加载的模型获取张量
❹ 使用其学习到的权重运行模型;获取输出 logits 并在其上运行 softmax 函数
❺ 返回最高置信度类别编号、名称和预测结果
您可以通过将预测函数应用于 CIFAR-10 的第三张训练图像(鹿,图 15.6)来测试您的预测函数。

图 15.6 来自 CIFAR-10 的鹿的图像
以下代码加载模型并获取类别编号、名称、预测置信度和该图像的完整预测集合:
class_num, class_name, confidence, all_preds =
➥ predict(data[3])
print('Class Num', class_num)
print('Class', class_name)
print('Confidence', confidence)
print('All Predictions', str(all_preds))
因为是 softmax,所以您会得到模型对 CIFAR-10 所有 10 个类别的预测置信度。所以您的模型有 93%的置信度(0.9301368值)认为这张图像是鹿的图片。下一个最高置信度是类别 6(frog)置信度约为 3%——类别 2(bird)和类别 5(dog)在约 1%的置信度上形成虚拟平局。deer与后三个类别之间的置信度下降在统计上具有显著性(90%+点):
INFO:tensorflow:Restoring parameters from ./cifar10-cnn-tf1n-ia-dropout-reg-
➥ dense-1000epochs.ckpt
Class Num 4
Class deer
Confidence 0.9301368
All Predictions [[6.24996528e-06 5.14547166e-04 1.39211295e-02 6.32673642e-03
9.30136800e-01 1.21700075e-02 3.20204385e-02 4.65520751e-03
2.48217111e-05 2.24148083e-04]]
该结果是对训练数据中单个图像的很好的置信度提升,但您的经过优化的深度 CNN 在 CIFAR-10 未见测试数据上的表现如何?您可以构建一个简单的评估函数,该函数将在所有测试数据上运行您的新模型并输出预测准确率。您可以以相同的方式加载模型。这次,给模型提供完整的 10,000 个测试图像和 10,000 个测试标签(每类 1,000 个);计算模型预测正确的次数;每次正确预测存储1,否则存储0。因此,整体准确率是所有图像预测数组的平均值。列表 15.9 包含对 CIFAR-10 测试数据深度 CNN 的完整评估。
列表 15.9:在 CIFAR-10 测试数据上运行您的深度 CNN
def get_test_accuracy(test_data, test_names, test_labels):
class_num, class_name, confidence = None, None, 0.
with tf.Session() as sess:
loaded_graph = tf.Graph()
with tf.Session(graph=loaded_graph) as sess:
loader = tf.train.import_meta_graph('./cifar10-cnn-tf1n-ia-
➥ dropout-reg-dense-'+str(epochs)+'epochs.ckpt' + '.meta')
loader.restore(sess, './cifar10-cnn-tf1n-ia-dropout-reg-dense- ❶
➥ '+str(epochs)+'epochs.ckpt') ❶
loaded_x = loaded_graph.get_tensor_by_name('input_x:0') ❷
loaded_y = loaded_graph.get_tensor_by_name('output_y:0') ❷
loaded_logits = loaded_graph.get_tensor_by_name('logits:0') ❷
loaded_acc = loaded_graph.get_tensor_by_name('accuracy:0') ❷
onehot_test_labels = tf.one_hot(test_labels, len(test_names), ❸
➥ on_value=1., off_value=0., axis=-1).eval() ❸
test_logits_out = sess.run(tf.nn.softmax(loaded_logits), ❸
➥ feed_dict={'input_x:0': test_data, "output_y:0" ❸
➥ :onehot_test_labels, "keep_prob:0": 1.0}) ❸
test_correct_pred = tf.equal(tf.argmax(test_logits_out, 1), ❹
➥ tf.argmax(onehot_test_labels, 1)) ❹
test_accuracy = tf.reduce_mean(tf.cast(test_correct_pred, ❺
➥ tf.float32)) ❺
print('Test accuracy %f' % (test_accuracy.eval())) ❺
predictions = tf.argmax(test_logits_out, 1).eval() ❻
return (predictions, tf.cast(test_correct_pred, ❻
➥ tf.float32).eval(), onehot_test_labels) ❻
❶ 加载模型
❷ 从加载的模型获取张量
❸ 根据输入测试图像和测试标签应用模型
❹ 计算模型与 one-hot 测试标签一致次数,每次正确预测存储 1,否则存储 0
❺ 通过计算平均值来衡量平均准确率
❻ 返回预测结果、正确预测次数和 one-hot 测试标签
使用以下简单调用运行列表 15.9:
predict_vals, test_correct_preds, onehot_test_lbls =
➥ get_test_accuracy(test_data, test_names, test_labels)
命令产生以下输出:
Test accuracy 0.647800
接下来,我将讨论如何评估你的 CNN 的准确性。一个熟悉的技术将再次出现:ROC 曲线。
15.4.1 CIFAR-10 准确率结果和 ROC 曲线
通常,在 10,000 张图像上产生大约 65% 的测试准确率不会感觉很好。你是否通过训练一个具有优化的深度 CNN 来改进了你的模型?实际上,你必须深入挖掘才能找到答案,因为你的模型准确率不仅仅是针对特定图像类别的对或错;每次,你都在预测 10 个类别,因为它是 softmax,得到一个错误的标签可能并不那么错误。
假设标签是 bird,而你的模型预测的最高置信度类别标签为 deer,置信度为 93%,但其次高置信度,91%,为 bird?当然,你的测试答案错了,但你并没有错得太离谱。如果你的模型在所有剩余类别上都表现出较低的不确定性,你可以说它在整体上表现良好,因为前两个预测中有一个是正确的。将这个结果扩展出去,如果它总是发生,你将会有很差的总体准确率。但考虑到 top-k(k 是一个超参数)预测,你的模型表现相当好,并且对正确的图像类别敏感。也许模型缺少区分鹿和鸟的能力,或者也许你没有足够的训练示例或数据增强来让模型区分它们。
你应用 ROC 曲线来评估预测的真阳性率与假阳性率,并查看所有类别的微观平均值以进行评估。ROC 曲线显示了你的模型在区分 CIFAR-10 测试数据中的类别时的表现,这是多类分类问题模型性能的一个更合适的衡量标准。你可以使用我在这本书中展示的友好的 Matplotlib 和 SK-learn 库。
SK-learn 库提供了基于假阳性率(fpr)和真阳性率(tpr)计算 ROC 曲线以及计算曲线下面积(AUC)的功能。然后 Matplotlib 提供了绘图和图形功能来显示结果。注意列表 15.10 中的 np.ravel() 函数,它提供 ROC 曲线生成;绘图代码用于返回一个连续的扁平化 NumPy 数组。运行列表 15.10 中 ROC 曲线生成代码的输出如图 15.7 所示。

图 15.7 你的 CIFAR-10 深度 CNN 模型的 ROC 曲线。总体而言,它在所有类别上表现相当好,除了 cat、bird 和 deer。
ROC 曲线显示,该模型在大多数类别上表现优秀,在少数类别上(cat、bird 和 deer)表现优于平均水平。
列表 15.10 CIFAR-10 ROC 曲线
from sklearn.preprocessing import label_binarize
from sklearn.metrics import roc_curve, auc
outcome_test = label_binarize(test_labels, classes=[0, 1, 2, 3, 4, 5, 6, 7, ❶
➥ 8, 9]) ❶
predictions_test = label_binarize(predict_vals, classes=[0, 1, 2, 3, 4, 5, 6, ❶
➥ 7, 8, 9]) ❶
fpr = dict() ❷
tpr = dict()
roc_auc = dict()
for i in range(n_classes):
fpr[i], tpr[i], _ = roc_curve(outcome_test[:, i], predictions_test[:, i])
roc_auc[i] = auc(fpr[i], tpr[i])
fpr["micro"], tpr["micro"], _ = roc_curve(outcome_test.ravel(),
➥ predictions_test.ravel()) ❸
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"]) ❹
plt.figure() ❺
plt.plot(fpr["micro"], tpr["micro"],
label='micro-average ROC curve (area = {0:0.2f})'
''.format(roc_auc["micro"]))
for i in range(n_classes):
plt.plot(fpr[i], tpr[i], label='ROC curve of class {0} (area = {1:0.2f})'
''.format(test_names[i], roc_auc[i]))
plt.plot([0, 1], [0, 1], 'k—')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
roc_mean = np.mean(np.fromiter(roc_auc.values(), dtype=float))
plt.title('ROC curve for CIFAR-10 CNN '+str(epochs)+' iter Tensorflow (area =
➥ %{0:0.2f})'.format(roc_mean))
plt.legend(loc="lower right")
plt.show() ❻
❶ 使用 SK-learn 的 label_binarize 函数创建单热预测值和测试标签以进行比较
❷ 计算每个类别的 ROC 曲线和 ROC 面积
❸ 计算微观平均 ROC 曲线和 ROC 面积
❹ 特定类别的 ROC 曲线图
❺ 绘制 ROC 曲线
❻ 显示所有类别的微观平均 ROC 曲线
因此,尽管您的模型测试准确率让您有些失望(约 65%),但该模型在所有类别上的整体微观平均 ROC 达到了 80%。此外,该模型在所有类别上除了猫、鸟和鹿之外都表现出色。该模型在区分不同的图像类别方面做得很好,并且对于前两个或三个预测,即使它并不总是正确地选择第一个,也可能会对某些预测有信心。我将在 15.4.2 节中更深入地探讨这个话题。
15.4.2 评估每个类别的 softmax 预测
您可以通过查看每个类别的 softmax 预测来进一步了解模型在尝试决定类别。想象一下,对于每张图像预测,都有一个水平条形图,其中每个可能的预测类别的 softmax 条值在0到1之间。基于 softmax 的条形图就像观察您的模型试图确定哪个类别最正确。softmax 过程还显示了模型在顶级预测中决定哪些类别,以及它丢弃或对哪些类别不自信的类别。
经过一些努力,您可以创建一个函数来显示此图表。您可能不想在成千上万张图像上运行该函数,但您可以在训练和测试集之外的一些图像上测试模型。为此,我整理了一个包含九个随机图像 URL 的列表,这些图像来自 CIFAR-10 的六个类别。这些图像包括一只青蛙、三艘船、两辆卡车、一只猫、一匹马和一辆车。简单的 URL(列表 15.11)后面跟着对predict函数的调用,以在每个图像上运行您的模型。
列表 15.11 来自互联网的未见过 CNN 评估 URL
predict_urls = [
'http://www.torontozoo.com/adoptapond/guide_images/Green%20Frog.jpg',
'https://cdn.cnn.com/cnnnext/dam/assets/160205192735-01-best-cruise-
➥ ships-disney-dream-super-169.jpg',
'https://www.sailboston.com/wp-content/uploads/2016/11/amerigo-
➥ vespucci.jpg',
'https://upload.wikimedia.org/wikipedia/commons/d/d9/Motorboat_at_Kankaria_
➥ lake.JPG',
'https://media.wired.com/photos/5b9c3d5e7d9d332cf364ad66/master/pass/
➥ AV-Trucks-187479297.jpg',
'https://images.schoolspecialty.com/images/1581176_ecommfullsize.jpg',
'https://img.purch.com/w/660/aHR0cDovL3d3dy5saXZlc2NpZW5jZS5jb20vaW1hZ2VzL2kv
➥ MDAwLzEwNC84MTkvb3JpZ2luYWwvY3V0ZS1raXR0ZW4uanBn',
'https://thehorse.com/wp-content/uploads/2017/01/iStock-510488648.jpg',
'http://media.wired.com/photos/5d09594a62bcb0c9752779d9/master/w_2560%2Cc_lim
➥ it/Transpo_G70_TA-518126.jpg'
]
注意,列表 15.12 中的predict函数已经略有改动,以准备在线图像供您的网络使用(通过 OpenCV 库将其转换为 24 × 24 灰度)以及使用 SK-learn 库及其imread函数读取图像。
列表 15.12 用于野外图像的predict函数
from skimage.io import imread
def predict_img_url(url):
image = color.rgb2gray(imread(url)) ❶
new_size = 24,24
image = cv2.resize(image, new_size, interpolation=cv2.INTER_CUBIC) ❷
images = np.expand_dims(image, axis=0)
im_data = images.astype(np.float32)
prediction = predict(im_data[0]) ❸
return prediction
❶ 从指定的 URL 读取图像并将其转换为灰度
❷ 使用双三次插值和 OpenCV 将图像重缩放为 24,24
❸ 将准备好的图像与您的网络运行并返回预测
以下代码片段在所有随机图像上运行该函数:
preds=[]
for url in predict_urls:
pred = predict_img_url(url)
preds.append(pred)
在做出预测并返回 softmax 值后,您可以创建一个evaluate_model函数,该函数执行以下操作:
-
从互联网抓取图像数据并重缩放为 24,24 灰度以显示每张图像
-
将输出 softmax 预测直接显示在重缩放后的灰度图像旁边,以显示网络对每个类别的信心程度
列表 15.13 展示了evaluate_model函数,图 15.8 显示了几个图像的部分输出截图。模型似乎对它是否在分类动物、物体或车辆方面很敏感,但在第三艘船的情况下,它在每个类别的第二和第三次猜测之间有些难以决定。这项测试可以用来评估您的模型对特定图像特征的敏感性。使用如图 15.8 所示的每个类别的置信度图来评估您的 CNN 模型,可以为通过扩展容量以捕获缺失特征来调整 CNN 提供指导。

图 15.8 展示了evaluate_model函数的输出,显示了模型在四个图像 URL 及其相关类别标签之间进行决策
测试也可能表明您需要更改数据增强方法或调整 dropout 或正则化的超参数。像evaluate_model这样的函数对于改进您的深度 CNN 以及提供调试和调查它的路线图是必不可少的。
列表 15.13 中的代码生成了图 15.8。首先,它使用 OpenCV 和 SK-learn 库将图像转换为灰度;接下来,将图像调整大小为 24 × 24。然后,evaluate_model函数将图像垂直堆叠成一个矩阵。对于每个预测,该函数显示要分类的图像;右侧是模型为它所知的每个类别推导出的 softmax 预测的横向条形图。
列表 15.13 CIFAR-10 的evaluate_model函数
def evaluate_model(urls, predicted):
im_data = []
for url in urls: ❶
image = color.rgb2gray(imread(url))
new_size = 24,24
image = cv2.resize(image, new_size, interpolation=cv2.INTER_CUBIC)
images = np.expand_dims(image, axis=0)
if len(im_data) > 0:
im_data = np.vstack((im_data, images.astype(np.float32)))
else:
im_data = images.astype(np.float32)
n_predictions = len(predicted)
fig, axies = plt.subplots(nrows=n_predictions, ncols=2, figsize=(24, 24))
fig.tight_layout()
fig.suptitle('Softmax Predictions for '+str(len(predicted))+' CIFAR-10
➥ CNN '+str(epochs)+' iter Image URLs', fontsize=20, y=1.1)
n_predictions = 10
margin = 0.05
ind = np.arange(n_predictions)
width = (1\. - 2\. * margin) / n_predictions
for i in range(0, len(im_data)): ❷
pred_names = names ❷
pred_values = predicted[i][3][0] ❷
correct_name = predicted[i][1] ❷
axies[i][0].imshow(im_data[i], cmap='Greys_r') ❷
axies[i][0].set_title(correct_name)
axies[i][0].set_axis_off()
axies[i][1].barh(ind + margin, pred_values, width) ❸
axies[i][1].set_yticks(ind + margin)
axies[i][1].set_yticklabels(pred_names)
axies[i][1].set_xticks([0, 0.5, 1.0])
❶ 准备图像
❷ 显示图像及其预测的类别名称
❸ 在图像旁边显示条形图
现在您已经创建了一个在 CIFAR-10 上具有 80%微平均 ROC 精度的模型,您在第十四章构建的浅层 CNN 上取得了显著改进。好消息是,这些改进也可以转化为类似的问题:人脸识别,这是一个具有类似结构的另一个图像分类问题。您将提供尺寸为 244 × 244 的图像作为输入,其中包含 2,622 位名人的图像,并尝试将这些标签分类为这 2,622 位名人之一。您在本章中学到的所有内容都可以帮助您创建 VGG-Face 模型。
15.5 构建用于人脸识别的 VGG-Face
面部识别问题已经研究了数十年,近年来由于各种原因而成为新闻焦点。2015 年,牛津大学的视觉几何组(VGG),在为 ImageNet 挑战赛创建深度 CNN 网络后,试图将它的 CNN 网络重新应用于名人面部识别问题。VGG 小组成员撰写了一篇开创性的论文,名为“深度面部识别”,并发布了他们通过深度 CNN 识别名人脸部的成果。该论文可在 mng.bz/MomW 获取。
作者——Omkar M. Parkhi、Andrea Vedaldi 和 Andrew Zisserman——构建了一个包含 2,622 张名人脸部图像的数据集(其中一些如图 15.9 所示),这些图像具有不同的姿态和背景。在初步收集后,数据集进一步筛选,使用人工整理员对每位名人的 1,000 个 URL 进行排序和整理。最终,作者创建了一个包含 2,622,000 张图像的数据集,用于在深度 CNN 中检测名人脸部。该网络使用了 13 个卷积滤波器,由 37 层组成,最后一层是一个全连接层,输出一个 softmax 概率值,对应于输入图像对应于 2,622 位名人中的哪一位。

图 15.9 一些你在 VGG-Face 数据集中会看到的脸部和姿态。
在我尝试为这本书重新创建这个网络的过程中,我发现了一些挑战,我将为您总结:
-
超过 50% 的数据,主要来自 2015 年,已经不再存在。VGG 小组发布了他们使用的数据的 URL,但互联网已经发展,所以这些 URL 指向的图像已经不存在了。
-
收集剩余存在的数据——大约 1,250,000 张图像——需要复杂的爬取技术、URL 验证,并在几周内使用超级计算机,结合试错和人工整理。
-
结果数据每个类别的平均图像样本数量约为 ~477 张图像——远少于原始的每个类别 1,000 张图像,这使得数据增强变得更加必要,但也降低了其有效性。
-
即使我收集的这个更新的 VGG-Face Lite 数据集也有 ~90 GB,这非常庞大,难以在笔记本电脑上运行,而且无法放入内存。此外,数据集的大小严重限制了批处理大小参数,因为笔记本电脑、GPU,甚至超级计算机都没有无限的内存。
-
处理大小为 244 × 244 且包含全彩 RGB 通道的图像需要深度网络及其 13 个滤波器来捕捉区分众多输出类别(2,622 个)所需的高阶和低阶特征。
我可以列出许多其他问题,包括收集此数据集的更新版本、基于 VGG-Face 论文测试和构建深度 CNN 的问题,但我不打算这么做。在这里总结数据收集问题不会增加太多色彩,除了我已经提到的关于数据清洗、增强和准备对机器学习重要性的观点。
为什么机器学习研究人员不提供他们的数据?
简短的回答是,这很复杂。本可以下载原始的 2015 年 VGG-Face 数据集(包含 200 万张图像)并开始训练,而不是不得不重新收集剩余的子集。但可能存在与开放数据收集及其使用相关的法律和其他问题。许多图像数据集只提供图像 URL,或者如果你得到了图像,它们是包含大量法律条款的小子集。这种情况使得难以复制机器学习模型,并且至今仍是困扰社区的难题。唯一的解决方案是准备一个精心整理的数据集,并提供重新收集它的配方。
好消息是,我已经有一个你可以用来执行面部识别和构建你自己的 VGG-Face 版本的数据集,我将称之为 VGG-Face Lite,这是一个包含四位名人的子集,可以在你的电脑上运行并展示架构。然后我会向你展示如何使用完整的模型通过 TensorFlow 的 Estimator API 进行预测。
15.5.1 选择 VGG-Face 的子集进行训练 VGG-Face Lite
我根据平均样本数量从更新的 VGG-Face 数据集中随机选择了一组四位名人,试图找到模型特征、背景和学习性的代表性子集。我还可以选择其他四组,但为了训练模型,这一组效果很好。我使用了四位随机名人来训练模型。你可以在mng.bz/awy7获取包含 244 × 244 图像的小型 VGG-Face 数据集子集。
该子集包含 1,903 张总图像,这些图像被组织在包含名人名字首字母和姓氏首字母连接下划线的目录中:Firstname_Lastname。将图像解压缩到名为 vgg_face 的顶级文件夹中。
在本章的早期部分,你开发了用于图像数据集增强的功能,使用较低级别的 NumPy 函数引入盐和胡椒噪声以及左右翻转图像。这次,我将向你展示如何使用 TensorFlow 强大的功能来完成同样的事情。我还会介绍 TensorFlow 的强大 Dataset API,它提供了批处理、在 epoch 中重复数据以及将数据和标签组合成强大的学习结构的原生功能。与 Dataset API 相辅相成的是 TensorFlow 通过其图结构支持数据增强,我将在 15.5.2 节中向你展示。
15.5.2 TensorFlow 的 Dataset API 和数据增强
无论您是否使用 TensorFlow 的原生结构来迭代数据集或为机器学习准备它们,或者您是否结合了 SK-learn 和 NumPy 等库的功能,TensorFlow 都能与训练和预测任务的结果很好地协同工作。
探索 TensorFlow 在这个领域的功能是值得的。TensorFlow 提供了一个强大的 Dataset API,它利用其优秀的属性进行懒加载评估和基于图的操作,用于数据集的准备和处理。这些功能在批处理、数据增强、周期管理以及其他准备和训练任务中非常有用,您可以通过对 Dataset API 进行几次调用来完成这些任务。
首先,您需要一个 TensorFlow 数据集。您将开始准备这个数据集,通过收集 1,903 张图像和 4 位名人的初始图像路径(列表 15.14)。这些图像以 index_244x244.png 的形式存在,并存储在 BGR(蓝色、绿色、红色)格式中。
列表 15.14 收集 VGG-Face Lite TensorFlow 数据集的图像路径
data_root_orig = './vgg-face'
data_root = pathlib.Path(data_root_orig)
celebs_to_test = [‘CelebA, 'CelebB', 'CelebC', 'CelebD'] ❶
all_image_paths = []
for c in celebs_to_test:
all_image_paths += list(data_root.glob(c+'/*')) ❷
all_image_paths_c = []
for p in all_image_paths:
path_str = os.path.basename(str(p))
if path_str.startswith('._'): ❸
print('Rejecting '+str(p))
else:
all_image_paths_c.append(p) ❸
all_image_paths = all_image_paths_c
all_image_paths = [str(path) for path in all_image_paths]
random.shuffle(all_image_paths) ❹
image_count = len(all_image_paths) ❺
❶ 您将要训练的四个名人
❷ 选择每个目录中的所有图像
❸ 忽略隐藏文件并附加图像
❹ 打乱图像路径,以便网络不会记住实际顺序
❺ 计算图像数量(1,903)
在定义了图像路径集合并关联了四位名人的标签后,您可以使用其 Dataset API 开始构建您的 TensorFlow 数据集。在 15.1 节中,您使用低级 NumPy 结构编写了大量的数据增强代码。该数据增强代码在图像矩阵上操作。现在,我将向您展示如何使用 TensorFlow API 创建新的代码以执行相同的功能。
TensorFlow 提供了对数据增强的优雅支持。这些函数由 tf.image 包提供。此包包括 tf.image.random_flip_left_right 以随机翻转图像;tf.image_random_brightness 和 tf.image_random_contrast 改变背景色调,并执行类似于您在章节中手动实现的盐和胡椒增强。更重要的是,TensorFlow API 提供的数据增强不是直接修改图像并生成新的训练图像来扩展数据集,而是一个懒加载的图结构,仅在调用时创建增强图像。
您可以使用 TensorFlow 的 Dataset API,它包括对打乱、批处理和周期重复的全面支持,以任意方式在随机位置提供数据增强,而无需创建物理新数据存储在内存或磁盘上。此外,增强仅在训练运行时发生,并在您的 Python 代码运行完成后释放。
要开始使用增强功能,编写一个 preprocess_image 函数,该函数接受一个大小为 244 × 244 的 VGG-Face Lite 图像,以 BGR 格式返回,并且仅在执行期间修改图像张量。您可以将张量视为与图像相同,但它要强大得多。结果是表示在训练期间运行时执行的操作图的张量。您还可以将增强技术管道化,并在训练过程中迭代批次和周期时由 TensorFlow 随机运行它们。
另一件事是 TensorFlow 可以做的图像标准化或清理,通过除以平均值来简化训练。TensorFlow 的 tf.image.per_image_standardization 函数在调用后返回一个张量。因为张量操作是图,您可以在原始输入图像上组合这些操作。您的 preprocess_image 函数将以下操作管道化,如列表 15.15 所示:
-
将图像转换为 RGB 格式而不是 BGR。
-
随机翻转图像从左到右。
-
随机调整图像的亮度。
-
随机创建图像对比度(类似于盐和胡椒)。
-
随机旋转图像 90 度。
-
应用固定的图像标准化并除以像素的平均方差。
列表 15.15 使用 TensorFlow 进行图像数据集增强
IMAGE_SIZE=244
def preprocess_image(image, distort=True):
image = tf.image.decode_png(image, channels=3)
image = image[..., ::-1] ❶
image = tf.image.resize(image, [IMAGE_SIZE, IMAGE_SIZE]) ❷
if distort: ❸
image = tf.image.random_flip_left_right(image)
image = tf.image.random_brightness(image, max_delta=63)
image = tf.image.random_contrast(image, lower=0.2, upper=1.8)
rotate_pct = 0.5 # 50% of the time do a rotation between 0 to 90
➥ degrees
if random.random() < rotate_pct:
degrees = random.randint(0, 90)
image = tf.contrib.image.rotate(image, degrees * math.pi / 180,
➥ interpolation='BILINEAR')
image = (tf.cast(image, tf.float32) - 127.5)/128.0 ❹
tf.image.per_image_standardization(image)
return image
❶ 图像存储在文件中,以 BGR 格式存储,需要转换为 RGB。
❷ 将图像调整大小到 244 × 244
❸ 使用张量图对图像应用随机翻转(左右)、亮度、对比度和旋转。
❹ 对图像进行固定的标准化,减去平均值并除以像素的方差
当您的图像数据增强 function preprocess_image 返回一个在训练期间应用的操作张量图时,您几乎准备好创建您的 Tensor-Flow 数据集了。首先,您需要将输入数据分割成训练集和测试集,使用 70/30 的比例:
def get_training_and_testing_sets(file_list):
split = 0.7
split_index = math.floor(len(file_list) * split)
training = file_list[:split_index]
testing = file_list[split_index:]
return training, testing
您可以使用 get_training_and_testing_sets 将您的图像路径列表分割成 70/30 的比例,其中 70% 的图像用于训练,其余 30% 用于测试。您还需要准备您的标签和图像路径来构建整个数据集。一个简单的方法是遍历对应名人名称的文件夹,然后为每个名人名称分配一个从 0 到 4 的索引:
label_names = sorted(celebs_to_test)
label_to_index = dict((name, index) for index,name in enumerate(label_names))
all_image_labels = [label_to_index[pathlib.Path(path).parent.name]
for path in all_image_paths]
最后,您可以通过调用 get_training_and_testing_sets 函数来分割它们,从而生成您的图像路径和训练和测试标签:
train_paths, test_paths = get_training_and_testing_sets(all_image_paths)
train_labels, test_labels = get_training_and_testing_sets(all_image_labels)
现在,您已经准备好创建您的 TensorFlow 数据集了。
15.5.3 创建 TensorFlow 数据集
TensorFlow 中的数据集也是可以以各种方式构建的延迟执行的运算图。一种简单的方法是提供一组现有的数据切片,可以操作以生成新的数据集张量。例如,如果你将 train_paths 作为输入传递给 TensorFlow 的 tf.data.Dataset.from_tensor_slices 函数,该函数将生成一个 TensorFlow Dataset 对象:一个在运行时执行的运算图,并提供包装的图像路径。然后,如果你将那个 Dataset 对象传递给 tf.data.Dataset.map 函数,你可以进一步构建你的 TensorFlow Dataset 图如下。
tf.data.Dataset.map 函数接受一个函数作为输入,该函数将在数据集的每个可迭代项上并行运行,因此你可以使用列表 15.15 中的 preprocess_image 函数。该函数返回另一个 Dataset 对象,对应于经过数据增强操作运行过的图像路径。
记得图像翻转、随机亮度、对比度和随机旋转等操作吗?将 preprocess_image 函数的副本提供给 tf.data.Dataset.map 会创建一个操作图,该图将应用于 Dataset 中的每个图像路径。最后,TensorFlow Dataset API 提供了一个 zip 方法,该方法结合两个数据集,每个条目都是来自两个数据集的每个项目对的枚举。
再次强调,所有这些操作都是延迟执行的,因此你正在构建一个操作图,仅在迭代或在对数据集执行 TensorFlow 会话中的某些其他操作时才会执行。图 15.10 显示了结果数据集管道,该管道结合了来自 VGG-Face 路径的输入图像的数据增强以及它们的标签(包含图像的目录名称)。

图 15.10 TensorFlow Dataset API 管道,将 VGG-Face 图像路径与数据增强和图像标签(包含路径中每个图像的目录)结合
列表 15.16 中的代码实现了图 15.10 中所示的过程,创建了名为 train_image_label_ds 和 val_image_label_ds 的 TensorFlow 训练和测试数据集,用于 VGG-Face 图像和标签。你将在训练过程中使用这些数据集,我将在第 15.5.4 节中解释。数据集 API 在训练过程中也非常有用,因为之前必须手动实现的操作,如批处理、预取和在纪元中的重复,都由 TensorFlow 本地提供。
列表 15.16 创建 VGG-Face 验证和训练数据集
train_paths, test_paths = get_training_and_testing_sets(all_image_paths) ❶
train_labels, test_labels = get_training_and_testing_sets(all_image_labels)❶
train_path_ds = tf.data.Dataset.from_tensor_slices(train_paths) ❷
val_path_ds = tf.data.Dataset.from_tensor_slices(test_paths) ❷
train_image_ds = train_path_ds.map(load_and_preprocess_image, ❸
➥ num_parallel_calls=AUTOTUNE) ❸
val_image_ds = val_path_ds.map(load_image, num_parallel_calls=AUTOTUNE) ❸
val_label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(test_labels, ❹
➥ tf.int64)) ❹
train_label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(train_labels, ❹
➥ tf.int64)) ❹
train_image_label_ds = tf.data.Dataset.zip((train_image_ds, train_label_ds))❺
val_image_label_ds = tf.data.Dataset.zip((val_image_ds, val_label_ds)) ❺
❶ 将输入图像路径和输入标签分为 70/30 的训练/测试分割
❷ 从图像路径创建初始数据集用于训练和测试
❸ 执行 map 函数以创建一个新的 Dataset,该 Dataset 将数据增强步骤应用于训练和测试图像
❹ 通过将训练和测试标签转换为 int64 值创建数据集
❺ 将增强后的图像数据和标签压缩成训练和验证/测试数据集
如果您检查数据增强的结果,可能会看到名人 A 的随机翻转图像、名人 B 的高对比度黑白照片,或者名人 C 的轻微旋转图片。其中一些增强在图 15.11 中展示。

图 15.11 使用 TensorFlow Dataset API 进行数据增强的结果
现在您已经准备好了 TensorFlow Dataset 图,是时候配置您的数据集,使用典型的训练超参数,例如批量大小和洗牌。关于 Dataset API 的酷之处在于,您可以在训练之前通过在 Dataset 对象上设置属性来执行这些操作。
15.5.4 使用 TensorFlow 数据集进行训练
使用您为 VGG-Face 创建的 TensorFlow 数据集图,您得到了一个表示数据增强操作的组合惰性可执行图,它只会在您使用 TensorFlow 会话迭代并实现数据集中每个条目时执行。数据集 API 的强大之处在于它在训练和设置过程中显现出来。
因为您有一个数据集,您可以对它执行显式操作,例如在执行训练之前定义每个迭代中想要的批量大小。您还可以提前定义您希望洗牌数据集,以确保在每个纪元中都能以不同的顺序获得数据集。这样做是为了确保网络不会记住数据集的顺序,这可能会在它尝试优化权重时发生。在反向传播中,以相同的顺序看到相同的图像可能永远无法使训练操作在反向传播期间达到特定的优化步骤,以及它需要更新的权重以实现最佳结果。因此,您可以在数据集之前打开洗牌(列表 15.17)。您还可以告诉数据集重复一定次数,从而无需使用循环进行纪元。TensorFlow Dataset API 的强大之处在列表 15.17 中得到了充分体现。
列表还设置了数据集 API 使用 128 的批量大小。每个批量中图像越多,您的 CPU 和 GPU(如果您有的话)将使用的内存就越多,所以您需要调整这个数字。此外,每个批量中图像越多,随机性越少,训练操作更新每个纪元中学习参数的权重的机会就越少。您将通过使用缓冲区大小来洗牌数据集,这是输入的长度,确保整个数据集在每个纪元中只洗牌一次。最后,您在数据集上预取数据,这允许它在图最终执行、优化、减少 I/O 等待并利用 TensorFlow 的并行性时收集数据。所有这些功能都是由于 Dataset API 的存在。
在为训练和验证创建数据集后,是时候为 VGG-Face Lite 构建模型了。
列表 15.17 准备 VGG-Face TensorFlow 数据集进行训练
BATCH_SIZE=128
train_ds= train_image_label_ds.shuffle(buffer_size=len(train_paths)) ❶
val_ds = val_image_label_ds.shuffle(buffer_size=len(test_paths)) ❶
train_ds = train_ds.batch(BATCH_SIZE) ❷
val_ds = val_ds.batch(len(test_paths)) ❸
train_ds = train_ds.prefetch(buffer_size=AUTOTUNE) ❹
val_ds = val_ds.prefetch(buffer_size=AUTOTUNE) ❹
❶ 在每个 epoch 期间对训练图像和验证图像的整个数据集进行洗牌
❷ 在训练期间使用 128 个图像/标签的批量大小,确保大约 11 个 epoch,因为有 1,903 个图像,其中 70%用于训练
❸ 使用剩余的 30%图像进行验证,并将整个集合分批进行验证
❹ Prefetch 允许数据集在模型训练的同时在后台获取批次。
现在你已经为训练参数化了 TensorFlow Dataset图,你将进入实际的训练过程。跟随我到 15.5.5 节!
15.5.5 VGG-Face Lite 模型和训练
完整的 VGG-Face 模型包括 37 层,是一个深度网络模型,在训练后需要数 GB 的内存来加载模型图以进行预测。如果我相信你有超级计算和云资源,我们会重新实现这个模型。但我不确定,所以我们将在你的笔记本电脑上大约一天内可以训练的模型中删除一些滤波器和层。即使没有 GPU,该模型在四个名人脸上的表现也将非常准确。
VGG-Face Lite 使用五个卷积滤波器,是一个 10 层的深度网络,它利用了本章讨论的一些优化,例如批量归一化。为了加速训练和学习,你可以将图像大小重新缩放为 64 × 64。这种缩放通过将输入像素减少约 4 倍来减少模型必须学习的数量。你可以确信,如果计算机程序可以在小规模图像中学习微分,你就可以开始将其扩展到更大的图像。CNN 模型的输出是输入图像对应于四个名人脸类的哪一个。
模型架构在列表 15.18 中展示。第一部分在完整的 RGB 三通道空间中定义卷积滤波器,然后使用 64 个卷积滤波器,接着是 64、128、128 和 256 个滤波器用于学习。这些滤波器对应于列表 15.18 中卷积滤波器的 4D 参数conv1_2到conv3_1。输出全连接层有 128 个神经元,通过最终的 softmax 映射到四个输出类别。在第一个输入滤波器中,第三个参数是 RGB 3 通道,因为你会使用彩色图像。
列表 15.18 VGG-Face Lite 模型
conv1_1_filter = tf.Variable(tf.random_normal(shape=[3, 3, 3, 64], mean=0,
➥ stddev=10e-2)) ❶
conv1_2_filter = tf.Variable(tf.random_normal(shape=[3, 3, 64, 64], mean=0,
➥ stddev=10e-2))
conv2_1_filter = tf.Variable(tf.random_normal(shape=[3, 3, 64, 128], mean=0,
➥ stddev=10e-2))
conv2_2_filter = tf.Variable(tf.random_normal(shape=[3, 3, 128, 128], mean=0,
➥ stddev=10e-2))
conv3_1_filter = tf.Variable(tf.random_normal(shape=[3, 3, 128, 256], mean=0,
➥ stddev=10e-2))
def model(x, keep_prob): ❷
conv1_1 = tf.nn.conv2d(x, conv1_1_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv1_1 = tf.nn.relu(conv1_1)
conv1_2 = tf.nn.conv2d(conv1_1, conv1_2_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv1_2 = tf.nn.relu(conv1_2)
conv1_pool = tf.nn.max_pool(conv1_2, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME')
conv1_bn = tf.layers.batch_normalization(conv1_pool)
conv2_1 = tf.nn.conv2d(conv1_bn, conv2_1_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv2_1 = tf.nn.relu(conv2_1)
conv2_2 = tf.nn.conv2d(conv2_1, conv2_2_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv2_2 = tf.nn.relu(conv2_2)
conv2_pool = tf.nn.max_pool(conv2_2, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME')
conv2_bn = tf.layers.batch_normalization(conv2_pool)
conv3_1 = tf.nn.conv2d(conv2_pool, conv3_1_filter, strides=[1,1,1,1],
➥ padding='SAME')
conv3_1 = tf.nn.relu(conv3_1)
conv3_pool = tf.nn.max_pool(conv3_1, ksize=[1,2,2,1], strides=[1,2,2,1],
➥ padding='SAME')
conv3_bn = tf.layers.batch_normalization(conv3_pool)
flat = tf.contrib.layers.flatten(conv3_bn)
full1 = tf.contrib.layers.fully_connected(inputs=flat, num_outputs=128,
➥ activation_fn=tf.nn.relu)
full1 = tf.nn.dropout(full1, keep_prob) ❸
full1 = tf.layers.batch_normalization(full1)
out = tf.contrib.layers.fully_connected(inputs=full1, num_outputs=4,
➥ activation_fn=None)
return out ❹
❶ 定义卷积滤波器(五个)
❷ 定义 VGG-Face Lite 的模型函数
❸ 只在最后一层使用 dropout
❹ 返回模型的 logits
模型定义完成后,你可以继续设置训练的超参数。你可以使用与你的 CIFAR-10 物体识别模型类似的超参数。在实践中,你会对这些超参数进行实验以获得最优值。但为了本例的目的,这些参数应该允许你在大约一天内完成训练。
一个可以尝试的新超参数是指数权重衰减,它使用整体全局训练 epoch 步长作为降低学习权重的因素。随着时间的推移,你的网络将做出更小的学习步长,并试图聚焦于一个最优值。结合ADAMOptimizer,权重衰减已被证明有助于 CNN 收敛到最优学习参数。TensorFlow 提供了易于使用的优化器,你可以进行实验。与权重衰减相关的技术,如本例中 15.19 列表所示,在框架中测试起来相当简单。
列表 15.19 设置 VGG-Face Lite 模型训练的超参数
IMAGE_SIZE=64
x = tf.placeholder(tf.float32, [None, IMAGE_SIZE, IMAGE_SIZE, 3], ❶
➥ name='input_x') ❶
y = tf.placeholder(tf.float32, [None, len(label_names)], name='output_y') ❷
keep_prob = tf.placeholder(tf.float32, name='keep_prob')
global_step = tf.Variable(0, name='global_step', trainable=False)
epochs = 1000
keep_probability = 0.5 ❸
starter_learning_rate = 0.001
learning_rate =
➥ tf.compat.v1.train.exponential_decay(starter_learning_rate,global_step,
➥ 100000, 0.96, staircase=True)
model_op = model(x, keep_probability)
model_ops = tf.identity(model_op, name='logits') ❹
beta = 0.01
weights = [conv1_1_filter, conv1_2_filter, conv2_1_filter, conv2_2_filter,
➥ conv3_1_filter]
regularizer = tf.nn.l2_loss(weights[0])
for w in range(1, len(weights)):
regularizer = regularizer + tf.nn.l2_loss(weights[w])
cost =
➥ tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=model_op,
➥ labels=y))
cost = tf.reduce_mean(cost + beta * regularizer) ❺
train_op = tf.train.AdamOptimizer(learning_rate=learning_rate, beta1=0.9, ❻
➥ beta2=0.999, epsilon=0.1).minimize(cost, global_step=global_step) ❻
correct_pred = tf.equal(tf.argmax(model_op, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32), name='accuracy')
❶ 输入 N 张三通道 RGB 图像 N × 64 × 64 × 3
❷ 长度为 N 的图像输出 N × 4 个类别
❸ 根据深度面部识别论文使用 0.5 的 dropout
❹ 将 logits 张量命名为,以便在训练后从磁盘加载
❺ 实现 L2 正则化
❻ 使用指数权重衰减来设置学习率
实现面部识别的模型定义和超参数与用于 CIFAR-10 物体检测的类似。无论你是在尝试构建 CNN 架构来学习面部特征还是物体特征,相同的技巧都适用。你通过添加滤波器和层来创建更深的网络,实验图像的缩放和尺寸。你可以使用 dropout 来启用更健壮的架构,并使用数据增强将静态数据集转换为新的数据。增强是通过 TensorFlow 强大的Dataset API 实现的。
在 15.5.6 节中,你将训练网络并学习一些新知识,通过在每几个 epoch 对训练中的未见数据进行验证准确度检查来实现早期停止。这种技术将在网络训练过程中给你带来更好的理解,并帮助你理解验证准确度和损失的影响。
15.5.6 训练和评估 VGG-Face Lite
在训练过程中,你可以进行的一种优化是使用验证损失而不是训练准确度来衡量你的模型收敛得有多好。理论很简单。如果你将训练数据和验证数据分开——比如使用 70/30 的分割,就像你在 VGG-Face Lite 中所做的那样——你的验证损失应该下降,而你的训练准确度应该上升。建模训练和验证损失是当你看到人们试图解释深度学习时可能看到的常见凸交曲线。右上角下降的曲线是验证损失,而左下角曲线以多项式或指数路径上升的是训练准确度。
您可以通过在训练周期中测试它并偶尔打印它来测量您的验证损失。列表 15.20 中的代码打印以下内容:
-
每 5 个周期验证损失和准确度
-
每个周期的训练准确度(以了解您的模型性能)
注意,在 CPU 笔记本电脑上训练模型可能需要长达 36 小时,而在具有 GPU 的机器上只需几小时。
列表中另一个重要点是使用 TensorFlow Dataset API。您使用make_one_shot_iterator()函数创建一个迭代器,该函数使用预设的批次大小和预取缓冲区,在每次迭代中消耗一批数据。要注意的另一件事是,您在训练中使用while True循环;在每个周期中,迭代器将消耗整个批次集,然后抛出一个tf.errors.OutOfRangeError,您捕获它以跳出while True循环并进入下一个周期。
验证批次大小在训练期间为集合的全尺寸。在每次训练周期中,您会得到 128 个图像的批次大小,这是您在列表 15.17 中配置的。代码还会在每五个周期进行验证,并在那时通过获取所有文件路径的列表并遍历该列表来保存模型检查点,在保存新模型检查点之前删除之前的检查点文件。
列表 15.20 训练 VGG-Face Lite
with tf.Session(config=config) as sess:
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver() ❶
for j in tqdm(range(0, epochs)): ❷
iter = train_ds.make_one_shot_iterator() ❸
val_iter = val_ds.make_one_shot_iterator() ❸
batch_num = 0
iter_op = iter.get_next()
val_iter_op = val_iter.get_next()
val_image_batch, val_label_batch = None, None
try:
val_image_batch, val_label_batch = sess.run(val_iter_op) ❹
except tf.errors.OutOfRangeError:
pass
while True:
try:
image_batch, label_batch = sess.run(iter_op) ❹
onehot_labels = tf.one_hot(label_batch, len(label_names),
➥ on_value=1., off_value=0., axis=-1).eval() ❺
onehot_val_labels = tf.one_hot(val_label_batch, ❺
➥ len(label_names), on_value=1., off_value=0., ❺
➥ axis=-1).eval() ❺
_, accuracy_val, t_cost = sess.run([train_op, accuracy,
➥ cost], feed_dict={x:image_batch, y: onehot_labels})
batch_num += 1
except tf.errors.OutOfRangeError:
print("Step %d Accuracy %f Loss %f " % (j, accuracy_val,
➥ t_cost))
break
if j != 0 and j % 5 == 0: ❻
v_loss, v_accuracy = sess.run([cost, accuracy],
➥ feed_dict={x:val_image_batch, y:onehot_val_labels,
➥ keep_prob:1.0})
print("Step %d Validation Accuracy %f Validation Loss %f" % (j,
➥ v_accuracy, v_loss))
last_v_accuracy = v_accuracy
if j != 0 and j % 10 == 0:
print('Saving model progress.')
fileList = glob.glob('vgg-face-'+str(epochs)+'epochs.ckpt*') ❼
for filePath in fileList: ❼
try: ❼
os.remove(filePath) ❼
except: ❼
print("Error while deleting file : ", filePath) ❼
saver.save(sess, './vgg-face-'+str(epochs)+'epochs.ckpt') ❼
❶ 为您的模型创建一个保存器
❷ 循环 1000 个周期
❸ 为验证和训练数据集创建 one_shot_iterators
❹ 训练批次大小为每批次 128 个图像
❻ 获取训练和验证的一热标签
❺ 每 5 步测量验证损失和准确度。
❷ 保存新的模型检查点。
接下来,我将向您展示如何使用模型进行预测以及如何评估它。
15.5.7 使用 VGG-Face Lite 进行评估和预测
您可以将训练好的输出模型用于构建一个用于输入面部图像的predict函数(列表 15.21),重用列表 15.8 中的代码。您加载图和其 logits,并确保输入图像的大小为IMAGE_SIZE(64 × 64 × 3),用于三通道 RGB。函数会输出四个名人中最有信心的人的类别名称和类别编号,以及所有预测的 softmax 置信度和它们的值。
列表 15.21 使用 VGG-Face Lite 进行预测
def predict(img_data, noise=False):
class_num, class_name, confidence = None, None, 0.
with tf.Session() as sess:
loaded_graph = tf.Graph() ❶
image = img_data
im_data = tf.reshape(image, [1, IMAGE_SIZE, IMAGE_SIZE, 3]) ❷
with tf.Session() as sess:
im_data = im_data.eval()
with tf.Session(graph=loaded_graph) as sess:
loader = tf.train.import_meta_graph('vgg-face-
➥ '+str(epochs)+'epochs.ckpt' + '.meta')
loader.restore(sess, 'vgg-face-'+str(epochs)+'epochs.ckpt')
loaded_x = loaded_graph.get_tensor_by_name('input_x:0')
loaded_logits = loaded_graph.get_tensor_by_name('logits:0')
logits_out = sess.run(tf.nn.softmax(loaded_logits), ❸
➥ feed_dict={'keep_prob:0': 1.0, 'input_x:0': im_data}) ❸
class_num = np.argmax(logits_out, axis=1)[0]
class_name = label_names[class_num]
confidence = logits_out[0,class_num]
all_preds = logits_out
return (class_num, class_name, confidence, all_preds) ❹
❶ 加载图
❷ 将输入图像重塑为 1 × 64 × 64 × 3
❹ 将 logits 应用于输入图像并获取 softmax
❸ 返回最高的预测类别编号、名称、置信度和所有 logits
与 CIFAR-10 一样,您可以在整个验证数据集上运行您的predict函数以在训练期间评估损失和准确度。您可以为 VGG-Face 构建一个get_test_accuracy函数,它也是列表 15.8 的副本,除了在加载时使用的不同模型名称。使用该函数为 VGG-Face 显示的测试准确度为 97.37%,这在四个名人面部类别中是非常惊人的。
您可以使用predict和get_test_accuracy生成所有四个名人类别的 ROC 曲线,并使用列表 15.22 中的代码评估您的模型性能。此列表与列表 15.10 类似,但 VGG-Face Lite 有四个输出类别而不是十个。图 15.12 中显示的输出表明,对于您第一个用于面部识别的深度 CNN,微平均 ROC 达到了 98%,表现卓越。

图 15.12 VGG-Face Lite 的 ROC 曲线。
列表 15.22 生成 VGG-Face Lite ROC 曲线
outcome_test = label_binarize(test_label_batch)
predictions_test = label_binarize(predict_vals, classes=np.arange(0,
➥ len(test_names)))
n_classes = outcome_test.shape[1]
fpr = dict() ❶
tpr = dict() ❶
roc_auc = dict()
for i in range(n_classes):
fpr[i], tpr[i], _ = roc_curve(outcome_test[:, i], predictions_test[:, i])
roc_auc[i] = auc(fpr[i], tpr[i])
fpr["micro"], tpr["micro"], _ = roc_curve(outcome_test.ravel(), ❷
➥ predictions_test.ravel()) ❷
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
plt.figure() ❸
plt.plot(fpr["micro"], tpr["micro"],
label='micro-average ROC curve (area = {0:0.2f})'
''.format(roc_auc["micro"]))
for i in range(n_classes):
plt.plot(fpr[i], tpr[i], label='ROC curve of class {0} (area = {1:0.2f})'
''.format(test_names[i], roc_auc[i]))
plt.plot([0, 1], [0, 1], 'k—')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
roc_mean = np.mean(np.fromiter(roc_auc.values(), dtype=float))
plt.title('ROC curve for VGG Deep Face CNN '+str(epochs)+' iter Tensorflow
➥ (area = %{0:0.2f})'.format(roc_mean))
plt.legend(loc="lower right")
plt.show()
❶ 计算每个类的 ROC 曲线和 ROC 面积
❷ 计算微平均 ROC 曲线和 ROC 面积
❸ 绘制 ROC 曲线
您还可以从本章早期借用的一个最终函数是evaluate_model()函数。对于 VGG-Face,该函数略有不同,因为您不会使用互联网上的数据;您可以使用您的验证数据集。但这个函数很有价值,因为它可以显示您的模型在每个类别预测中的信心程度。输出由列表 15.23 中显示的函数生成。
列表 15.23 使用验证图像评估 VGG-Face Lite
def evaluate_model(im_data, test_labels, predicted, div=False):
n_predictions = len(predicted)
fig, axies = plt.subplots(nrows=n_predictions, ncols=2, figsize=(24,24))
fig.tight_layout()
fig.suptitle('Softmax Predictions for '+str(len(predicted))+' VGG Deep
➥ Face CNN '+str(epochs)+' iter Test Data', fontsize=20, y=1.1)
n_predictions = 4 ❶
margin = 0.05
ind = np.arange(n_predictions)
width = (1\. - 2\. * margin) / n_predictions
for i in range(0, len(im_data)): ❷
pred_names = label_names
pred_values = predicted[i]
correct_name = pred_names[test_labels[i]]
if div:
axies[i][0].imshow(im_data[i] / 255.)
else:
image = (1/(2*2.25)) * im_data[i] + 0.5
axies[i][0].imshow(image)
axies[i][0].set_title(correct_name)
axies[i][0].set_axis_off()
axies[i][1].barh(ind + margin, pred_values, width)
axies[i][1].set_yticks(ind + margin)
axies[i][1].set_yticklabels(pred_names)
axies[i][1].set_xticks([0, 0.5, 1.0])
for i in range(1, 5):
evaluate_model(test_data[(i-1)*10:i*10], test_labels[(i-1)*10:i*10],
➥ out_logits[(i-1)*10:i*10])
❶ 输出类别数量
❷ 遍历预测并显示左侧的图像和右侧的 softmax 预测
呼吁!这一章做了很多工作。现在您已经将 CNN 应用于物体识别、面部识别和面部检测,我相信您可以想到其他类似的问题来尝试。您不需要面孔或物体;还有很多其他东西可以训练和预测。您已经拥有了所有必要的工具!
摘要
-
CNN 可以用于通用图像匹配问题和构建面部识别系统,但除非您在现实世界中应用优化,否则它们的表现不会很好。
-
在不应用诸如 dropout、更深层次架构和图像增强等优化措施的情况下训练 CNN 会导致过拟合,并且模型在未见过的数据上表现不佳。
-
TensorFlow 提供了增强图像数据的功能,以及使用 dropout 技术防止 CNN 架构中记忆化的 API,还有用于扫描数据集并准备训练数据的 API,使得创建现实世界的 CNN 变得简单。
16 循环神经网络
本章涵盖
-
理解循环神经网络组件
-
设计时间序列数据的预测模型
-
在实际数据上使用时间序列预测器
回到学校的时候,我记得当我发现一次期中考试只有是非题时,我如释重负。我不可能是唯一一个认为答案中一半是对的,另一半是错的的人。
我找到了大多数问题的答案,其余的留给了猜测。但那种猜测是基于一些聪明的策略,你可能也使用过这种策略。在数出我的正确答案数量后,我意识到有相当一部分错误答案缺乏。所以,我大多数的猜测都是错误的,以平衡分布。
它起作用了。那一刻,我确实感觉到了狡猾。是什么感觉让我们对自己的决定如此自信,我们如何才能赋予神经网络同样的力量?
一个答案是使用上下文来回答问题。上下文线索是重要的信号,可以提高机器学习算法的性能。假设你想检查一个英语句子,并标记每个单词的词性(在第十章之后,你可能对这个问题更熟悉)。
天真的方法是对每个单词单独进行分类,如名词、形容词等,而不承认邻近的单词。考虑一下将这种技术应用于这个句子中的单词。单词trying被用作动词,但根据上下文,你也可以将其用作形容词,这使得词性标注成为一个棘手的问题。
一个更好的方法考虑上下文。为了向神经网络提供上下文线索,你将学习一种称为循环神经网络(RNN)的架构。你将处理的是连续的时间序列数据,例如股票市场价格,而不是自然语言数据。到本章结束时,你将能够对时间序列数据中的模式进行建模,以预测未来的值。
16.1 循环神经网络简介
要理解 RNN,请查看图 16.1 中的简单架构。这个架构将向量X(t)作为输入,并在某个时间(t)生成输出向量Y(t)。中间的圆圈代表网络的隐藏层。

图 16.1 一个神经网络,输入层和输出层分别标记为 X(t)和 Y(t)
有足够的输入/输出示例,你可以在 TensorFlow 中学习网络的参数。让我们将输入权重称为矩阵W[in],将输出权重称为矩阵W[out]。假设有一个隐藏层,称为向量Z(t)。
如图 16.2 所示,神经网络的前半部分由函数 Z(t) = X(t) × W[in] 描述,而神经网络的第二半部分的形式为 Y(t) = Z(t) × W[out]。等价地,如果你愿意,整个神经网络是函数 Y(t) = (X(t) × W[in]) × W[out]。

图 16.2 神经网络的隐藏层可以被视为数据的隐藏表示,它由输入权重编码并由输出权重解码。
在花费了无数个夜晚微调网络之后,你可能想开始在现实场景中使用你学习到的模型。通常,这个过程意味着多次调用模型,如图 16.3 所示。

图 16.3 通常,你会在不知道之前运行隐藏状态的情况下多次运行相同的神经网络。
在每个时间 t,当调用学习到的模型时,这种架构没有考虑之前运行的知识。这个过程就像只看当天的数据来预测股市趋势。更好的想法是利用一周或一个月的数据中的总体模式。
RNN 与传统的神经网络不同,因为它引入了一个过渡权重 W 来在时间上传递信息。图 16.4 显示了在 RNN 中必须学习的三个权重矩阵。引入过渡权重意味着下一个状态不仅依赖于前一个模型,还依赖于前一个状态,因此你的模型有“记忆”它所做的事情。

图 16.4 RNN 架构可以利用网络的先前状态来获得优势。
图表很棒,但你来这里是为了亲自动手。让我们直接进入正题!第 16.2 节展示了如何使用 TensorFlow 内置的 RNN 模型。然后你将使用 RNN 对实际的时间序列数据进行预测。
16.2 实现循环神经网络
在实现 RNN 时,你将使用 TensorFlow 来完成大部分繁重的工作。你不需要像图 16.4 所示那样手动构建网络,因为 TensorFlow 库已经支持一些强大的 RNN 模型。
注意:有关 TensorFlow 库中 RNN 的信息,请参阅www.svds.com/tensorflow-rnn-tutorial。
一种 RNN 模型是长短期记忆(LSTM)——一个有趣的名字,意味着它听起来那样。短期模式在长期不会被遗忘。
LSTM 的确切实现细节超出了本书的范围。相信我,对 LSTM 模型的彻底检查会分散本章的注意力,因为没有明确的行业标准。TensorFlow 通过处理模型的定义来提供帮助,这样你就可以直接使用它。随着 TensorFlow 的更新,你将能够利用 LSTM 模型中的改进,而无需修改你的代码。
小贴士:要了解如何从头开始实现 LSTM,我建议访问网页apaszke.github.io/lstm-explained.html。描述本章列表中使用的正则化实现的论文可在arxiv.org/abs/1409.2329找到。最后,这个关于 RNN 和 LSTMs 的教程提供了一些真实的笔记本和代码以供尝试:www.svds.com/tensorflow-rnn-tutorial。
首先,在一个名为 simple_regression.py 的新文件中编写你的代码。然后,如列表 16.1 所示,导入相关库。
列表 16.1 导入相关库
import numpy as np
import tensorflow as tf
from tensorflow.contrib import rnn
接下来,定义一个名为SeriesPredictor的类。构造函数,如列表 16.2 所示,将设置模型超参数、权重和成本函数。
列表 16.2 定义类及其构造函数
class SeriesPredictor:
def __init__(self, input_dim, seq_size, hidden_dim=10):
self.input_dim = input_dim ❶
self.seq_size = seq_size ❶
self.hidden_dim = hidden_dim ❶
self.W_out = tf.Variable(tf.random_normal([hidden_dim, 1]), ❷
➥ name='W_out') ❷
self.b_out = tf.Variable(tf.random_normal([1]), name='b_out') ❷
self.x = tf.placeholder(tf.float32, [None, seq_size, input_dim]) ❷
self.y = tf.placeholder(tf.float32, [None, seq_size]) ❷
self.cost = tf.reduce_mean(tf.square(self.model() - self.y)) ❸
self.train_op = tf.train.AdamOptimizer().minimize(self.cost) ❸
self.saver = tf.train.Saver() ❹
❶ 超参数
❷ 权重变量和输入占位符
❸ 成本优化器
❹ 辅助操作
接下来,使用 TensorFlow 的内置 RNN 模型BasicLSTMCell。传递给BasicLSTMCell对象的隐藏维度是随时间传递的隐藏状态的维度。你可以通过使用rnn.dynamic_rnn函数来运行这个单元,以检索输出结果。列表 16.3 详细说明了如何使用 TensorFlow 实现带有 LSTM 的预测模型。
列表 16.3 定义 RNN 模型
def model(self):
"""
:param x: inputs of size [T, batch_size, input_size]
:param W: matrix of fully-connected output layer weights
:param b: vector of fully-connected output layer biases
"""
cell = rnn.BasicLSTMCell(self.hidden_dim) ❶
outputs, states = tf.nn.dynamic_rnn(cell, self.x, dtype=tf.float32) ❷
num_examples = tf.shape(self.x)[0]
W_repeated = tf.tile(tf.expand_dims(self.W_out, 0), ❸
➥ [num_examples, 1, 1]) ❸
out = tf.matmul(outputs, W_repeated) + self.b_out
out = tf.squeeze(out)
return out
❶ 创建一个 LSTM 单元
❷ 在输入上运行单元以获得输出和状态的张量
❸ 将输出层计算为全连接线性函数
定义了模型和成本函数后,你可以实现训练函数,该函数将根据示例输入/输出对学习 LSTM 权重。如列表 16.4 所示,你打开一个会话,并在训练数据上反复运行优化器。
注意:你可以使用交叉验证来确定训练模型所需的迭代次数。在这种情况下,你假设一个固定的 epoch 数量。在 ResearchGate(mng.bz/lB92)等问答网站上可以找到一些有价值的见解和答案。
训练完成后,将模型保存到文件中,以便以后加载。
列表 16.4 在数据集上训练模型
def train(self, train_x, train_y):
with tf.Session() as sess:
tf.get_variable_scope().reuse_variables()
sess.run(tf.global_variables_initializer())
for i in range(1000):
_, mse = sess.run([self.train_op, self.cost], ❶
➥ feed_dict={self.x: train_x, self.y: train_y}) ❶
if i % 100 == 0:
print(i, mse)
save_path = self.saver.save(sess, 'model.ckpt')
print('Model saved to {}'.format(save_path))
❶ 运行 train op 1,000 次
假设一切顺利,并且您的模型已经学习到了参数。接下来,您可能希望在其他数据上评估预测模型。列表 16.5 加载了保存的模型,并通过输入测试数据在会话中运行模型。如果学习到的模型在测试数据上表现不佳,您可以尝试调整 LSTM 单元的隐藏维数数量。
列表 16.5 测试学习到的模型
def test(self, test_x):
with tf.Session() as sess:
tf.get_variable_scope().reuse_variables()
self.saver.restore(sess, './model.ckpt')
output = sess.run(self.model(), feed_dict={self.x: test_x})
print(output)
完成了!但为了确信它工作正常,您可以创建一些数据来尝试训练预测模型。在列表 16.6 中,您将创建输入序列(train_x)和相应的输出序列(train_y)。
列表 16.6 在虚拟数据上进行训练和测试
if __name__ == '__main__':
predictor = SeriesPredictor(input_dim=1, seq_size=4, hidden_dim=10)
train_x = [[[1], [2], [5], [6]],
[[5], [7], [7], [8]],
[[3], [4], [5], [7]]]
train_y = [[1, 3, 7, 11],
[5, 12, 14, 15],
[3, 7, 9, 12]]
predictor.train(train_x, train_y)
test_x = [[[1], [2], [3], [4]], ❶
[[4], [5], [6], [7]]] ❷
predictor.test(test_x)
❶ 预测结果应为 1,3,5,7。
❷ 预测结果应为 4,9,11,13。
您可以将这个预测模型视为一个黑盒,并用真实世界的时间序列数据进行训练以进行预测。在第 16.3 节中,您将获得可以处理的数据。
16.3 使用预测模型处理时间序列数据
时间序列数据在网上大量可用。对于这个例子,您将使用特定时期国际航空公司乘客的数据。您可以从mng.bz/ggOV获取这些数据。点击该链接将带您到一个时间序列数据的好图,如图 16.5 所示。

图 16.5 显示了多年来国际航空公司乘客数量的原始数据
您可以通过点击数据选项卡然后选择 CSV 来下载数据。您将需要手动编辑 CSV 文件以删除标题行和额外的页脚行。
在名为data_loader.py的文件中,添加列表 16.7 中的代码。
列表 16.7 加载数据
import csv
import numpy as np
import matplotlib.pyplot as plt
def load_series(filename, series_idx=1):
try:
with open(filename) as csvfile:
csvreader = csv.reader(csvfile)
data = [float(row[series_idx]) for row in csvreader
if len(row) > 0] ❶
normalized_data = (data - np.mean(data)) / np.std(data) ❷
return normalized_data
except IOError:
return None
def split_data(data, percent_train=0.80):
num_rows = len(data) * percent_train ❸
return data[:num_rows], data[num_rows:] ❹
❶ 遍历文件中的行并将其转换为浮点数
❷ 通过均值中心化和除以标准差来预处理数据
❸ 计算训练数据样本
❹ 将数据集分为训练集和测试集
在这里,您定义了两个函数:load_series和split_data。第一个函数加载磁盘上的时间序列文件并对其进行归一化;另一个函数将数据集分为两个部分,用于训练和测试。
由于您将多次评估模型以预测未来的值,让我们修改SeriesPredictor中的test函数,使其接受一个会话作为参数,而不是在每次调用时初始化会话。请参阅列表 16.8 中的此调整。
列表 16.8 修改test函数以传入会话
def test(self, sess, test_x):
tf.get_variable_scope().reuse_variables()
self.saver.restore(sess, './model.ckpt')
output = sess.run(self.model(), feed_dict={self.x: test_x})
return output
现在您可以通过加载可接受格式的数据来训练预测器。列表 16.9 展示了如何训练网络,然后使用训练好的模型来预测未来的值。您将生成训练数据(train_x和train_y),使其看起来像列表 16.6 中早些时候显示的数据。
列表 16.9 生成训练数据
if __name__ == '__main__':
seq_size = 5
predictor = SeriesPredictor(
input_dim=1, ❶
seq_size=seq_size, ❷
hidden_dim=100) ❸
data = data_loader.load_series('international-airline-passengers.csv') ❹
train_data, actual_vals = data_loader.split_data(data)
train_x, train_y = [], []
for i in range(len(train_data) - seq_size - 1): ❺
train_x.append(np.expand_dims(train_data[i:i+seq_size],
➥ axis=1).tolist())
train_y.append(train_data[i+1:i+seq_size+1])
test_x, test_y = [], [] ❻
for i in range(len(actual_vals) - seq_size - 1):
test_x.append(np.expand_dims(actual_vals[i:i+seq_size],
➥ axis=1).tolist())
test_y.append(actual_vals[i+1:i+seq_size+1])
predictor.train(train_x, train_y, test_x, test_y) ❼
with tf.Session() as sess: ❽
predicted_vals = predictor.test(sess, test_x)[:,0]
print('predicted_vals', np.shape(predicted_vals))
plot_results(train_data, predicted_vals, actual_vals, 'predictions.png')
prev_seq = train_x[-1]
predicted_vals = []
for i in range(20):
next_seq = predictor.test(sess, [prev_seq])
predicted_vals.append(next_seq[-1])
prev_seq = np.vstack((prev_seq[1:], next_seq[-1]))
plot_results(train_data, predicted_vals, actual_vals, 'hallucinations.png')
❶ 序列中每个元素的维度是一个标量(1D)。
❷ 每个序列的长度
❸ RNN 隐藏维度的尺寸
❹ 加载数据
❺ 在时间序列数据上滑动窗口以构建训练数据集
❻ 使用相同的窗口滑动策略来构建测试数据集
❼ 在训练数据集上训练模型
❽ 可视化模型的性能
预测器生成两个图表。第一个图表是模型的预测结果,给定真实值(图 16.6)。

图 16.6 当与真实数据测试时,预测结果与趋势匹配得相当好。
另一个图表显示了仅提供训练数据时的预测结果(蓝色线)——没有其他任何东西(图 16.7)。这个过程可用的信息较少,但它仍然很好地匹配了数据趋势。

图 16.7 如果算法使用先前预测的结果来做出进一步的预测,总体趋势匹配得很好,但不是具体的峰值。
你可以使用时间序列预测器来重现数据中的真实波动。想象一下,基于你迄今为止学到的工具来预测市场繁荣与萧条周期。你在等什么?抓取一些市场数据,并学习你自己的预测模型!
16.4 应用 RNNs
RNNs 旨在用于序列数据。由于音频信号比视频(线性信号与 2D 像素数组)低一个维度,因此开始处理音频时间序列数据要容易得多。考虑一下语音识别在过去几年里取得了多大的进步;它正成为一个可处理的问题。
与你在第七章中进行的音频数据聚类分析中的音频直方图分析类似,大多数语音识别预处理都涉及以某种形式的音程图来表示声音。一种常见的技术是使用 梅尔频率倒谱系数 (MFCCs)。本博客文章概述了一个很好的介绍:mng.bz/411F。
接下来,你需要一个数据集来训练你的模型。以下是一些流行的数据集:
-
LibriSpeech (www.openslr.org/12)
-
TED-LIUM (www.openslr.org/7)
-
VoxForge (www.voxforge.org)
在 TensorFlow 中使用这些数据集进行简单语音识别实现的详细说明可在svds.com/tensorflow-rnn-tutorial找到。
摘要
-
一个循环神经网络(RNN)使用过去的信息。这样,它可以通过使用具有高时间依赖性的数据来做出预测。
-
TensorFlow 自带 RNN 模型。
-
由于数据中的时间依赖性,时间序列预测是 RNNs 的一个有用应用。
17 个 LSTMs 和自动语音识别
本章涵盖
-
使用 LibriSpeech 语料库为自动语音识别准备数据集
-
训练长短期记忆(LSTM)RNN 将语音转换为文本
-
在训练期间和之后评估 LSTM 性能
现在用电子设备说话和交谈已经司空见惯了。几年前,在我智能手机的早期版本上,我点击了麦克风按钮,并使用它的语音输入功能尝试将一封电子邮件说成现实。然而,我老板收到的电子邮件中却有一大堆拼写错误和音标错误,他想知道我是不是把太多的下班活动与我的正式职责混合在一起了!
世界在变化,神经网络在执行自动语音识别(ASR)方面的准确性也在提高,ASR 是将语音音频转换为书面文本的过程。无论你是使用手机的智能数字助手为你安排会议,口述那封可靠的电子邮件,还是要求家里的智能设备订购东西,播放背景音乐,甚至启动你的汽车,这些任务都是由 ASR 功能驱动的。
语音识别(ASR)是如何成为日常生活一部分的?之前的 ASR 系统依赖于脆弱的统计模型,这些模型依赖于特定语言的语法,而今天的 ASR 系统建立在鲁棒的循环神经网络(RNNs)之上,特别是长短期记忆(LSTM)网络,这是一种特定的 RNN。使用 LSTMs,你可以教会计算机分段听音频,并随着时间的推移将这些段转换为语言字符。像书中讨论的卷积神经网络(CNNs)和其他生物启发的神经网络一样,用于语音识别的 LSTMs 学习人类的学习方式。每个小的音频段对应于语言中的一个字符,你使用的细胞数量与语音中的字符数量相同。你试图教会网络以人类的方式理解语言。随着网络通过每次调用进行学习,网络权重被更新并向前和向后传递到 LSTM 细胞中,这些细胞学习每个声音及其对应的字母。映射到声音的字母组合成为语言。
这些方法因百度 Deep Speech 架构而闻名,该架构在 2015 年超越了最先进的语音识别系统,后来由 Mozilla 基金会开源实现,使用你和我最喜欢的工具包 TensorFlow。原始论文在arxiv.org/abs/1412.5567。我将向你展示如何收集和准备用于深度语音自动语音到文本模型的训练数据,如何使用 TensorFlow 对其进行训练,以及如何使用它来评估现实世界的声音数据。
嘿,TensorFlow!
当你对着手机说“嘿,数字助手”时,你是否好奇手机是如何工作的?起初,助手可能并不总是能正确理解你的话,但硅谷的智能手机和电脑制造商表示,随着时间的推移,它将变得更好。没错,这就是为什么数字助手会询问是否正确理解了你的话。TensorFlow 等框架允许你训练自己的模型,并提供经过数百万来自世界各地用户反馈的预训练 LSTM 模型,这些模型可以细化每个字符和每个单词的 ASR 输出。实际上,识别确实会随着时间的推移而变得更好。
ASR 网络训练的一个常见来源是有声读物。有声读物很有用,因为它们通常既有声音的并行语料库,也有与说话词对应的转录。LibriSpeech 语料库是 Open Speech and Language Resources (OpenSLR) 项目的一部分,可以用来训练深度语音模型。然而,正如你所知,你需要进行一些数据清理,以便为训练准备信息。
17.1 准备 LibriSpeech 语料库
有声读物是有用的发明,它允许我们在开车和其他活动时听我们喜欢的书籍。它们通常是大量声音——可能是数百小时——分成更小的片段,并且几乎总是包含相应的转录,以防你想阅读你正在听到的文本。
一套开源的有声读物可以从 Open Speech and Language Resources (OpenSLR) 网页和 LibriSpeech 语料库中获取。LibriSpeech 是一组来自有声读物的短剪辑和相应的转录。LibriSpeech 包括超过 1,000 小时的 16KHz 英语语音录音,包括元数据;原始 MP3 文件;以及 100、360 和 500 小时的语音训练集的分离和校对版本。该数据集包括转录,还有一个用于每个训练周期验证的开发数据集和一个用于训练后测试的测试集。
很不幸,由于深度语音模型期望的是 Windows 音频视频 (.wav) 交错文件音频格式,而不是 LibriSpeech 所使用的 Free Lossless Audio Codec (.flac) 文件格式,因此数据集在深度语音模型中不可用。通常,你的第一步是——没错——数据准备和清理。
17.1.1 下载、清理和准备 LibriSpeech OpenSLR 数据
首先,你需要下载一组训练语料库:100、360 或 500 小时的训练数据。根据你的内存大小,你可以选择其中任何一个,但我的建议是选择 100 小时,因为这对于训练一个不错的深度语音模型来说已经足够了。只有一个开发(验证)集和一个测试集,所以你不需要为这些文件选择不同的时间长度。
准备下载的 LibriSpeech 数据的整体过程相当直接:
-
下载 train-100-clean、dev-clean 和 test 的 tarball,请从
www.openslr.org/12。 -
将 tar 文件解压缩到 LibriSpeech/train-clean-100、LibriSpeech/dev-clean 和 LibriSpeech/test-clean 文件夹中。
-
将.flac 音频文件转换为训练、验证和测试的.wav 音频文件。
-
将章节中的每个音频文件对应的一行聚合转录文件取来,每个文件包含一行音频文件。每行包含与引用的短声音剪辑对应的提取出的单词。将这些聚合转录重新格式化为每个.wav 音频文件一个.txt 转录文件。
-
将.wav 和.txt 音频/转录元组的子文件夹收集到一个扁平的文件夹结构中,并通过删除聚合转录和.flac 文件来清理。
该过程在图 17.1 中从左到右展示。

图 17.1 将 LibriSpeech OpenSLR 数据转换为用于深度语音模型的清洗和准备过程
好消息是,您可以使用一些简单的 Python 实用代码构建这个数据清洗和准备流程。您将从列表 17.1 开始,使用urllib和tarfile库进行文件下载,这些库允许您下载远程 URL 并解压缩存档文件。
警告:下载这些数据可能需要相当长的时间,因为仅训练数据就大约有 7 GB。请准备好等待数小时,具体取决于您的带宽。
列表 17.1 下载并解压缩训练、开发和测试 LibriSpeech 数据
import urllib
import tarfile ❶
def download_and_extract_tar(url): ❷
print("Downloading and extracting %s " % (url))
tar_stream = urllib.request.urlopen(url)
tar_file = tarfile.open(fileobj=tar_stream, mode="r|gz")
tar_file.extractall()
train_url = "http://www.openslr.org/resources/12/train-clean-100.tar.gz" ❸
dev_url = "http://www.openslr.org/resources/12/dev-clean.tar.gz" ❸
test_url = "http://www.openslr.org/resources/12/test-clean.tar.gz" ❸
download_and_extract_tar(train_url) ❹
download_and_extract_tar(dev_url) ❹
download_and_extract_tar(test_url) ❹
❶ 导入 urllib 下载库和 tarfile 库以提取存档
❷ 创建一个函数从 URL 下载 tar 文件并在本地提取
❸ OpenSLR 100 小时训练、开发(验证)和测试集
❹ 下载数据并将流提取到其本地文件夹
在数据准备流程的下一步,您需要将.flac 音频文件转换为.wav 文件。幸运的是,有一个易于使用的 Python 库叫做pydub可以执行此任务,以及其他多媒体文件的转换和操作。尽管pydub功能强大,但您在这里只使用了其功能的一个子集。尝试使用它来发现更多功能。
17.1.2 转换音频
当 tar 文件被提取后,它们将出现在您的 LibriSpeech/目录中,其中是 train-clean-100、dev-clean 和 test-clean 之一。在这些文件夹下面是更多子文件夹,它们对应不同的章节编号,还有更多子文件夹对应章节的部分。因此,您的代码需要遍历这些子文件夹,并为每个.flac 文件使用 pydub 创建.wav 文件。您将按照列表 17.2 中所示处理该过程。如果您在笔记本电脑上运行此代码,不妨去倒杯咖啡;转换可能需要长达一个半小时。
列表 17.2 将.flac 文件转换为.wav 文件并遍历数据集
import pydub
import os
import tqdm
import glob
def flac2wav(filepath):
base_file_path = os.path.dirname(filepath) ❶
filename = os.path.basename(filepath) ❶
filename_no_ext = os.path.splitext(filename)[0] ❷
audio = AudioSegment.from_file(filepath, "flac") ❸
wav_file_path = base_file_path + '/' + filename_no_ext +'.wav' ❹
audio.export(wav_file_path, format="wav") ❺
def convert_flac_to_wav(train_path, dev_path, test_path):
train_flac = [file for file in glob.glob(train_path + "/*/*/*.flac")] ❻
dev_flac = [file for file in glob.glob(dev_path + "/*/*/*.flac")] ❻
test_flac = [file for file in glob.glob(test_path + "/*/*/*.flac")] ❻
print("Converting %d train %d dev and %d test flac files into wav files"
% (len(train_flac), len(dev_flac), len(test_flac)))
print("Processing train")
for f in tqdm(train_flac): ❼
flac2wav(f)
print("Processing dev")
for f in tqdm(dev_flac): ❼
flac2wav(f)
print("Processing test")
for f in tqdm(test_flac): ❼
flac2wav(f)
❶ 给定一个文件路径,例如 LibriSpeech/train-clean-100/307/127535/307-127535-000.flac,获取其目录名(base_file_path)和文件名。
❷ 移除扩展名并获取文件基本名称,例如 307-127535-000
❸ 使用 Pydub 读取 FLAC 文件
❹ 推导出 .wav 文件名,即 basename + .wav
❺ 使用 Pydub 保存新的 .wav 文件
❻ 使用 glob 库获取 train、dev(验证)和 test 中所有 .flac 文件的列表
❼ 将 train、dev 和 test 的 .flac 文件转换为 .wav
当音频文件格式正确时,你必须处理 Libri-Speech 的其他部分:之前提到的步骤 4 中的转录文本,每个音频剪辑一个文本文件转录。由于它们与数据集一起提供,转录文本被聚合到子章节集合中,每个子章节一个转录文本,转录子章节文件中的每一行对应于该子目录中的一个音频文件。对于深度语音,你需要每个音频文件一个文本文件转录。接下来,你将生成每个音频文件的转录文本,每个文件一个。
17.1.3 生成每个音频文件的转录文本
要创建每个音频文件的转录文本,你需要读取每个子章节目录的转录聚合文件。将该文件中的每一行分解成单独的文本转录文件,每个音频文件一个,在子章节文件夹中。列表 17.3 中的简单 Python 代码为你处理了这个任务。请注意,与 .flac 文件转换相比,运行此代码相当快。
列表 17.3 将子章节聚合分解成单个 .wav 文件转录文本
def create_per_file_transcripts(file):
file_toks = file.split('.') ❶
base_file_path = os.path.dirname(file) ❶
with open(file, 'r') as fd:
lines = fd.readlines()
for line in lines: ❷
toks = line.split(' ') ❷
wav_file_name = base_file_path + '/' + toks[0] + '.txt' ❷
with open(wav_file_name, 'w') as of: ❸
trans = " ".join([t.lower() for t in toks[1:]]) ❸
of.write(trans) ❸
of.write('\n')
def gen_transcripts(train_path, dev_path, test_path):
train_transcripts = [file for file in glob.glob(train_path + ❹
➥ "/*/*/*.txt")] ❹
dev_transcripts = [file for file in glob.glob(dev_path + "/*/*/*.txt")] ❹
test_transcripts = [file for file in glob.glob(test_path + "/*/*/*.txt")] ❹
print("Converting %d train %d dev and %d test aggregate transcripts into
➥ individual transcripts"
% (len(train_transcripts), len(dev_transcripts),
➥ len(test_transcripts)))
print("Processing train") ❺
for f in tqdm(train_transcripts):
create_per_file_transcripts(f)
print("Processing dev") ❺
for f in tqdm(dev_transcripts):
create_per_file_transcripts(f)
print("Processing test") ❺
for f in tqdm(test_transcripts):
create_per_file_transcripts(f)
❶ 获取子章节前缀文件名
❷ 对于子章节聚合转录文本中的每一行,根据空格分割,并使用第一个标记作为该行转录的音频文件名
❸ 除了第一个标记之外的其他标记是该音频文件的转录单词。
❹ 获取 train、dev 和 test 中所有子章节的转录文件
❺ 将 train、dev 和 test 处理成单独的转录文本
17.1.4 聚合音频和转录文本
数据处理的最后一步(如果你在计分的话是步骤 5)是收集所有音频 .wav 文件及其相关的转录文本到一个聚合的顶级目录,并通过删除 .flac 文件和聚合的转录文本进行清理。Python 中的简单移动文件函数和删除函数调用可以为你处理这个任务,如列表 17.4 所示。你可以使用以下简单的路径来表示数据集顶级路径的聚合:
speech_data_path = "LibriSpeech"
train_path = speech_data_path + "/train-clean-100"
dev_path = speech_data_path + "/dev-clean"
test_path = speech_data_path + "/test-clean"
all_train_path = train_path + "-all"
all_dev_path = dev_path + "-all"
all_test_path = test_path + "-all"
列表 17.4 中的代码应该运行得相当快,因为它只是清理工作。重要的是要注意,你必须运行清理步骤,因为你不希望转录文本或音频文件被重复计数。
列表 17.4 聚合音频和转录文本,并清理
def move_files(from_path, to_path):
if not os.path.exists(to_path):
print("Creating dir %s" % (to_path)) ❶
os.makedirs(to_path)
for root, _, files in os.walk(from_path):
for file in files:
path_to_file = os.path.join(root, file)
base_file_path = os.path.dirname(file)
to_path_file = to_path + '/' + file
print("Moving file from %s to %s " % (path_to_file,
➥ to_path_file))
shutil.move(path_to_file, to_path_file) ❷
def remove_files_with_ext(directory, ext):
for root, _, files in os.walk(directory): ❸
for file in files: ❸
if file.endswith(ext): ❸
path_to_file = os.path.join(root, file) ❸
print("Removing file %s " % (path_to_file)) ❸
os.remove(path_to_file) ❸
remove_files_with_ext(train_path, "flac") ❹
remove_files_with_ext(dev_path, "flac") ❹
remove_files_with_ext(test_path, "flac") ❹
move_files(train_path, all_train_path) ❺
move_files(dev_path, all_dev_path) ❺
move_files(test_path, all_test_path) ❺
remove_files_with_ext(all_train_path, "trans.txt") ❻
remove_files_with_ext(all_dev_path, "trans.txt") ❻
remove_files_with_ext(all_test_path, "trans.txt") ❻
❶ 如果目标路径不存在,则创建它。
❷ 遍历顶级 train、dev 和 test 目录,并将每个音频和转录文件(假设首先删除了 .flac 文件)移动到目标路径
❸ 遍历目录并删除所有具有提供扩展名的文件
❹ 删除 .flac 文件
❺ 将音频 .wav 文件和相关的转录文本移动到顶级目录
❻ 删除聚合的转录文本
凭借准备好的 LibriSpeech 数据集聚合(train-clean-100-all、dev-clean-all 和 test-clean-all),你就可以开始使用深度语音和 TensorFlow 进行训练了。
17.2 使用深度语音模型
深度语音是一个在 2014 年和 2015 年由百度在尝试改进搜索过程中构建的神经网络。百度是一家提供互联网服务和产品的中国公司;它收集和爬取网络,并使用人工智能来增强用户寻找他们所需内容的体验。因此,可以说百度拥有大量的数据,包括语音数据。在 2015 年,允许在移动设备上进行语音输入以辅助搜索的需求不断增加,并且现在被视为默认功能。
百度的论文“深度语音:端到端语音识别的扩展”(可在arxiv.org/abs/1412.5567找到)描述了一个多层神经网络,该网络将卷积层作为从分割到字符级别语音的输入音频频谱图的特征提取器。每个输入音频文件被分割成与字母表中一个字母相对应的语音。在特征提取之后,输出特征被提供给一个双向 LSTM 网络,该网络将其输出权重向前传递到一个反向层,该反向层反过来将其输出作为其学习的一部分。双向反向层的输出被提供给一个额外的特征提取器,并用于预测与语音相对应的字母表中的特定字符。整体架构如图 17.2 所示。

图 17.2 深度语音模型。音频输入并被分割成与字符级别语音对应的样本。这些语音由双向 LSTM 网络(MFCC = 梅尔频率倒谱系数)猜测。
深度语音架构最著名的实现之一是由 Mozilla 基金会承担的,并使用 TensorFlow 实现;它可在github.com/mozilla/DeepSpeech找到。鉴于重实现这个实现的所有功能将需要更多的空间,并覆盖本书的几个章节,我将突出显示重要的部分,以指导你设置和运行简化版本的代码。请注意,从头开始训练深度语音模型需要相当多的时间,即使有 GPU 也是如此。
17.2.1 准备深度语音的输入音频数据
在第七章中,我向您展示了如何使用 Bregman Toolkit 将音频文件从频域转换为时域。然而,并非只有这个库可以执行这个过程。一个类似的过程为音频文件生成梅尔频率声谱系数(MFCC)。这个过程对音频文件运行快速傅里叶变换(FFT),并将文件输出为与请求的声谱系数(或频率分箱)数量相对应的样本。与 Bregman Toolkit 一样,这些分箱的幅度对每个音频文件都是唯一的,并且可以用来生成用于机器学习的特征。
Bregman Toolkit 生成了这些分箱的近似值,但现在我将向您展示一种不同的方法,该方法使用 TensorFlow 原生代码,以及使用 SciPy 的代码。TensorFlow 附带了一些处理音频文件的有用功能,称为audio_ops。该库包括读取.wav 文件的代码,将它们解码成频谱图(类似于音程图;第七章),然后对它们进行 MFCC 变换,进入时间域。列表 17.5 包含一些简单的代码,使用 TensorFlow 的 audio_ops 从 LibriSpeech 语料库中的随机.wav 文件生成 MFCC 特征。运行代码生成一个特征向量大小为(1, 2545, 26)或每个样本 2,545 个 26 个声谱振幅的样本。
列表 17.5 使用 TensorFlow 从 wav 文件生成 MFCC 特征
numcep=26 ❶
with tf.Session() as sess:
filename = 'LibriSpeech/train-clean-100-all/3486-166424-0004.wav'
raw_audio = tf.io.read_file(filename) ❷
audio, fs = decode_wav(raw_audio) ❷
spectrogram = audio_ops.audio_spectrogram(
audio, window_size=1024,stride=64) ❸
orig_inputs = audio_ops.mfcc(spectrogram, sample_rate=fs, ❸
➥ dct_coefficient_count=numcep) ❸
audio_mfcc = orig_inputs.eval()
print(np.shape(audio_mfcc)) ❹
❶ 要使用的声谱系数分箱数量(根据 Deep Speech 论文,为 26)
❷ 读取文件并解释.wav 声音
❸ 生成相应的频谱图并生成 MFCC 特征
❹ 打印输出形状
您可以通过重新使用第七章中的某些绘图代码并绘制对应于 26 个分箱的第一个样本来查看音频文件的一些样本,并确信机器学习算法有东西可以学习。列表 17.6 中的代码执行此绘图,其输出如图 17.3 所示。
列表 17.6 绘制音频文件 MFCC 特征的五个样本的图表
labels=[]
for i in np.arange(26): ❶
labels.append("P"+str(i+1))
fig, ax = plt.subplots()
ind = np.arange(len(labels))
width = 0.15
colors = ['r', 'g', 'y', 'b', 'black']
plots = []
for i in range(0, 5): ❷
Xs = np.asarray(np.abs(audio_mfcc[0][i])).reshape(-1) ❸
p = ax.bar(ind + i*width, Xs, width, color=colors[i])
plots.append(p[0])
xticks = ind + width / (audio_mfcc.shape[0])
print(xticks)
ax.legend(tuple(plots), ('S1', 'S2', 'S3', 'S4', 'S5'))
ax.yaxis.set_units(inch)
ax.autoscale_view()
ax.set_xticks(xticks)
ax.set_xticklabels(labels)
ax.set_ylabel('Normalized freq coumt')
ax.set_xlabel('Pitch')
ax.set_title('Normalized frequency counts for Various Sounds')
plt.show() ❹
❶ 26 个声谱系数(cepstral)分箱
❷ 2,545 个样本中的 5 个
❸ 取绝对值,以确保没有负的分箱大小
❹ 显示图表

图 17.3 LibriSpeech 语料库中音频文件的五个样本的 MFCC 特征
样本之间存在差异。不同的样本对应于声音文件中的不同时间点,最终,您希望您的 ASR 机器学习模型能够预测这些语音的字母。在 Deep Speech 论文中,作者定义了一个上下文,这是一个包含向后查看样本和向前查看样本以及当前样本的集合。样本之间的一些重叠允许更好的区分,因为单词和字符级别的音调在语言中往往重叠。想想说“best”这个词。b 和 e 在声音上重叠。这个相同的概念也适用于训练一个好的机器学习模型,因为您希望它尽可能地反映现实世界。
您可以设置一个时间窗口来查看过去和未来的几个时间步——称这个时间窗口为 numcontext 样本。结合当前样本,您的新特征向量变为 N × (2number cepstrals numcontext + numcepstrals)。通过本地测试,9 是 numcontext 的一个合理值,并且因为您使用了 26 个倒谱系数,您有一个新特征向量的大小为 (N × 2269 + 26) 或 (N × 494),其中 N 是样本数量。您可以使用列表 17.5 中的代码为每个声音文件生成这个新特征向量,然后使用列表 17.7 中的代码完成工作。Deep Speech 论文的作者还使用了一种技术,即只从音频文件中提取一半的样本以进一步减少样本密度,尤其是在向前和向后查看的窗口中。列表 17.7 也为您处理了这个工作。
列表 17.7 生成过去、现在和未来的 MFCC 上下文窗口
orig_inputs = orig_inputs[:,::2] ❶
audio_mfcc = orig_inputs.eval()
train_inputs = np.array([], np.float32)
train_inputs.resize((audio_mfcc.shape[1], numcep + 2 * numcep *
➥ numcontext)) ❷
empty_mfcc = np.array([])
empty_mfcc.resize((numcep))
empty_mfcc = tf.convert_to_tensor(empty_mfcc, dtype=tf.float32)
empty_mfcc_ev = empty_mfcc.eval()
time_slices = range(train_inputs.shape[0])
context_past_min = time_slices[0] + numcontext ❸
context_future_max = time_slices[-1] - numcontext ❹
for time_slice in tqdm(time_slices):
need_empty_past = max(0, (context_past_min - time_slice)) ❺
empty_source_past = np.asarray([empty_mfcc_ev for empty_slots in
➥ range(need_empty_past)])
data_source_past = orig_inputs[0][max(0, time_slice -
➥ numcontext):time_slice]
need_empty_future = max(0, (time_slice - context_future_max)) ❻
empty_source_future = np.asarray([empty_mfcc_ev for empty_slots in
➥ range(need_empty_future)])
data_source_future = orig_inputs[0][time_slice + 1:time_slice +
➥ numcontext + 1]
if need_empty_past: ❼
past = tf.concat([tf.cast(empty_source_past, tf.float32),
➥ tf.cast(data_source_past, tf.float32)], 0)
else:
past = data_source_past
if need_empty_future:
future = tf.concat([tf.cast(data_source_future, tf.float32),
➥ tf.cast(empty_source_future, tf.float32)], 0)
else:
future = data_source_future
past = tf.reshape(past, [numcontext*numcep])
now = orig_inputs[0][time_slice]
future = tf.reshape(future, [numcontext*numcep])
train_inputs[time_slice] = np.concatenate((past.eval(), now.eval(),
➥ future.eval()))
train_inputs = (train_inputs - np.mean(train_inputs)) /
➥ np.std(train_inputs) ❽
❶ 取每个第二个样本,并按一半的数据子集
❷ 生成 (N × 494 样本) 占位符
❸ 过去内容的起始最小点;至少要有 9 个时间切片
❹ 未来内容的结束最大点;时间切片大小为 9 ts
❺ 在过去提取最多 numcontext 时间切片,并使用空的 MFCC 特征完成
❻ 在未来的 numcontext 时间切片中提取,并使用空的 MFCC 特征完成
❼ 如果需要,对过去或未来进行填充,或取过去和未来
❽ 对标准差取平均值,并对学习输入值进行归一化
现在您的音频已经准备好用于训练,并且可以在 LibriSpeech 音频文件上运行数据准备。请注意:这个数据准备过程可能需要许多小时,具体取决于您使用多少训练数据。文件超过 25,000 个,我的建议是从小规模开始,比如用 100 个进行训练。这个样本将产生一个特征向量 (100 × N × 494),其中 N 是样本数量(每个音频文件两个样本)。
您还需要处理的其他数据准备涉及那些转录文本。字符级数据需要转换为数字,这是一个简单的过程,我将在第 17.2.2 节中向您展示。
17.2.2 准备文本转录本为字符级数值数据
通过你为 LibriSpeech 语料库所做的准备工作,对于每个音频数据文件,例如 LibriSpeech/train-clean-100-all/3486-166424-0004.wav,你都有一个相应的 LibriSpeech/train-clean-100-all/3486-166424-0004.txt 文件,内容如下:
a hare sat upright in the middle of the ancient roadway the valley itself lay serenely under the ambering light smiling peaceful emptied of horror
要将你的输入特征向量大小(1 × N × 494)映射到字符级输出,你必须将文本输出处理成数字。一种简单的方法是使用 Python 的ord()函数,因为 Python 中的所有字符都是数字,通过字符集在屏幕上表示为字符。字符集是一个将特定整数值映射到表中某个字符的表。流行的字符集包括用于 8 位和 16 位 Unicode 的 ASCII、UTF-8 和 UTF-16。Python 的ord()函数返回字符的整数表示,这对于你的网络来说将非常有效。
第一步是打开转录文件,确保它是 ASCII 格式,并移除任何奇特的字符。一般来说,深度语音已经被移植以支持其他字符集(语言编码),但我在本章中会专注于 ASCII 和英语。(你可以在github.com/mozilla/DeepSpeech找到其他数据集。)最简单的方法是使用 Python 的codecs模块强制文件以 UTF-8 格式读取,然后强制将其转换为 ASCII。列表 17.8 提供了执行此任务的代码片段。该代码还将文本转换为小写。
列表 17.8 打开转录,强制转换为 ASCII,并规范化文本
def normalize_txt_file(txt_file, remove_apostrophe=True):
with codecs.open(txt_file, encoding="utf-8") as open_txt_file: ❶
return normalize_text(open_txt_file.rea(), ❷
➥ remove_apostrophe=remove_apostrophe) ❷
❷
def normalize_text(original, remove_apostrophe=True): ❷
result = unicodedata.normalize("NFKD", original).encode("ascii", ❸
➥ "ignore").decode() ❸
if remove_apostrophe: ❹
result = result.replace("'", "")
return re.sub("[^a-zA-Z']+", ' ', result).strip().lower() ❺
❶ 以 UTF-8 格式打开文件
❷ 只支持字母和撇号。
❸ 将任何 Unicode 字符转换为 ASCII 等效字符
❹ 移除撇号以保持缩写词在一起
❺ 返回小写字母字符
当文本被规范化和清理后,你可以使用ord()函数将其转换为数值数组。text_to_char_array()函数将清理后的文本转换为数值数组。为此,该函数扫描字符串文本,将字符串转换为字母数组——例如,I am的['I' '<space>' 'a' 'm']——然后将字母替换为它们的序数表示——I am转录的[9 0 1 13]——列表 17.9 提供了执行转录到数组转换的函数。
列表 17.9 从干净的转录生成数值数组
SPACE_TOKEN = '<space>'
SPACE_INDEX = 0
FIRST_INDEX = ord('a') - 1 ❶
def text_to_char_array(original):
result = original.replace(' ', ' ') ❷
result = result.split(' ') ❷
result = np.hstack([SPACE_TOKEN if xt == '' else list(xt) for xt in ❸
➥ result]) ❸
return np.asarray([SPACE_INDEX if xt == SPACE_TOKEN else ord(xt) - ❹
➥ FIRST_INDEX for xt in result]) ❹
❶ 为空格字符保留 0
❷ 创建句子单词列表,将空格替换为''
❸ 将每个单词转换为字母数组
❹ 将字母转换为序数表示
在音频输入准备和数值转录之后,你就有训练 LSTM 深度语音模型所需的一切。第 17.2.3 节简要介绍了其实现。
17.2.3 TensorFlow 中的深度语音模型
TensorFlow 实现的深度语音复杂,因此你几乎没有必要深入研究其细节。我更喜欢使用硅谷数据科学教程中流行的简化版本,该教程可在 www.svds.com/tensorflow-rnn-tutorial 和相关的 GitHub 代码 github.com/mrubash1/RNN-Tutorial 找到。该教程定义了一个更简单的深度语音架构版本,我将在列表 17.10 和后续列表中介绍。
该模型以形状为 (M, N, 494) 的训练样本作为输入,其中 M 是训练批的大小,N 是文件中样本的数量的一半,494 包括 26 个倒谱和过去和未来 9 步的上下文。模型的最初步骤包括设置网络及其初始三个隐藏层,以从输入音频中学习特征。该工作的初始超参数来自深度语音论文,包括在层 1-3 中设置 dropout 为 0.5,在 LSTM 双向层中为 0,在输出层中为 0.5。
学习这些超参数所花费的时间不会是时间的高效利用,因此直接使用论文中的超参数。relu_clip 是深度语音作者使用的修改过的 ReLU 激活函数,将任何输入设置为以下值:
-
任何小于 0 到 0 的值
-
任何大于 0 且小于裁剪值(20)的值 X 将变为 X 本身
-
任何大于裁剪值(20)的值将裁剪为裁剪值(20)
使用此激活函数对激活进行缩放和偏移,该激活函数也来自论文。
网络使用 1,024 个隐藏神经元作为其初始三个隐藏层和双向 LSTM 单元层,以及 1,024 个神经元用于对 29 个字符(包括空格、撇号和空格)进行字符级预测。列表 17.10 开始了模型定义。
列表 17.10 深度语音的超参数和设置
def BiRNN_model(batch_x, seq_length, n_input, n_context):
dropout = [0.05, 0.05, 0.05, 0.0, 0.0, 0.05] ❶
relu_clip = 20
b1_stddev = 0.046875 ❷
h1_stddev = 0.046875
b2_stddev = 0.046875
h2_stddev = 0.046875
b3_stddev = 0.046875
h3_stddev = 0.046875
b5_stddev = 0.046875
h5_stddev = 0.046875
b6_stddev = 0.046875
h6_stddev = 0.046875
n_hidden_1 = 1024 ❸
n_hidden_2 = 1024
n_hidden_5 = 1024
n_cell_dim = 1024
n_hidden_3 = 2048
n_hidden_6 = 29
n_character = 29 ❹
batch_x_shape = tf.shape(batch_x) ❺
batch_x = tf.transpose(batch_x, [1, 0, 2])
batch_x = tf.reshape(batch_x,
[-1, n_input + 2 * n_input * n_context]) ❻
❶ 每层要使用的 dropout
❷ 使用论文中定义的 ReLU 裁剪
❸ 每层的隐藏维度数量
❹ 每个单元的 29 个字符的输出概率
❺ 输入形状:[batch_size, n_steps, n_input + 226 cepstrals9 窗口前后]
❻ 对第一层输入进行重塑(n_stepsbatch_size, n_input + 226 cepstrals*9 窗口)
列表 17.11 中的模型接下来的三个层将输入数据批次传递给学习音频特征,这些特征将被用作双向 LSTM 单元(每个大小为 1,024)的输入。该模型还存储 TensorFlow 摘要变量,您可以使用 TensorBoard 检查这些变量,正如我在前面的章节中向您展示的那样,以防您需要检查变量值进行调试。
列表 17.11 深度语音的音频特征层
with tf.name_scope('fc1'): ❶
b1 = tf.get_variable(name='b1', shape=[n_hidden_1], initializer=tf.random_normal_initializer(stddev=b1_stddev))
h1 = tf.get_variable(name='h1', shape=[n_input + 2 * n_input *
➥ n_context, n_hidden_1],
initializer=tf.random_normal_initializer(stddev=h1_stddev))
layer_1 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(batch_x, h1), b1)),
➥ relu_clip) ❷
layer_1 = tf.nn.dropout(layer_1, (1.0 - dropout[0]))
tf.summary.histogram("weights", h1)
tf.summary.histogram("biases", b1)
tf.summary.histogram("activations", layer_1)
with tf.name_scope('fc2'): ❸
b2 = tf.get_variable(name='b2', shape=[n_hidden_2],
➥ initializer=tf.random_normal_initializer(stddev=b2_stddev))
h2 = tf.get_variable(name='h2', shape=[n_hidden_1, n_hidden_2],
➥ initializer=tf.random_normal_initializer(stddev=h2_stddev))
layer_2 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(layer_1, h2), b2)),
➥ relu_clip) ❹
layer_2 = tf.nn.dropout(layer_2, (1.0 - dropout[1]))
tf.summary.histogram("weights", h2)
tf.summary.histogram("biases", b2)
tf.summary.histogram("activations", layer_2)
with tf.name_scope('fc3'): ❺
b3 = tf.get_variable(name='b3', shape=[n_hidden_3], initializer=tf.random_normal_initializer(stddev=b3_stddev))
h3 = tf.get_variable(name='h3', shape=[n_hidden_2, n_hidden_3],
➥ initializer=tf.random_normal_initializer(stddev=h3_stddev))
layer_3 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(layer_2, h3), b3)),
➥ relu_clip) ❻
layer_3 = tf.nn.dropout(layer_3, (1.0 - dropout[2]))
tf.summary.histogram("weights", h3)
tf.summary.histogram("biases", b3)
tf.summary.histogram("activations", layer_3)
❶ 实现第一层
❷ 使用裁剪 ReLU 激活
❸ 实现第二层
❹ 使用裁剪 ReLU 激活
❺ 实现第三层
❻ 使用剪裁 ReLU 激活
深度语音包括列表 17.12 中的双向 LSTM 层来学习音频特征及其映射到单字符级输出。初始权重(lstm_fw_cell)传递给每个前向单元进行学习;然后使用反向单元权重(lstm_bw_cell)来反向传播字符预测的学习。
列表 17.12 双向 LSTM 层
with tf.name_scope('lstm'):
lstm_fw_cell = tf.contrib.rnn.BasicLSTMCell(n_cell_dim,
➥ forget_bias=1.0, state_is_tuple=True) ❶
lstm_fw_cell = tf.contrib.rnn.DropoutWrapper(lstm_fw_cell,
input_keep_prob=1.0 - dropout[3],
output_keep_prob=1.0 - dropout[3])
lstm_bw_cell = tf.contrib.rnn.BasicLSTMCell(n_cell_dim, ❷
➥ forget_bias=1.0, state_is_tuple=True) ❷
lstm_bw_cell = tf.contrib.rnn.DropoutWrapper(lstm_bw_cell,
input_keep_prob=1.0 - dropout[4],
output_keep_prob=1.0 - dropout[4])
layer_3 = tf.reshape(layer_3, [-1, batch_x_shape[0], n_hidden_3]) ❸
outputs, output_states =
➥ tf.nn.bidirectional_dynamic_rnn(cell_fw=lstm_fw_cell,
cell_bw=lstm_bw_cell,
inputs=layer_3,
dtype=tf.float32,
time_major=True,
sequence_length=seq_length)
tf.summary.histogram("activations", outputs)
outputs = tf.concat(outputs, 2)
outputs = tf.reshape(outputs, [-1, 2 * n_cell_dim]) ❹
❶ 前向方向单元
❷ 反向方向单元
❸ 重塑为[n_steps, batch_size, 2*n_cell_dim]
❹ 重塑为[n_stepsbatch_size, 2n_cell_dim]
最终层通过在映射到对应于 29 个字符类 softmax 分布的完全连接层之前使用一个额外的隐藏层来特征化 LSTM 输出(列表 17.13)。
列表 17.13 深度语音的最终层
with tf.name_scope('fc5'): ❶
b5 = tf.get_variable(name='b5', shape=[n_hidden_5], initializer=tf.random_normal_initializer(stddev=b5_stddev))
h5 = tf.get_variable(name='h5', shape=[(2 * n_cell_dim), n_hidden_5],
➥ initializer=tf.random_normal_initializer(stddev=h5_stddev))
layer_5 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(outputs, h5), b5)),
➥ relu_clip)
layer_5 = tf.nn.dropout(layer_5, (1.0 - dropout[5]))
tf.summary.histogram("weights", h5)
tf.summary.histogram("biases", b5)
tf.summary.histogram("activations", layer_5)
with tf.name_scope('fc6'): ❷
b6 = tf.get_variable(name='b6', shape=[n_hidden_6], initializer=tf.random_normal_initializer(stddev=b6_stddev))
h6 = tf.get_variable(name='h6', shape=[n_hidden_5, n_hidden_6],
➥ initializer=tf.random_normal_initializer(stddev=h6_stddev))
layer_6 = tf.add(tf.matmul(layer_5, h6), b6)
tf.summary.histogram("weights", h6)
tf.summary.histogram("biases", b6)
tf.summary.histogram("activations", layer_6)
layer_6 = tf.reshape(layer_6, [-1, batch_x_shape[0], n_hidden_6]) ❸
summary_op = tf.summary.merge_all()
return layer_6, summary_op
❶ 第五层带有剪裁 ReLU 激活和 dropout
❷ 创建 29 个字符类分布的输出 logits
❸ 重塑为时间优先的 n_steps, batch_size, n_hidden_6 分布
深度语音的输出是每个样本在每个音频文件批次上的 29 个字符类的概率分布。如果批次大小为 50,并且每个文件有 75 个时间步或样本,则输出将在每个步骤N,对应于每个批次中 50 个文件在该步骤的声音发音的 50 个输出字符。
在运行深度语音之前要讨论的最后一件事是如何评估预测的字符发音。在 17.2.4 节中,我将讨论如何使用连接主义时序分类(CTC)来区分连续时间步之间的重叠语言。
17.2.4 TensorFlow 中的连接主义时序分类
在深度语音 RNN 的理想情况下,每个音频文件输入中的每个发音和每个时间步都应该直接映射到网络预测输出的一个字符。但现实是,当你将输入划分为时间步时,一个发音和最终的字符级输出可能会跨越多个时间步。在我所说的音频文件中的人类事物(图 17.4),完全有可能预测输出在第一个四个时间步(t1-t3)将对应于字母I,因为发音在每个步骤发生,但该发音的一部分也渗透到时间步 t4,预测为字母a,因为那里a的声音开始出现。

图 17.4 CTC 和深度语音旨在在每个时间步(t)生成可能的字符级输出
在这种情况下,考虑到重叠,你如何决定时间步 t4 代表字母I还是a?
你可以使用 CTC 技术。该技术是一种类似于具有 logits 的交叉熵损失的损失函数(第六章)。损失函数根据输入的大小(时间步的数量)和时间步之间的关系计算输出所有可能的字符级组合。它定义了一个函数来关联每个时间步的输出概率类别。每个时间步的预测本身不是独立考虑的,而是作为一个整体。
TensorFlow 随带一个名为 ctc_ops 的 CTC 损失函数,作为 tensorflow.python.ops 包的一部分。你向它提供与字符级时间步预测对应的 logits 以及一个占位符(int32)用于预测。你将在每个训练步骤中用稀疏转录转换成数值数据填充预测,正如我在第 17.2.2 节中展示的那样,以及期望的输出长度和每个 epoch。然后计算 CTC 损失,并可用于扫描每个时间步的字符预测,并收敛到完整的转录预测,以最小化损失或反过来在预测空间中具有最高的似然。
在前面的例子中,CTC 损失将查看所有可能的字符级预测,寻找最小损失,不仅对于时间步 t4,也对于 t3-t5,这反过来又建议在 a 前面和 I 后面添加一个空格,正确划分转录中的单词。因为提供了所需的序列长度,并且等于转换后的稀疏转录的长度,CTC 算法可以找出如何权衡空格和其他非字母字符以实现最佳的转录输出。列表 17.14 设置了 CTC 损失函数并准备训练模型。
列表 17.14 设置 CTC 损失并准备训练模型
input_tensor = tf.placeholder(tf.float32, [None, None, numcep + (2 * numcep ❶
➥ * numcontext)], name='input') ❶
seq_length = tf.placeholder(tf.int32, [None], name='seq_length') ❷
targets = tf.sparse_placeholder(tf.int32, name='targets') ❸
logits, summary_op = BiRNN_model(input_tensor, tf.to_int64(seq_length),
➥ numcep, numcontext) ❹
total_loss = ctc_ops.ctc_loss(targets, logits, seq_length)
avg_loss = tf.reduce_mean(total_loss) ❺
beta1 = 0.9 ❻
beta2 = 0.999 ❻
epsilon = 1e-8 ❻
learning_rate = 0.001 ❻
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate, ❻
beta1=beta1, ❻
beta2=beta2, ❻
epsilon=epsilon) ❻
train_op = optimizer.minimize(avg_loss) ❻
❶ 输入转换后的音频特征
❷ 大小为 [batch_size] 的一维数组
❸ 使用 sparse_placeholder;将生成一个 SparseTensor,这是 ctc_loss 操作所需的
❹ 设置 BiRNN 模型
❺ 使用输入音频特征(logits)、稀疏转录目标和期望的转录长度(序列长度)设置 CTC 损失函数
❻ 使用来自 Deep Speech 论文的超参数创建优化器并进行训练操作
在定义并准备好 CTC 损失函数后,模型就准备好进行训练了,我将在第 17.3 节帮助你设置。
17.3 深度语音的训练与评估
使用 TensorFlow 运行 deep-speech 模型与运行迄今为止创建的所有其他模型类似。你需要设置训练大小,这控制了用于训练的音频文件数量。如果可能的话,我建议在笔记本电脑上一次使用数百个,但不要超过千个,并且使用 LibriSpeech 语料库,你有超过 25,000 个文件可用于训练。
给定 50 个批量大小的批次,在 150 个训练文件的情况下,每个 epoch 创建 3 次迭代。在 CPU 上训练 50 个 epoch 可能需要几个小时,在 GPU 上可能只需要几分钟。你可以调整这些超参数以适应你的计算资源。列表 17.15 使用 TensorFlow 数据集 API(第十五章)创建数据集,作为每个 epoch 懒加载的 TensorFlow 操作。
列表 17.15 设置深度语音的训练参数和数据集
num_epochs = 50 ❶
BATCH_SIZE=50 ❶
train_size=150 ❶
train_audio_ds =
➥ tf.data.Dataset.from_tensor_slices(train_audio_wav[0:train_size]) ❷
train_audio_ds = train_audio_ds.batch(BATCH_SIZE) ❷
train_audio_ds = train_audio_ds.shuffle(buffer_size=train_size) ❷
train_audio_ds = train_audio_ds.prefetch(buffer_size=AUTOTUNE) ❷
❶ 超参数设置,50 个 epoch,每个 epoch 有 3 个批次,每个批次 50 个,共 150 个训练文件
❷ 从训练文件构建 TensorFlow 数据集,并设置随机洗牌和预取
列表 17.16 训练模型,并按批次输出 CTC 损失以及通过除以训练文件数得到的平均训练损失。音频输入通过使用前面描述的技术来创建 MFCC 特征,具有过去和未来九步的上下文窗口,并进行了填充。转录数据根据 26 个字母字符的序数值转换为整数,还包括空格、空白和撇号,总共 29 个不同的字符值。
列表 17.16 训练深度语音
train_cost = 0.
with tf.Session() as sess: ❶
sess.run(tf.global_variables_initializer())
for epoch in tqdm(range(0, num_epochs)):
iter = train_audio_ds.make_one_shot_iterator()
batch_num = 0
iter_op = iter.get_next() ❷
while True:
try:
train_batch = sess.run(iter_op)
trans_batch = [fname.decode("utf-8").split('.')[0]+'.txt' for ❸
➥ fname in train_batch] ❸
audio_data = [process_audio(f) for f in train_batch] ❹
train, t_length = pad_sequences(audio_data) ❹
trans_txt = [normalize_txt_file(f) for f in trans_batch] ❺
trans_txt = [text_to_char_array(f) for f in trans_txt] ❺
transcript_sparse = sparse_tuple_from(np.asarray(trans_txt)) ❺
feed = {input_tensor: train,
targets: transcript_sparse,
seq_length: t_length} ❻
batch_cost, _ = sess.run([avg_loss, train_op], ❻
➥ feed_dict=feed) ❻
train_cost += batch_cost * BATCH_SIZE
batch_num += 1
print('Batch cost: %.2f' % (batch_cost)) ❼
except tf.errors.OutOfRangeError:
train_cost /= train_size
print('Epoch %d | Train cost: %.2f' % (epoch, train_cost)) ❽
break
❶ 创建一个新的 TF 会话
❷ 创建一个新的数据集迭代操作
❸ 获取训练音频文件名批次,然后获取相应的转录名称
❹ 通过创建具有上下文窗口的 MFCC 并填充序列来准备音频数据
❺ 创建每个字符级别的数值转录
❻ 准备每个训练步骤的输入并运行训练操作
❼ 打印每个批次的损失
❽ 打印每个训练样本的平均损失
运行深度语音最全面工具集和干净的代码库之一是位于github.com/mrubash1/RNN-Tutorial的 RNN-Tutorial GitHub 仓库。它允许以下操作:
-
模型参数的简单调整
-
超参数的使用
-
训练、测试和开发集
-
每个 epoch 的 dev 集验证
-
最终测试集验证
它还打印每个 epoch 和训练结束时的模型解码,以便你得到以下输出:
2020-07-04 17:40:02,850 [INFO] tf_train_ctc.py: Batch 0, file 35
2020-07-04 17:40:02,850 [INFO] tf_train_ctc.py: Original: he was impervious
➥ to reason
2020-07-04 17:40:02,850 [INFO] tf_train_ctc.py: Decoded: he was om pervius
➥ trreason____
2020-07-04 17:40:02,850 [INFO] tf_train_ctc.py: Batch 0, file 36
2020-07-04 17:40:02,850 [INFO] tf_train_ctc.py: Original: which clouds seeing
➥ that there was no roof sometimes wept over the masterpiece of ursus
2020-07-04 17:40:02,850 [INFO] tf_train_ctc.py: Decoded: whicht clouds saing
➥ the tere was no re some timns wath ofprh them master peaes eafversus
2020-07-04 19:50:36,602 [INFO] tf_train_ctc.py: Batch 0, file 45
2020-07-04 19:50:36,602 [INFO] tf_train_ctc.py: Original: the family had been
➥ living on corncakes and sorghum molasses for three days
2020-07-04 19:50:36,602 [INFO] tf_train_ctc.py: Decoded: the femwigh ha been
➥ lentang on qarncaes and sord am maolassis fo thre bys
另一个巧妙的功能是自动使用 TensorFlow 的 Summary 操作 API 来记录事件,这样你就可以使用 TensorBoard 在模型运行时可视化模型。
在几天内使用 GPU,我成功训练了相当多深语音模型的变体;我观察了验证损失、准确性和训练损失,并用 TensorBoard 进行了绘图。图 17.5 显示了深度语音训练的前 500 个 epoch。

图 17.5 深度语音模型前 500 个 epoch 的 TensorBoard 输出
模型在前 90 个 epoch 内快速收敛,训练损失和验证损失朝正确的方向前进。默认情况下,RNN-Tutorial 代码库每两个 epoch 计算一次验证标签错误率,也朝正确的方向前进,证明了模型的鲁棒性。
恭喜您——您已经学会了如何创建自己的 ASR 系统!大型网络公司并不是唯一能够做到这一点的人。RNNs、数据和一些 TensorFlow 是您完成任务所需的工具。
摘要
-
智能数字助手,从家用设备到您的手机再到您的电视,通过使用 RNNs 和被称为 LSTM 模型的特殊实例来识别语音并将其转换为文本。这个过程被称为 ASR。
-
您可以从有声读物等开放数据源中获取数据,例如 OpenSLR 和 LibriSpeech 数据,这些数据包括 100、500 和 1000 小时的录音,以及这些书籍的文本转录本,您可以使用这些数据来训练用于 ASR 的 LSTM 模型。
-
ASR 中最著名的模型之一称为 Deep Speech。您可以使用 TensorFlow 和 LibriSpeech 数据通过构建 deep-speech LSTM 模型来重新创建此模型。
-
TensorFlow 和相关工具包提供了使用 MFCCs 特征化音频的方法,将频率域转换为时间域。
-
文本转录本可以使用 Python 的
ord()函数和转换文本转录本为数字的实用工具进行特征化。 -
CTC 是一种损失函数和算法,它允许语音输入在非均匀时间步长上与统一字符级别的文本转录本对齐,以在 RNNs 中获得最佳的转录结果。
18 为聊天机器人构建序列到序列模型
本章涵盖了
-
检查序列到序列架构
-
执行单词的向量嵌入
-
使用真实世界数据实现聊天机器人
通过电话与客户服务交谈对客户和公司来说都是一种负担。服务提供商支付了一大笔钱来雇佣客户服务代表,但如果我们能够自动化大部分这项工作怎么办?我们能否开发出通过自然语言与客户交互的软件?
这个想法并不像你想象的那么遥远。由于深度学习技术在自然语言处理方面的前所未有的发展,聊天机器人受到了很多炒作。也许,给定足够的训练数据,聊天机器人可以学会通过自然对话来导航最常见的客户问题。如果聊天机器人真正高效,它不仅可以通过消除雇佣代表的需求来为公司节省资金,还可以加速客户寻找答案的过程。
在本章中,你将通过向神经网络提供成千上万的输入和输出句子示例来构建一个聊天机器人。你的训练数据集是一对英语话语。例如,如果你问“你好吗?”,聊天机器人应该回答,“很好,谢谢。”
注意:在本章中,我们将序列和句子视为可互换的概念。在我们的实现中,一个句子将是一系列字母的序列。另一种常见的方法是将句子表示为一系列单词的序列。
实际上,该算法将尝试为每个自然语言查询生成一个智能的自然语言响应。你将实现一个神经网络,它使用了前几章教授的两个主要概念:多类分类和循环神经网络(RNNs)。
18.1 建立在分类和 RNNs 之上
分类是一种机器学习方法,用于预测输入数据项的类别。此外,多类分类允许超过两个类别。你在第六章中看到了如何在 TensorFlow 中实现这样的算法。具体来说,模型预测(一个数字序列)与真实值(一个 one-hot 向量)之间的成本函数试图通过交叉熵损失来找到两个序列之间的距离。
注意:one-hot 向量就像一个全零向量,除了其中一个维度有一个值为1。
在这种情况下,实现一个聊天机器人,你会使用交叉熵损失函数的变体来衡量两个序列之间的差异:模型的响应(这是一个序列)与真实值(也是一个序列)。
练习 18.1
在 TensorFlow 中,你可以使用交叉熵损失函数来衡量一个 one-hot 向量,例如(1,0,0),与神经网络输出,例如(2.34,0.1,0.3)之间的相似度。另一方面,英文句子不是数值向量。你如何使用交叉熵损失来衡量英文句子之间的相似度?
答案
一种粗略的方法是将每个句子表示为一个向量,通过计算句子中每个单词的频率来实现。然后比较这些向量以查看它们匹配得有多接近。
你可能还记得,RNNs 是一种神经网络设计,它不仅能够结合当前时间步的输入,还能结合之前输入的状态信息。第十六章和第十七章详细介绍了这些网络,它们将在本章再次被使用。RNNs 将输入和输出表示为时间序列数据,这正是你需要用来表示序列的。
一个天真想法是使用现成的 RNN 来实现聊天机器人。让我们看看为什么这是一个不好的方法。RNN 的输入和输出是自然语言句子,所以输入 (x[t],x[t-1],x[t-2],...) 和输出 (y[t],y[t-1],y[t-2],...) 可以是单词序列。使用 RNN 来模拟对话的问题在于 RNN 会立即产生输出结果。如果你的输入是一个单词序列 (How,are,you),第一个输出单词将只取决于第一个输入单词。RNN 的输出序列项 y[t] 不能向前查看输入句子的未来部分来做出决定;它将仅限于了解之前的输入序列 (x[t],x[t-1],x[t-2],... )。天真的 RNN 模型试图在用户完成提问之前就给出一个响应,这可能导致错误的结果。
相反,你将使用两个 RNN:一个用于输入句子,另一个用于输出序列。当第一个 RNN 完成输入序列的处理后,它将隐藏状态发送到第二个 RNN 以处理输出句子。你可以在图 18.1 中看到这两个 RNN,分别标记为编码器(Encoder)和解码器(Decoder)。

图 18.1 这是您神经网络模型的高级视图。输入 ayy 被传递到编码器 RNN,解码器 RNN 预期会响应 lmao。这些例子是您聊天机器人的玩具例子,但你可以想象更复杂的输入和输出句子对。
我们将从前几章中引入多类分类和 RNN 的概念,来设计一个学习将输入序列映射到输出序列的神经网络。RNNs 提供了一种编码输入句子、将总结的状态向量传递到解码器,然后将其解码为响应句子的方法。为了衡量模型响应与真实值之间的成本,我们借鉴了多类分类中使用的函数——交叉熵损失。
这种架构被称为 序列到序列(seq2seq) 神经网络架构。你使用的训练数据将是成千上万对从电影剧本中挖掘出的句子。算法将观察这些对话示例,并最终学会对您可能提出的任意查询形成响应。
练习 18.2
哪些其他行业可以从聊天机器人中受益?
答案
一个例子是作为教学工具的对话伙伴,用于教授英语、数学甚至计算机科学等科目。
到本章结束时,你将拥有自己的聊天机器人,它可以对你的查询做出一定程度的智能响应。它不会完美,因为这个模型总是对相同的输入查询以相同的方式做出响应。
假设你正在前往一个外国旅行,而你没有任何说这种语言的能力。一个聪明的销售人员给你一本书,声称这是你需要用来回应外语句子的所有东西。你被要求像字典一样使用这本书。当有人用外语说一个短语时,你可以查找它,书中将为你准备好要大声读出的回答:“如果有人说你好,你就说嗨。”
当然,这本书可能是一个实用的查找表,用于日常闲聊,但查找表能为你提供任意对话的正确响应吗?当然不能!考虑查找问题“你饿吗?”答案在书中,永远不会改变。
查找表中缺少状态信息,这是对话中的一个关键组件。在你的 seq2seq 模型中,你将面临类似的问题,但这个模型是一个良好的起点。信不信由你,目前,用于智能对话的分层状态表示还不是标准;许多聊天机器人都是从这些 seq2seq 模型开始的。
18.2 理解 seq2seq 架构
seq2seq 模型试图学习一个神经网络,从输入序列预测输出序列。序列与传统向量有一点不同,因为序列暗示了事件的顺序。
时间是排序事件的直观方式:我们通常都会提到与时间相关的词汇,如时间、时间序列、过去和未来。我们喜欢说 RNN 将信息传播到未来时间步,例如,或者 RNN 捕获时间依赖性。
note RNNs 在第十六章中有详细讲解。
seq2seq 模型使用多个 RNN 实现。单个 RNN 单元如图 18.2 所示;它是 seq2seq 模型架构其余部分的构建块。

图 18.2 RNN 的输入、输出和状态。你可以忽略 RNN 实现细节的复杂性。重要的是你的输入和输出的格式。
首先,你将学习如何堆叠 RNN 来提高模型复杂性。然后你将学习如何将一个 RNN 的隐藏状态传递给另一个 RNN,以便你可以有一个编码器和解码器网络。正如你将开始看到的那样,开始使用 RNN 相对容易。
之后,你将了解将自然语言句子转换为向量序列的介绍。毕竟,RNN 只能理解数值数据,所以你绝对需要这个转换过程。因为 序列 另一种说法是“张量列表”,你需要确保你可以相应地转换你的数据。句子是一系列单词,但单词不是张量。将单词转换为张量或更常见的是向量的过程称为 嵌入。
最后,你将把这些概念结合起来,在真实世界数据上实现 seq2seq 模型。数据将来自数千个电影剧本的对话。
你可以直接使用列表 18.1 中的代码开始运行。打开一个新的 Python 文件,并开始复制列表代码以设置常量和占位符。你将定义占位符的形状为 [None, seq_size, input_dim],其中 None 表示大小是动态的(批处理大小可能会变化),seq_size 是序列的长度,input_dim 是每个序列项的维度。
列表 18.11 设置常量和占位符
import tensorflow as tf ❶
input_dim = 1 ❷
seq_size = 6 ❸
input_placeholder = tf.placeholder(dtype=tf.float32,
shape=[None, seq_size, input_dim])
❶ 所需的一切就是 TensorFlow。
❷ 每个序列元素的维度
❸ 序列的最大长度
为了生成如图 18.2 所示的 RNN 单元,TensorFlow 提供了一个有用的 LSTMCell 类。列表 18.2 展示了如何使用这个类并从单元中提取输出和状态。为了方便,列表定义了一个名为 make_cell 的辅助函数来设置 LSTM RNN 单元。然而,定义一个单元还不够;你还需要调用 tf.nn.dynamic_rnn 来设置网络;
列表 18.2 创建一个简单的 RNN 单元
def make_cell(state_dim):
return tf.contrib.rnn.LSTMCell(state_dim) ❶
with tf.variable_scope("first_cell") as scope:
cell = make_cell(state_dim=10)
outputs, states = tf.nn.dynamic_rnn(cell, ❷
input_placeholder, ❸
dtype=tf.float32)
❶ 查看 tf.contrib.rnn 文档了解其他类型的单元,例如 GRU。
❷ 将生成两个结果:输出和状态。
❸ 输入到 RNN 的序列
你可能还记得,从前几章中你可以通过添加隐藏层来提高神经网络的复杂性。更多的层意味着更多的参数,这很可能意味着模型可以表示更多的函数,因为它更灵活。
你知道吗?你可以堆叠单元。没有什么可以阻止你这样做。这样做会使模型更复杂,因此这个双层 RNN 模型可能会表现得更好,因为它更具表现力。图 18.3 显示了两个堆叠的单元。

图 18.3 你可以将 RNN 单元堆叠以形成一个更复杂的架构。
警告:模型越灵活,越有可能过拟合训练数据。
在 TensorFlow 中,你可以直观地实现这个双层 RNN 网络。首先,为第二个单元创建一个新的变量作用域。为了堆叠 RNN,你可以将第一个单元的输出管道连接到第二个单元的输入,如列表 18.3 所示。
列表 18.3 堆叠两个 RNN 单元
with tf.variable_scope("second_cell") as scope: ❶
cell2 = make_cell(state_dim=10)
outputs2, states2 = tf.nn.dynamic_rnn(cell2,
outputs, ❷
dtype=tf.float32)
❶ 定义变量作用域有助于防止由于变量重用导致的运行时错误。
❷ 输入到这个单元将是另一个单元的输出。
如果你想有四层的 RNN?图 18.4 显示了堆叠了四个 RNN 单元。

图 18.4 TensorFlow 允许你堆叠任意数量的 RNN 单元。
TensorFlow 库提供了一个有用的快捷方式来堆叠单元,称为MultiRNNCell。列表 18.4 展示了如何使用这个辅助函数构建任意大的 RNN 单元。
列表 18.4 使用MultiRNNCell堆叠多个单元
def make_multi_cell(state_dim, num_layers):
cells = [make_cell(state_dim) for _ in range(num_layers)] ❶
return tf.contrib.rnn.MultiRNNCell(cells)
multi_cell = make_multi_cell(state_dim=10, num_layers=4)
outputs4, states4 = tf.nn.dynamic_rnn(multi_cell,
input_placeholder,
dtype=tf.float32)
❶ for 循环语法是构建 RNN 单元列表的首选方式。
到目前为止,你已经通过将一个单元的输出管道到另一个单元的输入来垂直增长 RNN。在 seq2seq 模型中,你将想要一个 RNN 单元来处理输入句子,另一个 RNN 单元来处理输出句子。为了在两个单元之间进行通信,你还可以通过连接单元的状态来水平连接 RNN,如图 18.5 所示。
你已经垂直堆叠了 RNN 单元并将它们水平连接,大大增加了网络中的参数数量。你所做的是不是亵渎神灵?是的。你通过以各种方式组合 RNN 来构建了一个单体架构。但这种方法并非毫无道理,因为这个疯狂的人工神经网络架构是 seq2seq 模型的核心。

图 18.5 你可以使用第一个单元的最后状态作为下一个单元的初始状态。这个模型可以学习从输入序列到输出序列的映射。
如图 18.5 所示,seq2seq 模型似乎有两个输入序列和两个输出序列。但只有输入 1 将被用于输入句子,只有输出 2 将被用于输出句子。
你可能会想知道如何处理其他两个序列。奇怪的是,输出 1 序列在 seq2seq 模型中完全未被使用。而且正如你将看到的,输入 2 序列通过一些输出 2 数据在反馈循环中精心制作。
设计聊天机器人的训练数据将是输入和输出句子的配对,因此你需要更好地理解如何在张量中嵌入单词。第 18.3 节介绍了在 TensorFlow 中如何这样做。
练习 18.3
句子可以表示为字符或单词的序列,但你能否想到其他句子的顺序表示?
答案
两者短语和语法信息(动词、名词等)都可以使用。更常见的是,实际应用使用自然语言处理(NLP)查找来标准化单词形式、拼写和含义。一个执行此转换的库示例是 Facebook 的 fastText (github.com/facebookresearch/ fastText)。
18.3 符号的向量表示
单词和字母是符号,在 TensorFlow 中将符号转换为数值很容易。假设你的词汇表中有四个单词:word[0]: the ; word[1]: fight; word[2]: wind; 和 word[3]: like。
现在假设你想找到句子“Fight the wind.”的嵌入表示。符号fight位于查找表中的索引 1,the位于索引 0,而wind位于索引 2。如果你想找到单词fight的嵌入表示,你必须参考它的索引,即 1,并咨询索引 1 处的查找表以识别嵌入值。在第一个例子中,每个单词都与一个数字相关联,如图 18.6 所示。

图 18.6 从符号到标量的映射
下面的代码片段展示了如何使用 TensorFlow 代码定义符号与数值之间的这种映射:
embeddings_0d = tf.constant([17, 22, 35, 51])
或者,单词可能与向量相关联,如图 18.7 所示。这种方法通常是表示单词的首选方式。你可以在官方 TensorFlow 文档中找到一个关于单词向量表示的详细教程:mng.bz/35M8。

图 18.7 从符号到向量的映射
你可以在 TensorFlow 中实现单词与向量之间的映射,如图 18.5 所示。
列表 18.5 定义 4D 向量的查找表
embeddings_4d = tf.constant([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])
这可能听起来有些过分,但你可以用任何你想要的秩的张量来表示一个符号,而不仅仅是数字(秩 0)或向量(秩 1)。在图 18.8 中,你将符号映射到秩为 2 的张量。

图 18.8 从符号到张量的映射
列表 18.6 展示了如何在 TensorFlow 中实现这种将单词映射到张量的方法。
列表 18.6 定义张量查找表
embeddings_2x2d = tf.constant([[[1, 0], [0, 0]],
[[0, 1], [0, 0]],
[[0, 0], [1, 0]],
[[0, 0], [0, 1]]])
TensorFlow 提供的embedding_lookup函数是一种通过索引访问嵌入的优化方式,如列表 18.7 所示。
列表 18.7 查找嵌入
ids = tf.constant([1, 0, 2]) ❶
lookup_0d = sess.run(tf.nn.embedding_lookup(embeddings_0d, ids))
print(lookup_0d)
lookup_4d = sess.run(tf.nn.embedding_lookup(embeddings_4d, ids))
print(lookup_4d)
lookup_2x2d = sess.run(tf.nn.embedding_lookup(embeddings_2x2d, ids))
print(lookup_2x2d)
❶ 对应于单词 fight、the 和 wind 的嵌入查找
在现实中,嵌入矩阵并不是你需要硬编码的东西。这些列表提供是为了让你理解 TensorFlow 中embedding_lookup函数的细节,因为你很快就会大量使用它。嵌入查找表将通过训练神经网络自动随时间学习。你首先定义一个随机、正态分布的查找表。然后 TensorFlow 的优化器将调整矩阵值以最小化成本。
练习 18.4
按照官方 TensorFlow word2vec 教程www.tensorflow.org/tutorials/word2vec来熟悉嵌入表示。
答案
本教程将教会你如何使用 TensorBoard 和 TensorFlow 来可视化嵌入表示。
18.4 整合所有内容
在神经网络中使用自然语言输入的第一步是确定符号与整数索引之间的映射。表示句子的两种常见方式是字母序列和单词序列。为了简单起见,假设你处理的是字母序列,因此你需要构建字符与整数索引之间的映射。
注意:官方代码仓库可在本书网站(mng.bz/emeQ)和 GitHub(mng.bz/pz8z)上找到。从那里,你可以获取代码运行,无需从书中复制粘贴。
列表 18.8 展示了如何建立整数和字符之间的映射。如果你向这个函数提供一个字符串列表,它将生成两个字典,表示映射。
列表 18.8 提取字符词汇表
def extract_character_vocab(data):
special_symbols = ['<PAD>', '<UNK>', '<GO>', '<EOS>']
set_symbols = set([character for line in data for character in line])
all_symbols = special_symbols + list(set_symbols)
int_to_symbol = {word_i: word
for word_i, word in enumerate(all_symbols)}
symbol_to_int = {word: word_i
for word_i, word in int_to_symbol.items()}
return int_to_symbol, symbol_to_int
input_sentences = ['hello stranger', 'bye bye'] ❶
output_sentences = ['hiya', 'later alligator'] ❷
input_int_to_symbol, input_symbol_to_int =
extract_character_vocab(input_sentences)
output_int_to_symbol, output_symbol_to_int =
extract_character_vocab(output_sentences
❶ 训练输入句子列表
❷ 对应的训练输出句子列表
接下来,你将在列表 18.9 中定义所有超参数和常量。这些元素通常是你可以通过试错手动调整的值。通常,维度或层数的更大值会导致更复杂的模型,如果你有大量数据、快速处理能力和大量时间,这将是有益的。
列表 18.9 定义超参数
NUM_EPOCS = 300 ❶
RNN_STATE_DIM = 512 ❷
RNN_NUM_LAYERS = 2 ❸
ENCODER_EMBEDDING_DIM = DECODER_EMBEDDING_DIM = 64 ❹
BATCH_SIZE = int(32)
LEARNING_RATE = 0.0003
INPUT_NUM_VOCAB = len(input_symbol_to_int) ❺
OUTPUT_NUM_VOCAB = len(output_symbol_to_int) ❻
❶ 训练轮数
❷ RNN 的隐藏维度大小
❸ RNN 的堆叠单元数量
❹ 编码器和解码器序列元素的嵌入维度
❺ 批处理大小
❻ 编码器和解码器之间可能具有不同的词汇表。
接下来,列出所有占位符。如列表 18.10 所示,占位符很好地组织了训练网络所需的输入和输出序列。你需要跟踪序列及其长度。对于解码器部分,你还需要计算最大序列长度。这些占位符形状中的 None 值表示张量可以在该维度上具有任意大小。例如,批处理大小可能在每次运行中变化。但为了简单起见,你将始终保持批处理大小不变。
列表 18.10 列表占位符
# Encoder placeholders
encoder_input_seq = tf.placeholder( ❶
tf.int32,
[None, None], ❷
name='encoder_input_seq'
)
encoder_seq_len = tf.placeholder( ❸
tf.int32,
(None,), ❹
name='encoder_seq_len'
)
# Decoder placeholders
decoder_output_seq = tf.placeholder( ❺
tf.int32,
[None, None], ❻
name='decoder_output_seq'
)
decoder_seq_len = tf.placeholder( ❼
tf.int32,
(None,), ❽
name='decoder_seq_len'
)
max_decoder_seq_len = tf.reduce_max( ❾
decoder_seq_len,
name='max_decoder_seq_len'
)
❶ 编码器输入的整数序列
❷ 形状是批处理大小 × 序列长度。
❸ 批处理中序列的长度
❹ 形状是动态的,因为序列的长度可以改变。
❺ 解码器输出的整数序列
❻ 形状是批处理大小 × 序列长度。
❼ 批处理中序列的长度
❽ 形状是动态的,因为序列的长度可以改变。
❾ 批处理中解码器序列的最大长度
让我们定义辅助函数来构建 RNN 单元。这些函数,如列表 18.11 所示,应该对你来说在 18.3 节中已经熟悉了。
列表 18.11 构建 RNN 单元的辅助函数
def make_cell(state_dim):
lstm_initializer = tf.random_uniform_initializer(-0.1, 0.1)
return tf.contrib.rnn.LSTMCell(state_dim, initializer=lstm_initializer)
def make_multi_cell(state_dim, num_layers):
cells = [make_cell(state_dim) for _ in range(num_layers)]
return tf.contrib.rnn.MultiRNNCell(cells)
你将通过使用你定义的辅助函数来构建编码器和解码器 RNN 单元。作为提醒,我已经为你复制了图 18.9 中的 seq2seq 模型,以可视化编码器和解码器 RNN。

图 18.9 seq2seq 模型通过使用编码器 RNN 和解码器 RNN 来学习输入序列到输出序列之间的转换。
让我们先谈谈编码器单元部分,因为在列表 18.12 中,您将构建编码器单元。编码器 RNN 产生的状态将存储在一个名为encoder_state的变量中。RNN 还会产生一个输出序列,但在标准的 seq2seq 模型中,您不需要访问它,因此您可以忽略或删除它。
将字母或单词转换为向量表示也是常见的,通常称为嵌入。TensorFlow 提供了一个方便的函数embed_sequence,可以帮助您将符号的整数表示嵌入。图 18.10 显示了编码器如何从查找表中接受数值。您可以在列表 18.13 的开头看到编码器的实际操作。

图 18.10 RNN 只接受数值序列作为输入或输出,因此您需要将符号转换为向量。在这种情况下,符号是单词,如 the、fight、wind 和 like。它们对应的向量与嵌入矩阵相关联。
列表 18.12 编码器嵌入和单元
encoder_input_embedded = tf.contrib.layers.embed_sequence(
encoder_input_seq, ❶
INPUT_NUM_VOCAB, ❷
ENCODER_EMBEDDING_DIM ❸
)
# Encoder output
encoder_multi_cell = make_multi_cell(RNN_STATE_DIM, RNN_NUM_LAYERS)
encoder_output, encoder_state = tf.nn.dynamic_rnn(
encoder_multi_cell,
encoder_input_embedded,
sequence_length=encoder_seq_len,
dtype=tf.float32
)
del(encoder_output) ❹
❶ 输入数字序列(行索引)
❷ 嵌入矩阵的行
❸ 嵌入矩阵的列
❹ 您不需要坚持这个值。
解码器 RNN 的输出是一个表示自然语言句子和表示序列结束的特殊符号的数值序列。您将这个序列结束符号标记为<EOS>。图 18.11 说明了这个过程。
解码器 RNN 的输入序列将类似于解码器的输出序列,但每个句子的末尾没有<EOS>(序列结束)特殊符号,而是在开头有一个<GO>特殊符号。这样,解码器从左到右读取输入后,开始时没有关于答案的额外信息,这使得它成为一个健壮的模型。

图 18.11 解码器的输入前缀了一个特殊的<GO>符号,而输出后缀了一个特殊的<EOS>符号。
列表 18.13 展示了如何执行这些切片和连接操作。解码器输入的新序列将被称为decoder_input_seq。您将使用 TensorFlow 的tf.concat操作将矩阵粘合在一起。在列表中,您定义了一个go_prefixes矩阵,它将是一个只包含<GO>符号的列向量。
列表 18.13 准备解码器的输入序列
decoder_raw_seq = decoder_output_seq[:, :-1] ❶
go_prefixes = tf.fill([BATCH_SIZE, 1], output_symbol_to_int['<GO>']) ❷
decoder_input_seq = tf.concat([go_prefixes, decoder_raw_seq], 1) ❸
❶ 通过忽略最后一列来裁剪矩阵
❷ 创建一个包含
❸ 将
现在我们来构建解码器单元。如列表 18.14 所示,您首先将解码器整数序列嵌入到向量序列中,称为decoder_input_embedded。
输入序列的嵌入版本将被馈送到解码器的 RNN 中,因此创建解码器 RNN 单元。还有一件事:你需要一个层将解码器的输出映射到词汇表的一个热编码表示,你称之为output_layer。设置解码器的过程开始时与设置编码器的过程相似。
列表 18.14 解码器嵌入和单元
decoder_embedding = tf.Variable(tf.random_uniform([OUTPUT_NUM_VOCAB,
DECODER_EMBEDDING_DIM]))
decoder_input_embedded = tf.nn.embedding_lookup(decoder_embedding,
decoder_input_seq)
decoder_multi_cell = make_multi_cell(RNN_STATE_DIM, RNN_NUM_LAYERS)
output_layer_kernel_initializer =
tf.truncated_normal_initializer(mean=0.0, stddev=0.1)
output_layer = Dense(
OUTPUT_NUM_VOCAB,
kernel_initializer = output_layer_kernel_initializer
)
好吧,这里事情变得奇怪了。你有两种方法来检索解码器的输出:在训练期间和推理期间。训练解码器仅在训练期间使用,而推理解码器用于对从未见过的数据进行测试。
存在两种获取输出序列的方法的原因是,在训练过程中,你有可用的事实数据,因此你可以使用有关已知输出的信息来帮助加速学习过程。但在推理过程中,你没有事实输出标签,因此你必须求助于仅使用输入序列进行推理。
列表 18.15 实现了训练解码器。你将decoder_input_seq输入到解码器的输入中,使用TrainingHelper。这个辅助操作为你管理解码器 RNN 的输入。
列表 18.15 解码器输出(训练)
with tf.variable_scope("decode"):
training_helper = tf.contrib.seq2seq.TrainingHelper(
inputs=decoder_input_embedded,
sequence_length=decoder_seq_len,
time_major=False
)
training_decoder = tf.contrib.seq2seq.BasicDecoder(
decoder_multi_cell,
training_helper,
encoder_state,
output_layer
)
training_decoder_output_seq, _, _ = tf.contrib.seq2seq.dynamic_decode(
training_decoder,
impute_finished=True,
maximum_iterations=max_decoder_seq_len
)
如果你关心从测试数据中获取 seq2seq 模型的输出,你将不再能够访问decoder_input_seq。为什么?因为解码器输入序列是从解码器输出序列派生出来的,而输出序列仅在训练数据集中可用。
列表 18.16 实现了推理情况的解码器输出操作。在这里,你将再次使用辅助操作向解码器提供一个输入序列。
列表 18.16 解码器输出(推理)
with tf.variable_scope("decode", reuse=True):
start_tokens = tf.tile(
tf.constant([output_symbol_to_int['<GO>']],
dtype=tf.int32),
[BATCH_SIZE],
name='start_tokens')
inference_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper( ❶
embedding=decoder_embedding, ❶
start_tokens=start_tokens, ❶
end_token=output_symbol_to_int['<EOS>'] ❶
) ❶
inference_decoder = tf.contrib.seq2seq.BasicDecoder( ❷
decoder_multi_cell, ❷
inference_helper, ❷
encoder_state, ❷
output_layer ❷
) ❷
inference_decoder_output_seq, _, _ = tf.contrib.seq2seq.dynamic_decode( ❸
inference_decoder, ❸
impute_finished=True, ❸
maximum_iterations=max_decoder_seq_len ❸
) ❸
❶ 辅助推理过程
❷ 基本解码器
❸ 通过使用解码器执行动态解码
使用 TensorFlow 的sequence_loss方法计算成本。你需要访问推断的解码器输出序列和事实输出序列。列表 18.17 在代码中定义了cost函数。
列表 18.17 cost函数
training_logits = ❶
tf.identity(training_decoder_output_seq.rnn_output, name='logits') ❶
inference_logits = ❶
tf.identity(inference_decoder_output_seq.sample_id, name='predictions') ❶
masks = tf.sequence_mask( ❷
decoder_seq_len, ❷
max_decoder_seq_len, ❷
dtype=tf.float32, ❷
name='masks' ❷
) ❷
cost = tf.contrib.seq2seq.sequence_loss( ❸
training_logits, ❸
decoder_output_seq, ❸
masks ❸
) ❸
❶ 为你的方便重命名张量
❷ 创建序列损失的权重
❸ 使用 TensorFlow 的内置序列损失函数
最后,调用一个优化器来最小化成本。但你会做一个你可能从未见过的技巧。在像这样的深度网络中,你需要限制极端梯度变化,以确保梯度不会发生太大的变化,使用的技术称为梯度裁剪。列表 18.18 展示了如何操作。
练习 18.5
尝试不使用梯度裁剪的 seq2seq 模型,体验一下差异。
答案
你会注意到,没有梯度裁剪时,网络有时会过度调整梯度,导致数值不稳定性。
列表 18.18 调用一个优化器
optimizer = tf.train.AdamOptimizer(LEARNING_RATE)
gradients = optimizer.compute_gradients(cost)
capped_gradients = [(tf.clip_by_value(grad, -5., 5.), var) ❶
for grad, var in gradients if grad is not None]
train_op = optimizer.apply_gradients(capped_gradients)
❶ 梯度裁剪
该列表总结了 seq2seq 模型的实现。一般来说,在设置优化器后,模型就准备好训练了,如列表 18.18 所示。您可以通过运行 train_op 并使用训练数据批次来创建会话并学习模型的参数。
哦,对了——你需要从某处获取训练数据!你怎么能获得成千上万对输入和输出句子?别担心;第 18.5 节涵盖了该过程。
18.5 收集对话数据
康奈尔电影对话语料库 (mng.bz/W28O) 是一个包含超过 60 部电影中 22 万多段对话的数据集。您可以从官方网站下载 zip 文件。
警告 由于数据量巨大,您可能预计训练算法需要很长时间。如果您的 TensorFlow 库配置为仅使用 CPU,训练可能需要整整一天。在 GPU 上,训练此网络可能需要 30 分钟到 1 小时。
这里是两个人(A 和 B)之间来回对话的小片段示例:
A: 他们不做!
B: 他们也这样做!
A: 好的。
因为聊天机器人的目标是针对每个可能的输入话语产生智能输出,所以您将根据对话的偶然对来构建您的训练数据。在示例中,对话生成了以下输入和输出句子的对:
-
“他们不做!”®“他们也这样做!”
-
“他们也这样做!”®“好的。”
为了您的方便,我们已处理数据并将其在线提供给您。您可以在 mng.bz/OvlE 找到它。完成下载后,您可以运行列表 18.19,它使用了 GitHub 仓库下 Listing 18-eoc-assign.ipynb Jupyter 笔记本中的 load_sentences 辅助函数。
列表 18.19 训练模型
input_sentences = load_sentences('data/words_input.txt') ❶
output_sentences = load_sentences('data/words_output.txt') ❷
input_seq = [
[input_symbol_to_int.get(symbol, input_symbol_to_int['<UNK>'])
for symbol in line] ❸
for line in input_sentences ❹
]
output_seq = [
[output_symbol_to_int.get(symbol, output_symbol_to_int['<UNK>'])
for symbol in line] + [output_symbol_to_int['<EOS>']] ❺
for line in output_sentences ❻
]
sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver() ❼
for epoch in range(NUM_EPOCS + 1): ❽
for batch_idx in range(len(input_sentences) // BATCH_SIZE): ❾
input_data, output_data = get_batches(input_sentences, ❿
output_sentences,
batch_idx)
input_batch, input_lenghts = input_data[batch_idx]
output_batch, output_lengths = output_data[batch_idx]
_, cost_val = sess.run( ⓫
[train_op, cost],
feed_dict={
encoder_input_seq: input_batch,
encoder_seq_len: input_lengths,
decoder_output_seq: output_batch,
decoder_seq_len: output_lengths
}
)
saver.save(sess, 'model.ckpt')
sess.close()
❶ 将输入句子作为字符串列表加载
❷ 以相同的方式加载相应的输出句子
❸ 遍历字母
❹ 遍历文本行
❺ 将 EOS 符号追加到输出数据的末尾
❻ 遍历行
❼ 保存学习到的参数是个好主意。
❽ 遍历时代
❾ 通过批次数进行遍历
❿ 获取当前批次的输入和输出对
⓫ 在当前批次上运行优化器
因为您已将模型参数保存到文件中,您可以轻松地将模型加载到另一个程序中,并查询网络对新输入的响应。运行 inference_logits 操作以获取聊天机器人的响应。
摘要
-
TensorFlow 可以根据您到目前为止从书中获得的知识构建 seq2seq 神经网络。
-
您可以在 TensorFlow 中嵌入自然语言。
-
RNN 可以用作构建更复杂模型的基石。
-
在对电影剧本中的对话示例进行模型训练后,您可以像对待聊天机器人一样处理该算法,从自然输入中推断出自然语言响应。
19 效用景观
本章涵盖了
-
实现用于排名的神经网络
-
使用 VGG16 进行图像嵌入
-
可视化效用
处理感官输入使机器人能够调整他们对周围世界的模型。在吸尘机器人案例中,房间里的家具可能会每天变化,因此机器人必须能够适应混乱的环境。
假设你拥有一台未来派的家用机器人女仆,它具备一些基本技能,同时也能从人类的演示中学习新技能。也许你想教它如何叠衣服。
教机器人完成一项新任务是棘手的问题。一些直接的问题会浮现在脑海中:
-
机器人是否应该简单地模仿人类的动作序列?这样的过程被称为模仿学习。
-
机器人的手臂和关节如何与人类姿势相匹配?这个问题通常被称为对应问题。
在本章中,你将模拟一个从人类演示中学习到的任务,同时避免模仿学习和对应问题。真是太幸运了!你将通过研究一种使用效用函数对世界状态进行排名的方法来完成这个任务,该函数接受一个状态并返回一个表示其可取性的实数值。你不仅将避免将模仿作为成功衡量标准,还将绕过将机器人的动作集映射到人类动作集的复杂性(对应问题)。
练习 19.1
模仿学习的目标是让机器人重现演示者的动作序列。这个目标在纸上听起来不错,但这种方法有哪些局限性?
答案
模仿人类动作是从人类演示中学习的一种天真方法。相反,代理应该识别演示背后的隐藏目标。例如,当有人叠衣服时,目标是使衣物变平并压缩,这些概念与人类的手部动作无关。通过理解人类为何产生他们的动作序列,代理能够更好地概括它所教授的技能。
在第 19.1 节中,你将学习如何通过人类演示任务的视频实现世界状态的效用函数。学习到的效用函数是偏好模型。
你将探索如何教机器人如何折叠衣物。一件皱巴巴的衣物几乎肯定处于一种以前从未见过的配置。如图 19.1 所示,效用框架对状态空间的大小没有限制。偏好模型专门在人们以各种方式折叠 T 恤的视频上训练。

图 19.1 穿着皱巴巴的衣服比叠得好的衣服状态更差。此图展示了如何评估一块布的每种状态;分数越高代表状态越佳。
效用函数可以推广到各种状态(新颖配置的皱巴巴的 T 恤与熟悉配置的折叠 T 恤)并在服装(T 恤折叠与裤子折叠)之间重用知识。
我们可以用以下论点进一步说明良好效用函数的实际应用:在现实世界中,并非所有视觉观察都是针对学习任务进行优化的。演示技能的教师可能执行无关、不完整甚至错误的行为,但人类能够忽略这些错误。
当机器人观看人类演示时,你希望它能理解完成任务所涉及的因果关系。你的工作使得学习阶段可以互动,机器人对人类行为持积极怀疑态度,以完善训练数据。
为了实现这个目标,你首先从少量视频中学习一个效用函数来对各种状态的偏好进行排名。然后,当机器人通过人类演示展示一项新技能的实例时,它会咨询效用函数以验证预期的效用随时间增加。最后,机器人中断人类演示,询问该动作是否对于学习技能是必要的。
19.1 偏好模型
我们假设人类的偏好是从一个功利主义的角度得出的,这意味着一个数字决定了物品的排名。假设你调查了人们对各种食物的精致程度(如牛排、热狗、虾尾和汉堡)进行排名。
图 19.2 展示了食物成对之间的一些可能排名。正如你所预期的那样,在精致程度上,牛排比热狗排名更高,虾尾比汉堡排名更高。

图 19.2 展示了一组可能的对象成对排名。具体来说,你有四种食物,你想根据精致程度对它们进行排名,因此你采用了两个成对排名决策:牛排比热狗更精致,虾尾比汉堡更精致。
幸运的是,对于被调查的个人来说,并不是每一对物品都需要进行排名。可能并不明显的是,热狗和汉堡或牛排和虾尾之间哪个更精致。存在很多不同的意见空间。
如果状态 s[1] 的效用高于另一个状态 s[2],则相应的排名表示为 s[1] > s[2],这意味着 s[1] 的效用大于 s[2] 的效用。每个视频演示包含一个由 n 个状态 s[0],s[1],...,s[n] 组成的序列,提供了 n(n - 1)/2 个可能的有序对排名约束。
让我们实现一个能够进行排名的自己的神经网络。打开一个新的源文件,并使用列表 19.1 导入相关库。你即将创建一个神经网络,根据偏好对来学习效用函数。
列表 19.1 导入相关库
import tensorflow as tf
import numpy as np
import random
%matplotlib inline
import matplotlib.pyplot as plt
要学习基于效用分数对状态进行排名的神经网络,你需要训练数据。让我们先创建一些虚拟数据;你将在稍后用更真实的数据替换它。通过使用列表 19.2 重现图 19.3 中的 2D 数据。

图 19.3 你将工作的示例数据。圆圈代表更受欢迎的状态,而十字代表不太受欢迎的状态。由于数据成对出现,因此圆圈和十字的数量相等;每一对都是一个排名,如图 19.2 所示。
列表 19.2 生成虚拟训练数据
n_features = 2 ❶
def get_data():
data_a = np.random.rand(10, n_features) + 1 ❷
data_b = np.random.rand(10, n_features) ❸
plt.scatter(data_a[:, 0], data_a[:, 1], c='r', marker='x')
plt.scatter(data_b[:, 0], data_b[:, 1], c='g', marker='o')
plt.show()
return data_a, data_b
data_a, data_b = get_data()
❶ 你将生成 2D 数据,这样你可以轻松地可视化它。
❷ 应该产生更高效用值的点的集合
❸ 不太受欢迎的点的集合
接下来,你需要定义超参数。在这个模型中,让我们保持简单,通过保持架构浅层。你将创建一个包含一个隐藏层的网络。决定隐藏层神经元数量的相应超参数是
n_hidden = 10
排名神经网络将接收成对输入,因此你需要有两个单独的占位符——每个部分一个。此外,你将创建一个占位符来保存dropout参数值。继续通过将列表 19.3 添加到你的脚本中。
列表 19.3 占位符
with tf.name_scope("input"):
x1 = tf.placeholder(tf.float32, [None, n_features], name="x1") ❶
x2 = tf.placeholder(tf.float32, [None, n_features], name="x2") ❷
dropout_keep_prob = tf.placeholder(tf.float32, name='dropout_prob')
❶ 受欢迎点的输入占位符
❷ 不受欢迎点的输入占位符
排名神经网络将只包含一个隐藏层。在列表 19.4 中,你定义了权重和偏差,然后在这些两个输入占位符的每个上重用这些权重和偏差。
列表 19.4 隐藏层
with tf.name_scope("hidden_layer"):
with tf.name_scope("weights"):
w1 = tf.Variable(tf.random_normal([n_features, n_hidden]), name="w1")
tf.summary.histogram("w1", w1)
b1 = tf.Variable(tf.random_normal([n_hidden]), name="b1")
tf.summary.histogram("b1", b1)
with tf.name_scope("output"):
h1 = tf.nn.dropout(tf.nn.relu(tf.matmul(x1,w1) + b1), keep_prob=dropout_keep_prob)
tf.summary.histogram("h1", h1)
h2 = tf.nn.dropout(tf.nn.relu(tf.matmul(x2, w1) + b1), keep_prob=dropout_keep_prob)
tf.summary.histogram("h2", h2)
神经网络的目标是计算两个输入的分数。在列表 19.5 中,你定义了网络的输出层的权重、偏差和全连接架构。你将剩下两个输出向量:s1和s2,代表成对输入的分数。
列表 19.5 输出层
with tf.name_scope("output_layer"):
with tf.name_scope("weights"):
w2 = tf.Variable(tf.random_normal([n_hidden, 1]), name="w2")
tf.summary.histogram("w2", w2)
b2 = tf.Variable(tf.random_normal([1]), name="b2")
tf.summary.histogram("b2", b2)
with tf.name_scope("output"):
s1 = tf.matmul(h1, w2) + b2 ❶
s2 = tf.matmul(h2, w2) + b2 ❷
❶ 输入 x1 的效用分数
❷ 输入 x2 的效用分数
你将假设在训练神经网络时,x1应包含不太受欢迎的项目。s1应得分低于s2,因此s1和s2之间的差异应该是负数。正如列表 19.6 所示,loss函数通过使用 softmax 交叉熵损失来尝试保证负数差异。你将定义一个train_op来最小化loss函数。
列表 19.6 损失和优化器
with tf.name_scope("loss"):
s12 = s1 - s2
s12_flat = tf.reshape(s12, [-1])
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
labels=tf.zeros_like(s12_flat),
logits=s12_flat + 1)
loss = tf.reduce_mean(cross_entropy)
tf.summary.scalar("loss", loss)
with tf.name_scope("train_op"):
train_op = tf.train.AdamOptimizer(0.001).minimize(loss)
现在按照列表 19.7 设置 TensorFlow 会话,这涉及到初始化所有变量,并使用摘要编写器准备 TensorBoard 调试。
注意:你在第二章的结尾使用了摘要编写器,当时你被介绍到了 TensorBoard。
列表 19.7 准备会话
sess = tf.InteractiveSession()
summary_op = tf.summary.merge_all()
writer = tf.summary.FileWriter("tb_files", sess.graph)
init = tf.global_variables_initializer()
sess.run(init)
你已经准备好训练网络了!在生成的虚拟数据上运行train_op来学习模型的参数(列表 19.8)。
列表 19.8 训练网络
for epoch in range(0, 10000):
loss_val, _ = sess.run([loss, train_op], feed_dict={x1:data_a, x2:data_b, ❶
➥ dropout_keep_prob:0.5}) ❶
if epoch % 100 == 0 :
summary_result = sess.run(summary_op,
feed_dict={x1:data_a, ❷
x2:data_b, ❸
dropout_keep_prob:1}) ❹
writer.add_summary(summary_result, epoch)
❶ 训练 dropout keep_prob 为 0.5。
❷ 受欢迎的点
❸ 不受欢迎的点
❹ 测试 dropout 的 keep_prob 应该始终为 1。
最后,可视化学习到的得分函数。如图 19.9 所示,将二维点添加到列表中。
列表 19.9 准备测试数据
grid_size = 10
data_test = []
for y in np.linspace(0., 1., num=grid_size): ❶
for x in np.linspace(0., 1., num=grid_size): ❷
data_test.append([x, y])
❶ 遍历行
❷ 遍历列
你将在测试数据上运行s1操作以获得每个状态的效用值,并如图 19.4 所示可视化这些数据。使用列表 19.10 生成可视化。

图 19.4 排名神经网络学习到的得分景观
列表 19.10 可视化结果
def visualize_results(data_test):
plt.figure()
scores_test = sess.run(s1, feed_dict={x1:data_test, dropout_keep_prob:1})❶
scores_img = np.reshape(scores_test, [grid_size, grid_size]) ❷
plt.imshow(scores_img, origin='lower')
plt.colorbar()
visualize_results(data_test)
❶ 计算所有点的效用
❷ 将效用重塑为矩阵,以便您可以使用 Matplotlib 可视化图像
19.2 图像嵌入
在第十八章中,你大胆地给神经网络喂了一些自然语言句子。你是通过将句子中的单词或字母转换为数值形式,如向量来做到这一点的。每个符号(无论是单词还是字母)都通过查找表嵌入到向量中。
练习 19.2
为什么将符号转换为向量表示的查找表称为嵌入矩阵?
答案
符号正在被嵌入到向量空间中。
幸运的是,图像已经是数值形式,表示为像素矩阵。如果图像是灰度的,像素可能代表亮度值的标量。对于彩色图像,每个像素代表颜色强度(通常是三个:红色、绿色和蓝色)。无论如何,图像都可以通过数值数据结构,如 TensorFlow 中的张量,轻松表示。
练习 19.3
拍摄一个家庭用品的照片,比如一把椅子。将图像缩小,直到你不能再识别物体。你最终缩小了多少倍?原始图像中的像素数与较小图像中的像素数之比是多少?这个比率是数据冗余的粗略度量。
答案
一台典型的 500 万像素相机以 2560 × 1920 的分辨率生成图像,但当你将其缩小 40 倍(分辨率 64 × 48)时,图像的内容可能仍然可以辨认。
向神经网络输入一个大图像——比如说,大小为 1280 × 720(近 100 万个像素)——会增加参数的数量,从而增加模型过拟合的风险。图像中的像素高度冗余,因此您可以尝试以更简洁的表示捕捉图像的精髓。图 19.5 显示了折叠衣物图像的 2D 嵌入中形成的簇。

图 19.5 图像可以嵌入到更低的维度,例如 2D(如图所示)。注意,表示衬衫相似状态的点出现在附近的簇中。嵌入图像允许您使用排名神经网络学习布料状态的偏好。
你在 11 章和 12 章中看到了如何使用自编码器来降低图像的维度。另一种常见的实现图像低维嵌入的方法是使用深度卷积神经网络图像分类器的倒数第二层。让我们更详细地探讨后者。
因为设计、实现和学习深度图像分类器不是本章的主要焦点(有关 CNNs,请参阅第十四章和第十五章),所以你将使用现成的预训练模型。许多计算机视觉研究论文中引用的一个常见图像分类器是 VGG16;你在第十五章中使用它构建了人脸识别系统。
对于 TensorFlow,VGG16 的许多在线实现都存在。我推荐使用 Davi Frossard 的版本(www.cs.toronto.edu/~frossard/post/vgg16)。你可以从他的网站下载vgg16.py TensorFlow 代码和vgg16_weights.npz预训练模型参数。
图 19.6 展示了 Frossard 页面上的 VGG16 神经网络。正如你所见,它是一个深度神经网络,包含许多卷积层。最后几层是常规的完全连接层,输出层是一个 1000D 向量,表示多类分类的概率。

图 19.6 VGG16 架构是一个用于图像分类的深度卷积神经网络。这个特定的图表来自www.cs.toronto.edu/~frossard/post/vgg16.。
学习如何导航他人的代码是一项不可或缺的技能。首先,确保你已经下载了vgg16.py和vgg16_weights.npz,并通过使用python vgg16.py my_image.png测试你能否运行代码。
注意:你可能需要安装 SciPy 和 Pillow 来确保 VGG16 演示代码无问题地运行。你可以通过 pip 下载这两个库。
首先,让我们添加 TensorBoard 集成来可视化代码中的情况。在主函数中,在创建会话变量sess之后,插入以下代码行:
my_writer = tf.summary.FileWriter('tb_files', sess.graph)
现在再次运行分类器(python vgg16.py my_image.png)将生成一个名为tb_files的目录,该目录将由 TensorBoard 使用。你可以运行 TensorBoard 来可视化神经网络的计算图。以下命令运行 TensorBoard:
$ tensorboard —logdir=tb_files
在浏览器中打开 TensorBoard,导航到“图形”选项卡以查看计算图,如图 19.7 所示。一眼望去,你可以了解网络中涉及的层类型;最后三层是标记为 fc1、fc2 和 fc3 的完全连接密集层。

图 19.7 展示了 TensorBoard 中 VGG16 神经网络的计算图的一个小片段。最顶部的节点是用于分类的 softmax 运算符。三个完全连接的层分别标记为 fc1、fc2 和 fc3。
19.3 图像排名
你将使用第 19.2 节中的 VGG16 代码来获取图像的向量表示。这样,你可以在第 12.1 节中设计的排名神经网络中有效地对两个图像进行排名。
考虑图 19.8 所示的衬衫折叠视频。你将逐帧处理视频以对图像的状态进行排名。这样,在新的情况下,算法可以理解布折叠的目标是否已经达到。

图 19.8 衬衫折叠的视频展示了布料随时间如何改变形状。你可以将衬衫的第一个状态和最后一个状态作为你的训练数据来学习一个用于排名的状态效用函数。每个视频中的衬衫最终状态应该比视频开头附近的衬衫具有更高的效用。
首先,从mng.bz/eZsc下载布折叠数据集。解压 zip 文件。注意你解压文件的位置;在代码列表中,你将称该位置为DATASET_DIR。
打开一个新的源文件,并在 Python 中导入相关库(列表 19.11)。
列表 19.11 导入库
import tensorflow as tf
import numpy as np
from vgg16 import vgg16
import glob, os
from scipy.misc import imread, imresize
对于每个视频,你将记住第一个和最后一个图像。这样,你可以通过假设最后一个图像比第一个图像更受偏好来训练排名算法。换句话说,布折叠的最后一个状态比布折叠的第一个状态带给你更高的价值状态。列表 19.12 显示了如何将数据加载到内存中。
列表 19.12 准备训练数据
DATASET_DIR = os.path.join(os.path.expanduser('~'), 'res', ❶
➥ 'cloth_folding_rgb_vids') ❶
NUM_VIDS = 45 ❷
def get_img_pair(video_id): ❸
img_files = sorted(glob.glob(os.path.join(DATASET_DIR, video_id,
➥ '*.png')))
start_img = img_files[0]
end_img = img_files[-1]
pair = []
for image_file in [start_img, end_img]:
img_original = imread(image_file)
img_resized = imresize(img_original, (224, 224))
pair.append(img_resized)
return tuple(pair)
start_imgs = []
end_imgs= []
for vid_id in range(1, NUM_VIDS + 1):
start_img, end_img = get_img_pair(str(vid_id))
start_imgs.append(start_img)
end_imgs.append(end_img)
print('Images of starting state {}'.format(np.shape(start_imgs)))
print('Images of ending state {}'.format(np.shape(end_imgs)))
❶ 下载文件目录
❷ 要加载的视频数量
❸ 获取视频的开始和结束图像
运行列表 19.12 的结果如下:
Images of starting state (45, 224, 224, 3)
Images of ending state (45, 224, 224, 3)
使用列表 19.13 创建一个用于嵌入的图像输入占位符。
列表 19.13 占位符
imgs_plc = tf.placeholder(tf.float32, [None, 224, 224, 3])
将列表 19.3-19.7 中的排名神经网络代码复制过来;你将重用它来排名图像。然后准备列表 19.14 中的会话。
列表 19.14 准备会话
sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
接下来,你将通过调用构造函数来初始化 VGG16 模型。这样做,如列表 19.15 所示,将从磁盘将所有模型参数加载到内存中。
列表 19.15 加载 VGG16 模型
print('Loading model...')
vgg = vgg16(imgs_plc, 'vgg16_weights.npz', sess)
print('Done loading!')
接下来,为排名神经网络准备训练和测试数据。如列表 19.16 所示,你将向 VGG16 模型提供你的图像;然后你将访问输出附近的一层(在这种情况下,fc1)以获取图像嵌入。
最后,你将得到一个 4096D 的图像嵌入。因为你总共有 45 个视频,所以你需要将它们分割,一些用于训练,一些用于测试:
-
训练
-
-
开始帧大小:(33, 4096)
-
结束帧大小:(33, 4096)
-
-
测试
-
-
开始帧大小:(12, 4096)
-
结束帧大小:(12, 4096)
-
列表 19.16 准备排名数据
start_imgs_embedded = sess.run(vgg.fc1, feed_dict={vgg.imgs: start_imgs})
end_imgs_embedded = sess.run(vgg.fc1, feed_dict={vgg.imgs: end_imgs})
idxs = np.random.choice(NUM_VIDS, NUM_VIDS, replace=False)
train_idxs = idxs[0:int(NUM_VIDS * 0.75)]
test_idxs = idxs[int(NUM_VIDS * 0.75):]
train_start_imgs = start_imgs_embedded[train_idxs]
train_end_imgs = end_imgs_embedded[train_idxs]
test_start_imgs = start_imgs_embedded[test_idxs]
test_end_imgs = end_imgs_embedded[test_idxs]
print('Train start imgs {}'.format(np.shape(train_start_imgs)))
print('Train end imgs {}'.format(np.shape(train_end_imgs)))
print('Test start imgs {}'.format(np.shape(test_start_imgs)))
print('Test end imgs {}'.format(np.shape(test_end_imgs)))
在你的排名训练数据准备好后,运行train_op一个epoch次数(列表 19.17)。在训练网络后,在测试数据上运行模型以评估你的结果。
列表 19.17 训练排名网络
train_y1 = np.expand_dims(np.zeros(np.shape(train_start_imgs)[0]), axis=1)
train_y2 = np.expand_dims(np.ones(np.shape(train_end_imgs)[0]), axis=1)
for epoch in range(100):
for i in range(np.shape(train_start_imgs)[0]):
_, cost_val = sess.run([train_op, loss],
feed_dict={x1: train_start_imgs[i:i+1,:],
x2: train_end_imgs[i:i+1,:],
dropout_keep_prob: 0.5})
print('{}. {}'.format(epoch, cost_val))
s1_val, s2_val = sess.run([s1, s2], feed_dict={x1: test_start_imgs,
x2: test_end_imgs,
dropout_keep_prob: 1})
print('Accuracy: {}%'.format(100 * np.mean(s1_val < s2_val)))
注意,随着时间的推移,准确性接近 100%。你的排名模型学习到视频末尾出现的图像比接近视频开头出现的图像更有利。
出于好奇,让我们看看单个视频随时间变化的效用,如图 19.9 所示,逐帧展示。重现图 19.9 的代码需要加载视频中的所有图像,如列表 19.18 所述。

图 19.9 随着时间的推移,效用增加,表明目标正在实现。视频开头附近的布料效用接近 0,但增加到 120,000 单位。
列表 19.18 从视频中准备图像序列
def get_img_seq(video_id):
img_files = sorted(glob.glob(os.path.join(DATASET_DIR, video_id,
➥ '*.png')))
imgs = []
for image_file in img_files:
img_original = imread(image_file)
img_resized = imresize(img_original, (224, 224))
imgs.append(img_resized)
return imgs
imgs = get_img_seq('1')
你可以使用你的 VGG16 模型嵌入图像,然后运行排名网络来计算分数,如列表 19.19 所示。
列表 19.19 计算图像的效用
imgs_embedded = sess.run(vgg.fc1, feed_dict={vgg.imgs: imgs})
scores = sess.run([s1], feed_dict={x1: imgs_embedded,
dropout_keep_prob: 1})
可视化你的结果以重现图 19.9(列表 19.20)。
列表 19.20 可视化效用分数
from matplotlib import pyplot as plt
plt.figure()
plt.title('Utility of cloth-folding over time')
plt.xlabel('time (video frame #)')
plt.ylabel('Utility')
plt.plot(scores[-1])
摘要
-
你可以通过将对象表示为向量并学习这样的向量上的效用函数来对状态进行排名。
-
由于图像包含冗余数据,你使用了 VGG16 神经网络来降低数据的维度,以便可以使用真实世界图像的排名网络。
-
你学习了如何在视频中可视化图像随时间的变化,以验证视频演示增加了布料的效用。
接下来是什么
你已经完成了你的 TensorFlow 之旅!本书的 19 章从不同的角度探讨了机器学习,但它们共同教会了你掌握这些技能所需的概念:
-
将任意真实世界问题表述为机器学习框架
-
理解许多机器学习问题的基本原理
-
使用 TensorFlow 解决这些问题
-
可视化机器学习算法并使用专业术语
-
使用真实世界的数据和问题来展示你所学的知识
由于本书中教授的概念是永恒的,代码列表也应该是。为确保使用最新的库调用和语法,我积极管理一个位于 mng.bz/Yx5A 的 GitHub 仓库。请随时加入那里的社区,报告错误或发送拉取请求。
提示 TensorFlow 正处于快速发展阶段,因此更多功能将不断可用。
附录 安装说明
注意:除非另有说明,否则本书假定您将使用 Python 3,例如在第七章中,相关的依赖项 BregmanToolkit 需要 Python 2.7。同样,在第十九章中,VGG16.py 库需要 Python 2.7。Python 3 的代码列表遵循 TensorFlow v1.15,第七章和第十九章的示例使用 TensorFlow 1.14.0,因为它与 Python 2.7 兼容。GitHub 上的配套源代码将始终与最新版本保持一致(mng.bz/GdKO)。此外,目前正在进行将本书中的示例迁移到 TensorFlow 2.x 的重大努力,这项工作在 GitHub 仓库的 tensorflow2 分支中可见。您可以在那里找到更新的列表,请经常查看。
您可以通过几种方式安装 TensorFlow。本附录介绍了一种适用于所有平台(包括 Windows)的安装方法。如果您熟悉基于 UNIX 的系统(如 Linux 和 macOS),请随意使用官方文档中提供的安装方法之一,网址为mng.bz/zrAQ,或者如果您正在尝试 TensorFlow2 代码分支,请访问www.tensorflow.org/install。正如本书前言中 Google 应用 AI 和 TensorFlow 负责人 Scott Penberthy 所说,AI 和 ML 领域发展如此迅速,以至于在撰写本书的这一版时,TensorFlow 已经发布了几个版本,包括 2.x 系列的一些版本和 1.x 系列的一些版本。所有章节中模型构建的技术将独立于 TensorFlow API 的任何变化或任何单个模型的改进而持续存在。
我还在本附录中记录了所需的数据库集,以及运行本书中代码示例所需的库。我已经为您收集了数据库集,请注意放置输入数据的位置,代码示例将处理其余部分。
最后,为了让您了解 TensorFlow 2 和 TensorFlow 1 之间的一些细微差异,我将向您介绍一些更改,以便使用 TensorFlow 2 使客户呼叫中心预测示例正常工作。
不再拖延,让我们通过使用 Docker 容器来安装 TensorFlow。
A.1 使用 Docker 安装本书的代码
Docker是一种打包软件依赖项的系统,以确保每个人的安装环境相同。这种标准化有助于限制计算机之间的不一致性。
提示:您可以使用除了使用 Docker 容器之外的其他多种方式安装 TensorFlow。访问官方文档以获取有关安装 TensorFlow 的更多详细信息:www.tensorflow.org/install。同时,查看本书的官方 Dockerfile,它描述了您需要运行本书所需的软件、库和数据(mng.bz/0ZA6)。
A.1.1 在 Windows 中安装 Docker
Docker 只能在启用虚拟化的 64 位 Windows(7 或更高版本)上运行。幸运的是,大多数消费级笔记本电脑和台式机都满足这一要求。要检查你的电脑是否支持 Docker,请打开控制面板,点击“系统和安全”,然后点击“系统”。你将看到有关你的 Windows 机器的详细信息,包括处理器和系统类型。如果系统是 64 位,你几乎就可以开始了。
下一步是检查你的处理器是否支持虚拟化。在 Windows 8 或更高版本中,打开任务管理器(按 Ctrl-Shift-Esc),然后点击性能选项卡。如果虚拟化显示为启用(图 A.1),则一切准备就绪。对于 Windows 7,你应该使用 Microsoft 硬件辅助虚拟化检测工具(mng.bz/cBlu)。
A.1.2 在 Linux 中安装 Docker
现在你已经知道你的电脑是否支持 Docker,请安装 Docker Toolbox,位于mng.bz/K580。运行下载的安装可执行文件,并在对话框中点击“下一步”以接受所有默认设置。当工具箱安装完成后,运行 Docker Quickstart Terminal。
A.1.3 在 macOS 中安装 Docker
Docker 在 macOS 10.8 Mountain Lion 或更高版本上运行。从mng.bz/K580安装 Docker Toolbox。安装后,从应用程序文件夹或启动盘打开 Docker Quickstart Terminal。
A.1.4 使用 Docker
我创建了一个 Dockerfile,它构建了一个包含 Python 3.7 和 2.7 的镜像,使用 Python 的pip安装程序安装 Jupyter 和所需的库,然后创建必要的依赖库和文件夹结构以运行书中的代码示例。如果你想从头开始构建,可以使用build_environment.sh和run_environment.sh脚本分别构建和运行 Docker 镜像。Docker 构建还包括运行笔记本和训练模型所需的所有必要第三方库和输入数据。

图 A.1 确保你的 64 位电脑已启用虚拟化。
警告:请注意——当构建时,容器大约有 40 GB,因为机器学习是数据密集型和计算密集型的。请准备好你的笔记本电脑和/或构建 Docker 容器所需的时间。
或者,你可以运行以下命令来执行我为你创建并推送到 DockerHub 的镜像:
docker pull chrismattmann/mltf2
./run_environment.sh
将 DockerHub 想象成一个预构建环境镜像的家园。你可以在hub.docker.com上探索社区发布的各种容器。该环境包含一个 Jupyter Notebooks 中心,你可以在浏览器中输入http://127.0.0.1:8888来访问。请记住,根据特定章节的示例,选择正确的内核(Python3 或 Python2)。
A.2 获取数据和存储模型
在运行笔记本时,你会生成大量数据,尤其是在涉及构建模型的机器学习过程步骤中。但为了训练和构建这些模型,你也需要数据。我已经创建了一个 Dropbox 文件夹,你可以从中下载用于训练模型的数据。通过mng.bz/9A41访问该文件夹。
以下指针告诉你在哪些章节需要哪些数据以及放置的位置。除非另有说明,否则数据应放置在 data/文件夹中。请注意,当你运行笔记本时,笔记本将生成 TensorFlow 模型,并将它们和检查点文件写入 models/文件夹。GitHub 仓库提供了一个download-data.sh脚本,用于自动下载每章的数据并将数据放置在笔记本期望的文件夹中。此外,如果你使用 Docker 构建,容器将自动运行脚本并为你下载数据。
-
第四章
-
- data/311.csv
-
第六章
-
-
data/word2vec-nlp-tutorial/labeledTrainData.tsv
-
data/word2vec-nlp-tutorial/testData.tsv
-
data/aclImdb/test/neg
-
data/aclImdb/test/pos
-
-
第七章
-
-
data/audio_dataset
-
data/TalkingMachinesPodcast.wav
-
-
第八章
-
- data/User Identification From Walking Activity
-
第十章
-
- data/mobypos.txt
-
第十二章
-
-
data/cifar-10-batches-py
-
data/MNIST_data(如果你尝试 MNIST 附加示例)
-
-
第十四章
-
- data/cifar-10-batches-py
-
第十五章
-
-
data/cifar-10-batches-py
-
data/vgg_face_dataset—VGG -Face 的元数据,包括名人姓名
-
data/vgg-face—实际的 VGG -Face 数据
-
data/vgg_face_full_urls.csv—VGG -Face URL 的元数据信息
-
data/vgg_face_full.csv—所有 VGG -Face 数据的元数据信息
-
data/vgg-models/checkpoints-1e3x4-2e4-09202019—运行 VGG -Face 估算器附加示例
-
-
第十六章
-
- data/international-airline-passengers.csv
-
第十七章
-
-
data/LibriSpeech
-
libs/basic_units
-
libs/RNN-Tutorial
-
-
第十八章
-
- data/seq2seq
-
第十九章
-
-
libs/vgg16/laska.png
-
data/cloth_folding_rgb_vids
-
A.3 必要的库
尽管这本书的名字中有 TensorFlow,但这本书同样涉及广义机器学习和其理论,以及处理机器学习时非常有用的框架套件。运行笔记本的要求在以下列表中概述;它们也可以通过手动安装或通过 Docker 中的 requirements.txt 和 requirements-py2.txt 文件自动安装。此外,GitHub 仓库中有一个download-libs.sh脚本,你可以手动运行或让 Docker 为你运行。此脚本抓取笔记本所需的专用库,这些库不能通过pip安装。你应该使用你喜欢的 Python 版本安装其余的库。本书中的示例已在 Python 2.7 和 Python 3.7 中验证过。我没有时间测试所有这些,但我很高兴收到我遗漏的 pull 请求和代码贡献。
-
TensorFlow(本书适用于 1.13.1、1.14.0、1.15.0,并在书的存储库的 tensorflow2 分支上进行 2.2 及以后的版本的开发)
-
Jupyter
-
Pandas(用于数据框和易于表格数据处理)
-
NumPy 和 SciPy
-
Matplotlib
-
NLTK(用于任何文本或自然语言处理,如第六章中的情感分析)
-
TQDM(用于进度条)
-
SK-learn(用于各种辅助函数)
-
BregmanToolkit(用于第七章中的音频示例)
-
Tika
-
Ystockquote
-
Requests
-
OpenCV
-
Horovod(使用 0.18.2 或 0.18.1 与 Maverick2 VGG -Face 模型)
-
VGG16:vgg16.py、vgg16_weights.npz、imagenet_classes.py 和 laska.png(仅与 Python 2.7 兼容;将软件放在 libs/vgg16 中)
-
PyDub(在第十七章与 LSTMs 一起使用)
-
基本单位(第十七章;放置在 libs/basic_units/文件夹中)
-
RNN-Tutorial(在第十七章中使用,以帮助实现和训练深度语音模型)
A.4 将呼叫中心示例转换为 TensorFlow2
TensorFlow v2(TFv2)引入了许多破坏性变更。其中一些变更影响了工作流程;其他则需要采用新的范式。例如,急切执行需要从声明式编程转换为命令式编程。您不再使用 TensorFlow 的Placeholder;相反,您依赖于不同的库来完成在 v2 中已弃用的任务。文本和 GitHub 存储库页面上的示例、练习和列表正在积极地从 TensorFlow 转换为 TensorFlow2,在书的存储库的tensorflow2分支中(mng.bz/Qmq1)。我正在使用以下方法:
-
我尽可能使用官方的 TFv1 到 TFv2 迁移指南。
-
当迁移指南不足以解决问题时,我会尝试复制文本和存储库主分支中获得的成果。
如果您对如何将更复杂的项目从 v1 迁移到 v2 感兴趣,我鼓励您查看链接在www.tensorflow.org/guide/migrate的迁移指南。您还可以查看官方升级脚本是否适用于您的情况。升级脚本位于www .tensorflow.org/guide/upgrade。请注意,我在这存储库中不尝试使用自动升级脚本,原因如下:
-
转换脚本尽可能地自动化,但某些语法和风格上的更改无法由脚本执行。
-
完全检查从 TF v1 到 TF v2 的变化是有价值的。经历这个过程本身就是一种学习经历,即使是我也(有很多)东西要学习!
最后,有一行代码允许在使用新的 TensorFlow v2 库的同时,与 TensorFlow 1 完全向后兼容。您将此行代码放置在代码顶部,以便它表现得像 TFv1。根据 TensorFlow v1 到 v2 迁移指南,在书中的列表开头插入以下内容,以便在 TFv2 中运行未经修改的 TFv1 代码:
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
就这些!简单,对吧?你也会移除列表中现有的import tensorflow声明,并用前面提到的声明替换它,当然。在 A.4.1 节中,我快速回顾了第四章中的呼叫中心预测示例,以向你展示从 TFv1 到 TFv2 转换的核心。
A.4.1 使用 TF2 的呼叫中心示例
第四章通过使用纽约市 311 服务提供的真实呼叫中心数据,应用机器学习的回归概念,创建一个 TensorFlow 图来学习第 1-52 周的电话量值,并尝试准确预测它们。因为数据是连续的而不是离散的,在机器学习中,这个问题被称为回归而不是分类。图 A.2 显示了 311 呼叫中心数据。

图 A.2 y 轴上电话频率计数与 x 轴上的一年中的周数(0-51)的对比图。活动在 2014 年 5 月时间段内增加,并在 2014 年 8 月底减少。
在原始 TFv1 代码部分,我在本章中讨论了如何读取纽约市 311 的 CSV 数据并将其解析为 Python 字典。然后我向你展示了如何将这个字典转换为代表输入周和电话量数据的X_train和Y_train变量。数据通过 52 周内的最高电话量进行归一化,以实现Y_train的0到1的 y 轴刻度。为了构建 TensorFlow 模型图,我使用非线性高斯模型(钟形曲线形状)来表示量。你构建了一个训练 1,000 个 epoch 的 TensorFlow 图来学习输入高斯模型中的变量 mu 和 sigma。在模型学习参数后,我向你展示了如何将它们输入到 NumPy 版本的模型中,打印学习到的曲线,并可视化和绘制误差和准确度。原始列表显示在列表 A.1 中供你参考。
列表 A.1 设置和训练 TensorFlow 模型以拟合高斯曲线
learning_rate = 1.5 ❶
training_epochs = 5000 ❷
X = tf.placeholder(tf.float32) ❸
Y = tf.placeholder(tf.float32) ❸
def model(X, mu, sig):
return tf.exp(tf.div(tf.negative(tf.pow(tf.subtract(X, mu), 2.)),
➥ tf.multiply(2., tf.pow(sig, 2.))))
mu = tf.Variable(1., name="mu") ❹
sig = tf.Variable(1., name="sig") ❹
y_model = model(X, mu, sig) ❺
cost = tf.square(Y-y_model) ❻
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) ❻
sess = tf.Session() ❼
init = tf.global_variables_initializer() ❼
sess.run(init) ❼
for epoch in range(training_epochs):
for(x, y) in zip(X_train, nY_train):
sess.run(train_op, feed_dict={X:x, Y:y}) ❽
mu_val = sess.run(mu)
sig_val = sess.run(sig)
print(mu_val) ❾
print(sig_val) ❾
sess.close() ❿
❶ 设置每个 epoch 的学习率
❷ 训练 5,000 个 epoch
❸ 设置输入(X)和预测的值(Y)
❹ 定义模型学习的参数 mu 和 sig
❺ 基于 TensorFlow 图创建模型
❻ 定义成本函数为 L2 范数并设置训练操作
❼ 初始化 TensorFlow 会话
❽ 执行训练并学习 mu 和 sig 的值
❾ 打印 mu 和 sig 的学习值
❿ 关闭会话
如果我告诉你,第四章的 TensorFlow 2 版本重用了所有相同的数据准备、评估和绘图代码,只有对列表 A.1(模型)进行了一点点修改,我希望你会说这是可信的,因为正如我向你展示的那样,机器学习的大部分工作都涉及 TensorFlow 及其辅助库——如 Pandas 或 Tika 这样的数据准备辅助库,或使用 Jupyter 和 Matplotlib 进行数据评估和探索性分析。所有这些代码都保持不变,并且与 TensorFlow 独立。
TensorFlow 代码在整本书中遵循以下模式:
-
设置模型超参数。有时,我会向你展示如何使用书中使用的数据推导超参数。在其他时候,我会指出其他人已经花费了大量时间和资源来推导它们,所以你应该重用它们。
-
在你(或其他人)收集并清洗准备好的数据上训练模型。
-
评估学习模型以及你的表现和它的表现,
第 1 步和第 2 步是对呼叫中心预测模型的轻微修改。首先,TensorFlow2 原生地集成了 Keras 机器学习库。这种集成超出了本书的范围,但一个好处是使用了 Keras 的优化器。你不再使用tf.train中的优化器,如tf.train.GradientDescentOptimizer,而是使用tf.keras.optimizers,例如tf.keras.optimizers.SGD。
另一个变化是,在 TFv1 中,不是在每个 epoch 中通过for循环运行每个星期,并通过placeholders注入该周的个别呼叫量值,在 TFv2 中,我们希望去掉这些,所以你使用声明式编程。换句话说,train函数应该逐步或整体地获取数据并在其上训练,而不需要在feed_dict参数中进行每步注入。不需要使用数据注入。你可以使用tf.constant,用每个 epoch 的 52 周数据初始化常数,并在过程中消除for循环。由于 TFv2 鼓励无注入的声明式编程,你可以使用 Python 的 lambda 内联函数来定义你的模型,并使用cost和loss作为内联函数来声明式地构建模型。
如列表 A.2 所示,这就是所有的变化。
列表 A.2 TensorFlow 2 版本的呼叫中心预测模型
learning_rate = 1.5 ❶
training_epochs = 5000 ❶
momentum=0.979 ❷
X = tf.constant(X_train, dtype=tf.float32) ❸
Y = tf.constant(nY_train, dtype=tf.float32) ❸
mu = tf.Variable(1., name="mu") ❹
sig = tf.Variable(1., name="sig") ❹
model = lambda _X, _sig, _mu: ❺
➥ tf.exp(tf.div(tf.negative(tf.pow(tf.subtract(tf.cast(_X,tf.float32), _mu), ❺
➥ 2.)), tf.multiply(2., tf.pow(_sig, 2.)))) ❺
y_model = lambda: model(X, mu, sig) ❺
cost = lambda: tf.square(Y - y_model()) ❻
train_op = tf.keras.optimizers.SGD(learning_rate, momentum=momentum) ❼
for epoch in tqdm(range(training_epochs)):
train_op.minimize(cost, mu, sig) ❽
mu_val = mu.value() ❾
sig_val = sig.value() ❾
❾
print(mu_val.numpy()) ❾
print(sig_val.numpy()) ❾
❶ 定义超参数(与 TFv1 列表中的不变)
❷ 为 Keras 优化器定义的新超参数加速了正确方向的梯度。
❸ 占位符已消失,被常数整个布的周/呼叫量数据所取代
❹ 学习到的参数
❺ 使用 lambda 函数定义声明式高斯模型,并使用真实的 X 值而不是占位符
❻ 使用真实的 Y 值定义成本
❼ 使用 Keras 优化器
❽ 以声明式方式训练并移除了列表 A.1 中存在的带有注入的 for 循环
❾ 获取学习到的参数并打印结果
你可以检查 TensorFlow v2 版本的呼叫中心列表mng.bz/WqR1。笔记本的其余部分与 TensorFlow v1 版本相同,包括处理数据读取和清洗、准备和探索性分析的步骤。
请回到代码仓库的 tensorflow2 分支,并跟随我以及其他人(可能包括你!)将笔记本转换为 TensorFlow v2。我将使用迁移指南和附录中定义的最佳实践。重要的是要指出,无论这本书的发布还是未来发布的 TensorFlow 的版本,大部分代码和技术都将保持不变。坚持使用我教给你的数据准备、清洗、超参数选择和模型构建技术。无论 TensorFlow 的版本如何,它们都将伴随你一生!


浙公网安备 33010602011771号