XGBoost-和-Sklearn-梯度提升实用指南-全-

XGBoost 和 Sklearn 梯度提升实用指南(全)

原文:annas-archive.org/md5/8042b1d609c03cc86db1c68794ab294c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

XGBoost 是一个行业验证的开源软件库,提供一个梯度提升框架,能够快速高效地扩展数十亿个数据点。

本书介绍了机器学习和 XGBoost 在 scikit-learn 中的应用,先通过线性回归和逻辑回归引入,随后讲解梯度提升背后的理论。你将学习决策树,并在机器学习的背景下分析集成方法,学习扩展到 XGBoost 的超参数。在此过程中,你将从零开始构建梯度提升模型,并将其扩展到大数据中,同时认识到使用计时器时的速度限制。XGBoost 中的细节将着重于速度提升以及数学推导参数。在详细的案例研究帮助下,你将练习使用 scikit-learn 和原生 Python API 构建并微调 XGBoost 分类器和回归器。你将利用 XGBoost 超参数来提升得分、修正缺失值、调整不平衡数据集,并微调其他基础学习器。最后,你将应用先进的 XGBoost 技术,如构建非相关集成、堆叠模型,并使用稀疏矩阵、自定义转换器和管道准备模型以便于行业部署。

本书结束时,你将能够使用 XGBoost 构建高效的机器学习模型,最大限度地减少错误并提高速度。

适合人群

本书面向数据科学专业人员和爱好者、数据分析师以及希望构建快速且准确的机器学习模型并能够应对大数据的开发人员。掌握 Python 编程语言并对线性代数有基本了解,将帮助你最大限度地提高本书的学习效果。

本书涵盖内容

第一章机器学习全景,通过介绍线性回归和逻辑回归,将 XGBoost 放入机器学习的一般背景中,随后将其与 XGBoost 进行对比。pandas 被引入用于预处理机器学习的原始数据,方法包括转换分类列和以多种方式清理空值。

第二章深入探讨决策树,详细介绍了 XGBoost 使用的决策树超参数,并通过图形和统计分析,探讨了方差和偏差的分析,强调了过拟合的重要性,这是整本书贯穿的主题。

第三章使用随机森林的集成方法,概述了随机森林作为 XGBoost 的竞争者,重点讲解了集成方法。与随机森林共享的 XGBoost 超参数,如 n_estimatorssubsample,也得到了充分的讲解。

第四章从梯度提升到 XGBoost,介绍了提升方法的基本原理,如何在scikit-learn中从零开始构建一个提升器,微调新的 XGBoost 超参数,如eta,并通过比较梯度提升与 XGBoost 的运行时间,突出了 XGBoost 在速度上的优势。

第五章XGBoost 揭秘,分析了 XGBoost 算法的数学推导,并通过一个历史相关的案例研究,展示了 XGBoost 在 Higgs Boson Kaggle 竞赛中的获胜模型角色。讨论了标准的 XGBoost 参数,构建了基础模型,并介绍了原始 Python API。

第六章XGBoost 超参数,介绍了所有重要的 XGBoost 超参数,总结了之前树集成方法的超参数,并使用原始网格搜索函数来微调 XGBoost 模型,以优化得分。

第七章用 XGBoost 发现系外行星,通过一个从头到尾的案例研究,展示如何用 XGBoost 发现系外行星。分析了不平衡数据集的陷阱,利用混淆矩阵和分类报告,引出了不同的评分指标和重要的 XGBoost 超参数scale_pos_weight

第八章XGBoost 替代基础学习器,介绍了 XGBoost 所有的提升器,包括gbtreedartgblinear,用于回归和分类。将随机森林作为基础学习器,作为 XGBoost 的替代模型,并介绍了新的XGBRFRegressorXGBRFClassifier类。

第九章XGBoost Kaggle 大师,展示了 XGBoost Kaggle 获胜者使用的一些技巧和窍门,帮助他们在竞赛中获胜,内容包括高级特征工程、构建非相关的机器学习集成和堆叠方法。

第十章XGBoost 模型部署,通过使用自定义的转换器来处理混合数据,并通过机器学习管道对新数据进行预测,将原始数据转化为 XGBoost 机器学习预测,进而部署微调后的 XGBoost 模型。

为了充分利用这本书

读者应熟练掌握 Python,至少能够切片列表、编写自己的函数并使用点标记法。对线性代数有基本了解,能够访问矩阵中的行和列即可。具有 pandas 和机器学习背景会有帮助,但不是必需的,因为书中的所有代码和概念都会逐步解释。

本书使用了 Python 最新版本,并且配合 Anaconda 发行版在 Jupyter Notebook 中运行。强烈推荐使用 Anaconda,因为它包含了所有主要的数据科学库。在开始之前,值得更新一下 Anaconda。以下部分提供了详细的步骤,以便您像我们一样设置您的编码环境。

设置您的编码环境

下表总结了本书中使用的必要软件。

下面是将此软件上传到您系统的说明。

Anaconda

本书中您需要的数据科学库以及 Jupyter Notebook、scikit-learn (sklearn) 和 Python 可以一起通过 Anaconda 安装,强烈推荐使用 Anaconda。

以下是 2020 年在您的计算机上安装 Anaconda 的步骤:

  1. 前往 www.anaconda.com/products/individual

  2. 点击下图中的 下载,此时尚未开始下载,但会为您提供多个选项(参见第 3 步):

    图 0.1 – 准备下载 Anaconda

  3. 选择您的安装程序。推荐使用适用于 Windows 和 Mac 的 64 位图形安装程序。请确保您选择的是 Python 3.7 下的前两行,因为本书中使用的都是 Python 3.7:

    图 0.2 – Anaconda 安装程序

  4. 下载开始后,请按照计算机上的提示继续,以完成安装:

    Mac 用户警告

    如果遇到错误 无法在此位置安装 Anaconda3,请不要惊慌。只需点击高亮显示的 仅为我安装,然后 继续 按钮将会显示作为一个选项。

图 0.3 – Mac 用户警告 – 只需点击“仅为我安装”,然后点击“继续”

使用 Jupyter Notebook

现在您已经安装了 Anaconda,可以打开 Jupyter Notebook 使用 Python 3.7。以下是打开 Jupyter Notebook 的步骤:

  1. 点击您计算机上的 Anaconda-Navigator

  2. 点击 启动 下的 Jupyter Notebook,如下图所示:

    图 0.4 – Anaconda 主屏幕

    这应该会在浏览器窗口中打开一个 Jupyter Notebook。虽然 Jupyter Notebook 为了方便在网页浏览器中显示,但它们实际运行在您的个人计算机上,而非在线。Google Colab Notebook 是一个可以接受的在线替代方案,但本书中仅使用 Jupyter Notebook。

  3. 从 Jupyter Notebook 右侧的 新建 标签中选择 Python 3,如下图所示:

图 0.5 – Jupyter Notebook 主屏幕

这应该会将您带到以下屏幕:

图 0.6 – Jupyter Notebook 内部界面

恭喜!您现在可以运行 Python 代码了!只需在单元格中输入任何内容,例如 print('hello xgboost!'),然后按 Shift + Enter 执行代码。

解决 Jupyter Notebook 问题

如果你在运行或安装 Jupyter notebooks 时遇到问题,请访问 Jupyter 官方的故障排除指南:jupyter-notebook.readthedocs.io/en/stable/troubleshooting.html

XGBoost

在写作时,XGBoost 尚未包含在 Anaconda 中,因此必须单独安装。

以下是安装 XGBoost 到你电脑上的步骤:

  1. 访问anaconda.org/conda-forge/xgboost。你应该看到以下内容:

    图 0.7 – Anaconda 安装 XGBoost 的推荐方法

  2. 复制上面截图中显示的第一行代码,如下所示:

    图 0.8 – 包安装

  3. 打开你电脑上的终端(Terminal)。

    如果你不知道终端在哪里,可以在 Mac 上搜索Terminal,在 Windows 上搜索Windows Terminal

  4. 将以下代码粘贴到你的终端中,按 Enter,并按照提示操作:

    conda install -c conda-forge xgboost
    
  5. 通过打开一个新的 Jupyter notebook 来验证安装是否成功,具体步骤见前一部分。然后输入import xgboost并按 Shift + Enter。你应该会看到以下内容:

图 0.9 – 在 Jupyter notebook 中成功导入 XGBoost

如果没有错误,恭喜你!你现在已经具备了运行本书代码所需的所有技术要求。

提示

如果在设置编码环境时遇到错误,请重新检查前面的步骤,或者考虑查看 Anaconda 错误文档:docs.anaconda.com/anaconda/user-guide/troubleshooting/。Anaconda 用户应通过在终端中输入conda update conda来更新 Anaconda。如果在安装 XGBoost 时遇到问题,请参考官方文档:xgboost.readthedocs.io/en/latest/build.html

版本

这里是你可以在 Jupyter notebook 中运行的代码,用来查看你所使用的软件版本:

import platform; print(platform.platform())
import sys; print("Python", sys.version)
import numpy; print("NumPy", numpy.__version__)
import scipy; print("SciPy", scipy.__version__)
import sklearn; print("Scikit-Learn", sklearn.__version__)
import xgboost; print("XGBoost", xgboost.__version__)

以下是本书中生成代码所使用的版本:

Darwin-19.6.0-x86_64-i386-64bit
Python 3.7.7 (default, Mar 26 2020, 10:32:53) 
[Clang 4.0.1 (tags/RELEASE_401/final)]
NumPy 1.19.1
SciPy 1.5.2
Scikit-Learn 0.23.2
XGBoost 1.2.0

如果你的版本与我们的不同也没关系。软件是不断更新的,使用更新版本可能会获得更好的结果。如果你使用的是旧版本,建议通过运行conda update conda来使用 Anaconda 更新。若你之前安装过旧版本的 XGBoost 并通过 Anaconda 进行管理,可以按前一部分的说明运行conda update xgboost进行更新。

访问代码文件

如果你正在使用本书的数字版,我们建议你自己输入代码,或通过 GitHub 仓库访问代码(链接将在下一部分提供)。这样可以帮助你避免因复制和粘贴代码而导致的潜在错误。

本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn。如果代码有更新,将会在现有的 GitHub 仓库中更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,您可以在 github.com/PacktPublishing/ 查看。

下载彩色图片

我们还提供了一份包含本书中使用的截图/图表彩色图片的 PDF 文件。您可以在这里下载:

static.packt-cdn.com/downloads/9781839218354_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。以下是一个示例:“AdaBoostRegressorAdaBoostClassifier 算法可以从 sklearn.ensemble 库下载,并应用于任何训练集。”

代码块如下所示:

X_bikes = df_bikes.iloc[:,:-1]
y_bikes = df_bikes.iloc[:,-1]
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_bikes, y_bikes, random_state=2)

当我们希望您关注代码块的特定部分时,相关行或项目会以粗体显示:

Stopping. Best iteration:
[1]	validation_0-error:0.118421
Accuracy: 88.16%

提示或重要说明

显示效果如下。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并发送邮件至 customercare@packtpub.com。

勘误:尽管我们已尽最大努力确保内容的准确性,但难免会有错误。如果您在本书中发现错误,恳请您向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接并输入相关详情。

盗版:如果您在互联网上发现我们的作品以任何形式的非法复制,我们将非常感激您提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并附上相关材料的链接。

如果您有兴趣成为作者:如果您在某个领域具有专业知识,并且有兴趣撰写或为书籍做贡献,请访问 authors.packtpub.com

评审

请留下评论。阅读并使用本书后,为什么不在购买您书籍的网站上留下评价呢?潜在读者可以根据您的公正意见做出购买决策,我们 Packt 也能了解您对我们产品的看法,我们的作者能够看到您对其书籍的反馈。谢谢!

关于 Packt 的更多信息,请访问 packt.com

第一部分:集成学习(Bagging 和 Boosting)

使用 scikit-learn 默认设置的 XGBoost 模型,在用 pandas 进行数据预处理并构建标准回归和分类模型后打开了本书。通过深入探索 XGBoost 背后的实际理论,逐步了解决策树(XGBoost 基础学习器)、随机森林(集成学习),以及梯度提升,比较得分并微调集成与树模型的超参数。

本节包括以下章节:

  • 第一章**,机器学习概况

  • 第二章**,决策树深入解析

  • 第三章**,使用随机森林进行集成学习

  • 第四章**,从梯度提升到 XGBoost

第一章:第一章:机器学习的全景

欢迎来到XGBoost 与 Scikit-Learn 实战,本书将教授你 XGBoost 的基础知识、技巧和窍门,XGBoost 是最佳的用于从表格数据中进行预测的机器学习算法。

本书的重点是XGBoost,也称为极端梯度提升。XGBoost 的结构、功能以及原始能力将在每一章中逐步详细展开。本书的章节展开讲述了一个令人难以置信的故事:XGBoost 的故事。通过阅读完本书,你将成为利用 XGBoost 从真实数据中进行预测的专家。

在第一章中,XGBoost 将以预览的形式出现。它将在机器学习回归和分类的更大背景下首次亮相,为接下来的内容铺垫基础。

本章重点介绍为机器学习准备数据的过程,也叫做数据处理。除了构建机器学习模型,你还将学习如何使用高效的Python代码加载数据、描述数据、处理空值、将数据转换为数值列、将数据分割为训练集和测试集、构建机器学习模型、实施交叉验证,并且将线性回归逻辑回归模型与 XGBoost 进行比较。

本章中介绍的概念和库将在全书中使用。

本章包含以下内容:

  • 预览 XGBoost

  • 数据处理

  • 预测回归

  • 预测分类

预览 XGBoost

机器学习在 1940 年代随着第一个神经网络的出现而获得认可,接着在 1950 年代迎来了第一个机器学习国际象棋冠军。经过几十年的沉寂,机器学习领域在 1990 年代迎来了飞跃,当时深蓝在著名的比赛中击败了世界象棋冠军加里·卡斯帕罗夫。随着计算能力的飞速增长,1990 年代和 2000 年代初涌现出大量学术论文,揭示了诸如随机森林AdaBoost等新的机器学习算法。

提升的基本思路是通过反复改进错误,将弱学习器转变为强学习器。梯度提升的核心思想是利用梯度下降法最小化残差的错误。这一从标准机器学习算法到梯度提升的进化思路是本书前四章的核心内容。

XGBoost 是极端梯度提升(Extreme Gradient Boosting)的缩写。极端部分指的是通过极限计算来提高准确性和速度。XGBoost 的快速流行主要得益于其在Kaggle 竞赛中的无与伦比的成功。在 Kaggle 竞赛中,参赛者构建机器学习模型,力图做出最佳预测并赢取丰厚的现金奖励。与其他模型相比,XGBoost 在竞赛中常常碾压对手。

理解 XGBoost 的细节需要了解梯度提升算法中机器学习的全貌。为了呈现完整的图景,我们从机器学习的基础开始讲起。

什么是机器学习?

机器学习是计算机从数据中学习的能力。2020 年,机器学习能够预测人类行为、推荐产品、识别面孔、超越扑克高手、发现系外行星、识别疾病、操作自动驾驶汽车、个性化互联网体验,并直接与人类交流。机器学习正在引领人工智能革命,影响着几乎所有大公司底线。

在实践中,机器学习意味着实现计算机算法,当新数据进入时,算法的权重会随之调整。机器学习算法通过学习数据集来对物种分类、股市、公司利润、人类决策、亚原子粒子、最佳交通路线等进行预测。

机器学习是我们手中最好的工具,可以将大数据转化为准确、可操作的预测。然而,机器学习并非在真空中发生。机器学习需要大量的数据行和列。

数据清洗

数据清洗是一个全面的术语,涵盖了机器学习开始之前的数据预处理各个阶段。数据加载、数据清理、数据分析和数据操作都属于数据清洗的范畴。

本章详细介绍了数据清洗。示例旨在涵盖标准的数据清洗挑战,所有这些挑战都可以通过 Python 的数据分析专用库pandas快速处理。尽管不要求具有pandas的经验,但基本的pandas知识将对学习有帮助。所有代码都有详细解释,方便新手跟随学习。

数据集 1 – 自行车租赁

自行车租赁数据集是我们的第一个数据集。数据源来自世界著名的公共数据仓库 UCI 机器学习库(archive.ics.uci.edu/ml/index.php)。我们的自行车租赁数据集已从原始数据集(archive.ics.uci.edu/ml/datasets/bike+sharing+dataset)调整,添加了空值,以便你可以练习如何修正这些空值。

访问数据

数据清洗的第一步是访问数据。可以通过以下步骤实现:

  1. 下载数据。所有本书的文件都存储在 GitHub 上。你可以通过点击桌面上的Data文件夹,将所有文件下载到本地计算机。

  2. 打开 Jupyter Notebook。您可以在前言中找到下载 Jupyter Notebook 的链接。在终端中点击 jupyter notebook。网页浏览器打开后,您应该看到一列文件夹和文件。进入与自行车租赁数据集相同的文件夹,选择 New: Notebook: Python 3。这里有一个视觉指南:

    图 1.2 – 访问 Jupyter Notebook 的视觉指南

    图 1.2 – 访问 Jupyter Notebook 的视觉指南

    小贴士

    如果您在打开 Jupyter Notebook 时遇到困难,请参阅 Jupyter 的官方故障排除指南:jupyter-notebook.readthedocs.io/en/stable/troubleshooting.html

  3. 在 Jupyter Notebook 的第一个单元格中输入以下代码:

    import pandas as pd 
    

    按下 Shift + Enter 运行单元格。现在,当你输入 pd 时,你可以访问 pandas 库了。

  4. 使用 pd.read_csv 加载数据。加载数据需要一个 read 方法。read 方法将数据存储为 DataFrame,这是一个用于查看、分析和操作数据的 pandas 对象。加载数据时,将文件名放在引号内,然后运行单元格:

    df_bikes = pd.read_csv('bike_rentals.csv')
    

    如果您的数据文件与 Jupyter Notebook 不在同一位置,您必须提供文件目录,例如 Downloads/bike_rental.csv

    现在数据已正确存储在名为 df_bikes 的 DataFrame 中。

    小贴士

    Tab 补全:在 Jupyter Notebook 中编码时,输入几个字符后,按 Tab 键。对于 CSV 文件,您应该看到文件名出现。用光标高亮显示名称,然后按 Enter 键。如果文件名是唯一的选项,您可以按 Enter 键。Tab 补全可以使您的编码体验更快速、更可靠。

  5. 使用 .head() 显示数据。最后一步是查看数据以确保正确加载。.head() 是一个显示 DataFrame 前五行的方法。您可以在括号中放入任何正整数以查看任意数量的行。输入以下代码并按 Shift + Enter

    df_bikes.head()
    

    这里是前几行的屏幕截图以及预期的输出:

图 1.3 –  输出

图 1.3 – bike_rental.csv 输出

现在我们可以访问数据,让我们看看三种理解数据的方法。

理解数据

现在数据已加载,是时候理解数据了。理解数据对于未来做出明智决策至关重要。以下是三种理解数据的好方法。

您已经看到了 .head(),这是一个广泛使用的方法,用于解释列名和编号。如前面的输出所示,dteday 是日期,而 instant 是有序索引。

.describe()

可以使用 .describe() 查看数值统计信息,如下所示:

df_bikes.describe()

这是预期的输出:

图 1.4 –  输出

图 1.4 – .describe() 输出

你可能需要向右滚动才能查看所有列。

比较均值和中位数(50%)可以指示数据的偏斜程度。正如你所看到的,meanmedian 相近,所以数据大致对称。每列的 maxmin 值,以及四分位数和标准差(std)也被展示出来。

.info()

另一个很好的方法是 .info(),它显示有关列和行的一般信息:

df_bikes.info()

这是预期的输出:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 731 entries, 0 to 730
Data columns (total 16 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   instant     731 non-null    int64  
 1   dteday      731 non-null    object 
 2   season      731 non-null    float64
 3   yr          730 non-null    float64
 4   mnth        730 non-null    float64
 5   holiday     731 non-null    float64
 6   weekday     731 non-null    float64
 7   workingday  731 non-null    float64
 8   weathersit  731 non-null    int64  
 9   temp        730 non-null    float64
 10  atemp       730 non-null    float64
 11  hum         728 non-null    float64
 12  windspeed   726 non-null    float64
 13  casual      731 non-null    int64  
 14  registered  731 non-null    int64  
 15  cnt         731 non-null    int64  
dtypes: float64(10), int64(5), object(1)
memory usage: 91.5+ KB

如你所见,.info() 给出了行数、列数、列类型和非空值的数量。由于非空值的数量在列之间不同,空值一定存在。

修正空值

如果空值没有得到修正,未来可能会出现意外的错误。在本小节中,我们展示了多种修正空值的方法。我们的例子不仅用于处理空值,还展示了 pandas 的广度和深度。

以下方法可以用于修正空值。

查找空值的数量

以下代码显示空值的总数:

df_bikes.isna().sum().sum()

这是结果:

12

请注意,需要两个 .sum() 方法。第一个方法对每一列的空值进行求和,第二个方法对列数进行求和。

显示空值

你可以通过以下代码显示所有包含空值的行:

 df_bikes[df_bikes.isna().any(axis=1)]

这段代码可以分解如下:df_bikes[conditional] 是满足括号内条件的 df_bikes 子集。.df_bikes.isna().any 聚集所有的空值,而 (axis=1) 指定了列中的值。在 pandas 中,行是 axis 0,列是 axis 1

这是预期的输出:

图 1.5 – 自行车租赁数据集的空值

图 1.5 – 自行车租赁数据集的空值

从输出中可以看出,windspeedhumiditytemperature 列以及最后一行都存在空值。

提示

如果这是你第一次使用 pandas,可能需要一些时间来习惯这种表示法。你可以查看 Packt 的 Hands-On Data Analysis with Pandas,这是一本很好的入门书籍:subscription.packtpub.com/book/data/9781789615326

修正空值

修正空值的方法取决于列和数据集。我们来看看一些策略。

用中位数/均值替换

一种常见的策略是用中位数或均值替换空值。这里的想法是用列的平均值替换空值。

对于 'windspeed' 列,可以用 median 值替换空值,方法如下:

df_bikes['windspeed'].fillna((df_bikes['windspeed'].median()), inplace=True)

df_bikes['windspeed'].fillna 意味着 'windspeed' 列的空值将被填充。df_bikes['windspeed'].median()'windspeed' 列的中位数。最后,inplace=True 确保更改是永久性的。

提示

中位数通常比均值更合适。中位数保证数据中有一半的值大于该值,另一半小于该值。相比之下,均值容易受到异常值的影响。

在前面的单元格中,df_bikes[df_bikes.isna().any(axis=1)]显示了windspeed列为空值的行 5681。可以使用.iloc显示这些行,iloc索引位置的缩写:

df_bikes.iloc[[56, 81]]

这是预期的输出:

图 1.6 – 行 56 和 81

图 1.6 – 行 56 和 81

如预期的那样,空值已被替换为风速的中位数。

提示

用户在使用.iloc时常常会因单括号或双括号的使用不当而出错。.iloc使用单括号来表示一个索引,如:df_bikes.iloc[56]。现在,df_bikes也支持在括号内使用列表来接受多个索引。多个索引需要使用双括号,如:df_bikes.iloc[[56, 81]]。有关更多文档,请参考pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.iloc.html

使用中位数/均值进行 groupby

使用groupby可以在修正空值时获得更细致的结果。

groupby 通过共享的值来组织行。由于行中有四个共享的季节,按季节进行 groupby 会得到四行数据,每行对应一个季节。但是,每个季节的值来自许多不同的行。我们需要一种方法来合并或聚合这些值。常用的聚合方式包括.sum().count().mean().median()。我们使用.median()

通过.median()聚合按季节分组df_bikes的代码如下:

df_bikes.groupby(['season']).median()

这是预期的输出:

图 1.7 – 按季节分组的 df_bikes 输出

图 1.7 – 按季节分组的 df_bikes 输出

如你所见,列中的值为中位数。

要修正hum列中的空值,hum湿度的缩写,我们可以按季节取湿度的中位数。

修正hum列空值的代码是df_bikes['hum'] = df_bikes['hum'].fillna()

fillna中的代码是所需的值。从groupby获取的值需要使用transform方法,如下所示:

df_bikes.groupby('season')['hum'].transform('median')

这是合并后的代码,作为一步长操作:

df_bikes['hum'] = df_bikes['hum'].fillna(df_bikes.groupby('season')['hum'].transform('median'))

你可以通过检查df_bikes.iloc[[129, 213, 388]]来验证转换结果。

从特定行获取中位数/均值

在某些情况下,用特定行的数据替代空值可能更有利。

在修正温度时,除了参考历史记录外,取前后两天的平均温度通常可以得到一个较好的估算值。

要查找'temp'列中的空值,可以输入以下代码:

df_bikes[df_bikes['temp'].isna()]

这是预期的输出:

图 1.8 – 'temp' 列的输出

图 1.8 – 'temp' 列的输出

如你所见,索引701包含空值。

要找到 701 索引前一天和后一天的平均温度,完成以下步骤:

  1. 将第 700 和第 702 行的温度相加并除以 2。对 'temp''atemp' 列执行此操作:

    mean_temp = (df_bikes.iloc[700]['temp'] + df_bikes.iloc[702]['temp'])/2
    mean_atemp = (df_bikes.iloc[700]['atemp'] + df_bikes.iloc[702]['atemp'])/2
    
  2. 替换空值:

    df_bikes['temp'].fillna((mean_temp), inplace=True)
    df_bikes['atemp'].fillna((mean_atemp), inplace=True)
    

你可以自行验证,空值已经按预期填充。

外推日期

我们纠正空值的最终策略涉及日期。当提供了真实日期时,日期值可以进行外推。

df_bikes['dteday'] 是一列日期列;然而,df_bikes.info() 显示的列类型是对象,通常表示为字符串。日期对象,如年份和月份,必须从 datetime 类型中外推。可以使用 to_datetime 方法将 df_bikes['dteday'] 转换为 'datetime' 类型,如下所示:

df_bikes['dteday'] = pd.to_datetime(df_bikes['dteday'],infer_datetime_format=True)

infer_datetime_format=True 允许 pandas 决定存储哪种类型的日期时间对象,在大多数情况下这是一个安全的选项。

要外推单个列,首先导入 datetime 库:

import datetime as dt

现在我们可以使用不同的方法来外推空值的日期。一个标准方法是将 'mnth' 列转换为从 'dteday' 列外推得到的正确月份。这有助于纠正转换过程中可能出现的其他错误,前提是当然 'dteday' 列是正确的。

代码如下:

ddf_bikes['mnth'] = df_bikes['dteday'].dt.month

验证更改是非常重要的。由于空日期值位于最后一行,我们可以使用 .tail(),这是一个与 .head() 类似的 DataFrame 方法,用于显示最后五行:

df_bikes.tail()

这是预期的输出:

图 1.9 – 外推日期值的输出

图 1.9 – 外推日期值的输出

如你所见,月份值都是正确的,但年份值需要更改。

'dteday' 列中最后五行的年份都是 2012,但由 'yr' 列提供的对应年份是 1.0。为什么?

数据已被归一化,这意味着它已转换为介于 01 之间的值。

归一化数据通常更高效,因为机器学习权重不需要调整不同范围的值。

你可以使用 .loc 方法填充正确的值。.loc 方法用于按行和列定位条目,方法如下:

df_bikes.loc[730, 'yr'] = 1.0

现在你已经练习过修正空值并获得了相当的 pandas 使用经验,是时候处理非数值列了。

删除非数值列

对于机器学习,所有数据列都应该是数值型的。根据 df.info(),唯一不是数值型的列是 df_bikes['dteday']。此外,这列是冗余的,因为所有日期信息已经存在于其他列中。

可以按如下方式删除该列:

df_bikes = df_bikes.drop('dteday', axis=1)

现在我们已经有了所有数值列且没有空值,我们可以进行机器学习了。

预测回归

机器学习算法旨在利用一个或多个输入列的数据来预测一个输出列的值。这些预测依赖于由所处理的机器学习问题的总体类别所决定的数学方程式。大多数监督学习问题被分类为回归或分类问题。在这一部分中,机器学习将在回归的背景下进行介绍。

预测自行车租赁数量

在自行车租赁数据集中,df_bikes['cnt']是某一天的自行车租赁数量。预测这一列对于自行车租赁公司来说非常有用。我们的问题是基于数据(如是否为假期或工作日、预报温度、湿度、风速等)来预测某一天的自行车租赁数量。

根据数据集,df_bikes['cnt']df_bikes['casual']df_bikes['registered']的总和。如果将df_bikes['registered']df_bikes['casual']作为输入列,则预测结果将始终 100%准确,因为这些列的和始终是正确的结果。虽然完美的预测在理论上是理想的,但在现实中包括那些本应无法得知的输入列是没有意义的。

所有当前的列都可以用来预测df_bikes['cnt'],除了之前提到的'casual''registered'列。可以通过.drop方法删除'casual''registered'列,如下所示:

df_bikes = df_bikes.drop(['casual', 'registered'], axis=1)

数据集现在已经准备好了。

保存数据以供未来使用

本书中将多次使用自行车租赁数据集。为了避免每次运行笔记本进行数据整理,可以将清理后的数据集导出为 CSV 文件,以便未来使用:

df_bikes.to_csv('bike_rentals_cleaned.csv', index=False)

index=False参数防止索引创建额外的列。

声明预测列和目标列

机器学习通过对每个预测列(输入列)执行数学运算来确定目标列(输出列)。

通常将预测列用大写X表示,将目标列用小写y表示。由于我们的目标列是最后一列,可以通过使用索引表示法切片的方式将数据划分为预测列和目标列:

X = df_bikes.iloc[:,:-1]y = df_bikes.iloc[:,-1]

逗号用于分隔列和行。第一个冒号:表示所有行都包含在内。逗号后的:-1表示从第一列开始,一直到最后一列,但不包括最后一列。第二个-1只包含最后一列。

理解回归

预测自行车租赁数量,在实际情况中可能会得到任何非负整数。当目标列包含无限范围的值时,机器学习问题被归类为回归问题。

最常见的回归算法是线性回归。线性回归将每个预测变量列视为 多项式变量,并将这些值乘以 系数(也称为 权重),以预测目标变量列。梯度下降法在幕后工作,以最小化误差。线性回归的预测结果可以是任何实数。

在运行线性回归之前,我们必须将数据分割为训练集和测试集。训练集将数据拟合到算法中,使用目标列来最小化误差。模型建立后,将其在测试数据上进行评分。

保留一个测试集来评估模型的重要性不容小觑。在大数据的世界中,由于有大量的数据点可用于训练,过拟合训练集是常见的现象。过拟合通常是不好的,因为模型会过于贴合离群点、不寻常的实例和临时趋势。强大的机器学习模型能够在对新数据进行良好泛化的同时,准确地捕捉到当前数据的细微差异,这一概念在 第二章**《决策树深入解析》中有详细探讨。

访问 scikit-learn

所有机器学习库都将通过 scikit-learn 进行处理。Scikit-learn 的广泛功能、易用性和计算能力使其成为全球最广泛使用的机器学习库之一。

从 scikit-learn 导入 train_test_splitLinearRegression,如下所示:

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression

接下来,将数据分割为训练集和测试集:

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)

注意 random_state=2 参数。每当看到 random_state=2 时,这意味着你选择了伪随机数生成器的种子,以确保结果可复现。

静默警告

在构建你的第一个机器学习模型之前,先静默所有警告。Scikit-learn 包含警告,通知用户未来的更改。一般来说,不建议静默警告,但由于我们的代码已被测试过,建议在 Jupyter Notebook 中节省空间。

可以按如下方式静默警告:

import warnings
warnings.filterwarnings('ignore')

现在是时候构建你的第一个模型了。

构建线性回归模型

线性回归模型可以通过以下步骤构建:

  1. 初始化机器学习模型:

    lin_reg = LinearRegression()
    
  2. 在训练集上拟合模型。这是机器学习模型构建的地方。请注意,X_train 是预测变量列,y_train 是目标变量列。

    lin_reg.fit(X_train, y_train)
    
  3. 对测试集进行预测。X_test(测试集中的预测变量列)的预测结果使用 .predict 方法通过 lin_reg 存储为 y_pred

    y_pred = lin_reg.predict(X_test)
    
  4. 将预测结果与测试集进行比较。对模型进行评分需要一个比较基准。线性回归的标准是 mean_squared_error,即预测值与实际值之间差异的平方和,再取平方根,以保持单位一致。可以导入 mean_squared_error,并使用 Numerical Python,即 NumPy,一个为与 pandas 一起工作而设计的高速库,来计算平方根。

  5. 导入 mean_squared_error 和 NumPy,然后计算均方误差并取平方根:

    from sklearn.metrics import mean_squared_error
    import numpy as np
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    
  6. 打印你的结果:

    print("RMSE: %0.2f" % (rmse))
    

    结果如下:

    RMSE: 898.21
    

    这是构建你的第一个机器学习模型的所有代码的截图:

图 1.10 – 构建你的机器学习模型的代码

图 1.10 – 构建你的机器学习模型的代码

在不知道每日预期租赁量的范围时,很难判断 898 次租赁错误是否好坏。

.describe() 方法可以用于 df_bikes['cnt'] 列,以获取范围等信息:

df_bikes['cnt'].describe()

这是输出结果:

count     731.000000
mean     4504.348837
std      1937.211452
min        22.000000
25%      3152.000000
50%      4548.000000
75%      5956.000000
max      8714.000000
Name: cnt, dtype: float64

预测的值范围从 228714,均值为 4504,标准差为 1937,RMSE 为 898,虽然不差,但也不能说很好。

XGBoost

线性回归是解决回归问题的众多算法之一。其他回归算法可能会产生更好的结果。一般的策略是尝试不同的回归器进行比较。你将在本书中尝试多种回归器,包括决策树、随机森林、梯度提升,以及本书的重点,XGBoost。

本书后续将提供 XGBoost 的全面介绍。现在请注意,XGBoost 包括一个回归器,名为 XGBRegressor,可以用于任何回归数据集,包括刚才评分的自行车租赁数据集。现在我们将使用 XGBRegressor 来将自行车租赁数据集的结果与线性回归进行比较。

你应该已经在前面安装了 XGBoost。如果没有,请现在安装 XGBoost。

XGBRegressor

安装 XGBoost 后,可以按如下方式导入 XGBoost 回归器:

from xgboost import XGBRegressor

构建 XGBRegressor 的一般步骤与构建 LinearRegression 的步骤相同,唯一的区别是初始化 XGBRegressor 而不是 LinearRegression

  1. 初始化一个机器学习模型:

    xg_reg = XGBRegressor()
    
  2. 在训练集上拟合模型。如果此时 XGBoost 给出一些警告,请不用担心:

    xg_reg.fit(X_train, y_train)
    
  3. 对测试集进行预测:

    y_pred = xg_reg.predict(X_test)
    
  4. 将预测结果与测试集进行比较:

    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    
  5. 打印你的结果:

    print("RMSE: %0.2f" % (rmse))
    

    输出如下:

    RMSE: 705.11
    

XGBRegressor 表现明显更好!

XGBoost 为什么通常比其他方法表现更好将在第五章中探讨,书名为 XGBoost 揭秘

交叉验证

一个测试分数是不可靠的,因为将数据拆分为不同的训练集和测试集会得到不同的结果。实际上,将数据拆分为训练集和测试集是任意的,不同的random_state会得到不同的 RMSE。

解决不同分割之间评分差异的一种方法是k 折交叉验证。其思路是将数据多次拆分为不同的训练集和测试集,然后取这些评分的均值。分割次数,称为折叠,由k表示。标准做法是使用 k = 3、4、5 或 10 个分割。

下面是交叉验证的可视化描述:

图 1.11 – 交叉验证

图 1.11 – 交叉验证

(重绘自commons.wikimedia.org/wiki/File:K-fold_cross_validation_EN.svg

交叉验证通过在第一个训练集上拟合机器学习模型,并在第一个测试集上进行评分来工作。为第二次分割提供不同的训练集和测试集,从而生成一个新的机器学习模型,并对其进行评分。第三次分割会生成一个新的模型,并在另一个测试集上进行评分。

在训练集之间会有重叠,但测试集之间没有。

选择折叠数是灵活的,取决于数据。五折是标准做法,因为每次都会保留 20%的测试集。使用 10 折时,只有 10%的数据被保留;然而,90%的数据可用于训练,且均值对异常值的敏感性较小。对于较小的数据集,三折可能效果更好。

最后,将会有 k 个不同的评分,评估模型在 k 个不同的测试集上的表现。取这 k 个折叠的平均得分比任何单一折叠的得分更可靠。

cross_val_score是实现交叉验证的一种便捷方式。cross_val_score接受一个机器学习算法作为输入,以及预测列和目标列,可选的额外参数包括评分标准和所需的折叠次数。

使用线性回归进行交叉验证

让我们使用LinearRegression进行交叉验证。

首先,从cross_val_score库中导入cross_val_score

from sklearn.model_selection import cross_val_score

现在,使用交叉验证按以下步骤构建和评分机器学习模型:

  1. 初始化一个机器学习模型:

    model = LinearRegression()
    
  2. 使用cross_val_score实现模型、Xyscoring='neg_mean_squared_error'和折叠次数cv=10作为输入:

    scores = cross_val_score(model, X, y, scoring='neg_mean_squared_error', cv=10)
    

    提示

    为什么使用scoring='neg_mean_squared_error'?Scikit-learn 的设计是选择最高的得分来训练模型。这对于准确度是有效的,但对于误差则不适用,因为最低的误差才是最佳的。通过取每个均方误差的负值,最低的结果最终变为最高值。后续通过rmse = np.sqrt(-scores)来补偿这一点,因此最终结果是正数。

  3. 通过取负评分的平方根来找到 RMSE:

    rmse = np.sqrt(-scores)
    
  4. 显示结果:

    print('Reg rmse:', np.round(rmse, 2))
    print('RMSE mean: %0.2f' % (rmse.mean()))
    

    输出如下:

    Reg rmse: [ 504.01  840.55 1140.88  728.39  640.2   969.95 
    1133.45 1252.85 1084.64  1425.33]
    RMSE mean: 972.02
    

线性回归的平均误差为972.06。 这比之前获得的980.38略好。 关键不在于分数是好还是坏,而在于这是对线性回归在未见数据上表现的更好估计。

始终建议使用交叉验证以更好地估计分数。

关于print函数

在运行自己的机器学习代码时,全局print函数通常是不必要的,但如果要打印多行并格式化输出,则非常有用。

使用 XGBoost 进行交叉验证

现在让我们用XGBRegressor进行交叉验证。 步骤相同,只是初始化模型不同:

  1. 初始化机器学习模型:

    model = XGBRegressor()
    
  2. 使用模型Xy、评分和折数cv实现cross_val_score

    scores = cross_val_score(model, X, y, scoring='neg_mean_squared_error', cv=10)
    
  3. 通过取负分数的平方根来查找 RMSE:

    rmse = np.sqrt(-scores)
    
  4. 打印结果:

    print('Reg rmse:', np.round(rmse, 2))
    print('RMSE mean: %0.2f' % (rmse.mean()))
    

    输出如下:

    Reg rmse: [ 717.65  692.8   520.7   737.68  835.96 1006.24  991.34  747.61  891.99 1731.13]
    RMSE mean: 887.31
    

XGBRegressor再次胜出,比线性回归高约 10%。

预测分类

您已了解到 XGBoost 在回归中可能有优势,但分类呢? XGBoost 有分类模型,但它是否能像经过充分测试的分类模型(如逻辑回归)一样准确? 让我们找出答案。

什么是分类?

与回归不同,当预测具有有限输出数量的目标列时,机器学习算法被归类为分类算法。 可能的输出包括以下内容:

  • 是,否

  • 垃圾邮件,非垃圾邮件

  • 0, 1

  • 红色,蓝色,绿色,黄色,橙色

数据集 2 – 人口普查

我们将更快地通过第二个数据集,人口普查收入数据集 (archive.ics.uci.edu/ml/datasets/Census+Income),来预测个人收入。

数据整理

在实施机器学习之前,必须对数据集进行预处理。 在测试新算法时,所有数值列都没有空值是至关重要的。

数据加载

由于此数据集直接托管在 UCI 机器学习网站上,可以使用pd.read_csv直接从互联网下载:

df_census = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data')
df_census.head()

这里是预期的输出:

图 1.12 – 人口普查收入数据集

图 1.12 – 人口普查收入数据集

输出显示,列标题代表第一行的条目。 当发生这种情况时,可以使用header=None参数重新加载数据:

df_census = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data', header=None)
df_census.head()

这里是没有标题的预期输出:

图 1.13 – 参数输出

图 1.13 – header=None参数输出

如您所见,列名仍然缺失。 它们列在人口普查收入数据集网站的属性信息下 (archive.ics.uci.edu/ml/datasets/Census+Income)。

列名可以更改如下:

df_census.columns=['age', 'workclass', 'fnlwgt', 'education', 'education-num', 'marital-status', 'occupation', 'relationship', 'race', 'sex', 'capital-gain', 'capital-loss', 'hours-per-week', 'native-country', 'income']
df_census.head()

这是包含列名的预期输出:

图 1.14 – 预期的列名

图 1.14 – 预期的列名

如你所见,列名已经恢复。

空值

检查空值的好方法是查看数据框的.info()方法:

df_census.info()

输出如下:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32561 entries, 0 to 32560
Data columns (total 15 columns):
 #   Column          Non-Null Count  Dtype 
---  ------          --------------  ----- 
 0   age             32561 non-null  int64 
 1   workclass       32561 non-null  object
 2   fnlwgt          32561 non-null  int64 
 3   education       32561 non-null  object
 4   education-num   32561 non-null  int64 
 5   marital-status  32561 non-null  object
 6   occupation      32561 non-null  object
 7   relationship    32561 non-null  object
 8   race            32561 non-null  object
 9   sex             32561 non-null  object
 10  capital-gain    32561 non-null  int64 
 11  capital-loss    32561 non-null  int64 
 12  hours-per-week  32561 non-null  int64 
 13  native-country  32561 non-null  object
 14  income          32561 non-null  object
dtypes: int64(6), object(9)
memory usage: 3.7+ MB

由于所有列的非空行数相同,我们可以推断没有空值。

非数值列

所有dtype为对象的列必须转换为数值列。get_dummies方法将每一列的非数值唯一值转换为各自的列,其中1表示存在,0表示不存在。例如,如果数据框"书籍类型"的列值为"精装书"、"平装书"或"电子书",pd.get_dummies会创建三个新列,分别命名为"精装书"、"平装书"和"电子书",并替换原有的"书籍类型"列。

这是"书籍类型"数据框:

图 1.15 – "书籍类型"数据框

图 1.15 – "书籍类型"数据框

这是应用了pd.get_dummies后的相同数据框:

图 1.16 – 新的数据框

图 1.16 – 新的数据框

pd.get_dummies会创建许多新列,因此值得检查是否有某些列可以被删除。快速查看df_census数据可以发现'education'列和education_num列。education_num列是'education'列的数值转换,因为信息相同,'education'列可以删除:

df_census = df_census.drop(['education'], axis=1)

现在使用pd.get_dummies将非数值列转换为数值列:

df_census = pd.get_dummies(df_census)
df_census.head()

这是预期的输出:

图 1.17 – pd.get_dummies – 非数值列转换为数值列

图 1.17 – pd.get_dummies – 非数值列转换为数值列

如你所见,新的列是通过column_value语法创建的,引用了原始列。例如,native-country是原始列,而台湾是其中一个值。新的native-country_Taiwan列的值为1(如果这个人来自台湾),否则为0

提示

使用pd.get_dummies可能会增加内存使用量,可以通过在数据框上使用.info()方法并查看最后一行来验证。存储的是1,而0的值不会被存储。有关稀疏矩阵的更多信息,请参见第十章XGBoost 模型部署,或访问 SciPy 的官方文档:docs.scipy.org/doc/scipy/reference/

目标列和预测列

由于所有列都是数值型且没有空值,接下来是将数据分为目标列和预测列。

目标列是判断某人是否赚取 50K。经过pd.get_dummies处理后,生成了两个列,df_census['income_<=50K']df_census['income_>50K'],用来判断某人是否赚取 50K。由于任一列都能使用,我们删除了df_census['income_<=50K']

df_census = df_census.drop('income_ <=50K', axis=1)

现在将数据拆分为X(预测列)和y(目标列)。请注意,由于最后一列是目标列,因此使用-1进行索引:

X = df_census.iloc[:,:-1]y = df_census.iloc[:,-1]

现在是时候构建机器学习分类器了!

逻辑回归

逻辑回归是最基本的分类算法。从数学上讲,逻辑回归的工作方式类似于线性回归。对于每一列,逻辑回归会找到一个适当的权重或系数,最大化模型的准确度。主要的区别在于,逻辑回归使用sigmoid 函数,而不是像线性回归那样对每一项求和。

这是 sigmoid 函数及其对应的图:

图 1.18 – Sigmoid 函数图

图 1.18 – Sigmoid 函数图

Sigmoid 函数通常用于分类。所有大于 0.5 的值都会被匹配为 1,所有小于 0.5 的值都会被匹配为 0。

使用 scikit-learn 实现逻辑回归几乎与实现线性回归相同。主要的区别是,预测列应该适应类别,并且误差应该以准确率为度量。作为附加奖励,误差默认是以准确率为度量的,因此不需要显式的评分参数。

你可以按如下方式导入逻辑回归:

from sklearn.linear_model import LogisticRegression

交叉验证函数

让我们在逻辑回归上使用交叉验证,预测某人是否赚取超过 50K。

不要重复复制粘贴,让我们构建一个交叉验证分类函数,该函数接受机器学习算法作为输入,并输出准确度得分,使用cross_val_score

def cross_val(classifier, num_splits=10):    model = classifier     scores = cross_val_score(model, X, y, cv=num_splits)    print('Accuracy:', np.round(scores, 2))    print('Accuracy mean: %0.2f' % (scores.mean()))

现在使用逻辑回归调用函数:

cross_val(LogisticRegression())

输出如下:

Accuracy: [0.8  0.8  0.79 0.8  0.79 0.81 0.79 0.79 0.8  0.8 ]
Accuracy mean: 0.80

80%的准确率已经不错了。

让我们看看 XGBoost 是否能做得更好。

提示

每当你发现自己在复制和粘贴代码时,应该寻找更好的方法!计算机科学的一个目标是避免重复。编写你自己的数据分析和机器学习函数,能让你的工作更加轻松和高效,长远来看也会带来好处。

XGBoost 分类器

XGBoost 有回归器和分类器。要使用分类器,请导入以下算法:

from xgboost import XGBClassifier

现在,在cross_val函数中运行分类器,并进行一个重要的添加。由于有 94 列,并且 XGBoost 是一个集成方法,这意味着它每次运行时会结合多个模型,每个模型包含 10 个分割,我们将把n_estimators(模型数量)限制为5。通常,XGBoost 非常快速,事实上,它有着成为最快的提升集成方法的声誉,这个声誉我们将在本书中验证!然而,出于初步目的,5个估计器,虽然没有默认的100个那么强大,但已经足够。关于如何选择n_estimators的细节将在第四章**,从梯度提升到 XGBoost中深入探讨。

cross_val(XGBClassifier(n_estimators=5))

输出如下:

Accuracy: [0.85 0.86 0.87 0.85 0.86 0.86 0.86 0.87 0.86 0.86]
Accuracy mean: 0.86

正如你所看到的,XGBoost 在默认设置下的表现优于逻辑回归。

概述

你的 XGBoost 之旅正式开始了!你从学习数据处理的基础知识开始,并掌握了所有机器学习从业者必备的pandas技能,重点是处理空值。接着,你通过将线性回归与 XGBoost 进行比较,学习了如何在 scikit-learn 中构建机器学习模型。然后,你准备了一个分类数据集,并将逻辑回归与 XGBoost 进行了比较。在这两个案例中,XGBoost 都是明显的赢家。

恭喜你成功构建了第一个 XGBoost 模型!你已经完成了使用pandas、NumPy 和 scikit-learn 库进行数据处理和机器学习的入门。

第二章**,深入决策树中,你将通过构建决策树(XGBoost 机器学习模型的基础学习器)并微调超参数来提高你的机器学习技能,从而改善结果。

第二章:第二章:决策树深入解析

在本章中,你将熟练掌握决策树,这也是 XGBoost 模型的核心机器学习算法。你还将亲自体验超参数调优的科学和艺术。由于决策树是 XGBoost 模型的基础,因此你在本章学到的技能对构建强健的 XGBoost 模型至关重要。

在本章中,你将构建和评估决策树分类器决策树回归器,可视化并分析决策树的方差和偏差,并调优决策树的超参数。此外,你还将应用决策树进行一个案例研究,该研究预测患者的心脏病。

本章包含以下主要内容:

  • 引入 XGBoost 中的决策树

  • 探索决策树

  • 对比方差和偏差

  • 调优决策树超参数

  • 预测心脏病——案例研究

引入 XGBoost 中的决策树

XGBoost 是一种集成方法,意味着它由多个机器学习模型组成,这些模型共同协作。构成 XGBoost 集成的单个模型被称为基学习器

决策树是最常用的 XGBoost 基学习器,在机器学习领域具有独特性。与线性回归和逻辑回归(见第一章机器学习领域)通过数值权重乘以列值不同,决策树通过提问列的内容来分割数据。实际上,构建决策树就像是在玩“20 个问题”游戏。

例如,决策树可能包含一个温度列,该列可以分为两组,一组是温度高于 70 度,另一组是温度低于 70 度。接下来的分裂可能基于季节,如果是夏季,则走一条分支,否则走另一条分支。此时,数据已被分为四个独立的组。通过分支将数据分割成新组的过程会持续进行,直到算法达到预期的准确度。

决策树可以创建成千上万的分支,直到它将每个样本唯一地映射到训练集中的正确目标。这意味着训练集可以达到 100%的准确度。然而,这样的模型对于新数据的泛化能力较差。

决策树容易对数据过拟合。换句话说,决策树可能会过于精确地拟合训练数据,这个问题将在本章后面通过方差和偏差进行探讨。超参数调优是防止过拟合的一种解决方案。另一种解决方案是聚合多棵树的预测,这是随机森林和 XGBoost 采用的策略。

虽然随机森林和 XGBoost 将是后续章节的重点,但我们现在深入探讨决策树。

探索决策树

决策树通过将数据分割成分支来工作。沿着分支向下到达叶子,在这里做出预测。理解分支和叶子是如何创建的,通过实际示例要容易得多。在深入了解之前,让我们构建第一个决策树模型。

第一个决策树模型

我们首先通过构建一个决策树来预测某人是否年收入超过 50K 美元,使用的是来自第一章的《机器学习领域》中的人口普查数据集:

  1. 首先,打开一个新的 Jupyter Notebook,并从以下导入开始:

    import pandas as pd
    import numpy as np
    import warnings
    warnings.filterwarnings('ignore')
    
  2. 接下来,打开文件'census_cleaned.csv',它已经上传到github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter02供你使用。如果你按照前言的建议从 Packt GitHub 页面下载了本书的所有文件,那么在启动 Anaconda 后,你可以像浏览其他章节一样浏览到第二章决策树深入解析》。否则,现在就去我们的 GitHub 页面克隆文件:

    df_census = pd.read_csv('census_cleaned.csv')
    
  3. 将数据上传到 DataFrame 后,声明预测变量和目标列Xy,如下所示:

    X = df_census.iloc[:,:-1]
    y = df_census.iloc[:,-1]
    
  4. 接下来,导入train_test_split,以random_state=2的方式将数据分割为训练集和测试集,以确保结果一致:

    from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    

    与其他机器学习分类器一样,在使用决策树时,我们初始化模型,使用训练集进行训练,并使用accuracy_score进行测试。

accuracy_score确定的是正确预测的数量与总预测数量之比。如果 20 次预测中有 19 次正确,那么accuracy_score为 95%。

首先,导入DecisionTreeClassifieraccuracy_score

from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score

接下来,我们按照标准步骤构建一个决策树分类器:

  1. 初始化一个机器学习模型,设置random_state=2以确保结果一致:

    clf = DecisionTreeClassifier(random_state=2)
    
  2. 在训练集上拟合模型:

    clf.fit(X_train, y_train)
    
  3. 对测试集进行预测:

    y_pred = clf.predict(X_test)
    
  4. 将预测结果与测试集进行比较:

    accuracy_score(y_pred, y_test) 
    

    accuracy_score如下:

    0.8131679154894976
    

81%的准确率与同一数据集中的逻辑回归模型在第一章中的准确率相当,机器学习领域

现在你已经看到如何构建决策树,接下来让我们深入看看决策树内部。

决策树内部

决策树具有很好的可视化效果,可以揭示其内部工作原理。

这是一个来自人口普查数据集的决策树,只有两个分裂:

图 2.1 – 人口普查数据集决策树

图 2.1 – 人口普查数据集决策树

树的顶部是根节点,True/False箭头是分支,数据点是节点。在树的末端,节点被归类为叶子节点。让我们深入研究前面的图示。

根节点

树的根节点位于顶部。第一行显示 marital-status_Married-civ-spouse <=5marital-status 是一个二值列,因此所有值要么是 0(负类),要么是 1(正类)。第一次分割是基于一个人是否已婚。树的左侧是True分支,表示用户未婚,右侧是False分支,表示用户已婚。

Gini 系数

根节点的第二行显示:Gini 系数为 0 意味着没有错误。Gini 系数为 1 意味着所有的预测都是错误的。Gini 系数为 0.5 表示元素均匀分布,这意味着预测效果不比随机猜测好。越接近 0,错误率越低。在根节点处,Gini 系数为 0.364,意味着训练集不平衡,类别 1 占比 36.4%。

Gini 系数的公式如下:

图 2.2 – Gini 系数公式

图 2.2 – Gini 系数公式

是分割结果为正确值的概率,c 是类别的总数:在前面的例子中是 2。另一种解释是, 是集合中具有正确输出标签的项的比例。

样本、值、类别

树的根节点显示总共有 24,420 个样本。这是训练集中的样本总数。接下来的行显示 [18575 , 5845]。排序为 0 和 1,因此 18,575 个样本的值为 0(收入少于 50K),而 5,845 个样本的值为 1(收入超过 50K)。

True/False 节点

跟随第一条分支,你会看到左侧是True,右侧是False。True 在左,False 在右的模式贯穿整个树。

在第二行的左侧节点中,分割条件 capital_gain <= 7073.5 被应用到后续节点。其余的信息来自上一个分支的分割。对于 13,160 个未婚的人,12,311 人的收入低于 50K,而 849 人的收入高于 50K。此时的 Gini 系数 0.121,是一个非常好的得分。

树桩

一棵树有可能只有一个分割,这样的树叫做树桩。虽然树桩本身不是强大的预测器,但当用作提升器时,树桩可以变得非常强大,这在第四章《从梯度提升到 XGBoost》中有详细讨论。

叶节点

树的末端节点是叶节点,叶节点包含所有最终的预测结果。

最左侧的叶节点的 Gini 系数为 0.093,正确预测了 12,304 个中的 12,938 个案例,准确率为 95%。我们有 95% 的信心认为,资本收益低于 7,073.50 的未婚用户年收入不超过 50K。

其他叶节点可以类似地解释。

现在我们来看看这些预测在哪些地方出错。

对比方差和偏差

假设你有如下图所示的数据点。你的任务是拟合一条直线或曲线,以便可以对新的数据点进行预测。

这是一个随机点的图:

图 2.3 – 随机点图

图 2.3 – 随机点图

一种方法是使用线性回归,通过最小化每个点与直线之间的距离平方,来拟合数据,如下图所示:

图 2.4 – 使用线性回归最小化距离

图 2.4 – 使用线性回归最小化距离

直线通常具有较高的偏差。在机器学习中,偏差是一个数学术语,表示在将模型应用于现实问题时对误差的估计。直线的偏差较高,因为预测结果仅限于直线,未能考虑数据的变化。

在许多情况下,直线的复杂度不足以进行准确的预测。当发生这种情况时,我们说机器学习模型存在高偏差的欠拟合。

第二个选项是使用八次多项式拟合这些点。由于只有九个点,八次多项式将完美拟合这些数据,正如你在以下图形中看到的那样:

图 2.5 – 八次多项式

图 2.5 – 八次多项式

这个模型具有较高的方差。在机器学习中,方差是一个数学术语,表示给定不同的训练数据集时,模型变化的程度。严格来说,方差是随机变量与其均值之间的平方偏差的度量。由于训练集中有九个不同的数据点,八次多项式将会完全不同,从而导致高方差。

高方差的模型通常会过拟合数据。这些模型无法很好地推广到新的数据点,因为它们过于贴合训练数据。

在大数据的世界里,过拟合是一个大问题。更多的数据导致更大的训练集,而像决策树这样的机器学习模型会过度拟合训练数据。

作为最终选项,考虑使用三次多项式拟合数据点,如下图所示:

图 2.6 – 三次多项式

图 2.6 – 三次多项式

这个三次多项式在方差和偏差之间提供了良好的平衡,一般跟随曲线,但又能适应变化。低方差意味着不同的训练集不会导致曲线发生显著变化。低偏差表示在将该模型应用于实际问题时,误差不会太高。在机器学习中,低方差和低偏差的结合是理想的。

达到方差和偏差之间良好平衡的最佳机器学习策略之一是微调超参数。

调整决策树的超参数

超参数与参数不同。

在机器学习中,参数是在模型调整过程中进行调整的。例如,线性回归和逻辑回归中的权重就是在构建阶段调整的参数,目的是最小化误差。相比之下,超参数是在构建阶段之前选择的。如果没有选择超参数,则使用默认值。

决策树回归器

学习超参数的最佳方式是通过实验。虽然选择超参数范围背后有理论依据,但结果胜过理论。不同的数据集在不同的超参数值下会有不同的改进效果。

在选择超参数之前,让我们先使用DecisionTreeRegressorcross_val_score找到一个基准分数,步骤如下:

  1. 下载'bike_rentals_cleaned'数据集并将其拆分为X_bikes(预测变量)和y_bikes(训练数据):

    df_bikes = pd.read_csv('bike_rentals_cleaned.csv')X_bikes = df_bikes.iloc[:,:-1]y_bikes = df_bikes.iloc[:,-1]
    
  2. 导入DecisionTreeRegressorcross_val_score

    from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import cross_val_score
    
  3. 初始化DecisionTreeRegressor并在cross_val_score中拟合模型:

    reg = DecisionTreeRegressor(random_state=2)
    scores = cross_val_score(reg, X_bikes, y_bikes, scoring='neg_mean_squared_error', cv=5)
    
  4. 计算1233.36。这比在第一章**机器学习概况中通过线性回归得到的972.06以及通过 XGBoost 得到的887.31还要差。

模型是否因为方差太高而导致过拟合?

这个问题可以通过查看决策树在训练集上的预测效果来回答。以下代码检查训练集的误差,然后再对测试集进行预测:

reg = DecisionTreeRegressor()reg.fit(X_train, y_train)y_pred = reg.predict(X_train)
from sklearn.metrics import mean_squared_error reg_mse = mean_squared_error(y_train, y_pred)reg_rmse = np.sqrt(reg_mse)reg_rmse

结果如下:

0.0

一个0.0的 RMSE 意味着模型已经完美拟合了每一个数据点!这个完美的分数,再加上1233.36的交叉验证误差,证明决策树在数据上出现了过拟合,且方差很高。训练集完美拟合,但测试集表现很差。

超参数可能会解决这个问题。

一般而言的超参数

所有 scikit-learn 模型的超参数详细信息可以在 scikit-learn 的官方文档页面查看。

这是来自 DecisionTreeRegressor 网站的摘录(scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.html)。

注意

sklearnscikit-learn的缩写。

图 2.7. DecisionTreeRegressor 官方文档页面摘录

图 2.7. DecisionTreeRegressor 官方文档页面摘录

官方文档解释了超参数背后的含义。请注意,这里的Parameters超参数的缩写。在自己工作时,查看官方文档是最可靠的资源。

让我们逐个讨论超参数。

max_depth

max_depth 定义了树的深度,取决于分割的次数。默认情况下,max_depth 没有限制,这可能会导致成百上千次的分割,进而引起过拟合。通过限制 max_depth 为较小的值,可以减少方差,使模型在新数据上具有更好的泛化能力。

如何选择 max_depth 的最佳值?

您总是可以尝试 max_depth=1,然后是 max_depth=2,然后是 max_depth=3,依此类推,但这个过程会很累人。相反,您可以使用一个叫做 GridSearchCV 的神奇工具。

GridSearchCV

GridSearchCV 使用交叉验证搜索超参数网格,以提供最佳结果。

GridSearchCV 像任何机器学习算法一样工作,这意味着它在训练集上进行拟合,并在测试集上进行评分。主要的不同点是 GridSearchCV 在最终确定模型之前会检查所有超参数。

GridSearchCV 的关键是建立一个超参数值的字典。没有一个正确的值集可以尝试。一种策略是选择一个最小值和一个最大值,并在两者之间均匀分布数字。由于我们试图减少过拟合,通常的做法是为 max_depth 在较低端尝试更多的值。

导入 GridSearchCV 并按如下方式定义 max_depth 的超参数列表:

from sklearn.model_selection import GridSearchCV params = {'max_depth':[None,2,3,4,6,8,10,20]}

params 字典包含一个键 'max_depth',它是一个字符串,以及一个我们选择的数字列表。请注意,None 是默认值,这意味着 max_depth 没有限制。

提示

一般来说,减少最大超参数并增加最小超参数将减少变化并防止过拟合。

接下来,初始化一个 DecisionTreeRegressor,并将其放入 GridSearchCV 中,与 params 和评分标准一起使用:

reg = DecisionTreeRegressor(random_state=2)grid_reg = GridSearchCV(reg, params, scoring='neg_mean_squared_error', cv=5, n_jobs=-1)grid_reg.fit(X_train, y_train)

现在,GridSearchCV 已经在数据上进行了拟合,您可以按如下方式查看最佳超参数:

best_params = grid_reg.best_params_print("Best params:", best_params)

结果如下:

Best params: {'max_depth': 6}

如您所见,max_depth 值为 6 在训练集上获得了最佳交叉验证分数。

训练分数可以通过 best_score 属性显示:

best_score = np.sqrt(-grid_reg.best_score_)print("Training score: {:.3f}".format(best_score))

分数如下:

Training score: 951.938

测试分数可以如下显示:

best_model = grid_reg.best_estimator_
y_pred = best_model.predict(X_test) 
rmse_test = mean_squared_error(y_test, y_pred)**0.5
print('Test score: {:.3f}'.format(rmse_test))

分数如下:

Test score: 864.670

方差已经大大减少。

min_samples_leaf

min_samples_leaf 通过增加叶子节点可能包含的样本数量来提供限制。与 max_depth 类似,min_samples_leaf 旨在减少过拟合。

当没有限制时,min_samples_leaf=1 是默认值,这意味着叶子节点可能包含唯一的样本(容易导致过拟合)。增加 min_samples_leaf 可以减少方差。如果 min_samples_leaf=8,那么所有叶子节点必须包含至少八个样本。

测试 min_samples_leaf 的一系列值需要经过与之前相同的过程。我们写一个函数,使用 GridSearchCVDecisionTreeRegressor(random_state=2) 作为默认参数 reg 来显示最佳参数、训练分数和测试分数,而不是复制和粘贴:

def grid_search(params, reg=DecisionTreeRegressor(random_state=2)):
    grid_reg = GridSearchCV(reg, params,   
    scoring='neg_mean_squared_error', cv=5, n_jobs=-1):
    grid_reg.fit(X_train, y_train)
       best_params = grid_reg.best_params_    print("Best params:", best_params)    best_score = np.sqrt(-grid_reg.best_score_)    print("Training score: {:.3f}".format(best_score))
    y_pred = grid_reg.predict(X_test)    rmse_test = mean_squared_error(y_test, y_pred)**0.5
    print('Test score: {:.3f}'.format(rmse_test))

提示

在编写自己的函数时,包含默认关键字参数是有利的。默认关键字参数是一个带有默认值的命名参数,可以在后续使用和测试时修改。默认关键字参数大大增强了 Python 的功能。

在选择超参数的范围时,了解构建模型的训练集大小是非常有帮助的。Pandas 提供了一个很好的方法,.shape,它返回数据的行和列:

X_train.shape

数据的行和列如下:

(548, 12)

由于训练集有 548 行数据,这有助于确定 min_samples_leaf 的合理值。我们试试 [1, 2, 4, 6, 8, 10, 20, 30] 作为 grid_search 的输入:

grid_search(params={'min_samples_leaf':[1, 2, 4, 6, 8, 10, 20, 30]})

分数如下:

Best params: {'min_samples_leaf': 8}
Training score: 896.083
Test score: 855.620

由于测试分数高于训练分数,因此方差已减少。

当我们将 min_samples_leafmax_depth 放在一起时,会发生什么呢?我们来看一下:

grid_search(params={'max_depth':[None,2,3,4,6,8,10,20],'min_samples_leaf':[1,2,4,6,8,10,20,30]})

分数如下:

Best params: {'max_depth': 6, 'min_samples_leaf': 2}
Training score: 870.396
Test score: 913.000

结果可能会让人惊讶。尽管训练分数提高了,但测试分数却没有变化。min_samples_leaf8 降低到 2,而 max_depth 保持不变。

提示

这是一个有关超参数调优的重要经验:超参数不应单独选择。

至于减少前面示例中的方差,将 min_samples_leaf 限制为大于三的值可能会有所帮助:

grid_search(params={'max_depth':[6,7,8,9,10],'min_samples_leaf':[3,5,7,9]})

分数如下:

Best params: {'max_depth': 9, 'min_samples_leaf': 7}
Training score: 888.905
Test score: 878.538

如你所见,测试分数已提高。

现在,我们将探索其余的决策树超参数,而无需单独测试。

max_leaf_nodes

max_leaf_nodes 类似于 min_samples_leaf。不同之处在于,它指定了叶子的总数,而不是每个叶子的样本数。因此,max_leaf_nodes=10 意味着模型不能有超过 10 个叶子,但可以少于 10 个。

max_features

max_features 是减少方差的有效超参数。它并不是考虑所有可能的特征进行分裂,而是每次选择一部分特征进行分裂。

max_features 常见的选项如下:

  • 'auto' 是默认选项,没有任何限制。

  • 'sqrt' 是特征总数的平方根。

  • 'log2' 是特征总数的以 2 为底的对数。32 列特征对应 5,因为 2⁵ = 32。

min_samples_split

另一种分割技术是 min_samples_split。顾名思义,min_samples_split 为进行分裂之前要求的样本数设定了限制。默认值为 2,因为两个样本可以被分成各自的单个叶子。如果将限制增加到 5,那么对于包含五个样本或更少的节点,不再允许进一步的分裂。

splitter

splitter有两个选项,'random''best'。Splitter 告诉模型如何选择分裂每个分支的特征。默认的'best'选项选择能带来最大信息增益的特征。与此相对,'random'选项会随机选择分裂。

splitter更改为'random'是防止过拟合并使树结构多样化的好方法。

criterion

用于决策树回归器和分类器分裂的criterion是不同的。criterion提供了机器学习模型用于决定如何进行分裂的方法。这是分裂的评分方法。对于每个可能的分裂,criterion计算一个分裂的数值,并将其与其他选项进行比较。分裂得分最高的选项会被选中。

对于决策树回归器,选项有mse(均方误差)、friedman_mse(包括 Friedman 调整)和mae(平均绝对误差)。默认值是mse

对于分类器,之前提到的ginientropy通常会给出相似的结果。

min_impurity_decrease

以前被称为min_impurity_splitmin_impurity_decrease在不纯度大于或等于该值时会进行分裂。

不纯度是衡量每个节点预测纯度的标准。一个具有 100%准确率的树的不纯度为 0.0。一个具有 80%准确率的树的不纯度为 0.20。

不纯度是决策树中的一个重要概念。在整个树构建过程中,不纯度应不断减少。每个节点选择的分裂是那些能最大程度降低不纯度的分裂。

默认值为0.0。这个数值可以增大,以便树在达到某个阈值时停止构建。

min_weight_fraction_leaf

min_weight_fraction_leaf是成为叶节点所需的最小权重占总权重的比例。根据文档,当未提供 sample_weight 时,样本具有相等的权重

从实际应用角度来看,min_weight_fraction_leaf是另一个可以减少方差并防止过拟合的超参数。默认值是 0.0。假设权重相等,1%的限制,即 0.01,将要求至少有 500 个样本中的 5 个作为叶节点。

ccp_alpha

ccp_alpha超参数在此不进行讨论,因为它是用于树构建后修剪的。欲了解更多内容,请参阅最小成本复杂度修剪:scikit-learn.org/stable/modules/tree.html#minimal-cost-complexity-pruning

将所有内容整合在一起

在微调超参数时,涉及几个因素:

  • 分配的时间量

  • 超参数的数量

  • 所需的精度小数位数

所花费的时间、调优的超参数数量以及期望的准确度取决于你、数据集和手头的项目。由于超参数相互关联,并不要求修改所有超参数。调整较小范围的超参数可能会带来更好的结果。

现在你已经理解了决策树和决策树超参数的基本原理,是时候应用你所学到的知识了。

提示

决策树的超参数太多,无法始终如一地使用所有超参数。根据我的经验,max_depthmax_featuresmin_samples_leafmax_leaf_nodesmin_impurity_decreasemin_samples_split 通常已经足够。

预测心脏病——案例研究

医院要求你使用机器学习来预测心脏病。你的任务是开发一个模型,并突出显示医生和护士可以专注的两个到三个重要特征,以改善患者健康。

你决定使用经过调优的超参数的决策树分类器。在构建模型后,你将使用feature_importances_属性来解释结果,该属性确定了预测心脏病时最重要的特征。

心脏病数据集

心脏病数据集已上传至 GitHub,文件名为heart_disease.csv。这是对原始心脏病数据集(archive.ics.uci.edu/ml/datasets/Heart+Disease)的轻微修改,由 UCI 机器学习库(archive.ics.uci.edu/ml/index.php)提供,并已清理了空值,方便使用。

上传文件并显示前五行,如下所示:

df_heart = pd.read_csv('heart_disease.csv')df_heart.head()

上述代码会生成如下表格:

图 2.8 – heart_disease.csv 输出

图 2.8 – heart_disease.csv 输出

目标列,便捷地标记为target,是二值型的,其中1表示患者患有心脏病,0表示患者没有心脏病。

以下是从之前链接的数据源中提取的预测变量列的含义:

  • age:年龄(单位:年)

  • sex:性别(1 = 男性;0 = 女性)

  • cp:胸痛类型(1 = 典型心绞痛,2 = 非典型心绞痛,3 = 非心绞痛性疼痛,4 = 无症状)

  • trestbps:静息血压(单位:mm Hg,入院时测量)

  • chol:血清胆固醇(单位:mg/dl) fbs:(空腹血糖 > 120 mg/dl)(1 = 是;0 = 否)

  • fbs:空腹血糖 > 120 mg/dl(1 = 是;0 = 否)

  • restecg:静息心电图结果(0 = 正常,1 = 存在 ST-T 波异常(T 波倒置和/或 ST 段抬高或低于 0.05 mV),2 = 根据 Estes 标准显示可能或确诊的左心室肥厚)

  • thalach:最大心率

  • exang:运动诱发的心绞痛(1 = 是;0 = 否)

  • oldpeak:运动诱发的 ST 段压低(与静息状态相比)

  • slope:最大运动 ST 段的坡度(1 = 上升,2 = 平坦,3 = 下降)

  • ca: 主要血管数量(0-3),由透视法显示

  • thal: 3 = 正常;6 = 固定缺陷;7 = 可逆缺陷

将数据分为训练集和测试集,为机器学习做准备:

X = df_heart.iloc[:,:-1]y = df_heart.iloc[:,-1]from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)

你现在已经准备好进行预测了。

决策树分类器

在实现超参数之前,拥有一个基准模型进行比较是很有帮助的。

使用cross_val_scoreDecisionTreeClassifier如下:

model = DecisionTreeClassifier(random_state=2)
scores = cross_val_score(model, X, y, cv=5)
print('Accuracy:', np.round(scores, 2))
print('Accuracy mean: %0.2f' % (scores.mean()))
Accuracy: [0.74 0.85 0.77 0.73 0.7 ]

结果如下:

Accuracy mean: 0.76

初始准确率为 76%。我们来看看通过超参数微调可以获得多少提升。

随机搜索分类器函数

当调整多个超参数时,GridSearchCV可能需要过长时间。scikit-learn 库提供了RandomizedSearchCV作为一个很好的替代方案。RandomizedSearchCVGridSearchCV的工作方式相同,但它并不是尝试所有的超参数,而是尝试一个随机数量的组合。它并不是要穷举所有组合,而是要在有限的时间内找到最佳的组合。

这是一个使用RandomizedSearchCV的函数,它返回最佳模型及其得分。输入是params(待测试的超参数字典)、runs(要检查的超参数组合数)和DecisionTreeClassifier

def randomized_search_clf(params, runs=20, clf=DecisionTreeClassifier(random_state=2)):    rand_clf = RandomizedSearchCV(clf, params, n_iter=runs,    cv=5, n_jobs=-1, random_state=2)    rand_clf.fit(X_train, y_train)
    best_model = rand_clf.best_estimator_
    best_score = rand_clf.best_score_  
    print("Training score: {:.3f}".format(best_score))
    y_pred = best_model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    print('Test score: {:.3f}'.format(accuracy))
    return best_model

现在,让我们选择一个超参数范围。

选择超参数

选择超参数没有单一的正确方法。实验才是关键。这里是一个初步的列表,放入randomized_search_clf函数中。这些数值的选择旨在减少方差,并尝试一个广泛的范围:

randomized_search_clf(params={'criterion':['entropy', 'gini'],'splitter':['random', 'best'], 'min_weight_fraction_leaf':[0.0, 0.0025, 0.005, 0.0075, 0.01],'min_samples_split':[2, 3, 4, 5, 6, 8, 10],'min_samples_leaf':[1, 0.01, 0.02, 0.03, 0.04],'min_impurity_decrease':[0.0, 0.0005, 0.005, 0.05, 0.10, 0.15, 0.2],'max_leaf_nodes':[10, 15, 20, 25, 30, 35, 40, 45, 50, None],'max_features':['auto', 0.95, 0.90, 0.85, 0.80, 0.75, 0.70],'max_depth':[None, 2,4,6,8],'min_weight_fraction_leaf':[0.0, 0.0025, 0.005, 0.0075, 0.01, 0.05]})
Training score: 0.798
Test score: 0.855
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=8, max_features=0.8, max_leaf_nodes=45, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=0.04, min_samples_split=10,min_weight_fraction_leaf=0.05, presort=False, random_state=2, splitter='best')

这是一个显著的改进,且该模型在测试集上具有良好的泛化能力。我们来看看通过缩小范围能否做到更好。

缩小范围

缩小范围是改善超参数的一种策略。

例如,使用从最佳模型中选择的max_depth=8作为基准,我们可以将范围缩小到79

另一种策略是停止检查那些默认值已经表现良好的超参数。例如,entropy相较于'gini'的差异非常小,因此不推荐使用entropymin_impurity_splitmin_impurity_decrease也可以保留默认值。

这是一个新的超参数范围,增加了100次运行:

randomized_search_clf(params={'max_depth':[None, 6, 7],'max_features':['auto', 0.78], 'max_leaf_nodes':[45, None], 'min_samples_leaf':[1, 0.035, 0.04, 0.045, 0.05],'min_samples_split':[2, 9, 10],'min_weight_fraction_leaf': [0.0, 0.05, 0.06, 0.07],}, runs=100)
Training score: 0.802
Test score: 0.868
DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=7,max_features=0.78, max_leaf_nodes=45, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=0.045, min_samples_split=9, min_weight_fraction_leaf=0.06, presort=False, random_state=2, splitter='best')

这个模型在训练和测试得分上更为准确。

然而,为了进行适当的比较基准,必须将新模型放入cross_val_clf中。可以通过复制并粘贴前面的模型来实现:

model = DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=7, max_features=0.78, max_leaf_nodes=45, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=0.045, min_samples_split=9, min_weight_fraction_leaf=0.06, presort=False, random_state=2, splitter='best')
scores = cross_val_score(model, X, y, cv=5)
print('Accuracy:', np.round(scores, 2))
print('Accuracy mean: %0.2f' % (scores.mean()))
Accuracy: [0.82 0.9  0.8  0.8  0.78]

结果如下:

Accuracy mean: 0.82

这比默认模型高出六个百分点。在预测心脏病时,更高的准确性能够挽救生命。

feature_importances_

拼图的最后一块是传达机器学习模型中最重要的特征。决策树有一个非常有用的属性,feature_importances_,它正是用于显示这些重要特征的。

首先,我们需要最终确定最佳模型。我们的函数已经返回了最佳模型,但它尚未被保存。

在测试时,重要的是不要混用训练集和测试集。然而,在选择最终模型后,将模型拟合到整个数据集上可能是有益的。为什么呢?因为目标是测试模型在从未见过的数据上的表现,且将模型拟合到整个数据集可能会提高准确性。

让我们使用最佳超参数定义模型,并将其拟合到整个数据集上:

best_clf = DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=9,max_features=0.8, max_leaf_nodes=47,min_impurity_decrease=0.0, min_impurity_split=None,min_samples_leaf=1, min_samples_split=8,min_weight_fraction_leaf=0.05, presort=False,random_state=2, splitter='best')
best_clf.fit(X, y)

为了确定最重要的特征,我们可以在best_clf上运行feature_importances_属性:

best_clf.feature_importances_
array([0.04826754, 0.04081653, 0.48409586, 0.00568635, 0.        , 0., 0., 0.00859483, 0., 0.02690379, 0., 0.18069065, 0.20494446])

解释这些结果并不容易。以下代码会将列与最重要的特征一起压缩成字典,然后按相反的顺序显示,以便清晰地输出并容易理解:

feature_dict = dict(zip(X.columns, best_clf.feature_importances_))
# Import operator import operator
Sort dict by values (as list of tuples)sorted(feature_dict.items(), key=operator.itemgetter(1), reverse=True)[0:3]
[('cp', 0.4840958610240171),
 ('thal', 0.20494445570568706),
 ('ca', 0.18069065321397942)]

三个最重要的特征如下:

  • 'cp':胸痛类型(1 = 典型心绞痛,2 = 非典型心绞痛,3 = 非心绞痛性疼痛,4 = 无症状)

  • 'thalach':最大心率

  • 'ca':通过透视染色的主要血管数量(0-3)

这些数字可以解释为它们对方差的贡献,因此'cp'解释了 48%的方差,超过了'thal''ca'的总和。

你可以告诉医生和护士,使用胸痛、最大心率和透视作为三大最重要特征,你的模型能够以 82%的准确率预测患者是否患有心脏病。

总结

在本章中,通过研究决策树这一 XGBoost 的基础学习器,你已经在掌握 XGBoost 的道路上迈出了重要一步。你通过GridSearchCVRandomizedSearchCV微调超参数,构建了决策树回归器和分类器。你还可视化了决策树,并从方差和偏差的角度分析了它们的误差和准确性。此外,你还学会了一个不可或缺的工具——feature_importances_,它是 XGBoost 的一个属性,用于传达模型中最重要的特征。

在下一章中,你将学习如何构建随机森林,这是我们第一个集成方法,也是 XGBoost 的竞争对手。随机森林的应用对于理解 bagging 和 boosting 的区别、生成与 XGBoost 相当的机器学习模型以及了解随机森林的局限性至关重要,正是这些局限性促成了 XGBoost 的发展。

第三章:第三章:使用随机森林进行 Bagging

在本章中,你将掌握构建随机森林的技巧,随机森林是与 XGBoost 竞争的领先方法。与 XGBoost 一样,随机森林也是决策树的集成体。不同之处在于,随机森林通过bagging结合树,而 XGBoost 则通过boosting结合树。随机森林是 XGBoost 的一个可行替代方案,具有本章中强调的优点和局限性。了解随机森林非常重要,因为它们能为树基集成方法(如 XGBoost)提供宝贵的见解,并使你能够更深入地理解 boosting 与 bagging 之间的比较和对比。

在本章中,你将构建和评估随机森林分类器随机森林回归器,掌握随机森林的超参数,学习机器学习中的 bagging 技巧,并探索一个案例研究,突出随机森林的局限性,这些局限性促使了梯度提升(XGBoost)的发展。

本章涵盖以下主要内容:

  • Bagging 集成方法

  • 探索随机森林

  • 调整随机森林超参数

  • 推动随机森林的边界——案例研究

技术要求

本章的代码可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter03找到

Bagging 集成方法

在本节中,你将了解为什么集成方法通常优于单一的机器学习模型。此外,你还将学习 bagging 技巧。这两者是随机森林的重要特征。

集成方法

在机器学习中,集成方法是一种通过聚合单个模型的预测结果来构建的模型。由于集成方法结合了多个模型的结果,它们较不容易出错,因此往往表现更好。

假设你的目标是确定一栋房子是否会在上市的第一个月内售出。你运行了几个机器学习算法,发现逻辑回归的准确率为 80%,决策树为 75%,k 最近邻为 77%。

一个选项是使用逻辑回归,作为最准确的模型,作为你的最终模型。更有说服力的选项是将每个单独模型的预测结果结合起来。

对于分类器,标准选项是采用多数投票。如果至少有三个模型中的两个预测房子会在第一个月内卖掉,那么预测结果是YES。否则,预测结果是NO

使用集成方法通常能提高整体准确性。要让预测错误,仅仅一个模型出错是不够的;必须有大多数分类器都预测错误。

集成方法通常分为两类。第一类是将不同的机器学习模型组合在一起,例如用户选择的 scikit-learn 的VotingClassifier。第二类集成方法将同一模型的多个版本组合在一起,就像 XGBoost 和随机森林一样。

随机森林是所有集成方法中最流行和最广泛使用的。随机森林的单独模型是决策树,正如上一章的重点,第二章深入了解决策树。一个随机森林可能包含数百或数千棵决策树,其预测结果将被合并成最终结果。

尽管随机森林使用多数规则来分类器,使用所有模型的平均值来回归器,但它们还使用一种叫做袋装(bagging)的方法,袋装是自助聚合(bootstrap aggregation)的缩写,用来选择单独的决策树。

自助聚合

自助法(Bootstrapping)是指带放回的抽样。

想象一下,你有一袋 20 颗有色大理石。你将依次选择 10 颗大理石,每次选择后都将其放回袋中。这意味着,虽然极不可能,但你有可能选中同一颗大理石 10 次。

你更可能多次选中一些大理石,而有些大理石则可能一次也不选中。

这里是大理石的可视化图:

图 3.1 – 袋装的可视化示意图(改绘自:Siakorn,Wikimedia Commons,https://commons.wikimedia.org/wiki/File:Ensemble_Bagging.svg)

图 3.1 – 袋装的可视化示意图(改绘自:Siakorn,Wikimedia Commons,commons.wikimedia.org/wiki/File:Ensemble_Bagging.svg

从上面的示意图可以看出,自助样本是通过带放回的抽样获得的。如果大理石没有被放回,那么就不可能得到比原始袋中更多黑色(原图中的蓝色)大理石的样本,正如最右侧的框所示。

在随机森林中,自助法在幕后发挥作用。自助法发生在每棵决策树建立时。如果所有的决策树都由相同的样本组成,那么这些树会给出类似的预测,最终结果也会与单棵树的预测相似。而在随机森林中,树是通过自助法建立的,通常样本数与原始数据集相同。数学估计表明,每棵树中约三分之二的样本是唯一的,三分之一包含重复样本。

在模型建立的自助法阶段后,每棵决策树会做出自己的单独预测。结果是由多棵树组成的森林,这些树的预测结果会根据分类器的多数规则和回归器的平均值合并成最终预测。

总结一下,随机森林聚合了自助法生成的决策树的预测。这种通用的集成方法在机器学习中被称为自助法(bagging)。

探索随机森林

为了更好地了解随机森林的工作原理,让我们使用 scikit-learn 构建一个随机森林。

随机森林分类器

让我们使用一个随机森林分类器,使用我们在第一章机器学习概览中清理和评分的普查数据集,预测用户收入是否超过 50,000 美元,并在第二章决策树深入剖析中重新检查。我们将使用cross_val_score确保我们的测试结果具有良好的泛化能力:

以下步骤使用普查数据集构建并评分一个随机森林分类器:

  1. 导入pandasnumpyRandomForestClassifiercross_val_score,然后关闭警告:

    import pandas as pd
    import numpy as np
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import cross_val_score
    import warnings
    warnings.filterwarnings('ignore')
    
  2. 加载数据集census_cleaned.csv并将其拆分为X(预测变量)和y(目标变量):

    df_census = pd.read_csv('census_cleaned.csv')
    X_census = df_census.iloc[:,:-1]
    y_census = df_census.iloc[:,-1]
    

    在准备好导入和数据后,现在是时候构建模型了。

  3. 接下来,我们初始化随机森林分类器。在实践中,集成算法与其他机器学习算法一样工作。一个模型被初始化、拟合训练数据,并在测试数据上评分。

    我们通过提前设置以下超参数来初始化随机森林:

    a) random_state=2确保你的结果与我们的结果一致。

    b) n_jobs=-1通过利用并行处理加速计算。

    c) n_estimators=10,这是 scikit-learn 的默认值,足以加速计算并避免歧义;新的默认值已设置为n_estimators=100n_estimators将在下一节中详细探讨:

    rf = RandomForestClassifier(n_estimators=10, random_state=2, n_jobs=-1)
    
  4. 现在我们将使用cross_val_scorecross_val_score需要一个模型、预测列和目标列作为输入。回顾一下,cross_val_score会对数据进行拆分、拟合和评分:

    scores = cross_val_score(rf, X_census, y_census, cv=5)
    
  5. 显示结果:

    print('Accuracy:', np.round(scores, 3))
    print('Accuracy mean: %0.3f' % (scores.mean()))
    Accuracy: [0.851 0.844 0.851 0.852 0.851]
    Accuracy mean: 0.850
    

默认的随机森林分类器在第二章决策树深入剖析(81%)的数据集上,比决策树表现更好,但还不如第一章机器学习概览(86%)中的 XGBoost。为什么它比单一的决策树表现更好?

性能提升可能与上一节中描述的自助法(bagging)有关。在这个森林中有 10 棵树(因为n_estimators=10),每个预测是基于 10 棵决策树,而不是 1 棵。树是通过自助法生成的,这增加了多样性,并通过聚合减少了方差。

默认情况下,随机森林分类器在寻找分裂时会从特征总数的平方根中选择特征。因此,如果有 100 个特征(列),每棵决策树在选择分裂时只会考虑 10 个特征。因此,两个样本重复的树可能由于分裂的不同而给出完全不同的预测。这是随机森林减少方差的另一种方式。

除了分类,随机森林还可以用于回归。

随机森林回归器

在随机森林回归器中,样本是通过自助法(bootstrap)抽取的,和随机森林分类器一样,但最大特征数是特征总数,而不是平方根。这个变化是基于实验结果(参见 orbi.uliege.be/bitstream/2268/9357/1/geurts-mlj-advance.pdf)。

此外,最终的预测是通过对所有决策树的预测结果求平均得出的,而不是通过多数规则投票。

为了查看随机森林回归器的实际应用,请完成以下步骤:

  1. 第二章《决策树深度剖析》上传自行车租赁数据集,并提取前五行以供回顾:

    df_bikes = pd.read_csv('bike_rentals_cleaned.csv')
    df_bikes.head()
    

    上述代码应生成以下输出:

    图 3.2 – 自行车租赁数据集 – 已清理

    图 3.2 – 自行车租赁数据集 – 已清理

  2. 将数据划分为Xy,即预测列和目标列:

    X_bikes = df_bikes.iloc[:,:-1]
    y_bikes = df_bikes.iloc[:,-1]
    
  3. 导入回归器,然后使用相同的默认超参数进行初始化,n_estimators=10random_state=2n_jobs=-1

    from sklearn.ensemble import RandomForestRegressor
    rf = RandomForestRegressor(n_estimators=10, random_state=2, n_jobs=-1)
    
  4. 现在我们需要使用cross_val_score。将回归器rf与预测器和目标列一起放入cross_val_score。请注意,负均方误差('neg_mean_squared_error')应定义为评分参数。选择 10 折交叉验证(cv=10):

    scores = cross_val_score(rf, X_bikes, y_bikes, scoring='neg_mean_squared_error', cv=10)
    
  5. 查找并显示均方根误差RMSE):

    rmse = np.sqrt(-scores)
    print('RMSE:', np.round(rmse, 3))
    print('RMSE mean: %0.3f' % (rmse.mean()))
    

    输出如下:

    RMSE: [ 801.486  579.987  551.347  846.698  895.05  1097.522   893.738  809.284  833.488 2145.046]
    RMSE mean: 945.365
    

随机森林的表现令人满意,尽管不如我们之前看到的其他模型。我们将在本章后面的案例研究中进一步分析自行车租赁数据集,以了解原因。

接下来,我们将详细查看随机森林的超参数。

随机森林超参数

随机森林超参数的范围很大,除非已经具备决策树超参数的工作知识,如在第二章《决策树深度剖析》中所讨论的那样。

在本节中,我们将在介绍您已见过的超参数分组之前,讨论一些额外的随机森林超参数。许多超参数将被 XGBoost 使用。

oob_score

我们的第一个超参数,可能也是最引人注目的,是oob_score

随机森林通过袋装(bagging)选择决策树,这意味着样本是带替换地选取的。所有样本选择完后,应该会有一些未被选择的样本。

可以将这些样本作为测试集保留。在模型拟合完一棵树后,模型可以立即用这个测试集进行评分。当超参数设置为oob_score=True时,正是发生了这种情况。

换句话说,oob_score提供了一种获取测试分数的快捷方式。在模型拟合后,可以立即打印出oob_score

让我们在普查数据集上使用oob_score,看看它在实践中的表现。由于我们使用oob_score来测试模型,因此不需要将数据拆分为训练集和测试集。

随机森林可以像往常一样初始化,设置oob_score=True

rf = RandomForestClassifier(oob_score=True, n_estimators=10, random_state=2, n_jobs=-1)

接下来,可以在数据上拟合rf

rf.fit(X_census, y_census)

由于oob_score=True,在模型拟合后可以获得分数。可以通过模型的属性.oob_score_来访问分数,如下所示(注意score后有下划线):

rf.oob_score_

分数如下:

0.8343109855348423

如前所述,oob_score是通过对训练阶段被排除的个别树上的样本进行评分生成的。当森林中的树木数量较少时(例如使用 10 个估计器),可能没有足够的测试样本来最大化准确度。

更多的树意味着更多的样本,通常也意味着更高的准确性。

n_estimators

当森林中有很多树时,随机森林的效果非常强大。那么多少棵树才足够呢?最近,scikit-learn 的默认设置已从 10 改为 100。虽然 100 棵树可能足够减少方差并获得良好的分数,但对于较大的数据集,可能需要 500 棵或更多的树。

让我们从n_estimators=50开始,看看oob_score是如何变化的:

rf = RandomForestClassifier(n_estimators=50, oob_score=True, random_state=2, n_jobs=-1)
rf.fit(X_census, y_census)
rf.oob_score_

分数如下:

0.8518780135745216

有了明显的提升。那么 100 棵树呢?

rf = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=2, n_jobs=-1)
rf.fit(X_census, y_census)
rf.oob_score_

分数如下:

0.8551334418476091

增益较小。随着n_estimators的不断增加,分数最终会趋于平稳。

warm_start

warm_start超参数非常适合确定森林中树的数量(n_estimators)。当warm_start=True时,添加更多树木不需要从头开始。如果将n_estimators从 100 改为 200,构建 200 棵树的森林可能需要两倍的时间。使用warm_start=True时,200 棵树的随机森林不会从头开始,而是从先前模型停止的位置继续。

warm_start可以用来绘制不同n_estimators值下的各种分数。

作为示例,以下代码每次增加 50 棵树,从 50 开始,到 500 结束,显示一系列分数。由于每轮都在通过添加 50 棵新树来构建 10 个随机森林,这段代码可能需要一些时间才能完成运行!代码按以下步骤分解:

  1. 导入 matplotlib 和 seaborn,然后通过sns.set()设置 seaborn 的暗色网格:

    import matplotlib.pyplot as plt
    import seaborn as sns
    sns.set()
    
  2. 初始化一个空的分数列表,并用 50 个估计器初始化随机森林分类器,确保warm_start=Trueoob_score=True

    oob_scores = []
    rf = RandomForestClassifier(n_estimators=50, warm_start=True, oob_score=True, n_jobs=-1, random_state=2)
    
  3. rf拟合到数据集上,然后将oob_score添加到oob_scores列表中:

    rf.fit(X_census, y_census)
    oob_scores.append(rf.oob_score_)
    
  4. 准备一个估计器列表,其中包含从 50 开始的树的数量:

    est = 50
    estimators=[est]
    
  5. 写一个 for 循环,每轮添加 50 棵树。每一轮,向 est 添加 50, 将 est 附加到 estimators 列表中,使用 rf.set_params(n_estimators=est) 更改 n_estimators,在数据上拟合随机森林,然后附加新的 oob_score_

    for i in range(9):
        est += 50
        estimators.append(est)
        rf.set_params(n_estimators=est)
        rf.fit(X_census, y_census)
        oob_scores.append(rf.oob_score_)
    
  6. 为了更好的展示,显示一个更大的图表,然后绘制估计器和 oob_scores。添加适当的标签,然后保存并显示图表:

    plt.figure(figsize=(15,7))
    plt.plot(estimators, oob_scores)
    plt.xlabel('Number of Trees')
    plt.ylabel('oob_score_')
    plt.title('Random Forest Warm Start', fontsize=15)
    plt.savefig('Random_Forest_Warm_Start', dpi=325)
    plt.show()
    

    这将生成以下图表:

图 3.3 – 随机森林热启动 – 每棵树的 oob_score

图 3.3 – 随机森林热启动 – 每个树的 oob_score

如你所见,树木的数量在大约 300 时趋于峰值。使用超过 300 棵树成本较高且耗时,而且收益微乎其微。

bootstrap

尽管随机森林通常是自助法(bootstrap),但 bootstrap 超参数可以设置为 False。如果 bootstrap=False,则无法包含 oob_score,因为 oob_score 仅在样本被排除时才可能。

我们将不再继续此选项,尽管如果发生欠拟合,这个方法是合理的。

冗长

verbose 超参数可以设置为更高的数字,以在构建模型时显示更多信息。你可以自己尝试实验。当构建大型模型时,verbose=1 可以在过程中提供有用的信息。

决策树超参数

其余的超参数都来自决策树。事实证明,在随机森林中,决策树的超参数并不那么重要,因为随机森林本身通过设计减少了方差。

这里是按类别分组的决策树超参数,供您查看。

深度

属于此类别的超参数有:

  • max_depth:总是需要调整。决定分裂发生的次数,也就是树的长度。是减少方差的一个好方法。

分裂

属于此类别的超参数有:

  • max_features:限制在进行分裂时可选择的特征数。

  • min_samples_split:增加进行新分裂所需的样本数。

  • min_impurity_decrease:限制分裂以减少超过设定阈值的杂质。

叶子

属于此类别的超参数有:

  • min_samples_leaf:增加成为叶子节点所需的最小样本数。

  • min_weight_fraction_leaf:成为叶子的所需总权重的比例。

如需了解更多关于上述超参数的信息,请查阅官方随机森林回归器文档:scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html

推动随机森林的边界 – 案例研究

假设你为一家自行车租赁公司工作,目标是根据天气、一天中的时间、季节和公司的成长来预测每天的自行车租赁数量。

在本章的早期,您实现了一个带有交叉验证的随机森林回归器,得到了 945 辆自行车的 RMSE。您的目标是修改随机森林以获得尽可能低的误差得分。

准备数据集

在本章的早期,您下载了数据集df_bikes并将其分割为X_bikesy_bikes。现在,您进行一些严肃的测试,决定将X_bikesy_bikes拆分为训练集和测试集,如下所示:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_bikes, y_bikes, random_state=2)

n_estimators

首先选择一个合理的n_estimators值。回想一下,n_estimators可以增加以提高准确性,但会以计算资源和时间为代价。

以下是使用warm_start方法对多种n_estimators进行 RMSE 图形展示,所使用的代码与之前在warm_start部分提供的相同:

图 3.4 – 随机森林自行车租赁 – 每棵树的 RMSE

图 3.4 – 随机森林自行车租赁 – 每棵树的 RMSE

这个图表非常有趣。随机森林在 50 个估计器下提供了最佳得分。在 100 个估计器后,误差开始逐渐增加,这是一个稍后会重新讨论的概念。

现在,使用n_estimators=50作为起点是合理的选择。

cross_val_score

根据之前的图表,误差范围从 620 到 690 辆自行车租赁,现在是时候看看数据集在使用cross_val_score进行交叉验证时的表现了。回想一下,在交叉验证中,目的是将样本划分为k个不同的折,并在不同的折中使用所有样本作为测试集。由于所有样本都用于测试模型,oob_score将无法使用。

以下代码包含了您在本章早期使用的相同步骤:

  1. 初始化模型。

  2. 对模型进行评分,使用cross_val_score与模型、预测列、目标列、评分标准和折数作为参数。

  3. 计算 RMSE。

  4. 显示交叉验证得分和平均值。

这里是代码:

rf = RandomForestRegressor(n_estimators=50, warm_start=True, n_jobs=-1, random_state=2)
scores = cross_val_score(rf, X_bikes, y_bikes, scoring='neg_mean_squared_error', cv=10)
rmse = np.sqrt(-scores)
print('RMSE:', np.round(rmse, 3))
print('RMSE mean: %0.3f' % (rmse.mean()))

输出结果如下:

RMSE: [ 836.482  541.898  533.086  812.782  894.877  881.117   794.103  828.968  772.517 2128.148]
RMSE mean: 902.398

这个得分比本章之前的得分更好。注意,最后一个折中的误差显著更高,根据 RMSE 数组中的最后一项。这可能是由于数据中的错误或异常值所致。

微调超参数

是时候创建一个超参数网格,使用RandomizedSearchCV来微调我们的模型了。以下是一个使用RandomizedSearchCV的函数,用于显示 RMSE 和平均得分以及最佳超参数:

from sklearn.model_selection import RandomizedSearchCV
def randomized_search_reg(params, runs=16, reg=RandomForestRegressor(random_state=2, n_jobs=-1)):
    rand_reg = RandomizedSearchCV(reg, params, n_iter=runs, scoring='neg_mean_squared_error', cv=10, n_jobs=-1, random_state=2)
    rand_reg.fit(X_train, y_train)
    best_model = rand_reg.best_estimator_
    best_params = rand_reg.best_params_
    print("Best params:", best_params)
    best_score = np.sqrt(-rand_reg.best_score_)
    print("Training score: {:.3f}".format(best_score))
    y_pred = best_model.predict(X_test)
    from sklearn.metrics import mean_squared_error as MSE
    rmse_test = MSE(y_test, y_pred)**0.5
    print('Test set score: {:.3f}'.format(rmse_test))

这里是一个初学者的超参数网格,放入新的randomized_search_reg函数中以获得初步结果:

randomized_search_reg(params={'min_weight_fraction_leaf':[0.0, 0.0025, 0.005, 0.0075, 0.01, 0.05],'min_samples_split':[2, 0.01, 0.02, 0.03, 0.04, 0.06, 0.08, 0.1],'min_samples_leaf':[1,2,4,6,8,10,20,30],'min_impurity_decrease':[0.0, 0.01, 0.05, 0.10, 0.15, 0.2],'max_leaf_nodes':[10, 15, 20, 25, 30, 35, 40, 45, 50, None], 'max_features':['auto', 0.8, 0.7, 0.6, 0.5, 0.4],'max_depth':[None,2,4,6,8,10,20]})

输出结果如下:

Best params: {'min_weight_fraction_leaf': 0.0, 'min_samples_split': 0.03, 'min_samples_leaf': 6, 'min_impurity_decrease': 0.05, 'max_leaf_nodes': 25, 'max_features': 0.7, 'max_depth': None}
Training score: 759.076
Test set score: 701.802

这是一个重要的改进。让我们看看通过缩小范围是否能够得到更好的结果:

randomized_search_reg(params={'min_samples_leaf': [1,2,4,6,8,10,20,30], 'min_impurity_decrease':[0.0, 0.01, 0.05, 0.10, 0.15, 0.2],'max_features':['auto', 0.8, 0.7, 0.6, 0.5, 0.4], 'max_depth':[None,2,4,6,8,10,20]})

输出结果如下:

Best params: {'min_samples_leaf': 1, 'min_impurity_decrease': 0.1, 'max_features': 0.6, 'max_depth': 10}
Training score: 679.052
Test set score: 626.541

得分再次提高了。

现在,让我们增加运行次数,并为max_depth提供更多选项:

randomized_search_reg(params={'min_samples_leaf':[1,2,4,6,8,10,20,30],'min_impurity_decrease':[0.0, 0.01, 0.05, 0.10, 0.15, 0.2],'max_features':['auto', 0.8, 0.7, 0.6, 0.5, 0.4],'max_depth':[None,4,6,8,10,12,15,20]}, runs=20)

输出结果如下:

Best params: {'min_samples_leaf': 1, 'min_impurity_decrease': 0.1, 'max_features': 0.6, 'max_depth': 12}
Training score: 675.128
Test set score: 619.014

得分持续提升。此时,根据之前的结果,可能值得进一步缩小范围:

randomized_search_reg(params={'min_samples_leaf':[1,2,3,4,5,6], 'min_impurity_decrease':[0.0, 0.01, 0.05, 0.08, 0.10, 0.12, 0.15], 'max_features':['auto', 0.8, 0.7, 0.6, 0.5, 0.4],'max_depth':[None,8,10,12,14,16,18,20]})

输出如下:

Best params: {'min_samples_leaf': 1, 'min_impurity_decrease': 0.05, 'max_features': 0.7, 'max_depth': 18}
Training score: 679.595
Test set score: 630.954

测试分数已经回升。此时增加n_estimators可能是一个好主意。森林中的树木越多,可能带来的小幅提升也越大。

我们还可以将运行次数增加到20,以寻找更好的超参数组合。请记住,结果是基于随机搜索,而不是完全的网格搜索:

randomized_search_reg(params={'min_samples_leaf':[1,2,4,6,8,10,20,30], 'min_impurity_decrease':[0.0, 0.01, 0.05, 0.10, 0.15, 0.2], 'max_features':['auto', 0.8, 0.7, 0.6, 0.5, 0.4],'max_depth':[None,4,6,8,10,12,15,20],'n_estimators':[100]}, runs=20)

输出如下:

Best params: {'n_estimators': 100, 'min_samples_leaf': 1, 'min_impurity_decrease': 0.1, 'max_features': 0.6, 'max_depth': 12}
Training score: 675.128
Test set score: 619.014

这与迄今为止取得的最佳得分相匹配。我们可以继续调整。通过足够的实验,测试分数可能会降到低于 600 辆的水平。但我们似乎已经在 600 辆附近达到了瓶颈。

最后,让我们将最好的模型放入cross_val_score,看看结果与原始模型相比如何:

rf = RandomForestRegressor(n_estimators=100,  min_impurity_decrease=0.1, max_features=0.6, max_depth=12, warm_start=True, n_jobs=-1, random_state=2)
scores = cross_val_score(rf, X_bikes, y_bikes, scoring='neg_mean_squared_error', cv=10)
rmse = np.sqrt(-scores)
print('RMSE:', np.round(rmse, 3))
print('RMSE mean: %0.3f' % (rmse.mean()))

输出如下:

RMSE: [ 818.354  514.173  547.392  814.059  769.54   730.025  831.376  794.634  756.83  1595.237]
RMSE mean: 817.162

RMSE 回升至817。这个得分比903好得多,但比619差得多。这是怎么回事?

cross_val_score中可能存在最后一次分割的问题,因为它的得分比其他的差了两倍。让我们看看打乱数据是否能解决这个问题。Scikit-learn 有一个 shuffle 模块,可以从 sklearn.utils 导入,方法如下:

from sklearn.utils import shuffle

现在我们可以按如下方式打乱数据:

df_shuffle_bikes = shuffle(df_bikes, random_state=2)

现在将数据分成新的Xy,再次运行RandomForestRegressor并使用cross_val_score

X_shuffle_bikes = df_shuffle_bikes.iloc[:,:-1]
y_shuffle_bikes = df_shuffle_bikes.iloc[:,-1]
rf = RandomForestRegressor(n_estimators=100,  min_impurity_decrease=0.1, max_features=0.6, max_depth=12, n_jobs=-1, random_state=2)
scores = cross_val_score(rf, X_shuffle_bikes, y_shuffle_bikes, scoring='neg_mean_squared_error', cv=10)
rmse = np.sqrt(-scores)
print('RMSE:', np.round(rmse, 3))
print('RMSE mean: %0.3f' % (rmse.mean()))

输出如下:

RMSE: [630.093 686.673 468.159 526.676 593.033 724.575 774.402 672.63  760.253  616.797]
RMSE mean: 645.329

在打乱数据后,最后一次分割没有问题,得分比预期要高得多。

随机森林的缺点

到头来,随机森林的性能受限于其单棵树。如果所有树都犯了同样的错误,随机森林也会犯这个错误。正如本案例研究所揭示的,在数据未打乱之前,随机森林无法显著改进错误,原因是数据中的某些问题单棵树无法处理。

一种能够改进初始缺陷的集成方法,一种能够在未来回合中从决策树的错误中学习的集成方法,可能会带来优势。Boosting 的设计就是为了从早期回合中树木的错误中学习。Boosting,特别是梯度提升——下一章的重点——讨论了这一主题。

最后,以下图表展示了调优后的随机森林回归器和默认的 XGBoost 回归器,在没有打乱数据的情况下增加树木数量时的表现:

图 3.5 – 比较 XGBoost 默认模型和调优后的随机森林

如你所见,XGBoost 在树木数量增加时的学习表现远远优于其他方法。而且 XGBoost 模型甚至还没有进行调优!

总结

在本章中,你学习了集成方法的重要性,特别是了解了袋装法(bagging),即自助抽样(bootstrapping)、带放回的采样和聚合的结合,将多个模型合并成一个。你构建了随机森林分类器和回归器。你通过调整n_estimatorswarm_start超参数,并使用oob_score_来查找误差。然后,你修改了随机森林的超参数以优化模型。最后,你分析了一个案例研究,其中数据洗牌带来了优秀的结果,而对未洗牌数据增加更多的树并没有带来任何提升,这与 XGBoost 的结果形成了对比。

在下一章中,你将学习提升方法(boosting)的基本原理,这是一种集成方法,通过从错误中学习,随着更多决策树的加入,提升准确率。你将实现梯度提升(gradient boosting)进行预测,为极端梯度提升(Extreme Gradient Boosting,简称 XGBoost)奠定基础。

第四章:第四章:从梯度提升到 XGBoost

XGBoost 是一种独特的梯度提升形式,具有多个显著的优势,这些优势将在第五章,《XGBoost 揭示》中进行解释。为了理解 XGBoost 相较于传统梯度提升的优势,您必须首先了解传统梯度提升是如何工作的。XGBoost 融入了传统梯度提升的结构和超参数。在本章中,您将发现梯度提升的强大能力,而这正是 XGBoost 的核心所在。

在本章中,您将从零开始构建梯度提升模型,并与之前的结果对比梯度提升模型和错误。特别地,您将专注于学习率超参数,构建强大的梯度提升模型,其中包括 XGBoost。最后,您将预览一个关于外行星的案例研究,强调对更快算法的需求,这种需求在大数据领域中至关重要,而 XGBoost 正好满足了这一需求。

在本章中,我们将覆盖以下主要主题:

  • 从袋装法到提升法

  • 梯度提升的工作原理

  • 修改梯度提升的超参数

  • 面对大数据的挑战——梯度提升与 XGBoost 的对比

技术要求

本章的代码可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter04找到。

从袋装法到提升法

第三章,《随机森林的袋装法》中,您学习了为什么像随机森林这样的集成机器学习算法通过将多个机器学习模型结合成一个,从而做出更好的预测。随机森林被归类为袋装算法,因为它们使用自助法样本的聚合(决策树)。

相比之下,提升方法通过学习每棵树的错误来进行优化。其一般思路是基于前一棵树的错误来调整新树。

在提升方法(boosting)中,每棵新树的错误修正是与袋装法(bagging)不同的。在袋装模型中,新树不会关注之前的树。此外,新树是通过自助法(bootstrapping)从零开始构建的,最终的模型将所有单独的树进行聚合。然而,在提升方法中,每棵新树都是基于前一棵树构建的。这些树并不是孤立运作的,而是相互叠加构建的。

介绍 AdaBoost

AdaBoost是最早且最受欢迎的提升模型之一。在 AdaBoost 中,每一棵新树都会根据前一棵树的错误来调整权重。通过调整权重,更多地关注预测错误的样本,给予这些样本更高的权重。通过从错误中学习,AdaBoost 能够将弱学习者转变为强学习者。弱学习者是指那些表现几乎与随机猜测一样的机器学习算法。相比之下,强学习者则从数据中学习到了大量信息,表现优异。

提升算法背后的基本思想是将弱学习者转变为强学习者。弱学习者几乎不比随机猜测更好。但这种弱开始是有目的的。基于这一基本思想,提升通过集中精力进行迭代的错误修正来工作,而不是建立一个强大的基准模型。如果基准模型太强,学习过程就会受到限制,从而削弱提升模型背后的整体策略。

通过数百次迭代,弱学习者被转化为强学习者。从这个意义上来说,微小的优势可以带来很大不同。事实上,在过去几十年里,提升方法一直是产生最佳结果的机器学习策略之一。

本书不深入研究 AdaBoost 的细节。像许多 scikit-learn 模型一样,在实践中实现 AdaBoost 非常简单。AdaBoostRegressorAdaBoostClassifier算法可以从sklearn.ensemble库中下载,并适用于任何训练集。最重要的 AdaBoost 超参数是n_estimators,即创建强学习者所需的树的数量(迭代次数)。

注意

有关 AdaBoost 的更多信息,请查阅官方文档:scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostClassifier.html(分类器)和scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostRegressor.html(回归器)。

我们现在将介绍梯度提升,它是 AdaBoost 的强有力替代方案,在性能上略有优势。

区分梯度提升

梯度提升采用与 AdaBoost 不同的方法。虽然梯度提升也基于错误的预测进行调整,但它更进一步:梯度提升根据前一棵树的预测错误完全拟合每一棵新树。也就是说,对于每一棵新树,梯度提升首先查看错误,然后围绕这些错误完全构建一棵新树。新树不会关心那些已经正确的预测。

构建一个仅关注错误的机器学习算法需要一种全面的方法,累加错误以做出准确的最终预测。这种方法利用了残差,即模型预测值与实际值之间的差异。其基本思想如下:

梯度提升计算每棵树预测的残差,并将所有残差加总来评估模型。

理解计算累加残差至关重要,因为这个思想是 XGBoost(梯度提升的高级版本)核心原理之一。当你构建自己的梯度提升版本时,计算和累加残差的过程会变得更加清晰。在下一节中,你将构建自己的梯度提升模型。首先,让我们详细了解梯度提升的工作原理。

梯度提升的工作原理

在本节中,我们将深入了解梯度提升的内部原理,通过对前一棵树的错误训练新树,从零开始构建一个梯度提升模型。这里的核心数学思想是残差。接下来,我们将使用 scikit-learn 的梯度提升算法获得相同的结果。

残差

残差是给定模型的预测与实际值之间的差异。在统计学中,残差常常被分析,以判断线性回归模型与数据的拟合程度。

请考虑以下示例:

  1. 自行车租赁

    a) 预测: 759

    b) 结果: 799

    c) 残差: 799 - 759 = 40

  2. 收入

    a) 预测: 100,000

    b) 结果: 88,000

    c) 残差: 88,000 – 100,000 = -12,000

正如你所看到的,残差告诉你模型的预测与实际之间的偏差,残差可能是正的,也可能是负的。

下面是一个展示线性回归线的残差的可视化示例:

图 4.1 – 线性回归线的残差

图 4.1 – 线性回归线的残差

线性回归的目标是最小化残差的平方。正如图表所示,残差的可视化展示了线性拟合数据的效果。在统计学中,线性回归分析通常通过绘制残差图来深入了解数据。

为了从零开始构建一个梯度提升算法,我们将计算每棵树的残差,并对残差拟合一个新模型。现在我们开始吧。

学习如何从零开始构建梯度提升模型

从零开始构建梯度提升模型将帮助你更深入理解梯度提升在代码中的工作原理。在构建模型之前,我们需要访问数据并为机器学习做准备。

处理自行车租赁数据集

我们继续使用自行车租赁数据集,比较新模型与旧模型的表现:

  1. 我们将从导入pandasnumpy开始,并添加一行代码来关闭任何警告:

    import pandas as pd
    import numpy as np
    import warnings
    warnings.filterwarnings('ignore')
    
  2. 现在,加载bike_rentals_cleaned数据集并查看前五行:

    df_bikes = pd.read_csv('bike_rentals_cleaned.csv')
    df_bikes.head()
    

    你的输出应如下所示:

    图 4.2 – 自行车租赁数据集的前五行

    图 4.2 – 自行车租赁数据集的前五行

  3. 现在,将数据拆分为Xy。然后,将Xy拆分为训练集和测试集:

    X_bikes = df_bikes.iloc[:,:-1]
    y_bikes = df_bikes.iloc[:,-1]
    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_bikes, y_bikes, random_state=2)
    

现在是时候从头开始构建梯度提升模型了!

从头开始构建梯度提升模型

以下是从头开始构建梯度提升机器学习模型的步骤:

  1. 将数据拟合到决策树:你可以使用决策树桩,其max_depth值为1,或者使用深度为23的决策树。初始决策树被称为max_depth=2,并将其拟合到训练集上,作为tree_1,因为它是我们集成中的第一棵树:

    from sklearn.tree import DecisionTreeRegressor
    tree_1 = DecisionTreeRegressor(max_depth=2, random_state=2)
    tree_1.fit(X_train, y_train)
    
  2. 使用训练集进行预测:与使用测试集进行预测不同,梯度提升法中的预测最初是使用训练集进行的。为什么?因为要计算残差,我们需要在训练阶段比较预测结果。模型构建的测试阶段是在所有树构建完毕后才会进行。第一轮的训练集预测结果是通过将predict方法应用到tree_1,并使用X_train作为输入来得到的:

    y_train_pred = tree_1.predict(X_train)
    
  3. 计算残差:残差是预测值与目标列之间的差异。将X_train的预测值,定义为y_train_pred,从y_train目标列中减去,来计算残差:

    y2_train = y_train - y_train_pred
    

    注意

    残差定义为y2_train,因为它是下一棵树的目标列。

  4. 将新树拟合到残差上:将新树拟合到残差上与将模型拟合到训练集上有所不同。主要的区别在于预测。对于自行车租赁数据集,在将新树拟合到残差上时,我们应逐渐得到更小的数字。

    初始化一棵新树,并将其拟合到X_train和残差y2_train上:

    tree_2 = DecisionTreeRegressor(max_depth=2, random_state=2)
    tree_2.fit(X_train, y2_train)
    
  5. 重复步骤 2-4:随着过程的进行,残差应逐渐从正向和负向逼近0。迭代会持续进行,直到达到估计器的数量n_estimators

    让我们重复第三棵树的过程,如下所示:

    y2_train_pred = tree_2.predict(X_train)
    y3_train = y2_train - y2_train_pred
    tree_3 = DecisionTreeRegressor(max_depth=2, random_state=2)
    tree_3.fit(X_train, y3_train)
    

    这个过程可能会持续几十棵、几百棵甚至几千棵树。在正常情况下,你当然会继续进行。要将一个弱学习器转变为强学习器,肯定需要更多的树。然而,由于我们的目标是理解梯度提升背后的工作原理,因此在一般概念已经覆盖的情况下,我们将继续前进。

  6. 汇总结果:汇总结果需要为每棵树使用测试集进行预测,如下所示:

    y1_pred = tree_1.predict(X_test)
    y2_pred = tree_2.predict(X_test)
    y3_pred = tree_3.predict(X_test)
    

    由于预测值是正负差异,将预测值进行汇总应能得到更接近目标列的预测结果,如下所示:

    y_pred = y1_pred + y2_pred + y3_pred
    
  7. 最后,我们计算均方误差MSE)来获得结果,如下所示:

    from sklearn.metrics import mean_squared_error as MSE
    MSE(y_test, y_pred)**0.5
    

    以下是预期的输出:

    911.0479538776444
    

对于一个尚未强大的弱学习器来说,这样的表现还不错!现在,让我们尝试使用 scikit-learn 获得相同的结果。

在 scikit-learn 中构建梯度提升模型

让我们看看能否通过调整一些超参数,使用 scikit-learn 的GradientBoostingRegressor得到与前一节相同的结果。使用GradientBoostingRegressor的好处是,它构建得更快,且实现起来更简单:

  1. 首先,从sklearn.ensemble库中导入回归器:

    from sklearn.ensemble import GradientBoostingRegressor
    
  2. 在初始化GradientBoostingRegressor时,有几个重要的超参数。为了获得相同的结果,必须将max_depth=2random_state=2匹配。此外,由于只有三棵树,我们必须设置n_estimators=3。最后,我们还必须设置learning_rate=1.0超参数。稍后我们会详细讨论learning_rate

    gbr = GradientBoostingRegressor(max_depth=2, n_estimators=3, random_state=2, learning_rate=1.0)
    
  3. 现在模型已经初始化,可以在训练数据上进行拟合,并在测试数据上进行评分:

    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    MSE(y_test, y_pred)**0.5
    

    结果如下:

    911.0479538776439
    

    结果在小数点后 11 位都是相同的!

    回顾一下,梯度提升的关键是构建一个足够多的树的模型,将弱学习器转变为强学习器。通过将n_estimators(迭代次数)设置为一个更大的数字,这可以很容易地实现。

  4. 让我们构建并评分一个拥有 30 个估算器的梯度提升回归器:

    gbr = GradientBoostingRegressor(max_depth=2, n_estimators=30, random_state=2, learning_rate=1.0)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    MSE(y_test, y_pred)**0.5
    

    结果如下:

    857.1072323426944
    

    得分有所提升。现在,让我们看看 300 个估算器的情况:

    gbr = GradientBoostingRegressor(max_depth=2, n_estimators=300, random_state=2, learning_rate=1.0)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    MSE(y_test, y_pred)**0.5
    

    结果是这样的:

    936.3617413678853
    

这真是个惊讶!得分变差了!我们是不是被误导了?梯度提升真如人们所说的那样有效吗?

每当得到一个意外的结果时,都值得仔细检查代码。现在,我们更改了learning_rate,却没有详细说明。那么,如果我们移除learning_rate=1.0,并使用 scikit-learn 的默认值会怎样呢?

让我们来看看:

gbr = GradientBoostingRegressor(max_depth=2, n_estimators=300, random_state=2)
gbr.fit(X_train, y_train)
y_pred = gbr.predict(X_test)
MSE(y_test, y_pred)**0.5

结果是这样的:

653.7456840231495

难以置信!通过使用 scikit-learn 对learning_rate超参数的默认值,得分从936变为654

在接下来的章节中,我们将深入学习不同的梯度提升超参数,重点关注learning_rate超参数。

修改梯度提升超参数

在本节中,我们将重点关注learning_rate,这是最重要的梯度提升超参数,可能唯一需要注意的是n_estimators,即模型中的迭代次数或树的数量。我们还将调查一些树的超参数,以及subsample,这会导致RandomizedSearchCV,并将结果与 XGBoost 进行比较。

learning_rate

在上一节中,将GradientBoostingRegressorlearning_rate值从1.0更改为 scikit-learn 的默认值0.1,得到了巨大的提升。

learning_rate,也称为收缩率,会缩小单棵树的贡献,以确保在构建模型时没有一棵树的影响过大。如果整个集成模型是通过一个基础学习器的误差构建的,而没有仔细调整超参数,模型中的早期树可能会对后续的发展产生过大的影响。learning_rate限制了单棵树的影响。通常来说,随着n_estimators(树的数量)的增加,learning_rate应该减小。

确定最优的learning_rate值需要调整n_estimators。首先,让我们保持n_estimators不变,看看learning_rate单独的表现。learning_rate的取值范围是从01learning_rate值为1意味着不做任何调整。默认值0.1表示树的影响权重为 10%。

这里是一个合理的起始范围:

learning_rate_values = [0.001, 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 1.0]

接下来,我们将通过构建和评分新的GradientBoostingRegressor来遍历这些值,看看得分如何比较:

for value in learning_rate_values:
    gbr = GradientBoostingRegressor(max_depth=2,   n_estimators=300, random_state=2, learning_rate=value)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    rmse = MSE(y_test, y_pred)**0.5
    print('Learning Rate:', value, ', Score:', rmse)

学习率的值和得分如下:

Learning Rate: 0.001 , Score: 1633.0261400367258
Learning Rate: 0.01 , Score: 831.5430182728547
Learning Rate: 0.05 , Score: 685.0192988749717
Learning Rate: 0.1 , Score: 653.7456840231495
Learning Rate: 0.15 , Score: 687.666134269379
Learning Rate: 0.2 , Score: 664.312804425697
Learning Rate: 0.3 , Score: 689.4190385930236
Learning Rate: 0.5 , Score: 693.8856905068778
Learning Rate: 1.0 , Score: 936.3617413678853

从输出中可以看出,默认的learning_rate值为0.1时,300 棵树的得分最好。

现在让我们调整n_estimators。使用前面的代码,我们可以生成learning_rate图,其中n_estimators为 30、300 和 3,000 棵树,如下图所示:

图 4.3 – 30 棵树的 learning_rate 图

图 4.3 – 30 棵树的 learning_rate 图

如您所见,使用 30 棵树时,learning_rate值在大约0.3时达到峰值。

现在,让我们看一下 3,000 棵树的learning_rate图:

图 4.4 -- 3,000 棵树的 learning_rate 图

图 4.4 -- 3,000 棵树的 learning_rate 图

使用 3,000 棵树时,learning_rate值在第二个值,即0.05时达到峰值。

这些图表突显了调优learning_raten_estimators的重要性。

基础学习器

梯度提升回归器中的初始决策树被称为基础学习器,因为它是集成模型的基础。它是过程中的第一个学习器。这里的学习器一词表示一个弱学习器正在转变为强学习器

尽管基础学习器不需要为准确性进行微调,正如在第二章《决策树深入解析》中所述,当然也可以通过调优基础学习器来提高准确性。

例如,我们可以选择max_depth值为1234,并比较结果如下:

depths = [None, 1, 2, 3, 4]
for depth in depths:
    gbr = GradientBoostingRegressor(max_depth=depth, n_estimators=300, random_state=2)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    rmse = MSE(y_test, y_pred)**0.5
    print('Max Depth:', depth, ', Score:', rmse) 

结果如下:

Max Depth: None , Score: 867.9366621617327
Max Depth: 1 , Score: 707.8261886858736
Max Depth: 2 , Score: 653.7456840231495
Max Depth: 3 , Score: 646.4045923317708
Max Depth: 4 , Score: 663.048387855927

max_depth值为3时,得到最佳结果。

其他基础学习器的超参数,如在第二章《决策树深入解析》中所述,也可以采用类似的方式进行调整。

子样本(subsample)

subsample是样本的一个子集。由于样本是行,子集意味着在构建每棵树时可能并不是所有的行都会被包含。当将subsample1.0改为更小的小数时,树在构建阶段只会选择该百分比的样本。例如,subsample=0.8会为每棵树选择 80%的样本。

继续使用max_depth=3,我们尝试不同的subsample百分比,以改善结果:

samples = [1, 0.9, 0.8, 0.7, 0.6, 0.5]
for sample in samples:
    gbr = GradientBoostingRegressor(max_depth=3, n_estimators=300, subsample=sample, random_state=2)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    rmse = MSE(y_test, y_pred)**0.5
    print('Subsample:', sample, ', Score:', rmse)

结果如下:

Subsample: 1 , Score: 646.4045923317708
Subsample: 0.9 , Score: 620.1819001443569
Subsample: 0.8 , Score: 617.2355650565677
Subsample: 0.7 , Score: 612.9879156983139
Subsample: 0.6 , Score: 622.6385116402317
Subsample: 0.5 , Score: 626.9974073227554

使用subsample值为0.7,300 棵树和max_depth3时,获得了目前为止最佳的得分。

subsample不等于1.0时,模型被归类为随机梯度下降,其中随机意味着模型中存在某些随机性。

随机搜索交叉验证(RandomizedSearchCV)

我们有一个良好的工作模型,但尚未进行网格搜索,如第二章《决策树深入分析》中所述。我们的初步分析表明,以max_depth=3subsample=0.7n_estimators=300learning_rate = 0.1为中心进行网格搜索是一个不错的起点。我们已经展示了,当n_estimators增加时,learning_rate应当减少:

  1. 这是一个可能的起点:

    params={'subsample':[0.65, 0.7, 0.75],
            'n_estimators':[300, 500, 1000],
             'learning_rate':[0.05, 0.075, 0.1]}
    

    由于n_estimators从初始值 300 增加,learning_rate从初始值0.1减少。我们保持max_depth=3以限制方差。

    在 27 种可能的超参数组合中,我们使用RandomizedSearchCV尝试其中的 10 种组合,希望找到一个好的模型。

    注意

    尽管GridSearchCV可以实现 27 种组合,但最终你会遇到可能性过多的情况,RandomizedSearchCV在此时变得至关重要。我们在这里使用RandomizedSearchCV进行实践并加速计算。

  2. 让我们导入RandomizedSearchCV并初始化一个梯度提升模型:

    from sklearn.model_selection import RandomizedSearchCV
    gbr = GradientBoostingRegressor(max_depth=3, random_state=2)
    
  3. 接下来,初始化RandomizedSearchCV,以gbrparams作为输入,除了迭代次数、评分标准和折叠数。请记住,n_jobs=-1可能加速计算,而random_state=2确保结果的一致性:

    rand_reg = RandomizedSearchCV(gbr, params, n_iter=10, scoring='neg_mean_squared_error', cv=5, n_jobs=-1, random_state=2)
    
  4. 现在在训练集上拟合模型,并获取最佳参数和分数:

    rand_reg.fit(X_train, y_train)
    best_model = rand_reg.best_estimator_
    best_params = rand_reg.best_params_
    print("Best params:", best_params)
    best_score = np.sqrt(-rand_reg.best_score_)
    print("Training score: {:.3f}".format(best_score))
    y_pred = best_model.predict(X_test)
    rmse_test = MSE(y_test, y_pred)**0.5
    print('Test set score: {:.3f}'.format(rmse_test))
    

    结果如下:

    Best params: {'learning_rate': 0.05, 'n_estimators': 300, 'subsample': 0.65}
    Training score: 636.200
    Test set score: 625.985
    

    从这里开始,值得逐个或成对地调整参数进行实验。尽管当前最好的模型有n_estimators=300,但通过谨慎调整learning_rate,提高该超参数可能会得到更好的结果。subsample也可以进行实验。

  5. 经过几轮实验后,我们得到了以下模型:

    gbr = GradientBoostingRegressor(max_depth=3, n_estimators=1600, subsample=0.75, learning_rate=0.02, random_state=2)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    MSE(y_test, y_pred)**0.5 
    

    结果如下:

    596.9544588974487
    

n_estimators1600learning_rate0.02subsample0.75max_depth3时,我们得到了最佳得分597

可能还有更好的方法。我们鼓励你尝试!

现在,让我们看看 XGBoost 与梯度提升在使用迄今为止所涉及的相同超参数时有何不同。

XGBoost

XGBoost 是梯度提升的高级版本,具有相同的一般结构,这意味着它通过将树的残差求和,将弱学习器转化为强学习器。

上一节中的超参数唯一的不同之处是,XGBoost 将learning_rate称为eta

让我们用相同的超参数构建一个 XGBoost 回归模型来比较结果。

xgboost导入XGBRegressor,然后初始化并评分模型,代码如下:

from xgboost import XGBRegressor
xg_reg = XGBRegressor(max_depth=3, n_estimators=1600, eta=0.02, subsample=0.75, random_state=2)
xg_reg.fit(X_train, y_train)
y_pred = xg_reg.predict(X_test)
MSE(y_test, y_pred)**0.5

结果如下:

584.339544309016

分数更好。为什么分数更好将在下一章中揭示,第五章XGBoost 揭秘

准确性和速度是构建机器学习模型时最重要的两个概念,我们已经多次证明 XGBoost 非常准确。XGBoost 通常比梯度提升更受欢迎,因为它始终提供更好的结果,并且因为它更快,以下案例研究对此做出了证明。

面对大数据——梯度提升与 XGBoost 的对比

在现实世界中,数据集可能庞大,包含万亿个数据点。仅依靠一台计算机可能会因为资源有限而不利于工作。处理大数据时,通常使用云计算来利用并行计算机。

数据集之所以被认为是“大”,是因为它们突破了计算的极限。在本书至此为止,数据集的行数限制在数万行,列数不超过一百列,应该没有显著的时间延迟,除非你遇到错误(每个人都会发生)。

在本节中,我们将随时间考察系外行星。数据集包含 5,087 行和 3,189 列,记录了恒星生命周期不同阶段的光通量。将列和行相乘得到 150 万数据点。以 100 棵树为基准,我们需要 1.5 亿个数据点来构建模型。

在本节中,我的 2013 款 MacBook Air 等待时间大约为 5 分钟。新电脑应该会更快。我选择了系外行星数据集,以便等待时间对计算有显著影响,同时不会让你的计算机长时间占用。

介绍系外行星数据集

系外行星数据集来自 Kaggle,数据大约来自 2017 年:www.kaggle.com/keplersmachines/kepler-labelled-time-series-data。该数据集包含关于恒星光的资料。每一行是一个单独的恒星,列展示了随时间变化的不同光模式。除了光模式外,如果恒星有系外行星,系外行星列标记为2;否则标记为1

数据集记录了成千上万颗恒星的光通量。光通量,通常被称为光亮通量,是恒星的感知亮度。

注意

感知亮度与实际亮度不同。例如,一颗非常明亮但距离遥远的恒星,其光通量可能很小(看起来很暗),而像太阳这样距离很近的中等亮度恒星,其光通量可能很大(看起来很亮)。

当单颗恒星的光通量周期性变化时,可能是由于该恒星被外行星所绕。假设是外行星在恒星前方运行时,它会阻挡一小部分光线,导致观测到的亮度略微减弱。

小贴士

寻找外行星是非常罕见的。预测列,关于恒星是否拥有外行星,正例非常少,导致数据集不平衡。不平衡的数据集需要额外的注意。在第七章《使用 XGBoost 发现外行星》中,我们将进一步探讨该数据集的不平衡问题。

接下来,让我们访问外行星数据集并为机器学习做准备。

对外行星数据集进行预处理

外行星数据集已经上传到我们的 GitHub 页面:github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter04

以下是加载和预处理外行星数据集以供机器学习使用的步骤:

  1. 下载exoplanets.csv文件,并将其与 Jupyter Notebook 放在同一文件夹下。然后,打开该文件查看内容:

    df = pd.read_csv('exoplanets.csv')
    df.head() 
    

    DataFrame 会如下所示:

    图 4.5 – 外行星数据框

    图 4.5 – 外行星数据框

    由于空间限制,并非所有列都会显示。光通量列为浮动数值类型,而Label列对于外行星恒星是2,对于非外行星恒星是1

  2. 让我们使用df.info()来确认所有列都是数值型的:

    df.info()
    

    结果如下:

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 5087 entries, 0 to 5086
    Columns: 3198 entries, LABEL to FLUX.3197
    dtypes: float64(3197), int64(1)
    memory usage: 124.1 MB
    

    从输出结果可以看出,3197列是浮动数值类型,1列是int类型,所以所有列都是数值型的。

  3. 现在,让我们用以下代码确认空值的数量:

    df.isnull().sum().sum()
    

    输出结果如下:

    0
    

    输出结果显示没有空值。

  4. 由于所有列都是数值型且没有空值,我们可以将数据拆分为训练集和测试集。请注意,第 0 列是目标列y,其余列是预测列X

    X = df.iloc[:,1:]
    y = df.iloc[:,0]
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    

现在是时候构建梯度提升分类器来预测恒星是否拥有外行星了。

构建梯度提升分类器

梯度提升分类器的工作原理与梯度提升回归器相同,主要的区别在于评分方式。

让我们开始导入GradientBoostingClassiferXGBClassifier,并导入accuracy_score,以便我们可以比较这两个模型:

from sklearn.ensemble import GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score

接下来,我们需要一种方法来使用定时器比较模型。

模型计时

Python 自带了一个time库,可以用来标记时间。一般的做法是在计算前后标记时间,时间差告诉我们计算花费的时间。

time库的导入方法如下:

import time

time库中,.time()方法以秒为单位标记时间。

作为一个例子,查看通过使用time.time()在计算前后标记开始和结束时间,df.info()运行需要多长时间:

start = time.time()
df.info()
end = time.time()
elapsed = end - start
print('\nRun Time: ' + str(elapsed) + ' seconds.')

输出如下:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5087 entries, 0 to 5086
Columns: 3198 entries, LABEL to FLUX.3197
dtypes: float64(3197), int64(1)
memory usage: 124.1 MB

运行时间如下:

Run Time: 0.0525362491607666 seconds.

你的结果可能会与我们的不同,但希望在同一数量级范围内。

现在让我们使用前面的代码标记时间,比较GradientBoostingClassifierXGBoostClassifier在外行星数据集上的速度。

提示

Jupyter Notebooks 配有魔法函数,用%符号标记在命令前。%timeit就是这样一个魔法函数。与计算运行一次代码所需的时间不同,%timeit会计算多次运行代码所需的时间。有关魔法函数的更多信息,请参见ipython.readthedocs.io/en/stable/interactive/magics.html

比较速度

是时候用外行星数据集对GradientBoostingClassifierXGBoostClassifier进行速度对比了。我们设置了max_depth=2n_estimators=100来限制模型的大小。我们从GradientBoostingClassifier开始:

  1. 首先,我们将标记开始时间。在构建并评分模型后,我们将标记结束时间。以下代码可能需要大约 5 分钟来运行,具体时间取决于你的计算机速度:

    start = time.time()
    gbr = GradientBoostingClassifier(n_estimators=100, max_depth=2, random_state=2)
    gbr.fit(X_train, y_train)
    y_pred = gbr.predict(X_test)
    score = accuracy_score(y_pred, y_test)
    print('Score: ' + str(score))
    end = time.time()
    elapsed = end - start
    print('\nRun Time: ' + str(elapsed) + ' seconds')
    

    结果如下:

    Score: 0.9874213836477987
    Run Time: 317.6318619251251 seconds
    

    GradientBoostingRegressor在我的 2013 款 MacBook Air 上运行了超过 5 分钟。对于一台旧电脑来说,在 150 百万数据点的情况下,表现还不错。

    注意

    尽管 98.7%的得分通常在准确性上非常出色,但对于不平衡数据集而言情况并非如此,正如你将在第七章中看到的那样,使用 XGBoost 发现外行星

  2. 接下来,我们将构建一个具有相同超参数的XGBClassifier模型,并以相同的方式标记时间:

    start = time.time()
    xg_reg = XGBClassifier(n_estimators=100, max_depth=2, random_state=2)
    xg_reg.fit(X_train, y_train)
    y_pred = xg_reg.predict(X_test)
    score = accuracy_score(y_pred, y_test)
    print('Score: ' + str(score))
    end = time.time()
    elapsed = end - start
    print('Run Time: ' + str(elapsed) + ' seconds')
    

    结果如下:

    Score: 0.9913522012578616
    Run Time: 118.90568995475769 seconds
    

在我的 2013 款 MacBook Air 上,XGBoost 运行时间不到 2 分钟,速度是原来的两倍以上。它的准确性也提高了半个百分点。

在大数据的领域中,一个速度是原来两倍的算法可以节省数周甚至数月的计算时间和资源。这一优势在大数据领域中是巨大的。

在提升法的世界里,XGBoost 因其无与伦比的速度和出色的准确性而成为首选模型。

至于外行星数据集,它将在第七章中重新讨论,在一个重要的案例研究中揭示了处理不平衡数据集时的挑战,以及针对这些挑战的多种潜在解决方案。

注意

我最近购买了一台 2020 款的 MacBook Pro 并更新了所有软件。使用相同代码时,时间差异惊人:

梯度提升运行时间:197.38 秒

XGBoost 运行时间:8.66 秒

超过 10 倍的差异!

总结

在本章中,你了解了 bagging 与 boosting 的区别。你通过从零开始构建一个梯度提升回归器,学习了梯度提升是如何工作的。你实现了各种梯度提升超参数,包括learning_raten_estimatorsmax_depthsubsample,这导致了随机梯度提升。最后,你利用大数据,通过比较GradientBoostingClassifierXGBoostClassifier的运行时间,预测星星是否拥有系外行星,结果表明XGBoostClassifier的速度是前者的两倍,甚至超过十倍,同时更为准确。

学习这些技能的优势在于,你现在能明白何时使用 XGBoost,而不是像梯度提升这样的类似机器学习算法。你现在可以通过正确利用核心超参数,包括n_estimatorslearning_rate,来构建更强大的 XGBoost 和梯度提升模型。此外,你也已经具备了为所有计算设定时间的能力,而不再依赖直觉。

恭喜!你已经完成了所有初步的 XGBoost 章节。到目前为止,目的在于让你了解机器学习和数据分析在更广泛的 XGBoost 叙事中的背景。目的是展示 XGBoost 如何从集成方法、提升、梯度提升以及大数据的需求中应运而生。

下一章将为我们的旅程开启新的一篇,带来 XGBoost 的高级介绍。在这里,你将学习 XGBoost 算法背后的数学细节,并了解 XGBoost 如何通过硬件修改来提高速度。此外,你还将通过一个关于希格斯玻色子发现的历史性案例,使用原始的 Python API 构建 XGBoost 模型。接下来的章节将重点介绍如何构建快速、高效、强大且适合行业应用的 XGBoost 模型,其中涵盖了令人兴奋的细节、优势、技巧和窍门,这些模型将在未来多年内为你所用。

第二部分:XGBoost

通过回顾 XGBoost 的总体框架,包括基础模型、速度提升、数学推导以及原始 Python API,重新介绍并深入分析 XGBoost。对 XGBoost 的超参数进行了详细分析、总结和微调。科学相关的案例研究为构建和微调强大的 XGBoost 模型提供了丰富的实践,旨在修正权重不平衡和不足的得分问题。

本节包括以下章节:

  • 第五章**, XGBoost 揭秘

  • 第六章**, XGBoost 超参数

  • 第七章**, 使用 XGBoost 发现系外行星

第五章:第五章:XGBoost 揭示

在这一章中,你将最终看到极限梯度提升(Extreme Gradient Boosting),或称为XGBoost。XGBoost 是在我们构建的机器学习叙事框架中呈现的,从决策树到梯度提升。章节的前半部分聚焦于 XGBoost 带给树集成算法的独特进展背后的理论。后半部分则聚焦于在Higgs 博士 Kaggle 竞赛中构建 XGBoost 模型,正是这个竞赛让 XGBoost 向全世界展示了它的强大。

具体而言,你将识别出使 XGBoost 更加快速的速度增强,了解 XGBoost 如何处理缺失值,并学习 XGBoost 的正则化参数选择背后的数学推导。你将建立构建 XGBoost 分类器和回归器的模型模板。最后,你将了解大型强子对撞机(Large Hadron Collider),即希格斯玻色子发现的地方,在那里你将使用原始的 XGBoost Python API 来加权数据并进行预测。

本章涉及以下主要内容:

  • 设计 XGBoost

  • 分析 XGBoost 参数

  • 构建 XGBoost 模型

  • 寻找希格斯玻色子 – 案例研究

设计 XGBoost

XGBoost 是相较于梯度提升算法的重大升级。在本节中,你将识别 XGBoost 的关键特性,这些特性使它与梯度提升和其他树集成算法区分开来。

历史叙事

随着大数据的加速发展,寻找能产生准确、最优预测的优秀机器学习算法的探索开始了。决策树生成的机器学习模型过于精确,无法很好地泛化到新数据。集成方法通过集成提升组合多棵决策树,证明了更加有效。梯度提升是从树集成算法轨迹中出现的领先算法。

梯度提升的一致性、强大功能和出色结果使得华盛顿大学的陈天奇(Tianqi Chen)决定增强其能力。他将这一新算法命名为 XGBoost,代表极限梯度提升(Extreme Gradient Boosting)。陈天奇的新型梯度提升算法包含内建的正则化,并在速度上取得了显著提升。

在 Kaggle 竞赛中取得初步成功后,2016 年,陈天奇和卡洛斯·格斯特林(Carlos Guestrin)共同撰写了 XGBoost: A Scalable Tree Boosting System,向更广泛的机器学习社区介绍了他们的算法。你可以在arxiv.org/pdf/1603.02754.pdf上查看原文。以下部分总结了其中的关键要点。

设计特性

第四章所示,从梯度提升到 XGBoost,在处理大数据时,对更快算法的需求显而易见。极限(Extreme)在极限梯度提升(Extreme Gradient Boosting)中意味着将计算极限推向极致。推动计算极限不仅需要构建模型的知识,还需要了解磁盘读取、压缩、缓存和核心等方面的知识。

尽管本书的重点仍然是构建 XGBoost 模型,但我们将窥探一下 XGBoost 算法的内部机制,以区分其关键进展,如处理缺失值、提升速度和提高准确度,这些因素使 XGBoost 更快、更准确、并且更具吸引力。接下来,让我们看看这些关键进展。

处理缺失值

你在第一章《机器学习概述》中花费了大量时间,练习了不同的方法来处理空值。这是所有机器学习从业者必须掌握的基本技能。

然而,XGBoost 能够为你处理缺失值。它有一个名为 missing 的超参数,可以设置为任何值。遇到缺失数据时,XGBoost 会对不同的分割选项进行评分,并选择结果最好的那个。

提升速度

XGBoost 是专门为速度设计的。速度的提升使得机器学习模型能够更快速地构建,这在处理百万、十亿或万亿行数据时尤为重要。在大数据的世界里,这并不罕见,因为每天,工业和科学领域都会积累比以往更多的数据。以下新的设计功能使 XGBoost 在速度上相比同类集成算法具有显著优势:

  • 近似分割查找算法

  • 稀疏感知分割查找

  • 并行计算

  • 缓存感知访问

  • 块压缩与分片

让我们更详细地了解这些功能。

近似分割查找算法

决策树需要最优的分割才能产生最优结果。贪心算法在每一步选择最佳的分割,并且不会回溯查看之前的分支。请注意,决策树分割通常以贪心的方式进行。

XGBoost 提出了一个精确的贪心算法,并且增加了一种新的近似分割查找算法。分割查找算法使用分位数(用于拆分数据的百分比)来提出候选的分割点。在全局提议中,整个训练过程中使用相同的分位数;而在局部提议中,每一轮分割都会提供新的分位数。

一个已知的算法,分位数草图,在数据集权重相等的情况下表现良好。XGBoost 提出了一个新型的加权分位数草图,基于合并和修剪,并提供了理论保证。虽然该算法的数学细节超出了本书的范围,但你可以参考 XGBoost 原始论文的附录,链接请见arxiv.org/pdf/1603.02754.pdf

稀疏感知分割查找

使用pd.get_dummies类别列转换为数值列。这会导致数据集增大,并产生许多值为 0 的条目。将类别列转换为数值列,其中 1 表示存在,0 表示不存在,这种方法通常称为独热编码。你将在第十章中练习独热编码,XGBoost 模型部署

稀疏矩阵旨在仅存储具有非零且非空值的数据点,这样可以节省宝贵的空间。稀疏感知的分裂意味着,在寻找分裂时,XGBoost 更快,因为它的矩阵是稀疏的。

根据原始论文《XGBoost: A Scalable Tree Boosting System》,稀疏感知的分裂查找算法在All-State-10K数据集上比标准方法快 50 倍。

并行计算

增强方法并不适合并行计算,因为每棵树都依赖于前一棵树的结果。然而,仍然有一些并行化的机会。

并行计算发生在多个计算单元同时协同处理同一个问题时。XGBoost 将数据排序并压缩为块。这些块可以分发到多个机器,或者分发到外部内存(即核心外存储)。

使用块排序数据速度更快。分裂查找算法利用了块,因而查找分位数的速度也更快。在这些情况下,XGBoost 提供了并行计算,以加速模型构建过程。

缓存感知访问

计算机上的数据被分为缓存主内存。缓存是你最常使用的部分,保留给高速内存。较少使用的数据则保留在较低速的内存中。不同的缓存级别有不同的延迟量级,详细信息可以参见:gist.github.com/jboner/2841832

在梯度统计方面,XGBoost 使用缓存感知预取。XGBoost 分配一个内部缓冲区,提取梯度统计数据,并通过小批量进行累积。根据《XGBoost: A Scalable Tree Boosting System》中的描述,预取延长了读写依赖,并将大规模数据集的运行时间减少了大约 50%。

块压缩与分片

XGBoost 通过块压缩块分片提供额外的速度提升。

块压缩通过压缩列来帮助计算密集型磁盘读取。块分片通过将数据分片到多个磁盘,并在读取数据时交替使用,减少了读取时间。

准确性提升

XGBoost 增加了内建正则化,以在梯度提升之外实现准确性提升。正则化是通过增加信息来减少方差并防止过拟合的过程。

尽管数据可以通过超参数微调进行正则化,但也可以尝试使用正则化算法。例如,RidgeLassoLinearRegression 的正则化机器学习替代方法。

XGBoost 将正则化作为学习目标的一部分,与梯度提升和随机森林不同。正则化参数通过惩罚复杂性并平滑最终权重来防止过拟合。XGBoost 是一种正则化版本的梯度提升。

在下一节中,你将接触到 XGBoost 学习目标背后的数学原理,它将正则化与损失函数结合在一起。虽然你不需要知道这些数学内容来有效地使用 XGBoost,但掌握数学知识可能会让你对其有更深的理解。如果愿意,你可以跳过下一节。

分析 XGBoost 参数

在本节中,我们将通过数学推导分析 XGBoost 用于创建最先进机器学习模型的参数。

我们将保持与第二章《决策树深入解析》中的参数和超参数的区分。超参数是在模型训练之前选择的,而参数则是在模型训练过程中选择的。换句话说,参数是模型从数据中学习到的内容。

以下推导来自 XGBoost 官方文档,Boosted Trees 介绍,网址为 xgboost.readthedocs.io/en/latest/tutorials/model.html

学习目标

机器学习模型的学习目标决定了模型与数据的拟合程度。对于 XGBoost,学习目标包括两个部分:损失函数正则化项

数学上,XGBoost 的学习目标可以定义如下:

这里, 是损失函数,表示回归的均方误差MSE),或分类的对数损失, 是正则化函数,一个用于防止过拟合的惩罚项。将正则化项作为目标函数的一部分使 XGBoost 与大多数树集成方法有所区别。

让我们通过考虑回归的均方误差(MSE)更详细地看一下目标函数。

损失函数

定义为回归的均方误差(MSE)的损失函数可以用求和符号表示,如下所示:

这里, 是第 行的目标值, 是机器学习模型为第 行预测的值。求和符号 表示从 开始到 结束,所有行的求和。

对于给定的树,预测值 需要一个从树根开始,到叶子结束的函数。数学上可以表达为:

这里,xi 是一个向量,其条目为第 行的列, 表示函数 的成员,后者是所有可能的 CART 函数集合。CART分类与回归树Classification And Regression Trees)的缩写。CART 为所有叶子节点提供了一个实值,即使对于分类算法也是如此。

在梯度提升中,决定第 行预测的函数包括所有之前函数的和,如第四章《从梯度提升到 XGBoost》中所述。因此,可以写出以下公式:

这里,T 是提升树的数量。换句话说,为了获得第 棵树的预测结果,需要将之前所有树的预测结果与新树的预测结果相加。符号 表明这些函数属于 ,即所有可能的 CART 函数集合。

棵提升树的学习目标现在可以重写如下:

这里, 是第 棵提升树的总损失函数, 是正则化项。

由于提升树会将之前树的预测结果与新树的预测结果相加,因此必须满足 。这就是加法训练的思想。

将此代入之前的学习目标,我们得到以下公式:

对于最小二乘回归情况,可以将其重写如下:

展开多项式后,我们得到以下公式:

这里, 是一个常数项,不依赖于 。从多项式的角度来看,这是一个关于变量 的二次方程。请记住,目标是找到一个最优值 ,即最优函数,它将根节点(样本)映射到叶节点(预测值)。

任何足够光滑的函数,例如二次多项式(quadratic),都可以通过泰勒多项式来逼近。XGBoost 使用牛顿法与二次泰勒多项式来得到以下公式:

这里, 可以写作以下偏导数:

若要了解 XGBoost 如何使用 泰勒展开,请查阅 stats.stackexchange.com/questions/202858/xgboost-loss-function-approximation-with-taylor-expansion

XGBoost 通过使用仅需要 作为输入的求解器来实现这一学习目标函数。由于损失函数是通用的,相同的输入可以用于回归和分类。

这留下了正则化函数,

正则化函数

为叶子的向量空间。那么,,即将树根映射到叶子的函数,可以用 来重新表示,形式如下:

这里,q 是将数据点分配给叶子的函数,T 是叶子的数量。

经过实践和实验,XGBoost 确定了以下作为正则化函数,其中 是用于减少过拟合的惩罚常数:

目标函数

将损失函数与正则化函数结合,学习目标函数变为以下形式:

我们可以定义分配给 叶子的数据显示点的索引集,如下所示:

目标函数可以写成如下形式:

最后,通过设置 ,在重新排列索引并合并相似项后,我们得到目标函数的最终形式,即:

通过对目标函数求导并关于 令左侧为零,我们得到如下结果:

这可以代回到目标函数中,得到以下结果:

这是 XGBoost 用来确定模型与数据拟合程度的结果。

恭喜你完成了一个漫长且具有挑战性的推导过程!

构建 XGBoost 模型

在前两部分中,你学习了 XGBoost 如何在底层工作,包括参数推导、正则化、速度增强以及新的特性,如 missing 参数用于补偿空值。

本书中,我们主要使用 scikit-learn 构建 XGBoost 模型。scikit-learn 的 XGBoost 封装器在 2019 年发布。在完全使用 scikit-learn 之前,构建 XGBoost 模型需要更陡峭的学习曲线。例如,必须将 NumPy 数组转换为 dmatrices,才能利用 XGBoost 框架。

然而,在 scikit-learn 中,这些转换是后台自动完成的。在 scikit-learn 中构建 XGBoost 模型与构建其他机器学习模型非常相似,正如你在本书中所经历的那样。所有标准的 scikit-learn 方法,如 .fit.predict 都可以使用,此外还包括如 train_test_splitcross_val_scoreGridSearchCVRandomizedSearchCV 等重要工具。

在这一部分,你将开发用于构建 XGBoost 模型的模板。以后,这些模板可以作为构建 XGBoost 分类器和回归器的起点。

我们将为两个经典数据集构建模板:用于分类的 Iris 数据集 和用于回归的 Diabetes 数据集。这两个数据集都很小,内置于 scikit-learn,并且在机器学习社区中经常被测试。作为模型构建过程的一部分,你将显式定义默认的超参数,这些超参数使 XGBoost 模型得分优秀。这些超参数被显式定义,以便你了解它们是什么,并为将来调整它们做好准备。

Iris 数据集

Iris 数据集,机器学习领域的一个重要数据集,由统计学家 Robert Fischer 于 1936 年引入。它易于访问、数据量小、数据干净,且具有对称的数值,这些特点使其成为测试分类算法的热门选择。

我们将通过使用 datasets 库中的 load_iris() 方法直接从 scikit-learn 下载 Iris 数据集,如下所示:

import pandas as pd
import numpy as np
from sklearn import datasets
iris = datasets.load_iris()

Scikit-learn 数据集以 pandas DataFrame 的形式存储,后者更多用于数据分析和数据可视化。将 NumPy 数组视为 DataFrame 需要使用 pandasDataFrame 方法。这个 scikit-learn 数据集在预先划分为预测列和目标列后,合并它们需要用 np.c_ 来连接 NumPy 数组,然后再进行转换。列名也会被添加,如下所示:

df = pd.DataFrame(data= np.c_[iris['data'], iris['target']],columns= iris['feature_names'] + ['target'])

你可以使用 df.head() 查看 DataFrame 的前五行:

df.head()

结果 DataFrame 将如下所示:

图 5.1 – Iris 数据集

图 5.1 – Iris 数据集

预测列的含义不言自明,分别衡量萼片和花瓣的长度与宽度。目标列根据 scikit-learn 文档, scikit-learn.org/stable/auto_examples/datasets/plot_iris_dataset.html,包含三种不同的鸢尾花:setosaversicolorvirginica。数据集包含 150 行。

为了准备机器学习所需的数据,导入 train_test_split,然后相应地划分数据。你可以使用原始的 NumPy 数组 iris['data']iris['target'] 作为 train_test_split 的输入:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris['data'], iris['target'], random_state=2)

现在我们已经划分了数据,接下来让我们构建分类模板。

XGBoost 分类模板

以下模板用于构建 XGBoost 分类器,假设数据集已经分为 X_trainX_testy_trainy_test

  1. xgboost 库中导入 XGBClassifier

    from xgboost import XGBClassifier
    
  2. 根据需要导入分类评分方法。

    虽然 accuracy_score 是标准评分方法,但其他评分方法,如 auc曲线下的面积),将在后面讨论:

    from sklearn.metrics import accuracy_score
    
  3. 使用超参数初始化 XGBoost 分类器。

    超参数微调是 第六章 的重点,XGBoost 超参数。在本章中,最重要的默认超参数已明确列出:

    xgb = XGBClassifier(booster='gbtree', objective='multi:softprob', max_depth=6, learning_rate=0.1, n_estimators=100, random_state=2, n_jobs=-1)
    

    前述超参数的简要描述如下:

    a) booster='gbtree'booster'gbtree' 代表梯度提升树,是 XGBoost 的默认基础学习器。尽管不常见,但也可以使用其他基础学习器,这是一种我们在 第八章 中使用的策略,XGBoost 替代基础学习器

    b) objective='multi:softprob':标准的目标选项可以在 XGBoost 官方文档中查看,xgboost.readthedocs.io/en/latest/parameter.html,在 学习任务参数 部分。multi:softprob 目标是当数据集包含多个类别时,作为 binary:logistic 的标准替代方案。它计算分类的概率,并选择最高的一个。如果没有明确指定,XGBoost 通常会为你找到合适的目标。

    c) max_depth=6:树的 max_depth 决定了每棵树的分支数量。它是做出平衡预测时最重要的超参数之一。XGBoost 默认值为 6,不同于随机森林,后者不会提供值,除非明确编程。

    d) learning_rate=0.1:在 XGBoost 中,这个超参数通常被称为 eta。该超参数通过减少每棵树的权重来限制方差,达到给定的百分比。learning_rate 超参数在 第四章 中进行了详细探讨,从梯度提升到 XGBoost

    e) n_estimators=100:在集成方法中非常流行,n_estimators 是模型中的提升树数量。增加这个数量并降低 learning_rate 可以得到更稳健的结果。

  4. 将分类器拟合到数据上。

    这就是魔法发生的地方。整个 XGBoost 系统,包括前两节中探讨的细节,最佳参数选择,包括正则化约束,以及速度增强,例如近似分裂查找算法,以及阻塞和分片,都会在这行强大的 scikit-learn 代码中发生:

    xgb.fit(X_train, y_train)
    
  5. y 值预测为 y_pred

    y_pred = xgb.predict(X_test)
    
  6. 通过将 y_predy_test 进行比较来对模型进行评分:

    score = accuracy_score(y_pred, y_test)
    
  7. 显示你的结果:

    print('Score: ' + str(score))
    Score: 0.9736842105263158
    

不幸的是,Iris 数据集没有官方的得分列表,得分太多无法在一个地方汇总。使用默认超参数,在 Iris 数据集上的初始得分为 97.4 百分比,非常不错(参见 www.kaggle.com/c/serpro-iris/leaderboard)。

前面段落中提供的 XGBoost 分类器模板并非最终版本,而是未来构建模型的起点。

糖尿病数据集

现在你已经熟悉了 scikit-learn 和 XGBoost,你正逐步培养起快速构建和评分 XGBoost 模型的能力。在本节中,提供了一个使用 cross_val_score 的 XGBoost 回归器模板,并应用于 scikit-learn 的糖尿病数据集。

在构建模板之前,请导入预测列为 X,目标列为 y,如下所示:

X,y = datasets.load_diabetes(return_X_y=True)

现在我们已导入预测列和目标列,让我们开始构建模板。

XGBoost 回归器模板(交叉验证)

以下是在 scikit-learn 中使用交叉验证构建 XGBoost 回归模型的基本步骤,假设已定义预测列 X 和目标列 y

  1. 导入 XGBRegressorcross_val_score

    from sklearn.model_selection import cross_val_score
    from xgboost import XGBRegressor
    
  2. 初始化 XGBRegressor

    在这里,我们初始化 XGBRegressor,并设置 objective='reg:squarederror',即均方误差(MSE)。最重要的超参数默认值已明确给出:

    xgb = XGBRegressor(booster='gbtree', objective='reg:squarederror', max_depth=6, learning_rate=0.1, n_estimators=100, random_state=2, n_jobs=-1)
    
  3. 使用 cross_val_score 进行回归器的拟合和评分。

    使用 cross_val_score,拟合和评分在一个步骤中完成,输入包括模型、预测列、目标列和评分:

    scores = cross_val_score(xgb, X, y, scoring='neg_mean_squared_error', cv=5)
    
  4. 显示结果。

    回归得分通常以均方根误差 (RMSE) 展示,以保持单位一致:

    rmse = np.sqrt(-scores)
    print('RMSE:', np.round(rmse, 3))
    print('RMSE mean: %0.3f' % (rmse.mean()))
    

    结果如下:

    RMSE: [63.033 59.689 64.538 63.699 64.661]
    RMSE mean: 63.124
    

没有比较基准,我们无法理解该得分的意义。将目标列 y 转换为 pandas DataFrame 并使用 .describe() 方法将显示预测列的四分位数和一般统计数据,如下所示:

pd.DataFrame(y).describe()

这是预期的输出:

图 5.2 – 描述 y,糖尿病目标列的统计数据

图 5.2 – 描述 y,糖尿病目标列的统计数据

得分为 63.124 小于 1 个标准差,这是一个可敬的结果。

现在你拥有了可以用于构建模型的 XGBoost 分类器和回归器模板。

现在你已经习惯在 scikit-learn 中构建 XGBoost 模型,是时候深入探索高能物理学了。

寻找希格斯玻色子 – 案例研究

在本节中,我们将回顾希格斯玻色子 Kaggle 竞赛,该竞赛使 XGBoost 成为机器学习的焦点。为了铺垫背景,在进入模型开发之前,首先提供历史背景。我们构建的模型包括当时 XGBoost 提供的默认模型和 Gabor Melis 提供的获胜解决方案的参考。本节内容无需 Kaggle 账户,因此我们不会花时间展示如何提交结果。如果你感兴趣,我们已提供了相关指南。

物理背景

在大众文化中,希格斯玻色子被称为上帝粒子。该粒子由彼得·希格斯在 1964 年提出,用于解释为什么粒子具有质量。

寻找希格斯玻色子的过程最终在 2012 年通过大型强子对撞机在瑞士日内瓦的 CERN 发现达到了高潮。诺贝尔奖颁发了,物理学的标准模型,即解释所有已知物理力(除了重力之外)的模型,变得比以往任何时候都更加重要。

希格斯玻色子是通过以极高速度碰撞质子并观察结果来发现的。观测数据来自ATLAS探测器,该探测器记录了每秒数亿次质子-质子碰撞所产生的数据,具体内容可以参考竞赛的技术文档《Learning to discover: the Higgs boson machine learning challenge》,higgsml.lal.in2p3.fr/files/2014/04/documentation_v1.8.pdf

在发现希格斯玻色子后,下一步是精确测量其衰变特性。ATLAS 实验通过从背景噪声中提取的数据发现希格斯玻色子衰变成两个tau粒子。为了更好地理解数据,ATLAS 寻求了机器学习社区的帮助。

Kaggle 竞赛

Kaggle 竞赛是一种旨在解决特定问题的机器学习竞赛。机器学习竞赛在 2006 年变得有名,当时 Netflix 提供了 100 万美元奖励给任何能够改进其电影推荐系统 10%的人。2009 年,100 万美元奖金被颁发给了BellKorPragmatic Chaos团队 (www.wired.com/2009/09/bellkors-pragmatic-chaos-wins-1-million-netflix-prize/)。

许多企业、计算机科学家、数学家和学生开始意识到机器学习在社会中的日益重要性。机器学习竞赛逐渐火热,企业主和机器学习从业者都从中获得了互利的好处。从 2010 年开始,许多早期采用者前往 Kaggle 参与机器学习竞赛。

2014 年,Kaggle 宣布了希格斯玻色子机器学习挑战赛与 ATLAS 合作(www.kaggle.com/c/higgs-boson)。比赛奖金池为 13,000 美元,共有 1,875 支队伍参加了比赛。

在 Kaggle 比赛中,提供了训练数据以及所需的评分方法。团队在训练数据上构建机器学习模型,然后提交结果。测试数据的目标列不会提供。然而,允许多次提交,参赛者可以在最终日期之前不断优化自己的模型。

Kaggle 比赛是测试机器学习算法的沃土。与工业界不同,Kaggle 比赛吸引了成千上万的参赛者,这使得获奖的机器学习模型经过了非常充分的测试。

XGBoost 与希格斯挑战

XGBoost 于 2014 年 3 月 27 日公开发布,早于希格斯挑战赛 6 个月。在比赛中,XGBoost 大放异彩,帮助参赛者在 Kaggle 排行榜上攀升,同时节省了宝贵的时间。

让我们访问数据,看看参赛者们在使用什么数据。

数据

我们将使用源自 CERN 开放数据门户的原始数据,而不是 Kaggle 提供的数据:opendata.cern.ch/record/328。CERN 数据与 Kaggle 数据的区别在于,CERN 数据集要大得多。我们将选择前 250,000 行,并进行一些修改以匹配 Kaggle 数据。

你可以直接从github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter05下载 CERN 希格斯玻色子数据集。

atlas-higgs-challenge-2014-v2.csv.gz文件读取到pandas数据框中。请注意,我们仅选择前 250,000 行,并且使用compression=gzip参数,因为数据集是以csv.gz文件形式压缩的。读取数据后,查看前五行,如下所示:

df = pd.read_csv('atlas-higgs-challenge-2014-v2.csv.gz', nrows=250000, compression='gzip')
df.head()

输出的最右边几列应与以下截图所示相同:

图 5.3 – CERN 希格斯玻色子数据 – 包含 Kaggle 列

图 5.3 – CERN 希格斯玻色子数据 – 包含 Kaggle 列

请注意KagglesetKaggleWeight列。由于 Kaggle 数据集较小,Kaggle 在其权重列中使用了不同的数字,该列在前面的图中表示为KaggleWeightKaggleset下的t值表示它是 Kaggle 数据集的训练集的一部分。换句话说,这两列,KagglesetKaggleWeight,是 CERN 数据集中的列,用于包含将被用于 Kaggle 数据集的信息。在本章中,我们将限制 CERN 数据的子集为 Kaggle 训练集。

为了匹配 Kaggle 训练数据,我们将删除KagglesetWeight列,将KaggleWeight转换为'Weight',并将'Label'列移到最后一列,如下所示:

del df[‹Weight›]
del df[‹KaggleSet›]
df = df.rename(columns={«KaggleWeight»: «Weight»})

一种移动Label列的方法是将其存储为一个变量,删除该列,然后通过将其分配给新变量来添加新列。每当将新列分配给 DataFrame 时,新列会出现在末尾:

label_col = df['Label']
del df['Label']
df['Label'] = label_col

现在所有更改已经完成,CERN 数据与 Kaggle 数据一致。接下来,查看前五行数据:

df.head()

这是期望输出的左侧部分:

图 5.4 – CERN 希格斯玻色子数据 – 物理列

图 5.4 – CERN 希格斯玻色子数据 – 物理列

许多列没有显示,并且出现了-999.00这一不寻常的值。

EventId之后的列包含以PRI为前缀的变量,PRI代表原始值,即在碰撞过程中由探测器直接测量的值。相比之下,标记为DER的列是从这些测量值中得出的数值推导。

所有列名和类型可以通过df.info()查看:

df.info()

这是输出的一个示例,中间的列已被截断以节省空间:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 250000 entries, 0 to 249999
Data columns (total 33 columns):
 #   Column                       Non-Null Count   Dtype  
---  ------                       --------------   -----  
 0   EventId                      250000 non-null  int64  
 1   DER_mass_MMC                 250000 non-null  float64
 2   DER_mass_transverse_met_lep  250000 non-null  float64
 3   DER_mass_vis                 250000 non-null  float64
 4   DER_pt_h                     250000 non-null  float64
…
 28  PRI_jet_subleading_eta       250000 non-null  float64
 29  PRI_jet_subleading_phi       250000 non-null  float64
 30  PRI_jet_all_pt               250000 non-null  float64
 31  Weight                       250000 non-null  float64
 32  Label                        250000 non-null  object  
dtypes: float64(30), int64(3)
memory usage: 62.9 MB

所有列都有非空值,只有最后一列Label是非数字类型。列可以按如下方式分组:

  • 0EventId – 对于机器学习模型无关。

  • 1-30:来自 LHC 碰撞的物理列。这些列的详细信息可以在higgsml.lal.in2p3.fr/documentation中的技术文档链接中找到。这些是机器学习的预测列。

  • 31Weight – 该列用于对数据进行缩放。问题在于希格斯玻色子事件非常稀有,因此一个 99.9%准确率的机器学习模型可能无法找到它们。权重弥补了这一不平衡,但测试数据中没有权重。处理权重的策略将在本章后续部分讨论,并在第七章中讨论,使用 XGBoost 发现外星行星

  • 32Label – 这是目标列,标记为s表示信号,b表示背景。训练数据是从实际数据模拟而来,因此信号比实际情况下更多。信号是指希格斯玻色子衰变的发生。

数据的唯一问题是目标列Label不是数字类型。通过将s值替换为1,将b值替换为0,将Label列转换为数字列,如下所示:

df['Label'].replace(('s', 'b'), (1, 0), inplace=True)

现在所有列都变为数字类型并且没有空值,你可以将数据拆分为预测列和目标列。回顾一下,预测列的索引是 1–30,目标列是最后一列,索引为32(或 -1)。注意,Weight列不应包含在内,因为测试数据中没有该列:

X = df.iloc[:,1:31]
y = df.iloc[:,-1]

打分

Higgs 挑战不是普通的 Kaggle 竞赛。除了理解高能物理以进行特征工程(这不是我们将要追求的路径)的难度外,评分方法也不是标准的。Higgs 挑战要求优化 近似中位数显著性 (AMS)。

AMS 的定义如下:

这里, 是真阳性率, 是假阳性率,而 是一个常数正则化项,其值为 10

幸运的是,XGBoost 为比赛提供了一种 AMS 评分方法,因此不需要正式定义。高 AMS 的结果来自许多真阳性和很少的假阴性。关于为何选择 AMS 而不是其他评分方法的理由在技术文档中说明,地址为 higgsml.lal.in2p3.fr/documentation

提示

可以构建自己的评分方法,但通常不需要。在极少数需要构建自定义评分方法的情况下,你可以查看 scikit-learn.org/stable/modules/model_evaluation.html 获取更多信息。

权重

在构建 Higgs 玻色子的机器学习模型之前,了解并利用权重非常重要。

在机器学习中,权重可以用来提高不平衡数据集的准确性。考虑 Higgs 挑战中的 s(信号)和 b(背景)列。实际上,s << b,所以信号在背景噪声中非常稀少。例如,信号比背景噪声少 1,000 倍。你可以创建一个权重列,其中 b = 1,s = 1/1000 以补偿这种不平衡。

根据比赛的技术文档,权重列定义为 s(信号)事件。

首先应将权重按比例缩放以匹配测试数据,因为测试数据提供了测试集生成的信号和背景事件的预期数量。测试数据有 550,000 行,比训练数据提供的 250,000 行(len(y))多两倍以上。将权重按比例缩放以匹配测试数据可以通过将权重列乘以增加百分比来实现,如下所示:

df['test_Weight'] = df['Weight'] * 550000 / len(y)

接下来,XGBoost 提供了一个超参数 scale_pos_weight,它考虑了缩放因子。缩放因子是背景噪声权重之和除以信号权重之和。可以使用 pandas 的条件符号来计算缩放因子,如下所示:

s = np.sum(df[df['Label']==1]['test_Weight'])
b = np.sum(df[df['Label']==0]['test_Weight'])

在上述代码中,df[df['Label']==1] 缩小了 DataFrame 到 Label 列等于 1 的行,然后 np.sum 使用 test_Weight 列的值加总这些行的值。

最后,要查看实际比率,将 b 除以 s

b/s
593.9401931492318

总结来说,权重代表数据生成的预期信号和背景事件的数量。我们将权重缩放以匹配测试数据的大小,然后将背景权重之和除以信号权重之和,以建立 scale_pos_weight=b/s 超参数。

提示

要了解有关权重的更多详细讨论,请查看 KDnuggets 提供的精彩介绍:www.kdnuggets.com/2019/11/machine-learning-what-why-how-weighting.html

模型

现在是时候构建一个 XGBoost 模型来预测信号——即模拟的希格斯玻色子衰变事件。

在比赛开始时,XGBoost 是一种新工具,且 scikit-learn 的封装还没有发布。即便到了今天(2020 年),关于在 Python 中实现 XGBoost 的大多数信息仍然是在 scikit-learn 发布之前的版本。由于你可能会在线遇到 pre-scikit-learn 版本的 XGBoost Python API,而且这正是所有参与者在 Higgs Challenge 中使用的版本,因此本章只展示了使用原始 Python API 的代码。

以下是为 Higgs Challenge 构建 XGBoost 模型的步骤:

  1. 导入 xgboostxgb

    import xgboost as xgb
    
  2. 初始化 XGBoost 模型时,将 -999.0 设置为未知值。在 XGBoost 中,未知值可以通过 missing 超参数来设置,而不是将这些值转换为中位数、均值、众数或其他空值替代。在模型构建阶段,XGBoost 会自动选择最佳的拆分值。

  3. weight 超参数可以等于新列 df['test_Weight'],如 weight 部分所定义:

    xgb_clf = xgb.DMatrix(X, y, missing=-999.0, weight=df['test_Weight'])
    
  4. 设置其他超参数。

    以下超参数是 XGBoost 为竞赛提供的默认值:

    a) 初始化一个名为 param 的空字典:

    param = {}
    

    b) 将目标定义为 'binary:logitraw'

    这意味着一个二分类模型将从逻辑回归概率中创建。这个目标将模型定义为分类器,并允许对目标列进行排序,这是此特定 Kaggle 比赛提交所要求的:

    param['objective'] = 'binary:logitraw'
    

    c) 使用背景权重除以信号权重对正样本进行缩放。这有助于模型在测试集上表现得更好:

    param['scale_pos_weight'] = b/s
    

    d) 学习率 eta 设置为 0.1

    param['eta'] = 0.1
    

    e) max_depth 设置为 6

    param['max_depth'] = 6
    

    f) 将评分方法设置为 'auc',以便显示:

    param['eval_metric'] = 'auc'
    

    虽然会打印 AMS 分数,但评估指标设置为 auc,即 auc 是描述真正例与假正例的曲线,当其值为 1 时是完美的。与准确率类似,auc 是分类问题的标准评分指标,尽管它通常优于准确率,因为准确率对于不平衡数据集来说有局限性,正如在第七章《使用 XGBoost 发现系外行星》中所讨论的那样,难以遗忘

  5. 创建一个参数列表,包含前面提到的内容,并加上评估指标(auc)和ams@0.15,XGBoost 实现的使用 15%阈值的 AMS 分数:

    plst = list(param.items())+[('eval_metric', 'ams@0.15')]
    
  6. 创建一个观察列表,包含初始化的分类器和'train',这样你就可以在树继续提升的过程中查看分数:

    watchlist = [ (xg_clf, 'train') ]
    
  7. 将提升轮次设置为120

    num_round = 120
    
  8. 训练并保存模型。通过将参数列表、分类器、轮次和观察列表作为输入来训练模型。使用save_model方法保存模型,这样你就不必再经过耗时的训练过程。然后,运行代码并观察随着树的提升,分数如何提高:

    print ('loading data end, start to boost trees')
    bst = xgb.train( plst, xgmat, num_round, watchlist )
    bst.save_model('higgs.model')
    print ('finish training')
    

    你的结果应该包括以下输出:

    [110]	train-auc:0.94505	train-ams@0.15:5.84830
    [111]	train-auc:0.94507	train-ams@0.15:5.85186
    [112]	train-auc:0.94519	train-ams@0.15:5.84451
    [113]	train-auc:0.94523	train-ams@0.15:5.84007
    [114]	train-auc:0.94532	train-ams@0.15:5.85800
    [115]	train-auc:0.94536	train-ams@0.15:5.86228
    [116]	train-auc:0.94550	train-ams@0.15:5.91160
    [117]	train-auc:0.94554	train-ams@0.15:5.91842
    [118]	train-auc:0.94565	train-ams@0.15:5.93729
    [119]	train-auc:0.94580	train-ams@0.15:5.93562
    finish training
    

恭喜你构建了一个能够预测希格斯玻色子衰变的 XGBoost 分类器!

该模型的auc94.58百分比,AMS 为5.9。就 AMS 而言,竞赛的最高值在三分之三的上方。该模型在提交测试数据时,AMS 大约为3.6

你刚刚构建的模型是由 Tanqi Chen 为 XGBoost 用户在竞赛期间提供的基准模型。竞赛的获胜者,Gabor Melis,使用这个基准模型来构建他的模型。从查看获胜解决方案github.com/melisgl/higgsml并点击xgboost-scripts可以看出,对基准模型所做的修改并不显著。Melis 和大多数 Kaggle 参赛者一样,也进行了特征工程,向数据中添加了更多相关列,这是我们将在第九章中讨论的内容,XGBoost Kaggle 大师

在截止日期之后,你是可以自己构建和训练模型,并通过 Kaggle 提交的。对于 Kaggle 竞赛,提交必须经过排名、正确索引,并且必须使用 Kaggle API 主题进行进一步说明。如果你希望提交实际竞赛的模型,XGBoost 排名代码对你可能有所帮助,可以在github.com/dmlc/xgboost/blob/master/demo/kaggle-higgs/higgs-pred.py找到。

总结

在这一章中,你学习了 XGBoost 是如何通过处理缺失值、稀疏矩阵、并行计算、分片和块等技术,来提高梯度提升的准确性和速度。你学习了 XGBoost 目标函数背后的数学推导,该函数决定了梯度下降和正则化的参数。你使用经典的 scikit-learn 数据集构建了XGBClassifierXGBRegressor模板,获得了非常好的分数。最后,你构建了 XGBoost 为希格斯挑战提供的基准模型,这个模型最终引领了获胜解决方案,并将 XGBoost 推到了聚光灯下。

既然你已经对 XGBoost 的整体叙述、设计、参数选择和模型构建模板有了扎实的理解,在下一章中,你将微调 XGBoost 的超参数,以达到最佳评分。

第六章:第六章:XGBoost 超参数

XGBoost 有许多超参数。XGBoost 的基础学习器超参数包含所有决策树的超参数作为起点。由于 XGBoost 是梯度提升的增强版,因此也有梯度提升的超参数。XGBoost 特有的超参数旨在提升准确性和速度。然而,一次性尝试解决所有 XGBoost 超参数可能会让人感到头晕。

第二章中,决策树深入剖析,我们回顾并应用了基础学习器超参数,如max_depth,而在第四章中,从梯度提升到 XGBoost,我们应用了重要的 XGBoost 超参数,包括n_estimatorslearning_rate。我们将在本章中再次回顾这些超参数,并介绍一些新的 XGBoost 超参数,如gamma,以及一种叫做早停法的技术。

在本章中,为了提高 XGBoost 超参数微调的熟练度,我们将讨论以下主要主题:

  • 准备数据和基础模型

  • 调整核心 XGBoost 超参数

  • 应用早停法

  • 将所有内容整合在一起

技术要求

本章的代码可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter06找到。

准备数据和基础模型

在介绍和应用 XGBoost 超参数之前,让我们做好以下准备:

  • 获取心脏病数据集

  • 构建XGBClassifier模型

  • 实现StratifiedKFold

  • 基准 XGBoost 模型进行评分

  • GridSearchCVRandomizedSearchCV结合,形成一个强大的函数

良好的准备对于在微调超参数时获得准确性、一致性和速度至关重要。

心脏病数据集

本章使用的数据集是最初在第二章中提出的心脏病数据集,决策树深入剖析。我们选择相同的数据集,以最大化超参数微调的时间,并最小化数据分析的时间。让我们开始这个过程:

  1. 访问github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter06,加载heart_disease.csv到 DataFrame 中,并显示前五行。以下是代码:

    import pandas as pd
    df = pd.read_csv('heart_disease.csv')
    df.head()
    

    结果应如下所示:

    图 6.1 - 前五行数据

    图 6.1 - 前五行数据

    最后一列,target,是目标列,1表示存在,表示患者患有心脏病,2表示不存在。有关其他列的详细信息,请访问archive.ics.uci.edu/ml/datasets/Heart+Disease上的 UCI 机器学习库,或参见第二章《决策树深入剖析》。

  2. 现在,检查df.info()以确保数据全部为数值型且没有空值:

    df.info()
    

    这是输出结果:

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 303 entries, 0 to 302
    Data columns (total 14 columns):
     #   Column    Non-Null Count  Dtype  
    ---  ------    --------------  -----  
     0   age       303 non-null    int64  
     1   sex       303 non-null    int64  
     2   cp        303 non-null    int64  
     3   trestbps  303 non-null    int64  
     4   chol      303 non-null    int64  
     5   fbs       303 non-null    int64  
     6   restecg   303 non-null    int64  
     7   thalach   303 non-null    int64  
     8   exang     303 non-null    int64  
     9   oldpeak   303 non-null    float64
     10  slope     303 non-null    int64  
     11  ca        303 non-null    int64  
     12  thal      303 non-null    int64  
     13  target    303 non-null    int64  
    dtypes: float64(1), int64(13)
    memory usage: 33.3 KB
    

由于所有数据点都是非空且数值型的,数据已经准备好用于机器学习。现在是时候构建一个分类器了。

XGBClassifier

在调整超参数之前,我们先构建一个分类器,以便获得一个基准评分作为起点。

构建 XGBoost 分类器的步骤如下:

  1. 从各自的库中下载XGBClassifieraccuracy_score。代码如下:

    from xgboost import XGBClassifier
    from sklearn.metrics import accuracy_score
    
  2. 声明X为预测列,y为目标列,其中最后一行是目标列:

    X = df.iloc[:, :-1]
    y = df.iloc[:, -1]
    
  3. 使用booster='gbtree'objective='binary:logistic'的默认设置初始化XGBClassifier,并将random_state=2

    model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
    

    'gbtree'提升器,即基本学习器,是一个梯度提升树。'binary:logistic'目标是二分类中常用的标准目标,用于确定损失函数。虽然XGBClassifier默认包含这些值,但我们在此明确指出,以便熟悉这些设置,并为后续章节的修改做准备。

  4. 为了评估基准模型,导入cross_val_scorenumpy,以便进行拟合、评分并显示结果:

    from sklearn.model_selection import cross_val_score
    import numpy as np
    scores = cross_val_score(model, X, y, cv=5)
    print('Accuracy:', np.round(scores, 2))
    print('Accuracy mean: %0.2f' % (scores.mean()))
    

    准确率如下:

    Accuracy: [0.85 0.85 0.77 0.78 0.77]
    Accuracy mean: 0.81
    

81%的准确率是一个非常好的起点,远高于在第二章《决策树深入剖析》中,DecisionTreeClassifier通过交叉验证获得的 76%。

我们在这里使用了cross_val_score,并将使用GridSearchCV来调整超参数。接下来,让我们找到一种方法,确保使用StratifiedKFold时测试折叠保持一致。

StratifiedKFold

在调整超参数时,GridSearchCVRandomizedSearchCV是标准选项。来自第二章《决策树深入剖析》中的一个问题是,cross_val_scoreGridSearchCV/RandomizedSearchCV在划分数据时方式不同。

一种解决方案是在使用交叉验证时使用StratifiedKFold

分层折叠法在每一折中都包含相同百分比的目标值。如果数据集中目标列中包含 60%的 1 和 40%的 0,则每个分层测试集都包含 60%的 1 和 40%的 0。当折叠是随机的时,可能会出现一个测试集包含 70%-30%的划分,而另一个测试集包含 50%-50%的目标值划分。

提示

使用train_test_split时,shuffle 和 stratify 参数使用默认设置,以帮助你对数据进行分层抽样。有关一般信息,请参见scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html

使用StratifiedKFold时,执行以下操作:

  1. sklearn.model_selection中实现StratifiedKFold

    from sklearn.model_selection import StratifiedKFold
    
  2. 接下来,通过选择n_splits=5shuffle=Truerandom_state=2作为StratifiedKFold的参数来定义折叠数为kfold。请注意,random_state提供一致的索引排序,而shuffle=True允许初始时对行进行随机排序:

    kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=2)
    

    现在可以在cross_val_scoreGridSearchCVRandomizedSearchCV中使用kfold变量,以确保结果的一致性。

现在,让我们回到cross_val_score,使用kfold,这样我们就有了一个适当的基准进行比较。

基准模型

现在我们有了一种获取一致折叠的方法,是时候在cross_val_score中使用cv=kfold来评分一个正式的基准模型。代码如下:

scores = cross_val_score(model, X, y, cv=kfold)
print('Accuracy:', np.round(scores, 2))
print('Accuracy mean: %0.2f' % (scores.mean()))

准确率如下:

Accuracy: [0.72 0.82 0.75 0.8 0.82]
Accuracy mean: 0.78

分数下降了。这意味着什么?

重要的是不要过于执着于获得最高的评分。在这种情况下,我们在不同的折叠上训练了相同的XGBClassifier模型,并获得了不同的分数。这显示了在训练模型时,保持测试折叠的一致性的重要性,也说明了为什么分数不一定是最重要的。虽然在选择模型时,获得最好的评分是一个最佳策略,但这里的分数差异表明模型并不一定更好。在这种情况下,两个模型的超参数相同,分数差异归因于不同的折叠。

这里的关键是,当使用GridSearchCVRandomizedSearchCV微调超参数时,使用相同的折叠来获得新的分数,以确保分数的比较是公平的。

结合GridSearchCVRandomizedSearchCV

GridSearchCV在超参数网格中搜索所有可能的组合以找到最佳结果。RandomizedSearchCV默认选择 10 个随机超参数组合。通常,当GridSearchCV变得繁琐,无法逐一检查所有超参数组合时,RandomizedSearchCV就会被使用。

我们将GridSearchCVRandomizedSearchCV合并成一个精简的函数,而不是为它们写两个单独的函数,步骤如下:

  1. sklearn.model_selection导入GridSearchCVRandomizedSearchCV

    from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
    
  2. 定义一个grid_search函数,以params字典作为输入,random=False

    def grid_search(params, random=False): 
    
  3. 使用标准默认值初始化 XGBoost 分类器:

    xgb = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
    
  4. 如果random=True,则用xgbparams字典初始化RandomizedSearchCV。设置n_iter=20,以允许 20 个随机组合,而不是 10 个。否则,用相同的输入初始化GridSearchCV。确保设置cv=kfold以确保结果的一致性:

        if random:
            grid = RandomizedSearchCV(xgb, params, cv=kfold, n_iter=20, n_jobs=-1)
        else:
            grid = GridSearchCV(xgb, params, cv=kfold, n_jobs=-1)
    
  5. Xy拟合到grid模型:

    grid.fit(X, y)
    
  6. 获取并打印best_params_

    best_params = grid.best_params_
    print("Best params:", best_params)
    
  7. 获取并打印best_score_

    best_score = grid.best_score_
    print("Training score: {:.3f}".format(best_score))
    

现在可以使用grid_search函数来微调所有超参数。

调整 XGBoost 超参数

XGBoost 有许多超参数,其中一些在前几章已经介绍。下表总结了关键的 XGBoost 超参数,我们在本书中大部分进行了讨论。

注意

此处展示的 XGBoost 超参数并非详尽无遗,而是力求全面。要查看完整的超参数列表,请阅读官方文档中的XGBoost 参数xgboost.readthedocs.io/en/latest/parameter.html

紧随表格后,提供了进一步的解释和示例:

图 6.2 – XGBoost 超参数表

图 6.2 – XGBoost 超参数表

既然已经展示了关键的 XGBoost 超参数,让我们逐一调优它们,进一步了解它们的作用。

应用 XGBoost 超参数

本节中展示的 XGBoost 超参数通常由机器学习从业者进行微调。每个超参数简要解释后,我们将使用前面定义的grid_search函数测试标准的变动。

n_estimators

回顾一下,n_estimators表示集成中树的数量。在 XGBoost 中,n_estimators是训练残差的树的数量。

使用默认的100初始化n_estimators的网格搜索,然后将树的数量翻倍至800,如下所示:

grid_search(params={'n_estimators':[100, 200, 400, 800]})

输出如下:

Best params: {'n_estimators': 100}
Best score: 0.78235

由于我们的数据集较小,增加n_estimators并没有带来更好的结果。在本章的应用早期停止部分中讨论了寻找理想n_estimators值的策略。

learning_rate

learning_rate会缩小每一轮提升中树的权重。通过降低learning_rate,需要更多的树来获得更好的得分。降低learning_rate能防止过拟合,因为传递下来的权重较小。

默认值为0.3,尽管以前版本的 scikit-learn 使用的是0.1。这里是learning_rate的一个起始范围,已放入我们的grid_search函数中:

grid_search(params={'learning_rate':[0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5]})

输出如下:

Best params: {'learning_rate': 0.05}
Best score: 0.79585

改变学习率导致了轻微的增加。如在第四章中所述,从梯度提升到 XGBoost,当n_estimators增大时,降低learning_rate可能会带来好处。

max_depth

max_depth决定了树的长度,相当于分裂的轮次。限制max_depth可以防止过拟合,因为单棵树的生长受到max_depth的限制。XGBoost 默认的max_depth值为六:

grid_search(params={'max_depth':[2, 3, 5, 6, 8]})

输出如下:

Best params: {'max_depth': 2}
Best score: 0.79902

max_depth6调整到2后得到了更好的分数。较低的max_depth值意味着方差已被减少。

gamma

被称为gamma的值为节点提供了一个阈值,只有超过这个阈值后,节点才会根据损失函数进行进一步的分裂。gamma没有上限,默认值为0,而大于10的值通常认为非常高。增大gamma会使模型变得更加保守:

grid_search(params={'gamma':[0, 0.1, 0.5, 1, 2, 5]})

输出如下:

Best params: {'gamma': 0.5}
Best score: 0.79574

gamma0调整到0.5带来了轻微的改善。

min_child_weight

min_child_weight表示一个节点进行分裂成子节点所需的最小权重和。如果权重和小于min_child_weight的值,则不会进行进一步的分裂。通过增大min_child_weight的值,可以减少过拟合:

grid_search(params={'min_child_weight':[1, 2, 3, 4, 5]})

输出如下:

Best params: {'min_child_weight': 5}
Best score: 0.81219

min_child_weight的微调给出了最佳结果。

subsample

subsample超参数限制了每次提升轮次的训练实例(行)百分比。将subsample从 100%降低有助于减少过拟合:

grid_search(params={'subsample':[0.5, 0.7, 0.8, 0.9, 1]})

输出如下:

Best params: {'subsample': 0.8}
Best score: 0.79579

分数再次略有提高,表明存在轻微的过拟合现象。

colsample_bytree

类似于subsamplecolsample_bytree根据给定的百分比随机选择特定的列。colsample_bytree有助于限制列的影响并减少方差。请注意,colsample_bytree接受的是百分比作为输入,而不是列的数量:

grid_search(params={'colsample_bytree':[0.5, 0.7, 0.8, 0.9, 1]})

输出如下:

Best params: {'colsample_bytree': 0.7}
Best score: 0.79902

在这里的增益最多也只是微乎其微。建议你尝试自行使用colsample_bylevelcolsample_bynodecolsample_bylevel在每一层深度随机选择列,而colsample_bynode则在评估每个树的分裂时随机选择列。

微调超参数既是一门艺术,也是一门科学。与这两种学科一样,采取不同的策略都会有效。接下来,我们将探讨早停法作为微调n_estimators的一种特定策略。

应用早停法

早停法是一种通用方法,用于限制迭代机器学习算法的训练轮次。在本节中,我们将探讨如何通过eval_seteval_metricearly_stopping_rounds来应用早停法。

什么是早停法?

早停法为迭代机器学习算法的训练轮次提供了限制。与预定义训练轮次不同,早停法允许训练继续,直到n次连续的训练轮次未能带来任何增益,其中n是由用户决定的数字。

仅选择n_estimators的 100 的倍数是没有意义的。也许最佳值是 737 而不是 700。手动找到这么精确的值可能会很累人,特别是当超参数调整可能需要在后续进行更改时。

在 XGBoost 中,每轮增强后可能会确定一个得分。尽管得分会上下波动,但最终得分将趋于稳定或朝错误方向移动。

当所有后续得分未提供任何增益时,达到峰值分数。在经过 10、20 或 100 个训练轮次未能改进得分后,您会确定峰值。您可以选择轮次数。

在早停策略中,给予模型足够的失败时间是很重要的。如果模型停止得太早,比如在连续五轮没有改进后停止,那么模型可能会错过稍后可以捕捉到的一般模式。与深度学习类似,早停策略经常被使用,梯度提升需要足够的时间来在数据中找到复杂的模式。

对于 XGBoost,early_stopping_rounds是应用早停策略的关键参数。如果early_stopping_rounds=10,则模型将在连续 10 个训练轮次未能改进模型后停止训练。类似地,如果early_stopping_rounds=100,则训练将持续直到连续 100 轮未能改进模型。

现在您了解了什么是早停策略后,让我们来看看eval_seteval_metric

eval_set 和 eval_metric

early_stopping_rounds不是一个超参数,而是优化n_estimators超参数的策略。

通常在选择超参数时,会在所有增强轮次完成后给出测试分数。要使用早停策略,我们需要在每一轮后得到一个测试分数。

可以将eval_metriceval_set用作.fit的参数,以生成每个训练轮次的测试分数。eval_metric提供评分方法,通常为分类时的'error'和回归时的'rmse'eval_set提供要评估的测试集,通常为X_testy_test

以下六个步骤显示了每轮训练的评估指标,其中默认n_estimators=100

  1. 将数据分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    
  2. 初始化模型:

    model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
    
  3. 声明eval_set

    eval_set = [(X_test, y_test)]
    
  4. 声明eval_metric

    eval_metric = 'error'
    
  5. 使用eval_metriceval_set拟合模型:

    model.fit(X_train, y_train, eval_metric=eval_metric, eval_set=eval_set)
    
  6. 检查最终得分:

    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    print("Accuracy: %.2f%%" % (accuracy * 100.0))
    

    这里是截断的输出:

    [0]	validation_0-error:0.15790
    [1]	validation_0-error:0.10526
    [2]	validation_0-error:0.11842
    [3]	validation_0-error:0.13158
    [4]	validation_0-error:0.11842
    …
    [96]	validation_0-error:0.17105
    [97]	validation_0-error:0.17105
    [98]	validation_0-error:0.17105
    [99]	validation_0-error:0.17105
    Accuracy: 82.89%
    

不要对得分过于激动,因为我们尚未使用交叉验证。事实上,我们知道当n_estimators=100时,StratifiedKFold交叉验证给出的平均准确率为 78%。得分差异来自于测试集的不同。

early_stopping_rounds

early_stopping_rounds是在拟合模型时与eval_metriceval_set一起使用的可选参数。

让我们尝试early_stopping_rounds=10

以添加early_stopping_rounds=10的方式重复了上述代码:

model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
eval_set = [(X_test, y_test)]
eval_metric='error'
model.fit(X_train, y_train, eval_metric="error", eval_set=eval_set, early_stopping_rounds=10, verbose=True)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy: %.2f%%" % (accuracy * 100.0))

输出如下所示:

[0]	validation_0-error:0.15790
Will train until validation_0-error hasn't improved in 10 rounds.
[1]	validation_0-error:0.10526
[2]	validation_0-error:0.11842
[3]	validation_0-error:0.13158
[4]	validation_0-error:0.11842
[5]	validation_0-error:0.14474
[6]	validation_0-error:0.14474
[7]	validation_0-error:0.14474
[8]	validation_0-error:0.14474
[9]	validation_0-error:0.14474
[10]	validation_0-error:0.14474
[11]	validation_0-error:0.15790
Stopping. Best iteration:
[1]	validation_0-error:0.10526
Accuracy: 89.47%

结果可能会让人惊讶。早停止显示n_estimators=2给出了最佳结果,这可能是测试折叠的原因。

为什么只有两棵树?只给模型 10 轮来提高准确性,可能数据中的模式尚未被发现。然而,数据集非常小,因此两轮提升可能给出了最佳结果。

一个更彻底的方法是使用更大的值,比如n_estimators = 5000early_stopping_rounds=100

通过设置early_stopping_rounds=100,您将确保达到 XGBoost 提供的默认100个提升树。

这是一段代码,最多生成 5,000 棵树,并在连续 100 轮未找到任何改进时停止:

model = XGBClassifier(random_state=2, n_estimators=5000)
eval_set = [(X_test, y_test)]
eval_metric="error"
model.fit(X_train, y_train, eval_metric=eval_metric, eval_set=eval_set, early_stopping_rounds=100)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy: %.2f%%" % (accuracy * 100.0))

这里是截断的输出:

[0]	validation_0-error:0.15790
Will train until validation_0-error hasn't improved in 100 rounds.
[1]	validation_0-error:0.10526
[2]	validation_0-error:0.11842
[3]	validation_0-error:0.13158
[4]	validation_0-error:0.11842
...
[98]	validation_0-error:0.17105
[99]	validation_0-error:0.17105
[100]	validation_0-error:0.17105
[101]	validation_0-error:0.17105
Stopping. Best iteration:
[1]	validation_0-error:0.10526
Accuracy: 89.47%

在 100 轮提升后,两棵树提供的分数仍然是最佳的。

最后一点要注意的是,早停止在大型数据集中特别有用,当不清楚应该瞄准多高时。

现在,让我们使用早停止的结果,以及之前调整的所有超参数来生成最佳模型。

结合超参数

是时候将本章的所有组件结合起来,以提高通过交叉验证获得的 78%分数。

正如您所知,没有一种适合所有情况的超参数微调方法。一种方法是使用RandomizedSearchCV输入所有超参数范围。更系统化的方法是逐个处理超参数,使用最佳结果进行后续迭代。所有方法都有优势和局限性。无论采取何种策略,尝试多种变化并在数据到手时进行调整是至关重要的。

一次调整一个超参数

使用系统化的方法,我们一次添加一个超参数,并在途中聚合结果。

n_estimators

尽管n_estimators值为2给出了最佳结果,但值得在grid_search函数上尝试一系列值,该函数使用交叉验证:

grid_search(params={'n_estimators':[2, 25, 50, 75, 100]})

输出如下:

Best params: {'n_estimators': 50}
Best score: 0.78907

没有什么奇怪的,n_estimators=50,介于先前最佳值 2 和默认值 100 之间,给出了最佳结果。由于早停止没有使用交叉验证,这里的结果是不同的。

最大深度

max_depth超参数确定每棵树的长度。这里是一个不错的范围:

grid_search(params={'max_depth':[1, 2, 3, 4, 5, 6, 7, 8], 'n_estimators':[50]})

输出如下:

Best params: {'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869

这是一个非常可观的收益。深度为 1 的树称为决策树桩。通过调整只有两个超参数,我们从基线模型中获得了四个百分点的增长。

保留顶级值的方法的局限性在于我们可能会错过更好的组合。也许n_estimators=2n_estimators=100max_depth结合会给出更好的结果。让我们看看:

grid_search(params={'max_depth':[1, 2, 3, 4, 6, 7, 8], 'n_estimators':[2, 50, 100]})

输出如下:

Best params: {'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869

n_estimators=50max_depth=1仍然给出了最佳结果,因此我们将继续使用它们,并稍后回到我们的早停止分析。

学习率

由于n_estimators合理较低,调整learning_rate可能会改善结果。以下是一个标准范围:

grid_search(params={'learning_rate':[0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5], 'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'learning_rate': 0.3, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869

这是与之前获得的得分相同的结果。请注意,learning_rate值为 0.3 是 XGBoost 提供的默认值。

min_child_weight

让我们看看调整分裂为子节点所需的权重总和是否能提高得分:

grid_search(params={'min_child_weight':[1, 2, 3, 4, 5], 'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'max_depth': 1, 'min_child_weight': 1, 'n_estimators': 50}
Best score: 0.83869

在这种情况下,最佳得分保持不变。请注意,min_child_weight的默认值是 1。

subsample

如果减少方差是有益的,subsample可能通过限制样本的百分比来起作用。然而,在此情况下,初始只有 303 个样本,样本量较少使得调整超参数以提高得分变得困难。以下是代码:

grid_search(params={'subsample':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'max_depth': 1, 'n_estimators': 50, 'subsample': 1}
Best score: 0.83869

仍然没有提升。此时,你可能会想,若使用n_estimators=2,是否会继续带来新的提升。

让我们通过使用到目前为止所使用的值进行全面的网格搜索,找出结果。

grid_search(params={'subsample':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 
                    'min_child_weight':[1, 2, 3, 4, 5], 
                    'learning_rate':[0.1, 0.2, 0.3, 0.4, 0.5], 
                    'max_depth':[1, 2, 3, 4, 5],
                    'n_estimators':[2]})

输出结果如下:

Best params: {'learning_rate': 0.5, 'max_depth': 2, 'min_child_weight': 4, 'n_estimators': 2, 'subsample': 0.9}
Best score: 0.81224

一台仅有两棵树的分类器表现较差并不令人意外。尽管初始得分较高,但它没有经过足够的迭代,导致超参数没有得到显著调整。

超参数调整

当调整超参数方向时,RandomizedSearchCV非常有用,因为它涵盖了广泛的输入范围。

这是一个将新输入与先前知识结合的超参数值范围。通过RandomizedSearchCV限制范围增加了找到最佳组合的机会。回想一下,RandomizedSearchCV在总组合数量过多以至于网格搜索太耗时时特别有用。以下选项共有 4,500 种可能的组合:

grid_search(params={'subsample':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 
                    'min_child_weight':[1, 2, 3, 4, 5], 
                    'learning_rate':[0.1, 0.2, 0.3, 0.4, 0.5], 
                    'max_depth':[1, 2, 3, 4, 5, None], 
                    'n_estimators':[2, 25, 50, 75, 100]},
                    random=True)

输出结果如下:

Best params: {'subsample': 0.6, 'n_estimators': 25, 'min_child_weight': 4, 'max_depth': 4, 'learning_rate': 0.5}
Best score: 0.82208

这很有趣。不同的值取得了不错的结果。

我们将使用最佳得分对应的超参数继续调整。

Colsample

现在,我们按顺序尝试colsample_bytreecolsample_bylevelcolsample_bynode

colsample_bytree

让我们从colsample_bytree开始:

grid_search(params={'colsample_bytree':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'colsample_bytree': 1, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869

得分没有改善。接下来,尝试colsample_bylevel

colsample_bylevel

使用以下代码尝试colsample_bylevel

grid_search(params={'colsample_bylevel':[0.5, 0.6, 0.7, 0.8, 0.9, 1],'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'colsample_bylevel': 1, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869

仍然没有提升。

看来我们在浅层数据集上已经达到峰值。让我们尝试不同的方法。与其单独使用colsample_bynode,不如一起调整所有 colsamples。

colsample_bynode

尝试以下代码:

grid_search(params={'colsample_bynode':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'colsample_bylevel':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'colsample_bytree':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'colsample_bylevel': 0.9, 'colsample_bynode': 0.5, 'colsample_bytree': 0.8, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.84852

非常好。通过共同调整,colsamples 组合起来取得了目前为止的最高得分,比原始得分高出了 5 个百分点。

gamma

我们将尝试微调的最后一个超参数是gamma。这里是一个旨在减少过拟合的gamma值范围:

grid_search(params={'gamma':[0, 0.01, 0.05, 0.1, 0.5, 1, 2, 3], 'colsample_bylevel':[0.9], 'colsample_bytree':[0.8], 'colsample_bynode':[0.5], 'max_depth':[1], 'n_estimators':[50]})

输出结果如下:

Best params: {'colsample_bylevel': 0.9, 'colsample_bynode': 0.5, 'colsample_bytree': 0.8, 'gamma': 0, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.84852

gamma仍然保持在默认值0

由于我们的最佳得分比原始得分高出超过五个百分点,这在 XGBoost 中是一个不小的成就,我们将在这里停止。

总结

在这一章节中,你通过使用 StratifiedKFold 建立了一个基准的 XGBoost 模型,为超参数调优做了准备。接着,你将 GridSearchCVRandomizedSearchCV 结合成一个强大的功能。你学习了 XGBoost 关键超参数的标准定义、范围和应用,并且掌握了一种名为早停(early stopping)的新技巧。你将所有功能、超参数和技巧进行了综合应用,成功地对心脏病数据集进行了调优,从默认的 XGBoost 分类器中提升了令人印象深刻的五个百分点。

XGBoost 超参数调优需要时间来掌握,而你已经走在了正确的道路上。调优超参数是一项关键技能,它将机器学习专家与初学者区分开来。了解 XGBoost 的超参数不仅有用,而且对于最大化机器学习模型的性能至关重要。

恭喜你完成了这一重要章节。

接下来,我们展示了一个 XGBoost 回归的案例研究,从头到尾,突出展示了 XGBClassifier 的强大功能、适用范围和应用。

第七章:第七章:利用 XGBoost 发现外星行星

在本章中,你将穿越星际,尝试使用XGBClassifier来发现外星行星。

本章的目的有两个。首先,掌握从头到尾使用 XGBoost 进行分析的实践经验非常重要,因为在实际应用中,这正是你通常需要做的事情。尽管你可能无法凭借 XGBoost 独立发现外星行星,但本章中你所实施的策略,包括选择正确的评分指标并根据该指标精心调整超参数,适用于 XGBoost 的任何实际应用。第二个原因是,本案例研究非常重要,因为所有机器学习从业者必须熟练处理不平衡数据集,这是本章的关键主题。

具体来说,你将掌握使用scale_pos_weight等技能。要从XGBClassifier中获得最佳结果,需要仔细分析数据的不平衡性,并明确手头的目标。在本章中,XGBClassifier是贯穿始终的核心工具,用来分析光数据并预测宇宙中的外星行星。

本章将涵盖以下主要内容:

  • 寻找外星行星

  • 分析混淆矩阵

  • 重采样不平衡数据

  • 调优和缩放 XGBClassifier

技术要求

本章的代码可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter07找到。

寻找外星行星

在本节中,我们将通过分析外星行星数据集来开始寻找外星行星。我们将在尝试通过绘制和观察光图来探测外星行星之前,提供外星行星发现的历史背景。绘制时间序列是一个有价值的机器学习技能,可以用来洞察任何时间序列数据集。最后,在揭示一个明显的缺陷之前,我们将利用机器学习做出初步预测。

历史背景

自古以来,天文学家就一直在从光线中收集信息。随着望远镜的出现,天文学知识在 17 世纪迎来了飞跃。望远镜与数学模型的结合使得 18 世纪的天文学家能够精确预测我们太阳系内的行星位置和日食现象。

在 20 世纪,天文学研究随着技术的进步和数学的复杂化不断发展。围绕其他恒星运转的行星——外星行星——被发现位于宜居区。位于宜居区的行星意味着该外星行星的位置和大小与地球相当,因此它可能存在液态水和生命。

这些外行星不是通过望远镜直接观测的,而是通过恒星光的周期性变化来推测的。周期性围绕一颗恒星旋转、足够大以阻挡可检测的恒光的一部分的物体,按定义是行星。从恒光中发现外行星需要在较长时间内测量光的波动。由于光的变化通常非常微小,因此很难判断是否确实存在外行星。

本章我们将使用 XGBoost 预测恒星是否有外行星。

外行星数据集

你在 第四章《从梯度提升到 XGBoost》中预览了外行星数据集,揭示了 XGBoost 在处理大数据集时相较于其他集成方法的时间优势。本章将更深入地了解外行星数据集。

这个外行星数据集来自于 NASA Kepler 太空望远镜第 3 次任务2016 年夏季。关于数据源的信息可以在 Kaggle 上找到,链接为 www.kaggle.com/keplersmachines/kepler-labelled-time-series-data。在数据集中的所有恒星中,5,050 颗没有外行星,而 37 颗有外行星。

超过 300 列和 5000 多行数据,总共有超过 150 万条数据点。当乘以 100 棵 XGBoost 树时,总共有 1.5 亿多个数据点。为了加速处理,我们从数据的一个子集开始。使用子集是处理大数据集时的常见做法,以节省时间。

pd.read_csv 包含一个 nrows 参数,用于限制行数。请注意,nrows=n 会选择数据集中的前 n 行。根据数据结构,可能需要额外的代码来确保子集能够代表整个数据集。我们开始吧。

导入 pandas,然后用 nrows=400 加载 exoplanets.csv。然后查看数据:

import pandas as pd
df = pd.read_csv('exoplanets.csv', nrows=400)
df.head()

输出应如下所示:

图 7.1 – 外行星数据框

图 7.1 – 外行星数据框

数据框下列出的大量列(3198列)是有道理的。在寻找光的周期性变化时,需要足够的数据点来发现周期性。我们太阳系内的行星公转周期从 88 天(水星)到 165 年(海王星)不等。如果要检测外行星,必须频繁检查数据点,以便不会错过行星在恒星前面经过的瞬间。

由于只有 37 颗外行星恒星,因此了解子集中包含了多少颗外行星恒星是很重要的。

.value_counts() 方法用于确定特定列中每个值的数量。由于我们关注的是 LABEL 列,可以使用以下代码查找外行星恒星的数量:

df['LABEL'].value_counts()

输出如下所示:

1    363 2     37 Name: LABEL, dtype: int64

我们的子集包含了所有的外行星恒星。如 .head() 所示,外行星恒星位于数据的开头。

绘制数据图表

期望的是,当外行星遮挡了恒星的光时,光通量会下降。如果光通量下降是周期性的,那么很可能是外行星在起作用,因为根据定义,行星是绕恒星运行的大型天体。

让我们通过绘图来可视化数据:

  1. 导入matplotlibnumpyseaborn,然后将seaborn设置为暗网格,如下所示:

    import matplotlib.pyplot as plt
    import numpy as np
    import seaborn as sns
    sns.set()
    

    在绘制光变曲线时,LABEL列不感兴趣。LABEL列将作为我们机器学习的目标列。

    提示

    推荐使用seaborn来改进你的matplotlib图表。sns.set()默认设置提供了一个漂亮的浅灰色背景和白色网格。此外,许多标准图表,如plt.hist(),在应用 Seaborn 默认设置后看起来更加美观。有关 Seaborn 的更多信息,请访问seaborn.pydata.org/

  2. 现在,让我们将数据拆分为X(预测列,我们将绘制它们)和y(目标列)。请注意,对于外行星数据集,目标列是第一列,而不是最后一列:

    X = df.iloc[:,1:]
    y = df.iloc[:,0]
    
  3. 现在编写一个名为light_plot的函数,该函数以数据的索引(行号)为输入,将所有数据点绘制为y坐标(光通量),并将观测次数作为x坐标。图表应使用以下标签:

    def light_plot(index):
        y_vals = X.iloc[index]
        x_vals = np.arange(len(y_vals))
        plt.figure(figsize=(15,8))
        plt.xlabel('Number of Observations')
        plt.ylabel('Light Flux')
        plt.title('Light Plot ' + str(index), size=15)
        plt.plot(x_vals, y_vals)
        plt.show()
    
  4. 现在,调用函数绘制第一个索引。这颗恒星已被分类为外行星恒星:

    light_plot(0)
    

    这是我们第一个光曲线图的预期图表:

    图 7.2 – 光曲线 0. 存在周期性光通量下降

    图 7.2 – 光曲线 0. 存在周期性光通量下降

    数据中存在明显的周期性下降。然而,仅凭这张图表,无法明确得出有外行星存在的结论。

  5. 做个对比,将这个图与第 37 个索引的图进行比较,后者是数据集中第一个非外行星恒星:

    light_plot(37)
    

    这是第 37 个索引的预期图表:

    图 7.3 – 光曲线 37

    图 7.3 – 光曲线 37

    存在光强度的增加和减少,但不是贯穿整个范围。

    数据中确实存在明显的下降,但它们在整个图表中并不是周期性的。下降的频率并没有一致地重复。仅凭这些证据,还不足以确定是否存在外行星。

  6. 这是外行星恒星的第二个光曲线图:

    light_plot(1)
    

    这是第一个索引的预期图表:

图 7.4 – 明显的周期性下降表明存在外行星

图 7.4 – 明显的周期性下降表明存在外行星

图表显示出明显的周期性,且光通量有大幅下降,这使得外行星的存在极为可能!如果所有图表都如此清晰,机器学习就不再必要。正如其他图表所示,得出外行星存在的结论通常没有这么明确。

这里的目的是突出数据的特点以及仅凭视觉图表分类系外行星的难度。天文学家使用不同的方法来分类系外行星,而机器学习就是其中的一种方法。

尽管这个数据集是一个时间序列,但目标不是预测下一个时间单位的光通量,而是基于所有数据来分类恒星。在这方面,机器学习分类器可以用来预测给定的恒星是否有系外行星。这个思路是用提供的数据来训练分类器,进而用它来预测新数据中的系外行星。在本章中,我们尝试使用XGBClassifier来对数据中的系外行星进行分类。在开始分类数据之前,我们必须先准备数据。

准备数据

我们在前一节中已经看到,并非所有图表都足够清晰,无法仅凭图表来确定系外行星的存在。这正是机器学习可以大有帮助的地方。首先,让我们为机器学习准备数据:

  1. 首先,我们需要确保数据集是数值型的且没有空值。使用df.info()来检查数据类型和空值:

    df.info()
    

    这是预期的输出:

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 400 entries, 0 to 399
    Columns: 3198 entries, LABEL to FLUX.3197
    dtypes: float64(3197), int64(1)
    memory usage: 9.8 MB
    

    子集包含 3,197 个浮点数和 1 个整数,因此所有列都是数值型的。由于列数较多,因此没有提供关于空值的信息。

  2. 我们可以对.null()方法使用.sum()两次,第一次是对每一列的空值求和,第二次是对所有列的空值求和:

    df.isnull().sum().sum()
    

    预期的输出如下:

    0
    

由于数据中没有空值,并且数据是数值型的,我们将继续进行机器学习。

初始的 XGBClassifier

要开始构建初始的 XGBClassifier,请按照以下步骤操作:

  1. 导入XGBClassifieraccuracy_score

    from xgboost import XGBClassifier from sklearn.metrics import accuracy_score
    
  2. 将模型拆分为训练集和测试集:

    from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    
  3. 使用booster='gbtree'objective='binary:logistic'random_state=2作为参数构建并评分模型:

    model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)model.fit(X_train, y_train)y_pred = model.predict(X_test)score = accuracy_score(y_pred, y_test)print('Score: ' + str(score))
    

    评分如下:

    Score: 0.89
    

正确分类 89%的恒星看起来是一个不错的起点,但有一个明显的问题。

你能弄明白吗?

假设你向天文学教授展示了你的模型。假设你的教授在数据分析方面受过良好训练,教授可能会回应:“我看到你得到了 89%的准确率,但系外行星仅占数据的 10%,那么你怎么知道你的结果不是比一个总是预测没有系外行星的模型更好呢?”

这就是问题所在。如果模型判断没有恒星包含系外行星,它的准确率将约为 90%,因为 10 颗恒星中有 9 颗不包含系外行星。

对于不平衡的数据,准确度并不够。

分析混淆矩阵

混淆矩阵是一个表格,用来总结分类模型的正确预测和错误预测。混淆矩阵非常适合分析不平衡数据,因为它提供了哪些预测正确,哪些预测错误的更多信息。

对于外行星子集,以下是完美混淆矩阵的预期输出:

array([[88, 0],
       [ 0,  12]])

当所有正例条目都位于左对角线时,模型的准确度为 100%。在此情况下,完美的混淆矩阵预测了 88 个非外行星恒星和 12 个外行星恒星。请注意,混淆矩阵不提供标签,但在这种情况下,可以根据大小推断标签。

在深入细节之前,让我们使用 scikit-learn 查看实际的混淆矩阵。

confusion_matrix

sklearn.metrics 导入 confusion_matrix,代码如下:

from sklearn.metrics import confusion_matrix

使用 y_testy_pred 作为输入运行 confusion_matrix(这些变量在上一部分中获得),确保将 y_test 放在前面:

confusion_matrix(y_test, y_pred)

输出如下:

array([[86, 2],
       [9,  3]])

混淆矩阵对角线上的数字揭示了 86 个正确的非外行星恒星预测,以及仅 3 个正确的外行星恒星预测。

在矩阵的右上角,数字 2 显示有两个非外行星恒星被误分类为外行星恒星。同样,在矩阵的左下角,数字 9 显示有 9 个外行星恒星被误分类为非外行星恒星。

横向分析时,88 个非外行星恒星中有 86 个被正确分类,而 12 个外行星恒星中只有 3 个被正确分类。

如你所见,混淆矩阵揭示了模型预测的重要细节,而准确度得分无法捕捉到这些细节。

classification_report

在上一部分中混淆矩阵所揭示的各种百分比数值包含在分类报告(classification report)中。让我们查看分类报告:

  1. sklearn.metrics 导入 classification_report

    from sklearn.metrics import classification_report
    
  2. y_testy_pred 放入 classification_report 中,确保将 y_test 放在前面。然后将 classification_report 放入全局打印函数中,以确保输出对齐且易于阅读:

    print(classification_report(y_test, y_pred))
    

    这是预期的输出:

                  precision    recall  f1-score   support
               1       0.91      0.98      0.94        88
               2       0.60      0.25      0.35        12
        accuracy                           0.89       100
       macro avg       0.75      0.61      0.65       100
    weighted avg       0.87      0.89      0.87       100
    

了解上述得分的含义很重要,让我们逐一回顾它们。

精确度(Precision)

精确度给出了正类预测(2s)中实际上是正确的预测。它在技术上是通过真正例和假正例来定义的。

真正例(True Positives)

以下是关于真正例的定义和示例:

  • 定义 – 正确预测为正类的标签数。

  • 示例 – 2 被正确预测为 2。

假正例(False Positives)

以下是关于假正例的定义和示例:

  • 定义 – 错误地预测为负类的正标签数。

  • 示例 – 对于外行星恒星,2 被错误地预测为 1。

精确度的定义通常以其数学形式表示如下:

这里,TP 代表真正例(True Positive),FP 代表假正例(False Positive)。

在外行星数据集中,我们有以下两种数学形式:

精确率给出了每个目标类的正确预测百分比。接下来,让我们回顾分类报告中揭示的其他关键评分指标。

召回率

召回率给出了你的预测发现的正样本的百分比。召回率是正确预测的正样本数量除以真正例加上假负例的总和。

虚假负例

这里是虚假负例的定义和示例:

  • 定义 – 错误预测为负类的标签数量。

  • 示例 – 对于外行星星的预测,2 类被错误地预测为 1 类。

数学形式如下所示:

这里 TP 代表真正例(True Positive),FN 代表假负例(False Negative)。

在外行星数据集中,我们有以下内容:

召回率告诉你找到了多少正样本。在外行星的例子中,只有 25%的外行星被找到了。

F1 分数

F1 分数是精确率和召回率的调和平均值。使用调和平均值是因为精确率和召回率基于不同的分母,调和平均值将它们统一起来。当精确率和召回率同等重要时,F1 分数是最优的。请注意,F1 分数的范围从 0 到 1,1 为最高分。

替代评分方法

精确率、召回率和 F1 分数是 scikit-learn 提供的替代评分方法。标准评分方法的列表可以在官方文档中找到:scikit-learn.org/stable/modules/model_evaluation.html

提示

对于分类数据集,准确率通常不是最佳选择。另一种常见的评分方法是roc_auc_score,即接收者操作特征曲线下面积。与大多数分类评分方法一样,越接近 1,结果越好。更多信息请参见scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html#sklearn.metrics.roc_auc_score

选择评分方法时,了解目标至关重要。外行星数据集的目标是找到外行星。这一点是显而易见的。但并不明显的是,如何选择最佳评分方法以实现期望的结果。

想象两种不同的情境:

  • 场景 1:机器学习模型预测的 4 颗外行星星中,实际为外行星的有 3 颗:3/4 = 75% 精确率。

  • 场景 2:在 12 颗外行星星中,模型正确预测了 8 颗外行星星(8/12 = 66% 召回率)。

哪种情况更为理想?

答案是这取决于情况。召回率适合用于标记潜在的正样本(如外行星),目的是尽可能找到所有的正样本。精确率则适用于确保预测的正样本(外行星)确实是正样本。

天文学家不太可能仅仅因为机器学习模型说发现了外星行星就宣布这一发现。他们更可能在确认或否定这一发现之前,仔细检查潜在的外星行星,并根据额外的证据作出判断。

假设机器学习模型的目标是尽可能多地找到外星行星,召回率是一个极好的选择。为什么?召回率告诉我们找到了多少颗外星行星(例如:2/12、5/12、12/12)。让我们尝试找到所有的外星行星。

精确率说明

更高的精确率并不意味着更多的外星行星。例如,1/1 的召回率是 100%,但只发现了一颗外星行星。

recall_score

如前一节所述,我们将使用召回率作为评分方法,针对外星行星数据集寻找尽可能多的外星行星。让我们开始吧:

  1. sklearn.metrics导入recall_score

    from sklearn.metrics import recall_score
    

    默认情况下,recall_score报告的是正类的召回率,通常标记为1。在外星行星数据集中,正类标记为2,负类标记为1,这比较少见。

  2. 为了获得外星行星的recall_score值,输入y_testy_pred作为recall_score的参数,并设置pos_label=2

    recall_score(y_test, y_pred, pos_label=2)
    

    外星行星的评分如下:

    0.25
    

这是由分类报告中召回率为2时给出的相同百分比,即外星行星。接下来,我们将不再使用accuracy_score,而是使用recall_score及其前述参数作为我们的评分指标。

接下来,让我们了解一下重新采样,它是改善失衡数据集得分的重要策略。

重新采样失衡数据

现在我们有了一个适当的评分方法来发现外星行星,接下来是探索如重新采样、欠采样和过采样等策略,以纠正导致低召回率的失衡数据。

重新采样

应对失衡数据的一种策略是重新采样数据。可以通过减少多数类的行数来进行欠采样,或通过重复少数类的行数来进行过采样。

欠采样

我们的探索从从 5,087 行中选取了 400 行开始。这是一个欠采样的例子,因为子集包含的行数比原始数据少。

我们来编写一个函数,使其能够按任意行数对数据进行欠采样。这个函数应该返回召回率评分,这样我们就能看到欠采样如何改变结果。我们将从评分函数开始。

评分函数

以下函数接收 XGBClassifier 和行数作为输入,输出外星行星的混淆矩阵、分类报告和召回率。

以下是步骤:

  1. 定义一个函数xgb_clf,它接收model(机器学习模型)和nrows(行数)作为输入:

    def xgb_clf(model, nrows):
    
  2. 使用nrows加载 DataFrame,然后将数据分成Xy,并划分训练集和测试集:

        df = pd.read_csv('exoplanets.csv', nrows=nrows)
        X = df.iloc[:,1:]
        y = df.iloc[:,0]
        X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    
  3. 初始化模型,将模型拟合到训练集,并使用 y_testy_predpos_label=2 作为 recall_score 的输入对测试集进行评分:

        model.fit(X_train, y_train)
        y_pred = xg_clf.predict(X_test)
        score = recall_score(y_test, y_pred, pos_label=2)
    
  4. 打印混淆矩阵和分类报告,并返回评分:

        print(confusion_matrix(y_test, y_pred))
        print(classification_report(y_test, y_pred))
        return score
    

现在,我们可以通过欠采样减少行数,并观察评分的变化。

欠采样 nrows

让我们先将 nrows 加倍至 800。这仍然是欠采样,因为原始数据集有 5087 行:

xgb_clf(XGBClassifier(random_state=2), nrows=800)

这是预期的输出:

[[189   1]
 [  9   1]]
              precision    recall  f1-score   support
           1       0.95      0.99      0.97       190
           2       0.50      0.10      0.17        10
    accuracy                           0.95       200
   macro avg       0.73      0.55      0.57       200
weighted avg       0.93      0.95      0.93       200
0.1

尽管非外行星星体的召回率几乎完美,但混淆矩阵显示只有 10 个外行星星体中的 1 个被召回。

接下来,将 nrows400 减少到 200

xgb_clf(XGBClassifier(random_state=2), nrows=200)

这是预期的输出:

[[37  0]
 [ 8  5]]
              precision    recall  f1-score   support
           1       0.82      1.00      0.90        37
           2       1.00      0.38      0.56        13
    accuracy                           0.84        50
   macro avg       0.91      0.69      0.73        50
weighted avg       0.87      0.84      0.81        50

这个结果稍微好一些。通过减少 nrows,召回率有所提高。

让我们看看如果我们精确平衡类会发生什么。由于有 37 个外行星星体,37 个非外行星星体就能平衡数据。

使用 nrows=74 运行 xgb_clf 函数:

xgb_clf(XGBClassifier(random_state=2), nrows=74)

这是预期的输出:

[[6 2]
 [5 6]]
              precision    recall  f1-score   support
           1       0.55      0.75      0.63         8
           2       0.75      0.55      0.63        11
    accuracy                           0.63        19
   macro avg       0.65      0.65      0.63        19
weighted avg       0.66      0.63      0.63        19
0.5454545454545454

尽管子集要小得多,但这些结果仍然令人满意。

接下来,让我们看看当我们应用过采样策略时会发生什么。

过采样

另一种重采样技术是过采样。与其删除行,过采样通过复制和重新分配正类样本来增加行数。

尽管原始数据集有超过 5000 行,但我们仍然使用 nrows=400 作为起点,以加快过程。

nrows=400 时,正类与负类样本的比例为 10:1。为了获得平衡,我们需要 10 倍数量的正类样本。

我们的策略如下:

  • 创建一个新的 DataFrame,复制正类样本九次。

  • 将新的 DataFrame 与原始数据框连接,得到 10:10 的比例。

在继续之前,需要做一个警告。如果在拆分数据集成训练集和测试集之前进行重采样,召回评分将会被夸大。你能看出为什么吗?

在重采样时,将对正类样本进行九次复制。将数据拆分为训练集和测试集后,复制的样本可能会同时出现在两个数据集中。因此,测试集将包含大多数与训练集相同的数据点。

合适的策略是先将数据拆分为训练集和测试集,然后再进行重采样。如前所述,我们可以使用 X_trainX_testy_trainy_test。让我们开始:

  1. 使用 pd.merge 按照左索引和右索引合并 X_trainy_train,如下所示:

    df_train = pd.merge(y_train, X_train, left_index=True, right_index=True)
    
  2. 使用 np.repeat 创建一个包含以下内容的 DataFrame,new_df

    a) 正类样本的值:df_train[df_train['LABEL']==2.values

    b) 复制的次数——在本例中为 9

    c) axis=0 参数指定我们正在处理列:

    new_df = pd.DataFrame(np.repeat(df_train[df_train['LABEL']==2].values,9,axis=0))
    
  3. 复制列名:

    new_df.columns = df_train.columns
    
  4. 合并 DataFrame:

    df_train_resample = pd.concat([df_train, new_df])
    
  5. 验证 value_counts 是否如预期:

    df_train_resample['LABEL'].value_counts()
    

    预期的输出如下:

    1.0    275
    2.0    250
    Name: LABEL, dtype: int64
    
  6. 使用重采样后的 DataFrame 拆分 Xy

    X_train_resample = df_train_resample.iloc[:,1:]
    y_train_resample = df_train_resample.iloc[:,0]
    
  7. 在重采样后的训练集上拟合模型:

    model = XGBClassifier(random_state=2)
    model.fit(X_train_resample, y_train_resample)
    
  8. 使用X_testy_test对模型进行评分。将混淆矩阵和分类报告包括在结果中:

    y_pred = model.predict(X_test)
    score = recall_score(y_test, y_pred, pos_label=2)
    print(confusion_matrix(y_test, y_pred))
    print(classification_report(y_test, y_pred))
    print(score)
    

    得分如下:

    [[86  2]
     [ 8  4]]
                  precision    recall  f1-score   support
               1       0.91      0.98      0.95        88
               2       0.67      0.33      0.44        12
        accuracy                           0.90       100
       macro avg       0.79      0.66      0.69       100
    weighted avg       0.89      0.90      0.88       100
    0.3333333333333333
    

通过适当地留出测试集,过采样达到了 33.3%的召回率,这个得分是之前 17%的一倍,尽管仍然太低。

提示

imblearn,必须下载才能使用。我通过前面的重采样代码实现了与 SMOTE 相同的结果。

由于重采样的效果最多只能带来适度的提升,是时候调整 XGBoost 的超参数了。

调整和缩放 XGBClassifier

在本节中,我们将微调并缩放 XGBClassifier,以获得外星行星数据集的最佳recall_score值。首先,您将使用scale_pos_weight调整权重,然后运行网格搜索以找到最佳的超参数组合。此外,您将为不同的数据子集评分,然后整合并分析结果。

调整权重

第五章XGBoost 揭秘中,你使用了scale_pos_weight超参数来解决 Higgs 玻色子数据集中的不平衡问题。scale_pos_weight是一个用来调整类权重的超参数。这里强调的是非常重要的,因为 XGBoost 假设目标值为1的是类,目标值为0的是类。

在外星行星数据集中,我们一直使用数据集提供的默认值1为负类,2为正类。现在,我们将使用.replace()方法将其改为0为负类,1为正类。

replace

.replace()方法可以用来重新分配值。以下代码在LABEL列中将1替换为0,将2替换为1

df['LABEL'] = df['LABEL'].replace(1, 0)
df['LABEL'] = df['LABEL'].replace(2, 1)

如果两行代码顺序颠倒,所有列值都会变成 0,因为所有的 2 都会变成 1,然后所有的 1 会变成 0。在编程中,顺序非常重要!

使用value_counts方法验证计数:

df['LABEL'].value_counts()

这里是预期的输出:

0    363
1     37
Name: LABEL, dtype: int64

正类现在标记为1,负类标记为0

scale_pos_weight

现在是时候构建一个新的XGBClassifier,并设置scale_pos_weight=10,以解决数据中的不平衡问题:

  1. 将新的 DataFrame 拆分为X,即预测列和y,即目标列:

    X = df.iloc[:,1:]
    y = df.iloc[:,0]
    
  2. 将数据拆分为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    
  3. 构建、拟合、预测并评分XGBClassifier,设置scale_pos_weight=10。打印出混淆矩阵和分类报告以查看完整结果:

    model = XGBClassifier(scale_pos_weight=10, random_state=2)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    score = recall_score(y_test, y_pred)
    print(confusion_matrix(y_test, y_pred))
    print(classification_report(y_test, y_pred))
    print(score)
    

    这里是预期的输出:

    [[86  2]
     [ 8  4]]
                  precision    recall  f1-score   support
               0       0.91      0.98      0.95        88
               1       0.67      0.33      0.44        12
        accuracy                           0.90       100
       macro avg       0.79      0.66      0.69       100
    weighted avg       0.89      0.90      0.88       100
    0.3333333333333333
    

结果与上一节的重采样方法相同。

我们从头开始实现的过采样方法给出的预测结果与scale_pos_weightXGBClassifier一致。

调整 XGBClassifier

现在是时候看看超参数微调是否能够提高精度了。

在微调超参数时,标准做法是使用 GridSearchCVRandomizedSearchCV。两者都需要进行两折或更多折的交叉验证。由于我们的初始模型效果不佳,并且在大型数据集上进行多折交叉验证计算成本高昂,因此我们尚未实施交叉验证。

一种平衡的方法是使用 GridSearchCVRandomizedSearchCV,并采用两个折叠来节省时间。为了确保结果一致,推荐使用 StratifiedKFold第六章XGBoost 超参数)。我们将从基准模型开始。

基准模型

以下是构建基准模型的步骤,该模型实现了与网格搜索相同的 k 折交叉验证:

  1. 导入 GridSearchCVRandomizedSearchCVStratifiedKFoldcross_val_score

    from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, StratifiedKFold, cross_val_score
    
  2. StratifiedKFold 初始化为 kfold,参数为 n_splits=2shuffle=True

    kfold = StratifiedKFold(n_splits=2, shuffle=True, random_state=2)
    
  3. 使用 scale_pos_weight=10 初始化 XGBClassifier,因为负类样本是正类样本的 10 倍:

    model = XGBClassifier(scale_pos_weight=10, random_state=2)
    
  4. 使用 cross_val_score 对模型进行评分,参数为 cv=kfoldscore='recall',然后显示得分:

    scores = cross_val_score(model, X, y, cv=kfold, scoring='recall')
    print('Recall: ', scores)
    print('Recall mean: ', scores.mean())
    

    分数如下:

    Recall:  [0.10526316 0.27777778]
    Recall mean:  0.1915204678362573
    

使用交叉验证后,得分稍微差一些。当正例非常少时,训练集和测试集中的行的选择会产生差异。StratifiedKFoldtrain_test_split 的不同实现可能导致不同的结果。

网格搜索

我们将实现来自 第六章grid_search 函数的一个变体, XGBoost 超参数,以便微调超参数:

  1. 新函数将参数字典作为输入,同时还提供一个使用 RandomizedSearchCV 的随机选项。此外,Xy 被作为默认参数提供,用于其他子集,并且评分方法为召回率,具体如下:

    def grid_search(params, random=False, X=X, y=y, model=XGBClassifier(random_state=2)): 
        xgb = model
        if random:
            grid = RandomizedSearchCV(xgb, params, cv=kfold, n_jobs=-1, random_state=2, scoring='recall')
        else:
            grid = GridSearchCV(xgb, params, cv=kfold, n_jobs=-1, scoring='recall')
        grid.fit(X, y)
        best_params = grid.best_params_
        print("Best params:", best_params)
        best_score = grid.best_score_
        print("Best score: {:.5f}".format(best_score))
    
  2. 让我们运行不使用默认设置的网格搜索,试图提高得分。以下是一些初始的网格搜索及其结果:

    a) 网格搜索 1:

    grid_search(params={'n_estimators':[50, 200, 400, 800]})
    

    结果:

    Best params: {'n_estimators': 50}Best score: 0.19152
    

    b) 网格搜索 2:

    grid_search(params={'learning_rate':[0.01, 0.05, 0.2, 0.3]})
    

    结果:

    Best params: {'learning_rate': 0.01}
    Best score: 0.40351
    

    c) 网格搜索 3:

    grid_search(params={'max_depth':[1, 2, 4, 8]})
    

    结果:

    Best params: {'max_depth': 2}
    Best score: 0.24415
    

    d) 网格搜索 4:

    grid_search(params={'subsample':[0.3, 0.5, 0.7, 0.9]})
    

    结果:

    Best params: {'subsample': 0.5}
    Best score: 0.21637
    

    e) 网格搜索 5:

    grid_search(params={'gamma':[0.05, 0.1, 0.5, 1]})
    

    结果:

    Best params: {'gamma': 0.05}
    Best score: 0.24415
    
  3. 改变 learning_ratemax_depthgamma 取得了提升。让我们通过缩小范围来尝试将它们组合起来:

    grid_search(params={'learning_rate':[0.001, 0.01, 0.03], 'max_depth':[1, 2], 'gamma':[0.025, 0.05, 0.5]})
    

    分数如下:

    Best params: {'gamma': 0.025, 'learning_rate': 0.001, 'max_depth': 2}
    Best score: 0.53509
    
  4. 还值得尝试 max_delta_step,XGBoost 仅建议在不平衡数据集上使用。默认值为 0,增加步骤会导致模型更加保守:

    grid_search(params={'max_delta_step':[1, 3, 5, 7]})
    

    分数如下:

    Best params: {'max_delta_step': 1}
    Best score: 0.24415
    
  5. 作为最终策略,我们通过在随机搜索中结合 subsample 和所有列样本:

    grid_search(params={'subsample':[0.3, 0.5, 0.7, 0.9, 1], 
    'colsample_bylevel':[0.3, 0.5, 0.7, 0.9, 1], 
    'colsample_bynode':[0.3, 0.5, 0.7, 0.9, 1], 
    'colsample_bytree':[0.3, 0.5, 0.7, 0.9, 1]}, random=True)
    

    分数如下:

    Best params: {'subsample': 0.3, 'colsample_bytree': 0.7, 'colsample_bynode': 0.7, 'colsample_bylevel': 1}
    Best score: 0.35380
    

不继续使用包含 400 行数据的这个数据子集,而是切换到包含 74 行数据的平衡子集(欠采样),以比较结果。

平衡子集

包含 74 行数据的平衡子集数据点最少,它也是测试最快的。

由于Xy最后一次是在函数内为平衡子集定义的,因此需要显式地定义它们。X_shorty_short的新定义如下:

X_short = X.iloc[:74, :]
y_short = y.iloc[:74]

经过几次网格搜索后,结合max_depthcolsample_bynode给出了以下结果:

grid_search(params={'max_depth':[1, 2, 3], 'colsample_bynode':[0.5, 0.75, 1]}, X=X_short, y=y_short, model=XGBClassifier(random_state=2)) 

分数如下:

Best params: {'colsample_bynode': 0.5, 'max_depth': 2}
Best score: 0.65058

这是一个改进。

现在是时候在所有数据上尝试超参数微调了。

微调所有数据

在所有数据上实现grid_search函数的问题是时间。现在我们已经接近尾声,到了运行代码并在计算机“出汗”时休息的时刻:

  1. 将所有数据读入一个新的 DataFrame,df_all

    df_all = pd.read_csv('exoplanets.csv')
    
  2. 将 1 替换为 0,将 2 替换为 1:

    df_all['LABEL'] = df_all['LABEL'].replace(1, 0)df_all['LABEL'] = df_all['LABEL'].replace(2, 1)
    
  3. 将数据分为Xy

    X_all = df_all.iloc[:,1:]y_all = df_all.iloc[:,0]
    
  4. 验证'LABEL'列的value_counts

    df_all['LABEL'].value_counts()
    

    输出如下:

    0    5050 1      37 Name: LABEL, dtype: int64
    
  5. 通过将负类除以正类来缩放权重:

    weight = int(5050/37)
    
  6. 使用XGBClassifierscale_pos_weight=weight对所有数据进行基准模型评分:

    model = XGBClassifier(scale_pos_weight=weight, random_state=2)
    scores = cross_val_score(model, X_all, y_all, cv=kfold, scoring='recall')
    print('Recall:', scores)
    print('Recall mean:', scores.mean())
    

    输出如下:

    Recall: [0.10526316 0.        ]
    Recall mean: 0.05263157894736842
    

    这个分数很糟糕。可能是分类器在准确率上得分很高,尽管召回率很低。

  7. 让我们尝试基于迄今为止最成功的结果优化超参数:

    grid_search(params={'learning_rate':[0.001, 0.01]}, X=X_all, y=y_all, model=XGBClassifier(scale_pos_weight=weight, random_state=2)) 
    

    分数如下:

    Best params: {'learning_rate': 0.001}
    Best score: 0.26316
    

    这比使用所有数据时的初始分数要好得多。

    让我们尝试结合超参数:

    grid_search(params={'max_depth':[1, 2],'learning_rate':[0.001]}, X=X_all, y=y_all, model=XGBClassifier(scale_pos_weight=weight, random_state=2)) 
    

    分数如下:

    Best params: {'learning_rate': 0.001, 'max_depth': 2}
    Best score: 0.53509
    

这已经有所改善,但不如之前对欠采样数据集的得分强。

由于在所有数据上的分数起始较低且需要更多时间,自然而然会产生一个问题。对于系外行星数据集,机器学习模型在较小的子集上是否表现更好?

让我们来看看。

整合结果

将不同的数据集进行结果整合是很棘手的。我们一直在处理以下子集:

  • 5,050 行 – 大约 54%的召回率

  • 400 行 – 大约 54%的召回率

  • 74 行 – 大约 68%的召回率

获得的最佳结果包括learning_rate=0.001max_depth=2colsample_bynode=0.5

让我们在所有 37 个系外行星恒星上训练一个模型。这意味着测试结果将来自模型已经训练过的数据点。通常,这不是一个好主意。然而,在这种情况下,正例非常少,看看模型如何在它以前没有见过的正例上进行测试,可能会很有启发。

以下函数以Xy和机器学习模型为输入。模型在提供的数据上进行拟合,然后对整个数据集进行预测。最后,打印出recall_scoreconfusion matrixclassification report

def final_model(X, y, model):
    model.fit(X, y)
    y_pred = model.predict(X_all)
    score = recall_score(y_all, y_pred,)
    print(score)
    print(confusion_matrix(y_all, y_pred,))
    print(classification_report(y_all, y_pred))

让我们为我们的三个子集运行函数。在三种最强的超参数中,事实证明colsample_bynodemax_depth给出了最佳结果。

从行数最少的地方开始,其中系外行星恒星和非系外行星恒星的数量相匹配。

74 行

让我们从 74 行开始:

final_model(X_short, y_short, XGBClassifier(max_depth=2, colsample_by_node=0.5, random_state=2))

输出如下:

1.0
[[3588 1462]
 [   0   37]]
              precision    recall  f1-score   support
           0       1.00      0.71      0.83      5050
           1       0.02      1.00      0.05        37
    accuracy                           0.71      5087
   macro avg       0.51      0.86      0.44      5087
weighted avg       0.99      0.71      0.83      5087

所有 37 颗外行星恒星都被正确识别,但 1462 颗非外行星恒星被错误分类!尽管召回率达到了 100%,但精确度只有 2%,F1 得分为 5%。仅仅调优召回率会带来低精度和低 F1 得分的风险。实际上,天文学家需要筛选出 1462 颗潜在的外行星恒星,才能找到这 37 颗。这是不可接受的。

现在让我们看看在 400 行数据上训练时会发生什么。

400 行

在 400 行数据的情况下,我们使用scale_pos_weight=10超参数来平衡数据:

final_model(X, y, XGBClassifier(max_depth=2, colsample_bynode=0.5, scale_pos_weight=10, random_state=2))

输出结果如下:

1.0
[[4901  149]
 [   0   37]]
              precision    recall  f1-score   support
           0       1.00      0.97      0.99      5050
           1       0.20      1.00      0.33        37
    accuracy                           0.97      5087
   macro avg       0.60      0.99      0.66      5087
weighted avg       0.99      0.97      0.98      5087

再次,所有 37 颗外行星恒星都被正确分类,达到了 100%的召回率,但 149 颗非外行星恒星被错误分类,精确度为 20%。在这种情况下,天文学家需要筛选出 186 颗恒星,才能找到这 37 颗外行星恒星。

最后,让我们在所有数据上进行训练。

5050 行

在所有数据的情况下,将scale_pos_weight设置为与先前定义的weight变量相等:

final_model(X_all, y_all, XGBClassifier(max_depth=2, colsample_bynode=0.5, scale_pos_weight=weight, random_state=2))

输出结果如下:

1.0
[[5050    0]
 [   0   37]]
              precision    recall  f1-score   support
           0       1.00      1.00      1.00      5050
           1       1.00      1.00      1.00        37
    accuracy                           1.00      5087
   macro avg       1.00      1.00      1.00      5087
weighted avg       1.00      1.00      1.00      5087

惊人。所有预测、召回率和精确度都完美达到了 100%。在这种高度理想的情况下,天文学家无需筛选不良数据,就能找到所有的外行星恒星。

但请记住,这些得分是基于训练数据,而非未见过的测试数据,而后者是构建强大模型的必要条件。换句话说,尽管模型完美地拟合了训练数据,但它不太可能对新数据进行良好的泛化。然而,这些数字仍然有价值。

根据这个结果,由于机器学习模型在训练集上表现出色,但在测试集上的表现最多只是适中,方差可能过高。此外,可能需要更多的树和更多轮次的精调,以便捕捉数据中的细微模式。

分析结果

在训练集上评分时,经过调优的模型提供了完美的召回率,但精确度差异较大。以下是关键要点:

  • 仅使用精确度而不考虑召回率或 F1 得分,可能会导致次优模型。通过使用分类报告,能揭示更多细节。

  • 不建议过度强调小子集的高得分。

  • 当测试得分较低,而训练得分较高时,建议在不平衡数据集上使用更深的模型并进行广泛的超参数调优。

Kaggle 用户发布的公开笔记本中对内核的调查,位于www.kaggle.com/keplersmachines/kepler-labelled-time-series-data/kernels,展示了以下内容:

  • 许多用户未能理解,尽管高准确率得分容易获得,但在高度不平衡的数据下,它几乎没有意义。

  • 发布精确度的用户通常发布的是 50%到 70%之间的数据,而发布召回率的用户通常发布的是 60%到 100%之间(一个 100%召回率的用户精确度为 55%),这表明了该数据集的挑战和局限性。

当您向天文学教授展示您的结果时,您已经更加了解不平衡数据的局限性,您得出结论,您的模型最佳的召回率为 70%,而 37 颗外行星恒星不足以构建一个强大的机器学习模型来寻找其他行星上的生命。然而,您的 XGBClassifier 将使天文学家和其他经过数据分析训练的人能够使用机器学习来决定在宇宙中应集中关注哪些恒星,以发现下一个处于轨道上的外行星。

总结

在这一章中,您使用外行星数据集对宇宙进行了调查,旨在发现新的行星,甚至可能发现新的生命。您构建了多个 XGBClassifier 来预测外行星恒星是否由光的周期性变化所引起。在仅有 37 颗外行星恒星和 5,050 颗非外行星恒星的情况下,您通过欠采样、过采样和调整 XGBoost 超参数(包括 scale_pos_weight)来纠正数据的不平衡。

您使用混淆矩阵和分类报告分析了结果。您学习了各种分类评分指标之间的关键差异,并且理解了为什么在外行星数据集中,准确率几乎没有价值,而高召回率是理想的,尤其是当与高精度结合时,能够得到一个好的 F1 分数。最后,您意识到,当数据极其多样化且不平衡时,机器学习模型的局限性。

通过这个案例研究,您已具备了使用 XGBoost 完整分析不平衡数据集所需的背景知识和技能,掌握了 scale_pos_weight、超参数微调和替代分类评分指标的使用。

在下一章中,您将通过应用不同于梯度提升树的其他 XGBoost 基学习器,大大扩展您对 XGBoost 的应用范围。尽管梯度提升树通常是最佳选择,但 XGBoost 配备了线性基学习器、DART 基学习器,甚至是随机森林,接下来都会介绍!

第三部分:高级 XGBoost

构建高级 XGBoost 模型需要实践、分析和实验。在本节中,你将实验并微调替代基学习器,学习来自 Kaggle 大师的创新技巧——包括堆叠方法和高级特征工程——并实践构建稳健的模型,为行业部署做好准备,使用稀疏矩阵、定制的转换器和管道。

本节包含以下章节:

  • 第八章**,XGBoost 替代基学习器

  • 第九章**,XGBoost Kaggle 大师

  • 第十章**,XGBoost 模型部署

第八章:第八章: XGBoost 替代基础学习器

在本章中,您将分析并应用不同的gbtree,基础学习器的附加选项包括gblineardart。此外,XGBoost 还有其自己的随机森林实现,作为基础学习器以及作为树集成算法,您将在本章中进行实验。

通过学习如何应用替代的基础学习器,您将大大扩展 XGBoost 的使用范围。您将能够构建更多的模型,并学习开发线性、基于树的以及随机森林机器学习算法的新方法。本章的目标是让您熟练掌握使用替代基础学习器构建 XGBoost 模型,从而能够利用 XGBoost 的高级选项,在各种情况下找到最优的模型。

在本章中,我们将涵盖以下主要主题:

  • 探索替代的基础学习器

  • 应用gblinear

  • 比较dart

  • 查找 XGBoost 随机森林

技术要求

本章的代码和数据集可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter08找到。

探索替代的基础学习器

基础学习器是 XGBoost 用来构建其集成中第一个模型的机器学习模型。使用base这个词是因为它是第一个出现的模型,而使用learner是因为该模型会在从错误中学习后自我迭代。

决策树已成为 XGBoost 首选的基础学习器,因为提升树模型能够 consistently 产生优秀的分数。决策树的受欢迎程度不仅仅局限于 XGBoost,还扩展到其他集成算法,如随机森林以及ExtraTreesClassifierExtraTreesRegressorscikit-learn.org/stable/modules/ensemble.html)。

在 XGBoost 中,默认的基础学习器是gbtree,它是多种基础学习器之一。还有gblinear,一种梯度提升线性模型,以及dart,一种包括基于神经网络的 dropout 技术的决策树变种。此外,XGBoost 还有随机森林的实现。在接下来的部分中,我们将探讨这些基础学习器之间的差异,然后在后续章节中应用它们。

gblinear

决策树对于非线性数据非常适用,因为它们可以通过多次划分数据来轻松访问数据点。由于实际数据通常是非线性的,决策树通常更适合作为基础学习器。

然而,可能会有某些情况,gblinear作为线性基础学习器的选项。

提升线性模型的基本思想与提升树模型相同。首先构建一个基模型,然后在每个后续模型中训练残差。最后,将所有模型的结果求和得到最终结果。与线性基学习器的主要区别在于,集成中的每个模型都是线性的。

gblinear一样,dart也在线性回归中添加了正则化项。XGBoost 的创始人兼开发者 Tianqi Chin 在 GitHub 上评论称,经过多轮提升后,gblinear可以用来返回一个单一的 lasso 回归github.com/dmlc/xgboost/issues/332)。

gblinear也可以通过逻辑回归应用于分类问题。这是因为逻辑回归的构建方式与线性回归相同,都是通过找到最优系数(加权输入)并通过sigmoid 方程求和(见第一章机器学习概览)。

我们将在本章的应用 gblinear部分深入探讨gblinear的细节和应用。现在,先了解一下dart

DART

丢弃法与多重加法回归树,简称DART,由 UC Berkeley 的 K. V. Rashmi 和微软的 Ran Gilad-Bachrach 于 2015 年在以下论文中提出:proceedings.mlr.press/v38/korlakaivinayak15.pdf

Rashmi 和 Gilad-Bachrach 指出,多重加法回归树MART)是一种成功的模型,但其问题在于过度依赖早期的树。为了改进这一点,他们没有侧重于收缩这一标准的惩罚项,而是使用了来自神经网络丢弃法技术。简单来说,丢弃法技术通过从神经网络的每一层学习中删除节点(数学点),从而减少过拟合。换句话说,丢弃法通过在每一轮中删除信息,减缓了学习过程。

在 DART 中,每一轮新的提升中,DART 并不是通过对所有前一个树的残差求和来建立新模型,而是随机选择部分前一个树的样本,并通过一个缩放因子来归一化叶子节点!,其中是被丢弃的树的数量。

DART 是决策树的一种变体。XGBoost 中的 DART 实现类似于gbtree,但是增加了一些超参数来适应丢弃法。

有关 DART 的数学细节,请参考本节第一段中提到的原始论文。

你将在本章稍后的比较 dart部分,练习使用DART基学习器来构建机器学习模型。

XGBoost 随机森林

本节我们将探索的最后一个选项是 XGBoost 随机森林。通过将 num_parallel_trees 设置为大于 1 的整数,可以将随机森林实现为基础学习器,并作为 XGBoost 中的类选项,定义为 XGBRFRegressorXGBRFClassifier

请记住,梯度提升法是为改进相对较弱的基础学习器的错误而设计的,而不是像随机森林这样强大的基础学习器。然而,可能会有一些边缘情况,随机森林基础学习器也可能具有优势,因此它是一个不错的选择。

作为额外奖励,XGBoost 提供了 XGBRFRegressorXGBRFClassifier 作为随机森林机器学习算法,它们不是基础学习器,而是独立的算法。这些算法的工作方式与 scikit-learn 的随机森林类似(参见 第三章使用随机森林的袋装法)。主要的区别在于,XGBoost 包括默认的超参数来对抗过拟合,并有自己构建单棵树的方法。XGBoost 随机森林虽然还处于实验阶段,但自 2020 年末以来,它们已经开始超越 scikit-learn 的随机森林,如你将在本章中看到的。

在本章的最后部分,我们将分别尝试将 XGBoost 的随机森林作为基础学习器和独立模型进行实验。

现在你已经对 XGBoost 基础学习器有了一个概览,接下来我们将逐一应用它们。

应用 gblinear

很难找到与线性模型最为契合的真实世界数据集。实际数据往往比较杂乱,复杂的模型(如树集成)通常会产生更好的得分。而在其他情况下,线性模型可能会有更好的泛化能力。

机器学习算法的成功依赖于它们在真实世界数据上的表现。在接下来的部分,我们将首先将 gblinear 应用于糖尿病数据集,然后再应用于一个通过构造生成的线性数据集。

将 gblinear 应用于糖尿病数据集

糖尿病数据集是由 scikit-learn 提供的一个回归数据集,包含 442 名糖尿病患者。预测列包括年龄、性别、BMI体重指数)、BP血压)和五项血清测量值。目标列是疾病在 1 年后的进展。你可以在原始论文中阅读有关数据集的详细信息,链接在这里:web.stanford.edu/~hastie/Papers/LARS/LeastAngle_2002.pdf

Scikit-learn 的数据集已经为你将预测列和目标列拆分好了。它们已预处理,机器学习时 X 是预测列,y 是目标列,分别加载。

下面是你需要用来处理这个数据集以及本章其余部分的完整导入列表:

import pandas as pd
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.model_selection import cross_val_score
from xgboost import XGBRegressor, XGBClassifier, XGBRFRegressor, XGBRFClassifier
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.linear_model import Lasso, Ridge
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error as MSE

让我们开始吧!要使用糖尿病数据集,请执行以下操作:

  1. 你首先需要使用 load_diabetes 并将 return_X_y 参数设置为 True,以定义 Xy

    X, y = load_diabetes(return_X_y=True)
    

    计划是使用 cross_val_scoreGridSearchCV,所以我们先创建折叠以获得一致的得分。在 第六章XGBoost 超参数 中,我们使用了 StratifiedKFold,它对目标列进行分层,确保每个测试集包含相同数量的类别。

    这种方法适用于分类问题,但不适用于回归问题,其中目标列是连续值,不涉及类别。KFold 通过在数据中创建一致的分割来实现类似的目标,而不进行分层。

  2. 现在,打乱数据并使用 KFold 对数据进行 5 次分割,使用以下参数:

    kfold = KFold(n_splits=5, shuffle=True, random_state=2)  
    
  3. 构建一个使用 cross_val_score 的函数,输入一个机器学习模型,并返回 5 次折叠的平均得分,确保设置 cv=kfold

    def regression_model(model):
        scores = cross_val_score(model, X, y, scoring='neg_mean_squared_error', cv=kfold)
        rmse = (-scores)**0.5
        return rmse.mean()
    
  4. 要使用 gblinear 作为基本模型,只需在回归函数中的 XGBRegressor 中设置 booster='gblinear'

    regression_model(XGBRegressor(booster='gblinear'))
    

    得分如下:

    55.4968907398679
    
  5. 让我们检查这个得分与其他线性模型的对比,包括 LinearRegressionLasso,使用 Ridge,使用 LinearRegression 如下:

    regression_model(LinearRegression())
    

    得分如下:

    55.50927267834351
    

    b) Lasso 如下:

    regression_model(Lasso())
    

    得分如下:

    62.64900771743497
    

    c) Ridge 如下:

    regression_model(Ridge())
    

    得分如下:

    58.83525077919004
    

    如您所见,XGBRegressor 在使用 gblinear 作为基本学习器时表现最佳,与 LinearRegression 一同表现突出。

  6. 现在将 booster='gbtree' 放入 XGBRegressor 中,这是默认的基本学习器:

    regression_model(XGBRegressor(booster='gbtree'))
    

    得分如下:

    65.96608419624594
    

如您所见,在这种情况下,gbtree 基本学习器的表现远不如 gblinear 基本学习器,这表明线性模型更为理想。

让我们看看是否能通过调整超参数使 gblinear 作为基本学习器获得一些提升。

gblinear 超参数

在调整超参数时,理解 gblineargbtree 之间的区别非常重要。在 第六章**,XGBoost 超参数 中介绍的许多 XGBoost 超参数是树形超参数,不适用于 gblinear。例如,max_depthmin_child_weight 是专门为树形设计的超参数。

以下是针对线性模型设计的 XGBoost gblinear 超参数总结。

reg_lambda

Scikit-learn 使用 reg_lambda 代替 lambda,因为 lambda 是 Python 中保留的关键字,用于定义 Lambda 函数。这是 Ridge 使用的标准 L2 正则化。接近 0 的值通常效果最好:

  • 默认值:0

  • 范围:[0, inf)

  • 增大可防止过拟合

  • 别名:lambda

reg_alpha

Scikit-learn 接受 reg_alphaalpha 两种方式。这是 Lasso 使用的标准 L1 正则化。接近 0 的值通常效果最好:

  • 默认值:0

  • 范围:[0, inf)

  • 增大可防止过拟合

  • 别名:alpha

更新器

这是 XGBoost 在每轮提升过程中用于构建线性模型的算法。shotgun使用hogwild并行化与坐标下降法来生成非确定性解。相比之下,coord_descent是普通的坐标下降法,产生确定性解:

  • 默认值:shotgun

  • 范围:shotgun, coord_descent

    注意

    坐标下降法是机器学习术语,定义为通过逐一寻找每个坐标的梯度来最小化误差。

feature_selector

feature_selector决定了如何选择权重,具体选项如下:

a) cyclic – 迭代循环通过特征

b) shuffle – 每轮随机特征重排的循环方式

c) random – 坐标下降法中的坐标选择是随机的

d) greedy – 耗时;选择具有最大梯度幅度的坐标

e) thrifty – 大致是贪婪的,根据权重变化重新排序特征

  • 默认值:cyclic

  • 范围必须与updater一起使用,具体如下:

    a) shotgun: cyclic, shuffle

    b) coord_descent: random, greedy, thrifty

    注意

    对于大数据集来说,greedy计算开销较大,但通过更改参数top_k(见下文),可以减少greedy考虑的特征数量。

top_k

top_kgreedythrifty在坐标下降法中选择特征的数量:

gblinear 网格搜索

现在你已经熟悉了gblinear可能使用的超参数范围,接下来让我们使用GridSearchCV在自定义的grid_search函数中找到最佳参数:

  1. 这是我们从第六章中得到的grid_search函数版本,XGBoost 超参数

    def grid_search(params, reg=XGBRegressor(booster='gblinear')):
        grid_reg = GridSearchCV(reg, params, scoring='neg_mean_squared_error', cv=kfold)
        grid_reg.fit(X, y)
        best_params = grid_reg.best_params_
        print("Best params:", best_params)
        best_score = np.sqrt(-grid_reg.best_score_)
        print("Best score:", best_score)
    
  2. 让我们从使用标准范围修改alpha开始:

    grid_search(params={'reg_alpha':[0.001, 0.01, 0.1, 0.5, 1, 5]})
    

    输出如下:

    Best params: {'reg_alpha': 0.01}
    Best score: 55.485310447306425
    

    得分大致相同,但略有提升。

  3. 接下来,让我们使用相同的范围修改reg_lambda

    grid_search(params={'reg_lambda':[0.001, 0.01, 0.1, 0.5, 1, 5]})
    

    输出如下:

    Best params: {'reg_lambda': 0.001}
    Best score: 56.17163554152289
    

    这里的得分非常相似,但略逊一筹。

  4. 现在让我们将feature_selectorupdater一起使用。默认情况下,updater=shotgunfeature_selector=cyclic。当updater=shotgun时,feature_selector唯一的另一个选择是shuffle

    让我们看看shuffle是否比cyclic表现更好:

    grid_search(params={'feature_selector':['shuffle']})
    

    输出如下:

    Best params: {'feature_selector': 'shuffle'}
    Best score: 55.531684115240594
    

    在这种情况下,shuffle表现不佳。

  5. 现在让我们将updater更改为coord_descent。因此,feature_selector可以选择randomgreedythrifty。通过输入以下代码,尝试在grid_search中测试所有feature_selector选项:

    grid_search(params={'feature_selector':['random', 'greedy', 'thrifty'], 'updater':['coord_descent'] })
    

    输出如下:

    Best params: {'feature_selector': 'thrifty', 'updater': 'coord_descent'}
    Best score: 55.48798105805444
    This is a slight improvement from the base score.
    

    最后一个需要检查的超参数是 top_k,它定义了在坐标下降过程中,greedythrifty 检查的特征数量。由于总共有 10 个特征,top_k 的范围从 29 都是可以接受的。

  6. grid_search 中为 greedythrifty 输入 top_k 的范围,以找到最佳选项:

    grid_search(params={'feature_selector':['greedy', 'thrifty'], 'updater':['coord_descent'], 'top_k':[3, 5, 7, 9]})
    

    输出如下:

    Best params: {'feature_selector': 'thrifty', 'top_k': 3, 'updater': 'coord_descent'}
    Best score: 55.478623763746256
    

这是迄今为止最好的得分。

在继续之前,请注意,除了树以外,还可以使用其他超参数,比如 n_estimatorslearning_rate

现在让我们看看 gblinear 在一个构建时就是线性的数据集上的表现。

线性数据集

确保数据集是线性的一个方法是通过构建。我们可以选择一系列 X 值,比如从 199,然后乘以一个缩放因子,并加入一些随机性。

这是构建线性数据集的步骤:

  1. 设置 X 值的范围从 1100

    X = np.arange(1,100)
    
  2. 使用 NumPy 声明一个随机种子,以确保结果的一致性:

    np.random.seed(2) 
    
  3. 创建一个空列表,定义为 y

    y = []
    
  4. 遍历 X,将每个条目乘以一个从 -0.20.2 的随机数:

    for i in X:
           y.append(i * np.random.uniform(-0.2, 0.2))
    
  5. y 转换为 numpy 数组以用于机器学习:

    y = np.array(y)
    
  6. 重塑 Xy,使它们包含与数组成员相同数量的行和一列,因为列是 scikit-learn 期望的机器学习输入:

    X = X.reshape(X.shape[0], 1)
    y = y.reshape(y.shape[0], 1)
    

    我们现在有一个线性数据集,其中 Xy 含有随机性。

让我们再次运行 regression_model 函数,这次使用 gblinear 作为基础学习器:

regression_model(XGBRegressor(booster='gblinear', objective='reg:squarederror'))

得分如下:

6.214946302686011

现在使用 gbtree 作为基础学习器运行 regression_model 函数:

regression_model(XGBRegressor(booster='gbtree', objective='reg:squarederror'))

得分如下:

9.37235946501318

如你所见,gblinear 在我们构建的线性数据集上表现更好。

为了更精确,让我们在同一数据集上尝试 LinearRegression

regression_model(LinearRegression())

得分如下:

6.214962315808842

在这种情况下,gblinear 的表现稍好,或许差距微乎其微,得分比 LinearRegression 低了 0.00002 分。

分析 gblinear

gblinear 是一个有吸引力的选择,但只有在你有理由相信线性模型可能比基于树的模型表现更好时,才应使用它。在真实数据集和构建的数据集中,gblinearLinearRegression 的表现高出非常微弱的优势。在 XGBoost 中,gblinear 在数据集庞大且线性时,是一个很强的基础学习器选择。gblinear 也可以用于分类数据集,下一节中你将应用这一方法。

比较 dart

基础学习器 dart 类似于 gbtree,因为它们都是梯度提升树。主要的区别是,dart 在每一轮提升中移除一些树(称为 dropout)。

在本节中,我们将应用并比较基础学习器 dart 与其他基础学习器在回归和分类问题中的表现。

使用 XGBRegressor 的 DART 方法

让我们看看 dart 在糖尿病数据集上的表现:

  1. 首先,像之前一样使用 load_diabetes 重新定义 Xy

    X, y = load_diabetes(return_X_y=True)
    
  2. 要将dart作为 XGBoost 的基础学习器使用,需要在regression_model函数内将XGBRegressor参数booster='dart'设置:

    regression_model(XGBRegressor(booster='dart', objective='reg:squarederror'))
    

    分数如下:

    65.96444746130739
    

dart基础学习器与gbtree基础学习器的结果相同,精确到小数点后两位。结果相似是因为数据集较小,且gbtree默认的超参数能够有效防止过拟合,无需采用丢弃树技术。

让我们看看dartgbtree在更大数据集上的分类表现如何。

dart 与 XGBClassifier

你在本书的多个章节中使用了 Census 数据集。我们在第一章《机器学习概况》中修改过的清洁版数据集,已经为你预加载,另外还包括了第八章《XGBoost 替代基础学习器》中的代码,数据集可以从github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter08获取。现在让我们开始测试dart在更大数据集上的表现:

  1. 将 Census 数据集加载到 DataFrame 中,并使用最后一列索引(-1)作为目标列,将预测列和目标列拆分为Xy

    df_census = pd.read_csv('census_cleaned.csv')
    X_census = df_census.iloc[:, :-1]
    y_census = df_census.iloc[:, -1]
    
  2. 定义一个新的分类函数,使用cross_val_score,输入为机器学习模型,输出为均值分数,类似于本章前面定义的回归函数:

    def classification_model(model):
        scores = cross_val_score(model, X_census, y_census, scoring='accuracy', cv=kfold)
        return scores.mean()
    
  3. 现在使用XGBClassifier分别设置booster='gbtree'booster='dart'调用函数两次进行结果比较。请注意,由于数据集更大,运行时间会更长:

    a) 首先,让我们调用XGBClassifier并设置booster='gbtree'

    classification_model(XGBClassifier(booster='gbtree'))
    

    分数如下:

    0.8701208195968675
    

    b) 现在,让我们调用XGBClassifier并设置booster='dart'

    classification_model(XGBClassifier(booster='dart')
    

    分数如下:

    0.8701208195968675
    

这令人吃惊。dartgbtree的结果完全相同,精确到所有 16 位小数!目前尚不清楚是否真的丢弃了树,或者丢弃树对结果没有任何影响。

我们可以调整超参数以确保丢弃树,但首先,让我们看看dartgblinear的比较。回想一下,gblinear通过使用 sigmoid 函数来对权重进行缩放,类似于逻辑回归,从而也能用于分类:

  1. 使用XGBClassifier调用classification_model函数,并设置booster='gblinear'

    classification_model(XGBClassifier(booster='gblinear'))
    

    分数如下:

    0.8501275704120015
    

    这种线性基础学习器的表现不如树型基础学习器。

  2. 让我们看看gblinear与逻辑回归的比较。由于数据集较大,最好将逻辑回归的max_iter超参数从100调整为1000,以便有更多时间进行收敛并消除警告。请注意,在这种情况下,增加max_iter能提高准确率:

    classification_model(LogisticRegression(max_iter=1000))
    

    分数如下:

    0.8008968643699182
    

    在这种情况下,gblinear比逻辑回归具有明显的优势。值得强调的是,XGBoost 的gblinear选项在分类中提供了一个可行的替代逻辑回归的方案。

现在你已经了解了dartgbtreegblinear作为基本学习器的比较,接下来我们来修改dart的超参数。

DART 超参数

dart包含所有gbtree的超参数,并且还包括一组额外的超参数,用于调整丢弃树的百分比、频率和概率。有关详细信息,请参见 XGBoost 文档:xgboost.readthedocs.io/en/latest/parameter.html#additional-parameters-for-dart-booster-booster-dart

以下各节是 XGBoost 中专门针对dart的超参数总结。

sample_type

sample_type的选项包括uniform,表示树是均匀丢弃的,和weighted,表示树按其权重比例丢弃:

  • 默认值:"uniform"

  • 范围:["uniform", "weighted"]

  • 决定丢弃树的选择方式

normalize_type

normalize_type的选项包括tree,即新树的权重与丢弃的树相同,和forest,即新树的权重与丢弃的树的总和相同:

  • 默认值:"tree"

  • 范围:["tree", "forest"]

  • 计算树的权重,以丢弃的树为单位

rate_drop

rate_drop允许用户精确设置丢弃树的百分比:

  • 默认值:0.0

  • 范围:[0.0, 1.0]

  • 丢弃树的百分比

one_drop

当设置为1时,one_drop确保每次提升轮次中总有一棵树被丢弃:

  • 默认值:0

  • 范围:[0, 1]

  • 用于确保丢弃

skip_drop

skip_drop给出了完全跳过丢弃的概率。在官方文档中,XGBoost 说 skip_drop 的优先级高于rate_dropone_drop。默认情况下,每棵树被丢弃的概率相同,因此对于某次提升轮次可能没有树被丢弃。skip_drop允许更新此概率,以控制丢弃轮次的数量:

  • 默认值:0.0

  • 范围:[0.0, 1.0]

  • 跳过丢弃的概率

现在让我们修改dart的超参数,以区分不同的得分。

修改 dart 超参数

为确保每次提升轮次中至少有一棵树被丢弃,我们可以设置one_drop=1。现在通过classification_model函数使用 Census 数据集来实现:

classification_model(XGBClassifier(booster='dart', one_drop=1))

结果如下:

0.8718714338474818

这是一个提高了百分之一点的改进,表明每次提升轮次丢弃至少一棵树可能是有利的。

现在我们正在丢弃树以更改得分,让我们回到更小且更快的糖尿病数据集,修改剩余的超参数:

  1. 使用regression_model函数,将sample_typeuniform更改为weighted

    regression_model(XGBRegressor(booster='dart', objective='reg:squarederror', sample_type='weighted'))
    

    得分如下:

    65.96444746130739
    

    这个得分比之前 gbtree 模型得分高出 0.002 分。

  2. normalize_type 更改为 forest,以便在更新权重时包括树的总和:

    regression_model(XGBRegressor(booster='dart', objective='reg:squarederror', normalize_type='forest'))
    

    得分如下:

    65.96444746130739
    

    分数没有变化,这可能发生在数据集较浅的情况下。

  3. one_drop 更改为 1,确保每次提升回合至少丢弃一棵树:

    regression_model(XGBRegressor(booster='dart', objective='reg:squarederror', one_drop=1))
    

    得分如下:

    61.81275131335009
    

    这是一个明显的改进,得分提高了四个完整点。

对于 rate_drop,即将被丢弃的树的百分比,可以使用以下 grid_search 函数来设置百分比范围:

grid_search(params={'rate_drop':[0.01, 0.1, 0.2, 0.4]}, reg=XGBRegressor(booster='dart', objective='reg:squarederror', one_drop=1))

结果如下:

Best params: {'rate_drop': 0.2}
Best score: 61.07249602732062

这是迄今为止的最佳结果。

我们可以使用类似的范围来实现 skip_drop,它给出了某棵树被丢弃的概率:

grid_search(params={'skip_drop':[0.01, 0.1, 0.2, 0.4]}, reg=XGBRegressor(booster='dart', objective='reg:squarederror'))

结果如下:

Best params: {'skip_drop': 0.1}
Best score: 62.879753748627635

这是一个不错的得分,但 skip_drop 没有带来实质性的提升。

现在你看到 dart 的实际应用,让我们分析一下结果。

分析 dart

dart 在 XGBoost 框架中提供了一个很有吸引力的选项。由于 dart 接受所有 gbtree 超参数,因此在修改超参数时,可以轻松地将基础学习器从 gbtree 改为 dart。实际上,优点是你可以尝试包括 one_droprate_dropnormalize 等新的超参数,以查看是否能获得额外的收益。在你的研究和 XGBoost 模型构建中,dart 绝对值得尝试作为基础学习器。

现在你已经对 dart 有了很好的理解,是时候转向随机森林了。

查找 XGBoost 随机森林

在 XGBoost 中实现随机森林有两种策略。第一种是将随机森林作为基础学习器,第二种是使用 XGBoost 的原始随机森林,即 XGBRFRegressorXGBRFClassifier。我们从原始主题开始,即将随机森林作为替代基础学习器。

随机森林作为基础学习器

没有选项可以将提升器的超参数设置为随机森林。相反,可以将超参数 num_parallel_tree 从其默认值 1 增加,以将 gbtree(或 dart)转变为一个提升的随机森林。这里的思路是,每个提升回合将不再是单棵树,而是多个并行的树,这些树共同构成一片森林。

以下是 XGBoost 超参数 num_parallel_tree 的简要总结。

num_parallel_tree

num_parallel_tree 指定了在每次提升回合中构建的树的数量,可能大于 1:

  • 默认值:1

  • 范围:[1, inf)

  • 给出并行提升的树的数量

  • 大于 1 的值会将提升器转变为随机森林

通过在每回合中包含多棵树,基础学习器不再是单棵树,而是森林。由于 XGBoost 包含与随机森林相同的超参数,因此当 num_parallel_tree 超过 1 时,基础学习器被适当分类为随机森林。

让我们看看 XGBoost 随机森林基础学习器在实际中的表现:

  1. 使用 XGBRegressor 调用 regression_model 并设置 booster='gbtree'。此外,设置 num_parallel_tree=25,意味着每次提升回合由 25 棵树组成:

    regression_model(XGBRegressor(booster='gbtree', objective='reg:squarederror', num_parallel_tree=25))
    

    评分如下:

    65.96604877151103
    

    评分是相当不错的,在这种情况下,几乎和提升一个单独的 gbtree 相同。原因在于梯度提升的设计是通过从前一个树的错误中学习来优化性能。通过从一个强大的随机森林开始,学习的空间有限,因此收益最多是微小的。

    理解梯度提升算法的核心优势来自于学习过程至关重要。因此,尝试将 num_parallel_tree 设置为一个更小的值(例如 5)是有意义的。

  2. 在同一个回归模型中设置 num_parallel_tree=5

    regression_model(XGBRegressor(booster='gbtree', objective='reg:squarederror', num_parallel_tree=5))
    

    评分如下:

    65.96445649315855
    

    从技术上讲,这个得分比 25 棵树的森林产生的得分高 0.002 分。虽然这个改善不大,但一般来说,在构建 XGBoost 随机森林时,较低的 num_parallel_tree 值会更好。

现在你已经看到如何将随机森林作为 XGBoost 中的基础学习器实现,是时候将随机森林构建为原始的 XGBoost 模型了。

作为 XGBoost 模型的随机森林

除了 XGBRegressorXGBClassifierXGBoost 还提供了 XGBRFRegressorXGBRFClassifier 来构建随机森林。

根据官方 XGBoost 文档 xgboost.readthedocs.io/en/latest/tutorials/rf.html,scikit-learn 的随机森林包装器仍处于实验阶段,默认值可能会随时更改。在编写本文时(2020 年),以下是 XGBRFRegressorXGBRFClassifier 的默认值。

n_estimators

使用 n_estimators 而不是 num_parallel_tree 来构建随机森林时,请使用 XGBRFRegressorXGBRFClassifier。请记住,在使用 XGBRFRegressorXGBRFClassifier 时,你并不是在做梯度提升,而是在一次回合中对树进行集成,就像传统的随机森林一样:

  • 默认值: 100

  • 范围: [1, inf)

  • 自动转换为 num_parallel_tree 用于随机森林

learning_rate

learning_rate 通常是为学习型模型设计的,包括增强器,而不是 XGBRFRegressorXGBRFClassifier,因为它们仅由一轮树组成。然而,将 learning_rate 从 1 改变会影响得分,因此通常不建议修改这个超参数:

  • 默认值: 1

  • 范围: [0, 1]

subsample, colsample_by_node

Scikit-learn 的随机森林将这些默认值保持为 1,使得默认的 XGBRFRegressorXGBRFClassifier 更不容易过拟合。这是 XGBoost 和 scikit-learn 随机森林默认实现之间的主要区别:

  • 默认值: 0.8

  • 范围: [0, 1]

  • 减少有助于防止过拟合

现在,让我们看看 XGBoost 的随机森林在实践中的工作原理:

  1. 首先,将XGBRFRegressor放入regression_model函数中:

    regression_model(XGBRFRegressor(objective='reg:squarederror'))
    

    得分如下:

    59.447250741400595
    

    这个得分稍微比之前展示的gbtree模型好一些,但比本章中展示的最佳线性模型稍差。

  2. 为了对比,看看将RandomForestRegressor放入相同函数后的表现:

    regression_model(RandomForestRegressor())
    

    得分如下:

    59.46563031802505
    

    这个得分稍微比XGBRFRegressor差一些。

现在让我们使用更大的 Census 数据集进行分类,将 XGBoost 随机森林与 scikit-learn 的标准随机森林进行比较:

  1. XGBRFClassifier放入classification_model函数中,看看它在预测用户收入时的表现如何:

    classification_model(XGBRFClassifier())
    

    得分如下:

    0.856085650471878
    

    这个得分很好,比gbtree稍低,gbtree之前的得分是 87%。

  2. 现在将RandomForestClassifier放入相同的函数中,比较结果:

    classification_model(RandomForestClassifier())
    

    得分如下:

    0.8555328202034789
    

    这个得分稍微比 XGBoost 的实现差一些。

由于 XGBoost 的随机森林仍处于开发阶段,我们将在此结束并分析结果。

分析 XGBoost 随机森林

你可以通过将num_parallel_tree增加到大于1的值,随时尝试将随机森林作为 XGBoost 的基础学习器。尽管正如你在本节中所见,提升法(boosting)是为了从弱模型中学习,而不是从强模型中学习,因此num_parallel_tree的值应该保持接近1。将随机森林作为基础学习器应该谨慎使用。如果单棵树的提升法未能产生最佳分数,随机森林基础学习器是一个可选方案。

或者,XGBoost 的随机森林的XGBRFRegressorXGBRFClassifier可以作为 scikit-learn 随机森林的替代方法来实现。XGBoost 新的XGBRFRegressorXGBRFClassifier表现超过了 scikit-learn 的RandomForestRegressorRandomForestClassifier,尽管这次比较非常接近。鉴于 XGBoost 在机器学习社区中的总体成功,未来使用XGBRFRegressorXGBRFClassifier作为可行的选择绝对值得尝试。

总结

在本章中,你通过将所有 XGBoost 基础学习器(包括gbtreedartgblinear和随机森林)应用于回归和分类数据集,极大地扩展了 XGBoost 的使用范围。你预览、应用并调优了独特的基础学习器超参数以提高得分。此外,你还尝试了使用线性构建数据集的gblinear,以及使用XGBRFRegressorXGBRFClassifier构建没有任何提升法的 XGBoost 随机森林。现在,你已经熟悉了所有基础学习器,你对 XGBoost 的理解已经达到了高级水平。

在下一章中,你将分析 Kaggle 高手的技巧,进一步提升你的 XGBoost 技能!

第九章:第九章:XGBoost Kaggle 大师

在本章中,您将学习从VotingClassifierVotingRegressor中获得的宝贵技巧和窍门,以构建非相关的机器学习集成模型,并了解堆叠最终模型的优势。

在本章中,我们将涵盖以下主要内容:

  • 探索 Kaggle 竞赛

  • 构建新的数据列

  • 构建非相关集成模型

  • 堆叠最终模型

技术要求

本章的代码可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter09找到。

探索 Kaggle 竞赛

"我只用了 XGBoost(尝试过其他的,但没有一个能达到足够的表现以最终加入我的集成模型中)。"

Qingchen Wang,Kaggle 获胜者

(www.cnblogs.com/yymn/p/4847130.html)

在本节中,我们将通过回顾 Kaggle 竞赛的简短历史、它们的结构以及区分验证/测试集与保留/测试集的重要性,来探讨 Kaggle 竞赛。

XGBoost 在 Kaggle 竞赛中的表现

XGBoost 因其在赢得 Kaggle 竞赛中的无与伦比的成功而建立了作为领先机器学习算法的声誉。XGBoost 常常与深度学习模型如神经网络一起出现在获胜的集成模型中,除了单独获胜之外。一个 XGBoost Kaggle 竞赛获胜者的样例列表出现在分布式(深度)机器学习社区的网页上,github.com/dmlc/xgboost/tree/master/demo#machine-learning-challenge-winning-solutions。要查看更多 XGBoost Kaggle 竞赛获胜者,可以通过Winning solutions of Kaggle competitions (www.kaggle.com/sudalairajkumar/winning-solutions-of-kaggle-competitions)来研究获胜模型。

注意事项

虽然 XGBoost 经常出现在获胜者中,但其他机器学习模型也有出现。

第五章《XGBoost 揭秘》中提到的,Kaggle 竞赛是机器学习竞赛,机器学习从业者相互竞争,争取获得最佳分数并赢得现金奖励。当 XGBoost 在 2014 年参加希格斯玻色子机器学习挑战时,它迅速跃升至排行榜顶端,并成为 Kaggle 竞赛中最受欢迎的机器学习算法之一。

在 2014 年到 2018 年间,XGBoost 一直在表格数据(以行和列组织的数据,相对于图像或文本等非结构化数据,神经网络在这些领域占有优势)上表现出色。随着 LightGBM 在 2017 年问世,这款由微软推出的快速梯度提升算法,XGBoost 在表格数据上终于遇到了真正的竞争者。

以下是由八位作者编写的入门论文,LightGBM: A Highly Efficient Gradient Boosting Decision Tree,推荐用作了解 LightGBM 的入门材料:papers.nips.cc/paper/6907-lightgbm-a-highly-efficient-gradient-boosting-decision-tree.pdf

在 Kaggle 竞赛中实现一个优秀的机器学习算法,如 XGBoost 或 LightGBM,仅仅做到这一点还不够。同样,调优模型的超参数通常也不足够。尽管单个模型的预测很重要,但工程化新的数据并结合最优模型以获得更高的分数同样至关重要。

Kaggle 竞赛的结构

理解 Kaggle 竞赛的结构是很有价值的,这有助于你理解为什么像无相关的集成构建和堆叠技术在竞赛中如此广泛应用。此外,探索 Kaggle 竞赛的结构也会让你在未来如果选择参与此类竞赛时更有信心。

提示

Kaggle 推荐了 Housing Prices: Advanced Regression Techniqueswww.kaggle.com/c/house-prices-advanced-regression-techniques,适合那些希望从基础过渡到高级竞赛的机器学习学生。这是许多基于知识的竞赛之一,尽管它们不提供现金奖励。

Kaggle 竞赛存在于 Kaggle 网站上。以下是 Avito Context Ad Clicks 竞赛网站的链接,这场比赛于 2015 年由 XGBoost 用户 Owen Zhang 获胜:www.kaggle.com/c/avito-context-ad-clicks/overview。许多 XGBoost Kaggle 竞赛的获胜者,包括 Owen Zhang,在 2015 年就已获得奖项,这表明 XGBoost 在 Tianqi Chin 2016 年发表的里程碑论文 XGBoost: A Scalable Tree Boosting System 之前就已广泛传播:arxiv.org/pdf/1603.02754.pdf

这是 Avito Context Ad Clicks 网站的顶部:

图 9.1 – Avito Context Ad Clicks Kaggle 竞赛网站

图 9.1 – Avito Context Ad Clicks Kaggle 竞赛网站

该概览页面对竞赛的解释如下:

  • 概览(以蓝色高亮显示)旁边的附加链接包括 数据,在这里你可以访问竞赛的数据。

  • 笔记本,Kagglers 发布解决方案和起始笔记本的地方。

  • 讨论区,Kagglers 在这里发布和回答问题。

  • 排行榜,展示最高分的地方。

  • 规则,解释了竞赛的运作方式。

  • 另外,请注意右侧的延迟提交链接,表示即使竞赛已经结束,提交仍然是被接受的,这是 Kaggle 的一项常规政策。

若要下载数据,你需要通过注册一个免费的账户来参加竞赛。数据通常被分为两个数据集,training.csv 是用于构建模型的训练集,test.csv 是用于评估模型的测试集。提交模型后,你会在公共排行榜上获得一个分数。竞赛结束时,最终模型会提交给一个私有测试集,以确定获胜的解决方案。

保留集

在构建机器学习模型时,区分在 Kaggle 竞赛中构建模型与独立构建模型非常重要。到目前为止,我们已经将数据集分为训练集和测试集,以确保我们的模型能够很好地泛化。然而,在 Kaggle 竞赛中,模型必须在竞争环境中进行测试。因此,测试集的数据会保持隐藏。

下面是 Kaggle 的训练集和测试集之间的区别:

  • training.csv:这是你自己训练和评分模型的地方。这个训练集应该使用 train_test_splitcross_val_score 将其划分为自己的训练集和测试集,从而构建能够很好泛化到新数据的模型。在训练过程中使用的测试集通常被称为验证集,因为它们用来验证模型。

  • test.csv:这是一个独立的保留集。在模型准备好并可以在它从未见过的数据上进行测试之前,你不会使用测试集。隐藏测试集的目的是保持竞赛的公正性。测试数据对参与者是隐藏的,结果只会在参与者提交模型之后才会公开。

在构建研究或行业模型时,将测试集留存一旁始终是一个良好的做法。当一个模型在已经见过的数据上进行测试时,模型有过拟合测试集的风险,这种情况通常出现在 Kaggle 竞赛中,参赛者通过千分之一的微小差异来过度优化自己的成绩,从而在公共排行榜上提升名次。

Kaggle 竞赛与现实世界在保留集的使用上有所交集。构建机器学习模型的目的是使用未知数据进行准确的预测。例如,如果一个模型在训练集上达到了 100% 的准确率,但在未知数据上只有 50% 的准确率,那么这个模型基本上是没有价值的。

在测试集上验证模型与在保留集上测试模型之间的区别非常重要。

这里是验证和测试机器学习模型的一般方法:

  1. 将数据划分为训练集和保留集:将保留集隔离开,并抵制查看它的诱惑。

  2. 将训练集划分为训练集和测试集,或使用交叉验证:在训练集上拟合新模型,并验证模型,一来一回地改进得分。

  3. 在获得最终模型后,在保留集上进行测试:这是对模型的真正考验。如果得分低于预期,返回到第 2 步并重复。切记——这一点很重要——不要将保留集用作新的验证集,一来一回地调整超参数。这样做会导致模型根据保留集进行调整,这违背了保留集的初衷。

在 Kaggle 竞赛中,过于将机器学习模型与测试集紧密结合是行不通的。Kaggle 通常将测试集拆分为公共和私有两个部分。公共测试集让参赛者有机会评估他们的模型,并进行改进,一来一回地调整并重新提交。私有测试集直到竞赛最后一天才会揭晓。虽然公共测试集的排名会显示,但竞赛的胜者是基于未见测试集的结果宣布的。

赢得 Kaggle 竞赛需要在私有测试集上获得尽可能高的分数。在 Kaggle 竞赛中,每一个百分点都至关重要。业界有时对这种精确度嗤之以鼻,但它促使了创新的机器学习实践来提高得分。理解本章所介绍的这些技术,可以让我们构建更强的模型,并更深入地理解整体机器学习。

开发新列

"几乎总是,我都能找到我想做的事情的开源代码,我的时间应该更多地用在研究和特征工程上。"

Owen Zhang,Kaggle 冠军

(medium.com/kaggle-blog/profiling-top-kagglers-owen-zhang-currently-1-in-the-world-805b941dbb13)

许多 Kaggle 参赛者和数据科学家都承认,他们花了相当多的时间在研究和特征工程上。在本节中,我们将使用pandas来开发新的数据列。

什么是特征工程?

机器学习模型的效果取决于它们训练所用的数据。当数据不足时,构建一个强大的机器学习模型几乎是不可能的。

一个更具启发性的问题是,数据是否可以改进。当从其他列中提取新数据时,这些新列的数据被称为工程化数据。

特征工程是从原始列中开发新数据列的过程。问题不是你是否应该实施特征工程,而是你应该实施多少特征工程。

让我们在预测UberLyft打车费的数据集上进行特征工程实践。

Uber 和 Lyft 数据

除了举办竞赛,Kaggle 还主办了大量数据集,其中包括如下公开数据集,该数据集预测 Uber 和 Lyft 的出租车价格:www.kaggle.com/ravi72munde/uber-lyft-cab-prices

  1. 首先,导入本节所需的所有库和模块,并禁止警告:

    import pandas as pd
    import numpy as np
    from sklearn.model_selection import cross_val_score
    from xgboost import XGBClassifier, XGBRFClassifier
    from sklearn.ensemble import RandomForestClassifier, StackingClassifier
    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import train_test_split, StratifiedKFold
    from sklearn.metrics import accuracy_score
    from sklearn.ensemble import VotingClassifier
    import warnings
    warnings.filterwarnings('ignore')
    
  2. 接下来,加载'cab_rides.csv' CSV 文件并查看前五行。限制nrows10000,以加快计算速度。数据总共有超过 60 万行:

    df = pd.read_csv('cab_rides.csv', nrows=10000)
    df.head()
    

    以下是预期输出:

图 9.2 – 出租车数据集

图 9.2 – 出租车数据集

此展示显示了多种列,包括类别特征和时间戳。

空值

一如既往,进行任何计算之前要检查空值:

  1. 记得df.info()也提供了关于列类型的信息:

    df.info()
    

    输出结果如下:

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 10000 entries, 0 to 9999
    Data columns (total 10 columns):
     #   Column            Non-Null Count  Dtype  
    ---  ------            --------------  -----  
     0   distance          10000 non-null  float64
     1   cab_type          10000 non-null  object 
     2   time_stamp        10000 non-null  int64  
     3   destination       10000 non-null  object 
     4   source            10000 non-null  object 
     5   price             9227 non-null   float64
     6   surge_multiplier  10000 non-null  float64
     7   id                10000 non-null  object 
     8   product_id        10000 non-null  object 
     9   name              10000 non-null  object 
    dtypes: float64(3), int64(1), object(6)
    memory usage: 781.4+ KB
    

    从输出结果可以看到,price列中存在空值,因为非空浮动数值少于10,000

  2. 检查空值是值得的,以查看是否可以获得更多关于数据的信息:

    df[df.isna().any(axis=1)]
    

    以下是输出的前五行:

    图 9.3 – 出租车数据集中的空值

    图 9.3 – 出租车数据集中的空值

    如你所见,这些行没有什么特别明显的问题。可能是因为这次乘车的价格从未被记录。

  3. 由于price是目标列,可以使用dropna删除这些行,并使用inplace=True参数确保删除操作发生在 DataFrame 中:

    df.dropna(inplace=True)
    

你可以使用df.na()df.info()再检查一次,验证没有空值。

特征工程时间列

时间戳列通常表示Unix 时间,即自 1970 年 1 月 1 日以来的毫秒数。可以从时间戳列中提取特定时间数据,帮助预测出租车费用,如月份、小时、是否为高峰时段等:

  1. 首先,使用pd.to_datetime将时间戳列转换为时间对象,然后查看前五行:

    df['date'] = pd.to_datetime(df['time_stamp'])
    df.head()
    

    以下是预期输出:

    图 9.4 – 时间戳转换后的出租车数据集

    图 9.4 – 时间戳转换后的出租车数据集

    这个数据有问题。稍微具备领域知识的人就能知道 Lyft 和 Uber 在 1970 年并不存在。额外的小数位是转换不正确的线索。

  2. 尝试了几个乘数以进行适当的转换后,我发现10**6给出了合适的结果:

    df['date'] = pd.to_datetime(df['time_stamp']*(10**6))
    df.head()
    

    以下是预期输出:

    图 9.5 – 转换后的出租车数据集

    图 9.5 – 'date'转换后的出租车数据集

  3. 对于一个日期时间列,你可以在导入datetime后提取新列,如monthhourday of week,如下所示:

    import datetime as dt
    df['month'] = df['date'].dt.month
    df['hour'] = df['date'].dt.hour
    df['dayofweek'] = df['date'].dt.dayofweek
    

    现在,你可以使用这些列来进行特征工程,创建更多的列,例如判断是否是周末或高峰时段。

  4. 以下函数通过检查 'dayofweek' 是否等于 56 来确定一周中的某天是否为周末,这两个值分别代表星期六或星期天,具体参见官方文档:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.weekday.html

    def weekend(row):
        if row['dayofweek'] in [5,6]:
            return 1
        else:
            return 0
    
  5. 接下来,将该函数应用于 DataFrame 作为新列 df['weekend'],如下所示:

    df['weekend'] = df.apply(weekend, axis=1)
    
  6. 相同的策略可以用来创建一个高峰时段列,通过判断小时是否在早上 6-10 点(小时 6-10)和下午 3-7 点(小时 15-19)之间:

    def rush_hour(row):
        if (row['hour'] in [6,7,8,9,15,16,17,18]) & 
            (row['weekend'] == 0):
            return 1
        else:
            return 0
    
  7. 现在,将该函数应用于新的 'rush_hour' 列:

    df['rush_hour'] = df.apply(rush_hour, axis=1)
    
  8. 最后一行显示了新列的变化,正如df.tail()所揭示的:

    df.tail()
    

    下面是输出的摘录,展示了新列:

图 9.6 – 特征工程后出租车乘车数据集的最后五行

图 9.6 – 特征工程后出租车乘车数据集的最后五行

提取和工程化新时间列的过程可以继续进行。

注意

在进行大量新列的工程化时,值得检查是否有新的特征高度相关。数据的相关性将在本章稍后讨论。

现在你理解了时间列特征工程的实践,让我们来进行类别列的特征工程。

类别列特征工程

之前,我们使用 pd.get_dummies 将类别列转换为数值列。Scikit-learn 的 OneHotEncoder 特性是另一种选择,它使用稀疏矩阵将类别数据转换为 0 和 1,这种技术将在 第十章 XGBoost 模型部署 中应用。虽然使用这两种方法将类别数据转换为数值数据是常规做法,但也存在其他的替代方法。

虽然 0 和 1 作为类别列的数值表示是有意义的,因为 0 表示缺失,1 表示存在,但也有可能其他值能提供更好的结果。

一种策略是将类别列转换为它们的频率,这相当于每个类别在给定列中出现的百分比。因此,列中的每个类别都被转换为它在该列中的百分比,而不是一个类别列。

接下来,让我们查看将类别值转换为数值值的步骤。

工程化频率列

要对类别列进行工程化,例如 'cab_type',首先查看每个类别的值的数量:

  1. 使用 .value_counts() 方法查看各类型的频率:

    df['cab_type'].value_counts()
    

    结果如下:

    Uber    4654
    Lyft    4573
    Name: cab_type, dtype: int64
    
  2. 使用 groupby 将计数放入新列。df.groupby(column_name)groupby,而 [column_name].transform 指定要转换的列,后面跟着括号内的聚合操作:

    df['cab_freq'] = df.groupby('cab_type')['cab_type'].transform('count')
    
  3. 将新列除以总行数以获得频率:

    df['cab_freq'] = df['cab_freq']/len(df)
    
  4. 验证更改是否按预期进行:

    df.tail()
    

    下面是显示新列的输出摘录:

图 9.7 – 经出租车频率工程处理后的出租车乘车数据集

图 9.7 – 经出租车频率工程处理后的出租车乘车数据集

现在,出租车频率显示出预期的结果。

Kaggle 小贴士 – 均值编码

我们将通过一个经过竞赛验证的特征工程方法来结束这一部分,称为均值编码目标编码

均值编码将类别列转换为基于目标变量均值的数值列。例如,如果颜色橙色对应的七个目标值为 1,三个目标值为 0,那么均值编码后的列将是 7/10 = 0.7。由于在使用目标值时存在数据泄漏,因此需要额外的正则化技术。

数据泄漏发生在训练集和测试集之间,或者预测列和目标列之间共享信息时。这里的风险是目标列被直接用来影响预测列,这在机器学习中通常是个坏主意。不过,均值编码已被证明能产生出色的结果。当数据集很深,并且均值分布对于输入数据大致相同时,它仍然有效。正则化是减少过拟合可能性的一项额外预防措施。

幸运的是,scikit-learn 提供了TargetEncoder来帮助你处理均值转换:

  1. 首先,从category_encoders导入TargetEncoder。如果无法工作,可以使用以下代码安装category_encoders

    pip install --upgrade category_encoders
    from category_encoders.target_encoder import TargetEncoder
    
  2. 接下来,初始化encoder,如下所示:

    encoder = TargetEncoder()
    
  3. 最后,介绍一个新列,并使用编码器的fit_transform方法应用均值编码。将要更改的列和目标列作为参数传入:

    df['cab_type_mean'] = encoder.fit_transform(df['cab_type'], df['price'])
    
  4. 现在,验证更改是否按预期进行:

    df.tail()
    

    下面是显示新列的输出摘录:

图 9.8 – 经均值编码后的出租车乘车数据集

图 9.8 – 经均值编码后的出租车乘车数据集

最右侧的列cab_type_mean符合预期。

有关均值编码的更多信息,请参考这篇 Kaggle 研究:www.kaggle.com/vprokopev/mean-likelihood-encodings-a-comprehensive-study

这里的观点并不是说均值编码比独热编码更好,而是说明均值编码是一种经过验证的技术,在 Kaggle 竞赛中表现优异,可能值得实施来尝试提高得分。

更多特征工程

没有理由止步于此。更多的特征工程可能包括对其他列进行统计度量,使用groupby和附加编码器。其他类别型列,比如目的地和到达列,可以转换为纬度和经度,然后转换为新的距离度量方式,例如出租车距离或Vincenty距离,它考虑了球面几何。

在 Kaggle 竞赛中,参与者可能会进行数千列新的特征工程,希望能获得几位小数的准确度。如果你有大量的工程化列,可以使用.feature_importances_选择最重要的列,正如在第二章《决策树深入剖析》中所述。你还可以去除高度相关的列(将在下一节“构建无相关性的集成模型”中解释)。

对于这个特定的出租车乘车数据集,还附带了一个包含天气数据的 CSV 文件。但如果没有天气文件该怎么办呢?你可以自行查找提供日期的天气数据,并将其添加到数据集中。

特征工程是任何数据科学家构建鲁棒模型的必要技能。这里讲解的策略只是现存选项的一部分。特征工程涉及研究、实验、领域专业知识、标准化列、对新列的机器学习性能反馈,并最终缩小最终列的范围。

现在你已经了解了各种特征工程策略,让我们继续讨论构建无相关性的集成模型。

构建无相关性的集成模型

“在我们的最终模型中,我们使用了 XGBoost 作为集成模型,其中包含了 20 个 XGBoost 模型,5 个随机森林,6 个随机化决策树模型,3 个正则化贪婪森林,3 个逻辑回归模型,5 个 ANN 模型,3 个弹性网模型和 1 个 SVM 模型。”

Song, Kaggle 获胜者

(hunch243.rssing.com/chan-68612493/all_p1.html)

Kaggle 竞赛的获胜模型很少是单一模型;它们几乎总是集成模型。这里所说的集成模型,并不是指提升(boosting)或袋装(bagging)模型,如随机森林(random forests)或 XGBoost,而是纯粹的集成模型,包含任何不同的模型,包括 XGBoost、随机森林等。

在本节中,我们将结合机器学习模型,构建无相关性的集成模型,以提高准确性并减少过拟合。

模型范围

威斯康星州乳腺癌数据集用于预测患者是否患有乳腺癌,包含 569 行和 30 列数据,可以在scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.html?highlight=load_breast_cancer查看。

以下是使用几种分类器准备和评分数据集的步骤:

  1. 从 scikit-learn 导入 load_breast_cancer 数据集,以便我们能快速开始构建模型:

    from sklearn.datasets import load_breast_cancer
    
  2. 通过设置 return_X_y=True 参数,将预测变量列赋值给 X,将目标变量列赋值给 y

    X, y = load_breast_cancer(return_X_y=True)
    
  3. 使用 StratifiedKFold 准备 5 折交叉验证以确保一致性:

    kfold = StratifiedKFold(n_splits=5)
    
  4. 现在,构建一个简单的分类函数,该函数接收一个模型作为输入,并返回交叉验证的平均得分作为输出:

    def classification_model(model):
        scores = cross_val_score(model, X, y, cv=kfold)
        return scores.mean()
    
  5. 获取几个默认分类器的得分,包括 XGBoost 及其替代基础学习器、随机森林和逻辑回归:

    a) 使用 XGBoost 进行评分:

    classification_model(XGBClassifier())
    

    得分如下:

    0.9771619313771154
    

    b) 使用 gblinear 进行评分:

    classification_model(XGBClassifier(booster='gblinear'))
    

    得分如下:

    0.5782952957615277
    

    c) 使用 dart 进行评分:

    classification_model(XGBClassifier(booster='dart', one_drop=True))
    

    得分如下:

    0.9736376339077782
    

    请注意,对于 dart 增强器,我们设置 one_drop=True 以确保树木确实被丢弃。

    d) 使用 RandomForestClassifier 进行评分:

    classification_model(RandomForestClassifier(random_state=2))
    

    得分如下:

    0.9666356155876418
    

    e) 使用 LogisticRegression 进行评分:

    classification_model(LogisticRegression(max_iter=10000))
    

    得分如下:

    0.9490451793199813
    

大多数模型的表现都很不错,其中 XGBoost 分类器获得了最高分。然而,gblinear 基础学习器的表现不太好,因此我们以后将不再使用它。

实际上,应该对每个模型进行调整。由于我们在多个章节中已经介绍了超参数调整,因此在这里不再讨论该选项。然而,了解超参数的知识可以增加尝试快速模型并调整一些参数值的信心。例如,正如以下代码所示,可以尝试将 XGBoost 的 max_depth 降至 2,将 n_estimators 增加到 500,并确保将 learning_rate 设置为 0.1

classification_model(XGBClassifier(max_depth=2, n_estimators=500, learning_rate=0.1))

得分如下:

0.9701133364384411

这是一个非常不错的得分,尽管它不是最高的,但在我们的集成模型中可能仍然有价值。

现在我们有了多种模型,让我们了解它们之间的相关性。

相关性

本节的目的是选择非相关的模型,而不是选择所有模型进行集成。

首先,让我们理解相关性代表什么。

相关性是一个统计度量,范围从 -11,表示两组数据点之间线性关系的强度。相关性为 1 表示完全的直线关系,而相关性为 0 表示没有任何线性关系。

一些关于相关性的可视化图表可以使事情变得更加清晰。以下图表来自维基百科的 Correlation and Dependence 页面,en.wikipedia.org/wiki/Correlation_and_dependence

  • 显示相关性的散点图如下所示:

图 9.9 – 列出相关性

图 9.9 – 列出相关性

许可证信息

由 DenisBoigelot 上传,原上传者是 Imagecreator – 自制作品,CC0,commons.wikimedia.org/w/index.php?curid=15165296

  • Anscombe 四重奏 – 四个相关性为 0.816 的散点图如下所示:

图 9.10 – 相关性为 0.816

图 9.10 – 相关性为 0.816

许可证信息

由 Anscombe.svg 提供:Schutz(使用下标标记):Avenue – Anscombe.svg,CC BY-SA 3.0,commons.wikimedia.org/w/index.php?curid=9838454

第一个示例表明,相关性越高,点通常越接近直线。第二个示例表明,相同相关性的数据显示点可能会有较大差异。换句话说,相关性提供了有价值的信息,但它不能完全说明问题。

现在你理解了相关性是什么意思,接下来让我们将相关性应用于构建机器学习集成。

机器学习集成中的相关性

现在我们选择要包括在集成学习中的模型。

机器学习模型之间的高相关性在集成学习中是不可取的。那为什么呢?

考虑两个分类器每个有 1,000 个预测的情况。如果这两个分类器做出了相同的预测,那么从第二个分类器中没有获得新信息,使得它变得多余。

使用多数规则实现时,只有在大多数分类器预测错误时,预测才算错误。因此,拥有表现良好但给出不同预测的多样化模型是可取的。如果大多数模型给出了相同的预测,相关性就很高,那么将新模型加入集成学习的价值就不大了。找到模型预测的差异,尤其是强模型可能错误的地方,为集成学习提供了产生更好结果的机会。当模型不相关时,预测结果会有所不同。

要计算机器学习模型之间的相关性,我们首先需要用来比较的数据点。机器学习模型生成的不同数据点是它们的预测结果。获得预测结果后,我们将它们连接成一个数据框,然后应用.corr方法一次性获取所有相关性。

以下是找到机器学习模型之间相关性的步骤:

  1. 定义一个函数,返回每个机器学习模型的预测结果:

    def y_pred(model):
        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        score = accuracy_score(y_pred, y_test)
        print(score)
        return y_pred
    
  2. 使用train_test_split准备数据进行一次预测:

    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    
  3. 使用之前定义的函数获取所有分类器候选的预测结果:

    a) XGBClassifier使用以下方法:

    y_pred_gbtree = y_pred(XGBClassifier())
    

    准确率得分如下:

    0.951048951048951
    

    b) XGBClassifier使用dart,采用以下方法:

    y_pred_dart = y_pred(XGBClassifier(booster='dart', one_drop=True))
    

    准确率得分如下:

    0.951048951048951
    

    c) RandomForestClassifier使用以下方法:

    y_pred_forest = y_pred(RandomForestClassifier())
    

    准确率得分如下:

    0.9370629370629371
    

    d) LogisticRegression使用以下方法:

    y_pred_logistic = y_pred(LogisticRegression(max_iter=10000))
    

    准确率得分如下:

    0.9370629370629371
    y_pred_xgb = y_pred(XGBClassifier(max_depth=2, n_estimators=500, learning_rate=0.1))
    

    准确率得分如下:

    0.965034965034965
    
  4. 使用np.cc代表连接)将预测结果连接成一个新的数据框:

    df_pred = pd.DataFrame(data= np.c_[y_pred_gbtree, y_pred_dart, y_pred_forest, y_pred_logistic, y_pred_xgb], columns=['gbtree', 'dart','forest', 'logistic', 'xgb'])
    
  5. 使用.corr()方法在数据框上运行相关性计算:

    df_pred.corr()
    

    你应该看到以下输出:

图 9.11 – 各种机器学习模型之间的相关性

图 9.11 – 各种机器学习模型之间的相关性

如您所见,所有对角线上的相关性都是1.0,因为模型与自身之间的相关性必须是完全线性的。其他所有值也相当高。

没有明确的截断值来确定非相关性的阈值。最终选择依赖于相关性值和可选模型的数量。对于这个例子,我们可以选择与最佳模型xgb相关性最小的下两个模型,分别是随机森林和逻辑回归。

现在我们已经选择了模型,接下来我们将它们组合成一个集成模型,使用VotingClassifier集成,如下所述。

VotingClassifier 集成

Scikit-learn 的VotingClassifier集成旨在结合多个分类模型,并使用多数规则选择每次预测的输出。请注意,scikit-learn 还包含VotingRegressor,它通过取每个回归模型的平均值来结合多个回归模型。

以下是在 scikit-learn 中创建集成模型的步骤:

  1. 初始化一个空列表:

    estimators = []
    
  2. 初始化第一个模型:

    logistic_model = LogisticRegression(max_iter=10000)
    
  3. 将模型作为元组(model_name, model)追加到列表中:

    estimators.append(('logistic', logistic_model))
    
  4. 根据需要重复步骤 2步骤 3

    xgb_model = XGBClassifier(max_depth=2, n_estimators=500, learning_rate=0.1)
    estimators.append(('xgb', xgb_model))
    rf_model = RandomForestClassifier(random_state=2)
    estimators.append(('rf', rf_model))
    
  5. 使用模型列表作为输入初始化VotingClassifier(或VotingRegressor):

    ensemble = VotingClassifier(estimators)
    
  6. 使用cross_val_score评分分类器:

    scores = cross_val_score(ensemble, X, y, cv=kfold)
    print(scores.mean())
    

    得分如下:

    0.9754075454122031
    

如您所见,得分已经提高。

现在您已经了解了构建无相关性机器学习集成模型的目的和技术,让我们继续探讨一种类似但可能更有优势的技术——堆叠。

堆叠模型

“对于堆叠和提升方法,我使用 xgboost,主要是由于对它的熟悉以及它验证过的优异结果。”

David Austin, Kaggle 冠军

(www.pyimagesearch.com/2018/03/26/interview-david-austin-1st-place-25000-kaggles-popular-competition/)

在本节最后,我们将探讨 Kaggle 获奖者经常使用的最强大技巧之一——堆叠。

什么是堆叠?

堆叠将机器学习模型结合在两个不同的层次:基础层,模型对所有数据进行预测;元层,将基础层模型的预测作为输入,并用它们生成最终预测。

换句话说,堆叠中的最终模型并不直接使用原始数据作为输入,而是将基础机器学习模型的预测作为输入。

堆叠模型在 Kaggle 比赛中取得了巨大的成功。大多数 Kaggle 比赛都有合并截止日期,个人和团队可以在此期间合并。通过合并,作为团队而非个人竞争可以获得更大的成功,因为参赛者可以构建更大的集成模型并将其堆叠在一起。

注意,堆叠与标准集成方法不同,因为它有一个在最后进行预测组合的元模型。由于元模型将预测值作为输入,因此通常建议使用一个简单的元模型,比如回归任务中的线性回归和分类任务中的逻辑回归。

现在你对堆叠有所了解,让我们使用 scikit-learn 应用堆叠。

在 scikit-learn 中的堆叠

幸运的是,scikit-learn 提供了一个堆叠回归器和分类器,使得这一过程相当简单。其基本思路与上一节中的集成模型非常相似。选择多种基础模型,然后为元模型选择线性回归或逻辑回归。

以下是在 scikit-learn 中使用堆叠的步骤:

  1. 创建一个空的基础模型列表:

    base_models = []
    
  2. 使用语法(name, model)将所有基础模型作为元组附加到基础模型列表中:

    base_models.append(('lr', LogisticRegression()))
    base_models.append(('xgb', XGBClassifier()))
    base_models.append(('rf', RandomForestClassifier(random_state=2)))
    

    在堆叠中可以选择更多的模型,因为没有多数规则的限制,并且线性权重能更容易地调整到新数据。一个最佳方法是使用非相关性作为松散的指导原则,并尝试不同的组合。

  3. 选择一个元模型,最好是回归任务中的线性回归和分类任务中的逻辑回归:

    meta_model = LogisticRegression()
    
  4. 使用base_models作为estimatorsmeta_model作为final_estimator来初始化StackingClassifier(或StackingRegressor):

    clf = StackingClassifier(estimators=base_models, final_estimator=meta_model)
    
  5. 使用cross_val_score或任何其他评分方法来验证堆叠模型:

    scores = cross_val_score(clf, X, y, cv=kfold)
    print(scores.mean())
    

    得分如下:

    0.9789318428815401
    

这是迄今为止最强的结果。

正如你所看到的,堆叠是一种非常强大的方法,它超越了上一节中的非相关集成模型。

总结

在这一章中,你学习了一些来自 Kaggle 竞赛获胜者的经过验证的技巧和窍门。除了探索 Kaggle 竞赛并理解保留集的重要性外,你还获得了在时间列特征工程、类别列特征工程、均值编码、构建非相关集成模型以及堆叠方面的基本实践。这些高级技术在精英 Kaggler 中广泛使用,它们能在你开发用于研究、竞赛和行业的机器学习模型时,提供优势。

在下一章,也是最后一章,我们将从竞争世界转向技术世界,在这里我们将使用转换器和管道从头到尾构建一个 XGBoost 模型,完成一个适合行业部署的模型。

第十章:第十章: XGBoost 模型部署

在本章关于 XGBoost 的最后一部分,您将把所有内容结合起来,并开发新的技术,构建一个适应行业需求的强大机器学习模型。将模型部署到行业环境中与为研究和竞赛构建模型有所不同。在行业中,自动化非常重要,因为新数据会频繁到达。更多的重视放在流程上,而不是通过微调机器学习模型来获得微小的百分比提升。

本章中,您将获得与独热编码稀疏矩阵相关的丰富经验。此外,您还将实现并自定义 scikit-learn 转换器,以自动化机器学习管道,对包含分类数值列的混合数据进行预测。到本章结束时,您的机器学习管道将准备好处理任何新来的数据。

在本章中,我们将讨论以下主题:

  • 编码混合数据

  • 自定义 scikit-learn 转换器

  • 完成 XGBoost 模型

  • 构建机器学习管道

技术要求

本章的代码可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter10找到。

编码混合数据

想象一下,您正在为一家教育科技公司工作,您的工作是预测学生成绩,以便为弥补技术技能差距的服务提供目标。您的第一步是将包含学生成绩的数据加载到pandas中。

加载数据

由公司提供的学生表现数据集,可以通过加载已为您导入的student-por.csv文件来访问。

首先导入pandas并关闭警告。然后,下载数据集并查看前五行:

import pandas as pd
import warnings
warnings.filterwarnings('ignore')
df = pd.read_csv('student-por.csv')
df.head()

这里是预期的输出:

图 10.1 – 学生表现数据集原始样子

图 10.1 – 学生表现数据集的原始样子

欢迎来到行业世界,在这里数据并不总是如预期那样呈现。

推荐的选项是查看 CSV 文件。可以在 Jupyter Notebook 中通过定位到本章节的文件夹并点击student-por.csv文件来实现。

您应该看到如下内容:

图 10.2 – 学生表现 CSV 文件

图 10.2 – 学生表现 CSV 文件

如前图所示,数据是通过分号分隔的。CSV 代表pandas,它带有一个sep参数,表示分隔符,可以将其设置为分号(;),如下所示:

df = pd.read_csv('student-por.csv', sep=';')
df.head()

这里是预期的输出:

图 10.3 – 学生表现数据集

图 10.3 – 学生表现数据集

现在,DataFrame 看起来符合预期,包含了分类值和数值的混合数据,我们必须清理空值

清理空值

你可以通过在df.insull()上调用.sum()方法来查看所有包含空值的列。以下是结果的摘录:

df.isnull().sum()
school        0
sex           1
age           1
address       0
…
health        0
absences      0
G1            0
G2            0
G3            0
dtype: int64

你可以通过将df.isna().any(axis=1)放入括号中与df一起使用,来查看这些列的行:

df[df.isna().any(axis=1)]

以下是预期的输出:

图 10.4 – 学生成绩数据集的空数据

图 10.4 – 学生成绩数据集的空数据

最好将空列显示在中间,Jupyter 默认会删除它们,因为列数较多。可以通过将max columns设置为None来轻松修正,如下所示:

pd.options.display.max_columns = None

现在,再次运行代码会显示所有的列:

df[df.isna().any(axis=1)]

以下是预期的输出摘录:

图 10.5 – 学生成绩数据集中所有行的空数据

图 10.5 – 学生成绩数据集中所有行的空数据

如你所见,所有列,包括'guardian'下隐藏的空值,现已显示。

数值型空值可以设置为-999.0,或其他值,XGBoost 将使用missing超参数为你找到最佳替代值,正如在第五章中介绍的那样,XGBoost 揭示

下面是用-999.0填充'age'列的代码:

df['age'].fillna(-999.0)

接下来,类别列可以通过众数进行填充。众数是列中最常见的值。用众数填充类别列可能会扭曲结果分布,但只有在空值数量很大时才会发生。然而,我们只有两个空值,所以我们的分布不会受到影响。另一种选择是将类别空值替换为'unknown'字符串,经过独热编码后,这个字符串可能会变成一个单独的列。需要注意的是,XGBoost 需要数值型输入,因此从 2020 年起,missing超参数不能直接应用于类别列。

以下代码将'sex''guardian'这两列类别数据转换为众数:

df['sex'] = df['sex'].fillna(df['sex'].mode())
df['guardian'] = df['guardian'].fillna(df['guardian'].mode())

由于我们的空值出现在前两行,我们可以通过使用df.head()来显示它们已经被更改:

df.head()

以下是预期的输出:

图 10.6 – 删除空值后的学生成绩数据集(仅显示前五行)

图 10.6 – 删除空值后的学生成绩数据集(仅显示前五行)

空值已经按预期被清除。

接下来,我们将使用独热编码将所有类别列转换为数值列。

独热编码

之前,我们使用pd.get_dummies将所有类别变量转换为01的数值,其中0表示缺失,1表示存在。虽然这种方法是可以接受的,但它也存在一些不足之处。

第一个缺点是pd.get_dummies在计算上可能非常昂贵,就像你在前几章等待代码运行时所发现的那样。第二个缺点是pd.get_dummies在转换到 scikit-learn 的管道时效果不太好,这是我们将在下一节中探讨的概念。

一个不错的pd.get_dummies替代方案是 scikit-learn 的OneHotEncoder。与pd.get_dummies类似,独热编码将所有类别值转换为01,其中0表示缺失,1表示存在,但与pd.get_dummies不同的是,它并不在计算上昂贵。OneHotEncoder使用稀疏矩阵而不是密集矩阵,从而节省空间和时间。

稀疏矩阵通过只存储不包含零的值来节省空间。通过使用更少的位数,保存了相同的信息量。

此外,OneHotEncoder是一个 scikit-learn 的转换器,意味着它是专门设计用来在机器学习管道中使用的。

在以前的 scikit-learn 版本中,OneHotEncoder只接受数值输入。在那时,需要使用LabelEncoder先将所有类别列转换为数值列,作为中间步骤。

要在特定列上使用OneHotEncoder,可以按照以下步骤进行:

  1. 将所有dtype为对象的类别列转换为列表:

    categorical_columns = df.columns[df.dtypes==object].tolist()
    
  2. 导入并初始化OneHotEncoder

    from sklearn.preprocessing import OneHotEncoder
    ohe = OneHotEncoder()
    
  3. 在列上使用fit_transform方法:

    hot = ohe.fit_transform(df[categorical_columns])
    
  4. 01

  5. 如果你想查看hot稀疏矩阵的实际样子,可以按如下方式打印出来:

    print(hot)
    

    这是结果的一个摘录:

      (0, 0)		1.0
      (0, 2)		1.0
      (0, 5)		1.0
      (0, 6)		1.0
      (0, 8)		1.0
    …  
    0 have been skipped. For instance, the 0th row and the 1st column, denoted by (0, 1), has a value of 0.0 in the dense matrix, but it's skipped over in the one-hot matrix.
    

如果你想获得更多关于稀疏矩阵的信息,只需输入以下变量:

hot

结果如下:

<649x43 sparse matrix of type '<class 'numpy.float64'>'
	with 11033 stored elements in Compressed Sparse Row format>

这告诉我们,矩阵是64943列,但只有11033个值被存储,从而节省了大量空间。请注意,对于包含许多零的文本数据,稀疏矩阵是非常常见的。

合并独热编码矩阵和数值列

现在我们拥有一个独热编码的稀疏矩阵,接下来我们需要将它与原始 DataFrame 的数值列合并。

首先,让我们分离数值列。可以使用df.select_dtypesexclude=["object"]参数来选择特定类型的列,方法如下:

cold_df = df.select_dtypes(exclude=["object"])
cold_df.head()

这是预期的输出:

图 10.8 – 学生表现数据集的数值列

图 10.8 – 学生表现数据集的数值列

这些就是我们要查找的列。

对于这种规模的数据,我们可以选择将稀疏矩阵转换为常规的 DataFrame,如前面的截图所示,或者将这个 DataFrame 转换为稀疏矩阵。我们将选择后者,因为行业中的 DataFrame 可能非常庞大,节省空间是有利的:

  1. 要将cold_df DataFrame 转换为压缩稀疏矩阵,请从scipy.sparse导入csr_matrix并将 DataFrame 放入其中,如下所示:

    from scipy.sparse import csr_matrix
    cold = csr_matrix(cold_df)
    
  2. 最后,通过导入并使用hstack,将热矩阵和冷矩阵堆叠起来,hstack将稀疏矩阵水平组合:

    from scipy.sparse import hstack
    final_sparse_matrix = hstack((hot, cold))
    
  3. 通过将稀疏矩阵转换为密集矩阵并像往常一样显示数据框,验证final_sparse_matrix是否按预期工作:

    final_df = pd.DataFrame(final_sparse_matrix.toarray())
    final_df.head()
    

    这是预期的输出:

图 10.9 – 最终稀疏矩阵的数据框

图 10.9 – 最终稀疏矩阵的数据框

输出被向右移动,以显示一热编码和数值列一起呈现。

现在数据已准备好进行机器学习,让我们使用变换器和流水线自动化这个过程。

定制 Scikit-learn 变换器

现在我们有了一个将数据框转换为适合机器学习的稀疏矩阵的过程,将其用变换器进行泛化,以便可以轻松地为新的数据重复这个过程,将会是一个有利的选择。

Scikit-learn 的变换器通过使用fit方法与机器学习算法一起工作,fit方法用于找到模型参数,transform方法则将这些参数应用于数据。这些方法可以合并为一个fit_transform方法,在一行代码中同时完成拟合和变换数据。

当一起使用时,各种变换器,包括机器学习算法,可以在同一流水线中协同工作,便于使用。然后,数据被放入流水线中,进行拟合和变换,以实现期望的输出。

Scikit-learn 提供了许多优秀的变换器,例如StandardScalerNormalizer分别用于标准化和归一化数据,SimpleImputer用于转换空值。然而,当数据包含混合类别和数值列时(如本例所示),你需要小心。在某些情况下,Scikit-learn 提供的选项可能不是自动化的最佳选择。在这种情况下,值得创建你自己的变换器来精确地完成你想要的操作。

定制变换器

创建你自己的变换器的关键是使用 Scikit-learn 的TransformerMixin作为父类。

这是在 Scikit-learn 中创建定制变换器的一般代码框架:

class YourClass(TransformerMixin):
    def __init__(self):
        None
    def fit(self, X, y=None):
        return self
    def transform(self, X, y=None):
        # insert code to transform X
        return X

如你所见,你无需初始化任何内容,fit方法可以始终返回self。简而言之,你可以将所有用于变换数据的代码放在transform方法下。

现在你已经了解了定制化的一般操作方式,让我们创建一个定制的变换器,用来处理不同类型的空值。

定制混合空值填充器

让我们通过创建一个定制的混合空值填充器来看看这个如何工作。这里,定制化的原因是为了处理不同类型的列,并采用不同的方式修正空值。

以下是步骤:

  1. 导入TransformerMixin并定义一个新类,以TransformerMixin作为父类:

    from sklearn.base import TransformerMixin 
    class NullValueImputer(TransformerMixin):
    
  2. 使用self作为输入初始化类。如果这个操作没有实际作用也没关系:

    def __init__(self):
    None
    
  3. 创建一个fit方法,接受selfX作为输入,y=None,并返回self

    def fit(self, X, y=None):
    return self
    
  4. 创建一个transform方法,该方法接受selfX作为输入,y=None,并通过返回一个新的X来转换数据,如下所示:

    def transform(self, X, y=None):
    

    我们需要根据列的不同单独处理空值。

    以下是将空值转换为众数或-999.0的步骤,具体取决于列的类型:

    a) 通过将列转换为列表来循环遍历列:

    for column in X.columns.tolist():
    

    b) 在循环中,通过检查哪些列是object类型来访问字符串列:

        if column in X.columns[X.dtypes==object].tolist():
    

    c) 将字符串(object)列的空值转换为众数:

            X[column] = X[column].fillna(X[column].mode())
    

    d) 否则,将列填充为-999.0

        else:
            X[column]=X[column].fillna(-999.0)
          return X
    

在前面的代码中,你可能会想知道为什么使用了y=None。原因是,当将机器学习算法纳入管道时,y将作为输入使用。通过将y设置为None,只会对预测列进行预期的更改。

现在定制的插补器已经定义完成,可以通过在数据上调用fit_transform方法来使用它。

通过从 CSV 文件创建一个新的 DataFrame 并使用定制的NullValueImputer在一行代码中转换空值,来重置数据:

df = pd.read_csv('student-por.csv', sep=';')
nvi = NullValueImputer().fit_transform(df)
nvi.head()

这里是期望的输出:

图 10.10 – 使用 NullValueImputer()后的学生表现 DataFrame

图 10.10 – 使用 NullValueImputer()后的学生表现 DataFrame

如你所见,所有空值都已清除。

接下来,让我们像之前一样将数据转换为独热编码的稀疏矩阵。

独热编码混合数据

我们将采取与上一节类似的步骤,通过创建一个定制的转换器对分类列进行独热编码,然后将它们与数值列合并为一个稀疏矩阵(对于这个大小的数据集,密集矩阵也可以):

  1. 定义一个新的类,继承TransformerMixin作为父类:

    class SparseMatrix(TransformerMixin):
    
  2. self作为输入初始化类。如果这没有任何作用也没关系:

    def __init__(self):
        		None
    
  3. 创建一个fit方法,该方法接受selfX作为输入并返回self

    def fit(self, X, y=None):
        		return self
    
  4. 创建一个transform方法,该方法接受selfX作为输入,转换数据并返回新的X

    def transform(self, X, y=None):
    

    以下是完成转换的步骤;首先访问仅包含object类型的分类列,如下所示:

    a) 将分类列放入一个列表中:

        		categorical_columns= X.columns[X.dtypes==object].tolist()
    

    b) 初始化OneHotEncoder

        		ohe = OneHotEncoder() 
    

    c) 使用OneHotEncoder转换分类列:

    hot = ohe.fit_transform(X[categorical_columns])
    

    d) 通过排除字符串,创建一个仅包含数值列的 DataFrame:

    cold_df = X.select_dtypes(exclude=["object"])
    

    e) 将数值型 DataFrame 转换为稀疏矩阵:

            	cold = csr_matrix(cold_df)
    

    f) 将两个稀疏矩阵合并为一个:

             final_sparse_matrix = hstack((hot, cold))
    

    g) 将其转换为压缩稀疏行CSR)矩阵,以限制错误。请注意,XGBoost 要求使用 CSR 矩阵,并且根据你的 XGBoost 版本,转换可能会自动发生:

             final_csr_matrix = final_sparse_matrix.tocsr()
             return final_csr_matrix
    
  5. 现在我们可以使用强大的fit_transform方法对没有空值的nvi数据进行转换:

    sm = SparseMatrix().fit_transform(nvi)
    print(sm)
    

    这里给出的期望输出已被截断以节省空间:

      (0, 0)	1.0
      (0, 2)	1.0
      (0, 5)	1.0
      (0, 6)	1.0
      (0, 8)	1.0
      (0, 10)	1.0
      :	:
      (648, 53)	4.0
      (648, 54)	5.0
      (648, 55)	4.0
      (648, 56)	10.0
      (648, 57)	11.0
      (648, 58)	11.0
    
  6. 你可以通过将稀疏矩阵转换回密集矩阵来验证数据是否符合预期,方法如下:

    sm_df = pd.DataFrame(sm.toarray())
    sm_df.head()
    

    以下是预期的密集输出:

图 10.11 – 稀疏矩阵转换为密集矩阵

图 10.11 – 稀疏矩阵转换为密集矩阵

这看起来是正确的。图中显示第 27 列的值为0.0,第 28 列的值为1.0。前面的独热编码输出排除了(027),并显示(028)的值为1.0,与密集输出匹配。

现在数据已经转换,让我们将两个预处理步骤合并到一个单一的管道中。

预处理管道

在构建机器学习模型时,通常会先将数据分为Xy。在考虑管道时,合理的做法是转换X(预测列),而不是y(目标列)。此外,重要的是保留一个测试集以供后用。

在将数据放入机器学习管道之前,让我们将数据拆分为训练集和测试集,并保留测试集。我们按以下方式从头开始:

  1. 首先将 CSV 文件读取为一个 DataFrame:

    df = pd.read_csv('student-por.csv', sep=';')
    

    在为学生表现数据集选择Xy时,需要注意的是,最后三列都包含学生成绩。这里有两个潜在的研究方向值得关注:

    a) 包括之前的成绩作为预测列

    b) 不包括之前的成绩作为预测列

    假设你的 EdTech 公司希望基于社会经济变量进行预测,而不是基于以前获得的成绩,因此忽略前两列成绩,索引为-2-3

  2. 选择最后一列作为y,并将最后三列以外的所有列作为X

    y = df.iloc[:, -1]
    X = df.iloc[:, :-3]
    
  3. 现在导入train_test_split,并将Xy拆分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
    

现在让我们按照以下步骤构建管道:

  1. 首先从sklearn.pipeline导入Pipeline

    from sklearn.pipeline import Pipeline
    
  2. 接下来,使用语法(name,transformer)将元组分配为Pipeline中的参数,按顺序排列:

    data_pipeline = Pipeline([('null_imputer', NullValueImputer()), ('sparse', SparseMatrix())])
    
  3. 最后,通过将X_train放入data_pipelinefit_transform方法中来转换我们的预测列X_train

    X_train_transformed = data_pipeline.fit_transform(X_train)
    

现在你有了一个数值稀疏矩阵,没有空值,可以作为机器学习的预测列。

此外,你有一个管道,可以通过一行代码转换任何传入的数据!现在让我们完成一个 XGBoost 模型来进行预测。

完成 XGBoost 模型的最终调整

现在是时候构建一个稳健的 XGBoost 模型并将其添加到管道中。请按以下方式导入XGBRegressornumpyGridSearchCVcross_val_scoreKFoldmean_squared_error

import numpy as np
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score, KFold
from sklearn.metrics import mean_squared_error as MSE
from xgboost import XGBRegressor

现在让我们构建模型。

第一个 XGBoost 模型

这个学生表现数据集在预测列y_train方面有一个有趣的值范围,可以如下所示:

y_train.value_counts()

结果如下:

11    82
10    75
13    58
12    53
14    42
15    36
9     29
16    27
8     26
17    24
18    14
0     10
7      7
19     1
6      1
5      1

如你所见,数值范围从519,并且包括0

由于目标列是有序的,意味着其值是按数字顺序排列的,因此回归方法比分类方法更为合适,尽管输出有限。在通过回归训练模型后,最终结果可能会进行四舍五入,以给出最终预测。

下面是对 XGBRegressor 使用此数据集进行评分的步骤:

  1. 从设置使用 KFold 的交叉验证开始:

    kfold = KFold(n_splits=5, shuffle=True, random_state=2)
    
  2. 现在定义一个交叉验证函数,该函数返回 cross_val_score

    def cross_val(model):
        scores = cross_val_score(model, X_train_transformed, y_train, scoring='neg_root_mean_squared_error', cv=kfold)
        rmse = (-scores.mean())
        return rmse
    
  3. 通过调用 cross_valXGBRegressor 为输入并将 missing=-999.0,为 XGBoost 寻找最佳替代值,建立一个基准分数:

    cross_val(XGBRegressor(missing=-999.0))
    

    分数如下:

    2.9702248207546296
    

这是一个值得尊敬的起始分数。2.97 的均方根误差,基于 19 种可能性,表示分数的准确性相差不到几个点。这几乎是 15%,在美国 A-B-C-D-F 系统中准确到一个字母等级。在工业界,您甚至可以使用统计学方法来包括置信区间,提供预测区间,这是本书范围之外的推荐策略。

现在您已有了基准分数,让我们微调超参数以改进模型。

微调 XGBoost 超参数

我们从检查带有提前停止的 n_estimators 开始。回忆一下,为了使用提前停止,我们可能会检查一个测试折叠。创建测试折叠需要进一步划分 X_trainy_train

  1. 这是第二个 train_test_split,可用于创建一个测试集进行验证,同时确保将真实的测试集保留到后期:

    X_train_2, X_test_2, y_train_2, y_test_2 = train_test_split(X_train_transformed, y_train, random_state=2)
    
  2. 现在定义一个函数,使用提前停止来返回回归器的最佳估计器数量(参见 第六章,XGBoost 超参数):

    def n_estimators(model):
        eval_set = [(X_test_2, y_test_2)]
        eval_metric="rmse"
        model.fit(X_train_2, y_train_2, eval_metric=eval_metric, eval_set=eval_set, early_stopping_rounds=100)
        y_pred = model.predict(X_test_2)
        rmse = MSE(y_test_2, y_pred)**0.5
        return rmse  
    
  3. 现在运行 n_estimators 函数,将最大值设置为 5000

    n_estimators(XGBRegressor(n_estimators=5000, missing=-999.0))
    

    下面是输出的最后五行:

    [128]	validation_0-rmse:3.10450
    [129]	validation_0-rmse:3.10450
    [130]	validation_0-rmse:3.10450
    [131]	validation_0-rmse:3.10450
    Stopping. Best iteration:
    [31]	validation_0-rmse:3.09336
    

    分数如下:

    3.0933612343143153
    

使用我们默认的模型,31 个估计器目前给出最佳估算值。这将是我们的起点。

接下来,这是一个我们多次使用的 grid_search 函数,它搜索超参数网格并显示最佳参数和最佳分数:

def grid_search(params, reg=XGBRegressor(missing=-999.0)):
    grid_reg = GridSearchCV(reg, params, scoring='neg_mean_squared_error', cv=kfold)
    grid_reg.fit(X_train_transformed, y_train)
    best_params = grid_reg.best_params_
    print("Best params:", best_params)
    best_score = np.sqrt(-grid_reg.best_score_)
    print("Best score:", best_score)

以下是一些推荐的模型微调步骤:

  1. 从将 max_depth 范围设置为 18 开始,同时将 n_estimators 设置为 31

    grid_search(params={'max_depth':[1, 2, 3, 4, 6, 7, 8], 
                        'n_estimators':[31]})
    

    结果如下:

    Best params: {'max_depth': 1, 'n_estimators': 31}
    Best score: 2.6634430373079425
    
  2. max_depth1 缩小到 3,同时将 min_child_weight 范围设定为 15,并保持 n_estimators31

    grid_search(params={'max_depth':[1, 2, 3], 
                        'min_child_weight':[1,2,3,4,5], 
                        'n_estimators':[31]})
    

    结果如下:

    Best params: {'max_depth': 1, 'min_child_weight': 1, 'n_estimators': 31}
    Best score: 2.6634430373079425
    

    没有改进。

  3. 通过强制将 min_child_weight 设置为 23,同时包含 subsample 范围从 0.50.9,你可能可以保证一些变化。此外,增加 n_estimators 可能有助于为模型提供更多学习时间:

    grid_search(params={'max_depth':[2],
                        'min_child_weight':[2,3],
                        'subsample':[0.5, 0.6, 0.7, 0.8, 0.9],
                       'n_estimators':[31, 50]})
    

    结果如下:

    Best params: {'max_depth': 1, 'min_child_weight': 2, 'n_estimators': 50, 'subsample': 0.9}
    Best score: 2.665209161229433
    

    分数几乎相同,但略有下降。

  4. 缩小 min_child_weightsubsample 的范围,同时将 colsample_bytree 设置为 0.50.9 的范围:

    grid_search(params={'max_depth':[1],
                        'min_child_weight':[1, 2, 3], 
                        'subsample':[0.6, 0.7, 0.8], 
                        'colsample_bytree':[0.5, 0.6, 0.7, 0.8, 0.9, 1],
                       'n_estimators':[50]})
    

    结果如下:

    Best params: {'colsample_bytree': 0.9, 'max_depth': 1, 'min_child_weight': 3, 'n_estimators': 50, 'subsample': 0.8}
    Best score: 2.659649642579931
    

    这是目前为止最好的分数。

  5. 保持当前最优值,尝试colsample_bynodecolsample_bylevel的范围从0.61.0

     grid_search(params={'max_depth':[1],
                        'min_child_weight':[3], 
                        'subsample':[.8], 
                        'colsample_bytree':[0.9],
                        'colsample_bylevel':[0.6, 0.7, 0.8, 0.9, 1],
                        'colsample_bynode':[0.6, 0.7, 0.8, 0.9, 1],
                        'n_estimators':[50]})
    

    结果如下:

    Best params: {'colsample_bylevel': 0.9, 'colsample_bynode': 0.8, 'colsample_bytree': 0.9, 'max_depth': 1, 'min_child_weight': 3, 'n_estimators': 50, 'subsample': 0.8}
    Best score: 2.64172735526102
    

    得分再次提高。

进一步尝试使用基础学习器dartgamma,但未获得新收益。

根据项目的时间和范围,可能值得进一步调整超参数,甚至将它们一起放入RandomizedSearch中尝试。在工业界,你很有可能可以访问到云计算,在这里,廉价的、可抢占的虚拟机VMs)将允许更多的超参数搜索,以找到更好的结果。只需要注意的是,scikit-learn 目前没有提供一种方法来停止耗时的搜索,以便在代码完成之前保存最佳参数。

现在我们有了一个健壮的模型,可以继续前进并测试该模型。

测试模型

现在你有了一个潜在的最终模型,重要的是在测试集上对它进行测试。

记住,测试集在我们的管道中并没有进行转化。幸运的是,此时只需要一行代码即可将其转化:

X_test_transformed = data_pipeline.fit_transform(X_test)

现在我们可以使用之前部分中选择的最佳超参数初始化一个模型,拟合训练集,并在保留的测试集上进行测试:

model = XGBRegressor(max_depth=2, min_child_weight=3, subsample=0.9, colsample_bytree=0.8, gamma=2, missing=-999.0)
model.fit(X_train_transformed, y_train)
y_pred = model.predict(X_test_transformed)
rmse = MSE(y_pred, y_test)**0.5
rmse

得分如下:

2.7908972630881435

得分稍微高一些,尽管这可能是由于折叠的原因。

如果没有的话,我们的模型可能在验证集上拟合得过于紧密,这在微调超参数并将其精确调整以改进验证集时是可能发生的。模型的泛化能力还不错,但它可以有更好的泛化能力。

对于接下来的步骤,当考虑是否可以改进得分时,以下选项是可用的:

  • 返回超参数微调。

  • 保持模型不变。

  • 根据超参数知识进行快速调整。

快速调整超参数是可行的,因为模型可能会过拟合。例如,增加min_child_weight并降低subsample应该有助于模型更好地泛化。

让我们进行最终的调整,得到一个最终模型:

model = XGBRegressor(max_depth=1,
                       min_child_weight=5,
                       subsample=0.6, 
                       colsample_bytree=0.9, 
                       colsample_bylevel=0.9,
                       colsample_bynode=0.8,
                     n_estimators=50,
                       missing=-999.0)
model.fit(X_train_transformed, y_train)
y_pred = model.predict(X_test_transformed)
rmse = MSE(y_pred, y_test)**0.5
rmse

结果如下:

2.730601403138633

请注意,得分已经提高。

此外,你绝对不应该反复尝试提高验证集得分。在收到测试得分后进行少许调整是可以接受的;否则,你永远无法在第一次结果的基础上进行改进。

现在剩下的就是完成管道。

构建机器学习管道

完成机器学习管道需要将机器学习模型添加到之前的管道中。你需要在NullValueImputerSparseMatrix之后,得到一个机器学习元组,如下所示:

full_pipeline = Pipeline([('null_imputer', NullValueImputer()),  ('sparse', SparseMatrix()), 
('xgb', XGBRegressor(max_depth=1, min_child_weight=5, subsample=0.6, colsample_bytree=0.9, colsample_bylevel=0.9, colsample_bynode=0.8, missing=-999.0))]) 

这个管道现在已经完成了机器学习模型,并且它可以在任何Xy组合上进行拟合,如下所示:

full_pipeline.fit(X, y)

现在你可以对任何目标列未知的数据进行预测:

new_data = X_test
full_pipeline.predict(new_data)

这是预期输出的前几行:

array([13.55908  ,  8.314051 , 11.078157 , 14.114085 , 12.2938385, 11.374797 , 13.9611025, 12.025812 , 10.80344  , 13.479145 , 13.02319  ,  9.428679 , 12.57761  , 12.405045 , 14.284043 , 8.549758 , 10.158956 ,  9.972576 , 15.502667 , 10.280028 , ...

为了得到更真实的预测,数据可能需要按如下方式四舍五入:

np.round(full_pipeline.predict(new_data))

期望的输出如下:

array([14.,  8., 11., 14., 12., 11., 14., 12., 11., 13., 13.,  9., 13., 12., 14.,  9., 10., 10., 16., 10., 13., 13.,  7., 12.,  7.,  8., 10., 13., 14., 12., 11., 12., 15.,  9., 11., 13., 12., 11.,  8.,
...
11., 13., 12., 13.,  9., 13., 10., 14., 12., 15., 15., 11., 14., 10., 14.,  9.,  9., 12., 13.,  9., 11., 14., 13., 11., 13., 13., 13., 13., 11., 13., 14., 15., 13.,  9., 10., 13.,  8.,  8., 12., 15., 14., 13., 10., 12., 13.,  9.], dtype=float32)

最后,如果新数据到达,可以将其与之前的数据连接,并通过相同的管道处理,从而得到一个更强大的模型,因为新的模型可能会基于更多的数据进行训练,如下所示:

new_df = pd.read_csv('student-por.csv')
new_X = df.iloc[:, :-3]
new_y = df.iloc[:, -1]
new_model = full_pipeline.fit(new_X, new_y)

现在,这个模型可以用于对新数据进行预测,如以下代码所示:

more_new_data = X_test[:25]
np.round(new_model.predict(more_new_data))

期望的输出如下:

array([14.,  8., 11., 14., 12., 11., 14., 12., 11., 13., 13.,  9., 13., 12., 14.,  9., 10., 10., 16., 10., 13., 13.,  7., 12.,  7.],
      dtype=float32)

有一个小小的细节。

如果你只想对一行数据进行预测怎么办?如果你将单行数据通过管道处理,生成的稀疏矩阵将没有正确的列数,因为它只会对单行数据中存在的类别进行独热编码。这将导致数据中的不匹配错误,因为机器学习模型已经适配了一个需要更多数据行的稀疏矩阵。

一个简单的解决方法是将新的一行数据与足够多的行数据连接起来,以确保生成的稀疏矩阵包含所有可能的类别列并进行转换。我们已经看到,这样操作使用X_test中的 25 行数据是有效的,因为没有出现错误。使用X_test中的 20 行或更少的数据将导致在这种情况下出现不匹配错误。

因此,如果你想用单行数据进行预测,可以将这一行数据与X_test的前25行连接,并按如下方式进行预测:

single_row = X_test[:1]
single_row_plus = pd.concat([single_row, X_test[:25]])
print(np.round(new_model.predict(single_row_plus))[:1])

结果是这样的:

[14.]

你现在知道如何将机器学习模型纳入管道,以对新数据进行转换和预测。

总结

恭喜你完成了本书!这是一次非凡的旅程,从基础的机器学习和pandas开始,到最终构建自己定制的转换器、管道和函数,以便在工业场景中使用稀疏矩阵部署强大、精细调优的 XGBoost 模型进行新数据预测。

在此过程中,你了解了 XGBoost 的故事,从最初的决策树到随机森林和梯度提升,再到发现使 XGBoost 如此特别的数学细节和复杂性。你一次又一次地看到 XGBoost 超越了其他机器学习算法,并且你获得了调优 XGBoost 广泛超参数的实践经验,包括n_estimatorsmax_depthgammacolsample_bylevelmissingscale_pos_weight

你了解了物理学家和天文学家如何通过历史上重要的案例研究获得关于我们宇宙的知识,并且通过不平衡数据集和替代基本学习器的应用,深入了解了 XGBoost 的广泛范围。你甚至通过先进的特征工程、非相关集成和堆叠等技巧,从 Kaggle 竞赛中学到了行业技巧。最后,你了解了工业中的高级自动化过程。

此时,你对 XGBoost 的知识已经达到高级水平。你现在可以高效、迅速、强大地使用 XGBoost 来解决你将面对的机器学习问题。当然,XGBoost 并不完美。如果你处理的是非结构化数据,比如图像或文本,神经网络可能会更适合你。对于大多数机器学习任务,尤其是表格数据的任务,XGBoost 通常会给你带来优势。

如果你有兴趣继续深入研究 XGBoost,我个人的建议是参加 Kaggle 竞赛。原因是 Kaggle 竞赛汇聚了经验丰富的机器学习从业者,与他们竞争会让你变得更强。此外,Kaggle 竞赛提供了一个结构化的机器学习环境,许多从业者都在同一个问题上进行工作,这导致了共享笔记本和论坛讨论,进一步促进了学习过程。这也是 XGBoost 在希格斯玻色子竞赛中首次建立起非凡声誉的地方,如本书所述。

你现在可以自信地使用 XGBoost 进入大数据领域,推动研究、参加竞赛,并构建适合生产的机器学习模型。

posted @ 2025-07-14 17:27  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报