Python 集成学习实用指南(一)

原文:annas-archive.org/md5/681e70f53a5cd12054ae0d01b7b855ea

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

集成学习是一种将两种或更多相似或不相似的机器学习算法结合起来,以创建具有更强预测能力的模型的技术。本书将展示如何使用多种弱算法来构建强预测模型。

本书通过动手实践的方法,您不仅能快速掌握基本理论,还能了解各种集成学习技术的应用。通过示例和实际数据集,您将能够构建更好的机器学习模型,解决分类和回归等监督学习问题。随着书本的进展,您将进一步利用集成学习技术,如聚类,来生成无监督的机器学习模型。在后续章节中,您将学习广泛应用于实际世界中的各种机器学习算法,用于做出预测和分类。您还将掌握如何使用 Python 库,如 scikit-learn 和 Keras,实现不同的集成模型。

本书结束时,您将熟练掌握集成学习,并具备理解哪种集成方法适用于哪种问题的能力,以便在实际场景中成功实施它们。

本书适合谁阅读

本书面向数据分析师、数据科学家、机器学习工程师以及其他希望使用集成技术生成高级模型的专业人士。

本书内容简介

第一章,机器学习复习,概述了机器学习的基本概念,包括训练集/测试集、性能评估、监督学习和无监督学习、机器学习算法以及基准数据集等内容。

第二章,集成学习入门,介绍了集成学习的概念,重点介绍了它解决的问题以及它带来的挑战。

第三章,投票法,介绍了最简单的集成学习技术——投票法,同时解释了硬投票和软投票之间的区别。您将学习如何实现自定义分类器,并使用 scikit-learn 的硬/软投票实现。

第四章,堆叠,介绍了元学习(堆叠),这是一种更高级的集成学习方法。阅读完本章后,您将能够在 Python 中实现堆叠分类器,并与 scikit-learn 分类器一起使用。

第五章,袋装法,介绍了自助重采样以及第一种生成性集成学习技术——袋装法。此外,本章还将指导您如何在 Python 中实现该技术,并使用 scikit-learn 的实现。

第六章,提升方法,涉及集成学习中的更高级主题。本章解释了流行的提升算法如何工作以及如何实现它们。此外,本章还介绍了 XGBoost,这是一种成功的分布式提升库。

第七章,随机森林,讲解了通过对数据集的实例和特征进行子抽样来创建随机决策树的过程。此外,本章还解释了如何利用随机树集成创建随机森林。最后,本章介绍了 scikit-learn 的实现及其使用方法。

第八章,聚类,介绍了如何使用集成方法进行无监督学习任务,例如聚类。此外,还介绍了 OpenEnsembles Python 库,并提供了使用该库的指导。

第九章,分类欺诈交易,展示了如何使用前面章节介绍的集成学习技术,对一个真实世界数据集进行分类应用。该数据集涉及信用卡欺诈交易。

第十章,预测比特币价格,介绍了如何使用前面章节中介绍的集成学习技术对一个真实世界数据集进行回归分析。该数据集涉及流行加密货币比特币的价格。

第十一章,评估 Twitter 情感,展示了如何使用真实世界数据集评估各种推文的情感。

第十二章,使用 Keras 推荐电影,介绍了如何利用神经网络集成方法创建推荐系统的过程。

第十三章,聚类世界幸福指数,介绍了如何使用集成学习方法对 2018 年世界幸福报告的数据进行聚类。

为了最大限度地发挥本书的作用

本书面向分析师、数据科学家、工程师以及对生成高级模型、描述并概括与其相关数据集的专业人士。假设读者具有基本的 Python 编程经验,并且熟悉基础的机器学习模型。此外,假设读者具备基本的统计学知识,尽管关键点和更高级的概念会简单介绍。熟悉 Python 的 scikit-learn 模块会非常有帮助,尽管不是严格要求。需要安装标准的 Python 环境。Anaconda 发行版(www.anaconda.com/distribution/)极大地简化了安装和管理各种 Python 包的任务,尽管它不是必须的。最后,一个好的集成开发环境IDE)对于管理代码和调试非常有用。在我们的示例中,我们通常使用 Spyder IDE,您可以通过 Anaconda 轻松安装它。

下载示例代码文件

您可以从您的账户下载本书的示例代码文件,访问www.packt.com。如果您在其他地方购买了本书,可以访问www.packt.com/support,注册后将文件直接发送到您的邮箱。

您可以通过以下步骤下载代码文件:

  1. 登录或注册 www.packt.com

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并按照屏幕上的说明操作。

