原文:
annas-archive.org/md5/1799e60b9ee143f87b98f9fc0705a0c9
译者:飞龙
第十一章:降维
垃圾进,垃圾出——在本书中,我们看到,当将机器学习方法应用于训练数据时,这一模式同样成立。回顾过去,我们意识到,最有趣的机器学习挑战总是涉及某种特征工程,在这些挑战中,我们尝试利用对问题的洞察力,精心设计额外的特征,期望机器学习器能够识别并利用这些特征。
在本章中,我们将走向相反的方向,进行降维,去除那些无关或冗余的特征。移除特征乍一看似乎是反直觉的,因为更多的信息通常应该比更少的信息更好。此外,即使我们的数据集中有冗余特征,难道学习算法不会迅速识别并将它们的权重设为 0 吗?以下是一些仍然适用于尽可能减少维度的合理理由:
-
多余的特征可能会干扰或误导学习器。并非所有机器学习方法都会出现这种情况(例如,支持向量机喜欢高维空间)。然而,大多数模型在较少的维度下感觉更安全。
-
另一个反对高维特征空间的理由是,更多的特征意味着更多的参数需要调整,也增加了过拟合的风险。
-
我们为了解决任务而检索的数据可能具有人工高维性,而实际的维度可能很小。
-
更少的维度 = 更快的训练 = 在相同的时间框架内可以尝试更多的参数变化 = 更好的最终结果。
-
可视化——如果我们想要可视化数据,我们只能在二维或三维中进行。
因此,在这里我们将展示如何去除数据中的垃圾,同时保留其中真正有价值的部分。
绘制我们的路线图
降维大致可以分为特征选择和特征提取方法。在本书的几乎每一章中,我们都使用了一种特征选择方法,无论是在发明、分析还是可能丢弃某些特征时。在本章中,我们将介绍一些利用统计方法的方式,即相关性和互信息,来实现这一目标,尤其是在特征空间非常大的情况下。特征提取试图将原始特征空间转换为低维特征空间。这在我们无法通过选择方法去除特征,但仍然拥有过多特征以供学习器使用时尤其有用。我们将通过主成分分析(PCA)、线性判别分析(LDA)和多维尺度分析(MDS)来演示这一点。
特征选择
如果我们想对机器学习算法友好,我们应该提供那些彼此独立、但与待预测值高度相关的特征。这意味着每个特征都提供了显著的信息。移除任何特征都会导致性能下降。
如果我们只有少数几个特征,可以绘制一个散点图矩阵(每对特征组合画一个散点图)。特征之间的关系可以很容易地被发现。对于每一对明显依赖的特征,我们就会考虑是否应该去除其中一个,或者更好地设计一个新的、更干净的特征。
然而,大多数情况下,我们需要从多个特征中进行选择。想一想分类任务中,我们有一个词袋来分类答案的质量,这将需要一个 1000×1000 的散点图。在这种情况下,我们需要一种更自动化的方法来检测重叠特征并解决它们。我们将在以下小节中介绍两种常见的方法,即过滤器和包装器。
使用过滤器检测冗余特征
过滤器试图独立于任何后续使用的机器学习方法清理特征集。它们依赖统计方法来查找哪些特征是冗余的或无关的。在冗余特征的情况下,它只保留每个冗余特征组中的一个。无关的特征则会被直接移除。一般来说,过滤器按以下工作流程进行:
相关性
使用相关性,我们可以轻松看到特征对之间的线性关系。在以下图表中,我们可以看到不同程度的相关性,并且有一个潜在的线性依赖关系通过红色虚线(拟合的 1 维多项式)显示出来。每个单独图表顶部的相关系数 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_14.jpg 是通过 scipy.stat
库的 pearsonr()
函数计算的常见 Pearson 相关系数(Pearson r
值)。
给定两个相同大小的数据系列,它返回相关系数值和 p 值的元组。p 值描述了数据系列是否可能由一个不相关的系统生成。换句话说,p 值越高,我们就越不应该信任相关系数:
>>> from scipy.stats import pearsonr
>>> pearsonr([1,2,3], [1,2,3.1])
>>> (0.99962228516121843, 0.017498096813278487)
>>> pearsonr([1,2,3], [1,20,6])
>>> (0.25383654128340477, 0.83661493668227405)
在第一个案例中,我们明显看到两个系列是相关的。在第二个案例中,我们仍然看到一个明显非零的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_15.jpg 值。
然而,0.84 的 p 值告诉我们,相关系数并不显著,我们不应过于关注它。请查看以下图表:
在前三个具有高相关系数的案例中,我们可能会希望抛弃 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_16.jpg 或 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_17.jpg,因为它们似乎传递了相似的,甚至是相同的信息。
然而,在最后一个案例中,我们应该保留两个特征。在我们的应用中,当然,这个决定将由 p 值来驱动。
尽管在前面的例子中表现得很好,现实通常对我们并不友好。基于相关性的特征选择的一个大缺点是,它只能检测线性关系(即可以用直线建模的关系)。如果我们在非线性数据上使用相关性,就会发现问题。在以下的例子中,我们有一个二次关系:
尽管人眼可以立刻看到 X[1]与 X[2]之间的关系,除了右下角的图表外,相关系数却看不出这种关系。显然,相关性对于检测线性关系很有用,但对于其他类型的关系却无能为力。有时候,应用简单的变换就能获得线性关系。例如,在前面的图中,如果我们把 X[2]绘制在 X[1]的平方上,我们会得到一个较高的相关系数。然而,正常数据并不总是能提供这样的机会。
幸运的是,对于非线性关系,互信息来拯救我们。
互信息
在进行特征选择时,我们不应像在上一节中那样关注关系的类型(线性关系)。相反,我们应该从一个特征提供多少信息的角度来思考(前提是我们已经拥有另一个特征)。
为了理解这一点,我们假设要使用house_size
(房屋面积)、number_of_levels
(楼层数)和avg_rent_price
(平均租金)特征集来训练一个分类器,该分类器判断房屋是否有电梯。在这个例子中,我们直观地认为,知道了house_size
后,我们不再需要知道number_of_levels
,因为楼层数在某种程度上包含了冗余信息。而对于avg_rent_price
,情况不同,因为我们无法仅通过房屋的大小或楼层数来推断租金。因此,最好只保留其中一个特征,外加租金的平均值。
互信息通过计算两个特征之间有多少信息是共享的,来形式化上述推理。然而,与相关性不同,互信息不依赖于数据的顺序,而是依赖于数据的分布。为了理解它是如何工作的,我们需要稍微了解一下信息熵。
假设我们有一个公平的硬币。在我们抛掷硬币之前,关于它是正面还是反面我们将面临最大的未知,因为正反两面的概率各为 50%。这种不确定性可以通过 Claude Shannon 的信息熵来度量:
在我们公平硬币的例子中,有两种情况:让https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_19.jpg代表正面,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_20.jpg代表反面,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_21.jpg。
因此,得出结论:
提示
为了方便起见,我们也可以使用scipy.stats.entropy([0.5, 0.5], base=2)
。我们将 base 参数设置为2
,以便得到与之前相同的结果。否则,函数将使用自然对数(通过np.log()
)。一般来说,基数无关紧要(只要你始终如一地使用它)。
现在,假设我们事先知道这枚硬币其实并不公平,抛掷后正面朝上的概率是 60%:
我们看到这种情况的不确定性较小。随着我们离 0.5 越来越远,不确定性会减少,达到极端值 0,即正面朝上的概率为 0%或 100%,如以下图表所示:
我们将通过将熵应用于两个特征而非一个来修改熵!互信息,从而衡量当我们了解 Y 时,X 的不确定性减少了多少。然后,我们可以捕捉到一个特征如何减少另一个特征的不确定性。
例如,在没有任何关于天气的进一步信息的情况下,我们完全不确定外面是否在下雨。如果我们现在得知外面的草地是湿的,那么不确定性就减少了(我们仍然需要检查洒水器是否开启了)。
更正式地说,互信息被定义为:
这看起来有点让人害怕,但其实不过是求和和乘积。例如,计算https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_26.jpg是通过对特征值进行分箱,然后计算每个箱中值的比例。在以下图表中,我们将箱的数量设置为十个。
为了将互信息限制在区间[0,1]内,我们必须将其除以它们各自的熵之和,这样就得到了归一化互信息:
互信息的一个优点是,与相关性不同,它不仅仅关注线性关系,正如我们在以下图表中所看到的:
如我们所见,互信息能够指示线性关系的强度。下图展示了它同样适用于平方关系:
所以,我们需要做的是计算所有特征对的归一化互信息。对于每对具有过高值的特征(我们需要确定这意味着什么),我们将删除其中一个。在回归的情况下,我们可以删除与目标结果值互信息过低的特征。
这可能适用于特征集不太大的情况。然而,到了某个阶段,这个过程可能会非常昂贵,因为计算量会呈二次方增长(因为我们在计算特征对之间的互信息)。
过滤器的另一个重大缺点是,它们会丢弃那些在单独使用时似乎没有用的特征。实际上,往往有一些特征看起来与目标变量完全独立,但当它们结合起来时,却非常有效。为了保留这些特征,我们需要使用包装器。
使用包装器向模型询问特征
尽管过滤器可以在去除无用特征方面起到很大作用,但它们的效果也是有限的。经过所有的过滤后,仍然可能会有一些特征彼此独立,并且与结果变量之间有某种程度的依赖性,但从模型的角度来看,它们仍然是完全无用的。试想一下下面描述异或(XOR)函数的数据。单独来看,A
和 B
都不会显示出与 Y
之间的任何依赖性,而它们一起时却明显存在依赖关系:
A | B | Y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
那么,为什么不直接让模型本身来为每个特征投票呢?这就是包装器的作用,正如我们在以下过程图中所看到的那样:
在这里,我们将特征重要性的计算推送到了模型训练过程中。不幸的是(但可以理解),特征重要性并不是以二进制方式确定的,而是以排名值的形式给出的。因此,我们仍然需要指定切割点,决定我们愿意保留哪些特征,哪些特征我们想要丢弃?
回到 scikit-learn,我们可以在 sklearn.feature_selection
包中找到多种优秀的包装器类。在这个领域中,一个非常强大的工具是 RFE
,即递归特征消除。它接收一个估算器和要保留的特征数量作为参数,然后用各种特征集训练估算器,直到它找到一个足够小的特征子集。RFE
实例本身看起来就像一个估算器,实际上就是包装了提供的估算器。
在以下示例中,我们使用数据集方便的 make_classification()
函数创建了一个人工分类问题,包含 100 个样本。该函数允许我们指定创建 10 个特征,其中只有 3 个特征对于解决分类问题是非常有价值的:
>>> from sklearn.feature_selection import RFE
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.datasets import make_classification
>>> X,y = make_classification(n_samples=100, n_features=10, n_informative=3, random_state=0)
>>> clf = LogisticRegression()
>>> clf.fit(X, y)
>>> selector = RFE(clf, n_features_to_select=3)
>>> selector = selector.fit(X, y)
>>> print(selector.support_)
[False True False True False False False False True False]
>>> print(selector.ranking_)
[4 1 3 1 8 5 7 6 1 2]
现实世界中的问题当然是,如何知道 n_features_to_select
的正确值呢?事实上,我们无法得知这个值。然而,大多数时候我们可以利用数据的一个样本,并通过不同的设置进行尝试,快速感受出大致的合适范围。
好消息是,我们在使用包装器时不必非常精确。让我们尝试不同的 n_features_to_select
值,看看 support_
和 ranking_
是如何变化的:
n_features_to_select | support_ | ranking_ |
---|---|---|
1 | [False False False True False False False False False False] | [ 6 3 5 1 10 7 9 8 2 4] |
2 | [False False False True False False False False True False] | [5 2 4 1 9 6 8 7 1 3] |
3 | [假 真 假 真 假 假 假 假 真 假] | [4 1 3 1 8 5 7 6 1 2] |
4 | [假 真 假 真 假 假 假 假 真 真] | [3 1 2 1 7 4 6 5 1 1] |
5 | [假 真 真 真 假 假 假 假 真 真] | [2 1 1 1 6 3 5 4 1 1] |
6 | [ 真 真 真 真 假 假 假 假 真 真] | [1 1 1 1 5 2 4 3 1 1] |
7 | [ 真 真 真 真 假 真 假 假 真 真] | [1 1 1 1 4 1 3 2 1 1] |
8 | [ 真 真 真 真 假 真 假 真 真 真] | [1 1 1 1 3 1 2 1 1 1] |
9 | [ 真 真 真 真 假 真 真 真 真 真] | [1 1 1 1 2 1 1 1 1 1] |
10 | [ 真 真 真 真 真 真 真 真 真 真] | [1 1 1 1 1 1 1 1 1 1] |
我们看到结果非常稳定。当请求较小特征集时使用的特征,在允许更多特征进入时依然会被选择。最终,我们依赖于训练/测试集的划分,提醒我们何时走偏。
其他特征选择方法
在阅读机器学习文献时,你会发现还有其他几种特征选择方法。有些方法甚至看起来不像特征选择方法,因为它们被嵌入到学习过程中(不要与前面提到的包装器方法混淆)。例如,决策树在其核心中深深植入了特征选择机制。其他学习方法则采用某种正则化,惩罚模型复杂度,从而推动学习过程向着表现良好的“简单”模型发展。它们通过将影响较小的特征重要性降为零,然后将其丢弃(L1 正则化)来实现这一点。
所以,要小心!通常,机器学习方法的强大功能很大程度上要归功于它们植入的特征选择方法。
特征提取
在某些时候,在我们移除冗余特征并丢弃不相关特征后,通常仍然会发现特征过多。无论使用什么学习方法,它们的表现都很差,并且考虑到庞大的特征空间,我们理解它们实际上做得更好是不可行的。我们意识到必须“割肉”,必须剔除那些所有常识告诉我们它们是有价值的特征。另一个需要减少维度而特征选择方法帮助不大的情况是当我们想要可视化数据时。此时,我们需要最终只有最多三维,才能提供任何有意义的图表。
引入特征提取方法。它们重构特征空间,使其对模型更易接入,或简单地将维度降低到二或三,以便我们可以通过可视化显示依赖关系。
同样地,我们可以将特征提取方法区分为线性方法和非线性方法。另外,正如在 选择特征 部分中所见,我们将为每种类型介绍一种方法(主成分分析作为线性方法,以及多维尺度法的非线性版本)。尽管它们被广泛知晓和使用,但它们仅是许多更有趣且强大的特征提取方法的代表。
关于主成分分析
主成分分析(PCA)通常是你想要减少特征数量且不知道使用哪种特征提取方法时首先要尝试的内容。PCA 是一种线性方法,因此有其局限性,但它可能已经足够使你的模型学习得足够好。加上它所提供的强大数学性质以及它在找到变换后的特征空间的速度,以及之后在原始特征和变换特征之间的转换速度;我们几乎可以保证,它也会成为你常用的机器学习工具之一。
总结而言,给定原始特征空间,PCA 会在一个低维空间中找到其线性投影,并具有以下特性:
-
最大化保留的方差。
-
最小化最终的重构误差(即试图从变换后的特征恢复到原始特征时的误差)。
由于 PCA 只是对输入数据进行变换,它可以同时应用于分类和回归问题。在本节中,我们将使用分类任务来讨论这一方法。
绘制 PCA
PCA 涉及很多线性代数知识,我们不打算深入探讨。然而,基本的算法可以简单地描述如下:
-
通过从数据中减去均值来对数据进行中心化。
-
计算协方差矩阵。
-
计算协方差矩阵的特征向量。
如果我们从 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_28.jpg 特征开始,那么算法将返回一个变换后的特征空间,依然具有 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_28.jpg 维度(到目前为止我们并没有获得任何新东西)。然而,这个算法的优点在于,特征值表示对应特征向量所描述的方差量。
假设我们从 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_29.jpg 特征开始,并且我们知道模型在超过 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_30.jpg 特征时表现不好。那么,我们只需选择具有最大特征值的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_30.jpg 特征向量。
应用 PCA
让我们考虑以下人工数据集,并在左侧的图表中进行可视化:
>>> x1 = np.arange(0, 10, .2)
>>> x2 = x1+np.random.normal(loc=0, scale=1, size=len(x1))
>>> X = np.c_[(x1, x2)]
>>> good = (x1>5) | (x2>5) # some arbitrary classes
>>> bad = ~good # to make the example look good
Scikit-learn 提供了 PCA
类在其分解包中。在这个例子中,我们可以清楚地看到,使用一个维度就足够描述数据。我们可以通过 n_components
参数来指定这一点:
>>> from sklearn import linear_model, decomposition, datasets
>>> pca = decomposition.PCA(n_components=1)
此外,在这里我们可以使用pca
的fit()
和transform()
方法(或其fit_transform()
组合)来分析数据,并将其投影到变换后的特征空间:
>>> Xtrans = pca.fit_transform(X)
如我们所指定,Xtrans
仅包含一个维度。你可以在前面的右侧图中看到结果。在这种情况下,结果甚至是线性可分的。我们甚至不需要复杂的分类器来区分这两个类别。
为了理解重构误差,我们可以查看在变换中保留下来的数据的方差:
>>> print(pca.explained_variance_ratio_)
>>> [ 0.96393127]
这意味着,在从二维降到一维之后,我们仍然保留了 96%的方差。
当然,这并不总是如此简单。通常情况下,我们并不知道预先应该选择多少个维度。在这种情况下,我们在初始化PCA
时不指定n_components
参数,让它计算完整的变换。拟合数据后,explained_variance_ratio_
包含一个按降序排列的比率数组:第一个值是描述最大方差方向的基向量的比率,第二个值是描述第二大方差方向的比率,依此类推。绘制这个数组后,我们很快就能感觉出需要多少个主成分:图表上肘部之前的成分数通常是一个不错的估计。
提示
显示每个主成分方差解释度的图称为碎石图。将碎石图与网格搜索结合以找到分类问题的最佳设置的一个很好的例子可以在scikit-learn.sourceforge.net/stable/auto_examples/plot_digits_pipe.html
找到。
PCA 的局限性以及 LDA 如何提供帮助
作为一种线性方法,PCA 在面对具有非线性关系的数据时,当然也有其局限性。我们在这里不深入讨论,但可以简单地说,PCA 有一些扩展方法,例如核 PCA,它引入了非线性变换,使我们仍然可以使用 PCA 方法。
PCA 的另一个有趣的弱点是,当它应用于特殊的分类问题时,我们将在这里讨论这一点。让我们将good = (x1 > 5) | (x2 > 5)
替换为good = x1 > x2
来模拟这种特殊情况,并且我们很快就能看到问题所在:
在这里,类别并不是按照方差最大的轴进行分布,而是按照第二大方差的轴分布。显然,PCA 在这种情况下失效了。由于我们没有为 PCA 提供任何关于类别标签的提示,它无法做得更好。
线性判别分析(LDA)在这里派上了用场。它是一种方法,旨在最大化不同类别之间点的距离,同时最小化同一类别点之间的距离。我们不会详细说明底层理论的具体工作原理,只提供一个如何使用它的简要教程:
>>> from sklearn import lda
>>> lda_inst = lda.LDA(n_components=1)
>>> Xtrans = lda_inst.fit_transform(X, good)
就这些。请注意,与之前的 PCA 示例不同,我们将类别标签提供给了fit_transform()
方法。因此,PCA 是一种无监督特征提取方法,而 LDA 是有监督的。结果如预期所示:
那么,为什么还要考虑 PCA,而不是直接使用 LDA 呢?其实,事情并没有那么简单。随着类别数量的增加和每个类别样本的减少,LDA 的效果就不那么理想了。此外,PCA 似乎对不同训练集的敏感度不如 LDA。因此,当我们需要建议使用哪种方法时,我们只能明确地说“取决于具体情况”。
多维尺度法
尽管 PCA 试图通过优化保持方差,多维尺度法(MDS)则尽可能保留相对距离,以减少维度。这在我们处理高维数据集并希望获得可视化印象时非常有用。
MDS 不关心数据点本身,而是关注数据点对之间的异质性,并将其解释为距离。因此,MDS 算法首先做的事情是,取所有https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_28.jpg的维度https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_31.jpg的数据点,并使用距离函数https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/bd-ml-sys-py-2e/img/2772OS_11_32.jpg计算距离矩阵,该函数衡量原始特征空间中的(大多数情况下是欧几里得)距离:
现在,MDS 尝试将各个数据点放置到低维空间中,以使该空间中的新距离尽可能接近原始空间中的距离。由于 MDS 通常用于可视化,低维度的选择通常是二维或三维。
让我们看一下以下简单的数据,包含三个五维空间中的数据点。两个数据点较为接近,另一个则非常不同,我们想要在三维和二维中可视化这一点:
>>> X = np.c_[np.ones(5), 2 * np.ones(5), 10 * np.ones(5)].T
>>> print(X)
[[ 1\. 1\. 1\. 1\. 1.]
[ 2\. 2\. 2\. 2\. 2.]
[ 10\. 10\. 10\. 10\. 10.]]
使用 scikit-learn 的manifold
包中的MDS
类,我们首先指定希望将X
转换为三维欧几里得空间:
>>> from sklearn import manifold
>>> mds = manifold.MDS(n_components=3)
>>> Xtrans = mds.fit_transform(X)
为了在二维中可视化,我们需要相应地设置n_components
。
结果可以在以下两张图中看到。三角形和圆形点非常接近,而星形点则远离它们:
让我们来看一下稍微复杂一些的鸢尾花数据集。稍后我们将用它来对比 LDA 和 PCA。鸢尾花数据集每朵花包含四个属性。使用之前的代码,我们将其投影到三维空间,同时尽可能保持个别花朵之间的相对距离。在之前的例子中,我们没有指定任何度量,因此MDS
将默认使用欧几里得距离。这意味着,依据四个属性“不同”的花朵,应该在 MDS 缩放后的三维空间中保持较远的距离,而相似的花朵则应该聚集在一起,正如下图所示:
将 PCA 用于将维度减少到三维和二维后,我们可以看到同一类别的花朵在图中呈现出预期的更大分布,如下图所示:
当然,使用 MDS 需要理解各个特征的单位;也许我们使用的特征无法通过欧几里得度量来比较。例如,一个类别变量,即使它被编码为整数(0=圆形,1=星形,2=三角形,等等),也无法通过欧几里得距离来比较(圆形与星形比与三角形更接近吗?)。
然而,一旦我们意识到这个问题,MDS 就是一个有用的工具,它能够揭示数据中的相似性,这在原始特征空间中是难以察觉的。
更深入地看 MDS,我们意识到它并不是单一的算法,而是不同算法的一家族,而我们只使用了其中一个。PCA 也是如此。此外,如果你发现 PCA 或 MDS 都无法解决你的问题,可以查看 scikit-learn 工具包中提供的许多其他流形学习算法。
然而,在你被众多不同算法淹没之前,最好的方法是从最简单的一个开始,看看它能带你多远。然后,再尝试下一个更复杂的算法,并从那里继续。
总结
你学到了有时可以通过特征选择方法去除完整的特征。我们也看到,在某些情况下,这还不够,我们必须使用特征提取方法来揭示数据中的真实和低维结构,希望模型能够更轻松地处理它。
当然,我们只是触及了可用的维度减少方法这一巨大领域的表面。尽管如此,我们希望能激发你对这个领域的兴趣,因为还有许多其他方法等待你去发掘。最终,特征选择和提取是一门艺术,就像选择合适的学习方法或训练模型一样。
下一章将介绍 Jug 的使用,这是一个小型的 Python 框架,用于以多核或多机器的方式管理计算。你还将了解 AWS,亚马逊云服务。
第十二章. 更大的数据
很难界定什么是大数据。我们将采用一种操作性定义:当数据变得庞大到难以处理时,我们称之为大数据。在某些领域,这可能意味着拍字节级的数据或万亿级的交易数据:这些数据无法完全存储在单个硬盘中。在其他情况下,它可能只有一百倍小,但仍然难以处理。
为什么数据本身成为了一个问题?虽然计算机的处理速度不断提高,内存也在增加,但数据的规模也在不断增长。事实上,数据的增长速度快于计算速度,而且很少有算法能够随着输入数据规模的增加而线性扩展——综合来看,这意味着数据的增长速度超过了我们处理它的能力。
我们将首先基于前几章的一些经验,处理我们可以称之为中等数据规模的问题(不是大数据,但也不算小)。为此,我们将使用一个叫做jug的包,它使我们能够执行以下任务:
-
将你的管道分解为任务
-
缓存(缓存)中间结果
-
利用多个核心,包括网格上的多台计算机
下一步是转向真正的大数据,我们将看到如何使用云计算。特别地,你将了解 Amazon Web Services 基础设施。在这一部分,我们介绍了另一个名为 StarCluster 的 Python 包来管理集群。
学习大数据
"大数据"这一表达并不意味着特定数量的数据,无论是数据样本的数量,还是数据占用的千兆字节、太字节或拍字节的数量。它意味着数据的增长速度快于处理能力的提升。这意味着以下几点:
-
过去有效的方法和技术现在需要重新做或替换,因为它们无法很好地扩展到新的输入数据规模。
-
算法不能假设所有输入数据都能装入内存。
-
管理数据本身成为了一项主要任务
-
使用计算机集群或多核机器已经不再是奢侈,而是必要。
本章将重点讲解这一拼图的最后一块:如何利用多个核心(无论是在同一台机器上还是在不同机器上)来加速和组织你的计算。这对于其他中等规模的数据任务也会有帮助。
使用 jug 将你的管道分解为任务
我们通常有一个简单的管道:我们对初始数据进行预处理,计算特征,然后用计算出的特征调用机器学习算法。
Jug 是由本书作者之一 Luis Pedro Coelho 开发的一个包。它是开源的(使用自由的 MIT 许可),在许多领域都能派上用场,但它是专门围绕数据分析问题设计的。它同时解决了多个问题,例如:
-
它可以将结果缓存到磁盘(或数据库),这意味着如果你要求它计算已经计算过的内容,结果将直接从磁盘读取。
-
它可以使用多个核心,甚至在集群中的多台计算机上运行。Jug 的设计也非常适合批处理计算环境,这些环境使用排队系统,如PBS(Portable Batch System)、LSF(Load Sharing Facility)或Grid Engine。这将在本章后半部分使用,我们将构建在线集群并向它们分配任务。
jug 中的任务简介
任务是 jug 的基本构建块。一个任务由一个函数和其参数值组成。考虑这个简单的例子:
def double(x):
return 2*x
在本章中,代码示例通常需要键入到脚本文件中。因此,它们不会显示>>>
标记。应该在 shell 中输入的命令会以美元符号($
)作为前缀。
一个任务可以是“调用 double,参数为 3”。另一个任务可以是“调用 double,参数为 642.34”。使用 jug,我们可以按如下方式构建这些任务:
from jug import Task
t1 = Task(double, 3)
t2 = Task(double, 642.34)
将其保存为名为jugfile.py
的文件(这只是一个常规的 Python 文件)。现在,我们可以运行jug execute
来执行这些任务。这是你在命令行中输入的,而不是在 Python 提示符下输入的,因此我们用美元符号($
)标记它:
$ jug execute
你还会收到一些关于任务的反馈(jug 会显示两个名为double
的任务被执行)。再次运行jug execute
,它会告诉你它什么也没做!其实它不需要做任何事情。在这种情况下,我们并没有获得太多收益,但如果任务计算时间很长,这个缓存将非常有用。
你可能会注意到,在硬盘上出现了一个名为jugfile.jugdata
的新目录,并且里面有一些奇怪命名的文件。这是记忆化缓存。如果你删除它,jug execute
将会重新运行所有任务。
通常,区分纯函数和更一般的函数是有益的,纯函数只是接受输入并返回结果,而一般函数可以执行一些动作(例如从文件中读取、写入文件、访问全局变量、修改其参数,或任何该语言允许的操作)。一些编程语言,比如 Haskell,甚至有语法方法来区分纯函数和不纯函数。
使用 jug 时,你的任务不需要完全纯粹。甚至建议你使用任务来读取数据或写入结果。然而,访问和修改全局变量将不能很好地工作:任务可能在不同的处理器上以任何顺序执行。全局常量除外,但即使是常量,也可能会干扰记忆化系统(如果值在执行过程中发生变化)。类似地,你不应该修改输入值。jug 有一个调试模式(使用jug execute --debug
),虽然它会减慢计算速度,但如果你犯了类似的错误,它会给你有用的错误信息。
上面的代码是有效的,但有些繁琐。你总是在重复使用Task(function, argument)
这个构造。利用一些 Python 技巧,我们可以使代码看起来更加自然,如下所示:
from jug import TaskGenerator
from time import sleep
@TaskGenerator
def double(x):
sleep(4)
return 2*x
@TaskGenerator
def add(a, b):
return a + b
@TaskGenerator
def print_final_result(oname, value):
with open(oname, 'w') as output:
output.write('Final result: {}\n'.format(value))
y = double(2)
z = double(y)
y2 = double(7)
z2 = double(y2)
print_final_result('output.txt', add(z,z2))
除了使用TaskGenerator
,前面的代码实际上可以是一个标准的 Python 文件!然而,使用TaskGenerator
,它实际上创建了一系列任务,现在可以以一种利用多个处理器的方式来运行它。在幕后,装饰器将你的函数转化为在调用时不执行,而是创建一个Task
对象。我们还利用了可以将任务传递给其他任务的事实,这会生成一个依赖关系。
你可能已经注意到,我们在前面的代码中添加了一些sleep(4)
调用。这是为了模拟运行一个长时间的计算。否则,这个示例运行得非常快,根本没有使用多个处理器的必要。
我们通过运行jug status
来开始,结果如下图所示:
现在,我们同时启动两个进程(在后台使用&
操作符):
$ jug execute &
$ jug execute &
现在,我们再次运行jug status
:
我们可以看到,两个初始的双重操作符正在同时运行。大约 8 秒后,整个过程将完成,output.txt
文件将被写入。
顺便提一下,如果你的文件名不是jugfile.py
,你就需要在命令行上显式指定它。例如,如果你的文件名是analysis.py
,你应该运行以下命令:
$ jug execute analysis.py
这就是不使用jugfile.py
名称的唯一缺点。所以,尽管使用更有意义的名称吧。
查看引擎盖下
jug 是如何工作的?从基本层面上来说,它非常简单。一个Task
是一个函数加上它的参数。它的参数可以是值,也可以是其他任务。如果一个任务依赖于其他任务,那么这两个任务之间就有了依赖关系(并且第二个任务在第一个任务的结果可用之前无法运行)。
基于此,jug 会递归地计算每个任务的哈希值。这个哈希值编码了整个计算过程,以获得结果。当你运行jug execute
时,对于每个任务,会有一个小循环执行以下流程图中展示的逻辑:
默认的后端将文件写入磁盘(在这个名为jugfile.jugdata/
的有趣目录中)。另外,还有一个后端可用,使用 Redis 数据库。通过适当的锁定,jug 会处理这个问题,这也允许多个处理器执行任务;每个进程将独立查看所有任务,并运行那些尚未执行的任务,然后将它们写回共享的后端。这在同一台机器(使用多核处理器)或多台机器上都可以运行,只要它们都能访问相同的后端(例如,使用网络磁盘或 Redis 数据库)。在本章的后半部分,我们将讨论计算机集群,但现在我们先专注于多个核心。
你也可以理解为什么它能够记忆中间结果。如果后端已经有某个任务的结果,它就不会再执行。如果你更改了任务,即使是微小的更改(例如修改了某个参数),其哈希值也会改变。因此,任务会被重新执行。此外,所有依赖于该任务的任务也会改变它们的哈希值,并且会被重新执行。
使用 jug 进行数据分析
Jug 是一个通用框架,但它非常适合中等规模的数据分析。在开发分析管道时,最好让中间结果自动保存。如果你之前已经计算过预处理步骤,并且只是在更改计算的特征,你就不希望重新计算预处理步骤。如果你已经计算过特征,但希望尝试将一些新的特征组合起来,你也不希望重新计算所有其他特征。
Jug 还特别优化了与 NumPy 数组的协作。每当你的任务返回或接收 NumPy 数组时,你就利用了这种优化。Jug 是这个生态系统中的另一部分,所有内容都在一起工作。
现在,我们回顾一下第十章,计算机视觉。在那一章中,我们学习了如何计算图像的特征。记住,基本管道包括以下特征:
-
加载图像文件
-
计算特征
-
合并这些特征
-
归一化特征
-
创建分类器
我们将重新进行这个练习,但这次将使用 jug。这个版本的优势在于,现在可以在不需要重新计算整个管道的情况下,添加新的特征或分类器。
我们首先进行以下几个导入:
from jug import TaskGenerator
import mahotas as mh
from glob import glob
现在,我们定义第一个任务生成器和特征计算函数:
@TaskGenerator
def compute_texture(im):
from features import texture
imc = mh.imread(im)
return texture(mh.colors.rgb2gray(imc))
@TaskGenerator
def chist_file(fname):
from features import chist
im = mh.imread(fname)
return chist(im)
我们导入的features
模块来自第十章,计算机视觉。
注意
我们编写的函数将文件名作为输入,而不是图像数组。当然,使用完整的图像也是可行的,但这是一个小的优化。文件名是字符串,在写入后端时相对较小。如果需要计算哈希值,也非常快速。此外,这样可以确保只有需要图像的进程才会加载它们。
我们可以在任何函数上使用TaskGenerator
。即使是我们没有编写的函数,例如np.array
、np.hstack
,或者下面的命令,这也成立:
import numpy as np
to_array = TaskGenerator(np.array)
hstack = TaskGenerator(np.hstack)
haralicks = []
chists = []
labels = []
# Change this variable to point to
# the location of the dataset on disk
basedir = '../SimpleImageDataset/'
# Use glob to get all the images
images = glob('{}/*.jpg'.format(basedir))
for fname in sorted(images):
haralicks.append(compute_texture(fname))
chists.append(chist_file(fname))
# The class is encoded in the filename as xxxx00.jpg
labels.append(fname[:-len('00.jpg')])
haralicks = to_array(haralicks)
chists = to_array(chists)
labels = to_array(labels)
使用 jug 的一个小不便之处是,我们必须始终编写函数将结果输出到文件,如前面的示例所示。这是使用 jug 的额外便利性所付出的一个小代价。
@TaskGenerator
def accuracy(features, labels):
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn import cross_validation
clf = Pipeline([('preproc', StandardScaler()),
('classifier', LogisticRegression())])
cv = cross_validation.LeaveOneOut(len(features))
scores = cross_validation.cross_val_score(
clf, features, labels, cv=cv)
return scores.mean()
请注意,我们仅在此函数内部导入sklearn
。这是一个小优化。这样,只有在真正需要时,sklearn
才会被导入:
scores_base = accuracy(haralicks, labels)
scores_chist = accuracy(chists, labels)
combined = hstack([chists, haralicks])
scores_combined = accuracy(combined, labels)
最后,我们编写并调用一个函数来输出所有结果。它期望其参数是一个包含算法名称和结果的对列表:
@TaskGenerator
def print_results(scores):
with open('results.image.txt', 'w') as output:
for k,v in scores:
output.write('Accuracy [{}]: {:.1%}\n'.format(
k, v.mean()))
print_results([
('base', scores_base),
('chists', scores_chist),
('combined' , scores_combined),
])
就是这样。现在,在 shell 中运行以下命令,通过 jug 运行这个管道:
$ jug execute image-classification.py
复用部分结果
例如,假设你想添加一个新功能(甚至是一组功能)。正如我们在第十章中看到的,计算机视觉,通过更改特征计算代码是很容易做到的。然而,这将意味着需要重新计算所有特征,这样是浪费的,尤其是当你想快速测试新特征和技术时。
我们现在添加一组特征,也就是另一种叫做线性二值模式的纹理特征。这在 mahotas 中已实现;我们只需要调用一个函数,但我们将其封装在 TaskGenerator
中:
@TaskGenerator
def compute_lbp(fname):
from mahotas.features import lbp
imc = mh.imread(fname)
im = mh.colors.rgb2grey(imc)
# The parameters 'radius' and 'points' are set to typical values
# check the documentation for their exact meaning
return lbp(im, radius=8, points=6)
我们替换了之前的循环,增加了一个额外的函数调用:
lbps = []
for fname in sorted(images):
# the rest of the loop as before
lbps.append(compute_lbp(fname))
lbps = to_array(lbps)
我们使用这些较新的特征来计算准确度:
scores_lbps = accuracy(lbps, labels)
combined_all = hstack([chists, haralicks, lbps])
scores_combined_all = accuracy(combined_all, labels)
print_results([
('base', scores_base),
('chists', scores_chist),
('lbps', scores_lbps),
('combined' , scores_combined),
('combined_all' , scores_combined_all),
])
现在,当你再次运行 jug execute
时,新的特征将被计算出来,但旧的特征将从缓存中加载。这就是 jug 强大的地方。它确保你始终得到你想要的结果,同时避免不必要地重新计算缓存的结果。你还会看到,添加这组特征集改善了之前的方法。
本章未能提及 jug 的所有功能,但以下是我们未在正文中涵盖的一些可能感兴趣的功能总结:
-
jug invalidate
:这声明给定函数的所有结果应被视为无效,并需要重新计算。这还将重新计算任何依赖于(即使是间接依赖)无效结果的下游计算。 -
jug status --cache
:如果jug status
运行时间过长,你可以使用--cache
标志来缓存状态并加快速度。请注意,这不会检测 jugfile 的任何更改,但你可以随时使用--cache --clear
来清除缓存并重新开始。 -
jug cleanup
:这将删除备忘缓存中的所有额外文件。这是一个垃圾回收操作。
注意
还有其他更高级的功能,允许你查看在 jugfile 内部已计算的值。请阅读 jug 文档中的有关屏障等功能,网址为 jug.rtfd.org
。
使用亚马逊 Web 服务
当你有大量数据和大量计算需要执行时,你可能会开始渴望更多的计算能力。亚马逊(aws.amazon.com
)允许你按小时租用计算能力。这样,你可以在不需要提前购买大量机器(包括管理基础设施成本)的情况下,访问大量的计算能力。市场上还有其他竞争者,但亚马逊是最大的玩家,因此我们在此简单介绍。
亚马逊网络服务(AWS)是一个大型服务集。我们将只关注弹性计算云(EC2)服务。此服务为您提供虚拟机和磁盘空间,可以快速分配和释放。
使用有三种模式。第一种是预留模式,您通过预付费获得更便宜的每小时访问费用,固定的每小时费用,以及一个变化的费用,这取决于整体计算市场(当需求较少时,费用较低;当需求较多时,价格会上涨)。
在这个通用系统的基础上,有几种不同类型的机器可供选择,成本各异,从单核到具有大量内存或甚至图形处理单元(GPU)的多核系统。稍后我们会看到,您还可以获取几台较便宜的机器并构建一个虚拟集群。您还可以选择获取 Linux 或 Windows 服务器(Linux 略便宜)。在本章中,我们将在 Linux 上操作示例,但大部分信息对于 Windows 机器同样适用。
对于测试,您可以使用免费层中的单台机器。这允许您操作系统,熟悉界面等。请注意,这台机器的 CPU 较慢。
资源可以通过网页界面进行管理。但是,也可以通过编程方式进行管理,并编写脚本来分配虚拟机、格式化硬盘并执行所有通过网页界面可能执行的操作。事实上,虽然网页界面变化频繁(本书中展示的一些截图可能在出版时已经过时),但编程接口更加稳定,且自服务推出以来,整体架构保持稳定。
访问 AWS 服务通过传统的用户名/密码系统进行,尽管亚马逊将用户名称为访问密钥,密码称为秘密密钥。他们这样做可能是为了将其与用于访问网页界面的用户名/密码分开。事实上,您可以创建任意多的访问/秘密密钥对,并为其分配不同的权限。这对于较大的团队非常有帮助,团队中的高级用户可以访问完整的网页面板,进而为权限较少的开发人员创建其他密钥。
注意
亚马逊公司有多个区域。这些区域对应世界上的物理位置:美国西海岸、美国东海岸、几个亚洲位置、一个南美位置以及两个欧洲位置。如果您要传输数据,最好将数据保持在接收和发送的地方附近。此外,请记住,如果您处理用户信息,可能会有关于将数据传输到另一个司法管辖区的监管问题。在这种情况下,请咨询一位知情的律师,了解将关于欧洲客户的数据传输到美国或其他类似的转移所涉及的法律问题。
亚马逊云服务是一个非常庞大的话题,专门覆盖 AWS 的书籍有很多。本章的目的是让你对 AWS 所提供的服务和可能实现的功能有一个整体印象。本书的实践精神就是通过示例来实现这一目标,但我们并不会涵盖所有可能性。
创建你的第一个虚拟机
第一步是访问aws.amazon.com/
并创建一个账户。这些步骤与任何其他在线服务相似。一个机器是免费的,但如果你需要更多的机器,你将需要一张信用卡。在本示例中,我们将使用几台机器,因此如果你想跟着做,可能会花费你一些费用。如果你还不准备提供信用卡,你当然可以先阅读这一章,了解 AWS 提供的服务,而不需要亲自操作示例。然后你可以做出更有信息的决定,看看是否注册。
一旦你注册 AWS 并登录,你将进入控制台。在这里,你将看到 AWS 提供的众多服务,如下图所示:
我们选择并点击EC2(左侧栏中最顶部的元素——这是本书写作时显示的面板。亚马逊会定期进行小的更改,所以你看到的可能与我们书中的稍有不同)。现在我们看到了 EC2 管理控制台,如下图所示:
在右上角,你可以选择你的区域(请参见亚马逊区域信息框)。注意,你只会看到你当前选择的区域的信息。因此,如果你错误地选择了错误的区域(或者有多个区域的机器在运行),你的机器可能不会出现(这似乎是使用 EC2 网页管理控制台时常见的陷阱)。
在 EC2 术语中,正在运行的服务器称为实例。我们选择启动实例,这将进入下一个屏幕,要求我们选择要使用的操作系统:
选择Amazon Linux选项(如果你熟悉其他提供的 Linux 发行版,比如 Red Hat、SUSE 或 Ubuntu,你也可以选择其中一个,但配置会有所不同)。现在你已经选择了软件,接下来需要选择硬件。在下一个屏幕中,你将被要求选择要使用的机器类型:
我们将从一个t2.micro类型的实例开始(t1.micro类型是较老的、性能更弱的机器)。这是最小的机器,并且是免费的。不断点击下一步并接受所有默认设置,直到你看到提到密钥对的屏幕:
我们将为密钥对选择 awskeys
这个名字。然后勾选 Create a new key pair。将密钥对文件命名为 awskeys.pem
。下载并将此文件保存在一个安全的位置!这是 SSH(安全外壳)密钥,允许您登录到云端机器。接受其余默认设置,您的实例将会启动。
现在,您需要等待几分钟,直到您的实例启动完成。最终,实例将显示为绿色,并且状态为 running:
在前面的截图中,您应该能看到公共 IP 地址,可以用来登录实例,具体如下:
$ ssh -i awskeys.pem ec2-user@54.93.165.5
因此,我们将调用 ssh
命令,并将之前下载的密钥文件作为身份验证文件传递给它(使用 -i
选项)。我们作为 ec2-user
用户,登录到 IP 地址为 54.93.165.5 的机器。这个地址在您的情况下当然会有所不同。如果您为实例选择了其他发行版,用户名也可能会变化。在这种情况下,您可以尝试以 root
、ubuntu
(对于 Ubuntu 发行版)或 fedora
(对于 Fedora 发行版)登录。
最后,如果您正在运行 Unix 风格的操作系统(包括 macOS),您可能需要通过以下命令调整其权限:
$ chmod 600 awskeys.pem
这仅为当前用户设置读写权限。否则,SSH 会给您一个丑陋的警告。
现在,您应该能够登录到您的机器。如果一切正常,您应该能看到如下面截图所示的横幅:
这是一个常规的 Linux 机器,您拥有 sudo
权限:通过在命令前加上 sudo
,您可以以超级用户身份运行任何命令。您可以运行系统推荐的 update
命令来让您的机器保持最新状态。
在 Amazon Linux 上安装 Python 包
如果您更喜欢其他发行版,您可以利用您对该发行版的了解来安装 Python、NumPy 等包。在这里,我们将在标准的 Amazon 发行版上进行操作。我们首先安装几个基本的 Python 包,具体如下:
$ sudo yum -y install python-devel \
python-pip numpy scipy python-matplotlib
为了编译 mahotas,我们还需要一个 C++ 编译器:
$ sudo yum -y install gcc-c++
最后,我们安装 git
,以确保能够获取到本书的最新代码:
$ sudo yum -y install git
在该系统中,pip 被安装为 python-pip
。为了方便起见,我们将使用 pip 升级它自身。然后,我们将使用 pip 安装必要的包,具体如下:
$ sudo pip-python install -U pip
$ sudo pip install scikit-learn jug mahotas
此时,您可以使用 pip 安装任何其他您希望安装的包。
在我们的云机器上运行 jug
我们现在可以使用以下命令序列来下载本书的数据和代码:
$ git clone \
https://github.com/luispedro/BuildingMachineLearningSystemsWithPython
$ cd BuildingMachineLearningSystemsWithPython
$ cd ch12
最后,我们运行以下命令:
$ jug execute
这可以正常工作,但我们将不得不等待很长时间才能看到结果。我们的免费级别机器(t2.micro 类型)速度较慢,且仅有一个处理器。因此,我们将 升级我们的机器!
我们返回到 EC2 控制台,右键单击正在运行的实例以获得弹出菜单。我们需要首先停止该实例。这相当于关闭虚拟机。你可以随时停止你的机器,停止后就不再为其付费。请注意,你仍在使用磁盘空间,这部分会单独计费。你也可以终止实例,这样会销毁磁盘,丢失机器上保存的所有信息。
一旦机器停止,更改实例类型 选项将变得可用。现在,我们可以选择一个更强大的实例,例如具有八个核心的 c1.xlarge 实例。机器仍然是关闭的,所以你需要重新启动它(相当于虚拟机的开机)。
提示
AWS 提供了几种不同价格的实例类型。由于这些信息会随着更强大的选项引入和价格变化(通常是变便宜)而不断更新,我们无法在书中提供太多细节,但你可以在 Amazon 网站上找到最新的信息。
我们需要等待实例重新启动。一旦它恢复,按照之前的方式查找其 IP 地址。当你更改实例类型时,实例会被分配一个新的地址。
提示
你可以使用 Amazon.com 的弹性 IP 功能为实例分配固定 IP,弹性 IP 在 EC2 控制台的左侧可以找到。如果你经常创建和修改实例,这会非常有用。使用该功能会有少量费用。
拥有八个核心,你可以同时运行八个 jug 进程,如下面的代码所示:
$ # the loop below runs 8 times
$ for counter in $(seq 8); do
> jug execute &
> done
使用 jug status
检查这八个任务是否正在运行。任务完成后(这应该会很快),你可以停止机器并将其降级回 t2.micro 实例以节省费用。微型实例可以免费使用(在某些限制范围内),而我们使用的 c1.xlarge 实例则需要每小时 0.064 美元(截至 2015 年 2 月—请查阅 AWS 网站获取最新信息)。
使用 StarCluster 自动生成集群
正如我们刚刚学到的,我们可以使用 Web 界面创建机器,但这很快变得乏味且容易出错。幸运的是,Amazon 提供了一个 API。这意味着我们可以编写脚本,自动执行我们之前讨论的所有操作。更好的是,其他人已经开发了工具,可以用来机制化并自动化你想要在 AWS 上执行的许多流程。
麻省理工学院的一个小组开发了这样一个工具,名为 StarCluster。它恰好是一个 Python 包,所以你可以使用 Python 工具按照如下方式安装它:
$ sudo pip install starcluster
你可以从 Amazon 机器或本地机器上运行此操作。两种选择都可以使用。
我们需要指定集群的配置。我们通过编辑配置文件来实现这一点。我们通过运行以下命令生成模板配置文件:
$ starcluster help
然后选择在~/.starcluster/config
中生成配置文件的选项。完成后,我们将手动编辑它。
提示
密钥,密钥,再来一点密钥
在处理 AWS 时,有三种完全不同类型的密钥非常重要。首先是标准的用户名/密码组合,用于登录网站。其次是 SSH 密钥系统,它是一个通过文件实现的公钥/私钥系统;通过公钥文件,您可以登录远程机器。第三是 AWS 访问密钥/秘密密钥系统,它只是一个用户名/密码的形式,允许您在同一账户中拥有多个用户(包括为每个用户添加不同的权限,但本书不涉及这些高级功能)。
要查找我们的访问/密钥,返回 AWS 控制台,点击右上角的用户名,选择安全凭证。现在在屏幕底部应该会看到我们的访问密钥,它可能类似于AAKIIT7HHF6IUSN3OCAA,我们将在本章中使用它作为示例。
现在,编辑配置文件。这是一个标准的.ini
文件:一个文本文件,其中每个部分以括号中的名称开始,选项则以name=value
格式指定。第一个部分是aws info
部分,您需要将密钥复制并粘贴到这里:
[aws info]
AWS_ACCESS_KEY_ID = AAKIIT7HHF6IUSN3OCAA
AWS_SECRET_ACCESS_KEY = <your secret key>
接下来是有趣的部分,即定义集群。StarCluster 允许您根据需要定义任意多个集群。初始文件中有一个名为 smallcluster 的集群。它在cluster smallcluster
部分中定义。我们将编辑它,使其如下所示:
[cluster smallcluster]
KEYNAME = mykey
CLUSTER_SIZE = 16
这将把节点数从默认的两个节点更改为 16 个。我们还可以指定每个节点的实例类型,以及初始映像是什么(记住,映像用于初始化虚拟硬盘,它定义了您将运行的操作系统和已安装的软件)。StarCluster 有一些预定义的映像,但您也可以自己创建。
我们需要使用以下命令创建一个新的 SSH 密钥:
$ starcluster createkey mykey -o ~/.ssh/mykey.rsa
现在我们已经配置了一个 16 节点的集群并设置了密钥,让我们试试:
$ starcluster start smallcluster
这可能需要几分钟,因为它会分配 17 台新机器。为什么是 17 台,而不是我们的集群只有 16 个节点?StarCluster 始终会创建一个主节点。所有这些节点都共享相同的文件系统,因此我们在主节点上创建的任何内容都将被工作节点看到。这也意味着我们可以在这些集群上使用 jug。
这些集群可以按您的需求使用,但它们预先配备了一个作业队列引擎,这使得它们非常适合批处理。使用它们的过程非常简单:
-
您登录到主节点。
-
您在主节点上准备脚本(或者更好的是,提前准备好它们)。
-
您将作业提交到队列中。作业可以是任何 Unix 命令。调度程序将寻找空闲节点并运行您的作业。
-
您等待作业完成。
-
您可以在主节点上读取结果。您现在也可以终止所有从节点以节省费用。无论如何,在您不再需要时不要让系统持续运行!否则,这将花费您(以美元和美分)。
在登录到集群之前,我们将数据复制到其中(请记住,我们之前已经将存储库克隆到BuildingMachineLearningSystemsWithPython
):
$ dir=BuildingMachineLearningSystemsWithPython
$ starcluster put smallcluster $dir $dir
我们使用$dir
变量使命令行适应单行。我们可以用一条命令登录到主节点:
$ starcluster sshmaster smallcluster
我们也可以查找生成的机器地址,并像之前那样使用ssh
命令,但使用上述命令,无论地址是什么,StarCluster 都会在幕后为我们处理它。
正如我们之前所说,StarCluster 为其集群提供了批处理队列系统;您编写一个脚本来执行操作,将其放入队列,它将在任何可用节点上运行。
此时,我们需要再次安装一些包。幸运的是,StarCluster 已经完成了一半的工作。如果这是一个真实的项目,我们将设置一个脚本来为我们执行所有的初始化工作。StarCluster 可以做到这一点。由于这是一个教程,我们只需再次运行安装步骤:
$ pip install jug mahotas scikit-learn
我们可以像以前一样使用相同的jugfile
系统,只是现在,不再直接在主节点上运行,而是在集群上进行调度。
首先,编写一个非常简单的包装脚本如下:
#!/usr/bin/env bash
jug execute jugfile.py
将其命名为run-jugfile.sh
并使用chmod +x run-jugfile.sh
赋予它可执行权限。现在,我们可以使用以下命令在集群上安排十六个作业:
$ for c in $(seq 16); do
> qsub -cwd run-jugfile.sh
> done
这将创建 16 个作业,每个作业将运行run-jugfile.sh
脚本,我们简称为 jug。您仍然可以按照自己的意愿使用主节点。特别是,您随时可以运行jug status
来查看计算的状态。事实上,jug 就是在这样的环境中开发的,因此在这种环境中非常有效。
最终,计算将完成。此时,我们需要首先保存结果。然后,我们可以终止所有节点。我们在~/results
目录下创建一个目录,并将结果复制到此处:
# mkdir ~/results
# cp results.image.txt ~/results
现在,从集群注销回到我们的工作机器:
# exit
现在,我们回到我们的 AWS 机器(请注意下一个代码示例中的$
符号)。首先,我们使用starcluster get
命令将结果复制回这台计算机(这是我们之前使用put
命令的镜像):
$ starcluster get smallcluster results results
最后,为了节省费用,我们应该关闭所有节点如下:
$ starcluster stop smallcluster
$ starcluster terminate smallcluster
注意
请注意,终止操作将真正销毁文件系统和所有结果。在我们的情况下,我们已经手动将最终结果复制到了安全位置。另一个可能性是让集群写入一个不被 StarCluster 分配和销毁的文件系统,但在常规实例上对您可用;事实上,这些工具的灵活性是巨大的。然而,这些高级操作不可能全部在本章中展示。
StarCluster 在 star.mit.edu/cluster/
上有出色的在线文档,你应该阅读以了解更多关于该工具的所有可能性。我们这里只展示了其功能的很小一部分,并且只使用了默认设置。
总结
我们展示了如何使用 jug,一个小型 Python 框架来管理计算,以便利用多个核心或多台机器。虽然这个框架是通用的,但它是专门为其作者(本书的另一位作者)解决数据分析需求而构建的。因此,它有多个方面使其适配 Python 机器学习环境。
你还了解了 AWS,即亚马逊云。使用云计算通常比建设内部计算能力更有效,尤其是在需求不稳定且不断变化的情况下。StarCluster 甚至允许集群在你启动更多任务时自动扩展,在任务终止时自动缩减。
本书结束了。我们已经走过了很长的路。你学习了如何在标注数据时进行分类,在未标注数据时进行聚类。你了解了降维和主题建模,以便理解大数据集。接着,我们看了一些具体的应用(例如音乐流派分类和计算机视觉)。在实现中,我们依赖于 Python 语言。该语言拥有一个越来越庞大的数值计算包生态,建立在 NumPy 基础上。只要可能,我们依赖于 scikit-learn,但在必要时也会使用其他包。由于这些包都使用相同的基本数据结构(NumPy 多维数组),因此可以无缝地混合不同包的功能。本书中使用的所有包都是开源的,可以用于任何项目。
自然地,我们并没有涵盖所有机器学习话题。在附录中,我们提供了一些其他资源的指引,帮助感兴趣的读者进一步学习机器学习。
附录 A. 如何进一步学习机器学习
我们已接近本书结尾,现在花点时间看看其他可能对读者有用的资源。
网上有许多极好的资源可以用来进一步学习机器学习——多到我们无法在这里全部覆盖。因此,以下列表只能代表一小部分,并且是作者在写作时认为最好的资源,可能带有一定的偏向性。
在线课程
Andrew Ng 是斯坦福大学的教授,他在 Coursera (www.coursera.org
) 上开设了一门机器学习的在线课程,作为一门大型开放在线课程。这是免费的,但可能需要投入大量时间。
书籍
本书侧重于机器学习的实际应用方面。我们没有呈现算法背后的思考过程,也没有讲解理论依据。如果你对机器学习的这一方面感兴趣,我们推荐 Christopher Bishop 的*《模式识别与机器学习》*。这是该领域的经典入门书籍,将教你大多数本书中使用的算法的细节。
如果你想深入了解所有复杂的数学细节,那么 Kevin P. Murphy 的*《机器学习:一种概率视角》*是一个很好的选择 (www.cs.ubc.ca/~murphyk/MLbook)。这本书出版于 2012 年,非常新颖,涵盖了机器学习研究的前沿。其 1100 页内容也可以作为参考书,因为几乎没有遗漏任何机器学习的内容。
问答网站
MetaOptimize (metaoptimize.com/qa
) 是一个机器学习问答网站,许多非常有经验的研究者和从业者在此互动。
Cross Validated (stats.stackexchange.com
) 是一个通用的统计学问答网站,经常有关于机器学习的问题。
正如书中开头提到的,如果你对书中的某些部分有疑问,可以随时在 TwoToReal (www.twotoreal.com
) 提问。我们尽量尽快回答并提供帮助。
博客
这里列出的是一份显然不完全的博客列表,适合从事机器学习工作的人阅读:
-
机器学习理论:
hunch.net
平均发布频率约为每月一篇,内容更具理论性,提供了额外的大脑挑战。
-
实践中的文本与数据挖掘:
textanddatamining.blogspot.de
平均发布频率为每月一篇,内容非常实用,提供总是令人惊讶的独特方法。
-
Edwin Chen’s Blog:
blog.echen.me
平均发布频率为每月一篇,内容涵盖更多实际应用话题。
-
Machined Learnings:
www.machinedlearnings.com
平均发布频率为每月一篇,内容涵盖更多实际应用话题。
-
FlowingData:
flowingdata.com
平均发布频率为每天一篇,文章内容更多围绕统计学展开。
-
Simply Statistics:
simplystatistics.org
每月发布几篇文章,内容侧重于统计学和大数据。
-
统计建模、因果推断与社会科学:
andrewgelman.com
每天发布一篇文章,内容通常有趣,作者通过统计数据指出流行媒体的缺陷。
数据来源
如果你想尝试各种算法,可以从加州大学尔湾分校(UCI)的机器学习库中获取许多数据集。你可以在archive.ics.uci.edu/ml
找到它。
变得具有竞争力
了解更多关于机器学习的绝佳方式是尝试一个竞赛!Kaggle (www.kaggle.com
) 是一个机器学习竞赛的市场,在介绍中已经提到过。在这个网站上,你会发现多个不同结构和通常带有现金奖励的竞赛。
监督学习竞赛几乎总是遵循以下格式:你(以及每位其他参赛者)都可以访问带标签的训练数据和无标签的测试数据。你的任务是提交对测试数据的预测。当竞赛结束时,准确率最高的人获胜。奖品从荣耀到现金不等。
当然,赢得一些东西很好,但只要参与,你就可以获得许多有用的经验。因此,在竞赛结束后,你必须继续关注,因为参与者开始在论坛上分享他们的方法。大多数情况下,获胜并不是关于开发新算法,而是巧妙地预处理、归一化和组合现有方法。
所有被遗漏的内容
我们没有涵盖 Python 中所有可用的机器学习包。考虑到空间有限,我们选择专注于 scikit-learn。但是,还有其他选择,我们在这里列出了一些:
-
MDP 工具包 (
mdp-toolkit.sourceforge.net
):用于数据处理的模块化工具包 -
PyBrain (
pybrain.org
):基于 Python 的强化学习、人工智能和神经网络库 -
机器学习工具包(Milk)(
luispedro.org/software/milk
):这个包由本书的一位作者开发,涵盖了一些 scikit-learn 中未包含的算法和技术。 -
Pattern (
www.clips.ua.ac.be/pattern
):一个结合了网页挖掘、自然语言处理和机器学习的包,具有 Google、Twitter 和 Wikipedia 的包装 API。
一个更通用的资源是mloss.org
,这是一个开源机器学习软件库。像这样的库通常情况下,质量从优秀且维护良好的软件到一次性项目然后被抛弃的项目不等。如果你的问题非常具体,而通用包没有解决方案,这可能值得一看。
摘要
现在我们真的到了尽头。希望你喜欢这本书,并且感觉已经准备好开始自己的机器学习冒险了。
我们也希望你理解仔细测试方法的重要性。特别是,使用正确的交叉验证方法的重要性,以及不要报告训练测试结果,因为这些结果通常是对你方法真正效果的过高估计。