看到一篇介绍集成学习的好文,但里头代码有点乱,特意研读整理一下,感谢大神
假设你去随机问很多人一个很复杂的问题,然后把它们的答案合并起来。通常情况下你会发现这个合并的答案比一个专家的答案要好。这就叫做群体智慧。同样的,如果你合并了一组分类器的预测(像分类或者回归),你也会得到一个比单一分类器更好的预测结果。这一组分类器就叫做集成;因此,这个技术就叫做集成学习,一个集成学习算法就叫做集成方法。
例如,你可以训练一组决策树分类器,每一个都在一个随机的训练集上。为了去做预测,你必须得到所有单一树的预测值,然后通过投票(例如第六章的练习)来预测类别。例如一种决策树的集成就叫做随机森林,它除了简单之外也是现今存在的最强大的机器学习算法之一。
向我们在第二章讨论的一样,我们会在一个项目快结束的时候使用集成算法,一旦你建立了一些好的分类器,就把他们合并为一个更好的分类器。事实上,在机器学习竞赛中获得胜利的算法经常会包含一些集成方法。
在本章中我们会讨论一下特别著名的集成方法,包括bagging,boosting,stacking,和其他一些算法。我们也会讨论随机森林。
1 投票分类
假设你已经训练了一些分类器,每一个都有 80% 的准确率。你可能有了一个逻辑斯蒂回归、或一个 SVM、或一个随机森林,或者一个 KNN,或许还有更多(详见图 7-1)
一个非常简单去创建一个更好的分类器的方法就是去整合每一个分类器的预测然后经过投票去预测分类。这种分类器就叫做硬投票分类器(详见图 7-2)。
令人惊奇的是这种投票分类器得出的结果经常会比集成中最好的一个分类器结果更好。事实上,即使每一个分类器都是一个弱学习器(意味着它们也就比瞎猜好点),集成后仍然是一个强学习器(高准确率),只要有足够数量的弱学习者,他们就足够多样化。
这怎么可能?接下来的分析将帮助你解决这个疑问。假设你有一个有偏差的硬币,他有 51% 的几率为正面,49% 的几率为背面。如果你实验 1000 次,你会得到差不多 510 次正面,490 次背面,因此大多数都是正面。如果你用数学计算,你会发现在实验 1000 次后,正面概率为 51% 的人比例为 75%。你实验的次数越多,正面的比例越大(例如你试验了 10000 次,总体比例可能性就会达到 97%)。这是因为大数定律:当你一直用硬币实验时,正面的比例会越来越接近 51%。图 7-3 展示了始终有偏差的硬币实验。你可以看到当实验次数上升时,正面的概率接近于 51%。最终所有 10 种实验都会收敛到 51%,它们都大于 50%。
heads_proba = 0.51
coin_tosses = (np.random.rand(10000, 10) < heads_proba).astype(np.int32)
cumulative_heads_ratio = np.cumsum(coin_tosses, axis=0) / np.arange(1, 10001).reshape(-1, 1)
# law_of_large_numbers_plot
plt.figure(figsize=(8,3.5))
plt.plot(cumulative_heads_ratio)
plt.plot([0, 10000], [0.51, 0.51],"k--", linewidth=2, label="51%")
plt.plot([0, 10000], [0.5, 0.5],"k-", label="50%")
plt.xlabel("Number of coin tosses")
plt.ylabel("Heads ratio")
plt.legend(loc="lower right")
plt.axis([0, 10000, 0.42, 0.58])
plt.show()
同样的,假设你创建了一个包含 1000 个分类器的集成模型,
其中每个分类器的正确率只有 51%(仅比瞎猜好一点点)。如果你用投票去预测类别,你可能得到 75% 的准确率!
然而,这仅仅在所有的分类器都独立运行的很好、不会发生有相关性的错误的情况下才会这样,然而每一个分类器都在同一个数据集上训练,导致其很可能会发生这样的错误。
他们可能会犯同一种错误,所以也会有很多票投给了错误类别导致集成的准确率下降。
如果使每一个分类器都独立自主的分类,那么集成模型会工作的很好。去得到多样的分类器的方法之一就是用完全不同的算法,这会使它们会做出不同种类的错误,这会提高集成的正确率
接下来的代码创建和训练了在sklearn中的投票分类器。这个分类器由三个不同的分类器组成:
硬分类
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_moons
import warnings
warnings.filterwarnings('ignore')
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(random_state=42)
rnd_clf = RandomForestClassifier(random_state=42)
svm_clf = SVC(random_state=42)
voting_clf = VotingClassifier(
estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
voting='hard')
plt.scatter(X[:, 0], X[:,1], c=y)
plt.show()
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.872
#SVC 0.888
#VotingClassifier 0.896
你看!投票分类器比其他单独的分类器表现的都要好。
软分类
# 软投票
log_clf = LogisticRegression(random_state=42)
rnd_clf = RandomForestClassifier(random_state=42)
svm_clf = SVC(probability=True, random_state=42)
voting_clf = VotingClassifier(
estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
voting='soft')
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.872
#SVC 0.888
#VotingClassifier 0.912
如果所有的分类器都能够预测类别的概率(例如他们有一个predict_proba()方法),那么你就可以让sklearn以最高的类概率来预测这个类,平均在所有的分类器上。这种方式叫做软投票。
他经常比硬投票表现的更好,因为它给予高自信的投票更大的权重。你可以通过把voting="hard"设置为voting="soft"来保证分类器可以预测类别概率。然而这不是 SVC 类的分类器默认的选项,所以你需要把它的probability hyperparameter设置为True(这会使SVC使用交叉验证去预测类别概率,其降低了训练速度,但会添加predict_proba()方法)。如果你修改了之前的代码去使用软投票,你会发现投票分类器正确率高达91%
Bagging & Pasting
换句话说,Bagging和Pasting都允许在多个分类器间对训练集进行多次采样。
Bagging可以通过使用不同的训练算法去得到一些不同的分类器。
Pasting就是对每一个分类器都使用相同的训练算法,但是在不同的训练集上去训练它们。
有放回采样被称为装袋(Bagging,是bootstrap aggregating的缩写)。
无放回采样称为粘贴(pasting)。
换句话说,Bagging和Pasting都允许在多个分类器上对训练集进行多次采样,但只有Bagging允许对同一种分类器上对训练集进行进行多次采样。采样和训练过程如图7-4所示。
当所有的分类器被训练后,集成可以通过对所有分类器结果的简单聚合来对新的实例进行预测。聚合函数通常对分类是统计模式(例如硬投票分类器)或者对回归是平均。每一个单独的分类器在如果在原始训练集上都是高偏差,但是聚合降低了偏差和方差。通常情况下,集成的结果是有一个相似的偏差,但是对比与在原始训练集上的单一分类器来讲有更小的方差。
正如你在图 7-4 上所看到的,分类器可以通过不同的CPU 核或其他的服务器一起被训练。相似的,分类器也可以一起被制作。这就是为什么 Bagging 和 Pasting 是如此流行的原因之一:它们的可扩展性很好。
sklearn为Bagging和Pasting提供了一个简单的API:BaggingClassifier类(或者对于回归可以是BaggingRegressor。接下来的代码训练了一个500个决策树分类器的集成,每一个都是在数据集上有放回采样100个训练实例下进行训练(这是Bagging的例子,如果你想尝试Pasting,就设置bootstrap=False)。n_jobs参数告诉sklearn用于训练和预测所需要CPU核的数量。(-1代表着sklearn会使用所有空闲核):
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, random_state=42
)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)
print(accuracy_score(y_test, y_pred))
#0.904
plot_clf(X_train, y_train, bag_clf)
tree_clf = DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train, y_train)
y_pred = tree_clf.predict(X_test)
print(accuracy_score(y_test, y_pred))
#0.856
plot_clf(X_train, y_train, tree_clf)
这个plot.clf(xxxxx) function简单写了一下,不适用与所有情况哈,见谅见谅。不过很好在这个基础上改哈。逻辑上明白就好
def plot_clf(X, y, *args):
li = []
title_list = []
for clf in args:
li.append(clf)
title_list.append(clf.__class__.__name__)
x1_min, x1_max = X[:, 0].min()-1, X[:, 0].max()+1
x2_min, x2_max = X[:, 1].min()-1, X[:, 1].max()+1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.01), np.arange(x2_min, x2_max, 0.01))
if len(li) == 1:
fig, axes = plt.subplots()
for clf, title in zip(
li,
title_list
):
z = clf.predict(np.c_[xx1.ravel(), xx2.ravel()])
z = z.reshape(xx1.shape)
axes.contourf(xx1, xx2, z, alpha=0.4)
axes.scatter(X[:, 0], X[:, 1], c=y, s=20)
axes.set_title(title)
else:
for i in range(1, len(li)+1):
print(i)
plt.subplot(2, 2, i)
for clf, title in zip(
li,
title_list
):
z = clf.predict(np.c_[xx1.ravel(), xx2.ravel()])
z = z.reshape(xx1.shape)
plt.contourf(xx1, xx2, z, alpha=0.4)
plt.scatter(X[:, 0], X[:, 1], c=y, s=20)
plt.title(title)
plt.show()
如果基分类器可以预测类别概率(例如它拥有predict_proba()方法),那么BaggingClassifier会自动的运行软投票,这是决策树分类器的情况。
图对比了单一决策树的决策边界和Bagging集成500个树的决策边界,两者都在moons数据集上训练。正如你所看到的,集成的分类比起单一决策树的分类产生情况更好:集成有一个可比较的偏差但是有一个较小的方差(它在训练集上的错误数目大致相同,但决策边界较不规则)。
Bootstrap在每个预测器被训练的子集中引入了更多的分集,所以Bagging结束时的偏差比Pasting更高,但这也意味着预测因子最终变得不相关,从而减少了集合的方差。总体而言,Bagging通常会导致更好的模型,这就解释了为什么它通常是首选的。然而,如果你有空闲时间和CPU功率,可以使用交叉验证来评估Bagging和Pasting哪一个更好。
Out-of-Bag
对于Bagging来说,一些实例可能被一些分类器重复采样,但其他的有可能不会被采样。BaggingClassifier默认采样。BaggingClassifier默认是有放回的采样m个实例 (bootstrap=True),其中m是训练集的大小,这意味着平均下来只有63%的训练实例被每个分类器采样,剩下的37%个没有被采样的训练实例就叫做Out-of-Bag实例。注意对于每一个的分类器它们的37%不是相同的。
因为在训练中分类器从来没有看到过oob实例,所以它可以在这些实例上进行评估,而不需要单独的验证集或交叉验证。你可以拿出每一个分类器的oob来评估集成本身。
在sklearn中,你可以在训练后需要创建一个BaggingClassifier来自动评估时设置oob_score=True来自动评估。接下来的代码展示了这个操作。评估结果通过变量oob_score_来显示:
bag_clf = BaggingClassifier(
DecisionTreeClassifier(random_state=42), n_estimators=500,
bootstrap=True, n_jobs=-1, oob_score=True, random_state=40)
bag_clf.fit(X_train, y_train)
print(bag_clf.oob_score_)
#0.9013333333333333
bag_clf.predict(X_test)
print(accuracy_score(y_test, y_pred)) #0.904
对于每个训练实例oob决策函数也可通过oob_decision_function_变量来展示。在这种情况下(当基决策器有predict_proba()时)决策函数会对每个训练实例返回类别概率。例如,oob评估预测第二个训练实例有60.6%的概率属于正类(39.4%属于负类):
随机贴片与随机子空间
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)。接下来的代码训练了带有500个树(每个被限制为16叶子结点)的决策森林,使用所有空闲的 CPU 核:
from sklearn.ensemble import RandomForestClassifier
rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1, random_state=42)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
print(accuracy_score(y_test, y_pred))
plot_clf(X_train, y_train, rnd_clf)
除了一些例外,RandomForestClassifier使用DecisionTreeClassifier的所有超参数(决定数怎么生长),把BaggingClassifier的超参数加起来来控制集成本身。
随机森林算法在树生长时引入了额外的随机;与在节点分裂时需要找到最好分裂特征相反(详见第六章),它在一个随机的特征集中找最好的特征。它导致了树的差异性,并且再一次用高偏差换低方差,总的来说是一个更好的模型。以下是BaggingClassifier大致相当于之前的randomforestclassifier:
极端随机树
当你在随机森林上生长树时,在每个结点分裂时只考虑随机特征集上的特征(正如之前讨论过的一样)。相比于找到更好的特征我们可以通过使用对特征使用随机阈值使树更加随机(像规则决策树一样)。
这种极端随机的树被简称为Extremely Randomized Trees(极端随机树),或者更简单的称为Extra-Tree。再一次用高偏差换低方差。它还使得Extra-Tree比规则的随机森林更快地训练,因为在每个节点上找到每个特征的最佳阈值是生长树最耗时的任务之一。
你可以使用sklearn的ExtraTreesClassifier来创建一个Extra-Tree分类器。他的API跟RandomForestClassifier是相同的,相似的,ExtraTreesRegressor跟RandomForestRegressor也是相同的 API。
我们很难去分辨ExtraTreesClassifier和RandomForestClassifier到底哪个更好。通常情况下是通过交叉验证来比较它们(使用网格搜索调整超参数)。
特征重要度
最后,如果你观察一个单一决策树,重要的特征会出现在更靠近根部的位置,而不重要的特征会经常出现在靠近叶子的位置。因此我们可以通过计算一个特征在森林的全部树中出现的平均深度来预测特征的重要性。sklearn 在训练后会自动计算每个特征的重要度。你可以通过feature_importances_变量来查看结果。例如如下代码在 iris 数据集(第四章介绍)上训练了一个RandomForestClassifier模型,然后输出了每个特征的重要性。看来,最重要的特征是花瓣长度(44%)和宽度(42%),而萼片长度和宽度相对比较是不重要的(分别为 11% 和 2%):
随机森林可以非常方便快速得了解哪些特征实际上是重要的,特别是你需要进行特征选择的时候。
Boosting
提升(Boosting,最初称为假设增强)指的是可以将几个弱学习者组合成强学习者的集成方法。对于大多数的提升方法的思想就是按顺序去训练分类器,每一个都要尝试修正前面的分类。现如今已经有很多的提升方法了,但最著名的就是Adaboost(适应性提升,是Adaptive Boosting的简称) 和Gradient Boosting(梯度提升)。让我们先从Adaboost说起。
Adaboost
使一个新的分类器去修正之前分类结果的方法就是对之前分类结果不对的训练实例多加关注。这导致新的预测因子越来越多地聚焦于这种情况。这是Adaboost使用的技术。
举个例子,去构建一个Adaboost分类器,
第一个基分类器(例如一个决策树)被训练然后在训练集上做预测,在误分类训练实例上的权重就增加了;
第二个分类机使用更新过的权重然后再一次训练,权重更新,以此类推(详见图 7-7)
图 7-8 显示连续五次预测的moons数据集的决策边界(在本例中,每一个分类器都是高度正则化带有RBF核的SVM)。
第一个分类器误分类了很多实例,所以它们的权重被提升了。
第二个分类器因此对这些误分类的实例分类效果更好,以此类推。
右边的图代表了除了学习率减半外(误分类实例权重每次迭代上升一半)相同的预测序列。
你可以看出,序列学习技术与梯度下降很相似,除了调整单个预测因子的参数以最小化代价函数之外,AdaBoost 增加了集合的预测器,逐渐使其更好。
一旦所有的分类器都被训练后,除了分类器根据整个训练集上的准确率被赋予的权重外,集成预测就非常像Bagging和Pasting了。
序列学习技术的一个重要的缺点就是:它不能被并行化(只能按步骤),因为每个分类器只能在之前的分类器已经被训练和评价后再进行训练。因此,它不像Bagging和Pasting一样。
让我们详细看一下Adaboost算法。每一个实例的权重wi初始都被设为1/m第一个分类器被训练,然后他的权重误差率r1在训练集上算出,详见公式 7-1。
sklearn通常使用Adaboost的多分类版本SAMME(这就代表了分段加建模使用多类指数损失函数)。如果只有两类别,那么SAMME是与Adaboost相同的。如果分类器可以预测类别概率(例如如果它们有predict_proba()),如果sklearn可以使用SAMME叫做SAMME.R的变量(R 代表“REAL”),这种依赖于类别概率的通常比依赖于分类器的更好。
接下来的代码训练了使用sklearn的AdaBoostClassifier基于200个决策树桩Adaboost分类器(正如你说期待的,对于回归也有AdaBoostRegressor)。一个决策树桩是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, random_state=42)
ada_clf.fit(X_train, y_train)
y_pred = ada_clf.predict(X_test)
print(np.sum(y_pred == y_pred_rf) / len(y_pred))
plot_clf(X_train, y_train, ada_clf)
如果你的Adaboost集成过拟合了训练集,你可以尝试减少基分类器的数量或者对基分类器使用更强的正则化。
梯度提升
另一个非常著名的提升算法是梯度提升。与Adaboost一样,梯度提升也是通过向集成中逐步增加分类器运行的,每一个分类器都修正之前的分类结果。然而,它并不像Adaboost那样每一次迭代都更改实例的权重,这个方法是去使用新的分类器去拟合前面分类器预测的残差。
让我们通过一个使用决策树当做基分类器的简单的回归例子(回归当然也可以使用梯度提升)。这被叫做梯度提升回归树(GBRT,Gradient Tree Boosting或者Gradient Boosted Regression Trees)。
首先我们用DecisionTreeRegressor去拟合训练集(例如一个有噪二次训练集):
# 转换
X = X_raw.reshape(-1, 1)
#首先我们用DecisionTreeRegressor去拟合训练集(例如一个有噪二次训练集):
from sklearn.tree import DecisionTreeRegressor
tree_reg1 = DecisionTreeRegressor(max_depth=2)
tree_reg1.fit(X, y)
#现在在第一个分类器的残差上训练第二个分类器:
y2 = y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg2.fit(X, y2)
#随后在第二个分类器的残差上训练第三个分类器:
y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg3.fit(X, y3)
#现在我们有了一个包含三个回归器的集成。它可以通过集成所有树的预测来在一个新的实例上进行预测。
X_new = np.array([[0.8]])
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
y_pred
# gradient_boosting_plot
def plot_predictions(regressors, X, y, axes, label=None, style="r-", data_style="b.", data_label=None):
x1 = np.linspace(axes[0], axes[1], 500)
y_pred = sum(regressor.predict(x1.reshape(-1, 1)) for regressor in regressors)
plt.plot(X[:, 0], y, data_style, label=data_label)
plt.plot(x1, y_pred, style, linewidth=2, label=label)
plt.legend(loc="upper center", fontsize=16)
plt.axis(axes)
图7-9在左栏展示了这三个树的预测,在右栏展示了集成的预测。
在第一行,集成只有一个树,所以它与第一个树的预测相似。
在第二行,一个新的树在第一个树的残差上进行训练。在右边栏可以看出集成的预测等于前两个树预测的和。
相同的,在第三行另一个树在第二个数的残差上训练。你可以看到集成的预测会变的更好。
plt.figure(figsize=(15, 15))
plt.subplot(321)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h_1(x_1)$", style="g-", data_label="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Residuals and tree predictions", fontsize=16)
plt.subplot(322)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1)$", data_label="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Ensemble predictions", fontsize=16)
plt.subplot(323)
plot_predictions([tree_reg2], X, y2, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_2(x_1)$", style="g-", data_style="k+", data_label="Residuals")
plt.ylabel("$y- h_1(x_1)$", fontsize=16)
plt.subplot(324)
plot_predictions([tree_reg1, tree_reg2], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1)$")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.subplot(325)
plot_predictions([tree_reg3], X, y3, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_3(x_1)$", style="g-", data_style="k+")
plt.ylabel("$y- h_1(x_1) - h_2(x_1)$", fontsize=16)
plt.xlabel("$x_1$", fontsize=16)
plt.subplot(326)
plot_predictions([tree_reg1, tree_reg2, tree_reg3], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1) + h_3(x_1)$")
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.show()
我们可以使用sklean中的GradientBoostingRegressor来训练GBRT集成。与RandomForestClassifier相似,它也有超参数去控制决策树的生长(例如max_depth,min_samples_leaf等等),也有超参数去控制集成训练,例如基分类器的数量(n_estimators)。接下来的代码创建了与之前相同的集成:
from sklearn.ensemble import GradientBoostingRegressor
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0, random_state=42)
gbrt.fit(X, y)
gbrt_slow = GradientBoostingRegressor(max_depth=2, n_estimators=200, learning_rate=0.1, random_state=42)
gbrt_slow.fit(X, y)
# gbrt_learning_rate_plot
plt.figure(figsize=(11,4))
plt.subplot(121)
plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("learning_rate={}, n_estimators={}".format(gbrt.learning_rate, gbrt.n_estimators), fontsize=14)
plt.subplot(122)
plot_predictions([gbrt_slow], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow.learning_rate, gbrt_slow.n_estimators), fontsize=14)
plt.show()
超参数learning_rate确立了每个树的贡献。如果你把它设置为一个很小的树,例如0.1,在集成中就需要更多的树去拟合训练集,但预测通常会更好。这个正则化技术叫做shrinkage。
图 7-10 展示了两个在低学习率上训练的GBRT集成:其中左面是一个没有足够树去拟合训练集的树,右面是有过多的树过拟合训练集的树。
早停技术
为了找到树的最优数量,你可以使用早停技术(第四章讨论)。
最简单使用这个技术的方法就是使用staged_predict():
它在训练的每个阶段(用一棵树,两棵树等)返回一个迭代器。加下来的代码用120个树训练了一个GBRT集成,然后在训练的每个阶段验证错误以找到树的最佳数量,最后使用GBRT树的最优数量训练另一个集成:
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, random_state=49)
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=200, random_state=42)
gbrt.fit(X_train, y_train)
errors = [mean_squared_error(y_val, y_pred) for y_pred in gbrt.staged_predict(X_val)] # len(errors)=200
bst_n_estimators = np.argmin(errors) # =108
# 然后用108这个再跑一次!
gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators, random_state=42)
gbrt_best.fit(X_train, y_train)
min_error = np.min(errors)
# early_stopping_gbrt_plot
plt.figure(figsize=(11,4))
plt.subplot(121)
plt.plot(errors,"b.-")
plt.plot([bst_n_estimators, bst_n_estimators], [0, min_error],"k--")
plt.plot([0,200], [min_error, min_error],"k--")
plt.plot(bst_n_estimators, min_error,"ko")
plt.text(bst_n_estimators, min_error*1.2,"Minimum", ha="center", fontsize=14)
plt.axis([0,200,0,0.01])
plt.xlabel("Number of trees")
plt.title("Validation error", fontsize=14)
plt.subplot(122)
plot_predictions([gbrt_best], X, y, axes=[-0.5,0.5,-0.1,0.8])
plt.title("Best model (%d trees)"% bst_n_estimators, fontsize=14)
plt.show()
你也可以早早的停止训练来实现早停(与先在一大堆树中训练,然后再回头去找最优数目相反)。你可以通过设置warm_start=True来实现 ,这使得当fit()方法被调用时sklearn保留现有树,并允许增量训练。接下来的代码在当一行中的五次迭代验证错误没有改善时会停止训练:
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True, random_state=42)
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
print(gbrt.n_estimators) #84
print("Minimum validation MSE:", min_val_error) #M inimum validation MSE: 0.004607001304239584
GradientBoostingRegressor也支持指定用于训练每棵树的训练实例比例的超参数subsample。例如如果subsample=0.25,那么每个树都会在25%随机选择的训练实例上训练。你现在也能猜出来,这也是个高偏差换低方差的作用。它同样也加速了训练。这个技术叫做随机梯度提升。
也可能对其他损失函数使用梯度提升。这是由损失超参数控制(见sklearn文档)。
Stacking
本章讨论的最后一个集成方法叫做Stacking(stacked generalization的缩写)。这个算法基于一个简单的想法:不使用琐碎的函数(如硬投票)来聚合集合中所有分类器的预测,我们为什么不训练一个模型来执行这个聚合?
图 7-12 展示了这样一个在新的回归实例上预测的集成。底部三个分类器每一个都有不同的值(3.1,2.7 和 2.9),然后最后一个分类器(叫做blender或者meta learner)把这三个分类器的结果当做输入然后做出最终决策(3.0)。
为了训练这个blender,一个通用的方法是采用保持集。让我们看看它怎么工作。
首先,训练集被分为两个子集,第一个子集被用作训练第一层(详见图 7-13).
接下来,第一层的分类器被用来预测第二个子集(保持集)(详见 7-14)。这确保了预测结果很“干净”,因为这些分类器在训练的时候没有使用过这些事例。现在对在保持集中的每一个实例都有三个预测值。我们现在可以使用这些预测结果作为输入特征来创建一个新的训练集(这使得这个训练集是三维的),并且保持目标数值不变。随后blender在这个新的训练集上训练,因此,它学会了预测第一层预测的目标值。
显然我们可以用这种方法训练不同的blender(例如一个线性回归,另一个是随机森林等等):我们得到了一层blender。诀窍是将训练集分成三个子集:第一个子集用来训练第一层,第二个子集用来创建训练第二层的训练集(使用第一层分类器的预测值),第三个子集被用来创建训练第三层的训练集(使用第二层分类器的预测值)。以上步骤做完了,我们可以通过逐个遍历每个层来预测一个新的实例。详见图 7-15.
然而不幸的是,sklearn并不直接支持stacking,但是你自己组建是很容易的(看接下来的练习)。或者你也可以使用开源的项目例如brew(网址为https://github.com/viisar/brew)
安装brew
pip install git+https://github.com/viisar/brew.git
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import itertools
import sklearn
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from brew.base import Ensemble, EnsembleClassifier
from brew.stacking.stacker import EnsembleStack, EnsembleStackClassifier
from brew.combination.combiner import Combiner
from mlxtend.data import iris_data
from mlxtend.plotting import plot_decision_regions
# Initializing
Classifiersclf1 = LogisticRegression(random_state=0)
clf2 = RandomForestClassifier(random_state=0)
clf3 = SVC(random_state=0, probability=True)
# Creating Ensemble
ensemble = Ensemble([clf1, clf2, clf3])
eclf = EnsembleClassifier(ensemble=ensemble, combiner=Combiner('mean'))
# Creating Stacking
layer_1 = Ensemble([clf1, clf2, clf3])
layer_2 = Ensemble([sklearn.clone(clf1)])
stack = EnsembleStack(cv=3)
stack.add_layer(layer_1)
stack.add_layer(layer_2)
sclf = EnsembleStackClassifier(stack)
clf_list = [clf1, clf2, clf3, eclf, sclf]
lbl_list = ['Logistic Regression','Random Forest','RBF kernel SVM','Ensemble','Stacking']
# Loading some example data
X, y = iris_data()
X = X[:,[0,2]]
# WARNING, WARNING, WARNING
# brew requires classes from 0 to N, no skipping allowed
d = {yi : i for i, yi in enumerate(set(y))}
y = np.array([d[yi] for yi in y])
# Plotting Decision
Regionsgs = gridspec.GridSpec(2,3)
fig = plt.figure(figsize=(10,8))
itt = itertools.product([0,1,2], repeat=2)
for clf, lab, grd in zip(clf_list, lbl_list, itt):
clf.fit(X, y)
ax = plt.subplot(gs[grd[0], grd[1]])
fig = plot_decision_regions(X=X, y=y, clf=clf, legend=2)
plt.title(lab)
plt.show()