Python-大规模机器学习-全-
Python 大规模机器学习(全)
零、前言
| | “有脑子的好处是可以学习,无知可以被知识取代,小知识可以逐渐堆积成大堆。” | |
| | - 道格拉斯·霍夫斯塔德 |
机器学习通常被称为人工智能中真正起作用的部分。它的目标是找到一个基于现有数据集(训练集)的函数,以便以最高可能的正确性预测以前看不到的数据集(测试集)的结果。这或者以标签和类的形式出现(分类问题),或者以连续值的形式出现(回归问题)。机器学习在现实应用中的具体例子从预测未来股票价格到从一组文档中对作者的性别进行分类。在这本书里,最重要的机器学习概念,以及适用于更大数据集的方法,将通过 Python 中的实际例子向读者阐明。我们将研究监督学习(分类&回归),以及已被发现适用于较大数据集的无监督学习(如主成分分析(PCA)、聚类和主题建模)。
谷歌、脸书和优步等大型信息技术公司声称他们成功地大规模应用了这种机器学习方法,从而引起了很大的轰动。随着大数据的出现和可用,对可扩展机器学习解决方案的需求呈指数级增长,许多其他公司和个人已经开始渴望成熟大数据集中隐藏相关性的成果。不幸的是,大多数学习算法不能很好地扩展,这使得台式计算机或更大的计算集群上的 CPU 和内存都很紧张。在这期间,即使大数据已经过了炒作的高峰期,可扩展的机器学习解决方案也并不充裕。
坦率地说,我们仍然需要解决许多瓶颈,即使是我们很难归类为大数据的数据集(想想高达 2GB 甚至更小的数据集)。这本书的使命是提供方法(有时是非传统的方法),在更大规模上应用最强大的开源机器学习方法,而不需要昂贵的企业解决方案或大型计算集群。在本书中,我们将使用 Python 和其他一些现成的解决方案,它们很好地集成在可扩展的机器学习管道中。阅读这本书是一次旅程,它将重新定义你对机器学习的了解,将你置于真正大数据分析的起点。
这本书涵盖了什么
第 1 章、可伸缩性的第一步,在正确的视角下设置了可伸缩机器学习的问题,并让你熟悉我们将在本书中使用的工具。
第 2 章、Scikit-learn 中的可扩展学习,讨论了随机梯度下降(SGD)的策略,在此策略中,我们减轻了内存消耗;它是基于核心外学习的主题。我们还将讨论可以处理各种数据的数据准备技术,例如哈希技巧。
第 3 章、快速学习支持向量机涵盖了能够以支持向量机的形式发现非线性的流算法。我们将介绍 Scikit-learn 的替代方案,例如 LIBLINEAR 和 Vowpal Wabbit,它们虽然作为外壳命令运行,但可以很容易地被 Python 脚本包装和指导。
第 4 章、神经网络和深度学习,提供了在 antio 框架内应用深度神经网络的有用策略,以及与 H2O 的大规模应用。尽管这是一个热门话题,但成功应用它可能是一个相当大的挑战,更不用说提供可扩展的解决方案了。我们还将借助带有自动编码器的无监督预训练。
第 5 章、使用 TensorFlow 进行深度学习,涵盖了有趣的深度学习技术以及神经网络的在线方法。尽管 TensorFlow 还处于起步阶段,但该框架提供了优雅的机器学习解决方案。我们还将在 TensorFlow 环境中利用 Keras 卷积神经网络的功能。
第 6 章、分级和回归树,解释了随机森林、梯度增强和 XGboost 的可扩展解决方案。CART 是分类和回归树的缩写,是一种机器学习方法,通常应用于集成方法的框架中。我们还将提供使用 H2O 的大规模应用的例子。
第 7 章、无监督学习在规模下,深入到无监督学习,因为我们将涵盖主成分分析、聚类分析和主题建模,使用正确的方法来扩大它们。
第 8 章、分布式环境–Hadoop 和 Spark ,教我们如何在虚拟机环境中设置 Spark,从单机转向计算网络范式。由于 Python 可以轻松地在一个机器集群上粘合和增强我们的工作,因此利用 Hadoop 集群的力量变得轻而易举。
第 9 章、带 Spark 的实用机器学习与 Spark 一起行动,教授立即开始操作数据和在大数据集上构建预测模型的所有要领。
附录、对图形处理器和图形处理器的介绍,将涵盖图形处理器和图形处理器计算的基础知识。如果您的系统允许,它将帮助您安装和准备您的环境,以便在图形处理器上使用安诺。
这本书你需要什么
执行本书中提供的代码示例需要在 macOS、Linux 或微软 Windows 上安装 Python 2.7 或更高版本。
整本书中的例子将频繁使用 Python 的基本库,如 SciPy、NumPy、Scikit-learn 和 StatsModels,以及少量的 matplotlib 和 pandas,用于科学和统计计算。我们还将利用一个名为 H2O 的核心外云计算应用。
这本书高度依赖于 Jupyter 及其由 Python 内核驱动的笔记本。我们将使用它的最新版本 4.1 来写这本书。
第一章将为您提供设置 Python 环境、这些核心库以及所有必要工具的所有分步说明和一些有用的提示。
这本书是给谁的
这本书适合有抱负和实际的数据科学从业者、开发人员,以及打算处理大型复杂数据集的所有人。我们努力使这本书尽可能为更多的读者所理解。然而,考虑到这本书的主题相当高级,建议读者熟悉基本的机器学习概念,如分类和回归、误差最小化函数和交叉验证,但不是严格强制的。
我们还假设有一些 Python、Jupyter Notebooks 和命令行执行的经验,以及合理的数学知识水平,以掌握我们提出的各种大型解决方案背后的概念。文本是以其他语言(R、Java 和 MATLAB)的程序员可以遵循的风格编写的。理想情况下,它非常适合(但不限于)熟悉机器学习并对利用 Python 感兴趣的数据科学家,相对于其他语言,如 R 或 MATLAB,因为它具有计算、内存和输入/输出能力。
惯例
在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。
文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“检查线性模型时,首先检查coef_属性。”
代码块设置如下:
from sklearn import datasets
iris = datasets.load_iris()
由于我们将在大多数示例中使用 Jupyter Notebooks,因此期望包含代码块的单元格中总是有一个输入(标记为In:)和一个输出(标记为Out:)。在您的电脑上,您只需在In:后输入代码,并检查结果是否与Out:内容一致:
In: clf.fit(X, y)
Out: SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, degree=3, gamma=0.0, kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
当一个命令应该在终端命令行中给出时,你会发现这个命令带有前缀$>,否则,如果是 Python REPL,它的前面会有>>>:
$>python
>>> import sys
>>> print sys.version_info
新名词和重要词语以粗体显示。你在屏幕上看到的单词,例如,在菜单或对话框中,出现在文本中,如下所示:“通常,你只需在单元格中的 In: 后键入代码并运行它。”
注
警告或重要提示会出现在这样的框中。
型式
提示和技巧是这样出现的。
读者反馈
我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。
要给我们发送一般反馈,只需发送电子邮件<[feedback@packtpub.com](mailto:feedback@packtpub.com)>,并在您的邮件主题中提及书名。
如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于www.packtpub.com/authors的作者指南。
客户支持
现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。
下载示例代码
你可以从你在http://www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 使用您的电子邮件地址和密码登录或注册我们的网站。
- 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。
- 点击代码下载&勘误表。
- 在搜索框中输入图书名称。
- 选择要下载代码文件的书籍。
- 从您购买这本书的下拉菜单中选择。
- 点击代码下载。
您也可以通过点击 Packt 出版网站图书网页上的代码文件按钮来下载代码文件。可以通过在搜索框中输入图书名称来访问该页面。请注意,您需要登录您的 Packt 帐户。
下载文件后,请确保使用最新版本的解压缩文件夹:
- 视窗系统的 WinRAR / 7-Zip
- zipeg/izp/un ARX for MAC
- 适用于 Linux 的 7-Zip / PeaZip
这本书的代码包也托管在 GitHub 上,网址为。我们还有来自丰富的图书和视频目录的其他代码包,可在https://github.com/PacktPublishing/获得。看看他们!
型式
在 Github 上,你还可以找到 Windows 的 Vowpal Wabbit 可执行文件。
下载本书的彩色图片
我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从https://www . packtpub . com/sites/default/files/downloads/largescalechaningwith python _ color images . pdf下载此文件。
勘误表
尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问http://www.packtpub.com/submit-errata,选择您的书籍,点击勘误表提交表链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。
要查看之前提交的勘误表,请前往https://www.packtpub.com/books/content/support并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。
盗版
互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。
请通过<[copyright@packtpub.com](mailto:copyright@packtpub.com)>联系我们,获取疑似盗版资料的链接。
我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。
问题
如果您对本书的任何方面有问题,可以在<[questions@packtpub.com](mailto:questions@packtpub.com)>联系我们,我们将尽最大努力解决问题。
一、可扩展性的第一步
欢迎阅读这本书,关于 Python 的可扩展机器学习。
在本章中,我们将讨论如何使用 Python 从大数据中有效学习,以及如何使用您的单台机器或其他机器的集群来学习,例如,您可以从 【亚马逊网络服务】 ( AWS )或谷歌云平台获得这些机器。
在书中,我们将使用 Python 实现可扩展的机器学习算法。这意味着它们可以处理大量数据,并且不会因为内存限制而崩溃。它们还需要合理的时间,这对于数据科学原型和生产部署来说是可以管理的。章节围绕解决方案(如流数据)、算法(如神经网络或树的集合)和框架(如 Hadoop 或 Spark)进行组织。我们还将为您提供一些关于机器学习算法的基本提示,并解释如何使它们可扩展并适用于大规模数据集的问题。
给定这样的前提作为开始,你将需要学习基础知识(以便弄清楚写这本书的视角),并设置所有基本工具来立即开始阅读章节。
在本章中,我们将向您介绍以下主题:
- 可伸缩性实际上意味着什么
- 处理数据时应该注意哪些瓶颈
- 这本书会帮助你解决什么样的问题
- 如何使用 Python 有效地大规模分析数据集
- 如何快速设置机器来执行本书中的示例
让我们一起围绕使用 Python 的可扩展解决方案开始这一旅程!
详细说明可扩展性
即使现在炒作的是大数据,大数据集早在术语本身被创造出来之前就已经存在了。来自射电望远镜的大量文本、脱氧核糖核酸序列和大量数据一直是科学家和数据分析师面临的挑战。由于大多数机器学习算法的计算复杂度为 O(n 2 )甚至 O(n 3 ),其中 n 是训练实例的数量,数据科学家和分析师之前已经通过求助于可以更高效的数据算法来面对来自海量数据集的挑战。在大数据集的情况下,当机器学习算法在适当的设置后可以工作时,它被认为是可扩展的。数据集可能很大,因为有大量的案例或变量,或者两者都有,但是可伸缩的算法可以有效地处理它,因为它的运行时间几乎随着问题的大小而线性增加。因此,这只是一个用更多数据交换 1:1 更多时间(或更多计算能力)的问题。相反,机器学习算法在面对大量数据时无法扩展;它只是停止工作或运行,运行时间以非线性方式增加,例如指数级增加,从而使学习变得不可行。
廉价数据存储、大 RAM 和多处理器 CPU 的引入极大地改变了一切,提高了单台笔记本电脑分析大量数据的能力。在过去的几年里,另一个巨大的游戏改变者出现了,将注意力从单一的强大机器转移到了商品计算机集群(更便宜、更容易获得的机器)。这一重大变化是引入了 MapReduce 和开源框架 Apache Hadoop 及其 Hadoop 分布式文件系统 ( HDFS )以及通用计算机网络上的并行计算。
为了弄清楚这两种变化是如何深刻和积极地影响你解决大规模问题的能力的,我们应该首先从实际上阻止你(并且仍然阻止,这取决于你的问题有多大)分析大数据集的原因开始。
无论您的问题是什么,您最终都会发现,由于以下任何一个限制,您无法分析您的数据:
- 影响分析执行时间的计算
- I/O 会影响您在一个时间单位内可以从存储器中取出多少数据
- 内存影响您一次可以处理多少大数据
你的电脑有局限性,这将决定你是否能从你的数据中学习,以及在你碰壁之前需要多长时间。计算限制出现在许多密集的计算中,输入/输出问题会使您对数据的即时访问成为瓶颈,最后,内存限制会限制您只接受部分数据,从而限制您可能访问的矩阵计算类型或估计的精度甚至准确性。
就您正在分析的数据而言,这些硬件限制中的每一项也会对您产生不同的严重影响:
- 高数据,其特点是大量的案例
- 广泛的数据,其特征是大量的特征
- 又高又宽的数据,既有大量的案例又有大量的特征
- 稀疏数据,其特征在于大量的零条目或可被转换成零的条目(也就是说,数据矩阵可以是高的和/或宽的但有信息,但不是所有的矩阵条目都有信息值)
最后,它归结为您将使用的算法,以便从数据中学习。每个算法都有自己的特点,能够使用受偏差或方差影响不同的解决方案来映射数据。因此,关于你的问题,到目前为止,你已经通过机器学习解决了,你认为,基于经验或经验测试,某些算法可能比其他算法工作得更好。对于大规模问题,在决定算法时,您必须添加其他不同的考虑因素:
- 你的算法有多复杂;也就是说,如果数据中的行数和列数以线性或非线性方式影响计算量。大多数机器学习解决方案基于二次或三次复杂度的算法,因此强烈限制了它们对大数据的适用性。
- 你的模型有多少参数;在这里,这不仅仅是估计值的方差问题(过度拟合),而是计算它们可能需要的时间问题。
- 如果优化过程是可并行的;也就是说,您是否可以轻松地将计算分散到多个节点或 CPU 内核中,或者您是否必须依赖单个、顺序的优化过程?
- 算法应该一次性从所有数据中学习,还是可以用单个例子或小批量数据来代替?
如果你用数据特征和这些类型的算法交叉评估硬件限制,你会得到一大堆可能有问题的组合,这些组合会阻止你从大规模分析中得到结果。从实践的角度来看,所有有问题的组合都可以通过三种方法解决:
- 纵向扩展,即通过软件或硬件修改(更多内存、更快的 CPU、更快的存储磁盘和使用图形处理器)来提高单台机器的性能
- 横向扩展,即利用外部资源,即其他存储磁盘和其他中央处理器(或图形处理器),在多台机器上分配计算(和性能)
- 向上扩展和向外扩展,也就是说,将最好的向上扩展和向外扩展解决方案结合在一起
制作大规模的例子
一些激励性的例子可能会让事情对你来说更加清晰和难忘。让我们举两个简单的例子:
- 能够预测点击率 ( CTR )可以帮助你在互联网广告如此广泛、扩散、蚕食传统媒体传播大份额的这些天赚得相当多
- 当你的客户在搜索你的网站所提供的产品和服务时,能够向他们提供正确的信息,如果你能猜出在他们的结果顶部放什么,就能真正提高你的销售机会
在这两种情况下,我们都有相当大的数据集,因为它们是由用户在互联网上的交互产生的。
根据我们心目中的业务(我们可以想象这里的一些大公司),在我们的两个例子中,我们每天都在谈论数百万个数据点。在广告的情况下,数据当然很高,是一个连续的信息流,因为最近的数据,更能代表市场和消费者,取代了旧的数据。在搜索引擎的例子中,数据是广泛的,被你提供给你的客户的结果所提供的特征所丰富:例如,如果你在旅行业务中,你将有相当多的关于酒店、位置和所提供的服务的特征。
显然,可伸缩性是这两个问题的一个问题:
- 你必须从每天都在增长的数据中学习,你必须快速学习,因为当你在学习的时候,新的数据不断到来。然而,你必须处理显然无法存储的数据,因为矩阵太高或太大。
- 为了适应新的数据,你经常需要更新你的机器学习模型。你需要一种能够及时处理信息的算法。由于数据量的原因,O(n2)或 O(n3)的复杂性可能是您无法处理的;你需要一些能够以较低复杂度工作的算法(比如 O(n))或者通过划分数据使得 n 会小很多很多。
- 你必须能够快速预测,因为预测只能交付给新客户。同样,算法的复杂性也很重要。
可伸缩性问题可以通过一种或多种方式解决:
- 通过降低问题的维度来扩大规模;例如,在搜索引擎的情况下,通过有效地选择要使用的相关特征
- 使用正确的算法扩大规模;例如,在广告数据的情况下,有适当的算法来有效地从流中学习
- 通过利用多台机器扩展学习过程
- 在单个服务器上使用多处理和矢量化有效地扩展部署过程
在这本书里,我们将为你指出每一个提出的解决方案或算法能解决什么样的实际问题。您可以自动将特定的时间和执行约束(中央处理器、内存或输入/输出)与我们建议的最合适的解决方案联系起来。
介绍蟒蛇
由于我们的论文将依赖于 Python——我们为本书选择的开源语言——在阐明 Python 如何轻松帮助您解决大规模数据问题之前,我们必须停下来简单介绍一下这种语言。
Python 创建于 1991 年,是一种通用的、解释的、面向对象的语言,它已经缓慢而稳定地征服了科学界,并成长为一个成熟的数据处理和分析专用包生态系统。它可以让你进行无数次快速的实验,轻松的理论开发,以及快速的科学应用部署。
作为机器学习实践者,你会发现使用 Python 很有趣,原因有多种:
- 它为数据分析和机器学习提供了一个大型、成熟的软件包系统。它保证您将在数据分析过程中获得您可能需要的一切,有时甚至更多。
- 它非常通用。无论你的编程背景或风格是什么(面向对象还是过程),你都会喜欢用 Python 编程。
- 如果你还不知道,但是你很了解其他语言,比如 C/C++或者 Java,那么学习和使用起来非常简单。掌握基础知识后,没有比立即开始编码更好的学习方法了。
- 它是跨平台的;您的解决方案将在 Windows、Linux 和 macOS 系统上完美流畅地运行。你不必担心便携性。
- 虽然解释了,但与 R、MATLAB 等其他主流数据分析语言相比,无疑是快了(虽然比不上 C、Java 和新出现的 Julia 语言)。
- 由于其最小的内存占用和出色的内存管理,它可以处理内存中的大数据。当你使用数据角力的各种迭代和重复加载、转换、切割、切片、保存或丢弃数据时,内存垃圾收集器通常会保存天。
型式
如果你还不是专家(实际上我们需要一些 Python 的基础知识才能充分利用这本书),你可以阅读关于语言的所有内容,并直接从位于https://www.python.org/的 Python 基础上找到基本安装文件。
使用 Python 进行扩展
Python 是一种解释语言;它运行从内存中读取您的脚本,并在运行时执行它,从而访问必要的资源(文件、内存中的对象等)。除了解释之外,使用 Python 进行数据分析和机器学习时需要考虑的另一个重要方面是 Python 是单线程的。单线程意味着任何 Python 程序从脚本的开始到结束都是按顺序执行的,并且 Python 不能利用您的计算机中可能存在的多线程和处理器提供的额外处理能力(现在大多数计算机都是多核的)。
在这种情况下,使用 Python 进行扩展可以通过不同的策略来实现:
- 编译 Python 脚本,以实现更快的执行速度。虽然使用例如PyPy—一个及时的 ( JIT )编译器可以在http://pypy.org/找到,但是我们实际上在我们的书中没有采用这样的解决方案,因为它需要从头开始用 Python 编写算法。
- 使用 Python 作为包装语言;从而将 Python 执行的操作与外部库和程序的执行放在一起,其中一些能够进行多核处理。在我们的书中,当我们调用支持向量机的库 ( LIBSVM )或程序如Vowpal Wabbit(VW)、 XGBoost 或 H2O 来执行机器学习活动时,你会发现许多这样的例子。
- 有效使用矢量化技术,即矩阵计算专用库。这可以通过使用 NumPy 或熊猫来实现,这两个都使用来自图形处理器的计算。GPU 就像多核 CPU,每一个都有自己的内存和并行处理计算的能力(你可以算出它们有多个微小的内核)。尤其是在使用神经网络时,基于图形处理器的矢量化技术可以令人难以置信地加快计算速度。然而,图形处理器有其自身的局限性;首先,他们的可用内存在将你的数据传递到他们的内存并将结果传回你的 CPU 时有一定的 I/O,他们需要通过特殊的 API 进行并行编程,比如 CUDA 针对 NVIDIA 制造的 GPU(所以你必须安装合适的驱动和程序)。
- 将一个大问题分成几个块,并在内存中一次解决一个块(分治算法)。这将导致对内存或磁盘中的数据进行分区或二次采样,并管理机器学习问题的近似解决方案,这非常有效。需要注意的是,分区和二次采样都可以用于案例和特征(以及两者)。如果原始数据保存在磁盘存储器上,输入/输出限制将成为最终性能的决定性因素。
- 根据您将使用的学习算法,有效地利用多处理和多线程。一些算法自然能够将它们的操作分成并行的操作。在这种情况下,唯一的约束就是你的 CPU 和内存(因为你的数据必须为你将要使用的每个并行工作者复制)。其他一些算法将利用多线程,从而在相同的内存块上同时管理更多的操作。
使用 Python 横向扩展
横向扩展解决方案只需将多台机器连接成一个集群。当您连接机器(横向扩展)时,您还可以使用更强大的配置(从而增加中央处理器、内存和输入/输出),应用我们在上一段中提到的技术并提高它们的性能,来扩展每台机器。
通过连接多台机器,您可以以并行方式利用它们的计算能力。您的数据将分布在多个存储磁盘/内存中,通过让每台机器仅处理其可用数据(即,其自己的存储磁盘或内存)来限制输入/输出传输。
在我们看来,这意味着通过以下方式有效利用外部资源:
- H2O 框架
- Hadoop 框架及其组件,如 HDFS、MapReduce 和另一个资源协商者 ( 纱
- Hadoop 之上的 Spark 框架
这些框架中的每一个都将由 Python 控制(例如,Spark 通过其名为 pySpark 的 Python 接口)。
用于大规模机器学习的 Python
鉴于机器学习有许多有用的软件包,并且它是一种在数据科学家中非常流行的编程语言,Python 是我们在本书中介绍的所有代码的首选语言。
在本书中,当必要时,我们将提供进一步的说明,以便安装任何进一步必要的库或工具。在这里,我们将开始安装基础,即 Python 语言和最常用的计算和机器学习包。
在 Python 2 和 Python 3 之间选择
在开始之前,重要的是要知道 Python 有两个主要分支:版本 2 和版本 3。随着许多核心功能的发生变化,为一个版本构建的脚本有时与另一个版本不兼容(如果不出现错误和警告,它们将无法工作)。虽然第三个版本是最新的,但旧版本仍然是科学领域使用最多的版本,也是许多操作系统的默认版本(主要是为了升级时的兼容性)。当版本 3 发布时(2008 年),大多数科学包还没有准备好,所以科学界坚持使用以前的版本。幸运的是,从开始,几乎所有的包都被更新了,只留下了少数几个(参见http://py3readiness.org的兼容性概述)作为 Python 3 兼容性的孤儿。
尽管 Python 3(我们不应该忘记,它是 Python 的未来)最近越来越受欢迎,但 Python 2 仍然在数据科学家和数据分析师中广泛使用。此外,很长时间以来,Python 2 一直是默认的 Python 安装(例如,在 Ubuntu 上),所以这是大多数读者应该已经准备好的最有可能的版本。出于所有这些原因,我们将在本书中采用 Python 2。这不仅仅是对旧技术的热爱,这只是为了让Python 大规模机器学习能够被最大的受众所接受的一个实际选择:
- Python 2 代码将立即面向现有的数据专家受众。
- Python 3 用户会发现很容易转换我们的脚本,以便在他们喜欢的 Python 版本下工作,因为我们编写的代码很容易转换,我们将提供我们所有脚本和笔记本的 Python 3 版本,可从 Packt 网站免费下载。
型式
如果你需要深入了解 Python 2 和 Python 3 的区别,我们建议阅读这个关于编写 Python 2-3 兼容代码的网页:
http://python-future.org/compatible_idioms.html
从 Python-Future 开始,你可能还会发现关于如何将 Python 2 代码转换为 Python 3 的阅读很有用:
http://python-future.org/automatic_conversion.html
安装 Python
作为第一步,我们将为数据科学创建一个工作环境,您可以使用它来复制和测试书中的示例,并原型化您自己的大型解决方案。
无论您打算用什么语言开发应用,Python 都将为您提供一个轻松的时间来获取数据,根据数据构建模型,并提取在生产环境中进行预测所需的正确参数。
Python 是一种开源的、面向对象的、跨平台的编程语言,与它的直接竞争对手(例如,C/C++和 Java)相比,它产生的代码非常简洁易读。它允许您在很短的时间内构建一个工作的软件原型,并在未来对其进行测试、维护和扩展。它已经成为数据科学家工具箱中使用最多的语言,因为最终,它是一种通用语言,由于有大量可用的包,可以轻松快速地帮助您解决各种常见和特殊的问题,因此变得非常灵活。
分步安装
如果你从未使用过 Python(但这并不意味着你的机器上可能还没有安装它),你需要首先从项目的主网站https://www.python.org/downloads/下载安装程序(记住,我们使用的是版本 3),然后在你的本地机器上安装它。
本节为您提供了对机器上可安装内容的完全控制。当您打算使用 Python 作为原型和生产语言时,这非常有用。此外,它可以帮助您跟踪您正在使用的包的版本。无论如何,请注意,逐步安装确实需要时间和精力。相反,安装一个现成的科学发行版将减轻安装程序的负担,并且它可能非常适合首先开始和学习,因为它可以为您节省大量时间,尽管它将一次性在您的计算机上安装大量软件包(在大多数情况下,您可能永远不会使用)。因此,如果你想立即开始,不想为控制你的安装费太多心思,跳过这一部分,进入下一部分,科学发行版。
作为一种多平台编程语言,您可以找到运行在 Windows 或类似 Linux/Unix 操作系统上的计算机的安装程序。请记住,一些 Linux 发行版(如 Ubuntu)已经在存储库中打包了 Python 2,这使得安装过程更加容易。
-
打开一个 Python shell,在终端中键入
python,或者点击 Python 图标。 -
然后,要测试安装,请在 Python 交互式外壳或其由 Python 的标准 IDE 或其他解决方案(如 Spyder 或 PyCharm)提供的读取-评估-打印循环 ( REPL )界面中运行以下代码:
>>> import sys >>> print sys.version
如果出现语法错误,这意味着您运行的是 Python 2 而不是 Python 3。如果您没有遇到错误,并且您可以阅读到您的 Python 版本是 3.4.x 或 3.5.x(在撰写本文时,最新版本是 3.5.2),那么恭喜您运行了我们为这本书选择的 Python 版本。
为了澄清,当在终端命令行中给出一个命令时,我们在命令前面加上$。否则,如果是 Python REPL,前面会有>>>。
包装的安装
根据您的系统和过去的安装,Python 可能不会与您需要的所有东西捆绑在一起,除非您已经安装了一个发行版(另一方面,它通常填充了比您可能需要的更多的东西)。
要安装您需要的任何软件包,您可以使用pip或easy_install命令;但是,easy_install在未来会被放弃, pip 比它有重要的优势。
pip 是一个直接上网安装 Python 包并从 Python 包索引(https://pypi.python.org/pypi)中挑选的工具。 PyPI 是一个包含第三方开源包的存储库,这些包由它们的作者不断维护并存储在存储库中。
由于以下原因,最好使用pip安装所有设备:
- 它是 Python 的首选包管理器,从 Python 2.7.9 和 Python 3.4 开始,默认情况下它包含在 Python 二进制安装程序中
- 它提供卸载功能
- 无论出于什么原因,如果软件包安装失败,它都会回滚并清除您的系统
pip命令在命令行中运行,使得 Python 包的安装、升级和删除过程变得轻而易举。
正如我们提到的,如果您至少运行 Python 2.7.9 或 Python 3.4,则pip命令应该已经存在。要确保本地机器上安装了哪些工具,如果出现任何错误,请直接使用以下命令进行测试:
$ pip –V
在某些 Linux 和 Mac 安装中,安装的是 Python 3 而不是 Python 2,命令可能以pip3的形式出现,因此如果您在查找pip时收到错误,请尝试运行以下命令:
$ pip3 –V
如果是这样的话,记住pip3只适合在 Python 3 上安装软件包。由于我们在书中使用 Python 2(除非您决定使用最新的 Python 3.4),pip应该始终是您安装软件包的选择。
或者,您也可以测试旧的easy_install命令是否可用:
$ easy_install --version
型式
尽管有pip和它的优点,如果你在 Windows 上工作,使用easy_install还是有意义的,因为pip不会安装二进制包;因此,如果您在安装软件包时遇到意想不到的困难,easy_install可以拯救您的一天。
如果你的测试以错误结束,你真的需要从头开始安装 pip(这样做的同时也是easy_install)。
要安装 pip,只需按照在https://pip.pypa.io/en/stable/installing/给出的说明进行操作。最安全的方法是从https://bootstrap.pypa.io/get-pip.py下载get-pip.py脚本,然后使用以下命令运行:
$ python get-pip.py
顺便说一下,脚本还会从【https://pypi.python.org/pypi/setuptools】安装设置工具,其中包含easy_install。
作为替代,如果你运行的是一个类似 Debian/Ubuntu 的 Unix 系统,那么一个快速的快捷方式就是使用apt-get安装所有的东西:
$ sudo apt-get install python3-pip
检查完这个基本要求后,您现在可以安装运行本书中提供的示例所需的所有软件包了。要安装通用的<pk>包,您只需要运行以下命令:
$ pip install <pk>
或者,如果您更喜欢使用easy_install,您也可以运行以下命令:
$ easy_install <pk>
之后将下载并安装<pk>包及其所有依赖项。
如果您不确定是否安装了库,请尝试在其中导入模块。如果 Python 解释器引发一个 ImportError 错误,那么可以断定包还没有安装。
举个例子吧。安装了 NumPy 库后会出现这种情况:
>>> import numpy
如果没有安装,就会出现这种情况:
>>> import numpy
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named numpy
在后一种情况下,在导入之前,您需要通过pip或easy_install安装它。
注意不要将包与模块混淆。用pip,你安装一个包;在 Python 中,您可以导入一个模块。有时,包和模块有相同的名称,但在许多情况下,它们不匹配。例如, sklearn 模块包含在名为 Scikit-learn 的包中。
套餐升级
通常情况下,你会发现自己不得不升级一个包,因为新的版本要么是依赖项所需要的,要么是你想要使用的附加功能。为此,首先通过浏览__version__属性来检查您已经安装的库的版本,如下面使用 NumPy 包的示例所示:
>>> import numpy
>>> numpy.__version__ # 2 underscores before and after
'1.9.0'
现在,如果您想将其更新到较新的版本,确切地说是 1.9.2 版本,您可以从命令行运行以下命令:
$ pip install -U numpy==1.9.2
或者(但我们不建议这样做,除非有必要),您也可以使用以下命令:
$ easy_install --upgrade numpy==1.9.2
最后,如果您只是想将其升级到最新版本,只需运行以下命令:
$ pip install -U numpy
您也可以运行easy_install替代选项:
$ easy_install --upgrade numpy
科学分布
正如您到目前为止所读到的,对于数据科学家来说,创建工作环境是一项耗时的操作。你首先需要安装 Python,然后,一个接一个,你可以安装所有你需要的库。(有时,安装程序可能不会像您之前希望的那样顺利。)
如果您想节省时间和精力,并想确保您有一个可以使用的完全工作的 Python 环境,您可以下载、安装和使用科学的 Python 发行版。除了 Python,它们还包括各种各样的预装包,有时它们甚至有额外的工具和 IDE 设置供您使用。其中一些在数据科学家中非常有名,在接下来的部分中,您将发现其中两个包的一些关键特性,我们发现它们非常有用和实用。
要立即专注于本书的内容,我们建议您首先及时下载并安装一个科学的发行版,如 Anaconda (在我们看来,这是周围最完整的一个),并在练习本书中的示例后,决定完全卸载该发行版并单独设置 Python,它可以附带您的项目所需的软件包。
同样,如果可能的话,下载并安装包含 Python 3 的版本。
我们建议您尝试的第一个包是 Anaconda(https://www.continuum.io/downloads),这是由 Continuum Analytics 提供的 Python 发行版,包括近 200 个包,包括 NumPy、SciPy、pandas、IPython、matplotlib、Scikit-learn 和 StatsModels。这是一个跨平台的发行版,可以安装在其他现有 Python 发行版和版本的机器上,其基础版本是免费的。包含高级功能的附加组件单独收费。Anaconda 引入了二进制包管理器 conda ,作为管理您的包安装的命令行工具。正如其网站上所述,Anaconda 的目标是为大规模处理、预测分析和科学计算提供企业级 Python 分发。至于 Python 版本,我们推荐 Anaconda 发行版 4.0.0。(为了看一下安装了 Anaconda 的软件包,可以看看https://docs.continuum.io/anaconda/pkg-docs的列表。)
第二个建议是,如果你在 Windows 上工作,并且你想要一个可移植的发行版,那么 WinPython(http://winpython.sourceforge.net/)可能是一个非常有趣的替代版本(抱歉,没有 Linux 或 MacOS 版本)。WinPython 也是一个由社区维护的免费开源 Python 发行版。它也是为科学家设计的,它包括许多基本包,如 NumPy、SciPy、matplotlib 和 IPython(基本上与 Anaconda 的相同)。它还包括作为 IDE 的 Spyder ,如果您有使用 MATLAB 语言和界面的经验,这可能会有所帮助。它的关键优势是它是可移植的(你可以把它放在任何目录甚至 u 盘里),所以你可以在你的电脑上有不同的版本,把一个版本从一台 Windows 电脑移动到另一台,你可以通过替换它的目录很容易地用一个新版本替换一个旧版本。当您运行 WinPython 或其 shell 时,它会自动设置运行 Python 所必需的所有环境变量,就好像它是在您的系统上定期安装和注册的。
型式
在撰写时,Python 2.7 是 2015 年 10 月随着 2.7.10 版本准备的最新发行版;从那以后,WinPython 只发布了 Python 3 版本发行版的更新。在系统上安装发行版后,您可能需要更新本书中示例所需的一些关键包。
介绍 Jupyter/IPython
IPython 于 2001 年由 Fernando Perez 发起,作为一个免费项目,解决了 Python 栈缺乏科学研究的问题,该栈使用用户编程接口,可以将科学方法(主要是实验和交互发现)纳入软件开发过程。
科学方法意味着以可复制的方式对不同的假设进行快速实验(数据科学中的数据探索和分析任务也是如此),当使用 IPython 时,您将能够在代码编写过程中更自然地实现探索性、迭代性和试错研究策略。
最近,IPython 项目的很大一部分已经转移到了一个名为 Jupyter 的新项目。这个新项目将原始 IPython 接口的潜在可用性扩展到了广泛的编程语言中。(完整列表请访问https://github . com/IPython/IPython/wiki/IPython-其他语言内核。)
得益于内核的强大思想,运行用户代码的程序由前端接口进行通信,并将执行代码的结果反馈给接口本身;您可以使用相同的界面和交互式编程风格,无论您使用什么语言开发。
Jupyter (IPython 是零内核,最初的起始内核)可以简单地描述为可通过控制台或基于 web 的笔记本操作的交互式任务的工具,它提供了特殊的命令来帮助开发人员更好地理解和构建当前正在编写的代码。
与围绕编写脚本、随后运行并评估其结果的想法构建的 IDE 相反,Jupyter 允许您以名为单元格的块编写代码,依次运行每个单元格,并分别评估的结果,同时检查文本和图形输出。除了图形集成之外,由于可定制的命令、丰富的历史(JSON 格式)和计算并行性,它还为您提供了进一步的帮助,从而在处理繁重的数值计算时提高了性能。
这种方法对于涉及基于数据开发代码的任务也特别有效,因为它自动完成了经常被忽视的职责,即记录和说明数据分析是如何完成的,它的前提和假设,以及它的中间和最终结果。如果你工作的一部分也是展示你的作品,并说服项目的内部或外部利益相关者,Jupyter 只需付出很少的额外努力就能真正为你带来讲故事的魔力。在https://github . com/IPython/IPython/wiki/A-gallery-of-of-interior-IPython-Notebooks上有很多的例子,其中一些可能会像我们一样对你的工作有启发。
事实上,我们不得不承认,保持一个干净、最新的 Jupyter 笔记本为我们节省了无数次与经理/利益相关者的会议突然出现,要求我们匆忙呈现工作状态的时间。
简而言之,Jupyter 为您提供了以下功能:
- 查看每个分析步骤的中间(调试)结果
- 只运行代码的某些部分(或单元格)
- 以 JSON 格式存储中间结果,并能够对它们进行版本控制
- 展示您的作品(这将是文本、代码和图像的组合),通过 Jupyter Notebook Viewer 服务(http://nbviewer.jupyter.org/)共享,并轻松地将其导出为 HTML、PDF 或甚至幻灯片
Jupyter 是我们在这本书中最喜欢的选择,它被用来清晰有效地用脚本和数据来说明讲故事的操作及其结果。
尽管我们强烈建议使用 Jupyter,但如果您使用的是 REPL 或 IDE,则可以使用相同的说明并期望得到相同的结果(打印格式和返回结果的扩展除外)。
如果您的系统上没有安装 Jupyter,您可以使用以下命令立即进行设置:
$ pip install jupyter
型式
您可以在http://jupyter.readthedocs.io/en/latest/install.html找到关于 Jupyter 安装(涵盖不同操作系统)的完整说明。
如果你已经安装了 Jupyter,应该至少升级到 4.1 版本。
安装后,您可以立即开始使用 Jupyter,从命令行调用它:
$ jupyter notebook
一旦 Jupyter 实例在浏览器中打开,点击新建按钮,在笔记本部分选择 Python 2 (其他内核可能是出现在该部分,取决于您安装的内容):

此时,您的新空笔记本将看起来像下面的屏幕截图,您可以开始在单元格中输入命令:

例如,您可以开始在单元格中键入以下内容:
In: print ("This is a test")
写入单元格后,只需按下播放按钮(在单元格选项卡下方)即可运行并获得输出。然后,将出现另一个单元格供您输入。当您在单元格中书写时,如果您按下上面菜单栏上的加号按钮,您将获得一个新的单元格,您可以使用菜单上的箭头从一个单元格移动到另一个单元格。
其他大多数功能都非常直观,我们邀请您尝试。为了更好地了解 jupyter 的工作原理,您可以使用快速入门指南,如http://Jupyter-notebook-初学者指南. readthedocs.io/en/latest/ 或您可以获得一本专门介绍 Jupyter 功能的书籍。
注
有关运行 IPython 内核时所有 Jupyter 功能的完整论述,请参考以下两本 Packt 出版书籍:
- IPython 交互式计算和可视化烹饪书作者:西里尔·罗桑特,帕克特出版社,2014 年 9 月 25 日
- 学习交互式计算和数据可视化的 IPython西里尔·罗桑特帕克特出版,2013 年 4 月 25 日
出于说明的目的,只要考虑到每一个 Jupyter 指令块都有一个编号的输入语句和一个输出语句,那么你会发现在这本书中呈现的代码被构造成两个块——至少当输出一点也不琐碎的时候——否则,只期望输入部分:
In: <the code you have to enter>
Out: <the output you should get>
通常,您只需在单元格中输入之后的代码并运行它。然后,您可以将您的输出与我们使用输出:提供的输出进行比较,然后是我们在测试代码时在计算机上实际获得的输出。
Python 包
我们将在本段中介绍的软件包将在本书中经常使用。如果您没有使用科学的发行版,我们将为您提供一个演练,介绍您应该决定哪些版本以及如何快速成功地安装它们。
NumPy
NumPy 是特拉维斯·奥列芬特的创造,是 Python 语言中每一个分析解决方案的核心。它为用户提供了多维数组以及一大组函数来对这些数组进行多种数学运算。数组是沿多维排列的数据块,用于实现数学向量和矩阵。数组不仅对存储数据有用,而且对快速矩阵运算(矢量化)也很有用,当您希望解决特定的数据科学问题时,这是不可或缺的。
-
撰写时版本:1 . 11 . 1
-
建议安装命令:
$ pip install numpy
型式
作为 Python 社区大量采用的约定,在导入 NumPy 时,建议将其别名为np:
import numpy as np
黑桃
SciPy 是特拉维斯·奥列芬特、佩鲁·彼得森和埃里克·琼斯的原创项目,它完善了 NumPy 的功能,为线性代数、稀疏矩阵、信号和图像处理、优化、快速傅立叶变换等提供了更多种类的科学算法。
-
编写时版本:0.17.1
-
建议安装命令:
$ pip install scipy
熊猫
熊猫处理一切 NumPy 和 SciPy 做不到的事情。特别是,由于其特定的对象数据结构、数据帧和序列,它允许处理不同类型(NumPy 的数组无法处理)和时间序列的复杂数据表。由于韦斯·麦金尼的创建,您将能够轻松流畅地从各种来源加载数据,然后对其进行切片、切割、处理缺失的元素、添加、重命名、聚合、重塑,最后根据您的意愿将其可视化。
-
编写时版本:0.18.0
-
建议安装命令:
$ pip install pandas
型式
按照惯例,熊猫作为pd进口:
import pandas as pd
科学学习
Scikit-learn 作为 Scikit(SciPy Toolkits)的一部分开始,是 Python 中数据科学操作的核心。它在数据预处理、监督和非监督学习、模型选择、验证和误差度量方面提供了您可能需要的一切。期待我们在整本书中详细讨论这个包。
Scikit-learn 始于 2007 年,是由大卫·库纳波发起的谷歌代码之夏项目。自 2013 年以来,它已被 Inria(法国计算机科学和自动化研究所)的研究人员接管。
Scikit-learn 提供了用于数据处理(sklearn.preprocessing和sklearn.feature_extraction)、模型选择和验证(sklearn.cross_validation、sklearn.grid_search和sklearn.metrics)的模块,以及一整套方法(sklearn.linear_model),在这些方法中,作为数字或概率的目标值预计是输入变量的线性组合。
-
编写时版本:0.17.1
-
建议安装命令:
$ pip install scikit-learn
型式
注意,导入的模块名为sklearn。
matplotlib 包
matplotlib 最初由约翰·亨特开发,是一个包含所有构建块的库,用于从数组创建高质量的图,并交互式地可视化它们。
你可以在 PyLab 模块中找到所有类似 MATLAB 的绘图框架。
-
编写时版本:1.5.1
-
建议安装命令:
$ pip install matplotlib
您可以简单地导入可视化所需的内容:
import matplotlib as mpl
from matplotlib import pyplot as plt
玄垣
由 radim řehůřek 编程的 Gensim 是一个开源包,适合通过使用并行可分发的在线算法来分析大型文本集合。在高级功能中,它实现了潜在语义分析 ( LSA )、通过潜在狄利克雷分配 ( LDA )进行主题建模以及谷歌的 word2vec ,这是一种将文本转换为向量特征的强大算法,用于有监督和无监督的机器学习。
-
编写时版本:0.13.1
-
建议安装命令:
$ pip install gensim
H2O
H2O 是由初创公司 H2O.ai (之前命名为 0xdata)创建的用于大数据分析的开放源代码框架。R、Python、Scala 和 Java 编程语言都可以使用。H2O 轻松允许使用独立机器(利用多处理)或 Hadoop 集群(例如,AWS 环境中的集群),从而帮助您纵向扩展和横向扩展。
- 网站: http://www.h2o.ai
- 本书写作时的版本:3.8.3.3
为了安装这个包,你首先必须在你的系统上下载并安装 Java,(你需要安装 Java 开发工具包 ( JDK ) 1.8,因为 H2O 是基于 Java 的。)然后您可以参考http://www.h2o.ai/download/h2o/python提供的在线说明。
我们可以在下面几行中一起概述所有的安装步骤。
您可以按照以下说明安装 H2O 及其 Python 应用编程接口,就像我们在书中一直使用的那样:
$ pip install -U requests
$ pip install -U tabulate
$ pip install -U future
$ pip install -U six
这些步骤将安装所需的软件包,然后我们可以安装框架,注意删除任何以前的安装:
$ pip uninstall h2o
$ pip install h2o
为了让安装与我们书中相同的版本,您可以使用以下命令更改最后一个pip install命令:
$ pip install http://h2o-release.s3.amazonaws.com/h2o/rel-turin/3/Python/h2o-3.8.3.3-py2.py3-none-any.whl
如果您遇到问题,请访问 H2O 谷歌小组页面,在那里您可以获得有关您的问题的帮助:
https://groups.google.com/forum/#!forum/h2ostream
XGBoost
XGBoost 是一个可扩展的、可移植的、分布式梯度增强库(一种树集成机器学习算法)。它可用于 Python、R、Java、Scala、Julia 和 C++并且可以在一台机器上工作(利用多线程),在 Hadoop 和 Spark 集群中都是如此。
- 网站:https://xgboost.readthedocs.io/en/latest/
- 编写时版本:0.4
在您的系统上安装 xboost 的详细说明可以在https://github.com/dmlc/xgboost/blob/master/doc/build.md找到。
在 Linux 和 Mac 操作系统上安装 XGBoost 非常简单,但是对于 Windows 用户来说就有点复杂了。因此,我们提供了特定的安装步骤,让 XGBoost 在 Windows 上运行:
-
首先下载安装Git Windows(https://git-for-windows.github.io/)。
-
然后你需要一个极简 GNU for Windows ( MinGW )编译器出现在你的系统上。可以根据自己系统的特点从http://www.mingw.org/下载。
-
在命令行中,执行以下命令:
$ git clone --recursive https://github.com/dmlc/xgboost $ cd xgboost $ git submodule init $ git submodule update -
Then, from the command line, copy the configuration for 64-bit systems to be the default one:
$ copy make\mingw64.mk config.mk或者,您可以复制纯 32 位版本:
$ copy make\mingw.mk config.mk -
复制配置文件后,可以运行编译器,设置为使用四个线程以加快编译过程:
$ make -j4 -
最后,如果编译器没有错误地完成了它的工作,你可以通过执行以下命令在你的 Python 中安装这个包:
$ cd python-package $ python setup.py install
Theano
安诺是一个 Python 库,允许您定义、优化和评估涉及多维数组的数学表达式。基本上,它为您提供了创建深度神经网络所需的所有构件。
- 网站:http://deeplearning.net/software/theano/
- 在写入时释放:0.8.2
antio 的安装应该很简单,因为它现在是 PyPI 上的一个包:
$ pip install Theano
如果您想要包的最新版本,您可以通过 GitHub 克隆获得它们:
$ git clone git://github.com/Theano/Theano.git
然后,您可以继续直接安装 Python:
$ cd Theano
$ python setup.py install
要测试您的安装,您可以从 shell/CMD 运行以下命令并验证报告:
$ pip install nose
$ pip install nose-parameterized
$ nosetests theano
如果您使用的是 Windows 操作系统,并且之前的说明不起作用,您可以尝试以下步骤:
-
安装 TDM-GCC x64(http://tdm-gcc.tdragon.net/)。
-
打开 Anaconda 命令提示符并执行以下命令:
$ conda update conda $ conda update –all $ conda install mingw libpython $ pip install git+git://github.com/Theano/Theano.git
型式
antao 需要 libpython,它还不兼容 3.5 版本,所以如果您的 Windows 安装不工作,这可能是原因。
此外,安奈诺的网站向 Windows 用户提供了一些信息,当其他一切都失败时,这些信息可能会支持你:
http://deep learning . net/software/茶诺/install_windows.html
在 GPU 上横向扩展的一个重要要求是安装 NVIDIA CUDA 驱动程序和 SDK 用于在 GPU 上生成和执行代码。如果您对 CUDA 工具包不太了解,您实际上可以从这个网页开始,以了解更多正在使用的技术:
https://developer.nvidia.com/cuda-toolkit
因此,如果您的电脑拥有 NVIDIA GPU,您可以从 NVIDIA 本身找到安装 CUDA 的所有必要说明,使用本教程页面:
http://docs . NVIDIA . com/cuda/cuda-快速入门-指南/index.html#axzz4A8augxYy
TensorFlow
就像 antano 一样, TensorFlow 是另一个用于数值计算的开源软件库,使用数据流图而不仅仅是数组。这样的图中的节点代表数学运算,而图的边代表在节点之间移动的多维数据阵列(所谓的张量)。最初,作为谷歌大脑团队的一部分,谷歌研究人员开发了 TensorFlow,最近他们将其开源给了 T4 公众。
- 网站:https://github . com/tensorlow/tensorlow
- 编写时发布:0.8.0
要在您的计算机上安装 TensorFlow,请按照以下链接中的说明进行操作:
https://github . com/tensorlow/tensorlow/blob/master/tensorlow/G3 doc/get _ started/OS _ setup . MD
Windows 支持目前不存在,但在当前的路线图中:
https://github . com/tensorlow/tensorlow/blob/master/tensorlow/G3 doc/resources/roadmap . MD
对于 Windows 用户来说,一个很好的折衷方案是在基于 Linux 的虚拟机或 Docker 机器上运行该包。(前面的操作系统设置页面提供了相关说明。)
sknn 图书馆
sknn 库(对于扩展, scikit-neuralnetwork )是派尔恩 2 的包装器,帮助你实现深度神经网络,而不需要你成为安诺的专家。另外,该库与 Scikit-learn API 兼容。
-
发布时间发布时间:0.7
-
要安装库,只需使用以下命令:
$ pip install scikit-neuralnetwork
或者,如果您想利用最先进的功能,如卷积、池化或升级,您必须按如下方式完成安装:
$ pip install -r https://raw.githubusercontent.com/aigamedev/scikit-neuralnetwork/master/requirements.txt
安装后,您还必须执行以下操作:
$ git clone https://github.com/aigamedev/scikit-neuralnetwork.git
$ cd scikit-neuralnetwork
$ python setup.py develop
正如在 XGBoost 中看到的,这将使sknn包在您的 Python 安装中可用。
Theanets
茶氨酸包是一个用 Python 编写的深度学习和神经网络工具包,使用茶氨酸来加速计算。就像 sknn 一样,为了创建深度学习模型,它试图使其更容易与中的模拟功能交互。
-
编写时版本:0.7.3
-
建议安装程序:
$ pip install theanets
也可以从 GitHub 下载当前版本,直接用 Python 安装包:
$ git clone https://github.com/lmjohns3/theanets
$ cd theanets
$ python setup.py develop
硬
Keras 是一个用 Python 编写的极简、高度模块化的神经网络库,能够在 TensorFlow 或 antao 之上运行。
-
编写时的版本:1.0.5
-
建议从 PyPI
$ pip install keras安装
您也可以使用以下命令安装最新的可用版本(建议使用持续开发的软件包):
$ pip install git+git://github.com/fchollet/keras.git
要安装在系统上的其他有用的软件包
在这本书的各页中,我们将看到许多包的漫长旅程,最后,我们用三个简单但非常有用的包来结束,它们几乎不需要演示,但需要安装在您的系统上:内存剖析器、气候和 神经实验室。
内存分析器是一个监控进程内存使用情况的包。它还有助于逐行剖析特定 Python 脚本的内存消耗。它可以按如下方式安装:
$ pip install -U memory_profiler
气候只是由 Python 的一些基本命令行实用程序组成。它可以按如下方式迅速安装:
$ pip install climate
最后,NeuroLab 是一个非常基础的神经网络包,松散地基于 MATLAB 中的神经网络工具箱 ( NNT )。是基于 NumPy 和 SciPy,而不是 antao;因此,不要期待惊人的表现,但要知道这是一个很好的学习工具箱。它可以很容易地安装如下:
$ pip install neurolab
总结
在这一介绍性章节中,我们已经说明了使用 Python(纵向扩展和横向扩展技术)使机器学习算法可扩展的不同方法。我们还提出了一些激励性的例子,并通过说明如何在您的机器上安装 Python 来为这本书做准备。特别是,我们向您介绍了 Jupyter,并涵盖了将在本书中使用的所有最重要的软件包。
在下一章中,我们将深入讨论随机梯度下降如何通过在单台机器上利用输入/输出来帮助您处理海量数据集。基本上,我们将涵盖从大文件或数据存储库中流式传输数据的不同方式,并将其输入到基本的学习算法中。你会惊讶于简单的解决方案是多么有效,你会发现即使是你的台式电脑也可以轻松处理大数据。
二、Scikit-learn 中的可扩展学习
将数据集加载到内存中、准备数据矩阵、训练机器学习算法以及使用样本外观察来测试其泛化能力通常不是什么大事,因为当今时代的计算机相当强大,但价格合理。然而,越来越多的情况是,要处理的数据规模如此之大,以至于不可能将其加载到计算机的核心内存中,即使是可管理的,其结果在数据管理和机器学习方面也是难以处理的。
除了核心内存处理之外,其他可行的策略也是可能的:将数据分成样本,使用并行性,最后以小批量或单个实例进行学习。本章将重点介绍 Scikit-learn 包提供的现成解决方案:来自数据存储的小批量实例流(我们的观察)以及基于它们的增量学习。这样的解决方案叫做核心外学习。
通过处理可管理的块和增量学习来处理数据是一个好主意。然而,当您试图实现它时,它也可能被证明是具有挑战性的,因为可用的学习算法和流中的流数据的限制将要求您在数据管理和特征提取方面有不同的想法。除了展示用于核心外学习的 Scikit-learn 功能之外,我们还将努力向您展示 Python 解决方案,解决您在被迫一次只观察一小部分数据时可能面临的明显令人生畏的问题。
在本章中,我们将涵盖以下主题:
- 核心外学习的方式在 Scikit-learn 中实现
- 使用哈希技巧有效管理数据流
- 随机学习的基本原理
- 通过在线学习实施数据科学
- 数据流的无监督转换
核心外学习
核心外学习指的是一组处理数据的算法,这些数据不能放入单台计算机的内存中,但可以很容易地放入一些数据存储中,如本地硬盘或网络存储库。您的可用内存,也就是单台机器上的核心内存,在大型服务器上可能从几千兆字节(有时是 2 GB,更常见的是 4 GB,但我们假设您最多有 2 GB)到 256 GB 不等。大型服务器就像您可以在云计算服务上获得的服务器,例如亚马逊弹性计算云 ( EC2 ),而您的存储容量仅使用一个外部驱动器就可以轻松超过万亿字节的容量(很可能约为 1 TB,但最高可达 4 TB)。
由于机器学习是基于全局降低成本函数,许多算法最初被认为是使用所有可用数据并在优化过程的每次迭代中访问这些数据。对于所有基于利用矩阵演算的统计学习的算法来说尤其如此,例如,反转矩阵,但是基于贪婪搜索的算法在采取下一步之前需要对尽可能多的数据进行评估。因此,最常见的开箱即用的类似回归的算法(特征的加权线性组合)更新它们的系数,试图最小化整个数据集的汇集误差。同样,由于对数据集中存在的噪声非常敏感,决策树必须根据所有可用数据决定最佳分割,以便找到最佳解决方案。
如果在这种情况下,数据无法容纳在计算机的核心内存中,您没有太多可能的解决方案。您可以增加可用内存(取决于主板的限制;之后,您将不得不求助于分布式系统,如 Hadoop 和 Spark(我们将在本书的最后几章中提到这种解决方案),或者简单地减少数据集,以便让它适合内存。
如果您的数据是稀疏的,也就是说,您的数据集中有许多零值,您可以将密集矩阵转换为稀疏矩阵。这对于具有许多列的文本数据来说是典型的,因为每一列都是一个单词,但是代表单词计数的值很少,因为单个文档通常显示有限的单词选择。有时,使用稀疏矩阵可以解决允许您加载和处理其他相当大的数据集的问题,但这不是灵丹妙药(抱歉,没有免费的午餐,也就是说,没有适合所有问题的解决方案),因为一些数据矩阵虽然稀疏,但可能有令人生畏的大小。
在这种情况下,您总是可以尝试通过减少实例数量或限制要素数量来减少数据集,从而减少数据集矩阵的维度及其在内存中所占的面积。通过只选取一部分观察值来减小数据集的大小,是一种称为二次采样(或简称采样)的解决方案。二次抽样本身没有错,但它有严重的缺点,在决定分析过程之前,有必要记住它们。
二次采样是一个可行的选择
当你进行二次抽样时,你实际上是在丢弃你的信息丰富性的一部分,你不能确定你只是在丢弃多余的,不是那么有用的观察。其实一些隐藏的宝石只有综合考虑所有数据才能发现。尽管在计算上很有吸引力——因为二次采样只需要一个随机生成器来告诉你是否应该选择一个实例——但通过选择二次采样数据集,你确实有可能限制算法以完整的方式学习数据中的规则和关联的能力。在偏差-方差权衡中,二次抽样导致预测的方差膨胀,因为由于随机噪声或数据中的异常观察,估计将更加不确定。
在大数据的世界里,拥有更多高质量数据的算法获胜,因为它可以比其他拥有更少(或更多噪音)数据的模型学会更多将预测与预测因子联系起来的方法。因此,二次抽样虽然作为一种解决方案是可以接受的,但会对机器学习活动的结果造成限制,因为预测不太精确,估计的方差更大。
通过在数据的多个子样本上学习多个模型,然后最终将所有解集合在一起或将所有模型的结果叠加在一起,从而创建一个简化的数据矩阵以供进一步训练,可以以某种方式克服子采样限制。这个过程被称为装袋。(您实际上是以这种方式压缩功能,从而减少了内存中的数据空间。)我们将在后面的章节中探讨集合和堆叠,并发现它们实际上如何减少因二次抽样而膨胀的估计方差。
作为替代方案,我们可以不切割实例,而是切割特征,但同样,我们会遇到这样的问题,即我们需要从数据中构建模型,以测试我们可以选择哪些特征,因此我们仍然需要构建一个包含无法放入内存的数据的模型。
一次优化一个实例
意识到二次采样虽然总是可行的,但并不是一个最优的解决方案,我们必须评估一种不同的方法,而核外实际上并不需要你放弃观察或特征。只不过训练一个模型需要更长一点的时间,需要更多的迭代和从你的存储到你的计算机内存的数据传输。我们立即提供了核心外学习过程如何工作的第一直觉。
让我们从学习开始,这是一个过程,我们试图将表达响应的未知函数(一个回归或分类问题的数字或结果)映射到可用数据。学习是可能的,通过拟合学习算法的内部系数,试图在可用数据上实现最佳拟合,即最小化成本函数,这种度量告诉我们我们的近似有多好。归根结底,我们谈论的是优化过程。
不同的优化算法,就像梯度下降一样,是能够处理任何数据量的过程。他们致力于导出优化梯度(优化过程中的一个方向),并让学习算法调整其参数,以便遵循梯度。
在梯度下降的具体情况下,经过一定次数的迭代,如果问题可以解决,并且没有其他问题,比如学习率太高,梯度应该变得很小,这样我们就可以停止优化过程。在这个过程的最后,我们可以确信已经找到了一个最优解(因为它是一个全局最优解,尽管有时它可能是一个局部最小值,如果要逼近的函数不是凸的)。
由于由梯度决定的方向性可以基于任意数量的示例,因此也可以在单个实例上进行。在单个实例上采用梯度需要小的学习速率,但是最终,该过程可以达到与在全部数据上采用梯度下降相同的优化。最后,我们的算法所需要的只是一个方向,即在拟合可用数据的基础上正确定位学习过程。因此,从从数据中随机抽取的单个案例中学习这样的方向是完全可行的:
- 我们可以获得相同的结果,就好像我们一次处理所有数据一样,尽管优化路径可能会变得有点粗糙;如果你的大部分观察都指向一个最佳方向,算法就会采用那个方向。唯一的问题是正确调整学习过程的正确参数,并多次传递数据,以确保优化完成,因为此学习过程比处理所有可用数据要慢得多。
- 我们在设法将单个实例保留在核心内存中,将大部分数据排除在外方面没有任何特殊问题。通过单个例子将数据从其存储库移动到我们的核心内存可能会产生其他问题。可伸缩性是有保证的,因为处理数据所需的时间是线性的;无论我们要处理的实例总数是多少,多使用一个实例的时间成本总是一样的。
将学习算法拟合到单个实例或一次拟合到内存的数据子集上的方法称为在线学习,基于这种单次观测的梯度下降称为随机梯度下降。如前所述,在线学习是一种核心外技术,被 Scikit-learn 中的许多学习算法所采用。
构建核心外学习体系
在接下来的几个段落中,我们将说明随机梯度下降的内部工作原理,提供更多的细节和推理。现在知道如何可能学习核外(由于随机梯度下降)允许我们更清楚地描述我们应该做什么来使它在我们的计算机上工作。
您可以将活动划分为不同的任务:
- 准备您的数据存储库访问,以便逐个实例地流式传输数据。此活动可能要求您在将数据提取到计算机之前随机化数据行的顺序,以便删除排序可能带来的任何信息。
- 首先进行一些数据调查,可能是对所有数据的一部分(例如,前一万行),试图弄清楚到达的实例在特征数量、数据类型、数据值的存在与否、每个变量的最小值和最大值以及平均值和中值方面是否一致。找出目标变量的范围或类别。
- 将每个接收到的数据行准备成学习算法可以接受的固定格式(密集或稀疏向量)。在这个阶段,您可以执行任何基本的转换,例如,将分类特征转换为数字特征,或者让数字特征通过特征本身的叉积进行交互。
- 将示例的顺序随机化后(如第一点所述),使用系统保持或在一定数量的观察后保持来建立验证程序。
- 通过重复流式传输数据或处理小样本数据来调整超参数。这也是进行一些特征工程(使用无监督学习和特殊变换函数,如核近似)并利用正则化和特征选择的合适时机。
- 使用您为培训保留的数据构建您的最终模型,并在全新的数据上理想地测试模型的有效性。
作为第一步,我们将讨论如何准备您的数据,然后利用 Python 包(如 pandas 和 Scikit-learn)中的有用功能,轻松创建适合在线学习的流。
从来源流式传输数据
当你有一个传输数据的生成过程时,一些数据真的在你的计算机中流动,你可以动态处理或丢弃这些数据,但除非你已经把它们存储在某个数据档案库中,否则以后不会再调用。这就像从流动的河流中拖水一样——河流一直在流动,但你可以过滤和处理所有流动的水。这与一次处理所有数据是完全不同的策略,这更像是把所有的水放在一个大坝里(类似于处理内存中的所有数据)。
作为流式传输的一个例子,我们可以引用传感器即时产生的数据流,或者更简单地说,推特上的推文流。一般来说,数据流的主要来源如下:
- 测量温度、压力和湿度的环境传感器
- GPS 跟踪传感器记录位置(纬度/经度)
- 记录图像数据的卫星
- 监控录像和录音
- 网络流量
但是,您不会经常处理真实的数据流,而是处理存储在存储库或文件中的静态记录。在这种情况下,可以根据某些标准重新创建流,例如,一次按顺序或随机提取单个记录。例如,如果我们的数据包含在一个 TXT 或 CSV 文件中,我们需要做的就是一次提取文件的一行,并将其传递给学习算法。
对于本章和下一章中的示例,我们将处理存储在本地硬盘上的文件,并准备将其提取为流所需的 Python 代码。我们不会使用玩具数据集,但我们不会在您的本地硬盘上塞满太多测试和演示数据。
数据集自己去尝试真实的东西
自 1987 年以来,在加州大学欧文分校(UCI)UCI 机器学习资源库已经被托管,这是一个用于机器学习社区对机器学习算法进行经验测试的大型数据集资源库。在撰写本文时,知识库包含大约 350 个数据集,这些数据集来自非常不同的领域和目的,从有监督的回归和分类到无监督的任务。您可以在https://archive.ics.uci.edu/ml/查看可用的数据集。
从我们的角度来看,我们选择了几个数据集,这些数据集将在整本书中变得有用,用一台不寻常但仍可管理的 2 GB 内存计算机和大量的行或列向您提出了具有挑战性的问题:
|数据集名称
|
数据集网址
|
问题类型
|
行和列
|
| --- | --- | --- | --- |
| 自行车共享数据集 | https://archive . ics . UCI . edu/ml/datasets/Bike+Sharing+Dataset | 回归 | 17389, 16 |
| BlogFeedback 数据集 | https://archive . ics . UCI . edu/ml/datasets/blogging feedback | 回归 | 60021, 281 |
| 社交媒体数据集中的嗡嗡声 | https://archive . ics . UCI . edu/ml/datasets/Buzz+in+社交+媒体+ | 回归和分类 | 140000, 77 |
| 普查-收入(KDD)数据集 | https://archive . ics . UCI . edu/ml/datasets/Census-Income+% 28KDD % 29 | 带有缺失数据的分类 | 299285, 40 |
| Covertype 数据集 | https://archive.ics.uci.edu/ml/datasets/Covertype | 分类 | 581012, 54 |
| 1999 年 KDD 杯数据集 | https://archive.ics.uci.edu/ml/datasets/KDD+Cup+1999+Data | 分类 | 4000000, 42 |
要从 UCI 存储库中下载并使用数据集,您必须转到该数据集的专用页面,并按照标题下的链接:下载:数据文件夹。我们已经为自动下载数据准备了一些脚本,这些脚本将被放在 Python 中您正在使用的目录中,从而使数据访问更加容易。
以下是我们准备好的一些函数,当我们需要从 UCI 下载任何数据集时,我们会在整个章节中回忆这些函数:
In: import urllib2 # import urllib.request as urllib2 in Python3
import requests, io, os, StringIO
import numpy as np
import tarfile, zipfile, gzip
def unzip_from_UCI(UCI_url, dest=''):
"""
Downloads and unpacks datasets from UCI in zip format
"""
response = requests.get(UCI_url)
compressed_file = io.BytesIO(response.content)
z = zipfile.ZipFile(compressed_file)
print ('Extracting in %s' % os.getcwd()+'\\'+dest)
for name in z.namelist():
if '.csv' in name:
print ('\tunzipping %s' %name)
z.extract(name, path=os.getcwd()+'\\'+dest)
def gzip_from_UCI(UCI_url, dest=''):
"""
Downloads and unpacks datasets from UCI in gzip format
"""
response = urllib2.urlopen(UCI_url)
compressed_file = io.BytesIO(response.read())
decompressed_file = gzip.GzipFile(fileobj=compressed_file)
filename = UCI_url.split('/')[-1][:-3]
with open(os.getcwd()+'\\'+filename, 'wb') as outfile:
outfile.write(decompressed_file.read())
print ('File %s decompressed' % filename)
def targzip_from_UCI(UCI_url, dest='.'):
"""
Downloads and unpacks datasets from UCI in tar.gz format
"""
response = urllib2.urlopen(UCI_url)
compressed_file = StringIO.StringIO(response.read())
tar = tarfile.open(mode="r:gz", fileobj = compressed_file)
tar.extractall(path=dest)
datasets = tar.getnames()
for dataset in datasets:
size = os.path.getsize(dest+'\\'+dataset)
print ('File %s is %i bytes' % (dataset,size))
tar.close()
def load_matrix(UCI_url):
"""
Downloads datasets from UCI in matrix form
"""
return np.loadtxt(urllib2.urlopen(UCI_url))
型式
下载示例代码
下载代码包的详细步骤在本书的前言中提到。请看看。
这本书的代码包也托管在 GitHub 上,网址为。我们还有来自丰富的图书和视频目录的其他代码包,可在https://github.com/PacktPublishing/获得。看看他们!
这些功能只是围绕处理压缩数据的各种包构建的方便的包装器,如tarfile、zipfile和gzip。文件是使用urllib2模块打开的,该模块生成远程系统的句柄,允许数据的顺序传输,并以字符串(StringIO)或二进制模式(BytesIO)从io模块存储在内存中,该模块专门用于流处理(【https://docs.python.org/2/library/io.html】)。存储在内存中后,它会被重新调用,就像文件是从专门用于从磁盘中解压缩压缩文件的函数中调用一样。
所提供的四个函数应该可以方便地帮助您快速下载数据集,无论它们是压缩的、涂了焦油的、gzip 的还是只是矩阵形式的纯文本,避免了手动下载和提取操作的麻烦。
第一个例子——流式传输自行车共享数据集
作为第一个例子,我们将使用自行车共享数据集。该数据集由两个 CSV 文件组成,包含 2011 年至 2012 年期间在美国华盛顿首都自行车共享系统内租赁的自行车的小时数和日数。这些数据显示了租赁当天相应的天气和季节信息。该数据集与由 Fanaee-T,Hadi 和 Gama,Joao 出版的结合集成检测器和背景知识的事件标记,《人工智能进展》(2013 年):第 1-15 页,斯普林格·柏林海德堡。
我们的第一个目标是使用前面几段中定义的方便的包装函数将数据集保存在本地硬盘上:
In: UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00275/Bike-Sharing-Dataset.zip'
unzip_from_UCI(UCI_url, dest='bikesharing')
Out: Extracting in C:\scisoft\WinPython-64bit-2.7.9.4\notebooks\bikesharing
unzipping day.csv
unzipping hour.csv
如果运行成功,代码将指示 CSV 文件保存在哪个目录中,并打印两个解压缩文件的名称。
此时,将信息保存在物理设备上后,我们将编写一个脚本,构成核心外学习系统的核心,从文件中提供数据流。我们将首先使用csv库,为我们提供了双重选择:将数据恢复为列表或 Python 字典。我们将从一个列表开始:
In: import os, csv
local_path = os.getcwd()
source = 'bikesharing\\hour.csv'
SEP = ',' # We define this for being able to easily change it as required by the file
with open(local_path+'\\'+source, 'rb') as R:
iterator = csv.reader(R, delimiter=SEP)
for n, row in enumerate(iterator):
if n==0:
header = row
else:
# DATA PROCESSING placeholder
# MACHINE LEARNING placeholder
pass
print ('Total rows: %i' % (n+1))
print ('Header: %s' % ', '.join(header))
print ('Sample values: %s' % ', '.join(row))
Out: Total rows: 17380
Header: instant, dteday, season, yr, mnth, hr, holiday, weekday, workingday, weathersit, temp, atemp, hum, windspeed, casual, registered, cnt
Sample values: 17379, 2012-12-31, 1, 1, 12, 23, 0, 1, 1, 1, 0.26, 0.2727, 0.65, 0.1343, 12, 37, 49
输出将向我们报告已经读取了多少行,标题的内容 CSV 文件的第一行(存储在列表中)——以及一行的内容(为了方便起见,我们打印了最后一行)。csv.reader函数创建一个iterator,由于有一个for循环,它将逐个释放文件的每一行。请注意,我们在代码片段内部放置了两个注释,指出了在整个章节中,我们将在哪里放置其他代码来处理数据预处理和机器学习。
在这种情况下,必须使用定位方法来处理特征,即索引标签在标题中的位置。如果你必须广泛地操作你的特征,这可能是一个小麻烦。一个解决方案可能是使用csv.DictReader生成一个 Python 字典作为输出(这是无序的,但是特征很容易被它们的标签召回):
In: with open(local_path+'\\'+source, 'rb') as R:
iterator = csv.DictReader(R, delimiter=SEP)
for n, row in enumerate(iterator):
# DATA PROCESSING placeholder
# MACHINE LEARNING placeholder
pass
print ('Total rows: %i' % (n+1))
print ('Sample values: %s' % str(row))
Out: Total rows: 17379
Sample values: {'mnth': '12', 'cnt': '49', 'holiday': '0', 'instant': '17379', 'temp': '0.26', 'dteday': '2012-12-31', 'hr': '23', 'season': '1', 'registered': '37', 'windspeed': '0.1343', 'atemp': '0.2727', 'workingday': '1', 'weathersit': '1', 'weekday': '1', 'hum': '0.65', 'yr': '1', 'casual': '12'}
使用熊猫输入输出工具
作为 csv模块的替代,我们可以使用熊猫的read_csv功能。这样一个专门用于上传 CSV 文件的功能,是大量专门用于不同文件格式输入/输出的功能的一部分,如 http://pandas.pydata.org/pandas-docs/stable/io.html 熊猫文档所规定的。
使用熊猫输入输出功能的巨大优势如下:
- 如果您改变了源代码类型,您可以保持代码的一致性,也就是说,您只需要重新定义流迭代器
- 您可以访问大量不同的格式,例如 CSV、普通 TXT、HDF、JSON 和特定数据库的 SQL 查询
- 由于熊猫数据帧中典型的切片和切割方法
.loc、.iloc、.ix,数据以数据帧数据结构的形式被流式传输到所需大小的块中,以便您可以通过定位方式或通过调用其标签来访问特征
下面是一个使用与之前相同方法的例子,这次是围绕熊猫的read_csv功能构建的:
In: import pandas as pd
CHUNK_SIZE = 1000
with open(local_path+'\\'+source, 'rb') as R:
iterator = pd.read_csv(R, chunksize=CHUNK_SIZE)
for n, data_chunk in enumerate(iterator):
print ('Size of uploaded chunk: %i instances, %i features' % (data_chunk.shape))
# DATA PROCESSING placeholder
# MACHINE LEARNING placeholder
pass
print ('Sample values: \n%s' % str(data_chunk.iloc[0]))
Out:
Size of uploaded chunk: 2379 instances, 17 features
Size of uploaded chunk: 2379 instances, 17 features
Size of uploaded chunk: 2379 instances, 17 features
Size of uploaded chunk: 2379 instances, 17 features
Size of uploaded chunk: 2379 instances, 17 features
Size of uploaded chunk: 2379 instances, 17 features
Size of uploaded chunk: 2379 instances, 17 features
Sample values:
instant 15001
dteday 2012-09-22
season 3
yr 1
mnth 9
hr 5
holiday 0
weekday 6
workingday 0
weathersit 1
temp 0.56
atemp 0.5303
hum 0.83
windspeed 0.3284
casual 2
registered 15
cnt 17
Name: 0, dtype: object
这里,注意到迭代器是通过指定块大小来实例化的,也就是迭代器在每次迭代中必须返回的行数,这一点非常重要。chunksize参数可以取从 1 到任何值的值,尽管很明显,小批量(检索到的块)的大小与您的可用内存严格相连,以便在接下来的预处理阶段存储和操作它。
将更大的块带入内存仅在磁盘访问方面有优势。较小的块需要多次访问磁盘,并且根据物理存储的特性,需要更长的时间来传递数据。然而,从机器学习的角度来看,更小或更大的块对 Scikit 来说没有什么区别——学习核心外的函数,因为它们一次只考虑一个实例来学习,使它们在计算成本上真正线性。
使用数据库
作为熊猫输入/输出工具灵活性的一个例子,我们将提供一个使用 SQLite3 数据库的进一步例子,其中数据是从一个简单的查询逐块流式传输的。这个例子不仅仅是为了教学而提出的。从磁盘空间和处理时间的角度来看,在数据库中使用大型数据存储确实可以带来优势。
在 SQL 数据库中排列成表的数据可以标准化,从而消除冗余和重复,节省磁盘存储。数据库规范化是一种在数据库中排列列和表的方式,以减少它们的维度而不丢失任何信息。通常,这是通过拆分表并将重复的数据重新编码成键来实现的。此外,在内存、操作和多处理方面进行了优化的关系数据库可以加速和预测那些在 Python 脚本中处理的预处理活动。
使用 Python,SQLite(http://www.sqlite.org)是一个不错的默认选择,原因如下:
- 它是开源的
- 它可以处理大量数据(理论上每个数据库高达 140 TB,尽管不太可能看到任何 SQLite 应用处理如此大量的数据)
- 它在苹果操作系统、Linux 和视窗 32 和 64 位环境下运行
- 它不需要任何服务器基础架构或特定安装(零配置),因为所有数据都存储在磁盘上的单个文件中
- 使用 Python 代码可以很容易地将其扩展成存储过程
此外,Python 标准库包括一个sqlite3模块,提供从头开始创建数据库并使用它的所有功能。
在我们的示例中,我们将首先将包含自行车共享数据集的 CSV 文件每天和每小时上传到一个 SQLite 数据库,然后我们将像从 CSV 文件一样从该数据库进行流式传输。我们提供的数据库上传代码可以在整本书和您自己的应用中重复使用,而不局限于我们提供的特定示例(您只需更改输入和输出参数,仅此而已):
In : import os, sys
import sqlite3, csv,glob
SEP = ','
def define_field(s):
try:
int(s)
return 'integer'
except ValueError:
try:
float(s)
return 'real'
except:
return 'text'
def create_sqlite_db(db='database.sqlite', file_pattern=''):
conn = sqlite3.connect(db)
conn.text_factory = str # allows utf-8 data to be stored
c = conn.cursor()
# traverse the directory and process each .csv file useful for building the db
target_files = glob.glob(file_pattern)
print ('Creating %i table(s) into %s from file(s): %s' % (len(target_files), db, ', '.join(target_files)))
for k,csvfile in enumerate(target_files):
# remove the path and extension and use what's left as a table name
tablename = os.path.splitext(os.path.basename(csvfile))[0]
with open(csvfile, "rb") as f:
reader = csv.reader(f, delimiter=SEP)
f.seek(0)
for n,row in enumerate(reader):
if n==11:
types = map(define_field,row)
else:
if n>11:
break
f.seek(0)
for n,row in enumerate(reader):
if n==0:
sql = "DROP TABLE IF EXISTS %s" % tablename
c.execute(sql)
sql = "CREATE TABLE %s (%s)" % (tablename,\
", ".join([ "%s %s" % (col, ct) \
for col, ct in zip(row, types)]))
print ('%i) %s' % (k+1,sql))
c.execute(sql)
# Creating indexes for faster joins on long strings
for column in row:
if column.endswith("_ID_hash"):
index = "%s__%s" % \
( tablename, column )
sql = "CREATE INDEX %s on %s (%s)" % \
( index, tablename, column )
c.execute(sql)
insertsql = "INSERT INTO %s VALUES (%s)" % (tablename,
", ".join([ "?" for column in row ]))
rowlen = len(row)
else:
# raise an error if there are rows that don't have the right number of fields
if len(row) == rowlen:
c.execute(insertsql, row)
else:
print ('Error at line %i in file %s') % (n,csvfile)
raise ValueError('Houston, we\'ve had a problem at row %i' % n)
conn.commit()
print ('* Inserted %i rows' % n)
c.close()
conn.close()
该脚本提供了一个有效的数据库名称和模式来定位您想要导入的文件(接受通配符,如*),并从头创建一个您需要的新数据库和表,然后用所有可用的数据填充它们:
In: create_sqlite_db(db='bikesharing.sqlite', file_pattern='bikesharing\\*.csv')
Out: Creating 2 table(s) into bikesharing.sqlite from file(s): bikesharing\day.csv, bikesharing\hour.csv
1) CREATE TABLE day (instant integer, dteday text, season integer, yr integer, mnth integer, holiday integer, weekday integer, workingday integer, weathersit integer, temp real, atemp real, hum real, windspeed real, casual integer, registered integer, cnt integer)
* Inserted 731 rows
2) CREATE TABLE hour (instant integer, dteday text, season integer, yr integer, mnth integer, hr integer, holiday integer, weekday integer, workingday integer, weathersit integer, temp real, atemp real, hum real, windspeed real, casual integer, registered integer, cnt integer)
* Inserted 17379 rows
该脚本还报告了所创建字段的数据类型和行数,因此很容易验证在导入过程中一切是否顺利。现在很容易从数据库中流式传输。在我们的示例中,我们将在小时表和日表之间创建一个内部连接,以小时为基础提取数据,其中包含当天总租金的信息:
In: import os, sqlite3
import pandas as pd
DB_NAME = 'bikesharing.sqlite'
DIR_PATH = os.getcwd()
CHUNK_SIZE = 2500
conn = sqlite3.connect(DIR_PATH+'\\'+DB_NAME)
conn.text_factory = str # allows utf-8 data to be stored
sql = "SELECT H.*, D.cnt AS day_cnt FROM hour AS H INNER JOIN day as D ON (H.dteday = D.dteday)"
DB_stream = pd.io.sql.read_sql(sql, conn, chunksize=CHUNK_SIZE)
for j,data_chunk in enumerate(DB_stream):
print ('Chunk %i -' % (j+1)),
print ('Size of uploaded chunk: %i instances, %i features' % (data_chunk.shape))
# DATA PROCESSING placeholder
# MACHINE LEARNING placeholder
Out:
Chunk 1 - Size of uploaded chunk: 2500 instances, 18 features
Chunk 2 - Size of uploaded chunk: 2500 instances, 18 features
Chunk 3 - Size of uploaded chunk: 2500 instances, 18 features
Chunk 4 - Size of uploaded chunk: 2500 instances, 18 features
Chunk 5 - Size of uploaded chunk: 2500 instances, 18 features
Chunk 6 - Size of uploaded chunk: 2500 instances, 18 features
Chunk 7 - Size of uploaded chunk: 2379 instances, 18 features
如果您需要加速流,您只需要优化数据库,首先为您打算使用的关系查询构建正确的索引。
型式
conn.text_factory = str是剧本中非常重要的一部分;它允许存储 UTF-8 数据。如果忽略这样的命令,您可能会在输入数据时遇到奇怪的错误。
注意实例的排序
作为流数据主题的总结注释,我们必须警告您这样一个事实,即在流传输时,由于您的学习所基于的示例顺序,您实际上在学习过程中包含了隐藏的信息。
事实上,在线学习者基于他们评估的每个实例来优化他们的参数。每个实例都将引导学习者朝着优化过程中的某个方向前进。从全局来看,在给定足够多的评估实例的情况下,学习者应该采取正确的优化方向。然而,如果学习者改为通过有偏差的观察(例如,按时间排序的观察或以有意义的方式分组的观察)来训练,则算法也将学习偏差。为了不记起以前见过的例子,可以在训练中做一些事情,但无论如何都会引入一些偏见。如果你正在学习时间序列——对时间流动的反应通常是模型的一部分——这样的偏差是相当有用的,但在大多数其他情况下,它充当了某种过度拟合,并转化为最终模型中某种程度的泛化能力的缺乏。
如果您的数据具有某种排序,而您不希望机器学习算法学习到这种排序(例如 ID 顺序),作为一种谨慎措施,您可以在流式传输数据之前对其行进行洗牌,并获得更适合在线随机学习的随机顺序。
最快的方法,也是占用较少磁盘空间的方法,是在内存中流式传输数据集,并通过压缩来缩小数据集。在大多数情况下,但不是所有情况下,由于所应用的压缩算法以及您用于训练的数据的相对稀疏性和冗余性,这种方法都是可行的。在它不起作用的情况下,您必须直接在磁盘上洗牌,这意味着更多的磁盘空间消耗。
在这里,我们首先介绍一种快速的内存洗牌方法,这得益于能够将行快速压缩到内存中的zlib包,以及来自random模块的shuffle功能:
In: import zlib
from random import shuffle
def ram_shuffle(filename_in, filename_out, header=True):
with open(filename_in, 'rb') as f:
zlines = [zlib.compress(line, 9) for line in f]
if header:
first_row = zlines.pop(0)
shuffle(zlines)
with open(filename_out, 'wb') as f:
if header:
f.write(zlib.decompress(first_row))
for zline in zlines:
f.write(zlib.decompress(zline))
import os
local_path = os.getcwd()
source = 'bikesharing\\hour.csv'
ram_shuffle(filename_in=local_path+'\\'+source, \
filename_out=local_path+'\\bikesharing\\shuffled_hour.csv', header=True)
型式
对于 Unix 用户来说,sort命令可以很容易地用一次调用来使用(-R参数),它比任何 Python 实现都更容易、更高效地混合大量文本文件。它可以与使用管道的减压和压缩步骤相结合。
因此,类似下面的命令应该可以做到这一点:
zcat sorted.gz | sort -R | gzip - > shuffled.gz
如果内存不足以存储所有压缩数据,唯一可行的解决方案是像在磁盘上一样对文件进行操作。下面的代码片段定义了一个函数,该函数会重复地将您的文件分割成越来越小的文件,在内部对它们进行洗牌,并在一个更大的文件中再次随机排列它们。结果不是完美的随机重排,而是行分散到足以破坏任何可能影响在线学习的先前顺序:
In: from random import shuffle
import pandas as pd
import numpy as np
import os
def disk_shuffle(filename_in, filename_out, header=True, iterations = 3, CHUNK_SIZE = 2500, SEP=','):
for i in range(iterations):
with open(filename_in, 'rb') as R:
iterator = pd.read_csv(R, chunksize=CHUNK_SIZE)
for n, df in enumerate(iterator):
if n==0 and header:
header_cols =SEP.join(df.columns)+'\n'
df.iloc[np.random.permutation(len(df))].to_csv(str(n)+'_chunk.csv', index=False, header=False, sep=SEP)
ordering = list(range(0,n+1))
shuffle(ordering)
with open(filename_out, 'wb') as W:
if header:
W.write(header_cols)
for f in ordering:
with open(str(f)+'_chunk.csv', 'r') as R:
for line in R:
W.write(line)
os.remove(str(f)+'_chunk.csv')
filename_in = filename_out
CHUNK_SIZE = int(CHUNK_SIZE / 2)
import os
local_path = os.getcwd()
source = 'bikesharing\\hour.csv'
disk_shuffle(filename_in=local_path+'\\'+source, \
filename_out=local_path+'\\bikesharing\\shuffled_hour.csv', header=True)
随机学习
定义了流过程后,现在是时候看一看学习过程了,因为正是学习及其特定需求决定了在预处理阶段处理数据和转换数据的最佳方式。
与批处理学习相反,在线学习使用更大数量的迭代,一次从每个单个实例中获取方向,因此与批处理优化相比,在线学习允许更不稳定的学习过程,批处理优化会立即从整体数据中获取正确的方向。
分批梯度下降
机器学习的核心算法梯度下降因此被重新考虑,以适应在线学习。当处理批处理数据时,梯度下降可以使用比统计算法少得多的计算来最小化线性回归分析的成本函数。梯度下降的复杂度按照 O(np)* 的顺序排序,使得学习回归系数即使在出现大的 n (代表观测数)和大的 p (变量数)时也是可行的。当训练数据中存在高度相关甚至完全相同的特征时,它也能很好地工作。
一切都基于一个简单的优化方法:通过多次迭代来改变参数集,使其从一个随机的解开始逐渐收敛到最优解。梯度下降是一种理论上众所周知的优化方法,对于某些问题(如回归问题)具有已知的收敛保证。然而,让我们从下面的图像开始,该图像表示参数可以取的值(表示假设空间)之间的复杂映射(典型的神经网络),并导致成本函数的最小化:

用一个比喻的例子,梯度下降就像蒙着眼睛在山上行走。如果你想在看不到路径的情况下下降到最低的山谷,你可以沿着你感觉正在下坡的方向前进;尝试一会儿,然后停下来,再次感受地形,然后朝着你感觉它正在下坡的地方前进,以此类推,一次又一次。如果你继续朝着地面下降的地方前进,你最终会到达一个点,因为地形平坦,你不能再下降了。希望在那个时候,你已经到达目的地了。
使用这种方法,您需要执行以下操作:
-
决定起点。这通常是通过对函数参数的初始随机猜测来实现的(多次重启将确保初始化不会因为不幸运的初始设置而导致算法达到局部最优)。
-
能够感受到地形,也就是能够分辨出它什么时候下沉。用数学术语来说,这意味着你应该能够得到实际参数化函数相对于目标变量的导数,也就是你正在优化的成本函数的偏导数。请注意,梯度下降适用于您的所有数据,试图同时优化所有实例的预测。
-
决定你应该沿着导数指示的方向走多长时间。用数学术语来说,这相当于一个权重(通常称为 alpha),用来决定在优化的每一步你应该改变多少参数。这个方面可以被认为是学习因素,因为它指出了你应该在每个优化步骤中从数据中学到多少。与任何其他超参数一样,alpha 的最佳值可以通过对验证集的性能评估来确定。
-
Determine when to stop, given a too marginal improvement of the cost function with respect to the previous step. In such a sense, you also should be able to notice when something goes wrong and you are not going in the right direction maybe because you are using too large an alpha for the learning. This is actually a matter of momentum, that is, the speed at which the algorithm converges toward the optimum. It is just like throwing a ball down a mountainside: it just rolls over small dents in the surface, but if its speed is too high, it won't stop at the right point. Thus, if alpha is set correctly, the momentum will naturally slow down as the algorithm is approaching the optimum as shown in the following image in the right panel. However, if it is not set properly, it will just jump over the global optimum and report further errors to be minimized, as depicted in the following image on the right panel, when the optimization process causes parameters to jump across different values without achieving the required error minimization:
![Batch gradient descent]()
为了更好地描述梯度下降的情况,让我们以线性回归为例,其参数通过这样的过程进行优化。
我们从成本函数 J 开始,给定权重向量 w :

训练数据矩阵 X 和系数向量 w 之间的矩阵向量乘法 Xw 表示线性模型的预测,其与响应 y 的偏差被平方,然后求和,最后除以两次 n ,即实例数。
最初,向量 w 可以是实例化的,使用的随机数取自平均值为零、标准差为单位的标准化正态分布。(实际上,初始化可以用很多不同的方法来完成,所有这些方法都同样适用于近似成本函数为碗形且具有唯一最小值的线性回归。)这允许我们的算法沿着优化路径的某个地方开始,并且可以有效地加速过程的收敛。当我们优化线性回归时,初始化不应该给算法带来太多麻烦(最坏的情况是,错误的开始只会让它变慢)。相反,当我们使用梯度下降来优化不同的机器学习算法(如神经网络)时,我们可能会因为错误的初始化而陷入困境。举例来说,如果初始的 w 仅仅充满了零值(风险是被困在一个完美对称的山顶上,在那里没有方向性可以立即带来比任何其他更好的优化),就会发生这种情况。这也可能发生在具有多个局部极小值的优化过程中。
给定起始随机系数向量 w ,我们可以立即计算成本函数 J(w) ,并通过从每个系数中减去成本函数偏导数的部分α(α,学习率)来确定每个系数的初始方向,如下式所示:

这可以在求解偏导数后更好地表达出来,如下所示:

值得注意的是,在给定特征向量 xj 的情况下,对每个奇异系数( wj )进行更新,但同时基于所有预测(因此是求和)。
在迭代 w 中的所有系数之后,系数的更新将完成,并且优化可以通过计算偏导数和更新 w 向量来重新开始。
该过程的一个有趣的特征是,随着 w 向量接近最优配置,更新将越来越少。因此,当相对于之前的操作,在 w 中引起的变化很小时,该过程可以停止。无论如何,当学习率 alpha 设置为正确的大小时,我们的更新确实会减少。事实上,如果它的值太大,它可能导致优化绕道而行并失败,在某些情况下,导致过程完全发散,并且不可能最终收敛到解决方案。事实上,优化往往会超出目标,实际上离目标更远。
另一端,过小的α值不仅会使优化过程向目标移动得太慢,还可能很容易陷入局部极小值。对于更复杂的算法尤其如此,就像神经网络一样。至于线性回归和它的分类对应物,逻辑回归,因为优化曲线是碗状的,就像凹曲线一样,它的特点是只有一个极小值,根本没有局部极小值。
在我们说明的实现中,α是一个固定的常数(一个固定的学习速率梯度下降)。由于α在收敛到最优解的过程中扮演着如此重要的角色,因此人们设计了不同的策略,使其随着优化的进行而开始变大和缩小。我们将在检查 Scikit-learn 实现时讨论这些不同的方法。
随机梯度下降
到目前为止看到的梯度下降的版本被称为整批梯度下降,通过优化整个数据集的误差来工作,因此需要将其存储在内存中。核心外版本为随机梯度下降 ( SGD )和小批量梯度下降。
在这里,公式保持完全相同,但为了更新;一次只更新一个实例,这样我们就可以将核心数据留在存储中,只在内存中进行一次观察:

核心思想是,如果实例是随机挑选的,没有特定的偏差,优化将平均地朝着目标成本最小化的方向发展。这解释了为什么我们讨论了如何从一个流中删除任何排序,并使其尽可能随机。例如,在自行车共享的例子中,如果你有随机梯度下降,首先学习早期季节的模式,然后关注夏季,然后关注秋季,以此类推,取决于优化停止时的季节,模型将被调整为比其他季节更好地预测一个季节,因为最近的大多数例子来自那个季节。在随机梯度下降优化中,当数据为独立同分布 ( 内径)时,保证收敛到全局最小值。实际上,i.i.d .意味着您的示例不应该有顺序或分布,而应该像从可用示例中随机挑选一样向算法提出。
Scikit-learn SGD 实施
在 Scikit-learn 包中可以找到大量在线学习算法。并非所有的机器学习算法都有在线对应物,但这个列表很有趣,而且还在稳步增长。对于监督学习,我们可以将可用的学习者分为分类器和回归器,并对它们进行枚举。
作为量词,我们可以提到以下几点:
sklearn.naive_bayes.MultinomialNBsklearn.naive_bayes.BernoulliNBsklearn.linear_model.Perceptronsklearn.linear_model.PassiveAggressiveClassifiersklearn.linear_model.SGDClassifier
作为回归者,我们有两个选择:
sklearn.linear_model.PassiveAggressiveRegressorsklearn.linear_model.SGDRegressor
他们都可以增量学习,一个实例一个实例地更新自己;虽然只有SGDClassifier和SGDRegressor是基于我们之前描述的随机梯度下降优化,它们是本章的主要主题。SGD 学习器对于所有大规模问题来说都是最优的,因为它们的复杂性与 O(knp) 有关,其中 k 是数据的传递次数, n 是实例的数量, p 是特征的数量(如果我们使用稀疏矩阵,则自然是非零特征):一个完全线性的时间学习器,花费的时间与所显示的示例数量成正比。
其他在线算法将作为比较基准。此外,所有算法都使用相同的应用编程接口,基于在线学习和小批量的partial_fit方法(当您流式传输更大的块而不是单个实例时)。共享同一个应用编程接口使得所有这些学习技术在你的学习框架中可以互换。
与使用所有可用数据进行即时优化的 fit 方法相反,partial_fit基于传递的每个单个实例进行部分优化。即使将数据集传递给partial_fit方法,算法也不会处理整个批次,而是处理其单个元素,使得学习操作的复杂性确实是线性的。此外,partial_fit之后的学习者可以通过后续的partial_fit调用不断更新,这使得它非常适合从连续的数据流中进行在线学习。
分类时,唯一需要注意的是,在第一次初始化时,有必要知道我们将学习多少个类,以及它们是如何被标记的。这可以使用 classes 参数来完成,指出数值标签的列表。这需要事先探索,通过数据流记录问题的标签,并注意它们的分布,以防它们不平衡——一个类相对于其他类在数字上太大或太小(但是 Sciket-learn 实现提供了一种自动处理问题的方法)。如果目标变量是数字,知道它的分布仍然是有用的,但这不是成功运行学习者所必需的。
在 Scikit-learn 中,我们有两个实现——一个用于分类问题(SGDClassifier)一个用于回归问题(SGDRegressor)。分类实现可以使用一对全 ( OVA )策略处理多类问题。这个策略意味着,给定 k 个类,建立 k 个模型,每个类一个模型,与其他类的所有实例相对,因此创建 k 个二进制分类。这将产生 k 组系数和 k 个预测向量及其概率。最后,基于每个类别相对于其他类别的发射概率,将分类分配给具有最高概率的类别。如果我们需要给出多项式分布的实际概率,我们可以简单地通过除以它们的和来归一化结果。(这是神经网络中 softmax 层正在发生的事情,我们将在后面的章节中看到。)
Scikit-learn 中的分类和回归 SGD 实现都具有不同的损失函数(代价函数,随机梯度下降优化的核心)。
对于分类,用loss参数表示,我们可以依赖于以下内容:
loss='log':经典逻辑回归loss='hinge':软余量,即线性支持向量机loss='modified_huber':平滑的铰链损失
对于回归,我们有三个损失函数:
loss='squared_loss': 普通最小 平方 ( OLS )进行线性回归loss='huber':针对异常值的稳健回归的 Huber 损失loss='epsilon_insensitive':线性支持向量回归
我们将给出一些使用经典统计损失函数的例子,它们是逻辑损失和 OLS。铰链损失和支持向量机 ( 支持向量机)将在下一章讨论,详细介绍它们的功能是必要的。
提醒一下(这样你就不用去查阅其他任何补充的机器学习书籍了),如果我们把回归函数定义为 h,它的预测由 h(X) 给出,因为 X 是特征的矩阵,那么下面是合适的公式:

因此,要最小化的 OLS 成本函数如下:

在逻辑回归中,将二元结果 0 / 1 转化为优势比,πy 为正结果的概率,公式如下:

因此,物流成本函数定义如下:

定义 SGD 学习参数
为了在 Scikit-learn 中定义 SGD 参数,在分类和回归问题中(这样它们对SGDClassifier和SGDRegressor都有效),我们必须弄清楚当您不能一次评估所有数据时,如何处理正确学习所必需的一些重要参数。
第一个是n_iter,通过数据定义迭代次数。最初设置为 5 ,经验表明,在给定其他默认参数的情况下,应该对其进行调整,以便学习者查看10^6示例;因此,设置它的一个好的解决方案是n_iter = np.ceil(10**6 / n),其中 n 是实例的数量。值得注意的是,n_iter仅适用于内存中的数据集,因此它仅在通过 fit 方法操作时起作用,而不适用于partial_fit。实际上,partial_fit将重复相同的数据,只是如果你在你的过程中对它进行重新流,并且重新流的正确迭代次数是要沿着学习过程本身进行测试的,受数据类型的影响。在下一章中,我们将说明超参数优化,并讨论正确的遍数。
型式
在进行小批量学习时,每次完整地传递完所有数据后,重新整理数据可能是有意义的。
shuffle是需要的参数,如果你想打乱你的数据。它是指内存中的小批量,而不是指核心外的数据排序。它也适用于partial_fit,但在这种情况下,它的效果非常有限。请始终将其设置为 True,但是对于要以块为单位传递的数据,请将您的数据移出内核,如我们之前所述。
warm_start是与拟合方法一起工作的另一个参数,因为它会记住以前的拟合系数(但如果已经动态修改,则不会记住学习速率)。如果使用partial_fit方法,算法将记住先前学习的系数和学习速率表的状态。
average参数触发一个计算技巧,在特定情况下,开始对新系数和旧系数进行平均,从而加快收敛速度。它可以设置为True或一个整数值,指示从什么情况开始取平均值。
最后但同样重要的是,我们有learning_rate及其相关参数,eta0和power_t。learning_rate参数意味着每个观察到的实例如何影响优化过程。当从理论角度提出 SGD 时,我们提出了恒定速率学习,这可以通过设置learning_rate='constant'来复制。
然而,存在其他选择,让 eta
(在 Scikit-learn 中称为学习率,在时间 t 定义)逐渐降低。在分类中,提出的解决方案是learning_rate='optimal',由以下公式给出:

这里, t 是时间步长,由实例数乘以迭代次数给出, t0 是一个启发式选择的值,因为莱昂·博图的研究,其版本的随机梯度 SVM 严重影响了 SGD Scikit-learn 实现(http://leon.bottou.org/projects/sgd)。这种学习策略的明显优势是,随着看到更多的例子,学习会减少,避免了由异常值给出的优化的突然扰动。显然,这个策略也是现成的,也就是说你和它没有太多关系。
在回归中,建议的学习衰退由该公式给出,对应于learning_rate= 'invscaling':

这里,eta0和power_t是要通过优化搜索进行优化的超参数(它们最初被设置为0和0.5)。值得注意的是,使用invscaling学习速率,SGD 将从较低的学习速率开始,低于最佳速率,并且它将降低得更慢,在学习期间适应性更强。
数据流特征管理
数据流带来的问题是,您无法像处理完整的内存数据集那样进行评估。对于一个正确和最优的方法来馈送你的 SGD 核外算法,你首先必须调查数据(例如,通过对文件的初始实例进行卡盘)并找出你手头的数据类型。
我们区分以下类型的数据:
- 定量值
- 用整数编码的分类值
- 以文本形式表达的非结构化分类值
当数据是定量的时,它可以仅仅被馈送给 SGD 学习器,但是事实上该算法对特征缩放相当敏感;也就是说,你必须将所有的量化特征纳入相同的价值范围,否则学习过程不会轻易正确地收敛。可能的缩放策略是转换[0,1],[-1,1]范围内的所有值,或者通过将其平均值居中到零并将其方差转换为单位来标准化变量。对于缩放策略的选择,我们没有特别的建议,但是如果您正在处理稀疏矩阵,并且您的大部分值为零,那么在[0,1]范围内转换会特别有效。
至于内存学习,在转换训练集上的变量时,你必须注意你使用的值(基本上,你需要得到每个特征的最小值、最大值、平均值和标准差。)并在测试集中重用它们,以便获得一致的结果。
鉴于您正在流式传输数据,并且不可能将所有数据都上传到内存中,您必须通过传递所有数据或至少一部分数据来计算它们(采样总是一种可行的解决方案)。使用一个短暂的流(一个你无法复制的流)会带来更具挑战性的问题;事实上,你必须不断追踪你不断接受的价值。
如果采样只需要您计算一大块 n 实例的统计数据(假设您的流没有特定的顺序),那么动态计算统计数据需要您记录正确的度量。
对于最小值和最大值,您需要为每个定量特征存储一个变量。从第一个值开始,您将该值存储为您的初始最小值和最大值,对于您将从流中接收的每个新值,您将不得不将其与之前记录的最小值和最大值进行比较。如果新实例超出了先前的值范围,则只需相应地更新变量。
此外,平均值不会带来任何特别的问题,因为您只需要保存看到的值的总和和实例的计数。至于方差,你需要回忆一下教科书的公式如下:

值得注意的是,您需要知道平均值μ,这也是您从流中逐步学习的。然而,该公式可以解释如下:

由于您只是记录实例的数量 n 和 x 值的总和,您只需要存储另一个变量,它是 x 平方值的总和,您将拥有配方的所有成分。
例如,使用自行车共享数据集,我们可以计算报告最终结果的运行平均值、标准偏差和范围,并绘制数据从磁盘流出时这些统计数据的变化情况:
In: import os, csv
local_path = os.getcwd()
source = 'bikesharing\\hour.csv'
SEP=','
running_mean = list()
running_std = list()
with open(local_path+'\\'+source, 'rb') as R:
iterator = csv.DictReader(R, delimiter=SEP)
x = 0.0
x_squared = 0.0
for n, row in enumerate(iterator):
temp = float(row['temp'])
if n == 0:
max_x, min_x = temp, temp
else:
max_x, min_x = max(temp, max_x),min(temp, min_x)
x += temp
x_squared += temp**2
running_mean.append(x / (n+1))
running_std.append(((x_squared - (x**2)/(n+1))/(n+1))**0.5)
# DATA PROCESSING placeholder
# MACHINE LEARNING placeholder
pass
print ('Total rows: %i' % (n+1))
print ('Feature \'temp\': mean=%0.3f, max=%0.3f, min=%0.3f,\ sd=%0.3f' % (running_mean[-1], max_x, min_x, running_std[-1]))
Out: Total rows: 17379
Feature 'temp': mean=0.497, max=1.000, min=0.020, sd=0.193
过一会儿,数据将从数据源流出,与temp特征相关的关键数字将被记录为平均值的运行估计,标准偏差将被计算并存储在两个单独的列表中。
通过绘制列表中的值,我们可以检查估计值相对于最终数字的波动程度,并了解在获得稳定的平均值和标准偏差估计值之前需要多少个实例:
In: import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(running_mean,'r-', label='mean')
plt.plot(running_std,'b-', label='standard deviation')
plt.ylim(0.0,0.6)
plt.xlabel('Number of training examples')
plt.ylabel('Value')
plt.legend(loc='lower right', numpoints= 1)
plt.show()
如果您之前处理了原始的自行车共享数据集,您将获得一个图表,其中数据明显有趋势(由于时间顺序,因为温度自然会随着季节而变化):

相反,如果我们使用数据集的混洗版本作为源,shuffled_hour.csv文件,我们可以获得几个更加稳定和快速收敛的估计。因此,我们将会了解到一个近似但更可靠的平均值和标准偏差的估计,从流中观察到更少的实例:

两张图的不同提醒我们随机化观察顺序的重要性。即使学习简单的描述性统计也会受到数据趋势的严重影响;因此,在通过 SGD 学习复杂模型时,我们必须更加注意。
描述目标
此外,目标变量也需要在启动前进行探索。事实上,我们需要确定它假设了什么样的值,如果是绝对的,并弄清楚它在类中是否不平衡,或者当是一个数字时是否有偏斜分布。
如果我们正在学习一个数字响应,我们可以对特性采用前面显示的相同策略,而对于类,一个 Python 字典记录类的数量(键)和它们的频率(值)就足够了。
例如,我们将下载一个数据集进行分类,森林覆盖类型数据。
为了快速下载和准备数据,我们将使用数据集中定义的gzip_from_UCI函数亲自尝试本章的部分:
In: UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/covtype/covtype.data.gz'
gzip_from_UCI(UCI_url)
如果在运行代码时出现问题,或者您更喜欢自己准备文件,只需前往 UCI 网站,下载数据集,并将其解包到 Python 当前工作的目录中:
https://archive . ics . UCI . edu/ml/机器学习-数据库/covtype/
一旦数据在磁盘上可用,我们可以扫描 581,012 个实例,将代表我们应该估计的类别的每行的最后一个值转换为其对应的森林覆盖类型:
In: import os, csv
local_path = os.getcwd()
source = 'covtype.data'
SEP=','
forest_type = {1:"Spruce/Fir", 2:"Lodgepole Pine", \
3:"Ponderosa Pine", 4:"Cottonwood/Willow",\
5:"Aspen", 6:"Douglas-fir", 7:"Krummholz"}
forest_type_count = {value:0 for value in forest_type.values()}
forest_type_count['Other'] = 0
lodgepole_pine = 0
spruce = 0
proportions = list()
with open(local_path+'\\'+source, 'rb') as R:
iterator = csv.reader(R, delimiter=SEP)
for n, row in enumerate(iterator):
response = int(row[-1]) # The response is the last value
try:
forest_type_count[forest_type[response]] +=1
if response == 1:
spruce += 1
elif response == 2:
lodgepole_pine +=1
if n % 10000 == 0:
proportions.append([spruce/float(n+1),\
lodgepole_pine/float(n+1)])
except:
forest_type_count['Other'] += 1
print ('Total rows: %i' % (n+1))
print ('Frequency of classes:')
for ftype, freq in sorted([(t,v) for t,v \
in forest_type_count.iteritems()], key = \
lambda x: x[1], reverse=True):
print ("%-18s: %6i %04.1f%%" % \
(ftype, freq, freq*100/float(n+1)))
Out: Total rows: 581012
Frequency of classes:
Lodgepole Pine : 283301 48.8%
Spruce/Fir : 211840 36.5%
Ponderosa Pine : 35754 06.2%
Krummholz : 20510 03.5%
Douglas-fir : 17367 03.0%
Aspen : 9493 01.6%
Cottonwood/Willow : 2747 00.5%
Other : 0 00.0%
输出显示两个类Lodgepole Pine和Spruce/Fir占据了大部分观察值。如果示例在流中被适当地打乱,SGD 将适当地学习正确的先验分布,并因此调整其概率发射(后验概率)。
如果与我们目前的情况相反,您的目标不是提高分类精度,而是增加接收机工作特性 ( ROC ) 曲线下面积 ( AUC )或 f1-score(可用于评估的误差函数;有关概述,您可以直接参考 Scikit-learn 文档,网址为http://Scikit-learn . org/stable/modules/model _ evaluation . html关于在不平衡数据上训练的分类模型,然后提供的信息可以帮助您在定义SGDClassifier时使用class_weight参数平衡权重,或者在部分拟合模型时使用sample_weight参数平衡权重。两者都通过增加或减少观察到的实例的重量来改变其影响。在这两种方式中,操作这两个参数将改变先验分布。加权类和实例将在下一章讨论。
在进行训练和跟班工作之前,我们可以检查一下类的比例是否总是一致的,以便向 SGD 传达正确的先验概率:
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
proportions = np.array(proportions)
plt.plot(proportions[:,0],'r-', label='Spruce/Fir')
plt.plot(proportions[:,1],'b-', label='Lodgepole Pine')
plt.ylim(0.0,0.8)
plt.xlabel('Training examples (unit=10000)')
plt.ylabel('%')
plt.legend(loc='lower right', numpoints= 1)
plt.show()

在上图中,您可以注意到随着我们按现有顺序对数据进行流式传输,示例的百分比是如何变化的。在这种情况下,如果我们想要一个随机的在线算法从数据中正确学习,洗牌是非常必要的。
其实的比例是可变的;这个数据集有某种排序,可能是地理排序,应该通过重新排列数据来纠正,否则我们将冒着高估或低估某些类别相对于其他类别的风险。
散列技巧
如果在你的特性中,有类别(以值编码或者以文本形式留下),事情会变得有点棘手。通常,在批处理学习中,你会对类别进行一次热编码,并获得与类别一样多的新的二进制特征。不幸的是,在一个流中,你事先不知道你将处理多少个类别,甚至不能通过采样来确定它们的数量,因为稀有类别可能在流中出现得很晚,或者需要太大的样本才能被发现。您必须首先流式传输所有数据,并记录出现的每个类别。无论如何,流可能是短暂的,有时类的数量可能非常大,以至于它们不能存储在内存中。在线广告数据就是这样一个例子,因为它的量很大,很难存储起来,并且因为该流不能被传递超过一次。此外,广告数据多种多样,特征不断变化。
处理文本使这个问题变得更加明显,因为你无法预料你将要分析的文本中会有什么样的单词。在单词包模型中,对于每个文本,当前单词被计数,它们的频率值被粘贴在每个单词特有的特征向量的元素中,您应该能够预先将每个单词映射到一个索引。即使你能做到这一点,当一个未知的单词(因此以前从未映射过)在测试中或者预测器在生产中出现时,你也必须处理这种情况。此外,还应该补充的是,作为一种口语,由数十万甚至数百万个不同术语组成的词典并不罕见。
简单回顾一下,如果你能提前知道你的特性中的类,你可以使用 Scikit-learn(http://Scikit-learn . org/stable/modules/generated/sklearn . premization . onehotencoder . html)的一键编码器来处理它们。我们实际上不会在这里说明它,但基本上,这种方法与您在使用批处理学习时应用的方法没有任何不同。我们想向你说明的是,当你不能真正应用一热编码时。
有一种解决方案被称为哈希技巧,因为它基于哈希函数,可以处理整数或字符串形式的文本和分类变量。它也可以处理混合了数量特征数值的分类变量。one-hot 编码的核心问题是,它在将特征映射到特征向量的某个位置后,将该位置分配给该位置的值。哈希技巧可以将一个值唯一地映射到它的位置,而无需事先评估该特征,因为它利用了哈希函数的核心特性——确定性地将一个值或字符串转换为整数值。
因此,在应用它之前,唯一必要的准备工作是创建一个足够大的稀疏向量来表示数据的复杂性(可能包含从 2**19 到 2**30 的元素,具体取决于可用内存、计算机的总线架构以及您正在使用的哈希函数的类型)。如果您正在处理一些文本,您还需要一个标记器,即一个将您的文本拆分成单个单词并删除标点符号的功能。
一个简单的玩具例子就能说明这一点。我们将使用 Scikit-learn 包中的两个专用函数:HashingVectorizer,一个基于哈希技巧的用于文本数据的转换器,以及FeatureHasher,这是另一个转换器,专门用于将表示为 Python 字典的数据行转换为稀疏的特征向量。
作为第一个例子,我们将把一个短语变成一个向量:
In: from sklearn.feature_extraction.text import HashingVectorizer
h = HashingVectorizer(n_features=1000, binary=True, norm=None)
sparse_vector = h.transform(['A simple toy example will make clear how it works.'])
print(sparse_vector)
Out:
(0, 61) 1.0
(0, 271) 1.0
(0, 287) 1.0
(0, 452) 1.0
(0, 462) 1.0
(0, 539) 1.0
(0, 605) 1.0
(0, 726) 1.0
(0, 918) 1.0
生成的向量只有在特定索引处有单位值,指出短语(单词)中的标记和向量中特定位置之间的关联。不幸的是,除非我们在外部 Python 字典中映射每个令牌的哈希值,否则关联无法逆转。虽然这种映射是可能的,但它确实会消耗内存,因为根据语言和主题的不同,字典可能会很大,在数百万条目甚至更多的范围内。实际上,我们不需要保持这样的跟踪,因为散列函数保证总是从同一个令牌产生相同的索引。
哈希技巧的一个真正问题是冲突的可能性,当两个不同的令牌关联到同一个索引时就会发生冲突。在使用大型单词词典时,这是一种罕见但可能发生的情况。另一方面,在一个由数百万个系数组成的模型中,有影响力的很少。因此,如果发生冲突,可能会涉及两个不重要的令牌。当使用散列技巧时,概率是站在你这边的,因为有足够大的输出向量(例如,元素的数量在 2^24 之上),尽管冲突总是可能的,但是它们极不可能涉及模型的重要元素。
哈希技巧可以应用于正常特征向量,尤其是当有分类变量时。下面是一个带有FeatureHasher的例子:
In: from sklearn.feature_extraction import FeatureHasher
h = FeatureHasher(n_features=1000, non_negative=True)
example_row = {'numeric feature':3, 'another numeric feature':2, 'Categorical feature = 3':1, 'f1*f2*f3':1*2*3}
print (example_row)
Out: {'another numeric feature': 2, 'f1*f2*f3': 6, 'numeric feature': 3, 'Categorical feature = 3': 1}
如果您的 Python 字典包含数值的要素名称以及任何分类变量的要素名称和值的组合,字典的值将使用关键字的散列索引进行映射,从而创建一个一次性编码的要素向量,准备好由 SGD 算法学习:
In: print (h.transform([example_row]))
Out:
(0, 16) 2.0
(0, 373) 1.0
(0, 884) 6.0
(0, 945) 3.0
其他基本变换
正如我们从数据存储中绘制的示例一样,除了将分类特征转换为数字特征之外,还可以应用另一种转换,以便让学习算法增加其预测能力。变换可以通过函数(通过应用平方根、对数或其他变换函数)或通过对要素组的操作应用于要素。
在下一章,我们将提出关于多项式展开和随机厨房水槽方法的详细例子。在本章中,我们将预测如何通过嵌套迭代创建二次特征。二次特征通常是在创建多项式展开时创建的,其目的是截取预测特征如何在它们之间相互作用;这可能会以意想不到的方式影响目标变量中的响应。
作为直观阐明为什么二次特征在目标反应建模中很重要的一个例子,让我们解释两种药物对患者的影响。事实上,可能每种药物对我们正在对抗的疾病或多或少都有效。无论如何,这两种药物是由不同的成分组成的,当患者一起摄入时,往往会抵消彼此的效果。在这种情况下,虽然两种药物都有效,但由于它们的负面相互作用,它们一起根本不起作用。
从这个意义上说,特征之间的相互作用可以在各种各样的特征中找到,而不仅仅是在医学中,找到最重要的一个是至关重要的,这样我们的模型才能更好地预测它的目标。如果我们没有意识到某些特性与我们的问题相互作用,我们唯一的选择就是系统地测试它们,并让我们的模型保留那些工作得更好的特性。
在下面这个简单的例子中,一个名为 v 的向量,一个我们想象的刚刚在内存中流动以便被学习的例子,被转换成另一个向量 vv ,其中 v 的原始特征伴随着它们的乘法交互的结果(每个特征被所有其他特征相乘一次)。给定更多数量的特征,学习算法将使用 vv 向量代替原始的 v 向量,以实现数据的更好拟合:
In: import numpy as np
v = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
vv = np.hstack((v, [v[i]*v[j] for i in range(len(v)) for j in range(i+1, len(v))]))
print vv
Out:[ 1 2 3 4 5 6 7 8 9 10 2 3 4 5 6 7 8 9 10 6 8 10 12 14 16 18 20 12 15 18 21 24 27 30 20 24 28 32 36 40 30 35 40 45 50 42 48 54 60 56 63 70 72 80 90]
类似的转换,或者甚至更复杂的转换,可以在示例流向学习算法时动态生成,利用了数据批量小(有时减少到单个示例)的事实,并扩展了特征的数量,因此可以在内存中可行地实现几个示例。在下一章中,我们将探索更多这类转换的例子,以及它们成功集成到学习管道中的例子。
流中的测试和验证
在引入 SGD 后,我们没有展示完整的训练示例,因为我们需要介绍如何在流中测试和验证。使用批处理学习、测试和交叉验证是一个随机化观察顺序的问题,将数据集切割成折叠并取一个精确的折叠作为测试集,或者依次系统地取所有折叠来测试您的算法的学习能力。
流不能保存在内存中,因此在以下实例已经随机化的基础上,最好的解决方案是在流展开一段时间后进行验证实例,或者在数据流中系统地使用精确的、可复制的模式。
流的部分样本外方法实际上与测试样本相当,只有事先知道流的长度才能成功完成。对于连续流,这仍然是可能的,但是意味着一旦测试实例开始,就必须停止学习。这种方法被称为 n 策略后的保持。
交叉验证类型的方法可以使用系统的和可复制的验证实例抽样。定义了起始缓冲区后,每隔 n 次选择一个实例进行验证。这样的实例不是用于培训,而是用于测试目的。这种方法被称为每隔 n 次的周期性保持策略。
由于验证是在单个实例的基础上进行的,因此会计算一个全局性能度量,使用最新的一组 k 度量对数据的同一次传递中或以类似窗口的方式收集的所有误差度量进行平均,其中 k 是您认为具有有效代表性的一些测试。
事实上,在第一遍中,所有实例实际上都是学习算法看不到的。因此,当算法接收要学习的案例时,测试算法是有用的,在学习之前,根据观察结果验证其响应。这种方法被称为渐进验证。
在行动中尝试 SGD
作为本章的结论,我们将实现两个示例:一个用于基于森林覆盖类型数据的分类,一个用于基于自行车共享数据集的回归。我们将看到如何将之前对响应和特性分布的见解付诸实践,以及如何为每个问题使用最佳验证策略。
从分类问题开始,有两个值得注意的方面需要考虑。作为一个多类问题,首先我们注意到数据库中存在某种排序,并且类沿着实例流分布。作为第一步,我们将使用在章节中定义的ram_shuffle函数对数据进行洗牌,注意实例部分的顺序:
In: import os
local_path = os.getcwd()
source = 'covtype.data'
ram_shuffle(filename_in=local_path+'\\'+source, \
filename_out=local_path+'\\shuffled_covtype.data', \
header=False)
当我们在不占用太多磁盘的情况下压缩内存中的行并进行洗牌时,我们可以快速获得一个新的工作文件。下面的代码将使用对数丢失(相当于逻辑回归)来训练SGDClassifier,这样它就利用了我们以前对数据集中存在的类的了解。forest_type列表包含课程的所有代码,并且每次都传递给(尽管只有一个,第一个就足够了)SGD 学习者的partial_fit方法。
出于验证目的,我们在200.000观察到的情况下定义冷启动。每十次,就有一次被排除在培训之外,用于验证。这个模式允许重复性,即使我们要多次传递数据;每次通过时,相同的实例将作为样本外测试被忽略,从而允许创建验证曲线来测试多次通过相同数据的效果。
保持模式也伴随着渐进的验证。所以冷启动后的每一个病例在被送去训练前都要进行评估。虽然渐进式验证提供了有趣的反馈,但这种方法仅适用于第一遍;事实上,在最初的传递之后,所有的观察(但是保持模式中的观察)都将成为样本内的实例。在我们的示例中,我们将只进行一次传递。
提醒一下,数据集有581.012个实例,用 SGD 进行流式处理和建模可能会有点长(对于单台计算机来说,这是一个相当大的问题)。虽然我们放置了一个限制器来观察仅仅250.000的实例,但是仍然允许你的计算机运行大约 15-20 分钟,然后期待结果:
In: import csv, time
import numpy as np
from sklearn.linear_model import SGDClassifier
source = 'shuffled_covtype.data'
SEP=','
forest_type = [t+1 for t in range(7)]
SGD = SGDClassifier(loss='log', penalty=None, random_state=1, average=True)
accuracy = 0
holdout_count = 0
prog_accuracy = 0
prog_count = 0
cold_start = 200000
k_holdout = 10
with open(local_path+'\\'+source, 'rb') as R:
iterator = csv.reader(R, delimiter=SEP)
for n, row in enumerate(iterator):
if n > 250000: # Reducing the running time of the experiment
break
# DATA PROCESSING
response = np.array([int(row[-1])]) # The response is the last value
features = np.array(map(float,row[:-1])).reshape(1,-1)
# MACHINE LEARNING
if (n+1) >= cold_start and (n+1-cold_start) % k_holdout==0:
if int(SGD.predict(features))==response[0]:
accuracy += 1
holdout_count += 1
if (n+1-cold_start) % 25000 == 0 and (n+1) > cold_start:
print '%s holdout accuracy: %0.3f' % (time.strftime('%X'), accuracy / float(holdout_count))
else:
# PROGRESSIVE VALIDATION
if (n+1) >= cold_start:
if int(SGD.predict(features))==response[0]:
prog_accuracy += 1
prog_count += 1
if n % 25000 == 0 and n > cold_start:
print '%s progressive accuracy: %0.3f' % (time.strftime('%X'), prog_accuracy / float(prog_count))
# LEARNING PHASE
SGD.partial_fit(features, response, classes=forest_type)
print '%s FINAL holdout accuracy: %0.3f' % (time.strftime('%X'), accuracy / ((n+1-cold_start) / float(k_holdout)))
print '%s FINAL progressive accuracy: %0.3f' % (time.strftime('%X'), prog_accuracy / float(prog_count))
Out:
18:45:10 holdout accuracy: 0.627
18:45:10 progressive accuracy: 0.613
18:45:59 holdout accuracy: 0.621
18:45:59 progressive accuracy: 0.617
18:45:59 FINAL holdout accuracy: 0.621
18:45:59 FINAL progressive accuracy: 0.617
作为第二个例子,我们将尝试基于一系列天气和时间信息来预测华盛顿共享自行车的数量。给定数据集的历史顺序,我们不会将其打乱,并将问题视为时间序列问题。我们的验证策略是在看到一定数量的例子后测试结果,以便复制从那时起预测的不确定性。
有趣的是,注意到一些特征是分类的,所以我们应用了 Scikit-learn 的FeatureHasher类,以便将记录在字典中的类别表示为由变量名和类别代码组成的联合字符串。字典中为每个键分配的值是 1,以便类似于哈希技巧将创建的稀疏向量中的二进制变量:
In: import csv, time, os
import numpy as np
from sklearn.linear_model import SGDRegressor
from sklearn.feature_extraction import FeatureHasher
source = '\\bikesharing\\hour.csv'
local_path = os.getcwd()
SEP=','
def apply_log(x): return np.log(float(x)+1)
def apply_exp(x): return np.exp(float(x))-1
SGD = SGDRegressor(loss='squared_loss', penalty=None, random_state=1, average=True)
h = FeatureHasher(non_negative=True)
val_rmse = 0
val_rmsle = 0
predictions_start = 16000
with open(local_path+'\\'+source, 'rb') as R:
iterator = csv.DictReader(R, delimiter=SEP)
for n, row in enumerate(iterator):
# DATA PROCESSING
target = np.array([apply_log(row['cnt'])])
features = {k+'_'+v:1 for k,v in row.iteritems() \
if k in ['holiday','hr','mnth','season', \
'weathersit','weekday','workingday','yr']}
numeric_features = {k:float(v) for k,v in \
row.iteritems() if k in ['hum', 'temp', '\
atemp', 'windspeed']}
features.update(numeric_features)
hashed_features = h.transform([features])
# MACHINE LEARNING
if (n+1) >= predictions_start:
# HOLDOUT AFTER N PHASE
predicted = SGD.predict(hashed_features)
val_rmse += (apply_exp(predicted) \
- apply_exp(target))**2
val_rmsle += (predicted - target)**2
if (n-predictions_start+1) % 250 == 0 \
and (n+1) > predictions_start:
print '%s holdout RMSE: %0.3f' \
% (time.strftime('%X'), (val_rmse \
/ float(n-predictions_start+1))**0.5),
print 'holdout RMSLE: %0.3f' % ((val_rmsle \
/ float(n-predictions_start+1))**0.5)
else:
# LEARNING PHASE
SGD.partial_fit(hashed_features, target)
print '%s FINAL holdout RMSE: %0.3f' % \
(time.strftime('%X'), (val_rmse \
/ float(n-predictions_start+1))**0.5)
print '%s FINAL holdout RMSLE: %0.3f' % \
(time.strftime('%X'), (val_rmsle \
/ float(n-predictions_start+1))**0.5)
Out:
18:02:54 holdout RMSE: 281.065 holdout RMSLE: 1.899
18:02:54 holdout RMSE: 254.958 holdout RMSLE: 1.800
18:02:54 holdout RMSE: 255.456 holdout RMSLE: 1.798
18:52:54 holdout RMSE: 254.563 holdout RMSLE: 1.818
18:52:54 holdout RMSE: 239.740 holdout RMSLE: 1.737
18:52:54 FINAL holdout RMSE: 229.274
18:52:54 FINAL holdout RMSLE: 1.678
总结
在这一章中,我们已经看到了如何通过从硬盘上的文本文件或数据库中流式传输数据来实现核心外的学习,无论数据有多大。这些方法当然适用于比我们用来演示它们的例子大得多的数据集(这实际上可以使用非平均的、强大的硬件在内存中解决)。
我们还解释了使核外学习成为可能的核心算法——SGD——并检查了它的优缺点,强调了流必须是真正随机的(这意味着以随机的顺序)才能真正有效,除非顺序是学习目标的一部分。特别是,我们引入了 SGD 的 Scikit-learn 实现,将我们的重点限制在线性和逻辑回归损失函数上。
最后,我们讨论了数据准备,介绍了流的哈希技巧和验证策略,并总结了 SGD 拟合两种不同模型——分类和回归的知识。
在下一章中,我们将通过找出如何在我们的学习模式中启用非线性和支持向量机的铰链损失来继续丰富我们的核心外能力。我们还将展示 Scikit-learn 的替代产品,例如 Liblinear 、 Vowpal Wabbit 和 StreamSVM 。虽然作为外壳命令运行,但所有这些命令都可以很容易地被 Python 脚本包装和控制。
三、快速 SVM 实现
在前一章中已经试验过在线式学习,与批处理学习相比,您可能会对它的简单性、有效性和可扩展性感到惊讶。尽管一次只学习一个示例,但 SGD 可以很好地逼近结果,就好像所有数据都驻留在核心内存中,而您使用的是批处理算法一样。你所需要的是你的流确实是随机的(数据中没有趋势),并且学习者很好地适应了问题(学习率通常是固定的关键参数)。
无论如何,仔细检查这些成就,结果仍然只是可与批量线性模型相比,但不能与更复杂的学习者相比,这些学习者的特征是方差高于偏差,例如支持向量机、神经网络或决策树的打包和增强集成。
对于某些问题,如高而宽但稀疏的数据,根据观察,只有线性组合可能就足够了,因为具有更多数据的简单算法往往胜过在更少数据上训练的更复杂的算法。然而,即使使用线性模型,并通过将现有特征显式映射到更高维度的特征(使用不同顺序的交互、多项式展开和核近似),我们也可以加速和改进响应和特征之间复杂非线性关系的学习。
因此,在这一章中,我们将首先介绍线性支持向量机,作为线性模型的机器学习算法的替代,由不同的方法来解决从数据中学习的问题。然后,我们将演示如何从现有的特征中创建更丰富的特征,以便在面对大规模数据,尤其是高数据(即有许多案例可供学习的数据集)时,以更好的方式解决我们的机器学习任务。
总之,在本章中,我们将涵盖以下主题:
- 介绍支持向量机,并为您提供基本概念和数学公式,以了解它们的工作原理
- 提出具有铰链损失的 SGD 作为大规模任务的可行解决方案,该解决方案使用与批量 SVM 相同的优化方法
- 建议伴随 SGD 的非线性近似
- 提供除了 Scikit-learn 提供的 SGD 算法之外的其他大规模在线解决方案的概述
您自己要实验的数据集
与上一章一样,我们将使用来自 UCI 机器学习资源库的数据集,特别是自行车共享数据集(回归问题)和森林覆盖类型数据(多类分类问题)。
如果您以前没有这样做过,或者如果您需要再次下载这两个数据集,您将需要在数据集中定义的几个函数来亲自尝试真实的东西第 2 章、Scikit 中的可扩展学习-学习。需要的功能有unzip_from_UCI 和gzip_from_UCI。两者都有一个到 UCI 存储库的 Python 连接;下载一个压缩文件,并在 Python 工作目录下解压。如果您从 IPython 单元调用函数,您将在 IPython 查找它们的地方找到必要的新目录和文件。
万一功能对你不起作用,没关系;我们将为您提供直接下载的链接。之后,您所要做的就是将当前工作 Python 目录中的数据解包,您可以通过在您的 Python 接口(IPython 或任何 IDE)上运行以下命令来发现该目录:
In: import os
print "Current directory is: \"%s\"" % (os.getcwd())
Out: Current directory is: "C:\scisoft\WinPython-64bit-2.7.9.4\notebooks\Packt - Large Scale"
单车共享数据集
数据集由 CSV 格式的两个文件组成,包含 2011 年至 2012 年期间美国华盛顿首都自行车共享系统内每小时和每天租赁的自行车数量。提醒一下,这些数据包含租赁当天的相应天气和季节信息。
下面的代码片段将使用方便的unzip_from_UCI包装函数将数据集保存在本地硬盘上:
In: UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00275/Bike-Sharing-Dataset.zip'
unzip_from_UCI(UCI_url, dest='bikesharing')
Out: Extracting in C:\scisoft\WinPython-64bit-2.7.9.4\notebooks\bikesharing
unzipping day.csv
unzipping hour.csv
如果成功运行,代码将指示 CSV 文件保存在哪个目录中,并打印两个解压缩文件的名称。如果不成功,只需从https://archive . ics . UCI . edu/ml/machine-learning-databases/00275/Bike-Sharing-dataset . zip下载该文件,并将两个文件day.csv和hour.csv解压缩到您之前在 Python 工作目录中创建的名为bikesharing的目录中。
cover type 数据集
covertype 数据集由 Jock A. Blackard、Denis J. Dean 博士、Charles W. Anderson 博士和科罗拉多州立大学捐赠,包含 581,012 个示例和一系列 54 个制图变量,范围从海拔到土壤类型,预计能够预测森林覆盖类型,包括 7 种类型(因此这是一个多类问题)。为了确保与相同数据的学术研究的可比性,说明建议使用前 11,340 条记录进行培训,然后使用 3,780 条记录进行验证,最后使用剩余的 565,892 条记录作为测试示例:
In: UCI_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/covtype/covtype.data.gz'
gzip_from_UCI(UCI_url)
如果在运行代码时出现问题,或者您更喜欢自己准备文件,只需前往 UCI 网站,从https://archive . ics . UCI . edu/ml/机器学习-databases/covtype/covtype . data . gz下载数据集,并将其解包到 Python 当前正在处理的目录中。
支持向量机
支持向量机 ( 支持向量机)是一套用于分类和回归(也用于离群点检测)的监督学习技术,由于特殊函数——核函数的可用性,它可以适用于线性和非线性模型,因此非常通用。这种核函数的特点是能够使用有限的计算量将输入特征映射成新的、更复杂的特征向量。核函数非线性地重组原始特征,使得通过非常复杂的函数映射响应成为可能。从这个意义上说,支持向量机作为通用逼近器可以与神经网络相媲美,因此在许多问题中具有相似的预测能力。
与前一章中看到的线性模型相反,支持向量机最初是作为解决分类问题的一种方法,而不是回归问题。
支持向量机是由数学家弗拉基米尔·瓦普尼克和计算机科学家科琳娜·科尔特斯(但也有许多其他贡献者与瓦普尼克在算法上合作)于 90 年代在美国电话电报公司的实验室发明的。本质上,SVM 试图通过在特征空间中找到一个特定的分类超平面来解决分类问题。这种特定的超平面必须被表征为在类的边界之间具有最大的分隔边界的超平面(该边界旨在作为间隙,类本身之间的空间,没有任何示例)。
这种直觉意味着两个结果:
- 经验上,支持向量机试图通过在训练集中找到一个恰好在观察类中间的解来最小化测试误差,因此该解显然是计算性的(它是基于二次规划的优化—https://en.wikipedia.org/wiki/Quadratic_programming)。
- 由于解决方案仅基于相邻示例(称为支持向量)设置的类边界,因此可以忽略其他示例,这使得该技术对异常值不敏感,并且比基于矩阵求逆的方法(如线性模型)占用的内存更少。
给定这样一个算法的总体概述,我们将花费几页指出表征支持向量机的关键公式。尽管对该方法的完整而详细的解释超出了本书的范围,但概述它的工作原理确实有助于弄清楚该技术的幕后发生了什么,并为理解如何将其扩展到大数据提供了基础。
历史上,支持向量机被认为是硬边界分类器,就像感知器一样。事实上,最初设置支持向量机是为了寻找两个分离类的超平面,这两个超平面的倒易距离是最大可能的。这种方法对于线性可分离的合成数据非常有效。无论如何,在硬边界版本中,当 SVM 面对非线性可分离数据时,它只能使用特征的非线性变换来成功。然而,当错误分类错误是由于数据中的噪声而不是非线性时,它们总是被认为是失败的。
因此,通过考虑误差严重程度的成本函数引入了软裕度(因为硬裕度仅在发生误差时才进行跟踪),从而允许对误差不太大的错误分类情况有一定的容限,因为它们位于分离超平面旁边。
自从引入软边界以来,支持向量机也能够承受噪声引起的不可分性。通过围绕一个松弛变量构建成本函数来引入软边际,该变量近似于错误分类示例的数量。这样的松弛变量也被称为经验风险(从训练数据的角度来看,做出错误分类的风险)。
在数学公式中,给定 n 个示例的矩阵数据集 X 和 m 特征以及根据+1(归属)和-1(不归属)表示属于一个类的响应向量,二进制分类 SVM 努力最小化以下成本函数:

在前面的函数中,w 是表示分离超平面和偏差 b 的系数向量,表示与原点的偏移。还有λ(λ``>=0),是正则化参数。
为了更好地理解成本函数是如何工作的,有必要将其分为两部分。第一部分是正则项:

正则化项与向量 w 取高值时的最小化过程形成对比。第二项称为损失项或松弛变量,实际上是 SVM 最小化程序的核心:

损失项输出错误分类误差的近似值。事实上,总和将倾向于为每个分类误差增加一个单位值,其总和除以 n,即示例数,将提供分类误差的大致比例。
通常,如在 Scikit-learn 实现中,λ从正则化项中移除,并由错误分类参数 C 乘以损失项来代替:

前一个参数λ和新的 C 之间的关系如下:

这只是一个惯例问题,因为优化公式中从参数λ到 C 的变化并不意味着不同的结果。
损失项的影响由超参数 C 来调节。C 的高值会对错误施加高惩罚,从而迫使 SVM 尝试对所有训练示例进行正确分类。因此,较大的 C 值往往会迫使余量更紧,并考虑较少的支持向量。裕度的这种减小转化为偏差的增加和方差的减小。
这导致我们具体说明某些观察相对于其他观察的作用;事实上,我们将那些分类错误或分类不可信的例子定义为支持向量,因为它们在边界内(噪声观测使分类分离不可能)。只考虑这样的例子,优化是可能的,这使得 SVM 确实是一种内存高效的技术:

在前面的可视化中,可以注意到两组点(蓝色和白色)在两个特征维度上的投影。C 超参数设置为 1.0 的 SVM 解可以很容易地发现分隔线(在图中表示为连续线),尽管两边都有一些错误分类的情况。此外,由于离分隔线更远的相应类别的支持向量,边界可以被可视化(由两条虚线定义),是可识别的。在图表中,支持向量用一个外圆标记,你实际上可以注意到一些支持向量在边距之外;这是因为它们是错误分类的情况,SVM 必须出于优化目的跟踪它们,因为它们的误差在损失项中考虑:

随着 C 值的增加,由于 SVM 在优化过程中考虑的支持向量越来越少,余量趋于受限。因此,分隔线的斜率也会改变。
相反,较小的 C 值往往会放宽余量,从而增加方差。极小的 C 值甚至会导致 SVM 考虑边距内的所有示例点。当有许多嘈杂的例子时,较小的 C 值是理想的。这样的设置迫使 SVM 忽略了裕度定义中的许多错误分类的例子(误差的权重较小,因此在搜索最大裕度时它们被容忍得更多。):

继续前面的视觉示例,如果我们减少超参数 C,由于支持向量的数量增加,边界实际上会扩大。因此,保证金是不同的,SVM 解决了一个不同的分界线。在对数据进行测试之前,没有可以被认为是正确的 C 值;正确的值总是必须通过交叉验证凭经验找到。到目前为止,C 被认为是 SVM 中最重要的超参数,是在决定使用什么样的核函数后设置的。
相反,核函数通过以非线性方式组合原始特征,将它们映射到更高维的空间中。以这种方式,原始特征空间中明显不可分离的组可以在更高维的表示中变成可分离的。这样的投影不需要太复杂的计算,尽管当投影到高维度时,将原始特征值显式转换成新特征值的过程会产生特征数量的潜在爆炸。内核函数可以简单地插入到决策函数中,从而取代特征和系数向量之间的原始点积,获得与显式映射相同的优化结果,而不是进行如此繁琐的计算。(这样的封堵被称为内核绝招,因为它真的是数学绝招。)
标准核函数是线性函数(意味着没有变换)、多项式函数、径向基函数 ( 径向基函数)和 sigmoid 函数。为了提供一个想法,径向基函数可以表示如下:

基本上,径向基函数和其他内核只是将自己直接插入到之前看到的要最小化的函数的变体中。之前看到的优化函数被称为原始公式,而类似的表达式被称为对偶公式:

尽管在没有数学证明的情况下,从原始公式到对偶公式的转换相当具有挑战性,但重要的是要理解,给定一个通过对例子进行比较的核函数,核技巧只是一个关于它可以展开的无限维特征空间的有限数量计算的问题。这种核心技巧使得该算法对于诸如图像识别或文本分类等相当复杂的问题特别有效(可与神经网络相比):

例如,由于 sigmoid 核,前面的 SVM 解是可能的,而下面的是由于径向基函数核:

从视觉上看,径向基函数内核允许对边界进行非常复杂的定义,甚至将其分成多个部分(在前面的例子中,一个飞地是显而易见的)。
径向基函数核的公式如下:

伽马是一个你可以先验定义的超参数。核变换在支持向量周围创建某种分类气泡,从而允许通过合并气泡本身来定义非常复杂的边界形状。
乙状结肠仁的配方如下:

这里,除了γ,还应该选择 r 以获得最佳结果。
显然,基于 sigmoid、RBF 和多项式的解决方案,(是的,它隐式地进行了多项式展开,我们将在下面的段落中讨论。)内核呈现出比估计偏差更多的方差,因此在决定采用它们时需要严格的验证。尽管 SVM 对过度适应有抵抗力,但它肯定不能幸免。
支持向量回归与支持向量分类有关。它仅因符号(更类似于线性回归,使用β而不是系数向量 w)和损失函数而异:

值得注意的是,唯一显著的区别是损失函数 L-ε,如果示例在距离回归超平面一定距离ε内,则损失函数 L-ε对误差不敏感(因此不计算误差)。这种成本函数的最小化优化了回归问题的结果,输出值而不是类。
铰链损失及其变体
作为对 SVM 内部螺母和螺栓的总结,请记住算法核心的成本函数是铰链损耗:

如前所述, ŷ 表示为 X 和系数向量 w 的点积与偏差 b 之和:

这让人想起感知器,这样的损失函数线性地惩罚误差,表示当例子被分类在边缘的错误侧时的误差,与它与边缘本身的距离成比例。虽然是凸的,但缺点是不能处处可微,它有时被总是可微的变量所代替,例如平方铰链损耗(也称为 L2 损耗,而 L1 损耗是铰链损耗):

另一个变型是休伯损失,当误差等于或低于某个阈值 h 时,它是二次函数,否则是线性函数。这种方法混合了基于误差的铰链损失的 L1 和 L2 变体,并且它是一种非常抗异常值的替代方案,因为较大的误差值不平方,因此需要学习 SVM 较少的调整。Huber 损失也是对数损失(线性模型)的一种替代方法,因为它计算速度更快,并且能够提供类别概率的估计(铰链损失不具有这种能力)。
从实践的角度来看,没有特别的报告表明胡贝尔损失或 L2 铰链损失可以始终比铰链损失表现得更好。最后,成本函数的选择可以归结为针对每个不同的学习问题测试可用的函数。(根据无免费午餐定理的原理,在机器学习中没有适合所有问题的解。)
了解 Scikit-学习 SVM 实施
Scikit-learn 提供了 SVM 的一个实现使用两个 C++库(带有一个 C API 来与其他语言接口),一个支持向量机库 ( LIBSVM )用于 SVM 分类和回归(http://www.csie.ntu.edu.tw/~cjlin/libsvm/)和 LIBLINEAR 用于在大型稀疏数据集上使用线性方法分类问题(http://www.csie.ntu.edu.tw/~cjlin/liblinear/)。这两个库都可以自由使用,计算速度非常快,并且已经在许多其他解决方案中进行了测试,sklearn.svm模块中的所有 Scikit-learn 实现都依赖于其中的一个,Perceptron和LogisticRegression类也顺便使用了它们。)让 Python 只是一个方便的包装器。
另一方面,SGDClassifier和SGDRegressor使用不同的实现,因为 LIBSVM 和 LIBLINEAR 都没有在线实现,都是批处理学习工具。事实上,在操作时,当通过cache_size参数为内核操作分配合适的内存时,LIBSVM 和 LIBLINEAR 的性能都是最好的。
分类的实现如下:
|班级
|
目的
|
超参数
|
| --- | --- | --- |
| sklearn.svm.SVC | 二类和多类线性和核分类的 LIBSVM 实现 | c,核,度,γ |
| sklearn.svm.NuSVC | 同上 | 核,度,γ |
| sklearn.svm.OneClassSVM | 异常值的无监督检测 | 核,度,γ |
| sklearn.svm.LinearSVC | 它基于 LIBLINEAR,是一个二元多类线性分类器 | 罚金,损失,C |
关于回归,解决方案如下:
|班级
|
目的
|
超参数
|
| --- | --- | --- |
| sklearn.svm.SVR | 回归的 LIBSVM 实现 | c,核,度,γ,ε |
| sklearn.svm.NuSVR | 同上 | 核,度,γ |
如您所见,每个版本都有相当多的超参数需要调整,使用 Scikit-learn 中grid_search模块的GridSearchCV,当使用默认参数时,SVMs 是很好的学习者,当通过交叉验证进行适当调整时,SVMs 是很好的学习者。
作为一条黄金法则,有些参数对结果的影响更大,因此应事先确定,其他参数则取决于它们的值。根据这样的经验法则,你必须正确设置以下参数(按重要程度排序):
C:这个就是我们之前讨论过的罚值。减小它会使余量变大,从而忽略更多的噪声,但也会增加计算量。最佳值通常可以在np.logspace(-3, 3, 7)范围内找到。kernel:这是非线性老黄牛,因为一个 SVM 可以设置为linear、poly、rbf、sigmoid,或者自定义内核(针对专家!).广泛使用的当然是rbf。degree:这个和kernel='poly'一起工作,表示多项式展开的维度。它被其他内核忽略。通常,2-5 的值效果最好。gamma:这是'rbf'、'poly'、'sigmoid'的系数;较高的值倾向于以更好的方式拟合数据。建议的网格搜索范围是np.logspace(-3, 3, 7)。nu:这是用nuSVR``nuSVC进行回归分类;此参数近似于未按置信度分类的训练点、错误分类的点以及边缘内部或边缘上的正确点。它应该是一个在[0,1]范围内的数字,因为它是相对于您的训练集的一个比例。最后,它作为高比例的 C 扩大了利润。epsilon:这个参数通过定义一个ε大范围来指定一个支持向量回归机将接受多少误差,在这个范围内,相对于点的真实值没有损失。建议搜索范围为np.insert(np.logspace(-4, 2, 7),0,[0])。penalty、loss和dual:对于线性 SVC,这些参数接受('l1','squared_hinge',False)、('l2','hinge',True)、('l2','squared_hinge',True)和('l2','squared_hinge',False)组合。('l2','hinge',True)组合相当于SVC(kernel='linear')学习者。
作为使用来自 Scikit-learn 的sklearn.svm模块的 SVC 和 SVR 进行基本分类和回归的示例,我们将使用 Iris 和 Boston 数据集,这是两个流行的玩具数据集(http://scikit-learn.org/stable/datasets/)。
首先,我们将加载 Iris 数据集:
In: from sklearn import datasets
iris = datasets.load_iris()
X_i, y_i = iris.data, iris.target
然后,我们将用径向基函数核拟合一个支持向量机(根据 Scikit-learn 中的其他已知示例选择了 C 和γ),并使用cross_val_score函数测试结果:
from sklearn.svm import SVC
from sklearn.cross_validation import cross_val_score
import numpy as np
h_class = SVC(kernel='rbf', C=1.0, gamma=0.7, random_state=101)
scores = cross_val_score(h_class, X_i, y_i, cv=20, scoring='accuracy')
print 'Accuracy: %0.3f' % np.mean(scores)
Output: Accuracy: 0.969
拟合模型可以为您提供一个索引,指出您的训练示例中有哪些支持向量:
In: h_class.fit(X_i,y_i)
print h_class.support_
Out: [ 13 14 15 22 24 41 44 50 52 56 60 62 63 66 68 70 72 76 77 83 84 85 98 100 106 110 114 117 118 119 121 123 126 127 129 131 133 134 138 141 146 149]
以下是 SVC 为 Iris 数据集选择的支持向量的图形表示,用颜色决策边界表示(我们测试了离散的值网格,以便能够为图表的每个区域投影模型将预测的类别):

型式
如果你对复制相同的图表感兴趣,你可以看看并调整这个来自的代码片段。
为了测试一个 SVM 回归器,我们决定用波士顿数据集尝试 SVR 。首先,我们将数据集上传到核心内存中,然后我们将示例的顺序随机化,值得注意的是,这样的数据集实际上是以一种微妙的方式排序的,从而使非顺序随机化交叉验证的结果无效:
In: import numpy as np
from sklearn.datasets import load_boston
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
boston = load_boston()
shuffled = np.random.permutation(boston.target.size)
X_b = scaler.fit_transform(boston.data[shuffled,:])
y_b = boston.target[shuffled]
型式
由于我们在 NumPy 包中使用了random模块中的permutation函数,因此您可能会从以下测试中获得不同的混洗数据集和稍微不同的交叉验证分数。此外,具有不同尺度的特征,将特征标准化是一种好的做法,这样它们将具有零中心均值和单位方差。尤其是在使用内核的 SVM 时,标准化确实至关重要。
最后,我们可以拟合 SVR 模型(我们决定了一些已知有效的 C、γ和ε参数),并使用交叉验证,通过均方根误差对其进行评估:
In: from sklearn.svm import SVR
from sklearn.cross_validation import cross_val_score
h_regr = SVR(kernel='rbf', C=20.0, gamma=0.001, epsilon=1.0)
scores = cross_val_score(h_regr, X_b, y_b, cv=20, scoring='mean_squared_error')
print 'Mean Squared Error: %0.3f' % abs(np.mean(scores))
Out: Mean Squared Error: 28.087
通过二次采样追求非线性支持向量机
与其他机器学习算法相比,支持向量机有很多优势:
- 它们可以处理大多数有监督的问题,如回归、分类和异常检测,尽管它们实际上最擅长二进制分类。
- 它们可以很好地处理有噪声的数据和异常值,并且由于它们只处理支持向量,因此它们倾向于不太过度。
- 它们适用于广泛的数据集(特征比示例多);不过,和其他机器学习算法一样,SVM 可以从降维和特征选择中获益。
作为缺点,我们必须提到以下几点:
- 它们只提供估计值,但不提供概率,除非您通过普拉特缩放进行一些耗时且计算密集型的概率校准
- 它们随着例子的数量呈超线性扩展
特别是,最后一个缺点极大地限制了支持向量机在大型数据集上的使用。该学习技术核心的优化算法——二次规划——在 Scikit-learn 实现中在 O(特征数量 samples^2 数量)和 O(特征数量 samples^3 数量)之间缩放,这种复杂性严重限制了该算法对于小于10^4例数的数据集的可操作性。
同样,正如在上一章中所看到的,当您给出一个批处理算法和太多数据时,只有几个选项:二次采样、并行化和通过流的核外学习。子采样和并行化很少被引用为最佳解决方案,流是实现具有大规模问题的支持向量机的首选方法。
然而,尽管使用较少,但利用储层采样很容易实现二次采样,这可以从来自数据集和无限在线流的流中快速产生随机样本。通过二次采样,您可以生成多个 SVM 模型,这些模型的结果可以进行平均以获得更好的结果。来自多个 SVM 模型的预测甚至可以叠加,从而创建一个新的数据集,并用于构建融合所有预测能力的新模型,这将在第 6 章、分级和回归树中描述。
储层取样是一种算法,用于在事先不知道流有多长的情况下,从流中随机选择样本。事实上,流中的每个观察都有相同的被选择概率。使用从流中的第一个观察值获取的样本进行初始化,样本中的每个元素都可以根据与到目前为止流过的元素数量成比例的概率随时被流中的示例替换。因此,例如,当流的第I元素到达时,它有可能被插入来代替样本中的随机元素。这样的插入概率相当于样本维数除以I;因此,它相对于流长度逐渐减小。如果流是无限的,则随时停止可确保样本代表到目前为止看到的元素。
在我们的例子中,我们从溪流中随机抽取两个互斥的样本——一个用于训练,一个用于测试。我们将使用原始的有序文件从 Covertype 数据库中提取这样的样本。(由于我们将在取样前对所有数据进行流式处理,随机取样不会受到排序的影响。)我们确定了一个 5000 个示例的训练样本,这个数字应该可以在大多数台式计算机上很好地扩展。至于测试集,我们将使用 20,000 个示例:
In: from random import seed, randint
SAMPLE_COUNT = 5000
TEST_COUNT = 20000
seed(0) # allows repeatable results
sample = list()
test_sample = list()
for index, line in enumerate(open('covtype.data','rb')):
if index < SAMPLE_COUNT:
sample.append(line)
else:
r = randint(0, index)
if r < SAMPLE_COUNT:
sample[r] = line
else:
k = randint(0, index)
if k < TEST_COUNT:
if len(test_sample) < TEST_COUNT:
test_sample.append(line)
else:
test_sample[k] = line
算法应该在数据矩阵的超500,000行上以相当快的速度流动。事实上,我们在流式传输过程中确实没有进行任何预处理,以尽可能保持最快的速度。因此,我们现在需要将数据转换为 NumPy 阵列,并对其功能进行标准化:
In: import numpy as np
from sklearn.preprocessing import StandardScaler
for n,line in enumerate(sample):
sample[n] = map(float,line.strip().split(','))
y = np.array(sample)[:,-1]
scaling = StandardScaler()
X = scaling.fit_transform(np.array(sample)[:,:-1])
一旦完成了训练数据 X 、 y ,我们就要以同样的方式处理测试数据;特别是,我们必须使用标准化参数(平均值和标准偏差)对特征进行标准化,如训练样本中所示:
In: for n,line in enumerate(test_sample):
test_sample[n] = map(float,line.strip().split(','))
yt = np.array(test_sample)[:,-1]
Xt = scaling.transform(np.array(test_sample)[:,:-1])
当训练集和测试集都准备好时,我们可以拟合 SVC 模型和预测结果:
In: from sklearn.svm import SVC
h = SVC(kernel='rbf', C=250.0, gamma=0.0025, random_state=101)
h.fit(X,y)
prediction = h.predict(Xt)
from sklearn.metrics import accuracy_score
print accuracy_score(yt, prediction)
Out: 0.75205
用 SGD 大规模实现 SVM
考虑到子采样的限制(首先,相对于在较大数据集上训练的模型的欠拟合),在对应用于大规模流的线性支持向量机使用 Scikit-learn 时唯一可用的选项仍然是SGDClassifier和SGDRegressor方法,这两种方法都在linear_model模块中可用。让我们看看如何最好地使用它们,并在示例数据集上改进我们的结果。
我们将利用本章中看到的前面的例子进行线性和逻辑回归,并将它们转化为有效的 SVM。至于分类,要求你使用loss超参数设置损失类型。参数的可能值为'hinge'、'squared_hinge'和'modified_huber'。所有这样的损失函数都在前面介绍过,并在本章讨论 SVM 公式时讨论过。
所有这些都意味着应用软裕度线性 SVM(无核),从而导致 SVM 抗误分类和噪声数据。但是,您也可以尝试使用损失'perceptron',这是一种导致没有余量的铰链损失的损失类型,当有必要求助于比其他可能的损失选择更偏向的模型时,这是一种合适的解决方案。
当使用这种范围的铰链损耗函数时,要获得最佳结果,必须考虑两个方面:
- 当使用任何损失函数时,随机梯度下降变得懒惰,仅当一个例子违反先前定义的边界时更新系数向量。这与对数或平方误差中的损失函数完全相反,实际上每个例子都被考虑用于系数向量的更新。如果学习中涉及许多特征,这种懒惰的方法会导致系数向量更稀疏,从而减少过拟合。(更密集的向量意味着更多的过拟合,因为一些系数可能比来自数据的信号捕获更多的噪声。)
- 只有
'modified_huber'损失允许概率估计,使其成为对数损失的可行替代(如在随机逻辑回归中发现的)。当处理多类1 对全部 ( OVA )预测时,改进的 Huber 也表现得更好,因为多个模型的概率输出优于铰链损失的标准决策函数特性(概率比决策函数的原始输出更好,因为它们在相同的尺度上,从 0 到 1 有界)。这个损失函数的工作原理是直接从决策函数中导出一个概率估计值:(clip(decision_function(X), -1, 1) + 1) / 2。
至于回归问题,SGDRegressor提供两个 SVM 损失选项:
'epsilon_insensitive'
'squared_epsilon_insensitive'
两者都激活线性支持向量回归,其中ε值内的误差(预测的残差)被忽略。超过ε值后,epsilon_insensitive损失按原样考虑误差。squared_epsilon_insensitive损失以类似的方式运行,尽管这里的误差越平方越不利,更大的误差对模型构建的影响越大。
在这两种情况下,设置正确的ε超参数至关重要。作为一个默认值,Scikit-learn 建议ε= 0.1,但是您的问题的最佳值必须通过交叉验证支持的网格搜索来找到,我们将在接下来的段落中看到。
请注意,在回归损失中,还有一个'huber'损失没有激活 SVM 类型的优化,只是修改了通常的'squared_loss',通过从平方损失切换到线性损失超过ε参数值的距离,对异常值不敏感。
至于我们的例子,我们将重复流过程一定次数,以演示如何设置不同的超参数和变换特征;为了减少重复代码行的数量,我们将使用一些方便的函数。此外,为了加快示例的执行速度,我们将限制算法引用的案例数或容差值。通过这种方式,培训和验证时间都保持在最低限度,没有任何示例会要求您等待超过一杯茶或咖啡的时间。
至于方便的包装函数,第一个函数的目的是最初将部分或全部数据流式传输一次(我们使用max_rows 参数设置一个限制)。完成流式传输后,该功能将能够计算出所有分类特征的级别,并记录数字特征的不同范围。提醒一下,录音范围是需要注意的一个重要方面。SGD 和 SVM 都是对不同范围尺度敏感的算法,当处理[-1,1]范围之外的数字时,它们的性能更差。
作为输出,我们的函数将返回两个经过训练的 Scikit-learn 对象:DictVectorizer(能够将字典中存在的特征范围转换为特征向量)和MinMaxScaler来重新缩放[0,1]范围内的数值变量(有助于保持数据集中的值稀疏,从而保持低内存使用,并在大多数值为零时实现快速计算)。作为一个独特的约束,您需要知道要用于预测模型的数值和分类变量的特征名称。未包含在列表的binary_features或numeric_features参数中的特征实际上将被忽略。当流没有要素名称时,您需要使用fieldnames参数命名它们:
In: import csv, time, os
import numpy as np
from sklearn.linear_model import SGDRegressor
from sklearn.feature_extraction import DictVectorizer
from sklearn.preprocessing import MinMaxScaler
from scipy.sparse import csr_matrix
def explore(target_file, separator=',', fieldnames= None, binary_features=list(), numeric_features=list(), max_rows=20000):
"""
Generate from an online style stream a DictVectorizer and a MinMaxScaler.
Parameters
----------
target file = the file to stream from
separator = the field separator character
fieldnames = the fields' labels (can be omitted and read from file)
binary_features = the list of qualitative features to consider
numeric_features = the list of numeric futures to consider
max_rows = the number of rows to be read from the stream (can be None)
"""
features = dict()
min_max = dict()
vectorizer = DictVectorizer(sparse=False)
scaler = MinMaxScaler()
with open(target_file, 'rb') as R:
iterator = csv.DictReader(R, fieldnames, delimiter=separator)
for n, row in enumerate(iterator):
# DATA EXPLORATION
for k,v in row.iteritems():
if k in binary_features:
if k+'_'+v not in features:
features[k+'_'+v]=0
elif k in numeric_features:
v = float(v)
if k not in features:
features[k]=0
min_max[k] = [v,v]
else:
if v < min_max[k][0]:
min_max[k][0]= v
elif v > min_max[k][1]:
min_max[k][1]= v
else:
pass # ignore the feature
if max_rows and n > max_rows:
break
vectorizer.fit([features])
A = vectorizer.transform([{f:0 if f not in min_max else min_max[f][0] for f in vectorizer.feature_names_},
{f:1 if f not in min_max else min_max[f][1] for f in vectorizer.feature_names_}])
scaler.fit(A)
return vectorizer, scaler
型式
这个代码片段可以很容易地在你自己的大规模数据机器学习应用中重用。如果您的流是在线流(连续流)或过长的流,您可以通过设置max_rows参数对观察到的示例数量应用不同的限制。
第二个函数将从数据流中提取数据并将其转换为特征向量,如果提供了合适的MinMaxScaler对象而不是None设置,则对数字特征进行归一化:
In: def pull_examples(target_file, vectorizer, binary_features, numeric_features, target, min_max=None, separator=',',
fieldnames=None, sparse=True):
"""
Reads a online style stream and returns a generator of normalized feature vectors
Parameters
----------
target file = the file to stream from
vectorizer = a DictVectorizer object
binary_features = the list of qualitative features to consider
numeric_features = the list of numeric features to consider
target = the label of the response variable
min_max = a MinMaxScaler object, can be omitted leaving None
separator = the field separator character
fieldnames = the fields' labels (can be omitted and read from file)
sparse = if a sparse vector is to be returned from the generator
"""
with open(target_file, 'rb') as R:
iterator = csv.DictReader(R, fieldnames, delimiter=separator)
for n, row in enumerate(iterator):
# DATA PROCESSING
stream_row = {}
response = np.array([float(row[target])])
for k,v in row.iteritems():
if k in binary_features:
stream_row[k+'_'+v]=1.0
else:
if k in numeric_features:
stream_row[k]=float(v)
if min_max:
features = min_max.transform(vectorizer.transform([stream_row]))
else:
features = vectorizer.transform([stream_row])
if sparse:
yield(csr_matrix(features), response, n)
else:
yield(features, response, n)
给定这两个函数,现在让我们再次尝试对前一章中看到的第一个回归问题(自行车共享数据集)建模,但这次使用的是铰链损失,而不是我们之前使用的均方误差。
作为的第一步,我们提供要流的文件的名称和一个定性和数字变量列表(从文件的头和文件的初始探索中导出)。包装函数的代码将返回一些关于热编码变量和值范围的信息。在这种情况下,大多数变量将是二进制的,这对于稀疏表示来说是一种完美的情况,因为我们数据集中的大多数值都是零:
In: source = '\\bikesharing\\hour.csv'
local_path = os.getcwd()
b_vars = ['holiday','hr','mnth', 'season','weathersit','weekday','workingday','yr']
n_vars = ['hum', 'temp', 'atemp', 'windspeed']
std_row, min_max = explore(target_file=local_path+'\\'+source, binary_features=b_vars, numeric_features=n_vars)
print 'Features: '
for f,mv,mx in zip(std_row.feature_names_, min_max.data_min_, min_max.data_max_):
print '%s:[%0.2f,%0.2f] ' % (f,mv,mx)
Out:
Features:
atemp:[0.00,1.00]
holiday_0:[0.00,1.00]
holiday_1:[0.00,1.00]
...
workingday_1:[0.00,1.00]
yr_0:[0.00,1.00]
yr_1:[0.00,1.00]
您可以从输出中注意到,定性变量已经使用它们的变量名进行了编码,并在下划线字符后添加了它们的值,并转换为二进制特征(当特征存在时,该特征的值为 1,否则设置为零)。请注意,我们总是在中使用带有average=True参数的 SGD 模型,以确保更快的收敛(这对应于使用 平均随机梯度下降 ( ASGD )模型,如前一章所述。):
In:from sklearn.linear_model import SGDRegressor
SGD = SGDRegressor(loss='epsilon_insensitive', epsilon=0.001, penalty=None, random_state=1, average=True)
val_rmse = 0
val_rmsle = 0
predictions_start = 16000
def apply_log(x): return np.log(x + 1.0)
def apply_exp(x): return np.exp(x) - 1.0
for x,y,n in pull_examples(target_file=local_path+'\\'+source,
vectorizer=std_row, min_max=min_max,
binary_features=b_vars, numeric_features=n_vars, target='cnt'):
y_log = apply_log(y)
# MACHINE LEARNING
if (n+1) >= predictions_start:
# HOLDOUT AFTER N PHASE
predicted = SGD.predict(x)
val_rmse += (apply_exp(predicted) - y)**2
val_rmsle += (predicted - y_log)**2
if (n-predictions_start+1) % 250 == 0 and (n+1) > predictions_start:
print n,
print '%s holdout RMSE: %0.3f' % (time.strftime('%X'), (val_rmse / float(n-predictions_start+1))**0.5),
print 'holdout RMSLE: %0.3f' % ((val_rmsle / float(n-predictions_start+1))**0.5)
else:
# LEARNING PHASE
SGD.partial_fit(x, y_log)
print '%s FINAL holdout RMSE: %0.3f' % (time.strftime('%X'), (val_rmse / float(n-predictions_start+1))**0.5)
print '%s FINAL holdout RMSLE: %0.3f' % (time.strftime('%X'), (val_rmsle / float(n-predictions_start+1))**0.5)
Out:
16249 07:49:09 holdout RMSE: 276.768 holdout RMSLE: 1.801
16499 07:49:09 holdout RMSE: 250.549 holdout RMSLE: 1.709
16749 07:49:09 holdout RMSE: 250.720 holdout RMSLE: 1.696
16999 07:49:09 holdout RMSE: 249.661 holdout RMSLE: 1.705
17249 07:49:09 holdout RMSE: 234.958 holdout RMSLE: 1.642
07:49:09 FINAL holdout RMSE: 224.513
07:49:09 FINAL holdout RMSLE: 1.596
我们现在来尝试一下森林覆盖类型的分类问题:
In: source = 'shuffled_covtype.data'
local_path = os.getcwd()
n_vars = ['var_'+'0'*int(j<10)+str(j) for j in range(54)]
std_row, min_max = explore(target_file=local_path+'\\'+source, binary_features=list(),
fieldnames= n_vars+['covertype'], numeric_features=n_vars, max_rows=50000)
print 'Features: '
for f,mv,mx in zip(std_row.feature_names_, min_max.data_min_, min_max.data_max_):
print '%s:[%0.2f,%0.2f] ' % (f,mv,mx)
Out:
Features:
var_00:[1871.00,3853.00]
var_01:[0.00,360.00]
var_02:[0.00,61.00]
var_03:[0.00,1397.00]
var_04:[-164.00,588.00]
var_05:[0.00,7116.00]
var_06:[58.00,254.00]
var_07:[0.00,254.00]
var_08:[0.00,254.00]
var_09:[0.00,7168.00]
...
在从流中采样并拟合我们的DictVectorizer和MinMaxScaler对象之后,我们这次可以使用渐进验证来开始我们的学习过程(在案例用于训练之前,通过在案例上测试模型来给出误差度量),给定大量可用的示例。代码中的sample变量设置的每一个特定数量的例子,脚本都以平均精度报告最近例子的情况:
In: from sklearn.linear_model import SGDClassifier
SGD = SGDClassifier(loss='hinge', penalty=None, random_state=1, average=True)
accuracy = 0
accuracy_record = list()
predictions_start = 50
sample = 5000
early_stop = 50000
for x,y,n in pull_examples(target_file=local_path+'\\'+source,
vectorizer=std_row,
min_max=min_max,
binary_features=list(), numeric_features=n_vars,
fieldnames= n_vars+['covertype'], target='covertype'):
# LEARNING PHASE
if n > predictions_start:
accuracy += int(int(SGD.predict(x))==y[0])
if n % sample == 0:
accuracy_record.append(accuracy / float(sample))
print '%s Progressive accuracy at example %i: %0.3f' % (time.strftime('%X'), n, np.mean(accuracy_record[-sample:]))
accuracy = 0
if early_stop and n >= early_stop:
break
SGD.partial_fit(x, y, classes=range(1,8))
Out: ...
19:23:49 Progressive accuracy at example 50000: 0.699
型式
不得不处理超过 575,000 个例子,我们在 50,000 个之后设置学习过程的提前停止。您可以根据您电脑的功率和时间可用性自由修改这些参数。请注意,代码可能需要一些时间。我们在 2.20 千兆赫的英特尔酷睿 i3 处理器上进行了大约 30 分钟的计算。
正则化特征选择
在批处理环境中,通常通过以下方式操作特征选择:
- 基于完整性(缺失值的发生率)、方差和变量间高度多重共线性的初步筛选,以获得相关和可操作特征的更清晰数据集。
- 基于特征和响应变量之间的单变量关联(卡方检验、F 值和简单线性回归)的另一个初始过滤,以便立即移除对预测任务没有用处的特征,因为它们与响应很少或没有关系。
- 在建模过程中,递归方法根据特征的能力插入和/或排除特征,以提高算法的预测能力,如在保持样本上测试的那样。使用仅相关特征的较小子集允许机器学习算法较少受过度拟合的影响,因为噪声变量和参数由于特征的高维性而过量。
在在线环境中应用这种方法当然仍然是可能的,但是就所需的时间而言是相当昂贵的,因为完成单个模型需要流传输大量的数据。基于大量迭代和测试的递归方法需要能够适应内存的灵活数据集。如前所述,在这种情况下,二次抽样将是一个很好的选择,以便找出以后应用于更大规模的特征和模型。
继续我们的核心外方法,正则化是理想的解决方案,作为一种选择变量的方式,同时流式传输和过滤掉有噪声或冗余的特征。正则化在在线算法中运行良好,因为在线机器学习算法正在运行并根据示例拟合其系数,而不需要为了选择而运行其他流。正则化实际上只是一个罚值,加入到学习过程的优化中。它依赖于特征系数和一个名为alpha的参数设置正则化的影响。当模型更新系数的权重时,正则化平衡介入。这时,如果更新的值不够大,正则化通过减少结果权重来起作用。排除或衰减冗余变量的技巧是通过正则化alpha参数实现的,该参数必须根据经验设置在正确的大小,以获得关于要学习的每个特定数据的最佳结果。
SGD 实现了与批处理算法中相同的正则化策略:
- L1 罚将多余和不那么重要的变量推到零
- L2 减少了不太重要的功能的重量
- L1 和 L2 正则化效应的弹性网混合
当存在异常和冗余变量时,L1 正则化是完美的策略,因为它会将这些特征的系数推到零,使它们在计算预测时变得无关紧要。
当变量之间存在许多相关性时,L2 是合适的,因为它的策略只是降低特征的权重,这些特征的变化对于损失函数最小化来说不太重要。对于 L2 来说,所有的变量都在继续对预测做出贡献,尽管有些变量不那么重要。
弹性网使用加权和将 L1 和 L2 混合在一起。这个解决方案很有趣,因为当处理高度相关的变量时,L1 正则化有时是不稳定的,根据所看到的例子选择其中一个。使用ElasticNet,许多不寻常的特征仍然会像在 L1 正则化中一样被推到零,但是相关的特征会像在 L2 一样被衰减。
SGDClassifier和SGDRegressor都可以使用penalty、alpha和l1_ratio参数实现 L1、L2 和弹性网正则化。
型式
阿尔法参数是决定什么样的惩罚或两者混合后最关键的参数。理想情况下,您可以使用10.0**-np.arange(1,7)生成的值列表测试从0.1到10^-7范围内的合适值。
如果penalty决定选择哪种正则化,alpha如上所述,将决定其强度。因为alpha是乘以惩罚项的常数;低α值对最终系数的影响很小,而高α值会显著影响最终系数。最后,l1_ratio代表当penalty='elasticnet'时,L1 处罚相对于 L2 的百分比是多少。
用 SGD 设置正则化非常容易。例如,您可以尝试更改前面的代码示例,在SGDClassifier中插入一个惩罚 L2:
SGD = SGDClassifier(loss='hinge', penalty='l2', alpha= 0.0001, random_state=1, average=True)
如果你更喜欢测试一个混合了两种正则化方法效果的弹性网,你所要做的就是通过设置l1_ratio来明确 L1 和 L2 之间的比率:
SGD = SGDClassifier(loss=''hinge'', penalty=''elasticnet'', \ alpha= 0.001, l1_ratio=0.5, random_state=1, average=True)
由于正则化的成功取决于插入正确的惩罚和最佳α,所以在我们的例子中,正则化将在处理超参数优化问题时发挥作用。
在 SGD 中包含非线性
将非线性插入线性 SGD 学习器(基本上是一个不用动脑的学习器)的最快方式是将从流中接收到的示例向量转换成新向量,该新向量包括幂变换和一定程度的特征组合。
组合可以表示特征之间的相互作用(说明当两个特征同时对响应有特殊影响时),因此有助于 SVM 线性模型包含一定量的非线性。例如,双向交互是通过两个特征的相乘来实现的。三向是通过将三个特征相乘等方式实现的,为更高程度的扩展创造了更复杂的交互。
在 Scikit-learn 中,预处理模块包含PolynomialFeatures类,可以通过所需次数的多项式展开自动变换特征向量:
In: from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import PolynomialFeatures
source = '\\bikesharing\\hour.csv'
local_path = os.getcwd()
b_vars = ['holiday','hr','mnth', 'season','weathersit','weekday','workingday','yr']
n_vars = ['hum', 'temp', 'atemp', 'windspeed']
std_row, min_max = explore(target_file=local_path+'\\'+source, binary_features=b_vars, numeric_features=n_vars)
poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False)
SGD = SGDRegressor(loss='epsilon_insensitive', epsilon=0.001, penalty=None, random_state=1, average=True)
val_rmse = 0
val_rmsle = 0
predictions_start = 16000
def apply_log(x): return np.log(x + 1.0)
def apply_exp(x): return np.exp(x) - 1.0
for x,y,n in pull_examples(target_file=local_path+'\\'\
+source,vectorizer=std_row, min_max=min_max, \
sparse = False, binary_features=b_vars,\numeric_features=n_vars, target='cnt'):
y_log = apply_log(y)
# Extract only quantitative features and expand them
num_index = [j for j, i in enumerate(std_row.feature_names_) if i in n_vars]
x_poly = poly.fit_transform(x[:,num_index])[:,len(num_index):]
new_x = np.concatenate((x, x_poly), axis=1)
# MACHINE LEARNING
if (n+1) >= predictions_start:
# HOLDOUT AFTER N PHASE
predicted = SGD.predict(new_x)
val_rmse += (apply_exp(predicted) - y)**2
val_rmsle += (predicted - y_log)**2
if (n-predictions_start+1) % 250 == 0 and (n+1) > predictions_start:
print n,
print '%s holdout RMSE: %0.3f' % (time.strftime('%X'), (val_rmse / float(n-predictions_start+1))**0.5),
print 'holdout RMSLE: %0.3f' % ((val_rmsle / float(n-predictions_start+1))**0.5)
else:
# LEARNING PHASE
SGD.partial_fit(new_x, y_log)
print '%s FINAL holdout RMSE: %0.3f' % (time.strftime('%X'), (val_rmse / float(n-predictions_start+1))**0.5)
print '%s FINAL holdout RMSLE: %0.3f' % (time.strftime('%X'), (val_rmsle / float(n-predictions_start+1))**0.5)
Out: ...
21:49:24 FINAL holdout RMSE: 219.191
21:49:24 FINAL holdout RMSLE: 1.480
型式
PolynomialFeatures期望一个密集的矩阵,而不是一个稀疏的矩阵作为输入。我们的pull_examples功能允许设置稀疏参数,通常设置为True,可以改为设置为False,从而返回密集矩阵。
尝试显式高维映射
虽然多项式展开是一种非常强大的变换,但是当我们试图将展开到更高的程度,并快速对比由过度参数化(当您有太多冗余和无用的特征时)导致的过度拟合捕捉重要非线性的积极效果时,它们在计算上可能会很昂贵。正如在 SVC 和 SVR 中看到的,内核转换可以帮助我们。SVM 内核转换是隐式的,需要内存中的数据矩阵才能工作。Scikit-learn 中有一类基于随机近似的变换,在线性模型的上下文中,可以获得与核 SVM 非常相似的结果。
sklearn.kernel_approximation模块包含一些这样的算法:
RBFSampler:这近似于径向基函数核的特征图Nystroem:这使用训练数据的子集来近似内核映射AdditiveChi2Sampler:这近似于加法 chi2 内核的特征映射,一个用于计算机视觉的内核SkewedChi2Sampler:这近似于特征映射,类似于计算机视觉中使用的偏斜卡方核
除了 Nystroem 方法,前面的类都不需要从你的数据样本中学习,这使得它们非常适合在线学习。他们只需要知道一个示例向量是如何形成的(有多少特征),然后他们将产生许多随机的非线性,希望这些非线性能够很好地适合您的数据问题。
这些近似算法中没有复杂的优化算法可以解释;事实上,优化本身被随机化所取代,结果很大程度上取决于输出特征的数量,由n_components参数指出。输出特征越多,你就越有可能得到正确的非线性来完美地解决你的问题。
重要的是要注意,如果机会在创建正确的特征以提高预测方面真的有如此大的作用,那么结果的可再现性就变得至关重要,你应该努力获得它,否则你将无法以同样的方式持续地重新训练和调整你的算法。值得注意的是,每个类都有一个random_state参数,这样就可以控制随机特征的生成,并且以后可以在相同的计算机上重新生成。
科学文章 A .拉希米和本杰明·雷希特(http://www . eecs . Berkeley . edu/~ Brecht/papers/07 . rah . rec . nips . pdf)和随机厨房水槽的加权和:在学习中用随机化代替最小化中解释了这种特征创建技术的理论基础
就我们的目的而言,只要知道如何实施该技术并使其有助于改进我们的线性和基于 SVM 的 SGD 模型就足够了:
In: source = 'shuffled_covtype.data'
local_path = os.getcwd()
n_vars = ['var_'+str(j) for j in range(54)]
std_row, min_max = explore(target_file=local_path+'\\'+source, binary_features=list(),
fieldnames= n_vars+['covertype'], numeric_features=n_vars, max_rows=50000)
from sklearn.linear_model import SGDClassifier
from sklearn.kernel_approximation import RBFSampler
SGD = SGDClassifier(loss='hinge', penalty=None, random_state=1, average=True)
rbf_feature = RBFSampler(gamma=0.5, n_components=300, random_state=0)
accuracy = 0
accuracy_record = list()
predictions_start = 50
sample = 5000
early_stop = 50000
for x,y,n in pull_examples(target_file=local_path+'\\'+source,
vectorizer=std_row,
min_max=min_max,
binary_features=list(),
numeric_features=n_vars,
fieldnames= n_vars+['covertype'], target='covertype', sparse=False):
rbf_x = rbf_feature.fit_transform(x)
# LEARNING PHASE
if n > predictions_start:
accuracy += int(int(SGD.predict(rbf_x))==y[0])
if n % sample == 0:
accuracy_record.append(accuracy / float(sample))
print '%s Progressive accuracy at example %i: %0.3f' % (time.strftime('%X'), n, np.mean(accuracy_record[-sample:]))
accuracy = 0
if early_stop and n >= early_stop:
break
SGD.partial_fit(rbf_x, y, classes=range(1,8))
Out: ...
07:57:45 Progressive accuracy at example 50000: 0.707
超参数调谐
与批处理学习一样,在测试超参数的最佳组合时,核外算法没有捷径可走;您需要尝试一定数量的组合来找出可能的最佳解决方案,并使用样本外误差测量来评估它们的性能。
由于您实际上不知道您的预测问题是具有简单的平滑凸损失还是更复杂的凸损失,并且您不确切地知道您的超参数是如何相互作用的,因此如果没有尝试足够的组合,很容易陷入一些次优的局部最小值。不幸的是,目前 Scikit-learn 没有为核外算法提供专门的优化程序。考虑到在一个长流上训练一个 SGD 需要很长的时间,当使用这样的技术在你的数据上建立一个模型时,调整超参数真的会成为一个瓶颈。
在这里,我们提出一些经验法则,可以帮助您节省时间和精力,并取得最佳效果。
首先,您可以在一个窗口或一个适合内存的数据样本上调整参数。正如我们在内核支持向量机中看到的,即使你的数据流很大,使用一个储层样本也是相当快的。然后,您可以在内存中进行优化,并使用在流中找到的最佳参数。
正如微软研究院的莱昂·博图在他的技术论文中所说的那样:
“随机梯度下降的数学惊人地独立于训练集的大小。”
所有关键参数都是如此,尤其是学习率;对样本更有效的学习率对完整数据的效果最好。此外,通过尝试在一个小的采样数据集上收敛,可以猜测数据的理想传递次数。根据经验,我们报告了算法检查的10**6示例的指示性数量——正如 Scikit-learn 文档所指出的——我们经常发现这个数字是准确的,尽管理想的迭代次数可能会根据正则化参数而变化。
虽然在使用 SGD 时,大部分工作可以在相对较小的规模上完成,但我们必须定义如何解决固定多个参数的问题。传统上,手动搜索和网格搜索是最常用的方法,网格搜索通过系统地测试所有可能的参数组合的重要值来解决问题(例如,使用 10 或 2 的不同幂次的对数标度检查)。
最近,詹姆斯·伯格斯特拉(James Bergstra)和约舒亚·本吉奥(Yoshua Bengio)在他们的论文《超参数优化的随机搜索》中指出了一种基于超参数值随机采样的不同方法。尽管这种方法是基于随机选择的,但当超参数数量较低时,其结果通常与网格搜索相当(但需要较少的运行次数),当参数较多且并非所有参数都与算法性能相关时,其结果可能超过系统搜索的性能。
我们让读者通过参考前面提到的伯格斯特拉和本吉奥的论文来发现为什么这种简单而有吸引力的方法在理论上如此有效的更多原因。在实践中,在体验了它相对于其他方法的优越性之后,我们提出了一种基于 Scikit-learn 的ParameterSampler函数的方法,该方法在下面的示例代码片段中很好地适用于流。ParameterSampler能够随机采样不同的超参数集(来自分布函数或离散值列表),然后通过set_params方法应用于您的学习 SGD:
In: from sklearn.linear_model import SGDRegressor
from sklearn.grid_search import ParameterSampler
source = '\\bikesharing\\hour.csv'
local_path = os.getcwd()
b_vars = ['holiday','hr','mnth', 'season','weathersit','weekday','workingday','yr']
n_vars = ['hum', 'temp', 'atemp', 'windspeed']
std_row, min_max = explore(target_file=local_path+'\\'+source, binary_features=b_vars, numeric_features=n_vars)
val_rmse = 0
val_rmsle = 0
predictions_start = 16000
tmp_rsmle = 10**6
def apply_log(x): return np.log(x + 1.0)
def apply_exp(x): return np.exp(x) - 1.0
param_grid = {'penalty':['l1', 'l2'], 'alpha': 10.0**-np.arange(2,5)}
random_tests = 3
search_schedule = list(ParameterSampler(param_grid, n_iter=random_tests, random_state=5))
results = dict()
for search in search_schedule:
SGD = SGDRegressor(loss='epsilon_insensitive', epsilon=0.001, penalty=None, random_state=1, average=True)
params =SGD.get_params()
new_params = {p:params[p] if p not in search else search[p] for p in params}
SGD.set_params(**new_params)
print str(search)[1:-1]
for iterations in range(200):
for x,y,n in pull_examples(target_file=local_path+'\\'+source,
vectorizer=std_row, min_max=min_max, sparse = False,
binary_features=b_vars, numeric_features=n_vars, target='cnt'):
y_log = apply_log(y)
# MACHINE LEARNING
if (n+1) >= predictions_start:
# HOLDOUT AFTER N PHASE
predicted = SGD.predict(x)
val_rmse += (apply_exp(predicted) - y)**2
val_rmsle += (predicted - y_log)**2
else:
# LEARNING PHASE
SGD.partial_fit(x, y_log)
examples = float(n-predictions_start+1) * (iterations+1)
print_rmse = (val_rmse / examples)**0.5
print_rmsle = (val_rmsle / examples)**0.5
if iterations == 0:
print 'Iteration %i - RMSE: %0.3f - RMSE: %0.3f' % (iterations+1, print_rmse, print_rmsle)
if iterations > 0:
if tmp_rmsle / print_rmsle <= 1.01:
print 'Iteration %i - RMSE: %0.3f - RMSE: %0.3f\n' % (iterations+1, print_rmse, print_rmsle)
results[str(search)]= {'rmse':float(print_rmse), 'rmsle':float(print_rmsle)}
break
tmp_rmsle = print_rmsle
Out:
'penalty': 'l2', 'alpha': 0.001
Iteration 1 - RMSE: 216.170 - RMSE: 1.440
Iteration 20 - RMSE: 152.175 - RMSE: 0.857
'penalty': 'l2', 'alpha': 0.0001
Iteration 1 - RMSE: 714.071 - RMSE: 4.096
Iteration 31 - RMSE: 184.677 - RMSE: 1.053
'penalty': 'l1', 'alpha': 0.01
Iteration 1 - RMSE: 1050.809 - RMSE: 6.044
Iteration 36 - RMSE: 225.036 - RMSE: 1.298
代码利用了这样一个事实,即自行车共享数据集非常小,不需要任何采样。在其他情况下,通过水库取样或其他取样技术,限制已处理的行数或创建更小的样本是有意义的。如果您想更深入地探索优化,您可以更改random_tests变量,固定要测试的采样超参数组合的数量。然后,使用更接近于1.0的数字修改if tmp_rmsle / print_rmsle <= 1.01条件——如果不是1.0本身——从而让算法完全收敛,直到预测能力的某种可能增益可行。
型式
虽然建议使用分布函数,而不是从值列表中挑选,但是您仍然可以通过简单地增加可能从列表中挑选的值的数量来适当地使用我们之前建议的超参数范围。例如,对于 L1 和 L2 正则化中的 alpha,您可以使用 NumPy 的函数arrange,带有一个小步长,如10.0**-np.arange(1, 7, step=0.1),或者使用 NumPy logspace,带有一个高数值作为num参数:1.0/np.logspace(1,7,num=50)。
SVM 快速学习的其他选择
尽管 Scikit-learn 包提供了足够的工具和算法来学习内核外的东西,但是在自由软件中还有其他有趣的选择。有些是基于 Scikit-learn 本身使用的相同库,比如 Liblinear/SBM,还有一些是全新的,比如 sofia-ml、LASVM 和 Vowpal Wabbit。例如,基于选择性块最小化并作为原始库(https://www . csie . NTU . edu . tw/~ cjlin/libsvmtols/# large _ linear _ classification _ when _ data _ not _ fit _ in _ memory)的一个分叉liblinear-cdblock实现的 Liblinear/SBMis。Liblinear/SBM 通过使用新的数据样本训练学习者,并将其与已经用于最小化的先前样本混合(因此在算法名称中使用阻塞的术语),实现了在大量无法在内存中拟合的数据上拟合非线性支持向量机。
https://code.google.com/archive/p/sofia-ml/是 T2 的另一个选择。SofiaML 基于一种叫做 Pegasos SVM 的在线 SVM 优化算法。这个算法是一个在线的 SVM 近似,就像另一个软件由利昂·博图(http://leon.bottou.org/projects/lasvm)创建的叫做 LaSVM。所有这些解决方案都可以处理稀疏的数据,尤其是文本数据,并解决回归、分类和排序问题。到目前为止,我们测试的任何替代解决方案都没有像 Vowpal Wabbit 那样快速和通用,Vowpal Wabbit 是我们将在接下来的章节中介绍的软件,用于演示如何将外部程序与 Python 集成。
非线性,更快,带沃帕尔瓦比特
Vowpal Wabbit ( 大众)是一个为快速在线学习者开发的开源项目,最初于 2007 年由来自雅虎的约翰·兰福德、李立宏和亚历克斯·斯特雷尔发布!研究(http://hunch.net/?p=309)然后又相继被微软研究院赞助,因为约翰·兰福德成为了微软的首席研究员。这个项目已经发展了很多年,到今天已经到了 8.1.0 版本(在这一章写的时候),有将近一百个贡献者在做这个项目。(关于贡献随时间发展的可视化,在 https://www.youtube.com/watch?v=-aXelGLMMgk 有一个使用软件Gorce的有趣视频。).到目前为止,大众仍在不断开发,并在每次开发迭代中不断提高其学习能力。
大众汽车引人注目的特性是,与其他可用的解决方案(LIBLINEAR、Sofia-ml、svmsgd 和 Scikit-learn)相比,它非常快。它的秘密很简单,但非常有效:它可以同时加载数据和从中学习。异步线程对流入的示例进行解析,因为许多学习线程在一组不相交的特征上工作,因此即使在解析涉及高维特征创建(例如二次或三次多项式展开)时,也能确保高计算效率。在大多数情况下,学习过程的真正瓶颈是将数据传输到大众的磁盘或网络的传输带宽。
大众汽车可以解决分类(甚至多类和多标签)、回归(OLS 和分位数)和主动学习问题,提供大量附带的学习工具(称为约简),如矩阵分解、潜在狄利克雷分配 ( LDA )、神经网络、语言模型的 n-grams 和自举。
安装大众汽车
大众汽车可以从在线版本库 GitHub(https://github.com/JohnLangford/vowpal_wabbit)中检索到,在那里它可以被 Git 克隆或以打包 zip 的形式下载。它是在 Linux 系统上开发的,可以通过一系列简单的 make 和 make install 命令在任何 POSIX 环境下轻松编译。安装的详细说明可以直接在它的安装页面上找到,你可以直接从作者(https://github.com/JohnLangford/vowpal_wabbit/wiki/Download)那里下载 Linux 预编译二进制文件。
不幸的是,在视窗操作系统上运行的大众版本有点难获得。为了创建一个,首先参考大众的文档本身,其中详细解释了一个编译过程。
型式
在本书随附的网站上,我们将提供本书使用的大众 8.1.0 版本的 32 位和 64 位 Windows 二进制文件。
了解大众数据格式
大众可以使用特定的数据格式工作,并从外壳中调用。约翰·兰福德在他的在线教程中使用了这个样本数据集(https://github.com/JohnLangford/vowpal_wabbit/wiki/Tutorial,代表三座房屋,它们的屋顶可以被替换。我们觉得向您推荐并一起评论很有趣:
In:
with open('house_dataset','wb') as W:
W.write("0 | price:.23 sqft:.25 age:.05 2006\n")
W.write("1 2 'second_house | price:.18 sqft:.15 age:.35 1976\n")
W.write("0 1 0.5 'third_house | price:.53 sqft:.32 age:.87 1924\n")
with open('house_dataset','rb') as R:
for line in R:
print line.strip()
Out:
0 | price:.23 sqft:.25 age:.05 2006
1 2 'second_house | price:.18 sqft:.15 age:.35 1976
0 1 0.5 'third_house | price:.53 sqft:.32 age:.87 1924
文件格式的第一个值得注意的方面是它没有标题。这是因为大众使用哈希技巧将特征分配到一个稀疏向量中,因此事先知道根本不需要的特征。数据块由管道(字符|)划分为名称空间,作为不同的特征簇,每个特征簇包含一个或多个特征。
第一个命名空间总是包含响应变量的命名空间。响应可以是指出要回归的数值的实数(或整数)、二进制类或多个类中的一个类。响应总是在线上找到的第一个数字。一个二进制类可以使用1为正和-1为负进行编码(使用 0 作为响应只允许用于回归)。多个类应该从1开始编号,不建议使用间隙号,因为大众要求最后一个类,并考虑1和最后一个之间的所有整数。
响应值后紧接着的数字是权重(告诉您是否必须将一个示例视为多个示例或一个示例的一部分),然后是基数,它起着初始预测的作用(一种偏差)。最后,在撇号字符(')的前面,有一个标签,它可以是一个数字或文本,稍后会在大众输出中找到(在预测中,每个估计都有一个标识符)。重量、基数和标签不是强制性的:如果省略,重量将被估算为1,基数和标签无关紧要。
在第一个名称空间之后,您可以添加任意多个名称空间,用数字或字符串标记每个名称空间。为了被认为是命名空间的标签,它应该被粘在管道上,例如,|label。
在命名空间的标签之后,可以通过名称添加任何特征。要素名称可以是任何名称,但应该包含一个管道或冒号。您可以将整个文本放在名称空间中,每个单词都将被视为一个特性。每个特征都将被视为有价值的1。如果您想分配不同的数字,只需在要素名称的末尾加上一个冒号,然后将其值放在后面。
例如,Vowpal Wabbit 可读的有效行是:
0 1 0.5 'third_house | price:.53 sqft:.32 age:.87 1924
在第一个名称空间中,响应为0,示例权重为1,基数为0.5,标签为third_house。命名空间是无名的,由price(值为.53)、sqft(值为.32)、age(值为.87)、1924(值为1)四个特征构成。
如果您在一个示例中有一个特征,但在另一个示例中没有,则算法将在第二个示例中假设特征值为零。因此,像前面例子中的1924这样的特征可以作为二进制变量,因为当它存在时,当缺少0时,它被自动赋值1。这也告诉你大众如何处理缺失值——它会自动将它们视为0值。
型式
当一个值丢失时,您可以通过放置一个新的特性来轻松处理丢失的值。例如,如果特征是年龄,您可以添加一个新特征age_missing,它将是一个二进制变量,值为1。当估计系数时,该变量将充当缺失值估计器。
在作者的网站上,你还可以找到一个输入验证器,验证你的输入对大众来说是正确的,显示软件是如何解释的:
http://hunch.net/~vw/validate.html
Python 集成
有几个软件包将其与 Python(vowpal _ propowery、 Wabbit Wappa 或 pyvw )集成并安装它们在 Linux 系统中很容易,但在 Windows 上就难多了。无论您使用的是 Jupyter 还是 IDE,使用与 Python 脚本集成的大众汽车最简单的方法就是利用subprocess包中的Popen功能。这使得大众与 Python 并行运行。Python 只需通过捕捉其输出并打印在屏幕上,等待大众完成操作即可:
In: import subprocess
def execute_vw(parameters):
execution = subprocess.Popen('vw '+parameters, \
shell=True, stderr=subprocess.PIPE)
line = ""
history = ""
while True:
out = execution.stderr.read(1)
history += out
if out == '' and execution.poll() != None:
print '------------ COMPLETED ------------\n'
break
if out != '':
line += out
if '\n' in line[-2:]:
print line[:-2]
line = ''
return history.split('\r\n')
这些函数返回学习过程的输出列表,使其易于处理,提取相关的可重用信息(如错误度量)。作为其正确运行的先决条件,将大众可执行文件(即vw.exe文件)放在 Python 工作目录或系统路径中可以找到它的地方。
通过调用之前记录的房屋数据集上的函数,我们可以了解它是如何工作的,以及它产生了什么输出:
In:
params = "house_dataset"
results = execute_vw(params)
Out:
Num weight bits = 18
learning rate = 0.5
initial_t = 0
power_t = 0.5
using no cache
Reading datafile = house_dataset
num sources = 1
average since example example current current current
loss last counter weight label predict features
0.000000 0.000000 1 1.0 0.0000 0.0000 5
0.666667 1.000000 2 3.0 1.0000 0.0000 5
finished run
number of examples per pass = 3
passes used = 1
weighted example sum = 4.000000
weighted label sum = 2.000000
average loss = 0.750000
best constant = 0.500000
best constant's loss = 0.250000
total feature number = 15
------------ COMPLETED ------------
输出的初始行只是调用使用的参数,并确认正在使用哪个数据文件。最有趣的是按流式实例数量报告的递进(按 2 的幂报告,因此实例 1、2、4、8、16 等等)。关于损失函数,基于随后设置的暂停,报告平均损失度量,对于第一次迭代是递增的,其损失通过推迟字母h来表示(如果不包括暂停,则可能仅报告样本内度量)。在example weight栏中,会报告该示例的权重,然后该示例进一步描述为current label、current predict,并显示在该行中找到的特征数量(current features)。所有这些信息都应该有助于你持续关注学习流和学习过程。
完成学习后,汇报几项措施。平均损失是最重要的,尤其是在使用暂停时。由于比较的原因,使用这种损失是最有用的,因为它可以立即与best constant's loss(简单常数的基线预测能力)和使用不同参数配置的不同运行进行比较。
另一个非常有用的集成大众和 Python 的功能是我们准备的自动将 CSV 文件转换为大众数据文件的功能。您可以在下面的代码片段中找到它。这将有助于我们这次使用大众汽车复制以前的自行车共享和 covertype 问题,但它可以很容易地在您自己的项目中重用:
In: import csv
def vw_convert(origin_file, target_file, binary_features, numeric_features, target, transform_target=lambda(x):x,
separator=',', classification=True, multiclass=False, fieldnames= None, header=True, sparse=True):
"""
Reads a online style stream and returns a generator of normalized feature vectors
Parameters
----------
original_file = the CSV file you are taken the data from
target file = the file to stream from
binary_features = the list of qualitative features to consider
numeric_features = the list of numeric features to consider
target = the label of the response variable
transform_target = a function transforming the response
separator = the field separator character
classification = a Boolean indicating if it is classification
multiclass = a Boolean for multiclass classification
fieldnames = the fields' labels (can be omitted and read from file)
header = a boolean indicating if the original file has an header
sparse = if a sparse vector is to be returned from the generator
"""
with open(target_file, 'wb') as W:
with open(origin_file, 'rb') as R:
iterator = csv.DictReader(R, fieldnames, delimiter=separator)
for n, row in enumerate(iterator):
if not header or n>0:
# DATA PROCESSING
response = transform_target(float(row[target]))
if classification and not multiclass:
if response == 0:
stream_row = '-1 '
else:
stream_row = '1 '
else:
stream_row = str(response)+' '
quantitative = list()
qualitative = list()
for k,v in row.iteritems():
if k in binary_features:
qualitative.append(str(k)+\
'_'+str(v)+':1')
else:
if k in numeric_features and (float(v)!=0 or not sparse):
quantitative.append(str(k)+':'+str(v))
if quantitative:
stream_row += '|n '+\
' '.join(quantitative)
if qualitative:
stream_row += '|q '+\
' '.join(qualitative)
W.write(stream_row+'\n')
使用 SVM 约简和神经网络的几个例子
大众致力于最小化一般成本函数,如下所示:

与之前看到的其他公式一样,w 是系数向量,并且根据所选的损失函数(OLS、逻辑或铰链)分别为每个【Xi】和 yi 获得优化。Lambda1 和 lambda2 是正则化参数,默认情况下为零,但可以使用大众命令行中的--l1和--l2 选项进行设置。
鉴于这样的基本结构,大众已经变得更加复杂和完整,随着时间的推移,使用减少范式。约简只是重用现有算法以解决新问题的一种方式,而不是从头开始编写新的求解算法。换句话说,如果你有一个复杂的机器学习问题 A,你只需要把它简化为 B。解决 B 暗示了 A 的解决方案。这也是合理的,因为人们对机器学习的兴趣越来越大,无法解决的问题数量激增,产生了大量新算法。这是一种有趣的方法,利用了基本算法提供的现有可能性,也是为什么大众在程序保持相当紧凑的情况下,随着时间的推移,其适用性不断增强的原因。如果你对这种方法感兴趣,可以看看约翰·兰福德的这两本教程:http://hunch.net/~reductions_tutorial/和http://hunch.net/~jl/projects/reductions/reductions.html。
出于其他说明的目的,我们将向您简要介绍几个减少,以实现一个 SVM 与一个RBFkernel和一个浅层神经网络使用大众在一个纯粹的核心外的方式。为此,我们将使用一些玩具数据集。
这是鸢尾数据集,变成了一个二元分类问题,从濑户和弗吉尼亚猜测鸢尾的颜色:
In: import numpy as np
from sklearn.datasets import load_iris, load_boston
from random import seed
iris = load_iris()
seed(2)
re_order = np.random.permutation(len(iris.target))
with open('iris_versicolor.vw','wb') as W1:
for k in re_order:
y = iris.target[k]
X = iris.values()[1][k,:]
features = ' |f '+' '.join([a+':'+str(b) for a,b in zip(map(lambda(a): a[:-5].replace(' ','_'), iris.feature_names),X)])
target = '1' if y==1 else '-1'
W1.write(target+features+'\n')
然后对于一个回归问题,我们将使用波士顿房价数据集:
In: boston = load_boston()
seed(2)
re_order = np.random.permutation(len(boston.target))
with open('boston.vw','wb') as W1:
for k in re_order:
y = boston.target[k]
X = boston.data[k,:]
features = ' |f '+' '.join([a+':'+str(b) for a,b in zip(map(lambda(a): a[:-5].replace(' ','_'), iris.feature_names),X)])
W1.write(str(y)+features+'\n')
首先,我们将尝试 SVM。kvsm 是基于 LaSVM 算法(具有在线和主动学习的快速核分类器—http://www.jmlr.org/papers/volume6/bordes05a/bordes05a.pdf)的约简,没有偏差项。大众版本通常只在一个过程中工作,对随机选择的支持向量进行 1-2 次再处理(尽管有些问题可能需要多次过程和再处理)。在我们的例子中,我们只是使用了一次传递和几次重新处理,以便在我们的二进制问题上使用径向基函数核(KSVM 只适用于分类问题)。实现的核是线性、径向基函数和多项式。为了让它工作,使用--ksvm选项,通过--reprocess设置一个重新处理的数字(默认为 1),选择带有--kernel的内核(选项有linear、poly和rbf)。然后,如果内核是多项式,则为--degree设置一个整数,如果使用的是径向基函数,则为--bandwidth设置一个浮点数(默认值为 1.0)。您还必须强制指定 l2 正则化;否则,减少不会正常工作。在我们的示例中,我们制作了带宽为 0.1:
In: params = '--ksvm --l2 0.000001 --reprocess 2 -b 18 --kernel rbf --bandwidth=0.1 -p iris_bin.test -d iris_versicolor.vw'
results = execute_vw(params)
accuracy = 0
with open('iris_bin.test', 'rb') as R:
with open('iris_versicolor.vw', 'rb') as TRAIN:
holdouts = 0.0
for n,(line, example) in enumerate(zip(R,TRAIN)):
if (n+1) % 10==0:
predicted = float(line.strip())
y = float(example.split('|')[0])
accuracy += np.sign(predicted)==np.sign(y)
holdouts += 1
print 'holdout accuracy: %0.3f' % ((accuracy / holdouts)**0.5)
Out: holdout accuracy: 0.966
神经网络是大众的另一个很酷的补充;感谢 Paul Mineiro 的工作(http://www . machinedlearnings . com/2012/11/unpimp-you-sigmoid . html),大众可以实现一个具有双曲正切( tanh )激活和(可选的)脱扣(使用--dropout选项)的单层神经网络。虽然只可能决定神经元的数量,但神经约简对回归和分类问题都很有效,并且可以平滑地接受大众作为输入的其他转换(例如二次变量和 n-gram),使其成为一个非常好的集成、通用(神经网络可以解决相当多的问题)和快速的解决方案。在我们的示例中,我们使用五个神经元和 drop 将其应用于波士顿数据集:
In: params = 'boston.vw -f boston.model --loss_function squared -k --cache_file cache_train.vw --passes=20 --nn 5 --dropout'
results = execute_vw(params)
params = '-t boston.vw -i boston.model -k --cache_file cache_test.vw -p boston.test'
results = execute_vw(params)
val_rmse = 0
with open('boston.test', 'rb') as R:
with open('boston.vw', 'rb') as TRAIN:
holdouts = 0.0
for n,(line, example) in enumerate(zip(R,TRAIN)):
if (n+1) % 10==0:
predicted = float(line.strip())
y = float(example.split('|')[0])
val_rmse += (predicted - y)**2
holdouts += 1
print 'holdout RMSE: %0.3f' % ((val_rmse / holdouts)**0.5)
Out: holdout RMSE: 7.010
更快的自行车共享
让我们在之前创建的自行车共享示例文件上试试大众,以解释输出组件。作为第一步,您必须将 CSV 文件转换为大众文件,之前的vw_convert功能将在这样做时派上用场。和前面一样,我们将使用vw_convert函数的transform_target参数传递的apply_log函数对数值响应进行对数变换:
In: import os
import numpy as np
def apply_log(x):
return np.log(x + 1.0)
def apply_exp(x):
return np.exp(x) - 1.0
local_path = os.getcwd()
b_vars = ['holiday','hr','mnth', 'season','weathersit','weekday','workingday','yr']
n_vars = ['hum', 'temp', 'atemp', 'windspeed']
source = '\\bikesharing\\hour.csv'
origin = target_file=local_path+'\\'+source
target = target_file=local_path+'\\'+'bike.vw'
vw_convert(origin, target, binary_features=b_vars, numeric_features=n_vars, target = 'cnt', transform_target=apply_log,
separator=',', classification=False, multiclass=False, fieldnames= None, header=True)
几秒钟后,新文件应该准备好了。我们可以立即运行我们的解决方案,这是一个简单的线性回归(大众的默认选项)。学习预计将进行 100 次,由大众自动实施的样本外验证控制(以可重复的方式系统绘制,每 10 次观察中有一次作为验证)。在这种情况下,我们决定在 16,000 个示例后设置保持样本(使用--holdout_after选项)。当验证上的验证错误增加(而不是减少)时,大众会在几次迭代后停止(默认为三次,但可以使用--early_terminate选项更改数量),避免过度拟合数据:
In: params = 'bike.vw -f regression.model -k --cache_file cache_train.vw --passes=100 --hash strings --holdout_after 16000'
results = execute_vw(params)
Out: …
finished run
number of examples per pass = 15999
passes used = 6
weighted example sum = 95994.000000
weighted label sum = 439183.191893
average loss = 0.427485 h
best constant = 4.575111
total feature number = 1235898
------------ COMPLETED ------------
最终的报告显示,六次通过(100 次可能通过)已经完成,样本外平均损失为 0.428。因为我们对RMSE和RMSLE感兴趣,所以我们必须自己计算。
然后,我们预测文件(pred.test)中的结果,以便能够读取它们,并使用与训练集中相同的保持策略计算我们的误差度量。结果确实比我们之前使用 Scikit-learn 的 SGD 获得的结果好得多(在一小部分时间内):
In: params = '-t bike.vw -i regression.model -k --cache_file cache_test.vw -p pred.test'
results = execute_vw(params)
val_rmse = 0
val_rmsle = 0
with open('pred.test', 'rb') as R:
with open('bike.vw', 'rb') as TRAIN:
holdouts = 0.0
for n,(line, example) in enumerate(zip(R,TRAIN)):
if n > 16000:
predicted = float(line.strip())
y_log = float(example.split('|')[0])
y = apply_exp(y_log)
val_rmse += (apply_exp(predicted) - y)**2
val_rmsle += (predicted - y_log)**2
holdouts += 1
print 'holdout RMSE: %0.3f' % ((val_rmse / holdouts)**0.5)
print 'holdout RMSLE: %0.3f' % ((val_rmsle / holdouts)**0.5)
Out:
holdout RMSE: 135.306
holdout RMSLE: 0.845
大众处理的覆盖型数据集
covertype 问题大众也能比我们之前管理的更好更容易解决。这一次,我们需要设置一些参数决定纠错锦标赛 ( ECT ,由大众汽车上的--ect参数调用),其中每个职业在一场淘汰锦标赛中竞争成为一个例子的标签。在很多例子中,ECT 可以胜过一对所有 ( OAA ),但这不是一般规律,ECT 是处理多类问题时需要测试的方法之一。(另一个可能的选择是--log_multi,使用在线决策树将样本分割成更小的集合,在这些集合中我们应用单一预测模型。)我们还将学习率设置为 1.0,并使用--cubic 参数创建三次多项式展开,指出哪些名称空间必须相互相乘(在这种情况下,三次的名称空间 f 由nnn字符串后跟--cubic表示。):
In: import os
local_path = os.getcwd()
n_vars = ['var_'+'0'*int(j<10)+str(j) for j in range(54)]
source = 'shuffled_covtype.data'
origin = target_file=local_path+'\\'+source
target = target_file=local_path+'\\'+'covtype.vw'
vw_convert(origin, target, binary_features=list(), fieldnames= n_vars+['covertype'], numeric_features=n_vars,
target = 'covertype', separator=',', classification=True, multiclass=True, header=False, sparse=False)
params = 'covtype.vw --ect 7 -f multiclass.model -k --cache_file cache_train.vw --passes=2 -l 1.0 --cubic nnn'
results = execute_vw(params)
Out:
finished run
number of examples per pass = 522911
passes used = 2
weighted example sum = 1045822.000000
weighted label sum = 0.000000
average loss = 0.235538 h
total feature number = 384838154
------------ COMPLETED ------------
型式
为了让这个例子更快,我们将传球次数限制在两次。如果你有时间,把数字提高到 100,看看如何进一步提高获得的精度。
这里,我们不需要进一步检查误差度量,因为报告的平均损失是精度度量 1.0 的补充;我们只是计算它的完整性,确认我们的保持精度正好是0.769:
In: params = '-t covtype.vw -i multiclass.model -k --cache_file cache_test.vw -p covertype.test'
results = execute_vw(params)
accuracy = 0
with open('covertype.test', 'rb') as R:
with open('covtype.vw', 'rb') as TRAIN:
holdouts = 0.0
for n,(line, example) in enumerate(zip(R,TRAIN)):
if (n+1) % 10==0:
predicted = float(line.strip())
y = float(example.split('|')[0])
accuracy += predicted ==y
holdouts += 1
print 'holdout accuracy: %0.3f' % (accuracy / holdouts)
Out: holdout accuracy: 0.769
总结
在本章中,我们通过向简单的基于回归的线性模型添加支持向量机,扩展了对核外算法的初步讨论。大部分时间,我们专注于 Scikit-learn 实现——大部分是 SGD——并以可以与 Python 脚本集成的外部工具的概述结束,例如约翰·兰福德的 Vowpal Wabbit。在此过程中,我们通过讨论储层采样、正则化、显式和隐式非线性变换以及超参数优化,完成了对在核外工作时模型改进和验证技术的概述。
在下一章中,我们将涉及更复杂和更强大的学习方法,同时介绍大规模问题中的深度学习和神经网络。如果你的项目围绕着图像和声音的分析,那么到目前为止我们所看到的可能还不是你想要的神奇解决方案。下一章将提供所有期望的解决方案。
四、神经网络与深度学习
在这一章中,我们将讲述人工智能和机器学习中最令人兴奋的领域之一:深度学习。本章将介绍有效应用深度学习所必需的最重要的概念。我们将在本章中讨论的主题如下:
- 本质神经网络理论
- 在图形处理器或中央处理器上运行神经网络
- 神经网络的参数调整
- H2O 大规模深度学习
- 自动编码器深度学习(预处理)
深度学习产生于发展神经网络的人工智能领域。严格来说,任何大的神经网络都可以认为是深度学习。然而,深度架构的最新发展需要的不仅仅是建立大型神经网络。深度体系结构与普通多层网络的区别在于,深度体系结构由多个预处理和无监督步骤组成,这些步骤检测数据中的潜在维度,这些维度随后将被馈送到网络的其他阶段。关于深度学习要知道的最重要的一点就是通过这些深度架构来学习和转化新的特性,以提高整体的学习精度。因此,当前一代深度学习方法和其他机器学习方法之间的一个重要区别是,通过深度学习,特征工程的任务在一定程度上是自动化的。如果这些概念听起来很抽象,不要太担心,它们将在本章后面与实际例子一起阐明。这些深度学习方法引入了新的复杂性,这使得有效地应用它们非常具有挑战性。
最大的挑战是它们在训练、计算时间和参数调整方面的困难。本章将讨论这些困难的解决办法。
在过去的十年中,深度学习的有趣应用可以在计算机视觉、自然语言处理和音频处理中找到,例如由脸书的一个研究小组创建的脸书深度人脸项目,该研究小组部分由著名的深度学习学者扬·勒坤领导。深度人脸旨在从数字图像中提取和识别人脸。谷歌有自己的项目 DeepMind ,由杰弗里·辛顿领导。谷歌最近推出了 TensorFlow ,这是一个提供深度学习应用的开源库,将在下一章详细介绍。
在我们开始释放通过图灵测试和数学竞赛的自主智能代理之前,让我们稍微后退一步,从头开始。
神经网络架构
现在让我们关注神经网络是如何组织的,从它们的架构和一些定义开始。
学习流一路向前传递到输出的网络称为前馈神经网络。
一个基本的前馈神经网络可以很容易地用网络图来描述,如下图所示:

在网络图中,可以看到这个架构由输入层、隐藏层和输出层组成。输入层包含特征向量(其中每个观察都有 n 特征),输出层在分类的情况下由每类输出向量的独立单元组成,在回归的情况下由单个数值向量组成。
单元之间的连接强度通过稍后传递给激活函数的权重来表示。激活函数的目标是将其输入转换为输出,使二进制决策更加可分。
这些激活函数最好是可微的,这样就可以用来学习。
广泛使用的激活功能是 sigmoid 和 tanh ,最近更是整流线性单元 ( ReLU )获得了牵引力。让我们比较一下最重要的激活函数,以便了解它们的优缺点。注意,我们提到了功能的输出范围和活动范围。输出范围只是函数本身的实际输出。然而,活动范围稍微复杂一些;这是梯度在最终权重更新中方差最大的范围。这意味着在该范围之外,梯度接近于零,并且不会增加学习期间的参数更新。这个接近于零的梯度的问题也被称为消失梯度问题,并通过 ReLU 激活功能来解决,该功能此时是较大神经网络最流行的激活:

需要注意的是,需要将特征缩放至所选激活功能的激活范围。大多数最新的包都将此作为标准的预处理过程,因此您不需要自己做:

Sigmoid 函数通常用于数学上的便利,因为它们的导数非常容易计算,我们将使用它来计算训练算法中的权重更新:

有趣的是,tanh 和逻辑 sigmoid 函数是线性相关的,并且 tanh 可以被视为 sigmoid 函数的重新缩放版本,因此它的范围在-1和1之间。

该功能是更深层架构的最佳选择。它可以看作是一个斜坡函数,其范围位于0以上到无穷大。你可以看到,它比 sigmoid 函数更容易计算。这个函数的最大好处是它绕过了消失梯度问题。如果 ReLU 是深度学习项目中的一个选项,请使用它。
用于分类的 Softmax】
到目前为止,我们已经看到激活函数将值乘以权重向量后,在一定范围内进行转换。我们还需要在提供平衡类或概率输出(对数似然值)之前转换最后一个隐藏层的输出。
这将把前一层的输出转换成概率值,从而可以进行最终的类别预测。每当输出明显小于所有值的最大值时,这种情况下的幂运算将返回一个接近零的值;这样差异就被放大了:

正向传播
现在我们已经了解了激活函数和网络的最终输出,让我们看看输入特征是如何通过网络提供最终的预测的。具有大量单元和连接的计算看起来可能是一项复杂的任务,但幸运的是,神经网络的前馈过程归结为一系列向量计算:

通过执行以下步骤,我们得出最终预测:
- 用第一层和第二层之间的权重对输入执行点积,并用激活函数转换结果。
- 用第二和第三层之间的权重对第一隐藏层的输出执行点积。这些结果随后用第二隐藏层的每个单元上的激活函数进行转换。
- 最后,我们通过将向量乘以激活函数(softmax 用于分类)来得出我们的预测。
我们可以将网络中的每一层视为一个向量,并应用简单的向量乘法。更正式地说,这看起来如下:
=层 x 的权重向量
b1 和 b2 为偏差单位
f =激活功能




注
请注意,此示例基于单一隐藏层网络架构。
让我们对一个神经网络执行一个简单的前馈传递,该神经网络具有两个带有基本 NumPy 的隐藏层。我们将softmax功能应用于最终输出:
import numpy as np
import math
b1=0 #bias unit 1
b2=0 #bias unit 2
def sigmoid(x): # sigmoid function
return 1 /(1+(math.e**-x))
def softmax(x): #softmax function
l_exp = np.exp(x)
sm = l_exp/np.sum(l_exp, axis=0)
return sm
# input dataset with 3 features
X = np.array([ [.35,.21,.33],
[.2,.4,.3],
[.4,.34,.5],
[.18,.21,16] ])
len_X = len(X) # training set size
input_dim = 3 # input layer dimensionality
output_dim = 1 # output layer dimensionality
hidden_units=4
np.random.seed(22)
# create random weight vectors
theta0 = 2*np.random.random((input_dim, hidden_units))
theta1 = 2*np.random.random((hidden_units, output_dim))
# forward propagation pass
d1 = X.dot(theta0)+b1
l1=sigmoid(d1)
l2 = l1.dot(theta1)+b2
#let's apply softmax to the output of the final layer
output=softmax(l2)
注
请注意,偏置单元使函数能够上下移动,有助于更接近目标值。每个隐藏层由一个偏置单元组成。
反向传播
通过我们简单的前馈示例,我们已经迈出了训练模型的第一步。神经网络的训练非常类似于我们在其他机器学习算法中看到的梯度下降方法。也就是说,我们升级模型的参数,以便找到误差函数的全局最小值。神经网络的一个重要区别是,我们现在必须处理网络中的多个单元,我们需要独立训练这些单元。我们使用成本函数的偏导数来实现这一点,并计算当我们将特定参数向量改变一定量(学习速率)时,误差曲线下降了多少。我们从最接近输出的层开始,计算相对于损失函数导数的梯度。如果有隐藏层,我们移动到第二个隐藏层并更新权重,直到到达前馈网络中的第一层。
反向传播的核心思想与其他机器学习算法非常相似,重要的复杂性在于我们要处理多个层和单元。我们已经看到,网络中的每一层都由权重向量
ij 表示。那么,我们如何解决这个问题呢?我们不得不独立训练大量的重量,这似乎令人生畏。然而,非常方便的是,我们可以使用矢量化运算。就像我们对向前传球所做的一样,我们计算梯度并更新应用于权重向量的权重(
ij)。
我们可以总结反向传播算法中的以下步骤:
-
前馈传递:我们随机初始化权重向量,并将输入与后续权重向量相乘,得到最终输出。
-
Calculate the error: We calculate the error/loss of the output of the feedforward step.
随机初始化权重向量。
-
反向传播到最后一个隐藏层(相对于输出)。我们计算这个误差的梯度,并朝着梯度的方向改变权重。我们通过将权重向量
j 乘以所执行的梯度来实现这一点。 -
Update the weights till the stopping criterion is reached (minimum error or number of training rounds):
![The neural network architecture]()
我们现在已经讨论了任意两层神经网络的前馈通路;让我们在 NumPy 中将 SGD 的反向传播应用到上一个示例中使用的相同输入。特别注意我们如何升级重量参数:
import numpy as np
import math
def sigmoid(x): # sigmoid function
return 1 /(1+(math.e**-x))
def deriv_sigmoid(y): #the derivative of the sigmoid function
return y * (1.0 - y)
alpha=.1 #this is the learning rate
X = np.array([ [.35,.21,.33],
[.2,.4,.3],
[.4,.34,.5],
[.18,.21,16] ])
y = np.array([[0],
[1],
[1],
[0]])
np.random.seed(1)
#We randomly initialize the layers
theta0 = 2*np.random.random((3,4)) - 1
theta1 = 2*np.random.random((4,1)) - 1
for iter in range(205000): #here we specify the amount of training rounds.
# Feedforward the input like we did in the previous exercise
input_layer = X
l1 = sigmoid(np.dot(input_layer,theta0))
l2 = sigmoid(np.dot(l1,theta1))
# Calculate error
l2_error = y - l2
if (iter% 1000) == 0:
print "Neuralnet accuracy:" + str(np.mean(1-(np.abs(l2_error))))
# Calculate the gradients in vectorized form
# Softmax and bias units are left out for instructional simplicity
l2_delta = alpha*(l2_error*deriv_sigmoid(l2))
l1_error = l2_delta.dot(theta1.T)
l1_delta = alpha*(l1_error * deriv_sigmoid(l1))
theta1 += l1.T.dot(l2_delta)
theta0 += input_layer.T.dot(l1_delta)
现在看看如何随着每次通过网络而提高的准确度:
Neuralnet accuracy:0.983345051044
Neuralnet accuracy:0.983404936523
Neuralnet accuracy:0.983464255273
Neuralnet accuracy:0.983523015841
Neuralnet accuracy:0.983581226603
Neuralnet accuracy:0.983638895759
Neuralnet accuracy:0.983696031345
Neuralnet accuracy:0.983752641234
Neuralnet accuracy:0.983808733139
Neuralnet accuracy:0.98386431462
Neuralnet accuracy:0.983919393086
Neuralnet accuracy:0.983973975799
Neuralnet accuracy:0.984028069878
Neuralnet accuracy:0.984081682304
Neuralnet accuracy:0.984134819919
反向传播的常见问题
神经网络的一个常见问题是,在反向传播优化过程中,梯度会陷入局部最小值。当误差最小化被欺骗为看到一个最小值(图像中的点 S )时,这种情况就会发生,在该点处,通过峰值 S 实际上只是一个局部凸起:

另一个常见的问题是当梯度下降错过全局最小值时,这有时会导致令人惊讶的低性能模型。这个问题被称为超调。
通过在模型超调时选择较低的学习速率或者在陷入局部极小值时选择较高的学习速率,可以解决这两个问题。有时这种调整仍然没有带来令人满意的快速收敛。最近,已经找到了一系列解决方案来缓解这些问题。我们刚刚介绍的对普通的SGDalgorithms进行微调的学习算法已经开发出来了。理解它们很重要,这样你就可以为任何给定的任务选择正确的任务。让我们更详细地介绍这些学习算法。
小批量反向传播
批处理梯度下降使用整个数据集计算梯度,但是反向传播 SGD 也可以使用所谓的小批处理,其中大小为 k (批处理)的数据集样本用于更新学习参数。每次更新之间的误差不规则量可以用小批量平滑掉,这样可以避免陷入和超过局部最小值。在大多数神经网络包中,我们可以改变算法的批量大小(我们将在后面看到)。根据训练示例的数量,10 到 300 之间的批量可能会有所帮助。
动量训练
动量是一种将先前权重更新的一部分添加到当前权重更新的方法:

这里,先前权重更新的一部分被添加到当前权重更新中。高动量参数可以帮助加快收敛速度,更快地达到全局最小值。看公式,可以看到一个 v 参数。这相当于以学习速率
更新梯度的速度。理解这一点的一个简单方法是,当梯度在多个实例中不断指向同一方向时,收敛速度随着向最小值的每一步而增加。这也消除了梯度之间一定幅度的不规则性。大多数包都有这个动量参数(我们将在后面的例子中看到)。当我们将该参数设置得太高时,我们必须记住,存在超过全局最小值的风险。另一方面,当我们将动量参数设置得太低时,系数可能会陷入局部极小值,也会减慢学习速度。动量系数的理想设置通常在. 5 和. 99 范围内。
内斯特罗夫气势
内斯特罗夫动量是经典动量的更新和改进版本。除了经典的动量训练外,它还会在梯度的方向向前看。换句话说,内斯特罗夫动量从 x 到 y 简单地移动了一步,并在这个方向上再移动一点,这样 x 到 y 就变成了 x 到 {y (v1 +1)} 在前一点给定的方向上。我将省略技术细节,但请记住,在收敛性方面,它始终优于正常的动量训练。如果内斯特罗夫势头有选择,那就利用它。
自适应梯度(ADAGRAD)
ADAGRAD 提供了特定于功能的学习率,利用了以前升级的信息:

ADAGRAD 根据来自该参数先前迭代梯度的信息更新每个参数的学习速率。这是通过将每个项除以其先前梯度平方和的平方根来实现的。这使得学习率随着时间的推移而降低,因为每次迭代的平方和将继续增加。降低学习速率的优点是大大降低了超过全局最小值的风险。
弹性反向传播(RPROP)
RPROP 是一种自适应方法,它不查看历史信息,而仅查看训练实例上偏导数的符号,并相应地更新权重。

快速反向传播学习的直接自适应方法:RPROP 算法。马丁·里德米勒 1993
RPROP 是一种自适应方法,它不查看历史信息,而仅查看训练实例上偏导数的符号,并且相应地更新权重。仔细检查前面的图像,我们可以看到,一旦误差的偏导数改变其符号( > 0 或 < 0 ),梯度开始向相反的方向移动,导致对超调的全局最小校正。然而,如果这个符号没有任何变化,就会朝着全局最小值迈出更大的步伐。许多文章已经证明了 RPROP 相对于 ADAGRAD 的优越性,但在实践中,这并没有得到一致的证实。另一件需要记住的重要事情是,RPROP 在迷你批次中无法正常工作。
rmsprep
RMSProp 是一种自适应学习方法,不会降低学习速度:

RMSProp 也是一种自适应学习方法,它利用了动量学习和 ADAGRAD 的思想,重要的补充是它避免了学习速率随时间的收缩。使用这种技术,收缩由梯度平均值上的指数衰减函数控制。
以下是梯度下降优化算法的列表:
| |应用
|
常见问题
|
实用技巧
|
| --- | --- | --- | --- |
| 常规 SGD | 广泛适用 | 过冲,陷入局部最小值 | 与动量和小批量一起使用 |
| adagrad | 较小的数据集< 10k | 缓慢收敛 | 使用. 01 到. 1 之间的学习率。广泛适用。处理稀疏数据 |
| 丙肝 | 大于 10k 的较大数据集 | 不适用于小批量 | 尽可能使用 RMSProp |
| rmsprep | 大于 10k 的较大数据集 | 对宽而浅的网无效 | 对于宽稀疏数据特别有用 |
神经网络学习什么以及如何学习
既然我们已经对各种形式的反向传播有了基本的了解,是时候解决神经网络项目中最困难的任务了:我们如何选择正确的架构?神经网络的一个关键能力是架构内的权重可以将输入转换到非线性特征空间,从而解决非线性分类(决策边界)和回归问题。让我们做一个简单而有见地的练习,在neurolab包中演示这个想法。我们只会用neurolab进行短暂的锻炼;对于可扩展学习问题,我们将提出其他方法。
首先,用pip安装neurolab包装。
从终端安装neurolab:
> $pip install neurolab
通过这个例子,我们将使用numpy生成一个简单的非线性余弦函数,并训练一个神经网络来从一个变量预测余弦函数。我们将设置几个神经网络架构,以了解每个架构预测余弦目标变量的能力:
import neurolab as nl
import numpy as np
from sklearn import preprocessing
import matplotlib.pyplot as plt
plt.style.use('ggplot')
# Create train samples
x = np.linspace(-10,10, 60)
y = np.cos(x) * 0.9
size = len(x)
x_train = x.reshape(size,1)
y_train = y.reshape(size,1)
# Create network with 4 layers and random initialized
# just experiment with the amount of layers
d=[[1,1],[45,1],[45,45,1],[45,45,45,1]]
for i in range(4):
net = nl.net.newff([[-10, 10]],d[i])
train_net=nl.train.train_gd(net, x_train, y_train, epochs=1000, show=100)
outp=net.sim(x_train)
# Plot results (dual plot with error curve and predicted values)
import matplotlib.pyplot
plt.subplot(2, 1, 1)
plt.plot(train_net)
plt.xlabel('Epochs')
plt.ylabel('squared error')
x2 = np.linspace(-10.0,10.0,150)
y2 = net.sim(x2.reshape(x2.size,1)).reshape(x2.size)
y3 = outp.reshape(size)
plt.subplot(2, 1, 2)
plt.suptitle([i ,'hidden layers'])
plt.plot(x2, y2, '-',x , y, '.', x, y3, 'p')
plt.legend(['y predicted', 'y_target'])
plt.show()
现在仔细观察误差曲线如何表现,以及当我们给神经网络增加更多层时,预测值如何开始接近目标值。
通过零隐藏层,神经网络通过目标值投射一条直线。误差曲线在拟合不良的情况下迅速降至最小值:

有了一个隐藏层,网络开始接近目标输出。观察误差曲线有多不规则:

通过两个隐藏层,神经网络更接近目标值。误差曲线下降更快,不规则性更小:

一个几乎完美的符合三个隐藏层。误差曲线下降得更快(在 220 迭代左右)。

上图中的橙色线是误差如何随每个时期下降的可视化图(整个训练集)。它向我们表明,我们需要通过训练集的一定次数才能达到全局最小值。如果您更仔细地检查这个误差曲线,您会发现误差曲线在每个体系结构中的表现是不同的。下图(虚线)显示了预测值如何开始接近目标值。由于没有隐藏层,神经网络无法检测非线性函数,但一旦我们添加隐藏层,网络就会开始学习非线性函数和日益复杂的函数。事实上,神经网络可以学习任何可能的功能。这种学习每一个可能函数的能力被称为通用近似定理。我们可以通过向神经网络添加隐藏的神经元(单元和层)来修改这种近似。然而,我们确实需要谨慎,不要过量食用;增加大量的层和单元将导致训练数据的记忆,而不是拟合可推广的函数。通常,网络中的层数太多会影响预测的准确性。
选择合适的架构
正如我们已经看到的,可能的神经网络结构的组合空间几乎是无限的。那么如何提前知道哪种架构会适合我们的项目呢?我们需要某种启发式或经验法则来为特定任务设计架构。在最后一节中,我们使用了一个只有一个输出和一个特性的简单示例。然而,我们称之为深度学习的最近一波神经网络架构非常复杂,能够为任何给定任务构建正确的神经网络架构至关重要。正如我们之前提到的,典型的神经网络由输入层、一个或多个隐藏层和输出层组成。让我们详细看看架构的每一层,这样我们就可以有一种为任何给定任务设置正确架构的感觉。
输入层
当我们提到输入层时,我们基本上是在谈论将被用作神经网络的输入的特征。所需的预处理步骤高度依赖于数据的形状和内容。如果我们有在不同尺度上测量的特征,我们需要重新缩放和归一化数据。在我们有大量特征的情况下,像主成分分析或奇异值分解这样的降维技术将变得值得推荐。
在学习之前,可以对输入应用以下预处理技术:
- 标准化、缩放和异常值检测
- 降维(奇异值分解和因子分析)
- 预处理(自动编码器和玻尔兹曼机器)
我们将在接下来的例子中介绍这些方法。
隐藏层
我们如何选择隐藏层中的单位数量?我们在网络中增加了多少隐藏层?我们在前面的例子中已经看到,没有隐藏层的神经网络不能学习非线性函数(无论是在回归的曲线拟合中还是在分类的决策边界中)。因此,如果有一个非线性模式或决策边界要投影,我们将需要隐藏层。说到选择隐藏层中的单位数量,我们一般希望隐藏层中的单位数量少于输入层中的单位数量,而单位数量多于输出单位数量:
- 优选地,比输入特征的数量更少的隐藏单元
- 超过输出单位数量的单位(分类类别)
有时,当目标函数的形状非常复杂时,就会出现异常。在我们添加的单位多于输入尺寸的情况下,我们添加特征空间的扩展。具有这种层的网络通常被称为广域网。
复杂网络可以学习更复杂的函数,但这并不意味着我们可以简单地继续堆叠层。建议控制层数,因为层数过多会导致过拟合、较高的 CPU 负载甚至是欠拟合的问题。通常一到四个隐藏层就足够了。
型式
最好使用一到四层作为起点。
输出层
每个神经网络都有一个输出层,就像输入层一样,高度依赖于所讨论的数据的结构。对于分类,我们一般会使用softmax功能。在这种情况下,我们应该使用与我们预测的类别数量相同的单位数量。
作用中的神经网络
让我们获得一些训练神经网络进行分类的实践经验。我们将使用 sknn,用于千层面和派尔恩 2 的 Scikit-learn 包装纸。您可以在https://github.com/aigamedev/scikit-neuralnetwork/了解更多关于套餐的信息。
我们将使用这个工具,因为它的实用和 Pythonic 接口。这是对像 Keras 这样更复杂的框架的很好的介绍。
sknn 库可以在中央处理器或图形处理器上运行,无论您喜欢哪个。请注意,如果您选择使用图形处理器,sknn 将运行在:
For CPU (most stable) :
# Use the GPU in 32-bit mode, from sknn.platform import gpu32
from sknn.platform import cpu32, threading
# Use the CPU in 64-bit mode.from sknn.platform import cpu64
from sknn.platform import cpu64, threading
GPU:
# Use the GPU in 32-bit mode,
from sknn.platform import gpu32
# Use the CPU in 64-bit mode.
from sknn.platform import cpu64
【sknn 的并行化
我们可以通过以下方式利用并行处理,但是这个有一个警告。它不是最稳定的方法:
from sknn.platform import cpu64, threading
我们可以指定 Scikit-学会利用特定数量的线程:
from sknn.platform import cpu64, threads2 #any desired amount of threads
当您指定了适当数量的线程后,您可以通过在交叉验证中实现n_jobs=nthreads来并行化您的代码。
既然我们已经涵盖了最重要的概念并准备好了我们的环境,让我们实现一个神经网络。
对于这个例子,我们将使用方便但相当枯燥的 Iris 数据集。
之后,我们将以标准化和缩放的形式应用预处理,并开始构建我们的模型:
import numpy as np
from sklearn.datasets import load_iris
from sknn.mlp import Classifier, Layer
from sklearn import preprocessing
from sklearn.cross_validation import train_test_split
from sklearn import cross_validation
from sklearn import datasets
# import the familiar Iris data-set
iris = datasets.load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data,
iris.target, test_size=0.2, random_state=0)
这里,我们对输入进行预处理、归一化和缩放:
X_trainn = preprocessing.normalize(X_train, norm='l2')
X_testn = preprocessing.normalize(X_test, norm='l2')
X_trainn = preprocessing.scale(X_trainn)
X_testn = preprocessing.scale(X_testn)
让我们设置我们的神经网络结构和参数。让我们从一个具有两个层的神经网络开始。在Layer部分,我们单独指定每一层的设置。(我们将在 Tensorflow 和 Keras 中再次看到这种方法。)Iris 数据集由四个特征组成,但因为在这种特殊情况下宽的神经网络工作得相当好,我们将在每个隐藏层中使用 13 个单元。请注意,sknn 默认应用 SGD:
clf = Classifier(
layers=[
Layer("Rectifier", units=13),
Layer("Rectifier", units=13),
Layer("Softmax")], learning_rate=0.001,
n_iter=200)
model1=clf.fit(X_trainn, y_train)
y_hat=clf.predict(X_testn)
scores = cross_validation.cross_val_score(clf, X_trainn, y_train, cv=5)
print 'train mean accuracy %s' % np.mean(scores)
print 'vanilla sgd test %s' % accuracy_score(y_hat,y_test)
OUTPUT:]
train sgd mean accuracy 0.949909090909
sgd test 0.933333333333
在训练中取得了不错的成绩,但我们可能会做得更好。
我们讨论了内斯特罗夫动量如何将长度缩短到全球最小值;让我们用nesterov来运行这个算法,看看是否能提高精度和改善收敛性:
clf = Classifier(
layers=[
Layer("Rectifier", units=13),
Layer("Rectifier", units=13),
Layer("Softmax")], learning_rate=0.001,learning_rule='nesterov',random_state=101,
n_iter=1000)
model1=clf.fit(X_trainn, y_train)
y_hat=clf.predict(X_testn)
scores = cross_validation.cross_val_score(clf, X_trainn, y_train, cv=5)
print 'Nesterov train mean accuracy %s' % np.mean(scores)
print 'Nesterov test %s' % accuracy_score(y_hat,y_test)
OUTPUT]
Nesterov train mean accuracy 0.966575757576
Nesterov test 0.966666666667
我们的模型是改进的,在这种情况下有内斯特罗夫动量。
神经网络和正则化
即使我们在上一个例子中没有过度训练我们的模型,也有必要考虑神经网络的正则化策略。我们可以将正则化应用于神经网络的三种最广泛使用的方法如下:
-
L1 和 L2 正则化,权重衰减作为正则化强度的参数
-
Dropout means that deactivating units within the neural network at random can force other units in the network to take over
![Neural networks and regularization]()
在左边,我们看到一个应用了丢包的架构,随机去激活网络中的单元。在右边,我们看到一个普通的神经网络(标有 X)。
-
求平均值或集合多个神经网络(每个神经网络具有不同的设置)
让我们尝试这种模式的辍学,看看是否可行:
clf = Classifier(
layers=[
Layer("Rectifier", units=13),
Layer("Rectifier", units=13),
Layer("Softmax")],
learning_rate=0.01,
n_iter=2000,
learning_rule='nesterov',
regularize='dropout', #here we specify dropout
dropout_rate=.1,#dropout fraction of neural units in entire network
random_state=0)
model1=clf.fit(X_trainn, y_train)
scores = cross_validation.cross_val_score(clf, X_trainn, y_train, cv=5)
print np.mean(scores)
y_hat=clf.predict(X_testn)
print accuracy_score(y_hat,y_test)
OUTPUT]
dropout train score 0.933151515152
dropout test score 0.866666666667
在这种情况下,辍学并没有带来令人满意的结果,所以我们应该把它完全排除在外。也可以用其他方法随意试验。只需更改learning_rule参数,看看它对整体精度有何影响。可以尝试的车型有sgd、momentum、nesterov、adagrad、rmsprop。从这个例子中,您已经了解到内斯特罗夫动量可以提高整体精度。在这种情况下,dropout不是最佳的正则化方法,并且对模型性能有害。考虑到这大量的参数都相互作用并产生不可预测的结果,我们确实需要一种调优方法。这正是我们下一节要做的。
神经网络与超参数优化
由于神经网络和深度学习模型的参数空间如此之广,优化是一项艰巨的任务并且计算非常昂贵。错误的神经网络架构可能会导致失败。只有当我们应用正确的参数并为我们的问题选择正确的架构时,这些模型才能准确。不幸的是,提供调整方法的应用很少。我们发现目前最佳的参数调整方法是随机搜索,这是一种随机迭代参数空间的算法,节省了计算资源。sknn 库是唯一有这个选项的库。让我们以葡萄酒质量数据集为基础,通过下面的示例来浏览参数调整方法。
在这个例子中,我们首先加载葡萄酒数据集。然后我们对数据进行转换,从那里我们根据选择的参数调整模型。请注意,该数据集有 13 个要素;我们指定每层中的单位在 4 到 20 之间。在这种情况下,我们不使用小批量;数据集太小了:
import numpy as np
import scipy as sp
import pandas as pd
from sklearn.grid_search import RandomizedSearchCV
from sklearn.grid_search import GridSearchCV, RandomizedSearchCV
from scipy import stats
from sklearn.cross_validation import train_test_split
from sknn.mlp import Layer, Regressor, Classifier as skClassifier
# Load data
df = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv ' , sep = ';')
X = df.drop('quality' , 1).values # drop target variable
y1 = df['quality'].values # original target variable
y = y1 <= 5 # new target variable: is the rating <= 5?
# Split the data into a test set and a training set
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print X_train.shape
max_net = skClassifier(layers= [Layer("Rectifier",units=10),
Layer("Rectifier",units=10),
Layer("Rectifier",units=10),
Layer("Softmax")])
params={'learning_rate': sp.stats.uniform(0.001, 0.05,.1),
'hidden0__units': sp.stats.randint(4, 20),
'hidden0__type': ["Rectifier"],
'hidden1__units': sp.stats.randint(4, 20),
'hidden1__type': ["Rectifier"],
'hidden2__units': sp.stats.randint(4, 20),
'hidden2__type': ["Rectifier"],
'batch_size':sp.stats.randint(10,1000),
'learning_rule':["adagrad","rmsprop","sgd"]}
max_net2 = RandomizedSearchCV(max_net,param_distributions=params,n_iter=25,cv=3,scoring='accuracy',verbose=100,n_jobs=1,\
pre_dispatch=None)
model_tuning=max_net2.fit(X_train,y_train)
print "best score %s" % model_tuning.best_score_
print "best parameters %s" % model_tuning.best_params_
OUTPUT:]
[CV] hidden0__units=11, learning_rate=0.100932183167, hidden2__units=4, hidden2__type=Rectifier, batch_size=30, hidden1__units=11, learning_rule=adagrad, hidden1__type=Rectifier, hidden0__type=Rectifier, score=0.655914 - 3.0s
[Parallel(n_jobs=1)]: Done 74 tasks | elapsed: 3.0min
[CV] hidden0__units=11, learning_rate=0.100932183167, hidden2__units=4, hidden2__type=Rectifier, batch_size=30, hidden1__units=11, learning_rule=adagrad, hidden1__type=Rectifier, hidden0__type=Rectifier
[CV] hidden0__units=11, learning_rate=0.100932183167, hidden2__units=4, hidden2__type=Rectifier, batch_size=30, hidden1__units=11, learning_rule=adagrad, hidden1__type=Rectifier, hidden0__type=Rectifier, score=0.750000 - 3.3s
[Parallel(n_jobs=1)]: Done 75 tasks | elapsed: 3.0min
[Parallel(n_jobs=1)]: Done 75 out of 75 | elapsed: 3.0min finished
best score 0.721366278222
best parameters {'hidden0__units': 14, 'learning_rate': 0.03202394348494512, 'hidden2__units': 19, 'hidden2__type': 'Rectifier', 'batch_size': 30, 'hidden1__units': 17, 'learning_rule': 'adagrad', 'hidden1__type': 'Rectifier', 'hidden0__type': 'Rectifier'}
注
警告:由于参数空间是随机搜索的,结果可能不一致。
我们可以看到我们模型的最佳参数是,最重要的是,第一层包含 14 个单位,第二层包含 17 个单位,第三层包含 19 个单位。这是一个相当复杂的架构,我们可能永远无法自己推导出来,这证明了超参数优化的重要性。
神经网络和决策边界
我们在上一节已经介绍过,通过向神经网络添加隐藏单元,我们可以更接近地逼近目标函数。然而,我们还没有将其应用于分类问题。为此,我们将生成具有非线性目标值的数据,并查看一旦我们向架构中添加隐藏单元,决策面将如何变化。让我们看看普适近似定理在起作用!首先,让我们生成一些具有两个特征的非线性可分离数据,设置我们的神经网络体系结构,看看我们的决策边界如何随着每个体系结构而变化:
%matplotlib inline
from sknn.mlp import Classifier, Layer
from sklearn import preprocessing
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from itertools import product
X,y= datasets.make_moons(n_samples=500, noise=.2, random_state=222)
from sklearn.datasets import make_blobs
net1 = Classifier(
layers=[
Layer("Softmax")],random_state=222,
learning_rate=0.01,
n_iter=100)
net2 = Classifier(
layers=[
Layer("Rectifier", units=4),
Layer("Softmax")],random_state=12,
learning_rate=0.01,
n_iter=100)
net3 =Classifier(
layers=[
Layer("Rectifier", units=4),
Layer("Rectifier", units=4),
Layer("Softmax")],random_state=22,
learning_rate=0.01,
n_iter=100)
net4 =Classifier(
layers=[
Layer("Rectifier", units=4),
Layer("Rectifier", units=4),
Layer("Rectifier", units=4),
Layer("Rectifier", units=4),
Layer("Rectifier", units=4),
Layer("Rectifier", units=4),
Layer("Softmax")],random_state=62,
learning_rate=0.01,
n_iter=100)
net1.fit(X, y)
net2.fit(X, y)
net3.fit(X, y)
net4.fit(X, y)
# Plotting decision regions
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))
f, arxxx = plt.subplots(2, 2, sharey='row',sharex='col', figsize=(8, 8))
plt.suptitle('Neural Network - Decision Boundary')
for idx, clf, ti in zip(product([0, 1], [0, 1]),
[net1, net2, net3,net4],
['0 hidden layer', '1 hidden layer',
'2 hidden layers','6 hidden layers']):
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
arxxx[idx[0], idx[1]].contourf(xx, yy, Z, alpha=0.5)
arxxx[idx[0], idx[1]].scatter(X[:, 0], X[:, 1], c=y, alpha=0.5)
arxxx[idx[0], idx[1]].set_title(ti)
plt.show()

在这张截图中,我们可以看到,随着我们向神经网络添加隐藏层,我们可以越来越多地学习复杂的决策边界。一个有趣的边注是,具有两层的网络产生了最准确的预测。
注
请注意,结果可能在运行之间有所不同。
与 H2O 进行大规模深度学习
在前几节中,我们介绍了在本地计算机上运行的神经网络和深度架构,我们发现神经网络已经高度矢量化,但计算成本仍然很高。如果我们想使该算法在台式计算机上更具可扩展性,除了利用 Anano 和 GPU 计算之外,我们无能为力。因此,如果我们想更彻底地扩展深度学习算法,我们需要找到一种工具,可以在内核外运行算法,而不是在本地 CPU/GPU 上运行。此时此刻,H2O 是唯一能够快速运行深度学习算法的开源核心外平台。也是跨平台的;除了 Python,还有针对 R、Scala 和 Java 的 API。
H2O 是在一个基于 Java 的平台上编译的,该平台是为广泛的数据科学相关任务开发的,例如数据处理和机器学习。H2O 在内存中的分布式并行 CPU 上运行,因此数据将存储在 H2O 集群中。到目前为止,H2O 平台已经应用了通用线性模型(【GLM】)、随机森林、梯度增强机器 ( GBM )、K 均值、朴素贝叶斯、主成分分析、主成分回归,当然,我们本章的主要重点是深度学习。
太好了,现在我们准备进行第一次 H2O 核外分析。
让我们启动 H2O 实例,并在 H2O 的分布式内存系统中加载一个文件:
import sys
sys.prefix = "/usr/local"
import h2o
h2o.init(start_h2o=True)
Type this to get interesting information about the specifications of your cluster.
Look at the memory that is allowed and the number of cores.
h2o.cluster_info()
这看起来或多或少类似于以下内容(试验和系统之间可能会有细微差异):
OUTPUT:]
Java Version: java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
Starting H2O JVM and connecting: .................. Connection successful!
------------------------------ ---------------------------------------
H2O cluster uptime: 2 seconds 346 milliseconds
H2O cluster version: 3.8.2.3
H2O cluster name: H2O_started_from_python**********nzb520
H2O cluster total nodes: 1
H2O cluster total free memory: 3.56 GB
H2O cluster total cores: 8
H2O cluster allowed cores: 8
H2O cluster healthy: True
H2O Connection ip: 1**.***.***.***
H2O Connection port: 54321
H2O Connection proxy:
Python Version: 2.7.10
------------------------------ ---------------------------------------
------------------------------ ---------------------------------------
H2O cluster uptime: 2 seconds 484 milliseconds
H2O cluster version: 3.8.2.3
H2O cluster name: H2O_started_from_python_quandbee_nzb520
H2O cluster total nodes: 1
H2O cluster total free memory: 3.56 GB
H2O cluster total cores: 8
H2O cluster allowed cores: 8
H2O cluster healthy: True
H2O Connection ip: 1**.***.***.***
H2O Connection port: 54321
H2O Connection proxy:
Python Version: 2.7.10
------------------------------ ---------------------------------------
Sucessfully closed the H2O Session.
Successfully stopped H2O JVM started by the h2o python module.
与 H2O 进行大规模深度学习
在 H2O 深度学习中,我们将用来训练的数据集是著名的 MNIST 数据集。它由 28×28 手写数字图像的像素强度组成。训练集合有 70,000 个训练项目,784 个特征,以及包含目标标签数字的每个记录的标签。
现在,我们对在 H2O 管理数据更加放心了,让我们执行一个深入的学习示例。
在 H2O,我们不需要转换或标准化输入数据;它是内部自动标准化的。每个特征被变换到 N(0,1)空间。
让我们将著名的手写数字图像数据集 MNIST 从亚马逊服务器导入到 H2O 集群:
import h2o
h2o.init(start_h2o=True)
train_url ="https://h2o-public-test-data.s3.amazonaws.com/bigdata/laptop/mnist/train.csv.gz"
test_url="https://h2o-public-test-data.s3.amazonaws.com/bigdata/laptop/mnist/test.csv.gz"
train=h2o.import_file(train_url)
test=h2o.import_file(test_url)
train.describe()
test.describe()
y='C785'
x=train.names[0:784]
train[y]=train[y].asfactor()
test[y]=test[y].asfactor()
from h2o.estimators.deeplearning import H2ODeepLearningEstimator
model_cv=H2ODeepLearningEstimator(distribution='multinomial'
,activation='RectifierWithDropout',hidden=[32,32,32],
input_dropout_ratio=.2,
sparse=True,
l1=.0005,
epochs=5)
这个打印模型的输出将提供很多详细的信息。您将看到的第一个表格如下。这提供了关于神经网络架构的所有细节。您可以看到,我们使用了一个输入维数为 717 的神经网络,该网络有三个隐藏层(每个隐藏层由 32 个单元组成),softmax 激活应用于输出层,ReLU 应用于隐藏层之间:
model_cv.train(x=x,y=y,training_frame=train,nfolds=3)
print model_cv
OUTPUT]

如果你想要一个简短的模型性能概述,这是一个非常实用的方法。
在下表中,最有趣的指标是每个文件夹的训练分类错误和验证分类错误。如果您想要验证您的模型,您可以很容易地比较这些:
print model_cv.scoring_history()

我们在 MNIST 数据集上的训练分类误差 .096430 和精度在. 907 范围内是相当好的;这几乎和 Yann LeCun 的卷积神经网络提交一样好。
H2O 还提供了一种获取验证指标的便捷方法。我们可以通过将验证数据框传递给交叉验证函数来实现这一点:
model_cv.train(x=x,y=y,training_frame=train,validation_frame=test,nfolds=3)
print model_cv

在这种情况下,我们可以很容易地将训练 _ 分类 _ 错误 ( .089)与我们的验证 _ 分类 _ 错误 ( .0954)进行比较。
也许我们可以提高我们的分数;让我们使用超参数优化模型。
H2O 的网格研究
考虑到我们之前的模型表现相当好,我们将把我们的调优工作集中在我们网络的架构上。H2O 的 gridsearch 功能与 Scikit-learn 的随机化搜索非常相似;也就是说,它不是搜索整个参数空间,而是遍历随机的参数列表。首先,我们将设置一个传递给 gridsearch 函数的参数列表。H2O 将在参数搜索中为我们提供每个模型的输出和相应的分数:
hidden_opt = [[18,18],[32,32],[32,32,32],[100,100,100]]
# l1_opt = [s/1e6 for s in range(1,1001)]
# hyper_parameters = {"hidden":hidden_opt, "l1":l1_opt}
hyper_parameters = {"hidden":hidden_opt}
#important: here we specify the search parameters
#be careful with these, training time can explode (see max_models)
search_c = {"strategy":"RandomDiscrete",
"max_models":10, "max_runtime_secs":100,
"seed":222}
from h2o.grid.grid_search import H2OGridSearch
model_grid = H2OGridSearch(H2ODeepLearningEstimator, hyper_params=hyper_parameters)
#We have added a validation set to the gridsearch method in order to have a better #estimate of the model performance.
model_grid.train(x=x, y=y, distribution="multinomial", epochs=1000, training_frame=train, validation_frame=test,
score_interval=2, stopping_rounds=3, stopping_tolerance=0.05,search_criteria=search_c)
print model_grid
# Grid Search Results for H2ODeepLearningEstimator:
OUTPUT]
deeplearning Grid Build Progress: [##################################################] 100%
hidden \
0 [100, 100, 100]
1 [32, 32, 32]
2 [32, 32]
3 [18, 18]
model_ids logloss
0 Grid_DeepLearning_py_1_model_python_1464790287811_3_model_3 0.148162 ←------
1 Grid_DeepLearning_py_1_model_python_1464790287811_3_model_2 0.173675
2 Grid_DeepLearning_py_1_model_python_1464790287811_3_model_1 0.212246
3 Grid_DeepLearning_py_1_model_python_1464790287811_3_model_0 0.227706
我们可以看到,我们最好的架构是一个三层结构,每层 100 个单元。我们还可以清楚地看到【gridsearch 甚至在上大幅增加了训练时间,T1 是一个强大的计算集群,就像 H2O 运行的集群一样。因此,即使在 H2O,我们也应该谨慎使用 gridsearch,并对模型中解析的参数保持保守。
现在,让我们在继续之前关闭 H2O 实例:
h2o.shutdown(prompt=False)
深度学习和无监督预处理
在这一节中,我们将介绍深度学习中最重要的概念:如何通过无监督预训练来提高学习。通过无监督的预处理,我们使用神经网络来发现数据中的潜在特征和因素,以便稍后传递给神经网络。这种方法具有强大的训练网络的能力来学习其他机器学习方法不能学习的任务,没有手工特征。我们将进入细节,并介绍一个新的强大的库。
用茶氨酸进行深度学习
Scikit-learn 的神经网络应用对于参数调整特别有意思。不幸的是,它对于无监督神经网络应用的能力是有限的。对于下一个主题,我们深入更复杂的学习方法,我们需要另一个库。在这一章中,我们将集中讨论茶氨酸。我们喜欢茶氨酸是因为的易用性和稳定性;这是得克萨斯大学的利夫·约翰逊开发的一个非常流畅且维护良好的软件包。建立一个神经网络结构的工作原理与 sklearn 非常相似;也就是说,我们实例化一个学习目标(分类或回归),指定层,并训练它。更多信息,可登陆http://theanets.readthedocs.org/en/stable/。
你所要做的就是用pip安装天线:
$ pip install theanets
由于茶氨酸是建立在茶氨酸之上的,你也需要正确安装茶氨酸。让我们运行一个基本的神经网络模型来看看茶氨酸是如何工作的。与 Scikit-learn 的相似之处显而易见。请注意,我们在本例中使用动量,默认情况下,在示例中使用 softmax,因此我们不必指定它:
import climate # This package provides the reporting of iterations
from sklearn.metrics import confusion_matrix
import numpy as np
from sklearn import datasets
from sklearn.cross_validation import train_test_split
from sklearn.metrics import mean_squared_error
import theanets
import theano
import numpy as np
import matplotlib.pyplot as plt
import climate
from sklearn.cross_validation import train_test_split
import theanets
from sklearn.metrics import confusion_matrix
from sklearn import preprocessing
from sklearn.metrics import accuracy_score
from sklearn import datasets
climate.enable_default_logging()
digits = datasets.load_digits()
digits = datasets.load_digits()
X = np.asarray(digits.data, 'float32')
Y = digits.target
Y=np.array(Y, dtype=np.int32)
#X = (X - np.min(X, 0)) / (np.max(X, 0) + 0.0001) # 0-1 scaling
X_train, X_test, y_train, y_test = train_test_split(X, Y,
test_size=0.2,
random_state=0)
# Build a classifier model with 64 inputs, 1 hidden layer with 100 units and 10 outputs.
net = theanets.Classifier([64,100,10])
# Train the model using Resilient backpropagation and momentum.
net.train([X_train,y_train], algo='sgd', learning_rate=.001, momentum=0.9,patience=0,
validate_every=N,
min_improvement=0.8)
# Show confusion matrices on the training/validation splits.
print(confusion_matrix(y_test, net.predict(X_test)))
print (accuracy_score(y_test, net.predict(X_test)))
OUTPUT ]
[[27 0 0 0 0 0 0 0 0 0]
[ 0 32 0 0 0 1 0 0 0 2]
[ 0 1 34 0 0 0 0 1 0 0]
[ 0 0 0 29 0 0 0 0 0 0]
[ 0 0 0 0 29 0 0 1 0 0]
[ 0 0 0 0 0 38 0 0 0 2]
[ 0 1 0 0 0 0 43 0 0 0]
[ 0 0 0 0 1 0 0 38 0 0]
[ 0 2 1 0 0 0 0 0 36 0]
[ 0 0 0 0 0 1 0 0 0 40]]
0.961111111111
自动编码器和无监督学习
到目前为止,我们讨论了多层和多种参数优化的神经网络。我们常说的深度学习这一代神经网络能力更强;它能够自动学习新特性,因此只需要很少的特性工程和领域专业知识。这些特征是通过无监督的方法在未标记的数据上创建的,这些数据随后被输入到神经网络的后续层。这种方法被称为(无监督)预处理。这种方法已经被证明在图像识别、语言学习甚至普通的机器学习项目中非常成功。近年来最重要和占主导地位的技术被称为去噪自动编码器和基于玻尔兹曼技术的算法。玻尔兹曼机器,曾经是深度信念网络 ( DBN )的构建模块,最近在深度学习社区中失宠了,因为它们被证明很难训练和优化。因此,我们将只关注自动编码器。让我们以可管理的小步骤来讨论这个重要的话题。
自动编码器
我们试图找到一个函数( F ),有一个输出作为它的输入,误差最小F(x)∞' x。该功能通常被称为身份功能,我们尝试对其进行优化,使 x 尽可能接近 'x 。 x 与 'x 之间的差异称为重构误差。
让我们看一个简单的单层体系结构来直观地了解发生了什么。我们将看到这些架构非常灵活,需要仔细调整:

单层自动编码器架构
重要的是要理解,当我们在隐藏层中的单位少于输入空间时,我们会强制权重来压缩输入数据。
在本例中,我们有一个包含五个要素的数据集。中间是包含三个单位( Wij )的隐藏层。这些单位具有与我们在神经网络中看到的权重向量相同的属性;也就是说,它们由可以用反向传播训练的权重组成。利用隐藏层的输出,我们通过与我们在神经网络中看到的相同的前馈向量操作获得作为输出的特征表示。
计算向量 'x 的过程与我们通过计算各层权重向量的点积所看到的正向传播非常相似:
W=重量


重建误差可以用平方误差或交叉熵的形式来测量,这在许多其他方法中已经看到。在这种情况下, ŷ 代表重构输出, y 代表真实输入:

一个重要的概念是,在只有一个隐藏层的情况下,自动编码器模型捕获的数据中的维度近似于主成分分析 ( 主成分分析)的结果。然而,如果涉及到非线性,自动编码器的行为会大不相同。自动编码器将检测主成分分析永远无法检测到的不同潜在因素。既然我们对自动编码器的架构有了更多的了解,也知道了如何根据其同一性近似计算误差,那么让我们看看这些稀疏度参数,我们用它们来压缩输入。
你可能会问:为什么我们甚至需要这个稀缺参数?我们就不能运行算法找到身份函数然后继续前进吗?
不幸的是,事情没那么简单。有些情况下,身份函数几乎完美地投射了输入,但仍然无法提取输入特征的潜在维度。在这种情况下,该函数只是存储输入数据,而不是提取有意义的特征。我们可以做两件事。首先,我们故意给信号添加噪声(去噪自动编码器),其次,我们引入稀疏性参数,强制停用弱激活单元。让我们首先看看稀疏性是如何工作的。
我们讨论了生物神经元的激活阈值;如果一个神经元的电位接近 1,我们可以认为它是活动的,如果它的输出值接近 0,我们可以认为它是不活动的。我们可以通过提高激活阈值来限制神经元在大部分时间不活动。我们通过降低每个神经元/单位的平均激活概率来做到这一点。查看以下公式,我们可以了解如何最小化激活阈值:

j :隐藏层每个神经元的平均激活阈值。
ρ :我们预先指定的网络期望激活阈值。在大多数情况下,该值设置为. 05。
a :隐藏图层的权重向量。
在这里,我们看到了一个优化的机会,通过对
【j】和 ρ 之间的错误率进行惩罚。
在本章中,我们不会太担心这个优化目标的技术细节。在大多数包中,我们可以使用一个非常简单的指令来做到这一点(我们将在下一个例子中看到)。需要理解的最重要的一点是,对于自动编码器,我们有两个主要的学习目标:通过优化身份函数来最小化输入向量 x 和输出向量' x 之间的误差,以及最小化网络中每个神经元的期望激活阈值和平均激活之间的差异。
我们可以强制自动编码器检测潜在特征的第二种方法是在模型中引入噪声;这就是名字去噪自动编码器的由来。这个想法是通过破坏输入的 T4,我们迫使自动编码器学习更健壮的 T5 数据表示。在接下来的例子中,我们将简单地把高斯噪声引入自动编码器模型。
利用堆叠去噪自动编码器进行真正深度的学习–分类预处理
通过这个练习,你将把自己从众多谈论深度学习的人和少数真正做到的人中区分开来!现在,我们将对著名的 MNIST 数据集的迷你版本应用自动编码器,它可以方便地从 Scikit-learn 中加载。数据集由 28 x 28 个手写数字图像的像素强度组成。训练集有 1,797 个训练项目,64 个特征,每个记录的标签包含目标标签数字,从 0 到 9。所以我们有 64 个特征,目标变量由 10 个类组成(数字从 0-9)来预测。
首先,我们训练稀疏度为. 9 的堆叠去噪自动编码器模型,并检查重建误差。我们将使用深度学习研究论文的结果作为设置的指南。更多信息可以阅读本文(http://arxiv.org/pdf/1312.5663.pdf)。然而,我们有一些限制,因为这些类型的模型计算量巨大。因此,对于这个自动编码器,我们使用五层 ReLU 激活,并将数据从 64 个特征压缩到 45 个特征:
model = theanets.Autoencoder([64,(45,'relu'),(45,'relu'),(45,'relu'),(45,'relu'),(45,'relu'),64])
dAE_model=model.train([X_train],algo='rmsprop',input_noise=0.1,hidden_l1=.001,sparsity=0.9,num_updates=1000)
X_dAE=model.encode(X_train)
X_dAE=np.asarray(X_dAE, 'float32')
:OUTPUT:
I 2016-04-20 05:13:37 downhill.base:232 RMSProp 2639 loss=0.660185 err=0.645118
I 2016-04-20 05:13:37 downhill.base:232 RMSProp 2640 loss=0.660031 err=0.644968
I 2016-04-20 05:13:37 downhill.base:232 validation 264 loss=0.660188 err=0.645123
I 2016-04-20 05:13:37 downhill.base:414 patience elapsed!
I 2016-04-20 05:13:37 theanets.graph:447 building computation graph
I 2016-04-20 05:13:37 theanets.losses:67 using loss: 1.0 * MeanSquaredError (output out:out)
I 2016-04-20 05:13:37 theanets.graph:551 compiling feed_forward function
现在我们有了自动编码器的输出,它是我们用一组新的压缩特征创建的。让我们仔细看看这个新数据集:
X_dAE.shape
Output: (1437, 45)
在这里,我们实际上可以看到,我们已经将数据从 64 个特征压缩到 45 个特征。新的数据集不那么稀疏(意味着更少的零)并且在数字上更加连续。既然我们已经从自动编码器获得了预训练数据,我们可以对其应用深度神经网络进行监督学习:
#By default, hidden layers use the relu transfer function so we don't need to specify #them. Relu is the best option for auto-encoders.
# Theanets classifier also uses softmax by default so we don't need to specify them.
net = theanets.Classifier(layers=(45,45,45,10))
autoe=net.train([X_dAE, y_train], algo='rmsprop',learning_rate=.0001,batch_size=110,min_improvement=.0001,momentum=.9,
nesterov=True,num_updates=1000)
## Enjoy the rare pleasure of 100% accuracy on the training set.
OUTPUT:
I 2016-04-19 10:33:07 downhill.base:232 RMSProp 14074 loss=0.000000 err=0.000000 acc=1.000000
I 2016-04-19 10:33:07 downhill.base:232 RMSProp 14075 loss=0.000000 err=0.000000 acc=1.000000
I 2016-04-19 10:33:07 downhill.base:232 RMSProp 14076 loss=0.000000 err=0.000000 acc=1.000000
在我们对测试集预测这个神经网络之前,将我们已经训练的自动编码器模型应用于测试集是很重要的:
dAE_model=model.train([X_test],algo='rmsprop',input_noise=0.1,hidden_l1=.001,sparsity=0.9,num_updates=100)
X_dAE2=model.encode(X_test)
X_dAE2=np.asarray(X_dAE2, 'float32')
现在让我们检查一下测试集的性能:
final=net.predict(X_dAE2)
from sklearn.metrics import accuracy_score
print accuracy_score(final,y_test)
OUTPUT: 0.972222222222
我们可以看到,具有自动编码特征 (.9722)的模型的最终精度优于没有它的模型(. 9611)。
总结
在这一章中,我们研究了深度学习背后最重要的概念以及可扩展的解决方案。
我们通过学习如何为任何给定的任务构建正确的体系结构来消除一些黑箱现象,并通过前向传播和反向传播的机制来工作。更新神经网络的权重是一项艰巨的任务,常规的随机梯度下降会导致陷入全局最小值或超调。动量、ADAGRAD、RPROP 和 RMSProp 等更复杂的算法可以提供解决方案。尽管神经网络比其他机器学习方法更难训练,但它们具有转换特征表示的能力,并且可以学习任何给定的函数(通用近似定理)。我们还和 H2O 一起投入到大规模的深度学习中,甚至利用参数优化这个非常热门的话题进行深度学习。
使用自动编码器的无监督预训练可以提高任何给定深度网络的准确性,我们通过一个茶氨酸框架内的实际例子来实现这一点。
在这一章中,我们主要使用构建在 Anano 框架之上的包。在下一章中,我们将介绍基于新的开源框架 Tensorflow 构建的包的深度学习技术。
五、TensorFlow 深度学习
在本章中,我们将重点介绍 TensorFlow,并涵盖以下主题:
- 基本 TensorFlow 运算
- 使用 TensorFlow 从零开始的机器学习—回归、SGD 分类器和神经网络
- 使用 SkFlow 进行深度学习
- 大型文件的增量深度学习
- 带 Keras 的卷积神经网络
TensorFlow 框架是在撰写本书时引入的,并且已经被证明是机器学习领域的一大补充。
TensorFlow 是由谷歌大脑团队发起的,该团队由近十年来从事深度学习重要发展的大多数研究人员(杰弗里·辛顿、萨米·本吉奥等人)组成。它基本上是上一代框架的下一代开发,称为 dist 信仰,是分布式深度神经网络的平台。与 TensorFlow 相反,diststrust不是开源的。diststrust 项目成功的有趣例子是反转图像搜索引擎、谷歌深梦和谷歌应用中的语音识别。DistBelief 使谷歌开发人员能够利用数千个内核(包括 CPU 和 GPU)进行分布式训练。
TensorFlow 是对 diststrust 的改进,因为它现在是完全开源的,并且它的编程语言不那么抽象。TensorFlow 声称更灵活,应用范围更广。在撰写本文时(2015 年末),TensorFlow 框架还处于起步阶段,基于 TensorFlow 构建的有趣的轻量级包已经出现,我们将在本章后面部分看到。
与 Anano 类似,TensorFlow 在张量上进行符号计算;这意味着它的大部分计算都是基于向量和矩阵乘法。
常规编程语言定义了变量,这些变量包含可以应用操作的值或字符。
在符号编程语言中,如 Anano 或 TensorFlow,操作是围绕图而不是变量来构造的。这具有计算优势,因为它们可以跨计算单元(GPU 和 CPU)分布和并行化:

TensorFlow 的架构于 2015 年 11 月推出
TensorFlow 具有以下特征和应用:
- TensorFlow 可以用多个图形处理器并行化(水平)
- 开发框架也可用于移动部署
- TensorBoard 是一个用于可视化的仪表盘(处于早期阶段)
- 它是几种编程语言(Python Go、Java、Lua、JavaScript、R、C++,很快还有 Julia)的前端
- 为 Spark、谷歌云平台(https://cloud.google.com/ml/)等大规模解决方案提供集成
类似于图的结构中的张量运算提供了并行计算的新方法(因此谷歌声称)的想法可以通过下面的图片变得非常清楚:

一种基于 TensorFlow 的分布式处理架构
从这张图片中我们可以看到,每个模型都可以分配给单独的图形处理器。之后根据每个模型计算预测的平均值。在其他方法中,这种方法是在 GPU 集群上训练非常大的分布式神经网络的核心思想。
tensorlow 安装
我们将在本章中使用的 TensorFlow 版本是 0.8,所以请确保您安装了该版本。由于 TensorFlow 正处于重度开发阶段,小的变化是应该的。借助pip install,我们可以非常轻松地安装 TensorFlow,而与您使用的操作系统无关:
pip install tensorflow
如果您已经安装了以前的版本,您可以根据您的操作系统进行升级:
# Ubuntu/Linux 64-bit, CPU only:
$ sudo pip install --upgrade https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.8.1-cp27-none-linux_x86_64.whl
# Ubuntu/Linux 64-bit, GPU enabled:
$ sudo pip install --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.8.1-cp27-none-linux_x86_64.whl
# Mac OS X, CPU only:
$ sudo easy_install --upgrade six
$ sudo pip install --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.1-cp27-none-any.whl
现在安装了 TensorFlow,可以在终端进行测试:
$python
import tensorflow as tf
hello = tf.constant('Hello, TensorFlow!')
sess = tf.Session()
print(sess.run(hello))
Output Hello, TensorFlow!
TensorFlow 运算
让我们通过一些简单的例子来感受一下它是如何工作的。
一个重要的区别是,对于 TensorFlow,我们首先需要初始化变量,然后才能对它们应用操作。TensorFlow 在 C++后端运行以执行计算,因此,为了连接到这个后端,我们需要首先实例化一个会话:
x = tf.constant([22,21,32], name='x')
d=tf.constant([12,23,43],name='x')
y = tf.Variable(x * d, name='y')
print(y)
不提供 xd* 的输出向量,您将看到如下内容:
OUTPUT ]
<tensorflow.python.ops.variables.Variable object at 0x114a95710>
为了从 C++后端实际生成提供的计算结果,我们用以下方式实例化会话:
x = tf.constant([22,21,32], name='x')
d=tf.constant([12,23,43],name='d')
y = tf.Variable(x * d, name='y')
model = tf.initialize_all_variables()
with tf.Session() as session:
session.run(model)
print(session.run(y))
Output [ 264 483 1376]
到目前为止,我们已经直接使用了变量,但是为了更灵活地使用张量运算,如果我们可以将数据分配给一个预先指定的容器,这可能会很方便。这样,我们就可以对计算图执行操作,而无需事先将数据加载到内存中。在 TensorFlow 术语中,我们通过所谓的占位符将数据输入到图表中。这正是与安诺语言的相似之处(见附录、图形处理器和安诺简介)。
这些 TensorFlow 占位符只是具有某些预先指定的设置和类的对象的容器。因此,为了对一个对象执行操作,我们首先为该对象创建一个占位符,以及它对应的类(在本例中是一个整数):
a = tf.placeholder(tf.int8)
b = tf.placeholder(tf.int8)
sess = tf.Session()
sess.run(a+b, feed_dict={a: 111, b: 222})
Output 77
矩阵乘法的工作原理如下:
matrix1 = tf.constant([[1, 2,32], [3, 4,2],[3,2,11]])
matrix2 = tf.constant([[21,3,12], [3, 56,2],[35,21,61]])
product = tf.matmul(matrix1, matrix2)
with tf.Session() as sess:
result = sess.run(product)
print result
OUTPUT
[[1147 787 1968]
[ 145 275 166]
[ 454 352 711]]
有趣的是,对象result的输出是一个 NumPy ndarray对象,我们可以对 TensorFlow 之外的对象进行操作。
GPU 计算
如果我们想在图形处理器上执行 TensorFlow 操作,我们只需要指定一个设备。被警告;这仅适用于安装正确、与 CUDA 兼容的 NVIDIA GPU 单元:
with tf.device('/gpu:0'):
product = tf.matmul(matrix1, matrix2)
with tf.Session() as sess:
result = sess.run(product)
print result
如果我们想利用多个图形处理器,我们需要分配一个图形处理器设备给一个特定的任务:
matrix3 = tf.constant([[13, 21,53], [4, 3,6],[3,1,61]])
matrix4 = tf.constant([[13,23,32], [23, 16,2],[35,51,31]])
with tf.device('/gpu:0'):
product = tf.matmul(matrix1, matrix2)
with tf.Session() as sess:
result = sess.run(product)
print result
with tf.device('/gpu:1'):
product = tf.matmul(matrix3, matrix4)
with tf.Session() as sess:
result = sess.run(product)
print result
用 SGD 进行线性回归
既然我们已经涵盖了基础知识,我们可以开始在 TensorFlow 框架内从头开始编写我们的第一个机器学习算法。稍后,我们将在 TensorFlow 之上的更高抽象中使用更实用的轻量级应用。
我们将使用随机梯度下降执行一个非常简单的线性回归,以便了解训练和评估在 TensorFlow 中是如何工作的。首先,我们将创建一些变量来使用,以便在占位符中解析它们来包含这些变量。然后我们将和y输入到cost 功能中,用梯度下降训练模型:
import tensorflow as tf
import numpy as np
X = tf.placeholder("float") # create symbolic variables
Y = tf.placeholder("float")
X_train = np.asarray([1,2.2,3.3,4.1,5.2])
Y_train = np.asarray([2,3,3.3,4.1,3.9,1.6])
def model(X, w):
return tf.mul(X, w)
w = tf.Variable(0.0, name="weights")
y_model = model(X, w) # our predicted values
cost = (tf.pow(Y-y_model, 2)) # squared error cost
train_op = tf.train.GradientDescentOptimizer(0.01).minimize(cost) #sgd optimization
sess = tf.Session()
init = tf.initialize_all_variables()
sess.run(init)
for trials in range(50): #
for (x, y) in zip(X_train, Y_train):
sess.run(train_op, feed_dict={X: x, Y: y})
print(sess.run(w))
OUTPUT ]
0.844732
综上所述,我们用 SGD 进行线性回归的方式如下:首先,我们初始化回归权重(系数),然后在第二步中,我们设置成本函数,以稍后训练和优化梯度下降的函数。最后,我们需要编写一个for循环,以指定我们想要的训练回合数,并计算最终预测。相同的基本结构将在神经网络中变得明显。
TensorFlow 中从头开始的神经网络
现在让我们用 TensorFlow 语言来执行一个神经网络,并剖析这个过程。
在这种情况下,我们还将使用 Iris 数据集和一些 Scikit-learn 应用进行预处理:
import tensorflow as tf
import numpy as np
from sklearn import cross_validation
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.utils import shuffle
from sklearn import preprocessing
import os
import pandas as pd
from datetime import datetime as dt
import logging
iris = datasets.load_iris()
X = np.asarray(iris.data, 'float32')
Y = iris.target
from sklearn import preprocessing
X= preprocessing.scale(X)
min_max_scaler = preprocessing.MinMaxScaler()
X = min_max_scaler.fit_transform(X)
lb = preprocessing.LabelBinarizer()
Y=lb.fit_transform(iris.target)
这是重要的一步。TensorFlow 中的神经网络不能处理单个向量中的目标标签。目标标签需要被转换成二进制化的特征(有些人将此称为虚拟变量),这样神经网络将使用一对所有输出:
X_train, x_test, y_train, y_test = train_test_split(X,Y,test_size=0.3,random_state=22)
def init_weights(shape):
return tf.Variable(tf.random_normal(shape, stddev=0.01))
在这里,我们可以看到前馈通道:
def model(X, w_h, w_o):
h = tf.nn.sigmoid(tf.matmul(X, w_h))
return tf.matmul(h, w_o)
X = tf.placeholder("float", [None, 4])
Y = tf.placeholder("float", [None, 3])
在这里,我们用一个隐藏层建立了我们的层架构:
w_h = init_weights([4, 4])
w_o = init_weights([4, 3])
py_x = model(X, w_h, w_o)
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y)) # compute costs
train_op = tf.train.GradientDescentOptimizer(learning_rate=0.01).minimize(cost) # construct an optimizer
predict_op = tf.argmax(py_x, 1)
sess = tf.Session()
init = tf.initialize_all_variables()
sess.run(init)
for i in range(500):
for start, end in zip(range(0, len(X_train),1 ), range(1, len(X_train),1)):
sess.run(train_op, feed_dict={X: X_train[start:end], Y: y_train[start:end]})
if i % 100 == 0:
print i, np.mean(np.argmax(y_test, axis=1) ==
sess.run(predict_op, feed_dict={X: x_test, Y: y_test}))
OUTPUT:]
0 0.288888888889
100 0.666666666667
200 0.933333333333
300 0.977777777778
400 0.977777777778
该神经网络的精度约为 0.977%,但在次运行中会产生略有不同的结果。它或多或少是具有单一隐藏层和普通 SGD 的神经网络的基准。
就像我们在前面的例子中看到的,实现一个优化方法并设置张量是非常直观的。这比我们在 NumPy 中做同样的事情要直观得多。(参见第 4 章、神经网络和深度学习。)目前的缺点是评估和预测需要一个有时很繁琐的for循环,而像 Scikit-learn 这样的包可以用一行简单的脚本提供这些方法。幸运的是,在 TensorFlow 的基础上开发了更高级别的包,使得培训和评估变得更加容易。其中一个包是 SkFlow 顾名思义,它是一个基于脚本风格的包装器,就像 Scikit-learn 一样工作。
基于 SkFlow 的 TensorFlow 机器学习
现在我们已经看到了 TensorFlow 的基本操作,让我们深入到构建在 TensorFlow 之上的更高级别的应用中去让机器学习更实用一点。SkFlow 是我们将介绍的第一个应用。在 SkFlow 中,我们不必指定类型和占位符。我们可以像使用 Scikit-learn 和 NumPy 一样加载和管理数据。让我们用pip安装软件包。
最安全的方法是直接从 GitHub 安装软件包:
$ pip install git+git://github.com/tensorflow/skflow.git
SkFlow 有三类主要的学习算法:线性分类器、线性回归和神经网络。线性分类器基本上是一个简单的 SGD(多)分类器,而神经网络是 SkFlow 的强项。它为深度神经网络、循环网络和卷积神经网络提供了相对容易使用的包装器。不幸的是,其他算法,如随机森林、梯度增强、SVM 和朴素贝叶斯还没有实现。然而,在 GitHub 上有关于在 SkFlow 中实现随机森林算法的讨论,该算法可能会被命名为 tf_forest,这是一个令人兴奋的发展。
让我们在 SkFlow 中应用我们的第一个多类分类算法。在这个例子中,我们将使用葡萄酒数据集——一个最初来自 UCI 机器学习存储库的数据集。它由镁、酒精、苹果酸等 13 个连续化学指标组成。这是一个只有 178 个实例的光照数据集和一个有三个类的目标要素。目标变量由三个不同的品种组成。使用十三种化学指标的化学分析,根据各自的品种(用于酿酒的葡萄类型)对葡萄酒进行分类。您可以看到,我们从网址加载数据的方式与我们在 Scikit-learn 环境中工作时的方式相同:
import numpy as np
from sklearn.metrics import accuracy_score
import skflow
import urllib2
url = 'https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/wine.scale'
set1 = urllib2.Request(url)
wine = urllib2.urlopen(set1)
from sklearn.datasets import load_svmlight_file
X_train, y_train = load_svmlight_file(wine)
X_train=X_train.toarray()
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_train,
y_train, test_size=0.30, random_state=4)
classifier = skflow.TensorFlowLinearClassifier(n_classes=4,learning_rate=0.01, optimizer='SGD',continue_training=True, steps=1000)
classifier.fit(X_train, y_train)
score = accuracy_score(y_train, classifier.predict(X_train))
d=classifier.predict(X_test)
print("Accuracy: %f" % score)
c=accuracy_score(d,y_test)
print('validation/test accuracy: %f' % c)
OUTPUT:
Step #1, avg. loss: 1.58672
Step #101, epoch #25, avg. loss: 1.45840
Step #201, epoch #50, avg. loss: 1.09080
Step #301, epoch #75, avg. loss: 0.84564
Step #401, epoch #100, avg. loss: 0.68503
Step #501, epoch #125, avg. loss: 0.57680
Step #601, epoch #150, avg. loss: 0.50120
Step #701, epoch #175, avg. loss: 0.44486
Step #801, epoch #200, avg. loss: 0.40151
Step #901, epoch #225, avg. loss: 0.36760
Accuracy: 0.967742
validation/test accuracy: 0.981481
到现在,这个方法会相当熟悉;这与 Scikit-learn 中的分类器的工作方式基本相同。然而,有两件重要的事情需要注意。使用 SkFlow,我们可以交替使用 NumPy 和 TensorFlow 对象,这样我们就不需要在张量框架中合并和转换对象。这使得通过像 SkFlow 这样的高级方法来使用 TensorFlow 变得更加灵活。要注意的第二件事是,我们将toarray方法应用于主数据对象。这是因为数据集非常稀疏(大量零条目),并且 TensorFlow 不能很好地处理稀疏数据。
神经网络是 TensorFlow 擅长的地方,在 SkFlow 中,训练多层神经网络是非常容易的。让我们对糖尿病数据集执行神经网络。该数据集包含 21 岁以上怀孕女性和皮马遗产的糖尿病指标(二进制目标)诊断特征。亚利桑那州的皮马印第安人是世界上报告的糖尿病患病率最高的人群,因此这个种族群体一直是糖尿病研究的自愿对象。数据集由以下要素组成:
- 怀孕次数
- 口服葡萄糖耐量试验中两小时的血浆葡萄糖浓度
- 舒张压(毫米汞柱)
- 三头肌皮肤褶皱厚度(mm)
- 2 小时血清胰岛素(微克/毫升)
- 体重指数(以千克为单位的体重/(m)^2 的身高)
- 糖尿病谱系功能
- 年龄(岁)
- 类变量(0 或 1)
在本例中,我们首先加载和缩放数据:
import tensorflow
import tensorflow as tf
import numpy as np
import urllib
import skflow
from sklearn.preprocessing import Normalizer
from sklearn import datasets, metrics, cross_validation
from sklearn.cross_validation import train_test_split
# Pima Indians Diabetes dataset (UCI Machine Learning Repository)
url = "http://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data"
# download the file
raw_data = urllib.urlopen(url)
dataset = np.loadtxt(raw_data, delimiter=",")
print(dataset.shape)
X = dataset[:,0:7]
y = dataset[:,8]
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.2,
random_state=0)
from sklearn import preprocessing
X= preprocessing.scale(X)
min_max_scaler = preprocessing.MinMaxScaler()
X = min_max_scaler.fit_transform(X)
这一步很有意思;为了使神经网络更好地收敛,我们可以使用更灵活的衰减率。在训练多层神经网络时,随着时间的推移降低学习速率通常是有帮助的。一般来说,当我们的学习率过高时,我们可能会超过最佳值。另一方面,当学习率太低时,我们会浪费计算资源,陷入局部极小值。指数衰减是一种随着时间降低学习速率的方法,这样当学习速率开始接近最小值时,它会变得更加敏感。有三种常见的实现学习率衰减的方法;即阶跃衰减、1/t 衰减和指数衰减:
指数衰减:a = a0ektT5】
在这种情况下, a 为学习率, k 为超参数, t 为迭代。
在这个例子中,我们将使用指数衰减,因为它似乎对这个数据集非常有效。这就是我们如何实现指数衰减函数(利用 TensorFlow 的内置tf.train.exponential_decay函数):
def exp_decay(global_step):
return tf.train.exponential_decay(
learning_rate=0.01, global_step=global_step,
decay_steps=steps, decay_rate=0.01)
我们现在可以在 TensorFlow 神经网络模型中传递衰减函数。对于这个神经网络,我们将提供一个两层网络,第一层由五个单元组成,第二层由四个单元组成。默认情况下,SkFlow 实现 ReLU 激活功能,因为我们比其他功能(tanh、sigmoid 等)更喜欢它,所以我们坚持使用它。
下面这个例子,我们也可以实现随机梯度下降以外的优化算法。让我们基于迪德里克·金马和吉米·巴()的一篇文章,实现一个名为亚当的自适应算法。
亚当,在阿姆斯特丹大学开发,代表自适应力矩估计。在前一章中,我们看到了 ADAGRAD 是如何工作的——随着时间的推移,当梯度向(希望是)全局最小值移动时,通过降低梯度。Adam 也使用自适应方法,但是结合动量训练的思想,其中考虑了先前的梯度更新:
steps = 5000
classifier = skflow.TensorFlowDNNClassifier(
hidden_units=[5,4],
n_classes=2,
batch_size=300,
steps=steps,
optimizer='Adam',#SGD #RMSProp
learning_rate=exp_decay #here is the decay function
)
classifier.fit(X_train,y_train)
score1a = metrics.accuracy_score(y_train, classifier.predict(X_train))
print("Accuracy: %f" % score1a)
score1b = metrics.accuracy_score(y_test, classifier.predict(X_test))
print("Validation Accuracy: %f" % score1b)
OUTPUT
(768, 9)
Step #1, avg. loss: 12.83679
Step #501, epoch #167, avg. loss: 0.69306
Step #1001, epoch #333, avg. loss: 0.56356
Step #1501, epoch #500, avg. loss: 0.54453
Step #2001, epoch #667, avg. loss: 0.54554
Step #2501, epoch #833, avg. loss: 0.53300
Step #3001, epoch #1000, avg. loss: 0.53266
Step #3501, epoch #1167, avg. loss: 0.52815
Step #4001, epoch #1333, avg. loss: 0.52639
Step #4501, epoch #1500, avg. loss: 0.52721
Accuracy: 0.754072
Validation Accuracy: 0.740260
准确性不是那么有说服力;我们可以通过将主成分分析 ( 主成分分析)应用于输入来提高准确性。在 Stavros J Perantonis 和 Vassilis Virvilis 从 1999 年(http://rexa . info/paper/dc4f 2 babc 5c ca 4534 b 435280 AE 32 f 5816 DDB 53 b 0)撰写的这篇文章中,已经提出该糖尿病数据集在传入神经网络之前从 PCA 维度的降低中获益良多。我们将对此数据集使用 Sciket-learn 管道方法:
from sklearn.decomposition import PCA
from sklearn import linear_model, decomposition, datasets
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
pca = PCA(n_components=4,whiten=True)
lr = pca.fit(X)
classifier = skflow.TensorFlowDNNClassifier(
hidden_units=[5,4],
n_classes=2,
batch_size=300,
steps=steps,
optimizer='Adam',#SGD #RMSProp
learning_rate=exp_decay
)
pipe = Pipeline(steps=[('pca', pca), ('NNET', classifier)])
X_train, X_test, Y_train, Y_test = train_test_split(X, y,
test_size=0.2,
random_state=0)
pipe.fit(X_train, Y_train)
score2 = metrics.accuracy_score(Y_test, pipe.predict(X_test))
print("Accuracy Validation, with pca: %f" % score2)
OUTPUT:
Step #1, avg. loss: 1.07512
Step #501, epoch #167, avg. loss: 0.54236
Step #1001, epoch #333, avg. loss: 0.50186
Step #1501, epoch #500, avg. loss: 0.49243
Step #2001, epoch #667, avg. loss: 0.48541
Step #2501, epoch #833, avg. loss: 0.46982
Step #3001, epoch #1000, avg. loss: 0.47928
Step #3501, epoch #1167, avg. loss: 0.47598
Step #4001, epoch #1333, avg. loss: 0.47464
Step #4501, epoch #1500, avg. loss: 0.47712
Accuracy Validation, with pca: 0.805195
通过简单的主成分分析预处理步骤,我们已经能够相当大地提高神经网络的性能。我们从七个特征减少到四个维度,因此是四个特征。主成分分析通常通过将特征置零来平滑信号,仅使用包含最高特征值的向量来减少特征空间。白化确保特征被转换成零相关特征。这导致更平滑的信号和更小的特征集,使得神经网络能够更快地收敛。有关主成分分析的更详细解释,请参见第 7 章、无监督学习量表。
大文件深度学习——增量学习
到目前为止,我们已经在相对较小的数据集上处理了 SkFlow 上的一些 TensorFlow 操作和机器学习技术。然而这本书讲的是大规模、可扩展的机器学习;在这方面,TensorFlow 框架能为我们提供什么?
直到最近,并行计算还处于起步阶段,不够稳定,不足以在本书中涵盖。没有兼容 CUDA 的 NVIDIA 卡,读者无法访问多 GPU 计算。大型规模的云服务(https://cloud.google.com/products/machine-learning/)或者亚马逊 EC2 都是有相当可观的费用的。这使得我们只有一种方法可以扩展我们的项目——通过增量学习。
一般来说,任何超过计算机可用内存 25%的文件都会导致内存过载问题。因此,如果您有一台 2 GB 的计算机,并且想要将机器学习解决方案应用于 500 MB 的文件,那么是时候开始考虑绕过内存消耗的方法了。
为了防止内存过载,我们建议使用一种核外学习方法,将数据分解成更小的块,以增量方式训练和更新模型。我们在第 2 章、在 Scikit-learn 中介绍的部分拟合方法就是这方面的例子。
SkFlow 还为其所有的机器学习模型提供了一种非常棒的增量学习方法,就像 Scikit-learn 中的部分拟合方法一样。在这一节中,我们将逐步使用深度学习分类器,因为我们认为它是最令人兴奋的。
在本节中,我们将为我们的可扩展和核心外深度学习项目使用两种策略;即增量学习和随机子采样。
首先,我们生成一些数据,然后我们构建一个子样本函数,我们可以从该数据集抽取随机子样本,并在这些子集上增量训练深度学习模型:
import numpy as np
import pandas as pd
import skflow
from sklearn.datasets import make_classification
import random
from sklearn.cross_validation import train_test_split
import gc
import tensorflow as tf
from sklearn.metrics import accuracy_score
首先,我们将生成一些示例数据并将其写入磁盘:
X, y = make_classification(n_samples=5000000,n_features=10, n_classes=4,n_informative=6,random_state=222,n_clusters_per_class=1)
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.2, random_state=22)
Big_trainm=pd.DataFrame(X_train,y_train)
Big_testm = pd.DataFrame(X_test,y_test)
Big_trainm.to_csv('lsml-Bigtrainm', sep=',')
Big_testm.to_csv('lsml-Bigtestm', sep=',')
让我们通过删除所有创建的对象来释放内存。
借助gc.collect我们强制 Python 的垃圾收集器清空内存:
del(X,y,X_train,y_train,X_test)
gc.collect
在这里,我们创建了一个从磁盘中抽取随机子样本的函数。请注意,我们使用的是⅓.的样本分数我们可以使用较小的分数,但如果这样做,我们还需要调整两个重要的东西。首先,我们需要匹配深度学习模型的批量大小,以便批量大小永远不会超过样本大小。其次,我们需要调整我们的for循环中的纪元数量,以确保最大部分的训练数据用于训练模型:
import pandas as pd
import random
def sample_file():
global skip_idx
global train_data
global X_train
global y_train
big_train='lsml-Bigtrainm'
计算整个集合中的行数:
num_lines = sum(1 for i in open(big_train))
我们使用训练集的三分之一:
size = int(num_lines / 3)
跳过索引并保留索引:
skip_idx = random.sample(range(1, num_lines), num_lines - size)
train_data = pd.read_csv(big_train, skiprows=skip_idx)
X_train=train_data.drop(train_data.columns[[0]], axis=1)
y_train = train_data.ix[:,0]
我们在前面的部分看到了重量衰减;我们将在这里再次使用它:
def exp_decay(global_step):
return tf.train.exponential_decay(
learning_rate=0.01, global_step=global_step,
decay_steps=steps, decay_rate=0.01)
这里,我们分别用5、4和4单元建立了三个隐藏层的神经网络 DNN 分类器。请注意,我们将的批量设置为300,这意味着我们在每个时期使用 300 个训练案例。这也有助于防止内存过载:
steps = 5000
clf = skflow.TensorFlowDNNClassifier(
hidden_units=[5,4,4],
n_classes=4,
batch_size=300,
steps=steps,
optimizer='Adam',
learning_rate=exp_decay
)
这里,我们将子样本的数量设置为三个(epochs=3)。这意味着我们在三个连续的子样本上逐步训练我们的深度学习模型:
epochs=3
for i in range(epochs):
sample_file()
clf.partial_fit(X_train,y_train)
test_data = pd.read_csv('lsml-Bigtestm',sep=',')
X_test=test_data.drop(test_data.columns[[0]], axis=1)
y_test = test_data.ix[:,0]
score = accuracy_score(y_test, clf.predict(X_test))
print score
OUTPUT
Step #501, avg. loss: 0.55220
Step #1001, avg. loss: 0.31165
Step #1501, avg. loss: 0.27033
Step #2001, avg. loss: 0.25250
Step #2501, avg. loss: 0.24156
Step #3001, avg. loss: 0.23438
Step #3501, avg. loss: 0.23113
Step #4001, avg. loss: 0.23335
Step #4501, epoch #1, avg. loss: 0.23303
Step #1, avg. loss: 2.57968
Step #501, avg. loss: 0.57755
Step #1001, avg. loss: 0.33215
Step #1501, avg. loss: 0.27509
Step #2001, avg. loss: 0.26172
Step #2501, avg. loss: 0.24883
Step #3001, avg. loss: 0.24343
Step #3501, avg. loss: 0.24265
Step #4001, avg. loss: 0.23686
Step #4501, epoch #1, avg. loss: 0.23681
0.929022
我们设法在非常容易管理的训练时间内在测试集上获得了.929的准确度,并且没有使我们的记忆超负荷,比我们在整个数据集上一次训练相同的模型要快得多。
Keras 和 TensorFlow 安装
之前,我们已经看到了用于 TensorFlow 应用的 SkFlow 包装器的实际例子。对于神经网络和深度学习的更复杂的方法,我们对参数有更多的控制,我们提出了 Keras(http://keras.io/)。这个包最初是在“T4”框架内开发的,但最近也适应了 TensorFlow。这样,我们可以在 TensorFlow 之上使用 Keras 作为更高抽象的包。请记住,在方法上,Keras 比 SkFlow 稍微不那么简单。Keras 既可以在 GPU 上运行,也可以在 CPU 上运行,这使得这个包在移植到不同环境时非常灵活。
让我们首先安装 Keras,并确保它利用了 TensorFlow 后端。
安装工作只需在命令行中使用pip:
$pip install Keras
Keras 最初是建立在 antao 之上的,所以我们需要指定 Keras 来代替使用 TensorFlow。为了做到这一点,我们首先需要在 Keras 的默认平台 antao 上运行一次。
首先,我们需要运行一些 Keras 代码来确保所有的库项目都被正确安装。让我们训练一个基本的神经网络,并介绍一些关键概念。
为了方便起见,我们将使用由四个特性组成的 Scikit-learn 和由三个类组成的目标变量生成的数据。这些维度非常重要,因为我们需要它们来指定神经网络的架构:
import numpy as np
import keras
from sklearn.datasets import make_classification
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import OneHotEncoder
from keras.utils import np_utils, generic_utils
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation
from keras.optimizers import SGD
nb_classes=3
X, y = make_classification(n_samples=1000, n_features=4, n_classes=nb_classes,n_informative=3, n_redundant=0, random_state=101)
既然我们已经指定了变量,那么将目标变量转换成一个热编码数组就很重要了(就像我们在 TensorFlow 中所做的那样)。否则,Keras 将无法计算一个与所有目标输出的比较。对于 Keras,我们想用np_utils代替 sklearn 的一热编码器。我们将这样使用它:
y=np_utils.to_categorical(y,nb_classes)
print y
我们的y数组将如下所示:
OUTPUT]
array([[ 1., 0., 0.],
[ 0., 0., 1.],
[ 0., 0., 1.],
…,
现在让我们将数据分为测试和训练:
x_train, x_test, y_train, y_test = train_test_split(X, y,test_size=0.30, random_state=222)
这是我们开始形成我们心中的神经网络架构的地方。让我们开始一个双隐藏层神经网络,激活relu,每个隐藏层有三个单位。我们的第一层有四个输入,因为在这种情况下我们有四个特征。之后,我们添加三个单位的隐藏层,因此(model.add(dense(3))。
就像我们之前看到的,我们将使用softmax函数将网络传递到输出层:
model = Sequential()
model.add(Dense(4, input_shape=(4,)))
model.add(Activation('relu'))
model.add(Dense(3))
model.add(Activation('relu'))
model.add(Dense(3))
model.add(Activation('softmax'))
首先,我们指定我们的 SGD 函数,在这里我们实现了我们现在熟悉的最重要的参数,即:
- lr:学习率。
- 衰减:衰减学习速率的衰减函数。不要将其与权重衰减混淆,权重衰减是一个正则化参数。
- 动量:我们用这个来防止陷入局部极小。
- nesterov :这是一个布尔值,它指定我们是否要使用 nesterov 动量,并且仅当我们为动量参数指定了一个整数时才适用。(详见第四章、神经网络和深度学习,了解更多详细说明。)
- 优化器:这里我们将指定我们选择的优化算法(由 SGD、RMSProp、ADAGRAD、Adadelta 和 Adam 组成)。
让我们看看下面的代码片段:
#We use this for reproducibility
seed = 22
np.random.seed(seed)
model = Sequential()
model.add(Dense(4, input_shape=(4,)))
model.add(Activation('relu'))
model.add(Dense(3))
model.add(Activation('relu'))
model.add(Dense(3))
model.add(Activation('softmax'))
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(loss='categorical_crossentropy', optimizer=sgd)
model.fit(x_train, y_train, verbose=1, batch_size=100, nb_epoch=50,show_accuracy=True,validation_data=(x_test, y_test))
time.sleep(0.1)
在这种情况下,我们使用了100的batch_size,这意味着我们在每个历元中使用了 100 个训练示例的迷你批次梯度下降。在这个模型中,我们使用了50训练时代。这将为您提供以下输出:
OUTPUT:
acc: 0.8129 - val_loss: 0.5391 - val_acc: 0.8000
Train on 700 samples, validate on 300 samples
在上一个模型中,我们将 SGD 与 nesterov 一起使用,无论我们对它的训练使用了多少个时代,我们都无法提高我们的分数。
为了增加准确性。建议尝试其他优化算法。我们之前已经成功使用了 Adam 优化方法,那么我们在这里再使用一次,看看能否增加精度。像 Adam 这样的自适应学习率会随着时间的推移而降低学习率,因此需要更多的时期才能达到最佳解决方案。因此,在本例中,我们将纪元的数量设置为 200:
adam=keras.optimizers.Adam(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=adam)
model.fit(x_train, y_train, verbose=1, batch_size=100, nb_epoch=200,show_accuracy=True,validation_data=(x_test, y_test))
time.sleep(0.1)
OUTPUT:
Epoch 200/200
700/700 [==============================] - 0s - loss: 0.3755 - acc: 0.8657 - val_loss: 0.4725 - val_acc: 0.8200
我们现在已经成功地通过 Adam 优化算法实现了从 0.8 到 0.82 的令人信服的改进。
目前,我们已经介绍了 Keras 中神经网络的最重要的元素。现在让我们继续设置 Keras ,这样它将利用 TensorFlow 框架。默认情况下,Keras 将使用安诺后端。为了指示 Keras 在 TensorFlow 上工作,我们需要首先在 packages 文件夹中找到 Keras 文件夹:
import os
print keras.__file__
您的道路可能会有所不同:
Output: /Library/Python/2.7/site-packages/keras/__init__.pyc
现在我们已经找到了 Keras 的包文件夹,我们需要寻找~/.keras/keras.json文件。
这个文件中有一段脚本如下所示:
{"epsilon": 1e-07, "floatx": "float32", "backend": "theano"}
您只需将"backend":"theano"更改为"backend":"tensorflow",结果如下:
{"epsilon": 1e-07, "floatx": "float32", "backend": "tensorflow"}
如果由于某种原因.json文件不在 Keras 文件夹中,即/Library/Python/2.7/site-packages/keras/,您可以将其复制粘贴到文本编辑器中:
{"epsilon": 1e-07, "floatx": "float32", "backend": "tensorflow"}
保存为.json文件,放入keras文件夹。
要测试 TensorFlow 环境是否在 TensorFlow 中得到正确利用,我们可以键入以下内容:
from keras import backend as K
input = K.placeholder(shape=(4, 4, 5))
# also works:
input = K.placeholder(shape=(None, 2, 5))
# also works:
input = K.placeholder(ndim=2)
OUTPUT:
Using Theano backend.
有些用户可能根本没有输出,这很好。你的 TensorFlow 后端应该可以使用了。
TensorFlow 中的卷积神经网络
在这一章和上一章之间,我们已经走了很长的路,涵盖了深度学习中最重要的主题。我们现在了解如何通过在神经网络中堆叠多层来构建架构,以及如何识别和利用反向传播方法。我们还介绍了使用堆叠式去噪自动编码器进行无监督预处理的概念。深度学习的下一个真正令人兴奋的步骤是卷积神经网络 ( CNN )的快速发展领域,这是一种构建多层、局部连接网络的方法。CNNs,通常被称为convents,在撰写本书时发展如此迅速,以至于我们不得不在一个月的时间内重写和更新这一章。在这一章中,我们将涵盖中枢神经系统背后最基本和最重要的概念,这样我们就能够运行一些基本的例子,而不会被有时巨大的复杂性所淹没。然而,我们不能完全公正地对待巨大的理论和计算背景,所以这一段提供了一个实际的起点。
从概念上理解中枢神经系统最好的方法是追溯历史,从一点点认知神经科学开始,看看 Huber 和 Wiesel 对猫视觉皮层的研究。Huber 和 Wiesel 记录了猫视觉皮层的神经激活,同时通过在大脑的视觉皮层插入微电极来测量神经活动。(可怜的猫!)他们这样做的时候,猫正在观看投影在屏幕上的原始形状图像。有趣的是,他们发现某些神经元只对特定方向或形状的轮廓做出反应。这导致了视觉皮层由局部和定向特异性神经元组成的理论。这意味着特定的神经元只对特定方向和形状(三角形、圆形或正方形)的图像做出反应。考虑到猫和其他哺乳动物可以将复杂且不断进化的形状感知为一个连贯的整体,我们可以假设感知是所有这些局部和分层组织的神经元的集合。到那时,第一个多层感知器已经完全开发出来,所以用不了多久,神经元中的局部性和特定灵敏度的想法就在感知器架构中建模出来了。从计算神经科学的角度来看,这个想法被发展成大脑中局部感受区的地图,并增加了选择性连接的层。这被已经在进行的神经网络和人工智能领域所采用。第一位报道的科学家是福岛和他的所谓的新神经元(1982),他将局部特定计算的概念应用于多层感知器。
Yann LeCun 把新神经元的概念发展成了他的版本,叫做 LeNet。增加了梯度下降反向传播算法。这种 LeNet 架构仍然是最近引入的许多更进化的 CNN 架构的基础。像 LeNet 这样的基本 CNN 学习从第一层中的原始像素中检测边缘,然后使用这些边缘来检测第二层中的简单形状,然后在该过程的后期使用这些形状来检测更高级别的特征,例如更高层中的环境中的对象。神经序列的下一层是使用这些高级特征的最终分类器。我们可以在类似这样的 CNN 中看到前馈传递:我们从矩阵输入移动到像素,我们从像素中检测边缘,然后从边缘检测形状,并从形状中检测越来越有特色、越来越抽象和复杂的特征。
注
网络中的每个卷积或层都可以接受特定的特征(如形状、角度或颜色)。
更深的层将把这些特征组合成一个更复杂的集合。这样,它可以处理完整的图像,而不会在步骤中用图像的全部输入空间给网络增加负担。
到目前为止,我们只研究了完全连接的神经网络,其中每一层都连接到相邻的每一层。这些网络已经被证明是非常有效的,但是缺点是我们必须训练的参数数量急剧增加。另一方面,我们可能会想象,当我们训练一个小图像(28 x 28)大小时,我们可以摆脱完全连接的网络。然而,在跨越整个 T2 的更大的图像上训练一个完全连接的网络,计算量会非常大。
注
总而言之,我们可以说,与全连接神经网络相比,中枢神经系统具有以下优势:
- 它们减少了参数空间,从而防止过度训练和计算负荷
- 中枢神经系统对于物体方向是不变的(想象一下人脸识别对不同位置的人脸进行分类)
- 能够学习和概括复杂多维特征的中枢神经系统
- 神经网络可用于语音识别、图像分类和最近的复杂推荐引擎
中枢神经系统利用所谓的感受野将输入连接到特征地图。了解 CNNs 的最好方法是更深入地了解架构,当然也可以获得实际操作体验。让我们来看看构成中枢神经系统的各种层。美国有线电视新闻网的架构由三种类型的层组成;即卷积层、汇集层和全连接层,其中每层接受输入 3D 体积(h,w,d),并通过可微函数将其转换为 3D 输出。
卷积层
我们可以通过想象一定大小的聚光灯滑过输入(像素值和 RGB 颜色维度)来理解卷积的概念,之后我们可以方便地计算过滤值(也称为面片)和真实输入之间的点积。这做了两件重要的事情:第一,它压缩输入,更重要的是,第二,网络学习过滤器,只有当他们在输入中看到某些特定类型的特征空间位置时,过滤器才会激活。
请看下图,了解这是如何工作的:

处理图像输入的两个卷积层[7x7x3]输入体积:宽度为 7、高度为 7 并且具有三个颜色通道 R、G、B 的图像
从这张图中我们可以看到我们有两级滤波器(W0 和 W1)和输入的三维(以阵列的形式),所有这些都导致输入矩阵上滑动聚光灯/窗口的点积。我们把这个聚光灯的大小称为步幅,意思是步幅越大,输出越小。
如您所见,当我们应用 3×3 过滤器时,过滤器的整个范围都在矩阵的中心进行处理,但是一旦我们靠近或经过边缘,我们就开始在输入的边缘失去优势。在这种情况下,我们应用即所谓的零填充。在这种情况下,输入尺寸之外的所有元素都被设置为零。最近,零填充或多或少成为了大多数美国有线电视新闻网应用的默认设置。
汇集层
下一种类型的层通常位于过滤层之间,称为汇集层或子采样层。这样做是沿着空间维度(宽度、高度)执行下采样操作,这反过来又有助于过拟合和减少计算负载。有几种方法可以执行这种下采样,但最近最大池被证明是最有效的方法。
最大池化是一种简单的方法,它通过取相邻要素的一个面片的最大值来压缩要素。下图将阐明这一观点;矩阵中的每个颜色框代表步幅大小为 2:

跨度为 2 的最大池层
使用池层的主要原因如下:
- 减少参数数量,从而减少计算负载
- 正规化
有趣的是,最新的研究结果建议完全省略汇集层,这将导致更好的准确性(尽管代价是对 CPU 或 GPU 造成更大的压力)。
全连接层
关于这种类型的层没有太多要解释的。计算分类的最终输出(主要使用 softmax)是一个完全连接的层。然而,在卷积层之间,也有(虽然很少)完全连接的层。
在我们自己申请有线电视新闻网之前,让我们把你到目前为止学到的东西拿来,检查一下有线电视新闻网的架构,以检验我们的理解。当我们查看下图中的 ConvNet 架构时,我们已经可以了解到 ConvNet 会对输入做什么。这个例子是一个名为 AlexNet 的有效卷积神经网络,旨在将 120 万幅图像分类为 1000 类。它被用于 2012 年的 ImageNet 竞赛。ImageNet 是全球最重要的图像分类和本地化比赛,每年都会举办。AlexNet 指的是 Alex Krizhevsky(与 Vinod Nair 和 Geoffrey Hinton 一起)。

AlexNet 架构
当我们查看架构时,我们可以立即看到具有三维深度的输入维度 224 乘以 224。最大池层堆叠的输入中的步长为 4,降低了输入的维度。依次是卷积层。尺寸为 4,096 的两个密集层是完全连接的层,通向我们之前提到的最终输出。
另一方面,我们在前面的一段中提到,TensorFlow 的图形计算允许跨图形处理器的并行化。AlexNet 也做了同样的事情;请看下图,了解他们如何跨 GPU 并行化架构:

上图来自http://www.cs.toronto.edu/~fritz/absps/imagenet.pdf。
AlexNet 让不同的模型通过垂直拆分架构来利用 GPU,以便以后合并到最终的分类输出中。中枢神经系统更适合分布式处理,这是本地连接网络相对于完全连接网络的最大优势之一。该模型训练了一组 120 万张图像,在两台 NVIDIA GTX 580 3GB 图形处理器上用了 5 天时间完成。本项目使用了两个多图形处理器单元(总共六个图形处理器)。
美国有线电视新闻网采用渐进的方式
既然我们已经对有线电视新闻网的架构有了一个很好的了解,让我们开始应用有线电视新闻网吧。
对于本例,我们将使用著名的 CIFAR-10 人脸图像数据集,该数据集在 Keras 域中非常方便。数据集由 60,000 张 32 x 32 的彩色图像组成,10 个目标类别由飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车组成。这是一个比用于 AlexNet 示例的数据集更小的数据集。更多信息可以参考https://www.cs.toronto.edu/~kriz/cifar.html。
在这个 CNN 中,我们将使用以下体系结构根据我们指定的 10 个类别对图像进行分类:
input->convolution 1 (32,3,3)->convolution 2(32,3,3)->pooling->dropout -> Output (Fully connected layer and softmax)
GPU 计算
如果你安装了一个兼容 CUDA 的显卡,你可以通过在你的集成开发环境上放置下面的代码来利用你的图形处理器。
import os
os.environ['THEANO_FLAGS'] = 'device=gpu0, assert_no_cpu_op=raise, on_unused_input=ignore, floatX=float32'
然而,我们建议首先在您的常规中央处理器上尝试这个例子。
让我们首先导入并准备数据。
我们使用 32 x 32 的输入尺寸,因为这是图像的实际尺寸:
from keras.datasets import cifar10
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D
from keras.optimizers import SGD
from keras.utils import np_utils
batch_size = 32
nb_classes = 10
nb_epoch = 5 #these are the number of epochs, watch out because it might set your #cpu/gpu on fire.
# input image dimensions
img_rows, img_cols = 32, 32
# the CIFAR10 images are RGB
img_channels = 3
# the data, shuffled and split between train and test sets
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
print('X_train shape:', X_train.shape)
print(X_train.shape[0], 'train samples')
print(X_test.shape[0], 'test samples')
#remember we need to encode the target variable
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)
现在,让我们设置我们的 CNN 架构,并根据我们心中的架构构建模型。
对于这个例子,我们将用香草 SGD 和内斯特罗夫动量训练我们的 CNN 模型:
model = Sequential()
#this is the first convolutional layer, we set the filter size
model.add(Convolution2D(32, 3, 3, border_mode='same',
input_shape=(img_channels, img_rows, img_cols)))
model.add(Activation('relu'))
#the second convolutional layer
model.add(Convolution2D(32, 3, 3))
model.add(Activation('relu'))
#here we specify the pooling layer
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
#first we flatten the input towards the fully connected layer into the softmax function
model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))
# let's train the model using SGD + momentum like we have done before.
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(loss='categorical_crossentropy', optimizer=sgd)
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
#Here we apply scaling to the features
X_train /= 255
X_test /= 255
这一步非常重要,因为我们在这里指定美国有线电视新闻网进行增量训练。我们在前面几章(参考第二章、Scikit 中的可伸缩学习-learn )看到了,在前面一段,在线和增量学习的计算效率。我们可以模拟它的一些属性,并通过使用一个非常小的纪元大小和一个更小的batch_size(每个纪元中训练集的一部分)将其应用于中枢神经系统,并在 for 循环中对它们进行增量训练。这样,我们可以给定相同数量的时代,在更短的时间内训练我们的有线电视新闻网,同时对主存储器的负担也更低。我们可以用一个简单的 for 循环实现这个非常强大的想法,如下所示:
for epoch in xrange(nb_epoch):
model.fit(X_train, Y_train, batch_size=batch_size, nb_epoch=1,show_accuracy=True
,validation_data=(X_test, Y_test), shuffle=True)
OUTPUT:]
X_train shape: (50000, 3, 32, 32)
50000 train samples
10000 test samples
Train on 50000 samples, validate on 10000 samples
Epoch 1/1
50000/50000 [==============================] - 1480s - loss: 1.4464 - acc: 0.4803 - val_loss: 1.1774 - val_acc: 0.5785
Train on 50000 samples, validate on 10000 samples
Epoch 1/1
50000/50000 [==============================] - 1475s - loss: 1.0701 - acc: 0.6212 - val_loss: 0.9959 - val_acc: 0.6525
Train on 50000 samples, validate on 10000 samples
Epoch 1/1
50000/50000 [==============================] - 1502s - loss: 0.8841 - acc: 0.6883 - val_loss: 0.9395 - val_acc: 0.6750
Train on 50000 samples, validate on 10000 samples
Epoch 1/1
50000/50000 [==============================] - 1555s - loss: 0.7308 - acc: 0.7447 - val_loss: 0.9138 - val_acc: 0.6920
Train on 50000 samples, validate on 10000 samples
Epoch 1/1
50000/50000 [==============================] - 1587s - loss: 0.5972 - acc: 0.7925 - val_loss: 0.9351 - val_acc: 0.6820
我们可以看到我们的美国有线电视新闻网列车最终达到接近 0.7 的验证精度。考虑到我们已经在具有 50,000 个训练示例和 10 个目标类的高维数据集上训练了一个复杂模型,这已经令人满意了。美国有线电视新闻网在这个数据集上可以达到的最大可能分数至少需要 200 个时代。本例提出的方法绝不是的最终结果。这是让你开始使用中枢神经系统的一个相当基本的实现。可以通过添加或移除层、调整批次大小等方式自由进行实验。玩玩参数,感受一下这是如何工作的。
如果你想了解更多卷积层的最新进展,可以看看 【残留网络】 ( ResNet ),这是 CNN 架构上的最新改进之一。
和其他人是 2015 年 ImageNet(ILSVRC)的获奖者。它的特点是一个有趣的架构,使用了一种叫做批处理规范化的方法,一种规范化层之间的特征转换的方法。在 Keras 中有一个批处理规范化函数,您可能想要对其进行实验(http://keras.io/layers/normalization/)。
为了让您全面了解最新一代的氯化萘,您可能需要熟悉以下被发现更有效的氯化萘参数设置:
- 小步幅
- 权重衰减(正则化而不是丢弃)
- 没有辍学
- 中层之间的批处理规范化
- 少到没有预训练(自动编码器和玻尔兹曼机器慢慢退出图像分类的时尚)
另一个有趣的概念是,最近卷积网络被用于图像检测之外的应用。它们用于语言和文本分类、句子补全,甚至推荐系统。一个有趣的例子是 Spotify 的音乐推荐引擎,它基于卷积神经网络。您可以在这里查看更多信息:
- http://benane . github . io/2014/08/05/Spotify-CNNs . html
- http://machine learning . wustl . edu/mlpapers/paper _ files/NIPS 2013 _ 5004 . pdf
目前,卷积网络用于以下操作:
- 人脸检测(脸书)
- 电影分类(YouTube)
- 语音和文本
- 生成艺术(例如谷歌深度梦境)
- 推荐引擎(音乐推荐—Spotify)
总结
在这一章中,我们已经走了很长的路,涵盖了 TensorFlow 景观及其相应的方法。我们熟悉了如何建立基本的回归器、分类器和单隐层神经网络。尽管编程 TensorFlow 操作相对简单,但对于现成的机器学习任务来说,TensorFlow 可能有点过于繁琐。这正是 SkFlow 的用武之地,它是一个更高级的库,其界面非常类似于 Scikit-learn。对于增量式甚至核外解决方案,SkFlow 提供了一种部分适配的方法,可以轻松设置。其他大规模解决方案要么局限于 GPU 应用,要么处于过早阶段。因此,就目前而言,当涉及到可扩展的解决方案时,我们不得不满足于增量学习策略。
我们还介绍了卷积神经网络,并了解了如何在 Keras 中设置它们。
六、大规模回归树
在这一章中,我们将集中讨论分类和回归树的可伸缩方法。将涵盖以下主题:
- Scikit-learn 中快速随机森林应用的提示和技巧
- 加性随机森林模型和二次抽样
- 梯度升压
- XGBoost 和流方法
- 非常快的 GBM 和 H2O 的随机森林
决策树的目的是学习一系列决策规则,根据训练数据推断目标标签。使用递归算法,该过程从树根开始,并在杂质最少的特征上分割数据。目前,最广泛应用的可扩展的基于树的应用是基于 CART 的。 CART 是分类回归树的缩写,由布瑞曼、弗里德曼·斯通和奥尔森于 1984 年引入。CART 在两个方面不同于其他决策树模型(如 ID3、C4.5/C5.0、CHAID 和 MARS)。首先,CART 适用于分类和回归问题。其次,它构建二叉树(在每次分裂时,产生一个二进制分裂)。这使得 CART 树能够对给定的特征进行递归操作,并以贪婪的方式对杂质形式的误差度量进行优化。这些二叉树以及可扩展的解决方案是本章的重点。
让我们仔细看看这些树是如何建造的。我们可以看到决策树是一个有节点的图,从上到下传递信息。树中的每个决策都是通过对类(布尔型)或连续变量(阈值)进行二进制拆分而做出的,从而得到最终的预测。
树是通过以下过程构建和学习的:
- 递归地寻找将目标标签从根节点分割到终端节点的最佳变量。这是通过我们根据目标结果最小化的每个特征的杂质来衡量的。在本章中,相关的杂质测度是基尼杂质和交叉熵。
基尼杂质

基尼不纯度是一个度量标准,用于衡量目标类( k )的概率 pi 之间的差异,因此概率值在目标类上的平均分布会导致较高的基尼不纯度。
交叉熵

通过交叉熵,我们可以看到错误分类的对数概率。这两个指标都被证明产生了非常相似的结果。然而,基尼杂质在计算上更有效,因为它不需要计算日志。
我们这样做,直到满足停止标准。这个标准大致意味着两件事:第一,添加新变量不再改善目标结果,第二,达到最大树深度或树复杂度阈值。请注意,具有许多节点的非常深且复杂的树很容易导致过度拟合。为了防止这种情况,我们通常通过限制树的深度来修剪树。
为了获得对这个过程如何工作的直觉,让我们用 Scikit 构建一个决策树——用 graphviz 学习和可视化它。首先,创建一个玩具数据集,看看我们能否根据智商(数字)、年龄(数字)、年收入(数字)、企业主(布尔)和大学学位(布尔)来预测谁是吸烟者,谁不是。您需要从http://www.graphviz.org下载软件,以便加载我们将使用 Scikit-learn 创建的tree.dot文件的可视化:
import numpy as np
from sklearn import tree
iq=[90,110,100,140,110,100]
age=[42,20,50,40,70,50]
anincome=[40,20,46,28,100,20]
businessowner=[0,1,0,1,0,0]
univdegree=[0,1,0,1,0,0]
smoking=[1,0,0,1,1,0]
ids=np.column_stack((iq, age, anincome,businessowner,univdegree))
names=['iq','age','income','univdegree']
dt = tree.DecisionTreeClassifier(random_state=99)
dt.fit(ids,smoking)
dt.predict(ids)
tree.export_graphviz(dt,out_file='tree2.dot',feature_names=names,label=all,max_depth=5,class_names=True)
现在可以在工作目录中找到tree.dot文件。找到该文件后,您可以使用 graphviz 软件打开它:

- 根节点(收入):这是表示具有最高信息增益和最低杂质(基尼系数=.5)的特征的起始节点
- 内部节点(年龄和智商):这是根节点和终端之间的每个节点。父节点将决策规则传递给接收端—子节点(左和右)
- 终端节点(叶节点):由树结构划分的目标标签
树深是从根节点到终端节点的边数。在这种情况下,我们的树深度为 3。
我们现在可以看到生成的树产生的所有二进制拆分。在根节点的顶部,我们可以看到收入低于 24k 的人不是吸烟者(收入< 24)。我们还可以在每个节点上看到相应的基尼杂质(. 5)。没有剩余的子节点,因为决定是最终的。路径仅仅到此结束,因为它完全划分了目标类。然而,在收入的正确子节点(年龄)中,树分支出来。在这里,如果年龄小于等于 46,那么这个人不是吸烟者,但是年龄大于 46,智商低于 105,这个人就是吸烟者。同样重要的是,我们创建的几个不属于树的特性——学位和企业所有者。这是因为树中的变量能够对没有它们的目标标签进行分类。这些省略的特征根本无助于降低树的杂质水平。
单个树有其缺点,因为它们容易过度覆盖,因此不能很好地推广到看不见的数据。这些技术的当前一代是用集成方法训练的,在集成方法中,单棵树被聚合成更强大的模型。这些 CART 集成技术是最常用的机器学习方法之一,因为它们的准确性、易用性和处理异构数据的能力。这些技术已成功应用于最近的数据科学竞赛,如卡格尔杯和 KDD 杯。由于分类和回归树的集成方法目前是人工智能和数据科学领域的标准,CART 集成方法的可扩展解决方案将是本章的主要主题。
一般来说,我们可以辨别两类用于 CART 模型的集成方法,即打包和增强。让我们通过这些概念来更好地理解集成的形成过程。
引导聚合
装袋是自举聚合的缩写。自举技术起源于分析师不得不处理数据稀缺的背景。使用这种统计方法,当统计分布不能先验地计算出来时,使用子样本来估计总体参数。自举的目标是为群体参数提供更稳健的估计,其中通过随机二次抽样和替换将更多可变性引入较小的数据集。通常,引导遵循以下基本步骤:
- 从给定数据集中随机抽取一批大小为 x 的样本进行替换。
- 从每个样本中计算一个度量或参数来估计总体参数。
- 汇总结果。
近年来,自举方法也被用于机器学习模型的参数。当分类器提供高度多样化的决策边界时,集成是最有效的。这种集成的多样性可以通过其底层模型和这些模型所基于的数据的多样性来实现。树非常适合分类器之间的这种多样性,因为树的结构可以是高度可变的。然而,最流行的集成方法是使用不同的训练数据集来训练单个分类器。通常,这样的数据集是通过二次采样技术获得的,例如自举和打包。一切都始于这样一个想法,即通过利用更多的数据,我们可以减少估计的方差。如果手头不可能有更多的数据,重新采样可以提供显著的改进,因为它允许在训练样本的许多版本上重新训练算法。这就是打包的想法;使用 bagging,我们通过聚集(例如,平均)许多重采样的结果,将最初的 bootstrap 思想更进一步,以便得到最终的预测,在该预测中,由于样本内过拟合引起的误差被相互平滑。
当我们将打包等集成技术应用于树模型时,我们会在原始数据集的每个单独的自举样本(或使用采样进行二次采样,无需替换)上构建多个树,然后聚合结果(通常通过算术、几何平均或投票)。
以这种方式,标准打包算法将如下所示:
- 从整个数据集(【S1】、 S2 、… Sn )中抽取一个 n 个大小为 K 的随机样本进行替换。
- 在( S1 、 S2 、… 锡上种植不同的树木。
- 根据新数据计算样本( S1 、 S2 、… 锡)的预测值,并汇总其结果。
CART 模型从打包方法中受益匪浅,因为它引入了随机性和多样性。
随机林和极随机林
除了装袋,基于训练示例,我们还可以根据特征抽取随机子样本。这样的方法被称为随机子空间。随机子空间对于高维数据(具有大量特征的数据)特别有用,它是我们称之为随机森林的方法的基础。在撰写本文时,随机森林是最受欢迎的机器学习算法,因为它易于使用,对杂乱的数据具有鲁棒性,并且可并行化。它进入了各种各样的应用,如定位应用、游戏和医疗保健应用的筛选方法。例如,Xbox Kinect 使用随机森林模型进行运动检测。考虑到随机森林算法基于 bagging 方法,该算法相对简单:
- 从可用样本中抽取尺寸为 N 的 m 样本。
- 在每个节点分割处,使用特征集 G (无替换)的不同部分,在每个子集( S1 、 S2 、… 锡)上独立构建树。
- 最小化节点分裂的误差(基于基尼指数或熵度量)。
- 让每棵树进行预测并汇总结果,使用投票进行分类,使用平均进行回归。
由于 bagging 依赖于多个子样本,因此它是并行化的绝佳选择,其中每个 CPU 单元都专用于计算单独的模型。这样,我们可以利用多核的广泛可用性来加快学习速度。作为这种扩展策略的一个限制,我们必须意识到 Python 是单线程的,我们将不得不复制许多 Python 实例,每个实例都复制我们必须使用的示例中的内存空间。因此,我们需要有大量可用的内存来适应训练矩阵和进程数量。如果可用的内存不够,设置在我们的计算机上同时运行的并行树计算的数量将无助于缩放算法。在这种情况下,CPU 使用率和 RAM 内存是重要的瓶颈。
随机森林模型很容易用于机器学习,因为它们不需要大量的超参数调整就能很好地执行。感兴趣的最重要参数是对模型性能影响最大的树木数量和树木深度(树木深度)。当对这两个超参数进行操作时,它会导致精度/性能之间的权衡,即更多的树和更多的深度会导致更高的计算负载。作为实践者,我们的经验建议不要将树数量的值设置得太高,因为最终,该模型的性能将达到一个稳定水平,并且在添加更多树时不会再提高,而只会导致对 CPU 内核的征税。在这样的考虑下,尽管带有默认参数的随机森林模型在开箱即用时表现良好,但我们仍然可以通过调整树的数量来提高其性能。有关随机林的超参数的概述,请参见下表。
随机森林装袋的最重要参数:
n_estimators:模型中树的数量max_features:用于树构造的特征数量min_sample_leaf:如果终端节点包含的样本少于最小值,则删除节点分裂max_depth:我们从根节点到终端节点自上而下传递的节点数criterion:计算最佳分割的方法(基尼或熵)min_samples_split:分割一个内部节点所需的最小样本数
Scikit-learn 提供了大量强大的 CART 集成应用,其中一些计算效率很高。说到随机森林,有一个经常被忽视的算法叫做额外树,更广为人知的是极随机森林。当谈到 CPU 效率时,额外的树可以提供比常规随机森林相当大的加速——有时甚至是十倍。
在下表中,您可以看到每种方法的计算速度。一旦我们增加样本量,额外树的速度会大大加快,差异会更明显:
|样品化
|
提取物
|
随机森林
|
| --- | --- | --- |
| One hundred thousand | 25.9 秒 | 164 s |
| Fifty thousand | 9.95 秒 | 35.1 秒 |
| ten thousand | 2.11 秒 | 6.3 秒 |
用 50 个特征和 100 个估计量训练模型,用于额外的树和随机森林。在这个例子中,我们使用了一个具有 16GB 内存的四核 MacBook Pro。我们以秒为单位测量训练时间。
在这一段中,我们将使用极端森林来代替 Scikit-learn 中的香草随机森林方法。所以你可能会问:它们有什么不同?区别并不明显复杂。在随机森林中,节点分割决策规则基于每次迭代中随机选择的特征的最佳得分。在极度随机化的森林中,对随机子集中的每个要素生成随机分割(因此不需要计算来寻找每个要素的最佳分割),然后选择最佳评分阈值。这种方法带来了一些有利的特性,因为该方法导致获得具有较低方差的模型,即使每个单独的树生长直到在终端节点中具有最大可能的精度。随着更多的随机性被添加到分支分裂中,树学习器产生错误,这些错误因此在集合中的树之间不太相关。这将导致集成中更多的不相关估计,并且根据学习问题(毕竟没有免费的午餐),导致比标准随机森林集成更低的泛化误差。然而,实际上,规则随机森林模型可以提供稍高的精度。
给定如此有趣的学习属性,通过更有效的节点分裂计算和同样的可用于随机森林的并行性,如果我们想要加速核心内学习,我们认为极随机化树是集成树算法范围内的优秀候选。
要阅读极其随机化的森林算法的详细描述,您可以阅读以下启动一切的文章:
页(page 的缩写)厄恩斯特和魏汉高,极随机化树,机器学习,63(1),3-42,2006。本文可在https://www . semanticschool . org/paper/extreme-随机化-trees-Geurts-Ernst/336 a 165 c 17 c 9 c 56160d 332 b 9 F4 a2 b 403 fccbdbfb/pdf免费获取。
作为如何扩展核心树集成的一个例子,我们将运行一个例子,其中我们将一个有效的随机森林方法应用于信用数据。该数据集用于预测信用卡客户的违约率。数据由 18 个特征和 30,000 个训练示例组成。由于我们需要导入一个 XLS 格式的文件,您将需要安装xlrd包,我们可以通过在命令行终端中键入以下内容来实现:
$ pip install xlrd
import pandas as pd
import numpy as np
import os
import xlrd
import urllib
#set your path here
os.chdir('/your-path-here')
url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/00350/default%20of%20credit%20card%20clients.xls'
filename='creditdefault.xls'
urllib.urlretrieve(url, filename)
target = 'default payment next month'
data = pd.read_excel('creditdefault.xls', skiprows=1)
target = 'default payment next month'
y = np.asarray(data[target])
features = data.columns.drop(['ID', target])
X = np.asarray(data[features])
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.cross_validation import cross_val_score
from sklearn.datasets import make_classification
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.30, random_state=101)
clf = ExtraTreesClassifier(n_estimators=500, random_state=101)
clf.fit(X_train,y_train)
scores = cross_val_score(clf, X_train, y_train, cv=3,scoring='accuracy', n_jobs=-1)
print "ExtraTreesClassifier -> cross validation accuracy: mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))
Output]
ExtraTreesClassifier -> cross validation accuracy: mean = 0.812 std = 0.003
现在我们已经对训练集的准确性有了一些基本的估计,让我们看看它在测试集上的表现。在这种情况下,我们希望监控假阳性和假阴性,并检查目标变量上的类不平衡:
y_pred=clf.predict(X_test)
from sklearn.metrics import confusion_matrix
confusionMatrix = confusion_matrix(y_test, y_pred)
print confusionMatrix
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_pred)
OUTPUT:
[[6610 448]
[1238 704]]
Our overall test accuracy:
0.81266666666666665
有趣的是,测试集的准确性等于我们的训练结果。由于我们的基线模型只是使用默认设置,因此我们可以尝试通过调整超参数来提高性能,这是一项计算成本很高的任务。最近,计算效率更高的超参数优化方法得到了发展,我们将在下一节中介绍。
随机搜索快速参数优化
您可能已经熟悉了 Scikit-learn 的 gridsearch 功能。这是一个很好的工具,但是当它涉及到大文件时,它可以极大地增加训练时间,这取决于参数空间。对于极端随机森林,我们可以使用名为随机搜索的替代参数搜索方法来加快参数调整的计算时间。公共网格搜索通过系统测试所有可能的超参数设置组合来消耗 CPU 和内存,随机搜索随机选择超参数组合。当 gridsearch 测试超过 30 种组合时(对于较小的搜索空间,gridsearch 仍然具有竞争力),这种方法可以显著提高计算速度。可实现的增益与我们从随机森林切换到极随机森林时看到的相同(根据硬件规格、超参数空间和数据集大小,可以考虑 2 到 10 倍的增益)。
我们可以指定由n_iter参数随机评估的超参数设置的数量:
from sklearn.grid_search import GridSearchCV, RandomizedSearchCV
param_dist = {"max_depth": [1,3, 7,8,12,None],
"max_features": [8,9,10,11,16,22],
"min_samples_split": [8,10,11,14,16,19],
"min_samples_leaf": [1,2,3,4,5,6,7],
"bootstrap": [True, False]}
#here we specify the search settings, we use only 25 random parameter
#valuations but we manage to keep training times in check.
rsearch = RandomizedSearchCV(clf, param_distributions=param_dist,
n_iter=25)
rsearch.fit(X_train,y_train)
rsearch.grid_scores_
bestclf=rsearch.best_estimator_
print bestclf
在这里,我们可以看到我们模型的最佳参数设置列表。
我们现在可以使用这个模型对我们的测试集进行预测:
OUTPUT:
ExtraTreesClassifier(bootstrap=False, class_weight=None, criterion='gini',
max_depth=12, max_features=11, max_leaf_nodes=None,
min_samples_leaf=4, min_samples_split=10,
min_weight_fraction_leaf=0.0, n_estimators=500, n_jobs=1,
oob_score=False, random_state=101, verbose=0, warm_start=False)
y_pred=bestclf.predict(X_test)
confusionMatrix = confusion_matrix(y_test, y_pred)
print confusionMatrix
accuracy=accuracy_score(y_test, y_pred)
print accuracy
OUT
[[6733 325]
[1244 698]]
Out[152]:
0.82566666666666666
我们设法在可管理的训练时间范围内提高模型的性能,同时提高精度。
极度随机化的树和大数据集
到目前为止,由于随机森林的特定特征及其更有效的替代物,极度随机化森林,我们已经研究了利用多核处理器和随机化来扩大规模的解决方案。但是,如果您必须处理一个不适合内存或对 CPU 要求过高的大型数据集,您可能希望尝试一种核外解决方案。集成的核心外方法的最佳解决方案是 H2O 提供的解决方案,这将在本章后面详细介绍。然而,我们可以运用另一个实用技巧,让随机森林或额外的树在大规模数据集上平稳运行。第二好的解决方案是在数据的子样本上训练模型,然后在不同的数据子样本上集成每个模型的结果(毕竟,我们只需要对结果进行平均或分组)。在第 3 章、快速学习支持向量机中,我们已经介绍了储层采样的概念,处理数据流上的采样。在本章中,我们将再次使用采样,诉诸于更多的采样算法选择。首先,让我们安装一个名为 subsample 的非常方便的工具,由 Paul Butler(https://github.com/paulgb/subsample)开发,这是一个命令行工具,用于从一个大的、以换行符分隔的数据集(通常是一个类似 CSV 的文件)中采样数据。该工具提供了快速简便的采样方法,例如储层采样。
如第 3 章、快速学习支持向量机所示,储层采样是一种采样算法,有助于从一条河流中采样固定大小的样本。概念上很简单(我们已经在第 3 章中看到了公式),它只需要简单地传递数据来产生一个样本,该样本将存储在磁盘上的一个新文件中。(我们在第 3 章中的脚本将它存储在内存中。)
在下一个示例中,我们将使用这个子样本工具和一种方法来集成在这些子样本上训练的模型。
概括地说,在本节中,我们将执行以下操作:
- 创建我们的数据集,并将其分为测试和训练数据。
- 绘制训练数据的子样本,并将它们作为单独的文件保存在硬盘上。
- 加载这些子样本,并在其上训练极其随机的森林模型。
- 汇总模型。
- 检查结果。
让我们用pip安装这个子示例工具:
$pip install subsample
在命令行中,设置包含要采样的文件的工作目录:
$ cd /yourpath-here
此时,使用cd命令,您可以指定您的工作目录,您将需要在其中存储我们将在下一步创建的文件。
我们通过以下方式做到这一点:
from sklearn.datasets import fetch_covtype
import numpy as np
from sklearn.cross_validation import train_test_split
dataset = fetch_covtype(random_state=111, shuffle=True)
dataset = fetch_covtype()
X, y = dataset.data, dataset.target
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.3, random_state=0)
del(X,y)
covtrain=np.c_[X_train,y_train]
covtest=np.c_[X_test,y_test]
np.savetxt('covtrain.csv', covtrain, delimiter=",")
np.savetxt('covtest.csv', covtest, delimiter=",")
现在,我们已经将数据集划分为测试集和训练集,让我们对训练集进行二次采样,以便获得可以在内存中上传的数据块。考虑到完整训练数据集的大小为 30,000 个示例,我们将对三个较小的数据集进行二次采样,每个数据集由 10,000 个项目组成。如果您的计算机配备了小于 2GB 的内存,您可能会发现将初始训练集分割成更小的文件更容易管理,尽管您将获得的建模结果可能与我们基于三个子样本的示例不同。通常,子样本中的例子越少,模型的偏差就越大。当进行二次抽样时,我们实际上是在权衡处理更易管理的数据量的优势和估计偏差的增加:
$ subsample --reservoir -n 10000 covtrain.csv > cov1.csv
$ subsample --reservoir -n 10000 covtrain.csv > cov2.csv
$ subsample --reservoir -n 10000 covtrain.csv>cov3.csv
现在,您可以在命令行中指定的文件夹中找到这些子集。
现在确保在 IDE 或笔记本中设置了相同的路径。
让我们一个接一个地加载样本,并在样本上训练一个随机森林模型。
为了稍后将它们组合起来进行最终预测,请注意,我们保持一种逐行的方法,以便您可以密切关注连续的步骤。
为了使这些示例成功,请确保您使用的是 IDE 或 Jupyter 笔记本中设置的相同路径:
import os
os.chdir('/your-path-here')
此时,我们已经准备好开始从数据中学习,我们可以将样本逐个加载到内存中,并在这些样本上训练一组树:
import numpy as np
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.cross_validation import cross_val_score
from sklearn.cross_validation import train_test_split
import pandas as pd
import os
在报告验证分数之后,代码将在所有数据块上继续训练我们的模型,一次一个。当我们从不同的数据分区、一个数据块接一个数据块分别学习时,我们使用warm_start=Trueparameter和set_params方法初始化集成学习器(在本例中为ExtraTreeClassifier),其中从之前的训练会话中递增地添加树,因为 fit 方法被多次调用:
#here we load sample 1
df = pd.read_csv('/yourpath/cov1.csv')
y=df[df.columns[54]]
X=df[df.columns[0:54]]
clf1=ExtraTreesClassifier(n_estimators=100, random_state=101,warm_start=True)
clf1.fit(X,y)
scores = cross_val_score(clf1, X, y, cv=3,scoring='accuracy', n_jobs=-1)
print "ExtraTreesClassifier -> cross validation accuracy: mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))
print scores
print 'amount of trees in the model: %s' % len(clf1.estimators_)
#sample 2
df = pd.read_csv('/yourpath/cov2.csv')
y=df[df.columns[54]]
X=df[df.columns[0:54]]
clf1.set_params(n_estimators=150, random_state=101,warm_start=True)
clf1.fit(X,y)
scores = cross_val_score(clf1, X, y, cv=3,scoring='accuracy', n_jobs=-1)
print "ExtraTreesClassifier after params -> cross validation accuracy: mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))
print scores
print 'amount of trees in the model: %s' % len(clf1.estimators_)
#sample 3
df = pd.read_csv('/yourpath/cov3.csv')
y=df[df.columns[54]]
X=df[df.columns[0:54]]
clf1.set_params(n_estimators=200, random_state=101,warm_start=True)
clf1.fit(X,y)
scores = cross_val_score(clf1, X, y, cv=3,scoring='accuracy', n_jobs=-1)
print "ExtraTreesClassifier after params -> cross validation accuracy: mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))
print scores
print 'amount of trees in the model: %s' % len(clf1.estimators_)
# Now let's predict our combined model on the test set and check our score.
df = pd.read_csv('/yourpath/covtest.csv')
X=df[df.columns[0:54]]
y=df[df.columns[54]]
pred2=clf1.predict(X)
scores = cross_val_score(clf1, X, y, cv=3,scoring='accuracy', n_jobs=-1)
print "final test score %r" % np.mean(scores)
OUTPUT:]
ExtraTreesClassifier -> cross validation accuracy: mean = 0.803 std = 0.003
[ 0.805997 0.79964007 0.8021021 ]
amount of trees in the model: 100
ExtraTreesClassifier after params -> cross validation accuracy: mean = 0.798 std = 0.003
[ 0.80155875 0.79651861 0.79465626]
amount of trees in the model: 150
ExtraTreesClassifier after params -> cross validation accuracy: mean = 0.798 std = 0.006
[ 0.8005997 0.78974205 0.8033033 ]
amount of trees in the model: 200
final test score 0.92185447181058278
注
警告:这个方法看起来不像皮托尼克,但是非常有效。
我们现在已经提高了最终预测的分数;在测试集上,我们从大约 0.8 的精确度到 0.922 的精确度。这是因为我们有一个最终的组合模型,包含了前面三个随机森林模型的所有树信息。在代码的输出中,您还可以注意到添加到初始模型中的树的数量。
从这里开始,您可能希望在更大的数据集上尝试这样的方法,利用更多的子样本,或者对其中一个子样本应用随机搜索以进行更好的调整。
推车和助推
我们从装袋开始这一章;现在我们将使用 boosting 来完成我们的概述,boosting 是一种不同的集成方法。就像装袋一样,boosting 既可以用于回归也可以用于分类,并且最近为了更高的准确性而盖过了随机森林。
作为一个优化过程,boosting 基于我们在其他方法中看到的随机梯度下降原理,即通过根据梯度最小化误差来优化模型。迄今为止最常见的助推方法是 AdaBoost 和梯度助推(GBM 和最近的 XGBoost)。AdaBoost 算法归结为最小化那些预测略有错误的情况的误差,以便更难分类的情况得到更多的关注。最近,AdaBoost 失宠了,因为发现其他助推方法通常更准确。
在本章中,我们将介绍 Python 用户迄今为止可用的两种最有效的增强算法:Scikit-learn 包中的梯度增强机器 ( GBM )和 T5】极限梯度增强 ( XGBoost )。由于 GBM 本质上是顺序的,该算法很难并行化,因此比随机森林更难扩展,但是一些技巧可以做到这一点。一些加速算法的提示和技巧将会被一个不错的 H2O 内存不足解决方案所覆盖。
梯度增压机
正如我们在前面几节中看到的,随机森林和极端树是高效的算法,两者都可以用最少的努力很好地工作。虽然 GBM 被认为是一种更精确的方法,但它并不容易使用,并且总是需要调整它的许多超参数才能获得最佳结果。另一方面,随机森林只需要考虑几个参数(主要是树的深度和树的数量),就可以表现得相当好。另一个要注意的是过度训练。随机森林对过度训练的敏感度低于 GBM。所以,有了 GBM,我们还需要考虑正则化策略。最重要的是,随机森林更容易执行并行操作,因为 GBM 是顺序的,因此计算速度较慢。
在本章中,我们将在 Scikit-learn 中应用 GBM,看看名为 XGBoost 的下一代树提升算法,并在 H2O 上实现更大规模的提升。
我们在 Scikit-learn 和 H2O 中使用的 GBM 算法基于两个重要的概念:加法展开和通过最速下降算法的梯度优化。前者的一般思想是生成一系列相对简单的树(弱学习者),其中每个连续的树都是沿着一个梯度添加的。让我们假设我们有 M 树来聚合集合中的最终预测。每次迭代 fk 中的树现在是模型中所有可能的树(
)的更广阔空间的一部分(在 Scikit-learn 中,该参数更好地称为n_estimators):

加法扩展将以分阶段的方式向以前的树添加新树:

我们的梯度增强系综的预测只是所有先前的树和新添加的树
的预测的总和,更正式地导致以下结果:

GBM 算法的第二个重要但相当棘手的部分是通过最速下降进行梯度优化。这意味着我们在加法模型中加入了越来越强大的树。这是通过对新树应用梯度优化来实现的。由于没有像传统学习算法那样的参数,我们如何用树执行梯度更新?首先,我们参数化树;我们通过沿着梯度递归升级节点分割值来实现这一点,其中节点由向量表示。这样,最陡的下降方向是损失函数的负梯度,节点分裂将被升级和学习,导致:
:收缩参数(在本文中也称为学习率),随着更多树的加入,该参数将导致集成学习缓慢
:梯度升级参数,也称为步长
因此,每片叶子的预测分数就是新树的最终分数,它就是每片叶子的总和:

因此,总结来说,GBM 的工作原理是逐渐增加沿着梯度学习的更精确的树。
现在我们已经理解了核心概念,让我们运行一个 GBM 示例,看看最重要的参数。对于 GBM 来说,这些参数特别重要,因为当我们将树的数量设置得太高时,我们必然会成倍地消耗计算资源。所以要小心这些参数。Scikit-learn 的 GBM 应用中的大多数参数与我们在上一段中介绍的随机森林中的参数相同。我们需要考虑三个需要特别注意的参数。
最大深度
与随机森林相反,随机森林在将树结构构建到最大扩展时表现更好(从而构建和集合具有高方差的预测因子),GBM 倾向于更好地处理较小的树(从而利用具有较高偏差的预测因子,即弱学习者)。使用较小的决策树或仅使用树桩(只有一个分支的决策树)可以减少训练时间,在执行速度和较大偏差之间进行权衡(因为较小的树很难截取数据中更复杂的关系)。
学习率
也称为收缩
,这是一个与梯度下降优化和相关的参数,每个树将如何为集成做出贡献。该参数的较小值可以改善训练过程中的优化,尽管这将需要更多的估计器来收敛,从而需要更多的计算时间。由于它会影响集合中每棵树的权重,较小的值意味着每棵树对优化过程的贡献很小,在找到好的解决方案之前,您需要更多的树。因此,在为性能优化该参数时,我们应该避免可能导致次优模型的过大值;我们还必须避免使用太低的值,因为这将严重影响计算时间(集成需要更多的树来收敛到一个解)。根据我们的经验,一个好的起点是使用范围在< 0.1 和>001 之间的学习率。
子样本
让我们回忆一下装袋和粘贴的原理,我们随机抽取样本并在那些样本上构建树。如果我们对 GBM 应用子采样,我们会随机化树结构,防止过度拟合,减少内存负载,甚至有时会提高精度。我们也可以将这个过程应用于 GBM,使它更随机,从而利用打包的优势。我们可以通过将子样本参数设置为. 5 来随机化 GBM 中的树结构。
更快的 GBM,带热启动
该参数允许在每次迭代添加到前一次迭代后存储新的树信息,而不需要生成新的树。这样,我们可以节省内存,并大大加快计算时间。
使用 Scikit-learn 中提供的 GBM,我们可以采取两种措施来提高内存和 CPU 效率:
- (半)增量学习的热启动
- 我们可以在交叉验证期间使用并行处理
让我们运行一个 GBM 分类示例,其中我们使用了来自 UCI 机器学习库的垃圾邮件数据集。我们将首先加载数据,对其进行预处理,并查看每个特征的可变重要性:
import pandas
import urllib2
import urllib2
from sklearn import ensemble
columnNames1_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/spambase/spambase.names'
columnNames1 = [
line.strip().split(':')[0]
for line in urllib2.urlopen(columnNames1_url).readlines()[33:]]
columnNames1
n = 0
for i in columnNames1:
columnNames1[n] = i.replace('word_freq_','')
n += 1
print columnNames1
spamdata = pandas.read_csv(
'https://archive.ics.uci.edu/ml/machine-learning-databases/spambase/spambase.data',
header=None, names=(columnNames1 + ['spam'])
)
X = spamdata.values[:,:57]
y=spamdata['spam']
spamdata.head()
import numpy as np
from sklearn import cross_validation
from sklearn.metrics import classification_report
from sklearn.cross_validation import cross_val_score
from sklearn.cross_validation import cross_val_predict
from sklearn.cross_validation import train_test_split
from sklearn.metrics import recall_score, f1_score
from sklearn.cross_validation import cross_val_predict
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.ensemble import GradientBoostingClassifier
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.3, random_state=22)
clf = ensemble.GradientBoostingClassifier(n_estimators=300,random_state=222,max_depth=16,learning_rate=.1,subsample=.5)
scores=clf.fit(X_train,y_train)
scores2 = cross_val_score(clf, X_train, y_train, cv=3, scoring='accuracy',n_jobs=-1)
print scores2.mean()
y_pred = cross_val_predict(clf, X_test, y_test, cv=10)
print 'validation accuracy %s' % accuracy_score(y_test, y_pred)
OUTPUT:]
validation accuracy 0.928312816799
confusionMatrix = confusion_matrix(y_test, y_pred)
print confusionMatrix
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_pred)
clf.feature_importances_
def featureImp_order(clf, X, k=5):
return X[:,clf.feature_importances_.argsort()[::-1][:k]]
newX = featureImp_order(clf,X,2)
print newX
# let's order the features in amount of importance
print sorted(zip(map(lambda x: round(x, 4), clf.feature_importances_), columnNames1),
reverse=True)
OUTPUT]
0.945030177548
precision recall f1-score support
0 0.93 0.96 0.94 835
1 0.93 0.88 0.91 546
avg / total 0.93 0.93 0.93 1381
[[799 36]
[ 63 483]]
Feature importance:
[(0.2262, 'char_freq_;'),
(0.0945, 'report'),
(0.0637, 'capital_run_length_average'),
(0.0467, 'you'),
(0.0461, 'capital_run_length_total')
(0.0403, 'business')
(0.0397, 'char_freq_!')
(0.0333, 'will')
(0.0295, 'capital_run_length_longest')
(0.0275, 'your')
(0.0259, '000')
(0.0257, 'char_freq_(')
(0.0235, 'char_freq_$')
(0.0207, 'internet')
我们可以看到这个字符;在垃圾邮件的分类上是最有区别的。
注
可变重要性向我们展示了每个特征的拆分在多大程度上减少了树中所有拆分的相对杂质。
暖机加速 warm _ start
遗憾的是,Scikit-learn 中没有针对 GBM 的并行处理。只有交叉验证和 gridsearch 可以并行化。那么,我们能做些什么来加快速度呢?我们看到 GBM 的工作原理是加法扩展,其中树是递增添加的。我们可以在 Scikit-learn 中用warm-start参数利用这个想法。我们可以用 Scikit-learn 的 GBM 功能对此进行建模,方法是使用方便的 for 循环逐步构建树模型。因此,让我们用相同的数据集来做这件事,并检查它提供的计算优势:
gbc = GradientBoostingClassifier(warm_start=True, learning_rate=.05, max_depth=20,random_state=0)
for n_estimators in range(1, 1500, 100):
gbc.set_params(n_estimators=n_estimators)
gbc.fit(X_train, y_train)
y_pred = gbc.predict(X_test)
print(classification_report(y_test, y_pred))
print(gbc.set_params)
OUTPUT:
precision recall f1-score support
0 0.93 0.95 0.94 835
1 0.92 0.89 0.91 546
avg / total 0.93 0.93 0.93 1381
<bound method GradientBoostingClassifier.set_params of GradientBoostingClassifier(init=None, learning_rate=0.05, loss='deviance',
max_depth=20, max_features=None, max_leaf_nodes=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=1401,
presort='auto', random_state=0, subsample=1.0, verbose=0,
warm_start=True)>
建议特别注意树的设置输出(n_estimators=1401)。你可以看到我们用的树的大小是 1401。当我们将其与类似的 GBM 模型进行比较时,这个小技巧帮助我们减少了大量的训练时间(想想一半甚至更少),我们会同时用 1401 棵树进行训练。请注意,我们可以将此方法用于随机森林和极端随机森林。然而,我们发现这对于 GBM 特别有用。
让我们看看显示常规 GBM 的训练时间和我们的warm_start方法的图。计算速度相当快,精度保持相对不变:

训练和存储 GBM 模型
有没有想过同时在三台电脑上训练一个模型?还是在 EC2 实例上训练 GBM 模型?可能会有这样一种情况:你训练一个模型,并希望存储它,以便以后再次使用。当你要等两天才能完成一轮完整的训练时,我们不想再次经历这个过程。在一个案例中,您已经在亚马逊 EC2 实例上的云中训练了一个模型,您可以存储该模型,并在以后使用 Scikit-learn 的joblib在另一台计算机上重用它。因此,让我们走完这个过程,因为 Scikit-learn 为我们提供了一个方便的工具来管理它。
让我们导入正确的库,并为文件位置设置目录:
import errno
import os
#set your path here
path='/yourpath/clfs'
clfm=os.makedirs(path)
os.chdir(path)
现在让我们将模型导出到硬盘上的指定位置:
from sklearn.externals import joblib
joblib.dump( gbc,'clf_gbc.pkl')
现在,我们可以加载模型并将其重新用于其他目的:
model_clone = joblib.load('clf_gbc.pkl')
zpred=model_clone.predict(X_test)
print zpred
XGBoost
我们刚刚讨论过,当使用 Scikit-learn 的 GBM 时,没有并行处理的选项,这正是 XGBoost 的用武之地。在 GBM 的基础上,XGBoost 引入了更具可扩展性的方法,在单台机器上利用多线程,在多台服务器的集群上利用并行处理(使用分片)。XGBoost 相对于 GBM 最重要的改进在于后者管理稀疏数据的能力。XGBoost 自动接受稀疏数据作为输入,而不在内存中存储零值。XGBoost 的第二个好处在于在分支树时计算最佳节点分割值的方式,这种方法被称为分位数草图。这种方法通过加权算法对数据进行转换,以便根据一定的准确度对候选拆分进行排序。更多信息请阅读 http://arxiv.org/pdf/1603.02754v3.pdf 的文章。
XGBoost 代表 Extreme Gradient Boosting,这是一种开源的梯度 Boosting 算法,在数据科学竞赛中获得了大量的人气,比如 Kaggle(https://www.kaggle.com/)和 KDD 杯 2015。(如我们在第 1 章、可扩展性第一步中所述,代码可在 https://github.com/dmlc/XGBoost的 GitHub 上获得。)正如作者(陈天奎、佟赫和卡洛斯·盖斯特林)在他们的算法 XGBoost 上写的论文中所报告的那样,在 2015 年在 Kaggle 上举行的 29 场挑战中,17 场获胜的解决方案将 XGBoost 作为独立的或多种模型的某种集成的一部分。在他们的论文 XGBoost:一个可扩展的树促进系统(可以在http://learningsys.org/papers/LearningSys_2015_paper_32.pdf找到)中,作者报告说,在最近的 2015 年 KDD 杯中,XGBoost 被每个进入比赛前十的队伍使用。除了在准确性和计算效率方面的成功表现之外,我们在本书中主要关注的是可伸缩性,从不同的角度来看,XGBoost 确实是一个可伸缩的解决方案。XGBoost 是新一代 GBM 算法,对初始树 boost GBM 算法进行了重要调整。XGBoost 提供并行处理;该算法提供的可伸缩性是由于其作者开发了许多新的调整和添加:
- 一种接受稀疏数据的算法,可以利用稀疏矩阵,节省内存(不需要密集矩阵)和计算时间(零值以特殊方式处理)
- 一种近似树学习(加权分位数草图),具有类似的结果,但比经典的对可能的分支切割的完整探索花费的时间少得多
- 单台机器上的并行计算(在搜索最佳分割的阶段使用多线程)和多台机器上类似的分布式计算
- 利用名为“列块”的数据存储解决方案,在单台机器上进行核外计算,该解决方案按列在磁盘上排列数据,从而按照优化算法(适用于列向量)的预期,通过从磁盘中提取数据来节省时间
从实用的角度来看,XGBoost 的特性大多与 GBM 的参数相同。XGBoost 也很有能力处理丢失的数据。基于标准决策树的其他树集成要求首先使用一个非标度值(例如一个大的负数)来估算缺失数据,以便开发一个适当的树分支来处理缺失值。取而代之的是,XGBoost 首先拟合所有非缺失值,并在为变量创建分支后,决定哪个分支更适合缺失值,以最小化预测误差。这种方法导致树更紧凑,有效的插补策略导致更强的预测能力。
最重要的 XGBoost 参数如下:
eta(默认值=0.3):这相当于 Scikit-learn 的 GBM 中的学习速率min_child_weight(默认值=1):较高的值可防止过拟合和树的复杂性max_depth(默认值=6):这是树中的交互次数subsample(默认值=1):这是我们在每次迭代中获取的训练数据样本的一小部分colsample_bytree(默认值=1):这是每次迭代中特征的分数lambda(默认值=1):这是 L2 正则化(布尔型)seed(默认值=0):这是 Scikit-learn 的random_state参数的等价物,允许跨多个测试和不同机器的学习过程的再现性
现在我们知道了 XGBoost 最重要的参数,让我们在用于 GBM 的相同数据集上运行一个 XGBoost 示例,并使用相同的参数设置(尽可能多)。XGBoost 使用起来没有 Scikit-learn 包简单。因此,我们将提供一些基本示例,您可以将其用作更复杂模型的起点。在我们深入研究 XGBoost 应用之前,让我们将其与垃圾数据集中sklearn中的 GBM 方法进行比较;我们已经将数据加载到内存中:
import xgboost as xgb
import numpy as np
from sklearn.metrics import classification_report
from sklearn import cross_validation
clf = xgb.XGBClassifier(n_estimators=100,max_depth=8,
learning_rate=.1,subsample=.5)
clf1 = GradientBoostingClassifier(n_estimators=100,max_depth=8,
learning_rate=.1,subsample=.5)
%timeit xgm=clf.fit(X_train,y_train)
%timeit gbmf=clf1.fit(X_train,y_train)
y_pred = xgm.predict(X_test)
y_pred2 = gbmf.predict(X_test)
print 'XGBoost results %r' % (classification_report(y_test, y_pred))
print 'gbm results %r' % (classification_report(y_test, y_pred2))
OUTPUT:
1 loop, best of 3: 1.71 s per loop
1 loop, best of 3: 2.91 s per loop
XGBoost results ' precision recall f1-score support\n\n 0 0.95 0.97 0.96 835\n 1 0.95 0.93 0.94 546\n\navg / total 0.95 0.95 0.95 1381\n'
gbm results ' precision recall f1-score support\n\n 0 0.95 0.97 0.96 835\n 1 0.95 0.92 0.93 546\n\navg / total 0.95 0.95 0.95 1381\n
我们可以清楚地看到,尽管我们没有对 xboost 使用并行化,但 xboost 比 GBM 快得多(1.71s 对 2.91s)。后来,当我们应用内存外流时,当我们对 XGBoost 使用并行化和核外方法时,我们甚至可以达到更大的加速。在某些情况下,XGBoost 模型比 GBM 具有更高的精度,但是(几乎)从来没有相反的结果。
XGBoost 回归
增强方法通常用于分类,但对于回归任务也非常有效。由于回归经常被忽略,让我们运行一个回归示例,并遍历关键问题。让我们用 gridsearch 在加州住宅区安装一个助推模型。加州房屋数据集最近被添加到 Scikit-learn 中,这为我们节省了一些预处理步骤:
import numpy as np
import scipy.sparse
import xgboost as xgb
import os
import pandas as pd
from sklearn.cross_validation import train_test_split
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.metrics import mean_squared_error
pd=fetch_california_housing()
#because the y variable is highly skewed we apply the log transformation
y=np.log(pd.target)
X_train, X_test, y_train, y_test = train_test_split(pd.data,
y,
test_size=0.15,
random_state=111)
names = pd.feature_names
print names
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
from sklearn.grid_search import GridSearchCV
clf=xgb.XGBRegressor(gamma=0,objective= "reg:linear",nthread=-1)
clf.fit(X_train,y_train)
y_pred = clf.predict(X_test)
print 'score before gridsearch %r' % mean_squared_error(y_test, y_pred)
params = {
'max_depth':[4,6,8],
'n_estimators':[1000],
'min_child_weight':range(1,3),
'learning_rate':[.1,.01,.001],
'colsample_bytree':[.8,.9,1]
,'gamma':[0,1]}
#with the parameter nthread we specify XGBoost for parallelisation
cvx = xgb.XGBRegressor(objective= "reg:linear",nthread=-1)
clf=GridSearchCV(estimator=cvx,param_grid=params,n_jobs=-1,scoring='mean_absolute_error',verbose=True)
clf.fit(X_train,y_train)
print clf.best_params_
y_pred = clf.predict(X_test)
print 'score after gridsearch %r' %mean_squared_error(y_test, y_pred)
#Your output might look a little different based on your hardware.
OUTPUT
['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude']
score before gridsearch 0.07110580252173157
Fitting 3 folds for each of 108 candidates, totalling 324 fits
[Parallel(n_jobs=-1)]: Done 34 tasks | elapsed: 1.9min
[Parallel(n_jobs=-1)]: Done 184 tasks | elapsed: 11.3min
[Parallel(n_jobs=-1)]: Done 324 out of 324 | elapsed: 22.3min finished
{'colsample_bytree': 0.8, 'learning_rate': 0.1, 'min_child_weight': 1, 'n_estimators': 1000, 'max_depth': 8, 'gamma': 0}
score after gridsearch 0.049878294113796254
通过 gridsearch,我们已经能够大大提高我们的分数;你可以看到我们 gridsearch 的最佳参数。你可以在 sklearn 中看到它与常规助推方法的相似之处。但是,默认情况下,XGBoost 会在所有可用内核上并行化该算法。您可以通过将n_estimators参数增加到 2500 或 3000 左右来提高模型的性能。然而,我们发现,对于计算机功能较弱的读者来说,培训时间会有点太长。
XGBoost 和可变重要性
XGBoost 有一些非常实用的内置功能来绘制变量的重要性。首先,相对于手头的模型,有一个方便的特征选择工具。您可能知道,可变重要性是基于树结构中每个特征的相对影响。它为特征选择和洞察预测模型的本质提供了实用的方法。让我们看看如何用 XGBoost 绘制重要性:
import numpy as np
import os
from matplotlib import pylab as plt
# %matplotlib inline <- this only works in jupyter notebook
#our best parameter set
# {'colsample_bytree': 1, 'learning_rate': 0.1, 'min_child_weight': 1, 'n_estimators': 500, #'max_depth': 8, 'gamma': 0}
params={'objective': "reg:linear",
'eval_metric': 'rmse',
'eta': 0.1,
'max_depth':8,
'min_samples_leaf':4,
'subsample':.5,
'gamma':0
}
dm = xgb.DMatrix(X_train, label=y_train,
feature_names=names)
regbgb = xgb.train(params, dm, num_boost_round=100)
np.random.seed(1)
regbgb.get_fscore()
regbgb.feature_names
regbgb.get_fscore()
xgb.plot_importance(regbgb,color='magenta',title='california-housing|variable importance')

应谨慎使用要素重要性(对于 GBM 和随机森林也是如此)。特征重要性度量标准完全基于用特定模型的参数训练的特定模型上构建的树结构。这意味着如果我们改变模型的参数,重要性度量和一些排名也会改变。因此,重要的是要注意,对于任何重要性度量,它们都不应该被认为是跨模型的通用变量结论。
xboost 流式处理大型数据集
就准确性/性能权衡而言,这是最好的桌面解决方案。我们看到,在之前的随机森林示例中,我们需要执行二次采样,以防止我们的主内存过载。
XGBoost 的一个经常被忽视的功能是通过内存传输数据的方法。这个方法以分阶段的方式通过主内存解析数据,随后被解析成 XGBoost 模型训练。这种方法是在大数据集上训练模型的先决条件,而大数据集是不可能放入主存的。使用 XGBoost 进行流式处理仅适用于 LIBSVM 文件,这意味着我们首先必须将数据集解析为 LIBSVM 格式,并将其导入为 XGBoost 保留的内存缓存中。另外需要注意的是,我们使用不同的方法来实例化 XGBoost 模型。XGBoost 的类似 Scikit-learn 的接口只适用于常规的 NumPy 对象。让我们看看这是如何工作的。
首先,我们需要加载 LIBSVM 格式的数据集,并将其拆分为训练集和测试集,然后我们继续进行预处理和训练。不幸的是,使用这个 XGBoost 方法无法使用 gridsearch 进行参数调整。如果要调优参数,我们需要将 LIBSVM 文件转换成 Numpy 对象,这样会将数据从内存缓存转储到主内存。不幸的是,这是不可扩展的,因此如果您想要对大型数据集执行调优,我建议使用我们之前介绍的储层采样工具,并对子样本进行调优:
import urllib
from sklearn.datasets import dump_svmlight_file
from sklearn.datasets import load_svmlight_file
trainfile = urllib.URLopener()
trainfile.retrieve("http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/poker.bz2", "pokertrain.bz2")
X,y = load_svmlight_file('pokertrain.bz2')
dump_svmlight_file(X, y,'pokertrain', zero_based=True,query_id=None, multilabel=False)
testfile = urllib.URLopener()
testfile.retrieve("http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/poker.t.bz2", "pokertest.bz2")
X,y = load_svmlight_file('pokertest.bz2')
dump_svmlight_file(X, y,'pokertest', zero_based=True,query_id=None, multilabel=False)
del(X,y)
from sklearn.metrics import classification_report
import numpy as np
import xgboost as xgb
dtrain = xgb.DMatrix('/yourpath/pokertrain#dtrain.cache')
dtest = xgb.DMatrix('/yourpath/pokertest#dtestin.cache')
# For parallelisation it is better to instruct "nthread" to match the exact amount of cpu cores you want #to use.
param = {'max_depth':8,'objective':'multi:softmax','nthread':2,'num_class':10,'verbose':True}
num_round=100
watchlist = [(dtest,'eval'), (dtrain,'train')]
bst = xgb.train(param, dtrain, num_round,watchlist)
print bst
OUTPUT:
[89] eval-merror:0.228659 train-merror:0.016913
[90] eval-merror:0.228599 train-merror:0.015954
[91] eval-merror:0.227671 train-merror:0.015354
[92] eval-merror:0.227777 train-merror:0.014914
[93] eval-merror:0.226247 train-merror:0.013355
[94] eval-merror:0.225397 train-merror:0.012155
[95] eval-merror:0.224070 train-merror:0.011875
[96] eval-merror:0.222421 train-merror:0.010676
[97] eval-merror:0.221881 train-merror:0.010116
[98] eval-merror:0.221922 train-merror:0.009676
[99] eval-merror:0.221733 train-merror:0.009316
我们真的可以从内存 XGBoost 中体验到巨大的加速。如果我们使用内存版本,我们将需要更多的训练时间。在本例中,我们已经将测试集作为一轮验证包含在观察列表中。然而,如果我们想预测未知数据的值,我们可以简单地使用与 Scikit-learn 和 XGBoost 中任何其他模型相同的预测程序:
bst.predict(dtest)
OUTPUT:
array([ 0., 0., 1., ..., 0., 0., 1.], dtype=float32)
XGBoost 模型持久性
在前一章中,我们介绍了如何将 GBMmodel 存储到磁盘,以便以后导入并用于预测。XGBoost 提供同样的功能。让我们看看如何存储和导入模型:
import pickle
bst.save_model('xgb.model')
现在,您可以从之前指定的目录中导入保存的模型:
imported_model = xgb.Booster(model_file='xgb.model')
太好了,现在你可以用这个模型来预测:
imported_model.predict(dtest)
OUTPUT array([ 9., 9., 9., ..., 1., 1., 1.], dtype=float32)
带 H2O 的堆外推车
到目前为止,我们只处理 CART 机型的桌面解决方案。在第 4 章、神经网络和深度学习中,我们介绍了 H2O 的深度记忆外学习,它提供了一种强大的可扩展方法。幸运的是,H2O 还利用其强大的并行 Hadoop 生态系统提供了树集成方法。由于我们在前面的章节中已经详细介绍了 GBM 和随机森林,让我们马上开始吧。在本练习中,我们将使用之前使用的垃圾邮件数据集。
H2O 的随机森林和网格研究
让我们用 gridsearch 超参数优化实现一个随机森林。在本节中,我们首先从网址源加载垃圾邮件数据集:
import pandas as pd
import numpy as np
import os
import xlrd
import urllib
import h2o
#set your path here
os.chdir('/yourpath/')
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/spambase/spambase.data'
filename='spamdata.data'
urllib.urlretrieve(url, filename)
现在我们已经加载了数据,我们可以初始化 H2O 会话:
h2o.init(max_mem_size_GB = 2)
OUTPUT:

这里,我们对数据进行预处理,将数据分为训练集、验证集和测试集。我们用 H2O 函数(.split_frame)来做这件事。还要注意我们将目标向量C58转换为因子变量的重要步骤:
spamdata = h2o.import_file(os.path.realpath("/yourpath/"))
spamdata['C58']=spamdata['C58'].asfactor()
train, valid, test= spamdata.split_frame([0.6,.2], seed=1234)
spam_X = spamdata.col_names[:-1]
spam_Y = spamdata.col_names[-1]
在这一部分,我们将设置我们将使用 gridsearch 优化的参数。首先,我们将模型中棵树的数量设置为单个值 300。使用 gridsearch 迭代的参数如下:
max_depth:树的最大深度balance_classes:每一次迭代都为目标结果使用平衡类sample_rate:这是每次迭代采样的行数的分数
现在,让我们将这些参数传递到 Python 列表中,以便在我们的 H2O 网格搜索模型中使用:
hyper_parameters={'ntrees':[300], 'max_depth':[3,6,10,12,50],'balance_classes':['True','False'],'sample_rate':[.5,.6,.8,.9]}
grid_search = H2OGridSearch(H2ORandomForestEstimator, hyper_params=hyper_parameters)
grid_search.train(x=spam_X, y=spam_Y,training_frame=train)
print 'this is the optimum solution for hyper parameters search %s' % grid_search.show()
OUTPUT:

在所有可能的组合中,行采样率为. 9、树深度为 50、平衡类的模型产生最高的精度。现在,让我们训练一个新的随机森林模型,该模型具有我们的网格搜索得到的最佳参数,并预测测试集的结果:
final = H2ORandomForestEstimator(ntrees=300, max_depth=50,balance_classes=True,sample_rate=.9)
final.train(x=spam_X, y=spam_Y,training_frame=train)
print final.predict(test)
H2O 预测的最终输出产生一个数组,第一列包含实际预测的类别,第二列包含每个目标标签的类别概率:
OUTPUT:

H2O 随机梯度推进与网格研究
我们在前面的例子中已经看到,大多数情况下,一个调整良好的 GBM 模型比随机的森林表现更好。所以现在让我们在 H2O 进行一次 T2 网格搜索,看看我们是否能提高 T3 的分数。在本次会议中,我们将介绍与 H2O 随机森林模型相同的随机二次抽样方法。基于 Jerome Friedman 1999 年的文章(https://statweb.stanford.edu/~jhf/ftp/stobst.pdf),介绍了一种名为随机梯度推进的方法。添加到模型中的这种随机性利用了随机二次抽样,而无需在每次树迭代时从数据中进行替换,这被认为是为了防止过度拟合并提高整体精度。在这个例子中,我们通过在每次迭代中引入基于特征的随机子采样,进一步利用了随机性的思想。
这种随机子采样特征的方法也被称为随机子空间方法,我们已经在本章的随机森林和极随机森林部分看到过。我们通过col_sample_rate参数实现这一点。总而言之,在这个 GBM 模型中,我们将对以下参数执行 gridsearch 优化:
max_depth:最大树深sample_rate:每次迭代使用的行数的分数col_sample_rate:每次迭代使用的特征的分数
我们使用与上一节完全相同的垃圾邮件数据集,因此我们可以直接进入该数据集:
hyper_parameters={'ntrees':[300],'max_depth':[12,30,50],'sample_rate':[.5,.7,1],'col_sample_rate':[.9,1],
'learn_rate':[.01,.1,.3],}
grid_search = H2OGridSearch(H2OGradientBoostingEstimator, hyper_params=hyper_parameters)
grid_search.train(x=spam_X, y=spam_Y, training_frame=train)
print 'this is the optimum solution for hyper parameters search %s' % grid_search.show()

gbm Grid Build Progress: [##################################################] 100%
gridsearch 输出的上半部分显示,我们应该使用异常高的. 3 学习率、0 . 9 的列采样率和 30 的最大树深度。基于行的随机子采样没有提高性能,但是基于分数为. 9 的特征的子采样在这种情况下非常有效。现在让我们训练一个新的 GBM 模型,其最佳参数来自我们的网格搜索优化,并预测测试集的结果:
spam_gbm2 = H2OGradientBoostingEstimator(
ntrees=300,
learn_rate=0.3,
max_depth=30,
sample_rate=1,
col_sample_rate=0.9,
score_each_iteration=True,
seed=2000000
)
spam_gbm2.train(spam_X, spam_Y, training_frame=train, validation_frame=valid)
confusion_matrix = spam_gbm2.confusion_matrix(metrics="accuracy")
print confusion_matrix
OUTPUT:

这为模型的性能提供了有趣的诊断,例如accuracy、rmse、logloss和AUC。但是,它的输出太大,这里不包括。查看 IPython 笔记本的输出,了解完整的输出。
您可以通过以下方式利用这一点:
print spam_gbm2.score_history()
当然,最终的预测可以通过以下方式实现:
print spam_gbm2.predict(test)
太好了,我们已经能够将模型的准确性提高到接近 100%。如您所见,在 H2O,您在建模和管理数据方面可能不太灵活,但是的处理速度和的准确性是无与伦比的。要结束此会话,您可以执行以下操作:
h2o.shutdown(prompt=False)
总结
我们看到,使用集成例程训练的 CART 方法在预测准确性方面非常强大。然而,它们在计算上可能很昂贵,我们已经在 sklearn 的应用中介绍了一些加速它们的技术。我们注意到,如果使用得当,使用随机搜索调整的极端随机森林可以加快 10 倍的速度。然而,对于 GBM 来说,sklearn 中没有实现并行化,而这正是 XGBoost 的用武之地。
XGBoost 附带了一个有效的并行增强算法,可以很好地加速算法。当我们使用更大的文件(> 100k 个训练示例)时,有一种核心外的方法可以确保我们在训练模型时不会过载主内存。
速度和记忆力最大的进步可以从 H2O 身上找到;我们看到了强大的调优能力以及令人印象深刻的训练速度。
七、大规模无监督学习
在前几章中,问题的焦点是预测一个变量,它可能是一个数字、类或类别。在这一章中,我们将改变方法,并尝试在规模上创建新的特征和变量,希望比已经包含在观察矩阵中的特征和变量更好地用于我们的预测目的。我们将首先介绍无监督方法,并举例说明其中三种能够扩展到大数据的方法:
- 主成分分析 ( 主成分分析),一种减少特征数量的有效方法
- K-means ,一种可扩展的聚类算法
- 潜在狄利克雷分配 ( LDA ),一种能够从一系列文本文档中提取主题的非常有效的算法
无监督方法
无监督学习是机器学习的一个分支,其算法从没有明确标签(无标签数据)的数据中揭示推理。这种技术的目标是提取隐藏的模式,并将相似的数据分组。
在这些算法中,每个观察的未知感兴趣参数(例如组成员和主题组成)通常被建模为潜在变量(或一系列隐藏变量),隐藏在不能直接观察的观察变量系统中,而只能从系统的过去和现在的输出中推导出来。通常,系统的输出包含噪声,这使得操作更加困难。
在常见问题中,无监督方法主要用于两种情况:
- 使用标记数据集提取要由分类器/回归器向下处理到处理链的附加特征。通过附加功能的增强,它们可能会表现得更好。
- 用有标签或无标签的数据集来提取一些关于数据结构的信息。这类算法通常在建模的探索性数据分析 ( EDA )阶段使用。
首先,在开始演示之前,让我们沿着笔记本中的章节导入必要的模块:
In : import matplotlib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import pylab
%matplotlib inline
import matplotlib.cm as cm
import copy
import tempfile
import os
特征分解–主成分分析
主成分分析是一种常用的算法,用于分解输入信号的维度,只保留主成分的维度。从数学角度来看,主成分分析对观测矩阵进行正交变换,输出一组线性不相关变量,称为主成分。输出变量形成一个基集,其中每个分量都与其他分量正交。此外,可以对输出分量进行排序(以便仅使用主分量),因为第一个分量包含输入数据集的最大可能方差,第二个分量与第一个分量正交(根据定义),包含残差信号的最大可能方差,第三个分量与前两个分量正交,并且基于残差方差,以此类推。
主成分分析的一般变换可以表示为空间的投影。如果只从变换基中提取主成分,输出空间将比输入空间具有更小的维度。数学上,它可以表达如下:

这里, X 是维度 N 训练集的泛点, T 是来自 PCA 的变换矩阵,
是输出向量。请注意,符号表示该矩阵方程中的点积。从实用的角度来看,还要注意的是 X 的所有特性在做这个操作之前都必须以零为中心。
现在让我们从一个实际的例子开始;后面,我们将深入讲解数学 PCA。在本例中,我们将创建一个由两个点组成的虚拟数据集,一个点在(-5,0)中居中,另一个点在(5,5)中居中。让我们使用主成分分析来转换数据集,并将输出与输入进行比较。在这个简单的例子中,我们将使用所有的特征,也就是说,我们将不执行特征约简:
In:from sklearn.datasets.samples_generator import make_blobs
from sklearn.decomposition import PCA
X, y = make_blobs(n_samples=1000, random_state=101, \
centers=[[-5, 0], [5, 5]])
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
pca_comp = pca.components_.T
test_point = np.matrix([5, -2])
test_point_pca = pca.transform(test_point)
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='none')
plt.quiver(0, 0, pca_comp[:,0], pca_comp[:,1], width=0.02, \
scale=5, color='orange')
plt.plot(test_point[0, 0], test_point[0, 1], 'o')
plt.title('Input dataset')
plt.subplot(1, 2, 2)
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, edgecolors='none')
plt.plot(test_point_pca[0, 0], test_point_pca[0, 1], 'o')
plt.title('After "lossless" PCA')
plt.show()

如您所见,输出比原始要素的空间更有条理,如果下一个任务是分类,它将只需要数据集的一个要素,节省了几乎 50%的空间和所需的计算。在图像中,您可以清楚地看到 PCA 的核心:它只是输入数据集到左侧图像中以橙色绘制的变换基础的投影。你不确定吗?让我们测试一下:
In:print "The blue point is in", test_point[0, :]
print "After the transformation is in", test_point_pca[0, :]
print "Since (X-MEAN) * PCA_MATRIX = ", np.dot(test_point - \
pca.mean_, pca_comp)
Out:The blue point is in [[ 5 -2]]
After the transformation is in [-2.34969911 -6.2575445 ]
Since (X-MEAN) * PCA_MATRIX = [[-2.34969911 -6.2575445 ]
现在,我们来挖掘核心问题:如何从训练集中生成 T?它应该包含正交向量,向量应该根据它们可以解释的方差量(即观测矩阵携带的能量或信息)进行排序。已经实现了很多解决方案,但是最常见的实现是基于奇异值分解 ( 奇异值分解)。
奇异值分解是一种将任意矩阵 M 分解成三个具有特殊性质的矩阵
的技术,这三个矩阵的乘法又返回了 M :

具体来说,给定 M ,一个由 m 行和 n 列组成的矩阵,等价的结果元素如下:
- U 是矩阵 m x m (正方形矩阵),它是酉的,它的列构成了正交的基。同样,它们被命名为左奇异向量,或者输入奇异向量,它们是矩阵乘积
的特征向量。
是一个矩阵 m x n ,它的对角线上只有非零元素。这些值被称为奇异值,都是非负的,并且都是
和
的特征值。- w 是酉矩阵 n x n (方阵),它的列构成一个正交基,它们被命名为右(或输出)奇异向量。同样,它们是矩阵乘积
的特征向量。
为什么需要这样?解决方案非常简单:主成分分析的目标是尝试并估计输入数据集方差较大的方向。为此,我们首先需要去除每个特征的均值,然后对协方差矩阵
进行运算。
假设,通过用 SVD 分解矩阵 X ,我们得到了矩阵 W 的列,它们是协方差的主成分(即,我们要寻找的矩阵 T ),
的对角线包含由主成分解释的方差, U 的列是主成分。这就是为什么主成分分析总是用奇异值分解来完成。
现在我们来看一个真实的例子。让我们在 Iris 数据集上进行测试,提取前两个主成分(即从一个由四个特征组成的数据集传递到一个由两个特征组成的数据集):
In:from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
print "Iris dataset contains", X.shape[1], "features"
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
print "After PCA, it contains", X_pca.shape[1], "features"
print "The variance is [% of original]:", \
sum(pca.explained_variance_ratio_)
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, edgecolors='none')
plt.title('First 2 principal components of Iris dataset')
plt.show()
Out:Iris dataset contains 4 features
After PCA, it contains 2 features
The variance is [% of original]: 0.977631775025

这是对过程输出的分析:
- 解释的方差几乎是来自输入的原始方差的 98%。特征数量减少了一半,但只有 2%的信息不在输出中,希望只是噪音。
- 从视觉观察来看,组成 Iris 数据集的不同类似乎是相互分离的。这意味着,在这样一个约简集上工作的分类器在准确性方面将具有相当的性能,但是训练和运行预测将更快。
作为第二点的证明,让我们现在尝试训练和测试两个分类器,一个使用原始数据集,另一个使用约简集,并打印它们的准确性:
In:from sklearn.linear_model import SGDClassifier
from sklearn.cross_validation import train_test_split
from sklearn.metrics import accuracy_score
def test_classification_accuracy(X_in, y_in):
X_train, X_test, y_train, y_test = \
train_test_split(X_in, y_in, random_state=101, \
train_size=0.50)
clf = SGDClassifier('log', random_state=101)
clf.fit(X_train, y_train)
return accuracy_score(y_test, clf.predict(X_test))
print "SGDClassifier accuracy on Iris set:", \
test_classification_accuracy(X, y)
print "SGDClassifier accuracy on Iris set after PCA (2 components):", \
test_classification_accuracy(X_pca, y)
Out:SGDClassifier accuracy on Iris set: 0.586666666667
SGDClassifier accuracy on Iris set after PCA (2 components): 0.72
如您所见,这种技术不仅降低了学习者在链中的复杂性和空间,而且有助于实现泛化(完全像 Ridge 或 Lasso 正则化)。
现在,如果您不确定输出中应该有多少组件,通常根据经验,选择能够解释至少 90%(或 95%)输入差异的最小数量。从经验来看,这样的选择通常会确保只有噪音被切断。
到目前为止,一切似乎都很完美:我们找到了一个减少特征数量的很好的解决方案,构建了一些具有很高预测能力的特征,我们也有一个经验法则来猜测它们的正确数量。现在让我们检查一下这个解决方案的可伸缩性:我们正在研究当观察和特征的数量增加时,它是如何伸缩的。首先要注意的是,主成分分析的核心部分——奇异值分解算法不是随机的;因此,它需要整个矩阵,以便能够提取其主成分。现在,让我们看看在一些具有越来越多的特征和观察的合成数据集上,主成分分析在实践中是如何可扩展的。我们将执行完全(无损)分解(实例化对象 PCA 时的参数是None,因为要求较少数量的特征不会影响性能(这只是对 SVD 的输出矩阵进行切片的问题)。
在下面的代码中,我们首先创建包含 10,000 个点和 20,50,100,250,1,000 和 2,500 个要由 PCA 处理的特征的矩阵。然后,我们创建具有 100 个特征以及 1、5、10、25、50 和 100,000 个要用主成分分析处理的观察值的矩阵:
In:import time
def check_scalability(test_pca):
pylab.rcParams['figure.figsize'] = (10, 4)
# FEATURES
n_points = 10000
n_features = [20, 50, 100, 250, 500, 1000, 2500]
time_results = []
for n_feature in n_features:
X, _ = make_blobs(n_points, n_features=n_feature, \
random_state=101)
pca = copy.deepcopy(test_pca)
tik = time.time()
pca.fit(X)
time_results.append(time.time()-tik)
plt.subplot(1, 2, 1)
plt.plot(n_features, time_results, 'o--')
plt.title('Feature scalability')
plt.xlabel('Num. of features')
plt.ylabel('Training time [s]')
# OBSERVATIONS
n_features = 100
n_observations = [1000, 5000, 10000, 25000, 50000, 100000]
time_results = []
for n_points in n_observations:
X, _ = make_blobs(n_points, n_features=n_features, \
random_state=101)
pca = copy.deepcopy(test_pca)
tik = time.time()
pca.fit(X)
time_results.append(time.time()-tik)
plt.subplot(1, 2, 2)
plt.plot(n_observations, time_results, 'o--')
plt.title('Observations scalability')
plt.xlabel('Num. of training observations')
plt.ylabel('Training time [s]')
plt.show()
check_scalability(PCA(None))
Out:

可以清楚地看到,基于 SVD 的 PCA 是不可伸缩的:如果特征数量线性增加,训练算法所需的时间就会呈指数级增加。此外,处理具有几百个观察值的矩阵所需的时间变得太高,并且(图中未显示)内存消耗使得该问题对于家用计算机(具有 16gb 或更少的内存)不可行。似乎很明显,基于奇异值分解的主成分分析不是大数据的解决方案;幸运的是,近年来引入了许多变通方法。在接下来的几节中,你会发现它们的简短介绍。
随机主成分分析
这种技术的正确名称应该是基于随机化奇异值分解的主成分分析,但它已经以随机化主成分分析的名称而流行。随机化背后的核心思想是所有主成分的冗余;事实上,如果方法的目标是降维,那么应该期望在输出中只需要几个向量(K 个主向量)。通过关注寻找最佳 K 个主向量的问题,该算法的规模更大。注意,在这个算法中,K——要输出的主成分个数——是一个关键参数:设置太大,性能不会比 PCA 好;将它设置得太低,用结果向量解释的方差将太低。
如同在 PCA 中一样,我们希望找到包含观测值 X 的矩阵的近似值,例如
;我们还希望矩阵 Q 具有 K 正交列(它们将被称为主成分)。有了奇异值分解,我们现在可以计算小矩阵
的分解。正如我们所证明的,这不会花很长时间。作为
,通过取
,我们现在有了基于低秩奇异值分解
的 X 的截断近似。
从数学上看,这似乎很完美,但仍有两点缺失:随机化有什么作用?如何得到矩阵 Q ?两个问题都在这里回答:提取高斯随机矩阵
,计算 Y 为
。然后 Y 进行 QR 分解,创建
,这里是 Q ,我们要找的 K 正交列的矩阵。
这个分解下面的数学相当重,幸运的是一切都已经在 Scikit-learn 中实现了,所以你不需要去弄清楚如何处理高斯随机变量等等。让我们先看看用随机化主成分分析计算完全(无损)分解时,它的性能有多差:
In:from sklearn.decomposition import RandomizedPCA
check_scalability(RandomizedPCA(None))

表现比经典 PCA 差;在事实上,当要求一组简化的组件时,这种转换非常有效。现在来看看 K=20 时的表现:
In:check_scalability(RandomizedPCA(20))

不出所料,计算非常快;在不到一秒钟的时间内,该算法能够执行最复杂的因子分解。
检查结果和算法,我们仍然注意到一些奇怪的事情:训练数据集 X 必须都适合内存才能被分解,即使在随机化主成分分析的情况下。是否有一个在线版本的主成分分析能够增量拟合主向量,而无需将整个数据集存储在内存中?是的,有——增量主成分分析。
增量主成分分析
增量主成分分析,或称小批量主成分分析,是主成分分析的在线版本。算法的核心非常简单:将该批数据初步拆分为具有相同数量观测值的小批量。(唯一的限制是每个小批量的观察数量应该大于特征的数量。)然后,将第一个小批量居中(去除平均值),并执行其奇异值分解,存储主成分。然后,当下一个小批量进入流程时,它首先居中,然后与从上一个小批量中提取的主要成分堆叠在一起(它们作为附加观察值插入)。现在,执行另一个奇异值分解,并用新的主分量覆盖主分量。这个过程一直持续到最后一个小批量:对于每个小批量,首先是对中,然后是堆叠,最后是奇异值分解。这样做,而不是一个大奇异值分解,我们执行的小奇异值分解和小批量的数量一样多。
正如你所理解的,这种技术并没有优于随机化的主成分分析,但它的目标是在不适合内存的数据集上需要主成分分析时提供一种解决方案(或唯一的解决方案)。增量 PCA 不是为了赢得速度挑战而运行,而是为了限制内存消耗;内存使用在整个训练过程中是恒定的,可以通过设置小批量来调整。根据经验,内存占用量与小批量大小的平方大致相同。
作为一个代码示例,现在让我们检查增量 PCA 如何处理大数据集,在我们的示例中,该数据集由 1000 万个观察值和 100 个特征组成。以前的算法都不能做到这一点,除非你想让你的计算机崩溃(或者见证内存和存储磁盘之间的大量交换)。使用增量主成分分析,这样的任务就变成了小菜一碟,而且考虑到所有因素,这个过程并不那么慢(请注意,我们正在进行完全无损的分解,消耗了稳定的内存量):
In:from sklearn.decomposition import IncrementalPCA
X, _ = make_blobs(100000, n_features=100, random_state=101)
pca = IncrementalPCA(None, batch_size=1000)
tik = time.time()
for i in range(100):
pca.partial_fit(X)
print "PCA on 10M points run with constant memory usage in ", \
time.time() - tik, "seconds"
Out:PCA on 10M points run with constant memory usage in 155.642718077 seconds
稀疏主成分分析
稀疏主成分分析的运行方式不同于以前的算法;它不是使用应用在协方差矩阵上的奇异值分解来操作特征约简(居中后),而是对该矩阵进行类似特征选择的操作,找到最佳重建数据的稀疏分量集。与套索正则化一样,稀疏度可以通过对系数的惩罚(或约束)来控制。
关于主成分分析,稀疏主成分分析不能保证得到的分量是正交的,但是结果更容易解释,因为主向量实际上是输入数据集的一部分。此外,它在特征数量方面是可伸缩的:如果当特征数量变大时(假设超过 1000 个),PCA 及其可伸缩版本被卡住,稀疏 PCA 在速度方面仍然是一个最佳解决方案,这要归功于解决 Lasso 问题的内部方法,通常基于 Lars 或坐标下降。(记住套索试图最小化系数的 L1 范数。)此外,当特征的数量大于观察的数量时,例如,一些图像数据集,这很好。
现在让我们看看它如何在包含 10,000 个要素的 25,000 个观测数据集中工作。对于这个例子,我们使用的是SparsePCA算法的小批量版本,它确保了恒定的内存使用,并且能够处理大规模数据集,最终大于可用内存(注意,批量版本被命名为SparsePCA,但不支持在线训练):
In:from sklearn.decomposition import MiniBatchSparsePCA
X, _ = make_blobs(25000, n_features=10000, random_state=101)
tik = time.time()
pca = MiniBatchSparsePCA(20, method='cd', random_state=101, \
n_iter=1000)
pca.fit(X)
print "SparsePCA on matrix", X.shape, "done in ", time.time() - \
tik, "seconds"
Out:
SparsePCA on matrix (25000, 10000) done in 41.7692570686 seconds
在大约 40 秒内,SparsePCA能够使用恒定的内存量产生解决方案。
与 H2O 进行主成分分析
我们也可以使用 H2O 提供的 PCA 实现。(我们在之前的章节已经看到了 H2O,并在书中提到了它。)
有了 H2O,我们首先需要用init方法打开服务器。然后,我们将数据集转储到一个文件中(准确地说,是一个 CSV 文件),最后运行 PCA 分析。最后一步,我们关闭服务器。
我们正在一些迄今为止最大的数据集上尝试这种实现——一个包含 10 万个观测值和 100 个要素,另一个包含 10K 观测值和 2,500 个要素:
In: import h2o
from h2o.transforms.decomposition import H2OPCA
h2o.init(max_mem_size_GB=4)
def testH2O_pca(nrows, ncols, k=20):
temp_file = tempfile.NamedTemporaryFile().name
X, _ = make_blobs(nrows, n_features=ncols, random_state=101)
np.savetxt(temp_file, np.c_[X], delimiter=",")
del X
pca = H2OPCA(k=k, transform="NONE", pca_method="Power")
tik = time.time()
pca.train(x=range(100), \
training_frame=h2o.import_file(temp_file))
print "H2OPCA on matrix ", (nrows, ncols), \
" done in ", time.time() - tik, "seconds"
os.remove(temp_file)
testH2O_pca(100000, 100)
testH2O_pca(10000, 2500)
h2o.shutdown(prompt=False)
Out:[...]
H2OPCA on matrix (100000, 100) done in 12.9560530186 seconds
[...]
H2OPCA on matrix (10000, 2500) done in 10.1429388523 seconds
正如你所看到的,在这两种情况下,H2O 确实表现得非常快,与 Scikit-learn 相当(如果不是超越的话)。
聚类–K 均值
K-means 是一种无监督算法,它以相等的方差创建 K 个不相交的点簇,最小化失真(也称为惯性)。
仅给定一个参数 K,代表要创建的聚类数,K-means 算法创建 K 组点 S 1 、S 2 、…、S K ,每个点由其质心表示:C 1 、C 2 、…、C K 。通用形心,C i ,仅仅是与簇 Si 相关联的点的样本的平均值,以便最小化簇内距离。系统的输出如下:
-
聚类 S 1 、S 2 、…、S K 的组成,即组成与聚类号 1、2、…、K 相关联的训练集的点的集合
-
每个簇的质心,C 1 ,C 2 ,…,C K 。质心可用于未来的关联。
-
The distortion introduced by the clustering, computed as follows:
![Clustering – K-means]()
这个等式表示在 K-means 算法中本质上完成的优化:质心被选择为最小化簇内失真,即每个输入点和该点所关联的簇的质心之间的距离的欧几里德范数之和。换句话说,该算法试图拟合最佳矢量量化。
K-means 算法的训练阶段也被称为劳埃德算法,以最早提出该算法的斯图尔特·劳埃德的名字命名。这是一种迭代算法,由两个阶段组成,一遍又一遍地迭代,直到收敛(失真达到最小)。它是广义的期望最大化 ( EM )算法的变体,作为第一步,为分数的期望 ( E )创建函数,而最大化 ( M )步骤计算使分数最大化的参数。(注意,在这个公式中,我们试图实现相反的结果,即失真最小化。)这是它的公式:
-
The expectation step: In this step, the points in the training set are assigned to the closest centroid:
![Clustering – K-means]()
这一步也被称为赋值或矢量量化。
-
The maximization step: The centroid of each cluster is moved to the middle of the cluster by averaging the points composing it:
![Clustering – K-means]()
此步骤也称为更新步骤。
执行这两个步骤直到收敛(点在它们的簇中是稳定的),或者直到算法达到预设的迭代次数。请注意,对于每个合成,失真不会在整个训练阶段增加(不同于基于随机梯度下降的方法);因此,在这个算法中,迭代次数越多,结果越好。
现在让我们看看它在虚拟二维数据集上的样子。我们首先创建一组 1000 个点,集中在相对于原点对称的四个位置。每个集群,每个构造,都有相同的差异:
In:import matplotlib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
In:from sklearn.datasets.samples_generator import make_blobs
centers = [[1, 1], [1, -1], [-1, -1], [-1, 1]]
X, y = make_blobs(n_samples=1000, centers=centers,
cluster_std=0.5, random_state=101)
现在让我们绘制数据集。为了让事情变得更简单,我们将使用不同的颜色对集群进行着色:
In:plt.scatter(X[:,0], X[:,1], c=y, edgecolors='none', alpha=0.9)
plt.show()

现在让我们运行 K-means 并检查每次迭代中发生了什么。为此,我们将迭代停止到 1、2、3 和 4 次迭代,并绘制点及其相关聚类(颜色编码)以及质心、失真(在标题中)和决策边界(也称为沃罗诺伊单元)。质心的初始选择是随机的,即在训练的期望阶段,在第一次迭代中选择四个训练点作为质心:
In:pylab.rcParams['figure.figsize'] = (10.0, 8.0)
from sklearn.cluster import KMeans
for n_iter in range(1, 5):
cls = KMeans(n_clusters=4, max_iter=n_iter, n_init=1,
init='random', random_state=101)
cls.fit(X)
# Plot the voronoi cells
plt.subplot(2, 2, n_iter)
h=0.02
xx, yy = np.meshgrid(np.arange(-3, 3, h), np.arange(-3, 3, h))
Z = cls.predict(np.c_[xx.ravel(), \
yy.ravel()]).reshape(xx.shape)
plt.imshow(Z, interpolation='nearest', cmap=plt.cm.Accent, \
extent=(xx.min(), xx.max(), yy.min(), yy.max()), \
aspect='auto', origin='lower')
plt.scatter(X[:,0], X[:,1], c=cls.labels_, \
edgecolors='none', alpha=0.7)
plt.scatter(cls.cluster_centers_[:,0], \
cls.cluster_centers_[:,1], \
marker='x', color='r', s=100, linewidths=4)
plt.title("iter=%s, distortion=%s" %(n_iter, \
int(cls.inertia_)))
plt.show()

可以看到,随着迭代次数的增加,失真越来越小。对于这个虚拟数据集,似乎通过几次迭代(五次迭代),我们已经达到了收敛。
初始化方法
求 K 均值中失真的全局最小值是一个 NP 难问题;此外,与随机梯度下降的完全一样,该方法容易收敛到局部极小值,尤其是在维数较高的情况下。为了避免此类行为并限制最大迭代次数,您可以使用以下对策:
- 使用不同的初始条件多次运行该算法。在 Scikit-learn 中,
KMeans类有n_init参数,该参数控制用不同的质心种子运行 K-means 算法的次数。最后,选择确保较低失真的模型。如果有多个可用的内核,可以通过将n_jobs参数设置为需要剥离的作业数量来并行运行该过程。请注意,内存消耗与并行作业的数量呈线性关系。 - 比起随机选择训练点,更喜欢 k-means++初始化(默认为
KMeans类)。K-means++初始化选择彼此之间相距的点;这应该确保质心能够在空间的均匀子空间中形成簇。也证明了这个事实保证了更有可能找到最优解。
*## K-均值假设
K-means 依赖于假设每个聚类都有一个(超)球形,也就是说它没有拉长的形状(像箭头一样),所有的聚类内部都有相同的方差,并且它们的大小相当(或者非常远)。
所有这些假设都可以通过强大的特征预处理步骤来保证;主成分分析、核主成分分析、特征归一化和采样可以是一个很好的开始。
现在让我们看看当不满足 K 均值背后的假设时会发生什么:
In:pylab.rcParams['figure.figsize'] = (5.0, 10.0)
from sklearn.datasets import make_moons
# Oblong/elongated sets
X, _ = make_moons(n_samples=1000, noise=0.1, random_state=101)
cls = KMeans(n_clusters=2, random_state=101)
y_pred = cls.fit_predict(X)
plt.subplot(3, 1, 1)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, edgecolors='none')
plt.scatter(cls.cluster_centers_[:,0], cls.cluster_centers_[:,1],
marker='x', color='r', s=100, linewidths=4)
plt.title("Elongated clusters")
# Different variance between clusters
centers = [[-1, -1], [0, 0], [1, 1]]
X, _ = make_blobs(n_samples=1000, cluster_std=[0.1, 0.4, 0.1],
centers=centers, random_state=101)
cls = KMeans(n_clusters=3, random_state=101)
y_pred = cls.fit_predict(X)
plt.subplot(3, 1, 2)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, edgecolors='none')
plt.scatter(cls.cluster_centers_[:,0], cls.cluster_centers_[:,1],
marker='x', color='r', s=100, linewidths=4)
plt.title("Unequal Variance between clusters")
# Unevenly sized blobs
centers = [[-1, -1], [1, 1]]
centers.extend([[0,0]]*20)
X, _ = make_blobs(n_samples=1000, centers=centers,
cluster_std=0.28, random_state=101)
cls = KMeans(n_clusters=3, random_state=101)
y_pred = cls.fit_predict(X)
plt.subplot(3, 1, 3)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, edgecolors='none')
plt.scatter(cls.cluster_centers_[:,0], cls.cluster_centers_[:,1],
marker='x', color='r', s=100, linewidths=4)
plt.title("Unevenly Sized Blobs")
plt.show()

在前面所有的例子中,聚类操作并不完美,输出了错误且不稳定的结果。
到目前为止,我们已经假设确切知道哪个是确切的 K,即我们期望在聚类操作中使用的聚类数量。实际上,在现实问题中,这并不总是正确的。我们经常使用无监督学习方法来发现数据的底层结构,包括组成数据集的聚类数量。让我们看看当我们试图在一个简单的虚拟数据集上用一个错误的 K 运行 K-means 时会发生什么;我们将尝试较低的 K 和较高的 K:
In:pylab.rcParams['figure.figsize'] = (10.0, 4.0)
X, _ = make_blobs(n_samples=1000, centers=3, random_state=101)
for K in [2, 3, 4]:
cls = KMeans(n_clusters=K, random_state=101)
y_pred = cls.fit_predict(X)
plt.subplot(1, 3, K-1)
plt.title("K-means, K=%s" % K)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, edgecolors='none')
plt.scatter(cls.cluster_centers_[:,0], cls.cluster_centers_[:,1],
marker='x', color='r', s=100, linewidths=4)
plt.show()

如您所见,如果没有猜中正确的 K,即使对于这个简单的虚拟数据集,结果也是大错特错的。在下一节中,我们将解释一些最佳选择 k 的技巧。
最佳钾的选择
如果满足 K 均值背后的假设,有几种方法可以检测最佳 K。其中一些是基于输出的交叉验证和度量;它们可以用在所有的聚类方法上,但是只有当一个基本事实可用时(它们被称为监督度量)。其他一些是基于聚类算法的内在参数,并且可以通过存在或不存在基础事实(也称为无监督度量)来独立使用。不幸的是,它们都不能确保找到正确结果的 100%准确性。
监督度量需要一个基本事实(包含集合中的真实关联),并且它们通常与网格搜索分析相结合来理解最佳 k。这些度量中的一些是从等价的分类中导出的,但是它们允许有不同数量的无序集合作为预测标签。我们要看的第一个叫做同质化;正如你所料,它给出了预测的簇中有多少只包含一个类的点的度量。这是一种基于熵的度量,是分类精度的聚类等价。它是 0(最差)和 1(最好)之间的度量界限;它的数学公式如下:

这里, H(C|K) 是给定所提出的聚类分配的类分布的条件熵, H(C) 是类的熵。当聚类没有提供新信息时,H(C|K) 最大,等于H(C);当每个集群只包含一个类的成员时,它为零。
与之相连的是分类的精确度和召回率,还有完整性分数:它给出了一个度量,关于一个类的所有成员被分配到同一个聚类的程度。甚至这一个也在 0(最差)和 1(最好)之间,它的数学公式深深基于熵:

这里, H(K|C) 是给定类的建议聚类分布的条件熵, H(K) 是聚类的熵。
最后,相当于分类任务的 f1 分数,V-测度是同质性和完备性的调和平均值:

让我们回到第一个数据集(四个对称的有噪声的聚类),并尝试看看这些分数是如何操作的,以及它们是否能够突出最佳 K 来使用:
In:pylab.rcParams['figure.figsize'] = (6.0, 4.0)
from sklearn.metrics import homogeneity_completeness_v_measure
centers = [[1, 1], [1, -1], [-1, -1], [-1, 1]]
X, y = make_blobs(n_samples=1000, centers=centers,
cluster_std=0.5, random_state=101)
Ks = range(2, 10)
HCVs = []
for K in Ks:
y_pred = KMeans(n_clusters=K, random_state=101).fit_predict(X)
HCVs.append(homogeneity_completeness_v_measure(y, y_pred))
plt.plot(Ks, [el[0] for el in HCVs], 'r', label='Homogeneity')
plt.plot(Ks, [el[1] for el in HCVs], 'g', label='Completeness')
plt.plot(Ks, [el[2] for el in HCVs], 'b', label='V measure')
plt.ylim([0, 1])
plt.legend(loc=4)
plt.show()

在剧情上,最初( K < 4 )完整性高,但同质性低;对于 K > 4 来说,则相反:同质性高,但完备性低。在这两种情况下,V 值都很低。相反,对于 K=4 ,所有度量都达到了最大值,这表明这是集群数量 K 的最佳值。
除了这些被监督的度量之外,还有其他被命名为无监督的度量,它们不需要一个基本的事实,只是基于学习者本身。
在这个部分,我们首先要看到的是肘击法,应用于扭曲。这很简单,不需要任何数学:你只需要画出许多 K-means 模型不同 K 的失真,然后选择一个增加 K 不会在解中引入低得多的失真的模型。在 Python 中,这很容易实现:
In:Ks = range(2, 10)
Ds = []
for K in Ks:
cls = KMeans(n_clusters=K, random_state=101)
cls.fit(X)
Ds.append(cls.inertia_)
plt.plot(Ks, Ds, 'o-')
plt.xlabel("Value of K")
plt.ylabel("Distortion")
plt.show()

如你所料,失真下降到 K=4 ,然后慢慢下降。这里,最好的 K 是 4。
我们将看到的另一个无监督的度量是轮廓。它更复杂,但也比之前的试探法更强大。在很高的层次上,它衡量一个观测值与指定的聚类有多接近(相似),它与附近聚类的数据匹配有多松散(不相似)。剪影得分为 1 表示所有数据都在最佳聚类中,而-1 表示完全错误的聚类结果。由于 Scikit-learn 实现,使用 Python 代码获得这样的度量非常容易:
In:from sklearn.metrics import silhouette_score
Ks = range(2, 10)
Ds = []
for K in Ks:
cls = KMeans(n_clusters=K, random_state=101)
Ds.append(silhouette_score(X, cls.fit_predict(X)))
plt.plot(Ks, Ds, 'o-')
plt.xlabel("Value of K")
plt.ylabel("Silhouette score")
plt.show()

即使在这种情况下,我们也得出了相同的结论:K 的最佳值是 4,因为轮廓分数随着 K 的越来越高而越来越低。
换算 K 均值-小批量
现在让我们测试一下 K 均值的可伸缩性。从 UCI 的网站上,我们为这个任务选择了一个合适的数据集:美国 1990 年人口普查数据。这个数据集包含近 250 万个观察值和 68 个分类(但已经是数字编码的)属性。没有丢失数据,文件是 CSV 格式。每个观察都包含个人的 ID(在聚类之前要删除)和其他关于性别、收入、婚姻状况、工作等的信息。
注
更多关于数据集的信息可以在http://archive . ics . UCI . edu/ml/datasets/US+Census+Data+% 281990% 29或 Meek,Thiesson 和 Heckerman (2001)在《机器学习研究杂志》上发表的题为应用于聚类的学习曲线方法的论文中找到。
首先,您必须下载包含数据集的文件,并将其存储在临时目录中。请注意,它的大小为 345 兆字节,因此在慢速连接上下载可能需要很长时间:
In:import urllib
import os.path
url = "http://archive.ics.uci.edu/ml/machine-learning-databases/census1990-mld/USCensus1990.data.txt"
census_csv_file = "/tmp/USCensus1990.data.txt"
import os.path
if not os.path.exists(census_csv_file):
testfile = urllib.URLopener()
testfile.retrieve(url, census_csv_file)
现在,让我们运行一些测试,记录训练 K 均值学习者所需的时间,K 等于 4、8 和 12,数据集包含 20K、200K 和 0.5M 个观察值。由于我们不想让机器的内存饱和,因此我们将只读取前 500K 行,并删除包含用户标识符的列。最后,让我们为完整的绩效评估绘制培训时间图:
In:piece_of_dataset = pd.read_csv(census_csv_file, iterator=True).get_chunk(500000).drop('caseid', axis=1).as_matrix()
time_results = {4: [], 8:[], 12:[]}
dataset_sizes = [20000, 200000, 500000]
for dataset_size in dataset_sizes:
print "Dataset size:", dataset_size
X = piece_of_dataset[:dataset_size,:]
for K in [4, 8, 12]:
print "K:", K
cls = KMeans(K, random_state=101)
timeit = %timeit -o -n1 -r1 cls.fit(X)
time_results[K].append(timeit.best)
plt.plot(dataset_sizes, time_results[4], 'r', label='K=4')
plt.plot(dataset_sizes, time_results[8], 'g', label='K=8')
plt.plot(dataset_sizes, time_results[12], 'b', label='K=12')
plt.xlabel("Training set size")
plt.ylabel("Training time")
plt.legend(loc=0)
plt.show()
Out:Dataset size: 20000
K: 4
1 loops, best of 1: 478 ms per loop
K: 8
1 loops, best of 1: 1.22 s per loop
K: 12
1 loops, best of 1: 1.76 s per loop
Dataset size: 200000
K: 4
1 loops, best of 1: 6.35 s per loop
K: 8
1 loops, best of 1: 10.5 s per loop
K: 12
1 loops, best of 1: 17.7 s per loop
Dataset size: 500000
K: 4
1 loops, best of 1: 13.4 s per loop
K: 8
1 loops, best of 1: 48.6 s per loop
K: 12
1 loops, best of 1: 1min 5s per loop

很明显,给定图和实际时间,训练时间随着 K 和训练集大小线性增加,但是对于大的 K 和训练大小,这种关系变成非线性。对许多 Ks 的整个训练集进行详尽的搜索似乎是不可扩展的。
幸运的是,有一个基于小批量的在线版 K-means,已经在 Scikit-learn 中实现并命名为MiniBatchKMeans。让我们在前一个单元格最慢的情况下尝试一下,也就是 K=12 。使用经典的 K-means,对 500,000 个样本(约占整个数据集的 20%)的训练花费了一分多钟;让我们看看在线小批量版本的性能,将批量大小设置为 1000,并从数据集导入 50000 个观察值的组块。作为输出,我们绘制了训练时间与训练阶段已经通过的组块数量的关系图:
In:from sklearn.cluster import MiniBatchKMeans
import time
cls = MiniBatchKMeans(12, batch_size=1000, random_state=101)
ts = []
tik = time.time()
for chunk in pd.read_csv(census_csv_file, chunksize=50000):
cls.partial_fit(chunk.drop('caseid', axis=1))
ts.append(time.time()-tik)
plt.plot(range(len(ts)), ts)
plt.xlabel('Training batches')
plt.ylabel('time [s]')
plt.show()

每个数据块的训练时间是线性的,在将近 20 秒的时间内对全部 250 万个观察数据集执行聚类。有了这个实现,我们可以运行一个完整的搜索来选择最佳的 K 使用肘方法对失真。让我们做一个网格搜索,K 从 4 到 12,并绘制失真:
In:Ks = list(range(4, 13))
ds = []
for K in Ks:
cls = MiniBatchKMeans(K, batch_size=1000, random_state=101)
for chunk in pd.read_csv(census_csv_file, chunksize=50000):
cls.partial_fit(chunk.drop('caseid', axis=1))
ds.append(cls.inertia_)
plt.plot(Ks, ds)
plt.xlabel('Value of K')
plt.ylabel('Distortion')
plt.show()
Out:

从图中来看,肘似乎与 K=8 对应。除了价值之外,我们想指出的是,由于批量实现,在不到几分钟的时间内,我们已经能够在一个大数据集上执行这种大规模操作;因此,如果数据集越来越大,请记住永远不要使用普通的 K 均值。
K-表示与 H2O
在这里,我们将 H2O 的 K-means 实现与 Scikit-learn 进行比较。更具体地说,我们将使用在 H2O 可用的 K-means 的对象,运行小批量实验。设置类似于主成分分析与 H2O 部分所示,实验与前一部分相同:
In:import h2o
from h2o.estimators.kmeans import H2OKMeansEstimator
h2o.init(max_mem_size_GB=4)
def testH2O_kmeans(X, k):
temp_file = tempfile.NamedTemporaryFile().name
np.savetxt(temp_file, np.c_[X], delimiter=",")
cls = H2OKMeansEstimator(k=k, standardize=True)
blobdata = h2o.import_file(temp_file)
tik = time.time()
cls.train(x=range(blobdata.ncol), training_frame=blobdata)
fit_time = time.time() - tik
os.remove(temp_file)
return fit_time
piece_of_dataset = pd.read_csv(census_csv_file, iterator=True).get_chunk(500000).drop('caseid', axis=1).as_matrix()
time_results = {4: [], 8:[], 12:[]}
dataset_sizes = [20000, 200000, 500000]
for dataset_size in dataset_sizes:
print "Dataset size:", dataset_size
X = piece_of_dataset[:dataset_size,:]
for K in [4, 8, 12]:
print "K:", K
fit_time = testH2O_kmeans(X, K)
time_results[K].append(fit_time)
plt.plot(dataset_sizes, time_results[4], 'r', label='K=4')
plt.plot(dataset_sizes, time_results[8], 'g', label='K=8')
plt.plot(dataset_sizes, time_results[12], 'b', label='K=12')
plt.xlabel("Training set size")
plt.ylabel("Training time")
plt.legend(loc=0)
plt.show()
testH2O_kmeans(100000, 100)
h2o.shutdown(prompt=False)
Out:

得益于 H2O 架构,其 K-means 的实现非常快,并且可扩展,能够在不到 30 秒的时间内对所有选定的 K 执行 500K 点数据集的聚类。
LDA
LDA 代表潜在狄利克雷分配,是分析文本文档集合的常用技术之一。
注
线性判别分析是另一种技术使用的首字母缩略词,它是一种有监督的分类方法。注意 LDA 是如何使用的,因为这两种算法之间没有联系。
对线性判别分析的完整数学解释需要概率建模的知识,这超出了本实用书的范围。相反,在这里,我们将为您提供模型背后最重要的直觉,以及如何在大规模数据集上实际应用该模型。
首先,LDA 用于数据科学的一个分支,称为文本挖掘,其重点是帮助学习者理解自然语言,例如,基于文本示例。具体来说,LDA 属于主题建模算法的范畴,因为它试图对文档中包含的主题进行建模。理想情况下,例如,LDA 能够理解一份文件是关于金融、政治还是宗教。然而,与分类器不同,它还能够量化文档中主题的存在。例如,让我们想想罗琳的《哈利·波特》小说。一个分类器将能够评估它的类别(奇幻小说);相反,LDA 能够理解其中有多少喜剧、戏剧、神秘、浪漫和冒险。而且,LDA 不需要任何标签;这是一种无监督的方法,并在内部构建输出类别或主题及其组成(即由组成主题的词集给出)。
在处理过程中,线性判别分析建立了每个文档的主题模型和每个主题的单词模型,建模为狄利克雷分布。尽管复杂性很高,但由于类似蒙特卡罗的迭代核心函数,输出稳定结果所需的处理时间并没有那么长。
LDA 模型很容易理解:每个文档被建模为主题的分布,每个主题被建模为单词的分布。分布假设具有狄利克雷先验(具有不同的参数,因为每个主题的字数通常不同于每个文档的主题数)。由于吉布斯采样,分布不应该被直接采样,而是迭代地获得它的精确近似。使用变分贝叶斯技术可以获得类似的结果,其中近似是用期望最大化方法生成的。
最终的线性判别分析模型是生成的(就像隐马尔可夫模型、朴素贝叶斯和受限玻尔兹曼机器一样),因此每个变量都可以被模拟和观察。
现在让我们看看它是如何在一个真实的数据集上工作的——20 新闻组数据集。它由 20 个新闻组中交换的电子邮件组成。让我们首先加载它,从回复的电子邮件中删除电子邮件的页眉、页脚和引号:
In:from sklearn.datasets import fetch_20newsgroups
documents = fetch_20newsgroups(remove=('headers', 'footers', \
'quotes'), random_state=101).data
检查数据集的大小(即有多少个文档),并打印其中一个文档,以查看一个文档实际由什么组成:
In:len(documents)
Out:11314
In:document_num = 9960
print documents[document_num]
Out:Help!!!
I have an ADB graphicsd tablet which I want to connect to my
Quadra 950\. Unfortunately, the 950 has only one ADB port and
it seems I would have to give up my mouse.
Please, can someone help me? I want to use the tablet as well as
the mouse (and the keyboard of course!!!).
Thanks in advance.
举个例子,一个人正在平板电脑上为他的视频插座寻求帮助。
现在,我们导入运行 LDA 所需的 Python 包。Gensim 包是最好的包之一,正如您将在本节末尾看到的,它也是非常可扩展的:
In:import gensim
from gensim.utils import simple_preprocess
from gensim.parsing.preprocessing import STOPWORDS
from nltk.stem import WordNetLemmatizer, SnowballStemmer
np.random.seed(101)
作为第一步,我们应该清理文本。一些步骤是必要的,这是典型的任何 NLP 文本处理:
- 标记化是将文本拆分成句子,将句子拆分成单词。最后,单词是低级的。此时,标点符号(和重音符号)被删除。
- 由少于三个字符组成的单词将被删除。(这一步删除了大多数首字母缩略词、表情符号和连词。)
- 英语停止词列表中出现的词被删除。这个列表中的单词非常常见,没有预测能力(如,an,so,then,have,等等)。
- 然后标记被引理化;第三人称的单词变成第一人称,过去和将来时态的动词变成现在(比如 goes、goes、goes 都变成了 go)。
- 最后,词干去除了词形变化,将单词还原为词根(例如,鞋变成鞋)。
在下面这段代码中,我们将完全这样做:尽可能地清理文本,并列出构成每个文本的单词。在单元格的末尾,我们可以看到该操作如何改变之前看到的文档:
In:lm = WordNetLemmatizer()
stemmer = SnowballStemmer("english")
def lem_stem(text):
return stemmer.stem(lm.lemmatize(text, pos='v'))
def tokenize_lemmatize(text):
return [lem_stem(token)
for token in gensim.utils.simple_preprocess(text)
if token not in gensim.parsing.preprocessing.STOPWORDS and len(token) > 3]
print tokenize_lemmatize(documents[document_num])
Out:[u'help', u'graphicsd', u'tablet', u'want', u'connect', u'quadra', u'unfortun', u'port', u'mous', u'help', u'want', u'tablet', u'mous', u'keyboard', u'cours', u'thank', u'advanc']
现在,作为下一步,让我们操作所有文档的清理步骤。在这之后,我们必须建立一个字典,包含一个单词在训练集中出现的次数。由于有了 Gensim 包,该操作非常简单:
In:processed_docs = [tokenize(doc) for doc in documents]
word_count_dict = gensim.corpora.Dictionary(processed_docs)
现在,当我们想要构建一个通用的快速解决方案时,让我们去掉所有非常罕见和非常常见的单词。例如,我们可以过滤掉所有出现少于 20 次(总共)且不超过 20%的文档中的单词:
In:word_count_dict.filter_extremes(no_below=20, no_above=0.2)
作为下一步,使用这样一组缩减的单词,我们现在为每个文档构建单词包模型;也就是说,对于每个文档,我们创建一个字典来报告单词的数量和出现次数:
In:bag_of_words_corpus = [word_count_dict.doc2bow(pdoc) \
for pdoc in processed_docs]
例如,让我们来看一下前面文档的单词包模型:
In:bow_doc1 = bag_of_words_corpus[document_num]
for i in range(len(bow_doc1)):
print "Word {} (\"{}\") appears {} time[s]" \
.format(bow_doc1[i][0], \
word_count_dict[bow_doc1[i][0]], bow_doc1[i][1])
Out:Word 178 ("want") appears 2 time[s]
Word 250 ("keyboard") appears 1 time[s]
Word 833 ("unfortun") appears 1 time[s]
Word 1037 ("port") appears 1 time[s]
Word 1142 ("help") appears 2 time[s]
Word 1543 ("quadra") appears 1 time[s]
Word 2006 ("advanc") appears 1 time[s]
Word 2124 ("cours") appears 1 time[s]
Word 2391 ("thank") appears 1 time[s]
Word 2898 ("mous") appears 2 time[s]
Word 3313 ("connect") appears 1 time[s]
现在,我们已经到达了算法的核心部分:运行 LDA。至于我们的决定,让我们要求 12 个主题(有 20 种不同的时事通讯,但有些是相似的):
In:lda_model = gensim.models.LdaMulticore(bag_of_words_corpus, num_topics=10, id2word=word_count_dict, passes=50)
注
如果你得到这样一个代码的错误,试着用gensim.models.LdaModel类而不是gensim.models.LdaMulticore来处理这个版本。
现在我们打印题目作文,即每个题目中出现的单词及其相对权重:
In:for idx, topic in lda_model.print_topics(-1):
print "Topic:{} Word composition:{}".format(idx, topic)
print
Out:
Topic:0 Word composition:0.015*imag + 0.014*version + 0.013*avail + 0.013*includ + 0.013*softwar + 0.012*file + 0.011*graphic + 0.010*program + 0.010*data + 0.009*format
Topic:1 Word composition:0.040*window + 0.030*file + 0.018*program + 0.014*problem + 0.011*widget + 0.011*applic + 0.010*server + 0.010*entri + 0.009*display + 0.009*error
Topic:2 Word composition:0.011*peopl + 0.010*mean + 0.010*question + 0.009*believ + 0.009*exist + 0.008*encrypt + 0.008*point + 0.008*reason + 0.008*post + 0.007*thing
Topic:3 Word composition:0.010*caus + 0.009*good + 0.009*test + 0.009*bike + 0.008*problem + 0.008*effect + 0.008*differ + 0.008*engin + 0.007*time + 0.006*high
Topic:4 Word composition:0.018*state + 0.017*govern + 0.015*right + 0.010*weapon + 0.010*crime + 0.009*peopl + 0.009*protect + 0.008*legal + 0.008*control + 0.008*drug
Topic:5 Word composition:0.017*christian + 0.016*armenian + 0.013*jesus + 0.012*peopl + 0.008*say + 0.008*church + 0.007*bibl + 0.007*come + 0.006*live + 0.006*book
Topic:6 Word composition:0.018*go + 0.015*time + 0.013*say + 0.012*peopl + 0.012*come + 0.012*thing + 0.011*want + 0.010*good + 0.009*look + 0.009*tell
Topic:7 Word composition:0.012*presid + 0.009*state + 0.008*peopl + 0.008*work + 0.008*govern + 0.007*year + 0.007*israel + 0.007*say + 0.006*american + 0.006*isra
Topic:8 Word composition:0.022*thank + 0.020*card + 0.015*work + 0.013*need + 0.013*price + 0.012*driver + 0.010*sell + 0.010*help + 0.010*mail + 0.010*look
Topic:9 Word composition:0.019*space + 0.011*inform + 0.011*univers + 0.010*mail + 0.009*launch + 0.008*list + 0.008*post + 0.008*anonym + 0.008*research + 0.008*send
Topic:10 Word composition:0.044*game + 0.031*team + 0.027*play + 0.022*year + 0.020*player + 0.016*season + 0.015*hockey + 0.014*leagu + 0.011*score + 0.010*goal
Topic:11 Word composition:0.075*drive + 0.030*disk + 0.028*control + 0.028*scsi + 0.020*power + 0.020*hard + 0.018*wire + 0.015*cabl + 0.013*instal + 0.012*connect
不幸的是,LDA 没有为每个主题提供名称;我们应该根据我们对算法结果的解释,自己手动完成。仔细检查了作文之后,我们可以把发现的题目命名如下:
|主题
|
名字
|
| --- | --- |
| Zero | 软件 |
| one | 应用 |
| Two | 论证 |
| three | 运输 |
| four | 政府 |
| five | 宗教 |
| six | 人员行动 |
| seven | 中东 |
| eight | 个人电脑设备 |
| nine | 空间 |
| Ten | 比赛 |
| Eleven | 驱动 |
现在,让我们试着理解前面的文档中表示了哪些主题及其权重:
In:
for index, score in sorted( \
lda_model[bag_of_words_corpus[document_num]], \
key=lambda tup: -1*tup[1]):
print "Score: {}\t Topic: {}".format(score, lda_model.print_topic(index, 10))
Out:Score: 0.938887758964 Topic: 0.022*thank + 0.020*card + 0.015*work + 0.013*need + 0.013*price + 0.012*driver + 0.010*sell + 0.010*help + 0.010*mail + 0.010*look
最高分与主题 PC 设备相关。基于我们之前对文献集的了解,主题提取似乎表现得相当不错。
现在,让我们从整体上评估模型。困惑(或其对数)为我们提供了一个度量标准,以了解 LDA 在训练数据集上的表现:
In:print "Log perplexity of the model is", lda_model.log_perplexity(bag_of_words_corpus)
Out:Log perplexity of the model is -7.2985188569
在这种情况下,困惑度是 2-7.298,并且它与 LDA 模型能够生成测试集中的文档的(日志)可能性有关,给定这些文档的主题分布。困惑度越低,模型越好,因为这基本上意味着模型可以很好地重新生成文本。
现在,让我们尝试在一个看不见的文档上使用这个模型。为了简单起见,文档只包含了句子,高尔夫还是网球?哪项运动最好玩?:
In:unseen_document = "Golf or tennis? Which is the best sport to play?"
bow_vector = word_count_dict.doc2bow(\
tokenize_lemmatize(unseen_document))
for index, score in sorted(lda_model[bow_vector], \
key=lambda tup: -1*tup[1]):
print "Score: {}\t Topic: {}".format(score, \
lda_model.print_topic(index, 5))
Out:Score: 0.610691655136 Topic: 0.044*game + 0.031*team + 0.027*play + 0.022*year + 0.020*player
Score: 0.222640440339 Topic: 0.018*state + 0.017*govern + 0.015*right + 0.010*weapon + 0.010*crime
不出所料,得分较高的话题是关于“游戏”的话题,其次是得分相对较小的话题。
LDA 如何随着语料库的大小而缩放?幸运的是,非常好;该算法是迭代的,允许在线学习,类似于小批量学习。在线流程的关键是LdaModel(或LdaMulticore)提供的.update()方法。
我们将在由前 1000 个文档组成的原始语料库的子集上进行这个测试,并且我们将使用 50、100、200 和 500 个文档的批次来更新我们的 LDA 模型。对于每一个更新模型的小批量,我们将记录时间并将它们绘制在图表上:
In:small_corpus = bag_of_words_corpus[:1000]
batch_times = {}
for batch_size in [50, 100, 200, 500]:
print "batch_size =", batch_size
tik0 = time.time()
lda_model = gensim.models.LdaModel(num_topics=12, \
id2word=word_count_dict)
batch_times[batch_size] = []
for i in range(0, len(small_corpus), batch_size):
lda_model.update(small_corpus[i:i+batch_size], \
update_every=25, \
passes=1+500/batch_size)
batch_times[batch_size].append(time.time() - tik0)
Out:batch_size = 50
batch_size = 100
batch_size = 200
batch_size = 500
注意,我们已经在模型更新中设置了update_every和passes参数。这对于使模型在每次迭代时收敛并且不返回不收敛的模型是必要的。注意 500 是试探性选择的;如果您将其设置得更低,您将会收到 Gensim 关于模型不收敛的许多警告。
现在我们来绘制结果:
In:plt.plot(range(50, 1001, 50), batch_times[50], 'g', \
label='size 50')
plt.plot(range(100, 1001, 100), batch_times[100], 'b', \
label='size 100')
plt.plot(range(200, 1001, 200), batch_times[200], 'k', \
label='size 200')
plt.plot(range(500, 1001, 500), batch_times[500], 'r', \
label='size 500')
plt.xlabel("Training set size")
plt.ylabel("Training time")
plt.xlim([0, 1000])
plt.legend(loc=0)
plt.show()
Out:

批次越大,训练越快。(请记住,更新模型时,大批量需要较少的通过次数。)另一方面,批次越大,存储和处理语料库所需的内存量就越大。得益于小批量更新方法,LDA 能够扩展到处理数百万个文档的语料库。事实上,Gensim 包提供的实现能够在家用计算机上在几个小时内扩展和处理整个维基百科。如果你有足够的勇气亲自尝试,下面是完成任务的完整说明,由包的作者提供:
https://radimrehurek . com/genim/wiki . html
缩放 LDA–内存、中央处理器和机器
gensim 非常灵活,用于处理大型文本语料库;事实上,该库无需任何修改或额外下载即可扩展:
- 用 CPU 的数量,允许在单个节点上并行处理(用类,如第一个例子所示)。
- 随着观察次数的增加,允许基于小批量的在线学习。这可以通过
LdaModel和LdaMulticore中可用的update方法来实现(如前例所示)。 - 在集群上运行它,在集群中的节点之间分配工作负载,这要归功于 Python 库 Pyro4 和
models.lda_dispatcher(作为调度器)和models.lda_worker(作为工作进程)对象,这两个对象都由 Gensim 提供。
除了经典的 LDA 算法,Gensim 还提供了其分层版本,命名为分层狄利克雷处理 ( HDP )。使用这种算法,主题遵循多级结构,使用户能够更好地理解复杂的语料库(也就是说,一些文档是通用的,而一些特定于某个主题)。该模块相当新,截至 2015 年底,其可扩展性不如经典的 LDA。
总结
在本章中,我们介绍了三种流行的无监督学习者,它们能够扩展以应对大数据。第一种是主成分分析,它能够通过创建包含大部分方差的特征(即主特征)来减少特征的数量。K-means 是一种聚类算法,能够将相似的点组合在一起,并将它们与质心相关联。LDA 是对文本数据进行主题建模的有力方法,即对每个文档的主题和主题中出现的单词进行联合建模。
在下一章中,我们将介绍一些先进的和非常新的机器学习方法,它们仍然不是主流的一部分,对于小数据集来说自然很棒,但也适合处理大规模的机器学习。*
八、分布式环境——Hadoop 和 Spark
在本章中,我们将介绍一种处理数据的新方法,水平缩放。到目前为止,我们的注意力主要集中在独立机器上处理大数据;在这里,我们将介绍一些在机器集群上运行的方法。
具体来说,我们将首先说明我们需要集群来处理大数据的动机和环境。然后,我们将通过几个例子(HDFS、MapReduce 和 YARN)介绍 Hadoop 框架及其所有组件,最后,我们将介绍 Spark 框架及其 Python 接口——pySpark。
从单机到一堆节点
世界上存储的数据量呈指数级增长。如今,对于一个数据科学家来说,每天必须处理几万亿字节的数据并不是一个不寻常的要求。为了使事情更加复杂,通常数据来自许多不同的异构系统,业务的期望是在短时间内产生一个模型。
因此,处理大数据不仅仅是规模的问题,它实际上是一个三维现象。事实上,根据 3V 模型,基于大数据运行的系统可以使用三个(正交)标准进行分类:
- 第一个标准是系统归档处理数据的速度。虽然几年前,速度是指一个系统处理一批的速度;如今,速度表示系统是否能够提供流数据的实时输出。
- 第二个标准是容量,即有多少信息可供处理。它可以用行数、特征数或者仅仅是字节数来表示。对于数据流,容量表示到达系统的数据吞吐量。
- 最后一个标准是多样性,即数据源的类型。几年前,多样性受到结构化数据集的限制;如今,数据可以是结构化的(表格、图像等)、半结构化的(JSON、XML 等)和非结构化的(网页、社交数据等)。通常,大数据系统试图处理尽可能多的相关来源,混合各种来源。
除了这些标准,最近几年还出现了许多其他的 Vs,试图解释大数据的其他特征。其中一些如下:
- 准确性(提供数据中包含的异常、偏差和噪声的指示;最终,它的准确性)
- 易失性(表示数据可用于提取有意义信息的时间长度)
- 有效性(数据的正确性)
- 值(表示数据的投资回报)
最近几年,所有的 Vs 都大幅增长;现在,许多公司发现他们保留的数据具有巨大的价值,可以货币化,他们希望从中提取信息。技术挑战已经转移到拥有足够的存储和处理能力,以便能够快速、大规模地提取有意义的见解,并使用不同的输入数据流。
当前的计算机,即使是最新最贵的计算机,其磁盘、内存和中央处理器的数量也是有限的。每天处理万亿字节(或千兆字节)的信息看起来非常困难,这就产生了一个快速模型。此外,需要复制包含数据和处理软件的独立服务器;否则,它可能成为系统的单点故障。
因此,大数据的世界已经转向集群:它们由可变数量的不太昂贵的节点组成,并位于高速互联网连接上。通常,一些集群专用于存储数据(大硬盘、小 CPU 和低内存量),而其他集群专用于处理数据(强大的 CPU、中到大内存量和小硬盘)。此外,如果群集设置正确,它可以确保可靠性(无单点故障)和高可用性。
注意,当我们在分布式环境(像集群)中存储数据时,也要考虑 CAP 定理的局限性;在系统中,我们可以确保以下三个属性中的两个:
- 一致性:所有节点都能够同时向客户端提供相同的数据
- 可用性:对于成功和失败的请求,请求数据的客户端保证总是收到响应
- 分区容差:如果网络出现故障,所有节点都无法联系,系统可以继续工作
具体来说,CAP 定理的结果如下:
- 如果你放弃一致性,你将创建一个数据跨节点分布的环境,即使网络出现一些问题,系统仍然能够对每个请求提供响应,尽管不能保证对同一个问题的响应是相同的(可能不一致)。这种配置的典型例子是 DynamoDB、CouchDB 和 Cassandra。
- 如果您放弃可用性,您将创建一个分布式系统,该系统可能无法响应查询。这个类的例子是分布式缓存数据库,比如 Redis、MongoDb 和 MemcacheDb。
- 最后,如果你在分区容忍度上做了补充,你就陷入了不允许网络分裂的关系数据库的僵化模式中。这一类别包括 MySQL、Oracle 和 SQL Server。
为什么我们需要分布式框架?
构建集群最简单的方法是将一些节点用作存储节点,将其他节点用作处理节点。这个配置看起来非常简单,因为我们不需要复杂的框架来处理这种情况。事实上,许多小型集群正是以这种方式构建的:一对服务器处理数据(加上它们的副本),另一对服务器处理数据。
虽然这看起来是一个很好的解决方案,但由于许多原因,它并不常用:
- 它只适用于令人尴尬的并行算法。如果算法需要在处理服务器之间共享一个公共内存区域,则不能使用这种方法。
- 如果一个或多个存储节点死亡,则不能保证数据一致。(考虑一种情况,其中一个节点及其副本同时死亡,或者一个节点在尚未复制的写操作之后死亡。
- 如果一个处理节点死亡,我们就无法跟踪它正在执行的进程,从而很难在另一个节点上恢复处理。
- 如果网络出现故障,恢复正常后很难预测情况。
现在让我们计算节点故障的概率。是不是稀有到我们可以丢弃?我们是否应该考虑更具体的问题?解决方案很简单:让我们考虑一个 100 节点的集群,其中每个节点在第一年有 1%的故障概率(硬件和软件崩溃的累积)。这 100 个人第一年都活下来的概率有多大?假设每个服务器都是独立的(也就是说,每个节点都可以独立于所有其他节点崩溃,这只是一个乘法:

一开始的结果非常令人惊讶,但这解释了为什么大数据社区在过去十年中非常重视这个问题,并为集群管理开发了许多解决方案。从公式的结果来看,似乎崩溃事件(甚至不止一个)很有可能发生,这一事实要求必须提前想到这种情况,并妥善处理,以确保对数据的操作的连续性。此外,使用廉价的硬件或更大的集群,看起来几乎可以肯定至少有一个节点会出现故障。
型式
这里的学习点是,一旦你走向大数据企业,你必须对节点故障采取足够的对策;这是常态而不是例外,应该妥善处理,以确保运营的连续性。
到目前为止,绝大多数集群框架都使用名为分治的方法:
- 有专门用于数据节点的模块,也有专门用于数据处理节点的模块(也称为工人)。**
*** 数据跨数据节点复制,一个节点是主节点,确保写入和读取操作都成功。* 处理步骤在工作节点之间分割。它们不共享任何状态(除非存储在数据节点中),它们的主节点确保所有任务都以正确的顺序积极执行。**
**### 注
稍后,我们将在本章中介绍 Apache Hadoop 框架;虽然现在已经是一个成熟的集群管理系统,但它仍然依赖于坚实的基础。在此之前,让我们在机器上设置正确的工作环境。
设置虚拟机
建立集群是一项漫长而艰难的工作;高级大数据工程师赚到的(高)工资不仅仅是下载和执行一个二进制应用,而是熟练而谨慎地让集群管理器适应想要的工作环境。这是一个艰难而复杂的操作;这可能需要很长时间,如果结果低于预期,整个企业(包括数据科学家和软件开发人员)将无法提高生产力。在开始构建集群之前,数据工程师必须了解节点、数据、将要执行的操作和网络的每个小细节。输出通常是平衡的、自适应的、快速的、可靠的集群,可以被公司所有的技术人员使用多年。
注
拥有少量非常强大的节点的集群比拥有许多不太强大的服务器的集群更好吗?答案应该逐案评估,它高度依赖于数据、处理算法、访问数据的人数、我们想要结果的速度、总体价格、可扩展性的健壮性、网络速度和许多其他因素。简单地说,做出最好的决定一点也不容易!
由于设置环境非常困难,我们作者更愿意为读者提供一个虚拟机映像,其中包含您在集群上尝试某些操作所需的一切。在接下来的几节中,您将学习如何在您的机器上设置一个客户操作系统,该系统包含一个集群的一个节点以及您在真实集群上找到的所有软件。
为什么只有一个节点?由于我们使用的框架不是轻量级的,我们决定采用集群的原子块,确保您在节点中找到的环境与您在现实世界中找到的环境完全相同。为了在您的计算机上运行虚拟机,您需要两个软件:Virtualbox 和游民。两者都是免费开源的。
虚拟盒
VirtualBox 是一个开源软件,用于虚拟化 Windows、macOS 和 Linux 主机上的一对多客户操作系统。从用户的角度来看,一台虚拟机看起来就像另一台运行在窗口中的计算机,拥有所有功能。
VirtualBox 因其高性能、简单、干净的图形用户界面 ( GUI )而变得非常受欢迎。使用 VirtualBox 启动、停止、导入和终止虚拟机只需点击一下鼠标。
从技术上讲,VirtualBox 是一个虚拟机管理程序,它支持创建和管理多个虚拟机 ( VM )包括许多版本的 Windows、Linux 和类似 BSD 的发行版。VirtualBox 运行的机器命名为主机,而虚拟机命名为来宾。注意主人和客人之间没有限制;例如,一个 Windows 主机可以运行 Windows(相同的版本、以前的版本或最近的版本)以及任何与 VirtualBox 兼容的 Linux 和 BSD 发行版。
Virtualbox 通常用于运行特定于操作系统的软件;有些软件只在 Windows 上运行或者只是特定版本的 Windows,有些只在 Linux 上可用,等等。另一个应用是在克隆的生产环境中模拟新功能;在实时(生产)环境中尝试修改之前,软件开发人员通常会在一个克隆上测试它,就像在 VirtualBox 上运行的那样。由于来宾与主机隔离,如果来宾出现问题(甚至格式化硬盘),这不会影响主机。要拿回它,在做任何危险的事情之前,只要克隆你的机器;你总是能及时找回它。
对于想从头开始的人,VirtualBox 支持虚拟硬盘(包括硬盘、光盘、DVD、软盘);这使得新操作系统的安装非常简单。例如,如果你想安装一个普通版本的 Linux Ubuntu 14.04,你首先要下载.iso文件。您可以简单地将其作为虚拟驱动器添加到 VirtualBox,而不是将其刻录在 CD/DVD 上。然后,由于简单的分步界面,您可以选择硬盘大小和来宾机器的功能(内存、CPU 数量、视频内存和网络连接)。在使用真实 bios 操作时,可以选择引导顺序:选择 CD/DVD 作为更高的优先级,一打开 guest 就可以开始安装 Ubuntu 的过程。
现在,让我们下载 VirtualBox 请记住为您的操作系统选择正确的版本。
注
要将其安装到您的计算机上,请按照 https://www.virtualbox.org/wiki/Downloads T2 的说明进行操作。
在写这篇文章的时候,最新版本是 5.1。安装后,图形界面看起来像下面截图中的界面

我们强烈建议您看一看如何在您的机器上设置来宾机器。每个来宾机器将出现在窗口的左侧。(图中可以看到,在我们的电脑上,我们有三位被拦下的客人。)通过点击每个图标上的,右侧将出现虚拟化硬件的详细描述。在示例图像中,如果名为sparkbox_test的虚拟机(左边突出显示的那个)被打开,它将在一台虚拟计算机上运行,该计算机的硬件由一个 4GB 的内存、两个处理器、40GB 硬盘和一个带有 NAT 的连接到网络的 12MB 内存的视频卡组成。
流浪
游民是一个软件,配置虚拟环境在一个高水平。游民的核心部分是脚本功能,通常用于以编程方式创建和自动指定虚拟环境。游民使用 VirtualBox(以及其他虚拟器)来构建和配置虚拟机。
注
要安装它,请遵循 https://www.vagrantup.com/downloads.html T2 的说明。
使用虚拟机
安装了 float 和 VirtualBox 后,现在就可以运行集群环境的节点了。创建一个空目录,并将以下游民命令插入名为Vagrantfile的新文件中:
Vagrant.configure("2") do |config|
config.vm.box = "sparkpy/sparkbox_test_1"
config.vm.hostname = "sparkbox"
config.ssh.insert_key = false
# Hadoop ResourceManager
config.vm.network :forwarded_port, guest: 8088, host: 8088, auto_correct: true
# Hadoop NameNode
config.vm.network :forwarded_port, guest: 50070, host: 50070, auto_correct: true
# Hadoop DataNode
config.vm.network :forwarded_port, guest: 50075, host: 50075, auto_correct: true
# Ipython notebooks (yarn and standalone)
config.vm.network :forwarded_port, guest: 8888, host: 8888, auto_correct: true
# Spark UI (standalone)
config.vm.network :forwarded_port, guest: 4040, host: 4040, auto_correct: true
config.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
v.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
v.customize ["modifyvm", :id, "--nictype1", "virtio"]
v.name = "sparkbox_test"
v.memory = "4096"
v.cpus = "2"
end
end
从上到下,第一行下载正确的虚拟机(我们作者创建并上传到存储库中)。然后,我们设置一些端口转发给来宾机;这样,您将能够访问虚拟机的一些 web 服务。最后,我们设置节点的硬件。
注
该配置是为专用于 4GB 内存和两个内核的虚拟机设置的。如果您的系统不能满足这些要求,请将v.memory和v.cpus值修改为对您的机器有益的值。请注意,如果您设置的配置不合适,下面的一些代码示例可能会失败。
现在,打开一个终端,导航到包含Vagrantfile的目录。在这里,使用以下命令启动虚拟机:
$ vagrant up
第一次,这个命令需要一段时间来下载(几乎是 2GB 下载)并构建正确的虚拟机结构。下一次,这个命令花费的时间更少,因为没有更多的东西可以下载。
在本地系统上打开虚拟机后,您可以按如下方式访问它:
$ vagrant ssh
该命令模拟 SSH 访问,最终您将进入虚拟机。
注
在 Windows 机器上,由于缺少 SSH 可执行文件,此命令可能会失败并出现错误。在这种情况下,下载安装一个 Windows 的 SSH 客户端,比如 Putty(http://www.putty.org/)、CygwinOpenssh(http://www.cygwin.com/)或者 Windows 的 Openssh(http://sshwindows.sourceforge.net/)。Unix 系统不应该受到这个问题的影响。
要关闭 if,您首先需要退出机器。从虚拟机内部,只需使用exit命令退出 SSH 连接,然后关闭虚拟机:
$ vagrant halt
注
虚拟机消耗资源。当您使用虚拟机所在目录中的vagranthalt命令完成工作时,请记住将其关闭。
前面的命令关闭虚拟机,就像您关闭服务器一样。要删除它并删除其所有内容,请使用vagrant destroy命令。小心使用:在破坏了机器之后,您将无法恢复其中的文件。
以下是在虚拟机中使用 IPython (Jupyter)笔记本的说明:
-
从包含
Vagrantfile的文件夹中启动vagrant up和vagrant ssh。您现在应该在虚拟机内部了。 -
现在,启动脚本:
vagrant@sparkbox:~$ ./start_hadoop.sh -
At this point, launch the following shell script:
vagrant@sparkbox:~$ ./start_jupyter_yarn.sh在本地机器上打开浏览器,指向
http://localhost:8888。
这是由集群节点支持的笔记本。要关闭笔记本电脑和虚拟机,请执行以下步骤:
-
要终止 Jupyter 控制台,请按 Ctrl + C (然后键入 Y 表示是)。
-
终止 Hadoop 框架如下:
vagrant@sparkbox:~$ ./stop_hadoop.sh -
使用以下命令退出虚拟机:
vagrant@sparkbox:~$ exit -
用
vagrant halt关闭 VirtualBox 机器。
Hadoop 生态系统
Apache Hadoop 是一个非常流行的软件框架,用于集群上的分布式存储和分布式处理。它的优势在于价格(它是免费的)、灵活性(它是开源的,虽然是用 Java 编写的,但它可以被其他编程语言使用)、可扩展性(它可以处理由成千上万个节点组成的集群)和健壮性(它的灵感来自谷歌发表的一篇论文,自 2011 年以来一直存在),使它成为处理和处理大数据的事实标准。此外,Apache 基金会的许多其他项目扩展了它的功能。
建筑
从逻辑上讲,Hadoop 由两部分组成:分布式存储(HDFS)和分布式处理(纱和 MapReduce)。虽然代码非常复杂,但整体架构相当容易理解。客户端可以通过两个专用模块访问存储和处理;然后,他们负责在所有理论节点上分配工作:

所有 Hadoop 模块都作为服务(或实例)运行,也就是说,一个物理或虚拟节点可以运行其中的许多模块。典型地,对于小集群,所有节点都运行分布式计算和处理服务;对于大型集群,最好将两个专门用于节点的功能分开。
我们将详细了解这两层提供的功能。
HDFS
Hadoop 分布式文件系统 ( HDFS )是一个容错分布式文件系统,旨在运行在商用低成本硬件上,并能够处理非常大的数据集(几百千兆字节到十六千兆字节)。虽然 HDFS 需要快速的网络连接来跨节点传输数据,但是延迟不可能像传统文件系统那样低(可能在几秒钟的数量级);因此,HDFS 专为批量处理和高吞吐量而设计。每个 HDFS 节点包含文件系统数据的一部分;相同的数据也在其他实例中复制,这确保了高吞吐量访问和容错。
HDFS 的建筑是主从式的。如果主节点(名称节点)出现故障,则有一个辅助/备份节点准备接管。所有其他实例都是从属实例(数据节点);如果其中一个失败了,没有问题,因为 HDFS 在设计时就考虑到了这一点。
数据节点包含数据块:保存在 HDFS 的每个文件被分成块(或区块),通常每个 64MB,然后在一组数据节点中分发和复制。
名称节点仅存储分布式文件系统中文件的元数据;它不存储任何实际数据,而只是正确指示如何访问它管理的多个数据节点中的文件。
请求读取文件的客户端应首先联系名称节点,该节点将返回一个表,其中包含块及其位置的有序列表(如在数据节点中)。此时,客户端应该单独联系数据节点,下载所有块并重建文件(通过将块附加在一起)。
相反,要写入文件,客户端应该首先联系名称节点,该节点将首先决定如何处理该请求,更新其记录,然后向客户端回复一个有序的数据节点列表,说明在何处写入文件的每个块。客户端现在将联系数据节点并将数据块上传到数据节点,如名称节点回复中所述。
名称空间查询(例如,列出目录内容、创建文件夹等)完全由名称节点通过访问其元数据信息来处理。
此外,名称节点还负责正确处理数据节点故障(如果没有接收到心跳数据包,它将被标记为死亡),并将其数据重新复制到其他节点。
尽管这些操作很长,很难健壮地实现,但是由于许多库和 HDFS shell,它们对用户来说是完全透明的。您在 HDFS 上操作的方式与您当前在文件系统上所做的非常相似,这是 Hadoop 的一大优势:隐藏复杂性,让用户简单地使用它。
现在让我们来看看 HDFS shell 和后来的 Python 库。
注
使用前面的说明打开虚拟机,并在您的计算机上启动 IPython 笔记本。
现在,打开一个新笔记本;由于每个笔记本都连接到 Hadoop 集群框架,因此此操作将比平时花费更多的时间。当笔记本准备使用时,你会看到右上角的内核启动,请等待……标志消失。
第一篇是关于 HDFS 贝壳的;因此,以下所有命令都可以在虚拟机的提示符或 shell 下运行。要在 IPython 笔记本中运行它们,所有这些都需要一个问号!,这是在笔记本中执行 bash 代码的一种简单方法。
以下命令行的共同点是可执行文件;我们将始终运行hdfs命令。它是访问和管理 HDFS 系统的主要界面,也是 HDFS shell 的主要命令。
我们从一份关于 HDFS 状况的报告开始。要获取分布式文件系统 ( dfs )及其数据节点的详细信息,请使用dfsadmin子命令:
In:!hdfs dfsadmin –report
Out:Configured Capacity: 42241163264 (39.34 GB)
Present Capacity: 37569168058 (34.99 GB)
DFS Remaining: 37378433024 (34.81 GB)
DFS Used: 190735034 (181.90 MB)
DFS Used%: 0.51%
Under replicated blocks: 0
Blocks with corrupt replicas: 0
Missing blocks: 0
-------------------------------------------------
Live datanodes (1):
Name: 127.0.0.1:50010 (localhost)
Hostname: sparkbox
Decommission Status : Normal
Configured Capacity: 42241163264 (39.34 GB)
DFS Used: 190735034 (181.90 MB)
Non DFS Used: 4668290330 (4.35 GB)
DFS Remaining: 37380775936 (34.81 GB)
DFS Used%: 0.45%
DFS Remaining%: 88.49%
Configured Cache Capacity: 0 (0 B)
Cache Used: 0 (0 B)
Cache Remaining: 0 (0 B)
Cache Used%: 100.00%
Cache Remaining%: 0.00%
Xceivers: 1
Last contact: Tue Feb 09 19:41:17 UTC 2016
dfs 子命令允许使用一些众所周知的 Unix 命令来访问分布式文件系统并与之交互。例如,列出根目录的内容如下:
In:!hdfs dfs -ls /
Out:Found 2 items
drwxr-xr-x - vagrant supergroup 0 2016-01-30 16:33 /spark
drwxr-xr-x - vagrant supergroup 0 2016-01-30 18:12 /user
输出类似于 Linux 提供的ls命令,列出了权限、链接数量、拥有文件的用户和组、大小、上次修改的时间戳以及每个文件或目录的名称。
类似于df命令,我们可以调用-df参数来显示 HDFS 的可用磁盘空间量。-h选项将使输出更加易读(使用千兆字节和兆字节代替字节):
In:!hdfs dfs -df -h /
Out:Filesystem Size Used Available Use%
hdfs://localhost:9000 39.3 G 181.9 M 34.8 G 0%
类似于du,我们可以使用-du参数来显示根目录中包含的每个文件夹的大小。同样,-h将产生更具可读性的输出:
In:!hdfs dfs -du -h /
Out:178.9 M /spark
1.4 M /user
到目前为止,我们已经从 HDFS 提取了一些信息。现在让我们对分布式文件系统做一些操作,这会修改它。我们可以从创建一个名为-mkdir的文件夹开始。请注意,如果目录已经存在(与 Linux 中完全一样,使用mkdir命令),此操作可能会失败:
In:!hdfs dfs -mkdir /datasets
现在让我们将一些文件从节点的硬盘传输到分布式文件系统。在我们创建的虚拟机中,../datasets目录中已经有一个文本文件;让我们从网上下载一个文本文件。让我们将它们都移动到我们用前面的命令创建的 HDFS 目录中:
In:
!wget -q http://www.gutenberg.org/cache/epub/100/pg100.txt \
-O ../datasets/shakespeare_all.txt
!hdfs dfs -put ../datasets/shakespeare_all.txt \
/datasets/shakespeare_all.txt
!hdfs dfs -put ../datasets/hadoop_git_readme.txt \
/datasets/hadoop_git_readme.txt
导入成功吗?是的,我们没有任何错误。但是,为了消除任何疑问,让我们列出 HDFS 目录/数据集来查看这两个文件:
In:!hdfs dfs -ls /datasets
Out:Found 2 items
-rw-r--r-- 1 vagrant supergroup 1365 2016-01-31 12:41 /datasets/hadoop_git_readme.txt
-rw-r--r-- 1 vagrant supergroup 5589889 2016-01-31 12:41 /datasets/shakespeare_all.txt
要将一些文件连接到标准输出,我们可以使用-cat参数。在接下来的代码中,我们计算了文本文件中出现的新行。请注意,第一个命令通过管道传输到在本地计算机上运行的另一个命令中:
In:!hdfs dfs -cat /datasets/hadoop_git_readme.txt | wc –l
Out:30
实际上,通过-cat参数,我们可以连接本地机器和 HDFS 的多个文件。要查看它,现在让我们计算一下当存储在 HDFS 的文件与存储在本地机器上的文件连接在一起时,有多少新行出现。为了避免误解,我们可以使用完整的统一资源标识符 ( URI ),使用hdfs:方案引用 HDFS 的文件,使用file:方案引用本地文件:
In:!hdfs dfs -cat \
hdfs:///datasets/hadoop_git_readme.txt \
file:///home/vagrant/datasets/hadoop_git_readme.txt | wc –l
Out:60
为了在 HDFS 复制,我们可以使用-cp参数:
In : !hdfs dfs -cp /datasets/hadoop_git_readme.txt \
/datasets/copy_hadoop_git_readme.txt
要删除文件(或目录,用正确的选项),我们可以使用–rm参数。在这段代码中,我们删除了刚刚用前面的命令创建的文件。请注意,HDFS 有鞭打机制;因此,删除的文件实际上并没有从 HDFS 中删除,而只是移动到了一个特殊的目录中:
In:!hdfs dfs -rm /datasets/copy_hadoop_git_readme.txt
Out:16/02/09 21:41:44 INFO fs.TrashPolicyDefault: Namenode trash configuration: Deletion interval = 0 minutes, Emptier interval = 0 minutes.
Deleted /datasets/copy_hadoop_git_readme.txt
要清空经过反复研究的数据,命令如下:
In:!hdfs dfs –expunge
Out:16/02/09 21:41:44 INFO fs.TrashPolicyDefault: Namenode trash configuration: Deletion interval = 0 minutes, Emptier interval = 0 minutes.
要获得(获取)从 HDFS 到本地机器的文件,我们可以使用-get参数:
In:!hdfs dfs -get /datasets/hadoop_git_readme.txt \
/tmp/hadoop_git_readme.txt
要查看存储在 HDFS 的文件,我们可以使用-tail参数。请注意,在 HDFS 没有 head 功能,因为它可以使用cat来完成,然后将结果输入本地 head 命令。至于尾部,HDFS 外壳只显示最后一千字节的数据:
In:!hdfs dfs -tail /datasets/hadoop_git_readme.txt
Out:ntry, of
encryption software. BEFORE using any encryption software, please
check your country's laws, regulations and policies concerning the
import, possession, or use, and re-export of encryption software, to
see if this is permitted. See <http://www.wassenaar.org/> for more
information.
[...]
hdfs命令是 HDFS 的主要入口点,但是它很慢,从 Python 中调用系统命令并读取输出非常繁琐。为此,有一个 Python 库“蛇咬”,它包装了许多分布式文件系统操作。不幸的是,该库不像 HDFS 外壳那样完整,并且绑定到一个名称节点。要在本地机器上安装,只需使用pip install snakebite。
要实例化客户端对象,我们应该提供名称节点的 IP(或其别名)和端口。在我们提供的虚拟机中,它运行在端口 9000 上:
In:from snakebite.client import Client
client = Client("localhost", 9000)
要打印一些关于 HDFS 的信息,客户端对象有serverdefaults方法:
In:client.serverdefaults()
Out:{'blockSize': 134217728L,
'bytesPerChecksum': 512,
'checksumType': 2,
'encryptDataTransfer': False,
'fileBufferSize': 4096,
'replication': 1,
'trashInterval': 0L,
'writePacketSize': 65536}
要在根目录中列出文件和目录,我们可以使用ls方法。结果是一个字典列表,每个文件一个,包含信息,如权限、上次修改的时间戳等。在这个例子中,我们只对路径(即名称)感兴趣:
In:for x in client.ls(['/']):
print x['path']
Out:/datasets
/spark
/user
与前面的代码完全一样,蛇咬客户端有du(用于磁盘使用)和df(用于磁盘空闲)方法可用。请注意,许多方法(如du)返回生成器,这意味着它们需要被消费(如迭代器或列表)才能执行:
In:client.df()
Out:{'capacity': 42241163264L,
'corrupt_blocks': 0L,
'filesystem': 'hdfs://localhost:9000',
'missing_blocks': 0L,
'remaining': 37373218816L,
'under_replicated': 0L,
'used': 196237268L}
In:list(client.du(["/"]))
Out:[{'length': 5591254L, 'path': '/datasets'},
{'length': 187548272L, 'path': '/spark'},
{'length': 1449302L, 'path': '/user'}]
至于 HDFS shell 示例,我们现在将尝试计算与蛇咬出现在同一文件中的换行符。注意.cat方法返回一个生成器:
In:
for el in client.cat(['/datasets/hadoop_git_readme.txt']):
print el.next().count("\n")
Out:30
现在让我们从 HDFS 删除一个文件。同样,请注意delete方法返回一个生成器,并且执行永远不会失败,即使我们试图删除一个不存在的目录。事实上,蛇咬不会引发异常,只是在输出字典中向用户发出操作失败的信号:
In:client.delete(['/datasets/shakespeare_all.txt']).next()
Out:{'path': '/datasets/shakespeare_all.txt', 'result': True}
现在,让我们将一个文件从 HDFS 复制到本地文件系统。观察输出是发电机,需要查看输出字典,看操作是否成功:
In:
(client
.copyToLocal(['/datasets/hadoop_git_readme.txt'],
'/tmp/hadoop_git_readme_2.txt')
.next())
Out:{'error': '',
'path': '/tmp/hadoop_git_readme_2.txt',
'result': True,
'source_path': '/datasets/hadoop_git_readme.txt'}
最后,创建一个目录并删除所有匹配字符串的文件:
In:list(client.mkdir(['/datasets_2']))
Out:[{'path': '/datasets_2', 'result': True}]
In:client.delete(['/datasets*'], recurse=True).next()
Out:{'path': '/datasets', 'result': True}
在 HDFS 存档的代码在哪里?将 HDFS 文件复制到另一个文件的代码在哪里?嗯,这些功能还没有在蛇咬中实现。对于他们,我们将通过系统调用使用 HDFS 外壳。
MapReduce
MapReduce 是在最早的 Hadoop 版本中实现的编程模型。这是一个非常简单的模型,旨在并行批处理分布式集群上的大型数据集。MapReduce 的核心由两个可编程功能组成——一个执行过滤的映射器和一个执行聚合的缩减器——以及一个将对象从映射器移动到右侧缩减器的洗牌器。
注
谷歌在 2004 年发表了一篇关于 Mapreduce 的论文,几个月前它获得了一项专利。
具体来说,以下是 Hadoop 实现的 MapReduce 步骤:
-
Data chunker. Data is read from the filesystem and split into chunks. A chunk is a piece of the input dataset, typically either a fixed-size block (for example, a HDFS block read from a Data Node) or another more appropriate split.
例如,如果我们想计算一个文本文件中的字符、单词和行数,一个好的拆分可以是一行文本。
-
Mapper: From each chunk, a series of key-value pairs is generated. Each mapper instance applies the same mapping function on different chunks of data.
继续前面的例子,对于每一行,在这个步骤中生成三个键值对——一个包含该行中的字符数(键可以简单地是一个字符字符串),一个包含单词数(在这种情况下,键必须不同,让我们说单词),一个包含行数,它总是一个(在这种情况下,键可以是行)。
-
洗牌机:根据可用的缩减器的键和数量,洗牌机将具有相同键的所有键值对分配给相同的缩减器。典型地,这个操作是密钥的散列,取减数的模。这应确保每个减速器有相当数量的键。这个函数不是用户可编程的,而是由 MapReduce 框架提供的。
-
Reducer: Each reducer receives all the key-value pairs for a specific set of keys and can produce zero or more aggregate results.
在本例中,所有连接到字键的值都到达一个减速器;它的工作只是总结所有的价值。其他键也是如此,最终得到三个值:字符数、字数和行数。请注意,这些结果可能在不同的减速器上。
-
Output writer: The outputs of the reducers are written on the filesystem (or HDFS). In the default Hadoop configuration, each reducer writes a file (
part-r-00000is the output of the first reducer,part-r-00001of the second, and so on). To have a full list of results on a file, you should concatenate all of them.从视觉上看,该操作可以简单地传达和理解如下:
![MapReduce]()
在映射步骤之后,每个映射器实例还可以运行一个可选步骤——合并器。如果可能的话,它基本上预测映射器上的减少步骤,并且通常用于减少要混洗的信息量,从而加快过程。在前面的示例中,如果映射器在(可选的)组合器步骤中处理输入文件的多行,它可以预聚合结果,输出较少数量的键值对。例如,如果映射器处理每个块中的 100 行文本,那么当信息可以聚合为三个时,为什么要输出 300 个键值对(100 个字符,100 个单词,100 行)?这实际上是组合器的目标。
在 Hadoop 提供的 MapReduce 实现中,洗牌操作是分布式的,优化了通信成本,并且每个节点可以运行多个映射器和缩减器,充分利用了节点上可用的硬件资源。此外,Hadoop 基础架构提供冗余和容错,因为同一任务可以分配给多个工作人员。
现在让我们看看它是如何工作的。尽管 Hadoop 框架是用 Java 编写的,但由于 Hadoop Streaming 实用程序,映射器和缩减器可以是任何可执行文件,包括 Python。Hadoop Streaming 使用管道和标准输入和输出来流式传输内容;因此,mappers 和 reducers 必须实现 stdin 的读取器和 stdout 上的键值写入器。
现在,打开虚拟机,打开一个新的 IPython 笔记本。即使在这种情况下,我们也会首先引入命令行方式来运行 Hadoop 提供的【MapReduce 作业,然后引入一个纯 Python 库。第一个例子正是我们所描述的:一个文本文件的字符、单词和行数的计数器。
首先,让我们将数据集插入 HDFS;我们将使用 Hadoop Git readme(一个包含随 Apache Hadoop 分发的 readme 文件的简短文本文件)和所有莎士比亚书籍的全文,由 Project Gutenberg 提供(虽然它只有 5MB,但包含了几乎 125K 行)。在第一个单元格中,我们将清理上一个实验的文件夹,然后,我们在数据集文件夹中下载包含莎士比亚参考书目的文件,最后,我们将两个数据集放在 HDFS 上:
In:!hdfs dfs -mkdir -p /datasets
!wget -q http://www.gutenberg.org/cache/epub/100/pg100.txt \
-O ../datasets/shakespeare_all.txt
!hdfs dfs -put -f ../datasets/shakespeare_all.txt /datasets/shakespeare_all.txt
!hdfs dfs -put -f ../datasets/hadoop_git_readme.txt /datasets/hadoop_git_readme.txt
!hdfs dfs -ls /datasets
现在,让我们创建包含映射器和缩减器的 Python 可执行文件。我们将在这里使用一个非常肮脏的黑客:我们将使用笔记本中的写操作来编写 Python 文件(并使它们可执行)。
映射器和缩减器都从 stdin 读取并写入 stdout(使用简单的打印命令)。具体来说,映射器从 stdin 中读取行,并打印字符数(除了换行符)、字数(通过在空白上拆分行)和行数的键值对,始终为 1。相反,缩减器会汇总每个键的值,并打印总计:
In:
with open('mapper_hadoop.py', 'w') as fh:
fh.write("""#!/usr/bin/env python
import sys
for line in sys.stdin:
print "chars", len(line.rstrip('\\n'))
print "words", len(line.split())
print "lines", 1
""")
with open('reducer_hadoop.py', 'w') as fh:
fh.write("""#!/usr/bin/env python
import sys
counts = {"chars": 0, "words":0, "lines":0}
for line in sys.stdin:
kv = line.rstrip().split()
counts[kv[0]] += int(kv[1])
for k,v in counts.items():
print k, v
""")
In:!chmod a+x *_hadoop.py
为了在工作中看到它,让我们在不使用 Hadoop 的情况下在本地尝试它。事实上,当映射器和还原器读写标准输入和输出时,我们可以把所有的东西放在一起。请注意,洗牌机可以由sort -k1,1命令代替,该命令使用第一个字段(即键)对输入字符串进行排序:
In:!cat ../datasets/hadoop_git_readme.txt | ./mapper_hadoop.py | sort -k1,1 | ./reducer_hadoop.py
Out:chars 1335
lines 31
words 179
现在让我们使用 Hadoop MapReduce 方法来获得相同的结果。首先,我们应该在 HDFS 创建一个能够存储结果的空目录。在这种情况下,我们创建了一个名为/tmp的目录,并以与作业输出相同的方式删除其中的任何名称(如果输出文件已经存在,Hadoop 将失败)。然后,我们使用正确的命令来运行 MapReduce 作业。该命令包括以下内容:
- 我们想要使用 Hadoop 流功能的事实(表示 Hadoop 流 jar 文件)
- 我们想要使用的映射器和缩减器(T0 和 T1 选项)
- 我们希望将这些文件作为本地文件分发给每个映射器(使用
–files选项) - 输入文件(
–input选项)和输出目录(–output选项)
In:!hdfs dfs -mkdir -p /tmp
!hdfs dfs -rm -f -r /tmp/mr.out
!hadoop jar /usr/local/hadoop/share/hadoop/tools/lib/hadoop-streaming-2.6.4.jar \
-files mapper_hadoop.py,reducer_hadoop.py \
-mapper mapper_hadoop.py -reducer reducer_hadoop.py \
-input /datasets/hadoop_git_readme.txt -output /tmp/mr.out
Out:[...]
16/02/04 17:12:22 INFO mapreduce.Job: Running job: job_1454605686295_0003
16/02/04 17:12:29 INFO mapreduce.Job: Job job_1454605686295_0003 running in uber mode : false
16/02/04 17:12:29 INFO mapreduce.Job: map 0% reduce 0%
16/02/04 17:12:35 INFO mapreduce.Job: map 50% reduce 0%
16/02/04 17:12:41 INFO mapreduce.Job: map 100% reduce 0%
16/02/04 17:12:47 INFO mapreduce.Job: map 100% reduce 100%
16/02/04 17:12:47 INFO mapreduce.Job: Job job_1454605686295_0003 completed successfully
[...]
Shuffle Errors
BAD_ID=0
CONNECTION=0
IO_ERROR=0
WRONG_LENGTH=0
WRONG_MAP=0
WRONG_REDUCE=0
[...]
16/02/04 17:12:47 INFO streaming.StreamJob: Output directory: /tmp/mr.out
输出很啰嗦;我们只是从中提取了三个重要部分。第一个指示 MapReduce 作业的进度,跟踪和估计完成操作所需的时间非常有用。第二部分突出显示了在作业期间可能发生的错误,最后一部分报告了终止的输出目录和时间戳。小文件(30 行)上的整个过程几乎花了半分钟!原因很简单:第一,Hadoop MapReduce 是为强大的大数据处理而设计的,并且包含大量开销,第二,理想的环境是强大机器的集群,而不是具有 4GB RAM 的虚拟化 VM。另一方面,这些代码可以在更大的数据集和强大机器的集群上运行,而不需要做任何改变。
我们不要马上看到结果。首先,让我们看一下 HDFS 的输出目录:
In:!hdfs dfs -ls /tmp/mr.out
Out:Found 2 items
-rw-r--r-- 1 vagrant supergroup 0 2016-02-04 17:12 /tmp/mr.out/_SUCCESS
-rw-r--r-- 1 vagrant supergroup 33 2016-02-04 17:12 /tmp/mr.out/part-00000
有两个文件:第一个是空的,名为_SUCCESS,表示 MapReduce 作业已经完成了目录中的写入阶段,第二个名为 part-00000,包含实际结果(因为我们在只有一个 Reduce 的节点上操作)。阅读此文件将为我们提供最终结果:
In:!hdfs dfs -cat /tmp/mr.out/part-00000
Out:chars 1335
lines 31
words 179
正如预期的那样,它们与前面显示的管道命令行相同。
虽然概念上很简单,但 Hadoop Streaming 并不是用 Python 代码运行 Hadoop 作业的最佳方式。为此,Pypy 上有很多可用的库;我们在这里展示的这个是最灵活和维护最开放的源代码之一—MrJob。它允许您在本地机器、Hadoop 集群或相同的云集群环境(如亚马逊弹性地图缩减)上无缝运行作业;它将所有代码合并到一个独立的文件中,即使需要多个 MapReduce 步骤(考虑迭代算法),并解释代码中的 Hadoop 错误。此外,安装非常简单;要在本地机器上安装 MrJob 库,只需使用pip install mrjob。
虽然 MrJob 是一款很棒的软件,但它在 IPython Notebook 上运行不太好,因为它需要一个主要功能。这里,我们需要在一个单独的文件中编写 MapReduce Python 代码,然后运行一个命令行。
我们从我们已经看过很多次的例子开始:计算文件中的字符、单词和行。首先,让我们使用 MrJob 功能编写 Python 文件;mappers 和 reducers】被包裹在MRJob的一个子类中。输入不是从 stdin 读取的,而是作为函数参数传递的,输出不是打印的,而是产生的(或返回的)。
多亏了 MrJob,整个 MapReduce 程序变成了几行代码:
In:
with open("MrJob_job1.py", "w") as fh:
fh.write("""
from mrjob.job import MRJob
class MRWordFrequencyCount(MRJob):
def mapper(self, _, line):
yield "chars", len(line)
yield "words", len(line.split())
yield "lines", 1
def reducer(self, key, values):
yield key, sum(values)
if __name__ == '__main__':
MRWordFrequencyCount.run()
""")
现在让我们在本地执行它(使用数据集的本地版本)。MrJob 库除了执行映射器和缩减器步骤(在本例中是本地)之外,还打印结果并清理临时目录:
In:!python MrJob_job1.py ../datasets/hadoop_git_readme.txt
Out: [...]
Streaming final output from /tmp/MrJob_job1.vagrant.20160204.171254.595542/output
"chars" 1335
"lines" 31
"words" 179
removing tmp directory /tmp/MrJob_job1.vagrant.20160204.171254.595542
要在 Hadoop 上运行相同的进程,只需运行相同的 Python 文件,这次在命令行中插入–r hadoop选项,MrJob 将使用 Hadoop MapReduce 和 HDFS 自动执行它。在这种情况下,记得指向输入文件的hdfs路径:
In:
!python MrJob_job1.py -r hadoop hdfs:///datasets/hadoop_git_readme.txt
Out:[...]
HADOOP: Running job: job_1454605686295_0004
HADOOP: Job job_1454605686295_0004 running in uber mode : false
HADOOP: map 0% reduce 0%
HADOOP: map 50% reduce 0%
HADOOP: map 100% reduce 0%
HADOOP: map 100% reduce 100%
HADOOP: Job job_1454605686295_0004 completed successfully
[...]
HADOOP: Shuffle Errors
HADOOP: BAD_ID=0
HADOOP: CONNECTION=0
HADOOP: IO_ERROR=0
HADOOP: WRONG_LENGTH=0
HADOOP: WRONG_MAP=0
HADOOP: WRONG_REDUCE=0
[...]
Streaming final output from hdfs:///user/vagrant/tmp/mrjob/MrJob_job1.vagrant.20160204.171255.073506/output
"chars" 1335
"lines" 31
"words" 179
removing tmp directory /tmp/MrJob_job1.vagrant.20160204.171255.073506
deleting hdfs:///user/vagrant/tmp/mrjob/MrJob_job1.vagrant.20160204.171255.073506 from HDFS
您将看到与之前看到的相同的 Hadoop Streaming 命令行输出以及结果。在这种情况下,用于存储结果的 HDFS 临时目录在作业终止后被删除。
现在,为了查看 MrJob 的灵活性,让我们尝试运行一个需要比一个 MapReduce 步骤更多的的流程。虽然是从命令行完成的,但这是一项非常困难的任务;事实上,您必须运行 MapReduce 的第一次迭代,检查错误,读取结果,然后启动 MapReduce 的第二次迭代,再次检查错误,最后读取结果。这听起来非常耗时,并且容易出错。由于 MrJob,这个操作非常容易:在代码中,可以创建一个 MapReduce 操作的级联,其中每个输出都是下一阶段的输入。
举个例子,让我们现在找到莎士比亚最常用的单词(使用 125 千行文件作为输入)。此操作不能在单个 MapReduce 步骤中完成;它需要至少两个。我们将基于 MapReduce 的两次迭代实现一个非常简单的算法:
- 数据分块器:就像默认的 MrJob 一样,输入文件在每一行被分割。
- 阶段 1-映射:为每个单词生成一个键映射元组;关键字是小写的单词,值总是 1。
- 阶段 1–减少:对于每个键(较低的单词),我们将所有值相加。输出会告诉我们这个单词在文本中出现了多少次。
- 阶段 2-映射:在这个步骤中,我们翻转键值元组,并将它们作为新键值对的值。为了强制一个缩减器拥有所有元组,我们为每个输出元组分配相同的键 None 。
- 阶段 2–减少:我们简单地丢弃唯一可用的关键字,并提取最大值,从而提取所有元组(计数、单词)的最大值。
In:
with open("MrJob_job2.py", "w") as fh:
fh.write("""
from mrjob.job import MRJob
from mrjob.step import MRStep
import re
WORD_RE = re.compile(r"[\w']+")
class MRMostUsedWord(MRJob):
def steps(self):
return [
MRStep(mapper=self.mapper_get_words,
reducer=self.reducer_count_words),
MRStep(mapper=self.mapper_word_count_one_key,
reducer=self.reducer_find_max_word)
]
def mapper_get_words(self, _, line):
# yield each word in the line
for word in WORD_RE.findall(line):
yield (word.lower(), 1)
def reducer_count_words(self, word, counts):
# send all (num_occurrences, word) pairs to the same reducer.
yield (word, sum(counts))
def mapper_word_count_one_key(self, word, counts):
# send all the tuples to same reducer
yield None, (counts, word)
def reducer_find_max_word(self, _, count_word_pairs):
# each item of word_count_pairs is a tuple (count, word),
yield max(count_word_pairs)
if __name__ == '__main__':
MRMostUsedWord.run()
""")
然后,我们可以决定在本地或 Hadoop 集群上运行它,获得相同的结果:威廉·莎士比亚最常用的单词是单词,使用了 27K 多次。在这段代码中,我们只想输出结果;因此,我们使用--quiet选项启动作业:
In:!python MrJob_job2.py --quiet ../datasets/shakespeare_all.txt
Out:27801 "the"
In:!python MrJob_job2.py -r hadoop --quiet hdfs:///datasets/shakespeare_all.txt
Out:27801 "the"
纱
借助 Hadoop 2(截至 2016 年的当前分支),在 HDFS 之上引入了一层,允许多个应用运行,例如,MapReduce 就是其中之一(针对批处理)。这一层的名称是又一个资源协商者 ( 纱)和其目标是管理集群中的资源管理。
纱线遵循主/从模式,由两个服务组成:资源管理器和节点管理器。
资源管理器是主控器,负责两件事:调度(分配资源)和应用管理(处理作业提交和跟踪其状态)。每个节点管理器都是架构的从属,是运行任务并向资源管理器报告的每个工作框架。
Hadoop 2 引入的纱层确保了以下几点:
- 多租户,即拥有多个引擎来使用 Hadoop
- 更好的集群利用率,因为任务的分配是动态的和可调度的
- 更好的可扩展性;纱不提供处理算法,它只是集群的资源管理器
- 与 MapReduce 的兼容性(Hadoop 1 中的较高层)
Spark
Apache Spark 是 Hadoop 的一个发展,在过去的几年里变得非常流行。与 Hadoop 及其 Java 和以批处理为中心的设计相反,Spark 能够以快速简单的方式产生迭代算法。此外,它有一套非常丰富的多种编程语言的 API,并且本机支持许多不同类型的数据处理(机器学习、流、图形分析、SQL 等)。
Apache Spark 是一个集群框架,旨在快速和通用地处理大数据。速度上的改进之一是,数据在每次作业后都保存在内存中,而不是像 Hadoop、MapReduce 和 HDFS 那样存储在文件系统中(除非您想这样做)。这使得迭代作业(如聚类 K-means 算法)越来越快,因为内存提供的延迟和带宽比物理磁盘性能更好。因此,运行 Spark 的集群需要为每个节点分配大量内存。
虽然 Spark 是在 Scala 中开发的(它和 Java 一样运行在 JVM 上),但它有多种编程语言的 API,包括 Java、Scala、Python 和 r .在本书中,我们将重点介绍 Python。
Spark 可以通过两种不同的方式运行:
- 独立模式:它运行在您的本地机器上。在这种情况下,最大并行化是本地机器的内核数量,可用内存量与本地机器完全相同。
- 集群模式:它在多个节点的集群上运行,使用一个集群管理器,如纱。在这种情况下,最大并行化是组成集群的所有节点上的核心数量,内存量是每个节点的内存量之和。
pySpark
为了使用 Spark 功能(或 pySpark,包含 Spark 的 Python APIs),我们需要实例化一个名为 SparkContext 的特殊对象。它告诉 Spark 如何访问集群,并包含一些特定于应用的参数。在虚拟机中提供的 IPython Notebook 中,该变量已经可用并命名为sc(这是 IPython Notebook 启动时的默认选项);现在让我们看看它包含了什么。
首先,打开一个新的 IPython 笔记本;准备好使用时,在第一个单元格中键入以下内容:
In:sc._conf.getAll()
Out:[(u'spark.rdd.compress', u'True'),
(u'spark.master', u'yarn-client'),
(u'spark.serializer.objectStreamReset', u'100'),
(u'spark.yarn.isPython', u'true'),
(u'spark.submit.deployMode', u'client'),
(u'spark.executor.cores', u'2'),
(u'spark.app.name', u'PySparkShell')]
它包含多个信息:最重要的是spark.master,在这种情况下设置为纱中的客户端,spark.executor.cores设置为 2 作为虚拟机的 CPU 数量, spark.app.name,应用的名称。应用的名称在共享(纱)集群时特别有用;转到ht.0.0.1:8088,可以检查应用的状态:

Spark 使用的数据模型被命名为弹性分布式数据集 ( RDD ),它是可以并行处理的元素的分布式集合。RDD 可以从现有集合(例如 Python 列表)或外部数据集创建,并作为文件存储在本地计算机、HDFS 或其他来源上。
现在让我们创建一个包含从 0 到 9 的整数的 RDD。为此,我们可以使用SparkContext对象提供的parallelize方法:
In:numbers = range(10)
numbers_rdd = sc.parallelize(numbers)
numbers_rdd
Out:ParallelCollectionRDD[1] at parallelize at PythonRDD.scala:423
如您所见,您不能简单地打印 RDD 内容,因为它被分成多个分区(并分布在集群中)。默认的分区数量是 CPU 数量的两倍(因此,在提供的虚拟机中是四个),但是可以使用并行化方法的第二个参数手动设置。
要打印出 RDD 中包含的数据,您应该调用collect方法。请注意,当在集群上运行时,该操作会收集节点上的所有数据;因此,节点应该有足够的内存来容纳它:
In:numbers_rdd.collect()
Out:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
要获得部分预览,请使用take方法指示您想要查看的元素数量。请注意,由于它是一个分布式数据集,因此不能保证元素的顺序与我们插入时的顺序相同:
In:numbers_rdd.take()
Out:[0, 1, 2, 3]
要读取文本文件,我们可以使用 Spark Context 提供的textFile方法。它允许读取 HDFS 文件和本地文件,并在换行符上拆分文本;因此,RDD 的第一个元素是文本文件的第一行(使用first方法)。请注意,如果您使用本地路径,组成集群的所有节点应该通过相同的路径访问相同的文件:
In:sc.textFile("hdfs:///datasets/hadoop_git_readme.txt").first()
Out:u'For the latest information about Hadoop, please visit our website at:'
In:sc.textFile("file:///home/vagrant/datasets/hadoop_git_readme.txt").first()
Out:u'For the latest information about Hadoop, please visit our website at:'
要将 RDD 的内容保存在磁盘上,可以使用 RDD 提供的saveAsTextFile方法。在这里,你可以使用多个目的地;在这个例子中,让我们把它保存在 HDFS,然后列出输出的内容:
In:numbers_rdd.saveAsTextFile("hdfs:///tmp/numbers_1_10.txt")
In:!hdfs dfs -ls /tmp/numbers_1_10.txt
Out:Found 5 items
-rw-r--r-- 1 vagrant supergroup 0 2016-02-12 14:18 /tmp/numbers_1_10.txt/_SUCCESS
-rw-r--r-- 1 vagrant supergroup 4 2016-02-12 14:18 /tmp/numbers_1_10.txt/part-00000
-rw-r--r-- 1 vagrant supergroup 4 2016-02-12 14:18 /tmp/numbers_1_10.txt/part-00001
-rw-r--r-- 1 vagrant supergroup 4 2016-02-12 14:18 /tmp/numbers_1_10.txt/part-00002
-rw-r--r-- 1 vagrant supergroup 8 2016-02-12 14:18 /tmp/numbers_1_10.txt/part-00003
Spark 为每个分区写一个文件,与 MapReduce 完全一样,为每个 Reduce 写一个文件。这种方式加快了保存时间,因为每个分区都是独立保存的,但是在一个节点的集群中,这使得读取更加困难。
在写入文件之前,我们可以将所有的分区设为 1 吗?或者,一般来说,我们可以减少一个 RDD 的分区数量吗?答案是肯定的,通过 RDD 提供的coalesce方法,传递我们想要的分区数量作为参数。传递1会强制 RDD 位于一个独立的分区中,并且在保存时,只生成一个输出文件。请注意,即使保存在本地文件系统上也会发生这种情况:每个分区都会创建一个文件。请注意,在由多个节点组成的集群环境中这样做并不能确保所有节点看到相同的输出文件:
In:
numbers_rdd.coalesce(1) \
.saveAsTextFile("hdfs:///tmp/numbers_1_10_one_file.txt")
In : !hdfs dfs -ls /tmp/numbers_1_10_one_file.txt
Out:Found 2 items
-rw-r--r-- 1 vagrant supergroup 0 2016-02-12 14:20 /tmp/numbers_1_10_one_file.txt/_SUCCESS
-rw-r--r-- 1 vagrant supergroup 20 2016-02-12 14:20 /tmp/numbers_1_10_one_file.txt/part-00000
In:!hdfs dfs -cat /tmp/numbers_1_10_one_file.txt/part-00000
Out:0
1
2
3
4
5
6
7
8
9
In:numbers_rdd.saveAsTextFile("file:///tmp/numbers_1_10.txt")
In:!ls /tmp/numbers_1_10.txt
Out:part-00000 part-00001 part-00002 part-00003 _SUCCESS
RDD 只支持两种类型的操作:
- 转换将数据集转换为不同的数据集。变换的输入和输出都是 RDDs 因此,可以将多个转换链接在一起,接近函数式编程。此外,转换是懒惰的,也就是说,它们不会直接计算结果。
- 操作从 RDDs 返回值,例如元素的总和和计数,或者只收集所有元素。当需要输出时,操作是执行(惰性)转换链的触发器。
典型的 Spark 程序是一系列的转换,最后是一个动作。默认情况下,每次运行操作时都会执行 RDD 上的所有转换(即,不保存每个转换后的中间状态)。但是,只要您想要缓存转换后的元素的值,就可以使用persist方法(在 RDD 上)覆盖这种行为。persist方法允许内存和磁盘的持久性。
在下一个例子中,我们将对 RDD 中包含的所有值进行平方,然后对它们求和;这个算法可以通过一个映射器(正方形元素)后跟一个缩减器(对数组求和)来执行。根据 Spark 的说法,map方法是一个转换器,因为它只是逐个元素地转换数据;reduce是一个动作,因为它从所有元素中一起创造价值。
让我们一步一步地解决这个问题,看看我们可以有多种操作方式。首先,从映射开始:我们首先定义一个返回输入参数平方的函数,然后我们将这个函数传递给 RDD 的map方法,最后我们收集 RDD 的元素:
In:
def sq(x):
return x**2
numbers_rdd.map(sq).collect()
Out:[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
虽然输出正确,sq功能占用了大量空间;得益于 Python 的 lambda 表达式,我们可以以这种方式更简洁地重写转换:
In:numbers_rdd.map(lambda x: x**2).collect()
Out:[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
请记住:为什么我们需要打对方付费电话来打印转换后的 RDD 的价值?这是因为map法不会春天来行动,而只会懒洋洋地被评价。而reduce法,则是一个动作;因此,将 reduce 步骤添加到前面的 RDD 应该会输出一个值。对于 map,reduce 将一个函数作为参数,该函数应该有两个参数(左值和右值),并且应该返回值。即使在这种情况下,也可以是用def定义的详细函数或lambda函数:
In:numbers_rdd.map(lambda x: x**2).reduce(lambda a,b: a+b)
Out:285
更简单的是,我们可以使用 sum 动作代替 reducer:
In:numbers_rdd.map(lambda x: x**2).sum()
Out:285
到目前为止,我们已经展示了一个非常简单的 pySpark 示例。想想引擎盖下发生了什么:数据集首先被加载并跨集群分区,然后映射操作在分布式环境中运行,然后所有分区被折叠在一起生成结果(sum 或 reduce),最后打印在 IPython Notebook 上。这是一个巨大的任务,但被 pySpark 变得超级简单。
现在让我们前进一步,介绍键值对;虽然 RDDs 可以包含任何类型的对象(到目前为止,我们已经看到了整数和文本行),但是当元素是由两个元素组成的元组时,可以进行一些操作:键和值。
举个例子,让我们先把 RDD 的数字按赔率和均价分组,然后分别计算两组的总和。至于 MapReduce 模型,最好是用一个键(odd或even)映射每个数字,然后对每个键使用求和操作进行约简。
我们可以从 map 操作开始:让我们首先创建一个标记数字的函数,如果自变量数字为偶数,则输出even,否则输出odd。然后,创建键值映射,为每个数字创建键值对,其中键是标签,值是数字本身:
In:
def tag(x):
return "even" if x%2==0 else "odd"
numbers_rdd.map(lambda x: (tag(x), x) ).collect()
Out:[('even', 0),
('odd', 1),
('even', 2),
('odd', 3),
('even', 4),
('odd', 5),
('even', 6),
('odd', 7),
('even', 8),
('odd', 9)]
为了单独减少每个键,我们现在可以使用reduceByKey方法(这不是 Spark 动作)。作为一个参数,我们应该传递我们应该应用于所有每个键的值的函数;在这种情况下,我们将对它们进行总结。最后,我们应该调用 collect 方法来打印结果:
In:
numbers_rdd.map(lambda x: (tag(x), x) ) \
.reduceByKey(lambda a,b: a+b).collect()
Out:[('even', 20), ('odd', 25)]
现在,让我们列出 Spark 中可用的一些最重要的方法;这不是一个详尽的指南,但只是包括了最常用的。
我们从转变开始;它们可以应用于 RDD,产生 RDD:
-
map(function):返回每个元素通过函数形成的 RDD。 -
flatMap(function): This returns an RDD formed by flattening the output of the function for each element of the input RDD. It's used when each value at the input can be mapped to 0 or more output elements.例如,为了计算每个单词在文本中出现的次数,我们应该将每个单词映射到一个键值对(单词是键,
1是值),以这种方式为文本的每个输入行生成多个键值元素: -
filter(function):返回由函数返回真的所有值组成的数据集。 -
sample(withReplacement, fraction, seed):这启动了 RDD,允许你创建一个采样的 RDD(有或没有替换),其长度是输入长度的一小部分。 -
distinct():这将返回一个包含输入 RDD 的不同元素的 RDD。 -
coalesce(numPartitions):这减少了 RDD 的分区数量。 -
repartition(numPartitions):这改变了 RDD 的分区数量。这种方法总是通过网络打乱所有数据。 -
groupByKey():这创建了一个 RDD,其中,对于每个键,值是输入数据集中具有该键的一系列值。 -
reduceByKey(function):通过按键聚合输入的 RDD,然后将减少功能应用于每组的值。 -
sortByKey(ascending):这将按照升序或降序对 RDD 中的元素进行键排序。 -
union(otherRDD):这将两个 rdd 合并在一起。 -
intersection(otherRDD):这个返回一个 RDD,由输入和参数 RDD 中出现的值组成。 -
join(otherRDD): This returns a dataset where the key-value inputs are joined (on the key) to the argument RDD.类似于 SQL 中的连接函数,也有可用的方法:
cartesian、leftOuterJoin、rightOuterJoin和fullOuterJoin。
现在,让我们概述一下 pySpark 中最受欢迎的动作。注意动作通过链中的所有变压器触发 RDD 的处理:
reduce(function):这聚集了 RDD 的元素,产生一个输出值count():这会返回 RDD 元素的计数countByKey():这将返回一个 Python 字典,其中每个键都与该键在 RDD 中的元素数量相关联collect():这个在本地返回变换后的 RDD 中的所有元素first():这会返回 RDD 的第一个值take(N):这个返回 RDD 的第一个 N 值takeSample(withReplacement, N, seed):这将返回 RDD 的 N 元素的引导,有或没有替换,最终使用提供的随机种子作为参数takeOrdered(N, ordering):在按值(升序或降序)排序之后,这将返回 RDD 中的顶部 N 元素saveAsTextFile(path):这将 RDD 作为一组文本文件保存在指定的目录中
还有一些既不是转换器也不是动作的方法:
cache():这个隐藏了 RDD 的元素;因此,基于同一 RDD 的未来计算可以将此作为起点persist(storage):这与缓存相同,但是您可以指定将 RDD 的元素(内存、磁盘或两者)存储在哪里unpersist():此撤销持久化或缓存操作
现在让我们尝试用 Hadoop 复制我们在关于 MapReduce 一节中看到的例子。使用 Spark,算法应该如下:
- 输入文件在 RDD 上被读取和并行化。这个操作可以用 Spark 上下文提供的
textFile方法来完成。 - 对于输入文件的每一行,返回三个键值对:一个包含字符数,一个包含字数,最后一个包含行数。在 Spark 中,这是一个 flatMap 操作,因为每条输入线生成三个输出。
- 对于每个键,我们总结所有的值。这可以通过
reduceByKey方法来完成。 - 最后,收集结果。在这种情况下,我们可以使用
collectAsMap方法收集 RDD 中的键值对,并返回一个 Python 字典。注意,这是一个动作;因此,执行 RDD 链并返回结果。
In:
def emit_feats(line):
return [("chars", len(line)), \
("words", len(line.split())), \
("lines", 1)]
print (sc.textFile("/datasets/hadoop_git_readme.txt")
.flatMap(emit_feats)
.reduceByKey(lambda a,b: a+b)
.collectAsMap())
Out:{'chars': 1335, 'lines': 31, 'words': 179}
与 MapReduce 实现相比,我们可以立即注意到这种方法的巨大速度。这是因为所有数据集都存储在内存中,而不在 HDFS。其次,这是一个纯 Python 实现,我们不需要调用外部命令行或库——pySpark 是独立的。
现在让我们在包含莎士比亚文本的较大文件上处理这个例子,以提取最流行的单词。在 Hadoop MapReduce 实现中,它需要两个地图缩减步骤,因此在 HDFS 上需要四个写/读步骤。在 pySpark 中,我们可以在 RDD 实现所有这些:
- 使用
textFile方法在 RDD 上读取输入文件并将其并行化。 - 对于每一行,提取所有的单词。对于这个操作,我们可以使用 flatMap 方法和一个正则表达式。
- 文本中的每个单词(也就是 RDD 的每个元素)现在被映射到一个键值对:键是较低的单词,值总是
1。这是地图操作。 - 通过一个
reduceByKey的调用,我们计算每个单词(键)在文本中出现的次数(RDD)。输出是键值对,其中键是单词,值是单词在文本中出现的次数。 - 我们翻转按键和价值观,创造一个新的 RDD。这是一个地图操作。
- 我们按降序对 RDD 进行排序,并提取第一个元素。这是一个动作,可以用
takeOrdered方法一次操作完成。
In:import re
WORD_RE = re.compile(r"[\w']+")
print (sc.textFile("/datasets/shakespeare_all.txt")
.flatMap(lambda line: WORD_RE.findall(line))
.map(lambda word: (word.lower(), 1))
.reduceByKey(lambda a,b: a+b)
.map(lambda (k,v): (v,k))
.takeOrdered(1, key = lambda x: -x[0]))
Out:[(27801, u'the')]
结果与我们使用 Hadoop 和 MapReduce 的结果相同,但在这种情况下,计算花费的时间要少得多。我们实际上可以进一步改进解决方案,将第二步和第三步合并在一起(flat map-为每个单词创建一个键值对,其中键是较低的单词,值是出现的次数),将第五步和第六步合并在一起(获取第一个元素,并根据它们的值对 RDD 中的元素进行排序,即该对中的第二个元素):
In:
print (sc.textFile("/datasets/shakespeare_all.txt")
.flatMap(lambda line: [(word.lower(), 1) for word in WORD_RE.findall(line)])
.reduceByKey(lambda a,b: a+b)
.takeOrdered(1, key = lambda x: -x[1]))
Out:[(u'the', 27801)]
要检查处理的状态,您可以使用 Spark 用户界面:这是一个图形界面,显示由 Spark 逐步运行的作业。要访问用户界面,您应该首先弄清楚 pySpark IPython 应用的名称,在 bash shell 中搜索您启动笔记本的名称(通常是application_<number>_<number>形式),然后将浏览器指向页面:http://localhost:8088/proxy/application_<number>_<number>
结果是类似于下图。它包含所有在 spark 中运行的作业(作为 IPython Notebook 单元格),您还可以将执行计划可视化为有向无环图 ( DAG ):

总结
在本章中,我们介绍了一些能够在由多个节点组成的集群上运行分布式作业的原语。我们已经看到了 Hadoop 框架及其所有组件、特性和限制,然后我们展示了 Spark 框架。
在下一章中,我们将深入挖掘 Spark,展示如何在分布式环境中进行数据科学。***
九、Spark 实用机器学习
在前一章中,我们看到了 Spark 数据处理的主要功能。在这一章中,我们将与 Spark 一起关注一个真实数据问题的数据科学。在本章中,您将学习以下主题:
- 如何在集群的节点间共享变量
- 如何从结构化(CSV)和半结构化(JSON)文件创建数据帧,将其保存在磁盘上并加载
- 如何使用类似 SQL 的语法来选择、过滤、连接、分组和聚合数据集,从而使预处理变得极其容易
- 如何处理数据集中丢失的数据
- Spark 中有哪些现成的算法可用于特征工程,以及如何在实际场景中使用它们
- 哪些学习者可用,以及如何在分布式环境中衡量他们的表现
- 如何在集群中运行超参数优化的交叉验证
为本章设置虚拟机
由于机器学习需要大量的计算能力,为了节省一些资源(尤其是内存),我们将在本章中使用不被 save 支持的 Spark 环境。这种操作模式被命名为独立模式,并创建一个没有集群功能的 Spark 节点;所有的处理都将在驱动程序机器上进行,不会被共享。不用担心;我们将在本章中看到的代码也将在集群环境中工作。
要以这种方式操作,请执行以下步骤:
- 使用
vagrant up命令打开虚拟机。 - 当虚拟机准备就绪时,使用
vagrant ssh访问虚拟机。 - 通过
./start_jupyter.sh从虚拟机内部使用 IPython 笔记本启动 Spark 独立模式。 - 打开指向
http://localhost:8888的浏览器。
要关闭它,使用 Ctrl + C 键退出 IPython 笔记本,vagrant halt关闭虚拟机。
注
请注意,即使在这种配置下,您也可以通过以下网址访问 Spark 用户界面(至少在运行 IPython 笔记本时):
http://localhost:4040
跨集群节点共享变量
当我们在分布式环境中工作时,有时需要跨节点共享信息,以便所有节点都可以使用一致的变量进行操作。Spark 通过提供两种变量来处理这种情况:只读变量和只写变量。通过不再确保共享变量既可读又可写,它也降低了一致性要求,让管理这种情况的艰苦工作落在了开发人员的肩上。通常,解决方案会很快达成,因为 Spark 真的很灵活,适应性很强。
广播只读变量
广播变量是驱动节点共享的变量,也就是我们配置中运行 IPython Notebook 的节点,所有节点都在集群中。它是一个只读变量,因为该变量由一个节点广播,如果另一个节点更改了它,则永远不会读回。
现在让我们看一下它是如何在一个简单的例子中工作的:我们想要对一个只包含性别信息的数据集进行一次热编码。准确地说,虚拟数据集包含的特征可以是男性 M 、女性 F 或未知的 U (如果信息缺失)。具体来说,我们希望所有节点都使用一个定义好的单向编码,如下面的字典中所列:
In:one_hot_encoding = {"M": (1, 0, 0),
"F": (0, 1, 0),
"U": (0, 0, 1)
}
现在让我们试着一步一步来。
最简单的解决方案(尽管它不起作用)是并行化虚拟数据集(或者从磁盘中读取它),然后使用 RDD 上的 map 方法和 lambda 函数将性别映射到它的编码元组:
In:(sc.parallelize(["M", "F", "U", "F", "M", "U"])
.map(lambda x: one_hot_encoding[x])
.collect())
Out:
[(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 0, 1)]
这个解决方案在本地有效,但是不会在真实分布式环境中运行,因为所有节点的工作空间中都没有one_hot_encoding变量。一个快速的解决方法是将 Python 字典包含在映射函数中(这是分布式的),就像我们在这里所做的那样:
In:
def map_ohe(x):
ohe = {"M": (1, 0, 0),
"F": (0, 1, 0),
"U": (0, 0, 1)
}
return ohe[x]
sc.parallelize(["M", "F", "U", "F", "M", "U"]).map(map_ohe).collect()
Out:
[(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 0, 1)] here are you I love you hello hi is that email with all leave formal minutes very worrying A hey
这样的解决方案在本地和服务器上都有效,但不是很好:我们混合了数据和流程,使得映射功能不可重用。如果映射函数引用一个广播变量会更好,这样它就可以与我们需要的任何映射一起使用,对数据集进行一次热编码。
为此,我们首先在映射函数内部广播 Python 字典(调用 Spark 上下文提供的broadcast方法,sc);使用它的.value属性,我们现在可以访问它。这样做之后,我们就有了一个通用的地图函数,可以在任何一个热门地图字典上工作:
In:bcast_map = sc.broadcast(one_hot_encoding)
def bcast_map_ohe(x, shared_ohe):
return shared_ohe[x]
(sc.parallelize(["M", "F", "U", "F", "M", "U"])
.map(lambda x: bcast_map_ohe(x, bcast_map.value))
.collect())
Out:
[(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 0, 1)]
把广播的变量想象成一个写在 HDFS 的文件。然后,当一个通用节点想要访问它时,它只需要 HDFS 路径(作为 map 方法的一个参数传递),并且您确信它们都将使用相同的路径读取相同的内容。当然,Spark 不使用 HDFS,而是它的内存变体。
注
广播变量保存在组成集群的所有节点的内存中;因此,他们从不共享大量可以填充他们的数据,这使得以下处理变得不可能。
要删除广播变量,请对广播变量使用unpersist方法。此操作将释放所有节点上该变量的内存:
In:bcast_map.unpersist()
累加器只写变量
在 Spark 簇中可以共享的其他变量是累加器。累加器是只写变量,可以加在一起,通常用于实现和或计数器。只有运行 IPython 笔记本的驱动程序节点可以读取它的值;所有其他节点都不能。
让我们用一个例子来看看它是如何工作的:我们想处理一个文本文件,并了解在处理它时有多少行是空的。当然,我们可以通过扫描数据集两次(使用两个 Spark 作业)来做到这一点:第一次对空行进行计数,第二次进行真正的处理,但这种解决方案不是很有效。
在第一个无效的解决方案中——使用两个独立的 Spark 作业提取空行的数量——我们可以读取文本文件,过滤空行,并对它们进行计数,如下所示:
In:print "The number of empty lines is:"
(sc.textFile('file:///home/vagrant/datasets/hadoop_git_readme.txt')
.filter(lambda line: len(line) == 0)
.count())
Out:The number of empty lines is:
6
相反,第二种解决方案更有效(也更复杂)。我们实例化一个累加器变量(初始值为0),并为处理输入文件的每一行时找到的每一个空行添加1(用一个映射)。同时,我们可以对每一行做一些处理;例如,在下面这段代码中,我们简单地为每一行返回1,以这种方式计算文件中的所有行。
在处理结束时,我们将有两条信息:第一条是行数,来自对变换后的 RDD 的count()动作的结果,第二条是累加器的value属性中包含的空行数。请记住,在扫描数据集一次后,这两个选项都可用:
In:accum = sc.accumulator(0)
def split_line(line):
if len(line) == 0:
accum.add(1)
return 1
tot_lines = (
sc.textFile('file:///home/vagrant/datasets/hadoop_git_readme.txt')
.map(split_line)
.count())
empty_lines = accum.value
print "In the file there are %d lines" % tot_lines
print "And %d lines are empty" % empty_lines
Out:In the file there are 31 lines
And 6 lines are empty
本质上,Spark 支持数字类型的累加器,默认操作是求和。通过多一点编码,我们可以把它变成更复杂的东西。
广播和累加器在一起——一个例子
虽然广播和累加器是简单且非常有限的变量(一个是只读的,另一个是只写的),但它们可以被主动用于创建非常复杂的操作。例如,让我们尝试在分布式环境中对 Iris 数据集应用不同的机器学习算法。我们将通过以下方式创建一个 Spark 作业:
- 数据集被读取并广播到所有节点(因为它足够小,可以放入内存中)。
- 每个节点将在数据集上使用不同的分类器,并在整个数据集上返回分类器名称及其准确度分数。请注意,在这个简单的例子中,为了让事情变得简单,我们将不做任何预处理、训练/测试分割或超参数优化。
- 如果分类器引发任何异常,错误的字符串表示以及分类器名称应该存储在累加器中。
- 最终的输出应该包含一个分类器的列表,这些分类器执行分类任务时没有错误,并且它们的准确性得分。
作为第一步,我们加载 Iris 数据集,并将其广播给集群中的所有节点:
In:from sklearn.datasets import load_iris
bcast_dataset = sc.broadcast(load_iris())
现在,让我们创建一个自定义累加器。它将包含一个元组列表,以字符串形式存储分类器名称和它经历的异常。自定义累加器是由AccumulatorParam类派生的,应该至少包含两个方法:zero(初始化时调用)和addInPlace(在累加器上调用 add 方法时调用)。
下面的代码显示了最简单的方法,随后将其初始化为一个空列表。请注意,加法运算有点棘手:我们需要组合两个元素,一个元组和一个列表,但我们不知道哪个元素是列表,哪个是元组;因此,我们首先确保这两个元素都是列表,然后我们可以以一种简单的方式(使用+运算符)继续连接它们:
In:from pyspark import AccumulatorParam
class ErrorAccumulator(AccumulatorParam):
def zero(self, initialList):
return initialList
def addInPlace(self, v1, v2):
if not isinstance(v1, list):
v1 = [v1]
if not isinstance(v2, list):
v2 = [v2]
return v1 + v2
errAccum = sc.accumulator([], ErrorAccumulator())
现在,让我们定义映射函数:每个节点应该在广播的 Iris 数据集上训练、测试和评估一个分类器。作为一个参数,该函数将接收分类器对象,并应该返回一个元组,该元组包含列表中包含的分类器名称及其准确度分数。
如果这样做引发了任何异常,分类器名称和异常作为字符串被添加到累加器中,并作为空列表返回:
In:
def apply_classifier(clf, dataset):
clf_name = clf.__class__.__name__
X = dataset.value.data
y = dataset.value.target
try:
from sklearn.metrics import accuracy_score
clf.fit(X, y)
y_pred = clf.predict(X)
acc = accuracy_score(y, y_pred)
return [(clf_name, acc)]
except Exception as e:
errAccum.add((clf_name, str(e)))
return []
终于,我们到达了工作的核心。我们现在从 Scikit-learn 实例化几个对象(为了测试累加器,其中一些不是分类器)。我们将把它们转换成 RDD,并应用我们在前一个单元格中创建的映射函数。由于返回值是一个列表,我们可以使用flatMap只收集没有陷入任何异常的映射器的输出:
In:from sklearn.linear_model import SGDClassifier
from sklearn.dummy import DummyClassifier
from sklearn.decomposition import PCA
from sklearn.manifold import MDS
classifiers = [DummyClassifier('most_frequent'),
SGDClassifier(),
PCA(),
MDS()]
(sc.parallelize(classifiers)
.flatMap(lambda x: apply_classifier(x, bcast_dataset))
.collect())
Out:[('DummyClassifier', 0.33333333333333331),
('SGDClassifier', 0.66666666666666663)]
不出所料,只有真正的 分类器包含在输出中。现在让我们看看哪些分类器产生了错误。不出所料,这里我们发现了前面输出中缺少的两个:
In:print "The errors are:"
errAccum.value
Out:The errors are:
[('PCA', "'PCA' object has no attribute 'predict'"),
('MDS', "Proximity must be 'precomputed' or 'euclidean'. Got euclidean instead")]
最后一步,让我们清理广播数据集:
In:bcast_dataset.unpersist()
请记住,在这个例子中,我们使用了一个可以广播的小数据集。在现实世界的大数据问题中,您需要从 HDFS 加载数据集,广播 HDFS 路径。
Spark 中的数据预处理
到目前为止,我们已经看到了如何从本地文件系统和 HDFS 加载文本数据。文本文件可以包含非结构化数据(如文本文档)或结构化数据(如 CSV 文件)。至于半结构化数据,就像包含 JSON 对象的文件一样,Spark 有特殊的例程能够将文件转换成数据帧,类似于 R 和 Python 熊猫中的数据帧。数据帧非常类似于 RDBMS 表,在 RDBMS 表中设置了模式。
JSON 文件和 Spark 数据帧
为了导入符合 JSON 的文件,我们应该首先创建一个 SQL 上下文,从本地 Spark Context 创建一个SQLContext对象:
In:from pyspark.sql import SQLContext
sqlContext = SQLContext(sc)
现在,让我们看看一个小 JSON 文件的内容(它是在游民虚拟机中提供的)。它是一个有六行三列的表的 JSON 表示,其中缺少一些属性(例如带有user_id=0的用户的gender属性):
In:!cat /home/vagrant/datasets/users.json
Out:{"user_id":0, "balance": 10.0}
{"user_id":1, "gender":"M", "balance": 1.0}
{"user_id":2, "gender":"F", "balance": -0.5}
{"user_id":3, "gender":"F", "balance": 0.0}
{"user_id":4, "balance": 5.0}
{"user_id":5, "gender":"M", "balance": 3.0}
使用sqlContext提供的read.json方法,我们已经有了格式良好的表,并且在一个变量中有所有正确的列名。输出变量被键入为 Spark 数据帧。要在一个漂亮的格式化表格中显示变量,使用它的show方法:
In:
df = sqlContext.read \
.json("file:///home/vagrant/datasets/users.json")
df.show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 10.0| null| 0|
| 1.0| M| 1|
| -0.5| F| 2|
| 0.0| F| 3|
| 5.0| null| 4|
| 3.0| M| 5|
+-------+------+-------+
此外,我们可以使用printSchema方法研究数据帧的模式。我们意识到,在读取 JSON 文件的时候,每一个列类型都已经被数据推断出来了(示例中user_id列包含长整数,性别列由字符串组成,余额为双浮点):
In:df.printSchema()
Out:root
|-- balance: double (nullable = true)
|-- gender: string (nullable = true)
|-- user_id: long (nullable = true)
就像关系数据库管理系统中的一个表一样,我们可以滑动并切割数据框中的数据,选择列并按属性过滤数据。在本例中,我们要打印性别未缺失且余额严格大于零的用户的余额、性别和user_id。为此,我们可以使用filter和select方法:
In:(df.filter(df['gender'] != 'null')
.filter(df['balance'] > 0)
.select(['balance', 'gender', 'user_id'])
.show())
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| 3.0| M| 5|
+-------+------+-------+
我们也可以用类似 SQL 的语言重写前面工作的每一部分。事实上,filter和select方法可以接受 SQL 格式的字符串:
In:(df.filter('gender is not null')
.filter('balance > 0').select("*").show())
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| 3.0| M| 5|
+-------+------+-------+
我们也可以只使用对filter方法的一次调用:
In:df.filter('gender is not null and balance > 0').show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| 3.0| M| 5|
+-------+------+-------+
处理缺失数据
数据预处理的一个常见问题是处理缺失数据。Spark 数据框,类似熊猫数据框,提供了一系列你可以在上面做的操作。例如,让数据集仅由完整的行组成的最简单的选择是丢弃包含缺失信息的行。为此,在 Spark 数据框中,我们首先必须访问数据框的na属性,然后调用drop方法。生成的表将只包含完整的行:
In:df.na.drop().show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| -0.5| F| 2|
| 0.0| F| 3|
| 3.0| M| 5|
+-------+------+-------+
如果这样的操作删除了太多行,我们总是可以决定删除行时应该考虑哪些列(作为drop方法的扩充子集):
In:df.na.drop(subset=["gender"]).show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| -0.5| F| 2|
| 0.0| F| 3|
| 3.0| M| 5|
+-------+------+-------+
此外,如果您想为每列设置默认值而不是删除行数据,可以使用fill方法,传递由列名(作为字典键)和默认值组成的字典来替换该列中缺失的数据(作为字典中键的值)。
举个例子,如果你想确保变量 balance(缺失的地方)设置为0,变量 gender(缺失的地方)设置为U,你可以简单的做以下操作:
In:df.na.fill({'gender': "U", 'balance': 0.0}).show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 10.0| U| 0|
| 1.0| M| 1|
| -0.5| F| 2|
| 0.0| F| 3|
| 5.0| U| 4|
| 3.0| M| 5|
+-------+------+-------+
在内存中分组和创建表格
要将函数应用于一组行(与 SQL GROUP BY的情况完全相同),可以使用两种类似的方法。在下面的例子中,我们想要计算每个性别的平均平衡:
In:(df.na.fill({'gender': "U", 'balance': 0.0})
.groupBy("gender").avg('balance').show())
Out:
+------+------------+
|gender|avg(balance)|
+------+------------+
| F| -0.25|
| M| 2.0|
| U| 7.5|
+------+------------+
到目前为止,我们已经使用了数据框,但是正如您所看到的,数据框方法和 SQL 命令之间的距离很小。实际上,使用 Spark,可以将 DataFrame 注册为一个 SQL 表,以充分享受 SQL 的强大功能。该表保存在内存中,并以类似于 RDD 的方式分发。
要注册该表,我们需要提供一个名称,它将在未来的 SQL 命令中使用。在这种情况下,我们决定将其命名为users:
In:df.registerTempTable("users")
通过调用 Spark sql上下文提供的sql方法,我们可以运行任何符合 SQL 的表:
In:sqlContext.sql("""
SELECT gender, AVG(balance)
FROM users
WHERE gender IS NOT NULL
GROUP BY gender""").show()
Out:
+------+-----+
|gender| _c1|
+------+-----+
| F|-0.25|
| M| 2.0|
+------+-----+
毫不奇怪,命令输出的表(以及users表本身)属于 Spark 数据帧类型:
In:type(sqlContext.table("users"))
Out:pyspark.sql.dataframe.DataFrame
数据框、表和关系数据库紧密相连,RDD 方法可以用在数据框上。请记住,数据框的每一行都是 RDD 元素。让我们详细看看这个,首先收集整个表:
In:sqlContext.table("users").collect()
Out:[Row(balance=10.0, gender=None, user_id=0),
Row(balance=1.0, gender=u'M', user_id=1),
Row(balance=-0.5, gender=u'F', user_id=2),
Row(balance=0.0, gender=u'F', user_id=3),
Row(balance=5.0, gender=None, user_id=4),
Row(balance=3.0, gender=u'M', user_id=5)]
In:
a_row = sqlContext.sql("SELECT * FROM users").first()
a_row
Out:Row(balance=10.0, gender=None, user_id=0)
输出是一个Row对象列表(它们看起来像 Python 的namedtuple)。让我们深入挖掘一下:Row包含多个属性,可以作为属性或字典键访问;也就是说,要从第一行中取出余额,我们可以选择以下两种方式:
In:print a_row['balance']
print a_row.balance
Out:10.0
10.0
还有,Row可以用Row的asDict方法收集成 Python 字典。结果包含作为键的属性名和作为字典值的属性值:
In:a_row.asDict()
Out:{'balance': 10.0, 'gender': None, 'user_id': 0}
将预处理后的数据帧或 RDD 写入磁盘
要将数据帧或 RDD 写入磁盘,我们可以使用写入方法。我们有多种格式可供选择;在这种情况下,我们将把它保存为本地机器上的 JSON 文件:
In:(df.na.drop().write
.save("file:///tmp/complete_users.json", format='json'))
检查本地文件系统上的输出,我们立即看到一些与我们预期不同的东西:这个操作创建了多个文件(part-r-…)。
它们中的每一个都包含一些序列化为 JSON 对象的行,将它们合并在一起将创建全面的输出。由于 Spark 是为处理大型分布式文件而设计的,因此针对该文件调整了写入操作,每个节点写入完整 RDD 的一部分:
In:!ls -als /tmp/complete_users.json
Out:total 28
4 drwxrwxr-x 2 vagrant vagrant 4096 Feb 25 22:54 .
4 drwxrwxrwt 9 root root 4096 Feb 25 22:54 ..
4 -rw-r--r-- 1 vagrant vagrant 83 Feb 25 22:54 part-r-00000-...
4 -rw-rw-r-- 1 vagrant vagrant 12 Feb 25 22:54 .part-r-00000-...
4 -rw-r--r-- 1 vagrant vagrant 82 Feb 25 22:54 part-r-00001-...
4 -rw-rw-r-- 1 vagrant vagrant 12 Feb 25 22:54 .part-r-00001-...
0 -rw-r--r-- 1 vagrant vagrant 0 Feb 25 22:54 _SUCCESS
4 -rw-rw-r-- 1 vagrant vagrant 8 Feb 25 22:54 ._SUCCESS.crc
为了读回它,我们不需要创建一个独立的文件——即使在读操作中有多个片段也可以。一个 JSON 文件也可以在一个 SQL 查询的FROM子句中读取。现在,让我们尝试在不创建中间数据帧的情况下,打印刚刚写在磁盘上的 JSON:
In:sqlContext.sql(
"SELECT * FROM json.`file:///tmp/complete_users.json`").show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| -0.5| F| 2|
| 0.0| F| 3|
| 3.0| M| 5|
+-------+------+-------+
除了 JSON,还有另一种格式在处理结构化大数据集时非常流行:Parquet 格式。拼花是 Hadoop 生态系统中可用的柱状存储格式;它对数据进行压缩和编码,并且可以使用嵌套结构:所有这些特性都使它非常高效。
保存和加载与 JSON 非常相似,即使在这种情况下,该操作也会产生多个写入磁盘的文件:
In:df.na.drop().write.save(
"file:///tmp/complete_users.parquet", format='parquet')
In:!ls -als /tmp/complete_users.parquet/
Out:total 44
4 drwxrwxr-x 2 vagrant vagrant 4096 Feb 25 22:54 .
4 drwxrwxrwt 10 root root 4096 Feb 25 22:54 ..
4 -rw-r--r-- 1 vagrant vagrant 376 Feb 25 22:54 _common_metadata
4 -rw-rw-r-- 1 vagrant vagrant 12 Feb 25 22:54 ._common_metadata..
4 -rw-r--r-- 1 vagrant vagrant 1082 Feb 25 22:54 _metadata
4 -rw-rw-r-- 1 vagrant vagrant 20 Feb 25 22:54 ._metadata.crc
4 -rw-r--r-- 1 vagrant vagrant 750 Feb 25 22:54 part-r-00000-...
4 -rw-rw-r-- 1 vagrant vagrant 16 Feb 25 22:54 .part-r-00000-...
4 -rw-r--r-- 1 vagrant vagrant 746 Feb 25 22:54 part-r-00001-...
4 -rw-rw-r-- 1 vagrant vagrant 16 Feb 25 22:54 .part-r-00001-...
0 -rw-r--r-- 1 vagrant vagrant 0 Feb 25 22:54 _SUCCESS
4 -rw-rw-r-- 1 vagrant vagrant 8 Feb 25 22:54 ._SUCCESS.crc
处理 Spark 数据帧
到目前为止,我们已经描述了如何从 JSON 和 Parquet 文件中加载数据帧,但是没有描述如何从现有的 RDD 文件中创建它们。为此,您只需要为 RDD 中的每个记录创建一个Row对象,并调用 SQL 上下文的createDataFrame方法。最后,您可以将其注册为临时表,以充分利用 SQL 语法的强大功能:
In:from pyspark.sql import Row
rdd_gender = \
sc.parallelize([Row(short_gender="M", long_gender="Male"),
Row(short_gender="F", long_gender="Female")])
(sqlContext.createDataFrame(rdd_gender)
.registerTempTable("gender_maps"))
In:sqlContext.table("gender_maps").show()
Out:
+-----------+------------+
|long_gender|short_gender|
+-----------+------------+
| Male| M|
| Female| F|
+-----------+------------+
注
这也是操作 CSV 文件的首选方式。首先用sc.textFile读取文件;然后使用split方法、Row构造函数和createDataFrame方法,创建最终的数据框。
当内存中有多个数据帧或可以从磁盘加载的数据帧时,您可以加入并使用经典关系数据库管理系统中可用的所有操作。在本例中,我们可以将从 RDD 创建的数据框与我们存储的拼花文件中包含的用户数据集连接起来。结果令人震惊:
In:sqlContext.sql("""
SELECT balance, long_gender, user_id
FROM parquet.`file:///tmp/complete_users.parquet`
JOIN gender_maps ON gender=short_gender""").show()
Out:
+-------+-----------+-------+
|balance|long_gender|user_id|
+-------+-----------+-------+
| 3.0| Male| 5|
| 1.0| Male| 1|
| 0.0| Female| 3|
| -0.5| Female| 2|
+-------+-----------+-------+
在 web UI 中,每个 SQL 查询在 SQL 选项卡下被映射为一个虚拟的有向无环图 ( DAG ) 。这对于跟踪工作进度和理解查询的复杂性非常有用。在执行前面的 JOIN 查询时,您可以清楚地看到两个分支正在进入同一个BroadcastHashJoin块:第一个来自 RDD,第二个来自拼花文件。然后,以下块只是选定列上的一个投影:

由于表在内存中,最后要做的是清理释放用来保存它们的内存。通过调用sqlContext提供的tableNames方法,我们得到了当前内存中所有表的列表。然后,要释放它们,我们可以用dropTempTable用表的名字作为参数。超过这一点,对这些表的任何进一步引用都将返回一个错误:
In:sqlContext.tableNames()
Out:[u'gender_maps', u'users']
In:
for table in sqlContext.tableNames():
sqlContext.dropTempTable(table)
从 Spark 1.3 开始,在进行数据科学运算时,DataFrame 是对数据集进行运算的首选方式。
带 Spark 的机器学习
在这里,我们到达了您工作的主要任务:创建一个模型来预测数据集中缺失的一个或多个属性。为此,我们使用了一些机器学习建模,Spark 可以在这种情况下为我们提供一只大手。
MLlib 是 Spark 机器学习库;虽然它是用 Scala 和 Java 构建的,但它的功能在 Python 中也是可用的。它包含分类、回归和推荐学习器,一些用于降维和特征选择的例程,并且具有许多用于文本处理的功能。它们都能够处理巨大的数据集,并利用集群中所有节点的能力来实现目标。
截至目前(2016 年),它由两个主要包组成:mllib,在 RDDs 上运行,ml,在 DataFrames 上运行。由于后者表现良好,并且是数据科学中最流行的数据表示方式,开发人员选择贡献和改进ml分支,让前者保留下来,但不再进一步开发。乍一看,MLlib 似乎是一个完整的库,但是在开始使用 Spark 之后,您会注意到默认包中既没有统计库,也没有数字库。在这里,SciPy 和 NumPy 来帮助您,它们再次成为数据科学的基础!
在本节中,我们将尝试探索新的 pyspark.ml包的功能;截至目前,与最先进的 Scikit-learn 库相比,它仍处于早期阶段,但它在未来肯定有很大的潜力。
注
Spark 是一个高级的、分布式的、复杂的软件,应该只在大数据上使用,并且有多个节点的集群;事实上,如果数据集可以放在内存中,使用其他库(如 Scikit-learn 或类似的库)会更方便,这些库只关注问题的数据科学方面。在小数据集的单个节点上运行 Spark 可能比 Scikit-learn 等效算法慢五倍。
KDD 99 数据集上的 Spark
让我们使用一个真实的数据集来进行这个探索:KDD99 数据集。竞赛的目标是创建一个网络入侵检测系统,能够识别哪些网络流量是恶意的,哪些不是。而且,数据集中有很多不同的攻击;目标是使用数据集中包含的数据包流的特征来准确预测它们。
作为数据集上的一个边节点,在发布后的最初几年里,为入侵检测系统开发出色的解决方案非常有用。如今,由于这一点,数据集中包含的所有攻击都非常容易检测,因此不再用于入侵检测系统的开发。
例如,的特征是协议(tcp、icmp 和 udp)、服务(http、smtp 等)、数据包的大小、协议中活动的标志、成为根的尝试次数等。
注
更多关于 KDD99 挑战赛和数据集的信息可在http://kdd.ics.uci.edu/databases/kddcup99/kddcup99.html获得。
虽然这是一个经典的多类分类问题,但我们将深入研究它,向您展示如何在 Spark 中执行此任务。为了保持清洁,我们将使用新的 IPython 笔记本。
读取数据集
首先让我们下载并解压数据集。我们将非常保守,只使用原始训练数据集的 10%(75MB,未压缩),因为我们所有的分析都在一个小型虚拟机上运行。如果您想尝试一下,可以取消对以下代码片段中的行的注释,并下载完整的训练数据集(750MB 未压缩)。我们使用 bash 命令下载训练数据集、测试(47MB)和特征名:
In:!rm -rf ../datasets/kdd*
# !wget -q -O ../datasets/kddtrain.gz \
# http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data.gz
!wget -q -O ../datasets/kddtrain.gz \
http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data_10_percent.gz
!wget -q -O ../datasets/kddtest.gz \
http://kdd.ics.uci.edu/databases/kddcup99/corrected.gz
!wget -q -O ../datasets/kddnames \
http://kdd.ics.uci.edu/databases/kddcup99/kddcup.names
!gunzip ../datasets/kdd*gz
现在,打印前几行以了解格式。很明显,这是一个没有标题的经典 CSV,每行末尾都有一个点。此外,我们可以看到一些字段是数字的,但其中一些是文本的,目标变量包含在最后一个字段中:
In:!head -3 ../datasets/kddtrain
Out:
0,tcp,http,SF,181,5450,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0.00,0.00,1.00,0.00,0.00,9,9,1.00,0.00,0.11,0.00,0.00,0.00,0.00,0.00,normal.
0,tcp,http,SF,239,486,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0.00,0.00,1.00,0.00,0.00,19,19,1.00,0.00,0.05,0.00,0.00,0.00,0.00,0.00,normal.
0,tcp,http,SF,235,1337,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0.00,0.00,1.00,0.00,0.00,29,29,1.00,0.00,0.03,0.00,0.00,0.00,0.00,0.00,normal.
要创建带有命名字段的数据框,我们应该首先读取kddnames文件中包含的头。目标字段将简单命名为target。
读取并解析文件后,我们打印出问题的特征数量(记住目标变量不是特征)和它们的前 10 个名称:
In:
with open('../datasets/kddnames', 'r') as fh:
header = [line.split(':')[0]
for line in fh.read().splitlines()][1:]
header.append('target')
print "Num features:", len(header)-1
print "First 10:", header[:10]
Out:Num features: 41
First 10: ['duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot']
现在让我们创建两个独立的关系数据库,一个用于训练数据,另一个用于测试数据:
In:
train_rdd = sc.textFile('file:///home/vagrant/datasets/kddtrain')
test_rdd = sc.textFile('file:///home/vagrant/datasets/kddtest')
现在,我们需要解析每个文件的每一行来创建一个数据帧。首先,我们将 CSV 文件的每一行拆分为单独的字段,然后将每个数值转换为浮点,将每个文本值转换为字符串。最后,我们去掉每行末尾的点。
最后一步,使用sqlContext提供的createDataFrame方法,我们可以为训练和测试数据集创建两个带有命名列的 Spark 数据帧:
In:
def line_parser(line):
def piece_parser(piece):
if "." in piece or piece.isdigit():
return float(piece)
else:
return piece
return [piece_parser(piece) for piece in line[:-1].split(',')]
train_df = sqlContext.createDataFrame(
train_rdd.map(line_parser), header)
test_df = sqlContext.createDataFrame(
test_rdd.map(line_parser), header)
到目前为止,我们只写了 RDD 转换器;让我们引入一个操作,看看我们在数据集中有多少观察值,同时检查前面代码的正确性。
In:print "Train observations:", train_df.count()
print "Test observations:", test_df.count()
Out:Train observations: 494021
Test observations: 311029
尽管我们使用的是完整 KDD99 数据集的十分之一,但我们仍在进行 50 万次观测。乘以特征数 41,我们清楚地看到,我们将在包含 2000 多万个值的观察矩阵上训练我们的分类器。这对于 Spark 来说并不是一个很大的数据集(完整的 KDD99 也不是);世界各地的开发者已经在 petabytes 和十亿记录上使用它。如果数字看起来很大,不要害怕:Spark 旨在应对它们!
现在,让我们看看它在数据框的模式中是什么样子的。具体来说,我们想确定哪些字段是数字,哪些包含字符串(注意,为了简洁起见,结果被截断了):
In:train_df.printSchema()
Out:root
|-- duration: double (nullable = true)
|-- protocol_type: string (nullable = true)
|-- service: string (nullable = true)
|-- flag: string (nullable = true)
|-- src_bytes: double (nullable = true)
|-- dst_bytes: double (nullable = true)
|-- land: double (nullable = true)
|-- wrong_fragment: double (nullable = true)
|-- urgent: double (nullable = true)
|-- hot: double (nullable = true)
...
...
...
|-- target: string (nullable = true)
特征工程
从视觉分析来看,只有四个字段是字符串:protocol_type、service、flag和target(不出所料,这是多类目标标签)。
由于我们将使用基于树的分类器,我们希望将每个级别的文本编码为每个变量的数字。使用 Scikit-learn,这个操作可以通过一个sklearn.preprocessing.LabelEncoder对象来完成。在 Spark 中相当于pyspark.ml.feature套装的StringIndexer。
我们需要用 Spark 编码四个变量;然后我们必须将四个StringIndexer对象级联在一起:它们中的每一个都将对数据帧的特定列进行操作,输出一个带有附加列的数据帧(类似于映射操作)。映射是自动的,按频率排序:Spark 对所选列中每个级别的计数进行排序,将最受欢迎的级别映射为 0,下一个级别映射为 1,依此类推。请注意,通过此操作,您将遍历数据集一次,以统计每个级别的出现次数;如果您已经知道映射,那么广播它并使用map操作会更有效,如本章开头所示。
类似地,我们可以使用一个热编码器来生成一个数值观察矩阵。在单热编码器的情况下,我们会在数据帧中有多个输出列,每个分类特征的每个级别有一个输出列。为此,Spark 提供了pyspark.ml.feature.OneHotEncoder课程。
注
更一般地说,包含在pyspark.ml.feature包中的所有类都用于从数据帧中提取、转换和选择特征。所有的在数据框中读取一些列,创建一些其他列。
从 Spark 1.6 开始,Python 中可用的特性操作包含在以下详尽的列表中(所有这些都可以在pyspark.ml.feature包中找到)。名称应该是直观的,除了其中的几个,将在内联或稍后的文本中解释:
- 对于文本输入(理想情况下):
- 阿富汗国防军和以色列国防军
- 标记器及其基于正则表达式的实现
- Word2vec
- 停止词移除器
- Ngram
- 对于分类特征:
- 字符串索引和它的逆编码器,索引字符串
- OneHotEncoder
- 矢量分度器(现成的分类到数字分度器)
- 对于其他输入:
- 二值化器
- 污染控制局(Pollution Control Agency)
- 多项式展开
- 规格化器、标准缩放器和最小最大缩放器
- 桶化器(桶化特征的值)
- elemontwiseproduct _ 将列相乘)
- 通用:
- SQLTransformer(实现由 SQL 语句定义的转换,将 DataFrame 称为名为
__THIS__的表) - 公式(使用 R 型语法选择列)
- 向量装配器(从多列创建特征向量)
- SQLTransformer(实现由 SQL 语句定义的转换,将 DataFrame 称为名为
回到例子,我们现在想把每个分类变量中的级别编码为离散的数字。正如我们已经解释过的,为此,我们将为每个变量使用一个StringIndexer对象。此外,我们可以使用一个 ML 管道,并将它们设置为它的阶段。
然后,为了适合所有的索引器,你只需要调用管道的fit方法。在内部,它将按顺序适合所有分段对象。当它完成拟合操作时,会创建一个新对象,我们可以将其称为拟合管道。调用这个新对象的transform方法将依次调用所有的分段元素(已经拟合),每个都在前一个完成之后。在这段代码中,您将看到管道正在运行。请注意,变压器构成了管道。因此,由于不存在任何操作,因此实际上不会执行任何操作。在输出数据框中,您会注意到四个额外的列,其名称与原始分类列相同,但带有_cat后缀:
In:from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer
cols_categorical = ["protocol_type", "service", "flag","target"]
preproc_stages = []
for col in cols_categorical:
out_col = col + "_cat"
preproc_stages.append(
StringIndexer(
inputCol=col, outputCol=out_col, handleInvalid="skip"))
pipeline = Pipeline(stages=preproc_stages)
indexer = pipeline.fit(train_df)
train_num_df = indexer.transform(train_df)
test_num_df = indexer.transform(test_df)
让我们进一步研究管道。在这里,我们将看到管道中的阶段:不合适的管道和已安装的管道。请注意,Spark 和 Scikit-learn 有很大的区别:在 Scikit-learn 中,fit和transform是在同一个对象上调用的,而在 Spark 中,fit方法会产生一个新的对象(通常,它的名称会添加一个Model后缀,就像对于Pipeline和PipelineModel一样),在这里您可以调用transform方法。这种差异源于闭包——一个合适的对象很容易在进程和集群之间分布:
In:print pipeline.getStages()
print
print pipeline
print indexer
Out:
[StringIndexer_432c8aca691aaee949b8, StringIndexer_4f10bbcde2452dd1b771, StringIndexer_4aad99dc0a3ff831bea6, StringIndexer_4b369fea07873fc9c2a3]
Pipeline_48df9eed31c543ba5eba
PipelineModel_46b09251d9e4b117dc8d
让我们看看第一个观察,也就是 CSV 文件中的第一行,经过管道后是如何变化的。请注意,我们在这里使用了一个操作,因此管道和管道模型中的所有阶段都会被执行:
In:print "First observation, after the 4 StringIndexers:\n"
print train_num_df.first()
Out:First observation, after the 4 StringIndexers:
Row(duration=0.0, protocol_type=u'tcp', service=u'http', flag=u'SF', src_bytes=181.0, dst_bytes=5450.0, land=0.0, wrong_fragment=0.0, urgent=0.0, hot=0.0, num_failed_logins=0.0, logged_in=1.0, num_compromised=0.0, root_shell=0.0, su_attempted=0.0, num_root=0.0, num_file_creations=0.0, num_shells=0.0, num_access_files=0.0, num_outbound_cmds=0.0, is_host_login=0.0, is_guest_login=0.0, count=8.0, srv_count=8.0, serror_rate=0.0, srv_serror_rate=0.0, rerror_rate=0.0, srv_rerror_rate=0.0, same_srv_rate=1.0, diff_srv_rate=0.0, srv_diff_host_rate=0.0, dst_host_count=9.0, dst_host_srv_count=9.0, dst_host_same_srv_rate=1.0, dst_host_diff_srv_rate=0.0, dst_host_same_src_port_rate=0.11, dst_host_srv_diff_host_rate=0.0, dst_host_serror_rate=0.0, dst_host_srv_serror_rate=0.0, dst_host_rerror_rate=0.0, dst_host_srv_rerror_rate=0.0, target=u'normal', protocol_type_cat=1.0, service_cat=2.0, flag_cat=0.0, target_cat=2.0)
生成的数据框架看起来非常完整,也很容易理解:所有变量都有名称和值。我们立即注意到分类特征仍然存在,例如,我们有protocol_type(分类)和protocol_type_cat(从分类映射而来的变量的数字版本)。
从数据框中提取一些列就像在一个 SQL 查询中使用SELECT一样简单。现在让我们为所有的数字特征建立一个名称列表:从标题中找到的名称开始,我们移除分类的,并用数字派生的来替换它们。最后,由于我们只需要特征,我们移除了目标变量及其数值导出的等价物:
In:features_header = set(header) \
- set(cols_categorical) \
| set([c + "_cat" for c in cols_categorical]) \
- set(["target", "target_cat"])
features_header = list(features_header)
print features_header
print "Total numerical features:", len(features_header)
Out:['num_access_files', 'src_bytes', 'srv_count', 'num_outbound_cmds', 'rerror_rate', 'urgent', 'protocol_type_cat', 'dst_host_same_srv_rate', 'duration', 'dst_host_diff_srv_rate', 'srv_serror_rate', 'is_host_login', 'wrong_fragment', 'serror_rate', 'num_compromised', 'is_guest_login', 'dst_host_rerror_rate', 'dst_host_srv_serror_rate', 'hot', 'dst_host_srv_count', 'logged_in', 'srv_rerror_rate', 'dst_host_srv_diff_host_rate', 'srv_diff_host_rate', 'dst_host_same_src_port_rate', 'root_shell', 'service_cat', 'su_attempted', 'dst_host_count', 'num_file_creations', 'flag_cat', 'count', 'land', 'same_srv_rate', 'dst_bytes', 'num_shells', 'dst_host_srv_rerror_rate', 'num_root', 'diff_srv_rate', 'num_failed_logins', 'dst_host_serror_rate']
Total numerical features: 41
这里VectorAssembler类来帮助我们构建特征矩阵。我们只需要传递要作为参数选择的列和要在数据框中创建的新列。我们决定将输出列简单命名为features。我们将这种转换应用于训练集和测试集,然后我们只选择我们感兴趣的两个列— features和target_cat:
In:from pyspark.ml.feature import VectorAssembler
assembler = VectorAssembler(
inputCols=features_header,
outputCol="features")
Xy_train = (assembler
.transform(train_num_df)
.select("features", "target_cat"))
Xy_test = (assembler
.transform(test_num_df)
.select("features", "target_cat"))
此外,VectorAssembler的默认行为是产生DenseVectors或SparseVectors。在这种情况下,由于features的向量包含许多零,所以它返回一个稀疏向量。要查看输出内容,我们可以打印第一行。请注意,这是一个动作。因此,作业会在打印结果之前执行:
In:Xy_train.first()
Out:Row(features=SparseVector(41, {1: 181.0, 2: 8.0, 6: 1.0, 7: 1.0, 20: 9.0, 21: 1.0, 25: 0.11, 27: 2.0, 29: 9.0, 31: 8.0, 33: 1.0, 39: 5450.0}), target_cat=2.0)
训练学习者
最后,我们到达了任务的热点部分:训练一个分类器。分类器包含在pyspark.ml.classification包中,对于这个例子,我们使用的是随机森林。
从 Spark 1.6 开始,使用 Python 接口的分类器的广泛列表如下:
- 分类(第
pyspark.ml.classification包):LogisticRegressionDecisionTreeClassifierGBTClassifier(基于决策树分类的梯度增强实现)RandomForestClassifierNaiveBayesMultilayerPerceptronClassifier
请注意,不是所有的都能够处理多类问题,并且可能有不同的参数;请务必查看与使用版本相关的文档。除了分类器,Spark 1.6 中使用 Python 接口实现的其他学习者如下:
- 聚类(即
pyspark.ml.clustering包):- KMeans
- 回归(第
pyspark.ml.regression包):- 加速失效时间存活回归
- 决策树回归器
- 基于回归树的梯度增强回归实现
- 等渗
- 线性回归
- 随机森林回归器
- 推荐人(即
pyspark.ml.recommendation套餐):- 基于交替最小二乘法的协同过滤推荐系统
让我们回到 KDD99 挑战赛的目标。现在是实例化一个随机森林分类器并设置其参数的时候了。要设置的参数有featuresCol(包含特征矩阵的列)、labelCol(包含目标标签的数据框的列)、种子(使实验可复制的随机种子)和maxBins(树的每个节点中用于分裂点的最大箱数)。森林中树木数量的默认值为 20,每棵树最大深度为五级。此外,默认情况下,该分类器在数据框中创建三个输出列:rawPrediction(存储每个可能标签的预测分数)、probability(存储每个标签的可能性)和prediction(最可能标签):
In:from pyspark.ml.classification import RandomForestClassifier
clf = RandomForestClassifier(
labelCol="target_cat", featuresCol="features",
maxBins=100, seed=101)
fit_clf = clf.fit(Xy_train)
即使在这种情况下,训练的分类器也是不同的对象。与之前完全一样,训练好的分类器与带Model后缀的分类器命名相同:
In:print clf
print fit_clf
Out:RandomForestClassifier_4797b2324bc30e97fe01
RandomForestClassificationModel (uid=rfc_44b551671c42) with 20 trees
在训练好的分类器对象上,即RandomForestClassificationModel,可以调用transform方法。现在我们预测训练数据集和测试数据集上的标签,并打印测试数据集的第一行;正如分类器中设置的那样,预测将出现在名为prediction的栏中:
In:Xy_pred_train = fit_clf.transform(Xy_train)
Xy_pred_test = fit_clf.transform(Xy_test)
In:print "First observation after classification stage:"
print Xy_pred_test.first()
Out:First observation after classification stage:
Row(features=SparseVector(41, {1: 105.0, 2: 1.0, 6: 2.0, 7: 1.0, 20: 254.0, 27: 1.0, 29: 255.0, 31: 1.0, 33: 1.0, 35: 0.01, 39: 146.0}), target_cat=2.0, rawPrediction=DenseVector([0.0109, 0.0224, 19.7655, 0.0123, 0.0099, 0.0157, 0.0035, 0.0841, 0.05, 0.0026, 0.007, 0.0052, 0.002, 0.0005, 0.0021, 0.0007, 0.0013, 0.001, 0.0007, 0.0006, 0.0011, 0.0004, 0.0005]), probability=DenseVector([0.0005, 0.0011, 0.9883, 0.0006, 0.0005, 0.0008, 0.0002, 0.0042, 0.0025, 0.0001, 0.0004, 0.0003, 0.0001, 0.0, 0.0001, 0.0, 0.0001, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.0]), prediction=2.0)
评估学习者的表现
任何数据科学任务的下一步是检查学习者在训练和测试集上的表现。对于这个任务,我们将使用 F1 分数,因为它是一个很好的度量标准,结合了精度和召回性能。
评估指标包含在pyspark.ml.evaluation包中;在为数不多的选择中,我们使用一个来评估多类分类器:MulticlassClassificationEvaluator。作为参数,我们提供了度量(精确度、召回率、准确度、f1 分数等)以及包含真实标签和预测标签的列的名称:
In:
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
evaluator = MulticlassClassificationEvaluator(
labelCol="target_cat", predictionCol="prediction",
metricName="f1")
print "F1-score train set:", evaluator.evaluate(Xy_pred_train)
print "F1-score test set:", evaluator.evaluate(Xy_pred_test)
Out:F1-score train set: 0.992356962712
F1-score test set: 0.967512379842
获得的值非常高,并且在训练集和测试集上的性能有很大的差异。
除了多类分类器的评估器之外,回归器的评估器对象(其中度量可以是 MSE、RMSE、R2 或 MAE)和二进制分类器在同一个包中可用。
ML 管道的功率
到目前为止,我们已经一片一片地建立并显示了输出。也可以将所有操作级联,并将其设置为管道的阶段。事实上,我们可以在一个独立的管道中将到目前为止看到的东西(四个标签编码器、向量生成器和分类器)链接在一起,将其放在训练数据集上,最后在测试数据集上使用它来获得预测。
这种操作方式更有效,但你会失去逐步分析的探索力。作为数据科学家的读者被建议,只有当他们完全确定内部发生了什么,并且只构建生产模型时,才使用端到端管道。
为了显示管道相当于我们到目前为止所看到的,我们计算测试集上的 F1 分数并打印出来。不出所料,它的价值完全相同:
In:full_stages = preproc_stages + [assembler, clf]
full_pipeline = Pipeline(stages=full_stages)
full_model = full_pipeline.fit(train_df)
predictions = full_model.transform(test_df)
print "F1-score test set:", evaluator.evaluate(predictions)
Out:F1-score test set: 0.967512379842
在运行 IPython Notebook 的驱动程序节点上,我们也可以使用matplotlib库来可视化我们的分析结果。例如,为了显示分类结果的归一化混淆矩阵(由每个类的支持归一化),我们可以创建以下函数:
In:import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
def plot_confusion_matrix(cm):
cm_normalized = \
cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plt.imshow(
cm_normalized, interpolation='nearest', cmap=plt.cm.Blues)
plt.title('Normalized Confusion matrix')
plt.colorbar()
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
Spark 能够建立混淆矩阵,但是那个方法在pyspark.mllib包里。为了能够使用这个包中的方法,我们必须使用.rdd方法将数据帧转换成 RDD:
In:from pyspark.mllib.evaluation import MulticlassMetrics
metrics = MulticlassMetrics(
predictions.select("prediction", "target_cat").rdd)
conf_matrix = metrics.confusionMatrix()tArray()
plot_confusion_matrix(conf_matrix)
Out:

手动调谐
虽然 F1 得分接近 0.97,但归一化混淆矩阵显示类别严重不平衡,分类器刚刚学会如何正确分类最受欢迎的类别。为了提高结果,我们可以对每个类重新采样,试图更好地平衡训练数据集。
首先,让我们计算每个类的训练数据集中有多少个案例:
In:
train_composition = train_df.groupBy("target").count().rdd.collectAsMap()
train_composition
Out:
{u'back': 2203,
u'buffer_overflow': 30,
u'ftp_write': 8,
u'guess_passwd': 53,
u'neptune': 107201,
u'nmap': 231,
u'normal': 97278,
u'perl': 3,
...
...
u'warezmaster': 20}
这是强烈失衡的明显证据。我们可以尝试通过过采样稀有的类和过二次采样流行的类来提高性能。
在这个例子中,我们将创建一个训练数据集,其中每个类至少被表示 1000 次,但最多 25000 次。为此,让我们首先创建二次采样/过采样率,并在整个集群中广播,然后对训练数据集的每一行进行 flatMap,以对其进行正确的重新采样:
In:
def set_sample_rate_between_vals(cnt, the_min, the_max):
if the_min <= cnt <= the_max:
# no sampling
return 1
elif cnt < the_min:
# Oversampling: return many times the same observation
return the_min/float(cnt)
else:
# Subsampling: sometime don't return it
return the_max/float(cnt)
sample_rates = {k:set_sample_rate_between_vals(v, 1000, 25000)
for k,v in train_composition.iteritems()}
sample_rates
Out:{u'back': 1,
u'buffer_overflow': 33.333333333333336,
u'ftp_write': 125.0,
u'guess_passwd': 18.867924528301888,
u'neptune': 0.23320677978750198,
u'nmap': 4.329004329004329,
u'normal': 0.2569954152017928,
u'perl': 333.3333333333333,
...
...
u'warezmaster': 50.0}
In:bc_sample_rates = sc.broadcast(sample_rates)
def map_and_sample(el, rates):
rate = rates.value[el['target']]
if rate > 1:
return [el]*int(rate)
else:
import random
return [el] if random.random() < rate else []
sampled_train_df = (train_df
.flatMap(
lambda x: map_and_sample(x, bc_sample_rates))
.toDF()
.cache())
sampled_train_df数据框变量中重新采样的数据集也被缓存;我们将在超参数优化步骤中多次使用它。它应该很容易放入内存,因为行数比原来少:
In:sampled_train_df.count()
Out:97335
为了了解里面有什么,我们可以打印第一行。很快就能打印出价值,不是吗?当然是缓存了!
In:sampled_train_df.first()
Out:Row(duration=0.0, protocol_type=u'tcp', service=u'http', flag=u'SF', src_bytes=217.0, dst_bytes=2032.0, land=0.0, wrong_fragment=0.0, urgent=0.0, hot=0.0, num_failed_logins=0.0, logged_in=1.0, num_compromised=0.0, root_shell=0.0, su_attempted=0.0, num_root=0.0, num_file_creations=0.0, num_shells=0.0, num_access_files=0.0, num_outbound_cmds=0.0, is_host_login=0.0, is_guest_login=0.0, count=6.0, srv_count=6.0, serror_rate=0.0, srv_serror_rate=0.0, rerror_rate=0.0, srv_rerror_rate=0.0, same_srv_rate=1.0, diff_srv_rate=0.0, srv_diff_host_rate=0.0, dst_host_count=49.0, dst_host_srv_count=49.0, dst_host_same_srv_rate=1.0, dst_host_diff_srv_rate=0.0, dst_host_same_src_port_rate=0.02, dst_host_srv_diff_host_rate=0.0, dst_host_serror_rate=0.0, dst_host_srv_serror_rate=0.0, dst_host_rerror_rate=0.0, dst_host_srv_rerror_rate=0.0, target=u'normal')
现在,让我们使用我们创建的管道进行一些预测,并打印这个新解决方案的 F1 分数:
In:full_model = full_pipeline.fit(sampled_train_df)
predictions = full_model.transform(test_df)
print "F1-score test set:", evaluator.evaluate(predictions)
Out:F1-score test set: 0.967413322985
在 50 棵树的分类器上测试。为此,我们可以构建另一个管道(命名为refined_pipeline)并用新的分类器替换最后一个阶段。即使训练集被大幅缩减,表演看起来也是一样的:
In:clf = RandomForestClassifier(
numTrees=50, maxBins=100, seed=101,
labelCol="target_cat", featuresCol="features")
stages = full_pipeline.getStages()[:-1]
stages.append(clf)
refined_pipeline = Pipeline(stages=stages)
refined_model = refined_pipeline.fit(sampled_train_df)
predictions = refined_model.transform(test_df)
print "F1-score test set:", evaluator.evaluate(predictions)
Out:F1-score test set: 0.969943901769
交叉验证
我们可以继续进行手动优化,在穷尽尝试了许多不同的配置后,找到合适的型号。这样做,会导致大量的时间浪费(以及代码的可重用性),并且会过度填充测试数据集。相反,交叉验证是运行超参数优化的正确方法。现在让我们看看 Spark 是如何执行这一关键任务的。
首先,由于训练将被多次使用,我们可以缓存它。因此,让我们在所有转换之后缓存它:
In:pipeline_to_clf = Pipeline(
stages=preproc_stages + [assembler]).fit(sampled_train_df)
train = pipeline_to_clf.transform(sampled_train_df).cache()
test = pipeline_to_clf.transform(test_df)
带有交叉验证的超参数优化的有用类包含在pyspark.ml.tuning包中。两个要素是必不可少的:参数的网格图(可以用ParamGridBuilder构建)和实际的交叉验证程序(由CrossValidator类运行)。
在这个例子中,我们想要设置一些在交叉验证过程中不会改变的分类器参数。与 Scikit-learn 完全一样,它们是在创建分类对象时设置的(在本例中,是列名、种子和最大箱数)。
然后,感谢网格构建器,我们决定了交叉验证算法的每次迭代应该改变哪些参数。在示例中,我们希望检查分类性能,将林中每棵树的最大深度从 3 更改为 12(递增 3),并将林中的树数更改为 20 或 50。
最后,在设置了网格图、我们要测试的分类器和折叠数之后,我们启动交叉验证(使用fit方法)。参数评估器是必不可少的:它会告诉我们在交叉验证后保留哪个模型最好。请注意,该操作可能需要 15-20 分钟才能运行(在引擎盖下, 423=24 车型经过培训和测试):
In:
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
rf = RandomForestClassifier(
cacheNodeIds=True, seed=101, labelCol="target_cat",
featuresCol="features", maxBins=100)
grid = (ParamGridBuilder()
.addGrid(rf.maxDepth, [3, 6, 9, 12])
.addGrid(rf.numTrees, [20, 50])
.build())
cv = CrossValidator(
estimator=rf, estimatorParamMaps=grid,
evaluator=evaluator, numFolds=3)
cvModel = cv.fit(train)
最后,我们可以使用交叉验证的模型来预测标签,就像我们自己使用管道或分类器一样。在这种情况下,通过交叉验证选择的分类器的性能比前一种情况稍好,并允许我们突破 0.97 的障碍:
In:predictions = cvModel.transform(test)
print "F1-score test set:", evaluator.evaluate(predictions)
Out:F1-score test set: 0.97058134007
此外,通过绘制标准化混淆矩阵,您立即意识到该解决方案能够发现更广泛的攻击,甚至是不太流行的攻击:
In:metrics = MulticlassMetrics(predictions.select(
"prediction", "target_cat").rdd)
conf_matrix = metrics.confusionMa().toArray()
plot_confusion_matrix(conf_matrix)
Out:

最终清理
在这里,我们处于分类任务的末尾。请记住从缓存中删除您使用的所有变量和您创建的临时表:
In:bc_sample_rates.unpersist()
sampled_train_df.unpersist()
train.unpersist()
Spark 记忆清除后,我们可以关闭笔记本。
总结
这是这本书的最后一章。我们已经看到了如何在一群机器上进行大规模的数据科学。Spark 能够通过一个简单的界面,使用集群中的所有节点来训练和测试机器学习算法,非常类似于 Scikit-learn。事实证明,这种解决方案能够处理数十亿字节的信息,为观察子采样和在线学习创造了一个有效的替代方案。
要成为 Spark 和流处理方面的专家,我们强烈建议您阅读本书,掌握 Apache Spark ,迈克·弗兰普顿, Packt Publishing 。
如果你有足够的勇气去切换到 Spark 的主要编程语言 Scala,这本书就是这样一个过渡的最佳选择: Scala for Data Science,Pascal Bugnion,Packt Publishing 。
十、附录 a:图形处理器和硬件介绍
到目前为止,我们使用常规的中央处理器执行神经网络和深度学习任务。然而最近,图形处理器的计算优势变得广泛。本章深入探讨了图形处理器的基础知识以及深度学习的人工智能框架。
GPU 计算
当我们使用常规的 CPU 计算包进行机器学习时,例如 Scikit-learn,并行化的数量惊人地有限,因为默认情况下,即使有多个可用的内核,一个算法也只利用一个内核。在关于分类回归树 ( CART )的章节中,我们会看到一些加速 Scikit-learn 算法的高级例子。
与中央处理器不同,图形处理器单元被设计成从头开始并行工作。想象通过图形卡在屏幕上投影图像;GPU 单元必须能够同时处理和投影大量信息(运动、颜色和空间性),这并不奇怪。另一方面,CPU 是为顺序处理设计的,适合需要更多控制的任务,如分支和检查。与中央处理器相反,图形处理器由许多可以同时处理数千个任务的内核组成。GPU 可以以更低的成本胜过 CPU 100 倍。另一个优势是,与最先进的中央处理器相比,现代图形处理器相对便宜。
所以这一切听起来很棒,但是请记住,图形处理器只擅长执行特定类型的任务。中央处理器由几个为顺序串行处理而优化的内核组成,而图形处理器由数千个更小、更高效的内核组成,旨在同时处理任务。
中央处理器和图形处理器有不同的体系结构,使它们更适合不同的任务。仍然有很多任务,如检查、调试和切换,由于其体系结构,GPU 无法有效地完成。
了解中央处理器和图形处理器之间区别的一个简单方法是比较它们处理任务的方式。经常做的一个类比是分析和顺序左脑和整体右脑。这只是一个类比,不应该太认真。

在以下链接中查看更多信息:
- http://www . NVIDIA . com/object/what-is-GPU-computing . html # th ash . c4r 7 ej3s . dpuf
- http://www . NVIDIA . com/object/what-is-GPU-computing . html # th ash . c4r 7 ej3s . dpuf
为了利用图形处理器进行机器学习,需要一个特定的平台。遗憾的是,到目前为止,除了 CUDA,还没有稳定的 GPU 计算平台;这意味着您必须在计算机上安装 NVIDIA 图形卡。没有 NVIDIA 卡,图形处理器计算将无法工作。是的,我知道这对大多数苹果用户来说是个坏消息。我真的希望它是不同的,但这是我们必须忍受的限制。还有其他一些项目,比如 OpenCL,通过BLAS(https://github.com/clMathLibraries/clBLAS等举措,为其他 GPU 品牌提供 GPU 计算,但这些项目都在繁重的开发中,并且没有针对 Python 中的深度学习应用进行完全优化。OpenCL 的另一个限制是只有 AMD 积极参与,这样才会对 AMD GPUs 有利。在接下来的几年(甚至十年)里,不依赖硬件的机器学习 GPU 应用是没有希望的!).不过,看看 OpenCL 项目(https://www.khronos.org/opencl/的新闻和动态。考虑到媒体的广泛关注,这种对图形处理器可访问性的限制可能并不令人满意。似乎只有 NVIDIA 将他们的研究工作放在了开发 GPU 平台上,在未来几年内,该领域极不可能出现任何新的重大发展。
对于 CUDA 的使用,您将需要以下东西。
你需要测试你电脑上的显卡是否适合 CUDA。它至少应该是一个 NVIDIA 卡。您可以在终端中用这一行代码测试您的 GPU 是否适用于 CUDA:
$ su
现在在根目录下键入您的密码:
$ lspci | grep -i nvidia
如果你有一个基于 NVIDIA 的 GNU,你可以下载 NVIDIA CUDA 工具包(http://developer.nvidia.com/cuda-downloads)。
在撰写之时,NVIDIA 即将发布 CUDA 8 版本,会有不同的安装程序,建议大家按照 CUDA 网站上的说明进行操作。有关进一步的安装程序,请参考英伟达网站:
http://docs . NVIDIA . com/cuda/cuda-入门-linux 指南/#axzz3xBimv9ou
图形处理器上的并行计算
antao 是最初由蒙特利尔大学的詹姆斯·伯格斯特拉开发的 Python 库。它旨在为用符号表示法编写数学函数提供更具表现力的方法(F. Bastien,P. Lamblin,R. Pascanu,J. Bergstra,I. Goodfellow,A. Bergeron,N. Bouchard,d . ward-Farley 和 Y. Bengio。安诺:新功能和速度提升。NIPS 2012 深度学习研讨会)。有趣的是,安蒂诺是以希腊数学家 T2 的名字命名的,他可能是毕达哥拉斯的妻子。它的最大优点是快速的 c 编译计算、符号表达式和图形处理器计算,并且正在积极开发中。通过新功能定期进行改进。antao 的实现比可扩展的机器学习要广泛得多,所以我将缩小范围并使用 antao 进行深度学习。更多信息请访问安诺网站—http://deeplearning.net/software/theano/。
当我们想要在多维矩阵上执行更复杂的计算时,基本的 NumPy 将求助于昂贵的循环和迭代,如前所述,这会增加 CPU 负载。安诺的目标是通过将这些计算编译成高度优化的 C 代码,并在可能的情况下利用图形处理器来优化这些计算。对于神经网络和深度学习,人工神经网络具有自动区分数学函数的有用能力,这对于使用反向传播等算法计算偏导数非常方便。
目前,该平台已应用于各种深度学习项目,并成为该领域使用最多的平台。最近,为了让我们更容易地利用深度学习功能,新的软件包已经被建立在 antio 之上。考虑到茶氨酸陡峭的学习曲线,我们将使用基于茶氨酸构建的包,如茶氨酸、派尔恩 2 和千层面。
安装天线
首先,确保你从安诺页面安装了开发版本。请注意,如果您执行“$ pip install an ao”,您可能会遇到问题。直接从 GitHub 安装开发版本更安全:
$ git clone git://github.com/Theano/Theano.git
$ pip install Theano
如果要升级音频,可以使用以下命令:
$ sudo pip install --upgrade theano
如果您有疑问想联系安诺社区,可以参考https://groups.google.com/forum/#!forum/theano-users。
就这样,我们准备出发了!
为了确保我们将目录路径设置为“无”文件夹,我们需要执行以下操作:
#!/usr/bin/python
import cPickle as pickle
from six.moves import cPickle as pickle
import os
#set your path to the theano folder here
path = '/Users/Quandbee1/Desktop/pthw/Theano/'
让我们安装所有需要的软件包:
from theano import tensor
import theano.tensor as T
import theano.tensor.nnet as nnet
import numpy as np
import numpy
为了让 antio 在 GPU 上工作(如果你安装了 NVIDIA 卡+ CUDA),我们需要先配置 antio 框架。
通常情况下,NumPy 和 antao 使用双精度浮点格式(float64)。然而,如果我们想利用图形处理器来实现运算,就要使用 32 位浮点。这意味着我们必须根据需要在 32 位和 64 位浮点之间更改设置。如果您想查看系统默认使用的配置,请键入以下内容:
print(theano.config.floatX)
output: float64
您可以将 GPU 计算的配置更改为 32 位,如下所示:
theano.config.floatX = 'float32'
有时通过终端更改设置更为实际。
对于 32 位浮点,键入如下内容:
$ export THEANO_FLAGS=floatX=float32
对于 64 位浮点,键入如下内容:
$ export THEANO_FLAGS=floatX=float64
如果您希望将某个设置附加到特定的 Python 脚本,您可以这样做:
$ THEANO_FLAGS=floatX=float32 python you_name_here.py
如果您想查看您的系统正在使用哪种计算方法,请键入以下内容:
print(theano.config.device)
如果要更改特定脚本的所有设置,包括位浮点和计算方法(GPU 或 CPU),请键入以下内容:
$ THEANO_FLAGS=device=gpu,floatX=float32 python your_script.py
这对于测试和编码来说非常方便。你可能不想一直使用图形处理器;有时最好使用 CPU 进行原型制作和草图绘制,并在脚本准备就绪后在 GPU 上运行。
首先,让我们测试一下 GPU 是否适合您的设置。如果您的计算机上没有 NVIDIA GPU 卡,您可以跳过此步骤:
from theano import function, config, shared, sandbox
import theano.tensor as T
import numpy
import time
vlen = 10 * 30 * 768 # 10 x #cores x # threads per core
iters = 1000
rng = numpy.random.RandomState(22)
x = shared(numpy.asarray(rng.rand(vlen), config.floatX))
f = function([], T.exp(x))
print(f.maker.fgraph.toposort())
t0 = time.time()
for i in xrange(iters):
r = f()
t1 = time.time()
print("Looping %d times took %f seconds" % (iters, t1 - t0))
print("Result is %s" % (r,))
if numpy.any([isinstance(x.op, T.Elemwise) for x in f.maker.fgraph.toposort()]):
print('Used the cpu')
else:
print('Used the gpu')
既然我们已经知道了如何配置茶诺,让我们通过一些简单的例子来看看它是如何工作的。基本上,每段安诺代码都由相同的结构组成:
- 在类中声明变量的初始化部分。
- 形成函数的编译。
- 函数应用于数据类型的执行。
让我们在向量计算和数学表达式的一些基本例子中使用这些原理:
#Initialize a simple scalar
x = T.dscalar()
fx = T.exp(T.tan(x**2)) #initialize the function we want to use.
type(fx) #just to show you that fx is a theano variable type
#Compile create a tanh function
f = theano.function(inputs=[x], outputs=[fx])
#Execute the function on a number in this case
f(10)
正如我们之前提到的,我们可以用 Anano 来表示数学表达式。看看这个例子,我们使用了一个强大的名为自动分化的 antio 特性,这个特性对于反向传播非常有用:
fp = T.grad(fx, wrt=x)
fs= theano.function([x], fp)
fs(3)
output:] 4.59
现在我们已经理解了使用变量和函数的方式,让我们执行一个简单的逻辑函数:
#now we can apply this function to matrices as well
x = T.dmatrix('x')
s = 1 / (1 + T.exp(-x))
logistic = theano.function([x], s)
logistic([[2, 3], [.7, -2],[1.5,2.3]])
output:
array([[ 0.88079708, 0.95257413],
[ 0.66818777, 0.11920292],
[ 0.81757448, 0.90887704]])
我们可以清楚地看到,与 NumPy 相比,antio 提供了更快的方法将函数应用于数据对象。





:梯度升级参数,也称为步长
的特征向量。
的特征值。



浙公网安备 33010602011771号