原文:
annas-archive.org/md5/d906d6d9346f8d8b7965d192afaf9a47
译者:飞龙
第四章:分类
学习目标
到本章结束时,你将能够:
-
实现逻辑回归,并解释如何将其用于将数据分类为特定组或类别
-
使用 K 最近邻聚类算法进行分类
-
使用决策树进行数据分类,包括 ID3 算法
-
描述数据中的熵概念
-
解释决策树(如 ID3)如何旨在减少熵
-
使用决策树进行数据分类
本章介绍了分类问题,线性回归和逻辑回归分类,K 最近邻分类和决策树。
介绍
在上一章中,我们开始了使用回归技术的监督学习之旅,预测给定一组输入数据时的连续变量输出。现在,我们将转向我们之前描述的另一类机器学习问题:分类问题。回想一下,分类任务的目标是根据一组输入数据,预测数据属于指定数量的类别中的哪一类。
在本章中,我们将扩展在第三章《回归分析》中学到的概念,并将其应用于标注有类别而非连续值作为输出的数据集。
将线性回归作为分类器
在上一章中,我们在预测连续变量输出的上下文中讨论了线性回归,但它也可以用于预测一组数据属于哪个类别。线性回归分类器不如我们将在本章中讨论的其他类型的分类器强大,但它们在理解分类过程时特别有用。假设我们有一个虚构的数据集,其中包含两个独立的组,X 和 O,如图 4.1所示。我们可以通过首先使用线性回归拟合一条直线的方程来构建一个线性分类器。对于任何位于直线之上的值,将预测为X类别,而对于位于直线下方的任何值,将预测为O类别。任何可以通过一条直线分隔的数据集被称为线性可分,这构成了机器学习问题中的一个重要数据子集。尽管在基于线性回归的分类器中,这可能并不特别有用,但在其他分类器中,如支持向量机(SVM)、决策树和基于线性神经网络的分类器中,这通常是很有帮助的。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_01.jpg
图 4.1:将线性回归作为分类器
练习 36:将线性回归用作分类器
本练习包含了一个使用线性回归作为分类器的构造示例。在本练习中,我们将使用一个完全虚构的数据集,并测试线性回归作为分类器的效果。数据集由手动选择的x和y值组成,这些值大致分为两组。该数据集专门为本练习设计,旨在展示如何将线性回归作为分类器使用,数据集在本书的附带代码文件中以及 GitHub 上的github.com/TrainingByPackt/Supervised-Learning-with-Python
可以找到。
-
将
linear_classifier.csv
数据集加载到 pandas DataFrame 中:df = pd.read_csv('linear_classifier.csv') df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_02.jpg
图 4.2:前五行
浏览数据集,每行包含一组x, y坐标以及对应的标签,指示数据属于哪个类别,可能是叉号(x)或圆圈(o)。
-
绘制数据的散点图,每个点的标记为对应的类别标签:
plt.figure(figsize=(10, 7)) for label, label_class in df.groupby('labels'): plt.scatter(label_class.values[:,0], label_class.values[:,1], label=f'Class {label}', marker=label, c='k') plt.legend() plt.title("Linear Classifier");
我们将得到如下的散点图:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_03.jpg
图 4.3 线性分类器的散点图
-
使用上一章中的 scikit-learn
LinearRegression
API,拟合线性模型到数据集的x、y坐标,并打印出线性方程:# Fit a linear regression model model = LinearRegression() model.fit(df.x.values.reshape((-1, 1)), df.y.values.reshape((-1, 1))) # Print out the parameters print(f'y = {model.coef_[0][0]}x + {model.intercept_[0]}')
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_04.jpg
图 4.4:模型拟合的输出
-
在数据集上绘制拟合的趋势线:
# Plot the trendline trend = model.predict(np.linspace(0, 10).reshape((-1, 1))) plt.figure(figsize=(10, 7)) for label, label_class in df.groupby('labels'): plt.scatter(label_class.values[:,0], label_class.values[:,1], label=f'Class {label}', marker=label, c='k') plt.plot(np.linspace(0, 10), trend, c='k', label='Trendline') plt.legend() plt.title("Linear Classifier");
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_05.jpg
图 4.5:带趋势线的散点图
-
通过拟合的趋势线,可以应用分类器。对于数据集中的每一行,判断x, y点是位于线性模型(或趋势线)之上还是之下。如果点位于趋势线之下,则模型预测o类;如果位于线之上,则预测x类。将这些值作为预测标签的一列包含在内:
# Make predictions y_pred = model.predict(df.x.values.reshape((-1, 1))) pred_labels = [] for _y, _y_pred in zip(df.y, y_pred): if _y < _y_pred: pred_labels.append('o') else: pred_labels.append('x') df['Pred Labels'] = pred_labels df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_06.jpg
图 4.6:前五行
-
绘制带有相应真实标签的点。对于那些标签被正确预测的点,绘制对应的类别;对于错误预测的点,绘制一个菱形标记:
plt.figure(figsize=(10, 7)) for idx, label_class in df.iterrows(): if label_class.labels != label_class['Pred Labels']: label = 'D' s=70 else: label = label_class.labels s=50 plt.scatter(label_class.values[0], label_class.values[1], label=f'Class {label}', marker=label, c='k', s=s) plt.plot(np.linspace(0, 10), trend, c='k', label='Trendline') plt.title("Linear Classifier"); incorrect_class = mlines.Line2D([], [], color='k', marker='D', markersize=10, label='Incorrect Classification'); plt.legend(handles=[incorrect_class]);
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_07.jpg
图 4.7:显示错误预测的散点图
我们可以看到,在这个图中,线性分类器在这个完全虚构的数据集上做出了两次错误预测,一个是在x = 1时,另一个是在x = 3时。
但如果我们的数据集不是线性可分的,无法使用直线模型对数据进行分类,那该怎么办呢?这种情况非常常见。在这种情况下,我们会转向其他分类方法,其中许多方法使用不同的模型,但这一过程逻辑上是从我们简化的线性分类模型延伸出来的。
逻辑回归
逻辑或对数几率模型就是一种非线性模型,已经在许多不同领域的分类任务中得到了有效应用。在本节中,我们将用它来分类手写数字的图像。在理解逻辑模型的过程中,我们也迈出了理解一种特别强大的机器学习模型——人工神经网络的关键一步。那么,逻辑模型到底是什么呢?像线性模型一样,线性模型由一个线性或直线函数组成,而逻辑模型则由标准的逻辑函数组成,数学上看起来大致是这样的:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_08.jpg
图 4.8:逻辑函数
从实际角度来看,当经过训练后,这个函数返回输入信息属于某一特定类别或组的概率。
假设我们想预测某一数据项是否属于两个组中的一个。就像之前的例子中,在线性回归中,这等价于 y 要么为零,要么为一,而 x 可以取值范围在 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_02.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_03.png 之间:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_09.jpg
图 4.9:y 的方程
从零到一的范围与 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_02.png 到 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_03.png 的差异非常大;为了改进这一点,我们将计算赔率比,这样它就会从大于零的数值变化到小于 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_03.png 的数值,这就是朝着正确方向迈出的一步:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_10.jpg
图 4.10:赔率比
我们可以利用自然对数的数学关系进一步简化这一过程。当赔率比接近零时,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_04.pngss 接近 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_02.png;同样,当赔率比接近一时,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_04.png 接近 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_03.png。这正是我们想要的;也就是说,两个分类选项尽可能远离。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_11.jpg
图 4.11:分类点的自然对数
通过稍微调整方程,我们得到了逻辑函数:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_12.jpg
图 4.12:逻辑函数
注意 e 的指数,即 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_05.png,并且这个关系是一个线性函数,具有两个训练参数或 权重,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_06.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_04_02.png,以及输入特征 x。如果我们将逻辑函数绘制在 (-6, 6) 范围内,我们会得到以下结果:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_13.jpg
图 4.13:逻辑函数曲线
通过检查 图 4.13,我们可以看到一些对分类任务很重要的特征。首先需要注意的是,如果我们查看函数两端的 y 轴上的概率值,在 x = -6 时,概率值几乎为零,而在 x = 6 时,概率值接近 1。虽然看起来这些值实际上是零和一,但实际情况并非如此。逻辑函数在这些极值处接近零和一,只有当 x 达到正无穷或负无穷时,它才会等于零或一。从实际角度来看,这意味着逻辑函数永远不会返回大于一的概率或小于等于零的概率,这对于分类任务来说是完美的。我们永远不能有大于一的概率,因为根据定义,概率为一意味着事件发生是确定的。同样,我们不能有小于零的概率,因为根据定义,概率为零意味着事件不发生是确定的。逻辑函数接近但永远不等于一或零,意味着结果或分类总是存在某种不确定性。
逻辑函数的最后一个特点是,在 x = 0 时,概率为 0.5,如果我们得到这个结果,这意味着模型对相应类别的结果具有相等的不确定性;也就是说,它完全没有把握。通常,这是训练开始时的默认状态,随着模型接触到训练数据,它对决策的信心逐渐增加。
注意
正确理解和解释分类模型(如线性回归)提供的概率信息非常重要。可以将这个概率得分视为在给定训练数据所提供的信息的变动性下,输入信息属于某一特定类别的可能性。一个常见的错误是将这个概率得分作为衡量模型对预测是否可靠的客观标准;不幸的是,这并不总是准确的。一个模型可以提供 99.99% 的概率,认为某些数据属于某个特定类别,但它仍然可能有 99.99% 的错误。
我们使用概率值的目的是选择分类器预测的类别。假设我们有一个模型用于预测某些数据集是否属于类 A 或类 B。如果逻辑回归模型为类 A 返回的概率为 0.7,那么我们将返回类 A 作为模型的预测类别。如果概率仅为 0.2,则模型的预测类别为类 B。
练习 37:逻辑回归作为分类器 – 二类分类器
在本练习中,我们将使用著名的 MNIST 数据集的一个样本(可以在 yann.lecun.com/exdb/mnist/
或 GitHub 上的 github.com/TrainingByPackt/Supervised-Learning-with-Python
找到),它是一个包含手写邮政编码数字(从零到九)及相应标签的图像序列。MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,每个样本都是大小为 28 x 28 像素的灰度图像。在本练习中,我们将使用逻辑回归来构建一个分类器。我们将构建的第一个分类器是一个二类分类器,用来判断图像是手写的零还是一:
-
在这个练习中,我们需要导入一些依赖项。执行以下导入语句:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.linear_model import LogisticRegression
-
我们还需要下载 MNIST 数据集。你只需要执行此操作一次,因此在此步骤之后,可以随意注释或删除这些代码单元。下载图像数据,具体如下:
request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz') with open('train-images-idx3-ubyte.gz', 'wb') as f: f.write(request.read()) request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz') with open('t10k-images-idx3-ubyte.gz', 'wb') as f: f.write(request.read())
-
下载数据的相应标签:
request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz') with open('train-labels-idx1-ubyte.gz', 'wb') as f: f.write(request.read()) request = urllib.request.urlopen('http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz') with open('t10k-labels-idx1-ubyte.gz', 'wb') as f: f.write(request.read())
-
一旦所有文件成功下载,使用以下命令检查本地目录中的文件(适用于 Windows):
!dir *.gz
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_14.jpg
图 4.14:目录中的文件
注意
对于 Linux 和 macOS,使用
!ls *.gz
命令查看本地目录中的文件。 -
加载下载的数据。无需过于担心读取数据的具体细节,因为这些是 MNIST 数据集特有的:
with gzip.open('train-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('train-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels = np.array(array("B", f.read())) with gzip.open('t10k-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img_test = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('t10k-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels_test = np.array(array("B", f.read()))
-
一如既往,彻底理解数据至关重要,因此,创建训练样本中前 10 张图像的图像图。注意灰度图像以及相应的标签,它们是数字零到九:
for i in range(10): plt.subplot(2, 5, i + 1) plt.imshow(img[i], cmap='gray'); plt.title(f'{labels[i]}'); plt.axis('off')
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_15.jpg
图 4.15:训练图像
-
由于初始分类器旨在分类零图像或一图像,我们必须首先从数据集中选择这些样本:
samples_0_1 = np.where((labels == 0) | (labels == 1))[0] images_0_1 = img[samples_0_1] labels_0_1 = labels[samples_0_1] samples_0_1_test = np.where((labels_test == 0) | (labels_test == 1)) images_0_1_test = img_test[samples_0_1_test].reshape((-1, rows * cols)) labels_0_1_test = labels_test[samples_0_1_test]
-
可视化一个来自零选择的样本和另一个来自手写数字一的样本,以确保我们已正确分配数据。
下面是数字零的代码:
sample_0 = np.where((labels == 0))[0][0] plt.imshow(img[sample_0], cmap='gray');
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_16.jpg
图 4.16:第一张手写图像
下面是一个的代码:
sample_1 = np.where((labels == 1))[0][0] plt.imshow(img[sample_1], cmap='gray');
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_17.jpg
图 4.17:第二张手写图像
-
我们几乎可以开始构建模型了,然而,由于每个样本都是图像并且数据是矩阵格式,我们必须首先重新排列每张图像。模型需要图像以向量形式提供,即每张图像的所有信息存储在一行中。按以下步骤操作:
images_0_1 = images_0_1.reshape((-1, rows * cols)) images_0_1.shape
-
现在我们可以使用选择的图像和标签构建并拟合逻辑回归模型:
model = LogisticRegression(solver='liblinear') model.fit(X=images_0_1, y=labels_0_1)
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_18.jpg
图 4.18:逻辑回归模型
请注意,scikit-learn 的逻辑回归 API 调用与线性回归的一致性。这里有一个额外的参数
solver
,它指定要使用的优化过程类型。我们在这里提供了该参数的默认值,以抑制在当前版本的 scikit-learn 中要求指定solver
参数的未来警告。solver
参数的具体细节超出了本章的讨论范围,仅为抑制警告信息而包含。 -
检查此模型在相应训练数据上的表现:
model.score(X=images_0_1, y=labels_0_1)
我们将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_19.jpg
图 4.19:模型得分
在这个例子中,模型能够以 100% 的准确率预测训练标签。
-
使用模型显示训练数据的前两个预测标签:
model.predict(images_0_1) [:2]
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_20.jpg
图 4.20:模型预测的前两个标签
-
逻辑回归模型是如何做出分类决策的?观察模型为训练集产生的一些概率值:
model.predict_proba(images_0_1)[:2]
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_21.jpg
图 4.21:概率数组
我们可以看到,对于每个预测,都会有两个概率值。第一个是类为零的概率,第二个是类为一的概率,二者加起来为一。我们可以看到,在第一个例子中,预测概率为 0.9999999(类零),因此预测为类零。同样地,第二个例子则相反。
-
计算模型在测试集上的表现,以检查其在未见过的数据上的性能:
model.score(X=images_0_1_test, y=labels_0_1_test)
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_22.jpg
图 4.22:模型得分
注意
请参考第六章,模型评估,了解更好的客观衡量模型性能的方法。
我们可以看到,逻辑回归是一个强大的分类器,能够区分手写的零和一。
练习 38:逻辑回归——多类分类器
在之前的练习中,我们使用逻辑回归对两类进行分类。然而,逻辑回归也可以用于将一组输入信息分类到 k 个不同的组,这就是我们在本练习中要研究的多类分类器。加载 MNIST 训练和测试数据的过程与之前的练习相同:
-
加载训练/测试图像及其对应的标签:
with gzip.open('train-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('train-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels = np.array(array("B", f.read())) with gzip.open('t10k-images-idx3-ubyte.gz', 'rb') as f: magic, size, rows, cols = struct.unpack(">IIII", f.read(16)) img_test = np.array(array("B", f.read())).reshape((size, rows, cols)) with gzip.open('t10k-labels-idx1-ubyte.gz', 'rb') as f: magic, size = struct.unpack(">II", f.read(8)) labels_test = np.array(array("B", f.read()))
-
由于训练数据量较大,我们将选择整体数据的一个子集,以减少训练时间及训练过程中所需的系统资源:
np.random.seed(0) # Give consistent random numbers selection = np.random.choice(len(img), 5000) selected_images = img[selection] selected_labels = labels[selection]
请注意,在此示例中,我们使用的是所有 10 个类别的数据,而不仅仅是零类和一类,因此我们将此示例设置为多类分类问题。
-
再次将输入数据重塑为向量形式,以便后续使用:
selected_images = selected_images.reshape((-1, rows * cols)) selected_images.shape
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_23.jpg
图 4.23:数据重塑
-
下一单元格故意被注释掉。暂时保持此代码为注释:
# selected_images = selected_images / 255.0 # img_test = img_test / 255.0
-
构建逻辑回归模型。这里有一些额外的参数,如下所示:
solver
的lbfgs
值适用于多类问题,需要额外的max_iter
迭代次数来收敛到解。multi_class
参数设置为multinomial
,以计算整个概率分布的损失:model = LogisticRegression(solver='lbfgs', multi_class='multinomial', max_iter=500, tol=0.1) model.fit(X=selected_images, y=selected_labels)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_24.jpg
图 4.24:逻辑回归模型
注意
有关参数的更多信息,请参阅文档:
scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html
。 -
确定训练集的准确性评分:
model.score(X=selected_images, y=selected_labels)
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_25.jpg
图 4.25:模型评分
-
确定训练集的前两个预测值,并绘制相应预测的图像:
model.predict(selected_images)[:2]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_26.jpg
图 4.26:模型评分预测值
-
显示训练集前两个样本的图像,查看我们的判断是否正确:
plt.subplot(1, 2, 1) plt.imshow(selected_images[0].reshape((28, 28)), cmap='gray'); plt.axis('off'); plt.subplot(1, 2, 2) plt.imshow(selected_images[1].reshape((28, 28)), cmap='gray'); plt.axis('off');
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_27.jpg
图 4.27:使用预测绘制的图像
-
再次打印出模型为训练集第一个样本提供的概率分数。确认每个类别有 10 个不同的值:
model.predict_proba(selected_images)[0]
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_28.jpg
图 4.28:预测值数组
请注意,在第一个样本的概率数组中,第五个(索引为四)样本的概率最高,因此表示预测为四。
-
计算模型在测试集上的准确性。这将提供一个合理的估计值,表示模型在实际环境中的表现,因为它从未见过测试集中的数据。考虑到模型没有接触过这些数据,测试集的准确率预计会稍微低于训练集:
model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_29.jpg
图 4.29:模型得分
在测试集上检查时,模型的准确率为 87.8%。应用测试集时,性能下降是可以预期的,因为这是模型第一次接触这些样本;而在训练过程中,训练集已经多次呈现给模型。
-
找到包含注释掉的代码的单元格,如步骤四所示。取消注释该单元格中的代码:
selected_images = selected_images / 255.0 img_test = img_test / 255.0
这个单元格只是将所有图像值缩放到零和一之间。灰度图像由像素组成,这些像素的值在 0 到 255 之间,包括 0(黑色)和 255(白色)。
-
点击重新启动并运行全部以重新运行整个笔记本。
-
找到训练集误差:
model.score(X=selected_images, y=selected_labels)
我们将得到以下得分:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_30.jpg
图 4.30:训练集模型得分
-
找到测试集误差:
model.score(X=img_test.reshape((-1, rows * cols)), y=labels_test)
我们将得到以下得分:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_31.jpg
图 4.31:测试集模型得分
归一化图像对系统整体性能有什么影响?训练误差变得更糟!我们从训练集的 100%准确率下降到了 98.6%。是的,训练集的性能有所下降,但测试集的准确率却从 87.8%提高到了 90.02%。测试集的性能更为重要,因为模型之前没有见过这些数据,因此它能更好地代表模型在实际应用中的表现。那么,为什么我们会得到更好的结果呢?再次查看图 4.13,注意曲线接近零和接近一时的形状。曲线在接近零和接近一时趋于饱和或平坦。因此,如果我们使用 0 到 255 之间的图像(或x值),由逻辑函数定义的类别概率将位于曲线的平坦区域内。位于该区域内的预测变化很小,因为要产生任何有意义的变化,需要x值发生非常大的变化。将图像缩放到零和一之间,最初会将预测值拉近p(x) = 0.5,因此,x的变化会对y值产生更大的影响。这允许更敏感的预测,并导致训练集中的一些预测错误,但测试集中的更多预测是正确的。建议在训练和测试之前,将输入值缩放到零和一之间,或者负一和一之间,用于你的逻辑回归模型。
以下函数将把 NumPy 数组中的值缩放到-1 到 1 之间,均值大约为零:
def scale_input(x):
mean_x = x.mean()
x = x – mean_x
max_x = x / no.max(abs(x))
return x
活动 11:线性回归分类器 – 二分类器
在这个活动中,我们将使用 MNIST 数据集构建一个基于线性回归的二分类器,用于区分数字零和数字一。
执行的步骤如下:
-
导入所需的依赖项:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.linear_model import LinearRegression
-
将 MNIST 数据加载到内存中。
-
可视化一组数据样本。
-
构建一个线性分类器模型来分类数字零和数字一。我们将创建的模型是用来判断样本是数字零还是数字一。为此,我们首先需要仅选择这些样本。
-
使用零样本和一样本的图像可视化选择的信息。
-
为了向模型提供图像信息,我们必须先将数据展平,使每个图像的形状为 1 x 784 像素。
-
让我们构建模型;使用
LinearRegression
API 并调用fit
函数。 -
计算训练集的 R2 分数。
-
使用阈值 0.5 来确定每个训练样本的标签预测。大于 0.5 的值分类为一;小于或等于 0.5 的值分类为零。
-
计算预测训练值与实际值之间的分类准确率。
-
与测试集的性能进行比较。
注意
该活动的解决方案可以在第 352 页找到。
活动 12:使用逻辑回归进行虹膜分类
在这个活动中,我们将使用著名的虹膜物种数据集(可在 en.wikipedia.org/wiki/Iris_flower_data_set
或 GitHub 上的 github.com/TrainingByPackt/Supervised-Learning-with-Python
获取),该数据集由植物学家罗纳德·费舍尔于 1936 年创建。数据集包含三种不同虹膜花卉物种的萼片和花瓣的长度和宽度测量值:虹膜变色花、虹膜佛罗伦萨和虹膜维尔吉尼卡。在这个活动中,我们将使用数据集中提供的测量值来分类不同的花卉物种。
执行的步骤如下:
-
导入所需的软件包。在本次活动中,我们将需要 pandas 包来加载数据,Matplotlib 包来绘图,以及 scikit-learn 包来创建逻辑回归模型。导入所有必需的软件包和相关模块以完成这些任务:
import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression
-
使用 pandas 加载虹膜数据集并检查前五行。
-
下一步是特征工程。我们需要选择最合适的特征,以提供最强大的分类模型。绘制多个不同的特征与分配的物种分类之间的关系图,例如,萼片长度与花瓣长度及物种。通过可视化检查这些图形,查找可能表明不同物种之间分离的模式。
-
通过在以下列表中编写列名来选择特征:
selected_features = [ '', # List features here ]
-
在构建模型之前,我们必须先将
species
值转换为可以在模型中使用的标签。将Iris-setosa
物种字符串替换为值0
,将Iris-versicolor
物种字符串替换为值1
,将Iris-virginica
物种字符串替换为值2
。 -
使用
selected_features
和分配的species
标签创建模型。 -
计算模型在训练集上的准确性。
-
使用第二选择的
selected_features
构建另一个模型,并比较其性能。 -
使用所有可用信息构建另一个模型,并比较其性能。
注意
本活动的解决方案可以在第 357 页找到。
使用 K 近邻分类
现在我们已经熟悉了使用逻辑回归创建多类分类器,并且在这些模型中取得了合理的性能,接下来我们将注意力转向另一种分类器:K-最近邻(K-NN)聚类方法。这是一种非常实用的方法,因为它不仅可以应用于监督分类问题,还可以应用于无监督问题。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_32.jpg
图 4.32:K-NN 的可视化表示
大约位于中心的实心圆是需要分类的测试点,而内圆表示分类过程,其中K=3,外圆表示K=5。
K-NN 是数据分类中最简单的“学习”算法之一。这里使用“学习”一词是明确的,因为 K-NN 并不像其他方法(例如逻辑回归)那样从数据中学习并将这些学习结果编码为参数或权重。K-NN 使用基于实例或懒惰学习,它只是存储或记住所有训练样本及其对应的类别。当一个测试样本提供给算法进行分类时,它通过对K个最近点的多数投票来确定对应的类别,从而得名 K-最近邻。如果我们查看图 4.35,实心圆表示需要分类的测试点。如果我们使用K=3,最近的三个点位于内虚线圆内,在这种情况下,分类结果将是空心圆。然而,如果我们使用K=5,最近的五个点位于外虚线圆内,分类结果将是交叉标记(三个交叉标记对两个空心圆)。
该图突出显示了 K-NN 分类的一些特征,这些特征应当加以考虑:
-
K 的选择非常重要。在这个简单的例子中,将 K 从三改为五,由于两个类别的接近性,导致了分类预测的翻转。由于最终的分类是通过多数投票决定的,因此通常使用奇数的 K 值以确保投票中有一个获胜者。如果选择了偶数的 K 值并且投票发生了平局,那么有多种方法可以用来打破平局,包括:
将 K 减少一个直到平局被打破
基于最近点的最小欧几里得距离选择类别
应用加权函数以偏向距离较近的邻居
-
K-NN 模型具有形成极其复杂的非线性边界的能力,这在对图像或具有有趣边界的数据集进行分类时可能具有优势。考虑到在 图 4.35 中,随着 K 的增加,测试点的分类从空心圆变为十字,我们可以看到此处可能形成复杂的边界。
-
K-NN 模型对数据中的局部特征非常敏感,因为分类过程实际上仅依赖于邻近的点。
-
由于 K-NN 模型将所有训练信息都记住以进行预测,因此它们在对新的、未见过的数据进行泛化时可能会遇到困难。
K-NN 还有一种变体,它不是指定最近邻的数量,而是指定测试点周围的半径大小以进行查找。这种方法称为 半径邻居分类,本章将不考虑这种方法,但在理解 K-NN 的过程中,你也会对半径邻居分类有一定了解,并学习如何通过 scikit-learn 使用该模型。
注意
我们对 K-NN 分类的解释以及接下来的练习将重点研究具有两个特征或两个维度的数据建模,因为这样可以简化可视化并更好地理解 K-NN 建模过程。像线性回归和逻辑回归一样,K-NN 分类并不限于仅用于二维数据集;它也可以应用于 N 维数据集。我们将在 活动 13:K-NN 多分类分类器 中进一步详细探讨这一点,在该活动中,我们将使用 K-NN 对 MNIST 数据集进行分类。请记住,仅仅因为无法绘制过多维度的图形,并不意味着它不能在 N 维数据集中进行分类。
练习 39:K-NN 分类
为了允许可视化 K-NN 过程,本练习将把重点转移到另一个数据集——著名的 Iris 数据集。该数据集作为本书附带代码文件的一部分提供:
-
对于本练习,我们需要导入 pandas、Matplotlib 以及 scikit-learn 的
KNeighborsClassifier
。我们将使用简写符号KNN
以便快速访问:import pandas as pd import matplotlib.pyplot as plt from sklearn.neighbors import KNeighborsClassifier as KNN
-
加载 Iris 数据集并查看前五行:
df = pd.read_csv('iris-data.csv') df.head()
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_33.jpg
图 4.33:前五行
-
在这个阶段,我们需要从数据集中选择最合适的特征用于分类器。我们可以简单地选择所有四个(萼片和花瓣的长度和宽度),但由于这个练习旨在允许可视化 K-NN 过程,我们只选择萼片长度和花瓣宽度。为数据集中的每个类构建萼片长度与花瓣宽度的散点图,并标注相应的物种:
markers = { 'Iris-setosa': {'marker': 'x', 'facecolor': 'k', 'edgecolor': 'k'}, 'Iris-versicolor': {'marker': '*', 'facecolor': 'none', 'edgecolor': 'k'}, 'Iris-virginica': {'marker': 'o', 'facecolor': 'none', 'edgecolor': 'k'}, } plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): plt.scatter(group['Sepal Length'], group['Petal Width'], label=name, marker=markers[name]['marker'], facecolors=markers[name]['facecolor'], edgecolor=markers[name]['edgecolor']) plt.title('Species Classification Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)'); plt.legend();
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_34.jpg
图 4.34: 鸢尾花数据的散点图
-
从这张图中可以看出,花瓣宽度在某种程度上合理地分开了这些物种,特别是鸢尾花杂色和鸢尾花维吉尼亚物种之间具有最大的相似性。在鸢尾花维吉尼亚物种的群集中,有几个点位于鸢尾花杂色群集内。作为稍后使用的测试点,选择边界样本 134:
df_test = df.iloc[134] df = df.drop([134]) # Remove the sample df_test
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_35.jpg
图 4.35: 边界样本
-
再次绘制数据,突出显示测试样本/点的位置:
plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): plt.scatter(group['Sepal Length'], group['Petal Width'], label=name, marker=markers[name]['marker'], facecolors=markers[name]['facecolor'], edgecolor=markers[name]['edgecolor']) plt.scatter(df_test['Sepal Length'], df_test['Petal Width'], label='Test Sample', c='k', marker='D') plt.title('Species Classification Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)'); plt.legend();
输出将如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_36.jpg
图 4.36: 带有测试样本的散点图
-
使用 K = 3 构建 K-NN 分类器模型,并将其拟合到训练数据中:
model = KNN(n_neighbors=3) model.fit(X=df[['Petal Width', 'Sepal Length']], y=df.Species)
输出将如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_37.jpg
图 4.37: K 近邻分类器
-
检查模型在训练集上的性能:
model.score(X=df[['Petal Width', 'Sepal Length']], y=df.Species)
输出将显示性能评分:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_38.jpg
图 4.38: 模型得分
由于测试集的准确率超过了 97%,接下来的步骤将是检查测试样本。
-
预测测试样本的物种:
model.predict(df_test[['Petal Width', 'Sepal Length']].values.reshape((-1, 2)))[0]
输出将如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_39.jpg
图 4.39: 预测的测试样本
-
用实际样本的物种来验证它:
df.iloc[134].Species
输出将如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_40.jpg
图 4.40: 样本的物种
这个预测显然是错误的,但考虑到测试点位于边界上,这并不奇怪。有用的是知道模型的边界在哪里。我们将在下一个练习中绘制这个边界。
练习 40: 可视化 K-NN 边界
要可视化 K-NN 分类器生成的决策边界,我们需要在预测空间上进行扫描,即萼片宽度和长度的最小和最大值,并确定模型在这些点上的分类。一旦完成扫描,我们可以绘制模型所做的分类决策。
-
导入所有相关的包。对于这个练习,我们还需要使用 NumPy:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap from sklearn.neighbors import KNeighborsClassifier as KNN
-
将鸢尾花数据集加载到 pandas 的 DataFrame 中:
df = pd.read_csv('iris-data.csv') df.head()
输出将如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_41.jpg
图 4.41: 前五行
-
虽然我们可以在之前的练习中使用物种字符串来创建模型,但在绘制决策边界时,将物种映射到单独的整数值会更有用。为此,创建一个标签列表以供后续参考,并遍历该列表,用对应的索引替换现有的标签:
labelled_species = [ 'Iris-setosa', 'Iris-versicolor', 'Iris-virginica', ] for idx, label in enumerate(labelled_species): df.Species = df.Species.replace(label, idx) df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_42.jpg
图 4.42:前五行
注意在
for
循环定义中使用了enumerate
函数。在迭代for
循环时,enumerate
函数在每次迭代中提供列表中值的索引和该值本身。我们将值的索引赋给idx
变量,将值赋给label
。以这种方式使用enumerate
提供了一种简便的方法来用唯一的整数标签替换物种字符串。 -
构建一个 K-NN 分类模型,仍然使用三个最近邻,并将其拟合到新的物种标签数据的萼片长度和花瓣宽度上:
model = KNN(n_neighbors=3) model.fit(X=df[['Sepal Length', 'Petal Width']], y=df.Species)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_43.jpg
图 4.43:K 邻近分类器
-
为了可视化我们的决策边界,我们需要在信息空间内创建一个网格或预测范围,即所有萼片长度和花瓣宽度的值。从比花瓣宽度和萼片长度的最小值低 1 毫米开始,直到比它们的最大值高 1 毫米,使用 NumPy 的
arange
函数以 0.1(间距)为增量创建这些限制之间的值范围:spacing = 0.1 # 0.1mm petal_range = np.arange(df['Petal Width'].min() - 1, df['Petal Width'].max() + 1, spacing) sepal_range = np.arange(df['Sepal Length'].min() - 1, df['Sepal Length'].max() + 1, spacing)
-
使用 NumPy 的
meshgrid
函数将两个范围合并为一个网格:xx, yy = np.meshgrid(sepal_range, petal_range) # Create the mesh
查看
xx
:xx
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_44.jpg
图 4.44:meshgrid xx 值数组
查看
yy
:yy
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_45.jpg
图 4.45:meshgrid yy 值数组
-
使用
np.c_
将 mesh 拼接成一个单独的 NumPy 数组:pred_x = np.c_[xx.ravel(), yy.ravel()] # Concatenate the results pred_x
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_46.jpg
图 4.46:预测值数组
虽然这个函数调用看起来有些神秘,但它仅仅是将两个独立的数组连接在一起(参见
docs.scipy.org/doc/numpy/reference/generated/numpy.c_.html
),并且是concatenate
的简写形式。 -
为网格生成类别预测:
pred_y = model.predict(pred_x).reshape(xx.shape) pred_y
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_47.jpg
图 4.47:预测 y 值数组
-
为了持续可视化边界,我们需要两组一致的颜色:一组较浅的颜色用于决策边界,一组较深的颜色用于训练集点本身。创建两个
ListedColormaps
:# Create color maps cmap_light = ListedColormap(['#F6A56F', '#6FF6A5', '#A56FF6']) cmap_bold = ListedColormap(['#E6640E', '#0EE664', '#640EE6'])
-
为了突出显示决策边界,首先根据鸢尾花物种绘制训练数据,使用
cmap_bold
颜色方案,并为每个不同物种使用不同的标记:markers = { 'Iris-setosa': {'marker': 'x', 'facecolor': 'k', 'edgecolor': 'k'}, 'Iris-versicolor': {'marker': '*', 'facecolor': 'none', 'edgecolor': 'k'}, 'Iris-virginica': {'marker': 'o', 'facecolor': 'none', 'edgecolor': 'k'}, } plt.figure(figsize=(10, 7)) for name, group in df.groupby('Species'): species = labelled_species[name] plt.scatter(group['Sepal Length'], group['Petal Width'], c=cmap_bold.colors[name], label=labelled_species[name], marker=markers[species]['marker'] ) plt.title('Species Classification Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)'); plt.legend();
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_48.jpg
图 4.48:散点图与突出显示的决策边界
-
使用之前创建的预测网格,绘制决策边界并与训练数据一起展示:
plt.figure(figsize=(10, 7)) plt.pcolormesh(xx, yy, pred_y, cmap=cmap_light); plt.scatter(df['Sepal Length'], df['Petal Width'], c=df.Species, cmap=cmap_bold, edgecolor='k', s=20); plt.title('Species Decision Boundaries Sepal Length vs Petal Width'); plt.xlabel('Sepal Length (mm)'); plt.ylabel('Petal Width (mm)');
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_49.jpg
图 4.49:决策边界
注意
图 4.49已修改为灰度打印,并增加了额外的标签,指示类别的预测边界。
活动 13:K-NN 多类别分类器
在此活动中,我们将使用 K-NN 模型将 MNIST 数据集分类为 10 个不同的数字类别。
要执行的步骤如下:
-
导入以下包:
import struct import numpy as np import gzip import urllib.request import matplotlib.pyplot as plt from array import array from sklearn.neighbors import KNeighborsClassifier as KNN
-
将 MNIST 数据加载到内存中。首先是训练图像,然后是训练标签,再然后是测试图像,最后是测试标签。
-
可视化数据样本。
-
构建一个 K-NN 分类器,使用三个最近邻来分类 MNIST 数据集。同样,为了节省处理能力,随机抽取 5,000 张图像用于训练。
-
为了将图像信息提供给模型,我们必须先将数据展平,使每个图像的形状为 1 x 784 像素。
-
构建三邻居 KNN 模型并将数据拟合到模型中。请注意,在本活动中,我们为模型提供了 784 个特征或维度,而不仅仅是 2 个。
-
确定与训练集的评分。
-
显示模型在训练数据上的前两个预测结果。
-
比较与测试集的性能。
注意
该活动的解答可以在第 360 页找到。
使用决策树进行分类
本章将要研究的最终分类方法是决策树,它在自然语言处理等应用中得到了广泛应用。决策树下有多种不同的机器学习算法,例如 ID3、CART 以及强大的随机森林分类器(在第五章,集成建模中介绍)。在本章中,我们将研究 ID3 方法在分类类别数据中的应用,并使用 scikit-learn 中的 CART 实现作为另一种分类鸢尾花数据集的方式。那么,究竟什么是决策树呢?
如名称所示,决策树是一种学习算法,它通过一系列基于输入信息的决策来进行最终分类。回想一下你小时候的生物学课,你可能使用过类似决策树的过程,通过二分法键来分类不同类型的动物。就像图 4.50所示的二分法键,决策树旨在根据一系列决策或提问步骤的结果来分类信息:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_50.jpg
图 4.50:使用二分法键进行动物分类
根据所使用的决策树算法,决策步骤的实现可能会略有不同,但我们将特别考虑 ID3 算法的实现。迭代二分法 3(ID3)算法旨在根据每个决策提供的最大信息增益来对数据进行分类。为了进一步理解这一设计,我们还需要理解两个额外的概念:熵和信息增益。
注意
ID3 算法最早由澳大利亚研究员 Ross Quinlan 于 1985 年提出(doi.org/10.1007/BF00116251
)。
- 熵:在信息论的背景下,熵是随机数据源提供信息的平均速率。从数学角度来看,这个熵定义为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_51.jpg
图 4.51:熵方程
在这种情况下,当随机数据源产生低概率值时,事件携带的信息更多,因为与发生高概率事件时相比,它是不可预见的。
- 信息增益:与熵相关的概念是通过观察另一个随机变量获得关于某个随机变量的信息量。给定一个数据集 S 和一个观察的属性 a,信息增益在数学上定义为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_52.jpg
图 4.52:信息增益方程
数据集 S 对于属性 a 的信息增益等于 S 的熵减去条件于属性 a 的 S 的熵,或者说,数据集 S 的熵减去 t 中元素的比例与 S 中元素的比例,再乘以 t 的熵,其中 t 是属性 a 中的某个类别。
如果你一开始觉得这里的数学有些令人畏惧,别担心,它比看起来简单得多。为了澄清 ID3 过程,我们将使用 Quinlan 在原始论文中提供的相同数据集来演示这个过程。
练习 41:ID3 分类
在原始论文中,Quinlan 提供了一个包含 10 个天气观察样本的小数据集,每个样本被标记为 P,表示天气适合进行某种活动,比如说星期六早上的板球比赛,或者北美朋友们喜欢的棒球比赛。如果天气不适合比赛,则标记为 N。论文中描述的示例数据集将在练习中创建。
-
在 Jupyter notebook 中,创建以下训练集的 pandas DataFrame:
df = pd.DataFrame() df['Outlook'] = [ 'sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain' ] df['Temperature'] = [ 'hot', 'hot', 'hot', 'mild', 'cool', 'cool', 'cool', 'mild', 'cool', 'mild', 'mild', 'mild', 'hot', 'mild', ] df['Humidity'] = [ 'high', 'high', 'high', 'high', 'normal', 'normal', 'normal', 'high', 'normal', 'normal', 'normal', 'high', 'normal', 'high' ] df['Windy'] = [ 'Weak', 'Strong', 'Weak', 'Weak', 'Weak', 'Strong', 'Strong', 'Weak', 'Weak', 'Weak', 'Strong', 'Strong', 'Weak', 'Strong' ] df['Decision'] = [ 'N', 'N', 'P', 'P', 'P', 'N', 'P', 'N', 'P', 'P', 'P', 'P', 'P', 'N' ] df
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_53.jpg
图 4.53:Pandas DataFrame
-
在原始论文中,ID3 算法首先随机选择一个小的训练集样本,并将树拟合到这个窗口。对于大数据集,这可能是一个有用的方法,但考虑到我们的数据集相对较小,我们将直接从整个训练集开始。第一步是计算
P
和N
的熵:# Probability of P p_p = len(df.loc[df.Decision == 'P']) / len(df) # Probability of N p_n = len(df.loc[df.Decision == 'N']) / len(df) entropy_decision = -p_n * np.log2(p_n) - p_p * np.log2(p_p) print(f'H(S) = {entropy_decision:0.4f}')
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_54.jpg
图 4.54:熵决策
-
我们需要重复此计算,因此将其封装成一个函数:
def f_entropy_decision(data): p_p = len(data.loc[data.Decision == 'P']) / len(data) p_n = len(data.loc[data.Decision == 'N']) / len(data) return -p_n * np.log2(p_n) - p_p * np.log2(p_p)
-
下一步是计算哪一个属性通过
groupby
方法提供了最高的信息增益:IG_decision_Outlook = entropy_decision # H(S) # Create a string to print out the overall equation overall_eqn = 'Gain(Decision, Outlook) = Entropy(Decision)' # Iterate through the values for outlook and compute the probabilities # and entropy values for name, Outlook in df.groupby('Outlook'): num_p = len(Outlook.loc[Outlook.Decision == 'P']) num_n = len(Outlook.loc[Outlook.Decision != 'P']) num_Outlook = len(Outlook) print(f'p(Decision=P|Outlook={name}) = {num_p}/{num_Outlook}') print(f'p(Decision=N|Outlook={name}) = {num_n}/{num_Outlook}') print(f'p(Decision|Outlook={name}) = {num_Outlook}/{len(df)}') print(f'Entropy(Decision|Outlook={name}) = '\ f'-{num_p}/{num_Outlook}.log2({num_p}/{num_Outlook}) - '\ f'{num_n}/{num_Outlook}.log2({num_n}/{num_Outlook})') entropy_decision_outlook = 0 # Cannot compute log of 0 so add checks if num_p != 0: entropy_decision_outlook -= (num_p / num_Outlook) \ * np.log2(num_p / num_Outlook) # Cannot compute log of 0 so add checks if num_n != 0: entropy_decision_outlook -= (num_n / num_Outlook) \ * np.log2(num_n / num_Outlook) IG_decision_Outlook -= (num_Outlook / len(df)) * entropy_decision_outlook print() overall_eqn += f' - p(Decision|Outlook={name}).' overall_eqn += f'Entropy(Decision|Outlook={name})' print(overall_eqn) print(f'Gain(Decision, Outlook) = {IG_decision_Outlook:0.4f}')
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_56.jpg
图 4.56:概率熵和增益
前景的最终增益方程可以重新写为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_57.jpg
图 4.57:信息增益的方程
-
我们需要重复这个过程好几次,因此将其封装成一个函数,以便后续使用:
def IG(data, column, ent_decision=entropy_decision): IG_decision = ent_decision for name, temp in data.groupby(column): p_p = len(temp.loc[temp.Decision == 'P']) / len(temp) p_n = len(temp.loc[temp.Decision != 'P']) / len(temp) entropy_decision = 0 if p_p != 0: entropy_decision -= (p_p) * np.log2(p_p) if p_n != 0: entropy_decision -= (p_n) * np.log2(p_n) IG_decision -= (len(temp) / len(df)) * entropy_decision return IG_decision
-
对其他每一列重复此过程,以计算相应的信息增益:
for col in df.columns[:-1]: print(f'Gain(Decision, {col}) = {IG(df, col):0.4f}')
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_58.jpg
图 4.58:增益
-
这些信息为树提供了第一个决策。我们希望根据最大的信息增益进行拆分,因此我们在前景上进行拆分。查看基于前景的数据拆分:
for name, temp in df.groupby('Outlook'): print('-' * 15) print(name) print('-' * 15) print(temp) print('-' * 15)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_59.jpg
图 4.59:信息增益
注意到所有阴天记录的决策都是P。这为我们的决策树提供了第一个终止叶节点。如果是阴天,我们将会玩,而如果是雨天或晴天,则有可能我们不玩。到目前为止的决策树可以表示为下图:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_60.jpg
图 4.60:决策树
注意
该图是手动创建的用于参考,并未包含在随附的源代码中。
-
现在我们重复这个过程,根据信息增益进行拆分,直到所有数据都被分配,决策树的所有分支终止。首先,移除阴天样本,因为它们不再提供任何额外信息:
df_next = df.loc[df.Outlook != 'overcast'] df_next
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_61.jpg
图 4.61:移除阴天样本后的数据
-
现在,我们将注意力转向晴天样本,并重新运行增益计算以确定最佳的晴天信息拆分方式:
df_sunny = df_next.loc[df_next.Outlook == 'sunny']
-
重新计算晴天样本的熵:
entropy_decision = f_entropy_decision(df_sunny) entropy_decision
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_62.jpg
图 4.62:熵决策
-
对晴天样本运行增益计算:
for col in df_sunny.columns[1:-1]: print(f'Gain(Decision, {col}) = {IG(df_sunny, col, entropy_decision):0.4f}')
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_63.jpg
图 4.63:增益
-
再次,我们选择增益最大的属性,即湿度。根据湿度对数据进行分组:
for name, temp in df_sunny.groupby('Humidity'): print('-' * 15) print(name) print('-' * 15) print(temp) print('-' * 15)
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_64.jpg
图 4.64: 根据湿度分组后的数据
我们可以看到,当湿度高时,决策是“不玩”,而当湿度正常时,决策是“玩”。因此,更新我们对决策树的表示,我们得到:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_65.jpg
图 4.65: 拥有两个值的决策树
-
因此,最后需要分类的数据是雨天预报数据。只提取雨天数据并重新运行熵计算:
df_rain = df_next.loc[df_next.Outlook == 'rain'] entropy_decision = f_entropy_decision(df_rain) entropy_decision
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_66.jpg
图 4.66: 熵决策
-
对雨天子集重复计算增益:
for col in df_rain.columns[1:-1]: print(f'Gain(Decision, {col}) = {IG(df_rain, col, entropy_decision):0.4f}')
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_67.jpg
图 4.67: 增益
-
同样,基于最大增益值的属性进行分割时,需要根据风速值进行分割。所以,按风速对剩余信息进行分组:
for name, temp in df_rain.groupby('Windy'): print('-' * 15) print(name) print('-' * 15) print(temp) print('-' * 15)
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_68.jpg
图 4.68: 根据风速分组的数据
最后,我们得到了完成树所需的所有终止叶子节点,因为根据风速进行分割得到两个集合,这些集合都表示“玩”(P)或“不玩”(N)的值。我们的完整决策树如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_69.jpg
图 4.69: 最终决策树
我们可以看到,决策树与 K-NN 模型非常相似,都是利用整个训练集来构建模型。那么,如何用未见过的信息进行预测呢?只需要沿着树走,查看每个节点的决策,并应用未见样本的数据。最终的预测结果将是终止叶子节点指定的标签。假设我们有即将到来的周六的天气预报,想预测我们是否会进行游戏。天气预报如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_70.jpg
图 4.70: 即将到来的周六的天气预报
该决策树如下所示(虚线圆圈表示树中选择的叶子节点):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_71.jpg
图 4.71: 使用决策树进行新预测
现在,希望你已经对决策树的基本概念和序列决策过程有了合理的理解。在本练习中,我们介绍了其中一种初步的决策树方法,但实际上还有许多其他方法,而且许多更现代的方法,例如随机森林,比 ID3 更不容易发生过拟合(有关更多信息,请参见第五章,集成建模)。在掌握了决策树的基本原理后,我们现在将探讨如何使用 scikit-learn 提供的功能应用更复杂的模型。
scikit-learn 的决策树方法实现了 CART(分类与回归树)方法,这提供了在分类和回归问题中使用决策树的能力。CART 与 ID3 的不同之处在于,决策是通过将特征值与计算出的值进行比较来做出的,例如,对于 Iris 数据集,花瓣宽度是否小于 x 毫米?
练习 42:使用 CART 决策树进行 Iris 分类
在本练习中,我们将使用 scikit-learn 的决策树分类器对 Iris 数据进行分类,该分类器可以应用于分类和回归问题:
-
导入所需的包:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.tree import DecisionTreeClassifier
-
加载 Iris 数据集:
df = pd.read_csv('iris-data.csv') df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_72.jpg
图 4.72:前五行数据
-
随机抽取 10 行数据用于测试。决策树可能会过拟合训练数据,因此这将提供一个独立的测量指标来评估树的准确性:
np.random.seed(10) samples = np.random.randint(0, len(df), 10) df_test = df.iloc[samples] df = df.drop(samples)
-
将模型拟合到训练数据并检查相应的准确性:
model = DecisionTreeClassifier() model = model.fit(df[['Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width']], df.Species) model.score(df[['Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width']], df.Species)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_73.jpg
图 4.73:模型得分输出
我们的模型在训练集上的准确率为 100%。
-
检查测试集上的表现:
model.score(df_test[['Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width']], df_test.Species)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_74.jpg
图 4.74:使用 df_test 输出的模型得分
-
决策树的一个优点是我们可以直观地表示模型,并准确看到模型的运作方式。安装所需的依赖包:
!conda install python-graphviz
-
导入图形绘制包:
import graphviz from sklearn.tree import export_graphviz
-
绘制模型图:
dot_data = export_graphviz(model, out_file=None) graph = graphviz.Source(dot_data) graph
我们将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_04_75.jpg
图 4.75:CART 决策树的决策
该图展示了在 scikit-learn 模型中 CART 决策树的决策过程。每个节点的第一行表示在每个步骤做出的决策。第一个节点 X[2] <= 2.45 表示训练数据在第二列(花瓣长度)上进行划分,基于是否小于或等于 2.45。花瓣长度小于 2.45 的样本(共有 46 个)全部属于鸢尾花的 setosa 类,因此它们的 gini
值(类似于信息增益的度量)为零。如果花瓣长度大于 2.45,下一步的决策是花瓣宽度(第三列)是否小于或等于 1.75 毫米。这个决策/分支过程会一直持续,直到树被完全展开,所有终止叶子节点都已构建完成。
总结
本章介绍了许多强大且极其有用的分类模型,从使用线性回归作为分类器开始,然后通过使用逻辑回归分类器,我们观察到了显著的性能提升。接着,我们转向了 记忆 型模型,比如 K-NN,尽管它简单易于拟合,但能够在分类过程中形成复杂的非线性边界,即使输入信息是图像数据。最后,我们介绍了决策树和 ID3 算法。我们看到,像 K-NN 模型一样,决策树通过使用规则和决策门来记住训练数据,从而进行预测,并且具有相当高的准确性。
在下一章中,我们将扩展本章所学内容,涵盖集成技术,包括提升方法和非常有效的随机森林方法。
第五章:集成建模
学习目标
到本章结束时,你将能够:
-
解释偏差和方差的概念,以及它们如何导致欠拟合和过拟合
-
解释自助法(bootstrapping)背后的概念
-
使用决策树实现一个袋装分类器(bagging classifier)
-
实现自适应增强(adaptive boosting)和梯度增强(gradient boosting)模型
-
使用多个分类器实现堆叠集成(stacked ensemble)
本章介绍了偏差与方差,欠拟合与过拟合的内容,然后介绍集成建模。
介绍
在前几章中,我们讨论了两种监督学习问题:回归和分类。我们研究了每种类型的若干算法,并深入探讨了这些算法的工作原理。
但是,有时这些算法,无论多么复杂,都似乎无法在我们拥有的数据上表现得很好。可能有多种原因:也许数据本身不够好,也许我们试图找出的趋势根本不存在,或者可能是模型本身太复杂。
等等。什么?模型“过于复杂”怎么会是一个问题?哦,当然可以!如果模型过于复杂,而且数据量不足,模型可能会与数据拟合得过于精确,甚至学习到噪声和异常值,这正是我们所不希望发生的。
许多时候,当单个复杂算法给出的结果差异很大时,通过聚合一组模型的结果,我们可以得到更接近实际真相的结果。这是因为所有单个模型的误差有很大可能性会在我们做预测时互相抵消。
这种将多个算法组合在一起以进行聚合预测的方法就是集成建模的基础。集成方法的最终目标是将若干表现不佳的基本估计器(即各个独立算法)以某种方式组合起来,从而提高系统的整体性能,使得集成的算法结果能够生成一个比单一算法更强大、能更好地泛化的模型。
在本章中,我们将讨论如何构建一个集成模型来帮助我们建立一个强健的系统,使其能够做出准确的预测,而不会增加方差。我们将从讨论模型表现不佳的一些原因开始,然后转到偏差和方差的概念,以及过拟合和欠拟合。我们将介绍集成建模作为解决这些性能问题的方法,并讨论不同的集成方法,这些方法可以用于解决与表现不佳的模型相关的不同类型问题。
本章将讨论三种集成方法:装袋(bagging)、提升(boosting)和堆叠(stacking)。每种方法将从基本理论讨论到各种使用案例的讨论,以及每种方法可能不适合的使用案例。本章还将通过多个练习步骤引导您使用 Python 中的 scikit-learn 库来实现这些模型。
练习 43:导入模块并准备数据集
在这个练习中,我们将导入本章所需的所有模块,并准备好我们的数据集以进行接下来的练习:
-
导入所有必要的模块来操作数据和评估模型:
import pandas as pd import numpy as np %matplotlib inline import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score from sklearn.model_selection import KFold
-
本章中将使用的数据集是泰坦尼克号数据集,此数据集在之前的章节中也有介绍。读取数据集并打印前五行:
data = pd.read_csv('titanic.csv') data.head()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_01.jpg
图 5.1:前五行
-
为了使数据集准备好使用,我们将添加一个
preprocess
函数,该函数将预处理数据集以使其符合 scikit-learn 库可接受的格式。本章假设数据集已经经过预处理并准备好使用,但我们将添加一个
preprocess
函数,该函数将预处理数据集以使其符合Scikit-learn
库可接受的格式。首先,我们创建一个
fix_age
函数来预处理age
函数并获得整数值。如果年龄为空,函数将返回*-1以区分可用值,并且如果值小于1的分数,则将年龄值乘以100*。然后,我们将此函数应用于age
列。然后,我们将
Sex
列转换为二元变量,女性为1,男性为0,随后使用 pandas 的get_dummies
函数为Embarked
列创建虚拟二元列。之后,我们将包含虚拟列的 DataFrame 与其余数值列组合,以创建最终 DataFrame,并由该函数返回。def preprocess(data): def fix_age(age): if np.isnan(age): return -1 elif age < 1: return age*100 else: return age data.loc[:, 'Age'] = data.Age.apply(fix_age) data.loc[:, 'Sex'] = data.Sex.apply(lambda s: int(s == 'female')) embarked = pd.get_dummies(data.Embarked, prefix='Emb')[['Emb_C','Emb_Q','Emb_S']] cols = ['Pclass','Sex','Age','SibSp','Parch','Fare'] return pd.concat([data[cols], embarked], axis=1).values
-
将数据集分为训练集和验证集。
我们将数据集分为两部分 - 一部分用于练习中训练模型(
train
),另一部分用于进行预测以评估每个模型的性能(val
)。我们将使用前一步中编写的函数分别预处理训练和验证数据集。这里,
Survived
二元变量是目标变量,确定每行中个体是否幸存于泰坦尼克号的沉没,因此我们从这两个拆分中的依赖变量列创建y_train
和y_val
:train, val = train_test_split(data, test_size=0.2, random_state=11) x_train = preprocess(train) y_train = train['Survived'].values x_val = preprocess(val) y_val = val['Survived'].values
让我们开始吧。
过拟合和欠拟合
假设我们将一个监督学习算法拟合到数据上,并随后使用该模型对一个独立的验证集进行预测。基于该模型如何进行泛化,也就是它对验证数据集中的数据点做出的预测,我们将认为该模型表现良好。
有时我们发现模型无法做出准确的预测,并且在验证数据集上的表现较差。这种较差的表现可能是由于模型过于简单,无法适当地拟合数据,或者模型过于复杂,无法对验证数据集进行有效的泛化。在前一种情况下,模型具有高偏差,导致欠拟合,而在后一种情况下,模型具有高方差,导致过拟合。
偏差
机器学习模型预测中的偏差表示预测值与真实值之间的差异。如果平均预测值与真实值相差较大,则模型被认为具有高偏差;反之,如果平均预测值接近真实值,则模型被认为具有低偏差。
高偏差表示模型无法捕捉数据中的复杂性,并且无法识别输入和输出之间的相关关系。
方差
机器学习模型预测中的方差表示预测值与真实值之间的分散程度。如果预测值分散且不稳定,则模型被认为具有高方差;反之,如果预测值一致且不太分散,则模型被认为具有低方差。
高方差表示模型无法对模型以前未见过的数据点进行泛化和做出准确预测:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_02.jpg
图 5.2:数据点具有高偏差和高方差的可视化表示
欠拟合
假设我们在训练数据集上拟合了一个简单的模型,一个具有低模型复杂度的模型,例如一个简单的线性模型。我们拟合了一个能够在一定程度上表示训练数据中 X 和 Y 数据点之间关系的函数,但我们发现训练误差仍然很高。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_03.jpg
图 5.3:回归中的欠拟合与理想拟合
例如,查看图 5.3中的两个回归图;第一个图显示了一个将直线拟合到数据的模型,第二个图显示了一个尝试将相对复杂的多项式拟合到数据的模型,后者似乎很好地表示了 X 和 Y 之间的映射关系。
我们可以说,第一个模型展示了欠拟合,因为它表现出了高偏差和低方差的特征;也就是说,虽然它无法捕捉输入与输出之间映射的复杂性,但它在预测中保持一致。这个模型在训练数据和验证数据上都会有较高的预测误差。
过拟合
假设我们训练了一个高度复杂的模型,几乎可以完美地对训练数据集进行预测。我们已经设法拟合了一个函数来表示训练数据中 X 和 Y 数据点之间的关系,使得训练数据上的预测误差极低:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_04.jpg
图 5.4:回归中的理想拟合与过拟合
从图 5.4中的两个图中,我们可以看到,第二个图显示了一个试图对数据点拟合高度复杂函数的模型,而左侧的图代表了给定数据的理想拟合。
很明显,当我们尝试使用第一个模型预测在训练集未出现的 X 数据点的 Y 值时,我们会发现预测结果与相应的真实值相差甚远。这就是过拟合的表现:模型对数据拟合得过于精确,以至于无法对新的数据点进行泛化,因为模型学习了训练数据中的随机噪声和离群值。
这个模型展示了高方差和低偏差的特征:虽然平均预测值会接近真实值,但与真实值相比,预测值会相对分散。
克服欠拟合和过拟合的问题
从前面的章节中我们可以看到,当我们从一个过于简单的模型过渡到过于复杂的模型时,我们从一个具有高偏差和低方差的欠拟合模型,过渡到一个具有低偏差和高方差的过拟合模型。任何监督学习算法的目标是实现低偏差和低方差,并找到欠拟合和过拟合之间的平衡点。这将有助于算法从训练数据到验证数据点的良好泛化,从而在模型从未见过的数据上也能表现出良好的预测性能。
当模型对数据欠拟合时,改进性能的最佳方法是增加模型的复杂性,以便识别数据中的相关关系。这可以通过添加新特征或创建高偏差模型的集成来实现。然而,在这种情况下,添加更多的数据进行训练并没有帮助,因为限制因素是模型复杂度,更多的数据不会帮助减少模型的偏差。
然而,过拟合问题更难解决。以下是一些常见的应对过拟合问题的技术:
-
获取更多数据:一个高度复杂的模型很容易在小数据集上过拟合,但在大数据集上则不容易出现过拟合。
-
降维:减少特征数量有助于让模型变得不那么复杂。
-
正则化:向代价函数中添加一个新项,以调整系数(特别是线性回归中的高阶系数)使其趋向于较低值。
-
集成建模:聚合多个过拟合模型的预测结果可以有效地消除预测中的高方差,并且比单个过拟合训练数据的模型表现得更好。
我们将在第六章《模型评估》中更详细地讨论前三种方法的细微差别和考虑因素;本章将重点介绍不同的集成建模技术。一些常见的集成方法包括:
-
Bagging:即引导聚合的简称,这种技术也用于减少模型的方差并避免过拟合。其过程是一次性选择一部分特征和数据点,对每个子集训练一个模型,随后将所有模型的结果汇聚成最终的预测。
-
Boosting:这种技术用于减少偏差,而不是减少方差,它通过逐步训练新的模型,聚焦于之前模型中的错误分类数据点。
-
Stacking:这种技术的目的是提高分类器的预测能力,它涉及训练多个模型,然后使用组合算法根据所有这些模型的预测结果作为额外输入来做出最终预测。
我们将从 Bagging 开始,然后转向 Boosting 和 Stacking。
Bagging
“Bagging”一词来源于一种名为引导聚合(bootstrap aggregation)的方法。为了实现成功的预测模型,了解在何种情况下我们可以从使用引导法(bootstrapping)构建集成模型中受益是非常重要的。在本节中,我们将讨论如何利用引导方法创建一个最小化方差的集成模型,并探讨如何构建一个决策树集成模型,也就是随机森林算法。那么,什么是引导法,它如何帮助我们构建强健的集成模型呢?
引导法
引导法是指带有放回的随机抽样,即从由随机选择的数据点组成的数据集中抽取多个样本(每个样本称为重抽样),其中每个重抽样可能包含重复的数据点,每个数据点都有相同的概率从整个数据集中被选中:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_05.jpg
图 5.5:随机选择数据点
从前面的图示中,我们可以看到,从主数据集中抽取的五个引导样本各不相同,且具有不同的特征。因此,在每个重抽样上训练模型将会得到不同的预测结果。
以下是自举的优势:
-
每个重新采样可以包含与整个数据集不同的特征,这使我们能够从不同的视角了解数据的行为。
-
利用自举法的算法能够更加健壮,并且更好地处理未见过的数据,特别是在容易导致过拟合的较小数据集上。
-
自举法可以通过使用具有不同变化和特征的数据集来测试预测的稳定性,从而得到更加健壮的模型。
自举聚合
现在我们知道了什么是自举,那么装袋集成究竟是什么?它本质上是一个集成模型,它在每个重新采样上生成多个预测器的版本,并使用这些版本来获取聚合的预测器。在回归问题中,聚合步骤通过模型的平均值来进行元预测,在分类问题中则通过投票来进行预测类别。
以下图示展示了如何从自举抽样构建装袋估计器,具体见图 5.5:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_06.jpg
图 5.6:从自举抽样构建的装袋估计器
由于每个模型基本上是独立的,所有基础模型可以并行训练,这显著加快了训练过程,并允许我们利用当今手头的计算能力。
装袋通过在其构建过程中引入随机化来帮助减少整个集成的方差,并且通常与具有过度拟合训练数据倾向的基础预测器一起使用。在这里需要考虑的主要点是训练数据集的稳定性(或缺乏稳定性):在数据中轻微的扰动可能导致训练模型显著变化的情况下,装袋可以提高准确性。
scikit-learn 使用 BaggingClassifier
和 BaggingRegressor
来实现用于分类和回归任务的通用装袋集成。这些的主要输入是在每次重新采样上使用的基础估计器,以及要使用的估计器数量(即重新采样的数量)。
练习 44:使用装袋分类器
在本练习中,我们将使用 scikit-learn 的 BaggingClassifier
作为我们的集成,使用 DecisionTreeClassifier
作为基础估计器。我们知道决策树容易过拟合,因此在装袋集成中使用的基础估计器应具有高方差和低偏差,这两者都是重要的特征。
-
导入基础和集成分类器:
from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import BaggingClassifier
-
指定超参数并初始化模型。
在这里,我们将首先指定基础估计器的超参数,使用决策树分类器,并以熵或信息增益作为划分标准。我们不会对树的深度或每棵树的叶节点大小/数量设置任何限制,以便树能够完全生长。接下来,我们将定义袋装分类器的超参数,并将基础估计器对象作为超参数传递给分类器。
对于我们的示例,我们将选择 50 个基础估计器,这些估计器将并行运行并利用机器上所有可用的处理器(通过指定
n_jobs=-1
来实现)。此外,我们将指定max_samples
为 0.5,表示自助法样本数量应为总数据集的一半。我们还将设置一个随机状态(为任意值,且在整个过程中保持不变),以确保结果的可复现性:dt_params = { 'criterion': 'entropy', 'random_state': 11 } dt = DecisionTreeClassifier(**dt_params) bc_params = { 'base_estimator': dt, 'n_estimators': 50, 'max_samples': 0.5, 'random_state': 11, 'n_jobs': -1 } bc = BaggingClassifier(**bc_params)
-
拟合袋装分类器模型到训练数据并计算预测准确性。
让我们拟合袋装分类器,并找出训练集和验证集的元预测。接下来,找出训练集和验证集数据集的预测准确性:
bc.fit(x_train, y_train) bc_preds_train = bc.predict(x_train) bc_preds_val = bc.predict(x_val) print('Bagging Classifier:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=bc_preds_train), accuracy_score(y_true=y_val, y_pred=bc_preds_val) ))
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_07.jpg
图 5.7:袋装分类器的预测准确性
-
拟合决策树模型到训练数据以比较预测准确性。
我们还将拟合决策树(使用在第二步中初始化的对象),以便能够将集成模型的预测准确性与基础预测器进行比较:
dt.fit(x_train, y_train) dt_preds_train = dt.predict(x_train) dt_preds_val = dt.predict(x_val) print('Decision Tree:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=dt_preds_train), accuracy_score(y_true=y_val, y_pred=dt_preds_val) ))
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_08.jpg
图 5.8:决策树的预测准确性
在这里,我们可以看到,尽管决策树的训练准确度远高于袋装分类器,但在验证集上的准确度较低,明显表明决策树对训练数据发生了过拟合。另一方面,袋装集成方法减少了整体方差,从而得到更为准确的预测。
随机森林
决策树常见的问题是每个节点的划分是使用贪婪算法进行的,该算法通过最小化叶节点的熵来进行划分。考虑到这一点,袋装分类器中的基础估计器决策树在划分特征上可能仍然相似,因此其预测结果也可能非常相似。然而,只有当基础模型的预测结果不相关时,袋装方法才有助于减少预测的方差。
随机森林算法通过不仅对整体训练数据集中的数据点进行引导抽样,还对每棵树的分裂特征进行引导抽样,从而尝试克服这个问题。这确保了当贪心算法在搜索最佳特征进行分裂时,整体最佳特征可能并不总是在引导抽样的特征中可用,因此不会被选择——从而导致基础树具有不同的结构。这个简单的调整使得最佳估计器能够以这样的方式进行训练:即森林中每棵树的预测结果与其他树的预测结果相关的概率更低。
随机森林中的每个基础估计器都有一个随机的数据点样本和一个随机的特征样本。由于集成是由决策树构成的,因此该算法被称为随机森林。
练习 45:使用随机森林构建集成模型
随机森林的两个主要参数是特征的比例和训练每个基础决策树的引导数据点的比例。
在本次练习中,我们将使用 scikit-learn 的RandomForestClassifier
来构建集成模型:
-
导入集成分类器:
from sklearn.ensemble import RandomForestClassifier
-
指定超参数并初始化模型。
在这里,我们将使用熵作为决策树分裂标准,森林中包含 100 棵树。与之前一样,我们不会对树的深度或叶子节点的大小/数量设置任何限制。与袋装分类器不同,袋装分类器在初始化时需要输入
max_samples
,而随机森林算法只接受max_features
,表示引导样本中的特征数(或比例)。我们将把此值设置为 0.5,这样每棵树只考虑六个特征中的三个:rf_params = { 'n_estimators': 100, 'criterion': 'entropy', 'max_features': 0.5, 'min_samples_leaf': 10, 'random_state': 11, 'n_jobs': -1 } rf = RandomForestClassifier(**rf_params)
-
将随机森林分类器模型拟合到训练数据并计算预测准确度。
让我们拟合随机森林模型,并找到训练集和验证集的元预测。接下来,我们计算训练集和验证数据集上的预测准确度:
rf.fit(x_train, y_train) rf_preds_train = rf.predict(x_train) rf_preds_val = rf.predict(x_val) print('Random Forest:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=rf_preds_train), accuracy_score(y_true=y_val, y_pred=rf_preds_val) ))
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_09.jpg
图 5.9:使用随机森林的训练和验证准确度
如果我们将随机森林在我们的数据集上的预测准确度与袋装分类器的预测准确度进行比较,我们可以看到,尽管后者在训练数据集上的准确度更高,但在验证集上的准确度几乎相同。
提升法
我们将讨论的第二种集成技术是boosting,它涉及逐步训练新模型,这些模型专注于先前模型中被错分的数据点,并利用加权平均将弱模型(具有高偏差的欠拟合模型)转变为更强的模型。与 bagging 不同,其中每个基本估计器可以独立训练,boosted 算法中每个基本估计器的训练依赖于前一个估计器。
尽管 boosting 也使用了自举法的概念,但与 bagging 不同,由于每个数据样本都有权重,这意味着某些自举样本可能被更频繁地用于训练。在训练每个模型时,算法跟踪哪些特征最有用,哪些数据样本具有最大的预测误差;这些样本被赋予更高的权重,并被认为需要更多次迭代来正确训练模型。
在预测输出时,boosting 集成从每个基本估计器的预测中取加权平均值,对训练阶段中误差较小的模型给予较高的权重。这意味着对于在迭代中由模型错分的数据点,增加这些数据点的权重,以便下一个模型更有可能正确分类它们。
与 bagging 类似,所有 boosting 基本估计器的结果被聚合以产生元预测。然而,与 bagging 不同的是,boosted 集成的准确性随着集成中基本估计器的数量显著增加而增加:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_10.jpg
图 5.10: 一个 boosted 集成
在图中,我们可以看到,在每次迭代后,错分的点具有增加的权重(由较大的图标表示),以便下一个被训练的基本估计器能够专注于这些点。最终预测器已经整合了每个基本估计器的决策边界。
自适应 Boosting
让我们谈谈一种称为自适应 boosting的 boosting 技术,它最适合提升决策桩在二元分类问题中的性能。决策桩本质上是深度为一的决策树(只对一个特征进行一次分割),因此是弱学习器。自适应 boosting 的主要原理与之前相同:通过改进基本估计器在失败区域上的表现,将一组弱学习器转化为强学习器。
首先,第一个基学习器从主训练集中抽取一个数据点的自助样本(bootstrap),并拟合一个决策树桩来对样本点进行分类,之后将训练好的决策树桩拟合到完整的训练数据上。对于那些被误分类的样本,权重会增加,从而增加这些数据点在下一个基学习器的自助样本中被选中的概率。随后,在新的自助样本上再次训练一个决策树桩,对数据点进行分类。接下来,包含两个基学习器的小型集成模型被用来对整个训练集中的数据点进行分类。在第二轮中被误分类的数据点会获得更高的权重,以提高它们被选中的概率,直到集成模型达到所需的基学习器数量为止。
自适应增强(adaptive boosting)的一个缺点是,算法容易受到噪声数据点和异常值的影响,因为它试图完美拟合每一个数据点。因此,当基学习器的数量非常高时,算法容易出现过拟合。
练习 46:自适应增强
在本练习中,我们将使用 scikit-learn 实现的自适应增强分类算法 AdaBoostClassifier
:
-
导入分类器:
from sklearn.ensemble import AdaBoostClassifier
-
指定超参数并初始化模型。
在这里,我们首先指定基学习器的超参数,使用的分类器是最大深度为 1 的决策树分类器,即决策树桩。接下来,我们将定义 AdaBoost 分类器的超参数,并将基学习器对象作为超参数传递给分类器:
dt_params = { 'max_depth': 1, 'random_state': 11 } dt = DecisionTreeClassifier(**dt_params) ab_params = { 'n_estimators': 100, 'base_estimator': dt, 'random_state': 11 } ab = AdaBoostClassifier(**ab_params)
-
将模型拟合到训练数据。
让我们拟合 AdaBoost 模型,并找到训练集和验证集的元预测。接下来,计算训练集和验证集上的预测准确率:
ab.fit(x_train, y_train) ab_preds_train = ab.predict(x_train) ab_preds_val = ab.predict(x_val) print('Adaptive Boosting:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=ab_preds_train), accuracy_score(y_true=y_val, y_pred=ab_preds_val) ))
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_11.jpg
图 5.11:使用自适应增强的训练数据和验证数据的准确性
-
计算不同基学习器数量下,模型在训练数据和验证数据上的预测准确率。
之前我们提到过,随着基学习器数量的增加,准确性通常会提高,但如果使用过多的基学习器,模型也容易出现过拟合。让我们计算预测准确率,以便找出模型开始过拟合训练数据的点:
ab_params = { 'base_estimator': dt, 'random_state': 11 } n_estimator_values = list(range(10, 210, 10)) train_accuracies, val_accuracies = [], [] for n_estimators in n_estimator_values: ab = AdaBoostClassifier(n_estimators=n_estimators, **ab_params) ab.fit(x_train, y_train) ab_preds_train = ab.predict(x_train) ab_preds_val = ab.predict(x_val) train_accuracies.append(accuracy_score(y_true=y_train, y_pred=ab_preds_train)) val_accuracies.append(accuracy_score(y_true=y_val, y_pred=ab_preds_val))
-
绘制一条折线图,直观展示训练集和验证集上的预测准确率趋势:
plt.figure(figsize=(10,7)) plt.plot(n_estimator_values, train_accuracies, label='Train') plt.plot(n_estimator_values, val_accuracies, label='Validation') plt.ylabel('Accuracy score') plt.xlabel('n_estimators') plt.legend() plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_12.jpg
图 5.12:预测准确率的趋势
正如前面提到的,我们可以看到,当决策树桩的数量从 10 增加到 200 时,训练准确度几乎一直在增加。然而,验证准确度在 0.84 到 0.86 之间波动,并且随着决策树桩数量的增加开始下降。这是因为 AdaBoost 算法试图拟合噪声数据点和离群值。
梯度提升
梯度提升是对提升方法的扩展,它将提升过程视为一个优化问题。定义了一个损失函数,代表误差残差(预测值与真实值之间的差异),并使用梯度下降算法来优化损失函数。
在第一步中,添加一个基估计器(这将是一个弱学习器),并在整个训练数据集上进行训练。计算预测所带来的损失,并且为了减少误差残差,更新损失函数,为那些现有估计器表现不佳的数据点添加更多的基估计器。接着,算法迭代地添加新的基估计器并计算损失,以便优化算法更新模型,最小化残差。
在自适应提升的情况下,决策树桩被用作基估计器的弱学习器。然而,对于梯度提升方法,可以使用更大的树,但仍应通过限制最大层数、节点数、分裂数或叶节点数来约束弱学习器。这确保了基估计器仍然是弱学习器,但它们可以以贪婪的方式构建。
从第三章,回归分析中我们知道,梯度下降算法可以用来最小化一组参数,比如回归方程中的系数。然而,在构建集成时,我们使用的是决策树而不是需要优化的参数。每一步计算损失后,梯度下降算法必须修改将要加入集成的新树的参数,以减少损失。这种方法更常被称为功能梯度下降。
练习 47:GradientBoostingClassifier
随机森林的两个主要参数是特征的比例和用于训练每棵基决策树的自助法数据点的比例。
在本次练习中,我们将使用 scikit-learn 的GradientBoostingClassifier
来构建提升集成模型:
-
导入集成分类器:
from sklearn.ensemble import GradientBoostingClassifier
-
指定超参数并初始化模型。
在这里,我们将使用 100 棵决策树作为基估计器,每棵树的最大深度为 3,每个叶节点的最小样本数为 5。虽然我们没有像前面的例子那样使用决策树桩,但树仍然很小,并且可以被视为一个弱学习器:
gbc_params = { 'n_estimators': 100, 'max_depth': 3, 'min_samples_leaf': 5, 'random_state': 11 } gbc = GradientBoostingClassifier(**gbc_params)
-
拟合梯度提升模型到训练数据并计算预测准确性。
让我们拟合集成模型,并找到训练集和验证集的元预测结果。接下来,我们将找到训练集和验证集上的预测准确性:
gbc.fit(x_train, y_train) gbc_preds_train = gbc.predict(x_train) gbc_preds_val = gbc.predict(x_val) print('Gradient Boosting Classifier:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=gbc_preds_train), accuracy_score(y_true=y_val, y_pred=gbc_preds_val) ))
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_13.jpg
图 5.13:训练集和验证集上的预测准确性
我们可以看到,与自适应提升集成模型相比,梯度提升集成模型在训练集和验证集上的准确性都更高。
堆叠
堆叠(Stacking)或堆叠泛化(也叫元集成)是一种模型集成技术,涉及将多个预测模型的信息结合起来,并将其作为特征生成一个新模型。由于堆叠模型通过平滑效应以及能够“选择”在某些场景下表现最好的基础模型,它通常会优于每个单独的模型。考虑到这一点,当每个基础模型之间有显著差异时,堆叠通常是最有效的。
堆叠使用基础模型的预测作为训练最终模型的附加特征——这些被称为元特征。堆叠模型本质上充当一个分类器,决定每个模型在哪些地方表现良好,在哪些地方表现较差。
然而,你不能简单地在整个训练数据上训练基础模型,在整个验证数据集上生成预测结果,然后将这些结果用于二级训练。这会导致基础模型的预测结果已经“看过”测试集,因此在使用这些预测结果时可能会发生过拟合。
需要注意的是,对于每一行的元特征,其值不能使用包含该行训练数据的模型来预测,因为这样会导致过拟合的风险,因为基础预测已经“看过”该行的目标变量。常见的做法是将训练数据分成k个子集,这样,在为每个子集找到元特征时,我们只会在剩余数据上训练模型。这样做还能避免模型已经“看到”的数据过拟合的问题:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_14.jpg
图 5.14:一个堆叠集成模型
上图展示了如何实现这一过程:我们将训练数据分成k个折叠,并通过在剩余的k-1个折叠上训练模型,找到每个折叠上基础模型的预测结果。因此,一旦我们得到每个折叠的元预测结果,就可以将这些元预测结果与原始特征一起用于训练堆叠模型。
练习 48:构建堆叠模型
在此练习中,我们将使用支持向量机(scikit-learn 的 LinearSVC
)和 k 近邻(scikit-learn 的 KNeighborsClassifier
)作为基础预测器,堆叠模型将是逻辑回归分类器。
-
导入基础模型和用于堆叠的模型:
# Base models from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import LinearSVC # Stacking model from sklearn.linear_model import LogisticRegression
-
创建一个新的训练集,其中包含来自基础预测器的额外列。
我们需要为每个模型的预测值创建两个新列,这些列将作为集成模型在测试和训练集中的特征使用。由于 NumPy 数组是不可变的,我们将创建一个新数组,其行数与训练数据集相同,列数比训练数据集多两列。创建数据集后,让我们打印出来看看它的样子:
x_train_with_metapreds = np.zeros((x_train.shape[0], x_train.shape[1]+2)) x_train_with_metapreds[:, :-2] = x_train x_train_with_metapreds[:, -2:] = -1 print(x_train_with_metapreds)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_15.jpg
图 5.15:预测值的新列
正如我们所见,每行末尾有两列填充有 -1 值。
-
使用 k 折策略训练基础模型。
让我们取 k=5。对于这五个折叠,使用其他四个折叠进行训练,并在第五个折叠上进行预测。然后,将这些预测添加到新的 NumPy 数组中用于基础预测的占位列中。
首先,我们用值为
k
和一个随机状态初始化KFold
对象,以保持可重现性。kf.split()
函数将数据集作为输入进行分割,并返回一个迭代器,迭代器中的每个元素分别对应于训练和验证折叠中的索引列表。每次循环迭代器时,可以使用这些索引值将训练数据细分为每行的训练和预测。一旦数据适当地分割,我们就会在四分之四的数据上训练这两个基础预测器,并在剩余四分之一的行上预测值。然后,将这些预测值插入到在 步骤 2 中用
-1
初始化的两个占位列中:kf = KFold(n_splits=5, random_state=11) for train_indices, val_indices in kf.split(x_train): kfold_x_train, kfold_x_val = x_train[train_indices], x_train[val_indices] kfold_y_train, kfold_y_val = y_train[train_indices], y_train[val_indices] svm = LinearSVC(random_state=11, max_iter=1000) svm.fit(kfold_x_train, kfold_y_train) svm_pred = svm.predict(kfold_x_val) knn = KNeighborsClassifier(n_neighbors=4) knn.fit(kfold_x_train, kfold_y_train) knn_pred = knn.predict(kfold_x_val) x_train_with_metapreds[val_indices, -2] = svm_pred x_train_with_metapreds[val_indices, -1] = knn_pred
-
创建一个新的验证集,其中包含来自基础预测器的额外预测列。
就像我们在 步骤 2 中所做的那样,我们也会在验证数据集中添加两个基础模型预测的占位列:
x_val_with_metapreds = np.zeros((x_val.shape[0], x_val.shape[1]+2)) x_val_with_metapreds[:, :-2] = x_val x_val_with_metapreds[:, -2:] = -1 print(x_val_with_metapreds)
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_16.jpg
图 5.16:来自基础预测器的额外预测列
-
在完整的训练集上拟合基础模型,以获取验证集的元特征。
接下来,我们将在完整的训练数据集上训练这两个基础预测器,以获取验证数据集的元预测值。这类似于我们在 步骤 3 中对每个折叠所做的操作:
svm = LinearSVC(random_state=11, max_iter=1000) svm.fit(x_train, y_train) knn = KNeighborsClassifier(n_neighbors=4) knn.fit(x_train, y_train) svm_pred = svm.predict(x_val) knn_pred = knn.predict(x_val) x_val_with_metapreds[:, -2] = svm_pred x_val_with_metapreds[:, -1] = knn_pred
-
训练堆叠模型并使用最终预测计算准确性。
最后一步是使用训练数据集的所有列以及基模型的元预测结果来训练逻辑回归模型。我们使用该模型计算训练集和验证集的预测准确性:
lr = LogisticRegression(random_state=11) lr.fit(x_train_with_metapreds, y_train) lr_preds_train = lr.predict(x_train_with_metapreds) lr_preds_val = lr.predict(x_val_with_metapreds) print('Stacked Classifier:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=lr_preds_train), accuracy_score(y_true=y_val, y_pred=lr_preds_val) ))
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_17.jpg
图 5.17:使用堆叠分类器的准确率
-
比较准确度与基模型的准确度。
为了了解堆叠方法带来的性能提升,我们计算基预测器在训练集和验证集上的准确率,并将其与堆叠模型的准确率进行比较:
print('SVM:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=svm.predict(x_train)), accuracy_score(y_true=y_val, y_pred=svm_pred) )) print('kNN:\n> Accuracy on training data = {:.4f}\n> Accuracy on validation data = {:.4f}'.format( accuracy_score(y_true=y_train, y_pred=knn.predict(x_train)), accuracy_score(y_true=y_val, y_pred=knn_pred) ))
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_05_18.jpg
图 5.18:使用 SVM 和 K-NN 的训练和验证数据准确度
如我们所见,堆叠模型不仅使得验证准确度显著高于任何一个基预测器,而且它的准确度接近 89%,是本章讨论的所有集成模型中最高的。
活动 14:使用独立和集成算法进行堆叠
在本活动中,我们将使用Kaggle 房价:高级回归技术数据库(可在www.kaggle.com/c/house-prices-advanced-regression-techniques/data
上获取,或在 GitHub 上访问github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python
),该数据集我们在第二章《探索性数据分析与可视化》中做过 EDA。这份数据集旨在解决回归问题(即目标变量为连续值的范围)。在本活动中,我们将使用决策树、K-最近邻、随机森林和梯度提升算法在数据上训练个体回归器。然后,我们将构建一个堆叠线性回归模型,使用所有这些算法并比较每个模型的性能。我们将使用平均绝对误差(MAE)作为本活动的评估指标。
执行的步骤如下:
-
导入相关库。
-
读取数据。
-
对数据集进行预处理,去除空值,并对分类变量进行独热编码,为建模准备数据。
-
将数据集分为训练集和验证集。
-
初始化字典以存储训练和验证的 MAE 值。
-
使用以下超参数训练决策树模型并保存得分:
dt_params = { 'criterion': 'mae', 'min_samples_leaf': 10, 'random_state': 11 }
-
使用以下超参数训练 k-最近邻模型并保存得分:
knn_params = { 'n_neighbors': 5 }
-
使用以下超参数训练随机森林模型并保存得分:
rf_params = { 'n_estimators': 50, 'criterion': 'mae', 'max_features': 'sqrt', 'min_samples_leaf': 10, 'random_state': 11, 'n_jobs': -1 }
-
使用以下超参数训练梯度提升模型并保存得分:
gbr_params = { 'n_estimators': 50, 'criterion': 'mae', 'max_features': 'sqrt', 'min_samples_leaf': 10, 'random_state': 11 }
-
准备训练集和验证集,其中四个元估计器具有与前面步骤中使用的相同的超参数。
-
训练一个线性回归模型作为堆叠模型。
-
可视化每个独立模型和堆叠模型的训练误差和验证误差。
注意
该活动的解决方案可以在第 364 页找到。
总结
本章从讨论过拟合和欠拟合以及它们如何影响模型在未见数据上的表现开始。接着,本章探讨了集成建模作为解决这些问题的方法,并继续讨论了可以使用的不同集成方法,以及它们如何减少在进行预测时遇到的总体偏差或方差。
我们首先讨论了袋装算法并介绍了自助抽样的概念。然后,我们看了随机森林作为袋装集成的经典例子,并完成了在之前的泰坦尼克数据集上构建袋装分类器和随机森林分类器的练习。
然后,我们继续讨论了提升算法,如何成功减少系统中的偏差,并理解了如何实现自适应提升和梯度提升。我们讨论的最后一种集成方法是堆叠,正如我们从练习中看到的那样,它给出了我们实现的所有集成方法中最好的准确率。
尽管构建集成模型是减少偏差和方差的好方法,而且它们通常比单一模型表现得更好,但它们本身也有自己的问题和使用场景。虽然袋装(bagging)在避免过拟合时非常有效,但提升(boosting)可以减少偏差和方差,尽管它仍然可能有过拟合的倾向。而堆叠(stacking)则是当一个模型在某部分数据上表现良好,而另一个模型在另一部分数据上表现更好时的好选择。
在下一章,我们将通过探讨验证技术来详细研究克服过拟合和欠拟合问题的方法,也就是评估模型性能的方法,以及如何使用不同的指标作为参考,构建最适合我们用例的模型。
第六章:模型评估
学习目标
本章结束时,你将能够:
-
解释评估模型的重要性
-
使用多种指标评估回归和分类模型
-
选择合适的评估指标来评估和调优模型
-
解释持出数据集的重要性和采样的类型
-
进行超参数调优以找到最佳模型
-
计算特征重要性并解释它们为何重要
本章介绍了如何通过使用超参数和模型评估指标来提升模型性能。
介绍
在前面的三章中,我们讨论了两种类型的监督学习问题——回归和分类,接着介绍了集成模型,它是由多个基础模型的组合构建而成。我们建立了几个模型,并讨论了它们的工作原理及原因。
然而,这还不足以将模型投入生产。模型开发是一个迭代过程,模型训练步骤之后是验证和更新步骤:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_01.jpg
图 6.1:机器学习模型开发过程
本章将解释前面流程图中展示的外围步骤;我们将讨论如何选择合适的超参数,以及如何使用合适的误差指标进行模型验证。通过反复执行这两项任务,提升模型性能。
但是,为什么评估模型很重要呢?假设你已经训练好了模型,提供了一些超参数,做出了预测并找到了准确率。这就是其核心内容,但如何确保你的模型发挥出了最佳能力呢?我们需要确保你所制定的性能评估标准实际上能够代表模型,并且模型在未见过的测试数据集上也能够表现良好。
确保模型达到最佳状态的关键部分出现在初始训练之后:即评估和提升模型性能的过程。本章将引导你了解这一过程中所需的基本技术。
在本章中,我们将首先讨论为什么模型评估如此重要,并介绍几种回归任务和分类任务的评估指标,这些指标可以用来量化模型的预测性能。接下来,我们将讨论持出数据集和 k 折交叉验证,并解释为什么测试集必须独立于验证集。
在此之后,我们将讨论可以用来提高模型表现的策略。在上一章中,我们谈到了如何一个具有高偏差或高方差的模型会导致表现不佳,以及如何通过构建集成模型来帮助我们建立一个更加稳健、更加准确的系统,而不增加整体方差。我们还提到了一些避免过拟合训练数据的技巧:
-
获取更多数据:一个复杂的模型可能很容易在小数据集上过拟合,但在更大的数据集上却可能不容易过拟合。
-
降维:减少特征的数量有助于使模型变得不那么复杂。
-
正则化:在代价函数中添加一个新项,以便调整系数(尤其是线性回归中的高阶系数)使其趋向于较小的值。
在本章中,我们将介绍学习曲线和验证曲线,作为查看训练误差和验证误差变化的方式,以帮助我们了解模型是否需要更多的数据,并找到合适的复杂度水平。接下来将介绍超参数调优,以提升模型表现,并简要介绍特征重要性。
练习 49:导入模块并准备我们的数据集
在本练习中,我们将加载在第五章(集成建模)中训练的数据和模型。我们将使用活动 14: 使用独立和集成算法进行堆叠中的堆叠线性回归模型,以及练习 45: 使用随机森林构建集成模型中的随机森林分类模型来预测乘客的生存情况:
-
导入相关的库:
import pandas as pd import numpy as np import pickle %matplotlib inline import matplotlib.pyplot as plt
-
从第五章(集成建模)加载处理后的数据文件。我们将使用 pandas 的
read_csv()
方法读取准备好的数据集,并在本章练习中使用它们。首先,我们将读取房价数据:house_prices_reg = pd.read_csv('houseprices_regression.csv') house_prices_reg.head()
我们将看到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_02.jpg
图 6.2: 房价数据的前五行
接下来,我们将读取泰坦尼克号的数据:
titanic_clf = pd.read_csv('titanic_classification.csv') titanic_clf.head()
我们将看到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_03.jpg
图 6.3: 泰坦尼克号数据的前五行
-
接下来,使用
pickle
库从二进制文件中加载我们将在本章练习中使用的模型文件:with open('../Saved Models/titanic_regression.pkl', 'rb') as f: reg = pickle.load(f) with open('../Saved Models/random_forest_clf.pkl', 'rb') as f: rf = pickle.load(f)
让我们开始吧。
评估指标
评估机器学习模型是任何项目中的关键部分:一旦我们让模型从训练数据中学习,下一步就是衡量模型的表现。我们需要找到一种度量标准,不仅能告诉我们模型的预测准确度,还能让我们比较多个模型的表现,从而选择最适合我们用例的模型。
定义度量标准通常是我们在定义问题陈述和开始进行探索性数据分析(EDA)之前要做的第一件事,因为提前规划并思考我们打算如何评估构建的任何模型的性能以及如何判断模型是否达到最佳表现是个好主意。最终,计算性能评估度量将纳入机器学习管道中。
不用说,回归任务和分类任务的评估度量是不同的,因为前者的输出值是连续的,而后者的输出值是分类的。在这一部分,我们将探讨可以用来量化模型预测性能的不同度量标准。
回归
对于输入变量X,回归模型给出一个预测值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq1.png,该值可以取一系列不同的值。理想的情况是模型能够预测出尽可能接近实际值y的https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq11.png值。因此,两个值之间的差距越小,模型的表现就越好。回归度量通常涉及查看每个数据点的预测值与实际值之间的数值差异(即残差或误差值),然后以某种方式聚合这些差异。
我们来看一下下图,它绘制了每个点X的实际值和预测值:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_04.jpg
图 6.4:线性回归问题中的实际值与预测值之间的残差
然而,我们不能仅仅对所有数据点的https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq2.png的均值进行计算,因为可能存在某些数据点,其预测误差为正或负,最终的总和将抵消掉许多误差,并严重高估模型的性能。
相反,我们可以考虑每个数据点的绝对误差,并计算平均绝对误差(MAE),其公式如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_05.jpg
图 6.5:平均绝对误差
在这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq3.png和https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq4.png分别是第i个数据点的实际值和预测值。
MAE 是一个线性评分函数,意味着在聚合误差时,它给每个残差赋予相等的权重。MAE 的值可以从零到无穷大,并且不关心误差的方向(正误差或负误差)。由于这些是误差度量,通常希望其值越低(越接近零越好)。
为了避免误差方向影响性能评估,我们还可以对误差项进行平方处理。对平方误差取平均值即可得到均方误差(MSE):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_06.jpg
图 6.6:均方误差
虽然 MAE 的单位与目标变量 y 相同,但 MSE 的单位将是 y 的平方单位,这可能使得在实际应用中判断 MSE 变得稍微不太直观。然而,如果我们对 MSE 取平方根,就能得到 均方根误差(RMSE):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_07.jpg
图 6.7:均方根误差
由于在计算平均值之前对误差进行了平方处理,哪怕只有少数几个误差值很高,也会导致 RMSE 值显著增大。这意味着在我们希望惩罚大误差的模型中,RMSE 比 MAE 更有用。
由于 MAE 和 RMSE 的单位与目标变量相同,因此判断 MAE 或 RMSE 的某个特定值好坏可能很困难,因为没有参考的标准。为了解决这个问题,常用的指标是 R² 分数,也叫 R 平方分数:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_08.jpg
图 6.8:R 平方分数
R2 分数的下限为 -∞,上限为 1。基础模型预测目标变量等于训练数据集中目标值的均值,即,对于所有 i 的值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq41.png 等于 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq5.png。考虑到这一点,R2 的负值表示训练模型的预测结果比均值还要差,而接近 1 的值表示模型的均方误差(MSE)接近零时的情况。
练习 50:回归指标
在本次练习中,我们将使用在 第五章,集成建模 中的 活动 14:使用独立和集成算法进行堆叠 训练过的相同模型和处理过的数据集,来计算回归指标。我们将使用 scikit-learn 实现的 MAE 和 MSE:
-
导入度量函数:
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from math import sqrt
-
使用加载的模型对给定数据进行预测。我们将使用与 第五章,集成建模 中 活动 14:使用独立和集成算法进行堆叠 相同的特征,使用该模型对加载的数据集进行预测。我们保存的 y 列是目标变量,我们将相应地创建 X 和 y:
X = house_prices_reg.drop(columns=['y']) y = house_prices_reg['y'].values y_pred = reg.predict(X)
-
计算 MAE、RMSE 和 R2 分数。我们将打印预测值的 MAE 和 RMSE 值,并打印模型的 R2 分数:
print('Mean Absolute Error = {}'.format(mean_absolute_error(y, y_pred))) print('Root Mean Squared Error = {}'.format(sqrt(mean_squared_error(y, y_pred)))) print('R Squared Score = {}'.format(r2_score(y, y_pred)))
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_09.jpg
图 6.9:分数
我们可以看到 RMSE 明显高于 MAE。这表明某些数据点的残差特别大,这在较大的 RMSE 值中得到了突出表现。但 R2 分数接近 1,说明该模型相比于基础模型(基础模型预测的是均值)表现得几乎理想。
分类
对于一个输入变量X,分类任务给出了一个预测值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_Eq12.png,它可以取有限的几个值(在二分类问题中为两个值)。由于理想的情况是预测每个数据点的类别与实际类别相同,因此没有衡量预测类别与实际类别之间距离的指标。因此,要评判模型的表现,简单的方法就是判断模型是否正确地预测了类别。
判断分类模型表现的方法有两种:使用数值指标,或通过绘制曲线并观察曲线的形状。让我们更详细地探讨这两种方法。
数值指标
判断模型表现最简单且基本的方法是计算正确预测占总预测数的比例,这给出了准确率:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_10.jpg
图 6.10:准确率
尽管准确率指标适用于任何类别数量的情况,但接下来的几个指标将以二分类问题为背景进行讨论。此外,准确率在许多情况下可能不是评估分类任务表现的最佳指标。
让我们看一个欺诈检测的例子:假设问题是检测一封邮件是否欺诈。在这种情况下,我们的数据集高度倾斜(或不平衡,也就是说,一类数据点的数量远大于另一类数据点),在 10,000 封邮件中有 100 封(总数的 1%)被分类为欺诈(属于类别 1)。假设我们构建了两个模型:
-
第一个模型简单地将每封邮件预测为非欺诈,也就是说,10,000 封邮件中的每一封都被归类为类别 0。在这种情况下,10,000 封邮件中有 9,900 封被正确分类,这意味着该模型的准确率为 99%。
-
第二个模型将 100 封欺诈邮件预测为欺诈,但同时也错误地将另外 100 封邮件预测为欺诈。在这种情况下,同样有 100 个数据点在 10,000 封邮件中被误分类,模型的准确率为 99%。
我们如何比较这两种模型?构建欺诈检测模型的目的是让我们了解欺诈检测的效果:比起非欺诈邮件被误分类为欺诈邮件,正确分类欺诈邮件更为重要。尽管这两个模型的准确率相同,但第二个模型实际上比第一个更有效。
由于无法仅通过准确率捕获这一点,我们需要混淆矩阵,它是一个包含四种不同的预测值和实际值组合的表格,本质上为我们提供了分类问题预测结果的总结:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_11.jpg
图 6.11:混淆矩阵
下面是矩阵中使用的术语的含义:
-
真正例和真负例:这些是分别在正类和负类中被正确预测的数据点数量。
-
假正例:也称为类型 1 错误,指的是实际上属于负类但被预测为正类的数据点数量。从前面的例子继续,如果一个正常的邮件被分类为欺诈邮件,则为假正例。
-
假负例:也称为类型 2 错误,指的是实际上属于正类但被预测为负类的数据点数量。假负例的例子是,如果一封欺诈邮件被分类为非欺诈邮件。
从混淆矩阵中可以推导出两个极其重要的指标:精度和召回率。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_12.jpg
图 6.12: 精度
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_13.jpg
图 6.13: 召回率
精度告诉我们有多少实际的正例被正确地预测为正例(从模型认为相关的结果中,有多少实际是相关的?),而召回率告诉我们有多少预测为正例的结果实际上是正例(从真实的相关结果中,有多少被模型列入相关结果列表?)。这两个指标在类不平衡时尤其有用。
模型的精度和召回率通常存在权衡:如果必须召回所有相关的结果,模型将生成更多不准确的结果,从而降低精度。另一方面,要确保生成的结果中有更高比例的相关结果,就需要尽量少生成结果。大多数情况下,你会优先考虑精度或召回率,这完全取决于问题的具体要求。例如,由于确保所有欺诈性邮件被正确分类更为重要,因此召回率将是一个需要最大化的关键指标。
接下来的问题是,如何使用一个单一的数值来评估模型,综合考虑精度和召回率,而不是单独平衡这两个指标。F1 分数将两者合并成一个单一的数值,这个数值可以作为模型的公正评判标准,并且等于精度和召回率的调和平均值:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_14.jpg
图 6.14: F1 分数
F1 分数的值总是介于 0(如果精度或召回率为零)和 1(如果精度和召回率都为 1)之间。分数越高,说明模型的性能越好。F1 分数对两个指标赋予相等的权重,并且是一般 Fβ 指标的一个特例,其中 β 可以调整,以便根据以下公式为召回率或精度赋予更多权重:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_15.jpg
图 6.15: F β 值
β < 1 时,更注重精确度,而 β > 1 时,更注重召回率。F1 分数采用 β = 1,使两者权重相等。
曲线图
有时,我们不是预测类别,而是利用类别的概率值。举例来说,在一个二分类任务中,正类(类 1)和负类(类 0)的类别概率之和始终为 1(或 统一的 1),这意味着如果我们将分类概率视为类 1 的概率,并应用一个阈值,我们可以本质上将其作为一个截止值,来进行四舍五入(为 1)或下舍(为 0),从而得到输出的类别。
通常,通过改变阈值,我们可以得到分类概率接近 0.5 的数据点,这些数据点从一个类别转到另一个类别。例如,当阈值为 0.5 时,具有 0.4 概率的数据点会被分配为类 0,而具有 0.6 概率的数据点会被分配为类 1。但如果我们将阈值改为 0.35 或 0.65,这两个数据点都会被分类为 1 或 0。
事实证明,改变概率会改变精确度和召回率的值,这可以通过绘制精确度-召回率曲线来捕捉。图表的Y 轴表示精确度,X 轴表示召回率,对于从 0 到 1 的一系列阈值,图表绘制每一个(召回率,精确度)点。连接这些点便得到曲线。以下图显示了一个例子:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_16.jpg
图 6.16:精确度-召回率曲线
我们知道,在理想情况下,精确度和召回率的值将为 1。这意味着,当阈值从 0 增加到 1 时,精确度将保持为 1,但召回率会从 0 增加到 1,因为越来越多(相关的)数据点将被正确分类。因此,在理想情况下,精确度-召回率曲线基本上将是一个正方形,且曲线下面积(AUC)将等于 1。
因此,我们可以看到,和 F1 分数一样,AUC 是另一个从精确度和召回率行为中得出的指标,它结合了精确度和召回率的值来评估模型的性能。我们希望模型的 AUC 尽可能高,接近 1。
显示分类模型性能的另一个主要可视化技术是接收者操作特征(ROC)曲线。ROC 曲线绘制了真正例率(TPR)在Y 轴上的关系,以及假正例率(FPR)在X 轴上的关系,随着分类概率阈值的变化。TPR 恰好等于召回率(也称为模型的灵敏度),而 FPR 是特异性的补集(即 1 - FPR = 灵敏度);这两者都可以通过混淆矩阵使用以下公式推导:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_17.jpg
图 6.17:真正例率
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_18.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_18.jpg)
图 6.18:假阳性率
下图展示了一个 ROC 曲线的示例,其绘制方式与精度-召回曲线相同:通过改变概率阈值,使得曲线上的每个点代表一个*(TPR, FPR)*数据点,对应于一个特定的概率阈值。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_19.jpg
图 6.19:ROC 曲线
当类别比较平衡时,ROC 曲线更为有用,因为它们往往会在类别不平衡的数据集上呈现过于乐观的模型表现,尤其是在 ROC 曲线中的假阳性率使用了真正负例(而精度-召回曲线中没有此项)。
练习 51:分类指标
在本练习中,我们将使用在第五章《集成建模》中训练的随机森林模型,并使用其预测生成混淆矩阵,计算精度、召回率和 F1 得分,以此来评估我们的模型。我们将使用 scikit-learn 的实现来计算这些指标:
-
导入相关的库和函数:
from sklearn.metrics import (accuracy_score, confusion_matrix, precision_score, recall_score, f1_score)
-
使用模型对所有数据点进行类别预测。我们将使用与之前相同的特征,并使用随机森林分类器对加载的数据集进行预测。scikit-learn 中的每个分类器都有一个
.predict_proba()
函数,我们将在这里使用它,并结合标准的.predict()
函数来分别提供类别概率和预测的类别:X = titanic_clf.iloc[:, :-1] y = titanic_clf.iloc[:, -1] y_pred = rf.predict(X) y_pred_probs = rf.predict_proba(X)
-
计算准确率:
print('Accuracy Score = {}'.format(accuracy_score(y, y_pred)))
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_20.jpg
图 6.20:准确率得分
-
打印混淆矩阵:
print(confusion_matrix(y_pred=y_pred, y_true=y))
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_21.jpg
图 6.21:混淆矩阵
在这里,我们可以看到模型似乎有较高的假阴性数量,这意味着我们可以预期该模型的召回率将非常低。类似地,由于假阳性的数量仅为一个,我们可以预期模型将具有较高的精度。
-
计算精度和召回率:
print('Precision Score = {}'.format(precision_score(y, y_pred))) print('Recall Score = {}'.format(recall_score(y, y_pred)))
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_22.jpg
图 6.22:精度和召回得分
-
计算 F1 得分:
print('F1 Score = {}'.format(f1_score(y, y_pred)))
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_23.jpg
图 6.23:F1 得分
我们可以看到,由于召回率极低,这也影响了 F1 得分,使其接近零。
现在我们已经讨论了可以用来衡量模型预测性能的指标,让我们谈谈验证策略,我们将使用这些指标来评估模型在不同情况下的表现。
数据集划分
在评估模型表现时,一个常见的错误是计算模型在训练数据上的预测误差,并基于训练数据集上的高预测准确率得出模型表现良好的结论。
这意味着我们正在尝试在模型已经见过的数据上进行测试,也就是说,模型已经学到了训练数据的行为,因为它曾经接触过这些数据——如果要求模型再次预测训练数据的行为,它无疑会表现得很好。而且,模型在训练数据上的表现越好,就越有可能意味着模型对数据了解得过于透彻,甚至学会了数据中的噪声和异常值的行为。
现在,高训练准确度会导致模型具有高方差,正如我们在前一章中看到的那样。为了获得模型性能的无偏估计,我们需要找出它在训练过程中没有接触过的数据上的预测准确度。这时,留出数据集就显得非常重要。
留出数据
留出数据集是指从训练数据中剥离出的样本,这部分数据在训练过程中未被模型接触,因此它对模型来说是未见过的。由于噪声是随机的,留出数据点很可能包含异常值和噪声数据,这些数据的行为与训练数据集中的数据有所不同。因此,在留出数据集上计算模型的性能,可以帮助我们验证模型是否过拟合,并为我们提供对模型性能的无偏视角。
我们在上一章开始时将泰坦尼克号数据集拆分为训练集和验证集。那么,什么是验证数据集,它与测试数据集有何不同呢?我们经常看到“验证集”和“测试集”这两个术语互换使用——虽然它们都指代留出数据集,但在目的上存在一些差异:
-
验证数据:在模型从训练数据中学习后,会在验证数据集上评估其性能。然而,为了让模型发挥最佳表现,我们需要对模型进行微调,并反复迭代评估更新后的模型性能,这一过程是在验证数据集上进行的。通常,表现最好的微调版本模型会被选为最终模型。
因此,模型在每次改进的迭代过程中都会接触到验证数据集,尽管本质上并没有从数据中学习。可以说,验证集间接地影响了模型。
-
测试数据:选择的最终模型现在在测试数据集上进行评估。在该数据集上测得的性能将是一个无偏的度量,作为模型的最终性能指标。这一最终评估是在模型已经在合并后的训练集和验证集上完全训练后进行的。此后,模型不再进行训练或更新。
这意味着模型仅在计算最终性能指标时暴露于测试数据集一次。
应当记住,验证数据集绝不应被用来评估模型的最终性能:如果模型已经看到并被修改以特定提高在验证集上的表现,那么我们对模型真实性能的估计会存在正偏。
然而,单一的保留验证数据集确实存在一些局限性:
-
由于模型在每次改进的迭代中只进行一次验证,因此使用这个单一评估可能难以捕捉预测中的不确定性。
-
将数据划分为训练集和验证集会减少训练模型时使用的数据量,这可能导致模型具有较高的方差。
-
最终模型可能会过拟合此验证集,因为它是根据此数据集的最大性能进行调优的。
如果我们使用称为 K 折交叉验证的验证技术,而不是仅使用单一验证数据集,这些挑战是可以克服的。
K 折交叉验证
K 折交叉验证是一种验证技术,它通过在k折中轮换验证集,帮助我们得到模型性能的无偏估计。其工作原理如下:
-
首先,我们选择k的值,并将数据划分为k个子集。
-
然后,我们将第一子集作为验证集,剩余的数据用于训练模型。
-
我们在验证子集上衡量模型的性能。
-
然后,我们将第二子集作为验证子集,并重复这一过程。
-
一旦我们完成这k次操作后,我们将所有折叠的性能度量值聚合,并呈现最终指标。
下图直观地解释了这一过程:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_24.jpg
图 6.24:K 折交叉验证
尽管这种验证方法计算开销较大,但其优点超过了成本。这种方法确保模型在训练数据集中的每个示例上都得到验证一次,且最终得到的性能估计不偏向于验证集,尤其是在数据集较小的情况下。一个特例是留一法交叉验证,其中k的值等于数据点的数量。
采样
现在我们已经了解了用于划分数据集以进行模型训练和验证的策略,让我们讨论如何将数据点分配到这些划分中。我们可以通过两种方式对数据进行采样,这两种方式如下:
-
随机采样:这就是将整体数据集中的随机样本分配到训练集、验证集和/或测试集的过程。随机划分数据只有在所有数据点彼此独立时才有效。例如,如果数据是时间序列形式,随机划分就不适用,因为数据点是有序的,每个数据点都依赖于前一个数据点。随机划分数据会破坏这个顺序,忽视这种依赖关系。
-
分层采样:这是一种确保每个子集的目标变量的分布与原始数据集相同的方法。例如,如果原始数据集的两个类别的比例是 3:7,那么分层采样确保每个子集中的两个类别的比例也是 3:7。
分层采样很重要,因为在数据集的目标值分布与训练模型时的数据集不同的情况下,测试模型可能会得到一个无法代表模型实际性能的结果估计。
训练集、验证集和测试集的样本大小在模型评估过程中也起着重要作用。将一个较大的数据集留作最终模型性能测试,可以帮助我们获得对模型性能的无偏估计,并减少预测的方差,但如果测试集过大,以至于由于缺少训练数据影响了模型的训练能力,这将严重影响模型的表现。这个考虑特别适用于较小的数据集。
练习 52:使用分层采样的 K 折交叉验证
在这个练习中,我们将实现基于分层采样的 K 折交叉验证,使用 scikit-learn 的随机森林分类器。scikit-learn 中的 StratifiedKFold
类实现了交叉验证和采样的结合,我们将在练习中使用它:
-
导入相关的类。我们将导入 scikit-learn 的
StratifiedKFold
类,这是KFold
的一种变体,返回分层折叠,同时导入RandomForestClassifier
:from sklearn.model_selection import StratifiedKFold from sklearn.ensemble import RandomForestClassifier
-
为训练准备数据并初始化 k 折交叉验证对象。在这里,我们将使用五个折叠来评估模型,因此将
n_splits
参数设置为5
:X = titanic_clf.iloc[:, :-1].values y = titanic_clf.iloc[:, -1].values skf = StratifiedKFold(n_splits=5)
-
对每个折叠训练一个分类器并记录得分。
StratifiedKFold
类的功能类似于我们在上一章中使用的KFold
类,练习 48:构建堆叠模型:对于五个折叠中的每一个,我们将在其他四个折叠上进行训练,并在第五个折叠上进行预测,找到第五个折叠上的准确率得分。正如我们在上一章中看到的,skf.split()
函数以数据集为输入,返回一个迭代器,包含用于划分训练数据进行训练和验证的每行索引值:scores = [] for train_index, val_index in skf.split(X, y): X_train, X_val = X[train_index], X[val_index] y_train, y_val = y[train_index], y[val_index] rf_skf = RandomForestClassifier(**rf.get_params()) rf_skf.fit(X_train, y_train) y_pred = rf_skf.predict(X_val) scores.append(accuracy_score(y_val, y_pred)) print(scores)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_25.jpg
图 6.25:使用随机森林分类器的得分
-
打印汇总的准确率得分:
print('Mean Accuracy Score = {}'.format(np.mean(scores)))
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_26.jpg
图 6.26:平均准确率得分
性能提升策略
监督式机器学习模型的性能提升是一个迭代过程,通常需要持续更新和评估周期才能得到完美的模型。虽然本章前面的部分讨论了评估策略,本节将讨论模型更新:我们将探讨如何确定模型所需的性能提升,并如何在模型中做出这些改变。
训练误差和测试误差的变化
在上一章中,我们介绍了欠拟合和过拟合的概念,并提到了几种克服它们的方法,随后介绍了集成模型。但是我们没有讨论如何识别我们的模型是否出现了欠拟合或过拟合的情况。
通常来说,查看学习曲线和验证曲线是很有用的。
学习曲线
学习曲线展示了随着训练数据量的增加,训练误差和验证误差的变化。通过观察曲线的形状,我们可以大致判断增加更多数据是否有利于建模,并可能改善模型的表现。
让我们来看一下以下图表:虚线曲线表示验证误差,实线曲线表示训练误差。左侧的图表显示这两条曲线趋向于一个相对较高的误差值。这意味着模型存在较高的偏差,增加更多的数据很可能不会对模型的表现产生影响。因此,与其浪费时间和金钱去收集更多的数据,我们只需要增加模型的复杂度。
另一方面,右侧的图表显示,即使训练集中的数据点数量增加,训练误差和测试误差之间的差距依然很大。这个宽广的差距表示系统的方差很高,也意味着模型过拟合。在这种情况下,增加更多的数据点可能有助于模型更好地进行泛化:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_27.jpg
图 6.27:数据量增加的学习曲线
但是,我们如何识别完美的学习曲线呢?当我们有一个低偏差和低方差的模型时,我们会看到像下面图示那样的曲线。它展示了低训练误差(低偏差),以及当验证曲线和训练曲线汇聚时,二者之间的低差距(低方差)。在实际操作中,我们能看到的最好的学习曲线是那些趋于某个不可减少的误差值的曲线(该误差值由于数据集中的噪声和异常值而存在):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_28.jpg
图 6.28: 低偏差和低方差模型随着训练数据量增大,训练和验证误差的变化
验证曲线
正如我们之前讨论的,机器学习模型的目标是能够推广到未见过的数据。验证曲线可以帮助我们找到一个欠拟合和过拟合模型之间的理想点,在这个点上,模型能够良好地进行推广。在上一章中,我们讨论了模型复杂度如何影响预测性能:我们说,当我们从一个过于简单的模型走向一个过于复杂的模型时,我们会从一个具有高偏差和低方差的欠拟合模型,过渡到一个具有低偏差和高方差的过拟合模型。
验证曲线显示了随着模型参数值变化,训练误差和验证误差的变化,这些模型参数在某种程度上控制着模型的复杂度——这可能是线性回归中的多项式的次数,或者是决策树分类器的深度。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_29.jpg
图 6.29: 随着模型复杂度增加,训练和验证的变化
上面的图展示了随着模型复杂度变化(模型参数是其指示器之一),验证误差和训练误差如何变化。我们还可以看到,在阴影区域之间的某个点是总误差最小的地方,这个点位于欠拟合和过拟合之间的甜蜜点。找到这个点有助于我们找到理想的模型参数值,从而建立一个既具有低偏差又具有低方差的模型。
超参数调优
我们之前多次谈到超参数调优,现在让我们讨论一下它为什么如此重要。首先,需要注意的是,模型参数与模型超参数是不同的:前者是模型内部的,并且是从数据中学习得到的,而后者则定义了模型本身的架构。
一些超参数的示例如下:
-
用于线性回归模型的多项式特征的次数
-
允许的决策树分类器的最大深度
-
随机森林分类器中包含的树木数量
-
用于梯度下降算法的学习率
定义模型架构的设计选择可以显著影响模型的性能。通常,超参数的默认值可以工作,但是获取超参数的完美组合可以真正提升模型的预测能力,因为默认值可能完全不适合我们要建模的问题。在下图中,我们可以看到调整两个超参数值如何导致模型分数的巨大差异:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_30.jpg
图 6.30:模型分数(Z 轴)随两个模型参数值(X 和 Y 轴)变化的情况
通过探索一系列可能的值来找到完美组合,这就是所谓的超参数调优。由于没有损失函数可用于最大化模型性能,调整超参数通常只涉及尝试不同组合并选择在验证期间表现最佳的组合。
有几种方式可以调整我们模型的超参数:
-
手动调整:当我们手动选择超参数的值时,这被称为手动调整。这通常是低效的,因为通过手动解决高维优化问题不仅可能很慢,而且也不允许模型达到其性能峰值,因为我们可能不会尝试每个超参数值的所有组合。
-
网格搜索:网格搜索涉及对提供的超参数值的每一组合进行训练和评估,并选择产生最佳性能模型的组合。由于这涉及对超参数空间进行详尽采样,因此计算成本相当高,效率低下。
-
随机搜索:尽管第一种方法因尝试的组合过少而被认为效率低下,但第二种方法因尝试的组合过多而被认为效率低下。随机搜索旨在通过从网格中选择超参数组合的随机子集,并仅为这些组合训练和评估模型来解决这个问题。或者,我们还可以为每个超参数提供统计分布,从中随机抽样值。
随机搜索的逻辑已被 Bergstra 和 Bengio 证明:如果网格上至少有 5%的点产生接近最优解,那么进行 60 次随机搜索将高概率找到该区域。
注
您可以阅读 Bergstra 和 Bengio 的论文,网址为
www.jmlr.org/papers/v13/bergstra12a.html
。 -
贝叶斯优化:前两种方法涉及独立地实验不同超参数值的组合,并记录每个组合的模型性能。然而,贝叶斯优化是顺序地迭代实验,并允许我们利用先前实验的结果来改进下一个实验的采样方法。
练习 53:使用随机搜索进行超参数调优
使用 scikit-learn 的RandomizedSearchCV
方法,我们可以定义一个超参数范围的网格,并从网格中随机采样,使用每个超参数值组合执行 K 折交叉验证。在这个练习中,我们将使用随机搜索方法进行超参数调优:
-
导入随机搜索类:
from sklearn.model_selection import RandomizedSearchCV
-
为训练准备数据并初始化分类器。在这里,我们将初始化随机森林分类器,而不传递任何参数,因为这只是一个基础对象,稍后将在每个网格点上进行实例化并执行随机搜索:
X = titanic_clf.iloc[:, :-1].values y = titanic_clf.iloc[:, -1].values rf_rand = RandomForestClassifier()
-
指定需要采样的参数。在这里,我们将列出每个超参数的不同值,这些值将用于网格中:
param_dist = {"n_estimators": list(range(10,210,10)), "max_depth": list(range(3,20)), "max_features": list(range(1, 10)), "min_samples_split": list(range(2, 11)), "bootstrap": [True, False], "criterion": ["gini", "entropy"]}
-
运行随机搜索。我们用希望运行的试验总数、参数值字典、评分函数以及 K 折交叉验证中的折数来初始化随机搜索对象。然后,我们调用
.fit()
函数来执行搜索:n_iter_search = 60 random_search = RandomizedSearchCV(rf_rand, param_distributions=param_dist, scoring='accuracy', n_iter=n_iter_search, cv=5) random_search.fit(X, y)
-
打印前五个模型的评分和超参数。将
results
字典转换为 pandas DataFrame,并按rank_test_score
对值进行排序。然后,对于前五行,打印排名、平均验证得分和超参数:results = pd.DataFrame(random_search.cv_results_).sort_values('rank_test_score') for i, row in results.head().iterrows(): print("Model rank: {}".format(row.rank_test_score)) print("Mean validation score: {:.3f} (std: {:.3f})".format(row.mean_test_score, row.std_test_score)) print("Model Hyperparameters: {}\n".format(row.params))
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_31.jpg
图 6.31:前五个模型的评分和超参数
我们可以看到,表现最好的模型只有 70 棵树,而排名 2 到 4 的模型有 160 棵以上的树。此外,排名第 5 的模型只有 10 棵树,但其性能仍然与更复杂的模型相当。
特征重要性
虽然关注模型性能至关重要,但理解模型中各特征如何贡献于预测同样重要:
-
我们需要能够解释模型以及不同变量如何影响预测,以便向相关利益相关者说明为什么我们的模型成功。
-
数据可能存在偏差,在这些数据上训练模型可能会影响模型的性能,并导致模型评估出现偏差,在这种情况下,通过查找重要特征并分析它们来解释模型的能力将有助于调试模型的表现。
-
除了前面提到的点之外,还必须注意,某些模型偏差可能在社会或法律上是不可接受的。例如,如果一个模型表现良好,因为它隐含地对基于种族的特征赋予了较高的重要性,这可能会引发问题。
除了这些要点,找出特征重要性还可以帮助特征选择。如果数据具有高维度,并且训练的模型具有高方差,那么删除那些重要性较低的特征是一种通过降维来降低方差的方法。
练习 54:使用随机森林计算特征重要性
在这个练习中,我们将从我们之前加载的随机森林模型中找出特征的重要性:
-
找出特征重要性。让我们找出特征的重要性,并将其保存在一个 pandas DataFrame 中,索引为列名,并按降序排列这个 DataFrame:
feat_imps = pd.DataFrame({'importance': rf.feature_importances_}, index=titanic_clf.columns[:-1]) feat_imps.sort_values(by='importance', ascending=False, inplace=True)
-
将特征重要性绘制为条形图:
feat_imps.plot(kind='bar', figsize=(10,7)) plt.legend() plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_06_32.jpg
图 6.32:特征直方图
在这里,我们可以看到Sex
、Fare
和Pclass
特征似乎具有最高的重要性,也就是说,它们对目标变量的影响最大。
活动 15:最终测试项目
在这个活动中,我们将使用IBM HR Analytics 员工流失与绩效数据集(可在www.kaggle.com/pavansubhasht/ibm-hr-analytics-attrition-dataset
找到),以及相关的源代码(见github.com/TrainingByPackt/Supervised-Learning-with-Python
)来解决一个分类问题,在这个问题中,我们需要预测员工是否会离职。针对员工流失问题,我们的目标是最大化召回率,即我们希望能够识别所有即将离职的员工,即使这意味着预测一些表现良好的员工也会离职:这将帮助 HR 对这些员工采取适当的措施,防止他们离开。
数据集中的每一行代表一个员工,目标变量是Attrition
,它有两个值:1
和0
,分别表示该员工是否离职,是和否。我们将使用来自 scikit-learn 的梯度提升分类器来训练模型。这个活动是作为一个最终项目,旨在帮助巩固本书以及本章所学的概念的实际应用。
我们将通过使用交叉验证的随机搜索来找到模型的最优超参数。然后,我们将在数据集的一部分上使用梯度提升算法构建最终的分类器,并使用我们学到的分类指标评估其在数据集剩余部分的表现。我们将使用平均绝对误差作为此次活动的评估指标。
需要执行的步骤如下:
-
导入相关的库。
-
读取
attrition_train.csv
数据集。 -
读取
categorical_variable_values.json
文件,该文件包含了分类变量的详细信息。 -
处理数据集,将所有特征转换为数值型。
-
选择基础模型,并定义与模型对应的超参数值范围,以进行超参数调优。
-
定义初始化
RandomizedSearchCV
对象的参数,并使用 K 折交叉验证来找到最佳模型超参数。 -
将数据集划分为训练集和验证集,并使用最终超参数在训练集上训练新模型。
-
计算在验证集上的预测精度、精确度和召回率,并打印混淆矩阵。
-
尝试调整不同的阈值,找到具有高召回率的最佳点。绘制精确度-召回率曲线。
-
确定用于预测测试数据集的最终阈值。
-
读取并处理测试数据集,将所有特征转换为数值型。
-
在测试数据集上预测最终值。
注意
本活动的解决方案可以在第 373 页找到。
总结
本章讨论了模型评估在监督学习中的重要性,并介绍了几种用于评估回归和分类任务的重要指标。我们看到,虽然回归模型的评估相对简单,但分类模型的性能可以通过多种方式进行衡量,具体取决于我们希望模型优先考虑的内容。除了数值指标,我们还探讨了如何绘制精确度-召回率曲线和 ROC 曲线,以更好地解读和评估模型性能。
之后,我们讨论了为什么通过计算模型在其训练数据上的预测误差来评估模型是一个不好的主意,以及如何在模型已经见过的数据上进行测试会导致模型具有高方差。通过这一点,我们引入了保持集数据集的概念,并解释了 K 折交叉验证为何是一个有用的策略,以及确保模型训练和评估过程保持无偏的采样技术。
性能改进策略的最后一节从学习曲线和验证曲线的讨论开始,探讨了如何解读这些曲线来推动模型开发过程,最终提高模型性能。随后,我们介绍了超参数调优作为提升性能的一种方法,并简要介绍了特征重要性。