原文:
annas-archive.org/md5/20b62d9571c391689a2d53277f7e2459
译者:飞龙
第七章:6. 集成建模
概述
本章讨论了执行集成建模的不同方法及其优缺点。通过本章内容的学习,你将能够识别机器学习模型中的欠拟合和过拟合问题。你还将能够使用决策树设计一个袋装分类器,并实现自适应提升和梯度提升模型。最后,你将能够利用多个分类器构建一个堆叠集成模型。
介绍
在前面的章节中,我们讨论了两种类型的监督学习问题:回归和分类。我们研究了每种类型的若干算法,并深入了解了这些算法是如何工作的。
但有时这些算法,无论多么复杂,似乎都无法在我们拥有的数据上表现良好。这可能有多种原因——也许数据不够好,也许我们试图找到的趋势根本不存在,或者模型本身过于复杂。
等等,什么?!模型过于复杂怎么会是问题?如果一个模型过于复杂,并且数据不足,那么模型可能会非常拟合数据,甚至学习到噪声和异常值,这正是我们不希望发生的。
通常情况下,当一个复杂的单一算法给出的结果与实际结果相差甚远时,将多个模型的结果进行聚合可以得到一个更接近实际情况的结果。这是因为考虑所有模型的结果时,单个模型的误差很可能会相互抵消。
将多个算法组合在一起以给出综合预测的这种方法是集成建模的基础。集成方法的最终目标是以某种方式将多个表现不佳的基础估计器(即单个算法)组合起来,从而提高系统的整体性能,并使得算法的集成模型比单个算法更为健壮,能够更好地进行泛化。
本章的前半部分,我们将讨论如何通过构建集成模型来帮助我们建立一个健壮的系统,该系统能够做出准确的预测,而不会增加方差。我们将首先讨论模型表现不佳的一些原因,然后介绍偏差和方差的概念,以及过拟合和欠拟合问题。我们将把集成建模作为解决这些性能问题的一种方案,并讨论可用于解决不同类型问题的集成方法,尤其是针对表现不佳的模型。
我们将讨论三种集成方法;即:bagging、boosting 和 stacking。每种方法将从基本理论讲起,并讨论每种方法适合解决的用例,以及哪些用例可能不适合该方法。我们还将通过一些练习,使用 Python 中的 scikit-learn 库来实现这些模型。
在深入探讨这些主题之前,我们首先要熟悉一个数据集,使用这个数据集来演示和理解本章将要涉及的不同概念。接下来的练习将帮助我们做到这一点。在进入练习之前,有必要先了解一下独热编码的概念。
独热编码
那么,什么是独热编码呢?在机器学习中,我们有时会遇到分类输入特征,比如姓名、性别和颜色。这些特征包含的是标签值,而非数值,例如姓名中的 John 和 Tom,性别中的 male 和 female,颜色中的 red、blue 和 green。在这里,blue 就是分类特征——颜色的一个标签。所有机器学习模型都可以处理数值数据,但许多机器学习模型无法处理分类数据,因为它们的基础算法设计方式不支持这种数据。例如,决策树可以处理分类数据,但逻辑回归不能。
为了在使用像逻辑回归这样的模型时仍能利用分类特征,我们将这些特征转化为可用的数值格式。图 6.1 显示了这种转换的样子:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-A78FXQ2Y.jpg
图 6.1:独热编码
图 6.2 显示了应用独热编码后数据集的变化:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-46R63UX3.jpg
图 6.2:应用独热编码
基本上,在这个例子中,有三种颜色类别——红色、蓝色和绿色,因此需要三个二元变量——color_red、color_blue 和 color_green。使用 1 来表示该颜色的二元变量,其他颜色使用 0 表示。这些二元变量——color_red、color_blue 和 color_green——也被称为虚拟变量。有了这些信息,我们可以继续进行我们的练习。
练习 6.01:导入模块并准备数据集
在这个练习中,我们将导入本章所需的所有模块,并将数据集整理好,以便进行接下来的练习:
导入所有需要的模块来操作数据和评估模型:
import pandas as pd
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.model_selection import KFold
我们在本次练习中使用的数据集是 Titanic 数据集,它在前几章中已经介绍过。
读取数据集并打印前五行:
data = pd.read_csv(‘titanic.csv’)
data.head()
注意
上述代码假设数据集存储在与本次练习的 Jupyter Notebook 相同的文件夹中。如果数据集保存在 Datasets 文件夹中,则需要使用以下代码:data = pd.read_csv('../Datasets/titanic.csv')
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-YPAWIKGL.jpg
图 6.3: 前五行
为了使数据集准备好使用,我们将添加一个 preprocess
函数,该函数将预处理数据集,并将其转换为 scikit-learn 库可以接受的格式。
首先,我们创建一个 fix_age
函数来预处理年龄列并获取整数值。如果年龄为 null,该函数返回 -1,以区分其他有效值,否则返回实际值。然后,我们将此函数应用于年龄列。
接着,我们将性别(Gender)列转换为二元变量,女性为 1,男性为 0,然后使用 pandas 的 get_dummies
函数为登船港口(Embarked)列创建虚拟二元列。之后,我们将包含虚拟列的 DataFrame 与其余的数值列结合,生成最终的 DataFrame,该函数将返回该 DataFrame:
def preprocess(data):
def fix_age(age):
if np.isnan(age):
return -1
else:
return age
data.loc[:, ‘Age’] = data.Age.apply(fix_age)
data.loc[:, ‘Gender’] = data.Gender.apply(lambda s: \
int(s == ‘female’))
embarked = pd.get_dummies(data.Embarked, \
prefix=‘Emb’)[[‘Emb_C’,\
‘Emb_Q’,‘Emb_S’]]
cols = [‘Pclass’,‘Gender’,‘Age’,‘SibSp’,‘Parch’,‘Fare’]
return pd.concat([data[cols], embarked], axis=1).values
将数据集拆分为训练集和验证集。
我们将数据集拆分为两部分——一个用于在练习过程中训练模型(train),另一个用于进行预测并评估每个模型的性能(val)。我们将使用之前编写的函数分别预处理训练集和验证集数据。
在这里,Survived
二元变量是目标变量,用于确定每行数据中的个体是否在泰坦尼克号沉船中幸存。因此,我们将创建 y_train
和 y_val
作为从两个数据子集拆分出的因变量列:
train, val = train_test_split(data, test_size=0.2, random_state=11)
x_train = preprocess(train)
y_train = train[‘Survived’].values
x_val = preprocess(val)
y_val = val[‘Survived’].values
print(x_train.shape)
print(y_train.shape)
print(x_val.shape)
print(y_val.shape)
您应该得到如下输出:
(712, 9)
(712,)
(179, 9)
(179,)
如我们所见,数据集现在已分为两部分,训练集包含 712 个数据点,验证集包含 179 个数据点。
注意
要访问该特定部分的源代码,请参阅 packt.live/2Nm6KHM
。
您也可以在线运行此示例,访问 packt.live/2YWh9zg
。您必须执行整个 Notebook 才能获得预期结果。
在这个练习中,我们首先加载了数据并导入了必要的 Python 模块。然后,我们对数据集的不同列进行了预处理,使其可以用于训练机器学习模型。最后,我们将数据集分为两个子集。现在,在对数据集进行进一步操作之前,我们将尝试理解机器学习中的两个重要概念——过拟合和欠拟合。
过拟合与欠拟合
假设我们将一个有监督学习算法拟合到我们的数据上,然后使用该模型对一个独立的验证集进行预测。根据模型的泛化能力——即它对独立验证集中的数据点进行预测的能力——来评估其性能。
有时候,我们会发现模型无法做出准确的预测,并且在验证数据上表现较差。这种差劲的表现可能是因为模型过于简单,无法适当地建模数据,或者模型过于复杂,无法对验证数据集进行泛化。在前一种情况下,模型具有高偏差,导致欠拟合;在后一种情况下,模型具有高方差,导致过拟合。
偏差
机器学习模型预测中的偏差表示预测的目标值与数据点的真实目标值之间的差异。如果平均预测值与真实值相差较远,则模型被认为具有高偏差;反之,如果平均预测值接近真实值,则模型被认为具有低偏差。
高偏差表示模型无法捕捉数据中的复杂性,无法识别输入和输出之间的相关关系。
方差
机器学习模型预测中的方差表示预测值与真实值之间的分散程度。如果预测值分散且不稳定,则模型被认为具有高方差;反之,如果预测值一致且不太分散,则模型被认为具有低方差。
高方差表明模型无法泛化,并且在模型之前未见过的数据点上无法做出准确的预测。如图所示,这些圆的中心表示数据点的真实目标值,圆点表示数据点的预测目标值:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-9XB2XBIW.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-9XB2XBIW.jpg)
图 6.4:具有高偏差和低偏差的数据显示点的视觉表示
欠拟合
假设我们在训练数据集上拟合一个简单的模型,比如一个简单的线性模型。我们拟合了一个能够在一定程度上表示训练数据中 X(输入数据)和 Y(目标输出)数据点之间关系的函数,但我们发现训练误差依然很高:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-MSSATPXC.jpg
图 6.5:回归中的欠拟合与理想拟合
例如,看看图 6.5 中显示的两张回归图。第一张图展示了一个将直线拟合到数据的模型,而第二张图展示了一个尝试将相对复杂的多项式拟合到数据的模型,它似乎很好地表示了 X 和 Y 之间的映射关系。
如果我们仔细观察第一个模型(图中左侧),直线通常远离个别数据点,而第二个模型中的数据点则紧密贴合曲线。根据我们在上一节中定义的偏差,我们可以说第一个模型具有较高的偏差。而如果参考模型的方差定义,第一个模型的预测相对一致,因为它对给定输入总是预测一个固定的直线输出。因此,第一个模型具有较低的方差,我们可以说它展示了欠拟合的特征,因为它具有高偏差和低方差的特征;也就是说,尽管它无法捕捉到输入与输出之间的复杂映射,但它在预测上保持一致。该模型在训练数据和验证数据上的预测误差较大。
过拟合
假设我们训练了一个非常复杂的模型,它几乎能够完美地预测训练数据集上的数据。我们已经成功地拟合了一个函数来表示训练数据中 X 和 Y 数据点之间的关系,使得训练数据上的预测误差极低:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VBA7P45K.jpg
图 6.6:回归中的理想拟合与过拟合
通过查看图 6.6 中的两张图,我们可以看到第二张图展示了一个试图将一个高度复杂的函数拟合到数据点上的模型,而左侧的图表示了给定数据的理想拟合。
很明显,当我们尝试使用第二张图中的模型预测在训练集未出现的 X 数据点对应的 Y 值时,我们会发现预测值与真实值相差甚远。这就是过拟合的表现,模型对数据的拟合过于完美,以至于无法对新数据点进行泛化,因为模型学到了训练数据中的随机噪声和离群点。该模型表现出高方差和低偏差的特征:尽管平均预测值与真实值接近,但与真实值相比,它们的分散性较大。
过拟合可能发生的另一种情况是当数据点的数量小于或等于我们尝试拟合到模型的多项式的阶数时。因此,我们应该避免使用以下类型的模型:
多项式的阶数 > 数据点的数量
在数据集极其小的情况下,甚至尝试拟合一个简单的模型也可能导致过拟合。
克服欠拟合与过拟合的问题
从前面的章节中我们可以看到,当我们从过于简单的模型走向过于复杂的模型时,我们会从一个具有高偏差、低方差的欠拟合模型过渡到一个具有低偏差、高方差的过拟合模型。监督学习算法的目标是实现低偏差和低方差,达到欠拟合与过拟合之间的平衡。这也有助于算法从训练数据到验证数据的良好泛化,从而在模型从未见过的数据上表现出色。
当模型欠拟合数据时,提高模型复杂度是改善性能的最佳方式,从而识别数据中的相关关系。这可以通过添加新特征或创建高偏差模型的集成来实现。然而,在这种情况下,增加更多的训练数据并不会有帮助,因为限制因素是模型复杂度,更多的数据并不能减少模型的偏差。
然而,过拟合问题更难解决。以下是一些常用的技术,用于克服过拟合带来的问题:
获取更多数据:一个高度复杂的模型很容易在小数据集上发生过拟合,但在更大的数据集上则不容易发生。
降维:减少特征数量有助于使模型更简单。
正则化:在成本函数中添加一个新的项,以调整系数(特别是在线性回归中,高次项系数)向较低的值逼近。
集成建模:聚合多个过拟合模型的预测可以有效消除预测中的高方差,表现得比单个过拟合训练数据的模型更好。
我们将在本章的后续部分详细讨论集成建模技术。常见的集成类型包括:
袋装法:Bootstrap 聚合法的简称,该技术也用于减少模型的方差并避免过拟合。它涉及一次取出特征和数据点的一个子集,在每个子集上训练一个模型,并随后将所有模型的结果聚合成最终的预测。
提升法:该技术用于减少偏差,而不是减少方差,涉及逐步训练新的模型,重点关注前一个模型中分类错误的数据点。
堆叠法:该技术的目标是提高分类器的预测能力,因为它涉及训练多个模型,然后使用组合算法通过利用所有这些模型的预测输入来进行最终预测。
让我们从袋装法开始,然后再介绍提升法和堆叠法。
袋装法
术语“bagging”来源于一种叫做自助法聚合(bootstrap aggregation)的方法。为了实现一个成功的预测模型,了解在什么情况下我们可以通过使用自助法方法来构建集成模型是很重要的。这类模型在工业界和学术界都得到了广泛应用。
其中一个应用是,这些模型可以用于维基百科文章的质量评估。诸如文章长度、参考文献数量、标题数量和图片数量等特征被用来构建分类器,将维基百科文章分为低质量或高质量文章。在为此任务尝试的多个模型中,随机森林模型——一种我们将在下一节讨论的基于 bagging 的集成分类器——优于其他所有模型,如 SVM、逻辑回归甚至神经网络,其最佳精度和召回率分别为 87.3%和 87.2%。这展示了这类模型的强大能力以及它们在实际应用中的潜力。
在这一节中,我们将讨论如何使用自助法方法创建一个最小化方差的集成模型,并看看我们如何构建一个决策树集成模型,也就是随机森林算法。但什么是自助法,它如何帮助我们构建稳健的集成模型呢?
自助法
自助法方法本质上是指从包含随机选择数据点的数据集中绘取多个样本(每个样本称为重采样),其中每个重采样中的数据点可能会有重叠,并且每个数据点被从整个数据集中选择的概率是相等的:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HQEFDI7L.jpg
图 6.7:随机选择数据点
从前面的图示中,我们可以看到,从主数据集中取出的五个重采样样本各不相同,并具有不同的特征。因此,在这些重采样上训练模型将会得出不同的预测结果。
以下是自助法的优点:
每个重采样可能包含与整个数据集不同的特征,从而为我们提供了不同的视角来观察数据的行为。
使用自助法的算法具有强大的构建能力,能够更好地处理未见过的数据,尤其是在较小的数据集上,这类数据集往往会导致过拟合。
自助法方法可以通过使用具有不同变异性和特征的数据集测试模型的稳定性,从而得出一个更加稳健的模型。
现在我们已经了解了什么是自助抽样,那么袋装集成究竟是做什么的呢?简单来说,袋装意味着聚合并行模型的输出,每个模型都是通过自助抽样数据构建的。它本质上是一个集成模型,在每次重采样时生成多个预测器版本,并利用这些版本得到一个聚合的预测器。聚合步骤为我们提供了一个元预测,这个过程涉及对回归问题时的连续数值预测进行求平均,而在分类问题中,则是进行投票。投票可以分为两种类型:
硬投票(基于类别的投票)
软投票(概率投票)
在硬投票中,我们考虑由基础估计器预测的各个类别的多数意见,而在软投票中,我们首先平均各类别的概率值,然后再做出预测。
下图为我们提供了一个可视化的示意,展示了如何通过图 6.7 中所示的自助抽样构建袋装估计器:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-NAPC770H.jpg
图 6.8:通过自助抽样构建的袋装估计器
由于每个模型本质上是独立的,因此所有基础模型可以并行训练,这大大加快了训练过程,因为重采样的数据集比原始数据集小,从而使我们能够充分利用当前计算能力。
袋装本质上有助于减少整个集成的方差。它通过引入随机化到其构建过程中来实现这一点,通常用于那些容易对训练数据过拟合的基础预测器。在这里需要考虑的主要因素是训练数据集的稳定性(或缺乏稳定性):袋装在数据轻微扰动会导致模型结果显著变化的情况下表现有效,也就是高方差的模型。这就是袋装如何帮助减少方差的方式。
scikit-learn 使用 BaggingClassifier 和 BaggingRegressor 来分别实现用于分类和回归任务的通用袋装集成方法。这些方法的主要输入是用于每次重采样的基础估计器,以及要使用的估计器数量(即重采样次数)。
练习 6.02:使用袋装分类器
在这个练习中,我们将使用 scikit-learn 的袋装分类器作为我们的集成方法,并使用决策树分类器作为基础估计器。我们知道决策树容易发生过拟合,因此它们具有高方差和低偏差,这两者都是用于袋装集成中的基础估计器的重要特征。
我们将在这个练习中使用 Titanic 数据集。请在开始本练习之前,先完成练习 6.01:导入模块并准备数据集:
导入基础分类器和集成分类器:
从 sklearn.tree 导入决策树分类器(DecisionTreeClassifier)
from sklearn.ensemble import BaggingClassifier
指定超参数并初始化模型。
在这里,我们将首先指定基础估计器的超参数,我们使用决策树分类器,并将熵或信息增益作为分裂准则。我们不会对树的深度或每棵树的叶子数量/大小设置任何限制,以便它完全生长。接下来,我们将为 Bagging 分类器定义超参数,并将基础估计器对象作为超参数传递给分类器。
我们将以 50 个基础估计器作为示例,这些估计器将并行运行,并利用机器上可用的所有进程(通过指定 n_jobs=-1 实现)。此外,我们将指定 max_samples 为 0.5,表示引导法中的数据点数量应为总数据集的一半。我们还将设置一个随机状态(为任意值,且在整个过程中保持不变)以确保结果的可重复性:
dt_params = {‘criterion’: ‘entropy’, ‘random_state’: 11}
dt = DecisionTreeClassifier(**dt_params)
bc_params = {‘base_estimator’: dt, ‘n_estimators’: 50, \
‘max_samples’: 0.5, ‘random_state’: 11, ‘n_jobs’: -1}
bc = BaggingClassifier(**bc_params)
将 Bagging 分类器模型拟合到训练数据上并计算预测准确率。
现在让我们拟合 Bagging 分类器,并找出训练集和验证集的元预测值。接下来,我们计算训练集和验证数据集的预测准确率:
bc.fit(x_train, y_train)
bc_preds_train = bc.predict(x_train)
bc_preds_val = bc.predict(x_val)
print(‘Bagging 分类器:\n> 训练数据上的准确率 = {:.4f}’\
‘\n> 验证数据上的准确率 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=bc_preds_train),\
accuracy_score(y_true=y_val, y_pred=bc_preds_val)))
输出如下:
Bagging 分类器:
训练数据上的准确率 = 0.9270
验证数据上的准确率 = 0.8659
将决策树模型拟合到训练数据上,以比较预测准确率。
现在让我们也拟合决策树(来自步骤 2 中初始化的对象),这样我们就可以比较集成模型和基础预测器的预测准确率:
dt.fit(x_train, y_train)
dt_preds_train = dt.predict(x_train)
dt_preds_val = dt.predict(x_val)
print(‘决策树:\n> 训练数据上的准确率 = {:.4f}’\
‘\n> 验证数据上的准确率 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=dt_preds_train),\
accuracy_score(y_true=y_val, y_pred=dt_preds_val)))
输出如下:
决策树:
训练数据上的准确率 = 0.9831
验证数据上的准确率 = 0.7709
在这里,我们可以看到,尽管决策树的训练准确率远高于袋装分类器,但其在验证数据集上的准确率较低,这清楚地表明决策树对训练数据进行了过拟合。而袋装集成方法则通过减少整体方差,产生了更为准确的预测。
注意
要访问这一特定部分的源代码,请参阅 https://packt.live/37O6735。
你也可以在 https://packt.live/2Nh3ayB 上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。
接下来,我们将讨论可能是最广为人知的基于袋装的机器学习模型——随机森林模型。随机森林是一个袋装集成模型,使用决策树作为基础估计器。
随机森林
决策树常面临的一个问题是,在每个节点上的分割是通过贪婪算法完成的,旨在最小化叶节点的熵。考虑到这一点,袋装分类器中的基础估计器决策树在分割特征时仍然可能非常相似,因此它们的预测也可能非常相似。然而,袋装方法只有在基础模型的预测不相关时,才有助于减少预测的方差。
随机森林算法通过不仅对整体训练数据集中的数据点进行自助抽样,还对每棵树可分割的特征进行自助抽样,从而试图克服这个问题。这确保了当贪婪算法在寻找最佳分割特征时,整体最佳特征可能并不总是出现在基础估计器的自助抽样特征中,因此不会被选择——这导致基础树具有不同的结构。这个简单的调整使得最好的估计器能够以一种方式进行训练,从而使得森林中每棵树的预测与其他树的预测之间的相关性较低。
随机森林中的每个基础估计器都有一个数据点的随机样本和一个特征的随机样本。由于该集成由决策树组成,因此该算法被称为随机森林。
练习 6.03:使用随机森林构建集成模型
随机森林的两个主要参数是用于训练每个基础决策树的特征的分数和数据点的分数。
在本次练习中,我们将使用 scikit-learn 的随机森林分类器来构建集成模型。
本次练习中,我们将使用泰坦尼克号数据集。这个练习是练习 6.02《使用袋装分类器》的延续:
导入集成分类器:
来自 sklearn.ensemble
的 RandomForestClassifier
指定超参数并初始化模型。
在这里,我们将使用熵(entropy)作为决策树的分裂标准,森林中包含 100 棵树。与之前一样,我们不会指定树的深度或叶子节点的大小/数量的限制。与袋装分类器不同,袋装分类器在初始化时需要输入 max_samples,而随机森林算法只需要 max_features,表示自助法样本中的特征数量(或比例)。我们将此值设置为 0.5,以便每棵树只考虑六个特征中的三个:
rf_params = {‘n_estimators’: 100, ‘criterion’: ‘entropy’, \
‘max_features’: 0.5, ‘min_samples_leaf’: 10, \
‘random_state’: 11, ‘n_jobs’: -1}
rf = RandomForestClassifier(**rf_params)
将随机森林分类器模型拟合到训练数据,并计算预测准确度。
现在,我们将拟合随机森林模型,并找到训练集和验证集的元预测。接下来,找出训练和验证数据集的预测准确度:
rf.fit(x_train, y_train)
rf_preds_train = rf.predict(x_train)
rf_preds_val = rf.predict(x_val)
print(‘随机森林:\n> 训练数据集准确度 = {:.4f}’\
‘\n> 验证数据集准确度 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=rf_preds_train), \
accuracy_score(y_true=y_val, y_pred=rf_preds_val)))
输出如下:
随机森林:
训练数据集准确度 = 0.8385
验证数据集准确度 = 0.8771
如果将随机森林在我们的数据集上的预测准确度与袋装分类器进行比较,我们会发现验证集上的准确度几乎相同,尽管后者在训练数据集上的准确度较高。
注意
若要访问该特定部分的源代码,请参阅 https://packt.live/3dlvGtd。
你也可以在 https://packt.live/2NkSPS5 上在线运行此示例。你必须执行整个 Notebook 才能获得预期结果。
提升
我们接下来要讨论的第二种集成技术是提升(boosting),它通过逐步训练新模型来集中关注上一模型中被误分类的数据点,并利用加权平均将弱模型(具有较高偏差的欠拟合模型)转化为更强的模型。与袋装方法不同,袋装方法中的每个基础估计器可以独立训练,而提升算法中的每个基础估计器的训练依赖于前一个模型。
尽管提升(boosting)也使用了自助法(bootstrapping)概念,但与袋装(bagging)不同,它的实现方式不同,因为每个数据样本都有权重,这意味着某些自助法样本在训练中可能会比其他样本更频繁地被使用。在训练每个模型时,算法会跟踪哪些特征最有用,以及哪些数据样本的预测误差最大;这些样本会被赋予更高的权重,并被认为需要更多的迭代才能正确训练模型。
在预测输出时,提升集成会对每个基础估计器的预测结果进行加权平均,给训练阶段错误较低的预测结果赋予更高的权重。这意味着,对于在某次迭代中被错误分类的数据点,这些数据点的权重会被增加,从而使下一个模型更可能正确分类这些数据点。
与袋装法(bagging)类似,所有提升(boosting)基础估计器的结果被汇总以产生一个元预测。然而,与袋装法不同,提升集成的准确性会随着基础估计器数量的增加而显著提高:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-88NHH80A.jpg
图 6.9:一个提升集成模型
在图示中,我们可以看到,在每次迭代之后,被错误分类的点的权重(以较大的图标表示)增加,以便下一个训练的基础估计器能够集中关注这些点。最终的预测器将每个基础估计器的决策边界进行汇总。
提升在实际应用中被广泛使用。例如,商业化的网页搜索引擎 Yahoo 和 Yandex 在其机器学习排名引擎中使用了提升的变体。排名是根据搜索查询找到最相关文档的任务。特别是对于 Yandex 来说,他们采用了一种基于梯度提升(gradient boosting)的方法来构建集成树模型,该模型通过实现最低的折扣累积增益 4.14123,比其他模型,包括 Yandex 之前使用的模型表现得更好。这显示了基于提升的建模在现实场景中的巨大实用价值。
注意
阅读更多关于 Yandex 的信息,请访问以下链接: http://webmaster.ya.ru/replies.xml?item_no=5707&ncrnd=5118.
自适应提升
现在让我们谈谈一种叫做自适应提升(adaptive boosting)的技术,它最适用于提升决策树桩在二分类问题中的表现。决策树桩本质上是深度为一的决策树(仅对单一特征进行一次分裂),因此是弱学习器。自适应提升工作的主要原理与此相同:通过提高基础估计器无法正确分类的区域,将一组弱学习器转化为强学习器。
首先,首个基学习器从主训练集抽取一个自助法的数据点,并拟合一个决策桩来分类这些样本点,接着,训练过的决策桩会拟合整个训练数据。对于那些被错误分类的样本,增加权重,使这些数据点在下一个基学习器的自助法抽样中被选中的概率更高。然后,再次对新抽样的数据点进行训练,进行分类。接着,由这两个基学习器组成的小型集成模型被用来分类整个训练集中的数据点。第二轮中被错误分类的数据点会被赋予更高的权重,以提高它们被选中的概率,直到集成模型达到它应该包含的基学习器数量的限制。
自适应提升的一个缺点是,算法很容易受到噪声数据点和异常值的影响,因为它尝试完美拟合每一个数据点。因此,如果基学习器数量过多,它容易出现过拟合。
练习 6.04:实现自适应提升
在这个练习中,我们将使用 scikit-learn 的自适应提升算法进行分类,即 AdaBoostClassifier:
我们将再次使用 Titanic 数据集。这个练习是练习 6.03《使用随机森林构建集成模型》的延续:
导入分类器:
from sklearn.ensemble import AdaBoostClassifier
指定超参数并初始化模型。
在这里,我们首先指定基学习器的超参数,使用的是一个最大深度为 1 的决策树分类器,即一个决策桩。接着,我们会定义 AdaBoost 分类器的超参数,并将基学习器对象作为超参数传递给分类器:
dt_params = {‘max_depth’: 1, ‘random_state’: 11}
dt = DecisionTreeClassifier(**dt_params)
ab_params = {‘n_estimators’: 100, ‘base_estimator’: dt, \
‘random_state’: 11}
ab = AdaBoostClassifier(**ab_params)
将模型拟合到训练数据上。
现在,让我们拟合 AdaBoost 模型,并计算训练集和验证集的元预测。接着,我们来计算训练集和验证集的数据集上的预测准确度:
ab.fit(x_train, y_train)
ab_preds_train = ab.predict(x_train)
ab_preds_val = ab.predict(x_val)
print(‘自适应提升:\n> 训练数据的准确率 = {:.4f}’\
‘\n> 验证数据的准确率 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=ab_preds_train), \
accuracy_score(y_true=y_val, y_pred=ab_preds_val)
))
输出结果如下:
自适应提升:
训练数据的准确率 = 0.8272
验证数据的准确率 = 0.8547
计算模型在不同数量的基学习器下,对训练数据和验证数据的预测准确度。
之前我们提到过,准确率随着基本估计器数量的增加而增加,但如果使用的基本估计器过多,模型也有可能出现过拟合。现在让我们计算预测准确率,以便找出模型开始过拟合训练数据的点:
ab_params = {‘base_estimator’: dt, ‘random_state’: 11}
n_estimator_values = list(range(10, 210, 10))
train_accuracies, val_accuracies = [], []
对于 n_estimators 中的每个值:
ab = AdaBoostClassifier(n_estimators=n_estimators, **ab_params)
ab.fit(x_train, y_train)
ab_preds_train = ab.predict(x_train)
ab_preds_val = ab.predict(x_val)
train_accuracies.append(accuracy_score(y_true=y_train, \
y_pred=ab_preds_train))
val_accuracies.append(accuracy_score(y_true=y_val, \
y_pred=ab_preds_val))
绘制折线图,以可视化训练和验证数据集上预测准确率的趋势:
plt.figure(figsize=(10,7))
plt.plot(n_estimator_values, train_accuracies, label=‘训练’)
plt.plot(n_estimator_values, val_accuracies, label=‘验证’)
plt.ylabel(‘准确率’)
plt.xlabel(‘n_estimators’)
plt.legend()
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-OQWVD6TB.jpg
图 6.10:预测准确率的趋势
正如前面提到的,我们可以看到训练准确率几乎始终随着决策树桩数量的增加(从 10 到 200)而不断提高。然而,验证准确率在 0.84 到 0.86 之间波动,并且随着决策树桩数量的增加而开始下降。这是因为 AdaBoost 算法也在尝试拟合噪声数据点和异常值。
注意
要访问此特定部分的源代码,请参阅 https://packt.live/2V4zB7K。
您也可以在线运行此示例,网址为 https://packt.live/3dhSBpu。您必须执行整个 Notebook 才能获得预期的结果。
梯度提升
梯度提升是提升方法的扩展,它将提升视为一个优化问题。定义了一个损失函数,代表了误差残差(预测值与真实值之间的差异),并使用梯度下降算法来优化损失函数。
在第一步中,添加一个基本估计器(即弱学习器),并在整个训练数据集上进行训练。计算预测的损失,并为减少误差残差,更新损失函数,添加更多的基本估计器,来处理现有估计器表现不佳的数据点。接下来,算法迭代地添加新的基本估计器并计算损失,使得优化算法能够更新模型并最小化残差。
在自适应提升的情况下,决策树桩被用作基础估计器的弱学习器。然而,对于梯度提升方法,可以使用更大的树,但弱学习器仍然需要受到限制,通过设置最大层数、节点数、分裂数或叶节点数的限制。这确保了基础估计器仍然是弱学习器,但它们可以以贪婪的方式构建。
从之前的章节我们知道,梯度下降算法可以用来最小化一组参数,比如回归方程中的系数。然而,在构建集成时,我们使用的是决策树,而不是需要优化的参数。在每一步计算损失后,梯度下降算法必须调整将要加入集成的新树的参数,以减少损失。这种方法更常被称为功能梯度下降。
练习 6.05:实现 GradientBoostingClassifier 来构建集成模型
梯度提升分类器的两个主要参数是用于训练每棵基础决策树的特征的比例和需要进行自助抽样的数据点比例。
在这个练习中,我们将使用 scikit-learn 的梯度提升分类器来构建提升集成模型。
这个练习是练习 6.04:实现自适应提升的延续。
导入集成分类器:
来自 sklearn.ensemble 的导入 GradientBoostingClassifier
指定超参数并初始化模型。
在这里,我们将使用 100 棵决策树作为基础估计器,每棵树的最大深度为 3,并且每个叶节点至少有 5 个样本。尽管我们没有像之前的示例那样使用决策树桩,但树仍然很小,可以认为是一个弱学习器:
gbc_params = {‘n_estimators’: 100, ‘max_depth’: 3, \
‘min_samples_leaf’: 5, ‘random_state’: 11}
gbc = GradientBoostingClassifier(**gbc_params)
将梯度提升模型拟合到训练数据,并计算预测准确率。
现在,让我们拟合集成模型并找到训练集和验证集的元预测。接下来,我们将计算训练集和验证集上的预测准确率:
gbc.fit(x_train, y_train)
gbc_preds_train = gbc.predict(x_train)
gbc_preds_val = gbc.predict(x_val)
print(‘梯度提升分类器:’\
‘\n> 训练数据的准确率 = {:.4f}’\
‘\n> 验证数据的准确率 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=gbc_preds_train), \
accuracy_score(y_true=y_val, y_pred=gbc_preds_val)))
输出如下:
梯度提升分类器:
训练数据上的准确率 = 0.8961
验证数据上的准确率 = 0.8771
注意
要访问此特定部分的源代码,请参阅 https://packt.live/37QANjZ。
你也可以在线运行这个示例,网址是 https://packt.live/2YljJ2D。你必须执行整个 Notebook 才能获得期望的结果。
我们可以看到,梯度提升集成模型在训练和验证数据集上的准确度都高于自适应提升集成模型。
堆叠
堆叠(Stacking)或堆叠泛化,也叫元集成(meta ensembling),是一种模型集成技术,旨在将多个模型的预测结果合并并作为特征,生成一个新的模型。堆叠模型通常会超越每个单独模型的表现,因为它加入了平滑效应,并且能“选择”在某些情境下表现最好的基础模型。考虑到这一点,堆叠通常在每个基础模型彼此之间有显著差异时最为有效。
堆叠在实际应用中被广泛使用。一个著名的例子来自著名的 Netflix 竞赛,两位顶尖参赛者基于堆叠模型构建了解决方案。Netflix 是一个知名的流媒体平台,这场竞赛的内容是构建最好的推荐引擎。获胜算法基于特征加权线性堆叠(feature-weighted-linear-stacking),其基本上是通过从单个模型/算法(如奇异值分解(SVD)、限制玻尔兹曼机(RBM)和 K 最近邻(KNN))中提取元特征。例如,一个元特征是 60 因子序数 SVD 的标准差。这些元特征被认为是实现获胜模型所必需的,证明了堆叠在实际应用中的强大能力。
堆叠使用基础模型的预测作为训练最终模型时的附加特征——这些被称为元特征。堆叠模型本质上充当一个分类器,确定每个模型在何处表现良好,何处表现较差。
然而,不能简单地将基础模型在完整的训练数据上训练,在完整的验证数据集上生成预测,然后将这些预测用于二级训练。这么做有可能导致你的基础模型预测已经“看到”测试集,因此在输入这些预测时会发生过拟合。
需要注意的是,对于每一行的元特征,其值不能使用包含该行的训练数据中的模型进行预测,因为这样我们就有可能发生过拟合,因为基础预测模型已经“看到”了该行的目标变量。常见的做法是将训练数据分成 k 个子集,这样,在为每个子集寻找元特征时,我们只在剩余数据上训练模型。这样做还避免了模型已经“看到”数据而发生过拟合的问题:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-P8YI1C1F.jpg
图 6.11:堆叠集成
上面的图示展示了如何执行此操作:我们将训练数据划分为 k 个子集,并通过在其余 k-1 个子集上训练模型来找到每个子集上的基础模型预测值。因此,一旦我们获得了每个子集的元预测值,我们就可以将这些元预测值与原始特征一起用于训练堆叠模型。
练习 6.06:构建堆叠模型
在这个练习中,我们将使用支持向量机(scikit-learn 的 LinearSVC)和 k 近邻(scikit-learn 的 KNeighborsClassifier)作为基础预测器,而堆叠模型将是一个逻辑回归分类器。
这个练习是练习 6.05 的延续,练习 6.05 中实现了 GradientBoostingClassifier 来构建集成模型:
导入基础模型和用于堆叠的模型:
基础模型
来自 sklearn.neighbors 的 KNeighborsClassifier
来自 sklearn.svm 的 LinearSVC
堆叠模型
来自 sklearn.linear_model 的 LogisticRegression
创建一个新的训练集,新增基础预测器的预测结果列。
我们需要为每个模型的预测值创建两个新列,作为集成模型的特征,分别用于测试集和训练集。由于 NumPy 数组是不可变的,我们将创建一个新的数组,该数组的行数与训练数据集相同,列数比训练数据集多两个。创建完数据集后,我们可以打印它来查看效果:
x_train_with_metapreds = np.zeros((x_train.shape[0], \
x_train.shape[1]+2))
x_train_with_metapreds[:, :-2] = x_train
x_train_with_metapreds[:, -2:] = -1
print(x_train_with_metapreds)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-BQNA0P7L.jpg
图 6.12:预测值的新列
如我们所见,每行的末尾有两个额外的列,填充了-1 值。
使用 k 折交叉验证策略训练基础模型。
设定 k=5. 对于这五个子集,在其他四个子集上进行训练,并在第五个子集上进行预测。这些预测结果应该被添加到新的 NumPy 数组中用于存放基本预测结果的占位符列中。
首先,我们初始化 KFold 对象,设置 k 值和随机状态以保持可复现性。kf.split()函数接受需要拆分的数据集作为输入,并返回一个迭代器,每个迭代器元素对应于训练和验证子集的索引列表。在每次循环中,这些索引值可以用于划分训练数据,并为每行执行训练和预测。
一旦数据被适当地划分,我们在四分之五的数据上训练两个基础预测器,并在剩余的五分之一行上进行预测。然后,这些预测结果被插入我们在第 2 步初始化时填充为-1 的两个占位符列中:
kf = KFold(n_splits=5, random_state=11)
for train_indices, val_indices in kf.split(x_train):
kfold_x_train, kfold_x_val = x_train[train_indices], \
x_train[val_indices]
kfold_y_train, kfold_y_val = y_train[train_indices], \
y_train[val_indices]
svm = LinearSVC(random_state=11, max_iter=1000)
svm.fit(kfold_x_train, kfold_y_train)
svm_pred = svm.predict(kfold_x_val)
knn = KNeighborsClassifier(n_neighbors=4)
knn.fit(kfold_x_train, kfold_y_train)
knn_pred = knn.predict(kfold_x_val)
x_train_with_metapreds[val_indices, -2] = svm_pred
x_train_with_metapreds[val_indices, -1] = knn_pred
创建一个新的验证集,添加来自基础预测器的预测的附加列。
如同我们在步骤 2 中所做的,我们也将在验证数据集中为基础模型预测添加两列占位符:
x_val_with_metapreds = np.zeros((x_val.shape[0], \
x_val.shape[1]+2))
x_val_with_metapreds[:, :-2] = x_val
x_val_with_metapreds[:, -2:] = -1
print(x_val_with_metapreds)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VC7JID4I.jpg
图 6.13:来自基础预测器的预测的附加列
在完整的训练集上拟合基础模型,以获取验证集的元特征。
接下来,我们将在完整的训练数据集上训练两个基础预测器,以获取验证数据集的元预测值。这类似于我们在步骤 3 中为每个折叠所做的工作:
svm = LinearSVC(random_state=11, max_iter=1000)
svm.fit(x_train, y_train)
knn = KNeighborsClassifier(n_neighbors=4)
knn.fit(x_train, y_train)
svm_pred = svm.predict(x_val)
knn_pred = knn.predict(x_val)
x_val_with_metapreds[:, -2] = svm_pred
x_val_with_metapreds[:, -1] = knn_pred
训练堆叠模型并使用最终预测计算准确度。
最后的步骤是使用训练数据集的所有列加上基础估算器的元预测来训练逻辑回归模型。我们使用该模型来计算训练集和验证集的预测准确度:
lr = LogisticRegression(random_state=11)
lr.fit(x_train_with_metapreds, y_train)
lr_preds_train = lr.predict(x_train_with_metapreds)
lr_preds_val = lr.predict(x_val_with_metapreds)
print(‘堆叠分类器:\n> 训练数据集上的准确度 = {:.4f}’\
‘\n> 验证数据集上的准确度 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=lr_preds_train), \
accuracy_score(y_true=y_val, y_pred=lr_preds_val)))
输出结果如下:
堆叠分类器:
训练数据集上的准确度 = 0.7837
验证数据集上的准确度 = 0.8827
注意
由于随机化,你可能会得到一个与前一步骤中呈现的输出略有不同的结果。
比较与基础模型的准确度。
为了了解堆叠模型带来的性能提升,我们计算基础预测器在训练集和验证集上的准确度,并将其与堆叠模型的准确度进行比较:
print(‘SVM:\n> 训练数据集上的准确度 = {:.4f}’\
‘\n> 验证数据集上的准确度 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=svm.predict(x_train)), \
accuracy_score(y_true=y_val, y_pred=svm_pred)))
print(‘kNN:\n> 训练数据的准确度 = {:.4f}’\
‘\n> 验证数据的准确度 = {:.4f}’\
.format(accuracy_score(y_true=y_train, \
y_pred=knn.predict(x_train)), \
accuracy_score(y_true=y_val, y_pred=knn_pred)))
输出如下:
SVM
训练数据的准确度 = 0.7205
验证数据的准确度 = 0.7430
kNN:
训练数据的准确度 = 0.7921
验证数据的准确度 = 0.6816
注意
由于随机化,您可能会得到与前一步骤中呈现的输出略有不同的结果。
如我们所见,堆叠模型不仅给出了比任何基模型显著更高的验证准确度,而且它的准确度几乎达到 89%,是本章讨论的所有集成模型中最高的。
注意
要访问此特定部分的源代码,请参考 packt.live/37QANjZ
。
您也可以在 packt.live/2YljJ2D
上在线运行此示例。您必须执行整个 Notebook 才能获得预期结果。
活动 6.01:使用独立和集成算法的堆叠
在本活动中,我们将使用波士顿房价:高级回归技术数据库(可以在 archive.ics.uci.edu/ml/machine-learning-databases/housing/
或 GitHub 上的 packt.live/2Vk002e
获得)。
该数据集旨在解决回归问题(即,目标变量具有一系列连续值)。在本活动中,我们将使用决策树、k 最近邻、随机森林和梯度提升算法训练各个回归模型。然后,我们将构建一个堆叠线性回归模型,使用所有这些算法,并比较每个模型的表现。我们将使用平均绝对误差(MAE)作为本活动的评估标准。
注意
MAE 函数(mean_absolute_error())可以像以前使用的 accuracy_score()度量一样使用。
需要执行的步骤如下:
导入相关库。
读取数据。
预处理数据集以去除空值,并进行独热编码以处理类别变量,为建模准备数据。
将数据集划分为训练和验证 DataFrame。
初始化字典以存储训练和验证的 MAE 值。
使用以下超参数训练一个 DecisionTreeRegressor 模型(dt)并保存分数:
dt_params = {
‘criterion’: ‘mae’,
‘min_samples_leaf’: 15,
‘random_state’: 11
}
使用以下超参数训练一个 KNeighborsRegressor 模型(knn)并保存分数:
knn_params = {
‘n_neighbors’: 5
}
使用以下超参数训练一个 RandomForestRegressor 模型(rf)并保存分数:
rf_params = {
‘n_estimators’: 20,
‘criterion’: ‘mae’,
‘max_features’: ‘sqrt’,
‘min_samples_leaf’: 10,
‘random_state’: 11,
‘n_jobs’: -1
}
使用以下超参数训练一个 GradientBoostingRegressor 模型(gbr)并保存分数:
gbr_params = {
‘n_estimators’: 20,
‘criterion’: ‘mae’,
‘max_features’: ‘sqrt’,
‘min_samples_leaf’: 10,
‘random_state’: 11
}
准备训练和验证数据集,四个元估计器的超参数与前面的步骤中使用的超参数相同。
训练一个线性回归模型(lr)作为堆叠模型。
可视化每个单独模型和堆叠模型的训练和验证误差。
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZZ2AC8ZH.jpg
图 6.14:训练和验证误差的可视化
注意
这个活动的解决方案可以通过以下链接找到。
因此,我们已经成功演示了堆叠作为一种集成技术,在不同数据集上,相较于任何单一机器学习模型,在验证集准确性方面表现更优。
总结
在本章中,我们首先讨论了过拟合和欠拟合,以及它们如何影响模型在未见数据上的表现。本章探讨了集成建模作为这些模型的解决方案,并进一步讨论了可以使用的不同集成方法,以及它们如何减少在进行预测时遇到的整体偏差或方差。我们首先讨论了袋装算法,并介绍了自助采样的概念。
接着,我们以随机森林作为经典的袋装集成方法的例子,解决了涉及在之前看到的泰坦尼克号数据集上构建袋装分类器和随机森林分类器的练习。然后,我们讨论了提升算法,它们如何成功地减少系统中的偏差,并理解了如何实现自适应提升和梯度提升。我们讨论的最后一种集成方法是堆叠,从练习中我们看到,堆叠在所有实现的集成方法中给出了最佳的准确性得分。虽然构建集成模型是减少偏差和方差的好方法,并且这些模型通常比单一模型表现更好,但它们本身也有各自的问题和使用场景。袋装在避免过拟合时非常有效,而提升方法则能减少偏差和方差,尽管它仍然可能会有过拟合的趋势。堆叠则适用于当一个模型在部分数据上表现良好,而另一个模型在另一部分数据上表现更好时。
在下一章,我们将详细探讨如何通过查看验证技术来克服过拟合和欠拟合的问题,验证技术即是判断我们模型表现的方式,以及如何使用不同的指标作为标志,为我们的使用案例构建最佳模型。
第八章:7. 模型评估
概述
本章介绍了如何通过使用超参数和模型评估指标来提升模型的性能。你将了解如何使用多种指标评估回归和分类模型,并学习如何选择合适的指标来评估和调优模型。
本章结束时,你将能够实施各种采样技术并进行超参数调整,以找到最佳模型。你还将具备计算特征重要性以进行模型评估的能力。
介绍
在前几章中,我们讨论了两种监督学习问题:回归和分类,然后介绍了集成模型,这些模型是由多个基础模型组合而成。我们构建了多个模型并讨论了它们的工作原理和原因。然而,这些还不足以将模型投入生产。模型开发是一个迭代过程,模型训练步骤之后是验证和更新步骤,如下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-91L52I4W.jpg
图 7.1:机器学习模型开发过程
本章将解释上图流程图中的外围步骤;我们将讨论如何选择合适的超参数,并如何使用合适的错误指标进行模型验证。提高模型性能是通过迭代执行这两个任务来完成的。那么,为什么评估模型如此重要呢?假设你已经训练好了模型,并提供了一些超参数,做出了预测,并得到了准确度。这就是模型的核心,但你如何确保你的模型能够发挥最佳性能呢?我们需要确保你制定的性能评估标准确实能够代表模型,并且它在未见过的测试数据集上也能表现良好。确保模型是最佳版本的关键步骤发生在初始训练之后:评估和提高模型性能的过程。本章将带你了解这一过程中所需的核心技术。
在本章中,我们将首先讨论为什么模型评估很重要,并介绍几种用于回归任务和分类任务的评估指标,这些指标可以用来量化模型的预测性能。接下来,我们将讨论保留数据集和 k 折交叉验证,并解释为什么必须有一个独立于验证集的测试集。之后,我们将探讨可以用来提高模型性能的策略。在上一章中,我们讨论了高偏差或高方差的模型如何导致次优的性能,以及如何通过构建模型集成来帮助我们建立一个稳健的系统,使预测更加准确,而不会增加整体方差。我们还提到了以下技术来避免将模型过度拟合到训练数据:
获取更多数据:一个高度复杂的模型可能容易在小数据集上过拟合,但在较大数据集上可能不容易过拟合。
降维:减少特征数量可以帮助使模型更简单。
正则化:在代价函数中添加一个新的项,以便将系数(特别是线性回归中的高次项系数)调整到较小的值。
在本章中,我们将介绍学习曲线和验证曲线,这些曲线可以帮助我们查看训练和验证误差的变化,了解模型是否需要更多数据,以及适当的复杂度级别在哪里。接下来我们将讨论超参数调优以提高性能,并简要介绍特征重要性。
注意
本章的所有相关代码可以在这里找到:https://packt.live/2T1fCWM。
导入模块并准备数据集
在前面的练习和活动中,我们使用了诸如平均绝对误差(MAE)和准确率等术语。在机器学习中,这些被称为评估指标,在接下来的部分中,我们将讨论一些有用的评估指标,它们是什么,如何使用以及何时使用它们。
注意
虽然这一部分没有被定位为练习,但我们鼓励你通过执行呈现的代码,仔细跟进这一部分。我们将在接下来的练习中使用这里展示的代码。
我们现在将加载在第六章《集成建模》中训练的数据和模型。我们将使用在活动 6.01《使用独立和集成算法进行堆叠》中创建的堆叠线性回归模型,以及在练习 6.06《使用随机森林构建集成模型》中创建的随机森林分类模型,来预测乘客的生存情况。
首先,我们需要导入相关的库:
import pandas as pd
import numpy as np
import pickle
%matplotlib inline
import matplotlib.pyplot as plt
接下来,加载第六章集成建模中处理过的数据文件。我们将使用 pandas 的 read_csv()
方法读取我们准备好的数据集,这些数据集将在本章的练习中使用。首先,我们将读取房价数据:
house_prices_reg = \
pd.read_csv(‘…/Datasets/boston_house_prices_regression.csv’)
house_prices_reg.head()
我们将看到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RII8SDX9.jpg
图 7.2:房价数据集的前五行
接下来,我们将读取 Titanic 数据:
titanic_clf = pd.read_csv(‘…/Datasets/titanic_classification.csv’)
titanic_clf.head()
我们将看到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-7X7RBYDH.jpg
图 7.3:Titanic 数据集的前五行
接下来,使用 pickle 库从二进制文件加载本章练习中将使用的模型文件:
使用open('../../Saved_Models/titanic_regression.pkl', 'rb')
打开文件:
reg = pickle.load(f)
使用open('../../Saved_Models/random_forest_clf.pkl', 'rb')
打开文件:
rf = pickle.load(f)
使用open('../../Saved_Models/stacked_linear_regression.pkl',\
‘rb’) as f:
reg = pickle.load(f)
到目前为止,我们已经成功加载了必要的数据集以及从前面的练习和活动中训练好的机器学习模型。在开始使用这些加载的数据集和模型探索评估指标之前,让我们首先了解不同类型的评估指标。
注意
你可以在以下链接找到保存的模型文件:https://packt.live/2vjoSwf。
评估指标
评估机器学习模型是任何项目中的一个关键部分:一旦我们让模型从训练数据中学习,下一步就是衡量模型的性能。我们需要找到一种指标,不仅能够告诉我们模型预测的准确性,还能让我们比较多个模型的性能,从而选择最适合我们用例的模型。
定义一个指标通常是我们在定义问题陈述时要做的第一件事,而且是在开始进行探索性数据分析之前,因为提前规划并思考如何评估我们所构建的任何模型的表现,以及如何判断其是否达到最佳表现,都是一个好主意。最终,计算性能评估指标将成为机器学习管道的一部分。
不用多说,回归任务和分类任务的评估指标是不同的,因为前者的输出值是连续的,而后者的输出值是类别型的。在本节中,我们将探讨可以用来量化模型预测性能的不同指标。
回归指标
对于输入变量 X,回归模型给出了一个预测值,该值可以取一系列不同的值。理想的情况是模型预测的值尽可能接近实际值 y。因此,二者之间的差异越小,模型表现得越好。回归度量通常涉及查看每个数据点的预测值与实际值之间的数值差异(即残差或误差值),然后以某种方式聚合这些差异。
让我们看一下下面的图表,该图展示了每个 X 点的实际值与预测值:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HF45QXRY.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R47Y9F4G.jpg)
图 7.4:线性回归问题中实际输出与预测输出之间的残差
然而,我们不能仅仅对所有数据点求平均值,因为可能会有数据点的预测误差为正或负,最终聚合会取消掉许多误差,严重高估模型的性能。
相反,我们可以考虑每个数据点的绝对误差并计算 MAE,公式如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HF45QXRY.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HF45QXRY.jpg)
图 7.5:平均绝对误差
这里,yi 和 ŷi 分别是第 i 个数据点的实际值和预测值。
MAE 是一个线性评分函数,这意味着在聚合误差时,它对每个残差赋予相等的权重。MAE 可以取从零到无穷大的任何值,且对误差的方向(正或负)不敏感。由于这些是误差指标,通常情况下,较低的值(尽可能接近零)是更可取的。
为了避免误差方向影响性能估计,我们还可以对误差项进行平方处理。对平方误差求平均得到均方误差(MSE):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TZPF7OIL.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-E2RVLXAK.jpg)
图 7.6:均方误差
虽然 MAE 与目标变量 y 具有相同的单位,但 MSE 的单位将是 y 的平方单位,这可能会使得在实际应用中评估模型时,MSE 的解释性稍差。不过,如果我们取 MSE 的平方根,就得到了均方根误差(RMSE):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R47Y9F4G.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TZPF7OIL.jpg)
图 7.7:均方根误差
由于在求平均之前,误差被平方,甚至少数较高的误差值也可能导致 RMSE 值显著增加。这意味着 RMSE 在判断我们希望惩罚大误差的模型时,比 MAE 更有用。
由于 MAE 和 RMSE 与目标变量具有相同的单位,因此很难判断某个特定的 MAE 或 RMSE 值是好还是坏,因为没有可参考的尺度。为了解决这个问题,通常使用一个度量标准,即 R² 得分或 R-squared 得分:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-E2RVLXAK.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-BTCJID5U.jpg)
图 7.8:R 平方分数
R2 分数的下限为 -∞,上限为 1。基本模型将目标变量预测为训练数据集目标值的均值 μ,数学表示为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-KMJDX6RT.jpg
图 7.9:训练数据集目标值的均值表达式
因此,对于基本模型:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-01N2DWDR.jpg
图 7.10:基本模型目标变量的表达式
记住这一点,如果 R2 的值为负,则表示训练模型的预测比简单预测所有数据的均值还差;如果 R2 值接近 1,则表示模型的 MSE 接近 0。
练习 7.01:计算回归指标
在本练习中,我们将使用与第六章“集成建模”中活动 6.01“单一算法与集成算法堆叠”中训练的相同模型和处理后的数据集来计算回归指标。我们将使用 scikit-learn 实现的 MAE 和 MSE:
注意
在开始本练习之前,确保已按照“导入模块和准备数据集”部分列出的方式导入相关库和模型。
此练习的代码可以在这里找到:
导入指标函数:
from sklearn.metrics import mean_absolute_error, \
mean_squared_error, r2_score
from math import sqrt
使用已加载的模型对给定数据进行预测。我们将使用与活动 6.01“单一算法与集成算法堆叠”中相同的特征,在第六章“集成建模”中,并使用该模型对加载的数据集进行预测。我们保存的列 y 是目标变量,我们将相应地创建 X 和 y:
X = house_prices_reg.drop(columns=[‘y’])
y = house_prices_reg[‘y’].values
y_pred = reg.predict(X)
计算 MAE、RMSE 和 R2 分数。让我们打印预测值中的 MAE 和 RMSE 的值。同时,打印模型的 R2 分数:
print(‘平均绝对误差 = {}’\
.format(mean_absolute_error(y, y_pred)))
print(‘均方根误差 = {}’\
.format(sqrt(mean_squared_error(y, y_pred))))
print(‘R 平方分数 = {}’.format(r2_score(y, y_pred)))
输出将如下所示:
平均绝对误差 = 2.874084343939712
均方根误差 = 4.50458397908091
R 平方分数 = 0.7634986504091822
我们可以看到,RMSE 高于 MAE。这表明存在一些残差特别高的数据点,较大的 RMSE 值突出了这一点。但是 R2 分数接近 1,表明该模型与基本模型相比,实际上具有接近理想的性能,基本模型会预测目标变量为平均值。
注意
要访问此特定部分的源代码,请参考 https://packt.live/3epdfp3。
你也可以在网上运行这个示例,网址是 packt.live/3hMLBnY
。你必须执行整个笔记本才能获得期望的结果。
分类指标
对于一个输入变量 X,分类任务会给出一个预测值,该值可以取有限的一组值(在二分类问题中为两个值)。由于理想情况下是预测每个数据点的类别与实际类别相同,因此没有衡量预测类别与实际类别之间距离的标准。因此,判断模型性能的标准就简单得多,基本上就是判断模型是否正确预测了类别。
判断分类模型的性能可以通过两种方式进行:使用数值指标,或通过绘制曲线并观察曲线的形状。我们将更详细地探讨这两种方法。
数值指标
判断模型性能最简单、最基本的方法是计算正确预测与总预测数之比,这给出了准确率,如下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-LTXKNB1V.jpg
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-LTXKNB1V.jpg
图 7.11:准确率
尽管准确率指标无论类别数目如何都是相同的,接下来的几个指标将在二分类问题的背景下讨论。此外,在许多情况下,准确率可能不是判断分类任务性能的最佳指标。
我们来看一个欺诈检测的示例:假设问题是检测某封邮件是否欺诈。我们的数据集在这种情况下是高度倾斜的(或不平衡的,也就是说,一个类别的数据点比另一个类别的数据点要多得多),其中 10,000 封邮件中有 100 封(占总数的 1%)被标记为欺诈(属于类别 1)。假设我们构建了两个模型:
第一个模型简单地将每封邮件预测为不是欺诈邮件,也就是说,10,000 封邮件中每一封都被分类为类别 0。在这种情况下,10,000 封邮件中的 9,900 封被正确分类,这意味着模型的准确率为 99%。
第二个模型预测了 100 封欺诈邮件为欺诈邮件,但也错误地将另外 100 封邮件预测为欺诈邮件。在这种情况下,100 个数据点在 10,000 个数据中被错误分类,模型的准确率为 99%。
我们如何比较这两个模型呢?构建欺诈检测模型的目的是让我们了解欺诈邮件检测的效果:正确分类欺诈邮件比将非欺诈邮件错误分类为欺诈邮件更为重要。尽管这两个模型的准确率相同,第二个模型实际上比第一个更有效。
由于准确率无法捕捉到这一点,我们需要混淆矩阵,一个具有 n 种不同预测值和实际值组合的表格,其中 n 是类别数。混淆矩阵本质上给出了分类问题预测结果的总结。图 7.12 显示了一个二分类问题的混淆矩阵示例:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-U9FWM5QS.jpg
图 7.12:混淆矩阵
由于这是一个二分类问题,前面的混淆矩阵可以直接视为一个混淆表,换句话说,它是一个包含真阳性、真阴性、假阳性和假阴性的矩阵,如图 7.13 所示。混淆表的大小始终是 2 x 2,无论是二分类还是多分类。在多分类的情况下,如果我们使用一对多分类方法,那么会有与类数相同数量的混淆表:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-QJ73DZR8.jpg
图 7.13:混淆表
以下是混淆表中使用的术语的含义:
真阳性和真阴性:这些是分别在正类和负类中正确预测的数据点数量。
假阳性:这也被称为类型 1 错误,指的是实际上属于负类的数据点,但被预测为正类的数量。延续前面的例子,假阳性案例会是如果一封正常的邮件被分类为欺诈邮件。
假阴性:这也被称为类型 2 错误,指的是实际上属于正类的数据点,但被预测为负类的数量。一个假阴性案例的例子是,如果一封欺诈邮件被分类为不是欺诈邮件。
从混淆矩阵中可以得出两个极其重要的指标:精确度和召回率:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-U4FYMQ8E.jpg
图 7.14:精确度
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3A2JB47P.jpg
图 7.15:召回率
精确度告诉我们预测为正类的有多少实际上是正类(从模型认为相关的结果中,有多少实际是相关的?),而召回率告诉我们有多少实际的正类被正确预测为正类(从实际相关的结果中,有多少被包括在模型的相关结果列表中?)。这两个指标在两类之间存在不平衡时尤其有用。
通常,模型的精度和召回率之间存在权衡:如果你必须召回所有相关结果,模型会生成更多不准确的结果,从而降低精度。另一方面,如果从生成的结果中获得更高比例的相关结果,则需要尽量减少包含的结果。在大多数情况下,你会优先考虑精度或召回率,这完全取决于问题的具体要求。例如,由于所有欺诈邮件是否被正确分类更为重要,因此召回率将是一个需要最大化的关键指标。
接下来的问题是,我们如何通过一个单一的数字来评估模型,而不是平衡两个独立的指标,既考虑精度又考虑召回率。F1 分数将两者结合为一个单一的数字,能够公平地评判模型,它等于精度和召回率的调和平均数:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-1WZ3SX0O.jpg
图 7.16: F1 分数
F1 分数的值始终介于 0(如果精度或召回率为 0)和 1(如果精度和召回率都为 1)之间。分数越高,表示模型的性能越好。F1 分数对精度和召回率给予相等的权重。它是 Fβ 指标的一个具体实例,其中 β 可以调整,以便对两个参数(召回率或精度分数)中的一个给予更多的权重,公式如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-D8FPG7UG.jpg
图 7.17: F β 分数
当 β < 1 时,更加注重精度,而 β > 1 时,更加注重召回率。F1 分数取 β = 1,给这两个参数相等的权重。
曲线图
有时候,我们不是预测类别,而是得到类别的概率。在二分类任务中,正类(A 类)和负类(B 类)的类别概率总和始终为 1(或 1),这意味着如果我们将分类概率等于 A 类的概率并应用一个阈值,我们实际上可以将其作为一个临界值来进行四舍五入(取 1)或舍去(取 0),从而得出最终的输出类别。
通常,通过改变阈值,我们可以得到一些分类概率接近 0.5 的数据点,这些数据点会从一个类别转换到另一个类别。例如,当阈值为 0.5 时,概率为 0.4 的数据点会被归为 B 类,而概率为 0.6 的数据点会被归为 A 类。但是,如果我们将阈值改为 0.35 或 0.65,这两个数据点就会被归为另一个类别。
事实证明,改变概率阈值会改变精确度和召回率的值,这可以通过绘制精确度-召回率曲线来捕捉。该图的 Y 轴表示精确度,X 轴表示召回率,对于从 0 到 1 的一系列阈值,绘制每个(召回率,精确度)点。连接这些点得到曲线。以下图形提供了一个示例:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-D9L0M4OZ.jpg
图 7.18:精确度-召回率曲线
我们知道,在理想情况下,精确度和召回率的值将是 1。这意味着,当将阈值从 0 增加到 1 时,精确度将保持在 1,但召回率会随着越来越多(相关)数据点被正确分类而从 0 增加到 1。因此,在理想情况下,精确度-召回率曲线本质上会是一个正方形,曲线下面积(AUC)将等于 1。
因此,我们可以看到,和 F1 分数一样,AUC 也是一个由精确度和召回率行为衍生的指标,它结合了这两者的值来评估模型的表现。我们希望模型能够实现尽可能高且接近 1 的 AUC。
接收者操作特征(ROC)曲线是另一种用于可视化分类模型性能的技术。ROC 曲线绘制了 Y 轴上的真正阳性率(TPR)和 X 轴上的假阳性率(FPR)之间的关系,跨越不同的分类概率阈值。TPR 与召回率完全相同(也被称为模型的敏感性),而 FPR 是特异度的补数(即 1 – FPR = 特异度);这两者都可以通过以下公式从混淆矩阵中得出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RJ4GSRCG.jpg
图 7.19:真正阳性率
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2SYKGQPB.jpg
图 7.20:假阳性率
以下图示显示了 ROC 曲线的示例,通过改变概率阈值,以使曲线上的每个点都代表一个(TPR,FPR)数据点,这些点对应于特定的概率阈值:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-5EWLE67F.jpg
图 7.21:ROC 曲线
ROC 曲线在类平衡较好的情况下更为有用,因为它们通过在假阳性率中使用真阴性来表示模型在类别不平衡的数据集上的有利输出(而这一点在精确度-召回率曲线中没有体现)。
练习 7.02:计算分类指标
在本次练习中,我们将使用第六章《集成建模》中训练的随机森林模型,并使用其预测结果生成混淆矩阵,计算精确度、召回率和 F1 分数,以此来评估我们的模型。我们将使用 scikit-learn 的实现来计算这些指标:
注意
在开始这个练习之前,请确保你已经导入了相关的库和模型,详见“导入模块和准备数据集”部分。
导入相关库和函数:
from sklearn.metrics import (accuracy_score, confusion_matrix, \
precision_score, recall_score, f1_score)
使用模型对所有数据点进行类别预测。我们将使用与之前相同的特征,并使用随机森林分类器对加载的数据集进行预测。scikit-learn 中的每个分类器都有一个.predict_proba()函数,我们将在这里与标准的.predict()函数一起使用,分别给出类别概率和类别:
X = titanic_clf.iloc[:, :-1]
y = titanic_clf.iloc[:, -1]
y_pred = rf.predict(X)
y_pred_probs = rf.predict_proba(X)
计算准确率:
print(‘Accuracy Score = {}’.format(accuracy_score(y, y_pred)))
输出将如下所示:
Accuracy Score = 0.6251402918069585
62.5%的准确率并不是很高,特别是考虑到如果每次猜测输出都像掷硬币一样,那么准确率将是 50%。然而,本次练习的目标是理解指标的作用。因此,在发现我们的分类器在准确率方面表现不佳后,我们将转向其他一些指标,帮助我们更详细地分析模型的表现。
打印混淆矩阵:
print(confusion_matrix(y_pred=y_pred, y_true=y))
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-882W286D.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-882W286D.jpg)
图 7.22:混淆矩阵
在这里,我们可以看到模型似乎有很多假阴性,这意味着我们可以预期该模型的召回率极低。同样,由于假阳性的数量只有一个,我们可以预期该模型具有较高的精度。
计算精度和召回率:
print(‘Precision Score = {}’.format(precision_score(y, y_pred)))
print(‘Recall Score = {}’.format(recall_score(y, y_pred)))
输出将如下所示:
Precision Score = 0.9
Recall Score = 0.02631578947368421
计算 F1 得分:
print(‘F1 Score = {}’.format(f1_score(y, y_pred)))
输出将如下所示:
F1 Score = 0.05113636363636364
我们可以看到,由于召回率极低,这也影响了 F1 得分,使得它接近零。
注释
若要访问该特定部分的源代码,请参考 packt.live/2V6mbYQ
。
你还可以在线运行此示例,网址为 packt.live/37XirOr
。你必须执行整个 Notebook 才能得到预期结果。
现在我们已经讨论了可以用来衡量模型预测性能的指标,接下来我们将讨论验证策略,在这些策略中,我们将使用某个指标来评估模型在不同情况下的表现。
数据集拆分
在确定一个模型表现如何时,一个常见的错误是计算模型在其训练数据上的预测误差,并基于在训练数据集上高准确度的结果得出模型表现非常好的结论。
这意味着我们正在尝试在模型已经见过的数据上进行测试,也就是说,模型已经学习了训练数据的行为,因为它已经接触过这些数据——如果要求模型再次预测训练数据的行为,它无疑会表现得很好。而且,在训练数据上的表现越好,模型对数据的了解程度可能越高,以至于它甚至学会了数据中的噪声和异常值的行为。
现在,高训练准确度导致模型具有高方差,正如我们在上一章中看到的那样。为了获得模型表现的无偏估计,我们需要找到模型在它没有在训练过程中接触过的数据上的预测准确度。这就是持留数据集发挥作用的地方。
持留数据
持留数据集是指从训练模型时被暂时保留的样本,它本质上对模型来说是“未见过”的。由于噪声是随机的,持留数据点很可能包含与训练数据集中的数据行为不同的异常值和噪声数据点。因此,计算持留数据集上的表现可以帮助我们验证模型是否过拟合,并且为我们提供模型表现的无偏见视角。
我们在上一章开始时将泰坦尼克号数据集分为训练集和验证集。那么,这个验证数据集是什么?它与测试数据集有什么不同?我们经常看到验证集和测试集这两个术语被交替使用——尽管它们都表示一个持留数据集,但它们在目的上有所不同:
验证数据:在模型从训练数据中学习后,它的表现会在验证数据集上进行评估。然而,为了使模型发挥最佳性能,我们需要对模型进行微调,并反复评估更新后的模型表现,这一过程是在验证数据集上完成的。通常,表现最好且通过验证数据集验证过的微调模型会被选为最终模型。
因此,尽管模型本质上没有从数据中学习,但它会在每次改进的迭代中多次接触到验证数据集。可以说,验证集虽然间接,但确实影响了模型。
测试数据:选择的最终模型现在将在测试数据集上进行评估。测量的性能将在此数据集上提供一个无偏的度量,这个度量将作为模型的最终性能指标。这个最终评估是在模型已经在合并的训练和验证数据集上完全训练之后进行的。在计算这个度量值后,不再对模型进行训练或更新。
这意味着模型只在计算最终性能度量时暴露于测试数据集一次。
需要记住的是,验证数据集绝不能用于评估模型的最终性能:如果模型已经看到并且在后续的修改中专门为提高在验证集上的表现而进行过调整,那么我们对模型真实性能的估计将会有正向偏差。
然而,只有一个保留的验证数据集确实存在一些局限性,因为模型在每次改进迭代中只进行一次验证,因此使用这个单一的评估可能很难捕捉到预测的不确定性。
将数据划分为训练集和验证集会减少用于训练模型的数据量,这可能导致模型具有较高的方差。
最终模型可能会过拟合于这个验证集,因为它是为了最大化在这个数据集上的表现而进行调优的。
如果我们使用称为 k 折交叉验证的验证技术,而不是使用单一的验证数据集,那么这些挑战是可以克服的。
K 折交叉验证
K 折交叉验证是一种验证技术,它通过本质上将验证集轮换成 k 个折叠,从而帮助我们得到一个无偏的模型性能估计。它是如何工作的:
首先,我们选择 k 的值并将数据划分为 k 个子集。
然后,我们将第一个子集作为验证集,其余数据用于训练模型。
我们在验证子集上测量模型的性能。
然后,我们将第二个子集作为验证子集,重复这一过程。
一旦我们完成了 k 次迭代,我们将所有折叠的性能度量值进行汇总,并呈现最终的度量值。
下图直观地解释了这一点:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-9PFF1KHD.jpg
图 7.23:K 折交叉验证
尽管这种验证方法计算开销较大,但其好处超过了成本。这种方法确保模型在训练数据集中的每个样本上都被验证一次,并且最终获得的性能估计不会偏向于验证数据集,尤其是在小型数据集的情况下。一个特殊情况是留一交叉验证,其中 k 的值等于数据点的数量。
抽样
现在我们已经讨论了用于拆分数据集以进行模型训练和验证的策略,让我们来讨论如何将数据点分配到这些拆分中。我们可以在拆分中采样数据的方式有两种,分别如下:
随机抽样:这就是从整体数据集中随机分配样本到训练集、验证集和/或测试集中。随机拆分数据仅在所有数据点彼此独立时有效。例如,如果数据以时间序列的形式呈现,随机拆分就不适用了,因为数据点是有序的,并且每个数据点都依赖于前一个数据点。随机拆分数据会破坏这种顺序,并且不会考虑这种依赖关系。一个常见的现实世界示例是手写数字分类任务,因为在这种情况下,所有数据样本(手写数字的图像)彼此独立,且数据在所有 10 个类别(数字)之间大致均匀分布。
分层抽样:这是一种确保每个子集的目标变量值分布与原始数据集相同的方法。例如,如果原始数据集中的两个类别的比例是 3:7,则分层抽样确保每个子集也包含按 3:7 比例分布的两个类别。
分层抽样很重要,因为在一个与模型训练数据集目标值分布不同的数据集上测试模型,可能会给出一个不代表模型实际性能的性能估计。
这种抽样技术的实际应用示例是在金融交易中的欺诈检测。由于欺诈事件发生的频率较低,因此欺诈(FRAUD)和非欺诈(NOT_FRAUD)类别之间的不平衡非常大。例如,假设我们有 1,000 笔金融交易,其中有 5 笔是欺诈性的,我们必须使用分层抽样来将这些交易分成训练集和测试集。如果不使用分层抽样,那么所有 5 笔欺诈交易可能都被分配到训练集(或测试集),这将导致我们无法进行有效的验证。
训练集、验证集和测试集样本的大小在模型评估过程中也起着重要作用。保留一个大数据集用于测试模型的最终性能,将有助于我们获得一个无偏的模型性能估计,并减少预测的方差,但如果测试集太大,以至于由于缺乏训练数据而影响模型的训练能力,这将严重影响模型的效果。这一点在较小的数据集中特别重要。
练习 7.03:使用分层抽样进行 K-折交叉验证
在此练习中,我们将实现基于 scikit-learn 的随机森林分类器的 K 折交叉验证与分层抽样。scikit-learn 中的 StratifiedKFold 类实现了交叉验证和抽样的结合,我们将在本练习中使用它:
注意
在开始此练习之前,请确保已导入《导入模块和准备数据集》部分中列出的相关库和模型。
导入相关类。我们将导入 scikit-learn 的 StratifiedKFold 类,它是 KFold 的变体,返回分层折叠,并与 RandomForestClassifier 一起使用:
from sklearn.metrics import accuracy_score
from sklearn.model_selection import StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
为训练准备数据并初始化 k 折对象。在这里,我们将使用五折来评估模型,因此将 n_splits 参数设置为 5:
X = titanic_clf.iloc[:, :-1].values
y = titanic_clf.iloc[:, -1].values
skf = StratifiedKFold(n_splits=5)
为每个折叠训练一个分类器并记录得分。StratifiedKFold 类的功能类似于我们在上一章中使用的 KFold 类,上一章为第六章《集成建模》中第 6.06 节《构建堆叠模型》中的内容。对于每个五折交叉验证,我们将在其他四折上进行训练,并在第五折上进行预测,然后计算预测结果与第五折之间的准确率。如上一章所示,skf.split() 函数接受要拆分的数据集作为输入,并返回一个迭代器,其中包含用于将训练数据细分为训练和验证的索引值:
scores = []
for train_index, val_index in skf.split(X, y):
X_train, X_val = X[train_index], X[val_index]
y_train, y_val = y[train_index], y[val_index]
rf_skf = RandomForestClassifier(**rf.get_params())
rf_skf.fit(X_train, y_train)
y_pred = rf_skf.predict(X_val)
scores.append(accuracy_score(y_val, y_pred))
scores
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-G9XNLB0R.jpg
图 7.24: 使用随机森林分类器的得分
打印聚合的准确率得分:
print(‘平均准确率得分 = {}’.format(np.mean(scores)))
输出将如下所示:
平均准确率得分 = 0.7105606912862568
注意
要查看此特定部分的源代码,请参考 https://packt.live/316TUF5。
你也可以在 https://packt.live/2V6JilY 在线运行此示例。你必须执行整个 Notebook,才能获得预期结果。
因此,我们已经演示了如何使用 k 折交叉验证来对模型性能进行稳健评估。我们在前述方法中使用了分层抽样,确保训练集和验证集具有相似的类别分布。接下来,我们将专注于如何提升模型性能。
性能提升策略
监督式机器学习模型的性能提升是一个迭代过程,通常需要不断的更新和评估循环才能得到完美的模型。尽管本章之前的部分讨论了评估策略,本节将讨论模型更新:我们将讨论一些方法,帮助我们确定模型需要什么来提升性能,以及如何对模型进行这些调整。
训练和测试误差的变化
在前一章中,我们介绍了欠拟合和过拟合的概念,并提到了解决这些问题的一些方法,随后引入了集成模型。但我们没有讨论如何识别我们的模型是否在训练数据上出现了欠拟合或过拟合。
通常查看学习曲线和验证曲线是很有帮助的。
学习曲线
学习曲线显示了随着训练数据量增加,训练误差和验证误差的变化。通过观察曲线的形状,我们可以大致判断增加更多数据是否会对建模产生积极影响,并可能提高模型的性能。
我们来看以下图:虚线表示验证误差,实线表示训练误差。左侧的图显示这两条曲线都趋向于一个较高的误差值。这意味着模型具有较高的偏差,增加更多数据不太可能影响模型的表现。因此,我们不必浪费时间和金钱去收集更多数据,而需要做的只是增加模型的复杂性。
另一方面,右侧的图显示了即使训练集中的数据点数量不断增加,训练误差和测试误差之间仍然存在显著差异。这个较大的差距表明系统的方差较高,这意味着模型出现了过拟合。在这种情况下,增加更多的数据点可能有助于模型更好地泛化,正如下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-LBZS46E0.jpg
图 7.25:数据量增加的学习曲线
那么我们如何识别完美的学习曲线呢?当我们的模型具有低偏差和低方差时,我们会看到类似下图的曲线。它显示了低训练误差(低偏差)以及训练曲线和验证曲线之间的低差距(低方差),因为它们会趋于一致。在实践中,我们能看到的最好的学习曲线是那些趋向不可减少的误差值(由于数据集中的噪声和异常值存在),如以下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-5EK0XCQE.jpg
图 7.26:低偏差和低方差模型在训练数据量增加时,训练误差和验证误差的变化
验证曲线
正如我们之前讨论的,机器学习模型的目标是能够对未见过的数据进行泛化。验证曲线帮助我们找到一个理想的点,这个点介于欠拟合和过拟合的模型之间,在这里模型能够很好地进行泛化。在前一章中,我们谈到了模型复杂度如何影响预测性能:我们说过,随着我们从一个过于简单的模型到一个过于复杂的模型,我们会从一个欠拟合的高偏差低方差模型,过渡到一个过拟合的低偏差高方差模型。
验证曲线展示了随着模型参数值变化,训练和验证误差的变化,其中该模型参数在某种程度上控制着模型的复杂度——这可以是线性回归中的多项式度数,或者是决策树分类器的深度:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2PLE7KLZ.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-1MWNFP2X.jpg)
图 7.27:随着模型复杂度增加,训练和验证的变化
前面的图示展示了随着模型复杂度(其模型参数是一个指标)变化,验证和训练误差是如何变化的。我们还可以看到,阴影区域之间的点就是总误差最小的地方,这正是欠拟合和过拟合之间的最佳点。找到这个点将帮助我们找到模型参数的理想值,从而构建一个低偏差和低方差的模型。
超参数调整
我们之前已经多次讨论了超参数调整。现在,让我们讨论为什么它如此重要。首先,需要注意的是,模型参数与模型超参数是不同的:前者是模型内部的,并且是从数据中学习得来的,而后者则定义了模型本身的架构。
超参数的示例包括以下内容:
用于线性回归的多项式特征的度数
决策树分类器的最大深度
随机森林分类器中要包含的树的数量
梯度下降算法使用的学习率
定义模型架构的设计选择可以对模型的表现产生巨大影响。通常,超参数的默认值是有效的,但找到超参数的完美组合可以大大提升模型的预测能力,因为默认值可能完全不适合我们正在尝试建模的问题。
在下面的图示中,我们可以看到改变两个超参数的值如何导致模型得分的巨大差异:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-1MWNFP2X.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2PLE7KLZ.jpg)
图 7.28:随着两个模型参数(X 轴和 Y 轴)值的变化,模型得分(Z 轴)的变化
通过探索一系列可能的值来找到完美的组合,这就是所谓的超参数调优。由于没有可用于最大化模型表现的损失函数,超参数调优通常只是通过实验不同的组合,并选择在验证过程中表现最好的组合。
我们有几种方法可以进行模型的超参数调优:
手动调优:当我们手动选择超参数的值时,这被称为手动调优。它通常效率低下,因为通过手工解决高维度优化问题不仅会很慢,而且也无法让模型达到最佳性能,因为我们可能不会尝试每一种超参数值的组合。
网格搜索:网格搜索涉及对每一个超参数值的组合进行模型训练和评估,并选择表现最佳的组合。由于这需要对超参数空间进行详尽的采样,因此从计算角度来看,它是相当低效的。
随机搜索:虽然第一种方法因为尝试的组合太少被认为效率低下,但第二种方法因为尝试的组合太多也被认为低效。随机搜索通过从之前定义的网格中随机选择一个超参数组合的子集,然后只对这些组合进行训练和评估,从而解决了这一问题。或者,我们还可以为每个超参数提供一个统计分布,从中随机抽取值。
随机搜索的逻辑由 Bergstra 和 Bengio 提出,指出如果网格中至少 5% 的点能提供接近最优的解,那么通过 60 次试验,随机搜索能够以 95% 的概率找到这一区域。
注意:
你可以阅读 Bergstra 和 Bengio 的论文,网址是 www.jmlr.org/papers/v13/bergstra12a.html
。
贝叶斯优化:前两种方法涉及独立地尝试超参数值的组合,并记录每个组合的模型表现。然而,贝叶斯优化通过顺序地进行实验并利用前一个实验的结果来改善下一个实验的采样方法。
练习 7.04:使用随机搜索进行超参数调优
在本练习中,我们将使用随机搜索方法进行超参数调优。我们将定义一个超参数范围的网格,并使用 RandomizedSearchCV 方法从该网格中随机采样。我们还将对每个组合的值进行 K 折交叉验证。本练习是练习 7.03“使用分层抽样执行 K 折交叉验证”的延续:
导入随机搜索的类:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV
准备训练数据并初始化分类器。这里,我们将初始化我们的随机森林分类器而不传递任何参数,因为这只是一个基础对象,将为每个网格点实例化进行随机搜索:
X = titanic_clf.iloc[:, :-1].values
y = titanic_clf.iloc[:, -1].values
rf_rand = 随机森林分类器()
def report(results, max_rank=3):
for rank in range(1, max_rank+1):
results_at_rank = np.flatnonzero\
(results[‘rank_test_score’] == i)
def report(results, n_top=3):
for i in range(1, n_top + 1):
candidates = np.flatnonzero\
(results[‘rank_test_score’] == i)
for candidate in candidates:
print(“排名模型:{0}”.format(i))
print(“平均验证得分:{0:.3f}(标准差:{1:.3f})”\
.format(results[‘mean_test_score’][candidate], \
results[‘std_test_score’][candidate]))
print(“参数:{0}”.format(results[‘params’]\
[candidate]))
print(“”)
指定要采样的参数。这里,我们将列出每个超参数在网格中需要的不同值:
param_dist = {“n_estimators”: list(range(10,210,10)), \
“max_depth”: list(range(3,20)), \
“max_features”: list(range(1, 10)), \
“min_samples_split”: list(range(2, 11)), \
“bootstrap”: [True, False], \
“criterion”: [“gini”, “entropy”]}
执行随机搜索。我们初始化随机搜索对象,指定我们要运行的试验总数、参数值字典、评分函数以及 K 折交叉验证的折数。然后,我们调用 .fit() 函数执行搜索:
n_iter_search = 60
random_search = RandomizedSearchCV(rf_rand, \
param_distributions=param_dist, \
scoring=‘accuracy’, \
n_iter=n_iter_search, cv=5)
random_search.fit(X, y)
输出结果将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-AFPFRK4C.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VHCZRJC2.jpg)
图 7.29:随机搜索 CV 输出
打印前五名模型的得分和超参数。将结果字典转换为 pandas DataFrame,并按 rank_test_score 排序。然后,对于前五行,打印排名、平均验证得分和超参数:
results = pd.DataFrame(random_search.cv_results_)\
.sort_values(‘rank_test_score’)
for i, row in results.head().iterrows():
print(“模型排名:{}”.format(row.rank_test_score))
print(“平均验证得分:{:.3f}(标准差:{:.3f})”\
.format(row.mean_test_score, row.std_test_score))
print(“模型超参数:{}\n”.format(row.params))
输出结果将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VHCZRJC2.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-AFPFRK4C.jpg)
图 7.30:前五名模型的得分和超参数
生成随机搜索 CV 结果的报告
report(random_search.cv_results_)
输出结果将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZKVL6UYV.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZKVL6UYV.jpg)
图 7.31:随机搜索 CV 结果报告
注:
要访问该部分的源代码,请参阅 https://packt.live/314tqUX。
你也可以在 https://packt.live/2V3YC2z 在线运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。
我们可以看到,表现最佳的模型只有 70 棵树,而排名第 2 到第 7 的模型有 160 多棵树。此外,排名第 5 的模型只有 10 棵树,仍然表现得与更复杂的模型相当。这表明,随机森林模型中的树木数量并不完全能反映模型的表现。
一个模型的表现会受到其他因素的影响,包括以下几点:
每棵树使用的最大特征数(max_features)
每棵树选择的特征有多具描述性
这些特征集在树间的区别有多大
用于训练每棵树的数据样本数
数据实例在决策树中经过多少次决策(max_depth)
树叶中允许的最小样本数(min_samples_split)等等。
特征重要性
尽管关注模型性能至关重要,但理解模型中各特征如何对预测结果产生影响也是很重要的:
我们需要能够向相关利益相关者解释模型及其不同变量如何影响预测,以便他们了解为什么我们的模型是成功的。
数据可能存在偏差,在这些数据上训练模型可能会影响模型的表现,并导致模型评估结果偏颇,在这种情况下,通过找到重要特征并分析它们来解释模型的能力将有助于调试模型的性能。
除了前一点之外,还必须注意到某些模型偏差可能是社会上或法律上不可接受的。例如,如果一个模型的表现良好,因为它隐式地对基于种族的特征赋予了很高的权重,这可能会引发问题。
除了这些点,寻找特征重要性还可以帮助进行特征选择。如果数据具有高维度并且训练后的模型具有较高的方差,去除那些重要性低的特征是一种通过降维来降低方差的方式。
练习 7.05:使用随机森林进行特征重要性分析
在这个练习中,我们将从之前加载的随机森林模型中找出特征的重要性。这个练习是练习 7.04 “使用随机搜索进行超参数调优”的延续。
找到特征重要性。让我们找到特征的重要性并将其保存在一个 pandas DataFrame 中,索引设置为列名,并按降序对该 DataFrame 进行排序:
feat_imps = pd.DataFrame({‘importance’: rf.feature_importances_}, \
index=titanic_clf.columns[:-1])
feat_imps.sort_values(by=‘importance’, ascending=False, \
inplace=True)
绘制特征重要性柱状图:
feat_imps.plot(kind=‘bar’, figsize=(10,7))
plt.legend()
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-T0MUHCEU.jpg
图 7.32:特征直方图
在这里,我们可以看到,性别、票价和舱位特征似乎具有最高的重要性;也就是说,它们对目标变量的影响最大。
注意
要访问此特定部分的源代码,请参考 https://packt.live/2YYnxWz。
你也可以在 https://packt.live/2Yo896Y 在线运行这个示例。你必须执行整个 Notebook 才能获得所需的结果。
活动 7.01:最终测试项目
在本活动中,我们将使用在第五章《分类技术》中使用的乳腺癌诊断数据集(有关数据集的详细信息,请参见活动 5.04,使用人工神经网络进行乳腺癌诊断分类),来解决一个二分类问题,其中我们必须预测乳腺细胞是良性还是恶性,给定的特征。在这个问题中,我们希望最大化我们的召回率;也就是说,我们希望能够识别所有恶性细胞,因为如果错过其中任何一个,我们可能会误判没有癌症,而实际上有癌症。而且,我们不希望发生这种情况。
我们将使用 scikit-learn 中的梯度提升分类器来训练模型。本活动作为一个最终项目,旨在帮助巩固本书中学到的概念的实践方面,特别是在本章中。
我们将使用随机搜索与交叉验证来为模型找到最优的超参数组合。然后,我们将使用梯度提升算法在数据集的一部分上构建最终分类器,并使用我们在数据集的剩余部分上学到的分类度量来评估其性能。我们将使用精确度和召回率作为此活动的评估标准。
执行的步骤如下:
导入相关库。
读取 breast-cancer-data.csv 数据集。
将数据集分成训练集和测试集。
选择一个基础模型,并定义与该模型对应的超参数值范围,以进行超参数调优。
定义初始化 RandomizedSearchCV 对象的参数,并使用 K 折交叉验证找到最佳的模型超参数。
将训练数据集进一步划分为训练集和验证集,并在划分后的训练数据集上使用最终超参数训练一个新模型。
计算与验证集相关的预测的准确性、精确度和召回率,并打印混淆矩阵。
尝试不同的阈值,以找到具有高召回率的最优点。绘制精确度-召回率曲线。
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-W7R7BD57.jpg
图 7.33:精确度召回率曲线
确定一个阈值,用于与测试数据集相关的预测。
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-7WEPT1JF.jpg
图 7.34:随着阈值增加而变化的精确度和召回率
预测测试数据集的最终数值。
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8Q00N88H.jpg
图 7.35:癌症数据集的预测结果
注意:
可通过此链接找到此活动的解决方案。
概要
本章讨论了为什么在监督学习中模型评估很重要,并查看了用于评估回归和分类任务的几个重要指标。我们看到,虽然回归模型评估相对直接,但分类模型的性能可以用多种方式衡量,具体取决于我们希望模型优先考虑的内容。除了数值指标外,我们还看了如何绘制精确率-召回率曲线和 ROC 曲线,以更好地解释和评估模型性能。在此之后,我们讨论了为什么通过计算模型在训练数据上的预测误差来评估模型是个坏主意,以及如何在模型已经看到的数据上测试模型会导致模型具有很高的方差。因此,我们引入了保留数据集的概念,并演示了为什么 K 折交叉验证是一种有用的策略,以及确保模型训练和评估过程保持无偏的抽样技术。性能改进策略的最后一部分从讨论学习曲线和验证曲线开始,以及如何解释它们以推动模型开发过程朝着找到性能更好的模型方向发展。接着是一节关于通过调整超参数来提升性能的讨论,并简要介绍了特征重要性的概念。
从监督学习和回归分类模型的基本原理到集成学习和模型性能评估的概念,我们现在已经为我们的监督学习工具包添加了所有必要的工具。这意味着我们已经准备好开始处理真实的监督学习项目,并应用我们通过这个研讨会获得的所有知识和技能。
第九章
附录
1. 基础知识
第十章:活动 1.01:实现 Pandas 函数
打开一个新的 Jupyter notebook。
使用 pandas 加载 Titanic 数据集:
import pandas as pd
df = pd.read_csv(r’…/Datasets/titanic.csv’)
使用以下方法在数据集上调用 head 函数:
查看数据的前五个样本
df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-C8LRLI2Q.jpg
图 1.26:前五行
使用以下方法调用 describe 函数:
df.describe(include=‘all’)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-08I9W255.jpg
图 1.27:describe()的输出
我们不需要 Unnamed: 0 列。可以通过以下方法删除该列,而无需使用 del 命令:
del df[‘Unnamed: 0’]
df = df[df.columns[1:]] # 使用列
df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3IS9NYQ8.jpg
图 1.28:删除 Unnamed: 0 列后的前五行
计算数据框列的平均值、标准差、最小值和最大值,无需使用 describe:
df.mean()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-UT30C1VE.jpg
图 1.29:mean()的输出
现在,计算标准差:
df.std()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HM1BXAG1.jpg
图 1.30:std()的输出
计算列的最小值:
df.min()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-OZ5WBQAJ.jpg
图 1.31:min()的输出
接下来,计算数据框中列的最大值。
df.max()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-EM11IS7E.jpg
图 1.32:max()的输出
使用 33%、66%和 99%的分位数方法,如下所示代码片段:
df.quantile(0.33)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-4IN2M7WQ.jpg
图 1.33:33%分位数的输出
类似地,使用 66%的分位数方法:
df.quantile(0.66)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-Q0TROBCG.jpg
图 1.34:66%分位数的输出
使用相同的方法处理 99%:
df.quantile(0.99)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-7CC087HR.jpg
图 1.35:99%分位数的输出
使用 groupby 方法查找每个类别的乘客数量:
class_groups = df.groupby(‘Pclass’)
for name, index in class_groups:
print(f’类别: {name}: {len(index)}')
输出将如下所示:
类别:1:323
类别:2:277
类别:3:709
使用选择/索引方法找出每个类别的乘客数量:
for clsGrp in df.Pclass.unique():
num_class = len(df[df.Pclass == clsGrp])
print(f’类别 {clsGrp}: {num_class}')
结果将如下所示:
类别 3:709
类别 1:323
类别 2:277
第 6 步和第 7 步的答案是匹配的。
确定第三类中最年长的乘客:
third_class = df.loc[(df.Pclass == 3)]
third_class.loc[(third_class.Age == third_class.Age.max())]
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3E0I4FN3.jpg
图 1.36:第三类中最年长的乘客
对于许多机器学习问题,将数值缩放至 0 和 1 之间是非常常见的做法。使用 agg 方法和 Lambda 函数将 Fare 和 Age 列缩放到 0 和 1 之间:
fare_max = df.Fare.max()
age_max = df.Age.max()
df.agg({‘Fare’: lambda x: x / fare_max, \
‘Age’: lambda x: x / age_max,}).head()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-B8SLI1KY.jpg
图 1.37:将数值缩放至 0 和 1 之间
确定数据集中没有列出票价的个人条目:
df_nan_fare = df.loc[(df.Fare.isna())]
df_nan_fare
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-FKM88B4Y.jpg
图 1.38:没有列出票价的个人
使用 groupby 方法将此行的 NaN 值替换为与相同舱位和登船地点对应的平均票价值:
embarked_class_groups = df.groupby([‘Embarked’, ‘Pclass’])
indices = embarked_class_groups\
.groups[(df_nan_fare.Embarked.values[0], \
df_nan_fare.Pclass.values[0])]
mean_fare = df.iloc[indices].Fare.mean()
df.loc[(df.index == 1043), ‘Fare’] = mean_fare
df.iloc[1043]
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HQ9K9JV1.jpg
图 1.39:没有列出票价详细信息的个人输出
注
要访问此部分的源代码,请参阅 https://packt.live/2AWHbu0。
您也可以在线运行此示例,网址是 https://packt.live/2NmAnse。您必须执行整个笔记本才能获得期望的结果。
2. 探索性数据分析与可视化
第十一章:活动 2.01:汇总统计与缺失值
完成此活动的步骤如下:
导入所需的库:
import json
import pandas as pd
import numpy as np
import missingno as msno
from sklearn.impute import SimpleImputer
import matplotlib.pyplot as plt
import seaborn as sns
读取数据。使用 pandas 的 .read_csv
方法将 CSV 文件读取到 pandas DataFrame 中:
data = pd.read_csv(‘…/Datasets/house_prices.csv’)
使用 pandas 的 .info()
和 .describe()
方法查看数据集的汇总统计信息:
data.info()
data.describe().T
info() 方法的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZT8SEDKR.jpg
图 2.50:info() 方法的输出(缩略)
describe() 方法的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-OOF44R7X.jpg
图 2.51:describe() 方法的输出(缩略)
查找每列中缺失值的总数和缺失值的总百分比,并按缺失百分比降序显示至少有一个空值的列。
正如我们在练习 2.02:可视化缺失值 中所做的那样,我们将对 DataFrame 使用 .isnull()
函数来获取一个掩码,使用 .sum()
函数计算每列中的空值数量,使用 .mean()
函数计算空值的比例,并乘以 100 将其转换为百分比。然后,我们将使用 pd.concat()
将缺失值的总数和百分比合并到一个 DataFrame 中,并根据缺失值的百分比对行进行排序:
mask = data.isnull()
total = mask.sum()
percent = 100*mask.mean()
missing_data = pd.concat([total, percent], axis=1, join=‘outer’, \
keys=[‘count_missing’, ‘perc_missing’])
missing_data.sort_values(by=‘perc_missing’, ascending=False, \
inplace=True)
missing_data[missing_data.count_missing > 0]
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-4D9WP7GA.jpg
图 2.52:每列缺失值的总数和百分比
绘制空值矩阵和空值相关热图。首先,我们找到至少有一个空值的列名列表。然后,使用 missingno 库为这些列中的数据绘制空值矩阵(如同在练习 2.02:可视化缺失值 中所做的那样),并绘制空值相关热图:
nullable_columns = data.columns[mask.any()].tolist()
msno.matrix(data[nullable_columns].sample(500))
plt.show()
msno.heatmap(data[nullable_columns], vmin = -0.1, \
figsize=(18,18))
plt.show()
空值矩阵如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VEDY4A9O.jpg
图 2.53:空值矩阵
空值相关热图将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-S4SOOAAB.jpg
图 2.54:空值相关性热图
删除缺失值超过 80% 的列。使用我们在第 2 步中创建的 DataFrame 的 .loc 操作符,仅选择缺失值少于 80% 的列:
data = data.loc[:,missing_data[missing_data.perc_missing < 80].index]
将 FireplaceQu 列中的空值替换为 NA 值。使用 .fillna() 方法将空值替换为 NA 字符串:
data[‘FireplaceQu’] = data[‘FireplaceQu’].fillna(‘NA’)
data[‘FireplaceQu’]
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-1P5WR7JE.jpg
图 2.55:替换空值
注意
要访问此特定部分的源代码,请参阅 https://packt.live/316c4a0。
您还可以在 https://packt.live/2Z21v5c 上在线运行此示例。您必须执行整个 Notebook 才能获得所需的结果。
活动 2.02:以可视化方式表示值的分布
使用 Matplotlib 绘制目标变量 SalePrice 的直方图。首先,我们使用 plt.figure 命令初始化图形并设置图形大小。然后,使用 matplotlib 的 .hist() 函数作为主要绘图函数,将 SalePrice 系列对象传递给它以绘制直方图。最后,我们指定坐标轴标签并显示图形:
plt.figure(figsize=(8,6))
plt.hist(data.SalePrice, bins=range(0,800000,50000))
plt.ylabel(‘房屋数量’)
plt.xlabel(‘销售价格’)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-QTG3WQJ1.jpg
图 2.56:目标变量的直方图
查找每个列中具有对象类型的唯一值的数量。通过对原始 DataFrame 使用 .select_dtypes 函数来选择那些具有 numpy.object 数据类型的列,创建一个名为 object_variables 的新 DataFrame。然后,使用 .nunique() 函数查找此 DataFrame 中每列的唯一值数量,并对结果进行排序:
object_variables = data.select_dtypes(include=[np.object])
object_variables.nunique().sort_values()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-KSB6TTSG.jpg
图 2.57:每个具有对象类型的列中的唯一值数量(已截断)
创建一个 DataFrame 来表示 HouseStyle 列中每个类别值的出现次数。使用 .value_counts() 函数按降序计算每个值的频率,以 pandas 系列的形式,然后重置索引以生成 DataFrame,并根据索引排序值:
counts = data.HouseStyle.value_counts(dropna=False)
counts.reset_index().sort_values(by=‘index’)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-FK26JP04.jpg
图 2.58:HouseStyle 列中每个类别值的出现次数
绘制一个饼图表示这些计数。如同第 1 步中一样,我们使用 plt.figure()初始化图形,并分别使用 plt.title()和 plt.show()方法设置图表标题和显示图形。主要绘图函数是 plt.pie(),我们将前一步创建的系列传递给它:
fig, ax = plt.subplots(figsize=(10,10))
slices = ax.pie(counts, labels = counts.index, \
colors = [‘white’], \
wedgeprops = {‘edgecolor’: ‘black’})
patches = slices[0]
hatches = [‘/’, ‘\’, ‘|’, ‘-’, ‘+’, ‘x’, ‘o’, ‘O’, ‘.’, ‘*’]
colors = [‘white’, ‘white’, ‘lightgrey’, ‘white’, \
‘lightgrey’, ‘white’, ‘lightgrey’, ‘white’]
for patch in range(len(patches)):
patches[patch].set_hatch(hatches[patch])
patches[patch].set_facecolor(colors[patch])
plt.title(‘显示不同房屋样式计数的饼图’)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-JTOOX73Y.jpg
图 2.59:表示计数的饼图
找出每一列中具有数字类型的唯一值数量。如同在第 2 步中执行的那样,现在选择具有 numpy.number 数据类型的列,并使用.nunique()查找每列的唯一值数量。将结果序列按降序排序:
numeric_variables = data.select_dtypes(include=[np.number])
numeric_variables.nunique().sort_values(ascending=False)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3RPWU49J.jpg
图 2.60:每个数值列中唯一值的数量(已截断)
使用 seaborn 绘制 LotArea 变量的直方图。使用 seaborn 的.distplot()函数作为主要绘图函数,需要传递 DataFrame 中的 LotArea 系列(去除任何空值,使用.dropna()方法删除空值)。为了改善图形视图,还可以设置 bins 参数,并使用 plt.xlim()指定 X 轴范围:
plt.figure(figsize=(10,7))
sns.distplot(data.LotArea.dropna(), bins=range(0,100000,1000))
plt.xlim(0,100000)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-YDX4ZTO7.jpg
图 2.61:LotArea 变量的直方图
计算每列值的偏度和峰度值:
data.skew().sort_values()
data.kurt()
偏度值的输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-EDDXHS8I.jpg
图 2.62:每列的偏度值(已截断)
峰度值的输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-WMP9BEKX.jpg
图 2.63:每列的峰度值(已截断)
注意
要查看此特定部分的源代码,请访问 https://packt.live/3fR91qj。
你还可以在 https://packt.live/37PYOI4 上在线运行此示例。你必须执行整个 Notebook 才能获得期望的结果。
活动 2.03:数据内的关系
绘制数据集的相关性热图。正如我们在练习 2.13:绘制相关性热图中所做的那样,使用 seaborn 的.heatmap()函数绘制热图,并传递通过 pandas 的.corr()函数计算出的特征相关性矩阵。除此之外,使用 cmap 参数将颜色映射设置为 RdBu,并分别使用 vmin 和 vmax 参数将颜色刻度的最小值和最大值设置为-1 和 1:
plt.figure(figsize = (12,10))
sns.heatmap(data.corr(), square=True, cmap=“RdBu”, \
vmin=-1, vmax=1)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VCRY5EOD.jpg
图 2.64:数据集的相关性热图
使用以下特征子集绘制更紧凑的热图,并在热图上添加相关性值注释:
feature_subset = [‘GarageArea’,‘GarageCars’,‘GarageCond’, \
‘GarageFinish’, ‘GarageQual’,‘GarageType’, \
‘GarageYrBlt’,‘GrLivArea’,‘LotArea’, \
‘MasVnrArea’,‘SalePrice’]
现在与前一步相同,这次只选择数据集中的上述列,并将参数 annot 添加到主绘图函数中,值为 True,其他内容保持不变:
plt.figure(figsize = (12,10))
sns.heatmap(data[feature_subset].corr(), square=True, \
annot=True, cmap=“RdBu”, vmin=-1, vmax=1)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-20XN5WXG.jpg
图 2.65:带有相关性值注释的特征子集相关性热图
显示相同特征子集的 Pairplot,主对角线为 KDE 图,其他位置为散点图。使用 seaborn 的.pairplot()函数绘制 DataFrame 中选定列的非空值的 Pairplot。为了渲染对角线的 KDE 图,将 kde 传递给 diag_kind 参数,而将 scatter 传递给 kind 参数,以设置所有其他图为散点图:
sns.pairplot(data[feature_subset].dropna(), \
kind =‘scatter’, diag_kind=‘kde’)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-NOERLBX6.jpg
图 2.66:相同特征子集的 Pairplot
创建一个箱线图,展示每个 GarageCars 类别下 SalePrice 的变化。在这里使用的主要绘图函数是 seaborn 的.boxplot()函数,我们将 DataFrame 以及 x 和 y 参数传递给它,前者是分类变量,后者是我们想要查看每个类别内部变化的连续变量,即 GarageCars 和 SalePrice:
plt.figure(figsize=(10, 10))
sns.boxplot(x=‘GarageCars’, y=“SalePrice”, data=data)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-IOJJQAGE.jpg
图 2.67: 箱型图显示每个 GarageCars 类别中销售价格的变化
使用 seaborn 绘制折线图,显示从较旧到最近建成的公寓销售价格的变化。在这里,我们将使用 seaborn 的 .lineplot()
函数绘制折线图。由于我们想查看销售价格的变化,因此我们将销售价格作为 y 变量,并且由于变化跨越了一段时间,我们将建造年份 (YearBuilt) 作为 x 变量。考虑到这一点,我们将相应的系列数据作为值传递给主要绘图函数的 y 和 x 参数。同时,我们还传递 ci=None
参数,以隐藏图中线条周围的标准偏差指示器:
plt.figure(figsize=(10,7))
sns.lineplot(x=data.YearBuilt, y=data.SalePrice, ci=None)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-GI1DMCQ1.jpg
图 2.68: 折线图显示从较旧到最近建成的公寓销售价格的变化
图 2.68 展示了如何使用折线图来突出显示整体趋势以及短期时间周期中的波动。你可能想将此图表与相同数据的散点图进行比较,并考虑每种图表传达了什么信息。
注意
要访问此部分的源代码,请参阅 https://packt.live/2Z4bqHM。
你也可以在线运行这个示例,网址是 https://packt.live/2Nl5ggI。你必须执行整个 Notebook 才能得到预期的结果。
3. 线性回归
第十二章:活动 3.01:使用移动平均绘制数据
加载所需的两个包:
import pandas as pd
import matplotlib.pyplot as plt
从 CSV 文件将数据集加载到 pandas 数据框中:
df = pd.read_csv(‘…/Datasets/austin_weather.csv’)
df.head()
输出将显示 austin_weather.csv 文件的前五行:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2E71SJK6.jpg
图 3.61:奥斯丁天气数据的前五行(请注意右侧的附加列未显示)
由于我们只需要日期和温度列,我们将从数据集中删除所有其他列:
df = df.loc[:, [‘Date’, ‘TempAvgF’]]
df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TIPMXHMW.jpg
图 3.62:奥斯丁天气数据的日期和温度列
最初,我们只关心第一年的数据,因此我们需要仅提取该信息。在数据框中为年份创建一列,从日期列中的字符串提取年份值作为整数,并将这些值赋给年份列(请注意,温度是按天记录的)。重复此过程以创建月份和日期列,然后提取第一年的数据:
df.loc[:, ‘Year’] = df.loc[:, ‘Date’].str.slice(0, 4).astype(‘int’)
df.loc[:, ‘Month’] = df.loc[:, ‘Date’].str.slice(5, 7).astype(‘int’)
df.loc[:, ‘Day’] = df.loc[:, ‘Date’].str.slice(8, 10).astype(‘int’)
df = df.loc[df.index < 365]
print(df.head())
print(df.tail())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-NKHKLFEF.jpg
图 3.63:包含一年数据的新数据框
使用 rolling() 方法计算 20 天移动平均:
window = 20
rolling = df.TempAvgF.rolling(window).mean()
print(rolling.head())
print(rolling.tail())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-EXGYSL7Q.jpg
图 3.64:带有移动平均数据的数据框
绘制原始数据和移动平均数据,x 轴为年份中的天数:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
ax.scatter(df.index, df.TempAvgF, \
label = ‘原始数据’, c = ‘k’)
移动平均
ax.plot(rolling.index, rolling, c = ‘r’, \
linestyle = ‘–’, label = f’{window} 天移动平均’)
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘天数’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df.index.min(), df.index.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8UW6PKOO.jpg
图 3.65:带有 20 天移动平均线的数据
注意
要访问此特定部分的源代码,请参考 https://packt.live/2Nl5m85。
你也可以在 https://packt.live/3epJvs6 上在线运行这个例子。你必须执行整个笔记本才能得到期望的结果。
活动 3.02:使用最小二乘法的线性回归
导入所需的包和类:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
从 CSV 文件(austin_weather.csv)加载数据并检查数据(使用 head() 和 tail() 方法):
加载数据并检查
df = pd.read_csv(‘…/Datasets/austin_weather.csv’)
print(df.head())
print(df.tail())
df.head() 的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-BFFQ5FTD.jpg
图 3.66:df.head() 的输出
df.tail() 的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8HWYSH8L.jpg
图 3.67:df.tail() 的输出
删除除了 Date 和 TempAvgF 列以外的所有列:
df = df.loc[:, [‘Date’, ‘TempAvgF’]]
df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TBONUR9Z.jpg
图 3.68:用于活动 3.02 的两列
创建新的 Year、Month 和 Day 列,并通过解析 Date 列来填充它们:
添加一些有用的列
df.loc[:, ‘Year’] = df.loc[:, ‘Date’]\
.str.slice(0, 4).astype(‘int’)
df.loc[:, ‘Month’] = df.loc[:, ‘Date’]\
.str.slice(5, 7).astype(‘int’)
df.loc[:, ‘Day’] = df.loc[:, ‘Date’]\
.str.slice(8, 10).astype(‘int’)
print(df.head())
print(df.tail())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HRXD8ELZ.jpg
图 3.69:增强数据
创建一个新的列用于移动平均,并用 TempAvgF 列的 20 天移动平均填充它:
“”"
设置 20 天窗口,然后使用它来平滑温度并填充到新列中
“”"
window = 20
df[‘20_d_mov_avg’] = df.TempAvgF.rolling(window).mean()
print(df.head())
print(df.tail())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-W20OBT4B.jpg
图 3.70:添加 20 天移动平均线
切割出一整年的数据用于模型。确保该年份没有因移动平均而缺失数据。同时创建一个 Day_of_Year 列(它应该从 1 开始):
“”"
现在让我们精确切割出一年时间的数据
日历的开始和结束日期
从之前的输出中我们可以看到
2014 年是第一个拥有完整数据的年份,
然而,它仍然会有 NaN 值
移动平均,所以我们将使用 2015 年
“”"
df_one_year = df.loc[df.Year == 2015, :].reset_index()
df_one_year[‘Day_of_Year’] = df_one_year.index + 1
print(df_one_year.head())
print(df_one_year.tail())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3PS8EM65.jpg
图 3.71:一年的数据
创建原始数据(原始 TempAvgF 列)的散点图,并叠加 20 天移动平均线:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \
label = ‘原始数据’, c = ‘k’)
移动平均
ax.plot(df_one_year.Day_of_Year, df_one_year[‘20_d_mov_avg’], \
c = ‘r’, linestyle = ‘–’, \
label = f’{window}天移动平均’)
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘天数’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df_one_year.Day_of_Year.min(), \
df_one_year.Day_of_Year.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-7IRREJQI.jpg
图 3.72:叠加了 20 天移动平均的原始数据
使用默认参数创建一个线性回归模型,即为模型计算一个 y 截距,并且不对数据进行归一化。年份的天数(1 到 365)构成输入数据,平均温度构成输出数据。打印模型的参数和 r²值:
拟合线性模型
linear_model = LinearRegression(fit_intercept = True)
linear_model.fit(df_one_year[‘Day_of_Year’]\
.values.reshape((-1, 1)), \
df_one_year.TempAvgF)
print(‘模型斜率:’, linear_model.coef_)
print(‘模型截距:’, linear_model.intercept_)
print(‘模型 r²值:’, \
linear_model.score(df_one_year[‘Day_of_Year’]\
.values.reshape((-1, 1)), \
df_one_year.TempAvgF))
结果应如下所示:
模型斜率:[0.04304568]
模型截距:62.23496914044859
模型 r²值:0.09549593659736466
请注意,r²值非常低,这并不令人惊讶,因为数据的斜率随时间有显著变化,而我们拟合的是一个具有常数斜率的单一线性模型。
使用相同的 x 数据从模型生成预测值:
使用训练数据进行预测
y_pred = linear_model.predict(df_one_year[‘Day_of_Year’]\
.values.reshape((-1, 1)))
x_pred = df_one_year.Day_of_Year
如之前一样,创建一个新的散点图,并叠加模型的预测结果:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \
label = ‘原始数据’, c = ‘k’)
移动平均
ax.plot(df_one_year.Day_of_Year, df_one_year[‘20_d_mov_avg’], \
c = ‘r’, linestyle = ‘–’, \
label = f’{window}天移动平均’)
线性模型
ax.plot(x_pred, y_pred, c = “blue”, linestyle = ‘-.’, \
label = ‘线性模型’)
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘天数’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df_one_year.Day_of_Year.min(), \
df_one_year.Day_of_Year.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-S2PMM96W.jpg
图 3.73:原始数据,20 天滑动平均和线性拟合
注意
要访问本节的源代码,请参阅 https://packt.live/2CwEKyT.
你也可以在 https://packt.live/3hKJSzD 在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。
活动 3.03:虚拟变量
导入所需的包和类:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
加载并检查数据:
加载数据并检查
df = pd.read_csv(‘…/Datasets/austin_weather.csv’)
print(df.head())
print(df.tail())
df.head()的输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-XIIHGZ3P.jpg
图 3.74:df.head()函数的输出
df.tail()的输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2UIYD0ID.jpg
图 3.75:df.tail()函数的输出
执行与之前相同的预处理。去除除 Date 和 TempAvgF 列外的所有列。添加 Year、Month 和 Day 列。创建一个包含 20 天滑动平均的新列。切出第一个完整年份(2015 年):
df = df.loc[:, [‘Date’, ‘TempAvgF’]]
添加一些有用的列
df.loc[:, ‘Year’] = df.loc[:, ‘Date’].str.slice(0, 4).astype(‘int’)
df.loc[:, ‘Month’] = df.loc[:, ‘Date’].str.slice(5, 7).astype(‘int’)
df.loc[:, ‘Day’] = df.loc[:, ‘Date’].str.slice(8, 10).astype(‘int’)
“”"
设置一个 20 天的滑动窗口,然后用它来平滑数据
在新列中存储温度
“”"
window = 20
df[‘20_d_mov_avg’] = df.TempAvgF.rolling(window).mean()
“”"
现在让我们切片出完整的一年
日历的开始和结束日期
我们从之前的输出中看到
2014 年是第一个完整的数据年份,
然而,它仍然会有 NaN 值
滑动平均,因此我们将使用 2015 年数据
“”"
df_one_year = df.loc[df.Year == 2015, :].reset_index()
df_one_year[‘Day_of_Year’] = df_one_year.index + 1
print(df_one_year.head())
print(df_one_year.tail())
数据应该如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-XGAKT751.jpg
图 3.76:预处理数据
可视化结果:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \
label = ‘原始数据’, c = ‘k’)
滑动平均
ax.plot(df_one_year.Day_of_Year, df_one_year[‘20_d_mov_avg’], \
c = ‘r’, linestyle = ‘–’, \
label = f’{window}天滑动平均’)
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘天数’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df_one_year.Day_of_Year.min(), \
df_one_year.Day_of_Year.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
图表应该如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3G0548EA.jpg
图 3.77:奥斯丁温度及其滑动平均
我们可以看到温度从一月开始上升,直到九月左右,然后再次下降。这是一个明显的季节性循环。作为第一次改进,我们可以在模型中加入月份。如同在虚拟变量介绍中所述,如果我们仅将月份编码为 1 到 12 的整数,模型可能会认为 12 月(12)比 1 月(1)更重要。所以,我们将月份编码为虚拟变量来避免这一问题:
使用月份作为虚拟变量
dummy_vars = pd.get_dummies(df_one_year[‘Month’], drop_first = True)
dummy_vars.columns = [‘Feb’, ‘Mar’, ‘Apr’, ‘May’, ‘Jun’, \
‘Jul’, ‘Aug’, ‘Sep’, ‘Oct’, ‘Nov’, ‘Dec’]
df_one_year = pd.concat([df_one_year, dummy_vars], \
axis = 1).drop(‘Month’, axis = 1)
df_one_year
数据应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-XN7VJ280.jpg
图 3.78:使用虚拟变量增强的月份数据
现在,使用 Day_of_Year 和虚拟变量拟合一个线性模型,并打印模型系数和 R² 值:
使用月份虚拟变量拟合模型
linear_model = LinearRegression(fit_intercept = True)
linear_model.fit(pd.concat([df_one_year.Day_of_Year, \
df_one_year.loc[:, ‘Feb’:‘Dec’]], \
axis = 1),
df_one_year[‘TempAvgF’])
print(‘模型系数:’, linear_model.coef_)
print(‘模型截距:’, linear_model.intercept_)
print(‘模型 R 平方:’, \
linear_model.score(pd.concat([df_one_year.Day_of_Year, \
df_one_year.loc[:, ‘Feb’:‘Dec’]], \
axis = 1),
df_one_year[‘TempAvgF’]))
结果应如下所示:
模型系数:[ 0.03719346 1.57445204 9.35397321 19.16903518 22.02065629 26.80023439
30.17121033 30.82466482 25.6117698 15.71715435 1.542969 -4.06777548]
模型截距:48.34038858048261
模型 R 平方:0.7834805472165678
注意系数的符号——第一个值与 Day_of_Year 相关,接下来是 1 月到 12 月的值。1 月、2 月、3 月、11 月和 12 月的系数为负,而 6 月到 9 月的系数为正。这对德州的季节来说是合理的。
现在,使用单年数据进行预测,并可视化结果:
使用数据进行预测
y_pred = \
linear_model.predict(pd.concat([df_one_year.Day_of_Year, \
df_one_year.loc[:, ‘Feb’:‘Dec’]], \
axis = 1))
x_pred = df_one_year.Day_of_Year
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \
标签 = ‘原始数据’, c = ‘k’)
移动平均
ax.plot(df_one_year.Day_of_Year, df_one_year[‘20_d_mov_avg’], \
c = ‘r’, linestyle = ‘–’, \
标签 = f’{window}天移动平均’)
回归预测
ax.plot(x_pred, y_pred, c = “blue”, linestyle = ‘-.’, \
标签 = ‘线性模型 w/虚拟变量’
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘天数’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df_one_year.Day_of_Year.min(), \
df_one_year.Day_of_Year.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12, loc = ‘upper left’)
plt.show()
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-KI6KH1EE.jpg
图 3.79:带有月份虚拟变量的线性回归结果
注意
要访问该特定部分的源代码,请参阅 https://packt.live/3enegOg。
你也可以在 https://packt.live/2V4VgMM 在线运行这个例子。你必须执行整个 Notebook 才能得到期望的结果。
活动 3.04:线性回归特征工程
加载所需的包和类:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
加载数据并进行预处理,直到添加 Day_of_Year 为止:
加载数据
df = pd.read_csv(‘…/Datasets/austin_weather.csv’)
df = df.loc[:, [‘Date’, ‘TempAvgF’]]
添加一些有用的列
df.loc[:, ‘Year’] = df.loc[:, ‘Date’].str.slice(0, 4).astype(‘int’)
df.loc[:, ‘Month’] = df.loc[:, ‘Date’].str.slice(5, 7).astype(‘int’)
df.loc[:, ‘Day’] = df.loc[:, ‘Date’].str.slice(8, 10).astype(‘int’)
“”"
设置一个 20 天窗口,然后使用它进行平滑处理
新列中的温度
“”"
window = 20
df[‘20_d_mov_avg’] = df.TempAvgF.rolling(window).mean()
“”"
现在让我们准确地切割出一年
日历的开始和结束日期
我们从之前的输出中可以看到
2014 年是第一个有完整数据的年份,
然而,它仍然会有 NaN 值
移动平均值,因此我们将使用 2015 年的数据
“”"
df_one_year = df.loc[df.Year == 2015, :].reset_index()
df_one_year[‘Day_of_Year’] = df_one_year.index + 1
现在,进行特征工程,我们构建 Day_of_Year 的正弦和余弦,周期为 365 天:
为 Day_of_Year 添加两个列,分别表示其正弦和余弦值
df_one_year[‘sine_Day’] = np.sin(2 * np.pi \
- df_one_year[‘Day_of_Year’] / 365)
df_one_year[‘cosine_Day’] = np.cos(2 * np.pi \
- df_one_year[‘Day_of_Year’] / 365)
df_one_year
数据应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-N39ZI5CT.jpg
图 3.80:包含新特征 sine_Day 和 cosine_Day 的奥斯丁天气数据
我们现在可以使用 scikit-learn 的 LinearRegression 类拟合模型,并打印系数和 R² 值:
使用 Day_of_Year 和 sin/cos 拟合模型
linear_model = LinearRegression(fit_intercept = True)
linear_model.fit(df_one_year[[‘Day_of_Year’, ‘sine_Day’, \
‘cosine_Day’]],\
df_one_year[‘TempAvgF’])
print(‘模型系数:’, linear_model.coef_)
print(‘模型截距:’, linear_model.intercept_)
print(‘模型 R 平方值:’, \
linear_model.score(df_one_year[[‘Day_of_Year’, ‘sine_Day’, \
‘cosine_Day’]],\
df_one_year[‘TempAvgF’]))
输出应如下所示:
模型系数: [ 1.46396364e-02 -5.57332499e+00 -1.67824174e+01]
模型截距:67.43327530313064
模型 R 平方值:0.779745650129063
请注意,r2 值与我们通过虚拟变量得到的结果差不多。然而,我们来看看预测结果,看看这个模型是否比以前更合适或更不合适。
使用增强数据生成预测:
使用数据进行预测
y_pred = \
linear_model.predict(df_one_year[[‘Day_of_Year’, ‘sine_Day’, \
‘cosine_Day’]])
x_pred = df_one_year.Day_of_Year
现在,查看结果:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
原始数据
ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \
label = ‘原始数据’, c = ‘k’)
移动平均
ax.plot(df_one_year.Day_of_Year, df_one_year[‘20_d_mov_avg’], \
c = ‘r’, linestyle = ‘–’, \
label = f’{window} 天移动平均’)
回归预测
ax.plot(x_pred, y_pred, c = “blue”, linestyle = ‘-.’, \
label = ‘线性模型带正余弦拟合’)
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘天’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df_one_year.Day_of_Year.min(), \
df_one_year.Day_of_Year.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12, loc = ‘upper left’)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-G4DBEE5E.jpg
图 3.81:奥斯丁温度数据,带有移动平均叠加和周期性特征拟合叠加
注意
要访问此特定部分的源代码,请参见 https://packt.live/3dvkmet.
您还可以在线运行此示例,网址:https://packt.live/3epnOIJ。您必须执行整个笔记本才能获得预期的结果。
Activity 3.05: 梯度下降
导入模块和类:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
from sklearn.linear_model import SGDRegressor
加载数据(austin_weather.csv)并进行预处理,直到创建 Day_of_Year 列并切割出完整的一年数据(2015 年):
加载数据并检查
df = pd.read_csv(‘…/Datasets/austin_weather.csv’)
df = df.loc[:, [‘Date’, ‘TempAvgF’]]
添加基于时间的列
df.loc[:, ‘Year’] = df.loc[:, ‘Date’].str.slice(0, 4).astype(‘int’)
df.loc[:, ‘Month’] = df.loc[:, ‘Date’].str.slice(5, 7).astype(‘int’)
df.loc[:, ‘Day’] = df.loc[:, ‘Date’].str.slice(8, 10).astype(‘int’)
“”"
设置一个 20 天窗口,然后使用该窗口进行平滑
温度放在一个新的列中
“”"
window = 20
df[‘20_d_mov_avg’] = df.TempAvgF.rolling(window).mean()
“”"
现在让我们精确切割出一年的数据
日历开始和结束日期
从之前的输出可以看到
2014 年是第一个完整数据的年份,
但是,它仍然会对某些值产生 NaN
移动平均,因此我们将使用 2015 年数据
“”"
df_one_year = df.loc[df.Year == 2015, :].reset_index()
df_one_year[‘Day_of_Year’] = df_one_year.index + 1
print(df_one_year.head())
print(df_one_year.tail())
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-WYG31OSL.jpg
图 3.82:缩放前的预处理数据
缩放数据以进行训练:
缩放数据
X_min = df_one_year.Day_of_Year.min()
X_range = df_one_year.Day_of_Year.max() \
- df_one_year.Day_of_Year.min()
Y_min = df_one_year.TempAvgF.min()
Y_range = df_one_year.TempAvgF.max() \
- df_one_year.TempAvgF.min()
scale_X = (df_one_year.Day_of_Year - X_min) / X_range
train_X = scale_X.ravel()
train_Y = ((df_one_year.TempAvgF - Y_min) / Y_range).ravel()
设置随机种子,实例化 SGDRegressor 模型对象,并拟合模型到训练数据:
创建模型对象
np.random.seed(42)
model = SGDRegressor(loss = ‘squared_loss’, max_iter = 100, \
learning_rate = ‘constant’, eta0 = 0.0005, \
tol = 0.00009, penalty = ‘none’)
拟合模型
model.fit(train_X.reshape((-1, 1)), train_Y)
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-837LOO8W.jpg
图 3.83:使用 SGDRegressor 的模型对象
提取模型系数并重新缩放:
Beta0 = (Y_min + Y_range * model.intercept_[0] \
- Y_range * model.coef_[0] * X_min / X_range)
Beta1 = Y_range * model.coef_[0] / X_range
print(Beta0)
print(Beta1)
输出应类似于以下内容:
61.45512325422412
0.04533603293003107
使用缩放后的数据生成预测值,然后获取 r2 值:
生成预测
pred_X = df_one_year[‘Day_of_Year’]
pred_Y = model.predict(train_X.reshape((-1, 1)))
计算 r 平方值
r2 = r2_score(train_Y, pred_Y)
print('r 平方 = ', r2)
结果应类似于以下内容:
r 平方 = 0.09462157379706759
将预测结果缩放回实际值并可视化结果:
将预测值缩放回实际值
pred_Y = (pred_Y * Y_range) + Y_min
fig = plt.figure(figsize = (10, 7))
ax = fig.add_axes([1, 1, 1, 1])
原始数据
ax.scatter(df_one_year.Day_of_Year, df_one_year.TempAvgF, \
label = ‘原始数据’, c = ‘k’)
移动平均
ax.plot(df_one_year.Day_of_Year, df_one_year[‘20_d_mov_avg’], \
c = ‘r’, linestyle = ‘–’, \
label = f’{window} 日移动平均’)
回归预测
ax.plot(pred_X, pred_Y, c = “blue”, linestyle = ‘-.’, \
linewidth = 4, label = ‘线性拟合(来自 SGD)’)
将模型添加到图表上
ax.text(1, 85, 'Temp = ’ + str(round(Beta0, 2)) + ’ + ’ \
- str(round(Beta1, 4)) + ’ * 日期’, fontsize = 16)#
ax.set_title(‘空气温度测量’, fontsize = 16)
ax.set_xlabel(‘日期’, fontsize = 16)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘F)’, fontsize = 14)
ax.set_xticks(range(df_one_year.Day_of_Year.min(), \
df_one_year.Day_of_Year.max(), 30))
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-9X6YLZSP.jpg
图 3.84:使用 SGDRegressor 进行线性回归的结果
注意
若要访问此特定部分的源代码,请参阅 https://packt.live/2AY1bMZ。
你也可以在 https://packt.live/2NgCI86 上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。