一、机器学习 - 温和的介绍
“在数据变大之前,我已经进入了这个领域。” – @ml_hipster
您最近可能听说过大数据。互联网,具有巨大计算能力的电子设备的爆炸式增长,以及我们世界上几乎每个流程都使用某种软件的事实,每分钟都在为我们提供大量数据。
想想社交网络,我们存储有关人员,他们的兴趣和他们的互动的信息。想想过程控制设备,从 Web 服务器到汽车和心脏起搏器,永久地保留关于其表现的数据记录。想想科学研究计划,例如基因组计划,它必须分析关于我们 DNA 的大量数据。
你可以用这些数据做很多事情:检查它,总结它,甚至以几种美妙的方式将它可视化。但是,本书讨论了数据的另一种用途:作为改善算法表现的经验来源。这些算法可以从以前的数据中学习,属于机器学习领域,这是人工智能的一个子领域。
任何机器学习问题都可以用以下三个概念表示:
- 我们将必须解决任务
T。例如,构建一个垃圾邮件过滤器,学习将电子邮件归类为垃圾邮件或正常邮件。 - 我们需要一些经验
E来学习执行任务。通常,经验通过数据集表示。对于垃圾邮件过滤器,体验来自一组电子邮件,由人工分类为垃圾邮件或正常邮件。 - 我们需要一定程度的表现
P来了解我们解决任务的能力,以及了解经过一些修改后,我们的结果是在改善还是在恶化。我们的垃圾邮件过滤器将电子邮件正确分类为垃圾邮件或正常邮件的百分比,可能是我们的垃圾邮件过滤任务的P。
Scikit-learn 是一个开源的 Python 机器学习算法库,它允许我们构建这些类型的系统。该项目于 2007 年由 David Cournapeau 作为 Google Summer of Code 项目启动。那年晚些时候,Matthieu Brucher 作为他论文的一部分开始研究这个项目。2010 年,Fabian Pedregosa,Gael Varoquaux,Alexandre Gramfort,INRIA 的 Vincent Michel 领导项目并首次公开发布。如今,该项目正在由热情的贡献者社区积极开发。它建立在 NumPy 和 SciPy 之上,它是用于科学计算的标准 Python 库。通过本书,我们将使用它向您展示,如何将先前数据整合为经验来源,来有效解决几个常见的编程任务。
在本章的以下部分中,我们将开始查看如何安装 scikit-learn 并准备您的工作环境。之后,我们将以实用的方式简要介绍机器学习,尝试在解决简单实际任务的同时介绍关键机器学习概念。
安装 scikit-learn
有关 scikit-learn 的安装说明,请访问这个页面。本书中的几个例子包括可视化,因此您还应该从 matplotlib.org 安装matplotlib包。 我们还建议安装 IPython 笔记本,这是一个非常有用的工具,包括一个基于 Web 的控制台来编辑和运行代码片段,并渲染结果。本书附带的源代码是通过 IPython 笔记本提供的。
安装所有包的简单方法是从 store.continuum.io 下载并安装用于科学计算的 Anaconda 发行版,该发行版为 Linux,Mac 和 Windows 平台提供所有必需的包。或者,如果您愿意,以下部分提供了有关如何在每个特定平台上安装每个包的一些建议。
Linux
安装环境的最简单方法可能是通过操作系统包。对于基于 Debian 的操作系统,例如 Ubuntu,您可以通过运行以下命令来安装包:
-
首先,要安装包,我们输入以下命令:
#sudo apt-get install build-essential python-dev python-numpy python-setuptools python-scipy libatlas-dev -
然后,要安装 matplotlib,请运行以下命令:
#sudo apt-get install python-matplotlib -
之后,我们应该准备通过键入以下命令来安装 scikit-learn:
#sudo pip install scikit-learn -
要安装 IPython 笔记本,请运行以下命令:
#sudo apt-get install ipython-notebook -
如果你想从源代码安装,假设在虚拟环境中安装所有库,你应该键入以下命令:
#pip install numpy #pip install scipy #pip install scikit-learn -
要安装 Matplotlib,您应该运行以下命令:
#pip install libpng-dev libjpeg8-dev libfreetype6-dev #pip install matplotlib -
要安装 IPython 笔记本,你应该运行以下命令:
#pip install ipython #pip install tornado #pip install pyzmq
Mac
您可以以类似方式使用 MacPorts 和 HomeBrew 等工具,它们包含这些包的预编译版本。
Windows
要在 Windows 上安装 scikit-learn,你可以从项目网页的下载部分下载 Windows 安装程序。
检查您的安装
要检查是否已准备好运行,只需打开 Python(或可能更好的 IPython)控制台并输入以下内容:
>>> import sklearn as sk
>>> import numpy as np
>>> import matplotlib.pyplot as plt
我们决定在 Python 代码之前加上>>>将它与句子结果分开。 Python 将静默导入 scikit-learn,NumPy 和 matplotlib 包,我们将在本书的其余部分中使用它们。
如果要执行本书中介绍的代码,则应运行 IPython 笔记本:
## ipython notebook
这将允许您直接在浏览器中打开相应的笔记本。
数据集
正如我们所说,机器学习方法依赖于以前的经验,通常由数据集表示。在 scikit-learn 上实现的每个方法,都假定数据来自数据集,这是一种特定形式的输入数据表示,使程序员可以更容易地对同一数据尝试不同的方法。Scikit-learn 包含一些众所周知的数据集。在本章中,我们将使用其中一个,鸢尾花数据集,由 Sir Ronald Fisher 在 1936 年引入,来显示统计方法(判别分析)如何工作(是的,他们在大数据出现之前就收集了数据。您可以在维基百科页面上找到该数据集的描述,但实际上,它包含来自三种不同鸢尾花物种的 150 个元素(或机器学习术语,实例)的信息,包括萼片和花瓣的长度和宽度。使用这个数据集解决的自然任务是,知道萼片和花瓣的测量值并学习猜测鸢尾种类。它已广泛用于机器学习任务,因为它是一个非常简单的数据集,我们稍后会看到。让我们导入数据集并显示第一个实例的值:
>>> from sklearn import datasets
>>> iris = datasets.load_iris()
>>> X_iris, y_iris = iris.data, iris.target
>>> print X_iris.shape, y_iris.shape
(150, 4) (150,)
>>> print X_iris[0], y_iris[0]
[ 5.1 3.5 1.4 0.2] 0
我们可以看到iris数据集是一个对象(类似于字典),它有两个主要成分:
- 一个
data数组, ,其中,对于每个实例,我们都有萼片长度,萼片宽度,花瓣长度和花瓣宽度的实际值(请注意,出于效率原因,scikit-learn 方法使用了 NumPyndarrays,而不是更具描述性但效率更低的 Python 词典或列表。这个数组的形状是(150, 4),这意味着我们有 150 行(每个实例一个)和四列(每个特征一个)。 - 一个
target数组,值在 0 到 2 的范围内,对应于每种鸢尾种类(0:山鸢尾,1:杂色鸢尾和 2:弗吉尼亚鸢尾),您可以通过打印iris.target.target_names值来验证。
虽然我们用于 scikit 的每个数据集都没有必要具有这种精确结构,但我们将看到每个方法都需要这个数据数组,其中每个实例都表示为一个特征或属性列表,另一个目标数组代表某个值,我们希望我们的学习方法能够学会预测。在我们的例子中,花瓣和萼片测量值是我们的实值属性,而花种是我们想要预测的几个类别之一。
我们的第一个机器学习方法 - 线性分类
为了处理 scikit-learn 中机器学习的问题,我们将从一个非常简单的机器学习问题开始:我们将尝试仅预测两个鸢尾花种属性:萼片宽度和萼片长度。这是分类问题的一个实例,我们希望根据其特征为项目分配标签(从离散集合中取得的值)。
让我们首先构建我们的训练数据集 - 原始样本的子集,由我们选择的两个属性及其各自的目标值表示。导入数据集后,我们将随机选择大约 75% 的实例,并保留剩余的实例(评估数据集)来用于评估目的(我们将在后面看到为什么我们应该总是这样做):
>>> from sklearn.cross_validation import train_test_split
>>> from sklearn import preprocessing
>>> # Get dataset with only the first two attributes
>>> X, y = X_iris[:, :2], y_iris
>>> # Split the dataset into a training and a testing set
>>> # Test set will be the 25% taken randomly
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=33)
>>> print X_train.shape, y_train.shape
(112, 2) (112,)
>>> # Standardize the features
>>> scaler = preprocessing.StandardScaler().fit(X_train)
>>> X_train = scaler.transform(X_train)
>>> X_test = scaler.transform(X_test)
train_test_split函数自动构建训练和评估数据集,随机选择样本。为什么不选择前 112 个例子呢?这是因为样本中的实例顺序可能很重要,并且第一个实例可能与最后一个实例不同。事实上,如果你看一下鸢尾数据集,实例按其目标类排序,这意味着新训练集中0和1类的比例将更高 ,与原始数据集进行比较。我们总是希望我们的训练数据成为他们所代表的总体的代表性样本。
前一代码的最后三行在通常称为特征缩放的过程中修改训练集。对于每个特征, 计算平均值,从特征值中减去平均值,并将结果除以它们的标准差。缩放后,每个特征的平均值为零,标准差为 1。这种值的标准化(不会改变它们的分布,因为你可以通过在缩放之前和之后绘制X值来验证)是机器学习方法的常见要求,来避免具有大值的特征可能在权重上过重。
现在,让我们看一下我们的训练实例如何在由学习特征生成的,二维空间中分布。来自 matplotlib 库的pyplot将帮助我们:
>>> import matplotlib.pyplot as plt
>>> colors = ['red', 'greenyellow', 'blue']
>>> for i in xrange(len(colors)):
>>> xs = X_train[:, 0][y_train == i]
>>> ys = X_train[:, 1][y_train == i]
>>> plt.scatter(xs, ys, c=colors[i])
>>> plt.legend(iris.target_names)
>>> plt.xlabel('Sepal length')
>>> plt.ylabel('Sepal width')
scatter函数简单地绘制每个实例的第一个特征值(萼片宽度)与其第二个特征值(萼片长度),并使用目标类值为每个类指定不同的颜色。通过这种方式,我们可以很好地了解这些属性如何有助于确定目标类。以下屏幕截图显示了生成的图:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_01_01.jpg
看一下前面的截图,我们可以看到红点(对应于 Iris setosa)和绿点和蓝点(对应于另外两种鸢尾物种)之间的分离非常清楚,而鉴于这两个特征可用,将绿色与蓝点分开似乎非常任务艰巨。这是一个非常常见的场景:我们想要在机器学习任务中回答的第一个问题是,我们使用的特征集是否对我们正在解决的任务实际上有用,或者我们是否需要添加新属性或更改我们的方法。
鉴于可用数据,让我们在这里重新定义我们的学习任务:假设我们瞄准鸢尾花实例,预测它是否是一个山鸢尾。我们已经将我们的问题转换为二元分类任务(也就是说,我们只有两个可能的目标类)。
如果我们看一下图片,似乎我们可以绘制一条正确分隔两个集合的直线(可能除了一个或两个点,它们可能位于线的不正确的一侧)。这正是我们的第一种分类方法 - 线性分类模型试图做的:构建一条线(或更一般地说,在特征空间中的超平面),最优地分离两个目标类,并将其用作决策边界(是,类成员身份取决于实例在超平面的哪一侧)。
要实现线性分类,我们将使用 scikit-learn 中的SGDClassifier。 SGD 代表随机梯度下降,这是一种非常流行的数值过程,用于查找函数的局部最小值(在本例中为损失函数),测量每个实例离我们边界的距离。该算法将通过最小化损失函数来学习超平面的系数。
要在 scikit-learn,中使用任何方法,我们必须首先创建相应的分类器对象,初始化其参数,并训练拟合训练数据的模型。当你在本书中前进时,你会看到这个程序对于最初看起来非常不同的任务几乎是一样的。
>>> from sklearn.linear_model import SGDClassifier
>>> clf = SGDClassifier()
>>> clf.fit(X_train, y_train)
SGDClassifier初始化函数允许多个参数。目前,我们将使用默认值,但请记住,这些参数可能非常重要,尤其是当您面对更多真实世界的任务时,实例数量(甚至属性数量)可能非常大。 fit函数可能是 scikit-learn 中最重要的函数。它接收训练数据和训练类别,并构建分类器。 scikit-learn 中的每个监督学习方法都实现了这个函数。
在我们的线性模型方法中,分类器看起来像什么?正如我们已经说过的,每个未来的分类决策都只取决于超平面。那个超平面就是我们的模型。clf对象的coef_属性(暂时考虑,只考虑矩阵的第一行),现在具有线性边界的系数和intercept_属性,直线与 y 轴的交点。我们打印出来:
>>> print clf.coef_
[[-28.53692691 15.05517618]
[ -8.93789454 -8.13185613]
[ 14.02830747 -12.80739966]]
>>> print clf.intercept_
[-17.62477802 -2.35658325 -9.7570213 ]
实际上,在实际平面中,使用这三个值,我们可以绘制一条线,由以下等式表示:
-17.62477802 - 28.53692691 * x1 + 15.05517618 * x2 = 0
现在,给定x1和x2(我们的实值特征),我们只需要计算等式左侧的值:如果它的值大于零,那么该点在决策边界(红色一侧)之上,否则它将在直线之下(绿色或蓝色一侧)。我们的预测算法将简单地检查这个并预测任何新鸢尾花的相应类别。
但是,为什么我们的系数矩阵有三行?因为我们没有告诉方法我们已经改变了我们的问题定义(我们怎么能这样做?),它面临着一个三类问题,而不是一个二元决策问题。在这种情况下,分类器的作用与我们所做的相同 - 它在一对一设置中将问题转换为三个二元分类问题(它提出了三个将一个类与其他类别分开的直线)。
以下代码绘制了三个决策边界,并告诉我们它们是否按预期工作:
>>> x_min, x_max = X_train[:, 0].min() - .5, X_train[:, 0].max() +
.5
>>> y_min, y_max = X_train[:, 1].min() - .5, X_train[:, 1].max() +
.5
>>> xs = np.arange(x_min, x_max, 0.5)
>>> fig, axes = plt.subplots(1, 3)
>>> fig.set_size_inches(10, 6)
>>> for i in [0, 1, 2]:
>>> axes[i].set_aspect('equal')
>>> axes[i].set_title('Class '+ str(i) + ' versus the rest')
>>> axes[i].set_xlabel('Sepal length')
>>> axes[i].set_ylabel('Sepal width')
>>> axes[i].set_xlim(x_min, x_max)
>>> axes[i].set_ylim(y_min, y_max)
>>> sca(axes[i])
>>> plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train,
cmap=plt.cm.prism)
>>> ys = (-clf.intercept_[i] –
Xs * clf.coef_[i, 0]) / clf.coef_[i, 1]
>>> plt.plot(xs, ys, hold=True)
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_01_02.jpg
第一个图显示了为原始二元问题构建的模型。看起来这条线与其余的线条相当好地分离了山鸢尾。对于其他两个任务,正如我们所预料的那样,有几个点位于超平面的错误一侧。
现在,故事的结尾:假设我们有一个新的花,萼片宽度为 4.7,萼片长度为 3.1,我们想要预测它的类别。我们只需要使用我们全新的分类器(标准化后!)。预测方法接受实例列表(在这种情况下,仅使用一个元素)并返回预测类别的列表:
>>>print clf.predict(scaler.transform([[4.7, 3.1]]))
[0]
如果我们的分类器是正确的,这个鸢尾花是一个山鸢尾。可能你已经注意到我们正在从可能的三个类中预测一个类,但是线性模型本质上是二元的:缺少某些东西。你是对的。我们的预测程序结合了三个二元分类器的结果,并选择了更有置信度的类。在这种情况下,我们将选择与实例的距离更长的边界线。我们可以使用分类器decision_function方法检查:
>>>print clf.decision_function(scaler.transform([[4.7, 3.1]]))
[[ 19.73905808 8.13288449 -28.63499119]]
评估我们的结果
当我们谈论良好的分类器时,我们想要更正式一些。那是什么意思?分类器的表现是衡量其有效性的标准。最简单的表现测量是准确率:给定分类器和评估数据集,它测量由分类器正确分类的实例的比例。首先,让我们测试训练集的准确率:
>>> from sklearn import metrics
>>> y_train_pred = clf.predict(X_train)
>>> print metrics.accuracy_score(y_train, y_train_pred)
0.821428571429
这个数字告诉我们 82% 的训练集实例被我们的分类器正确分类。
也许,你应该从本章学到的最重要的事情是,测量训练集的准确率真的是一个坏主意。您已经使用此数据构建了模型,并且您的模型可能会很好地拟合它们,但将来表现不佳(以前看不见的数据),这就是它的目的。这种现象被称为过拟合,当你读这本书时,你会不时地看到它。如果您根据训练数据进行测量,则永远不会检测到过拟合。因此,永远不要根据您的训练数据进行衡量。
这就是为什么我们保留了原始数据集的一部分(测试部分) - 我们想要评估以前看不见的数据的表现。让我们再次检查准确率,现在在评估集上(回想一下它已经缩放):
>>> y_pred = clf.predict(X_test)
>>> print metrics.accuracy_score(y_test, y_pred)
0.684210526316
我们在测试集中获得了 68% 的准确率。通常,测试集的准确率低于训练集的准确率,因为模型实际上是对训练集进行建模,而不是测试集。我们的目标始终是生成模型,以便在训练集上训练时避免过拟合,因此它们具有足够的泛化能力,可以正确地模拟看不见的数据。
当每个类的实例数相似时,测试集的准确率是一个很好的表现测量,也就是说,我们有均匀的类分布。但是如果你有一个偏斜的分布(比如 99% 的实例属于一个类),那么总是预测大多数类的分类器在准确率方面可以有很好的表现,尽管它是一种非常朴素的方法。
在 scikit-learn 中,有几个评估函数;我们将展示三种流行的:精确率,召回率和 F1 得分(或 F 度量)。他们假设二元分类问题和两个类 - 正面和负面。在我们的例子中,正类可以是山鸢尾,而其他两个将合并为一个负类。
- 精确率: 计算预测为正例的实例中,正确评估的比例(它测量分类器在表示实例为正时的正确程度) 。
- 召回率:计算正确评估的正例示例的比例(测量我们的分类器在面对正例实例时的正确率)。
- F1 得分:这是精确率和召回率的调和平均值。
注意
使用调和平均值代替算术平均值,因为后者补偿低精确率值和高召回率值(反之亦然)。另一方面,对于调和平均值,如果精确率或召回率较低,我们将始终具有低值。有关此问题的有趣描述,请参阅文章。
我们可以用真假,正例负例来定义这些指标:
| 预测:正例 | 预测:负例 | |
|---|---|---|
| 目标:正例 | 真正例(TP) | 假负例(FN) |
| 目标:负例 | 假正例(FP) | 真负例(TN) |
m为样本量(即TP + TN + FP + FN),我们有以下公式:
准确率 =(TP + TN) / m精确率 = TP / (TP + FP)召回率 = TP / (TP + FN)F1 得分 = 2 * 精确率 * 召回 / (精确率 + 召回率)
让我们在实践中看看它:
>>> print metrics.classification_report(y_test, y_pred,target_names=iris.target_names)
precision recall f1-score support
setosa 1.00 1.00 1.00 8
versicolor 0.43 0.27 0.33 11
virginica 0.65 0.79 0.71 19
avg / total 0.66 0.68 0.66 38
我们计算了每个类别的精确率,召回率和 F1 得分及其平均值。我们在这张表中看到的是:
- 分类器在
setosa类中获得 1.0 精确率和召回率。这意味着,对于精确率,100% 被归类为山鸢尾的实例实际上是山鸢尾实例,并且对于召回率,100% 的山鸢尾实例被归类为山鸢尾。 - 另一方面,在
versicolor类中,结果不太好:我们的精确率为 0.43,也就是说,只有 43% 被分类为杂色鸢尾的实例才是真杂色鸢尾实例。此外,对于杂色鸢尾,我们召回率会 0.27,也就是说,只有 27% 的杂色鸢尾实例被正确分类。
现在,我们可以看到我们的方法(正如我们所期望的)非常擅长预测setosa,而当它必须分离versicolor或virginica类时它会受到影响。支持值显示我们在测试集中拥有的每个类的实例数。
另一个有用的指标(特别是对于多类问题)是混淆矩阵:在其(i, j)单元格中,它显示了预测为类j的类实例i的数量。良好的分类器将在混淆矩阵对角线上累积值,其中正确分类的实例属于该对角线。
>>> print metrics.confusion_matrix(y_test, y_pred)
[[ 8 0 0]
[ 0 3 8]
[ 0 4 15]]
在我们的评估中,我们的分类器在分类类别0的花(setosa)时,永远不会出错。但是,当它面对类别1和2的花(versicolor和virginica)时,它会混淆它们。混淆矩阵为我们提供了有用的信息,来了解分类器正在犯的错误类型。
为了完成我们的评估过程,我们将介绍一种非常有用的方法,称为交叉验证。如前所述,我们必须将数据集划分为训练集和测试集。但是,对数据进行划分,结果是训练的实例较少,而且,根据我们生成的特定分区(通常随机生成),我们可以得到更好或更差的结果。交叉验证允许我们避免这种特殊情况,减少结果差异并为我们的模型产生更真实的分数。K 折交叉验证的常用步骤如下:
- 将数据集划分为
k个不同的子集。 - 通过训练
k-1个子集并测试剩余子集来创建k不同模型。 - 测量
k个模型的每个的表现并采取平均测量。
让我们用线性分类器做到这一点。首先,我们必须创建一个由标准化和线性模型的管道制作的复合估计器。使用这种技术,我们确保每次迭代都将标准化数据,然后对转换后的数据进行训练/测试。 Pipeline 类也可用于简化更复杂模型的构建,它们是变换的链式乘法。我们将选择k = 5倍,因此每次我们将训练 80% 的数据并测试剩余的 20%。默认情况下,交叉验证使用准确率作为其表现度量,但我们可以通过将任何得分函数作为参数来选择度量。
>>> from sklearn.cross_validation import cross_val_score, KFold
>>> from sklearn.pipeline import Pipeline
>>> # create a composite estimator made by a pipeline of the
standarization and the linear model
>>> clf = Pipeline([
('scaler', StandardScaler()),
('linear_model', SGDClassifier())
])
>>> # create a k-fold cross validation iterator of k=5 folds
>>> cv = KFold(X.shape[0], 5, shuffle=True, random_state=33)
>>> # by default the score used is the one returned by score
method of the estimator (accuracy)
>>> scores = cross_val_score(clf, X, y, cv=cv)
>>> print scores
[ 0.66666667 0.93333333 0.66666667 0.7 0.6 ]
我们获得了具有k个得分的数组。 我们可以计算平均值和标准差来获得最终数字:
>>> from scipy.stats import sem
>>> def mean_score(scores):
return ("Mean score: {0:.3f} (+/-
{1:.3f})").format(np.mean(scores), sem(scores))
>>> print mean_score(scores)
Mean score: 0.713 (+/-0.057)
我们的模型的平均准确率为 0.71。
机器学习类别
分类只是可能的机器学习问题之一,可以通过 scikit-learn 解决。我们可以按以下类别组织它们:
- 在前面的示例中,我们有一组实例(即从群体中收集的一组数据),由某些特征和特定目标属性表示。监督学习算法尝试从这些数据构建模型,这使我们可以预测新实例的目标属性,只知道这些实例特征的情况。当目标类属于离散集(例如花的物种列表)时,我们面临着分类问题。
- 有时,我们想要预测的类不是属于离散集,而是连续集上的范围,例如实数行。在这种情况下,我们试图解决回归问题(该术语是由弗朗西斯·高尔顿创造的,他认为高大祖先的身高倾向于向正常值回归,人的平均身高)。例如,我们可以尝试根据其他三个特征预测花瓣宽度。我们将看到用于回归的方法与用于分类的方法完全不同。
- 另一种不同类型的机器学习问题是无监督学习。在这种情况下,我们没有要预测的目标类,而是希望根据可用的特征集合,和某些相似性度量对实例进行分组。例如,假设您有一个由电子邮件组成的数据集,并希望按主题对其进行分组(分组实例的任务称为聚类) 。我们可以将它用作特征,例如,每个特征中使用的不同单词。
与机器学习相关的重要概念
我们在上一节中提出的线性分类器看起来太简单了。如果我们使用更高次多项式怎么办?如果我们不仅将萼片的长度和宽度,还有花瓣长度和花瓣宽度作为特征,该怎么办?这是完全可能的,并且根据样本分布,它可以更好地拟合训练数据,从而提高准确率。这种方法的问题是,现在我们不仅要估计三个原始参数(x1,x2的系数,以及截距),还有新特征的参数x3和x4(花瓣长度和宽度)以及这四个特征乘积的组合。
直观地说,我们需要更多的训练数据来充分估计这些参数。如果我们添加更多特征或更高阶项,参数的数量(以及因此,充分估计它们所需的训练数据量)将迅速增加。这种现象存在于每种机器学习方法中,被称为维度的概念:当模型的参数数量增加时,学习它们所需的数据呈指数增长。
这个概念与前面提到的过拟合问题密切相关。由于我们的训练数据不够,我们就有风险生成一个模型,它非常擅长预测训练数据集上的目标类,但在面对新数据时是失败的,也就是说,我们的模型没有泛化能力。这就是为什么,在以前看不见的数据上评估我们的方法如此重要。
一般规则是,为了避免过拟合,我们应该偏向简单(即参数较少)的方法,这可以被视为奥卡姆剃刀的哲学原理的实例化,它指出在竞争假设中,应该选择假设最少的假设。
但是,我们也应该考虑到爱因斯坦的话:
“一切都应尽可能简单,但并不是过于简单。”
维度诅咒可能暗示我们保持模型简单,但另一方面,如果我们的模型过于简单,我们就会面临遭受欠拟合风险。当我们的模型具有如此低的表现力,以及即使我们拥有我们想要的所有训练数据也无法对数据建模时,就会出现欠拟合问题。当我们的算法即使在训练集上进行测量时也无法拥有良好的表现测量,我们显然有不足之处。
因此,我们必须在过拟合和欠拟合之间取得平衡。这是我们在设计机器学习模型时必须解决的最重要问题之一。
要考虑的其他关键概念是机器学习方法的偏差和方差。考虑一种极端方法,在二元分类设置中,始终将任何新实例预测为正类。它的预测通常是相同的,或者在统计学上,它具有零方差;但它无法预测负例:它非常偏向于正面的结果。另一方面,考虑一种方法,将新实例预测为训练集中最近实例的类(实际上,此方法存在,并且它被称为 1 最近邻)。该方法使用的泛化假设非常小:它具有非常低的偏差;但是,如果我们改变训练数据,结果可能会发生巨大变化,也就是说,它的方差非常高。这些是偏差 - 方差权衡的极端例子。可以证明,无论我们使用哪种方法,如果我们减少偏差,方差将增加,反之亦然。
线性分类器通常具有低方差:无论我们选择哪种子集进行训练,结果都是相似的。但是,如果数据分布(如在杂色和维吉尼亚物种的情况下)使得目标类别不能由超平面分离,则这些结果将始终是错误的,即,该方法是高偏差的。
另一方面,kNN(我们将在本书中未涉及的基于记忆的方法)具有非常低的偏差但是方差很大:结果通常非常擅长描述训练数据,但是在训练不同的训练实例时往往变化很大。
还有其他与实际应用相关的重要概念,其中我们的数据不会自然地变为实值特征列表。在这些情况下,我们需要用方法将非实值特征转换为实值特征。此外,还有其他与特征标准化和规范化相关的步骤,正如我们在鸢尾示例中所看到的,需要避免不同值范围的非预期的影响。特征空间的这些变换称为数据预处理。
在定义了特征集之后,我们将看到并非原始数据集中的所有特征都可用于解决我们的任务。因此,我们还必须有方法来进行特征选择,即选择最有希望的特征的方法。
在本书中,我们将提出几个问题,在每个问题中,我们将展示不同的方法来转换和找到用于学习任务的最相关的特征,称为特征工程,根据我们对问题领域和/或数据分析方法的了。这些方法通常不够重要,但是取得良好结果的基本步骤。
总结
在本章中,我们介绍了机器学习中的主要一般概念,并介绍了 scikit-learn,我们将在本书的其余部分中使用的 Python 库。我们提供了一个非常简单的分类示例,试图展示学习的主要步骤,并包括我们将使用的最重要的评估措施。在本书的其余部分,我们计划向您展示使用不同的实际示例的,不同机器学习方法和技术。在几乎所有计算任务中,历史数据的存在可以使我们提高表现,在本章开头介绍的意义上。
下一章介绍了监督学习方法:我们有带注解的数据(即目标类/值已知的实例),我们希望为来自同一群体的未来数据预测相同的类/值。在分类任务的情况下,即离散值目标类,存在几种不同的模型,范围从统计方法,如简单朴素贝叶斯到高级线性分类器,例如支持向量机(SVM)。一些方法,例如决策树,将允许我们可视化特征的重要性,来区分不同目标类别,和对决策过程进行人工解释。我们还将讨论另一种类型的监督学习任务:回归,即尝试预测实值数据的方法。
二、监督学习
在第一章“机器学习 - 温和介绍”中,我们概述了监督学习算法的一般概念。 我们有训练数据,其中每个实例都有一个输入(一组属性)和一个所需的输出(一个目标类)。然后我们使用这些数据来训练一个模型,该模型将新的未见实例预测为相同的目标类。
监督学习方法如今已成为各种学科的标准工具,从医学诊断到自然语言处理,图像识别,以及在大型强子对撞机(LHC)中搜索新粒子。在本章中,我们将通过使用 scikit-learn 中实现的许多算法中的一些,实现几种实际示例。本章不打算替换 scikit-learn 参考,而是介绍主要的监督学习技巧,并展示如何使用它们来解决实际问题。
支持向量机和图像识别
想象一下,数据集中的实例是多维空间中的点;我们可以假设我们的分类器构建的模型可以是表面,也可以使用线性代数术语,即将一个类的实例(点)与其余类别分开的超平面。支持向量机(SVM)是监督学习方法,试图以最佳方式获得这些超平面,通过选择那些通过不同类的实例之间最宽的间距。新实例将根据它们在表面的哪一侧,被归类为属于某个类别。
下图显示了具有两个特征(X1和X2)和两个类别(黑色和白色)的二维空间示例:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_02_01.jpg
我们可以观察到,绿色超平面不会将两个类分开,从而产生一些分类错误。蓝色和红色超平面将两个类分开,没有错误。但是,红色表面以最大边距分隔两个类别;它是距离两个类别的最近实例的最远超平面。这种方法的主要优点是它可能会降低泛化误差,使得该模型能够抵抗过拟合,这实际上已在几个不同的分类任务中得到验证。
这种方法不仅可以在二维中构造超平面,而且可以推广到高维或无限维空间中。更重要的是,我们可以使用非线性曲面,例如多项式或径向基函数,通过使用所谓的核技巧,隐式地将输入映射到高维特征空间。
SVM 已成为许多任务中最先进的机器学习模型之一,在许多实际应用中具有出色的结果。SVM 的最大优势之一是它们在高维空间上工作时非常有效,即在具有许多要学习的特征的问题上。当数据稀疏时,它们也非常有效(考虑具有极少数实例的高维空间)。此外,它们在存储空间方面非常有效,因为学习空间中仅有一部分点用于表示决策表面。
提到一些缺点,SVM 模型在训练模型时可能非常耗费计算量,并且它们不会返回数字指标,表明它们对预测的置信度。但是,我们可以使用一些技术,如 K 折交叉验证来避免这种情况,代价是增加计算成本。
译者注:其实任何线性模型中,实例到决策超平面的距离都是(没有归一的)置信度。
我们将 SVM 应用于图像识别,这是一个具有非常大的维度空间的经典问题(图像的每个像素的值被视为一个特征)。我们将尝试做的是,给出一个人脸的图像,预测它可能属于列表中的哪些人(例如,在社交网络应用中使用这种方法来自动标记照片中的人物)。我们的学习集将是一组人脸的带标记图像,我们将尝试学习一种模型,可以预测没见过的实例的标签。第一种直观的方法是将图像像素用作学习算法的特征,因此像素值将是我们的学习属性,而个体的标签将是我们的目标类。
我们的数据集在 scikit-learn 中提供,所以让我们从导入开始并打印其描述。
>>> import sklearn as sk
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from sklearn.datasets import fetch_olivetti_faces
>>> faces = fetch_olivetti_faces()
>>> print faces.DESCR
该数据集包含 40 个不同人脸的 400 张图像。拍摄的照片采用不同的光线条件和面部表情(包括睁眼/闭眼,微笑/不笑,戴眼镜/不戴眼镜)。有关数据集的其他信息,请参阅这个页面。
查看faces对象的内容,我们得到以下属性:images,data和target。图像包含表示为64 x 64像素矩阵的 400 个图像。 data包含相同的 400 个图像,但是作为 4096 个像素的数组。正如预期的那样,target是一个具有目标类的数组,范围从 0 到 39。
>>> print faces.keys()
['images', 'data', 'target', 'DESCR']
>>> print faces.images.shape
(400, 64, 64)
>>> print faces.data.shape
(400, 4096)
>>> print faces.target.shape
(400,)
正如我们在前一章中看到的那样,规范化数据非常重要。对于 SVM 的应用来说,获得良好结果也很重要。在我们的特定情况下,我们可以通过运行以下代码段,来验证我们的图像已经成为 0 到 1 之间的非常均匀范围内的值(像素值):
>>> print np.max(faces.data)
1.0
>>> print np.min(faces.data)
0.0
>>> print np.mean(faces.data)
0.547046432495
因此,我们没有规范化数据。在学习之前,让我们绘制一些人脸。我们将定义以下helper函数:
>>> def print_faces(images, target, top_n):
>>> # set up the figure size in inches
>>> fig = plt.figure(figsize=(12, 12))
>>> fig.subplots_adjust(left=0, right=1, bottom=0, top=1,
hspace=0.05, wspace=0.05)
>>> for i in range(top_n):
>>> # plot the images in a matrix of 20x20
>>> p = fig.add_subplot(20, 20, i + 1, xticks=[],
yticks=[])
>>> p.imshow(images[i], cmap=plt.cm.bone)
>>>
>>> # label the image with the target value
>>> p.text(0, 14, str(target[i]))
>>> p.text(0, 60, str(i))
如果我们打印前 20 张图像,我们可以看到两个人脸。
>>> print_faces(faces.images, faces.target, 20)
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_02_02.jpg
训练支持向量机
要在 scikit-learn 中使用 SVM 来解决我们的任务,我们将从sklearn.svm模块导入SVC类:
>>> from sklearn.svm import SVC
支持向量分类器(SVC)将用于分类。在本章的最后一节中,我们将使用 SVM 进行回归任务。
SVC 实现具有不同的重要参数;可能最相关的是kernel,它定义了在我们的分类器中使用的核函数(将核函数看作实例之间的不同相似性度量)。默认情况下,SVC类使用rbf核,这允许我们模拟非线性问题。首先,我们将使用最简单的核,即linear。
>>> svc_1 = SVC(kernel='linear')
在继续之前,我们将把数据集分成训练和测试数据集。
>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(
faces.data, faces.target, test_size=0.25, random_state=0)
我们将定义函数来评估 K 折交叉验证。
>>> from sklearn.cross_validation import cross_val_score, KFold
>>> from scipy.stats import sem
>>>
>>> def evaluate_cross_validation(clf, X, y, K):
>>> # create a k-fold croos validation iterator
>>> cv = KFold(len(y), K, shuffle=True, random_state=0)
>>> # by default the score used is the one returned by score
method of the estimator (accuracy)
>>> scores = cross_val_score(clf, X, y, cv=cv)
>>> print scores
>>> print ("Mean score: {0:.3f} (+/-{1:.3f})").format(
np.mean(scores), sem(scores))
>>> evaluate_cross_validation(svc_1, X_train, y_train, 5)
[ 0.93333333 0.91666667 0.95 0.95 0.91666667]
Mean score: 0.933 (+/-0.007)
交叉验证五次,获得了相当不错的结果(准确率为 0.933)。在几个步骤中,我们获得了人脸分类器。
我们还将定义一个函数来对训练集进行训练并评估测试集上的表现。
>>> from sklearn import metrics
>>>
>>> def train_and_evaluate(clf, X_train, X_test, y_train, y_test):
>>>
>>> clf.fit(X_train, y_train)
>>>
>>> print "Accuracy on training set:"
>>> print clf.score(X_train, y_train)
>>> print "Accuracy on testing set:"
>>> print clf.score(X_test, y_test)
>>>
>>> y_pred = clf.predict(X_test)
>>>
>>> print "Classification Report:"
>>> print metrics.classification_report(y_test, y_pred)
>>> print "Confusion Matrix:"
>>> print metrics.confusion_matrix(y_test, y_pred)
如果我们训练和评估,分类器执行操作并几乎没有错误。
>>> train_and_evaluate(svc_1, X_train, X_test, y_train, y_test)
Accuracy on training set:
1.0
Accuracy on testing set:
0.99
让我们多做一点,为什么不尝试将人脸分类为有眼镜和没有眼镜的人?我们这样做。
首先要做的是定义图像范围,它显示戴眼镜的人脸。以下列表显示了这些图像的索引:
>>> # the index ranges of images of people with glasses
>>> glasses = [
(10, 19), (30, 32), (37, 38), (50, 59), (63, 64),
(69, 69), (120, 121), (124, 129), (130, 139), (160, 161),
(164, 169), (180, 182), (185, 185), (189, 189), (190, 192),
(194, 194), (196, 199), (260, 269), (270, 279), (300, 309),
(330, 339), (358, 359), (360, 369)
]
您可以使用之前定义的print_faces函数检查这些值,绘制 400 个人脸并查看左下角的索引。
然后我们将定义一个函数,从这些片段返回一个新的目标数组,用1标记带有眼镜的人脸,而0用于没有眼镜的人脸(我们的新目标类):
>>> def create_target(segments):
>>> # create a new y array of target size initialized with
zeros
>>> y = np.zeros(faces.target.shape[0])
>>> # put 1 in the specified segments
>>> for (start, end) in segments:
>>> y[start:end + 1] = 1
>>> return y
>>> target_glasses = create_target(glasses)
所以我们必须再次进行训练/测试。
>>> X_train, X_test, y_train, y_test = train_test_split(
faces.data, target_glasses, test_size=0.25, random_state=0)
现在让我们创建一个新的 SVC 分类器,并使用以下命令使用新的目标向量训练它:
>>> svc_2 = SVC(kernel='linear')
如果我们通过以下代码检查交叉验证的表现:
>>> evaluate_cross_validation(svc_2, X_train, y_train, 5)
[ 0.98333333 0.98333333 0.93333333 0.96666667 0.96666667]
Mean score: 0.967 (+/-0.009)
如果我们在测试集上进行评估,我们使用交叉验证获得 0.967 的平均准确率。
>>> train_and_evaluate(svc_2, X_train, X_test, y_train, y_test)
Accuracy on training set:
1.0
Accuracy on testing set:
0.99
Classification Report:
precision recall f1-score support
0 1.00 0.99 0.99 67
1 0.97 1.00 0.99 33
avg / total 0.99 0.99 0.99 100
Confusion Matrix:
[[66 1]
[ 0 33]]
我们的分类器是否可能学会识别有眼镜和没有眼镜的人脸?我们怎么能确定这种情况没有发生呢?如果我们得到新的没见过的人脸,它会按预期工作吗?让我们分离同一个人的所有图像,有时戴眼镜,有时不戴眼镜。我们还将同一个人的所有图像,索引从 30 到 39 的图像分离,通过使用剩余的实例进行训练,并评估我们新的 10 个实例集。通过这个实验,我们将尝试排除一个事实,它记住人脸,而不是眼镜相关的特征。
>>> X_test = faces.data[30:40]
>>> y_test = target_glasses[30:40]
>>> print y_test.shape[0]
10
>>> select = np.ones(target_glasses.shape[0])
>>> select[30:40] = 0
>>> X_train = faces.data[select == 1]
>>> y_train = target_glasses[select == 1]
>>> print y_train.shape[0]
390
>>> svc_3 = SVC(kernel='linear')
>>> train_and_evaluate(svc_3, X_train, X_test, y_train, y_test)
Accuracy on training set:
1.0
Accuracy on testing set:
0.9
Classification Report:
precision recall f1-score support
0 0.83 1.00 0.91 5
1 1.00 0.80 0.89 5
avg / total 0.92 0.90 0.90 10
Confusion Matrix:
[[5 0]
[1 4]]
10 张图片中,只有一个错误, 仍然是非常好的结果,让我们看看哪一个被错误分类。首先,我们必须将数据从数组重新整形为64 x 64矩阵:
>>> y_pred = svc_3.predict(X_test)
>>> eval_faces = [np.reshape(a, (64, 64)) for a in X_eval]
然后使用我们的print_faces函数绘图:
>>> print_faces(eval_faces, y_pred, 10)
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_02_03.jpg
上图中的图像编号8带有眼镜,并且被分类为无眼镜。如果我们看一下这个例子,我们可以看到它与其他带眼镜的图像不同(眼镜的边框看不清楚,人闭着眼睛),这可能就是它误判的原因。
通过几行,我们创建了一个带有线性 SVM 模型的人脸分类器。通常我们在第一次试验中不会得到如此好的结果。在这些情况下(除了查看不同的特征),我们可以开始调整算法的超参数。在 SVM 的特定情况下,我们可以尝试不同的核函数;如果线性没有给出好的结果,我们可以尝试使用多项式或 RBF 核。此外,C和gamma参数可能会影响结果。有关参数及其值的说明,请参阅 scikit-learn 文档。
朴素贝叶斯和文本分类
朴素贝叶斯是一个简单但强大的分类器,它基于贝叶斯定理推导出的概率模型。基本上,它基于每个特征值概率,确定实例属于类的概率。朴素的术语来自于它假定每个特征独立于其余特征,即特征的值与另一个特征的值无关。
尽管非常简单,但它已在许多领域中使用,并且具有非常好的结果。独立性假设虽然是一种朴素而强烈的简化,但却是使该模型在实际应用中有用的特性之一。训练模型被简化为所涉及的条件概率的计算,它可以通过计算特征值和类值之间的相关性频率来估计。
朴素贝叶斯最成功的应用之一是自然语言处理(NLP)。NLP 是一个与机器学习密切相关的领域,因为它的许多问题可以被表述为分类任务。通常,NLP 问题有用文本文档的形式的大量标记数据。该数据可用作机器学习算法的训练数据集。
在本节中,我们将使用朴素贝叶斯进行文本分类;我们将有一组带有相应类别的文本文档,我们将训练一个朴素贝叶斯算法,来学习预测新的没见过的实例的类别。这项简单的任务有许多实际应用;可能是最知名和广泛使用的垃圾邮件过滤。在本节中,我们将尝试使用可以从 scikit-learn 中检索的数据集,对新闻组消息进行分类。该数据集包括来自 20 个不同主题的大约 19,000 条新闻组信息,从政治和宗教到体育和科学。
像往常一样,我们首先导入我们的pylab环境:
>>> %pylab inline
我们的数据集可以通过从sklearn.datasets模块导入fetch_20newgroups函数来获得。我们必须指定我们是否要导入部分或全部实例(我们将导入所有实例)。
>>> from sklearn.datasets import fetch_20newsgroups
>>> news = fetch_20newsgroups(subset='all')
如果我们查看数据集的属性,我们会发现我们有通常的那些:DESCR,data,target和target_names。现在的区别是数据包含文本内容列表,而不是numpy矩阵:
>>> print type(news.data), type(news.target), type(news.target_names)
<type 'list'> <type 'numpy.ndarray'> <type 'list'>
>>> print news.target_names
['alt.atheism', 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.mac.hardware', 'comp.windows.x', 'misc.forsale', 'rec.autos', 'rec.motorcycles', 'rec.sport.baseball', 'rec.sport.hockey', 'sci.crypt', 'sci.electronics', 'sci.med', 'sci.space', 'soc.religion.christian', 'talk.politics.guns', 'talk.politics.mideast', 'talk.politics.misc', 'talk.religion.misc']
>>> print len(news.data)
18846
>>> print len(news.target)
18846
如果您查看第一个实例,您将看到新闻组消息的内容,您可以获得相应的类别:
>>> print news.data[0]
>>> print news.target[0], news.target_names[news.target[0]]
预处理数据
我们的机器学习算法只能用于数字数据,因此我们的下一步是将基于文本的数据集转换为数字数据集。目前我们只有一个特征,即消息的文本内容;我们需要一些函数,将文本转换为一组有意义的数字特征。直观地,我们可以尝试查看每个文本类别中使用的单词(或更确切地说,标记,包括数字或标点符号),并尝试表示每个类别中每个单词的频率分布。sklearn.feature_extraction.text模块具有一些有用的工具,可以从文本文档构建数字特征向量。
在开始转换之前,我们必须将数据划分为训练和测试集。加载的数据已经是随机顺序,因此我们只需要将数据分成例如 75% 用于训练,其余 25% 用于测试:
>>> SPLIT_PERC = 0.75
>>> split_size = int(len(news.data)*SPLIT_PERC)
>>> X_train = news.data[:split_size]
>>> X_test = news.data[split_size:]
>>> y_train = news.target[:split_size]
>>> y_test = news.target[split_size:]
如果您查看sklearn.feature_extraction.text模块,您会发现三个不同的类可以将文本转换为数字特征:CountVectorizer,HashingVectorizer和TfidfVectorizer。它们之间的区别在于它们为获得数字特征而执行的计算。 CountVectorizer基本上从文本语料库中创建单词词典。然后,将每个实例转换为数字特征的向量,其中每个元素将是特定单词在文档中出现的次数的计数。
HashingVectorizer在内存中限制并维护字典,实现了将标记映射到特征索引的散列函数,然后计算CountVectorizer中的计数。
TfidfVectorizer的工作方式与CountVectorizer类似,但更高级的计算称为单词频率逆文档频率(TF-IDF)。这是用于测量在文档或语料库中单词的重要性的统计量。直观地说,它在当前文档中查找中更频繁的单词,与它们在整个文档集中的频率的比值。您可以将此视为一种方法,标准化结果并避免单词过于频繁而无法用于表征实例。
训练朴素贝叶斯分类器
我们将创建一个朴素贝叶斯分类器,它由一个特征向量化器和实际的贝叶斯分类器组成。我们将使用sklearn.naive_bayes模块中的MultinomialNB类。为了用向量化器组成分类器,正如我们在第一章中看到的那样,scikit-learn 在sklearn.pipeline模块中有一个非常有用的类,称为Pipeline,可以简化复合分类器的构建,该分类器由几个向量化器和分类器组成。
我们将通过将MultinomialNB与刚刚提到的三个不同的文本向量化器相结合来创建三个不同的分类器,并使用默认参数比较哪个分类器更好:
>>> from sklearn.naive_bayes import MultinomialNB
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.feature_extraction.text import TfidfVectorizer, >>> HashingVectorizer, CountVectorizer
>>>
>>> clf_1 = Pipeline([
>>> ('vect', CountVectorizer()),
>>> ('clf', MultinomialNB()),
>>> ])
>>> clf_2 = Pipeline([
>>> ('vect', HashingVectorizer(non_negative=True)),
>>> ('clf', MultinomialNB()),
>>> ])
>>> clf_3 = Pipeline([
>>> ('vect', TfidfVectorizer()),
>>> ('clf', MultinomialNB()),
>>> ])
我们将定义一个函数,该函数接受分类器,并对指定的X和y值执行K折交叉验证:
>>> from sklearn.cross_validation import cross_val_score, KFold
>>> from scipy.stats import sem
>>>
>>> def evaluate_cross_validation(clf, X, y, K):
>>> # create a k-fold croos validation iterator of k=5 folds
>>> cv = KFold(len(y), K, shuffle=True, random_state=0)
>>> # by default the score used is the one returned by score >>> method of the estimator (accuracy)
>>> scores = cross_val_score(clf, X, y, cv=cv)
>>> print scores
>>> print ("Mean score: {0:.3f} (+/-{1:.3f})").format(
>>> np.mean(scores), sem(scores))
然后我们将使用每个分类器执行五折交叉验证。
>>> clfs = [clf_1, clf_2, clf_3]
>>> for clf in clfs:
>>> evaluate_cross_validation(clf, news.data, news.target, 5)
这些计算可能需要一些时间;结果如下:
[ 0.86813478 0.86415495 0.86893075 0.85831786 0.8729443 ]
Mean score: 0.866 (+/-0.002)
[ 0.76359777 0.77182276 0.77765986 0.76147519 0.78222812]
Mean score: 0.771 (+/-0.004)
[ 0.86282834 0.85195012 0.86282834 0.85619528 0.87612732]
Mean score: 0.862 (+/-0.004)
正如您所见,CountVectorizer和TfidfVectorizer具有相似的表现,并且比HashingVectorizer好得多。
让我们继续TfidfVectorizer;我们可以通过尝试使用不同正则表达式将文本文档解析为标记来改进结果。
>>> clf_4 = Pipeline([
>>> ('vect', TfidfVectorizer(
>>> token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0->>> 9_\-
>>> \.]+\b",
>>> )),
>>> ('clf', MultinomialNB()),
>>> ])
默认正则表达式:ur"\b\w\w+\b"考虑字母数字字符和下划线。也许还考虑斜线和点可以改善分词,并开始将标记视为Wi-Fi和site.com。新的正则表达式可能是:ur"\b[a-z0-9_\-\.]+[a-z][a-z0-9_\-\.]+\b"。如果您对如何定义正则表达式有疑问,请参考 Python re模块文档。让我们尝试新的分类器:
>>> evaluate_cross_validation(clf_4, news.data, news.target, 5)
[ 0.87078801 0.86309366 0.87689042 0.86574688 0.8795756 ]
Mean score: 0.871 (+/-0.003)
我们稍微改善了,从 0.86 到 0.87。
我们可以使用的另一个参数是stop_words:这个参数允许我们传递一个我们不想考虑的单词列表,例如过于频繁的单词,或者一些单词,我们不希望它们事先提供有关特定话题的信息。
我们将定义一个函数来加载文本文件中的停止词,如下所示:
>>> def get_stop_words():
>>> result = set()
>>> for line in open('stopwords_en.txt', 'r').readlines():
>>> result.add(line.strip())
>>> return result
并使用以下新参数创建一个新的分类器:
>>> clf_5 = Pipeline([
>>> ('vect', TfidfVectorizer(
>>> stop_words= get_stop_words(),
>>> token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0->>> 9_\-\.]+\b",
>>> )),
>>> ('clf', MultinomialNB()),
>>> ])
>>> evaluate_cross_validation(clf_5, news.data, news.target, 5)
[ 0.88989122 0.8837888 0.89042186 0.88325816 0.89655172]
Mean score: 0.889 (+/-0.002)
前面的代码显示了从 0.87 到 0.89 的另一个改进。
让我们保留这个向量化器并开始查看MultinomialNB参数。这个分类器几乎没有可调的参数;最重要的是alpha参数,它是一个平滑参数。我们将其设置为较低的值;不将alpha设置为1.0(默认值),我们将其设置为0.01:
>>> clf_7 = Pipeline([
>>> ('vect', TfidfVectorizer(
>>> stop_words=stop_words,
>>> token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0->>> 9_\-\.]+\b",
>>> )),
>>> ('clf', MultinomialNB(alpha=0.01)),
>>> ])
>>> evaluate_cross_validation(clf_7, news.data, news.target, 5)
[ 0.92305651 0.91377023 0.92066861 0.91907668 0.92281167]
Mean score: 0.920 (+/-0.002)
结果从 0.89 升至 0.92,非常好。此时,我们可以通过使用不同的alpha值或对向量化器进行新的修改来继续进行试验。在第四章“高级功能”中,我们将向您展示尝试多种工具,尝试不同配置并保持最佳配置。但就目前而言,让我们再看看朴素贝叶斯模型。
评估表现
如果我们确定在我们的模型中做了足够的改进,我们就可以在测试集上评估其表现。
我们将定义一个辅助函数,该函数将在整个训练集中训练模型,并评估训练和测试集中的准确率。它还将打印分类报告(每个类的精确率和召回率)和相应的混淆矩阵:
>>> from sklearn import metrics
>>>
>>> def train_and_evaluate(clf, X_train, X_test, y_train, y_test):
>>>
>>> clf.fit(X_train, y_train)
>>>
>>> print "Accuracy on training set:"
>>> print clf.score(X_train, y_train)
>>> print "Accuracy on testing set:"
>>> print clf.score(X_test, y_test)
>>> y_pred = clf.predict(X_test)
>>>
>>> print "Classification Report:"
>>> print metrics.classification_report(y_test, y_pred)
>>> print "Confusion Matrix:"
>>> print metrics.confusion_matrix(y_test, y_pred)
我们将评估我们的最佳分类器。
>>> train_and_evaluate(clf_7, X_train, X_test, y_train, y_test)
Accuracy on training set:
0.99398613273
Accuracy on testing set:
0.913837011885
正如我们所看到的,我们获得了非常好的结果,正如我们所期望的那样,训练集的准确率比测试集中的准确率要好。在新的没见过的实例中,我们可能会预计,准确率约为 0.91。
如果我们查看向量化器,我们可以看到哪些标记已用于创建我们的字典:
>>> print len(clf_7.named_steps['vect'].get_feature_names())
61236
这表明该词典由 61236 个标记组成。让我们打印特征名称。
>>> clf_7.named_steps['vect'].get_feature_names()
下表显示了结果的摘录:
| 向量化器获得的特征 |
|---|
u''sanctuaries'',u''sanctuary'',u''sanctum'',u''sand'',u''sandals'',u''sandbags'',u''sandberg'',u''sandblasting'',u''sanders'', |
你可以看到一些词在语义上非常相似,例如sand和sands,sanctuary和sanctuaries。也许如果将复数和单数计算在同一个桶中,我们能最好地表示文档。这是一个非常常见的任务,可以使用词干提取来解决,词干提取是一种技术,关联具有相同词汇词根的两个词。
使用决策树解释泰坦尼克号假设
针对线性分类器和针对统计学习方法的一个常见观点是,难以解释构建的模型如何决定其对目标类的预测。如果你有一个高维度的 SVM,那么人类甚至无法想象超平面的构建方式。 朴素贝叶斯分类器会告诉你类似的事情:“这个类是最可能的,假设它来自与训练数据类似的分布,并做出一些假设”,一些不太有用的东西,例如,我们想知道为什么这个或那个邮件应该被视为垃圾邮件。
决策树是非常简单而强大的监督学习方法,它构建了一个决策树模型,用于进行预测。下图显示了一个非常简单的决策树,用于确定是否应将电子邮件视为垃圾邮件:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_02_04.jpg
它首先询问电子邮件是否包含单词Viagra;如果答案是肯定的,它会将其归类为垃圾邮件;如果答案是否定的,它会进一步询问,它是否来自您的联系人列表中的某个人;这次,如果答案是肯定的,它会将电子邮件归类为正常;如果答案是否定的,则将其归类为垃圾邮件。该模型的主要优点是,人类可以轻松地理解和再现决策序列(特别是如果属性的数量很小),来预测新实例的目标类。这对于医疗诊断或信用审批等任务非常重要,我们希望在这些任务中显示决策的原因,而不是仅仅说,这是训练数据所表明的内容(根据定义,这是每种监督学习方法的作用) 。在本节中,我们将通过一个工作示例向您展示决策树的外观,它们是如何构建的,以及它们如何用于预测。
我们想要解决的问题是,确定泰坦尼克号的乘客是否会幸存下来,考虑到年龄,乘客等级和性别。我们将使用泰坦尼克数据集。与本章中的其他所有示例一样,我们从数据集开始,包含泰坦尼克号乘客列表,以及表明他们是否幸存的特征。数据集中的每个实例都具有以下形式:
"1","1st",1,"Allen, Miss Elisabeth Walton",29.0000,"Southampton","St Louis, MO","B-5","24160 L221","2","female"
属性列表为:Ordinal(序号),Class(等级),Survived(是否幸存,0=no,1=yes),Name(名称),Age(年龄),Port of Embarkation(登船港口),Home/Destination(家/目的地),Room(房间),Ticket(票号),Boat(救生艇)和Sex(性别)。我们将首先将数据集加载到numpy数组中。
>>> import csv
>>> import numpy as np
>>> with open('data/titanic.csv', 'rb') as csvfile:
>>> titanic_reader = csv.reader(csvfile, delimiter=',',
>>> quotechar='"')
>>>
>>> # Header contains feature names
>>> row = titanic_reader.next()
>>> feature_names = np.array(row)
>>>
>>> # Load dataset, and target classes
>>> titanic_X, titanic_y = [], []
>>> for row in titanic_reader:
>>> titanic_X.append(row)
>>> titanic_y.append(row[2]) # The target value is
"survived"
>>>
>>> titanic_X = np.array(titanic_X)
>>> titanic_y = np.array(titanic_y)
显示的代码使用 Python csv模块加载数据。
>>> print feature_names
['row.names' 'pclass' 'survived' 'name' 'age' 'embarked' 'home.dest' 'room' 'ticket' 'boat' 'sex']
>>> print titanic_X[0], titanic_y[0]
['1' '1st' '1' 'Allen, Miss Elisabeth Walton' '29.0000' 'Southampton' 'St Louis, MO' 'B-5' '24160 L221' '2' 'female'] 1
预处理数据
我们必须采取的第一步是选择我们将用于学习的属性:
>>> # we keep class, age and sex
>>> titanic_X = titanic_X[:, [1, 4, 10]]
>>> feature_names = feature_names[[1, 4, 10]]
基于其余属性对乘客生存没有影响的假设,我们选择了特征1,4和10,即等级,年龄和性别。在创建机器学习解决方案时,特征选择是非常重要的一步。如果算法没有良好的特征作为输入,它将没有足够的材料可供学习,结果将不会很好,即使我们拥有有史以来最佳设计的机器学习算法。
有时,根据我们对问题域的了解,以及我们计划使用的机器学习方法,手动进行特征选择。有时可以通过使用自动工具,来评估和选择最有希望的特征来完成特征选择。在第四章“高级功能”中,我们将讨论这些技术,但是现在,我们将手动选择我们的属性。非常具体的属性(例如我们案例中的Name)可能导致过拟合(考虑一个树,只询问名称是否为X,就判定她幸存下来);每个值都有少量实例的属性,存在类似问题(它们可能对泛化无用)。我们将使用等级,年龄和性别,因为先验,我们期望它们影响乘客的生存。
现在,我们的学习数据如下:
>>> print feature_names
['pclass' 'age' 'sex']
>>> print titanic_X[12],titanic_y[12]
['1st' 'NA' 'female'] 1
我们已经显示了实例编号12,因为它提出了一个需要解决的问题;其中一个特征(年龄)不可用。我们有缺失值,这是数据集的常见问题。在这种情况下,我们决定用训练数据中的平均年龄替换缺失值。我们可以采用不同的方法,例如,使用训练数据中最常见的值或中值。当我们替换缺失值时,我们必须理解我们正在修改原始问题,因此我们必须非常小心我们正在做的事情。这是机器学习的一般规则;当我们改变数据时,我们应该清楚地知道我们正在改变什么,以避免扭曲最终结果。
>>> # We have missing values for age
>>> # Assign the mean value
>>> ages = titanic_X[:, 1]
>>> mean_age = np.mean(titanic_X[ages != 'NA',
1].astype(np.float))
>>> titanic_X[titanic_X[:, 1] == 'NA', 1] = mean_age
scikit-learn 中的决策树的实现,期望实值特征列表作为输入,并且模型的决策规则将具有以下形式:
Feature <= value
例如,age <= 20.0。我们的属性(年龄除外)是绝对的;也就是说,它们对应于从离散集合中获取的值,诸如男性和女性。因此,我们必须将分类数据转换为实际值。让我们从性别特征开始吧。scikit-learn 的预处理模块包括LabelEncoder类,其fit方法允许将类别集合转换为0..K-1的整数,其中K是集合中不同类的数量(对于性别,只有 0 或 1):
>>> # Encode sex
>>> from sklearn.preprocessing import LabelEncoder
>>> enc = LabelEncoder()
>>> label_encoder = enc.fit(titanic_X[:, 2])
>>> print "Categorical classes:", label_encoder.classes_
Categorical classes: ['female' 'male']
>>> integer_classes =
label_encoder.transform(label_encoder.classes_)
>>> print "Integer classes:", integer_classes
Integer classes: [0 1]
>>> t = label_encoder.transform(titanic_X[:, 2])
>>> titanic_X[:, 2] = t
最后两行将的性别属性值转换为0-1值,并修改训练集。
print feature_names
['pclass' 'age' 'sex']
print titanic_X[12], titanic_y[12]
['1st' '31.1941810427' '0'] 1
我们仍然有一个分类属性:class。我们可以使用相同的方法并将其三个类转换为 0, 1 和 2。这种转换隐式地引入了类之间的顺序,这在我们的问题中不是问题。但是,我们将尝试一种不假设顺序的更通用的方法,并且它被广泛用于将类别转换为实值属性。我们将引入一个额外的编码器,并将类属性转换为三个新的二元特征,每个特征表明实例是否属于特征值(1)或(0)。这被称为单热编码,它是一种非常常见的方式,为基于实值的方法管理类别属性:
>>> from sklearn.preprocessing import OneHotEncoder
>>>
>>> enc = LabelEncoder()
>>> label_encoder = enc.fit(titanic_X[:, 0])
>>> print "Categorical classes:", label_encoder.classes_
Categorical classes: ['1st' '2nd' '3rd']
>>> integer_classes =
label_encoder.transform(label_encoder.classes_).reshape(3, 1)
>>> print "Integer classes:", integer_classes
Integer classes: [[0] [1] [2]]
>>> enc = OneHotEncoder()
>>> one_hot_encoder = enc.fit(integer_classes)
>>> # First, convert classes to 0-(N-1) integers using
label_encoder
>>> num_of_rows = titanic_X.shape[0]
>>> t = label_encoder.transform(titanic_X[:,
0]).reshape(num_of_rows, 1)
>>> # Second, create a sparse matrix with three columns, each one
indicating if the instance belongs to the class
>>> new_features = one_hot_encoder.transform(t)
>>> # Add the new features to titanix_X
>>> titanic_X = np.concatenate([titanic_X,
new_features.toarray()], axis = 1)
>>> #Eliminate converted columns
>>> titanic_X = np.delete(titanic_X, [0], 1)
>>> # Update feature names
>>> feature_names = ['age', 'sex', 'first_class', 'second_class',
'third_class']
>>> # Convert to numerical values
>>> titanic_X = titanic_X.astype(float)
>>> titanic_y = titanic_y.astype(float)
前面的代码首先将类转换为整数,然后使用OneHotEncoder类创建添加到特征数组的三个新属性。它最终从训练数据中消除了原始的class特征。
>>> print feature_names
['age', 'sex', 'first_class', 'second_class', 'third_class']
>>> print titanic_X[0], titanic_y[0]
[29\. 0\. 1\. 0\. 0.] 1.0
我们现在有一个适合 scikit-learn 的学习集来学习决策树。此外,标准化不是决策树的问题,因为特征的相对大小不会影响分类器表现。
预处理步骤在机器学习方法中通常被低估,但我们在这个非常简单的例子中看到,可能需要一些时间,来使数据看起来像我们的方法的预期。它在整个机器学习过程中也非常重要;如果我们在此步骤中失败(例如,错误地编码属性或选择错误的特征),则无论我们用于学习的方法有多好,以下步骤都将失败。
训练决策树分类器
现在到了有趣的部分;让我们根据训练数据建立一个决策树。像往常一样,我们将首先分开训练和测试数据。
>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(titanic_X, >>> titanic_y, test_size=0.25, random_state=33)
现在,我们可以创建一个新的DecisionTreeClassifier并使用分类器的fit方法来完成学习工作。
>>> from sklearn import tree
>>> clf = tree.DecisionTreeClassifier(criterion='entropy',
max_depth=3,min_samples_leaf=5)
>>> clf = clf.fit(X_train,y_train)
DecisionTreeClassifier接受控制其行为的几个超参数(像大多数学习方法一样)。在这种情况下,我们使用信息增益(IG)标准来分割学习数据,告诉方法最多构建三层的树,并且如果节点包括至少五个训练实例,则接受该节点作为叶子。为了解释这一点并展示决策树如何工作,让我们可视化建立的模型。以下代码假定您使用的是 IPython,并且您的 Python 发行版包含pydot模块。此外,它允许从树生成 Graphviz 代码,并假设安装了 Graphviz 本身。有关 Graphviz 的更多信息,请参阅这里。
>>> import pydot,StringIO
>>> dot_data = StringIO.StringIO()
>>> tree.export_graphviz(clf, out_file=dot_data,
feature_names=['age','sex','1st_class','2nd_class'
'3rd_class'])
>>> graph = pydot.graph_from_dot_data(dot_data.getvalue())
>>> graph.write_png('titanic.png')
>>> from IPython.core.display import Image
>>> Image(filename='titanic.png')
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_02_05.jpg
我们构建的决策树代表了一系列基于训练数据的决策。要对实例进行分类,我们应该回答每个节点的问题。例如,在我们的根节点,问题是:性别<= 0.5嘛?(我们在谈论一个女人吗?)如果答案是肯定的,则转到树中的左子节点;否则访问右子节点。你一直在回答问题(她是在三等舱吗?她是在头等舱吗?她是 13 岁以下吗?),直到你到达了叶子。当你在那里时,预测具有大多数实例的目标类(即,如果答案被给予先前的问题)。在我们的案例中,如果她是二等舱的女性,那么答案就是 1(即她活了下来),依此类推。
您可能会问,我们的方法如何决定每一步在中应该询问哪些问题。答案是信息增益( IG,或基尼指数,这是一种 scikit-learn 使用的类似的无序度量)。如果我们回答这个问题,IG 会测量我们失去多少熵,或者在回答问题之后,我们确定了多少熵。熵是分组中的无序度量,如果我们熵为零,则意味着所有值都相同(在我们的例子中,所有实例的目标类都是相同的) ,当每个类的实例数相等时达到最大值(在我们的例子中,当一半实例对应幸存者而另一半对应于非幸存者)。在每个节点,我们有一定数量的实例(从整个数据集开始),我们测量它的熵。当我们仅考虑那些实例时,其中问题答案为是或否,即当回答问题后的熵减少时,我们的方法将选择产生更均匀分区(具有最低熵)的问题。
解释决策树
正如您在树中看到的那样,在决策树生长过程的开始处,您在训练集中有 984 个实例,其中 662 个对应于类0(死亡),以及 322 个对应于类1(幸存者)。该初始组的熵测量值约为 0.632。从我们可以提出的问题清单中,产生最大信息增益的问题是:她是女性吗?(记住,女性类别被编码为0)。如果答案是肯定的,那么熵几乎是相同的,但如果答案是否定的,那么它就会大大减少(死亡者比例远远大于幸存者比例)。从这个意义上讲,女性的问题似乎是最好的问题。之后,该过程继续,在每个节点中,仅仅使用这些实例,它们的特征值与节点路径中的问题对应。
如果你看一下树,我们在每个节点中都有:问题,初始香农熵,我们正在考虑的实例数,以及它们相对于目标类的分布。在每个步骤中,实例的数量减少到对该节点提出的问题回答是(左分支)和否(右分支)的实例数。该过程一直持续到满足某个停止标准(在我们的例子中,直到我们有第四级节点,或者所考虑的样本数低于 5)。
在预测时,我们选取一个实例并开始遍历树,根据实例特征回答问题,直到我们到达一个叶子。此时,我们将查看训练集中每个类的实例数,并选择大多数实例所属的类。
例如,考虑确定一个 10 岁女孩是否能够幸存下来的问题。第一个问题的答案(她是女性吗?)是肯定的,所以我们走树的左分支。在接下来的两个问题中答案是否(她来自三等舱?)和是(她来自头等舱?),所以我们分别走左右分支。这时,我们已经到了一片叶子。在训练集中,我们有 102 人具有这些属性,其中 97 人是幸存者。所以,我们的答案是幸存。
一般来说,我们找到了合理的结果:死亡人数较多的群体(496 人中有 449 人)对应二等或三等舱的成年男子,因为您可以在树上查看。大多数来自头等舱的女孩幸存下来。让我们在训练集中测量方法的准确率(我们将首先定义辅助函数来测量分类器的表现):
>>> from sklearn import metrics
>>> def measure_performance(X,y,clf, show_accuracy=True,
show_classification_report=True, show_confussion_matrix=True):
>>> y_pred=clf.predict(X)
>>> if show_accuracy:
>>> print "Accuracy:{0:.3f}".format(
>>> metrics.accuracy_score(y, y_pred)
>>> ),"\n"
>>>
>>> if show_classification_report:
>>> print "Classification report"
>>> print metrics.classification_report(y,y_pred),"\n"
>>>
>>> if show_confussion_matrix:
>>> print "Confussion matrix"
>>> print metrics.confusion_matrix(y,y_pred),"\n"
>>> measure_performance(X_train,y_train,clf,
show_classification=False, show_confusion_matrix=False))
Accuracy:0.838
我们的树在训练集上的准确率为 0.838。但请记住,这不是一个好的指标。对于决策树尤其如此,因为该方法非常容易过拟合。由于我们没有将评估集分开,因此我们应该应用交叉验证。对于这个例子,我们将使用交叉验证的极端情况,命名为留一交叉验证。对于训练样本中的每个实例,我们对样本的其余部分进行训练,并仅在留出的实例上评估构建的模型。在执行与训练实例一样多的分类之后,我们将准确率简单地计算为,我们的方法正确预测剩余实例的类的次数的比例,并且发现它稍低(如我们所预期的),而不是训练集上的重新计算的准确率。
>>> from sklearn.cross_validation import cross_val_score, LeaveOneOut
>>> from scipy.stats import sem
>>>
>>> def loo_cv(X_train, y_train,clf):
>>> # Perform Leave-One-Out cross validation
>>> # We are preforming 1313 classifications!
>>> loo = LeaveOneOut(X_train[:].shape[0])
>>> scores = np.zeros(X_train[:].shape[0])
>>> for train_index, test_index in loo:
>>> X_train_cv, X_test_cv = X_train[train_index],
X_train[test_index]
>>> y_train_cv, y_test_cv = y_train[train_index],
y_train[test_index]
>>> clf = clf.fit(X_train_cv,y_train_cv)
>>> y_pred = clf.predict(X_test_cv)
>>> scores[test_index] = metrics.accuracy_score(
y_test_cv.astype(int), y_pred.astype(int))
>>> print ("Mean score: {0:.3f} (+/-{1:.3f})").format(np.mean(scores), sem(scores))
>>> loo_cv(X_train, y_train,clf)
Mean score: 0.837 (+/-0.012)
留一法交叉验证的主要优点是它允许训练的数据几乎与我们可用的数据一样多,因此它特别适用于数据稀缺的情况。其主要问题是,就计算时间而言,为每个实例训练不同的分类器可能是非常昂贵的。
这里还有一个很大的问题:我们如何为方法实例化选择超参数?这个问题是一般的 ,它被称为模型选择, 我们将在第四章“高级功能”中更详细地解决它。
随机森林 - 随机决策
对决策树的一个常见批评是,一旦在回答问题后对训练集进行划分,就不可能重新考虑这个决策。例如,如果我们将男性和女性分开,那么每个后续问题都只涉及男性或女性,而且该方法不能考虑其他类型的问题(例如,年龄不到一岁,不论性别如何)。随机森林尝试在每个步骤中引入一定程度的随机化,创建备选树并将它们组合来获得最终预测。考虑几个回答相同问题的分类器的这些类型的算法,被称为集成方法。在泰坦尼克号任务中,可能很难看到这个问题,因为我们的特征很少,但考虑到特征数量达到数千的情况。
随机森林建议基于训练实例的子集(带放回随机选择)来构建决策树,但是在特征集的每个集合中使用少量随机的特征。这种树生长过程重复几次,产生一组分类器。在预测时,给定一个实例的每个成型的树都会像决策树一样预测其目标类。大多数树所投票的类(即树中预测最多的类)是集成分类器所建议的类。
在 scikit-learn 中,使用随机森林,并按如下方式拟合训练数据,就像从 sklearn.ensemble模块导入RandomForestClassifier一样简单:
>>> from sklearn.ensemble import RandomForestClassifier
>>> clf = RandomForestClassifier(n_estimators=10, random_state=33)
>>> clf = clf.fit(X_train, y_train)
>>> loo_cv(X_train, y_train, clf)
Mean score: 0.817 (+/-0.012)
我们发现随机森林的结果实际上更糟。毕竟,引入随机化似乎不是一个好主意,因为特征数量太少。然而,对于具有更多特征的更大数据集,随机森林是一种非常快速,简单且流行的方法,可以提高准确率,保留决策树的优点。实际上,在下一节中,我们将使用它们进行回归。
评估表现
每个监督学习任务的最后一步应该是,在以前没见过的数据上评估我们的最佳分类器,来了解其预测表现。请记住,此步骤不应用于在竞争方法或参数中进行选择。这将是作弊(因为我们再次有过拟合新数据的风险)。因此,在我们的例子中,让我们测量决策树对测试数据的表现。
>>> clf_dt = tree.DecisionTreeClassifier(criterion='entropy', max_depth=3, min_samples_leaf=5)
>>> clf_dt.fit(X_train, y_train)
>>> measure_performance(X_test, y_test, clf_dt)
Accuracy:0.793
Classification report
precision recall f1-score support
0 0.77 0.96 0.85 202
1 0.88 0.54 0.67 127
avg / total 0.81 0.79 0.78 329
Confusion matrix
[[193 9]
[ 59 68]]
从分类结果和混淆矩阵来看,似乎我们的方法倾向于预测太多人没有幸存。
通过回归预测房价
在我们见过的每个例子中,我们已经在第一章“机器学习 - 温和介绍”见过了,我们称之为分类问题:我们打算预测的输出属于离散集。但通常,我们希望预测从真实直线中提取的值。学习模式仍然相同:将模型拟合到训练数据,并评估新数据来获得实数值的目标类。我们的分类器不是从列表中选择一个类,而应该成为一个实值函数,对于每个(可能是无限的)学习特征组合返回一个实数。我们可以将回归视为具有无限目标类别的分类。
根据我们选择作为目标的类,可以将许多问题建模为分类和回归任务。例如,预测血糖水平是回归任务,而预测某人是否患有糖尿病则是分类任务。
在第一个图的示例中,我们使用一条线来拟合学习数据(由唯一属性和目标值组成),也就是说,我们已经执行了线性回归。如果我们想要预测新实例的值, 我们得到它们的实值属性,并通过将推断直线投影到第二个轴来获得预测值。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_02_06.jpg
在本节中,我们将使用相同的数据集比较几种回归方法。我们将尝试根据房屋属性预测价格。作为数据集,我们将使用波士顿房价数据集,其中包括 506 个实例,通过 14 个特征代表波士顿郊区的房屋,其中一个(自住房屋的中位数)是目标类。此数据集中的每个属性都是实值。
数据集包含在标准的 scikit-learn 分布中,所以让我们先加载它:
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from sklearn.datasets import load_boston
>>> boston = load_boston()
>>> print boston.data.shape
(506, 13)
>>> print boston.feature_names
['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' 'RAD' 'TAX' 'PTRATIO' 'B' 'LSTAT' 'MEDV']
>>> print np.max(boston.target), np.min(boston.target),
np.mean(boston.target)
50.0 5.0 22.5328063241
您应该尝试打印boston.DESCR来了解每个特征的含义。这是一个非常良好的习惯:机器学习不仅仅是数字运算,理解我们面临的问题至关重要,尤其是选择最佳学习模型。
像往常一样,我们开始将学习集分割为训练和测试数据集,并对数据进行规范化:
>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test =
train_test_split(boston.data, boston.target, test_size=0.25,
random_state=33)
>>> from sklearn.preprocessing import StandardScaler
>>> scalerX = StandardScaler().fit(X_train)
>>> scalery = StandardScaler().fit(y_train)
>>> X_train = scalerX.transform(X_train)
>>> y_train = scalery.transform(y_train)
>>> X_test = scalerX.transform(X_test)
>>> y_test = scalery.transform(y_test)
在查看我们的最佳分类器之前,让我们定义如何比较我们的结果。由于我们希望保留用于评估最终分类器表现的测试集,因此我们应该找到一种方法来选择最佳模型,同时避免过拟合。我们已经知道答案:交叉验证。回归带来了另一个问题:我们应该如何评估我们的结果?准确率不是一个好主意,因为我们预测真实值,我们几乎不可能准确预测最终值。有几种措施可以使用(您可以查看sklearn.metrics模块下的函数列表)。最常见的是 R 方评分,或测定系数,用于测量模型解释的结果的变动比例,以及它是 scikit-learn 中回归方法的默认得分函数。当模型完美地预测所有测试目标值时,该分数达到最大值1。使用这一度量,我们将建立一个函数,训练模型并使用五重交叉验证和测定系数来评估其表现。
>>> from sklearn.cross_validation import *
>>> def train_and_evaluate(clf, X_train, y_train):
>>> clf.fit(X_train, y_train)
>>> print "Coefficient of determination on training
set:",clf.score(X_train, y_train)
>>> # create a k-fold cross validation iterator of k=5 folds
>>> cv = KFold(X_train.shape[0], 5, shuffle=True,
random_state=33)
>>> scores = cross_val_score(clf, X_train, y_train, cv=cv)
>>> print "Average coefficient of determination using 5-fold
crossvalidation:",np.mean(scores)
第一个尝试 - 线性模型
线性模型试图回答的问题,是由我们的学习特征(包括目标值)创建的 14 维空间中的超平面,它位于特征附近。在找到该超平面之后,预测简化为计算新点在超平面上的投影,并返回目标值坐标。想想我们在第一章“机器学习 - 温和介绍”中的第一个例子,我们想找到一条分隔训练实例的线。我们可以使用该线来预测第二个学习属性对于第一个学习属性的函数,即线性回归。
但是,我们的意思是什么呢?通常的度量是最小二乘:计算每个实例与超平面的距离,将其平方(以避免符号问题),并对它们求和。总和较小的超平面是最小二乘估计(超平面在二维中只是一条线)。
由于我们不知道我们的数据如何拟合(很难打印 14 维散点图!),我们将从称为SGDRegressor的线性模型开始,该模型试图最小化平方损失。
>>> from sklearn import linear_model
>>> clf_sgd = linear_model.SGDRegressor(loss='squared_loss',
penalty=None, random_state=42)
>>> train_and_evaluate(clf_sgd,X_train,y_train)
Coefficient of determination on training set: 0.743303511411
Average coefficient of determination using 5-fold crossvalidation: 0.715166411086
我们可以打印我们的方法所计算的超平面系数,如下所示:
>>> print clf_sgd.coef_
[-0.07641527 0.06963738 -0.05935062 0.10878438 -0.06356188 0.37260998 -0.02912886 -0.20180631 0.08463607 -0.05534634
-0.19521922 0.0653966 -0.36990842]
我们在调用方法时可能注意到了penalty=None参数。引入线性回归方法的惩罚参数以避免过拟合。它的原理是,惩罚系数太大的超平面,并寻找超平面,其中每个特征对预测值或多或少贡献相同。该参数通常是 L2 范数(系数的平方和)或 L1 范数(即系数的绝对值之和)。如果我们引入 L2 惩罚,让我们看看我们的模型是如何工作的。
>>> clf_sgd1 = linear_model.SGDRegressor(loss='squared_loss',
penalty='l2', random_state=42)
>>> train_and_evaluate(clf_sgd1, X_train, y_train)
Coefficient of determination on training set: 0.743300616394
Average coefficient of determination using 5-fold crossvalidation: 0.715166962417
在这种情况下,我们没有获得改善。
第二次尝试 - 支持向量机回归
可以使用 SVM 的回归版本来找到超平面。
>>> from sklearn import svm
>>> clf_svr = svm.SVR(kernel='linear')
>>> train_and_evaluate(clf_svr, X_train, y_train)
Coefficient of determination on training set: 0.71886923342
Average coefficient of determination using 5-fold crossvalidation: 0.694983285734
在这里,我们没有任何改善。然而,SVM 的一个主要优点是(使用我们称之为核技巧)我们可以使用非线性函数,例如,多项式函数来近似我们的数据。
>>> clf_svr_poly = svm.SVR(kernel='poly')
>>> train_and_evaluate(clf_svr_poly, X_train, y_train)
Coefficient of determination on training set: 0.904109273301
Average coefficient of determination using 5-fold cross validation: 0.754993478137
现在,我们的结果在测定系数方面要好六个点。我们实际上可以通过使用径向基函数(RBF)核来改善这一点。
>>> clf_svr_rbf = svm.SVR(kernel='rbf')
>>> train_and_evaluate(clf_svr_rbf, X_train, y_train)
Coefficient of determination on training set: 0.900132065979
Average coefficient of determination using 5-fold cross validation: 0.821626135903
RBF 核已经在几个问题中使用并且已经证明是非常有效的。实际上,RBF 是 scikit-learn 中 SVM 方法使用的默认内核。
第三次尝试 - 回到随机森林
我们可以尝试非常不同的方法,使用随机森林进行回归。我们以前使用随机森林进行分类。当用于回归时,树生长过程完全相同,但是在预测时,当我们到达叶子时,我们返回代表性的实际值而不是多数类,例如,目标值的平均值。
实际上,我们将使用 Extra 树,在sklearn.ensemble模块中的ExtraTreesRegressor类中实现。该方法增加了额外的随机化水平。它不仅为每个树选择不同的随机特征子集,而且还为每个决策随机选择阈值。
>>> from sklearn import ensemble
>>> clf_et=ensemble.ExtraTreesRegressor(n_estimators=10,
compute_importances=True, random_state=42)
>>> train_and_evaluate(clf_et, X_train, y_train)
Coefficient of determination on training set: 1.0
Average coefficient of determination using 5-fold cross validation: 0.852511952001
首先要注意的是,我们不仅完全消除了欠拟合(实现了对训练值的完美预测),而且在使用交叉验证时也提高了三个点的表现。Extra 树的一个有趣特性是,它们允许计算每个特征对于回归任务的重要性。让我们按如下方式计算这个重要性:
>>> print sort(zip(clf_et.feature_importances_,
boston.feature_names), axis=0)
[['0.000231085384564' 'AGE']
['0.000909210196652' 'B']
['0.00162702734638' 'CHAS']
['0.00292361527201' 'CRIM']
['0.00472492264278' 'DIS']
['0.00489022243822' 'INDUS']
['0.0067481487587' 'LSTAT']
['0.00852353178943' 'NOX']
['0.00873406149286' 'PTRATIO']
['0.0366902590312' 'RAD']
['0.0982265323415' 'RM']
['0.385904111089' 'TAX']
['0.439867272217' 'ZN']]
我们可以看到ZN(占地面积超过 25,000 平方英尺的住宅用地比例)和TAX(全价值物业税率)是我们最终决策中最具影响力的特征。
评价
像往常一样,让我们在测试集上评估我们的最佳方法的表现(之前,我们稍微修改了measure_performance函数来显示测定系数):
>>> from sklearn import metrics
>>> def measure_performance(X, y, clf, show_accuracy=True,
show_classification_report=True, show_confusion_matrix=True,
show_r2_score=False):
>>> y_pred = clf.predict(X)
>>> if show_accuracy:
>>> print "Accuracy:{0:.3f}".format(
>>> metrics.accuracy_score(y, y_pred)
>>> ),"\n"
>>>
>>> if show_classification_report:
>>> print "Classification report"
>>> print metrics.classification_report(y, y_pred),"\n"
>>>
>>> if show_confusion_matrix:
>>> print "Confusion matrix"
>>> print metrics.confusion_matrix(y, y_pred),"\n"
>>>
>>> if show_r2_score:
>>> print "Coefficient of determination:{0:.3f}".format(
>>> metrics.r2_score(y, y_pred)
>>> ),"\n"
>>> measure_performance(X_test, y_test, clf_et,
show_accuracy=False, show_classification_report=False,
show_confusion_matrix=False, show_r2_score=True)
Coefficient of determination:0.793
一旦我们选择了最佳方法并使用了所有可用数据,我们就可以在整个训练集上训练我们的最佳方法,但我们无法测量其在未来数据上的表现,仅仅因为我们没有更多可用数据。
总结
在本章中,我们回顾了一些最常见的监督学习方法和一些实际应用。我们了解到,监督方法要求实例同时具有输入特征和目标类。在下一章中,我们将回顾无监督的学习方法,这些方法不需要学习目标类别。这些方法对于理解数据的结构非常有用,并且在使用监督学习模型之前也可以用作前一步骤。
三、无监督学习
如今,人们常常断言,互联网上有大量的数据可供学习。如果您阅读前面的章节,您将会看到,即使监督学习方法在根据现有数据预测未来值方面非常强大,但它们有一个明显的缺点:数据必需是整理好的;一个人应该为一定数量的实例标注目标类。这种劳动通常由专家完成(如果你想将正确的物种分配给鸢尾花,你至少需要知道这些花的人);它可能需要一些时间和金钱才能完成,而且通常不会产生大量数据(至少不会与互联网相比!)。每个监督学习的构建都必须基于尽可能多的整理好的数据。
但是,如果没有带标签的数据,我们可以做一些事情。考虑您想要在婚礼中分配桌席的情况。你想把人聚在一起,把同样的人放在同一张桌子上(新娘的家人,新郎的朋友等等)。任何组织婚礼的人都知道这个任务并不容易,它在机器学习术语中称为聚类。有时人们属于不止一个群体,你必须决定不相似的人是否可以在一起(例如,新娘和新郎的父母)。聚类涉及为寻找一些分组,其中相同组中所有元素相似,但不同组中的对象不相似。每个聚类方法必须回答的问题是相似的。另一个关键问题是如何分离簇。当面对二维数据时,人类非常擅长寻找簇(考虑仅根据街道的存在来识别地图中的城市),但随着维度的增长,事情变得更加困难。
在本章中,我们将提出用于聚类的几个近似:K 均值(可能是最流行的聚类方法),亲和传播,均值漂移,基于模型的方法称为高斯混合模型。
无监督学习的另一个例子是降维。假设我们用大量属性表示学习实例,并希望将它们可视化来识别它们的主要模式。当特征数量超过三个时,这非常困难,原因很简单,因为我们无法想象三个以上的维度。降维方法提供了一种方式,在较低维度空间中表示高维数据集的数据点,保持(至少部分地)其图案结构。这些方法也有助于选择我们应该用于学习的模型。例如,使用线性超平面近似某些监督学习任务是否是合理的,或者我们应该求助于更复杂的模型。
主成分分析
主成分分析(PCA)是正交线性变换,将一组可能相关的变量转化为一组尽可能不相关的新变量。新变量位于新的坐标系中,使得通过在第一坐标中投影数据来获得最大方差,通过在第二坐标中投影来获得第二大方差,等等。这些新坐标称为主成分;我们拥有与原始维度数量一样多的主成分,但我们只保留那些具有高方差的成分。添加到主成分集的每个新主成分必须符合它应与其余主成分正交(即不相关)的限制。 PCA 可以看作是揭示数据内部结构的一种方法;它为用户提供原始对象的低维阴影。如果我们只保留第一个主成分,则数据维度会降低,因此更容易可视化数据结构。例如,如果我们仅保留第一和第二成分,我们可以使用二维散点图检查数据。因此,在构建预测模型之前,PCA 可用于探索性数据分析。
对于我们的学习方法,PCA 将允许我们将高维空间缩小为低维空间,同时保留尽可能多的方差。它是一种无监督的方法,因为它不需要目标类来执行其转换;它只依赖于学习属性的值。这对于两个主要目的非常有用:
- 可视化:例如,将高维空间投影到二维,将允许我们将我们的实例映射到二维图形。使用这些图形来可视化,我们可以获得有关实例分布的见解,并查看不同类的可分离实例。在本节中,我们将使用 PCA 转换和可视化数据集。
- 特征选择:由于 PCA 可以将实例从高维度转换为低维度,我们可以使用此方法来解决维度诅咒。我们可以使用 PCA 转换实例,然后在新特征空间中应用学习算法,而不是学习原始特征集。
作为一个工作示例,在本节中,我们将使用在8x8像素矩阵中的手写数字数据集,因此每个实例最初将包含 64 个属性。我们如何可视化实例的分布?对于人类来说,同时可视化 64 个维度是不可能的,因此我们将使用 PCA 将实例缩减为二维,并在二维散点图中可视化其分布。
我们首先加载我们的数据集(数字数据集是 scikit-learn 提供的示例数据集之一)。
>>> from sklearn.datasets import load_digits
>>> digits = load_digits()
>>> X_digits, y_digits = digits.data, digits.target
如果我们打印数字键,我们得到:
>>> print digits.keys()
['images', 'data', 'target_names', 'DESCR', 'target']
我们将使用data矩阵,每个矩阵是 64 个属性的实例,并且target向量具有相应的数字编号。
让我们打印数字来看看实例的显示方式:
>>> import matplotlib.pyplot as plt
>>> n_row, n_col = 2, 5
>>>
>>> def print_digits(images, y, max_n=10):
>>> # set up the figure size in inches
>>> fig = plt.figure(figsize=(2\. * n_col, 2.26 * n_row))
>>> i=0
>>> while i < max_n and i < images.shape[0]:
>>> p = fig.add_subplot(n_row, n_col, i + 1, xticks=[],
yticks=[])
>>> p.imshow(images[i], cmap=plt.cm.bone,
interpolation='nearest')
>>> # label the image with the target value
>>> p.text(0, -1, str(y[i]))
>>> i = i + 1
>>>
>>> print_digits(digits.images, digits.target, max_n=10)
这些实例可以在下图中看到:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_01.jpg
定义一个函数,该函数将使用从 PCA 变换获得的二维点绘制散点图。我们的数据点也将根据其类别进行着色。回想一下,目标类不会用于执行转换;我们想调查 PCA 之后的分布是否揭示了不同类别的分布,以及它们是否明显可分。我们将为每个数字使用十种不同的颜色,从0到9。
>>> def plot_pca_scatter():
>>> colors = ['black', 'blue', 'purple', 'yellow', 'white',
'red', 'lime', 'cyan', 'orange', 'gray']
>>> for i in xrange(len(colors)):
>>> px = X_pca[:, 0][y_digits == i]
>>> py = X_pca[:, 1][y_digits == i]
>>> plt.scatter(px, py, c=colors[i])
>>> plt.legend(digits.target_names)
>>> plt.xlabel('First Principal Component')
>>> plt.ylabel('Second Principal Component')
此时,我们已准备好执行 PCA 转换。在 scikit-learn 中,PCA 被实现为变换器对象,通过fit方法学习 n 个成分,并且可以用于新数据来将其投影到这些成分上。在 scikit-learn 中,我们有各种实现不同类型的 PCA 分解的类,例如PCA,ProbabilisticPCA,RandomizedPCA和KernelPCA。如果您需要每个的详细说明,请参阅 scikit-learn 文档。在我们的例子中,我们将使用sklearn.decomposition模块中的PCA类。我们可以更改的最重要的参数是n_components,它允许我们指定获取的实例的特征数量。在我们的例子中,我们想要将 64 个特征的实例转换为仅两个特征的实例,因此我们将n_components设置为2。
现在我们执行转换并绘制结果:
>>> from sklearn.decomposition import PCA
>>> estimator = PCA(n_components=10)
>>> X_pca = estimator.fit_transform(X_digits)
>>> plot_pca_scatter()
绘制的结果可以在下图中看到:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_02.jpg
从上图中,我们可以得出一些有趣的结论:
- 我们可以一眼就看到对应于 10 位数的 10 个不同类别。我们看到,对于大多数类,它们的实例根据其目标类清楚地分组,并且簇相对不同。例外是对应于数字 5 的类,其中实例非常稀疏地分布在平面上与其他类重叠。
- 在另一个极端,对应于数字 0 的类是最可分离的簇。直观地说,这个类可能是最容易与其他类分开的类;也就是说,如果我们训练一个分类器,它应该是具有最佳评估数字的类。
- 此外,对于拓扑分布,我们可以预测相邻类对应于相似的数字,这意味着它们将是最难分离的。例如,对应于数字 9 和 3 的簇看起来是相邻的(由于它们的图形表示是相似的,因此可以预计),因此,从 3 分离 9 可能比从 3 分离 4 更难,它位于左侧,远离这些簇。
请注意,我们很快就得到了一个图表,让我们对这个问题有了很多了解。可以在训练监督分类器之前使用该技术,以便更好地理解我们可能遇到的困难。有了这些知识,我们可以规划更好的特征预处理,特征选择,选择更合适的学习模型等等。正如我们之前提到的,它也可以用于执行降维以避免维度诅咒,并且还可以允许我们使用更简单的学习方法,例如线性模型。
为了完成,让我们看一下主成分转换。我们将通过访问components属性从估计器中获取主成分。它的每个成分都是一个矩阵,用于将向量从原始空间变换到变换空间。在我们之前绘制的散点图中,我们只考虑了前两个成分。
我们将绘制与原始数据(数字)相同形状的所有成分。
>>> def print_pca_components(images, n_col, n_row):
>>> plt.figure(figsize=(2\. * n_col, 2.26 * n_row))
>>> for i, comp in enumerate(images):
>>> plt.subplot(n_row, n_col, i + 1)
>>> plt.imshow(comp.reshape((8, 8)),
interpolation='nearest')
>>> plt.text(0, -1, str(i + 1) + '-component')
>>> plt.xticks(())
>>> plt.yticks(())
成分可以看作如下:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_03.jpg
通过查看上图中的前两个成分,我们可以得出一些有趣的观察结果:
- 如果查看第二个成分,可以看到它主要突出显示图像的中心区域。受此模式影响最大的
digit类是 0,因为其中心区域为空。通过查看我们之前的散点图来证实这种直觉。如果查看对应于数字 0 的簇,您可以看到它是第二个成分具有较低值的簇。 - 关于第一个成分,正如我们在散点图中看到的那样,可用于分离对应于数字 4(极左,低值)和 3 (极右,高值)数字的簇。如果你看到第一个成分图,它证实了这个观察结果。您可以看到区域非常相似于数字 3 ,而在数字 4 的特征区域中,它也拥有颜色。
如果我们使用其他成分,我们将获得更多的特性,以便能够将类在新的维度分离。例如,我们可以添加第三个主成分并尝试在三维散点图中绘制我们的实例。
在下一节中,我们将展示另一组无监督方法:聚类算法。与降维算法一样,聚类不需要知道目标类。但是,聚类方法会尝试对实例进行分组,寻找那些(以某种方式)相似的实例。但是,我们将看到聚类方法(如监督方法)可以使用 PCA 更好地可视化和分析其结果。
使用 K 均值聚类手写数字
K 均值是最受欢迎的聚类算法,因为它非常简单易行,并且在不同的任务中有着良好的表现。它属于一类聚类算法,它同时将数据点分成称为簇的不同组。另一组方法,我们将在本书中不涉及,是层次聚类算法。它们找到一组初始的簇并将它们分开或合并来形成新的簇。
K 均值背后的主要思想是找到数据点的簇,使得簇均值与簇中每个点之间的平方距离最小。请注意,此方法假定您事先知道应将数据分成多少个簇。
我们将在本节中展示 K 均值原理的启发性示例,即手写数字聚类的问题。因此,让我们首先将我们的数据集导入我们的 Python 环境并显示手写数字的外观(我们将使用我们在上一节中介绍的print_digits 函数的略有不同版本)。
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>>
>>> from sklearn.datasets import load_digits
>>> from sklearn.preprocessing import scale
>>> digits = load_digits()
>>> data = scale(digits.data)
>>>
>>> def print_digits(images,y,max_n=10):
>>> # set up the figure size in inches
>>> fig = plt.figure(figsize=(12, 12))
>>> fig.subplots_adjust(left=0, right=1, bottom=0, top=1,
hspace=0.05, wspace=0.05)
>>> i = 0
>>> while i <max_n and i <images.shape[0]:
>>> # plot the images in a matrix of 20x20
>>> p = fig.add_subplot(20, 20, i + 1, xticks=[],
yticks=[])
>>> p.imshow(images[i], cmap=plt.cm.bone)
>>> # label the image with the target value
>>> p.text(0, 14, str(y[i]))
>>> i = i + 1
>>>
>>> print_digits(digits.images, digits.target, max_n=10)
打印数字可以在下面看到:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_04.jpg
您可以看到数据集包含与目标类关联的数字,但由于我们是聚类,因此在评估之前不会使用此信息。我们将看看我们是否可以根据它们的相似性对数字进行分组,并形成我们可以预期的十个簇。
像往常一样,我们必须将训练和测试集分开如下:
>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test, images_train,
images_test = train_test_split(
data, digits.target, digits.images, test_size=0.25,
random_state=42)
>>>
>>> n_samples, n_features = X_train.shape
>>> n_digits = len(np.unique(y_train))
>>> labels = y_train
一旦我们完成了训练集,我们就可以聚类实例了。 K 均值算法的作用是:
- 随机选择一组初始簇中心。
- 找到每个数据点最近的簇中心,并分配最接近该簇的数据点。
- 计算新的簇中心,平均簇中数据点的值,并重复,直到簇成员稳定为止;也就是说,直到一些数据点在每次迭代后改变它们的簇。
由于 K 均值的原理,它可以收敛到局部最小值,初始的簇中心集可以极大地影响找到的簇。减轻这种情况的常用方法是尝试几个初始集,并选择具有最小值的集合,用于簇中心(或惯性)之间的平方距离之和。 scikit-learn 中 K 均值的实现已经做到了这一点(n-init参数允许我们确定,算法将尝试多少不同的质心配置)。它还允许我们指定初始质心将被充分分离,从而产生更好的结果。让我们看看它如何在我们的数据集上运行。
>>> from sklearn import cluster
>>> clf = Cluster.KMeans(init='kmeans++',
n_clusters=10, random_state=42)
>>> clf.fit(X_train)
该过程类似于用于监督学习的过程,但请注意fit方法仅将训练数据作为参数。还要注意我们需要指定簇的数量。我们可以感知这个数字,因为我们知道簇所代表的数字。
如果我们打印分类器的labels_属性的值,我们将获得与每个训练实例关联的簇编号列表。
>>> print_digits(images_train, clf.labels_, max_n=10)
可以在下图中看到该簇:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_05.jpg
请注意,簇编号与实数值无关。请记住,我们没有使用类别进行分类;我们只按相似度对图像进行分组。让我们看看我们的算法在测试数据上的表现。
为了预测训练数据的簇,我们使用分类器的常用predict方法。
>>> y_pred=clf.predict(X_test)
让我们看看簇的外观:
>>> def print_cluster(images, y_pred, cluster_number):
>>> images = images[y_pred==cluster_number]
>>> y_pred = y_pred[y_pred==cluster_number]
>>> print_digits(images, y_pred,max_n=10)
>>> for i in range(10):
>>> print_cluster(images_test, y_pred, i)
此代码显示每个簇的十个图像。有些簇非常清晰,如下图所示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_06.jpg
簇 2 对应于零。簇 7怎么样?
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_07.jpg
它不是那么清楚。似乎簇 7 类似于绘制的数字,看起来类似于数字九。簇 9 只有六个实例,如下图所示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_08.jpg
在阅读之后必须清楚,我们不在这里对图像进行分类(如前一章中的面部示例)。我们分为十组(您可以尝试更改簇的数量,看看会发生什么)。
我们如何评估我们的表现?精确率和所有这些东西都不起作用,因为我们没有可比较的目标类。为了评估,我们需要知道“真正的”簇,无论这意味着什么。我们可以假设,对于我们的示例,每个簇包括特定数字的每个绘图,并且仅包括该数字。知道这一点,我们可以计算我们的簇分布和预期之间的修正兰德系数。兰德系数是一个类似的准确率量度,但它考虑到两个分布中的类可以有不同名称的事实。也就是说,如果我们更改类名,索引不会改变。调整兰德系数试图消除偶然发生的结果的巧合。当两个集合中具有完全相同的簇时,兰德系数等于 1,而当没有簇共享数据点时,它等于零。
>>> from sklearn import metrics
>>> print "Adjusted rand score:
{:.2}".format(metrics.adjusted_rand_score(y_test, y_pred))
Adjusted rand score:0.57
我们还可以打印混淆矩阵如下:
>>> print metrics.confusion_matrix(y_test, y_pred)
[[ 0 0 43 0 0 0 0 0 0 0]
[20 0 0 7 0 0 0 10 0 0]
[ 5 0 0 31 0 0 0 1 1 0]
[ 1 0 0 1 0 1 4 0 39 0]
[ 1 50 0 0 0 0 1 2 0 1]
[ 1 0 0 0 1 41 0 0 16 0]
[ 0 0 1 0 44 0 0 0 0 0]
[ 0 0 0 0 0 1 34 1 0 5]
[21 0 0 0 0 3 1 2 11 0]
[ 0 0 0 0 0 2 3 3 40 0]]
观察到测试集中的类0(与编号0图形一致)完全分配给簇编号2。我们在编号8时遇到问题:21 个实例被分配了类0,而 11 个被分配了类8,依此类推。毕竟不太好。
如果我们想以图形方式显示 K 均值簇的外观,我们必须在二维平面上绘制它们。我们在上一节中已经学会了如何做到这一点:主成分分析(PCA)。让我们构造点的网格(降维后),计算它们簇,并绘制它们。
注意
这个例子取自非常好的 scikit-learn 教程。
>>> from sklearn import decomposition
>>> pca = decomposition.PCA(n_components=2).fit(X_train)
>>> reduced_X_train = pca.transform(X_train)
>>> # Step size of the mesh.
>>> h = .01
>>> # point in the mesh [x_min, m_max]x[y_min, y_max].
>>> x_min, x_max = reduced_X_train[:, 0].min() + 1,
reduced_X_train[:, 0].max() - 1
>>> y_min, y_max = reduced_X_train[:, 1].min() + 1,
reduced_X_train[:, 1].max() - 1
>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
>>> kmeans = cluster.KMeans(init='k-means++', n_clusters=n_digits,
n_init=10)
>>> kmeans.fit(reduced_X_train)
>>> Z = kmeans.predict(np.c_[xx.ravel(), yy.ravel()])
>>> # Put the result into a color plot
>>> Z = Z.reshape(xx.shape)
>>> plt.figure(1)
>>> plt.clf()
>>> plt.imshow(Z, interpolation='nearest',
extent=(xx.min(), xx.max(), yy.min(),
yy.max()), cmap=plt.cm.Paired, aspect='auto', origin='lower')
>>> plt.plot(reduced_X_train[:, 0], reduced_X_train[:, 1], 'k.',
markersize=2)
>>> # Plot the centroids as a white X
>>> centroids = kmeans.cluster_centers_
>>> plt.scatter(centroids[:, 0], centroids[:, 1],marker='.',
s=169, linewidths=3, color='w', zorder=10)
>>> plt.title('K-means clustering on the digits dataset (PCA
reduced data)\nCentroids are marked with white dots')
>>> plt.xlim(x_min, x_max)
>>> plt.ylim(y_min, y_max)
>>> plt.xticks(())
>>> plt.yticks(())
>>> plt.show()
数字数据集上的 K 均值聚类可以在下图中看到:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_09.jpg
备选聚类方法
scikit-learn 工具包包括几种聚类算法,所有聚类算法的方法和参数都与 K 均值中类似。在本节中,我们将简要回顾其中的一些,提出它们的一些优点。
聚类的典型问题是,大多数方法需要我们想要识别的簇数量。解决此问题的一般方法是尝试不同的数字,并让专家使用诸如降维的技术来可视化簇,来确定哪种方法最有效。还有一些方法试图自动计算簇的数量。 Scikit-learn 包括亲和传播的实现,该方法查找最具代表性的实例,并使用它们来描述簇。让我们看看它如何用于我们的数字学习问题:
>>> aff = cluster.AffinityPropagation()
>>> aff.fit(X_train)
>>> print aff.cluster_centers_indices_.shape
(112,)
亲和传播在我们的训练集中检测到112个簇。毕竟,似乎他们之间的数字并不那么相似。您可以尝试使用print_digits函数绘制簇,并查看哪些簇似乎已分组。 cluster_centers_indices_属性表示亲和传播所发现的内容,作为每个簇的代表元素。
另一种计算簇数的方法是MeanShift()。如果我们将它应用于我们的示例,它会检测18个簇,如下所示:
>>> ms = cluster.MeanShift()
>>> ms.fit(X_train)
>>> print ms.cluster_centers_.shape
(18, 64)
在这种情况下,cluster_centers_属性显示超平面的簇质心。前两个示例显示结果可能会有很大差异,具体取决于我们使用的方法。使用哪种聚类方法取决于我们要解决的问题以及我们想要查找的簇的类型。
请注意,对于最后两种方法,我们不能使用兰德分数来评估表现,因为我们没有要与之比较的规范簇集。但是,我们可以测量簇的惯性,因为惯性是从每个数据点到质心的距离之和;我们期待接近零的数字。不幸的是,除了 K 均值方法之外,scikit-learn 中目前还没有办法测量惯性。
最后,我们将使用高斯混合模型(GMM)尝试概率聚类方法。从程序的角度来看,我们将看到它与 k 均值非常相似,但它们的理论原理却截然不同。 GMM 假设数据来自具有未知参数的有限高斯分布的混合。高斯分布是用于模拟许多现象的,统计学中众所周知的分布函数。它具有以平均值为中心的钟形函数;您之前可能已经看过以下图形:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_03_10.jpg
如果我们选取足够大的男性样本并测量他们的身高,直方图(每个特定身高的男性比例)可能是高斯分布,平均值为 1.774 米,标准差为 0.1466 米。平均值表示最可能的值(与曲线的峰值一致),标准差表示结果的展开程度;也就是说,他们距离平均值有多远。如果我们测量两个特定高度之间曲线下面的区域(即它的积分),我们可以知道,给定一个人,他的高度两个值之间的概率是多少,如果分布是正确的。现在,我们为什么要期望这种分布而不是另一种呢?实际上,并非每个现象具有相同的分布,但是一个称为大数定律的定理告诉我们,每当我们重复实验很多次(例如,测量某人的身高),结果的分布可以用高斯近似。
通常,我们有一个多变量(即涉及多个特征)分布,但这个想法是一样的。超平面中的点大多数情况会更接近平均值;当我们离开均值时,在簇中找到一个点的概率会降低。该概率降低都少取决于第二个参数,即方差。正如我们所说,GMM 假设每个簇具有多元正态分布,方法目标是找到 k 个质心(使用称为期望最大化(EM)的算法,从训练数据估计均值和方差),并将每个点分配给最接近的平均值。让我们看看它如何在我们的例子中起作用。
>>> from sklearn import mixture
>>> gm = mixture.GMM(n_components=n_digits,
covariance_type='tied', random_state=42)
>>> gm.fit(X_train)
GMM(covariance_type='tied', init_params='wmc', min_covar=0.001,n_components=10, n_init=1, n_iter=100, params='wmc',random_state=42,thresh=0.01)
您可以观察到该过程与我们用于 K 均值的过程完全相同。covariance_type是一个方法参数,表示我们对特征的期望;也就是说,每个像素都是相关的。例如,我们可以假设它们是独立的,但我们也可以期望更近的点是相关的,依此类推。目前,我们将使用绑定协方差类型。在下一章中,我们将展示一些在不同参数值之间进行选择的技术。
让我们看看它对我们的测试数据的表现如何:
>>> # Print train clustering and confusion matrix
>>> y_pred = gm.predict(X_test)
>>> print "Adjusted rand
score:{:.2}".format(metrics.adjusted_rand_score(y_test,
y_pred))
Adjusted rand score:0.65
>>> print "Homogeneity score:{:.2}
".format(metrics.homogeneity_score(y_test, y_pred))
Homogeneity score:0.74
>>> print "Completeness score: {:.2}
".format(metrics.completeness_score(y_test, y_pred))
Completeness score: 0.79
与 K 均值相比,我们获得了更好的兰德得分(0.65 对 0.59),表明我们更好地将我们的簇与原始数字对齐。我们还包括sklearn.metrics中包含的两个有趣的度量。同质性是介于 0.0 和 1.0 之间的数字(越大越好)。值 1.0 表示簇仅包含来自单个类的数据点;也就是说,簇有效分组了类似的实例。 另一方面,完整性在给定类的每个数据点都在同一个簇内时得到满足(这意味着我们已经对该类的所有可能实例进行了分组,而不是构建几个均匀但较小的簇)。我们可以看到同质性和完整性是精确率和召回率的无监督版本。
总结
在本章中,我们介绍了一些最重要的无监督学习方法。我们并不打算向您提供所有可能方法的详尽介绍,而是简要介绍这些技术。我们描述了如何使用无监督算法执行快速数据分析,来理解数据集的行为并执行降维。在应用监督学习方法之前,这两个应用都非常有用。我们还应用无监督学习技术(如 K 均值)来解决问题而不使用目标类 - 这是一种在未标记数据之上创建应用的非常有用的方法。
在第四章“高级功能”中,我们将研究能够让我们在机器学习算法应用中获得更好结果的技术。我们将研究数据预处理和特征选择技术,来获得更好的学习特征。此外,我们将使用网格搜索技术来获取使我们的算法产生最佳表现的参数。
四、高级功能
在前面的章节中,我们研究了几种非常不同的算法,从分类和回归到聚类和降维。我们展示了在面对新数据时如何应用这些算法来预测结果。这就是机器学习的全部意义所在。在最后一章中,我们想要展示一些重要的概念和方法,如果你想进行真实的机器学习,你应该考虑这些概念和方法。
- 在实际问题中,通常数据尚未由属性/浮点值对表示,而是通过更复杂的结构或根本非结构化。我们将学习特征提取技术,这将允许我们从数据中提取 scikit-learn 特征。
- 从最初的可用特征集中,并非所有特征都可用于我们的算法来学习;事实上,其中一些可能会降低我们的表现。我们将解决选择最合适的特征集的问题,这个过程称为特征选择。
- 最后,正如我们在本书的示例中所看到的,许多机器学习算法都有必须设置的参数才能使用它们。为此,我们将回顾模型选择技术;也就是说,为我们的算法选择最有希望的超参数的方法。
对于在使用机器学习应用时获得不错的结果,所有这些步骤至关重要。
特征提取
学习任务的通常场景,例如本书中介绍的任务,包括实例列表(表示为特征/值对)和特殊特征(目标类),我们想要使用其余特征的值,为将来的实例预测它。但是,源数据通常不会采用这种格式。我们必须提取我们认为可能有用的特征,并将其转换为我们的学习格式。这个过程称为特征提取或特征工程,在大多数现实世界的机器学习任务中,它经常被低估但非常重要且耗时。我们可以在此任务中确定两个不同的步骤:
- 获取特征:此步骤涉及处理源数据并提取学习实例,通常采用特征/值对的形式,其中值可以是整数或浮点值,字符串,类别值等。用于提取的方法在很大程度上取决于数据的呈现方式。例如,我们可以拥有一组图片,并为每个像素生成一个整数值特征,指示其颜色级别,就像我们在第二章“监督学习”中的人脸识别示例中所做的那样。由于这是一项非常依赖于任务的工作,我们不会深入研究细节,并假设我们已经为我们的示例设置了此设置。
- 转换特征:大多数 scikit-learn 算法接受实例集作为输入,表示为浮点值特征列表。如何获得这些特征将成为本节的主题。
我们可以像第二章“监督学习”那样,构建转换源数据的临时程序。但是,有一些工具可以帮助我们获得合适的表示。例如,Python 包 Pandas 提供了用于数据分析的数据结构和工具。它旨在提供与 R(流行语言和统计计算环境)类似的功能。我们将使用 pandas 导入我们在第二章“监督学习”中提供的泰坦尼克号数据,并将它们转换为 scikit-learn 格式。
让我们首先将原始titanic.csv数据导入到 pandas DataFrame数据结构中(DataFrame本质上是一个二维标记数据结构,其中列可能包含不同的数据类型,每行代表一个实例)。像往常一样,我们事先导入numpy和pyplot包。
>>> %pylab inline
>>> import pandas as pd
>>> import numpy as np
>>> import matplotlib.pyplot as plt
然后我们用 pandas 导入泰坦尼克号数据。
>>> titanic = pd.read_csv('data/titanic.csv')
>>> print titanic
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1313 entries, 0 to 1312
Data columns (total 11 columns):
row.names 1313 non-null values
pclass 1313 non-null values
survived 1313 non-null values
name 1313 non-null values
age 633 non-null values
embarked 821 non-null values
home.dest 754 non-null values
room 77 non-null values
ticket 69 non-null values
boat 347 non-null values
sex 1313 non-null values
dtypes: float64(1), int64(2), object(8)
您可以看到每个csv列在DataFrame中都有相应的特征,并且特征类型是从可用数据推断的。我们可以检查一些特征,看看它们的样子。
>>> print titanic.head()[['pclass', 'survived', 'age', 'embarked',
'boat', 'sex']]
pclass survived age embarked boat sex
0 1st 1 29.0000 Southampton 2 female
1 1st 0 2.0000 Southampton NaN female
2 1st 0 30.0000 Southampton (135) male
3 1st 0 25.0000 Southampton NaN female
4 1st 1 0.9167 Southampton 11 male
我们现在面临的主要困难是 scikit-learn 方法期望实数作为特征值。在第二章“监督学习”中,我们使用LabelEncoder和OneHotEncoder预处理方法将某些分类特征手动转换为单热值(为每个可能的值生成新特征;如果原始特征具有相应的值则为1,否则为0)。这一次,我们将使用类似的 scikit-learn 方法DictVectorizer,它可以根据不同的原始特征值自动构建这些特征。此外,我们将编写一个方法来在一个独特的步骤中编码一组列。
>>> from sklearn import feature_extraction
>>> def one_hot_dataframe(data, cols, replace=False):
>>> vec = feature_extraction.DictVectorizer()
>>> mkdict = lambda row: dict((col, row[col]) for col in cols)
>>> vecData = pd.DataFrame(vec.fit_transform(
>>> data[cols].apply(mkdict, axis=1)).toarray())
>>> vecData.columns = vec.get_feature_names()
>>> vecData.index = data.index
>>> if replace:
>>> data = data.drop(cols, axis=1)
>>> data = data.join(vecData)
>>> return (data, vecData)
one_hot_dataframe方法(基于这个页面的脚本)接受 pandas DataFrame数据结构和列表的列表,并将每列编码为必要的单热特征。如果replace参数为True,它也将用新组替换原始列。让我们看看它应用于分类pclass,embarked和sex特征(titanic_n仅包含以前创建的列):
>>> titanic,titanic_n = one_hot_dataframe(titanic, ['pclass',
'embarked', 'sex'], replace=True)
>>> titanic.describe()
<class 'pandas.core.frame.DataFrame'>
Index: 8 entries, count to max
Data columns (total 12 columns):
row.names 8 non-null values
survived 8 non-null values
age 8 non-null values
embarked 8 non-null values
embarked=Cherbourg 8 non-null values
embarked=Queenstown 8 non-null values
embarked=Southampton 8 non-null values
pclass=1st 8 non-null values
pclass=2nd 8 non-null values
pclass=3rd 8 non-null values
sex=female 8 non-null values
sex=male 8 non-null values
dtypes: float64(12)
pclass属性已被转换为三个pclass=1st,pclass=2nd,pclass=3rd特征,并且类似方式用于其他两个特征。请注意,embarked特征没有消失,这是因为原始embarked属性包含NaN值,表示缺失值;在这些情况下,登船港口的每个特征都将变为0,但值为NaN的原始特征仍然存在,表明某些情况下缺少该特征。接下来,我们编码剩余的类别属性:
>>> titanic, titanic_n = one_hot_dataframe(titanic, ['home.dest',
'room', 'ticket', 'boat'], replace=True)
我们还必须处理缺失值,因为我们计划使用的DecisionTreeClassifier不会在输入时承认它们。 Pandas 允许我们使用fillna方法用固定值替换它们。我们将平均年龄用于age特征,以及0用于剩余缺失属性。
>>> mean = titanic['age'].mean()
>>> titanic['age'].fillna(mean, inplace=True)
>>> titanic.fillna(0, inplace=True)
现在,我们的所有特征(Name除外)都采用合适的格式。我们像往常一样准备建立测试和训练集。
>>> from sklearn.cross_validation import train_test_split
>>> titanic_target = titanic['survived']
>>> titanic_data = titanic.drop(['name', 'row.names', 'survived'],
axis=1)
>>> X_train, X_test, y_train, y_test =
train_test_split(titanic_data, titanic_target, test_size=0.25,
random_state=33)
我们决定简单地删除name属性,因为我们不期望它提供有关生存状态的信息(我们对每个实例都有一个不同的值,因此我们可以对其进行泛化)。我们还将survived特征指定为目标类,并因此将其从训练向量中消除。
让我们看看决策树如何处理当前特征集。
>>> from sklearn import tree
>>> dt = tree.DecisionTreeClassifier(criterion='entropy')
>>> dt = dt.fit(X_train, y_train)
>>> from sklearn import metrics
>>> y_pred = dt.predict(X_test)
>>> print "Accuracy:{0:.3f}".format(metrics.accuracy_score(y_test,
y_pred)), "\n"
Accuracy:0.839
特征选择
到目前为止,在训练我们的决策树时,我们使用了学习数据集中的每个可用特征。这看起来非常合理,因为我们希望使用尽可能多的信息来构建我们的模型。但是,有两个主要原因可以限制使用的特征数量:
- 首先,对于某些方法,尤其是减少用于在每个步骤中细化模型的实例数量的那些方法(例如决策树),不相关的特征可能会建议仅偶然出现的特征和目标类之间的相关性。没有正确建模问题。这方面也与过拟合有关;具有某些过度特定的特征可能会导致一般性不佳。此外,某些特征可能高度相关,并且只会添加冗余信息。
- 第二个原因是现实世界。如果没有相应的分类器改进,大量特征可以大大增加计算时间。在使用大数据时,这一点尤其重要,因为大数据的实例和特征的数量很容易增加到数千或更多。此外,关于维度的诅咒,从具有相对于实例数量太多特征的数据集学习可推广模型可能是困难的。
因此,使用较小的特征集可能会产生更好的结果。所以我们想找到一些通过算法找到最佳特征的方法。此任务称为特征选择,当我们的目标是通过机器学习算法获得不错的结果时,这是一个至关重要的步骤。如果我们的特征很差,无论我们的机器学习算法多么复杂,我们的算法都会返回不良结果。
例如,考虑一下我们非常简单的泰坦尼克号示例。我们从 11 个特征开始,但经过 1-K 编码后,它们增长到581。
>>> print titanic
<class 'pandas.core.frame.DataFrame'> Int64Index: 1313 entries, 0 to 1312 Columns: 581 entries, row.names to ticket=L15 1s dtypes: float64(578), int64(2), object(1)
这不会构成重要的计算问题,但如果如前所述,我们将数据集中的每个文档表示为每个可能单词的出现次数,请考虑会发生什么。另一个问题是决策树遭受过拟合。如果分支基于非常少的实例,则构建模型的预测能力将在未来数据上减少。对此的一个解决方案是调整模型参数(例如最大树深度或叶节点处所需的最小实例数)。但是,在此示例中,我们将采用不同的方法:我们将尝试将特征限制为最相关的特征。
相关的是什么意思?这是一个重要的问题。一般方法是找到正确表征训练数据的最小特征集。如果一个特征总是与目标类重合(也就是说,它是一个完美的预测器),那么表征数据就足够了。另一方面,如果特征总是具有相同的值,则其预测能力将非常低。
特征选择的一般方法是获得某种评估函数,当给定潜在特征时,返回特征的有用程度得分,然后保持具有最高分数的特征。这些方法可能具有不检测特征之间的相关性的缺点。其他方法可能更蛮力:尝试原始特征列表的所有可能子集,在每个组合上训练算法,并保持获得最佳结果的组合。
作为一种评估方法,我们可以使用统计检验来衡量两个随机变量(比如给定特征和目标类)是否独立的可能性;也就是说,它们之间没有相关性。
Scikit-learn 在feature_selection模块中提供了几种方法。我们将使用SelectPercentile方法, 在进行统计检验时,选择用户指定的具有最高得分的特征百分位数。最流行的统计检验是 χ²(卡方)统计量。让我们看看它如何适用于我们的泰坦尼克号例子;我们将用它来选择 20% 最重要的特征:
>>> from sklearn import feature_selection
>>> fs = feature_selection.SelectPercentile(
feature_selection.chi2, percentile=20)
>>> X_train_fs = fs.fit_transform(X_train, y_train)
X_train_fs数组现在具有在统计上更重要的特征。我们现在可以根据这些数据训练决策树。
>>> dt.fit(X_train_fs, y_train)
>>> X_test_fs = fs.transform(X_test)
>>> y_pred_fs = dt.predict(X_test_fs)
>>> print "Accuracy:{0:.3f}".format(metrics.accuracy_score(y_test,
y_pred_fs)),"\n"
Accuracy:0.845
我们可以看到训练集的精确率在训练集上的特征选择后提高了半个点。
是否有可能找到最佳数量的特征?如果最佳我们的意思是训练集上的最佳表现,它实际上是可能的;我们可以简单地使用蛮力方法,尝试使用不同数量的特征,同时使用交叉验证测量他们在训练集上的表现。
>>> from sklearn import cross_validation
>>>
>>> percentiles = range(1, 100, 5)
>>> results = []
>>> for i in range(1,100,5):
>>> fs = feature_selection.SelectPercentile(
feature_selection.chi2, percentile=i
)
>>> X_train_fs = fs.fit_transform(X_train, y_train)
>>> scores = cross_validation.cross_val_score(dt, X_train_fs,
y_train, cv=5)
>>> results = np.append(results, scores.mean())
>>> optimal_percentil = np.where(results == results.max())[0]
>>> print "Optimal number of features:{0}".format(
percentiles[optimal_percentil]), "\n"
Optimal number of features:11
>>>
>>> # Plot number of features VS. cross-validation scores
>>> import pylab as pl
>>> pl.figure()
>>> pl.xlabel("Number of features selected")
>>> pl.ylabel("Cross-validation accuracy)")
>>> pl.plot(percentiles, results)
下图显示了交叉验证精确率如何随特征数量的变化而变化:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_04_01.jpg
我们可以看到,当我们开始添加特征时,准确率会迅速提高,在特征的百分比变为大约 10 之后保持稳定。事实上,当使用 64 个原始 581 特征(11% 百分位数)时,可以获得最佳准确率。让我们看看这是否真的改善了测试集的表现。
>>> fs = feature_selection.SelectPercentile(
feature_selection.chi2,
percentile=percentiles[optimal_percentil])
>>> X_train_fs = fs.fit_transform(X_train, y_train)
>>> dt.fit(X_train_fs, y_train)
>>> X_test_fs = fs.transform(X_test)
>>> y_pred_fs = dt.predict(X_test_fs)
>>> print "Accuracy:{0:.3f}".format(metrics.accuracy_score(y_test,
y_pred_fs)), "\n"
Accuracy:0.848
表现再次略有改善。与我们的初始表现相比,我们最终仅使用 11% 的特征提高了几乎一个精确率点。
读者可能已经注意到,在创建分类器时,我们使用了默认参数,除了分割标准,我们使用了entropy。我们可以使用不同的参数改进模型吗?此任务称为模型选择,我们将在下一个部分中使用不同的学习示例详细说明。现在,让我们测试替代方法(gini)是否会为我们的示例带来更好的表现。为此,我们将再次使用交叉验证。
>>> dt = tree.DecisionTreeClassifier(criterion='entropy')
>>> scores = cross_validation.cross_val_score(dt, X_train_fs,
y_train, cv=5)
>>> print "Entropy criterion accuracy on
cv: {0:.3f}".format(scores.mean())
Entropy criterion accuracy on cv: 0.889
>>> dt = tree.DecisionTreeClassifier(criterion='gini')
>>> scores = cross_validation.cross_val_score(dt, X_train_fs,
y_train, cv=5)
>>> print "Gini criterion accuracy on
cv: {0:.3f}".format(scores.mean())
Gini criterion accuracy on cv: 0.897
基尼标准在我们的训练集上表现更好。它在测试集上的表现如何?
>>> dt.fit(X_train_fs, y_train)
>>> X_test_fs = fs.transform(X_test)
>>> y_pred_fs = dt.predict(X_test_fs)
>>> print "Accuracy:
{0:.3f}".format(metrics.accuracy_score(y_test,
y_pred_fs)),"\n"
Accuracy: 0.848
似乎训练集的表现改进不适用于评估集。这总是可行的。事实上,表现可能会降低(召回过拟合)。我们的模型仍然是最好的。如果我们将模型更改为使用测试集中表现最佳的模型,我们就无法测量其表现,因为测试数据集不再被视为“看不见的数据”。
模型选择
在上一节中,我们研究了预处理数据的方法,并选择了最有前途的特征。正如我们所说,选择一组好的特征是获得良好结果的关键步骤。现在我们将关注另一个重要步骤:选择算法参数,称为超参数,以区别于机器学习算法中调整的参数。许多机器学习算法包括超参数(从现在起我们将简称为参数),它们指导底层方法的某些方面并对结果产生很大影响。在本节中,我们将回顾一些方法来帮助我们获得最佳参数配置,这个过程称为模型选择。
我们将回顾第二章“监督学习”中提到的文本分类问题。在那个例子中,我们将 TF-IDF 向量化合物与多项朴素贝叶斯(NB)算法一起复合以对一组新闻组进行分类。消息分成若干个类别。MultinomialNB算法有一个重要参数,名为alpha,用于调整平滑。我们最初使用该类及其默认参数值(alpha = 1.0)并获得0.89的准确率。但是当我们将alpha设置为0.01时,我们对0.92的准确率有了明显的提高。显然,alpha参数的配置对算法的表现有很大影响。我们如何确定0.01 的最佳值?也许如果我们尝试其他可能的值,我们仍然可以获得更好的结果。
让我们从我们的文本分类问题开始,但是现在我们只使用减少数量的实例。我们只会处理 3,000 个实例。我们首先导入pylab环境并加载数据。
>>> %pylab inline
>>> from sklearn.datasets import fetch_20newsgroups
>>> news = fetch_20newsgroups(subset='all')
>>> n_samples = 3000
>>> X_train = news.data[:n_samples]
>>> y_train = news.target[:n_samples]
之后,我们需要导入类来构造分类器。
>>> from sklearn.naive_bayes import MultinomialNB
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.feature_extraction.text import TfidfVectorizer
然后导入一组停止词,并创建一个复合 TF-IDF 向量化器和朴素贝叶斯算法的管道(回想一下,我们有一个带有停止词列表的stopwords_en.txt文件)。
>>> def get_stop_words():
>>> result = set()
>>> for line in open('stopwords_en.txt', 'r').readlines():
>>> result.add(line.strip())
>>> return result
>>> stop_words = get_stop_words()
>>> clf = Pipeline([('vect', TfidfVectorizer(
>>> stop_words=stop_words,
>>> token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0-9_\-
\.]+\b",
>>> )),
>>> ('nb', MultinomialNB(alpha=0.01)),
>>>])
如果我们使用三重交叉验证来评估我们的算法,我们获得的平均分数约为 0.811。
>>> from sklearn.cross_validation import cross_val_score, KFold
>>> from scipy.stats import sem
>>> def evaluate_cross_validation(clf, X, y, K):
>>> # create a k-fold croos validation iterator of k=5 folds
>>> cv = KFold(len(y), K, shuffle=True, random_state=0)
>>> # by default the score used is the one returned by score
method of the estimator (accuracy)
>>> scores = cross_val_score(clf, X, y, cv=cv)
>>> print scores
>>> print ("Mean score: {0:.3f} (+/-{1:.3f})").format(
>>> np.mean(scores), sem(scores))
>>> evaluate_cross_validation(clf, X_train, y_train, 3)
[ 0.814 0.815 0.804]
Mean score: 0.811 (+/-0.004)
看起来我们应该使用不同参数值列表训练算法并保持参数值以获得最佳结果。让我们实现一个辅助函数来做到这一点。该函数将使用值列表训练算法,每次获得通过对训练实例执行 K 折交叉验证而计算的准确率分数。之后,绘制训练和测试分数对于参数值的函数。
>>> def calc_params(X, y, clf, param_values, param_name, K):
>>> # initialize training and testing scores with zeros
>>> train_scores = np.zeros(len(param_values))
>>> test_scores = np.zeros(len(param_values))
>>>
>>> # iterate over the different parameter values
>>> for i, param_value in enumerate(param_values):
>>> print param_name, ' = ', param_value
>>> # set classifier parameters
>>> clf.set_params({param_name:param_value})
>>> # initialize the K scores obtained for each fold
>>> k_train_scores = np.zeros(K)
>>> k_test_scores = np.zeros(K)
>>> # create KFold cross validation
>>> cv = KFold(n_samples, K, shuffle=True, random_state=0)
>>> # iterate over the K folds
>>> for j, (train, test) in enumerate(cv):
>>> clf.fit([X[k] for k in train], y[train])
>>> k_train_scores[j] = clf.score([X[k] for k in
train], y[train])
>>> k_test_scores[j] = clf.score([X[k] for k in test],
y[test])
>>> train_scores[i] = np.mean(k_train_scores)
>>> test_scores[i] = np.mean(k_test_scores)
>>>
>>> # plot the training and testing scores in a log scale
>>> plt.semilogx(param_values, train_scores, alpha=0.4, lw=2,
c='b')
>>> plt.semilogx(param_values, test_scores, alpha=0.4, lw=2,
c='g')
>>> plt.xlabel("Alpha values")
>>> plt.ylabel("Mean cross-validation accuracy")
>>> # return the training and testing scores on each parameter
value
>>> return train_scores, test_scores
该函数接受六个参数:特征数组,目标数组,要使用的分类器对象,参数值列表,要调整的参数的名称以及交叉验证评估中使用的 K 折数。
我们来调用这个函数;我们将使用 numpy 的logspace函数生成一个在对数刻度上均匀间隔的 alpha 值列表。
>>> alphas = np.logspace(-7, 0, 8)
>>> print alphas
[ 1.00000000e-07 1.00000000e-06 1.00000000e-05 1.00000000e-04
1.00000000e-03 1.00000000e-02 1.00000000e-01 1.00000000e+00]
我们将在管道内设置 NB 分类器的alpha参数的值,该参数对应于参数名称nb__alpha。我们将使用三个折叠进行交叉验证。
>>> train_scores, test_scores = calc_params(X_train, y_train, clf, alphas, 'nb__alpha', 3)
在下图中,顶部的线对应于训练精确率,底部的线对应于测试精确率:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_04_02.jpg
正如所料,训练精确率始终高于测试精确率。我们可以在图中看到,使用10^-2和10^-1范围内的 alpha 值可以获得最佳测试精确率。低于此范围,分类器显示过拟合的迹象(训练精确率高但测试精确率低于可能的值)。超出此范围,分类器显示欠拟合的迹象(训练集的准确率低于其可能)。
值得一提的是,在点处,可以在10^-2和10^-1的范围内以更精细的网格执行第二遍,以找到更好的 alpha 值。
让我们打印得分向量来查看实际值。
>>> print 'training scores: ', train_scores
>>> print 'testing scores: ', test_scores
training scores: [ 1\. 1\. 1\. 1\. 1\. 0.99933333 0.99633333 0.96933333]
testing scores: [ 0.75 0.75666667 0.76433333 0.77533333 0.78866667 0.811 0.81233333 0.753]
使用0.1 0.1值(精确率为 0.812)获得最佳结果。
我们创建了一个非常有用的函数来绘制图并获得分类器的最佳参数值。让我们用它来调整另一个使用支持向量机( SVM)而不是MultinomialNB的分类器:
>>> from sklearn.svm import SVC
>>>
>>> clf = Pipeline([
>>> ('vect', TfidfVectorizer(
>>> stop_words=stop_words,
>>> token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0-
9_\-\.]+\b",
>>> )),
>>> ('svc', SVC()),
>>> ])
我们像以前一样创建了一个管道,但现在我们使用 SVC 分类器及其默认值。现在我们将使用calc_params函数来调整gamma参数。
>>> gammas = np.logspace(-2, 1, 4)
>>> train_scores, test_scores = calc_params(X_train, y_train, clf, gammas,'svc__gamma', 3)
对于小于 1 的伽玛值,我们有欠拟合,对于大于 1 的伽马值,我们有过拟合。
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/learning-sklearn/img/1930_04_03.jpg
因此,最好的结果是1的gamma值,我们获得的训练精确率为 0.999,测试精确率为 0.760。
如果仔细查看 SVC 类构造器参数,除了 gamma 之外,我们还有其他参数,这些参数也可能影响分类器表现。如果我们只调整伽马值,我们隐含地声明最佳C值是1.0(我们没有明确设置的默认值)。也许我们可以通过C和gamma值的新组合获得更好的结果。 这开启了一种新的复杂程度;我们应该尝试所有参数组合并保持更好的参数组合。
网格搜索
为了缓解这个问题,我们在sklearn.grid_search模块中有一个名为GridSearchCV的非常有用的类。我们在calc_params函数中所做的是一种网格搜索。使用GridSearchCV,我们可以指定要遍历的任意数量的参数和参数值的网格。它将训练每个组合的分类器并获得交叉验证准确率以评估每个组合。
我们用它来同时调整C和gamma参数。
>>> from sklearn.grid_search import GridSearchCV
>>> parameters = {
>>> 'svc__gamma': np.logspace(-2, 1, 4),
>>> 'svc__C': np.logspace(-1, 1, 3),
>>> }
>>> clf = Pipeline([
>>> ('vect', TfidfVectorizer(
>>> stop_words=stop_words,
>>> token_pattern=ur"\b[a-z0-9_\-\.]+[a-z][a-z0-
9_\-\.]+\b",
>>> )),
>>> ('svc', SVC()),
>>> ])
>>> gs = GridSearchCV(clf, parameters, verbose=2, refit=False, cv=3)
让我们执行网格搜索并打印最佳参数值和分数。
>>> %time _ = gs.fit(X_train, y_train)
>>> gs.best_params_, gs.best_score_
CPU times: user 304.39 s, sys: 2.55 s, total: 306.94 s
Wall time: 306.56 s
({'svc__C': 10.0, 'svc__gamma': 0.10000000000000001}, 0.81166666666666665)
通过网格搜索,我们获得了C和gamma参数的更好组合,分别为10.0和0.10值,0.811的交叉验证精确率为 3 倍,远远优于0.811。我们在之前的实验中获得的最佳值(0.76)仅通过调整gamma并将C值保持在1.0。
此时,我们可以继续执行实验,不仅要调整 SVC 的其他参数,还要调整TfidfVectorizer上的参数,这也是估计器的一部分。注意,这另外增加了复杂性。您可能已经注意到,之前的网格搜索实验大约需要五分钟才能完成。如果我们添加新参数进行调整,则时间将呈指数级增长。结果,这些方法非常资源/时间密集;这也是我们仅使用总实例的一部分的原因。
并行网格搜索
网格搜索计算随着每个参数及其想要调整的可能值呈指数增长。如果我们按照并行计算每个组合而不是按顺序计算每个组合,我们可以减少响应时间。在前面的例子中,gamma有四个不同的值,C有三个不同的值,总结了 12 个参数组合。此外,我们还需要对每个组合进行三次训练(通过三次交叉验证),因此我们总结了 36 次训练和评估。我们可以尝试并行运行这 36 个任务,因为任务是独立的。
大多数现代计算机都有多个核心,可用于并行运行任务。我们在 IPython 中也有一个非常有用的工具,叫做 IPython 并行,它允许我们并行运行独立任务,每个任务都在我们不同的核心机。让我们用我们的文本分类器示例来做到这一点。
我们将首先声明一个函数,该函数将持续所有 K 个折叠以用于不同文件中的交叉验证。这些文件将由执行相应折叠的进程加载。为此,我们将使用joblib库。
>>> from sklearn.externals import joblib
>>> from sklearn.cross_validation import ShuffleSplit
>>> import os
>>> def persist_cv_splits(X, y, K=3, name='data',
suffix="_cv_%03d.pkl"):
>>> """Dump K folds to filesystem."""
>>>
>>> cv_split_filenames = []
>>>
>>> # create KFold cross validation
>>> cv = KFold(n_samples, K, shuffle=True, random_state=0)
>>>
>>> # iterate over the K folds
>>> for i, (train, test) in enumerate(cv):
>>> cv_fold = ([X[k] for k in train], y[train], [X[k] for
k in test], y[test])
>>> cv_split_filename = name + suffix % i
>>> cv_split_filename = os.path.abspath(cv_split_filename)
>>> joblib.dump(cv_fold, cv_split_filename)
>>> cv_split_filenames.append(cv_split_filename)
>>>
>>> return cv_split_filenames
>>> cv_filenames = persist_cv_splits(X, y, name='news')
以下函数加载特定折叠并使分类器适合指定的参数集,返回测试分数。 每个并行任务都会调用此函数。
>>> def compute_evaluation(cv_split_filename, clf, params):
>>>
>>> # All module imports should be executed in the worker
namespace
>>> from sklearn.externals import joblib
>>>
>>> # load the fold training and testing partitions from the
filesystem
>>> X_train, y_train, X_test, y_test = joblib.load(
>>> cv_split_filename, mmap_mode='c')
>>>
>>> clf.set_params(**params)
>>> clf.fit(X_train, y_train)
>>> test_score = clf.score(X_test, y_test)
>>> return test_score
最后,以下函数在并行任务中执行网格搜索。对于每个参数组合(由IterGrid迭代器返回),它迭代 K 次折叠并创建计算评估的任务。它返回任务列表旁边的参数组合。
>>> from sklearn.grid_search import IterGrid
>>>
>>> def parallel_grid_search(lb_view, clf, cv_split_filenames, param_grid):
>>> all_tasks = []
>>> all_parameters = list(IterGrid(param_grid))
>>>
>>> # iterate over parameter combinations
>>> for i, params in enumerate(all_parameters):
>>> task_for_params = []
>>> # iterate over the K folds
>>> for j, cv_split_filename in
enumerate(cv_split_filenames):
>>> t = lb_view.apply(
>>> compute_evaluation, cv_split_filename, clf,
params)
>>> task_for_params.append(t)
>>>
>>> all_tasks.append(task_for_params)
>>>
>>> return all_parameters, all_tasks
现在我们使用 IPython 并行来获取客户端和负载平衡视图。我们必须首先使用 IPython Notebook 中的Cluster选项卡创建 N 个引擎的本地群集(每个核心一个)。然后我们创建客户端和视图,执行我们的parallel_grid_search函数。
>>> from sklearn.svm import SVC
>>> from IPython.parallel import Client
>>>
>>> client = Client()
>>> lb_view = client.load_balanced_view()
>>>
>>> all_parameters, all_tasks = parallel_grid_search(
lb_view, clf, cv_filenames, parameters)
IPython 并行将开始并行运行任务。我们可以用它来监控整个任务组的进度。
>>> def print_progress(tasks):
>>> progress = np.mean([task.ready() for task_group in tasks
for task in task_group])
>>> print "Tasks completed: {0}%".format(100 * progress)
完成所有任务后,使用以下函数:
>>> print_progress(all_tasks)
Tasks completed: 100.0%
我们可以定义一个函数来计算已完成任务的平均分数。
>>> def find_bests(all_parameters, all_tasks, n_top=5):
>>> """Compute the mean score of the completed tasks"""
>>> mean_scores = []
>>>
>>> for param, task_group in zip(all_parameters, all_tasks):
>>> scores = [t.get() for t in task_group if t.ready()]
>>> if len(scores) == 0:
>>> continue
>>> mean_scores.append((np.mean(scores), param))
>>>
>>> return sorted(mean_scores, reverse=True)[:n_top]
>>> print find_bests(all_parameters, all_tasks)
[(0.81733333333333336, {'svc__gamma': 0.10000000000000001, 'svc__C': 10.0}), (0.78733333333333333, {'svc__gamma': 1.0, 'svc__C': 10.0}), (0.76000000000000012, {'svc__gamma': 1.0, 'svc__C': 1.0}), (0.30099999999999999, {'svc__gamma': 0.01, 'svc__C': 10.0}), (0.19933333333333333, {'svc__gamma': 0.10000000000000001, 'svc__C': 1.0})]
您可以观察到我们计算的结果与上一节相同,但是在一半的时间内(如果您使用了两个核心)或在四分之一的时间内(如果您使用了四个核心)。
总结
在本章中,我们回顾了两种在应用机器学习算法时改进结果的重要方法:特征选择和模型选择。首先,我们使用不同的技术来预处理数据,提取特征,并选择最有希望的特征。然后我们使用技术自动计算最有希望的机器学习算法的超参数,并使用方法来并行化这些计算。
读者必须意识到,本书仅涵盖了主要的机器学习线和一些方法。请记住,除了监督和无监督学习之外,还有很多其他内容。例如:
- 半监督学习方法是有监督和无监督学习之间的中间立场。它们将少量带标签的数据与大量未标记的数据相结合。通常,未标记的数据可以揭示元素的基础分布,并与小的标记数据集结合获得更好的结果。
- 主动学习是半监督方法中的一个特例。同样,当标记数据稀缺或难以获得时,它是有用的。在主动学习中,该算法主动查询人类专家以回答某些未标记实例的标签,从而在减少的标记实例集上学习该概念。
- 强化学习提出了一种方法,即智能体在环境中执行操作后从反馈(奖励或增援)中学习。智能体通过尝试最大化累积奖励来学习执行任务。这些方法在机器人和视频游戏中非常成功。
- 顺序分类(在自然语言处理(NLP)中非常常用)为一系列项目分配标签序列;例如,句子中单词的词性。
除此之外,还有许多监督学习方法,与我们提出的方法截然不同;例如,神经网络,最大熵模型,基于记忆的模型和基于规则的模型。机器学习是一个非常活跃的研究领域,文献越来越多;读者可以使用许多书籍和课程来深入了解理论和细节。
Scikit-learn 已经实现了许多这些算法,并且缺少其他算法,但期望其积极和热情的贡献者很快就能构建它们。我们鼓励读者成为社区的一员!
1万+

被折叠的 条评论
为什么被折叠?