文件下载后,请确保使用以下最新版本的工具解压或提取文件夹:

  • Windows 上的 WinRAR/7-Zip

  • macOS 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址是**github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python**。如果代码有更新,它会在现有的 GitHub 仓库中更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,您可以在**github.com/PacktPublishing/**找到。快来查看吧!

下载彩色图片

我们还提供了一个 PDF 文件,里面包含本书中使用的截图/图表的彩色图片。您可以在这里下载: static.packt-cdn.com/downloads/9781789612851_ColorImages.pdf

代码实战

访问以下链接,查看代码运行的视频:bit.ly/2GfnRrv

使用的约定

本书中使用了若干文本约定。

CodeInText:表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为您系统中的另一个磁盘。”

一块代码设置如下:

# --- SECTION 6 ---
# Accuracy of hard voting
print('-'*30)
print('Hard Voting:', accuracy_score(y_test, hard_predictions))

粗体:表示一个新术语、一个重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇会以这种方式显示在文本中。以下是一个示例:“因此,首选的方法是利用K 折交叉验证。”

警告或重要提示通常以这种形式出现。

提示和技巧通常以这种形式出现。

联系我们

我们总是欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提到书名,并通过customercare@packtpub.com联系我们。

勘误:尽管我们已尽一切努力确保内容的准确性,但难免会出现错误。如果您发现本书中的错误,我们将不胜感激您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接并输入详细信息。

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

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有意撰写或参与编写书籍,请访问authors.packtpub.com

评论

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

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

第一部分:简介与所需的软件工具

本节回顾了基本的机器学习概念,并介绍了集成学习。我们将概述机器学习及其相关概念,例如训练集和测试集、监督学习与非监督学习等。我们还将学习集成学习的概念。

本节包含以下章节:

  • 第一章,机器学习基础回顾

  • 第二章,开始使用集成学习

第一章:机器学习复习

机器学习是人工智能AI)的一个子领域,致力于开发使计算机能够从海量数据中学习的算法和技术。随着数据产生速度的不断增加,机器学习在近年来在解决复杂问题中发挥了关键作用。这一成功是许多优秀机器学习库的资金支持和发展的主要推动力,这些库利用数据来构建预测模型。此外,企业也开始意识到机器学习的潜力,推动了对数据科学家和机器学习工程师的需求飙升,以设计性能更优的预测模型。

本章旨在复习主要概念和术语,并介绍将在本书中使用的框架,以便以扎实的基础接触集成学习。

本章覆盖的主要内容如下:

  • 各种机器学习问题和数据集

  • 如何评估预测模型的性能

  • 机器学习算法

  • Python 环境设置及所需库

技术要求

你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 语法和约定。最后,熟悉 NumPy 库将极大帮助读者理解一些自定义算法的实现。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter01

查看以下视频,观看代码实例:bit.ly/30u8sv8

从数据中学习

数据是机器学习的原材料。处理数据可以生成信息;例如,测量一部分学生的身高(数据),并计算他们的平均值(处理),可以帮助我们了解整个学校的身高(信息)。如果我们进一步处理数据,例如,将男性和女性分组并计算每个组的平均值,我们将获得更多信息,因为我们将能够知道学校男性和女性的平均身高。机器学习旨在从任何给定的数据中提取尽可能多的信息。在这个例子中,我们生成了一个非常基础的预测模型。通过计算两个平均值,我们只需知道学生是男性还是女性,就能预测任何学生的平均身高。

机器学习算法处理的数据集合被称为问题的数据集。在我们的例子中,数据集包括身高测量(单位为厘米)和孩子的性别(男性/女性)。在机器学习中,输入变量称为特征,输出变量称为目标。在这个数据集中,我们预测模型的特征仅由学生的性别组成,而我们的目标是学生的身高(以厘米为单位)。从现在开始,除非另有说明,否则生成的预测模型将简单地称为模型。每个数据点称为一个实例。在这个问题中,每个学生都是数据集的一个实例。

当目标是一个连续变量(即数字)时,问题是回归问题,因为目标是根据特征进行回归。当目标是一个类别集时,问题是分类问题,因为我们试图将每个实例分配到一个类别或类中。

在分类问题中,目标类可以用一个数字表示;这并不意味着它是回归问题。判断是否为回归问题的最有用的方法是思考是否可以通过目标对实例进行排序。在我们的例子中,目标是身高,因此我们可以将学生按身高从高到低排序,因为 100 厘米小于 110 厘米。举个反例,如果目标是他们最喜欢的颜色,我们可以用数字表示每种颜色,但无法对其进行排序。即使我们把红色表示为一,蓝色表示为二,我们也不能说红色是“在前”或“比”蓝色小。因此,这个反例是一个分类问题。

流行的机器学习数据集

机器学习依赖于数据来生成高性能模型。没有数据,甚至无法创建模型。在本节中,我们将介绍一些流行的机器学习数据集,我们将在本书中使用这些数据集。

糖尿病

糖尿病数据集涉及 442 名糖尿病患者及其在基准测量后一年内病情的进展。数据集包括 10 个特征,包括患者的年龄、性别、体重指数bmi)、平均血压bp)以及六个血清测量值。数据集的目标是基准测量后一年内病情的进展。这是一个回归数据集,因为目标是一个数字。

在本书中,数据集的特征已经进行了均值中心化和缩放处理,使得每个特征的数据集平方和等于 1。下表展示了糖尿病数据集的一个样本:

年龄性别bmi血压s1s2s3s4s5s6目标
0.040.050.060.02-0.04-0.03-0.040.000.02-0.02151
0.00-0.04-0.05-0.03-0.01-0.020.07-0.04-0.07-0.0975
0.090.050.04-0.01-0.05-0.03-0.030.000.00-0.03141
-0.09-0.04-0.01-0.040.010.02-0.040.030.02-0.01206

乳腺癌

乳腺癌数据集涉及 569 例恶性和良性肿瘤的活检。该数据集提供了从细针穿刺活检图像中提取的 30 个特征,这些特征描述了细胞核的形状、大小和纹理。此外,对于每个特征,还提供了三个不同的值:均值、标准误差以及最差或最大值。这确保了每个图像中的细胞群体得到充分描述。

数据集的目标是诊断,即肿瘤是恶性还是良性。因此,这是一个分类数据集。可用的特征如下所列:

  • 平均半径

  • 平均纹理

  • 平均周长

  • 平均面积

  • 平均平滑度

  • 平均紧密度

  • 平均凹度

  • 平均凹点

  • 平均对称性

  • 平均分形维度

  • 半径误差

  • 纹理误差

  • 周长误差

  • 面积误差

  • 平滑度误差

  • 紧密度误差

  • 凹度误差

  • 凹点误差

  • 对称性误差

  • 分形维度误差

  • 最差半径

  • 最差纹理

  • 最差周长

  • 最差面积

  • 最差平滑度

  • 最差紧密度

  • 最差凹度

  • 最差凹点

  • 最差对称性

  • 最差分形维度

手写数字

MNIST 手写数字数据集是最著名的图像识别数据集之一。它由 8 x 8 像素的方形图像组成,每个图像包含一个手写数字。因此,数据集的特征是一个 8 x 8 的矩阵,包含每个像素的灰度色值。目标包括 10 个类别,分别对应数字 0 到 9。这个数据集是一个分类数据集。下图是手写数字数据集中的一个样本:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/67c53ed8-f82c-4835-9794-93a597511b7a.png

手写数字数据集样本

有监督学习与无监督学习

机器学习可以分为多个子类别;其中两个大类是有监督学习和无监督学习。这些类别包含了许多流行且广泛应用的机器学习方法。在本节中,我们将介绍这些方法,并给出一些有监督学习和无监督学习的小例子。

有监督学习

在上一节的例子中,数据包含了一些特征和一个目标;无论目标是定量的(回归)还是分类的(分类)。在这种情况下,我们将数据集称为标注数据集。当我们尝试从标注数据集中生成一个模型,以便对看不见的或未来的数据做出预测(例如,诊断新的肿瘤病例)时,我们使用有监督学习。在简单的情况下,有监督学习模型可以被视为一条线。这条线的目的是根据目标(分类)将数据分开,或者紧密跟随数据(回归)。

下图展示了一个简单的回归示例。在这里,y是目标,x是数据集特征。我们的模型由简单的方程y=2x-5 组成。显然,直线紧密地跟随数据。为了估算一个新的、未见过的点的y值,我们使用上述公式计算其值。下图显示了一个使用y=2x-5 作为预测模型的简单回归:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/b1680c76-c8ad-4c1f-92f7-7091d33a1870.png

使用预测模型 y=2x-5 的简单回归

在下图中,展示了一个简单的分类问题。在这里,数据集特征是xy,目标是实例的颜色。再次,虚线是y=2x-5,但这次我们测试点是位于线的上方还是下方。如果该点的y值低于预期(较小),我们期望它是橙色的。如果它较高(较大),我们期望它是蓝色的。下图是使用y=2x-5 作为边界的简单分类:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/210bb1bf-db70-4be2-ba9c-a344b16a1bf7.png

使用 y=2x-5 作为边界的简单分类

无监督学习

在回归和分类中,我们清楚地理解数据的结构或行为。我们的目标只是对该结构或行为进行建模。在某些情况下,我们不知道数据的结构。在这些情况下,我们可以利用无监督学习来发现数据中的结构,从而获得信息。无监督学习的最简单形式是聚类。顾名思义,聚类技术尝试将数据实例分组(或聚类)。因此,属于同一聚类的实例在特征上有很多相似之处,而与属于不同聚类的实例则有很大差异。下图展示了一个具有三个聚类的简单例子。在这里,数据集特征是xy,没有目标。

聚类算法发现了三个不同的组,分别以点(0, 0)、(1, 1)和(2, 2)为中心:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/c0ab1932-b0fd-4959-b37a-583d2257a464.png

具有三个不同组的聚类

降维

另一种无监督学习的形式是降维。数据集中存在的特征数量等于数据集的维度。通常,许多特征可能是相关的、有噪声的,或者根本没有提供太多信息。然而,存储和处理数据的成本与数据集的维度是相关的。因此,通过减少数据集的维度,我们可以帮助算法更好地建模数据。

降维的另一个用途是高维数据集的可视化。例如,使用 t-分布随机邻居嵌入(t-SNE)算法,我们可以将乳腺癌数据集降至两个维度或组件。尽管可视化 30 个维度并不容易,但可视化两个维度却相当简单。

此外,我们还可以通过可视化测试数据集中包含的信息是否能够用来区分数据集的各个类别。下图展示了y轴和x轴上的两个组件,而颜色则代表实例的类别。尽管我们无法绘制所有维度,但通过绘制这两个组件,我们可以得出类之间存在一定可分性的结论:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/a007fc1c-de3b-4a3f-9f7f-afeea4c6546e.png

使用 t-SNE 降维乳腺癌数据集

性能指标

机器学习是一个高度定量的领域。虽然我们可以通过绘制模型如何区分类别以及它如何紧密地跟随数据来衡量模型的性能,但为了评估模型的表现,还需要更多的定量性能指标。在本节中,我们将介绍代价函数和评估指标。它们都用于评估模型的表现。

代价函数

机器学习模型的目标是对我们的数据集进行建模。为了评估每个模型的表现,我们定义了目标函数。这些函数通常表示一个代价,或者说是模型距离完美的程度。这些代价函数通常使用损失函数来评估模型在每个单独数据实例上的表现。

以下几节描述了一些最常用的代价函数,假设数据集有n个实例,实例i的目标真实值为t[i],而模型的输出为y[i]

平均绝对误差

平均绝对误差MAE)或 L1 损失是目标真实值与模型输出之间的均绝对距离。它的计算公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/e08a7ef6-713b-408c-af9f-ae0a55486bc5.png

均方误差

均方误差MSE)或 L2 损失是目标真实值与模型输出之间的均方距离。它的计算公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/3233ee2b-4852-4c38-baeb-1c1482ac59b7.png

交叉熵损失

交叉熵损失用于输出介于 0 和 1 之间的概率的模型,通常用来表示一个实例属于特定类别的概率。当输出概率偏离实际标签时,损失值会增加。在一个简单的例子中,假设数据集由两个类别组成,计算方法如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/ca72266c-12e1-4d41-828d-049d20b0a341.png

指标

代价函数在我们尝试通过数值优化模型时非常有用。但作为人类,我们需要一些既有用又直观的指标来理解和报告。因此,有许多可以提供模型性能洞察的指标。以下几节将介绍最常用的指标。

分类准确率

所有指标中最简单且最容易掌握的分类准确率,指的是正确预测的百分比。为了计算准确率,我们将正确预测的数量除以总实例数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/be71cf1e-4842-4b5c-888f-7226840eb631.png

为了使准确率具有实质性的意义,数据集必须包含相等数量的属于每个类别的实例。如果数据集不平衡,准确率将受到影响。例如,如果一个数据集由 90%的 A 类和 10%的 B 类组成,那么一个将每个实例预测为 A 类的模型将有 90%的准确率,尽管它的预测能力为零。

混淆矩阵

为了解决前面的问题,可以使用混淆矩阵。混淆矩阵呈现正确或错误预测为每个可能类别的实例数量。在只有两个类别(“是”和“否”)的数据集中,混淆矩阵具有以下形式:

n = 200预测: 是预测: 否
目标: 是8070
目标: 否2030

这里有四个单元格,每个对应以下之一:

  • 真正例 (TP): 当目标属于“是”类别且模型预测为“是”

  • 真负例 (TN): 当目标属于“否”类别且模型预测为“否”

  • 假正例 (FP): 当目标属于“否”类别且模型预测为“是”

  • 假负例 (FN): 当目标属于“是”类别且模型预测为“否”

混淆矩阵提供有关真实类别和预测类别平衡的信息。为了从混淆矩阵中计算准确率,我们将 TP 和 TN 的总和除以实例的总数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/56ea3bcb-3940-47ff-bae2-6699fa045bf1.png

灵敏度、特异性和曲线下面积

曲线下面积(AUC)关注二分类数据集,它描述了模型正确排名任何给定实例的概率。为了定义 AUC,我们首先必须定义灵敏度和特异性:

  • 灵敏度 (真正例率): 灵敏度是指相对于所有正例,正确预测为正例的正例比例。其计算方法如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/44038c3e-a8c4-466f-8c94-001702317038.png

  • 特异性 (假正例率): 特异性是指相对于所有负例,错误预测为正例的负例比例。其计算方法如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/f1bd2469-3805-4932-86c7-e075947d82ce.png

通过在特定的间隔(例如,每 0.05 增加一次)计算(1-特异性)和灵敏度,我们可以观察模型的表现。间隔与模型对每个实例的输出概率相关;例如,首先我们计算所有估计属于“是”类别的概率小于 0.05 的实例。然后,重新计算所有估计概率小于 0.1 的实例,以此类推。结果如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/289e210c-25ec-47c6-a2be-a45556c5018e.png

接收者操作特征曲线

直线表示正确或错误地对实例进行排名的概率相等:一个随机模型。橙色线(ROC 曲线)描绘了模型的概率。如果 ROC 曲线位于直线下方,意味着模型的表现比随机的、没有信息的模型差。

精确度、召回率和 F1 分数

精确度衡量模型的表现,通过量化正确分类为特定类别的实例的百分比,相对于所有预测为同一类别的实例。计算公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/996f726a-e333-4ba7-b981-460abc3ff77a.png

召回率也叫做敏感度。精确度和召回率的调和平均数称为 F1 分数,计算公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/89a6f1fc-1249-429a-802a-e77de45ad214.png

使用调和平均数而不是简单平均数的原因是,调和平均数受两者(精确度和召回率)之间不平衡的影响很大。因此,如果精确度或召回率显著小于另一个,F1 分数将反映这种不平衡。

评估模型

尽管有各种各样的指标来表明模型的表现,但仔细设置测试环境是非常重要的。最重要的事情之一是将数据集分成两部分。数据集的一部分将由算法用于生成模型;另一部分将用于评估模型。这通常被称为训练集和测试集。

训练集可以被算法用来生成和优化模型,使用任何代价函数。算法完成后,生成的模型将在测试集上进行测试,以评估其对未见数据的预测能力。虽然算法可能在训练集上生成表现良好的模型(样本内表现),但它可能无法泛化并在测试集上表现同样好(样本外表现)。这可以归因于许多因素——将在下一章中讨论。一些出现的问题可以通过使用集成方法来解决。然而,如果算法面对的是低质量的数据,几乎无法改进样本外的表现。

为了获得一个公正的估计,我们有时会将数据集的不同部分迭代地分成固定大小的训练集和测试集,比如 90%的训练集和 10%的测试集,直到对整个数据集进行了测试。这被称为 K 折交叉验证。在 90%与 10%的分割情况下,它被称为 10 折交叉验证,因为我们需要进行 10 次以便获得整个数据集的估计。

机器学习算法

有许多机器学习算法,适用于监督学习和无监督学习。在本书中,我们将介绍一些最受欢迎的算法,这些算法可以在集成方法中使用。本章将介绍每个算法背后的关键概念、基本算法以及实现这些算法的 Python 库。

Python 包

为了充分发挥任何编程语言的强大功能,库是必不可少的。它们提供了许多算法的便捷且经过测试的实现。在本书中,我们将使用 Python 3.6,并结合以下库:NumPy,因其出色的数值运算符和矩阵实现;Pandas,因其便捷的数据操作方法;Matplotlib,用于可视化我们的数据;scikit-learn,因其出色的各种机器学习算法实现;Keras,用于构建神经网络,利用其 Pythonic、直观的接口。Keras 是其他框架的接口,如 TensorFlow、PyTorch 和 Theano。本书中使用的每个库的具体版本如下:

  • numpy==1.15.1

  • pandas==0.23.4

  • scikit-learn==0.19.1

  • matplotlib==2.2.2

  • Keras==2.2.4

监督学习算法

最常见的机器学习算法类别是监督学习算法。这类算法涉及数据具有已知结构的问题。这意味着每个数据点都有一个与之相关的特定值,我们希望对其进行建模或预测。

回归

回归是最简单的机器学习算法之一。普通最小二乘法OLS)回归形式为 y=ax+b,旨在优化 ab 参数,以便拟合数据。它使用均方误差(MSE)作为其代价函数。顾名思义,它能够解决回归问题。

我们可以使用 scikit-learn 实现的 OLS 来尝试建模糖尿病数据集(该数据集随库一起提供):

# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression
from sklearn import metrics
diabetes = load_diabetes()

第一部分处理导入库和加载数据。我们使用 linear_model 包中的 LinearRegression 实现:

# --- SECTION 2 ---
# Split the data into train and test set
train_x, train_y = diabetes.data[:400], diabetes.target[:400]
test_x, test_y = diabetes.data[400:], diabetes.target[400:]

第二部分将数据划分为训练集和测试集。在这个示例中,我们使用前 400 个实例作为训练集,另外 42 个作为测试集:

# --- SECTION 3 ---
# Instantiate, train and evaluate the model
ols = LinearRegression()
ols.fit(train_x, train_y)
err = metrics.mean_squared_error(test_y, ols.predict(test_x))
r2 = metrics.r2_score(test_y, ols.predict(test_x))

接下来的部分通过 ols = LinearRegression() 创建一个线性回归对象。然后,它通过使用 ols.fit(train_x, train_y) 在训练实例上优化参数,或者说拟合模型。最后,通过使用 metrics 包,我们使用第四部分中的测试数据计算模型的 MSE 和

# --- SECTION 4 ---
# Print the model
print('---OLS on diabetes dataset.---')
print('Coefficients:')
print('Intercept (b): %.2f'%ols.intercept_)
for i in range(len(diabetes.feature_names)):
 print(diabetes.feature_names[i]+': %.2f'%ols.coef_[i])
print('-'*30)
print('R-squared: %.2f'%r2, ' MSE: %.2f \n'%err)

代码的输出如下:

---OLS on diabetes dataset.---
Coefficients:
Intercept (b): 152.73
age: 5.03
sex: -238.41
bmi: 521.63
bp: 299.94
s1: -752.12
s2: 445.15
s3: 83.51
s4: 185.58
s5: 706.47
s6: 88.68
------------------------------
R-squared: 0.70 MSE: 1668.75

另一种回归形式,逻辑回归,试图建模一个实例属于两个类之一的概率。同样,它试图优化 ab 参数,以便建模 p=1/(1+e^(-(ax+b)))。同样,使用 scikit-learn 和乳腺癌数据集,我们可以创建并评估一个简单的逻辑回归。以下代码部分与前面的类似,不过这次我们将使用分类准确率和混淆矩阵,而不是 作为度量:

# --- SECTION 1 ---
# Libraries and data loading
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn import metrics
bc = load_breast_cancer()

# --- SECTION 2 ---
# Split the data into train and test set
train_x, train_y = bc.data[:400], bc.target[:400]
test_x, test_y = bc.data[400:], bc.target[400:]

# --- SECTION 3 ---
# Instantiate, train and evaluate the model
logit = LogisticRegression()
logit.fit(train_x, train_y)
acc = metrics.accuracy_score(test_y, logit.predict(test_x))

# --- SECTION 4 ---
# Print the model
print('---Logistic Regression on breast cancer dataset.---')
print('Coefficients:')
print('Intercept (b): %.2f'%logit.intercept_)
for i in range(len(bc.feature_names)):
 print(bc.feature_names[i]+': %.2f'%logit.coef_[0][i])
print('-'*30)
print('Accuracy: %.2f \n'%acc)
print(metrics.confusion_matrix(test_y, logit.predict(test_x)))

该模型实现的测试分类准确率为 95%,表现相当不错。此外,下面的混淆矩阵表明该模型并没有试图利用类别不平衡的问题。在本书的后续章节中,我们将学习如何通过集成方法进一步提高分类准确率。以下表格展示了逻辑回归模型的混淆矩阵:

n = 169预测:恶性预测:良性
目标:恶性381
目标:良性8122

支持向量机

支持向量机(SVM)使用训练数据的一个子集,特别是每个类别边缘附近的数据点,用以定义一个分隔超平面(在二维中为一条直线)。这些边缘数据点称为支持向量。SVM 的目标是找到一个最大化支持向量之间间距(距离)的超平面(如下图所示)。为了分类非线性可分的类别,SVM 使用核技巧将数据映射到更高维的空间,在该空间中数据可以变得线性可分:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/d9689d3f-38d0-478f-abdb-313232de0725.png

SVM 边界和支持向量

如果你想了解更多关于核技巧的内容,这是一个很好的起点:en.wikipedia.org/wiki/Kernel_method#Mathematics:_the_kernel_trick

在 scikit-learn 中,SVM 被实现为 sklearn.svm,支持回归(使用 sklearn.svm.SVR)和分类(使用 sklearn.svm.SVC)。我们将再次使用 scikit-learn 测试算法的潜力,并使用回归示例中的代码。使用带有线性核的 SVM 对乳腺癌数据集进行测试,结果为 95% 的准确率,混淆矩阵如下:

n = 169预测:恶性预测:良性
目标:恶性390
目标:良性9121

在糖尿病数据集上,通过在 (svr = SVR(kernel='linear', C=1e3)) 对象实例化过程中将 C 参数调整为 1000,我们能够实现一个 R2 值为 0.71 和 MSE 值为 1622.36,略微优于逻辑回归模型。

神经网络

神经网络,灵感来自于生物大脑的连接方式,由许多神经元或计算模块组成,这些模块按层组织。数据从输入层提供,预测结果由输出层产生。所有中间层称为隐藏层。属于同一层的神经元之间没有直接连接,只与属于其他层的神经元连接。每个神经元可以有多个输入,每个输入都与特定权重相乘,乘积的和被传递到激活函数,激活函数决定神经元的输出。常见的激活函数包括以下几种:

SigmoidTanhReLU线性
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/15d85d5f-1a56-4f98-a267-9bec857704fa.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/3b36b9f5-f2db-4490-b19a-977905bd2c45.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/263f0f9f-6ebc-4155-a347-2f46288986d6.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/1c60ae33-f144-43ff-890a-0739f7e46fc1.png

网络的目标是优化每个神经元的权重,以使成本函数最小化。神经网络可以用于回归问题,其中输出层由一个神经元组成,或者用于分类问题,其中输出层由多个神经元组成,通常与类别数相等。神经网络有多种优化算法或优化器可供选择,最常见的是随机梯度下降(SGD)。其基本思想是,根据误差梯度的方向和大小(即一阶导数),乘以一个称为学习率的因子来更新权重。

已提出了多种变体和扩展,考虑了二阶导数,调整了学习率,或利用前一个权重变化的动量来更新权重。

尽管神经网络的概念已经存在很长时间,但随着深度学习的出现,它们的受欢迎程度最近大大增加。现代架构由卷积层组成,每个层的权重是矩阵,输出通过将权重矩阵滑动到输入上来计算。另一种类型的层是最大池化层,它通过将固定大小的窗口滑动到输入上来计算输出,输出为最大输入元素。递归层则保留了关于前一个状态的信息。

最后,全连接层是传统神经元,如前所述。

Scikit-learn 实现了传统的神经网络,位于 sklearn.neural_network 包下。再次使用之前的示例,我们将尝试对糖尿病和乳腺癌数据集进行建模。在糖尿病数据集中,我们将使用 MLPRegressor,并选择随机梯度下降SGD)作为优化器,代码为 mlpr = MLPRegressor(solver='sgd')。在没有进一步微调的情况下,我们达到了 0.64 的 R² 和 1977 的均方误差(MSE)。在乳腺癌数据集上,我们使用有限记忆 Broyden–Fletcher–Goldfarb–ShannoLBFGS)优化器,代码为 mlpc = MLPClassifier(solver='lbfgs'),我们得到了 93% 的分类准确率,并得到了一个合格的混淆矩阵。下表展示了乳腺癌数据集的神经网络混淆矩阵:

n = 169预测:恶性预测:良性
目标:恶性354
目标:良性8122

关于神经网络的一个非常重要的说明:网络的初始权重是随机初始化的。因此,如果多次执行相同的代码,结果可能会不同。为了确保非随机(非随机性)执行,必须固定网络的初始随机状态。两个 scikit-learn 类通过对象构造器中的 random_state 参数实现了这一功能。为了将随机状态设置为特定的种子值,构造器应按以下方式调用:mlpc = MLPClassifier(solver='lbfgs', random_state=12418)

决策树

决策树相比其他机器学习算法,更不具有黑箱性质。它们可以轻松解释如何产生预测,这被称为可解释性。其主要概念是通过使用提供的特征分割训练集来生成规则。通过迭代地分割数据,形成了一棵树,因此它们的名字由此而来。我们考虑一个数据集,其中的实例是每个个体在决定度假地点时的选择。

数据集的特征包括个人的年龄和可用资金,而目标是他们偏好的度假地点,可能的选择为 夏令营湖泊巴哈马。以下是一个可能的决策树模型:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/f80c05fa-627e-4833-b9ae-d3ae34c8aa1e.png

度假目的地问题的决策树模型

正如可以看到的,模型能够解释它是如何产生任何预测的。模型本身的构建方式是通过选择能够最大化信息量的特征和阈值。大致而言,这意味着模型会尝试通过迭代的方式分割数据集,从而最大程度地分离剩余的实例。

尽管决策树直观易懂,但它们也可能产生不合理的模型,极端情况下会生成过多的规则,最终每个规则组合都会指向一个单独的实例。为了避免此类模型,我们可以通过要求模型的深度不超过特定的最大值(即连续规则的最大数量),或者要求每个节点在进一步分裂之前,至少包含一定数量的实例来限制模型。

在 scikit-learn 中,决策树是通过 sklearn.tree 包实现的,包含 DecisionTreeClassifierDecisionTreeRegressor。在我们的示例中,使用 DecisionTreeRegressordtr = DecisionTreeRegressor(max_depth=2),我们得到了 R² 为 0.52,均方误差(MSE)为 2655。在乳腺癌数据集上,使用 dtc = DecisionTreeClassifier(max_depth=2),我们得到了 89% 的准确率,并且得到了以下的混淆矩阵:

n = 169预测: 恶性预测: 良性
目标: 恶性372
目标: 良性17113

虽然这个算法迄今为止表现得不是最好,但我们可以清晰地看到每个个体是如何被分类的,通过将树导出为graphviz格式,使用export_graphviz(dtc, feature_names=bc.feature_names, class_names=bc.target_names, impurity=False)

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/9da5bb06-3901-4bb1-9903-c3e921d77f90.png

为乳腺癌数据集生成的决策树

K-最近邻

k-最近邻k-NN)是一个相对简单的机器学习算法。每个实例通过与其 K 个最近的样本进行比较来分类,采用多数类作为分类结果。在回归中,使用邻居的平均值。Scikit-learn 的实现位于库的sklearn.neighbors包中。根据库的命名规范,KNeighborsClassifier实现了分类版本,KNeighborsRegressor实现了回归版本。通过在我们的示例中使用它们,回归器生成的 R²为 0.58,均方误差(MSE)为 2342,而分类器达到了 93%的准确率。下表显示了乳腺癌数据集的 k-NN 混淆矩阵:

n = 169预测:恶性预测:良性
目标:恶性372
目标:良性9121

K-means

K-means 是一种聚类算法,与 k-NN 有相似之处。它生成多个聚类中心,并将每个实例分配给其最近的聚类。所有实例分配到聚类后,聚类的中心点变成新的中心,直到算法收敛到稳定的解。在 scikit-learn 中,该算法通过sklearn.cluster.KMeans实现。我们可以尝试对乳腺癌数据集的前两个特征进行聚类:平均半径和 FNA 成像的纹理。

首先,我们加载所需的数据和库,同时只保留数据集的前两个特征:

# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.cluster import KMeans
bc = load_breast_cancer()
bc.data=bc.data[:,:2]

然后,我们将聚类拟合到数据上。注意,我们不需要将数据拆分为训练集和测试集:

# --- SECTION 2 ---
# Instantiate and train
km = KMeans(n_clusters=3)
km.fit(bc.data)

接下来,我们创建一个二维网格并对每个点进行聚类,以便绘制聚类区域和边界:

# --- SECTION 3 ---
# Create a point mesh to plot cluster areas
# Step size of the mesh. 
h = .02
# Plot the decision boundary. For that, we will assign a color to each
x_min, x_max = bc.data[:, 0].min() - 1, bc.data[:, 0].max() + 1
y_min, y_max = bc.data[:, 1].min() - 1, bc.data[:, 1].max() + 1
# Create the actual mesh and cluster it
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
Z = km.predict(np.c_[xx.ravel(), yy.ravel()])
# Put the result into a color plot
Z = Z.reshape(xx.shape)
plt.figure(1)
plt.clf()
plt.imshow(Z, interpolation='nearest',
 extent=(xx.min(), xx.max(), yy.min(), yy.max()),
 aspect='auto', origin='lower',)

最后,我们绘制实际数据,并将其颜色映射到相应的聚类:

 --- SECTION 4 ---
# Plot the actual data
c = km.predict(bc.data)
r = c == 0
b = c == 1
g = c == 2
plt.scatter(bc.data[r, 0], bc.data[r, 1], label='cluster 1')
plt.scatter(bc.data[b, 0], bc.data[b, 1], label='cluster 2')
plt.scatter(bc.data[g, 0], bc.data[g, 1], label='cluster 3')
plt.title('K-means')
plt.xlim(x_min, x_max)
plt.ylim(y_min, y_max)
plt.xticks(())
plt.yticks(())
plt.xlabel(bc.feature_names[0])
plt.ylabel(bc.feature_names[1])
`()
plt.show()

结果是一个二维图像,显示了每个聚类的彩色边界,以及各个实例:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/5590354e-8202-4dda-9414-be4d73bfa4ec.png

乳腺癌数据集前两个特征的 K-means 聚类

总结

在本章中,我们介绍了将在整本书中使用的基本数据集、算法和指标。我们讨论了回归和分类问题,其中数据集不仅包含特征,还包含目标。我们称这些为标注数据集。我们还讨论了无监督学习,包括聚类和降维。我们介绍了成本函数和模型指标,这些将用于评估我们生成的模型。此外,我们介绍了将在大多数示例中使用的基本学习算法和 Python 库。

在下一章中,我们将介绍偏差和方差的概念,以及集成学习的概念。以下是一些关键点:

  • 当目标变量是一个连续数值且其值具有某种大小意义时,例如速度、成本、血压等,我们尝试解决回归问题。分类问题的目标可能会以数字编码,但我们不能将其视为数值来处理。在问题编码时,尝试根据所分配的数字对颜色或食物进行排序是没有意义的。

  • 成本函数是一种量化预测模型与完美建模数据之间差距的方法。指标提供的信息更容易让人理解和报告。

  • 本章中介绍的所有算法在 scikit-learn 中都有分类和回归问题的实现。有些算法更适合某些特定任务,至少在不调整其超参数的情况下,决策树生成的模型易于人类解释。

第二章:开始使用集成学习

集成学习涉及将多种技术结合在一起,允许多个机器学习模型(称为基本学习器或有时称为弱学习器)整合它们的预测,并在给定各自的输入和输出的情况下输出一个单一的、最优的预测。

本章将概述集成学习尝试解决的主要问题,即偏差和方差,以及它们之间的关系。这将帮助我们理解识别表现不佳的模型的根本原因,并使用集成学习来解决该问题的动机。此外,我们还将介绍可用方法的基本类别,以及在实施集成学习时可能遇到的困难。

本章涵盖的主要主题如下:

  • 偏差、方差以及两者之间的权衡

  • 使用集成学习的动机

  • 识别表现不佳的模型的根本原因

  • 集成学习方法

  • 成功应用集成学习的难点

技术要求

你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 语言的约定和语法。最后,熟悉 NumPy 库将大大有助于读者理解一些自定义算法的实现。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter02

请查看以下视频,查看代码实践:bit.ly/2JKkWYS

偏差、方差及其权衡

机器学习模型并不完美;它们容易出现许多错误。最常见的两种错误来源是偏差和方差。尽管这两者是不同的问题,但它们是相互关联的,并且与模型的自由度或复杂性有关。

什么是偏差?

偏差是指方法无法正确估计目标。这不仅仅适用于机器学习。例如,在统计学中,如果我们想要测量一个人群的平均值,但没有仔细采样,那么估算出的平均值就会存在偏差。简单来说,方法(采样)估算的结果与实际目标(平均值)之间存在差距。

在机器学习中,偏差指的是预期预测与目标之间的差异。偏差模型无法正确拟合训练数据,导致在样本内和样本外的表现都很差。一个偏差模型的经典例子是当我们尝试用简单的线性回归来拟合一个正弦函数时。该模型无法拟合正弦函数,因为它缺乏所需的复杂度。因此,它无法在样本内或样本外表现良好。这个问题被称为欠拟合。下图提供了一个图形示例:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/7a3d6234-f46c-4cad-8796-7ab8628778b7.png

对正弦函数数据的有偏线性回归模型

偏差的数学公式是目标值与期望预测值之间的差异:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/65ccbf7c-cef5-44d5-9493-84f1cfd3b861.png

什么是方差?

方差指的是个体在一个群体中的差异程度。同样,方差是统计学中的一个概念。从一个群体中抽取样本时,方差表示每个个体的数值与平均值的偏差程度。

在机器学习中,方差指的是模型对数据变化的敏感性或变动性。这意味着,高方差模型通常能够很好地拟合训练数据,从而在训练集上取得较高的表现,但在测试集上表现较差。这是由于模型的复杂性。例如,如果决策树为训练数据集中的每一个实例都创建一条规则,那么该决策树可能会有较高的方差。这被称为过拟合。下图展示了在前述数据集上训练的决策树。蓝色点代表训练数据,橙色点代表测试数据。

如图所示,模型能够完美拟合训练数据,但在测试数据上表现较差:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/30604a53-1e14-4353-ad9b-39214f6d22b4.png

高方差的决策树模型在正弦函数上的表现

方差的数学公式如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/257b5bbd-0482-441b-a392-ba62d837f00c.png

本质上,这就是人口方差的标准公式,假设我们的人口是由模型组成的,因为这些模型是由机器学习算法生成的。例如,正如我们在第一章《机器学习基础》中看到的,神经网络的训练结果可能不同,这取决于它们的初始权重。如果我们考虑所有具有相同架构但初始权重不同的神经网络,通过训练它们,我们将得到一组不同的模型。

权衡

偏差和方差是组成模型误差的三个主要组成部分中的两个。第三个是不可减少的误差,通常归因于数据中的固有随机性或变异性。模型的总误差可以分解如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/78620b7b-b43f-4b03-a677-6c991c9c441f.png

正如我们之前所见,偏差和方差都源自同一个因素:模型复杂度。偏差源于模型复杂度过低和自由度不足,而方差则在复杂模型中表现得更为突出。因此,不可能在不增加方差的情况下减少偏差,反之亦然。然而,存在一个复杂度的最优点,在这个点上,偏差和方差达到了最优的权衡,误差最小。当模型的复杂度达到这个最优点(下图中的红色虚线)时,模型在训练集和测试集上的表现都是最佳的。正如下图所示,误差永远不可能被减少到零。

此外,尽管有些人可能认为减少偏差,即使以增加方差为代价会更好,但显然即便模型没有偏差,由于方差不可避免地引起的误差,模型也不会表现得更好:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/b59162a8-d7de-44fe-8b33-b599478bb9d8.png

偏差-方差权衡及其对误差的影响

下图展示了完美模型,具有最小的偏差和方差的结合,或者说是可减少的误差。尽管该模型并没有完全拟合数据,但这是由于数据集中的噪声。如果我们尝试更好地拟合训练数据,将会引起过拟合(方差)。如果我们进一步简化模型,将会引起欠拟合(偏差):

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/30ecba78-5dd9-4246-83f7-365dc7db0a82.png

对于我们的数据,完美的模型是一个正弦函数

集成学习

集成学习涉及一组机器学习方法,旨在通过结合多个模型来提高算法的预测性能。我们将分析使用这些方法来解决高偏差和方差问题的动机。此外,我们还将介绍识别机器学习模型中偏差和方差的方法,以及集成学习方法的基本分类。

动机

集成学习旨在解决偏差和方差的问题。通过结合多个模型,我们可以减少集成的误差,同时保留各个独立模型的复杂性。如前所述,每个模型误差都有一定的下限,这与模型的复杂性有关。

此外,我们提到同一个算法由于初始条件、超参数和其他因素的不同,可能会产生不同的模型。通过结合不同且多样化的模型,我们可以减少群体的预期误差,同时每个独立的模型保持不变。这是由于统计学原理,而非纯粹的学习。

为了更好地展示这一点,假设我们有一个由 11 个基学习器组成的集成,用于分类,每个学习器的误分类(误差)概率为err=0.15 或 15%。现在,我们想要创建一个简单的集成。我们始终假设大多数基学习器的输出是正确的。假设它们是多样化的(在统计学中是无关的),那么大多数学习器出错的概率是 0.26%:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/83545e56-347d-450e-82b7-46f736d3c1f0.png

正如显而易见的,随着我们向集成中添加更多的基学习器,集成的准确度也会提高,前提是每个学习器之间彼此独立。当然,这一点越来越难以实现。此外,还存在递减收益法则。每增加一个不相关的基学习器,减少的整体误差会比之前添加的基学习器少。下图展示了多个不相关基学习器的集成误差百分比。显然,添加两个不相关基学习器时,误差的减少最大:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/4da2f13b-6bd6-4d1d-915f-6a2600b83d12.png

基学习器数量与集成误差之间的关系

识别偏差和方差

尽管偏差和方差有理论公式,但很难计算它们的实际值。估算它们的一个简单方法是使用学习曲线和验证曲线进行经验计算。

验证曲线

验证曲线指的是在不同超参数条件下,算法的实际表现。对于每个超参数值,我们执行 K 折交叉验证,并存储样本内表现和样本外表现。然后,我们计算并绘制每个超参数值的样本内和样本外表现的均值和标准差。通过检查相对和绝对表现,我们可以评估模型的偏差和方差水平。

借用来自第一章KNeighborsClassifier示例,*《机器学习复习》*中,我们对其进行了修改,以便尝试不同的邻居数。我们首先加载所需的库和数据。请注意,我们从sklearn.model_selection导入了validation_curve,这是 scikit-learn 自带的验证曲线实现:

# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import validation_curve
from sklearn.neighbors import KNeighborsClassifier
bc = load_breast_cancer()

接下来,我们定义我们的特征和目标(xy),以及我们的基学习器。此外,我们使用param_range = [2,3,4,5]定义参数搜索空间,并使用validation_curve。为了使用它,我们必须定义基学习器、特征、目标、我们希望测试的参数名称以及待测试的参数值。此外,我们还使用cv=10定义交叉验证的 K 折数,并设置我们希望计算的度量标准为scoring="accuracy"

# --- SECTION 2 ---
# Create in-sample and out-of-sample scores
x, y = bc.data, bc.target
learner = KNeighborsClassifier()
param_range = [2,3,4,5]
train_scores, test_scores = validation_curve(learner, x, y,
                                             param_name='n_neighbors',
                                             param_range=param_range,
                                             cv=10,
                                             scoring="accuracy")

然后,我们计算样本内表现(train_scores)和样本外表现(test_scores)的均值和标准差:

# --- SECTION 3 ---
# Calculate the average and standard deviation for each hyperparameter
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)

最后,我们绘制均值和标准差。我们使用plt.plot绘制均值曲线。为了绘制标准差,我们创建一个透明矩形,包围这些曲线,矩形的宽度等于每个超参数值点的标准差。这是通过使用plt.fill_between实现的,方法是将值点作为第一个参数,最低矩形点作为第二个参数,最高点作为第三个参数。此外,alpha=0.1指示matplotlib使矩形透明(将矩形的颜色与背景按照 10%-90%的比例进行混合):

第 3 和第四部分改编自scikit-learn.org/stable/auto_examples/model_selection/plot_validation_curve.html中的 scikit-learn 示例。

# --- SECTION 4 ---
# Plot the scores
plt.figure()
plt.title('Validation curves')
# Plot the standard deviations
plt.fill_between(param_range, train_scores_mean - train_scores_std,
                 train_scores_mean + train_scores_std, alpha=0.1,
                 color="C1")
plt.fill_between(param_range, test_scores_mean - test_scores_std,
                 test_scores_mean + test_scores_std, alpha=0.1, color="C0")

# Plot the means
plt.plot(param_range, train_scores_mean, 'o-', color="C1",
         label="Training score")
plt.plot(param_range, test_scores_mean, 'o-', color="C0",
         label="Cross-validation score")
plt.xticks(param_range)
plt.xlabel('Number of neighbors')
plt.ylabel('Accuracy')
plt.legend(loc="best")
plt.show()

脚本最终输出以下结果。当曲线之间的距离缩小时,方差通常会减少。当它们远离期望的准确度时(考虑到不可减少的误差),偏差增加。

此外,相对标准差也是方差的一个指标:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/2029909d-0236-464d-b286-61b25feaed3b.png

K-近邻算法的验证曲线,邻居数量从 2 到 5

以下表格展示了基于验证曲线的偏差和方差识别:

曲线之间的距离高方差低方差
与期望准确度的距离高偏差低偏差
相对矩形面积比高方差低方差

基于验证曲线的偏差和方差识别

学习曲线

另一种识别偏差和方差的方法是生成学习曲线。与验证曲线类似,我们通过交叉验证生成一系列的样本内和样本外性能统计数据。我们不再尝试不同的超参数值,而是利用不同量的训练数据。通过检查样本内和样本外性能的均值和标准差,我们可以了解模型中固有的偏差和方差。

Scikit-learn 在sklearn.model_selection模块中实现了学习曲线,名为learning_curve。我们将再次使用第一章中机器学习复习KNeighborsClassifier示例。首先,我们导入所需的库并加载乳腺癌数据集:

# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import learning_curve
bc = load_breast_cancer()

接下来,我们定义每个交叉验证集将使用的训练实例数量为train_sizes = [50, 100, 150, 200, 250, 300],实例化基础学习器,并调用learning_curve。该函数返回一个包含训练集大小、样本内性能得分和样本外性能得分的元组。该函数接受基础学习器、数据集特征和目标,以及训练集大小作为参数,其中train_sizes=train_sizes,并且交叉验证的折数为cv=10

# --- SECTION 2 ---
# Create in-sample and out-of-sample scores
x, y = bc.data, bc.target
learner = KNeighborsClassifier()
train_sizes = [50, 100, 150, 200, 250, 300]
train_sizes, train_scores, test_scores = learning_curve(learner, x,                                 y,  train_sizes=train_sizes, cv=10)

再次,我们计算样本内和样本外性能的均值和标准差:

# --- SECTION 3 ---
# Calculate the average and standard deviation for each hyperparameter
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)

最后,我们像之前一样,将均值和标准差绘制为曲线和矩形:

# --- SECTION 4 ---
# Plot the scores
plt.figure()
plt.title('Learning curves')
# Plot the standard deviations
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
 train_scores_mean + train_scores_std, alpha=0.1,
 color="C1")
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
 test_scores_mean + test_scores_std, alpha=0.1, color="C0")

# Plot the means
plt.plot(train_sizes, train_scores_mean, 'o-', color="C1",
 label="Training score")
plt.plot(train_sizes, test_scores_mean, 'o-', color="C0",
 label="Cross-validation score")

plt.xticks(train_sizes)
plt.xlabel('Size of training set (instances)')
plt.ylabel('Accuracy')
plt.legend(loc="best")
plt.show()

最终输出如下图所示。模型似乎在前 200 个训练样本中降低了其方差。之后,均值似乎开始发散,同时交叉验证得分的标准差增加,从而表明方差的增加。

请注意,尽管两条曲线在训练集(至少包含 150 个实例)的准确率都超过 90%,但这并不意味着低偏差。高度可分的数据集(良好的质量数据,噪声低)往往会产生这样的曲线——无论我们选择什么样的算法组合和超参数。此外,噪声数据集(例如,具有相同特征但目标不同的实例)将无法生成高准确率的模型——无论我们使用什么技术。

因此,偏差应通过将学习曲线和验证曲线与期望的准确率(考虑到数据集质量认为可以达到的准确率)进行比较来衡量,而不是通过其绝对值:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/d1e5331a-a5fe-4ce5-bc55-42fedbddc08e.png

K 近邻的学习曲线,50 到 300 个训练实例

集成方法

集成方法分为两大类或分类法:生成方法和非生成方法。非生成方法侧重于组合一组预训练模型的预测。这些模型通常是独立训练的,集成算法决定如何组合它们的预测。基础分类器不受其存在于集成中的影响。

本书将涵盖两种主要的非生成方法:投票和堆叠。如其名所示(参见第三章,投票),投票指的是允许模型投票以产生单一答案的技术,类似于个人在国家选举中的投票方式。最受欢迎(投票最多)的答案被选为赢家。第四章,堆叠,则指的是利用一个模型(元学习器)来学习如何最好地组合基础学习器的预测。尽管堆叠涉及生成一个新模型,但它不影响基础学习器,因此它是一种非生成方法。

另一方面,生成方法能够生成并影响它们所使用的基本学习器。它们可以调整其学习算法或用于训练它们的数据集,以确保多样性和高模型性能。此外,一些算法可以在模型中引入随机性,从而进一步加强多样性。

本书中我们将介绍的主要生成方法包括袋装(bagging)、提升(boosting)和随机森林(random forests)。提升是一种主要针对偏差模型的技术,其基本思想是顺序生成模型,使得每个新模型都能解决前一个模型中的偏差。因此,通过迭代修正之前的错误,最终的集成模型偏差会显著降低。袋装旨在减少方差。袋装算法对训练数据集中的实例进行重采样,创建许多源自同一数据集的独立且多样化的数据集。随后,在每个采样的数据集上训练单独的模型,从而强制集成模型之间的多样性。最后,随机森林与袋装相似,都是对训练数据集进行重采样。不同的是,它采样的是特征,而不是实例,这样可以生成更多样化的树,因为与目标高度相关的特征可能在许多树中缺失。

集成学习中的难点

虽然集成学习可以显著提高机器学习模型的性能,但它也有成本。正确实现集成学习存在一些困难和缺点,接下来将讨论其中的一些困难和缺点。

弱数据或噪声数据

一个成功模型最重要的要素是数据集。如果数据中包含噪声或不完整信息,那么没有任何一种机器学习技术能够生成一个高性能的模型。

让我们通过一个简单的例子来说明这一点。假设我们研究的是汽车的种群(统计学意义上的),并收集了关于颜色、形状和制造商的数据。对于任一变量,生成非常准确的模型都是困难的,因为很多汽车颜色和形状相同,但制造商不同。以下表格展示了这个样本数据集。

任何模型能够做到的最好结果是达到 33%的分类准确率,因为对于任一给定的特征组合,都有三种可能的选择。向数据集添加更多特征可以显著提高模型的性能。而向集成中添加更多模型则无法提高性能:

颜色形状制造商
黑色轿车宝马
黑色轿车奥迪
黑色轿车阿尔法·罗密欧
蓝色两厢车福特
蓝色两厢车欧宝
蓝色两厢车菲亚特

汽车数据集

理解可解释性

通过使用大量模型,模型的可解释性大大降低。例如,单个决策树可以通过简单地跟随每个节点的决策,轻松地解释它是如何产生预测的。另一方面,很难解释为什么一个由 1000 棵树组成的集成预测了一个单一的值。此外,根据集成方法的不同,可能需要解释的不仅仅是预测过程本身。集成是如何以及为什么选择训练这些特定的模型的?为什么它没有选择训练其他模型?为什么它没有选择训练更多的模型?

当模型的结果需要向观众呈现时,尤其是面对技术水平不高的观众时,简单且易于解释的模型可能是更好的解决方案。

此外,当预测结果还需要包括概率(或置信度)时,一些集成方法(如提升法)往往会给出较差的概率估计:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/ccb77e87-ebd1-47a0-b835-3e054f2bb8f3.png

单棵树与 1000 棵树的可解释性

计算成本

集成模型的另一个缺点是它们带来的计算成本。训练单个神经网络的计算成本很高。训练 1000 个神经网络则需要 1000 倍的计算资源。此外,一些方法本质上是顺序的。这意味着无法利用分布式计算的优势。相反,每个新的模型必须在前一个模型完成后才开始训练。这不仅增加了计算成本,还对模型开发过程带来了时间上的惩罚。

计算成本不仅会阻碍开发过程;当集成模型投入生产时,推理时间也会受到影响。如果集成由 1000 个模型组成,那么所有这些模型必须输入新的数据,生成预测,然后将这些预测结合起来,才能产生集成输出。在延迟敏感的环境中(如金融交易、实时系统等),通常期望亚毫秒的执行时间,因此几微秒的延迟增加可能会造成巨大的差异。

选择合适的模型

最后,组成集成模型的模型必须具备一定的特征。没有意义从多个相同的模型中创建集成。生成方法可以生成自己的模型,但所使用的算法以及其初始超参数通常由分析师选择。此外,模型的可实现多样性取决于多个因素,例如数据集的大小和质量,以及学习算法本身。

一个与数据生成过程类似行为的单一模型通常会在准确性和延迟方面优于任何集成模型。在我们的偏差-方差示例中,简单的正弦函数始终会优于任何集成模型,因为数据是从同一个函数生成的,只是添加了一些噪声。许多线性回归的集成可能能够逼近正弦函数,但它们总是需要更多时间来训练和执行。此外,它们将无法像正弦函数那样很好地泛化(预测样本外数据)。

概要

在本章中,我们介绍了偏差和方差的概念及其之间的权衡。这些对于理解模型可能在样本内或样本外表现不佳的原因至关重要。然后,我们介绍了集成学习的概念和动机,以及如何在模型中识别偏差和方差,以及集成学习方法的基本类别。我们介绍了使用 scikit-learn 和 matplotlib 测量和绘制偏差和方差的方法。最后,我们讨论了实施集成学习方法的困难和缺点。记住的一些关键点如下。

高偏差模型通常在样本内表现不佳。这也被称为欠拟合。这是由于模型的简单性(或缺乏复杂性)导致的。高方差模型通常在样本内表现良好,但在样本外泛化或表现良好较难,这被称为过拟合。这通常是由于模型的不必要复杂性引起的。偏差-方差权衡指的是随着模型复杂性的增加,其偏差减少,而方差增加的事实。集成学习旨在通过结合许多不同模型的预测来解决高偏差或方差问题。这些模型通常被称为基学习器。对于模型选择,验证曲线指示了模型在给定一组超参数的情况下在样本内和样本外的表现。学习曲线与验证曲线相同,但它们使用不同的训练集大小而不是一组超参数。训练曲线和测试曲线之间的显著距离表示高方差。测试曲线周围的大矩形区域也表示高方差。两条曲线与目标准确率之间的显著距离表示高偏差。生成方法可以控制其基学习器的生成和训练;非生成方法则不能。当数据质量差或模型相关时,集成学习对性能可能会产生微乎其微或负面影响。它可能会对模型的解释能力和所需的计算资源产生负面影响。

在下一章中,我们将介绍投票集成,以及如何将其用于回归和分类问题。

第二部分:非生成方法

在本节中,我们将介绍集成学习的最简单方法。

本节包括以下章节:

  • 第三章,投票

  • 第四章,堆叠

第三章:投票

所有集成学习方法中最直观的就是多数投票。它之所以直观,是因为其目标是输出基学习器预测中最流行(或得票最多)的结果。本章将介绍关于多数投票的基本理论及实际实现。通过本章学习后,你将能够做到以下几点:

  • 理解多数投票

  • 理解硬投票和软投票的区别,以及它们各自的优缺点

  • 在 Python 中实现两种版本

  • 使用投票技术来提高分类器在乳腺癌数据集上的表现

技术要求

你需要具备基本的机器学习技术和算法知识。此外,还要求了解 Python 语法规范,最后,熟悉 NumPy 库将大大帮助读者理解一些自定义算法的实现。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter03

查看以下视频以查看代码实例:bit.ly/2M52VY7

硬投票和软投票

多数投票是最简单的集成学习技术,它允许将多个基学习器的预测结果结合起来。类似于选举的工作原理,算法假设每个基学习器是一个选民,每个类别是一个竞争者。算法根据投票情况来选举获胜的竞争者。结合多个预测的投票方法主要有两种:一种是硬投票,另一种是软投票。我们在这里介绍这两种方法。

硬投票

硬投票通过假设得票最多的类别为胜者来结合多个预测。在一个简单的两类三基学习者的情况下,如果某个目标类别至少有两个投票,它就成为集成模型的输出,如下图所示。实现一个硬投票分类器就像是统计每个目标类别的投票数一样简单:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/cf089773-85dd-4f2a-84de-a4c10f12254f.png

使用两类和三个基学习者进行投票

例如,假设有三个不同的基学习者,他们在预测一个样本是否属于三个类别中的某一个,并给出相应的概率(表 1)。

在下表中,每个学习者预测实例属于某个类别的概率:

类别 A类别 B类别 C
学习者 10.50.30.2
学习者 200.480.52
学习者 30.40.30.3

分配的类别概率

在此示例中,类别 A 有两个投票,而类别 C 只有一个投票。根据硬投票,类别 A 将成为集成的预测结果。这是一种非常稳健的基学习者合并方法,尽管它没有考虑到某些类别可能仅因略微优于其他类别而被某个基学习者选择。

软投票

软投票考虑了预测类别的概率。为了合并预测,软投票计算每个类别的平均概率,并假设胜者是具有最高平均概率的类别。在三个基学习者和两个类别的简单情况下,我们必须考虑每个类别的预测概率,并在三个学习者中求其平均:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/9a69ad7d-0f1d-4116-abd2-572423922f69.png

软投票:两个类别和三个基学习者

使用我们之前的例子,并通过对表 1中每一列的平均值求平均,我们可以扩展该表,添加一行用于显示平均概率。

以下表格显示了每个学习者对每个类别的预测概率,以及平均概率:

类别 A类别 B类别 C
学习者 10.50.30.2
学习者 200.480.52
学习者 30.40.30.3
平均0.30.360.34

每个学习者对每个类别的预测概率,以及平均概率

如我们所见,类别 A 的平均概率为 0.3,类别 B 的平均概率为 0.36,类别 C 的平均概率为 0.34,因此类别 B 获胜。注意,类别 B 并不是由任何基学习者选作预测类别,但通过合并预测概率,类别 B 成为预测中最好的折中选择。

为了让软投票比硬投票更有效,基分类器必须提供关于样本属于特定类别的概率的良好估计。如果这些概率没有意义(例如,如果它们总是对于某一类别为 100%,而对于所有其他类别为 0%),那么软投票可能会比硬投票更糟糕。

关于投票的说明:正如 Kenneth Arrow 博士通过其不可能性定理所证明的那样,完美的投票系统是不可实现的。然而,某些类型的投票系统能够更好地反映一个群体的偏好。软投票更能反映个体学习者的偏好,因为它考虑的是评分(概率),而不是排名(预测类别)。

有关不可能性定理的更多内容,请参见《社会福利概念中的困难》Arrow, K.J., 1950政治经济学杂志,58(4),第 328-346 页。

​Python 实现

在 Python 中实现硬投票的最简单方法是使用scikit-learn来创建基学习者,训练它们以适应某些数据,并将它们的预测结果结合起来应用于测试数据。为此,我们将按照以下步骤进行:

  1. 加载数据并将其拆分为训练集和测试集

  2. 创建一些基础学习器

  3. 在训练数据上训练它们

  4. 为测试数据生成预测

  5. 使用硬投票合并预测结果

  6. 将各个学习器的预测结果与合并后的预测结果与实际的正确类别(ground truth)进行比较

尽管 scikit-learn 提供了投票的实现,通过创建自定义实现,我们可以更容易理解算法的工作原理。此外,这还将帮助我们更好地理解如何处理和分析基础学习器的输出。

自定义硬投票实现

为了实现自定义硬投票解决方案,我们将使用三个基础学习器:感知机(一个单神经元的神经网络)、支持向量机SVM)和最近邻。它们分别包含在sklearn.linear_modelsklearn.svmsklearn.neighbors包中。此外,我们将使用 NumPy 的argmax函数。此函数返回数组(或类数组数据结构)中最大值元素的索引。最后,accuracy_score将计算每个分类器在我们测试数据上的准确性:

# --- SECTION 1 ---
# Import the required libraries
from sklearn import datasets, linear_model, svm, neighbors
from sklearn.metrics import accuracy_score
from numpy import argmax
# Load the dataset
breast_cancer = datasets.load_breast_cancer()
x, y = breast_cancer.data, breast_cancer.target

然后,我们实例化我们的基础学习器。我们精心挑选了它们的超参数,以确保它们在多样性上有所体现,从而能够产生一个表现良好的集成模型。由于breast_cancer是一个分类数据集,我们使用SVC,即 SVM 的分类版本,以及KNeighborsClassifierPerceptron。此外,我们将Perceptron的随机状态设置为 0,以确保示例的可复现性:

# --- SECTION 2 ---
# Instantiate the learners (classifiers)
learner_1 = neighbors.KNeighborsClassifier(n_neighbors=5)
learner_2 = linear_model.Perceptron(tol=1e-2, random_state=0)
learner_3 = svm.SVC(gamma=0.001)

我们将数据拆分为训练集和测试集,使用 100 个实例作为测试集,并在训练集上训练我们的基础学习器:

# --- SECTION 3 ---
# Split the train and test samples
test_samples = 100
x_train, y_train = x[:-test_samples], y[:-test_samples]
x_test, y_test = x[-test_samples:], y[-test_samples:]

# Fit learners with the train data
learner_1.fit(x_train, y_train)
learner_2.fit(x_train, y_train)
learner_3.fit(x_train, y_train)

通过将每个基础学习器的预测存储在predictions_1predictions_2predictions_3中,我们可以进一步分析并将它们合并成我们的集成模型。请注意,我们分别训练了每个分类器;此外,每个分类器都会独立地对测试数据进行预测。正如在第二章《集成学习入门》中提到的那样,这是非生成性集成方法的主要特点

#--- SECTION 4 ---
# Each learner predicts the classes of the test data
predictions_1 = learner_1.predict(x_test)
predictions_2 = learner_2.predict(x_test)
predictions_3 = learner_3.predict(x_test)

根据预测结果,我们将每个基学习器对每个测试实例的预测结果进行合并。hard_predictions 列表将包含集成模型的预测结果(输出)。通过 for i in range(test_samples) 遍历每个测试样本,我们统计每个类别从三个基学习器收到的投票总数。由于数据集仅包含两个类别,我们需要一个包含两个元素的列表:counts = [0 for _ in range(2)]。在 # --- SECTION 3 --- 中,我们将每个基学习器的预测结果存储在一个数组中。该数组的每个元素包含实例预测类别的索引(在我们这里是 0 和 1)。因此,我们通过将 counts[predictions_1[i]] 中相应元素的值加 1 来统计基学习器的投票数。接着,argmax(counts) 会返回获得最多投票的元素(类别):

# --- SECTION 5 ---
# We combine the predictions with hard voting
hard_predictions = []
# For each predicted sample
for i in range(test_samples):
    # Count the votes for each class
    counts = [0 for _ in range(2)]
    counts[predictions_1[i]] = counts[predictions_1[i]]+1
    counts[predictions_2[i]] = counts[predictions_2[i]]+1
    counts[predictions_3[i]] = counts[predictions_3[i]]+1
    # Find the class with most votes
    final = argmax(counts)
    # Add the class to the final predictions
    hard_predictions.append(final) 

最后,我们通过 accuracy_score 计算每个基学习器以及集成模型的准确度,并将结果打印在屏幕上:

# --- SECTION 6 ---
# Accuracies of base learners
print('L1:', accuracy_score(y_test, predictions_1))
print('L2:', accuracy_score(y_test, predictions_2))
print('L3:', accuracy_score(y_test, predictions_3))
# Accuracy of hard voting
print('-'*30)
print('Hard Voting:', accuracy_score(y_test, hard_predictions))

最终输出如下:

L1: 0.94
L2: 0.93
L3: 0.88
------------------------------
Hard Voting: 0.95

使用 Python 分析我们的结果

最终的准确度比三种分类器中最好的分类器(k-最近邻 (k-NN) 分类器)高出 1%。我们可以通过可视化学习器的错误来分析集成模型为何以这种特定方式表现。

首先,我们 import matplotlib 并使用特定的 seaborn-paper 绘图风格,方法是 mpl.style.use('seaborn-paper')

# --- SECTION 1 ---
# Import the required libraries
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.style.use('seaborn-paper')

然后,我们通过从预测结果中减去实际目标值来计算错误。因此,每次学习器预测为正类(1),而真实类别为负类(0)时,我们得到 -1;每次学习器预测为负类(0),而真实类别为正类(1)时,我们得到 1。如果预测正确,我们得到 0:

# --- SECTION 2 ---
# Calculate the errors 
errors_1 = y_test-predictions_1
errors_2 = y_test-predictions_2
errors_3 = y_test-predictions_3

对于每个基学习器,我们绘制其预测错误的实例。我们的目标是绘制 xy 列表的散点图。这些列表将包含实例编号(x 列表)和错误类型(y 列表)。通过 plt.scatter,我们可以使用上述列表来指定点的坐标,并且可以指定这些点的表现方式。这一点非常重要,因为我们可以同时可视化所有分类器的错误及其相互关系。

每个点的默认形状是圆形。通过指定 marker 参数,我们可以改变这个形状。此外,通过 s 参数,我们可以指定标记的大小。因此,第一个学习器(k-NN)将具有大小为 120 的圆形,第二个学习器(感知器)将具有大小为 60 的 x 形状,而第三个学习器(SVM)将具有大小为 20 的圆形。if not errors_*[i] == 0 的保护条件确保我们不会存储正确分类的实例:

# --- SECTION 3 ---
# Discard correct predictions and plot each learner's errors
x=[]
y=[]
for i in range(len(errors_1)):
    if not errors_1[i] == 0:
        x.append(i)
        y.append(errors_1[i])
plt.scatter(x, y, s=120, label='Learner 1 Errors')

x=[]
y=[]
for i in range(len(errors_2)):
     if not errors_2[i] == 0:
         x.append(i)
         y.append(errors_2[i])
plt.scatter(x, y, marker='x', s=60, label='Learner 2 Errors')

x=[]
y=[]
for i in range(len(errors_3)):
    if not errors_3[i] == 0:
        x.append(i)
        y.append(errors_3[i])
plt.scatter(x, y, s=20, label='Learner 3 Errors')

最后,我们指定图表的标题和标签,并绘制图例:

plt.title('Learner errors')
plt.xlabel('Test sample')
plt.ylabel('Error')
plt.legend()
plt.show()

如下所示,有五个样本至少有两个学习器预测了错误的类别。这五个案例是 100 个样本中集成预测错误的 5 个,因为最投票的类别是错的,从而导致 95%的准确性。在所有其他情况下,三个学习器中有两个预测了正确的类别,因此集成模型预测了正确的类别,因为它是最投票的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/0ba4d397-fc7a-43f0-8009-ef44003bcf9a.png

学习器在测试集上的错误

使用 scikit-learn

scikit-learn 库包含许多集成学习算法,包括投票。为了实现硬投票,我们将遵循与之前相同的程序,不过这次我们不再自己实现个别的拟合、预测和投票过程。而是使用提供的实现,这使得训练和测试变得快速而简单。

硬投票实现

与我们自定义实现类似,我们导入所需的库,划分训练和测试数据,并实例化我们的基础学习器。此外,我们从sklearn.ensemble包中导入 scikit-learn 的VotingClassifier投票实现,如下所示:

# --- SECTION 1 ---
# Import the required libraries
from sklearn import datasets, linear_model, svm, neighbors
from sklearn.ensemble import VotingClassifier
from sklearn.metrics import accuracy_score
# Load the dataset
breast_cancer = datasets.load_breast_cancer()
x, y = breast_cancer.data, breast_cancer.target

# Split the train and test samples
test_samples = 100
x_train, y_train = x[:-test_samples], y[:-test_samples]
x_test, y_test = x[-test_samples:], y[-test_samples:]

# --- SECTION 2 ---
# Instantiate the learners (classifiers)
learner_1 = neighbors.KNeighborsClassifier(n_neighbors=5)
learner_2 = linear_model.Perceptron(tol=1e-2, random_state=0)
learner_3 = svm.SVC(gamma=0.001)

在上述代码之后,我们实例化VotingClassifier类,传入一个包含基础分类器名称和对象的元组列表作为参数。请注意,如果将参数传递在列表外部,将会导致错误:

# --- SECTION 3 ---
# Instantiate the voting classifier
voting = VotingClassifier([('KNN', learner_1),
                           ('Prc', learner_2),
                           ('SVM', learner_3)])

现在,实例化了分类器后,我们可以像使用任何其他分类器一样使用它,而无需单独处理每个基础学习器。接下来的两部分执行了所有基础学习器的拟合和预测,以及为每个测试实例计算最投票的类别:

# --- SECTION 4 ---
# Fit classifier with the training data
voting.fit(x_train, y_train)

# --- SECTION 5 ---
# Predict the most voted class
hard_predictions = voting.predict(x_test)

最后,我们可以打印集成模型的准确性:

# --- SECTION 6 ---
# Accuracy of hard voting
print('-'*30)
print('Hard Voting:', accuracy_score(y_test, hard_predictions))

这与我们自定义实现相同:

------------------------------
Hard Voting: 0.95

请注意,VotingClassifier不会拟合作为参数传入的对象,而是会克隆它们并拟合克隆的对象。因此,如果你尝试打印每个基础学习器在测试集上的准确性,你将得到NotFittedError,因为你访问的对象实际上并没有被拟合。这是使用 scikit-learn 的实现而非自定义实现的唯一缺点。

软投票实现

Scikit-learn 的实现也支持软投票。唯一的要求是基础学习器必须实现predict_proba函数。在我们的示例中,Perceptron完全没有实现该函数,而SVC仅在传递probability=True参数时才会生成概率。考虑到这些限制,我们将Perceptron替换为sklearn.naive_bayes包中实现的朴素贝叶斯分类器。

要实际使用软投票,VotingClassifier对象必须使用voting='soft'参数进行初始化。除了这里提到的更改外,大部分代码保持不变。加载库和数据集,并按如下方式进行训练/测试集划分:

# --- SECTION 1 ---
# Import the required libraries
from sklearn import datasets, naive_bayes, svm, neighbors
from sklearn.ensemble import VotingClassifier
from sklearn.metrics import accuracy_score
# Load the dataset
breast_cancer = datasets.load_breast_cancer()
x, y = breast_cancer.data, breast_cancer.target

# Split the train and test samples
test_samples = 100
x_train, y_train = x[:-test_samples], y[:-test_samples]
x_test, y_test = x[-test_samples:], y[-test_samples:]

实例化基学习器和投票分类器。我们使用一个高斯朴素贝叶斯分类器,命名为GaussianNB。注意,我们使用probability=True,以便GaussianNB对象能够生成概率:

# --- SECTION 2 ---
# Instantiate the learners (classifiers)
learner_1 = neighbors.KNeighborsClassifier(n_neighbors=5)
learner_2 = naive_bayes.GaussianNB()
learner_3 = svm.SVC(gamma=0.001, probability=True)

# --- SECTION 3 ---
# Instantiate the voting classifier
voting = VotingClassifier([('KNN', learner_1),
                           ('NB', learner_2),
                           ('SVM', learner_3)],
                            voting='soft')

我们拟合VotingClassifier和单独的学习器。我们希望分析我们的结果,正如前面提到的,分类器不会拟合我们传入的对象,而是会克隆它们。因此,我们需要手动拟合我们的学习器,如下所示:

# --- SECTION 4 ---
# Fit classifier with the training data
voting.fit(x_train, y_train)
learner_1.fit(x_train, y_train)
learner_2.fit(x_train, y_train)
learner_3.fit(x_train, y_train)

我们使用投票集成和单独的学习器预测测试集的目标:

# --- SECTION 5 ---
# Predict the most probable class
hard_predictions = voting.predict(x_test)

# --- SECTION 6 ---
# Get the base learner predictions
predictions_1 = learner_1.predict(x_test)
predictions_2 = learner_2.predict(x_test)
predictions_3 = learner_3.predict(x_test)

最后,我们打印每个基学习器的准确率以及软投票集成的准确率:

# --- SECTION 7 ---
# Accuracies of base learners
print('L1:', accuracy_score(y_test, predictions_1))
print('L2:', accuracy_score(y_test, predictions_2))
print('L3:', accuracy_score(y_test, predictions_3))
# Accuracy of hard voting
print('-'*30)
print('Hard Voting:', accuracy_score(y_test, hard_predictions))

最终输出如下:

L1: 0.94
L2: 0.96
L3: 0.88
------------------------------
Hard Voting: 0.94

分析我们的结果

如图所示,软投票的准确率比最佳学习器低 2%,并与第二最佳学习器持平。我们希望像分析硬投票自定义实现的性能一样分析我们的结果。但由于软投票考虑了预测的类别概率,我们不能使用相同的方法。相反,我们将绘制每个基学习器预测的每个实例作为正类的概率,以及集成模型的平均概率。

再次,我们import matplotlib并设置绘图样式:

# --- SECTION 1 ---
# Import the required libraries
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.style.use('seaborn-paper')

我们通过errors = y_test-hard_predictions计算集成模型的误差,并使用predict_proba(x_test)函数获取每个基学习器的预测概率。所有基学习器都实现了这个函数,因为这是在软投票集成中使用它们的要求:


# --- SECTION 2 ---
# Get the wrongly predicted instances
# and the predicted probabilities for the whole test set
errors = y_test-hard_predictions

probabilities_1 = learner_1.predict_proba(x_test)
probabilities_2 = learner_2.predict_proba(x_test)
probabilities_3 = learner_3.predict_proba(x_test)

之后,对于每个错误分类的实例,我们存储该实例属于类 0 的预测概率。我们也对每个基学习器以及它们的平均值实现此功能。每个probabilities_*数组是一个二维数组,每行包含对应实例属于类 0 或类 1 的预测概率。因此,存储其中一个就足够了。如果数据集有N个类别,我们至少需要存储N-1 个概率,才能获得清晰的视图:

# --- SECTION 2 ---
# Store the predicted probability for 
# each wrongly predicted instance, for each base learner
# as well as the average predicted probability
#
x=[]
y_1=[]
y_2=[]
y_3=[]
y_avg=[]

for i in range(len(errors)):
    if not errors[i] == 0:
         x.append(i)
         y_1.append(probabilities_1[i][0])
         y_2.append(probabilities_2[i][0])
         y_3.append(probabilities_3[i][0])
         y_avg.append((probabilities_1[i][0]+
                       probabilities_2[i][0]+probabilities_3[i][0])/3)

最后,我们使用plt.bar将概率绘制为不同宽度的条形图。这确保了任何重叠的条形图仍然可以被看到。第三个plt.bar参数决定了条形图的宽度。我们使用散点图标记平均概率为黑色“X”,并确保它绘制在任何条形图之上,使用zorder=10。最后,我们绘制一条在 0.5 概率处的阈值线,使用plt.plot(y, c='k', linestyle='--'),确保它为黑色虚线,c='k', linestyle='--'。如果平均概率高于该线,样本将被分类为正类,如下所示:


# --- SECTION 3 ---
# Plot the predicted probaiblity of each base learner as 
# a bar and the average probability as an X
plt.bar(x, y_1, 3, label='KNN')
plt.bar(x, y_2, 2, label='NB')
plt.bar(x, y_3, 1, label='SVM')
plt.scatter(x, y_avg, marker='x', c='k', s=150, 
            label='Average Positive', zorder=10)

y = [0.5 for x in range(len(errors))]
plt.plot(y, c='k', linestyle='--')

plt.title('Positive Probability')
plt.xlabel('Test sample')
plt.ylabel('probability')
plt.legend()
plt.show()

上述代码输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/557d63bc-fadb-42d6-bdea-ace7257b02da.png

测试集的预测和平均概率

如我们所见,只有两个样本具有极端的平均概率(样本 22 的 p = 0.98 和样本 67 的 p = 0.001)。其余四个样本的概率接近 50%。在这四个样本中,有三个样本 SVM 似乎给出了一个错误类别的极高概率,从而大大影响了平均概率。如果 SVM 没有对这些样本的概率进行如此高估,集成模型可能会比每个单独的学习器表现得更好。对于这两个极端情况,无法采取任何措施,因为所有三个学习器都一致地将其分类错误。我们可以尝试用另一个邻居数显著更多的 k-NN 替换 SVM。在这种情况下,(learner_3 = neighbors.KNeighborsClassifier(n_neighbors=50)),我们可以看到集成模型的准确率大幅提高。集成模型的准确率和错误如下:

L1: 0.94
L2: 0.96
L3: 0.95
------------------------------
Hard Voting: 0.97

看一下以下截图:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/f7a6f54d-3272-4c1a-bfc4-430a293f2a76.png

使用两个 k-NN 的测试集的预测值和平均概率

总结

在本章中,我们介绍了最基本的集成学习方法:投票法。虽然它相当简单,但它可以证明是有效的,并且是结合多个机器学习模型的一种简便方法。我们介绍了硬投票和软投票、硬投票的自定义实现,以及 scikit-learn 中硬投票和软投票的实现。最后,我们展示了通过使用matplotlib绘制每个基学习器的错误来分析集成模型性能的方法。以下是本章的关键点总结。

硬投票假设得票最多的类别是赢家。软投票假设具有最高平均概率的类别是赢家。软投票要求基学习器以较高的准确度预测每个实例的每个类别的概率。Scikit-learn 通过VotingClassifier类实现投票集成。一个元组数组,格式为[(learner_name, learner_object), …],被传递给VotingClassifierVotingClassifier并不直接训练作为参数传递的对象,而是生成并训练一个副本。VotingClassifier的默认模式实现硬投票。要使用软投票,可以将voting='soft'参数传递给构造函数。软投票要求基学习器返回每个预测的概率。如果基学习器大幅高估或低估了概率,集成模型的预测能力将受到影响。

在下一章中,我们将讨论另一种非生成方法——堆叠法(Stacking),以及它如何应用于回归和分类问题。

第四章:堆叠

堆叠是我们将要研究的第二种集成学习技术。与投票一起,它属于非生成方法类别,因为它们都使用单独训练的分类器作为基础学习器。

在本章中,我们将介绍堆叠的主要思想、优缺点,以及如何选择基础学习器。此外,我们还将介绍如何使用 scikit-learn 实现回归和分类问题的堆叠过程。

本章涵盖的主要主题如下:

  • 堆叠的方法论及使用元学习器组合预测

  • 使用堆叠的动机

  • 堆叠的优缺点

  • 选择集成的基础学习器

  • 实现堆叠回归和分类问题

技术要求

你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 的约定和语法。最后,熟悉 NumPy 库将大大帮助读者理解一些自定义算法的实现。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter04

查看以下视频,查看代码的实际应用:bit.ly/2XJgyD2

元学习

元学习是一个广泛的机器学习术语。它有多重含义,但通常指的是利用特定问题的元数据来解决该问题。它的应用范围从更高效地解决问题,到设计全新的学习算法。它是一个日益发展的研究领域,最近通过设计新颖的深度学习架构取得了令人瞩目的成果。

堆叠

堆叠是一种元学习形式。其主要思想是,我们使用基础学习器生成问题数据集的元数据,然后利用另一种学习器——元学习器,来处理这些元数据。基础学习器被视为 0 级学习器,而元学习器则被视为 1 级学习器。换句话说,元学习器堆叠在基础学习器之上,因此得名堆叠。

一种更直观的集成描述方式是通过投票类比。在投票中,我们结合多个基础学习器的预测,以提高它们的性能。在堆叠中,我们不是明确地定义组合规则,而是训练一个模型,学习如何最好地结合基础学习器的预测。元学习器的输入数据集由基础学习器的预测(元数据)组成,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/8becb6b8-de8a-4fb7-9718-921f8c998776.png

堆叠集成数据流,从原始数据到基础学习器,生成元学习器的元数据

创建元数据

如前所述,我们需要元数据来训练和操作我们的集成模型。在操作阶段,我们仅需传递基本学习器的数据。另一方面,训练阶段稍微复杂一些。我们希望元学习器能够发现基本学习器之间的优缺点。尽管有人认为我们可以在训练集上训练基本学习器,对其进行预测,并使用这些预测来训练我们的元学习器,但这会引入方差。我们的元学习器将发现已经被基本学习器“看到”的数据的优缺点。由于我们希望生成具有良好预测(样本外)性能的模型,而不是描述性(样本内)能力,因此必须采用另一种方法。

另一种方法是将训练集分为基本学习器训练集和元学习器训练(验证)集。这样,我们仍然可以保留一个真实的测试集,用于衡量集成模型的表现。此方法的缺点是我们必须将部分实例分配给验证集。此外,验证集的大小和训练集的大小都会小于原始训练集的大小。因此,首选方法是使用K 折交叉验证。对于每个K,基本学习器将在K-1 个折上进行训练,并在第K个折上进行预测,生成最终训练元数据的 100/K百分比。通过将该过程重复K次,每次针对一个折,我们将为整个训练数据集生成元数据。该过程在以下图表中有所展示。最终结果是为整个数据集生成的元数据,其中元数据是基于样本外数据生成的(从基本学习器的角度来看,对于每个折):

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/63d4726b-b178-4827-aadd-36539dd7fe8d.png

使用五折交叉验证创建元数据

决定集成模型的组成

我们将堆叠描述为一种高级的投票形式。与投票(以及大多数集成学习技术)类似,堆叠依赖于基本学习器的多样性。如果基本学习器在问题的整个领域中表现相同,那么元学习器将很难显著提升它们的集体表现。此外,可能需要一个复杂的元学习器。如果基本学习器具有多样性,并且在问题的不同领域中表现出不同的性能特征,即使是一个简单的元学习器也能大大提升它们的集体表现。

选择基本学习器

通常,混合不同的学习算法是个好主意,以便捕捉特征之间以及特征与目标变量之间的线性和非线性关系。例如,考虑以下数据集,其中特征(x)与目标变量(y)之间既有线性关系也有非线性关系。显然,单一的线性回归或单一的非线性回归都无法完全建模数据。而使用线性和非线性回归的堆叠集成将大大超越这两种模型。即使不使用堆叠,通过手工制定一个简单的规则(例如“如果 x 在 [0, 30] 或 [60, 100] 区间内,使用线性模型,否则使用非线性模型”),我们也能大大超过这两个模型:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/5e1ab97e-f28c-4e60-b92a-d9550621d653.png

示例数据集中的 x=5 和 x 的平方的组合

选择元学习器

通常,元学习器应该是一个相对简单的机器学习算法,以避免过拟合。此外,还应采取额外的步骤来正则化元学习器。例如,如果使用决策树,则应限制树的最大深度。如果使用回归模型,应该首选正则化回归(如弹性网或岭回归)。如果需要更复杂的模型以提高集成的预测性能,可以使用多级堆叠,其中每个层级的模型数量和每个模型的复杂度会随着堆叠层级的增加而减少:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/7206e732-7010-4481-a15a-9667fd20e787.png

层级堆叠集成。每一层的模型比上一层更简单。

元学习器的另一个非常重要的特性是能够处理相关输入,特别是不能像朴素贝叶斯分类器那样对特征间的独立性做出假设。元学习器的输入(元数据)将高度相关。这是因为所有基学习器都被训练来预测相同的目标。因此,它们的预测将来自对相同函数的近似。尽管预测值会有所不同,但它们会非常接近。

Python 实现

尽管 scikit-learn 实现了本书中涵盖的大多数集成方法,但堆叠(stacking)并不包括在内。在这一部分,我们将为回归和分类问题实现自定义的堆叠解决方案。

回归的堆叠

在这里,我们将尝试为糖尿病回归数据集创建一个堆叠集成。该集成将包含一个 5 邻居的k-最近邻k-NN)、一个最大深度限制为四的决策树,以及一个岭回归(最小二乘回归的正则化形式)。元学习器将是一个简单的普通最小二乘法OLS)线性回归。

首先,我们需要导入所需的库和数据。Scikit-learn 提供了一个便捷的方法,可以使用sklearn.model_selection模块中的KFold类将数据拆分为 K 个子集。与之前的章节一样,我们使用前 400 个实例进行训练,剩余的实例用于测试:

# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_diabetes
from sklearn.neighbors import KNeighborsRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.model_selection import KFold
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()

train_x, train_y = diabetes.data[:400], diabetes.target[:400]
test_x, test_y = diabetes.data[400:], diabetes.target[400:]

在以下代码中,我们实例化了基础学习器和元学习器。为了方便后续访问每个基础学习器,我们将每个基础学习器存储在一个名为base_learners的列表中:

# --- SECTION 2 ---
# Create the ensemble's base learners and meta-learner
# Append base learners to a list for ease of access
base_learners = []
knn = KNeighborsRegressor(n_neighbors=5)

base_learners.append(knn)
dtr = DecisionTreeRegressor(max_depth=4 , random_state=123456)

base_learners.append(dtr)
ridge = Ridge()

base_learners.append(ridge)
meta_learner = LinearRegression()

在实例化我们的学习器之后,我们需要为训练集创建元数据。我们通过首先创建一个KFold对象,指定分割数(K),即KFold(n_splits=5),然后调用KF.split(train_x)将训练集拆分成五个子集。这将返回一个生成器,用于获取这五个子集的训练集和测试集索引。对于每个拆分,我们使用train_indices(四个子集)对应的数据来训练我们的基础学习器,并为与test_indices对应的数据创建元数据。此外,我们将每个分类器的元数据存储在meta_data数组中,将相应的目标存储在meta_targets数组中。最后,我们转置meta_data,以获得一个(实例,特征)的形状:

# --- SECTION 3 ---
# Create the training metadata

# Create variables to store metadata and their targets
meta_data = np.zeros((len(base_learners), len(train_x)))
meta_targets = np.zeros(len(train_x))

# Create the cross-validation folds
KF = KFold(n_splits=5)
meta_index = 0
for train_indices, test_indices in KF.split(train_x):
  # Train each learner on the K-1 folds 
  # and create metadata for the Kth fold
  for i in range(len(base_learners)):
    learner = base_learners[i]
    learner.fit(train_x[train_indices], train_y[train_indices])
    predictions = learner.predict(train_x[test_indices])
    meta_data[i][meta_index:meta_index+len(test_indices)] = \
                              predictions

  meta_targets[meta_index:meta_index+len(test_indices)] = \
                          train_y[test_indices]
  meta_index += len(test_indices)

# Transpose the metadata to be fed into the meta-learner
meta_data = meta_data.transpose()

对于测试集,我们不需要将其拆分成多个子集。我们仅需在整个训练集上训练基础学习器,并在测试集上进行预测。此外,我们会评估每个基础学习器并存储评估指标,以便与集成模型的表现进行比较。由于这是一个回归问题,我们使用 R 平方和均方误差MSE)作为评估指标:

# --- SECTION 4 ---
# Create the metadata for the test set and evaluate the base learners
test_meta_data = np.zeros((len(base_learners), len(test_x)))
base_errors = []
base_r2 = []
for i in range(len(base_learners)):
  learner = base_learners[i]
  learner.fit(train_x, train_y)
  predictions = learner.predict(test_x)
  test_meta_data[i] = predictions

  err = metrics.mean_squared_error(test_y, predictions)
  r2 = metrics.r2_score(test_y, predictions)

  base_errors.append(err)
  base_r2.append(r2)

test_meta_data = test_meta_data.transpose()

现在,既然我们已经获得了训练集和测试集的元数据,我们就可以在训练集上训练我们的元学习器,并在测试集上进行评估:

# --- SECTION 5 ---
# Fit the meta-learner on the train set and evaluate it on the test set
meta_learner.fit(meta_data, meta_targets)
ensemble_predictions = meta_learner.predict(test_meta_data)

err = metrics.mean_squared_error(test_y, ensemble_predictions)
r2 = metrics.r2_score(test_y, ensemble_predictions)

# --- SECTION 6 ---
# Print the results 
print('ERROR R2 Name')
print('-'*20)
for i in range(len(base_learners)):
  learner = base_learners[i]
  print(f'{base_errors[i]:.1f} {base_r2[i]:.2f} {learner.__class__.__name__}')
print(f'{err:.1f} {r2:.2f} Ensemble')

我们得到如下输出:

ERROR R2 Name
--------------------
2697.8 0.51 KNeighborsRegressor
3142.5 0.43 DecisionTreeRegressor
2564.8 0.54 Ridge
2066.6 0.63 Ensemble

如图所示,R 平方从最佳基础学习器(岭回归)提高了超过 16%,而 MSE 几乎提高了 20%。这是一个相当可观的改进。

分类任务的堆叠方法

堆叠方法既适用于回归问题,也适用于分类问题。在这一节中,我们将使用堆叠方法对乳腺癌数据集进行分类。我们依然会使用三个基础学习器:一个 5 邻居的 k-NN,一个最大深度为 4 的决策树,和一个带有 1 个隐藏层、100 个神经元的简单神经网络。对于元学习器,我们使用一个简单的逻辑回归模型。

再次加载所需的库,并将数据拆分为训练集和测试集:

# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_breast_cancer
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold
from sklearn import metrics
import numpy as np
bc = load_breast_cancer()

train_x, train_y = bc.data[:400], bc.target[:400]
test_x, test_y = bc.data[400:], bc.target[400:]

我们实例化了基础学习器和元学习器。请注意,MLPClassifier具有一个hidden_layer_sizes =(100,)参数,用来指定每个隐藏层的神经元数量。这里,我们只有一个隐藏层,包含 100 个神经元:

# --- SECTION 2 ---
# Create the ensemble's base learners and meta-learner
# Append base learners to a list for ease of access
base_learners = []

knn = KNeighborsClassifier(n_neighbors=2)
base_learners.append(knn)

dtr = DecisionTreeClassifier(max_depth=4, random_state=123456)
base_learners.append(dtr)

mlpc = MLPClassifier(hidden_layer_sizes =(100, ), 
           solver='lbfgs', random_state=123456)
base_learners.append(mlpc)

meta_learner = LogisticRegression(solver='lbfgs')

同样,使用KFolds,我们将训练集拆分成五个折叠,以便在四个折叠上进行训练并为剩余的折叠生成元数据,重复五次。请注意,我们使用learner.predict_proba(train_x[test_indices])[:,0]来获取实例属于第一类的预测概率。鉴于我们只有两个类别,这已经足够了。如果是N个类别,我们必须保存N-1 个特征,或者简单地使用learner.predict,以便保存预测的类别:

# --- SECTION 3 ---
# Create the training metadata

# Create variables to store metadata and their targets
meta_data = np.zeros((len(base_learners), len(train_x)))
meta_targets = np.zeros(len(train_x))

# Create the cross-validation folds
KF = KFold(n_splits=5)
meta_index = 0
for train_indices, test_indices in KF.split(train_x):
   # Train each learner on the K-1 folds and create 
   # metadata for the Kth fold
   for i in range(len(base_learners)):
   learner = base_learners[i]

   learner.fit(train_x[train_indices], train_y[train_indices])
   predictions = learner.predict_proba(train_x[test_indices])[:,0]

   meta_data[i][meta_index:meta_index+len(test_indices)] = predictions

   meta_targets[meta_index:meta_index+len(test_indices)] = \
                           train_y[test_indices]
   meta_index += len(test_indices)

# Transpose the metadata to be fed into the meta-learner
meta_data = meta_data.transpose()

然后,我们在训练集上训练基础分类器,并为测试集创建元数据,同时使用metrics.accuracy_score(test_y, learner.predict(test_x))评估它们的准确度:

# --- SECTION 4 ---
# Create the metadata for the test set and evaluate the base learners
test_meta_data = np.zeros((len(base_learners), len(test_x)))
base_acc = []
for i in range(len(base_learners)):
  learner = base_learners[i]
  learner.fit(train_x, train_y)
  predictions = learner.predict_proba(test_x)[:,0]
  test_meta_data[i] = predictions

  acc = metrics.accuracy_score(test_y, learner.predict(test_x))
  base_acc.append(acc)
test_meta_data = test_meta_data.transpose()

最后,我们在训练元数据上拟合元学习器,评估其在测试数据上的表现,并打印出集成模型和单个学习器的准确度:

# --- SECTION 5 ---
# Fit the meta-learner on the train set and evaluate it on the test set
meta_learner.fit(meta_data, meta_targets)
ensemble_predictions = meta_learner.predict(test_meta_data)

acc = metrics.accuracy_score(test_y, ensemble_predictions)

# --- SECTION 6 ---
# Print the results
print('Acc Name')
print('-'*20)
for i in range(len(base_learners)):
  learner = base_learners[i]
  print(f'{base_acc[i]:.2f} {learner.__class__.__name__}')
print(f'{acc:.2f} Ensemble')

最终输出如下:

Acc Name
--------------------
0.86 KNeighborsClassifier
0.88 DecisionTreeClassifier
0.92 MLPClassifier  
0.93 Ensemble

在这里,我们可以看到,元学习器仅能将集成模型的表现提高 1%,与表现最好的基础学习器相比。如果我们尝试利用learner.predict方法生成元数据,我们会发现集成模型实际上表现不如神经网络:

Acc Name
--------------------
0.86 KNeighborsClassifier
0.88 DecisionTreeClassifier
0.92 MLPClassifier
0.91 Ensemble

为 scikit-learn 创建一个堆叠回归器类

我们可以利用前面的代码来创建一个可重用的类,用于协调集成模型的训练和预测。所有 scikit-learn 分类器都使用标准的fit(x, y)predict(x)方法,分别用于训练和预测。首先,我们导入所需的库,并声明类及其构造函数。构造函数的参数是一个包含 scikit-learn 分类器子列表的列表。每个子列表包含该层的学习器。因此,构建一个多层堆叠集成模型非常容易。例如,可以使用StackingRegressor([ [l11, l12, l13], [ l21, l22], [l31] ])来构建一个三层集成模型。我们创建一个包含每个堆叠层大小(学习器数量)的列表,并且还创建基础学习器的深拷贝。最后一个列表中的分类器被视为元学习器:

以下所有代码,直到(不包括)第五部分(注释标签),都是StackingRegressor类的一部分。如果将其复制到 Python 编辑器中,应正确缩进。

# --- SECTION 1 ---
# Libraries
import numpy as np

from sklearn.model_selection import KFold
from copy import deepcopy

class StackingRegressor():
  # --- SECTION 2 ---
  # The constructor 
  def __init__(self, learners):
    # Create a list of sizes for each stacking level
    # And a list of deep copied learners 
    self.level_sizes = []
    self.learners = []
    for learning_level in learners:
      self.level_sizes.append(len(learning_level))
      level_learners = []
      for learner in learning_level:
        level_learners.append(deepcopy(learner))
      self.learners.append(level_learners)

在跟随构造函数定义的过程中,我们定义了fit函数。与我们在前一部分展示的简单堆叠脚本的唯一区别在于,我们不再为元学习器创建元数据,而是为每个堆叠层创建一个元数据列表。我们将元数据和目标保存到meta_data, meta_targets列表中,并使用data_z, target_z作为每个层的对应变量。此外,我们在上一层的元数据上训练该层的学习器。我们使用原始训练集和目标初始化元数据列表:

  # --- SECTION 3 ---
  # The fit function. Creates training metadata for every level
  # and trains each level on the previous level's metadata
  def fit(self, x, y):
    # Create a list of training metadata, one for each stacking level
    # and another one for the targets. For the first level, 
    # the actual data is used.
    meta_data = [x]
    meta_targets = [y]
    for i in range(len(self.learners)):
      level_size = self.level_sizes[i]

      # Create the metadata and target variables for this level
      data_z = np.zeros((level_size, len(x)))
      target_z = np.zeros(len(x))

      train_x = meta_data[i]
      train_y = meta_targets[i]

      # Create the cross-validation folds
      KF = KFold(n_splits=5)
      meta_index = 0
      for train_indices, test_indices in KF.split(x):
        # Train each learner on the K-1 folds and create
        # metadata for the Kth fold
        for j in range(len(self.learners[i])):

          learner = self.learners[i][j]
          learner.fit(train_x[train_indices], 
                train_y[train_indices])
          predictions = learner.predict(train_x[test_indices])

          data_z[j][meta_index:meta_index+len(test_indices)] = \
                              predictions

        target_z[meta_index:meta_index+len(test_indices)] = \
                          train_y[test_indices]
        meta_index += len(test_indices)

      # Add the data and targets to the metadata lists
      data_z = data_z.transpose()
      meta_data.append(data_z)
      meta_targets.append(target_z)

      # Train the learner on the whole previous metadata
      for learner in self.learners[i]:
        learner.fit(train_x, train_y)

最后,我们定义了predict函数,该函数为提供的测试集创建每个层的元数据,使用与fit中相同的逻辑(存储每个层的元数据)。该函数返回每个层的元数据,因为它们也是每个层的预测结果。集成输出可以通过meta_data[-1]访问:


  # --- SECTION 4 ---
  # The predict function. Creates metadata for the test data and returns
  # all of them. The actual predictions can be accessed with 
  # meta_data[-1]
  def predict(self, x):

    # Create a list of training metadata, one for each stacking level
    meta_data = [x]
    for i in range(len(self.learners)):
      level_size = self.level_sizes[i]

      data_z = np.zeros((level_size, len(x)))

      test_x = meta_data[i]

      # Create the cross-validation folds
      KF = KFold(n_splits=5)
      for train_indices, test_indices in KF.split(x):
        # Train each learner on the K-1 folds and create
        # metadata for the Kth fold
        for j in range(len(self.learners[i])):

          learner = self.learners[i][j]
          predictions = learner.predict(test_x)
          data_z[j] = predictions

      # Add the data and targets to the metadata lists
      data_z = data_z.transpose()
      meta_data.append(data_z)

    # Return the meta_data the final layer's prediction can be accessed
    # With meta_data[-1]
    return meta_data

如果我们用与回归示例中相同的元学习器和基础学习器实例化StackingRegressor,我们可以看到它的表现完全相同!为了访问中间预测,我们必须访问该层的索引加一,因为meta_data[0]中的数据是原始的测试数据:

# --- SECTION 5 ---
# Use the classifier
from sklearn.datasets import load_diabetes
from sklearn.neighbors import KNeighborsRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression, Ridge
from sklearn import metrics
diabetes = load_diabetes()

train_x, train_y = diabetes.data[:400], diabetes.target[:400]
test_x, test_y = diabetes.data[400:], diabetes.target[400:]

base_learners = []

knn = KNeighborsRegressor(n_neighbors=5)
base_learners.append(knn)

dtr = DecisionTreeRegressor(max_depth=4, random_state=123456)
base_learners.append(dtr)

ridge = Ridge()
base_learners.append(ridge)

meta_learner = LinearRegression()

# Instantiate the stacking regressor
sc = StackingRegressor([[knn,dtr,ridge],[meta_learner]])

# Fit and predict
sc.fit(train_x, train_y)
meta_data = sc.predict(test_x)

# Evaluate base learners and meta-learner
base_errors = []
base_r2 = []
for i in range(len(base_learners)):
  learner = base_learners[i]
  predictions = meta_data[1][:,i]
  err = metrics.mean_squared_error(test_y, predictions)
  r2 = metrics.r2_score(test_y, predictions)
  base_errors.append(err)
  base_r2.append(r2)

err = metrics.mean_squared_error(test_y, meta_data[-1])
r2 = metrics.r2_score(test_y, meta_data[-1])

# Print the results
print('ERROR R2 Name')
print('-'*20)
for i in range(len(base_learners)):
  learner = base_learners[i]
  print(f'{base_errors[i]:.1f} {base_r2[i]:.2f} 
      {learner.__class__.__name__}')
print(f'{err:.1f} {r2:.2f} Ensemble')

结果与我们之前示例中的结果一致:

ERROR R2 Name
--------------------
2697.8 0.51 KNeighborsRegressor
3142.5 0.43 DecisionTreeRegressor
2564.8 0.54 Ridge
2066.6 0.63 Ensemble

为了进一步澄清meta_dataself.learners列表之间的关系,我们通过图示方式展示它们的交互关系。为了代码简洁,我们初始化了meta_data[0]。虽然将实际输入数据存储在meta_data列表中可能会误导,但它避免了需要以不同于其他层的方式处理基础学习器第一层:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/90f001b3-9e06-4847-8dc0-7ed5beb2952f.png

每一层meta_dataself.learners之间的关系

总结

本章介绍了一种名为堆叠(或堆叠泛化)的集成学习方法。它可以视为一种更高级的投票方法。我们首先介绍了堆叠的基本概念,如何正确创建元数据,以及如何决定集成的组成。我们为堆叠提供了回归和分类的实现。最后,我们展示了一个集成类的实现(类似于scikit-learn类的实现),使得多层堆叠集成更易于使用。以下是本章的一些关键要点:

堆叠可以由多个组成。每一层都会为下一层生成元数据。你应该通过将训练集划分为K 折并迭代地在 K-1 折上训练,同时为第 K 折创建元数据来创建每一层的元数据。创建元数据后,你应该在整个训练集上训练当前层。基础学习器必须具有多样性。元学习器应该是一个相对简单的算法,并能抵抗过拟合。如果可能的话,尽量在元学习器中引入正则化。例如,如果使用决策树,则限制其最大深度,或使用正则化回归。元学习器应该能够相对较好地处理相关输入。你不应该害怕将表现不佳的模型添加到集成中,只要它们为元数据引入了新的信息(即,它们以不同于其他模型的方式处理数据集)。在下一章中,我们将介绍第一个生成式集成方法——袋装(Bagging)。

第三部分:生成方法

在本节中,我们将涵盖更高级的集成学习方法。

本节包括以下章节:

  • 第五章,装袋法

  • 第六章,提升法

  • 第七章,随机森林

第五章:装袋法(Bagging)

装袋法,或称为自助聚合(Bootstrap Aggregating),是本书介绍的第一个生成性集成学习技术。它可以作为减少方差的有用工具,通过对原始训练集进行子抽样来创建多个基础学习器。在本章中,我们将讨论装袋法所基于的统计方法——自助法。接下来,我们将介绍装袋法的优缺点,并最终用 Python 实现该方法,同时使用 scikit-learn 实现解决回归和分类问题。

本章涵盖的主要内容如下:

  • 计算统计学中的自助法方法

  • 装袋法的工作原理

  • 装袋法的优缺点

  • 实现自定义的装袋集成方法

  • 使用 scikit-learn 实现

技术要求

你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 的规范和语法。最后,熟悉 NumPy 库将极大地帮助读者理解一些自定义算法实现。

本章的代码文件可以在 GitHub 上找到:

github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter05

查看以下视频,查看代码的实际操作:bit.ly/2JKcokD

自助法(Bootstrapping)

自助法是一种重抽样方法。在统计学中,重抽样是指使用从原始样本生成的多个样本。在机器学习术语中,样本即为我们的训练数据。其主要思想是将原始样本视为总体(问题的整个领域),而将生成的子样本视为样本。

本质上,我们在模拟如果我们从原始总体中收集多个样本,统计量将如何表现,正如以下图示所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/eea515ec-c607-48f5-9db1-8eb8b6159a9f.png

自助法如何工作的示意图

创建自助样本

为了创建自助样本,我们使用有放回抽样(每个实例可能被多次选择)从原始样本中抽取数据。这意味着一个实例可以被多次选择。假设我们有 100 个人的数据,数据中包含每个人的体重和身高。如果我们从 1 到 100 生成随机数字,并将对应的数据添加到一个新数据集中,那么我们基本上就创建了一个自助样本。

在 Python 中,我们可以使用 numpy.random.choice 来创建给定大小的子样本。我们可以尝试创建自助样本并估算糖尿病数据集的均值和标准差。首先,我们加载数据集和库,并打印样本的统计信息,如下例所示:

# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_diabetes

diabetes = load_diabetes()

# --- SECTION 2 ---
# Print the original sample's statistics
target = diabetes.target

print(np.mean(target))
print(np.std(target))

接下来,我们创建自助样本和统计量,并将其存储在bootstrap_stats中。我们本可以存储整个自助样本,但这样做会消耗过多内存。而且,我们只关心统计量,因此只存储它们更有意义。在这里,我们创建了 10,000 个自助样本和统计量:

# --- SECTION 3 ---
# Create the bootstrap samples and statistics
bootstrap_stats = []
for _ in range(10000):
    bootstrap_sample = np.random.choice(target, size=len(target))
    mean = np.mean(bootstrap_sample)
    std = np.std(bootstrap_sample)
    bootstrap_stats.append((mean, std))

bootstrap_stats = np.array(bootstrap_stats)

现在我们可以绘制平均值和标准差的直方图,并计算每个值的标准误差(即统计量分布的标准差):

# --- SECTION 4 ---
# plot the distributions
plt.figure()
plt.subplot(2,1,1)
std_err = np.std(bootstrap_stats[:,0])
plt.title('Mean, Std. Error: %.2f'%std_err)
plt.hist(bootstrap_stats[:,0], bins=20)

plt.subplot(2,1,2)
std_err = np.std(bootstrap_stats[:,1])
plt.title('Std. Dev, Std. Error: %.2f'%std_err)
plt.hist(bootstrap_stats[:,1], bins=20)
plt.show()

我们得到如下图所示的输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/8114287f-2280-4cd4-8aa7-fcd3b478a8dd.png

平均值和标准差的自助分布

注意,由于该过程的固有随机性(每个自助样本将选择哪些实例),每次执行时结果可能会有所不同。增加自助样本的数量有助于稳定结果。尽管如此,这仍然是一种非常有用的技术,可以在不做假设的情况下计算标准误差、置信区间和其他统计量,而无需假设底层分布。

集成法(Bagging)

集成法利用自助采样(bootstrap sampling)训练一系列基础学习器,然后通过投票方式合并它们的预测结果。这种方法的动机是通过多样化训练集,产生多样化的基础学习器。在本节中,我们讨论这种方法的动机、优势与劣势。

创建基础学习器

集成法对训练集应用自助采样,创建多个N个自助样本。接着,使用相同的机器学习算法创建相同数量N的基础学习器。每个基础学习器都在相应的训练集上进行训练,所有基础学习器通过投票合并(分类时使用硬投票,回归时使用平均值)。该过程如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/7a27a871-ef8e-4465-9e56-2e53afe717f4.png

通过集成法创建基础学习器

通过使用与原始训练集大小相同的自助样本,每个实例在任何给定的自助样本中出现的概率为 0.632。因此,在许多情况下,这种自助估计被称为 0.632 自助估计。在我们的案例中,这意味着我们可以使用原始训练集中剩余的 36.8%实例来估算单个基础学习器的性能。这被称为袋外得分out-of-bag score),而这 36.8%的实例则被称为袋外实例out-of-bag instances)。

优势与劣势

Bagging 通常使用决策树作为基本学习器,但它可以与任何机器学习算法一起使用。Bagging 大大减少了方差,并且已被证明在使用不稳定的基本学习器时最为有效。不稳定的学习器生成的模型具有较大的模型间方差,即使训练集仅略微变化。此外,随着基本学习器数量的增加,bagging 会收敛。类似于估计自助法统计量,通过增加基本学习器的数量,我们也增加了自助样本的数量。最后,bagging 允许轻松并行化,因为每个模型都是独立训练的。

Bagging 的主要缺点是模型的可解释性和透明度的丧失。例如,使用单个决策树可以提供很好的可解释性,因为每个节点的决策都是可直接获取的。使用 100 棵树的 bagging 集成模型会使得单个决策变得不那么重要,而是集体预测定义了集成模型的最终输出。

Python 实现

为了更好地理解集成模型的创建过程及其优点,我们将使用决策树在 Python 中实现它。在这个示例中,我们将尝试对手写数字的 MNIST 数据集进行分类。虽然我们之前一直使用癌症数据集作为分类示例,但它只有两个类别,并且样本数量相对较少,不适合有效的自助法。数字数据集包含大量样本,且更加复杂,因为它总共有 10 个类别。

实现

在这个示例中,我们将使用 1500 个实例作为训练集,剩余的 297 个作为测试集。我们将生成 10 个自助样本,因此会得到 10 个决策树模型。接着,我们将通过硬投票将基本预测结果结合起来:

  1. 我们加载库和数据,如下所示:
# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_digits
from sklearn.tree import DecisionTreeClassifier
from sklearn import metrics
import numpy as np
digits = load_digits()

train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]
  1. 然后我们创建自助样本并训练相应的模型。请注意,我们没有使用 np.random.choice,而是使用 np.random.randint(0, train_size, size=train_size) 生成一个索引数组,这样我们可以为每个自助样本选择特征和相应的目标。为了后续方便访问,我们将每个基本学习器存储在 base_learners 列表中:
# --- SECTION 2 ---
# Create our bootstrap samples and train the classifiers

ensemble_size = 10
base_learners = []

for _ in range(ensemble_size):
 # We sample indices in order to access features and targets
 bootstrap_sample_indices = np.random.randint(0, train_size, size=train_size)
 bootstrap_x = train_x[bootstrap_sample_indices]
 bootstrap_y = train_y[bootstrap_sample_indices]
 dtree = DecisionTreeClassifier()
 dtree.fit(bootstrap_x, bootstrap_y)
 base_learners.append(dtree)
  1. 接下来,我们使用每个基本学习器预测测试集的目标,并存储它们的预测结果以及评估后的准确性,如下方代码块所示:
# --- SECTION 3 ---
# Predict with the base learners and evaluate them

base_predictions = []
base_accuracy = []
for learner in base_learners:
 predictions = learner.predict(test_x)
 base_predictions.append(predictions)
 acc = metrics.accuracy_score(test_y, predictions)
 base_accuracy.append(acc)
  1. 现在我们已经在 base_predictions 中得到了每个基本学习器的预测,我们可以像在第三章中做的那样,使用硬投票将它们结合起来,投票,用于个体基本学习器的预测。此外,我们还评估了集成模型的准确性:
# Combine the base learners' predictions 

ensemble_predictions = []
# Find the most voted class for each test instance
for i in range(len(test_y)):
    counts = [0 for _ in range(10)]
    for learner_predictions in base_predictions:
        counts[learner_predictions[i]] = counts[learner_predictions[i]]+1
    # Find the class with most votes 
    final = np.argmax(counts)
    # Add the class to the final predictions 
    ensemble_predictions.append(final)

ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
  1. 最后,我们打印每个基本学习器的准确性以及集成模型的准确性,并按升序排序:
# --- SECTION 5 ---
# Print the accuracies
print('Base Learners:')
print('-'*30)
for index, acc in enumerate(sorted(base_accuracy)):
 print(f'Learner {index+1}: %.2f' % acc)
print('-'*30)
print('Bagging: %.2f' % ensemble_acc)

最终输出如下所示:


Base Learners:
------------------------------
Learner 1: 0.72
Learner 2: 0.72
Learner 3: 0.73
Learner 4: 0.73
Learner 5: 0.76
Learner 6: 0.76
Learner 7: 0.77
Learner 8: 0.77
Learner 9: 0.79
Learner 10: 0.79
------------------------------
Bagging: 0.88

显然,集成模型的准确率比表现最佳的基模型高出近 10%。这是一个相当大的改进,特别是如果我们考虑到该集成模型由相同的基学习器组成(考虑到所使用的机器学习方法)。

并行化实现

我们可以通过 from concurrent.futures import ProcessPoolExecutor 来轻松并行化我们的袋装实现。这个执行器允许用户生成多个任务并在并行进程中执行。它只需要传入一个目标函数及其参数。在我们的例子中,我们只需要将代码块(第 2 和第三部分)封装成函数:

def create_learner(train_x, train_y):
 # We sample indices in order to access features and targets
 bootstrap_sample_indices = np.random.randint(0, train_size, size=train_size)
 bootstrap_x = train_x[bootstrap_sample_indices]
 bootstrap_y = train_y[bootstrap_sample_indices]
 dtree = DecisionTreeClassifier()
 dtree.fit(bootstrap_x, bootstrap_y)
 return dtree

def predict(learner, test_x):
 return learner.predict(test_x)

接着,在原始的第 2 和第三部分中,我们将代码修改如下:

# Original Section 2
with ProcessPoolExecutor() as executor:
 futures = []
 for _ in range(ensemble_size):
 future = executor.submit(create_learner, train_x, train_y)
 futures.append(future)

for future in futures:
 base_learners.append(future.result())

# Original Section 3
base_predictions = []
 base_accuracy = []
 with ProcessPoolExecutor() as executor:
 futures = []
 for learner in base_learners:
 future = executor.submit(predict, learner, test_x)
 futures.append(future)

for future in futures:
 predictions = future.result()
 base_predictions.append(predictions)
 acc = metrics.accuracy_score(test_y, predictions)
 base_accuracy.append(acc)

executor 返回一个对象(在我们的例子中是 future),其中包含了我们函数的结果。其余代码保持不变,唯一的变化是它被封装在 if __name__ == '__main__' 的保护代码块中,因为每个新进程都会导入整个脚本。这个保护代码块防止它们重新执行其余的代码。由于我们的示例较小,且有六个进程可用,因此我们需要至少 1,000 个基学习器才能看到执行时间的显著加速。有关完整的工作版本,请参考提供的代码库中的 'bagging_custom_parallel.py'

使用 scikit-learn

Scikit-learn 为回归和分类问题提供了出色的袋装(bagging)实现。在本节中,我们将通过使用提供的实现,创建数字和糖尿病数据集的集成模型。

分类问题的袋装(Bagging)

Scikit-learn 的袋装实现位于 sklearn.ensemble 包中。BaggingClassifier 是分类问题的相应类。它有许多有趣的参数,提供了更大的灵活性。通过指定 base_estimator,它可以使用任何 scikit-learn 估计器。此外,n_estimators 决定了集成模型的大小(也就是说,决定了生成的自助样本数量),而 n_jobs 则决定了在训练和预测每个基学习器时将使用多少个作业(进程)。最后,如果设置为 Trueoob_score 会计算基学习器的袋外得分。

使用实际的分类器是非常简单的,与所有其他 scikit-learn 估计器类似。首先,我们加载所需的数据和库,如以下示例所示:

# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_digits
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn import metrics

digits = load_digits()

train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]

然后,我们创建、训练并评估估计器:

# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 10
ensemble = BaggingClassifier(base_estimator=DecisionTreeClassifier(),
 n_estimators=ensemble_size,
 oob_score=True)

# --- SECTION 3 ---
# Train the ensemble
ensemble.fit(train_x, train_y)

# --- SECTION 4 ---
# Evaluate the ensemble
ensemble_predictions = ensemble.predict(test_x)

ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)

# --- SECTION 5 ---
# Print the accuracy
print('Bagging: %.2f' % ensemble_acc)

最终的准确率为 88%,与我们自己的实现相同。此外,我们可以通过 ensemble.oob_score_ 访问袋外得分,在我们的例子中,它等于 89.6%。通常情况下,袋外得分略微高估了集成模型的样本外预测能力,这在这个示例中得到了体现。

在我们的示例中,我们选择了ensemble_size10。假设我们希望测试不同集成模型大小如何影响集成模型的表现。由于 bagging 分类器接受该大小作为构造函数的参数,我们可以使用第二章中的验证曲线,开始使用集成学习,来进行该测试。我们测试了 1 到 39 个基础学习器,步长为 2。我们观察到偏差和方差的初始下降。对于具有超过 20 个基础学习器的集成模型,似乎增加集成模型的大小并没有带来任何好处。结果在下图中显示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-ensm-lrn-py/img/1366c425-06ba-4e76-9523-a11d125fe96f.png

1 到 39 个基础学习器的验证曲线

用于回归的 Bagging

对于回归目的,我们将使用来自相同sklearn.ensemble包的BaggingRegressor类。我们还将实例化一个单独的DecisionTreeRegressor以比较结果。我们按惯例开始加载库和数据:

# --- SECTION 1 ---
 # Libraries and data loading
 from sklearn.datasets import load_diabetes
 from sklearn.tree import DecisionTreeRegressor
 from sklearn.ensemble import BaggingRegressor
 from sklearn import metrics
 import numpy as np
 diabetes = load_diabetes()

np.random.seed(1234)

train_x, train_y = diabetes.data[:400], diabetes.target[:400]
test_x, test_y = diabetes.data[400:], diabetes.target[400:]

我们实例化了单个决策树和集成模型。请注意,我们通过指定max_depth=6来允许相对较深的决策树。这允许创建多样化且不稳定的模型,极大地有利于 bagging。如果我们将最大深度限制为 2 或 3 层,我们会看到 bagging 的表现不比单一模型更好。训练和评估集成模型和单一模型的过程遵循标准程序:

# --- SECTION 2 ---
# Create the ensemble and a single base learner for comparison
estimator = DecisionTreeRegressor(max_depth=6)
ensemble = BaggingRegressor(base_estimator=estimator,
n_estimators=10)

# --- SECTION 3 ---
# Train and evaluate both the ensemble and the base learner
ensemble.fit(train_x, train_y)
ensemble_predictions = ensemble.predict(test_x)

estimator.fit(train_x, train_y)
single_predictions = estimator.predict(test_x)

ensemble_r2 = metrics.r2_score(test_y, ensemble_predictions)
ensemble_mse = metrics.mean_squared_error(test_y, ensemble_predictions)

single_r2 = metrics.r2_score(test_y, single_predictions)
single_mse = metrics.mean_squared_error(test_y, single_predictions)

# --- SECTION 4 ---
# Print the metrics
print('Bagging r-squared: %.2f' % ensemble_r2)
print('Bagging MSE: %.2f' % ensemble_mse)
print('-'*30)
print('Decision Tree r-squared: %.2f' % single_r2)
print('Decision Tree MSE: %.2f' % single_mse)

集成模型可以显著优于单一模型,通过产生更高的 R 平方值和更低的均方误差MSE)。如前所述,这是因为基础学习器可以创建深度和不稳定的模型。以下是两个模型的实际结果:

 Bagging r-squared: 0.52
 Bagging MSE: 2679.12
 ------------------------------
 Decision Tree r-squared: 0.15
 Decision Tree MSE: 4733.35

摘要

本章介绍了创建自助法样本和估算自助法统计量的主要概念。在此基础上,我们介绍了自助法聚合(或称为 bagging),它使用多个自助法样本来训练许多基础学习器,这些学习器使用相同的机器学习算法。随后,我们提供了用于分类的自定义 bagging 实现,并介绍了如何并行化它。最后,我们展示了 scikit-learn 自身实现的 bagging 在回归和分类问题中的应用。

本章可以总结如下:自助法样本是通过从原始数据集进行有放回的重采样来创建的。其主要思想是将原始样本视为总体,将每个子样本视为原始样本。如果原始数据集和自助法数据集的大小相同,则每个实例有63.2%的概率被包含在自助法数据集中(样本)。自助法方法对于计算统计量(如置信区间和标准误差)非常有用,无需对潜在的分布做假设集成自助法(Bagging)通过生成多个自助法样本来训练每个独立的基学习器。集成自助法对于不稳定学习器很有帮助,因为训练集中的小变化可能会导致生成的模型发生较大变化。集成自助法是减少方差的适合的集成学习方法。

集成自助法支持并行化,因为每个自助法样本和基学习器都可以独立生成、训练和测试。与所有集成学习方法一样,使用集成自助法会降低单个预测的可解释性和动机。

在下一章,我们将介绍第二种生成性方法——提升法(Boosting)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值