结构化数据的深度学习-全-
结构化数据的深度学习(全)
原文:Deep Learning with Structured Data
译者:飞龙
前言
我认为,当人们 50 年后回顾并评估本世纪的头二十年时,深度学习将名列技术创新之首。深度学习的理论基础在 20 世纪 50 年代就已经确立,但直到 2012 年,深度学习的潜力才对非专业人士显现。现在,几乎十年过去了,深度学习已经渗透到我们的生活中,从能够无缝将我们的语音转换为文本的智能音箱,到能够在不断扩大的游戏范围内击败任何人类的系统。本书考察了深度学习世界中一个被忽视的角落:将深度学习应用于结构化、表格数据(即按行和列组织的数据)。
如果传统观点是避免在结构化数据中使用深度学习,而深度学习的标志性应用(如图像识别)处理的是非结构化数据,那么为什么你应该阅读一本关于结构化数据深度学习的书呢?首先,正如我在第一章和第二章中论证的,对于使用深度学习解决结构化数据问题(如深度学习过于复杂或结构化数据集太小)的一些反对意见在今天根本站不住脚。当我们评估哪种机器学习方法适用于结构化数据问题时,我们需要保持开放的心态,并将深度学习视为一种潜在的解决方案。其次,尽管非表格数据支撑着深度学习的许多热门应用领域(如图像识别、语音到文本和机器翻译),但作为消费者、雇员和公民,我们的生活仍然主要由表格数据定义。每一次银行交易、每一次税款支付、每一次保险索赔以及我们日常生活中的数百个其他方面都通过结构化、表格数据流动。无论你是深度学习的初学者还是经验丰富的从业者,当你处理涉及结构化数据的问题时,你都应该在自己的工具箱中拥有深度学习。
通过阅读这本书,你将学习到将深度学习应用于各种结构化数据问题的所需知识。你将完成深度学习在真实世界数据集上的全面应用,从准备数据到训练深度学习模型,再到部署训练好的模型。本书附带代码示例是用 Python 编写的,Python 是机器学习的通用语言,并利用了 Keras/TensorFlow 框架,这是工业界最常用的深度学习平台。
致谢
在过去一年半的时间里,我写这本书期间,有很多要感谢的人给予他们的支持和帮助。首先,我要感谢 Manning Publications 的团队,特别是我的编辑 Christina Taylor,他们的精湛指导。我要感谢我在 IBM 的前任主管——特别是 Jessica Rockwood、Michael Kwok 和 Al Martin——给予我写这本书的动力。我要感谢我在 Intact 的当前团队的支持——特别是 Simon Marchessault-Groleau、Dany Simard 和 Nicolas Beaupré。我的朋友们一直给予我持续的鼓励。我特别感谢 Dr. Laurence Mussio 和 Flavia Mussio,他们都是我写作的无私和热情的支持者。Jamie Roberts、Luc Chamberland、Alan Hall、Peter Moroney、Fred Gandolfi 和 Alina Zhang 都给予了鼓励。最后,我要感谢我的家人——Steve 和 Carol、John 和 Debby、Nina——他们的爱。(“感谢上帝,我们是一个文学家庭。”)
致所有审稿人:Aditya Kaushik、Atul Saurav、Gary Bake、Gregory Matuszek、Guy Langston、Hao Liu、Ike Okonkwo、Irfan Ullah、Ishan Khurana、Jared Wadsworth、Jason Rendel、Jeff Hajewski、Jesús Manuel López Becerra、Joe Justesen、Juan Rufes、Julien Pohie、Kostas Passadis、Kunal Ghosh、Malgorzata Rodacka、Matthias Busch、Michael Jensen、Monica Guimaraes、Nicole Koenigstein、Rajkumar Palani、Raushan Jha、Sayak Paul、Sean T Booker、Stefano Ongarello、Tony Holdroyd、Vlad Navitski,你们的建议帮助使这本书变得更好。
关于这本书
这本书将带你经历将深度学习应用于表格化、结构化数据集的完整旅程。通过解决一个扩展的、现实世界的例子,你将学习如何清理一个混乱的数据集,并使用流行的 Keras 框架来训练深度学习模型。然后,你将学习如何通过网页或 Facebook Messenger 中的聊天机器人使你的训练好的深度学习模型对全世界可用。最后,你将学习如何扩展和改进你的深度学习模型,以及如何将本书中展示的方法应用于涉及结构化数据的其他问题。
适合阅读这本书的人
为了最大限度地利用这本书,你应该熟悉在 Jupyter Notebooks 环境中进行 Python 编码。你还应该熟悉一些非深度学习机器学习方法,如逻辑回归和支持向量机,并熟悉机器学习的标准词汇。最后,如果你经常处理以行和列组织的数据,你会发现将这本书中的概念应用于你的工作将更容易。
这本书的组织结构:路线图
这本书由九个章节和一个附录组成:
-
第一章快速回顾了深度学习的高级概念,并总结了为什么(以及为什么不)你想将深度学习应用于结构化数据。它还解释了我所说的结构化数据是什么。
-
第二章解释了可用于本书代码示例的开发环境。它还介绍了用于表格、结构化数据的 Python 库(Pandas),并描述了本书其余部分使用的重大示例:预测轻轨交通系统的延误。这个示例是电车延误预测问题。最后,第二章通过一个简单的深度学习模型训练示例快速预览了后续章节的细节。
-
第三章探讨了主要示例的数据集,并描述了如何处理数据集中的一组问题。它还考察了训练深度学习模型所需数据量的多少问题。
-
第四章涵盖了如何解决数据集中的额外问题,以及处理在所有清理工作完成后仍留在数据中的不良值。它还展示了如何准备非数值数据以训练深度学习模型。第四章以端到端代码示例的总结结束。
-
第五章描述了为电车延误预测问题准备和构建深度学习模型的过程。它解释了数据泄露(使用在您想用模型进行预测时不可用的数据进行模型训练)的问题以及如何避免它。然后章节详细介绍了构成深度学习模型的代码细节,并展示了检查模型结构的选项。
-
第六章解释了从选择输入数据集的子集以训练和测试模型,到进行第一次训练运行,再到通过一系列实验迭代以提高训练模型性能的端到端模型训练过程。
-
第七章通过进行三个更深入的实验,扩展了第六章中引入的模型训练技术。第一个实验证明,第四章中的一项清理步骤(删除包含无效值的记录)提高了模型的性能。第二个实验展示了将学习向量(嵌入)与分类列关联的性能优势。最后,第三个实验比较了深度学习模型与流行非深度学习方法 XGBoost 的性能。
-
第八章提供了如何使您训练的深度学习模型对外界有用的详细信息。首先,它描述了如何进行一个简单的模型网络部署。然后,它描述了如何使用 Rasa 开源聊天机器人框架在 Facebook Messenger 中部署训练好的模型。
-
第九章首先总结了本书所涵盖的内容。然后它描述了可能提高模型性能的额外数据源,包括位置和天气数据。接下来,它描述了如何将本书附带的代码适应于解决表格结构数据中的全新问题。本章最后列出了更多关于结构化数据深度学习的书籍、课程和在线资源。
-
附录描述了如何使用免费的 Colab 环境运行本书附带的代码示例。
我建议您按顺序阅读这本书,因为每一章都是基于前一章的内容构建的。如果您执行了本书附带的代码示例——特别是电车延误预测问题的代码——您将能从本书中获得最大收益。最后,我强烈建议您练习第六章和第七章中描述的实验,并探索第九章中描述的额外增强功能。
关于代码
这本书附带大量的代码示例。除了第 3-8 章中关于电车延误预测问题的扩展代码示例外,第二章(用于演示 Pandas 库以及 Pandas 与 SQL 之间的关系)和第五章(用于演示 Keras 顺序和功能 API)还有额外的独立代码示例。
第二章描述了您运行代码示例的选项,附录中进一步详细说明了其中一个选项,即 Google 的 Colab。无论您选择哪种环境,您都需要安装 Python(至少版本 3.7)以及以下关键库:
-
Pandas
-
Scikit-learn
-
Keras/TensorFlow 2.x
当您运行代码片段时,您可能需要使用 pip 安装额外的库。
主电车延误预测示例的部署部分有一些额外的要求:
-
Flask 库用于 Web 部署
-
Rasa 聊天机器人框架和 ngrok 用于 Facebook Messenger 部署
源代码以fixed-width font like this的格式进行格式化,以将其与普通文本区分开来。有时代码也会以bold显示,以突出显示与章节中先前步骤相比已更改的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新整理了缩进,以适应书籍中的可用页面空间。在极少数情况下,即使这样也不够,列表中包括行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中删除。代码注释伴随着许多列表,突出显示重要概念。
您可以在 GitHub 仓库mng.bz/v95x中找到这本书的所有代码示例。
liveBook 讨论论坛
购买《结构化数据深度学习》包括免费访问由曼宁出版社运营的私人网络论坛,您可以在论坛上对书籍发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问livebook.manning.com/#!/book/deep-learning-with-structured-data/discussion。您还可以在livebook.manning.com/#!/discussion了解更多关于曼宁论坛和行为准则的信息。
曼宁对读者的承诺是提供一个平台,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未支付报酬)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要书籍仍在印刷中,论坛和先前讨论的存档将可通过出版社的网站访问。
关于作者
马克·瑞安是加拿大多伦多 Intact 保险公司的数据科学经理。马克热衷于分享机器学习的益处,包括提供机器学习训练营,让参与者亲身体验机器学习的世界。除了深度学习及其在结构化、表格数据中解锁额外价值的能力外,他的兴趣还包括聊天机器人和自动驾驶汽车的可能性。他拥有滑铁卢大学的数学学士学位和多伦多大学的计算机科学硕士学位。
关于封面插图
《结构化数据深度学习》封面上的插图标题为“Homme de Navarre”,或“来自纳瓦拉的男子”,这是西班牙北部一个多元化的地区。这幅插图取自雅克·格拉塞·德·圣索沃尔(1757-1810)的作品集,名为《不同国家的服饰》,于 1797 年在法国出版。每一幅插图都是手工精细绘制和着色的。格拉塞·德·圣索沃尔收藏中的丰富多样性生动地提醒我们,200 年前世界的城镇和地区在文化上有多么不同。他们彼此孤立,说着不同的方言和语言。在街道或乡村,仅凭他们的服饰就能轻易识别他们居住的地方以及他们的职业或社会地位。
自那时以来,我们的着装方式已经改变,而当时区域间的多样性已经逐渐消失。现在很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。也许我们用文化多样性换取了更加丰富多彩的个人生活——当然,是为了更加多样化和快节奏的技术生活。
在难以区分一本计算机书与另一本计算机书的年代,曼宁通过基于两百年前丰富多样的地区生活所设计的书封面,庆祝了计算机行业的创新精神和主动性,这些画面由格拉塞特·德·圣索沃尔重新赋予生命。
1 为什么使用结构化数据的深度学习?
本章节涵盖
-
深度学习的高级概述
-
深度学习的利弊
-
深度学习软件堆栈的介绍
-
结构化数据与非结构化数据
-
对结构化数据深度学习的反对意见
-
结构化数据深度学习的优势
-
介绍本书所附代码
自 2012 年以来,我们见证了只能称之为人工智能的复兴。一个在 20 世纪 80 年代末迷失方向的学科再次变得重要。发生了什么?
2012 年 10 月,一支与多伦多大学深度学习主要倡导者杰弗里·辛顿(Geoffrey Hinton)合作的学生团队在 ImageNet 计算机视觉竞赛中宣布了一个结果,该结果在识别物体时的错误率接近竞争对手的一半。这一结果利用了深度学习,并引发了人们对这一主题的极大兴趣。从那时起,我们在许多领域看到了具有世界级结果的深度学习应用,包括图像处理、音频转文本和机器翻译。在过去的几年里,深度学习的工具和基础设施已经达到了成熟和易于访问的水平,使得非专业人士能够利用深度学习的优势。本书展示了您如何使用深度学习来深入了解和预测结构化数据:以表格形式组织的数据,如关系数据库中的行和列。您将通过逐步通过一个完整的、端到端的深度学习示例来了解深度学习的能力,从摄取原始输入结构化数据到将深度学习模型提供给最终用户。通过将深度学习应用于具有现实世界结构化数据集的问题,您将看到深度学习在结构化数据中的挑战和机遇。
1.1 深度学习概述
在回顾深度学习的高级概念之前,让我们介绍一个简单的例子,我们可以用它来探索这些概念:信用卡欺诈检测。第二章介绍了现实世界的数据集和大量的代码示例,该示例准备了这个数据集并使用它来训练一个深度学习模型。目前,这个基本的欺诈检测示例足以回顾一些深度学习的概念。
您为什么要利用深度学习进行欺诈检测?有几个原因:
-
欺诈者可以找到绕过传统基于规则欺诈检测方法的方法(
mng.bz/emQw)。 -
作为工业级管道一部分的深度学习方法——在该管道中,模型的性能经常被评估,如果性能低于给定的阈值,模型将自动重新训练——可以适应欺诈模式的变化。
-
深度学习方法有可能提供对新的交易进行近乎实时的评估。
总结来说,深度学习对于欺诈检测来说是值得考虑的,因为它可以成为灵活、快速解决方案的核心。请注意,除了这些优点之外,使用深度学习作为欺诈检测问题的解决方案也有一个缺点:与其他方法相比,深度学习更难以解释。其他机器学习方法允许你确定哪些输入特征对结果影响最大,但这个关系可能很难或无法用深度学习建立。
假设一家信用卡公司将其客户交易记录作为表格中的记录。该表格中的每条记录都包含有关交易的信息,包括一个唯一标识客户的 ID,以及有关交易的详细信息,包括交易日期和时间、供应商 ID、交易地点以及交易的货币和金额。除了这些信息,每次报告交易时都会添加到表格中,每条记录都有一个字段来指示交易是否被报告为欺诈。
信用卡公司计划使用该表格中的历史数据训练一个深度学习模型,并使用这个训练好的模型来预测新到达的交易是否为欺诈。目标是尽快识别潜在的欺诈(并采取纠正措施),而不是等待几天让客户或供应商报告特定交易为欺诈。
让我们检查客户交易表。图 1.1 包含了这个表格中一些记录的片段。

图 1.1 信用卡欺诈示例数据集
列客户 ID、交易日期、交易时间、供应商 ID、城市、国家、货币和金额包含关于上一季度单个信用卡交易的详细信息。欺诈列是特殊的,因为它包含标签:当我们用训练数据训练深度学习模型时,我们希望模型预测的值。假设欺诈列的默认值为0(表示“非欺诈”),并且当我们的客户或供应商报告欺诈交易时,该交易在表格中的欺诈列的值被设置为1。
当新的交易到来时,我们希望能够预测它们是否是欺诈性的,这样我们就可以迅速采取纠正措施。通过在历史数据集上训练深度学习模型,我们将定义一个可以预测新的信用卡交易是否欺诈的函数。在这个监督学习(mng.bz/pzBE)的例子中,模型是通过包含带有标签的示例的数据集来训练的。用于训练模型的数据库包括训练模型将预测的值(在这种情况下,是否为欺诈交易)。相比之下,在无监督学习中,训练数据集不包含标签。
现在我们已经介绍了信用卡欺诈的例子,让我们用它来简要地浏览一下深度学习的一些概念。对于这些概念的更深入描述,请参阅弗朗索瓦·肖莱特的《Python 深度学习,第 2 版》(mng.bz/OvM2),其中对这些概念有出色的描述:
-
深度学习是一种机器学习方法,通过在每个层设置权重和偏置来训练多层人工神经网络,通过使用基于梯度的优化和反向传播来优化损失函数(实际结果[欺诈列中的值]与预测结果之间的差异)。
-
深度学习模型中的神经网络有一系列层,从输入层开始,接着是几个隐藏层,最后是输出层。
-
在这些每一层中,前一层(或者,在第一层的情况下,训练数据,对于我们的例子来说,是来自客户 ID、日期、时间、供应商 ID、城市、国家、货币和金额的数据集列)的输出会经过一系列操作(通过权重矩阵相乘、添加偏置[偏差]以及应用非线性激活函数)以产生下一层的输入。在图 1.2 中,每个圆圈(节点)都有自己的权重集。输入会被这些权重相乘,加上偏置,然后对结果应用激活函数以产生下一层接收的输出。
![CH01_F02_Ryan]()
图 1.2 多层神经网络
模型的最终输出层根据输入生成预测。在我们的预测信用卡欺诈的例子中,输出指示模型是否预测了欺诈(输出为
1)或不是欺诈(输出为0)对于特定的交易。 -
深度学习通过迭代更新网络中的权重来最小化损失函数(该函数定义了模型预测与训练数据集中实际结果值之间的总差异)。随着权重的调整,模型在总体上的预测将更接近输入表欺诈列中的实际结果值。每次训练迭代时,根据损失函数的梯度调整权重。
-
你可以将损失函数的梯度视为大致等同于山丘的斜率。如果你朝着与山丘斜率相反的方向迈出小而渐进的步伐,你最终会到达山丘的底部。通过在网络的每次迭代中对权重进行与梯度相反方向的小幅调整,你可以逐步减少损失函数。一个称为反向传播的过程用于获取损失函数的梯度,然后可以将它应用于更新神经网络中每个节点的权重,这样通过重复应用,损失函数被最小化,模型的预测准确性被最大化。训练过程总结在图 1.3 中。
![CH01_F03_Ryan]()
图 1.3 当在网络上迭代更新权重以训练模型时,使用训练数据。
-
当训练完成(模型中的权重已经通过反向传播提供的梯度反复更新,以达到使用训练数据所期望的性能)时,得到的模型可以用于对新数据进行预测,这些数据模型之前从未见过。
该过程的输出是一个经过训练的深度学习模型,它包含了最终的权重,并可用于从新的输入数据中预测输出,如图 1.4 所示。

图 1.4 训练好的模型对新数据进行预测。
本书不涵盖深度学习的数学基础。在《Python 深度学习》第 2 版中关于深度学习数学构建块的章节提供了对深度学习背后数学的清晰、简洁描述。你还可以在第九章中找到对深度学习背后数学的概述,该章节提到了 deeplearning.ai 课程。
1.2 深度学习的利弊
深度学习的核心观点既简单又深刻:一个训练好的深度学习模型可以包含一个极其复杂的函数,该函数可以准确描述模型训练数据中隐含的模式。给定足够的标记数据来训练(例如,足够大的信用卡交易数据集,其中有一列指示每笔交易是否为欺诈),深度学习可以定义一个模型,该模型可以预测模型在训练过程中从未见过的新的数据标签值。深度学习以训练模型的形式定义的函数可以包括数百万个参数,远远超出任何人类可以手动创建的数量。
在某些用例中,例如图像识别,深度学习模型有在比非深度学习机器学习方法更接近原始输入数据的数据上进行训练的优势。那些方法可能需要广泛的 特征工程(输入数据的编码转换和输入表中的新列)以达到良好的性能。
深度学习的益处并非免费。深度学习有几个显著的缺点,你需要准备好应对。为了使深度学习有效,你需要
-
大量的标记数据 — 根据领域不同,你可能需要数百万个示例。
-
能够进行大量矩阵操作的硬件 — 正如你在第二章中将会看到的,现代笔记本电脑可能足以训练一个简单的深度学习模型。更大的模型将需要专门的硬件(GPU 和 TPU)来高效训练。
-
对模型不完美透明度的容忍度 — 当你将深度学习与经典的非深度学习机器学习进行比较时,可能更难解释为什么深度学习模型会做出这样的预测。特别是,如果一个模型是在某个特征集(客户 ID、交易日期、交易时间等)上训练的,那么确定哪些特征对模型预测结果的能力贡献最大可能很困难。
-
避免常见陷阱的重大工程 — 这些陷阱包括过拟合(模型对其训练数据准确,但不能推广到新数据)和梯度消失/爆炸(反向传播因为权重的修改在每个步骤变得过大或过小而爆炸或停止)。
-
操纵多个超参数的能力 — 数据科学家需要控制一组被称为超参数的旋钮,包括学习率(每次更新权重时采取的步长大小)、正则化(避免过拟合的各种策略)以及训练过程迭代输入数据集的次数以训练模型。调整这些旋钮以获得良好的结果就像尝试驾驶直升机。作为一名直升机飞行员需要协调双手和双脚以保持机器稳定路径并避免坠毁,训练深度学习模型的数据科学家需要协调超参数以从模型中获得期望的结果并避免过拟合等陷阱。有关本书扩展示例中使用的超参数的详细信息,请参阅第五章。
-
对不完美准确度的容忍度 — 深度学习本质上不可能产生 100%准确的预测。如果需要绝对准确度,最好使用更确定性的方法。
这里有一些缓解这些缺点的措施:
-
大量标记数据 — 深度学习对大量标记数据的渴望可以通过迁移学习来缓解:重用训练用于在相关任务上执行一个任务的模型或模型子集。在大型、通用的标记图像数据集上训练的模型可以用来启动应用于特定领域(在该领域中标记图像数据稀缺)的模型。本书的扩展示例不使用迁移学习,但您可以查看 Paul Azunre 的《自然语言处理中的迁移学习》(
mng.bz/GdVV),了解迁移学习在深度学习用例(如自然语言处理和计算机视觉)中的关键作用。 -
能够进行大规模矩阵操作的硬件 — 现在,很容易获得具有足够硬件能力的环境(包括第二章中介绍的云环境),以较低的成本训练具有挑战性的模型。本书中扩展的深度学习示例在专门为深度学习设计的硬件的云环境中可以更快地运行,但你也可以在配置合理的现代笔记本电脑上运行它。
-
对模型不透明性的容忍度 — 一些供应商(包括亚马逊、谷歌和 IBM)现在提供解决方案,帮助使深度学习模型更加透明,并解释深度学习模型的行为。
-
避免常见陷阱的重大工程 — 算法改进不断进入常见的深度学习框架,以帮助您避免梯度爆炸等问题。
-
调整多个超参数的能力 — 自动化超参数优化方法有可能减少调整超参数的复杂性,并使训练深度学习模型的体验更像开车而不是驾驶直升机,因为有限的一组输入(方向盘、油门)会产生直接的结果(汽车改变方向,汽车改变速度)。
准确度不足仍然是一个挑战。不完美准确度的影响取决于您试图解决的问题。如果您正在预测客户是否会流失(将其业务带到竞争对手那里),85% 或 90% 的时间正确可能已经足够解决该问题。然而,如果您正在预测可能致命的医疗状况,那么深度学习的内在限制就更加难以克服。您可以容忍多少不准确将取决于您正在解决的问题。
1.3 深度学习堆栈概述
今天,有各种各样的深度学习框架可供选择。其中最受欢迎的两个是 TensorFlow (www.tensorflow.org),它在深度学习的工业应用中占据主导地位,以及 PyTorch (pytorch.org),它在研究社区中拥有众多支持者。
在这本书中,我们将使用 Keras (keras.io) 作为我们的深度学习库。Keras 最初是一个独立的项目,可以用作各种深度学习框架的前端。正如第五章所述,截至 TensorFlow 2.0,Keras 已集成到 TensorFlow 中。Keras 是 TensorFlow 推荐的高级 API。本书附带的代码已在 TensorFlow 2.0 上进行了验证,但您应该可以使用 TensorFlow 的后续版本,而不会遇到任何问题。
下面是对堆栈主要组件的简要介绍:
-
Python — 这种易于学习、灵活的解释性语言是目前机器学习最受欢迎的语言。Python 的普及增长与过去十年中机器学习的复兴紧密相关,现在它远远超过了其最接近的竞争对手 R,成为机器学习的通用语言。Python 拥有一个庞大的生态系统和一套庞大的库,不仅涵盖了您想用机器学习做的所有事情,还包括了开发的全套工具。此外,Python 拥有一个庞大的开发者社区,您几乎可以在线找到几乎任何 Python 问题或问题的答案。本书中的代码示例完全用 Python 编写,除了第三章中描述的 YAML 配置文件;第二章中的 SQL 示例;以及第八章中描述的部署,这些部署包括 Markdown、HTML 和 JavaScript 中的代码。
-
Pandas —这个 Python 库为你提供了在 Python 中方便地处理表格结构化数据的所有工具。你可以轻松地将结构化数据(无论是从 CSV 或 Excel 文件中还是直接从关系数据库中的表中)导入到 Pandas 数据框中,然后通过表操作(如删除和添加列、按列值过滤和连接表)来操作它。你可以将 Pandas 视为 Python 对 SQL 的回应。第二章包含了一些将数据加载到 Pandas 数据框中并使用 Pandas 执行常见 SQL 类型操作的示例。
-
scikit-learn —scikit-learn 是一个广泛的 Python 机器学习库。本书中的扩展示例大量使用了这个库,包括第三章和第四章中描述的数据转换工具以及第八章中描述的功能,以定义可训练的数据管道,这些管道既为训练深度学习模型做准备,也为从训练好的模型中获得预测做准备。
-
Keras —Keras 是一个简单的深度学习库,它提供了足够的灵活性和控制,同时抽象出了一些底层 TensorFlow API 的复杂性。Keras 有一个庞大而活跃的社区,包括初学者和经验丰富的机器学习从业者,并且很容易找到使用 Keras 进行深度学习应用的可靠示例。
1.4 结构化数据与非结构化数据
这本书的标题包含两个不常一起出现的术语:深度学习和结构化数据。结构化数据(在本书的上下文中)是指以行和列的形式组织的数据,这种数据存储在关系数据库中。深度学习是一种高级机器学习技术,它在处理不常存储在表中的数据(如图像、视频、音频和文本)的一系列问题上已经证明了其成功。
为什么要将深度学习应用于结构化数据?为什么将一个 40 年之久的数据库范式与前沿的深度学习相结合?对于涉及结构化数据的问题,难道没有更简单的方法来解决吗?难道没有比尝试用存储在表中的数据训练模型更好的深度学习应用吗?
为了回答这些有效的问题,我们首先将更详细地定义我们所说的结构化和非结构化数据;在 1.5 节中,我们将解决这些以及其他关于将深度学习应用于结构化表格数据的反对意见。
在这本书中,结构化数据是指已经被组织起来以存储在具有行和列的关系数据库中的数据。列可以包含数值(例如货币金额、温度、时间长度或其他以整数或浮点值表示的数量)或非数值(例如字符串、嵌入的结构化对象或非结构化对象)。
所有关系型数据库都支持 SQL(尽管有不同的方言)作为数据库的主要接口。常见的关系型数据库包括以下几种:
-
专有数据库 —Oracle、SQL Server、Db2、Teradata
-
开源数据库 —Postgres、MySQL、MariaDB
-
基于开源的专有数据库 —AWS Redshift(基于 Postgres)
关系型数据库可以包含表之间的关系,例如外键(其中一个表列的可允许值取决于另一个表中标识列的值)。可以通过连接表来创建新的表,这些新表包含参与连接的表的行和列的组合。关系型数据库还可以包含代码集,例如存储过程,这些存储过程可以调用以访问和操作数据库中的数据。为了这本书的目的,我们将重点关注表的行和类型化列的性质,而不是关系型数据库提供的额外表间交互和代码接口。
关系型数据库并不是结构化表格数据的唯一可能存储库。如图 1.5 所示,Excel 或 CSV 文件中的数据在本质上是以行和列结构化的,尽管与关系表不同,列的类型不是作为结构的一部分编码,而是从列内容中推断出来的。本书主要示例的数据集来自一组 Excel 文件。

图 1.5 表格结构化数据的示例
为了这本书的目的,我们不会探讨非结构化数据——那些没有组织成关系数据库中表格形式的数据。如图 1.6 所示,非结构化数据包括图像、视频和音频文件,以及文本和标记格式,如 XML、HTML 和 JSON。根据这个定义,非结构化数据并不一定完全没有结构。例如,JSON 中的键值对就是一种结构,但 JSON 在其原生状态下并不是以行和列的表格形式组织,因此,为了这本书的目的,它被视为非结构化数据。更复杂的是,结构化数据可以包含非结构化元素,例如表格中的自由文本列或引用 XML 文档或 BLOBs(二进制大对象)的列。

图 1.6 非结构化数据的示例
许多书籍涵盖了深度学习在非结构化数据(如图像和文本)中的应用。本书则采取不同的方向,专注于深度学习在表格结构化数据上的应用。第 1.5 节和第 1.6 节提供了一些对结构化数据关注的理由,首先讨论了一些你可能会对结构化数据关注持怀疑态度的原因,然后回顾了使用深度学习探索结构化数据问题的好处。
1.5 使用结构化数据进行深度学习的反对意见
许多备受赞誉的深度学习应用都涉及非结构化数据,如图像、音频和文本。一些深度学习专家质疑是否应该将深度学习应用于结构化数据,并坚持认为非深度学习的方法最适合结构化数据。
为了激励你探索结构化数据中的深度学习,让我们回顾一些反对意见:
-
结构化数据集太小,无法用于深度学习。 这个反对意见是否有效取决于领域。当然,有许多领域(包括本书中探讨的问题)中的标记结构化数据集包含数万个甚至数百万个示例,使它们足够大,可以用于训练深度学习模型。
-
保持简单。 深度学习既困难又复杂,为什么不使用更简单的解决方案,比如非深度学习机器学习或传统的商业智能应用?这个反对意见三年前比现在更有道理。在简单性和广泛应用方面,深度学习已经达到了一个转折点。多亏了深度学习的普及,利用它的工具现在使用起来要容易得多。正如你将在本书的扩展编码示例中看到的那样,深度学习现在对非专业人士来说也是可访问的。
-
手工制作的深度学习解决方案变得越来越不必要了。 为什么要费劲去创建一个端到端的深度学习解决方案,尤其是如果你不是全职数据科学家,如果手工解决方案将越来越多地被需要少量或不需要编码的解决方案所取代?例如,fast.ai 库(
docs.fast.ai)允许你用几行代码创建强大的深度学习模型,像 Watson Studio 这样的数据科学环境提供基于 GUI 的模型构建器(如图 1.7 所示),让你无需编写任何代码就能创建深度学习模型。 -
使用这些解决方案,为什么还要努力学习如何直接编写深度学习模型的代码?为了理解如何使用低代码或无代码解决方案,你仍然需要了解深度学习模型是如何构建的,而最快的学习方式就是编写代码来利用深度学习框架。如果你主要处理工作中的表格数据,那么能够将深度学习应用于这些数据是有意义的。通过为涉及你彻底理解的有序表格数据的特定问题编写深度学习解决方案,你将了解深度学习的概念、优势和局限性。有了这种理解,你将能够利用深度学习(无论是否手工编码)来解决更多的问题。本书中的扩展示例将带你通过将深度学习应用于结构化表格数据的端到端示例。在第九章,你将学习如何将本书中的示例适应你自己的结构化数据集。
![CH01_F07_Ryan]()
图 1.7 使用 GUI(Watson Studio)创建深度学习模型
在本节中,我们探讨了使用深度学习解决涉及结构化数据的问题的常见反对意见,并回顾了主观回应。然而,主观回应是不够的;我们还需要比较深度学习与非深度学习的代码实现。在第七章中,我们对本书中的扩展示例的两个解决方案进行了面对面比较:深度学习解决方案和基于非深度学习方法 XGBoost 的解决方案。我们比较了这两种方法在性能、模型训练时间、代码复杂性和灵活性方面的差异。
1.6 为什么要用结构化数据问题来研究深度学习?
在 1.5 节中,我们回顾了将深度学习应用于结构化数据的一些反对意见。假设你对这些反对意见的处理方式感到满意。然而,还有一个问题,那就是通过花时间通过一个扩展示例来应用深度学习于结构化数据,你将获得什么好处。许多书籍可以带你通过将深度学习应用于各种问题和数据集的过程。这本书有什么独特之处?使用结构化数据集通过深度学习完成端到端问题的好处是什么?
让我们从大局开始:世界上非结构化数据比结构化数据多得多(learn.g2.com/structured-vs-unstructured-data)。如果 80%的数据是非结构化的,为什么还要费心尝试将深度学习应用于所有数据中结构化数据的小部分呢?尽管非结构化数据可能是结构化数据的四倍,但结构化数据在数据饼图中的份额极其重要。银行、零售商、保险公司、制造商、政府——现代生活的基石——都依赖于关系数据库。每天当你进行日常活动时,你会在各种关系数据库中的数十个甚至数百个表中生成更新。当你用借记卡支付东西、打手机或在线查看银行余额时,你正在访问或更新关系数据库中的数据。除了结构化数据对我们日常生活的重大意义外,许多工作都围绕着结构化表格数据展开。在图像和视频上使用深度学习很有趣,但如果你从事的工作不涉及这类数据怎么办?如果你的工作完全是关于关系数据库中的表格或 CSV 和 Excel 文件怎么办?如果你掌握了将深度学习应用于结构化数据的技术,你将能够将这些技术应用于解决你在工作中遇到的实际问题。
在本书中,您将从始至终学习如何将深度学习应用于表格结构化数据集。您将学习如何准备一个现实世界的数据集(这些数据集具有所有典型的瑕疵和问题),以便为训练深度学习模型做准备,如何根据表格中的列类型对数据集进行分类,以及如何创建一个由这种数据分类自动定义的简单深度学习模型。您将学习这个模型如何结合适应每个数据类别层的组合,以便您可以利用源表中的不同类型的数据(文本、分类和连续)来训练模型。您还将学习如何部署深度学习模型,使其可供其他人使用。本书中您将学习的技巧适用于各种结构化数据集,并允许您释放深度学习解决这些数据集问题的潜力。
1.7 伴随本书的代码概述
本书的核心是一个扩展的编码示例,它将深度学习应用于解决一个具有现实世界结构化数据集的问题。第二章介绍了这个问题,并描述了在此示例中使用的所有代码。在本节中,我们简要总结了用于解决此问题的最重要的程序。
伴随本书的代码由一系列 Jupyter 笔记本和 Python 程序组成,这些程序将您从原始输入数据集引导到部署并训练好的深度学习模型。您可以在mng.bz/v95x找到所有代码,以及相关的数据和配置文件。以下是一些存储库中的关键文件:
-
第二章.ipynb —与第二章中介绍性代码相关的代码片段。
-
第五章.ipynb —与第二章中描述的如何使用 Pandas 进行 SQL 类型操作相关的代码片段。
-
数据准备笔记本 —用于摄取原始数据集并执行常见的数据清洗和准备步骤的代码。此笔记本的输出是一个包含清洗后的训练数据的 Pandas 数据框的 Python pickle 文件。
-
基本数据探索笔记本 —对本书主要示例的数据集进行的基本探索性数据分析,如第三章所述。
-
地理编码数据准备 —从主数据集中的位置值中提取纬度和经度值的相关代码,如第四章所述。
-
时间序列预测数据探索笔记本 —使用第三章中描述的时间序列预测技术,对本书中主要示例的数据集进行额外探索。
-
深度学习模型训练笔记本 —将清洗后的数据重构为考虑特定电车没有延迟期间的格式,并将这种重构的数据形式准备为输入到 Keras 深度学习模型,如第五章和第六章所述。此笔记本的输出是一个训练好的深度学习模型。
-
XGBoost 模型训练笔记本 —用于练习非深度学习模型的代码。这个笔记本与用于训练深度学习模型的笔记本在真正的模型训练代码之前是相同的。在第七章中,我们比较了该模型与深度学习模型的结果。
-
Web 部署 —将训练好的深度学习模型以简单 Web 方式部署的代码,如第八章所述。
-
Facebook Messenger 部署 —将训练好的深度学习模型作为聊天机器人部署到 Facebook Messenger 中的代码,如第八章所述。
书中主要示例所使用的原始数据集不在 repo 中,而是发布在mng.bz/4B2B。
1.8 您需要了解的内容
要充分利用本书,您应该熟悉在 Jupyter Notebooks 以及原始 Python 文件中用 Python 进行编码。您还应该熟悉非深度学习机器学习方法。特别是,您应该掌握以下概念:过拟合、欠拟合、损失函数和目标函数。您应该熟悉常见云环境中的基本操作,例如 AWS、Google Cloud 或 Azure。对于部署部分,您应该对 Web 编程有一些基本的了解。最后,您应该具备关系型数据库的背景知识,并且对 SQL 感到舒适。
本书涵盖了深度学习的核心内容,但并未深入探讨理论细节。相反,它通过一个实际应用的深度学习示例的扩展示例来引导您。如果您需要更深入地了解深度学习及其在 Python 环境中的实现,《Python 深度学习》 是一个极好的资源。我强烈推荐这本书作为本书的补充。以下是提供关于一般深度学习主题背景的三个章节:
-
“神经网络数学基础”——提供了对深度学习基本概念背景的介绍,包括张量(深度学习核心理论数据容器)和反向传播。
-
“神经网络入门”——通过一系列简单的深度学习问题进行指导,涵盖分类(预测输入数据点属于哪个类别)和回归(预测输入数据点的连续值目标)。
-
“高级深度学习最佳实践”——探讨了各种深度学习架构,并包括关于 Keras 回调(本书第六章介绍的主题)以及使用 TensorBoard 监控深度学习模型的详细信息。
在这本书中,我强调提供对深度学习端到端过程的实用探索,从原始输入数据开始,一直到最后部署和训练好的深度学习模型。由于本书涵盖范围广泛,不可能总是深入探讨每个相关技术主题。在整个书中,在适当的地方,我会参考《用 Python 进行深度学习》以及其他 Manning 出版社的出版物和技术文章,以获取更多相关主题的详细信息。此外,第九章推荐了关于深度学习理论背景的资源。
摘要
-
深度学习是一种强大的技术,在过去十年中已经崭露头角。到目前为止,深度学习备受赞誉的应用主要处理非表格型数据,如图像和文本。在这本书中,我展示了深度学习也应该被考虑用于与表格型、结构化数据相关的问题。
-
深度学习通过应用一系列技术(包括基于梯度的优化和反向传播)到输入数据上,自动定义可以预测新数据上结果的功能。
-
深度学习在多个领域已经取得了最先进的结果,但与其他机器学习技术相比,它也存在一些缺点。这些缺点包括模型对哪些特征最为重要的透明度不足,以及对训练数据的强烈需求。
-
有些人认为深度学习不应该应用于表格型、结构化数据。这些人认为深度学习过于复杂,结构化数据集太小,不足以训练深度学习模型,而更简单的替代方案对于结构化数据问题来说是足够的。
-
同时,结构化数据对现代生活至关重要。为什么要把深度学习的范围限制在图像和自由文本上呢?许多重要问题都涉及结构化数据,因此学习如何利用深度学习来解决结构化数据问题是非常有价值的。
2. 介绍示例问题和 Pandas 数据框
本章涵盖
-
深度学习开发环境选项
-
Pandas 数据框的介绍
-
介绍本书中用于说明结构化数据深度学习的主要示例:预测电车延误
-
示例数据集的格式和范围
-
关于使用结构化数据进行深度学习的常见反对意见的更多细节
-
深度学习模型训练过程的初步了解
在本章中,你将了解你可以选择的深度学习环境选项以及如何将表格结构化数据引入你的 Python 程序。你将获得 Pandas 的概述,这是用于操作表格结构化数据的 Python 工具。你还将了解本书中用于演示结构化数据深度学习的主要示例,包括主要示例中使用的详细数据集和该示例代码的整体结构。然后,你将获得关于第一章中引入的结构化数据深度学习反对意见的更多细节。最后,我们将提前一瞥,并通过一轮深度学习模型的训练来激发你对第 3-8 章中检查的扩展示例的兴趣。
2.1 深度学习开发环境选项
在你开始一个深度学习项目之前,你需要访问一个提供你所需要的硬件和软件堆栈的环境。本节描述了你在深度学习环境方面可以选择的选项。
在你查看专门为深度学习设计的特定环境之前,重要的是要知道,你可以在标准的 Windows 或 Linux 环境中完成本书中的扩展代码示例。使用具有深度学习特定硬件的环境可以加快模型训练过程,但这并不是必需的。我在我的 Windows 10 笔记本电脑上运行了本书中的代码示例,该电脑有 8 GB 的 RAM 和单核处理器,以及 Paperspace Gradient 环境(在本节中描述)。在 Gradient 中模型训练快了大约 30%,但这意味着第七章中描述的每个实验的训练时间只相差一分钟左右。对于更大的深度学习项目,我强烈推荐本节中描述的深度学习启用环境之一,但配备合理的现代笔记本电脑就足以完成本书中的扩展示例。如果你决定在你的本地系统上尝试运行代码示例,请确保你的 Python 版本至少为 3.7.4。如果你正在安装 Python 或使用虚拟 Python 环境,你需要安装 Pandas、Jupyter、sci-kit learn 和 TensorFlow 2.0。随着你通过示例的进展,你可能需要安装额外的库。
重要提示:本书中的大多数代码示例都可以在相同的 Python 环境中运行。唯一的例外是第八章中描述的 Facebook Messenger 部署。这个部署需要在具有 TensorFlow 1.x 的 Python 环境中完成,而模型训练则需要具有 TensorFlow 2.0 或更高版本的 Python 环境。为了解决 TensorFlow 版本之间的冲突,您应该利用 Python 虚拟环境来运行代码示例。我建议您将您的基 Python 环境升级到最新的 TensorFlow 1.x 版本,并用于除运行模型训练笔记本之外的所有操作。为模型训练笔记本创建一个虚拟环境,并在该环境中安装 TensorFlow 2.0。这样做将使您在模型训练步骤中享受到 TensorFlow 2.0 的好处,同时保持其余 Python 环境的向后兼容性和稳定性。您可以在本文中找到设置 Python 虚拟环境的详细信息:mng.bz/zrjr。
几个云服务提供商提供的深度学习环境大约每小时只需一杯咖啡的费用。每个云环境都有其优势和劣势,其中一些(Azure 和 IBM Cloud)强调创建第一个项目的简便性,而其他一些(Amazon Web Services [AWS])则提供规模和先发优势。以下是一些提供深度学习环境的云服务提供商:
-
AWS 可以在这里访问:
mng.bz/0Z4m。AWS 中的 SageMaker 环境抽象了一些管理机器学习模型的复杂性。AWS 包括关于 SageMaker 的良好教程,包括一个(mng.bz/9A0a),它引导您完成训练和部署模型的端到端过程。 -
Google Cloud (
mng.bz/K524) 也提供了易于使用的教程,包括一个(mng.bz/jVgy),展示了如何在 Google Cloud 平台上部署深度学习模型。 -
Azure (
mng.bz/oREN) 是微软的云环境,包括几个深度学习项目的选项。在mng.bz/8Gp2上的教程提供了一个简单的介绍。 -
Watson Studio Cloud (
mng.bz/nz8v) 提供了一个专注于机器学习的环境,您可以在不深入了解 IBM Cloud 的所有细节的情况下利用它。mng.bz/Dz29上的文章提供了一个快速概述,以及到 AWS SageMaker、Google Cloud 和 Azure 的配套概述文章的链接。
所有这些云环境都提供了深度学习项目所需的一切,包括 Python、大多数所需的库,以及访问深度学习加速硬件,包括图形处理单元(GPUs)和张量处理单元(TPUs)。
深度学习模型的训练依赖于大规模矩阵操作,这些加速器使得这些操作可以更快地运行。正如之前所述,您可以在没有 GPU 或 TPU 的环境中训练简单的深度学习模型,但训练过程将会更慢。有关本书扩展示例中哪些部分将受益于在具有深度学习特定硬件的环境中运行的详细信息,请参阅第 2.11 节。
除了提供深度学习所需的硬件和软件外,本节列出的云环境还满足了许多对开发深度学习模型感兴趣之外的用户需求。以下是两个专门针对机器学习的云环境:
-
Google Colaboratory (Colab ;
mng.bz/QxNm) 是由 Google 提供的一个免费 Jupyter Notebooks 环境。 -
Paperspace (
towardsdatascience.com/paperspace-bc56efaf6c1f) 是一个专注于机器学习的云环境。您可以使用 Paperspace Gradient 环境 (gradient.paperspace.com/) 通过一键创建一个 Jupyter Notebooks 环境,在那里您可以开始您的深度学习项目。图 2.1 显示了 Paperspace 控制台中的 Gradient 笔记本。

图 2.1 Paperspace Gradient:一键式深度学习云环境
您可以使用这些云环境中的任何一个来练习本书附带的代码。为了简化流程并最大限度地利用您学习深度学习的时间,如果您打算使用云环境而不是本地系统,我建议您利用 Paperspace Gradient。您将获得一个可靠的环境,它正好提供您所需的一切,无需担心其他云环境提供的任何附加云服务。Gradient 需要信用卡来设置。您预计每小时大约需要支付 1 美元的基本 Gradient 环境费用。根据您处理代码的速度以及您在不用时关闭 Gradient 笔记本的勤奋程度,您可能需要支付总计 30-50 美元来完成本书中的代码示例。
如果成本是关键考虑因素,并且您不想使用本地系统,Colab 是一个很好的替代云环境。您在使用 Colab 时的体验可能不会像在 Paperspace Gradient 中那么顺畅,但您不必担心成本。参见附录 A,了解有关设置 Colab 所需了解的更多信息,以及与 Paperspace Gradient 相比 Colab 的优缺点描述。
除了 Colab 和 Paperspace Gradient 之外,主流的云服务提供商(包括 AWS、Google Cloud Platform 和 IBM Cloud)都提供了可用于深度学习开发的 ML 环境。所有提供商都提供某种形式的有限免费访问其 ML 环境。如果你已经在使用这些平台之一,并且在你用完免费访问后可以接受付费,那么这些主流提供商中的一个可能是一个不错的选择。
2.2 探索 Pandas 的代码
当你克隆了与这本书相关的 GitHub 仓库 (mng.bz/v95x),你将在 notebooks 子目录中找到与探索 Pandas 相关的代码。下一个列表显示了包含本章描述的代码的文件。
列表 2.1 与 Pandas 基础相关的代码
├── data ❶
│
├── sql
│ streetcarjan2014.sql ❷
│
├── notebooks
│ chapter2.ipynb ❸
│ chapter5.ipynb ❹
❶ 数据文件目录
❷ 定义与输入数据集相同列的表的 SQL 代码
❸ 包含基本 Pandas 示例的笔记本
❹ 包含如何在 Pandas 中执行你通常在 SQL 中做的操作的示例的笔记本
2.3 Python 中的 Pandas 数据框
如果你正在阅读这本书,你将熟悉关系数据库以及数据在行和列组成的表中的组织。你可以将 Pandas 库视为 Python 的本地(Pythonic)方法来表示和操作表格结构化数据。Pandas 中的关键结构是 dataframe 。你可以将 dataframe 视为 Python 对关系表的实现。像表一样,dataframe
-
有行和列(并且列可以有不同的数据类型)。
-
可以有索引。
-
可以根据某些列的值与其他数据框连接。
在我详细介绍 Pandas 数据框之前,假设你想通过 Python 操作一个简单的表格数据集,即 Iris 数据集 (gist.github.com/curran/a08a1080b88344b0c8a7)。
-
将 CSV 文件加载到 Python 结构中
-
计算数据集中的行数
-
计算数据集中有多少行物种为 setosa
图 2.2 显示了 Iris 数据集的子集作为 CSV 文件的样子。

图 2.2 Iris 数据集作为 CSV 文件
要做到这一点,你需要创建一个包含数据集内容的 Pandas 数据框。你可以在下一个列表中找到 chapter2.ipynb 笔记本中的代码。
列表 2.2 从 URL 引用的 CSV 创建 Pandas 数据框
import pandas as pd ❶
url="https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/\
raw/d546eaee765268bf2f487608c537c05e22e4b221/iris.csv" ❷
iris_dataframe=pd.read_csv(url) ❸
iris_dataframe.head() ❹
❶ 导入 pandas 库。
❷ Iris 数据集的原始 GitHub URL
❸ 将 URL 的内容读取到 Pandas 数据框中。
❹ 显示新数据框的前几行。
在 chapter2 笔记本中运行此单元格,并注意输出。head () 调用以易于阅读的格式列出数据框的前几行(默认为五行),如图 2.3 所示。

图 2.3 使用 Iris 数据集加载数据框的 head() 输出
与原始数据集的前几行进行比较:

原始 CSV 文件和 Pandas 数据框具有相同的列名和相同的值,但数据框中的第一列是怎么回事呢?默认情况下,Pandas 数据框中的每一行都有一个名称,这个名称(默认情况下包含在数据框的第一列中)是一个从零开始的顺序号。你也可以将这个第一列视为数据框的默认索引。
现在你想要获取数据框中的行数。以下语句返回数据框中的总行数:
iris_dataframe.shape[0]
150
最后,你想要计算数据框中物种为 setosa 的行数。在以下语句中,iris_dataframe[iris_dataframe["species"] == 'setosa'] 定义了一个只包含原始数据框中 species 等于 "setosa" 的行的数据框。使用与获取原始数据框行数相同的方式使用 shape 属性,你可以使用这个 Python 语句来获取 species 等于 "setosa" 的数据框中的行数:
iris_dataframe[iris_dataframe["species"] == 'setosa'].shape[0]
50
随着我们通过本书中的主要示例进行学习,我们将探索 Pandas 数据框的更多功能。此外,2.5 节包含使用 Pandas 执行常见 SQL 操作的示例。到目前为止,这次探索已经教会了你如何将表格结构化数据导入 Python 程序,在那里你可以准备它以训练深度学习模型。
2.4 将 CSV 文件导入 Pandas 数据框
在 2.3 节中,你看到了如何将标识为 URL 的 CSV 文件导入 Pandas 数据框,但假设你有一个自己修改过的数据集的私有副本。你想要将这个修改后的文件中的数据加载到数据框中。在本节中,你将了解如何从文件系统中读取 CSV 文件到数据框中。在第三章中,你将学习如何将具有多个标签的 XLS 文件导入单个数据框。
假设你想将 Iris 数据集加载到一个数据框中,但你已经在本地文件系统中修改了你自己的数据集副本(称为 iriscaps.csv),将物种名称大写以符合使用此数据集的应用程序的样式指南。你需要从文件系统中加载这个修改后的数据集,而不是从原始的 Iris 数据集中加载。将文件系统中的 CSV 文件导入 Pandas 数据框的代码(如下所示)与您已经看到的从 URL 加载数据框的代码相似。
列表 2.3 从 CSV 文件名创建 Pandas 数据框
import pandas as pd ❶
file = "iriscaps.csv" ❷
iris_dataframe=pd.read_csv(os.path.join(path,file)) ❸
iris_dataframe.head() ❹
❶ 导入 pandas 库。
❷ 定义文件名。
❸ 将文件内容读入 Pandas 数据框。
❹ 显示新数据框的前几行。
如何获取正确的路径值,即数据文件所在的目录?本书中的所有代码示例都假设所有数据都存在于一个名为 data 的目录中,该目录是笔记本所在目录的兄弟目录。在这个存储库中,顶级目录 notebooks 和 data 分别包含代码文件和数据文件。
列表 2.4 是获取笔记本(rawpath)所在目录的代码。然后,代码使用该目录通过先访问笔记本所在目录的父目录,然后访问父目录中的数据目录来获取数据文件所在的目录。
列表 2.4 获取数据目录的路径
rawpath = os.getcwd() ❶
print("raw path is",rawpath)
path = os.path.abspath(os.path.join(rawpath, '..', 'data')) ❷
print("path is", path)
❶ 获取这个笔记本所在的目录。
❷ 获取数据文件所在的目录。
注意,你使用的是相同的 read_csv 函数,但参数是文件的文件系统路径,而不是 URL。图 2.4 显示了修改后的数据集,其中物种名称已大写。

图 2.4 从文件加载的 DataFrame,其中物种值已大写
在本节中,你已经回顾了如何使用文件系统中的 CSV 文件内容加载 Pandas DataFrame。在第三章中,你将看到如何加载包含 XLS 文件内容的 DataFrame。
2.5 使用 Pandas 实现与 SQL 相同的操作
2.3 节介绍了 Pandas 库作为 Python 解决方案,用于操作结构化、表格数据。在本节中,我们将深入探讨这个堆栈的部分,展示一些如何使用 Pandas 完成你习惯用 SQL 完成的表格操作的示例。本节不是 SQL 到 Pandas 的完整字典;请参阅 mng.bz/lXGM 和 http://sergilehkyi.com/ translating-sql-to-pandas 以获取更多 SQL 到 Pandas 的示例。以下是一些说明如何使用 Pandas 产生与 SQL 相同结果的示例。
为了练习以下示例
-
通过加载 2014 年街车延误数据集中第一个标签页的 CSV 文件内容,在关系型数据库中创建一个名为 streetcarjan2014 的表(示例假设使用 Postgres)。确保
"Min Delay"的类型是数值型。 -
使用 chapter5.ipynb 笔记本从相同的 CSV 文件创建一个 Pandas DataFrame。此笔记本假设 CSV 文件位于一个名为 data 的目录中,该目录是笔记本所在目录的兄弟目录。
现在我们来看一些等价的 SQL 和 Pandas 语句。首先,获取表的头三行
-
SQL —
select * from streetcarjan2014 limit 3 -
Pandas —
streetcarjan2014.head(3)
图 2.5 显示了 SQL 查询和结果,图 2.6 显示了 Pandas 的相同内容。
在 select 语句上有一个单一的条件
-
SQL —
select"Route"fromstreetcarjan2014where"Location"='King andShaw' -
Pandas —
streetcarjan2014[streetcarjan2014.Location == "King and Shaw"].Route

图 2.5 SQL 前三条记录

图 2.6 Pandas 前三条记录
列出列中的唯一条目
-
SQL —
select distinct "Incident" from streetcarjan2014 -
Pandas —
streetcarjan2014.Incident.unique()
在select语句中设置多个条件
-
SQL —
select * from streetcarjan2014 where "Min Delay" > 20 and "Day" = 'Sunday' -
Pandas —
streetcarjan2014[(streetcarjan2014['Min Delay'] > 20) & (streetcarjan2014['Day'] == "Sunday")]
图 2.7 显示了 SQL 查询和结果,图 2.8 显示了 Pandas 的相同内容。

图 2.7 多条件 SQL 查询

图 2.8 多条件 Pandas 查询
在select语句中设置order by
-
SQL —
select "Route", "Min Delay" from streetcarjan2014 where "Min Delay" > 20 order by "Min Delay" -
Pandas —
streetcarjan2014[['Route','Min Delay']][(streetcarjan2014['Min Delay'] > 20)].sort_values('Min Delay')
在本节中,我们介绍了一些如何使用 Pandas 进行常见 SQL 操作的例子。随着你继续使用 Pandas,你会发现许多其他方式,Pandas 可以使你在 Python 的世界中使用 SQL 经验变得更加容易。
2.6 主要示例:预测电车延误
现在你已经体验了如何将表格结构化数据引入 Python 程序,让我们来检查本书中使用的重大示例:预测电车延误。
要成功进行深度学习项目,你需要数据和一个明确要解决的问题。在本书中,我使用了一个由多伦多市政府发布的公开数据集(mng.bz/4B2B),该数据集描述了自 2014 年 1 月以来城市电车系统中遇到的每一个延误。要解决的问题是如何预测多伦多电车系统的延误,以便可以预防。在本章中,你将了解这个数据集的格式。在随后的章节中,你将学习如何纠正数据集中需要修复的问题,以便可以使用它来训练深度学习模型。
为什么多伦多电车延误问题很重要?在第二次世界大战之前,北美许多城市都有电车系统。这些系统在一些地区被称为有轨电车,由轻轨车辆组成,通常单独运行,由从架空电缆或有时从街道上的轨道获取的电力驱动,并在与其他道路交通共享的空间上行驶。尽管多伦多的一些电车网络在专用车道上,但大多数系统都在公共街道上与其他交通混合运行。
在战后时期,大多数北美城市用公交车取代了电车。一些城市保留了一些象征性的电车服务作为旅游景点。然而,多伦多在北美城市中是独一无二的,因为它将其广泛的电车网络作为其整体公共交通系统的一个关键部分保留下来。如今,电车服务多伦多五个最繁忙的地面路线中的四个,并且每个工作日可搭载多达 30 万名乘客。
电车网络相对于公交车和地铁等其他构成多伦多公共交通系统的交通方式有许多优势。与公交车相比,电车使用寿命更长,不排放污染物,每名驾驶员可搭载的乘客至少是公交车的一倍,建设和维护成本更低,并提供更灵活的服务。
电车有两个主要缺点:它们容易受到一般交通中的障碍物的影响,并且难以绕过这些障碍物。当电车被阻挡时,它会导致电车网络中的累积延误,并加剧城市最繁忙街道的整体交通拥堵。
使用多伦多市提供的电车延误数据集,我们将应用深度学习来预测和预防电车延误。图 2.9 显示了叠加在多伦多地图上的电车延误热图。您可以在 streetcar_data-geocode-get-boundaries 笔记本中找到生成此地图的代码。地图上电车延误最严重的区域(较暗的块状区域)是城市核心最繁忙的街道。

图 2.9 多伦多电车网络:延误热图
在我详细说明这个问题的数据集之前,解释一下为什么选择这个特定问题是有意义的。为什么不选择一个标准的商业问题,比如客户流失(预测客户是否会取消服务)或库存控制(预测零售店何时会耗尽某种商品的库存)?为什么选择一个特定活动(公共交通)和特定地点(多伦多)的问题?以下是选择这个问题的几个原因:
-
它有一个“金发姑娘”数据集(既不太大也不太小)。一个非常大的数据集会带来额外的数据管理问题,这些问题与学习深度学习并不相关。大数据集也可能掩盖代码和算法中的不足。俗语“数据越多,胜算越大”可能适用于深度学习,但在学习过程中,没有大量数据作为支撑也有其道理。另一方面,如果数据太少,深度学习就没有足够的信号来检测。电车数据集(目前,超过 70,000 行)足够大,可以应用于深度学习,但又不至于太大,使得探索变得困难。
-
该数据集是实时更新的。每隔几个月就会更新一次,因此有足够的机会使用它从未见过的数据进行模型测试。
-
该数据集是真实且原始的。这个数据集是为了几个目的在几年内收集的,其中没有一个目的是训练深度学习模型。正如你将在下一章中看到的,这个数据集有许多错误和异常,需要在数据集用于训练深度学习模型之前进行处理。在实践中,你将在许多商业应用中看到类似的混乱数据集。通过清理现实世界的电车数据集,包括所有瑕疵,你将准备好应对其他现实世界的数据集。
-
企业必须应对竞争和监管压力,这使得它们无法公开分享他们的数据集,这使得从严肃的商业中找到真实、非平凡的数据集变得困难。相比之下,公共机构通常有法律义务发布他们的数据集。我利用多伦多对电车延误数据集的开放性,为本书构建了主要示例。
-
该问题对广泛的受众都是可访问的,并且与任何特定行业或学科无关。
尽管电车问题具有不特定于任何商业的优势,但它可以直接关联到深度学习通常应用的常见商业问题,包括
-
客户支持 —数据集中的每一行都相当于客户支持系统中的一个工单。事件列类似于工单系统中出现的自由文本摘要,而最小延迟和最小间隔列则扮演着类似于在工单系统中通常记录的问题严重性信息的作用。报告日期、时间和日期列映射到客户支持工单系统中的时间戳信息。
-
物流 —与物流系统类似,电车网络具有空间性质(在数据集中的路线、位置和方向列中隐含)和时间性质(在报告日期、时间和日期列中隐含)。
2.7 为什么现实世界的数据集对于学习深度学习至关重要?
当你学习深度学习时,为什么与真实且混乱的数据集一起工作如此重要?当你考虑将深度学习应用于真实世界的数据集时,你可以将其比作被要求从过去四十年收集的媒体盒中创建蓝光光盘。这个盒子包含各种格式,包括 3:4 宽高比的模拟视频、模拟照片、单声道音频录制和数字视频。有一点很清楚:盒中的所有媒体都不是为了蓝光而制作的,因为其中大部分是在蓝光存在之前录制的。你将不得不对每个媒体源进行预处理,以便为蓝光包做准备,例如校正颜色、清理 VHS 抖动和调整模拟视频的宽高比。对于单声道音频,你需要去除磁带噪音并将单声道音频轨道扩展到立体声。图 2.10 总结了将各种元素组装起来生成蓝光光盘的过程。
同样,你可以打赌你组织中的深度学习候选问题都有数据集,这些数据集在收集时并没有考虑到深度学习。你需要进行清理、转换和扩展,以获得一个可以用于训练深度学习模型的准备好的数据集。

图 2.10 准备真实世界数据集以用于深度学习就像从媒体盒中创建蓝光光盘。
2.8 输入数据集的格式和范围
现在我们已经回顾了街车延误问题和与真实世界数据集一起工作的重要性,让我们深入了解街车延误数据集的结构(mng.bz/4B2B)。这个数据集具有以下文件结构:
-
每年一个 XLS 文件
-
在每个 XLS 文件中,为每个月份设置一个标签页
图 2.11 展示了街车延误数据集的文件结构。

图 2.11 街车延误数据集的文件结构

图 2.12 街车延误数据集的列
图 2.12 展示了街车延误数据集中的列:
-
报告日期 — 导致延误事件发生的日期(YYYY/MM/DD)
-
路线 — 街车路线的编号
-
时间 — 导致延误事件发生的时间(hh:mm:ss AM/PM)
-
日期 — 日期名称
-
位置 — 导致延误事件的位置
-
事件 — 导致延误的事件描述
-
最小延误 — 下一个街车在计划中的延误时间(分钟)
-
最小间隔 — 前一个街车与下一个街车之间的总计划时间(分钟)
-
方向 — 路线的方向(E/B, W/B, N/B, S/B, B/W 等),其中 B/W 表示双向
-
车辆—涉及事件的车辆 ID
值得花更多时间来审查这些列的一些特征:
-
报告日期 — 这列包含了许多对深度学习模型可能很有价值的信息。在第五章中,我们将重新审视这一列,为该列的子组件(年、月和日)添加派生列到数据集中。
-
日期 — 这列是否重复了已经在“报告日期”列中编码的信息?对于这个问题,一个事件发生在周一并且恰好是月底的最后一天是否相关?我们将在第九章中探讨这些问题。
-
位置 — 这列是数据集中最有趣的一列。它以具有挑战性和开放式的方式编码了数据集的地理方面。在第四章中,我们将重新审视这一列,以回答一些重要问题,包括为什么这些数据没有以经纬度的形式编码,以及电车网络的独特地形是如何在“位置”列的值中体现出来的。在第九章中,我们将探讨为深度学习模型编码这些信息的最有效方法。

图 2.13 电车数据集按年记录数
当前数据集有超过 70,000 条记录,每月新增 1,000 至 2,000 条新记录。图 2.13 显示了自 2014 年 1 月数据集开始以来的每年记录数。原始记录数是输入数据集中的记录数。清理后的记录数是删除了无效值(如指定在无效的电车路线上延误)的记录后的记录数,如第三章所述。
2.9 目标:端到端解决方案
在本书的其余部分,我们将解决预测电车延误的问题。我们将清理输入数据集,构建深度学习模型,对其进行训练,然后部署它以帮助用户获得关于他们的电车行程是否会延误的预测。
图 2.14 显示了遵循本书中扩展示例后你将得到的一个结果。你将能够从使用本章引入的原始数据集派生出的数据训练的深度学习模型中获得关于特定电车行程是否会延误的预测。

图 2.14 扩展示例的一个结果:Facebook Messenger 部署
图 2.15 总结了通过扩展示例电车延误的端到端旅程,从本章引入的原始数据集到允许用户获得关于他们的电车行程是否会延误的模型部署。请注意,图 2.15 中显示的两个部署方法(Facebook Messenger 和 Web 部署)是达到同一目的的两种手段:让想要获得关于他们的电车行程是否会延误的预测的用户能够访问训练好的深度学习模型。

图 2.15 从原始数据到街车行程预测的旅程
图 2.15 中的数字突出了每个章节所涵盖的内容:
-
第二章介绍了我们将用于扩展示例的原始数据集。
-
第三章和第四章描述了您清理原始数据集并准备训练深度学习模型的步骤。
-
第五章描述了如何使用 Keras 库创建一个简单的深度学习模型。
-
第六章描述了如何使用第三章和第四章中准备的数据集来训练 Keras 深度学习模型。
-
第七章描述了如何进行一系列实验,以确定深度学习模型变化方面的影响,以及用非深度学习方法替换深度学习模型的影响。
-
第八章展示了如何部署在第六章中训练的深度学习模型。您将使用 scikit-learn 库的管道工具来处理用户提供的行程数据,以便训练的深度学习模型可以进行预测。第八章将引导您了解两种部署选项。首先,我们将通过 Flask(
flask.palletsprojects.com/en/1.1.x)这个 Python 基本网络应用框架提供的网页来部署训练好的模型。这种方法简单直接,但用户体验有限。第二种部署方法通过使用 Rasa 聊天机器人框架来解释用户的行程预测请求,并在 Facebook Messenger 中显示训练模型的预测结果,从而提供更丰富的用户体验。
2.10 关于构成解决方案的代码的更多细节
第 2.9 节中描述的端到端旅程是通过一系列 Python 程序以及每个程序的输入和输出文件来实现的。正如第一章所述,这些文件可在本书的代码库中找到:mng.bz/v95x。下面的列表显示了代码库中的关键目录,并总结了每个目录中的文件。
列表 2.5 代码库的目录结构
│
├── data ❶
│
├── deploy ❷
│ │
│ ├── data ❸
│ │
│ └── models ❹
│
├── deploy_web ❺
│ │
│ ├── static
│ │ └── css ❻
│ │
│ └── templates ❼
│
├── models ❽
│
├── notebooks ❾
│
├── pipelines ❿
│
└── sql ⓫
❶ 保存的中间 Pandas 数据框和其他数据输入。请注意,原始数据集的 XLS 文件不在代码库中;您需要从 mng.bz/4B2B 获取它们。
❷ Facebook Messenger 部署文件
❸ Rasa 聊天机器人训练数据
❹ Rasa 聊天机器人模型。请注意,在 Facebook Messenger 部署中使用的 Rasa 聊天机器人模型与深度学习模型不同。
❺ 网络部署文件
❻ 网络部署的 CSS
❼ 网络部署的 HTML 文件
❽ 保存的训练深度学习和 XGBoost 模型
❾ 用于数据清理、数据探索和模型训练的 Jupyter 笔记本,以及相关的配置文件。有关配置文件在街车延误预测示例中使用的描述,请参阅第三章。
❿ 保存的管道文件
⓫ SQL 示例
图 2.16 和图 2.17 从 Python 程序及其之间的文件流的角度描述了与图 2.15 相同的端到端旅程。图 2.16 展示了从原始数据集到训练好的深度学习模型的演变过程:

图 2.16 从原始数据集到训练好的模型的文件演变
-
我们从本章描述的由 XLS 文件组成的原始数据集开始。当你运行数据准备笔记本后,你可以将 XLS 文件中的数据保存为 pickle 格式的 dataframe,这样你就可以方便地重新运行数据准备笔记本。第三章解释了 pickle 功能,它允许你在文件中序列化 Python 对象,以便在 Python 会话之间保存对象。
-
数据准备笔记本 streetcar_data_preparation.ipynb 通过清理(例如,将重复值映射到公共值,并删除无效值的记录)数据集来生成清洁后的 pickle 格式的 dataframe。这个过程在第三章和第四章中有所描述。
-
清洁后的 pickle 格式的 dataframe 被输入到模型训练笔记本 streetcar_model_training.ipynb 中,该笔记本重构数据集并使用它生成 pickle 格式的管道文件和训练好的深度学习模型文件。这个过程在第五章和第六章中有所描述。
图 2.17 从模型训练笔记本生成的训练好的模型文件和 pickle 格式的管道文件中继续讲述故事。这些管道在部署中使用,用于将用户输入的行程信息(例如,电车路线和方向)转换为训练好的深度学习模型可以接受的格式。正如你将在第八章中看到的,这些管道在过程中出现两次。首先,它们转换用于训练深度学习模型的数据;然后它们转换用户输入,以便训练好的模型可以为其生成预测。第八章描述了两种部署方式:网页部署,用户在网页中输入行程信息,以及 Facebook Messenger 部署,用户在 Messenger 会话中输入行程信息。

图 2.17 从训练好的模型到部署的文件演变
在本节中,你看到了从输入数据集到可部署的深度学习模型旅程的两个视角,该模型可以用来预测电车行程是否会延误。这个旅程涵盖了广泛的技术组件,但如果你对其中的一些组件不熟悉,请不要担心。我们将逐章检查它们。当你完成第八章时,你将拥有一个端到端的深度学习解决方案,用于预测电车延误,该解决方案使用结构化数据。
2.11 开发环境:普通与深度学习支持
在 2.1 节中,我们回顾了在这本书中使用的环境选项。在本节中,我们将回顾哪些代码子集会从深度学习支持的环境中受益,哪些则可以在没有深度学习特定硬件(如 GPU 和 TPU)的vanilla系统中运行良好。这个 vanilla 系统可以是您的本地系统(在您安装了 Jupyter Notebooks、Python 和所需的 Python 库之后)或非 GPU/非 TPU 的云环境。
图 2.18 展示了端到端解决方案,突出了哪些区域将受益于深度学习支持的环境,哪些可以在 vanilla 环境中工作:
-
数据准备代码(在第 2-4 章中描述)可以在 vanilla 环境或深度学习环境中使用。在这些部分中您将执行的操作不需要任何深度学习特定硬件。我已经在 Paperspace Gradient 和我的没有深度学习特定硬件的本地 Windows 机器上运行了数据准备代码。
-
如果您没有访问深度学习特定硬件(如 Paperspace、Azure 或 Colab 中可用的 GPU,或 Google Cloud Services 和 Colab 中可用的 TPU),那么第 5-7 章中描述的模型训练代码将运行得更慢。
-
第八章中描述的部署代码可以在 vanilla 环境或深度学习环境中使用。我已经在 Azure(使用标准、非 GPU 启用虚拟机)和我的本地 Windows 机器上完成了部署。当模型部署时,它不需要深度学习特定硬件。
![CH02_F18_Ryan]()
图 2.18 深度学习支持环境受益的过程部分
2.12 深入探讨深度学习的反对意见
在第一章中,我们简要回顾了深度学习的优缺点。对深度学习与非深度学习机器学习进行更详细的比较是值得的。为了简化,在本章中,我们将简单地称后者为经典机器学习。
当我们处理具有结构化表格数据的问题时,我们需要将经典机器学习与深度学习进行对比。传统观点是,在结构化数据上使用经典机器学习,而不是深度学习。本书的整个目的就是探讨深度学习如何应用于结构化数据,因此我们需要为这种方法提供一些动机,并检查“如果数据是结构化的,不要使用深度学习”这一格言背后的推理。

图 2.19 使用结构化数据进行深度学习的反对意见
让我们更深入地探讨第一章中引入的、图 2.19 中展示的深度学习的反对意见:
-
结构化数据集太小,无法用于深度学习。 这个反对意见背后有一个非常合理的观点,即使整个反对意见经不起推敲。当然,许多结构化数据集对于深度学习来说太小了,这可能就是人们认为结构化数据集太小无法用于深度学习的来源。然而,关系表通常有数千万甚至数十亿行。顶级商业关系数据库供应商支持超过十亿行的表。易于找到的开源结构化数据集往往很小,因此当您在寻找要研究的问题时,找到适合经典机器学习的小型结构化数据集比找到大型开源结构化数据集要容易。因此,数据集大小的问题更多的是一个便利性问题,而不是结构化数据集固有的规模问题。幸运的是,本书中使用的电车数据集既公开可用,又足够大,使其成为深度学习的一个有趣主题。
-
保持简单。 选择经典机器学习而不是深度学习来处理结构化数据集的最常见论点是简单性。其理由是,经典机器学习算法比深度学习算法更简单、更容易使用,并且更透明。在第七章中,您将看到针对电车延误预测问题的深度学习解决方案与基于 XGBoost(一种非深度学习方法)的解决方案的直接比较。这两种实现都使用了相同的数据准备步骤和相同的管道,这构成了构成解决方案的大部分代码。解决方案的核心是模型的定义、训练和评估,这是两种解决方案的分岔点。在该解决方案的部分中,深度学习方法的代码行数大约有 60 行,而 XGBoost 的代码行数少于 20 行。尽管 XGBoost 解决方案的核心代码行数较少,但这一部分代码在两种方法的总代码行数中不到 10%。此外,深度学习方法核心部分的额外复杂性使其具有更大的灵活性,因为它可以处理包含自由文本列的数据集。
-
深度学习容易受到对抗性攻击。 . 有一些广为人知的例子表明,深度学习系统被愚弄,错误地评分了故意修改以利用系统漏洞的数据示例。例如,一只熊猫的照片可以通过一种对人类来说难以察觉的方式被篡改,但会欺骗深度学习模型将其误识别为长臂猿。您可以在
mng.bz/BE2g看到这张照片。对我们来说,篡改后的图像仍然看起来像一只熊猫,但深度学习系统将该图像评分为一长臂猿。 -
我在第一章中提出,商业和政府的瑰宝在于结构化数据。如果恶意行为者能够误导深度学习系统,我们为何还要信任深度学习系统去分析如此有价值的数据呢?这种漏洞是否仅限于深度学习?
mng.bz/dwyX上的文章认为,经典机器学习也遭受与深度学习相同的欺骗性漏洞。如果情况如此,为何对深度学习被欺骗的关注如此之多?围绕深度学习的炒作引发了反叙事,深度学习被欺骗的例子成为更好的口头禅。听到一个深度学习模型将长颈鹿误认为是北极熊,比听到一个线性回归模型因为输入表中某一列被篡改的值而预测错误更有趣。最后,深度学习和经典机器学习对对抗性攻击的脆弱性取决于攻击者获取关于模型性质的内部信息。如果对模型安全有适当的治理,数据瑰宝不应受到对抗性攻击的威胁。 -
手工制作深度学习解决方案的时代即将结束。机器学习的世界发展迅速,其潜在影响巨大,因此预测非专业人士在 2030 年代初如何利用机器学习是不明智的。回想一下 1990 年代中期对多媒体的兴趣。当时,即使没有直接参与创建多媒体平台的人,如果想要利用多媒体,也必须关注声卡设备驱动程序和中断请求冲突的奥秘。如今,我们理所当然地认为计算机具有音频和视频功能,只有少数人需要担心多媒体的细节。由于缺乏使用,多媒体这个术语现在已经过时;这项技术已经变得如此普遍,以至于我们不再需要为它命名。机器学习和深度学习会步上相同的命运吗?自动化解决方案,如谷歌的 Auto ML (
cloud.google.com/automl),可能会发展到只有相对少数的深度机器学习专家需要手动编码机器学习解决方案的程度。即使真的发生这种情况,深度学习的根本概念以及在实际数据集上利用深度学习所需采取的实际步骤仍然非常重要,需要理解。这些概念对机器学习来说就像元素周期表对化学一样。除非我们专攻化学或相关学科,否则我们中的大多数人不必直接应用元素周期表中表达的基础概念。然而,了解元素周期表以及它告诉我们关于物质性质的信息仍然至关重要。以类似的方式,即使在未来不久机器学习系统的实现大部分是自动化的,深度学习的根本概念仍然值得理解。由于你了解它在某种程度上是如何工作的,你将能够更好地判断深度学习的适用性。
我们更详细地考察了一些对深度学习的反对意见。特别是,深度学习对非专业人士来说过于困难这一反对意见,五年前比现在更为真实。近年来发生了什么变化,使得深度学习能够在更多的问题领域中成为竞争者,并使其对非专业人士易于访问?我们将在下一节中回答这个问题。首先,我们可以问哪种方法更适合处理结构化数据:深度学习还是经典机器学习。在第五章和第六章中,你将看到用于预测电车延误的深度学习模型的代码。你还将看到一系列实验的结果,以衡量训练好的深度学习模型的表现。此外,第七章描述了使用经典机器学习算法 XGBoost 的替代解决方案来解决电车延误预测问题。在该章中,你将能够直接比较深度学习解决方案的代码与 XGBoost 解决方案的代码。你还将看到两种解决方案的性能对比,以便你可以自己得出结论,关于哪种方法更适合解决此问题。
2.13 深度学习如何变得更加易于访问
在 2.12 节中,我们回顾了一些对深度学习的反对意见。是什么变化使得这种比较更有意义,为什么深度学习现在成为与经典机器学习并驾齐驱的可行方法?图 2.20 总结了过去十年中的一些变化,这些变化使得深度学习的力量得以向更广泛的受众开放。

图 2.20 使深度学习对非专业人士易于访问的近期变化
以下是图 2.20 中列出的变化的更多细节:
-
云环境 ——如 2.1 节所述,所有主要的云服务提供商都提供深度学习环境,包括用于深度学习模型高效训练的软件堆栈和专用硬件。在这些环境中可用的选择,从专注于深度学习的 Paperspace 的 Gradient 环境到 AWS 的全覆盖范围,硬件选项和性价比都在逐年提高。深度学习不再是只有那些在科技巨头或资金充足的学术机构工作的幸运儿才能享有的专属技术,这些机构能够负担得起用于深度学习的专用本地硬件。
-
稳定的、可用的深度学习库 ——自从 2015 年初 Keras 的首次发布以来,通过直观且易于学习的界面,人们已经可以利用深度学习的力量。2016 年末 PyTorch(
pytorch.org)的发布和 2019 年中 TensorFlow 2.0 的推出,为开发者提供了更多可用的深度学习选择。 -
大型、开放的数据集 —— 在过去十年中,大量适合深度学习的大数据集如雨后春笋般涌现。2000 年代社交媒体和智能手机的出现、政府提供开放数据集的倡议以及谷歌的数据集搜索(
toolbox.google.com/datasetsearch)共同使得大型、有趣的数据集变得可访问。为了补充这股数据洪流,Kaggle(www.kaggle.com)等网站提供了一个想要利用这些数据集的机器学习研究者社区。 -
高质量的深度学习教育资源 —— 这包括一些优秀的自学在线课程。自 2017 年以来,deeplearning.ai 和 fast.ai 等课程使得非专业人士能够学习深度学习,尤其是在 fast.ai 的情况下,可以在真实的编码示例中练习所学知识。这些课程意味着你不需要获得人工智能的硕士学位来学习深度学习。
2.14 深度学习模型训练初体验
在接下来的六章中,我们将逐步分析扩展示例中的代码,从第三章的数据清理开始,到第八章部署训练好的模型结束。如果你把扩展示例看作是一部动作片,那么第六章中训练深度学习模型的部分就是高潮部分。为了让你提前感受一下高潮,在本节中,我们将通过一个简化的场景来训练深度学习模型。
如果你克隆了第 2.10 节中描述的仓库,你将看到以下文件,我们将在本节中使用这些文件:
-
在 notebooks 目录中,名为 streetcar_model_training.ipynb 的 notebook 包含了定义和训练深度学习模型的代码。配置文件 streetcar_model_training_config.yml 允许你设置模型训练过程的参数。
-
在 data 目录中,你可以找到一个现成的、清理过的数据集文件,名为 2014_2019_df_cleaned_remove_bad_values_may16_2020.pkl。你可以使用这个文件作为模型训练 notebook 的输入,因此你不需要为这个场景运行数据集清理代码。如果训练好的深度学习模型是一顿美食,那么这个清理过的数据集文件就可以看作是一份已经清洗和切好的食材,准备烹饪。当你阅读第三章和第四章时,你需要自己清洗和切片食材,但在这个部分,准备工作已经为你完成,你只需将所有食材放入烤箱即可。
下面是进行深度学习模型简单训练运行的步骤:
-
将 streetcar_model_training_config.yml 文件更新为设置以下列表中的参数。
列表 2.6 简单训练运行中需要更新的配置文件项
modifier: 'initial_walkthrough_2020' ❶ pickled_dataframe: '2014_2019_df_cleaned_remove_bad_values_may16_2020.pkl' ❷ current_experiment: 9 ❸ get_test_train_acc: False ❹❶ 生成输出管道和训练模型文件唯一名称的修饰符
❷ 模型训练过程输入文件名
❸ 预设实验,指定训练运行的特性,包括通过训练集进行的运行次数、训练是否考虑训练集的不平衡性,以及模型不再改进时是否提前停止训练。
❹ 切换是否运行测试和训练准确率的扩展计算。这段代码运行可能需要很长时间,因此在这个初步训练运行中,我们将此开关设置为 False。
-
在您为示例选择的环境中打开模型训练笔记本,并运行笔记本中的所有单元格。
-
当笔记本运行完成后,请检查模型子目录中名为 scmodelinitial_walkthrough_2020_9.h5 的文件。此文件是训练过程中学习到的所有权重的训练模型。
您已成功训练了一个深度学习模型。如果您愿意,可以按照第八章的说明创建一个简单的网站,该网站将调用此模型来预测给定的电车行程是否会延误。但尽管这个模型是一个功能完整的训练好的深度学习模型,其性能并不是最好的。如果您查看笔记本的末尾,您将看到一个称为混淆矩阵的彩色框,它总结了您在这个训练运行中获得的好模型的程度。混淆矩阵看起来类似于图 2.21。

图 2.21 示例混淆矩阵
左上角和右下角象限显示了模型在测试集(即未用于训练过程的 dataset 的子集)上做出的正确预测数量,而右上角和左下角象限显示了模型预测错误的数量。第六章提供了对混淆矩阵的详细描述以及如何解释它,以及构成训练实验的参数的详细信息。现在,只需注意混淆矩阵的最后一行显示,大约 40%的时间,模型在发生延误时预测没有延误。正如您将在第六章中看到的,这种结果对我们用户来说是最糟糕的,因此它如此频繁地发生并不好。您能做些什么来获得更好的深度学习模型?第 2.8 节中描述的原始输入数据集与用于此训练运行的清理后的数据集有什么区别?现在您已经尝试过训练深度学习模型,请参阅第 3-7 章来回答这些问题,然后参阅第八章来使您的训练好的深度学习模型可供外界使用。
摘要
-
在深度学习项目中,有两个基本决策:使用什么环境和解决什么问题。
-
你可以选择你的本地系统来运行深度学习项目,或者你可以选择一个功能齐全的云环境,如 Azure 或 AWS。介于两者之间的是专门为深度学习设计的环境,包括 Paperspace 和 Google Colab。
-
Pandas 是用于处理表格数据集的标准 Python 库。如果你熟悉 SQL,你会发现 Pandas 可以方便地完成你习惯用 SQL 做的事情。
-
将深度学习应用于结构化数据最大的反对意见之一是深度学习过于复杂。得益于针对深度学习开发的易用环境、更好的深度学习框架以及面向非专业人士的深度学习教育,这种反对意见不像五年前那样相关了。
-
本书中的主要示例代码设计得如此,你可以运行它的子集而不必运行所有前面的步骤。例如,你可以直接运行深度学习模型训练来体验训练过程。
3 准备数据(第一部分):探索和清洗数据
本章涵盖
-
在 Python 中使用配置文件
-
将 XLS 文件导入 Pandas 数据框并使用 pickle 保存数据框
-
探索输入数据集
-
将数据分类为连续型、分类型和文本型
-
纠正数据集中的缺失值和错误
-
计算成功深度学习项目的数据量
在本章中,你将学习如何将表格结构化数据从 XLS 文件导入到你的 Python 程序中,以及如何使用 Python 中的 pickle 功能在 Python 会话之间保存你的数据结构。你将学习如何将结构化数据分类为深度学习模型所需的三个类别:连续型、分类型和文本型。你将学习如何检测和处理数据集中必须纠正的缺失值和错误,以便在训练深度学习模型之前可以使用。最后,你将获得一些关于如何评估给定的数据集是否足够大,可以应用于深度学习的指导。
3.1 探索和清洗数据的代码
在你克隆了与本书相关的 GitHub 仓库(mng.bz/v95x)之后,探索和清洗数据的相关代码将位于 notebooks 子目录中。下面的列表显示了包含本章描述的代码的文件。
列表 3.1 仓库中与探索和清洗数据相关的代码
├── data ❶
│
├── notebooks
│ streetcar_data_exploration.ipynb ❷
│ streetcar_data_preparation.ipynb ❸
│ streetcar_data_preparation_config.yml ❹
│ streetcar_time_series.ipynb ❺
❶ 用于 pickled 输入和输出数据框的目录
❷ 主要数据探索笔记本
❸ 数据准备笔记本
❹ 数据准备笔记本的配置文件:是否从头开始加载数据,保存转换后的输出数据框,以及删除坏值以及 pickled 输入和输出数据框的文件名
❺ 使用时间序列方法进行数据探索的笔记本
3.2 使用 Python 配置文件
清洗数据集的主要代码包含在 streetcar_data_preparation.ipynb 笔记本中。这个笔记本有一个配套的配置文件,即 streetcar_data_preparation_config.yml,用于设置主要参数。在探索数据准备代码之前,让我们看看这个配置文件和其他配置文件在电车延误预测示例中的应用。
配置文件 是位于你的 Python 程序之外的外部文件,你可以在其中设置参数值。你的 Python 程序读取配置文件,并使用其中的值来设置 Python 程序中的参数。使用配置文件,你可以将设置参数值与 Python 代码分离,这样你就可以更新参数值并重新运行你的代码,而无需对代码本身进行更新。这种技术可以降低在更新参数值时将错误引入代码的风险,并使你的代码更整洁。对于电车延误预测示例,我们为所有主要的 Python 程序都提供了配置文件,如图 3.1 所示。

图 3.1 电车延误预测示例中使用的配置文件摘要
您可以为您的 Python 程序定义 JSON (www.json.org/json-en.html) 或 YAML (yaml.org) 格式的配置文件。对于本书中的电车延误预测示例,我们使用 YAML。列表 3.2 显示了 streetcar_data_preparation 笔记本使用的配置文件,streetcar_data_preparation_config.yml。
列表 3.2 数据准备配置文件 streetcar_data_preparation_config.yml
general: ❶
load_from_scratch: False ❷
save_transformed_dataframe: False ❸
remove_bad_values: True ❹
file_names:
pickled_input_dataframe: 2014_2019_upto_june.pkl ❺
pickled_output_dataframe: 2014_2019_df_cleaned_remove_bad_oct18.pkl ❻
❶ 您可以使用类别来组织您的配置文件。此配置文件有一个用于一般参数的类别,另一个用于文件名。
❷ 用于控制是否直接读取原始 XLS 文件的参数。如果此参数为 True,则从原始 XLS 文件中读取原始数据集。如果此参数为 False,则从 pickle 数据框中读取数据集。
❸ 用于控制是否将输出数据框保存到 pickle 文件的参数
❹ 用于控制是否将坏值包含在输出数据框中的参数
❺ 如果 load_from_scratch 设置为 False,则读取的 pickle 数据框的文件名
❻ 如果 save_transformed_dataframe 为 True,则写入的 pickle 数据框的文件名
下一个列表是 streetcar_data_preparation 笔记本中读取配置文件并根据配置文件中的值设置参数的代码。
列表 3.3 数据准备笔记本中处理配置文件的代码
current_path = os.getcwd() ❶
path_to_yaml = os.path.join(current_path,
'streetcar_data_preparation_config.yml') ❷
try:
with open (path_to_yaml, 'r') as c_file:
config = yaml.safe_load(c_file) ❸
except Exception as e:
print('Error reading the config file')
load_from_scratch = config['general']['load_from_scratch'] ❹
save_transformed_dataframe = config['general']['save_transformed_dataframe']
remove_bad_values = config['general']['remove_bad_values']
pickled_input_dataframe = config['file_names']['pickled_input_dataframe']
pickled_output_dataframe = config['file_names']['pickled_output_dataframe']
❶ 获取笔记本的路径。
❷ 定义完全合格的配置文件路径。请注意,配置文件的名称是需要在 Python 代码中硬编码的一个参数,尽管这不应该是一个问题,因为配置文件的名称不应该改变。另外请注意,我们使用 os.path.join 将目录和文件名组合成一个单独的路径。我们使用此函数是因为它使路径名与平台无关。
❸ 定义包含配置文件中键/值对的 Python 字典 config。
❹ 将配置字典中的值复制到程序其余部分使用的变量中。
在本节中,您已经看到了为什么我们在电车延误预测示例中使用配置文件,以及数据准备代码配置文件详细描述。对于这个例子来说,配置文件特别有用,因为我们使用 pickle 文件来保存中间结果。通过在配置文件中设置这些 pickle 文件名的值,我们可以在不同的中间结果集上重新运行代码,而无需修改代码本身。
3.3 将 XLS 文件导入 Pandas 数据框
在第二章中,我们检查了电车延误问题的输入数据集的格式。在本节中,我们将介绍如何在 Python 中将此数据集导入 Pandas 数据框。输入数据集由多个 XLS 文件组成。首先,让我们按照本章笔记本中的过程将单个 XLS 文件导入到 Pandas 数据框中。
首先,你需要安装一个库来读取 Excel 文件:
!pip install xlrd
然后,你需要获取 XLS 文件的元数据(标签名称)并遍历标签名称列表,将所有标签加载到一个数据框中,如以下列表所示。
列表 3.4 遍历 XLS 文件标签的代码
def get_path(): ❶
rawpath = os.getcwd()
path = os.path.abspath(os.path.join(rawpath, '..', 'data'))
return(path)
import pandas as pd ❷
path = get_path()
file = "ttc-streetcar-delay-data-2014.xlsx" ❸
xlsf = pd.ExcelFile(os.path.join(path,file)) ❹
df = pd.read_excel(os.path.join(path,file),sheet_name=xlsf.sheet_names[0])
for sheet_name in xlsf.sheet_names[1:]: ❺
print("sheet_name",sheet_name)
data = pd.read_excel(os.path.join(path,file),sheet_name=sheet_name) ❻
df = df.append(data) ❼
❶ 返回数据目录路径的函数
❷ 导入 pandas 库。
❸ 定义基本路径和文件名。
❹ 加载 Excel 文件的相关元数据;然后加载 XLS 文件的第一个工作表到数据框中。
❺ 遍历 XLS 文件中剩余的工作表,并将它们的内 容追加到数据框中。
❻ 将当前工作表加载到数据框中。
❼ 将此数据框追加到聚合数据框中。
输出显示从第二个标签开始显示标签名称(因为第一个名称在for循环之前加载):
sheet_name Feb 2014
sheet_name Mar 2014
sheet_name Apr 2014
sheet_name May 2014
sheet_name Jun 2014
sheet_name July 2014
sheet_name Aug 2014
sheet_name Sept 2014
sheet_name Oct 2014
sheet_name Nov 2014
sheet_name Dec 2014
从输入的 XLS 文件创建数据框后,你会在数据框的head ()输出中注意到一些意外的列(如图 3.2 所示)。

图 3.2 加载的数据框中包含多余列
除了预期的最小延迟和最小间隔列之外,还有意外的延迟和间隔列,以及一个意外的事件 ID 列。实际上,源数据集在 2019 XLS 文件的 4 月和 6 月标签中引入了一些异常,如图 3.3 所示。2019 XLS 文件的 4 月标签有称为延迟和间隔的列(而不是所有其他标签中的最小延迟和最小间隔),以及一个事件 ID 列。2019 XLS 文件的 6 月标签有一个不同的问题:它没有最小延迟和最小间隔列,而是有称为延迟和间隔的列。


图 3.3 2019 XLS 文件 4 月和 6 月标签中的异常
由于这些异常列出现在 2019 XLS 文件的两个标签中,如果你读取包括 2019 年 XLS 文件的完整数据集,那么整体数据框也会包含这些列。数据准备笔记本包括以下列表中的代码,通过复制所需数据然后删除多余的列来纠正问题。
列表 3.5 修正异常列的代码
def fix_anomalous_columns(df): ❶
df['Min Delay'].fillna(df['Delay'], inplace=True)
df['Min Gap'].fillna(df['Gap'], inplace=True)
del df['Delay'] ❷
del df['Gap']
del df['Incident ID'] ❸
return(df) ❹
❶ 如果最小延迟或最小间隔列中存在 NaN,则从延迟或间隔中复制值。
❷ 现在已经从延迟和间隔中复制了有用的值,删除延迟和间隔列。
❸ 删除事件 ID 列;它是多余的。
❹ 返回更新后的数据框。
在此清理之后,head 函数的输出确认了异常列已被消除,并且数据框具有预期的列(见图 3.4)。

图 3.4 包含输入 XLS 文件所有标签页的数据框的开始部分
我们检查 tail 函数的输出以确认数据框的末尾也符合预期(见图 3.5)。

图 3.5 包含输入 XLS 文件所有标签页的数据框的结束部分
从源电车数据集中的这些异常中可以学到重要的一课。在使用实时、真实世界的数据集时,我们需要准备好灵活应对数据集的变化。在我撰写这本书的时候,本节中描述的异常被引入到数据集中,因此我需要准备好更新数据准备代码以解决这些异常。当你处理一个你无法控制的数据集时,你需要能够从容应对数据集中的意外变化。
现在你已经知道如何加载单个 XLS 文件,让我们通过将来自多个 XLS 文件的数据引入单个 Pandas 数据框来了解如何导入整个输入数据集。
本节中的代码示例来自 streetcar_data_preparation 笔记本。该笔记本假设你已经将所有 XLS 文件从原始输入数据集(mng.bz/ry6y)复制到了名为 data 的目录中,该目录是包含笔记本的目录的兄弟目录。
街车数据准备笔记本中的代码使用两个函数将多个 XLS 文件导入到单个数据框中。reloader 函数通过将第一个文件的第一个标签页加载到数据框中启动这个过程,然后调用 load_xls 函数来加载第一个文件的其余标签页以及所有其他 XLS 文件的所有标签页。下一列表中的代码假设数据目录中的 XLS 文件正好是构成数据集的 XLS 文件。
列表 3.6 导入 XLS 文件的代码
def load_xls(path, files_xls, firstfile, firstsheet, df): ❶
'''
load all the tabs of all the XLS files in a list of XLS files, minus
tab that has seeded dataframe
Parameters:
path: directory containing the XLS files
files_xls: list of XLS files
firstfile: file whose first tab has been preloaded
firstsheet: first tab of the file that has been preloaded
df: Pandas dataframe that has been preloaded with the first tab
of the first XLS file and is loaded with all the data
when the function returns
Returns:
df: updated dataframe
'''
for f in files_xls: ❷
print("filename",f)
xlsf = pd.ExcelFile(path+f)
for sheet_name in xlsf.sheet_names: ❸
print("sheet_name",sheet_name)
if (f != firstfile) or (sheet_name != firstsheet):
print("sheet_name in loop",sheet_name)
data = pd.read_excel(path+f,sheetname=sheet_name)
df = df.append(data) ❹
return (df)
❶ 将列表中所有 XLS 文件的所有标签页加载到一个数据框中,但不包括包含已播种数据框的标签页。
❷ 遍历目录中的所有 XLS 文件。
❸ 遍历当前 XLS 文件中的所有标签页。
❹ 将当前工作表的 data frame 追加到整体数据框中。
下面的列表代码展示了 reloader 函数,该函数调用 load_xls 函数来导入所有 XLS 文件,并将结果保存为 pickle 格式的数据框。
列表 3.7 导入多个 XLS 文件并将结果保存为 pickle 格式数据框的代码
def reloader(path,picklename): ❶
files_xls = get_xls_list(path) ❷
print("list of xls",files_xls)
dfnew = pd.read_excel(path+files_xls[0]) ❸
xlsf = pd.ExcelFile(path+files_xls[0]) ❹
dflatest = load_xls(path,files_xls,files_xls[0], \ xlsf.sheet_names[0], dfnew) ❺
dflatest.to_pickle(os.path.join(path,picklename)) ❻
return(dflatest) ❼
❶ 给定一个路径和一个文件名,将路径中的所有 XLS 文件加载到一个数据框中。
❷ 获取路径中所有 XLS 文件的列表。
❸ 在初始 XLS 文件的初始标签页上播种。
❹ 获取第一个文件中所有标签页的列表。
❺ 从所有其他 XLS 文件中加载剩余的标签页。
❻ 将数据框保存到 pickle 文件中。
❼ 返回加载了所有 XLS 文件所有标签的数据框。
你如何获取正确的路径值,即你复制了构成数据集的 XLS 文件的目录?代码假设所有数据都存在于一个名为 data 的目录中,这个目录是这个笔记本所在目录的兄弟目录。接下来的列表是第二章中引入的代码片段,它获取包含 XLS 文件的目录的正确路径;它获取当前目录(笔记本所在的位置)以及这个目录兄弟目录 data 的路径。
列表 3.8 获取数据目录路径的代码
rawpath = os.getcwd() ❶
print("raw path is",rawpath)
path = os.path.abspath(os.path.join(rawpath, '..', 'data')) ❷
print("path is", path)
❶ 获取这个笔记本所在的目录。
❷ 获取与这个笔记本所在的目录同级的目录 data 的完整合格路径。
3.4 使用 pickle 在一个会话到另一个会话之间保存你的 Pandas 数据框
Pandas 数据框存在于你的笔记本会话期间。这个考虑因素非常重要,尤其是当你使用 Paperspace 这样的云环境时。当你关闭笔记本(无论是明确关闭还是通过关闭云会话)时,你将丢失在会话期间创建的数据框。下次你想做更多的工作时,你必须从头开始重新加载数据。如果你想让 Pandas 数据框在会话之间持续存在,以避免每次都必须从其源重新加载数据,或者如果你想在两个笔记本之间共享数据框,你该怎么办?
对于适度大小的数据集,保持数据框超过会话生命周期的答案是 pickle。这个极其有用的标准 Python 库允许你将你的 Python 对象(包括 Pandas 数据框)保存为文件系统中的文件,你可以在以后将其读回到 Python 中。在详细介绍如何使用 pickle 之前,我必须承认并不是每个人都喜欢 pickle。例如,Ben Frederickson 认为 pickle 的效率不如 JSON 这样的序列化替代方案,如果解包来源不明的文件,可能会暴露你于安全风险(www.benfrederickson.com/dont-pickle-your-data)。此外,pickle 也不是每个用例的正确选择。如果你需要在编程语言之间共享序列化对象(pickle 仅适用于 Python),例如,或在 Python 的不同级别之间,那么它是不推荐的。如果你在一个 Python 级别中 pickle 一个对象,然后尝试将其带入运行在不同 Python 级别的另一段代码中,你可能会遇到问题。对于本书中描述的示例目的,我坚持使用 pickle,因为它简化了序列化过程,并且所有 pickle 文件的来源都是已知的。
假设你想要使用公开可用的 Iris 数据集,而不是将其复制到你的文件系统中,但你正在一个网络连接不可靠的环境中工作。为了这个例子,假设你正在使用本地安装的机器学习框架,例如在你的本地系统中安装的 Jupyter Notebooks,这样你就可以在离线时工作在 notebooks 上。你希望在连接到互联网时能够加载 dataframe,然后将 dataframe 保存到你的文件系统中,这样你就可以在离线时重新加载 dataframe 并继续工作。
首先,你将 Iris 数据集加载到 dataframe 中,就像你在第二章中做的那样(列表 3.9),如下所示。
列表 3.9 使用 URL 引用导入 CSV 文件的代码
url=”https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/\
➥ raw/d546eaee765268bf2f487608c537c05e22e4b221/iris.csv” ❶
iris_dataframe=pd.read_csv(url) ❷
❶ Iris 数据集的原始 GitHub URL
❷ 将 URL 的内容读取到 Pandas dataframe 中。
接下来,你调用to_pickle()方法将 dataframe 保存到你的文件系统中的一个文件中,如下所示。按照惯例,pickle 文件的扩展名为pkl。
列表 3.10 保存 dataframe 为 pickle 文件的代码
file = "iris_dataframe.pkl" ❶
iris_dataframe.to_pickle(os.path.join(path,file)) ❷
❶ 为 pickle 文件定义一个文件名。
❷ 将 dataframe 写入命名的 pickle 文件。
现在你正在没有互联网连接的航班上,并想继续使用这个数据集,你所要做的就是调用read_pickle方法,并将你保存的 pickle 文件作为参数,如下所示。
列表 3.11 将 pickle 文件读取到 dataframe 中的代码
file = "iris_dataframe.pkl"
iris_dataframe_from_pickle = pd.read_pickle(os.path.join(path,file)) ❶
iris_dataframe_from_pickle.head()
❶ 调用 read_pickle 函数将 pickle 文件读取到 dataframe 中。
head()函数的输出显示,你已将数据重新加载到 dataframe 中,而无需回到原始数据源(图 3.6)。

图 3.6 将保存为 pickle 文件的 dataframe 反序列化的结果
图 3.7 从原始源数据集(CSV 文件)到 Pandas dataframe,再到 pickle 文件,最后回到 Pandas dataframe 的流程总结。

图 3.7 数据集的生命周期
如果你有大量数据集,且将其从外部源加载到 dataframe 中需要一些时间,那么 pickle 操作将非常有用。对于大型数据集,从保存的 dataframe 中反序列化通常比重新从外部源加载数据更快。
3.5 探索数据
现在我们已经学习了如何将完整输入数据集导入到 Pandas dataframe 中,以及如何使数据集在会话之间持久化,我们需要探索数据以了解其特征。通过使用 Python 中可用的数据可视化工具,我们可以探索数据以找到模式和异常,这有助于我们为下游过程做出良好的选择。你可以在这个部分的代码在 streetcar_data_exploration 笔记本和 streetcar _time_series 笔记本中找到。
首先,让我们对原始 dataframe 使用describe()函数(见图 3.8)。

图 3.8 describe()函数的输出
这里有一些需要注意的事项:
-
路线和车辆被解释为连续的;我们需要纠正这两个列的类型。
-
最大延迟为 23 小时,最大间隔为 72 小时。这两个值看起来都不正确。我们需要检查记录以确认它们是否不正确。
-
平均延迟时间为 12 分钟;平均间隔时间为 18 分钟。
通过使用sample()函数随机抽样数据集,我们得到图 3.9 所示的输出。

图 3.9 对原始输入数据框的 sample()函数的输出
这个输出告诉我们什么?
-
一些事件有零长度的延迟和间隔,这与预期记录延迟的数据集不符。我们需要审查这些记录以确定零长度的延迟和间隔是故意的还是错误。
-
位置值没有一致的连接词:
at、and和&都出现在这个随机样本中 -
事件可能是一个分类列。我们应该计算唯一值的数量,以确定是否可以将事件视为一个分类列。
如果我们查看事件唯一值的数量,我们会发现这个数字足够小,使得事件可以被视为一个分类列:
print("incident count",df['Incident'].nunique())
incident count 9
这证实了事件应该被视为一个分类列,而不是一个自由文本列。
让我们在下一个列表中通过计算给定事件中一个值比另一个值大多少来探索最小延迟和最小间隔的相对大小。
列表 3.12 计算最小延迟比最小间隔大或小的次数的代码
df[df['Min Gap'] > df['Min Delay']].shape ❶
(65176, 10) ❷
df[df['Min Gap'] < df['Min Delay']].shape ❸
(1969, 10) ❹
❶ 获取最小间隔大于最小延迟的记录数量。
❷ 这些记录的数量
❸ 获取最小间隔小于最小延迟的记录数量。
❹ 这些记录的数量
结果告诉我们,对于给定的事件,最小间隔通常比最小延迟长,但大约 3%的时间,最小延迟比最小间隔长。这不是我们预期的。我们需要审查这些最小延迟比最小间隔长的记录,以确定它们是否是错误。
接下来,让我们看看按月聚类事件数量的视图(见图 3.10)。你可以在这个部分的 streetcar_time_series 笔记本中找到生成图表的代码。在这个视图中,每个点代表给定年份一个月内的事件总数。点的紧密垂直簇意味着事件数量在年份间变化不大。

图 3.10 按月延迟事件
下一个列表显示了生成此图表的代码。
列表 3.13 生成按月图表的延迟事件的代码
dfmonthav.plot.scatter(x = 'Month', y = 'Delay Count') ❶
plt.show() ❷
❶ 在 x 轴上按年月绘制,在 y 轴上绘制延迟计数
❷ 渲染图表。
这个视图告诉我们什么?
-
三月、四月、九月和十月(或许还有七月)的事件数量在年份间的变化不如其他月份。
-
一月、二月和十二月拥有最高的最高值范围。
我们能从这些观察结果中得出任何结论吗?或许在温度极端的月份会有更多事件发生。或许在天气不可预测的月份,事件的数量变化更大。这两个结论都是合理的,但都不确定。我们需要让数据来驱动结论。有可能天气是导致延误数量的一个影响因素,但我们需要小心不要在没有支持数据的情况下归因。在第九章中,我们讨论了将天气作为额外数据源添加到电车预测模型中。按月延迟图表表明这可能是一项有用的练习。
现在我们来看延迟持续时间的滚动平均值。此图表上每个月的数据点是前六个月延迟持续时间的平均值(见图 3.11)。

图 3.11 滚动平均延迟持续时间
下一个列表是生成此图表的代码。
列表 3.14 生成滚动平均延迟持续时间图表的代码
mean_delay = dfmonthav[['Min Delay']]
mean_delay.rolling(6).mean().plot(figsize=(20,10), linewidth=5, fontsize=20)❶
plt.show() ❷
❶ 以月份为数据点绘制延迟持续时间的六个月滚动平均值。
❷ 渲染图表。
图 3.11 告诉我们,延迟事件的整体趋势是缩短,但在 2019 年有所上升。
Python 提供了许多探索数据集的选项。本节展示了这些选项的有用子集,以及探索结果后可能采取的行动。
下一个列表是图 3.12 中图表的代码。
列表 3.15 生成滚动平均延迟计数图表的代码
count_delay = dfmonthav[['Delay Count']]
count_delay.rolling(6).mean().plot(figsize=(20,10), \
linewidth=5, fontsize=20) ❶
plt.show() ❷
❶ 以月份为数据点绘制延迟计数的六个月滚动平均值。
❷ 渲染图表。

图 3.12 滚动平均延迟计数
图 3.12 表明,延迟计数的趋势是增加的,与延迟持续时间的趋势相反。
3.6 将数据分类为连续、分类和文本类别
现在你已经探索了数据,是时候解决如何对数据集中的列进行分类的问题了。本书中描述的方法是基于将输入列分为三个类别:
-
连续 —这些值是数值,并且可以对这些值进行算术运算。连续值的例子包括温度、货币价值、时间跨度(如已过小时数)和对象和活动的计数。
-
分类 —这些值可以是单个字符串,例如一周中的某一天,或者构成标识符的一组一个或多个字符串,例如美国各州的名称。分类列中不同值的数量可以从两个到几千个不等。
-
文本 —这些值是字符串集合。
这种分类对于两个原因至关重要:
-
与其他机器学习算法一样,深度学习算法在数值上工作。最终输入到深度学习模型中的数据流需要完全由数值组成,因此所有非数值都需要转换成数值。分类告诉我们是否需要为一个列进行这种转换,如果是的话,需要什么样的转换。
-
正如你在第五章中将会看到的,深度学习模型的层是根据输入列的分类自动构建的。每种类型的列(连续型、分类型和文本型)都会生成具有不同特征的层。通过分类输入列,你使得 streetcar_model_training 笔记本中的代码能够自动构建深度学习模型,这意味着如果你向你的数据集中添加列或从数据集中删除列,当你重新运行笔记本时,模型将自动更新。
图 3.13 展示了如何对输入的街车延误数据集的列进行分类:

图 3.13 本书中主要示例的输入数据集列的分类
这里是每个列分类的描述:
-
最小延误和最小间隔是已过时间的测量,因此是连续列。
-
事件是一个分类列,描述了导致服务中断的原因。有九个有效值。
-
路线是一个分类列,有 12 个有效值。
-
日期是一个具有七个有效值的分类列。
-
位置是一个分类列,尽管在数据输入时它是一个自由格式字段。这个列是一个需要预处理以将大量潜在值映射到更小的一组唯一值的例子。在第四章中,我们将讨论将这些位置字符串映射到严格定义的经纬度值的优势和劣势。
-
方向是一个具有五个有效值的分类列:四个方向和表示多个方向的一个值。
-
车辆是一个分类列,尽管当这些值最初被引入到 dataframe 中时,它们看起来像浮点值。这个列中的值实际上是大约 300 辆活跃街车的四字符标识符。在第四章中,我们将处理 Python 分配给这个列的类型与实际数据类型之间的不匹配问题。
那么时间列“报告日期和时间”呢?在第五章中,我们将描述一种方法,将这些列中的值解析成新的分类列,以识别事件中最有趣的时序方面:年份、月份、月份中的日期和小时。
注意,在某些情况下,一列可能合法地属于多个类别。例如,包含时间戳的列可以根据你的业务问题的需求被处理为连续或分类。好消息是,本书中描述的方法足够灵活,你可以改变对某一列类别的看法,并以最小的干扰重新训练模型。
3.7 清理数据集中的问题:缺失数据、错误和猜测
电车延误问题是应用深度学习到表格结构数据的良好示例,因为输入数据集很混乱,有很多缺失、无效和多余的值。我们希望用深度学习解决的问题涉及这些类型的混乱数据集,因此清理混乱数据是我们需要学会如何做的一件事,如果我们想利用深度学习来解决使用表格结构数据解决的实际问题。
我们需要清理数据集中的这些问题,原因有很多:
-
缺失值需要被处理,因为深度学习模型不能在包含缺失值的数据集上进行训练。
-
无效值需要被处理,因为正如你将在第七章中看到的,在无效值仍然存在的情况下训练深度学习模型会降低训练模型的性能。
-
需要处理这些额外的值——对于同一现实世界特征的多个不同标记(例如,
E/B,e/b,和eb代表 向东行驶)——因为这些值在后续过程中的携带会增加模型的复杂性,而不会在训练过程中提供任何额外的信号。
以下是在清理过程完成后我们希望数据集具有的特征:
-
所有值都是数值型 . 机器学习算法依赖于所有数据都是数值型。缺失值需要被替换,非数值型值(在分类或文本列中)需要被数值标识符替换。
-
包含无效值的记录 将被识别并消除。消除无效值(例如,位于电车网络地理区域之外的位置或无效的电车 ID)的原因是为了防止模型在训练过程中使用不反映现实世界问题的数据进行训练。
-
多余的类别 将被识别并消除。我们知道,方向列应该只有五个有效值(方向指示符加上表示两个方向的标识符)。所有记录都需要使用相同的统一类别。
首先,让我们看看每个列中的缺失值。深度学习模型只处理数字作为输入。用于训练深度学习模型的所有值都必须是数字。原因是用于训练深度学习模型的数据需要反复进行第一章中描述的过程:乘以权重、添加偏差值以及应用激活函数。这些操作不能与缺失值、字符值或任何不是数字的东西一起工作,因此处理缺失值是清理输入数据集的基本部分。
你如何知道哪些列有缺失值以及每个列中有多少行缺失值?这里有一个简单的命令,其输出是每个列的缺失值数量:
df.isnull().sum(axis = 0)
Report Date 0
Route 0
Time 0
Day 0
Location 270
Incident 0
Min Delay 58
Min Gap 77
Direction 232
Vehicle 0
输出告诉我们,位置、最小延迟、最小间隔和方向都有需要处理的缺失值。fill_missing函数遍历数据框中的所有列,并根据列类别用占位符值替换空值,如下所示。
列表 3.16 用占位符值替换缺失值的代码
def fill_missing(dataset): ❶
print("before mv")
for col in collist:
dataset[col].fillna(value="missing", inplace=True)
for col in continuouscols:
dataset[col].fillna(value=0.0,inplace=True) ❷
for col in textcols:
dataset[col].fillna(value="missing", inplace=True)
return (dataset)
❶ 根据列类别填充缺失值。
❷ 我们将用零填充连续列中的缺失值。请注意,对于某些列,该列值的平均值可以作为缺失值的有效替代。
如果我们用加载输入数据集的数据框作为参数调用此函数
df = fill_missing(df)
然后重新运行命令来计数空值,我们可以看到所有缺失值都已处理:
df.isnull().sum(axis = 0)
Report Date 0
Route 0
Time 0
Day 0
Location 0
Incident 0
Min Delay 0
Min Gap 0
Direction 0
Vehicle 0
现在我们已经修复了缺失值,让我们深入探讨输入数据集的一个列的剩余清理操作:方向。
方向列表示在给定记录中事故影响了哪种交通方向。根据随数据集附带的 readme 文件,方向有七个有效值:
-
对于影响双向交通的事故,使用
B、b或BW。当我们清理此列的值时,我们将使用单个值来表示两个方向。 -
对于影响单方向交通的事故(北行、南行、东行或西行),使用
NB、SB、EB和WB。
现在,让我们看看在已加载输入数据的 DataFrame 中方向列的实际唯一值数量:
print("unique directions before cleanup:",df['Direction'].nunique())
unique directions before cleanup: 95
发生了什么?为什么一个只有七个合法值的列会有 95 个不同的值?让我们看看value_counts的输出,以了解所有这些唯一值来自哪里:
df['Direction'].value_counts()
W/B 32466
E/B 32343
N/B 6006
B/W 5747
S/B 5679
missing 232
EB 213
eb 173
WB 149
wb 120
SB 20
nb 18
NB 18
sb 14
EW 13
eastbound 8
bw 7
5 7
w/b 7
w 6
BW 4
8 4
ew 4
E 4
w/B 4
s 4
2 4
W 3
b/w 3
10 3
...
e/w 1
如您所见,方向列中的 95 个值来自冗余标记和错误的组合。为了纠正这些问题,我们需要
-
获取该列值的统一情况,以避免像
EB和eb这样的值被当作不同的值处理。 -
从该列的值中移除
/以避免像wb和w/b被视为不同值的问题。 -
进行以下替换以消除对指南针标记的冗余标记:
-
e代表eb和eastbound -
w代表wb和westbound -
n代表nb和northbound -
s代表sb和southbound -
b代表 bw
-
-
将所有剩余的标记(包括我们为缺失值插入的
missing标记)替换为单个标记,bad direction,以表示无法映射到任何有效方向的价值。
我们在下一列表中应用了 direction_cleanup 函数到方向列以实现这些更改。
列表 3.17 清理方向列的代码
def check_direction (x): ❶
if x in valid_directions:
return(x)
else:
return("bad direction")
def direction_cleanup(df): ❷
print("Direction count pre cleanup",df['Direction'].nunique())
df['Direction'] = df['Direction'].str.lower() ❸
df['Direction'] = df['Direction'].str.replace('/','') ❹
df['Direction'] =
➥ df['Direction'].replace({'eastbound':'e','westbound':'w', \
➥ 'southbound':'s','northbound':'n'}) ❺
df['Direction'] = df['Direction'].replace('b','',regex=True) ❻
df['Direction'] = df['Direction'].apply(lambda x:check_direction(x)) ❼
print("Direction count post cleanup",df['Direction'].nunique())
return(df)
❶ 函数用于将无效的方向值替换为通用字符串
❷ 清理方向值的函数
❸ 将所有值转换为小写。
❹ 从所有值中移除 /。
❺ 将东向、西向等替换为单个字母方向标记。
❻ 从 eb 和 nb 等字符串中移除多余的 b。
❼ 调用 check_direction 将任何剩余的无效值替换为通用字符串。
输出显示了这些变化的效果:
Unique directions before cleanup: 95
Unique directions after cleanup: 6
这里是按方向列剩余的唯一值计数的:
w 32757
e 32747
n 6045
b 5763
s 5719
bad direction 334
注意,现在我们只有六个有效值,而不是说明中指定的七个。我们已经将三个“双向”标记——B、b和BW——合并为b,并为错误的方向值添加了一个新标记“bad direction”。这种清理对于第五章中描述的输入数据集重构至关重要,以获得每个路线/方向/时间段组合的记录。这种重构依赖于任何路线最多有五个方向值,多亏了方向列的清理,我们知道这个条件是成立的。
方向列只是需要清理的列之一。与其他列相比,方向列相对简单,因为它有较少的有效值。此外,将输入值转换为有效值集的操作相对简单。我们将在第四章中详细介绍其他列的更复杂的清理过程。
3.8 查找深度学习需要多少数据
如果您进行简单的搜索以找出训练深度学习模型所需的数据量,您将不会得到令人满意的结果。所有答案都是“这取决于”的各种变体。一些著名的深度学习模型是在数百万个示例上训练的。一般来说,非线性方法如深度学习需要比线性方法如线性回归更多的数据才能达到适当的结果。与其他机器学习方法一样,深度学习需要一个训练数据集,该数据集涵盖了模型部署时可能遇到的输入组合。在街车延误预测问题的情况下,我们需要确保训练集包括用户可能希望预测的所有街车路线的记录。例如,如果系统添加了新的街车路线,我们就需要使用包含该路线延误信息的数据集重新训练模型。
对于我们来说,真正的问题不是广泛的“深度学习模型需要多少数据才能充分训练”的问题,而是“街车数据集是否有足够的数据,以便我们可以将其应用于深度学习?”最终,答案取决于我们的模型表现如何,这个观察结果中有一个教训,即所需的数据量取决于训练模型的性能。如果您有一个包含数万个条目的数据集,如街车数据集,它既不是如此之小以至于深度学习不可行,也不是如此之大以至于数据量不会成为问题。确定深度学习是否适用于您的问题的唯一方法就是尝试它并查看其性能。正如第六章所述,对于街车延误预测模型,我们测量测试精度(即模型在训练过程中未看到的行程中预测延误的能力)以及其他与模型良好用户体验相关的测量。好消息是,完成这本书后,您将拥有评估深度学习在表格结构数据集上性能所需的所有工具。
我们将在第六章中更详细地讨论模型精度,但现在简要地反思一下精度问题。街车预测模型的精度需要有多好?对于这个问题,任何超过 70%的精度都会有所帮助,因为这将使乘客在大多数情况下能够避免长时间的延误。
摘要
-
深度学习项目的一个基本步骤是摄取原始数据集,以便您可以在代码中对其进行操作。当您摄取了数据后,您可以探索它并开始对其进行清理。
-
您可以使用配置文件与您的 Python 程序结合使用,以保持您的参数组织有序,并使更新参数变得容易,而无需修改 Python 代码。
-
您可以通过单个函数调用直接将 CSV 文件导入 Pandas 数据框。使用简单的 Python 函数,您还可以将 XLS 文件中的所有标签或甚至多个 XLS 文件中的所有标签导入到数据框中。
-
Python 的 pickle 工具是保存 Python 程序中对象的一种简单方法,这样您就可以在 Python 会话之间使用它们。如果您有一个对数据集执行一系列转换的 Python 程序,您可以 pickle 转换后的数据框,在另一个 Python 程序中读取 pickle 的数据框,然后继续在数据集上工作,而无需重新执行转换。
-
当您将数据集导入到数据框中后,Python 提供了许多方便的函数来探索数据集,以确定每列值的类型和范围,以及列中值的趋势。这种探索可以帮助您检测数据集中的异常,并避免对数据进行未经证实的假设。
-
当您清理数据集以准备训练深度学习模型时,您需要解决的问题包括缺失值、无效值以及由多个不同标记表示的值(例如
e、e/b和EB都表示eastbound)。 -
对于“您需要多少数据来训练深度学习模型?”这个问题,答案是“足够的数据来训练模型以满足您的性能标准。”在大多数涉及结构化数据的情况下,您至少需要数万条记录。
4 准备数据,第二部分:转换数据
本章涵盖
-
处理更多错误值
-
将复杂的多词值映射到单个标记
-
修复类型不匹配
-
处理清理后仍包含错误值的行
-
从现有列派生新的列
-
准备用于训练深度学习模型的分类和文本列
-
回顾第二章中引入的端到端解决方案
在第三章中,我们纠正了输入数据集中的一组错误和异常。数据集中仍有更多清理和准备工作要做,这就是本章我们将要做的。我们将处理剩余问题(包括多词标记和类型不匹配),并回顾你在所有清理后如何处理仍然存在的错误值的选择。然后,我们将回顾创建派生列以及如何准备非数值数据以训练深度学习模型。最后,我们将更仔细地查看第二章中引入的端到端解决方案,以了解我们已完成的数据准备步骤如何融入我们部署、训练用于预测电车延误的深度学习模型的总体旅程。
你将在本章中看到一致的主题:对数据集进行更新,使其更接近电车延误的现实世界情况。通过消除错误和歧义,使数据集更好地匹配现实世界,我们增加了获得准确深度学习模型的机会。
4.1 准备和转换数据的代码
当你克隆了与本书相关的 GitHub 仓库 (mng.bz/v95x) 后,探索和清洗数据的代码位于 notebooks 子目录中。下表展示了本章中描述的文件。
列表 4.1 仓库中与数据准备相关的代码
├── data ❶
│
├── notebooks
│ streetcar_data-geocode-get-boundaries.ipynb ❷
│ streetcar_data_preparation-geocode.ipynb ❸
│ streetcar_data_preparation.ipynb ❹
│ streetcar_data_preparation_config.yml ❺
❶ 用于存储和输出数据框的 pickled 输入目录
❷ 包含定义电车网络边界代码的笔记本(见 4.6 走得更远:位置)
❸ 生成与延迟位置相关的经纬度值的笔记本(见 4.6 走得更远:位置)
❹ 数据准备笔记本
❺ 数据准备笔记本的配置文件:是否从头开始加载数据,保存转换后的输出数据框,以及删除错误值以及 pickled 输入和输出数据框的文件名
4.2 处理错误值:路线
在第三章中,我们清理了方向列。正如你所回忆的,该列的有效值对应于罗盘方向,以及一个额外的标记来表示两个方向。方向列的有效值是通用的(北、东、南、西),并不特定于电车延误问题。那么,对于具有唯一电车使用案例值的列(路线和车辆)怎么办?我们如何清理这些列,以及我们能从这次清理中学到什么,这些经验可以应用到其他数据集中?
如果你查看数据准备笔记本的开始部分,你会注意到一个包含有效电车路线的单元格(图 4.1)。

图 4.1 有效电车路线
以下列表显示了数据准备笔记本中清理路线列值的代码。
列表 4.2 清理路线列值的代码
valid_routes = ['501','502','503','504','505','506','509', \
'510','511','512','301','304','306','310'] ❶
print("route count",df['Route'].nunique())
route count 106 ❷
def check_route (x): ❸
if x in valid_routes:
return(x)
else:
return("bad route")
df['Route'] = df['Route'].apply(lambda x:check_route(x)) ❹
print("route count post cleanup",df['Route'].nunique()) ❺
df['Route'].value_counts()
route count post cleanup 15
❶ 定义一个包含所有有效路线值的列表。
❷ 打印出路线列中所有唯一值的计数。
❸ 将不在有效值列表中的路线值替换为占位符值的函数
❹ 将 check_route 函数应用于路线列。
❺ 打印出路线列中所有唯一值的修订计数。
当输入数据集中的数据被输入时,路线列中的值并未限制在有效路线上,因此该列包含许多不是有效电车路线的值。如果我们不解决这个问题,我们将用不反映现实世界情况的数据来训练我们的深度学习模型。除非我们清理路线列中的值,否则我们无法将数据集重构为每个记录都是一个路线/方向/时间段组合(第五章)。
值得回顾路线和车辆列的清理过程,因为同样的困境可能出现在许多现实世界的数据集中:你有一个具有严格定义的有效值列表的列,但由于数据输入方式或数据输入过程中的错误检查不严格,数据集中仍然存在不良值。
路线列的问题有多严重?当我们列出路线列中的值时,我们看到有 14 条有效的电车路线,但路线列包含超过 100 个不同的值。
我们定义了一个简单的函数,check_route,该函数检查路线列中的值,并将任何不在有效路线值列表中的值替换为bad value标记。我们将此函数应用于整个路线列,使用 lambda 函数,以便该函数应用于列中的每个值。有关使用 lambda 将函数应用于 Pandas 数据框的更多详细信息,请参阅mng.bz/V8gO。
在应用了check_route函数之后,我们再次检查“路线”列中唯一值的数量,以确认“路线”列不再包含任何意外的值。正如我们所预期的,“路线”列现在有 15 个不同的值:14 个有效的路线值,加上bad route来表示原始数据集中有一个不是有效路线的值。
4.3 为什么只对一个所有错误值进行替换?
你可能会问,我们是否有除了用一个单一值替换所有错误值之外的其他选项。如果我们用一个占位符替换所有无效路线值时,我们是否可能丢失某种信号?也许用反映值为何错误的占位符来替换错误值是有意义的,例如以下内容:
-
公交路线— 对于“路线”列中不是有效电车路线但却是有效公交路线的值。 -
废弃路线— 对于“路线”列中曾经是电车路线的值。 -
非 TTC 路线— 对于“路线”列中是来自多伦多以外地区公交路线的有效路线标识,但不由多伦多交通委员会(TTC)运营。围绕多伦多的市政当局(包括西边的密西沙加,西北边的沃恩,东北边的马克汉姆和东边的达勒姆)有自己的独立交通运营商,从理论上讲,这些非 TTC 运营商中的一家公交路线可能会因电车延误而延误。当前数据集中没有非 TTC 路线的实例,但这并不意味着这种非 TTC 路线不能在未来数据集中出现。正如我们在第九章中将会看到的,一旦模型投入生产,我们就可以预期模型将在新数据上反复重新训练,因此数据准备代码应该具有弹性,以允许潜在的未来输入数据集的变化。允许“路线”列中的非 TTC 路线值是一个预测数据集潜在变化的例子。 -
错误路线— 对于“路线”列中从未是大多伦多地区任何交通运营有效路线的值,包括路线。
对于电车延误问题,这些区别并不相关。我们只对电车路线的延误感兴趣。但如果问题被不同地表述,包括预测电车以外的交通运营的延误,那么在“路线”列中对错误值使用更细粒度的替换,如前面列表中的那些,是有意义的。当然,值得问一下,对于一个列的所有非有效值在项目目标方面是否都是等效的。在电车延误问题的案例中,答案是肯定的,但同样的答案并不一定适用于所有结构化数据问题。
4.4 处理不正确的值:车辆
与存在固定有效电车路线列表一样,也存在一个固定有效的车辆列表。你可以看到如何编译这个有效车辆列表,并在数据准备笔记本中查看该信息的来源(图 4.2)。

图 4.2 有效的电车 ID
公交车也可能成为电车延误的受害者。有效公交车 ID 的列表更复杂(图 4.3)。

图 4.3 有效的公交车 ID
正如我们对路线列所做的那样,我们在下一个列表中定义了一个函数,该函数将无效的车辆值替换为一个单一标记以表示坏值,从而将车辆列中的值数量减少了超过一半。
列表 4.3 清理车辆列值的代码
print("vehicle count pre cleanup",df['Vehicle'].nunique())
df['Vehicle'] = df['Vehicle'].apply(lambda x:check_vehicle(x))
print("vehicle count post cleanup",df['Vehicle'].nunique())
vehicle count pre cleanup 2438 ❶
vehicle count post cleanup 1017 ❷
❶ 清理前车辆列中唯一值的数量
❷ 清理后车辆列中唯一值的数量
结果表明,我们最终不会在第五章和第六章中描述的模型训练中使用车辆列。首先,在第八章中描述的部署场景中,用户将是想要乘坐电车并需要知道是否会延误的人。在这种情况下,用户将不知道他们将乘坐哪一辆具体的车辆。因为用户在想要获取预测时无法提供车辆 ID,所以我们不能使用车辆列中的数据进行模型训练。但我们可以使用车辆列在未来模型的一个变体中,该变体针对的是不同的用户群体(例如,运营电车的交通管理局的行政人员),他们知道特定行程的车辆 ID,因此清理车辆列中的值以备将来使用是值得的。
4.5 处理不一致值:位置
路线和车辆列是典型的分类列,因为它们有一个固定且易于定义的有效值集合。位置列则呈现出一组不同的问题,因为它没有一个整洁定义的有效值集合。花些时间研究位置列相关的问题以及如何解决这些问题是值得的,因为这个列展示了你将在现实世界数据集中遇到的混乱类型。这个列中的值是针对电车数据集的,但我们用来清理这些值的方法(获取一致的大小写,获取值的有序排列,以及用一个单一标记替换指代同一现实世界实体的不一致标记)适用于许多数据集。
以下是位置列值的一些特性:
-
位置列中的值可以是街道交叉口(“皇后街和康纳格街”)或地标(“CNE 环”,“主站”,“莱斯利场”)。
-
有数千个有效的街道交汇值。因为路线超出了其名称街道的范围,所以一个路线可以具有不包含路线名称的有效交汇值。例如,“Broadview and Dundas”是 King 路线事件的有效位置值。
-
地标值可以是普遍知名的(例如“St. Clair West station”),也可以是针对电车网络内部运作的特定值(例如“Leslie Yard”)。
-
街道名称的顺序不一致(“Queen and Broadview”、“Broadview and Queen”)。
-
许多位置有多个标记来表示它们(“Roncy Yard”、“Roncesvalles Yard”和“Ronc. Carhouse”代表同一位置)。
-
位置列中的值总数远大于我们迄今为止查看的任何其他列:
print("Location count pre cleanup:",df['Location'].nunique()) Location count pre cleanup: 15691
这里是我们将要采取的清理位置列的步骤:
-
将所有值转换为小写。
-
使用一致的标记。当使用多个不同的值来表示同一位置时(“Roncy Yard”、“Roncesvalles Yard”和“Ronc. Carhouse”),将这些不同的字符串替换为一个字符串(“Roncesvalles yard”),以确保任何给定位置只使用一个字符串来表示。
-
使交汇处的街道顺序一致(将“Queen and Broadview”替换为“Broadview and Queen”)。我们通过确保交汇处的街道名称总是按照字母顺序排列,即字母顺序排在首位的街道名称在交汇字符串中排在首位来实现这一点。
我们将通过计算在每一步之后位置列中不同值的总数下降的百分比来跟踪我们的进度。在我们将位置列中的所有值转换为小写后,唯一值的数量下降了 15%:
df['Location'] = df['Location'].str.lower()
print("Unique Location values after lcasing:",df['Location'].nunique())
Unique Location values after lcasing: 13263
接下来,我们进行一系列替换以删除重复值,例如“stn”和“station”。你可能会问我们如何确定哪些值是重复的。我们如何知道,例如,“carhouse”、“garage”和“barn”都与“yard”相同,我们应该用“yard”替换这三个值?这个问题问得好,因为它引出了一个关于机器学习项目的重要观点。为了确定位置列中哪些术语是等效的,我们需要对多伦多(特别是其地理)以及特定的电车网络有领域知识。任何机器学习项目都将需要机器学习的技术专长以及应用领域的领域知识。例如,要真正解决第一章中概述的信用卡欺诈检测问题,我们就需要能够接触到对信用卡欺诈细节有深入了解的人。对于本书的主要例子,我选择电车延误问题,因为我对多伦多很了解,并且恰好拥有关于电车网络的知识,这使我能够确定,“carhouse”、“garage”和“barn”与“yard”意思相同。在机器学习项目中,领域知识的需求绝不能被低估,当你考虑要解决的问题时,你应该确保团队中有足够了解项目领域的人。
我们对位置列应用的变化有什么影响?到目前为止,这些变化使唯一值的总减少率达到 30%:
Unique Location values after substitutions: 10867
最后,对于所有街道交叉口的位置值,使街道名称的顺序一致,例如通过消除“broadview and queen”和“queen and broadview”之间的差异。这种转换后,唯一值的总减少率为 36%:
df['Location'] = df['Location'].apply(lambda x:order_location(x))
print("Location values post cleanup:",df['Location'].nunique())
Location values post cleanup: 10074
通过清理位置值(将所有值转换为小写,删除重复值,并确保连接对的一致顺序),我们将独特的位置值数量从超过 15,000 个减少到 10,074 个——减少了 36%。除了使训练数据更准确地反映现实世界外,减少此列中唯一值的数量还可以为我们节省实际费用,正如我们在第 4.6 节中将要看到的。
4.6 走得更远:位置
第 4.5 节中描述的清理是我们对位置值可以做的所有清理吗?还有一种可能的转换可以给我们位置列中的高保真值:用经纬度替换自由文本位置值。地理编码准备笔记本包含了一种使用谷歌地理编码 API(mng.bz/X06Y)进行这种转换的方法。
我们为什么要用经纬度来替换自由文本位置?使用经纬度而不是自由文本位置的优势包括
-
位置精确到地理编码 API 可以做到的程度。
-
输出值是数值的,可以直接用于训练深度学习模型。
不利之处包括
-
35%的位置不是街道交叉口/地址,而是特定于电车网络的地点,例如“birchmount yard”。其中一些地点无法解析为经纬度值。
-
电车网络拓扑和延迟影响模式与实际地图没有直接关系。考虑两个潜在的延迟位置:“king and church” = 纬度 43.648949 / 经度-79.377754 和“queen and church” = 纬度 43.652908 / 经度-79.379458。从纬度和经度的角度来看,这两个位置很近,但在电车网络中它们相距甚远,因为它们位于不同的线路上。
本节(以及地理编码准备笔记本中的代码)假设您正在使用谷歌的地理编码 API。这种方法有几个优点,包括丰富的文档和广泛的用户基础,但它不是免费的。为了在单次批量运行中(即,不是将运行分散到多天以保持在地理编码 API 调用免费剪辑级别内)获取包含 60,000 个位置的数据集的经纬度值,您可能需要花费大约 50 美元。Google 地理编码 API 的几个替代方案可以节省您费用。例如,Locationiq (locationiq.com) 提供了一个免费层,允许您在少于您需要保持 Google 地理编码 API 免费限制的迭代次数中处理更大的位置数据集。
由于调用谷歌地理编码 API 不是免费的,并且由于账户限制在 24 小时内可以对该 API 进行的调用次数,因此准备位置数据非常重要,以便我们尽可能少地进行地理编码 API 调用以获取经纬度值。
为了最小化地理编码 API 调用次数,我们首先定义一个新的 DataFrame,df_unique,其中包含一个单列,该列恰好包含唯一位置值的列表:
loc_unique = df['Location'].unique().tolist()
df_unique = pd.DataFrame(loc_unique,
➥ columns=['Location'])
df_unique.head()
图 4.4 展示了这个新数据框的行片段。

图 4.4 仅包含唯一位置值的 DataFrame
我们定义了一个函数,该函数调用谷歌地理编码 API,以位置作为参数,并返回一个 JSON 结构。如果返回的结构不为空,则解析该结构,并返回一个包含经纬度值的列表。如果结构为空,则返回一个占位符值,如下面的列表所示。
列表 4.4 获取街道交叉口经纬度值的代码
def get_geocode_result(junction):
geo_string = junction+", "+city_name
geocode_result = gmaps.geocode(geo_string)
if len(geocode_result) > 0: ❶
locs = geocode_result[0]["geometry"]["location"]
return [locs["lat"], locs["lng"]]
else:
return [0.0,0.0]
❶ 检查结果是否为空。
如果我们用地理编码可以解释的位置调用此函数,我们将返回一个包含相应纬度和经度值的列表:
get_geocode_result("queen and bathurst")[0]
43.6471969
如果我们调用此函数时使用地理位置编码无法解释的位置,我们将返回占位符值:
locs = get_geocode_result("roncesvalles to longbranch")
print("locs ",locs)
locs [0.0, 0.0]
我们调用get_geocode_results函数在此数据框中创建一个新列,该列包含经纬度值。将这两个值都放入一个列中需要做一些额外的处理以获得我们想要的结果:单独的经纬度列。但以这种方式调用可以减少我们需要进行的地理编码 API 调用次数,节省金钱并帮助我们保持在每日地理编码 API 调用限制内:
df_unique['lat_long'] = df_unique.Location.apply(lambda s:
➥ get_geocode_result(s))
接下来,我们创建单独的经纬度列:
df_unique["latitude"] = df_unique["lat_long"].str[0]
df_unique["longitude"] = df_unique["lat_long"].str[1]
最后,我们将df_unique dataframe与原始数据框合并,以获取添加到原始数据框中的经纬度列:
df_out = pd.merge(df, df_unique, on="Location", how='left')
如您所见,需要几个步骤(包括 Google 地理位置编码 API 的初始设置)才能将经纬度值添加到数据集中。在 Python 程序中理解如何操作经纬度值对于许多具有空间维度的常见商业问题来说是一项有用的技能,经纬度值使得创建可视化(如图 4.5 所示)以识别延误热点成为可能。实际上,第五章和第六章中描述的深度学习模型没有使用位置数据——无论是自由文本还是经纬度。自由文本位置不能用于重构后的数据集,将自由文本位置转换为经纬度的过程复杂且难以集成到管道中。第九章包括一个关于增强模型以包含位置数据以识别电车路线上的延误子段的章节。

图 4.5 显示电车延误热点图的散点图
4.7 修复类型不匹配
要获取分配给数据框的类型,您可以使用数据框的dtypes属性。对于最初用于电车延误数据集的数据框,此属性的值如下所示:
Day object
Delay float64
Direction object
Gap float64
Incident object
Incident ID float64
Location object
Min Delay float64
Min Gap float64
Report Date datetime64[ns]
Route int64
Time object
Vehicle float64
Python 在数据摄入时预测数据类型做得很好,但并不完美。幸运的是,确保您不会遇到类型惊喜很容易。此代码确保连续列具有可预测的类型:
for col in continuouscols:
df[col] = df[col].astype(float)
类似地,看起来像数字的值,如车辆 ID,可能会被 Python 错误地解释,Python 将float64类型分配给车辆列(图 4.6)。

图 4.6 Python 错误地解释了车辆列的类型。
为了纠正这些类型,我们使用astype函数将列转换为字符串类型,然后剪切车辆列的末尾以移除残留的小数点和零:
df['Route'] = df['Route'].astype(str)
df['Vehicle'] = df['Vehicle'].astype(str)
df['Vehicle'] = df['Vehicle'].str[:-2]
4.8 处理仍然包含不良数据的行
在所有清理工作完成后,数据集中还剩下多少不良值?
print("Bad route count pre:",df[df.Route == 'bad route'].shape[0])
print("Bad direction count pre:",df[df.Direction ==
➥ 'bad direction'].shape[0])
print("Bad vehicle count pre:",df[df.Vehicle == 'bad vehicle'].shape[0])
Bad route count pre: 2544
Bad direction count pre: 407
Bad vehicle count pre: 14709
将此结果与数据集中的总行数进行比较:
df.shape output (78525, 13)
如果我们移除包含一个或多个剩余不良值的所有行,数据集的大小会发生什么变化?
if remove_bad_values:
df = df[df.Vehicle != 'bad vehicle']
df = df[df.Direction != 'bad direction']
df = df[df.Route != 'bad route']
df.shape output post removal of bad records (61500, 11)
移除不良值大约移除了 20%的数据。问题是:移除不良值对模型性能的影响是什么?第七章描述了一个实验,比较了在有无不良值的情况下训练模型的结果。图 4.7 显示了该实验的结果。

图 4.7 训练数据集中有无不良值时模型性能的比较
尽管未移除不良值的训练模型的验证准确率与移除不良值的训练模型的验证准确率大致相同,但当我们不移除不良值时,召回率和假阴性计数要差得多。我们可以得出结论,移除不良值对训练模型的性能有益。
4.9 创建派生列
在某些情况下,您可能希望创建由原始数据集中的列派生的新列。具有日期值(如街车数据集中的报告日期)的列包含信息(如年、月和日),这些信息可以被拉入单独的派生列中,这可能有助于提高模型的性能。
图 4.8 显示了在添加基于报告日期的派生列之前的数据框。

图 4.8 在添加基于报告日期的派生列之前的数据框
下面是创建年份、月份和月份日从现有报告日期列中显式列的代码:
merged_data['year'] = pd.DatetimeIndex(merged_data['Report Date']).year
merged_data['month'] = pd.DatetimeIndex(merged_data['Report Date']).month
merged_data['daym'] = pd.DatetimeIndex(merged_data['Report Date']).day
图 4.9 展示了在添加派生列后数据框的样式。

图 4.9 基于报告日期的派生列 DataFrame
在第五章中,我们将生成派生列作为重构数据集过程的一部分。通过将年份、月份和月份日从报告日期列中提取出来并放入它们自己的列中,我们简化了部署过程,使得从用户那里获取日期/时间信息变得直接。
4.10 准备非数值数据以训练深度学习模型
机器学习算法只能在数值数据上训练,因此任何非数值数据都需要转换为数值数据。图 4.10 显示了具有原始分类值的 DataFrame。

图 4.10 在将分类和文本列值替换为数值 ID 之前的数据框
您可以采取两种一般方法中的任何一种来用数值值替换分类值:标签编码,其中列中的每个唯一分类值被替换为一个数值标识符,或者独热编码,其中为每个唯一分类值生成一个新列。行在新列中表示其原始分类值时为1,在其他新列中为0。
标签编码可能会对一些机器学习算法造成问题,这些算法在数值标识符没有实际意义时,会赋予它们相对值的重要性。例如,如果数值标识符用于替换加拿大各省的值,从新 foundland and Labrador 的0开始,到 British Columbia 的9结束,那么 Alberta 的标识符(8)小于 British Columbia 的标识符并不具有意义。
One-hot encoding also has its problems. If a column has more than a few values, one-hot encoding can generate an explosion of new columns that can gobble up memory and make manipulation of the dataset difficult. For the streetcar delay dataset, we are sticking with label encoding to control the number of columns in the dataset.
下一个列表中的代码片段,来自在custom_classes中定义的encode_categorical类,使用了 sci-kit learn 库中的LabelEncoder函数,将分类列中的值替换为数值标识符。
列表 4.5 替换分类列值以使用数值标识符的代码
def fit(self, X, y=None, **fit_params):
for col in self.col_list:
print("col is ",col)
self.le[col] = LabelEncoder() ❶
self.le[col].fit(X[col].tolist())
return self
def transform(self, X, y=None, **tranform_params):
for col in self.col_list:
print("transform col is ",col)
X[col] = self.le[col].transform(X[col]) ❷
print("after transform col is ",col)
self.max_dict[col] = X[col].max() +1
return X
❶ 创建一个 LabelEncoder 的实例。
❷ 使用 LabelEncoder 的实例将分类列中的值替换为数值标识符。
关于这个类的完整描述,请参阅第八章中关于管道的描述。
图 4.11 展示了在将分类列 Day、Direction、Route、hour、month、Location、daym 和 year 进行编码后,基本数据框的外观。

图 4.11 替换分类列值后的数据框
仍然有一列包含非数值数据:Incident。这列包含描述发生延误类型的短语。如果您还记得我们在第三章中进行的探索,我们确定 Incident 可以被处理为一个分类列。为了说明如何准备文本列,让我们现在将其视为一个文本列,并应用 Python Tokenizer API 来完成以下操作:
-
将所有值转换为小写。
-
删除标点符号。
-
将所有单词替换为数值标识符。
下面的列表包含了完成这种转换的代码。
列表 4.6 准备文本列以成为模型训练数据集一部分的代码
from keras.preprocessing.text import Tokenizer
for col in textcols:
if verboseout:
print("processing text col",col)
tok_raw = Tokenizer(num_words=maxwords,lower=True) ❶
tok_raw.fit_on_texts(train[col])
train[col] = tok_raw.texts_to_sequences(train[col])
test[col] = tok_raw.texts_to_sequences(test[col])
❶ Tokenizer 默认将文本转换为小写并删除标点符号。
图 4.12 展示了将这种转换应用于 Incident 列的结果。

图 4.12 替换分类和文本列中的值后的数据框
图 4.13 比较了 Incident 列中的前后值。Incident 列中的每个条目现在都是一个列表(或数组,如果您更喜欢不那么 Python 化的术语)的数值标识符。请注意,在“After”视图中,每个原始列中的单词都有一个列表条目,并且 ID 分配是一致的(相同的单词无论在列中出现的位置如何,都会分配一个一致的 ID)。同时请注意以下几点:
-
最后,我们将事件视为一个分类列,这意味着多令牌值如“Emergency Services”被编码为单个数值。在本节中,我们将事件视为一个文本列,以说明这部分代码的工作原理,因此“Emergency Services”中的每个令牌都被单独编码。
-
我们只展示了一个文本列,但代码可以处理具有多个文本列的数据集。
![CH04_F13_Ryan]()
图 4.13 在文本值编码前后的事件列
4.11 端到端解决方案概述
在我们检查深度学习和我们将用于解决电车延误问题的堆栈之前,让我们通过回顾第二章中引入的端到端图来重新审视整个问题的解决方案。图 4.14 显示了构成解决方案的所有主要元素,从输入数据集到通过简单网站或 Facebook Messenger 部署和访问的训练模型。组件分为三个部分:清洗数据集、构建和训练模型以及部署模型。

图 4.14 完整电车延误项目的总结
图 4.15 放大了我们用于在第 2、3 和 4 章中从输入数据集到清洗后的数据集的组件,包括用于处理表格数据的 Python 和 Pandas 库。

图 4.15 从输入数据集到清洗后的数据集
图 4.16 放大了我们将在第五章和第六章中用于构建和训练深度学习模型的组件,包括深度学习库 Keras 和 scikit-learn 库中的管道工具。

图 4.16 从清洗后的数据集到训练好的深度学习模型和管道
图 4.17 放大了我们将在第八章中用于部署训练好的深度学习模型的组件。对于 Web 部署,这些组件包括 Flask Web 部署库(用于渲染 HTML 网页,用户可以在其中指定他们的电车之旅的详细信息,并查看训练好的深度学习模型对行程是否会延误的预测)。对于 Facebook Messenger 部署,这些组件包括 Rasa 聊天机器人框架、ngrok 用于连接 Rasa 和 Facebook,以及 Facebook 应用程序配置。

图 4.17 从训练模型和管道到部署的深度学习模型
图 4.18 和图 4.19 展示了目标:一个已部署并可用于预测给定电车之旅是否会延误的深度学习模型,无论是在网页上(图 4.18)还是在 Facebook Messenger 上(图 4.19)。

图 4.18 网页上可用的电车延误预测
在本节中,我们已经简要概述了端到端电车延误预测项目。在接下来的四章中,我们将详细介绍从本章的清洗数据集到使用训练模型预测特定电车行程是否会延误的简单网页所需的全部步骤。

图 4.19 在 Facebook Messenger 中可用的电车延误预测
摘要
-
清洗数据并不是我们在数据集上需要进行的唯一准备工作。我们还需要确保非数值值(例如分类列中的字符串)被转换为数值。
-
如果数据集包含无效值(例如不是实际电车路线的路线值或无法映射到任何一个方向上的方向值),这些值可以被替换为一个有效的占位符(例如分类列中最常见的值),或者包含这些值的记录可以从数据集中删除。
-
当 CSV 或 XLS 文件被导入 Pandas 数据框时,列的类型并不总是被正确分配。如果 Python 分配了错误的类型给一个列,你可以将该列转换为所需的类型。
-
分类列中的字符串值需要映射到数值,因为无法使用非数值数据训练深度学习模型。你可以通过使用 scikit-learn 库中的
LabelEncoder函数来完成此映射。
5 准备和构建模型
本章涵盖
-
重新审视数据集,并确定用于训练模型的特征
-
对数据集进行重构,包括没有延误的时间段
-
将数据集转换为 Keras 模型期望的格式
-
根据数据结构自动构建 Keras 模型
-
检查模型结构
-
设置参数,包括激活函数和优化函数以及学习率
本章首先快速回顾数据集,以考虑哪些列可以合法用于训练模型。然后,我们将讨论将数据从我们一直在操作的形式(Pandas 数据框)转换为深度学习模型期望的形式所需的转换。接下来,我们将查看模型的代码本身,并了解模型是如何根据输入列的类别逐层构建的。最后,我们将回顾你可以用来检查模型结构的方法以及你可以用来调整模型训练参数的方法。
本书的所有前几章都为这一刻做准备。在检查问题、准备数据之后,我们终于准备好深入研究深度学习模型本身了。在阅读本章时要注意的一点是:如果你之前没有直接与深度学习模型工作过,你可能会发现模型的代码在完成所有详细的数据准备工作之后显得有些平淡无奇。这种感觉可能与你使用 Python 库进行经典机器学习算法时相似。将逻辑回归或线性回归应用于已准备好的训练数据集的代码并不令人兴奋,尤其是如果你不得不编写非平凡的代码来驯服现实世界的数据集。你可以在这章中看到描述的代码,在 streetcar_model_training 笔记本中。
5.1 数据泄露和可用于训练模型的公平特征
在我们深入探讨构成模型的代码细节之前,我们需要回顾哪些列(无论是原始数据集的列还是从原始列派生出的列)是合法用于训练模型的。如果我们需要使用模型来预测电车延误,我们需要确保避免数据泄露。数据泄露发生在你使用训练数据集之外的数据(包括你试图预测的结果)来训练模型时。如果你在训练模型时依赖于在你想要做出预测时不可用的数据,你可能会遇到以下问题:
-
削弱你做出预测的能力
-
获得过于乐观的模型性能度量
为了理解数据泄露的问题,考虑一个简单的模型,该模型预测特定房地产市场中的房屋销售价格。对于这个模型,你有一组关于该市场最近售出的房屋的信息。你可以使用这些信息来训练一个模型,你稍后可以使用这个模型来预测即将上市房屋的销售价格,如图 5.1 所示。

图 5.1 训练和应用模型以预测房价
图 5.2 显示了在已售房屋数据集中可供你选择的特征。

图 5.2 已售房屋数据集中的可用特征
我们的目的是在房屋首次上市时对其销售价格进行预测,这可能在它售出之前几周。当房屋准备上市时,以下特征将可用:
-
卧室数量
-
卫生间数量
-
层面积
-
前景
我们知道销售价格(我们想要预测的特征)将不可用。那么这些特征呢?
-
市场时间
-
出售价格
当我们想要进行预测时,市场时间将不可用,因为房屋尚未上市,因此我们不应使用此特征来训练模型。要价有些模糊;在我们想要进行预测的时候,它可能可用也可能不可用。这个例子表明,在我们确定给定特征是否会导致数据泄露之前,我们需要对现实世界的业务情况有更深入的了解。
5.2 领域专业知识与最小评分测试以防止数据泄露
你可以采取哪些措施来防止 5.1 节中描述的数据泄露问题?如果你已经在你尝试解决的深度学习模型所涉及的业务问题中拥有领域专业知识,那么你将更容易避免数据泄露。本书的一个目的就是让你能够在日常工作中练习深度学习,以便你能利用你在工作中所拥有的领域专业知识。
回到 5.1 节中的房屋价格示例,如果你使用已被识别为数据泄露来源的特征(如市场时间)来训练你的模型,会发生什么?首先,在训练过程中,你可能看到模型性能(例如,通过准确率来衡量)看起来很好。然而,这种出色的性能是误导性的。这相当于一位教师在考试期间每个学生都偷看了答案时对学生的表现感到高兴。学生们并没有真正做得好,因为他们接触到了在考试时不应接触到的信息。数据泄露的第二个结果是,当你完成模型训练并尝试应用它时,你会发现一些你需要提供以获得预测的特征缺失。
除了应用领域知识(例如,当房子首次上市时,市场时间未知),你还能做什么来防止数据泄露?对模型早期迭代进行最小评分测试可以帮助。在房价示例中,我们可以采用训练模型的临时版本,并应用一两个新上市房屋的数据。由于训练迭代尚未完成,预测可能不准确,但这项练习将揭示模型所需的特征,这些特征在预测时不可用,从而允许我们从训练过程中删除它们。
5.3 防止电车延误预测问题中的数据泄露
在电车延误示例中,我们想要预测一次特定的电车行程是否会延误。在这种情况下,将事件列作为特征来训练模型将构成数据泄露,因为在行程开始之前,我们不知道一次特定的行程是否会延误,以及如果会延误,延误的性质是什么。在行程开始之前,我们不知道事件列对一次特定的电车行程将有什么值。
如图 5.3 所示,最小延误和最小间隔列也会导致数据泄露。我们的标签(我们试图预测的值)来自最小延误,并与最小间隔相关联,因此这两个列都是潜在的数据泄露来源。当我们想要预测一次特定的电车行程是否会延误时,我们不会知道这些值。

图 5.3 原始数据集中可能导致数据泄露的列
从另一个角度来看这个问题,哪些列包含在我们想要为特定行程或一系列行程做出延误预测时将合法可用的信息?用户在预测时提供的信息在第八章中描述。用户提供的信息取决于部署的类型:
-
Web(图 5.4)——在模型的 Web 部署中,用户选择七个评分参数(路线、方向和日期/时间细节),这些参数将被输入到训练好的模型中以获得预测。
![CH05_F04_Ryan]()
图 5.4 Web 部署中用户提供的信息
-
Facebook Messenger(图 5.5)——在 Facebook Messenger 模型的部署中,如果用户没有明确提供,日期/时间细节默认为预测时的当前时间。在这个部署中,用户只需要提供两个评分参数:他们打算乘坐的电车行程的路线和方向。
![CH05_F05_Ryan]()
图 5.5 Facebook Messenger 模型部署中用户提供的信息
让我们检查这些参数以确保我们不会冒着数据泄露的风险:
-
路线 —当我们想要预测街车行程是否会延误时,我们将知道行程将采取哪条路线(例如 501 王后路线或 503 金斯顿路)。
-
方向 —当我们想要预测行程是否会延误时,我们将知道行程的方向(北行、南行、东行或西行)。
我们还将知道以下日期/时间信息(因为用户在 Web 部署中明确设置了值,或者因为我们假设 Facebook Messenger 部署中的当前日期/时间):
-
小时
-
天(星期几,例如星期一)
-
月份中的某一天
-
月份
-
年份
我们希望在预测数据输入中包含一个额外的特征:位置。这个特征有点复杂。如果我们查看图 5.6 中源数据集中的位置信息,我们知道在我们要预测街车行程是否会延误时,我们不会有这些信息。但我们将知道行程的起点和终点(例如,501 王后路线的行程从女王街和舍伯恩街出发,到女王街和斯帕丁纳街)。

图 5.6 位置列中的值
我们如何将这些起点和终点与行程中特定点的延误可能性相关联?与有站点的地铁线路不同,街车路线相对灵活。许多站点沿着路线分布,这些站点不是静态的,就像地铁站一样;街车站点会移动。我们将在第九章中探讨的一种方法是,将每条路线划分为几个部分,并预测在整个街车路线上的行程中,任何部分是否会发生延误。现在,我们将检查一个不包含位置数据的模型版本,这样我们就可以在第一次从头到尾通过模型时,不会遇到太多的额外复杂性。
如果我们将训练街车延误模型限制在我们已识别的列(路线、方向和日期/时间列),我们可以防止数据泄露,并确信我们正在训练的模型将能够对新街车行程做出预测。
5.4 探索 Keras 和构建模型的代码
当你克隆了与本书相关的 GitHub 仓库 (mng.bz/v95x) 后,你将在 notebooks 子目录中找到与探索 Keras 和构建街车延误预测模型相关的代码。下一条列表显示了包含本章描述的代码的文件。
列表 5.1 与探索 Keras 和构建模型相关的代码
├── data ❶
│
├── notebooks
│ streetcar_model_training.ipynb ❷
│ streetcar_model_training_config.yml ❸
│ keras_sequential_api_mnist.py ❹
│ keras_functional_api_mnist.py ❺
❶ 数据准备步骤输出为 pickled dataframe 的目录
❷ 包含重构输入数据集和构建模型的代码的笔记本
❸ 模型训练笔记本的配置文件,包括输入到模型训练笔记本的 pickled dataframe 的名称
❹ 使用 Keras 顺序 API 定义简单深度学习模型的示例(详细信息请见第 5.10 节)
❺ 使用 Keras 功能 API 定义简单深度学习模型的示例(详细信息请见第 5.10 节)
5.5 推导用于训练模型的 dataframe
在第 1-4 章中,我们经历了许多步骤来清洗和转换数据,包括
-
将冗余值替换为单个一致值,例如在方向列中将
eastbound、e/b和eb替换为 e -
移除具有无效值的记录,例如具有无效路线值的记录
-
将分类值替换为数值标识符
图 5.7 显示了这些转换的结果。

图 5.7 到第四章末的转换后的输入数据集
这个数据集是否足够训练一个模型以实现预测给定电车行程是否会延迟的目标?答案是:不。目前,这个数据集只有延迟的记录。缺少的是关于所有没有延迟的情况的信息。我们需要的是一个重构的数据集,它也记录了在特定路线和特定方向上没有延迟的所有时间。
图 5.8 总结了原始数据集和重构数据集之间的差异。在原始数据集中,每条记录描述了一个延迟,包括时间、路线、方向和导致延迟的事件。在重构数据集中,对于每个时间槽(从 2014 年 1 月 1 日以来的每小时),路线和方向的组合都有一个记录,无论在那个时间槽内该路线在该方向上是否有延迟。

图 5.8 比较原始数据集与重构数据集
图 5.9 明确显示了重构数据集的外观。如果在给定时间槽(特定一天的小时)的给定路线的给定方向上有延迟,计数不为零;否则,计数为零。图 5.9 中重构数据集的片段显示,在 2014 年 1 月 1 日午夜到早上 5 点之间,301 路线在东行方向上没有发生任何延迟。

图 5.9 每个路线/方向/时间槽组合的行重构数据集
图 5.10 总结了获取每个时间槽/路线/方向组合条目的重构数据集的步骤。

图 5.10 重构 dataframe
以下步骤:
- 创建一个包含每个路线/方向组合的行
routedirection_frame数据框(图 5.11)。

图 5.11 路向帧数据框
- 创建一个包含从中选择训练数据的日期范围的每个日期的
date_frame数据框(图 5.12)。

图 5.12 date_frame 数据框
- 创建一个包含每天每小时一行数据的
hour_frame数据框(图 5.13)。

图 5.13 hour_frame 数据框
- 将这三个数据框合并以获得
result2数据框(图 5.14)。

图 5.14 result2 数据框
-
将日期的组成部分的派生列添加到
result2数据框中:result2['year'] = pd.DatetimeIndex(result2['Report Date']).year result2['month'] = pd.DatetimeIndex(result2['Report Date']).month result2['daym'] = pd.DatetimeIndex(result2['Report Date']).day result2['day'] = pd.DatetimeIndex(result2['Report Date']).weekday -
从输入数据集(
merged_data数据框)中删除多余的列,并将其与result2数据框合并,以获得完成重构的数据框(图 5.15)。![CH05_F15_Ryan]()
图 5.15 完成重构的数据框
为了比较重构前后的数据集大小,一个包含 56,000 行的数据集在重构后变成了包含 2.5 百万行以覆盖五年期间的数据集。这个重构数据集覆盖的时间段的开始和结束由以下变量控制,这些变量定义在整体参数块中:
start_date = date(config['general']['start_year'], \
config['general']['start_month'], config['general']['start_day'])
end_date = date(config['general']['end_year'], \
config['general']['end_month'], config['general']['end_day'])
开始日期与 2014 年 1 月延迟数据集的开始相对应。你可以通过更新配置文件 streetcar_model_training_config.yml 中的参数来更改结束日期,但请注意,你希望 end_date 不晚于原始来源的最新延迟数据(mng.bz/4B2B)。
5.6 将数据框转换为 Keras 模型期望的格式
Keras 模型期望输入张量。你可以将张量视为矩阵的推广。矩阵是一个二维张量,而向量是一个一维张量。图 5.16 总结了按维度总结的张量常见术语。

图 5.16 张量摘要
当我们在 Pandas 数据框中完成所有需要进行的所有数据转换后,在将数据输入模型进行训练之前的最后一步是将数据放入 Keras 模型所需的张量格式。通过将这种转换作为训练模型之前的最后一步,我们可以在需要将数据放入 Keras 模型期望的格式之前,享受到 Pandas 数据框的便利性和熟悉感。执行此转换的代码位于 prep_for_keras_input 类的 transform 方法中,如下所示。这个类是第八章中描述的管道的一部分,该管道对训练和评分的数据进行转换。
列表 5.2 将数据放入模型所需的张量格式的代码
def __init__(self):
self.dictlist = [] ❶
return None
def transform(self, X, y=None, **tranform_params):
for col in self.collist:
print("cat col is",col)
self.dictlist.append(np.array(X[col])) ❷
for col in self.textcols:
print("text col is",col)
self.dictlist.append(pad_sequences(X[col], \
➥ maxlen=max_dict[col])) ❸
for col in self.continuouscols:
print("cont col is",col)
self.dictlist.append(np.array(X[col])) ❹
return self.dictlis.t
❶ 将包含每个列的 numpy 数组的列表
❷ 将当前分类列的 numpy 数组追加到整体列表中。
❸ 将当前文本列的 numpy 数组追加到整体列表中。
❹ 将当前连续列的 numpy 数组追加到整体列表中。
这段代码是灵活的。像本例中的其他代码一样,只要输入数据集的列被正确分类,这段代码就可以与各种表格结构化数据一起工作。它并不特定于电车数据集。
5.7 Keras 和 TensorFlow 的简要历史
我们已经审查了数据集在准备训练深度学习模型之前需要经历的最终转换集。本节提供了关于 Keras 的背景信息,Keras 是用于创建本书主要示例中模型的通用深度学习框架。我们将从本节开始,简要回顾 Keras 的历史及其与低级深度学习框架 TensorFlow 的关系。在第 5.8 节中,我们将回顾从 TensorFlow 1.x 迁移到 TensorFlow 2(本书代码示例所使用的后端深度学习框架)所需的步骤。在第 5.9 节中,我们将简要对比 Keras/TensorFlow 框架与其他主要深度学习框架 PyTorch。在第 5.10 节中,我们将回顾两个代码示例,展示如何在 Keras 中构建深度学习模型的层。有了关于 Keras 的这些背景知识,我们就可以准备检查 Keras 框架是如何用于实现第 5.11 节中的电车延迟预测深度学习模型的了。
Keras 最初是作为各种后端深度学习框架的前端而诞生的,包括 TensorFlow (www.tensorflow.org) 和 Theano。Keras 的目的是提供一套易于访问、易于使用的 API,开发者可以使用这些 API 来探索深度学习。当 Keras 在 2015 年发布时,它所支持的深度学习后端库(最初是 Theano,然后是 TensorFlow)提供了一系列广泛的功能,但对于初学者来说可能难以掌握。有了 Keras,开发者可以通过使用熟悉的语法开始深度学习,而无需担心后端库中暴露的所有细节。
如果你是在 2017 年开始一个深度学习项目,你的选择包括
-
直接使用 TensorFlow 库
-
将 Keras 作为 TensorFlow 的前端使用
-
使用 Keras 与其他后端,如 Theano(尽管到 2017 年,除了 TensorFlow 之外的后端变得越来越少见)
尽管大多数使用 Keras 进行深度学习项目的人使用了 TensorFlow 作为后端,但 Keras 和 TensorFlow 是两个独立的项目。所有这些都在 2019 年随着 TensorFlow 2 的发布而改变:
-
鼓励使用 Keras 进行深度学习的程序员使用集成到 TensorFlow 中的
tf.keras包,而不是独立的 Keras。 -
鼓励 TensorFlow 用户使用 Keras(通过 TensorFlow 中的
tf.keras包)作为 TensorFlow 的高层 API。截至 TensorFlow 2,Keras 是 TensorFlow 的官方高层 API (mng.bz/xrWY)。
简而言之,原本是独立但相关的项目 Keras 和 TensorFlow 已经合并。特别是,随着新的 TensorFlow 点版本发布(例如,2020 年 5 月发布的 TensorFlow 2.2.0 [mng.bz/yrnJ]),它们将包括后端以及 Keras 前端的改进。您可以在 Keras 和 TensorFlow 的 Python 深度学习 章节中找到有关 Keras 和 TensorFlow 之间关系的更多详细信息,特别是 TensorFlow 在使用 Keras 定义的深度学习模型整体操作中扮演的角色(mng.bz/AzA7)。
5.8 从 TensorFlow 1.x 迁移到 TensorFlow 2
本章和第六章中描述的深度学习模型代码最初是编写为使用独立的 Keras,以 TensorFlow 1.x 作为后端。在本书编写期间,TensorFlow 2 已发布,因此我决定迁移到 TensorFlow 2 中的集成 Keras 环境。因此,要在 streetcar_model_training .ipynb 中运行代码,您需要在 Python 环境中安装 TensorFlow 2。如果您有尚未迁移到 TensorFlow 2 的其他深度学习项目,您可以为此书中的代码示例创建一个特定的 Python 虚拟环境,并在其中安装 TensorFlow 2。这样,您就不会在您的其他深度学习项目中引入更改。
本节总结了我在模型训练笔记本中需要进行的代码更改,以将其从使用 TensorFlow 1.x 作为后端的独立 Keras 迁移到 TensorFlow 2 上下文中的 Keras。TensorFlow 文档中包含了全面的迁移步骤,网址为 www.tensorflow.org/guide/migrate。以下是我在以下步骤中采取的简要总结:
-
将我的现有 TensorFlow 版本升级到最新版本的 TensorFlow 1.x:
pip install tensorflow==1.1.5 -
完整运行模型训练笔记本以验证在最新版本的 TensorFlow 1.x 中一切是否正常工作。
-
在模型训练笔记本上运行升级脚本 tf_upgrade_v2。
-
将所有 Keras 导入语句更改为引用 tf.keras 包(包括将
from keras import regularizers更改为from tensorflow.keras import regularizers)。 -
使用更新的导入语句完整运行模型训练笔记本以验证一切是否正常工作。
-
按照以下网址的说明创建了 Python 虚拟环境:
janakiev.com/blog/jupyter-virtual-envs。 -
在 Python 虚拟环境中安装了 TensorFlow 2。这一步骤是必要的,因为第八章中描述的 Facebook Messenger 部署方法的一部分 Rasa 聊天机器人框架需要 TensorFlow 1.x。通过在虚拟环境中安装 TensorFlow 2,我们可以利用虚拟环境进行模型训练步骤,而不会破坏 TensorFlow 1.x 的部署先决条件。以下是安装 TensorFlow 2 的命令:
pip install tensorflow==2.0.0
迁移到 TensorFlow 2 的过程非常顺利,多亏了 Python 虚拟环境,我能够在需要的地方进行模型训练迁移,而不会对我的其他 Python 项目造成任何副作用。
5.9 TensorFlow 与 PyTorch
在更深入地探索 Keras 之前,快速讨论一下目前用于深度学习的另一个主要库:PyTorch (pytorch.org)。PyTorch 由 Facebook 开发,并于 2017 年作为开源软件发布。在 mng.bz/Moj2 的文章中对这两个库进行了简洁的比较。目前使用 TensorFlow 的社区比使用 PyTorch 的社区大,尽管 PyTorch 的增长速度很快。PyTorch 在学术界/研究界有更强的存在感(并且是第九章中描述的 fast.ai 课程编码方面的基础),而 TensorFlow 在工业界占主导地位。
5.10 Keras 中深度学习模型的架构
你可能还记得,第一章将神经网络描述为由层组织的一系列节点,每一层都与一些权重相关联。简单来说,在训练过程中,这些权重会反复更新,直到损失函数最小化,并且模型预测的准确性得到优化。在本节中,我们将通过回顾两个简单的深度学习模型来展示第一章中引入的层这一抽象概念如何在 Keras 代码中体现。
定义 Keras 模型的层有两种方式:顺序 API 和功能 API。顺序 API 是更简单的方法,但灵活性较低;功能 API 更灵活,但使用起来稍微复杂一些。
为了说明这两个 API,我们将探讨如何使用两种方法创建最小的 Keras 深度学习模型,用于 MNIST (www.tensorflow.org/datasets/catalog/mnist)。如果你之前没有遇到过 MNIST,它是由手写数字的标记图像组成的数据库。x 值来自图像文件,标签(y 值)是数字的文本表示。MNIST 模型的目标是正确识别手写图像中的数字。MNIST 通常用作练习深度学习模型的简化数据集。如果你想要更多关于 MNIST 以及它是如何用于练习深度学习框架的背景信息,一篇优秀的文章在 mng.bz/Zr2a 提供了更多细节。
值得注意的是,MNIST 并不是一个根据第一章中结构化数据的定义所定义的结构化数据集。尽管它不是一个结构化数据集,但选择 MNIST 作为本节示例的原因有两个:Keras API 的已发布入门示例使用 MNIST,并且没有公认的与 MNIST 相当的结构化数据集用于训练深度学习模型。
使用顺序 API,模型定义接受一个有序的层列表作为参数。您可以从 TensorFlow 2 中支持的 Keras 层列表中选择您想要包含在模型中的层(mng.bz/awaJ)。列表 5.3 中的代码片段来自keras_sequential_api_mnist.py,它改编自 TensorFlow 2 文档(mng.bz/RMAO),并展示了一个简单的 MNIST 深度学习模型,该模型使用 Keras 顺序 API。
列表 5.3 使用 Keras 顺序 API 的 MNIST 模型代码
import tensorflow as tf
import pydotplus
from tensorflow.keras.utils import plot_model
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
model = tf.keras.models.Sequential([ ❶
tf.keras.layers.Flatten(input_shape=(28, 28)), ❷
tf.keras.layers.Dense(128, activation='relu'), ❸
tf.keras.layers.Dropout(0.2), ❹
tf.keras.layers.Dense(10) ❺
])
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),\
metrics=['accuracy']) ❻
history = model.fit(x_train, y_train, \
batch_size=64, \
epochs=5, \
validation_split=0.2) ❼
test_scores = model.evaluate(x_test, y_test, verbose=2) ❽
print('Test loss:', test_scores[0])
print('Test accuracy:', test_scores[1])
❶ 定义 Keras 顺序模型
❷ 展平层,将输入张量重塑为形状等于输入张量元素数量的张量
❸ 密集层执行标准操作,即计算层的输入与层中的权重以及偏置的点积
❹ Dropout 层,随机关闭网络的一部分
❺ 输出密集层
❻ 编译模型,指定损失函数、优化器和训练过程中要跟踪的指标。
❼ 通过调整权重以最小化损失函数来拟合模型。
❽ 评估模型性能
这个 Keras 深度学习模型的简单示例与您已经看到的非深度学习模型有几个共同特点:
-
输入数据集被分为训练集和测试集。训练集用于训练过程中调整模型中的权重。测试数据集应用于训练好的模型以评估其性能;在这个例子中,根据准确率(即模型的预测与实际输出值的接近程度)。
-
训练集和测试集都由输入
x值(对于 MNIST,手写数字的图像)和标签或y值(对于 MNIST,对应手写数字的 ASCII 数字)组成。 -
非深度学习和深度学习模型在定义和拟合模型时都有类似的语句。下一列表中的代码片段对比了定义和拟合逻辑回归模型和 Keras 深度学习模型的语句。
-
列表 5.4 对比逻辑回归模型和 Keras 模型的代码
from sklearn.linear_model import LogisticRegression clf_lr = LogisticRegression(solver = 'lbfgs') ❶ model = clf_lr.fit(X_train, y_train) ❷ model = tf.keras.models.Sequential([ ❸ tf.keras.layers.Flatten(input_shape=(28, 28)), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(10) ]) model.compile(optimizer='adam', \ ❹ loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),\ metrics=['accuracy']) history = model.fit(x_train, y_train, \ ❺ batch_size=64, \ epochs=5, \ validation_split=0.2)❶ 定义逻辑回归模型。
❷ 调整逻辑回归模型。
❸ 定义 Keras 深度学习模型的第一部分:定义层
❹ 定义 Keras 深度学习模型的第二部分:设置编译参数
❺ 拟合 Keras 深度学习模型。
图 5.17 显示了plot_model函数对 MNIST 顺序 API 模型的输出。

图 5.17 简单顺序 API Keras 模型的 plot_model 输出
与序列 API 相比,Keras 功能 API 的语法更复杂,但提供了更大的灵活性。特别是,功能 API 允许您定义具有多个输入的模型。正如您将在第 5.13 节中看到的,本书中的扩展示例利用了功能 API,因为它需要多个输入。
列表 5.5 中的代码片段来自 keras_ functional_api_mnist.py。它改编自www .tensorflow.org/guide/keras/functional,展示了如何使用 Keras 功能 API 定义一个简单的深度学习模型,用于解决我们之前展示的序列 API 解决方案相同的 MNIST 问题。
列表 5.5 使用 Keras 功能 API 的 MNIST 模型代码
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
inputs = keras.Input(shape=(784,))
flatten = layers.Flatten(input_shape=(28, 28)) ❶
flattened = flatten(inputs)
dense = layers.Dense(128, activation='relu')(flattened) ❷
dropout = layers.Dropout(0.2) (dense) ❸
outputs = layers.Dense(10) (dropout) ❹
# define model inputs and outputs (taken from layer definition)
model = keras.Model(inputs=inputs, outputs=outputs, \
name='mnist_model')
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype('float32') / 255
x_test = x_test.reshape(10000, 784).astype('float32') / 255
# compile model, including specifying the loss function, \
optimizer, and metrics
model.compile(loss=keras.losses.SparseCategoricalCrossentropy( \
from_logits=True), \
optimizer=keras.optimizers.RMSprop(), \
metrics=['accuracy']) ❺
# train model
history = model.fit(x_train, y_train, \
batch_size=64, \
epochs=5, \
validation_split=0.2) ❻
# assess model performance
test_scores = model.evaluate(x_test, y_test, verbose=2) ❼
print('Test loss:', test_scores[0])
print('Test accuracy:', test_scores[1])
❶ 定义层,通过重新塑形输入张量,将其转换为与输入张量元素数量相等的张量
❷ 执行标准操作的密集层,即计算层的输入与层中的权重以及偏置的乘积
❸ 随机关闭网络一部分比例的 Dropout 层
❹ 输出密集层
❺ 编译模型,指定损失函数、优化器和训练过程中要跟踪的指标。
❻ 通过调整权重以最小化损失函数,将模型(使用训练数据集的参数、批量大小、训练轮数以及为验证保留的训练集子集)拟合。
❼ 评估模型性能。
您可以看到序列 API 和功能 API 在此问题上的许多相似之处。例如,loss函数的定义方式相同,compile和fit语句也相同。序列 API 和功能 API 之间的不同之处在于层的定义。在序列 API 方法中,层定义在一个单独的列表中,而在功能 API 方法中,层是递归定义的,每个层都是在其前驱层的基础上构建的。
图 5.18 显示了此简单功能 API Keras 模型的plot_model输出。

图 5.18 简单功能 API Keras 模型的 plot_model 输出
在本节中,我们考察了几种简单的 Keras 模型,并回顾了 Keras 提供的两种基本方法的关键特性:序列 API 和功能 API。在第 5.13 节中,我们将看到街道延误预测模型如何利用功能 API 的灵活性。
5.11 数据结构如何定义 Keras 模型
在第三章中,您了解到结构化数据集中的列被分类为分类、连续或文本(图 5.19)。

图 5.19 街道数据集的列类别
-
连续 —这些值是数值。常见的连续值示例包括温度、货币值、时间跨度(如已过小时数)和对象或活动的计数。在街车示例中,最小延迟和最小间隔(包含延迟造成的分钟数和街车之间产生的间隔长度的分钟数)是连续列。从位置列派生出的纬度和经度值也被视为连续列。
-
类别 —这些值可以是单个字符串,例如一周中的某一天,或者由一个或多个字符串组成的标识符集合,例如美国各州的名称。类别列中不同值的数量可以从两个到几千个不等。街车数据集中的大多数列都是类别列,包括路线、日期、位置、事件、方向和车辆。
-
文本 —这些值是字符串集合。
将输入数据集组织成这些类别是至关重要的,因为这些类别定义了本书中描述的深度学习方法中深度学习模型代码的组装方式。Keras 模型的层是基于这些类别构建的,每个类别都有自己的层结构。
以下插图总结了为类别和文本列构建的层。图 5.20 显示了为类别列构建的层:
-
嵌入 —如第 5.12 节所述,嵌入提供了一种方法,使模型能够在整体预测的上下文中学习类别中值之间的关系。
-
批量归一化 —批量归一化是一种通过控制隐藏层中权重变化量来防止过拟合(模型在训练数据集上表现良好,但在其他数据上表现不佳)的方法。
-
展平 —将输入重塑以准备后续层。
-
丢弃 —使用此技术来防止过拟合。正如其名所示,丢弃技术会在网络的前向和反向传播过程中忽略网络中节点的随机子集。
-
连接 —将此输入的层与其他输入的层连接起来。
![CH05_F20_Ryan]()
图 5.20 Keras 类别输入层
图 5.21 显示了为文本输入列构建的层。除了分类列的层之外,文本列还获得一个 GRU 层。GRU(keras.io/api/layers/recurrent_layers/gru)是一种循环神经网络(RNN),这是深度学习模型中常用的一种文本处理模型。RNN 与其他神经网络的不同之处在于控制先前输入对当前输入如何改变模型中权重的影响的门。有趣的是,这些门的行为——从先前输入中“记住”多少——是以与网络中通用权重相同的方式学习的。将此层添加到文本列的层集中意味着,从高层次来看,文本列中单词的顺序(而不仅仅是单个单词的存在)有助于模型的训练。

图 5.21 Keras 文本输入层
我们已经回顾了添加到深度学习模型中的分类和文本列的层。那么连续列呢?这些列没有特殊额外的层,而是直接连接到整体模型中。
本节简要描述了在结构化数据集中为每种列类型(连续、分类和文本)构建的深度学习模型中的层。在 5.13 节中,我们将详细讲解实现这些层的代码。
5.12 嵌入的力量
在 5.11 节中,您看到了在结构化数据集中为每种类型的列定义的层。特别是,您看到了分类和文本列获得嵌入层。在本节中,我们将检查嵌入及其应用。在第七章中,我们将通过一个实验回到嵌入的强大功能,该实验展示了分类列嵌入对模型性能的影响。
嵌入来自自然语言处理领域,其中它们用于将单词映射到数值表示。嵌入是本书的一个重要主题,因为它们对于使深度学习模型能够利用结构化数据集中的分类和文本列至关重要。本节并不是嵌入的全面描述——这是一个值得单独成书的话题,但它介绍了该概念并描述了为什么在结构化数据的深度学习项目中需要嵌入。您可以在 Stephan Raaijmakers 的《深度学习自然语言处理》一书中找到嵌入的更详细描述(mng.bz/2WXm)。
在mng.bz/1gzn上的优秀文章中指出,嵌入是作为学习到的低维连续向量对分类值的表示。这句话包含了大量信息。让我们逐个分析:
-
类别值的表示 —回想一下第三章中类别值的定义:这些值可以是“单个字符串,如一周中的某一天,或者由一个或多个字符串组成的标识符集合,如美国各州的名称。类别列中不同值的数量可以从两个到几千个不等。”与嵌入相关的街车数据集的列包括类别列路线、日期、位置、事件、方向和车辆。
-
学习得到的 —就像第一章中介绍的深度学习模型权重一样,嵌入的值是通过学习得到的。嵌入的值在训练之前被初始化,然后通过深度学习模型的迭代更新。结果是,在嵌入被学习之后,那些倾向于产生相同结果的类别值的嵌入会更接近彼此。考虑一周中的日子(从星期一到星期日)作为街车延误数据集的派生特征。假设周末的延误较少。如果是这样,周六和周日的嵌入将比工作日的嵌入更接近。
-
低维 —这个术语意味着嵌入向量的维度相对于类别值的数量来说是低的。在本书中主要示例创建的模型中,路线列有超过 1,000 个不同的值,但其嵌入维度为 10。
-
连续的 —这个术语意味着嵌入中的值由浮点数表示,而不是表示类别值的整数。
一个著名的嵌入示例(arxiv.org/pdf/1711.09160.pdf)展示了它们如何捕捉与它们相关联的类别值之间的关系。这个示例显示了 Word2Vec(mng.bz/ggGR)中与四个单词相关的向量之间的关系:
v(king) − v(man) + v(woman) ≈ v(queen)
这意味着国王的嵌入向量减去男人的嵌入向量再加上女人的嵌入向量接近于皇后的嵌入向量。在这个例子中,对嵌入的算术运算与与嵌入相关联的单词之间的语义关系相匹配。这个例子展示了嵌入将非数值、类别值映射到平面空间的能力,在这个空间中,值可以像数字一样被操作。
嵌入的另一个好处是,它们可以用来说明类别值之间的隐含关系。当你解决监督学习问题时,你会免费获得对类别进行无监督学习分析。
嵌入的最后一个好处是,它们为你提供了一种将分类值纳入深度学习框架的方法,而无需一热编码的缺点(mng.bz/P1Av)。在一热编码中,如果有七个值(如一周中的日子),每个值都由一个大小为 7 的向量表示,其中包含六个 0 和一个 1:
-
星期一:[1,0,0,0,0,0,0]
-
星期二:[0,1,0,0,0,0,0,0]
-
. . .
-
星期日:[0,0,0,0,0,0,0,1]
在结构化数据集中,一周中各天的分类的一热编码需要七个列,对于一个值数量较少的分类来说,这并不算太糟糕。但关于车辆列,它有数百个值呢?你可以看到一热编码如何迅速增加数据集中的列数以及处理数据的内存需求。通过利用深度学习中的嵌入,我们可以处理分类列,而无需一热编码的糟糕缩放行为。
本节简要介绍了嵌入主题。使用嵌入的好处包括能够操作类似于常见数值操作的类似非数值值,能够在解决监督学习问题的副产品中获得对分类范围内值的无监督学习型分类,以及能够在没有一热编码的缺点的情况下使用分类输入训练深度学习模型。
5.13 基于数据结构自动构建 Keras 模型的代码
Keras 模型由一系列层组成,输入列通过这些层流动以生成给定路线/方向/时间槽组合的延迟预测。图 5.22 展示了输入列流经的层,以及根据其数据类别(连续、分类或文本)的示例数据类型。要查看探索这些层的选项,请参阅第 5.14 节。

图 5.22 按列类型划分的 Keras 层及示例
下一个列表展示了将每个列分配到这些类别之一的代码。
列表 5.6 将列分配给类别的代码
textcols = [] ❶
continuouscols = [] ❷
if targetcontinuous:
excludefromcolist = ['count','Report Date', 'target', \
➥ 'count_md','Min Delay'] ❸
else:
excludefromcolist = ['count','Report Date', \
➥ 'target','count_md', 'Min Delay']
nontextcols = list(set(allcols) - set(textcols))
collist = list(set(nontextcols) - \
set(excludefromcolist) - set(continuouscols)) ❹
❶ 在重构后的模型中,文本列的集合为空。
❷ 在重构后的模型中,连续列的集合为空。
❸ excludefromcolist 是将不会训练模型的列集合。
❹ collist 是分类列的列表。在这里,它是通过从列列表中移除文本列、连续列和排除列生成的。
这些列列表——textcols、continuouscols 和 collist——在代码中用于确定对列采取的操作,包括如何为每个用于训练模型的列构建深度学习模型的层。以下各节展示了为每种列类型添加的层。
以下列表显示了在应用于模型进行训练之前,训练数据的样子。训练数据是一个 numpy 数组的列表——每个训练数据列一个数组。
列表 5.7 在用于训练模型之前的数据格式
X_train_list is
[array([13, 23, 2, ..., 9, 22, 16]), ❶
array([13, 13, 3, ..., 4, 11, 2]), ❷
array([1, 6, 5, ..., 5, 3, 2]), ❸
array([8, 9, 0, ..., 7, 0, 4]), ❹
array([4, 1, 1, ..., 1, 4, 4]), ❺
array([10, 10, 23, ..., 21, 17, 15]), ❻
array([4, 1, 3, ..., 0, 3, 0])] ❼
❶ 用于小时的 numpy 数组。值范围从 0 到 23。
❷ 用于路线的 numpy 数组。值范围从 0 到 14。
❸ 用于星期的 numpy 数组。值范围从 0 到 6。
❹ 用于月份的 numpy 数组。值范围从 0 到 11。
❺ 用于年份的 numpy 数组。值范围从 0 到 6。
❻ 用于月份天数的 numpy 数组。值范围从 0 到 30。
❼ 用于方向的 numpy 数组。值范围从 0 到 4。
组装每种类型列层的代码位于streetcar_model_training笔记本中的get_model()函数中。以下是get_model()函数中用于连续列的代码块,它只是让输入层流过:
for col in continuouscols:
continputs[col] = Input(shape=[1],name=col)
inputlayerlist.append(continputs[col])
在get_model()函数的分类列代码块中,我们看到这些列获得了嵌入和批归一化层,如下一列表所示。
列表 5.8 将嵌入和批归一化应用于分类列的代码
for col in collist:
catinputs[col] = Input(shape=[1],name=col) ❶
inputlayerlist.append(catinputs[col]) ❷
embeddings[col] = \
➥ (Embedding(max_dict[col],catemb) (catinputs[col])) ❸
embeddings[col] = (BatchNormalization() (embeddings[col])) ❹
❶ 从输入开始。
❷ 将此列的输入层追加到输入层列表中,该列表将在模型定义语句中使用。
❸ 添加嵌入层。
❹ 添加批归一化层。
在get_model ()函数的文本列代码块中,我们看到这些列获得了嵌入、批归一化、dropout 和 GRU 层,如下所示。
列表 5.9 将适当的层应用于文本列的代码
for col in textcols:
textinputs[col] = \
Input(shape=[X_train[col].shape[1]], name=col) ❶
inputlayerlist.append(textinputs[col]) ❷
textembeddings[col] = (Embedding(textmax,textemb) (textinputs[col])) ❸
textembeddings[col] = (BatchNormalization() (textembeddings[col])) ❹
textembeddings[col] = \
Dropout(dropout_rate)( GRU(16,kernel_regularizer=l2(l2_lambda)) \
(textembeddings[col])) ❺
❶ 从输入开始。
❷ 将此列的输入层追加到输入层列表中,该列表将在模型定义语句中使用。
❸ 添加嵌入层。
❹ 添加批归一化层。默认情况下,样本是单独归一化的。
❺ 添加 dropout 层和 GRU 层。
在本节中,我们探讨了构成街车延误预测问题的 Keras 深度学习模型核心的代码。我们看到了get_model()函数如何根据输入列的类型(连续、分类和文本)构建模型的层。由于get_model()函数不依赖于任何特定输入数据集的表格结构,因此此函数可以与各种输入数据集一起使用。像本例中的其他代码一样,只要输入数据集的列被正确分类,get_model()函数就会为各种表格结构数据生成 Keras 模型。
5.14 探索你的模型
我们创建的用于预测街车延误的模型相对简单,但如果之前没有遇到过 Keras,理解起来可能仍然有些令人困惑。幸运的是,你可以使用一些工具来检查模型。在本节中,我们将回顾三个你可以用来探索模型的工具:model.summary ()、plot_model ()和 TensorBoard。
model.summary() API 列出了模型中的每一层,其输出形状、参数数量以及输入到其中的层。图 5.23 中的 model.summary() 输出片段显示了输入层 daym、year、Route 和 hour。你可以看到 daym 如何连接到 embedding_1,而 embedding_1 又连接到 batch_normalization_1,batch_normalization_1 再连接到 flatten_1。

图 5.23 model.summary() 输出
当你最初创建一个 Keras 模型时,model.summary() 的输出可以真正帮助你理解层的连接方式,并验证你对层之间关系的假设。
如果你想要一个关于 Keras 模型层之间关系的图形视角,可以使用 plot_model 函数 (keras.io/visualization)。model.summary() 以表格格式生成有关模型的信息;由 plot_model 生成的文件以图形方式说明了相同的信息。model.summary() 更容易使用。因为 plot_model 依赖于 Graphviz 软件包(Graphviz [www.graphviz.org ] 可视化软件的 Python 实现),在新的环境中使 plot_model 工作可能需要一些工作,但如果需要一种向更广泛的受众解释你的模型的方法,这种努力是值得的。
在我的 Windows 10 环境中使 plot_model 工作所需进行的操作如下:
pip install pydot
pip install pydotplus
conda install python-graphviz
当我完成这些 Python 库更新后,我在 Windows 中下载并安装了 Graphviz 软件包 (graphviz.gitlab.io/download)。最后,为了在 Windows 中使 plot_model 工作,我必须更新 PATH 环境变量,以显式包含 Graphviz 安装路径中的 bin 目录。
下面的列表显示了 streetcar_model_training 笔记本中调用 plot_model 的代码。
列表 5.10 调用 plot_model 的代码
if save_model_plot: ❶
model_plot_file = "model_plot"+modifier+".png" ❷
model_plot_path = os.path.join(get_path(),model_plot_file)
print("model plot path: ",model_plot_path)
plot_model(model, to_file=model_plot_path) ❸
❶ 检查在街车模型训练配置文件中是否设置了 save_model_plot 开关。
❷ 如果设置了,请设置模型图将保存的文件名和路径。
❸ 使用模型对象和完全限定的文件名作为参数调用 plot_model。
图 5.24 和 5.25 显示了街车延迟预测模型的 plot_model 输出。每个列的层开始处用数字突出显示:
-
方向
-
小时
-
年
-
路线
-
月
-
月份的日
-
日

图 5.24 plot_model 输出显示了模型中的层(顶部部分)

图 5.25 plot_model 输出显示了模型中的层(底部部分)
图 5.26 显示了 plot_model 对日列输出的近距离视图。
除了model .summary() 和 plot_model,您还可以使用 TensorBoard 实用程序来检查训练模型的特性。TensorBoard (www.tensorflow.org/tensorboard/get_started) 是 TensorFlow 提供的一个工具,它允许您图形化跟踪指标,如损失和准确率,并生成模型图的图表。

图 5.26 天数列层的特写
要使用 TensorBoard 与电车延误预测模型一起使用,请按照以下步骤操作:
-
导入所需的库:
from tensorflow.python.keras.callbacks import TensorBoard -
定义一个 TensorBoard 回调,包括 TensorBoard 日志的路径,如从电车 _ 模型训练笔记本中的
set_early_stop函数此片段所示。列表 5.11 定义回调的代码
if tensorboard_callback: ❶ tensorboard_log_dir = \ os.path.join(get_path(),"tensorboard_log", \ datetime.now().strftime("%Y%m%d-%H%M%S")) ❷ tensorboard = TensorBoard(log_dir= tensorboard_log_dir) ❸ callback_list.append(tensorboard) ❹❶ 如果在电车 _model_training 配置文件中将 tensorboard_callback 参数设置为 True,则定义一个 TensorBoard 回调。
❷ 定义一个使用当前日期的日志文件路径。
❸ 使用日志目录路径作为参数定义 tensorboard 回调。
❹ 将 tensorboard 回调添加到整体回调列表中。请注意,只有当 early_stop 为 True 时,tensorboard 回调才会被调用。
-
将模型训练时
early_stop设置为True,以便将回调列表(包括 TensorBoard 回调)作为参数包含在内。
当您使用已定义 TensorBoard 回调的训练模型时,您可以在终端中使用以下命令启动 TensorBoard,如下所示。
列表 5.12 调用 TensorBoard 的命令
tensorboard —logdir="C:\personal\manning\deep_learning_for_structured_data\
➥ data\tensorboard_log" ❶
Serving TensorBoard on localhost; to expose to the network,
➥ use a proxy or pass —bind_all
TensorBoard 2.0.2 at http://localhost:6006/ (Press CTRL+C to quit) ❷
❶ 启动 TensorBoard 的命令。logdir 值与 TensorBoard 回调定义中设置的目录相对应。
❷ 命令返回用于启动此训练运行的 TensorBoard 的 URL。
现在如果您在浏览器中打开 TensorBoard 命令返回的 URL,您将看到 TensorBoard 界面。图 5.27 显示了 TensorBoard,其中包含了针对电车延误预测模型训练运行的每个 epoch 的准确率结果。

图 5.27 显示模型准确率的 TensorBoard
在本节中,您已经看到了获取模型信息的三种选项:model .summary(),plot_model 和 TensorBoard。特别是 TensorBoard 具有丰富的功能,用于探索您的模型。您可以在 TensorFlow 文档中找到 TensorBoard 的许多可视化选项的详细描述(www.tensorflow.org/tensorboard/get_started)。
5.15 模型参数
代码包括一组控制模型及其训练过程的参数。图 5.28 总结了这些参数的有效值和用途。关于标准参数(包括学习率、损失函数、激活函数、批量大小和 epoch 数量)的详细描述超出了本书的范围。你可以在mng.bz/Ov8P找到关键超参数的简洁总结。
你可以使用图 5.28 中列出的参数来调整模型的行为。如果你正在将此模型适应不同的数据集,你可能想要从较小的学习率开始,直到你对模型是否收敛到良好的结果有一个概念。你还将想要减少在新的数据集上训练模型的前几次迭代的 epoch 数量,直到你对在训练环境中完成一个 epoch 所需的时间有一个概念。
output_activation参数控制模型是预测一个类别(如电车行程延误/电车行程未延误)还是连续值(如“电车行程延误 5 分钟”)。考虑到电车延误输入数据集的大小,我选择了类别预测模型。但如果你根据第九章的描述将此模型适应不同的数据集,你可能决定使用连续预测,并且你可以调整output_activation参数到一个连续预测的值,例如线性。

图 5.28 你可以在代码中设置的参数以控制模型及其训练
这些参数在配置文件streetcar_model_training_config.yml中定义。第三章介绍了使用 Python 作为配置文件的方式,以将硬编码的值从代码中排除,并使调整参数更快、更少出错。在mng.bz/wpBa的出色文章中对配置文件的价值有很好的描述。
在进行训练迭代时,你很少需要更改图 5.28 中列出的参数。以下是这些参数控制的内容:
-
学习率控制模型训练过程中每迭代权重变化的幅度。如果学习率过高,你可能会跳过最优权重,如果过低,则向最优权重的进展可能非常缓慢。代码中最初设置的学习率值应该是足够的,但你也可以调整此值以查看它如何影响模型的训练进度。
-
辍学率控制了在训练迭代中网络中被忽略的部分比例。正如我们在第 5.11 节中提到的,通过 dropout 旋转,网络中的随机节点子集在网络的正向和反向传播过程中被忽略,以减少过拟合。
-
L2_lambda控制 GRU RNN 层的正则化。此参数仅影响文本输入列。正则化限制了模型中的权重以减少过拟合。通过降低权重的大小,正则化防止模型过多地受到训练集特定特征的影响。您可以将正则化视为使模型更加保守(
arxiv.org/ftp/arxiv/papers/2003/2003.05182.pdf)或更简单。 -
损失函数是模型优化的函数。模型的目标是优化此函数以从模型中获得最佳预测。您可以在Keras 中可用的损失函数选择的全面描述中看到。街车延误模型预测二元选择(是否会在特定行程中延误),因此
binary_crossentropy是loss函数的默认选择。 -
输出激活是模型最后一层中生成最终输出的函数。此值的默认设置
hard_sigmoid将在0和1之间生成输出。 -
批量大小是指在模型更新之前处理的记录数。您通常不需要更新此值。
-
Epochs是通过训练样本的完整遍历次数。您可以调整此值,从较小的值开始以获得初始结果,然后在看到更多 epoch 数时获得更好的结果。根据运行单个 epoch 所需的时间,您可能需要平衡更多 epoch 带来的额外模型性能与运行更多 epoch 所需的时间。
对于定义损失函数和输出激活函数的两个参数,使用 Python 进行深度学习包含一个有用的部分(mng.bz/7GX7),该部分更详细地介绍了这些参数的选项以及您将应用这些选项的问题类型。
摘要
-
构成深度学习模型的代码可能看起来平淡无奇,但它是一个完整解决方案的核心,该方案一端输入原始数据集,另一端输出部署的训练模型。
-
在使用结构化数据集训练深度学习模型之前,我们需要确认模型将要训练的所有列在评分时都将可用。如果我们用我们不会在想要用模型进行预测时拥有的数据进行模型训练,我们可能会得到过于乐观的训练性能,最终得到一个无法生成有用预测的模型。
-
Keras 深度学习模型需要在格式化为 numpy 数组列表的数据上训练,因此数据集需要从 Pandas 数据框转换为 numpy 数组列表。
-
TensorFlow 和 Keras 最初是两个独立但相关的项目。从 TensorFlow 2.0 开始,Keras 已成为 TensorFlow 的官方高级 API,并且其推荐库被打包为 TensorFlow 的一部分。
-
Keras 功能 API 结合了易用性、灵活性和可扩展性,是我们用于电车延误预测模型的 Keras API。
-
电车延误预测模型的层是根据列的类别自动生成的:连续型、分类型和文本型。
-
嵌入(Embeddings)是从自然语言处理领域借鉴的一个强大概念。使用嵌入,您可以将向量与非数值值(如分类列中的值)关联起来。使用嵌入的好处包括能够操作类似于常见数值操作的数值,能够在解决监督学习问题的过程中作为副产品获得对分类范围内值的无监督学习型分类,以及能够避免独热编码(为非数值标记分配数值值的另一种常见方法)的缺点。
-
您可以使用多种方法来探索您的 Keras 深度学习模型,包括
model.summary()(用于表格视图)、plot_model(用于图形视图)和 TensorBoard(用于交互式仪表板)。 -
您可以通过一组参数来控制 Keras 深度学习模型及其训练过程。对于电车延误预测模型,这些参数在 streetcar_model_training_config.yml 配置文件中定义。
6 训练模型和运行实验
本章涵盖
-
查看端到端训练过程
-
选择数据集的子集用于训练、验证和测试
-
进行初始训练运行
-
测量你模型的性能
-
通过利用 Keras 的早期停止功能优化训练时间
-
得分快捷方式
-
保存训练好的模型
-
运行一系列训练实验以改进模型性能
到目前为止,在这本书中,我们已经准备好了数据并检查了构成模型本身的代码。现在我们终于准备好训练模型了。我们将回顾一些基础知识,包括选择训练、测试和验证数据集。然后我们将进行一次初始训练运行以验证代码没有错误,并涵盖监控模型性能的关键主题。接下来,我们将展示如何利用 Keras 中的早期停止功能从训练运行中获得最大收益。之后,我们将介绍如何在模型完全部署之前使用你的训练模型对新记录进行评分。最后,我们将运行一系列实验以改进深度学习模型的表现。
6.1 训练深度学习模型的代码
当你克隆了与本书相关的 GitHub 仓库 (mng.bz/v95x),你会在 notebooks 子目录中找到与训练模型相关的代码。以下列表显示了包含本章描述的代码的文件。
列表 6.1 仓库中与训练模型相关的代码
├── data ❶
│
├── models ❷
│
├── notebooks
│ │ custom_classes.py ❸
│ │ streetcar_model_training.ipynb ❹
│ └── streetcar_model_training_config.yml ❺
│
└── pipelines ❻
❶ 保存中间数据集数据框的目录
❷ 保存训练好的模型的目录
❸ 包含管道类的定义
❹ 包含数据集重构和模型训练代码的笔记本
❺ 模型训练配置文件:超参数值的定义、训练/验证/测试比例以及其他配置参数
❻ 保存管道的目录
6.2 回顾训练深度学习模型的过程
如果你已经训练了一个经典的机器学习模型,训练深度学习模型的过程对你来说应该是熟悉的。本书不涉及经典机器学习模型的详细审查,但 Alexey Grigorev 的 Machine Learning Bookcamp (Manning Publications, 2021) 有一个关于模型训练过程的优秀部分 (mng.bz/Qxxm),更详细地介绍了这个主题。
当深度学习模型被训练时,与每个节点关联的权重会迭代更新,直到损失函数最小化并且模型的预测性能优化。请注意,默认情况下,权重是随机初始化的。
让我们考虑你训练机器学习模型时会采取的高级步骤:
-
定义数据集的子集用于训练、验证和测试。除了用于训练模型的子集之外,还需要为验证(检查训练过程中的性能)和测试(检查训练完成时的性能)保留一个独特的子集。维护一个未用于训练的数据子集来检查模型性能是至关重要的。假设你使用了所有可用的数据来训练你的模型,没有为测试保留数据。在你训练了模型之后,你将如何了解它在预测之前从未见过的数据上的准确性?如果没有验证数据集来跟踪训练过程中的性能,以及测试数据集来评估训练完成时的性能,你将无法在部署之前了解你模型的表现。
-
进行初始运行以验证函数。这个初始运行将确保你的模型代码在功能上是正确的,不会导致任何立即的错误。本书中描述的 Keras 深度学习模型很简单。即便如此,它也经过了多次迭代,才能使模型无错误地完成第一次运行,并清理输入数据最终阶段定义和模型层组装的问题。使你的模型完成一个小型的初始运行,可能只包含一个 epoch(一次训练数据的迭代),是训练模型道路上的一个必要步骤。
-
迭代训练运行。检查性能并进行调整(包括调整超参数,如epoch数量,即训练数据的迭代次数)以获得模型的最佳性能。当你完成了一个初始运行以验证你的模型代码可以无错误运行后,你需要进行重复实验来观察你的模型行为,并看看你可以调整什么来提高性能。这些迭代实验(例如第 6.11 节中描述的实验)涉及一定程度的试错。
-
当你开始训练一个深度学习模型时,你可能会倾向于从具有许多 epochs 的长运行开始(希望如果允许运行足够长的时间,模型将找到其最佳状态)或者同时更改多个超参数(如学习率或批量大小),希望你会找到将提高模型性能的黄金组合的超参数设置。我强烈建议你开始时要慢。从一个小的 epochs 数量(整个训练集的迭代)和一个训练集的子集开始,这样你可以快速完成相当数量的训练运行。请注意,在街车延误预测模型的情况下,训练集不是很大,因此我们可以从一开始就使用整个训练集进行训练,并且在标准 Paperspace Gradient 环境中在不到五分钟内完成 20 个 epochs 的适度运行。
-
随着你的模型性能的提高,你可以进行更长时间的运行,并且有更多的 epochs 来观察额外的迭代是否提高了模型的性能。我也强烈建议你一次只调整一个超参数。
-
保存最佳训练运行的模型。第 6.9 节描述了如何在完成训练运行后显式保存训练好的模型。然而,当你进行更长时间的训练运行并且有更多的 epoch 时,你可能会发现模型在其不是最后一个 epoch 的 epoch 上达到了最佳性能。你如何确保在模型性能最佳的 epoch 上保存模型?第 6.7 节描述了如何使用 Keras 的回调功能在训练运行期间定期保存模型,并在模型性能不再提高时停止训练。有了这个功能,你可以进行长时间的训练运行,并且有信心在运行结束时,你会保存具有最佳性能的模型,即使这种最佳性能是在运行中途实现的。
-
使用测试集验证你的训练模型,并至少评分一个新数据点。你希望通过对一个数据点应用你的训练模型并检查结果来获得部署过程中你将在第八章创建的早期验证。你可以将这种早期验证视为你训练模型的彩排。正如彩排是表演的演员和工作人员在需要面对现场观众之前练习他们的动作的方式一样,将一个数据点应用于你的训练模型是在你进行部署之前了解它将如何表现的一种方式。
-
第 6.8 节描述了如何将你的模型应用于整个测试集,以及如何通过在新数据点上使用它来提前了解模型部署时的行为。这两个活动相关但目标不同。将训练好的模型应用于测试集,基于你拥有的数据集,给你提供了对其性能的最佳感觉,因为你在使用的数据并未参与训练过程。
-
相比之下,一次性评分,或者对来自原始数据集之外的新数据点的评分,迫使你像模型部署时那样使用训练好的模型,但无需完成模型部署所需的所有工作。一次性评分让你快速了解模型部署时的行为,帮助你预测并纠正问题(例如,评分时可能无法获得的数据),在你部署模型之前。
这些步骤在深度学习和经典机器学习之间大体上是相同的。关键的区别是需要跟踪和维护的深度学习模型的超参数数量。查看第五章(图 6.1)中的超参数列表,哪些是专门用于训练深度学习模型的?

图 6.1 超参数列表
在这些超参数中,以下是与深度学习特定的:
-
dropout_rate—这个参数主要与深度学习相关,因为它通过关闭网络中的随机子集来控制过拟合。其他类型的模型也使用 dropout 来控制过拟合。XGBoost(极端梯度提升;mng.bz/8GnZ)模型可以包含 dropout(参见 Dart 增强器的参数xgboost.readthedocs.io/en/latest/parameter.html)。但控制过拟合的 dropout 方法更常应用于深度学习模型。 -
Output_activation—这个参数是专门针对深度学习的,因为它控制着应用于深度学习模型最终层的函数。这个参数与其说是调整参数(控制模型的性能)不如说是功能参数,因为它控制着模型的功能行为。你可以设置输出激活函数,使模型产生二元结果(例如,电车延迟预测模型,预测是否会有特定的电车行程延误),预测一组结果中的一个,或者是一个连续值。
其他参数对深度学习和至少一些经典机器学习方法是通用的。请注意,在这个模型的训练过程中,我手动调整了超参数。也就是说,我一次调整一个超参数,重复进行实验,直到得到满意的结果。这个过程非常棒,因为我能够密切观察调整学习率等变化的影响。但是,手动调整超参数对于业务关键模型来说并不实用。以下是一些展示如何采取更系统方法进行超参数调整的优质资源:
-
《现实世界机器学习》由 Henrik Brink 等人著(Manning Publications,2016) 包含一个描述网格搜索(
mng.bz/X00Y)基础的章节,这是一种搜索每个超参数可能值组合的方法,并评估每个组合的模型性能以找到最佳组合。 -
mng.bz/yrrJ上的文章推荐了一种针对 Keras 模型的端到端超参数调整方法。
6.3 审查电车延误预测模型的整体目标
在我们深入到训练模型涉及的步骤之前,让我们回顾一下电车延误预测模型的整体目的。我们希望预测一次特定的电车旅行是否会遇到延误。请注意,对于这个模型,我们不是预测延误的长度,而是预测是否会延误。以下是只预测延误而不是延误长度的原因:
-
早期从模型中预测电车延误长度(即,线性预测)的实验并不成功。我怀疑数据集太小,无法给模型提供足够的机会来捕捉这种特定的信号。
-
从用户的角度来看,延误的可能性可能比延误的持续时间更为重要。任何超过五分钟的延误都值得考虑电车旅行的替代方案,例如步行、打车或选择其他交通路线。因此,为了简化,对于这个模型的受众来说,预测延误与否的二进制预测比预测延误长度更有用。
现在我们已经回顾了模型将要预测的内容,让我们看看一个具体的用户体验示例。假设用户让模型预测他们现在想要乘坐的旅行,从皇后街电车路线的西行开始,是否会延误。让我们分析可能的结果:
-
模型预测无延误,并且确实没有延误发生。在这种情况下,模型的预测与现实生活中发生的情况相符。我们称这种结果为 真正负例,因为模型预测该事件(西行皇后街电车的延误)不会发生,而事件确实没有发生:没有延误。
-
该模型预测没有延迟,但实际上发生了延迟。 在这种结果下,模型的预测与现实生活中发生的情况不符。这种结果被称为假阴性,因为模型预测该事件(西行皇后街的电车延迟)不会发生,但事件实际上发生了:行程被延误。
-
该模型预测会有延迟,但实际上没有发生延迟。 在这种结果下,模型的预测与现实生活中发生的情况不符。这种结果被称为假阳性,因为模型预测该事件(西行皇后街的电车延迟)将要发生,但事件实际上并没有发生:没有发生延迟。
-
该模型预测会有延迟,并且实际上发生了延迟。 在这种结果下,模型的预测与现实生活中发生的情况相符。这种结果被称为真正阳性,因为模型预测该事件(西行皇后街的电车延迟)将要发生,并且事件确实发生了:行程被延误。
图 6.2 总结了这四种结果。

图 6.2 街车延迟预测的四种可能结果
在四种可能的结果中,我们希望得到尽可能高的真正阴性和真正阳性的比例。但随着我们通过训练模型的迭代过程,我们看到假阴性和假阳性数量之间存在权衡。考虑第 6.10 节中描述的实验 1。在这个实验中,我们没有对模型进行调整以考虑训练数据集的不平衡。延误很少见。训练数据集中所有路线/方向/时间段组合中只有大约 2%有延误。如果我们不考虑到训练数据集的不平衡,训练过程(在实验 1 中是为了优化准确性)将为模型生成权重,导致训练好的模型总是预测“没有延误”。这样的模型看起来好像有很高的准确性:超过 97%。但这个模型对我们用户来说将毫无用处,因为它永远不会预测有延误。尽管延误可能很少见,但模型的使用者需要知道何时可能发生延误,以便他们可以做出替代的旅行安排。
图 6.3 显示了将实验 1 中的模型应用于测试数据集的结果:零个真正阳性和最大可能的假阴性数量。

图 6.3 实验 1 中训练模型应用于测试集的结果
当我们进行第 6.10 节中的实验时,你会看到在误报和漏报之间可能存在权衡。当我们减少漏报(模型预测没有延误而实际上有延误的次数)时,误报可能会增加(模型预测有延误而实际上没有延误的次数)。显然,我们希望模型误报和漏报的比例尽可能低,但如果我们必须在误报和漏报之间做出权衡,对我们用户来说最好的结果是什么?最坏的结果是模型预测没有延误,然后实际上发生了延误。换句话说,对我们用户来说最坏的结果是漏报。如果有误报,如果用户遵循模型的建议步行或乘坐出租车避免从未发生的街车延误,他们仍然有相当的机会准时到达目的地。然而,如果有漏报,如果用户遵循模型的建议乘坐街车,他们就会错过乘坐替代交通方式的机会,并冒着迟到到达目的地的风险。
正如你在第 6.6 节中将会看到的,我们通过监控两个指标来引导模型训练向我们的总体目标迈进:
-
召回率 —真阳性/(真阳性 + 假阴性)
-
验证准确率 —模型在验证数据集上预测正确的比例
既然我们已经回顾了街车延误预测模型训练过程的总体目标,并确定了哪些结果对用户来说最为重要,那么让我们回到模型训练的步骤,从选择用于训练、验证和测试的数据库子集开始。
6.4 选择训练、验证和测试数据集
我们最初工作的原始数据库记录少于 10 万条。根据第五章中描述的重构,我们现在有一个包含超过 200 万条记录的数据库。如第 6.2 节所述,我们需要将数据库划分为以下子集,以便我们有可用于评估模型性能的数据库记录:
-
训练 —用于训练模型的数据库子集
-
验证 —用于在模型训练过程中跟踪模型性能的数据库子集
-
测试 —在训练过程中未使用的数据库子集。训练好的模型应用于测试集,作为模型从未见过的数据的最终验证。
我们应该将数据集的多少比例分配给这些子集?我选择了 60%的数据集用于训练,每个验证和测试集各占 20%。这个比例在拥有足够大的训练集以给模型一个很好的机会提取信号并在训练期间获得良好的性能,以及需要拥有足够大的验证和测试集以在训练过程中未看到的数据上测试模型之间取得了平衡。70/15/15 的比例也是一个合理的选择。对于记录数少于百万的数据集,验证和测试的比例不应低于 10%,以确保在训练迭代(验证集)期间有足够的数据集部分来跟踪性能,以及有足够的保留集(测试集)来应用于训练模型,以确保在它从未见过的数据上具有足够的性能。
6.5 初始训练运行
在对训练运行进行优化更改之前,我们想要先进行一次初始运行,以确保一切功能正常。在这个初始运行中,我们不是试图获得很高的准确率或最小化假阴性。我们将在后续运行中关注模型的性能。对于这次初始训练运行,我们只想确认代码在功能上是可行的——从头到尾执行而不产生错误。
对于你模型的初始运行,你可以通过使用 streetcar_model_training 笔记本,并在 streetcar 模型训练配置文件中指定默认设置来指定运行的超参数值和其他配置设置来跟进。接下来的列表显示了配置文件中的关键值。
列表 6.2 在配置文件中定义的关键参数
test_parms:
testproportion: 0.2 ❶
trainproportion: 0.8
current_experiment: 0 ❷
hyperparameters:
learning_rate: 0.001
dropout_rate: 0.0003
l2_lambda: 0.0003
loss_func: "binary_crossentropy"
output_activation: "hard_sigmoid"
batch_size: 1000
epochs: 50
❶ 为测试数据集保留的数据集比例
❷ 实验编号确定了一组其他参数,包括训练运行中的 epoch 数量以及是否使用提前停止。
下一个列表显示了实验 0 的设置,即初始训练运行。
列表 6.3 实验 0 的设置
early_stop = False ❶
one_weight = 1.0 ❷
epochs = 1 ❸
❶ 实验 0 没有提前停止。
❷ 实验 0 没有考虑到数据集的不平衡性。
❸ 实验 0 包含对训练数据集的单次迭代:1 个 epoch。
下一个列表是触发模型训练的代码块。
列表 6.4 触发模型训练的代码
model = get_model() ❶
if early_stop:
modelfit = model.fit(X_train_list, dtrain.target, epochs=epochs, \ batch_size=batch_size, validation_data=(X_valid_list, \
dvalid.target), class_weight = {0 : zero_weight, 1: one_weight}, \
verbose=1,callbacks=callback_list)
else:
modelfit = model.fit(X_train_list, dtrain.target, epochs=epochs, \
batch_size=batch_size, validation_data=(X_valid_list, dvalid.target), \
class_weight = {0 : zero_weight, 1: one_weight}, verbose=1) ❷
❶ 调用第五章中描述的构建模型的功能。
❷ 因为在实验 0 中 early_stop 被设置为 False,所以这个拟合语句被调用。
让我们更仔细地看看图 6.4 中显示的拟合模型语句。

图 6.4 拟合语句的关键元素
-
训练集和标签(
target)也被确定了。 -
验证集和验证标签(
target)也被确定了。 -
由于数据集存在偏差(没有延迟的路线/方向/时间槽位比有延迟的槽位多得多),我们使用 Keras 功能对输出类别应用权重,以解决这种不平衡。请注意,这里的
weight的使用纯粹是为了补偿输入数据集的不平衡。它与第一章中描述的训练过程中设置的权重无关。
随着fit命令的执行,你将看到如图 6.5 所示的输出。

图 6.5 fit 命令的输出
输出首先回顾了可用于训练和验证模型的样本数量。接下来是每个 epoch(遍历训练数据集)的行,显示了各种测量值。我们将关注这些测量值:
-
损失 —预测值与训练集实际目标值之间的总 delta
-
准确率 (acc) —在本轮预测中匹配训练集实际目标值的预测比例
-
验证损失 (val_loss) —预测值与验证集实际目标值之间的总 delta
-
验证准确率 (val_accuracy) —在本轮预测中匹配验证集实际目标值的预测比例
当fit命令完成后,你将拥有一个训练好的模型。可训练参数已分配值,并且你可以使用该模型对新值进行评分:
Total params: 1,341
Trainable params: 1,201
Non-trainable params: 140
6.6 测量你的模型性能
训练运行执行时产生的输出为你提供了模型性能的初步了解。当训练运行完成后,你有两种简单的方法来检查模型的性能。
检查模型性能的一种方法是将训练和验证损失以及准确率进行绘图。图 6.6 显示了 30 个 epoch 训练运行的训练和验证准确率。

图 6.6 训练和验证准确率图
下一个列表显示了生成准确率和损失图的代码。
列表 6.5 生成准确率和损失图的代码
# acc
plt.plot(modelfit.history['accuracy'])
plt.plot(modelfit.history['val_accuracy']) ❶
plt.title('model accuracy') ❷
plt.ylabel('accuracy') ❸
plt.xlabel('epoch') ❹
plt.legend(['train', 'validation'], loc='upper left') ❺
plt.show() ❻
❶ 指定图表的标题。
❷ 指定图表正在跟踪准确率和 val_accuracy
❸ 指定 y 轴标签。
❹ 指定 x 轴标签。
❺ 指定图例中的标签。
❻ 显示图表。
回想一下 6.3 节中的表格,它显示了预测的四种可能结果,如图 6.7 所示。

图 6.7 街车延迟预测模型的四种可能结果
检查模型性能的另一种方法是混淆矩阵,如图 6.8 所示。sklearn.metrics库包括一个功能,允许你生成这样的表格,显示训练模型运行的真实负例、假正例、假负例和真正例的计数。

图 6.8 混淆矩阵
混淆矩阵有四个象限:
-
左上角 —对于这条路线/方向/时间段,没有延迟,模型也预测没有延迟。
-
左下角 —对于这条路线/方向/时间段,存在延迟,但模型预测没有延迟。
-
右上角 —对于这条路线/方向/时间段,没有延迟,但模型预测了延迟。
-
右下角 —对于这条路线/方向/时间段,存在延迟,并且模型预测了延迟。
混淆矩阵需要一些解释:
-
每个象限中的结果计数以科学记数法表示,因此有 370,000(= 3.7E+05)个真实负例。
-
阴影表示每个象限中的结果绝对数量。阴影越浅,数量越大;阴影越深,数量越小。
-
四个象限沿 x 轴标记为预测(
0/1),以表明模型没有预测延迟(0)和模型预测了延迟(1)。 -
y 轴表示实际发生的情况:没有延迟(
0)和延迟(1)。
尽管在视觉吸引力方面存在不足,混淆矩阵仍然是一个有用的工具,因为它可以将大量信息压缩到一个易于生成的包中。
下面的列表显示了生成混淆矩阵的代码。
列表 6.6 生成混淆矩阵的代码
cfmap=metrics.confusion_matrix(y_true=test['target'], \
y_pred=test["predround"]) ❶
label = ["0", "1"]
sns.heatmap(cfmap, annot = True, xticklabels = label, \
yticklabels = label)
plt.xlabel("Prediction")
plt.title("Confusion Matrix for streetcar delay prediction (weighted)")
plt.show() ❷
❶ 指定实际(y_true)和预测(y_pred)结果。
❷ 显示图表。
我们将使用两个特定的度量来研究在多个训练迭代中街道车延迟预测模型的表现:验证准确率和回收率。验证准确率(在验证数据集上正确预测的比例)将给我们一个关于模型在新数据上整体准确性的指示,但它不会讲述整个故事。如 6.3 节所述,我们希望最小化错误负例的数量(模型在存在延迟时预测没有延迟)。也就是说,我们不希望模型在将发生延迟时错过预测延迟。为了监控这种结果,我们可以跟踪回收率:
true positive / (true positive + false negative)
让我们考虑一下,在 6.3 节中介绍的输出表的情况下,如何用图 6.9 中显示的标签来解释回收率。

图 6.9 回收率 = A / (A + B)
回收率很重要,因为它允许我们跟踪我们想要避免的关键结果:当发生延迟时预测没有延迟。通过同时监控验证准确率和回收率,我们可以得到模型性能的平衡图景。
6.7 Keras 回调:充分利用训练运行
默认情况下,当你调用fit语句启动 Keras 模型的训练运行时,训练运行会持续进行,直到fit语句中指定的 epoch 数。你只能在最后一个 epoch 运行完毕后保存权重(一个训练好的模型)。以其默认行为,你可以将 Keras 训练运行想象成一个工厂,在传送带上生产派。随着每个 epoch 的进行,一个派(训练好的模型)被烘焙。我们的目标是得到最大的派(最优训练模型),如果工厂停止烘焙更大的派,我们希望关闭工厂,而不是浪费原料制作小派。也就是说,如果模型没有改进,我们不想运行很多 epoch。
派工厂烘焙了一系列不同大小的派(代表具有不同性能特性的模型),如图 6.10 所示。

图 6.10 默认的 Keras 训练运行
问题在于,默认的 Keras 训练运行行为,派工厂即使在开始烘焙更小的派(模型性能不再改进)时也会继续烘焙派,而保存的派(在训练运行结束时可以保存的训练模型)是最后一个,即使它不是最大的派。结果是,派工厂可能会浪费很多没有变大的派,而最后得到的派可能是一个小的派,如图 6.11 所示。

图 6.11 在默认训练运行之后,你只能保存最终模型,即使它不是最好的模型。
幸运的是,Keras 提供了回调功能,让你有机会进行更有效的训练。在派工厂的术语中,回调让你做两件事:
-
即使不是最后一个派,也要保存最大的派(最优训练模型),如图 6.12 所示。
![CH06_F12_Ryan]()
图 6.12 保存最大的派。
-
如果派工厂不再烘焙更大的派(生成性能更好的模型),则自动停止派工厂,这样你就不会浪费资源烘焙小派,如图 6.13 所示。
-
![CH06_F13_Ryan]()
图 6.13 如果派工厂停止烘焙更大的派,则提前关闭派工厂。
让我们实际考察一下 Keras 回调是如何工作的。Keras 回调让你能够控制训练运行的时间长度,并在不再改进时停止训练运行。Keras 回调还允许你在满足给定标准(如验证准确度)达到局部最大值后的每个 epoch 中保存模型。通过结合这些功能(提前停止和保存获得最优关键指标测量的模型),你可以实现两个目标:控制训练运行的时间长度,并在停止改进时停止训练运行。
要使用早期停止,我们首先必须定义一个回调(keras.io/callbacks)。一个回调由一组可以在训练运行期间应用的功能组成,以提供对训练过程的洞察。回调允许我们在训练过程中与之交互。我们可以使用回调在训练运行中逐个 epoch 监控性能指标,并根据该性能指标发生的情况采取行动。例如,我们可以跟踪验证准确率,并允许运行在验证准确率继续增加的情况下继续进行。当验证准确率下降时,我们可以让回调停止训练运行。我们可以使用patience选项来延迟停止训练运行,以便在验证准确率不再增加的情况下继续进行给定数量的 epochs。此选项允许我们避免错过在暂时下降或平台期之后发生的验证准确率增加。
从早期停止回调获得的控制权比让训练运行完整地运行所有 epochs 有大幅改进。但是,如果我们关心的性能指标的最佳结果发生在最后一个 epoch 之外的其他 epoch,会发生什么?如果我们简单地保存最终模型,我们可能会错过在中间 epoch 发生的更好的性能。我们可以通过使用另一个回调来解决这种情况,该回调允许我们在训练运行期间持续保存具有我们跟踪的性能指标最佳结果的模型。使用此回调与早期停止回调一起,我们知道在训练运行期间最后保存的模型将具有所有 epochs 中最佳的性能指标结果。
图 6.14 显示了在 streetcar_model_training 笔记本中定义回调的代码片段。

图 6.14 定义回调以跟踪性能指标并保存具有最佳性能的模型
现在让我们通过一个例子来看看回调对训练运行的影响。我们将查看一个 20-epoch 的运行,首先是没有回调的情况,然后是应用了回调的情况。接下来的列表显示了在进行此运行之前需要设置的参数。
列表 6.7 设置的参数以控制早期停止和数据集平衡
early_stop = False ❶
one_weight = (count_no_delay/count_delay) + one_weight_offset ❷
❶ 指定不使用回调。
❷ 考虑数据集中有延迟和无延迟的记录之间的不平衡性。
首先,图 6.15 是此 20-epoch 运行没有早期停止回调的准确率图。

图 6.15 无回调的 20-epoch 运行的准确率图
在这个 20-epoch 的运行之后,最终的val_accuracy值为0.73:
val_accuracy: 0.7300
图 6.16 显示,最终 epoch 产生的val_accuracy并不是训练运行中的最大值。

图 6.16 终端 val_accuracy 与最大 val_accuracy 之间的差异
让我们看看当我们添加早期停止到这个运行时会发生什么,使用下一列表中的代码在验证准确度停止增加时停止运行。
列表 6.8 设置回调的代码
def set_early_stop(es_monitor, es_mode): ❶
callback_list = [] ❷
es = EarlyStopping(monitor=es_monitor, mode=es_mode, \ ❸
➥ verbose=1,patience = patience_threshold)
callback_list.append(es)
model_path = get_model_path()
save_model_path = os.path.join(model_path, \
➥ 'scmodel'+modifier+"_"+str(experiment_number)+'.h5') ❹
mc = ModelCheckpoint(save_model_path, monitor=es_monitor, \ ❺
➥ mode=es_mode, verbose=1, save_best_only=True)
callback_list.append(mc)
if tensorboard_callback: ❻
tensorboard_log_dir =
os.path.join(get_path(), \
"tensorboard_log",datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard = TensorBoard(log_dir= tensorboard_log_dir)
callback_list.append(tensorboard)
return(callback_list,save_model_path)
if early_stop:
modelfit = model.fit(X_train_list, dtrain.target, epochs=epochs, \
➥ batch_size=batch_size, validation_data=(X_valid_list, dvalid.target), \
➥ class_weight = {0 : zero_weight, 1: one_weight}, \
➥ verbose=1,callbacks=callback_list) ❼
❶ 定义 Keras 回调的函数。参数是 es_monitor(在 mc 回调中跟踪的测量)和 es_mode(在 es 早期停止回调中跟踪的极端最小值或最大值)。
❷ 包含在此函数中定义的所有回调的列表
❸ 定义基于 es_monitor 测量不再按 es_mode 指示的方向移动的 es 回调,并将其添加到回调列表中
❹ 在训练过程中,当测量 es_monitor 达到新的最优值时,定义保存模型的路径
❺ 定义基于 es_monitor 测量达到由 es_mode 定义的最佳值的最佳模式保存的 mc 回调
❻ 如有必要,定义 TensorBoard 回调。有关定义 TensorBoard 回调的详细信息,请参阅第五章。
❼ 如果 early_stop 设置为 true,fit 命令将使用回调参数调用 set_early_stop()函数返回的回调列表。
要获取早期停止回调,我们设置下一列表中的参数。
列表 6.9 设置早期停止回调的参数
early_stop = True ❶
es_monitor="val_accuracy" ❷
es_mode = "max" ❸
❶ 指定回调包含在 fit 语句的调用中。
❷ 指定 val_accuracy 是控制回调的测量
❸ 指定 val_accuracy 的最大值被回调跟踪
设置了这些参数后,我们重新运行实验,这次回调在训练过程中被调用。您可以在图 6.17 中看到结果。

图 6.17 带有早期停止回调的准确度
由于验证准确度下降,训练运行在 2 个 epoch 后停止,而不是完整的 20 个 epoch。这个结果并不是我们想要的;我们希望模型有改进的机会,我们当然不希望准确度一下降就停止训练。为了得到更好的结果,我们可以在模型训练配置文件中将patience_threshold参数设置为除默认0以外的值:
patience_threshold: 4
如果我们在早期停止回调中添加patience_threshold参数重新运行相同的训练练习会发生什么?运行在 12 个 epoch 后停止,而不是 2 个,并且需要三分钟来完成。终端验证准确度为 0.73164,如图 6.18 所示。

图 6.18 带有患者早期停止的准确度
这些更改(添加回调和将patience_threshold参数设置为非零值)的结果是更好的验证准确度,需要更少的 epoch 和更少的时间来完成训练。图 6.19 总结了这组实验的结果。

图 6.19 回调实验总结
这些实验表明,Keras 中的回调功能是一种通过避免对关键测量没有带来益处的重复 epoch 来提高训练运行效率的强大方式。回调功能还允许你保存模型迭代,该迭代为你提供了关键测量的最优值,无论这个迭代发生在训练运行的哪个位置。通过调整耐心参数,你可以平衡运行更多 epoch 的成本与在暂时下降后获得更好性能的潜在益处。
6.8 从多次训练运行中获得相同的结果
你可能想知道第 6.7 节中展示的实验是如何在训练运行之间获得一致结果的。默认情况下,训练过程的各个方面,包括分配给网络节点的初始权重,都是随机设置的。如果你使用具有相同输入和参数设置的深度学习模型运行重复实验,你会看到这种效果;即使输入相同,每次运行也会得到不同的结果(例如每个 epoch 的验证准确率)。如果在训练深度学习模型中存在有意引入的随机元素,我们如何控制这个元素,以便我们可以进行重复的、受控的实验来评估特定变化的影响,例如第 6.7 节示例中引入的回调?关键是设置随机数生成器的固定种子。随机数生成器为训练过程提供随机输入(例如模型的初始权重),导致训练运行之间结果不同。当你想要从多次训练运行中获得相同的结果时,你可以显式设置随机数生成器的种子。
如果你查看模型训练笔记本的配置文件,你会在test_parms部分看到一个名为repeatable_run的参数(参见下一列表)。
列表 6.10 控制测试执行的参数
test_parms:
testproportion: 0.2 # proportion of data reserved for test set
trainproportion: 0.8 # proportion of non-test data dedicated \
to training (vs. validation)
current_experiment: 9
repeatable_run: True ❶
❶ 控制我们是否想要重复实验的参数。我们是否希望用固定值对随机数生成器进行种子设置,以获得多次运行的一致结果?
模型训练笔记本使用repeatable_run参数来确定是否显式设置随机数生成器的种子,从而从多次训练运行中生成相同的结果:
if repeatable_run:
from numpy.random import seed
seed(1)
tf.random.set_seed(2)
在本节中,我们总结了如何从多次训练运行中获得相同的结果。有关使用 Keras 获得可重复结果的更多详细信息,请参阅优秀的文章mng.bz/Moo2。
6.9 得分捷径
当我们有一个训练好的模型时,我们希望能够对其进行练习。让我们快速回顾一下高级步骤:
-
训练模型。使用本章中描述的过程,模型通过反复遍历训练数据集,迭代地设置模型中的权重,以最小化损失函数。在我们的案例中,损失函数衡量模型对延迟/不延迟的预测与训练数据集中每个路线/方向/时间槽组合的实际延迟/不延迟结果之间的差异。
-
使用模型评分(一次性评分)。获取训练好的模型对新数据点的预测:对于模型在训练过程中从未见过的路线/方向/时间槽组合,预测是延迟/不延迟。这些新数据点可以来自原始数据集的测试子集或全新的数据点。
-
部署模型。使训练好的模型可用于对新数据点提供高效的预测。除了描述部署之外,第八章还描述了部署和一次性评分之间的区别。
正如你在第八章中将会看到的,部署模型可能需要几个步骤。在完成完整的部署过程之前,你想要能够使用模型进行一些评分,以验证模型在训练过程中未见过的数据上的性能。本节描述了在全面模型部署之前可以采用的评分捷径。
要在完整的测试集上测试模型,你可以用测试集作为输入调用模型的 predict 方法:
preds = saved_model.predict(X_test, batch_size=BATCH_SIZE)
如果你想测试一个单一的新测试示例呢?这是部署模型的典型用例:一个客户端使用模型来确定他们想要乘坐的电车行程是否会被预测为延迟。
要对一个单一数据点进行评分,我们首先需要检查模型输入的结构。X_test 的结构是什么?
print("X_test ", X_test)
X_test {'hour': array([18, 4, 11, ..., 2, 23, 17]),
'Route': array([ 0, 12, 2, ..., 10, 12, 2]),
'daym': array([21, 16, 10, ..., 12, 26, 6]),
'month': array([0, 1, 0, ..., 6, 2, 1]),
'year': array([5, 2, 3, ..., 1, 4, 3]),
'Direction': array([1, 1, 4, ..., 2, 3, 0]),
'day': array([1, 2, 2, ..., 0, 1, 1])}
X_test 是一个字典,其中每个值都是一个 numpy 数组。如果我们想对一个单一的新数据点进行评分,我们可以创建一个字典,其中包含一个字典中每个键的单个条目 numpy 数组:
score_sample = {}
score_sample['hour'] = np.array([18])
score_sample['Route'] = np.array([0])
score_sample['daym'] = np.array([21])
score_sample['month'] = np.array([0])
score_sample['year'] = np.array([5])
score_sample['Direction'] = np.array([1])
score_sample['day'] = np.array([1])
现在我们已经定义了一个单一的数据点,我们可以使用训练好的模型来获取这个数据点的预测:
preds = loaded_model.predict(score_sample, batch_size=BATCH_SIZE)
print("pred is ",preds)
print("preds[0] is ",preds[0])
print("preds[0][0] is ",preds[0][0])
对于我们训练的其中一个模型,我们得到以下输出:
pred is [[0.35744822]]
preds[0] is [0.35744822]
preds[0][0] is 0.35744822
因此,对于这个单一数据点,模型不预测延迟。能够对一个单一数据点进行评分是快速验证模型的好方法,尤其是如果你已经准备了代表你认为不太可能延迟的行程的数据点,以及代表你认为将会延迟的行程的数据点。通过让训练好的模型对这些两个数据点进行评分,你可以验证训练好的模型是否做出了你预期的预测。
图 6.20 显示了两个可以评分以锻炼训练模型的示例行程。我们预计行程 A(在周末晚些时候的较不繁忙的路线上)不会延迟,而行程 B(在高峰时段的繁忙路线上)有很高的可能性会延迟。

图 6.20 通过评分单个行程来锻炼模型
6.10 明确保存训练好的模型
与 Pandas 数据框一样,训练好的模型仅在 Python 会话的生命周期内存在,除非你将其保存。你需要能够序列化和保存你的训练模型,以便你可以在以后加载它来进行实验,并最终部署它,这样你可以方便地使用训练好的模型对新数据进行评分。如果你设置了early_stopping参数,模型将作为回调的一部分保存。如果没有,下一列表中的代码块将保存模型。
列表 6.11 如果不使用早期停止,保存模型的代码
if early_stop == False: ❶
model_json = model.to_json()
model_path = get_model_path()
with open(os.path.join(model_path,'model'+modifier+'.json'), \
➥ "w") as json_file:
json_file.write(model_json) ❷
model.save_weights(os.path.join(model_path, \
➥ 'scweights'+modifier+'.h5'))
save_model_path = os.path.join(model_path, \
➥ 'scmodel'+modifier+'.h5') ❸
model.save(save_model_path,save_format='h5') ❹
saved_model = model
❶ 通过回调检查模型是否已经被保存。
❷ 将模型保存到 JSON 文件中。
❸ 将训练好的模型权重保存到 h5 文件中。
❹ 将模型和权重保存到 h5 文件中。
你可以练习加载你保存到 h5 文件中的模型(见下一列表)。
列表 6.12 从 h5 文件加载模型的代码
from keras.models import load_model
loaded_model = load_model(os.path.join(model_path, \
'scmodel'+modifier+'.h5')) ❶
❶ 使用与保存模型相同的路径加载你保存的模型。
现在你已经加载了保存的模型,你可以将其应用于测试集以获取预测:
preds = loaded_model.predict(X_test, batch_size=BATCH_SIZE)
6.11 运行一系列训练实验
现在我们通过运行一系列实验来汇总所有内容,这些实验基于本章到目前为止所学的内容。你可以通过更改模型训练配置文件中的current_experiment参数(列表 6.13)来运行这些实验,如下所示。
列表 6.13 控制测试执行的参数
test_parms:
testproportion: 0.2 # proportion of data reserved for test set
trainproportion: 0.8 # proportion of non-test data \
dedicated to training (vs. validation)
current_experiment: 5 ❶
repeatable_run: True # switch to control whether \
runs are repeated identically
get_test_train_acc: False # switch to control whether \
block to get test and train accuracy is after training)
❶ 设置实验编号。
current_experiment参数反过来用于在调用set_experiment_parameters函数时设置实验的参数:
experiment_number = current_experiment
early_stop, one_weight, epochs,es_monitor,es_mode = set_experiment_parameters
➥ (experiment_number, count_no_delay, count_delay)
图 6.21 总结了这些实验的参数设置以及关键结果:验证准确率、测试集上的假阴性数量和召回率。

图 6.21 一组训练模型实验结果的总结
这些实验层层叠加了各种技术,从增加额外的训练轮次到对较少出现的延迟结果进行加权,再到早期停止。这些实验由一系列参数的设置值定义,如下所示。
列表 6.14 控制编号实验的参数
if experiment_number == 1:
#
early_stop = False
#
one_weight = 1.0
#
epochs = 10
elif experiment_number == 2:
#
early_stop = False
#
one_weight = 1.0
#
epochs = 50
elif experiment_number == 3:
#
early_stop = False
#
one_weight = (count_no_delay/count_delay) + one_weight_offset
#
epochs = 50
elif experiment_number == 4:
#
early_stop = True
es_monitor = "val_loss"
es_mode = "min"
#
one_weight = (count_no_delay/count_delay) + one_weight_offset
#
epochs = 50
elif experiment_number == 5:
#
early_stop = True
es_monitor = "val_accuracy"
es_mode = "max"
#
one_weight = (count_no_delay/count_delay) + one_weight_offset
#
epochs = 50
在这些实验中,我们将调整训练轮次的数量、延迟结果的加权以及早期停止回调。对于每个实验,我们将跟踪以下性能指标:
-
终端验证准确率 — 运行的最后一个训练轮次的验证准确率
-
总误判负例数 —模型预测无延迟而实际上存在延迟的次数
-
召回率 —真阳性数 / (真阳性数 + 误判负例数)
当我们对实验的参数进行更改时,性能指标会提高。这一组实验对于像电车延迟问题这样简单的问题是有用的,但它并不能代表在现实世界的深度学习问题中可能需要的实验量。在一个工业强度的模型训练情况下,你可能会包括更多种类的实验,这些实验会改变更多参数的值(例如,第五章中描述的学习率、dropout 和正则化参数)。如果你从原始模型中没有获得所需的表现指标,你还可以调整模型中层数的数量和类型。
当你刚开始接触深度学习时,如果你在原始架构中没有看到足够的表现,可能会倾向于对这些模型架构进行这类更改。我建议你首先专注于理解原始模型的表现特性,从少量 epoch 的测试运行开始,逐个调整参数,并在整个运行过程中测量一致的性能指标(如验证准确率或验证损失)。然后,如果你已经用尽原始架构的性能改进,仍然没有达到你的性能目标,考虑更改模型架构。
让我们回顾一下本节中定义的五个训练实验中的每一个。从实验 1 开始,我们运行了少量 epoch,没有考虑到训练数据中延迟和非延迟之间的不平衡,也没有使用回调。准确率的结果看起来不错,但图 6.22 所示的混淆矩阵揭示了发生的情况:模型总是预测无延迟。

图 6.22 实验一的混淆矩阵
由于这个模型永远不会预测延迟,所以它对我们想要应用这个模型的应用场景将没有用。图 6.23 显示了在 epoch 增加到五倍时实验 2 发生的情况。

图 6.23 实验二的混淆矩阵
随着 epoch 的增加,模型开始预测一些延迟,但误判负例的数量是真阳性数量的两倍,所以这个模型没有达到最小化误判负例的目标。
在实验 3 中,我们通过加权延迟(图 6.24)来考虑数据集中延迟和非延迟之间的不平衡。

图 6.24 实验三的混淆矩阵
通过这次更改,我们看到了比误判负例更多的真阳性,所以这是提高召回率的正确方向,但我们还可以做得更好。
在实验 4 中,我们添加了回调。训练过程将监控验证损失:验证集预测值与实际值之间的累积差异。如果验证损失在给定数量的 epoch 后没有下降,则训练运行将停止。此外,具有最低验证损失的 epoch 的模型将在训练运行结束时被保存。再次强调,随着这个变化,正如图 6.25 所示,真正阳性和假阴性(如召回率所示)的比例增加,但我们仍然可以做得更好。

图 6.25 实验 4 的混淆矩阵
在实验 5 中,我们也有回调,但不是监控验证损失,而是监控验证准确率。如果准确率在给定数量的 epoch 后没有提高,我们将停止训练。我们将在训练运行结束时保存具有此测量值最大值的模型。图 6.26 显示了此运行的混淆矩阵。

图 6.26 实验 5 的混淆矩阵
在实验 5 中,真正阳性和假阴性(如召回率所反映)的比例甚至更好,验证准确率也有所提高。
注意,当您使用相同的输入运行这些实验时,您可能会得到不同的结果。但您应该看到与这些五个实验中所示的变化相同的总体趋势。此外,请注意,您可以采取额外的步骤来获得更好的结果:更高的准确率和更好的真正阳性和假阴性比例。我们将在第七章中检查其中的一些步骤。
摘要
-
训练深度学习模型是一个迭代的过程。通过在训练运行期间和结束时跟踪正确的性能度量,您将能够系统地调整训练过程中涉及的参数,看到变化的效果,并朝着满足训练过程目标的有训练模型迈进。
-
在开始训练过程之前,您需要定义数据集的子集用于训练、验证(跟踪训练过程中的模型性能)和测试(评估训练模型的性能)。
-
对于您的初始训练运行,选择一个简单、单 epoch 的运行来验证一切是否正常工作,并且没有功能性故障。当您成功完成这个初始运行后,您可以进行更复杂的训练运行以提高模型的性能。
-
Keras 提供了一套测量方法,您可以使用这些方法来评估您模型的性能。您选择哪一种取决于您的训练模型将如何被使用。对于电车延误预测模型,我们通过验证准确率(训练模型在验证集上的预测与验证集的实际延误/无延误值匹配的程度)和召回率(模型避免在发生延误时预测无延误的程度)来评估性能。
-
默认情况下,Keras 的训练运行会遍历所有指定的周期,您得到的训练模型是最终周期产生的模型。如果您希望通过避免不必要的周期并确保保存最佳模型来提高训练过程的效率,您需要利用回调功能。使用回调,您可以在您关心的性能测量停止改进时停止训练过程,并确保从您的训练运行中保存最佳模型。
-
当您有一个训练好的模型时,尝试用它评分几个数据点是个好主意。在电车延误预测模型的情况下,您会用您的训练模型评分一些时间/路线/方向组合。这样做为您提供了一种在投入大量努力进行模型全面部署之前验证训练模型整体行为的方法。
7 使用训练模型的更多实验
本章涵盖
-
验证移除不良值是否可以提高模型性能
-
验证分类列的嵌入是否可以提高模型性能
-
提高模型性能的可能方法
-
比较深度学习模型与非深度学习模型的性能
在第六章中,我们训练了深度学习模型并做了一系列实验来衡量和改进其性能。在本章中,我们将进行一系列额外的实验来验证模型的两个关键方面:移除不良值(这是我们作为第三章和第四章中描述的数据准备的一部分所采取的步骤)以及包括分类列的嵌入(如第五章所述)。然后我们将描述一个实验,比较使用电车延误预测深度学习模型的深度学习解决方案与使用称为 XGBoost 的非深度学习方法的解决方案。
7.1 模型更多实验的代码
当你克隆了与本书相关的 GitHub 仓库(mng.bz/v95x)时,你将在 notebooks 子目录中找到与实验相关的代码。以下列表显示了本章中描述的实验所使用的文件。
列表 7.1 存储库中与模型训练实验相关的代码
├── data ❶
│
├── models ❷
│
├── notebooks
│ │ custom_classes.py ❸
│ │ streetcar_model_training.ipynb ❹
│ │ streetcar_model_training_xgb.ipynb ❺
│ └── streetcar_model_training_config.yml ❻
│
└── pipelines ❼
❶ 存储中间数据集的目录
❷ 保存训练模型的目录
❸ 包含管道类定义
❹ 包含数据集重构和深度学习模型训练代码的笔记本
❺ 包含数据集重构和 XGBoost 模型训练代码的笔记本
❻ 模型训练配置文件:超参数值的定义、训练/验证/测试比例以及其他配置参数。请注意,我们使用一个通用的配置文件来训练深度学习模型和 XGBoost 模型。
❼ 保存管道的目录
7.2 验证移除不良值是否可以提高模型
回到第四章,我们回顾了数据集中不良值记录的数量——那些在一列中的值无效的记录。输入记录可能有一个不存在的路线值,例如,或者一个不是罗盘方向的值。默认情况下,我们在 streetcar_data_preparation 笔记本的末尾保存输出数据框之前移除这些值。我们想要进行一个实验来验证这个选择对于模型性能来说是最好的选择。以下是这样的一个实验:
-
使用以下值设置 streetcar_data_preparation_config.yml 重新运行 streetcar_data_preparation 笔记本,以保存包含不良值记录的清洁输出数据框,如下一列表所示。
列表 7.2 数据准备配置中不良值实验的参数
general: load_from_scratch: False save_transformed_dataframe: True ❶ remove_bad_values: False ❷ file_names: pickled_input_dataframe: 2014_2019.pkl pickled_output_dataframe: 2014_2019_df_cleaned_no_remove_bad_values_xxx.pkl ❸❶ 指定输出数据框应保存。
❷ 指定不良值不应从输出数据框中移除。
❸ 为输出数据框设置一个唯一的文件名。
-
使用以下值在 streetcar_model_training_config.yml 中重新运行 streetcar_model_training 笔记本,以使用控制文件重构数据集,该数据集包括带有“坏路线”和“坏方向”组合的路线/方向,如下所示。
列表 7.3 恶值实验的参数设置
pickled_dataframe: \ ➥ '2014_2019_df_cleaned_no_remove_bad_values_xxx.pkl' ❶ route_direction_file: 'routedirection_badvalues.csv' ❷❶ 在数据准备配置文件中指定与 pickled_output_dataframe 相同的文件名。
❷ 包含“坏路线”和“坏方向”组合的控制文件。
现在我们使用这些更改(使用包含坏值的输入数据集)从第六章运行实验 5,我们得到图 7.1 中所示的结果。验证准确率没有太大差异,但使用包含坏值输入数据集训练的模型的召回率要差得多。

图 7.1 比较训练数据集中有无坏值时的模型性能
总体而言,我们从排除坏值的训练数据集训练的模型中获得了更好的性能。这个实验证实了我们在第四章中决定默认排除模型训练过程中的坏值记录的决定。
7.3 验证列嵌入是否提高了模型的性能
嵌入在我们的第五章中创建并在第六章中训练的深度学习模型中起着重要作用。该模型为所有分类列包含嵌入层。作为一个实验,我只移除了这些层,并训练了模型,以比较其具有和没有分类列嵌入层的性能。
为了进行这个实验,我在 streetcar_model_training 笔记本中的模型构建部分替换了这两行
embeddings[col] = (Embedding(max_dict[col],catemb) (catinputs[col]))
embeddings[col] = (BatchNormalization() (embeddings[col]))
与以下行
embeddings[col] = (BatchNormalization() (catinputs[col]))
并重新运行了第六章中描述的实验 5。这个实验是一个 50 个 epoch 的训练运行,基于验证准确率定义了提前停止。图 7.2 显示了带有和不带有分类列嵌入的实验运行结果。

图 7.2 比较具有和没有嵌入层的分类列的模型性能
当从模型中移除分类列的嵌入层时,每个性能指标都变得非常糟糕。这个例子展示了嵌入在像我们为电车延迟定义的简单深度学习模型中的价值。
7.4 比较深度学习模型与 XGBoost
本书的主张是,值得考虑将深度学习视为在结构化、表格数据上执行机器学习的一个选项。在第六章中,我们在 streetcar delay 数据集上训练了一个深度学习模型,并检查了模型的表现。如果我们用同样的 streetcar delay 数据集训练一个非深度学习模型会怎样呢?在本节中,我们将展示这种实验的结果。我们将用 XGBoost 替换深度学习模型,XGBoost 是一种在处理涉及结构化、表格数据的机器学习问题中赢得“首选”机器学习方法的声誉的梯度提升决策树算法。我们将比较这两个模型的结果,并确定这些结果告诉我们关于深度学习作为解决涉及结构化数据问题的解决方案的可行性的什么信息。
就像一本关于蝙蝠侠的书如果没有描述小丑就不会完整一样,一本关于使用结构化数据深度学习的书如果没有提到 XGBoost 也会显得不完整。在处理结构化、表格数据方面,XGBoost 是深度学习的宿敌,并且它是最常被推荐用来处理结构化数据的替代深度学习的方法。
XGBoost 是一种非深度学习机器学习,称为梯度提升机。在梯度提升中,从一组简单模型中聚合预测以获得一个综合预测。值得注意的是,XGBoost 提供了一套与深度学习模型不完全相同的特性。XGBoost 内置了一个特征重要性功能(mng.bz/awwJ),可以帮助你确定每个特征对模型贡献的大小,尽管这个功能应该谨慎使用,正如 mng.bz/5pa8上的文章所展示的。XGBoost 的所有特性的详细描述超出了本书的范围,但《商业机器学习》(Machine Learning for Business)中的 XGBoost 部分(mng.bz/EEGo)提供了一个优秀且易于理解的 XGBoost 工作原理的描述。
为了比较深度学习模型和 XGBoost 之间的差异,我更新了模型训练笔记本 streetcar_model_training,将深度学习模型替换为 XGBoost。我的意图是尽量减少对代码的修改。如果你把整个模型训练笔记本比作一辆车,我希望在不改变车身面板、车轮、轮胎、内饰或其他任何车辆部件的情况下,更换现有的引擎(深度学习模型)并安装另一个引擎(XGBoost),如图 7.3 所示。

图 7.3 替换深度学习引擎为 XGBoost 引擎
当我让新引擎工作后,我想把车开到赛道上,评估与原引擎相同的车的驾驶感受。如果我在车上保持其他一切不变,只更换引擎,我可以期望得到一个公平的比较,比较两种引擎的驾驶感受。同样,我希望通过将笔记本中的代码更改保持在最低限度,我能够得到深度学习模型和 XGBoost 之间的公平比较。
您可以在 streetcar_model_training_xgb 笔记本中找到训练 XGBoost 模型的代码,如果您检查这个笔记本,您会发现汽车类比是成立的:我更换了引擎,但汽车的其他部分保持不变。这个笔记本的前部分与深度学习模型训练笔记本 streetcar_model_training 相同,除了包含 XGBoost 模型的导入语句:
from xgboost import XGBClassifier
在调用管道的主块之后开始 XGBoost 特定的内容。此时,数据集是一个 numpy 数组的列表,每个数据集的列对应一个 numpy 数组:
[array([ 9, 13, 6, ..., 11, 8, 2]),
array([20, 22, 13, ..., 6, 16, 22], dtype=int64),
array([4, 4, 1, ..., 0, 2, 0]),
array([ 2, 18, 14, ..., 24, 11, 21], dtype=int64),
array([0, 2, 3, ..., 3, 1, 2], dtype=int64),
array([0, 5, 4, ..., 3, 6, 0], dtype=int64),
array([ 2, 10, 11, ..., 4, 6, 7], dtype=int64)]
深度学习训练代码中的多输入 Keras 模型期望这种格式。然而,XGBoost 期望数据集是一个列表的 numpy 数组,因此在我们能够使用这些数据训练 XGBoost 模型之前,我们需要将其转换为 XGBoost 所期望的格式。我们首先将训练和测试数据集从 numpy 数组的列表转换为列表的列表,如下所示。
列表 7.4 将训练和测试数据集转换为列表的列表的代码
list_of_lists_train = []
list_of_lists_test = []
for i in range(0,7): ❶
list_of_lists_train.append(X_train_list[i].tolist())
list_of_lists_test.append(X_test_list[i].tolist())
❶ 对于训练和测试数据集,遍历 numpy 数组,并将它们转换为列表,最终得到两个列表的列表。
接下来,对于测试和训练数据集,我们将列表转换为 numpy 数组,并转置 numpy 数组:
xgb_X_train = np.array(list_of_lists_train).T
xgb_X_test = np.array(list_of_lists_test).T
下面是生成的训练数据集 xgb_X_train 的样子:
array([[ 9, 20, 4, ..., 0, 0, 2],
[13, 22, 4, ..., 2, 5, 10],
[ 6, 13, 1, ..., 3, 4, 11],
...,
[11, 6, 0, ..., 3, 3, 4],
[ 8, 16, 2, ..., 1, 6, 6],
[ 2, 22, 0, ..., 2, 0, 7]])
作为列表的 numpy 数组从管道中输出的数据集已经被转换成列表的 numpy 数组,并且内容已经转置——这正是我们在下一个块中训练 XGBoost 模型所需要的,如下所示。
列表 7.5 训练 XGBoost 模型的代码
model_path = get_model_path()
xgb_save_model_path = \
os.path.join(model_path, \
'sc_xgbmodel'+modifier+"_"+str(experiment_number)+'.txt') ❶
model = XGBClassifier(scale_pos_weight=one_weight) ❷
model.fit(xgb_X_train, dtrain.target) ❸
model.save_model(xgb_save_model_path) ❹
y_pred = model.predict(xgb_X_test) ❺
xgb_predictions = [round(value) for value in y_pred]
xgb_accuracy = accuracy_score(test.target, xgb_predictions) ❻
print("Accuracy: %.2f%%" % (xgb_accuracy * 100.0))
❶ 构建训练好的 XGBoost 模型将保存的路径。
❷ 定义 XGB 模型对象,使用所有参数的默认值,除了 scale_pos_weight,该参数用于解决正(延迟)和负(无延迟)目标之间的不平衡。此值与用于解决深度学习模型不平衡的值相同。
❸ 使用我们转换成列表的 numpy 数组的训练数据集来拟合模型。
❹ 保存训练好的模型。
❺ 将训练好的模型应用于测试数据集。
❻ 计算模型的准确率。
现在我们已经看到了为了让模型训练笔记本与 XGBoost 一起工作需要做出哪些改变,那么当我们训练和评估 XGBoost 模型时发生了什么?图 7.4 总结了使用 XGBoost 和深度学习进行的比较训练和评估运行的结果,以及两种方法之间的高级差异。

图 7.4 XGBoost 与 Keras 深度学习模型比较
-
性能 —XGBoost 模型在没有任何调整的情况下,性能就优于深度学习。在测试集的准确性方面,深度学习的最高记录是 78.1%,而 XGBoost 是 80.1%。在召回率和假阴性数量(如我们在第六章中所述,这个因素是用户最终体验中模型性能的关键)方面,XGBoost 也表现更好。将图 7.5 中 XGBoost 的混淆矩阵与图 7.6 中高精度深度学习运行的混淆矩阵进行比较,可以看出 XGBoost 领先。
![CH07_F05_Ryan]()
图 7.5 XGBoost 混淆矩阵
![CH07_F06_Ryan]()
图 7.6 深度学习混淆矩阵
-
训练时间 —深度学习模型的训练时间比 XGBoost 的训练时间更依赖于硬件。在一个普通的 Windows 系统上,XGBoost 的训练时间大约为 1.5 分钟,而深度学习模型运行实验 5 的时间大约为 3 分钟。但深度学习实验 5 的训练时间(50 个 epoch,提前停止的耐心参数设置为 15)会根据耐心参数(在训练运行停止之前,优化性能测量,如验证准确性,停止改进时运行的 epoch 数量)和环境中的硬件而大幅变化。尽管 XGBoost 有更短的训练时间,但差距足够小,深度学习模型的训练性能变化足够大,因此我会将这种比较称为平局。
-
代码复杂度 —在复杂度方面,深度学习模型的拟合代码与 XGBoost 之间几乎没有区别。在拟合语句之前的代码有所不同。在拟合代码之前,深度学习模型在
get_model()函数中有复杂的代码来构建模型本身。如第五章所述,该函数为不同类型的输入列组装不同的层。XGBoost 不需要这个复杂的代码块,但它需要额外的步骤将数据集从深度学习模型所需的 numpy 数组列表格式转换为 XGBoost 所需的 numpy 数组列表格式。我也可以将这一类别称为平局,尽管可以争论说 XGBoost 更简单,因为其所需的数据准备代码比模型构建代码简单。但模型构建代码是深度学习在最后一个类别中的评分的关键部分。 -
灵活性 —— 如第五章所述,深度学习模型被构建来与各种结构化数据集一起工作。由于 XGBoost 被重新整合到代码中,它受益于这种灵活性,并且其在 streetcar_model_training_xgb 笔记本中的实现也将与各种结构化数据集一起工作。有一个重要的例外:深度学习模型将能够处理包含自由文本列的数据集。如第四章所述,街车延误数据集没有这样的列,但它们在许多结构化数据集中很常见。
-
例如,考虑一个追踪在线鞋类零售网站销售物品的表格。这个表格可以包括连续列(如价格)和分类列(如颜色和鞋码)。它还可以包括每个物品的自由文本描述。深度学习模型能够整合并从这样的文本列中获取训练数据。XGBoost 需要从其分析中排除这个列。在这个重要的方面,深度学习模型比 XGBoost 更灵活。
值得注意的是,我能够以最小的额外工作将 streetcar_model_training 中的代码调整为与 XGBoost 一起工作。当我开始训练 XGBoost 模型时,我发现,除了将 scale_pos_weight 参数设置为考虑到输入数据集中“无延误”记录比“延误”记录多得多的数量之外,无需其他调整,XGBoost 模型始终优于深度学习模型。
比较深度学习和 XGBoost 在街车延误预测问题上的最终结论是什么?XGBoost 的性能优于深度学习模型,并且它相对容易集成到我为深度学习模型创建的现有代码结构中。回到汽车的比喻,XGBoost 是一个容易安装到汽车中并使其运行的引擎。这意味着比较的结论必须与传统的智慧一致,即非深度学习方法——特别是 XGBoost——对于结构化数据问题比深度学习更好吗?
如果我们将电车延误预测问题冻结在当前状态,答案可能确实是肯定的,但在现实世界情况下,期望模型不会发生变化则过于天真。正如你在第九章中将会看到的,有许多选项可以通过考虑额外的数据集来扩展和改进电车延误预测模型,一旦我们考虑这些改进,我们就会遇到 XGBoost 的限制。例如,任何包含自由文本列(如第九章中描述的天气数据集)的数据集都很容易被整合到深度学习模型中,但无法直接整合到 XGBoost 中。XGBoost 可能不适合第九章中描述的电车延误预测问题的某些扩展;它也不适合任何结构化数据中包含任何类型的 BLOB 数据(techterms.com/definition/blob)的问题,例如图像、视频或音频。如果我们想要一个适用于真正广泛范围表格化、结构化数据的通用解决方案,XGBoost 可能不是正确答案。尽管 XGBoost 在特定应用上的性能可以击败深度学习,但深度学习具有利用结构化数据全范围的灵活性。
7.5 改进深度学习模型的可能下一步
在第六章中描述的所有实验之后,我们最终得到一个训练好的模型,在测试集上的准确率略高于 78%,召回率为 0.79。为了在后续迭代中提高性能,我们可以采取哪些步骤?以下是一些想法:
-
调整特征组合。在深度学习模型中使用的特征集相对有限,以尽可能简化训练过程。可以添加的一个特征是基于第四章中地址生成的纬度和经度值的地理空间测量。正如第九章所述,你可以为每条路线使用边界框将其划分为横向段(例如,每条路线 10 个部分),并将这些段作为模型的一个特征。这种方法可以将延误隔离到每条路线的特定子集中,并可能提高模型的表现。
-
调整延误的阈值 . 如果构成延误的阈值太小,模型将不太有用,因为短暂的延误将被计为事件。另一方面,如果阈值设置得太高,模型的预测将失去价值,因为对旅客构成不便的延误将不会被捕捉到。这个边界在模型训练配置文件中的
targetthresh参数中设置。调整这个阈值可能会提高模型性能,尤其是在输入数据随时间演变的情况下。如第三章所述,多年来,延误的整体趋势是更加频繁但时间更短,因此,targetthresh参数的较小值值得探索。 -
调整学习率 . 学习率控制每次训练迭代中权重调整的程度。如果设置得太高,训练过程可能会跳过最小损失点。如果设置得太低,训练进度会变慢,你将消耗比训练模型所需更多的时间和系统资源。在模型训练过程的早期阶段,我调整了学习率,并确定了目前模型训练配置文件中的值,因为它产生了稳定的结果。进一步调整学习率的实验可能会提高模型性能。
模型达到了我们最初为其设定的性能目标(至少 70%的准确率),但总有改进的空间。上述列表显示了一些可以调整深度学习模型以改进其性能的潜在调整。如果你已经完成了到这一点的代码,我鼓励你尝试本节中的建议,并运行实验以查看这些调整是否提高了模型的性能。
摘要
-
默认情况下,我们从数据集中丢弃具有不良值的记录。你可以进行一个实验来验证这个选择,并证明在移除不良值后训练的模型比保留不良值的模型具有更好的性能。
-
默认情况下,模型包含分类列的嵌入。你可以进行一个实验来确认并展示包含分类列嵌入的模型比没有分类列嵌入的模型具有更好的性能。
-
XGBoost 目前是处理涉及表格化、结构化数据的机器学习问题的默认方法。你可以在一个模型训练笔记本的版本上执行一系列实验,其中深度学习模型已被 XGBoost 替换。
-
XGBoost 在街车延误预测问题上确实比深度学习有更好的性能,但性能并不是唯一要考虑的因素,在灵活性这一关键领域,深度学习胜过 XGBoost。
8 部署模型
本章涵盖
-
模型部署概述
-
部署与一次性评分的比较
-
为什么部署是一个难题
-
部署模型步骤
-
管道简介
-
部署后的模型维护
在第六章中,我们经历了迭代训练深度学习模型以预测电车延误的过程,在第七章中,我们进行了一系列实验来探索模型的行为。现在我们有一个训练好的模型,我们将探讨两种部署模型的方法,或者换句话说,使电车用户能够获得关于他们的电车行程是否会延误的预测。首先,我们将概述部署过程。接下来,我们将对比第六章中引入的一次性评分与部署。然后,我们将通过两种方法(一个网页和 Facebook Messenger)具体说明部署模型的具体步骤。接下来,我们将描述如何使用管道封装数据准备过程,并回顾实现电车延误预测模型管道的细节。我们将以回顾如何维护已部署的模型来结束本章。
注意:在本章中,为了避免混淆我们在第六章中为预测电车延误而训练的 Keras 深度学习模型和在第 2 种部署方法中使用的 Rasa 聊天机器人模型,如果有任何歧义,我们将把前者称为 Keras 模型。
8.1 模型部署概述
部署是使深度学习模型变得有用的关键步骤。部署意味着使我们的训练模型在开发环境之外对用户或其他应用程序可用。换句话说,部署是我们需要做的所有事情,以便我们的训练模型对外界有用。部署可能意味着通过 REST API 使模型可供其他应用程序使用,或者在我们这个案例中,直接使它对希望了解他们的电车行程是否会延误的用户可用。
如果我们回顾第四章的端到端图,部署涵盖了图 8.1 的右侧。

图 8.1 从原始数据集到部署的训练模型的端到端流程
在本章中,我们将使用两种技术来部署我们的训练模型:
-
网页部署 —— 这种最小化部署使用 Flask (
flask.palletsprojects.com/en/1.1.x),这是一个基本的 Python 网页应用框架,用于提供网页,用户可以指定他们的行程参数并查看模型的预测。此解决方案包括用于 Flask 服务器的 Python flask_server.py 文件和相关代码,以及两个 HTML 文件以获取评分参数(如路线、方向和时间)并显示结果(home.html和show-prediction.html)。HTML 页面 home.html 包含 JavaScript 函数以收集评分参数(如路线、方向和时间)。这些评分参数传递给 flask_server.py 中的 Python 代码,该代码将管道应用于评分参数,并将训练好的模型应用于管道的输出。默认情况下,Flask 在本地主机上提供网页。在第九章中,我们将描述如何使用 ngrok (ngrok.com)使本地主机上提供的网页可供无法访问您的开发系统的用户使用。 -
Facebook Messenger 部署 —— 网页部署很简单,但用户体验并不理想。为了提供更好的用户体验,我们还将使用在 Facebook Messenger 中公开的 Rasa 聊天机器人来部署我们的模型。为了部署我们的模型,我们将结合在第六章中完成的训练模型,并将其集成到 Rasa 的 Python 层中,同时整合管道以准备新的数据点,以便模型进行预测。用户将通过 Facebook Messenger 输入他们的请求,以确定特定的电车行程是否会延误。我们的 Rasa 聊天机器人将解析这些请求,并将行程信息(路线、方向和日期/时间)传递给与 Rasa 聊天机器人关联的 Python 代码。此 Python 代码(评分代码)将管道应用于行程信息,将训练好的 Keras 模型应用于管道的输出,并根据训练好的 Keras 模型的预测结果编写响应。最后,这个响应将通过 Facebook Messenger 返回给用户。
8.2 如果部署如此重要,为什么它如此困难?
部署是实验模型和能够提供益处的模型之间的区别。遗憾的是,在关于深度学习的入门材料中,部署往往被忽略,甚至专业的云服务提供商也还没有能够使部署变得简单。为什么情况会是这样?
部署之所以困难,是因为它涉及各种技术主题,这些主题远远超出了我们在本书中迄今为止所涵盖的深度学习堆栈。要在工业级的生产环境中部署一个模型,您必须与一个广泛的技术堆栈合作,这可能包括 Azure 或 AWS 这样的云平台,Docker 和 Kubernetes 进行容器化和编排,REST API 为您的训练模型提供一个可调用的接口,以及 Web 基础设施为您的模型提供一个前端。这个堆栈复杂且技术要求高。要进行甚至是最基本的、简单的 Web 部署(如第 8.5 节和第 8.6 节所述),您需要与 Web 服务器、HTML 和 JavaScript 合作,所有这些都超出了您迄今为止关于机器学习(特别是深度学习)所学的所有内容。
在本章中,我们介绍了两种对比的部署方法:网站部署和 Facebook Manager 部署。网站部署相对容易,但用户体验并不理想地适合预测电车行程延误的问题。模型的真实用户不太可能想要去一个单独的网站来查看他们的电车是否会延误。但他们可能会非常乐意在 Facebook Messenger 中简短地聊天来获取相同的信息。这两种部署选项都是免费的,并使用开源堆栈(除了 Facebook Messenger 本身)。这两种部署选项还允许您完全从自己的本地系统进行部署,同时为其他人提供访问权限,以便与他们共享模型结果。
8.3 一次性评分回顾
在第六章中,您学习了如何将新的数据记录应用于训练模型以获得预测。我们称这种快速执行训练模型的方法为“一次性评分”。您可以通过练习 Python 文件one_off_scoring.py来查看如何手动准备单个数据点并使用训练模型为该数据点获得预测(也称为评分)的示例。
要理解全面部署的含义,让我们对比一下一次性评分与全面部署。图 8.2 总结了一次性评分。

图 8.2 Python 会话中一次性评分的总结
你在 Python 会话的上下文中进行一次性评分,并且你需要手动准备你想要评分的数据点。而不是直接处理你想要评分的值——例如route = 501、direction = westbound和time = 1:00 pm today——你需要一个已经通过所有数据转换的数据点,例如将整数值分配给替换501作为路线值。此外,你必须将数据点组装成模型期望的结构:一个 numpy 数组的列表。当你已经以所需的格式准备了一个数据点并应用了训练好的模型来获取其预测时,你可以在 Python 会话中显示预测结果。正如你所看到的,一次性评分适合对训练好的模型进行快速合理性测试,但由于一次性评分是在 Python 会话中进行的,并且需要手动准备输入数据,因此它不是在规模上测试你的模型或使其可供最终用户使用的方法。
8.4 使用 Web 部署的用户体验
对于电车延误预测问题,我们需要一种简单的方法让用户指定他们想要获取延误预测的行程,以及一种简单的方法来展示模型的预测。Web 部署是实现这一目标的最简单方式。
在第 8.5 节中,我们将详细介绍为您的训练有素的深度学习模型设置 Web 部署的细节,但首先,让我们回顾一下完成 Web 部署后的用户体验(如图 8.3 所示):
-
用户访问
home.html(由 Flask 在localhost:5000上提供)并选择他们想要预测的行程的详细信息:路线、方向、年份、月份、月份中的某一天、星期中的某一天和小时。 -
用户点击“获取预测”按钮。
-
预测结果在
show-prediction.html中显示。 -
用户可以点击“获取另一个预测”返回到
home.html并输入关于另一个行程的详细信息。![CH08_F03_Ryan]()
图 8.3 使用 Web 部署的用户体验
8.5 使用 Web 部署部署您的模型步骤
在第 8.4 节中,我们探讨了 Web 部署模型的用户体验。本节将引导你通过设置本地 Web 部署的训练有素模型的步骤。
如第 8.4 节所述,Web 部署依赖于 Flask 来提供部署所需的网页和相关代码。本书不涉及 Flask 的端到端描述,但如果你想要了解更多关于这个易于使用的 Python 网络应用程序框架的背景信息,mng.bz/oRPy上的教程提供了一个优秀的概述。
Flask 不是 Python 中网络服务的唯一选择。Django(www.djangoproject.com)是另一个 Python 网络应用程序框架,它以 Flask 的简单性换取了更丰富的功能集。有关 Flask 和 Django 的良好比较,请参阅mng.bz/nzPV。对于电车延误预测项目,我选择了 Flask,因为我们不需要复杂的网络应用程序来部署模型,而且 Flask 更容易上手。
当你克隆了与本书相关的 GitHub repo (mng.bz/v95x)后,你将看到以下列表中的目录结构。
列表 8.1 与部署相关的代码
├── data
├── deploy
│ ├── data
│ ├── input_sample_data
│ ├── keras_models
│ ├── models
│ ├── pipelines
│ ├── test_results
│ └── __pycache__
├── deploy_web
│ ├── static
│ │ └── css
│ ├── templates
│ └── __pycache__
├── models
├── notebooks
│ ├── .ipynb_checkpoints
│ └── __pycache__
├── pipelines
└── sql
下一个示例中显示的与网络部署相关的文件位于 deploy_web 子目录中。
列表 8.2 与网络部署相关的代码
│ custom_classes.py ❶
│ deploy_web_config.yml ❷
│ flask_server.py ❸
│
├── static
│ └── css
│ main.css ❹
│ main2.css ❺
│
└── templates
home.html ❻
show-prediction.html ❼
❶ 包含管道类定义
❷ 网络部署配置文件:管道文件名、模型文件名和调试设置
❸ Flask 服务器的主 Python 文件,以及将管道和训练模型应用于用户关于他们的电车之旅的输入参数
❹ 用于显示 HTML 文件显示特性的 CSS 文件
❺ 交替 CSS 文件(用于开发期间强制更新)
❻ 用于输入评分参数的 HTML 文件
❼ 用于显示评分结果的 HTML 文件
在文件就绪后,以下是使网络部署工作所需的步骤:
-
前往本地实例的 repo 中的 deploy_web 子目录。
-
编辑 deploy_web_config.yml 配置文件以指定你想要用于部署的训练模型和管道文件。如果你是按照第六章中的说明自己创建的模型和管道文件,请确保你使用的是来自同一运行的管道和模型文件,如下一个列表所示。
列表 8.3 网络部署配置文件中要设置的参数
general: debug_on: False logging_level: "WARNING" BATCH_SIZE: 1000 file_names: pipeline1_filename: sc_delay_pipeline_dec27b.pkl ❶ pipeline2_filename: sc_delay_pipeline_keras_prep_dec27b.pkl model_filename: scmodeldec27b_5.h5 ❷❶ 将参数 pipeline1_filename 和 pipeline2_filename 的值替换为你想要使用的管道文件名——位于 deploy_web 子目录的兄弟目录 pipelines 子目录中的 pickle 文件。对于管道文件和模型文件,只需指定文件名;其余路径将由 flask_server.py 生成。
❷ 将 model_filename 参数的值替换为你保存想要使用的训练模型的文件名——位于 models 子目录中的 h5 文件。
-
如果你还没有这样做,请输入以下命令来安装 Flask:
pip install flask -
输入以下命令以启动 Flask 服务器和相关代码:
python flask_server.py -
在浏览器中输入此 URL 以加载 home.html:
localhost:5000 -
如果一切正常,你会看到 home.html,如图 8.4 所示。
![CH08_F04_Ryan]()
图 8.4 在浏览器中加载 localhost:5000 时显示的 home.html
-
进行一次合理性测试。通过选择路线、方向和时间/日期参数的值来设置评分参数,然后点击获取预测。此点击将启动可能需要一些时间的处理(加载管道、加载训练模型以及将评分参数通过管道和训练模型运行)。因此,如果这一步需要几秒钟才能完成,请耐心等待。
-
如果您的 Web 部署成功,您将看到显示预测结果的
show-prediction.html页面,如图 8.5 所示。![CH08_F05_Ryan]()
图 8.5 Web 部署成功的合理性测试
-
如果您想尝试另一组评分参数,请点击获取另一个预测以返回
home.html,在那里您可以输入新旅行的评分参数。
就这些了。如果您已经达到这个阶段,您已经成功部署了一个训练好的深度学习模型。如您所见,即使这个简单的部署也要求我们使用一组之前在这本书中没有使用过的技术,包括 Flask、HTML 和 JavaScript。正如您将在第 8.8 节中看到的那样,为了通过在 Facebook Messenger 中部署模型来获得更流畅的用户体验,我们需要一个更大的组件集。这种对技术组件集的需求说明了第 8.2 节中提出的问题:部署深度学习模型并不容易,因为当前的部署需要一套与准备数据集和训练模型所使用的技术截然不同的技术栈。
如果您想与他人分享您的部署,可以使用 ngrok 将本地系统上的 localhost 对本地系统外的用户开放,如第九章所述。请注意,如果您使用 ngrok 的免费版本,您一次只能运行一个 ngrok 服务器,因此您无法同时运行 Web 部署和 Facebook Messenger 部署。
8.6 Web 部署背后的场景
让我们更详细地看看 Web 部署背后的情况。图 8.6 显示了从用户在home.html中输入他们计划乘坐的电车旅行的详细信息到用户在show-prediction.html中获得响应的流程。以下列表提供了图 8.6 中编号步骤的更多详细信息。

图 8.6 Web 部署中从查询到答案的往返流程
-
在由 Flask 在 localhost:5000 上提供的
home.html网页中,用户通过从路线、方向和时间/日期的下拉列表中选择详细信息来选择他们的电车旅行;然后用户点击获取预测按钮。 -
home.html 中的 JavaScript 函数
getOption()提取用户在下拉列表中选择的评分参数,并构建包含这些评分参数的 URL。JavaScript 函数link_with_args()将与获取预测按钮关联的链接设置为包含在getOption()中构建的 URL,如下一列表所示。列表 8.4 JavaScript 函数 getOption()的代码
function getOption() { selectElementRoute = document.querySelector('#route'); ❶ selectElementDirection = document.querySelector('#direction'); selectElementYear = document.querySelector('#year'); selectElementMonth = document.querySelector('#month'); selectElementDaym = document.querySelector('#daym'); selectElementDay = document.querySelector('#day'); selectElementHour = document.querySelector('#hour'); route_string = \ selectElementRoute.options[selectElementRoute.selectedIndex].value ❷ direction_string = \ selectElementDirection.options[selectElementDirection.\ selectedIndex].value year_string = \ selectElementYear.options[selectElementYear.selectedIndex].value month_string = \ selectElementMonth.options[selectElementMonth.selectedIndex].value daym_string = \ selectElementDaym.options[selectElementDaym.selectedIndex].value day_string = \ selectElementDay.options[selectElementDay.selectedIndex].value hour_string = \ selectElementHour.options[selectElementHour.selectedIndex].value // build complete URL, including scoring parameters prefix = "/show-prediction/?" ❸ window.output = \ prefix.concat("route=",route_string,"&direction=",direction_string,\ "&year=",year_string,"&month=",month_string,"&daym=",daym_string,\ "&day=",day_string,"&hour=",hour_string) ❹ document.querySelector('.output').textContent = window.output; } function link_with_args(){ getOption(); ❺ console.log("in link_with_args"); console.log(window.output); window.location.href = window.output; ❻ }❶ 为每个评分参数创建 querySelector 对象。
❷ 将每个评分参数的值加载到 JS 变量中。
❸ 设置目标 URL 的前缀。
❹ 将每个评分参数值作为参数添加到目标 URL 中。
❺ 调用 getOption()构建目标 URL。目标 URL 将类似于这样:/show-prediction/?route=501&direction=e&year=2019&month=1&daym=1&day=6&hour=5。
❻ 将目标 URL 设置为与获取预测按钮关联的链接的目标。
-
flask_server.py 包括视图函数(
mng.bz/v9xm)——处理组成部署的 HTML 文件的 Flask 模块中的不同路由/URLs 的函数——为每个 HTML 文件。show-prediction 视图函数包含下一列表中的评分代码。列表 8.5 用于 show-prediction 视图函数的代码
@app.route('/') ❶ def home(): title_text = "Test title" title = {'titlename':title_text} return render_template('home.html',title=title) ❷ @app.route('/show-prediction/') ❸ def about(): score_values_dict = {} score_values_dict['Route'] = request.args.get('route') ❹ score_values_dict['Direction'] = request.args.get('direction') score_values_dict['year'] = int(request.args.get('year')) score_values_dict['month'] = int(request.args.get('month')) score_values_dict['daym'] = int(request.args.get('daym')) score_values_dict['day'] = int(request.args.get('day')) score_values_dict['hour'] = int(request.args.get('hour')) loaded_model = load_model(model_path) ❺ loaded_model._make_predict_function() pipeline1 = load(open(pipeline1_path, 'rb')) ❻ pipeline2 = load(open(pipeline2_path, 'rb')) score_df = pd.DataFrame(columns=score_cols) ❼ for col in score_cols: ❽ score_df.at[0,col] = score_values_dict[col] prepped_xform1 = pipeline1.transform(score_df) ❾ prepped_xform2 = pipeline2.transform(prepped_xform1) pred = loaded_model.predict(prepped_xform2, batch_size=BATCH_SIZE) ❿ if pred[0][0] >= 0.5: ⓫ predict_string = "yes, delay predicted" else: predict_string = "no delay predicted" prediction = {'prediction_key':predict_string} ⓬ # render the page that will show the prediction return(render_template('show-prediction.html', \ prediction=prediction)) ⓭❶ home.html 的视图函数;这是当用户导航到 localhost:5000 时执行的功能。
❷ home.html 的视图函数渲染网页。
❸ show-prediction.html 的视图函数;这是当用户在 home.html 中点击获取预测链接时执行的功能。
❹ 将从 URL 中加载的参数(由 home.html 中的 link_with_args() JavaScript 函数加载的评分参数)加载到 Python 字典中。
❺ 加载训练好的模型。注意,model_path 在 flask _server.py 中之前已构建,使用从部署配置文件 deploy_web_config.yml 中加载的值。
❻ 加载管道对象。
❼ 创建一个包含评分参数的数据框。
❽ 将评分参数加载到数据框中。
❾ 将管道应用于评分参数数据框。
❿ 将训练好的模型应用于管道的输出以获得预测。
⓫ 将预测转换为字符串。
⓬ 创建一个用于输出预测字符串的字典。
⓭ 使用预测字符串作为参数渲染 show-prediction.html。
-
Flask 提供 show-prediction.html,显示由 flask_server.py 中的 show-prediction.html 视图函数生成的预测字符串。
本节提供了一些关于当你使用 Web 部署时幕后发生的事情的细节。这个 Web 部署的目的是展示一个简单但完整的部署。你可能已经看到了(尤其是如果你是一个经验丰富的 Web 开发者)改进 Web 部署的机会。例如,将预测显示在 home.html 而不是单独的页面上会很好。同样,提供一个按钮在 home.html 中让用户指定他们现在想要出行也是一个很好的主意。为了使部署尽可能简单,我在这个 Web 部署中选择了简单的一侧。第 8.7-8.10 节描述了一个更优雅的部署,在我看来,它更适合街车延误预测问题。尽管如此,这里描述的 Web 部署提供了一个简单的结构,你可以通过一些对 HTML 和 JavaScript 的修改来适应其他机器学习模型的基本部署。
8.7 Facebook Messenger 部署的用户体验
Web 部署的用户体验很简单,但它有一些严重的限制:
-
用户必须访问一个特定的网站。
-
他们必须输入他们旅行的所有信息。没有任何假设。
-
旅行参数的输入和预测出现在不同的网页上。
我们可以通过花费更多时间来细化它,直接在 Web 部署中解决所有这些问题,但有一种更好的方法来提升用户体验:在 Facebook Messenger 中进行部署。图 8.7 展示了在 Facebook Messenger 部署中用户获取旅行预测是多么简单。

图 8.7 使用部署的模型得到的新数据点
将用户在 Facebook Messenger 部署中的评分体验与他们在 Web 部署中的体验进行对比。使用通过 Facebook Messenger 部署的模型,用户只需在 Facebook Messenger 中输入一个英文句子,就能得到预测。用户可以提供最少的信息,仍然可以得到预测。最好的是,用户在 Facebook Messenger 中输入请求并得到预测,这是这种轻量级交互的自然环境。
通过 Facebook Messenger 和 Rasa 部署模型的好处之一是灵活性。在图 8.8 中,查询对被标记为 1 到 4 的数字。考虑带有相同数字的查询对。这些对中的每个查询都有相同的意思,而 Rasa 模型能够检测到这一点,尽管这些对中的每个成员的措辞有所不同。Rasa 模型从 Rasa 模型训练中使用的例子中获得了部分这种能力,这些例子包括 nlu.md(单句示例)和 stories.md(多句示例)。这两组训练示例赋予了 Rasa 解析特定于电车行程的语言方面的能力。

图 8.8 Rasa 模型正确评估查询对为相同。
使用 Rasa 编写的聊天机器人的能力很大一部分来自于利用 Rasa 的默认自然语言处理(NLP)能力。值得一提的是,Rasa 的 NLP 能力基于深度学习。因此,深度学习推动了端到端解决方案的两个部分(包括本章中描述的 Facebook Messenger 部署),如图 8.9 所示:
-
我们在这本书中一直在创建的电车延误预测深度学习模型
-
我们通过将 Rasa 作为电车延误模型部署的一部分来获取的 NLP 深度学习
-
![CH08_F09_Ryan]()
图 8.9 深度学习驱动 Facebook Messenger 部署的端到端解决方案的两个部分。
8.8 Facebook Messenger 部署背后的场景
当用户在 Facebook Messenger 中输入有关电车行程的问题时,幕后发生了什么?图 8.10 显示了从用户输入的查询到在 Facebook Messenger 中显示的响应的流程。以下列表提供了图 8.10 中编号步骤的更多详细信息:
-
当用户在 Facebook Messenger 中用英语输入查询时,该查询会被一个简单的 Rasa 聊天机器人捕获。
-
Rasa 将一个 NLP 模型应用于查询以获取用户想要预测的行程的关键值(称为 槽位),这些值指定了关于行程的详细信息:路线名称或编号、方向和时间。
-
Rasa 将这些槽位值传递给一个自定义动作类(actions.py 中评分代码的一部分),该类是用 Python 编写的。该类中的代码解析槽位值,并为任何空槽位设置默认值。特别是,如果用户在他们的查询中没有指定任何时间信息,自定义动作将星期几、月份和年份设置为当前日期和时间。
-
自定义操作准备行程细节,使用与准备训练数据相同的管道。(有关管道的更多背景信息,请参阅第 8.11-8.13 节。)然后,自定义操作通过在准备好的行程细节上调用训练好的深度学习模型来对这些细节进行评分。
-
最后,自定义操作在 Facebook Messenger 中向用户发送一个响应。
![CH08_F10_Ryan]()
图 8.10 使用 Facebook Messenger 部署的从查询到答案的往返过程
8.9 关于 Rasa 的更多背景信息
对 Rasa 聊天机器人框架的全面审查超出了本书的范围。此外,仓库包含您为模型部署所需的全部更新后的 Rasa 文件。当您按照第 8.10 节中的步骤操作后,您不应需要更新任何与 Rasa 相关的文件。但如果您想了解更多关于 Rasa 如何工作的细节,本节提供了关于 Rasa 基本概念的额外背景信息,以及一些指向更详细信息的方法。
Rasa 是一个开源的聊天机器人开发框架,允许您使用自然语言界面创建和训练聊天机器人。它提供了一套简单的接口,让您可以利用其内置的 NLP,而无需处理训练 NLP 模型的细节。Rasa 与 Python 连接,让您能够编写复杂的动作以响应用户输入。它还支持连接到各种消息平台,包括 Facebook Messenger。总之,Rasa 框架为我们提供了从自然语言解释到 Python 连接再到 Facebook Messenger 的最终用户界面的所有简单部署所需的一切。
Rasa 界面是围绕一系列聊天机器人概念构建的:
-
意图 —用户输入的目标,例如获取预测。
-
动作 —聊天机器人系统可以执行的动作。一个简单的动作可能是一个预先准备好的文本响应(例如在问候时返回
hello)。在我们的深度学习模型部署中,我们将动作定义为 Python 中的actions.py文件中的ActionPredictDelayComplete类。此动作接受 Rasa 从用户输入中提取的槽位值,填写未由槽位指定的值,将值通过管道运行,将管道的输出应用于训练好的模型,并最终根据训练模型的预测为用户编写响应。 -
槽位 —一组键和值,用于捕获用户的基本输入。在我们的深度学习模型部署中,为模型期望的所有输入列(路线、方向、小时、月份等)定义了槽位。
-
故事 —用户与聊天机器人之间对话的抽象,可以表示多次来回交流。在我们深度学习模型部署中的主要故事是一个简单的交流:用户询问一次旅行是否会延误,机器人提供响应以指示延误或无延误。
图 8.11 显示了用于训练 Rasa 模型的关键文件以及每个文件中定义的 Rasa 对象。当您训练 Rasa 模型(您将在第 8.10 节中这样做)时,Rasa 使用 nlu.md 和 stories.md 文件中的训练数据来训练模型。

图 8.11 定义在 Rasa 中的关键文件和对象
Rasa 框架中的另一个关键文件是 actions.py,该文件包含 Python 自定义操作。如果 Facebook Messenger 是部署的漂亮面孔,Rasa 的 NLP 能力是其可爱的声音,那么 actions.py 就是部署的大脑。让我们更详细地看看 actions.py 中的代码,该代码获取 Rasa 设置的槽位值。
Rasa 与 actions.py 之间的连接是 actions.py 中自定义类中的跟踪器结构。tracker.get_slot() 方法允许您获取 Rasa 设置的槽位值,或者如果 Rasa 没有设置槽位值,则为 None。actions.py 中的此循环遍历从 Rasa 传递的槽位值,并加载与槽位值相对应的评分数据框列,如果没有设置槽位值,则使用默认值,如下一列表所示。
列表 8.6 从 Rasa 加载槽位值到数据框的代码
for col in score_cols:
if tracker.get_slot(col) != None: ❶
if tracker.get_slot(col) == "today":
score_df.at[0,col] = score_default[col] ❷
else:
score_df.at[0,col] = tracker.get_slot(col) ❸
else:
score_df.at[0,col] = score_default[col] ❹
❶ 如果设置了槽位,则使用其值。
❷ 如果日期由 Rasa 设置为今天,则使用默认值,即当前日期。
❸ 否则,对于将要评分的数据框,将值设置为等于 Rasa 的槽位值。
❹ 如果 Rasa 没有设置值(例如日期和时间),则使用默认值,即当前时间/日期。
本节简要概述了 Rasa 的一些关键概念。有关更多详细信息,您可以在 rasa.com 上了解 Rasa 及其架构。
8.10 使用 Rasa 在 Facebook Messenger 中部署您的模型的步骤
本节描述了使用 Facebook Messenger 部署您的模型的步骤。完成这些步骤后,您将拥有一个部署的深度学习模型,您可以从 Facebook Messenger 中查询它。
当您克隆与本书相关的 GitHub 仓库 (mng.bz/v95x) 时,您将在 deploy 子目录中找到与 Facebook Messenger 部署相关的文件,如下所示。
列表 8.7 与 Facebook Messenger 部署相关的代码
│ actions.py ❶
│ config.yml ❷
│ credentials.yml ❸
│ custom_classes.py ❹
│ deploy_config.yml ❺
│ domain.yml ❻
│ endpoints.yml ❼
│ one_off_scoring.py
│ __init__.py
│
├── data
│ nlu.md ❽
│ stories.md ❾
│
└── models ❿
❶ 包含 Rasa 模型自定义操作的文件
❷ Rasa 配置文件
❸ Rasa 凭证文件
❹ 包含管道类的定义
❺ actions.py 的配置文件:管道文件名、模型文件名和调试设置
❻ Rasa 领域文件:指定意图、槽位和操作
❼ Rasa 端点文件:指定自定义操作的端点 URL。
❽ Rasa nlu.md 文件:Rasa 模型的单轮对话训练数据
❾ Rasa stories.md 文件:Rasa 模型的多轮对话训练数据
❿ 包含 Rasa 模型的目录
在步骤 1 到 4 中,您将通过安装 Python(如果您尚未在本地系统上安装它)和 Rasa 开源聊天机器人环境来完成基本设置:
-
如果您本地系统上尚未安装 Python 3.7,请安装它 (
www.python.org/downloads)。注意:Python 3.8 中 TensorFlow 依赖项与 Rasa 存在问题,因此请确保您使用的是 Python 3.7,以避免在 Rasa 安装步骤中出现问题。另外请注意,Rasa 与 TensorFlow 2 不兼容,因此您用于 Facebook Messenger 部署的 Python 环境需要与您用于训练 Keras 模型的 Python 环境分开。
-
安装开源聊天机器人框架 Rasa (
rasa.com/docs/rasa/user-guide/installation):pip install rasa -
如果您在 Windows 上,并且 Rasa 安装失败,显示需要 C++ 的消息,您可以下载并安装 Visual C++ Build Tools (
mng.bz/4BA5)。安装构建工具后,重新运行 Rasa 安装: -
前往您克隆的仓库中的部署目录。
-
在部署目录中运行以下命令以设置基本的 Rasa 环境:
rasa init -
在您的部署目录中运行以下命令以在 Rasa 的 Python 环境中调用 actions.py。如果您收到任何关于缺少库的消息,请运行
pip install以添加缺少的库:rasa run actions -
在步骤 6 到 13 中,您将设置 ngrok(用于连接本地系统上的部署环境与 Facebook Messenger)并设置您需要连接到 Facebook Messenger 的 Facebook 应用和 Facebook 页面:
-
安装 ngrok (
ngrok.com/download)。 -
在您安装 ngrok 的目录中,调用 ngrok 使您的本地主机在端口 5005 上可供 Facebook Messenger 使用。以下是 Windows 的命令:
.\ngrok http 5005 -
记下 ngrok 输出的 https 转发 URL,如图 8.12 所示;您将需要该 URL 来完成步骤 13。
![CH08_F12_Ryan]()
图 8.12 调用 ngrok 的输出
-
在部署目录中运行以下命令以训练 Rasa 模型:
rasa train -
按照以下说明
mng.bz/Qxy1添加新的 Facebook 应用。记下页面访问令牌和应用程序密钥;您需要在步骤 11 中使用这些值更新 credentials.yml 文件。 -
更新部署目录中的 credentials.yml 文件,以设置验证令牌(您选择的字符串值)和密钥以及页面访问令牌(在步骤 10 中您设置的 Facebook 应用设置期间提供):
facebook: verify: <verify token that you choose> secret: <app secret from Facebook app setup> page-access-token: <page access token from Facebook app setup> -
在部署目录中运行以下命令以启动 Rasa 服务器,使用您在步骤 11 中设置的 credentials.yml 中的凭据:
rasa init -
在第 10 步中创建的 Facebook 应用中,选择 Messenger -> 设置,滚动到 Webhooks 部分,并点击编辑回调 URL。将回调 URL 值的初始部分替换为你在第 7 步调用 ngrok 时记录的 https 转发 URL。在验证令牌字段中输入你在第 11 步设置的 credentials.yml 中的验证令牌,然后点击验证并保存,如图 8.13 所示。
![CH08_F13_Ryan]()
图 8.13 设置 Facebook Messenger 的 webhook 回调 URL
-
最后,在第 14 步和第 15 步中,验证你在 Facebook Messenger 中的部署:
-
在 Facebook Messenger(移动或网页应用)中,搜索你在第 10 步中创建的 Facebook 页面的 ID,并向该 ID 发送以下消息:
Will Queen west be delayed -
如果你的部署成功,你将看到如图 8.14 所示的响应,这是从我本地系统提供的。不用担心预测是否为延迟;只需确认你收到了响应。
![CH08_F14_Ryan]()
图 8.14 使用 Facebook Messenger 成功进行模型部署的合理性测试
8.11 管道简介
现在我们已经完成了模型的部署过程,我们需要回顾一个重要的过程部分,它使得准备用户输入以便模型能够对其生成预测成为可能:管道。通过使用管道,我们可以将完全相同的准备步骤(例如,将分类列中的值分配给数值标识符)应用于用户输入的电车行程详情,这些步骤与我们训练 Keras 模型时应用于训练数据时相同。
让我们看看用户在 Facebook Messenger 部署中期望如何输入对电车行程延迟预测的请求,并将其与训练模型期望作为预测输入的内容进行比较。图 8.15 展示了用户请求与模型期望输入之间的差距。

图 8.15 如何从用户输入转换为模型期望的格式
我们需要有一种方便的方式来准备用户提供的用于新预测的数据,使其符合训练模型期望的格式。正如我们在第 8.6 节中看到的,Rasa 通过从用户的请求中提取必要信息并推断缺失信息,帮助我们走了一半的路,如图 8.16 所示。

图 8.16 如何从 Rasa 输出转换为模型期望的格式
我们如何将 Rasa 从用户输入中提取的数据点转换为模型期望的格式?特别是,我们如何将诸如路线、月份和星期几这样的分类值转换为模型期望的整数标识符?
一种方法是在训练代码中包含函数,这些函数将评分代码可用的分类值编码。在评分代码中,我们可以调用这些相同的函数来对新数据点(我们希望应用模型的数据点)中的分类值(如路线和方向)进行编码。问题是如何确保在评分过程中使用与我们在训练数据集期间使用的相同映射。例如,如果“2019”在训练过程中被映射为“5”,那么我们如何确保在评分过程中“2019”发生相同的映射?我们可以将训练过程中使用的编码器对象进行 pickle,然后在使用训练模型评分新数据时解 pickle 并应用这些相同的编码器,但这个过程会繁琐且容易出错。我们需要的是一个方便的方式来封装我们用来准备训练数据的数据准备过程,以便我们可以将相同的流程应用于将被训练模型评分的新数据点。实现这一目标的有效方法之一是使用 scikit-learn 提供的管道功能(mng.bz/X0Pl)。
scikit-learn 中的管道功能使得将所有数据转换(以及如果您愿意,还包括模型本身)封装在一个对象中成为可能,您可以将其作为一个整体进行训练。当您训练了一个管道后,您可以在评分新数据点时应用它;管道会处理新数据点上的所有数据转换,然后应用模型以获得结果。
scikit-learn 中的管道功能是为了与 scikit-learn 中包含的经典机器学习算法一起使用而设计的,包括支持向量机、逻辑回归和随机森林。虽然可以创建一个包含 Keras 深度学习模型的 scikit-learn 管道,但我还没有能够成功地创建一个多输入 Keras 模型(如电车延误预测模型)的此类管道。因此,当我们在这本书中应用 scikit-learn 管道时,它们仅涵盖数据准备步骤,并不封装模型本身。
此外,为了解决在应用最终数据准备步骤(将数据集从 Pandas dataframe 转换为 numpy 数组字典)后将数据集分为训练、验证和测试时的问题,我们使用两个管道串联。第一个管道编码分类值(并处理任何剩余的缺失值),第二个管道将数据集从 Pandas dataframe 转换为模型期望的 numpy 数组字典。请参阅第 8.12 节,了解构成这两个管道的关键代码元素。
图 8.17 显示了用户如何输入将被训练模型评分的新数据点,Rasa 如何解释这些数据点,然后通过管道处理,以便它们以正确的格式供训练的 Keras 模型进行预测。

图 8.17 从用户输入到模型期望的完整流程
在将训练模型应用于新数据点之前,应用于用户输入的新数据点的管道与在训练模型之前应用于数据集的管道相同,如图 8.18 所示。

图 8.18 训练数据通过与评分数据相同的管道
在本节中,我们介绍了管道的概念,并从高层次上展示了它们在训练和部署过程中的位置。在下一节中,我们将深入研究定义管道的代码。
8.12 模型训练阶段定义管道
既然我们已经了解了管道的整体目的,让我们来看看实现街道延误预测项目中使用的管道的代码细节。在本节中,我们将回顾如何在 streetcar_model_training 笔记本中定义管道。在第 8.13 节中,我们将回顾管道在模型部署过程中的评分过程中应用于新数据点的细节。
scikit-learn 中的管道功能附带了一组可现成使用的转换类,或者您可以通过创建新的类作为核心管道类的子类来创建自己的自定义转换器。对于街道延误预测项目,我们通过从 scikit-learn 提供的类中派生新的 Python 类来创建自定义转换器。您可以在 custom_classes.py 中找到这些类的定义:
-
encode_categorical— 对类别列(如路线、方向和年份)进行编码。 -
prep_for_keras_input— 将数据集从 Pandas 数据框转换为 Keras 模型期望的格式:一个 numpy 数组的字典。 -
fill_empty— 用占位符值替换空值。 -
encode_text— 对文本列进行编码(未用于街道延误项目)。
您可能会想知道为什么这些类定义与代码的其他部分分开在一个单独的文件中。有两个原因:
-
这些类需要由包含训练模型代码的 streetcar_model_training 笔记本和包含 Facebook Messenger 部署评分代码的 actions.py 使用。由于这两个 Python 程序都需要访问相同的类定义,因此将类定义放在一个单独的文件中,这两个程序都可以导入,是有意义的。
-
如果直接将类定义包含在评分代码文件中,类定义将无法正确解析。将类定义放在单独的文件中允许它们在将类导入评分代码 actions.py 时正确解析。导入语句如下:
-
from custom_classes import encode_categorical from custom_classes import prep_for_keras_input from custom_classes import fill_empty from custom_classes import encode_text
让我们看看在 streetcar_model_training 笔记本中训练阶段如何定义管道。首先,我们创建 custom_classes.py 中定义的三个类的实例:
fe = fill_empty()
ec = encode_categorical()
pk = prep_for_keras_input()
以下两点需要注意:
-
如果这是你第一次接触 Python 的面向对象方面,不要担心。你可以将前面的定义视为创建三个对象,每个对象都具有与其对应的类相同的类型。这些类从其父类继承数据结构和函数,因此,使用这些对象,你可以应用它们在类中明确定义的函数以及它们从父类
BaseEstimator和TransformerMixin继承的函数。 -
由于数据集中没有文本列,我们没有创建
encode_text类的对象。
接下来,我们定义两个管道对象,使用我们创建的类实例。第一个管道包含了用于填充空值和编码分类列的类。第二个管道包含了将数据集从 Pandas 数据框转换为 numpy 数组列表的类:
sc_delay_pipeline = Pipeline([('fill_empty',fe), \
('encode_categorical',ec)])
sc_delay_pipeline_keras_prep = Pipeline([('prep_for_keras',pk)])
接下来,我们为管道中的类实例设置参数:
sc_delay_pipeline.set_params(fill_empty__collist = collist, \
fill_empty__continuouscols = continuouscols, \
fill_empty__textcols = textcols, \
encode_categorical__col_list = collist)
sc_delay_pipeline_keras_prep.set_params(prep_for_keras__collist = \
collist, prep_for_keras__continuouscols = continuouscols, \
prep_for_keras__textcols = textcols)
这些语句设置了在各个类中定义的 set_params 函数中定义的参数。语法是类名后跟两个下划线,然后是参数名和分配给参数的值。在图 8.19 中,顶部框显示了带有 encode_categorical 类的 col_list 参数的 set_params 语句。底部框显示了在 encode_categorical 类定义中指定 col_list 参数的位置。

图 8.19 set_param 语句为管道类中定义的参数设置值。
现在参数已经设置好了,让我们看看如何应用第一个管道来编码分类列。以下代码中的第一行将管道拟合并转换输入的数据框,第二行将拟合的管道保存起来,以便在需要评分新数据点时使用:
X = sc_delay_pipeline.fit_transform(merged_data)
dump(sc_delay_pipeline, open(pipeline1_file_name,'wb'))
fit_transform 语句调用了 encode_categorical 客户端转换器类中的以下方法,如下一列表所示。
列表 8.8 由 fit_transform 语句调用的代码
def fit(self, X, y=None, **fit_params):
for col in self.col_list:
print("col is ",col)
self.le[col] = LabelEncoder()
self.le[col].fit(X[col].tolist()) ❶
return self
def transform(self, X, y=None, **tranform_params):
for col in self.col_list:
print("transform col is ",col)
X[col] = self.le[col].transform(X[col]) ❷
print("after transform col is ",col)
self.max_dict[col] = X[col].max() +1
return X
❶ 在类的 fit 方法中,编码器被实例化。
❷ 在类的 transform 方法中,fit 方法中实例化的编码器被应用。
现在我们已经回顾了定义管道的代码,深入探讨一下在管道的上下文中,“拟合”意味着什么。在这种情况下,“拟合”意味着需要训练的管道部分使用输入数据进行训练。对于编码分类值的管道部分,训练管道意味着设置分类列中的输入值与将替换它们的整数标识符之间的对应关系。回到第 8.11 节的例子,如果模型训练时将“2019”映射到“5”,则在部署时评分的新数据项中,“2019”也将映射到“5”。这正是我们想要的,也是由于管道的存在而实现的。
8.13 评分阶段应用管道
在第 8.12 节中,我们详细介绍了管道在模型训练阶段是如何定义、训练和保存的。在本节中,我们将介绍如何使用部署的 Keras 模型将这些管道应用于评分阶段。
评分代码位于 actions.py(用于 Facebook Messenger 部署)或 flask_server.py(用于 Web 部署)中,它从 custom_classes.py 中导入自定义转换器类,并加载在 streetcar_model_training 笔记本中保存的训练好的管道。图 8.20 总结了这三个文件中管道元素之间的关系。
让我们回顾一下评分代码中与管道相关的部分。这里显示的语句对于 Web 部署和 Facebook Messenger 部署是相同的。首先,评分代码使用与模型训练代码中相同的代码导入自定义转换器类定义:
from custom_classes import encode_categorical
from custom_classes import prep_for_keras_input
from custom_classes import fill_empty
from custom_classes import encode_text
评分代码中自定义动作的定义包括以下语句,用于加载在模型训练阶段保存的训练好的管道:
pipeline1 = load(open(pipeline1_path, 'rb'))
pipeline2 = load(open(pipeline2_path, 'rb'))

图 8.20 包含管道代码的文件之间的关系
得分代码将我们想要获取延误预测的街车行程的数据点加载到 Pandas 数据框 score_df 中。在以下语句中,将这些管道应用于此数据框:
prepped_xform1 = pipeline1.transform(score_df)
prepped_xform2 = pipeline2.transform(prepped_xform1)
现在,可以将这些管道的输出应用于训练好的模型,以预测街车行程是否会延误:
pred = loaded_model.predict(prepped_xform2, batch_size=BATCH_SIZE)
我们已经介绍了在电车延误项目中如何使用管道,从模型训练阶段的管道定义到部署模型在评分阶段对新数据点的应用。管道是强大的工具,通过封装训练过程中使用的用于数据转换的步骤,使得它们可以在评分过程中方便地使用。在电车延误预测项目中,我们在模型训练阶段训练用于数据准备的管道,然后在评分阶段使用相同的管道准备新的输入数据点,用于 Web 部署和 Facebook Messenger 部署。
8.14 部署后维护模型
部署并不是训练好的模型的终点。在深度学习模型的全面部署中,监控生产中的模型以确保其性能不会随着时间的推移而变差是至关重要的。如果性能确实变差(称为模型漂移或概念漂移的现象),则有必要在新鲜数据上重新训练模型。图 8.21 总结了模型维护的周期。当模型经过训练和部署后,需要评估其性能。如果需要,需要使用包括更多近期数据点的数据重新训练模型。然后,需要部署重新训练后的模型。

图 8.21 模型维护周期
在工业级部署中维护深度学习模型的详细描述超出了本书的范围。(你可以在mng.bz/yry7 和 mng.bz/ModE 找到一些好的建议,以及一个包括部署选项的概述,见 mng.bz/awRx。)但我们可以看看在模型部署后不遵循模型维护周期会发生什么。以第一章中的信用卡欺诈检测为例。信用卡欺诈预测模型可能捕捉到的信号之一是同一天在物理零售地点使用同一张卡进行的两次交易,而这些地点在一天内不可能往返。例如,目前不可能在 24 小时内乘坐商业航班从魁北克城飞往新加坡,所以如果同一天在同一张卡在魁北克城的高端餐厅支付餐费和在新加坡的珠宝店支付钻石戒指,那就说明有问题。但如果航空公司开始提供魁北克城和新加坡之间的直飞航班,使得在同一天内可以到达这两个城市,会发生什么?如果发生更剧烈的变化,比如在 2030 年代初超音速客机旅行卷土重来,又会怎样?这样的变化将破坏任何依赖于在遥远城市同一天购买信号的欺诈检测模型。为机器学习模型提供的数据通常来自现实世界,而现实世界以不可预测的方式不断变化。我们需要预期我们的模型将需要定期在新鲜数据上重新训练。
模型需要多久重新训练一次?需要监控哪些模型性能指标来确定何时需要重新训练?我们是否可以简单地替换掉旧模型并替换上新模型,或者我们需要在生产中保留两个模型,并在过渡期间使用一个混合评分(部分来自旧模型,部分来自新模型)以避免用户体验的突然变化?请参阅 mlinproduction.com/model-retraining 和 mng.bz/ggyZ 了解关于重新训练问题的更详细讨论。以下是关于模型重新训练的一些最佳实践的简要总结:
-
保存性能指标,以便对部署模型的性能进行评估。为了评估预测的准确性,你需要有预测和匹配的真实世界结果。对于电车延误问题,为了评估一个月内生产中的模型性能,我们需要实际的延误数据和模型对该月路线/方向/时间段组合的预测。如果我们保存一个月内做出的预测,我们可以在一个月后将其与实际延误数据进行比较。
-
选择一个性能度量指标,它能够让你评估模型表现而不会产生过多的延迟。以信用卡欺诈问题为例。假设我们将一个模型投入生产以预测交易是否为欺诈,而我们部署的模型性能度量依赖于一个月内实际欺诈交易的完整报告。可能需要几个月的时间才能得出给定月份所有实际欺诈交易的完整结论。在这种情况下,拥有一个与同一个月内确定无疑为欺诈的交易一起工作的性能度量会更好。简而言之,一个能够尽快产生良好结果以便你快速做出重新训练决策的性能度量比一个产生卓越结果但可能使表现不佳的模型部署数月之久的性能度量要好。
-
使用历史数据进行实验,以了解你的部署模型性能将如何快速退化。对于信用卡欺诈问题,你可以用 2018 年底的数据来训练你的模型,然后应用训练好的模型来预测 2019 年前 6 个月的交易。你可以将这些预测与那些 6 个月的实际欺诈交易数据进行比较,以查看基于 2018 年数据的模型在 2019 年数据上的准确性是否会随着时间的推移而变差。这个过程可能会让你对模型性能退化速度有一个概念,但这并不是万无一失的,因为根据问题的不同,你的数据可能会以意想不到的方式发生变化。
-
在新数据上重复你在训练模型之前所做的数据探索步骤。回想一下我们在第三章中进行的那些数据探索。如果我们对新街车延误数据重复这些步骤,一旦数据可用,我们就可以检测到数据特征的变化,并在发现显著变化时重新训练模型。
让我们以街车延误预测模型为例,看看模型重新训练的问题。首先,让我们看看原始数据。原始数据每月更新一次,但会有两到三个月的延迟。例如,一月份的最新延误数据来自前一年的十月。在良好的开发环境中,从下载最新原始数据到部署更新模型的端到端过程不到一个小时。以这么低的成本,我们每个月重新训练模型是可能的,但这是否必要?我们在第三章中进行的那些数据探索显示,存在一些长期趋势(例如,延误时间变短但更频繁),但月份之间没有巨大的波动。我们可能可以每季度刷新一次模型,但要确保这一点,我们希望通过将预测与可用新月份的实际延误数据进行比较来监控模型的准确性。
此外,我们可能想要进行一些实验,只使用更近期的数据进行模型训练,例如通过保持三年滚动窗口作为训练数据,而不是从 2014 年 1 月以来的整个数据集进行训练。
最后,我们希望为用户提供一种直接反馈他们使用最终应用体验的方式。来自用户子集的直接反馈可以揭示监控中忽视的模型问题。
摘要
-
一个训练好的深度学习模型本身并没有用处。为了使其变得有用,你需要部署它,使其能够被其他程序或需要利用模型预测的用户访问。部署是一个挑战,因为它涉及一系列与本书数据准备和模型训练章节中学到的技术方法不同的技术能力。
-
你可以通过使用 Flask,一个 Python 网络框架库,以及一组 HTML 页面来获得你模型的最低部署。通过 Flask、HTML 和 JavaScript 的组合,你的用户可以在网页中输入他们计划乘坐的电车旅行的详细信息,并获得关于旅行是否会延误的预测。在幕后,训练好的深度学习模型使用旅行详细信息被调用,并产生一个预测,该预测被准备在网页中显示。
-
如果你想要更流畅的用户体验,你可以通过结合使用 Rasa 聊天机器人框架和 Facebook Messenger 来部署你的训练好的深度学习模型。当你完成部署后,你的用户可以通过 Facebook Messenger 向聊天机器人发送英语问题,例如“501 路东行是否会延误?”并在 Facebook Messenger 中得到回答(延误/不延误)。在幕后,Rasa 聊天机器人从用户在 Facebook Messenger 中输入的问题中提取关键细节,调用一个 Python 模块,应用训练好的深度学习模型对这些细节进行预测,并将预测结果准备在 Facebook Messenger 中显示。
-
管道允许你封装数据准备步骤(包括为分类列条目分配数值和将数据集从 Pandas 数据框转换为 Keras 深度学习模型所需的格式),以便在训练时间和评分时间(当训练好的模型被应用于新的数据点时,例如电车旅行的/时间/路线/方向组合)应用相同的转换。
-
当你部署了一个训练好的深度学习模型后,你需要监控其性能。如果数据发生变化,模型的性能可能会随着时间的推移而下降,你可能需要使用更近期的数据进行模型重新训练,然后替换当前部署的模型。
9 建议的下一步行动
本章涵盖
-
回顾本书到目前为止所涵盖的内容
-
你可以对电车延误预测项目进行的额外改进
-
你如何将所学应用到其他现实世界项目中
-
选择使用结构化数据的深度学习项目时应使用的标准
-
额外学习的资源
我们几乎到达了这本书的结尾。在本章中,我们将回顾和展望。首先,我们将回顾在前几章中学到的内容,从清理现实世界的数据集到部署训练好的深度学习模型。接下来,我们将讨论你可以采取的步骤来通过新的数据源增强电车延误预测项目。然后,我们将讨论如何将你所学应用到其他现实世界项目中,包括如何确定一个涉及结构化数据的问题是否适合作为深度学习项目。最后,我们将回顾一些关于深度学习的额外学习资源。
9.1 回顾本书到目前为止所涵盖的内容
为了回顾本书到目前为止所学的知识,让我们回到第二章中引入的端到端图,如图 9.1 所示。

图 9.1 电车延误预测项目的端到端视图
在第二章中,我们学习了如何使用 Pandas 将表格结构化数据集导入 Python。在第三章和第四章中,我们经历了处理数据集问题的过程,包括格式不正确的条目、错误和缺失值。在第五章中,我们对数据集进行了重构,以考虑到它包含了电车延误的信息,但没有关于没有延误情况下的明确信息。我们使用这个重构后的数据集创建了一个简单的 Keras 模型,其层是根据数据集的列结构自动生成的。在第六章中,我们使用准备好的数据集迭代训练这个模型,并利用 Keras 的功能来控制训练过程并保存具有最佳性能特性的模型。在第七章中,我们对训练好的模型进行了一系列实验,以验证删除不良记录和使用嵌入的影响。我们还进行了一个实验,比较深度学习模型与关键竞争对手 XGBoost。在第八章中,我们通过简单的 Web 部署和更复杂的 Facebook Messenger 部署部署了训练好的模型。通过这些部署,我们完成了从原始数据集到一个用户可以使用它来获取电车延误预测的工作系统的旅程。
9.2 我们可以用电车延误预测项目做什么下一步
本书到目前为止已经覆盖了很多内容,但我们还可以在电车预测项目中采取其他几种路径。例如,我们可以增加训练数据集以包含额外的数据源。
我们为什么要用额外的数据源来训练模型呢?第一个原因可能是为了尝试提高模型的准确性。可能一个使用额外数据源训练的模型会比我们在第六章中训练的模型做出更准确的预测。从其他来源(如历史天气数据或交通数据)添加数据或利用原始数据集中更多的数据(如延迟位置)可以为模型提供更强的信号,以便在尝试预测延误时检测到。
我们是否事先知道使用额外的数据源训练模型是否会提高模型的延迟预测准确性?简而言之,不知道,但提高模型的准确性并不是增强训练数据集的唯一目标。使用额外的数据源训练模型的第二个原因是,这样做是一个很好的学习练习。在您使用额外数据源训练模型的过程中,您将了解更多关于代码的知识,并为下一步做好准备:将结构化数据方法应用于全新的数据集,如第 9.8 节中介绍的那样。
在以下几节中,我们将简要回顾一些您可以添加到用于训练模型的数据库中的额外数据。第 9.4 节回顾了您如何利用原始数据集中存在但未用于第六章中我们进行的模型训练的延迟位置数据。它展示了您如何使用包含全新数据源的数据集来训练模型:历史天气信息。第 9.5 节为您提供了一些想法,说明您如何通过从第六章中用于训练模型的数据库中派生新列来增强训练数据。在您审阅了这些章节后,您将准备好阅读第 9.8-9.11 节,在那里您将学习如何将用于电车延迟预测问题的方法适应涉及结构化数据的新问题。在第 9.12 节中,您将看到该方法应用于一个具体的新问题,即预测纽约市 Airbnb 列表的价格。
9.3 将位置细节添加到电车延迟预测项目中
在第四章中,我们解释了您如何使用谷歌的地理编码 API( mng.bz/X06Y)将电车延迟数据集中的位置数据替换为经纬度值。我们没有在扩展示例的其余部分使用这种方法,但您可以回顾一下,看看是否将地理空间数据添加到重构后的数据集中可以提高模型的性能。您预计会得到更高保真度的预测,因为,如图 9.2 所示的延迟热图所示,延迟集中在城市中心。一个起点和终点都在城市中心以外的行程不太可能延误,即使它是在可能遇到延误的路线/方向/时间组合上。

图 9.2 延迟集中在城市的中心部分。
您可以利用从原始数据集中的位置推导出的经纬度值的一种方法是将每条路线划分为子路线。以下是一种基于路线的经纬度值自动将每条路线划分为子节的方法:
-
在整个路线上定义一个边界框,该边界框由路线的最大和最小经纬度值确定。您可以使用路线上的延迟经纬度值作为整个路线的代理来获取最大和最小值。streetcar_data_geocode_get_boundaries 笔记本中包含了您可以使用的代码,包括
def_min_max()函数,该函数创建一个包含每个路线的最小和最大经纬度值的 dataframe,如下所示。列表 9.1 定义包含路线边界的 dataframe 的代码
def def_min_max(df): # define dataframes with the maxes and mins for each route df_max_lat = \ df.sort_values('latitude',ascending=False).drop_duplicates(['Route']) df_max_long = \ df.sort_values('longitude',ascending=False).drop_duplicates(['Route']) df_min_lat = \ df.sort_values('latitude',ascending=True).drop_duplicates(['Route']) df_min_long = \ df.sort_values('longitude',ascending=True).drop_duplicates(['Route']) # rename column names for final dataframe df_max_lat = df_max_lat.rename(columns = {'latitude':'max_lat'}) df_max_long = df_max_long.rename(columns = {'longitude':'max_long'}) df_min_lat = df_min_lat.rename(columns = {'latitude':'min_lat'}) df_min_long = df_min_long.rename(columns = {'longitude':'min_long'}) # join the max dataframes df_max = pd.merge(df_max_lat,df_max_long, on='Route', how='left') df_max = df_max.drop(['longitude','latitude'],1) # join the min dataframes df_min = pd.merge(df_min_lat,df_min_long, on='Route', how='left') df_min = df_min.drop(['longitude','latitude'],1) # join the intermediate dataframes to get the df with the bounding boxes df_bounding_box = pd.merge(df_min,df_max, on='Route', how='left') return(df_bounding_box) -
图 9.3 显示了部分路线的最小和最大纬度和经度值。
![CH09_F03_Ryan]()
图 9.3 部分路线的最小和最大纬度和经度值
-
现在您已经为每条路线定义了边界框,作为每条路线的最大和最小经纬度值,您可以将边界框沿其主轴划分为若干(例如,10 个)等大小的矩形。对于大多数路线,这个轴将是东西轴。对于斯帕丁纳和巴瑟斯特路线,它将是南北轴。结果将为每条路线定义子路线,由最小和最大经纬度值确定。图 9.4 显示了斯克莱尔路线的子路线边界框可能的样子。
![CH09_F04_Ryan]()
图 9.4 斯克莱尔路线的子路线边界框
-
在为每条路线定义了这些子路线之后,您可以在重构的数据集中添加一个列,以便每个修订后的重构数据集的行代表一个路线/子路线/方向/日期和时间组合。使用延迟位置的经纬度值,对于每个延迟,您可以确定发生延迟的子路线。图 9.5 显示了原始重构数据集的一个片段,图 9.6 显示了在添加子路线列后重构数据集的外观。
![CH09_F05_Ryan]()
图 9.5 原始重构数据集
![CH09_F06_Ryan]()
图 9.6 添加了子路线列的重构数据集
当你为重构后的数据集添加了子路由并使用这个增强的数据集重新训练了模型后,你面临的一个挑战是如何让用户定义他们正在通过给定路由的哪些子路由。为了使用修订后的模型得分,你需要从用户那里获取他们的旅行起点和终点位置。对于网络部署,你可以在 home.html 中添加一个新的控件,让用户选择他们的旅行子路由。使用网络部署的用户体验可能不是理想的,那么增强 Facebook Messenger 部署以允许用户指定子路由怎么样?你可以采取两种方法:
-
增强 Rasa 模型,允许用户输入主要交叉街道名称,然后使用 geocode API 将这些街道的交叉口转换为纬度和经度值。
-
使用 Facebook Messenger 的 webview 功能(
mng.bz/xmB6)在网页中显示一个交互式地图小部件,允许用户选择路线点。
总体而言,将子路由添加到电车延误预测模型可能会提高性能,但将网络部署或 Facebook Messenger 部署调整为允许用户指定他们的旅行起点和终点将是一项非同小可的工作。
9.4 使用天气数据训练我们的深度学习模型
多伦多拥有四个不同的季节,冬季和夏季的天气极端。这些极端可能会影响电车延误。例如,即使是轻微的降雪也可能导致交通拥堵,从而延误整个网络中的电车。假设我们想利用天气数据来查看它是否能够提供更好的电车延误预测。我们从哪里开始?本节总结了将天气数据添加到电车延误预测模型中所需进行的操作。
第一个挑战是找到一个天气数据源,将其纳入训练数据集。有几个开源数据源(mng.bz/5pp4)提供天气信息。图 9.7 显示了练习此类数据源端点的界面:Dark Sky(mng.bz/A0DQ)。你需要提供凭证(如 GitHub ID 和密码)来访问此接口,尽管你得到了免费的 API 调用配额,但你仍需要提供支付信息来运行 API 的测试练习。

图 9.7 练习天气信息 API
假设我们想查看 2007 年 3 月 3 日早上在多伦多市政厅的天气情况。以下是 API 所需的参数:
-
日期时间:2007-03-01T01:32:33
-
经度:-79.383186
-
纬度:43.653225
API 接口显示了此请求的 Python API 调用看起来是什么样子,如下一列表所示。
列表 9.2 Dark Sky API 接口生成的示例代码
import requests
url = https://dark-sky.p.rapidapi.com/\
43.653225,-79.383186,2007-03-01T01%253A32%253A33 ❶
headers = {
'x-rapidapi-host': "dark-sky.p.rapidapi.com",
'x-rapidapi-key': <API KEY> ❷
}
response = requests.request("GET", url, headers=headers)
print(response.text)
❶ 使用日期/时间和经纬度输入构建的 URL
❷ 要运行此调用,您需要获取 Dark Sky 的 API 密钥并将其粘贴在此处。
图 9.8 显示了此 API 调用返回的结果。我们如何使用这些天气数据?首先,我们希望控制需要进行的 API 调用次数,以最小化整体成本。每次 Dark Sky 调用的成本是几分之一美分,但如果不够小心,整体成本可能会很高。考虑以下获取特定位置和时间的天气数据的方法:
-
使用第 9.3 节中介绍的子路由,并通过使用每个子路由边界框的平均纬度和经度值来获取每个子路由的独立天气数据点。
-
对于每个子路由,每小时获取四个天气数据点。

图 9.8 2007 年 3 月 3 日早上天气 API 调用对多伦多市政厅的天气详情响应
采用这种方法,我们需要超过 3100 万个天气数据点来覆盖从 2014 年 1 月开始的数据集。所有这些 API 调用的成本将超过 40,000 美元——对于一个实验来说,这是一个巨大的成本。我们如何在不需要这么多数据点的情况下仍然获取有用的天气数据?
我们很幸运,电车延误问题局限于一个相对较小的地理区域,并且有可预测的天气模式,因此我们可以做出一些简化的假设来控制所需的天气数据点数量。以下简化假设将使我们能够以最少的 API 调用次数将天气数据添加到重构后的数据集中:
-
特定小时的天气条件是一致的。我们每天只获取 24 个天气数据点,而不是每小时多个数据点。天气确实可能在小时内发生变化,但导致电车延误的天气(如大雨或大雪)很少在多伦多一个小时内开始和结束。我们可以安全地每小时读取一次天气数据。
-
电车网络中的天气条件是一致的。整个电车网络位于一个东西宽 26 公里、南北宽 11 公里的区域内,如图 9.9 中的边界框所示,因此可以合理地假设,导致电车延误的天气类型在整个网络中的任何时间都是一致的。也就是说,如果网络西端的 Long Branch 下大雪,那么网络东端的 The Beach 很可能也在下雪。基于这个假设,我们可以使用(43.653225, -79.383186),即多伦多市政厅的纬度和经度,对所有天气 API 的调用。
-
![CH09_F09_Ryan]()
图 9.9 电车网络的边界框
这里是电车网络纬度和经度的极端值:
min_lat = 43.58735
max_lat = 43.687840
min_long = -79.547860
max_long = -79.280260
在这些简化假设下,我们需要从 2014 年 1 月 1 日以来的每小时天气数据,大约有 52,500 个数据点。考虑到 Dark Sky 按 API 调用收费,生成所需天气数据点将花费大约 60 美元。
现在我们已经确定了所需的历天气候数据量,我们希望将哪些天气特征纳入数据集中?以下是一些可能对预测电车延误相关的明显天气数据集字段:
-
温度 —在没有活跃降水的情况下,温度极端值可能与延误有关。温度将是数据集中的连续列。
-
图标 —该字段中的值,如“雪”或“雨”,整洁地封装了天气状况。图标将是数据集中的分类列。
-
摘要 —该字段中的值,如“全天降雨”和“上午开始轻雪”,提供了关于图标列中捕获的整体天气状况的额外背景信息。摘要列可能是一个文本列。回想一下,用于在第六章训练深度学习模型的重构数据集不包含任何文本列。将摘要作为文本列添加将很有趣,因为它将锻炼深度学习模型代码的一个方面,而核心电车延误数据集没有利用这一点。
假设你得到了前面列表中描述的天气数据点,你需要更新电车模型训练笔记本中的数据集重构代码,以包含天气字段。特别是,你需要将天气字段的列名添加到def_col_lists ()中的适当列表中:
-
将温度列名添加到
continuouscols。 -
将摘要列名添加到
textcols。
如果你没有将图标列名放入任何其他列表中,它将自动添加到分类列的列表中,这正是我们想要的。
训练代码编写为适用于任何一列,只要在def_col_lists ()中正确识别了列名。其余的训练代码应与新列一起工作,并为你提供一个包含天气列的新训练模型。
当您有一个包含天气列的已训练模型时,当用户想知道他们的电车行程是否会延误时,您如何在评分时间考虑天气条件?首先,您将新的天气列添加到评分代码中的score_cols列表中。这个列表是得分列的列表,用于定义score_df,即包含通过评分代码中的管道运行值的 dataframe。您可以在评分代码中调用 Dark Sky API 以获取当前天气条件,使用之前提到的多伦多市政厅的纬度和经度,并构建符合 Dark Sky 要求的当前时间字符串:[YYYY]-[MM]-[DD]T[HH]:[MM]:[SS]。所以如果当前日期是 2021 年 5 月 24 日,时间是中午,Dark Sky API 的日期时间字符串是 2020-05-24T12:00:00。当您从 API 调用中获取所需的天气字段时,您可以使用这些值来设置score_df中的天气列。评分代码将score_df通过管道运行,并将管道的输出应用于训练模型,从而得到延误预测。由于之前提到的简化假设,您在评分时间不需要从用户那里获取任何信息来获取评分所需的天气数据。
图 9.10 总结了通过 Facebook Messenger 部署将天气数据纳入电车延误预测深度学习项目所需的更改。为了适应 Web 部署,您需要对 Web 部署的主要 Python 程序flask_server.py中的评分代码进行类似的更改,如图 9.10 中为actions.py(Facebook Messenger 部署的主要 Python 程序)所指定的。

图 9.10 添加天气数据到电车延误模型所需更改的总结
将天气数据添加到您的深度学习模型中的这项练习不仅为您提供了提高模型性能的机会,还展示了您需要采取的步骤以将其他数据源添加到电车延误深度学习模型中。您可以从本节中描述的步骤中推断出创建基于新数据集的深度学习模型的方法。第 9.8 节介绍了将本书中描述的方法应用于新结构化数据集所需采取的额外步骤。但在我们查看新项目之前,在第 9.5 节中,我们将探讨两个简单选项,以增强电车延误预测项目的训练数据集。
9.5 将季节或一天中的时间添加到电车延误预测项目中
在 9.3 节和 9.4 节中,我们回顾了我们可以添加到模型训练数据中的两个额外数据源:延迟位置数据和天气数据。这两个数据源相对难以添加到训练过程中。如果你想采取更简单的方法向训练数据集添加数据,你可以尝试从我们在第六章中用于训练模型的数据库中的列推导出新列。例如,从月份列中推导出的季节列,值为 0-3 代表四个季节,这是一个相对简单的添加。
由于你可以从月份列中推导出季节列,你也可以从小时列中推导出一天中的时间列。这个列的有趣之处在于你可以控制每天每个时间段的边界。假设你定义了一个包含五个值的一天中的时间列:
-
深夜
-
早晨高峰时段
-
中午
-
下午高峰时段
-
晚上
你可以尝试为每个类别不同的开始和结束时间进行实验,看看对模型性能的影响。如果你将早晨高峰时段定义为从 5:30 到上午 10 点而不是从 6:30 到上午 9:00,这会对模型性能产生影响吗?
9.6 插补:移除包含不良值的记录的替代方案
在第七章中,我们进行了一个实验,比较了模型性能与两种训练数据集形式:
-
排除包含不良值的记录(如包含无效路线的记录)
-
包含包含不良值的记录
这个实验的结论是,当移除不良值时,模型的性能更好。尽管有这个结论,我们为移除包含不良值的记录付出了代价。对于在 2019 年底之前使用延迟数据在数据准备笔记本上进行的给定运行,输入数据集中大约有 78,500 条延迟记录,但在移除不良值记录后,只有大约 61,500 条记录。在这种情况下,当我们移除不良记录时,我们失去了大约 20%的记录。重要的是要记住,当记录在一个字段中有不良值时,我们会移除整个记录,因此当我们丢弃所有包含不良值的记录时,我们可能会丢失信号的有用部分。有没有任何替代方案可以让我们保留一些丢失的信号?
事实上,一种称为插补的方法,即用另一个值替换缺失值,可能会有所帮助。在结构化、表格数据的情况下,可用的插补类型取决于列的类型:
-
连续型 —你可以用固定值(如零)或计算值(如该列所有值的平均值)来替换缺失值。
-
分类型 —你可以用列中最常见的值来替换缺失值,或者采取更复杂的方法应用模型(如使用 1,000 个最近邻)来找到缺失值的替代值。
如果你想要对电车延迟预测模型进行插补实验,你可以在处理缺失值的文章中找到关于插补方法的更完整讨论(mng.bz/6AAG)。
9.7 使电车延迟预测模型的网页部署普遍可用
在第八章中,我们描述了如何创建训练模型的简单网页部署。第八章中描述的网页部署完全是本地的;你只能在部署的系统上访问它。如果你想与其他系统上的朋友分享这个部署怎么办?
打开网页部署最简单的方法是使用 ngrok,这是我们第八章中用于 Facebook Messenger 部署的实用程序。在第八章中,我们使用 ngrok 将 localhost 外部化,以便你的 Facebook 应用程序可以与运行在你本地系统上的 Rasa 聊天机器人服务器通信。
要使用 ngrok 使你的网页部署在本地系统之外可访问,请按照以下步骤操作:
-
如果你还没有这样做,请按照
ngrok.com/download上的安装说明安装 ngrok。 -
在你安装 ngrok 的目录中,调用 ngrok 使你的系统上的 localhost:5000 可以外部访问。以下是 Windows 的命令:
.\ngrok http 5000 -
复制 ngrok 输出中的 https 转发 URL,如图 9.11 所示。
![CH09_F11_Ryan]()
图 9.11 ngrok 输出,突出显示转发 URL
-
在 deploy_web 目录下运行此命令以启动网页部署:
python flask_server.py
现在你已经运行了 ngrok 将 localhost:5000 外部化,网页部署将通过 ngrok 提供的转发 URL 对其他用户可用。如果其他用户在浏览器中打开 ngrok 转发 URL,他们将看到如图 9.12 所示的 home.html。

图 9.12 通过 ngrok 提供的具有外部访问 URL 的 home.html
当另一个系统上的用户在 ngrok 转发 URL 中打开 home.html 时,他们可以选择评分参数并点击获取预测以显示他们的电车行程的延迟预测。请注意,只有当你的本地系统连接到互联网且 flask_server.py 正在运行时,此部署才对其他用户可用。另外请注意,使用免费 ngrok 计划,每次调用 ngrok 时都会获得不同的转发 URL。如果你需要为 ngrok 的网页部署获取一个固定 URL,你需要选择 ngrok 的付费订阅选项之一。
在 9.3-9.5 节中,我们介绍了一些可能改进电车延迟预测模型性能的额外数据源的想法。在第 9.8 节中,我们将概述如何将用于电车延迟问题的方法应用于新的问题。
9.8 将电车延迟预测模型适配到新的数据集
前几节概述了如何通过在模型的训练集中加入额外的数据来增强电车延误预测模型。如果您想将模型适应不同的数据集,本节总结了将本书中描述的方法适应新的结构化数据集的步骤。
本书中的代码示例可以应用于其他表格结构化数据集,但您需要采取一些步骤,如图 9.13 所示,以适应电车延误预测代码。

图 9.13 创建新数据集模型所需更改的摘要
当您考虑将本书中描述的方法应用于新的数据集时,第一个挑战是数据集是否满足应用深度学习的最低要求。以下是一些可以考虑用于深度学习的结构化数据集的特征:
-
足够大 —回想一下第三章中关于为了使深度学习有成功机会,结构化数据集需要有多大规模的讨论。没有至少数万条记录的数据集太小。另一方面,除非您有经验和资源将大数据方法添加到您的工具箱中,否则拥有数千万条记录的数据集将是一个挑战。一个拥有超过 70,000 条记录但少于 1,000 万条记录的数据集是一个良好的起点。
-
异构性 —如果您的数据集完全由连续列组成,您将使用 XGBoost 等非深度学习方法进行预测,从而获得更好的投资回报。但如果您的数据集包括各种列类型,包括分类列和特别是文本列,那么它可能是一个深度学习的良好候选者。如果您的数据集包含包含非文本 BLOB 数据(如图像)的列,您可以通过将深度学习应用于整个数据集而不是仅应用于 BLOB 数据,从而获得许多好处。
-
不太失衡 —在重构的电车延误数据集中,大约 2%的记录表明记录的路线/方向/时间段有延误。如第六章所述,Keras 的
fit命令有参数可以考虑到不平衡的数据集。但如果数据集极度失衡,只有极小的一部分属于某个结果,深度学习模型可能无法捕捉到表征少数结果的信号。
让我们考虑一些公开的数据集,并将这些指南应用于它们,以快速评估与这些数据集相关的问题是否可以通过深度学习方法解决:
-
交通信号车辆和行人流量 (
mng.bz/ZPw9)——这个数据集来自与电车延误数据集相同的精选集合。它包含多伦多一组交叉路口的交通流量信息。使用这个数据集来预测未来的交通流量将很有趣。这个问题适合作为深度学习项目吗?图 9.14 显示,该数据集包含各种列,包括连续列、分类列和地理空间列。![CH09_F14_Ryan]()
图 9.14 交通信号车辆和行人流量数据集的列
图 9.15 显示,流量分布并不太不平衡。
![CH09_F15_Ryan]()
图 9.15 交通信号车辆和行人流量数据集的流量分布
-
这个数据集的问题在于它太小——只有 2,300 条记录——所以尽管它有一组有趣的列和良好的平衡性,但这个数据集并不适合作为深度学习项目。那么,一个覆盖与电车延误类似但不同的问题的数据集会怎样呢?
-
多伦多地铁延误数据集 (
open.toronto.ca/dataset/ttc-subway-delay-data)——如图 9.16 所示,地铁延误数据集包括多种类型的列,包括分类列、连续列和地理空间列。整个数据集大约有 50 万条记录,因此它足够大,有趣,但又不至于难以处理。 -
与电车延误数据集相比,这个数据集稍微平衡一些,因为地铁系统报告的延误是电车系统的约七倍。这个数据集的位置数据的有趣之处在于它是精确的。每个延误都与多伦多地铁的 75 个车站中的一个相对应,任何两个车站之间的空间关系很容易编码,无需使用经纬度。此外,在评分时,用户可以通过从下拉列表中选择地铁站来精确指定行程的开始和结束。因此,将位置信息纳入地铁延误预测模型的训练数据比向电车延误模型添加位置数据要容易得多。总的来说,地铁延误预测项目是深度学习的一个不错的候选项目。
![CH09_F16_Ryan]()
图 9.16 地铁延误数据集
现在我们已经查看了一些可能适合深度学习的结构化数据集,第 9.9 节概述了为训练深度学习模型准备新数据集所需的步骤。
9.9 准备数据集和训练模型
当你选择了一个想要围绕其构建深度学习项目的 dataset 后,下一个挑战就是清理这个 dataset。你在第三章和第四章中看到的例子应该能指导你处理 dataset 中的错误和缺失值,尽管所需的清理步骤将根据 dataset 及其杂乱程度而有所不同。
关于数据清理的话题,你可能会问为什么在第 9.4 节中描述的天气数据没有考虑数据清理。为了回答这个问题,我想回到第二章的一个例子:从各种媒体创建蓝光光盘(图 9.17)。

图 9.17 从各种媒体创建蓝光光盘
这个示例的目的是为了说明,由于各种媒体内容最初并没有考虑到使用蓝光光盘进行记录,因此,那些对深度学习感兴趣的 dataset 并不是有意用于机器学习或深度学习的应用而收集的。这些杂乱的真实世界 dataset 在使用之前必须进行清理,以便用于训练深度学习模型。另一方面,像 Dark Sky 的天气数据或从 Google API 可用的地理编码数据这样的数据源,是为了提供干净、连贯的数据流而设计的,不需要进行清理。这些数据源对于深度学习来说,就像高清数字视频片段对于蓝光光盘问题一样:无需清理即可直接整合。
当你清理完新的 dataset 后,下一步是替换任何非数值值,例如通过将分类值替换为整数标识符。你可以调整训练代码来创建一个新的 dataset 的 dataframe。就像添加天气数据的例子一样,你需要将用于训练模型的列与def_col_lists()函数中的正确类别关联起来。此外,你还需要确定你的 dataset 中哪个列包含目标值。通过这些更改,你应该能够训练一个模型来对新 dataset 进行预测,并最终得到一个训练好的模型以及用于准备数据的训练好的管道。
在我们更详细地讨论如何将电车延误问题的代码适应到其他领域之前,值得回顾一下在第四章中引入的一个想法:在机器学习项目中领域知识的重要性。在第四章中,我解释了选择电车延误问题作为本书扩展示例的原因之一,即我对这个话题恰好有所了解。当你考虑将深度学习应用于新的领域时,请记住这一点:无论是一个旨在磨练自己技能的适度副项目,还是一个组织押注未来的重大项目,你都需要获取该领域专业知识。
9.10 使用 Web 部署部署模型
现在你已经有一个训练好的深度学习模型和针对新数据集的管道,是时候考虑如何部署模型了。如果你选择使用第八章中描述的 Web 部署选项来部署你的新模型,你将需要在 deploy_web 目录中的代码进行以下更新:
-
更新 home.html 中的下拉列表(如下代码片段所示),以反映用户能够选择并发送到训练模型进行评分的评分参数。在街车延误预测的 Web 部署中,所有评分参数都是分类的(也就是说,可以从列表中的元素中选择)。如果你的模型包括连续的评分参数,你需要在
home.html中添加控件,以便用户可以输入连续值: -
<select id="route"> <option value="501">501 / Queen</option> <option value="502">502 / Downtowner</option> <option value="503">503 / Kingston Rd</option> <option value="504">504 / King</option> <option value="505">505 / Dundas</option> <option value="506">506 / Carlton</option> <option value="510">510 / Spadina</option> <option value="511">511 / Bathurst</option> <option value="512">512 / St Clair</option> <option value="301">301 / Queen (night)</option> <option value="304">304 / King (night)</option> <option value="306">306 / Carlton (night)</option> <option value="310">310 / Spadina (night)</option> </select> -
更新 home.html 中的
getOption()JavaScript 函数,以便将模型的评分参数加载到 JavaScript 变量中。以下代码块展示了getOption()函数中加载街车延误预测模型评分参数到 JavaScript 变量的代码: -
selectElementRoute = document.querySelector('#route'); selectElementDirection = document.querySelector('#direction'); selectElementYear = document.querySelector('#year'); selectElementMonth = document.querySelector('#month'); selectElementDaym = document.querySelector('#daym'); selectElementDay = document.querySelector('#day'); selectElementHour = document.querySelector('#hour'); route_string = \ selectElementRoute.options\ [selectElementRoute.selectedIndex].value direction_string = \ selectElementDirection.options\ [selectElementDirection.selectedIndex].value year_string = \ selectElementYear.options\ [selectElementYear.selectedIndex].value month_string = \ selectElementMonth.options\ [selectElementMonth.selectedIndex].value daym_string = \ selectElementDaym.options\ [selectElementDaym.selectedIndex].value day_string = \ selectElementDay.options\ [selectElementDay.selectedIndex].value hour_string = \ selectElementHour.options\ [selectElementHour.selectedIndex].value -
更新 home.html 中的
getOption()JavaScript 函数,以构建包含模型评分参数的目标 URL。以下代码片段展示了getOption()中定义街车延误预测部署目标 URL 的语句: -
window.output = \ prefix.concat("route=",route_string,"&direction=",\ direction_string,"&year=",year_string,"&month=",\ month_string,"&daym=",daym_string,"&day=",\ day_string,"&hour=",hour_string) -
更新 flask_server.py 中 show_prediction.html 的视图函数,以构建你希望在 show_prediction.html 中显示的字符串,用于每个预测结果:
-
if pred[0][0] >= 0.5: predict_string = "yes, delay predicted" else: predict_string = "no delay predicted"
通过这些更改,你应该能够使用第八章中描述的 Web 部署来简单地部署你的新模型。
9.11 使用 Facebook Messenger 部署模型
如果你选择使用第八章中描述的 Facebook Messenger 部署方法来解决你的新问题,你将需要更新 actions.py 中的评分代码,包括将 score_cols 设置为训练模型所使用的列名。你还需要更新设置默认值的代码,以防用户在评分时没有提供这些值。通过这些更改,你将拥有准备就绪的 Python 代码,用于使用训练好的模型评分新的数据点。
使用 Rasa 与 Facebook Messenger 进行部署时,Python 代码并不是全部。你还需要在 Facebook Messenger 中有一个自然的界面,为此,你需要创建一个简单的 Rasa 模型。你可以从街车延误部署的示例中了解如何指定 nlu.md 文件中的句子级 Rasa 训练示例和在 stories.md 文件中的多句子 Rasa 示例。定义正确的 slot 集合更具挑战性。我建议你为score_cols中的每个列名创建一个 slot。你可以在 domain.yml 文件中定义这些 slot。为了简化,你可以将每个 slot 的类型设置为 text。最好避免不必要的 slot,所以如果你是从街车延误示例中复制 domain.yml 文件作为起点,请在定义新的 slot 值之前清除现有的 slot 值。
要完成新模型部署的其余部分,你将遵循第八章中部署街车延误预测模型时遵循的以下步骤子集:
-
创建一个名为 new_deploy 的目录。
-
在 deploy 目录中运行以下命令以设置基本的 Rasa 环境:
rasa init -
分别将你的训练模型的 h5 文件和 pipelines 的 pkl 文件复制到 models 和 pipelines 目录。
-
将 new_deploy 目录中的 actions.py 文件替换为你为新的部署更新的 actions.py 文件。
-
将 data 子目录中的 nlu.md 和 stories.md 文件替换为你为新的部署创建的 nlu.md 和 stories.md 文件。
-
将 new_deploy 目录中的 domain.yml 文件替换为你的新部署的 domain.yml 文件。
-
将 repo 中的 custom_classes.py 和 endpoints.yml 文件复制到新的 deploy 目录。
-
将 deploy_config.yml 配置文件复制到新的 deploy 目录,并更新 pipeline 和模型文件名参数以匹配步骤 3 中复制的文件,如下所示。
列表 9.3 需要在部署配置文件中更新的参数
general: debug_on: False logging_level: "WARNING" # switch to control logging - WARNING for full ➥ logging; ERROR to minimize logging BATCH_SIZE: 1000 file_names: pipeline1_filename: <your pipeline1 pkl file> ❶ pipeline2_filename: <your pipeline2 pkl file> model_filename: <your trained model file> ❷❶ 替换为步骤 3 中复制到 pipelines 目录的 pipeline 文件名。
❷ 替换为步骤 3 中复制到 models 目录的训练模型 h5 文件名。
-
在你的 new_deploy 目录中运行以下命令以在 Rasa 的 Python 环境中调用 actions.py:
rasa run actions -
在你安装 ngrok 的目录中,调用 ngrok 以使你的 localhost 在端口 5005 上可供 Facebook Messenger 使用。以下是 Windows 的命令;注意 ngrok 输出中的 HTTPS 转发 URL:
.\ngrok http 5005 -
在 new_deploy 目录中运行以下命令以训练 Rasa 模型:
rasa train -
按照以下链接中的说明 http://mng.bz/oRRN 添加一个新的 Facebook 页面。您可以使用在第八章中创建的相同 Facebook 应用进行这次新部署。注意记录页面访问令牌和应用程序密钥;您需要在第 13 步更新 credentials.yml 文件,并使用这些值。
-
更新 new_deploy 目录中的 credentials.yml 文件,以设置验证令牌(您选择的字符串值)、密钥和页面访问令牌(在第 12 步的 Facebook 设置期间提供):
facebook: verify: <verify token that you choose> secret: <app secret from Facebook app setup> page-access-token: <page access token from Facebook app setup> -
在 new_deploy 目录中运行以下命令以启动 Rasa 服务器,使用第 13 步在 credentials.yml 中设置的凭据:
rasa run —credentials credentials.yml -
在第八章中创建的 Facebook 应用中,选择“消息传递”->“设置”,滚动到 Webhooks 部分,并点击“编辑回调 URL”。将回调 URL 值的前一部分替换为在第 10 步调用 ngrok 时记录的 HTTPS 转发 URL。在第 13 步中设置的 verify token(您选择的字符串值)输入到“验证令牌”字段中,然后点击“验证并保存”。
-
在 Facebook 消息传递(移动或 Web 应用)中,搜索第 12 步创建的 Facebook 页面的 ID,并输入查询以确认您的部署模型是可访问的。
我们已经审查了将用于电车延误问题的方法适应新结构数据集所需的步骤。现在,当您想要将深度学习应用于结构化数据时,您已经有了应用本书中代码示例所需的新问题。
9.12 将本书中的方法适应不同的数据集
为了使将本书中的方法应用于新的问题领域更容易,我们将通过将用于电车延误预测问题的代码适应新数据集的过程。我们不会处理整个端到端问题——只从初始数据集到最小训练的深度学习模型。
我们希望找到一个足够大的数据集,以便给深度学习一个公平的机会(记录数至少在数千条),但又不能太大,以至于数据量成为练习的主要焦点。我检查了流行的机器学习竞赛网站 Kaggle,以找到一个适合的表格结构化数据问题。我发现预测纽约市 Airbnb 物业价格的www.kaggle.com/dgomonov/new-york-city-airbnb-open-data问题有一个有趣的数据集,可能适合适应本书中描述的方法。图 9.18 显示了该数据集的数据片段。

图 9.18 Airbnb 纽约市数据集的示例记录
该数据集包含略少于 89,000 条记录,分布在 16 列中,因此数据集的大小和复杂性是合适的。让我们检查每一列的内容:
-
id—列表的数字标识符 -
name—列表的描述 -
host_id—与列表关联的主人的数字标识符 -
host_name—与列表关联的主人的名字 -
neighbourhood_group—列表所在的纽约市行政区:曼哈顿、布鲁克林、皇后区、布朗克斯或斯塔滕岛 -
neighbourhood—列表所在的社区 -
latitude—列表的纬度 -
longitude—列表的经度 -
room_type—列表的房间类型:整个住宅、私人房间或共享房间 -
price—列表的价格(深度学习模型的目标) -
minimum_nights—列表可以预订的最少夜数 -
number_of_reviews—Airbnb 网站上可用的列表评论数量 -
last_review—列表最近一次评论的日期 -
reviews_per_month—列表每月的平均评论数量 -
calculated_host_listings_count—与该列表关联的主人的列表数量 -
availability_365—该列表一年中可供出租的比例
我们得到了这些列类型的一个有趣的混合:
-
连续 —
price、minimum_nights、number_of_reviews、reviews_per_month、calculated_host_listings_count、availability_365 -
分类 —
neighbourhood_group、neighbourhood、room_type、host_id -
文本 —
name和可能host_name
除了这些容易分类的列之外,还有 id(从模型训练的角度来看并不有趣)、longitude 和 latitude(在本练习中,我们将依赖 neighborhood 来确定位置),以及 last_review(在本练习中,我们不会使用此列)。
与电车延误数据集相比,Airbnb 纽约市数据集的混乱程度要低得多。首先,让我们看看每个列中缺失值和唯一值的数量(图 9.19)。
与电车延误数据集相比,Airbnb 纽约市数据集具有较少的缺失值列。此外,所有分类列(neighbourhood_group、neighbourhood 和 host_name)似乎都有合理的值数量。相比之下,电车延误数据集中的方向、位置和路线列都包含了一些无关的值。例如,原始电车延误数据集中的方向列有 15 个不同的值,但只有 5 个有效值。
Airbnb 纽约数据集中相对较少的杂乱突显了从 Kaggle 可用的数据集的问题之一。尽管这些数据集对于练习机器学习的各个方面很有用,尽管参加 Kaggle 的比赛是学习的好方法,但用于比赛的精心策划和清洗的数据集并不能替代真实世界的数据集。正如您在第三章和第四章中看到的,真实世界的数据集在意外的方式下很杂乱,并且需要非平凡的工作来准备训练深度学习模型。Airbnb 数据集所需的数据准备量有限(将 CSV 文件导入 Pandas dataframe 并用默认值替换缺失值)包含在 airbnb_data_preparation 笔记本中。

图 9.19 Airbnb 纽约数据集的列特征
下一列表显示了与此示例相关的存储库中的文件。
列表 9.4 与 Airbnb 定价预测示例相关的代码存储库中的代码
├── data
│ AB_NYC_2019_remove_bad_values_jun21_2020.pkl ❶
│
├── notebooks
│ airbnb_data_preparation.ipynb ❷
│ airbnb_data_preparation_config.yml ❸
│ airbnb_model_training.ipynb ❹
│ airbnb_model_training_config.yml ❺
❶ airbnb_data_preparation 笔记本保存的清洗后的数据集输出
❷ 数据准备笔记本
❸ 数据准备配置文件
❹ 模型训练笔记本
❺ 模型训练配置文件
airbnb_data_preparation 笔记本保存了清洗后的 dataframe 的 pickle 版本,该版本可以用作输入到深度学习模型训练笔记本 airbnb_model_training。这个笔记本是街车延误预测问题模型训练笔记本的简化版本。这个简单模型的目标是 Airbnb 物业的价格是否会低于(0)或高于(1)平均价格。与本书主要示例中使用的版本相比,这个笔记本的关键变化包括以下内容:
-
列表列的成员资格(分类、连续和文本列)在配置文件 airbnb_model_training_config.yml 中设置,而不是在笔记本本身中(见下一列表)。
-
列表 9.5 Airbnb 价格预测模型列类别的参数
categorical: ❶ - 'neighbourhood_group' - 'neighbourhood' - 'room_type' continuous: ❷ - 'minimum_nights' - 'number_of_reviews' - 'reviews_per_month' - 'calculated_host_listings_count' text: [] ❸ excluded: ❹ - 'price' - 'id' - 'latitude' - 'longitude' - 'host_name' - 'last_review' - 'name' - 'host_name' - 'availability_365'❶ 分类列列表
❷ 连续列列表
❸ 文本列列表
❹ 从模型训练中排除的列列表
-
数据集直接从由 airbnb_data_preparation 笔记本生成的 pickle 文件中读取,并输入到管道中。相比之下,用于街车延误预测模型的模型训练笔记本包含大量代码来重构数据集。
为了检查模型训练如何与 Airbnb 纽约数据集一起工作,我们将运行第六章中用于街车延误预测模型的相同一系列实验,使用以下列来训练模型:
-
连续 —
minimum_nights、number_of_reviews、reviews_per_month、calculated_host_listings_count -
分类 —
neighbourhood_group、neighbourhood、room_type
您可以在图 9.20 中看到这些实验在 Airbnb 纽约模型上的结果。
正如你所见,通过最小量的代码更改,我们能够使用 Airbnb 纽约市数据集获得合理的结果。本节中描述的适应方法远非完整,但它展示了如何将本书中描述的方法适应于训练新的数据集的深度学习模型。

图 9.20 Airbnb 纽约市模型实验摘要
9.13 额外学习资源
在本书中,我们涵盖了构建端到端深度学习解决方案以解决电车延误预测问题的广泛技术问题。但当我们谈到深度学习这个极其丰富且快速发展的世界时,我们只是触及了表面。以下是一些额外的资源,如果你想要更深入地了解深度学习,我推荐你查看:
-
在线 深度学习概述课程 — fast.ai 的《面向程序员的实用深度学习》课程(
course.fast.ai/)是一个易于理解的深度学习入门课程,侧重于通过实践学习。它涵盖了经典深度学习应用,如图像分类,以及推荐系统和其他深度学习应用。我感谢这门课程激发了我将深度学习应用于结构化数据的兴趣。你可以免费在线跟随课程学习,同时使用课程论坛与其他学习者建立联系。讲师 Jeremy Howard 讲解清晰且充满激情。 -
另一种了解深度学习的方法是 deeplearning.ai 的《深度学习专项课程》(
mng.bz/PPm5)。由深度学习传奇人物 Andrew Ng 教授的这一系列在线课程从深度学习的理论基础知识开始,逐步扩展到涵盖编码主题。你可以免费审计 deeplearning.ai 课程,但若要获得课程评分和完成课程后的证书,则需要付费。该专项课程分为五个主题,涵盖了与深度学习相关的技术和实际问题。fast.ai 中的编码工作更有趣,但 deeplearning.ai 在深度学习背后的数学方面做得更好。如果你有时间和精力,完成这两个项目将为你提供一个全面的深度学习基础。 -
书籍 —我在第一章介绍了 Francois Chollet 的《Python 深度学习》,这是一本关于如何使用 Python 应用深度学习的优秀概述。如果你想获得更多使用 fast.ai 课程中使用的 PyTorch 库的经验,Eli Stevens 等人所著的《PyTorch 深度学习》是一个很好的资源。Stephan Raaijmakers 所著的《自然语言处理深度学习》是一本专注于深度学习特定应用的书籍,Mohamed Elgendy 所著的《视觉系统深度学习》也是如此。如果你想在其他语言中考察深度学习,François Chollet 和 J. J. Allaire 合著的《R 语言深度学习》提供了使用其他经典机器学习语言探索深度学习的方法,Shanqing Cai 等人合著的《JavaScript 深度学习》则展示了如何利用 TensorFlow.js 在 Web 开发的通用语言 JavaScript 中创建深度学习模型。最后,fast.ai 课程的主要讲师 Jeremy Howard 是《Fastai & PyTorch 深度学习编码者指南》(O’Reilly Media,2020)的合著者,这本书不仅扩展了 fast.ai 课程的内容,还包含了一个关于结构化数据深度学习的章节。
-
其他资源 —除了在线课程和书籍之外,还有许多关于深入学习更多知识的来源。实际上,关于深度学习的材料如此之多,以至于很难确定最佳来源。为了了解前沿动态,arXiv 对最近机器学习提交的审阅列表(
arxiv.org/list/cs.LG/recent)是一个很好的起点,尽管材料的数量和挑战性可能会让人望而却步。我依赖 Medium,尤其是 Towards Data Science 出版物(towardsdatascience.com),以获取关于深度学习主题的定期、易于消化的文章。Medium 也是一个友好的地方,可以撰写文章与对机器学习感兴趣的其他人分享你的技术成就。
除了这些用于深度学习的资源之外,在将深度学习应用于结构化数据领域的进展方面也有一些有趣的发展。例如,Google 的 TabNet (arxiv.org/abs/1908.07442) 直接针对将深度学习应用于结构化数据的问题。在 mng.bz/ v99x 的文章中,提供了对 TabNet 方法的精彩总结以及将 TabNet 应用于新问题的实用指南。文章解释说,TabNet 实现了一个注意力机制 (mng.bz/JDmQ),这使得网络能够学习对输入的哪个子集进行关注,并促进了可解释性(识别哪些输入对输出是重要的)。
摘要
-
你可以向用于训练电车延误预测模型的数据集中添加额外的数据源。例如,你可以添加关于电车路线子集的信息,以便你的用户能够获得针对电车路线特定部分的预测。你还可以结合天气数据来考虑极端天气对电车延误的影响。
-
本书描述的方法可以应用于其他数据集。通过一些小的修改,你可以将电车延误数据准备和模型训练笔记本适应,以训练一个基本模型来预测纽约市 Airbnb 物业的价格。
-
当你在评估某个结构化数据问题是否适合深度学习时,你应该确认数据集足够大(至少有数万条记录),足够多样化(各种类型的列),并且足够平衡(有足够的示例,以便深度学习模型能够捕捉到信号),以便适用于深度学习。
-
你可以使用 ngrok 将第八章中的 Web 部署提供给本地系统之外的用户。
-
深度学习的知识体系一直在不断增长。你可以利用从不同角度探讨深度学习的书籍,例如其他编程语言(如 JavaScript)或其他应用领域(如自然语言处理)。除了书籍之外,还有优秀的在线资源,包括课程、博客和学术论文。
附录。使用 Google Colaboratory
在第二章中,我们介绍了创建和训练您的深度学习模型可用的开发环境。在那个章节中,我推荐 Paperspace Gradient 作为一个平衡成本和特性的最佳基于云的深度学习开发环境。Gradient 不是免费的,但跟踪 Gradient 的成本比控制 Google Cloud Platform、AWS 或 Azure 中的机器学习环境成本要容易得多。然而,如果您的首要关注点是成本,那么 Google 的 Colaboratory(在本附录的其余部分中称为Colab)提供了一个完全免费的、对于基本的深度学习项目来说绰绰有余的环境。在本附录中,我们将讨论您需要了解的关键点,以便使用 Colab 练习代码示例,并将其优势与 Paperspace Gradient 的优势进行对比。
A.1 Colab 简介
Colab 是一个免费的、基于云的 Jupyter Notebooks 环境,您可以使用它来开发您的深度学习项目。Google 提供了一份全面的 Colab 使用介绍(mng.bz/w92g),涵盖了您开始所需的所有内容。一篇位于mng.bz/VdBG的文章也包含了许多有用的提示。
这里是一些关于 Colab 关键特性的快速总结:
-
Colab 提供各种硬件配置,包括对 GPU 和 TPU(Google 专门为与 TensorFlow 一起使用而设计的硬件加速器)的访问。
-
要使用 Colab,您需要一个 Google ID(
mng.bz/7VY4)。 -
如果你还没有为你的 Google 账户设置好 Google Drive,请按照
mng.bz/4BBB上的说明来设置 Drive。 -
Colab 有一个界面,它结合了 JupyterLab(Jupyter 的基于网络的界面)的一些方面。尽管这个界面与熟悉的 Jupyter Notebooks 界面不完全相同,但只需几分钟就能习惯它,并且它有一些标准笔记本中没有的实用功能,包括目录和代码片段画廊,您可以将它们轻松地复制到您的 Colab 笔记本中。图 A.1 显示了 Colab 界面。
![APPA_F01_Ryan]()
图 A.1 Colab 界面:并非普通的笔记本
-
默认情况下,当您在 Colab 中保存笔记本时,它将保存在 Drive 中的一个特殊目录中,因此您可以在 Colab 之外访问您的工作(图 A.2)。
![APPA_F02_Ryan]()
图 A.2 Google Drive 中 Colab 笔记本的默认目录
本节涵盖了您使用 Colab 所需了解的一些关键信息。您可以参考 Google 文档(mng.bz/mgE0)以获取关于如何使用 Colab 的完整详细信息,但我们在下一节将介绍一个基本功能:如何在 Colab 中使 Google Drive 可用。
A.2 在您的 Colab 会话中使 Google Drive 可用
要充分利用 Colab,您需要挂载您的 Google Drive,以便在您的 Colab 会话中访问。当您设置了对 Drive 的访问权限后,您的 Colab 笔记本可通过路径/content/drive/My Drive 访问。您可以从 Drive 中的目录读取文件,并将文件写入 Drive,就像写入本地文件系统一样。
要从您的笔记本中访问 Drive 上的文件,请按照以下步骤操作:
-
在您的笔记本中运行以下语句:
from google.colab import drive drive.mount('/content/drive') -
当您运行这些语句时,您会得到图 A.3 中显示的结果。
![APPA_F03_Ryan]()
图 A.3 输入授权码提示
-
点击链接选择账户(图 A.4)。
![APPA_F04_Ryan]()
图 A.4 选择账户
-
在 Google Drive File Stream 访问屏幕中,点击允许(图 A.5)。
![APPA_F05_Ryan]()
图 A.5 允许 Google Drive File Stream 访问
-
在登录屏幕中,点击复制图标以复制您的访问码(图 A.6)。
-
返回 Colab,在授权码字段中粘贴,然后按 Enter(图 A.7)。
单元运行并产生图 A.8 中显示的挂载消息,以确认您的 Google Drive 已挂载并且可用于您的 Colab 笔记本。
通过遵循本节中的步骤,您已在您的 Colab 笔记本中使 Google Drive 可用。在 A.3 节中,我们将对比 Colab 和 Paperspace Gradient 的优势。

图 A.6 复制访问码

图 A.7 在您的 Colab 笔记本中粘贴访问码

图 A.8 确认 Google Drive 已成功挂载
A.3 在 Colab 中使仓库可用并运行笔记本
如果您使用 Colab 运行本书中的代码示例,您需要了解 Colab 和 Drive 协同工作的一些怪癖。除了遵循 A.2 节中的说明使 Drive 在 Colab 中可访问外,您还需要
-
将仓库(
mng.bz/xmXX)克隆到 Drive 中的新文件夹。 -
确保您运行笔记本时当前目录是您仓库克隆的笔记本目录。
首先,按照以下步骤克隆仓库:
-
在 Drive 中,在根文件夹中创建一个新文件夹。
对于这个练习,将新文件夹命名为 dl_june_17。
-
访问新文件夹,右键单击背景,并在上下文菜单中选择更多 -> Google Colaboratory(图 A.9)。
Colab 在新标签页中打开。
![APPA_F09_Ryan]()
图 A.9 从 Drive 的新文件夹中启动 Colab
-
选择连接 -> 连接到托管运行时(图 A.10)。
![APPA_F10_Ryan]()
图 A.10 在 Colab 中连接到托管运行时
-
按照 A.2 节中的步骤使 Drive 在 Colab 中可访问。
-
要访问步骤 1 中创建的 dl_june_17 文件夹,请在您的笔记本中创建一个新单元格,点击+code;然后将以下代码复制并粘贴到新单元格中并运行:
%cd /content/drive/My Drive/dl_june_17 -
在 Colab 笔记本中创建另一个新单元格,并在新单元格中作为单行运行以下命令以克隆仓库:
! git clone https://github.com/ryanmark1867/\ deep_learning_for_structured_data.git
现在您已经将仓库克隆到驱动器中,您就可以打开 Colab 中的一个笔记本了。以下步骤显示了如何从仓库中打开模型训练笔记本并使其准备好运行:
-
在 Colab 中,选择文件 -> 定位到驱动器(图 A.11)。
![APPA_F11_Ryan]()
图 A.11 定位到驱动器菜单选择
-
导航到您克隆仓库的笔记本目录。
-
双击 streetcar_model_training.ipynb,在出现的屏幕中选择 Google Colaboratory(图 A.12)。
-
notebook streetcar_model_training.ipynb 在 Colab 中打开。
-
选择连接 -> 连接到托管运行时。
-
按照 A.2 节中的说明操作,以确保在此笔记本中可以访问驱动器。
-
在笔记本中添加一个新单元格,并运行以下命令以将克隆仓库中的笔记本目录设置为当前目录:
%cd /content/drive/My Drive/dl_june_17/deep_learning_for_structured_data
现在您已经完成了使仓库在 Colab 中可访问以及使笔记本可运行的步骤。请注意,每次您开始一个新的 Colab 会话,在您打开其中一个笔记本之后,您都需要按照步骤使驱动器在笔记本中可访问,并将克隆仓库中的笔记本目录设置为当前目录。

图 A.12 在驱动器中双击笔记本文件时出现的屏幕
A.4 Colab 和 Paperspace 的优缺点
您选择 Colab 还是 Paperspace Gradient 来处理您的深度学习项目将取决于您的需求。对于大多数人来说,成本是选择 Colab 的决定性因素。在我看来,Paperspace Gradient 在便利性和可预测性方面的优势足以证明其成本。但如果您想要一个零成本的选项,Colab 是一个很好的选择。本节对比了 Colab(包括成本)的优势与 Paperspace Gradient 的强项。
这里是 Colab 的一些优点:
-
免费 — Paperspace Gradient 有适度的每小时费用和完全透明的计费模式,但您仍然需要为 Paperspace Gradient 笔记本的每个活动小时付费。此外,在基本订阅下,Gradient 笔记本在 12 小时后自动关闭(图 A.13)。如果您有一个活动的 Gradient 会话并且忘记关闭它,您将为此支付 12 小时。我根据经验知道,醒来并意识到我因为忘记关闭 Gradient 而浪费了一些钱,我的笔记本整夜都在运行,这种感觉真的很糟糕。
![APPA_F13_Ryan]()
图 A.13 在基本订阅下,Gradient 笔记本在 12 小时后自动关闭。
-
与谷歌驱动集成 —如果您已经利用了谷歌驱动,您会欣赏 Colab 与 Drive 的流畅集成。谷歌在这方面做得非常出色。
-
庞大的用户社区 —Colab 拥有庞大的用户社区,许多关于它的问题在 Stack Overflow(
mng.bz/6gO5)上得到了解答。Paperspace 表示,超过 10 万开发者使用 Gradient(paperspace.com/gradient)。我未能找到关于 Colab 用户的类似估计,但 Colab 在 Stack Overflow 上的流量表明,它拥有更大的用户社区。
这里是 Paperspace Gradient 的一些优势:
-
完全集成的环境 —Paperspace Gradient 完全针对深度学习进行了优化,并使用标准的 Jupyter 环境。相比之下,Colab 有一些较为复杂的方面,如果您习惯了普通的 Jupyter 笔记本,可能需要一些时间来适应。
-
独立于谷歌基础设施 —Colab 与谷歌基础设施深度融合;您需要谷歌 ID 和谷歌驱动来使用 Colab。如果您在一个限制访问谷歌基础设施的司法管辖区工作,这一要求可能会阻止您使用 Colab。您日常工作的地点并不是唯一需要考虑的因素;问问自己,您是否需要在访问谷歌基础设施受限的司法管辖区进行深度学习工作的演示或会议展示。
-
专用资源 —您的 Paperspace Gradient 虚拟环境属于您,一旦启动实例,您就可以访问所有资源。Colab 的资源没有保证,您可能在特定时间无法获得深度学习项目所需的资源。如果您对工作的时间灵活,这种情况应该不会成为问题。我从未在 Colab 中遇到获取资源的问题,但从理论上讲,您可能在需要时无法在 Colab 中获得 GPU 或 TPU 资源。
-
支持 —当您设置 Paperspace Gradient 环境时,您需要为环境活跃的每一小时付费。您从这笔费用中获得的部分是支持服务。在过去两年中,我不得不联系 Paperspace 支持三次,每次都从 Paperspace 那里得到了快速的初步响应和问题的快速解决。Colab 是免费的,因此您将不会获得这种个性化的支持。
-
更快的模型训练 —比较第七章中实验 1 的训练运行(10 个 epoch,无提前停止,默认权重为 0 和 1 的结果),Paperspace 比 Colab 快约 30%(Paperspace 为 1 分 49 秒,Colab 为 2 分 32 秒)。我的经验是,通过在笔记本设置中的硬件加速器字段选择 None 或 TPU,可以得到最佳结果(通过选择运行时 -> 更改运行时类型显示)。将 GPU 作为硬件加速器选择会导致本测试结果更差(图 A.14)。
![APPA_F14_Ryan]()
图 A.14 设置硬件加速器
-
训练运行的一致时间 —在 Paperspace 上多次运行相同的训练实验所需时间大致相同,而将相同的训练实验在 Colab 上运行所需时间则差异很大。
在本节中,我们回顾了 Colab 和 Paperspace Gradient 的优缺点。这两个环境都是深度学习项目的优秀选择;你选择哪一个取决于你自己的需求。










































浙公网安备 33010602011771号