原文:
annas-archive.org/md5/8042b1d609c03cc86db1c68794ab294c
译者:飞龙
前言
XGBoost 是一个行业验证的开源软件库,提供一个梯度提升框架,能够快速高效地扩展数十亿个数据点。
本书介绍了机器学习和 XGBoost 在 scikit-learn 中的应用,先通过线性回归和逻辑回归引入,随后讲解梯度提升背后的理论。你将学习决策树,并在机器学习的背景下分析集成方法,学习扩展到 XGBoost 的超参数。在此过程中,你将从零开始构建梯度提升模型,并将其扩展到大数据中,同时认识到使用计时器时的速度限制。XGBoost 中的细节将着重于速度提升以及数学推导参数。在详细的案例研究帮助下,你将练习使用 scikit-learn 和原生 Python API 构建并微调 XGBoost 分类器和回归器。你将利用 XGBoost 超参数来提升得分、修正缺失值、调整不平衡数据集,并微调其他基础学习器。最后,你将应用先进的 XGBoost 技术,如构建非相关集成、堆叠模型,并使用稀疏矩阵、自定义转换器和管道准备模型以便于行业部署。
本书结束时,你将能够使用 XGBoost 构建高效的机器学习模型,最大限度地减少错误并提高速度。
适合人群
本书面向数据科学专业人员和爱好者、数据分析师以及希望构建快速且准确的机器学习模型并能够应对大数据的开发人员。掌握 Python 编程语言并对线性代数有基本了解,将帮助你最大限度地提高本书的学习效果。
本书涵盖内容
第一章,机器学习全景,通过介绍线性回归和逻辑回归,将 XGBoost 放入机器学习的一般背景中,随后将其与 XGBoost 进行对比。pandas
被引入用于预处理机器学习的原始数据,方法包括转换分类列和以多种方式清理空值。
第二章,深入探讨决策树,详细介绍了 XGBoost 使用的决策树超参数,并通过图形和统计分析,探讨了方差和偏差的分析,强调了过拟合的重要性,这是整本书贯穿的主题。
第三章,使用随机森林的集成方法,概述了随机森林作为 XGBoost 的竞争者,重点讲解了集成方法。与随机森林共享的 XGBoost 超参数,如 n_estimators
和 subsample
,也得到了充分的讲解。
第四章,从梯度提升到 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 所有的提升器,包括gbtree
、dart
和gblinear
,用于回归和分类。将随机森林作为基础学习器,作为 XGBoost 的替代模型,并介绍了新的XGBRFRegressor
和XGBRFClassifier
类。
第九章,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 的步骤:
-
点击下图中的 下载,此时尚未开始下载,但会为您提供多个选项(参见第 3 步):
图 0.1 – 准备下载 Anaconda
-
选择您的安装程序。推荐使用适用于 Windows 和 Mac 的
64 位图形安装程序
。请确保您选择的是 Python 3.7 下的前两行,因为本书中使用的都是 Python 3.7:图 0.2 – Anaconda 安装程序
-
下载开始后,请按照计算机上的提示继续,以完成安装:
Mac 用户警告
如果遇到错误 无法在此位置安装 Anaconda3,请不要惊慌。只需点击高亮显示的 仅为我安装,然后 继续 按钮将会显示作为一个选项。
图 0.3 – Mac 用户警告 – 只需点击“仅为我安装”,然后点击“继续”
使用 Jupyter Notebook
现在您已经安装了 Anaconda,可以打开 Jupyter Notebook 使用 Python 3.7。以下是打开 Jupyter Notebook 的步骤:
-
点击您计算机上的 Anaconda-Navigator。
-
点击 启动 下的 Jupyter Notebook,如下图所示:
图 0.4 – Anaconda 主屏幕
这应该会在浏览器窗口中打开一个 Jupyter Notebook。虽然 Jupyter Notebook 为了方便在网页浏览器中显示,但它们实际运行在您的个人计算机上,而非在线。Google Colab Notebook 是一个可以接受的在线替代方案,但本书中仅使用 Jupyter Notebook。
-
从 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 到你电脑上的步骤:
-
访问
anaconda.org/conda-forge/xgboost
。你应该看到以下内容:图 0.7 – Anaconda 安装 XGBoost 的推荐方法
-
复制上面截图中显示的第一行代码,如下所示:
图 0.8 – 包安装
-
打开你电脑上的终端(Terminal)。
如果你不知道终端在哪里,可以在 Mac 上搜索
Terminal
,在 Windows 上搜索Windows Terminal
。 -
将以下代码粘贴到你的终端中,按 Enter,并按照提示操作:
conda install -c conda-forge xgboost
-
通过打开一个新的 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 账号。以下是一个示例:“AdaBoostRegressor
和 AdaBoostClassifier
算法可以从 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
)调整,添加了空值,以便你可以练习如何修正这些空值。
访问数据
数据清洗的第一步是访问数据。可以通过以下步骤实现:
-
下载数据。所有本书的文件都存储在 GitHub 上。你可以通过点击桌面上的
Data
文件夹,将所有文件下载到本地计算机。 -
打开 Jupyter Notebook。您可以在前言中找到下载 Jupyter Notebook 的链接。在终端中点击
jupyter notebook
。网页浏览器打开后,您应该看到一列文件夹和文件。进入与自行车租赁数据集相同的文件夹,选择 New: Notebook: Python 3。这里有一个视觉指南:图 1.2 – 访问 Jupyter Notebook 的视觉指南
小贴士
如果您在打开 Jupyter Notebook 时遇到困难,请参阅 Jupyter 的官方故障排除指南:
jupyter-notebook.readthedocs.io/en/stable/troubleshooting.html
。 -
在 Jupyter Notebook 的第一个单元格中输入以下代码:
import pandas as pd
按下 Shift + Enter 运行单元格。现在,当你输入
pd
时,你可以访问pandas
库了。 -
使用
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 补全可以使您的编码体验更快速、更可靠。
-
使用
.head()
显示数据。最后一步是查看数据以确保正确加载。.head()
是一个显示 DataFrame 前五行的方法。您可以在括号中放入任何正整数以查看任意数量的行。输入以下代码并按 Shift + Enter:df_bikes.head()
这里是前几行的屏幕截图以及预期的输出:
图 1.3 – bike_rental.csv
输出
现在我们可以访问数据,让我们看看三种理解数据的方法。
理解数据
现在数据已加载,是时候理解数据了。理解数据对于未来做出明智决策至关重要。以下是三种理解数据的好方法。
.head()
您已经看到了 .head()
,这是一个广泛使用的方法,用于解释列名和编号。如前面的输出所示,dteday
是日期,而 instant
是有序索引。
.describe()
可以使用 .describe()
查看数值统计信息,如下所示:
df_bikes.describe()
这是预期的输出:
图 1.4 – .describe()
输出
你可能需要向右滚动才能查看所有列。
比较均值和中位数(50%)可以指示数据的偏斜程度。正如你所看到的,mean
和 median
相近,所以数据大致对称。每列的 max
和 min
值,以及四分位数和标准差(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 – 自行车租赁数据集的空值
从输出中可以看出,windspeed
、humidity
和 temperature
列以及最后一行都存在空值。
提示
如果这是你第一次使用 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
列为空值的行 56
和 81
。可以使用.iloc
显示这些行,iloc
是索引位置的缩写:
df_bikes.iloc[[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 输出
如你所见,列中的值为中位数。
要修正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’ 列的输出
如你所见,索引701
包含空值。
要找到 701
索引前一天和后一天的平均温度,完成以下步骤:
-
将第
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
-
替换空值:
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 – 外推日期值的输出
如你所见,月份值都是正确的,但年份值需要更改。
‘dteday
’ 列中最后五行的年份都是 2012
,但由 ‘yr
’ 列提供的对应年份是 1.0
。为什么?
数据已被归一化,这意味着它已转换为介于 0
和 1
之间的值。
归一化数据通常更高效,因为机器学习权重不需要调整不同范围的值。
你可以使用 .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_split
和 LinearRegression
,如下所示:
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')
现在是时候构建你的第一个模型了。
构建线性回归模型
线性回归模型可以通过以下步骤构建:
-
初始化机器学习模型:
lin_reg = LinearRegression()
-
在训练集上拟合模型。这是机器学习模型构建的地方。请注意,
X_train
是预测变量列,y_train
是目标变量列。lin_reg.fit(X_train, y_train)
-
对测试集进行预测。
X_test
(测试集中的预测变量列)的预测结果使用.predict
方法通过lin_reg
存储为y_pred
:y_pred = lin_reg.predict(X_test)
-
将预测结果与测试集进行比较。对模型进行评分需要一个比较基准。线性回归的标准是
mean_squared_error
,即预测值与实际值之间差异的平方和,再取平方根,以保持单位一致。可以导入mean_squared_error
,并使用 Numerical Python,即 NumPy,一个为与 pandas 一起工作而设计的高速库,来计算平方根。 -
导入
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)
-
打印你的结果:
print("RMSE: %0.2f" % (rmse))
结果如下:
RMSE: 898.21
这是构建你的第一个机器学习模型的所有代码的截图:
图 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
预测的值范围从 22
到 8714
,均值为 4504
,标准差为 1937
,RMSE 为 898
,虽然不差,但也不能说很好。
XGBoost
线性回归是解决回归问题的众多算法之一。其他回归算法可能会产生更好的结果。一般的策略是尝试不同的回归器进行比较。你将在本书中尝试多种回归器,包括决策树、随机森林、梯度提升,以及本书的重点,XGBoost。
本书后续将提供 XGBoost 的全面介绍。现在请注意,XGBoost 包括一个回归器,名为 XGBRegressor
,可以用于任何回归数据集,包括刚才评分的自行车租赁数据集。现在我们将使用 XGBRegressor
来将自行车租赁数据集的结果与线性回归进行比较。
你应该已经在前面安装了 XGBoost。如果没有,请现在安装 XGBoost。
XGBRegressor
安装 XGBoost 后,可以按如下方式导入 XGBoost 回归器:
from xgboost import XGBRegressor
构建 XGBRegressor
的一般步骤与构建 LinearRegression
的步骤相同,唯一的区别是初始化 XGBRegressor
而不是 LinearRegression
:
-
初始化一个机器学习模型:
xg_reg = XGBRegressor()
-
在训练集上拟合模型。如果此时 XGBoost 给出一些警告,请不用担心:
xg_reg.fit(X_train, y_train)
-
对测试集进行预测:
y_pred = xg_reg.predict(X_test)
-
将预测结果与测试集进行比较:
mse = mean_squared_error(y_test, y_pred) rmse = np.sqrt(mse)
-
打印你的结果:
print("RMSE: %0.2f" % (rmse))
输出如下:
RMSE: 705.11
XGBRegressor
表现明显更好!
XGBoost 为什么通常比其他方法表现更好将在第五章中探讨,书名为 XGBoost 揭秘。
交叉验证
一个测试分数是不可靠的,因为将数据拆分为不同的训练集和测试集会得到不同的结果。实际上,将数据拆分为训练集和测试集是任意的,不同的random_state
会得到不同的 RMSE。
解决不同分割之间评分差异的一种方法是k 折交叉验证。其思路是将数据多次拆分为不同的训练集和测试集,然后取这些评分的均值。分割次数,称为折叠,由k表示。标准做法是使用 k = 3、4、5 或 10 个分割。
下面是交叉验证的可视化描述:
图 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
现在,使用交叉验证按以下步骤构建和评分机器学习模型:
-
初始化一个机器学习模型:
model = LinearRegression()
-
使用
cross_val_score
实现模型、X
、y
、scoring='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)
来补偿这一点,因此最终结果是正数。 -
通过取负评分的平方根来找到 RMSE:
rmse = np.sqrt(-scores)
-
显示结果:
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
进行交叉验证。 步骤相同,只是初始化模型不同:
-
初始化机器学习模型:
model = XGBRegressor()
-
使用模型
X
、y
、评分和折数cv
实现cross_val_score
:scores = cross_val_score(model, X, y, scoring='neg_mean_squared_error', cv=10)
-
通过取负分数的平方根来查找 RMSE:
rmse = np.sqrt(-scores)
-
打印结果:
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 – 人口普查收入数据集
输出显示,列标题代表第一行的条目。 当发生这种情况时,可以使用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 – 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 – 预期的列名
如你所见,列名已经恢复。
空值
检查空值的好方法是查看数据框的.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 – "书籍类型"数据框
这是应用了pd.get_dummies
后的相同数据框:
图 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 – 非数值列转换为数值列
如你所见,新的列是通过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 函数图
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 美元,使用的是来自第一章的《机器学习领域》中的人口普查数据集:
-
首先,打开一个新的 Jupyter Notebook,并从以下导入开始:
import pandas as pd import numpy as np import warnings warnings.filterwarnings('ignore')
-
接下来,打开文件
'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')
-
将数据上传到 DataFrame 后,声明预测变量和目标列
X
和y
,如下所示:X = df_census.iloc[:,:-1] y = df_census.iloc[:,-1]
-
接下来,导入
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%。
首先,导入DecisionTreeClassifier
和accuracy_score
:
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
接下来,我们按照标准步骤构建一个决策树分类器:
-
初始化一个机器学习模型,设置
random_state=2
以确保结果一致:clf = DecisionTreeClassifier(random_state=2)
-
在训练集上拟合模型:
clf.fit(X_train, y_train)
-
对测试集进行预测:
y_pred = clf.predict(X_test)
-
将预测结果与测试集进行比较:
accuracy_score(y_pred, y_test)
accuracy_score
如下:0.8131679154894976
81%的准确率与同一数据集中的逻辑回归模型在第一章中的准确率相当,机器学习领域。
现在你已经看到如何构建决策树,接下来让我们深入看看决策树内部。
决策树内部
决策树具有很好的可视化效果,可以揭示其内部工作原理。
这是一个来自人口普查数据集的决策树,只有两个分裂:
图 2.1 – 人口普查数据集决策树
树的顶部是根节点,True/False箭头是分支,数据点是节点。在树的末端,节点被归类为叶子节点。让我们深入研究前面的图示。
根节点
树的根节点位于顶部。第一行显示 marital-status_Married-civ-spouse <=5。marital-status 是一个二值列,因此所有值要么是 0(负类),要么是 1(正类)。第一次分割是基于一个人是否已婚。树的左侧是True分支,表示用户未婚,右侧是False分支,表示用户已婚。
Gini 系数
根节点的第二行显示:Gini
系数为 0 意味着没有错误。Gini 系数为 1 意味着所有的预测都是错误的。Gini 系数为 0.5 表示元素均匀分布,这意味着预测效果不比随机猜测好。越接近 0,错误率越低。在根节点处,Gini 系数为 0.364,意味着训练集不平衡,类别 1 占比 36.4%。
Gini 系数的公式如下:
图 2.2 – Gini 系数公式
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_02_002.png 是分割结果为正确值的概率,c 是类别的总数:在前面的例子中是 2。另一种解释是,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_02_003.png 是集合中具有正确输出标签的项的比例。
样本、值、类别
树的根节点显示总共有 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.4 – 使用线性回归最小化距离
直线通常具有较高的偏差。在机器学习中,偏差是一个数学术语,表示在将模型应用于现实问题时对误差的估计。直线的偏差较高,因为预测结果仅限于直线,未能考虑数据的变化。
在许多情况下,直线的复杂度不足以进行准确的预测。当发生这种情况时,我们说机器学习模型存在高偏差的欠拟合。
第二个选项是使用八次多项式拟合这些点。由于只有九个点,八次多项式将完美拟合这些数据,正如你在以下图形中看到的那样:
图 2.5 – 八次多项式
这个模型具有较高的方差。在机器学习中,方差是一个数学术语,表示给定不同的训练数据集时,模型变化的程度。严格来说,方差是随机变量与其均值之间的平方偏差的度量。由于训练集中有九个不同的数据点,八次多项式将会完全不同,从而导致高方差。
高方差的模型通常会过拟合数据。这些模型无法很好地推广到新的数据点,因为它们过于贴合训练数据。
在大数据的世界里,过拟合是一个大问题。更多的数据导致更大的训练集,而像决策树这样的机器学习模型会过度拟合训练数据。
作为最终选项,考虑使用三次多项式拟合数据点,如下图所示:
图 2.6 – 三次多项式
这个三次多项式在方差和偏差之间提供了良好的平衡,一般跟随曲线,但又能适应变化。低方差意味着不同的训练集不会导致曲线发生显著变化。低偏差表示在将该模型应用于实际问题时,误差不会太高。在机器学习中,低方差和低偏差的结合是理想的。
达到方差和偏差之间良好平衡的最佳机器学习策略之一是微调超参数。
调整决策树的超参数
超参数与参数不同。
在机器学习中,参数是在模型调整过程中进行调整的。例如,线性回归和逻辑回归中的权重就是在构建阶段调整的参数,目的是最小化误差。相比之下,超参数是在构建阶段之前选择的。如果没有选择超参数,则使用默认值。
决策树回归器
学习超参数的最佳方式是通过实验。虽然选择超参数范围背后有理论依据,但结果胜过理论。不同的数据集在不同的超参数值下会有不同的改进效果。
在选择超参数之前,让我们先使用DecisionTreeRegressor
和cross_val_score
找到一个基准分数,步骤如下:
-
下载
'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]
-
导入
DecisionTreeRegressor
和cross_val_score
:from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import cross_val_score
-
初始化
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)
-
计算
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
)。
注意
sklearn是scikit-learn的缩写。
图 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
的一系列值需要经过与之前相同的过程。我们写一个函数,使用 GridSearchCV
和 DecisionTreeRegressor(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_leaf
和 max_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_leaf
从 8
降低到 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
。
对于分类器,之前提到的gini
和entropy
通常会给出相似的结果。
min_impurity_decrease
以前被称为min_impurity_split
,min_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_depth
、max_features
、min_samples_leaf
、max_leaf_nodes
、min_impurity_decrease
和 min_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 输出
目标列,便捷地标记为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_score
和DecisionTreeClassifier
如下:
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
作为一个很好的替代方案。RandomizedSearchCV
与GridSearchCV
的工作方式相同,但它并不是尝试所有的超参数,而是尝试一个随机数量的组合。它并不是要穷举所有组合,而是要在有限的时间内找到最佳的组合。
这是一个使用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
作为基准,我们可以将范围缩小到7
到9
。
另一种策略是停止检查那些默认值已经表现良好的超参数。例如,entropy
相较于'gini'
的差异非常小,因此不推荐使用entropy
。min_impurity_split
和min_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 的道路上迈出了重要一步。你通过GridSearchCV
和RandomizedSearchCV
微调超参数,构建了决策树回归器和分类器。你还可视化了决策树,并从方差和偏差的角度分析了它们的误差和准确性。此外,你还学会了一个不可或缺的工具——feature_importances_
,它是 XGBoost 的一个属性,用于传达模型中最重要的特征。
在下一章中,你将学习如何构建随机森林,这是我们第一个集成方法,也是 XGBoost 的竞争对手。随机森林的应用对于理解 bagging 和 boosting 的区别、生成与 XGBoost 相当的机器学习模型以及了解随机森林的局限性至关重要,正是这些局限性促成了 XGBoost 的发展。
第三章:第三章:使用随机森林进行 Bagging
在本章中,你将掌握构建随机森林的技巧,随机森林是与 XGBoost 竞争的领先方法。与 XGBoost 一样,随机森林也是决策树的集成体。不同之处在于,随机森林通过bagging结合树,而 XGBoost 则通过boosting结合树。随机森林是 XGBoost 的一个可行替代方案,具有本章中强调的优点和局限性。了解随机森林非常重要,因为它们能为树基集成方法(如 XGBoost)提供宝贵的见解,并使你能够更深入地理解 boosting 与 bagging 之间的比较和对比。
在本章中,你将构建和评估随机森林分类器和随机森林回归器,掌握随机森林的超参数,学习机器学习中的 bagging 技巧,并探索一个案例研究,突出随机森林的局限性,这些局限性促使了梯度提升(XGBoost)的发展。
本章涵盖以下主要内容:
-
Bagging 集成方法
-
探索随机森林
-
调整随机森林超参数
-
推动随机森林的边界——案例研究
技术要求
Bagging 集成方法
在本节中,你将了解为什么集成方法通常优于单一的机器学习模型。此外,你还将学习 bagging 技巧。这两者是随机森林的重要特征。
集成方法
在机器学习中,集成方法是一种通过聚合单个模型的预测结果来构建的模型。由于集成方法结合了多个模型的结果,它们较不容易出错,因此往往表现更好。
假设你的目标是确定一栋房子是否会在上市的第一个月内售出。你运行了几个机器学习算法,发现逻辑回归的准确率为 80%,决策树为 75%,k 最近邻为 77%。
一个选项是使用逻辑回归,作为最准确的模型,作为你的最终模型。更有说服力的选项是将每个单独模型的预测结果结合起来。
对于分类器,标准选项是采用多数投票。如果至少有三个模型中的两个预测房子会在第一个月内卖掉,那么预测结果是YES。否则,预测结果是NO。
使用集成方法通常能提高整体准确性。要让预测错误,仅仅一个模型出错是不够的;必须有大多数分类器都预测错误。
集成方法通常分为两类。第一类是将不同的机器学习模型组合在一起,例如用户选择的 scikit-learn 的VotingClassifier
。第二类集成方法将同一模型的多个版本组合在一起,就像 XGBoost 和随机森林一样。
随机森林是所有集成方法中最流行和最广泛使用的。随机森林的单独模型是决策树,正如上一章的重点,第二章,深入了解决策树。一个随机森林可能包含数百或数千棵决策树,其预测结果将被合并成最终结果。
尽管随机森林使用多数规则来分类器,使用所有模型的平均值来回归器,但它们还使用一种叫做袋装(bagging)的方法,袋装是自助聚合(bootstrap aggregation)的缩写,用来选择单独的决策树。
自助聚合
**自助法(Bootstrapping)**是指带放回的抽样。
想象一下,你有一袋 20 颗有色大理石。你将依次选择 10 颗大理石,每次选择后都将其放回袋中。这意味着,虽然极不可能,但你有可能选中同一颗大理石 10 次。
你更可能多次选中一些大理石,而有些大理石则可能一次也不选中。
这里是大理石的可视化图:
图 3.1 – 袋装的可视化示意图(改绘自:Siakorn,Wikimedia Commons,commons.wikimedia.org/wiki/File:Ensemble_Bagging.svg
)
从上面的示意图可以看出,自助样本是通过带放回的抽样获得的。如果大理石没有被放回,那么就不可能得到比原始袋中更多黑色(原图中的蓝色)大理石的样本,正如最右侧的框所示。
在随机森林中,自助法在幕后发挥作用。自助法发生在每棵决策树建立时。如果所有的决策树都由相同的样本组成,那么这些树会给出类似的预测,最终结果也会与单棵树的预测相似。而在随机森林中,树是通过自助法建立的,通常样本数与原始数据集相同。数学估计表明,每棵树中约三分之二的样本是唯一的,三分之一包含重复样本。
在模型建立的自助法阶段后,每棵决策树会做出自己的单独预测。结果是由多棵树组成的森林,这些树的预测结果会根据分类器的多数规则和回归器的平均值合并成最终预测。
总结一下,随机森林聚合了自助法生成的决策树的预测。这种通用的集成方法在机器学习中被称为自助法(bagging)。
探索随机森林
为了更好地了解随机森林的工作原理,让我们使用 scikit-learn 构建一个随机森林。
随机森林分类器
让我们使用一个随机森林分类器,使用我们在第一章,机器学习概览中清理和评分的普查数据集,预测用户收入是否超过 50,000 美元,并在第二章,决策树深入剖析中重新检查。我们将使用cross_val_score
确保我们的测试结果具有良好的泛化能力:
以下步骤使用普查数据集构建并评分一个随机森林分类器:
-
导入
pandas
、numpy
、RandomForestClassifier
和cross_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')
-
加载数据集
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]
在准备好导入和数据后,现在是时候构建模型了。
-
接下来,我们初始化随机森林分类器。在实践中,集成算法与其他机器学习算法一样工作。一个模型被初始化、拟合训练数据,并在测试数据上评分。
我们通过提前设置以下超参数来初始化随机森林:
a)
random_state=2
确保你的结果与我们的结果一致。b)
n_jobs=-1
通过利用并行处理加速计算。c)
n_estimators=10
,这是 scikit-learn 的默认值,足以加速计算并避免歧义;新的默认值已设置为n_estimators=100
。n_estimators
将在下一节中详细探讨:rf = RandomForestClassifier(n_estimators=10, random_state=2, n_jobs=-1)
-
现在我们将使用
cross_val_score
。cross_val_score
需要一个模型、预测列和目标列作为输入。回顾一下,cross_val_score
会对数据进行拆分、拟合和评分:scores = cross_val_score(rf, X_census, y_census, cv=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
)。
此外,最终的预测是通过对所有决策树的预测结果求平均得出的,而不是通过多数规则投票。
为了查看随机森林回归器的实际应用,请完成以下步骤:
-
从第二章《决策树深度剖析》上传自行车租赁数据集,并提取前五行以供回顾:
df_bikes = pd.read_csv('bike_rentals_cleaned.csv') df_bikes.head()
上述代码应生成以下输出:
图 3.2 – 自行车租赁数据集 – 已清理
-
将数据划分为
X
和y
,即预测列和目标列:X_bikes = df_bikes.iloc[:,:-1] y_bikes = df_bikes.iloc[:,-1]
-
导入回归器,然后使用相同的默认超参数进行初始化,
n_estimators=10
,random_state=2
,n_jobs=-1
:from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor(n_estimators=10, random_state=2, n_jobs=-1)
-
现在我们需要使用
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)
-
查找并显示均方根误差(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 个随机森林,这段代码可能需要一些时间才能完成运行!代码按以下步骤分解:
-
导入 matplotlib 和 seaborn,然后通过
sns.set()
设置 seaborn 的暗色网格:import matplotlib.pyplot as plt import seaborn as sns sns.set()
-
初始化一个空的分数列表,并用 50 个估计器初始化随机森林分类器,确保
warm_start=True
和oob_score=True
:oob_scores = [] rf = RandomForestClassifier(n_estimators=50, warm_start=True, oob_score=True, n_jobs=-1, random_state=2)
-
将
rf
拟合到数据集上,然后将oob_score
添加到oob_scores
列表中:rf.fit(X_census, y_census) oob_scores.append(rf.oob_score_)
-
准备一个估计器列表,其中包含从 50 开始的树的数量:
est = 50 estimators=[est]
-
写一个 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_)
-
为了更好的展示,显示一个更大的图表,然后绘制估计器和
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
如你所见,树木的数量在大约 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_bikes
和y_bikes
。现在,您进行一些严肃的测试,决定将X_bikes
和y_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
这个图表非常有趣。随机森林在 50 个估计器下提供了最佳得分。在 100 个估计器后,误差开始逐渐增加,这是一个稍后会重新讨论的概念。
现在,使用n_estimators=50
作为起点是合理的选择。
cross_val_score
根据之前的图表,误差范围从 620 到 690 辆自行车租赁,现在是时候看看数据集在使用cross_val_score
进行交叉验证时的表现了。回想一下,在交叉验证中,目的是将样本划分为k个不同的折,并在不同的折中使用所有样本作为测试集。由于所有样本都用于测试模型,oob_score
将无法使用。
以下代码包含了您在本章早期使用的相同步骤:
-
初始化模型。
-
对模型进行评分,使用
cross_val_score
与模型、预测列、目标列、评分标准和折数作为参数。 -
计算 RMSE。
-
显示交叉验证得分和平均值。
这里是代码:
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)
现在将数据分成新的X
和y
,再次运行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_estimators
和warm_start
超参数,并使用oob_score_
来查找误差。然后,你修改了随机森林的超参数以优化模型。最后,你分析了一个案例研究,其中数据洗牌带来了优秀的结果,而对未洗牌数据增加更多的树并没有带来任何提升,这与 XGBoost 的结果形成了对比。
在下一章中,你将学习提升方法(boosting)的基本原理,这是一种集成方法,通过从错误中学习,随着更多决策树的加入,提升准确率。你将实现梯度提升(gradient boosting)进行预测,为极端梯度提升(Extreme Gradient Boosting,简称 XGBoost)奠定基础。