5.2 Scikit-Learn 简介
原文:Introducing Scikit-Learn 译者:飞龙 协议:CC BY-NC-SA 4.0 译文没有得到原作者授权,不保证与原文的意思严格一致。
有几个 Python 库提供一系列机器学习算法的实现。最著名的是 Scikit-Learn,一个提供大量常见算法的高效版本的软件包。 Scikit-Learn 的特点是简洁,统一,流线型的 API,以及非常实用和完整的在线文档。这种一致性的好处是,一旦了解了 Scikit-Learn 中一种类型的模型的基本用法和语法,切换到新的模型或算法就非常简单。
本节提供了 Scikit-Learn API 的概述;对这些API元素的了解,会成为理解以下章节中机器学习算法和方法的更深入的实际讨论的基础。
我们将首先介绍 Scikit-Learn 中的数据表示形式,然后设计 Estimator API,最后通过一个更有趣的例子,使用这些工具来探索一组手写数字图像。
Scikit-Learn 中的数据表示
机器学习是从数据创建模型:因此,我们将首先讨论如何表示数据,以便计算机理解。 在 Scikit-Learn 中考虑数据的最佳方式就是数据表。
数据作为表
一个基本表格是二维数据网格,其中行表示数据集的各个元素,列表示与这些元素中的每一个相关的数量。 例如,考虑 Iris 数据集,由 Ronald Fisher 于 1936 年进行了著名的分析。我们可以使用 Seaborn 库以 Pandas DataFrame 的形式下载此数据集:
代码语言:javascript复制import seaborn as sns
iris = sns.load_dataset('iris')
iris.head()
这里的每一行数据指代一个观察到的花,行数是数据集中花的总数。 一般来说,我们将把矩阵行作为样本,将行数称为n_samples
。
类似地,数据的每一列都是描述每个样本的特定的定量信息。 一般来说,我们将把矩阵的列称为特征,列数称为n_features
。
特征矩阵
该表的布局清楚地表明,信息可以当做二维数组或矩阵,我们称之为特征矩阵。 按照惯例,这个特征矩阵通常被存储在一个名为X
的变量中。特征矩阵被假设为二维的,形状为[n_samples,n_features]
,并且最常使用NumPy
数组或Pandas DataFrame
来存放,尽管有些 Scikit-Learn 模型也接受 SciPy 稀疏矩阵。
样本(即行)总是指代由数据集描述的各个对象。 例如,样本可能是一朵花,一个人,一个文档,一个图像,一个声音文件,一个视频,一个天文物体,或者你可以用一组定量测量来描述的任何东西。
特征(即列)总是指以定量方式描述每个样本的不同观察结果。 特征通常是实值,但在某些情况下可能是布尔值或离散值。
目标数组
除了特征矩阵X
之外,我们还通常使用标签或目标数组,按照惯例,我们通常称为y
。目标数组通常是一维,长度为n_samples
,通常包含在 NumPy 数组或 Pandas Series 中。目标数组可以具有连续的数值或离散分类/标签。虽然一些 Scikit-Learn 估计器确实以二维[n_samples,n_targets]
目标数组的形式处理多个目标值,但我们将主要处理一维目标数组的常见情况。
常常有一点令人困惑的是,目标数组与其他特征列的不同之处。目标数组的特征在于,它通常是我们要从数据中预测的数量:在统计学上,它是因变量。例如,在上述数据中,我们可能希望构建一个模型,可以基于其他度量来预测花的种类;在这种情况下,物种列将被视为目标数组。
考虑到这个目标数组,我们可以使用 Seaborn(参见可视化与 Seaborn)来方便地显示数据:
代码语言:javascript复制%matplotlib inline
import seaborn as sns; sns.set()
sns.pairplot(iris, hue='species', size=1.5);
对于 Scikit-Learn 中的使用,我们会从DataFrame
提取特征矩阵和目标数组。我们可以用一些第三章中的 PandasDataFrame
操作实现。
X_iris = iris.drop('species', axis=1)
X_iris.shape
# (150, 4)
y_iris = iris['species']
y_iris.shape
# (150,)
总之,特征和目标值的预期布局,可以在下图中显示:
将这个数据合理格式化之后,我们可以转而思考 Scikit-Learn 的估计器 API 了。
Scikit-Learn 的估计器 API
Scikit-Learn API 的设计思想,是 Scikit-Learn API 的说明书所述的以下指导原则:
一致性:所有对象共享一个通用接口,从一组有限方法抽取,具有一致的文档。
检查:所有指定的参数值都公开为公共属性。
有限对象层次:只有算法由 Python 类表示;数据集以标准格式(NumPy 数组,Pandas DataFrames,SciPy 稀疏矩阵)表示,参数名称使用标准 Python 字符串。
组成:许多机器学习任务可以表达为更基础的算法的序列,而 Scikit-Learn 可以尽可能地利用这一点。
敏感默认值:当模型需要用户指定的参数时,库定义了一个适当的默认值。
在实践中,一旦理解了基本原理,这些原则使 Scikit-Learn 非常容易使用。 Scikit-Learn 中的每个机器学习算法都通过 Estimator API 实现,该 API 为广泛的机器学习应用提供了一致的接口。
API 基础
通常,使用 Scikit-Learn 估计器 API 的步骤如下(我们将在以下部分中详细介绍一些详细示例)。
- 通过从 Scikit-Learn 导入适当的估计类,来选择一类模型。
- 通过使用所需的值实例化此类,来选择模型超参数。
- 在上述讨论之后,将数据排列成特征矩阵和目标向量。
- 通过调用模型实例的
fit
方法,使用模型来拟合数据。 - 将模型应用于新数据:
- 对于监督学习,我们通常使用
predict()
方法预测未知数据的标签。 - 对于无监督学习,我们经常使用
transform()
或predict()
方法来转换或推断数据的属性。
- 对于监督学习,我们通常使用
我们现在将逐步介绍几个简单示例,应用监督和无监督学习方法。
监督学习示例:简单线性回归
作为这个过程的一个例子,让我们考虑一个简单的线性回归,也就是说,一种常见情况,使用直线来拟合(x,y)
数据。 我们将以下简单数据用于回归示例:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.RandomState(42)
x = 10 * rng.rand(50)
y = 2 * x - 1 rng.randn(50)
plt.scatter(x, y);
有了这些数据,我们可以使用前面提到的秘籍。 我们来看一下这个过程:
1. 选择一个模型类
在 Scikit-Learn 中,每个模型类都由 Python 类表示。 所以,例如,如果我们想要计算一个简单的线性回归模型,我们可以导入线性回归类:
代码语言:javascript复制from sklearn.linear_model import LinearRegression
要注意也存在更通用的线性回归模型,你可以在 sklearn.linear_model 模型文档中了解更多。
2. 选择模型超参数
一个重点是,模型类与模型实例不一样。
一旦我们决定了我们的模型类,我们仍然有一些选择。根据我们正在使用的模型类,我们可能需要回答以下一个或多个问题:
- 我们希望拟合偏移(即纵截距)吗?
- 我们是否希望将模型归一化?
- 我们是否希望预处理我们的特征,来增加模型的灵活性?
- 我们想在我们的模型中使用什么程度的正则化?
- 我们想要使用多少个模型组件?
这些是重要选择的示例,在选择模型类后必须做出。这些选择通常表示为超参数,或在模型拟合数据之前必须设置的参数。在 Scikit-Learn 中,通过在模型实例化下传递值来选择超参数。我们将在超参数和模型验证中,探讨如何定量地改进超参数的选择。
对于我们的线性回归示例,我们可以实例化LinearRegression
类,并指定我们想使用fit_intercept
超参数拟合截距:
model = LinearRegression(fit_intercept=True)
model
# LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
请记住,当模型被实例化时,唯一的操作是存储这些超参数值。 特别是,我们还没有将模型应用于任何数据:Scikit-Learn API 非常清楚模型选择和模型对数据应用之间的区别。
3. 将数据排列为特征矩阵和目标向量
以前,我们详细介绍了 Scikit-Learn 数据表示,它需要二维特征矩阵和一维目标数组。 这里我们的目标变量y
已经是正确的形式(长度为n_samples
的数组),但是我们需要调整数据x
,使其成为大小为[n_samples,n_features]
的矩阵。 在这种情况下,这相当于一维数组的简单重塑:
X = x[:, np.newaxis]
X.shape
# (50, 1)
4. 使用模型来拟合数据
现在是时候将模型应用于数据了。这可以使用fit
方法来完成。
model.fit(X, y)
# LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
这个fit
命令会导致一些模型相关的内部计算,这些计算的结果存储在特定于模型的属性中,用户可以探索。 在 Scikit-Learn 中,按照惯例,在fit
过程中学习的所有模型参数,都有尾随的下划线;例如在这个线性模型中,我们有以下这些东西:
model.coef_
# array([ 1.9776566])
model.intercept_
# -0.90331072553111635
这两个参数表示对数据的简单线性拟合的斜率和截距。 与数据定义相比,我们看到它们非常接近输入斜率 2 和截距 -1。
经常出现的一个问题是,这些内部模型参数的不确定性。 一般来说,Scikit-Learn 不提供从内部模型参数本身得出结论的工具:模型参数的解释更多是统计建模问题,而不是机器学习问题。 机器学习的重点是模型预测。 如果你希望深入了解模型中参数的含义,则可以使用其他工具,包括 Python Statsmodels 包。
5. 预测未知数据的标签
一旦模型训练完成,监督机器学习的主要任务是,根据对不是训练集的一部分的新数据做出评估。 在 Scikit-Learn 中,可以使用predict
方法来完成。 对于这个例子,我们的“新数据”将是一个x
值的网格,我们将询问模型预测的y
值:
xfit = np.linspace(-1, 11)
像之前一样,我们需要将这些x
值调整为[n_samples, n_features]
的特征矩阵,之后我们可以将它扔给模型了。
Xfit = xfit[:, np.newaxis]
yfit = model.predict(Xfit)
最后,让我们通过首先绘制原始数据,之后是这个模型,来展示结果。
代码语言:javascript复制plt.scatter(x, y)
plt.plot(xfit, yfit);
通常,通过将其结果与某些已知基准进行比较,来评估模型的功效,如下例所示。
监督学习示例,鸢尾花分类
我们来看看这个过程的另一个例子,使用我们前面讨论过的 Iris 数据集。 我们的问题是这样的:给出一个模型,使用 Iris 数据的一部分进行培训,我们如何能够预测剩余的标签?
对于这个任务,我们将使用一个非常简单的生成模型,称为高斯朴素贝叶斯,它们通过假设每个类别服从轴对齐的高斯分布(更多细节参见朴素贝叶斯分类)。 因为高斯朴素贝叶斯如此之快,没有超参数可供选择。在探索是否可以通过更复杂的模型做出改进之前,它通常是一个用作基准分类的良好模型。
我们想对之前没有看到的数据进行评估,因此我们将数据分成训练集和测试集。 这可以手工完成,但是使用train_test_split
工具更方便:
from sklearn.cross_validation import train_test_split
Xtrain, Xtest, ytrain, ytest = train_test_split(X_iris, y_iris,
random_state=1)
排列好数据之后,我们可以遵循秘籍来预测标签了。
代码语言:javascript复制from sklearn.naive_bayes import GaussianNB # 1. choose model class
model = GaussianNB() # 2. instantiate model
model.fit(Xtrain, ytrain) # 3. fit model to data
y_model = model.predict(Xtest) # 4. predict on new data
最后,我们可以使用accuracy_score
工具来查看匹配真实值的预测标签的百分比。
from sklearn.metrics import accuracy_score
accuracy_score(ytest, y_model)
# 0.97368421052631582
准确率超过了 97%,我们可以看到,即使是这个非常朴素的分类算法,对于特定数据集也是高效的。
无监督学习示例:Iris 降维
作为无监督学习问题的一个例子,我们来看一下 Iris 数据的降维,以便更容易地将其视觉化。 回想一下,Iris 数据是四维的:每个样本都记录了四个特征。
降维的任务是询问是否存在合适的低维表示,保留数据的基本特征。 降维通常用于来辅助数据可视化:毕竟,绘制二维数据比四维或更高维度中更容易!
在这里,我们将使用主成分分析(PCA,参见主成分分析),这是一种快速线性降维技术。 我们要求模型返回两个组件 - 即数据的二维表示。
按照先前列出的步骤,我们有:
代码语言:javascript复制from sklearn.decomposition import PCA # 1. Choose the model class
model = PCA(n_components=2) # 2. Instantiate the model with hyperparameters
model.fit(X_iris) # 3. Fit to data. Notice y is not specified!
X_2D = model.transform(X_iris) # 4. Transform the data to two dimensions
现在我们来绘制结果。 一个快速的方法是,将结果插入到原始的 Iris DataFrame 中,并使用 Seaborn 的lmplot
来显示结果:
iris['PCA1'] = X_2D[:, 0]
iris['PCA2'] = X_2D[:, 1]
sns.lmplot("PCA1", "PCA2", hue='species', data=iris, fit_reg=False);
我们看到,在二维表示中,物种的分隔相当良好,尽管 PCA 算法不知道物种标签! 这对我们来说,正如我们以前看到的那样,相对简单的分类可能对数据集有效。
无监督学习示例:Iris 聚类
接下来我们来看一下 Iris 数据的聚类应用。 聚类算法尝试找到不同的数据分析,而不参考任何标签。 在这里,我们将使用一种称为高斯混合模型(GMM)的强大的聚类方法,在高斯混合模型中有更详细的讨论。 GMM 尝试将数据建模为高斯数据块的集合。
我们可以这样拟合高斯混合模型:
代码语言:javascript复制from sklearn.mixture import GMM # 1. Choose the model class
model = GMM(n_components=3,
covariance_type='full') # 2. Instantiate the model with hyperparameters
model.fit(X_iris) # 3. Fit to data. Notice y is not specified!
y_gmm = model.predict(X_iris) # 4. Determine cluster labels
像之前一样,我们向 IrisDataFrame
添加簇的标签,并使用 Seaborn 绘制结果:
iris['cluster'] = y_gmm
sns.lmplot("PCA1", "PCA2", data=iris, hue='species',
col='cluster', fit_reg=False);
通过按照簇号分割数据,我们看到 GMM 算法已经恢复了潜在的标签:组 0 已经完全分离了 setosa 物种,而在 versicolor 和 virginica 之间仍然存在少量的混合。 这意味着即使没有专家告诉我们个别花朵的物种标签,这些花朵的度量是非常明显的,我们可以用简单的聚类算法,自动识别这些不同种类的物种的存在! 这种算法可能会进一步向专家提供现场线索,关于他们正在观察的样本之间关系。
应用:手写体数字探索
为了在一个更有趣的问题上演示这些原理,我们来考虑一个光学字符识别问题:识别手写数字。 粗略来说,这个问题涉及定位和识别图像中的字符。 在这里,我们将使用捷径,并使用 Scikit-Learn 的一组预格式化数字,这是内置在库中的。
加载和展示数字的数据
我们使用 Scikit-Learn 的数据访问接口,并看一看这个数据:
代码语言:javascript复制from sklearn.datasets import load_digits
digits = load_digits()
digits.images.shape
# (1797, 8, 8)
这个图像的数据是三维数组:1797 个样本,每个包含 8x8 的像素。让我们先展示前一百个。
代码语言:javascript复制import matplotlib.pyplot as plt
fig, axes = plt.subplots(10, 10, figsize=(8, 8),
subplot_kw={'xticks':[], 'yticks':[]},
gridspec_kw=dict(hspace=0.1, wspace=0.1))
for i, ax in enumerate(axes.flat):
ax.imshow(digits.images[i], cmap='binary', interpolation='nearest')
ax.text(0.05, 0.05, str(digits.target[i]),
transform=ax.transAxes, color='green')
为了在 Scikit-Learn 中处理这些数据,我们需要一个二维的[n_samples,n_features]
表示。 我们可以将图像中的每个像素视为一个特征:即通过展开像素阵列,使得我们具有长度为 64 的数组,代表每个数字的像素值。 另外,我们需要目标数组,它为每个数字给出了先前确定的标签。 这两个数量分别内置在数字数据集的data
和target
属性中:
X = digits.data
X.shape
# (1797, 64)
y = digits.target
y.shape
# (1797,)
我们可以看到,有 1797 个样本和 64 个特征。
无监督学习:降维
我们希望在 64 维参数空间内可视化我们的点,但很难有效地在这样一个高维空间中可视化点。 相反,我们将使用无监督的方法将维度减小到 2。 在这里,我们将利用一种称为 Isomap 的流形学习算法(参见流形学习),并将数据转换为两个维度:
代码语言:javascript复制from sklearn.manifold import Isomap
iso = Isomap(n_components=2)
iso.fit(digits.data)
data_projected = iso.transform(digits.data)
data_projected.shape
# (1797, 2)
我们可以看到,投影的数据现在是二维了。让我们绘制数据,来看看是否可以从结构中学到什么东西。
代码语言:javascript复制plt.scatter(data_projected[:, 0], data_projected[:, 1], c=digits.target,
edgecolor='none', alpha=0.5,
cmap=plt.cm.get_cmap('spectral', 10))
plt.colorbar(label='digit label', ticks=range(10))
plt.clim(-0.5, 9.5);
这个绘图给了我们很好的直觉,在更大的 64 维空间中各种数字的分离程度如何。 例如,零(黑色)和一(紫色)在参数空间中几乎没有重叠。 直观上来说,这是有道理的:零的图像中间是空的,而一的中间通常会有墨迹。 另一方面,一和四之间似乎有一个或多或少的连续频谱:我们可以理解,有些人在一上画了个“帽子”,从而使他们看起来像四。
然而,总的来说,不同的组的似乎在参数空间中分离良好的:这告诉我们,即使是一个非常简单的监督分类算法,应该也适合于这些数据。 让我们试试看吧。
对数字分类
让我们对数字应用分类算法。就像之前的 Iris 数据那样,我们将数据分为训练和测试集,之后拟合高斯朴素贝叶斯模型。
代码语言:javascript复制Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, random_state=0)
from sklearn.naive_bayes import GaussianNB
model = GaussianNB()
model.fit(Xtrain, ytrain)
y_model = model.predict(Xtest)
既然我们预测了我们的模型,我们可以通过比较测试集和预测,来看看它的准确度。
代码语言:javascript复制from sklearn.metrics import accuracy_score
accuracy_score(ytest, y_model)
# 0.83333333333333337
即使是这个非常简单的模型,我们发现数字分类的准确率约为 80%! 然而,这个单一的数字并没有告诉我们哪里不对 - 一个很好的方式是使用混淆矩阵,我们可以用 Scikit-Learn 和 Seaborn 进行计算:
代码语言:javascript复制from sklearn.metrics import confusion_matrix
mat = confusion_matrix(ytest, y_model)
sns.heatmap(mat, square=True, annot=True, cbar=False)
plt.xlabel('predicted value')
plt.ylabel('true value');
这显示了错误标记的点往往是什么:例如,这里的大量二被错误分类为一或者八。 获取模型特征的直觉的另一种方法,是用预测的标签再次绘制输入。 我们将使用绿色标签表示正确,红色标签表示不正确:
代码语言:javascript复制fig, axes = plt.subplots(10, 10, figsize=(8, 8),
subplot_kw={'xticks':[], 'yticks':[]},
gridspec_kw=dict(hspace=0.1, wspace=0.1))
test_images = Xtest.reshape(-1, 8, 8)
for i, ax in enumerate(axes.flat):
ax.imshow(test_images[i], cmap='binary', interpolation='nearest')
ax.text(0.05, 0.05, str(y_model[i]),
transform=ax.transAxes,
color='green' if (ytest[i] == y_model[i]) else 'red')
检查这个数据的这个子集,我们可以深入了解,算法在哪里可能表现不是最好。 为了超过我们 80% 的分类准确率,我们可能会采用更复杂的算法,如支持向量机(参见支持向量机),随机森林(参见决策树和随机森林)或其他分类方式。
总结
在本节中,我们已经介绍了 Scikit-Learn 数据表示的基本特征和估计器 API。 不管估计类型如何,都需要相同的导入/实例化/拟合/预测模式。 为了掌握有关估计 API 的信息,你可以浏览 Scikit-Learn 文档,并开始在数据上尝试各种模型。
在下一节中,我们将探讨机器学习中最重要的主题:如何选择和验证你的模型。