Python 真实世界的数据科学(十一)
三十九、通过降维压缩数据
在第 4 章和“建立良好的训练集–数据预处理”中,您了解了使用不同的特征选择技术来降低数据集维数的不同方法。 用于降维的特征选择的替代方法是特征提取。 在本章中,您将学习三种基本技术,这些技术可以帮助我们通过将数据集转换为维数比原始维数低的新特征子空间来总结数据集的信息内容。 数据压缩是机器学习中的一个重要主题,它有助于我们存储和分析在现代技术时代生成和收集的不断增长的数据量。 在本章中,我们将介绍以下主题:
- 用于无监督数据压缩的主成分分析(PCA)
- 线性判别分析(LDA)作为一种监督降维技术,可最大限度地提高类别可分离性
- 通过核主成分分析进行非线性降维
通过主成分分析进行无监督的降维
与特征选择类似,我们可以使用特征提取来减少数据集中的特征数量。 但是,虽然我们在使用特征选择算法(例如顺序向后选择)时保留了原始特征,但是我们使用特征提取将数据转换或投影到新的特征空间上。 在降维的情况下,特征提取可以理解为一种数据压缩的方法,其目的是保留大多数相关信息。 特征提取通常用于提高计算效率,但也可以帮助减少维度的诅咒-特别是在我们使用非正规模型的情况下。
主成分分析(PCA)是无监督线性变换技术,已广泛应用于不同领域,最显着的是用于降维。 PCA 的其他流行应用包括在股票市场交易中进行探索性数据分析和信号去噪,以及在生物信息学领域分析基因组数据和基因表达水平。 PCA 帮助我们根据特征之间的相关性来识别数据中的模式。 简而言之,PCA 旨在在高维数据中找到最大方差的方向,并将其投影到尺寸等于或小于原始维的新子空间中。 鉴于新特征轴彼此正交的约束,新子空间的正交轴(主分量)可以解释为最大方差方向,如下图所示。 这里,x[1]和x[2]是原始特征轴,PC1 和 PC2 是主要组成部分:
如果我们使用 PCA 进行降维,则会构建一个d × k-维变换矩阵W,该矩阵允许我们将样本向量x映射到一个新的k-维特征子空间,该子空间的维数少于 原始的d-维特征空间:
将原始d维数据转换到这个新的k维子空间(通常为k << d)的结果是,第一个主成分将具有最大的方差,并且所有随后的主成分将具有最大的可能方差 假设它们与其他主成分不相关(正交),则为方差。 请注意,PCA 方向对数据缩放高度敏感,如果在不同尺度上对特征进行测量,并且我们想对所有特征赋予同等的重要性,则我们需要先将特征优先于。
在更详细地研究用于降维的 PCA 算法之前,让我们通过几个简单的步骤来总结该方法:
- 标准化
d维数据集。 - 构造协方差矩阵。
- 将协方差矩阵分解为其特征向量和特征值。
- 选择与
k最大特征值相对应的k特征向量,其中k是新特征子空间(k <= d)的维数。 - 从“顶部”
k特征向量构造投影矩阵W。 - 使用投影矩阵
W变换d维输入数据集X,以获得新的k维特征子空间。
总计和解释方差
在本小节中,我们将处理主成分分析的前四个步骤:标准化数据,构建协方差矩阵,获得协方差矩阵的特征值和特征向量,以及通过将特征值递减排序以 对特征向量进行排名。
首先,我们将从加载第 4 章和“构建良好的训练集–数据预处理”中一直使用的葡萄酒数据集开始:
>>> import pandas as pd
>>> df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)
接下来,我们将 Wine 数据处理为单独的训练和测试集(分别使用 70%和 30%的数据)并将其标准化为单位差异。
>>> from sklearn.cross_validation import train_test_split
>>> from sklearn.preprocessing import StandardScaler
>>> X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
>>> X_train, X_test, y_train, y_test = \
... train_test_split(X, y,
... test_size=0.3, random_state=0)
>>> sc = StandardScaler()
>>> X_train_std = sc.fit_transform(X_train)
>>> X_test_std = sc.transform(X_test)
通过执行前面的代码完成强制性预处理步骤后,让我们进入第二步:构造协方差矩阵。 对称d × d维协方差矩阵(其中d是数据集中的维数)存储不同特征之间的成对协方差。 例如,可以通过以下公式计算总体水平上两个特征x[k]之间的协方差:
在此,μ[j]和μ[k]分别是特征j和k的样本均值。 请注意,如果我们标准化数据集,则样本均值为零。 两个特征之间的正协方差表示特征一起增加或减少,而负协方差则表示特征沿相反的方向变化。 例如,然后可以将三个特征的协方差矩阵写为(请注意Σ代表希腊字母 sigma,请勿与和符号混淆) :
协方差矩阵的特征向量表示主成分(最大方差的方向),而相应的特征值将定义其大小。 对于 Wine 数据集,我们将从13 × 13维协方差矩阵中获得 13 个特征向量和特征值。
现在,让我们获得协方差矩阵的特征对。 正如我们从线性代数或微积分入门课中肯定记得的那样,特征向量v满足以下条件:
在这里,λ是一个标量:特征值。 由于特征向量和特征值的手动计算是一项繁琐且复杂的任务,因此我们将使用 NumPy 的linalg.eig函数来获取 Wine 协方差矩阵的特征对:
>>> import numpy as np
>>> cov_mat = np.cov(X_train_std.T)
>>> eigen_vals, eigen_vecs = np.linalg.eig(cov_mat)
>>> print('\nEigenvalues \n%s' % eigen_vals)
Eigenvalues
[ 4.8923083 2.46635032 1.42809973 1.01233462 0.84906459 0.60181514
0.52251546 0.08414846 0.33051429 0.29595018 0.16831254 0.21432212
0.2399553 ]
使用numpy.cov函数,我们计算了标准化训练数据集的协方差矩阵。 使用linalg.eig函数,我们进行了特征分解,生成了一个向量(eigen_vals),该向量由 13 个特征值组成,并且对应的特征向量作为列存储在13 × 13-维矩阵(eigen_vecs)中。
注意
尽管numpy.linalg.eig函数旨在分解非对称方阵,但您可能会发现在某些情况下它会返回复杂的特征值。
已实现相关函数numpy.linalg.eigh来分解 Hermetian 矩阵,这是在数值上更稳定的方法,可用于处理对称矩阵(例如协方差矩阵); numpy.linalg.eigh始终返回实特征值。
由于我们想通过将压缩到新的特征子空间中来降低数据集的维数,因此,我们仅选择包含大部分信息(方差)的特征向量的子集(主要成分)。 由于特征值定义了特征向量的大小,因此我们必须通过减小大小对特征值进行排序。 我们基于其对应特征值的值对顶部k特征向量感兴趣。 但是在收集那些k信息最多的特征向量之前,让我们绘制特征值的方差解释比率。
特征值λ[j]的方差解释比率只是特征值λ[j]与特征值总和的分数:
使用 NumPy cumsum函数,我们可以计算出解释方差的累积和,我们将通过 matplotlib 的step函数进行绘制:
>>> tot = sum(eigen_vals)
>>> var_exp = [(i / tot) for i in
... sorted(eigen_vals, reverse=True)]
>>> cum_var_exp = np.cumsum(var_exp)
>>> import matplotlib.pyplot as plt
>>> plt.bar(range(1,14), var_exp, alpha=0.5, align='center',
... label='individual explained variance')
>>> plt.step(range(1,14), cum_var_exp, where='mid',
... label='cumulative explained variance')
>>> plt.ylabel('Explained variance ratio')
>>> plt.xlabel('Principal components')
>>> plt.legend(loc='best')
>>> plt.show()
生成的图表明仅第一个主成分占方差的 40%。 此外,我们可以看到前两个主要成分的组合几乎解释了数据中 60%的方差:
尽管解释的方差图使我们想起了功能重要性,但我们在第 4 章,“建立良好的训练集–数据预处理”中通过随机森林计算了, 提醒自己,PCA 是一种不受监督的方法,这意味着有关类标签的信息将被忽略。 随机森林使用类成员关系信息来计算节点杂质,而方差则测量值沿特征轴的分布。
功能转换
在成功将协方差矩阵分解为特征对之后,我们现在进行最后三个步骤,将 Wine 数据集转换到新的主成分轴上。 在本节中,我们将按照特征值的降序对特征对进行排序,从选定的特征向量构建投影矩阵,然后使用该投影矩阵将数据转换到低维子空间上。
我们首先通过降低特征值的顺序对特征对进行排序:
>>> eigen_pairs =[(np.abs(eigen_vals[i]),eigen_vecs[:,i])
... for i inrange(len(eigen_vals))]
>>> eigen_pairs.sort(reverse=True)
接下来,我们收集与两个最大值对应的两个特征向量,以捕获此数据集中约 60%的方差。 请注意,出于说明的目的,我们仅选择了两个特征向量,因为我们将在本小节的后面部分通过二维散点图绘制数据。 实际上,必须根据计算效率和分类器性能之间的折衷来确定主成分的数量:
>>> w= np.hstack((eigen_pairs[0][1][:, np.newaxis],
... eigen_pairs[1][1][:, np.newaxis]))
>>> print('Matrix W:\n',w)
Matrix W:
[[ 0.14669811 0.50417079]
[-0.24224554 0.24216889]
[-0.02993442 0.28698484]
[-0.25519002 -0.06468718]
[ 0.12079772 0.22995385]
[ 0.38934455 0.09363991]
[ 0.42326486 0.01088622]
[-0.30634956 0.01870216]
[ 0.30572219 0.03040352]
[-0.09869191 0.54527081]
[ 0.30032535 -0.27924322]
[ 0.36821154 -0.174365 ]
[ 0.29259713 0.36315461]]
通过执行前面的代码,我们从顶部的两个特征向量创建了13 × 2维投影矩阵W。 使用投影矩阵,我们现在可以将样本x(表示为1 × 13-维行向量)变换到 PCA 子空间上,从而获得x',这是一个由两个新功能组成的二维样本向量:
>>> X_train_std[0].dot(w)
array([ 2.59891628, 0.00484089])
类似地,我们可以通过计算矩阵点积,将将整个124 × 13维训练数据集转换为两个主成分:
>>> X_train_pca = X_train_std.dot(w)
最后,让我们在二维散点图中可视化转换后的 Wine 训练集,现在将其存储为124 × 2-维矩阵。
>>> colors = ['r', 'b', 'g']
>>> markers = ['s', 'x', 'o']
>>> for l, c, m in zip(np.unique(y_train), colors, markers):
... plt.scatter(X_train_pca[y_train==l, 0],
... X_train_pca[y_train==l, 1],
... c=c, label=l, marker=m)
>>> plt.xlabel('PC 1')
>>> plt.ylabel('PC 2')
>>> plt.legend(loc='lower left')
>>> plt.show()
正如我们在结果图中看到的那样(如下图所示),数据沿x轴(第一个主成分)分布的比第二个主成分(y-轴),这与我们在上一个小节中创建的解释的方差比图一致。 但是,我们可以直观地看到线性分类器很可能能够很好地分离这些类:
尽管出于前面的散点图中的说明目的,我们对类标签信息进行了编码,但我们必须记住 PCA 是一种不使用类标签信息的无监督技术。
scikit-learn 中的主成分分析
尽管前面小节中的详细方法帮助我们了解了 PCA 的内部工作原理,但是我们现在将讨论如何使用 scikit-learn 中实现的PCA类。 PCA是 scikit-learn 的提升器类中的另一类,在该类中,我们首先使用训练数据拟合模型,然后再使用相同的模型参数转换训练数据和测试数据。 现在,让我们在 Wine 训练数据集上使用来自 scikit-learn 的PCA,通过 logistic 回归对转换后的样本进行分类,并通过我们在中定义的plot_decision_region函数可视化决策区域 第 2 章和“训练机器学习分类算法”:
from matplotlib.colors import ListedColormap
def plot_decision_regions(X, y, classifier, resolution=0.02):
# setup marker generator and color map
markers = ('s', 'x', 'o', '^', 'v')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
np.arange(x2_min, x2_max, resolution))
Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
Z = Z.reshape(xx1.shape)
plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot class samples
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
alpha=0.8, c=cmap(idx),
marker=markers[idx], label=cl)
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.decomposition import PCA
>>> pca = PCA(n_components=2)
>>> lr = LogisticRegression()
>>> X_train_pca = pca.fit_transform(X_train_std)
>>> X_test_pca = pca.transform(X_test_std)
>>> lr.fit(X_train_pca, y_train)
>>> plot_decision_regions(X_train_pca, y_train, classifier=lr)
>>> plt.xlabel('PC1')
>>> plt.ylabel('PC2')
>>> plt.legend(loc='lower left')
>>> plt.show()
通过执行前面的代码,我们现在应该看到训练模型的决策区域缩小为两个主要成分轴。
如果我们通过 scikit-learn 将 PCA 投影与我们自己的 PCA 实现进行比较,则我们会注意到,通过我们的分步方法,上图是先前 PCA 的镜像。 请注意,这不是由于这两种实现方式中的任何一种都会导致错误,而是这种差异的原因在于,根据本征求解器,本征向量可以具有负号或正号。 没关系,但是如果需要,我们可以通过将数据乘以-1来简单地还原镜像。 注意,特征向量通常按比例缩放到单位长度1。 为了完整起见,让我们在转换后的测试数据集上绘制逻辑回归的决策区域,以查看它是否可以很好地分离类:
>>> plot_decision_regions(X_test_pca, y_test, classifier=lr)
>>> plt.xlabel('PC1')
>>> plt.ylabel('PC2')
>>> plt.legend(loc='lower left')
>>> plt.show()
在通过执行前面的代码绘制测试集的决策区域之后,我们可以看到逻辑回归在这个小的二维特征子空间上执行得很好,并且只对测试数据集中的一个样本进行了错误分类。
如果我们对对不同主成分的解释方差比率感兴趣,我们可以简单地将n_components参数设置为None来初始化PCA类,以便保留所有主成分和解释方差比率 然后可以通过explained_variance_ratio_属性进行访问:
>>> pca = PCA(n_components=None)
>>> X_train_pca = pca.fit_transform(X_train_std)
>>> pca.explained_variance_ratio_
array([ 0.37329648, 0.18818926, 0.10896791, 0.07724389, 0.06478595,
0.04592014, 0.03986936, 0.02521914, 0.02258181, 0.01830924,
0.01635336, 0.01284271, 0.00642076])
请注意,我们在初始化 PCA 类时设置了n_components=None,以便它将按排序顺序返回所有主成分,而不是执行降维。
通过线性判别分析进行监督数据压缩
线性判别分析(LDA)可以用作特征提取技术,以提高计算效率并减少因非规则模型中的尺寸诅咒而引起的过拟合程度。
LDA 背后的一般概念与 PCA 非常相似,而 PCA 则试图在数据集中找到最大方差的正交分量轴。 LDA 中的目标是找到可优化类可分离性的特征子空间。 LDA 和 PCA 都是线性变换技术,可用于减少数据集中的维数。 前者是无监督算法,而后者是有监督的。 因此,我们可能会直观地认为,与 PCA 相比,LDA 是用于分类任务的高级特征提取技术。 但是,上午 Martinez 报告说,在某些情况下,例如,如果每个类别仅包含少量样本(AM Martinez 和 AC Kak。 PCA 与 LDA [。Pattern Analysis and Machine Intelligence,IEEE Transactions on,23(2):228-233,2001)。
注意
尽管 LDA 有时也称为 Fisher 的 LDA,但 Ronald A. Fisher 于 1936 年最初针对两类分类问题制定了 Fisher 线性判别式(RA Fisher。在分类学问题中使用多重度量 (《优生学年鉴》,7(2):179–188,1936 年)。 费舍尔线性判别式后来由 C.Radhakrishna Rao 在 1948 年等分类协方差和正态分布类的假设下推广到多类问题,我们现在将其称为 LDA(CR Rao。 生物学分类,英国皇家统计学会杂志,B 系列(方法论),10(2):159–203,1948 年)。
下图总结了针对两类问题的 LDA 概念。 来自类别 1 的样本显示为十字形,来自类别 2 的样本显示为圆形:
x-轴(LD 1)上显示的线性判别式可以很好地分隔两个正态分布的类。 尽管y轴(LD 2)上显示的示例性线性判别式捕获了数据集中的许多方差,但由于它无法捕获任何类别歧视性,因此它作为良好的线性判别式将失败 信息。
LDA 中的一种假设是数据是正态分布的。 同样,我们假设这些类具有相同的协方差矩阵,并且这些特征在统计上彼此独立。 但是,即使略微违反了这些假设中的一个或多个假设,用于降维的 LDA 仍然可以很好地发挥作用(RO Duda,PE Hart 和 DG Stork。模式分类。第二版,纽约, 2001)。
在下面的小节中,我们将深入研究 LDA 的内部工作原理,然后让我们总结一下 LDA 方法的关键步骤:
- 标准化
d维数据集(d是要素数量)。 - 对于每个类,计算
d维平均向量。 - 构造类间散布矩阵
S[B]和类内散布矩阵S[w]。 - 计算矩阵
S[w]^(-1)S[B]的特征向量和相应的特征值。 - 选择与
k最大特征值对应的k特征向量,以构建d × k维变换矩阵W; 特征向量是该矩阵的列。 - 使用变换矩阵
W将样本投影到新的特征子空间上。
注意
我们在使用 LDA 时所做的假设是,这些要素呈正态分布且彼此独立。 同样,LDA 算法假定各个类的协方差矩阵相同。 但是,即使我们在一定程度上违反了这些假设,LDA 在降维和分类任务(RO Duda,PE Hart 和 DG Stork。模式分类。2nd。Edition。New)上仍然可以很好地发挥作用。 约克,2001 年)。
计算散点矩阵
由于我们已经在本章开始的[PCA]部分中对葡萄酒 Wine 数据集的功能进行了标准化,因此我们可以跳过第一步,继续进行均值向量的计算,我们将 分别用于构造类内散布矩阵和类间散布矩阵。 每个平均值向量m[i]存储有关i类样本的平均特征值μ[m]:
结果是三个均值向量:
>>> np.set_printoptions(precision=4)
>>> mean_vecs = []
>>> for label in range(1,4):
... mean_vecs.append(np.mean(
... X_train_std[y_train==label], axis=0))
... print('MV %s: %s\n' %(label, mean_vecs[label-1]))
MV 1: [ 0.9259 -0.3091 0.2592 -0.7989 0.3039 0.9608 1.0515 -0.6306 0.5354
0.2209 0.4855 0.798 1.2017]
MV 2: [-0.8727 -0.3854 -0.4437 0.2481 -0.2409 -0.1059 0.0187 -0.0164 0.1095
-0.8796 0.4392 0.2776 -0.7016]
MV 3: [ 0.1637 0.8929 0.3249 0.5658 -0.01 -0.9499 -1.228 0.7436 -0.7652
0.979 -1.1698 -1.3007 -0.3912]
使用均值向量,我们现在可以计算类内散布矩阵S[w]:
这是通过将每个单独类别i的各个散布矩阵S[i]相加得出的:
>>> d = 13 # number of features
>>> S_W = np.zeros((d, d))
>>> for label,mv in zip(range(1,4), mean_vecs):
... class_scatter = np.zeros((d, d))
... for row in X_train[y_train == label]:
... row, mv = row.reshape(d, 1), mv.reshape(d, 1)
... class_scatter += (row-mv).dot((row-mv).T)
... S_W += class_scatter
>>> print('Within-class scatter matrix: %sx%s'
... % (S_W.shape[0], S_W.shape[1]))
Within-class scatter matrix: 13x13
我们在计算散点矩阵时所做的假设是训练集中的类标签是均匀分布的。 但是,如果我们打印类标签的数量,则会发现违反了该假设:
>>> print('Class label distribution: %s'
... % np.bincount(y_train)[1:])
Class label distribution: [40 49 35]
因此,在将各个散点矩阵S[i]汇总为散点矩阵S[w]之前,我们希望对它们进行缩放。 当将散布矩阵除以类别样本的数量N[i]时,我们可以看到,计算散布矩阵实际上与计算协方差矩阵Σ[i]相同。 协方差矩阵是散射矩阵的归一化版本:
>>> d = 13 # number of features
>>> S_W = np.zeros((d, d))
>>> for label,mv in zip(range(1, 4), mean_vecs):
... class_scatter = np.cov(X_train_std[y_train==label].T)
... S_W += class_scatter
>>> print('Scaled within-class scatter matrix: %sx%s'
... % (S_W.shape[0], S_W.shape[1]))
Scaled within-class scatter matrix: 13x13
在计算了缩放后的类内散布矩阵(或协方差矩阵)之后,我们可以继续进行下一步,并计算类间散布矩阵S[B]:
此处,m是所计算的总体平均值,包括来自所有类别的样本。
>>> mean_overall = np.mean(X_train_std, axis=0)
>>> d = 13 # number of features
>>> S_B = np.zeros((d, d))
>>> for i,mean_vec in enumerate(mean_vecs):
... n = X_train[y_train==i+1, :].shape[0]
... mean_vec = mean_vec.reshape(d, 1)
... mean_overall = mean_overall.reshape(d, 1)
S_B += n * (mean_vec - mean_overall).dot(
... (mean_vec - mean_overall).T)
print('Between-class scatter matrix: %sx%s'
... % (S_B.shape[0], S_B.shape[1]))
Between-class scatter matrix: 13x13
为新特征子空间选择线性判别式
LDA 的其余步骤与 PCA 的步骤相似。 但是,我们没有对协方差矩阵进行特征分解,而是解决了矩阵S[w]^(-1)S[B]的广义特征值问题:
>>>eigen_vals, eigen_vecs =\
...np.linalg.eig(np.linalg.inv(S_W).dot(S_B))
计算完特征对之后,我们现在可以按降序对特征值进行排序:
>>> eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:,i])
... for i in range(len(eigen_vals))]
>>> eigen_pairs = sorted(eigen_pairs,
... key=lambda k: k[0], reverse=True)
>>> print('Eigenvalues in decreasing order:\n')
>>> for eigen_val in eigen_pairs:
... print(eigen_val[0])
Eigenvalues in decreasing order:
452.721581245
156.43636122
8.11327596465e-14
2.78687384543e-14
2.78687384543e-14
2.27622032758e-14
2.27622032758e-14
1.97162599817e-14
1.32484714652e-14
1.32484714652e-14
1.03791501611e-14
5.94140664834e-15
2.12636975748e-16
在 LDA 中,线性判别式的数量最多为c-1,其中c是类别标签的数量,因为类别之间的散布矩阵S[B]是等级为 1 或更小的c矩阵之和。 我们确实可以看到我们只有两个非零特征值(特征值 3-13 不完全为零,但这是由于 NumPy 中的浮点算法所致)。 请注意,在极少数情况下,理想的共线性(所有对齐的样本点都位于一条直线上),协方差矩阵的秩为 1,这将导致仅一个特征向量具有非零特征值。
为了测量线性判别式(特征向量)捕获了多少类别区分信息,让我们通过减少特征值来绘制线性判别式,类似于在 PCA 部分中创建的解释方差图。 为简单起见,我们将类别区分信息的内容称为可辨别性。
>>> tot = sum(eigen_vals.real)
>>> discr = [(i / tot) for i in sorted(eigen_vals.real, reverse=True)]
>>> cum_discr = np.cumsum(discr)
>>> plt.bar(range(1, 14), discr, alpha=0.5, align='center',
... label='individual "discriminability"')
>>> plt.step(range(1, 14), cum_discr, where='mid',
... label='cumulative "discriminability"')
>>> plt.ylabel('"discriminability" ratio')
>>> plt.xlabel('Linear Discriminants')
>>> plt.ylim([-0.1, 1.1])
>>> plt.legend(loc='best')
>>> plt.show()
从结果图中可以看出,前两个线性判别式在 Wine 训练数据集中捕获了约 100%的有用信息:
现在让我们堆叠两个最有区别的特征向量列,以创建变换矩阵W:
>>> w = np.hstack((eigen_pairs[0][1][:, np.newaxis].real,
... eigen_pairs[1][1][:, np.newaxis].real))
>>> print('Matrix W:\n', w)
Matrix W:
[[ 0.0662 -0.3797]
[-0.0386 -0.2206]
[ 0.0217 -0.3816]
[-0.184 0.3018]
[ 0.0034 0.0141]
[-0.2326 0.0234]
[ 0.7747 0.1869]
[ 0.0811 0.0696]
[-0.0875 0.1796]
[-0.185 -0.284 ]
[ 0.066 0.2349]
[ 0.3805 0.073 ]
[ 0.3285 -0.5971]]
将样本投影到新特征空间上
使用我们在上一节中创建的转换矩阵W,我们现在可以通过乘以矩阵来转换训练数据集:
>>> X_train_lda = X_train_std.dot(w)
>>> colors = ['r', 'b', 'g']
>>> markers = ['s', 'x', 'o']
>>> for l, c, m in zip(np.unique(y_train), colors, markers):
... plt.scatter(X_train_lda[y_train==l, 0]*(-1)
... X_train_lda[y_train==l, 1]*(-1)
... c=c, label=l, marker=m)
>>> plt.xlabel('LD 1')
>>> plt.ylabel('LD 2')
>>> plt.legend(loc='lower right')
>>> plt.show()
正如我们在结果图中看到的那样,这三个酒类现在在新功能子空间中是线性可分离的:
通过 scikit-learn 进行的 LDA
分步实施对于了解 LDA 的内部工作原理以及了解 LDA 与 PCA 之间的差异是一个很好的练习。 现在,让我们看一下在 scikit-learn 中实现的LDA类:
>>> from sklearn.lda import LDA
>>> lda = LDA(n_components=2)
>>> X_train_lda = lda.fit_transform(X_train_std, y_train)
接下来,让我们看看逻辑回归分类器在 LDA 转换后如何处理低维训练数据集:
>>> lr = LogisticRegression()
>>> lr = lr.fit(X_train_lda, y_train)
>>> plot_decision_regions(X_train_lda, y_train, classifier=lr)
>>> plt.xlabel('LD 1')
>>> plt.ylabel('LD 2')
>>> plt.legend(loc='lower left')
>>> plt.show()
查看结果图,我们发现逻辑回归模型对类别 2 的样本之一进行了错误分类:
通过降低正则化强度,我们可能可以改变决策边界,以便逻辑回归模型可以对训练数据集中的所有样本进行正确分类。 但是,让我们看一下测试集上的结果:
>>> X_test_lda = lda.transform(X_test_std)
>>> plot_decision_regions(X_test_lda, y_test, classifier=lr)
>>> plt.xlabel('LD 1')
>>> plt.ylabel('LD 2')
>>> plt.legend(loc='lower left')
>>> plt.show()
正如我们在结果图中看到的那样,逻辑回归分类器通过仅使用二维特征子空间而不是原始的 13 Wine,就可以对测试数据集中的样本进行分类,从而获得完美的准确性得分。 特征:
使用内核主成分分析进行非线性映射
许多机器学习算法都对输入数据的线性可分离性进行了假设。 您了解到,感知器甚至需要完全线性可分离的训练数据来收敛。 到目前为止,我们已经介绍的其他算法都假设缺乏完美的线性可分离性是由于噪声引起的:Adaline,逻辑回归和(标准)支持向量机(SVM)[ 仅举几例。 但是,如果要处理非线性问题(在实际应用中可能会经常遇到),则用于降维的线性变换技术(例如 PCA 和 LDA)可能不是最佳选择。 在本节中,我们将研究 PCA 的内核版本,或“内核 PCA”,它与我们从第 3 章中记住的内核 SVM 的概念有关。 使用 Scikit 学习*的机器学习分类器的浏览。 使用内核 PCA,我们将学习如何将不可线性分离的数据转换为适合线性分类器的新的较低维子空间。
内核功能和内核技巧
我们在第 3 章和“使用 Scikit-learn” 进行的机器学习分类器讨论中对内核 SVM 的讨论中还记得,我们可以通过投影非线性问题来解决它们 到更高维度的新特征空间上,这些类可以线性分离。 为了将样本x ∈ R^d转换到这个更高的k维子空间,我们定义了非线性映射函数φ:
我们可以将φ视为创建原始特征的非线性组合以将原始d-维数据集映射到更大的k-维特征空间的函数。 例如,如果我们具有二维d = 2的特征向量x ∈ R^d(x是由d特征组成的列向量),则到 3D 空间的潜在映射可能如下:
换句话说,通过内核 PCA,我们执行了非线性映射,将数据转换为到更高维度的空间,并在该更高维度的空间中使用标准 PCA 将数据投射回更低维度的空间, 样本可以通过线性分类器分离(在样本可以通过输入空间中的密度分离的条件下)。 但是,这种方法的一个缺点是它在计算上非常昂贵,这就是我们使用内核技巧的地方。 使用内核技巧,我们可以计算原始特征空间中两个高维特征向量之间的相似度。
在继续使用内核技巧来解决这个计算量巨大的问题的更多细节之前,让我们回顾一下在本章开始时实现的标准 PCA 方法。 我们计算了两个特征k和j之间的协方差,如下所示:
由于特征的标准化使它们居中于零均值(例如μ[j] = 0和μ[k] = 0),因此可以如下简化此方程:
请注意,前面的方程是指两个特征之间的协方差。 现在,让我们编写通用方程式以计算协方差矩阵Σ:
Bernhard Scholkopf 推广了这种方法(B. Scholkopf,A。Smola 和 K.-R. Muller。核主成分分析。第 583-588 页,1997 年),以便我们可以替换样本之间的点积 通过φ的非线性特征组合在原始特征空间中:
要从此协方差矩阵中获得特征向量(主要成分),我们必须求解以下方程式:
此处,λ和v是协方差矩阵Σ的特征值和特征向量,而a可以是通过提取内核(相似性)矩阵K的特征向量而获得的。 我们将在以下段落中看到。
内核矩阵的推导如下:
首先,让我们以矩阵表示法编写协方差矩阵,其中φ(X)是n × k-维矩阵:
现在,我们可以写出特征向量公式,如下所示:
由于Σv = λv,我们得到:
在两侧将其乘以φ(X)会得到以下结果:
在这里,K是相似度(内核)矩阵:
正如我们在第 3 章,“使用 Scikit-learn”的机器学习分类器中的 SVM 部分所回顾的那样,我们使用内核技巧来避免计算成对的点。 使用内核函数K显式地提取φ下的样本x的乘积,因此我们无需显式地计算特征向量:
换句话说,我们在内核 PCA 之后获得的是已经投影到各个组件上的样本,而不是像标准 PCA 方法那样构造转换矩阵。 基本上,内核函数(或简称为内核)可以理解为一种计算两个向量之间的点积(一种相似性度量)的函数。
最常用的内核如下:
-
多项式内核:
此处,
θ是阈值,p是用户必须指定的功率。 -
双曲正切(Sigmoid)核:
-
下一节将在以下示例中使用的“径向基函数”(RBF)或高斯内核:
它也写成如下:
总结到目前为止,我们可以定义以下三个步骤来实现 RBF 内核 PCA:
-
我们计算内核(相似度)矩阵
k,我们需要计算以下内容:我们对每对样本执行此操作:
例如,如果我们的数据集包含 100 个训练样本,则成对相似性的对称核矩阵将是
100 × 100维。 -
我们使用以下等式将内核矩阵
k居中:这里,
l[n]是一个n × n维矩阵(与内核矩阵相同的维),其中所有值都等于l / n。 -
我们基于中心核矩阵的相应特征值收集顶部的
k特征向量,这些特征向量通过减小幅度进行排序。 与标准 PCA 相比,特征向量不是主要成分轴,而是投影到这些轴上的样本。
此时,您可能想知道为什么我们需要在第二步中将内核矩阵居中。 以前我们假设我们正在使用标准化数据,当我们制定协方差矩阵并通过φ将非线性特征组合替换为点积时,所有特征均均值为零。 因此,由于我们没有明确地计算新特征空间,并且不能保证新特征空间也以零为中心,因此在第二步中将内核矩阵居中成为必要。
在下一节中,我们将通过在 Python 中实现内核 PCA 来将这三个步骤付诸实践。
用 Python 实现内核主成分分析
在前面的小节中,我们讨论了内核 PCA 背后的核心概念。 现在,我们将按照概述内核 PCA 方法的三个步骤,用 Python 实现 RBF 内核 PCA。 使用 SciPy 和 NumPy 帮助函数,我们将看到实现内核 PCA 实际上非常简单:
from scipy.spatial.distance import pdist, squareform
from scipy import exp
from scipy.linalg import eigh
import numpy as np
def rbf_kernel_pca(X, gamma, n_components):
"""
RBF kernel PCA implementation.
Parameters
------------
X: {NumPy ndarray}, shape = [n_samples, n_features]
gamma: float
Tuning parameter of the RBF kernel
n_components: int
Number of principal components to return
Returns
------------
X_pc: {NumPy ndarray}, shape = [n_samples, k_features]
Projected dataset
"""
# Calculate pairwise squared Euclidean distances
# in the MxN dimensional dataset.
sq_dists = pdist(X, 'sqeuclidean')
# Convert pairwise distances into a square matrix.
mat_sq_dists = squareform(sq_dists)
# Compute the symmetric kernel matrix.
K = exp(-gamma * mat_sq_dists)
# Center the kernel matrix.
N = K.shape[0]
one_n = np.ones((N,N)) / N
K = K - one_n.dot(K) - K.dot(one_n) + one_n.dot(K).dot(one_n)
# Obtaining eigenpairs from the centered kernel matrix
# numpy.eigh returns them in sorted order
eigvals, eigvecs = eigh(K)
# Collect the top k eigenvectors (projected samples)
X_pc = np.column_stack((eigvecs[:, -i]
for i in range(1, n_components + 1)))
return X_pc
使用 RBF 内核 PCA 进行降维的一个缺点是我们必须先指定参数γ。 为γ找到合适的值需要进行实验,最好使用参数调整算法来完成,例如网格搜索,我们将在第 6 章,“模型评估和超参数调整最佳实践”中详细讨论。
示例 1 –分离半月形
现在,让我们将rbf_kernel_pca应用于某些非线性示例数据集。 我们将首先创建一个包含两个半月形的 100 个采样点的二维数据集:
>>> from sklearn.datasets import make_moons
>>> X, y = make_moons(n_samples=100, random_state=123)
>>> plt.scatter(X[y==0, 0], X[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> plt.scatter(X[y==1, 0], X[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> plt.show()
为了说明的目的,三角形符号的半月形代表一个类别,圆形符号表示的半月形代表另一个类别的样本:
显然,这两个半月形不是线性可分离的,我们的目标是通过内核 PCA 展开半月,以便数据集可以用作线性分类器的合适输入。 但是首先,让我们看看如果通过标准 PCA 将数据集投影到主要组件上,数据集会是什么样子:
>>> from sklearn.decomposition import PCA
>>> scikit_pca = PCA(n_components=2)
>>> X_spca = scikit_pca.fit_transform(X)
>>> fig, ax = plt.subplots(nrows=1,ncols=2, figsize=(7,3))
>>> ax[0].scatter(X_spca[y==0, 0], X_spca[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> ax[0].scatter(X_spca[y==1, 0], X_spca[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> ax[1].scatter(X_spca[y==0, 0], np.zeros((50,1))+0.02,
... color='red', marker='^', alpha=0.5)
>>> ax[1].scatter(X_spca[y==1, 0], np.zeros((50,1))-0.02,
... color='blue', marker='o', alpha=0.5)
>>> ax[0].set_xlabel('PC1')
>>> ax[0].set_ylabel('PC2')
>>> ax[1].set_ylim([-1, 1])
>>> ax[1].set_yticks([])
>>> ax[1].set_xlabel('PC1')
>>> plt.show()
显然,我们可以在结果图中看到,线性分类器将无法在通过标准 PCA 转换的数据集上很好地发挥作用:
请注意,当我们仅绘制第一个主成分(右子图)时,我们将三角形样本稍微向上移动,将圆形样本稍微向下移动,以更好地可视化类别重叠。
注意
请记住,PCA 是一种不受监督的方法,并且不使用类标签信息来最大化与 LDA 相比的差异。 此处,仅出于可视化目的添加了三角形和圆形符号以指示分离程度。
现在,让我们尝试一下我们在上一节中实现的内核 PCA 函数rbf_kernel_pca:
>>> from matplotlib.ticker import FormatStrFormatter
>>> X_kpca = rbf_kernel_pca(X, gamma=15, n_components=2)
>>> fig, ax = plt.subplots(nrows=1,ncols=2, figsize=(7,3))
>>> ax[0].scatter(X_kpca[y==0, 0], X_kpca[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> ax[0].scatter(X_kpca[y==1, 0], X_kpca[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> ax[1].scatter(X_kpca[y==0, 0], np.zeros((50,1))+0.02,
... color='red', marker='^', alpha=0.5)
>>> ax[1].scatter(X_kpca[y==1, 0], np.zeros((50,1))-0.02,
... color='blue', marker='o', alpha=0.5)
>>> ax[0].set_xlabel('PC1')
>>> ax[0].set_ylabel('PC2')
>>> ax[1].set_ylim([-1, 1])
>>> ax[1].set_yticks([])
>>> ax[1].set_xlabel('PC1')
>>> ax[0].xaxis.set_major_formatter(FormatStrFormatter('%0.1f'))
>>> ax[1].xaxis.set_major_formatter(FormatStrFormatter('%0.1f'))
>>> plt.show()
现在我们可以看到两个类(圆形和三角形)线性良好地分开,因此它成为线性分类器的合适训练数据集:
不幸的是,没有适用于不同数据集的调整参数γ的通用值。 要找到适合给定问题的γ值,需要进行实验。 在第 6 章,“学习模型评估和超参数调整的最佳实践”中,我们将讨论可以帮助我们自动执行优化调整参数任务的技术。 在这里,我将使用我发现产生良好结果的γ值。
示例 2 –分离同心圆
在上一小节中,我们向您展示了如何通过内核 PCA 分离半月形。 由于我们花了很多精力来理解内核 PCA 的概念,因此让我们看一下另一个有趣的非线性问题示例:同心圆。
代码如下:
>>> from sklearn.datasets import make_circles
>>> X, y = make_circles(n_samples=1000,
... random_state=123, noise=0.1, factor=0.2)
>>> plt.scatter(X[y==0, 0], X[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> plt.scatter(X[y==1, 0], X[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> plt.show()
同样,我们假设一个两类问题,其中三角形分别代表一类,而圆形分别代表另一类:
让我们从标准 PCA 方法开始,将其与 RBF 内核 PCA 的结果进行比较:
>>> scikit_pca = PCA(n_components=2)
>>> X_spca = scikit_pca.fit_transform(X)
>>> fig, ax = plt.subplots(nrows=1,ncols=2, figsize=(7,3))
>>> ax[0].scatter(X_spca[y==0, 0], X_spca[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> ax[0].scatter(X_spca[y==1, 0], X_spca[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> ax[1].scatter(X_spca[y==0, 0], np.zeros((500,1))+0.02,
... color='red', marker='^', alpha=0.5)
>>> ax[1].scatter(X_spca[y==1, 0], np.zeros((500,1))-0.02,
... color='blue', marker='o', alpha=0.5)
>>> ax[0].set_xlabel('PC1')
>>> ax[0].set_ylabel('PC2')
>>> ax[1].set_ylim([-1, 1])
>>> ax[1].set_yticks([])
>>> ax[1].set_xlabel('PC1')
>>> plt.show()
同样,我们可以看到标准 PCA 无法产生适合训练线性分类器的结果:
给定γ的适当值,让我们看看使用 RBF 内核 PCA 实现是否更幸运:
>>> X_kpca = rbf_kernel_pca(X, gamma=15, n_components=2)
>>> fig, ax = plt.subplots(nrows=1,ncols=2, figsize=(7,3))
>>> ax[0].scatter(X_kpca[y==0, 0], X_kpca[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> ax[0].scatter(X_kpca[y==1, 0], X_kpca[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> ax[1].scatter(X_kpca[y==0, 0], np.zeros((500,1))+0.02,
... color='red', marker='^', alpha=0.5)
>>> ax[1].scatter(X_kpca[y==1, 0], np.zeros((500,1))-0.02,
... color='blue', marker='o', alpha=0.5)
>>> ax[0].set_xlabel('PC1')
>>> ax[0].set_ylabel('PC2')
>>> ax[1].set_ylim([-1, 1])
>>> ax[1].set_yticks([])
>>> ax[1].set_xlabel('PC1')
>>> plt.show()
再次,RBF 内核 PCA 将数据投影到新的子空间上,在这两个类之间可以线性分离:
投影新数据点
在内核 PCA 的前两个示例应用,半月形和同心圆中,我们将单个数据集投影到了新功能上。 但是,在实际应用中,我们可能要转换一个以上的数据集,例如训练和测试数据,并且通常还会在模型构建和评估后收集新样本。 在本节中,您将学习如何投影不属于训练数据集的数据点。
我们从本章开始的标准 PCA 方法中记得,我们通过计算转换矩阵与输入样本之间的点积来投影数据。 投影矩阵的列是我们从协方差矩阵获得的顶部v)。 现在,问题是如何将这一概念转移到内核 PCA? 如果我们回想一下内核 PCA 背后的想法,我们记得我们已经获得了中心内核矩阵(不是协方差矩阵)的特征向量(a),这意味着这些是已经投影到主成分上的样本 轴v。 因此,如果要将新样本x'投影到该主分量轴上,则需要计算以下内容:
幸运的是,我们可以使用内核技巧,因此我们不必显式计算投影φ(x')^T v。 但是,值得注意的是,与标准 PCA 相比,内核 PCA 是一种基于内存的方法,这意味着我们每次都必须重用原始训练集来投影新样本。 我们必须计算训练数据集中的每个i样本与新样本x'之间的成对 RBF 核(相似性):
在此,内核矩阵K的特征向量a和特征值λ在公式中满足以下条件:
在计算新样本与训练集中样本之间的相似度后,我们必须通过特征向量a对其特征值进行归一化。 因此,让我们修改我们先前实现的rbf_kernel_pca函数,使其也返回内核矩阵的特征值:
from scipy.spatial.distance import pdist, squareform
from scipy import exp
from scipy.linalg import eigh
import numpy as np
def rbf_kernel_pca(X, gamma, n_components):
"""
RBF kernel PCA implementation.
Parameters
------------
X: {NumPy ndarray}, shape = [n_samples, n_features]
gamma: float
Tuning parameter of the RBF kernel
n_components: int
Number of principal components to return
Returns
------------
X_pc: {NumPy ndarray}, shape = [n_samples, k_features]
Projected dataset
lambdas: list
Eigenvalues
"""
# Calculate pairwise squared Euclidean distances
# in the MxN dimensional dataset.
sq_dists = pdist(X, 'sqeuclidean')
# Convert pairwise distances into a square matrix.
mat_sq_dists = squareform(sq_dists)
# Compute the symmetric kernel matrix.
K = exp(-gamma * mat_sq_dists)
# Center the kernel matrix.
N = K.shape[0]
one_n = np.ones((N,N)) / N
K = K - one_n.dot(K) - K.dot(one_n) + one_n.dot(K).dot(one_n)
# Obtaining eigenpairs from the centered kernel matrix
# numpy.eigh returns them in sorted order
eigvals, eigvecs = eigh(K)
# Collect the top k eigenvectors (projected samples)
alphas = np.column_stack((eigvecs[:,-i]
for i in range(1,n_components+1)))
# Collect the corresponding eigenvalues
lambdas = [eigvals[-i] for i in range(1,n_components+1)]
return alphas, lambdas
现在,让我们创建一个新的半月数据集,并使用更新的 RBF 内核 PCA 实现将其投影到一维子空间上:
>>> X, y = make_moons(n_samples=100, random_state=123)
>>> alphas, lambdas =rbf_kernel_pca(X, gamma=15, n_components=1)
为了确保实现用于投影新样本的代码,我们假设半月数据集的第 26 个点是一个新数据点x',我们的任务是将其投影到这个新子空间上:
>>> x_new = X[25]
>>> x_new
array([ 1.8713187 , 0.00928245])
>>> x_proj = alphas[25] # original projection
>>> x_proj
array([ 0.07877284])
>>> def project_x(x_new, X, gamma, alphas, lambdas):
... pair_dist = np.array([np.sum(
... (x_new-row)**2) for row in X])
... k = np.exp(-gamma * pair_dist)
... return k.dot(alphas / lambdas)
通过执行以下代码,我们可以再现原始投影。 使用project_x功能,我们也可以投影任何新的数据样本。 代码如下:
>>> x_reproj = project_x(x_new, X,
... gamma=15, alphas=alphas, lambdas=lambdas)
>>> x_reproj
array([ 0.07877284])
最后,让我们可视化第一个主要成分上的投影:
>>> plt.scatter(alphas[y==0, 0], np.zeros((50)),
... color='red', marker='^',alpha=0.5)
>>> plt.scatter(alphas[y==1, 0], np.zeros((50)),
... color='blue', marker='o', alpha=0.5)
>>> plt.scatter(x_proj, 0, color='black',
... label='original projection of point X[25]',
... marker='^', s=100)
>>> plt.scatter(x_reproj, 0, color='green',
... label='remapped point X[25]',
... marker='x', s=500)
>>> plt.legend(scatterpoints=1)
>>> plt.show()
正如我们在散点图的中所看到的,我们将样本x'正确映射到了第一个主成分上:
scikit-learn 中的内核主成分分析
为了我们的方便,scikit-learn 在sklearn.decomposition子模块中实现了内核 PCA 类。 用法类似于标准 PCA 类,我们可以通过kernel参数指定内核:
>>> from sklearn.decomposition import KernelPCA
>>> X, y = make_moons(n_samples=100, random_state=123)
>>> scikit_kpca = KernelPCA(n_components=2,
... kernel='rbf', gamma=15)
>>> X_skernpca = scikit_kpca.fit_transform(X)
为了查看是否获得与我们自己的内核 PCA 实现一致的结果,让我们将转换后的半月形数据绘制到前两个主要成分上:
>>> plt.scatter(X_skernpca[y==0, 0], X_skernpca[y==0, 1],
... color='red', marker='^', alpha=0.5)
>>> plt.scatter(X_skernpca[y==1, 0], X_skernpca[y==1, 1],
... color='blue', marker='o', alpha=0.5)
>>> plt.xlabel('PC1')
>>> plt.ylabel('PC2')
>>> plt.show()
如我们所见, scikit-learn KernelPCA的结果与我们自己的实现一致:
注意
Scikit-learn 还实现了非线性降维的高级技术,这超出了本书的范围。 您可以通过这个页面上的说明示例,在 scikit-learn 中找到有关当前实现的很好概述。
四十、学习模型评估和超参数调整的最佳实践
在前面的章节中,您了解了用于分类的基本机器学习算法,以及在将数据输入这些算法之前如何使数据成形。 现在,是时候通过微调算法和评估模型的性能来学习构建良好的机器学习模型的最佳实践! 在本章中,我们将学习如何:
- 获得模型性能的无偏估计
- 诊断机器学习算法的常见问题
- 微调机器学习模型
- 使用不同的绩效指标评估预测模型
使用管道简化工作流程
当在前几章中应用了不同的预处理技术时,例如第 4 章和“构建良好的训练集–数据预处理”标准化* 用于特征缩放。 第 5 章或“通过降维压缩”进行数据压缩的*或主数据分析,您了解到必须重用参数 这些数据是在训练数据拟合以缩放和压缩任何新数据(例如,单独的测试数据集中的样本)期间获得的。 在本节中,您将学习一个非常方便的工具,scikit-learn 中的Pipeline类。 它使我们能够拟合包含任意数量的转换步骤的模型,并将其应用于对新数据的预测。
加载乳腺癌威斯康星州数据集
在本章中,我们将与乳腺癌威斯康星州数据集一起使用,该数据集包含 569 个恶性和良性肿瘤细胞样本。 数据集中的前两列分别存储样本的唯一 ID 号和相应的诊断( M =恶性,B =良性)。 第 3-32 栏包含 30 个实值特征,这些特征是根据细胞核的数字化图像计算得出的,可用于构建模型来预测肿瘤是良性还是恶性。 威斯康星州乳腺癌数据集已保存在 UCI 机器学习存储库中,有关此数据集的更多详细信息,请访问这个页面。
在本节中,我们将读取数据集,并通过三个简单的步骤将其分为训练和测试数据集:
-
我们将直接使用 Pandas 从 UCI 网站读取数据集:
>>> import pandas as pd >>> df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data', header=None) -
接下来,我们将 30 个特征分配给 NumPy 数组
X。 使用LabelEncoder,我们将类标签从其原始字符串表示形式(M和B)转换为整数:>>> from sklearn.preprocessing import LabelEncoder >>> X = df.loc[:, 2:].values >>> y = df.loc[:, 1].values >>> le = LabelEncoder() >>> y = le.fit_transform(y)在将类别标签(诊断)编码为数组
y后,现在将恶性肿瘤表示为1类,将良性肿瘤表示为0类,我们可以通过称为transform来说明 两个假类标签上的LabelEncoder的]方法:>>> le.transform(['M', 'B']) array([1, 0]) -
在以下小节中构建第一个模型管道之前,让我们将数据集分为一个单独的训练数据集(数据的 80%)和一个单独的测试数据集(数据的 20%):
>>> from sklearn.cross_validation import train_test_split >>> X_train, X_test, y_train, y_test = \ ... train_test_split(X, y, test_size=0.20, random_state=1)
在管道中组合提升器和估计器
在的前一章中,您了解到许多学习算法需要使用相同规模的输入功能才能获得最佳性能。 因此,我们需要在之前将乳腺癌威斯康星州数据集中的列标准化,然后才能将其输入到线性分类器中,例如逻辑回归。 此外,假设我们要通过主成分分析(PCA)将数据从最初的 30 个维压缩到较低的二维子空间,这是一种用于降维的特征提取技术 我们在第 5 章,“通过降维压缩数据”中介绍。 无需分别进行训练和测试数据集的拟合和转换步骤,我们可以将StandardScaler,PCA和LogisticRegression对象链接在管道中:
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.decomposition import PCA
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.pipeline import Pipeline
>>> pipe_lr = Pipeline([('scl', StandardScaler()),
... ('pca', PCA(n_components=2)),
... ('clf', LogisticRegression(random_state=1))])
>>> pipe_lr.fit(X_train, y_train)
>>> print('Test Accuracy: %.3f' % pipe_lr.score(X_test, y_test))
Test Accuracy: 0.947
Pipeline对象将一个元组列表作为输入,其中每个元组中的第一个值是一个任意的标识符字符串,我们可以使用它来访问管道中的各个元素,如本章稍后所述,第二个 每个元组中的元素是一个 scikit 学习转换器或估计器。
流水线中的中间步骤构成 scikit-learn 提升器,最后一步是估算器。 在前面的代码示例中,我们构建了一个包含两个中间步骤的管道,一个是StandardScaler和一个PCA转换器,另一个是逻辑回归分类器作为最终估计量。 当我们在管道pipe_lr上执行fit方法时,StandardScaler对训练数据执行fit和transform,然后将转换后的训练数据传递到管道中的下一个对象,即[ PCA。 与上一步相似,PCA也对缩放后的输入数据执行了fit和transform,并将其传递给管道的最后一个元素(估计器)。 我们应注意,此管道中的中间步骤数量没有限制。 下图概述了管道如何工作的概念:
使用 k 倍交叉验证来评估模型性能
建立机器学习模型的关键步骤之一是根据该模型以前未见的数据评估其性能。 让我们假设使我们的模型适合于训练数据集,并使用相同的数据来估计其在实践中的表现。 我们从第 3 章,“使用 Scikit-learn” 的机器学习分类器的通过正则化处理部分解决过拟合问题中记得,模型可能会遭受拟合不足 (高偏差)(如果模型太简单),或者如果模型对于基础训练数据来说太复杂,则可能过度拟合训练数据(高方差)。 为了找到可接受的偏差方差折衷,我们需要仔细评估模型。 在本节中,您将学习有用的交叉验证技术保持交叉验证和 k 倍交叉验证,这可以帮助我们获得 模型泛化误差的可靠估计,即模型在看不见的数据上的表现如何。
保持方法
估计机器学习模型泛化性能的经典且流行的方法是保持交叉验证。 使用保持方法,我们将初始数据集分为单独的训练和测试数据集-前者用于模型训练,而后者用于估计其性能。 但是,在典型的机器学习应用中,我们也对调整和比较不同的参数设置感兴趣,以进一步提高对看不见的数据进行预测的性能。 此过程称为模型选择,其中术语“模型选择”指的是给定的分类问题,我们要为其选择调整参数的最佳最佳值(也称为超参数)。 但是,如果我们在模型选择期间一遍又一遍地重复使用相同的测试数据集,则它将成为我们训练数据的一部分,因此该模型更可能过拟合。 尽管存在此问题,但许多人仍将测试集用于模型选择,这不是一个好的机器学习实践。
使用保留方法进行模型选择的一种更好的方法是将数据分为三个部分:训练集,验证集和测试集。 训练集用于拟合不同的模型,然后将验证集上的性能用于模型选择。 拥有模型在训练和模型选择步骤中从未见过的测试集的优点是,我们可以获得对其通用化为新数据的能力的较少偏倚的估计。 下图说明了保持交叉验证的概念,其中在使用不同的参数值训练后,我们使用验证集重复评估模型的性能。 一旦对参数值的调整感到满意,就可以在测试数据集上估计模型的泛化误差:
保持方法的缺点是,性能估算对我们如何将训练集划分为训练和验证子集非常敏感; 对于不同的数据样本,估算值将有所不同。 在下一个小节中,我们将介绍一种用于性能评估的更强大的技术,即 k 倍交叉验证,其中我们对k子集的重复保留方法k次 训练数据。
K 折交叉验证
在 k 倍交叉验证中,我们将训练数据集随机分为k折叠,而无需替换,其中K - 1折叠用于模型训练,而一折叠用于测试。 重复此过程k次,以便获得k模型和性能估算。
注意
如果您不熟悉在不替换的情况下使用和采样的术语,让我们进行一个简单的思想实验。 假设我们正在玩彩票游戏,我们从 an 中随机抽取数字。 我们从一个 holds 着五个唯一数字 0、1、2、3 和 4 的开始,然后每转一圈绘制一个正好一个数字。 在第一轮中,从骨灰盒中提取特定数字的机会是 1/5。 现在,在不更换样本的情况下,我们不会在每次旋转后将数字重新放入骨灰盒。 因此,在下一轮中从剩余号码集合中提取特定号码的可能性取决于前一轮。 例如,如果我们有剩余的一组数字 0、1、2 和 4,则在下一轮中绘制数字 0 的机会将变为 1/4。**
但是,在随机抽样替换中,我们总是将抽取的数字返回到骨灰盒,以使每次旋转绘制特定数字的概率都不会改变。 我们可以多次绘制相同的数字。 换句话说,在替换抽样中,样本(数量)是独立的,并且协方差为零。 例如,五轮绘制随机数的结果如下所示:
- 随机抽样而不更换:2、1、3、4、0
- 随机抽样替换:1、3、3、4、1
然后,我们基于不同的独立折叠计算模型的平均性能,以获得与保持方法相比对训练数据细分不那么敏感的性能估计。 通常,我们使用 k 倍交叉验证进行模型调整,也就是说,找到产生令人满意的泛化性能的最佳超参数值。 一旦找到令人满意的超参数值,我们就可以在完整的训练集上对模型进行重新训练,并使用独立的测试集获得最终的性能估算。
由于 k 倍交叉验证是一种无需替换的重采样技术,因此该方法的优势在于,每个样本点将仅是训练和测试数据集的一部分,一次即可得出模型性能的较低方差估算值 比保持方法。 下图概述了k = 10与 k 倍交叉验证背后的概念。 训练数据集分为 10 折,在 10 次迭代期间,将 9 折用于训练,将 1 折用作模型评估的测试集。 同样,每个折叠的估计性能E[i](例如,分类准确度或错误)然后用于计算模型的估计平均性能E:
k 倍交叉验证中k的标准值为 10,对于大多数应用而言,这通常是一个合理的选择。 但是,如果我们使用相对较小的训练集,则增加折叠数可能很有用。 如果我们增加k的值,则在每次迭代中将使用更多的训练数据,这将导致通过平均各个模型估计值来降低对泛化性能的估计偏差。 但是,k的较大值也会增加交叉验证算法的运行时间,并且由于训练折叠会彼此更类似于,因此具有较高方差的产量估算值。 另一方面,如果我们使用大型数据集,则可以为k选择一个较小的值,例如k = 5,并且仍然可以获得模型平均性能的准确估计值,同时降低 在不同折痕处重新拟合和评估模型的计算成本。
注意
k 倍交叉验证的一种特殊情况是留一法则(LOO)交叉验证方法。 在 LOO 中,我们将折数设置为等于训练样本的数量( k = n ),以便在每次迭代期间仅使用一个训练样本进行测试。 这是处理非常小的数据集的推荐方法。
分层 k 折叠交叉验证比标准 k 折叠交叉验证方法稍有改进,这可以产生更好的偏差和方差估计,尤其是在类比例不相等的情况下,正如 R 的研究表明的那样。 Kohavi 等。 (R. Kohavi 等人交叉验证和自举的研究,用于准确性估计和模型选择。在伊贾伊,第 14 卷,第 1137-1145 页,1995 年)。 在分层交叉验证中,每个折叠中的类比例都保留下来,以确保每个折叠都代表训练数据集中的类比例,我们将在 scikit-learn 中使用StratifiedKFold迭代器进行说明:
>>> import numpy as np
>>> from sklearn.cross_validation import StratifiedKFold
>>> kfold = StratifiedKFold(y=y_train,
... n_folds=10,
... random_state=1)
>>> scores = []
>>> for k, (train, test) in enumerate(kfold):
... pipe_lr.fit(X_train[train], y_train[train])
... score = pipe_lr.score(X_train[test], y_train[test])
... scores.append(score)
... print('Fold: %s, Class dist.: %s, Acc: %.3f' % (k+1,
... np.bincount(y_train[train]), score))
Fold: 1, Class dist.: [256 153], Acc: 0.891
Fold: 2, Class dist.: [256 153], Acc: 0.978
Fold: 3, Class dist.: [256 153], Acc: 0.978
Fold: 4, Class dist.: [256 153], Acc: 0.913
Fold: 5, Class dist.: [256 153], Acc: 0.935
Fold: 6, Class dist.: [257 153], Acc: 0.978
Fold: 7, Class dist.: [257 153], Acc: 0.933
Fold: 8, Class dist.: [257 153], Acc: 0.956
Fold: 9, Class dist.: [257 153], Acc: 0.978
Fold: 10, Class dist.: [257 153], Acc: 0.956
>>> print('CV accuracy: %.3f +/- %.3f' % (
... np.mean(scores), np.std(scores)))
CV accuracy: 0.950 +/- 0.029
首先,我们使用训练集中的类标签y_train从sklearn.cross_validation模块初始化了StratifiedKfold迭代器,并通过n_folds参数指定了折叠次数。 当我们使用kfold迭代器遍历k折叠时,我们使用train中返回的索引来适应本章开始时设置的逻辑回归管线。 使用pile_lr管道,我们确保在每次迭代中都正确缩放了样本(例如标准化)。 然后,我们使用test指数来计算模型的准确性得分,然后在scores列表中收集该模型以计算估计的平均准确性和标准偏差。
尽管前面的代码示例对于说明 k 折交叉验证的工作原理很有用,但是 scikit-learn 还实现了 k 折交叉验证评分器,这使我们能够更有效地使用分层 k 折交叉验证来评估模型:
>>> from sklearn.cross_validation import cross_val_score
>>> scores = cross_val_score(estimator=pipe_lr,
... X=X_train,
... y=y_train,
... cv=10,
... n_jobs=1)
>>> print('CV accuracy scores: %s' % scores)
CV accuracy scores: [ 0.89130435 0.97826087 0.97826087
0.91304348 0.93478261 0.97777778
0.93333333 0.95555556 0.97777778
0.95555556]
>>> print('CV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores)))
CV accuracy: 0.950 +/- 0.029
cross_val_score方法的极其有用的功能是,我们可以在机器上的多个 CPU 上分布不同倍数的评估。 如果我们将n_jobs参数设置为1,则就像前面的StratifiedKFold示例一样,将仅使用一个 CPU 来评估性能。 但是,通过设置n_jobs=2,我们可以将 10 轮交叉验证分配给两个 CPU(如果在我们的计算机上可用),并且通过设置n_jobs=-1,我们可以使用我们计算机上的所有可用 CPU 并行进行计算 。
注意
请注意,关于如何在交叉验证中估算泛化性能方差的详细讨论超出了本书的范围,但是您可以在 M. Markatou 等人的出色文章(M. Markatou, H. Tian,S。Biswas 和 GM Hripcsak。泛化误差的交叉验证估计量方差分析机器学习研究杂志,6:1127-1168,2005 )。
您还可以阅读有关其他交叉验证技术的信息,例如.632 Bootstrap 交叉验证方法(B. Efron 和 R. Tibshirani。交叉验证的改进:632+ Bootstrap 方法。美国统计协会杂志,92(438):548-560,1997)。
具有学习和验证曲线的调试算法
在此部分中,我们将介绍两个非常简单但功能强大的诊断工具,这些工具可帮助我们改善学习算法的性能:学习曲线和验证曲线。 在接下来的小节中,我们将讨论如何使用学习曲线来诊断学习算法是否存在过拟合(高方差)或欠拟合(高偏差)的问题。 此外,我们将查看验证曲线,这些曲线可帮助我们解决学习算法的常见问题。
使用学习曲线诊断偏差和方差问题
如果模型对于给定的训练数据集过于复杂(该模型中的自由度或参数太多),则该模型往往会过度拟合训练数据,而不能很好地推广到看不见的数据。 通常,它可以帮助收集更多的训练样本以减少过度拟合的程度。 但是,实际上,收集更多数据通常非常昂贵,或者根本不可行。 通过将模型训练和验证准确性绘制为训练集大小的函数,我们可以轻松地检测出模型是遭受高方差还是高偏差,以及收集更多数据是否可以帮助解决该问题。 但是在讨论如何在 sckit-learn 中绘制学习曲线之前,让我们通过遍历下图讨论这两个常见的模型问题:
左上方的图显示了具有高偏差的模型。 该模型具有较低的训练和交叉验证的准确性,这表明它不适合训练数据。 解决此问题的常用方法是增加模型参数的数量,例如,通过收集或构造其他特征,或通过降低正则化的程度,例如在 SVM 或逻辑回归分类器中。 右上方的图形显示了一个模型,该模型存在较大的差异,这由训练和交叉验证准确性之间的巨大差异表示。 为了解决过度拟合的问题,我们可以收集更多训练数据或降低模型的复杂性,例如,通过增加正则化参数; 对于非正规模型,它还可以通过特征选择(第 4 章,“构建良好的训练集–数据预处理”)或特征提取(第 5 章)来减少特征数量 ,通过降维压缩数据。 我们将注意到,收集更多的训练数据会减少过度拟合的机会。 但是,例如当训练数据非常嘈杂或模型已经非常接近最佳值时,它可能并不总是有帮助。
在的下一个小节中,我们将看到如何使用验证曲线解决这些模型问题,但首先让我们看看如何使用 scikit-learn 的学习曲线函数来评估模型:
>>> import matplotlib.pyplot as plt
>>> from sklearn.learning_curve import learning_curve
>>> pipe_lr = Pipeline([
... ('scl', StandardScaler()),
... ('clf', LogisticRegression(
... penalty='l2', random_state=0))])
>>> train_sizes, train_scores, test_scores =\
... learning_curve(estimator=pipe_lr,
... X=X_train,
... y=y_train,
... train_sizes=np.linspace(0.1, 1.0, 10),
... cv=10,
... n_jobs=1)
>>> train_mean = np.mean(train_scores, axis=1)
>>> train_std = np.std(train_scores, axis=1)
>>> test_mean = np.mean(test_scores, axis=1)
>>> test_std = np.std(test_scores, axis=1)
>>> plt.plot(train_sizes, train_mean,
... color='blue', marker='o',
... markersize=5,
... label='training accuracy')
>>> plt.fill_between(train_sizes,
... train_mean + train_std,
... train_mean - train_std,
... alpha=0.15, color='blue')
>>> plt.plot(train_sizes, test_mean,
... color='green', linestyle='--',
... marker='s', markersize=5,
... label='validation accuracy')
>>> plt.fill_between(train_sizes,
... test_mean + test_std,
... test_mean - test_std,
... alpha=0.15, color='green')
>>> plt.grid()
>>> plt.xlabel('Number of training samples')
>>> plt.ylabel('Accuracy')
>>> plt.legend(loc='lower right')
>>> plt.ylim([0.8, 1.0])
>>> plt.show()
成功执行了前面的代码后,我们将获得以下学习曲线图:
通过learning_curve功能中的train_sizes参数,我们可以控制用于生成学习曲线的训练样本的绝对或相对数量。 在这里,我们将train_sizes=np.linspace(0.1, 1.0, 10)设置为使用 10 个均匀间隔的相对间隔作为训练集大小。 默认情况下,learning_curve函数使用分层的 k 倍交叉验证来计算交叉验证的准确性,我们通过cv参数设置了k = 10。 然后,我们简单地根据返回的交叉验证的训练和测试得分(针对不同大小的训练集)计算平均准确度,并使用 matplotlib 的plot函数对其进行绘制。 此外,我们使用fill_between函数将平均准确度的标准偏差添加到该图以指示估计的方差。
正如我们在前面的学习曲线图中所看到的,我们的模型在测试数据集上的表现非常好。 但是,它可能会略微拟合训练数据和交叉验证准确性曲线之间相对较小但可见的间隙所指示的训练数据。
使用验证曲线解决过度拟合和过度拟合
验证曲线是通过解决过度拟合或拟合不足等问题来改善模型性能的有用工具。 验证曲线与学习曲线有关,但是我们不改变训练和测试精度作为样本量的函数,而是改变模型参数的值,例如,逻辑回归中的逆正则化参数C。 让我们继续看看如何通过 sckit-learn 创建验证曲线:
>>> from sklearn.learning_curve import validation_curve
>>> param_range = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]
>>> train_scores, test_scores = validation_curve(
... estimator=pipe_lr,
... X=X_train,
... y=y_train,
... param_name='clf__C',
... param_range=param_range,
... cv=10)
>>> train_mean = np.mean(train_scores, axis=1)
>>> train_std = np.std(train_scores, axis=1)
>>> test_mean = np.mean(test_scores, axis=1)
>>> test_std = np.std(test_scores, axis=1)
>>> plt.plot(param_range, train_mean,
... color='blue', marker='o',
... markersize=5,
... label='training accuracy')
>>> plt.fill_between(param_range, train_mean + train_std,
... train_mean - train_std, alpha=0.15,
... color='blue')
>>> plt.plot(param_range, test_mean,
... color='green', linestyle='--',
... marker='s', markersize=5,
... label='validation accuracy')
>>> plt.fill_between(param_range,
... test_mean + test_std,
... test_mean - test_std,
... alpha=0.15, color='green')
>>> plt.grid()
>>> plt.xscale('log')
>>> plt.legend(loc='lower right')
>>> plt.xlabel('Parameter C')
>>> plt.ylabel('Accuracy')
>>> plt.ylim([0.8, 1.0])
>>> plt.show()
使用前面的代码,我们获得了参数C的验证曲线图:
与learning_curve函数类似的,如果我们使用分类算法,则validation_curve函数默认情况下使用分层的 k 倍交叉验证来估计模型的性能。 在validation_curve函数中,我们指定了要评估的参数。 在这种情况下,它是C,LogisticRegression分类器的逆正则化参数,我们将其写为'clf__C'来访问 scikit-learn 管道内的LogisticRegression对象,以获取我们通过设置的指定值范围 param_range参数。 与上一节中的学习曲线示例相似,我们绘制了平均训练和交叉验证精度以及相应的标准偏差。
尽管C的变化值在精度上的差异很小,但是我们可以看到,当我们增加正则强度时,该模型会略微拟合数据(C的值很小)。 但是,对于C较大的值,这意味着降低正则化的强度,因此该模型倾向于稍微拟合数据。 在这种情况下,最有效点似乎在C=0.1附近。
通过网格搜索对机器学习模型进行微调
在机器学习中,我们有两种类型的参数:从训练数据中学习的参数,例如 logistic 回归中的权重,以及分别优化的学习算法的参数。 后者是模型的调整参数,也称为超参数,例如,逻辑回归中的正则化参数或决策树的深度参数 。
在上一节中,我们使用验证曲线通过调整模型的超参数之一来改善其性能。 在本节中,我们将介绍一种称为网格搜索的强大超参数优化技术,该技术可以通过找到以下内容的最佳组合来进一步帮助改善模型的性能: 超参数值。
通过网格搜索调整超参数
网格搜索的方法非常简单,它是一种蛮力的穷举搜索范例,其中我们为不同的超参数指定值列表,并且计算机针对每种组合的模型性能进行评估,以获得最优的 放:
>>> from sklearn.grid_search import GridSearchCV
>>> from sklearn.svm import SVC
>>> pipe_svc = Pipeline([('scl', StandardScaler()),
... ('clf', SVC(random_state=1))])
>>> param_range = [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0]
>>> param_grid = [{'clf__C': param_range,
... 'clf__kernel': ['linear']},
... {'clf__C': param_range,
... 'clf__gamma': param_range,
... 'clf__kernel': ['rbf']}]
>>> gs = GridSearchCV(estimator=pipe_svc,
... param_grid=param_grid,
... scoring='accuracy',
... cv=10,
... n_jobs=-1)
>>> gs = gs.fit(X_train, y_train)
>>> print(gs.best_score_)
0.978021978022
>>> print(gs.best_params_)
{'clf__C': 0.1, 'clf__kernel': 'linear'}
使用前面的代码,我们从sklearn.grid_search模块初始化了GridSearchCV对象,以训练和调整支持向量机(SVM)管道。 我们将GridSearchCV的param_grid参数设置为词典列表,以指定要调整的参数。 对于线性 SVM,我们仅评估反正则化参数C; 对于 RBF 内核 SVM,我们调整了C和gamma参数。 请注意,gamma参数特定于内核 SVM。 在使用训练数据执行网格搜索之后,我们通过best_score_属性获得了性能最佳模型的得分,并查看了可通过best_params_属性访问的参数。 在这种特殊情况下,带有'clf__C'= 0.1'的线性 SVM 模型产生了最佳的 k 倍交叉验证准确性:97.8%。
最后,我们将使用独立的测试数据集来评估最佳选择模型的性能,该模型可通过GridSearchCV对象的best_estimator_属性获得:
>>> clf = gs.best_estimator_
>>> clf.fit(X_train, y_train)
>>> print('Test accuracy: %.3f' % clf.score(X_test, y_test))
Test accuracy: 0.965
注意
尽管网格搜索是用于找到最佳参数集的强大方法,但是对所有可能参数组合的评估在计算上也非常昂贵。 使用 scikit-learn 采样不同参数组合的另一种方法是随机搜索。 使用 scikit-learn 中的RandomizedSearchCV类,我们可以从具有指定预算的采样分布中绘制随机参数组合。 可在这个页面中找到更多有关其用法的详细信息和示例。
具有嵌套交叉验证的算法选择
结合使用网格搜索将 k 倍交叉验证与网格搜索结合使用是一种有用的方法,可以通过更改机器学习模型的超参数值来对其性能进行微调,正如我们在上一小节中看到的那样。 如果我们想在不同的机器学习算法中进行选择,则另一种推荐的方法是嵌套交叉验证,并且在对误差估计的偏差进行了很好的研究中,Varma 和 Simon 得出结论:相对于误差估计,估计的真实误差几乎是无偏的。 使用嵌套交叉验证的测试集(S. Varma 和 R. Simon。使用交叉验证进行模型选择时的误差估计偏差。BMC bioinformatics,7(1):91,2006)。
在嵌套交叉验证中,我们有一个外部 k 折叠交叉验证循环将数据分为训练和测试折叠,并使用一个内部循环在训练上使用 k 折叠交叉验证选择模型 折叠。 选择模型后,然后使用测试倍数评估模型的性能。 下图说明了具有五个外部折叠和两个内部折叠的嵌套交叉验证的概念,这对于计算性能很重要的大型数据集很有用; 这种特殊的嵌套交叉验证类型也称为 5x2 交叉验证:
在 scikit-learn 中,我们可以执行嵌套的交叉验证,如下所示:
>>> gs = GridSearchCV(estimator=pipe_svc,
... param_grid=param_grid,
... scoring='accuracy',
... cv=2,
... n_jobs=-1)
>>> scores = cross_val_score(gs, X_train, y_train, scoring='accuracy', cv=5)
>>> print('CV accuracy: %.3f +/- %.3f' % (
... np.mean(scores), np.std(scores)))
CV accuracy: 0.965 +/- 0.025
返回的平均交叉验证准确性为我们提供了一个很好的估计,即如果我们调整模型的超参数然后将其用于看不见的数据,该期望什么。 例如,我们可以使用嵌套交叉验证方法将 SVM 模型与简单的决策树分类器进行比较。 为简单起见,我们仅调整其 depth 参数:
>>> from sklearn.tree import DecisionTreeClassifier
>>> gs = GridSearchCV(
... estimator=DecisionTreeClassifier(random_state=0),
... param_grid=[
... {'max_depth': [1, 2, 3, 4, 5, 6, 7, None]}],
... scoring='accuracy',
... cv=5)
>>> scores = cross_val_score(gs,
... X_train,
... y_train,
... scoring='accuracy',
... cv=2)
>>> print('CV accuracy: %.3f +/- %.3f' % (
... np.mean(scores), np.std(scores)))
CV accuracy: 0.921 +/- 0.029
正如我们在这里看到的那样,SVM 模型的嵌套交叉验证性能(97.8%)明显优于决策树的性能(90.8%)。 因此,我们希望这可能是对来自与该特定数据集相同总体的新数据进行分类的更好的选择。
查看不同的绩效评估指标
在的前面的章节中,我们使用模型准确性评估了模型,该准确性是量化模型总体性能的有用度量。 但是,还有一些其他性能指标可用于测量模型的相关性,例如精度,召回和 F1 得分。
读取混淆矩阵
在进入不同评分指标的细节之前,让我们打印一个所谓的混淆矩阵,该矩阵列出学习算法的性能。 混淆矩阵只是一个正方形矩阵,它报告真阳性,真阴性,假阳性和假阴性的计数 分类器的预测,如下图所示:
尽管可以通过比较真实和预测的类标签轻松地手动计算这些指标,但是 scikit-learn 提供了便捷的confusion_matrix函数,我们可以按以下方式使用它:
>>> from sklearn.metrics import confusion_matrix
>>> pipe_svc.fit(X_train, y_train)
>>> y_pred = pipe_svc.predict(X_test)
>>> confmat = confusion_matrix(y_true=y_test, y_pred=y_pred)
>>> print(confmat)
[[71 1]
[ 2 40]]
执行前面的代码后返回的数组为我们提供了有关测试数据集上分类器不同类型错误的信息,我们可以使用 matplotlib 的matshow函数将其映射到上图中的混淆矩阵图中:
>>> fig, ax = plt.subplots(figsize=(2.5, 2.5))
>>> ax.matshow(confmat, cmap=plt.cm.Blues, alpha=0.3)
>>> for i in range(confmat.shape[0]):
... for j in range(confmat.shape[1]):
... ax.text(x=j, y=i,
... s=confmat[i, j],
... va='center', ha='center')
>>> plt.xlabel('predicted label')
>>> plt.ylabel('true label')
>>> plt.show()
现在,如下所示的混淆矩阵图应该使结果更易于解释:
假设在此示例中类别 1(恶性)为阳性类别,我们的模型分别正确分类了属于类别 0 的样本 71 个(真阴性)和属于类别 1 的 40 个样本(真阳性)。 但是,我们的模型还将 0 类中的 1 个样本错误地错误分类为 1 类(假阳性),并且尽管它是恶性肿瘤(假阴性),但它预测 2 个样本是良性的。 在下一节中,我们将学习如何使用此信息来计算各种不同的错误度量。
优化分类模型的精度和召回率
预测误差(ERR)和精度(ACC) 许多样本被错误分类。 可以将错误理解为所有错误预测的总和除以总预测的数目,准确度是计算为正确预测的总和除以预测的总数目:
然后可以直接从误差中计算出预测精度:
真阳性率(TPR)和假阳性率(FPR)是特别重要的性能指标 对于班级不平衡问题很有用:
例如,在肿瘤诊断中,我们更加关注恶性肿瘤的检测,以帮助患者进行适当的治疗。 但是,减少不必要分类为恶性肿瘤(假阳性)的良性肿瘤的数量也很重要,以免不必要地引起患者的注意。 与 FPR 相比,真实的阳性率提供了有关阳性(或相关)样本中被正确识别出的阳性总数(P)的分数的有用信息。
精度(PRE)和召回(REC)是与那些真实阳性和 真正的负税率,实际上,回忆是真正的正税率的代名词:
实际上,通常会结合使用精度和查全率,即所谓的 F1 分数:
这些评分指标都在 scikit-learn 中实现,可以从sklearn.metrics模块导入,如以下代码片段所示:
>>> from sklearn.metrics import precision_score
>>> from sklearn.metrics import recall_score, f1_score
>>> print('Precision: %.3f' % precision_score(
... y_true=y_test, y_pred=y_pred))
Precision: 0.976
>>> print('Recall: %.3f' % recall_score(
... y_true=y_test, y_pred=y_pred))
Recall: 0.952
>>> print('F1: %.3f' % f1_score(
... y_true=y_test, y_pred=y_pred))
F1: 0.964
此外,我们可以通过评分参数使用除GridSearch中的准确性以外的其他评分指标。 可在这个页面上找到评分参数接受的不同值的完整列表。
请记住,scikit-learn 中的正类是标记为类 1 的类。如果我们要指定不同的正标签,我们可以通过make_scorer函数构造自己的得分器, 然后可以直接在GridSearchCV中将 scoring 参数作为参数提供:
>>> from sklearn.metrics import make_scorer, f1_score
>>> scorer = make_scorer(f1_score, pos_label=0)
>>> gs = GridSearchCV(estimator=pipe_svc,
... param_grid=param_grid,
... scoring=scorer,
... cv=10)
绘制接收机工作特性
接收器操作员特征(ROC)图是有用的工具,可根据其相对于假阳性率和真阳性率的性能来选择分类模型,这些值是通过移位计算得出的 分类器的决策阈值。 ROC 图的对角线可以解释为随机猜测,而低于对角线的分类模型被认为比随机猜测更糟。 理想的分类器将落在图的左上角,其真率为 1,假率为 0。基于 ROC 曲线,我们可以计算曲线下的面积(AUC)表征分类模型的性能。
注意
与 ROC 曲线类似,我们可以为分类器的不同概率阈值计算精确召回曲线。 scikit-learn 中还实现了绘制这些精确调用曲线的功能,并记录在这个页面中。
通过执行以下代码示例,我们将绘制仅使用威斯康星州乳腺癌数据集中的两个特征来预测肿瘤是良性还是恶性的分类器的 ROC 曲线。 尽管我们将使用与先前定义的逻辑回归管道,但对于分类器来说,使分类任务更具挑战性,从而使生成的 ROC 曲线在视觉上变得更加有趣。 出于类似的原因,我们还将StratifiedKFold验证程序中的折叠数减少为三。 代码如下:
>>> from sklearn.metrics import roc_curve, auc
>>> from scipy import interp
>>> pipe_lr = Pipeline([('scl', StandardScaler()),
... ('pca', PCA(n_components=2)),
... ('clf', LogisticRegression(penalty='l2',
... random_state=0,
... C=100.0))])
>>> X_train2 = X_train[:, [4, 14]]
>>> cv = StratifiedKFold(y_train,
... n_folds=3,
... random_state=1)
>>> fig = plt.figure(figsize=(7, 5))
>>> mean_tpr = 0.0
>>> mean_fpr = np.linspace(0, 1, 100)
>>> all_tpr = []
>>> for i, (train, test) in enumerate(cv):
... probas = pipe_lr.fit(X_train2[train], >>> y_train[train]).predict_proba(X_train2[test])
... fpr, tpr, thresholds = roc_curve(y_train[test],
... probas[:, 1],
... pos_label=1)
... mean_tpr += interp(mean_fpr, fpr, tpr)
... mean_tpr[0] = 0.0
... roc_auc = auc(fpr, tpr)
... plt.plot(fpr,
... tpr,
... lw=1,
... label='ROC fold %d (area = %0.2f)'
... % (i+1, roc_auc))
>>> plt.plot([0, 1],
... [0, 1],
... linestyle='--',
... color=(0.6, 0.6, 0.6),
... label='random guessing')
>>> mean_tpr /= len(cv)
>>> mean_tpr[-1] = 1.0
>>> mean_auc = auc(mean_fpr, mean_tpr)
>>> plt.plot(mean_fpr, mean_tpr, 'k--',
... label='mean ROC (area = %0.2f)' % mean_auc, lw=2)
>>> plt.plot([0, 0, 1],
... [0, 1, 1],
... lw=2,
... linestyle=':',
... color='black',
... label='perfect performance')
>>> plt.xlim([-0.05, 1.05])
>>> plt.ylim([-0.05, 1.05])
>>> plt.xlabel('false positive rate')
>>> plt.ylabel('true positive rate')
>>> plt.title('Receiver Operator Characteristic')
>>> plt.legend(loc="lower right")
>>> plt.show()
在前面的代码示例中,我们使用了 scikit-learn 中已经熟悉的StratifiedKFold类,并使用sklearn.metrics中的roc_curve函数计算了pipe_lr流水线中LogisticRegression分类器的 ROC 性能。 每次迭代分别使用模块。 此外,我们通过从 SciPy 导入的interp函数对三倍平均 ROC 曲线进行插值,并通过auc函数计算了曲线下的面积。 产生的 ROC 曲线表明不同倍数之间存在一定程度的差异,并且平均 ROC AUC(0.75)介于完美分数(1.0)和随机猜测(0.5)之间:
如果仅对 ROC AUC 分数感兴趣,我们也可以直接从sklearn.metrics子模块导入roc_auc_score函数。 以下代码将其拟合到具有两个特征的训练集后,可在独立测试数据集上计算分类器的 ROC AUC 分数:
>>> pipe_lr = pipe_lr.fit(X_train2, y_train)
>>> y_pred2 = pipe_lr.predict(X_test[:, [4, 14]])
>>> from sklearn.metrics import roc_auc_score
>>> from sklearn.metrics import accuracy_score
>>> print('ROC AUC: %.3f' % roc_auc_score(
... y_true=y_test, y_score=y_pred2))
ROC AUC: 0.662
>>> print('Accuracy: %.3f' % accuracy_score(
... y_true=y_test, y_pred=y_pred2))
Accuracy: 0.711
将分类器的性能报告为 ROC AUC 可以针对分类器针对不平衡样本的性能提供进一步的见解。 但是,虽然准确度得分可以解释为 ROC 曲线上的单个分界点,但 AP Bradley 指出,ROC AUC 和准确度指标大多彼此一致(AP Bradley。 机器学习算法评估中 ROC 曲线下的面积。模式识别,30(7):1145-1159,1997)。
多类别分类的评分指标
我们在本节中讨论的评分指标特定于二进制分类系统。 但是,scikit-learn 还实现了宏和微型平均方法,以通过将单项与全部(OvA)分类。 微观平均值是根据系统的各个真实肯定,真实否定,错误肯定和错误否定来计算的。 例如,k 级系统中精度得分的微平均值可以计算如下:
宏平均值可以简单地计算为不同系统的平均分数:
如果我们想对每个实例或预测进行平均加权,则微平均很有用;而对每个类进行宏平均加权,则可以对最频繁使用的类标签评估分类器的整体性能。
如果我们使用二进制性能指标来评估 scikit-learn 中的多类分类模型,则默认情况下使用宏平均值的归一化或加权变体。 在计算平均值时,通过将每个类别标签的得分乘以真实实例的数量进行加权,可以计算出加权宏平均值。 如果我们要处理类不平衡问题,即每个标签的实例数量不同,则加权宏平均值很有用。
对于 scikit-learn 中的多类问题,默认为加权宏平均,但我们可以通过sklearn.metrics模块导入的不同评分函数中的average参数指定平均方法,例如, precision_score或make_scorer功能:
>>> pre_scorer = make_scorer(score_func=precision_score,
... pos_label=1,
... greater_is_better=True,
... average='micro')
四十一、组合不同的模型以便集成学习
在上一章中,我们重点介绍了优化和评估不同分类模型的最佳实践。 在本章中,我们将基于这些技术并探索构建一组分类器的不同方法,这些分类器通常比其任何单个成员具有更好的预测性能。 你将学到如何:
- 根据多数投票做出预测
- 通过绘制带有重复的训练集的随机组合来减少过度拟合
- 从弱小学习者建立强大的模型,从他们的错误中学习
通过合奏学习
集成方法 背后的目标是将不同的分类器组合成一个元分类器,该分类器比单独的每个分类器具有更好的泛化性能。 例如,假设我们收集了 10 位专家的预测,那么集成方法将使我们能够策略性地组合 10 位专家的这些预测,以得出比每位专家的预测更准确,更可靠的预测。 正如我们将在本章稍后看到的那样,有几种不同的方法可以创建分类器集合。 在本节中,我们将介绍有关集成的工作原理以及为什么通常以产生良好的泛化性能而闻名的原因的基本认识。
在本章中,我们将重点介绍使用多数表决原理的最流行的合奏方法。 多数投票只是意味着我们选择大多数分类器预测的分类标签,即获得超过 50%的投票。 严格来说,多数票一词仅指二进制类设置。 但是,很容易将多数表决原则推广到多类别设置,这称为多个表决。 在这里,我们选择获得最多投票(模式)的类别标签。 下图说明了由 10 个分类器组成的集合的多数表决和多数表决的概念,其中每个唯一符号(三角形,正方形和圆形)代表一个唯一的类别标签:
使用训练集合,我们从训练m不同分类器(C[1], ..., C[m])开始。 根据技术的不同,可以根据不同的分类算法(例如决策树,支持向量机,逻辑回归分类器等)构建整体。 或者,我们也可以使用适合训练集不同子集的相同基础分类算法。 这种方法的一个突出示例是随机森林算法,该算法结合了不同的决策树分类器。 下图说明了使用多数投票的一般合奏方法的概念:
为了通过简单多数或多次投票来预测类别标签,我们将每个分类器C[j]的预测类别标签进行组合,然后选择获得最多投票的类别标签y_hat:
例如,在class1 = -1和class2 = +1的二进制分类任务中,我们可以编写如下的多数投票预测:
为了说明,为什么集成方法比单独的分类器可以更好地工作,让我们应用组合器的简单概念。 对于以下示例,我们假设二进制分类任务的所有n个基本分类器均具有相同的错误率ε。 此外,我们假设分类器是独立的,并且错误率不相关。 在这些假设下,我们可以简单地将一组基本分类器的错误概率表示为二项式分布的概率质量函数:
此处,C(n, k)是二项式系数 n 选择 k。 换句话说,我们计算整体预测错误的概率。 现在,让我们看一下 11 个基本分类器(n = 11)的更具体示例,错误率为 0.25(ε = 0.25):
如我们所见,如果满足所有假设,则集合的错误率(0.034)远低于每个单独分类器的错误率(0.25)。 注意,在此简化图示中,将 50-50 除以偶数个分类器n视为错误,而这只有一半的时间是真实的。 为了在一系列不同的基本错误率上将这种理想的整体分类器与基本分类器进行比较,让我们在 Python 中实现概率质量函数:
>>> from scipy.misc import comb
>>> import math
>>> def ensemble_error(n_classifier, error):
... k_start = math.ceil(n_classifier / 2.0)
... probs = [comb(n_classifier, k) *
... error**k *
... (1-error)**(n_classifier - k)
... for k in range(k_start, n_classifier + 1)]
... return sum(probs)
>>> ensemble_error(n_classifier=11, error=0.25)
0.034327507019042969
在实现ensemble_error函数之后,我们可以计算范围从 0.0 到 1.0 的不同基础误差的整体误差率,以在折线图中可视化整体和基础误差之间的关系:
>>> import numpy as np
>>> error_range = np.arange(0.0, 1.01, 0.01)
>>> ens_errors = [ensemble_error(n_classifier=11, error=error)
... for error in error_range]
>>> import matplotlib.pyplot as plt
>>> plt.plot(error_range, ens_errors,
... label='Ensemble error',
... linewidth=2)
>>> plt.plot(error_range, error_range,
... linestyle='--', label='Base error',
... linewidth=2)
>>> plt.xlabel('Base error')
>>> plt.ylabel('Base/Ensemble error')
>>> plt.legend(loc='upper left')
>>> plt.grid()
>>> plt.show()
正如我们在结果图中看到的那样,只要基本分类器的性能优于随机猜测(ε < 0.25),则集成的错误概率总是比单个基本分类器的错误概率要好。 请注意,y轴描述了基本误差(虚线)以及整体误差(实线):
实现简单的多数投票分类器
在上一节对集成学习的简短介绍之后,让我们开始进行热身练习,并为 Python 中的多数投票实现一个简单的集成分类器。 尽管以下算法也可以通过复数投票将其推广到多类别设置,但为简便起见,我们将使用术语多数投票,这在文献中也经常这样做。
我们将要实现的算法将使我们能够将与各个权重相关联的不同分类算法组合在一起,以提高置信度。 我们的目标是建立一个更强大的元分类器,以平衡特定数据集上各个分类器的弱点。 用更精确的数学术语,我们可以编写加权多数投票,如下所示:
在这里,w[j]是与基本分类器关联的权重,C[j],y_hat是集合的预测类别标签,χ[A](希腊语 chi )是特征函数C[j](x) = i ∈ A和A是唯一类标签的集合。 对于相等的权重,我们可以简化此等式并将其编写如下:
为了更好地理解加权的概念,我们现在来看一个更具体的示例。 假设我们有三个基本分类器C[j](j ∈ {0, 1})的集合,并且要预测给定样本实例的类别标签x。三个基本分类器中有两个预测类别标签 0, 一个C[3]预测该样本属于 1 类。如果我们对每个基本分类器的预测进行平均加权,则多数投票将预测该样本属于 0 类:
现在让我们分别为C[3]分配 0.6 的权重,并为C[1]和C[2]分配权重 0.2 的系数。
直观地讲,更多,因为3 x 0.2 = 0.6可以说,C[3]的预测权重分别比C[1]或C[2]的预测权重高三倍。 我们可以这样写:
要将加权多数投票的概念转换为 Python 代码,我们可以使用 NumPy 方便的argmax和bincount函数:
>>> import numpy as np
>>> np.argmax(np.bincount([0, 0, 1],
... weights=[0.2, 0.2, 0.6]))
1
如第 3 章和“使用 Scikit-learn” 进行的机器学习分类器之旅中所述,scikit-learn 中的某些分类器还可以通过predict_proba返回预测的类标签的概率。 方法。 如果对我们集成中的分类器进行了很好的校准,则使用预测的分类概率代替多数投票的分类标签将很有用。 用于根据概率预测类别标签的多数投票的修改版本可以写成:
在此,p[ij]是分类标签i的第 jth 个分类器的预测概率。
继续前面的示例,我们假设我们有一个带有类别标签i ∈ {0, 1}和三个分类器C[j](j ∈ {1, 2, 3})的集合的二进制分类问题。 假设分类器C[j]针对特定样本x返回以下类成员资格概率:
然后,我们可以如下计算各个类别的概率:
为了实现基于类概率的加权多数投票,我们可以再次使用numpy.average和np.argmax使用 NumPy:
>>> ex = np.array([[0.9, 0.1],
... [0.8, 0.2],
... [0.4, 0.6]])
>>> p = np.average(ex, axis=0, weights=[0.2, 0.2, 0.6])
>>> p
array([ 0.58, 0.42])
>>> np.argmax(p)
0
将所有放在一起,现在让我们在 Python 中实现一个MajorityVoteClassifier:
from sklearn.base import BaseEstimator
from sklearn.base import ClassifierMixin
from sklearn.preprocessing import LabelEncoder
from sklearn.externals import six
from sklearn.base import clone
from sklearn.pipeline import _name_estimators
import numpy as np
import operator
class MajorityVoteClassifier(BaseEstimator,
ClassifierMixin):
""" A majority vote ensemble classifier
Parameters
----------
classifiers : array-like, shape = [n_classifiers]
Different classifiers for the ensemble
vote : str, {'classlabel', 'probability'}
Default: 'classlabel'
If 'classlabel' the prediction is based on
the argmax of class labels. Else if
'probability', the argmax of the sum of
probabilities is used to predict the class label
(recommended for calibrated classifiers).
weights : array-like, shape = [n_classifiers]
Optional, default: None
If a list of `int` or `float` values are
provided, the classifiers are weighted by
importance; Uses uniform weights if `weights=None`.
"""
def __init__(self, classifiers,
vote='classlabel', weights=None):
self.classifiers = classifiers
self.named_classifiers = {key: value for
key, value in
_name_estimators(classifiers)}
self.vote = vote
self.weights = weights
def fit(self, X, y):
""" Fit classifiers.
Parameters
----------
X : {array-like, sparse matrix},
shape = [n_samples, n_features]
Matrix of training samples.
y : array-like, shape = [n_samples]
Vector of target class labels.
Returns
-------
self : object
"""
# Use LabelEncoder to ensure class labels start
# with 0, which is important for np.argmax
# call in self.predict
self.lablenc_ = LabelEncoder()
self.lablenc_.fit(y)
self.classes_ = self.lablenc_.classes_
self.classifiers_ = []
for clf in self.classifiers:
fitted_clf = clone(clf).fit(X,
self.lablenc_.transform(y))
self.classifiers_.append(fitted_clf)
return self
我在代码中添加了很多注释,以更好地理解各个部分。 但是,在实现其余方法之前,让我们先休息一下,讨论一些乍一看可能令人困惑的代码。 我们使用父类BaseEstimator和ClassifierMixin免费获得了一些基本功能*,包括用于设置和返回分类器参数以及score的方法get_params和set_params 分别计算预测精度的方法。 还要注意,我们导入了six以使MajorityVoteClassifier与 Python 2.7 兼容。*
接下来,如果我们使用vote='classlabel'初始化新的MajorityVoteClassifier对象,则将添加predict方法以基于类标签通过多数表决来预测类标签。 或者,我们将能够使用vote='probability'初始化整体分类器,以根据类成员资格概率预测类标签。 此外,我们还将添加predict_proba方法以返回平均概率,这对于计算曲线(ROC AUC)下的接收机操作员特征区域很有用。
def predict(self, X):
""" Predict class labels for X.
Parameters
----------
X : {array-like, sparse matrix},
Shape = [n_samples, n_features]
Matrix of training samples.
Returns
----------
maj_vote : array-like, shape = [n_samples]
Predicted class labels.
"""
if self.vote == 'probability':
maj_vote = np.argmax(self.predict_proba(X),
axis=1)
else: # 'classlabel' vote
# Collect results from clf.predict calls
predictions = np.asarray([clf.predict(X)
for clf in
self.classifiers_]).T
maj_vote = np.apply_along_axis(
lambda x:
np.argmax(np.bincount(x,
weights=self.weights)),
axis=1,
arr=predictions)
maj_vote = self.lablenc_.inverse_transform(maj_vote)
return maj_vote
def predict_proba(self, X):
""" Predict class probabilities for X.
Parameters
----------
X : {array-like, sparse matrix},
shape = [n_samples, n_features]
Training vectors, where n_samples is
the number of samples and
n_features is the number of features.
Returns
----------
avg_proba : array-like,
shape = [n_samples, n_classes]
Weighted average probability for
each class per sample.
"""
probas = np.asarray([clf.predict_proba(X)
for clf in self.classifiers_])
avg_proba = np.average(probas,
axis=0, weights=self.weights)
return avg_proba
def get_params(self, deep=True):
""" Get classifier parameter names for GridSearch"""
if not deep:
return super(MajorityVoteClassifier,
self).get_params(deep=False)
else:
out = self.named_classifiers.copy()
for name, step in\
six.iteritems(self.named_classifiers):
for key, value in six.iteritems(
step.get_params(deep=True)):
out['%s__%s' % (name, key)] = value
return out
另外,请注意,我们定义了自己的get_params方法的修改版本以使用_name_estimators函数,以便访问集合中各个分类器的参数。 乍一看,这可能看起来有些复杂,但是当我们在稍后的部分中使用网格搜索进行超参数调整时,这将非常有意义。
注意
尽管我们的MajorityVoteClassifier实现对于演示非常有用,但我还在 scikit-learn 中实现了多数表决分类器的更高级版本。 在下一发行版(v0.17)中将以sklearn.ensemble.VotingClassifier的形式提供。
结合不同的分类算法和多数票
现在是时候,将我们在上一节中实现的MajorityVoteClassifier付诸实践。 但是首先,让我们准备一个可以测试的数据集。 由于我们已经熟悉从 CSV 文件加载数据集的技术,因此,我们将采取捷径并从 scikit-learn 的数据集模块中加载 Iris 数据集。 此外,我们将仅选择萼片宽度 和花瓣长度这两个特征,以使分类任务更具挑战性。 尽管我们的MajorityVoteClassifier泛化为多类问题,但我们仅将 Iris-Versicolor 和 Iris-Virginica 这两个类别的花朵样本进行分类,以计算 ROC AUC。 代码如下:
>>> from sklearn import datasets
>>> from sklearn.cross_validation import train_test_split
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.preprocessing import LabelEncoder
>>> iris = datasets.load_iris()
>>> X, y = iris.data[50:, [1, 2]], iris.target[50:]
>>> le = LabelEncoder()
>>> y = le.fit_transform(y)
注意
请注意,scikit-learn 使用predict_proba方法(如果适用)来计算 ROC AUC 分数。 在第 3 章和“使用 Scikit-learn” 的机器学习分类器中,我们看到了如何在逻辑回归模型中计算类概率。 在决策树中,概率是根据在训练时为每个节点创建的频率向量计算的。 该向量收集从该节点处的类标签分布计算出的每个类标签的频率值。 然后对频率进行归一化,使它们的总和为 1。类似地,将 k 个近邻的类别标签聚合在一起,以在 k 近邻算法中返回归一化的类别标签频率。 尽管决策树和 k 最近邻分类器返回的归一化概率看起来与从 Logistic 回归模型获得的概率相似,但我们必须意识到,这些概率实际上并非来自概率质量函数。
接下来,我们将虹膜样本分成 50%的训练和 50%的测试数据:
>>> X_train, X_test, y_train, y_test =\
... train_test_split(X, y,
... test_size=0.5,
... random_state=1)
使用训练数据集,我们现在将训练三个不同的分类器(逻辑回归分类器,决策树分类器和 k 最近邻分类器),并通过对训练数据集进行 10 倍交叉验证来查看它们的个人表现, 我们将它们组合成一个整体分类器:
>>> from sklearn.cross_validation import cross_val_score
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn.neighbors import KNeighborsClassifier
>>> from sklearn.pipeline import Pipeline
>>> import numpy as np
>>> clf1 = LogisticRegression(penalty='l2',
... C=0.001,
... random_state=0)
>>> clf2 = DecisionTreeClassifier(max_depth=1,
... criterion='entropy',
... random_state=0)
>>> clf3 = KNeighborsClassifier(n_neighbors=1,
... p=2,
... metric='minkowski')
>>> pipe1 = Pipeline([['sc', StandardScaler()],
... ['clf', clf1]])
>>> pipe3 = Pipeline([['sc', StandardScaler()],
... ['clf', clf3]])
>>> clf_labels = ['Logistic Regression', 'Decision Tree', 'KNN']
>>> print('10-fold cross validation:\n')
>>> for clf, label in zip([pipe1, clf2, pipe3], clf_labels):
... scores = cross_val_score(estimator=clf,
>>> X=X_train,
>>> y=y_train,
>>> cv=10,
>>> scoring='roc_auc')
>>> print("ROC AUC: %0.2f (+/- %0.2f) [%s]"
... % (scores.mean(), scores.std(), label))
如以下代码段所示,我们收到的输出表明各个分类器的预测性能几乎相等:
10-fold cross validation:
ROC AUC: 0.92 (+/- 0.20) [Logistic Regression]
ROC AUC: 0.92 (+/- 0.15) [Decision Tree]
ROC AUC: 0.93 (+/- 0.10) [KNN]
您可能想知道为什么我们训练逻辑回归和 k 最近邻分类器作为管道的一部分。 其背后的原因是,如第 3 章和“使用 Scikit-learn” 进行的机器学习分类器之旅中所述,逻辑回归和 k 最近邻算法(使用欧几里得 距离度量)与决策树相比,其比例不变。 尽管所有虹膜特征都是在相同的比例尺(cm)上测量的,但是使用标准化特征是一个好习惯。
现在,让我们继续进行更令人兴奋的部分,并在我们的MajorityVoteClassifier中结合各个分类器进行多数规则投票:
>>> mv_clf = MajorityVoteClassifier(
... classifiers=[pipe1, clf2, pipe3])
>>> clf_labels += ['Majority Voting']
>>> all_clf = [pipe1, clf2, pipe3, mv_clf]
>>> for clf, label in zip(all_clf, clf_labels):
... scores = cross_val_score(estimator=clf,
... X=X_train,
... y=y_train,
... cv=10,
... scoring='roc_auc')
... print("Accuracy: %0.2f (+/- %0.2f) [%s]"
... % (scores.mean(), scores.std(), label))
ROC AUC: 0.92 (+/- 0.20) [Logistic Regression]
ROC AUC: 0.92 (+/- 0.15) [Decision Tree]
ROC AUC: 0.93 (+/- 0.10) [KNN]
ROC AUC: 0.97 (+/- 0.10) [Majority Voting]
如我们所见,在 10 倍交叉验证评估中,MajorityVotingClassifier的性能大大优于单个分类器。
评估和调整集成分类器
在本节中,我们将根据测试集计算 ROC 曲线,以检查MajorityVoteClassifier是否能很好地推广到看不见的数据。 我们应该记住,测试集不能用于模型选择; 它的唯一目的是报告分类器系统的泛化性能的无偏估计。 代码如下:
>>> from sklearn.metrics import roc_curve
>>> from sklearn.metrics import auc
>>> colors = ['black', 'orange', 'blue', 'green']
>>> linestyles = [':', '--', '-.', '-']
>>> for clf, label, clr, ls \
... in zip(all_clf, clf_labels, colors, linestyles):
... # assuming the label of the positive class is 1
... y_pred = clf.fit(X_train,
... y_train).predict_proba(X_test)[:, 1]
... fpr, tpr, thresholds = roc_curve(y_true=y_test,
... y_score=y_pred)
... roc_auc = auc(x=fpr, y=tpr)
... plt.plot(fpr, tpr,
... color=clr,
... linestyle=ls,
... label='%s (auc = %0.2f)' % (label, roc_auc))
>>> plt.legend(loc='lower right')
>>> plt.plot([0, 1], [0, 1],
... linestyle='--',
... color='gray',
... linewidth=2)
>>> plt.xlim([-0.1, 1.1])
>>> plt.ylim([-0.1, 1.1])
>>> plt.grid()
>>> plt.xlabel('False Positive Rate')
>>> plt.ylabel('True Positive Rate')
>>> plt.show()
正如我们在生成的 ROC 中看到的那样,集成分类器在测试集上的效果也不错( ROC AUC = 0.95 ),而 k 最近邻分类器似乎过度拟合了训练数据(训练 ROC AUC = 0.93,测试 ROC AUC = 0.86 ):
由于我们仅选择作为分类示例,因此很有趣的是查看集合分类器的决策区域实际上是什么样的。 尽管在模型拟合之前不必标准化训练功能,因为我们的逻辑回归和 k 最近邻管道会自动处理此问题,但我们将标准化训练集,以便决策树的决策区域位于 出于视觉目的相同的比例。 代码如下:
>>> sc = StandardScaler()
>>> X_train_std = sc.fit_transform(X_train)
>>> from itertools import product
>>> x_min = X_train_std[:, 0].min() - 1
>>> x_max = X_train_std[:, 0].max() + 1
>>> y_min = X_train_std[:, 1].min() - 1
>>> y_max = X_train_std[:, 1].max() + 1
>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
... np.arange(y_min, y_max, 0.1))
>>> f, axarr = plt.subplots(nrows=2, ncols=2,
... sharex='col',
... sharey='row',
... figsize=(7, 5))
>>> for idx, clf, tt in zip(product([0, 1], [0, 1]),
... all_clf, clf_labels):
... clf.fit(X_train_std, y_train)
... Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
... Z = Z.reshape(xx.shape)
... axarr[idx[0], idx[1]].contourf(xx, yy, Z, alpha=0.3)
... axarr[idx[0], idx[1]].scatter(X_train_std[y_train==0, 0],
... X_train_std[y_train==0, 1],
... c='blue',
... marker='^',
... s=50)
... axarr[idx[0], idx[1]].scatter(X_train_std[y_train==1, 0],
... X_train_std[y_train==1, 1],
... c='red',
... marker='o',
... s=50)
... axarr[idx[0], idx[1]].set_title(tt)
>>> plt.text(-3.5, -4.5,
... s='Sepal width [standardized]',
... ha='center', va='center', fontsize=12)
>>> plt.text(-10.5, 4.5,
... s='Petal length [standardized]',
... ha='center', va='center',
... fontsize=12, rotation=90)
>>> plt.show()
有趣的是,但也正如预期的那样,集成分类器的决策区域似乎是各个分类器的决策区域的混合体。 乍一看,多数表决决策边界看起来很像 k 近邻分类器的决策边界。 但是,我们可以看到它与sepal_width >= 1的y轴正交,就像决策树树桩一样:
在您学习如何调整单个分类器参数以进行整体分类之前,让我们调用get_params方法以基本了解如何访问GridSearch对象内的单个参数:
>>> mv_clf.get_params()
{'decisiontreeclassifier': DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=1,
max_features=None, max_leaf_nodes=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
random_state=0, splitter='best'),
'decisiontreeclassifier__class_weight': None,
'decisiontreeclassifier__criterion': 'entropy',
[...]
'decisiontreeclassifier__random_state': 0,
'decisiontreeclassifier__splitter': 'best',
'pipeline-1': Pipeline(steps=[('sc', StandardScaler(copy=True, with_mean=True, with_std=True)), ('clf', LogisticRegression(C=0.001, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr',
penalty='l2', random_state=0, solver='liblinear', tol=0.0001,
verbose=0))]),
'pipeline-1__clf': LogisticRegression(C=0.001, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr',
penalty='l2', random_state=0, solver='liblinear', tol=0.0001,
verbose=0),
'pipeline-1__clf__C': 0.001,
'pipeline-1__clf__class_weight': None,
'pipeline-1__clf__dual': False,
[...]
'pipeline-1__sc__with_std': True,
'pipeline-2': Pipeline(steps=[('sc', StandardScaler(copy=True, with_mean=True, with_std=True)), ('clf', KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_neighbors=1, p=2, weights='uniform'))]),
'pipeline-2__clf': KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_neighbors=1, p=2, weights='uniform'),
'pipeline-2__clf__algorithm': 'auto',
[...]
'pipeline-2__sc__with_std': True}
基于get_params方法返回的值,我们现在知道如何访问各个分类器的属性。 现在,我们通过网格搜索调整逻辑回归分类器的逆正则化参数C和决策树深度,以进行演示。 代码如下:
>>> from sklearn.grid_search import GridSearchCV
>>> params = {'decisiontreeclassifier__max_depth': [1, 2],
... 'pipeline-1__clf__C': [0.001, 0.1, 100.0]}
>>> grid = GridSearchCV(estimator=mv_clf,
... param_grid=params,
... cv=10,
... scoring='roc_auc')
>>> grid.fit(X_train, y_train)
网格搜索完成后,我们可以打印不同的超参数值组合和通过 10 倍交叉验证计算的平均 ROC AUC 得分。 代码如下:
>>> for params, mean_score, scores in grid.grid_scores_:
... print("%0.3f+/-%0.2f %r"
... % (mean_score, scores.std() / 2, params))
0.967+/-0.05 {'pipeline-1__clf__C': 0.001, 'decisiontreeclassifier__max_depth': 1}
0.967+/-0.05 {'pipeline-1__clf__C': 0.1, 'decisiontreeclassifier__max_depth': 1}
1.000+/-0.00 {'pipeline-1__clf__C': 100.0, 'decisiontreeclassifier__max_depth': 1}
0.967+/-0.05 {'pipeline-1__clf__C': 0.001, 'decisiontreeclassifier__max_depth': 2}
0.967+/-0.05 {'pipeline-1__clf__C': 0.1, 'decisiontreeclassifier__max_depth': 2}
1.000+/-0.00 {'pipeline-1__clf__C': 100.0, 'decisiontreeclassifier__max_depth': 2}
>>> print('Best parameters: %s' % grid.best_params_)
Best parameters: {'pipeline-1__clf__C': 100.0, 'decisiontreeclassifier__max_depth': 1}
>>> print('Accuracy: %.2f' % grid.best_score_)
Accuracy: 1.00
如我们所见,当我们选择较低的正则化强度(C = 100.0)时,我们会获得最佳的交叉验证结果,而树的深度似乎根本不会影响性能,这表明了一个决定 树桩足以分隔数据。 为了提醒自己,多次使用测试数据集进行模型评估是一种不好的做法,在本节中,我们将不估计已调整超参数的泛化性能。 我们将迅速转向集成学习的另一种方法:套袋。
注意
我们在本节中实现的多数表决方法有时也称为堆叠。 但是,堆叠算法通常与逻辑回归模型结合使用,该逻辑回归模型使用集合中各个分类器的预测作为输入来预测最终分类标签,这已由 DH Wolpert 的 David H. Wolpert 进行了更详细的描述。 。 堆叠概括。 神经网络,5(2):241-259,1992 年。
套袋–从引导程序样本构建分类器集合
套袋是的一种整体学习技术,与我们在上一节中实现的MajorityVoteClassifier紧密相关,如下图所示:
但是,我们没有使用相同的训练集来适合集合中的各个分类器,而是从初始训练集中绘制了引导样本(带有替换的随机样本),这就是为什么袋装也称为引导聚集。 为了提供有关引导过程的更具体示例,让我们考虑下图所示的示例。 在这里,我们有七个不同的训练实例(表示为索引 1-7),它们在每轮装袋中都随机抽样替换。 然后,每个引导程序样本都用于拟合分类器C[j],该分类器通常是未修剪的决策树:
套袋与也与我们在第 3 章,“使用 Scikit-learn” 的机器学习分类器介绍中引入的随机森林分类器有关。 实际上,随机森林是装袋的一种特殊情况,在这种情况下,我们还使用随机特征子集来拟合各个决策树。 套袋是 Leo Breiman 在 1994 年的一份技术报告中首次提出的; 他还表明,套袋可以提高不稳定模型的准确性,并减少过度拟合的程度。 我强烈建议您阅读有关他在布莱曼(L. Breiman)的研究。 套袋预测器。 机器学习,24(2):123–140,1996,可以免费在线在线获取有关装袋的更多信息。
为了了解实际情况,让我们使用我们在第 4 章,“建立良好的训练集–数据预处理”中引入的 Wine 数据集创建一个更复杂的分类问题。 在这里,我们只考虑 Wine 类 2 和 3,我们选择两个功能:酒精和色相。
>>> import pandas as pd
>>> df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)
>>> df_wine.columns = ['Class label', 'Alcohol',
... 'Malic acid', 'Ash',
... 'Alcalinity of ash',
... 'Magnesium', 'Total phenols',
... 'Flavanoids', 'Nonflavanoid phenols',
... 'Proanthocyanins',
... 'Color intensity', 'Hue',
... 'OD280/OD315 of diluted wines',
... 'Proline']
>>> df_wine = df_wine[df_wine['Class label'] != 1]
>>> y = df_wine['Class label'].values
>>> X = df_wine[['Alcohol', 'Hue']].values
接下来,我们将类标签编码为二进制格式,并将数据集分别分为 60%训练和 40%测试集:
>>> from sklearn.preprocessing import LabelEncoder
>>> from sklearn.cross_validation import train_test_split
>>> le = LabelEncoder()
>>> y = le.fit_transform(y)
>>> X_train, X_test, y_train, y_test =\
... train_test_split(X, y,
... test_size=0.40,
... random_state=1)
scikit-learn 中已经实现了BaggingClassifier算法,我们可以从ensemble子模块中导入该算法。 在这里,我们将使用未修剪的决策树作为基础分类器,并在训练数据集的不同引导样本上创建一个由 500 个决策树组成的集合:
>>> from sklearn.ensemble import BaggingClassifier
>>> tree = DecisionTreeClassifier(criterion='entropy',
... max_depth=None,
... random_state=1)
>>> bag = BaggingClassifier(base_estimator=tree,
... n_estimators=500,
... max_samples=1.0,
... max_features=1.0,
... bootstrap=True,
... bootstrap_features=False,
... n_jobs=1,
... random_state=1)
接下来,我们将在训练和测试数据集上计算预测的准确性得分,以将装袋分类器的性能与单个未修剪的决策树的性能进行比较:
>>> from sklearn.metrics import accuracy_score
>>> tree = tree.fit(X_train, y_train)
>>> y_train_pred = tree.predict(X_train)
>>> y_test_pred = tree.predict(X_test)
>>> tree_train = accuracy_score(y_train, y_train_pred)
>>> tree_test = accuracy_score(y_test, y_test_pred)
>>> print('Decision tree train/test accuracies %.3f/%.3f'
... % (tree_train, tree_test))
Decision tree train/test accuracies 1.000/0.833
根据我们通过执行前面的代码段打印的精度值,未修剪的决策树会正确预测训练样本的所有类别标签; 但是,实质上较低的测试准确性表明该模型具有较高的方差(过度拟合):
>>> bag = bag.fit(X_train, y_train)
>>> y_train_pred = bag.predict(X_train)
>>> y_test_pred = bag.predict(X_test)
>>> bag_train = accuracy_score(y_train, y_train_pred)
>>> bag_test = accuracy_score(y_test, y_test_pred)
>>> print('Bagging train/test accuracies %.3f/%.3f'
... % (bag_train, bag_test))
Bagging train/test accuracies 1.000/0.896
尽管决策树和装袋分类器的训练精度在训练集上都相似(均为 1.0),但我们可以看到,装袋分类器具有比测试集更高的泛化性能。 接下来,让我们比较决策树和装袋分类器之间的决策区域:
>>> x_min = X_train[:, 0].min() - 1
>>> x_max = X_train[:, 0].max() + 1
>>> y_min = X_train[:, 1].min() - 1
>>> y_max = X_train[:, 1].max() + 1
>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
... np.arange(y_min, y_max, 0.1))
>>> f, axarr = plt.subplots(nrows=1, ncols=2,
... sharex='col',
... sharey='row',
... figsize=(8, 3))
>>> for idx, clf, tt in zip([0, 1],
... [tree, bag],
... ['Decision Tree', 'Bagging']):
... clf.fit(X_train, y_train)
...
... Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
... Z = Z.reshape(xx.shape)
... axarr[idx].contourf(xx, yy, Z, alpha=0.3)
... axarr[idx].scatter(X_train[y_train==0, 0],
... X_train[y_train==0, 1],
... c='blue', marker='^')
... axarr[idx].scatter(X_train[y_train==1, 0],
... X_train[y_train==1, 1],
... c='red', marker='o')
... axarr[idx].set_title(tt)
>>> axarr[0].set_ylabel(Alcohol', fontsize=12)
>>> plt.text(10.2, -1.2,
... s=Hue',
... ha='center', va='center', fontsize=12)
>>> plt.show()
正如我们在结果图中看到的所示,在装袋集合中,三节点深度决策树的分段线性决策边界看起来更平滑:
在本节中,我们仅看一个非常简单的装袋示例。 实际上,更复杂的分类任务和数据集的高维性很容易导致单个决策树的过度拟合,而这正是装袋算法可以真正发挥其优势的地方。 最后,我们将注意到装袋算法可能是减少模型方差的有效方法。 但是,套袋不能有效地减少模型偏差,这就是的原因,我们想要选择低偏差的分类器集合,例如未修剪的决策树。
通过自适应提升来利用弱势学习者
在本节中有关集成方法的部分,我们将讨论提升,特别关注其最常见的实现,AdaBoost([ Adaptive Boosting)。
注意
AdaBoost 背后的最初想法是由 Robert Schapire 在 1990 年提出的(R. E. Schapire。弱学习能力。机器学习,5(2):197–227,1990)。 Robert Schapire 和 Yoav Freund 在第十三届国际会议论文集(ICML 1996)中提出 AdaBoost 算法后,AdaBoost 成为随后几年中使用最广泛的集成方法之一(Y. Freund,RE Schapire 等人[ 使用新的增强算法进行实验。在 ICML 中,第 96 卷,第 148-156 页,1996 年)。 在 2003 年,Freund 和 Schapire 的开创性工作获得了戈德尔奖,这是计算机科学领域最杰出出版物的著名奖项。
在增强中,合奏由非常简单的基本分类器组成,这些分类器通常也称为弱学习者和,它们在性能上比随机猜测略有优势。 学习者能力弱的一个典型例子是决策树桩。 增强后的关键概念是集中于难以分类的训练样本,即让弱学习者随后从错误分类的训练样本中学习,以提高整体表现。 与装袋(加强的最初公式)相反,该算法使用从训练数据集中抽取的训练样本的随机子集而无需替换。 原始的增强过程分为四个关键步骤,如下所示:
- 抽取训练样本
d[1]的随机子集,而不用从训练集D进行替换来训练弱学习者C[1]。 - 从训练集中抽取第二个随机训练子集
d[2]而不进行替换,并添加 50%先前被错误分类以训练弱学习者的样本C[2]。 - 在训练集
D中找到训练样本d[3],在该训练集上C[1]和C[2]不同意训练第三位弱学习者C[3]。 - 通过多数投票将弱学习者
C[1],C[2]和C[3]合并在一起。
正如 Leo Breiman(L。Breiman。 Bias,Variance 和 Arcing 分类器。1996)所讨论的,与套袋模型相比,提振可以导致偏差和方差的减少。 然而,实际上,诸如 AdaBoost 之类的增强算法也因其高方差而闻名,也就是说,倾向于过度拟合训练数据(G. Raetsch,T。Onoda 和 KR Mueller。 Adaboost 的一种避免方法) 过度拟合,见《国际神经信息处理会议公报》(Citeseer,1998 年)。
与此处所述的原始增强过程相反,AdaBoost 使用完整的训练集来训练弱学习者,在每次迭代中对训练样本进行加权,以建立一个强大的分类器,该学习者从集合中以前的弱学习者的错误中学习。 在深入研究 AdaBoost 算法的具体细节之前,让我们看一下下图以更好地了解 AdaBoost 背后的基本概念:
为了逐步通过 AdaBoost 插图介绍,我们从子图1开始,它代表了针对二进制分类的训练集,其中所有训练样本均被分配了相同的权重。 基于此训练集,我们训练一个决策树桩(以虚线显示),该树桩试图对两个类别(三角形和圆形)的样本进行分类,并通过最小化成本函数(或特殊样本中的杂质评分) 决策树集成的案例)。 对于下一轮(子图2),我们将较大的权重分配给两个先前错误分类的样本(圆圈)。 此外,我们降低了正确分类的样本的权重。 现在,下一个决策树桩将更加集中于权重最大的训练样本,即据称难以分类的训练样本。 子图2中显示的弱学习者对圆形类的三个不同样本进行了错误分类,如子图3所示,它们被赋予了较大的权重。 假设我们的 AdaBoost 合奏仅由三轮提升组成,然后我们将通过加权多数投票将在不同的重新加权训练子集中训练的三个弱学习者组合在一起,如子图4所示。
现在,对 AdaBoost 的基本概念有了的更好的理解,让我们更详细地了解使用伪代码的算法。 为了清楚起见,我们将分别用叉号×和按两个符号之间的点积·分别表示元素乘积。 步骤如下:
-
将权重向量
w设置为统一权重,其中sum(w) = 1 -
对于
m增强回合中的j,请执行以下操作: -
训练加权的弱学习者:
C[j] = train(X, y, w)。 -
预测类别标签:
y_hat = predict(C[j], X)。 -
计算加权错误率:
ε = w · (y_hat == y)。 -
计算系数:
α = 0.5 log((1 - ε) / ε)。 -
更新权重:
w := w × exp(-α[j] × y_hat × y)。 -
将权重归一化为 1:
w = w / sum(w)。 -
计算最终预测:
注意,步骤 5 中的表达式(y_hat == y)表示 1s 和 0s 的向量,如果预测不正确,则将其分配为 1,否则将其分配为 0。
尽管 AdaBoost 算法看似非常简单,但让我们通过一个包含 10 个训练样本的训练集来遍历更具体的示例,如下表所示:
该表的第一列描述了训练样本 1 至 10 的样本索引。在第二列中,假设这是一维数据集,我们将看到各个样本的特征值。 第三列显示每个训练样本x[i]的真实类别标签y[i],其中y[i] ∈ {1, -1}。 初始权重显示在第四列; 我们将权重初始化为统一的并将其标准化为总和。 因此,在 10 个样本训练集的情况下,我们将 0.1 分配给权重向量w中的每个权重w[i]。 假设我们的分割标准为!x <= 3.0,则预测的类别标签y_hat显示在第五列中。 然后,表格的最后一列显示基于我们在伪代码中定义的更新规则的更新权重。
由于权重更新的计算乍看起来可能有点复杂,因此我们现在将逐步进行计算。 我们首先按照步骤 5 中所述计算加权错误率ε:
接下来,我们计算系数α[j](在步骤 6 中显示),该系数随后在步骤 7 中用于更新权重以及多数表决预测中的权重(步骤 10):
在计算了系数α[j]之后,我们现在可以使用以下公式更新权重向量:
在此,y_hat × y分别是预测类别标签和真实类别标签的向量之间的逐元素乘法。 因此,如果预测y_hat[i]是正确的,则y_hat[i] × y[i]将具有正号,因此由于α[j]也是正数,因此我们将的权重降低了*。*
同样,如果y_hat[i]像这样错误地预测标签,我们将增加的权重:
或像这样:
更新权重向量中的每个权重后,我们将权重归一化,以使它们的总和为 1(第 8 步):
在这里:
因此,对应于正确分类的样本的每个权重将从初始值 0.1 降低到0.065 / 0.914 ≈ 0.071,以进行下一轮增强。 类似地,每个错误分类的样本的权重将从 0.1 增加到0.153 / 0.914 ≈ 0.167。
简而言之,这就是 AdaBoost 。 跳到更实际的部分,让我们现在通过 scikit-learn 训练 AdaBoost 集成分类器。 我们将使用与上一节相同的 Wine 子集来训练装袋元分类器。 通过base_estimator属性,我们将在 500 个决策树树桩上训练AdaBoostClassifier:
>>> from sklearn.ensemble import AdaBoostClassifier
>>> tree = DecisionTreeClassifier(criterion='entropy',
... max_depth=None,
... random_state=0)
>>> ada = AdaBoostClassifier(base_estimator=tree,
... n_estimators=500,
... learning_rate=0.1,
... random_state=0)
>>> tree = tree.fit(X_train, y_train)
>>> y_train_pred = tree.predict(X_train)
>>> y_test_pred = tree.predict(X_test)
>>> tree_train = accuracy_score(y_train, y_train_pred)
>>> tree_test = accuracy_score(y_test, y_test_pred)
>>> print('Decision tree train/test accuracies %.3f/%.3f'
... % (tree_train, tree_test))
Decision tree train/test accuracies 0.845/0.854
如我们所见,与上一节中未修剪的决策树相比,决策树树桩倾向于不适合训练数据:
>>> ada = ada.fit(X_train, y_train)
>>> y_train_pred = ada.predict(X_train)
>>> y_test_pred = ada.predict(X_test)
>>> ada_train = accuracy_score(y_train, y_train_pred)
>>> ada_test = accuracy_score(y_test, y_test_pred)
>>> print('AdaBoost train/test accuracies %.3f/%.3f'
... % (ada_train, ada_test))
AdaBoost train/test accuracies 1.000/0.875
我们可以看到,AdaBoost 模型可以正确预测训练集的所有类别标签,并且与决策树树桩相比,还显示出测试集性能略有改善。 但是,我们也看到通过尝试减少模型偏差而引入了额外的方差。
尽管我们使用进行演示的另一个简单示例,但我们可以看到,与决策树桩相比,AdaBoost 分类器的性能略有提高,并且获得了与上一节中训练的装袋分类器非常相似的准确性得分。 但是,我们应该注意,基于重复使用测试集来选择模型被认为是不好的做法。 泛化性能的估计可能过于乐观,我们将在第 6 章,“学习模型评估和超参数调整”的最佳实践中对此进行更详细的讨论。
最后,让我们检查决策区域是什么样的:
>>> x_min = X_train[:, 0].min() - 1
>>> x_max = X_train[:, 0].max() + 1
>>> y_min = X_train[:, 1].min() - 1
>>> y_max = X_train[:, 1].max() + 1
>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
... np.arange(y_min, y_max, 0.1))
>>> f, axarr = plt.subplots(1, 2,
... sharex='col',
... sharey='row',
... figsize=(8, 3))
>>> for idx, clf, tt in zip([0, 1],
... [tree, ada],
... ['Decision Tree', 'AdaBoost']):
... clf.fit(X_train, y_train)
... Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
... Z = Z.reshape(xx.shape)
... axarr[idx].contourf(xx, yy, Z, alpha=0.3)
... axarr[idx].scatter(X_train[y_train==0, 0],
... X_train[y_train==0, 1],
... c='blue',
... marker='^')
... axarr[idx].scatter(X_train[y_train==1, 0],
... X_train[y_train==1, 1],
... c='red',
... marker='o')
... axarr[idx].set_title(tt)
... axarr[0].set_ylabel('Alcohol', fontsize=12)
>>> plt.text(10.2, -1.2,
... s=Hue',
... ha='center',
... va='center',
... fontsize=12)
>>> plt.show()
通过查看决策区域,我们可以看到 AdaBoost 模型的决策边界实际上比决策树桩的决策边界复杂得多。 此外,我们注意到 AdaBoost 模型与上一节中训练的装袋分类器非常相似,将要素空间分开。
作为关于集成技术的总结,值得注意的是,与单个分类器相比,集成学习会增加计算复杂性。 在实践中,我们需要仔细考虑是否要为通常相对适度的预测性能提高付出付出的计算成本。
这种权衡取舍的一个经常被引用的例子是著名的 100 万美元的 Netflix 奖,该奖项是通过合奏技术获得的。 有关该算法的详细信息发表在 A. Toescher,M。Jahrer 和 R.M. Bell 中。 Netflix 大奖的 Bigchaos 解决方案。 Netflix 奖状文档,2009 年(可在这个页面中找到)。 尽管获胜的团队获得了 100 万美元的奖金,但 Netflix 由于其复杂性而从未实施他们的模型,这使其在现实应用中不可行。 引用他们的确切字词:
“ […]我们测得的额外精度增益似乎不足以证明将其投入生产环境所需的工程努力。”