· Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 2nd Edition, by Aurélien Géron (O’Reilly). Copyright 2019 Aurélien Géron, 978-1-492-03264-9.
· 《机器学习》周志华
· 环境:Anaconda(Python 3.8) + Pycharm
· 学习时间:2022.05.05~2022.05.05
如果你随机向几千个人询问一个复杂问题,然后汇总他们的回答。在许多情况下,你会发现,这个汇总的回答比专家的回答还要好,这被称为群体智慧。同样,如果你聚合一组预测器(比如分类器或回归器)的预测,得到的预测结果也比最好的单个预测器要好。这样的一组预测器称为集成,所以这种技术也被称为集成学习,而一个集成学习算法则被称为集成方法。
集成学习(ensemble learning)通过构建并结合多个学习器来完成学习任务,有时也被称为多分类器系统(multi-classifier system))、基于委员会的学习(committee-based learning)等。
弱学习器(weak learner)常指泛化性能略优于随机猜测的学习器。集成学习的很多理论研究都是针对弱学习器进行的,而基学习器有时也被直接称为弱学习器。
例如,你可以训练一组决策树分类器,每一棵树都基于训练集不同的随机子集进行训练。做出预测时,你只需要获得所有树各自的预测,然后给出得票最多的类别作为预测结果(见第6章练习题8)。这样一组决策树的集成被称为随机森林,尽管很简单,但它是迄今可用的最强大的机器学习算法之一。
此外,正如我们在第2章讨论过的,在项目快要结束时,你可能已经构建好了一些不错的预测器,这时就可以通过集成方法将它们组合成一个更强的预测器。事实上,在机器学习竞赛中获胜的解决方案通常都涉及多种集成方法(最知名的是Nerflix大奖赛)。
集成学习的一般结构∶ 先产生一组"个体学习器"(individual learner),再用某种策略将它们结合起来、个体学习器通常由一个现有的学习算法从训练数据产生,例如 C4.5 决策树算法、BP神经网络算法等,此时集成中只包含同种类型的个体学习器、例如"决策树集成"中全是决策树,“神经网络集成"中全是神经网络,这样的集成是"同质"的(homogeneous)。同质集成中的个体学习器亦称"基学习器”(base learner),相应的学习算法称为"基学习算法"(base learning algorithm)。
集成也可包含不同类型的个体学习器,例如同时包含决策树和神经网络。这样的集成是"异质"的(heterogenous)。异质集成中的个体学习器由不同的学习算法生成,这时就不再有基学习算法;相应的,个体学习器一般不称为基学习器,常称为"组件学习器"(component learner)或直接称为个体学习器。
本章我们将探讨最流行的几种集成方法,包括bagging、boosting、stacking等,也将探索随机森林。
假设你已经训练好了一些分类器,每个分类器的准确率约为80%。大概包括一个逻辑回归分类器、一个SVM分类器、一个随机森林分类器、一个K-近邻分类器,或许还有更多。这时,要创建出一个更好的分类器,最简单的办法就是聚合每个分类器的预测,然后将得票最多的结果作为预测类别。这种大多数投票分类器被称为硬投票分类器。
你会多少有点惊讶地发现,这个投票法分类器的准确率通常比集成中最好的分类器还要高。事实上,即使每个分类器都是弱学习器(意味着它仅比随机猜测好一点),通过集成依然可以实现一个强学习器(高准确率),只要有足够大数量并且足够多种类的弱学习器即可。
辅助理解:集成学习voting Classifier在sklearn中的实现。
这怎么可能呢?下面这个类比可以帮助你掀开这层神秘面纱。假设你有一个略微偏倚的硬币,它有51%的可能正面数字朝上,49%的可能背面花朝上。如果你掷1000次,你大致会得到差不多510次数字和490次花,所以正面是大多数。而如果你做数学题,你会发现,“在1000次投掷后,大多数为正面朝上”的概率接近75%。投掷硬币的次数越多,这个概率越高(例如,投掷10 000次后,这个概率攀升至97%)。这是因为大数定理导致的:随着你不断投掷硬币,正面朝上的比例越来越接近于正面的概率(51%)。下图显示了10条偏倚硬币的投掷结果。可以看出随着投掷次数的增加,正面的比例逐渐接近51%,最终所有10条线全都接近51%,并且始终位于50%以上。
同样,假设你创建了一个包含1000个分类器的集成,每个分类器都只有51%的概率是正确的(几乎不比随机猜测强多少)。如果你以大多数投票的类别作为预测结果,可以期待的准确率高达75%。但是,这基于的前提是所有的分类器都是完全独立的,彼此的错误毫不相关。显然这是不可能的,因为它们都是在相同的数据上训练的,很可能会犯相同的错误,所以也会有很多次大多数投给了错误的类别,导致集成的准确率有所降低。
当预测器尽可能互相独立时,集成方法的效果最优。获得多种分类器的方法之一就是使用不同的算法进行训练。这会增加它们犯不同类型错误的机会,从而提升集成的准确率。
下面的代码用Scikit-Learn创建并训练一个投票分类器,由三种不同的分类器组成(训练集是卫星数据集,见第5章):
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=500, noise=0.30, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
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.896
# VotingClassifier 0.904
如果所有分类器都能够估算出类别的概率(即有predict_proba()方法),那么你可以将概率在所有单个分类器上平均,然后让Scikit-Learn给出平均概率最高的类别作为预测。这被称为软投票法。通常来说,它比硬投票法的表现更优,因为它给予那些高度自信的投票更高的权重。而所有你需要做的就是用voting="soft"代替voting=“hard”,并确保所有分类器都可以估算出概率。默认情况下,SVC类是不行的,所以你需要将其超参数probability设置为True(这会导致SVC使用交叉验证来估算类别概率,减慢训练速度,并会添加predict_proba()方法)。如果修改上面代码为使用软投票,你会发现投票分类器的准确率达到91.2%以上!
前面提到,获得不同种类分类器的方法之一是使用不同的训练算法。还有另一种方法是每个预测器使用的算法相同,但是在不同的训练集随机子集上进行训练。
采样时如果将样本放回,这种方法叫作bagging(bootstrap aggregating的缩写,也叫自举汇聚法)。采样时样本不放回,这种方法则叫作pasting。
换句话说,bagging和pasting都允许训练实例在多个预测器中被多次采样,但是只有bagging允许训练实例被同一个预测器多次采样。采样过程和训练过程如下图所示。
一旦预测器训练完成,集成就可以通过简单地聚合所有预测器的预测来对新实例做出预测。聚合函数通常是统计法(即最多数的预测与硬投票分类器一样)用于分类,或是平均法用于回归。每个预测器单独的偏差都高于在原始训练集上训练的偏差,但是通过聚合,同时降低了偏差和方差。总体来说,最终结果是,与直接在原始训练集上训练的单个预测器相比,集成的偏差相近,但是方差更低。
你可以通过不同的CPU内核甚至不同的服务器并行地训练预测器。类似地,预测也可以并行。这正是bagging和pasting方法如此流行的原因之一,它们非常易于扩展。
Scikit-Learn提供了一个简单的API,可用BaggingClassifier类进行bagging和pasting(或BaggingRegressor用于回归)。以下代码训练了一个包含500个决策树分类器的集成,每次从训练集中随机采样100个训练实例进行训练,然后放回(这是一个bagging的示例,如果你想使用pasting,只需要设置bootstrap=False即可)。参数n_jobs用来指示Scikit-Learn用多少CPU内核进行训练和预测(-1表示让Scikit-Learn使用所有可用内核):
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自动执行软投票而不是硬投票,在决策树分类器中就是这种情况。
下图比较了两种决策边界,一种是单个决策树,一种是由500个决策树组成的bagging集成(来自前面的代码),二者均在卫星数据集上训练完成。可以看出,集成预测的泛化效果很可能会比单独的决策树要好一些:二者偏差相近,但是集成的方差更小(两边训练集上的错误数量差不多,但是集成的决策边界更规则)。
由于自举法给每个预测器的训练子集引入了更高的多样性,所以最后bagging比pasting的偏差略高,但这也意味着预测器之间的关联度更低,所以集成的方差降低。总之,bagging生成的模型通常更好,这也就是为什么它更受欢迎。但是,如果你有充足的时间和CPU资源,可以使用交叉验证来对bagging和pasting的结果进行评估,再做出最合适的选择。
对于任意给定的预测器,使用bagging,有些实例可能会被采样多次,而有些实例则可能根本不被采样。BaggingClassifier默认采样m个训练实例,然后放回样本(bootstrap=True),m是训练集的大小。这意味着对每个预测器来说,平均只对63%的训练实例进行采样。剩余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, random_state=42)
bag_clf.fit(X_train, y_train)
print(bag_clf.oob_score_)
# 输出:0.896
根据此oob评估,此BaggingClassifier能在测试集上达到约90.1%的准确率。让我们验证一下:
from sklearn.metrics import accuracy_score
y_pred_2 = bag_clf.predict(X_test)
print(accuracy_score(y_test, y_pred_2))
# 输出:0.92
每个训练实例的包外决策函数也可以通过变量oob_decision_function_获得。本例中(基本预测器有predict_proba()方法),决策函数返回的是每个实例的类别概率。例如,包外评估估计,第二个训练实例有68.25%的概率属于正类(以及31.75%的概率属于负类):
print(bag_clf.oob_decision_function_)
# 输出:
# [[0.32352941 0.67647059]
# [0.3375 0.6625 ]
# ……
# [1. 0. ]
# [0.02564103 0.97435897]
# [0.6344086 0.3655914 ]]
BaggingClassifier类也支持对特征进行采样。采样由两个超参数控制:max_features和bootstrap_features。它们的工作方式与max_samples和bootstrap相同,但用于特征采样而不是实例采样。因此,每个预测器将用输入特征的随机子集进行训练。
这对于处理高维输入(例如图像)特别有用。对训练实例和特征都进行抽样,这称为随机补丁方法。而保留所有训练实例(即bootstrap=False并且max_samples=1.0)但是对特征进行抽样(即bootstrap_features=True并且/或max_features<1.0),这被称为随机子空间法。
对特征抽样给预测器带来更大的多样性,所以以略高一点的偏差换取了更低的方差。
个人理解:随机补丁是对样本和属性都进行一定程度的抽样;随机子空间就是仅对属性进行抽样。属性抽样就是起到一个提取特征的作用。
前面已经提到,随机森林是决策树的集成,通常用bagging(有时也可能是pasting)方法训练,训练集大小通过max_samples来设置。除了先构建一个BaggingClassifier然后将其传输到DecisionTreeClassifier,还有一种方法就是使用RandomForestClassifier类,这种方法更方便,对决策树更优化(同样,对于回归任务也有一个RandomForestRegressor类)。
以下代码使用所有可用的CPU内核,训练了一个拥有500棵树的随机森林分类器(每棵树限制为最多16个叶节点):
from sklearn.ensemble import RandomForestClassifier
rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
除少数例外,RandomForestClassifier具有DecisionTreeClassifier的所有超参数(以控制树的生长方式),以及BaggingClassifier的所有超参数来控制集成本身。
随机森林在树的生长上引入了更多的随机性:分裂节点时不再是搜索最好的特征(见第6章),而是在一个随机生成的特征子集里搜索最好的特征。这导致决策树具有更大的多样性,(再一次)用更高的偏差换取更低的方差,总之,还是产生了一个整体性能更优的模型。
随机森林(RF)就是以决策树为base model,利用bagging的思想训练出来的,每个base model都是决策树。
如前所述,在随机森林里单棵树的生长过程中,每个节点在分裂时仅考虑到了一个随机子集所包含的特征。如果我们对每个特征使用随机阈值,而不是搜索得出的最佳阈值(如常规决策树),则可能让决策树生长得更加随机。
这种极端随机的决策树组成的森林称为极端随机树集成(或简称Extra-Trees)。同样,它也是以更高的偏差换取了更低的方差。极端随机树训练起来比常规随机森林要快很多,因为在每个节点上找到每个特征的最佳阈值是决策树生长中最耗时的任务之一。
使用Scikit-Learn的ExtraTreesClassifier类可以创建一个极端随机树分类器。它的API与RandomForestClassifier类相同。同理,ExtraTreesRegressor类与RandomForestRegressor类的API也相同。
通常来说,很难预先知道一个RandomForestClassifier类是否会比一个ExtraTreesClassifier类更好或是更差。唯一的方法是两种都尝试一遍,然后使用交叉验证(还需要使用网格搜索调整超参数)进行比较。
随机森林的另一个好特性是它们使测量每个特征的相对重要性变得容易。Scikit-Learn通过查看使用该特征的树节点平均(在森林中的所有树上)减少不纯度的程度来衡量该特征的重要性。更准确地说,它是一个加权平均值,其中每个节点的权重等于与其关联的训练样本的数量(见第6章)。
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, random_state=42)
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.11249225099876375
# sepal width (cm) 0.02311928828251033
# petal length (cm) 0.4410304643639577
# petal width (cm) 0.4233579963547682
同样,如果在MNIST数据集上训练随机森林分类器(在第3章中介绍)并绘制每个像素的重要性,则会得到如下图所示的图像。
随机森林非常便于你快速了解哪些特征是真正重要的,特别是在需要执行特性选择时。
提升法(boosting,最初被称为假设提升)是指可以将几个弱学习器结合成一个强学习器的任意集成方法。大多数提升法的总体思路是循环训练预测器,每一次都对其前序做出一些改正。可用的提升法有很多,但目前最流行的方法是AdaBoost(Adaptive Boosting的简称)和梯度提升。我们先从AdaBoost开始介绍。
新预测器对其前序进行纠正的方法之一就是更多地关注前序欠拟合的训练实例,从而使新的预测器不断地越来越专注于难缠的问题,这就是AdaBoost使用的技术。
例如,当训练AdaBoost分类器时,该算法首先训练一个基础分类器(例如决策树),并使用它对训练集进行预测。然后,该算法会增加分类错误的训练实例的相对权重。然后,它使用更新后的权重训练第二个分类器,并再次对训练集进行预测,更新实例权重,以此类推(见下图)。
下图显示了在卫星数据集上5个连续的预测器的决策边界(在本例中,每个预测器都使用RBF核函数的高度正则化的SVM分类器)。第一个分类器产生了许多错误实例,所以这些实例的权重得到提升。因此第二个分类器在这些实例上的表现有所提升,然后第三个、第四个……右图绘制的是相同预测器序列,唯一的差别在于学习率减半(即每次迭代仅提升一半错误分类的实例的权重)。可以看出,AdaBoost这种依序循环的学习技术跟梯度下降有一些异曲同工之处,差别只在于——不再是调整单个预测器的参数使成本函数最小化,而是不断在集成中加入预测器,使模型越来越好。
一旦全部预测器训练完成,集成整体做出预测时就跟bagging或pasting方法一样了,除非预测器有不同的权重,因为它们总的准确率是基于加权后的训练集。
这种依序学习技术有一个重要的缺陷就是无法并行(哪怕只是一部分),因为每个预测器只能在前一个预测器训练完成并评估之后才能开始训练。因此,在扩展方面,它的表现不如bagging和pasting方法。
让我们仔细看看AdaBoost算法。每个实例权重w(i)最初设置为1/m。对第一个预测器进行训练,并根据训练集计算其加权误差率r1。预测器的准确率越高,其权重就越高。如果它只是随机猜测,则其权重接近于零。但是,如果大部分情况下它都是错的(也就是准确率比随机猜测还低),那么它的权重为负。
接下来,AdaBoost算法使用公式更新实例权重,从而提高了误分类实例的权重。
最后,使用更新后的权重训练一个新的预测器,然后重复整个过程(计算新预测器的权重,更新实例权重,然后对另一个预测器进行训练,等等)。当到达所需数量的预测器或得到完美的预测器时,算法停止。
预测的时候,AdaBoost就是简单地计算所有预测器的预测结果,并使用预测器权重αj对它们进行加权。最后,得到大多数加权投票的类就是预测器给出的预测类。
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)
如果你的AdaBoost集成过度拟合训练集,你可以试试减少估算器数量,或是提高基础估算器的正则化程度。
另一个非常受欢迎的提升法是梯度提升。与AdaBoost一样,梯度提升也是逐步在集成中添加预测器,每一个都对其前序做出改正。不同之处在于,它不是像AdaBoost那样在每个迭代中调整实例权重,而是让新的预测器针对前一个预测器的残差进行拟合。
我们来看一个简单的回归示例,使用决策树作为基础预测器(梯度提升当然也适用于回归任务),这被称为梯度树提升(GBDT)或者是梯度提升回归树(GBRT)。
import numpy as np
from sklearn.tree import DecisionTreeRegressor
X_new = np.array([[0.8]])
# 首先,在训练集(比如带噪声的二次训练集)上拟合一个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))
下图的左侧表示这三棵树单独的预测,右侧表示集成的预测。第一行,集成只有一棵树,所以它的预测与第一棵树的预测完全相同。第二行是在第一棵树的残差上训练的一棵新树,从右侧可见,集成的预测等于前面两棵树的预测之和。类似地,第三行又有一棵在第二棵树的残差上训练的新树,集成的预测随着新树的添加逐渐变好。
训练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,则需要更多的树来拟合训练集,但是预测的泛化效果通常更好,这是一种被称为收缩的正则化技术。下图显示了用低学习率训练的两个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
from sklearn.ensemble import GradientBoostingRegressor
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%的随机选择的实例进行训练。现在你可以猜到,这也是用更高的偏差换取了更低的方差,同时在相当大的程度上加速了训练过程。这种技术被称为随机梯度提升。
梯度提升也可以使用其他成本函数,通过超参数loss来控制(有关详细信息,请参阅Scikit-Learn的文档)。
值得注意的是,流行的Python库XGBoost(该库代表ExtremeGradient Boosting)中提供了梯度提升的优化实现,该软件包最初是由Tianqi Chen作为分布式(深度)机器学习社区(DMLC)的一部分开发的,其开发目标是极快、可扩展和可移植。实际上,XGBoost通常是ML竞赛中获胜的重要组成部分。XGBoost的API与Scikit-Learn的非常相似(XGBoost还提供了一些不错的特性,例如自动处理提前停止(early_stopping_rounds=2)):
import xgboost
xgb_reg.fit(X_train, y_train,
eval_set=[(X_val, y_val)], early_stopping_rounds=2)
y_pred = xgb_reg.predict(X_val)
本章我们要讨论的最后一个集成方法叫作堆叠法(stacking),又称层叠泛化法。它基于一个简单的想法:与其使用一些简单的函数(比如硬投票)来聚合集成中所有预测器的预测,我们为什么不训练一个模型来执行这个聚合呢?
下图显示了在新实例上执行回归任务的这样一个集成。底部的三个预测器分别预测了不同的值(3.1、2.7和2.9),然后最终的预测器(称为混合器或元学习器)将这些预测作为输入,进行最终预测(3.0)。
训练混合器的常用方法是使用留存集。我们看看它是如何工作的(见下图)。首先,将训练集分为两个子集,第一个子集用来训练第一层的预测器。
除使用留存集外,也可以使用折外(out-of-fold)预测。在某些情况下,这才被称为堆叠(stacking),而使用留存集被称为混合(blending)。但是对多数人而言,这二者是同义词。
具体而言,在训练时,stacking每个基学习器使用全部训练数据进行训练,blending的各个基学习器使用训练集不相交的子集进行训练,对训练数据利用少了。在融合时,stacking是将基学习器的预测结果输入到下级学习器,属于非线性融合,blending是将不同基分类器按照权重进行线性融合。
然后,用第一层的预测器在第二个(留存)子集上进行预测(见下图)。因为预测器在训练时从未见过这些实例,所以可以确保预测是“干净的”。那么现在对于留存集中的每个实例都有了三个预测值。我们可以使用这些预测值作为输入特征,创建一个新的训练集(新的训练集有三个维度),并保留目标值。在这个新的训练集上训练混合器,让它学习根据第一层的预测来预测目标值。
事实上,通过这种方法可以训练多种不同的混合器(例如,一个使用线性回归,另一个使用随机森林回归,等等)。于是我们可以得到一个混合器层。诀窍在于将训练集分为三个子集:第一个用来训练第一层,第二个用来创造训练第二层的新训练集(使用第一层的预测),而第三个用来创造训练第三层的新训练集(使用第二层的预测)。一旦训练完成,我们可以按照顺序遍历每层来对新实例进行预测,如下图所示。
不幸的是,Scikit-Learn不直接支持堆叠,但是推出自己的实现并不太难(参见接下来的练习题)。或者,你也可以使用开源的实现方案,例如DESlib。
DESlib库相关介绍:DESlib: A Python library for dynamic classifier and ensemble selection、Dynamic Classifier Selection Ensembles in Python、基于Python实现动态分类器集成学习。
四种方法都属于集成学习,只不过侧重的方面/方法策略不同。
bagging/boosting强调抽取数据的策略。两者都采取随机有放回取样(random sampling with replacement)的方式抽取数据,不同的是在bagging中,所有数据被抽到的概率相同;而在boosting中,每一轮被错误标记的数据会被增加权重,从而增加在下一轮学习中被抽到的概率。
而且对于bagging集成来说,将其分布在多个服务器上能够有效加速训练过程,因为集成中的每个预测器都是独立工作的。同理,对于pasting集成和随机森林来说也是如此。但是,boosting集成的每个预测器都是基于其前序的结果,因此训练过程必须是有序的,将其分布在多个服务器上毫无意义。
GBDT所用到的思想就是boosting,XGBoost和LightGBM中都可以使用该策略。
stacking/blending其实是同一个概念,强调集成弱学习器输出的策略。在stacking中,所有弱学习器被称作0级学习器,它们的输出结果紧接着被输入一个1级学习器(又叫做元学习器),然后由元学习器输出最后的预测结果。stacking这个概念的提出,主要是针对voting/weighting/averaging等较为简单的集成策略的,它们在一些较为复杂的问题上表现可能不尽如人意。
对于stacking集成来说,某个指定层的预测器之间彼此独立,因而可以在多台服务器上并行训练,但是,某一层的预测器只能在其前一层的预测器全部训练完成之后才能开始训练。
如果你已经在完全相同的训练集上训练了5个不同的模型,并且它们都达到了95%的准确率,是否还有机会通过结合这些模型来获得更好的结果?如果可以,该怎么做?如果不行,为什么?
硬投票分类器和软投票分类器有什么区别?
是否可以通过在多个服务器上并行来加速bagging集成的训练?pasting集成呢?boosting集成呢?随机森林或stacking集成呢?
包外评估的好处是什么?
是什么让极端随机树比一般随机森林更加随机?这部分增加的随机性有什么用?极端随机树比一般随机森林快还是慢?
如果你的AdaBoost集成对训练数据欠拟合,你应该调整哪些超参数?怎么调整?
如果你的梯度提升集成对训练集过拟合,你是应该提升还是降低学习率?
加载MNIST数据集(第3章中有介绍),将其分为一个训练集、一个验证集和一个测试集(例如,使用50 000个实例训练、10 000个实例验证、10 000个实例测试)。然后训练多个分类器,比如一个随机森林分类器、一个极端随机树分类器和一个SVM分类器。接下来,尝试使用软投票法或者硬投票法将它们组合成一个集成,这个集成在验证集上的表现要胜过它们各自单独的表现。成功找到集成后,在测试集上测试。与单个的分类器相比,它的性能要好多少?
运行练习题8中的单个分类器,用验证集进行预测,然后用预测结果创建一个新的训练集:新训练集中的每个实例都是一个向量,这个向量包含所有分类器对于一张图像的一组预测,目标值是图像的类。恭喜,你成功训练了一个混合器,结合第一层的分类器,它们一起构成了一个stacking集成。现在在测试集上评估这个集成。对于测试集中的每张图像,使用所有的分类器进行预测,然后将预测结果提供给混合器,得到集成的预测。与前面训练的投票分类器相比,这个集成的结果如何?
如果你已经训练了5个不同的模型,并且都达到了95%的精度,则可以尝试将它们组合成一个投票集成,这通常会带来更好的结果。如果模型之间非常不同(例如,一个SVM分类器、一个决策树分类器,以及一个Logistic回归分类器等),则效果更优。如果它们是在不同的训练实例(这是bagging和pasting集成的关键点)上完成训练,那就更好了,但如果不是,只要模型非常不同,这个集成仍然有效。
硬投票分类器只是统计每个分类器的投票,然后挑选出得票最多的类。软投票分类器计算出每个类的平均估算概率,然后选出概率最高的类别。它比硬投票法的表现更优,因为它给予那些高度自信的投票更高的权重。但是它要求每个分类器都能够估算出类别概率才可以正常工作(例如,Scikit-Learn中的SVM分类器必须要设置probability=True)。
对于bagging集成来说,将其分布在多个服务器上能够有效加速训练过程,因为集成中的每个预测器都是独立工作的。同理,对于pasting集成和随机森林来说也是如此。但是,boosting集成的每个预测器都是基于其前序的结果,因此训练过程必须是有序的,将其分布在多个服务器上毫无意义。对于stacking集成来说,某个指定层的预测器之间彼此独立,因而可以在多台服务器上并行训练,但是,某一层的预测器只能在其前一层的预测器全部训练完成之后才能开始训练。
包外评估可以对bagging集成中的每个预测器使用其未经训练的实例(它们是被保留的)进行评估。不需要额外的验证集,就可以对集成实施相当公正的评估。所以,如果训练使用的实例越多,集成的性能可以略有提升。
随机森林在生长过程中,每个节点的分裂仅考虑到了特征的一个随机子集。极限随机树也是如此,它甚至走得更远:常规决策树会搜索出特征的最佳阈值,极端随机树直接对每个特征使用随机阈值。这种极端随机性就像是一种正则化的形式:如果随机森林过拟合训练数据,那么极端随机树可能执行效果更好。而且,由于极端随机树不需要计算最佳阈值,因此它训练起来比随机森林快得多。但是,在做预测的时候,相比随机森林它不快也不慢。
如果你的AdaBoost集成欠拟合训练集,可以尝试提升估算器的数量或是降低基础估算器的正则化超参数。你也可以尝试略微提升学习率。
如果你的梯度提升集成过拟合训练集,你应该试着降低学习率,也可以通过提前停止法来寻找合适的预测器数量(可能是因为预测器太多)。
对于练习8和练习9的解答,参见https://github.com/ageron/handson-ml2的Jupyter notebook。
集成学习方面的主要推荐读物是 【Zhou,2012】。【Kuncheva,2004; Rokach,2010b】可供参考。【Schapire and Freund,2012则是专门关于Boosting的著作。
Boosting 源于【Schapire,1990】对【Kearns and Valiant,1989】提出的"弱学习是否等价于强学习"这个重要理论问题的构造性证明。最初的Boosting算法仅有理论意义,经数年努力后Freund and Schapire,1997提出 AdaBoost,并因此获得理论计算机科学方面的重要奖项——哥德尔奖。不同集成学习方法的工作机理和理论性质往往有显著不同,例如从偏差-方差分解的角度看、Boosting 主要关注降低偏差,而Bagging主要关注降低方差、MultiBoostingWebb,2000】等方法尝试将二者的优点加以结合。关于Boosting和Bagging已有很多理论研究结果,可参阅【Zhou,2012】第2~3 章。
在集成产生之后再试图通过去除一些个体学习器来获得较小的集成、称为集成修剪(ensemble pruning)。这有助于减小模型的存储开销和预测时间开销。早期研究主要针对序列化集成进行、减小集成规模后常导致泛化性能下降【Rokach,2010al】;【Zhou et al,2002】揭示出对并行化集成进行修剪能在减小规模的同时提升泛化性能,并催生了基于优化的集成修剪技术。这方面的内容可参阅 【Zhou,2012】第6章。
关于聚类、半监督学习、代价敏感学习等任务中集成学习的内容,可参阅【Zhou,2012】第7~8章。事实上,集成学习已被广泛用于几乎所有的学习任务。著名数据挖掘竞赛 KDDCup历年的冠军几乎都使用了集成学习。
由于集成包含多个学习器,即便个体学习器有较好的可解释性,集成仍是黑箱模型。已有一些工作试图改善集成的可解释性,例如将集成转化为单模型、从集成中抽取符号规则等,这方面的研究衍生出了能产生性能超越集成的单学习器的"二次学习"(twice-1earning)技术,例如 NeC4.5算法【Zhou andJiang,2004】。可视化技术也对改善可解释性有一定帮助、可参阅 【Zhou,2012】第8章。