集成学习和随机森林
如果你随机向几千个人询问一个复杂问题,然后汇总他们的回答。在许多情况下,你会发现,这个汇总的回答比专家的回答还要好,这被称为群体智慧。同样,如果你聚合一组预测器(比如分类器或回归器)的预测,得到的预测结果也比最好的单个预测器要好。这样的一组预测器称为集成,所以这种技术也被称为集成学习,而一个集成学习算法则被称 为集成方法。
例如,你可以训练一组决策树分类器,每一棵树都基于训练集不同的随机子集进行训练。做出预测时,你只需要获得所有树各自的预测,然后给出得票最多的类别作为预测结果。这样一组决策树的集成被称为随机森林,尽管很简单,但它是迄今可用的最强大的机器学习算法之一。
在项目快要结束时,你可能已经构建好了一些不错的预测器,这时就可以通过集成方法将它们组合成一个更强的预测器。
投票分类器
假设你已经训练好了一些分类器,每个分类器的准确率约为80%。大概包括一个逻辑回归分类器、一个SVM分类器、一个随机森林分类器、一个K-近邻分类器,或许还有更多。这时,要创建出一个更好的分类器,最简单的办法就是聚合每个分类器的预测,然后将得票最多的结果作为预测类别。这种大多数投票分类器被称为硬投票分类器。
这个投票法分类器的准确率通常比集成中最好的分类器还要高。事实上,即使每个分类器都是弱学习器(意味着它仅比随机猜测好一点),通过集成依然可以实现一个强学习器(高准确率),只要有足够大数量并且足够多种类的弱学习器即可。
当预测器尽可能互相独立时,集成方法的效果最优。获得多种分类器的方法之一就是使用不同的算法进行训练。这会增加它们犯不同类型错误的机会,从而提升集成的准确率。
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
log_clf = LogisticRegression()
rnd_clf = RandomForestClassifier()
svm_clf = SVC()
voting_clf = VotingClassifier(
estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
voting='hard')
voting_clf.fit(X_train, y_train)
>>> from sklearn.metrics import accuracy_score
>>> for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
... clf.fit(X_train, y_train)
... y_pred = clf.predict(X_test)
... print(clf.__class__.__name__, accuracy_score(y_test, y_pred))
...
LogisticRegression 0.864
RandomForestClassifier 0.896
SVC 0.888
VotingClassifier 0.904
如果所有分类器都能够估算出类别的概率(即有predict_proba()方法),那么你可以将概率在所有单个分类器上平均,然后让Scikit-Learn给出平均概率最高的类别作为预测。这被称为软投票法。通常来说,它比硬投票法的表现更优,因为它给予那些高度自信的投票更高的权重。而所有你需要做的就是用voting="soft"代替voting=“hard”,并确保所有分类器都可以估算出概率。默认情况下,SVC类是不行的,所以你需要将其超参数probability设置为True(这会导致SVC使用交叉验证来估算类别概率,减慢训练速度,并会添加
predict_proba()方法)。如果修改上面代码为使用软投票,你会发现投票分类器的准确率达到91.2%以上
bagging和pasting
前面提到,获得不同种类分类器的方法之一是使用不同的训练算法。还有另一种方法是每个预测器使用的算法相同,但是在不同的训练集随机子集上进行训练。采样时如果将样本放回,这种方法叫作bagging(bootstrap aggregating的缩写,也叫自举汇聚法)。采样时样本不放回,这种方法则叫作pasting。
换句话说,bagging和pasting都允许训练实例在多个预测器中被多次采样,但是只有bagging允许训练实例被同一个预测器多次采样。一旦预测器训练完成,集成就可以通过简单地聚合所有预测器的预测来对新实例做出预测。聚合函数通常是统计法(即最多数的预测与硬投票分类器一样)用于分类,或是平均法用于回归。每个预测器单独的偏差都高于在原始训练集上训练的偏差,但是通过聚合,同时降低了偏差和方差[4]。总体来说,最终结果是,与直接在原始训练集上训练的单个预测器相比,集成的偏差相近,但是方差更低。
Scikit-Learn提供了一个简单的API,可用BaggingClassifier类进行bagging和pasting(或BaggingRegressor用于回归)。以下代码训练了一个包含500个决策树分类器[5]的集成,每次从训练集中随机采样100个训练实例进行训练,然后放回(这是一个bagging的示例,如果你想使用pasting,只需要设置bootstrap=False即可)。参数n_jobs用来指示Scikit-Learn用多少CPU内核进行训练和预测(-1表示让ScikitLearn使用所有可用内核)
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bag_clf = BaggingClassifier(
DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=True, n_jobs=-1)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)
如果基本分类器可以估计类别概率(如果它具有predict_proba()方法),则BaggingClassifier自动执行软投票而不是硬投票,在决策树分类器中就是这种情况
由于自举法给每个预测器的训练子集引入了更高的多样性,所以最后bagging比pasting的偏差略高,但这也意味着预测器之间的关联度更低,所以集成的方差降低。总之,bagging生成的模型通常更好,这也就是为什么它更受欢迎。但是,如果你有充足的时间和CPU资源,可以使用交叉验证来对bagging和pasting的结果进行评估,再做出最合适的选择。
包外评估
对于任意给定的预测器,使用bagging,有些实例可能会被采样多次,而有些实例则可能根本不被采样。BaggingClassifier默认采样m个训练实例,然后放回样本(bootstrap=True),m是训练集的大小。这意味着对每个预测器来说,平均只对63%的训练实例进行采样[6]。剩余37%未被采样的训练实例称为包外(oob)实例。注意,对所有预测器来说,这是不一样的37%。由于预测器在训练过程中从未看到oob实例,因此可以在这些实例上进行评估,而无须单独的验证集。你可以通过平均每个预测器的oob评估来评估整体。
在Scikit-Learn中,创建BaggingClassifier时,设置oob_score=True就可以请求在训练结束后自动进行包外评估。下面的代码演示了这一点。通过变量oob_score_可以得到最终的评估分数
>>> bag_clf = BaggingClassifier(
... DecisionTreeClassifier(), n_estimators=500,
... bootstrap=True, n_jobs=-1, oob_score=True)
...
>>> bag_clf.fit(X_train, y_train)
>>> bag_clf.oob_score_
0.90133333333333332
每个训练实例的包外决策函数也可以通过变量oob_decision_function_获得。本例中(基本预测器有predict_proba()方法),决策函数返回的是每个实例的类别概率。例如,包外评估估计,第二个训练实例有68.25%的概率属于正类(以及31.75%的概率属于负类):
>>> bag_clf.oob_decision_function_
array([[0.31746032, 0.68253968],
[0.34117647, 0.65882353],
[1. , 0. ],
...
[1. , 0. ],
[0.03108808, 0.96891192],
[0.57291667, 0.42708333]])
随机森林
前面已经提到,随机森林[1]是决策树的集成,通常用bagging(有时也可能是pasting)方法训练,训练集大小通过max_samples来设置。除了先构建一个BaggingClassifier然后将其传输到DecisionTreeClassifier,还有一种方法就是使用RandomForestClassifier类,这种方法更方便,对决策树更优化(同样,对于回归任务也有一个RandomForestRegressor类)。
除少数例外,RandomForestClassifier具有DecisionTreeClassifier的所有超参数(以控制树的生长方式),以及BaggingClassifier的所有超参数来控制集成本身
随机森林在树的生长上引入了更多的随机性:分裂节点时不再是搜索最好的特征(见第6章),而是在一个随机生成的特征子集里搜索最好的特征。这导致决策树具有更大的多样性,(再一次)用更高的偏差换取更低的方差,总之,还是产生了一个整体性能更优的模型
极端随机树
如前所述,在随机森林里单棵树的生长过程中,每个节点在分裂时仅考虑到了一个随机子集所包含的特征。如果我们对每个特征使用随机阈值,而不是搜索得出的最佳阈值(如常规决策树),则可能让决策树生长得更加随机。
这种极端随机的决策树组成的森林称为极端随机树集成(或简称Extra-Trees)。同样,它也是以更高的偏差换取了更低的方差。极端随机树训练起来比常规随机森林要快很多,因为在每个节点上找到每个特征的最佳阈值是决策树生长中最耗时的任务之一。使用Scikit-Learn的ExtraTreesClassifier类可以创建一个极端随机树分类器。它的API与RandomForestClassifier类相同。同理,ExtraTreesRegressor类与RandomForestRegressor类的API也相同。
特征重要性
随机森林的另一个好特性是它们使测量每个特征的相对重要性变得容易。Scikit-Learn通过查看使用该特征的树节点平均(在森林中的所有树上)减少不纯度的程度来衡量该特征的重要性。更准确地说,它是一个加权平均值,其中每个节点的权重等于与其关联的训练样本的数量。Scikit-Learn会在训练后为每个特征自动计算该分数,然后对结果进行缩放以使所有重要性的总和等于1。你可以使用feature_importances_变量来访问结果。例如,以下代码在鸢尾花数据集上训练了RandomForestClassifier(在第4章中介绍),并输出每个特征的重要性。看起来最重要的特征是花瓣长度(44%)和宽度
(42%),而花萼的长度和宽度则相对不那么重要(分别是11%和2%):
>>> from sklearn.datasets import load_iris
>>> iris = load_iris()
>>> rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1)
>>> rnd_clf.fit(iris["data"], iris["target"])
>>> for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_):
... print(name, score)
...
sepal length (cm) 0.112492250999
sepal width (cm) 0.0231192882825
petal length (cm) 0.441030464364
petal width (cm) 0.423357996355
提升法
提升法(boosting,最初被称为假设提升)是指可以将几个弱学习器结合成一个强学习器的任意集成方法。大多数提升法的总体思路是循环训练预测器,每一次都对其前序做出一些改正。可用的提升法有很多,但目前最流行的方法是AdaBoost[1](Adaptive Boosting的简称)和梯度提升。我们先从AdaBoost开始介绍。
AdaBoost
新预测器对其前序进行纠正的方法之一就是更多地关注前序欠拟合的训练实例,从而使新的预测器不断地越来越专注于难缠的问题,这就是AdaBoost使用的技术。
例如,当训练AdaBoost分类器时,该算法首先训练一个基础分类器(例如决策树),并使用它对训练集进行预测。然后,该算法会增加分类错误的训练实例的相对权重。然后,它使用更新后的权重训练第二个分类器,并再次对训练集进行预测,更新实例权重,以此类推
这种依序学习技术有一个重要的缺陷就是无法并行(哪怕只是一部分),因为每个预测器只能在前一个预测器训练完成并评估之后才能开始训练。因此,在扩展方面,它的表现不如bagging和pasting方法
Scikit-Learn使用的其实是AdaBoost的一个多分类版本,叫作SAMME(基于多类指数损失函数的逐步添加模型)。当只有两类时,SAMME即等同于AdaBoost。此外,如果预测器可以估算类概率(即具有predict_proba()方法),Scikit-Learn会使用一种SAMME的变体,称为SAMME.R(R代表“Real”),它依赖的是类概率而不是类预测,通常表现更好。
下面的代码使用Scikit-Learn的AdaBoostClassifier(正如你猜想的,还有一个AdaBoostRegressor类)训练了一个AdaBoost分类器,它基于200个单层决策树。顾名思义,单层决策树就是max_depth=1的决策树,换言之,就是一个决策节点加两个叶节点。这是AdaBoostClassifier默认使用的基础估算器。
from sklearn.ensemble import AdaBoostClassifier
ada_clf = AdaBoostClassifier(
DecisionTreeClassifier(max_depth=1), n_estimators=200,
algorithm="SAMME.R", learning_rate=0.5)
ada_clf.fit(X_train, y_train)
梯度提升
另一个非常受欢迎的提升法是梯度提升[5]。与AdaBoost一样,梯度提升也是逐步在集成中添加预测器,每一个都对其前序做出改正。不同之处在于,它不是像AdaBoost那样在每个迭代中调整实例权重,而是让新的预测器针对前一个预测器的残差进行拟合。
我们来看一个简单的回归示例,使用决策树作为基础预测器(梯度提升当然也适用于回归任务),这被称为梯度树提升或者是梯度提升回归树(GBRT)。首先,在训练集(比如带噪声的二次训练集)上拟合一个DecisionTreeRegressor
from sklearn.tree import DecisionTreeRegressor
tree_reg1 = DecisionTreeRegressor(max_depth=2)
tree_reg1.fit(X, y)
针对第一个预测器的残差,训练第二个DecisionTreeRegressor
y2 = y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeRegressor(max_depth=2)
tree_reg2.fit(X, y2)
针对第二个预测器的残差,训练第三个回归器:
y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2)
tree_reg3.fit(X, y3)
现在,我们有了一个包含三棵树的集成。它将所有树的预测相加,从而对新实例进行预测:
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
图7-9的左侧表示这三棵树单独的预测,右侧表示集成的预测。第一行,集成只有一棵树,所以它的预测与第一棵树的预测完全相同。第二行是在第一棵树的残差上训练的一棵新树,从右侧可见,集成的预测等于前面两棵树的预测之和。类似地,第三行又有一棵在第二棵树的残差上训练的新树,集成的预测随着新树的添加逐渐变好。
训练GBRT集成有个简单的方法,就是使用Scikit-Learn的
GradientBoostingRegressor类。与RandomForestRegressor类似,它具
有控制决策树生长的超参数(例如max_depth、min_samples_leaf等),以及控制集成训练的超参数,例如树的数量(n_estimators)。以下代码可创建上面的集成:
from sklearn.ensemble import GradientBoostingRegressor
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0)
gbrt.fit(X, y)
超参数learning_rate对每棵树的贡献进行缩放。如果你将其设置为低值,比如0.1,则需要更多的树来拟合训练集,但是预测的泛化效果通常更好,这是一种被称为收缩的正则化技术。图7-10显示了用低学习率训练的两个GBRT集成:左侧拟合训练集的树数量不足,而右侧拟合训练集的树数量过多从而导致过拟合。
要找到树的最佳数量,可以使用提前停止法(参见第4章)。简单的实现方法就是使用staged_predict()方法:它在训练的每个阶段(一棵树时,两棵树时,等等)都对集成的预测返回一个迭代器。以下代码训练了一个拥有120棵树的GBRT集成,然后测量每个训练阶段的验证误差,从而找到树的最优数量,最后使用最优树数重新训练了一个GBRT集成:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
X_train, X_val, y_train, y_val = train_test_split(X, y)
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120)
gbrt.fit(X_train, y_train)
errors = [mean_squared_error(y_val, y_pred)
for y_pred in gbrt.staged_predict(X_val)]
bst_n_estimators = np.argmin(errors) + 1
gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators)
gbrt_best.fit(X_train, y_train)
实际上,要实现提前停止法,不一定需要先训练大量的树,然后再回头找最优的数字,还可以提前停止训练。设置warm_start=True,当fit()方法被调用时,Scikit-Learn会保留现有的树,从而允许增量训练。以下代码会在验证误差连续5次迭代未改善时,直接停止训练
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True)
min_val_error = float("inf")
error_going_up = 0
for n_estimators in range(1, 120):
gbrt.n_estimators = n_estimators
gbrt.fit(X_train, y_train)
y_pred = gbrt.predict(X_val)
val_error = mean_squared_error(y_val, y_pred)
if val_error < min_val_error:
min_val_error = val_error
error_going_up = 0
else:
error_going_up += 1
if error_going_up == 5:
break # early stopping
GradientBoostingRegressor类还可以支持超参数subsample,指定用于训练每棵树的实例的比例。例如,如果subsample=0.25,则每棵树用25%的随机选择的实例进行训练。现在你可以猜到,这也是用更高的偏差换取了更低的方差,同时在相当大的程度上加速了训练过程。这种技术被称为随机梯度提升
流行的Python库XGBoost(该库代表ExtremeGradient Boosting)中提供了梯度提升的优化实现,该软件包最初是由Tianqi Chen作为分布式(深度)机器学习社区(DMLC)的一部分开发的,其开发目标是极快、可扩展和可移植
堆叠法(stacing)
堆叠法(stacking),又称层叠泛化法[1]。它基于一个简单的想法:与其使用一些简单的函数(比如硬投票)来聚合集成中所有预测器的预测,我们为什么不训练一个模型来执行这个聚合呢?图7-12显示了在新实例上执行回归任务的这样一个集成。底部的三个预测器分别预测了不同的值(3.1、2.7和
2.9),然后最终的预测器(称为混合器或元学习器)将这些预测作为输入,进行最终预测(3.0)。
训练混合器的常用方法是使用留存集[2]。我们看看它是如何工作的。首先,将训练集分为两个子集,第一个子集用来训练第一层的预测器
然后,用第一层的预测器在第二个(留存)子集上进行预测(见图7-14)。因为预测器在训练时从未见过这些实例,所以可以确保预测是“干净的”。那么现在对于留存集中的每个实例都有了三个预测值。我们可以使用这些预测值作为输入特征,创建一个新的训练集(新的训练集有三个维度),并保留目标值。在这个新的训练集上训练混合器,让它学习根据第一层的预测来预测目标值。
事实上,通过这种方法可以训练多种不同的混合器(例如,一个使用线性回归,另一个使用随机森林回归,等等)。于是我们可以得到一个混合器层。诀窍在于将训练集分为三个子集:第一个用来训练第一层,第二个用来创造训练第二层的新训练集(使用第一层的预测),而第三个用来创造训练第三层的新训练集(使用第二层的预测)。一旦训练完成,我们可以按照顺序遍历每层来对新实例进行预测
不幸的是,Scikit-Learn不直接支持堆叠