Python-数据科学本质论-全-
Python 数据科学本质论(全)
零、前言
"A journey of a thousand miles begins with a single step."**– Laozi (604 BC - 531 BC)
数据科学是一个相对较新的知识领域,需要成功集成线性代数,统计建模,可视化,计算语言学,图形分析,机器学习,商业智能以及数据存储和检索。
在过去的十年中,Python 编程语言征服了科学界,如今它已成为数据科学从业人员必不可少的工具,并且是每个有抱负的数据科学家必备的工具。 Python 将为您提供快速,可靠,跨平台且成熟的环境,用于数据分析,机器学习和算法问题解决。 通过我们简单,循序渐进,面向示例的方法,可以轻松克服以前阻止您掌握 Python 用于数据科学应用的一切,这将帮助您将最直接,最有效的 Python 工具应用于示例和现实世界的数据集。
作为《Python 数据科学基础知识》的第三版,这本书提供了更新和扩展的内容。 基于最新的 Jupyter 笔记本和 JupyterLab 接口(结合了可互换的内核,一个真正的多语言数据科学系统),本书结合了 NumPy,pandas和 Scikit-learn 的所有最新改进。 此外,它以新的 GBM 算法(XGBoost,LightGBM 和 CatBoost),深度学习(通过提供基于 TensorFlow 的 Keras 解决方案),精美的可视化效果(主要是由于 Seaborn)和 Web 部署(使用瓶子)的形式提供了新内容。 。
本书首先向您展示了如何使用单源方法在 Python 的最新版本(3.6)中设置基本的数据科学工具箱(这意味着该书的代码也可以在 Python 2.7 上轻松重用)。 然后,它将以某种方式指导您跨所有数据处理和预处理阶段,以解释与加载数据,转换和修复数据以进行分析以及探索/处理有关的所有核心数据科学活动。 最后,这本书将向您介绍主要的机器学习算法,图形分析技术以及所有可视化和部署工具,从而使向数据科学专家和业务用户的受众展示结果更加容易,从而完成其概述。
这本书是给谁的
如果您是一位有抱负的数据科学家,并且至少具有数据分析和 Python 的工作知识,那么这本书将帮助您开始进行数据科学。 具有 R 或 MATLAB/GNU Octave 经验的数据分析还将发现这本书是增强其数据处理和机器学习技能的综合参考。
本书涵盖的内容
第 1 章,“第一步”,介绍了 Jupyter 笔记本,并演示了如何访问教程中运行的数据。
第 2 章,“数据整理”介绍了所有关键的数据操作和转换技术,重点介绍了进行谋杀活动的最佳实践。
第 3 章,“数据管道”讨论了可能改善数据科学项目结果的所有操作,使读者能够进行高级数据操作。
第 4 章,“机器学习”提出了 Scikit-learn 库中可用的最重要的学习算法。 将向读者展示实际应用以及需要检查的重要内容以及为从每种学习技术中获得最佳效果而需要调整的参数。
第 5 章,“可视化,见解和结果”为您提供基础和中级的图形表示形式,对于表示和视觉理解复杂的数据结构和从机器学习中获得的结果是必不可少的。
第 6 章,“社交网络分析”为读者提供了处理代表社会关系和互动的数据的实用有效技巧。
第 7 章,“超越基础的深度学习”演示了如何从头开始构建卷积神经网络,介绍了该行业的所有工具以增强您的深度学习模型,并说明了迁移学习以及如何使用循环神经网络对文本进行分类和预测序列的工作。
第 8 章,“用于大数据的 Spark”介绍了一种新的数据处理方式:水平扩展大数据。 这意味着运行已安装 Hadoop 和 Spark 框架的计算机集群。
附录和“增强 Python 基础”涵盖了一些 Python 示例和教程,这些示例和教程侧重于在数据科学项目上必不可少的关键语言功能。
充分利用这本书
为了充分利用本书,您将需要以下内容:
- 熟悉基本的 Python 语法和数据结构(例如,列表和字典)
- 关于数据分析的一些知识,特别是关于描述统计的知识
您可以在阅读本书时建立这两种技能,尽管本书并没有过多地介绍细节,而是仅提供了数据科学家要想在她身上取得成功所必须知道的大多数技术的基础知识 /他的项目。
您还将需要以下内容:
- 装有 Windows,macOS 或 Linux 操作系统且至少有 8 GB 内存的计算机(如果您的计算机上只有 4 GB,则无论如何都可以使用大多数示例)
- 如果您想加快计算速度,可以在计算机上安装 GPU,您可以在第 7 章,“超越基础的深度学习”中找到。
- Python 3.6 安装,最好通过 Anaconda。
下载示例代码文件
您可以从这个页面的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问这个页面并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或注册 www.packt.com 。
- 选择支持选项卡。
- 单击代码下载和勘误。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 版 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在这个页面。 如果代码有更新,它将在现有的 GitHub 存储库中进行更新。
我们还有丰富的书籍和视频目录中的其他代码包,可通过这个页面获得。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本中的代码字,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。 这是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件安装为系统中的另一个磁盘。”
代码块设置如下:
In: G.add_edge(3,4)
G.add_edges_from([(2, 3), (4, 1)]) nx.draw_networkx(G) plt.show()
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“从管理面板中选择系统信息。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、第一步
无论您是数据科学的急切学习者还是有充分基础的数据科学从业人员,都可以利用 Python 的基本介绍来进行数据科学。 如果您已经至少有一些基本的编码,使用 Python 或其他特定于数据分析的语言(例如 MATLAB 或 R)编写通用计算机程序的经验,则可以充分利用它。
本书将直接研究 Python 的数据科学,为您提供一条直接且快速的途径,以使用 Python 及其强大的数据分析和机器学习包解决各种数据科学问题。 本书提供的代码示例不要求您精通 Python。 但是,他们将假设您至少了解 Python 脚本编写的基础知识,包括列表和字典等数据结构以及类对象的工作原理。 如果您对这些主题没有信心或对 Python 语言了解甚少,那么在阅读本书之前,建议您阅读在线教程。 您可以选择一些不错的在线教程,例如 Code Academy 课程在这个页面上提供的教程,该课程由 Google 的 Python 类提供甚至是 Jake Vanderplas 撰写的《Python 旋风之旅》。 所有课程都是免费的,并且在几个小时的学习中,它们应该为您提供所有基础知识,以确保您充分享受这本书。 为了提供上述两个免费课程的集成,我们还准备了自己的教程,可以在本书的附录中找到该教程。
无论如何,不要被我们的开始要求吓倒; 对数据科学应用掌握足够的 Python 并不像您想象的那么艰巨。 只是我们必须假定读者有一些基础知识,因为我们的目的是直接进行数据科学,而不必过多地解释将要使用的 Python 语言的一般方面。
那你准备好了吗? 让我们开始吧!
在这一简短的介绍性章节中,我们将介绍基础知识,以全面展开,并涉及以下主题:
- 如何设置 Python 数据科学工具箱
- 使用 Jupyter
- 我们将在本书中学习的数据概述
数据科学和 Python 简介
数据科学是一个相对较新的知识领域,尽管计算机科学界已经对它的核心组件进行了多年的研究。 它的组件包括线性代数,统计建模,可视化,计算语言学,图形分析,机器学习,商业智能以及数据存储和检索。
数据科学是一个新领域,您必须考虑到当前,它的前沿仍然有些模糊和动态。 由于数据科学是由各种学科构成的,所以请记住,根据数据科学家的能力和专业领域的不同,它们的个人资料也会有所不同(例如,您可以阅读 Harlan D Harris 在这个页面上发表的《数据科学家》的说明性文章,或深入讨论有关 A 型或 B 型数据科学家以及其他有趣的分类法的信息,请访问这个页面)。
在这种情况下,作为数据科学家,您可以学习并有效使用的最佳交易工具是什么? 我们相信最好的工具是 Python,并且我们打算为您提供快速入门所需的所有基本信息。
此外,R 和 MATLAB 等其他编程语言为数据科学家提供了专用工具,以解决数据科学中的统计分析和矩阵处理中的特定问题。 但是,只有 Python 才能以可扩展且有效的方式真正利用所有关键技术来完善您的数据科学家技能。 这种多用途语言适用于开发和生产。 无论您的背景或经验如何,它都能处理从小到大的数据问题,并且易于学习和掌握。
Python 创建于 1991 年,是一种通用的,解释性的和面向对象的语言,Python 逐渐稳定地征服了科学界,并成长为成熟的用于数据处理和分析的专用包生态系统。 它使您能够进行无数且快速的实验,轻松进行理论开发,并迅速部署科学应用。
目前,使其成为必不可少的数据科学工具的 Python 核心特征如下:
- 它提供了一个大型,成熟的包系统,用于数据分析和机器学习。 它保证您将获得数据分析过程中可能需要的全部信息,有时甚至更多。
- Python 可以轻松地集成不同的工具,并为不同的语言,数据策略和学习算法提供了真正的统一基础,可以轻松地将它们组合在一起,并且可以具体地帮助数据科学家构建强大的解决方案。 有些包允许您调用其他语言(Java,C,Fortran,R 或 Julia)的代码,将一些计算外包给它们并提高脚本性能。
- 它非常通用。 无论您的编程背景或风格是什么(面向对象,过程甚至函数式),您都将喜欢使用 Python 进行编程。
- 它是跨平台的; 您的解决方案将在 Windows,Linux 和 macOS 系统上完美流畅地运行。 您不必太担心可移植性。
- 尽管可以解释,但是与其他主流数据分析语言(例如 R 和 MATLAB)相比,它无疑是快速的(尽管它不能与 C,Java 和新出现的 Julia 语言相提并论)。 此外,还有静态编译器,例如 Cython 或即时编译器,例如 PyPy,它们可以将 Python 代码转换为 C 以获得更高的性能。
- 由于其最小的内存占用和出色的内存管理,它可以处理大型内存数据。 内存垃圾回收器通常会在您使用各种迭代和重复的数据处理方式来加载,转换,切块,切片,保存或丢弃数据时节省一天的时间。
- 学习和使用非常简单。 掌握了基础知识之后,没有比立即开始编码更好的方法来学习更多。
- 此外,使用 Python 的数据科学家的数量也在不断增长:社区每天都在发布新的包和改进,使 Python 生态系统成为数据科学中越来越多产和丰富的语言。
安装 Python
首先,让我们继续介绍所需的所有设置,以创建一个可以正常运行的数据科学环境,以测试示例并尝试我们将为您提供的代码。
Python 是一种开源的,面向对象的,跨平台的编程语言。 与它的一些直接竞争对手(例如 C++ 或 Java)相比,Python 非常简洁。 它使您可以在很短的时间内构建可运行的软件原型,但不仅如此,它已成为数据科学家工具箱中使用最多的语言。 它也是一种通用语言,并且由于解决各种问题和必要性的各种可用包而非常灵活。
Python 2 还是 Python 3?
Python 有两个主要分支:2.7.x 和 3.x。 在本书的第三版修订时,Python 基金会( www.python.org/ )正在提供 Python 版本 2.7.15(发布日期为 2018 年 1 月 5 日)和 3.6 的下载。 .5(发布日期 2018 年 1 月 3 日)。 尽管最新的 Python 3 版本,但较旧的 Python 2 在 2017 年仍在科学(采用 20%)和商业(采用 30%)领域中使用,如 JetBrains 的调查所详细描述的那样。 如果您仍在使用 Python 2,那么情况很快就会变得很麻烦,因为在短短的一年时间内,Python 2 将被淘汰并且维护将停止( pythonclock.org/ 将为您提供倒计时, 但是要获得对此的正式声明,只需阅读这个页面,实际上只有少数几个库在两者版本之间仍然不兼容并没有足够的理由继续使用旧版本。
除了所有这些原因之外,Python 3 和 2 之间没有立即的向后兼容性。实际上,如果尝试使用 Python 3 解释器运行一些为 Python 2 开发的代码,则可能无法正常工作。 最新版本进行了重大更改,这影响了以前的兼容性。 一些数据科学家已经将大部分工作建立在 Python 2 及其包上,因此他们不愿意切换到新版本。
在本书的第三版中,我们将继续针对广大的数据科学家,数据分析师和开发人员,这些人在 Python 2 中没有如此强大的遗产。因此,我们将继续使用 Python 3,并且我们建议使用最新版本的 Python 3.6 等版本。 毕竟,Python 3 是 Python 的现在和未来。 它是 Python 基金会将进一步开发和改进的唯一版本,并且它将是许多操作系统上未来的默认版本。
无论如何,如果您当前正在使用版本 2,并且希望继续使用它,则仍然可以使用本书及其所有示例。 实际上,在大多数情况下,在代码本身带有以下导入之前,我们的代码将仅在 Python 2 上运行:
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from future import standard_library
standard_library.install_aliases()
from __future__ import命令应始终出现在脚本的开头,否则您可能会遇到 Python 报告错误的情况。
如 Python-future 网站中所述,这些导入将帮助将几种仅 Python 3 的构造转换为与 Python 3 和 Python 2 兼容的形式(无论如何) ,即使没有上述导入,大多数 Python 3 代码也只能在 Python 2 上运行)。
为了成功运行上行命令,如果将来的包在您的系统上尚不可用,则应使用以下命令来安装它(版本> = 0.15.2),该命令将从外壳程序执行:
$> pip install -U future
如果您想进一步了解 Python 2 和 Python 3 之间的差异,建议阅读 Python 基金会本身提供的 Wiki 页面。
分步安装
从未使用过 Python(可能未在计算机上轻松安装该语言)的新手数据科学家需要首先从项目的主要网站下载安装程序,然后将其安装在其本地计算机上。
本节提供对可以在计算机上安装的内容的完全控制。 当您必须设置单台计算机来处理数据科学中的不同任务时,这非常有用。 无论如何,请注意逐步安装确实需要花费时间和精力。 相反,安装现成的科学发行版将减轻安装过程的负担,并且它可能会节省您的时间,有时甚至是麻烦,尽管它会放置大量包,但它可能非常适合第一次开始和学习。 不会一次全部在您的计算机上使用。 因此,如果要立即开始简单的安装过程,只需跳过此部分,然后继续进行下一节“科学发行版”。
这是一种多平台编程语言,您将找到在 Windows 或类 Unix 操作系统上运行的计算机的安装程序。
请记住,大多数 Linux 发行版的某些最新版本(例如 CentOS,Fedora,Red Hat Enterprise 和 Ubuntu)都在存储库中打包了 Python 2。 在这种情况下,并且在您的计算机上已经有 Python 版本的情况下(因为我们的示例在 Python 3 上运行),您首先必须检查您确切在运行哪个版本。 要进行此类检查,只需按照以下说明进行操作:
- 打开 python shell,在终端中键入
python,或单击在系统上找到的任何 Python 图标。 - 然后,在启动 Python 之后,要测试安装,请在 Python 交互式 Shell 或 REPL 中运行以下代码:
>>> import sys >>> print (sys.version_info)
- 如果您可以看到您的 Python 版本具有
major=2属性,则表明您正在运行 Python 2 实例。 否则,如果该属性的值为 3,或者print语句向您报告诸如 v3.xx(例如 v3.5.1)之类的内容,则说明您运行的 Python 版本正确,因此您可以继续前进 。
为了澄清我们刚才提到的操作,当在终端命令行中给出命令时,我们在命令前添加$>。 否则,如果用于 Python REPL,则在其前面加上>>>。
安装必要的包
除非您采用特定的预制发行版,否则 Python 不会捆绑您所需的一切。 因此,要安装所需的包,可以使用pip或easy_install。 这两个工具都在命令行中运行,使安装,升级和删除 Python 包的过程变得轻而易举。 要检查本地计算机上已经安装了哪些工具,请运行以下命令:
$> pip
要安装pip,请按照这个页面中给出的说明进行操作。
或者,您也可以运行以下命令:
$> easy_install
如果这两个命令均以错误结尾,则需要安装其中任何一个。 我们建议您使用pip,因为它被认为是easy_install的改进。 而且,easy_install将来会被删除,并且pip具有重要的优势。 最好使用pip安装所有组件,因为以下原因:
- 它是 Python 3 的首选包管理器。从 Python 2.7.9 和 Python 3.4 开始,它默认包含在 Python 二进制安装程序中。
- 它提供了卸载功能
- 如果由于某种原因安装包失败,它将回滚并让您的系统保持畅通
尽管在 Windows 上使用pip的优势,仍然可以使用easy_install,因为pip并不总是安装预编译的二进制包。 有时,它将尝试直接从 C 源代码构建包的扩展,因此需要正确配置的编译器(在 Windows 上这并非易事)。 这取决于包是在鸡蛋上运行(并且pip无法直接使用其二进制文件,但是需要根据其源代码进行构建)还是在轮子上运行(在这种情况下,pip可以安装二进制文件(如果可用),如此处所述)。 相反,easy_install将始终从鸡蛋和车轮上安装可用的二进制文件。 因此,如果您在安装包时遇到了意外的困难,easy_install可以节省您的一天(无论如何,都需要付出一定的代价,就像我们在列表中提到的那样)。
默认情况下,最新版本的 Python 应该已经安装了pip。 因此,您可能已经在系统上安装了它。 如果不是,最安全的方法是从这个页面下载get-pi.py脚本,然后使用以下命令运行它:
$> python get-pip.py
该脚本还将从这个页面安装安装工具,该工具也包含easy_install。
现在您已经准备好安装所需的包,以运行本书中提供的示例。 要安装< package-name >通用包,您只需运行以下命令:
$> pip install < package-name >
或者,您可以运行以下命令:
$> easy_install < package-name >
请注意,在某些系统中,pip可能会被命名为pip3,easy_install会被命名为easy_install-3,以强调一个事实,即两者都在 Python 3 的包上运行。如果不确定,请检查 Python 的版本pip正在使用:
$> pip -V
对于easy_install,命令略有不同:
$> easy_install --version
此后,将下载并安装<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 的包中。
最后,要搜索和浏览可用于 Python 的 Python 包,请查看 pypi.org。
包的升级
很多时候,您会发现自己不得不升级包,因为依赖项需要新版本,或者它具有您要使用的其他功能。 首先,通过浏览__version__属性来检查已安装的库的版本,如以下示例numpy所示:
>>> import numpy >>> numpy.__version__ # 2 underscores before and after
'1.11.0'
现在,如果要将其更新为较新的版本(例如1.12.1版本),则可以从命令行运行以下命令:
$> pip install -U numpy==1.12.1
或者,您可以使用以下命令:
$> easy_install --upgrade numpy==1.12.1
最后,如果您有兴趣将其升级到最新的可用版本,只需运行以下命令:
$> pip install -U numpy
您也可以运行以下命令:
$> easy_install --upgrade numpy
科学发行版
到目前为止,您已经了解到,创建工作环境对于数据科学家而言是一项耗时的操作。 首先,您需要安装 Python,然后可以一个接一个地安装所有需要的库(有时,安装过程可能不如您先前希望的那样顺利)。
如果您想节省时间和精力,并确保可以使用完整的 Python 环境,则可以下载,安装和使用科学的 Python 发行版。 除 Python 外,它们还包括各种预装的包,有时甚至还具有其他工具和 IDE。 其中一些在数据科学家中是非常著名的,在下面的部分中,您将找到每个包的一些关键功能。
我们建议您首先立即下载并安装科学发行版,例如 Anaconda(这是最完整的发行版),然后在练习本书中的示例之后,决定是否完全卸载发行版并单独设置 Python。 可以仅随附项目所需的包。
Anaconda
Anaconda 是 Continuum Analytics 提供的 Python 发行版,包含近 200 个包,其中包括 NumPy,SciPy,pandas,Jupyter ,Matplotlib,Scikit-learn 和 NLTK。 它是一个跨平台发行版(Windows,Linux 和 macOS),可以安装在具有其他现有 Python 发行版和版本的计算机上。 它的基本版本是免费的。 相反,包含高级功能的加载项将单独收费。 Anaconda 引入了二进制包管理器conda,作为管理包安装的命令行工具。
如网站上所述,Anaconda 的目标是为大型处理,预测分析和科学计算提供企业级 Python 分发。
利用 Conda 安装包
如果您决定安装 Anaconda 发行版,则可以利用我们前面提到的conda二进制安装程序。 conda是一个开源包管理系统,因此,可以与 Anaconda 发行版分开安装。 与pip的核心区别在于conda可用于在conda环境(即已安装conda并将其用于其中的环境)中安装任何包(不仅仅是 Python 的包)。 提供包)。 正如 Jack VanderPlas 在其著名博客文章中所描述的那样,使用conda优于pip有很多优势: jakevdp.github.io/blog/2016/08/25/conda-myths-and- 误解。
您可以立即测试conda在系统上是否可用。 打开一个外壳并数字以下内容:
$> conda -V
如果conda可用,则会显示您的版本; 否则,将报告错误。 如果conda不可用,则可以通过转到这个页面并安装适合您计算机的 Miniconda 软件,将其快速安装到系统上。 Miniconda 是仅包含conda及其依赖项的最小安装。
Conda 可以帮助您管理两项任务:安装包和创建虚拟环境。 在本段中,我们将探讨conda如何帮助您轻松安装数据科学项目中可能需要的大多数包。
开始之前,请检查手边是否有最新版本的conda:
$> conda update conda
现在,您可以安装所需的任何包。 要安装<package-name>通用包,您只需运行以下命令:
$> conda install <package-name>
您还可以通过指出来安装包的特定版本:
$> conda install <package-name>=1.11.0
同样,您可以通过列出所有包的名称来一次安装多个包:
$> conda install <package-name-1> <package-name-2>
如果您只需要更新以前安装的包,则可以继续使用conda:
$> conda update <package-name>
您只需使用--all参数即可更新所有可用的包:
$> conda update --all
最后,conda也可以为您卸载包:
$> conda remove <package-name>
如果您想进一步了解conda,可以在这个页面阅读其文档。 总而言之,它的主要优点是处理二进制文件甚至比easy_install更好(通过始终在 Windows 上提供成功的安装,而无需从源代码编译包),但没有问题和局限性。 使用conda,包易于安装(安装始终成功),更新甚至卸载。 另一方面,conda无法直接从git服务器安装(因此它无法访问许多正在开发中的包的最新版本),并且它无法像pip本身那样涵盖 PyPI 上的所有可用包。
Enthought Canopy
Enthought Canopy 是 Enthought Inc.的 Python 发行版本。它包含 200 多个预装的包,例如 NumPy, SciPy,Matplotlib,Jupyter 和 Pandas。 此分发针对工程师,数据科学家,定量和数据分析师以及企业。 它的基本版本是免费的(称为 Canopy Express),但是如果您需要高级功能,则必须购买一个前端版本。 它是一个多平台发行版,其命令行安装工具为canopy_cli。
WinPython
WinPython 是由社区维护的免费开源 Python 发行版。 它是为科学家设计的,包括许多包,例如 NumPy,SciPy,Matplotlib 和 Jupyter。 它还包括 Spyder 作为 IDE。 它是免费和便携式的。 您可以将 WinPython 放入任何目录,甚至可以放入 USB 闪存驱动器,同时在系统上维护它的多个副本和版本。 它仅适用于 Microsoft Windows,其命令行工具为 WinPython 包管理器(WPPM)。
虚拟环境
无论您是选择安装独立的 Python 还是使用科学发行版,您都可能已经注意到您实际上已在系统上绑定到已安装的 Python 版本。 对于 Windows 用户,唯一的例外是使用 WinPython 发行版,因为它是可移植的安装,您可以根据需要进行任意多个安装。
摆脱这种限制的一个简单解决方案是使用virtualenv,它是用于创建隔离的 Python 环境的工具。 这意味着,通过使用不同的 Python 环境,您可以轻松实现以下目标:
- 在您的 Python 环境中测试任何新的包安装或进行实验,而不必担心会以无法修复的方式破坏任何东西。 在这种情况下,您需要一个充当沙箱的 Python 版本。
- 拥有多个 Python 版本(Python 2 和 Python 3),与已安装包的不同版本配合使用。 这可以帮助您处理出于不同目的的不同版本的 Python(例如,我们将在 Windows OS 上提供的某些包仅在使用 Python 3.4(不是最新版本)时才有效。
- 轻松获取 Python 环境的可复制快照,并使数据科学原型在任何其他计算机或生产环境中均能顺利运行。 在这种情况下,您主要关心的是工作环境的不变性和可复制性。
您可以在这个页面中找到有关virtualenv的文档,尽管我们将为您提供立即开始使用它的所有指导。 为了利用virtualenv,您首先必须在系统上安装它:
$> pip install virtualenv
安装完成后,您可以开始构建虚拟环境。 在继续之前,您必须做出一些决定:
-
如果您的系统上安装了更多版本的 Python,则必须决定选择哪个版本。 否则,
virtualenv将采用在您的系统上安装virtualenv时使用的 Python 版本。 为了设置不同的 Python 版本,您必须在参数-p后面加上所需的 Python 版本,或者插入要使用的 Python 可执行文件的路径(例如,使用-p python2.7或只是指向 Python 可执行文件,例如-p c:Anaconda2python.exe)。 -
使用
virtualenv,当需要安装某些包时,它将从头开始安装,即使它已在系统级别可用(在您从中创建虚拟环境的 python 目录中)。 此默认行为很有意义,因为它允许您创建一个完全分离的空环境。 为了节省磁盘空间并限制所有包的安装时间,您可以改为使用参数--system-site-packages来决定利用系统上已经可用的包。 -
您可能希望以后可以跨 Python 安装甚至在不同的机器之间在虚拟环境中移动。 因此,您可能想通过使用参数
--relocatable来使所有环境脚本相对于其放置的路径实现功能。
在确定要使用的 Python 版本之后,链接到现有的全局包,以及是否可重定位虚拟环境,要启动,只需从外壳启动命令即可。 声明要分配给新环境的名称:
$> virtualenv clone
virtualenv将使用您提供的名称在实际启动命令的路径中创建一个新目录。 要开始使用它,您只需输入目录和数字activate:
$> cd clone $> activate
此时,您可以开始在单独的 Python 环境中工作,安装包并使用代码。
如果需要一次安装多个包,则可能需要pip freeze的某些特殊功能,该特殊功能将列出您已在系统上安装的所有包(及其版本)。 您可以使用以下命令在文本文件中记录整个列表:
$> pip freeze > requirements.txt
将列表保存在文本文件中后,只需将其放入虚拟环境中,并使用一个命令即可轻松安装所有包:
$> pip install -r requirements.txt
将按照列表中的顺序安装每个包(包以不区分大小写的排序顺序列出)。 如果某个包需要列表中后面列出的其他包,则没什么大不了的,因为pip会自动管理此类情况。 因此,如果您的包需要 NumPy 且尚未安装 NumPy,则pip将首先安装它。
完成安装包并使用环境进行脚本和实验后,为了返回系统默认值,只需发出以下命令:
$> deactivate
如果要完全删除虚拟环境,请在停用并退出环境目录后,只需执行递归删除操作即可摆脱环境目录本身。 例如,在 Windows 上,您只需执行以下操作:
$> rd /s /q clone
在 Linux 和 macOS 上,命令如下:
$> rm -r -f clone
如果要广泛使用虚拟环境,则应考虑使用virtualenvwrapper,它是virtualenv的一组包装,以帮助您轻松管理多个虚拟环境。 可以在这个页面中找到。 如果您在 Unix 系统(Linux 或 macOS)上运行,我们必须引用的另一个解决方案是pyenv,可以在这个页面中找到它。 它使您可以设置主要的 Python 版本,允许安装多个版本,并创建虚拟环境。 它的特点是它不依赖于 Python 进行安装,并且可以在用户级别完美运行(不需要sudo命令)。
Conda 管理环境
如果您已经安装了 Anaconda 发行版,或者通过使用 Miniconda 安装来尝试了conda,则还可以利用conda命令来运行虚拟环境,以代替virtualenv。 让我们看看如何在实践中使用conda。 我们可以检查可用的环境如下:
>$ conda info -e
该命令将基于conda向您报告可以在系统上使用哪些环境。 最有可能的情况是,您的唯一环境是root,它指向 Anaconda distribution文件夹。
例如,我们可以基于 Python 版本 3.6 创建一个环境,其中安装了所有必需的 Anaconda 打包的库。 例如,在为数据科学项目安装一组特定的包时,这是有道理的。 为了创建这样的环境,只需执行以下操作:
$> conda create -n python36 python=3.6 anaconda
前面的命令要求使用特定的 Python 版本 3.6,并要求安装 Anaconda 发行版上所有可用的包(参数anaconda)。 它使用参数-n将环境命名为python36。 考虑到 Anaconda 安装中的大量包,完整的安装应该花费一些时间。 完成所有安装后,您可以激活环境:
$> activate python36
如果在激活时需要将其他包安装到您的环境中,则只需使用以下命令:
$> conda install -n python36 <package-name1> <package-name2>
也就是说,使所需包的列表遵循您的环境名称。 当然,您也可以像在virtualenv环境中那样使用pip安装。
您也可以使用文件来代替自己列出所有包。 您可以使用 list 参数在环境中创建列表,并将输出通过管道传递到文件:
$> conda list -e > requirements.txt
然后,在目标环境中,可以使用以下命令安装整个列表:
$> conda install --file requirements.txt
您甚至可以根据需求列表创建环境:
$> conda create -n python36 python=3.6 --file requirements.txt
最后,在使用环境后,关闭会话,您只需使用以下命令:
$> deactivate
与virtualenv相反,有一个专门的参数可以从您的系统中完全删除环境:
$> conda remove -n python36 --all
基本包一览
前面我们提到过,Python 的两个最相关的特征是它与其他语言集成的能力和成熟的包系统,而 PyPI(Python 包索引: pypi.org )很好地体现了这一点。 大多数 Python 开源包的存储库,这些包不断维护和更新。
我们现在要介绍的包具有很强的分析能力,它们将构成一个完整的数据科学工具箱。 所有包均由经过广泛测试和高度优化的功能组成,以提高内存使用率和性能,并准备成功执行任何脚本操作。 下一节提供了有关如何安装它们的演练。
部分受到 R 和 MATLAB 环境中存在的类似工具的启发,我们将探索一些选定的 Python 命令如何使您能够有效地处理数据,然后在无需编写过多代码或重新发明轮子的情况下进行探索,转换,实验和学习。
NumPy
NumPy 是 Travis Oliphant 的创作,是 Python 语言的真正分析主力。 它为用户提供了多维数组,以及为这些数组进行大量数学运算的大量函数。 数组是沿多个维度排列的数据块,它们实现数学向量和矩阵。 数组具有最佳的内存分配特征,不仅对存储数据有用,而且对快速矩阵运算(向量化)也很有用,当您希望解决临时数据科学问题时,数组必不可少:
- 网站
- 打印时的版本:1.12.1
- 建议的安装命令:
pip install numpy
作为 Python 社区广泛采用的约定,在导入 NumPy 时,建议将其别名为np:
import numpy as np
在本书的整个过程中,我们都会这样做。
SciPy
SciPy 由 Travis Oliphant,Pearu Peterson 和 Eric Jones 共同发起的原始项目完善了 NumPy 的功能,该功能为线性代数,稀疏矩阵,信号和图像处理,优化,快速傅立叶变换等提供了更多科学算法:
- 网站
- 打印时的版本:1.1.0
- 建议的安装命令:
pip install scipy
Pandas
Pandas 包处理 NumPy 和 SciPy 无法执行的所有操作。 借助其特定的数据结构,即DataFrame和 Series,Pandas 允许您处理不同类型(NumPy 的数组无法执行的操作)和时间序列的复杂数据表。 由于 Wes McKinney 的创造,您将能够轻松,平稳地从各种来源加载数据。 然后,您可以切片,切块,处理丢失的元素,添加,重命名,聚合,重塑形状,最后随意可视化数据:
- 网站
- 打印时的版本:0.23.1
- 建议的安装命令:
pip install pandas
按照惯例,Pandas 包是作为pd导入的:
import pandas as pd
Pandas 分析
这是一个 GitHub 项目,可以轻松地从 Pandas DataFrame创建报告。 该包将在交互式 HTML 报告中提供以下措施,该报告用于评估数据科学项目中的手头数据:
- 基本知识,例如类型,唯一值和缺失值
- 分位数统计信息,例如最小值,Q1,中位数,Q3,最大值,范围和四分位数范围
- 描述性统计数据,例如均值,众数,标准差,总和,中位数绝对偏差,变异系数,峰度和偏度
- 最频繁的值
- 直方图
- 相关性突出显示高度相关的变量,以及 Spearman 和 Pearson 矩阵
这是有关此包的所有信息:
- 网站
- 打印时的版本:1.4.1
- 建议的安装命令:
pip install pandas-profiling
Scikit-learn
作为 SciKits(SciPy 工具包)的一部分开始,Scikit-learn 是 Python 数据科学运算的核心。 它提供了数据预处理,有监督和无监督学习,模型选择,验证和误差度量方面可能需要的所有功能。 希望我们在本书中详细讨论这个包。 Scikit-learn 于 2007 年由 David Cournapeau 发起,是一个 Google Summer of Code 项目。 自 2013 年以来,它已由 INRIA(国家信息和自动化研究所,即法国计算机科学与自动化研究所)的研究人员接管:
- 网站
- 打印时的版本:0.19.1
- 建议的安装命令:
pip install Scikit-learn
请注意,导入的模块名为sklearn。
Jupyter
科学方法要求以可重复的方式对不同的假设进行快速实验。 Jupyter 最初命名为 IPython,并且仅限于使用 Python 语言,它是由 Fernando Perez 创建的,旨在满足对具有图形化集成的交互式 Python 命令 shell(基于 shell,Web 浏览器和应用界面)的需求。 ,可自定义的命令,丰富的历史记录(以 JSON 格式)和计算并行性以提高性能。 Jupyter 是本书中我们最喜欢的选择。 它用于清楚有效地说明脚本和数据的操作及其结果:
- 网站
- 打印时的版本:4.4.0(
ipykernel = 4.8.2) - 建议的安装命令:
pip install jupyter
JupyterLab
JupyterLab 是 Jupyter 项目的下一个用户界面,该项目目前处于测试阶段。 它是为交互式和可重复计算而设计的环境,它将以更灵活和强大的用户界面提供所有常用的笔记本,终端,文本编辑器,文件浏览器,丰富的输出等。 JupyterLab 达到 1.0 版后,JupyterLab 最终将取代经典的 Jupyter 笔记本。 因此,我们打算现在介绍此包,以使您了解它及其功能:
- 网站
- 打印时的版本:0.32.0
- 建议的安装命令:
pip install jupyterlab
Matplotlib
Matplotlib 最初由 John Hunter 开发,是一个库,其中包含从数组创建质量图并以交互方式可视化所需的所有构造块。
您可以在 PyLab 模块中找到所有类似 MATLAB 的绘图框架:
- 网站
- 打印时的版本:2.2.2
- 建议的安装命令:
pip install matplotlib
您可以使用以下命令简单地导入实现可视化所需的内容:
import matplotlib.pyplot as plt
Seaborn
由于这个原因,使用 Matplotlib 制作漂亮的图形确实非常耗时,Michael Waskom 开发了 Seaborn,这是一种基于 Matplotlib 的包的高级可视化工具,并与 Pandas 数据结构(例如 Series 和DataFrame)集成在一起,能够产生信息丰富且美观的统计可视化。
- 网站
- 打印时的版本:0.9.0
- 建议的安装命令:
pip install seaborn
您可以使用以下命令简单地导入实现可视化所需的内容:
import seaborn as sns
statsmodels
statsmodels以前是 SciKits 的一部分,被认为是 SciPy 统计功能的补充。 它具有广义线性模型,离散选择模型,时间序列分析,一系列描述性统计以及参数和非参数测试:
- 网站
- 打印时的版本:0.9.0
- 建议的安装命令:
pip install statsmodels
BeautifulSoup
BeautifulSoup 是 Leonard Richardson 的作品,是一种从 HTML 和 XML 文件中提取数据的出色工具,这些文件是从互联网检索的。 即使在标签的汤(因此得名)的情况下,它的效果也非常好,这些汤是畸形,矛盾和不正确标签的集合。 选择解析器(Python 标准库中包含的 HTML 解析器可以正常工作)之后,借助 BeautifulSoup,您可以浏览页面中的对象并提取文本,表格和其他可能有用的信息:
- 网站
- 打印时的版本:4.6.0
- 建议的安装命令:
pip install beautifulsoup4
请注意,导入的模块名为bs4。
NetworkX
NetworkX 由美国洛斯阿拉莫斯国家实验室开发,是专门用于创建,处理,分析和图形表示真实网络数据的包(它可以轻松地使用由一百万个节点组成的图形进行操作, 边缘)。 除了用于图形的专用数据结构和精细的可视化方法(2D 和 3D)之外,它还为用户提供了许多标准的图形度量和算法,例如最短路径,中心,组件,社区,聚类和 PageRank。 我们将在第 6 章,“社交网络分析”中主要使用此包:
- 网站
- 打印时的版本:2.1
- 建议的安装命令:
pip install networkx
按照惯例,NetworkX 被导入为nx:
import networkx as nx
NLTK
自然语言工具包(NLTK)提供对语料库和词汇资源的访问,以及用于自然语言处理(NLP),从分词器到词性标记器,从树模型到命名实体识别。 最初,史蒂芬·伯德(Steven Bird)和爱德华·洛珀(Edward Loper)创建了该包,将其作为宾夕法尼亚大学课程的 NLP 教学基础设施。 现在,它是一个了不起的工具,可用于原型和构建 NLP 系统:
- 网站
- 打印时的版本:3.3
- 建议的安装命令:
pip install nltk
Gensim
Gensim 由 RadimŘehůřek 编程,是一个开源包,适用于借助并行可分配的在线算法分析大型文本集。 在高级功能中,它实现潜在语义分析(LSA),通过潜在 Dirichlet 分配(LDA)进行主题建模。 word2vec,一种强大的算法,可将文本转换为向量特征,可用于有监督和无监督的机器学习:
- 网站
- 打印时的版本:3.4.0
- 建议的安装命令:
pip install gensim
PyPy
PyPy 不是包; 它是 Python 3.5.3 的替代实现,它支持大多数常用的 Python 标准包(不幸的是,当前尚未完全支持 NumPy)。 优点是,它可以提高速度和内存处理能力。 因此,对于大数据量的重型操作非常有用,它应该成为大数据处理策略的一部分:
XGBoost
XGBoost 是可伸缩,可移植且分布式的梯度提升库(树型集成机器学习算法)。 它最初是由华盛顿大学的 Chen Tianqi 创建的,并由 Bing Xu 的 Python 包装程序和 Tong He 的 R 接口进行了完善(您可以直接从其主要创建者的这个页面上阅读 XGBoost 背后的故事。 XGBoost 适用于 Python,R,Java,Scala,Julia 和 C++,并且可以在 Hadoop 和 Spark 集群中的单台机器上使用(利用多线程):
可以在这个页面上找到在系统上安装 XGBoost 的详细说明。
在 Linux 和 macOS 上安装 XGBoost 的过程非常简单,但是对于 Windows 用户而言却有点棘手,尽管最近发布的针对 Python 的预构建二进制轮子使该过程对每个人来说都是小菜一碟。 您只需要在外壳上键入以下内容:
$> pip install xgboost
如果由于需要最新的错误修复或 GPU 支持而从头开始安装 XGBoost,则需要首先从 C++(对于 Linux/macOS 为libxgboost.so,对于 Windows 为xgboost.dll)构建共享库。 包裹。 在 Linux/macOS 系统上,您只需要通过make命令来构建可执行文件,但是在 Windows 上则要棘手一些。
通常,请参考这个页面,其中提供了有关从头开始构建的最新说明。 为了快速参考,在这里,我们将提供特定的安装步骤以使 XGBoost 在 Windows 上运行:
-
首先,下载并安装 Windows 版 Git。
-
在命令行中,执行以下操作:
$> git clone --recursive https://github.com/dmlc/xgboost $> cd xgboost $> git submodule init $> git submodule update
- 然后,始终从命令行将 64 字节系统的配置复制为默认配置:
$> copy make\mingw64.mk config.mk
- 另外,您只需复制普通的 32 字节版本:
$> copy make\mingw.mk config.mk
- 复制配置文件后,您可以运行编译器,将其设置为使用四个线程以加快编译过程:
$> mingw32-make -j4
- 在 MinGW 中,
make命令带有名称mingw32-make。 如果您使用其他编译器,则先前的命令可能不起作用。 如果是这样,您可以尝试以下操作:
$> make -j4
- 最后,如果编译器顺利完成工作,则可以使用以下命令在 Python 中安装包:
$> cd python-package $> python setup.py install
按照上述所有说明操作后,如果尝试在 Python 中导入 XGBoost 且未加载 XGBoost 并导致错误,则很可能是 Python 无法找到 MinGW 的 g++ 运行时库。
您只需要在计算机上查找 MinGW 二进制文件的位置(在我们的示例中,它在C:\mingw-w64\mingw64\bin中;只需修改以下代码并放入您的代码),然后在导入 XGBoost 之前放置以下代码段:
import os
mingw_path = 'C:\mingw-w64\mingw64\bin'
os.environ['PATH']=mingw_path + ';' + os.environ['PATH']
import xgboost as xgb
LightGBM
LightGBM 是由 Microsoft 开发的一种梯度提升框架,它以与其他 GBM 不同的方式使用基于树的学习算法,从而有利于探索更有前途的叶子(叶面),而不是逐层开发 。
In graph terminology, LightGBM is pursuing a depth-first search strategy than a breadth-first search one.
它被设计为分布式的(支持并行和 GPU 学习),并且其独特的方法确实以较低的内存使用量实现了更快的训练速度(从而允许处理更大范围的数据):
- 网站
- 打印时的版本:2.1.0
XGBoost 的安装需要比常规 Python 包更多的操作。 如果您在 Windows 系统上运行,请打开外壳并发出以下命令:
$> git clone --recursive https://github.com/Microsoft/LightGBM
$> cd LightGBM
$> mkdir build
$> cd build $> cmake -G "MinGW Makefiles" ..
$> mingw32-make.exe -j4
您可能需要先在系统上安装 CMake,并且如果报告sh.exe was found in your PATH错误,可能还需要运行cmake -G "MinGW Makefiles" ..。
如果要在 Linux 系统上运行,则只需在 shell 上输入数字:
$> git clone --recursive https://github.com/Microsoft/LightGBM
$> cd LightGBM
$> mkdir build
$> cd build $> cmake ..
$> make -j4
编译完包后,无论您使用的是 Windows 还是 Linux,都可以在 Python 命令行上将其导入:
import lightgbm as lgbm
您也可以使用 MPI 构建用于并行计算架构,HDFS 或 GPU 版本的包。 您可以在这个页面中找到所有详细说明。
CatBoost
由 Yandex 研究人员和工程师开发的 CatBoost(代表类别增强)是一种基于决策树的梯度提升算法,该算法在处理类别特征方面进行了优化,无需太多预处理(非 -表示质量的数字特征,例如颜色,品牌或类型。 由于在大多数数据库中,大多数特征都是分类的,因此 CatBoost 确实可以提高预测结果:
CatBoost 需要msgpack,可以使用pip install msgpack命令轻松安装。
TensorFlow
TensorFlow 最初由 Google Brain 团队开发,可在 Google 内部使用,然后发布给更大的公众。 2015 年 11 月 9 日,它以 Apache 2.0 开源许可的形式分发,此后,它已成为用于高性能数值计算(主要用于深度学习)的最广泛的开源软件库。 它能够跨各种平台(具有多个 CPU,GPU 和 TPU 的系统)进行计算,从台式机到服务器集群再到移动设备和边缘设备都可以进行计算。
在本书中,我们将 TensorFlow 用作 Keras 的后端,也就是说,我们不会直接使用它,但是我们需要在系统上运行它:
- 网站
- 打印时的版本:1.8.0
在 CPU 系统上安装 TensorFlow 非常简单:只需使用pip install tensorflow。 但是,如果您的系统上装有 NVIDIA GPU(您实际上需要具有 CUDA Compute Capability 3.0 或更高版本的 GPU 卡),则要求会不断提高,因此您首先必须安装以下设备:
- CUDA 工具包 9.0
- 与 CUDA Toolkit 9.0 关联的 NVIDIA 驱动程序
- cuDNN v7.0
对于每个操作,您需要根据您的系统完成各个步骤,如 NVIDIA 网站所述。 您可以在这个页面上找到取决于系统(Ubuntu,Windows 或 macOS)的所有安装说明。
完成所有必要步骤后,pip install tensorflow-gpu将安装针对 GPU 计算优化的 TensorFlow 包。
Keras
Keras 是使用 Python 编写的极简和高度模块化的神经网络库,能够在 TensorFlow(由 Google 发布的用于数值计算的源软件库)以及 Microsoft Cognitive Toolkit(以前称为 CNTK),Theano 或 MXNet。 它的主要开发者和维护者是 FrançoisChollet,他是 Google 的机器学习研究人员:
- 网站
- 打印时的版本:2.2.0
- 建议的安装命令:
pip install keras
或者,您可以使用以下命令来安装最新的可用版本(建议使用此包,因为该包正在持续开发中):
$> pip install git+git://github.com/fchollet/keras.git
介绍 Jupyter
该项目最初称为 IPython,由 Fernando Perez 于 2001 年作为一个免费项目启动。 通过他的工作,作者打算解决 Python 栈中的不足问题,并向公众提供用于数据调查的用户编程接口,该接口可以轻松地在数据发现和软件过程中结合科学方法(主要是实验和交互式发现) 发展。
科学的方法意味着可以以可重复的方式对不同的假设进行快速实验(数据科学中的数据探索和分析也是如此),当使用此界面时,在您的代码编写过程中,您将能够更自然地实现探索性,迭代性,反复试验和错误研究策略。
最近(在 2015 年春季期间),IPython 项目的很大一部分移到了一个名为 Jupyter 的新项目中。 这个新项目将原始 IPython 接口的潜在可用性扩展到了广泛的编程语言,例如:
有关 Jupyter 可用内核的更完整列表,请访问这个页面。
例如,一旦安装了 Jupyter 及其 IPython 内核,就可以轻松添加另一个有用的内核,例如 R 内核,以便通过同一接口访问 R 语言。 您所要做的就是安装 R,运行 R 界面,然后输入以下命令:
install.packages(c('pbdZMQ', 'devtools')) devtools::install_github('IRkernel/repr') devtools::install_github('IRkernel/IRdisplay') devtools::install_github('IRkernel/IRkernel') IRkernel::installspec()
这些命令将在您的 R 上安装 devtools 库,然后从 GitHub 中提取并安装所有必需的库(运行其他命令时您需要连接到互联网),最后在 R 安装中以及在 R 上注册 R 内核。 Jupyter。 之后,每次调用 Jupyter 笔记本时,您都可以选择运行 Python 或 R 内核,从而可以对所有数据科学项目使用相同的格式和方法。
您不能为不同的内核混合使用相同的笔记本命令。 每个笔记本仅指一个内核,即最初创建时使用的那个内核。
得益于强大的内核思想,程序可以运行由前端接口传达的用户代码,并向接口本身提供有关执行代码结果的反馈,因此无论您使用哪种语言,都可以将相同的接口和交互式编程风格用于开发。
在这种情况下,IPython 是零内核,最初的内核是零,但仍然存在,但不再打算用于引用整个项目。
因此,Jupyter 可以简单地描述为用于可通过控制台或基于 Web 的笔记本操作的交互式任务的工具,该工具提供了特殊的命令,可帮助开发人员更好地理解和构建当前正在编写的代码。
与 IDE(与编写脚本,事后运行并最终评估其结果的思想相反)相反,Jupyter 允许您以块,命名单元的形式编写代码,依次运行每个单元,并单独评估每个结果,检查文本和图形输出。 除了图形集成,它还提供可定制的命令,丰富的历史记录(JSON 格式)和计算并行性,可为您提供进一步的帮助,从而在处理大量数值计算时提高了性能。
这种方法对于涉及基于数据的代码开发的任务也特别富有成果,因为它自动完成了经常被忽视的文档编制工作,并说明了数据分析的完成方式,前提和假设以及中间结果和最终结果。 如果您的工作之一是同时介绍您的工作并说服项目的内部或外部利益相关者,那么 Jupyter 确实可以毫不费力地为您讲故事。
您可以轻松地组合代码,注释,公式,图表,交互式绘图以及诸如图像和视频之类的富媒体,使每个 Jupyter 笔记本成为一个完整的科学素描板,可以一起找到所有实验及其结果。
Jupyter 可在您喜欢的浏览器(例如,可能是 Explorer,Firefox 或 Chrome)上运行,并且在启动时会显示一个单元格,等待编写代码。单元格中包含的每个代码块都可以运行, 结果报告在单元格后面的空白处。 可以在笔记本(内联图)或单独的窗口中表示图。 在我们的示例中,我们决定内联绘制图表。
此外,可以使用 Markdown 语言(一种非常简单且易于掌握的标记语言)轻松书写笔记。 可以使用 MathJax 处理数学公式,以在 HTML/markdown 中呈现任何 LaTeX 脚本。
有几种方法可以在单元格中插入 LaTeX 代码。 最简单的方法是简单地使用 Markdown 语法,将方程式用单美元符号$表示为内联 LaTeX 公式,或用双美元符号$$表示为单行中央方程式。 请记住,要获得正确的输出,应将单元格设置为 Markdown。 这是一个例子:
在 Markdown 中:
This is a $LaTeX$ inline equation: $x = Ax+b$
And this is a one-liner: $$x = Ax + b$$
这将产生以下输出:

如果您正在寻找更复杂的内容,例如,跨越多条直线的公式,表格,应对齐的一系列方程式,或者只是使用特殊的 LaTeX 函数,那么最好使用 Jupyter 笔记本提供的%%latex魔术命令。 在这种情况下,单元格必须处于代码模式,并且包含魔术命令作为第一行。 以下几行必须定义一个可以由 LaTeX 解释器编译的完整 LaTeX 环境。
以下是一些示例,向您展示您可以做什么:
In:%%latex
[
|u(t)| =
begin{cases}
u(t) & text{if } t geq 0 \
-u(t) & text{otherwise }
end{cases}
]
这是第一个示例的输出:

In:%%latex
begin{align}
f(x) &= (a+b)^2 \
&= a^2 + (a+b) + (a+b) + b^2 \
&= a^2 + 2cdot (a+b) + b^2
end{align}
运行第二个示例时的新输出为:


请记住,通过使用%%latex魔术命令,整个单元必须符合 LaTeX 语法。 因此,如果您只需要在文本中编写一些简单的方程式,我们强烈建议您使用 Markdown 方法(由 John Gruber 在 Aaron Swartz 的帮助下为 Web 作家开发的文本到 HTML 转换工具)。
能够将技术公式集成到 markdown 中对于涉及基于数据的代码开发的任务特别有用,因为它可以自动完成通常被忽略的文档编制工作,并说明如何管理数据分析及其前提,假设,中间和中间数据。 最终结果。 如果您的工作之一是同时介绍您的工作并说服项目中的内部或外部利益相关者,那么 Jupyter 确实可以毫不费力地为您讲故事。
在网页上,有很多示例,您可能会发现其中的一些启发性工作,就像我们所做的那样。 实际上,我们不得不承认,保持清洁,最新的 Jupyter 笔记本可以节省与管理者和利益相关者见面时突然出现的不计其数的时间,这需要我们草率地介绍工作的状态。
简而言之,Jupyter 允许您执行以下操作:
- 查看分析的每个步骤的中间(调试)结果
- 仅运行代码的某些部分(或单元格)
- 以 JSON 格式存储中间结果,并能够对其执行版本控制
- 展示您的作品(这是文本,代码和图像的组合),通过 Jupyter Notebook Viewer 服务进行共享,并轻松将其导出到 HTML,PDF 甚至幻灯片
在下一节中,我们将更详细地讨论 Jupyter 的安装,并显示其在数据科学任务中的用法示例。
快速安装和首次测试使用
Jupyter 是本书中我们最喜欢的选择。 它用于使用脚本和数据及其结果来清晰有效地说明和叙述操作。
尽管我们强烈建议您使用 Jupyter,但是如果您使用的是 REPL 或 IDE,则可以使用相同的说明并期望获得相同的结果(打印格式和返回结果的扩展名除外)。
如果您的系统上未安装 Jupyter,则可以使用以下命令立即进行设置:
$> pip install jupyter
您可以在这个页面上找到有关 Jupyter 安装(涵盖不同的操作系统)的完整说明。
安装后,您可以通过从命令行调用 Jupyter 来立即开始使用 Jupyter:
$> jupyter notebook
在浏览器中打开 Jupyter 实例后,单击“新建”按钮。 在“笔记本”部分中,选择“Python 3”(该部分中可能存在其他内核,具体取决于您安装的内核)。
此时,您的新空白笔记本将如下图所示:

此时,您可以开始在第一个单元格中输入命令。 例如,您可以开始尝试在光标闪烁的单元格中键入以下内容:
In: print ("This is a test")
写入单元格后,只需按一下单元格选项卡下面的播放按钮(或作为键盘热键,您可以同时按 shift 键和输入按钮)即可运行它并获取输出。 然后,将出现另一个单元格供您输入。 在单元格中书写时,如果按菜单栏上的加号按钮,将获得一个新的单元格,并且可以使用菜单上的箭头从一个单元格移动到另一个单元格。
其他大多数功能都非常直观,我们邀请您尝试一下。 为了了解 Jupyter 的工作原理,您可以使用快速入门指南,例如这个页面,或购买专门的书籍 Jupyter 功能。
有关运行 IPython 内核时所有 Jupyter 功能的完整论述,请参阅以下 Packt Publishing 书籍:
-
Cyrille Rossant 撰写的《IPython 交互式计算和可视化秘籍》,Packt Publishing,2014 年 9 月 25 日
-
《学习用于交互式计算和数据可视化的 IPython》,Cyrille Rossant,Packt Publishing,2013 年 4 月 25 日
出于说明目的,只需考虑每个 Jupyter 指令块都有一个编号的输入语句和一个输出语句。 因此,至少在输出并非微不足道的情况下,您会发现本书的代码分为两个模块。 否则,仅期望输入部分:
In: <the code you have to enter> Out: <the output you should get>
通常,您只需要在单元格中的In:之后键入代码并运行即可。 然后,您可以将您的输出与我们可以使用Out:提供的输出进行比较,然后将我们在测试代码时在计算机上实际获得的输出进行比较。
如果您正在使用conda或env环境,则可能会在 Jupyter 界面中找不到新的环境。 如果发生这种情况,只需从命令行发出conda install ipykernel并重新启动 Jupyter 笔记本。 您的内核应出现在新建按钮下的笔记本选项中。
Jupyter 魔术命令
作为用于交互任务的特殊工具,Jupyter 提供了特殊命令,可帮助您更好地理解当前正在编写的代码。
例如,一些命令如下:
* <object>? and <object>??:这会打印<object>的详细说明(??更详细)%<function>:使用特殊的<magic function>
让我们通过一个示例来演示这些命令的用法。 我们首先使用jupyter命令启动交互式控制台,该命令用于从命令行运行 Jupyter,如下所示:
$> jupyter console
Jupyter Console 4.1.1 In [1]: obj1 = range(10)
然后,在第一行代码(由 Jupyter 标记为[1])中,我们创建 10 个数字(从 0 到 9)的列表,并将输出分配给名为obj1的对象:
In [2]: obj1?
Type: range
String form: range(0, 10)
Length: 10
Docstring:
range(stop) -> range object
range(start, stop[, step]) -> range object
Return an object that produces a sequence of integers from
start (inclusive)
to stop (exclusive) by step. range(i, j) produces i, i+1, i+2,
..., j-1.
start defaults to 0, and stop is omitted! range(4) produces 0,
1, 2, 3.
These are exactly the valid indices for a list of 4 elements.
When step is given, it specifies the increment (or decrement). In [3]: %timeit x=100
The slowest run took 184.61 times longer than the fastest.
This could mean that an intermediate result is being cached.
10000000 loops, best of 3: 24.6 ns per loop In [4]: %quickref
在下一行代码[2]中,我们使用 Jupyter 命令?检查obj1对象。 Jupyter 对对象进行内省,打印其详细信息(obj是可以生成值[1, 2, 3..., 9]和元素的范围对象),最后在范围对象上打印一些常规文档。 对于复杂的对象,使用??而不是?可以提供更详细的输出。
在[3]行中,我们将timeit魔术函数与 Python 分配一起使用(x=100)。 timeit函数多次运行该指令并存储执行该指令所需的计算时间。 最后,它显示运行 Python 函数所需的平均时间。
通过运行quickref助手函数,我们通过所有可能的特殊 Jupyter 函数的列表来完成概述,如[4]行所示。
您必须已经注意到,每次使用 Jupyter 时,如果必须在stdout上打印某些内容,就会有一个输入单元格,或者有一个可选的输出单元格。 每个输入都有编号,因此可以在 Jupyter 环境本身中引用它。 就我们的目的而言,我们无需在本书的代码中提供此类参考。 因此,我们将只报告输入和输出而没有它们的编号。 但是,我们将使用通用的In:和Out:表示法来指出输入和输出单元格。 只需将In:之后的命令复制到您自己的 Jupyter 单元中,并期待将在以下Out:上报告的输出。
因此,基本符号如下:
In:命令Out:输出(无论在何处存在并在本书中进行报告都很有用)
否则,如果我们希望您直接在 Python 控制台上操作,我们将使用以下形式:
>>> command
必要时,命令行输入和输出将编写如下:
$> command
此外,要在 Jupyter 控制台中运行bash命令,请在其前面加上!(感叹号):
In: !ls
Applications Google Drive Public Desktop
Develop
Pictures env temp
...
In: !pwd
/Users/mycomputer
直接从 Jupyter 笔记本安装包
Jupyter 魔术命令在完成不同任务方面确实非常有效,但是有时您可能会发现在 Jupyter 会话期间很难安装新包(并且由于您使用的基于conda或env的不同环境,这种情况经常发生)。 正如 Jake VanderPlas 在他的博客文章中解释的那样,《从 Jupyter 笔记本电脑安装 Python 包》,事实上,Jupyter 内核与您启动时使用的外壳不同,也就是说,当您发出魔术命令(例如!pip install numpy或!conda install --yes numpy)时,您可能会升级错误的环境 。
除非您使用笔记本计算机外壳上活动的默认 Python 内核,否则您实际上不会成功,因为 Jupyter 笔记本指向的内核与pip和conda在外壳层操作的内核不同。
在 Jupyter 笔记本下使用pip安装 NumPy 的正确方法是创建一个像这样的单元:
In: import sys
!"{sys.executable}" -m pip install numpy
相反,如果要使用conda,则必须创建以下单元格:
In: import sys
!conda install --yes --prefix "{sys.prefix}" numpy
只需将numpy替换为您想要安装并运行的任何包,即可保证安装成功。
检查新的 JupyterLab 环境
如果您想使用 JupyterLab 并想成为使用该接口的先驱,该接口将在短时间内成为标准,则可以从发出$> jupyter notebook切换为$> jupyter lab。 JupyterLab 将在您的浏览器上的http://localhost:8888地址上自动启动:

您会受到由启动器组成的用户界面的欢迎,在该界面中您可以找到许多表示为图标的启动选项(在原始界面中为菜单项),以及一系列选项卡,这些选项卡可直接访问 Google 云端硬盘上的磁盘文件 ,显示正在运行的内核和笔记本,以及配置笔记本和格式化其中的信息的命令。
基本上,它是一个高级且灵活的界面,如果您访问远程服务器上的所有此类资源,则该界面特别有用,它使您可以在同一工作台上一目了然。
Jupyter 笔记本如何帮助数据科学家
Jupyter 笔记本的主要目标是讲故事。 讲故事对于数据科学至关重要,因为您必须具备执行以下操作的能力:
- 查看正在开发的算法每一步的中间(调试)结果
- 仅运行代码的某些部分(或单元格)
- 存储中间结果并能够对其进行版本控制
- 展示您的作品(这是文本,代码和图像的组合)
Jupyter 来了; 它实际上执行了所有上述操作:
- 要启动 Jupyter 笔记本,请运行以下命令:
$> jupyter notebook
- 一个由 Jupyter 服务器实例支持的 Web 浏览器窗口将在您的桌面上弹出。 这是主窗口的样子:

- 然后,单击“新建笔记本”。 如以下屏幕截图所示,将打开一个新窗口。 内核就绪后,您就可以开始使用笔记本了。 Python 图标下方右上角的小圆圈表示内核的状态:如果已填充,则表明内核正在工作; 如果它是空的(如屏幕快照中的那个),则意味着内核处于空闲状态,即可以运行任何代码:

这是您用来撰写故事的网络应用。 它与 Python IDE 非常相似,底部(您可以在其中编写代码)由单元格组成。
单元格可以是一段文本(最终使用一种标记语言格式化)或一段代码。 在第二种情况下,您可以运行代码,并且所有最终输出(标准输出)都将放置在单元格下面。 以下是一个非常简单的示例:
In: import random
a = random.randint(0, 100)
a Out: 16 In: a*2 Out: 32
在以In:表示的第一个单元格中,我们导入随机模块,将0和100之间的随机值分配给变量a,然后打印该值。 运行此单元格时,输出表示为Out:,是随机数。 然后,在下一个单元格中,我们将只打印变量a的值的两倍。
如您所见,它是调试和确定哪个参数最适合给定操作的好工具。 现在,如果我们在第一个单元格中运行代码会怎样? 因为a不同,第二个单元的输出会被修改吗? 实际上,不,不会。 每个单元都是独立且自治的。 实际上,在第一个单元格中运行代码后,我们最终得到以下不一致状态:
In: import random
a = random.randint(0, 100)
a Out: 56 In: a*2 Out: 32
请注意,括号中的数字已更改(从 1 到 3),因为它是笔记本启动时执行的第三个命令(及其输出)。 由于每个单元都是自治的,因此通过查看这些数字,您可以了解它们的执行顺序。
Jupyter 是一种简单,灵活且功能强大的工具。 但是,如前面的示例所示,必须注意,当更新稍后将在笔记本中使用的变量时,请记住按照更新后的代码运行所有单元格,以使状态保持一致。
保存 Jupyter 笔记本时,生成的.ipynb文件为 JSON 格式,其中包含所有单元格及其内容以及输出。 这使事情变得更容易,因为您无需运行代码即可查看笔记本(实际上,您也不需要安装 Python 及其工具集)。 这非常方便,尤其是当输出中包含图片且代码中包含一些非常耗时的例程时。 使用 Jupyter 笔记本的不利之处在于,其文件格式为 JSON 结构,因此人们无法轻易读取。 实际上,它包含图像,代码,文本等。
现在,让我们讨论一个与数据科学相关的示例(不用担心完全理解它):
In: %matplotlib inline
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
在以下单元格中,导入了一些 Python 模块:
In: boston_dataset = datasets.load_boston()
X_full = boston_dataset.data
Y = boston_dataset.target
print (X_full.shape)
print (Y.shape) Out:(506, 13)
(506,)
然后,在cell [2]中加载数据集,并显示其形状的指示。 数据集包含在波士顿郊区出售的506房屋价值,以及按列排列的各自数据。 数据的每一列代表一个特征。 特征是观察的特征。 机器学习使用特征来建立可以将其转化为预测的模型。 如果您来自统计学背景,则可以添加可以用作变量的特征(值随观察值而变化)。
要查看数据集的完整说明,请使用print boston_dataset.DESCR。
加载观测值及其特征之后,为了演示 Jupyter 如何有效地支持数据科学解决方案的开发,我们将对数据集进行一些转换和分析。 我们将使用诸如SelectKBest之类的类和诸如.getsupport()或.fit()之类的方法。 不用担心这些现在还不清楚。 本书稍后将对它们进行全面的介绍。 尝试运行以下代码:
In: selector = SelectKBest(f_regression, k=1)
selector.fit(X_full, Y)
X = X_full[:, selector.get_support()]
print (X.shape) Out:(506, 1)
对于In:,我们选择SelectKBest类的一项特征(最具区别性的一项),该特征通过使用.fit()方法适合数据。 因此,我们借助通过对所有行和所选特征建立索引进行选择的操作将数据集简化为向量,可以通过.get_support()方法检索该索引。
由于目标值是向量,因此,我们可以尝试查看输入(特征)和输出(房屋价值)之间是否存在线性关系。 当两个变量之间存在线性关系时,输出将以相同的比例量和方向不断对输入的变化做出反应:
In: def plot_scatter(X,Y,R=None):
plt.scatter(X, Y, s=32, marker='o', facecolors='white')
if R is not None:
plt.scatter(X, R, color='red', linewidth=0.5)
plt.show() In: plot_scatter(X,Y)
以下是执行上述命令后获得的输出:

在我们的示例中,随着X增加,Y减少。 但是,这不会以恒定的速率发生,因为变化率会一直升高到一定的X值,然后降低并变为恒定。 这是非线性的条件,我们可以使用回归模型将其进一步可视化。 该模型假设X和Y之间的关系是线性的,形式为y = a + bX。 根据特定标准估计其a和b参数。
在第四个单元格中,我们分散了此问题的输入和输出值:
In: regressor = LinearRegression(normalize=True).fit(X, Y)
plot_scatter(X, Y, regressor.predict(X))
以下是执行上述代码后获得的输出:

在下一个单元格中,我们创建一个回归器(具有特征归一化的简单线性回归),训练回归器,最后绘制输入和输出之间的最佳线性关系(即回归器的线性模型)。 显然,线性模型不是很好的近似值。 在这一点上,我们有两种可能的方法。 我们可以转换变量以使其线性关系,或者可以使用非线性模型。 支持向量机(SVM)是一类可以轻松解决非线性问题的模型。 同样,随机森林是用于自动解决类似问题的另一种模型。 让我们看看它们在 Jupyter 中的作用:
In: regressor = SVR().fit(X, Y)
plot_scatter(X, Y, regressor.predict(X))
以下是执行上述代码后获得的输出:

现在我们继续使用更复杂的算法,即随机森林回归器:
In: regressor = RandomForestRegressor().fit(X, Y) plot_scatter(X, Y, regressor.predict(X))
以下是执行上述代码后获得的输出:

最后,在最后两个单元格中,我们将重复相同的过程。 这次,我们将使用两种非线性方法:SVM 和基于随机森林的回归器。
该演示代码解决了非线性问题。 此时,通过简单地修改脚本所在的单元格来更改所选特征,回归变量以及用于训练模型的特征数非常容易。 一切都可以交互进行,根据我们看到的结果,我们可以决定应该保留或更改什么以及下一步要做什么。
Jupyter 的替代品
如果您不喜欢使用 Jupyter,实际上有一些替代方法可以帮助您测试本书中的代码。 如果您有使用 R 的经验,RStudio 布局可能会更吸引您。 在这种情况下,为决策 API 提供数据科学解决方案的公司 Yhat 免费提供了其针对 Python 的数据科学 IDE,名为 Rodeo。 Rodeo 通过在后台使用 Jupyter 的 IPython 内核进行工作,但是鉴于其不同的用户界面,它是一个有趣的选择。
使用 Rodeo 的主要优点如下:
- 在四个 Windows 中排列的视频布局:编辑器,控制台,绘图和环境
- 编辑器和控制台的自动完成
- 绘图始终在特定窗口中的应用内部可见
- 您可以在环境窗口中轻松检查工作变量
可以使用安装程序轻松安装 Rodeo。 您可以从其网站下载它,也可以在命令行中简单地使用以下内容:
$> pip install rodeo
安装后,您可以使用以下命令立即运行 Rodeo IDE:
$> rodeo .
相反,如果您有 MathWorks 的 MATLAB 经验,就会发现使用 Spyder 更容易,这是主要的 Scientific Python 中都可以找到的科学 IDE。 发行版(在 Anaconda,WinPython 和 PythonXY 中存在-我们在本书中建议的所有发行版)。 如果不使用发行版,则要安装 Spyder,必须遵循这个页面上的说明。 Spyder 允许进行高级编辑,交互式编辑,调试和自省功能,并且您的脚本可以在 Jupyter 控制台或类似 Shell 的环境中运行。
本书中使用的数据集和代码
在我们逐步理解本书中介绍的概念时,为了促进读者的理解,学习和记忆过程,我们将在各种解释性数据集上说明实用且有效的数据科学 Python 应用。 读者将始终能够根据我们将在本书中使用的数据立即复制,修改和试验建议的说明和脚本。
至于您将在本书中找到的代码,我们将把讨论限制在最基本的命令上,以便从使用 Python 的数据科学之旅的开始就激发您,从而通过利用 Python 的关键功能来事半功倍。 我们事先提供的包。
鉴于我们之前的介绍,我们将展示要在 Jupyter 控制台或笔记本上交互运行的代码。
所有提供的代码将在笔记本中提供,该笔记本可在 Packt 网站上找到(“前言”中指出)。 至于数据,我们将提供不同的数据集示例。
Scikit-learn 玩具数据集
Scikit-learn 玩具数据集模块嵌入在 Scikit-learn 包中。 这样的数据集可以通过import命令轻松地直接加载到 Python 中,并且不需要从任何外部互联网资源库中下载任何内容。 这种数据集的一些示例是鸢尾花,波士顿和数字数据集,以列举无数出版物和书籍中提到的主要数据集,以及一些用于分类和回归的其他经典数据集。
除了特征和目标变量外,它们还构造在类似于字典的对象中,它们提供了数据本身的完整描述和上下文信息。
例如,要加载鸢尾花数据集,请输入以下命令:
In: from sklearn import datasets
iris = datasets.load_iris()
加载数据集后,我们可以浏览数据描述并了解特征和目标的存储方式。 所有 Scikit-learn 数据集都提供以下方法:
.DESCR:这提供了数据集的一般描述.data:包含所有特征.feature_names:此报告特征的名称.target:包含目标值,以值或编号的类别表示.target_names:这将报告目标中的类的名称.shape:这是一种可以同时应用于.data和.target的方法; 它报告存在的观测值的数量(第一个值)和特征(第二个值,如果存在)
现在,让我们尝试实现它们(没有输出报告,但是print命令将为您提供大量信息):
In: print (iris.DESCR)
print (iris.data)
print (iris.data.shape)
print (iris.feature_names)
print (iris.target)
print (iris.target.shape)
print (iris.target_names)
您应该了解有关数据集的其他信息-存在多少示例和变量,以及它们的名称是什么。 注意,鸢尾花对象中包含的主要数据结构是两个数组,数据和目标:
In: print (type(iris.data)) Out: <class 'numpy.ndarray'>
Iris.data提供名为sepal length,sepal width,petal length和petal width的变量的数值,它们以矩阵形式(150, 4)排列,其中 150 是观察数,4 是特征数量。 变量的顺序是iris.feature_names中显示的顺序。
Iris.target是整数的向量,其中每个数字代表一个不同的类(请参阅target_names的内容;每个类名称都与其索引号和山鸢尾相关,它是列表中的零元素,在目标向量中表示为0。
Iris flower数据集由现代统计分析之父之一的罗纳德·费舍尔(Ronald Fisher)于 1936 年首次使用,目的是通过一小部分可通过经验验证的示例(150 个数据点的每个代表鸢尾花)展示线性判别分析的功能。 这些示例被安排在树木平衡的物种类别中(每个类别由三分之一的示例组成),并提供了四个度量描述变量,这些变量在组合时能够分离类别。
使用这种数据集的优势在于,从监督学习到图形表示,可以很容易地出于不同目的加载,处理和探索。 无论其规格如何,在任何计算机上进行建模活动几乎都不会花费时间。 而且,类与显式变量的作用之间的关系是众所周知的。 因此,任务是艰巨的,但不是很艰巨。
例如,让我们观察一下当您希望通过使用散点图矩阵组合四个可用变量中的至少两个时,如何轻松地分离类。
散点图矩阵以矩阵格式排列,其列和行是数据集变量。 矩阵的元素包含单个散点图,其x值由矩阵的行变量确定,y值由列变量确定。 矩阵的对角元素可以同时在其行和列中包含分布直方图或变量的其他单变量表示形式。
Pandas 库提供了一种现成的功能,可快速构建散点图矩阵并开始探索数据集中定量变量之间的关系和分布:
In: import pandas as pd
import numpy as np
colors = list()
palette = {0: "red", 1: "green", 2: "blue"} In: for c in np.nditer(iris.target): colors.append(palette[int(c)])
# using the palette dictionary, we convert
# each numeric class into a color string
dataframe = pd.DataFrame(iris.data, columns=iris.feature_names) In: sc = pd.scatter_matrix(dataframe, alpha=0.3, figsize=(10, 10),
diagonal='hist', color=colors, marker='o', grid=True)
以下是执行上述代码后获得的输出:

我们鼓励您在处理其他复杂的真实数据之前,对该数据集和类似数据集进行大量试验,因为专注于可访问的,非平凡的数据问题的优势在于,它可以帮助您快速建立数据科学基础 。
无论如何,尽管它们对于您的学习活动有用且有趣,但是玩具数据集将开始限制您可以实现的各种不同实验的种类。 尽管提供了见解,但为了取得进展,您将需要访问复杂而现实的数据科学主题。 因此,我们将不得不求助于一些外部数据。
MLdata.org 和其他开源数据的公共存储库
我们将提供的第二种示例数据集可以直接从机器学习数据集存储库或 LIBSVM 数据网站下载。 与以前的数据集相反,在这种情况下,您将需要访问互联网。
首先,mldata.org是机器学习数据集的公共存储库,由柏林工业大学托管,并由模式分析,统计建模和计算学习(PASCAL)支持。 网络由欧盟资助。 您可以从该存储库免费下载任何数据集并进行试验。
例如,如果您需要下载美国地质调查局报告的自 1972 年以来与地震有关的所有数据,为了分析数据以寻找预测模式,您可以在这个页面(在这里,您会找到有关数据的详细说明)。
注意,包含数据集的目录为global-earthquakes; 您可以使用以下命令直接获取数据:
In: from sklearn.datasets import fetch_mldata
earthquakes = fetch_mldata('global-earthquakes')
print (earthquakes.data)
print (earthquakes.data.shape) Out: (59209L, 4L)
与 Scikit-learn 包装玩具数据集一样,获得的对象是一个类似于字典的复杂结构,其中您的预测变量为earthquakes.data,而您要预测的目标为earthquakes.target。 这是真实的数据,在这种情况下,您将有很多示例,只有几个可用变量。
LIBSVM 数据示例
LIBSVM 数据是一个页面,可从许多其他集合中收集数据。 它由 LIBSVM 的作者之一 Chih-Jen LiN 维护,LIBSVM 是一种用于预测的支持向量机学习算法(Chih-Chung Chang and Chih-Jen Lin, LIBSVM: a library for support vector machines. ACM Transactions on Intelligent Systems and Technology, 2:27:1--27:27, 2011)。 这提供了以 LIBSVM 格式存储的不同回归,二进制和多标签分类数据集。 如果您想尝试使用支持向量机的算法,该存储库将非常有趣,并且再次免费供您下载和使用数据。
如果要加载数据集,请首先转到网页,您可以在该网页上可视化浏览器上的数据。 在我们的示例中,请访问这个页面并记下地址(a1a是数据集 (最初来自 UC Irvine 机器学习存储库,另一个开源数据存储库)。 然后,您可以使用该地址执行直接下载:
In: import urllib2
url =
'http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary/a1a'
a2a = urllib2.urlopen(url) In: from sklearn.datasets import load_svmlight_file
X_train, y_train = load_svmlight_file(a2a)
print (X_train.shape, y_train.shape)
Out: (1605, 119) (1605,)
作为回报,您将获得两个单独的对象:一组稀疏矩阵格式的训练示例和一组响应。
直接从 CSV 或文本文件加载数据
有时,您可能必须使用 Web 浏览器或wget命令(在 Linux 系统上)直接从其存储库下载数据集。
如果您已经将数据下载并解压缩(如果需要)到工作目录中,则 NumPy 和 Pandas 库及其各自的loadtxt和read_csv函数提供了加载数据并开始工作的最简单方法。
例如,如果您打算分析波士顿的住房数据并使用这个页面中提供的版本,则必须首先在您本地目录中下载regression-datasets-housing.csv文件。
由于数据集中的变量都是数字变量(13 个连续和一个二进制),因此加载和开始使用它的最快方法是尝试使用loadtxt NumPy 函数并将所有数据直接加载到数组中。
即使在现实的数据集中,您也经常会发现混合类型的变量,可以通过pandas.read_table或pandas.read_csv解决。 然后可以通过values方法提取数据; 如果您的数据已经是数字,loadtxt可以节省大量内存。 实际上,loadtxt命令不需要任何内存中的复制:
In: housing = np.loadtxt('regression-datasets-housing.csv',
delimiter=',')
print (type(housing)) Out: <class 'numpy.ndarray'> In: print (housing.shape) Out: (506, 14)
默认情况下,loadtxt函数期望将制表符作为文件上的值之间的分隔符。 如果分隔符是冒号(,)或分号(;),则必须使用参数定界符使其明确:
>>> import numpy as np >>> type(np.loadtxt)
<type 'function'> >>> help(np.loadtxt)
可以在numpy.lib.npyio模块中找到loadtxt函数的帮助。
另一个重要的默认参数是dtype,它设置为float。
这意味着loadtxt将强制所有加载的数据转换为浮点数。
如果需要确定其他类型(例如int),则必须事先声明它。
例如,如果要将数字数据转换为int,请使用以下代码:
In: housing_int =housing.astype(int)
打印housing和housing_int数组的行的前三个元素可以帮助您了解不同之处:
In: print (housing[0,:3], 'n', housing_int[0,:3]) Out: [ 6.32000000e-03 1.80000000e+01 2.31000000e+00]
[ 0 18 2]
通常,尽管在我们的示例中情况并非总是如此,但文本标题第一行中的文件数据特征包含变量的名称。 在这种情况下,被跳过的参数将指出loadtxt文件中的行,从该行开始读取数据。 作为0行的标头(在 Python 中,计数始终从 0 开始),skip=1参数将节省一天的时间,并允许您避免错误并无法加载数据。
如果要下载位于这个页面的鸢尾花数据集,情况将略有不同。 实际上,此数据集提供了定性目标变量class,它是表达鸢尾花种类的字符串。 具体来说,它是具有四个级别的类别变量。
因此,如果要使用loadtxt函数,则将出现值错误,因为数组必须具有相同类型的所有元素。 变量类是一个字符串,而其他变量则由浮点值组成。
由于DataFrame数据结构可以轻松处理由不同类型的变量组成的矩阵形式(每列行)的数据集,因此 Pandas 库为这种情况和许多类似情况提供了解决方案。
首先,只需下载datasets-uci-iris.csv文件并将其保存在本地目录中。
可以从这个页面下载数据集。 该档案库是 UC Irvine 机器学习存储库,目前存储着 440 个数据集,作为对机器学习社区的服务。 除了该鸢尾花数据集之外,您还可以自由下载并尝试使用存储库中存在的任何其他数据集。
此时,使用 Pandas 的read_csv非常简单:
In: iris_filename = 'datasets-uci-iris.csv'
iris = pd.read_csv(iris_filename, sep=',', decimal='.',
header=None, names= ['sepal_length', 'sepal_width', \
'petal_length', 'petal_width', 'target'])
print (type(iris)) Out: <class 'pandas.core.frame.DataFrame'>
为了不使本书中印刷的代码片段过于繁琐,我们经常对其进行包装并使其格式正确。 必要时,为了安全地中断代码并将其包装到新行,我们使用反斜杠符号\,如前面的代码示例中所示。 当您自己渲染本书的代码时,您可以忽略反斜杠符号并继续在同一行上编写所有指令,或者可以对反斜杠进行数字化后再开始一行,并继续执行代码指令。 请注意,输入反斜杠然后在同一行上继续执行指令将导致执行错误。
除了文件名之外,您还可以指定分隔符(sep),小数点的表示方式(十进制),是否有标头(在这种情况下为header=None;通常,如果您有标头,则header=0),以及该变量的名称(您可以使用列表;否则,Pandas 将提供一些自动命名)。
另外,我们定义的名称使用单个单词(而不是空格,而是使用下划线)。 因此,我们以后可以像调用方法一样直接调用它们来直接提取单个变量。 例如,iris.sepal_length将提取萼片长度数据。
此时,如果您需要将 Pandas DataFrame转换为几个包含数据和目标值的 NumPy 数组,则可以通过以下命令轻松完成:
In: iris_data = iris.values[:,:4]
iris_target, iris_target_labels = pd.factorize(iris.target)
print (iris_data.shape, iris_target.shape) Out: (150, 4) (150,)
Scikit-learn 样本生成器
作为最后的学习资源,Scikit-learn 包还提供了快速创建用于回归,二进制和多标签分类,聚类分析和降维的综合数据集的可能性。
重复合成数据的主要优点在于在您的 Python 控制台的工作内存中的瞬时创建。 因此,可以创建更大的数据示例,而不必进行从互联网进行长时间的下载会话(并且可以在磁盘上节省很多内容)。
例如,您可能需要处理涉及一百万个数据点的分类问题:
In: from sklearn import datasets
X,y = datasets.make_classification(n_samples=10**6,
n_features=10, random_state=101)
print (X.shape, y.shape) Out: (1000000, 10) (1000000,)
在仅导入数据集模块之后,我们使用make_classification命令要求提供一百万个示例(n_samples参数)和 10 个有用的特征(n_features)。 random_state应该是101,因此我们确信可以在不同的时间和不同的机器上复制相同的数据集。
例如,您可以键入以下命令:
In: datasets.make_classification(1, n_features=4, random_state=101)
这将始终为您提供以下输出:
Out: (array([[-3.31994186, -2.39469384, -2.35882002, 1.40145585]]),
array([0]))
不论计算机和具体情况如何,random_state都可以确保确定性的结果,使您的实验可以完美地复制。
使用特定的整数定义random_state参数(在本例中为101,但是它可能是您喜欢或认为有用的任何数字),可以轻松复制你的计算机上的相同的数据集,以及它的配置方式,到不同的操作系统和不同的机器上。
顺便说一下,花了太长时间吗?
在 2.20 GHz 的 i3-2330M CPU 上,需要执行以下操作:
In: %timeit X,y = datasets.make_classification(n_samples=10**6,
n_features=10, random_state=101) Out: 1 loops, best of 3: 1.17 s per loop
如果看起来您的计算机似乎并不需要花费太长时间,并且如果您已经准备好了,到目前为止已经完成了所有的设置和测试,那么我们可以开始我们的数据科学之旅。
总结
在本介绍性章节中,我们安装了本书中将使用的所有内容,从 Python 包到示例。 它们既可以直接安装也可以通过科学分发安装。 我们还介绍了 Jupyter 笔记本,并在教程中演示了如何访问运行的数据。
在下一章“数据整理”中,我们将概述数据科学管道,并探索所有关键工具来处理和准备数据,然后再应用任何学习算法并设置假设实验时间表。
二、数据整理
我们只是将数据付诸行动! 在本章中,您将学习如何处理数据。 数据处理是什么意思?
术语整理(Munging)是大约半个世纪前由麻省理工学院(MIT)的学生创造的技术术语。 整理的意思是按照一系列明确规定的和可逆的步骤将原始数据更改为完全不同的(希望是更有用的)原始数据。 根深蒂固于黑客文化中,在数据科学流水线中通常使用其他几乎同义的术语(例如数据争用或数据准备)来描述整理。
在这种前提下,本章将讨论以下主题:
- 数据科学过程(这样您就可以知道发生了什么以及下一步是什么)
- 从文件上传数据
- 选择您需要的数据
- 清理任何丢失或错误的数据
- 添加,插入和删除数据
- 分组和转换数据来获得新的有意义的信息
- 管理获取数据集矩阵或数组以馈入数据科学管道
数据科学过程
尽管每个数据科学项目都是不同的,但是出于说明目的,我们可以将理想的数据科学项目划分为一系列简化和简化的阶段。
该过程从获取数据开始(一个阶段,称为数据摄取)。 数据摄取意味着一系列可能的选择,从简单地上传数据到从 RDBMS 或 NoSQL 存储库组装数据,或者从综合生成数据到从 Web API 或 HTML 页面抓取数据。
特别是在面对新挑战时,上传数据可以证明自己是数据科学家工作的关键部分。 您的数据可以来自多种来源:数据库,CSV 或 Excel 文件,原始 HTML,图像,录音,API(如果您对 API 的含义一无所知,则可以在此处阅读有关使用 Python 的 API 的很好的教程)提供 JavaScript 对象表示法(JSON)文件,依此类推。 鉴于存在多种选择,我们将通过提供基本工具来简要介绍这一方面,方法是使用硬盘上存在的文本文件或将 Web 或关系数据库管理系统(RDBMS)中的表格。
成功上传数据后,进入数据整理阶段。 尽管现在已经可以在内存中使用,但您的数据肯定会以不适合任何分析和实验的形式出现。 现实世界中的数据非常复杂,混乱,有时甚至是错误或丢失。 但是,由于有了一系列基本的 Python 数据结构和命令,您将解决所有有问题的数据,并将其输入到项目的下一个阶段,并适当地转换为一个典型的数据集,该数据集的行和列的变量均具有观察值。 数据集是任何统计和机器学习分析的基本要求,您可能会听到它被提及为平面文件(当它是将数据库中多个关系表连接在一起的结果时)或数据矩阵(当列和行为未标记,并且其中包含的值只是数字)。
尽管与其他刺激性阶段(例如应用算法或机器学习)相比,数据回馈的回报不高,但是数据处理为您可能想获得的每个复杂而复杂的增值分析奠定了基础。 项目的成功很大程度上取决于它。
完全定义了要使用的数据集后,一个新的阶段打开了。 这时,您将开始观察数据。 然后,您将在一个循环中继续发展和检验您的假设。 例如,您将以图形方式浏览变量。 借助描述性统计信息,您将了解如何通过将领域知识付诸实践来创建新变量。 您将处理冗余和意外信息(首先是异常值),并选择最有意义的变量和有效参数,以通过多种机器学习算法进行测试。
此阶段被构造为管道,其中根据一系列步骤处理您的数据。 之后,最终创建了一个模型,但是您可能意识到必须重申并从数据整理或数据管道中的某处重新开始,提供更正或尝试不同的实验,直到获得有意义的结果。
根据我们在该领域的经验,我们可以向您保证,无论您的计划在开始分析数据时有多么有希望,最终,您的解决方案都将与任何最初设想的想法有很大不同。 与实验结果相抵触,您将获得规则,包括数据处理,优化,模型以及在达到令人满意的项目效果之前必须经过的总迭代次数。 这就是为什么如果您想成为一名成功的数据科学家,仅提供理论上合理的解决方案根本是不够的。 必须能够在最快的时间内快速建立大量可能的解决方案的原型,以便确定哪一条是最佳途径。 我们的目的是通过在数据科学过程中使用本书提供的代码段帮助您最大程度地加速。
项目的结果由错误或优化措施(为了表示业务目标而精心选择)表示。 除了误差度量之外,您的成就还可以通过可解释的见解来传达,该见解必须以口头或视觉方式描述给数据科学项目的发起者或其他数据科学家。 在这一点上,使用表格,图表和图表适当地可视化结果和见解确实是必不可少的。
也可以使用首字母缩写词 OSEMN(获取,清理,探索,建模,解释),由 Hilary Mason 和 Chris Wiggins 在博客 dataists,描述了数据科学分类法。 OSEMN 也很令人难忘,因为它与possum和awesome押韵:

我们永远不会厌倦于说明一切都是从整理数据开始的,并且整理可能很容易需要数据项目中多达 80% 的工作。 由于即使是最长的旅程也只有一步之遥,所以让我们立即进入本章,学习成功的调整阶段的基础!
Pandas 数据加载和预处理
在上一章中,我们讨论了在哪里找到有用的数据集,并检查了 Python 包的基本导入命令。 在本节中,将工具箱准备好,您将学习如何使用 Pandas 和 NumPy 在结构上加载,操纵,处理和抛光数据。
快速简便的数据加载
让我们从 CSV 文件和 Pandas 开始。 Pandas 库提供了最可访问且最完整的功能,可以从文件(或 URL)中加载表格数据。 默认情况下,它将以专用的 Pandas 数据结构存储数据,为每一行建立索引,通过自定义分隔符分隔变量,为每一列推断正确的数据类型,转换数据(如有必要),以及解析日期,缺失值和错误的值。
我们将首先导入 Pandas 包并阅读我们的Iris数据集:
In: import pandas as pd
iris_filename = 'datasets-uci-iris.csv'
iris = pd.read_csv(iris_filename, sep=',', decimal='.', header=None,
names= ['sepal_length', 'sepal_width',
'petal_length', 'petal_width',
'target'])
您可以指定文件名,用作分隔符的字符(sep),用于十进制占位符的字符(decimal),是否有头文件(header)和变量名( 使用names和列表)。 sep=','和decimal='.'参数的设置为默认值,并且在功能上是多余的。 对于欧式 CSV,必须指出两者,因为在许多欧洲国家/地区,分隔符和小数位占位符与默认值不同。
如果无法在线获取数据集,则可以按照以下步骤从互联网下载数据集:
In: import urllib
url = "http://aima.cs.berkeley.edu/data/iris.csv"
set1 = urllib.request.Request(url)
iris_p = urllib.request.urlopen(set1)
iris_other = pd.read_csv(iris_p, sep=',', decimal='.',
header=None, names= ['sepal_length', 'sepal_width',
'petal_length', 'petal_width',
'target'])
iris_other.head()
生成的名为iris的对象是 Pandas DataFrame。 它不仅仅是一个简单的 Python 列表或字典,而且在以下各节中,我们将探讨其某些功能。 要了解其内容,可以使用以下命令打印第一(或最后)行:
In: iris.head()
DataFrame的头部将打印在输出中:

In: iris.tail()
如果不带参数调用该函数,则将打印五行。 如果要返回不同的行数,只需使用要查看的行数作为参数来调用该函数,如下所示:
In: iris.head(2)
前面的命令将仅打印前两行。 现在,要获取列的名称,只需使用以下方法:
In: iris.columns Out: Index(['sepal_length', 'sepal_width',
'petal_length', 'petal_width',
'target'], dtype='object')
生成的对象是一个非常有趣的对象。 它看起来像一个列表,但实际上是一个 Pandas 索引。 正如对象名称所建议的那样,它索引列的名称。 例如,要提取目标列,只需执行以下操作:
In: y = iris['target']
y Out: 0 Iris-setosa
1 Iris-setosa
2 Iris-setosa
3 Iris-setosa
...
149 Iris-virginica
Name: target, dtype: object
对象y的类型是 Pandas 序列。 现在,将其视为带有轴标签的一维数组,我们稍后将对其进行深入研究。 现在,我们刚刚了解到,Pandas Index类的作用类似于表中各列的字典索引。 请注意,您还可以获得按其索引引用它们的列的列表,如下所示:
In: X = iris[['sepal_length', 'sepal_width']]
X Out: [150 rows x 2 columns]
以下是X数据集的四行:

这是四个尾巴:

在这种情况下,结果是一个 Pandas DataFrame。 使用相同功能时,结果为何如此不同? 在第一种情况下,我们要求提供一列。 因此,输出为一维向量(即 Pandas 序列)。 在第二个示例中,我们询问了多列,并获得了类似矩阵的结果(并且我们知道矩阵被映射为 Pandas DataFrame)。 新手阅读者只需查看输出标题即可发现差异。 如果列被标记,则说明您正在处理 Pandas DataFrame。 另一方面,如果结果是向量,并且不显示标题,则为 Pandas 序列。
到目前为止,我们已经从数据科学过程中学到了一些常见步骤; 加载数据集后,通常会分离特征和目标标签。
在分类问题中,目标标签是指示与每组特征关联的类的序数或文本字符串。
然后,以下步骤要求您了解问题的严重程度,因此,您需要了解数据集的大小。 通常,对于每个观察,我们计算一条线,对于每个特征,计算一列。
要获取数据集的维度,只需在 Pandas DataFrame或序列上使用属性形状,如以下示例所示:
In: print (X.shape) Out: (150, 2) In: print (y.shape) Out: (150,)
生成的对象是一个元组,其中包含每个维度中矩阵/数组的大小。 另外,请注意,Pandas 序列遵循相同的格式(即只有一个元素的元组)。
处理有问题的数据
现在,您应该对流程的基础更加自信,并准备好面对问题更大的数据集,因为现实中通常会出现混乱的数据。 因此,让我们看看如果 CSV 文件包含标头以及一些缺少的值和日期会发生什么。 例如,为了使我们的示例更现实,让我们想象一下旅行社的情况:
- 根据三个热门目的地的温度,他们记录用户是选择第一个,第二个还是第三个目的地:
Date,Temperature_city_1,Temperature_city_2,Temperature_city_3,Which_destination 20140910,80,32,40,1 20140911,100,50,36,2 20140912,102,55,46,1 20140912,60,20,35,3 20140914,60,,32,3 20140914,,57,42,2
- 在这种情况下,所有数字都是整数,并且标头位于文件中。 在我们第一次尝试加载该数据集时,我们可以提供以下命令:
In: import pandas as pd In: fake_dataset = pd.read_csv('a_loading_example_1.csv', sep=',')
fake_dataset
打印fake_dataset的第一行:

从第一个数据行中选取它们后,Pandas 会自动为它们提供实际名称。 我们首先发现一个问题:所有数据,甚至日期,都被解析为整数(或者在其他情况下,解析为字符串)。 如果日期格式不是很奇怪,则可以尝试使用自动检测例程,该例程指定包含日期数据的列。 在以下示例中,当使用以下参数时,它可以很好地工作:
In: fake_dataset = pd.read_csv('a_loading_example_1.csv',
parse_dates=[0])
fake_dataset
这是fake_dataset,其日期列现在已由read_csv正确解释:

现在,为了摆脱NaN指示的缺失值,请用更有意义的数字(例如 50 华氏度)替换它们。 我们可以通过以下方式执行命令:
In: fake_dataset.fillna(50)
在这一点上,您将注意到不再缺少变量:

之后,所有丢失的数据都消失了,并已由常量50.0代替。 处理丢失的数据也可能需要不同的方法。 作为先前命令的替代方法,可以用负常数值替换值,以标记它们与其他值不同的事实(并由学习算法来猜测):
In: fake_dataset.fillna(-1)
请注意,此方法仅填充数据视图中的缺失值(也就是说,它不会修改原始DataFrame)。 为了实际更改它们,请使用inplace=True argument命令。
NaN值也可以用列的平均值或中值代替,以最大程度地减少猜测误差:
In: fake_dataset.fillna(fake_dataset.mean(axis=0))
.mean方法计算指定轴的平均值。
请注意,axis= 0表示对跨越行的均值的计算; 结果获得的均值是从逐列计算得出的。 相反,axis=1跨列,因此获得了逐行结果。 对于其他要求使用axis参数的其他方法(在 Pandas 和 NumPy 中),其工作方式相同。
.median方法类似于.mean,但是它计算中值,如果分布太偏斜(例如,有很多极限值的时候)。
处理实际数据集时,另一个可能的问题是加载包含错误或错误行的数据集时。 在这种情况下,read_csv方法的默认行为是停止并引发异常。 一种可能的解决方法是忽略导致错误的行,这在错误的示例不是多数的情况下是可行的。 在许多情况下,这种选择仅具有训练机器学习算法而没有错误观察的含义。 举个例子,假设您有一个格式错误的数据集,并且您只想加载所有好的行而忽略格式错误的行。
现在,这是您的a_loading_example_2.csv文件:
Val1,Val2,Val3 0,0,0 1,1,1 2,2,2,2 3,3,3
这是您可以使用error_bad_lines选项执行的操作:
In: bad_dataset = pd.read_csv('a_loading_example_2.csv',
error_bad_lines=False)
bad_dataset Out: Skipping line 4: expected 3 fields, saw 4
结果输出的第四行被跳过,因为它具有四个值而不是三个值:

处理大型数据集
如果要加载的数据集太大而无法容纳在内存中,则可以使用批量机器学习算法来处理它,该算法一次只能处理部分数据。 如果您只需要数据样本(假设您要查看数据),则使用批量方法也很有意义。 多亏了 Python,您实际上可以分块加载数据。 此操作也称为数据流,因为数据集作为连续流式流入DataFrame或某些其他数据结构。 与以前的所有情况相反,该数据集已在独立步骤中完全加载到内存中。
对于 Pandas,有两种方法可以分块和加载文件。 第一种方法是将数据集以相同大小的块加载; 每个块都是数据集的一部分,包含所有列和有限数量的行,最多不超过您在函数调用中实际设置的数量(chunksize参数)。 请注意,在这种情况下,read_csv函数的输出不是 Pandas DataFrame,而是类似迭代器的对象。 实际上,要将结果存储在内存中,您需要迭代该对象:
In: import pandas as pd
iris_chunks = pd.read_csv(iris_filename, header=None,
names=['C1', 'C2', 'C3', 'C4', 'C5'],
chunksize=10)
for chunk in iris_chunks:
print ('Shape:', chunk.shape)
print (chunk,'n') Out: Shape: (10, 5)
C1 C2 C3 C4 C5
0 5.1 3.5 1.4 0.2 Iris-setosa
1 4.9 3.0 1.4 0.2 Iris-setosa
2 4.7 3.2 1.3 0.2 Iris-setosa
3 4.6 3.1 1.5 0.2 Iris-setosa
4 5.0 3.6 1.4 0.2 Iris-setosa
5 5.4 3.9 1.7 0.4 Iris-setosa
6 4.6 3.4 1.4 0.3 Iris-setosa
7 5.0 3.4 1.5 0.2 Iris-setosa
8 4.4 2.9 1.4 0.2 Iris-setosa
9 4.9 3.1 1.5 0.1 Iris-setosa
...
还将有其他 14 个类似的作品,每个都是Shape: 10, 5。 加载大数据集的另一种方法是专门要求它的迭代器。 在这种情况下,您可以动态确定每个 Pandas DataFrame的长度(即要获得多少行):
In: iris_iterator = pd.read_csv(iris_filename, header=None,
names=['C1', 'C2', 'C3', 'C4', 'C5'],
iterator=True)
In: print (iris_iterator.get_chunk(10).shape) Out: (10, 5) In: print (iris_iterator.get_chunk(20).shape) Out: (20, 5) In: piece = iris_iterator.get_chunk(2)
piece
输出仅代表原始数据集的一部分:

在此示例中,我们首先定义了迭代器。 接下来,我们检索了一条包含 10 行的数据。 然后,我们又获得了 20 行,最后得到了最后打印的两行。
除 Pandas 外,您还可以使用 CSV 包,该包提供了两个函数来迭代文件中的小块数据:reader和DictReader函数。 让我们通过导入 CSV 包来说明这些函数:
In:import csv
reader将数据从磁盘输入到 Python 列表。 DictReader而是将数据转换为字典。 这两个函数都是通过遍历正在读取的文件的行来工作的。 reader完全返回其读取的内容,除去返回回车,并通过分隔符(默认为逗号,但可以修改)分成列表。 DictReader会将列表的数据映射到字典中,其关键字将由第一行(如果存在标题)或fieldnames参数(使用报告列名的字符串列表)定义。
以本机方式读取列表不是限制。 例如,使用快速的 Python 实现(例如 PyPy)来加速代码会更容易。 此外,我们始终可以将列表转换为 NumPy ndarrays(我们即将介绍的数据结构)。 通过将数据读取为 JSON 样式的字典,将很容易获得DataFrame。
这是一个使用 CSV 包中的此类函数的简单示例。
让我们假设从这个页面下载的datasets-uci-iris.csv文件是一个巨大的文件,我们无法将其完全加载到内存中(实际上,我们只是假装,这种情况是因为我们记得我们在本章的开头看到了该文件;它仅由 150 个示例组成,并且 CSV 缺少标题行)。
因此,我们唯一的选择是将其加载到块中。 首先,让我们进行一个实验:
In: with open(iris_filename, 'rt') as data_stream:
# 'rt' mode
for n, row in enumerate(csv.DictReader(data_stream,
fieldnames = ['sepal_length', 'sepal_width',
'petal_length', 'petal_width',
'target'],
dialect='excel')):
if n== 0:
print (n, row)
else:
break Out: 0 OrderedDict([('sepal_length', '5.1'), ('sepal_width', '3.5'),
('petal_length', '1.4'), ('petal_width', '0.2'), ('target', 'Iris-
setosa')])
前面的代码完成什么工作? 首先,它打开一个到文件的读取二进制连接,该文件别名为data_stream。 使用with命令可确保完全执行前面缩进中的命令后关闭文件。
然后,迭代(for...in)并枚举csv.DictReader调用,该调用包装来自data_stream的数据流。 由于文件中没有标题行,因此fieldnames提供了有关字段名称的信息。 dialect仅指定我们正在调用标准的逗号分隔 CSV(稍后将提供一些有关如何修改此参数的提示)。
在迭代内部,如果正在读取的行是第一行,则将其打印出来。 否则,通过break命令停止循环。 print命令为我们提供行号0和一个字典。 因此,您只需调用带有变量名称的键,即可调用该行的所有数据。
同样,我们可以使csv.reader命令使用相同的代码,如下所示:
In: with open(iris_filename, 'rt') as data_stream:
for n, row in enumerate(csv.reader(data_stream,
dialect='excel')):
if n==0:
print (row)
else:
break Out: ['5.1', '3.5', '1.4', '0.2', 'Iris-setosa']
在这里,代码更加简单,输出也更加简单,提供了一个包含序列中行值的列表。
至此,基于第二段代码,我们可以创建一个可从for循环迭代中调用的生成器。 这将从函数的批量参数定义的大小的块中的文件中即时获取数据:
In: def batch_read(filename, batch=5):
# open the data stream
with open(filename, 'rt') as data_stream:
# reset the batch
batch_output = list()
# iterate over the file
for n, row in enumerate(csv.reader(data_stream, dialect='excel')):
# if the batch is of the right size
if n > 0 and n % batch == 0:
# yield back the batch as an ndarray
yield(np.array(batch_output))
# reset the batch and restart
batch_output = list()
# otherwise add the row to the batch
batch_output.append(row)
# when the loop is over, yield what's left
yield(np.array(batch_output))
与上一个示例类似,由于enumerate函数包装的csv.reader函数与提取的数据列表以及示例编号(从零开始)一起提供,因此可以提取数据。 根据示例编号,批量列表将附加到数据列表中,或者使用生成的yield函数返回到主程序。 重复此过程,直到整个文件被读取并批量返回为止:
In: import numpy as np
for batch_input in batch_read(iris_filename, batch=3):
print (batch_input)
break Out: [['5.1' '3.5' '1.4' '0.2' 'Iris-setosa']
['4.9' '3.0' '1.4' '0.2' 'Iris-setosa']
['4.7' '3.2' '1.3' '0.2' 'Iris-setosa']]
这样的功能可以为随机梯度下降学习提供基本功能,如第 4 章,“机器学习”中所述,我们将回到这段代码并通过引入一些更高级的示例,来扩展这个示例。
访问其他数据格式
到目前为止,我们仅处理 CSV 文件。 为了加载 MS Excel,HDFS,SQL,JSON,HTML 和 Stata 数据集,Pandas 包提供了类似的功能(和函数)。 由于这些格式中的大多数格式在数据科学中并未常规使用,因此大多数人只能了解如何加载和处理每种格式,您可以参考 Pandas 网站上的可用文档。 在这里,我们将仅演示如何有效地使用磁盘空间以快速有效的方式存储和检索机器学习算法信息的要点。 在这种情况下,您可以利用 SQLite 数据库来访问特定的信息子集并将其转换为 Pandas DataFrame。 如果您不需要对数据进行特定的选择或过滤,但是唯一的问题是从 CSV 文件读取数据非常耗时,并且每次都需要很多工作(例如,设置正确的变量类型和名称),则可以使用 HDF5 数据结构来加快保存和加载数据的速度。
在第一个示例中,我们将使用 SQLite 和 SQL 语言存储一些数据并检索其过滤后的版本。 与其他数据库相比,SQLite 具有许多优点:它是独立的(所有数据都将存储在一个文件中),无服务器(Python 将提供存储,操作和访问数据的接口)且速度很快。 导入sqlite3包(它是 Python 栈的一部分,因此无论如何都不需要安装)后,您定义了两个查询:一个删除同名的先前数据表,另一个创建一个新表,它能够保留日期,城市,温度和目的地数据(并且您使用整数,浮点数和varchar类型,它们对应于int,float和str)。
打开数据库(此时已创建,如果尚未在磁盘上创建的数据库)之后,执行两个查询,然后提交更改(通过提交,实际上是在一个批量中开始执行所有先前的数据库命令):
In: import sqlite3
drop_query = "DROP TABLE IF EXISTS temp_data;" create_query = "CREATE TABLE temp_data \ (date INTEGER, city VARCHAR(80), \ temperature REAL, destination INTEGER);" connection = sqlite3.connect("example.db") connection.execute(drop_query) connection.execute(create_query) connection.commit()
此时,数据库及其所有数据表均已在磁盘上创建。
在上一个示例中,您在磁盘上创建了一个数据库。 您还可以通过将连接输出更改为':memory:'在内存中创建它,如代码段connection = sqlite3.connect(':memory:') you can use ':memory:' to create an in-memory database中所示。
为了将数据插入数据库表,最好的方法是创建一个包含要存储的数据行的值的元组列表。 然后,插入查询将负责记录每个数据行。 请注意,这次我们对多个命令(每行分别插入到表中)使用了executemany方法,而不是前一个命令execute:
In: data = [(20140910, "Rome", 80.0, 0),
(20140910, "Berlin", 50.0, 0), (20140910, "Wien", 32.0, 1), (20140911, "Paris", 65.0, 0)] insert_query = "INSERT INTO temp_data VALUES(?, ?, ?, ?)" connection.executemany(insert_query, data) connection.commit()
在这一点上,我们仅需通过选择查询就可以根据特定条件确定要获取内存的数据,然后使用read_sql_query命令对其进行检索:
In: selection_query = "SELECT date, city, temperature, destination \
FROM temp_data WHERE Date=20140910" retrieved = pd.read_sql_query(selection_query, connection)
现在,您需要的所有数据(以 Pandas DataFrame格式)都包含在retrieved变量中。 您需要做的就是关闭与数据库的连接:
In: connection.close()
在以下示例中,我们将面对大型 CSV 文件的情况,该文件需要很长时间才能加载和解析其列变量。 在这种情况下,我们将使用 HDF5 数据格式,该格式适合于快速存储和检索DataFrame。
HDF5 是一种文件格式,最初是由国家超级计算应用中心(NCSA)开发的,用于存储和访问大量科学数据。 1990 年代 NASA 的要求,以便对地球观测系统和其他太空观测系统产生的数据具有便携式文件格式。 HDF5 安排为分层数据存储,可保存同构类型或组的多维数组,这些数组是数组和其他组的容器。 作为文件系统,它非常适合DataFrame结构,并且通过自动数据压缩,对于大文件,这样的文件系统可以使数据加载比简单地读取 CSV 文件快得多。
使用 Pandas 包可以使用 HDF5 格式存储序列和DataFrame数据结构。 您可能会发现它对于存储二进制数据(例如预处理的图像或视频文件)也非常有用。 当您需要从磁盘访问大量文件时,由于文件分散在文件系统中,因此将数据存入内存可能会遇到一些延迟。 将所有文件存储到单个 HDF5 文件中将仅解决问题。 您可以在这个页面,尤其是[上阅读如何使用h5py包,这是一个 Python 包,它提供了以 NumPy 数组形式存储和检索数据的接口。 ] http://docs.h5py.org/en/stable/](http://docs.h5py.org/en/stable/) ,其主要文档网站。 您也可以通过发出conda install h5py或pip install h5py命令来安装h5py。
我们将从使用HDFStore命令初始化 HDF5 文件example.h5开始,该命令允许对数据文件进行低级操作。 实例化文件后,您可以像使用 Python 字典一样开始使用它。 在以下代码段中,将Iris数据集存储在字典键iris下。 之后,您只需关闭 HDF5 文件:
In: storage = pd.HDFStore('example.h5')
storage['iris'] = iris storage.close()
当需要检索存储在 HDF5 文件中的数据时,可以使用HDFStore命令重新打开该文件。 首先,检查可用键(就像在字典中一样):
In: storage = pd.HDFStore('example.h5')
storage.keys() Out: ['/iris']
然后,通过使用相应的键调用所需的值来分配所需的值:
In: fast_iris_upload = storage['iris']
type(fast_iris_upload) Out: pandas.core.frame.DataFrame
数据将立即加载,并且先前的DataFrame现在可在变量fast_iris_upload下进行进一步处理。
将数据放在一起
最后,可以通过合并序列或其他类似列表的数据来创建 Pandas DataFrame。 请注意,标量将转换为列表,如下所示:
In: import pandas as pd
my_own_dataset = pd.DataFrame({'Col1': range(5),
'Col2': [1.0]*5,
'Col3': 1.0,
'Col4': 'Hello World!'})
my_own_dataset
这是my_own_dataset的输出:

可以很容易地说,对于要堆叠在一起的每个列,您都可以提供它们的名称(作为字典键)和值(作为该键的字典值)。 如前面的示例所示,Col2和Col3以两种不同的方式创建,但是它们提供了相同的结果值列。 这样,您可以创建一个 Pandas 数据帧,其中包含具有非常简单特征的多种数据类型。
在此过程中,请确保不要混合使用不同大小的列表; 否则,将引发异常,如下所示:
In: my_wrong_own_dataset = pd.DataFrame({'Col1': range(5),
'Col2': 'string', 'Col3': range(2)}) Out: ...
ValueError: arrays must all be same length
为了组装整个现有的DataFrame,您必须使用基于连接的其他方法。 Pandas 包提供了concat命令,该命令通过在轴0(默认选项)上工作时堆叠行或在轴1上连接时堆叠列来对 Pandas 数据结构(Series和DataFrame)进行操作:
In: col5 = pd.Series([4, 3, 2, 1, 0])
col6 = pd.Series([0, 0, 1, 1, 1]) a_new_dataset = pd.concat([col5, col6], axis=1,
ignore_index = True, keys=['Col5', 'Col6']) my_new_dataset = pd.concat([my_own_dataset, a_new_dataset], axis=1) my_new_dataset
结果数据集是col5和col6序列的连接:

在前面的示例中,我们基于两个Series创建了一个新的DataFrame``a_new_dataset。 不管它们的索引如何,我们都将它们堆叠在一起,因为我们使用了ignore_index参数,该参数设置为True。 如果对索引进行匹配对您的项目很重要,则不要使用ignore_index参数(其默认值为False),您将基于两个索引的并集或仅基于两个索引而获得一个新的DataFrame结果匹配的索引元素。
通过在参数join='inner'中添加等效于 SQL 内部连接的参数join='inner',可以在pd.concat中基于公共列连接两个不同的数据集(有关连接的主题,将在以下示例中处理) 。
根据索引进行匹配有时可能不足以满足您的需求。 有时,您可能需要在特定列或一系列列上匹配不同的Series或DataFrame。 在这种情况下,您需要merge方法,该方法可以在每个DataFrame中运行。
为了查看merge方法的作用,我们将创建一个参考表,其中包含一些要基于Col5进行匹配的值:
In: key = pd.Series([1, 2, 4])
value = pd.Series(['alpha', 'beta', 'gamma']) reference_table = pd.concat([key, value], axis=1, ignore_index = True,
keys=['Col5', 'Col7']) reference_table
这是key和value到一个DataFrame之间的连接:

通过将how参数设置为left来操作合并,从而实现 SQL 左外部连接。 除left以外,此参数的其他可能设置如下:
right:等效于 SQL 右外部连接outer:等效于 SQL 完全外部连接inner:等效于 SQL 内部连接(如前所述)
In: my_new_dataset.merge(reference_table,
on='Col5', how='left')
产生的DataFrame是左外部连接:

回到我们的初始my_own_dataset,为了检查每一列中存在的数据类型,您可以检查dtypes属性的输出:
In: my_own_dataset.dtypes Out: Col1 int64
Col2 float64
Col3 float64
Col4 object
dtype: object
如果您希望检查数据是分类的,整数的还是浮点的,则在此示例中看到的最后一个方法非常方便,并且它的精度也很高。 实际上,有时可以通过将浮点数舍入为整数并将双精度浮点数转换为单精度浮点数或仅使用一种类型的数据来提高处理速度。 在下面的示例中,让我们看看如何转换类型。 该示例也可以看作是有关如何重新分配列数据的广泛示例:
In: my_own_dataset['Col1'] = my_own_dataset['Col1'].astype(float)
my_own_dataset.dtypes Out: Col1 float64
Col2 float64
Col3 float64
Col4 object
dtype: object
您还可以使用info()获得有关DataFrame结构和数据类型的信息,如本示例所示:my_own_dataset.info()。
数据预处理
现在,我们能够导入数据集,甚至是有问题的大型数据集。 现在,我们需要学习基本的预处理器,以便使其在下一步的数据科学步骤中可行。
首先,如果您需要将函数应用于行的有限部分,则可以创建掩码。 掩码是一系列布尔值(即True或False),它们告诉您是否选择了该行。
例如,假设我们要选择Iris数据集的sepal length大于6的所有行。 我们可以简单地执行以下操作:
In: mask_feature = iris['sepal_length'] > 6.0 In: mask_feature
Out: 0 False
1 False
...
146 True
147 True
148 True
149 False
在前面的简单示例中,我们可以立即看到哪些观测值是True,哪些不是(False),哪些符合选择查询。
现在,让我们检查一下如何在另一个示例中使用选择蒙版。 我们想用New label标签代替Iris-virginica目标标签。 我们可以使用以下两行代码来做到这一点:
In: mask_target = iris['target'] == 'Iris-virginica'
iris.loc[mask_target, 'target'] = 'New label'
您会看到Iris-virginica的所有出现现在都被New label代替了。 以下代码说明了loc()方法。 只需将其视为借助行列索引访问矩阵数据的一种方法即可。
要在目标列中查看标签的新列表,我们可以使用unique()方法。 如果要首先评估数据集,此方法非常方便:
In: iris['target'].unique() Out: array(['Iris-setosa', 'Iris-versicolor', 'New label'],
dtype=object)
如果要查看有关每个特征的一些统计信息,可以将每列相应地分组; 最终,您也可以使用口罩。 Pandas 方法groupby将产生与 SQL 语句中的GROUP BY子句类似的结果。 下一个要应用的方法应该是一个或多个列上的聚合方法。 例如,mean() Pandas 聚合方法是AVG() SQL 函数的对应方法,用于计算组中值的平均值。 Pandas 聚合方法var()计算方差; sum()求和; count()组中的行数; 等等。 请注意,结果仍然是 Pandas DataFrame,因此可以将多个操作链接在一起。
对变量的许多常见操作,例如mean或sum都是DataFrame方法,可以按列(使用参数axis=0,即iris.sum(axis=0)或按行(使用axis=1):
count:非空(NaN)值的计数median:返回中位数; 即第 50 个百分位min:最低值max:最高值mode:众数,是最频繁出现的值var:方差,用于测量值的离散度std:标准差,是方差的平方根mad:平均绝对偏差,这是一种测量对异常值稳健的值的离散度的方法skew:偏度,指示分布对称性kurt:峰度,指示分布形状
下一步,我们可以尝试使用groupby的几个示例。 通过按目标(即标签)对观察结果进行分组,我们可以检查每组特征的平均值和方差之间的差异:
In: grouped_targets_mean = iris.groupby(['target']).mean()
grouped_targets_mean
输出是已分组的Iris数据集,分组函数是平均值:

In: grouped_targets_var = iris.groupby(['target']).var()
grouped_targets_var
现在,分组函数就是方差:

由于您可能需要对每个变量进行多个统计,因此您可以直接使用agg方法,并针对每个变量应用特定的函数,而不是通过连接将多个聚合的数据集放在一起。 您可以通过字典定义变量,字典中的键是变量标签,值是要应用的函数的列表–由字符串(例如'mean','std','min','max','sum'和'prod')或通过当场声明的预定义函数甚至是 Lambda 函数:
In: funcs = {'sepal_length': ['mean','std'],
'sepal_width' : ['max', 'min'], 'petal_length': ['mean','std'], 'petal_width' : ['max', 'min']} grouped_targets_f = iris.groupby(['target']).agg(funcs) grouped_targets_f
现在,每列都有不同的分组函数:

以后,如果需要使用函数对观察结果进行排序,则可以使用.sort_index()方法,如下所示:
In: iris.sort_index(by='sepal_length').head()
作为输出,您将获得数据集的顶部行:

最后,如果您的数据集包含一个时间序列(例如,对于数字目标),并且您需要对其应用rolling操作(对于嘈杂的数据点),则只需执行以下操作:
In: smooth_time_series = pd.rolling_mean(time_series, 5)
可以针对这些值的滚动平均值执行此操作。 或者,您可以给出以下命令:
In: median_time_series = pd.rolling_median(time_series, 5)
相反,可以执行此操作以获得值的滚动中值。 在这两种情况下,窗口的大小均为 5。
更一般而言,apply() Pandas 方法能够以编程方式执行任何按行或按列的操作。 apply()应该直接在DataFrame上调用; 第一个参数是按行或按列应用的函数; 第二个参数是对其应用轴。 请注意,该函数可以是内置的,库提供的,Lambda 或任何其他用户定义的函数。
作为此强大方法的示例,让我们尝试计算每行中有多少个非零元素。 使用apply方法,这很简单:
In: iris.apply(np.count_nonzero, axis=1).head() Out: 0 5
1 5
2 5
3 5
4 5
dtype: int64
类似地,要按特征(即按列)计算非零元素,只需更改第二个参数并将其设置为0即可:
In: iris.apply(np.count_nonzero, axis=0) Out: sepal_length 150
sepal_width 150
petal_length 150
petal_width 150
target 150
dtype: int64
最后,要在元素方面进行操作,应在DataFrame上使用applymap()方法。 在这种情况下,应仅提供一个参数:要应用的函数。
例如,假设您对每个单元格的字符串表示形式的长度感兴趣。 要获得该值,您应该首先将每个单元格转换为字符串值,然后计算长度。 使用applymap,此操作非常简单:
In: iris.applymap(lambda x:len(str(x))).head()
转换后的DataFrame的前几行是:

在对数据应用转换时,实际上不需要将相同的函数应用于每一列。 使用 Pandas apply方法,您实际上可以通过修改相同的变量或另外创建新的变量将转换应用于单个变量或多个变量:
In: def square(x):
return x**2 original_variables = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
squared_iris = iris[original_variables].apply(square)
这种方法的一个弱点是,转换可能要花费很长时间,因为 Pandas 库没有利用最新 CPU 模型的多处理功能。
由于使用 Jupyter 时 Windows 中存在多处理问题,因此以下示例只能在 Linux 机器上运行,或者如果转换为脚本后只能在 Windows 机器上运行,正如以下 Stack Overflow 答案所建议的那样。
为了缩短此类计算延迟,您可以通过创建parallel_apply函数来利用多处理包。 这样的函数将一个DataFrame,一个函数以及该函数的参数作为输入,并创建一个工作池(许多 Python 复制内存中,理想情况下每个线程都在系统的不同 CPU 上运行) 并行执行所需的转换:
In: import multiprocessing
def apply_df(args): df, func, kwargs = args return df.apply(func, **kwargs) def parallel_apply(df, func, **kwargs): workers = kwargs.pop('workers') pool = multiprocessing.Pool(processes=workers) df_split = np.array_split(df, workers) results = pool.map(apply_df, [(ds, func, kwargs) for ds in df_split]) pool.close() return pd.concat(list(results))
使用此功能时,重要的是指定正确的工作线程数(取决于您的系统)和进行计算的轴(由于您按列进行操作,因此axis=1是您将要使用的常规参数配置) ):
In: squared_iris = parallel_apply(iris[['sepal_length', 'sepal_width',
'petal_length', 'petal_width']],
func=square,
axis=1,
workers=4)
squared_iris
Iris数据集很小,在这种情况下,执行所需要的时间可能比仅应用命令还要长,但是在较大的数据集上,差异可能会非常明显,尤其是如果您可以依靠大量的数据工作器。
提示,在 Intel i5 CPU 上,您可以设置workers=4以获得最佳结果,而在 Intel i7 上,您可以设置workers=8。
数据选择
我们将关注的关于 Pandas 的最后一个主题是数据选择。 让我们从一个例子开始。 我们可能遇到数据集包含索引列的情况。 我们如何正确地将其与 Pandas 一起导入? 然后,我们可以积极利用它来简化我们的工作吗?
我们将使用一个非常简单的数据集,其中包含一个索引列(这只是一个计数器而不是特征)。 为了使示例更加通用,让我们从 100 开始索引。因此,行号0的索引为100:
n,val1,val2,val3 100,10,10,C 101,10,20,C 102,10,30,B 103,10,40,B 104,10,50,A
尝试以经典方式加载文件时,您会发现自己遇到了n作为特征(或列)的情况。 几乎没有任何错误,但是不应将索引误用作特征。 因此,最好将其分开。 如果恰巧在模型的学习阶段使用了它,则可能会发生泄漏的情况,这是机器学习中错误的主要来源之一。
实际上,如果索引是随机数,则不会损害模型的功效。 但是,如果索引包含渐进,时间或什至是信息性元素(例如,某些数值范围可用于正面结果,而其他数值范围用于负面结果),则可以将其合并到模型的泄漏信息中。 当对新数据使用模型时,这将无法复制:
In: import pandas as pd In: dataset = pd.read_csv('a_selection_example_1.csv')
dataset
这是read数据集:

因此,在加载这样的数据集时,我们可能要指定n为索引列。 由于索引n是第一列,因此我们可以给出以下命令:
In: dataset = pd.read_csv('a_selection_example_1.csv', index_col=0)
dataset
read_csv函数现在使用第一列作为索引:

在这里,数据集已加载且索引正确。 现在,要访问单元格的值,我们可以做一些事情。 让我们一一列出:
- 首先,您可以简单地指定感兴趣的列和行(通过使用其索引)。
- 要提取第五行的
val3(索引为n = 104),可以给出以下命令:
In: dataset['val3'][104] Out: 'A'
- 请仔细应用此操作,因为它不是矩阵,因此您可能会想先输入行然后输入列。 请记住,它实际上是一个 Pandas
DataFrame,[]运算符首先在列上工作,然后在结果 PandasSeries的元素上工作。 - 要具有与前述访问数据的方法类似的功能,可以使用
.loc()方法,该方法基于标签。 也就是说,它通过索引和列标签起作用:
In: dataset.loc[104, 'val3'] Out: 'A'
在这种情况下,您应该首先指定索引,然后再指定感兴趣的列。
请注意,有时,DataFrame中的索引可以用数字表示。 在这种情况下,很容易将其与位置索引混淆,但是数字索引不一定是有序的或连续的。
- 最后,指定位置(位置索引,如在矩阵中)的完全优化函数为
iloc()。 使用它,您必须使用行号和列号指定单元格:
In: dataset.iloc[4, 2] Out: 'A'
- 子矩阵的检索是非常直观的操作。 您只需要指定索引列表而不是标量即可:
In: dataset[['val3', 'val2']][0:2]
- 此命令等效于此:
In: dataset.loc[range(100, 102), ['val3', 'val2']]
- 它也等效于以下内容:
In: dataset.iloc[range(2), [2,1]]
在所有情况下,所得的DataFrame如下:

在 Pandas DataFrame中还有另一种索引方法:ix方法通过结合基于标签的索引和位置索引来工作:dataset.ix[104, 'val3']。 请注意,ix必须猜测您指的是什么。 因此,如果您不想混用标签和位置索引,则绝对首选loc和iloc,以便创建更安全有效的方法。 ix将在即将发布的 Pandas 版本中弃用。
处理分类和文本数据
通常,您会发现自己要处理两种主要的数据:分类数据和数值数据。 温度,金额,使用天数或门牌号之类的数字数据可以由浮点数(例如 1.0,-2.3、99.99 等)或整数(例如 -3, 9、0、1,依此类推)。 数据可以假定的每个值与其他值都具有直接关系,因为它们具有可比性。 换句话说,您可以说值为 2.0 的特征要比假定值为 1.0 的特征更大(实际上是两倍)。 这种类型的数据定义非常明确且易于理解,具有等于,大于和小于等二进制运算符。
您在职业中可能会看到的另一种数据类型是分类数据。 类别基准表示无法测量的属性,并采用一组有限或无限值(通常称为级别)中的值。 例如,天气是一种分类特征,因为它采用离散集sunny,cloudy,snowy,rainy和foggy中的值。 其他示例包括包含 URL,IP,设备品牌,您放入电子商务购物车中的项目,设备 ID 等的特征。 在此数据上,您无法定义等于,大于和小于二元运算符,因此无法对它们进行排名。
分类和数值的加号是布尔值。 实际上,它们可以看作是分类的(特征的存在/不存在),或者另一方面,可以认为特征具有展览(已显示,未显示)的概率。 由于许多机器学习算法不允许将输入分类,因此布尔特征通常用于将分类特征编码为数值。
让我们继续以天气为例。 如果要映射包含当前天气并采用集合[sunny, cloudy, snowy, rainy, foggy]中的值并将其编码为二进制特征的特征,则应创建五个True/False函数,每个级别的分类特征。 现在,映射很简单:
Categorical_feature = sunny binary_features = [1, 0, 0, 0, 0] Categorical_feature = cloudy binary_features = [0, 1, 0, 0, 0] Categorical_feature = snowy binary_features = [0, 0, 1, 0, 0] Categorical_feature = rainy binary_features = [0, 0, 0, 1, 0] Categorical_feature = foggy binary_features = [0, 0, 0, 0, 1]
只有一个二元特征揭示了分类特征的存在。 其他保持0。 这称为二进制编码或单热编码。 通过执行此简单步骤,我们从分类世界转移到了数字世界。 此操作的代价是它在内存和计算方面的复杂性; 现在,我们有五个特征,而不是单个特征。 通常,我们将创建N个特征,而不是具有N个可能水平的单个分类特征,每个特征都有两个数值(1/0)。 该操作称为伪编码。
Pandas 包可帮助我们完成此操作,只需一个命令即可简化映射:
In: import pandas as pd
categorical_feature = pd.Series(['sunny', 'cloudy',
'snowy', 'rainy', 'foggy'])
mapping = pd.get_dummies(categorical_feature)
mapping
这是mapping数据集:

输出是一个DataFrame,其中包含作为列标签的分类级别以及沿列的各个二进制特征。 要将分类值映射到数字值列表,只需使用 Pandas 的功能:
In: mapping['sunny'] Out: 0 1.0
1 0.0
2 0.0
3 0.0
4 0.0
Name: sunny, dtype: float64 In: mapping['cloudy'] Out: 0 0.0
1 1.0
2 0.0
3 0.0
4 0.0
Name: cloudy, dtype: float64
如本例所示,sunny被映射到布尔值[1, 0, 0, 0, 0],cloudy至[0, 1, 0, 0, 0]等的列表中。
可以使用另一个工具包 Scikit-learn 完成相同的操作。 由于必须首先将文本转换为分类索引,所以它以某种方式更加复杂,但是结果是相同的。 让我们再次看一下前面的示例:
In: from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
ohe = OneHotEncoder()
levels = ['sunny', 'cloudy', 'snowy', 'rainy', 'foggy']
fit_levs = le.fit_transform(levels)
ohe.fit([[fit_levs[0]], [fit_levs[1]], [fit_levs[2]],
[fit_levs[3]], [fit_levs[4]]])
print (ohe.transform([le.transform(['sunny'])]).toarray())
print (ohe.transform([le.transform(['cloudy'])]).toarray()) Out: [[ 0\. 0\. 0\. 0\. 1.]]
[[ 1\. 0\. 0\. 0\. 0.]]
基本上,LabelEncoder将文本映射到 0 到 N 的整数(请注意,在这种情况下,它仍然是类别变量,因为对其进行排名没有意义)。 现在,这五个值映射到五个二进制变量。
特殊类型的数据——文本
让我们介绍另一种类型的数据。 文本数据是机器学习算法的常用输入,因为它包含了我们语言中数据的自然表示形式。 它是如此丰富,它也包含了我们所寻找的答案。 处理文本时,最常见的方法是使用词袋方法。 根据这种方法,每个单词都成为一个特征,文本成为一个向量,其中包含体内所有特征(即单词)的非零元素。 给定一个文本数据集,特征数量是多少? 很简单。 只需提取其中的所有唯一单词并将其枚举即可。 对于使用所有英语单词的非常丰富的文本,该数字在 100 万范围内。 如果您不打算对其进行进一步处理(删除任何第三人称,缩写,缩略语和首字母缩写词),则可能会发现自己处理的不止这些,但这是非常罕见的情况。 用简单而简单的方法(这是本书的目标),我们让 Python 尽力而为。
本节中使用的数据集是文本的; 它是著名的 20newsgroup (有关此信息,请访问这个页面)。 它是大约 20,000 个文档的集合,这些文档属于新闻组的 20 个主题。 它是处理文本分类和聚类时最常用(如果不是最常用)的数据集之一。 要导入它,我们将使用其受限子集,其中包含所有科学主题(医学和太空):
In: from sklearn.datasets import fetch_20newsgroups
categories = ['sci.med', 'sci.space']
twenty_sci_news = fetch_20newsgroups(categories=categories)
首次运行此命令时,它将自动下载数据集并将其放置在$HOME/scikit_learn_data/20news_home/默认目录中。 您可以通过询问文件的位置,文件的内容和标签(即,文档发布所在的讨论主题)来查询数据集对象。 它们分别位于对象的.filenames,.data和.target属性中:
In: print(twenty_sci_news.data[0]) Out: From: flb@flb.optiplan.fi ("F.Baube[tm]")
Subject: Vandalizing the sky
X-Added: Forwarded by Space Digest
Organization: [via International Space University]
Original-Sender: isu@VACATION.VENARI.CS.CMU.EDU
Distribution: sci
Lines: 12
From: "Phil G. Fraering" <pgf@srl03.cacs.usl.edu>
[...] In: twenty_sci_news.filenames Out: array([
'/Users/datascientist/scikit_learn_data/20news_home/20news-bydate-
train/sci.space/61116',
'/Users/datascientist/scikit_learn_data/20news_home/20news-
bydate-train/sci.med/58122',
'/Users/datascientist/scikit_learn_data/20news_home/20news-
bydate-train/sci.med/58903',
...,
'/Users/datascientist/scikit_learn_data/20news_home/20news-
bydate-train/sci.space/60774',
[...] In: print (twenty_sci_news.target[0])
print (twenty_sci_news.target_names[twenty_sci_news.target[0]]) Out: 1
sci.space
目标是分类的,但它表示为整数(sci.med的0和sci.space的1)。 如果要阅读,请对照twenty_sci_news.target数组的索引进行检查。
处理文本的最简单方法是将数据集的主体转换为一系列单词。 这意味着,对于每个文档,将计算特定单词在正文中出现的次数。
例如,让我们制作一个小的,易于处理的数据集:
Document_1:we love data scienceDocument_2:data science is hard
在包含Document_1和Document_2的整个数据集中,只有六个不同的词:we,love,data,science,is和hard。 给定此数组,我们可以将每个文档与特征向量关联:
In: Feature_Document_1 = [1 1 1 1 0 0]
Feature_Document_2 = [0 0 1 1 1 1]
请注意,我们将丢弃单词的位置,仅保留单词在文档中出现的次数。 就这样。
在20newsletter数据库中,使用 Python,这可以通过简单的方式完成:
In: from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer()
word_count = count_vect.fit_transform(twenty_sci_news.data)
word_count.shape Out: (1187, 25638)
首先,我们实例化一个CountVectorizer对象。 然后,我们调用该方法对每个文档中的单词进行计数,并为每个文档生成特征向量(fit_transform)。 然后,我们查询矩阵大小。 请注意,输出矩阵是稀疏的,因为每个文档只有很少的单词选择是很常见的(因为每行中非零元素的数量非常少,并且存储所有冗余零都没有意义)。 无论如何,输出形状为(1187, 25638)。 第一个值是数据集中的观测数量(文档数量),而第二个值是特征数量(数据集中唯一单词的数量)。
CountVectorizer转换后,每个文档都与其特征向量相关联。 让我们看一下第一个文档:
In: print (word_count[0]) Out: (0, 10827) 2
(0, 10501) 2
(0, 17170) 1
(0, 10341) 1
(0, 4762) 2
(0, 23381) 2
(0, 22345) 1
(0, 24461) 1
(0, 23137) 7
[...]
您会注意到输出是一个稀疏向量,其中仅存储非零元素。 要检查与单词的直接对应关系,请尝试以下代码:
In: word_list = count_vect.get_feature_names()
for n in word_count[0].indices:
print ('Word "%s" appears %i times' % (word_list[n],
word_count[0, n]))
Out: Word: from appears 2 times
Word: flb appears 2 times
Word: optiplan appears 1 times
Word: fi appears 1 times
Word: baube appears 2 times
Word: tm appears 2 times
Word: subject appears 1 times
Word: vandalizing appears 1 times
Word: the appears 7 times
[...]
到目前为止,一切都非常简单,不是吗? 让我们前进到另一个增加复杂性和有效性的任务。 数词固然好,但我们可以管理更多; 我们可以计算他们的频率。 您可以跨大小不同的数据集进行比较。 它给出了一个单词是停止词(即一个很常见的词,例如a,an,the或is)还是一个稀有的,唯一的单词的想法。 通常,这些单词是最重要的,因为它们能够根据这些单词来表征实例和特征,这在学习过程中是非常有区别的。 要检索每个文档中每个单词的出现频率,请尝试以下代码:
In: from sklearn.feature_extraction.text import TfidfVectorizer
tf_vect = TfidfVectorizer(use_idf=False, norm='l1')
word_freq = tf_vect.fit_transform(twenty_sci_news.data)
word_list = tf_vect.get_feature_names()
for n in word_freq[0].indices:
print ('Word "%s" has frequency %0.3f' % (word_list[n],
word_freq[0, n])) Out: Word "from" has frequency 0.022
Word "flb" has frequency 0.022
Word "optiplan" has frequency 0.011
Word "fi" has frequency 0.011
Word "baube" has frequency 0.022
Word "tm" has frequency 0.022
Word "subject" has frequency 0.011
Word "vandalizing" has frequency 0.011
Word "the" has frequency 0.077
[...]
频率的总和为 1(或由于近似值而接近 1)。 发生这种情况是因为我们选择了l1规范。 在这种特定情况下,单词frequency是概率分布函数。 有时,增加稀有词与常见词之间的差异会很好。 在这种情况下,可以使用l2范数归一化特征向量。
向量化文本数据的一种更有效的方法是使用tf-idf。 简而言之,您可以将构成文档的单词的词频乘以单词本身的逆文档频率(即,它出现在文档中的数量或对数缩放转换中)。 这对于突出显示有效描述每个文档的单词非常有用,这些单词是数据集中强大的区分元素:
In: from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vect = TfidfVectorizer() # Default: use_idf=True
word_tfidf = tfidf_vect.fit_transform(twenty_sci_news.data)
word_list = tfidf_vect.get_feature_names()
for n in word_tfidf[0].indices:
print ('Word "%s" has tf-idf %0.3f' % (word_list[n],
word_tfidf[0, n])) Out: Word "fred" has tf-idf 0.089
Word "twilight" has tf-idf 0.139
Word "evening" has tf-idf 0.113
Word "in" has tf-idf 0.024
Word "presence" has tf-idf 0.119
Word "its" has tf-idf 0.061
Word "blare" has tf-idf 0.150
Word "freely" has tf-idf 0.119
Word "may" has tf-idf 0.054
Word "god" has tf-idf 0.119
Word "blessed" has tf-idf 0.150
Word "is" has tf-idf 0.026
Word "profiting" has tf-idf 0.150
[...]
在此示例中,第一个文档的四个最具特征性的单词是caste,baube,flb和tm(它们具有最高的tf-idf分数)。 这意味着它们在文档中的任期频率很高,而在其余文档中却很少见。
到目前为止,我们已经为每个单词生成了一个特征。 一起说几句话怎么办? 当您考虑使用双字母组而不是单字组时,这正是发生的情况。 对于双字(或一般为 N 元组),一个单词及其相邻单词的存在与否很重要(即靠近它的单词及其位置)。 当然,您可以混合使用一元组字母和 N 元组字母,并为每个文档创建一个丰富的特征向量。 在下面的简单示例中,让我们测试 N 元组的工作方式:
In: text_1 = 'we love data science'
text_2 = 'data science is hard'
documents = [text_1, text_2]
documents Out: ['we love data science', 'data science is hard'] In: # That is what we say above, the default one
count_vect_1_grams = CountVectorizer(ngram_range=(1, 1),
stop_words=[], min_df=1)
word_count = count_vect_1_grams.fit_transform(documents)
word_list = count_vect_1_grams.get_feature_names()
print ("Word list = ", word_list)
print ("text_1 is described with", [word_list[n] + "(" +
str(word_count[0, n]) + ")" for n in word_count[0].indices]) Out: Word list = ['data', 'hard', 'is', 'love', 'science', 'we']
text_1 is described with ['we(1)', 'love(1)', 'data(1)', 'science(1)'] In: # Now a bi-gram count vectorizer
count_vect_1_grams = CountVectorizer(ngram_range=(2, 2))
word_count = count_vect_1_grams.fit_transform(documents)
word_list = count_vect_1_grams.get_feature_names()
print ("Word list = ", word_list)
print ("text_1 is described with", [word_list[n] + "(" +
str(word_count[0, n]) + ")" for n in word_count[0].indices]) Out: Word list = ['data science', 'is hard', 'love data',
'science is', 'we love']
text_1 is described with ['we love(1)', 'love data(1)',
'data science(1)'] In: # Now a uni- and bi-gram count vectorizer
count_vect_1_grams = CountVectorizer(ngram_range=(1, 2))
word_count = count_vect_1_grams.fit_transform(documents)
word_list = count_vect_1_grams.get_feature_names()
print ("Word list = ", word_list)
print ("text_1 is described with", [word_list[n] + "(" +
str(word_count[0, n]) + ")" for n in word_count[0].indices]) Out: Word list = ['data', 'data science', 'hard', 'is', 'is hard', 'love',
'love data', 'science', 'science is', 'we', 'we love']
text_1 is described with ['we(1)', 'love(1)', 'data(1)', 'science(1)',
'we love(1)', 'love data(1)', 'data science(1)']
前面的示例非常直观地结合了我们先前介绍的第一种和第二种方法。 在这种情况下,我们使用了CountVectorizer,但是这种方法在TfidfVectorizer中非常常见。 请注意,使用 N 元组时,特征数量呈指数爆炸式增长。
如果特征太多(词典可能太丰富, N 元组可能太多,或者计算机可能很有限),则可以使用一种技巧来降低问题的复杂性(但是您应该首先评估和权衡性能/复杂性)。 通常会使用散列技巧,其中会散列多个单词(或 N 元组),并且它们的散列会发生冲突(这会产生一堆单词)。 存储桶是语义上不相关的单词集,但具有冲突的哈希。 使用HashingVectorizer(),如以下示例所示,您可以决定所需的单词存储桶数。 当然,结果矩阵反映了您的设置:
In: from sklearn.feature_extraction.text import HashingVectorizer
hash_vect = HashingVectorizer(n_features=1000)
word_hashed = hash_vect.fit_transform(twenty_sci_news.data)
word_hashed.shape Out: (1187, 1000)
请注意,您不能反转哈希过程(因为它是摘要操作)。 因此,在完成此转换之后,您将必须按原样处理散列特征。 散列具有许多优点:允许将一袋单词快速转换为特征向量(在这种情况下,散列桶是我们的特征),可以轻松地在特征之间容纳从未见过的单词,并通过使不相关的单词在同一特征中碰撞在一起,来避免过拟合。
使用 BeatifulSoup 爬取网络
在上一节中,鉴于已经有了数据集,我们讨论了如何对文本数据进行操作。 如果我们需要抓取网络并手动下载该怎么办? 这个过程的发生频率超出您的预期,这是数据科学中非常受欢迎的话题。 例如:
- 金融机构会在网上爬取以提取有关其投资组合中公司的最新详细信息。 报纸,社交网络,博客,论坛和公司网站是这些分析的理想目标。
- 广告和媒体公司会分析网络上许多元素的人气和流行度,以了解人们的反应。
- 专门研究洞察力分析和建议的公司会在网上爬取以了解模式并为用户行为建模。
- 比较网站使用 Web 来比较价格,产品和服务,从而为用户提供有关当前情况的更新的摘要表。
不幸的是,了解网站是非常艰苦的工作,因为每个网站都是由不同的人来构建和维护的,它们具有不同的基础结构,位置,语言和结构。 它们之间唯一的共同方面是由标准公开语言表示,大多数时候,它是超文本标记语言(HTML)。
这就是为什么到目前为止,大多数 Web 爬虫都只能以通用方式理解和浏览 HTML 页面的原因。 最常用的 Web 解析器之一被称为 BeautifulSoup。 它是用 Python 编写的,是开源的,非常稳定且易于使用。 而且,它能够检测 HTML 页面中的错误和格式错误的代码段(始终记住,网页通常是人为产品,容易出错)。
对《美丽汤》的完整描述将需要整本书。 在这里,我们只会看到一些。 首先,BeautifulSoup 不是爬虫。 为了下载网页,我们可以(例如)使用urllib库:
- 让我们在维基百科上下载 William Shakespeare 页面背后的代码:
In: import urllib.request
url = 'https://en.wikipedia.org/wiki/William_Shakespeare' request = urllib.request.Request(url) response = urllib.request.urlopen(request)
- 现在是时候指示 BeautifulSoup 读取资源并使用 HTML 解析器对其进行解析了:
In: from bs4 import BeautifulSoup
soup = BeautifulSoup(response, 'html.parser')
- 现在,
soup已准备就绪并且可以查询。 要提取标题,我们可以简单地要求title属性:
In: soup.title Out: <title>William Shakespeare - Wikipedia,
the free encyclopedia</title>
如您所见,将返回整个标题标签,从而可以更深入地研究嵌套的 HTML 结构。 如果我们想知道与威廉·莎士比亚的维基百科页面相关的类别怎么办? 只需反复下载和解析相邻页面,创建条目图可能非常有用。 我们应该首先手动分析 HTML 页面本身,以找出包含我们所寻找信息的最佳 HTML 标签是什么。 请记住这里的没有免费午餐定理:没有自动发现功能,此外,如果维基百科修改其格式,事情可能会改变。
经过手动分析,我们发现类别位于名为'mw-normal-catlinks'的 DIV 中; 除了第一个链接,其他所有都可以。 现在,该进行编程了。 让我们将观察到的内容放入一些代码中,为每个类别打印链接页面的标题及其相对链接:
In: section = soup.find_all(id='mw-normal-catlinks')[0]
for catlink in section.find_all("a")[1:]:
print(catlink.get("title"), "->", catlink.get("href")) Out: Category:William Shakespeare -> /wiki/Category:William_Shakespeare
Category:1564 births -> /wiki/Category:1564_births
Category:1616 deaths -> /wiki/Category:1616_deaths
Category:16th-century English male actors -> /wiki/Category:16th-
century_English_male_actors
Category:English male stage actors -> /wiki/Category:
English_male_stage_actors
Category:16th-century English writers -> /wiki/Category:16th-
century_English_writers
我们已经使用find_all方法两次来查找带有参数中包含文本的所有 HTML 标签。 在第一种情况下,我们专门在寻找 ID。 在第二种情况下,我们正在寻找所有的"a"标签。
然后,根据给定的输出,并使用与新 URL 相同的代码,可以递归下载维基百科类别页面,此时到达祖先类别。
关于抓取的最后一点:永远不要总是禁止这种做法,在这种情况下,请记住调低下载速率(高速率时,网站的服务器可能会认为您正在进行小规模的 DoS 攻击, 最终可能会将其列入黑名单/禁止您的 IP 地址)。 有关更多信息,您可以阅读网站的条款和条件,或直接联系管理员。
使用 NumPy 的数据处理
引入了基本的 Pandas 命令后,您可以在数据科学流水线的这一点上完全,小批量甚至单个数据行上载和预处理数据到内存中,您必须处理监督和非监督学习程序的数据矩阵来准备合适的数据。
作为最佳实践,我们建议您在数据仍然是异构的(数字和符号值的混合)工作阶段和将其转换为数据数字表的另一个阶段之间划分任务。 数据表或矩阵排列在代表示例的行中,而列则包含示例的特征观察值(即变量)。
根据我们的建议,您必须在两个用于科学分析的关键 Python 包 Pandas 和 NumPy 之间进行纠缠,并在它们的两个关键数据结构DataFrame和ndarray之间进行权衡。 这意味着您的数据科学管道将更加高效,快捷。
由于我们要馈入下一个机器学习阶段的目标数据结构是由NumPy ndarray对象表示的矩阵,因此让我们从要获得的结果开始,即如何生成ndarray对象。
NumPy 的 N 维数组
Python 提供了本机数据结构,例如列表和字典,您应尽量使用它们。 例如,列表可以顺序存储异构对象(例如,您可以将数字,文本,图像和声音保存在同一列表中)。 另一方面,由于基于查询表(哈希表),词典可以重新调用内容。 内容可以是任何 Python 对象,并且通常是另一个字典的列表。 因此,词典使您可以访问复杂的多维数据结构。
无论如何,列表和字典都有其自身的局限性,例如:
- 内存和速度存在问题。 它们并未真正针对使用近乎连续的内存块进行优化,并且在尝试应用高度优化的算法或多处理器计算时,这可能会成为问题,因为内存处理可能会成为瓶颈。
- 它们非常适合存储数据,但不适用于对其进行操作。 因此,无论您想对数据做什么,都必须先定义自定义函数,然后在列表或字典元素上进行迭代或映射。
- 在处理大量数据时,迭代通常可能证明不是最佳选择。
NumPy 提供了具有以下属性的ndarray对象类(N 维数组):
- 它是内存最佳的(除了其他方面,还配置为以性能最佳的内存块布局将数据传输到 C 或 Fortran 例程)
- 它允许进行快速的线性代数计算(向量化)和逐元素运算(广播),而无需使用带有
for循环的迭代 - 诸如 SciPy 或 Scikit-learn 之类的关键库期望数组作为其功能正常运行的输入
所有这些都有一些限制。 实际上,ndarray对象具有以下缺点:
- 它们通常只存储单个特定数据类型的元素,您可以预先定义(但是可以定义复杂数据和异构数据类型,尽管出于分析目的可能很难处理)。
- 初始化之后,它们的大小是固定的。 如果要更改其形状,则必须重新创建它们。
NumPy ndarray对象的基础
在 Python 中,数组是特定类型的内存连续数据块,其标头包含索引方案和数据类型描述符。
多亏了索引方案,数组可以表示多维数据结构,其中每个元素都以n整数的元组索引,其中n是维数。 因此,如果您的数组是一维的(即顺序数据的向量),则索引将从零开始(如 Python 列表中所示)。
如果是二维的,则必须使用两个整数作为索引(x,y类型的坐标元组); 如果存在三个维度,则使用的整数数将为三个(元组x,y,z),依此类推。
在每个索引位置,数组将包含指定数据类型的数据。 数组可以存储许多数值数据类型,以及字符串和其他 Python 对象。 也可以创建自定义数据类型,从而处理不同类型的数据序列,尽管我们建议不要这样做,并且建议您在这种情况下使用 Pandas DataFrame。 Pandas 数据结构确实对于数据科学家所必需的任何异构数据类型的密集使用都具有更大的灵活性。 因此,在本书中,我们将仅考虑特定的已定义类型的 NumPy 数组,并留给 Pandas 处理异质性。
由于应该从一开始就定义数组的类型(及其以字节为单位的存储空间),因此数组创建过程将保留确切的存储空间以容纳所有数据。 因此,数组元素的访问,修改和计算都非常快,尽管这也因此意味着该数组是固定的,不能更改其结构。
Python 列表数据结构实际上更麻烦,更慢,它是将列表结构链接到包含数据本身的分散内存位置的指针的集合。 取而代之,如下图所示,NumPy ndarray由仅一个指针组成,该指针寻址单个存储位置,在该位置上依次存储数据。 当您访问NumPy ndarray中的数据时,实际上,与使用列表相比,您将需要较少的操作和对不同内存部分的访问,因此在处理大量数据时要提高效率和速度。 缺点是,无法更改连接到 NumPy 数组的数据。 在插入或删除数据时必须重新创建它:

无论 NumPy 数组的尺寸如何,数据始终将按连续的值序列(连续的内存块)进行排列。 正是对数组大小和步幅(告诉我们必须在内存中跳过多少字节才能沿着某个轴移至下一个位置)的跨度知识,才能轻松正确地表示和操作数组。
为了快速实现内存优化,为了存储多维数组,严格地有两种方法称为行优先和列优先。 由于 RAM(随机存取存储器)被安排在存储单元的线性存储中(存储单元作为一条线的点是连续的– RAM 中没有像数组这样的东西 ),您必须将数组展开为向量并将其存储在内存中。 展开时,您可以在 NumPy 包中逐行(主要顺序)执行 C/C++ 的典型操作,或者逐列(主要顺序执行)的 Fortran 或 R.Python 的典型操作。 在实现中,使用主要行排序(也称为 C 连续,而主要列排序也称为 Fortran 连续),这意味着在逐行应用的计算操作中,它比逐列工作更快。 无论如何,在创建 NumPy 数组时,您可以根据对行或列进行更多操作的期望来确定数据结构的顺序。 导入包import numpy as np后,给定数组a = [[1,2,3],[4,5,6],[7,8,9]],您可以按行优先顺序c = np.array(a, order='C')或按列优先顺序f = np.array(a, order='F')重新定义它
相反,代表多个维度的数据结构列表只能将自己变成嵌套列表,从而增加访问数据时的开销和内存碎片。
到目前为止,您所阅读的所有内容听起来都像是计算机科学家在胡言乱语。 毕竟,所有数据科学家关心的是使 Python 进行有用且快速的操作。 确实是这样,但是从句法的角度快速执行某些操作有时并不能自动等同于从执行本身的角度快速执行操作。 如果您掌握了 NumPy 和 Pandas 的内部结构,则可以真正提高代码速度,并在更短的时间内实现更多的项目。 我们拥有使用 NumPy 和 Pandas 在语法上正确的数据处理代码的经验,这些代码通过正确的重构将其执行时间减少了一半或更多。
就我们的目的而言,了解在访问或转换数组时,我们可能只是在查看它或在复制它,这也是非常重要的。 当我们查看数组时,我们实际上调用了一个过程,该过程使我们可以将其结构中存在的数据转换为其他数据,但是采购数组不变。 根据前面的示例,在查看时,我们只是在更改ndarray的size属性; 数据保持不变。 因此,除非我们将它们固定到新的数组中,否则在查看数组时经历的任何数据转换都只是短暂的。
相反,当我们复制一个数组时,我们实际上是在创建一个具有不同结构的新数组(因此占用了新的内存)。 我们不仅改变相对于数组大小的参数,还需要改变数组的大小。 我们还将保留另一个连续的内存块并将数据复制到那里。
所有的 Pandas DataFrame实际上都是由一维 NumPy 数组构成的。 因此,当您按列进行操作时,它们会继承ndarrays的速度和内存效率(因为每个列都是 NumPy 数组)。 当按行操作时,DataFrame效率较低,因为您要依次访问不同的列。 也就是说,不同的 NumPy 数组。 出于相同的原因,通过 NumPy 数组使用整数作为位置来工作,因此通过位置索引而不是通过 Pandas 索引来寻址 Pandas DataFrame的各个部分更加快捷。 使用 Pandas 索引(也可以是文本索引,而不仅仅是数字索引)实际上需要将索引转换为DataFrame才能在数据上正确运行的相应位置。
创建 NumPy 数组
创建 NumPy 数组有多种方法。 以下是创建它们的一些方法:
- 通过将现有数据结构转换为数组
- 通过从头开始创建数组并使用默认值或计算值填充它
- 通过将一些数据从磁盘上传到数组
如果要转换现有的数据结构,则最好使用结构化列表或 Pandas DataFrame。
从列表到一维数组
处理数据时,最常见的情况之一就是将列表转换为数组。
在进行这种转换时,考虑列表中包含的对象很重要,因为这将确定结果数组的维数和dtype。
让我们从仅包含整数的列表的第一个示例开始:
In: import numpy as np In: # Transform a list into a uni-dimensional array
list_of_ints = [1,2,3]
Array_1 = np.array(list_of_ints) In: Array_1 Out: array([1, 2, 3])
请记住,您可以像使用标准 Python 列表一样访问一维数组(索引从零开始):
In: Array_1[1] # let's output the second value Out: 2
我们可以要求提供有关对象类型及其元素类型的更多信息(有效产生的类型取决于您的系统是 32 位还是 64 位):
In: type(Array_1) Out: numpy.ndarray In: Array_1.dtype Out: dtype('int64')
默认值dtype取决于您所运行的系统。
我们简单的整数列表将变成一维数组。 也就是一个 32 位整数的向量(范围从 -231 到231-1,这是我们用于示例的平台上的默认整数)。
控制内存大小
如果值的范围如此有限,您可能会认为使用int64数据类型会浪费内存。
实际上,意识到数据密集型情况,您可以计算Array_1对象占用了多少内存空间:
In: import numpy as np
Array_1.nbytes Out: 24
请注意,在 32 位平台上(或在 64 位平台上使用 32 位 Python 版本时),结果为 12。
为了节省内存,您可以事先指定最适合您的数组的类型:
In: Array_1 = np.array(list_of_ints, dtype= 'int8')
现在,您的简单数组仅占据了先前存储空间的四分之一。 这似乎是一个明显且过于简单的示例,但是当处理数百万行和列时,为分析定义最佳数据类型确实可以节省一天的时间,使您可以将所有内容很好地放入内存中。
供您参考,下面的表格列出了数据科学应用中最常见的数据类型及其单个元素的内存使用情况:
| 类型 | 字节大小 | 说明 |
|---|---|---|
bool |
1 | 布尔值(True或False)存储为字节 |
int |
4 | 默认整数类型(通常为int32或int64) |
int8 |
1 | 字节(-128 至 127) |
int16 |
2 | 整数(-32768 至 32767) |
int32 |
4 | 整数(-2 ** 31至2 ** 31-1) |
int64 |
8 | 整数(-2 ** 63至2 ** 63-1) |
uint8 |
1 | 无符号整数(0 到 255) |
uint16 |
2 | 无符号整数(0 到 65535) |
uint32 |
4 | 无符号整数(0 到2 ** 32-1) |
uint64 |
8 | 无符号整数(0 到2 ** 64-1) |
float_ |
8 | float64的简写 |
float16 |
2 | 半精度浮点数(指数 5 位,尾数 10 位) |
float32 |
4 | 单精度浮点数(指数 8 位,尾数 23 位) |
float64 |
8 | 双精度浮点数(指数 11 位,尾数 52 位) |
还有其他一些数字类型,例如复数,这种数字类型不太常见,但您的应用可能需要(例如,在频谱图中)。 您可以在这个页面上的 NumPy 用户指南中获得完整的构想。
如果数组具有要更改的类型,则可以通过强制转换新的指定类型来轻松创建新数组:
In: Array_1b = Array_1.astype('float32')
Array_1b Out: array([ 1., 2., 3.], dtype=float32)
如果您的数组占用内存,请注意.astype方法将复制该数组,因此它总是创建一个新的数组。
异构列表
如果列表由异构元素组成,例如整数,浮点数和字符串怎么办? 这变得更加棘手。 一个简单的例子可以为您描述这种情况:
In: import numpy as np
complex_list = [1,2,3] + [1.,2.,3.] + ['a','b','c']
# at first the input list is just ints
Array_2 = np.array(complex_list[:3])
print ('complex_list[:3]', Array_2.dtype)
# then it is ints and floats Array_2 = np.array(complex_list[:6])
print ('complex_list[:6]', Array_2.dtype) # finally we add strings print
Array_2 = np.array(complex_list)
('complex_list[:] ',Array_2.dtype)
Out: complex_list[:3] int64
complex_list[:6] float64
complex_list[:] <U32
如我们的输出所示,浮点类型似乎胜过int类型,而字符串(<U32表示大小为 32 或更小的 Unicode 字符串)将取代其他所有内容。
使用列表创建数组时,您可以混合使用不同的元素,并且检查结果的最 Pythonic 方法是质疑生成的数组的dtype。
请注意,如果不确定数组的内容,则必须进行检查。 否则,您可能会发现无法对结果数组进行操作,并且以后可能会发生错误(不支持的操作数类型):
In: # Check if a NumPy array is of the desired numeric type
print (isinstance(Array_2[0],np.number)) Out: False
在我们的数据处理过程中,无意中发现了一个字符串类型的数组作为输出,这意味着我们忘记了在前面的步骤中将所有变量都转换为数字变量。 例如,当所有数据都存储在 Pandas DataFrame中时。 在上一节中,“处理类别和文本数据”,我们提供了一些简单明了的方式来处理这种情况。
在此之前,让我们完成对如何从列表对象派生数组的概述。 如前所述,列表中对象的类型也影响数组的维数。
从列表到多维数组
如果将包含数字或文本对象的列表渲染为一维数组(例如,可以表示系数向量),则列表的列表转换为二维数组,并且列表的列表的列表变为三维:
In: import numpy as np
# Transform a list into a bidimensional array
a_list_of_lists = [[1,2,3],[4,5,6],[7,8,9]]
Array_2D = np.array(a_list_of_lists )
Array_2D Out: array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
如前所述,您可以在列表中用索引调出单个值,尽管这里您将有两个索引-一个用于行维(也称为轴 0),一个用于列维(轴 1):
In: Array_2D[1, 1] Out: 5
二维数组通常是数据科学问题中的常态,尽管当一个维表示时间时可以找到三维数组,例如:
In: # Transform a list into a multi-dimensional array
a_list_of_lists_of_lists = [[[1,2],[3,4],[5,6]],
[[7,8],[9,10],[11,12]]]
Array_3D = np.array(a_list_of_lists_of_lists)
Array_3D
Out: array([[[ 1, 2],
[ 3, 4], [ 5, 6]], [[ 7, 8], [ 9, 10], [11, 12]]])
要访问三维数组的单个元素,只需指出三个索引:
In: Array_3D[0,2,0] # Accessing the 5th element Out: 5
可以用类似于创建列表的方法从元组中创建数组。 另外,借助.items()方法,字典可以转换为二维数组,该方法返回字典的键值对列表的副本:
In: np.array({1:2,3:4,5:6}.items())
Out: array([[1, 2],
[3, 4],
[5, 6]])
调整数组大小
之前,我们提到了如何更改数组元素的类型。 现在,我们将停一会儿,以研究最常见的指令来修改现有数组的形状。
让我们从使用.reshape方法的示例开始,该方法接受n-元组,其中包含新尺寸的大小作为参数:
In: import numpy as np
# Restructuring a NumPy array shape
original_array = np.array([1, 2, 3, 4, 5, 6, 7, 8])
Array_a = original_array.reshape(4,2)
Array_b = original_array.reshape(4,2).copy()
Array_c = original_array.reshape(2,2,2)
# Attention because reshape creates just views, not copies
original_array[0] = -1
我们原始的数组是一个整数,从 1 到 8 的一维向量。这是我们在代码中执行的内容:
- 我们将
Array_a分配给大小为4 x 2的重塑original_array - 我们对
Array_b进行了相同的操作,尽管我们追加了.copy()方法,该方法会将数组复制到新数组中 - 最后,我们将
Array_c分配给尺寸为2 x 2 x 2的三个维度的变形数组 - 完成分配后,
original_array的第一个元素的值从1更改为-1
现在,如果我们检查数组的内容,我们将注意到Array_a和Array_c尽管具有所需的形状,但它们以-1为第一个元素来表征。 那是因为它们动态地镜像了他们在视图中所见的原始数组:
In: Array_a Out: array([[-1, 2],
[3, 4],
[5, 6],
[7, 8]]) In: Array_c Out: array([[[-1, 2],
[3, 4]],
[[5, 6],
[7, 8]]])
只有Array_b数组在突变原始数组之前已被复制,其第一个元素的值为1:
In: Array_b Out: array([[1, 2],
[3, 4], [5, 6], [7, 8]])
如果有必要更改原始数组的形状,则首选resize方法:
In: original_array.resize(4,2)
original_array Out: array([[-1, 2],
[ 3, 4], [ 5, 6], [ 7, 8]])
通过分配表示预期尺寸的大小的值的元组,对.shape值进行操作,可以获得相同的结果:
In: original_array.shape = (4,2)
相反,如果您的数组是二维的,并且您需要与列交换行,即要转置数组,则.T或.transpose()方法将帮助您获得这种转换( 视图,例如.reshape):
In: original_array Out: array([[-1, 2],
[ 3, 4], [ 5, 6], [ 7, 8]])
从 NumPy 函数派生的数组
如果您需要一个具有特定数字序列(零,一,序数和特定统计分布)的向量或矩阵,则 NumPy 函数可为您提供多种选择。
首先,如果使用arange函数,则创建一个序数(整数)的 NumPy 数组很简单,该函数会在给定的时间间隔(通常从零开始)中返回整数值,并调整其结果的形状:
In: import numpy as np In: ordinal_values = np.arange(9).reshape(3,3)
ordinal_values Out: array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
如果必须按值顺序反转数组,请使用以下命令:
In: np.arange(9)[::-1]
Out: array([8, 7, 6, 5, 4, 3, 2, 1, 0])
如果整数只是随机的(无序且可能重复),请提供以下命令:
In: np.random.randint(low=1,high=10,size=(3,3)).reshape(3,3)
其他有用的数组由零和一组成,或为单位矩阵:
In: np.zeros((3,3)) In: np.ones((3,3)) In: np.eye(3)
如果将数组用于网格搜索以搜索最佳参数,则间隔或对数增长中的分数值应被证明是最有用的:
In: fractions = np.linspace(start=0, stop=1, num=10)
growth = np.logspace(start=0, stop=1, num=10, base=10.0)
取而代之的是,统计分布,例如正态分布或均匀分布,对于系数的向量或矩阵的初始化可能是方便的。
在这里可以看到3 x 3的标准化标准值矩阵(平均值为 0,标准差为 1):
In: std_gaussian = np.random.normal(size=(3,3))
如果需要指定其他均值和标准差,则只需输入以下命令:
In: gaussian = np.random.normal(loc=1.0, scale= 3.0, size=(3,3))
loc参数代表平均值,scale实际上是标准差。
用于初始化向量的统计分布的另一个常见选择当然是均匀分布:
In: rand = np.random.uniform(low=0.0, high=1.0, size=(3,3))
直接从文件获取数组
NumPy 数组也可以直接从文件中存在的数据创建。
让我们使用上一章中的示例:
In: import numpy as np
housing = np.loadtxt('regression-datasets-housing.csv',
delimiter=',', dtype=float)
给定filename,delimiter和dtype的 NumPy loadtxt会将数据上传到数组,除非dtype错误; 例如,有一个string变量,所需的数组类型是float,如以下示例所示:
In: np.loadtxt('datasets-uci-iris.csv',delimiter=',',dtype=float) Out: ValueError: could not convert string to float: Iris-setosa
在这种情况下,可行的解决方案可能是知道字符串是哪一列(或任何其他非数字格式),并借助loadtxt的converters参数准备一个转换函数以将其转换为数字, 允许您将特定的转换函数应用于数组的特定列,例如以下示例:
In: def from_txt_to_iris_class(x):
if x==b'Iris-setosa': return 0 elif x==b'Iris-versicolor': return 1 elif x== b'Iris-virginica': return 2 else: return np.nan
np.loadtxt('datasets-uci-iris.csv', delimiter=',', converters= {4: from_txt_to_iris_class})
从 Pandas 中提取数据
与 Pandas 互动非常容易。 实际上,通过在 NumPy 上构建 Pandas,可以轻松地从DataFrame对象中提取数组,并将它们本身转换为DataFrame。
首先,让我们将一些数据上传到DataFrame中。 我们在上一章从 ML 存储库中下载的BostonHouse示例是合适的:
In: import pandas as pd
import numpy as np
housing_filename = 'regression-datasets-housing.csv'
housing = pd.read_csv(housing_filename, header=None)
如“异构列表”部分中所演示的,此时.values方法将提取一个类型数组,该数组容纳DataFrame中存在的所有不同类型:
In: housing_array = housing.values
housing_array.dtype Out: dtype('float64')
在这种情况下,所选类型为float64,因为浮点类型优先于int类型:
In: housing.dtypes Out: 0 float64
1 int64
2 float64
3 int64
4 float64
5 float64
6 float64
7 float64
8 int64
9 int64
10 int64
11 float64
12 float64
13 float64
dtype: object
在使用DataFrame上的.dtypes方法提取 NumPy 数组之前询问DataFrame对象使用的类型,可以使您预期所得数组的dtype。 因此,它允许您在继续操作之前决定是要转换还是更改DataFrame对象中的变量类型(请参考本章“处理类别和文本数据”)。
NumPy 快速运算和计算
当需要通过数学运算来操纵数组时,您只需要针对数字常数(标量)或形状相同的数组在数组上应用该运算:
In: import numpy as np
a = np.arange(5).reshape(1,5) a += 1 a*a Out: array([[ 1, 4, 9, 16, 25]])
结果,该操作将按元素进行; 也就是说,数组的每个元素都可以通过标量值或另一个数组的相应元素来操作。
当对不同维数的数组进行操作时,如果其中一个维数为 1,则仍然可以进行元素操作,而不必重新构建数据。实际上,在这种情况下,大小为 1 的维数会一直扩展来匹配相应数组的尺寸。 这种转换称为广播。
例如:
In: a = np.arange(5).reshape(1,5) + 1
b = np.arange(5).reshape(5,1) + 1
a * b Out: array([[ 1, 2, 3, 4, 5],
[ 2, 4, 6, 8, 10],
[ 3, 6, 9, 12, 15],
[ 4, 8, 12, 16, 20],
[ 5, 10, 15, 20, 25]])
上面的代码等效于以下代码:
In: a2 = np.array([1,2,3,4,5] * 5).reshape(5,5)
b2 = a2.T
a2 * b2
但是,它不需要扩展原始数组的内存即可获得成对乘法。
此外,存在大量可以对数组进行元素方式操作的 NumPy 函数:abs(),sign(),round(),floor(),sqrt(),log()和exp()。
可以由 NumPy 函数进行操作的其他常用操作是sum()和prod(),它们根据指定的轴提供数组行或列的求和和乘积:
In: print (a2) Out: [[1 2 3 4 5]
[1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5]] In: np.sum(a2, axis=0) Out: array([ 5, 10, 15, 20, 25]) In: np.sum(a2, axis=1) Out: array([15, 15, 15, 15, 15])
在对数据进行操作时,请记住,与简单的 Python 列表相比,数组上的操作和 NumPy 函数非常快。 让我们尝试一些实验。 首先,让我们尝试在处理常量之和时将列表理解与数组进行比较:
In: %timeit -n 1 -r 3 [i+1.0 for i in range(10**6)]
%timeit -n 1 -r 3 np.arange(10**6)+1.0 Out: 1 loops, best of 3: 158 ms per loop
1 loops, best of 3: 6.64 ms per loop
在 Jupyter 上,%time使您可以轻松地对操作进行基准测试。 然后,-n 1参数仅要求基准测试仅执行一个循环的代码段; -r 3要求您重试循环执行 3 次(在这种情况下,仅执行一次循环),并报告从此类重复中记录的最佳性能。
计算机上的结果可能会有所不同,具体取决于您的配置和操作系统。 无论如何,标准 Python 操作和 NumPy 操作之间的差异将仍然很大。 尽管在处理小型数据集时并不明显,但这种差异确实会影响您在处理较大数据时或在同一分析管道上循环进行参数或变量选择时的分析。
当应用复杂的操作(例如求平方根)时,也会发生这种情况:
In: import math
%timeit -n 1 -r 3 [math.sqrt(i) for i in range(10**6)] Out: 1 loops, best of 3: 222 ms per loop In: %timeit -n 1 -r 3 np.sqrt(np.arange(10**6)) Out: 1 loops, best of 3: 6.9 ms per loop
有时,您可能需要将自定义函数应用于数组。 apply_along_axis函数可让您使用自定义函数并将其应用于数组的轴上:
In: def cube_power_square_root(x):
return np.sqrt(np.power(x, 3)) np.apply_along_axis(cube_power_square_root,axis=0, arr=a2) Out: array([[ 1., 2.82842712, 5.19615242, 8., 11.18033989],
[ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989]])
矩阵运算
除了使用np.dot()函数进行逐元素计算外,您还可以基于矩阵计算将乘法应用于二维数组,例如向量矩阵和矩阵矩阵乘法:
In: import numpy as np M = np.arange(5*5, dtype=float).reshape(5,5) M
Out: array([[ 0., 1., 2., 3., 4.], [ 5., 6., 7., 8., 9.], [ 10., 11., 12., 13., 14.], [ 15., 16., 17., 18., 19.], [ 20., 21., 22., 23., 24.]])
例如,我们将创建一个5 x 5的二维数组,其中包含从 0 到 24 的序数:
- 我们将定义一个系数向量和一个将向量及其反面堆叠的数组列:
In: coefs = np.array([1., 0.5, 0.5, 0.5, 0.5])
coefs_matrix = np.column_stack((coefs,coefs[::-1]))
print (coefs_matrix) Out: [[ 1\. 0.5]
[ 0.5 0.5] [ 0.5 0.5] [ 0.5 0.5] [ 0.5 1\. ]]
- 现在,我们可以使用
np.dot函数将数组与向量相乘:
In: np.dot(M,coefs) Out: array([ 5., 20., 35., 50., 65.])
- 或向量到数组:
In: np.dot(coefs,M) Out: array([ 25., 28., 31., 34., 37.])
- 或由堆叠系数向量组成的数组(这是一个
5 x 2矩阵):
In: np.dot(M,coefs_matrix) Out: array([[ 5., 7.],
[ 20., 22.], [ 35., 37.], [ 50., 52.], [ 65., 67.]])
NumPy 还提供了一个对象类矩阵,它实际上是ndarray的子类,继承了其所有属性和方法。 默认情况下,NumPy 矩阵仅是二维的(因为数组实际上是多维的)。 当相乘时,它们应用矩阵乘积,而不是按元素的乘积(提高幂时也会发生这种情况),并且它们具有一些特殊的矩阵方法(共轭转置使用.H,逆使用.I)。
除了以类似于 MATLAB 的方式进行操作带来的便利之外,它们没有提供任何其他优势。 您可能会有脚本混乱的风险,因为您必须为矩阵对象和数组处理不同的产品表示法。
从 Python 3.5 开始,Python 中引入了一个新的运算符@(AT)运算符,专门用于矩阵乘法(更改适用于 Python 中的所有包,而不仅仅是 NumPy)。 引入此新运算符会带来很多好处。
首先,不会再有*运算符用于矩阵乘法的情况了。 *运算符将仅用于按元素的操作(在具有相同维的两个矩阵(或向量)的情况下,您将操作应用于两个矩阵中具有相同位置的元素之间的运算)。
然后,表示公式的代码将具有更高的可读性,因此变得更易于阅读和解释。 您将不再需要一起评估运算符(+ - / *)和方法(.),只需评估运算符(+ - / * @)。
您可以了解有关此介绍的更多信息(这只是形式上的-使用.dot方法可与@运算符一起使用之前可以应用的所有内容),并通过阅读 Python 查看一些应用示例。 Python 基金会网站上的增强提案(PEP465)。
NumPy 数组的切片和索引
索引使我们可以通过指出要可视化的列和行的切片或索引来查看ndarray:
- 让我们定义一个工作数组:
In: import numpy as np
M = np.arange(10*10, dtype=int).reshape(10,10)
- 我们的数组是一个
10 x 10的二维数组。 我们首先可以将其切成一个维度。 单一维度的符号与 Python 列表中的符号相同:
[start_index_included:end_index_exclude:steps]
- 假设我们要提取 2 到 8 的偶数行:
In: M[2:9:2,:]
Out: array([[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]])
- 在对行进行切片之后,我们可以通过仅从索引
5中获取列来进一步对列进行切片:
In: M[2:9:2,5:]
Out: array([[25, 26, 27, 28, 29],
[45, 46, 47, 48, 49], [65, 66, 67, 68, 69], [85, 86, 87, 88, 89]])
- 如列表中所示,可以使用负索引值以从头开始计数。 此外,参数的负数(例如步长)会颠倒输出数组的顺序,如以下示例所示,其中计数从列索引
5开始但以相反的顺序进行,并朝索引0进行:
In: M[2:9:2,5::-1]
Out: array([[25, 24, 23, 22, 21, 20],
[45, 44, 43, 42, 41, 40], [65, 64, 63, 62, 61, 60], [85, 84, 83, 82, 81, 80]])
- 我们还可以创建布尔索引,指出要选择的行和列。 因此,我们可以使用
row_index和col_index变量来复制前面的示例:
In: row_index = (M[:,0]>=20) & (M[:,0]<=80)
col_index = M[0,:]>=5
M[row_index,:][:,col_index]
Out: array([[25, 26, 27, 28, 29],
[35, 36, 37, 38, 39], [45, 46, 47, 48, 49], [55, 56, 57, 58, 59], [65, 66, 67, 68, 69], [75, 76, 77, 78, 79],
[85, 86, 87, 88, 89]])
尽管我们可以使用整数索引将通常的索引应用于其他维度,但我们无法在同一方括号中的列和行上在上下文上使用布尔索引。 因此,我们必须首先对行进行布尔选择,然后重新打开方括号,然后对第一个选择第二个选择,这次重点关注列。
- 如果需要全局选择数组中的元素,则还可以使用布尔值的掩码,如下所示:
In: mask = (M>=20) & (M<=90) & ((M / 10.) % 1 >= 0.5)
M[mask] Out: array([25, 26, 27, 28, 29, 35, 36, 37, 38, 39, 45, 46, 47, 48, 49,
55, 56, 57, 58, 59, 65, 66, 67, 68, 69, 75, 76, 77, 78, 79,
85, 86, 87, 88, 89])
如果您需要对由掩码选择的数组分区进行操作(例如M[mask]=0),则此方法特别有用。
指出需要从数组中选择哪些元素的另一种方法是通过提供由整数组成的行或列索引。 可以通过将数组上的布尔条件转换为索引的np.where()函数来定义此类索引,也可以通过简单地提供整数索引序列来定义此类索引,其中整数可以按特定顺序排列,甚至可以重复。 这种方法称为花式索引:
In: row_index = [1,1,2,7]
col_index = [0,2,4,8]
定义行和列的索引后,您必须根据上下文应用它们以选择其坐标由两个索引的值元组给定的元素:
In: M[row_index,col_index] Out: array([10, 12, 24, 78])
这样,选择将报告以下几点:(1,0),(1,2),(2,4)和(7,8)。 否则,如前所述,您只需要先选择行,然后再选择由方括号分隔的列:
In: M[row_index,:][:,col_index] Out: array([[10, 12, 14, 18],
[10, 12, 14, 18],
[20, 22, 24, 28],
[70, 72, 74, 78]])
最后,请记住切片和索引只是数据视图。 如果需要从此类视图创建新数据,则必须在切片上使用.copy方法并将其分配给另一个变量。 否则,对原始数组的任何修改都会反映在您的切片上,反之亦然。 复制方法如下所示:
In: N = M[2:9:2,5:].copy()
堆叠 NumPy 数组
当使用二维数据数组进行操作时,NumPy 函数可以轻松快速地执行一些常用操作,例如添加数据和变量。
最常见的此类操作是在数组中添加更多案例:
- 让我们从创建一个数组开始:
In: import numpy as np
dataset = np.arange(10*5).reshape(10,5)
- 现在,让我们添加一行和一排要彼此连接的行:
In: single_line = np.arange(1*5).reshape(1,5)
a_few_lines = np.arange(3*5).reshape(3,5)
- 我们可以首先尝试添加一行:
In: np.vstack((dataset,single_line))
- 您所要做的就是提供一个元组,其中包含前一个垂直数组和后一个垂直数组。 在我们的示例中,如果要添加更多行,则相同的命令可以工作:
In: np.vstack((dataset,a_few_lines))
- 或者,如果您想多次添加同一行,则该元组可以表示新连接数组的顺序结构:
In: np.vstack((dataset,single_line,single_line))
另一种常见情况是,您必须向现有数组添加新变量。 在这种情况下,您必须使用hstack(h代表水平),而不是刚刚显示的vstack命令(其中v为垂直)。
- 假设您必须向原始数组中添加单位值的
bias:
In: bias = np.ones(10).reshape(10,1)
np.hstack((dataset,bias))
- 无需更改
bias的形状(因此,它可以是与数组行相同长度的任何数据序列),您可以使用column_stack()函数将其作为序列添加,该函数可获得相同的结果,但数据重塑的问题数量较少:
In: bias = np.ones(10)
np.column_stack((dataset,bias))
在二维科学数组中添加行和列基本上是在数据科学项目中有效处理数据所需要做的全部工作。 现在,让我们看一些针对稍微不同的数据问题的更具体的函数。
首先,尽管二维数组是常态,但您也可以对三维数据结构进行操作。 因此,类似于hstack()和vstack()但在第三轴上运行的dstack()将非常方便:
In: np.dstack((dataset*1,dataset*2,dataset*3))
在此示例中,第三维向原始 2D 数组提供了被乘数,表示变化的渐进速率(时间或变化维度)。
另一个有问题的变化可能是在您的数组中的特定位置插入一行,或更常见的是插入一列。 您可能还记得,数组是连续的内存块。 插入实际上需要重新创建一个新数组,以拆分原始数组。 NumPy insert命令可以帮助您快速而轻松地进行操作:
In: np.insert(dataset, 3, bias, axis=1)
您只需要定义要插入的数组(dataset),位置(索引3),要插入的序列(在本例中为数组bias)以及沿其插入的轴(轴1是垂直轴)。
自然地,您可以通过确保要插入的数组与操作插入的尺寸对齐来插入整个数组(而不仅仅是向量),例如偏移。 在此示例中,为了将相同的数组插入自身,我们必须将其作为插入的元素进行转置:
In: np.insert(dataset, 3, dataset.T, axis=1)
您还可以在不同的轴上进行插入(在以下情况下,轴0是水平轴,但也可以在可能具有的数组的任何维上进行操作):
In: np.insert(dataset, 3, np.ones(5), axis=0)
所做的是将原始数组沿所选轴在指定位置拆分。 然后,将拆分数据与要插入的新数据连接在一起。
使用稀疏数组
稀疏矩阵是其值大多为零的矩阵。 当处理某些类型的数据问题(例如自然语言处理(NLP),数据计数事件(例如客户购买),分类数据转换为二进制变量( 一种称为“单热编码”的技术,我们将在下一章中对其进行讨论),如果图像中有很多黑色像素,甚至是图像。
使用正确的工具来处理稀疏矩阵,因为它们代表了大多数机器学习算法的内存和计算难题。
首先,稀疏矩阵很大(如果作为普通矩阵处理,它们将无法容纳到内存中),并且它们大多数包含零值,但包含几个单元格。 针对稀疏矩阵进行了优化的数据结构使我们能够高效地存储矩阵,其中大多数值为零的元素都不会占用任何内存空间。 相反,在任何 NumPy 数组中(相反,我们将其称为密集数组),任何零值都会占用一些内存空间,因为数组会跟踪所有值。
另外,稀疏矩阵很大,需要大量计算才能进行处理,但是,它们的大多数值并未用于任何预测。 与在密集矩阵上运行的标准算法相比,可以利用稀疏矩阵数据结构的算法所执行的计算时间要少得多。
在 Python 中,SciPy 的稀疏模块提供了能够解决稀疏问题的不同稀疏数据结构。 更具体地说,它提供了七种不同的稀疏矩阵:
csc_matrix:压缩的稀疏列格式csr_matrix:压缩的稀疏行格式bsr_matrix:块稀疏行格式lil_matrix:列表格式dok_matrix:键的字典格式coo_matrix:坐标格式(也称为 IJV,三联格式)dia_matrix:对角格式
每种矩阵都具有不同的方式来存储稀疏信息,这是一种特殊的方式,可以影响矩阵在不同情况下的性能。 我们将说明每种稀疏矩阵类型,并查看哪些操作快速有效,哪些操作根本不执行。 例如,文档指出dok_matrix,lil_matrix或coo_matrix是从头开始构建稀疏矩阵的最佳方法。 我们将从coo_matrix开始讨论这个问题。
You can find all of SciPy's documentation about sparse matrices at https://docs.scipy.org/doc/scipy/reference/sparse.html.
让我们开始创建一个稀疏矩阵:
- 为了创建稀疏矩阵,您可以从 NumPy 数组生成它(仅通过将数组传递给 SciPy 的稀疏矩阵格式之一),或通过向 COO 矩阵分别提供包含行索引,列索引和数据值的三个向量:
In: row_idx = np.array([0, 1, 3, 3, 4])
col_idx = np.array([1, 2, 2, 4, 2]) values = np.array([1, 1, 2, 1, 2], dtype=float) sparse_coo = sparse.coo_matrix((values, (row_idx, col_idx))) sparse_coo Out: <5x5 sparse matrix of type '<class 'numpy.float64'>' with 5 stored elements in COOrdinate format>
- 调用 COO 矩阵将告诉您形状以及它包含多少个非零元素。 零元素的数量相对于矩阵的大小将为您提供稀疏度度量,可以通过以下方式计算稀疏度:
In: sparsity = 1.0 - (sparse_coo.count_nonzero() /
np.prod(sparse_coo.shape))
print(sparsity)
Out: 0.8
稀疏度为0.8; 也就是说,矩阵的 80% 实际上是空的。
您也可以使用 Matplotlib 中的spy命令以图形方式研究稀疏性。 在下面的示例中,我们将创建一个随机的稀疏矩阵,并以图形形式轻松表示它,以提供一个关于矩阵中有效可用数据的概念:
In: import matplotlib.pyplot as plt
%matplotlib inline
large_sparse = sparse.random(10 ** 3, 10 ** 3, density=0.001, format='coo') plt.spy(large_sparse, marker=',') plt.show()
生成的图形将为您提供矩阵中空白区域的概念:

如果需要,您始终可以使用to_dense:sparse_coo.to_dense()方法将稀疏矩阵转换为密集矩阵。
您可以尝试通过打印来确定 COO 矩阵的构成:
In: print(sparse_coo) Out: (0, 1) 1.0
(1, 2) 1.0 (3, 2) 2.0 (3, 4) 1.0 (4, 2) 2.0
从输出表示中,我们可以得出一个稀疏坐标格式矩阵的工作原理是将打印值存储在三个单独的存储数组中:一个用于x坐标,一个用于y坐标,以及一个用于值。 这意味着在插入信息时,COO 矩阵的确非常快(每个新元素是每个存储数组中的新行),但是处理起来缓慢,因为它无法通过扫描数组立即找出行或列中的值。
对于键字典(DOK)和列表的列表(LIL)也是如此。 第一个使用坐标字典操作(因此它可以快速检索单个元素),第二个使用两个列表,两个列表被排列来代表行,包含该行中的非零坐标以及其他值(很容易通过添加更多行进行扩展)。
COO 矩阵的另一个优点是,它们可以立即转换为专门用于在行或列级别有效工作的其他类型的矩阵:CSR 和 CSC 矩阵。
压缩的稀疏行(CSR)和压缩的稀疏列(CSC)是创建稀疏矩阵后用于操作它们的最常用的格式。 他们使用的索引系统更支持对 CSR 的行和对 CSC 的列进行计算。 但是,这使得编辑的计算量很大(因此,创建它们后更改它们并不方便)。
CSR 和 CSC 的性能实际上取决于所使用的算法及其优化参数的方式。 您必须实际对算法进行试验,以找出效果最佳的算法。
最后,对角线格式矩阵是专用于对角线矩阵和块稀疏行格式矩阵的稀疏数据结构。 除了基于整个数据块的数据存储方式外,这些特性在本质上类似于 CSR 矩阵。
总结
在本章中,我们讨论了 Pandas 和 NumPy 如何为您提供所有工具来加载和有效地修剪数据。
我们从 Pandas 及其数据结构,DataFrame和序列开始,然后进行到最终的 NumPy 二维数组,该数组具有适合于后续实验和机器学习的数据结构。 在此过程中,我们涉及到诸如向量和矩阵的操纵,分类数据编码,文本数据处理,修复丢失的数据和错误,切片和切块,合并和堆叠等主题。
Pandas 和 NumPy 肯定比我们在此介绍的基本构建块以及所示的命令和过程提供更多的功能。 现在,您可以获取任何可用的原始数据,并应用数据科学项目所需的所有清理和整形转换。
在下一章中,我们将进行数据操作。 我们已经对机器学习过程正常运行所需的所有基本数据处理操作进行了简要概述。 在下一章中,我们将讨论可能改善甚至提高结果的所有操作。
三、数据管道
到目前为止,我们已经探索了如何将数据加载到 Python 中并进行处理,以创建一个包含数值(您的数据集)的二维 NumPy 数组。 现在,我们准备完全融入数据科学,从数据中提取含义,并开发潜在的数据产品。 关于数据处理和转换的这一章以及关于机器学习的下一章是整本书中最具挑战性的部分。
在本章中,您将学习如何执行以下操作:
- 简要浏览数据并创建新特征
- 降低数据的维数
- 发现并处理异常值
- 确定项目的最佳得分或损失指标
- 应用科学的方法论并有效地测试您的机器学习假设的表现
- 通过减少特征部件数量来降低数据科学问题的复杂性
- 优化您的学习参数
介绍 EDA
探索性数据分析(EDA)或数据探索是数据科学过程中的第一步。 约翰·图基(John Tukey)在 1977 年首次撰写他的著作《探索性数据分析》时就创造了这个名词,强调了 EDA 的重要性。 要求 EDA 更好地理解数据集,检查其特征和形状,验证您要记住的一些第一个假设,并获得有关在后续后续数据科学任务中要进行的下一步的初步思路。
在本节中,您将处理上一章中已使用的鸢尾花数据集。 首先,让我们加载数据集:
In: import pandas as pd
iris_filename = 'datasets-uci-iris.csv'
iris = pd.read_csv(iris_filename, header=None,
names= ['sepal_length', 'sepal_width',
'petal_length', 'petal_width', 'target'])
iris.head()
调用head方法将显示前五行:

伟大的! 使用一些命令,您已经加载了数据集。 现在,调查阶段开始。 .describe()方法提供了一些很好的见解,可以按如下方式使用:
In: iris.describe()
随即出现了对数据集的描述,包括频率,均值和其他描述性信息:

对于所有数字特征,您都有观测值的数量,它们各自的平均值,标准差,最小值和最大值以及一些常规报告的分位数(以25百分比,50百分比和75百分比), 所谓的四分位数。 这为您提供了有关每个特征的分布的一个好主意。 如果要可视化此信息,只需使用boxplot()方法,如下所示:
In: boxes = iris.boxplot(return_type='axes')
每个变量的箱线图将出现:

有时,本章中介绍的图形/图表可能与在本地计算机上获得的图形/图表略有不同,因为图形布局的初始化是使用随机参数进行的。
如果需要了解其他分位数,可以使用.quantile()方法。 例如,如果您需要的值是值分布的 10% 和 90%,则可以尝试以下代码:
In: iris.quantile([0.1, 0.9])
以下是所需百分位数的值:

最后,要计算中位数,可以使用.median()方法。 同样,为了获得均值和标准差,分别使用了.mean()和.std()方法。 对于分类特征,要获取有关特征中存在的级别的信息(即特征所采用的不同值),可以使用.unique()方法,如下所示:
In: iris.target.unique()
Out: array(['Iris-setosa', 'Iris-versicolor', 'Iris-virginica'],
dtype=object)
要检查特征之间的关系,可以创建同现矩阵或相似矩阵。
在下面的示例中,我们将针对petal_width函数的相同计数计算petal_length函数出现的次数多于平均值的次数。 为此,您需要使用crosstab方法,如下所示:
In: pd.crosstab(iris['petal_length'] > 3.758667,
iris['petal_width'] > 1.198667)
该命令生成一个双向表:

结果,您将注意到这些特征几乎总是联合出现的。 因此,您可以假设这两个事件之间存在很强的关系。 以图形方式,您可以使用以下代码检查这样的假设:
In: scatterplot = iris.plot(kind='scatter',
x='petal_width', y='petal_length',
s=64, c='blue', edgecolors='white')
您将获得指定为x和y的变量的散点图:

趋势十分明显。 我们推断x和y密切相关。 您通常在 EDA 期间执行的最后一项操作是检查特征的分布。 要使用 Pandas 进行管理,您可以使用直方图来估计分布,这要归功于以下代码段:
In: distr = iris.petal_width.plot(kind='hist', alpha=0.5, bins=20)
结果,显示直方图:

经过仔细搜索,我们选择了 20 个箱子。 在其他情况下,20 个箱子可能是一个极低或极高的值。 根据经验,绘制分布直方图时,起始值为观察数的平方根。 初始可视化之后,您将需要修改箱子的数量,直到您在分布中识别出众所周知的形状为止。
我们建议您探索所有特征,以检查它们之间的关系并估计其分布。 实际上,鉴于其分布,您可能决定对每个特征进行不同的处理,以便随后获得最大的分类或回归表现。
建立新特征
有时,您会发现自己与特征和target变量没有真正的联系。 在这种情况下,您可以修改输入数据集。 您可以应用线性或非线性变换来提高系统的精度,等等。 这是整个过程中非常重要的一步,因为它完全取决于数据科学家的技能,后者是人为地更改数据集并调整输入数据以更好地适合学习模型的人。 尽管此步骤直观地增加了复杂性,但是这种方法通常可以提高学习器的表现; 这就是为什么它被诸如深度学习之类的尖端技术所使用的原因。
例如,如果您要预测房屋的价值并且知道每个房间的高度,宽度和长度,则可以人为地构建代表房屋体积的特征。 严格来说,这不是可观察的特征,而是在现有特征之上构建的特征。 让我们从一些代码开始:
In: import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
cali = datasets.california_housing.fetch_california_housing()
X = cali['data']
Y = cali['target']
X_train, X_test, Y_train, Y_test = train_test_split(X, Y,
test_size=0.2)
我们导入了包含加州房价的数据集。 这是一个回归问题,因为target变量是房价(即实数)。 立即应用称为 KNN 回归器的简单回归器(以简单学习器为例;将在第 4 章,机器学习),在测试数据集上的平均绝对误差(MAE)结尾。 如果您不能完全理解代码,请不要担心。 本书稍后将介绍 MAE 和其他回归变量。 现在,假设 MAE 代表误差。 因此,MAE 的值越低,解决方案越好:
In: from sklearn.neighbors import KNeighborsRegressor
regressor = KNeighborsRegressor()
regressor.fit(X_train, Y_train)
Y_est = regressor.predict(X_test)
print ("MAE=", mean_squared_error(Y_test, Y_est))
Out: MAE= 1.07452795578
1.07的MAE结果看似不错,但让我们努力做得更好。 我们将使用 Z 分数对输入特征进行归一化,并比较此新特征集上的回归任务。 Z 归一化只是将每个特征映射到具有零均值和单一方差的新特征。 使用 Scikit-learn,可以通过以下方式实现:
In: from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
regressor = KNeighborsRegressor()
regressor.fit(X_train_scaled, Y_train)
Y_est = regressor.predict(X_test_scaled)
print ("MAE=", mean_squared_error(Y_test, Y_est)) Out: MAE= 0.402334179429
借助这一简单的步骤,我们将 MAE 降低了一半以上,现在的值约为0.40。
请注意,我们没有使用原始特征。 我们使用了它们的线性修改,这更适合使用 KNN 回归器进行学习。
代替 Z 归一化,我们可以在对异常值更鲁棒的特征上使用缩放函数,即RobustScaler。这样的缩放器使用中位数和四分位间距(IQR),而不是使用均值和标准差来独立缩放每个特征。 它比异常值更健壮,因为如果几个点(最终只有一个)距离中心较远,例如由于错误的读数,传输错误或传感器损坏:
In: from sklearn.preprocessing import RobustScaler
scaler2 = RobustScaler()
X_train_scaled = scaler2.fit_transform(X_train)
X_test_scaled = scaler2.transform(X_test)
regressor = KNeighborsRegressor()
regressor.fit(X_train_scaled, Y_train)
Y_est = regressor.predict(X_test_scaled)
print ("MAE=", mean_squared_error(Y_test, Y_est)) Out: MAE=0.41749216189
现在,让我们尝试为特定特征添加非线性修改。 我们可以假设输出大致与房屋的占用人数有关。 实际上,一个人居住的房屋的价格与三个人住在同一所房屋的价格之间存在很大差异。 但是,居住在那里的 10 个人的价格与居住在那里的 12 个人的价格之间的差异并不大(尽管两者之间仍然存在差异)。 因此,让我们尝试添加另一个特征,该特征是对另一个特征进行非线性转换而构建的:
In: non_linear_feat = 5 # AveOccup
X_train_new_feat = np.sqrt(X_train[:,non_linear_feat])
X_train_new_feat.shape = (X_train_new_feat.shape[0], 1)
X_train_extended = np.hstack([X_train, X_train_new_feat])
X_test_new_feat = np.sqrt(X_test[:,non_linear_feat])
X_test_new_feat.shape = (X_test_new_feat.shape[0], 1)
X_test_extended = np.hstack([X_test, X_test_new_feat])
scaler = StandardScaler()
X_train_extended_scaled = scaler.fit_transform(X_train_extended)
X_test_extended_scaled = scaler.transform(X_test_extended)
regressor = KNeighborsRegressor()
regressor.fit(X_train_extended_scaled, Y_train)
Y_est = regressor.predict(X_test_extended_scaled)
print ("MAE=", mean_squared_error(Y_test, Y_est))
Out: MAE= 0.325402604306
通过添加此新特征,我们额外减少了MAE,最终获得了更令人满意的回归指标。 当然,我们可以尝试其他转换来改善此情况,但是这个简单的示例应该表明,对您分析 EDA 发现的线性和非线性转换的应用并获得在概念上与转换更为相关的特征有多重要。 输出变量。
降维
通常,您必须处理包含大量特征的数据集,其中许多特征可能是不必要的。 这是一个典型的问题,其中某些特征对预测很有帮助,某些特征以某种方式相关,而某些特征则完全不相关(也就是说,它们仅包含噪声或不相关的信息)。 仅保留有趣的特征是一种方法,不仅可以使数据集更易于管理,而且可以使预测算法更好地工作,而不会因数据中的噪声而使其预测迷惑。
因此,降维是消除输入数据集的某些特征并创建一组受限制的特征的操作,这些特征包含了以更有效和可靠的方式预测target变量所需的所有信息。 如前所述,减少特征部件的数量通常还会减少输出变异性和学习过程的复杂性(以及所需的时间)。
许多用于归约的算法的主要假设是与加性高斯白噪声(AWGN)有关的一种假设。 我们假设一个独立的高斯型噪声已添加到数据集的每个特征中。 因此,减小维数也会减少噪声的能量,因为您要减小其跨度设置。
协方差矩阵
协方差矩阵为您提供了所有不同对特征之间的相关性的概念。 通常,这是降维的第一步,因为它使您可以了解高度相关的特征数量(因此可以丢弃的特征数量)和独立的特征数量。 使用每个观测具有四个特征的鸢尾花数据集,可以轻松地计算相关矩阵,并且您可以借助简单的图形表示来了解其结果,可以通过以下代码获得:
In: from sklearn import datasets
import numpy as np
iris = datasets.load_iris()
cov_data = np.corrcoef(iris.data.T)
print (iris.feature_names)
print (cov_data) Out: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)',
'petal width (cm)']
[[ 1\. -0.10936925 0.87175416 0.81795363]
[-0.10936925 1\. -0.4205161 -0.35654409]
[ 0.87175416 -0.4205161 1\. 0.9627571 ]
[ 0.81795363 -0.35654409 0.9627571 1\. ]]
使用热图,让我们以图形形式可视化协方差矩阵:
In: import matplotlib.pyplot as plt
img = plt.matshow(cov_data, cmap=plt.cm.rainbow)
plt.colorbar(img, ticks=[-1, 0, 1], fraction=0.045)
for x in range(cov_data.shape[0]):
for y in range(cov_data.shape[1]):
plt.text(x, y, "%0.2f" % cov_data[x,y],
size=12, color='black', ha="center", va="center")
plt.show()
这是生成的热图:

从上图中,您可以看到主要对角线的值为 1。这是因为我们使用的是协方差矩阵的归一化版本(将每个特征协方差归一化为 1.0)。 我们还可以注意到第一和第三特征,第一和第四以及第三和第四特征之间的高度相关性。 另外,我们可以验证只有第二个特征几乎独立于其他特征; 所有其他特征都以某种方式相互关联。
现在,我们有了关于简化集中潜在特征数量的想法,想象一下如何压缩相关矩阵指出的重复信息-我们可以将所有内容简单地简化为两个特征。
主成分分析
主成分分析(PCA)是一种有助于定义更小且更相关的特征集的技术。 从 PCA 获得的新特征是当前特征的线性组合(即旋转),即使它们是二进制的。 输入空间旋转后,输出集的第一个向量包含信号的大部分能量(或换句话说,其方差)。 第二个正交于第一个,它包含大部分剩余能量; 第三个与前两个向量正交,并且包含大部分剩余能量,依此类推。 就像通过将尽可能多的信息聚合到 PCA 产生的初始向量上来重构数据集中的信息一样。
在 AWGN 的理想情况下,初始向量包含输入信号的所有信息; 靠近末端的仅包含噪音。 此外,由于输出基础是正交的,因此您可以分解并合成输入数据集的近似版本。 能量是决定人能使用多少个基向量的关键参数。 由于该算法在本质上是用于奇异值分解的,因此在阅读有关 PCA 时经常会引用两个特征向量(基本向量)和特征值(与该向量相关的标准差)。 通常,输出集的基数是保证存在 95%(在某些情况下需要 90% 或 99%)输入能量(或方差)的基数。 对 PCA 的严格解释超出了本书的范围,因此,我们仅向您介绍有关如何在 Python 中使用此功能强大的工具的准则。
这是有关如何将数据集缩小为二维的示例。 在上一节中,我们推论 2 是降维的好选择; 让我们检查是否正确:
In: from sklearn.decomposition import PCA
pca_2c = PCA(n_components=2)
X_pca_2c = pca_2c.fit_transform(iris.data)
X_pca_2c.shape
Out: (150, 2) In: plt.scatter(X_pca_2c[:,0], X_pca_2c[:,1], c=iris.target, alpha=0.8,
s=60, marker='o', edgecolors='white')
plt.show()
pca_2c.explained_variance_ratio_.sum()
Out: 0.97763177502480336
在执行代码时,您还会获得前两个组件的散点图:

前两个组件的散点图
我们可以立即看到,在应用 PCA 之后,输出集仅具有两个特征。 这是因为在n_components参数设置为2的情况下调用了PCA()对象。 获得相同结果的另一种方法是对1,2和3组件运行PCA(),然后从解释的方差比和视觉检查得出结论,对于n_components = 2,我们得到了最好的结果。 然后,我们将有证据表明,当使用两个基向量时,输出数据集包含几乎 98% 的输入信号能量,并且在模式中,这些类几乎可以很好地分离。 每种颜色位于二维欧几里得空间的不同区域。
请注意,此过程是自动的,训练 PCA 时不需要提供标签。 实际上,PCA 是一种无监督算法,它不需要与自变量有关的数据来旋转投影基础。
对于好奇的读者,可以通过以下代码查看转换矩阵(将原始数据集转换为 PCA 重组的数据集):
In: pca2c.componentsOut: array([[ 0.36158968, -0.08226889, 0.85657211, 0.35884393],
[-0.65653988, -0.72971237, 0.1757674 , 0.07470647]])
转换矩阵由四列(即输入特征的数量)和两行(即归约特征的数量)组成。
有时,您会发现自己处于 PCA 不够有效的情况下,尤其是在处理高维数据时,这是因为特征可能非常相关,同时差异不平衡。 对于这种情况,可能的解决方案是尝试使信号变白(或使其更加球形)。 在这种情况下,特征向量被迫以单位分量方差为单位。 变白会删除信息,但有时会提高 PCA 减少后将使用的机器学习算法的准确率。 这是采用美白时的代码外观(在我们的示例中,除了输出减少的数据集的比例外,它没有任何改变):
In: pca_2cw = PCA(n_components=2, whiten=True)
X_pca_1cw = pca_2cw.fit_transform(iris.data)
plt.scatter(X_pca_1cw[:,0], X_pca_1cw[:,1], c=iris.target, alpha=0.8,
s=60, marker='o', edgecolors='white')
plt.show()
pca_2cw.explained_variance_ratio_.sum()
Out: 0.97763177502480336
您还可以使用变白来获得 PCA 的第一部分的散点图:

现在,让我们看看如果将输入数据集投影到 PCA 生成的一维空间上会发生什么,如下所示:
In: pca_1c = PCA(n_components=1)
X_pca_1c = pca_1c.fit_transform(iris.data)
plt.scatter(X_pca_1c[:,0], np.zeros(X_pca_1c.shape),
c=iris.target, alpha=0.8, s=60, marker='o', edgecolors='white')
plt.show()
pca_1c.explained_variance_ratio_.sum() Out: 0.9246162071742684
投影沿一条水平线分布:

在这种情况下,输出能量较低(原始信号的 92.4%),并且输出点被添加到一维欧几里德空间中。 这可能不是一个很棒的特征简化步骤,因为许多具有不同标签的点被混合在一起。
最后,这是一个把戏。 为确保生成的输出集至少包含 95% 的输入能量,您可以在第一次调用 PCA 对象时指定该值。 可以使用以下代码获得等于两个向量的结果:
In: pca_95pc = PCA(n_components=0.95)
X_pca_95pc = pca_95pc.fit_transform(iris.data)
print (pca_95pc.explained_variance_ratio_.sum())
print (X_pca_95pc.shape) Out: 0.977631775025
(150, 2)
大数据 PCA ——随机 PCA
PCA 的主要问题是进行还原操作的基础奇异值分解(SVD)算法的复杂性,使得整个过程很难扩展。 Scikit-learn 中有一种基于随机 SVD 的更快算法。 这是一种较轻但近似的迭代分解方法。 使用随机 SVD,全秩重构并不完美,并且在每次迭代过程中都对基向量进行了局部优化。 另一方面,它只需要几个步骤就可以得到一个很好的近似值,证明了随机 SVD 比传统 SVD 算法要快得多。 因此,如果训练数据集很大,则此约简算法是一个不错的选择。 在以下代码中,我们将其应用于鸢尾花数据集。 由于问题的规模很小,因此输出非常接近经典 PCA。 但是,将算法应用于大型数据集时,结果会有很大不同:
In: from sklearn.decomposition import PCA
rpca_2c = PCA(svd_solver='randomized', n_components=2)
X_rpca_2c = rpca_2c.fit_transform(iris.data)
plt.scatter(X_rpca_2c[:,0], X_rpca_2c[:,1],
c=iris.target, alpha=0.8, s=60, marker='o', edgecolors='white')
plt.show()
rpca_2c.explained_variance_ratio_.sum()
Out: 0.97763177502480414
这是使用 SVD 求解器的 PCA 的前两个组件的散点图:

潜在因子分析
潜在因子分析(LFA)是另一种帮助您降低数据集维数的技术。 总体思路类似于 PCA。 但是,在这种情况下,输入信号没有正交分解,因此没有输出基础。 一些数据科学家认为 LFA 是 PCA 的概括,它消除了正交性的约束。 通常,当预期在系统中存在潜在因子或构建体时,将使用 LFA。 在这种假设下,所有特征都是对变量的观察,这些变量是由线性变换的潜因子导出或影响的,并且具有任意波形生成器(AWG)噪声。 通常假设潜因子具有高斯分布和一元协方差。 因此,在这种情况下,不是破坏信号的能量/方差,而是在输出数据集中说明变量之间的协方差。 Scikit-learn 工具箱实现了迭代算法,使其适用于大型数据集。
下面的代码通过假设系统中的两个潜在因素来降低鸢尾花数据集的维数:
In: from sklearn.decomposition import FactorAnalysis
fact_2c = FactorAnalysis(n_components=2)
X_factor = fact_2c.fit_transform(iris.data)
plt.scatter(X_factor[:,0], X_factor[:,1],
c=iris.target, alpha=0.8, s=60,
marker='o', edgecolors='white')
plt.show()
这是散点图中表示的两个潜在因素(与以前的 PCA 不同的解决方案):

线性判别分析
严格来说,线性判别分析(LDA)是分类器(现代统计之父 Ronald Fisher 开发的一种经典统计方法),但通常用于降维。 它不能很好地扩展到较大的数据集(像许多统计方法一样),但这是有待尝试的方法,它可以带来比其他分类方法(例如逻辑回归)更好的结果。 由于这是一种有监督的方法,因此需要标签集来优化减少步骤。 LDA 输出输入特征的线性组合,从而尝试对最好区分它们的类之间的差异进行建模(因为 LDA 使用标签信息)。 与 PCA 相比,在 LDA 的帮助下获得的输出数据集包含了类之间的精妙区别。 但是,它不能用于回归问题,因为它来自分类过程。
这是 LDA 在鸢尾花数据集上的应用:
In: from sklearn.lda import LDA
lda_2c = LDA(n_components=2)
X_lda_2c = lda_2c.fit_transform(iris.data, iris.target)
plt.scatter(X_lda_2c[:,0], X_lda_2c[:,1],
c=iris.target, alpha=0.8, edgecolors='none')
plt.show()
此散点图是由 LDA 生成的前两个组件得出的:

潜在语义分析
通常,潜在语义分析(LSA)在通过TfidfVectorizer或CountVectorizer处理后应用于文本。 与 PCA 相比,它将 SVD 应用于输入数据集(通常是稀疏矩阵),从而生成通常与同一概念关联的单词语义集。 这就是为什么当特征是同质的(即文档中的所有单词)并大量存在时使用 LSA 的原因。
Python 中带有文本和TfidfVectorizer的示例如下。 输出显示了潜在向量的部分内容:
In: from sklearn.datasets import fetch_20newsgroups
categories = ['sci.med', 'sci.space']
twenty_sci_news = fetch_20newsgroups(categories=categories)
from sklearn.feature_extraction.text import TfidfVectorizer
tf_vect = TfidfVectorizer()
word_freq = tf_vect.fit_transform(twenty_sci_news.data)
from sklearn.decomposition import TruncatedSVD
tsvd_2c = TruncatedSVD(n_components=50)
tsvd_2c.fit(word_freq)
arr_vec = np.array(tf_vect.get_feature_names())
arr_vec[tsvd_2c.components_[20].argsort()[-10:][::-1]]
Out: array(['jupiter', 'sq', 'comet', 'of', 'gehrels', 'zisfein', 'jim', 'gene', 'are', 'omen'], dtype='<U79')
独立成分分析
可以从名称中猜出,独立分量分析(ICA)是一种尝试从输入信号派生独立分量的方法。 实际上,ICA 是一种允许您从初始多元输入信号创建最大独立的加性子成分的技术。 该技术的主要假设集中在子组件及其非高斯分布的统计独立性上。 ICA 在神经病学数据中有许多应用,并且在神经科学领域中被广泛使用。
可能需要使用 ICA 的典型方案是盲源分离。 例如,两个或多个麦克风将录制两种声音(例如,一个人讲话并同时播放一首歌曲)。 在这种情况下,ICA 可以将两种声音分成两个输出特征。
Scikit-learn 包提供了算法的更快版本(sklearn.decomposition.FastICA),其用法与迄今为止介绍的其他技术相似。
核 PCA
核 PCA 是一种使用核将信号映射到(通常是)非线性空间并使其线性可分离(或接近获得)的技术。 它是 PCA 的扩展,其中映射是线性子空间上的实际投影。 有许多众所周知的核(当然,您始终可以随时构建自己的核),但是最常用的核是线性,多项式,RBF,Sigmoid 和余弦。 它们都只能为输入数据集提供不同的配置,因为它们只能线性化某些选定类型的数据。 例如,假设我们有一个磁盘状的数据集,就像我们将要使用以下代码创建的那样:
In: def circular_points (radius, N):
return np.array([[np.cos(2*np.pi*t/N)*radius,
np.sin(2*np.pi*t/N)*radius] for t in range(N)])
N_points = 50
fake_circular_data = np.vstack([circular_points(1.0, N_points),
circular_points(5.0, N_points)])
fake_circular_data += np.random.rand(*fake_circular_data.shape)
fake_circular_target = np.array([0]*N_points + [1]*N_points)
plt.scatter(fake_circular_data[:,0], fake_circular_data[:,1],
c=fake_circular_target, alpha=0.8,
s=60, marker='o', edgecolors='white')
plt.show()
这是示例的输出:

对于此输入数据集,由于数据集包含圆周形状的类,因此所有线性变换都将无法分离蓝色点和红色点。 现在,让我们使用 RBF 核与核 PCA 一起尝试一下,看看会发生什么:
In: from sklearn.decomposition import KernelPCA
kpca_2c = KernelPCA(n_components=2, kernel='rbf')
X_kpca_2c = kpca_2c.fit_transform(fake_circular_data)
plt.scatter(X_kpca_2c[:,0], X_kpca_2c[:,1], c=fake_circular_target,
alpha=0.8, s=60, marker='o', edgecolors='white')
plt.show()
下图表示示例的转换:

本章中的图形/图表可能与在本地计算机上获得的图形/图表不同,因为图形布局的初始化是使用随机参数进行的。
我们实现了我们的目标-蓝点在左边,红点在右边。 感谢核 PCA 的转换,您现在可以使用线性技术来处理此数据集。
T-SNE
PCA 是一种广泛的降维技术,但是当我们处理大数据并呈现许多特征时,我们首先需要了解特征空间中的情况。 实际上,在 EDA 阶段,您通常会对数据进行几次散点图绘制,以了解特征之间的关系。 在这一点上,T 分布随机邻居嵌入或 T-SNE 可以为您提供帮助,因为它的设计目标是将高维数据嵌入 2D 或 3D 空间中以充分利用散点图。 它是由 Laurens van der Maaten 和 Geoffrey Hinton 开发的一种非线性降维技术,该算法的核心是基于两个规则:第一个是周期性的相似观测必须对输出有更大的贡献(这是通过概率实现的)。 分配特征); 其次,高维空间中的分布必须类似于小空间中的分布(这是通过最小化 Kullback-Leibler(KL 来实现的),两者概率分布函数之间的差异)。 输出在视觉上是不错的,并允许您猜测特征之间的非线性相互作用。
让我们通过将 T-SNE 应用于鸢尾花数据集并将其绘制到二维空间来查看一个简单的示例如何工作:
In: from sklearn.manifold import TSNE
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
X_tsne = TSNE(n_components=2).fit_transform(X)
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, alpha=0.8,
s=60, marker='o', edgecolors='white')
plt.show()
这是T_SNE的结果,将一个类与另一个类完全分开:

受限玻尔兹曼机
受限玻尔兹曼机(RBM)是另一种由线性函数(通常称为隐藏单元或神经元)组成的技术,可对输入数据进行非线性变换。 隐藏单元表示系统的状态,而输出数据集实际上是该层的状态。
该技术的主要假设是,输入数据集由表示概率的特征([0,1]范围内的二进制值或实数值)组成,因为 RBM 是一种概率方法。 在下面的示例中,我们将使用图像的二值化像素作为特征(1 为白色,0 为黑色)来馈送 RBM,并且将打印系统的潜在组件。 这些组件代表原始图像中出现的不同通用面孔:
In: from sklearn import preprocessing
from sklearn.neural_network
import BernoulliRBM
n_components = 64 # Try with 64, 100, 144
olivetti_faces = datasets.fetch_olivetti_faces()
X = preprocessing.binarize(
preprocessing.scale(olivetti_faces.data.astype(float)),
0.5)
rbm = BernoulliRBM(n_components=n_components, learning_rate=0.01,
n_iter=100)
rbm.fit(X)
plt.figure(figsize=(4.2, 4))
for i, comp in enumerate(rbm.components_):
plt.subplot(int(np.sqrt(n_components+1)), int(np.sqrt(n_components+1)), i + 1)
plt.imshow(comp.reshape((64, 64)), cmap=plt.cm.gray_r,
interpolation='nearest')
plt.xticks(()); plt.yticks(())
plt.suptitle(str(n_components) + ' components extracted by RBM',
fontsize=16)
plt.subplots_adjust(0.08, 0.02, 0.92, 0.85, 0.08, 0.23)
plt.show()
这是 RBM 提取的 64 个组件:

请注意,Scikit-learn 仅包含 RBM 处理的基础层。 如果您正在处理大型数据集,则最好使用基于 GPU 的工具箱(例如基于 CUDA 或 OpenCL 顶部构建的工具箱),因为 RBM 具有高度可并行性。
异常值的检测和处理
在数据科学中,示例是从数据过程中学习的核心。 如果将异常,不一致或错误的数据输入到学习过程中,则结果模型可能无法正确概括任何新数据的容纳情况。 除了歪曲诸如均值和方差之类的描述性度量外,变量中存在的异常高的值还会使多少个机器学习算法从数据中学习,从而导致预测失真。
当数据点与样本中的其他数据点明显偏离时,称为离群值。 任何其他预期的观察结果都标记为内部值。
数据点可能由于以下三个普遍原因而异常(并且每个隐含着不同的补救措施):
- 考虑到可用数据只是原始数据分布的一个事实,该点表示很少出现,但它也是一个可能的值。 在这种情况下,生成点的基本过程对于所有点都是相同的,但是由于其稀有性,外围点可能被认为不适合通过机器学习进行概括。 在这种情况下,通常会删除该点或将其权重降低。 另一种解决方案是增加样本数量,从而使异常值在数据集中的相关性降低。
- 该点表示通常发生的另一种分布。 当发生类似情况时,可以想象一下影响样本生成的错误或规格错误。 无论如何,您的学习算法都会从无关的分布中获取数据,而这些分布并不是您的数据科学项目关注的重点(重点是泛化)。 在这种情况下,只需删除异常值即可。
- 显然,这是某种错误。 由于某些原因,存在数据输入错误或数据完整性问题,这些问题修改了原始值,并用不一致的值替换了它。 最好的做法是删除该值,并将其视为随机丢失的值。 在这种情况下,通常根据平均数或最常见的类替换异常值,这取决于它是回归问题还是分类问题。 如果这样做不方便或不可能,那么我们建议您从数据集中删除示例。
单变量离群值检测
为了解释数据点为何是异常值的原因,首先需要在数据中找到可能的异常值。 有很多方法-有些是单变量的(您可以一次观察每个奇异变量),而另一些是多变量的(他们同时考虑更多的变量)。 单变量方法通常基于 EDA 和可视化显示,例如箱形图(在本章的开头已介绍;我们将在第 5 章,“可视化,见解和结果”中更详细地讨论箱形图)。
通过检查单个变量来追踪异常值时,需要牢记一些经验法则。 实际上,离群值可能会被视为极值:
- 如果您正在观察 Z 分数,则绝对值高于 3 的观察结果必须视为可疑离群值。
- 如果您正在观察数据描述,则可以将小于 25% 百分值的观察值减去
IQR * 1.5(即 75% 与 25% 百分值之间的差),大于第 75 个百分点值的观察值加上IQR * 1.5视为可疑离群值。 通常,您可以借助箱线图来实现这种区分。
为了说明我们如何使用 Z 分数轻松检测出一些离群值,让我们加载并浏览“波士顿房屋价格”数据集。 正如数据集的描述所指出的那样(您可以在boston.DESCR的帮助下获得),索引为 3 的变量CHAS是二进制的。 因此,在检测异常值时使用它几乎没有意义。 实际上,此类变量的值只能为 0 或 1:
In: from sklearn.datasets import load_boston
boston = load_boston()
continuous_variables = [n for n in range(boston.data.shape[1]) if n!=3]
现在,让我们使用 Sklearn 的StandardScaler函数快速标准化所有连续变量。 我们的目标是对boston.data boston.data[:,continuous_variables]进行华丽索引,以便创建另一个包含除上一个索引为 3 的变量以外的所有变量的数组。
StandardScaler自动标准化为零均值和单位方差。 这是必需的常规操作,应在将数据馈送到学习阶段之前执行。 否则,许多算法将无法正常工作(例如由梯度下降和支持向量机支持的线性模型)。
最后,让我们找到高于三个标准差的绝对值的值:
In: import numpy as np
from sklearn import preprocessing
scaler= preprocessing.StandardScaler()
normalized_data = scaler.fit_transform(
boston.data[:,continuous_variables])
outliers_rows, outliers_columns = np.where(np.abs(normalized_data)>3)
outliers_rows和outliers_columns变量包含可疑离群值的行和列索引。 我们可以打印示例的索引:
In: print(outliers_rows) Out: [ 55 56 57 102 141 199 200 201 202 203 204 225 256 257 262 283 284
...
另外,我们可以在数组中显示行/列坐标的元组:
In: print (list(zip(outliers_rows, outliers_columns))) Out: [(55, 1), (56, 1), (57, 1), (102, 10), (141, 11), (199, 1), (200, 1),
...
单变量方法可以揭示很多潜在的异常值。 它不会披露没有极值的离群值-相反,它的特征是两个或多个变量中的值异常组合。 在这种情况下,所涉及变量的值甚至可能不是极端值,因此,异常值可能会在单变量检查中未被注意到而消失。 这些离群值称为多元离群值。
为了发现多元离群值,您可以使用降维算法,例如前面说明的 PCA,然后检查超出三个标准差的组件的绝对值,或目视检查双变量图以找到孤立的聚类。 数据点。
Scikit-learn 包提供了两个类,这些类可以直接为您自动工作并发出所有可疑情况的信号:
covariance.EllipticEnvelope类适合您的数据的可靠分布估计,指出可能污染数据集的异常值,因为它们是数据一般分布中的极端点。svm.OneClassSVM类是一种支持向量机算法,可以近似于数据的形状并确定是否应将提供的任何新实例视为新颖性(它充当新颖性检测器,因为默认情况下,它假定数据中没有异常值)。 通过仅修改其参数,它也可以在存在异常值的数据集上工作,从而提供比EllipticEnvelope更鲁棒和可靠的异常值检测系统。
这两个类都基于不同的统计和机器学习方法,需要在建模阶段进行了解和应用。
EllipticEnvelope
EllipticEnvelope是一项函数,它通过假设您的整个数据是基础多元高斯分布的表达式来尝试找出数据总体分布的关键参数。 这个假设不能适用于所有数据集,但是当它成立时,就证明了一种发现异常值的有效方法。 我们可以说,它尽可能地简化了算法背后的复杂估计,它可以检查每个观测值相对于考虑到数据集中所有变量的均值的距离。 因此,它可以发现单变量和多变量离群值。
使用协方差模块中的此函数时,您唯一需要考虑的参数是污染参数,该参数的值最多为 0.5。 它为算法提供有关数据集中存在的异常值比例的信息。 情况因数据集而异。 但是,作为一个开始的数字,我们建议使用 0.01-0.02 的值,因为它是观察值的百分比应落在标准化正态分布中距平均值的 Z 分数距离的绝对值 3 之上。 因此,我们认为默认值 0.1 太高。
让我们在综合分发的帮助下看一下该算法的作用:
In: from sklearn.datasets import make_blobs
blobs = 1
blob = make_blobs(n_samples=100, n_features=2, centers=blobs,
cluster_std=1.5, shuffle=True, random_state=5)
# Robust Covariance Estimate
from sklearn.covariance import EllipticEnvelope
robust_covariance_est = EllipticEnvelope(contamination=.1).fit(blob[0])
detection = robust_covariance_est.predict(blob[0])
outliers = np.where(detection==-1)[0]
inliers = np.where(detection==1)[0]
# Draw the distribution and the detected outliers
from matplotlib import pyplot as plt
# Just the distribution
plt.scatter(blob[0][:,0],blob[0][:,1], c='blue', alpha=0.8, s=60,
marker='o', edgecolors='white')
plt.show()
# The distribution and the outliers
in_points = plt.scatter(blob[0][inliers,0],blob[0][inliers,1],
c='blue', alpha=0.8,
s=60, marker='o',
edgecolors='white')
out_points = plt.scatter(blob[0][outliers,0],blob[0][outliers,1],
c='red', alpha=0.8, s=60, marker='o',
edgecolors='white')
plt.legend((in_points,out_points),('inliers','outliers'),
scatterpoints=1, loc='lower right')
plt.show()
让我们仔细检查这段代码。
make_blobs函数在二维空间中创建一定数量的分布,总共 100 个示例(n_samples参数)。 分布的数量(参数中心)与用户定义的变量斑点有关,该变量最初设置为 1。
创建完人工示例数据后,以 10% 的污染率运行EllipticEnvelope可帮助您找出分布中的极值。 该模型首先通过在EllipticEnvelope类上使用.fit()方法来部署拟合。 然后,通过使用.predict()方法获得用于拟合的数据的预测。
通过使用matplotlib中pyplot模块的plot函数,可以通过散点图显示与值 1 和 -1 的向量相对应的结果(其中 -1 为异常示例的标记)。
离群值和离群值的区别记录在变量的离群值和离群值中,其中包含示例的索引。
现在,让我们在更改斑点的数目之后再运行几次代码,并检查斑点的值为1和4时的结果:

更改斑点数后的数据点分布如下:

在唯一的基础多元分布的情况下(当变量blob = 1时),EllipticEnvelope算法已成功将观测值的 10% 定位在分布本身的边缘,并因此向所有可疑异常值发出信号。
相反,当数据中存在多个分布时,好像有两个或两个以上的自然群集时,该算法试图拟合唯一的一般分布,往往会将潜在异常值定位在最远端的群集上,从而忽略了数据的其他区域,它们可能会受到外围案例的影响。
对于真实数据,这并非罕见情况,它代表了EllipticEnvelope算法的重要局限性。
现在,让我们回到最初的波士顿房屋价格数据集,以验证比合成斑点更真实的更多数据。 这是我们可以用于实验的代码的第一部分:
In: from sklearn.decomposition import PCA
# Normalized data relative to continuos variables
continuous_variables = [n for n in range(boston.data.shape[1]) if n!=3]
scaler = preprocessing.StandardScaler()
normalized_data = scaler.fit_transform(
boston.data[:,continuous_variables])
# Just for visualization purposes pick the first 2 PCA components
pca = PCA(n_components=2)
Zscore_components = pca.fit_transform(normalized_data)
vtot = 'PCA Variance explained ' + str(round(np.sum( pca.explained_variance_ratio_),3))
v1 = str(round(pca.explained_variance_ratio_[0],3))
v2 = str(round(pca.explained_variance_ratio_[1],3))
在此脚本中,我们将首先对数据进行标准化,然后仅出于随后的可视化目的,通过使用 PCA 来减少两个分量。
这两个 PCA 分量约占初始方差的 62%,该初始方差由数据集中的 12 个连续变量(.explained_variance_ratio_变量的总和,位于已拟合的PCA类的内部)表示。
尽管只有两个 PCA 组件足以实现可视化目的,但是通常,此数据集将获得两个以上的组件,因为目标是要有足够的分量以至少占总方差的 95%(如本章前面所述) 。
我们将继续执行脚本:
In: robust_covariance_est = EllipticEnvelope(store_precision=False,
assume_centered = False,
contamination=.05)
robust_covariance_est.fit(normalized_data)
detection = robust_covariance_est.predict(normalized_data)
outliers = np.where(detection==-1)
regular = np.where(detection==1)
In: # Draw the distribution and the detected outliers
from matplotlib import pyplot as plt
in_points = plt.scatter(Zscore_components[regular,0],
Zscore_components[regular,1], c='blue', alpha=0.8, s=60, marker='o',
edgecolors='white')
out_points = plt.scatter(Zscore_components[outliers,0],
Zscore_components[outliers,1], c='red', alpha=0.8, s=60, marker='o',
edgecolors='white')
plt.legend((in_points,out_points),('inliers','outliers'),
scatterpoints=1, loc='best')
plt.xlabel('1st component ('+v1+')')
plt.ylabel('2nd component ('+v2+')')
plt.xlim([-7,7])
plt.ylim([-6,6])
plt.title(vtot)
plt.show()
前两个组件的可视化占原始差异的 62.2%:

与前面的示例一样,在我们假设等于 0.05 的低污染的情况下,基于EllipticEnvelope的代码将预测异常值,并将其存储在数组中的方式与存储异常值的方式相同。 最后,是可视化(如前所述,我们将在第 5 章,“可视化,见解和结果”中讨论所有可视化方法)。
现在,让我们观察由散点图提供的结果,该散点图用于可视化数据的前两个PCA分量并标记外围观察。 关于我们示例中数据点的总体分布,由两个组成部分提供,它们约占数据方差的 62%,看来波士顿似乎有两个不同的房价集群,分别对应于市场中存在高端和低端设备。 一般而言,对于EllipticEnvelope估计,数据中群集的存在并不是最佳的情况。 实际上,根据我们在使用合成斑点进行实验时已经注意到的结果,该算法仅指出了一个群集上的离群值-较小的一个。 鉴于这样的结果,有充分的理由相信我们刚刚收到了有偏见的,部分的回应,在将这些点视为异常值之前,需要进行一些进一步的调查。 Scikit-learn 包实际上将鲁棒的协方差估计方法(从根本上讲是一种统计方法)与一种扎根于机器学习的另一种方法集成在一起:OneClassSVM类。 现在,我们将继续进行试验。
在离开此示例之前,请注意,为了同时适合 PCA 和EllipticEnvelope,我们使用了一个名为normalized_data的数组,该数组仅包含标准化的连续数据集变量。 请始终注意,使用非标准化数据并将二进制或分类数据与连续数据混合可能会导致EllipticEnvelope算法的错误和近似估计。
OneClassSVM
由于EllipticEnvelope利用参数和统计假设拟合假设的高斯分布,因此OneClassSVM是一种机器学习算法,可从数据本身学习特征的分布,因此适用于多种情况,其中您希望能够捕获所有离群值以及异常数据示例。
如果您已经拥有一个干净的数据集,并且已通过机器学习算法进行了拟合,那就太好了。 之后,可以召唤OneClassSVM来检查是否有任何新示例适合历史分布,如果不合适,它将发出一个新示例的信号,该示例可能是错误,也可能是某些新的,以前看不见的情况。
只需将数据科学情况视为经过训练的机器学习分类算法,即可识别网站上的帖子和新闻并采取在线操作。 OneClassSVM可以轻松地发现与网站上其他帖子(垃圾邮件,也许是?)不同的帖子,而其他算法只会尝试将新示例适合现有主题的分类。
但是,OneClassSVM也可以用来发现现有的异常值。 如果这个专门的 SVM 类不能容纳某些数据(指出该数据处于数据分布的边缘),那么这些示例肯定有一些问题。
为了使OneClassSVM作为离群值检测器,您需要研究其核心参数; 它要求您定义核,度,伽玛和nu:
-
核和阶数:它们相互关联。 通常,我们根据经验建议的值是默认值。 核的类型应为
rbf,其阶数应为 3。此类参数将通知OneClassSVM创建一系列跨越三个维度的分类气泡,甚至可以对最复杂的多维分布形式进行建模。 -
Gamma:这是连接到 RBF 核的参数。 我们建议您将其保持在尽可能低的水平。 一个好的经验法则是为其分配一个最小值,该最小值介于案例数和变量之间。γ在 SVM 中的作用将在第 4 章,“机器学习”中进一步说明。 现在就可以说,较高的gamma值倾向于使算法遵循数据,但更多地定义分类气泡的形状。 -
Nu:此参数确定是否必须拟合精确的分布,或者是否通过不对当前数据示例进行过多调整来尝试获得一定程度的概括(如果存在异常值,则为必要选择)。 可以通过以下公式轻松确定:
nu_estimate = 0.95 * outliers_fraction + 0.05
- 如果离群分数的值非常小,则
nu将很小,并且 SVM 算法将尝试拟合数据点的轮廓。 另一方面,如果分数较高,则参数为,这将强制使内部点分布的边界更平滑。
让我们立即观察该算法在我们之前在波士顿房价数据集上遇到的问题上的表现:
In: from sklearn.decomposition import PCA
from sklearn import preprocessing
from sklearn import svm
# Normalized data relative to continuos variables
continuous_variables = [n for n in range(boston.data.shape[1]) if n!=3]
scaler = preprocessing.StandardScaler()
normalized_data = scaler.fit_transform(
boston.data[:,continuous_variables])
# Just for visualization purposes pick the first 5 PCA components
pca = PCA(n_components=5)
Zscore_components = pca.fit_transform(normalized_data)
vtot = 'PCA Variance explained ' + str(round( np.sum(pca.explained_variance_ratio_),3)) # OneClassSVM fitting and estimates
outliers_fraction = 0.02 #
nu_estimate = 0.95 * outliers_fraction + 0.05
machine_learning = svm.OneClassSVM(kernel="rbf",
gamma=1.0/len(normalized_data),
degree=3, nu=nu_estimate)
machine_learning.fit(normalized_data)
detection = machine_learning.predict(normalized_data)
outliers = np.where(detection==-1)
regular = np.where(detection==1)
现在,我们将可视化结果:
In: # Draw the distribution and the detected outliers
from matplotlib import pyplot as plt
for r in range(1,5):
in_points = plt.scatter(Zscore_components[regular,0],
Zscore_components[regular,r], c='blue', alpha=0.8, s=60,
marker='o', edgecolors='white')
out_points = plt.scatter(Zscore_components[outliers,0],
Zscore_components[outliers,r], c='red', alpha=0.8, s=60,
marker='o', edgecolors='white')
plt.legend((in_points,out_points),('inliers','outliers'),
scatterpoints=1, loc='best')
plt.xlabel('Component 1 (' + str(round(
pca.explained_variance_ratio_[0],3))+')')
plt.ylabel('Component '+str(r+1)+'('+str(round( pca.explained_variance_ratio_[r],3))+')')
plt.xlim([-7,7])
plt.ylim([-6,6])
plt.title(vtot)
plt.show()
与先前提供的代码相比,此代码段有所不同,因为最终 PCA 分解由五个组件组成。 为了探索更多的数据维度,需要更大的数量。 增加所得 PCA 组件数量的另一个原因是因为我们打算将转换后的数据集与OneClassSVM一起使用。
核心参数是根据观察次数计算得出的,如下所示:
gamma = 1.0 / len(normalized_data)
nu = no_estimated
nu特别取决于:
nu_estimate = 0.95 * outliers_fraction + 0.05
因此,通过将outliers_fraction(从0.02更改为较大的值,例如0.1),当假定您数据中的异常情况发生率较高时,您需要算法对可能出现的异常现象给予更多关注。
我们还要观察 PCA 组件从 2 到 5 的图形输出,并将其与主组件进行比较(所解释方差的 51%)。 该序列的第一个图(包括四个散点图)如下:

从我们的图形探索中,似乎OneClassSVM很好地模拟了房价数据的分布,并帮助发现了分布边界上的一些极值。
此时,您可以决定我们将要提出的新颖性和异常检测方法之一。 您甚至可以同时使用:
- 仔细检查异常值的特征以找出存在异常的原因(这一事实可能会使您进一步思考数据的潜在生成过程)
- 通过对偏远的观察使用权重较低或仅排除它们来尝试构建一些机器学习模型
最后,采用纯粹的数据科学方法,可以帮助您决定对任何遥远的观察采取下一步措施的方法是测试决策结果以及对数据的后续操作。 如何测试和试验关于您的数据的假设是我们将在接下来的部分中与您讨论的主题。
验证指标
为了评估已构建的数据科学系统的表现并检查与目标之间的距离,您需要使用对结果进行评分的函数。 通常,使用不同的评分函数来处理二分类,多标签分类,回归或聚类问题。 现在,让我们看看这些任务中最受欢迎的特征,以及机器学习算法如何使用它们。
学习如何为数据科学项目选择正确的分数/误差度量标准实际上是一个经验问题。 我们发现咨询(并参加)Kaggle 举办的数据科学竞赛非常有帮助,该公司致力于组织来自世界各地的数据科学家之间的数据挑战。 通过观察各种挑战以及他们尝试优化的分数或误差度量,您一定可以对自己的问题获得有用的见解。 Kaggle 的首席技术官本·哈默(Ben Hammer)甚至创建了一个比赛中常用指标的 Python 库,您可以在这个页面上查询并使用pip install ml_metrics安装在计算机上。
多标签分类
当您的任务是预测多个标签时(例如:今天的天气如何?这是什么花?您的工作是什么?),我们将此问题称为多标签分类。 多标签分类是一项非常流行的任务,并且存在许多表现指标来评估分类器。 当然,在二分类的情况下,您可以使用所有这些度量。 现在,让我们使用一个简单的真实示例来说明其工作原理:
In: from sklearn import datasets
iris = datasets.load_iris()
# No crossvalidation for this dummy notebook
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(iris.data,
iris.target, test_size=0.50, random_state=4)
# Use a very bad multiclass classifier
from sklearn.tree import DecisionTreeClassifier
classifier = DecisionTreeClassifier(max_depth=2)
classifier.fit(X_train, Y_train)
Y_pred = classifier.predict(X_test)
iris.target_names Out: array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
现在,让我们看一下多标签分类中常用的度量:
- 混淆矩阵:在描述多标签分类的表现指标之前,让我们看一下混淆矩阵,该表使我们了解了每个类别的错误分类。 理想情况下,在理想分类中,所有不在对角线上的像元应为 0s。 在下面的示例中,您将看到类别 0(山鸢尾)从未被错误分类,类别 1(杂色鸢尾)被两次错误分类为弗吉尼亚鸢尾,并且类别 2(弗吉尼亚鸢尾)被误分类为杂色鸢尾两次:
In: from sklearn import metrics
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_pred)
print cm
Out: [[30 0 0]
[ 0 19 3]
[ 0 2 21]] In: import matplotlib.pyplot as plt
img = plt.matshow(cm, cmap=plt.cm.autumn)
plt.colorbar(img, fraction=0.045)
for x in range(cm.shape[0]):
for y in range(cm.shape[1]):
plt.text(x, y, "%0.2f" % cm[x,y],
size=12, color='black', ha="center", va="center")
plt.show()
混淆矩阵以这种方式用图形表示:

- 准确率:准确率是预测标签中与实际标签完全相等的部分。 换句话说,它是正确分类的标签总数的百分比:
In: print ("Accuracy:", metrics.accuracy_score(Y_test, Y_pred)) Out: Accuracy: 0.933333333333
- 精度:这是从信息检索领域中采取的一种措施。 它计算结果集中相关结果的数量。 等效地,在分类任务中,它计算每组分类标签中正确标签的数量。 然后,将所有标签的结果取平均值:
In: print ("Precision:", metrics.precision_score(y_test, y_pred)) Out: Precision: 0.933333333333
- 召回:这是从信息检索中提取的另一个概念。 与数据集中的所有相关标签相比,它计算结果集中相关结果的数量。 在分类任务中,这是一组中正确分类的标签的数量除以该组的标签总数。 最后,对结果进行平均,如以下代码所示:
In: print ("Recall:", metrics.recall_score(y_test, y_pred)) Out: Recall: 0.933333333333
- F1 得分:这是精度和召回率的谐波平均值,通常在处理不平衡数据集时使用,以显示分类器在所有类别中的表现是否良好:
In: print ("F1 score:", metrics.f1_score(y_test, y_pred)) Out: F1 score: 0.933267359393
这些是多标签分类中最常用的度量。 方便的函数classification_report显示有关这些措施的报告,非常方便。 支持只是带有该标签的观察次数。 了解数据集是否平衡(即每个类的示例份额是否相同)非常有用:
In: from sklearn.metrics import classification_report
print classification_report(y_test, y_pred,
target_names=iris.target_names)
这是完整报告,具有精度,召回,F1 得分和支持度(该类案件的数量):

在数据科学实践中,精度和召回比准确率更为广泛地使用,因为数据问题中的大多数数据集倾向于不平衡。 为了解决这种不平衡,数据科学家经常以精度,召回和 F1 分数来表示其结果。 此外,我们必须注意准确率,精度,召回和 F1 分数如何采用[0.0, 1.0]范围。 完美的分类器在所有这些指标上的得分都达到1.0(但是要当心任何完美的分类,如果太令人难以置信,请当心,因为这通常意味着出了点问题;现实世界中的数据问题永远不会有完美的解决方案)。
二分类
除了上一节中显示的误差度量之外,在只有两个输出类的问题中(例如,如果您必须猜测用户的性别或预测用户是否会点击/购买/喜欢该商品), 还有一些其他措施。 因为它非常有用,所以使用最多的是受试者工作特性(ROC)的曲线下面积(AUC)。
ROC 曲线是一种图形化的方式,用于表达分类器的表现在所有可能的分类阈值上如何变化(即,当参数变化时结果的变化)。 具体而言,这些表演具有真正(或命中)率和假正(或失误)率。 第一个是真正面结果的比率,第二个是假正面结果的比率。 该曲线下方的区域表示分类器相对于随机分类器(其 AUC 为 0.50)的效果。
在这里,我们有一个随机分类器(虚线)和一个更好的分类器(实线)的图形示例。 您可以看到随机分类器的 AUC 为 0.5(是平方的一半),另一个具有更高的 AUC(其上限为1.0):

用于使用 Python 计算 AUC 的函数为sklearn.metrics.roc_auc_score()。
回归
在必须预测实数或回归的任务中,许多误差度量均来自欧几里得代数:
- 平均绝对误差或 MAE:这是预测值和实际值之间的差向量的平均 L1 范数:
In: from sklearn.metrics import mean_absolute_error
mean_absolute_error([1.0, 0.0, 0.0], [0.0, 0.0, -1.0]) Out: 0.66666666666666663
- 均方误差或 MSE:这是预测值和实际值之间的差向量的平均 L2 范数:
In: from sklearn.metrics import mean_squared_error
mean_squared_error([-10.0, 0.0, 0.0], [0.0, 0.0, 0.0]) Out: 33.333333333333
- R² 得分:R² 也称为测定系数。 简而言之,R² 确定在预测变量和
target变量之间存在线性拟合的程度。 取值介于 0 和 1(含)之间; R² 越高,模型越好。 这是一个很好的评分标准,但是并不能说明所有有关故事的信息,尤其是在您的数据中存在异常值的情况下。 您可以在《统计》的参考书中找到关于此指标的更多错综复杂的信息。 作为建议,可以使用它,但要同时进行其他评分或错误测量。 在这种情况下使用的函数是sklearn.metrics.r2_score。
测试和验证
加载数据,对其进行预处理,创建新的有用特征,检查异常值和其他不一致的数据点并最终选择正确的指标后,我们准备应用机器学习算法。
通过观察一系列示例并将它们与结果配对,机器学习算法能够提取一系列规则,通过正确猜测它们的结果,可以将这些规则成功地推广到新示例。 这就是有监督的学习方法,其中应用了一系列高度专业化的学习算法,我们希望这些算法可以正确地预测(并概括)任何新数据。
但是,我们如何才能正确地应用学习过程,以便获得最佳的预测模型,以便通常将其用于相似但新的数据?
在数据科学中,有一些最佳实践可以遵循,可以确保在将来将模型推广到任何新数据时获得最佳结果。 让我们通过逐步进行说明,首先加载以下示例中将要处理的数据集:
In: from sklearn.datasets import load_digits
digits = load_digits()
print (digits.DESCR)
X = digits.data
y = digits.target
数字数据集包含从 0 到 9 的手写数字图像。数据格式由8 x 8此类图像的矩阵组成:

这些数字实际上存储为向量(从每个8 x 8图像的平整度得出),其向量值为 0 到 16 之间的 64 个数值,代表每个像素的灰度色调:
In: X[0] Out: array([0., 0., 5., 13., 9., 1., 0., 0., ...])
我们还将使用三个不同的支持向量机上载三个不同的机器学习假设(在机器学习语言中,一个假设是一种已将其所有参数设置为可学习的算法)。 它们对于我们的实际示例很有用:
In: from sklearn import svm
h1 = svm.LinearSVC(C=1.0)
h2 = svm.SVC(kernel='rbf', degree=3, gamma=0.001, C=1.0)
h3 = svm.SVC(kernel='poly', degree=3, C=1.0)
作为第一个实验,让我们将线性 SVM 分类器拟合到我们的数据并验证结果:
In: h1.fit(X,y)
print (h1.score(X,y))
Out: 0.984974958264
第一种方法是使用X数组拟合模型,以便正确预测y向量指示的 10 个类别之一。 此后,通过调用.score()方法并指定相同的预测变量(X数组),该方法根据相对于y向量给定的真实值的平均准确率来评估表现。 结果在预测正确的数字上约为 98.5% 的准确率。
该数字表示样本内表现,即学习算法的表现。 它纯粹是指示性的,尽管它代表了表现的上限(提供不同的示例,但平均表现始终会很差)。 实际上,每种学习算法都具有一定的记忆能力,可以记忆训练过的数据。 因此,样本内表现部分归因于该算法从数据中学习一些一般推断的能力,部分归因于其记忆能力。 在极端情况下,如果模型相对于可用数据而言训练过度或过于复杂,则存储的模式将优先于派生的规则,并且该算法将不适合正确地预测新的观测值(尽管对过去的观测值非常有用)。 这样的问题称为过拟合。 由于在机器学习中,我们无法分离这两种伴随的影响,为了正确估计我们的假设的预测表现,我们需要在没有记忆效应的一些新数据上对其进行测试。
记忆的发生是由于算法的复杂性。 复杂的算法拥有许多系数,可以在其中存储有关训练数据的信息。 不幸的是,由于预测过程变得随机,因此在预测未见实例时,记忆效应会导致估计的高方差。 三种解决方案是可能的:
- 首先,您可以增加示例数,这样就无法存储所有以前见过的案例的信息,但是查找所有必要数据可能会变得更加昂贵
- 其次,您可以使用较简单的机器学习算法,该算法不太容易记忆,但是会以使用机器学习解决方案的能力为代价,该解决方案不太适合数据基础规则的复杂性。
- 第三,您可以使用正则化对极其复杂的模型进行惩罚,并迫使算法过轻,甚至从模型中排除一定数量的变量,从而有效地减少了模型中系数的数量及其存储数据的能力。
在许多情况下,即使没有一定的成本,也无法获得新的数据。 在这种常见情况下,一种好的方法是将初始数据分为训练集(通常为总数据的 70-80%)和测试集(其余为 20-30%)。 考虑到任何可能的不平衡类别分布,训练和测试集之间的划分应该是完全随机的:
In: chosen_random_state = 1
X_train, X_test, y_train, y_test = model_selection.train_test_split(
X, y,
test_size=0.30, random_state=chosen_random_state)
print ("(X train shape %s, X test shape %s, n/y train shape %s, \
y test shape %s" % (X_train.shape, X_test.shape,y_train.shape, y_test.shape))
h1.fit(X_train,y_train)
print (h1.score(X_test,y_test))
# Returns the mean accuracy on the given test data and labels Out: (X train shape (1257, 64), X test shape (540, 64),
y train shape (1257,), y test shape (540,)
0.953703703704
通过执行前面的代码,model_selection.train_test_split()函数根据参数test_size将初始数据随机分为两个互斥的集合(该整数可以表示测试集示例的确切数目,也可以是浮点数,表示要用于测试目的的总数据的百分比)。 拆分由random_state控制,该操作可确保在不同时间和不同计算机上(即使您使用的是完全不同的操作系统)也可以重现该操作。
目前的平均准确率为 0.94。 如果您尝试使用chosen_random_state参数使用不同的整数值再次运行同一单元,则实际上您会注意到准确率会发生变化,这表明测试集进行的表现评估并非绝对的表现指标,并且应谨慎使用。 给定不同的测试样本,您必须了解它的可变性。
实际上,我们甚至可以从测试集中获得有偏差的表现估计。 如果我们选择(在使用random_state进行各种试验后)可以确认我们假设的测试集,或者开始使用测试集作为参考以对学习过程做出决定(例如,选择符合特定测试样本的最佳假设)。
与仅评估训练数据的拟合度一样,对选定的测试集进行操作也可以确保最终的表现看起来不错。 但是,您建立的模型不会在不同的测试集上复制相同的表现(再次是过拟合问题)。
因此,当我们在将每个假设拟合到训练数据之后必须在多个假设之间进行选择(数据科学中的一个常见实验)时,我们需要一个可用于比较其表现的数据样本,并且它不能是测试集( 由于我们之前提到的原因)。
正确的方法是使用验证集。 我们建议您拆分初始数据-可以为训练集保留 60% 的初始数据,为验证集保留 20% 的数据,为测试集保留 20% 的数据。 为了考虑到这一点,我们可以更改初始代码,并且可以对其进行修改以测试所有三个假设:
In: chosen_random_state = 1
X_train, X_validation_test, y_train, y_validation_test =
model_selection.train_test_split(X, y,
test_size=.40,
random_state=chosen_random_state)
X_validation, X_test, y_validation, y_test =
model_selection.train_test_split(X_validation_test, y_validation_test,
test_size=.50,
random_state=chosen_random_state)
print ("X train shape, %s, X validation shape %s, X test shape %s,
/ny train shape %s, y validation shape %s, y test shape %s/n" %
(X_train.shape, X_validation.shape, X_test.shape,
y_train.shape, y_validation.shape, y_test.shape))
for hypothesis in [h1, h2, h3]:
hypothesis.fit(X_train,y_train)
print ("%s -> validation mean accuracy = %0.3f" % (hypothesis,
hypothesis.score(X_validation,y_validation)) )
h2.fit(X_train,y_train)
print ("n%s -> test mean accuracy = %0.3f" % (h2,
h2.score(X_test,y_test)))
Out: X train shape, (1078, 64), X validation shape (359, 64),
X test shape (360, 64),
y train shape (1078,), y validation shape (359,), y test shape (360,)
LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
intercept_scaling=1, loss='squared_hinge', max_iter=1000,
multi_class='ovr', penalty='l2', random_state=None, tol=0.0001,
verbose=0) -> validation mean accuracy = 0.958
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape=None, degree=3, gamma=0.001, kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False) -> validation mean accuracy = 0.992
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape=None, degree=3, gamma='auto', kernel='poly',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False) -> validation mean accuracy = 0.989
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape=None, degree=3, gamma=0.001, kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False) -> test mean accuracy = 0.978
如产出所报告的那样,现在,训练集由 1,078 例(占总数的 60%)组成。 为了将数据分为三个部分-训练,验证和测试-首先使用函数model_selection.train_test_split在训练集和测试/验证数据集之间提取数据(从而提取训练样本)。 然后,使用相同的函数将测试/验证数据集进一步分为两部分。 在对每个假设进行训练后,将根据验证集对其进行测试。 根据验证集,使用 RBF 核的 SVC 的精度为 0.992,是最佳模型。 决定使用此模型后,将在测试集上评估其表现,从而得出 0.978 的准确率(这是该模型实际表现的实测代表)。
由于测试的准确率与验证的准确率不同,因此选择的假设真的是最好的假设吗? 我们建议您尝试多次运行单元格中的代码(理想情况下,至少运行 30 次代码应确保具有统计意义),每次都更改chosen_random_state值。 这样,将针对不同的样本验证相同的学习过程,从而使您对自己的期望更有信心。
交叉验证
如果您运行了先前的实验,则可能已经意识到:
- 验证和测试结果都不同,因为它们的样本不同。
- 选择的假设通常是最好的假设,但并非总是如此。
不幸的是,依赖于样本的验证和测试阶段会带来不确定性,同时减少了专门用于训练的学习示例(示例越少,模型估计值的差异就越大)。
一种解决方案是使用交叉验证,Scikit-learn 提供了一个完整的交叉验证和表现评估模块(sklearn.model_selection)。
通过使用交叉验证,您只需要将数据分为训练和测试集,就可以将训练数据用于模型优化和模型训练。
交叉验证如何工作? 这个想法是将您的训练数据划分为一定数量的分区(称为折叠),然后对您的模型进行多达该分区数量的训练,每次从训练阶段就将一个不同的分区排除在外。 每次模型训练后,您都将在未折叠的折痕上测试结果并将其存储起来。 最后,您将获得与折痕一样多的结果,并且您可以计算出折痕的平均值和标准差:

在前面的图形示例中,图表描述了一个数据集,该数据集被分为五个大小相等的折叠,根据迭代的不同,它们在机器学习过程中被用作训练或测试集的一部分。
在我们推荐的交叉验证中,十折是很常见的配置。 对于线性估计等有偏估计量,使用较少的折痕可能会很好,但它可能会惩罚更复杂的机器学习算法。 在某些情况下,您确实需要使用更多的折痕以确保有足够的训练数据供机器学习算法正确归纳。 这在没有足够数据点的医学数据集中很常见。 另一方面,如果手头的示例数量不成问题,则使用更多的折叠会占用更多的计算资源,并且交叉验证完成可能需要更长的时间。 有时,使用五折可以很好地权衡估计的准确率和运行时间。
标准差将提示您模型如何受到训练提供的数据(实际上是模型的方差)的影响,并且平均值提供了对其总体表现的合理估计。 使用从不同模型获得的交叉验证结果的平均值(由于使用了不同的模型类型,或者因为使用了不同的训练变量选择,或者因为模型的不同超参数),您可以放心地选择表现最佳的假设,可进行总体表现测试。
我们强烈建议您将交叉验证仅用于优化目的,而不是用于表现估计(也就是说,找出新数据上的模型误差可能是什么)。 交叉验证只是根据最佳平均结果指出最佳算法和参数选择。 将其用于表现评估将意味着使用找到的最佳结果,这比应该的结果更为乐观。 为了报告您可能的表现的无偏估计,您应该首选使用测试集。
让我们执行一个示例,以查看交叉验证的实际效果。 此时,我们可以回顾一下对数字数据集的三个可能假设的先前评估:
In: choosen_random_state = 1
cv_folds = 10 # Try 3, 5 or 20
eval_scoring='accuracy' # Try also f1
workers = -1 # this will use all your CPU power
X_train, X_test, y_train, y_test = model_selection.train_test_split(
X, y,
test_size=0.30,
random_state=choosen_random_state)
for hypothesis in [h1, h2, h3]:
scores = model_selection.cross_val_score(hypothesis,
X_train, y_train,
cv=cv_folds, scoring= eval_scoring, n_jobs=workers)
print ("%s -> cross validation accuracy: mean = %0.3f \
std = %0.3f" % (hypothesis, np.mean(scores),
np.std(scores)))
Out: LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
intercept_scaling=1, loss='squared_hinge', max_iter=1000,
multi_class='ovr', penalty='l2', random_state=None, tol=0.0001,
verbose=0) -> cross validation accuracy: mean = 0.930 std = 0.021
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape=None, degree=3, gamma=0.001, kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False) -> cross validation accuracy:
mean = 0.990 std = 0.007
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape=None, degree=3, gamma='auto', kernel='poly',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False) -> cross validation accuracy:
mean = 0.987 std = 0.010
脚本的核心是model_selection.cross_val_score函数。 我们脚本中的函数接收以下参数:
- 学习算法(
estimator) - 一组预测变量(
X) - 目标变量(
y) - 交叉验证器(
cv) - 评分函数(
scoring) - 要使用的 CPU 数(
n_jobs)
给定这样的输入,该函数将包装一些其他复杂函数。 它创建 n 个重复项,训练 n 个交叉验证样本中的模型,测试结果,并存储从样本外折叠中每次迭代得出的分数。 最后,该函数将报告此类记录的分数列表:
In: scores Out: array([ 0.96899225, 0.96899225, 0.9921875, 0.98412698, 0.99206349, 1, 1., 0.984, 0.99186992, 0.98347107])
使用cross_val_score的主要优点在于使用简单,并且它自动合并了所有必要步骤以进行正确的交叉验证。 例如,在决定如何将训练样本分成多个折叠时,如果提供了y向量,则它在每个折叠中的目标类别标签所占的比例将与最初提供的y相同。
使用交叉验证迭代器
尽管model_selection模块中的cross_val_score函数充当大多数交叉验证目的的完整帮助器函数,但您可能仍需要构建自己的交叉验证过程。 在这种情况下,相同的model_selection模块可确保对迭代器的强大选择。
在研究最有用的迭代器之前,让我们通过研究其中一个迭代器model_selection.KFold的工作方式,清楚地概述它们的功能。
KFold的功能非常简单。 如果给出 n 次折叠,则它将 n 次迭代返回到训练和验证集的索引以测试每个折叠。
假设我们有一个由 100 个示例组成的训练集,并且我们想创建一个 10 倍的交叉验证。 首先,让我们设置迭代器:
In: kfolding = model_selection.KFold(n_splits=10, shuffle=True,
random_state=1)
for train_idx, validation_idx in kfolding.split(range(100)): print (train_idx, validation_idx) Out: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 21 22 23 24 25 26 27
28 29 30 31 32 34 35 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
54 55 56 57 58 59 60 61 62 63 64 66 67 68 70 71 72 73 74 75 76 77 78 79
83 85 86 87 88 89 90 91 92 94 95 96 97 98 99] [17 33 36 65 69 80 81 82
84 93] ...
通过使用 n 参数,我们可以指示迭代器对 100 个索引执行折叠。 n_splits指定折数。 将随机播放设置为True时,它将随机选择折叠分量。 相反,如果将其设置为False,则将根据索引的顺序创建折叠(因此,第一个折叠将为[0 1 2 3 4 5 6 7 8 9])。
像往常一样,random_state参数可允许产生折痕。
在迭代器循环中,会根据您的假设提供评估和验证的索引,以进行评估。 (让我们通过使用线性 SVC 来了解它是如何工作的。)您只需在花式索引的帮助下同时选择X和y:
In: h1.fit(X[train_idx],y[train_idx])
h1.score(X[validation_idx],y[validation_idx]) Out:0.90000000000000002
如您所见,交叉验证迭代器仅为您提供索引功能,并且在使用索引对假设进行评分评估时由您自己决定。 这为复杂的验证操作打开了机会。
在其他最有用的迭代器中,值得一提的是:
StratifiedKFold的工作方式与Kfold相似,但它始终返回与训练集大致相同的类别百分比的折叠。 这样可以使每个折痕保持平衡; 因此,学习器适合正确的类别比例。 代替案例数,它需要输入目标变量 y 作为输入参数。 如上一节所述,默认情况下,迭代器包装在cross_val_score函数中。LeaveOneOut的工作方式类似于Kfold,但它仅作为一个观测值的验证集返回。 因此,最后,折叠数将等于训练集中的示例数。 我们建议您仅在训练集严重不平衡(例如,在欺诈检测问题中)或很小的训练集时使用此交叉验证方法,尤其是在观察值少于 100 的情况下 – K 倍验证会极大减少训练集。LeavePOut在LeaveOneOut的优点和局限性方面相似,但其验证集由 P 个案例组成。 因此,总折叠数将是所有可用案例中 P 个案例的组合(随着数据集大小的增长,实际上可能是相当大的数量)。LeaveOneLabelOut提供了一种方便的方法,可根据您预先准备或计算的方案进行交叉验证。 实际上,它的行为类似于Kfolds,但是对于折叠已经被标记并提供给标签参数这一事实。LeavePLabelOut是LeaveOneLabelOut的变体。 在这种情况下,根据您事先准备的方案,测试折痕由许多标签组成。
要了解有关每个迭代器所需的特定参数的更多信息,建议您访问 Scikit-learn 网站。
实际上,交叉验证也可以用于预测目的。 实际上,对于特定的数据科学项目,可能会要求您从可用数据中构建模型,然后对完全相同的数据进行预测。 如前所述,使用训练预测将导致高方差估计,因为该模型已经拟合到该数据上,因此它已经记住了其许多特征。
应用于预测的交叉验证过程可以解决:
- 创建一个交叉验证迭代器(最好具有大量的 K 折)。
- 反复进行交叉验证,每次使用
k-1训练倍数训练模型。 - 在每次迭代中,在验证折痕上(实际上是超出样本的折痕),生成预测并将其存储起来,以跟踪其索引。 这样做的最好方法是拥有一个预测矩阵,该矩阵将通过使用花式索引进行预测。
这种方法通常称为跨验证倍数预测。
采样和自举
在说明了基于折叠,p-out 和自定义方案的迭代器之后,我们将继续对交叉验证迭代器进行概述,并引用所有基于采样的迭代器。
采样方案是不同的,因为它们不会拆分训练集,但是会使用不同的方法对其进行采样:子采样或自举。
当您随机选择可用数据的一部分,从而获得比初始数据集小的数据集时,将执行子采样。
二次采样非常有用,尤其是当您需要广泛地检验假设时,尤其是您不希望从极小的测试样本中获得验证时(因此,您可以选择不采用遗忘式方法或使用KFold大量折叠)。 以下是相同的示例:
In: subsampling = model_selection.ShuffleSplit(n_splits=10,
test_size=0.1, random_state=1)
for train_idx, validation_idx in subsampling.split(range(100)): print (train_idx, validation_idx) Out:[92 39 56 52 51 32 31 44 78 10 2 73 97 62 19 35 94 27 46 38 67 99 54
95 88 40 48 59 23 34 86 53 77 15 83 41 45 91 26 98 43 55 24 4 58 49
21 87 3 74 30 66 70 42 47 89 8 60 0 90 57 22 61 63 7 96 13 68 85
14 29 28 11 18 20 50 25 6 71 76 1 16 64 79 5 75 9 72 12 37] [80
84 33 81 93 17 36 82 69 65]
...
与其他迭代器类似,n_splits将设置子样本数,test_size百分比(如果给出浮点数)或用作测试的观察数。
自举作为一种重采样方法,已经很长时间用于估计统计数据的采样分布。 因此,根据对机器学习假设的样本外表现的评估,这是一种合适的方法。
自举的工作方式是随机选择观察值并允许重复,直到建立了一个与原始数据集大小相同的新数据集。
不幸的是,由于引导是通过替换采样(即允许重复相同的观察值)来工作的,因此,由于以下原因会出现问题:
- 案例可能同时出现在训练和测试集上(您仅需出于测试目的使用引导外样本观察)
- 与交叉验证估计相比,方差少且偏差大,这是由于替换采样导致的观察结果不一致。
尽管该功能很有用(至少从我们作为数据科学从业人员的角度来看),但我们向您建议了Bootstrap的简单替代品,该替代品适用于交叉验证,可以通过迭代调用。 它生成与输入数据(索引的长度)大小相同的样本引导程序,以及可用于测试目的的排除索引列表(样本之外):
In: import random
def Bootstrap(n, n_iter=3, random_state=None):
"""
Random sampling with replacement cross-validation generator.
For each iter a sample bootstrap of the indexes [0, n) is
generated and the function returns the obtained sample
and a list of all the excluded indexes.
"""
if random_state:
random.seed(random_state)
for j in range(n_iter):
bs = [random.randint(0, n-1) for i in range(n)]
out_bs = list({i for i in range(n)} - set(bs))
yield bs, out_bs
boot = Bootstrap(n=100, n_iter=10, random_state=1)
for train_idx, validation_idx in boot:
print (train_idx, validation_idx)
Out:[37, 12, 72, 9, 75, 5, 79, 64, 16, 1, 76, 71, 6, 25, 50, 20, 18, 84,
11, 28, 29, 14, 50, 68, 87, 87, 94, 96, 86, 13, 9, 7, 63, 61, 22, 57,
1, 0, 60, 81, 8, 88, 13, 47, 72, 30, 71, 3, 70, 21, 49, 57, 3, 68,
24, 43, 76, 26, 52, 80, 41, 82, 15, 64, 68, 25, 98, 87, 7, 26, 25,
22, 9, 67, 23, 27, 37, 57, 83, 38, 8, 32, 34, 10, 23, 15, 87, 25, 71,
92, 74, 62, 46, 32, 88, 23, 55, 65, 77, 3] [2, 4, 17, 19, 31, 33, 35,
36, 39, 40, 42, 44, 45, 48, 51, 53, 54, 56, 58, 59, 66, 69, 73, 78,
85, 89, 90, 91, 93, 95, 97, 99]
...
该函数执行二次采样,并接受n_iter索引的参数n以绘制引导程序样本,并接受random_state索引以提高可重复性。
超参数优化
机器学习假设不仅由学习算法确定,还由其超参数(算法的参数必须事先确定,并且在训练过程中无法学习)确定,并选择要用于实现最佳学习参数的变量。
在本节中,我们将探索如何扩展交叉验证方法,以找到能够推广到我们的测试集的最佳超参数。 我们将继续使用 Scikit-learn 包提供的手写数字数据集。 这是有关如何加载数据集的有用提示:
In: from sklearn.datasets import load_digits
digits = load_digits()
X, y = digits.data, digits.target
此外,我们将继续使用支持向量机作为学习算法:
In: from sklearn import svm
h = svm.SVC()
hp = svm.SVC(probability=True, random_state=1)
这次,我们将处理两个假设。 第一个假设只是输出标签作为预测的普通 SVC。 第二个假设是通过将random_state固定为1的标签概率(probability=True参数)的计算来增强 SVC,以保证结果的可重复性。 SVC 输出概率可以通过所有需要概率的损失度量来评估,而不是通过结果来进行标签预测,例如 AUC。
运行上述代码段后,我们准备导入model_selection模块并设置要通过交叉验证测试的超参数列表。
我们将使用GridSearchCV函数,该函数将根据搜索时间表自动搜索最佳参数,并根据预定义或自定义评分函数对结果进行评分:
In: from sklearn import model_selection
search_grid = [
{'C': [1, 10, 100, 1000], 'kernel': ['linear']},
{'C': [1, 10, 100, 1000], 'gamma': [0.001, 0.0001],
'kernel': ['rbf']},
]
scorer = 'accuracy'
现在,我们导入了模块,使用字符串参数('accuracy')设置了scorer变量,并创建了一个由两个字典组成的列表。
记分器是一个字符串,我们从 Scikit-learn 文档的预定义值部分中找到的可能范围中选择了一个字符串,可以在这个页面。
使用预定义值只需要您从列表中选择一个评估指标(有一些用于分类和回归,有一些用于聚类)并通过将字符串直接插入或使用字符串变量插入GridSearchCV函数。
GridSearchCV还接受称为param_grid的参数,该参数可以是一个字典,其中包含要更改的所有超参数的指示作为键,并作为要引用字典键的值包含要测试的参数列表。 因此,如果要测试关于超参数C的假设的表现,则可以创建如下字典:
{'C' : [1, 10, 100, 1000]}
另外,根据您的喜好,您可以使用专门的 NumPy 函数来生成在对数刻度上均匀分布的数字(就像我们在上一章中看到的那样):
{'C' :np.logspace(start=-2, stop=3, num=6, base=10.0)}
因此,您可以枚举所有可能的参数值并测试它们的所有组合。 但是,您还可以堆叠不同的字典,使每个字典仅包含应一起测试的部分参数。 例如,当使用 SVC 时,设置为线性的核会自动排除gamma参数。 实际上,将其与线性核相结合将浪费计算能力,因为它不会对学习过程产生任何影响。
现在,让我们继续进行网格搜索,对它进行计时(由于%timeit命令魔术命令),以了解完成整个过程将花费多少时间:
In: search_func = model_selection.GridSearchCV(estimator=h,
param_grid=search_grid, scoring=scorer,
n_jobs=-1, iid=False, refit=True, cv=10)
%timeit search_func.fit(X,y)
print (search_func.best_estimator_)
print (search_func.best_params_)
print (search_func.best_score_) Out: 4.52 s ± 75.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
SVC(C=10, cache_size=200, class_weight=None, coef0=0.0, degree=3,
gamma=0.001,
kernel='rbf', max_iter=-1, probability=False, random_state=None,
shrinking=True, tol=0.001, verbose=False)
{'kernel': 'rbf', 'C': 10, 'gamma': 0.001}
0.981081122784
在我们的计算机上完成搜索大约需要10秒。 搜索指出,最好的解决方案是具有rbf核,C=10和gamma=0.001的支持向量机分类器,交叉验证的平均准确率为0.981。
至于GridSearchCV命令,除了我们的假设(估计参数)param_grid和我们刚刚谈到的得分外,我们决定设置其他可选但有用的参数:
-
首先,我们将设置
n_jobs=-1。 这迫使该函数使用计算机上所有可用的处理器,因此我们运行 Jupyter 单元。 -
然后,我们将使用最佳估计器的参数设置
refit=True,以使函数适合整个训练集。 现在,我们只需要对新数据应用search_funct.predict()方法即可获得新的预测。 -
cv参数设置为 10 倍(但是,您可以选择较小的倍数,以牺牲测试精度为代价进行折衷)。 -
iid参数设置为False。 此参数决定如何计算有关类的误差度量。 如果类是平衡的(如本例所示),则设置iid不会有太大影响。 但是,如果它们不平衡,则默认情况下,iid=True将使带有更多示例的类在全局误差的计算上更具权重。 相反,iid=False意味着应将所有类都视为相同。 由于我们希望 SVC 能够识别从 0 到 9 的每个手写数字,无论给每个手写数字提供了多少示例,将iid参数设置为False是正确的选择。 根据您的数据科学项目,您可以决定实际上更喜欢将默认值设置为True。
建立自定义评分函数
对于我们的实验,我们选择了预定义的评分函数。 对于分类,有五种可用的度量(准确率,AUC,精确度,召回率和 F1 得分),对于回归,有三种度量(R²,MAE 和 MSE)。 尽管它们是一些最常见的度量,但您可能不得不使用其他度量。 在我们的示例中,我们发现使用损失函数来找出正确答案的可能性仍然很高,即使分类器是错误的(因此请考虑正确答案是否是第二个或第三个选项) 算法)。 我们该如何处理?
在sklearn.metrics模块中,实际上有一个log_loss函数。 我们要做的就是将其包装为GridSearchCV可以使用的方式:
In: from sklearn.metrics import log_loss, make_scorer
Log_Loss = make_scorer(log_loss,
greater_is_better=False,
needs_proba=True)
这里是。 基本上,它是单线的。 通过从sklearn.metrics调用make_scorer到log_loss误差函数,我们创建了另一个函数(Log_Loss)。 我们还想指出,我们想通过设置greater_is_better=False来最小化该度量(这是损失,而不是分数)。 我们还将指定它适用于概率,而不适用预测(因此,设置needs_proba=True)。 由于它使用概率,因此我们将使用在上一节中刚刚定义的hp假设,因为否则 SVC 不会为其预测提供任何概率:
In: search_func = model_selection.GridSearchCV(estimator=hp,
param_grid=search_grid, scoring=Log_Loss,
n_jobs=-1, iid=False, refit=True, cv=3)
search_func.fit(X,y)
print (search_func.best_score_)
print (search_func.best_params_) Out: -0.16138394082
{'kernel': 'rbf', 'C': 1, 'gamma': 0.001}
现在,我们的超参数针对日志损失(而非准确率)进行了优化。
要记住的一件好事是,针对正确的特征进行优化可以为您的项目带来更好的结果。 因此,花费在分数函数上的时间始终是花费在数据科学上的时间。
在这一点上,让我们想象您有一项艰巨的任务。 由于很容易将手写数字 1 和 7 弄错,因此您必须优化算法以最大程度地减少手写数字 1 和 7 的错误。 您可以通过定义新的损失函数来实现此目标:
In: import numpy as np
from sklearn.preprocessing import LabelBinarizer
def my_custom_log_loss_func(ground_truth,
p_predictions,
penalty = list(),
eps=1e-15):
adj_p = np.clip(p_predictions, eps, 1 - eps)
lb = LabelBinarizer()
g = lb.fit_transform(ground_truth)
if g.shape[1] == 1:
g = np.append(1 - g, g, axis=1)
if penalty:
g[:,penalty] = g[:,penalty] * 2
summation = np.sum(g * np.log(adj_p))
return summation * (-1.0/len(ground_truth))
通常,函数的第一个参数应该是实际答案,第二个参数应该是预测或预测的概率。 您还可以添加具有默认值的参数,或者稍后在调用make_scorer函数时可以固定其值:
In: my_custom_scorer = make_scorer(my_custom_log_loss_func,
greater_is_better=False,
needs_proba=True, penalty = [4,9])
在这种情况下,我们为易混淆的数字4和9设置了罚款(但是,您可以更改它,甚至将其保留为空,以检查由此产生的损失是否与之前的实验相同) sklearn.metrics.log_loss函数)。
现在,当评估数字4和9类别的结果时,新的损失函数将log_loss误差计算为两倍:
In: from sklearn import model_selection
search_grid = [{'C': [1, 10, 100, 1000], 'kernel': ['linear']},
{'C': [1, 10, 100, 1000], 'gamma': [0.001, 0.0001], 'kernel': ['rbf']}]
search_func = model_selection.GridSearchCV(estimator=hp,
param_grid=search_grid, scoring=my_custom_scorer, n_jobs=1,
iid=False, cv=3)
search_func.fit(X,y)
print (search_func.best_score_)
print (search_func.best_params_)
Out: -0.199610271298
{'kernel': 'rbf', 'C': 1, 'gamma': 0.001}
请注意,对于最后一个示例,我们设置n_jobs=1。 此选择背后有技术原因。 如果您在 Windows 上运行此代码(在任何 Unix 或 macOS 系统中,实际上都可以),则可能会发生错误,该错误可能会阻塞 Jupyter 笔记本。 借助joblib包,Scikit-learn 包中的所有交叉验证函数(以及许多其他函数)都可以使用多处理器来工作。 这样的包要求所有函数都必须在多个处理器上运行,以便将它们导入,并且如果它们是动态定义的,则它们不能接受(它们应该是可选取的)。 可能的解决方法是将该函数保存到磁盘上的文件中,例如custom_measure.py,然后使用from custom_measure import Log_Loss命令将其导入。
减少网格搜索运行时间
通过检查网格规范要求的所有参数组合,GridSearchCV函数实际上可以为您管理大量工作。 无论如何,当数据或网格搜索空间很大时,该过程可能需要很长时间才能计算出来。
从model_selection模块采用以下方法可以解决此问题。 RandomizedSearchCV提供了一种程序,可随机抽取组合样本并报告找到的最佳组合。
这具有一些明显的优点:
- 您可以限制计算数量。
- 您可以获得良好的结果,或者在最坏的情况下,了解在网格搜索中将精力集中在哪里。
RandomizedSearchCV具有与GridSearchCV相同的选项,但:- 有一个
n_iter参数,它是随机样本的数量。 - 包括
param_distributions,其功能与param_grid相同。 但是,它仅接受字典,如果将分布分配为值而不是离散值列表,则效果更好。 例如,您可以分配C:scipy.stats.expon(scale=100)之类的分配,而不是C: [1, 10, 100, 1000]。
- 有一个
让我们使用之前的设置来测试此函数:
In: search_dict = {'kernel': ['linear','rbf'],'C': [1, 10, 100, 1000],
'gamma': [0.001, 0.0001]}
scorer = 'accuracy'
search_func = model_selection.RandomizedSearchCV(estimator=h,
param_distributions=search_dict,
n_iter=7,
scoring=scorer,
n_jobs=-1,
iid=False,
refit=True,
cv=10,
return_train_score=False)
%timeit search_func.fit(X,y)
print (search_func.best_estimator_)
print (search_func.best_params_)
print (search_func.best_score_) Out: 1.53 s ± 265 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
SVC(C=10, cache_size=200, class_weight=None, coef0=0.0, degree=3,
gamma=0.001, kernel='rbf', max_iter=-1, probability=False,
random_state=None, shrinking=True, tol=0.001, verbose=False)
{'kernel': 'rbf', 'C': 1000, 'gamma': 0.001}
0.981081122784
仅使用一半的计算(7 个得出详尽的网格搜索得出的 14 个试验结果),它找到了一个等效的解决方案。 让我们来看看已经测试过的组合:
In: res = search_func.cv*results* for el in zip(res['mean_test_score'], res['std_test_score'],
res['params']):
print(el) Out: (0.9610800248897716, 0.021913085707003094, {'kernel': 'linear',
'gamma': 0.001, 'C': 1000})
(0.9610800248897716, 0.021913085707003094, {'kernel': 'linear', 'gamma': 0.001, 'C': 1})
(0.9716408520553866, 0.02044204452092589, {'kernel': 'rbf', 'gamma': 0.0001, 'C': 1000})
(0.981081122784369, 0.015506818968315338, {'kernel': 'rbf', 'gamma': 0.001, 'C': 10})
(0.9610800248897716, 0.021913085707003094, {'kernel': 'linear', 'gamma': 0.001, 'C': 10})
(0.9610800248897716, 0.021913085707003094, {'kernel': 'linear', 'gamma': 0.0001, 'C': 1000})
(0.9694212166750269, 0.02517929728858225, {'kernel': 'rbf', 'gamma': 0.0001, 'C': 10})
即使没有所有组合的完整概述,一个好的示例也会提示您仅寻找 RBF 核以及某些C和gamma范围,从而将随后的网格搜索限制在潜在搜索空间的有限范围内。
依靠基于随机过程的优化可能看起来是靠运气,但是实际上,这是探索超参数空间的一种非常有效的方法,尤其是在高维空间中。 如果安排得当,随机搜索不会在一定程度上牺牲探索的完整性。 在高维超参数空间中,网格搜索探索趋向于重复相似参数组合的测试,事实证明,在存在不相关参数或效果密切相关的参数的情况下,计算效率低下。
James Bergstra 和 Yoshua Bengio 设计了随机搜索,以使深度学习中超参数的最佳组合搜索更加有效。 原始论文为进一步了解这种方法提供了很好的资源。
统计测试表明,要使随机搜索取得良好的效果,您应尝试从最少 30 次试验到最多 60 次试验(此经验法则是基于最佳覆盖了 5% 至 10% 超参数的假设空间,并且 95% 的成功率是可以接受的)。 因此,如果您的网格搜索需要类似的搜索(这样您就可以利用随机搜索的属性)或需要进行大量的实验(从而可以节省计算量),通常可以选择随机搜索。
特征选择
对于将要使用的机器学习算法,不相关且多余的特征可能会导致结果模型的可解释性不足,训练时间长,最重要的是过拟合和泛化性差。
过拟合与观察次数与数据集中可用变量的比率有关。 当变量与观测值相比有很多时,由于变量之间的相关性,您的学习算法将有更多的机会以某种局部优化或某些杂散噪声的拟合结果。
除了降维(这需要您转换数据)之外,特征选择还可以解决上述问题。 通过选择最具预测性的变量集,可以简化高维结构。 也就是说,即使某些特征在独立级别上并不是很好的预测指标,它也会选择能够很好地协同工作的特征。
Scikit-learn 包提供了广泛的特征选择方法:
- 基于方差的选择
- 单变量选择
- 递归消除
- 随机逻辑回归/稳定性选择
- 基于 L1 的特征选择
- 基于树的特征选择
方差,单变量和递归消除可以在feature_selection模块中找到。 其他是特定机器学习算法的副产品。 除了基于树的选择(将在第 4 章,“机器学习”中提到)之外,我们还将介绍所有前面的方法,并指出它们如何帮助您改善您从数据中学习的结果。
基于特征差异的选择
此方法是最简单的特征选择方法,通常用作基准。 它只是删除所有差异较小的特征; 通常低于一组。 默认情况下,VarianceThresholder对象将删除所有零方差特征,但您可以使用阈值参数进行控制。
让我们创建一个由10观测值和5特征组成的小型数据集,其中3具有参考意义:
In: from sklearn.datasets import make_classification
X, y = make_classification(n_samples=10, n_features=5,
n_informative=3, n_redundant=0, random_state=101)
现在,让我们测量它们的Variance:
In: print ("Variance:", np.var(X, axis=0)) Out: Variance: [ 2.50852168 1.47239461 0.80912826 1.51763426
1.37205498]
较低的方差与第三个特征相关; 因此,如果要选择四个最佳特征,则应将最小方差阈值设置为1.0。 让我们这样做,看看第一次观察数据集会发生什么:
In: from sklearn.feature_selection import VarianceThreshold
X_selected = VarianceThreshold(threshold=1.0).fit_transform(X)
print ("Before:", X[0, :])
print ("After: ", X_selected[0, :]) Out: Before: [ 1.26873317 -1.38447407 0.99257345 1.19224064 -2.07706183]
After: [ 1.26873317 -1.38447407 1.19224064 -2.07706183]
正如预期的那样,在特征选择过程中删除了第三列,并且所有输出观察都没有该列。 仅保留方差大于 1.0 的那些。 记住在应用VarianceThresholder之前不要对数据集进行 Z 归一化(例如,使用StandardScaler); 否则,所有特征将具有单一方差。
单变量选择
在单变量选择的帮助下,我们打算根据统计测试选择与target变量关联最大的单个变量。
有三种可用的测试可以使我们的选择基于:
f_regression对象根据变量与目标的线性回归中的解释方差与无法解释的方差之比,使用 F 检验和 p 值。 这仅对回归问题有用。f_classif对象是 ANOVA F 测试,可用于处理分类问题。Chi2对象是卡方检验,适用于目标是分类且变量是计数或二进制数据(它们应该为正)的情况。
所有测试均具有得分和 p 值。 较高的分数和 p 值表示该变量是关联的,因此对目标有用。 测试未考虑变量为重复变量或与另一个变量高度相关的实例。 因此,最适合排除不那么有用的变量,而不是突出显示最有用的变量。
为了使过程自动化,还有一些可用的选择例程:
SelectKBest基于测试的分数,采用k个最佳变量。- 根据测试分数,
SelectPercentile占执行变量的最高百分比。 - 根据测试的 p 值,选择
SelectFpr(假阳性率测试),SelectFdr(假发现率测试)和SelectFwe(家庭错误率程序)。
您还可以使用score_func参数使用GenericUnivariateSelect函数创建自己的选择过程,该参数将获取预测变量和目标,并根据您喜欢的统计检验返回得分和 p 值。
这些函数提供的最大优势是,它们提供了一系列方法来选择变量(拟合),然后自动将所有集合缩减(转换)为最佳变量。 在我们的示例中,我们使用.get_support()方法来从前 25% 的预测变量的Chi2和f_classif测试中获取布尔索引。 然后,我们确定两个测试都选择的变量:
In: X, y = make_classification(n_samples=800, n_features=100,
n_informative=25,
n_redundant=0, random_state=101)
make_classification创建一个800案例和100特征的数据集。 重要变量占总数的四分之一:
In: from sklearn.feature_selection import SelectPercentile
from sklearn.feature_selection import chi2, f_classif
from sklearn.preprocessing import Binarizer, scale
Xbin = Binarizer().fit_transform(scale(X))
Selector_chi2 = SelectPercentile(chi2, percentile=25).fit(Xbin, y)
Selector_f_classif = SelectPercentile(f_classif,
percentile=25).fit(X, y)
chi_scores = Selector_chi2.get_support()
f_classif_scores = Selector_f_classif.get_support()
selected = chi_scores & f_classif_scores # use the bitwise and operator
如果使用卡方关联度量,如上例所示,则输入X必须为非负数:X必须包含布尔值或频率,因此,如果变量高于,则选择在归一化后进行二值化。 平均。
最终选择的变量包含一个布尔向量,指出了两个测试均已证明的 21 个预测变量。
作为基于经验的建议,通过使用不同的统计检验并保留较高百分比的变量,您可以通过排除信息量较小的变量来有效地利用单变量选择,从而简化预测变量集。
递归消除
单变量选择的问题是选择包含冗余信息的子集的可能性,而我们的兴趣是获得与我们的预测器算法一起使用的最小集合。 在这种情况下,递归消除可以帮助提供答案。
通过运行以下脚本,您将发现问题的再现,这是一个非常具有挑战性的问题,并且通常还会在不同案例和可变大小的数据集中遇到该问题:
In: from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=100, n_features=100,
n_informative=5,
n_redundant=2, random_state=101)
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.30,
random_state=101)
In: from sklearn.linear_model import LogisticRegression
classifier = LogisticRegression(random_state=101)
classifier.fit(X_train, y_train)
print ('In-sample accuracy: %0.3f' %
classifier.score(X_train, y_train))
print ('Out-of-sample accuracy: %0.3f' %
classifier.score(X_test, y_test))
Out: In-sample accuracy: 1.000
Out-of-sample accuracy: 0.667
我们有一个带有大量变量的小型数据集。 这是p > n类型的问题,其中p是变量数,n是观察数。
在这种情况下,数据集中肯定会有一些信息变量,但是其他变量提供的噪声可能会在为正确的特征分配正确的系数时使学习算法蒙蔽。 请记住,这种情况不是进行数据科学的最佳操作环境。 因此,最多只能期待平庸的结果。
这反映出较高的(在我们的情况下,是完美的)样本内准确率,当在样本外进行测试或使用交叉验证时,准确率会急剧下降。
在这种情况下,提供学习算法,关于评分/损失函数和交叉验证过程的指令RFECV类,开始对所有变量拟合初始模型并基于交叉验证计算分数。 在这一点上,RFECV开始修剪变量,直到达到交叉验证得分开始降低的一组变量为止(而通过修剪,得分应该保持稳定或增加):
In: from sklearn.feature_selection import RFECV
selector = RFECV(estimator=classifier, step=1, cv=10,
scoring='accuracy')
selector.fit(X_train, y_train)
print('Optimal number of features : %d' % selector.n_features_)
Out: Optimal number of features : 4
在我们的示例中,RFECV从100变量中只选择了其中四个。 在转换训练和测试集之后,我们可以在测试集上检查结果,以反映变量修剪:
In: X_train_s = selector.transform(X_train)
X_test_s = selector.transform(X_test)
classifier.fit(X_train_s, y_train)
print ('Out-of-sample accuracy: %0.3f' %
classifier.score(X_test_s, y_test)) Out: Out-of-sample accuracy: 0.900
通常,当您发现训练结果(基于交叉验证,而不是样本中得分)与样本外结果之间存在较大差异时,递归选择可以通过以下方法帮助您从学习算法中获得更好的表现: 指出一些最重要的变量。
稳定性和基于 L1 的选择
尽管有效,但递归消除实际上是一种逐步算法,其选择基于单个求值序列。 修剪时,它会选择某些选项,可能会排除许多其他选项。 这是将特别具有挑战性和耗时的问题(例如在可能的集合中进行详尽搜索)减少为更易于管理的一个好方法。 无论如何,还有另一种解决问题的方法,就是联合使用所有手头的变量。 一些算法使用正则化来限制系数的权重,从而防止过拟合和最相关变量的选择而不会失去预测能力。 特别是,正则化 L1(套索)在创建变量系数的稀疏选择方面众所周知,因为它会根据设置的正则化强度将许多变量推到 0 值。
一个示例将阐明逻辑回归分类器和我们用于递归消除的综合数据集的用法。
顺便说一句,linear_model.Lasso将计算出 L1 正则化以进行回归,而linear_model.LogisticRegression和svm.LinearSVC则将其分类:
In: from sklearn.svm import LinearSVC
classifier = LogisticRegression(C=0.1, penalty='l1', random_state=101)
classifier.fit(X_train, y_train)
print ('Out-of-sample accuracy: %0.3f' %
classifier.score(X_test, y_test)) Out: Out-of-sample accuracy: 0.933
样本外精度比使用贪婪方法获得的精度更高。 秘密是在初始化LogisticRegression类时分配的penalty='l1'和C值。 由于C是基于 L1 的选择的主要成分,因此正确选择它非常重要。 这可以通过使用网格搜索和交叉验证来完成,但是通过正则化获得变量选择的方法更简单,更有效:稳定性选择。
稳定性选择即使在默认值下也可以成功使用 L1 正则化(尽管您可能需要更改它们才能改善结果),因为它通过随机选择训练数据集中的一部分来二次采样(即通过使用来重新计算正则化过程多次)来验证其结果。
结果不包括通常将其系数估计为零的所有变量。 仅当变量的系数为非零时,该变量才被认为对数据集和特征集变化稳定,这是要包含在模型中的重要内容(因此,其名称为“稳定性选择”)。
让我们通过实现选择方法(通过使用我们之前使用的数据集)进行测试:
In: from sklearn.linear_model import RandomizedLogisticRegression
selector = RandomizedLogisticRegression(n_resampling=300,
random_state=101)
selector.fit(X_train, y_train)
print ('Variables selected: %i' % sum(selector.get_support()!=0))
X_train_s = selector.transform(X_train)
X_test_s = selector.transform(X_test)
classifier.fit(X_train_s, y_train)
print ('Out-of-sample accuracy: %0.3f' %
classifier.score(X_test_s, y_test))
Out: Variables selected: 3
Out-of-sample accuracy: 0.933
实际上,仅通过使用RandomizedLogisticRegression类的默认参数,我们获得了与基于 L1 的选择相似的结果。
该算法工作正常。 它是可靠的并且开箱即用(没有参数可调整,除非您想降低C值以加快速度)。 我们只是建议您将n_resampling参数设置为较大的数目,以便您的计算机可以在合理的时间内处理稳定性选择。
如果要对回归问题求助于相同的算法,则应改用RandomizedLasso类。 让我们看看如何使用它。 首先,我们创建一个足以解决回归问题的数据集。 为简单起见,我们将使用 100 个样本,10 个特征的观察矩阵; 信息特征的数量为4。
然后,我们可以让RandomizezLasso通过打印得分来找出最重要的特征(信息量最大的特征)。 请注意,所得分数是浮点数:
In: from sklearn.linear_model import RandomizedLasso
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=100, n_features=10,
n_informative=4,
random_state=101)
rlasso = RandomizedLasso()
rlasso.fit(X, y)
list(enumerate(rlasso.scores_))
Out: [(0, 1.0),
(1, 0.0),
(3, 0.0),
(4, 0.0),
(5, 1.0),
(6, 0.0),
(7, 0.0),
(8, 1.0),
(9, 0.0)]
不出所料,权重非零的特征数为4。 选择它们,因为它们是进行任何进一步分析的最有用的信息。 也就是说,演示该方法的有效性,您可以在大多数特征选择情况下安全地应用该方法,以便快速选择适用于逻辑或线性回归模型以及其他线性模型的有用特征。
在管道中包装所有内容
作为最后一个主题,我们将讨论如何将到目前为止已经看到的转换和选择操作包装到一个命令中,该命令将把您的数据从源到机器学习算法。
将所有数据操作包装到一个命令中可以带来一些好处:
- 因为流水线迫使您依赖函数进行操作(每个步骤都是一个函数),所以您的代码变得清晰且逻辑上更合理。
- 您以与训练数据完全相同的方式对待测试数据,而无需重复代码或过程中可能出现任何错误。
- 您可以轻松地在已设计的所有数据管道上网格搜索最佳参数,而不仅仅是在机器学习超参数上。
我们根据需要构建的数据流来区分两种包装器:串行或并行。
串行处理意味着您的转换步骤彼此依赖,因此必须按一定顺序执行。 对于串行处理,Scikit-learn 提供了Pipeline类,可以在pipeline模块中找到该类。
另一方面,并行处理意味着您所有的转换都仅取自相同数据,并且可以通过单独的过程轻松执行,这些过程的结果将在最后收集在一起。 Scikit-learn 还具有用于并行处理的类FeatureUnion,该类再次位于pipeline模块中。 FeatureUnion有趣的方面是它也可以并行化任何串行管线。
将特征组合在一起并链接转换
弄清楚FeatureUnion和Pipeline如何工作的最佳方法是什么? 只要回想一下 Scikit-learn API 的工作方式:首先,实例化一个类,然后将其拟合到一些数据,然后根据先前的拟合来转换相同的数据(或某些不同的数据)。 您只需提供包含步骤名称和要执行的命令的元组来指示管道,而不是与脚本一起执行。 根据顺序,这些操作将由您的 Python 线程执行或分配给多个处理器上的不同线程。
在我们的示例中,我们试图复制我们之前的示例,通过稳定性选择来构建逻辑回归分类器。 首先,我们在此基础上添加一些无监督的学习和特征创建。 我们首先通过创建训练和测试数据集来设置问题:
In: import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.pipeline import FeatureUnion
X, y = make_classification(n_samples=100, n_features=100,
n_informative=5,
n_redundant=2, random_state=101)
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.30,
random_state=101)
classifier = LogisticRegression(C=0.1, penalty='l1', random_state=101)
这样做之后,我们指示并行执行 PCA,KernelPCA和两个自定义转换器-一个仅按原样传递特征,而另一个按逆向传递。 您可以期望transformer_list中的每个元素都适合,应用转换以及所有结果按列堆叠在一起,但是仅当执行transform方法时(这是一个懒惰的执行;定义FeatureUnion不会触发任何执行)。
您还会发现使用make_pipeline和make_union命令获得相同结果很有用。 实际上,这些命令可以生成FeatureUnion和Pipeline类,可以将它们设置为输出。 值得一提的是,它们不需要您命名步骤,因为该功能将自动完成命名:
In: from sklearn.decomposition import PCA
from sklearn.decomposition import KernelPCA
from sklearn.preprocessing import FunctionTransformer
def identity(x):
return x
def inverse(x):
return 1.0 / x
parallel = FeatureUnion(transformer_list=[
('pca', PCA()),
('kernelpca', KernelPCA()),
('inverse', FunctionTransformer(inverse)),
('original',FunctionTransformer(identity))], n_jobs=1)
请注意,我们已经将n_jobs设置为1,从而完全避免了多重处理。 这是因为负责在 Scikit-learn 上进行多核并行处理的joblib包无法与 Windows 上运行的 Jupyter 笔记本上的自定义函数一起正常使用。 如果您使用的是 macOS 或 Linux,则可以安全地将n_jobs设置为多个工作程序,或将问题上的所有多核资源都设置为(-1)。 但是,在 Windows 上运行时,除非您没有使用自定义函数而是从包中选择它们,或者您在将__name__变量设置为__main__的脚本中运行代码,否则肯定会遇到一些问题。 在本章的“构建自定义评分函数”部分的结尾,我们已经在技术上更详细地讨论了这个问题。 请同时参阅该部分提示中的建议,以获取有关该问题的更多见解。
定义并行操作后,我们可以继续准备完整的管道:
In: from sklearn.preprocessing import RobustScaler
from sklearn.linear_model import RandomizedLogisticRegression
from sklearn.feature_selection import RFECV
selector = RandomizedLogisticRegression(n_resampling=300,
random_state=101,
n_jobs=1)
pipeline = Pipeline(steps=[('parallel_transformations', parallel),
('random_selection', selector),
('logistic_reg', classifier)])
拥有完整的转换和学习流水线的一大优势是可以控制其所有参数。 我们可以在管道上测试网格搜索,以便找到超参数的最佳配置:
In: from sklearn import model_selection
search_dict = {'logistic_reg__C':[10,1,0.1], 'logistic_reg__penalty':
['l1','l2']}
search_func = model_selection.GridSearchCV(estimator=pipeline,
param_grid =search_dict, scoring='accuracy', n_jobs=1,
iid=False, refit=True, cv=10)
search_func.fit(X_train,y_train)
print (search_func.best_estimator_)
print (search_func.best_params_)
print (search_func.best_score_)
在定义参数网格搜索时,可以通过以下方式引用管道的不同部分:编写管道名称,添加两个下划线以及要调整的参数名称。 例如,对逻辑回归的C超参数起作用,需要将其命名为'logistic_reg__C'。 如果参数嵌套在多个管道中,则只需将它们全部命名,并用双下划线分隔,就好像您导航到磁盘目录一样。
由于使用双下划线来构造管道的步骤和超参数的层次结构,因此在命名管道的步骤时不能使用它。
作为最后的步骤,我们仅使用结果搜索对测试集进行预测。 完成此操作后,Python 将执行完整的管道,并使用网格搜索设置的超参数,并为您提供结果。 您不必担心将在训练上完成的工作复制到测试集中; 管道中的一组指令将始终确保数据处理操作的一致性和可重复性:
In: from sklearn.metrics import classification_report
print (classification_report(y_test, search_func.predict(X_test)))
Out: precision recall f1-score support
0 0.94 0.94 0.94 17
1 0.92 0.92 0.92 13
avg / total 0.93 0.93 0.93 30
构建自定义转换函数
就像您将注意到的那样,在我们的示例中,我们使用了两个自定义转换函数,一个标识和一个逆函数,以使原始特征与转换后的特征保持一致并使特征逆。 自定义转换可以帮助您处理针对您的问题的特定解决方案,并且您还将发现它们很有用,因为它们可以通过过滤不必要或错误的值来充当过滤器。
您可以通过应用sklearn.preprocessing中的FunctionTransformer函数来创建自定义转换,该函数使用fit和transform方法将任何函数转换为 Scikit-learn 类。 从头开始创建转换可能有助于您清楚其工作方式。
首先,您必须创建一个类。 让我们看一个过滤某些列的示例,该列先前是您从数据集中定义的:
In: from sklearn.base import BaseEstimator, TransformerMixin
class filtering(BaseEstimator, TransformerMixin):
def __init__(self, columns):
self.columns = columns
def fit(self, X, y=None):
return self
def transform(self, X):
if len(self.columns) == 0:
return X
else:
return X[:,self.columns]
使用__init__方法,您可以定义参数以实例化该类。 在这种情况下,您只需记录一个列表,其中包含要过滤的列的位置。 然后,您必须为该类准备fit和transform方法。
在我们的示例中,fit方法仅返回自身。 在不同情况下,使用fit方法可能很有用,以便跟踪稍后要应用于测试集的训练集的特征(例如,特征的均值和方差, 最大值和最小值,依此类推)。
您想对数据执行的实际操作是在transform方法中执行的。
您可能还记得,由于 Scikit-learn 在内部使用 NumPy 数组进行操作,因此将转换后的数据视为 NumPy 数组非常重要。
定义了类后,可以根据需要将其包装在Pipeline或FeatureUnion中。 在我们的示例中,我们只是通过选择训练集的前五个特征并对其进行 PCA 转换来创建管道:
In: ff = filtering([1,2,3])
ff.fit_transform(X_train)
Out: array([[ 0.78503915, 0.84999568, -0.63974955],
[-2.4481912 , -0.38522917, -0.14586868],
[-0.6506899 , 1.71846072, -1.14010846],
...
总结
在本章中,我们通过应用许多高级数据操作(从 EDA 和特征创建到降维和离群值检测)从数据中提取了有意义的含义。
更重要的是,我们借助许多示例开始开发我们的数据管道。 这是通过将训练/交叉验证/测试设置封装到我们的假设中来实现的,该假设以各种活动表示-从数据选择和转换到学习算法及其最佳超参数的选择。
在下一章中,我们将深入研究 Scikit-learn 包提供的主要机器学习算法,例如线性模型,支持向量机,树的集合以及无监督的聚类技术等。
四、机器学习
在说明了数据科学项目中的所有数据准备步骤之后,我们终于到达了学习阶段,在此阶段应用了学习算法。 为了向您介绍 Scikit-learn 和其他 Python 包中现成的最有效的机器学习工具,我们准备了所有主要算法系列的简要介绍。 我们使用超参数示例和技巧来完成此过程,以确保获得最佳结果。
在本章中,我们将介绍以下主题:
- 线性和逻辑回归
- 朴素贝叶斯
- K 最近邻居(KNN)
- 支持向量机(SVM)
- 解决方案包
- 装袋和提升分类器
- 基于随机梯度的大数据分类与回归
- 使用 K 均值和 DBSCAN 的无监督聚类
神经网络和深度学习将在下一章中介绍。
准备工具和数据集
如前几章所述,用于机器学习的 Python 包是 Scikit-learn。 在本章中,我们还将使用 XGboost,LightGBM 和 Catboost:您将在相关部分中找到说明。
法国计算机科学与自动化研究所 Inria 开发的 Scikit-learn 的使用动机是多种多样的。 在这一点上,有必要提及使用 Scikit-learn 成功实现数据科学项目的最重要原因:
- 跨模型的一致 API(
fit,predict,transform和partial_fit)自然有助于正确实现对以 NumPy 数组组织的数据进行处理的数据科学过程 - 完整的经过测试的可扩展经典机器学习模型供您选择,提供了许多核心实现,用于从 RAM 内存中无法容纳的数据中学习
- 一群杰出的贡献者(Andreas Mueller,Olivier Grisel,Fabian Pedregosa,Gael Varoquaux,Gilles Loupe,Peter Prettenhofer 等)为我们带来了稳定的发展,并增加了许多新产品。
- 包含许多示例的大量文档,可使用
help命令在线或在线查阅
在本章中,我们将 Scikit-learn 的机器学习算法应用于一些示例数据集。 我们将分解非常有启发性但太常用的鸢尾和波士顿数据集,以证明将机器学习应用于更现实的数据集。 我们从以下示例中选择了有趣的示例:
- 柏林工业大学托管的机器学习数据集存储库( mldata.org )
- UCI 机器学习存储库( archive.ics.uci.edu/ml/datasets.html )
- LIBSVM 数据集(由台湾大学林志仁提供)
为了让您拥有这样的数据集,而不必每次都要测试示例时都不必依赖互联网连接,建议您下载它们并将其存储在硬盘上。 因此,我们准备了一些脚本来自动下载数据集,这些脚本将精确地放置在您使用 Python 的目录中,从而使数据访问更加容易:
In: import pickle
import urllib
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
from sklearn.datasets import fetch_mldata
from sklearn.datasets import load_svmlight_file
from sklearn.datasets import fetch_covtype
from sklearn.datasets import fetch_20newsgroups
mnist = fetch_mldata("MNIST original")
pickle.dump(mnist, open("mnist.pickle", "wb"))
target_page =
'http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary/ijcnn1.bz2'
with urllib.request.urlopen(target_page) as response:
with open('ijcnn1.bz2','wb') as W: W.write(response.read()) target_page =
'http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/cadata'
cadata = load_svmlight_file(urllib.request.urlopen(target_page))
pickle.dump(cadata, open("cadata.pickle", "wb"))
covertype_dataset = fetch_covtype(random_state=101, shuffle=True)
pickle.dump(covertype_dataset, open(
"covertype_dataset.pickle", "wb"))
newsgroups_dataset = fetch_20newsgroups(shuffle=True,
remove=('headers', 'footers', 'quotes'), random_state=6)
pickle.dump(newsgroups_dataset, open(
"newsgroups_dataset.pickle", "wb"))
如果下载程序的任何部分对您不起作用,我们将为您直接下载数据集。 获取压缩的 zip 包后,您要做的就是将其数据解压缩到当前工作的 Python 目录中,您可以通过使用以下命令在 Python 界面(Jupyter 笔记本电脑或任何 Python IDE)上运行来找到该目录:
In: import os
print ("Current directory is: "%s"" % (os.getcwd()))
您可以使用其他开源代码测试本书中的所有算法,并可以随意使用数据集。 Google 提供了一个搜索引擎,可以在这个页面上为您的实验寻找合适的数据:您只需要询问搜索引擎想要的内容即可。
线性和逻辑回归
线性回归和逻辑回归是分别可用于线性预测目标值或目标类别的两种方法。 让我们从线性回归预测目标值的示例开始。
在本节中,我们将再次使用波士顿数据集,其中包含 506 个样本,13 个特征(所有实数)和一个(实数)数字目标(这使其非常适合回归问题)。 我们将使用训练/测试拆分交叉验证来将我们的数据集分为两部分,以测试我们的方法(在本示例中,我们的数据集的 80% 用于训练,而 20% 的数据在测试集中):
In: from sklearn.datasets import load_boston
boston = load_boston()
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(boston.data,
boston.target, test_size=0.2, random_state=0)
现在已加载数据集,并已创建训练/测试对。 在接下来的几个步骤中,我们将训练回归变量并将其拟合到训练集中,并预测测试数据集中的目标变量。 然后,我们将使用 MAE 分数来衡量回归任务的准确率(如第 3 章,“数据管道”中所述)。 至于评分函数,我们决定了平均绝对误差,以与误差本身的大小成正比地惩罚误差(使用更常见的均方误差会更多地强调较大的误差,因为误差是平方的):
In: from sklearn.linear_model import LinearRegression
regr = LinearRegression()
regr.fit(X_train, Y_train)
Y_pred = regr.predict(X_test)
from sklearn.metrics import mean_absolute_error
print ("MAE", mean_absolute_error(Y_test, Y_pred)) Out: MAE 3.84281058945
伟大的! 我们以最简单的方式实现了我们的目标。 现在让我们看一下训练系统所需的时间:
In: %timeit regr.fit(X_train, y_train) Out: 544 µs ± 37.4 µs per loop
(mean ± std. dev. of 7 runs, 1000 loops each)
那真的很快! 结果当然不是那么好(如果您在本书前面的第 1 章,“第一步”)。 但是,线性回归在表现与训练速度和简单性之间提供了很好的权衡。 现在,让我们看一下该算法的内幕。 为什么这么快却不那么准确? 答案在某种程度上是可以预期的-这是因为它是一种非常简单的线性方法。
让我们简要地对此技术进行数学解释。 让我们将X(i)命名为第i个样本(实际上是具有数字特征的行向量),并将Y(i)作为目标。 线性回归的目标是找到一个良好的权重(列)向量W,该向量最适合于与观察向量相乘时近似目标值,即X(i) * W ≈ Y(i)(请注意,这是点积)。W应该相同,并且对每个观察结果都是最佳的。 因此,解决以下等式变得容易:

W可以借助矩阵求逆(或更可能是伪求逆,这是一种计算有效的方法)和点积的帮助容易地找到。 这就是线性回归如此之快的原因。 请注意,这只是一个简单的解释-实际方法添加了另一个虚拟特征来补偿过程的偏差。 但是,这并没有很大程度地改变回归算法的复杂性。
我们现在进行逻辑回归。 尽管顾名思义,它是一个分类器,而不是回归器。 它只能用于仅处理两个类的分类问题(二分类)。 通常,目标标签是布尔值; 也就是说,它们的值为True/False或 0/1(指示是否存在预期结果)。 在我们的示例中,我们继续使用相同的数据集。 目标是猜测房屋价值是高于还是低于我们感兴趣的阈值的平均值。本质上,我们从回归问题转向二元分类问题,因为现在我们的目标是猜测一个示例成为一个小组的一部分的可能性有多大。 我们开始使用以下命令来准备数据集:
In: import numpy as np
avg_price_house = np.average(boston.target)
high_priced_idx = (Y_train >= avg_price_house)
Y_train[high_priced_idx] = 1
Y_train[np.logical_not(high_priced_idx)] = 0
Y_train = Y_train.astype(np.int8)
high_priced_idx = (Y_test >= avg_price_house)
Y_test[high_priced_idx] = 1
Y_test[np.logical_not(high_priced_idx)] = 0
Y_test = Y_test.astype(np.int8)
现在我们将训练并应用分类器。 要衡量其表现,我们只需打印分类报告:
In: from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, Y_train)
Y_pred = clf.predict(X_test)
from sklearn.metrics import classification_report
print (classification_report(Y_test, Y_pred))
Out:
precision recall f1-score support
0 0.81 0.92 0.86 61
1 0.85 0.68 0.76 41
avg / total 0.83 0.82 0.82 102
根据LogisticRegression分类器的优化过程,此命令的输出可以在您的计算机上更改(未设置种子以提高结果的可复制性)。
precision和recall值超过80百分比。 对于一个非常简单的方法来说,这已经是一个很好的结果。 训练速度也令人印象深刻。 感谢 Jupyter 笔记本,我们可以在表现和速度方面将算法与更高级的分类器进行比较:
In: %timeit clf.fit(X_train, y_train)
Out: 2.75 ms ± 120 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
什么是逻辑回归的内幕? 人们可以想象的最简单的分类器(除均值外)是线性回归变量,后跟一个硬阈值:
y_pred[i] = sign(X[i] * W)
在此,如果a大于或等于零,则sign(a) = 1,否则为 0。
为了降低阈值的难度并预测属于某个类别的可能性,逻辑回归求助于logit函数。 它的输出是一个(0 到 1)实数(只能通过舍入获得 0.0 和 1.0;否则, logit函数将趋向于它们),这表示观察值属于 1 类的可能性。 使用公式,结果如下:

在上面的公式中,您具有:logistic(α) = exp(α) / (1 + exp(α))。
为什么logistic函数而不是其他函数? 好吧,因为它在大多数实际情况下都很好用。 在其余情况下,如果您对其结果不完全满意,则可能需要尝试其他一些非线性函数(不过,合适的函数种类有限)。
朴素贝叶斯
朴素贝叶斯是用于概率二进制和多类分类的非常常见的分类器。 给定特征向量,它利用贝叶斯规则来预测每个类别的概率。 它通常用于文本分类,因为它对大量数据(即具有许多特征的数据集)非常有效,并且具有一致的先验概率,可有效处理维数问题的诅咒。
朴素贝叶斯分类器分为三种: 他们每个人都对这些特征有很强的假设(假设)。 如果要处理真实/连续数据,则高斯朴素贝叶斯分类器会假定特征是从高斯过程生成的(也就是说,它们是正态分布的)。 或者,如果要处理事件模型,其中可以使用多项式分布来建模事件(在这种情况下,特征是计数器或频率),则需要使用“多项朴素贝叶斯”分类器。 最后,如果您所有的特征都是独立且布尔的,并且可以安全地假设它们是伯努利过程的结果,则可以使用伯努利朴素贝叶斯分类器。
现在让我们尝试高斯朴素贝叶斯分类器的应用示例。 此外,本章结尾给出了文本分类的示例。 您可以通过简单地将示例的 SGDClassifier 替换为 MultinomialNB 来与朴素贝叶斯一起测试它。
在以下示例中,假设特征为高斯特征,我们将使用鸢尾花数据集:
In: from sklearn import datasets
iris = datasets.load_iris()
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(iris.data,
iris.target, test_size=0.2, random_state=0) In: from sklearn.naive_bayes import GaussianNB
clf = GaussianNB()
clf.fit(X_train, Y_train)
Y_pred = clf.predict(X_test) In: from sklearn.metrics import classification_report
print (classification_report(Y_test, Y_pred)) Out:
precision recall f1-score support
0 1.00 1.00 1.00 11
1 0.93 1.00 0.96 13
2 1.00 0.83 0.91 6
avg / total 0.97 0.97 0.97 30 In: %timeit clf.fit(X_train, y_train)
Out: 685 µs ± 9.86 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
生成的模型似乎具有良好的表现和较高的训练速度,尽管我们不要忘记我们的数据集也很小。 现在,让我们看看它如何处理另一个多类问题。
分类器的目的是预测特征向量属于Ck类的可能性。 在该示例中,存在三个类别(setosa,versicolor和virginica)。 因此,我们需要计算所有类别的成员资格概率; 为了使说明简单,我们将其命名为1,2和3。 因此,对于观察i,朴素贝叶斯分类器的目标是计算以下内容:

在此, X(i)是特征的向量(在示例中,它由四个实数组成),其成分为[X(i, 0), X(i, 1), X(i, 2), X(i, 3)]。
使用贝叶斯规则,它将变为以下内容:

我们可以描述相同的公式,如下所示:
后验概率是类别的先验概率乘以可能性,然后除以证据。
从概率论出发,我们知道联合概率可以表示为以下形式(简化问题):

然后,乘法的第二个因子可以重写如下(条件概率):

然后,您可以使用条件概率定义来表示乘法的第二个成员。 最后,您将得到一个很长的乘法:

朴素的假设是,当与每个类别相关时,每个特征都被认为有条件地独立于其他特征。 因此,概率可以简单地相乘。 公式如下:

因此,总结一下数学,以选择最佳类别,使用以下公式:

这是一种简化,因为除去了证据概率(贝叶斯规则的分母),因为所有类别的事件概率都相同。
从前面的公式中,您可以了解学习阶段为何如此之快的原因,因为它只是对事件的计数。
请注意,对于此分类器,不存在相应的回归变量,但是您可以通过对它进行分类来实现对连续目标变量的建模,即将其转换为类(例如,我们房屋价格的低,平均和高值) 问题)。
K 最近邻居
K 最近邻居或简称为 KNN,属于基于实例的学习类别,也称为惰性分类器。 这是最简单的分类方法之一,因为在我们要分类的情况下,仅通过查看训练集中的 K 个最接近的示例(按照欧几里得距离或某种其他距离)来完成分类。 然后,在给出 K 个相似示例的情况下,选择最受欢迎的目标(多数投票)作为分类标签。 该算法必须使用两个参数:邻域基数(K)和评估相似度的度量(尽管最常用的是欧几里德距离或 L2,并且是大多数实现的默认参数)。
让我们看一个例子。 我们将使用大型数据集MNIST手写数字。 稍后我们将解释为什么我们决定在示例中使用此数据集。 我们打算仅使用其中的一小部分(1,000 个样本)来保持合理的计算时间,并且我们将观察值进行混洗以获得更好的结果(尽管如此,您的最终输出可能与我们的最终输出略有不同):
In: from sklearn.utils import shuffle
from sklearn.datasets import fetch_mldata
from sklearn.model_selection import train_test_split
import pickle
mnist = pickle.load(open( "mnist.pickle", "rb" ))
mnist.data, mnist.target = shuffle(mnist.data, mnist.target)
# We reduce the dataset size, otherwise it'll take too much time to run mnist.data = mnist.data[:1000]
mnist.target = mnist.target[:1000]
X_train, X_test, y_train, y_test = train_test_split(mnist.data,
mnist.target, test_size=0.8, random_state=0) In: from sklearn.neighbors import KNeighborsClassifier # KNN: K=10, default measure of distance (euclidean)
clf = KNeighborsClassifier(3)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test) In: from sklearn.metrics import classification_report print (classification_report(y_test, y_pred)) Out:
precision recall f1-score support
0.0 0.79 0.91 0.85 82
1.0 0.62 0.98 0.76 86
2.0 0.88 0.68 0.76 77
3.0 0.71 0.83 0.77 69
4.0 0.68 0.88 0.77 91
5.0 0.69 0.66 0.67 56
6.0 0.93 0.86 0.89 90
7.0 0.91 0.85 0.88 102
8.0 0.91 0.41 0.57 73
9.0 0.79 0.50 0.61 74
avg / total 0.80 0.77 0.76 800
该数据集的表现不是很高。 但是,请考虑一下分类器必须处理十个不同的类。 现在,让我们检查分类器进行训练和预测所需的时间:
In: %timeit clf.fit(X_train, y_train)
Out: 1.18 ms ± 119 µs per loop (mean ± std. dev. of 7 runs,
1000 loops each)
In: %timeit clf.predict(X_test)
Out: 179 ms ± 1.68 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
训练速度异常出色。 现在考虑算法。 训练阶段只是将数据复制到该算法以后将要使用的某种数据结构中,而没有别的(这就是它被称为惰性学习器的原因)。 相反,预测速度与您在训练步骤中拥有的样本数量以及构成它的特征数量(实际上就是元素的特征矩阵数量)有关。 在我们看到的所有其他算法中,预测速度与数据集中训练案例的数量无关。 总而言之,我们可以说 KNN 非常适合小型数据集,但绝对不是处理大数据时要使用的算法。
关于该分类算法的最后一句话-您还可以尝试类似的回归器KNeighborsRegressor,其工作方式相同。 它的算法几乎相同,除了预测值是邻域的 K 目标值的平均值。
非线性算法
支持向量机(SVM)是一种强大的高级监督学习技术,用于分类和回归,可以自动拟合线性和非线性模型。
与其他机器学习算法相比,SVM 算法具有很多优势:
- 它们可以处理大多数监督问题,例如回归,分类和异常检测(无论如何,它们实际上最擅长于二元分类)。
- 它们可以很好地处理嘈杂的数据和异常值。 由于它们仅与某些特定示例(支持向量)一起使用,因此它们往往不太适合。
- 尽管与其他机器学习算法一样,SVM 可以从降维和特征选择中获得好处,但它们与呈现更多特征而不是示例的数据集配合得很好。
- 至于缺点,我们必须提到这些:
- 它们仅提供估计值,而不提供概率,除非您通过普拉特定标进行了一些耗时且计算量大的概率校准
- 它们与示例数量呈超线性比例关系(因此,它们无法处理非常大的数据集)
Scikit-learn 提供了一个基于 LIBSVM 的实现,LIBSVM 是一个完整的 SVM 分类和回归实现的库,而 LIBLINEAR 是一个可扩展的库,适用于大型数据集的线性分类,尤其是任何基于稀疏文本的数据集。 这两个库都是在国立台湾大学开发的,并且都使用 C++ 和 C API 编写,可以与其他语言接口。 这两个库都经过了广泛的测试(免费,它们已在其他开源机器学习工具包中使用),并且早已被证明既快速又可靠。 C API 很好地解释了它们在 Python Scikit-learn 下最佳运行的两个棘手需求:
- LIBSVM 在运行时需要为核操作保留一些内存。
cache_size参数用于设置核缓存的大小,以兆字节为单位指定。 尽管默认值为 200,但建议根据可用资源将其提高到 500 或 1000。 - 他们都期望 C 顺序的 NumPy
ndarray或 SciPysparse.csr_matrix(行优化的稀疏矩阵类型),最好是float64类型。 如果 Python 包装器以不同的数据结构接收它们,则它将必须以适当的格式复制数据,从而减慢训练过程并消耗更多的 RAM 内存。
LIBSVM 或 LIBLINEAR 都不提供能够处理大型数据集的实现。 SGDClassifier和SGDRegressor是 Scikit-learn 类,即使数据太大而无法容纳到内存中,它们也可以在合理的计算时间内生成解决方案。 在以下有关处理大数据的段落中将讨论它们。
支持分类的 SVM
Scikit-learn 提供的 SVM 分类的实现如下所示:
| 类 | 目的 | 超参数 |
|---|---|---|
sklearn.svm.SVC |
用于二进制和多类线性和核分类的 LIBSVM 实现 | C,核,度和伽马 |
sklearn.svm.NuSVC |
与.SVC版本相同 |
nu,核,度和gamma |
sklearn.svm.OneClassSVM |
无监督检测异常值 | nu,核,度和gamma |
sklearn.svm.LinearSVC |
基于 LIBLINEAR; 它是一个二进制和多类线性分类器 | 罚款,损失和C |
作为使用 SVM 进行分类的示例,我们将同时使用带有线性核和 RBF 核的 SVC(RBF 代表径向基函数,这是有效的非线性函数)。 取而代之的是,将LinearSVC用于表示大量观测值的复杂问题(由于三次复杂性的提高,标准 SVC 在处理 10,000 多个观测值时效果不佳;相反,LinearSVC可以线性缩放)。
对于我们的第一个分类示例(二进制),我们将使用 IJCNN'01 神经网络竞赛的数据集。 它是由 10 缸内燃机的物理系统产生的 50,000 个样本的时间序列。 我们的目标是二进制:正常发动机点火或不点火。 我们将使用本章开头的脚本从 LIBSVM 网站检索的数据集。 数据文件为 LIBSVM 格式,并由 Bzip2 压缩。 我们使用 Scikit-learn 的load_svmlight_file函数对其进行操作:
In: from sklearn.datasets import load_svmlight_file
X_train, y_train = load_svmlight_file('ijcnn1.bz2')
first_rows = 2500
X_train, y_train = X_train[:first_rows,:], y_train[:first_rows]
为了举例说明,我们将观测值的数量从 25,000 限制为 2,500。 可用特征的数量为 22。此外,由于它已经与 SVM 要求兼容,并且已经在 0 到 1 之间范围内重新缩放了特征,因此我们不会对其进行预处理。
In: import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.svm import SVC
hypothesis = SVC(kernel='rbf', random_state=101)
scores = cross_val_score(hypothesis, X_train, y_train,
cv=5, scoring='accuracy')
print ("SVC with rbf kernel -> cross validation accuracy: \
mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))) Out: SVC with rbf kernel -> cross validation accuracy:
mean = 0.910 std = 0.001
在我们的示例中,我们测试了带有 RBF 核的 SVC。 所有其他参数均保持默认值。 您可以尝试将first_rows修改为更大的值(最大 25,000),并验证算法扩展到观察数量增加的程度。 跟踪计算时间,您会注意到缩放比例不是线性的。 也就是说,计算时间将与数据大小成比例增加。 关于 SVM 可伸缩性,有趣的是,看到这种算法在遇到多类问题和大量情况时的行为。 我们将使用的 Covertype 数据集以美国 30x30 米的大片森林为例。 收集与它们有关的数据以预测每个斑块(覆盖类型)的优势树种。 这是一个多类分类问题(可以预测为七个covertypes)。 每个样本都具有 54 个特征,并且有超过 580,000 个示例(但出于性能原因,我们将仅处理 25,000 个此类示例)。 而且,类是不平衡的,大多数示例都有两种树。
这是可用于加载先前准备的数据集的脚本:
In: import pickle
covertype_dataset = pickle.load(open("covertype_dataset.pickle", "rb"))
covertype_X = covertype_dataset.data[:25000,:]
covertype_y = covertype_dataset.target[:25000] -1
使用此脚本,您可以了解要预测的示例,特征和目标:
In: import numpy as np
covertypes = ['Spruce/Fir', 'Lodgepole Pine', 'Ponderosa Pine',
'Cottonwood/Willow', 'Aspen', 'Douglas-fir', 'Krummholz']
print ('original dataset:', covertype_dataset.data.shape)
print ('sub-sample:', covertype_X.shape)
print('target freq:', list(zip(covertypes,np.bincount(covertype_y)))) Out: original dataset: (581012, 54)
sub-sample: (25000, 54)
target freq: [('Spruce/Fir', 9107), ('Lodgepole Pine', 12122),
('Ponderosa Pine', 1583), ('Cottonwood/Willow', 120), ('Aspen', 412),
('Douglas-fir', 779), ('Krummholz', 877)]
假设我们认为,由于我们有七个类别,因此我们需要训练七个不同的分类器,重点是针对一个类别与另一个类别进行预测(在多类别问题中,LinearSVC的默认行为是“剩余”)。 这样,每个交叉验证测试都将有 175,000 个数据点(因此,如果cv = 3,则必须重复三遍)。 考虑到有 54 个变量,这对许多算法来说都是一个挑战,但是LinearSVC可以演示如何在合理的时间内处理它:
In: from sklearn.cross_validation import cross_val_score, StratifiedKFold
from sklearn.svm import LinearSVC
hypothesis = LinearSVC(dual=False, class_weight='balanced')
cv_strata = StratifiedKFold(covertype_y, n_folds=3,
shuffle=True, random_state=101)
scores = cross_val_score(hypothesis, covertype_X, covertype_y,
cv=cv_strata, scoring='accuracy')
print ("LinearSVC -> cross validation accuracy: \
mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))) Out: LinearSVC -> cross validation accuracy: mean = 0.645 std = 0.007
得出的精度为0.65,这是一个很好的结果。 但是,它肯定会留下进一步改进的空间。 另一方面,这个问题似乎是一个非线性的问题,尽管将 SVC 与非线性核一起使用会导致观察过程很多,因此训练过程非常漫长。 在下面的示例中,我们将通过使用其他非线性算法来重现此问题,以检查是否可以提高LinearSVC获得的分数。
支持向量机回归
至于回归,下面显示了 Scikit-learn 提出的 SVM 算法:
| 类 | 目的 | 超参数 |
|---|---|---|
sklearn.svm.SVR |
LIBSVM 实现回归 | C,核,度,伽玛和epsilon |
sklearn.svm.NuSVR |
与.SVR相同 |
nu,C,核,度和伽玛 |
为了提供回归示例,我们决定使用加利福尼亚州房屋的房地产价格数据集(与之前看到的波士顿房屋价格数据集略有不同的问题):
In: import pickle
X_train, y_train = pickle.load(open( "cadata.pickle", "rb" ))
from sklearn.preprocessing import scale
first_rows = 2000
X_train = scale(X_train[:first_rows,:].toarray())
y_train = y_train[:first_rows]/10**4.0
由于性能原因,将数据集中的案例减少为2,000。 对特征进行了缩放,以避免原始变量的不同比例产生影响。 此外,目标变量除以1,000以使其在千美元值中更具可读性:
In: import numpy as np
from sklearn.cross_validation import cross_val_score
from sklearn.svm import SVR
hypothesis = SVR()
scores = cross_val_score(hypothesis, X_train, y_train, cv=3,
scoring='neg_mean_absolute_error')
print ("SVR -> cross validation accuracy: mean = %0.3f \
std = %0.3f" % (np.mean(scores), np.std(scores))) Out: SVR -> cross validation accuracy: mean = -4.618 std = 0.347
选择的误差是平均绝对误差,它由sklearn类报告为负数(但实际上要解释为不带符号;负号只是 Scikit-learn 的内部函数使用的计算技巧) 。
调整 SVM
在我们开始处理超参数(根据实现通常是不同的参数集)之前,在使用 SVM 算法时有两个方面需要澄清。
首先是关于 SVM 对不同规模和大量变量的敏感性。 与基于线性组合的其他学习算法类似,具有不同比例的变量会使该算法被具有较大范围或方差的特征所控制。 而且,极高或极低的数目可能在学习算法的优化过程中引起问题。 建议以有限的时间间隔缩放所有数据,例如[0, +1],这是使用稀疏数组时的必要选择。 实际上,希望保留零个条目。 否则,数据将变得密集,从而消耗更多的内存。 您也可以将数据缩放到[-1, +1]间隔。 或者,您可以将它们标准化为零均值和单位方差。 您可以从预处理模块中使用MinMaxScaler和StandardScaler工具类,方法是先将它们拟合到训练数据上,然后转换训练和测试集。
第二个方面是关于不平衡的阶级。 该算法倾向于偏爱频繁类。 除了重采样或下采样(将多数类减少为较小的相同数目)之外,一种解决方案是根据类的频率加权C惩罚参数(低值将对类进行更多的惩罚,高值将对类进行更多的惩罚) 。 对于不同的实现,有两种方法可以实现此目的。 首先,SVC 中有class_weight参数(可以将其设置为关键字balanced,或提供包含每个类的特定值的字典)。 然后,在SVC,NuSVC,SVR,NuSVR和OneClassSVM的.fit()方法中还有sample_weight参数(它需要一维数组作为输入,其中每个位置是指每个训练示例的权重)。
处理了比例和类的平衡之后,您可以使用 Sklearn 中model_selection模块中的GridSearchCV详尽搜索其他参数的最佳设置。 尽管 SVM 可以使用默认参数很好地工作,但是它们通常不是最佳的,并且您需要使用交叉验证来测试各种值组合以找到最佳参数。
根据其重要性,您必须设置以下参数:
C:惩罚值。 减小它会使余量更大,从而忽略了更多噪声,同时也使该模型更具通用性。 通常可以在np.logspace(-3, 3, 7)的范围内考虑最佳值。kernel:可以将 SVM 的非线性主力设置为linear,poly,rbf,sigmoid或自定义核(供专家使用!)。 当然,最常用的是rbf。degree:它与kernel='poly'一起使用,表示多项式展开的维数。 相反,它被其他核忽略。 通常,将其值设置为 2 到 5 效果最佳。gamma:'rbf','poly'和'sigmoid'的系数。 高值倾向于以更好的方式拟合数据,但可能导致一些过拟合。 直观地,我们可以将伽马想象为单个示例对模型的影响。 较低的值使每个示例的影响都很大。 由于必须考虑许多点,因此 SVM 曲线倾向于采用受局部点影响较小的形状,其结果将是病态的轮廓曲线。 相反,较高的gamma值会使曲线更多地考虑点的局部排列方式。 许多小气泡会阐明结果,这些小气泡通常会表示局部点所产生的影响。 该超参数的建议网格搜索范围为np.logspace(-3, 3, 7)。nu:对于使用nuSVR和nuSVC进行回归和分类,此参数将对没有可靠分类的训练点进行近似估计,即,错误分类的点和边界内或边界上的正确点。 它应该在[0, 1]的范围内,因为它是相对于您的训练集的比例。 最后,它以C的形式发挥作用,高比例扩大了边距。epsilon:此参数通过定义epsilon大范围来指定 SVR 将接受多少误差,在该范围内,与该点的真实值无关的惩罚。 建议的搜索范围是np.insert(np.logspace(-4, 2, 7),0,[0])。penalty,loss和dual:对于LinearSVC,这些参数接受('l1', 'squared_hinge', False), ('l2', 'hinge', True), ('l2', ' squared_hinge', True)和('l2', 'squared_hinge', False)组合。('l2', 'hinge', True)组合类似于SVC(kernel = 'linear')学习器。
例如,我们将再次加载 IJCNN'01 数据集,并通过寻找更好的度数C和gamma值来尝试提高初始精度 0.91。 为了节省时间,我们将使用RandomizedSearchCV类将精度提高到0.989(交叉验证估计值):
In: from sklearn.svm import SVC
from sklearn.model_selection import RandomizedSearchCV
X_train, y_train = load_svmlight_file('ijcnn1.bz2')
first_rows = 2500
X_train, y_train = X_train[:first_rows,:], y_train[:first_rows]
hypothesis = SVC(kernel='rbf', random_state=101)
search_dict = {'C': [0.01, 0.1, 1, 10, 100],
'gamma': [0.1, 0.01, 0.001, 0.0001]}
search_func = RandomizedSearchCV(estimator=hypothesis,
param_distributions=search_dict,
n_iter=10, scoring='accuracy',
n_jobs=-1, iid=True, refit=True,
cv=5, random_state=101) search_func.fit(X_train, y_train)
print ('Best parameters %s' % search_func.best_params_)
print ('Cross validation accuracy: mean = %0.3f' %
search_func.best_score_) Out: Best parameters {'C': 100, 'gamma': 0.1}
Cross validation accuracy: mean = 0.989
策略集
到目前为止,我们已经看到越来越复杂的单一学习算法。 集成表示一种有效的替代方法,因为它们通过组合或链接基于不同数据样本和算法设置的模型结果来达到更好的预测准确率。 集成策略将自己分为两个分支。 根据使用的方法,他们通过以下方式将预测组合在一起:
- 平均算法:这些算法通过平均各种并行估计量的结果来进行预测。 估计量的变化可进一步划分为四个系列:粘贴,装袋,子空间和补丁。
- 增强算法:这些算法通过使用顺序聚合估计量的加权平均值进行预测。
在研究分类和回归的一些示例之前,我们将为您提供重新加载 Covertype 数据集的必要步骤,Covertype 数据集是在处理线性 SVC 之前我们开始探讨的多类分类问题:
In: import pickle
covertype_dataset = pickle.load(open("covertype_dataset.pickle", "rb"))
print (covertype_dataset.DESCR)
covertype_X = covertype_dataset.data[:15000,:]
covertype_y = covertype_dataset.target[:15000]
covertypes = ['Spruce/Fir', 'Lodgepole Pine', 'Ponderosa Pine',
'Cottonwood/Willow', 'Aspen', 'Douglas-fir', 'Krummholz']
随机样本的粘贴
粘贴是我们将讨论的第一类平均集成。 在粘贴过程中,使用从数据中获取的小样本来构建一定数量的估计器(使用不替换的样本)。 最后,将结果汇总起来,通过对结果进行平均(在回归的情况下)或在处理分类时采用投票率最高的类别来获得估计值。 当处理非常大的数据时(例如无法容纳到内存中的情况),粘贴非常有用,因为它只允许处理计算机的可用 RAM 和计算资源可管理的那些数据部分。
作为一种方法,随机森林算法的创建者 Leo Breiman 首先设计了这种策略。 尽管可以通过使用可用的装袋算法(下段主题BaggingClassifier或BaggingRegressor,并将其bootstrap设置为)轻松实现,但 Scikit-learn 包中没有使用粘贴的特定算法。 False和max_features设为 1.0。
弱分类器的装袋
装袋用于样本的方式与粘贴类似,但是可以替换。 同样,在理论上由 Leo Breiman 阐述,装袋是在特定的 Scikit-learn 类中进行回归的,而在分类中进行。 您只需要决定要用于训练的算法即可。 将其插入BaggingClassifier或BaggingRegressor中以解决回归问题,并设置足够多的估计量(因此要设置大量的样本):
In: import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier
hypothesis = BaggingClassifier(KNeighborsClassifier(n_neighbors=1),
max_samples=0.7, max_features=0.7,
n_estimators=100)
scores = cross_val_score(hypothesis, covertype_X, covertype_y, cv=3,
scoring='accuracy', n_jobs=-1)
print ("BaggingClassifier -> cross validation accuracy: mean = %0.3f
std = %0.3f" % (np.mean(scores), np.std(scores))) Out: BaggingClassifier -> cross validation accuracy:
mean = 0.795 std = 0.001
预测器较弱是用于装袋的估计器的不错选择。 在分类或预测方面学习能力较弱的人,由于其简单性或估计的偏见,其执行效果不佳(刚好超过数据问题的机会基准)。 朴素贝叶斯和 K 最近邻就是很好的例子。 使用弱学习器并将其组合在一起的好处是,与复杂算法相比,它们可以更快地得到训练。 尽管预测能力较弱,但结合起来使用时,它们通常比更复杂的单个算法具有可比甚至更好的预测表现。
随机子空间和随机补丁
在随机子空间中,由于特征的随机子集,估计量会有所不同。 同样,可以通过调整BaggingClassifier和BaggingRegressor的参数,并将max_features设置为小于 1.0 的数字来实现这样的解决方案,该数字表示要为集合的每个模型随机选择的特征的百分比。
取而代之的是,在随机补丁中,估计器建立在样本和特征的子集上。
现在让我们在表中检查粘贴,装袋,随机子空间和随机补丁的不同特征,这些特征是使用 Scikit-learn 中的BaggingClassifier和BaggingRegressor实现的:
| 组装 | 目的 | 超参数 |
|---|---|---|
| 粘贴 | 使用子样本构建了许多模型(在不替换小于原始数据集的样本的情况下进行抽样) | bootstrap=False``max_samples <1.0``max_features=1.0 |
| 装袋 | 使用自举案例的随机选择构建了许多模型(通过替换原始样本的相同大小进行采样) | bootstrap=True``max_samples = 1.0``max_features=1.0 |
| 随机子空间 | 这与装袋相同,但是在选择每个模型时也会对特征进行采样 | bootstrap=True``max_samples = 1.0``max_features<1.0 |
| 随机补丁 | 这与装袋相同,但是在选择每个模型时也会对特征进行采样 | bootstrap=False``max_samples <1.0``max_features<1.0 |
当max_features或max_samples必须小于 1.0 时,可以将它们设置为(0, 1)范围内的任何值,并且可以通过网格搜索测试最佳值。 根据我们的经验,如果您需要限制或加快搜索速度,最有效的值是 0.7 到 0.9。
随机森林和多余的树木
Leo Breiman 和 Adele Cutler 最初是在“随机森林”算法的核心中设计出这个主意的,而该算法的名称至今仍是他们的商标(尽管该算法是开源的)。 随机森林在 Scikit-learn 中实现为RandomForestClassifier/RandomForestRegressor。
随机森林的工作方式与 Leo Breiman 所设计的装袋相似,但它只能使用二叉拆分决策树进行操作,而二叉决策树会不断发展壮大。 此外,它使用自举对每个模型中要使用的案例进行采样。 并且,随着树的生长,在分支的每个分割处,也将随机绘制要用于分割处的变量集。 最后,这是该算法的核心秘密,因为它使由于不同样本和拆分时所考虑的变量而彼此不同的树融合在一起。 与众不同,它们也不相关。 这是有益的,因为当结果汇总时,可以排除很多差异,因为平均而言,分布两侧的极值趋于彼此平衡。 换句话说,装袋算法保证了预测中的一定程度的多样性,从而允许制定可能永远不会遇到单个学习器(例如决策树)的规则。
在 Scikit-learn 中以ExtraTreesClassifier/ExtraTreesRegressor类表示的极端树是一种随机性更高的随机森林,其估计值的方差较小,但代价是估计量的偏差更大。 无论如何,在 CPU 效率方面,与随机森林相比,极端树可以提供相当大的提速,因此从示例和特征上来说,当您使用大型数据集时,极端树是理想的选择。 产生更高偏差但速度更快的原因是在极端树中构建拆分的方式。 随机森林会从要考虑拆分树的分支的采样特征中仔细搜索最佳值,以分配给每个分支,而在极端树中,这是随机决定的。 因此,尽管随机选择的分割可能不是最有效的分割(因此存在偏差),但无需进行大量计算。
让我们看看这两种算法在预测的准确率和执行时间方面如何与 Covertype 森林问题进行比较。 为此,我们将在 Jupyter 笔记本电脑的单元格中使用神奇的%%time单元格,以测量计算表现:
In: import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier
In: %%time
hypothesis = RandomForestClassifier(n_estimators=100, random_state=101)
scores = cross_val_score(hypothesis, covertype_X, covertype_y,
cv=3, scoring='accuracy', n_jobs=-1)
print ("RandomForestClassifier -> cross validation accuracy: \
mean = %0.3f std = %0.3f" % (np.mean(scores), np.std(scores))) Out: RandomForestClassifier -> cross validation accuracy:
mean = 0.809 std = 0.009
Wall time: 7.01 s In: %%time
hypothesis = ExtraTreesClassifier(n_estimators=100, random_state=101)
scores = cross_val_score(hypothesis, covertype_X, covertype_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))) Out: ExtraTreesClassifier -> cross validation accuracy:
mean = 0.821 std = 0.009
Wall time: 6.48 s
对于这两种算法,应设置的关键超参数如下:
max_features:这是每次拆分中可以确定算法表现的采样特征数量。 数字越小,速度越快,但偏差越大。min_samples_leaf:这使您可以确定树木的深度。 较大的数字会减少方差并增加偏差。bootstrap:这是一个布尔值,允许进行引导。n_estimators:这是树的数量(请记住,树越多越好,但这要以计算成本为基础)。
实际上,随机森林和极端树都是并行算法。 不要忘记设置适当的n_jobs数量以加快执行速度。 在分类时,他们决定投票最多的阶层(多数投票); 回归时,它们只是对结果值求平均值。 作为示例,我们提出一个基于加利福尼亚房价数据集的回归示例:
In: import pickle
from sklearn.preprocessing import scale
X_train, y_train = pickle.load(open( "cadata.pickle", "rb" ))
first_rows = 2000 In: import numpy as np
from sklearn.ensemble import RandomForestRegressor
X_train = scale(X_train[:first_rows,:].toarray())
y_train = y_train[:first_rows]/10**4.
hypothesis = RandomForestRegressor(n_estimators=300, random_state=101)
scores = cross_val_score(hypothesis, X_train, y_train, cv=3,
scoring='neg_mean_absolute_error', n_jobs=-1)
print ("RandomForestClassifier -> cross validation accuracy: mean = %0.3f
std = %0.3f" % (np.mean(scores), np.std(scores))) Out: RandomForestClassifier -> cross validation accuracy:
mean = -4.642 std = 0.514
估计整体概率
随机森林具有很大的优势,它们被认为是您应该尝试对数据进行研究以找出可以获得哪种结果的第一种算法。 这是因为随机森林没有太多要固定的超参数,并且开箱即用地工作得很好。 他们自然可以解决多类问题。 此外,随机森林提供了一种方法来估计变量对您的见解或特征选择的重要性,并且它们有助于估计示例之间的相似性,因为相似的情况应最终出现在集合中许多树的同一末端。
但是,在分类问题中,该算法缺乏预测结果概率的能力(除非使用CalibratedClassifierCV使用 Scikit-learn 中提供的概率校准进行校准)。 在分类问题中,通常不足以预测响应标签。 我们还需要与之相关的概率(概率为真;这是预测的可信度)。 这对于多类问题特别有用,因为正确的答案可能是第二大或第三大可能的答案(因此,概率提供了答案的等级)。
但是,当需要使用“随机森林”来估计响应类别的概率时,该算法将仅报告示例相对于该集合本身中所有树的数量被分类为该类别的次数。 这样的比率实际上并不对应于正确的概率,但是它是一个有偏的比率(预测的概率仅与真实的比率相关;它不能以数字上正确的方式表示出来)。
为了帮助随机森林和受类似情况影响的其他算法(如朴素贝叶斯或线性 SVM)发出正确的响应概率,在 Scikit-learn 中引入了CalibratedClassifierCV包装器类。
CalibrateClassifierCV使用两种方法将机器学习算法的响应重新映射为概率:普拉特定标和等渗回归(后者是表现较好的非参数方法,条件是您有足够的示例,即至少有 1,000 个)。 这两种方法都是第二级模型,旨在仅对算法的原始响应和预期概率之间的链接进行建模。 通过将原始概率分布与校准概率分布进行比较,可以绘制结果。
例如,在这里我们使用CalibratedClassifierCV来修正 Covertype 问题:
In: import pandas as pd
import matplotlib.pyplot as plt
from sklearn.calibration import CalibratedClassifierCV
from sklearn.calibration import calibration_curve
hypothesis = RandomForestClassifier(n_estimators=100, random_state=101)
calibration = CalibratedClassifierCV(hypothesis, method='sigmoid',
cv=5)
covertype_X = covertype_dataset.data[:15000,:]
covertype_y = covertype_dataset.target[:15000]
covertype_test_X = covertype_dataset.data[15000:25000,:]
covertype_test_y = covertype_dataset.target[15000:25000]
为了评估校准的行为,我们准备了一个由 10,000 个示例组成的测试集,这些示例不用于训练。 我们的校准模型将基于 Platt 的模型(method='sigmoid'),并使用五个交叉验证折叠来调整校准:
In: hypothesis.fit(covertype_X,covertype_y)
calibration.fit(covertype_X,covertype_y)
prob_raw = hypothesis.predict_proba(covertype_test_X)
prob_cal = calibration.predict_proba(covertype_test_X)
拟合原始模型和校准模型后,我们估计概率,现在将它们绘制在散点图中以突出显示差异。 在对美国黄松的估计概率进行预测之后,似乎已经对原始的随机森林概率(实际投票百分比)进行了重新缩放,使其类似于对数曲线。 现在,我们尝试编写一些代码,并探讨校准为概率输出带来的变化类型:
In: %matplotlib inline
tree_kind = covertypes.index('Ponderosa Pine')
probs = pd.DataFrame(list(zip(prob_raw[:,tree_kind],
prob_cal[:,tree_kind])),
columns=['raw','calibrted'])
plot = probs.plot(kind='scatter', x=0, y=1, s=64,
c='blue', edgecolors='white')
校准虽然不会改变模型的表现,但通过重塑概率输出可以帮助您获得与训练数据更相符的概率。 在下图中,您可以通过添加一些非线性作为校正来观察校准程序如何修改了原始概率:

模型序列—— AdaBoost
AdaBoost 是基于梯度下降优化方法的增强算法。 它适合数据重新加权版本上的一系列弱学习器(最初是树桩,即单级决策树)。 根据案例的可预测性分配权重。 较难处理的案件的权重更高。 想法是,树首先学习简单的示例,然后将更多的精力集中在困难的示例上。 最后,对弱学习器的顺序进行加权以最大程度地提高整体表现:
In: import numpy as np
from sklearn.ensemble import AdaBoostClassifier
hypothesis = AdaBoostClassifier(n_estimators=300, random_state=101)
scores = cross_val_score(hypothesis, covertype_X, covertype_y, cv=3,
scoring='accuracy', n_jobs=-1)
print ("Adaboost -> cross validation accuracy: mean = %0.3f
std = %0.3f" % (np.mean(scores), np.std(scores)))
Out: Adaboost -> cross validation accuracy: mean = 0.610 std = 0.014
梯度树增强(GTB)
梯度提升是增强的另一个改进版本。 像 AdaBoost 一样,它基于梯度下降函数。 尽管该算法的特征在于估计的方差增加,对数据中的噪声更敏感(这两个问题都可以通过使用子采样来减弱),但已证明是该算法中最熟练的算法之一。 由于非并行操作。
为了演示 GTB 的表现,我们将再次尝试检查是否可以改善 Covertype 数据集的预测表现,在说明线性 SVM 和集成算法时已经进行了检查:
In: import pickle
covertype_dataset = pickle.load(open("covertype_dataset.pickle", "rb"))
covertype_X = covertype_dataset.data[:15000,:]
covertype_y = covertype_dataset.target[:15000] -1
covertype_val_X = covertype_dataset.data[15000:20000,:]
covertype_val_y = covertype_dataset.target[15000:20000] -1
covertype_test_X = covertype_dataset.data[20000:25000,:]
covertype_test_y = covertype_dataset.target[20000:25000] -1
加载数据后,将训练样本大小限制为 15000 个观察值,以实现合理的训练效果。 我们还提取了由 5,000 个示例组成的验证样本和由另外 5,000 个示例组成的测试样本。 现在,我们开始训练模型:
In: import numpy as np
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.ensemble import GradientBoostingClassifier
hypothesis = GradientBoostingClassifier(max_depth=5,
n_estimators=50,
random_state=101)
hypothesis.fit(covertype_X, covertype_y) In: from sklearn.metrics import accuracy_score
print ("GradientBoostingClassifier -> test accuracy:",
accuracy_score(covertype_test_y,
hypothesis.predict(covertype_test_X))) Out: GradientBoostingClassifier -> test accuracy: 0.8202
为了从GradientBoostingClassifier和GradientBoostingRegression获得最佳表现,您必须进行以下调整:
n_estimators:超过估计量,会增加方差。 无论如何,如果估计量不足,则该算法将遭受高偏差。 正确的数字不是先验已知的,必须通过交叉验证测试各种配置来启发式地找到。max_depth:它增加了方差和复杂性。subsample:使用 0.9 到 0.7 的值可以有效减少估计的方差。learning_rate:较小的值可以改善训练过程中的优化,尽管这将需要更多的估计量来收敛,因此需要更多的计算时间。in_samples_leaf:这可以减少由于噪声数据导致的方差,从而保留对极少数情况的过拟合。
除了深度学习,梯度提升实际上是最先进的机器学习算法。 自从 Adaboost 和 Jerome Friedman 开发的以下梯度提升实现以来,出现了各种算法实现,最近的是 XGBoost,LightGBM 和 CatBoost。 在以下各段中,我们将探索这些新解决方案,并使用 Forest Covertype 数据在道路上对其进行测试。
XGBoost
XGBoost 代表极限梯度提升,这是一个不属于 Scikit-learn 的开源项目,尽管最近它已通过 scikit-Learn 包装器接口进行了扩展,该接口使用基于 XGBoost 的模型进行渲染,并已集成到您的计算机的数据管道中。
XGBoost 源代码可从 GitHub 上获得; 可以在这个页面上找到其文档和一些教程。
XGBoost 算法在 Kaggle 和 KDD-cup 2015 等数据科学竞赛中获得了动力和普及。作者(Tianqui Chen,Tong He,Carlos Guestrin)在有关该算法的论文中报告,2015 年期间在 Kaggle 上进行了 29 个挑战,其中有 17 个获奖解决方案使用 XGBoost 作为独立解决方案或作为多个不同模型集合的一部分。
在他们的论文《XGBoost:可扩展树增强系统》中(可在这个页面中找到),作者报告说,每个团队也都使用 XGBoost 在最近的 KDD 杯 2015 的前十名中排名第二。
除了在准确率和计算效率上都取得了成功的成就外,从不同的角度来看,XGBoost 还是可扩展的解决方案。 XGBoost 代表了新一代 GBM 算法,这要归功于对初始树增强 GBM 算法的重要调整:
- 稀疏感知算法; 它可以利用稀疏矩阵,既节省内存(不需要密集矩阵),又节省计算时间(以特殊方式处理零值)。
- 近似树学习(加权分位数草绘),其结果相似,但所需时间比可能的分支切割的经典完整探索要少得多。
- 一台机器上的并行计算(在搜索最佳分割阶段使用多线程),以及在多个机器上进行类似分布的计算。
- 利用称为列块的数据存储解决方案,在一台计算机上进行核外计算。 这样可以按列在磁盘上排列数据,从而通过优化算法(适用于列向量)期望从磁盘上提取数据,从而节省了时间。
- XGBoost 还可以有效地处理丢失的数据。 基于标准决策树的其他树集合要求首先使用诸如负数之类的非标度值来估计缺失数据,以便开发出适当的树分支以处理缺失值。
相反,XGBoost 首先适合所有非缺失值。 在为变量创建分支之后,它决定哪个分支更适合丢失值,以最大程度地减少预测误差。 这样的方法可以使两棵树都更加紧凑,并提供有效的插补策略,从而提高预测能力。
从实际的角度来看,XGBoost 具有与 Scikit-learn 的 GBT 几乎相同的参数。 关键参数如下:
eta:相当于 Scikit-learn 的 GTB 中的学习率。 它影响算法学习的速度,从而影响需要多少棵树。 较高的值有助于更好地融合学习过程,但代价是要花费更多的训练时间和更多的树木。gamma:这是树开发中的停止标准,因为它表示在树的叶节点上进行进一步分区所需的最小损失减少。 更高的值使学习更加保守。min_child_weight:这些代表树的叶子节点上存在的最小权重(示例)。 较高的值可防止过拟合和树的复杂性。max_depth:树的最大深度。subsample:训练数据中示例的一部分,将在每次迭代中使用。colsample_bytree:在每次迭代中使用的特征分数。colsample_bylevel:在每个分支分割中使用的特征分数(如在随机森林中)。
在我们如何应用 XGBoost 的示例中,我们首先回顾如何通过部分切片包含完整数据集的初始 NumPy 数组来上载 Covertype 数据集并将其分为训练集,验证集和测试集:
In: from sklearn.datasets import fetch_covtype
from sklearn.model_selection import cross_val_score, StratifiedKFold covertype_dataset = fetch_covtype(random_state=101, shuffle=True) covertype_dataset.target = covertype_dataset.target.astype(int) covertype_X = covertype_dataset.data[:15000,:] covertype_y = covertype_dataset.target[:15000] -1 covertype_val_X = covertype_dataset.data[15000:20000,:] covertype_val_y = covertype_dataset.target[15000:20000] -1 covertype_test_X = covertype_dataset.data[20000:25000,:] covertype_test_y = covertype_dataset.target[20000:25000] -1
加载数据后,我们首先通过设置目标(如multi:softprob来定义超参数,但是 XGBoost 为回归,分类,多类和排名提供了其他选择),然后设置了一些前述参数。
当拟合数据时,可以对算法给出进一步的指示。 在我们的案例中,我们将eval_metric设置为针对多类问题的准确率('merror'),并提供了eval_set,这是 XGBoost 在训练过程中必须通过计算评估指标来监控的验证集。 如果训练在 25 个回合中没有提高评估指标(由early_stopping_rounds定义),则训练将在达到先前定义的估计器数量(n_estimators)之前停止。 这种方法称为“早期停止”,它源自神经网络训练,有效地避免了训练阶段的过拟合:
有关参数和评估指标的完整列表,请参见这个页面。 在这里,我们开始导入包,设置其参数并将其适合我们的问题:
In: import xgboost as xgb
hypothesis = xgb.XGBClassifier(objective= "multi:softprob", max_depth = 24,
gamma=0.1,
subsample = 0.90, learning_rate=0.01,
n_estimators = 500,
nthread=-1)
hypothesis.fit(covertype_X, covertype_y, eval_set=[(covertype_val_X, covertype_val_y)],
eval_metric='merror', early_stopping_rounds=25,
verbose=False)
为了获得预测,我们只使用与 Scikit-learn API 相同的方法:predict和predict_proba。 打印准确率揭示了到目前为止,XGBoost 算法的长期拟合实际上是如何带来最佳测试结果的。 对混淆矩阵的检查表明,只有白杨树类型很难预测:
In: from sklearn.metrics import accuracy_score, confusion_matrix
print ('test accuracy:', accuracy_score(covertype_test_y, hypothesis.predict(covertype_test_X)))
print (confusion_matrix(covertype_test_y,hypothesis.predict(covertype_test_X))) Out: test accuracy: 0.848
[[1512 288 0 0 0 2 18]
[ 215 2197 18 0 7 11 0] [ 0 17 261 4 0 19 0] [ 0 0 4 20 0 3 0] [ 1 54 3 0 19 0 0] [ 0 16 42 0 0 86 0] [ 37 1 0 0 0 0 145]]
LightGBM
当您的数据集包含大量案例或变量时,即使 XGBoost 是用 C++ 编译的,训练起来也确实需要很长时间。 因此,尽管 XGBoost 取得了成功,但仍有空间在 2017 年 1 月推出另一种算法(XGBoost 的首次出现是在 2015 年 3 月)。 它是高性能的 LightGBM,能够分发和快速处理大量数据,并且由 Microsoft 的团队作为一个开源项目开发。
这是它的 GitHub 页面。 并且,这里是说明该算法背后思想的学术论文。
LightGBM 基于决策树以及 XGBoost,但遵循不同的策略。 XGBoost 使用决策树对变量进行拆分,并在该变量上探索不同的切分(逐级树增长策略),而 LightGBM 则专注于拆分并从那里进行拆分,以实现更好的拟合(这就是叶子级别的树木生长策略)。 这使得 LightGBM 可以首先快速获得数据的良好拟合度,并生成与 XGBoost 相比的替代解决方案(如果您希望将这两个解决方案进行混合(即平均),则可以很好地解决这一问题,以减少估计值的方差) )。
从算法上讲,XGBoost 仔细研究了广度优先搜索(BFS),而 LightGBM 则是深度优先搜索(DFS)。
这是该算法的其他亮点:
- 由于采用逐叶策略,因此树木更为复杂,从而导致预测的准确率更高,但过拟合的风险也更高; 因此,这对于小型数据集(使用具有 10,000 多个示例的数据集)尤其无效。
- 在较大的数据集上速度更快。
- 它可以利用并行化和 GPU 使用率; 因此,它可以扩展到更大的问题(实际上,它仍然是 GBM,一种顺序算法;并行化的是决策树的“查找最佳拆分”部分)。
- 这是内存节约的方法,因为它不会按原样存储和处理连续变量,但是会将它们变成离散的值箱(基于直方图的算法)。
调整 LightGBM 可能会令人望而生畏,需要修复一百多个参数(您可以在此处找到所有这些参数), 但是,实际上,您只需调整几个即可获得出色的结果。 LightGBM 中的参数在以下方面有所不同:
- 核心参数,指定要对数据完成的任务
- 控制参数,决定决策树的行为
- 度量参数,无视您的误差度量(除了用于分类和回归的经典误差之外,实际上还有很多可供选择的列表)
- IO 参数,主要决定输入的处理方式
这是每个类别的主要参数的快速概述。
至于核心参数,您可以通过以下操作来选择关键选项:
task:您要用模型完成的任务; 可以是train,predict和convert_model(将其作为一系列if-else语句获取),重新装配(用于使用新数据更新模型)。application:默认情况下,预期模型为回归模型,但可以为regression,binary,multiclass和其他许多模型(也可以作为lambdarank用于对搜索引擎优化等任务进行排名 )。boosting:LightGBM 可以使用不同的算法进行学习迭代。 默认值为gbdt(单个决策树),但它可以为rf(随机森林),darts(下降点符合多个加性回归树)或goss(基于梯度的单边采样)。device:默认情况下为cpu,但是如果系统上有可用的gpu,则使用gpu。
IO 参数定义如何加载(甚至由模型存储)数据:
max_bin:用于存储特征值的最大仓数(越多,处理数字变量时的近似值越小,但更多的内存和计算时间)categorical_feature:类别特征的索引ignore_column:要忽略的特征的索引save_binary:如果将数据以二进制格式保存在磁盘上以加快加载和保存内存的速度
最后,通过设置控制参数,您可以更具体地决定模型如何从数据中学习:
num_boost_round:要执行的增强迭代次数。learning_rate:在生成的模型的构建中,每次增强迭代权重的速率。num_leaves:一棵树上的最大叶子数,默认为 31。max_depth:一棵树可以达到的最大深度。min_data_in_leaf:要创建的叶子的最小示例数。bagging_fraction:在每次迭代中随机使用的数据比例。feature_fraction:当增强为rf时,此参数指示要为分割随机考虑的全部特征的比例。early_stopping_round:固定此参数,如果您的模型在一定回合中没有改善,它将停止训练。 它有助于减少过拟合和训练时间。lambda_l1或lambda_l2:正则化参数,范围为 0 到 1(最大值)。min_gain_to_split:此参数指示在树上创建拆分的最小增益。 通过不发展分裂对模型的贡献不大,它限制了树的复杂性。max_cat_group:在处理具有高基数(大量类别)的类别变量时,此参数通过聚合次要变量来限制变量可以具有的类别数目。 此参数的默认值为 64。is_unbalance:对于二分类中的不平衡数据集,将其设置为True可使算法针对不平衡类进行调整。scale_pos_weight:对于二分类中的不平衡数据集,它也为正类别设置权重。
实际上,我们仅引用了 LightGBM 模型的所有可能参数的一小部分,但引用了最重要和最重要的参数。 浏览文档,您可以找到更多适合您特定情况和项目的参数。
我们如何调整所有这些参数? 实际上,您可以有效地进行一些操作。 如果要实现更快的计算,只需使用save_binary并设置一个较小的max_bin即可。 您还可以使用数量较少的bagging_fraction和feature_fraction来减少训练集的大小并加快学习过程(以增加解决方案的差异为代价,因为它将从更少的数据中学习)。
如果要通过误差度量获得更高的精度,则应改用较大的max_bin(这意味着在使用数字变量时精度更高),应使用较小的learning_rate和较大的num_iterations(这是必需的,因为算法会收敛),并使用较大的num_leaves(尽管可能会导致过拟合)。
在过拟合的情况下,您可以尝试设置lambda_l1,lambda_l2和min_gain_to_split并获得更多的正则化。 您也可以尝试max_depth避免生长过深的树木。
在我们的示例中,我们承担与之前相同的任务,以对 Forest Covertype 数据集进行分类。 我们首先导入必要的包。
接下来,下一步是为该增强算法设置参数以使其正常工作。 我们定义目标('multiclass'),设置较低的学习率(0.01),并允许其分支几乎完全像随机森林一样散布:其树的最大深度设置为 128,并且结果数 leaves 是 256。为此,我们还对情况和特征都进行了随机抽样(每次将其中的 90% 装袋):
In: import lightgbm as lgb
import numpy as np params = {'task': 'train', 'boosting_type': 'gbdt', 'objective': 'multiclass', 'num_class':len(np.unique(covertype_y)), 'metric': 'multi_logloss', 'learning_rate': 0.01, 'max_depth': 128, 'num_leaves': 256, 'feature_fraction': 0.9, 'bagging_fraction': 0.9, 'bagging_freq': 10}
然后,我们使用 LightGBM 包中的Dataset命令设置数据集进行训练,验证和测试:
In: train_data = lgb.Dataset(data=covertype_X, label=covertype_y)
val_data = lgb.Dataset(data=covertype_val_X, label=covertype_val_y)
最后,我们通过提供先前设置的参数,确定最大迭代次数为 2500,设置验证集,并要求误差大于 25 的迭代不能改善验证,要求提早停止训练,从而设置训练实例( 这将使我们避免由于太多的迭代而导致的过拟合,也就是说,增加了树的数量):
In: bst = lgb.train(params,
train_data, num_boost_round=2500, valid_sets=val_data, verbose_eval=500, early_stopping_rounds=25)
过了一会儿,训练停止指出对 0.40 和 851 迭代的验证所产生的对数损失是最好的选择。 进行训练,直到 25 轮验证分数没有提高:
Out: Early stopping, best iteration is:[851]
valid_0's multi_logloss: 0.400478
除了使用验证集,我们还可以通过交叉验证来测试最佳迭代次数,即在同一训练集上:
In: lgb_cv = lgb.cv(params,
train_data, num_boost_round=2500, nfold=3, shuffle=True, stratified=True, verbose_eval=500, early_stopping_rounds=25) nround = lgb_cv['multi_logloss-mean'].index(np.min(lgb_cv[ 'multi_logloss-mean'])) print("Best number of rounds: %i" % nround) Out: cv_agg's multi_logloss: 0.468806 + 0.0124661
Best number of rounds: 782
结果并不像验证集那样出色,但是回合的数量与我们之前的发现并不遥远。 无论如何,我们将在提早停车的情况下使用首列训练。 首先,我们使用预测方法获得最佳类别的概率,并获得最佳迭代,然后选择概率最高的类别作为预测。
这样做之后,我们将检查准确率并绘制混淆矩阵。 获得的分数类似于 XGBoost,但获得的训练时间较短:
In: y_probs = bst.predict(covertype_test_X,
num_iteration=bst.best_iteration)
y_preds = np.argmax(y_probs, axis=1) from sklearn.metrics import accuracy_score, confusion_matrix
print('test accuracy:', accuracy_score(covertype_test_y, y_preds)) print(confusion_matrix(covertype_test_y, y_preds)) Out: test accuracy: 0.8444
[[1495 309 0 0 0 2 14] [ 221 2196 17 0 5 9 0]
[ 0 20 258 5 0 18 0]
[ 0 0 3 19 0 5 0]
[ 1 51 4 0 21 0 0]
[ 0 14 43 0 0 87 0]
[ 36 1 0 0 0 0 146]]
CatBoost
2017 年 7 月,另一种有趣的 GBM 算法由俄罗斯搜索引擎 Yandex 公开:它是 CatBoost,其名称来自两个词 Category 和 Boosting。 实际上,它的最强之处是能够处理分类变量,该变量实际上通过采用“单热编码”和“均值编码”的混合策略来充分利用大多数关系数据库中的信息(一种通过分配变量来表达分类级别的方法) 适当的数值来解决手头的问题;稍后再讨论)。
正如《CatBoost:具有分类特征的梯度提升支持》所解释的,因为其他 GBM 解决方案都通过一次热编码来处理分类变量(就数据矩阵的内存打印而言而言非常昂贵)或通过将任意数字代码分配给分类级别(至多一种不精确的方法,需要大的分支才能生效),CatBoost 的解决方法有所不同。
您可以为算法提供分类变量的索引,并设置一个one_hot_max_size参数,告诉 CatBoost 使用单热编码来处理分类变量(如果变量的级别小于或等于该级别)。 如果变量具有更多分类级别,从而超过了one_hot_max_size参数,则算法将以与均值编码不太相同的方式对它们进行编码,如下所示:
- 排列示例级别。
- 根据损失函数将级别转换为整数,以使其最小化。
- 根据对目标的随机排序,根据对级别标签进行计数,将级别号转换为浮点数值(更多详细信息请参见这个页面(带有简单示例)。
CatBoost 用来编码分类变量的想法并不是什么新鲜事物,但它已经成为一种特征工程,已经被广泛使用,主要是在 Kaggle 的数据科学竞赛中使用。 均值编码(也称为似然编码,影响编码或目标编码)只是将标签与目标变量的关联关系转换为数字的一种方法。 如果您进行回归分析,则可以根据该级别的平均目标值来转换标签; 如果是分类,则只是给定该标签的目标分类的概率(目标概率,取决于每个类别值)。 它可能看起来像是一个简单而明智的特征工程技巧,但实际上,它具有副作用,主要是在过拟合方面,因为您正在将来自目标的信息纳入预测变量。
有几种经验方法可以限制过拟合并利用将分类变量作为数字变量进行处理。 最好了解更多信息,实际上是 Coursera 的视频,因为没有关于它的正式论文。 我们的建议是谨慎使用此技巧。
CatBoost 除了具有 R 和 Python API 并在 GBM 领域中以与 XGBoost 和 LightGBM 竞争的程度相同之外,还得益于 GPU 和多 GPU 支持(您可以在这个页面上查看性能比较。它也是一个完全开源的项目,您可以从其 GitHub 存储库中阅读以下所有代码。
即使在 CatBoost 的情况下,参数列表也非常庞大,尽管详细,但位于这个页面。 对于简单的应用,您只需要调整以下关键参数:
one_hot_max_size:目标编码任何分类变量的阈值iterations:迭代次数od_wait:评估指标未改善时要等待的迭代次数learning_rate:学习率depth:树的深度l2_leaf_reg:正则化系数random_strength和bagging_temperature控制随机装袋
我们首先导入所有必需的包和函数:
- 由于 CatBoost 在处理分类变量时表现出色,因此我们必须重建 Forest Covertype 数据集,因为其所有分类变量都已经被热编码。 因此,我们只需重建它们并重新创建数据集即可:
In: import numpy as np
from sklearn.datasets import fetch_covtype from catboost import CatBoostClassifier, Pool covertype_dataset = fetch_covtype(random_state=101,
shuffle=True)
label = covertype_dataset.target.astype(int) - 1 wilderness_area =
np.argmax(covertype_dataset.data[:,10:(10+4)],
axis=1) soil_type = np.argmax(
covertype_dataset.data[:,(10+4):(10+4+40)],
axis=1) data = (covertype_dataset.data[:,:10],
wilderness_area.reshape(-1,1), soil_type.reshape(-1,1))
data = np.hstack(data)
- 创建它之后,我们像以前一样选择训练,验证和测试部分:
In: covertype_train = Pool(data[:15000,:],
label[:15000], [10, 11])
covertype_val = Pool(data[15000:20000,:],
label[15000:20000], [10, 11]) covertype_test = Pool(data[20000:25000,:],
None, [10, 11]) covertype_test_y = label[20000:25000]
- 现在是时候设置
CatBoostClassifier了。 我们决定采用较低的学习率(0.05)和大量的迭代,最大树深度为 8(CatBoost 的实际最大值为 16),针对MultiClass进行优化(对数丢失),但同时监控训练和验证的准确率:
In: model = CatBoostClassifier(iterations=4000,
learning_rate=0.05, depth=8, custom_loss = 'Accuracy', eval_metric = 'Accuracy', use_best_model=True, loss_function='MultiClass')
- 然后,我们开始训练,关闭详细程度,但允许在样本内,更重要的是在样本外,直观地表示训练及其结果:
In: model.fit(covertype_train, eval_set=covertype_val,
verbose=False, plot=True)
这是可以为在 CoverType 数据集上训练的模型获得什么样的可视化效果的示例:

- 训练后,我们只需预测类别及其相关概率:
In: preds_class = model.predict(covertype_test)
preds_proba = model.predict_proba(covertype_test)
- 准确率评估指出,结果等效于 XGBoost(对 848 而言为 0.847),并且混淆矩阵看起来更清晰,指出此算法可以完成更好的分类工作:
In: from sklearn.metrics import accuracy_score, confusion_matrix
print('test accuracy:', accuracy_score(covertype_test_y,
preds_class))
print(confusion_matrix(covertype_test_y, preds_class))
Out: test accuracy: 0.847 [[1482 320 0 0 0 0 18] [ 213 2199 12 0 10 12 2]
[ 0 13 260 5 0 23 0]
[ 0 0 6 18 0 3 0]
[ 2 40 5 0 30 0 0]
[ 0 16 33 1 0 94 0]
[ 31 0 0 0 0 0 152]]
处理大数据
大数据将数据科学项目置于四个角度:数量(数据量),速度,多样性和准确率(您的数据是否真正代表了它应该是什么,还是受到一些偏见,失真或错误的影响?)。 Scikit-learn 包提供了一系列类和函数,可以帮助您有效地处理如此大的数据,以至于无法完全容纳在标准计算机的内存中。
在向您提供大数据解决方案概述之前,我们必须创建或导入一些数据集,以便使您更好地了解不同算法的可伸缩性和性能。 这将需要约 1.5 GB 的硬盘空间,实验后将释放该硬盘空间。
(本身不是大数据-如今,很难找到内存少于 4 GB 的计算机-但是,甚至没有玩具数据集,它也应该提供一些想法)。
创建一些大数据集作为示例
作为大数据分析的典型示例,我们将使用互联网上的一些文本数据,并利用可用的fetch_20newsgroups,其中包含 11,314 个帖子的数据,每个帖子平均约 206 个单词,出现在 20 个不同的新闻组中:
In: import numpy as np
from sklearn.datasets import fetch_20newsgroups
newsgroups_dataset = fetch_20newsgroups(shuffle=True,
remove=('headers', 'footers', 'quotes'),
random_state=6)
print ('Posts inside the data: %s' % np.shape(newsgroups_dataset.data))
print ('Average number of words for post: %0.0f' %
np.mean([len(text.split(' ')) for text in
newsgroups_dataset.data])) Out: Posts inside the data: 11314
Average number of words for post: 206
相反,为了得出通用分类示例,我们将创建三个合成数据集,其中包含 100,000 至 1000 万个案例。 您可以根据计算机的资源来创建和使用其中任何一个。 我们将始终在实验中引用最大的一个:
In: from sklearn.datasets import make_classification
X,y = make_classification(n_samples=10**5, n_features=5,
n_informative=3, random_state=101)
D = np.c_[y,X]
np.savetxt('large_dataset_10__5.csv', D, delimiter=",")
# the saved file should be around 14,6 MB
del(D, X, y)
X,y = make_classification(n_samples=10**6, n_features=5,
n_informative=3, random_state=101)
D = np.c_[y,X]
np.savetxt('large_dataset_10__6.csv', D, delimiter=",")
# the saved file should be around 146 MB
del(D, X, y)
X,y = make_classification(n_samples=10**7, n_features=5,
n_informative=3, random_state=101)
D = np.c_[y,X]
np.savetxt('large_dataset_10__7.csv', D, delimiter=",")
#the saved file should be around 1,46 GB
del(D, X, y)
创建并使用任何数据集后,可以通过以下命令将它们从磁盘中删除:
In: import os
os.remove('large_dataset_10__5.csv')
os.remove('large_dataset_10__6.csv')
os.remove('large_dataset_10__7.csv')
体积可扩展
在不将太多兆字节(或千兆字节)数据加载到内存中的情况下管理大量数据的技巧是一次仅使用部分示例来增量更新算法的参数,然后对以下数据块重复更新,直到机器学习器至少对所有观察进行了详细阐述。
借助.partial_fit()方法,这在 Scikit-learn 中是可能的,该方法已可用于一定数量的有监督和无监督算法。 通过使用.partial_fit()方法并提供一些基本信息(例如,对于分类,您应该事先知道要预测的类的数量),即使您只有一个案例或几个观察结果,也可以立即开始拟合模型 。
该方法称为incremental learning。 您逐步输入到学习算法中的数据块称为批量。 增量学习的关键点如下:
- 批量大小
- 数据预处理
- 相同示例的传递次数
- 验证和参数微调
批量大小通常取决于您的可用内存。 原理是数据块越大越好,因为随着数据样本的大小增加,数据样本将获得更多的数据分布代表。 此外,数据预处理也具有挑战性。 增量学习算法可以很好地处理[-1, +1]或[0, +1]范围内的数据(例如,多项式贝叶斯不接受负值)。 但是,要缩放到如此精确的范围,您需要事先知道每个变量的范围。 或者,您必须执行以下操作之一:一次传递所有数据,记录最小值和最大值,或者从第一批数据中导出它们,并修剪超出初始最大值和最小值的以下观察值。
解决此问题的一种更可靠的方法是使用 Sigmoid 归一化,将所有可能值范围限制在 0 到 1 之间。
通过次数可能会成为问题。 实际上,当您多次传递相同的示例时,可以帮助将预测系数收敛到最佳解决方案。 如果您通过太多相同的观察结果,该算法将趋于过拟合; 也就是说,它将过多地适应重复多次的数据。 一些算法(例如 SGD 系列)对您建议的要学习的示例的顺序也非常敏感。 因此,您必须设置其随机播放选项(shuffle = True)或在学习开始之前随机播放文件行,请记住,为提高效率,为学习建议的行顺序应该是随意的。
验证是一个批量流,可以通过两种方式实现:
- 逐步验证; 也就是说,先测试模型如何预测新到达的数据块,然后再将它们传递给训练。
- 从每个块中拿出一些观察结果。 后者也是为网格搜索或其他优化保留样本的最佳方法。
在我们的示例中,我们将对数损失(类似于逻辑回归)委托给SGDClassifier,以学习在给定10 ** 7个观察值的情况下如何预测二进制结果:
In: from sklearn.linear_model import SGDClassifier
from sklearn.preprocessing import MinMaxScaler
import pandas as pd
streaming = pd.read_csv('large_dataset_10__7.csv',
header=None, chunksize=10000)
learner = SGDClassifier(loss='log', max_iter=100)
minmax_scaler = MinMaxScaler(feature_range=(0, 1))
cumulative_accuracy = list()
for n,chunk in enumerate(streaming): if n == 0: minmax_scaler.fit(chunk.iloc[:,1:].values) X = minmax_scaler.transform(chunk.iloc[:,1:].values) X[X>1] = 1 X[X<0] = 0 y = chunk.iloc[:,0]
if n > 8: cumulative_accuracy.append(learner.score(X,y)) learner.partial_fit(X,y,classes=np.unique(y)) print ('Progressive validation mean accuracy %0.3f' %
np.mean(cumulative_accuracy)) Out: Progressive validation mean accuracy 0.660
首先,pandas read_csv允许我们通过读取 10,000 个观测值的批量来遍历文件(可以根据您的计算资源来增加或减少数目)。
我们使用MinMaxScaler来记录第一批中每个变量的范围。 对于以下批次,我们将使用以下规则:如果超过[0, +1]的限制之一,则将它们修剪到最接近的限制。 否则,我们可以使用MinMaxScaler的partial_fit方法,并在使用模型学习时了解特征的边界。 使用MinMaxScaler时,唯一需要注意的注意事项是离群值,因为离群值可以将数值转换压缩到[0, +1]间隔的一部分。
最终,从第十批开始,我们将在使用新算法更新训练之前,在每个新接收的批上记录学习算法的准确率。 最后,对累计的准确率得分进行平均,以提供整体表现评估。
速度
各种算法都使用增量学习来工作。 对于分类,我们将回忆以下内容:
sklearn.naive_bayes.MultinomialNBsklearn.naive_bayes.BernoulliNBsklearn.linear_model.Perceptronsklearn.linear_model.SGDClassifiersklearn.linear_model.PassiveAggressiveClassifier
对于回归,我们将回忆以下内容:
sklearn.linear_model.SGDRegressorsklearn.linear_model.PassiveAggressiveRegressor
至于速度,它们在速度上都是可比的。 您可以使用以下脚本自行尝试:
In: from sklearn.naive_bayes import MultinomialNB
from sklearn.naive_bayes import BernoulliNB
from sklearn.linear_model import Perceptron
from sklearn.linear_model import SGDClassifier
from sklearn.linear_model import PassiveAggressiveClassifier
import pandas as pd
from datetime import datetime
classifiers = {'SGDClassifier hinge loss' : SGDClassifier(loss='hinge',
random_state=101, max_iter=10),
'SGDClassifier log loss' : SGDClassifier(loss='log',
random_state=101, max_iter=10),
'Perceptron' : Perceptron(random_state=101,max_iter=10),
'BernoulliNB' : BernoulliNB(),
'PassiveAggressiveClassifier' : PassiveAggressiveClassifier(
random_state=101, max_iter=10)
}
large_dataset = 'large_dataset_10__6.csv'
for algorithm in classifiers:
start = datetime.now()
minmax_scaler = MinMaxScaler(feature_range=(0, 1))
streaming = pd.read_csv(large_dataset, header=None, chunksize=100)
learner = classifiers[algorithm]
cumulative_accuracy = list()
for n,chunk in enumerate(streaming):
y = chunk.iloc[:,0]
X = chunk.iloc[:,1:]
if n > 50 :
cumulative_accuracy.append(learner.score(X,y))
learner.partial_fit(X,y,classes=np.unique(y))
elapsed_time = datetime.now() - start
print (algorithm + ' : mean accuracy %0.3f in %s secs'
% (np.mean(cumulative_accuracy),elapsed_time.total_seconds())) Out: BernoulliNB : mean accuracy 0.734 in 41.101 secs
Perceptron : mean accuracy 0.616 in 37.479 secs
SGDClassifier hinge loss : mean accuracy 0.712 in 38.43 secs
SGDClassifier log loss : mean accuracy 0.716 in 39.618 secs
PassiveAggressiveClassifier : mean accuracy 0.625 in 40.622 secs
作为一般说明,请记住较小的批量速度较慢,因为这意味着从数据库或文件访问更多磁盘,这始终是瓶颈。
处理多样性
多样性是大数据的另一个典型特征。 当我们处理文本数据或很大的分类变量(例如,在程序化广告中存储网站名称的变量)时,尤其如此。 当您从大量示例中学到的知识以及展开类别或单词时,您会看到每个都是合适的排他变量。 您可能会发现难以应对多样化的挑战以及大型数据流的不可预测性。 Scikit-learn 为您提供了一种简单而又快速的方法来实现哈希技巧,并且完全忘记了预先定义刚性变量结构的问题。
哈希技巧使用哈希函数和稀疏矩阵来节省时间,资源和麻烦。 哈希函数是以确定性的方式映射它们收到的任何输入的函数。 不管您给它们提供数字还是字符串,它们都将始终为您提供一定范围内的整数。 相反,稀疏矩阵是仅记录不为零的值的数组,因为其行和列的任何组合的默认值为零。 因此,哈希技巧限制了所有可能的输入。 以前是否未在相应的输入稀疏矩阵上看到某个范围或位置没有关系,该稀疏矩阵的值不为 0。
除了 Python 中的内置哈希函数外,hashlib。 有趣的是,Scikit-learn 在许多函数和方法中也大量使用了哈希函数,并且可以使用 MurmurHash 32。 可以在针对开发人员的工具中找到它; 只需将其导入并直接使用即可:
In: from sklearn.utils import murmurhash3_32
print (murmurhash3_32("something", seed=0, positive=True))
例如,如果您输入的是Python,则诸如abs(hash('Python'))之类的哈希命令可以将其转换为整数 539294296,然后将值 1 分配给 539294296 列索引处的单元格。 哈希函数是一种非常快速便捷的方法,可以在给定相同输入的情况下始终定位相同的列索引。 仅使用绝对值可确保每个索引仅对应于数组中的一列(负索引仅从最后一列开始,因此在 Python 中,数组的每一列都可以由正数和负数表示)。
下面的示例使用HashingVectorizer类,这是一个方便的类,该类自动获取文档,分离单词,并借助哈希技巧将其转换为输入矩阵。 该脚本旨在根据新闻组中现有帖子上使用的单词来学习为什么在 20 个不同的新闻组中发布帖子:
In: import pandas as pd
from sklearn.linear_model import SGDClassifier
from sklearn.feature_extraction.text import HashingVectorizer
def streaming():
for response, item in zip(newsgroups_dataset.target,
newsgroups_dataset.data):
yield response, item
hashing_trick = HashingVectorizer(stop_words='english', norm = 'l2')
learner = SGDClassifier(random_state=101, max_iter=10)
texts = list()
targets = list()
for n, (target, text) in enumerate(streaming()):
texts.append(text)
targets.append(target)
if n % 1000 == 0 and n >0:
learning_chunk = hashing_trick.transform(texts)
if n > 1000:
last_validation_score = learner.score(learning_chunk, targets),
learner.partial_fit(learning_chunk, targets,
classes=[k for k in range(20)])
texts, targets = list(), list()
print ('Last validation score: %0.3f' % last_validation_score) Out: Last validation score: 0.710
此时,无论您输入什么文本,预测算法都将始终通过指出一个类来回答。 在我们的案例中,它指出了适合在该帖子上显示的newsgroup。 让我们使用分类广告中的文字来尝试这种算法:
In: New_text = ["A 2014 red Toyota Prius v Five with fewer than 14K" +
"miles. Powered by a reliable 1.8L four cylinder " +
"hybrid engine that averages 44mpg in the city and " +
"40mpg on the highway."]
text_vector = hashing_trick.transform(New_text)
print (np.shape(text_vector), type(text_vector))
print ('Predicted newsgroup: %s' %
newsgroups_dataset.target_names[learner.predict(text_vector)]) Out: (1, 1048576) <class 'scipy.sparse.csr.csr_matrix'>
Predicted newsgroup: rec.autos
当然,您可以更改New_text变量,并发现您的文本最有可能在新闻组中显示的位置。 请注意,HashingVectorizer类已将文本转换为csr_matrix(这是一种非常有效的稀疏矩阵)以节省内存,该数据集具有大约一百万列。
随机梯度下降(SGD)概述
我们将通过对 SGD 系列的快速概述来完成本章中的有关大数据学习的这一部分,其中包括SGDClassifier(用于分类)和SGDRegressor(用于回归)。
像其他分类器一样,可以通过使用.fit()方法(将内存中的数据集逐行传递到学习算法)或以前基于批次的.partial_fit()方法进行拟合。 在后一种情况下,如果要分类,则必须使用class参数声明预测的类。 它可以接受一个列表,其中包含在训练阶段应该满足的所有类代码。
当损失参数设置为loss时,SGDClassifier可以充当逻辑回归。 如果将损失设置为hinge,它将转换为线性 SVC。 它也可以采取其他损失函数的形式,甚至可以采用损失函数进行回归。
SGDRegressor使用squared_loss损失参数模拟线性回归。 取而代之的是,Huber 损失将平方损失转换为一定距离epsilon上的线性损失(另一固定参数)。 它也可以使用epsilon_insensitive损失函数或稍有不同的squared_epsilon_insensitive(会更严重地影响异常值)充当线性 SVR。
与在机器学习的其他情况下一样,无法先验估计数据科学问题上不同损失函数的表现。 无论如何,请考虑到如果要进行分类并且需要估计类概率,则只能将选择限制为log或modified_huber。
需要调整以使该算法最适合您的数据的关键参数如下:
n_iter:数据上的迭代次数。 根据经验,通过次数越多,算法的优化效果越好。 但是,如果通过次数过多,则存在过度装配的较高风险。 根据经验,SGD 在看到10 ** 6个示例后趋于收敛到一个稳定的解。 根据您的示例,相应地设置迭代次数。penalty:您必须选择 l1,l2 或 Elasticnet,它们都是不同的正则化策略,以避免由于过度参数化而导致过拟合(使用过多不必要的参数导致的记忆远大于对模式的学习)。 简而言之,l1 倾向于将无用系数减小为零,l2 只是将其衰减,而 Elasticnet 是 l1 和 l2 策略的混合。alpha:这是正则项的乘数;alpha越高,正则化越多。 我们建议您通过执行10 ** -7至10 ** -1的网格搜索来找到最佳的alpha值。l1_ratio:l1 比率用于弹性网惩罚。 建议值或 0.15 通常会非常有效。learning_rate:这设置每个示例对系数的影响程度。 通常,对于分类器而言,它是最佳选择;对于回归而言,invscaling是最佳选择。 如果要使用invscaling进行分类,则必须设置eta0和power_t (invscaling = eta0 / (t**power_t))。 使用invscaling,您可以从较低的学习率开始,尽管它会降低得较慢,但学习率低于最佳率。epsilon:如果您的损失是huber,epsilon_insensitive或squared_epsilon_insensitive,则应使用此选项。shuffle:如果为True,则该算法将打乱训练数据的顺序,以提高学习的通用性。
窥探自然语言处理(NLP)
本节与机器学习不是严格相关的,但它包含自然语言处理领域的一些机器学习结果。 Python 有许多处理文本数据的包,用于文本处理的功能最强大且最完整的工具箱之一是 NLTK,即自然语言工具箱。
适用于 Python 社区的其他 NLP 工具包是 gensim 和 spaCy。
在以下各节中,我们将探讨 NLTK 核心功能。 我们将使用英语。 对于其他语言,您首先需要下载语言语料库(请注意,有时语言没有用于 NLTK 的免费开源语料库)。
请访问 NLTK 数据的官方网站,以使用多种语言来访问语料库和词法资源,从而可以与 NLTK 一起使用。
单词分词
分词是将文本拆分为单词的动作。 块空白似乎很容易,但并非如此,因为文本包含标点和收缩。 让我们从一个例子开始:
In: my_text = "The coolest job in the next 10 years will be " +\
"statisticians. People think I'm joking, but " +\
"who would've guessed that computer engineers " +\
"would've been the coolest job of the 1990s?"
simple_tokens = my_text.split(' ')
print (simple_tokens)Out: ['The', 'coolest', 'job', 'in', 'the', 'next', '10', 'years', 'will',
'be', 'statisticians.', 'People', 'think', "I'm", 'joking,', 'but',
'who', "would've", 'guessed', 'that', 'computer', 'engineers',
"would've", 'been', 'the', 'coolest', 'job', 'of', 'the', '1990s?']
在这里,您可以立即看到有问题。 以下标记包含多个单词:statisticians.(带有最后一个句点),I'm(两个单词),would've和1990s?(带有最后一个问号)。 现在让我们看一下 NLTK 在此任务中的表现如何(当然,在幕后,该算法比简单的空白分块器还要复杂):
In: import nltk
nltk_tokens = nltk.word_tokenize(my_text)
print (nltk_tokens)
Out: ['The', 'coolest', 'job', 'in', 'the', 'next', '10', 'years',
'will', 'be', 'statisticians', '.', 'People', 'think', 'I',
"'m", 'joking', ',', 'but', 'who', 'would', "'ve", 'guessed',
'that', 'computer', 'engineers', 'would', "'ve", 'been', 'the',
'coolest', 'job', 'of', 'the', '1990s', '?']
在执行此调用或其他一些 NLTK 包调用时,如果出现错误消息"Resource u'tokenizers/punkt/english.pickle' not found.",只需在控制台上键入nltk.download()并选择下载所有内容或浏览触发警告的丢失资源。
在这里,质量更好,并且每个令牌都与文本中的一个单词相关联。
注意.,,和?也是令牌。
还存在一个句子标记器(请参见nltk.tokenize.punkt模块),但是在数据科学中很少使用。
此外,除了通用英语标记器外,NLTK 还包含许多其他标记器,可在不同的上下文中使用。 例如,如果您正在处理推文,则TweetTokenizer对解析类似推文的文档非常有用。 最有用的选项是删除句柄,缩短连续字符并正确分词标签。 这是一个例子:
In:
from nltk.tokenize import TweetTokenizer
tt = TweetTokenizer(strip_handles=True, reduce_len=True)
tweet = '@mate: I loooooooove this city!!!!!!! #love #foreverhere'
tt.tokenize(tweet) Out: [':', 'I', 'looove', 'this', 'city', '!', '!', '!', '#love',
'#foreverhere']
词干提取
词干提取是减少单词的变形形式并将单词带入其核心概念的动作。 例如,is,be,are和am后面的概念相同。 同样,go和goes以及table和tables后面的概念相同。 推导每个单词的词根概念的操作称为词干提取。 在 NLTK 中,您可以选择要使用的词干提取(有几种获取词根的方法)。 我们将向您展示其中一个,让 Jupyter 笔记本中的其他内容与本书的这一部分相关联:
In: from nltk.stem import *
stemmer = LancasterStemmer()
print ([stemmer.stem(word) for word in nltk_tokens]) Out: ['the', 'coolest', 'job', 'in', 'the', 'next', '10', 'year',
'wil', 'be', 'stat', '.', 'peopl', 'think', 'i', "'m", 'jok',
',', 'but', 'who', 'would', "'ve", 'guess', 'that', 'comput',
'engin', 'would', "'ve", 'been', 'the', 'coolest', 'job',
'of', 'the', '1990s', '?']
在示例中,我们使用了 Lancaster 词干提取器,它是功能最强大且最新的算法之一。 检查结果,您将立即看到它们全部为小写,并且statistician与它的根stat相关联。 做得好!
单词标记
标记或 POS 标记是单词(或令牌)与其词性标记(POS 标签)。 标记后,您知道动词,形容词,名词等在句子中的位置(和位置)。 即使在这种情况下,NLTK 也会使此复杂操作变得非常容易:
In: import nltk
print (nltk.pos_tag(nltk_tokens)) Out: [('The', 'DT'), ('coolest', 'NN'), ('job', 'NN'), ('in', 'IN'),
('the', 'DT'), ('next', 'JJ'), ('10', 'CD'), ('years', 'NNS'),
('will', 'MD'), ('be', 'VB'), ('statisticians', 'NNS'), ('.', '.'),
('People', 'NNS'), ('think', 'VBP'), ('I', 'PRP'), ("'m", 'VBP'),
('joking', 'VBG'), (',', ','), ('but', 'CC'), ('who', 'WP'),
('would', 'MD'), ("'ve", 'VB'), ('guessed', 'VBN'), ('that', 'IN'),
('computer', 'NN'), ('engineers', 'NNS'), ('would', 'MD'),
("'ve", 'VB'), ('been', 'VBN'), ('the', 'DT'), ('coolest', 'NN'),
('job', 'NN'), ('of', 'IN'), ('the', 'DT'), ('1990s', 'CD'),
('?', '.')]
使用 NLTK 的语法,您将认识到The令牌表示确定符(DT),coolest和job表示名词(NN),in表示连词,依此类推。 关联非常详细; 对于动词,有六个可能的标记,如下所示:
take:VB(动词,基本形式)took:VBD(动词,过去时)taking:VBG(动词,动名词)taken:VBN(动词,过去分词)take:VBP(动词,单数现在时)takes:VBZ(动词,第三人称单数现在时)
如果您需要更详细的句子视图,则可能需要使用解析树标记器来理解其语法结构。 由于此操作非常适合逐句分析,因此在数据科学中很少使用此操作。
命名实体识别(NER)
NER 的目标是识别与人员,组织和位置相关的令牌。 让我们用一个例子来进一步解释它:
In: import nltk
text = "Elvis Aaron Presley was an American singer and actor. Born in \
Tupelo, Mississippi, when Presley was 13 years old he and his \
family relocated to Memphis, Tennessee."
chunks = nltk.ne_chunk(nltk.pos_tag(nltk.word_tokenize(text)))
print (chunks) Out: (S
(PERSON Elvis/NNP)
(PERSON Aaron/NNP Presley/NNP)
was/VBD
an/DT
(GPE American/JJ)
singer/NN
and/CC
actor/NN
./.
Born/NNP
in/IN
(GPE Tupelo/NNP)
,/,
(GPE Mississippi/NNP)
,/,
when/WRB
(PERSON Presley/NNP)
was/VBD
13/CD
years/NNS
old/JJ
he/PRP
and/CC
his/PRP$
family/NN
relocated/VBD
to/TO
(GPE Memphis/NNP)
,/,
(GPE Tennessee/NNP)
./.)
对猫王的维基百科页面的摘录进行了分析和 NER 处理。 此处列出了 NER 认可的一些实体:
- 埃尔维斯·亚伦·普雷斯利:
PERSON(人物) - 美国人:
GPE(地缘政治实体) - 密西西比州图珀洛:
GPE(地缘政治实体) - 田纳西州孟菲斯:
GPE(地缘政治实体)
停用词
停用词是文本中信息最少的片(或标记),因为它们是最常见的词(例如,the,it,is,as和not)。 停用词通常会被删除。 而且,如果将其删除,则与在特征选择阶段中发生的情况完全相同,处理所需的时间更少,内存也更少。 此外,有时它更准确。 删除停用词会降低文本的整体熵,从而使其中的任何信号更明显,更易于在特征中表示。
在 Scikit-learn 中也可以找到英语停用词列表。 有关其他语言的停用词,请查看 NLTK:
In: from sklearn.feature_extraction import text
stop_words = text.ENGLISH_STOP_WORDS
print (stop_words) Out: frozenset(['all', 'six', 'less', 'being', 'indeed', 'over', 'move',
'anyway', 'four', 'not', 'own', 'through', 'yourselves',
'fify', 'where', 'mill', 'only', 'find', 'before', 'one',
'whose', 'system', 'how', ...
In: from nltk.corpus import stopwords
print(stopwords.words('english'))
Out: ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves',
'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him',
'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its',
'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what',
'which', 'who', 'whom', 'this', 'that', 'these', ...
In: print(stopwords.words('german'))
Out: ['aber', 'alle', 'allem', 'allen', 'aller', 'alles', 'als', 'also',
'am', 'an', 'ander', 'andere', 'anderem', 'anderen', 'anderer',
'anderes', 'anderm', 'andern', 'anderr', 'anders', 'auch',
'auf', 'au', ...
完整的数据科学示例——文本分类
现在,这是一个完整的示例,可让您将每个文本放在正确的类别中。 我们将使用20newsgroup数据集,该数据集已在第 1 章,“第一步”中引入。 为了使事情更真实,并防止分类器过拟合数据,我们将删除电子邮件标头,页脚(例如签名)和引号。 另外,在这种情况下,目标是在两个相似的类别之间进行分类:sci.med和sci.space。 我们将使用准确率度量来评估分类:
In: import nltk
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import accuracy_score
from sklearn.datasets import fetch_20newsgroups
import numpy as np
categories = ['sci.med', 'sci.space']
to_remove = ('headers', 'footers', 'quotes')
twenty_sci_news_train = fetch_20newsgroups(subset='train',
remove=to_remove, categories=categories)
twenty_sci_news_test = fetch_20newsgroups(subset='test',
remove=to_remove, categories=categories)
让我们从使用TfIdf预处理文本数据的最简单方法开始。 请记住,Tfidf是文档中单词的频率乘以所有文档中其频率的倒数。 高分表示该单词在当前文档中已被多次使用,但在其他文档中很少见(也就是说,它是文档的关键字):
In: tf_vect = TfidfVectorizer()
X_train = tf_vect.fit_transform(twenty_sci_news_train.data)
X_test = tf_vect.transform(twenty_sci_news_test.data)
y_train = twenty_sci_news_train.target
y_test = twenty_sci_news_test.target
现在,让我们使用线性分类器(SGDClassifier)来执行分类任务。 最后要做的是打印出分类精度:
In: clf = SGDClassifier()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print ("Accuracy=", accuracy_score(y_test, y_pred)) Out: Accuracy= 0.878481012658
精确度为87.8,是非常好的结果。 整个程序由少于 20 行代码组成。 现在,让我们看看是否可以做得更好。 在本章中,我们学习了停用词的删除,分词和词干提取。 让我们看看我们是否通过使用它们来获得准确率:
In: def clean_and_stem_text(text):
tokens = nltk.word_tokenize(text.lower()) clean_tokens = [word for word in tokens if word not in stop_words]
stem_tokens = [stemmer.stem(token) for token in clean_tokens]
return " ".join(stem_tokens)
cleaned_docs_train = [clean_and_stem_text(text) for text in
twenty_sci_news_train.data]
cleaned_docs_test = [clean_and_stem_text(text) for text in
twenty_sci_news_test.data]
clean_and_stem_text函数基本上将小写,分词,词干提取和重构数据集中的每个文档。 最后,我们将应用与先前示例相同的预处理(Tfidf)和分类器(SGDClassifier):
In: X1_train = tf_vect.fit_transform(cleaned_docs_train)
X1_test = tf_vect.transform(cleaned_docs_test)
clf.fit(X1_train, y_train)
y1_pred = clf.predict(X1_test)
print ("Accuracy=", accuracy_score(y_test, y1_pred)) Out: Accuracy= 0.893670886076
此处理需要更多时间,但我们获得了约 1.5% 的精度。 对Tfidf参数的精确调整和对分类器参数的交叉验证选择最终将使准确率提高到 90% 以上。 到目前为止,我们对这种表现感到满意,但是您可以尝试突破这一障碍。
无监督学习概述
到目前为止,在所有方法中,每个样本或观察值都有其自己的目标标签或值。 在其他一些情况下,数据集是未标记的,并且要提取数据结构,您需要一种无监督的方法。 在本节中,我们将介绍两种执行聚类的方法,因为它们是无监督学习中最常用的方法之一。
请记住,经常将术语聚类和无监督学习视为同义词,尽管实际上,无监督学习的含义更大。
K 均值
我们将介绍的第一种方法称为 K 均值,尽管存在不可避免的缺点,但它是最常用的聚类算法。 在信号处理中,K 均值等效于向量量化,即,选择最佳码字(从给定的码本中选择) 近似输入的观察值(或单词)。
您必须为算法提供 K 参数,即群集数。 有时,这可能是一个限制,因为您必须首先研究当前数据集的正确 K。
K 均值迭代 EM(期望最大化)方法。 在第一阶段,它将每个训练点分配给最近的聚类质心; 在第二阶段中,它将聚类质心移动到分配给它的点的质心(以减少变形)。 重心的初始位置是随机的。 因此,您可能需要多次运行算法,以免找到局部最小值。
这就是算法背后的理论。 现在,让我们在实践中看到它。 在本节中,我们使用两个二维虚拟数据集,这些数据集将说明发生了什么情况。 两个数据集均由 2,000 个样本组成,因此您也可以对处理时间有所了解。
现在,让我们创建人工数据集,然后通过图表示它们:
In: %matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
N_samples = 2000
dataset_1 = np.array(datasets.make_circles(n_samples=N_samples,
noise=0.05, factor=0.3)[0])
dataset_2 = np.array(datasets.make_blobs(n_samples=N_samples,
centers=4, cluster_std=0.4, random_state=0)
plt.scatter(dataset_1[:,0], dataset_1[:,1], c=labels_1,
alpha=0.8, s=64, edgecolors='white')
plt.show()
这是我们创建的第一个数据集,由点的同心圆环组成(这是一个非常棘手的问题,因为所表示的群集是非球形的):

In: plt.scatter(dataset_2[:,0], dataset_2[:,1], alpha=0.8, s=64,
c='blue', edgecolors='white')
plt.show()
这是第二个,由点分开的气泡组成:

现在是时候应用 K 均值了。 在这种情况下,我们将设置K=2。 让我们看看结果:
In: from sklearn.cluster import KMeans
K_dataset_1 = 2
km_1 = KMeans(n_clusters=K_dataset_1)
labels_1 = km_1.fit(dataset*1).labels* plt.scatter(dataset_1[:,0], dataset_1[:,1], c=labels_1,
alpha=0.8, s=64, edgecolors='white')
plt.scatter(km_1.cluster_centers_[:,0], km_1.cluster_centers_[:,1],
s=200, c=np.unique(labels_1), edgecolors='black')
plt.show()
这是获取关于此问题的结果:

如您所见,K 均值在此数据集上的表现不佳,因为它期望球形数据群集。 对于此数据集,应在使用 K 均值之前应用核 PCA。
现在,让我们看看它如何处理球面集群数据。 在这种情况下,基于对问题的了解和轮廓系数,我们将设置K=4:
In: K_dataset_2 = 4
km_2 = KMeans(n_clusters=K_dataset_2)
labels_2 = km_2.fit(dataset*2).labels* plt.scatter(dataset_2[:,0], dataset_2[:,1], c=labels_2,
alpha=0.8, s=64, edgecolors='white')
plt.scatter(km_2.cluster_centers_[:,0], km_2.cluster_centers_[:,1],
marker='s', s=100, c=np.unique(labels_2), edgecolors='black')
plt.show()
我们在此问题上得到的结果要好得多:

正如预期的那样,绘制的结果很好。 质心和聚类正是我们在查看未标记数据集时所想到的。 现在,我们将检查是否有其他聚类方法可以帮助我们解决非球形聚类问题。
在实际情况下,您可以考虑使用轮廓系数来了解群集的定义良好程度。 它是组内一致性的评估指标,适用于各种聚类结果,甚至适用于监督学习中的类别结构。 您可以在这个页面上了解有关剪影系数的更多信息。
DBSCAN ——一种基于密度的聚类技术
现在,我们将向您介绍 DBSCAN,这是一种基于密度的聚类技术。 这是一种非常简单的技术。 它选择一个随机点; 如果该点位于密集区域(如果它的邻居数超过 N 个),则它将开始扩展群集,包括所有邻居以及邻居的邻居,直到达到不再有邻居的点。
如果该点不在密集区域中,则将其分类为噪声。 然后,随机选择另一个未标记的点,然后过程重新开始。 该技术对非球形群集非常有用,但与球形群集同样有效。 输入只是邻域半径(eps参数,即被视为邻居的两个点之间的最大距离),输出是每个点的聚类成员资格标签。
请注意,由值 1 标记的点被 DBSCAN 分类为噪声。
让我们看一下先前介绍的数据集的示例:
In: from sklearn.cluster import DBSCAN
dbs_1 = DBSCAN(eps=0.25)
labels_1 = dbs_1.fit(dataset*1).labels* plt.scatter(dataset_1[:,0], dataset_1[:,1], c=labels_1,
alpha=0.8, s=64, edgecolors='white')
plt.show()
现在,可以通过 DBSCAN 算法正确定位群集:

结果现在是完美的。 没有将点分类为噪音(标签集中仅显示0和1标签):
In: np.unique(labels_1) Out: array([0, 1])
现在让我们转到另一个数据集:
In: dbs_2 = DBSCAN(eps=0.5)
labels_2 = dbs_2.fit(dataset*2).labels* plt.scatter(dataset_2[:,0], dataset_2[:,1], c=labels_2,
alpha=0.8, s=64, edgecolors='white')
plt.show() In: np.unique(labels_2) Out: array([-1, 0, 1, 2, 3])
为 DBSCAN 选择最佳设置花费了一些时间,在这种情况下,已检测到四个聚类,并将几个点归类为噪声(因为标签集包含 -1):

在本节的最后,最后一个重要说明是,在 K 均值和 DBSCAN 的基本介绍中,我们一直使用欧几里得距离,因为它是这些函数中的默认距离度量(尽管如果您认为合适的话,其他距离度量也可以使用)。 在实际情况下使用此距离时,请记住您必须对每个特征进行归一化(z 归一化),以便每个特征对最终失真的贡献均相等。 如果未对数据集进行规范化,则具有更大支持的特征将对输出标签具有更大的决策权,而这正是我们所不希望的。
潜在狄利克雷分布(LDA)
对于文本,可以使用潜在狄利克雷分布或 LDA 作为流行的无监督算法,该算法可用于理解文档集合中的一组常见单词。
请注意,另一种算法,线性判别分析,也具有相同的首字母缩写词,但是这两种算法是完全不相关的。
LDA 的目的是从一组文档中提取同类单词或主题集。 该算法的数学原理非常先进。 在这里,我们将看到它的一个实际概念。
让我们从一个示例开始,解释为什么 LDA 如此流行以及为什么其他无监督的方法在处理文本时不够好。 例如,K 均值和 DBSCAN 为每个样本提供了一个艰难的决定,将每个点置于不相交的分区中。 相反,文档常常描述一起涵盖的主题(想想莎士比亚的书;它们很好地融合了悲剧,浪漫史和冒险经历)。 在文本文件上,任何艰难的决定几乎肯定是错误的。 相反,LDA 提供构成文档的主题的混合输出,以及指示在文档中代表了多少主题的指示。
让我们用一个例子来解释它是如何工作的。 我们将在两个类别上训练算法; 汽车和药品,来自 20 个新闻组的数据集(在本章前面的较早段落“准备工具和数据集”中,我们已经使用了相同的数据集):
In: import nltk
Import gensim
from sklearn.datasets import fetch_20newsgroups
def tokenize(text):
return [token.lower() \
for token in gensim.utils.simple_preprocess(text) \ if token not in gensim.parsing.preprocessing.STOPWORDS] text_dataset=fetch_20newsgroups(categories=['rec.autos','sci.med'],
random_state=101, remove=('headers', 'footers', 'quotes')) documents = text_dataset.data print("Document count:", len(documents)) Out: Document count: 1188
构成数据集的 1,188 个文档中的每个文档都是一个字符串。 例如,第一个文档包含以下文本:
In: documents[0]
Out: 'nI have a new doctor who gave me a prescription today for something called nSeptra DS. He said it may cause GI problems and I have a sensitive stomach nto begin with. Anybody ever taken this antibiotic. Any good? Suggestions nfor avoiding an upset stomach? Other tips?n'
这份文件绝对是关于医学的。 无论如何,算法没有什么真正重要的。 现在,让我们分词并创建一个包含数据集中所有单词的字典。 请注意,令牌化操作还会删除停用词并将每个词都用小写字母表示:
In: processed_docs = [tokenize(doc) for doc in documents]
word_dic = gensim.corpora.Dictionary(processed_docs)
print("Num tokens:", len(word_dic)) **Out:** **Num tokens: 16161**
在数据集中,有超过 16,000 个不同的词。 现在是时候过滤太常见的单词和太稀有的单词了。 在这一步中,我们将使单词出现至少 10 次,且不超过文档的 20%。 至此,我们有了每个文档的“单词袋”(或 BoW)表示形式; 也就是说,每个文档都表示为字典,其中包含每个单词在文本中出现的次数。 文本中每个单词的绝对位置都会丢失,就像将文档中的所有单词都放在袋子中一样。 结果,并非基于此方法在特征中捕获文本中的所有信号,但在大多数情况下,足以建立有效的模型:
In: word_dic.filter_extremes(no_below=10, no_above=0.2)
bow = [word_dic.doc2bow(doc) for doc in processed_docs]
最后,这是 LDA 的核心类。 在此示例中,我们指示 LDA 在数据集中只有两个主题。 我们还提供了其他参数来使算法收敛(如果不收敛,您将收到 Python 解释器的警告)。 请注意,此算法可在计算机上的许多 CPU 上使用,以加快处理速度。 如果它不起作用,请使用具有相同参数的单进程类gensim.models.ldamodel.LdaModel:
In: lda_model = gensim.models.LdaMulticore(bow, num_topics=2,
id2word=word_dic, passes=10,
iterations=500)
最后,几分钟后,模型被训练。 要查看单词和主题之间的关联,请运行以下代码:
In: lda_model.print_topics(-1)
Out: [(0, '0.011*edu + 0.008*com + 0.007*health + 0.007*medical +
0.007*new + 0.007*use + 0.006*people + 0.005*time +
0.005*years + 0.005*patients'), (1, '0.018*car + 0.008*good +
0.008*think + 0.008*cars + 0.007*msg + 0.006*time +
0.006*people + 0.006*water + 0.005*food + 0.005*engine')]
如您所见,该算法遍历了所有文档,并了解到主要主题是汽车和医学。 请注意,该算法并未提供主题的简称,而是其组成(数字是每个主题中每个单词的权重,从高到低排列)。 另外,请注意,两个主题中都出现了一些单词; 它们是模棱两可的词,可以在两种意义上使用。
最后,让我们看看该算法如何在看不见的文档上工作。 为了使事情变得容易,让我们创建一个包含两个主题的句子,例如“我已经给医生看了我的新车。 他喜欢它的大轮子!”。然后,在为这个新文档创建了词袋表示法之后,LDA 将产生两个分数,每个主题一个分数:
In: new_doc = "I've shown the doctor my new car. He loved its big wheels!"
bow_doc = word_dic.doc2bow(tokenize(new_doc))
for index, score in sorted(lda_model[bow_doc], key=lambda tup:
-1*tup[1]):
print("Score: {}t Topic: {}".format(score,
lda_model.print_topic(index, 5))) Out: Score: 0.5047402389474193 Topic: 0.011*edu + 0.008*com +
0.007*health + 0.007*medical + 0.007*new
Score: 0.49525976105258074 Topic: 0.018*car + 0.008*good +
0.008*think + 0.008*cars + 0.007*msg
两个主题的得分都在 0.5 和 0.5 左右,这意味着句子包含了主题car和medicine的良好平衡。 我们在这里显示的只是两个主题的示例; 但是由于执行库 Gensim,相同的实现也可以在几小时内分配整个英语维基百科的进程。
Word2Vec 算法提供了与 LDA 不同的方法,Word2Vec 算法是一种用于在向量中嵌入单词的最新模型。 与 LDA 相比,Word2Vec 跟踪句子中单词的位置,并且此附加上下文有助于更好地消除某些单词的歧义。 Word2Vec 使用类似于深度学习的方法进行训练,但是 Gensim 库提供的实现使训练和使用变得非常容易。 请注意,虽然 LDA 旨在理解文档中的主题,但 Word2Vec 在单词级别起作用,并试图了解低维空间中单词之间的语义关系(即,为每个单词创建一个 N 维向量) 。 让我们看一个例子,使事情变得清楚。
我们将使用电影评论数据集来训练 Word2Vec 模型。 只需通过将组成语料库的句子传递给 Word2Vec 构造器,并最终传递可以并行执行训练任务的工作器数量即可完成训练:
In: from gensim.models import Word2Vec
from nltk.corpus import movie_reviews
w2v = Word2Vec(movie_reviews.sents(), workers=4)
w2v.init_sims(replace=True)
最后一行代码只是冻结了模型,不允许进行任何其他更新。 这也带来了另一个非常受欢迎的好处:减少对象的内存指纹。
表示单词的向量可视化可能很复杂; 因此,让我们看一些相似性(即,低维子空间中的相似向量)。 在这里,我们将要求模型提供与单词house和countryside最相似的前五个单词(以及相似性得分)。 这只是一个例子。 可以为输入语料库中包含的所有单词检索相似的单词:
In: w2v.wv.most_similar('house', topn=5)
Out: [('apartment', 0.8799251317977905), ('body', 0.8719735145568848), ('hotel', 0.8618944883346558), ('head', 0.848749041557312), ('boat', 0.8469674587249756)]
In: w2v.vw.most_similar('countryside', topn=5)
Out: [('motorcycle', 0.9531803131103516), ('marches', 0.9499938488006592), ('rural', 0.9467764496803284), ('shuttle', 0.9466159343719482), ('mining', 0.9461280107498169)]
Word2Vec 如何做到这一点? 简单地说,在低维向量空间中具有相似性得分。 实际上,要查看每个单词的向量表示,请执行以下操作:
In: w2v.wv['countryside']
Out: array([-0.09412272, 0.07695948, -0.14981066, 0.04894404,
-0.03712097, -0.17099065, -0.0379245 , -0.05336253,
0.06084964, -0.01273731, -0.03949985, -0.06456301,
-0.03289359, -0.06889232, 0.02217194, ...
数组由 100 个维度组成; 您可以在训练模型时通过设置size参数来增加或减少它。 默认值为 100。
在我们先前使用的most_similar方法中,您还可以指定要使用的否定词(即,减去相似的词)。 一个经典的例子是找到与woman和king类似的词,而没有queen。 毫不奇怪,最高结果是man:
In: w2v.wv.most_similar(positive=['woman', 'king'], negative=['queen'],
topn=3)
Out: [('man', 0.8440324068069458), ('girl', 0.7671926021575928), ('child', 0.7635241746902466)]
由于向量表示,该模型还提供了在一组相似单词中识别不匹配单词的方法。 也就是说,与上下文不匹配的单词(在这种情况下,上下文是卧室):
In: w2v.wv.doesnt_match(['bed', 'pillow', 'cake', 'mattress']) Out: 'cake'
最后,所有前面的方法都基于相似度分数。 该模型还提供了单词之间相似度的原始分数。 这是woman和girl以及woman和boy相似性得分的示例。 第一个相似度更高,但第二个相似度不为零,这是因为我们在谈论人的事实将两个词联系在一起:
In: w2v.wv.similarity('woman', 'girl'), w2v.similarity('woman', 'boy')
Out: (0.90198267746062233, 0.82372486297773828)
总结
在本章中,我们介绍了机器学习的要点。 我们从一些简单但仍然非常有效的分类器开始(线性和逻辑回归器,朴素贝叶斯和 K 最近邻)。 然后,我们继续使用更高级的(SVM)。 我们介绍了如何将弱分类器组合在一起(集合,随机森林,梯度树增强),并介绍了三个很棒的梯度提升分类器:XGboost,LightGBM 和 CatBoost。 最后,我们窥视了大数据,集群和 NLP 中使用的算法。
在下一章中,我们将向您介绍 Matplotlib 可视化的基础知识,如何使用 Pandas 操作 EDA 并通过 Seaborn 实现漂亮的可视化,以及如何设置 Web 服务器以按需提供信息。
五、可视化,见解和结果
在探索了机器学习之后,但并不是因为这个主题比其他主题没有那么重要,所以我们将说明如何使用 Python 创建可视化来丰富您的数据科学项目。 可视化在帮助您交流从数据和学习过程中获得的结果和见解方面发挥着重要作用。
在本章中,您将学习如何执行以下操作:
- 使用
matplotlib包中的基本pyplot函数 - 利用 Pandas
DataFrame进行探索性数据分析(EDA) - 与 Seaborn 一起创建漂亮的交互式图表
- 可视化我们在第 3 章,“数据管道”和第 4 章,“机器学习”中讨论的
的机器学习和优化过程 - 了解并以视觉方式传达变量的重要性及其与目标结果的关系
- 设置使用 HTTP 接受并提供预测作为服务的预测服务器
Matplotlib 的基础知识介绍
可视化是数据科学的基本方面,它使数据科学家可以更好,更有效地将其发现传达给他们所从事的组织,数据专家和非专家。 提供信息交流和精心制作引人入胜的可视化效果背后的原理的基本知识超出了本书的范围,但是如果您想提高自己的技能,我们可以建议合适的资源。
对于基本的可视化规则,您可以访问这个页面。 我们还推荐 Edward Tufte 教授有关分析设计和可视化的书。
取而代之的是,我们可以提供一系列快速,重要的基本秘籍,使您可以开始使用 Python 进行可视化,并且可以在需要创建特定图形图表时随时参考。 将所有代码片段都视为可视化构建块; 您只需使用我们将为您提供的大量参数,就可以为它们安排不同的配置和功能。
matplotlib是用于绘制图形的 Python 包。 它是由约翰·亨特(John Hunter)创建的,旨在解决 Python 与具有图形功能(例如 MATLAB 或 gnuplot)的外部软件之间缺乏集成的问题。 matplotlib在很大程度上受到 MATLAB 的操作方式和功能的影响,其语法非常相似。 特别是,与 MATLAB 完全兼容的matplotlib.pyplot模块将成为我们介绍所有必要的图形工具以表示数据和分析的基本介绍的核心。 MATLAB 确实是数据分析和科学界可视化的标准,因为它在探索性分析方面具有公认的功能,这主要是因为它具有平滑且易于使用的绘图功能。
每个pyplot命令都会对最初实例化的图形进行更改。 设置图形后,所有其他命令都将在该图形上运行。 因此,很容易逐步改进和丰富您的图形表示。 为了使您能够利用代码并能够根据需要进行个性化设置,以下所有示例均与注释的构建块一起提供,以便您以后可以起草基本表示形式,然后在本章中查找特定的参数。 在示例中,以按计划改进图表。
使用pyplot.figure()命令,您可以初始化新的可视化效果,尽管它足以调用绘图命令来自动启动它。 相反,通过使用pyplot.show(),您可以关闭正在使用的图形,然后可以打开并使用新图形。
在开始一些可视化示例之前,让我们导入必要的包以运行所有示例:
In: import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
这样,我们始终可以将 MATLAB 风格的模块pyplot称为plt,并在mpl的帮助下访问完整的matplotlib函数集。
如果您正在使用 Jupyter 笔记本(或 Jupyter Lab),则可以使用以下行魔术:%matplotlib内联。 在笔记本的单元格中写入命令并运行命令后,您可以直接在笔记本本身上绘制绘图,而不必在单独的窗口中显示图形(默认情况下,matplotlib的 GUI 后端是TkAgg后端)。 如果您更喜欢 Qt 等通常随 Python 科学发行版一起分发的后端,则只需运行以下行魔术:%matplotlib Qt。
尝试曲线图
我们的第一个问题将要求您使用pyplot绘制函数。 绘制函数非常简单; 您只需要获取一系列x坐标,然后使用要绘制的函数将它们映射到y轴即可。 由于映射结果存储在两个向量中,因此plot函数将处理曲线表示。 如果映射的点足够多(50 个点是一个好的采样数),则表示的精度将更高:
In: import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 5, 50)
y_cos = np.cos(x)
y_sin = np.sin(x)
使用 NumPy linspace()函数,我们将创建一系列 50 个等距的数字,范围从 0 到 5。我们可以使用它们将y映射到余弦和正弦函数:
In: plt.figure() # initialize a figure
plt.plot(x,y_cos) # plot series of coordinates as a line
plt.plot(x,y_sin)
plt.xlabel('x') # adds label to x axis
plt.ylabel('y') # adds label to y axis
plt.title('title') # adds a title
plt.show() # close a figure
这是您的第一个绘图:

pyplot.plot命令可以在一个序列中绘制更多曲线,每个曲线根据内部颜色模式使用不同的颜色,可以通过阐述喜欢的颜色序列进行自定义。 为此,您必须操作包含matplotlib使用的颜色序列的列表:
In: list(mpl.rcParams['axes.prop_cycle'])
Out: [{'color': '#1f77b4'},
{'color': '#ff7f0e'},
{'color': '#2ca02c'},
{'color': '#d62728'},
{'color': '#9467bd'},
{'color': '#8c564b'},
{'color': '#e377c2'},
{'color': '#7f7f7f'},
{'color': '#bcbd22'},
{'color': '#17becf'}]
#1f77b4, #ff7f0e, #2ca02c, 以及所有其他颜色均为以十六进制形式表示的颜色。 为了弄清楚它们的外观,您可以使用 colorhexa 网站,为您提供有关它们的有用信息。
可以通过使用cycler函数并向其提供一串字符串名称来完成该黑客操作,这些字符串名称涉及您要依次使用的颜色:
In: mpl.rcParams['axes.prop_cycle'] = mpl.cycler('color',
['blue', 'red', 'green'])
此外,plot命令(如果未提供任何其他信息)将假定您要绘制一条线。 因此,它将链接曲线中所有提供的点。 如果添加新参数,例如'.' –即plt.plot(x,y_cos,'.') –,则表示您想绘制一系列分离的点(直线的字符串为'-',但是我们将很快显示另一个示例 )。
这样,如果您按照先前的建议自定义了rcParams['axes.prop_cycle'],则下一个图形将首先具有蓝色曲线,然后第二个图形将具有红色曲线,第三个图形将具有绿色曲线。 然后,色彩循环将重新开始。 我们将这个决定留给您。 本章中的所有示例都将遵循标准的颜色顺序,但是您可以随意尝试使用更好的颜色设置。
请注意,您还可以设置图形标题,并用标题pyplot中的xlabel和ylabel标记轴。
使用面板的更清晰表示
我们的第二个示例将向您演示如何创建多个图形面板并在每个图形面板上绘制表示形式。 我们还将尝试通过使用不同的颜色,大小和样式来个性化绘制的曲线。 这是示例:
In: import matplotlib.pyplot as plt
# defines 1 row 2 column panel, activates figure 1
plt.subplot(1,2,1)
plt.plot(x,y_cos,'r--')
# adds a title
plt.title('cos')
# defines 1 row 2 column panel, activates figure 2
plt.subplot(1,2,2)
plt.plot(x,y_sin,'b-')
plt.title('sin')
plt.show()
该图在两个不同的图形面板上显示余弦和正弦曲线:

subplot命令接受subplot(nrows, ncols, plot_number)参数形式。 因此,在实例化时,它会基于nrows和ncols参数以及plot_number区域(从左侧的1区域开始)的绘图数量为表示保留一定的空间。
您还可以将plot命令坐标与另一个字符串参数一起使用,这对于定义颜色和所表示曲线的类型很有用。 字符串通过组合您可以在以下链接上找到的代码来工作:
matplotlib.lines.Line2D.set_linestyle:将显示不同的线型。- 颜色 API:提供基本内置颜色的完整概述。 该页面还指出,您可以将
color参数与 HTML 名称或十六进制字符串一起用于颜色,也可以通过使用 RGB 元组来定义所需的颜色,其中元组的每个值都在[0,1]。 例如,有效参数为color = (0.1,0.9,0.9),它将创建由 10% 红色,90% 绿色和 90% 蓝色构成的颜色。 - 标记 API:列出您可以为点采用的所有可能的标记样式。
为数据关系绘制散点图
散点图将两个变量绘制为平面上的点,它们可以帮助您找出两个变量之间的关系。 如果要表示组和集群,它们也非常有效。 在我们的示例中,我们将创建三个数据集群,并在散点图中以不同的形状和颜色表示它们:
In: from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
D = make_blobs(n_samples=100, n_features=2,
centers=3, random_state=7)
groups = D[1]
coordinates = D[0]
由于必须绘制三个不同的组,因此必须使用三个不同的plot命令。 每个命令指定不同的颜色和形状('ys','m*','rD'字符串,其中第一个字母是颜色,第二个字母是标记)。 另请注意,每个绘图实例均由label参数标记,该参数用于为稍后在图例中报告的组分配名称:
In: plt.plot(coordinates[groups==0,0],
coordinates[groups==0,1],
'ys', label='group 0') # yellow square
plt.plot(coordinates[groups==1,0],
coordinates[groups==1,1],
'm*', label='group 1') # magenta stars
plt.plot(coordinates[groups==2,0],
coordinates[groups==2,1],
'rD', label='group 2') # red diamonds
plt.ylim(-2,10) # redefines the limits of y axis
plt.yticks([10,6,2,-2]) # redefines y axis ticks
plt.xticks([-15,-5,5,-15]) # redefines x axis ticks
plt.grid() # adds a grid
plt.annotate('Squares', (-12,2.5)) # prints text at coordinates
plt.annotate('Stars', (0,6))
plt.annotate('Diamonds', (10,3))
plt.legend(loc='lower left', numpoints= 1)
# places a legend of labelled items
plt.show()
结果图将是三个组的散点图,并附有它们各自的标签:

我们还添加了一个图例(pyplot.legend),固定了两个轴(pyplot.xlim和pyplot ylim)的限制,并精确地说明了必须放在它们上的刻度线(plt.xticks和plt.yticks) 通过指定值列表。 因此,网格(pyplot.grid)将图精确地划分为 9 个象限,使您可以更好地了解组的位置。 最后,我们打印了一些文本,指出了组名(pyplot.annotate)。
直方图
直方图可以有效地表示变量的分布。 在这里,我们将可视化两个正态分布,均以单位标准差为特征,一个均值为0,另一个均值为3.0:
In: import numpy as np
import matplotlib.pyplot as plt
x = np.random.normal(loc=0.0, scale=1.0, size=500)
z = np.random.normal(loc=3.0, scale=1.0, size=500)
plt.hist(np.column_stack((x,z)),
bins=20,
histtype='bar',
color = ['c','b'],
stacked=True)
plt.grid()
plt.show()
如果存在分类问题,则联合分布可以对数据提供不同的见解:

有几种方法可以个性化此类图并获得有关分析分布的进一步见解。 首先,通过更改箱子的数量,您将更改分布的离散方式(离散是将连续函数或一系列值转换为可减少的,可数的一组数字的过程)。 通常,10 到 20 个箱子可以很好地理解分布,尽管它实际上取决于数据集的大小以及分布。 例如,Freedman-Diaconis 规则规定,直方图中要有效地可视化数据的最佳箱子数量取决于箱子的宽度,可以使用四分位间距(IQR)和观察次数:

在计算了h(箱宽)后,通过将最大值和最小值之间的差除以h来计算箱数:
bins = (max-min) / h
通过将参数从histtype='bar'更改为histtype='step',我们还可以将可视化类型从条形更改为阶梯形。 通过将stacked布尔参数更改为False,曲线将不会重叠到重叠部分中的唯一条形中,但是您会清楚地看到每个单独的条形。
条形图
条形图对于比较不同类别的数量很有用。 它们可以水平或垂直排列以呈现平均估计和误差带。 它们可用于显示预测变量的各种统计数据以及它们与目标变量的关系。
在我们的示例中,我们将给出鸢尾花数据集的四个变量的平均值和标准差:
In: from sklearn.datasets import load_iris
import numpy as np
import matplotlib.pyplot as plt
iris = load_iris()
average = np.mean(iris.data, axis=0)
std = np.std(iris.data, axis=0)
range_ = range(np.shape(iris.data)[1])
在我们的表示中,我们将准备两个子图:一个带有水平条(plt.barh),另一个带有垂直条(plt.bar)。 标准误差由误差条表示,根据图形方向,我们可以将xerr参数用于水平条并将yerr参数用于垂直条:
In: plt.subplot(1,2,1) # defines 1 row, 2 columns panel, activates figure 1
plt.title('Horizontal bars')
plt.barh(range_,average, color="r",
xerr=std, alpha=0.4, align="center")
plt.yticks(range_, iris.feature_names)
plt.subplot(1,2,2) # defines 1 row 2 column panel, activates figure 2
plt.title('Vertical bars')
plt.bar(range_,average, color="b", yerr=std, alpha=0.4, align="center")
plt.xticks(range_, range_)
plt.show()
现在,水平和垂直条都在同一图中:

重要的是注意使用plt.xticks命令(纵坐标轴使用plt.yticks)。 第一个参数告知命令必须在轴上放置的刻度数,第二个参数说明必须放置在刻度上的标签。
注意的另一个有趣参数是alpha,它已用于设置条的透明度级别。 alpha参数是一个从 0.0(完全透明)到 1.0 的浮点数,它使颜色以不同的不透明度级别显示。
图像可视化
我们使用matplotlib探索的最后一个可视化与图像有关。 在处理图像数据时,求助于plt.imgshow很有用。 让我们以 Olivetti 数据集为例,该数据集是一个由 40 个人组成的开源图像集,这些人在不同时间提供了 10 张自己的图像(并且使用不同的表达式,这使得测试面部识别算法更具挑战性)。 来自该数据集的图像作为像素强度的特征向量提供。 因此,重要的是对向量进行整形,以使其类似于像素矩阵。 将插值设置为'nearest'有助于平滑图像:
In: from sklearn.datasets import fetch_olivetti_faces
import numpy as np
import matplotlib.pyplot as plt
dataset = fetch_olivetti_faces(shuffle=True, random_state=5)
photo = 1
for k in range(6):
plt.subplot(2, 3, k+1)
plt.imshow(dataset.data[k].reshape(64, 64),
cmap=plt.cm.gray,
interpolation='nearest')
plt.title('subject '+str(dataset.target[k]))
plt.axis('off')
plt.show()
将绘制完整的图像面板:

我们还可以可视化手写数字或字母。 在我们的示例中,我们将绘制 Scikit-learn 手写数字数据集中的前九个数字,并设置两个轴的范围(通过使用extent参数并提供最小值和最大值的列表)以将像素对齐网格:
In: from sklearn.datasets import load_digits
digits = load_digits()
for number in range(1,10):
fig = plt.subplot(3, 3, number)
fig.imshow(digits.images[number],
cmap='binary',
interpolation='none',
extent=[0,8,0,8])
fig.set_xticks(np.arange(0, 9, 1))
fig.set_yticks(np.arange(0, 9, 1))
fig.grid()
plt.show()
通过仅打印一张图像可以获得单个编号的简单特写:

In: plt.imshow(digits.images[0],
cmap='binary',
interpolation='none',
extent=[0,8,0,8]) # Extent defines the images max and min
# of the horizontal and vertical values plt.grid()
生成的图像清楚地突出显示像素如何构成图像及其灰度级:

精选的 Pandas 图形实例
使用适当设置的超参数,许多机器学习算法可以最佳地学习如何根据目标结果映射数据。 但是,通过解决数据中隐藏和细微的问题,可以进一步提高其预测表现。 这不仅仅是检测任何丢失或异常情况的问题。 有时,这取决于数据中是否存在任何组或异常分布(例如,多峰分布)。 明确起草的数据图可以阐明变量之间的关系,并且可以导致创建新的更好的特征,从而以更高的准确率预测目标变量。
刚刚描述的实践称为探索性数据分析(EDA),并且可以通过以下方式相应地获得有效的结果:
- 它应该很快,可以让您探索和发展新的想法,进行测试,然后以新的探索和新想法重新开始
- 它应基于图形表示,以便更好地整体描述数据,无论其维数多高
pandas``DataFrame提供了许多 EDA 工具,可以帮助您进行探索。 但是,首先必须将数据转换为DataFrame:
In: import pandas as pd
print ('Your pandas version is: %s' % pd.__version__)
from sklearn.datasets import load_iris iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
groups = list(iris.target)
iris_df['groups'] = pd.Series([iris.target_names[k] for k in groups]) Out: Your pandas version is: 0.23.1
请检查您的 Pandas 版本。 我们在pandas的 0.23.1 版本下测试了该书中的代码,它在以后的版本中也应适用。
以下段落中介绍的所有示例都将使用iris_df DataFrame。
pandas包实际上依赖于 Matplotlib 函数进行可视化。 它只是为其他复杂的绘图指令提供了一个方便的包装器。 这在速度和简便性方面具有优势,这是任何 EDA 过程的核心价值。 相反,如果您的目的是通过使用精美的可视化效果最好地传达发现,您可能会注意到自定义 Pandas 图形输出并不是那么容易。 因此,当最重要的是要创建特定的图形输出时,最好使用 Matplotlib 指令从头开始直接工作。
使用箱线图和直方图
分发应该始终是数据中要检查的第一个方面。 盒子图可以起草分布图中的关键数据,并帮助您发现异常值。 只需在DataFrame上使用boxplot方法即可获得快速概述:
In: boxplots = iris_df.boxplot(return_type='axes')
这是数据集的所有数字变量的箱形图:

如果您的数据中已经有分组(来自类别变量,或从无监督学习中得出),只需指出变量,您需要在箱线图中表示数据,并指定您需要按分组将其分开(使用by参数,然后是分组变量的字符串名称):
In: boxplots = iris_df.boxplot(column='sepal length (cm)',
by='groups',
return_type='axes')
运行代码后,将按组获取箱线图:

通过这种方式,您可以快速知道变量是否是组差异的良好区分器。 无论如何,箱形图无法以直方图和密度图的形式为您提供完整的分布图。 例如,通过使用直方图和密度图,您可以确定是否存在分布峰或谷:
In: densityplot = iris_df.plot(kind='density')
该代码显示数据集所有数字变量的分布:

In: single_distribution = iris_df['petal width (cm)'].plot(kind='hist',
alpha=0.5)
这是由直方图表示的结果分布:

您可以使用plot方法获得直方图和密度图。 该方法使您可以表示整个数据集,特定的变量组(您只需要提供字符串名称列表并进行一些华丽的索引编制),甚至单个变量即可。
绘制散点图
散点图可用于有效地了解变量是否处于非线性关系中,并且您可以对变量的最佳转换方式有所了解,以实现线性化。 如果您使用基于线性组合的算法,例如线性或逻辑回归,那么弄清楚如何使它们的关系更线性地呈现将有助于您获得更好的预测能力:
In: colors_palette = {0: 'red', 1: 'yellow', 2:'blue'}
colors = [colors_palette[c] for c in groups]
simple_scatterplot = iris_df.plot(kind='scatter', x=0, y=1, c=colors)
运行代码后,将出现一个精美绘制的散点图:

散点图可以变成六边形合并图。 此外,它们还可以帮助您有效地可视化点密度,这些点自然会更多地聚集在一起,从而揭示隐藏在数据中的群集。 为了获得这样的结果,可以使用数据集中最初存在的一些变量,或者使用 PCA 或其他降维算法获得的尺寸:
In: hexbin = iris_df.plot(kind='hexbin', x=0, y=1, gridsize=10)
这是生成的hexbin图:

gridsize参数指示图表将在一个网格中汇总多少个数据点。 较大的网格将创建大型网格单元,而较小的网格将创建小型网格单元。
散点图是双变量的。 因此,每个变量组合都需要一个图。 如果变量的数量不是很多(否则,可视化将变得混乱),一种快速的解决方案是使用pandas命令自动绘制散点图矩阵(使用核密度估计'kde'依次在图表的对角线上绘制每个特征的分布):
In: from pandas.plotting import scatter_matrix
colors_palette = {0: "red", 1: "green", 2: "blue"}
colors = [colors_palette[c] for c in groups]
matrix_of_scatterplots = scatter_matrix(iris_df,
alpha=0.2,
figsize=(6, 6),
color=colors,
diagonal='kde')
运行之前的代码后,您将获得完整的图形矩阵(对角线上的密度):

一些参数可以控制散点图矩阵的各个方面。 alpha参数控制透明度的量,figsize提供以英寸为单位的矩阵的宽度和高度。 最后,color接受一个列表,该列表指示图中每个点的颜色,从而允许描绘数据中的不同组。 另外,通过在diagonal参数上选择'kde'或'hist',您可以选择在散布矩阵对角线上代表每个变量的密度曲线或直方图。
通过平行坐标发现模式
散点图矩阵可以通知您有关特征的联合分布。 它可以帮助您找到数据中的组并验证它们是否可区分。 平行坐标是另一种绘图,有助于为您提供有关数据中存在的最多区分组变量的提示。
通过将所有观测值相对于所有可能的变量绘制为平行线(在横坐标上任意对齐),平行坐标将帮助您发现是否有观测流分组为您的类,并了解最能区分这些流的变量( 最有用的预测变量)。 自然地,为了使图表有意义,图中的特征应具有与鸢尾花数据集相同的比例尺(否则将其标准化):
In: from pandas.tools.plotting import parallel_coordinates
pll = parallel_coordinates(iris_df,'groups')
前面的代码将输出平行坐标:

parallel_coordinates是一个 Pandas 函数,为了正常工作,仅需要数据DataFrame和包含要测试其可分离性的组的变量的字符串名称作为参数。 因此,您应该在数据集中使用组变量。 但是,使用DataFrame.drop('variable name', axis=1, inplace=True)方法完成探索后,不要忘记将其删除。
总结 Matplotlib 的命令
正如我们在上一段中所看到的那样,Pandas 可以将可视化的数据浏览速度提高,因为它可以打包成单个命令,而使用 Matplotlib 则需要完整的代码片段。 其背后的想法是,除非需要定制和配置特殊的可视化效果,否则使用包装器可以使您更快地创建标准图形。
除 Pandas 外,其他包还将来自 Matplotlib 的低级指令组装为更用户友好的命令,以用于特定的表示形式和用法:
- Seaborn 是一个包,它通过为您提供一组统计图来扩展趋势图和区分组的功能,从而扩展了您的可视化功能
ggplot是流行的 R 库ggplot2的端口,它基于 Leland Wilkinson 的书《图形语法》中提出的可视化语法。 R 库是不断开发的,它提供了很多功能; Python 移植具有基础知识,并且其完整的开发工作仍在进行中。- MPLD3 利用 JavaScript 库进行图形处理 D3.js,以便轻松地将任何 Matplotlib 输出转换为 HTML 代码,可以使用浏览器和工具(如 Jupyter 笔记本; 或在互联网网站中。
- Bokeh 是一个交互式可视化包,它利用 JavaScript 和浏览器呈现的输出。 它是 D3.js 的理想替代品,因为您只需要 Python 即可利用 JavaScript 的功能以交互方式快速表示数据。
在接下来的页面中,我们将介绍 Seaborn,并提供一些构建块以利用它们在您的数据科学项目中的可视化效果。
Seaborn 介绍
Seaborn 由 Michael Waskom 创建并托管在 PyData 网站上,是一个将底层 Matplotlib 与整个 pyData 栈包装在一起的库,允许集成图表和来自 NumPy 和 Pandas 的数据结构,以及 SciPy 和 StatModels 的统计例程。 借助内置主题以及专为揭示数据模式而设计的调色板,所有这些工作都特别注重美学。
如果您当前尚未在系统上安装 Seaborn(例如,Anaconda 发行版默认提供它),则可以通过pip和conda轻松获得它(提醒您conda版本可能落后于直接从 PyPI(Python 包索引)获取的pip版本)。
$> pip install seaborn
$> conda install seaborn
在这些示例中,我们使用了 Seaborn 包的 0.9 版。
您可以通过以下方式上传包并将 Seaborn 样式设置为默认的 Matplotlib 样式:
In: import seaborn as sns
sns.set()
这足以将您所有基于 Matplotlib 的表示形式转换为更具视觉吸引力的图表:
In: x = np.linspace(0, 5, 50)
y_cos = np.cos(x)
y_sin = np.sin(x)
plt.figure()
plt.plot(x,y_cos)
plt.plot(x,y_sin)
plt.xlabel('x')
plt.ylabel('y')
plt.title('sin/cos functions')
plt.show()
结果如下:

您可以从任何以前看到的图表中获得有趣的结果,甚至可以使用 Pandas 中的图形方法生成的图表(毕竟,Pandas 还依赖于 Matplotlib 来创建其探索性图表)。
Seaborn 中有五个预设主题:
darkgridwhitegriddarkwhiteticks
默认值为darkgrid。 您可以通过使用set_style命令和首选主题的名称,然后运行plot命令轻松地尝试每个命令:
In: sns.set_style('whitegrid')
您所要做的只是确定哪个主题可以帮助您更好地传达图表上的信息。 您可以将样式限制为包含该样式的单个表示形式:
In: with sns.axes_style('whitegrid'):
# Your plot commands here pass
其他时髦的变化可能涉及到轴线,这是图表的边界。 使用despine命令,您可以轻松删除上下边框:
In: sns.despine()
此外,您可以使用left=True参数删除左边框,使用offset参数偏移轴,并对其进行修剪(使用trim=True)。 否则,仅凭 Matplotlib 命令就无法访问所有这些操作。
Seaborn 允许您使用的另一个有用控件是图表的比例。 某个特定的图表比例(涉及不同的线条粗细,字体大小等)称为上下文,可用的上下文是自说明纸,笔记本,谈话和海报,这是可能的选择。 例如,如果您的图表必须显示在 MS PowerPoint 演示文稿上,则只需在创建图形之前运行以下命令:
In: sns.set_context("talk")
让我们在最初的 sin/cos 图表上看到一些此类时尚效果的示例:
In: sns.set_context("talk")
with sns.axes_style('whitegrid'): plt.figure() plt.plot(x,y_cos) plt.plot(x,y_sin) plt.show() sns.set()
该代码将绘制以下图表:

同样,选择正确的颜色周期或设置可能有助于图形表示效果。 为此,Seaborn 提供了color_palette()命令,该命令不仅会告诉当前调色板的 RBG 值(如果不带参数运行); 它还将接受 Seaborn 提供的任何调色板的名称或任何 Matplotlib 颜色图。 它甚至接受您以任何 Matplotlib 格式提供的自定义颜色列表(RGB 元组,十六进制颜色代码或 HTML 颜色名称),以便创建自己的调色板:
In: current_palette = sns.color_palette()
print (current_palette) sns.palplot(current_palette)
运行代码后,您将在值和颜色上可视化当前调色板:

如上所述,有一些调色板可用。 首先,所有 Seaborn 调色板如下:
deepmutedbrightpasteldarkcolorblind
您还必须添加hls,husl和所有 Matplotlib 颜色图,可以通过在名称后添加_r来反转它们,或者通过添加_d使其更暗。
Matplotlib 颜色图的名称和示例都可以在以下网页上找到。
hls颜色空间是 RGB 值刻度的自动转换,由于颜色的强度不同(例如,黄色和绿色被感知为较亮,而蓝色被视为为较暗),因此它可能对您的表示形式有效或无效。 。
作为hsl的替代,您可以使用husl调色板,它对人眼更友好,如这个页面所述。
最后,您可以使用 Color Brewer 工具创建个性化的调色板,该工具可以在网上找到或在 Jupyter 笔记本中的应用中是所需的。 在笔记本单元中,使用choose_colorbrewer_palette命令将显示一个交互式工具。 为了使一切正常工作,必须将data_type作为参数指定,这是一个字符串,该字符串说明与要表示的数据相关的调色板的性质:
- 顺序:如果要表示连续性
- 发散:用于表示反差
- 定性:当您只想区分不同的类别时
让我们看看如何创建自定义顺序调色板并使用它:
In: your_palette = sns.choose_colorbrewer_palette('sequential')
完整的仪表板将出现:

设置颜色后,your_palette将变成 RGB 值列表:
In: print(your_palette) Out:[(0.91109573770971852, 0.90574395025477683, 0.94832756940056306),
(0.7764706015586853, 0.77908498048782349, 0.88235294818878174),
(0.61776242186041452, 0.60213766261643054, 0.78345253116944269),
(0.47320263584454858, 0.43267974257469177, 0.69934642314910889),
(0.35681661753093497, 0.20525952297098493, 0.58569783322951374)]
选择完毕后,您只需调用sns.set_palette(your_palette),并获得绘制所有图表时使用的颜色即可。
如果您只需要对具有某些特定颜色的图表进行操作,则可以使用with语句并在其下嵌套图表摘要即可,就像我们之前针对主题所看到的那样。 相反,如果您确实需要设置某个调色板,请使用set_palette。
调色板由六种颜色组成,可帮助您区分至少六个趋势或类别。 如果您需要进一步区分,只需使用hls调色板进行操作,并指出需要循环的颜色数量:
In: new_palette=sns.color_palette('hls', 10)
sns.palplot(new_palette)
这是生成的调色板:

最后,关闭主题和颜色部分,因为 Seaborn 是使用 Matplotlib 提供的功能的另一种更智能的方式,因此提醒您,可以使用来自 Matplotlib 本身的任何基本命令来进一步修改结果图表。 或者,它们可以通过 MPLD3 或 Bokeh 之类的包进一步转换为 JavaScript。
增强您的 EDA 功能
Seaborn 不仅使您的图表更加美观,而且在其方面也易于控制。 它还为您提供了用于 EDA 的新工具,可帮助您发现变量之间的分布和关系。
在继续之前,让我们重新加载包,并以 Pandas DataFrame格式准备好 Iris 和波士顿数据集:
In: import seaborn as sns
sns.set()
from sklearn.datasets import load_iris
iris = load_iris()
X_iris, y_iris = iris.data, iris.target
features_iris = [a[:-5].replace(' ','_') for a in iris.feature_names]
target_labels = {j: flower \
for j, flower in enumerate(iris.target_names)}
df_iris = pd.DataFrame(X_iris, columns=features_iris)
df_iris['target'] = [target_labels[y] for y in y_iris]
from sklearn.datasets import load_boston
boston = load_boston()
X_boston, y_boston = boston.data, boston.target
features_boston = np.array(['V'+'_'.join([str(b), a])
for a,b in zip(boston.feature_names,
range(len(boston.feature_names)))])
df_boston = pd.DataFrame(X_boston, columns=features_boston)
df_boston['target'] = y_boston
df_boston['target_level'] = pd.qcut(y_boston,3)
至于鸢尾花数据集,目标变量已转换为鸢尾花种类的描述性文本。 对于波士顿数据集,连续目标变量(自有住房的中位数)已分为三个相等的部分,分别代表较低,中位数和较高的价格(使用 Pandas 函数qcut)。
Seaborn 首先可以找出离散值或分类变量与数字变量之间的关系,从而帮助您进行数据探索。 这可以通过catplot函数实现:
In: with sns.axes_style('ticks'):
sns.catplot(data=df_boston, x='V8_RAD', y='target', kind='point')
您会发现探索类似的图很有见地,因为它们明确了目标水平及其变化:

在我们的示例中,在波士顿数据集中,对离散值的径向公路的可及性指标与目标进行了比较,以检查其关系的函数形式以及每个级别的相关方差。
而是在数字变量之间进行比较; Seaborn 提供了增强的散点图,并结合了回归拟合曲线趋势,当关系不是线性的时,可以提示您进行可能的数据转换:
In: with sns.axes_style("whitegrid"):
sns.regplot(data=df_boston, x='V12_LSTAT', y="target", order=3)
装配线将立即显示:

Seaborn 中的regplot可以可视化任何顺序的回归图(我们显示了二次多项式拟合)。 在可用的回归图中,如果检查的特征之一是二元的,则可以使用标准线性回归,稳健回归甚至逻辑回归。
在也需要考虑分布的地方,jointplot将在散点图的侧面提供其他图:
In: with sns.axes_style("whitegrid"):
sns.jointplot("V4_NOX", "V7_DIS",
data=df_boston, kind='reg',
order=3)
jointplot产生以下图表:

通过作用于kind参数来表示双变量关系的理想选择,jointplot也可以表示简单的散点图或密度(种类= scatter或种类= kde)。
当目的是要发现区分类别的内容时,FacetGrid可以以可比的方式排列不同的图,并帮助您了解哪里存在差异。 例如,我们可以检查鸢尾花种类的散点图,以确定它们是否占据了特征状态的不同部分:
In: with sns.axes_style("darkgrid"):
chart = sns.FacetGrid(df_iris, col="target_level")
chart.map(plt.scatter, "sepal_length", "petal_length")
该代码将很好地打印一个面板,该面板代表基于组的比较:

可以使用分布(sns.distplot)或回归斜率(sns.regplot)进行类似的比较:
In: with sns.axes_style("darkgrid"):
chart = sns.FacetGrid(df_iris, col="target") chart.map(sns.distplot, "sepal_length")
第一个比较基于分布:

随后的比较基于拟合线性回归线:
In: with sns.axes_style("darkgrid"):
chart = sns.FacetGrid(df_boston, col="target_level")chart.map(sns.regplot, "V4_NOX", "V7_DIS")
这是基于回归的比较:

至于评估跨类别的数据分布,Seaborn 提供了另一种工具,即提琴图。 提琴图只是一个箱形图,其框是根据密度估计来成形的,因此可以直观地传达信息:
In: with sns.axes_style("whitegrid"):
ax = sns.violinplot(x="target", y="sepal_length",
data=df_iris, palette="pastel")
sns.despine(offset=10, trim=True)
先前代码生成的提琴图可以为数据集提供有趣的见解:

最后,Seaborn 通过使用pairplot命令提供了一种更好的方法来创建散点图矩阵,并允许您定义组颜色(参数色相)以及如何填充对角线行。 通过使用diag_kind参数,它可以是直方图('hist')或核密度估计('kde'):
In: with sns.axes_style("whitegrid"):
chart = sns.pairplot(data=df_iris, hue="target", diag_kind="hist")
先前的代码将为数据集输出完整的散点图矩阵:

学习高级数据的表示
一些有用的表示可以从数据科学过程中得出。 也就是说,表示不是直接从数据中完成,而是通过使用机器学习过程来实现的,机器学习过程向我们介绍了算法的运行方式,并为我们提供了每个预测变量在获得的预测中的作用的更精确概述。 特别地,学习曲线可以提供快速诊断以改善模型。 这可以帮助您确定是需要更多观察还是需要丰富变量。
学习曲线
学习曲线是有用的诊断图形,它描述了机器学习算法(您的假设)相对于可用观察量的行为。 这样做的目的是比较使用不同样本内大小的交叉验证(通常为十倍)时训练表现(样本内案例的误差或准确率)如何表现。
就训练误差而言,您应该期望它在开始时会很高,然后再减少。 但是,根据假设的偏差和方差级别,您会注意到不同的行为:
- 高偏差假说倾向于从平均误差表现开始,在暴露于更复杂的数据时迅速降低,然后再保持相同的表现水平,无论您进一步添加多少种情况。
- 低偏的学习器在很多情况下倾向于更好地概括,但是他们近似复杂数据结构的能力有限,因此其表现有限。
- 高方差的假设往往会开始提高误差表现,然后随着您添加更多案例而逐渐降低。 由于它具有记录样本内特性的高容量,因此趋于缓慢降低。
至于交叉验证,我们可以注意到两种行为:
- 高偏见的假设往往始于低表现,但增长很快,直到达到与训练几乎相同的表现。 然后,它停止增长。
- 高方差的假设往往始于非常低的表现。 然后,随着更多案例的推广,它稳步但缓慢地改善了。 它几乎不读取样本中的演奏,并且它们之间始终存在差距。
能够估计您的机器学习解决方案表现为高偏差还是高方差假设,将立即帮助您决定如何改善数据科学项目。 尽管有了learning_curve类,但 Scikit-learn 使得计算可视化图形所需的所有统计信息更为简单,尽管正确地可视化它们还需要一些进一步的计算和命令:
In: import numpy as np
from sklearn.learning_curve import learning_curve, validation_curve
from sklearn.datasets import load_digits
from sklearn.linear_model import SGDClassifier
digits = load_digits()
X, y = digits.data, digits.target
hypothesis = SGDClassifier(loss='log', shuffle=True,
n_iter=5, penalty='l2',
alpha=0.0001, random_state=3)
train_size, train_scores, test_scores = learning_curve(hypothesis, X,
y, train_sizes=np.linspace(0.1,1.0,5), cv=10,
scoring='accuracy',
exploit_incremental_learning=False,
n_jobs=-1)
mean_train = np.mean(train_scores,axis=1)
upper_train = np.clip(mean_train + np.std(train_scores,axis=1),0,1)
lower_train = np.clip(mean_train - np.std(train_scores,axis=1),0,1)
mean_test = np.mean(test_scores,axis=1)
upper_test = np.clip(mean_test + np.std(test_scores,axis=1),0,1)
lower_test = np.clip(mean_test - np.std(test_scores,axis=1),0,1)
plt.plot(train_size,mean_train,'ro-', label='Training')
plt.fill_between(train_size, upper_train,
lower_train, alpha=0.1, color='r')
plt.plot(train_size,mean_test,'bo-', label='Cross-validation')
plt.fill_between(train_size, upper_test, lower_test,
alpha=0.1, color='b')
plt.grid()
plt.xlabel('sample size') # adds label to x axis
plt.ylabel('accuracy') # adds label to y axis
plt.legend(loc='lower right', numpoints= 1)
plt.show()
根据不同的样本量,您很快就会获得学习曲线图:

learning_curve类需要以下内容作为输入:
- 一系列存储在列表中的训练值
- 指示要使用的折数以及误差度量
- 您要测试的机器学习算法(参数估计器)
- 预测变量(参数
X)和目标结果(参数y)
结果,该类将产生三个数组。 第一个包含有效训练量,第二个显示在每次交叉验证迭代中获得的训练分数,最后一个显示交叉验证分数。
通过对训练和交叉验证应用均值和标准差,可以在曲线图中显示曲线趋势及其变化。 您还可以提供有关录制的演奏稳定性的信息。
验证曲线
由于学习曲线适用于不同的样本大小,因此验证曲线会根据超参数可以采用的值来估计训练和交叉验证表现。 与在学习曲线中一样,可以应用类似的注意事项,尽管这种特殊的可视化将使您对参数的优化行为有更深入的了解,并在视觉上向您建议应将搜索重点放在超参数空间的一部分:
In: from sklearn.learning_curve import validation_curve
testing_range = np.logspace(-5,2,8)
hypothesis = SGDClassifier(loss='log', shuffle=True,
n_iter=5, penalty='l2',
alpha=0.0001, random_state=3)
train_scores, test_scores = validation_curve(hypothesis, X, y,
param_name='alpha',
param_range=testing_range,
cv=10, scoring='accuracy', n_jobs=-1)
mean_train = np.mean(train_scores,axis=1)
upper_train = np.clip(mean_train + np.std(train_scores,axis=1),0,1)
lower_train = np.clip(mean_train - np.std(train_scores,axis=1),0,1)
mean_test = np.mean(test_scores,axis=1)
upper_test = np.clip(mean_test + np.std(test_scores,axis=1),0,1)
lower_test = np.clip(mean_test - np.std(test_scores,axis=1),0,1)
plt.semilogx(testing_range,mean_train,'ro-', label='Training')
plt.fill_between(testing_range, upper_train, lower_train,
alpha=0.1, color='r')
plt.fill_between(testing_range, upper_train, lower_train,
alpha=0.1, color='r')
plt.semilogx(testing_range,mean_test,'bo-', label='Cross-validation')
plt.fill_between(testing_range, upper_test, lower_test,
alpha=0.1, color='b')
plt.grid()
plt.xlabel('alpha parameter') # adds label to x axis
plt.ylabel('accuracy') # adds label to y axis
plt.ylim(0.8,1.0)
plt.legend(loc='lower left', numpoints= 1)
plt.show()
经过一些计算,您将获得参数验证曲线的表示形式:

validation_curve类的语法类似于先前看到的learning_curve的语法,但对于param_name和param_range参数,应分别为它们提供超参数和必须测试的范围。 至于结果,训练和测试结果以数组形式返回。
随机森林的特征重要性
正如在第 3 章,“数据管道”的结论中所讨论的那样,选择正确的变量可以通过减少噪声,估计方差和过多计算负担来改善学习过程 。 集成方法(例如,随机森林)可以为您提供与数据集中其他变量一起使用时变量所扮演角色的不同视图。
在这里,我们向您展示如何提取随机森林和极端树模型的重要性。 重要性的计算方式最初由 Breiman,Friedman 等人在《分类和回归树》一书中描述。 1984 年。这是一个真正的经典,为分类树奠定了坚实的基础。 在本书中,重要性用基尼重要性或平均减少不纯度来描述,这是由于在整个集成树上平均一个特定变量而导致的节点杂质的总减少量。 换句话说,平均减少不纯度是在该变量上分割的节点的总误差减少乘以路由到每个节点的样本数。 值得注意的是,根据这种重要性计算方法,减少误差不仅取决于误差度量-基尼或熵用于分类,而 MSE 用于回归,而且树顶的拆分也被认为更重要,因为它们涉及处理更多问题。 例子。
在几个步骤中,我们将学习如何获取此类信息并将其投影到清晰的可视化文件中:
In: from sklearn.datasets import load_boston
boston = load_boston()
X, y = boston.data, boston.target
feature_names = np.array([' '.join([str(b), a]) for a,b in
zip(boston.feature_names,range(
len(boston.feature_names)))])
from sklearn.ensemble import RandomForestRegressor
RF = RandomForestRegressor(n_estimators=100,
random_state=101).fit(X, y)
importance = np.mean([tree.feature_importances_ for tree in
RF.estimators_],axis=0)
std = np.std([tree.feature_importances_ for tree in
RF.estimators_],axis=0)
indices = np.argsort(importance)
range_ = range(len(importance))
plt.figure()
plt.title("Random Forest importance")
plt.barh(range_,importance[indices],
color="r", xerr=std[indices], alpha=0.4, align="center")
plt.yticks(range(len(importance)), feature_names[indices])
plt.ylim([-1, len(importance)])
plt.xlim([0.0, 0.65])
plt.show()
该代码将产生以下图表,突出显示该模型的重要特征:

对于每个估计量(在我们的示例中,我们有 100 个模型),该算法估计得分以对每个变量的重要性进行排名。 随机森林模型由决策树组成,决策树可以由许多分支组成,因为该算法试图获得非常小的终叶。 如果在随意排列其原始值之后,排列后的模型的结果预测与原始模型的预测相比在准确率方面有很大差异,则认为该变量之一很重要。
重要性向量在估计器的数量上平均,并且估计的标准差通过列表理解(重要性和std的赋值)进行计算。 现在,根据重要性得分(向量索引)进行排序,结果将投影到带有标准差提供的误差线的条形图上。
在我们的 LSTAT 分析中,该区域中处于较低地位的人口百分比和 RM(即每个住宅的平均房间数)被指出是我们随机森林模型中最具决定性的变量。
梯度提升树的部分依赖图
对特征重要性的估计是一条信息,可以帮助您根据最佳选择来确定要使用的特征。 有时,您可能需要更好地理解为什么变量对预测特定结果很重要。 通过控制分析中涉及的所有其他变量的效果,梯度提升树为您提供了变量与预测结果之间关系的清晰观点。 与使用非常有效的 EDA 可能获得的信息相比,此类信息可以为您提供更多因果关系动态的见解:
In: from sklearn.ensemble.partial_dependence import
plot_partial_dependence
from sklearn.ensemble import GradientBoostingRegressor
GBM = GradientBoostingRegressor(n_estimators=100,
random_state=101).fit(X, y)
features = [5,12,(5,12)]
fig, axis = plot_partial_dependence(GBM, X, features,
feature_names=feature_names)
作为输出,您将获得三个图,它们构成了 RM 和 LSTAT 特征的局部图:

在您提供分析计划后,plot_partial_dependence类将自动为您提供可视化效果。 您需要提供要单独绘制的特征的索引列表,以及要在热图上绘制的那些索引的元组(特征是轴,并且热值对应于结果) 。
在前面的示例中,平均房间数和较低身份人口的百分比均已表示,因此显示了预期的行为。 有趣的是,热图解释了它们如何共同为结果的值做出贡献,它揭示了它们没有以任何特定的方式相互作用(这是一次爬坡)。 但是,还显示出 LSTAT 大于 5 时,它是所得房屋价值的有力分隔符。
使用机器学习即服务创建预测服务器
很多时候,在您作为数据科学家的职业生涯中,您会发现自己需要与当前正在研究的代码分离的预测变量。 例如,如下:
- 您正在为手机开发应用,并且想要节省内存
- 您正在使用非 Python 编程语言(Java,Scala,C,C++ 等)进行编码,并且需要调用在 Python 中开发的预测变量
- 您正在处理大数据,并且模型在存储数据的同一远程位置进行了训练
在所有这些情况下,最好通过 HTTP 进行服务即预测的服务,或者一般来说,将任何机器学习即服务(ML-AAS)。
Bottle 是一个 Python 网络框架,是 HTTP 上微型应用的起点。 这是一个非常简单的 Python 库,提供了创建 Web 应用所需的基本对象和功能。 而且,它可以与 Python 中可用的所有其他库配对。 在进入即服务预测之前,让我们看看如何使用 Bottle 构建基本的Hello World程序。 请注意,以下清单作为脚本用于 Python REPL,而不用于 Jupyter 笔记本:
# File: bottle1.py from bottle import route, run, template port = 9099 @route('/personal/<name>') def homepage(name):
return template('Hi <b>{{name}}</b>!', name=name) print("Try going to http://localhost:{}/personal/Tom".format(port)) print("Try going to http://localhost:{}/personal/Carl".format(port)) run(host='localhost', port=port)
在执行代码之前,让我们逐行分析代码:
- 我们开始从 Bottle 模块中导入所需的函数和类。
- 然后,我们指定 HTTP 服务器将监听的端口。
- 在示例中,我们选择端口
9099; 您可以随意将其更改为另一个,但是首先请检查是否有其他服务正在使用它(请记住 HTTP 位于 TCP 之上)。 - 下一步是 API 端点的定义。 当对指定为参数的路径执行 HTTP 调用时,
route装饰器将应用其后定义的函数。 请注意,在路径中,它表示name,这是即将到来的函数的参数。 这意味着name是调用的参数; 您可以在 HTTP 调用中选择所需的任何字符串,然后您的选择将作为参数名称传递给函数。 - 然后,在函数主页中,返回了带有 HTML 代码的模板。 以一种更简单的方式,将其视为
template函数,该函数将创建您将从浏览器中看到的页面。
模板,是一个示例,它只是一个普通的 HTML 页面,但它可能更复杂(它实际上可以是一个模板页面,其中需要填充一些空白)。 模板的完整描述超出了本节的范围,因为我们仅将框架用于简单的纯输出。 如果您需要其他信息,请浏览 Bottle 帮助页面。
- 最后,在
print函数之后,有核心run函数。 这是一个阻止函数,它将在作为参数提供的主机和端口上设置 Web 服务器。 当您运行清单中的代码时,一旦执行了该函数,就可以打开浏览器并将其指向http://localhost:9099/personal/Carl,您将找到以下文本:Hi Carl!
当然,将 HTTP 调用中的名称从Carl更改为Tom或任何其他名称将导致不同的页面,其中包含调用中指定的名称。
请注意,在此虚拟示例中,我们仅定义了/personal/<name>路由。 除非代码中定义,否则任何其他调用都将导致Error 404。
要关闭它,我们需要在命令行中按Ctrl + C(记住run函数正在阻止)。
现在让我们创建一个更加面向数据科学的服务; 我们将创建一个 HTML 页面,该页面的表单要求隔垫的长度和宽度以及花瓣的长度和宽度,以对鸢尾花样本进行分类。 在此示例中,我们将使用鸢尾花数据集来训练 Scikit-learn 分类器。 然后,对于每个预测,我们只需在分类器上调用predict函数,将预测发送回去:
# File: bottle2.py from sklearn.datasets import load_iris from sklearn.linear_model import LogisticRegression from bottle import run, request, get, post import numpy as np port = 9099 @get('/predict') def predict():
return ''' <form action="/prediction" method="post"> Sepal length [cm]: <input name="sl" type="text" /><br/> Sepal width [cm]: <input name="sw" type="text" /><br/> Petal length [cm]: <input name="pl" type="text" /><br/> Petal width [cm]: <input name="pw" type="text" /><br/> <input value="Predict" type="submit" /> </form> ''' @post('/prediction') def do_prediction():
try: sample = [float(request.POST.get('sl')), float(request.POST.get('sw')), float(request.POST.get('pl')), float(request.POST.get('pw'))] pred = classifier.predict(np.matrix(sample))[0] return "<p>The predictor says it's a <b>{}</b></p>"\
.format(iris['target_names'][pred]) except: return "<p>Error, values should be all numbers</p>" iris = load_iris() classifier = LogisticRegression() classifier.fit(iris.data, iris.target)
print("Try going to http://localhost:{}/predict".format(port)) run(host='localhost', port=port) # Try insert the following values: # [ 5.1, 3.5, 1.4, 0.2] -> setosa # [ 7.0 3.2, 4.7, 1.4] -> versicolor # [ 6.3, 3.3, 6.0, 2.5] -> virginica
导入后,在这里我们使用get装饰器,指定仅对 HTTP GET 调用有效的路由。 装饰器以及后面的函数没有参数,因为所有特征都应插入predict函数中定义的 HTML 表单中。 表单提交后,将使用 HTTP POST传递到/prediction页面。
现在,我们需要为此调用创建一个路由,这就是我们在do_prediction函数中所做的。 它的装饰器是/prediction页面上的post(即与get相反;它仅定义POST路由)。 数据被解析并转换为double(默认参数为字符串),然后将特征向量馈送到classifier全局变量中以获得预测。 这是使用简单模板返回的。 对象请求包含传递给服务的所有参数,包括我们 POST 到路由的整个变量。 最后,似乎我们只需要定义全局变量分类器-即在鸢尾花数据集上训练的分类器-最后,我们可以调用run函数。
对于此虚拟示例,我们将逻辑回归器用作分类器,并在完整的鸢尾花数据集上进行训练,而所有参数均保留为默认值。 在实际情况下,这里您将尽可能优化分类器。
运行此代码后,如果一切正常,则可以将浏览器指向http://localhost:9099/predict,您会看到以下表格:

单击预测按钮后插入值(5.1,3.5,1.4和0.2),应将您重定向到http://localhost:9099/prediction,其中应显示The predictor says it's a setosa字符串。 另外,请注意,如果您在表单中插入无效的条目(例如,将其保留为空或插入字符串而不是数字),则会显示一个 HTML 页面,指出存在错误。
我们已经完成了本节的一半,并且已经看到了用 Bottle 创建 HTTP 端点是多么容易和快捷。 现在,让我们尝试创建一个可以在任何程序中调用的预测即服务。 我们将以get调用的形式提交特征向量,并且返回的预测将为 JSON 格式。 这是此解决方案的代码:
# File: bottle3.py from sklearn.datasets import load_iris from sklearn.linear_model import LogisticRegression from bottle import run, request, get, response import numpy as np import json port = 9099 @get('/prediction') def do_prediction():
pred = {} try: sample = [float(request.GET.get('sl')), float(request.GET.get('sw')), float(request.GET.get('pl')), float(request.GET.get('pw'))] pred['predicted_label'] = iris['target_names'] [classifier.predict(np.matrix(sample))[0]]
pred['status'] = "OK" except: pred['status'] = "ERROR" response.content_type = 'application/json' return json.dumps(pred) iris = load_iris() classifier = LogisticRegression() classifier.fit(iris.data, iris.target) print("Try going to http://localhost:{}/prediction\
sl=5.1&sw=3.5&pl=1.4&pw=0.2".format(port)) print("Try going to http://localhost:{}/prediction\
sl=A&sw=B&pl=C&pw=D".format(port)) run(host='localhost', port=port)
该解决方案非常简单明了。 不过,让我们逐步分析它。 函数的入口点由/prediction路径上的get装饰器定义。 在此,我们将访问GET值以提取预测(请注意,如果您的分类器需要许多特征,则最好在此处使用POST调用)。 与前面的示例完全相同,生成了预测; 最后,将该值与status键的OK值一起插入 Python 字典中。 如果此函数引发异常,将不会进行预测,但status键中将出现ERROR字符串。 然后,将输出应用格式设置为 JSON,然后将 Python 字典序列化为 JSON 字符串。
当它运行时,我们可以访问 URL localhost:9099/prediction,后跟特征值,然后我们将预测作为 JSON 取回。 请注意,由于它是 JSON,因此不需要浏览器来解释返回的 HTTP 响应。 因此,我们可以从不同的应用(wget,浏览器或curl)或任何编程语言(包括 Python 本身)调用终结点。 要使其正常运行,请启动它并将您的浏览器指向(或以任何方式请求 URL)http://localhost:9099/prediction?sl=5.1&sw=3.5&pl=1.4&pw=0.2。 您将取回有效的 JSON:{"predicted_label": "setosa", "status": "OK"}。 另外,如果参数解析出错,您将得到以下 JSON:{"status": "ERROR"}。 这是您的第一个 ML-AAS!
尽管简单快捷,但是 Bottle 还有许多其他功能需要探索。 但是,它不如其他框架完整。 如果您的应用需要一些非凡的功能,请查看 Flask 或 Django 模块。
总结
本章通过提供数据,机器学习过程和结果的基本和高级图形表示的示例,概述了基本数据科学。 我们探索了来自 Matplotlib 的pylab模块,该模块可让您最轻松,最快地访问该包的图形功能。 我们将 Pandas 用于 EDA,并测试了 Scikit-learn 提供的图形工具。 所有示例都像构建块一样,并且都易于定制,以便为您提供快速的可视化模板。
在下一章中,将向您介绍图,它们是与预测变量/目标平坦矩阵的有趣偏差。 现在,它们已成为数据科学中的热门话题。 期望深入研究非常复杂和复杂的网络。
六、社交网络分析
“社交网络分析”(通常称为 SNA)创建模型并研究以网络形式存在的一组社交实体的关系。 实体可以是人,计算机或网页,关系可以是喜欢,链接或友谊(即,实体之间的连接)。
在本章中,您将了解以下内容:
- 图,因为社交网络通常以这种形式表示
- 用于从图中获取见解的重要算法
- 如何加载,转储和采样大型图
图论概论
基本上,图是一种能够表示对象集合中的关系的数据结构。 在这种范式下,对象是图的节点,关系是图的链接(或边)。 如果链接具有方向(在概念上,它们就像城市的单向街道),则图是有向的。 否则,该图是无向的。 下表提供了众所周知的图的示例:
| 图示例 | 类型 | 节点 | 边 |
|---|---|---|---|
| 全球资讯网 | 导演 | 网页 | 链接 |
| 无向 | 人们 | 友谊 | |
| 推特 | 导演 | 人们 | 追随者 |
| IP 网络 | 无向 | 主持人 | 电线/连接 |
| 导航系统 | 导演 | 地点/地址 | 街道 |
| 维基百科 | 导演 | 页数 | 锚链接 |
| 科学文献 | 导演 | 文件 | 引文 |
| 马尔可夫链 | 导演 | 状态 | 排放概率 |
如在传统的关系数据库管理系统(RDBMS)(例如 MySQL 或 Postgres)中,所有上述示例都可以表示为节点之间的关系。 现在,我们将发现图数据结构的优点,并开始考虑以下 SQL 查询对于诸如 Facebook 之类的社交网络的复杂程度(考虑一种推荐系统,该系统可以帮助您找到您可能认识的人 ):
- 检查以下查询:
Find all people who are friends of my friends, but not my friends
- 在图上将前面的查询与下面的查询进行比较:
Get all friends connected to me having distance=2
- 现在,让我们看看如何使用 Python 创建图或社交网络。 在本章中将广泛使用的库称为
NetworkX。 它能够处理中小型图,并且功能齐全:
In: %matplotlib inline
import networkx as nx
import matplotlib.pyplot as plt
G = nx.Graph()
G.add_edge(1,2)
nx.draw_networkx(G)
plt.show()
下图是上述代码的可视化视图,显示了两个节点及其连接边缘:

该命令是不言自明的。 检查之前的代码,在包导入之后,我们将首先定义一个(NetworkX)图对象(默认情况下,它是无向的)。 然后,我们将在两个节点之间添加一条边(即一个连接)(由于节点尚未在图中,因此会自动创建它们)。 最后,我们将绘制图。 库自动生成图布局(节点的位置)。
使用.add_note()方法,将其他节点添加到图非常简单。 例如,如果要添加节点3和4,则可以简单地使用以下代码:
In: G.add_nodes_from([3, 4])
nx.draw_networkx(G)
plt.show()
现在,我们的图变得越来越复杂,如您从图中所看到的:

前面的代码将添加两个节点。 由于它们未链接到其他节点,因此将断开连接。 同样,要向图添加更多边,可以使用以下代码:
In: G.add_edge(3,4)
G.add_edges_from([(2, 3), (4, 1)]) nx.draw_networkx(G) plt.show()
通过使用前面的代码,我们已经完成了图中的节点的连接:

要获取图中的节点集合,只需使用.nodes()方法。 同样,.edges()给出边列表作为连接节点的列表:
In: G.nodes() Out: [1, 2, 3, 4]
In: G.edges() Out: [(1, 2), (1, 4), (2, 3), (3, 4)]
有几种表示和描述图的方法。 在以下部分中,我们将说明最受欢迎的。 第一种选择是使用邻接表。 它列出了每个节点的邻居; 即,list[0]包含以邻接表格式表示的邻接节点:
In: list(nx.generate_adjlist(G)) Out: ['1 2 4', '2 3', '3 4', '4']
按照这种格式,第一个数字始终是来源,后面的数字始终是目标,如以下 URL 所示。
为了使描述独立,可以将图表示为列表的字典。 由于其简洁,这是描述图的最流行(也是实用)方法。 在这里,节点的名称是字典键,其值是节点的邻接表:
In: nx.to_dict_of_lists(G) Out: {1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]}
另一方面,您可以将图描述为边的集合。 在输出中,每个元组的第三个元素是边的属性。 实际上,每个边缘都可以具有一个或多个属性(例如其权重,其基数等)。 由于我们创建了一个非常简单的图,因此在以下示例中,我们没有属性:
In: nx.to_edgelist(G) Out: [(1, 2, {}), (1, 4, {}), (2, 3, {}), (3, 4, {})]
最后,图可以描述为 NumPy 矩阵。 如果矩阵在(i, j)位置包含1,则表示i和j节点之间存在链接。 由于矩阵通常只包含很少的 1(与零的数量相比),因此通常表示为稀疏(SciPy)矩阵,NumPy 矩阵或 Pandas DataFrame。
请注意,矩阵描述是详尽无遗的。 因此,将无向图转换为有向图,将连接(i, j)的链接转换为两个链接,即(i, j)和(j, i)。 该表示形式通常被称为邻接矩阵或连接矩阵。
因此,将创建一个对称矩阵,如以下示例所示:
In: nx.to_numpy_matrix(G) Out: matrix([[ 0., 1., 0., 1.],
[ 1., 0., 1., 0.],
[ 0., 1., 0., 1.],
[ 1., 0., 1., 0.]]) In: print(nx.to_scipy_sparse_matrix(G)) Out: (0, 1) 1
(0, 3) 1
(1, 0) 1
(1, 2) 1
(2, 1) 1
(2, 3) 1
(3, 0) 1
(3, 2) 1 In: nx.convert_matrix.to_pandas_adjacency(G)
下表显示了结果输出:

当然,如果要加载 NetworkX 图,则可以使用相反的函数(将函数名称中的from更改为to),并且可以加载 NetworkX 列表,边列表以及 NumPy,SciPy 和pandas结构的字典中的图。
图中每个节点的重要度量是其度。 在无向图中,节点的度数表示该节点具有的链接数。 对于有向图,度有两种类型:入度和出度。 这些分别计算节点的入站和出站链接。
让我们添加一个节点(以使图不平衡)并计算节点的度,如下所示:
In: G.add_edge(1, 3)
nx.draw_networkx(G) plt.show()
图的结果图如下:

本章中的图可能与在本地计算机上获得的图有所不同,因为图布局的初始化是使用随机参数进行的。
节点的度显示如下:
In: G.degree() Out: {1: 3, 2: 2, 3: 3, 4: 2}
对于大型图,此度量是不切实际的,因为输出字典的每个节点都有一个项目。 在这种情况下,通常使用节点度的直方图来近似其分布。 在以下示例中,构建了一个具有 10,000 个节点且链接概率为 1% 的随机网络。 然后,提取节点度的直方图,如下所示:
In: k = nx.fast_gnp_random_graph(10000, 0.01).degree()
plt.hist(list(dict(k).values()))
前面代码的直方图如下:

图算法
为了从图中获得见解,已经开发了许多算法。 在本章中,我们将在NetworkX中使用一个众所周知的图,即Krackhardt Kite图。 它是一个包含 10 个节点的伪图,通常用于证明图算法。 大卫·克拉克哈特(David Krackhardt)是风筝形状的结构的创造者。 它由两个不同的区域组成。 在第一个区域(由节点 0 到 6 组成)中,节点相互链接; 在另一个区域(节点 7 至 9)中,它们以链状连接:
In: G = nx.krackhardt_kite_graph()
nx.draw_networkx(G) plt.show()
在以下图表中,您可以检查 Krackhardt Kite 的图结构:

让我们从连通性开始。 如果图的两个节点之间至少存在一条路径(即一系列节点),则将它们连接起来。
如果存在至少一条路径,则这两个节点之间的最短路径就是从源节点到目标节点必须经过(或遍历)的节点集合最短的路径。
请注意,在有向图中,必须遵循链接的指示。
在NetworkX中,检查两个节点之间是否存在路径,计算最短路径并获取其长度非常容易。 例如,要检查节点 1 和 9 之间的连通性和路径,可以使用以下代码:
In: print(nx.has_path(G, source=1, target=9))
print(nx.shortest_path(G, source=1, target=9)) print(nx.shortest_path_length(G, source=1, target=9)) Out: True
[1, 6, 7, 8, 9] 4
此函数仅给出从一个节点到另一节点的最短路径。 如果我们要查看从节点 1 到达节点 9 的所有路径,该怎么办? Jin Yen 提出的算法提供了此答案,并在 NetworkX 的shortest_simple_paths函数中实现。 此函数返回图中节点源和目标之间从最短到最长的所有路径的生成器:
In: print (list(nx.shortest_simple_paths(G, source=1, target=9))) Out: [[1, 6, 7, 8, 9], [1, 0, 5, 7, 8, 9], [1, 6, 5, 7, 8, 9],
[1, 3, 5, 7, 8, 9], [1, 4, 6, 7, 8, 9], [1, 3, 6, 7, 8, 9],
[1, 0, 2, 5, 7, 8, 9], [...]]
最后,NetworkX 提供的另一个便捷函数是all_pairs_shortest_path函数,它返回一个 Python 字典,其中包含网络中所有节点对之间的最短路径。 例如,要查看从节点5的最短路径,您只需要查看键5的内容:
In: paths = list(nx.all_pairs_shortest_path(G))
paths[5][1] Out: {0: [5, 0],
1: [5, 0, 1],
2: [5, 2],
3: [5, 3],
4: [5, 3, 4],
5: [5],
6: [5, 6],
7: [5, 7],
8: [5, 7, 8],
9: [5, 7, 8, 9]}
不出所料,5与所有其他节点之间的路径以5本身开始。 请注意,此结构也是字典,因此,为了获得节点a和b之间的最短路径,可以将其称为path[a][b]。 在大型网络上,请谨慎使用此功能。 实际上,在后台,它以O(N²)的计算复杂度来计算所有成对的最短路径。
节点中心性的类型
现在我们将开始讨论节点中心性,这大致代表了节点在网络内部的重要性。 它还给出了节点连接网络的程度的想法。 我们将在此处查看多种类型的中心性,包括中间性中心,度中心性,接近性中心性,谐波中心性和特征向量中心性。
- 中间性中心性:这种类型的中心性可让您大致了解节点所在的最短路径的数量。 具有高度中间性的节点是网络的核心组件,许多最短的路径都经过它们。 在下面的示例中,
NetworkX提供了一种直接的方法来计算所有节点之间的中间性:
In: nx.betweenness_centrality(G) Out: {0: 0.023148148148148143,
1: 0.023148148148148143,
2: 0.0,
3: 0.10185185185185183,
4: 0.0,
5: 0.23148148148148148,
6: 0.23148148148148148,
7: 0.38888888888888884,
8: 0.2222222222222222,
9: 0.0}
可以想象,最高的中间性是通过节点7实现的。 这似乎非常重要,因为它是连接元素8和9(这是它们到网络的网关)的唯一节点。 相反,诸如9,2和4之类的节点位于网络的极端边界上,并且它们不存在于网络的任何最短路径中。 因此,可以删除这些节点而不会影响网络的连接性。
- 度中心性:这种类型的中心性只是入射到节点上的顶点的百分比。 请注意,在有向图中,每个节点都有两个度中心:即度中心和度中心。 让我们看下面的例子:
In: nx.degree_centrality(G) Out: {0: 0.4444444444444444,
1: 0.4444444444444444,
2: 0.3333333333333333,
3: 0.6666666666666666,
4: 0.3333333333333333,
5: 0.5555555555555556,
6: 0.5555555555555556,
7: 0.3333333333333333,
8: 0.2222222222222222,
9: 0.1111111111111111}
不出所料,节点3具有最高的中心度,因为它是链接数最多的节点(已连接到其他六个节点)。 相反,节点9是度数最低的节点,因为它只有一个边缘。
- 紧密度中心性:要为每个节点计算此距离,请计算到所有其他节点的最短路径距离,将其平均后除以最大距离,然后取该值的倒数。 这导致得分在 0(平均距离较大)和 1(平均距离较低)之间。 在我们的示例中,对于节点
9,最短路径距离为[1, 2, 3, 3, 4, 4, 4, 5, 5]。 然后将平均值(3.44)除以 5(最大距离)并从 1 中减去,得出的紧密度中心得分为 0.31。 您可以使用以下代码来计算示例图中所有节点的接近中心性:
In: nx.closeness_centrality(G) Out: {0: 0.5294117647058824,
1: 0.5294117647058824,
2: 0.5,
3: 0.6,
4: 0.5,
5: 0.6428571428571429,
6: 0.6428571428571429,
7: 0.6,
8: 0.42857142857142855,
9: 0.3103448275862069}
具有高度接近中心性的节点是5,6和3。 实际上,它们是存在于网络中间的节点,平均而言,它们可以通过几跳到达所有其他节点。 最低分属于节点9。 实际上,它到达所有其他节点的平均距离相当高。
- 谐波中心性:此度量类似于接近中心点,但不是距离的倒数之和的倒数,而是距离的倒数的总和。 这样做强调了距离的极限。 让我们看看我们的网络中的谐波距离是什么样的:
In: nx.harmonic_centrality(G) Out: {0: 6.083333333333333,
1: 6.083333333333333,
2: 5.583333333333333,
3: 7.083333333333333,
4: 5.583333333333333,
5: 6.833333333333333,
6: 6.833333333333333,
7: 6.0,
8: 4.666666666666666,
9: 3.4166666666666665}
节点3是具有最高谐波中心度的节点,而5和6具有可比较但较低的值。 同样,这些节点位于网络的中心,平均而言,它们可以通过几跳到达所有其他节点。 相反,节点9具有最低的谐波中心度; 实际上,它平均距离所有其他节点最远。
- 特征向量中心性:如果图是有向的,则节点表示 Web 页面,而边缘表示页面链接。 稍作修改的版本称为 PageRank。 该指标由拉里·佩奇(Larry Page)发明,是 Google,必应(Bing)以及其他搜索引擎的核心排名算法。 从随机冲浪者的角度来看,它可以使每个节点衡量该节点的重要性。 其名称源于以下事实:如果将图视为马尔可夫链,则图表示与最大特征值相关的特征向量。 因此,从这个角度来看,这种概率测度表示访问节点概率的静态分布。 让我们看下面的例子:
In: nx.eigenvector_centrality(G) Out: {0: 0.35220918419838565,
1: 0.35220918419838565,
2: 0.28583482369644964,
3: 0.481020669200118,
4: 0.28583482369644964,
5: 0.3976909028137205,
6: 0.3976909028137205,
7: 0.19586101425312444,
8: 0.04807425308073236,
9: 0.011163556091491361}
在此示例中,根据特征向量中心度度量,节点3和9的得分最高和最低。 与度中心性相比,特征值中心性提供了关于冲浪者在网络中的静态分布的想法,因为它针对每个节点不仅考虑直接连接的邻居(如度中心性),而且还考虑网络的整个结构。 如果该图表示网页及其连接,则表示它们是访问次数最多/最少(可能)的页面。
作为结束语,我们将介绍聚类系数。 简而言之,就是节点的邻居的比例也就是彼此的邻居(即存在的可能的三元组或三角形的比例)。 较高的值表示较高的湿度。 之所以这样命名,是因为它代表节点趋于聚集在一起的程度。 让我们看下面的例子:
In: nx.clustering(G) Out: {0: 0.6666666666666666,
1: 0.6666666666666666,
2: 1.0,
3: 0.5333333333333333,
4: 1.0,
5: 0.5,
6: 0.5,
7: 0.3333333333333333,
8: 0.0,
9: 0.0}
在图的高度连接的部分中可以看到较高的值,而在连接最少的区域中可以看到较低的值。
分割网络
现在,让我们看一下将网络划分为节点的多个子网络的方式。 最常用的算法之一是 Louvain 方法,该方法专门用于精确检测大型图(具有一百万个节点)中的社区。 我们将首先介绍模块化措施。 这是对图结构的度量(它不是面向节点的),其形式上的数学定义非常长且复杂,并且超出了本书的范围(读者可以在这个页面)。 它直观地衡量将图划分为社区的质量,将实际的社区联系与随机的联系进行比较。 模块化得分在 -0.5 至 +1.0 之间; 值越高,划分就越好(组内连接密集,而组间连接稀疏)。
这是一个两步迭代的算法:首先是局部优化,然后是全局优化,然后是局部优化,依此类推:
- 第一步,该算法局部最大化小社区的模块化。
- 然后,它聚合同一社区的节点,并按层次构建一个图,其节点为社区。
- 该方法迭代地重复这两个步骤,直到达到最大的全局模块化评分。
为了在实际示例中窥视该算法,我们首先需要创建一个更大的图。 让我们考虑一个具有 100 个节点的随机网络:
- 在此示例中,我们将使用
powerlaw算法构建图,该算法试图维持近似平均聚类。 - 对于添加到图中的每个新节点,还将向其中添加
m个随机边缘,每个随机边缘都有p的概率来创建三角形。 - 源代码未包含在
NetworkX中,但在名为community的单独模块中。 以下示例显示了该算法的实现:
In: import community
# Module for community detection and clustering
G = nx.powerlaw_cluster_graph(100, 1, .4, seed=101)
partition = community.best_partition(G)
for i in set(partition.values()):
print("Community", i)
members = [nodes for nodes in partition.keys()
if partition[nodes] == i]
print(members)
values = [partition.get(node) for node in G.nodes()]
nx.draw(G, pos=nx.fruchterman_reingold_layout(G),
cmap = plt.get_cmap('jet'),
node_color = values,
node_size=150,
with_labels=False)
plt.show()
print ("Modularity score:", community.modularity(partition, G))
Out: Community 0
[0, 46, 50, 61, 73, 74, 75, 82, 86, 96]
Community 1
[1, 2, 9, 16, 20, 28, 29, 35, 57, 65, 78, 83, 89, 93]
[...] Modularity score: 0.7941026425874911
程序的第一个输出是图中检测到的社区列表(每个社区都是节点的集合)。 在这种情况下,该算法检测到八个组。 我们想强调的是,我们没有指定要查找的输出社区的数量,但是它是由算法自动确定的。 这是并非所有聚类算法都具有的理想功能(例如,K 均值需要聚类数作为参数)。
然后,我们打印了图,为每个社区分配了不同的颜色。 您可以看到边缘节点上的颜色非常均匀:

最后,该算法返回解决方案的模块化评分:0.79(这是一个很高的评分)。
关于图的简短介绍将要介绍的最后一个算法是coloring。 这是一种为节点分配标签的图方式,邻居(即具有链接的节点)必须具有不同的标签(或颜色)。 为了解释该算法为何如此重要,我们将使用一个实际示例。 电信网络由遍布地球的不同频率的天线组成。 将每个天线视为一个节点,并将频率视为该节点的标签。 如果天线的距离小于定义的距离(假设距离足够近,会引起干扰),则说明它们与边缘相连。 我们是否可以找到分配的最低频率数(以使公司必须支付的费用降至最低)并避免相邻天线之间的干扰(即通过为链接节点分配不同的频率)?
该解决方案由图着色算法给出。 从理论上讲,这类算法的解决方案是 NP 难解的,尽管有很多近似方法可以快速获得次优解决方案,但几乎找不到最佳解决方案。 NetworkX 实现贪婪方法来解决着色问题。 该函数返回的是一个字典,其中每个节点(字典中的键)包含颜色(字典中键的值)。 作为示例,让我们在示例图中查看颜色的分配,然后让我们看到颜色:
In: G = nx.krackhardt_kite_graph()
d = nx.coloring.greedy_color(G)
print(d)
nx.draw_networkx(G,
node_color=[d[n] for n in sorted(d.keys())])
plt.show() Out:{3: 0, 5: 1, 6: 2, 0: 2, 1: 1, 2: 3, 4: 3, 7: 0, 8: 1, 9: 0}
这是图的图形,对链接的节点使用了不同的颜色:

正如预期的那样,链接的节点具有不同的颜色。 似乎对于这种网络配置,需要四种颜色。 如果这代表一个电信网络,它将向我们显示需要四个频率来避免干扰。
图加载,转储和采样
除了NetworkX以外,还可以使用其他软件来生成和分析图和网络。 可用于分析的最佳开源多平台软件之一称为 Gephi。 这是一种视觉工具,不需要编程技能。 可在这个页面免费获得。
与机器学习数据集中一样,甚至图都具有用于存储,加载和交换的标准格式。 这样,您可以使用 NetworkX 创建图,将其转储到文件中,然后使用 Gephi 进行加载和分析。
最常用的格式之一是图建模语言(GML)。 现在,让我们看看如何将图转储到 GML 文件中:
In: dump_file_base = "dumped_graph"
# Be sure the dump_file file doesn't exist
def remove_file(filename):
import os
if os.path.exists(filename):
os.remove(filename)
G = nx.krackhardt_kite_graph()
# GML format write and read
GML_file = dump_file_base + '.gml'
remove_file(GML_file)
to_string = lambda x: str(x)
nx.write_gml(G, GML_file, stringizer=to_string)
to_int = lambda x: int(x)
G2 = nx.read_gml(GML_file, destringizer = to_int)
assert(G.edges() == G2.edges())
在前面的代码块中,我们执行了以下操作:
- 如果转储文件确实存在,我们将其删除。
- 然后,我们创建了一个图(风筝),然后,我们将其转储并加载。
- 最后,我们比较了原始结构和加载的结构,断言它们是相等的。
除了 GML,还有多种格式。 这些格式均具有不同的功能。 请注意,其中一些会删除与网络有关的信息(例如边缘/节点属性)。 与write_gml函数及其等效项read_gml相似,以下内容(名称不言而喻):
- 邻接表(
read_adjlist和write_adjlist) - 多行邻接表(
read_multiline_adjlist和write_multiline_adjlist) - 边缘列表(
read_edgelist和write_edgelist) - GEXF(
read_gexf和write_gexf) - 泡菜(
read_gpickle和write_gpickle) - GraphML(
read_graphml和write_graphml) - LEDA(
read_leda和parse_leda) - YAML(
read_yaml和write_yaml) - 帕杰克(
read_pajek和write_pajek) - GIS Shapefile(
read_shp和write_shp) - JSON(加载/加载和转储/转储,并提供 JSON 序列化)
本章的最后一个主题是采样。 为什么要采样图? 我们对图进行采样是因为处理大型图有时是不切实际的(请记住,在最佳情况下,处理时间与图的大小成正比)。 因此,最好对其进行采样,通过在小规模场景中工作来创建算法,然后在全面问题上对其进行测试。 有几种采样图的方法。 在这里,我们将介绍三种最常用的技术。
在第一种技术(称为节点采样)中,节点的有限子集及其链接形成了采样集。 在第二种技术(称为链接采样)中,链接的子集形成采样集。 这两种方法既简单又快速,但是它们可能会为网络创建不同的结构。 第三种方法称为雪球采样。 初始节点,它的所有邻居以及邻居的邻居(以这种方式扩展选择,直到我们达到最大遍历深度参数)都形成了采样集。 换句话说,选择就像滚雪球一样。
请注意,您还可以对遍历的链接进行二次采样。 换句话说,每个链接都有p的概率,必须在输出集中进行跟踪和选择。
最后一种采样方法不是NetworkX的一部分,但是您可以在snowball_sampling.py文件中找到相同的实现。
在此示例中,我们将从具有alberto ID 的人开始对LiveJournal网络进行二次采样,然后递归扩展两次(在第一个示例中)和三次(在第二个示例中)。 在后一种情况下,每个链接后面都有 20% 的概率,因此减少了检索时间。 这是一个说明相同的示例:
In: import snowball_sampling
import matplotlib.pyplot as plot
my_social_network = nx.Graph()
snowball_sampling.snowball_sampling(my_social_network, 2, 'alberto')
nx.draw(my_social_network)
ax = plot.gca()
ax.collections[0].set_edgecolor("#000000")
plt.show() Out: Reching depth 0
new nodes to investigate: ['alberto'] Reching depth 1 new nodes to investigate: ['mischa', 'nightraven', 'seraph76',
'adriannevandal', 'hermes3x3', 'clymore', 'cookita', 'deifiedsoul',
'msliebling', 'ph8th', 'melisssa', '______eric_', 'its_kerrie_duhh',
'eldebate']
这是采样代码的结果:

现在,我们将使用特定的采样率0.2:
In: my_sampled_social_network = nx.Graph()
snowball_sampling.snowball_sampling(my_sampled_social_network, 3,
'alberto', sampling_rate=0.2)
nx.draw(my_sampled_social_network)
ax = plot.gca()
ax.collections[0].set_edgecolor("#000000")
plt.show() Out: Reching depth 0
new nodes to investigate: ['alberto']
Reching depth 1 new nodes to investigate: ['mischa', 'nightraven', 'seraph76',
'adriannevandal', 'hermes3x3', 'clymore', 'cookita', 'deifiedsoul',
'msliebling', 'ph8th', 'melisssa', '______eric_', 'its_kerrie_duhh',
'eldebate']
Reching depth 2
new nodes to investigate: ['themouse', 'brynna', 'dizzydez', 'lutin',
'ropo', 'nuyoricanwiz', 'sophia_helix', 'lizlet', 'qowf', 'cazling',
'copygirl', 'cofax7', 'tarysande', 'pene', 'ptpatricia', 'dapohead',
'infinitemonkeys', 'noelleleithe', 'paulisper', 'kirasha', 'lenadances',
'corianderstem', 'loveanddarkness', ...]
结果图更详细:

总结
在本章中,我们学习了社交网络是什么,包括社交网络的创建和修改,表示以及社交网络及其节点的一些重要度量。 最后,我们讨论了大图的加载和保存以及处理方法。
在本章中,几乎所有基本数据科学算法都已介绍。 在第 4 章,“机器学习”中讨论了机器学习技术,并在此讨论了社交网络分析方法。 我们将在下一章“超越基础的深度学习”中最终讨论深度学习和神经网络的最先进,最前沿的技术。
七、超越基础的深度学习
在本章中,我们将介绍深度模型,并且将显示三个如何构建深度模型的示例。 更具体地说,在本章中,您将学习以下内容:
- 深度学习的基础
- 如何优化深度网络
- 速度/复杂性/准确率问题
- 如何将 CNN 用于图像分类
- 如何将预训练的网络用于分类和迁移学习
- 如何将 LSTM 用于序列操作
我们将使用 Keras 包,这是用于深度学习的高级 API,它将使用于深度学习的神经网络更加容易和易于理解,因为其特点是类似于乐高的方法(这里的积木是神经网络的组成元素)。
接近深度学习
深度学习是使用神经网络的经典机器学习方法的扩展:我们可以堆叠数百层,而不是构建几层网络(所谓的浅层网络) 创建一个精巧但功能更强大的学习器。 深度学习是当今最流行的人工智能(AI)方法之一,因为它非常有效并且有助于解决模式识别中的许多问题,例如对象或序列识别。 使用标准的机器学习工具似乎牢不可破。
神经网络的思想来自人类的中枢神经系统,其中将能够处理简单信息的多个节点(或神经元)连接在一起以创建能够处理复杂信息的网络。 实际上,神经网络之所以这么称呼是因为它们可以自动和自适应地学习模型的权重,并且在足够复杂的网络架构下,它们能够近似任何非线性函数。 在深度学习中,节点通常称为单元或神经元。
让我们看看如何构建深度架构及其组成部分。 我们将从一个由三层组成的分类问题的小型深度架构开始,如下图所示:

该网络具有以下特征:
- 它分为三层。 左边的一层称为输入层,右边的一层称为输出层,中间的一层称为隐藏层。 通常,在神经网络中,总是有一个输入层和一个输出层,以及零个或多个隐藏层(当隐藏层为零时,整个神经架构将有效地转化为逻辑回归系统)。
- 输入层由五个单元组成,这意味着每个观察向量由五个数值特征组成(即观察矩阵有五列)。 请注意,特征必须是数字并且在值的有限范围内(为实现更好的数值收敛,理想情况下范围为 0 到 +1,但 -1 到 +1 也可以)。 因此,必须对分类特征进行预处理才能使其成为数值。
- 输出层由三个单元组成,这意味着我们要在三个输出类之间进行区分(即执行三类分类)。 在发生回归问题的情况下,这一层应该只有一个单元。
- 隐藏层由八个单元组成。 请注意,对于在深层结构中应显示多少个隐藏层以及每个应具有多少个单元没有任何规则。 这些参数留给科学家使用,通常,它们需要进行一些优化和微调才能发挥最佳表现。
- 每个连接都有一个与之关联的权重。 在学习算法中对此进行了优化。
输入层的每个单元都连接到下一层的所有单元。 在同一层中的单元之间既没有连接,在彼此之间的距离大于 1 的两个层中的单元之间也没有连接。
在该示例中,信息流从输入到输出(最终通过隐藏层)传递; 在文献中,此网络称为前馈神经网络。
它如何产生最终预测? 让我们逐步了解它的工作方式:
- 从隐藏层的顶部单元开始,它在第一层的输出向量(即输入观察向量)与第一层和第一层的第一单元之间的连接权重向量之间执行点积。 隐藏层。
- 然后使用该单元的激活函数转换该值。
- 对隐藏层中的所有单元重复此操作。
- 最后,我们可以以相同的方式计算隐藏层和输出层之间的前馈传播值,从而产生网络的输出。
这个过程看起来非常简单,它由多个令人尴尬的并行任务组成。 解释的最后一个缺失点是激活函数:它是什么,为什么需要它? 激活函数有助于使二元决策更加可分离(它使决策边界成为非线性,从而有助于更好地分离示例),并且它是每个单元的属性(或属性); 理想情况下,每个单元应该具有不同的激活函数,尽管通常将它们按层分组。
典型的激活函数是 Sigmoid,双曲正切和softmax(用于分类问题)函数,尽管目前最流行的函数是整流线性单元(或 ReLU),其输出是 0 和输入之间的最大值(其中输入是上一层输出和连接权重之间的点积)。
激活函数(单元数量和隐藏层数量)是深层网络的参数,科学家应该对其进行优化以获得更好的表现。
训练具有多个层次的神经网络是一项艰巨的操作,因为要调整的参数非常多(有时数百万):权重。 向连接分配权重的最常见方法是使用类似的方法来梯度下降,这种方法称为反向传播,因为它会将误差从输出层传播回输入层,并根据误差的梯度成比例地更新每个权重。 指向网络。 最初,权重是随机分配的,但是经过几步,它们应该收敛到最佳值。
这是对深度学习和神经网络的简短介绍; 如果您发现该主题很有趣,并且希望对其进行深入研究,我们建议您从 Packt 观看以下视频系列,在其中可以找到更好的解释和一些不错的技巧来掌握学习过程:
现在让我们看一些实用的东西:如何用神经网络解决分类问题。 在此示例中,我们将使用 Keras。 第一个是用于低级原语的 Python 库,通常用于深度学习,并能够利用最新的 GPU 和数值加速来有效处理多维数组。 Keras 是用于神经网络的高级,快速且模块化的 Python 库,能够在不同的数值计算框架之上运行,例如 TensorFlow,Microsoft Cognitive Tool(以前称为 CNTK)或 Theano。
使用 CNN 的图像分类
现在让我们将深度神经网络应用于图像分类问题。 在这里,我们将尝试根据其图像预测交通标志。 对于此任务,我们将使用 CNN(卷积神经网络),该技术能够利用图像中附近像素之间的空间相关性,这是目前在解决此类问题时的深度学习水平。
数据集可在此处找到。 我们要感谢团队免费发布了数据集,并参考了涉及该数据集的出版物:
J. Stallkamp, M. Schlipsing, J. Salmen, and C. Igel. The German Traffic Sign Recognition Benchmark: A multi-class classification competition. In Proceedings of the IEEE International Joint Conference on Neural Networks, pages 1453–1460. 2011.
首先,下载数据集,然后将其解压缩。 数据集的文件名为GTSRB_Final_Training_Images.zip,解压缩后会发现一个名为GTSRB的新目录,其中包含与 Jupyter 笔记本相同目录中的所有图像。
下一步是导入 Keras 并检查后端是否配置正确。 在本章中,我们将使用 TensorFlow 后端,并且所有代码都在该后端上进行了测试。
后端选择是可逆的。 如果要从 TensorFlow 切换到另一个后端,请遵循此处的指南。 无论使用哪种后端,使用 Keras 编写的脚本都可以成功运行(尽管在计算时间和最小化错误方面的表现可能有所不同)。
要检查您的后端,请运行以下代码,并检查操作是否成功执行,并且结果输出与此处报告的结果匹配。
In: import keras
Out: Using TensorFlow backend.
现在是时候开始处理了,因此我们必须为任务定义一些静态参数。 其中有两种,主要是:我们要识别的不同信号的数量(即类别的数量)和图片的大小。 类别数量是 43; 也就是说,我们可以识别 43 种不同的交通标志。
第二个参数,图像大小,很重要,因为输入图像可以具有不同的大小和形状。 我们需要将它们调整为标准尺寸,以便对它们运行深层网络。 我们选择32x32像素作为标准像素:它足够小以识别信号,并且同时不需要太多内存(也就是说,每个灰度图像仅使用 1,024 字节或 1 KB)。 增加大小意味着增加保存数据集所需的内存,再加上深网的输入层和计算所需的时间。 在文献中,对于只有一个项目的图像,32x32是相当标准的选择。 因此,就我们而言,我们有充分的理由来决定该大小。
In: N_CLASSES = 43
RESIZED_IMAGE = (32, 32)
在这一点上,我们必须读取图像并调整其大小,以创建观察矩阵和标签数组。 为此,我们执行以下步骤:
-
导入处理所需的模块。 最重要的是 Scikit-learn(即 Sklearn),其中包含处理图像的功能。
-
我们一个接一个地读取图像。 标签包含在路径中。 例如,图像
GTSRB/Final_Training/Images/00000/00003_00024.ppm的标签为00000,即0; 并且图像GTSRB/Final_Training/Images/00025/00038_00005.ppm带有标签00025,即25。 标签存储为标记编码的数组,该数组是一个 43 单元长的数组,只有一个值为1的数组(所有其他均为0)。 -
图像以 PPM(可移植像素图)格式存储,这是一种将像素存储在图像中的无损方式。 Scikit 图像或只是 skimage 可以使用函数
imread读取该格式。 如果您的系统上尚未安装 Scikit 映像,只需在 shell 中键入以下内容:conda install scikit-image或pip install -U scikit-image。 返回的对象是 3D NumPy 数组。 -
然后将包含图像像素表示的 3D NumPy 数组(具有三个通道-红色,蓝色和绿色)转换为灰度。 在这里,我们首先转换为 LAB 颜色空间(请参见这个页面-该颜色空间比线性颜色更具感知性) 其他,这意味着颜色值的相同变化量应产生相同视觉重要性的影响),然后保留第一个通道(包含亮度)。 再次,此操作很容易使用 Skimage 完成。 结果,我们有一个包含图像像素的一维 NumPy 数组。
-
最终,再次使用 Skimage 函数将图像调整为
32x32像素格式。 -
最后,将所有图像压缩到一个 4 维矩阵中:第一维用于索引数据集中的图像; 第二个和第三个分别代表图像的高度和宽度; 最后一个维度是渠道。 因此,拥有 39,208 张图像,并且所有
32x32像素均为灰度,因此观察矩阵的形状为39,208, 32, 32, 1。 -
标签被压缩成二维矩阵。 第一维是图像的索引,第二维是类。 由于具有相同数量的图像和 43 种可能的类别,因此将对矩阵进行整形
39,208, 43。
下面显示了翻译成代码的所有七个步骤:
In: import matplotlib.pyplot as plt
import glob
from skimage.color import rgb2lab
from skimage.transform import resize
from collections import namedtuple
import numpy as np
np.random.seed(101)
%matplotlib inline
Dataset = namedtuple('Dataset', ['X', 'y'])
def to_tf_format(imgs):
return np.stack([img[:, :, np.newaxis] for img in imgs],
axis=0).astype(np.float32)
def read_dataset_ppm(rootpath, n_labels, resize_to):
images = []
labels = []
for c in range(n_labels):
full_path = rootpath + '/' + format(c, '05d') + '/'
for img_name in glob.glob(full_path + "*.ppm"):
img = plt.imread(img_name).astype(np.float32)
img = rgb2lab(img / 255.0)[:,:,0]
if resize_to:
img = resize(img, resize_to, mode='reflect',
anti_aliasing=True)
label = np.zeros((n_labels, ), dtype=np.float32)
label[c] = 1.0
images.append(img.astype(np.float32))
labels.append(label)
return Dataset(X = to_tf_format(images).astype(np.float32),
y = np.matrix(labels).astype(np.float32))
dataset = read_dataset_ppm('GTSRB/Final_Training/Images', N_CLASSES,
RESIZED_IMAGE)
print(dataset.X.shape)
print(dataset.y.shape)
Out: (39209, 32, 32, 1)
(39209, 43)
数据集由近 40,000 张图像组成; 在更改颜色和调整大小之后,让我们看看它们的第一个样子:
In: plt.imshow(dataset.X[0, :, :, :].reshape(RESIZED_IMAGE))
print("Label:", dataset.y[0, :])
Out: Label: [[1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0.]]
以下是绘制的样本图像:

即使图像的清晰度很低(32x32像素),我们也可以立即识别出代表哪个符号。 到目前为止,整形操作似乎使图像甚至对人类都无法理解。 再次注意,标签是一个 43 维向量。 由于此图像属于第一类(即类00000),因此仅标签的第一元素不为null。
不同类别的另一个元素如下所示。 这是数据集中的图像编号 1,000,其类别为2(实际上,这是一个不同的符号):

现在让我们将数据集分为训练和测试。 我们使用 Scikit-learn 随机分离并随机排列图像。 在此单元格中,我们选择数据集的 25% 作为测试集; 也就是说,几乎有 10,000 张图像,剩下的 29K+ 张图像用于训练深层网络:
In: from sklearn.model_selection import train_test_split
idx_train, idx_test = train_test_split(range(dataset.X.shape[0]),
test_size=0.25,
random_state=101)
X_train = dataset.X[idx_train, :, :, :]
X_test = dataset.X[idx_test, :, :, :]
y_train = dataset.y[idx_train, :]
y_test = dataset.y[idx_test, :]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)
Out: (29406, 32, 32, 1)
(29406, 43)
(9803, 32, 32, 1)
(9803, 43)
这是创建卷积深度网络的时刻。 我们从一个简单易懂的神经网络开始。 然后,我们将转到更复杂但更准确的地方。
用 Keras 创建深层网络非常容易:您必须依次定义所有层。 Keras 对象需要按名为Sequential的序列定义层。 在这里,我们将创建一个包含三层的深层网络:
- 输入层定义为卷积 2D 层(实际上是图像和内核之间的卷积运算),包含 32 个
3x3像素形状的过滤器,并带有 ReLU 类型的激活层。 - 平滑前一个输出层; 也就是说,将展开方形观察以创建一维数组。
- 激活
softmax的密集输出层,由 43 个单元组成,每个单元一个。
然后编译模型,最后将其拟合到训练数据。 在此操作期间,我们选择了以下内容:
- 优化器:SGD,最简单的一种
- 批量大小:每批 32 张图像
- 周期数:10
这是将生成我们刚刚描述的模型的代码:
In: from keras.models import Sequential
from keras.layers.core import Dense, Flatten
from keras.layers.convolutional import Conv2D
from keras.optimizers import SGD
from keras import backend as K
K.set_image_data_format('channels_last')
def cnn_model_1():
model = Sequential()
model.add(Conv2D(32, (3, 3),
padding='same',
input_shape=(RESIZED_IMAGE[0], RESIZED_IMAGE[1], 1),
activation='relu'))
model.add(Flatten())
model.add(Dense(N_CLASSES, activation='softmax'))
return model
cnn = cnn_model_1()
cnn.compile(loss='categorical_crossentropy',
optimizer=SGD(lr=0.001, decay=1e-6),
metrics=['accuracy'])
cnn.fit(X_train, y_train,
batch_size=32,
epochs=10,
validation_data=(X_test, y_test))
Out: Train on 29406 samples, validate on 9803 samples
Epoch 1/10
29406/29406 [==============================] - 11s 368us/step -
loss: 2.7496 - acc: 0.5947 - val_loss: 0.6643 - val_acc: 0.8533
Epoch 2/10
29406/29406 [==============================] - 10s 343us/step -
loss: 0.4838 - acc: 0.8937 - val_loss: 0.4456 - val_acc: 0.9001
[...]
Epoch 9/10
29406/29406 [==============================] - 10s 337us/step -
loss: 0.0739 - acc: 0.9876 - val_loss: 0.2306 - val_acc: 0.9553
Epoch 10/10
29406/29406 [==============================] - 10s 343us/step -
loss: 0.0617 - acc: 0.9897 - val_loss: 0.2208 - val_acc: 0.9574
训练集的最终准确率接近 99%,而测试集则接近 96%。 我们有点过拟合,但让我们在测试集中查看该模型的混淆矩阵和分类报告。 我们还将打印混淆矩阵的log2,以更好地识别错误分类。
为此,我们首先需要预测标签,然后应用argmax运算符选择最可能的类别:
In: from sklearn.metrics import classification_report, confusion_matrix
def test_and_plot(model, X, y):
y_pred = cnn.predict(X)
y_pred_softmax = np.argmax(y_pred, axis=1).astype(np.int32)
y_test_softmax = np.argmax(y, axis=1).astype(np.int32).A1
print(classification_report(y_test_softmax, y_pred_softmax))
cm = confusion_matrix(y_test_softmax, y_pred_softmax)
plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
plt.colorbar()
plt.tight_layout()
plt.show()
# And the log2 version, to emphasize the misclassifications
plt.imshow(np.log2(cm + 1), interpolation='nearest',
cmap=plt.get_cmap("tab20"))
plt.colorbar()
plt.tight_layout()
plt.show()
test_and_plot(cnn, X_test, y_test)
Out:
precision recall f1-score support
0 0.87 0.90 0.88 67
1 0.97 0.94 0.95 539
2 0.93 0.94 0.94 558
[........]
40 0.93 0.96 0.95 85
41 0.92 0.94 0.93 47
42 1.00 0.91 0.95 53
avg / total 0.96 0.96 0.96 9803
以下是诊断图,可为您提供有关模型表现的证据:

这是混淆矩阵的log2版本:

分类似乎已经很好了。 我们可以做得更好,避免过拟合吗? 是的,这是我们可以使用的:
- 丢弃层:这等效于正则化,并且可以防止过拟合。 基本上,在训练的每个步骤中,都会停用一部分单元,因此该层的输出不会过分依赖其中的一些单元。
- 批量规范化层:通过减去批次平均值并将其除以标准差,此 z 标准化层。 这对于更新数据很有用,并且在每个步骤都会放大/衰减信号。
- 最大池化:这是一个非线性变换,用于通过对内核下的每个区域应用最大过滤器来对输入进行下采样。 用于选择最大特征,该特征在同一类中的位置可能略有不同。
除此之外,总是有改变深层网络和训练属性的空间。 也就是说,优化程序(及其参数),批量大小和周期数。 在下一个单元格中,这里是具有以下几层的改进的深层网络:
- 卷积层,具有 32 个
3x3过滤器和 ReLU 激活 BatchNormalization层- 另一个卷积层,然后是
BatchNormalization层 - 丢弃层,被丢弃的可能性为 0.4
- 展开层
- 512 单元密集层,具有 ReLU 激活
BatchNormalization层- 丢弃层,被丢弃的可能性为 0.5
- 输出层; 如上例所示,这是一个具有 43 个单元的
softmax密集层
那将如何在我们的数据集上执行?
In: from keras.layers.core import Dropout
from keras.layers.pooling import MaxPooling2D
from keras.optimizers import Adam
from keras.layers import BatchNormalization
def cnn_model_2():
model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same',
input_shape=(RESIZED_IMAGE[0], RESIZED_IMAGE[1], 1),
activation='relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3),
padding='same',
input_shape=(RESIZED_IMAGE[0], RESIZED_IMAGE[1], 1),
activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.4))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Dense(N_CLASSES, activation='softmax'))
return model
cnn = cnn_model_2()
cnn.compile(loss='categorical_crossentropy',
optimizer=Adam(lr=0.001, decay=1e-6), metrics=['accuracy'])
cnn.fit(X_train, y_train,
batch_size=32,
epochs=10,
validation_data=(X_test, y_test))
Out: Train on 29406 samples, validate on 9803 samples
Epoch 1/10
29406/29406 [==============================] - 24s 832us/step -
loss: 0.7069 - acc: 0.8145 - val_loss: 0.1611 - val_acc: 0.9584
Epoch 2/10
29406/29406 [==============================] - 23s 771us/step -
loss: 0.1784 - acc: 0.9484 - val_loss: 0.1065 - val_acc: 0.9714
[...]
Epoch 10/10
29406/29406 [==============================] - 23s 770us/step -
loss: 0.0370 - acc: 0.9878 - val_loss: 0.0332 - val_acc: 0.9920
<keras.callbacks.History at 0x7fd7ac0f17b8>
训练集的准确率与测试集的准确率相似,均为约 99%; 也就是说,每 100 张图像中有 99 张用正确的标签分类! 该网络更长,需要更多的内存和计算能力,但不太容易过拟合并且表现更好。
现在,让我们看一下分类报告和混淆矩阵(完整版本和log2版本):
In: test_and_plot(cnn, X_test, y_test)
Out:
precision recall f1-score support
0 1.00 0.97 0.98 67
1 1.00 0.98 0.99 539
2 0.99 1.00 0.99 558
[..........]
38 1.00 1.00 1.00 540
39 1.00 1.00 1.00 60
40 1.00 1.00 1.00 85
41 0.98 0.96 0.97 47
42 1.00 1.00 1.00 53
avg / total 0.99 0.99 0.99 9803
这是结果的视觉表示:


显然,错误分类的数量已大大减少。 现在,让我们尝试通过更改参数来做更好的事情。
使用预训练的模型
如您在前面的示例中所见,增加网络的复杂性会增加时间和训练所需的内存。 有时,我们必须接受我们没有足够强大的机器来尝试所有组合的机器。 在那种情况下我们该怎么办? 基本上,我们可以做两件事:
- 简化网络; 也就是说,通过删除参数和变量
- 使用预训练的网络,该网络已经由功能足够强大的人进行过训练
在这两种情况下,我们都将在次优条件下工作,因为深度网络不会像我们曾经使用的那样强大。 更具体地说,在第一种情况下,网络的精度不是很高,因为我们的参数较少。 在第二种情况下,我们必须应对其他人的决策和训练。 尽管这不是一件容易的事,但预训练的模型也可以根据您的数据集进行微调。 在这种情况下,网络将不会随机初始化参数。 尽管这很有趣,但是此操作不在本书的讨论范围之内。
在本节中,我们将快速展示如何使用预训练的模型,这是一种常见的处理方法。 请记住,预训练模型可以在多种情况下使用:
- 特征增强,以将特征(在这种情况下为预测标签)以及观察向量添加到模型中
- 迁移学习,以将更多特征(来自一层或模型层的系数)以及观察向量添加到模型中
- 预测;也就是说,计算标签
现在让我们看看如何使用预训练的网络来实现我们的目的。
在 Keras 中,可以从此处获得各种预训练的模型。
首先下载一些图像进行测试。 在以下示例中,我们将使用 Caltech 提供的数据集,该数据集可在此处使用。
我们要感谢数据集的作者,并建议阅读他们的论文:L. Fei-Fei, R. Fergus and P. Perona. One-Shot learning of object categories. IEEE Trans. Pattern Recognition and Machine Intelligence。
它包含 101 个类别的几张图像,并以tar.gz格式显示。
现在,用一个新的笔记本,导入我们将要使用的模块。 在此示例中,我们将使用 InceptionV3 预训练网络,该网络能够很好地识别图像中的对象。 它是由 Google 开发的,其输出可与人眼媲美。
- 首先,我们导入建立网络,预处理输入并提取预测所需的特征:
In: from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input
from keras.applications.inception_v3 import decode_predictions
from keras.preprocessing import image
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Out: Using TensorFlow backend.
- 现在,让我们加载庞大的网络及其系数:
In: model = InceptionV3(weights='imagenet')
很简单,不是吗?
- 下一步(也是最后一步)是创建一个函数进行预测。 在这种情况下,我们将预测前三个标签:
In: def predict_top_3(model, img_path):
img = image.load_img(img_path, target_size=(299, 299))
plt.imshow(img)
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3)[0])
基本上,此函数会加载图像并将其调整为299x299像素(这是预训练网络 InceptionV3 的默认输入大小),并将图像转换为模型的正确格式。 之后,它会预测图像的所有标签并选择(并打印)前三个。
让我们通过使用预训练的模型并根据概率来询问前三个预测,看看它如何与示例图像配合使用:
In: predict_top_3(model, "101_ObjectCategories/umbrella/image_0001.jpg")
我们要预测的图像以及前三个预测的结果输出如下:

Out: Predicted: [('n04507155', 'umbrella', 0.88384396),
('n04254680', 'soccer_ball', 0.07257448),
('n03888257', 'parachute', 0.012849103)]
我们确认这是一个很好的结果; 第一个标签(得分为 88%)是一把雨伞,其次是足球和降落伞。 现在让我们测试一个肯定更困难的图像,该图像的标签未包含在 InceptionV3 训练集中:
In: predict_top_3(model, "101_ObjectCategories/bonsai/image_0001.jpg")
这是图像及其最重要的三个结果:

Out: Predicted: [('n02704792', 'amphibian', 0.20315942),
('n04389033', 'tank', 0.07383019),
('n04252077', 'snowmobile', 0.055828683)]
正如预期的那样,由于它不在其预定义的类别之内,因此网络无法识别第一个预测标签中的盆景。
实际上,可以通过所谓的迁移学习技术来训练预训练的模型甚至识别全新的类别。 这项技术不在本书的讨论范围之内,但是您可以从 Keras 的博客中的示例中了解它。
最后,让我们看看如何从中间层提取特征,如下所示:
- 第一步,让我们验证标签名称:
In: print([l.name for l in model.layers])
Out: ['input_1', 'conv2d_1', 'batch_normalization_1',
..........
'activation_94', 'mixed10', 'avg_pool', 'predictions']
- 我们可以选择任何一层; 我们将继续进行
softmax预测之前的预测。 让我们创建一个对象Model,其输出为avg_pool层:
In: from keras.models import Model
feat_model = Model(inputs=model.input,
outputs=model.get_layer('avg_pool').output)
def extract_features(feat_model, img_path):
img = image.load_img(img_path, target_size=(299, 299))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
return feat_model.predict(x)
- 最后,要提取图片的特征,让我们用图像调用上一个函数:
In: f = extract_features(feat_model,
"101_ObjectCategories/bonsai/image_0001.jpg")
print(f.shape)
print(f)
Out: (1, 2048)
[[0.12340261 0.0833823 0.7935947 ... 0.50869745 0.34015656]]
如您所见,avg_pool层包含2048单元,该函数的输出正好是 2,048D 数组。 现在,您可以将此数组连接到您选择的任何其他特征数组。
处理时间序列
本章的最后一个示例是关于时间序列的。 更具体地说,我们将看到如何处理文本,这是一个可变长度的单词序列。
一些数据科学算法使用“词袋”方法处理文本。 也就是说,他们不在乎单词的位置以及它们在文本中的放置方式,他们只是在乎它们的存在与否(以及频率)。 取而代之的是,专门设计一类特殊的深度网络来对顺序很重要的序列进行操作。
一些示例如下:
-
根据历史数据预测未来股价:在这种情况下,输入是数字序列,而输出是数字
-
预测市场是上升还是下降:在这种情况下,给定一系列数字,我们要预测一个类别(上升或下降)
-
将英语文本翻译为法语:在这种情况下,输入序列将转换为另一个序列
-
聊天机器人:在这种情况下,输入和输出都是序列(使用相同语言)
对于此示例,让我们做些简单的事情。 我们将尝试检测电影评论的情感。 在此特定示例中,输入数据是单词序列(和顺序计数!),输出是二进制标签(即情感为正或为负)。
让我们开始导入数据集。 幸运的是,Keras 已经包含了该数据集,并且已经进行了预索引。 也就是说,每个评论不是由单词组成,而是由字典索引组成。 另外,可以只选择最重要的单词,并使用此代码选择包含最重要的25000单词的字典:
In: from keras.datasets import imdb
((data_train, y_train),
(data_test, y_test)) = imdb.load_data(num_words=25000)
让我们看看data和shape里面是什么:
In: print(data_train.shape)
print(data_train[0])
print(len(data_train[0]))
Out: (25000,)
[1, 14, 22, 16, 43, 530, .......... 19, 178, 32]
218
首先,有25000条评论; 即观察。 其次,每条评论由 1 到 24,999 之间的数字序列组成; 1 表示序列的开始,而最后一个数字表示不在词典中的单词。 请注意,每个评论的大小都不同; 例如,第一个是218个字的长度。
现在是时候将所有序列修剪或填充到特定大小。 使用 Keras,这很容易做到,并且为了填充,添加了整数0:
In: from keras.preprocessing.sequence import pad_sequences
X_train = pad_sequences(data_train, maxlen=100) X_test = pad_sequences(data_test, maxlen=100)
现在,我们的训练矩阵为矩形。 修剪/填充操作之后的第一个元素变为:
In: print(X_train[0])
print(X_train[0].shape) Out: [1415, .......... 19, 178, 32]
(100,)
对于此观察,仅保留最后的100字。 总体而言,现在,所有观测值均具有100维。 现在,让我们创建一个时间深度模型来预测评论情感。
这里提出的模型分为三层:
- 嵌入层。 原始词典设置为 25,000 个单词,组成嵌入的单元数(即层的输出)为 256。
- LSTM 层。 LSTM 代表长短期记忆,它是最强大的序列深度模型之一。 得益于其深厚的架构,它能够从序列中相距较远的单词中提取信息(因此得名)。 在此示例中,像元数设置为
256(作为上一层输出尺寸),其中0.4的缺失用于正则化。 - 具有 Sigmoid 活化的密集层。 这就是我们需要的二分类器。
这是执行此操作的代码:
In: from keras.models import Sequential
from keras.layers import LSTM, Dense
from keras.layers.embeddings import Embedding
from keras.optimizers import Adam
model = Sequential()
model.add(Embedding(25000, 256, input_length=100))
model.add(LSTM(256, dropout=0.4, recurrent_dropout=0.4))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer=Adam(),
metrics=['accuracy'])
model.fit(X_train, y_train,
batch_size=64,
epochs=10,
validation_data=(X_test, y_test))
Out: Train on 25000 samples, validate on 25000 samples
Epoch 1/10
25000/25000 [==============================] - 139s 6ms/step -
loss:0.4923 - acc:0.7632 - val_loss:0.4246 - val_acc:0.8144
Epoch 2/10
25000/25000 [==============================] - 139s 6ms/step -
loss:0.3531 - acc:0.8525 - val_loss:0.4104 - val_acc: 0.8235
Epoch 3/10
25000/25000 [==============================] - 138s 6ms/step -
loss:0.2564 - acc:0.9000 - val_loss:0.3964 - val_acc: 0.8404
...
Epoch 10/10
25000/25000 [==============================] - 138s 6ms/step -
loss:0.0377 - acc:0.9878 - val_loss:0.8090 - val_acc:0.8230
这就是 25K 审查测试数据集的准确率。 这是可以接受的结果,因为我们通过这样一个简单的模型实现了 80% 以上的正确分类。 如果您想对其进行改进,可以尝试使架构更复杂,但是请始终记住,通过增加网络的复杂性,训练和预测结果所需的时间,以及内存占用量会增加。
总结
在本章中,我们了解了深度网络的基本知识和一些高级模型。 我们介绍了神经网络的工作原理以及浅层网络和深度学习之间的区别。 然后,我们学习了如何构建能够对交通标志图像进行分类的 CNN 深度网络。 我们还使用预训练的网络预测了图像的类别。 使用评论中发现的文字来检测电影评论的情感也是学习的一部分。
深度学习模型确实非常强大,但是要付出很多处理自由度和许多系数来训练的代价,这需要手头有大量数据。
在下一章中,我们将看到当数据量太大而无法由单台计算机处理和处理时,Spark 将如何提供帮助。
八、大数据和 Spark
世界上存储的数据量正以准指数的方式增长。 如今,对于数据科学家而言,每天必须处理几 TB 的数据已不再是不寻常的请求,并且要使事情变得更加复杂,这意味着必须处理来自许多不同的异构系统的数据。 此外,尽管您需要处理的数据量很大,但您的业务期望是在短时间内不断生成模型,因为您只是在处理玩具数据集。
在围绕数据科学要点的旅程的总结中,我们不能忽略数据科学中如此关键的必要性。 因此,我们将向您介绍一种处理大量数据,通过多台计算机进行扩展以获取数据,对其进行处理以及构建有效的机器学习算法的新方法。 在我们进行必要的介绍之后,处理大量数据并产生有效的机器学习模型就不会了。
在本章中,您将:
- 了解分布式框架,解释 Hadoop,MapReduce 和 Spark 技术
- 从 PySpark(Spark 的 Python API 接口)开始
- 试验弹性分布式数据集,这是一种处理大数据的新方法
- 在 Spark 中的分布式系统中定义和共享变量
- 使用 Spark 中的
DataFrame处理数据 - 在 Spark 中应用机器学习算法
在本章的最后,只要有适当的计算机集群,您将可以面对任何数据科学问题,而无论手头的数据规模如何。
从一台独立的机器到一堆节点
处理大数据不仅仅是大小问题; 这实际上是一个多方面的现象。 实际上,根据 3V 模型(体积,速度和变化),可以使用三个(正交)标准对在大数据上运行的系统进行分类:
- 要考虑的第一个标准是系统处理数据所达到的速度。 尽管几年前,速度还是用来表示系统处理批量的速度,但如今,速度表示系统是否可以提供流数据的实时输出。
- 第二个标准是体积; 也就是说,可以处理多少信息。 它可以用行数或特征数来表示,也可以仅用字节数表示。 在流数据中,卷指示到达系统的数据的吞吐量。
- 最后一个标准是种类; 即数据源的类型。 几年前,这种类型受到结构化数据集的限制,但如今,数据可以结构化(表,图像等),半结构化(JSON,XML 等)和非结构化(网页, 社交数据,等等)。 通常,大数据系统会尝试处理尽可能多的相关源并将各种源混合在一起。
除了这些标准,最近几年还出现了许多其他 V,它们试图解释大数据的其他特征。 其中一些如下:
- 准确率:提供数据中包含的异常,偏差和噪声的指示; 最终表明其准确率
- 波动率:指示可以将数据用于提取有意义的信息的时间
- 有效率:数据的正确性
- 价值:根据数据指示投资回报率
近年来,所有 V 都急剧增加。 现在,许多公司发现他们保留的数据具有可被货币化的巨大价值,并且他们希望从中提取信息。 技术挑战已经转向具有足够的存储和处理能力,以便能够快速,大规模并使用不同的输入数据流来提取有意义的见解。
当前的计算机,甚至是最新,最昂贵的计算机,其磁盘,内存和 CPU 的数量也有限。 每天处理太字节(PB)的信息并及时生成模型似乎非常困难。 此外,需要复制同时包含数据和处理软件的独立服务器。 否则,它可能成为系统的单点故障。
因此,大数据世界已经转移到了集群:它们由可变数量的并不十分昂贵的节点组成,并且位于高速互联网连接上。 通常,有些集群专用于存储数据(大硬盘,少 CPU 和少量内存),而另一些专用于处理数据(功能强大的 CPU,中等到大量内存,以及小硬盘)。 此外,如果正确设置了群集,则可以确保可靠性(没有单点故障)和高可用性。
理解为什么我们需要分布式框架
建立集群的最简单方法是将某些节点用作存储节点,而另一些节点用作处理节点。 这种配置似乎非常易于使用,因为我们不需要复杂的框架来处理这种情况。 实际上,许多小型集群正是以这种方式构建的:几个服务器处理数据(加上副本),另一个服务器处理数据。 尽管这可能是一个不错的解决方案,但是由于许多原因,它并不常用:
- 它仅适用于令人尴尬的并行算法。 如果算法要求在处理服务器之间共享一个公共的内存区域,则无法使用此方法。
- 如果一个或多个存储节点死亡,则不能保证数据是一致的。 (考虑一种情况,其中节点及其副本同时死亡,或者节点在尚未复制的写操作之后死亡。)
- 如果处理节点死亡,我们将无法跟踪其正在执行的进程,从而很难在另一个节点上恢复处理。
- 如果网络出现故障,则在恢复正常后很难预测这种情况。
崩溃事件(甚至不止一个)很可能发生,这是一个事实,要求必须事先考虑这种情况并进行适当处理,以确保对数据进行操作的连续性。 此外,当使用便宜的硬件或更大的群集时,几乎可以肯定至少有一个节点将发生故障。 到目前为止,绝大多数集群框架都使用名为分治的方法(拆分和征服):
- 有用于数据节点的专用模块和用于数据处理节点(也称为工作器)的其他一些专用模块。
- 数据跨数据节点复制,一个节点为主节点,确保写入和读取操作均成功。
- 处理步骤在工作节点之间划分。 它们不共享任何状态(除非存储在数据节点中),并且它们的主设备确保所有任务都正确且正确地执行。
Hadoop 生态系统
Apache Hadoop 是用于群集上的分布式存储和分布式处理的非常流行的软件框架。 它的优势在于价格(免费),灵活性(它是开源的,尽管它是用 Java 编写的,但可以被其他编程语言使用),可伸缩性(它可以处理由数千个节点组成的集群)和鲁棒性。 (它的灵感来自 Google 的已发表论文,并且自 2011 年以来一直存在),使其成为处理和处理大数据的事实上的标准。 此外,Apache 基金会的许多其他项目都扩展了其功能。
Hadoop 架构
从逻辑上讲,Hadoop 由两部分组成:分布式存储(HDFS)和分布式处理(YARN 和 MapReduce)。 尽管代码非常复杂,但整体架构相当容易理解。 客户端可以通过两个专用模块访问存储和处理。 然后,他们负责在所有工作节点之间分配作业,如下图所示:

所有 Hadoop 模块均作为服务(或实例)运行; 也就是说,物理或虚拟节点可以运行其中的许多节点。 通常,对于小型集群,所有节点都运行分布式计算和处理服务。 对于大型集群,最好将两个功能分开并专门化节点。
我们将详细看到两层提供的功能。
Hadoop 分布式文件系统
Hadoop 分布式文件系统(HDFS)是容错的分布式文件系统,旨在在低成本硬件上运行,并且能够处理非常大的数据集(数百 PB 到 EB 级) )。 尽管 HDFS 需要快速的网络连接来跨节点传输数据,但是延迟不能像传统文件系统中那样低(它可能在几秒钟的时间内)。 因此,HDFS 被设计用于批量和高吞吐量。 每个 HDFS 节点都包含文件系统数据的一部分; 在其他实例中也将复制相同的数据,这可确保高吞吐量访问和容错。
HDFS 的架构是主从结构。 如果主服务器(称为名称节点)发生故障,则表明有一个辅助节点/备份节点可以控制。 所有其他实例都是从属(数据节点); 如果其中一个发生故障,这并不是问题,因为 HDFS 就是在设计时考虑到这一点的,因此不会丢失任何数据(进行冗余复制),并且可以将操作迅速重新分配给尚存的节点。 数据节点包含数据块:HDFS 中保存的每个文件被分解成块(或块),每个文件通常为 64 MB,然后在一组数据节点中进行分发和复制。 名称节点仅存储分布式文件系统中文件的元数据; 它不存储任何实际数据,而只是存储有关如何访问其管理的多个数据节点中文件的正确指示。
请求读取文件的客户端必须首先联系名称节点,它将返回一个表,其中包含块及其位置的有序列表(如数据节点中所示)。 此时,客户端应分别与数据节点联系,下载所有块并重建文件(通过将块附加在一起)。
要写入文件,客户端应首先联系名称节点,后者将首先决定如何处理请求,然后更新其记录并使用文件每个块的写入位置的数据节点的有序列表回复客户端。 客户端现在将根据名称节点答复中的报告,将块联系并上传到数据节点。 命名空间查询(例如,列出目录内容,创建文件夹等)由名称节点通过访问其元数据信息完全处理。
此外,名称节点还负责正确处理数据节点故障(如果未接收到心跳数据包,则标记为死),并将其数据复制到其他节点。
尽管这些操作很长且难以实现,但是由于许多库和 HDFS Shell,它们对用户完全透明。 在 HDFS 上进行操作的方式与当前在文件系统上进行的操作非常相似,这是 Hadoop 的一大优势:隐藏复杂性并让用户简单地使用它。
MapReduce
MapReduce 是在 Hadoop 的最早版本中实现的编程模型。 这是一个非常简单的模型,旨在并行处理分布式群集上的大型数据集。 MapReduce 的核心由两个可编程功能组成:一个执行过滤的映射器和一个执行聚合的归约器,以及一个将对象从映射器移动到正确的归约器的 shuffler。 Google 于 2004 年在 MapReduce 上获得了专利,专利发表了几个月。
具体来说,这是 MapReduce 用于 Hadoop 实现的步骤:
- 数据分块器:从文件系统读取数据并将其拆分为块。 块是输入数据集的一部分,通常是固定大小的块(例如,从数据节点读取的 HDFS 块)或另一个更合适的拆分。 例如,如果我们要计算文本文件中的字符,单词和行数,则很好的拆分可以是一行文本。
- 映射器:从每个块中生成一系列键值对。 每个映射器实例将相同的映射函数应用于不同的数据块。 继续前面的示例,对于每一行,在此步骤中生成了三个键值对-一个键值对包含该行中的字符数(键可以简单地是字符串),其中一个包含单词个数(在这种情况下,键必须不同,所以假设是单词),其中一个包含行数,该行数始终为 1(在这种情况下,键可以是行)。
- 打乱器:从可用键的数量和可用的化简器的数量,混洗器将具有相同键的所有键-值对分配给同一异化器。 通常,此操作是计算键的哈希值,将其除以精简器的数量,然后使用余数来指出特定的精简器。 这应确保每个异径管有足够数量的钥匙。 该功能不是用户可编程的,而是由 MapReduce 框架提供的。
- 归约器:每个简化器都接收一组特定键的所有键值对,并且可以产生零个或多个聚合结果。 在该示例中,所有与单词键相关的值都到达简化器; 它的工作只是总结所有值。 其他键也一样,这将产生三个最终值:字符数,单词数和行数。 请注意,这些结果可能在不同的减速器上。
- 输出编写器:减速器的输出写在文件系统(或 HDFS)上。 在默认的 Hadoop 配置中,每个归约器都会写入一个文件(
part-r-00000是第一个归约器的输出,part-r-00001是第二个归约器的输出,依此类推)。 要在文件上显示完整的结果列表,应将所有结果连接起来。
在视觉上,可以简单地传达和理解此操作,如下所示:

映射步骤之后,每个映射器实例还可以运行一个可选步骤,即合并器。 基本上可以预期,如果可能的话,在映射器上执行减少步骤,通常用于减少要重新整理的信息量,从而加快了处理速度。 在前面的示例中,如果一个映射程序处理输入文件的多行,则在(可选)组合器步骤中,它可以预聚合结果,并输出较少数量的键值对。 例如,如果映射器在每个块中处理 100 行文本,那么当可以将信息汇总为三时,为什么输出 300 个键值对(字符数为 100,单词为 100,行为 100)? 这实际上是合并器的目标。
在 Hadoop 提供的 MapReduce 实现中,随机分配操作被分配,从而优化了通信成本,并且每个节点可以运行多个映射器和归约器,从而充分利用了节点上可用的硬件资源。 此外,Hadoop 基础架构还提供了冗余和容错功能,因为同一任务可以分配给多个工作器。
Apache Spark 介绍
Apache Spark 是 Hadoop 的演进,并在最近几年变得非常流行。 与 Hadoop 及其以 Java 和批量为重点的设计相比,Spark 能够以快速简便的方式生成迭代算法。 此外,它具有用于多种编程语言的非常丰富的 API 套件,并且本身支持许多不同类型的数据处理(机器学习,流传输,图形分析,SQL 等)。
Apache Spark 是一个集群框架,旨在用于大数据的快速通用处理。 速度的提高之一来自以下事实:与 Hadoop,MapReduce 和 HDFS 一样,每项工作之后的数据都保留在内存中,而不是存储在文件系统中(除非您愿意这样做)。 由于内存提供的延迟和带宽比物理磁盘更具表现,因此这种事情使迭代作业(例如群集 K 均值算法)越来越快。 因此,运行 Spark 的集群需要为每个节点配备大量 RAM。
尽管 Spark 是在 Scala(类似于 Java 一样在 JVM 上运行)中开发的,但它具有适用于多种编程语言的 API,包括 Java,Scala,Python 和 R。在本书中,我们将重点介绍 Python。
Spark 可以两种不同的方式运行:
- 独立模式:它在本地计算机上运行。 在这种情况下,最大并行化是本地计算机的内核数,并且可用内存量与本地计算机完全相同。
- 群集模式:它使用群集管理器(例如 YARN)在多个节点的群集上运行。 在这种情况下,最大并行化是组成集群的所有节点上的核心数,而内存量是每个节点的内存量之和。
PySpark
为了使用 Spark 功能(或包含 Spark 的 Python API 的 PySpark),我们需要实例化一个名为SparkContext的特殊对象。 它告诉 Spark 如何访问集群,并包含一些特定于应用的参数。 在虚拟机提供的 Jupyter 笔记本中,此变量已可用,并称为sc(启动 IPython 笔记本时的默认选项)。 让我们在下一部分中查看它包含的内容。
从 PySpark 开始
Spark 使用的数据模型名为弹性分布式数据集(RDD),它是可以并行处理的元素的分布式集合。 可以从现有集合(例如 Python 列表)或从外部数据集创建 RDD,这些数据以文件形式存储在本地计算机,HDFS 或其他来源上。
设置本地 Spark 实例
从头开始完整安装 Apache Spark 并非易事。 通常,这是在通常可以在云上访问的计算机群集上完成的,并且将其委派给技术专家(即数据工程师)。 这可能是一个限制,因为您可能无法访问测试本章将学到的内容的环境。
但是,为了测试本章的内容,实际上您不需要进行太复杂的安装。 通过使用 Docker,您可以在自己计算机上的 Linux 服务器上访问 Spark 的安装以及 Jupyter 笔记本和 PySpark。 不论是 Linux,macOS 还是基于 Windows 的计算机都没有关系。
实际上,这主要是因为 Docker。 Docker 允许操作系统级别的虚拟化,也称为容器化。 容器化意味着允许一台计算机运行多个隔离的文件系统实例,其中每个实例彼此之间只是简单地分开(尽管共享相同的硬件资源),就好像它们是单台计算机本身一样。 基本上,在 Docker 中运行的任何软件都包装在一个完整,稳定且预先定义的文件系统中,该文件系统完全独立于运行 Docker 的文件系统。 使用 Docker 容器意味着您的代码将按预期(如本章所述)完美运行。 命令执行的一致性是 Docker 是将解决方案投入生产的最佳方法的主要原因:您只需要将使用的容器移到服务器中,并创建一个 API 即可访问您的解决方案(我们之前在这个页面,“可视化,见解和结果”,我们在其中介绍了 Bottle 包装。
这是您需要采取的步骤:
- 首先,首先安装适合您系统的 Docker 软件。 您可以在这里找到所需的全部信息,具体取决于您所使用的操作系统:
| Windows |
| Linux |
| MacOS |
安装非常简单,但是在从中下载软件的同一页面上,您可以找到可能需要的任何其他信息。
- 完成安装后,我们可以使用可在这个页面找到的 Docker 映像。 它包含完整的 Spark 安装,可通过 Jupyter 笔记本访问,以及 Miniconda 安装以及最新版本的 Python 2 和 3。您可以在此处找到有关映像内容的更多信息。
- 此时,只需打开 Docker 接口即可; 在那里,将出现一个带有鲸鱼的 ASCII 文字和 IP 地址的外壳。 只要记下 IP 地址(在我们的例子中是
192.168.99.100)。 现在,在 shell 中运行以下命令:
$> docker run -d -p 8888:8888 --name spark jupyter/pyspark-notebook start-notebook.sh –NotebookApp.token=''
- 如果您更喜欢安全性而不是易用性,只需键入以下内容:
$> docker run -d -p 8888:8888 --name spark jupyter/pyspark-notebook start-notebook.sh –NotebookApp.token='mypassword'
用您选择的密码替换mypassword占位符。 请注意,Jupyter 笔记本电脑在启动时会要求输入该密码。
- 运行上述命令后,Docker 将开始下载
pyspark-notebook image(可能需要一段时间); 为它分配名称spark,将 Docker 映像上的8888端口复制到您计算机上的8888端口,然后执行start-notebook.sh脚本,并将笔记本密码设置为空(这样您就可以仅通过前面提到的 IP 地址和8888端口立即访问 Jupyter)。
此时,您需要做的唯一另一件事就是在浏览器中输入以下内容:
http://192.168.99.100:8888/
也就是说,在您的浏览器中输入 Docker 启动时给您的 IP 地址,冒号,然后是8888(即端口号)。 Jupyter 应该立即出现。
- 作为一个简单的测试,您可以立即打开一个新的笔记本并进行以下测试:
In: import pyspark
sc = pyspark.SparkContext('local[*]')
# do something to prove it works
rdd = sc.parallelize(range(1000))
rdd.takeSample(False, 5)
- 同样重要的是要注意,您有停止 Docker 机器的命令以及什至会破坏它的命令。 这个 shell 命令将停止它:
$> docker stop spark
为了在停止容器后销毁该容器,请使用以下命令(顺便说一下,您将丢失容器中的所有工作):
$> docker rm spark
如果您的容器尚未被销毁,则要使其在停止后再次运行,只需使用以下 shell 命令:
$> docker start spark
此外,您需要知道,在 Docker 机器上,您在/home/jovyan目录上进行操作,并且可以直接从 Docker shell 获取其内容列表:
$> docker exec -t -i spark ls /home/jovyan
您还可以执行任何其他 Linux bash 命令。
值得注意的是,您还可以在容器之间来回复制数据(否则,您的工作将只保存在机器的操作系统中)。 假设您必须将 Windows 桌面目录中的文件(file.txt)复制到 Docker 计算机:
$> docker cp c:/Users/Luca/Desktop/spark_stuff/file.txt spark:/home/jovyan/file.txt
同样,相反的情况也是可能的:
$> docker cp spark:/home/jovyan/test.ipynb c:/Users/Luca/Desktop/spark_stuff/test.ipynb
这就是全部。 仅需几个步骤,您就可以在本地运行的 Spark 环境中运行所有实验(显然,它仅使用一个节点,并且仅限于单个 CPU 的功能)。
试验弹性分布式数据集
现在,让我们创建一个包含 0 到 9 之间的整数的弹性分布式数据集。为此,我们可以使用SparkContext对象提供的parallelize方法:
In: numbers = range(10)
numbers_rdd = sc.parallelize(numbers)
numbers_rdd
Out: PythonRDD[2672] at RDD at PythonRDD.scala:49
如您所见,您不能简单地打印 RDD 内容,因为它会分为多个分区(并分布在群集中)。 分区的默认数量是 CPU 数量的两倍(因此,在提供的 VM 中为四个),但是可以使用parallelize方法的第二个参数手动设置。
要打印出 RDD 中包含的数据,应调用collect方法。 请注意,此操作在群集上运行时会收集节点上的所有数据。 因此,该节点需要有足够的内存来包含所有内容:
In: numbers_rdd.collect()
Out: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
要仅获得部分预览,请使用take方法,指示您希望看到多少个元素。 请注意,由于它是分布式数据集,因此不能保证元素的插入顺序与插入时的顺序相同:
In: numbers_rdd.take(3)
Out: [0, 1, 2]
要读取文本文件,我们可以使用SparkContext提供的textFile方法。
它允许读取 HDFS 文件和本地文件,并在换行符上拆分文本; 因此,RDD 的第一个元素是文本文件的第一行(使用第一种方法)。 请注意,如果您使用的是本地路径,则组成集群的所有节点都应通过相同的路径访问相同的文件。 为此,我们首先下载威廉·莎士比亚的完整剧集:
In: import urllib.request
url = "http://www.gutenberg.org/files/100/100-0.txt"
urllib.request.urlretrieve(url, "shakespeare_all.txt")
In: sc.textFile("file:////home//jovyan//shakespeare_all.txt").take(6)
Out: ['',
'Project Gutenberg\'s The Complete Works of William Shakespeare, by William',
'Shakespeare', '',
'This eBook is for the use of anyone anywhere in the United States and',
'most other parts of the world at no cost and with almost no restrictions']
要将 RDD 的内容保存到磁盘上,可以使用 RDD 提供的saveAsTextFile方法:
In: numbers_rdd.saveAsTextFile("file:////home//jovyan//numbers_1_10.txt")
RDD 仅支持两种类型的操作:
- 转换,将数据集转换为另一数据集。 转换的输入和输出都是 RDD。 因此,可以将多个转换链接在一起,从而达到一种编程的函数式风格。 而且,转换是懒惰的。 也就是说,他们不会立即计算出结果。
- 动作从 RDD 返回值,例如元素的总和和计数,或者仅收集所有元素。 动作是执行(惰性)转换链的触发器,因为需要输出。
典型的 Spark 程序是一系列转换,最后是一个动作。 默认情况下,RDD 上的所有转换都是在您每次执行操作时执行的(即,不保存每个转换器之后的中间状态)。 但是,只要您想cache转换后的元素的值,就可以使用persist方法(在 RDD 上)覆盖此行为。 persist方法允许内存和磁盘持久性。
在下面的示例中,我们将对 RDD 中包含的所有值求平方,然后对其求和; 该算法可以通过一个映射器(正方形元素),然后是一个约简器(对数组求和)来执行。 根据 Spark 的说法,map方法是一种转换器,因为它只是逐个元素地转换数据。 归约是一个动作,因为它可以从所有元素中共同创造出值。
让我们逐步解决此问题,以了解可以进行操作的多种方式。 首先,我们将使用一个函数来转换(映射)所有数据:首先定义一个函数,该函数返回输入参数的平方,然后将该函数传递给 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]
您还记得为什么我们需要调用collect来打印转换后的 RDD 中的值吗? 这是因为map方法不会付诸实践,而只会被懒惰地评估。 另一方面,reduce方法是一个动作; 因此,将reduce步骤添加到先前的 RDD 应该会输出一个值。 至于map,reduce将应具有两个自变量(左值和右值)的函数作为自变量,并应返回一个值。 在这种情况下,它可以是用def定义的verbose函数或lambda函数:
In: numbers_rdd.map(lambda x: x**2).reduce(lambda a,b: a+b)
Out: 285
为了使其更简单,我们可以使用sum动作来代替归约器:
In: numbers_rdd.map(lambda x: x**2).sum()
Out: 285
现在让我们前进一个步骤,并介绍键值对。 尽管 RDD 可以包含任何类型的对象(到目前为止,我们已经看到了整数和文本行),但是当元素是由两个元素组成的元组时,可以进行一些操作:键和值。
举个例子,让我们将 RDD 中的数字分组为偶数和偶数,然后分别计算两组的总和。 对于 MapReduce 模型,最好将每个数字与一个键(奇数或偶数)映射,然后对于每个键,使用求和运算来减少。
我们可以从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):这将返回一个 RDD,该 RDD 是通过将
的输出对输入 RDD 的每个元素进行平坦化而形成的。 当
输入上的每个值都可以映射到 0 个或更多输出元素时使用。
例如,要计算每个单词在文本中出现的次数,
我们应该将每个单词映射到一个键-值对(该单词将是键,而 1
是值) ,以这种方式为
文本的每个输入行生成多个键值元素。filter(function):这将返回由所有值组成的数据集,其中
函数返回true。sample(withReplacement, fraction, seed):这将引导 RDD,
允许您创建采样的 RDD(有或没有替换),其
长度是输入长度的一小部分。distinct():这将返回包含输入
RDD 的不同元素的 RDD。coalesce(numPartitions):这减少了
RDD 中的分区数。repartition(numPartitions):更改
RDD 中的分区数。 这种方法总是通过网络混洗所有数据。groupByKey():这将创建一个 RDD,其中对于每个键,其值是在输入数据集中具有该键的
值序列。reduceByKey(function):这将通过键汇总输入的 RDD,然后,
将reduce函数应用于每个组的值。sortByKey(ascending):按
升序或降序对 RDD 中的元素进行排序。union(otherRDD):这会将两个 RDD 合并在一起。intersection(otherRDD):这将返回一个仅由同时出现在输入和参数 RDD 中的
值组成的 RDD。join(otherRDD):这将返回一个数据集,在该数据集中,将键值输入
(在键上)连接到参数 RDD。
与 SQL 中的join函数类似,这些方法也可用: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):与cache相同,但是您可以指定
在何处存储 RDD 的元素(内存,磁盘或两者)。unpersist():撤消persist或cache操作
现在,让我们尝试使用 RDD 的示例,以计算一些文本统计信息并从大文本中提取最受欢迎的单词(莎士比亚的戏剧)。 使用 Spark 时,用于计算文本统计信息的算法应如下所示:
-
在 RDD 上读取输入文件并对其进行并行化。 可以使用
SparkContext提供的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("file:////home//jovyan//shakespeare_all.txt")
.flatMap(emit_feats)
.reduceByKey(lambda a,b: a+b)
.collectAsMap()))
Out: {'chars': 5535014, 'words': 959893, 'lines': 149689}
要确定文本中最受欢迎的单词,请按照下列步骤操作:
- 使用
textFile方法在 RDD 上读取并并行化输入文件。 - 对于每一行,提取所有单词。 对于此操作,我们可以使用
flatMap方法和正则表达式。 - 现在,文本中的每个单词(即 RDD 的每个元素)都映射到一个键值对:键是小写单词,值始终是
1。 这是一个映射操作。 - 通过
reduceByKey调用,我们计算每个单词(键)在文本(RDD)中出现的次数。 输出是键值对,其中键是一个单词,值是单词在文本中出现的次数。 - 我们翻转键和值并创建一个新的 RDD。 这是一个映射操作。
- 我们将 RDD 降序排列,然后提取(获取)第一个元素。 这是一项操作,可以通过
takeOrdered方法执行一次操作。
我们实际上可以进一步改进该解决方案,将第二步和第三步合并在一起(flatMap-为每个单词分配一个键-值对,其中键是小写单词,值是出现的次数),以及第五步和第六步(将 RDD 中的第一个元素按值排序,即该对中的第二个元素):
In: import re
WORD_RE = re.compile(r"[\w']+")
print((sc.textFile("file:////home//jovyan//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: [('the', 29998)]
跨集群节点共享变量
当我们在分布式环境中工作时,有时需要跨节点共享信息,以便所有节点都可以使用一致的变量进行操作。 Spark 通过提供两种变量来处理这种情况:只读和只写变量。 通过不再确保共享变量是可读写的,它也降低了一致性要求,从而使管理这种情况的辛苦工作落在了开发人员的肩膀上。 通常,由于 Spark 确实具有灵活性和自适应性,因此可以很快找到解决方案。
只读广播变量
广播变量是驱动程序节点共享的变量; 也就是说,该节点在我们的配置中运行 IPython 笔记本,并且所有节点都在集群中。 这是一个只读变量,因为该变量由一个节点广播,并且如果另一个节点对其进行更改,则永远不会回读。
现在,让我们在一个简单的示例中查看其工作方式:我们希望对仅包含性别信息作为字符串的数据集进行一次热编码。 虚拟数据集仅包含一个特征,该特征可以是男性M,女性F或未知U(如果信息丢失)。 具体来说,我们希望所有节点都使用定义的“单热编码”,如以下词典中列出:
In: one_hot_encoding = {"M": (1, 0, 0), "F": (0, 1, 0),
"U": (0, 0, 1)}
在我们的解决方案中,我们首先在映射函数内部广播 Python 字典(调用SparkContext提供的sc.broadcast方法); 使用其value属性,我们现在可以访问它。 完成此操作后,我们有了一个通用的map函数,该函数可以在任何一个热图字典上运行:
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())
广播变量保存在组成集群的所有节点的内存中; 因此,它们永远不会共享大量数据,这些数据会填满它们并使后续处理变得不可能。
要删除广播变量,请在广播变量上使用unpersist方法。 此操作将释放所有节点上该变量的内存:
In: bcast_map.unpersist()
只写累加器变量
Spark 集群中可以共享的其他变量是累加器。 累加器是只写变量,可以将它们加在一起,通常用于实现总和或计数器。 只有运行 IPython 笔记本的驱动程序节点可以读取其值; 其他所有节点都无法读取。 让我们通过一个示例看一下它是如何工作的:我们要处理一个文本文件,并了解在处理文本文件时有多少行是空的。 当然,我们可以通过两次扫描数据集(使用两个 Spark 作业)来做到这一点,第一个计数空行,第二个进行实际处理,但是这种解决方案不是很有效。 之后,您将执行处理文本文件并计算其行数所需的所有步骤。
所需步骤如下:
- 首先,我们从网上下载要处理的文本文件,由古腾堡计划提供的亚瑟·柯南·道尔爵士的《福尔摩斯历险记》:
In: import urllib.request
url = "http://gutenberg.pglaf.org/1/6/6/1661/1661.txt"
urllib.request.urlretrieve(url, "sherlock.tx")
-
然后,我们实例化一个累加器变量(初始值为
0),并在处理输入文件的每一行(带有映射)时为发现的每一个空行添加1。 同时,我们可以在每一行上进行一些处理; 例如,在下面的代码中,我们只为每一行返回1,就以此方式对文件中的所有行进行计数。 -
在处理的最后,我们将获得两条信息:第一条是行数,来自对变换后的 RDD 的
count()操作的结果,第二条是累加器的属性中包含的空行数。 请记住,在扫描数据集一次之后,这两个选项均可用:
In: accum = sc.accumulator(0)
def split_line(line):
if len(line) == 0:
accum.add(1)
return 1
filename = 'file:////home//jovyan//sherlock.txt'
tot_lines = (
sc.textFile(filename)
.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 13053 lines
And 2666 lines are empty
广播变量和累加器变量一起——示例
尽管广播和累加器变量很简单,并且变量非常有限(一个变量是只读变量,另一个变量是只写变量),但是可以将它们有效地用于创建非常复杂的操作。 例如,让我们尝试在分布式环境中的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,并应用在上一个单元格中创建的map函数。 由于返回值是一个列表,因此我们可以使用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.85333333333333339)]
如预期的那样,输出中仅包含真实分类器。 让我们看看哪些分类器产生了错误。 毫不奇怪,这里我们从前面的输出中发现了两个缺失的:
In: print("The errors are:", errAccum.value)
Out: The errors are: [('PCA', "'PCA' object has no attribute 'predict'"),
('MDS', "'MDS' object has no attribute 'predict'")]
最后,让我们清理广播数据集:
In: bcast_dataset.unpersist()
请记住,在此示例中,我们使用了一个可以广播的小型数据集。 在实际的大数据问题中,您需要从 HDFS 加载数据集并广播 HDFS 路径。
Spark 中的数据预处理
到目前为止,我们已经看到了如何从本地文件系统和 HDFS 加载文本数据。 文本文件可以包含非结构化数据(例如文本文档)或结构化数据(例如 CSV 文件)。 至于半结构化数据,就像包含 JSON 对象的文件一样,Spark 具有特殊的例程,可以将文件转换为DataFrame,类似于 R 和 Python 包 Pandas 中的DataFrame。DataFrame与 RDBMS 表非常相似,其中设置了模式。
CSV 文件和 Spark DataFrame
我们首先向您展示如何读取 CSV 文件并将其转换为 Spark DataFrame。 只需按照以下示例中的步骤操作:
- 为了导入兼容 CSV 的文件,我们首先需要通过从本地
SparkContext创建一个SQLContext对象来创建一个 SQL 上下文:
In: from pyspark.sql import SQLContext
sqlContext = SQLContext(sc)
- 对于我们的示例,我们创建了一个简单的 CSV 文件,该文件是一个具有六行三列的表,其中缺少某些属性(例如
user_id=0用户的性别属性):
In: data = """balance,gender,user_id
10.0,,0
1.0,M,1
-0.5,F,2
0.0,F,3
5.0,,4
3.0,M,5
"""
with open("users.csv", "w") as output:
output.write(data)
- 使用
sqlContext提供的read.format方法,我们已经使表格式正确,并且在变量中具有所有正确的列名称。 输出变量类型是 SparkDataFrame。 要在一个漂亮的格式化表中显示该变量,请使用其show方法:
In: df = sqlContext.read.format('com.databricks.spark.csv')\
.options(header='true', inferschema='true').load('users.csv')
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方法研究DataFrame的架构。 我们认识到,在读取 CSV 文件时,数据会推断出每种列的类型(在前面的示例中,user_id列包含长整数,gender列由字符串组成,并且余额是双浮点数) :
In: df.printSchema()
Out: root
|-- balance: double (nullable = true) |-- gender: string (nullable = true) |-- user_id: long (nullable = true)
- 就像 RDBMS 中的表格一样,我们可以对
DataFrame中的数据进行切片和切块,选择列,并按属性过滤数据。 在此示例中,我们要打印不丢失gender且余额严格大于0的用户的balance,gender和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())
- 我们还可以仅使用一次对
filter方法的调用:
In: df.filter('gender is not null and balance > 0').show()
处理丢失的数据
数据预处理的一个常见问题是如何处理丢失的数据。 SparkDataFrame与 Pandas DataFrame类似,提供了您可以对它们执行的多种操作。 例如,获得仅包含完整行的数据集的最简单选择是丢弃包含缺失信息的行。 为此,在 SparkDataFrame中,我们首先必须访问DataFrame的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()
另外,如果要为每列设置默认值而不是删除行数据,则可以使用fill方法,传递由列名组成的字典(作为字典键)和默认值(作为字典中键的值)以替换该列中丢失的数据。
例如,如果要确保将变量平衡(缺少的地方)设置为0并将变量性别(缺少的地方)设置为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|
+------+------------+
到目前为止,我们已经使用了DataFrame,但是,如您所见,DataFrame方法和 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|avg(balance)|
+------+------------+
| F| -0.25|
| M| 2.0|
+------+------------+
毫不奇怪,命令输出的表(以及users表本身)是 Spark DataFrame类型的:
In: type(sqlContext.table("users"))
Out: pyspark.sql.dataframe.DataFrame
DataFrame,表和 RDD 紧密相连,并且可以在DataFrame上使用 RDD 方法。 请记住,DataFrame的每一行都是 RDD 的元素。 让我们详细了解一下,首先收集整个表:
In: sqlContext.table("users").collect()
Out: [Row(balance=10.0, gender=None, user_id=0),
Row(balance=1.0, gender='M', user_id=1),
Row(balance=-0.5, gender='F', user_id=2),
Row(balance=0.0, gender='F', user_id=3),
Row(balance=5.0, gender=None, user_id=4),
Row(balance=3.0, gender='M', user_id=5)]
In: a_row = sqlContext.sql("SELECT * FROM users").first()
print(a_row)
Out: Row(balance=10.0, gender=None, user_id=0)
输出是Row对象的列表(它们看起来像 Python 的namedtuple)。 让我们深入研究。 Row包含多个属性,可以将它们作为属性或字典键来访问; 也就是说,要从第一行获取balance,我们可以选择以下两种方式:
In: print(a_row['balance'])
print(a_row.balance)
Out: 10.0
10.0
另外,可以使用Row的asDict方法将Row收集为 Python 字典。 结果包含属性名称作为键和属性值(作为字典值):
In: a_row.asDict()
Out: {'balance': 10.0, 'gender': None, 'user_id': 0}
将预处理的DataFrame或 RDD 写入磁盘
要将DataFrame或 RDD 写入磁盘,我们可以使用write方法。 我们可以选择多种格式; 在这种情况下,我们会将其另存为 CSV 文件在本地计算机上:
In: (df.na.drop().write
.save("file:////home//jovyan//complete_users.csv", format='csv'))
检查本地文件系统上的输出后,我们立即发现与我们期望的有所不同:此操作将创建多个文件(part-r-…)。 它们每个都包含一些序列化为 JSON 对象的行,将它们合并在一起将创建全面的输出。 随着 Spark 可以处理大型分布式文件,为此调整了write操作,并且每个节点都写入了完整 RDD 的一部分:
In: !ls -als ./complete_users.json
Out: total 20
4 drwxr-sr-x 2 jovyan users 4096 Jul 21 19:48 .
4 drwsrwsr-x 20 jovyan users 4096 Jul 21 19:48 ..
4 -rw-r--r-- 1 jovyan users 33 Jul 21 19:48
part-00000-bc9077c5-67de-46b2-9ab7-c1da67ffcadd-c000.csv
4 -rw-r--r-- 1 jovyan users 12 Jul 21 19:48
.part-00000-bc9077c5-67de46b2-9ab7-c1da67ffcadd-c000.csv.crc
0 -rw-r--r-- 1 jovyan users 0 Jul 21 19:48 _SUCCESS
4 -rw-r--r-- 1 jovyan users 8 Jul 21 19:48 ._SUCCESS.crc
为了读回它,我们不必创建一个独立的文件-在读取操作中即使多个都可以。 还可以在 SQL 查询的FROM子句中读取 CSV 文件。 现在让我们尝试在不创建中间DataFrame的情况下打印刚刚写入磁盘的 CSV:
In: sqlContext.sql("""SELECT * FROM
csv.`file:////home//jovyan//complete_users.csv`""").show()
Out: +----+---+---+
| _c0|_c1|_c2|
+----+---+---+
| 1.0| M| 1|
|-0.5| F| 2|
| 0.0| F| 3|
| 3.0| M| 5|
+----+---+---+
除 JSON 外,还有另一种在处理结构化大型数据集时非常流行的格式:Parquet 格式。 Parquet 是 Hadoop 生态系统中可用的列式存储格式。 它压缩和编码数据,并可以使用嵌套结构; 所有这些品质使其变得非常高效。 保存和加载与 CSV 非常相似,即使在这种情况下,此操作也会产生多个文件写入磁盘:
In: (df.na.drop().write
.save("file:////home//jovyan//complete_users.parquet",
format='parquet'))
使用 Spark DataFrame
到目前为止,我们已经描述了如何从 CSV 和 Parquet 文件加载DataFrame,但是没有描述如何从现有 RDD 创建它们。 为此,您只需为 RDD 中的每条记录创建一个Row对象,然后调用 SQL 上下文的createDataFrame方法。 最后,您可以将其注册为temp表,以充分利用 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"))
sqlContext.table("gender_maps").show()
Out: +-----------+------------+
|long_gender|short_gender|
+-----------+------------+
| Male| M|
| Female| F|
+-----------+------------+
这也是处理 CSV 文件的首选方式。 首先,使用sc.textFile读取文件; 然后,使用split方法,Row构造器和createDataFrame方法创建最终的DataFrame。
当您有多个内存中的DataFrame或可以从磁盘加载时,您可以加入并使用经典 RDBMS 中可用的所有操作。 在此示例中,我们可以将从 RDD 中创建的DataFrame与存储在 Parquet 文件中的users数据集结合在一起。 结果令人惊讶:
In: sqlContext.sql("""
SELECT balance, long_gender, user_id
FROM parquet.`file:////home//jovyan//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|
+-------+-----------+-------+
由于表是内存中的,所以最后要做的就是通过释放用于保留它们的内存来进行清理。 通过调用sqlContext提供的tableNames方法,我们可以获得当前内存中所有表的列表。 然后,为了释放它们,我们可以使用dropTempTable作为表名作为参数。 除此之外,对这些表的任何进一步引用都将返回错误:
In: sqlContext.tableNames()
Out: ['gender_maps', 'users']
In: for table in sqlContext.tableNames():
sqlContext.dropTempTable(table)
从 Spark 1.3 开始,在执行数据科学操作时,DataFrame已成为对数据集进行操作的首选方式。
Spark 机器学习
在本章的这一点上,我们完成了您的主要任务:创建一个模型来预测数据集中缺少的一个或多个属性。 对于此任务,我们可以使用一些机器学习建模,而 Spark 可以在这种情况下为我们提供帮助。
MLlib 是 Spark 机器学习库; 尽管它是用 Scala 和 Java 构建的,但是它的功能也可以在 Python 中使用。 它包含分类,回归,推荐算法,一些用于降维和特征选择的例程,并且具有许多用于文本处理的功能。 它们所有人都能够应对庞大的数据集,并利用集群中所有节点的力量来实现其目标。
到目前为止,它由两个主要包组成:MLlib(可在 RDD 上运行)和 ML(可在DataFrame上运行)。 由于后者表现良好,并且是表示数据科学中数据的最流行方式,因此开发人员选择贡献和改进 ML 分支,让 ML 分支保留下来,但没有进一步的发展。 MLlib 乍看之下似乎是一个完整的库,但是,在开始使用 Spark 之后,您会注意到默认包中既没有统计库,也没有数值库。 在这里,SciPy 和 NumPy 会为您提供帮助,它们再次对数据科学至关重要。
在本节中,我们将尝试探索pyspark.ml包的功能; 截至目前,与最新的 Scikit-learn 库相比,它仍处于早期阶段,但它肯定在未来具有很大的潜力。
Spark 是一款高级,分布式且复杂的软件,应仅在大数据上以及具有多个节点的集群使用; 实际上,如果数据集可以容纳在内存中,那么使用其他库(例如 Scikit-learn 或类似库)就更方便了,这些库仅关注问题的数据科学方面。 在小型数据集的单个节点上运行 Spark 的速度可能比 Scikit-learn 等价算法慢五倍。
KDD99 数据集上的 Spark
让我们使用真实数据集:KDD99 数据集进行此探索。 竞赛的目的是创建一个网络入侵检测系统,该系统能够识别哪个网络流是恶意的,哪些不是。 而且,数据集中有许多不同的攻击。 目标是使用数据集中包含的数据包流的特征准确预测它们。
作为数据集的补充说明,它在发布后的最初几年中为开发入侵检测系统(IDS)的强大解决方案非常有用。 如今,作为其结果,数据集中包含的所有攻击都非常易于检测,因此不再用于 IDS 开发。 功能包括协议(tcp,icmp和udp),服务(http,smtp等),数据包的大小,协议中有效的标志,尝试访问的次数。 成为根,等等。
有关 KDD99 挑战和数据集的更多信息,请访问这个页面。
尽管这是一个经典的多类分类问题,但我们将对其进行深入研究,以向您展示如何在 Spark 中执行此任务。
读取数据集
首先,让我们下载并解压缩数据集。 我们将非常保守,只使用原始训练数据集的 10%(75 MB,未压缩),因为我们所有的分析都是在小型虚拟机上运行的。 如果想尝试一下,可以取消注释以下代码段中的行,并下载完整的训练数据集(未压缩的 750 MB)。 我们使用 bash 命令下载训练数据集,测试(47 MB)和功能名称:
In: !mkdir datasets
!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.
要创建具有命名字段的DataFrame,我们应该首先阅读kddnames文件中包含的标头。 目标字段将简单地命名为target。 读取并解析文件后,我们打印问题的特征数量(请记住目标变量不是特征)及其前十个名称:
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']
现在,让我们创建两个单独的 RDD-一个用于训练数据,另一个用于测试数据:
In: train_rdd = sc.textFile('file:////home//jovyan//datasets//kddtrain')
test_rdd = sc.textFile('file:////home//jovyan//datasets//kddtest')
现在,我们需要解析每个文件的每一行以创建一个DataFrame。 首先,我们将 CSV 文件的每一行拆分为单独的字段,然后将每个数字值转换为浮点,并将每个文本值转换为字符串。 最后,我们删除每行末尾的点。
最后一步,通过使用sqlContext提供的createDataFrame方法,我们可以为训练和测试数据集创建两个带有命名列的 Spark DataFrame:
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); 世界各地的开发人员已经在 PB 级和数十亿条记录上使用了它。 如果数字看起来很大,请不要害怕:Spark 旨在解决这些问题。
现在,让我们看一下它在DataFrame架构上的外观。 具体来说,我们要确定哪些字段是数字字段,哪些字段包含字符串(请注意,为简洁起见,结果已被截断):
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)
... |-- target: string (nullable = true
特征工程
从视觉分析来看,只有四个字段是字符串:protocol_type,service,flag和target(这是预期的多类目标标签)。
由于我们将使用基于树的分类器,因此我们希望将每个级别的文本编码为每个变量的数字。 使用 Scikit-learn,可以使用sklearn.preprocessing.LabelEncoder对象完成此操作。 在 Spark 中等效的是pyspark.ml.feature包的StringIndexer。
我们需要用 Spark 编码四个变量,然后我们必须将四个StringIndexer对象以级联的方式链接在一起:它们每个都将在DataFrame的特定列上操作,并输出一个带有附加列的DataFrame(类似map操作)。 映射是自动的,按频率排序:Spark 在所选列中对每个级别的计数进行排名,将最流行的级别映射到0,然后映射到1,依此类推。 请注意,通过此操作,您将遍历数据集一次以计算每个级别的出现次数。 如果您已经知道该映射,则广播该映射并使用map操作会更有效,如本章开头所示。
更笼统地说,pyspark.ml.feature包中包含的所有类都用于从DataFrame中提取,转换和选择特征。 他们所有人都在DataFrame中读取一些列并创建其他列。
类似地,我们可以使用单热编码器生成数值观察矩阵。 对于单编码器,在DataFrame中将有多个输出列,每个分类特征的每个级别对应一个输出列。 为此,Spark 提供了pyspark.ml.feature.OneHotEncoderEstimator类。
从 Spark 2.3.1 开始,Python 可用的功能操作包含在以下详尽列表中(所有这些可以在pyspark.ml.feature包中找到)。 除了几个名称外,其他名称应直观易懂,这些内容将以内联或稍后的方式进行解释。
回到该示例,我们现在想将每个分类变量中的级别编码为离散数字。 正如我们所解释的,为此,我们将为每个变量使用StringIndexer对象。 此外,我们可以使用 ML 管道并将其设置为其阶段。
然后,要适合所有索引器,只需调用管道的fit方法。 在内部,它将顺序地适合所有暂存的对象。 完成拟合操作后,将创建一个新对象,我们可以将其称为拟合管道。 调用此新对象的transform方法将顺序调用所有已登台的元素(已装配),每个元素都在上一个元素完成后被调用。 在以下代码片段中,您将看到运行中的管道。 请注意,转换器组成了管道。 因此,由于不存在任何动作,因此实际上不执行任何操作。 在输出DataFrame中,您会注意到另外四列,它们的名称与原始分类的列相同,但后缀为_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(), '\n')
print(pipeline)
print(indexer)
Out: [StringIndexer_44f6bd05e502a8ace0aa,
StringIndexer_414084eb873c15c387cd,
StringIndexer_4ca38a4ad6ffeb6ddc95,
StringIndexer_489c92cd030c80c6f677]
Pipeline_46a68853ff9dcdece078
PipelineModel_4f61afaf96ccc4be4b02
从DataFrame中提取某些列就像在 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: ['flag_cat', 'count', 'land', 'serror_rate', 'num_compromised',
'num_access_files', 'dst_host_srv_serror_rate', 'src_bytes',
'num_root', 'srv_serror_rate', 'num_shells', 'diff_srv_rate',
'dst_host_serror_rate',
'rerror_rate', 'num_file_creations', 'same_srv_rate',
'service_cat',
'num_failed_logins', 'duration', 'dst_host_diff_srv_rate', 'hot',
'is_guest_login', 'dst_host_same_srv_rate', 'num_outbound_cmds',
'su_attempted', 'dst_host_count', 'dst_bytes',
'srv_diff_host_rate',
'dst_host_srv_count', 'srv_count', 'root_shell',
'srv_rerror_rate',
'wrong_fragment', 'dst_host_rerror_rate', 'protocol_type_cat',
'urgent',
'dst_host_srv_rerror_rate', 'dst_host_srv_diff_host_rate',
'logged_in',
'is_host_login', 'dst_host_same_src_port_rate']
Total numerical features: 41
在这里,VectorAssembler类可以帮助我们构建特征矩阵。 我们只需要传递要选择的列作为参数,并传递要在DataFrame中创建的新列。 我们决定将输出列简单地命名为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。 在这种情况下,由于特征的向量包含许多零,因此它将返回稀疏向量。 要查看输出内容,我们可以打印第一行。 请注意,这是一个动作。 因此,将在打印结果之前执行作业:
In: Xy_train.first()
Out: Row(features=SparseVector(41, {1: 8.0, 7: 181.0, 15: 1.0, 16: 2.0, 22:
1.0, 25: 9.0, 26: 5450.0, 28: 9.0, 29: 8.0, 34: 1.0, 38: 1.0,
40: 0.11}), target_cat=2.0)
训练学习器
最后,我们到达了任务的热点:训练分类器。 分类器包含在pyspark.ml.classification包中,对于本示例,我们使用随机森林。 对于 Spark 2.3.1,可以在这个页面中找到可用的算法的广泛列表。 算法列表非常完整,包括线性模型,SVM,朴素贝叶斯和树集成。 请注意,并非所有这些工具都能够处理多类问题,并且可能具有不同的参数。 始终检查与使用版本有关的文档。 除分类器外,Spark 2.3.1 中使用 Python 接口实现的其他学习器如下:
- 群集(
pyspark.ml.clustering包):KMeans - 推荐人(
pyspark.ml.recommendation包):ALS(协同过滤推荐器,基于交替最小二乘法)
让我们回到 KDD99 挑战的目标。 现在,该实例化随机森林分类器并设置其参数了。 要设置的参数是featuresCol(包含特征矩阵的列),labelCol(包含目标标签的数据帧的列),seed(使实验可复制的随机种子)和maxBins (用于树的每个节点中的分割点的最大箱子数)。 森林中树木数量的默认值为20,每棵树的最大深度为 5 级。 此外,默认情况下,此分类器在DataFrame中创建三个输出列: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_4c47a18a99f683bec69e
RandomForestClassificationModel
(uid=RandomForestClassifier_4c47a18a99f683bec69e) with 20 trees
在训练好的classifier对象(即RandomForestClassificationModel)上,可以调用transform方法。 我们预测训练和test数据集上的标签,并打印test数据集的第一行。 根据分类器中的定义,预测将在名为prediction的列中找到:
In: Xy_pred_train = fit_clf.transform(Xy_train)
Xy_pred_test = fit_clf.transform(Xy_test)
print("First observation after classification stage:")
print(Xy_pred_test.first())
Out: First observation after classification stage:
Row(features=SparseVector(41, {1: 1.0, 7: 105.0, 15: 1.0, 16: 1.0, 19:
0.01, 22: 1.0, 25: 255.0, 26: 146.0, 28: 254.0, 29: 1.0, 34: 2.0}),
target_cat=2.0, rawPrediction=DenseVector([0.0152, 0.0404, 19.6276,
0.0381, 0.0087, 0.0367, 0.034, 0.1014, 0.0641, 0.0051, 0.0105, 0.0053,
0.002, 0.0005, 0.0026, 0.0009, 0.0018, 0.0009, 0.0009, 0.0006, 0.0013,
0.0006, 0.0008]), probability=DenseVector([0.0008, 0.002, 0.9814,
0.0019,
0.0004, 0.0018, 0.0017, 0.0051, 0.0032, 0.0003, 0.0005, 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-score,因为它是合并精度和召回表现的好指标。 评估指标包含在pyspark.ml.evaluation包中; 在我们拥有的几种选择中,我们正在使用一种来评估多类分类器:MulticlassClassificationEvaluator。 作为参数,我们提供指标(precision,recall,accuracy,F1-score等)以及包含真实标签和预测标签的列的名称:
In: from pyspark.ml.evaluation import MulticlassClassificationEvaluator
evaluator = MulticlassClassificationEvaluator(
labelCol="target_cat",
predictionCol="prediction",
metricName="f1")
f1_train = evaluator.evaluate(Xy_pred_train)
f1_test = evaluator.evaluate(Xy_pred_test)
print("F1-score train set: %0.3f" % f1_train)
print("F1-score test set: %0.3f" % f1_test)
Out: F1-score train set: 0.993
F1-score test set: 0.968
所获得的值非常高,并且训练数据集和测试数据集的表现之间存在很大差异。 除了用于多类分类器的评估器之外,用于回归器(度量标准可以是 MSE,RMSE,R2 或 MAE)和二分类器的评估器对象在同一包中可用。
机器学习管道的力量
到目前为止,我们已经逐步构建并显示了输出。 也可以将所有操作层叠在一起并将它们设置为管道的各个阶段。 实际上,我们可以将到目前为止所看到的内容(四个标签编码器,向量生成器和分类器)链接到一个独立的管道中,将其适合训练数据集,最后在测试数据集上使用它来获得预测 。
这种操作方式更有效,但是您将失去逐步分析的探索能力。 建议作为数据科学家的读者仅在完全确定内部情况后才使用端到端管道,并且仅构建生产模型。 为了显示该管道与我们到目前为止所看到的等效,我们在测试数据集上计算F1-score并进行打印。 毫不奇怪,它的值完全相同:
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)
f1_preds = evaluator.evaluate(predictions)
print("F1-score test set: %0.3f" % f1_preds)
Out: F1-score test set: 0.968
在运行 IPython 笔记本的驱动程序节点上,我们还可以使用matplotlib库来可视化我们的分析结果。 例如,要显示分类结果的归一化混淆矩阵(在每个类的支持下归一化),我们可以创建以下函数:
In: import numpy as np
import matplotlib.pyplot as plt
%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 可以构建confusion矩阵,但是该方法在pyspark.mllib包中。 为了能够使用此包中的方法,我们必须使用.rdd方法将DataFrame转换为 RDD:
In: from pyspark.mllib.evaluation import MulticlassMetrics
metrics = MulticlassMetrics(
predictions.select("prediction", "target_cat").rdd)
conf_matrix = metrics.confusionMatrix()toArray()
plot_confusion_matrix(conf_matrix)
这是从前面的代码片段得到的绘制的混淆矩阵:

手动调整
尽管F1-score接近0.97,但归一化的混淆矩阵显示类别非常不平衡,并且分类器刚刚学会了如何正确地对最受欢迎的类别进行分类。 为了改善结果,我们可以对每个类别进行重新采样,从而有效地尝试更好地平衡训练数据集。
首先,让我们计算一下每个类别的训练数据集中有多少个案例:
In: train_composition = (train_df.groupBy("target")
.count()
.rdd
.collectAsMap())
print(train_composition)
Out: {'neptune': 107201,
'nmap': 231,
'portsweep': 1040,
'back': 2203,
'warezclient': 1020,
'normal': 97278,
... 'loadmodule': 9,
'phf': 4}
这清楚地表明了严重的不平衡。 我们可以尝试通过对稀有类进行过度采样和对太受欢迎的类进行二次采样来提高表现。 在此示例中,我们将创建一个训练数据集,其中每个类别至少代表 1,000 次,但最多代表 25,000 次。 为此,我们执行以下步骤:
- 让我们首先创建子采样/过采样率,并将其在整个集群中广播,然后
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.items()}
sample_rates
Out: {'neptune': 0.23320677978750198,
'nmap': 4.329004329004329,
'portsweep': 1,
'back': 1,
'warezclient': 1,
'normal': 0.2569954152017928,
... 'loadmodule': 111.11111111111111,
'phf': 250.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
.rdd
.flatMap(
lambda x: map_and_sample(x,
bc_sample_rates))
.toDF()
.cache())
sampled_train_df``DataFrame变量中的这些采样数据集也被缓存; 我们将在超参数优化步骤中多次使用它。 它应该很容易装入内存,因为行数少于原始数:
In: sampled_train_df.count()
Out: 96559
- 为了了解其中的内容,我们可以打印第一行。 非常快速地打印出值,不是吗? 当然,这已经非常快了,因为它已经被缓存了:
In: sampled_train_df.first()
Out: Row(duration=0.0, protocol_type='tcp', service='http',
flag='SF',
src_bytes=210.0, dst_bytes=624.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=18.0,
srv_count=18.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=18.0,
dst_host_srv_count=109.0,
dst_host_same_srv_rate=1.0, dst_host_diff_srv_rate=0.0,
dst_host_same_src_port_rate=0.06,
dst_host_srv_diff_host_rate=0.05,
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='normal')
- 现在,让我们使用我们创建的管道进行一些预测,并打印此新解决方案的
F1-score:
In: full_model = full_pipeline.fit(sampled_train_df)
predictions = full_model.transform(test_df)
f1_preds = evaluator.evaluate(predictions)
print("F1-score test set: %0.3f" % f1_preds)
Out: F1-score test set: 0.967
- 在
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)
f1_preds = evaluator.evaluate(predictions)
print ("F1-score test set: %0.3f" % f1_preds )
Out: F1-score test set: 0.968
到此,我们结束了有关在 Spark 上调整模型的示例。 最终测试为我们提供了关于模型在生产中的有效性的合理估计。
交叉验证
在尝试了许多不同的配置之后,我们可以继续进行手动优化并找到合适的模型。 这样做会导致时间的大量浪费(以及代码的可重用性),并使测试数据集过拟合。 交叉验证是运行超参数优化的正确关键。 现在让我们看看 Spark 如何执行这项关键任务。
首先,由于训练将被多次使用,因此我们可以cache。 因此,让我们在所有转换后使用cache:
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 一样,它们是在创建classification对象时设置的(在这种情况下,是列名,种子和最大数量的容器)。
然后,借助网格生成器,我们决定应为交叉验证算法的每次迭代更改哪些参数。 在此示例中,我们要检查分类表现是否将森林中每棵树的最大深度从3更改为12(增加 3),并将森林中的树数从 20 或 50 改变为最后。 设置网格图,我们要测试的分类器和折叠次数后,启动交叉验证(使用fit方法)。 参数评估器是必不可少的:它会告诉我们哪种是交叉验证后保持的最佳模型。 请注意,此操作可能需要 15-20 分钟才能运行(在引擎盖下,训练并测试了4 * 2 * 3 = 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)
f1_preds = evaluator.evaluate(predictions)
print("F1-score test set: %0.3f" % f1_preds)
Out: F1-score test set: 0.970
此外,通过绘制归一化的混淆矩阵,您会立即意识到,该解决方案能够发现更广泛的攻击,甚至是不太流行的攻击:
In: metrics = MulticlassMetrics(
predictions.select("prediction", "target_cat").rdd)
conf_matrix = metrics.confusionMatrix().toArray()
plot_confusion_matrix(conf_matrix)
这次,输出是归一化的混淆矩阵,显示了预测中错位最多的地方:

最终清理
在这里,我们处于分类任务的结尾。 记住要从cache中删除所有已使用的变量和已创建的临时表:
In: bc_sample_rates.unpersist()
sampled_train_df.unpersist()
train.unpersist()
清除 Spark 内存后,我们可以关闭 Jupyter 笔记本电脑。
总结
在本章中,我们向您介绍了 Hadoop 生态系统,包括架构,HDFS 和 PySpark。 介绍之后,我们开始设置本地 Spark 实例,并在群集节点之间共享变量后,我们使用 RDD 和DataFrame在 Spark 中进行了数据处理。
在本章的稍后部分,我们学习了使用 Spark 进行机器学习的知识,其中包括读取数据集,训练学习器,机器学习管道的功能,交叉验证,甚至测试通过示例数据集获得的知识。
到此结束我们围绕 Python 进行数据科学基础研究的旅程,下一章只是刷新和增强 Python 基础的附录。 总之,通过本书的所有章节,我们已经完成了数据科学项目的介绍,接触了项目的所有关键步骤,并向您展示了使用 Python 成功操作自己的项目的所有基本工具。 作为学习工具,这本书陪同您完成了数据科学的所有阶段,从数据加载到机器学习和可视化,阐述了无论数据大小的最佳实践和避免常见陷阱的方法。 作为参考,本书涉及各种命令和包,为您提供了简单明了的说明和示例,如果在项目中重复使用这些示例和示例,则可以在工作期间节省大量时间。
从现在开始,Python 肯定会在您的项目开发中扮演更重要的角色,到目前为止,我们很高兴能陪同您一起为数据科学掌握 Python。
九、加强您的 Python 基础
这些章节随附的代码示例不需要您精通 Python。 但是,他们将假定您之前至少已经掌握了 Python 脚本编写的基础知识。 他们还将特别假设您了解数据结构(例如列表和字典),并且对如何使类对象起作用具有想法。
如果您对上述主题没有信心或对 Python 语言了解甚少,建议您在开始阅读本书之前,先参加在线教程,例如这个页面,位于这个页面的 Google Python 课程或 Kaggle 提供的课程。 所有课程都是免费的,并且在几个小时的学习中,它们应该为您提供所有基础知识,以确保您充分享受这本书。 如果您喜欢在一本书中学习 Python 基础知识,则可以阅读 Jake Vanderplas 的《Python 旋风之旅》,您需要的 Python 基本知识:从变量赋值到导入包。 我们还准备了一些注释,这些注释在本简短但具有挑战性的奖金一章中进行了介绍,以突出显示重要性和增强您对 Python 语言对数据科学使用至关重要的所有方面的知识。 在本章中,您将学到以下内容:
- 关于成为有效数据科学家的 Python 知识
- 通过观看视频学习 Python 的最佳资源
- 通过直接编写和测试代码来学习 Python 的最佳资源
- 通过阅读学习 Python 的最佳资源
您的学习列表
这是您需要学习像数据科学家一样熟练的基本 Python 数据结构。 除了实际的基础知识(数字,算术,字符串,布尔值,变量赋值和比较)之外,列表确实很短。 我们将通过仅涉及数据科学项目中的递归结构来简要地处理它。 请记住,这些主题颇具挑战性,但是如果您想编写有效的代码,则必须掌握这些主题:
- 列表
- 字典
- 类,对象和面向对象的编程
- 异常
- 迭代器和生成器
- 条件
- 推导式
- 函数
根据您对 Python 语言的实际了解,将其作为复习或学习列表。 但是,请检查所有建议的示例,因为在本书学习过程中您将再次遇到它们。
列表
列表是元素的集合。 元素可以是整数,浮点数,字符串或一般对象。 此外,您可以将不同的类型混合在一起。 此外,列表比数组更灵活,因为数组仅允许单个数据类型。 要创建列表,可以使用方括号或list()构造器,如下所示:
a_list = [1, 2.3, 'a', True]
an_empty_list = list()
以下是一些在使用列表时需要记住的便捷方法:
- 要访问第个元素,请使用
[]表示法:
请记住,列表从 0(零)开始索引; 也就是说,第一个元素的位置为 0。
a_list[1]
# prints 2.3
a_list[1] = 2.5
# a_list is now [1, 2.5, 'a', True]
- 您可以通过指出起点和终点(所得终点中不包括终点)来对列表进行切片,如下所示:
a_list[1:3]
# prints [2.3, 'a']
- 您可以使用冒号分隔的
start:end:skip表示法对跳过进行切片,以便为每个跳过值获取一个元素,如下所示:
a_list[::2]
# returns only odd elements: [1, 'a'] a_list[::-1]
# returns the reverse of the list: [True, 'a', 2.3, 1]
- 要在列表末尾添加元素,可以使用
append():
a_list.append(5)
# a_list is now [1, 2.5, 'a', True, 5]
- 要获取列表的长度,请使用
len()函数,如下所示:
len(a_list)
# prints 5
- 要删除元素,请使用
del语句,后跟要删除的元素:
del a_list[0]
# a_list is now [2.5, 'a', True, 5]
- 要连接两个列表,请使用
+,如下所示:
a_list += [1, 'b']
# a_list is now [2.5, 'a', True, 5, 1, 'b']
- 您可以通过将列表分配给变量列表(或只是序列)而不是单个变量来解压缩列表:
a, b, c, d, e, f = [2.5, 'a', True, 5, 1, 'b']
# a now is 2.5, b is 'a' and so on
请记住,列表是可变的数据结构; 您可以随时添加,删除和修改元素。 不可变列表称为元组,并用圆括号(和)表示,而不是列表[和]中的方括号:
tuple(a_list)
# prints (2.5, 'a', True, 5, 1, 'b')
字典
词典是可以快速查找内容的表,因为每个键都与一个值相关联。 确实就像使用书的索引立即跳到您需要的内容。 键和值可以属于不同的数据类型。 键的唯一先决条件是它们应该是可散列的(这是一个相当复杂的概念;只需使键尽可能简单,因此,请勿尝试使用字典或列表作为键)。 要创建字典,可以使用大括号,如下所示:
b_dict = {1: 1, '2': '2', 3.0: 3.0}
以下是使用字典时可以记住的一些便捷方法:
- 要访问通过
k键索引的值,请使用[]表示法,如下所示:
b_dict['2']
# prints '2'
b_dict['2'] = '2.0'
# b_dict is now {1: 1, '2': '2.0', 3.0: 3.0}
- 要插入或替换键的值,请再次使用
[]表示法:
b_dict['a'] = 'a'
# b_dict is now {3.0: 3.0, 1: 1, '2': '2.0', 'a': 'a'}
- 要获取字典中的元素数,请使用
len()函数,如下所示:
len(b_dict)
# prints 4
- 要删除元素,请使用
del语句,后跟要删除的元素:
del b_dict[3.0]
# b_dict is now {1: 1, '2': '2.0', 'a': 'a'}
请记住,字典(如列表)是可变的数据结构。 还要记住,如果您尝试访问其键不存在的元素,则会引发KeyError异常:
b_dict['a_key']
Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'a_key'
显而易见的解决方案是始终先检查字典中是否有元素:
if 'a_key' in b_dict:
b_dict['a_key']
else:
print("'a_key' is not present in the dictionary")
否则,您可以使用.get方法。 如果键在字典中,则返回其值; 否则,不返回任何内容:
b_dict.get('a_key')
最后,您可以使用集合模块中的数据结构defaultdict,它将永远不会引发KeyError,因为它是由不带任何参数,并为它可能想要的任何不存在的键提供默认值的函数实例化的:
from collections import defaultdict
c_dict = defaultdict(lambda: 'empty')
c_dict['a_key']
# requiring a nonexistent key will always return the string 'empty'
可以使用def或lambda命令定义defaultdict使用的default函数,如下节所述。
定义函数
函数是指令的集合,通常会从您那里接收特定的输入,并提供与这些输入相关的一组特定的输出。 您可以将它们定义为单线,如下所示:
def half(x):
return x/2.0
您还可以通过以下方式将它们定义为一组许多指令:
import math
def sigmoid(x):
try:
return 1.0 / (1 + math.exp(-x))
except:
if x < 0:
return 0.0
else:
return 1.0
最后,您可以使用lambda函数来创建匿名函数。 将匿名函数视为简单函数,您可以在代码中的任何位置内联定义,而无需使用verbose构造器(以def开头的函数)。 只需调用lambda,然后输入其输入参数即可; 然后,冒号将发信号通知要由lambda函数执行的命令的开始,这些命令必须在同一行上。 (没有return命令!这些命令将从lambda函数返回。)您可以将lambda函数用作另一个函数的参数,如先前在defaultdict中看到的那样,也可以使用它在一行中表达一个函数。 在我们的示例中就是这种情况,我们通过返回lambda函数并结合第一个参数来定义函数:
def sum_a_const(c):
return lambda x: x+c
sum_2 = sum_a_const(2)
sum_3 = sum_a_const(3)
print(sum_2(2))
print(sum_3(2))
# prints 4 and 5
要调用一个函数,请写上函数名称,然后在括号内写上其参数:
half(10)
# prints 5.0
sigmoid(0)
# prints 0.5
通过使用函数,您可以通过对重复过程进行形式化的输入和输出来使重复过程组合在一起,而不会让它们的计算以任何方式干扰主程序的执行。 实际上,除非您声明变量是全局变量,否则将丢弃您在函数中使用的所有变量,并且主程序将仅接收return命令返回的内容。
顺便说一句,请注意,如果您将一个列表传递给一个仅函数的列表,而该列表不会在变量中发生,那么即使不返回,也将对其进行修改,除非您将其复制。 为了复制列表,您可以使用复制或深层复制功能(要从复制包中导入),也可以仅使用应用于列表的运算符[:]。
为什么会这样? 因为列表尤其是由地址而不是整个对象引用的数据结构。 因此,当您将列表传递给函数时,您只是将一个地址传递给计算机的内存,该函数将通过修改您的实际列表来对该地址进行操作:
a_list = [1,2,3,4,5]
def modifier(L):
L[0] = 0
def unmodifier(L):
M = L[:] # Here we are copying the list
M[0] = 0
unmodifier(a_list)
print(a_list)
# you still have the original list, [1, 2, 3, 4, 5]
modifier(a_list)
print(a_list)
# your list have been modified: [0, 2, 3, 4, 5]
类,对象和面向对象的编程
类是方法和属性的集合。 简而言之,属性是对象的变量(例如,Employee类的每个实例都有其自己的name,age,salary和benefits;它们都是属性)。
方法只是修改属性的简单函数(例如,设置员工姓名,设置他/她的年龄以及从数据库或 CSV 列表中读取此信息)。 要创建一个类,请使用class关键字。
在下面的示例中,我们将为增量器创建一个类。 此对象的目的是跟踪整数值,并最终将其增加 1:
class Incrementer(object):
def __init__(self):
print ("Hello world, I'm the constructor")
self._i = 0
def缩进中的所有内容都是class方法。 在这种情况下,名为__init__的方法会将i内部变量设置为零(它看起来就像上一章中描述的函数一样)。 仔细查看方法的定义。 它的参数是self(这是对象本身),每个内部变量的访问都通过self进行:
__init__不仅仅是一种方法; 它是构造器(在创建对象时调用)。 实际上,当我们构建Increment对象时,将自动调用此方法,如下所示:
i = Incrementer()
# prints "Hello world, I'm the constructor"
- 现在,让我们创建
increment()方法,该方法将增加i内部计数器并返回状态。 在类定义中,包括方法:
def increment(self):
self._i += 1
return self._i
- 然后,运行以下代码:
i = Incrementer()
print (i.increment())
print (i.increment())
print (i.increment())
- 上面的代码产生以下输出:
Hello world, I'm the constructor
1
2
3
最后,让我们看看如何创建接受参数的方法。 现在,我们将创建set_counter方法,该方法设置_i内部变量:
- 在类定义中,添加以下代码:
def set_counter(self, counter):
self._i = counter
- 然后,运行以下代码:
i = Incrementer()
i.set_counter(10)
print (i.increment())
print (i._i)
- 上面的代码给出以下输出:
Hello world, I'm the constructor
11
11
请注意前面代码的最后一行,您可以在其中访问内部变量。 请记住,在 Python 中,默认情况下对象的所有内部属性都是公共的,并且可以在外部读取,写入和更改它们。
异常
异常和错误密切相关,但是它们是不同的东西。 例如,可以很好地处理异常。 以下是一些异常示例:
0/0
Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: integer division or modulo by zero
len(1, 2)
Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: len() takes exactly one argument (2 given)
pi * 2
Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'pi' is not defined
在此示例中,提出了三个不同的异常(请参见每个块的最后一行)。 要处理异常,可以通过以下方式使用try...except块:
try:
a = 10/0
except ZeroDivisionError:
a = 0
您可以使用一个以上的except子句来处理多个异常。 您最终可以使用最终的all-the-other异常处理器。 在这种情况下,结构如下:
try:
<code which can raise more than one exception>
except KeyError:
print ("There is a KeyError error in the code")
except (TypeError, ZeroDivisionError):
print ("There is a TypeError or a ZeroDivisionError error in the code") except:
print ("There is another error in the code")
最后,重要的是要提到有最后一个子句finally,它将在所有情况下执行。 如果您要清理代码(关闭文件,取消分配资源等),这非常方便。 这些都是应该独立完成的事情,无论是否发生错误。 在这种情况下,代码采用以下形状:
try:
<code that can raise exceptions>
except:
<eventually more handlers for different exceptions>
finally:
<clean-up code>
迭代器和生成器
遍历列表或字典非常简单。 请注意,对于字典而言,迭代是基于键的,下面的示例对此进行了演示:
for entry in ['alpha', 'bravo', 'charlie', 'delta']:
print (entry)
# prints the content of the list, one entry for line
a_dict = {1: 'alpha', 2: 'bravo', 3: 'charlie', 4: 'delta'}
for key in a_dict:
print (key, a_dict[key])
# Prints:
# 1 alpha
# 2 bravo
# 3 charlie
# 4 delta
另一方面,如果您需要遍历序列并快速生成对象,则可以使用生成器。 这样做的一个很大好处是,您不必在一开始就创建和存储完整的序列。 取而代之的是,每次调用生成器时,都会构建每个对象。 作为一个简单的示例,让我们为数字序列创建一个生成器,而无需事先存储完整列表:
def incrementer():
i = 0
while i<5:
yield(i)
i +=1
for i in incrementer():
print (i)
# Prints:
# 0
# 1
# 2
# 3
# 4
条件
因为您可以分支程序,所以条件常用于数据科学中。 最常用的是if语句。 它的工作原理与其他编程语言大致相同。 这是一个例子:
def is_positive(val):
if val< 0:
print ("It is negative")
elif val> 0:
print ("It is positive")
else:
print ("It is exactly zero!")
is_positive(-1)
is_positive(1.5)
is_positive(0)
# Prints:
# It is negative
# It is positive
# It is exactly zero!
用if检查第一个条件。 如果还有其他条件,则用elif定义(代表else...if)。 最后,默认行为由else处理。
注意,elif和else不是必需的。
列表和字典的推导式
使用通过迭代器和条件构建为一行的推导式,列表和字典:
a_list = [1,2,3,4,5]
a_power_list = [value**2 for value in a_list]
# the resulting list is [1, 4, 9, 16, 25]
filter_even_numbers = [value**2 for value in a_list if value % 2 == 0]
# the resulting list is [4, 16]
another_list = ['a','b','c','d','e']
a_dictionary = {key:value for value, key in zip(a_list, another_list)}
# the resulting dictionary is {'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4}
zip是一个函数,该函数将多个相同长度的列表作为输入,并同时遍历具有相同索引的每个元素,因此您可以将每个列表的前几个元素匹配在一起,依此类推。
理解是过滤和转换迭代器中存在的数据的快速方法。
通过观看,阅读和实践来学习
如果进修课程和我们的学习列表还不够,您需要更多支持以增强您的 Python 知识怎么办? 我们将建议您在网上免费获得更多资源。 通过观看教程视频,您可以尝试复杂且不同的示例,并通过一项艰巨的任务挑战自己,该任务需要您与其他数据科学家和 Python 专家进行互动。
大规模公开在线课程(MOOC)
近年来,MOOC 变得越来越流行,在其在线平台上免费提供来自世界各地最好的大学和专家的一些最好的课程。 您将在 Coursera,Edx 和 Udacity。 另一个很好的来源是 MIT 开放式课件,它很容易访问。
当您访问这些站点中的每一个时,您可能会发现有关 Python 的其他有效课程。 我们建议 Google Inc.研究总监 Peter Norvig 提供免费的,始终可用的,自己动手的进度课程。该课程旨在使您对 Python 的了解达到更高的水平。
PyCon 和 PyData
Python 大会(PyCon)是在世界各地举办的年度大会,旨在促进 Python 语言的使用和传播。 在此类会议期间,通常会举行教程,动手演示和培训课程。 您可以查看这个页面,了解在您附近举行下一次 PyCon 的地点和时间。 如果您无法参加,则仍然可以在这个页面上进行搜索,因为大多数有趣的会话均已记录并上传到那里。 无论如何,参加和观看真实的演示是另一回事,所以我们热烈建议您参加这样的会议,因为这确实值得。
类似地,PyData 是致力于数据分析的 Python 开发人员和用户社区,在世界各地组织了许多活动。 您可以查看这个页面即将发生的事件,并查看是否有任何以往的事件对您感兴趣。 与 PyCon 一样,通常可以在 YouTube 上的 PyDataTV 等专用频道上进行演示。
交互式 Jupyter
有时,您需要一些书面说明,并有机会自己测试一些示例代码。 Jupyter 是一个类似 Python 本身的开放工具,可通过其笔记本(交互式网页)为您提供所有这些功能,您可以在其中找到说明和可以直接测试的示例代码。 我们在整本书中都对 Jupyter 及其内核进行了说明,因为它是真正的数据科学。 它使您可以轻松地运行 Python 脚本,并评估它们对正在处理的数据的影响。
IPython 内核(Jupyter 的 Python 内核,因为 Jupyter 可以运行许多不同的编程语言)的 GitHub 位置提供了示例笔记本的完整列表。 您可以在这个页面上进行检查。 特别是,列表的一部分与通用 Python 编程有关,而另一部分与统计,机器学习和数据科学有关,您会在其中找到很多 Python 脚本的示例,您可以从中学到灵感。
别害羞,接受真正的挑战
如果您想做一些可以使您的 Python 编码能力达到不同水平的事情,我们建议您去挑战 Kaggle。 Kaggle 是用于预测建模和分析竞赛的平台,在数据中应用了竞争性编程(参与者尝试根据提供的规范进行编程)的思想,向参与者提出具有挑战性的数据问题,并要求他们提供可能在测试集上评估的解决方案,从而实现科学。 测试集的结果部分是公开的,部分是私有的。
对于 Python 学习者来说,最有趣的部分是有机会参加没有明显解决方案的实际问题,这需要您编写一些代码以提出可能的问题解决方案,甚至是简单或幼稚的解决方案(我们建议您从头开始) 参与复杂的解决方案之前)。 这样,学习者将发现有趣的教程,基准测试代码,有用的数据科学家社区,以及其他数据科学家或 Kaggle 本身在其博客中提出的一些非常聪明的解决方案。
您可能想知道如何为自己找到正确的挑战。 只需在这个页面上查看过去和现在的比赛,并寻找每一个有知识作为奖励的比赛。 您会惊讶地发现一个理想的阶段来学习其他数据科学家如何用 Python 进行编码,并且您可以立即应用从本书中学到的知识。


浙公网安备 33010602011771号