Python-应用监督学习-全-
Python 应用监督学习(全)
原文:
annas-archive.org/md5/d906d6d9346f8d8b7965d192afaf9a47译者:飞龙
前言
关于
本节简要介绍了作者、本书内容、你需要掌握的技术技能,以及完成所有活动和练习所需的硬件和软件要求。
关于本书
机器学习——机器基于输入数据给出正确答案的能力——彻底改变了我们做生意的方式。《应用 Python 监督学习》提供了如何将机器学习技术应用于你的数据科学项目的丰富理解。你将探索 Jupyter 笔记本,一种在学术界和商业圈广泛使用的技术,支持运行内联代码。
在有趣的示例帮助下,你将获得使用 Python 机器学习工具包的经验——从执行基本的数据清理和处理,到使用多种回归和分类算法。掌握基础后,你将学习如何使用决策树、集成建模、验证和误差指标等高级技术来构建和训练自己的模型。你还将学习如何使用强大的 Python 库,如 Matplotlib 和 Seaborn,进行数据可视化。
本书还涵盖了集成建模和随机森林分类器,以及结合多个模型结果的其他方法,最后深入探讨了交叉验证,用于测试你的算法并检查模型在未见数据上的表现。
到本书结束时,你不仅将能够使用机器学习算法,还将能够创建你自己的算法!
关于作者
本杰明·约翰斯顿是全球领先的以数据驱动的医疗科技公司之一的高级数据科学家,参与从问题定义、解决方案研发到最终部署的整个产品开发过程中创新数字解决方案的开发。他目前正在攻读机器学习博士学位,专注于图像处理和深度卷积神经网络。他在医疗设备设计与开发方面拥有超过 10 年的经验,曾在多个技术岗位工作,并获得澳大利亚悉尼大学的工程学与医学科学的双学士学位,并且均为一等荣誉学位。
伊希塔·马图尔在数据科学领域有 2.5 年的工作经验,曾在面向产品的初创公司工作,解决来自各个领域的业务问题,并将这些问题转化为可以通过数据和机器学习解决的技术问题。她目前在 GO-JEK 的工作涉及机器学习项目的端到端开发,作为产品团队的一员,定义、原型设计并在产品中实现数据科学模型。她在英国爱丁堡大学完成了高性能计算与数据科学的硕士学位,并在德里圣斯蒂芬学院获得了物理学的荣誉学士学位。
目标
-
理解监督学习的概念及其应用
-
使用机器学习 Python 库实现常见的监督学习算法
-
使用 k 折交叉验证技术验证模型
-
使用决策树构建模型,轻松获得结果
-
使用集成模型技术提高模型的性能
-
使用多种度量标准比较机器学习模型
受众
使用 Python 的应用监督学习 适合那些想要通过 Python 深入理解机器学习的人。如果你有任何函数式或面向对象编程语言的经验,并对 Python 的库和表达式(如数组和字典)有基本了解,将对你有帮助。
方法
使用 Python 的应用监督学习 采用实践性的方法,帮助理解如何使用 Python 进行监督学习。它包含多个活动,使用实际的商业场景,帮助你在高度相关的环境中练习和应用新技能。
硬件要求
为了获得最佳的学习体验,我们推荐以下硬件配置:
-
处理器:双核或更高
-
内存:4 GB RAM
-
硬盘:10 GB 可用空间
-
网络连接
软件要求
你还需要预先安装以下软件:
-
以下操作系统中的任何一个:
Windows 7 SP1 32/64 位,Windows 8.1 32/64 位,或 Windows 10 32/64 位
Ubuntu 14.04 或更高版本
macOS Sierra 或更高版本
-
浏览器:Google Chrome 或 Mozilla Firefox
-
Anaconda
约定
书中出现的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名均按如下方式展示:“可以通过 Python 内建的 type 函数轻松验证这一点。”
一段代码如下所示:
description_features = [
'injuries_description', 'damage_description',
'total_injuries_description', 'total_damage_description'
]
新术语和重要单词以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,文本中会这样出现:“点击 Untitled 文本,弹出框将允许你重命名该笔记本。”
安装与设置
安装 Anaconda 后,可以使用 Jupyter notebooks。可以通过docs.anaconda.com/anaconda/install/windows/上的步骤,在 Windows 系统上安装 Anaconda。
对于其他系统,请从docs.anaconda.com/anaconda/install/导航到相应的安装指南。
安装代码包
将书中的代码包复制到 C:/Code 文件夹中。
附加资源
本书的代码包也托管在 GitHub 上,地址为:github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python。
我们还提供其他代码包,来自我们丰富的书籍和视频目录,地址为 https://github.com/PacktPublishing/。快来看看吧!
第一章:Python 机器学习工具包
学习目标
在本章结束时,你将能够:
-
解释监督学习,并描述常见的机器学习问题示例
-
安装并加载 Python 库到开发环境中,用于分析和机器学习问题
-
访问并解读 Python 库子集的文档,包括强大的 pandas 库
-
创建一个 IPython Jupyter 笔记本,并使用可执行代码单元和 markdown 单元来创建动态报告
-
使用 pandas 加载外部数据源,并利用各种方法对数据进行搜索、过滤和计算描述性统计
-
清洗质量较差的数据源,并评估数据源中各种问题可能产生的影响
本章介绍了监督学习、Jupyter 笔记本以及一些常见的 pandas 数据方法。
介绍
机器学习和人工智能的研究与应用,近年来成为了科技和商业界广泛关注和研究的热点。先进的数据分析和机器学习技术在推动许多领域(如个性化医疗和自动驾驶汽车)发展方面展现出了巨大的潜力,同时也在解决全球一些重大挑战(如应对气候变化)方面发挥着重要作用。本书旨在帮助你抓住当前数据科学和机器学习领域的独特机遇。在全球范围内,私营企业和政府意识到数据驱动的产品和服务的价值和效率。同时,硬件成本的降低和开源软件解决方案的普及大大降低了学习和应用机器学习技术的门槛。
本书将帮助你发展所需的技能,使你能够使用 Python 编程语言中的监督学习技术来识别、准备和构建预测模型。六个章节分别涵盖了监督学习的各个方面。本章介绍了 Python 机器学习工具包的一个子集,以及在加载和使用数据源时需要考虑的一些事项。这个数据探索过程将在第二章《探索性数据分析与可视化》中进一步探讨,介绍探索性数据分析和可视化。第三章《回归分析》和第四章《分类》将探讨机器学习问题的两个子集——回归分析和分类分析,并通过实例展示这些技术。最后,第五章《集成建模》介绍了集成网络,该技术通过多个不同模型的预测来提高整体性能,而第六章《模型评估》则涵盖了验证和评估指标这两个极为重要的概念。这些指标提供了一种估计模型真实表现的方法。
有监督机器学习
机器学习算法通常被认为仅仅是数学过程(或算法)本身,例如神经网络、深度神经网络或随机森林算法。然而,这只是整体系统的一个组成部分;首先,我们必须定义可以通过这些技术充分解决的问题。接着,我们必须指定并获取一个干净的数据集,该数据集由可以从第一个数值空间映射到第二个数值空间的信息组成。一旦数据集被设计并获得,机器学习模型就可以被指定和设计;例如,使用tanh激活函数的 100 个隐藏节点的单层神经网络。
在数据集和模型被明确定义后,可以指定确定模型精确值的方法。这是一个重复的优化过程,它通过评估模型的输出与现有数据的匹配度,通常被称为训练。一旦训练完成,且你拥有已定义的模型,接下来最好通过一些参考数据对其进行评估,以提供整体性能的基准。
考虑到完整机器学习算法的这个一般描述,问题定义和数据收集阶段通常是最关键的。你要解决的问题是什么?你希望达到什么样的结果?你打算如何实现这一目标?你如何回答这些问题将决定并定义后续的许多决策或模型设计选择。在回答这些问题时,我们将选择哪种机器学习算法类别:有监督还是无监督方法。
那么,什么是有监督和无监督机器学习问题或方法呢?有监督学习技术的核心是通过为训练过程提供输入信息和期望输出,映射某些信息集到另一个信息集,并检查其提供正确结果的能力。例如,假设你是一本杂志的出版商,负责评审并排名来自不同时间段的发型。你的读者常常会给你发送大量的发型图片,比你手动处理的要多。为了节省时间,你希望自动对收到的发型图片按时间段进行分类,从 1960 年代和 1980 年代的发型开始:

图 1.1:来自不同时间段的发型图片
要创建你的发型分类算法,你需要先收集大量发型图片,并手动为每一张图片标注对应的时间段。这样的数据集(称为标注数据集)是输入数据(发型图片),并且所需的输出信息(时间段)是已知且记录下来的。这种问题是经典的监督学习问题;我们试图开发一个算法,输入一组数据,然后让它学会返回我们已经告诉它正确的答案。
何时使用监督学习
通常情况下,如果你试图自动化或复制一个现有的过程,那么这个问题就是一个监督学习问题。监督学习技术既非常有用又非常强大,或许你已经接触过它们,甚至在不知情的情况下帮助创建了它们的标注数据集。举个例子,几年前,Facebook 引入了在平台上传任何图片时标记朋友的功能。要标记一个朋友,你只需在朋友的面部上画一个框,然后添加朋友的名字以通知他们图片的存在。快进到今天,Facebook 会自动识别图片中的朋友并为你标记他们。这又是一个监督学习的例子。如果你曾经使用过早期的标记系统,并手动在图片中标记你的朋友,实际上你是在帮助创建 Facebook 的标注数据集。用户上传一个人的面部图片(输入数据),并用该人物的名字标记照片,这样就为数据集创建了标签。随着用户持续使用这个标记服务,一个足够大的标注数据集就被创建出来,解决了监督学习问题。现在,Facebook 自动完成朋友标记,使用监督学习算法替代了手动输入的过程:

图 1.2:在 Facebook 上标记朋友
一个特别及时且直接的监督学习例子是自动驾驶汽车的训练。在这个例子中,算法使用由 GPS 系统确定的目标路线,以及车载仪器,如速度测量、刹车位置和/或 光学雷达(LIDAR)进行道路障碍物检测,作为系统的标注输出。在训练过程中,算法采集人类驾驶员提供的控制输入,如速度、转向角度和刹车位置,并将其与系统的输出进行映射,从而提供标注数据集。这些数据可以用来训练自动驾驶汽车的驾驶/导航系统,或用于模拟练习。
基于图像的监督问题虽然很流行,但并不是监督学习问题的唯一例子。监督学习也常用于自动分析文本,以判断消息的意见或语气是积极的、消极的还是中立的。这种分析被称为情感分析,通常涉及创建并使用一个标记数据集,其中一系列的单词或语句被手动标识为积极、消极或中立。例如,考虑以下句子:我喜欢这部电影 和 我讨厌这部电影。第一个句子显然是积极的,而第二个是消极的。我们可以将句子中的单词分解为积极、消极或中立(都积极、都消极);见下表:

图 1.3:单词的分解
使用情感分析,可能会创建一个监督学习算法,比如使用电影数据库网站 IMDb 分析发布的关于电影的评论,以确定观众是对电影进行积极评价还是消极评价。监督学习方法还可以应用于其他领域,如分析客户投诉、自动化故障排除电话/聊天会话,甚至在医学领域,如分析痣的图像来检测异常(www.nature.com/articles/nature21056)。
这应该能帮助你更好地理解监督学习的概念,以及一些可以使用这些技术解决的问题示例。监督学习涉及训练一个算法,将输入信息映射到相应的已知输出;而无监督学习方法则不同,它们不使用已知的输出,可能是因为这些输出不可用或甚至未知。无监督学习方法并不依赖于一组手动标注的标签,而是通过在训练过程中设计的特定约束或规则来对提供的数据进行建模。
聚类分析是一种常见的无监督学习形式,其中数据集会根据所使用的聚类过程被划分为指定数量的不同组。以 k 近邻聚类为例,数据集中的每个样本会根据与该样本最接近的 k 个点的多数投票结果进行标记或分类。由于没有手动识别的标签,无监督算法的性能会根据使用的数据以及模型选择的参数大不相同。例如,我们是否应该在 k 个最接近的点中使用 5 个最接近的点,还是 10 个最接近的点进行多数投票?由于训练过程中缺乏已知和目标输出,无监督方法通常用于探索性分析或在那些真实目标比较模糊且通过学习方法的约束来更好定义的场景中。
本书中我们不会深入讨论无监督学习,但总结两种方法之间的主要区别是有帮助的。监督学习方法需要真实标签或输入数据的答案,而无监督学习方法不使用这些标签,最终结果由训练过程中应用的约束决定。
为什么选择 Python?
那么,为什么我们选择 Python 编程语言来进行监督学习的研究呢?虽然有多种替代语言可供选择,包括 C++、R 和 Julia,甚至 Rust 社区也在为其新兴语言开发机器学习库,但 Python 仍是机器学习的首选语言,原因有很多:
-
在工业界和学术研究中,对具备 Python 技能的开发人员有着极大的需求。
-
Python 目前是最受欢迎的编程语言之一,甚至在IEEE Spectrum杂志对十大编程语言的调查中位居第一(
spectrum.ieee.org/at-work/innovation/the-2018-top-programming-languages)。 -
Python 是一个开源项目,Python 编程语言的全部源代码在 GNU GPL Version 2 许可证下免费提供。这种许可机制使得 Python 能够在许多其他项目中使用、修改甚至扩展,包括 Linux 操作系统、支持 NASA 的项目(
www.python.org/about/success/usa/),以及众多提供额外功能、选择和灵活性的库和项目。在我们看来,这种灵活性是 Python 如此受欢迎的关键因素之一。 -
Python 提供了一套通用的功能,可用于运行 Web 服务器、嵌入式设备上的微服务,或利用图形处理单元的强大功能对大型数据集进行精确计算。
-
使用 Python 和一些特定的库(或在 Python 中称为“包”),可以开发出整个机器学习产品——从探索性数据分析、模型定义与优化,到 API 构建和部署。这些步骤都可以在 Python 中完成,构建一个端到端的解决方案。这是 Python 相对于一些竞争者,特别是在数据科学和机器学习领域的一个显著优势。尽管 R 和 Julia 在数值计算和统计计算方面具有优势,但用这些语言开发的模型通常需要在部署到生产环境之前,转译成其他语言。
我们希望通过本书,你能了解 Python 编程语言的灵活性和强大功能,并开始开发 Python 中端到端监督学习解决方案的道路。那么,让我们开始吧。
Jupyter 笔记本
数据科学开发环境的一个独特之处是使用 IPython Jupyter 笔记本 (jupyter.org),这与其他 Python 项目有所不同。Jupyter 笔记本提供了一种创建和共享交互式文档的方式,文档中可以包含实时执行的代码片段、图表以及通过 Latex (www.latex-project.org) 排版系统渲染的数学公式。本章节将介绍 Jupyter 笔记本及其一些关键特性,确保你的开发环境正确设置。
在本书中,我们将经常参考每个介绍的工具/包的文档。有效阅读和理解每个工具的文档非常重要。我们将使用的许多包包含了如此多的功能和实现细节,以至于很难记住它们的所有内容。以下文档可能对接下来的 Jupyter 笔记本部分有所帮助:
-
Anaconda 文档可以在
docs.anaconda.com找到。 -
Anaconda 用户指南可以在
docs.anaconda.com/anaconda/user-guide找到。 -
Jupyter 笔记本文档可以在
jupyter-notebook.readthedocs.io/en/stable/找到。
练习 1:启动 Jupyter 笔记本
在本练习中,我们将启动 Jupyter 笔记本。请确保你已按 前言 中的说明正确安装了 Python 3.7 版本的 Anaconda:
-
通过 Anaconda 启动 Jupyter 笔记本有两种方式。第一种方法是通过 Windows 开始菜单中的
Anaconda文件夹打开 Jupyter。点击http://localhost:8888,它将在默认的文件夹路径下启动。 -
第二种方法是通过 Anaconda 提示符启动 Jupyter。要启动 Anaconda 提示符,只需点击 Windows 开始菜单中的 Anaconda Prompt 菜单项,你应该会看到一个类似以下屏幕截图的弹出窗口:
![图 1.4:Anaconda 提示符]()
图 1.4:Anaconda 提示符
-
进入 Anaconda 提示符后,使用
cd(更改目录)命令切换到所需的目录。例如,要切换到Packt用户的Desktop目录,可以执行以下操作:C:\Users\Packt> cd C:\Users\Packt\Desktop -
进入所需的目录后,使用以下命令启动 Jupyter 笔记本:
C:\Users\Packt> jupyter notebook笔记本将会在你之前指定的工作目录中启动。这样,你可以在你选择的目录中浏览并保存你的笔记本,而不是默认的目录,默认目录因系统而异,但通常是你的主目录或
我的电脑目录。无论是通过何种方式启动 Jupyter,都会在你的默认浏览器中打开一个类似下面的窗口。如果目录中有现有文件,你也应该在这里看到它们:

图 1.5:Jupyter 笔记本启动窗口
练习 2:Hello World
Hello World 练习是一个必经之路,所以你当然不能错过这个体验!现在,让我们在这个练习中在 Jupyter 笔记本中打印出Hello World:
-
首先通过点击新建按钮并选择Python 3来创建一个新的 Jupyter 笔记本。Jupyter 允许你在同一个界面中运行不同版本的 Python 以及其他语言,如 R 和 Julia。我们也可以在这里创建新的文件夹或文本文件。但现在,我们将从一个 Python 3 笔记本开始:
![图 1.6:创建一个新笔记本]()
图 1.6:创建一个新笔记本
这将会在一个新的浏览器窗口中启动一个新的 Jupyter 笔记本。我们将首先花一些时间查看笔记本中可用的各种工具:
![图 1.7:新笔记本]()
图 1.7:新笔记本
每个 Jupyter 笔记本有三个主要部分,如下截图所示:标题栏(1)、工具栏(2)和文档正文(3)。我们按顺序来看一下这些组件:
![图 1.8:笔记本的组件]()
图 1.8:笔记本的组件
-
标题栏仅显示当前 Jupyter 笔记本的名称,并允许对笔记本进行重命名。点击
Hello World,然后点击重命名:![图 1.9:重命名笔记本]()
图 1.9:重命名笔记本
-
大多数情况下,工具栏包含所有你预期的常见功能。你可以在文件菜单中打开、保存和复制笔记本,或者创建新的 Jupyter 笔记本。在编辑菜单中,你可以查找替换、复制和剪切内容,在视图菜单中,你可以调整文档的显示方式。在我们讨论文档正文的同时,我们还会更详细地描述一些其他功能,例如插入、单元格和内核菜单中的功能。工具栏有一个部分需要进一步检查,即位于 Python 3 右侧的圆形轮廓区域。
将鼠标悬停在圆圈上,你将看到内核空闲的弹出窗口。这个圆圈是一个指示器,用来表示 Python 内核当前是否正在处理;当正在处理时,这个圆圈指示器将被填充。如果你怀疑某些操作正在运行或没有运行,你可以轻松查看这个图标获取更多信息。当 Python 内核没有运行时,你将看到这个:
![图 1.10:内核空闲]()
图 1.10:内核空闲
当 Python 内核正在运行时,你会看到这个:
![图 1.11:内核忙碌]()
图 1.11:内核忙碌
-
这将带我们进入文档的主体部分,实际的 notebook 内容将在这里输入。Jupyter notebooks 与标准的 Python 脚本或模块不同,它们被分成多个可执行的单元格。虽然 Python 脚本或模块在执行时会运行整个脚本,但 Jupyter notebooks 可以按顺序运行所有单元格,或者如果手动执行,还可以单独运行它们并以不同的顺序执行。
双击第一个单元格并输入以下内容:
>>> print('Hello World!') -
点击运行(或使用Ctrl + Enter 快捷键):

图 1.12:运行单元格
恭喜!你刚刚在 Jupyter notebook 中完成了Hello World。
练习 3:Jupyter Notebook 中的执行顺序
在上一个练习中,注意到print语句是在单元格下方执行的。现在让我们再深入一点。如前所述,Jupyter notebooks 由多个可单独执行的单元格组成;最好将它们视为你输入到 Python 解释器中的代码块,代码在你按下Ctrl + Enter 键之前不会执行。尽管代码在不同的时间运行,但所有的变量和对象都保持在 Python 内核的会话中。让我们进一步探讨这一点:
-
启动一个新的 Jupyter notebook,然后在三个单独的单元格中输入以下截图中显示的代码:
![图 1.13:在多个单元格中输入代码]()
图 1.13:在多个单元格中输入代码
-
点击
hello_world变量在第二个单元格中被声明(并执行),并保持在内存中,因此会在第三个单元格中打印出来。如前所述,你还可以不按顺序运行这些单元格。 -
点击第二个单元格,里面包含了
hello_world的声明,修改值,添加几个感叹号,然后重新运行该单元格:![图 1.14:更改第二个单元格的内容]()
图 1.14:更改第二个单元格的内容
注意第二个单元格现在是最新执行的单元格(
print语句后它没有更新)。要更新print语句,你需要执行其下方的单元格。警告:小心执行顺序。如果不小心,你很容易覆盖值或在变量首次使用之前在下方单元格中声明变量,因为在 notebooks 中,你不需要一次性运行整个脚本。因此,建议定期点击 Kernel | Restart & Run All。这将清除内存中的所有变量,并按顺序从上到下运行所有单元格。你还可以在 Cell 菜单中选择运行特定单元格下方或上方的所有单元格:![图 1.15:重启内核]()
图 1.15:重启内核
注意
写作和组织你的 notebook 单元格时,应该像是要按顺序从上到下依次运行它们一样。仅在调试/早期调查时使用手动单元格执行。
-
你还可以使用
hello_world变量左侧的上下箭头将单元格移动到其声明之前:![图 1.16:移动单元格]()
图 1.16:移动单元格
-
点击 Restart & Run All 单元格:

图 1.17:变量未定义错误
注意错误报告中显示变量未定义。这是因为它在声明之前被使用了。还要注意,错误后的单元格没有被执行,In [ ] 显示为空。
练习 4:Jupyter Notebooks 的优势
Jupyter notebooks 还有许多其他有用的功能。在本练习中,我们将探讨其中的一些功能:
-
Jupyter notebooks 可以通过在 Anaconda 提示符中包含感叹号前缀 (
!) 来直接执行命令。输入以下截图所示的代码并运行该单元格:![图 1.18:运行 Anaconda 命令]()
图 1.18:运行 Anaconda 命令
-
Jupyter notebooks 最棒的功能之一是能够创建包含可执行代码的实时报告。这不仅节省了防止分开创建报告和代码的时间,还能帮助传达正在完成的分析的准确性质。通过使用 Markdown 和 HTML,我们可以嵌入标题、章节、图片,甚至是用于动态内容的 JavaScript。
要在我们的 notebook 中使用 Markdown,首先需要更改单元格类型。首先,点击你想要更改为 Markdown 的单元格,然后点击 Code 下拉菜单,选择 Markdown:
![图 1.19:运行 Anaconda 命令]()
图 1.19:运行 Anaconda 命令
请注意,In [ ] 已经消失,单元格边框的颜色不再是蓝色。
-
现在您可以通过双击单元格并点击运行来输入有效的 Markdown 语法和 HTML,以渲染 Markdown。输入以下截图中显示的语法并运行单元格以查看输出:

图 1.20:Markdown 语法
输出如下所示:

图 1.21:Markdown 输出
注意
若要快速参考 Markdown,请查看本章的代码文件中的Markdown Syntax.ipynb Jupyter 笔记本。
Python 包和模块
尽管 Python 包含的标准功能确实非常丰富,但 Python 真正的力量在于额外的库(在 Python 中也称为包),由于开源许可证,可以通过几个简单的命令轻松下载和安装。在 Anaconda 安装中,这更加简单,因为许多最常见的包都预先在 Anaconda 中构建。您可以通过在笔记本单元格中运行以下命令来获取 Anaconda 环境中预安装包的完整列表:
!conda list
在本书中,我们将使用以下附加 Python 包:
-
NumPy(发音为Num Pie,可访问
www.numpy.org/):NumPy(即数值 Python)是 Python 科学计算的核心组件之一。NumPy 提供了基础数据类型,包括线性代数、向量和矩阵,以及关键的随机数功能。 -
SciPy(发音为Sigh Pie,可访问
www.scipy.org):SciPy 与 NumPy 一起,是核心科学计算包。SciPy 提供了许多统计工具、信号处理工具以及傅立叶变换等其他功能。 -
pandas(可访问
pandas.pydata.org/):pandas 是一个高性能库,用于加载、清理、分析和操作数据结构。 -
Matplotlib(可访问
matplotlib.org/):Matplotlib 是创建数据集的图形和图表的基础 Python 库,也是其他 Python 绘图库的基础包。Matplotlib API 与 Matlab 绘图库设计保持一致,以便轻松过渡到 Python。 -
Seaborn(可访问
seaborn.pydata.org/):Seaborn 是建立在 Matplotlib 之上的绘图库,提供吸引人的颜色和线条样式,以及许多常见的绘图模板。 -
Scikit-learn(可在
scikit-learn.org/stable/获取):Scikit-learn 是一个 Python 机器学习库,提供了一系列简单 API 的数据挖掘、建模和分析技术。Scikit-learn 包含了许多开箱即用的机器学习算法,包括分类、回归和聚类技术。
这些包构成了一个多功能的机器学习开发环境,每个包都提供了一套关键功能。如前所述,通过使用 Anaconda,您将已经安装并准备好所有必需的包。如果您需要一个在 Anaconda 安装中未包含的包,可以通过在 Jupyter 笔记本单元中输入并执行以下命令来安装:
!conda install <package name>
作为示例,如果我们想要安装 Seaborn,只需运行以下命令:
!conda install seaborn
要在笔记本中使用这些包,我们只需要导入它:
import matplotlib
pandas
如前所述,pandas 是一个用于加载、清洗和分析各种数据结构的库。正是由于 pandas 的灵活性,以及内置功能的丰富性,使其成为一个强大、流行且实用的 Python 包。它也是一个非常适合入门的包,因为显然,如果我们不先将数据加载到系统中,就无法对其进行分析。由于 pandas 提供了如此多的功能,使用该包的一个非常重要的技能就是能够阅读和理解文档。即使是多年使用 Python 编程和 pandas 的经验,我们仍然经常参考文档。API 中的功能如此广泛,以至于无法记住所有特性和实现细节。
注意
pandas 文档可以在 pandas.pydata.org/pandas-docs/stable/index.html 找到。
在 pandas 中加载数据
pandas 具有读取和写入多种文件格式和数据结构的能力,包括 CSV、JSON 和 HDF5 文件,以及 SQL 和 Python Pickle 格式。pandas 的输入/输出文档可以在pandas.pydata.org/pandas-docs/stable/user_guide/io.html找到。我们将继续通过加载 CSV 文件来探讨 pandas 的功能。本章使用的数据集是TITANIC: 机器学习灾难数据集,可以从www.kaggle.com/c/Titanic/data或github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python下载,该数据集包含了泰坦尼克号上乘客的名单以及他们的年龄、生存状态和兄弟姐妹/父母人数。在我们开始加载数据到 Python 之前,至关重要的是我们花些时间查看数据集提供的信息,以便全面了解其内容。请下载数据集并将其放置在你正在使用的目录中。
查看数据的描述,我们可以看到我们有以下字段可用:

图 1.22: 泰坦尼克数据集中的字段
我们还提供了一些额外的上下文信息:
-
pclass:这是社会经济地位的代理变量,其中头等舱为上层,中等舱为中层,三等舱为下层。 -
age:如果年龄小于 1,这是一个分数值;例如,0.25表示 3 个月。如果年龄是估算的,通常以xx.5的形式表示。 -
sibsp:兄弟姐妹定义为兄弟、姐妹、继兄或继姐,配偶定义为丈夫或妻子。 -
parch:父母是指母亲或父亲,孩子是指女儿、儿子、继女或继子。只有与保姆一起旅行的儿童才不与父母一起旅行。因此,这一字段为0。 -
embarked:登船地点是乘客登船的地点。
请注意,数据集提供的信息没有说明数据是如何收集的。survival、pclass和embarked字段被称为类别变量,因为它们被分配到固定数量的标签或类别中,用于表示其他信息。例如,在embarked字段中,C标签表示乘客在谢尔堡登船,而survival字段中的值1表示他们在沉船事故中幸存。
练习 5: 加载和总结泰坦尼克数据集
在本练习中,我们将把泰坦尼克数据集读入 Python,并对其执行一些基本的总结操作:
-
使用简写符号导入 pandas 包,如下图所示:
![图 1.23: 导入 pandas 包]()
图 1.23:导入 pandas 包
-
在 Jupyter notebook 首页中点击
titanic.csv文件以打开它:![图 1.24:打开 CSV 文件]()
图 1.24:打开 CSV 文件
该文件是 CSV 文件,可以视为一个表格,其中每一行是表格中的一行,每个逗号分隔表格中的列。幸运的是,我们不需要以原始文本形式处理这些表格,可以使用 pandas 将其加载:
![图 1.25:CSV 文件的内容]()
图 1.25:CSV 文件的内容
注意
花点时间查看 pandas 文档中的
read_csv函数,地址:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html。请注意,加载 CSV 数据到 pandas DataFrame 中有多种不同的选项。 -
在可执行的 Jupyter notebook 单元格中,执行以下代码来从文件加载数据:
df = pd.read_csv('Titanic.csv')pandas DataFrame 类提供了一整套属性和方法,可以在其自身内容上执行,范围涵盖排序、过滤、分组方法到描述性统计分析、绘图和转换等功能。
注意
打开并阅读关于 pandas DataFrame 对象的文档,地址:
pandas.pydata.org/pandas-docs/stable/reference/frame.html。 -
使用 DataFrame 的
head()方法读取前五行数据:df.head()

图 1.26:读取前五行
在这个示例中,我们可以看到 DataFrame 中信息的可视化表示。我们可以看到数据是按表格形式组织的,几乎像一个电子表格。不同类型的数据按列组织,每个样本按行组织。每一行都有一个索引值,并且在 DataFrame 的左侧以粗体数字 0 到 4 显示。每一列都有一个标签或名称,如同在 DataFrame 顶部以粗体显示的那样。
将 DataFrame 看作一种电子表格是一个合理的类比;正如我们将在本章中看到的那样,我们可以像在电子表格程序中一样对数据进行排序、过滤和计算。虽然本章没有涉及,但有趣的是,DataFrame 还包含数据透视表功能,就像电子表格一样(pandas.pydata.org/pandas-docs/stable/reference/api/pandas.pivot_table.html)。
练习 6:索引和选择数据
现在我们已经加载了一些数据,让我们使用 DataFrame 的选择和索引方法来访问一些感兴趣的数据:
-
以类似普通字典的方式选择单独的列,方法是使用列的标签,如下所示:
df['Age']![图 1.27:选择 Age 列]()
图 1.27:选择 Age 列
如果列名中没有空格,我们也可以使用点操作符。如果列名中有空格,则需要使用括号表示法:
df.Age![图 1.28:使用点操作符选择 Age 列]()
图 1.28:使用点操作符选择 Age 列
-
使用括号表示法一次选择多个列,如下所示:
df[['Name', 'Parch', 'Sex']]![图 1.29:选择多个列]()
图 1.29:选择多个列
-
使用
iloc选择第一行:df.iloc[0]![图 1.30:选择第一行]()
图 1.30:选择第一行
-
使用
iloc选择前三行:df.iloc[[0,1,2]]![图 1.31:选择前三行]()
图 1.31:选择前三行
-
我们还可以获取所有可用列的列表。按以下方式操作:
columns = df.columns # Extract the list of columns print(columns)![图 1.32:获取所有列]()
图 1.32:获取所有列
-
使用这个列列表和标准的 Python 切片语法来获取第 2、3 和 4 列及其对应的值:
df[columns[1:4]] # Columns 2, 3, 4![图 1.33:获取第二、第三和第四列]()
图 1.33:获取第二、第三和第四列
-
使用
len操作符获取 DataFrame 中的行数:len(df)![图 1.34:获取行数]()
图 1.34:获取行数
-
如果我们想要获取
Fare列在第2行的值,该怎么做呢?有几种不同的方法。首先,我们将尝试行中心方法。按以下方式操作:df.iloc[2]['Fare'] # Row centric![图 1.35:使用正常的行中心方法获取特定值]()
图 1.35:使用正常的行中心方法获取特定值
-
尝试使用点操作符来选择列。按以下方式操作:
df.iloc[2].Fare # Row centric![图 1.36:使用行中心点操作符获取特定值]()
图 1.36:使用行中心点操作符获取特定值
-
尝试使用列中心方法。按以下方式操作:
df['Fare'][2] # Column centric![图 1.37:使用正常的列中心方法获取特定值]()
图 1.37:使用正常的列中心方法获取特定值
-
尝试使用列中心方法与点操作符。按以下方式操作:
df.Fare[2] # Column centric

图 1.38:使用列中心点操作符获取特定值
练习 7:高级索引和选择
在掌握了索引和选择的基础知识后,我们可以将注意力转向更高级的索引和选择。在这个练习中,我们将介绍一些执行高级索引和选择数据的重要方法:
-
为年龄小于 21 岁的乘客创建一个包含姓名和年龄的列表,如下所示:
child_passengers = df[df.Age < 21][['Name', 'Age']] child_passengers.head()![图 1.39:列出所有 21 岁以下乘客的姓名和年龄]()
图 1.39:列出所有 21 岁以下乘客的姓名和年龄
-
计算儿童乘客的数量,如下所示:
print(len(child_passengers))![图 1.40:儿童乘客计数]()
图 1.40:儿童乘客计数
-
计算年龄在 21 到 30 岁之间的乘客数量。不要使用 Python 的
and逻辑运算符,而是使用与符号(&)。操作如下:young_adult_passengers = df.loc[ (df.Age > 21) & (df.Age < 30) ] len(young_adult_passengers)![图 1.41:计算年龄在 21 到 30 岁之间的乘客]()
图 1.41:计算年龄在 21 到 30 岁之间的乘客
-
计算那些持有一等舱或三等舱票的乘客。再次提醒,我们不会使用 Python 的
or逻辑运算符,而是使用管道符号(|)。操作如下:df.loc[ (df.Pclass == 3) | (df.Pclass ==1) ]![图 1.42:计算那些持有一等舱或三等舱票的乘客]()
图 1.42:计算那些持有一等舱或三等舱票的乘客
-
计算那些既没有持有一等舱也没有持有三等舱票的乘客人数。不要简单地选择二等舱票的持有者,而是使用
~符号作为not逻辑运算符。操作如下:df.loc[ ~((df.Pclass == 3) | (df.Pclass ==1)) ]![图 1.43:计算那些既没有持有一等舱也没有持有三等舱票的乘客]()
图 1.43:计算那些既没有持有一等舱也没有持有三等舱票的乘客
-
我们不再需要
Unnamed: 0列,因此可以使用del运算符将其删除:del df['Unnamed: 0'] df.head()

图 1.44:del 运算符
pandas 方法
现在我们已经掌握了一些 pandas 基础知识以及一些更高级的索引和选择工具,让我们来看一下其他 DataFrame 方法。有关所有可用方法的完整列表,我们可以参考类文档。
注
pandas 文档可以在pandas.pydata.org/pandas-docs/stable/reference/frame.html找到。
现在你应该知道 DataFrame 中有哪些方法可用。本章内容太多,无法详细介绍所有方法,因此我们将选择一些方法,帮助你在监督学习中起步。
我们已经看过使用head()方法,它可以提供 DataFrame 的前五行。如果我们希望选择更多或更少的行,可以通过提供行数作为参数来实现,如下所示:
df.head(n=20) # 20 lines
df.head(n=32) # 32 lines
另一个有用的方法是describe,它是快速获取 DataFrame 中数据描述性统计信息的方式。我们接下来可以看到,对于 DataFrame 中的所有数值数据列(注意文本列已被省略),返回了样本量(count)、均值、最小值、最大值、标准差以及 25%、50%和 75%的分位数:
df.describe()

图 1.45:describe 方法
请注意,只有数值型数据列包含在汇总中。这个简单的命令为我们提供了很多有用的信息;查看count的值(它统计有效样本的数量),我们可以看到Age类别中有 1,046 个有效样本,Fare中有 1,308 个,Survived中只有 891 个。我们可以看到最年轻的人是 0.17 岁,平均年龄是 29.898 岁,最年长的是 80 岁。最低票价是£0,平均票价为£33.30,最贵票价为£512.33。如果我们查看Survived列,我们有 891 个有效样本,均值为 0.38,这意味着大约 38%的人存活。
我们还可以通过调用 DataFrame 的各自方法,单独获取每一列的这些值,如下所示:
df.count()

图 1.46:count 方法
但我们有些列包含文本数据,如Embarked、Ticket、Name和Sex。那这些呢?我们怎么获取这些列的描述性信息呢?我们仍然可以使用describe,只需要为其提供更多信息。默认情况下,describe只会包含数值列,并计算第 25、第 50 和第 75 百分位数。但我们可以通过传递include = 'all'参数来配置它,包含文本列,如下所示:
df.describe(include='all')

图 1.47:带有文本列的 describe 方法
现在好多了——我们有更多的信息了。查看Cabin列,我们可以看到有 295 条记录,其中 186 个唯一值。最常见的值是C32、C25和C27,它们出现了 6 次(根据freq值)。类似地,如果我们查看Embarked列,我们可以看到有 1,307 条记录,3 个唯一值,最常出现的值是S,出现了 914 次。
请注意我们在describe输出表格中出现的NaN值。NaN,即非数字,在 DataFrame 中非常重要,因为它们表示缺失或不可用的数据。pandas 库能够读取包含缺失或不完整信息的数据源既是一个优势也是一个弊端。许多其他库会在缺失信息的情况下直接无法导入或读取数据文件,而它能够读取数据也意味着必须适当地处理这些缺失的数据。
查看describe方法的输出时,你应该注意到 Jupyter Notebook 将其呈现的方式与我们通过read_csv读取的原始 DataFrame 相同。这样做是有充分理由的,因为describe方法返回的结果本身就是一个 pandas DataFrame,因此它具有与从 CSV 文件中读取的数据相同的方法和特性。你可以通过 Python 内建的type函数轻松验证这一点:

图 1.48:检查类型
现在我们已经有了数据集的概览,接下来让我们深入一些,详细了解可用数据。
注意
对可用数据的全面理解在任何监督学习问题中都是至关重要的。数据的来源和类型、收集数据的方式以及收集过程中可能出现的任何错误都会影响最终模型的表现。
希望到目前为止,您已经能够熟练使用 pandas 提供数据的高层次概览。接下来我们将花些时间更深入地分析这些数据。
练习 8:拆分、应用和合并数据源
我们已经看到如何从 DataFrame 中索引或选择行或列,并使用高级索引技术根据特定标准过滤可用数据。另一个有用的方法是 groupby 方法,它提供了一种快速选择一组数据的方法,并通过 DataFrameGroupBy 对象提供了额外的功能:
-
使用
groupby方法按Embarked列对数据进行分组。Embarked列有多少个不同的值?让我们来看一下:embarked_grouped = df.groupby('Embarked') print(f'There are {len(embarked_grouped)} Embarked groups')![图 1.49:按 Embarked 列对数据进行分组]()
图 1.49:按 Embarked 列对数据进行分组
-
groupby方法到底做了什么?我们来看看。显示embarked_grouped.groups的输出:embarked_grouped.groups![图 1.50:embarked_grouped.groups 的输出]()
图 1.50:embarked_grouped.groups 的输出
在这里我们可以看到,三个组是
C、Q和S,而embarked_grouped.groups实际上是一个字典,字典的键是这些组,值是属于该组的行或索引。 -
使用
iloc方法检查第1行,并确认它属于 Embarked 组C:df.iloc[1]![图 1.51:检查第 1 行]()
图 1.51:检查第 1 行
-
由于这些组是字典,我们可以遍历它们,并对各个组执行计算。计算每个组的平均年龄,如下所示:
for name, group in embarked_grouped: print(name, group.Age.mean())![图 1.52:使用迭代计算每个组的平均年龄]()
图 1.52:使用迭代计算每个组的平均年龄
-
另一种选择是使用
aggregate方法,简称agg,并提供要应用到列上的函数。使用agg方法来计算每个组的均值:embarked_grouped.agg(np.mean)![图 1.53:使用 agg 方法]()
图 1.53:使用 agg 方法
那么,
agg到底是如何工作的,我们可以传递什么类型的函数给它呢?在回答这些问题之前,我们需要首先考虑 DataFrame 中每列的数据类型,因为每列都会传递给此函数,以生成我们在此看到的结果。每个 DataFrame 由一组 pandas 系列数据列组成,这在许多方面类似于列表。因此,任何可以接受列表或类似可迭代对象并计算出一个单一值的函数,都可以与agg一起使用。 -
例如,定义一个简单的函数,返回列中的第一个值,然后将该函数传递给
agg:def first_val(x): return x.values[0] embarked_grouped.agg(first_val)

图 1.54:使用 agg 方法与函数
Lambda 函数
使用 Lambda 函数实现agg是一种常见且实用的方法。
def关键字。Lambda 函数本质上是为了方便而提供的,并不打算长期使用。Lambda 函数的标准语法如下(始终以lambda关键字开头):
lambda <input values>: <computation for values to be returned>
练习 9:Lambda 函数
在本练习中,我们将创建一个 Lambda 函数,该函数返回列中的第一个值,并与agg一起使用:
-
将
first_val函数写为一个 Lambda 函数,并传递给agg:embarked_grouped.agg(lambda x: x.values[0])![图 1.55:使用 agg 方法与 Lambda 函数]()
图 1.55:使用 agg 方法与 Lambda 函数
显然,我们得到了相同的结果,但请注意,Lambda 函数的使用更加便捷,尤其是考虑到它本意仅用于短时间的操作。
-
我们还可以通过列表将多个函数传递给
agg,以便对整个数据集应用这些函数。传递 Lambda 函数以及 NumPy 的均值和标准差函数,如下所示:embarked_grouped.agg([lambda x: x.values[0], np.mean, np.std])![图 1.56:使用 agg 方法与多个 Lambda 函数]()
图 1.56:使用 agg 方法与多个 Lambda 函数
-
如果我们想对 DataFrame 中的不同列应用不同的函数怎么办?可以通过向
agg传递一个字典,字典的键是需要应用函数的列,而值是相应的函数,从而将numpy.sum应用到Fare列,将 Lambda 函数应用到Age列:embarked_grouped.agg({ 'Fare': np.sum, 'Age': lambda x: x.values[0] })![图 1.57:使用 agg 方法与包含不同列的字典]()
图 1.57:使用 agg 方法与包含不同列的字典
-
最后,您还可以使用多个列来执行
groupby方法。向方法提供一个包含列(Sex和Embarked)的列表进行groupby,如下所示:age_embarked_grouped = df.groupby(['Sex', 'Embarked']) age_embarked_grouped.groups

图 1.58:使用 groupby 方法与多个列
类似于我们仅通过 Embarked 列进行分组时的情况,我们可以看到这里返回的是一个字典,其中键是Sex和Embarked列的组合,作为元组返回。字典中的第一个键值对是一个元组 ('Male', 'S'),值对应的是具有该特定组合的行的索引。对于Sex和Embarked列中每个唯一值的组合,都将有一个键值对。
数据质量考虑因素
在任何机器学习问题中,无论是监督学习还是无监督学习,数据的质量对最终模型的表现至关重要,应该在规划任何机器学习项目时放在首位。简单的经验法则是,如果你拥有干净的数据,数据量足够,而且输入数据类型与期望输出之间有良好的相关性,那么关于所选监督学习模型的类型和细节就变得不那么重要,仍然可以获得良好的结果。
然而,实际上这种情况很少发生。通常会涉及到一些关于数据量、数据质量或信噪比、输入和输出之间的相关性,或者这些因素的某种组合。因此,我们将利用本章的最后部分来讨论可能出现的一些数据质量问题以及一些解决这些问题的机制。之前我们提到过,在任何机器学习问题中,彻底理解数据集是至关重要的,尤其是在构建高性能模型时。当涉及到数据质量并试图解决数据中的一些问题时,这一点尤其重要。如果没有对数据集的全面了解,在数据清理过程中可能会引入额外的噪声或其他意外问题,从而导致性能进一步下降。
注意
关于 Titanic 数据集的详细描述以及其中包含的数据类型,详见 在 pandas 中加载数据 部分。如果你需要快速回顾这些细节,请现在回去查看。
处理缺失数据
正如我们之前讨论的,pandas 处理缺失数据的能力既是一个福音也是一个诅咒,这无疑是我们在继续开发监督学习模型之前需要管理的最常见问题。最简单的做法(但不一定是最有效的)是直接删除或忽略缺失数据的条目。我们可以通过 pandas 中 dropna 方法轻松实现这一点:
complete_data = df.dropna()
简单地丢弃缺失数据的行有一个非常重大的后果,那就是我们可能会丢失很多重要的信息。这在泰坦尼克数据集中非常明显,因为很多行都包含缺失数据。如果我们简单地忽略这些行,我们将从 1,309 个样本开始,最终只剩下 183 个样本。在仅使用不到 10%的数据的情况下,开发一个合理的监督学习模型将变得非常困难:

图 1.59:行总数和含有 NaN 值的行总数
所以,除了早期的探索性阶段,简单地丢弃所有包含无效信息的行通常是不被接受的。不过,我们可以对此稍微做得更复杂一些。到底是哪些行缺失了信息?缺失信息的问题是某些特定列独有的,还是贯穿整个数据集的所有列?我们也可以使用aggregate来帮助我们:
df.aggregate(lambda x: x.isna().sum())

图 1.60:使用 agg 与 Lambda 函数识别含有 NaN 值的行
现在,这很有用!我们可以看到,大多数缺失的信息都在Cabin列中,一些在Age列中,还有一点在Survived列中。这是数据清洗过程中第一次我们可能需要做出有根据的判断。
我们该如何处理Cabin列呢?这里缺失的信息太多,实际上可能无法以任何合理的方式使用它。我们可以尝试通过查看姓名、年龄和父母/兄弟姐妹的数量来恢复信息,看看能否将一些家庭联系起来提供信息,但这个过程会充满不确定性。我们也可以通过使用船舱的等级而不是具体的船舱号来简化该列,这可能与姓名、年龄和社会地位更好地相关。这是令人遗憾的,因为Cabin与Survived之间可能有很好的相关性,或许船舱较低层的乘客可能更难撤离。我们也可以仅查看包含有效Cabin值的行,看看Cabin条目是否具有任何预测能力;但现在,我们会暂时忽略Cabin作为一个合理的输入(或特征)。
我们可以看到,Embarked和Fare列之间只有三个缺失的样本。如果我们决定需要Embarked和Fare列来进行建模,那么仅仅丢弃这些行是一个合理的做法。我们可以使用索引技巧来完成这项操作,其中~表示not操作,或者翻转结果(即,df.Embarked不是NaN并且df.Fare不是NaN):
df_valid = df.loc[(~df.Embarked.isna()) & (~df.Fare.isna())]
缺失的年龄值稍微有些复杂,因为有太多行缺少年龄值,不能仅仅把它们丢弃。但我们在这里有更多的选择,因为我们可以对一些合理的值有更多的信心来填充。最简单的选项是直接用数据集的平均年龄填补缺失的年龄值:
df_valid[['Age']] = df_valid[['Age']].fillna(df_valid.Age.mean())
这样做没问题,但可能有更好的方法来填充数据,而不是直接给所有 263 个人相同的值。记住,我们的目标是清理数据,以最大化输入特征的预测能力和生存率。虽然这种做法简单,但似乎不太合理。如果我们考虑每个类别(Pclass)成员的平均年龄呢?这可能会给出一个更好的估算,因为从类别 1 到类别 3,平均年龄逐渐减少:

图 1.61: 各个类别成员的平均年龄
如果我们考虑性别以及票种类(社会地位)呢?平均年龄在这里也有差异吗?让我们来看看:
for name, grp in df_valid.groupby(['Pclass', 'Sex']):
print('%i' % name[0], name[1], '%0.2f' % grp['Age'].mean())

图 1.62: 各性别和类别成员的平均年龄
我们可以看到,所有票种类的男性通常年龄较大。性别和票种类的组合提供了比简单地用平均年龄填补所有缺失值更高的分辨率。为了实现这一点,我们将使用transform方法,它将一个函数应用于序列或数据框的内容,并返回一个包含转换后值的序列或数据框。将它与groupby方法结合使用时尤其强大:
mean_ages = df_valid.groupby(['Pclass', 'Sex'])['Age'].\
transform(lambda x: x.fillna(x.mean()))
df_valid.loc[:, 'Age'] = mean_ages
这两行代码中有很多内容,所以让我们将其分解为几个部分。我们先看第一行:
mean_ages = df_valid.groupby(['Pclass', 'Sex'])['Age'].\
transform(lambda x: x.fillna(x.mean()))
我们已经熟悉df_valid.groupby(['Pclass', 'Sex'])['Age'],它根据票种类和性别对数据进行分组,并仅返回Age列。lambda x: x.fillna(x.mean()) Lambda 函数接受输入的 pandas 序列,并用该序列的均值填充NaN值。
第二行将mean_ages中的填充值赋给Age列。请注意使用了loc[:, 'Age']索引方法,这表示Age列中的所有行都将被赋予mean_ages中包含的值:
df_valid.loc[:, 'Age'] = mean_ages
我们已经描述了几种填补Age列缺失值的方法,但这并不是一个详尽的讨论。我们还有很多其他方法可以用来填补缺失数据:我们可以为分组数据应用均值一个标准差范围内的随机值,也可以根据性别、父母/子女数量(Parch)或兄弟姐妹数量,或者根据舱位、性别和父母/子女数量对数据进行分组。这个过程中最重要的决策是最终预测准确度的结果。在这个过程中,我们可能需要尝试不同的选项,重新运行模型并考虑其对最终预测准确度的影响。这是特征工程过程中一个重要的方面,即选择能为模型提供最大预测能力的特征或组件;在这个过程中,你会尝试不同的特征,运行模型,查看最终结果并重复,直到你对模型的表现感到满意。
这个监督学习问题的最终目标是根据我们可用的信息预测泰坦尼克号乘客的生还情况。因此,这意味着Survived列提供了我们训练的标签。如果我们缺失了 418 个标签,我们该怎么办?如果这是一个我们可以控制数据收集并访问其来源的项目,我们显然可以通过重新收集数据或要求澄清标签来纠正这一点。在泰坦尼克号数据集中,我们无法做到这一点,因此必须做出另一个有根据的判断。我们可以尝试一些无监督学习技术,看看是否能发现一些可以用于生还信息的模式。然而,我们可能别无选择,只能忽略这些行。我们的任务是预测一个人是否生还,而不是他们是否可能生还。通过估算真实标签,我们可能会给数据集引入显著的噪音,降低我们准确预测生还情况的能力。
类别不平衡
缺失数据并不是数据集可能存在的唯一问题。类别不平衡——即某一类别或多个类别的样本数远多于其他类别——可能是一个显著的问题,特别是在分类问题中(我们将在第四章,分类中深入讨论分类问题),在这些问题中,我们试图预测一个样本属于哪个类别(或哪些类别)。查看我们的Survived列,我们可以看到数据集中死亡人数(Survived为0)远多于生还人数(Survived为1):

图 1.63: 死亡与生还人数对比
如果我们不考虑类别不平衡,模型的预测能力可能会大大降低,因为在训练过程中,模型只需要猜测“该人未能幸存”就可以正确预测 61%(549 / (549 + 342))的时间。如果现实中的实际生存率是 50%,那么当模型应用于未见数据时,可能会过度预测未幸存的情况。
有几种方法可以用来管理类别不平衡,其中一种方法与缺失数据情境类似,就是随机从过多样本的类别中删除样本,直到达到平衡。再说一次,这个选项并不理想,甚至可能不合适,因为它涉及到忽略可用数据。一个更具建设性的例子是通过从数据集中随机复制样本,来对不足样本的类别进行过采样,从而增加样本数量。虽然删除数据可能会导致由于丢失有用信息而出现准确性问题,但对不足样本类别进行过采样可能会导致无法预测未见数据的标签,这也叫做过拟合(我们将在第五章,集成建模中讨论)。
向过采样数据的输入特征添加一些随机噪声可能有助于防止一定程度的过拟合,但这高度依赖于数据集本身。与缺失数据一样,检查任何类别不平衡修正对整体模型性能的影响非常重要。使用append方法将更多数据复制到 DataFrame 中相对简单,它的工作方式与列表非常相似。如果我们想把第一行复制到 DataFrame 的末尾,我们可以这样做:
df_oversample = df.append(df.iloc[0])
样本量不足
机器学习领域可以看作是更大统计学领域的一个分支。因此,置信度和样本量的原理同样可以应用于理解小数据集的问题。回想一下,如果我们从一个具有高方差的数据源中采样,那么测量值的不确定性程度也会很高,并且为了对均值值达到指定的置信度,需要更多的样本。这些样本原理也可以应用到机器学习数据集中。那些具有最具预测力的特征方差的数据集通常需要更多的样本来获得合理的性能,因为也需要更多的置信度。
有几种技术可以用来弥补样本量不足的问题,例如迁移学习。然而,这些技术超出了本书的范围。不过,最终来说,使用小数据集能做的事情有限,显著的性能提升可能只有在样本量增加后才会出现。
活动 1:pandas 函数
在这个活动中,我们将测试自己在本章中学习到的各种 pandas 函数。我们将使用相同的 Titanic 数据集进行测试。
要执行的步骤如下:
-
打开一个新的 Jupyter 笔记本。
-
使用 pandas 加载泰坦尼克号数据集,并描述所有列的摘要数据。
-
我们不需要
Unnamed: 0列。在练习 7:高级索引和选择中,我们展示了如何使用del命令删除该列。我们还可以通过其他方式删除此列?不使用del命令删除此列。 -
计算数据框列的均值、标准差、最小值和最大值,而不使用
describe方法。 -
那么 33%、66%和 99%的四分位数呢?我们如何使用各自的方法得到这些值?使用
quantile方法来完成此操作(pandas.pydata.org/pandas-docs/stable/reference/frame.html)。 -
每个类别的乘客有多少?使用
groupby方法找到答案。 -
每个类别的乘客有多少?通过选择/索引方法统计每个类别的成员数量,找到答案。
确认步骤 6和步骤 7的答案是否匹配。
-
确定第三类中最年长的乘客是谁。
-
对于许多机器学习问题,常常需要将数值缩放到 0 和 1 之间。使用带有 Lambda 函数的
agg方法将Fare和Age列缩放到 0 和 1 之间。 -
数据集中有一名个体没有列出
Fare值,可以通过以下方法找出。df_nan_fare = df.loc[(df.Fare.isna())] df_nan_fare输出结果如下:

图 1.64:没有列出Fare值的个体
使用groupby方法,将主数据框中这一行的NaN值替换为与相同类别和Embarked位置对应的Fare平均值。
注意
该活动的解决方案可以在第 300 页找到。
总结
本章介绍了监督学习机器学习的概念,并列举了一些应用案例,包括自动化手动任务,如识别 1960 年代和 1980 年代的发型。在此介绍中,我们接触到了标签数据集的概念,以及将一个信息集(输入数据或特征)映射到相应标签的过程。
我们采用了实际操作的方法,通过 Jupyter 笔记本和强大的 pandas 库加载和清洗数据。需要注意的是,本章仅涵盖了 pandas 功能的一小部分,实际上整个书籍都可以专门讨论该库。建议你熟悉 pandas 文档,并通过实践不断提升你的 pandas 技能。
本章的最后部分讨论了数据质量问题,这些问题在开发高效的监督学习模型时需要考虑,包括缺失数据、类别不平衡和样本量过小。我们讨论了多种处理这些问题的方法,并强调了根据模型表现检查这些缓解措施的重要性。
在下一章,我们将扩展我们所涵盖的数据清理过程,并将探讨数据探索和可视化过程。数据探索是任何机器学习解决方案中的关键环节,因为没有对数据集的全面了解,几乎不可能对提供的信息进行建模。
第二章:探索性数据分析与可视化
学习目标
到本章结束时,你将能够:
-
解释数据探索的重要性并传达数据集的总结性统计信息
-
可视化数据中缺失值的模式,并能够适当替换空值
-
识别连续特征和类别特征
-
可视化单个变量值的分布
-
使用相关性和可视化分析描述不同类型变量之间的关系
本章将带领我们进行对新数据集的探索与分析。
介绍
假设我们有一个问题陈述,涉及预测某次地震是否引发了海啸。我们如何决定使用什么模型?我们知道我们拥有的数据是什么吗?什么都不知道!但如果我们不了解数据,最终可能会构建一个既难以解释又不可靠的模型。
在数据科学中,深入理解我们处理的数据非常重要,这样可以生成高度信息量的特征,从而构建准确且强大的模型。
为了获得这种理解,我们对数据进行探索性分析,看看数据能告诉我们特征与目标变量之间的关系。了解数据甚至有助于我们解释构建的模型,并找出改善其准确性的方法。
我们实现这一目标的方法是让数据揭示其结构或模型,这有助于我们获得一些新的、常常是意想不到的数据洞察。让我们深入了解这种方法。
探索性数据分析(EDA)
探索性数据分析(EDA)被定义为一种分析数据集的方法,旨在总结其主要特征,通常采用可视化方法。
EDA 的目的:
-
发现数据集中的模式
-
发现异常
-
对数据的行为形成假设
-
验证假设
从基础的总结性统计数据到复杂的可视化图形,都帮助我们对数据本身形成直观的理解,这在形成新的假设和揭示哪些参数影响目标变量时非常重要。通常,通过发现目标变量如何随单一特征的变化而变化,可以帮助我们判断某个特征的重要性,而多个特征组合的变化则帮助我们构思新的有用特征。
大多数探索和可视化的目的是理解特征与目标变量之间的关系。因为我们想要找出我们拥有的数据和我们想要预测的值之间存在哪些(或不存在的)关系。
一些非常基本的领域知识通常是必要的,以便能够理解问题陈述本身以及数据所传达的信息。在本章中,我们将探索通过分析我们拥有的特征,了解更多关于数据的方式。
EDA 可以告诉我们:
-
不干净、缺失值或存在异常值的特征
-
具有信息价值并且是目标的良好指示器的特征
-
特征与目标之间的关系类型
-
数据可能需要的其他特征,而这些特征我们现在还没有
-
你可能需要单独处理的边缘情况
-
你可能需要在数据集上应用的过滤器
-
错误或虚假的数据点的存在
现在我们已经了解了为什么 EDA 很重要以及它能告诉我们什么,接下来让我们讨论一下 EDA 具体包括什么内容。EDA 可以包括从查看基本的摘要统计到可视化多个变量之间复杂趋势的任何操作。然而,即使是简单的统计数据和图表也可以是强大的工具,因为它们可能揭示出数据中的重要事实,这些事实可能会改变我们建模的视角。当我们看到表示数据的图表时,我们能够轻松地检测到趋势和模式,相比于仅仅处理原始数据和数字。这些可视化进一步让我们能够提出类似“如何?”和“为什么?”的问题,并对数据集形成假设,这些假设可以通过进一步的可视化来验证。这是一个持续的过程,能够帮助我们更深入地理解数据。本章将向你介绍一些可以用来探索任何数据集的基本工具,同时牢记最终的问题陈述。
我们将从一些基本的摘要统计量开始,介绍如何解读它们,接着是关于查找、分析和处理缺失值的部分。然后我们将研究单变量关系,即单个变量的分布和行为。最后,我们会探讨变量之间关系的探索部分。本章将向你介绍可以用来获得数据集及其特征基本概览的图表类型,以及如何通过创建结合多个特征的可视化来获得见解,我们还将通过一些示例展示它们如何使用。
我们将用于探索性分析和可视化的数据集来自 NOAA 的重大地震数据库,该数据集作为公共数据集可在 Google BigQuery 上获取(表 ID:'bigquery-public-data.noaa_significant_earthquakes.earthquakes')。我们将使用其中部分列,相关元数据可以在console.cloud.google.com/bigquery?project=packt-data&folder&organizationId&p=bigquery-public-data&d=noaa_significant_earthquakes&t=earthquakes&page=table查看,并将其加载到 pandas DataFrame 中以进行探索。我们主要使用 Matplotlib 来进行大多数可视化,同时也会使用 Seaborn 和 Missingno 进行部分可视化。然而需要注意的是,Seaborn 只是对 Matplotlib 功能的封装,因此任何使用 Seaborn 绘制的图表都可以使用 Matplotlib 绘制。我们会通过混合使用两个库的可视化方式,来保持事情的趣味性。
探索和分析将以一个示例问题为基础进行:给定我们拥有的数据,我们希望预测地震是否引发了海啸。这将是一个分类问题(更多内容请见第四章,分类),其中目标变量是flag_tsunami列。
练习 10:导入数据探索所需的库
在我们开始之前,首先导入我们将用于大多数数据操作和可视化的必要库:
-
在 Jupyter Notebook 中,导入以下库:
import json import pandas as pd import numpy as np import missingno as msno from sklearn.impute import SimpleImputer %matplotlib inline import matplotlib.pyplot as plt import seaborn as sns%matplotlib inline命令允许 Jupyter 在 Notebook 中直接显示图表。 -
我们还可以读取包含每列数据类型的元数据,这些数据类型以 JSON 文件的形式存储。可以使用以下命令完成此操作。该命令将以可读格式打开文件,并使用
json库将文件读入字典:with open('dtypes.json', 'r') as jsonfile: dtyp = json.load(jsonfile)
现在,让我们开始吧。
汇总统计与中心值
为了了解我们的数据到底是什么样子,我们使用一种叫做数据分析的技术。数据分析被定义为检查现有信息源(例如数据库或文件)中的数据,并收集关于该数据的统计信息或信息摘要的过程。目标是确保你充分理解你的数据,并能够尽早识别数据可能带来的挑战,这通过总结数据集并评估其结构、内容和质量来实现。
数据分析包括收集描述性统计和数据类型。以下是一些常用的命令,可以用来获取数据集的总结信息:
-
data.info():此命令告诉我们每列中有多少非空值,并显示每列值的数据类型(非数值类型以object类型表示)。 -
data.describe():该命令为 DataFrame 中所有数值列提供基本的总结统计数据,例如非空值的计数、最小值和最大值、均值和标准差,以及所有数值特征的四分位数百分位数。如果有任何字符串类型的特征,则不包括这些特征的总结。 -
data.head()和data.tail():这两个命令分别显示 DataFrame 的前五行和后五行数据。虽然前面的命令可以给我们一个数据集的总体概览,但更深入地了解实际数据本身是一个好主意,可以使用这些命令来完成。
标准差
标准差表示 x 的值分布的广泛程度。
对于一组数值 xi,标准差由以下公式给出:

图 2.1:标准差公式
这里,𝝈 是标准差,N 是数据点的数量,𝝁 是均值。
假设我们有一组 10 个值,x = [0,1,1,2,3,4,2,2,0,1]。均值 𝝁 将是这些值的总和除以 10。也就是说,𝝁 = 1.6:

图 2.2:x 的均方值
然后,标准差 = sqrt(14.4/10) = 1.2。
百分位数
对于一组值,n**th 百分位数是大于该值的数值所占比例为 n% 的数据点。例如,第 50th 百分位数是数据集中大于该值和小于该值的元素数量相同的值。此外,数据集的第 50 百分位数也称为其中位数,第 25 百分位数和第 75 百分位数也称为下四分位数和上四分位数。
假设我们有与之前相同的 10 个值,x = [0,1,1,2,3,4,2,2,0,1]。我们先对这个值列表进行排序。排序后,得到 x = [0,0,1,1,1,2,2,2,3,4]。为了找到第 25 百分位数,我们首先计算该值所在的索引:i = (p/100) * n),其中 p = 25,n = 10。然后,i = 2.5。
由于 i 不是整数,我们将其四舍五入为 3,并取排序列表中的第三个元素作为第 25 百分位数。给定列表中的第 25 百分位数是 1,即我们排序后的列表中的第三个元素。
练习 11:我们数据集的总结统计
在本练习中,我们将使用之前了解过的总结统计函数,获取我们数据集的基本情况:
-
将地震数据读入一个
datapandas DataFrame,并使用在上一练习中通过json库读取的dtyp字典来指定 CSV 中每一列的数据类型:data = pd.read_csv('earthquake_data.csv', dtype=dtyp) -
使用
data.info()函数来获取数据集的概览:data.info()输出将如下所示:
![图 2.3:数据集概览]()
图 2.3:数据集概览
-
打印数据集的前五行和最后五行。前五行打印如下:
data.head()输出将如下所示:
![图 2.4:前五行]()
图 2.4:前五行
最后五行打印如下:
data.tail()输出将如下所示:
![图 2.5:最后五行]()
图 2.5:最后五行
我们可以在这些输出中看到,共有 28 列,但并不是所有列都显示出来。只显示了前 10 列和最后 10 列,省略号表示中间还有其他未显示的列。
-
使用
data.describe()查找数据集的摘要统计信息。运行data.describe().T:data.describe().T在这里,
.T表示我们正在对应用的 DataFrame 进行转置操作,即将列转换为行,反之亦然。将其应用于describe()函数,可以使我们更容易地查看输出,因为转置后的 DataFrame 中每一行现在都对应于单个特征的统计数据。我们应该得到如下输出:

图 2.6:摘要统计
请注意,这里的 describe() 函数仅显示具有数值的列的统计信息。这是因为我们无法为具有非数值的列计算统计信息。
缺失值
当某个数据点的特征没有记录值(即缺失值)时,我们称数据缺失。在真实的数据集中出现缺失值是不可避免的;没有数据集是完美的。然而,理解为什么数据会缺失,以及是否有某种因素影响了数据丢失是很重要的。理解和认识到这一点可以帮助我们以合适的方式处理剩余数据。例如,如果数据缺失是随机的,那么剩余数据很可能仍能代表总体。然而,如果缺失的数据不是随机的,我们却假设它是随机的,可能会偏向我们的分析和后续建模。
让我们来看一下缺失数据的常见原因(或机制):
-
完全随机缺失(MCAR):如果数据集中的缺失值与任何其他记录的变量或外部参数之间没有任何相关性,则该值被认为是 MCAR。这意味着剩余的数据仍然能够代表总体,尽管这种情况很少发生,并且将缺失数据视为完全随机通常是一个不现实的假设。
例如,在一项研究中,研究 K12 儿童肥胖的原因,MCAR 是父母忘记带孩子去诊所参加研究的情况。
-
随机缺失 (MAR):如果数据缺失的情况与已记录的数据相关,而不是与未记录的数据相关,那么数据被认为是 MAR。由于无法通过统计方法验证数据是否为 MAR,我们只能依赖于其是否为合理的可能性。
在 K12 研究中,缺失数据是由于父母搬到其他城市,导致孩子不得不退出研究;缺失性与研究本身无关。
-
缺失非随机 (MNAR):既不是 MAR 也不是 MCAR 的数据被称为 MNAR。这是一个不可忽略的非响应情况,也就是说,缺失的变量值与其缺失的原因相关。
继续以案例研究为例,如果父母对研究的性质感到反感,不希望孩子受到欺负,因此他们将孩子从研究中撤出,那么数据将是 MNAR。
寻找缺失值
既然我们已经了解了熟悉数据缺失原因的重要性,接下来我们来讨论如何在数据集中找到这些缺失值。对于 pandas DataFrame,通常使用 .isnull() 方法创建空值掩码(即一个布尔值的 DataFrame),用以指示空值的位置——在任意位置的 True 值表示空值,而 False 值表示该位置存在有效值。
注意
.isnull() 方法可以与 .isna() 方法互换使用。对于 pandas DataFrame,这两个方法的功能完全相同——之所以有两个方法实现相同的功能,是因为 pandas DataFrame 最初是基于 R DataFrame 的,因此复用了很多 R 中的语法和思想。
数据缺失是否随机可能不会立刻显现:通过两种常见的可视化技术,可以发现数据集中各特征之间缺失值的性质:
-
空值矩阵:这是一种数据密集的展示方式,能帮助我们快速可视化数据补全中的模式。它让我们一眼看到每个特征(以及跨特征)的空值分布、数量以及它们与其他特征出现的频率。
-
空值相关热图:该热图形象地描述了每对特征之间的空值关系(或数据完整性关系),即它衡量了一个变量的存在或缺失对另一个变量存在的影响强度。
与常规相关性类似,空值相关性值的范围从 -1 到 1:前者表示一个变量出现时另一个变量肯定不出现,后者则表示两个变量同时存在。值为 0 表示一个变量的空值对另一个变量为空没有影响。
练习 12:可视化缺失值
我们来分析缺失值的性质,首先查看每个特征的缺失值数量和百分比,然后使用 Python 中的 missingno 库绘制空值矩阵和相关性热图:
-
计算每列缺失值的数量和百分比,并按降序排列。我们将使用
.isnull()函数在 DataFrame 上获取掩码。每列的空值数量可以通过对掩码 DataFrame 使用.sum()函数来获得。同样,空值的比例可以通过对掩码 DataFrame 使用.mean()函数得到,并乘以 100 转换为百分比。然后,我们使用
pd.concat()函数将空值的总数和百分比合并成一个单一的 DataFrame,并按空值的百分比对行进行排序,最后打印出 DataFrame:mask = data.isnull() total = mask.sum() percent = 100*mask.mean() missing_data = pd.concat([total, percent], axis=1,join='outer', keys=['count_missing', 'perc_missing']) missing_data.sort_values(by='perc_missing', ascending=False, inplace=True) missing_data输出结果如下:
![图 2.7:每列缺失值的数量和百分比]()
图 2.7:每列缺失值的数量和百分比
在这里,我们可以看到
state、total_damage_millions_dollars和damage_millions_dollars列的缺失值超过 90%,这意味着数据集中这些列中不到 10% 的数据点是可用的。另一方面,year、flag_tsunami、country和region_code列没有缺失值。 -
绘制空值矩阵。首先,我们使用
.any()函数在上一步的掩码 DataFrame 上查找包含任何空值的列列表。然后,我们使用missingno库绘制空值矩阵,针对数据集中随机抽取的 500 个数据点,仅绘制那些包含缺失值的列:nullable_columns = data.columns[mask.any()].tolist() msno.matrix(data[nullable_columns].sample(500)) plt.show()输出结果如下:
![图 2.8:空值矩阵]()
图 2.8:空值矩阵
在这里,黑色线条代表非空值,而白色线条则表示该列中存在空值。通过一眼看去,
location_name似乎完全填充(我们从之前的步骤知道,这一列实际上只有一个缺失值),而latitude和longitude列大多完整,但有一些空白。右侧的火花线总结了数据完整性的总体形状,并指出数据集中空值最多和最少的行。
-
绘制空值相关性热图。我们将使用
missingno库绘制数据集的空值相关性热图,仅针对那些包含空值的列:msno.heatmap(data[nullable_columns], figsize=(18,18)) plt.show()输出结果如下:

图 2.9:空值相关性热图
在这里,我们还可以看到一些标记为injuries和total_injuries的框,这告诉我们有一些记录分别包含这两个值中的一个,但不同时包含这两个值。这类情况需要特别关注——如果这些变量之间的相关性较高,那么拥有两个值并没有意义,两个值中的一个可以被删除。
缺失值的插补策略
处理列中缺失值的方法有多种。最简单的方法是直接删除缺失值所在的行;然而,这样做可能会导致丢失其他列中的有价值信息。另一个选项是插补数据,即用从已知部分数据推断出的有效值替代缺失值。常见的插补方法列举如下:
-
创建一个新的值,该值不同于其他值,用来替代列中的缺失值,以便完全区分这些行。然后,使用非线性机器学习算法(如集成模型或支持向量机)来区分这些值。
-
使用列中的适当中心值(均值、中位数或众数)来替换缺失值。
-
使用模型(例如 K 近邻或高斯混合模型)来学习替换缺失值的最佳值。
Python 有一些函数对于用静态值替换列中的空值非常有用。实现这一功能的一种方法是使用内建的 pandas .fillna(0)函数:在插补中没有歧义——用来替代列中空数据点的静态值即为传递给函数的参数(括号中的值)。
然而,如果列中空值的数量较多,并且无法立即明确可以用来替换每个空值的适当中心值,那么我们可以选择删除包含空值的行,或从建模的角度完全删除该列,因为它可能不会带来任何显著的价值。这可以通过在 DataFrame 上使用.dropna()函数来完成。可以传递给该函数的参数包括:
-
axis:此参数定义了是删除行还是删除列,通过将参数分别赋值为 0 或 1 来确定。 -
how:可以将all或any的值赋给此参数,以指示行/列是否应包含所有空值以删除该列,或者是否至少有一个空值时删除该列。 -
thresh:此参数定义了行/列必须具有的最小空值数量,才会被删除。
此外,如果无法为分类特征的空值确定合适的替代值,可以考虑在特征中创建一个新的类别来表示空值,而不是删除该列。
注意
如果从直观理解或领域知识上立即能够看出如何替换列的空值,那么我们可以当场替换这些值。然而,在许多情况下,这些推断会在探索过程的后期变得更加明显。在这些情况下,我们可以根据找到的合适方法,随时替换空值。
练习 13:使用 pandas 填充
让我们查看缺失值,并将它们替换为零,针对那些具有至少一个空值的基于时间的(连续)特征(如月份、日期、小时、分钟和秒)。我们这样做是因为对于没有记录值的情况,可以安全地假设事件发生在时间段的开始。
-
创建一个列表,包含我们想要填充值的列名:
time_features = ['month', 'day', 'hour', 'minute', 'second'] -
使用
.fillna()填充空值。我们将使用 pandas 的内建.fillna()函数,将这些列中的缺失值替换为0,并将0作为参数传递给函数:data[time_features] = data[time_features].fillna(0) -
使用
.info()函数查看填充列的空值计数:data[time_features].info()输出将如下所示:

图 2.10:空值计数
如今,我们可以看到,数据框中所有特征的值都已经是非空值。
练习 14:使用 scikit-learn 填充
让我们使用 scikit-learn 的 SimpleImputer 类,替换与描述相关的分类特征中的空值。在练习 12:可视化缺失值中,我们看到几乎所有这些特征中超过 50% 的值都是空值。将这些空值替换为中心值可能会对我们试图构建的模型产生偏差,使它们变得不相关。因此,我们将空值替换为一个单独的类别,值为 NA:
-
创建一个列表,包含我们想要填充值的列名:
description_features = [ 'injuries_description', 'damage_description', 'total_injuries_description', 'total_damage_description' ] -
创建一个
SimpleImputer类的对象。在这里,我们首先创建一个imp对象,并使用表示如何填充数据的参数初始化它。我们将传递给对象初始化的参数包括:missing_values:这是缺失值的占位符,即所有出现在missing_values参数中的值将被填充。strategy:这是填充策略,可以是mean、median、most_frequent(即众数)或constant。前三者只能用于数值数据,并会通过指定的中心值替换每列中的缺失值,而最后一个则会根据fill_value参数,用常数替换缺失值。fill_value:指定用来替换所有missing_values的值。如果保持默认设置,当填充数值数据时,填充值将为0,而字符串或对象数据类型将使用missing_value字符串。imp = SimpleImputer(missing_values=np.nan, strategy='constant', fill_value='NA') -
执行填充。我们将使用
imp.fit_transform()实际执行填充。它将带有空值的 DataFrame 作为输入,并返回填充后的 DataFrame:data[description_features] = imp.fit_transform(data[description_features]) -
使用
.info()函数查看填充列的空值计数:data[description_features].info()输出将如下所示:

图 2.11:空值计数
练习 15:使用推断值进行填充
让我们用来自类别 damage_description 特征的信息替换连续的 damage_millions_dollars 特征中的空值。尽管我们可能不知道确切的损失金额,但类别特征为我们提供了因地震造成的损失金额的范围信息:
-
找出有多少行的
damage_millions_dollars值为空,以及其中有多少行的damage_description值不为空:print(data[pd.isnull(data.damage_millions_dollars)].shape[0]) print(data[pd.isnull(data.damage_millions_dollars) & (data.damage_description != 'NA')].shape[0])输出将如下所示:
![]()
图 2.12:包含空值的行数
如我们所见,5,594 个空值中有 3,849 个可以通过另一个变量轻松替换。
-
找出每个类别的平均
damage_millions_dollars值。由于damage_description中的每个类别代表一系列值,我们从已存在的非空值中找到每个类别的平均damage_millions_dollars值。这些为该类别提供了合理的最可能值估算:category_means = data[['damage_description', 'damage_millions_dollars']].groupby('damage_description').mean() category_means输出将如下所示:
![图 2.13:每个类别的平均 damage_millions_dollars 值]()
图 2.13:每个类别的平均 damage_millions_dollars 值
-
将平均值存储为字典。在此步骤中,我们将包含平均值的 DataFrame 转换为字典(Python
dict对象),这样访问它们就更加方便。此外,由于新创建的
NA类别(在前一个练习中的填充值)的值为NaN,并且0类别的值缺失(数据集中没有damage_description等于0的行),我们也明确地将这些值添加到字典中:replacement_values = category_means.damage_millions_dollars.to_dict() replacement_values['NA'] = -1 replacement_values['0'] = 0 replacement_values输出将如下所示:
![图 2.14:平均值字典]()
图 2.14:平均值字典
-
创建一系列替代值。对于
damage_description列中的每个值,我们使用map函数将类别值映射到平均值。.map()函数用于将列中的键映射到replacement_values字典中每个元素的对应值:imputed_values = data.damage_description.map(replacement_values) -
替换列中的空值。我们通过使用
np.where作为三元运算符来实现:第一个参数是掩码,第二个是如果掩码为正时从中获取值的系列,第三个是如果掩码为负时从中获取值的系列。这确保了
np.where返回的数组仅将damage_millions_dollars中的空值替换为imputed_values序列中的值:data['damage_millions_dollars'] = np.where(condition=data.damage_millions_dollars.isnull(), x=imputed_values, y=data.damage_millions_dollars) -
使用
.info()函数查看填充列的空值计数:data[['damage_millions_dollars']].info()输出结果如下:

图 2.15:空值计数
我们可以看到,替换后 damage_millions_dollars 列中没有空值。
活动 2:汇总统计与缺失值
在本活动中,我们将回顾本章中到目前为止看到的一些汇总统计和缺失值分析。我们将使用一个新的数据集,来自 Kaggle 的 House Prices: Advanced Regression Techniques 比赛(可以在 www.kaggle.com/c/house-prices-advanced-regression-techniques/data 或 GitHub 上的 github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python 获取)。虽然在练习中使用的地震数据集是针对解决分类问题的(当目标变量只有离散值时),我们将在活动中使用的数据集将用于解决回归问题(当目标变量包含连续值的范围时)。我们将使用 pandas 函数生成汇总统计,并通过空值矩阵和空值相关热图可视化缺失值。
执行的步骤如下:
-
读取数据。
-
使用 pandas 的
.info()和.describe()方法查看数据集的汇总统计信息。 -
查找每列的缺失值总数和缺失值百分比,并按缺失百分比的降序显示至少有一个空值的列。
-
绘制空值矩阵和空值相关热图。
-
删除缺失值超过 80% 的列。
-
将
FireplaceQu列中的空值替换为NA值。注意
本活动的解决方案可以在第 307 页找到。
值的分布
在本节中,我们将查看各个变量的行为——它们取什么值,这些值的分布如何,以及如何以可视化方式表示这些分布。
目标变量
目标变量可以是连续值(回归问题的情况)或离散值(分类问题的情况)。本章讨论的问题是预测地震是否引发海啸,也就是 flag_tsunami 变量,它只有两个离散值——因此是一个分类问题。
可视化有多少地震引发了海啸,以及有多少没有引发海啸的一种方法是柱状图,其中每个柱子代表一个离散值,柱子的高度等于具有相应离散值的数据点的计数。这为我们提供了每个类别的绝对计数的良好比较。
练习 16:绘制柱状图
让我们看看数据集中有多少地震引发了海啸。我们将通过对列使用value_counts()方法,并直接对返回的 pandas 系列使用.plot(kind='bar')函数来完成。按照以下步骤操作:
-
使用
plt.figure()初始化绘图:plt.figure(figsize=(8,6)) -
接下来,输入我们的主要绘图命令:
data.flag_tsunami.value_counts().plot(kind='bar') -
设置显示参数并显示图表:
plt.ylabel('Number of data points') plt.xlabel('flag_tsunami') plt.show()输出将如下所示:

图 2.16:柱状图显示有多少地震引发了海啸
从这个柱状图中,我们可以看到大多数地震没有引发海啸,而且不到三分之一的地震引发了海啸。这显示出数据集略微失衡。
让我们更仔细地看一下这些 Matplotlib 命令的作用:
-
plt.figure(figsize=(8,6)):此命令定义了我们的图表大小,通过提供宽度和高度的值。这是所有绘图命令之前的第一条命令。 -
plt.xlabel()和plt.ylabel():这些命令接受字符串作为输入,允许我们指定图表中X轴和Y轴的标签。 -
plt.show():这是绘图时写入的最后一条命令,它将图表以内联方式显示在 Jupyter Notebook 中。
类别数据
类别变量是那些具有离散值,表示不同类别或观察水平的变量,可以是字符串对象或整数值。例如,我们的目标变量flag_tsunami是一个类别变量,具有两个类别:Tsu和No。
类别变量可以分为两种类型:
-
location_name。这个变量的值不能说是有序的,也就是说,一个地点并不大于另一个地点。类似的变量示例还包括颜色、鞋类类型、种族类型等。 -
damage_description,因为每个值表示逐渐增加的损害值。另一个例子可以是星期几,其值从星期一到星期天,具有一定的顺序关系,我们知道星期四在星期三之后,但在星期五之前。尽管有序变量可以通过对象数据类型表示,但它们通常也表示为数值数据类型,这通常使得它们与连续变量之间的区分变得困难。
处理数据集中的类别变量时面临的主要挑战之一是高基数,即大量的类别或不同的值,其中每个值在数据集中的出现次数相对较少。例如,location_name具有大量的唯一值,每个值在数据集中的出现频率较低。
此外,非数值型的类别变量总是需要某种形式的预处理,将其转换为数值格式,以便机器学习模型能够读取并进行训练。在没有丢失上下文信息的情况下,如何将类别变量编码为数值型是一个挑战。尽管这些信息对于人类来说(由于领域知识或常识)非常容易理解,但计算机却很难自动理解。例如,像国家或地点名称这样的地理特征本身并不能表明不同值之间的地理接近性,但这可能是一个重要特征——如果发生在东南亚地区的地震比欧洲地区的地震触发更多海啸呢?仅仅通过数值编码特征,无法捕获这些信息。
练习 17:类别变量的数据类型
让我们找出地震数据集中哪些变量是类别型的,哪些是连续型的。正如我们现在所知道的,类别变量也可以具有数值值,因此,拥有数值数据类型并不意味着变量就是连续型的:
-
找出所有数值型和对象型的列。我们在 DataFrame 上使用
.select_dtypes()方法,创建一个包含数值型(np.number)和类别型(np.object)列的子集 DataFrame,然后打印每个列的列名。对于数值列,请使用以下方法:numeric_variables = data.select_dtypes(include=[np.number]) numeric_variables.columns输出将如下所示:
![图 2.17:所有数值型的列]()
图 2.17:所有数值型的列
对于类别列,请使用以下方法:
object_variables = data.select_dtypes(include=[np.object]) Object_variables.columns输出将如下所示:
![图 2.18:所有对象类型的列]()
图 2.18:所有对象类型的列
这里显然可以看出,对象类型的列是类别变量。为了区分数值列中的类别变量和连续变量,让我们查看这些特征的唯一值数量。
-
找出数值特征的唯一值数量。我们在 DataFrame 上使用
select_dtypes方法,找到每一列的唯一值数量,并将结果序列按升序排序。对于数值列,请使用以下方法:numeric_variables.nunique().sort_values()输出将如下所示:

图 2.19:数值特征的唯一值数量
对于类别列,请使用以下方法:
object_variables.nunique().sort_values()
输出将如下所示:

图 2.20:分类列的唯一值数量
对于数值型变量,我们可以看到前九个变量的唯一值显著少于其余行,这些变量很可能是分类变量。然而,我们必须记住,其中一些可能只是具有较小范围的四舍五入值的连续变量。另外,month 和 day 在这里不会被视为分类变量。
练习 18:计算类别值计数
对于具有分类值的列,查看该特征的唯一值(类别)以及这些类别的频率将非常有用,也就是说,每个不同的值在数据集中出现的次数。我们来找出 injuries_description 分类变量中每个 0 到 4 标签和 NaN 值的出现次数:
-
对
injuries_description列使用value_counts()函数来找出每个类别的频率。使用value_counts会以降序形式返回每个值的频率,并以 pandas 系列的形式显示:counts = data.injuries_description.value_counts(dropna=False) counts输出应如下所示:
![图 2.21:每个类别的频率]()
图 2.21:每个类别的频率
-
按照顺序变量的升序排序这些值。如果我们希望按值本身的顺序显示频率,我们可以重置索引,从而得到一个 DataFrame,并按索引(即顺序变量)排序值:
counts.reset_index().sort_values(by='index')

图 2.22:排序后的值
练习 19:绘制饼图
由于我们示例数据中的目标变量是分类的,练习 16:绘制条形图 中的示例展示了可视化分类值分布的一种方式(使用条形图)。另一种可以帮助我们轻松查看每个类别在整个数据集中所占比例的图表是饼图。让我们绘制一个饼图来可视化 damage_description 变量的离散值分布:
-
将数据格式化成需要绘制的形式。在这里,我们对该列使用
value_counts()并按索引排序系列:counts = data.damage_description.value_counts() counts = counts.sort_index() -
绘制饼图。
plt.pie()分类函数使用计数数据绘制饼图。我们将按照 练习 16:绘制条形图 中描述的相同三步进行绘图:plt.figure(figsize=(10,10)) plt.pie(counts, labels=counts.index) plt.title('Pie chart showing counts for\ndamage_description categories') plt.show()输出结果将是:

图 2.23:显示 damage_description 类别计数的饼图
连续数据
连续变量可以取任意数量的值,通常是整数(例如,死亡人数)或浮动数据类型(例如,山脉的高度)。了解特征中值的基本统计信息是非常有用的:describe() 函数的输出显示的最小值、最大值和百分位数给我们提供了一个合理的估算。
然而,对于连续变量来说,了解其在操作范围内的分布情况也非常有用。由于我们不能简单地计算各个值的计数,我们会将值按升序排列,将其分组为等间隔的区间,然后计算每个区间的计数。这为我们提供了底层的频率分布,绘制该分布便能得到直方图,从而让我们查看数据的形态、中心值以及变异性。
直方图为我们提供了一个简洁的视角,帮助我们了解正在查看的数据。它们让我们一目了然地看到数据的行为,揭示了底层分布(例如正态分布或指数分布)、异常值、偏度等信息。
注意
很容易将条形图与直方图混淆。主要的区别在于,直方图用于绘制已被分组的连续数据以可视化频率分布,而条形图可以用于多种其他用途,包括表示我们之前处理的类别变量。此外,条形图中的条形高度表示该箱子的频率,但直方图中的宽度也会影响频率的表示,这在条形图中并不适用。
最常见的频率分布之一是高斯(或正态)分布。这是一种对称分布,具有钟形曲线,表示接近中间值的范围在数据集中出现频率最高,随着远离中间部分,频率对称性地减少。
它是一个概率分布,曲线下的面积等于一。

图 2.24:正态分布
偏度
如果一个分布不是对称的,我们称其为偏斜的,偏度衡量的是变量相对于其均值的非对称性。偏度的值可以是正值、负值(或未定义)。在前一种情况下,尾巴位于分布的右侧,而后一种情况则表示尾巴位于左侧。
然而,必须注意的是,厚而短的尾部对偏度的影响与长而细的尾部相同。
峰度
峰度是衡量变量分布的尾部形态的一个指标,用来衡量一侧尾部是否存在异常值。较高的峰度值表示尾部较厚,且存在异常值。与偏度的概念类似,峰度也描述了分布的形态。
练习 20:绘制直方图
让我们使用 Seaborn 库绘制eq_primary特征的直方图:
-
使用
plt.figure()来启动绘图:plt.figure(figsize=(10,7)) -
sns.distplot()是我们用来绘制直方图的主要命令。第一个参数是用来绘制直方图的单维数据,bins 参数定义了箱子的数量和大小。使用方式如下:sns.distplot(data.eq_primary.dropna(), bins=np.linspace(0,10,21)) -
使用
plt.show()显示图表:plt.show()输出结果如下:

图 2.25:eq_primary 特征的直方图
该图给出了一个标准化(或归一化)直方图,这意味着直方图下方的区域总和为 1。此外,直方图上的曲线是核密度估计,它给我们提供了变量的概率分布的形态。
从图中我们可以看到,eq_primary 的值大多位于 5 到 8 之间,这意味着大多数地震的震级是中等到高值,几乎没有地震震级非常低或非常高。
练习 21:偏度与峰度
让我们使用 pandas 核心函数计算数据集中所有特征的偏度和峰度值:
-
使用
.skew()数据框方法计算所有特征的偏度,然后按升序排序值:data.skew().sort_values()输出结果将是:
![]()
图 2.26:数据集中所有特征的偏度值
-
使用
.kurt()数据框方法计算所有特征的峰度:data.kurt()输出结果将是:

图 2.27:数据集中所有特征的峰度值
在这里,我们可以看到某些变量的峰度值显著偏离 0。这意味着这些列具有长尾。但是,这些变量尾部的值(即表示死亡、受伤人数以及损失金额的数值)在我们的案例中,可能是离群值,我们可能需要特别关注它们。较大的值可能实际上表示额外的因素,增加了由地震引起的破坏,即海啸。
活动 3:可视化表示值的分布
在这个活动中,我们将复习上一节关于不同类型数据的内容。我们将使用与活动 2:摘要统计和缺失值中相同的数据集,即 房价:高级回归技术(可以在www.kaggle.com/c/house-prices-advanced-regression-techniques/data 或在 GitHub 上找到 github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python)。我们将使用不同类型的图表来可视化表示该数据集的值分布。
执行的步骤如下:
-
使用 Matplotlib 绘制目标变量
SalePrice的直方图。 -
找出每个对象类型列中唯一值的数量。
-
创建一个数据框,表示
HouseStyle列中每个类别值的出现次数。 -
绘制一个饼图,表示这些计数。
-
查找每个具有数字类型的列中唯一值的数量。
-
使用 Seaborn 绘制
LotArea变量的直方图。 -
计算每列值的偏度和峰度值。
注意
此活动的解决方案可以在第 312 页找到。
数据中的关系
找到数据中变量之间关系的重要性有两个原因:
-
找出哪些特征可能是重要的可以被认为是至关重要的,因为找到与目标变量有强烈关系的特征将有助于特征选择过程。
-
找到不同特征之间的关系是有用的,因为数据集中的变量通常不可能完全独立于其他所有变量,而这可能会以多种方式影响我们的建模。
现在,我们有许多方法可以可视化这些关系,具体方法取决于我们试图找到关系的变量类型,以及我们考虑作为方程或比较的一部分的变量数量。
两个连续变量之间的关系
找到两个连续变量之间的关系,基本上是看一个变量的值增加时另一个变量如何变化。最常见的可视化方法是使用散点图,其中我们将每个变量沿一个轴(当我们有两个变量时,在二维平面中的X和Y轴)绘制,并使用标记在X-Y平面中绘制每个数据点。这种可视化能够很好地展示这两个变量之间是否存在某种关系。
然而,如果我们想量化两个变量之间的关系,最常用的方法是找到它们之间的相关性。如果目标变量是连续的,并且与另一个变量高度相关,这表明该特征在模型中是一个重要部分。
皮尔逊相关系数
皮尔逊相关系数是一种常用的相关系数,用来显示一对变量之间的线性关系。公式返回一个介于-1 和+1 之间的值,其中:
-
+1 表示强正相关
-
-1 表示强负相关
-
0 表示没有关系
同样,找到特征对之间的相关性也很有用。尽管高度相关的特征的存在不会使模型变差,但它们也不一定会使任何模型变得更好。为了简化起见,最好从一组高度相关的特征中只保留一个。
注意
在拟合线性模型时,特征之间高度相关可能会导致模型不可预测且变化幅度较大。这是因为线性模型中每个特征的系数可以解释为在保持其他特征不变的情况下,目标变量的单位变化。然而,当一组特征不独立(即存在相关性)时,我们无法确定每个特征对目标变量的独立变化所造成的影响,导致系数变化幅度较大。
要找到 DataFrame 中每个数值特征与其他特征的成对相关性,可以在 DataFrame 上使用.corr()函数。
练习 22:绘制散点图
让我们绘制主震震中震级(X轴)与对应的受伤人数(Y轴)之间的散点图:
-
过滤掉空值。由于我们知道两列中都有空值,首先让我们过滤数据,只保留非空行:
data_to_plot = data[~pd.isnull(data.injuries) & ~pd.isnull(data.eq_primary)] -
创建并显示散点图。我们将使用 Matplotlib 的
plt.scatter(x=..., y=...)作为绘制数据的主要命令。x和y参数指定哪个特征应沿哪个轴绘制。它们接受单一维度的数据结构,如列表、元组或 pandas 系列。我们还可以向scatter函数传递更多参数,例如指定绘制单个数据点时使用的图标。例如,要使用红色的交叉符号作为图标,我们需要传递以下参数:marker='x', c='r':plt.figure(figsize=(12,9)) plt.scatter(x=data_to_plot.eq_primary, y=data_to_plot.injuries) plt.xlabel('Primary earthquake magnitude') plt.ylabel('No. of injuries') plt.show()输出结果将如下所示:

图 2.28:散点图
从图表中我们可以推断出,虽然受伤人数和震中震级之间似乎没有明显的趋势,但随着震级的增加,受伤人数较多的地震数量也在增加。然而,对于大多数地震而言,似乎没有明显的关系。
练习 23:相关性热力图
让我们使用 Seaborn 的sns.heatmap()函数绘制数据集中所有数值变量之间的相关性热力图,该函数基于数据集中的特征间相关性值。
传递给sns.heatmap()函数的可选参数是square和cmap,它们分别表示绘制的图表中每个像素应为正方形,并指定要使用的颜色方案:
-
绘制一个包含所有特征的基本热力图:
plt.figure(figsize = (12,10)) sns.heatmap(data.corr(), square=True, cmap="YlGnBu") plt.show()输出结果将是:
![]()
图 2.29:相关性热力图
从图表右侧的颜色条中我们可以看到,最小值大约为
-0.2,对应的颜色是最浅的,这在某种程度上误导了相关性值的表示,实际相关性值范围应该是从-1 到 1。 -
在一个更自定义的热图中绘制特征的子集。我们将使用
vmin和vmax参数指定上下限,并使用带有注释的热图重新绘制,注释显示特征对之间的相关系数值。我们还将更改颜色方案,使其更易于解读——中性色白色表示无相关性,而越来越深的蓝色和红色分别表示更高的正相关性和负相关性:feature_subset = [ 'focal_depth', 'eq_primary', 'eq_mag_mw', 'eq_mag_ms', 'eq_mag_mb', 'intensity', 'latitude', 'longitude', 'injuries', 'damage_millions_dollars', 'total_injuries', 'total_damage_millions_dollars'] plt.figure(figsize = (12,10)) sns.heatmap(data[feature_subset].corr(), square=True, annot=True, cmap="RdBu", vmin=-1, vmax=1) plt.show()输出结果如下:

图 2.30:自定义的相关性热图
现在,虽然我们可以计算相关系数的值,但这仅仅提供了线性关系的指示。为了更好地判断是否存在可能的依赖关系,我们可以绘制特征对之间的散点图,这在变量之间的关系不明确时特别有用,因为通过可视化数据点的分布,我们可以大致判断这两个变量是否(以及如何)相关。
练习 24:Pairplot
pairplot 对于同时可视化多个特征对之间的关系非常有用,可以使用 Seaborn 的 .pairplot() 函数绘制。在这个练习中,我们将查看数据集中具有最高成对相关性的特征之间的 pairplot:
-
定义一个包含要创建 pairplot 的特征子集的列表:
feature_subset = [ 'focal_depth', 'eq_primary', 'eq_mag_mw', 'eq_mag_ms', 'eq_mag_mb', 'intensity', 'latitude', 'longitude', 'injuries', 'damage_millions_dollars', 'total_injuries', 'total_damage_millions_dollars'] -
使用 Seaborn 创建 pairplot。传递给绘图函数的参数是:
kind='scatter',表示我们希望网格中每一对变量之间的单独图形以散点图的形式展示;diag_kind='kde',表示我们希望对角线上的图形(即两变量相同的位置)为核密度估计图。此外,还应该注意,对角线对称的图形本质上是相同的,只不过坐标轴被反转了:
plt.figure(figsize = (12,10)) sns.heatmap(data[feature_subset].corr(), square=True, annot=True, cmap="RdBu", vmin=-1, vmax=1) plt.show()输出结果如下:

图 2.31:具有最高成对相关性的特征之间的 pairplot
连续型变量与类别型变量之间的关系
当一个变量是类别型变量,另一个是连续型变量时,查看它们之间关系的一种常见方法是使用条形图或箱线图。
-
条形图有助于比较一个变量在一组离散参数中的值,是最常见的图形类型之一。每根条形代表一个类别值,条形的高度通常表示该类别下连续变量的聚合值(如平均值、总和或该类别中连续变量的值的计数)。
-
箱型图是一个矩形,用来表示每个离散类别变量的连续变量的分布。它不仅能够有效地展示异常值,还可以让我们比较不同类别变量下连续变量的分布。矩形的下边缘和上边缘分别代表第一和第三四分位数,矩形中间的线代表中位数,而矩形上下的点(或离群点)代表异常值。
练习 25:柱状图
让我们使用柱状图来可视化每个强度级别的地震所造成的海啸总数:
-
预处理
flag_tsunami变量。在使用flag_tsunami变量之前,我们需要预处理它,将No值转换为零,将Tsu值转换为一。这将为我们提供二元目标变量。为此,我们使用.loc操作符设置列中的值,:表示需要为所有行设置值,第二个参数指定要设置值的列名称:data.loc[:,'flag_tsunami'] = data.flag_tsunami.apply(lambda t: int(str(t) == 'Tsu')) -
删除所有
intensity值为 null 的行,去掉我们要绘制的数据:subset = data[~pd.isnull(data.intensity)][['intensity','flag_tsunami']] -
查找每个
intensity级别的海啸总数并显示数据框。为了将数据转换为可视化柱状图的格式,我们需要按每个强度级别对行进行分组,然后对flag_tsunami值进行求和,以获得每个强度级别的海啸总数:data_to_plot = subset.groupby('intensity').sum() data_to_plot输出将如下所示:
![图 2.32:每个强度级别的海啸总数]()
图 2.32:每个强度级别的海啸总数
-
使用 Matplotlib 的
plt.bar(x=..., height=...)方法绘制柱状图,该方法需要两个参数,一个指定绘制柱形的位置(x值),另一个指定每个柱形的高度。这两者都是一维数据结构,必须具有相同的长度:plt.figure(figsize=(12,9)) plt.bar(x=data_to_plot.index, height=data_to_plot.flag_tsunami) plt.xlabel('Earthquake intensity') plt.ylabel('No. of tsunamis') plt.show()输出将如下所示:

图 2.33:柱状图
从这个图中,我们可以看到,随着地震强度的增加,造成的海啸数量也增加,但在强度超过 9 之后,海啸的数量似乎突然下降。
想想为什么会发生这种情况。或许只是因为高强度的地震较少,因此海啸也较少。或者可能是完全独立的因素;也许高强度的地震历史上发生在陆地上,无法引发海啸。探索数据以找出原因。
练习 26:箱型图
在本练习中,我们将绘制一个箱型图,表示具有至少 100 次地震的国家中eq_primary的变化:
-
查找发生超过 100 次地震的国家。我们将查找数据集中所有国家的计数值。然后,我们将创建一个仅包含计数大于 100 的国家的系列:
country_counts = data.country.value_counts() top_countries = country_counts[country_counts > 100] top_countries输出将如下所示:
![图 2.34:地震超过 100 次的国家]()
图 2.34:地震超过 100 次的国家
-
对 DataFrame 进行子集化,筛选出前面集合中包含的国家的行。为了筛选这些行,我们使用
.isin()方法在 pandas 系列上选择包含传递为参数的类似数组对象中的值的行:subset = data[data.country.isin(top_countries.index)] -
创建并显示箱形图。绘制数据的主要命令是
sns.boxplot(x=..., y=..., data=..., order=)。x和y参数是 DataFrame 中要绘制在每个坐标轴上的列名——前者假定是类别变量,后者是连续变量。data参数指定要从中获取数据的 DataFrame,order参数接受一个类别名称列表,指示在 X 轴上显示类别的顺序:plt.figure(figsize=(15, 15)) sns.boxplot(x='country', y="eq_primary", data=subset, order=top_countries.index) plt.show()输出将如下所示:

图 2.35:箱形图
两个类别变量之间的关系
当我们只关注一对类别变量以查找它们之间的关系时,最直观的方式是基于第一个类别将数据进行划分,然后再根据第二个类别变量进一步细分,查看结果的计数以找到数据点的分布。虽然这可能看起来有些混乱,但一种常见的可视化方式是使用堆叠条形图。与常规的条形图一样,每个条形图表示一个类别值。但每个条形图将再次被细分为颜色编码的子类别,这可以指示在主类别中有多少数据点落入每个子类别(即第二个类别)。类别数较多的变量通常被认为是主类别。
练习 27:堆叠条形图
在本练习中,我们将绘制一个堆叠条形图,表示每个强度级别发生的海啸数量:
-
查找落入每个
intensity和flag_tsunami分组值中的数据点数量:grouped_data = data.groupby(['intensity', 'flag_tsunami']).size() grouped_data输出将如下所示:
![]()
图 2.36:每个分组的强度和 flag_tsunami 中落入的数据点
-
对结果 DataFrame 使用
.unstack()方法,将一级索引(flag_tsunami)作为列:data_to_plot = grouped_data.unstack() data_to_plot输出将如下所示:
![图 2.37:一级索引]()
图 2.37:一级索引
-
创建堆叠条形图。我们首先使用
sns.set()函数来指示我们希望使用 Seaborn 作为可视化库。然后,我们可以轻松地使用 pandas 中的原生.plot()函数,通过传递kind='bar'和stacked=True参数来绘制堆叠条形图:sns.set() data_to_plot.plot(kind='bar', stacked=True, figsize=(12,8)) plt.show()输出将如下所示:

图 2.38:堆叠条形图
现在,该图表让我们能够可视化和解释每个强度级别导致海啸的地震比例。在练习 25: 条形图中,我们看到大于 9 级的地震所导致的海啸数量有所下降。从这个图中,我们可以确认,主要是因为超过 10 级的地震数量减少了;而 11 级的地震所导致的海啸比例甚至有所增加。
活动 4: 数据内的关系
在本次活动中,我们将回顾上一节中关于数据关系的学习内容。我们将使用与活动 2: 汇总统计和缺失值相同的数据集,即房价:高级回归技术(可在www.kaggle.com/c/house-prices-advanced-regression-techniques/data或 GitHub 上的github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python找到)。我们将使用不同的图表来突出显示数据集中的值之间的关系。
要执行的步骤如下:
-
绘制数据集的相关性热图。
-
绘制一个更紧凑的热图,使用以下特征子集并带有相关性值的注释:
feature_subset = [ 'GarageArea', 'GarageCars','GarageCond','GarageFinish','GarageQual','GarageType', 'GarageYrBlt','GrLivArea','LotArea','MasVnrArea','SalePrice' ] -
显示相同特征子集的配对图,KDE 图位于对角线位置,散点图位于其他地方。
-
创建一个箱型图,显示
SalePrice在每个GarageCars类别中的变化。 -
使用 Seaborn 绘制折线图,显示旧公寓和新建公寓的
SalePrice变化。注意
本次活动的解决方案可以在第 319 页找到。
概述
在本章中,我们首先讨论了为什么数据探索是建模过程中的重要部分,它不仅有助于对数据集进行预处理,还能帮助我们设计具有信息量的特征并提高模型的准确性。本章不仅侧重于对数据集及其特征的基本概览,还通过创建结合多个特征的可视化来获得洞见。
我们研究了如何使用 pandas 的核心功能找到数据集的汇总统计数据。我们还研究了如何发现缺失值,并讨论了它们的重要性,同时学习如何使用 Missingno 库分析这些缺失值,以及使用 pandas 和 scikit-learn 库填补缺失值。
接下来,我们研究了如何研究数据集中变量的单变量分布,并通过条形图、饼图和直方图等可视化方式展示这些分布。最后,我们学习了如何探索变量之间的关系,并了解到它们可以通过散点图、热图、箱型图、堆积条形图等形式进行表示。
在接下来的章节中,我们将开始探索监督式机器学习算法。现在我们已经了解了如何探索我们拥有的数据集,我们可以进入建模阶段。下一章将介绍回归:一种主要用于构建连续目标变量模型的算法类别。
第三章:回归分析
学习目标
在本章结束时,您将能够:
-
描述回归模型,并解释回归与分类问题的区别
-
解释梯度下降的概念,它在线性回归问题中的应用,以及如何应用到其他模型架构中
-
使用线性回归为x-y平面上的数据构建线性模型
-
评估线性模型的性能,并利用评估结果选择最佳模型
-
使用特征工程为构建更复杂的线性模型创建虚拟变量
-
构建时间序列回归模型,使用自回归方法
本章涵盖了回归问题和分析,向我们介绍了线性回归以及多元线性回归、梯度下降和自回归。
介绍
在前两章中,我们介绍了 Python 中监督机器学习的概念,以及加载、清理、探索和可视化原始数据源所需的基本技术。我们讨论了指定输入和所需输出之间相关性的关键性,以及初始数据准备过程有时可能占据整个项目所花费时间的大部分。
在本章中,我们将深入探讨模型构建过程,并使用线性回归构建我们的第一个监督机器学习解决方案。所以,让我们开始吧。
回归与分类问题
我们在第一章、Python 机器学习工具包中讨论了两种不同的方法:监督学习和无监督学习。监督学习问题旨在将输入信息映射到已知的输出值或标签,但在此基础上,还有两个进一步的子类别需要考虑。监督学习和无监督学习问题都可以进一步细分为回归问题或分类问题。本章的主题是回归问题,它们旨在预测或建模连续值,例如预测明天的气温(摄氏度)或确定图像中人脸的位置。相反,分类问题则不同,它们预测的是某个输入属于预定类别中的某一个,而不是返回一个连续值。第一章、Python 机器学习工具包中的示例监督学习问题,即我们想要预测一个假发是来自 1960 年代还是 1980 年代,就是一个很好的监督分类问题的例子。在这个例子中,我们试图预测一个假发是否来自两个不同的类别:类别 1 为 1960 年代,类别 2 为 1980 年代。其他分类问题包括预测泰坦尼克号的乘客是否幸存或死亡,或者经典的 MNIST 问题(yann.lecun.com/exdb/mnist/)。MNIST 是一个包含 70,000 个标注过的手写数字图像的数据库,数字范围为 0 到 9。MNIST 分类任务的目标是,从 70,000 张输入图像中挑选一张,预测或分类图像中的数字 0-9。因此,模型必须预测该图像属于 10 个类别中的哪一个。
数据、模型、训练和评估
在我们深入探讨回归问题之前,我们将首先审视创建任何机器学习模型所涉及的四个主要阶段,无论是监督回归还是其他类型的模型。这些阶段如下:
-
数据准备
-
模型架构的规范
-
训练过程的设计与执行
-
训练模型的评估
建议确保你完全理解这个流程以及本节中描述的内容,因为每个阶段对实现高效或合理的系统性能都至关重要。我们将在第一章、Python 机器学习工具包的背景下,考虑这些阶段如何应用于假发分类问题。
数据准备
管道的第一阶段是数据准备,这是第一章、《Python 机器学习工具包》的一个重要组成部分,因此在本节中不再进一步分析。然而,重要的是要理解数据规范、收集和清理/整理过程的关键性。如果输入数据是次优的,我们不能期望能够产生一个高性能的系统。关于数据质量,有一句常见的格言是垃圾进,垃圾出。如果你输入的是垃圾数据,那么你得到的结果也将是垃圾。在我们的假发示例中,我们希望样本量至少在几百个,理想情况下是几千个,并且这些样本已正确标记为 1960 年代或 1980 年代的样本。我们不希望样本被错误标记,或者根本不属于这两个时代。
模型架构
第二阶段是模型架构规范,在本章中将进行更详细的描述。该阶段定义了将要使用的模型类型,以及组成模型本身的参数的类型和值。模型本质上是一个数学方程,用于定义输入数据与期望结果之间的关系。与任何方程一样,模型由变量和常量组成,并通过一组过程进行组合,例如加法、减法或卷积。模型参数的性质和值会根据选择的模型类型以及模型能够描述所观察关系的复杂性水平而有所不同。较简单的模型将包含较少的参数,并对其值有更大的约束,而更复杂的模型则可能包含更多的参数,并且这些参数可能会发生变化。在本章中,我们将使用一个线性模型,与一些其他模型(如卷积神经网络模型,它可能包含超过一百万个需要优化的参数)相比,线性模型是较简单的。这种简单性不应被误认为是缺乏能力,或者无法描述数据中的关系,而仅仅是意味着可调参数较少(即调整这些参数的值以优化性能)。
模型训练
系统管道的第三个阶段是训练过程的设计与执行,即通过这种机制来确定模型参数的值。在监督学习问题中,我们可以将训练过程类比为一个学生在课堂中的学习过程。在典型的课堂环境中,老师已经知道给定问题的答案,并尝试向学生展示如何根据一组输入来解决问题。在这种情况下,学生就是模型,参数就像学生大脑中的知识,是学生正确解答问题的手段。
训练过程是教师用来训练学生正确回答问题的方法;该方法可以根据学生的学习能力和理解能力进行调整和变化。一旦模型架构被定义(即班级中的学生),训练过程就会提供所需的指导和约束,以接近最优解。就像一些学生在不同的学习环境中表现更好一样,模型也如此。因此,还存在一组额外的参数,称为超参数,它们虽然不在模型内部用于根据某些输入数据集进行预测,但它们被定义、使用并调优,旨在通过指定的成本(或误差)函数(例如,均方根误差)优化模型的性能。我们将在本章中更详细地讨论超参数,但目前,最简单的理解方式是将超参数视为决定模型实际参数的环境。
模型评估
管道的最终阶段是模型评估,它产生最终的性能指标。这是我们知道模型是否值得发布、是否优于先前版本,或是否在不同的编程语言或开发环境中有效迁移的机制。我们将在第六章,模型评估中更详细地介绍这些指标,因此在此阶段不会详细讨论。只需记住,无论选择哪种验证技术,它都需要能够持续报告并独立地衡量模型在数据集上的性能表现。再次以我们的假发数据集为例,评估阶段将查看模型在给定假发图像和已知年代标签的情况下,达成多少个正确预测。
线性回归
我们将从选择线性模型开始研究回归问题。线性模型因其直观性而成为一个很好的首选,同时也具有强大的预测能力,前提是数据集在输入特征和数值之间存在某种线性或多项式关系。线性模型的直观性通常来源于能够将数据绘制成图表,并观察数据中的趋势模式,例如输出(数据的y轴值)随着输入(x轴值)的变化呈正向或负向趋势。虽然通常不会以这种方式呈现,线性回归模型的基本组成部分也常常是在高中数学课程中学习的。你可能还记得,直线或线性模型的方程式定义如下:

图 3.1:直线方程
这里,x 是输入值,y 是对应的输出或预测值。模型的参数是由 m 定义的线的梯度或斜率(y 值的变化除以 x 值的变化),以及 y 截距值 b,它表示线与 y 轴的交点。通过这样的模型,我们可以提供 m 和 b 参数的值来构建线性模型。例如,y = 2x + 1,其斜率为 2,表示 y 值的变化速度是 x 的两倍;这条线与 y 截距相交于 1:

图 3.2:直线的参数
所以,我们了解了定义直线所需的参数,但这实际上并没有做什么特别有趣的事情。我们只是规定了模型的参数来构建一条线。我们真正想做的是,拿一个数据集并构建一个最好描述数据集的模型。正如之前提到的,这个数据集需要在输入特征和输出值之间有某种线性关系的近似。为此,我们创建了一个合成数据集,记录了从 1841 年到 2010 年的空气温度数据,这些数据可以在本书附带的代码包中找到,或者在 GitHub 上查看 github.com/TrainingByPackt/Supervised-Learning-with-Python。这个数据集包含的值旨在演示本章的主题,不应与来自科学研究的实际数据混淆。
练习 28:使用移动平均绘制数据
正如我们在第一章中讨论的,Python 机器学习工具包,如果要构建一个高性能的模型,彻底理解所使用的数据集是至关重要的。所以,考虑到这一点,让我们利用这个练习加载、绘制并查询数据源:
-
导入
numpy、pandas和matplotlib包,并使用替代名称:Import numpy as np import pandas as pd import matplotlib.pyplot as plt -
使用 pandas 的
read_csv函数加载包含synth_temp.csv数据集的 CSV 文件,然后显示前五行数据:df = pd.read_csv('synth_temp.csv') df.head()输出将如下所示:
![图 3.3:前五行数据]()
图 3.3:前五行数据
-
由于我们只对 1901 年至 2010 年的数据感兴趣,因此需要删除 1901 年之前的所有行:
df = df.loc[df.Year > 1901] df.head()输出将是:
![图 3.4:删除所有 1901 年之前的行后的前五行数据]()
图 3.4:删除所有 1901 年之前的行后的前五行数据
-
原始数据集包含每年多个温度测量值,较晚年份的测量值更多(2010 年有 12 个),较早年份的测量值较少(1841 年有 6 个);然而,我们感兴趣的是每年的平均温度列表。按年份对数据进行分组,并使用 DataFrame 的
agg方法来计算每年的平均值:df_group_year = df.groupby('Year').agg(np.mean) df_group_year.head()输出将是:
![图 3.5:每年平均数据]()
图 3.5:每年平均数据
-
鉴于数据相当嘈杂,移动平均滤波器将提供总体趋势的有用指标。移动平均滤波器仅计算最后 N 个值的平均值,并将此平均值分配给 (N+1) 个样本。使用 10 年的窗口计算温度测量的移动平均信号的值:
window = 10 rolling = df_group_year.AverageTemperature.rolling(window).mean() rolling.head(n=20)我们将获得以下输出:
![图 3.6: 移动平均信号的值]()
图 3.6:移动平均信号的值
注意,前 9 个样本是
NaN,这是由于移动平均滤波器窗口的大小。窗口大小为 10,因此需要 9(10-1)个样本来生成第一个平均值,因此前 9 个样本为NaN。 -
最后,绘制每年的测量值以及移动平均信号:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();输出如下:

图 3.7:年均空气温度
图 3.7 是此练习的预期输出,是每年平均陆地温度测量的图表,并显示了 10 年移动平均趋势。通过查看此图,我们可以立即得出几个有趣的观察结果。第一个观察结果是,从 1901 年到大约 1960 年间温度保持相对稳定,之后呈增加趋势,直到数据截至于 2010 年。其次,测量中存在相当多的散点或噪声。
活动 5:绘制带有移动平均线的数据
对于此活动,我们获取了奥斯汀,德克萨斯州的天气信息数据集(austin_weather.csv),该数据集可在附带的源代码中找到,并将查看平均日温度的变化。我们将为此数据集绘制移动平均滤波器。
在开始之前,我们需要导入一些库,可以按如下方式完成:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
需要执行的步骤如下:
-
从 CSV 文件中将数据集加载到 pandas DataFrame 中。
-
我们只需要
Date和TempAvgF列;将数据集中的所有其他列删除。 -
最初,我们只对第一年的数据感兴趣,因此需要仅提取该信息。在 DataFrame 中为年份值创建一列,并从
Date列中的字符串中提取年份值作为整数,并将这些值分配给Year列。注意,温度是每天记录的。
-
重复此过程以提取月份值,并将这些值存储为
Month列中的整数。 -
将第一年的数据复制到一个 DataFrame 中。
-
计算 20 天的移动平均滤波器。
-
绘制原始数据和移动平均信号,其中 x 轴是年中的日期编号。
注意
此活动的解决方案可在第 325 页找到。
最小二乘法
机器学习和人工智能领域本质上是统计学的一个专业分支,因此,时不时地反思这些起源是很重要的,这有助于我们深入理解模型如何作为预测工具被应用。通过回顾机器学习如何从统计学中发展而来,并对比今天更现代的方法,亦是很有趣的。线性回归模型就是一个很好的例子,它既可以用来展示一些经典的求解方法,如最小二乘法,也可以展示一些更现代的方法,如梯度下降法,这些我们将在本章中讨论。线性模型还具有额外的优势,它包含了高中数学中常见的概念,例如直线方程,为描述拟合数据的方法提供了一个有用的平台。
求解线性模型的传统方法是通过诸如 scikit-learn、SciPy、Minitab 和 Excel 等工具包执行的最小二乘法,这也是我们将要讨论的第一个方法。参考我们的标准直线方程(图 3.1),m 是直线的斜率或梯度,c 是 y 轴偏移。通过首先确定平均的 x 和 y 值(分别表示为
和
),在最小二乘法中这些值可以直接计算出来。计算出平均值后,我们可以通过将 x 值的差与平均值的差以及 y 值的差与平均值的差相乘,再除以 x 与平均值的差的平方,来计算梯度 m。然后,可以通过使用新计算的 m 和
和
来解出偏移 b。数学上可以表示如下:

图 3.8:最小二乘法
我们可以从更实际的角度来理解这一点,回顾一下梯度实际上就是纵向(或 y)值的变化除以横向(或 x)值的变化。在年均气温随时间变化的背景下,我们可以看到我们在计算的是每个温度值与平均值的差的总和,乘以每个时间值与平均值的差。通过将结果除以时间差的平方和,得出的趋势梯度即为完成,从而为温度随时间的模型提供部分数据。
现在,我们不需要手动计算这些值,虽然这并不困难。但像 SciPy 和 scikit-learn 这样的专业库可以帮助我们完成这项工作,并且关注一些细节,例如计算效率。对于本节内容,我们将使用 scikit-learn 作为我们的首选库,因为它提供了很好的 scikit-learn 接口入门。
需要注意的一个实现细节是,scikit-learn 的线性回归模型实际上是 SciPy 普通最小二乘法函数的封装,并提供了一些附加的便捷方法:

图 3.9:scikit-learn 的线性回归实现
scikit-learn 模型 API
scikit-learn API 使用一个相对简单的代码模式,无论构建的是何种类型的模型。简单来说,模型必须首先定义所有与训练或拟合过程相关的超参数。在定义模型时,会返回一个模型对象,然后在模型构建的第二阶段——训练或拟合时使用该对象。调用模型对象的fit方法并提供适当的训练数据,即可使用定义的超参数训练模型。我们现在将使用这个模式来构建我们的第一个线性回归模型。
练习 29:使用最小二乘法拟合线性模型
在这个练习中,我们将使用最小二乘法构建我们的第一个线性回归模型。
-
我们将在这个练习中使用 scikit-learn 的
LinearRegression模型,因此从 scikit-learn 的linear_regression模块中导入该类:from sklearn.linear_model import LinearRegression -
使用默认值构建线性回归模型;即,计算 y 截距的值并且不对输入数据进行归一化:
model = LinearRegression() model![图 3.10:线性回归模型]()
图 3.10:线性回归模型
-
现在我们准备好将模型拟合或训练到数据上了。我们将提供年份作为输入,年均温度作为输出。请注意,scikit-learn 模型的
fit方法期望提供二维数组作为X和Y的值。因此,年份或索引值需要调整为适合该方法的形状。使用.values方法获取索引值,并将其调整为((-1, 1))的形状,这样就变成了一个 N x 1 数组。NumPy 形状定义中的值-1表示该值由数组的当前形状和目标形状推断得出:model.fit(df_group_year.index.values.reshape((-1, 1)), gf_group_year.AverageTemperature)输出将如下所示:
![图 3.11:fit 方法的输出]()
图 3.11:fit 方法的输出
-
通过打印
model.coef_(即 m 的值)和model.intercept_(即 y 截距的值),获取模型的参数:print(f'm = {model.coef_[0]}') print(f'c = {model.intercept_}') print('\nModel Definition') print(f'y = {model.coef_[0]:0.4}x + {model.intercept_:0.4f}')输出将是:
![图 3.12:模型系数和模型截距的输出]()
图 3.12:模型系数和模型截距的输出
-
现在我们已经生成了模型,可以预测一些值来构建趋势线。那么,让我们使用第一个、最后一个和平均年份的值作为输入来预测当地温度。用这些值构建一个 NumPy 数组并将其命名为
trend_x。完成后,将trend_x的值传递给模型的predict方法来获取预测值:trend_x = np.array([ df_group_year.index.values.min(), df_group_year.index.values.mean(), df_group_year.index.values.max() ]) trend_y = model.predict(trend_x.reshape((-1, 1))) trend_y输出将如下所示:
![图 3.13:数组显示最小值、均值和最大值]()
图 3.13:数组显示最小值、均值和最大值
-
现在绘制由模型生成的趋势线,并将模型参数叠加到之前的图表上,包含原始数据:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(trend_x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();输出将如下所示:

图 3.14:线性回归 – 第一个简单的线性模型
现在我们有了模型,需要评估其性能,以了解它与数据的拟合程度,并与其他可能生成的模型进行比较。我们将在第六章《模型评估》中详细讨论这个主题,我们将探讨验证和交叉验证的方法,但目前我们将计算模型与数据集之间的R-squared值。R-squared,通常在统计建模中报告,是预测值与实际值之间的平方和与实际值与其均值之间的平方和之比。完美的拟合将有一个 R² 值为 1,而随着性能的下降,分数会降低至 0。

图 3.15:R-squared 分数
我们可以使用score方法计算 R² 值:
# Note the year values need to be provided as an N x 1 array
r2 = model.score(df_group_year.index.values.reshape((-1, 1)), df_group_year.AverageTemperature)
print(f'r2 score = {r2:0.4f}')
我们将得到如下输出:

图 3.16:模型与数据集的 R-squared 分数
因此,观察图 3.14中的趋势线,我们可以看到线性模型表现良好。在 1960 年后的移动平均线性区域,它的表现明显更好,但对于 1970 年之前的数据仍有改进的空间。我们能做些什么来处理这个问题吗?似乎两个单独的线性模型可能比一个模型表现得更好。1960 年前的数据可以作为一个模型,1960 年后的数据作为另一个模型?我们可以这样做,直接将数据分开,创建两个独立的模型,分别评估它们,然后将它们以分段的方式合并起来。但我们也可以通过使用虚拟变量,在现有模型中加入类似的特征。
注意
在继续之前,需要注意的是,在报告机器学习模型的表现时,不得使用用于训练模型的数据来评估模型表现,因为这会给出模型表现的乐观看法。我们将在第六章《模型评估》中讨论验证的概念,包括评估和报告模型表现。然而,为了本章的目的,我们将使用训练数据来检查模型的表现;只需记住,一旦你完成了第六章《模型评估》,你将能更好地理解这一点。
活动 6:使用最小二乘法进行线性回归
对于本活动,我们将使用前一个活动中使用的德克萨斯州奥斯丁的天气数据集。我们将使用最小二乘法为该数据集绘制线性回归模型。
在我们开始之前,我们需要导入一些库并加载来自前一个活动的数据,方法如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = df[:365]
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
需要执行的步骤如下:
-
可视化测量值。
-
可视化滚动平均值。
-
使用默认参数创建线性回归模型,也就是说,为模型计算 y 截距,并且不对数据进行归一化处理。
-
现在拟合模型,其中输入数据为年份中的天数(1 到 365),输出为平均温度。为了便于后续计算,插入一列(
DayOfYear),对应于该测量的年份中的天数。 -
使用
DayOfYear值作为输入,df_first_year.TempAvgF作为输出,来拟合模型。 -
打印模型的参数。
-
让我们检查模型提供的趋势线。只需使用线性方程中的第一个、中间和最后的值(年份中的天数)来绘制即可。
-
绘制带有趋势线的值。
-
评估模型的表现。
-
让我们检查模型拟合数据的情况。计算 r2 得分来了解。
注意
本活动的解答可以在第 329 页找到。
带虚拟变量的线性回归
虚拟变量是我们可以通过现有数据集提供的信息引入模型中的分类变量。这些变量的设计和选择被视为特征工程的一部分,具体结果会根据变量的选择而有所不同。我们之前观察到,移动平均值从大约 1960 年开始持续上升,初始的平稳期大约在 1945 年结束。我们将引入两个虚拟变量,Gt_1960和Gt_1945;这两个变量将表示测量时间是否大于 1960 年和 1945 年。虚拟变量通常被赋值为 0 或 1,以表示每行数据是否具有指定类别。在我们的例子中,由于Year的值较大,我们需要增加虚拟变量的正值,因为在Year的值达到千位数时,1 的值几乎没有影响。在接下来的练习中,我们将展示回归模型可以由离散值和连续值组成,并且根据适当选择虚拟变量,模型的性能可以得到改善。
练习 30: 引入虚拟变量
在本练习中,我们将向线性回归模型中引入两个虚拟变量:
-
为了方便起见,将
df_group_year数据框的索引值分配给Year列:df_group_year['Year'] = df_group_year.index -
创建一个虚拟变量,并为其添加一个名为
Gt_1960的列,其中,如果年份小于 1960,则值为0,如果大于 1960,则值为10:df_group_year['Gt_1960'] = [0 if year < 1960 else 10 for year in df_group_year.Year] # Dummy Variable - greater than 1960 df_group_year.head(n=2)输出结果如下:
![图 3.17: 添加的列 Gt_1960]()
图 3.17: 添加的列 Gt_1960
-
创建一个虚拟变量,并为其添加一个名为
Gt_1945的列,其中,如果年份小于 1945,则值为0,如果大于 1945,则值为10:df_group_year['Gt_1945'] = [0 if year < 1945 else 10 for year in df_group_year.Year]# Dummy Variable - greater than 1945 df_group_year.head(n=2)输出结果将是:
![图 3.18: 添加的列 Gt_1945]()
图 3.18: 添加的列 Gt_1945
-
调用
tail()方法查看df_group_year数据框的最后两行,以确认 1960 年后和 1945 年后的标签是否已正确分配:df_group_year.tail(n=2)输出结果将是:
![图 3.19: 最后两行值]()
图 3.19: 最后两行值
-
通过将
Year、Gt_1960和Gt_1945列作为输入,AverageTemperature列作为输出,来拟合带有附加虚拟变量的线性模型:# Note the year values need to be provided as an N x 1 array model.fit(df_group_year[['Year', 'Gt_1960', 'Gt_1945']], df_group_year.AverageTemperature)输出结果将是:
![图 3.20: 基于数据拟合的线性模型]()
图 3.20: 基于数据拟合的线性模型
-
检查新模型的 R-squared 得分,并与训练数据进行比较,看看是否有所改进:
# Note the year values need to be provided as an N x 1 array r2 = model.score(df_group_year[['Year', 'Gt_1960', 'Gt_1945']], df_group_year.AverageTemperature) print(f'r2 score = {r2:0.4f}')输出结果如下:
![图 3.21: 模型的 R-squared 得分]()
图 3.21: 模型的 R-squared 得分
-
我们已经取得了进展!考虑到第一个模型的性能为 0.8618,这在准确度上是一个合理的步骤。我们将绘制另一条趋势线,但由于虚拟变量增加了额外的复杂性,我们需要更多的值。使用
linspace创建 20 个在 1902 到 2013 年之间均匀分布的值:# Use linspace to get a range of values, in 20 year increments x = np.linspace(df_group_year['Year'].min(), df_group_year['Year'].max(), 20) x我们将得到以下输出:
![图 3.22:使用 linspace 创建的 20 年数组]()
图 3.22:使用 linspace 创建的 20 年数组
-
创建一个形状为20 x 3的零数组,并将第一列的值填充为
x,第二列填充为大于 1960 的虚拟变量值,第三列填充为大于 1945 的虚拟变量值:trend_x = np.zeros((20, 3)) trend_x[:,0] = x # Assign to the first column trend_x[:,1] = [10 if _x > 1960 else 0 for _x in x] # Assign to the second column trend_x[:,2] = [10 if _x > 1945 else 0 for _x in x] # Assign to the third column trend_x输出结果将是:
![图 3.23:寻找 trend_x]()
图 3.23:寻找 trend_x
-
现在通过对
trend_x进行预测,获取趋势线的 y 值:trend_y = model.predict(trend_x) trend_y输出结果如下:
![图 3.24:寻找 trend_y]()
图 3.24:寻找 trend_y
-
绘制趋势线:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(trend_x[:,0], trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();输出结果如下:

图 3.25:使用虚拟变量的预测
引入虚拟变量对模型进行了相当大的改进,但从趋势线来看,这似乎不是一个自然现象(如温度)应遵循的合理路径,可能存在过拟合的问题。我们将在第五章,集成建模中详细讲解过拟合;不过,我们可以先使用线性回归来拟合一个更平滑的预测曲线模型,比如抛物线。
活动 7:虚拟变量
对于本活动,我们将使用在之前活动中使用的德克萨斯州奥斯汀的天气数据集。在本活动中,我们将使用虚拟变量来增强该数据集的线性回归模型。
在我们开始之前,我们需要导入一些库,并从之前的活动中加载数据,具体操作如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = pd.read_csv('activity_first_year.csv')
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
# Trendline values
trend_x = np.array([
1,
182.5,
365
])
需要执行的步骤如下:
-
绘制原始数据(
df)和移动平均(rolling)。 -
从上一步的结果来看,似乎在第 250 天左右有一个拐点。创建一个虚拟变量,将此特征引入线性模型中。
-
检查首尾样本,以确认虚拟变量是否正确。
-
使用最小二乘法线性回归模型,将模型拟合到
DayOfYear的值和虚拟变量,预测TempAvgF。 -
计算 R2 分数。
-
使用
DayOfYear的值,利用该模型构造趋势线进行预测。 -
将趋势线与数据和移动平均进行对比绘制。
注意
本活动的解答请见第 334 页。
线性回归的抛物线模型
线性回归模型不仅仅局限于直线模型。我们可以使用完全相同的技术拟合一些更复杂的模型。我们提到过数据似乎具有一些抛物线特征,所以我们来尝试拟合一个抛物线模型。提醒一下,抛物线的方程是:

图 3.26:抛物线的方程
添加这个平方项将把模型从一条直线转换为具有抛物线(或弧线)轨迹的模型。

图 3.27:抛物线曲线
练习 31:使用线性回归拟合抛物线模型
为了使用线性回归拟合一个抛物线模型,我们只需要稍微调整一下输入。在这个练习中,我们将看到如何做到这一点:
-
我们需要做的第一件事是为年份值提供平方项。为了方便,创建一个索引的副本并将其存储在
Year列中。现在对Year列进行平方,提供抛物线特征,并将结果分配给Year2列:df_group_year['Year'] = df_group_year.index df_group_year['Year2'] = df_group_year.index ** 2 df_group_year.head()我们将得到这个:
![图 3.28:前五行数据]()
图 3.28:前五行数据
-
将数据拟合到模型中。这一次,我们需要提供两组值作为模型的输入,
Year和Year2,这相当于将 x 和 x**2 传递给抛物线方程。由于我们提供了两列数据,因此不需要重新调整输入数据,它将默认作为 N x 2 数组提供。目标 y 值保持不变:# Note the year values need to be provided as an N x 1 array model.fit(df_group_year[['Year2', 'Year']], df_group_year.AverageTemperature)输出将如下所示:
![图 3.29:模型拟合]()
图 3.29:模型拟合
-
打印模型的参数,通过查看系数和截距;现在将有两个系数需要打印:
print(f'a = {model.coef_[0]}') print(f'm = {model.coef_[1]}') print(f'c = {model.intercept_}') print('\nModel Definition') print(f'y = {model.coef_[0]:0.4}x² + {model.coef_[1]:0.4}x + {model.intercept_:0.4f}')输出将会是:
![图 3.30:模型系数和截距]()
图 3.30:模型系数和截距
-
使用
score方法评估模型的表现。性能有所提高吗?# Note the year values need to be provided as an N x 1 array r2 = model.score(df_group_year[['Year2', 'Year']], df_group_year.AverageTemperature) print(f'r2 score = {r2:0.4f}')我们将获得以下输出:
![图 3.31:R 平方得分]()
图 3.31:R 平方得分
-
是的,模型在虚拟变量方法上略有改进,但让我们看看趋势线,看看它是否更合理地拟合。像之前那样绘制趋势线。为了有效地绘制趋势线的抛物线弧线,我们需要更多的预测值。使用
linspace创建 1902 和 2013 之间 20 个线性间隔的值:# Use linspace to get a range of values, in 20 yr increments x = np.linspace(df_group_year['Year'].min(), df_group_year['Year'].max(), 20) x我们将得到这个:
![图 3.32:使用 linspace 查找 20 个增量]()
图 3.32:使用 linspace 查找 20 个增量
-
现在我们训练的模型需要两列年份数据作为输入:第一列包含平方的年份值,第二列仅包含年份值本身。为了向模型提供数据,创建一个 20 行 2 列的零数组(
trend_x)。将x的平方值赋值给trend_x的第一列,直接将x赋值给trend_x的第二列:trend_x = np.zeros((20, 2)) trend_x[:,0] = x ** 2 # Assign to the first column trend_x[:,1] = x # Assign to the second column trend_x输出结果为:
![图 3.33:x 变量的趋势]()
图 3.33:x 变量的趋势
-
现在通过对
trend_x进行预测,获取趋势线的y值:trend_y = model.predict(trend_x) trend_y我们将得到如下结果:
![图 3.34:y 变量的趋势]()
图 3.34:y 变量的趋势
-
按照直线模型绘制趋势线。记住,
trend_y的x轴值是年份,也就是trend_x的第二列,而不是年份的平方:fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(trend_x[:,1], trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();输出结果如下:

图 3.35:带有抛物线模型的线性回归
参考图 3.35,我们可以看到使用抛物线模型的性能优势,趋势线几乎与 10 年移动平均线相吻合。考虑到年度平均原始数据中的噪声量,这个拟合效果相当不错。在这种情况下,不应该期望模型能够完美地拟合数据。如果我们的模型能够完美拟合观察到的示例,那么就可能存在严重的过拟合问题,导致模型在面对未见数据时的预测能力较差。
活动 8:其他线性回归模型类型
我们尝试了标准的线性模型以及虚拟变量。在这个活动中,我们将尝试几个不同的函数,看看如何更好地拟合数据。对于每个不同的函数,确保打印函数参数、R2 值,并将趋势线与原始数据及移动平均数据进行比较。
尝试几个不同的函数,实验数据,看看你的预测能力能达到什么程度。在这个活动中,我们将使用正弦函数。
在开始之前,我们需要导入一些库并加载来自先前活动的数据,操作步骤如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = pd.read_csv('activity_first_year.csv')
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
# Trendline values
trend_x = np.array([
1,
182.5,
365
])
执行的步骤如下:
-
使用正弦曲线函数作为模型的基础。
-
打印模型的参数。
-
计算 r2 值以衡量模型性能。
-
构建趋势线值。
-
绘制带有原始数据和移动平均线的趋势线。
注意
该活动的解答可以在第 338 页找到。
通用模型训练
最小二乘法构建线性回归模型是一种有用且准确的训练方法,前提是数据集的维度较低,并且系统内存足够大,能够管理数据集以及在 scikit-learn 实现中进行矩阵除法操作。近年来,大型数据集变得更加容易获取,许多大学、政府甚至一些公司都在线免费发布了大型数据集;因此,在使用最小二乘法回归建模时,可能相对容易超出系统内存。在这种情况下,我们需要采用不同的训练方法,比如梯度下降,它不那么容易受到高维度的影响,能够训练大规模数据集,并避免使用内存密集型的矩阵运算。在我们进一步探讨梯度下降之前,我们将重新审视训练模型的一般过程,因为大多数训练方法,包括梯度下降,都遵循这一通用过程(图 3.36)。训练过程涉及将模型及其参数反复暴露给一组示例训练数据,并将模型输出的预测值传递给指定的成本或误差函数。
成本函数用于确定模型与目标值的接近程度,并衡量训练过程中进展的程度。过程的最后一步是定义训练超参数,如本章开头所讨论的那样,这些超参数是调节模型更新过程的手段:

图 3.36:通用训练过程
梯度下降
梯度下降过程可以总结为根据成本函数定义的误差,逐步更新模型参数,并响应系统中的错误。有多种成本函数可以选择,具体取决于拟合的模型类型或解决的问题。我们将选择简单但有效的均方误差成本函数,但首先,我们将以与机器学习文献中普遍使用的符号一致的方式重写我们的模型方程。以直线方程为我们的模型:

图 3.37:直线方程
它可以重写为:

图 3.38:简化线性模型
其中
是模型做出的预测,按照惯例,
用来表示截距项。使用新的模型符号,我们可以定义均方误差函数如下:

图 3.39:均方误差
其中 y**t 是对应的真实值,N 是训练样本的数量。
定义了这两个函数后,我们现在可以更详细地看一下梯度下降算法:
-
梯度下降法从对所有
的初始随机猜测开始。 -
对训练集中的每个样本,使用随机值为
进行预测。 -
然后计算这些参数
的误差。 -
然后修改
的值,做出与误差成比例的小调整,试图最小化误差。更正式地说,更新过程使用
的当前值,并减去
中与
相关的部分,该部分等于
乘以小调整量
,也称为学习率。
不深入探讨数学细节,更新参数或权重(
)的方程可以写成如下:

图 3.40:梯度下降更新步骤
让我们来讨论这个方程:
-
:=操作符表示计算机编程概念中的变量重新赋值或更新。 -
这个训练过程将持续到收敛;也就是说,直到权重的变化非常小,以至于参数几乎没有变化,或者直到我们介入并停止该过程,就像在交叉验证中的情况一样。
-
为学习率分配的值对训练过程至关重要,因为它定义了权重变化的大小,从而决定了沿误差曲线下降的步伐大小。如果值过小,训练过程可能会耗费过长时间,或可能会陷入误差曲线的局部最小值,无法找到最优的全局值。相反,如果步伐过大,训练过程可能会变得不稳定,因为它们会越过局部和全局最小值。
该过程在下图中进行了可视化:

图 3.41:梯度下降过程
注意
关于设置学习率的一般建议是,从较大的值开始,比如 0.1,如果找不到解决方案,即误差为NaN(不是一个数字),则将其减少一个数量级。继续这样做,直到训练过程持续进行并且误差不断减少。一旦你对模型满意并且几乎完成时,将学习率稍微减小,并让训练继续更长时间。
虽然这个过程听起来可能很复杂,但它远没有看起来那么可怕。梯度下降可以总结为:第一次猜测权重的值,计算猜测的误差,对权重进行微小调整,并不断重复这一过程,直到误差收敛到最小值。为了加深我们的理解,让我们看一个更具体的例子。我们将使用梯度下降法来训练我们在练习 29:使用最小二乘法拟合线性模型中构建的原始线性回归模型,使用梯度下降法替代最小二乘法。
练习 32:使用梯度下降法的线性回归
在开始梯度下降过程之前,我们需要花一点时间来设置模型。在我们的 Jupyter 笔记本中,执行以下步骤:
-
编写一个函数来定义我们的线性模型。这时,使用线性模型的简化形式(图 3.38)的优势就显现出来了。我们可以利用线性代数乘法,将权重(theta)与输入值,x,进行相乘,这等价于
:def h_x(weights, x): return np.dot(weights, x).flatten() -
为了使用这种线性代数乘法技巧,我们必须通过插入一行全为 1 的值来修改输入数据,以表示偏置项。创建一个具有两列形状的全 1 数组(一个用于权重的梯度项,另一个用于偏置项)。将标准化后的
Year值插入到新创建数组的第一行。要在梯度下降过程中使用输入数据,我们还必须将所有值标准化到 0 到 1 之间。这是过程中的一个关键步骤,因为如果一个变量的值是 1,000 级别,而第二个变量是 10 级别,那么第一个变量在训练过程中将比第二个变量影响大 100 倍,这可能会导致模型无法训练。通过确保所有变量都在 0 到 1 之间缩放,它们将在训练过程中有相同的影响。通过将
Year的值除以最大值来对输入数据进行缩放:x = np.ones((2, len(df_group_year))) x[0,:] = df_group_year.Year x[1,:] = 1 x /= x.max() x[:,:5]你将得到如下输出:
![图 3.42:修改后的数据]()
图 3.42:修改后的数据
-
正如我们所学,我们需要对权重的值进行初始猜测。我们需要定义两个权重值,一个用于梯度,另一个用于y截距。为了确保每次初始化时使用相同的第一个随机数,需要对 NumPy 的随机数生成器进行初始化。初始化随机数生成器确保每次运行脚本时,生成的随机数集是相同的。这保证了多次运行中模型的一致性,并提供了检查模型表现是否受变化影响的机会:
np.random.seed(255) # Ensure the same starting random values -
使用均值为 0、标准差为 0.1 的正态分布随机数初始化权重。我们希望初始化的权重是随机的,但仍然接近零,以便给它们找到良好解的机会。为了执行
h_x中的矩阵乘法操作,将随机数重塑为一行两列(一个用于梯度,一个用于y截距):Theta = np.random.randn(2).reshape((1, 2)) * 0.1 Theta我们将得到如下输出:
![图 3.43:Theta 值]()
图 3.43:Theta 值
-
定义真实值为平均年气温:
y_true = df_group_year.AverageTemperature.values -
将代价函数(均方误差)定义为 Python 函数:
def J_theta(pred, true): return np.mean((pred - true) ** 2) # mean squared error -
定义如前所述的学习率。这是一个非常重要的参数,必须适当设置。如前所述,设置得太小,模型可能需要很长时间才能找到最小值;设置得太大,可能根本找不到最小值。将学习率定义为
1e-6:gamma = 1e-6 -
定义一个函数来实现梯度下降的一步(图 3.40)。该函数将接收预测值和真实值,以及x和gamma的值,并返回需要添加到权重(theta)的值:
def update(pred, true, x, gamma): return gamma * np.sum((true - pred) * x, axis=1) -
定义我们希望训练过程运行的最大 epoch 数(或迭代次数)。每个 epoch 根据给定的x预测y值(标准化的年均气温),并根据预测误差更新权重:
max_epochs = 100000 -
做一个初步预测,并使用定义的
h_x和J_theta函数计算该预测的误差或代价:y_pred = h_x(Theta, x) print(f'Initial cost J(Theta) = {J_theta(y_pred, y_true): 0.3f}')输出将如下所示:
![图 3.44:J theta 的初始代价]()
图 3.44:J theta 的初始代价
-
手动完成第一次更新步骤。使用新预测的值来调用
update函数,再次调用h_x获取预测值,并得到新的误差:Theta += update(y_pred, y_true, x, gamma) y_pred = h_x(Theta, x) print(f'Initial cost J(Theta) = {J_theta(y_pred, y_true): 0.3f}')我们将得到如下输出:
![图 3.45:更新后的 J theta 代价]()
图 3.45:更新后的 J theta 代价
-
注意到误差的微小减少;因此,需要很多 epoch 的训练。将
predict和update函数调用放入for循环中,循环次数为max_epochs,并在每第十个 epoch 打印相应的误差:error_hist = [] epoch_hist = [] for epoch in range(max_epochs): Theta += update(y_pred, y_true, x, gamma) y_pred = h_x(Theta, x) if (epoch % 10) == 0: _err = J_theta(y_pred, y_true) error_hist.append(_err) epoch_hist.append(epoch) print(f'epoch:{epoch:4d} J(Theta) = {_err: 9.3f}')输出将如下所示:
![图 3.46:十个 epoch]()
图 3.46:十个 epoch
-
通过绘制
epoch_hist与error_hist来可视化训练历史:plt.figure(figsize=(10, 7)) plt.plot(epoch_hist, error_hist); plt.title('Training History'); plt.xlabel('epoch'); plt.ylabel('Error');输出将是:
![图 3.47:训练历史曲线:一个非常重要的工具]()
图 3.47:训练历史曲线:一个非常重要的工具
注意到误差在 30,000 个 epoch 时达到了一个渐近值,因此
max_epochs可以减少。 -
使用
sklearn.metrics中的r2_score函数来计算通过梯度下降训练的模型的 R 平方值:from sklearn.metrics import r2_score r2_score(y_true, y_pred)我们将得到如下输出:
![图 3.48:R 平方值]()
图 3.48:R 平方值
-
为了绘制新模型的趋势线,再次创建 1901 到 2013 年之间线性间隔的 20 个年份值:
# Use linspace to get a range of values, in 20 yr increments x = np.linspace(df_group_year['Year'].min(), df_group_year['Year'].max(), 20) x输出结果如下:
![图 3.49:使用 linspace 的值]()
图 3.49:使用 linspace 的值
-
为了将这些数据与我们的模型一起使用,我们必须首先将最大值标准化到 0 到 1 之间,并插入一行 1。执行此步骤的方式与为训练准备数据时的 步骤 2 类似。
trend_x = np.ones((2, len(x))) trend_x[0,:] = x trend_x[1,:] = 1 trend_x /= trend_x.max() trend_x输出结果如下:
![图 3.50:x 的趋势]()
图 3.50:x 的趋势
-
使用训练过程中保存的权重,调用
h_x模型函数,得到趋势线的预测 y 值:trend_y = h_x(Theta, trend_x) trend_y![图 3.51:y 的趋势]()
图 3.51:y 的趋势
-
用数据绘制趋势线:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();输出结果如下:

图 3.52:使用梯度下降的平均气温测量值
恭喜!你刚刚用梯度下降训练了第一个模型。这是一个重要的步骤,因为这个简单的工具可以用来构建更复杂的模型,如逻辑回归和神经网络模型。然而,我们首先需要注意一个重要的观察结果:梯度下降模型产生的 r 方值不如最小二乘法模型高。
在梯度下降的第一步,我们猜测一些合理的权重值,然后对权重进行小幅调整,试图减少误差,并在误差停止减少时停止训练。梯度下降的优势体现在两个特定的应用中:
-
解决更复杂的模型,这些模型的数学最优解尚未找到或无法找到
-
提供一种训练方法,适用于数据集或参数非常庞大,以至于物理硬件的限制(例如可用内存)阻止了使用其他方法,如最小二乘法
所以,如果数据集不是特别庞大并且可以通过优化解决,我们应该毫不犹豫地使用更精确的方法。话虽如此,还有很多方法可以修改梯度下降过程,包括不同类型的梯度下降算法、更高级的学习率使用方式以及训练过程中数据的供给方式。这些修改超出了本书的范围,因为关于梯度下降过程和改进性能方法的整本书都有写作空间。
练习 33:优化梯度下降
在前一个练习中,我们直接实现了梯度下降;然而,我们通常不会使用这种实现。scikit-learn 的梯度下降方法包含了许多优化,并且只需要几行代码即可使用:
-
导入
SGDRegressor类,并使用与前一个练习中相同的参数构建模型:from sklearn.linear_model import SGDRegressor model = SGDRegressor( max_iter=100000, learning_rate='constant', eta0=1e-6, random_state=255, tol=1e-6, penalty='none', ) -
使用年份值,除以最大年份值,作为输入,并与
AverageTemperature值作为真实值进行拟合:x = df_group_year.Year / df_group_year.Year.max() y_true = df_group_year.AverageTemperature.values.ravel() model.fit(x.values.reshape((-1, 1)), y_true) -
使用训练好的模型预测值,并确定 r-squared 值:
y_pred = model.predict(x.values.reshape((-1, 1))) r2_score(y_true, y_pred) -
绘制由模型确定的趋势线,除了原始数据和移动平均值之外:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();输出将如下所示:

图 3.53:优化后的梯度下降预测趋势线
将此图与使用梯度下降手动实现构建的图形进行比较。注意它们的相似性:这使我们有信心,梯度下降的两种实现都是正确的。
活动 9:梯度下降
在本活动中,我们将实现与 活动 6,使用最小二乘法进行线性回归 相同的模型;但是,我们将使用梯度下降过程。
在我们开始之前,我们需要导入一些库并加载来自先前活动的数据,可以按照以下方式进行:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import SGDRegressor
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = pd.read_csv('activity_first_year.csv')
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
# Trendline values
trend_x = np.array([
1,
182.5,
365
])
执行的步骤如下:
-
创建一个通用的梯度下降模型,并将年份值标准化到 0 到 1 之间。
-
拟合模型。
-
打印模型的详细信息。
-
准备 x
(trend_x)趋势线的值,通过除以最大值。使用梯度下降模型预测y_trend_values。 -
绘制数据、移动平均值和趋势线。
注意
该活动的解决方案可以在第 341 页找到。
多元线性回归
我们已经涵盖了常规线性回归,以及带有多项式项的线性回归,并考虑了使用最小二乘法和梯度下降法训练它们。本章的这一部分将考虑另一种类型的线性回归:多元线性回归,其中使用多个变量(或特征)来构建模型。为了研究多元线性回归,我们将使用波士顿住房数据集的修改版,该数据集可从 archive.ics.uci.edu/ml/index.php 获取。修改后的数据集可以在随附的源代码中找到,或者在 GitHub 上找到:github.com/TrainingByPackt/Supervised-Learning-with-Python,并且已被重新格式化以简化使用。该数据集包含波士顿地区不同属性的列表,包括按城镇计算的人均犯罪率、低社会经济状态人口的百分比、每个住宅的平均房间数,以及该地区业主自住房屋的中位数价值。
练习 34:多元线性回归
我们将使用波士顿住房数据集来构建一个多元线性模型,该模型预测在给定低社会经济状态人口百分比和每个住宅的平均房间数的情况下,业主自住房屋的中位数价值:
-
导入所需的依赖项:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression -
读取住房数据库:
df = pd.read_csv('housing_data.csv') df.head()head()函数将返回以下输出:![图 3.54:前五行]()
图 3.54:前五行
-
绘制两个列:平均房间数量(
RM)和低社会经济状态人口的百分比(PTRATIO):fig = plt.figure(figsize=(10, 7)) fig.suptitle('Parameters vs Median Value') ax1 = fig.add_subplot(121) ax1.scatter(df.LSTAT, df.MEDV, marker='*', c='k'); ax1.set_xlabel('% lower status of the population') ax1.set_ylabel('Median Value in $1000s') ax2 = fig.add_subplot(122, sharey=ax1) ax2.scatter(df.RM, df.MEDV, marker='*', c='k'); ax2.get_yaxis().set_visible(False) ax2.set_xlabel('average number of rooms per dwelling');输出结果如下:
![图 3.55:参数与中位数值的关系]()
图 3.55:参数与中位数值的关系
-
构建一个线性回归模型,用于预测低社会经济状态的百分比(
LSTAT)与中位数房产价值(MEDV)之间的关系,并计算模型的性能,使用 R 平方值来衡量:model = LinearRegression() model.fit(df.LSTAT.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1))) model.score(df.LSTAT.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1)))我们将获得以下输出:
![图 3.56:使用 LSTAT 的模型评分]()
图 3.56:使用 LSTAT 的模型评分
-
计算使用平均房间数量训练的线性模型的预测性能,以预测房产价值:
model.fit(df.RM.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1))) model.score(df.RM.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1)))输出结果如下:
![图 3.57:使用 RM 的模型评分]()
图 3.57:使用 RM 的模型评分
-
创建一个多元线性回归模型,使用
LSTAT和RM值作为输入,预测中位数房产价值:model.fit(df[['LSTAT', 'RM']], df.MEDV.values.reshape((-1, 1))) model.score(df[['LSTAT', 'RM']], df.MEDV.values.reshape((-1, 1)))输出结果如下:

图 3.58:使用 LSTAT 和 RM 的模型评分
自回归模型
自回归模型是经典统计建模技术的一部分,通常用于时间序列数据(即任何随时间变化的数据集),并在本章中涉及的线性回归技术基础上进行扩展。自回归模型在经济学和金融行业中广泛应用,因为它们在拥有大量测量数据的时间序列数据集中尤其有效。为了反映这一点,我们将数据集更改为 1986 年到 2018 年的标准普尔每日收盘价格,该数据可在随附的源代码中找到。

图 3.59:标准普尔 500 指数每日收盘价
自回归模型背后的主要原理是,给定足够的先前观察数据,就可以对未来做出合理的预测;换句话说,我们实际上是在使用数据集将其自身作为回归来构建模型,因此称为 自回归。这种关系可以用线性方程在数学上建模:

图 3.60:一阶自回归模型
其中
是时间 t 的预测值,
是模型的第一个权重,
是第二个权重,且
是数据集中前一个值,
是误差项。
图中的方程 图 3.60 表示使用数据集中的前一个值进行预测的模型。这是一个一阶自回归模型,可以扩展为包括更多的前置样本。
图 3.61 中的方程提供了一个二阶模型的示例,包括之前的两个值。
类似地,k**阶自回归模型包含与
相应的参数值,增加了关于模型前期观测的更多上下文。同样,参考 图 3.61 中的方程和 k**阶自回归模型,可以观察到自回归模型的递归特性。每个预测都会使用前一个值(或多个值)进行求和,因此,如果我们取之前预测的值,它们本身也会使用前一个值的预测,因此产生了递归。

图 3.61:二阶和 k 阶自回归模型
练习 35:创建自回归模型
我们将使用标准普尔 500 指数模型来创建一个自回归模型:
-
加载标准普尔 500 指数数据集,从日期列中提取表示年份的两位数字,并创建一个新的列
Year,将年份转换为四位数格式(例如,02-Jan-86 会变为 1986,31-Dec-04 会变为 2004):df = pd.read_csv('spx.csv') yr = [] for x in df.date: x = int(x[-2:]) if x < 10: x = f'200{x}' elif x < 20: x = f'20{x}' else: x = f'19{x}' yr.append(x) df['Year'] = yr df.head()我们将获得以下输出:
![图 3.62:前五行]()
图 3.62:前五行
-
绘制原始数据集,x 轴按五年为单位:
plt.figure(figsize=(10, 7)) plt.plot(df.close.values); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)');输出将如下所示:
![图 3.63:历年收盘价图]()
图 3.63:历年收盘价图
-
在我们构建自回归模型之前,必须首先检查该模型是否能够作为一个回归模型使用。为此,我们可以再次使用 pandas 库检查数据集与其平移了定义数量样本的副本之间的相关性,这种方法称为
shift方法,引入一个3的样本滞后并查看前 10 个收盘价的结果:df.close[:10].values df.close[:10].shift(3).values我们将得到这个输出:
![图 3.64:滞后三期的值]()
图 3.64:滞后三期的值
注意数组中引入了三个 NaN 值,并且最后三个值已从数组中删除。这是平移的效果,实质上是将数据集根据滞后期定义的时间段向前滑动。
-
将数据集按 100 的滞后平移并绘制结果:
plt.figure(figsize=(15, 7)) plt.plot(df.close.values, label='Original Dataset', c='k', linestyle='-'); plt.plot(df.close.shift(100), c='k', linestyle=':', label='Lag 100'); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)'); plt.legend();输出将如下所示:
![图 3.65:全年收盘价图]()
图 3.65:全年收盘价图
-
现在我们已经理解了时间平移,我们将确认数据是否能够与其自身相关。为此,使用 pandas 的
autocorrelation_plot方法来检查数据中的随机性:plt.figure(figsize=(10, 7)) pd.plotting.autocorrelation_plot(df.close);输出将如下所示:
![图 3.66:自相关与滞后的关系]()
图 3.66:自相关与滞后的关系
确定是否可以进行自动回归的所有信息都定义在这个图中。我们可以在x轴上看到,Lag的值从 0 到 8,000 个样本变化,而Autocorrelation的值大约在-0.4 到 1 之间。还有五条其他的附加线比较重要;不过,在这个y轴的刻度下,很难看到它们。
-
将y轴的限制设置为-0.1 到 0.1 之间:
plt.figure(figsize=(10, 7)) ax = pd.plotting.autocorrelation_plot(df.close); ax.set_ylim([-0.1, 0.1]);输出将如下所示:
![图 3.67:自相关与滞后图]()
图 3.67:自相关与滞后图
在增强视图中,我们可以看到有两条灰色虚线,表示 99%置信带,表明该序列是非随机的。实线灰色线表示 95%置信带。一旦自相关图在这些带内接近零,带有指定滞后的时间序列就变得足够随机,自动回归模型将不再适用。
-
为了进一步巩固我们的理解,绘制收盘价与滞后 100 个样本的收盘价的图。根据我们的自相关图,这两组数据之间存在高度的相关性。那是什么样子的呢?
plt.figure(figsize=(10,7)) ax = pd.plotting.lag_plot(df.close, lag=100);输出将是:
![图 3.68:自相关图]()
图 3.68:自相关图
-
创建收盘价与滞后 4,000 个样本的收盘价图。同样,根据自相关图,在滞后 4,000 时,自相关值大约为 0,表示两者之间没有真正的相关性,它们大多是随机的:
plt.figure(figsize=(10,7)) ax = pd.plotting.lag_plot(df.close, lag=4000);输出将是:
![图 3.69:收盘价与滞后 4,000 个样本的收盘价图]()
图 3.69:收盘价与滞后 4,000 个样本的收盘价图
-
现在我们准备创建模型了。然而,为了做到这一点,我们还需要一个 Python 包——
statsmodel包(www.statsmodels.org),它类似于 scikit-learn,但专门用于创建模型并执行使用更经典统计技术的测试。安装statsmodel包。你可以使用conda install或pip来安装。对于 Anaconda 安装,推荐使用conda install方法:#!pip install statsmodels !conda install -c conda-forge statsmodels -
从
statsmodel导入自回归类(AR),并使用收盘价数据构建模型:from statsmodels.tsa.ar_model import AR model = AR(df.close) -
使用
fit方法拟合模型,并打印出选择使用的滞后值以及模型的系数:model_fit = model.fit() print('Lag: %s' % model_fit.k_ar) print('Coefficients: %s' % model_fit.params)输出将是:
![图 3.70:滞后系数]()
图 3.70:滞后系数
请注意,每个权重都有 36 个系数,还有一个常数;为了简便起见,这里仅显示了一部分。所有系数可以在随附源代码中的
Ex7-AutoRegressors.ipynbJupyter 笔记本中找到。 -
使用该模型从第 36 个样本(滞后)开始创建一组预测,直到数据集结束后的 500 个样本为止:
predictions = model_fit.predict(start=36, end=len(df) + 500) predictions[:10].values我们将得到以下输出:
![]()
图 3.71:预测值
-
将预测值覆盖在原始数据集上进行绘制:
plt.figure(figsize=(10, 7)) plt.plot(predictions, c='g', linestyle=':', label='Predictions'); plt.plot(df.close.values, label='Original Dataset'); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)'); plt.legend();这将产生以下输出:
![图 3.72:全年价格变化图]()
图 3.72:全年价格变化图
请注意,预测值很好地跟踪了数据集,并且在数据集结束后,预测值相对线性。由于模型是基于之前的样本构建的,因此一旦数据集结束,它变得不那么确定,尤其是数据中没有重复模式时,这一点尤为明显。
-
拟合看起来非常接近——预测值与原始数据集之间的差异是什么样的?增强模型以观察差异:
plt.figure(figsize=(10, 7)) plt.plot(predictions, c='g', linestyle=':', label='Predictions'); plt.plot(df.close.values, label='Original Dataset'); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)'); plt.xlim([2000, 2500]) plt.ylim([420, 500]) plt.legend();这将提供以下图形:

图 3.73:原始数据集值的预测
通过使用自回归模型的这个练习,我们可以看到,在数据集缺失数据时,或当我们尝试在测量间隔之间进行预测时,使用这些模型具有显著的预测能力。对于 S&P 500 数据集显示的自回归模型,它能够有效地提供观测样本范围内的预测。然而,超出该范围,当预测未来值且没有测量数据时,预测能力可能会有所限制。
活动 10:自回归模型
在本活动中,我们将使用自回归模型来建模奥斯汀的天气数据集,并预测未来的值:
在开始之前,我们需要导入一些库,并从之前的活动中加载数据,操作如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.ar_model import AR
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
需要执行的步骤如下:
-
绘制完整的平均温度值(
df.TempAvgF)图,年份作为X轴。 -
创建一个 20 天的滞后,并将滞后的数据绘制在原始数据集上。
-
构建自相关图,查看平均温度是否可以与自回归模型一起使用。
-
选择一个可接受的滞后和一个不可接受的滞后,并使用这些值构建滞后图。
-
创建一个自回归模型,注意所选的滞后,计算 r2 值,并将自回归模型与原始图形一起绘制。该模型将预测超出可用数据的 1,000 个样本。
-
将模型拟合到数据中。
-
为数据集结束后的 1,000 天创建一组预测。
-
绘制预测值以及原始数据集。
-
通过显示第 100 个到第 200 个样本,增强视图以查找差异。
注意
本活动的解决方案可以在第 344 页找到。
总结
在本章中,我们迈出了构建机器学习模型和使用带标签数据进行预测的第一大步。我们首先分析了多种构建线性模型的方式,从精确的最小二乘法开始,当对少量数据建模时,这种方法非常有效,可以使用可用的计算机内存进行处理。我们通过使用从分类变量中创建的虚拟变量来改进我们的基础线性模型,为模型增加了额外的特征和上下文。接着,我们使用了带抛物线模型的线性回归分析来进一步改进性能,使得模型更贴近数据集的自然曲线。我们还实施了梯度下降算法,虽然我们注意到,对于我们的有限数据集来说,它并不像最小二乘法那样精确,但在系统资源无法处理数据集时表现最为强大。
最后,我们研究了自回归模型的应用,这些模型基于先前数据的经验预测未来值。通过使用自回归模型,我们能够准确地对 1986 年至 2018 年间标准普尔 500 指数的收盘价进行建模。
现在我们已经有了监督回归问题的经验,我们将在下一章节关注分类问题。
第四章:分类
学习目标
到本章结束时,你将能够:
-
实现逻辑回归,并解释如何将其用于将数据分类为特定组或类别
-
使用 K 最近邻聚类算法进行分类
-
使用决策树进行数据分类,包括 ID3 算法
-
描述数据中的熵概念
-
解释决策树(如 ID3)如何旨在减少熵
-
使用决策树进行数据分类
本章介绍了分类问题,线性回归和逻辑回归分类,K 最近邻分类和决策树。
介绍
在上一章中,我们开始了使用回归技术的监督学习之旅,预测给定一组输入数据时的连续变量输出。现在,我们将转向我们之前描述的另一类机器学习问题:分类问题。回想一下,分类任务的目标是根据一组输入数据,预测数据属于指定数量的类别中的哪一类。
在本章中,我们将扩展在第三章《回归分析》中学到的概念,并将其应用于标注有类别而非连续值作为输出的数据集。
将线性回归作为分类器
在上一章中,我们在预测连续变量输出的上下文中讨论了线性回归,但它也可以用于预测一组数据属于哪个类别。线性回归分类器不如我们将在本章中讨论的其他类型的分类器强大,但它们在理解分类过程时特别有用。假设我们有一个虚构的数据集,其中包含两个独立的组,X 和 O,如图 4.1所示。我们可以通过首先使用线性回归拟合一条直线的方程来构建一个线性分类器。对于任何位于直线之上的值,将预测为X类别,而对于位于直线下方的任何值,将预测为O类别。任何可以通过一条直线分隔的数据集被称为线性可分,这构成了机器学习问题中的一个重要数据子集。尽管在基于线性回归的分类器中,这可能并不特别有用,但在其他分类器中,如支持向量机(SVM)、决策树和基于线性神经网络的分类器中,这通常是很有帮助的。

图 4.1:将线性回归作为分类器
练习 36:将线性回归用作分类器
本练习包含了一个使用线性回归作为分类器的构造示例。在本练习中,我们将使用一个完全虚构的数据集,并测试线性回归作为分类器的效果。数据集由手动选择的x和y值组成,这些值大致分为两组。该数据集专门为本练习设计,旨在展示如何将线性回归作为分类器使用,数据集在本书的附带代码文件中以及 GitHub 上的github.com/TrainingByPackt/Supervised-Learning-with-Python可以找到。
-
将
linear_classifier.csv数据集加载到 pandas DataFrame 中:df = pd.read_csv('linear_classifier.csv') df.head()输出将如下所示:
![图 4.2:前五行]()
图 4.2:前五行
浏览数据集,每行包含一组x, y坐标以及对应的标签,指示数据属于哪个类别,可能是叉号(x)或圆圈(o)。
-
绘制数据的散点图,每个点的标记为对应的类别标签:
plt.figure(figsize=(10, 7)) for label, label_class in df.groupby('labels'): plt.scatter(label_class.values[:,0], label_class.values[:,1], label=f'Class {label}', marker=label, c='k') plt.legend() plt.title("Linear Classifier");我们将得到如下的散点图:
![图 4.3 线性分类器的散点图]()
图 4.3 线性分类器的散点图
-
使用上一章中的 scikit-learn
LinearRegressionAPI,拟合线性模型到数据集的x、y坐标,并打印出线性方程:# Fit a linear regression model model = LinearRegression() model.fit(df.x.values.reshape((-1, 1)), df.y.values.reshape((-1, 1))) # Print out the parameters print(f'y = {model.coef_[0][0]}x + {model.intercept_[0]}')输出将是:
![图 4.4:模型拟合的输出]()
图 4.4:模型拟合的输出
-
在数据集上绘制拟合的趋势线:
# Plot the trendline trend = model.predict(np.linspace(0, 10).reshape((-1, 1))) plt.figure(figsize=(10, 7)) for label, label_class in df.groupby('labels'): plt.scatter(label_class.values[:,0], label_class.values[:,1], label=f'Class {label}', marker=label, c='k') plt.plot(np.linspace(0, 10), trend, c='k', label='Trendline') plt.legend() plt.title("Linear Classifier");输出将如下所示:
![图 4.5:带趋势线的散点图]()
图 4.5:带趋势线的散点图
-
通过拟合的趋势线,可以应用分类器。对于数据集中的每一行,判断x, y点是位于线性模型(或趋势线)之上还是之下。如果点位于趋势线之下,则模型预测o类;如果位于线之上,则预测x类。将这些值作为预测标签的一列包含在内:
# Make predictions y_pred = model.predict(df.x.values.reshape((-1, 1))) pred_labels = [] for _y, _y_pred in zip(df.y, y_pred): if _y < _y_pred: pred_labels.append('o') else: pred_labels.append('x') df['Pred Labels'] = pred_labels df.head()输出将如下所示:
![图 4.6:前五行]()
图 4.6:前五行
-
绘制带有相应真实标签的点。对于那些标签被正确预测的点,绘制对应的类别;对于错误预测的点,绘制一个菱形标记:
plt.figure(figsize=(10, 7)) for idx, label_class in df.iterrows(): if label_class.labels != label_class['Pred Labels']: label = 'D' s=70 else: label = label_class.labels s=50 plt.scatter(label_class.values[0], label_class.values[1], label=f'Class {label}', marker=label, c='k', s=s) plt.plot(np.linspace(0, 10), trend, c='k', label='Trendline') plt.title("Linear Classifier"); incorrect_class = mlines.Line2D([], [], color='k', marker='D', markersize=10, label='Incorrect Classification'); plt.legend(handles=[incorrect_class]);输出将如下所示:

图 4.7:显示错误预测的散点图
我们可以看到,在这个图中,线性分类器在这个完全虚构的数据集上做出了两次错误预测,一个是在x = 1时,另一个是在x = 3时。
但如果我们的数据集不是线性可分的,无法使用直线模型对数据进行分类,那该怎么办呢?这种情况非常常见。在这种情况下,我们会转向其他分类方法,其中许多方法使用不同的模型,但这一过程逻辑上是从我们简化的线性分类模型延伸出来的。
逻辑回归
逻辑或对数几率模型就是一种非线性模型,已经在许多不同领域的分类任务中得到了有效应用。在本节中,我们将用它来分类手写数字的图像。在理解逻辑模型的过程中,我们也迈出了理解一种特别强大的机器学习模型——人工神经网络的关键一步。那么,逻辑模型到底是什么呢?像线性模型一样,线性模型由一个线性或直线函数组成,而逻辑模型则由标准的逻辑函数组成,数学上看起来大致是这样的:

图 4.8:逻辑函数
从实际角度来看,当经过训练后,这个函数返回输入信息属于某一特定类别或组的概率。
假设我们想预测某一数据项是否属于两个组中的一个。就像之前的例子中,在线性回归中,这等价于 y 要么为零,要么为一,而 x 可以取值范围在
和
之间:

图 4.9:y 的方程
从零到一的范围与
到
的差异非常大;为了改进这一点,我们将计算赔率比,这样它就会从大于零的数值变化到小于
的数值,这就是朝着正确方向迈出的一步:

图 4.10:赔率比
我们可以利用自然对数的数学关系进一步简化这一过程。当赔率比接近零时,
ss 接近
;同样,当赔率比接近一时,
接近
。这正是我们想要的;也就是说,两个分类选项尽可能远离。

图 4.11:分类点的自然对数
通过稍微调整方程,我们得到了逻辑函数:

图 4.12:逻辑函数
注意 e 的指数,即
,并且这个关系是一个线性函数,具有两个训练参数或 权重,
和
,以及输入特征 x。如果我们将逻辑函数绘制在 (-6, 6) 范围内,我们会得到以下结果:

图 4.13:逻辑函数曲线
通过检查 图 4.13,我们可以看到一些对分类任务很重要的特征。首先需要注意的是,如果我们查看函数两端的 y 轴上的概率值,在 x = -6 时,概率值几乎为零,而在 x = 6 时,概率值接近 1。虽然看起来这些值实际上是零和一,但实际情况并非如此。逻辑函数在这些极值处接近零和一,只有当 x 达到正无穷或负无穷时,它才会等于零或一。从实际角度来看,这意味着逻辑函数永远不会返回大于一的概率或小于等于零的概率,这对于分类任务来说是完美的。我们永远不能有大于一的概率,因为根据定义,概率为一意味着事件发生是确定的。同样,我们不能有小于零的概率,因为根据定义,概率为零意味着事件不发生是确定的。逻辑函数接近但永远不等于一或零,意味着结果或分类总是存在某种不确定性。
逻辑函数的最后一个特点是,在 x = 0 时,概率为 0.5,如果我们得到这个结果,这意味着模型对相应类别的结果具有相等的不确定性;也就是说,它完全没有把握。通常,这是训练开始时的默认状态,随着模型接触到训练数据,它对决策的信心逐渐增加。
注意
正确理解和解释分类模型(如线性回归)提供的概率信息非常重要。可以将这个概率得分视为在给定训练数据所提供的信息的变动性下,输入信息属于某一特定类别的可能性。一个常见的错误是将这个概率得分作为衡量模型对预测是否可靠的客观标准;不幸的是,这并不总是准确的。一个模型可以提供 99.99% 的概率,认为某些数据属于某个特定类别,但它仍然可能有 99.99% 的错误。
我们使用概率值的目的是选择分类器预测的类别。假设我们有一个模型用于预测某些数据集是否属于类 A 或类 B。如果逻辑回归模型为类 A 返回的概率为 0.7,那么我们将返回类 A 作为模型的预测类别。如果概率仅为 0.2,则模型的预测类别为类 B。
练习 37:逻辑回归作为分类器 – 二类分类器
在本练习中,我们将使用著名的 MNIST 数据集的一个样本(可以在 yann.lecun.com/exdb/mnist/ 或 GitHub 上的 github.com/TrainingByPackt/Supervised-Learning-with-Python 找到),它是一个包含手写邮政编码数字(从零到九)及相应标签的图像序列。MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,每个样本都是大小为 28 x 28 像素的灰度图像。在本练习中,我们将使用逻辑回归来构建一个分类器。我们将构建的第一个分类器是一个二类分类器,用来判断图像是手写的零还是一:
-
在这个练习中,我们需要导入一些依赖项。执行以下导入语句:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.linear_model import LogisticRegression -
我们还需要下载 MNIST 数据集。你只需要执行此操作一次,因此在此步骤之后,可以随意注释或删除这些代码单元。下载图像数据,具体如下:
request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz') with open('train-images-idx3-ubyte.gz', 'wb') as f: f.write(request.read()) request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz') with open('t10k-images-idx3-ubyte.gz', 'wb') as f: f.write(request.read()) -
下载数据的相应标签:
request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz') with open('train-labels-idx1-ubyte.gz', 'wb') as f: f.write(request.read()) request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz') with open('t10k-labels-idx1-ubyte.gz', 'wb') as f: f.write(request.read()) -
一旦所有文件成功下载,使用以下命令检查本地目录中的文件(适用于 Windows):
!dir *.gz输出结果如下:
![图 4.14:目录中的文件]()
图 4.14:目录中的文件
注意
对于 Linux 和 macOS,使用
!ls *.gz命令查看本地目录中的文件。 -
加载下载的数据。无需过于担心读取数据的具体细节,因为这些是 MNIST 数据集特有的:
with gzip.open('train-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('train-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels = np.array(array("B", f.read())) with gzip.open('t10k-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img_test = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('t10k-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels_test = np.array(array("B", f.read())) -
一如既往,彻底理解数据至关重要,因此,创建训练样本中前 10 张图像的图像图。注意灰度图像以及相应的标签,它们是数字零到九:
for i in range(10): plt.subplot(2, 5, i + 1) plt.imshow(img[i], cmap='gray'); plt.title(f'{labels[i]}'); plt.axis('off')输出结果如下:
![图 4.15:训练图像]()
图 4.15:训练图像
-
由于初始分类器旨在分类零图像或一图像,我们必须首先从数据集中选择这些样本:
samples_0_1 = np.where((labels == 0) | (labels == 1))[0] images_0_1 = img[samples_0_1] labels_0_1 = labels[samples_0_1] samples_0_1_test = np.where((labels_test == 0) | (labels_test == 1)) images_0_1_test = img_test[samples_0_1_test].reshape((-1, rows * cols)) labels_0_1_test = labels_test[samples_0_1_test] -
可视化一个来自零选择的样本和另一个来自手写数字一的样本,以确保我们已正确分配数据。
下面是数字零的代码:
sample_0 = np.where((labels == 0))[0][0] plt.imshow(img[sample_0], cmap='gray');输出结果如下:
![图 4.16:第一张手写图像]()
图 4.16:第一张手写图像
下面是一个的代码:
sample_1 = np.where((labels == 1))[0][0] plt.imshow(img[sample_1], cmap='gray');输出结果如下:
![图 4.17:第二张手写图像]()
图 4.17:第二张手写图像
-
我们几乎可以开始构建模型了,然而,由于每个样本都是图像并且数据是矩阵格式,我们必须首先重新排列每张图像。模型需要图像以向量形式提供,即每张图像的所有信息存储在一行中。按以下步骤操作:
images_0_1 = images_0_1.reshape((-1, rows * cols)) images_0_1.shape -
现在我们可以使用选择的图像和标签构建并拟合逻辑回归模型:
model = LogisticRegression(solver='liblinear') model.fit(X=images_0_1, y=labels_0_1)输出将是:
![图 4.18:逻辑回归模型]()
图 4.18:逻辑回归模型
请注意,scikit-learn 的逻辑回归 API 调用与线性回归的一致性。这里有一个额外的参数
solver,它指定要使用的优化过程类型。我们在这里提供了该参数的默认值,以抑制在当前版本的 scikit-learn 中要求指定solver参数的未来警告。solver参数的具体细节超出了本章的讨论范围,仅为抑制警告信息而包含。 -
检查此模型在相应训练数据上的表现:
model.score(X=images_0_1, y=labels_0_1)我们将得到如下输出:
![图 4.19:模型得分]()
图 4.19:模型得分
在这个例子中,模型能够以 100% 的准确率预测训练标签。
-
使用模型显示训练数据的前两个预测标签:
model.predict(images_0_1) [:2]输出将是:
![图 4.20:模型预测的前两个标签]()
图 4.20:模型预测的前两个标签
-
逻辑回归模型是如何做出分类决策的?观察模型为训练集产生的一些概率值:
model.predict_proba(images_0_1)[:2]输出将如下所示:
![图 4.21:概率数组]()
图 4.21:概率数组
我们可以看到,对于每个预测,都会有两个概率值。第一个是类为零的概率,第二个是类为一的概率,二者加起来为一。我们可以看到,在第一个例子中,预测概率为 0.9999999(类零),因此预测为类零。同样地,第二个例子则相反。
-
计算模型在测试集上的表现,以检查其在未见过的数据上的性能:
model.score(X=images_0_1_test, y=labels_0_1_test)输出将是:

图 4.22:模型得分
注意
请参考第六章,模型评估,了解更好的客观衡量模型性能的方法。
我们可以看到,逻辑回归是一个强大的分类器,能够区分手写的零和一。
练习 38:逻辑回归——多类分类器
在之前的练习中,我们使用逻辑回归对两类进行分类。然而,逻辑回归也可以用于将一组输入信息分类到 k 个不同的组,这就是我们在本练习中要研究的多类分类器。加载 MNIST 训练和测试数据的过程与之前的练习相同:
-
加载训练/测试图像及其对应的标签:
with gzip.open('train-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('train-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels = np.array(array("B", f.read())) with gzip.open('t10k-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img_test = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('t10k-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels_test = np.array(array("B", f.read())) -
由于训练数据量较大,我们将选择整体数据的一个子集,以减少训练时间及训练过程中所需的系统资源:
np.random.seed(0) # Give consistent random numbers selection = np.random.choice(len(img), 5000) selected_images = img[selection] selected_labels = labels[selection]请注意,在此示例中,我们使用的是所有 10 个类别的数据,而不仅仅是零类和一类,因此我们将此示例设置为多类分类问题。
-
再次将输入数据重塑为向量形式,以便后续使用:
selected_images = selected_images.reshape((-1, rows * cols)) selected_images.shape输出结果如下:
![图 4.23:数据重塑]()
图 4.23:数据重塑
-
下一单元格故意被注释掉。暂时保持此代码为注释:
# selected_images = selected_images / 255.0 # img_test = img_test / 255.0 -
构建逻辑回归模型。这里有一些额外的参数,如下所示:
solver的lbfgs值适用于多类问题,需要额外的max_iter迭代次数来收敛到解。multi_class参数设置为multinomial,以计算整个概率分布的损失:model = LogisticRegression(solver='lbfgs', multi_class='multinomial', max_iter=500, tol=0.1) model.fit(X=selected_images, y=selected_labels)输出结果如下:
![图 4.24:逻辑回归模型]()
图 4.24:逻辑回归模型
注意
有关参数的更多信息,请参阅文档:
scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html。 -
确定训练集的准确性评分:
model.score(X=selected_images, y=selected_labels)输出结果将是:
![图 4.25:模型评分]()
图 4.25:模型评分
-
确定训练集的前两个预测值,并绘制相应预测的图像:
model.predict(selected_images)[:2]![图 4.26:模型评分预测值]()
图 4.26:模型评分预测值
-
显示训练集前两个样本的图像,查看我们的判断是否正确:
plt.subplot(1, 2, 1) plt.imshow(selected_images[0].reshape((28, 28)), cmap='gray'); plt.axis('off'); plt.subplot(1, 2, 2) plt.imshow(selected_images[1].reshape((28, 28)), cmap='gray'); plt.axis('off');输出结果如下:
![图 4.27:使用预测绘制的图像]()
图 4.27:使用预测绘制的图像
-
再次打印出模型为训练集第一个样本提供的概率分数。确认每个类别有 10 个不同的值:
model.predict_proba(selected_images)[0]输出结果如下:
![图 4.28:预测值数组]()
图 4.28:预测值数组
请注意,在第一个样本的概率数组中,第五个(索引为四)样本的概率最高,因此表示预测为四。
-
计算模型在测试集上的准确性。这将提供一个合理的估计值,表示模型在实际环境中的表现,因为它从未见过测试集中的数据。考虑到模型没有接触过这些数据,测试集的准确率预计会稍微低于训练集:
model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)输出结果如下:
![图 4.29:模型得分]()
图 4.29:模型得分
在测试集上检查时,模型的准确率为 87.8%。应用测试集时,性能下降是可以预期的,因为这是模型第一次接触这些样本;而在训练过程中,训练集已经多次呈现给模型。
-
找到包含注释掉的代码的单元格,如步骤四所示。取消注释该单元格中的代码:
selected_images = selected_images / 255.0 img_test = img_test / 255.0这个单元格只是将所有图像值缩放到零和一之间。灰度图像由像素组成,这些像素的值在 0 到 255 之间,包括 0(黑色)和 255(白色)。
-
点击重新启动并运行全部以重新运行整个笔记本。
-
找到训练集误差:
model.score(X=selected_images, y=selected_labels)我们将得到以下得分:
![图 4.30:训练集模型得分]()
图 4.30:训练集模型得分
-
找到测试集误差:
model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)我们将得到以下得分:

图 4.31:测试集模型得分
归一化图像对系统整体性能有什么影响?训练误差变得更糟!我们从训练集的 100%准确率下降到了 98.6%。是的,训练集的性能有所下降,但测试集的准确率却从 87.8%提高到了 90.02%。测试集的性能更为重要,因为模型之前没有见过这些数据,因此它能更好地代表模型在实际应用中的表现。那么,为什么我们会得到更好的结果呢?再次查看图 4.13,注意曲线接近零和接近一时的形状。曲线在接近零和接近一时趋于饱和或平坦。因此,如果我们使用 0 到 255 之间的图像(或x值),由逻辑函数定义的类别概率将位于曲线的平坦区域内。位于该区域内的预测变化很小,因为要产生任何有意义的变化,需要x值发生非常大的变化。将图像缩放到零和一之间,最初会将预测值拉近p(x) = 0.5,因此,x的变化会对y值产生更大的影响。这允许更敏感的预测,并导致训练集中的一些预测错误,但测试集中的更多预测是正确的。建议在训练和测试之前,将输入值缩放到零和一之间,或者负一和一之间,用于你的逻辑回归模型。
以下函数将把 NumPy 数组中的值缩放到-1 到 1 之间,均值大约为零:
def scale_input(x):
mean_x = x.mean()
x = x – mean_x
max_x = x / no.max(abs(x))
return x
活动 11:线性回归分类器 – 二分类器
在这个活动中,我们将使用 MNIST 数据集构建一个基于线性回归的二分类器,用于区分数字零和数字一。
执行的步骤如下:
-
导入所需的依赖项:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.linear_model import LinearRegression -
将 MNIST 数据加载到内存中。
-
可视化一组数据样本。
-
构建一个线性分类器模型来分类数字零和数字一。我们将创建的模型是用来判断样本是数字零还是数字一。为此,我们首先需要仅选择这些样本。
-
使用零样本和一样本的图像可视化选择的信息。
-
为了向模型提供图像信息,我们必须先将数据展平,使每个图像的形状为 1 x 784 像素。
-
让我们构建模型;使用
LinearRegressionAPI 并调用fit函数。 -
计算训练集的 R2 分数。
-
使用阈值 0.5 来确定每个训练样本的标签预测。大于 0.5 的值分类为一;小于或等于 0.5 的值分类为零。
-
计算预测训练值与实际值之间的分类准确率。
-
与测试集的性能进行比较。
注意
该活动的解决方案可以在第 352 页找到。
活动 12:使用逻辑回归进行虹膜分类
在这个活动中,我们将使用著名的虹膜物种数据集(可在 en.wikipedia.org/wiki/Iris_flower_data_set 或 GitHub 上的 github.com/TrainingByPackt/Supervised-Learning-with-Python 获取),该数据集由植物学家罗纳德·费舍尔于 1936 年创建。数据集包含三种不同虹膜花卉物种的萼片和花瓣的长度和宽度测量值:虹膜变色花、虹膜佛罗伦萨和虹膜维尔吉尼卡。在这个活动中,我们将使用数据集中提供的测量值来分类不同的花卉物种。
执行的步骤如下:
-
导入所需的软件包。在本次活动中,我们将需要 pandas 包来加载数据,Matplotlib 包来绘图,以及 scikit-learn 包来创建逻辑回归模型。导入所有必需的软件包和相关模块以完成这些任务:
import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression -
使用 pandas 加载虹膜数据集并检查前五行。
-
下一步是特征工程。我们需要选择最合适的特征,以提供最强大的分类模型。绘制多个不同的特征与分配的物种分类之间的关系图,例如,萼片长度与花瓣长度及物种。通过可视化检查这些图形,查找可能表明不同物种之间分离的模式。
-
通过在以下列表中编写列名来选择特征:
selected_features = [ '', # List features here ] -
在构建模型之前,我们必须先将
species值转换为可以在模型中使用的标签。将Iris-setosa物种字符串替换为值0,将Iris-versicolor物种字符串替换为值1,将Iris-virginica物种字符串替换为值2。 -
使用
selected_features和分配的species标签创建模型。 -
计算模型在训练集上的准确性。
-
使用第二选择的
selected_features构建另一个模型,并比较其性能。 -
使用所有可用信息构建另一个模型,并比较其性能。
注意
本活动的解决方案可以在第 357 页找到。
使用 K 近邻分类
现在我们已经熟悉了使用逻辑回归创建多类分类器,并且在这些模型中取得了合理的性能,接下来我们将注意力转向另一种分类器:K-最近邻(K-NN)聚类方法。这是一种非常实用的方法,因为它不仅可以应用于监督分类问题,还可以应用于无监督问题。

图 4.32:K-NN 的可视化表示
大约位于中心的实心圆是需要分类的测试点,而内圆表示分类过程,其中K=3,外圆表示K=5。
K-NN 是数据分类中最简单的“学习”算法之一。这里使用“学习”一词是明确的,因为 K-NN 并不像其他方法(例如逻辑回归)那样从数据中学习并将这些学习结果编码为参数或权重。K-NN 使用基于实例或懒惰学习,它只是存储或记住所有训练样本及其对应的类别。当一个测试样本提供给算法进行分类时,它通过对K个最近点的多数投票来确定对应的类别,从而得名 K-最近邻。如果我们查看图 4.35,实心圆表示需要分类的测试点。如果我们使用K=3,最近的三个点位于内虚线圆内,在这种情况下,分类结果将是空心圆。然而,如果我们使用K=5,最近的五个点位于外虚线圆内,分类结果将是交叉标记(三个交叉标记对两个空心圆)。
该图突出显示了 K-NN 分类的一些特征,这些特征应当加以考虑:
-
K 的选择非常重要。在这个简单的例子中,将 K 从三改为五,由于两个类别的接近性,导致了分类预测的翻转。由于最终的分类是通过多数投票决定的,因此通常使用奇数的 K 值以确保投票中有一个获胜者。如果选择了偶数的 K 值并且投票发生了平局,那么有多种方法可以用来打破平局,包括:
将 K 减少一个直到平局被打破
基于最近点的最小欧几里得距离选择类别
应用加权函数以偏向距离较近的邻居
-
K-NN 模型具有形成极其复杂的非线性边界的能力,这在对图像或具有有趣边界的数据集进行分类时可能具有优势。考虑到在 图 4.35 中,随着 K 的增加,测试点的分类从空心圆变为十字,我们可以看到此处可能形成复杂的边界。
-
K-NN 模型对数据中的局部特征非常敏感,因为分类过程实际上仅依赖于邻近的点。
-
由于 K-NN 模型将所有训练信息都记住以进行预测,因此它们在对新的、未见过的数据进行泛化时可能会遇到困难。
K-NN 还有一种变体,它不是指定最近邻的数量,而是指定测试点周围的半径大小以进行查找。这种方法称为 半径邻居分类,本章将不考虑这种方法,但在理解 K-NN 的过程中,你也会对半径邻居分类有一定了解,并学习如何通过 scikit-learn 使用该模型。
注意
我们对 K-NN 分类的解释以及接下来的练习将重点研究具有两个特征或两个维度的数据建模,因为这样可以简化可视化并更好地理解 K-NN 建模过程。像线性回归和逻辑回归一样,K-NN 分类并不限于仅用于二维数据集;它也可以应用于 N 维数据集。我们将在 活动 13:K-NN 多分类分类器 中进一步详细探讨这一点,在该活动中,我们将使用 K-NN 对 MNIST 数据集进行分类。请记住,仅仅因为无法绘制过多维度的图形,并不意味着它不能在 N 维数据集中进行分类。
练习 39:K-NN 分类
为了允许可视化 K-NN 过程,本练习将把重点转移到另一个数据集——著名的 Iris 数据集。该数据集作为本书附带代码文件的一部分提供:
-
对于本练习,我们需要导入 pandas、Matplotlib 以及 scikit-learn 的
KNeighborsClassifier。我们将使用简写符号KNN以便快速访问:import pandas as pd import matplotlib.pyplot as plt from sklearn.neighbors import KNeighborsClassifier as KNN -
加载 Iris 数据集并查看前五行:
df = pd.read_csv('iris-data.csv') df.head()输出将是:
![图 4.33:前五行]()
图 4.33:前五行
-
在这个阶段,我们需要从数据集中选择最合适的特征用于分类器。我们可以简单地选择所有四个(萼片和花瓣的长度和宽度),但由于这个练习旨在允许可视化 K-NN 过程,我们只选择萼片长度和花瓣宽度。为数据集中的每个类构建萼片长度与花瓣宽度的散点图,并标注相应的物种:
markers = { 'Iris-setosa': {'marker': 'x', 'facecolor': 'k', 'edgecolor': 'k'}, 'Iris-versicolor': {'marker': '*', 'facecolor': 'none', 'edgecolor': 'k'}, 'Iris-virginica': {'marker': 'o', 'facecolor': 'none', 'edgecolor': 'k'}, } plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): plt.scatter(group['Sepal Length'], group['Petal Width'], label=name, marker=markers[name]['marker'], facecolors=markers[name]['facecolor'], edgecolor=markers[name]['edgecolor']) plt.title('Species Classification Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)'); plt.legend();输出如下:
![图 4.34: 鸢尾花数据的散点图]()
图 4.34: 鸢尾花数据的散点图
-
从这张图中可以看出,花瓣宽度在某种程度上合理地分开了这些物种,特别是鸢尾花杂色和鸢尾花维吉尼亚物种之间具有最大的相似性。在鸢尾花维吉尼亚物种的群集中,有几个点位于鸢尾花杂色群集内。作为稍后使用的测试点,选择边界样本 134:
df_test = df.iloc[134] df = df.drop([134]) # Remove the sample df_test输出将如下所示:
![图 4.35: 边界样本]()
图 4.35: 边界样本
-
再次绘制数据,突出显示测试样本/点的位置:
plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): plt.scatter(group['Sepal Length'], group['Petal Width'], label=name, marker=markers[name]['marker'], facecolors=markers[name]['facecolor'], edgecolor=markers[name]['edgecolor']) plt.scatter(df_test['Sepal Length'], df_test['Petal Width'], label='Test Sample', c='k', marker='D') plt.title('Species Classification Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)'); plt.legend();输出将如下:
![图 4.36: 带有测试样本的散点图]()
图 4.36: 带有测试样本的散点图
-
使用 K = 3 构建 K-NN 分类器模型,并将其拟合到训练数据中:
model = KNN(n_neighbors=3) model.fit(X=df[['Petal Width', 'Sepal Length']], y=df.Species)输出将如下:
![图 4.37: K 近邻分类器]()
图 4.37: K 近邻分类器
-
检查模型在训练集上的性能:
model.score(X=df[['Petal Width', 'Sepal Length']], y=df.Species)输出将显示性能评分:
![]()
图 4.38: 模型得分
由于测试集的准确率超过了 97%,接下来的步骤将是检查测试样本。
-
预测测试样本的物种:
model.predict(df_test[['Petal Width', 'Sepal Length']].values.reshape((-1, 2)))[0]输出将如下:
![图 4.39: 预测的测试样本]()
图 4.39: 预测的测试样本
-
用实际样本的物种来验证它:
df.iloc[134].Species输出将如下:

图 4.40: 样本的物种
这个预测显然是错误的,但考虑到测试点位于边界上,这并不奇怪。有用的是知道模型的边界在哪里。我们将在下一个练习中绘制这个边界。
练习 40: 可视化 K-NN 边界
要可视化 K-NN 分类器生成的决策边界,我们需要在预测空间上进行扫描,即萼片宽度和长度的最小和最大值,并确定模型在这些点上的分类。一旦完成扫描,我们可以绘制模型所做的分类决策。
-
导入所有相关的包。对于这个练习,我们还需要使用 NumPy:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap from sklearn.neighbors import KNeighborsClassifier as KNN -
将鸢尾花数据集加载到 pandas 的 DataFrame 中:
df = pd.read_csv('iris-data.csv') df.head()输出将如下:
![图 4.41: 前五行]()
图 4.41: 前五行
-
虽然我们可以在之前的练习中使用物种字符串来创建模型,但在绘制决策边界时,将物种映射到单独的整数值会更有用。为此,创建一个标签列表以供后续参考,并遍历该列表,用对应的索引替换现有的标签:
labelled_species = [ 'Iris-setosa', 'Iris-versicolor', 'Iris-virginica', ] for idx, label in enumerate(labelled_species): df.Species = df.Species.replace(label, idx) df.head()输出将如下所示:
![图 4.42:前五行]()
图 4.42:前五行
注意在
for循环定义中使用了enumerate函数。在迭代for循环时,enumerate函数在每次迭代中提供列表中值的索引和该值本身。我们将值的索引赋给idx变量,将值赋给label。以这种方式使用enumerate提供了一种简便的方法来用唯一的整数标签替换物种字符串。 -
构建一个 K-NN 分类模型,仍然使用三个最近邻,并将其拟合到新的物种标签数据的萼片长度和花瓣宽度上:
model = KNN(n_neighbors=3) model.fit(X=df[['Sepal Length', 'Petal Width']], y=df.Species)输出将如下所示:
![图 4.43:K 邻近分类器]()
图 4.43:K 邻近分类器
-
为了可视化我们的决策边界,我们需要在信息空间内创建一个网格或预测范围,即所有萼片长度和花瓣宽度的值。从比花瓣宽度和萼片长度的最小值低 1 毫米开始,直到比它们的最大值高 1 毫米,使用 NumPy 的
arange函数以 0.1(间距)为增量创建这些限制之间的值范围:spacing = 0.1 # 0.1mm petal_range = np.arange(df['Petal Width'].min() - 1, df['Petal Width'].max() + 1, spacing) sepal_range = np.arange(df['Sepal Length'].min() - 1, df['Sepal Length'].max() + 1, spacing) -
使用 NumPy 的
meshgrid函数将两个范围合并为一个网格:xx, yy = np.meshgrid(sepal_range, petal_range) # Create the mesh查看
xx:xx输出将是:
![图 4.44:meshgrid xx 值数组]()
图 4.44:meshgrid xx 值数组
查看
yy:yy输出将是:
![图 4.45:meshgrid yy 值数组]()
图 4.45:meshgrid yy 值数组
-
使用
np.c_将 mesh 拼接成一个单独的 NumPy 数组:pred_x = np.c_[xx.ravel(), yy.ravel()] # Concatenate the results pred_x我们将得到以下输出:
![图 4.46:预测值数组]()
图 4.46:预测值数组
虽然这个函数调用看起来有些神秘,但它仅仅是将两个独立的数组连接在一起(参见
docs.scipy.org/doc/numpy/reference/generated/numpy.c_.html),并且是concatenate的简写形式。 -
为网格生成类别预测:
pred_y = model.predict(pred_x).reshape(xx.shape) pred_y我们将得到以下输出:
![图 4.47:预测 y 值数组]()
图 4.47:预测 y 值数组
-
为了持续可视化边界,我们需要两组一致的颜色:一组较浅的颜色用于决策边界,一组较深的颜色用于训练集点本身。创建两个
ListedColormaps:# Create color maps cmap_light = ListedColormap(['#F6A56F', '#6FF6A5', '#A56FF6']) cmap_bold = ListedColormap(['#E6640E', '#0EE664', '#640EE6']) -
为了突出显示决策边界,首先根据鸢尾花物种绘制训练数据,使用
cmap_bold颜色方案,并为每个不同物种使用不同的标记:markers = { 'Iris-setosa': {'marker': 'x', 'facecolor': 'k', 'edgecolor': 'k'}, 'Iris-versicolor': {'marker': '*', 'facecolor': 'none', 'edgecolor': 'k'}, 'Iris-virginica': {'marker': 'o', 'facecolor': 'none', 'edgecolor': 'k'}, } plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): species = labelled_species[name] plt.scatter(group['Sepal Length'], group['Petal Width'], c=cmap_bold.colors[name], label=labelled_species[name], marker=markers[species]['marker'] ) plt.title('Species Classification Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)'); plt.legend();输出结果如下:
![]()
图 4.48:散点图与突出显示的决策边界
-
使用之前创建的预测网格,绘制决策边界并与训练数据一起展示:
plt.figure(figsize=(10, 7)) plt.pcolormesh(xx, yy, pred_y, cmap=cmap_light); plt.scatter(df['Sepal Length'], df['Petal Width'], c=df.Species, cmap=cmap_bold, edgecolor='k', s=20); plt.title('Species Decision Boundaries Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)');输出结果如下:
![图 4.49:决策边界]()
图 4.49:决策边界
注意
图 4.49已修改为灰度打印,并增加了额外的标签,指示类别的预测边界。
活动 13:K-NN 多类别分类器
在此活动中,我们将使用 K-NN 模型将 MNIST 数据集分类为 10 个不同的数字类别。
要执行的步骤如下:
-
导入以下包:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.neighbors import KNeighborsClassifier as KNN -
将 MNIST 数据加载到内存中。首先是训练图像,然后是训练标签,再然后是测试图像,最后是测试标签。
-
可视化数据样本。
-
构建一个 K-NN 分类器,使用三个最近邻来分类 MNIST 数据集。同样,为了节省处理能力,随机抽取 5,000 张图像用于训练。
-
为了将图像信息提供给模型,我们必须先将数据展平,使每个图像的形状为 1 x 784 像素。
-
构建三邻居 KNN 模型并将数据拟合到模型中。请注意,在本活动中,我们为模型提供了 784 个特征或维度,而不仅仅是 2 个。
-
确定与训练集的评分。
-
显示模型在训练数据上的前两个预测结果。
-
比较与测试集的性能。
注意
该活动的解答可以在第 360 页找到。
使用决策树进行分类
本章将要研究的最终分类方法是决策树,它在自然语言处理等应用中得到了广泛应用。决策树下有多种不同的机器学习算法,例如 ID3、CART 以及强大的随机森林分类器(在第五章,集成建模中介绍)。在本章中,我们将研究 ID3 方法在分类类别数据中的应用,并使用 scikit-learn 中的 CART 实现作为另一种分类鸢尾花数据集的方式。那么,究竟什么是决策树呢?
如名称所示,决策树是一种学习算法,它通过一系列基于输入信息的决策来进行最终分类。回想一下你小时候的生物学课,你可能使用过类似决策树的过程,通过二分法键来分类不同类型的动物。就像图 4.50所示的二分法键,决策树旨在根据一系列决策或提问步骤的结果来分类信息:

图 4.50:使用二分法键进行动物分类
根据所使用的决策树算法,决策步骤的实现可能会略有不同,但我们将特别考虑 ID3 算法的实现。迭代二分法 3(ID3)算法旨在根据每个决策提供的最大信息增益来对数据进行分类。为了进一步理解这一设计,我们还需要理解两个额外的概念:熵和信息增益。
注意
ID3 算法最早由澳大利亚研究员 Ross Quinlan 于 1985 年提出(doi.org/10.1007/BF00116251)。
- 熵:在信息论的背景下,熵是随机数据源提供信息的平均速率。从数学角度来看,这个熵定义为:

图 4.51:熵方程
在这种情况下,当随机数据源产生低概率值时,事件携带的信息更多,因为与发生高概率事件时相比,它是不可预见的。
- 信息增益:与熵相关的概念是通过观察另一个随机变量获得关于某个随机变量的信息量。给定一个数据集 S 和一个观察的属性 a,信息增益在数学上定义为:

图 4.52:信息增益方程
数据集 S 对于属性 a 的信息增益等于 S 的熵减去条件于属性 a 的 S 的熵,或者说,数据集 S 的熵减去 t 中元素的比例与 S 中元素的比例,再乘以 t 的熵,其中 t 是属性 a 中的某个类别。
如果你一开始觉得这里的数学有些令人畏惧,别担心,它比看起来简单得多。为了澄清 ID3 过程,我们将使用 Quinlan 在原始论文中提供的相同数据集来演示这个过程。
练习 41:ID3 分类
在原始论文中,Quinlan 提供了一个包含 10 个天气观察样本的小数据集,每个样本被标记为 P,表示天气适合进行某种活动,比如说星期六早上的板球比赛,或者北美朋友们喜欢的棒球比赛。如果天气不适合比赛,则标记为 N。论文中描述的示例数据集将在练习中创建。
-
在 Jupyter notebook 中,创建以下训练集的 pandas DataFrame:
df = pd.DataFrame() df['Outlook'] = [ 'sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain' ] df['Temperature'] = [ 'hot', 'hot', 'hot', 'mild', 'cool', 'cool', 'cool', 'mild', 'cool', 'mild', 'mild', 'mild', 'hot', 'mild', ] df['Humidity'] = [ 'high', 'high', 'high', 'high', 'normal', 'normal', 'normal', 'high', 'normal', 'normal', 'normal', 'high', 'normal', 'high' ] df['Windy'] = [ 'Weak', 'Strong', 'Weak', 'Weak', 'Weak', 'Strong', 'Strong', 'Weak', 'Weak', 'Weak', 'Strong', 'Strong', 'Weak', 'Strong' ] df['Decision'] = [ 'N', 'N', 'P', 'P', 'P', 'N', 'P', 'N', 'P', 'P', 'P', 'P', 'P', 'N' ] df输出将如下所示:
![图 4.53:Pandas DataFrame]()
图 4.53:Pandas DataFrame
-
在原始论文中,ID3 算法首先随机选择一个小的训练集样本,并将树拟合到这个窗口。对于大数据集,这可能是一个有用的方法,但考虑到我们的数据集相对较小,我们将直接从整个训练集开始。第一步是计算
P和N的熵:# Probability of P p_p = len(df.loc[df.Decision == 'P']) / len(df) # Probability of N p_n = len(df.loc[df.Decision == 'N']) / len(df) entropy_decision = -p_n * np.log2(p_n) - p_p * np.log2(p_p) print(f'H(S) = {entropy_decision:0.4f}')我们将得到以下输出:
![图 4.54:熵决策]()
图 4.54:熵决策
-
我们需要重复此计算,因此将其封装成一个函数:
def f_entropy_decision(data): p_p = len(data.loc[data.Decision == 'P']) / len(data) p_n = len(data.loc[data.Decision == 'N']) / len(data) return -p_n * np.log2(p_n) - p_p * np.log2(p_p) -
下一步是计算哪一个属性通过
groupby方法提供了最高的信息增益:IG_decision_Outlook = entropy_decision # H(S) # Create a string to print out the overall equation overall_eqn = 'Gain(Decision, Outlook) = Entropy(Decision)' # Iterate through the values for outlook and compute the probabilities # and entropy values for name, Outlook in df.groupby('Outlook'): num_p = len(Outlook.loc[Outlook.Decision == 'P']) num_n = len(Outlook.loc[Outlook.Decision != 'P']) num_Outlook = len(Outlook) print(f'p(Decision=P|Outlook={name}) = {num_p}/{num_Outlook}') print(f'p(Decision=N|Outlook={name}) = {num_n}/{num_Outlook}') print(f'p(Decision|Outlook={name}) = {num_Outlook}/{len(df)}') print(f'Entropy(Decision|Outlook={name}) = '\ f'-{num_p}/{num_Outlook}.log2({num_p}/{num_Outlook}) - '\ f'{num_n}/{num_Outlook}.log2({num_n}/{num_Outlook})') entropy_decision_outlook = 0 # Cannot compute log of 0 so add checks if num_p != 0: entropy_decision_outlook -= (num_p / num_Outlook) \ * np.log2(num_p / num_Outlook) # Cannot compute log of 0 so add checks if num_n != 0: entropy_decision_outlook -= (num_n / num_Outlook) \ * np.log2(num_n / num_Outlook) IG_decision_Outlook -= (num_Outlook / len(df)) * entropy_decision_outlook print() overall_eqn += f' - p(Decision|Outlook={name}).' overall_eqn += f'Entropy(Decision|Outlook={name})' print(overall_eqn) print(f'Gain(Decision, Outlook) = {IG_decision_Outlook:0.4f}')输出将如下所示:
![图 4.56:概率熵和增益]()
图 4.56:概率熵和增益
前景的最终增益方程可以重新写为:
![图 4.57:信息增益的方程]()
图 4.57:信息增益的方程
-
我们需要重复这个过程好几次,因此将其封装成一个函数,以便后续使用:
def IG(data, column, ent_decision=entropy_decision): IG_decision = ent_decision for name, temp in data.groupby(column): p_p = len(temp.loc[temp.Decision == 'P']) / len(temp) p_n = len(temp.loc[temp.Decision != 'P']) / len(temp) entropy_decision = 0 if p_p != 0: entropy_decision -= (p_p) * np.log2(p_p) if p_n != 0: entropy_decision -= (p_n) * np.log2(p_n) IG_decision -= (len(temp) / len(df)) * entropy_decision return IG_decision -
对其他每一列重复此过程,以计算相应的信息增益:
for col in df.columns[:-1]: print(f'Gain(Decision, {col}) = {IG(df, col):0.4f}')我们将得到以下输出:
![图 4.58:增益]()
图 4.58:增益
-
这些信息为树提供了第一个决策。我们希望根据最大的信息增益进行拆分,因此我们在前景上进行拆分。查看基于前景的数据拆分:
for name, temp in df.groupby('Outlook'): print('-' * 15) print(name) print('-' * 15) print(temp) print('-' * 15)输出将如下所示:
![图 4.59:信息增益]()
图 4.59:信息增益
注意到所有阴天记录的决策都是P。这为我们的决策树提供了第一个终止叶节点。如果是阴天,我们将会玩,而如果是雨天或晴天,则有可能我们不玩。到目前为止的决策树可以表示为下图:
![图 4.60:决策树]()
图 4.60:决策树
注意
该图是手动创建的用于参考,并未包含在随附的源代码中。
-
现在我们重复这个过程,根据信息增益进行拆分,直到所有数据都被分配,决策树的所有分支终止。首先,移除阴天样本,因为它们不再提供任何额外信息:
df_next = df.loc[df.Outlook != 'overcast'] df_next我们将得到以下输出:
![图 4.61:移除阴天样本后的数据]()
图 4.61:移除阴天样本后的数据
-
现在,我们将注意力转向晴天样本,并重新运行增益计算以确定最佳的晴天信息拆分方式:
df_sunny = df_next.loc[df_next.Outlook == 'sunny'] -
重新计算晴天样本的熵:
entropy_decision = f_entropy_decision(df_sunny) entropy_decision输出将是:
![图 4.62:熵决策]()
图 4.62:熵决策
-
对晴天样本运行增益计算:
for col in df_sunny.columns[1:-1]: print(f'Gain(Decision, {col}) = {IG(df_sunny, col, entropy_decision):0.4f}')输出将如下所示:
![图 4.63:增益]()
图 4.63:增益
-
再次,我们选择增益最大的属性,即湿度。根据湿度对数据进行分组:
for name, temp in df_sunny.groupby('Humidity'): print('-' * 15) print(name) print('-' * 15) print(temp) print('-' * 15)输出将是:
![图 4.64: 根据湿度分组后的数据]()
图 4.64: 根据湿度分组后的数据
我们可以看到,当湿度高时,决策是“不玩”,而当湿度正常时,决策是“玩”。因此,更新我们对决策树的表示,我们得到:
![图 4.65: 拥有两个值的决策树]()
图 4.65: 拥有两个值的决策树
-
因此,最后需要分类的数据是雨天预报数据。只提取雨天数据并重新运行熵计算:
df_rain = df_next.loc[df_next.Outlook == 'rain'] entropy_decision = f_entropy_decision(df_rain) entropy_decision输出将是:
![图 4.66: 熵决策]()
图 4.66: 熵决策
-
对雨天子集重复计算增益:
for col in df_rain.columns[1:-1]: print(f'Gain(Decision, {col}) = {IG(df_rain, col, entropy_decision):0.4f}')输出将是:
![图 4.67: 增益]()
图 4.67: 增益
-
同样,基于最大增益值的属性进行分割时,需要根据风速值进行分割。所以,按风速对剩余信息进行分组:
for name, temp in df_rain.groupby('Windy'): print('-' * 15) print(name) print('-' * 15) print(temp) print('-' * 15)输出将是:

图 4.68: 根据风速分组的数据
最后,我们得到了完成树所需的所有终止叶子节点,因为根据风速进行分割得到两个集合,这些集合都表示“玩”(P)或“不玩”(N)的值。我们的完整决策树如下:

图 4.69: 最终决策树
我们可以看到,决策树与 K-NN 模型非常相似,都是利用整个训练集来构建模型。那么,如何用未见过的信息进行预测呢?只需要沿着树走,查看每个节点的决策,并应用未见样本的数据。最终的预测结果将是终止叶子节点指定的标签。假设我们有即将到来的周六的天气预报,想预测我们是否会进行游戏。天气预报如下:

图 4.70: 即将到来的周六的天气预报
该决策树如下所示(虚线圆圈表示树中选择的叶子节点):

图 4.71: 使用决策树进行新预测
现在,希望你已经对决策树的基本概念和序列决策过程有了合理的理解。在本练习中,我们介绍了其中一种初步的决策树方法,但实际上还有许多其他方法,而且许多更现代的方法,例如随机森林,比 ID3 更不容易发生过拟合(有关更多信息,请参见第五章,集成建模)。在掌握了决策树的基本原理后,我们现在将探讨如何使用 scikit-learn 提供的功能应用更复杂的模型。
scikit-learn 的决策树方法实现了 CART(分类与回归树)方法,这提供了在分类和回归问题中使用决策树的能力。CART 与 ID3 的不同之处在于,决策是通过将特征值与计算出的值进行比较来做出的,例如,对于 Iris 数据集,花瓣宽度是否小于 x 毫米?
练习 42:使用 CART 决策树进行 Iris 分类
在本练习中,我们将使用 scikit-learn 的决策树分类器对 Iris 数据进行分类,该分类器可以应用于分类和回归问题:
-
导入所需的包:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.tree import DecisionTreeClassifier -
加载 Iris 数据集:
df = pd.read_csv('iris-data.csv') df.head()输出将如下所示:
![图 4.72:前五行数据]()
图 4.72:前五行数据
-
随机抽取 10 行数据用于测试。决策树可能会过拟合训练数据,因此这将提供一个独立的测量指标来评估树的准确性:
np.random.seed(10) samples = np.random.randint(0, len(df), 10) df_test = df.iloc[samples] df = df.drop(samples) -
将模型拟合到训练数据并检查相应的准确性:
model = DecisionTreeClassifier() model = model.fit(df[['Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width']], df.Species) model.score(df[['Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width']], df.Species)输出将如下所示:
![图 4.73:模型得分输出]()
图 4.73:模型得分输出
我们的模型在训练集上的准确率为 100%。
-
检查测试集上的表现:
model.score(df_test[['Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width']], df_test.Species)输出将如下所示:
![图 4.74:使用 df_test 输出的模型得分]()
图 4.74:使用 df_test 输出的模型得分
-
决策树的一个优点是我们可以直观地表示模型,并准确看到模型的运作方式。安装所需的依赖包:
!conda install python-graphviz -
导入图形绘制包:
import graphviz from sklearn.tree import export_graphviz -
绘制模型图:
dot_data = export_graphviz(model, out_file=None) graph = graphviz.Source(dot_data) graph我们将得到如下输出:

图 4.75:CART 决策树的决策
该图展示了在 scikit-learn 模型中 CART 决策树的决策过程。每个节点的第一行表示在每个步骤做出的决策。第一个节点 X[2] <= 2.45 表示训练数据在第二列(花瓣长度)上进行划分,基于是否小于或等于 2.45。花瓣长度小于 2.45 的样本(共有 46 个)全部属于鸢尾花的 setosa 类,因此它们的 gini 值(类似于信息增益的度量)为零。如果花瓣长度大于 2.45,下一步的决策是花瓣宽度(第三列)是否小于或等于 1.75 毫米。这个决策/分支过程会一直持续,直到树被完全展开,所有终止叶子节点都已构建完成。
总结
本章介绍了许多强大且极其有用的分类模型,从使用线性回归作为分类器开始,然后通过使用逻辑回归分类器,我们观察到了显著的性能提升。接着,我们转向了 记忆 型模型,比如 K-NN,尽管它简单易于拟合,但能够在分类过程中形成复杂的非线性边界,即使输入信息是图像数据。最后,我们介绍了决策树和 ID3 算法。我们看到,像 K-NN 模型一样,决策树通过使用规则和决策门来记住训练数据,从而进行预测,并且具有相当高的准确性。
在下一章中,我们将扩展本章所学内容,涵盖集成技术,包括提升方法和非常有效的随机森林方法。
第五章:集成建模
学习目标
到本章结束时,你将能够:
-
解释偏差和方差的概念,以及它们如何导致欠拟合和过拟合
-
解释自助法(bootstrapping)背后的概念
-
使用决策树实现一个袋装分类器(bagging classifier)
-
实现自适应增强(adaptive boosting)和梯度增强(gradient boosting)模型
-
使用多个分类器实现堆叠集成(stacked ensemble)
本章介绍了偏差与方差,欠拟合与过拟合的内容,然后介绍集成建模。
介绍
在前几章中,我们讨论了两种监督学习问题:回归和分类。我们研究了每种类型的若干算法,并深入探讨了这些算法的工作原理。
但是,有时这些算法,无论多么复杂,都似乎无法在我们拥有的数据上表现得很好。可能有多种原因:也许数据本身不够好,也许我们试图找出的趋势根本不存在,或者可能是模型本身太复杂。
等等。什么?模型“过于复杂”怎么会是一个问题?哦,当然可以!如果模型过于复杂,而且数据量不足,模型可能会与数据拟合得过于精确,甚至学习到噪声和异常值,这正是我们所不希望发生的。
许多时候,当单个复杂算法给出的结果差异很大时,通过聚合一组模型的结果,我们可以得到更接近实际真相的结果。这是因为所有单个模型的误差有很大可能性会在我们做预测时互相抵消。
这种将多个算法组合在一起以进行聚合预测的方法就是集成建模的基础。集成方法的最终目标是将若干表现不佳的基本估计器(即各个独立算法)以某种方式组合起来,从而提高系统的整体性能,使得集成的算法结果能够生成一个比单一算法更强大、能更好地泛化的模型。
在本章中,我们将讨论如何构建一个集成模型来帮助我们建立一个强健的系统,使其能够做出准确的预测,而不会增加方差。我们将从讨论模型表现不佳的一些原因开始,然后转到偏差和方差的概念,以及过拟合和欠拟合。我们将介绍集成建模作为解决这些性能问题的方法,并讨论不同的集成方法,这些方法可以用于解决与表现不佳的模型相关的不同类型问题。
本章将讨论三种集成方法:装袋(bagging)、提升(boosting)和堆叠(stacking)。每种方法将从基本理论讨论到各种使用案例的讨论,以及每种方法可能不适合的使用案例。本章还将通过多个练习步骤引导您使用 Python 中的 scikit-learn 库来实现这些模型。
练习 43:导入模块并准备数据集
在这个练习中,我们将导入本章所需的所有模块,并准备好我们的数据集以进行接下来的练习:
-
导入所有必要的模块来操作数据和评估模型:
import pandas as pd import numpy as np %matplotlib inline import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score from sklearn.model_selection import KFold -
本章中将使用的数据集是泰坦尼克号数据集,此数据集在之前的章节中也有介绍。读取数据集并打印前五行:
data = pd.read_csv('titanic.csv') data.head()输出如下:
![图 5.1:前五行]()
图 5.1:前五行
-
为了使数据集准备好使用,我们将添加一个
preprocess函数,该函数将预处理数据集以使其符合 scikit-learn 库可接受的格式。本章假设数据集已经经过预处理并准备好使用,但我们将添加一个
preprocess函数,该函数将预处理数据集以使其符合Scikit-learn库可接受的格式。首先,我们创建一个
fix_age函数来预处理age函数并获得整数值。如果年龄为空,函数将返回-1以区分可用值,并且如果值小于1的分数,则将年龄值乘以100。然后,我们将此函数应用于age列。然后,我们将
Sex列转换为二元变量,女性为1,男性为0,随后使用 pandas 的get_dummies函数为Embarked列创建虚拟二元列。之后,我们将包含虚拟列的 DataFrame 与其余数值列组合,以创建最终 DataFrame,并由该函数返回。def preprocess(data): def fix_age(age): if np.isnan(age): return -1 elif age < 1: return age*100 else: return age data.loc[:, 'Age'] = data.Age.apply(fix_age) data.loc[:, 'Sex'] = data.Sex.apply(lambda s: int(s == 'female')) embarked = pd.get_dummies(data.Embarked, prefix='Emb')[['Emb_C','Emb_Q','Emb_S']] cols = ['Pclass','Sex','Age','SibSp','Parch','Fare'] return pd.concat([data[cols], embarked], axis=1).values -
将数据集分为训练集和验证集。
我们将数据集分为两部分 - 一部分用于练习中训练模型(
train),另一部分用于进行预测以评估每个模型的性能(val)。我们将使用前一步中编写的函数分别预处理训练和验证数据集。这里,
Survived二元变量是目标变量,确定每行中个体是否幸存于泰坦尼克号的沉没,因此我们从这两个拆分中的依赖变量列创建y_train和y_val:train, val = train_test_split(data, test_size=0.2, random_state=11) x_train = preprocess(train) y_train = train['Survived'].values x_val = preprocess(val) y_val = val['Survived'].values
让我们开始吧。
过拟合和欠拟合
假设我们将一个监督学习算法拟合到数据上,并随后使用该模型对一个独立的验证集进行预测。基于该模型如何进行泛化,也就是它对验证数据集中的数据点做出的预测,我们将认为该模型表现良好。
有时我们发现模型无法做出准确的预测,并且在验证数据集上的表现较差。这种较差的表现可能是由于模型过于简单,无法适当地拟合数据,或者模型过于复杂,无法对验证数据集进行有效的泛化。在前一种情况下,模型具有高偏差,导致欠拟合,而在后一种情况下,模型具有高方差,导致过拟合。
偏差
机器学习模型预测中的偏差表示预测值与真实值之间的差异。如果平均预测值与真实值相差较大,则模型被认为具有高偏差;反之,如果平均预测值接近真实值,则模型被认为具有低偏差。
高偏差表示模型无法捕捉数据中的复杂性,并且无法识别输入和输出之间的相关关系。
方差
机器学习模型预测中的方差表示预测值与真实值之间的分散程度。如果预测值分散且不稳定,则模型被认为具有高方差;反之,如果预测值一致且不太分散,则模型被认为具有低方差。
高方差表示模型无法对模型以前未见过的数据点进行泛化和做出准确预测:

图 5.2:数据点具有高偏差和高方差的可视化表示
欠拟合
假设我们在训练数据集上拟合了一个简单的模型,一个具有低模型复杂度的模型,例如一个简单的线性模型。我们拟合了一个能够在一定程度上表示训练数据中 X 和 Y 数据点之间关系的函数,但我们发现训练误差仍然很高。

图 5.3:回归中的欠拟合与理想拟合
例如,查看图 5.3中的两个回归图;第一个图显示了一个将直线拟合到数据的模型,第二个图显示了一个尝试将相对复杂的多项式拟合到数据的模型,后者似乎很好地表示了 X 和 Y 之间的映射关系。
我们可以说,第一个模型展示了欠拟合,因为它表现出了高偏差和低方差的特征;也就是说,虽然它无法捕捉输入与输出之间映射的复杂性,但它在预测中保持一致。这个模型在训练数据和验证数据上都会有较高的预测误差。
过拟合
假设我们训练了一个高度复杂的模型,几乎可以完美地对训练数据集进行预测。我们已经设法拟合了一个函数来表示训练数据中 X 和 Y 数据点之间的关系,使得训练数据上的预测误差极低:

图 5.4:回归中的理想拟合与过拟合
从图 5.4中的两个图中,我们可以看到,第二个图显示了一个试图对数据点拟合高度复杂函数的模型,而左侧的图代表了给定数据的理想拟合。
很明显,当我们尝试使用第一个模型预测在训练集未出现的 X 数据点的 Y 值时,我们会发现预测结果与相应的真实值相差甚远。这就是过拟合的表现:模型对数据拟合得过于精确,以至于无法对新的数据点进行泛化,因为模型学习了训练数据中的随机噪声和离群值。
这个模型展示了高方差和低偏差的特征:虽然平均预测值会接近真实值,但与真实值相比,预测值会相对分散。
克服欠拟合和过拟合的问题
从前面的章节中我们可以看到,当我们从一个过于简单的模型过渡到过于复杂的模型时,我们从一个具有高偏差和低方差的欠拟合模型,过渡到一个具有低偏差和高方差的过拟合模型。任何监督学习算法的目标是实现低偏差和低方差,并找到欠拟合和过拟合之间的平衡点。这将有助于算法从训练数据到验证数据点的良好泛化,从而在模型从未见过的数据上也能表现出良好的预测性能。
当模型对数据欠拟合时,改进性能的最佳方法是增加模型的复杂性,以便识别数据中的相关关系。这可以通过添加新特征或创建高偏差模型的集成来实现。然而,在这种情况下,添加更多的数据进行训练并没有帮助,因为限制因素是模型复杂度,更多的数据不会帮助减少模型的偏差。
然而,过拟合问题更难解决。以下是一些常见的应对过拟合问题的技术:
-
获取更多数据:一个高度复杂的模型很容易在小数据集上过拟合,但在大数据集上则不容易出现过拟合。
-
降维:减少特征数量有助于让模型变得不那么复杂。
-
正则化:向代价函数中添加一个新项,以调整系数(特别是线性回归中的高阶系数)使其趋向于较低值。
-
集成建模:聚合多个过拟合模型的预测结果可以有效地消除预测中的高方差,并且比单个过拟合训练数据的模型表现得更好。
我们将在第六章《模型评估》中更详细地讨论前三种方法的细微差别和考虑因素;本章将重点介绍不同的集成建模技术。一些常见的集成方法包括:
-
Bagging:即引导聚合的简称,这种技术也用于减少模型的方差并避免过拟合。其过程是一次性选择一部分特征和数据点,对每个子集训练一个模型,随后将所有模型的结果汇聚成最终的预测。
-
Boosting:这种技术用于减少偏差,而不是减少方差,它通过逐步训练新的模型,聚焦于之前模型中的错误分类数据点。
-
Stacking:这种技术的目的是提高分类器的预测能力,它涉及训练多个模型,然后使用组合算法根据所有这些模型的预测结果作为额外输入来做出最终预测。
我们将从 Bagging 开始,然后转向 Boosting 和 Stacking。
Bagging
“Bagging”一词来源于一种名为引导聚合(bootstrap aggregation)的方法。为了实现成功的预测模型,了解在何种情况下我们可以从使用引导法(bootstrapping)构建集成模型中受益是非常重要的。在本节中,我们将讨论如何利用引导方法创建一个最小化方差的集成模型,并探讨如何构建一个决策树集成模型,也就是随机森林算法。那么,什么是引导法,它如何帮助我们构建强健的集成模型呢?
引导法
引导法是指带有放回的随机抽样,即从由随机选择的数据点组成的数据集中抽取多个样本(每个样本称为重抽样),其中每个重抽样可能包含重复的数据点,每个数据点都有相同的概率从整个数据集中被选中:

图 5.5:随机选择数据点
从前面的图示中,我们可以看到,从主数据集中抽取的五个引导样本各不相同,且具有不同的特征。因此,在每个重抽样上训练模型将会得到不同的预测结果。
以下是自举的优势:
-
每个重新采样可以包含与整个数据集不同的特征,这使我们能够从不同的视角了解数据的行为。
-
利用自举法的算法能够更加健壮,并且更好地处理未见过的数据,特别是在容易导致过拟合的较小数据集上。
-
自举法可以通过使用具有不同变化和特征的数据集来测试预测的稳定性,从而得到更加健壮的模型。
自举聚合
现在我们知道了什么是自举,那么装袋集成究竟是什么?它本质上是一个集成模型,它在每个重新采样上生成多个预测器的版本,并使用这些版本来获取聚合的预测器。在回归问题中,聚合步骤通过模型的平均值来进行元预测,在分类问题中则通过投票来进行预测类别。
以下图示展示了如何从自举抽样构建装袋估计器,具体见图 5.5:

图 5.6:从自举抽样构建的装袋估计器
由于每个模型基本上是独立的,所有基础模型可以并行训练,这显著加快了训练过程,并允许我们利用当今手头的计算能力。
装袋通过在其构建过程中引入随机化来帮助减少整个集成的方差,并且通常与具有过度拟合训练数据倾向的基础预测器一起使用。在这里需要考虑的主要点是训练数据集的稳定性(或缺乏稳定性):在数据中轻微的扰动可能导致训练模型显著变化的情况下,装袋可以提高准确性。
scikit-learn 使用 BaggingClassifier 和 BaggingRegressor 来实现用于分类和回归任务的通用装袋集成。这些的主要输入是在每次重新采样上使用的基础估计器,以及要使用的估计器数量(即重新采样的数量)。
练习 44:使用装袋分类器
在本练习中,我们将使用 scikit-learn 的 BaggingClassifier 作为我们的集成,使用 DecisionTreeClassifier 作为基础估计器。我们知道决策树容易过拟合,因此在装袋集成中使用的基础估计器应具有高方差和低偏差,这两者都是重要的特征。
-
导入基础和集成分类器:
from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import BaggingClassifier -
指定超参数并初始化模型。
在这里,我们将首先指定基础估计器的超参数,使用决策树分类器,并以熵或信息增益作为划分标准。我们不会对树的深度或每棵树的叶节点大小/数量设置任何限制,以便树能够完全生长。接下来,我们将定义袋装分类器的超参数,并将基础估计器对象作为超参数传递给分类器。
对于我们的示例,我们将选择 50 个基础估计器,这些估计器将并行运行并利用机器上所有可用的处理器(通过指定
n_jobs=-1来实现)。此外,我们将指定max_samples为 0.5,表示自助法样本数量应为总数据集的一半。我们还将设置一个随机状态(为任意值,且在整个过程中保持不变),以确保结果的可复现性:dt_params = { 'criterion': 'entropy', 'random_state': 11 } dt = DecisionTreeClassifier(**dt_params) bc_params = { 'base_estimator': dt, 'n_estimators': 50, 'max_samples': 0.5, 'random_state': 11, 'n_jobs': -1 } bc = BaggingClassifier(**bc_params) -
拟合袋装分类器模型到训练数据并计算预测准确性。
让我们拟合袋装分类器,并找出训练集和验证集的元预测。接下来,找出训练集和验证集数据集的预测准确性:
bc.fit(x_train, y_train) bc_preds_train = bc.predict(x_train) bc_preds_val = bc.predict(x_val) print('Bagging Classifier:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=bc_preds_train), accuracy_score(y_true=y_val, y_pred=bc_preds_val) ))输出如下:
![图 5.7:袋装分类器的预测准确性]()
图 5.7:袋装分类器的预测准确性
-
拟合决策树模型到训练数据以比较预测准确性。
我们还将拟合决策树(使用在第二步中初始化的对象),以便能够将集成模型的预测准确性与基础预测器进行比较:
dt.fit(x_train, y_train) dt_preds_train = dt.predict(x_train) dt_preds_val = dt.predict(x_val) print('Decision Tree:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=dt_preds_train), accuracy_score(y_true=y_val, y_pred=dt_preds_val) ))输出如下:

图 5.8:决策树的预测准确性
在这里,我们可以看到,尽管决策树的训练准确度远高于袋装分类器,但在验证集上的准确度较低,明显表明决策树对训练数据发生了过拟合。另一方面,袋装集成方法减少了整体方差,从而得到更为准确的预测。
随机森林
决策树常见的问题是每个节点的划分是使用贪婪算法进行的,该算法通过最小化叶节点的熵来进行划分。考虑到这一点,袋装分类器中的基础估计器决策树在划分特征上可能仍然相似,因此其预测结果也可能非常相似。然而,只有当基础模型的预测结果不相关时,袋装方法才有助于减少预测的方差。
随机森林算法通过不仅对整体训练数据集中的数据点进行引导抽样,还对每棵树的分裂特征进行引导抽样,从而尝试克服这个问题。这确保了当贪心算法在搜索最佳特征进行分裂时,整体最佳特征可能并不总是在引导抽样的特征中可用,因此不会被选择——从而导致基础树具有不同的结构。这个简单的调整使得最佳估计器能够以这样的方式进行训练:即森林中每棵树的预测结果与其他树的预测结果相关的概率更低。
随机森林中的每个基础估计器都有一个随机的数据点样本和一个随机的特征样本。由于集成是由决策树构成的,因此该算法被称为随机森林。
练习 45:使用随机森林构建集成模型
随机森林的两个主要参数是特征的比例和训练每个基础决策树的引导数据点的比例。
在本次练习中,我们将使用 scikit-learn 的RandomForestClassifier来构建集成模型:
-
导入集成分类器:
from sklearn.ensemble import RandomForestClassifier -
指定超参数并初始化模型。
在这里,我们将使用熵作为决策树分裂标准,森林中包含 100 棵树。与之前一样,我们不会对树的深度或叶子节点的大小/数量设置任何限制。与袋装分类器不同,袋装分类器在初始化时需要输入
max_samples,而随机森林算法只接受max_features,表示引导样本中的特征数(或比例)。我们将把此值设置为 0.5,这样每棵树只考虑六个特征中的三个:rf_params = { 'n_estimators': 100, 'criterion': 'entropy', 'max_features': 0.5, 'min_samples_leaf': 10, 'random_state': 11, 'n_jobs': -1 } rf = RandomForestClassifier(**rf_params) -
将随机森林分类器模型拟合到训练数据并计算预测准确度。
让我们拟合随机森林模型,并找到训练集和验证集的元预测。接下来,我们计算训练集和验证数据集上的预测准确度:
rf.fit(x_train, y_train) rf_preds_train = rf.predict(x_train) rf_preds_val = rf.predict(x_val) print('Random Forest:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=rf_preds_train), accuracy_score(y_true=y_val, y_pred=rf_preds_val) ))输出结果如下:

图 5.9:使用随机森林的训练和验证准确度
如果我们将随机森林在我们的数据集上的预测准确度与袋装分类器的预测准确度进行比较,我们可以看到,尽管后者在训练数据集上的准确度更高,但在验证集上的准确度几乎相同。
提升法
我们将讨论的第二种集成技术是boosting,它涉及逐步训练新模型,这些模型专注于先前模型中被错分的数据点,并利用加权平均将弱模型(具有高偏差的欠拟合模型)转变为更强的模型。与 bagging 不同,其中每个基本估计器可以独立训练,boosted 算法中每个基本估计器的训练依赖于前一个估计器。
尽管 boosting 也使用了自举法的概念,但与 bagging 不同,由于每个数据样本都有权重,这意味着某些自举样本可能被更频繁地用于训练。在训练每个模型时,算法跟踪哪些特征最有用,哪些数据样本具有最大的预测误差;这些样本被赋予更高的权重,并被认为需要更多次迭代来正确训练模型。
在预测输出时,boosting 集成从每个基本估计器的预测中取加权平均值,对训练阶段中误差较小的模型给予较高的权重。这意味着对于在迭代中由模型错分的数据点,增加这些数据点的权重,以便下一个模型更有可能正确分类它们。
与 bagging 类似,所有 boosting 基本估计器的结果被聚合以产生元预测。然而,与 bagging 不同的是,boosted 集成的准确性随着集成中基本估计器的数量显著增加而增加:

图 5.10: 一个 boosted 集成
在图中,我们可以看到,在每次迭代后,错分的点具有增加的权重(由较大的图标表示),以便下一个被训练的基本估计器能够专注于这些点。最终预测器已经整合了每个基本估计器的决策边界。
自适应 Boosting
让我们谈谈一种称为自适应 boosting的 boosting 技术,它最适合提升决策桩在二元分类问题中的性能。决策桩本质上是深度为一的决策树(只对一个特征进行一次分割),因此是弱学习器。自适应 boosting 的主要原理与之前相同:通过改进基本估计器在失败区域上的表现,将一组弱学习器转化为强学习器。
首先,第一个基学习器从主训练集中抽取一个数据点的自助样本(bootstrap),并拟合一个决策树桩来对样本点进行分类,之后将训练好的决策树桩拟合到完整的训练数据上。对于那些被误分类的样本,权重会增加,从而增加这些数据点在下一个基学习器的自助样本中被选中的概率。随后,在新的自助样本上再次训练一个决策树桩,对数据点进行分类。接下来,包含两个基学习器的小型集成模型被用来对整个训练集中的数据点进行分类。在第二轮中被误分类的数据点会获得更高的权重,以提高它们被选中的概率,直到集成模型达到所需的基学习器数量为止。
自适应增强(adaptive boosting)的一个缺点是,算法容易受到噪声数据点和异常值的影响,因为它试图完美拟合每一个数据点。因此,当基学习器的数量非常高时,算法容易出现过拟合。
练习 46:自适应增强
在本练习中,我们将使用 scikit-learn 实现的自适应增强分类算法 AdaBoostClassifier:
-
导入分类器:
from sklearn.ensemble import AdaBoostClassifier -
指定超参数并初始化模型。
在这里,我们首先指定基学习器的超参数,使用的分类器是最大深度为 1 的决策树分类器,即决策树桩。接下来,我们将定义 AdaBoost 分类器的超参数,并将基学习器对象作为超参数传递给分类器:
dt_params = { 'max_depth': 1, 'random_state': 11 } dt = DecisionTreeClassifier(**dt_params) ab_params = { 'n_estimators': 100, 'base_estimator': dt, 'random_state': 11 } ab = AdaBoostClassifier(**ab_params) -
将模型拟合到训练数据。
让我们拟合 AdaBoost 模型,并找到训练集和验证集的元预测。接下来,计算训练集和验证集上的预测准确率:
ab.fit(x_train, y_train) ab_preds_train = ab.predict(x_train) ab_preds_val = ab.predict(x_val) print('Adaptive Boosting:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=ab_preds_train), accuracy_score(y_true=y_val, y_pred=ab_preds_val) ))输出如下:
![图 5.11:使用自适应增强的训练数据和验证数据的准确性]()
图 5.11:使用自适应增强的训练数据和验证数据的准确性
-
计算不同基学习器数量下,模型在训练数据和验证数据上的预测准确率。
之前我们提到过,随着基学习器数量的增加,准确性通常会提高,但如果使用过多的基学习器,模型也容易出现过拟合。让我们计算预测准确率,以便找出模型开始过拟合训练数据的点:
ab_params = { 'base_estimator': dt, 'random_state': 11 } n_estimator_values = list(range(10, 210, 10)) train_accuracies, val_accuracies = [], [] for n_estimators in n_estimator_values: ab = AdaBoostClassifier(n_estimators=n_estimators, **ab_params) ab.fit(x_train, y_train) ab_preds_train = ab.predict(x_train) ab_preds_val = ab.predict(x_val) train_accuracies.append(accuracy_score(y_true=y_train, y_pred=ab_preds_train)) val_accuracies.append(accuracy_score(y_true=y_val, y_pred=ab_preds_val)) -
绘制一条折线图,直观展示训练集和验证集上的预测准确率趋势:
plt.figure(figsize=(10,7)) plt.plot(n_estimator_values, train_accuracies, label='Train') plt.plot(n_estimator_values, val_accuracies, label='Validation') plt.ylabel('Accuracy score') plt.xlabel('n_estimators') plt.legend() plt.show()输出如下:

图 5.12:预测准确率的趋势
正如前面提到的,我们可以看到,当决策树桩的数量从 10 增加到 200 时,训练准确度几乎一直在增加。然而,验证准确度在 0.84 到 0.86 之间波动,并且随着决策树桩数量的增加开始下降。这是因为 AdaBoost 算法试图拟合噪声数据点和离群值。
梯度提升
梯度提升是对提升方法的扩展,它将提升过程视为一个优化问题。定义了一个损失函数,代表误差残差(预测值与真实值之间的差异),并使用梯度下降算法来优化损失函数。
在第一步中,添加一个基估计器(这将是一个弱学习器),并在整个训练数据集上进行训练。计算预测所带来的损失,并且为了减少误差残差,更新损失函数,为那些现有估计器表现不佳的数据点添加更多的基估计器。接着,算法迭代地添加新的基估计器并计算损失,以便优化算法更新模型,最小化残差。
在自适应提升的情况下,决策树桩被用作基估计器的弱学习器。然而,对于梯度提升方法,可以使用更大的树,但仍应通过限制最大层数、节点数、分裂数或叶节点数来约束弱学习器。这确保了基估计器仍然是弱学习器,但它们可以以贪婪的方式构建。
从第三章,回归分析中我们知道,梯度下降算法可以用来最小化一组参数,比如回归方程中的系数。然而,在构建集成时,我们使用的是决策树而不是需要优化的参数。每一步计算损失后,梯度下降算法必须修改将要加入集成的新树的参数,以减少损失。这种方法更常被称为功能梯度下降。
练习 47:GradientBoostingClassifier
随机森林的两个主要参数是特征的比例和用于训练每棵基决策树的自助法数据点的比例。
在本次练习中,我们将使用 scikit-learn 的GradientBoostingClassifier来构建提升集成模型:
-
导入集成分类器:
from sklearn.ensemble import GradientBoostingClassifier -
指定超参数并初始化模型。
在这里,我们将使用 100 棵决策树作为基估计器,每棵树的最大深度为 3,每个叶节点的最小样本数为 5。虽然我们没有像前面的例子那样使用决策树桩,但树仍然很小,并且可以被视为一个弱学习器:
gbc_params = { 'n_estimators': 100, 'max_depth': 3, 'min_samples_leaf': 5, 'random_state': 11 } gbc = GradientBoostingClassifier(**gbc_params) -
拟合梯度提升模型到训练数据并计算预测准确性。
让我们拟合集成模型,并找到训练集和验证集的元预测结果。接下来,我们将找到训练集和验证集上的预测准确性:
gbc.fit(x_train, y_train) gbc_preds_train = gbc.predict(x_train) gbc_preds_val = gbc.predict(x_val) print('Gradient Boosting Classifier:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=gbc_preds_train), accuracy_score(y_true=y_val, y_pred=gbc_preds_val) ))输出如下:

图 5.13:训练集和验证集上的预测准确性
我们可以看到,与自适应提升集成模型相比,梯度提升集成模型在训练集和验证集上的准确性都更高。
堆叠
堆叠(Stacking)或堆叠泛化(也叫元集成)是一种模型集成技术,涉及将多个预测模型的信息结合起来,并将其作为特征生成一个新模型。由于堆叠模型通过平滑效应以及能够“选择”在某些场景下表现最好的基础模型,它通常会优于每个单独的模型。考虑到这一点,当每个基础模型之间有显著差异时,堆叠通常是最有效的。
堆叠使用基础模型的预测作为训练最终模型的附加特征——这些被称为元特征。堆叠模型本质上充当一个分类器,决定每个模型在哪些地方表现良好,在哪些地方表现较差。
然而,你不能简单地在整个训练数据上训练基础模型,在整个验证数据集上生成预测结果,然后将这些结果用于二级训练。这会导致基础模型的预测结果已经“看过”测试集,因此在使用这些预测结果时可能会发生过拟合。
需要注意的是,对于每一行的元特征,其值不能使用包含该行训练数据的模型来预测,因为这样会导致过拟合的风险,因为基础预测已经“看过”该行的目标变量。常见的做法是将训练数据分成k个子集,这样,在为每个子集找到元特征时,我们只会在剩余数据上训练模型。这样做还能避免模型已经“看到”的数据过拟合的问题:

图 5.14:一个堆叠集成模型
上图展示了如何实现这一过程:我们将训练数据分成k个折叠,并通过在剩余的k-1个折叠上训练模型,找到每个折叠上基础模型的预测结果。因此,一旦我们得到每个折叠的元预测结果,就可以将这些元预测结果与原始特征一起用于训练堆叠模型。
练习 48:构建堆叠模型
在此练习中,我们将使用支持向量机(scikit-learn 的 LinearSVC)和 k 近邻(scikit-learn 的 KNeighborsClassifier)作为基础预测器,堆叠模型将是逻辑回归分类器。
-
导入基础模型和用于堆叠的模型:
# Base models from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import LinearSVC # Stacking model from sklearn.linear_model import LogisticRegression -
创建一个新的训练集,其中包含来自基础预测器的额外列。
我们需要为每个模型的预测值创建两个新列,这些列将作为集成模型在测试和训练集中的特征使用。由于 NumPy 数组是不可变的,我们将创建一个新数组,其行数与训练数据集相同,列数比训练数据集多两列。创建数据集后,让我们打印出来看看它的样子:
x_train_with_metapreds = np.zeros((x_train.shape[0], x_train.shape[1]+2)) x_train_with_metapreds[:, :-2] = x_train x_train_with_metapreds[:, -2:] = -1 print(x_train_with_metapreds)输出如下:
![图 5.15:预测值的新列]()
图 5.15:预测值的新列
正如我们所见,每行末尾有两列填充有 -1 值。
-
使用 k 折策略训练基础模型。
让我们取 k=5。对于这五个折叠,使用其他四个折叠进行训练,并在第五个折叠上进行预测。然后,将这些预测添加到新的 NumPy 数组中用于基础预测的占位列中。
首先,我们用值为
k和一个随机状态初始化KFold对象,以保持可重现性。kf.split()函数将数据集作为输入进行分割,并返回一个迭代器,迭代器中的每个元素分别对应于训练和验证折叠中的索引列表。每次循环迭代器时,可以使用这些索引值将训练数据细分为每行的训练和预测。一旦数据适当地分割,我们就会在四分之四的数据上训练这两个基础预测器,并在剩余四分之一的行上预测值。然后,将这些预测值插入到在 步骤 2 中用
-1初始化的两个占位列中:kf = KFold(n_splits=5, random_state=11) for train_indices, val_indices in kf.split(x_train): kfold_x_train, kfold_x_val = x_train[train_indices], x_train[val_indices] kfold_y_train, kfold_y_val = y_train[train_indices], y_train[val_indices] svm = LinearSVC(random_state=11, max_iter=1000) svm.fit(kfold_x_train, kfold_y_train) svm_pred = svm.predict(kfold_x_val) knn = KNeighborsClassifier(n_neighbors=4) knn.fit(kfold_x_train, kfold_y_train) knn_pred = knn.predict(kfold_x_val) x_train_with_metapreds[val_indices, -2] = svm_pred x_train_with_metapreds[val_indices, -1] = knn_pred -
创建一个新的验证集,其中包含来自基础预测器的额外预测列。
就像我们在 步骤 2 中所做的那样,我们也会在验证数据集中添加两个基础模型预测的占位列:
x_val_with_metapreds = np.zeros((x_val.shape[0], x_val.shape[1]+2)) x_val_with_metapreds[:, :-2] = x_val x_val_with_metapreds[:, -2:] = -1 print(x_val_with_metapreds)输出如下:
![图 5.16:来自基础预测器的额外预测列]()
图 5.16:来自基础预测器的额外预测列
-
在完整的训练集上拟合基础模型,以获取验证集的元特征。
接下来,我们将在完整的训练数据集上训练这两个基础预测器,以获取验证数据集的元预测值。这类似于我们在 步骤 3 中对每个折叠所做的操作:
svm = LinearSVC(random_state=11, max_iter=1000) svm.fit(x_train, y_train) knn = KNeighborsClassifier(n_neighbors=4) knn.fit(x_train, y_train) svm_pred = svm.predict(x_val) knn_pred = knn.predict(x_val) x_val_with_metapreds[:, -2] = svm_pred x_val_with_metapreds[:, -1] = knn_pred -
训练堆叠模型并使用最终预测计算准确性。
最后一步是使用训练数据集的所有列以及基模型的元预测结果来训练逻辑回归模型。我们使用该模型计算训练集和验证集的预测准确性:
lr = LogisticRegression(random_state=11) lr.fit(x_train_with_metapreds, y_train) lr_preds_train = lr.predict(x_train_with_metapreds) lr_preds_val = lr.predict(x_val_with_metapreds) print('Stacked Classifier:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=lr_preds_train), accuracy_score(y_true=y_val, y_pred=lr_preds_val) ))输出如下:
![图 5.17:使用堆叠分类器的准确率]()
图 5.17:使用堆叠分类器的准确率
-
比较准确度与基模型的准确度。
为了了解堆叠方法带来的性能提升,我们计算基预测器在训练集和验证集上的准确率,并将其与堆叠模型的准确率进行比较:
print('SVM:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=svm.predict(x_train)), accuracy_score(y_true=y_val, y_pred=svm_pred) )) print('kNN:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=knn.predict(x_train)), accuracy_score(y_true=y_val, y_pred=knn_pred) ))输出如下:

图 5.18:使用 SVM 和 K-NN 的训练和验证数据准确度
如我们所见,堆叠模型不仅使得验证准确度显著高于任何一个基预测器,而且它的准确度接近 89%,是本章讨论的所有集成模型中最高的。
活动 14:使用独立和集成算法进行堆叠
在本活动中,我们将使用Kaggle 房价:高级回归技术数据库(可在www.kaggle.com/c/house-prices-advanced-regression-techniques/data上获取,或在 GitHub 上访问github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python),该数据集我们在第二章《探索性数据分析与可视化》中做过 EDA。这份数据集旨在解决回归问题(即目标变量为连续值的范围)。在本活动中,我们将使用决策树、K-最近邻、随机森林和梯度提升算法在数据上训练个体回归器。然后,我们将构建一个堆叠线性回归模型,使用所有这些算法并比较每个模型的性能。我们将使用平均绝对误差(MAE)作为本活动的评估指标。
执行的步骤如下:
-
导入相关库。
-
读取数据。
-
对数据集进行预处理,去除空值,并对分类变量进行独热编码,为建模准备数据。
-
将数据集分为训练集和验证集。
-
初始化字典以存储训练和验证的 MAE 值。
-
使用以下超参数训练决策树模型并保存得分:
dt_params = { 'criterion': 'mae', 'min_samples_leaf': 10, 'random_state': 11 } -
使用以下超参数训练 k-最近邻模型并保存得分:
knn_params = { 'n_neighbors': 5 } -
使用以下超参数训练随机森林模型并保存得分:
rf_params = { 'n_estimators': 50, 'criterion': 'mae', 'max_features': 'sqrt', 'min_samples_leaf': 10, 'random_state': 11, 'n_jobs': -1 } -
使用以下超参数训练梯度提升模型并保存得分:
gbr_params = { 'n_estimators': 50, 'criterion': 'mae', 'max_features': 'sqrt', 'min_samples_leaf': 10, 'random_state': 11 } -
准备训练集和验证集,其中四个元估计器具有与前面步骤中使用的相同的超参数。
-
训练一个线性回归模型作为堆叠模型。
-
可视化每个独立模型和堆叠模型的训练误差和验证误差。
注意
该活动的解决方案可以在第 364 页找到。
总结
本章从讨论过拟合和欠拟合以及它们如何影响模型在未见数据上的表现开始。接着,本章探讨了集成建模作为解决这些问题的方法,并继续讨论了可以使用的不同集成方法,以及它们如何减少在进行预测时遇到的总体偏差或方差。
我们首先讨论了袋装算法并介绍了自助抽样的概念。然后,我们看了随机森林作为袋装集成的经典例子,并完成了在之前的泰坦尼克数据集上构建袋装分类器和随机森林分类器的练习。
然后,我们继续讨论了提升算法,如何成功减少系统中的偏差,并理解了如何实现自适应提升和梯度提升。我们讨论的最后一种集成方法是堆叠,正如我们从练习中看到的那样,它给出了我们实现的所有集成方法中最好的准确率。
尽管构建集成模型是减少偏差和方差的好方法,而且它们通常比单一模型表现得更好,但它们本身也有自己的问题和使用场景。虽然袋装(bagging)在避免过拟合时非常有效,但提升(boosting)可以减少偏差和方差,尽管它仍然可能有过拟合的倾向。而堆叠(stacking)则是当一个模型在某部分数据上表现良好,而另一个模型在另一部分数据上表现更好时的好选择。
在下一章,我们将通过探讨验证技术来详细研究克服过拟合和欠拟合问题的方法,也就是评估模型性能的方法,以及如何使用不同的指标作为参考,构建最适合我们用例的模型。
第六章:模型评估
学习目标
本章结束时,你将能够:
-
解释评估模型的重要性
-
使用多种指标评估回归和分类模型
-
选择合适的评估指标来评估和调优模型
-
解释持出数据集的重要性和采样的类型
-
进行超参数调优以找到最佳模型
-
计算特征重要性并解释它们为何重要
本章介绍了如何通过使用超参数和模型评估指标来提升模型性能。
介绍
在前面的三章中,我们讨论了两种类型的监督学习问题——回归和分类,接着介绍了集成模型,它是由多个基础模型的组合构建而成。我们建立了几个模型,并讨论了它们的工作原理及原因。
然而,这还不足以将模型投入生产。模型开发是一个迭代过程,模型训练步骤之后是验证和更新步骤:

图 6.1:机器学习模型开发过程
本章将解释前面流程图中展示的外围步骤;我们将讨论如何选择合适的超参数,以及如何使用合适的误差指标进行模型验证。通过反复执行这两项任务,提升模型性能。
但是,为什么评估模型很重要呢?假设你已经训练好了模型,提供了一些超参数,做出了预测并找到了准确率。这就是其核心内容,但如何确保你的模型发挥出了最佳能力呢?我们需要确保你所制定的性能评估标准实际上能够代表模型,并且模型在未见过的测试数据集上也能够表现良好。
确保模型达到最佳状态的关键部分出现在初始训练之后:即评估和提升模型性能的过程。本章将引导你了解这一过程中所需的基本技术。
在本章中,我们将首先讨论为什么模型评估如此重要,并介绍几种回归任务和分类任务的评估指标,这些指标可以用来量化模型的预测性能。接下来,我们将讨论持出数据集和 k 折交叉验证,并解释为什么测试集必须独立于验证集。
在此之后,我们将讨论可以用来提高模型表现的策略。在上一章中,我们谈到了如何一个具有高偏差或高方差的模型会导致表现不佳,以及如何通过构建集成模型来帮助我们建立一个更加稳健、更加准确的系统,而不增加整体方差。我们还提到了一些避免过拟合训练数据的技巧:
-
获取更多数据:一个复杂的模型可能很容易在小数据集上过拟合,但在更大的数据集上却可能不容易过拟合。
-
降维:减少特征的数量有助于使模型变得不那么复杂。
-
正则化:在代价函数中添加一个新项,以便调整系数(尤其是线性回归中的高阶系数)使其趋向于较小的值。
在本章中,我们将介绍学习曲线和验证曲线,作为查看训练误差和验证误差变化的方式,以帮助我们了解模型是否需要更多的数据,并找到合适的复杂度水平。接下来将介绍超参数调优,以提升模型表现,并简要介绍特征重要性。
练习 49:导入模块并准备我们的数据集
在本练习中,我们将加载在第五章(集成建模)中训练的数据和模型。我们将使用活动 14: 使用独立和集成算法进行堆叠中的堆叠线性回归模型,以及练习 45: 使用随机森林构建集成模型中的随机森林分类模型来预测乘客的生存情况:
-
导入相关的库:
import pandas as pd import numpy as np import pickle %matplotlib inline import matplotlib.pyplot as plt -
从第五章(集成建模)加载处理后的数据文件。我们将使用 pandas 的
read_csv()方法读取准备好的数据集,并在本章练习中使用它们。首先,我们将读取房价数据:house_prices_reg = pd.read_csv('houseprices_regression.csv') house_prices_reg.head()我们将看到以下输出:
![图 6.2: 房价数据的前五行]()
图 6.2: 房价数据的前五行
接下来,我们将读取泰坦尼克号的数据:
titanic_clf = pd.read_csv('titanic_classification.csv') titanic_clf.head()我们将看到以下输出:
![图 6.3: 泰坦尼克号数据的前五行]()
图 6.3: 泰坦尼克号数据的前五行
-
接下来,使用
pickle库从二进制文件中加载我们将在本章练习中使用的模型文件:with open('../Saved Models/titanic_regression.pkl', 'rb') as f: reg = pickle.load(f) with open('../Saved Models/random_forest_clf.pkl', 'rb') as f: rf = pickle.load(f)
让我们开始吧。
评估指标
评估机器学习模型是任何项目中的关键部分:一旦我们让模型从训练数据中学习,下一步就是衡量模型的表现。我们需要找到一种度量标准,不仅能告诉我们模型的预测准确度,还能让我们比较多个模型的表现,从而选择最适合我们用例的模型。
定义度量标准通常是我们在定义问题陈述和开始进行探索性数据分析(EDA)之前要做的第一件事,因为提前规划并思考我们打算如何评估构建的任何模型的性能以及如何判断模型是否达到最佳表现是个好主意。最终,计算性能评估度量将纳入机器学习管道中。
不用说,回归任务和分类任务的评估度量是不同的,因为前者的输出值是连续的,而后者的输出值是分类的。在这一部分,我们将探讨可以用来量化模型预测性能的不同度量标准。
回归
对于输入变量X,回归模型给出一个预测值,
,该值可以取一系列不同的值。理想的情况是模型能够预测出尽可能接近实际值y的
值。因此,两个值之间的差距越小,模型的表现就越好。回归度量通常涉及查看每个数据点的预测值与实际值之间的数值差异(即残差或误差值),然后以某种方式聚合这些差异。
我们来看一下下图,它绘制了每个点X的实际值和预测值:

图 6.4:线性回归问题中的实际值与预测值之间的残差
然而,我们不能仅仅对所有数据点的
的均值进行计算,因为可能存在某些数据点,其预测误差为正或负,最终的总和将抵消掉许多误差,并严重高估模型的性能。
相反,我们可以考虑每个数据点的绝对误差,并计算平均绝对误差(MAE),其公式如下:

图 6.5:平均绝对误差
在这里,
和
分别是第i个数据点的实际值和预测值。
MAE 是一个线性评分函数,意味着在聚合误差时,它给每个残差赋予相等的权重。MAE 的值可以从零到无穷大,并且不关心误差的方向(正误差或负误差)。由于这些是误差度量,通常希望其值越低(越接近零越好)。
为了避免误差方向影响性能评估,我们还可以对误差项进行平方处理。对平方误差取平均值即可得到均方误差(MSE):

图 6.6:均方误差
虽然 MAE 的单位与目标变量 y 相同,但 MSE 的单位将是 y 的平方单位,这可能使得在实际应用中判断 MSE 变得稍微不太直观。然而,如果我们对 MSE 取平方根,就能得到 均方根误差(RMSE):

图 6.7:均方根误差
由于在计算平均值之前对误差进行了平方处理,哪怕只有少数几个误差值很高,也会导致 RMSE 值显著增大。这意味着在我们希望惩罚大误差的模型中,RMSE 比 MAE 更有用。
由于 MAE 和 RMSE 的单位与目标变量相同,因此判断 MAE 或 RMSE 的某个特定值好坏可能很困难,因为没有参考的标准。为了解决这个问题,常用的指标是 R² 分数,也叫 R 平方分数:

图 6.8:R 平方分数
R2 分数的下限为 -∞,上限为 1。基础模型预测目标变量等于训练数据集中目标值的均值,即,对于所有 i 的值,
等于
。考虑到这一点,R2 的负值表示训练模型的预测结果比均值还要差,而接近 1 的值表示模型的均方误差(MSE)接近零时的情况。
练习 50:回归指标
在本次练习中,我们将使用在 第五章,集成建模 中的 活动 14:使用独立和集成算法进行堆叠 训练过的相同模型和处理过的数据集,来计算回归指标。我们将使用 scikit-learn 实现的 MAE 和 MSE:
-
导入度量函数:
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from math import sqrt -
使用加载的模型对给定数据进行预测。我们将使用与 第五章,集成建模 中 活动 14:使用独立和集成算法进行堆叠 相同的特征,使用该模型对加载的数据集进行预测。我们保存的 y 列是目标变量,我们将相应地创建 X 和 y:
X = house_prices_reg.drop(columns=['y']) y = house_prices_reg['y'].values y_pred = reg.predict(X) -
计算 MAE、RMSE 和 R2 分数。我们将打印预测值的 MAE 和 RMSE 值,并打印模型的 R2 分数:
print('Mean Absolute Error = {}'.format(mean_absolute_error(y, y_pred))) print('Root Mean Squared Error = {}'.format(sqrt(mean_squared_error(y, y_pred)))) print('R Squared Score = {}'.format(r2_score(y, y_pred)))输出结果如下:

图 6.9:分数
我们可以看到 RMSE 明显高于 MAE。这表明某些数据点的残差特别大,这在较大的 RMSE 值中得到了突出表现。但 R2 分数接近 1,说明该模型相比于基础模型(基础模型预测的是均值)表现得几乎理想。
分类
对于一个输入变量X,分类任务给出了一个预测值,
,它可以取有限的几个值(在二分类问题中为两个值)。由于理想的情况是预测每个数据点的类别与实际类别相同,因此没有衡量预测类别与实际类别之间距离的指标。因此,要评判模型的表现,简单的方法就是判断模型是否正确地预测了类别。
判断分类模型表现的方法有两种:使用数值指标,或通过绘制曲线并观察曲线的形状。让我们更详细地探讨这两种方法。
数值指标
判断模型表现最简单且基本的方法是计算正确预测占总预测数的比例,这给出了准确率:

图 6.10:准确率
尽管准确率指标适用于任何类别数量的情况,但接下来的几个指标将以二分类问题为背景进行讨论。此外,准确率在许多情况下可能不是评估分类任务表现的最佳指标。
让我们看一个欺诈检测的例子:假设问题是检测一封邮件是否欺诈。在这种情况下,我们的数据集高度倾斜(或不平衡,也就是说,一类数据点的数量远大于另一类数据点),在 10,000 封邮件中有 100 封(总数的 1%)被分类为欺诈(属于类别 1)。假设我们构建了两个模型:
-
第一个模型简单地将每封邮件预测为非欺诈,也就是说,10,000 封邮件中的每一封都被归类为类别 0。在这种情况下,10,000 封邮件中有 9,900 封被正确分类,这意味着该模型的准确率为 99%。
-
第二个模型将 100 封欺诈邮件预测为欺诈,但同时也错误地将另外 100 封邮件预测为欺诈。在这种情况下,同样有 100 个数据点在 10,000 封邮件中被误分类,模型的准确率为 99%。
我们如何比较这两种模型?构建欺诈检测模型的目的是让我们了解欺诈检测的效果:比起非欺诈邮件被误分类为欺诈邮件,正确分类欺诈邮件更为重要。尽管这两个模型的准确率相同,但第二个模型实际上比第一个更有效。
由于无法仅通过准确率捕获这一点,我们需要混淆矩阵,它是一个包含四种不同的预测值和实际值组合的表格,本质上为我们提供了分类问题预测结果的总结:

图 6.11:混淆矩阵
下面是矩阵中使用的术语的含义:
-
真正例和真负例:这些是分别在正类和负类中被正确预测的数据点数量。
-
假正例:也称为类型 1 错误,指的是实际上属于负类但被预测为正类的数据点数量。从前面的例子继续,如果一个正常的邮件被分类为欺诈邮件,则为假正例。
-
假负例:也称为类型 2 错误,指的是实际上属于正类但被预测为负类的数据点数量。假负例的例子是,如果一封欺诈邮件被分类为非欺诈邮件。
从混淆矩阵中可以推导出两个极其重要的指标:精度和召回率。

图 6.12: 精度

图 6.13: 召回率
精度告诉我们有多少实际的正例被正确地预测为正例(从模型认为相关的结果中,有多少实际是相关的?),而召回率告诉我们有多少预测为正例的结果实际上是正例(从真实的相关结果中,有多少被模型列入相关结果列表?)。这两个指标在类不平衡时尤其有用。
模型的精度和召回率通常存在权衡:如果必须召回所有相关的结果,模型将生成更多不准确的结果,从而降低精度。另一方面,要确保生成的结果中有更高比例的相关结果,就需要尽量少生成结果。大多数情况下,你会优先考虑精度或召回率,这完全取决于问题的具体要求。例如,由于确保所有欺诈性邮件被正确分类更为重要,因此召回率将是一个需要最大化的关键指标。
接下来的问题是,如何使用一个单一的数值来评估模型,综合考虑精度和召回率,而不是单独平衡这两个指标。F1 分数将两者合并成一个单一的数值,这个数值可以作为模型的公正评判标准,并且等于精度和召回率的调和平均值:

图 6.14: F1 分数
F1 分数的值总是介于 0(如果精度或召回率为零)和 1(如果精度和召回率都为 1)之间。分数越高,说明模型的性能越好。F1 分数对两个指标赋予相等的权重,并且是一般 Fβ 指标的一个特例,其中 β 可以调整,以便根据以下公式为召回率或精度赋予更多权重:

图 6.15: F β 值
β < 1 时,更注重精确度,而 β > 1 时,更注重召回率。F1 分数采用 β = 1,使两者权重相等。
曲线图
有时,我们不是预测类别,而是利用类别的概率值。举例来说,在一个二分类任务中,正类(类 1)和负类(类 0)的类别概率之和始终为 1(或 统一的 1),这意味着如果我们将分类概率视为类 1 的概率,并应用一个阈值,我们可以本质上将其作为一个截止值,来进行四舍五入(为 1)或下舍(为 0),从而得到输出的类别。
通常,通过改变阈值,我们可以得到分类概率接近 0.5 的数据点,这些数据点从一个类别转到另一个类别。例如,当阈值为 0.5 时,具有 0.4 概率的数据点会被分配为类 0,而具有 0.6 概率的数据点会被分配为类 1。但如果我们将阈值改为 0.35 或 0.65,这两个数据点都会被分类为 1 或 0。
事实证明,改变概率会改变精确度和召回率的值,这可以通过绘制精确度-召回率曲线来捕捉。图表的Y 轴表示精确度,X 轴表示召回率,对于从 0 到 1 的一系列阈值,图表绘制每一个(召回率,精确度)点。连接这些点便得到曲线。以下图显示了一个例子:

图 6.16:精确度-召回率曲线
我们知道,在理想情况下,精确度和召回率的值将为 1。这意味着,当阈值从 0 增加到 1 时,精确度将保持为 1,但召回率会从 0 增加到 1,因为越来越多(相关的)数据点将被正确分类。因此,在理想情况下,精确度-召回率曲线基本上将是一个正方形,且曲线下面积(AUC)将等于 1。
因此,我们可以看到,和 F1 分数一样,AUC 是另一个从精确度和召回率行为中得出的指标,它结合了精确度和召回率的值来评估模型的性能。我们希望模型的 AUC 尽可能高,接近 1。
显示分类模型性能的另一个主要可视化技术是接收者操作特征(ROC)曲线。ROC 曲线绘制了真正例率(TPR)在Y 轴上的关系,以及假正例率(FPR)在X 轴上的关系,随着分类概率阈值的变化。TPR 恰好等于召回率(也称为模型的灵敏度),而 FPR 是特异性的补集(即 1 - FPR = 灵敏度);这两者都可以通过混淆矩阵使用以下公式推导:

图 6.17:真正例率

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_18.jpg)
图 6.18:假阳性率
下图展示了一个 ROC 曲线的示例,其绘制方式与精度-召回曲线相同:通过改变概率阈值,使得曲线上的每个点代表一个(TPR, FPR)数据点,对应于一个特定的概率阈值。

图 6.19:ROC 曲线
当类别比较平衡时,ROC 曲线更为有用,因为它们往往会在类别不平衡的数据集上呈现过于乐观的模型表现,尤其是在 ROC 曲线中的假阳性率使用了真正负例(而精度-召回曲线中没有此项)。
练习 51:分类指标
在本练习中,我们将使用在第五章《集成建模》中训练的随机森林模型,并使用其预测生成混淆矩阵,计算精度、召回率和 F1 得分,以此来评估我们的模型。我们将使用 scikit-learn 的实现来计算这些指标:
-
导入相关的库和函数:
from sklearn.metrics import (accuracy_score, confusion_matrix, precision_score, recall_score, f1_score) -
使用模型对所有数据点进行类别预测。我们将使用与之前相同的特征,并使用随机森林分类器对加载的数据集进行预测。scikit-learn 中的每个分类器都有一个
.predict_proba()函数,我们将在这里使用它,并结合标准的.predict()函数来分别提供类别概率和预测的类别:X = titanic_clf.iloc[:, :-1] y = titanic_clf.iloc[:, -1] y_pred = rf.predict(X) y_pred_probs = rf.predict_proba(X) -
计算准确率:
print('Accuracy Score = {}'.format(accuracy_score(y, y_pred)))输出将如下所示:
![图 6.20:准确率得分]()
图 6.20:准确率得分
-
打印混淆矩阵:
print(confusion_matrix(y_pred=y_pred, y_true=y))输出将如下所示:
![图 6.21:混淆矩阵]()
图 6.21:混淆矩阵
在这里,我们可以看到模型似乎有较高的假阴性数量,这意味着我们可以预期该模型的召回率将非常低。类似地,由于假阳性的数量仅为一个,我们可以预期模型将具有较高的精度。
-
计算精度和召回率:
print('Precision Score = {}'.format(precision_score(y, y_pred))) print('Recall Score = {}'.format(recall_score(y, y_pred)))输出将如下所示:
![图 6.22:精度和召回得分]()
图 6.22:精度和召回得分
-
计算 F1 得分:
print('F1 Score = {}'.format(f1_score(y, y_pred)))输出将如下所示:

图 6.23:F1 得分
我们可以看到,由于召回率极低,这也影响了 F1 得分,使其接近零。
现在我们已经讨论了可以用来衡量模型预测性能的指标,让我们谈谈验证策略,我们将使用这些指标来评估模型在不同情况下的表现。
数据集划分
在评估模型表现时,一个常见的错误是计算模型在训练数据上的预测误差,并基于训练数据集上的高预测准确率得出模型表现良好的结论。
这意味着我们正在尝试在模型已经见过的数据上进行测试,也就是说,模型已经学到了训练数据的行为,因为它曾经接触过这些数据——如果要求模型再次预测训练数据的行为,它无疑会表现得很好。而且,模型在训练数据上的表现越好,就越有可能意味着模型对数据了解得过于透彻,甚至学会了数据中的噪声和异常值的行为。
现在,高训练准确度会导致模型具有高方差,正如我们在前一章中看到的那样。为了获得模型性能的无偏估计,我们需要找出它在训练过程中没有接触过的数据上的预测准确度。这时,留出数据集就显得非常重要。
留出数据
留出数据集是指从训练数据中剥离出的样本,这部分数据在训练过程中未被模型接触,因此它对模型来说是未见过的。由于噪声是随机的,留出数据点很可能包含异常值和噪声数据,这些数据的行为与训练数据集中的数据有所不同。因此,在留出数据集上计算模型的性能,可以帮助我们验证模型是否过拟合,并为我们提供对模型性能的无偏视角。
我们在上一章开始时将泰坦尼克号数据集拆分为训练集和验证集。那么,什么是验证数据集,它与测试数据集有何不同呢?我们经常看到“验证集”和“测试集”这两个术语互换使用——虽然它们都指代留出数据集,但在目的上存在一些差异:
-
验证数据:在模型从训练数据中学习后,会在验证数据集上评估其性能。然而,为了让模型发挥最佳表现,我们需要对模型进行微调,并反复迭代评估更新后的模型性能,这一过程是在验证数据集上进行的。通常,表现最好的微调版本模型会被选为最终模型。
因此,模型在每次改进的迭代过程中都会接触到验证数据集,尽管本质上并没有从数据中学习。可以说,验证集间接地影响了模型。
-
测试数据:选择的最终模型现在在测试数据集上进行评估。在该数据集上测得的性能将是一个无偏的度量,作为模型的最终性能指标。这一最终评估是在模型已经在合并后的训练集和验证集上完全训练后进行的。此后,模型不再进行训练或更新。
这意味着模型仅在计算最终性能指标时暴露于测试数据集一次。
应当记住,验证数据集绝不应被用来评估模型的最终性能:如果模型已经看到并被修改以特定提高在验证集上的表现,那么我们对模型真实性能的估计会存在正偏。
然而,单一的保留验证数据集确实存在一些局限性:
-
由于模型在每次改进的迭代中只进行一次验证,因此使用这个单一评估可能难以捕捉预测中的不确定性。
-
将数据划分为训练集和验证集会减少训练模型时使用的数据量,这可能导致模型具有较高的方差。
-
最终模型可能会过拟合此验证集,因为它是根据此数据集的最大性能进行调优的。
如果我们使用称为 K 折交叉验证的验证技术,而不是仅使用单一验证数据集,这些挑战是可以克服的。
K 折交叉验证
K 折交叉验证是一种验证技术,它通过在k折中轮换验证集,帮助我们得到模型性能的无偏估计。其工作原理如下:
-
首先,我们选择k的值,并将数据划分为k个子集。
-
然后,我们将第一子集作为验证集,剩余的数据用于训练模型。
-
我们在验证子集上衡量模型的性能。
-
然后,我们将第二子集作为验证子集,并重复这一过程。
-
一旦我们完成这k次操作后,我们将所有折叠的性能度量值聚合,并呈现最终指标。
下图直观地解释了这一过程:

图 6.24:K 折交叉验证
尽管这种验证方法计算开销较大,但其优点超过了成本。这种方法确保模型在训练数据集中的每个示例上都得到验证一次,且最终得到的性能估计不偏向于验证集,尤其是在数据集较小的情况下。一个特例是留一法交叉验证,其中k的值等于数据点的数量。
采样
现在我们已经了解了用于划分数据集以进行模型训练和验证的策略,让我们讨论如何将数据点分配到这些划分中。我们可以通过两种方式对数据进行采样,这两种方式如下:
-
随机采样:这就是将整体数据集中的随机样本分配到训练集、验证集和/或测试集的过程。随机划分数据只有在所有数据点彼此独立时才有效。例如,如果数据是时间序列形式,随机划分就不适用,因为数据点是有序的,每个数据点都依赖于前一个数据点。随机划分数据会破坏这个顺序,忽视这种依赖关系。
-
分层采样:这是一种确保每个子集的目标变量的分布与原始数据集相同的方法。例如,如果原始数据集的两个类别的比例是 3:7,那么分层采样确保每个子集中的两个类别的比例也是 3:7。
分层采样很重要,因为在数据集的目标值分布与训练模型时的数据集不同的情况下,测试模型可能会得到一个无法代表模型实际性能的结果估计。
训练集、验证集和测试集的样本大小在模型评估过程中也起着重要作用。将一个较大的数据集留作最终模型性能测试,可以帮助我们获得对模型性能的无偏估计,并减少预测的方差,但如果测试集过大,以至于由于缺少训练数据影响了模型的训练能力,这将严重影响模型的表现。这个考虑特别适用于较小的数据集。
练习 52:使用分层采样的 K 折交叉验证
在这个练习中,我们将实现基于分层采样的 K 折交叉验证,使用 scikit-learn 的随机森林分类器。scikit-learn 中的 StratifiedKFold 类实现了交叉验证和采样的结合,我们将在练习中使用它:
-
导入相关的类。我们将导入 scikit-learn 的
StratifiedKFold类,这是KFold的一种变体,返回分层折叠,同时导入RandomForestClassifier:from sklearn.model_selection import StratifiedKFold from sklearn.ensemble import RandomForestClassifier -
为训练准备数据并初始化 k 折交叉验证对象。在这里,我们将使用五个折叠来评估模型,因此将
n_splits参数设置为5:X = titanic_clf.iloc[:, :-1].values y = titanic_clf.iloc[:, -1].values skf = StratifiedKFold(n_splits=5) -
对每个折叠训练一个分类器并记录得分。
StratifiedKFold类的功能类似于我们在上一章中使用的KFold类,练习 48:构建堆叠模型:对于五个折叠中的每一个,我们将在其他四个折叠上进行训练,并在第五个折叠上进行预测,找到第五个折叠上的准确率得分。正如我们在上一章中看到的,skf.split()函数以数据集为输入,返回一个迭代器,包含用于划分训练数据进行训练和验证的每行索引值:scores = [] for train_index, val_index in skf.split(X, y): X_train, X_val = X[train_index], X[val_index] y_train, y_val = y[train_index], y[val_index] rf_skf = RandomForestClassifier(**rf.get_params()) rf_skf.fit(X_train, y_train) y_pred = rf_skf.predict(X_val) scores.append(accuracy_score(y_val, y_pred)) print(scores)输出结果如下:
![图 6.25:使用随机森林分类器的得分]()
图 6.25:使用随机森林分类器的得分
-
打印汇总的准确率得分:
print('Mean Accuracy Score = {}'.format(np.mean(scores)))输出结果如下:

图 6.26:平均准确率得分
性能提升策略
监督式机器学习模型的性能提升是一个迭代过程,通常需要持续更新和评估周期才能得到完美的模型。虽然本章前面的部分讨论了评估策略,本节将讨论模型更新:我们将探讨如何确定模型所需的性能提升,并如何在模型中做出这些改变。
训练误差和测试误差的变化
在上一章中,我们介绍了欠拟合和过拟合的概念,并提到了几种克服它们的方法,随后介绍了集成模型。但是我们没有讨论如何识别我们的模型是否出现了欠拟合或过拟合的情况。
通常来说,查看学习曲线和验证曲线是很有用的。
学习曲线
学习曲线展示了随着训练数据量的增加,训练误差和验证误差的变化。通过观察曲线的形状,我们可以大致判断增加更多数据是否有利于建模,并可能改善模型的表现。
让我们来看一下以下图表:虚线曲线表示验证误差,实线曲线表示训练误差。左侧的图表显示这两条曲线趋向于一个相对较高的误差值。这意味着模型存在较高的偏差,增加更多的数据很可能不会对模型的表现产生影响。因此,与其浪费时间和金钱去收集更多的数据,我们只需要增加模型的复杂度。
另一方面,右侧的图表显示,即使训练集中的数据点数量增加,训练误差和测试误差之间的差距依然很大。这个宽广的差距表示系统的方差很高,也意味着模型过拟合。在这种情况下,增加更多的数据点可能有助于模型更好地进行泛化:

图 6.27:数据量增加的学习曲线
但是,我们如何识别完美的学习曲线呢?当我们有一个低偏差和低方差的模型时,我们会看到像下面图示那样的曲线。它展示了低训练误差(低偏差),以及当验证曲线和训练曲线汇聚时,二者之间的低差距(低方差)。在实际操作中,我们能看到的最好的学习曲线是那些趋于某个不可减少的误差值的曲线(该误差值由于数据集中的噪声和异常值而存在):

图 6.28: 低偏差和低方差模型随着训练数据量增大,训练和验证误差的变化
验证曲线
正如我们之前讨论的,机器学习模型的目标是能够推广到未见过的数据。验证曲线可以帮助我们找到一个欠拟合和过拟合模型之间的理想点,在这个点上,模型能够良好地进行推广。在上一章中,我们讨论了模型复杂度如何影响预测性能:我们说,当我们从一个过于简单的模型走向一个过于复杂的模型时,我们会从一个具有高偏差和低方差的欠拟合模型,过渡到一个具有低偏差和高方差的过拟合模型。
验证曲线显示了随着模型参数值变化,训练误差和验证误差的变化,这些模型参数在某种程度上控制着模型的复杂度——这可能是线性回归中的多项式的次数,或者是决策树分类器的深度。

图 6.29: 随着模型复杂度增加,训练和验证的变化
上面的图展示了随着模型复杂度变化(模型参数是其指示器之一),验证误差和训练误差如何变化。我们还可以看到,在阴影区域之间的某个点是总误差最小的地方,这个点位于欠拟合和过拟合之间的甜蜜点。找到这个点有助于我们找到理想的模型参数值,从而建立一个既具有低偏差又具有低方差的模型。
超参数调优
我们之前多次谈到超参数调优,现在让我们讨论一下它为什么如此重要。首先,需要注意的是,模型参数与模型超参数是不同的:前者是模型内部的,并且是从数据中学习得到的,而后者则定义了模型本身的架构。
一些超参数的示例如下:
-
用于线性回归模型的多项式特征的次数
-
允许的决策树分类器的最大深度
-
随机森林分类器中包含的树木数量
-
用于梯度下降算法的学习率
定义模型架构的设计选择可以显著影响模型的性能。通常,超参数的默认值可以工作,但是获取超参数的完美组合可以真正提升模型的预测能力,因为默认值可能完全不适合我们要建模的问题。在下图中,我们可以看到调整两个超参数值如何导致模型分数的巨大差异:

图 6.30:模型分数(Z 轴)随两个模型参数值(X 和 Y 轴)变化的情况
通过探索一系列可能的值来找到完美组合,这就是所谓的超参数调优。由于没有损失函数可用于最大化模型性能,调整超参数通常只涉及尝试不同组合并选择在验证期间表现最佳的组合。
有几种方式可以调整我们模型的超参数:
-
手动调整:当我们手动选择超参数的值时,这被称为手动调整。这通常是低效的,因为通过手动解决高维优化问题不仅可能很慢,而且也不允许模型达到其性能峰值,因为我们可能不会尝试每个超参数值的所有组合。
-
网格搜索:网格搜索涉及对提供的超参数值的每一组合进行训练和评估,并选择产生最佳性能模型的组合。由于这涉及对超参数空间进行详尽采样,因此计算成本相当高,效率低下。
-
随机搜索:尽管第一种方法因尝试的组合过少而被认为效率低下,但第二种方法因尝试的组合过多而被认为效率低下。随机搜索旨在通过从网格中选择超参数组合的随机子集,并仅为这些组合训练和评估模型来解决这个问题。或者,我们还可以为每个超参数提供统计分布,从中随机抽样值。
随机搜索的逻辑已被 Bergstra 和 Bengio 证明:如果网格上至少有 5%的点产生接近最优解,那么进行 60 次随机搜索将高概率找到该区域。
注
您可以阅读 Bergstra 和 Bengio 的论文,网址为
www.jmlr.org/papers/v13/bergstra12a.html。 -
贝叶斯优化:前两种方法涉及独立地实验不同超参数值的组合,并记录每个组合的模型性能。然而,贝叶斯优化是顺序地迭代实验,并允许我们利用先前实验的结果来改进下一个实验的采样方法。
练习 53:使用随机搜索进行超参数调优
使用 scikit-learn 的RandomizedSearchCV方法,我们可以定义一个超参数范围的网格,并从网格中随机采样,使用每个超参数值组合执行 K 折交叉验证。在这个练习中,我们将使用随机搜索方法进行超参数调优:
-
导入随机搜索类:
from sklearn.model_selection import RandomizedSearchCV -
为训练准备数据并初始化分类器。在这里,我们将初始化随机森林分类器,而不传递任何参数,因为这只是一个基础对象,稍后将在每个网格点上进行实例化并执行随机搜索:
X = titanic_clf.iloc[:, :-1].values y = titanic_clf.iloc[:, -1].values rf_rand = RandomForestClassifier() -
指定需要采样的参数。在这里,我们将列出每个超参数的不同值,这些值将用于网格中:
param_dist = {"n_estimators": list(range(10,210,10)), "max_depth": list(range(3,20)), "max_features": list(range(1, 10)), "min_samples_split": list(range(2, 11)), "bootstrap": [True, False], "criterion": ["gini", "entropy"]} -
运行随机搜索。我们用希望运行的试验总数、参数值字典、评分函数以及 K 折交叉验证中的折数来初始化随机搜索对象。然后,我们调用
.fit()函数来执行搜索:n_iter_search = 60 random_search = RandomizedSearchCV(rf_rand, param_distributions=param_dist, scoring='accuracy', n_iter=n_iter_search, cv=5) random_search.fit(X, y) -
打印前五个模型的评分和超参数。将
results字典转换为 pandas DataFrame,并按rank_test_score对值进行排序。然后,对于前五行,打印排名、平均验证得分和超参数:results = pd.DataFrame(random_search.cv_results_).sort_values('rank_test_score') for i, row in results.head().iterrows(): print("Model rank: {}".format(row.rank_test_score)) print("Mean validation score: {:.3f} (std: {:.3f})".format(row.mean_test_score, row.std_test_score)) print("Model Hyperparameters: {}\n".format(row.params))输出将如下所示:

图 6.31:前五个模型的评分和超参数
我们可以看到,表现最好的模型只有 70 棵树,而排名 2 到 4 的模型有 160 棵以上的树。此外,排名第 5 的模型只有 10 棵树,但其性能仍然与更复杂的模型相当。
特征重要性
虽然关注模型性能至关重要,但理解模型中各特征如何贡献于预测同样重要:
-
我们需要能够解释模型以及不同变量如何影响预测,以便向相关利益相关者说明为什么我们的模型成功。
-
数据可能存在偏差,在这些数据上训练模型可能会影响模型的性能,并导致模型评估出现偏差,在这种情况下,通过查找重要特征并分析它们来解释模型的能力将有助于调试模型的表现。
-
除了前面提到的点之外,还必须注意,某些模型偏差可能在社会或法律上是不可接受的。例如,如果一个模型表现良好,因为它隐含地对基于种族的特征赋予了较高的重要性,这可能会引发问题。
除了这些要点,找出特征重要性还可以帮助特征选择。如果数据具有高维度,并且训练的模型具有高方差,那么删除那些重要性较低的特征是一种通过降维来降低方差的方法。
练习 54:使用随机森林计算特征重要性
在这个练习中,我们将从我们之前加载的随机森林模型中找出特征的重要性:
-
找出特征重要性。让我们找出特征的重要性,并将其保存在一个 pandas DataFrame 中,索引为列名,并按降序排列这个 DataFrame:
feat_imps = pd.DataFrame({'importance': rf.feature_importances_}, index=titanic_clf.columns[:-1]) feat_imps.sort_values(by='importance', ascending=False, inplace=True) -
将特征重要性绘制为条形图:
feat_imps.plot(kind='bar', figsize=(10,7)) plt.legend() plt.show()输出将如下所示:

图 6.32:特征直方图
在这里,我们可以看到Sex、Fare和Pclass特征似乎具有最高的重要性,也就是说,它们对目标变量的影响最大。
活动 15:最终测试项目
在这个活动中,我们将使用IBM HR Analytics 员工流失与绩效数据集(可在www.kaggle.com/pavansubhasht/ibm-hr-analytics-attrition-dataset找到),以及相关的源代码(见github.com/TrainingByPackt/Supervised-Learning-with-Python)来解决一个分类问题,在这个问题中,我们需要预测员工是否会离职。针对员工流失问题,我们的目标是最大化召回率,即我们希望能够识别所有即将离职的员工,即使这意味着预测一些表现良好的员工也会离职:这将帮助 HR 对这些员工采取适当的措施,防止他们离开。
数据集中的每一行代表一个员工,目标变量是Attrition,它有两个值:1和0,分别表示该员工是否离职,是和否。我们将使用来自 scikit-learn 的梯度提升分类器来训练模型。这个活动是作为一个最终项目,旨在帮助巩固本书以及本章所学的概念的实际应用。
我们将通过使用交叉验证的随机搜索来找到模型的最优超参数。然后,我们将在数据集的一部分上使用梯度提升算法构建最终的分类器,并使用我们学到的分类指标评估其在数据集剩余部分的表现。我们将使用平均绝对误差作为此次活动的评估指标。
需要执行的步骤如下:
-
导入相关的库。
-
读取
attrition_train.csv数据集。 -
读取
categorical_variable_values.json文件,该文件包含了分类变量的详细信息。 -
处理数据集,将所有特征转换为数值型。
-
选择基础模型,并定义与模型对应的超参数值范围,以进行超参数调优。
-
定义初始化
RandomizedSearchCV对象的参数,并使用 K 折交叉验证来找到最佳模型超参数。 -
将数据集划分为训练集和验证集,并使用最终超参数在训练集上训练新模型。
-
计算在验证集上的预测精度、精确度和召回率,并打印混淆矩阵。
-
尝试调整不同的阈值,找到具有高召回率的最佳点。绘制精确度-召回率曲线。
-
确定用于预测测试数据集的最终阈值。
-
读取并处理测试数据集,将所有特征转换为数值型。
-
在测试数据集上预测最终值。
注意
本活动的解决方案可以在第 373 页找到。
总结
本章讨论了模型评估在监督学习中的重要性,并介绍了几种用于评估回归和分类任务的重要指标。我们看到,虽然回归模型的评估相对简单,但分类模型的性能可以通过多种方式进行衡量,具体取决于我们希望模型优先考虑的内容。除了数值指标,我们还探讨了如何绘制精确度-召回率曲线和 ROC 曲线,以更好地解读和评估模型性能。
之后,我们讨论了为什么通过计算模型在其训练数据上的预测误差来评估模型是一个不好的主意,以及如何在模型已经见过的数据上进行测试会导致模型具有高方差。通过这一点,我们引入了保持集数据集的概念,并解释了 K 折交叉验证为何是一个有用的策略,以及确保模型训练和评估过程保持无偏的采样技术。
性能改进策略的最后一节从学习曲线和验证曲线的讨论开始,探讨了如何解读这些曲线来推动模型开发过程,最终提高模型性能。随后,我们介绍了超参数调优作为提升性能的一种方法,并简要介绍了特征重要性。
第八章:附录
关于
本节内容帮助学生完成书中的活动。它包括学生为实现活动目标所需执行的详细步骤。
第一章:Python 机器学习工具包
活动 1:pandas 函数
解决方案
-
打开一个新的 Jupyter Notebook。
-
使用 pandas 加载 Titanic 数据集:
import pandas as pd df = pd.read_csv('titanic.csv')使用
head()函数查看数据集,如下:# Have a look at the first 5 sample of the data df.head()输出结果如下:
![图 1.65:前五行]()
图 1.65:前五行
使用
describe函数如下:df.describe(include='all')输出结果如下:
![图 1.66:describe()的输出]()
图 1.66:describe()的输出
-
我们不需要
Unnamed: 0列。我们可以通过以下方式删除该列,而不使用del命令:df = df[df.columns[1:]] # Use the columns df.head()输出结果如下:
![图 1.67:删除 列后的前五行]()
图 1.67:删除
Unnamed: 0列后的前五行 -
计算 DataFrame 列的均值、标准差、最小值和最大值,而不使用
describe:df.mean() Fare 33.295479 Pclass 2.294882 Age 29.881138 Parch 0.385027 SibSp 0.498854 Survived 0.383838 dtype: float64 df.std() Fare 51.758668 Pclass 0.837836 Age 14.413493 Parch 0.865560 SibSp 1.041658 Survived 0.486592 dtype: float64 df.min() Fare 0.00 Pclass 1.00 Age 0.17 Parch 0.00 SibSp 0.00 Survived 0.00 dtype: float64 df.max() Fare 512.3292 Pclass 3.0000 Age 80.0000 Parch 9.0000 SibSp 8.0000 Survived 1.0000 dtype: float64 -
33%、66% 和 99% 的四分位数如何?使用
quantile方法如下:df.quantile(0.33) Fare 8.559325 Pclass 2.000000 Age 23.000000 Parch 0.000000 SibSp 0.000000 Survived 0.000000 Name: 0.33, dtype: float64 df.quantile(0.66) Fare 26.0 Pclass 3.0 Age 34.0 Parch 0.0 SibSp 0.0 Survived 1.0 Name: 0.66, dtype: float64 df.quantile(0.99) Fare 262.375 Pclass 3.000 Age 65.000 Parch 4.000 SibSp 5.000 Survived 1.000 Name: 0.99, dtype: float64 -
每个班级的乘客有多少人?让我们来看一下,使用
groupby方法:class_groups = df.groupby('Pclass') for name, index in class_groups: print(f'Class: {name}: {len(index)}') Class: 1: 323 Class: 2: 277 Class: 3: 709 -
每个班级的乘客有多少人?你可以通过使用选择/索引方法来统计每个班级的成员数量:
for clsGrp in df.Pclass.unique(): num_class = len(df[df.Pclass == clsGrp]) print(f'Class {clsGrp}: {num_class}') Class 3: 709 Class 1: 323 Class 2: 277第 6 步 和 第 7 步 的答案是匹配的。
-
确定第三类中的最年长乘客是谁:
third_class = df.loc[(df.Pclass == 3)] third_class.loc[(third_class.Age == third_class.Age.max())]输出结果如下:
![图 1.68:第三类中的最年长乘客]()
图 1.68:第三类中的最年长乘客
-
对于许多机器学习问题,通常会将数值缩放到 0 到 1 之间。使用
agg方法与 Lambda 函数将Fare和Age列缩放到 0 到 1 之间:fare_max = df.Fare.max() age_max = df.Age.max() df.agg({ 'Fare': lambda x: x / fare_max, 'Age': lambda x: x / age_max, }).head()输出结果如下:
![图 1.69:将数值缩放到 0 到 1 之间]()
图 1.69:将数值缩放到 0 到 1 之间
-
数据集中有一个个体没有列出
Fare票价:df_nan_fare = df.loc[(df.Fare.isna())] df_nan_fare这是输出:

图 1.70:没有列出票价的个体
使用 groupby 方法,将此行的 NaN 值替换为与相同班级和 Embarked 位置对应的平均 Fare 值:
embarked_class_groups = df.groupby(['Embarked', 'Pclass'])
indices = embarked_class_groups.groups[(df_nan_fare.Embarked.values[0], df_nan_fare.Pclass.values[0])]
mean_fare = df.iloc[indices].Fare.mean()
df.loc[(df.index == 1043), 'Fare'] = mean_fare
df.iloc[1043]
输出结果如下:
Cabin NaN
Embarked S
Fare 14.4354
Pclass 3
Ticket 3701
Age 60.5
Name Storey, Mr. Thomas
Parch 0
Sex male
SibSp 0
Survived NaN
Name: 1043, dtype: object
第二章:探索性数据分析与可视化
活动 2:汇总统计与缺失值
解决方案
完成此活动的步骤如下:
-
读取数据。使用 pandas 的
.read_csv方法将 CSV 文件读取为 pandas DataFrame:data = pd.read_csv('house_prices.csv') -
使用 pandas 的
.info()和.describe()方法查看数据集的汇总统计:data.info() data.describe().Tinfo()的输出将是:![图 2.39:info() 方法的输出]()
图 2.39:info() 方法的输出
describe()方法的输出将是:![图 2.40:describe()方法的输出]()
图 2.40:describe()方法的输出
-
找出 DataFrame 中每列的缺失值总数和缺失值百分比,并按缺失值百分比降序显示至少有一个空值的列:
正如我们在练习 12:可视化缺失值中所做的那样,我们将使用
.isnull()函数在 DataFrame 上获取掩码,通过在掩码 DataFrame 上使用.sum()函数找出每列的空值数量,并通过在掩码 DataFrame 上使用.mean()函数计算空值的比例,再乘以 100 将其转换为百分比。然后,我们使用pd.concat()将空值的总数和百分比合并成一个单独的 DataFrame,并按缺失值的百分比排序行:mask = data.isnull() total = mask.sum() percent = 100*mask.mean() missing_data = pd.concat([total, percent], axis=1,join='outer', keys=['count_missing', 'perc_missing']) missing_data.sort_values(by='perc_missing', ascending=False, inplace=True) missing_data[missing_data.count_missing > 0]输出结果如下:
![图 2.41:每列缺失值的总数和百分比]()
图 2.41:每列缺失值的总数和百分比
-
绘制空值矩阵和空值相关性热图。首先,我们找出包含至少一个空值的列名列表。然后,我们使用
missingno库绘制空值矩阵(正如我们在练习 12:可视化缺失值中所做的那样),并对这些列中的数据绘制空值相关性热图(样本为 500 个点):nullable_columns = data.columns[mask.any()].tolist() msno.matrix(data[nullable_columns].sample(500)) plt.show() msno.heatmap(data[nullable_columns], figsize=(18,18)) plt.show()空值矩阵将如下所示:
![图 2.42:空值矩阵]()
图 2.42:空值矩阵
空值相关性热图将如下所示:
![图 2.43:空值相关性热图]()
图 2.43:空值相关性热图
-
删除缺失值超过 80%的列。使用我们在第 3 步中创建的 DataFrame 的
.loc操作符,只选择缺失值少于 80%的列:data = data.loc[:,missing_data[missing_data.perc_missing < 80].index] -
用 NA 值替换
FireplaceQu列中的空值。使用.fillna()方法将空值替换为NA字符串:data['FireplaceQu'] = data['FireplaceQu'].fillna('NA')
活动 3:可视化表示值的分布
解决方案
-
使用 Matplotlib 绘制目标变量
SalePrice的直方图。首先,我们使用plt.figure命令初始化图形并设置图形大小。然后,使用 Matplotlib 的.hist()函数作为主要绘图函数,将SalePrice系列对象传入以绘制直方图。最后,指定坐标轴标签并显示图形:plt.figure(figsize=(8,6)) plt.hist(data.SalePrice, bins=range(0,800000,50000)) plt.ylabel('Number of data points') plt.xlabel('SalePrice') plt.show()输出结果如下:
![图 2.44:目标变量的直方图]()
图 2.44:目标变量的直方图
-
找出每一列具有对象类型的唯一值的数量。通过在原始 DataFrame 上使用
.select_dtypes函数,选择具有numpy.object数据类型的列,创建一个名为object_variables的新 DataFrame。然后,使用.nunique()函数查找该 DataFrame 中每一列的唯一值数量,并对结果系列进行排序:object_variables = data.select_dtypes(include=[np.object]) object_variables.nunique().sort_values()输出结果将是:
![图 2.45:每一列具有对象类型的唯一值的数量]()
图 2.45:每一列具有对象类型的唯一值的数量
-
创建一个 DataFrame,表示
HouseStyle列中每个类别值的出现次数。使用.value_counts()函数计算每个值的频率,按降序以 pandas 系列的形式表示,然后重置索引,得到一个 DataFrame,并按索引对值进行排序:counts = data.HouseStyle.value_counts(dropna=False) counts.reset_index().sort_values(by='index')输出结果将是:
![图 2.46:HouseStyle 列中每个类别值的出现次数]()
图 2.46:HouseStyle 列中每个类别值的出现次数
-
绘制表示这些计数的饼图。如同 步骤 1,我们使用
plt.figure()初始化图像,并分别使用plt.title()和plt.show()方法来设置图形标题并显示图像。主要使用的绘图函数是plt.pie(),将我们在前一步创建的系列传递给它:plt.figure(figsize=(10,10)) plt.pie(counts, labels=counts.index) plt.title('Pie chart showing counts for\nHouseStyle categories') plt.show()输出结果如下:
![图 2.47:表示计数的饼图]()
图 2.47:表示计数的饼图
-
找出每一列具有数字类型的唯一值的数量。如同 步骤 2,现在选择具有
numpy.number数据类型的列,并使用.nunique()查找每一列的唯一值数量。按降序对结果系列进行排序:numeric_variables = data.select_dtypes(include=[np.number]) numeric_variables.nunique().sort_values(ascending=False)输出结果如下:
![图 2.48:每一列具有数字类型的唯一值的数量]()
图 2.48:每一列具有数字类型的唯一值的数量
-
使用 Seaborn 绘制
LotArea变量的直方图。使用 Seaborn 的.distplot()函数作为主要绘图函数,并将 DataFrame 中的LotArea系列传递给它(去除空值;使用.dropna()函数删除空值)。为了改善图形的显示,还可以设置bins参数,并使用plt.xlim()指定 X 轴的限制:plt.figure(figsize=(10,7)) sns.distplot(data.LotArea.dropna(), , bins=range(0,100000,1000)) plt.xlim(0,100000) plt.show()输出结果如下:
![图 2.49:LotArea 变量的直方图]()
图 2.49:LotArea 变量的直方图
-
计算每一列数值的偏斜值和峰度值:
data.skew().sort_values() data.kurt()偏斜值的输出结果将是:

图 2.50:每一列的偏斜值
峰度值的输出结果将是:

图 2.51:每一列的峰度值
活动 4:数据中的关系
解答
-
绘制数据集的相关性热力图。正如我们在练习 23:相关性热力图中所做的那样,使用 Seaborn 的
.heatmap()函数绘制热力图,并传递特征相关性矩阵(通过使用 pandas 的.corr()函数在数据框上确定)。此外,使用cmap参数将颜色图设置为RdBu,并通过vmin和vmax参数分别将颜色刻度的最小值和最大值设置为-1和1:plt.figure(figsize = (12,10)) sns.heatmap(data.corr(), square=True, cmap="RdBu", vmin=-1, vmax=1) plt.show()输出结果如下:
![图 2.52:数据集的热力图]()
图 2.52:数据集的热力图
-
使用以下特征子集绘制一个更紧凑的热力图,并带有相关值的注释:
feature_subset = [ 'GarageArea', 'GarageCars','GarageCond','GarageFinish','GarageQual','GarageType', 'GarageYrBlt','GrLivArea','LotArea','MasVnrArea','SalePrice' ]现在,按照上一步的方法操作,这次只选择数据集中上述列,并向主绘图函数添加一个值为
True的annot参数,其他保持不变:plt.figure(figsize = (12,10)) sns.heatmap(data[feature_subset].corr(), square=True, annot=True, cmap="RdBu", vmin=-1, vmax=1) plt.show()输出结果如下:
![图 2.53:带有相关值注释的热力图]()
图 2.53:带有相关值注释的热力图
-
显示同一特征子集的配对图,主对角线为 KDE 图,其它部分为散点图。使用 Seaborn 的
.pairplot()函数绘制数据框中选定列的非空值的配对图。要将主对角线图设置为 KDE 图,可以将kde传递给diag_kind参数,要将其它图设置为散点图,则将scatter传递给kind参数:sns.pairplot(data[feature_subset].dropna(), kind ='scatter', diag_kind='kde') plt.show()输出结果如下:
![图 2.54:同一特征子集的配对图]()
图 2.54:同一特征子集的配对图
-
创建一个箱型图,显示每个
GarageCars类别中的SalePrice变动。这里使用的主要绘图函数是 Seaborn 的.boxplot()函数,我们将数据框以及x和y参数传递给它,前者是分类变量,后者是我们希望看到在每个类别中变动的连续变量,即GarageCars和SalePrice:plt.figure(figsize=(10, 10)) sns.boxplot(x='GarageCars', y="SalePrice", data=data) plt.show()输出结果如下:
![图 2.55:显示每个类别中变动的箱型图]()
图 2.55:显示每个
GarageCars类别中SalePrice变动的箱型图 -
使用 Seaborn 绘制折线图,显示较旧和较新建公寓的
SalePrice变化。在这里,我们将使用 Seaborn 的.lineplot()函数绘制折线图。由于我们希望查看SalePrice的变化,因此将其作为y变量,而由于变化跨越一段时间,我们将YearBuilt作为x变量。记住这一点,我们将相应的系列作为值传递给主绘图函数的y和x参数。同时,我们传递ci=None参数,以隐藏图中折线周围的标准差指示器:plt.figure(figsize=(10,7)) sns.lineplot(x=data.YearBuilt, y=data.SalePrice, ci=None) plt.show()输出结果如下:

图 2.56:显示旧公寓和新建公寓的 SalePrice 变化的折线图
第三章:回归分析
活动 5:使用移动平均绘制数据
解答
-
从 CSV 文件中将数据集加载到 pandas DataFrame 中:
df = pd.read_csv('austin_weather.csv') df.head()输出结果将显示
austin_weather.csv文件的前五行:![图 3.74:奥斯汀天气数据的前五行]()
图 3.74:奥斯汀天气数据的前五行
-
由于我们只需要
Date和TempAvgF列,因此我们将从数据集中删除其他所有列:df = df[['Date', 'TempAvgF']] df.head()输出结果如下:
![图 3.75:奥斯汀天气数据的日期和 TempAvgF 列]()
图 3.75:奥斯汀天气数据的日期和 TempAvgF 列
-
最初,我们只关心第一年的数据,因此我们只需要提取该信息。在 DataFrame 中为年份创建一列,从
Date列的字符串中提取年份值作为整数,并将这些值分配给Year列。请注意,温度是按天记录的:df['Year'] = [int(dt[:4]) for dt in df.Date] df.head()输出结果如下:
![图 3.76:提取年份]()
图 3.76:提取年份
-
重复此过程以提取月份值,并将值以整数形式存储在
Month列中:df['Month'] = [int(dt[5:7]) for dt in df.Date] df.head()输出结果如下:
![图 3.77:提取月份]()
图 3.77:提取月份
-
将第一年的数据复制到 DataFrame 中:
df_first_year = df[:365] df_first_year.head()输出结果如下:
![图 3.78:复制数据到新的数据框]()
图 3.78:复制数据到新的数据框
-
计算 20 天的移动平均滤波器:
window = 20 rolling = df_first_year.TempAvgF.rolling(window).mean(); rolling.head(n=20)输出结果如下:
0 NaN 1 NaN 2 NaN 3 NaN 4 NaN 5 NaN 6 NaN 7 NaN 8 NaN 9 NaN 10 NaN 11 NaN 12 NaN 13 NaN 14 NaN 15 NaN 16 NaN 17 NaN 18 NaN 19 47.75 Name: TempAvgF, dtype: float64 -
绘制原始数据和移动平均信号,x轴为年份中的天数:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(range(1, 366), df_first_year.TempAvgF, label='Raw Data'); ax.plot(range(1, 366), rolling, c='r', label=f'{window} day moving average'); ax.set_title('Daily Mean Temperature Measurements') ax.set_xlabel('Day') ax.set_ylabel('Temperature (degF)') ax.set_xticks(range(1, 366), 10) ax.legend();输出结果如下:

图 3.79:全年温度的散点图
活动 6:使用最小二乘法进行线性回归
解答
-
可视化测量值:
df.head()输出结果如下:
![图 3.80:activity2_measurements.csv 数据集的前五行]()
图 3.80:activity2_measurements.csv 数据集的前五行
-
可视化滚动平均值:
rolling.head(n=30)输出结果如下:
![图 3.81:滚动头平均值]()
图 3.81:滚动头平均值
-
使用默认参数创建线性回归模型;即,计算模型的y截距并且不对数据进行归一化:
model = LinearRegression() model输出结果如下:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False) -
现在拟合模型,其中输入数据为年份的天数(1 到 365),输出为平均温度。为了便于后续计算,插入一列(
DayOfYear),该列与该测量的年份天数对应:df_first_year.loc[:,'DayOfYear'] = [i + 1 for i in df_first_year.index] df_first_year.head()输出结果如下:
![图 3.82: 添加年份中的日期列]()
图 3.82: 添加年份中的日期列
-
使用
DayOfYear值作为输入,df_first_year.TempAvgF作为输出,拟合模型:# Note the year values need to be provided as an N x 1 array model.fit(df_first_year.DayOfYear.values.reshape((-1, 1)), df_first_year.TempAvgF)输出将如下所示:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False) -
打印模型的参数:
print(f'm = {model.coef_[0]}') print(f'c = {model.intercept_}') print('\nModel Definition') print(f'y = {model.coef_[0]:0.4}x + {model.intercept_:0.4f}')输出将如下所示:
m = 0.04909173467448788 c = 60.28196597922625 Model Definition y = 0.04909x + 60.2820 -
我们可以通过使用线性方程中的第一个、中间和最后的值(年份中的天数)来计算趋势线值:
trend_x = np.array([ 1, 182.5, 365 ]) trend_y = model.predict(trend_x.reshape((-1, 1))) trend_y输出将如下所示:
array([60.33105771, 69.24120756, 78.20044914]) -
绘制这些值与趋势线:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_first_year.DayOfYear, df_first_year.TempAvgF, label='Raw Data'); ax.plot(df_first_year.DayOfYear, rolling, c='r', label=f'{window} day moving average'); ax.plot(trend_x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Daily Mean Temperature Measurements') ax.set_xlabel('Day') ax.set_ylabel('Temperature (degF)') ax.set_xticks(range(1, 366), 10) ax.legend();输出将如下所示:
![图 3.83: 全年温度散点图与预测趋势线]()
图 3.83: 全年温度的散点图与预测趋势线
-
评估模型的表现。模型如何拟合数据?计算 r2 分数来找出答案:
# Note the year values need to be provided as an N x 1 array r2 = model.score(df_first_year.DayOfYear.values.reshape((-1, 1)), df_first_year.TempAvgF) print(f'r2 score = {r2:0.4f}')输出将如下所示:
r2 score = 0.1222
活动 7: 虚拟变量
解决方案
-
绘制原始数据(
df)和移动平均值(rolling):fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_first_year.DayOfYear, df_first_year.TempAvgF, label='Raw Data'); ax.plot(df_first_year.DayOfYear, rolling, c='r', label=f'{window} day moving average'); ax.set_title('Daily Mean Temperature Measurements') ax.set_xlabel('Day') ax.set_ylabel('Temperature (degF)') ax.set_xticks(range(1, 366), 10) ax.legend();输出将如下所示:
![图 3.84: 全年温度的散点图]()
图 3.84: 全年温度的散点图
-
从前面的图来看,似乎在第 250 天左右有一个拐点。创建一个虚拟变量,将此特征引入线性模型:
df_first_year.loc[:,'inflection'] = [1 * int(i < 250) for i in df_first_year.DayOfYear] -
检查首尾样本,确认虚拟变量是否正确。检查前五个样本:
df_first_year.head()输出将如下所示:
![图 3.85: 前五列]()
图 3.85: 前五列
然后,检查最后五个样本:
df_first_year.tail()输出将如下所示:
![图 3.86: 最后五列]()
图 3.86: 最后五列
-
使用最小二乘法线性回归模型,将模型拟合到
DayOfYear值和虚拟变量上,以预测TempAvgF:# Note the year values need to be provided as an N x 1 array model = LinearRegression() model.fit(df_first_year[['DayOfYear', 'inflection']], df_first_year.TempAvgF)输出将如下所示:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False) -
计算 r2 分数:
# Note the year values need to be provided as an N x 1 array r2 = model.score(df_first_year[['DayOfYear', 'inflection']], df_first_year.TempAvgF) print(f'r2 score = {r2:0.4f}')输出将如下所示:
r2 score = 0.3631 -
使用
DayOfYear值,创建一组预测值,使用模型构建趋势线:trend_y = model.predict(df_first_year[['DayOfYear', 'inflection']].values) trend_y输出将如下所示:
array([51.60311133, 51.74622654, 51.88934175, 52.03245696, 52.17557217, 52.31868739, 52.4618026 , 52.60491781, 52.74803302, 52.89114823, 53.03426345, 53.17737866, 53.32049387, 53.46360908, 53.60672429, 53.7498395 , 53.89295472, 54.03606993, 54.17918514, 54.32230035, 54.46541556, 54.60853078, 54.75164599, 54.8947612 , 55.03787641, … … 73.88056649, 74.0236817 , 74.16679692, 74.30991213, 74.45302734, 74.59614255, 74.73925776, 74.88237297, 75.02548819, 75.1686034 , 75.31171861, 75.45483382, 75.59794903, 75.74106425, 75.88417946, 76.02729467, 76.17040988, 76.31352509, 76.4566403 , 76.59975552, 76.74287073, 76.88598594, 77.02910115, 77.17221636, 77.31533157]) -
绘制趋势线与数据和移动平均值的对比:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_first_year.DayOfYear, df_first_year.TempAvgF, label='Raw Data'); ax.plot(df_first_year.DayOfYear, rolling, c='r', label=f'{window} day moving average'); ax.plot(df_first_year.DayOfYear, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Daily Mean Temperature Measurements') ax.set_xlabel('Day') ax.set_ylabel('Temperature (degF)') ax.set_xticks(range(1, 366), 10) ax.legend();输出将如下所示:

图 3.87: 预测趋势线
活动 8: 其他类型的线性回归模型
解决方案
-
使用正弦曲线函数作为模型的基础:
# Using a sine curve df_first_year['DayOfYear2'] = np.sin(df_first_year['DayOfYear'] / df_first_year['DayOfYear'].max()) df_first_year.head()输出将如下所示:
![图 3.88: 前五行]()
图 3.88: 前五行
-
拟合模型:
# Note the year values need to be provided as an N x 1 array model = LinearRegression() model.fit(df_first_year[['DayOfYear2', 'DayOfYear']], df_first_year.TempAvgF)输出将如下所示:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False) -
打印模型的参数:
print(f'a = {model.coef_[0]}') print(f'm = {model.coef_[1]}') print(f'c = {model.intercept_}') print('\nModel Definition') print(f'y = {model.coef_[0]:0.4}x² + {model.coef_[1]:0.4}x + {model.intercept_:0.4f}')输出将如下所示:
a = 634.322313570282 m = -1.4371290614190075 c = 39.93286585807408 Model Definition y = 634.3x² + -1.437x + 39.9329 -
计算 r2 值来衡量模型的表现:
# Note the year values need to be provided as an N x 1 array r2 = model.score(df_first_year[['DayOfYear2', 'DayOfYear']], df_first_year.TempAvgF) print(f'r2 score = {r2:0.4f}')输出将如下所示:
r2 score = 0.7047 -
构建趋势线值:
trend_y = model.predict(df_first_year[['DayOfYear2', 'DayOfYear']].values) trend_y输出将如下所示:
array([40.23360397, 40.53432905, 40.83502803, 41.13568788, 41.43629555, 41.736838 , 42.03730219, 42.33767507, 42.6379436 , 42.93809474, 43.23811546, 43.5379927 , 43.83771344, 44.13726463, 44.43663324, 44.73580624, 45.03477059, 45.33351327, 45.63202123, 45.93028146, 46.22828093, 46.52600661, 46.82344549, 47.12058453, 47.41741073, … … 59.96306563, 59.55705293, 59.14720371, 58.73351024, 58.31596484, 57.89455987, 57.46928769, 57.04014072, 56.60711138, 56.17019215, 55.7293755 , 55.28465397, 54.83602011, 54.38346649, 53.92698572, 53.46657045, 53.00221334, 52.53390709, 52.06164442, 51.58541811, 51.10522093, 50.62104569, 50.13288526, 49.6407325 , 49.14458033]) -
绘制趋势线与原始数据和移动平均值:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_first_year.DayOfYear, df_first_year.TempAvgF, label='Raw Data'); ax.plot(df_first_year.DayOfYear, rolling, c='r', label=f'{window} day moving average'); ax.plot(df_first_year.DayOfYear, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Daily Mean Temperature Measurements') ax.set_xlabel('Day') ax.set_ylabel('Temperature (degF)') ax.set_xticks(range(1, 366), 10) ax.legend();输出将如下所示:

图 3.89: 预测趋势线
活动 9: 梯度下降法
解决方案
-
创建一个通用的梯度下降模型,并将年份中的日期值归一化为 0 到 1 之间:
grad_model = SGDRegressor(max_iter=None, tol=1e-3) _x = df_first_year.DayOfYear / df_first_year.DayOfYear.max() -
拟合模型:
grad_model.fit(_x.values.reshape((-1, 1)), df_first_year.TempAvgF)输出将如下所示:
SGDRegressor(alpha=0.0001, average=False, early_stopping=False, epsilon=0.1, eta0=0.01, fit_intercept=True, l1_ratio=0.15, learning_rate='invscaling', loss='squared_loss', max_iter=None, n_iter=None, n_iter_no_change=5, penalty='l2', power_t=0.25, random_state=None, shuffle=True, tol=None, validation_fraction=0.1, verbose=0, warm_start=False) -
打印模型的详细信息:
print(f'm = {grad_model.coef_[0]}') print(f'c = {grad_model.intercept_[0]}') print('\nModel Definition') print(f'y = {grad_model.coef_[0]:0.4}x + {grad_model.intercept_[0]:0.4f}')输出将如下所示:
m = 26.406162532140563 c = 55.07470859678077 Model Definition y = 26.41x + 55.0747 -
准备 x (
_trend_x) 趋势线值,通过将其除以最大值。使用梯度下降模型预测y_trend_values:_trend_x = trend_x / trend_x.max() trend_y = grad_model.predict(_trend_x.reshape((-1, 1))) trend_y输出将如下所示:
array([55.14705425, 68.27778986, 81.48087113]) -
绘制数据和带趋势线的移动平均图:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_first_year.DayOfYear, df_first_year.TempAvgF, label='Raw Data'); ax.plot(df_first_year.DayOfYear, rolling, c='r', label=f'{window} day moving average'); ax.plot(trend_x, trend_y, c='k', linestyle='--', label='Model: Predicted trendline') ax.set_title('Daily Mean Temperature Measurements') ax.set_xlabel('Day') ax.set_ylabel('Temperature (degF)') ax.set_xticks(range(1, 366), 10) ax.legend();输出将如下所示:

图 3.90:梯度下降预测的趋势线
活动 10:自回归模型
解决方案
-
绘制完整的平均温度值(
df.TempAvgF)图,x 轴为年份:plt.figure(figsize=(10, 7)) plt.plot(df.TempAvgF.values); yrs = [yr for yr in df.Year.unique()] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('Austin Texas Average Daily Temperature'); plt.xlabel('Year'); plt.ylabel('Temperature (F)');输出将是:
![图 3.91:通过年份的温度变化图]()
图 3.91:通过年份的温度变化图
-
创建一个 20 天滞后,并将滞后数据绘制到原始数据集上:
plt.figure(figsize=(10, 7)) plt.plot(df.TempAvgF.values, label='Original Dataset'); plt.plot(df.TempAvgF.shift(20), c='r', linestyle='--', label='Lag 20'); yrs = [yr for yr in df.Year.unique()] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('Austin Texas Average Daily Temperature'); plt.xlabel('Year'); plt.ylabel('Temperature (F)'); plt.legend();输出将是:
![图 3.92:带有 20 天滞后的温度变化图]()
图 3.92:带有 20 天滞后的温度变化图
-
构建自相关图,查看是否可以使用自回归模型来预测平均温度。对于自回归模型,在哪些地方滞后是可接受的,在哪些地方滞后不可接受?
plt.figure(figsize=(10, 7)) pd.plotting.autocorrelation_plot(df.TempAvgF);我们将得到以下输出:
![图 3.93:自相关与滞后关系图]()
图 3.93:自相关与滞后关系图
只有当自相关线超出 99% 置信区间(由虚线表示)时,滞后才是可接受的。
-
选择一个可接受滞后和不可接受滞后,并使用这些值构建滞后图示:
plt.figure(figsize=(10,7)) ax = pd.plotting.lag_plot(df.TempAvgF, lag=5);我们将得到以下输出:
![图 3.94:可接受滞后的图示]()
图 3.94:可接受滞后的图示
使用这些值来表示不可接受的滞后:
plt.figure(figsize=(10,7)) ax = pd.plotting.lag_plot(df.TempAvgF, lag=1000);我们将得到以下输出:
![图 3.95:不可接受滞后的图示]()
图 3.95:不可接受滞后的图示
-
创建一个自回归模型,注意选择的滞后值,计算 R2 值,并将自回归模型与原始图一起绘制。该模型用于预测超出可用数据的 1,000 个样本:
from statsmodels.tsa.ar_model import AR model = AR(df.TempAvgF) -
将模型拟合到数据:
model_fit = model.fit() print('Lag: %s' % model_fit.k_ar) print('Coefficients: %s' % model_fit.params)输出将是:
Lag: 23 Coefficients: const 1.909395 L1.TempAvgF 0.912076 L2.TempAvgF -0.334043 L3.TempAvgF 0.157353 L4.TempAvgF 0.025721 L5.TempAvgF 0.041342 L6.TempAvgF 0.030831 L7.TempAvgF -0.021230 L8.TempAvgF 0.020324 L9.TempAvgF 0.025147 L10.TempAvgF 0.059739 L11.TempAvgF -0.017337 L12.TempAvgF 0.043553 L13.TempAvgF -0.027795 L14.TempAvgF 0.053547 L15.TempAvgF 0.013070 L16.TempAvgF -0.033157 L17.TempAvgF -0.000072 L18.TempAvgF -0.026307 L19.TempAvgF 0.025258 L20.TempAvgF 0.038341 L21.TempAvgF 0.007885 L22.TempAvgF -0.008889 L23.TempAvgF -0.011080 dtype: float64 -
创建一个预测集,预测最后一个样本之后的 1,000 天:
predictions = model_fit.predict(start=model_fit.k_ar, end=len(df) + 1000) predictions[:10].values输出将是:
array([54.81171857, 56.89097085, 56.41891585, 50.98627626, 56.11843512, 53.20665111, 55.13941554, 58.4679288 , 61.92497136, 49.46049801]) -
绘制预测图以及原始数据集:
plt.figure(figsize=(10, 7)) plt.plot(df.TempAvgF.values, label='Original Dataset'); plt.plot(predictions, c='g', linestyle=':', label='Predictions'); yrs = [yr for yr in df.Year.unique()] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('Austin Texas Average Daily Temperature'); plt.xlabel('Year'); plt.ylabel('Temperature (F)'); plt.legend();输出将是:
![图 3.96:通过年份的温度变化图]()
图 3.96:通过年份的温度变化图
-
通过显示第 100 到第 200 个样本,增强视图以查看差异:
plt.figure(figsize=(10, 7)) plt.plot(df.TempAvgF.values, label='Original Dataset'); plt.plot(predictions, c='g', linestyle=':', label='Predictions'); yrs = [yr for yr in df.Year.unique()] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('Austin Texas Average Daily Temperature'); plt.xlabel('Year'); plt.ylabel('Temperature (F)'); plt.xlim([100, 200]) plt.legend();我们将得到以下输出:

图 3.97:带有原始数据集的预测图
第四章:分类
活动 11:线性回归分类器 - 二类分类器
解决方案
-
导入所需的依赖项:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.linear_model import LinearRegression -
将 MNIST 数据加载到内存中:
with gzip.open('train-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('train-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels = np.array(array("B", f.read())) with gzip.open('t10k-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img_test = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('t10k-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels_test = np.array(array("B", f.read())) -
可视化数据的一个样本:
for i in range(10): plt.subplot(2, 5, i + 1) plt.imshow(img[i], cmap='gray'); plt.title(f'{labels[i]}'); plt.axis('off')我们将得到以下输出:
![图 4.76:样本数据]()
图 4.76:样本数据
-
构建一个线性分类器模型来分类数字 0 和 1。我们将要创建的模型是确定样本是否为数字 0 或 1。为此,我们首先需要仅选择这些样本:
samples_0_1 = np.where((labels == 0) | (labels == 1))[0] images_0_1 = img[samples_0_1] labels_0_1 = labels[samples_0_1] samples_0_1_test = np.where((labels_test == 0) | (labels_test == 1)) images_0_1_test = img_test[samples_0_1_test].reshape((-1, rows * cols)) labels_0_1_test = labels_test[samples_0_1_test] -
可视化选择的信息。以下是零的代码:
sample_0 = np.where((labels == 0))[0][0] plt.imshow(img[sample_0], cmap='gray');输出将如下所示:
![图 4.77:第一个样本数据]()
图 4.77:第一个样本数据
这是零的代码:
sample_1 = np.where((labels == 1))[0][0] plt.imshow(img[sample_1], cmap='gray');输出将为:
![图 4.78:第二个样本数据]()
图 4.78:第二个样本数据
-
为了将图像信息提供给模型,我们必须首先将数据展平,使得每个图像变为 1 x 784 像素的形状:
images_0_1 = images_0_1.reshape((-1, rows * cols)) images_0_1.shape输出将为:
(12665, 784) -
让我们构建模型;使用
LinearRegressionAPI 并调用fit函数:model = LinearRegression() model.fit(X=images_0_1, y=labels_0_1)输出将为:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False) -
确定训练集的 R2 评分:
model.score(X=images_0_1, y=labels_0_1)输出将为:
0.9705320567708795 -
使用 0.5 的阈值来确定每个训练样本的标签预测。大于 0.5 的值分类为一;小于或等于 0.5 的值分类为零:
y_pred = model.predict(images_0_1) > 0.5 y_pred = y_pred.astype(int) y_pred输出将为:
array([0, 1, 1, ..., 1, 0, 1]) -
计算预测的训练值与真实值之间的分类准确性:
np.sum(y_pred == labels_0_1) / len(labels_0_1)输出将为:
0.9947887879984209 -
与测试集的性能进行比较:
y_pred = model.predict(images_0_1_test) > 0.5 y_pred = y_pred.astype(int) np.sum(y_pred == labels_0_1_test) / len(labels_0_1_test)输出将为:
0.9938534278959811
活动 12:使用逻辑回归进行鸢尾花分类
解决方案
-
导入所需的包。对于此活动,我们需要使用 pandas 包加载数据,Matplotlib 包绘图,以及 scikit-learn 创建逻辑回归模型。导入所有必要的包和相关模块:
import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression -
使用 pandas 加载鸢尾花数据集并检查前五行:
df = pd.read_csv('iris-data.csv') df.head()输出将为:
![图 4.79:鸢尾花数据集的前五行]()
图 4.79:鸢尾花数据集的前五行
-
下一步是特征工程。我们需要选择最合适的特征,以便提供最强大的分类模型。绘制多个不同特征与分配的物种分类之间的关系,例如,萼片长度与花瓣长度及物种。目视检查图表,寻找任何可能表示物种间分离的模式:
markers = { 'Iris-setosa': {'marker': 'x'}, 'Iris-versicolor': {'marker': '*'}, 'Iris-virginica': {'marker': 'o'}, } plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): plt.scatter(group['Sepal Width'], group['Petal Length'], label=name, marker=markers[name]['marker'], ) plt.title('Species Classification Sepal Width vs Petal Length'); plt.xlabel('Sepal Width (mm)'); plt.ylabel('Petal Length (mm)'); plt.legend();输出将为:
![图 4.80:物种分类图]()
图 4.80:物种分类图
-
通过在以下列表中写入列名来选择特征:
selected_features = [ 'Sepal Width', # List features here 'Petal Length' ] -
在构建模型之前,我们必须先将
species值转换为模型中可以使用的标签。将Iris-setosa物种字符串替换为值0,将Iris-versicolor物种字符串替换为值1,将Iris-virginica物种字符串替换为值2:species = [ 'Iris-setosa', # 0 'Iris-versicolor', # 1 'Iris-virginica', # 2 ] output = [species.index(spec) for spec in df.Species] -
使用
selected_features和分配的species标签创建模型:model = LogisticRegression(multi_class='auto', solver='lbfgs') model.fit(df[selected_features], output)输出将为:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='auto', n_jobs=None, penalty='l2', random_state=None, solver='lbfgs', tol=0.0001, verbose=0, warm_start=False) -
计算模型在训练集上的准确性:
model.score(df[selected_features], output)输出将为:
0.9533333333333334 -
使用你第二选择的
selected_features构建另一个模型,并比较其性能:selected_features = [ 'Sepal Length', # List features here 'Petal Width' ] model.fit(df[selected_features], output) model.score(df[selected_features], output)输出将如下:
0.96 -
使用所有可用信息构建另一个模型并比较其性能:
selected_features = [ 'Sepal Length', # List features here 'Sepal Width' ] model.fit(df[selected_features], output) model.score(df[selected_features], output)输出将如下:
0.82
活动 13: K-NN 多分类器
解决方案
-
导入以下包:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.neighbors import KNeighborsClassifier as KNN -
将 MNIST 数据加载到内存中。
训练图像:
with gzip.open('train-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img = np.array(array("B", f.read())).reshape((size, rows, cols))训练标签:
with gzip.open('train-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels = np.array(array("B", f.read()))测试图像:
with gzip.open('t10k-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img_test = np.array(array("B", f.read())).reshape((size, rows, cols))测试标签:
with gzip.open('t10k-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels_test = np.array(array("B", f.read())) -
可视化数据样本:
for i in range(10): plt.subplot(2, 5, i + 1) plt.imshow(img[i], cmap='gray'); plt.title(f'{labels[i]}'); plt.axis('off')输出将如下:
![图 4.81: 示例图像]()
图 4.81: 示例图像
-
构建一个 K-NN 分类器,使用三个最近邻来分类 MNIST 数据集。同样,为了节省处理能力,我们随机抽取 5,000 张图像用于训练:
selection = np.random.choice(len(img), 5000) selected_images = img[selection] selected_labels = labels[selection] -
为了将图像信息提供给模型,我们必须先将数据展平,使得每个图像的形状为 1 x 784 像素:
selected_images = selected_images.reshape((-1, rows * cols)) selected_images.shape输出将如下:
(5000, 784) -
构建三邻近 KNN 模型并将数据拟合到模型中。请注意,在本活动中,我们为模型提供的是 784 个特征或维度,而不仅仅是 2:
model = KNN(n_neighbors=3) model.fit(X=selected_images, y=selected_labels)输出将如下:
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=None, n_neighbors=3, p=2, weights='uniform') -
确定与训练集的得分:
model.score(X=selected_images, y=selected_labels)输出将如下所示:
0.9692 -
显示模型对训练数据的前两个预测:
model.predict(selected_images)[:2] plt.subplot(1, 2, 1) plt.imshow(selected_images[0].reshape((28, 28)), cmap='gray'); plt.axis('off'); plt.subplot(1, 2, 2) plt.imshow(selected_images[1].reshape((28, 28)), cmap='gray'); plt.axis('off');输出将如下:
![图 4.82: 第一个预测值]()
图 4.82: 第一个预测值
-
与测试集的表现进行比较:
model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)输出将如下:
0.9376
第五章: 集成模型
活动 14: 使用独立和集成算法进行堆叠
解决方案
-
导入相关库:
import pandas as pd import numpy as np import seaborn as sns %matplotlib inline import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error from sklearn.model_selection import KFold from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor from sklearn.neighbors import KNeighborsRegressor from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor -
读取数据并打印前五行:
data = pd.read_csv('house_prices.csv') data.head()输出将如下:
![图 5.19: 前五行]()
图 5.19: 前五行
-
对数据集进行预处理,移除空值并对类别变量进行独热编码,以准备建模数据。
首先,我们删除所有超过 10% 值为空的列。为此,使用
.isnull()方法计算缺失值的比例,得到一个掩码 DataFrame,并使用.mean()方法计算每一列的空值比例。将结果乘以 100,得到百分比形式的系列。然后,找到该系列中百分比值低于 10 的子集,并将索引(这将给我们列名)保存为列表。打印该列表以查看我们得到的列:
perc_missing = data.isnull().mean()*100 cols = perc_missing[perc_missing < 10].index.tolist() cols输出将如下:
![图 5.20: 数据集预处理的输出]()
图 5.20: 数据集预处理的输出
由于第一列是
id,我们也将排除这一列,因为它对模型没有任何帮助。我们将从数据中提取所有在
col列表中的列,除了第一个元素id:data = data.loc[:, cols[1:]]对于类别变量,我们用字符串
NA替换空值,并使用 pandas 的.get_dummies()方法进行独热编码,而对于数值变量,我们将空值替换为-1。然后,我们将数值列和类别列合并,得到最终的数据框:data_obj = pd.get_dummies(data.select_dtypes(include=[np.object]).fillna('NA')) data_num = data.select_dtypes(include=[np.number]).fillna(-1) data_final = pd.concat([data_obj, data_num], axis=1) -
将数据集划分为训练和验证 DataFrame。
我们使用 scikit-learn 的
train_test_split()方法将最终的 DataFrame 按 4:1 的比例划分为训练集和验证集。然后,我们将每个数据集进一步拆分为各自的x和y值,分别表示特征和目标变量:train, val = train, val = train_test_split(data_final, test_size=0.2, random_state=11) x_train = train.drop(columns=['SalePrice']) y_train = train['SalePrice'].values x_val = val.drop(columns=['SalePrice']) y_val = val['SalePrice'].values -
初始化字典,用于存储训练和验证的 MAE 值。我们将创建两个字典,用于存储训练和验证数据集上的 MAE 值:
train_mae_values, val_mae_values = {}, {} -
训练一个决策树模型并保存分数。我们将使用 scikit-learn 的
DecisionTreeRegressor类来训练一个回归模型,使用单棵决策树:# Decision Tree dt_params = { 'criterion': 'mae', 'min_samples_leaf': 10, 'random_state': 11 } dt = DecisionTreeRegressor(**dt_params) dt.fit(x_train, y_train) dt_preds_train = dt.predict(x_train) dt_preds_val = dt.predict(x_val) train_mae_values['dt'] = mean_absolute_error(y_true=y_train, y_pred=dt_preds_train) val_mae_values['dt'] = mean_absolute_error(y_true=y_val, y_pred=dt_preds_val) -
训练一个 k 最近邻模型并保存分数。我们将使用 scikit-learn 的
kNeighborsRegressor类来训练一个回归模型,k=5:# k-Nearest Neighbors knn_params = { 'n_neighbors': 5 } knn = KNeighborsRegressor(**knn_params) knn.fit(x_train, y_train) knn_preds_train = knn.predict(x_train) knn_preds_val = knn.predict(x_val) train_mae_values['knn'] = mean_absolute_error(y_true=y_train, y_pred=knn_preds_train) val_mae_values['knn'] = mean_absolute_error(y_true=y_val, y_pred=knn_preds_val) -
训练一个随机森林模型并保存分数。我们将使用 scikit-learn 的
RandomForestRegressor类来训练一个回归模型,采用自助法(bagging):# Random Forest rf_params = { 'n_estimators': 50, 'criterion': 'mae', 'max_features': 'sqrt', 'min_samples_leaf': 10, 'random_state': 11, 'n_jobs': -1 } rf = RandomForestRegressor(**rf_params) rf.fit(x_train, y_train) rf_preds_train = rf.predict(x_train) rf_preds_val = rf.predict(x_val) train_mae_values['rf'] = mean_absolute_error(y_true=y_train, y_pred=rf_preds_train) val_mae_values['rf'] = mean_absolute_error(y_true=y_val, y_pred=rf_preds_val) -
训练一个梯度提升模型并保存分数。我们将使用 scikit-learn 的
GradientBoostingRegressor类来训练一个增强回归模型:# Gradient Boosting gbr_params = { 'n_estimators': 50, 'criterion': 'mae', 'max_features': 'sqrt', 'max_depth': 3, 'min_samples_leaf': 5, 'random_state': 11 } gbr = GradientBoostingRegressor(**gbr_params) gbr.fit(x_train, y_train) gbr_preds_train = gbr.predict(x_train) gbr_preds_val = gbr.predict(x_val) train_mae_values['gbr'] = mean_absolute_error(y_true=y_train, y_pred=gbr_preds_train) val_mae_values['gbr'] = mean_absolute_error(y_true=y_val, y_pred=gbr_preds_val) -
使用在之前步骤中使用的相同超参数准备训练和验证数据集,其中包含四个元估计器。我们将创建一个
num_base_predictors变量,表示堆叠模型中基估计器的数量,以帮助计算训练和验证数据集的形状。这个步骤几乎可以像本章的练习一样编写代码,只不过基估计器的数量(和类型)不同。 -
首先,我们创建一个新的训练集,增加来自基估计器的预测列,方法与之前相同:
num_base_predictors = len(train_mae_values) # 4 x_train_with_metapreds = np.zeros((x_train.shape[0], x_train.shape[1]+num_base_predictors)) x_train_with_metapreds[:, :-num_base_predictors] = x_train x_train_with_metapreds[:, -num_base_predictors:] = -1然后,我们使用 k 折交叉验证策略训练基模型。在每次迭代中,我们将预测值保存在列表中,并遍历列表,将预测值分配到该折中的列:
kf = KFold(n_splits=5, random_state=11) for train_indices, val_indices in kf.split(x_train): kfold_x_train, kfold_x_val = x_train.iloc[train_indices], x_train.iloc[val_indices] kfold_y_train, kfold_y_val = y_train[train_indices], y_train[val_indices] predictions = [] dt = DecisionTreeRegressor(**dt_params) dt.fit(kfold_x_train, kfold_y_train) predictions.append(dt.predict(kfold_x_val)) knn = KNeighborsRegressor(**knn_params) knn.fit(kfold_x_train, kfold_y_train) predictions.append(knn.predict(kfold_x_val)) gbr = GradientBoostingRegressor(**gbr_params) rf.fit(kfold_x_train, kfold_y_train) predictions.append(rf.predict(kfold_x_val)) gbr = GradientBoostingRegressor(**gbr_params) gbr.fit(kfold_x_train, kfold_y_train) predictions.append(gbr.predict(kfold_x_val)) for i, preds in enumerate(predictions): x_train_with_metapreds[val_indices, -(i+1)] = preds之后,我们创建一个新的验证集,增加来自基估计器的预测列:
x_val_with_metapreds = np.zeros((x_val.shape[0], x_val.shape[1]+num_base_predictors)) x_val_with_metapreds[:, :-num_base_predictors] = x_val x_val_with_metapreds[:, -num_base_predictors:] = -1 -
最后,我们在完整的训练集上拟合基模型,以获取验证集的元特征:
predictions = [] dt = DecisionTreeRegressor(**dt_params) dt.fit(x_train, y_train) predictions.append(dt.predict(x_val)) knn = KNeighborsRegressor(**knn_params) knn.fit(x_train, y_train) predictions.append(knn.predict(x_val)) gbr = GradientBoostingRegressor(**gbr_params) rf.fit(x_train, y_train) predictions.append(rf.predict(x_val)) gbr = GradientBoostingRegressor(**gbr_params) gbr.fit(x_train, y_train) predictions.append(gbr.predict(x_val)) for i, preds in enumerate(predictions): x_val_with_metapreds[:, -(i+1)] = preds -
训练一个线性回归模型作为堆叠模型。为了训练堆叠模型,我们在训练数据集的所有列上训练逻辑回归模型,外加基估计器的元预测。然后,我们使用最终预测值来计算 MAE 值,并将其存储在相同的
train_mae_values和val_mae_values字典中:lr = LinearRegression(normalize=False) lr.fit(x_train_with_metapreds, y_train) lr_preds_train = lr.predict(x_train_with_metapreds) lr_preds_val = lr.predict(x_val_with_metapreds) train_mae_values['lr'] = mean_absolute_error(y_true=y_train, y_pred=lr_preds_train) val_mae_values['lr'] = mean_absolute_error(y_true=y_val, y_pred=lr_preds_val) -
可视化每个独立模型和堆叠模型的训练和验证误差。然后,我们将字典转换为两个系列,并将它们组合成一个 Pandas DataFrame 的两列:
mae_scores = pd.concat([pd.Series(train_mae_values, name='train'), pd.Series(val_mae_values, name='val')], axis=1) mae_scores输出将如下所示:
![图 5.21:每个独立模型和堆叠模型的训练和验证误差]()
图 5.21:每个独立模型和堆叠模型的训练和验证误差
-
然后,我们从这个 DataFrame 绘制条形图,使用每个模型可视化训练集和验证集的 MAE 值:
mae_scores.plot(kind='bar', figsize=(10,7)) plt.ylabel('MAE') plt.xlabel('Model') plt.show()输出将如下所示:

图 5.22:可视化 MAE 值的条形图
正如我们在图中所看到的,线性回归堆叠模型在训练和验证数据集上都具有最低的平均绝对误差值,甚至低于其他集成模型(随机森林和梯度增强回归器)。
第六章:模型评估
活动 15:最终测试项目
解决方案
-
导入相关的库:
import pandas as pd import numpy as np import json %matplotlib inline import matplotlib.pyplot as plt from sklearn.preprocessing import OneHotEncoder from sklearn.model_selection import RandomizedSearchCV, train_test_split from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import (accuracy_score, precision_score, recall_score, confusion_matrix, precision_recall_curve) -
读取
attrition_train.csv数据集。将 CSV 文件读取到 DataFrame 中,并打印 DataFrame 的.info():data = pd.read_csv('attrition_train.csv') data.info()输出将如下所示:
![图 6.33:info() 输出]()
图 6.33:info() 输出
-
读取包含分类变量详情的 JSON 文件。该 JSON 文件包含一个字典,字典的键是分类特征的列名,值是该特征的类别列表。这个文件将帮助我们将分类特征进行独热编码,转换为数值特征。使用
json库加载文件对象到字典中,并打印该字典:with open('categorical_variable_values.json', 'r') as f: cat_values_dict = json.load(f) cat_values_dict输出将如下所示:
![图 6.34:JSON 文件]()
图 6.34:JSON 文件
-
处理数据集,将所有特征转换为数值。首先,找出那些保持原始形式的列(即数值特征)和需要进行独热编码的列(即分类特征)。
data.shape[1]给我们提供了data的列数,我们从中减去len(cat_values_dict),就能得到数值列的数量。要找出分类列的数量,我们只需从cat_values_dict字典中统计所有分类变量的类别总数:num_orig_cols = data.shape[1] - len(cat_values_dict) num_enc_cols = sum([len(cats) for cats in cat_values_dict.values()]) print(num_orig_cols, num_enc_cols)输出将是:
26 24创建一个全零的 NumPy 数组作为占位符,其形状等于先前确定的总列数减去 1(因为
Attrition目标变量也包含在内)。对于数值列,我们接着创建一个掩码,从 DataFrame 中选择数值列,并将它们赋值给数组X中的前num_orig_cols-1列:X = np.zeros(shape=(data.shape[0], num_orig_cols+num_enc_cols-1)) mask = [(each not in cat_values_dict and each != 'Attrition') for each in data.columns] X[:, :num_orig_cols-1] = data.loc[:, data.columns[mask]]接下来,我们从 scikit-learn 初始化
OneHotEncoder类,传入一个包含每个分类列的值列表的列表。然后,我们将分类列转换为独热编码列,并将它们赋值给X中剩余的列,并将目标变量的值保存在y变量中:cat_cols = list(cat_values_dict.keys()) cat_values = [cat_values_dict[col] for col in data[cat_cols].columns] ohe = OneHotEncoder(categories=cat_values, sparse=False, ) X[:, num_orig_cols-1:] = ohe.fit_transform(X=data[cat_cols]) y = data.Attrition.values print(X.shape) print(y.shape)输出将是:
(1176, 49) (1176,) -
选择一个基础模型,并定义要在超参数调优时搜索的超参数值范围。我们选择使用梯度增强分类器作为模型。然后,我们定义一个字典形式的所有超参数的值范围,以便进行调优:
meta_gbc = GradientBoostingClassifier() param_dist = { 'n_estimators': list(range(10, 210, 10)), 'criterion': ['mae', 'mse'], 'max_features': ['sqrt', 'log2', 0.25, 0.3, 0.5, 0.8, None], 'max_depth': list(range(1, 10)), 'min_samples_leaf': list(range(1, 10)) } -
定义初始化
RandomizedSearchCV对象的参数,并使用 K 折交叉验证来寻找最佳模型超参数。定义随机搜索所需的参数,包括cv为5,表示通过 5 折交叉验证来选择超参数。然后,初始化RandomizedSearchCV对象并使用.fit()方法开始优化:rand_search_params = { 'param_distributions': param_dist, 'scoring': 'accuracy', 'n_iter': 100, 'cv': 5, 'return_train_score': True, 'n_jobs': -1, 'random_state': 11 } random_search = RandomizedSearchCV(meta_gbc, **rand_search_params) random_search.fit(X, y)输出将如下所示:
![图 6.35:优化过程的输出]()
图 6.35:优化过程的输出
一旦调优完成,找到获得最高均值测试分数的位置(迭代次数)。找出相应的超参数并将其保存到字典中:
idx = np.argmax(random_search.cv_results_['mean_test_score']) final_params = random_search.cv_results_['params'][idx] final_params输出将如下所示:
![图 6.36:超参数字典]()
图 6.36:超参数字典
-
将数据集拆分为训练集和验证集,并使用最终超参数在训练数据集上训练一个新模型。使用 scikit-learn 的
train_test_split()方法将X和y拆分为训练集和测试集,其中测试集占数据集的 15%:train_X, val_X, train_y, val_y = train_test_split(X, y, test_size=0.15, random_state=11) print(train_X.shape, train_y.shape, val_X.shape, val_y.shape)输出将如下所示:
((999, 49), (999,), (177, 49), (177,))使用最终超参数训练梯度提升分类模型,并对训练集和验证集进行预测。还计算验证集上的概率:
gbc = GradientBoostingClassifier(**final_params) gbc.fit(train_X, train_y) preds_train = gbc.predict(train_X) preds_val = gbc.predict(val_X) pred_probs_val = np.array([each[1] for each in gbc.predict_proba(val_X)]) -
计算验证集上的准确度、精度和召回率,并打印混淆矩阵:
print('train accuracy_score = {}'.format(accuracy_score(y_true=train_y, y_pred=preds_train))) print('validation accuracy_score = {}'.format(accuracy_score(y_true=val_y, y_pred=preds_val))) print('confusion_matrix: \n{}'.format(confusion_matrix(y_true=val_y, y_pred=preds_val))) print('precision_score = {}'.format(precision_score(y_true=val_y, y_pred=preds_val))) print('recall_score = {}'.format(recall_score(y_true=val_y, y_pred=preds_val)))输出将如下所示:
![图 6.37:准确度、精度、召回率和混淆矩阵]()
图 6.37:准确度、精度、召回率和混淆矩阵
-
尝试不同的阈值以找到最佳的高召回率点。
绘制精度-召回曲线:
plt.figure(figsize=(10,7)) precision, recall, thresholds = precision_recall_curve(val_y, pred_probs_val) plt.plot(recall, precision) plt.xlabel('Recall') plt.ylabel('Precision') plt.show()输出将如下所示:
![图 6.38:精度-召回曲线]()
图 6.38:精度-召回曲线
绘制精度和召回率随着阈值增加的变化:
PR_variation_df = pd.DataFrame({'precision': precision, 'recall': recall}, index=list(thresholds)+[1]) PR_variation_df.plot(figsize=(10,7)) plt.xlabel('Threshold') plt.ylabel('P/R values') plt.show()输出将如下所示:
![图 6.39:精度和召回率随着阈值增加的变化]()
图 6.39:精度和召回率随着阈值增加的变化
-
确定一个最终的阈值,用于在测试数据集上进行预测。我们确定一个值,例如 0.3。这个值完全依赖于你在前一步的探索中认为最优的结果:
final_threshold = 0.3 -
读取并处理测试数据集,将所有特征转换为数值。这将以类似于步骤 4的方式进行,唯一的区别是我们不需要考虑目标变量列,因为数据集中不包含它:
test = pd.read_csv('attrition_test.csv') test.info() num_orig_cols = test.shape[1] - len(cat_values_dict) num_enc_cols = sum([len(cats) for cats in cat_values_dict.values()]) print(num_orig_cols, num_enc_cols) test_X = np.zeros(shape=(test.shape[0], num_orig_cols+num_enc_cols)) mask = [(each not in cat_values_dict) for each in test.columns] test_X[:, :num_orig_cols] = test.loc[:, test.columns[mask]] cat_cols = list(cat_values_dict.keys()) cat_values = [cat_values_dict[col] for col in test[cat_cols].columns] ohe = OneHotEncoder(categories=cat_values, sparse=False, ) test_X[:, num_orig_cols:] = ohe.fit_transform(X=test[cat_cols]) print(test_X.shape) -
在测试数据集上预测最终值并保存到文件。使用在步骤 10中确定的最终阈值来找到训练集中的每个值的类别。然后,将最终预测写入
final_predictions.csv文件:pred_probs_test = np.array([each[1] for each in gbc.predict_proba(test_X)]) preds_test = (pred_probs_test > final_threshold).astype(int) with open('final_predictions.csv', 'w') as f: f.writelines([str(val)+'\n' for val in preds_test])输出将是一个 CSV 文件,如下所示:

图 6.40:CSV 文件


















































































的初始随机猜测开始。
的误差。
的当前值,并减去
,也称为学习率。
:


















































































































































浙公网安备 33010602011771号