动手学机器学习——随机森林和集成学习(附 sklearn 实战及详细讲解)

发布于:2025-05-01 ⋅ 阅读:(52) ⋅ 点赞:(0)

文章目录

  • 前言
  • 概念
    • 投票分类器
    • bagging和pasting
      • 随机补丁和随机子空间
    • 随机森林
    • 提升法
      • AdaBoost
      • XGBoost
    • 堆叠法(stacking)
  • 总结
  • 写在文末

前言

本期将介绍集成学习和随机森林,随机森林是集成学习方法中的一种。我们可以看到在一些机器学习竞赛中最终获胜的解决方案,往往是涉及多种集成方法。相信大家都听过“三个臭皮匠,赛过诸葛亮”这个谚语吧,这就是群体智慧的力量。接下来我们将具体进行介绍。

概念

集成学习是一种通过组合多个弱学习器(如决策树、逻辑回归等)来构建一个性能更优的强学习器的机器学习方法。

集成学习的主要类型包括Boosting、Bagging和Pasting以及Stacking。
1 Boosting(提升法)

  • 原理:通过迭代训练弱学习器,逐步调整样本权重(或数据分布),让后续模型更关注前序模型预测错误的样本,最终通过加权投票或加权平均整合结果。
  • 代表算法:AdaBoost、GBDT、XGBoost、LightGBM 等。

2 Bagging(自助聚合)(Pasting和Bagging区别在于采样后不放回)

  • 原理:从原始数据中通过自助采样(Bootstrap)生成多个子集,每个子集独立训练一个弱学习器,最终通过简单投票(分类)或平均(回归)整合结果。
  • 特点:降低模型方差,适合处理过拟合问题。
  • 代表算法:随机森林(Random Forest)。

3 Stacking(堆叠法)

  • 原理:使用第一层模型的输出作为第二层模型的输入,通过训练一个元模型来整合第一层的预测结果。
  • 特点:灵活性高,但需注意过拟合风险。

接下来我们先介绍一下投票分类器。

投票分类器

假设现在我们已经训练好了一些分类器,每个分类器的准确率约为80%。大概包括一个逻辑回归分类器、一个SVM分类器、一个随机森林分类器、一个K-近邻分类器,或许还有更多,这时候,要创建出一个更好的分类器,最简单的办法就是聚合每个分类器的预测,然后将得票最多的结果作为预测类别,这种大多数投票分类器被称为硬投票分类器。如果所有分类器都能够估算出类别的概率,那么你可以将概率在所有单个分类器上平均,然后让Scikit-Learn给出平均概率最高的类别作为预测。这被称为软投票法
注意:当预测器尽可能互相独立时,集成方法的效果最优。
在这里插入图片描述
我们看一下具体的代码实现:

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

我们可以看到投票分类器略胜于所有单个分类器。

bagging和pasting

每个预测器使用的算法相同,但是在不同的训练集随机子集上进行训练。采样时如果将样本放回,这种方法叫作bagging(bootstrap aggregating的缩写,也叫自举汇聚法)。采样时样本不放回,这种方法则叫作pasting。换句话说,bagging和pasting都允许训练实例在多个预测器中被多次采样,但是只有bagging允许训练实例被同一个预测器多次采样。
在这里插入图片描述
代码实现:

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)
# 包外评估(利用没有采样到的训练实例进行评估,oob_score=True)
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_
#可以通过bag_clf.oob_score_得到最终的评估分数
#0.90133333333333332

Scikit-Learn提供了一个简单的API,可用BaggingClassifier类进行bagging和pasting(或BaggingRegressor用于回归)​。以下代码训练了一个包含500个决策树分类器的集成,每次从训练集中随机采样100个训练实例进行训练,然后放回(这是一个bagging的示例,如果你想使用pasting,只需要设置bootstrap=False即可)​。参数n_jobs用来指示Scikit-Learn用多少CPU内核进行训练和预测。我们看一下效果对比:
在这里插入图片描述

随机补丁和随机子空间

同时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)

随机森林在树的生长上引入了更多的随机性:分裂节点时不再是搜索最好的特征​,而是在一个随机生成的特征子集里搜索最好的特征。这导致决策树具有更大的多样性,​(再一次)用更高的偏差换取更低的方差,总之,还是产生了一个整体性能更优的模型。
下面的BaggingClassifier大致与前面的RandomForestClassifier相同:

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(splitter="random", max_leaf_nodes=16),
    n_estimators=500, max_samples=1.0, bootstrap=True, n_jobs=-1)
  • 由于在随机森林里单棵树的生长过程中,每个节点在分裂时仅考虑到了一个随机子集所包含的特征。如果我们对每个特征使用随机阈值,而不是搜索得出的最佳阈值(如常规决策树)​,则可能让决策树生长得更加随机。这种极端随机的决策树组成的森林称为极端随机树集成[(或简称Extra-Trees)​。同样,它也是以更高的偏差换取了更低的方差。极端随机树训练起来比常规随机森林要快很多,因为在每个节点上找到每个特征的最佳阈值是决策树生长中最耗时的任务之一。使用Scikit-Learn的ExtraTreesClassifier类可以创建一个极端随机树分类器。它的API与RandomForestClassifier类相同。同理,ExtraTreesRegressor类与RandomForestRegressor类的API也相同。
  • 随机森林的另一个好特性是它们使测量每个特征的相对重要性变得容易。Scikit-Learn通过查看使用该特征的树节点平均(在森林中的所有树上)减少不纯度的程度来衡量该特征的重要性。更准确地说,它是一个加权平均值,其中每个节点的权重等于与其关联的训练样本的数量​。Scikit-Learn会在训练后为每个特征自动计算该分数,然后对结果进行缩放以使所有重要性的总和等于1。我没可以通过使用feature_importances_变量来访问结果。
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(Adaptive Boosting的简称)和XgBoost。我们先从AdaBoost开始介绍。

AdaBoost

AdaBoost使用的技术主要是新预测器对其前序进行纠正的方法之一就是更多地关注前序欠拟合的训练实例,从而使新的预测器不断地越来越专注于难缠的问题。例如,当训练AdaBoost分类器时,该算法首先训练一个基础分类器(例如决策树)​,并使用它对训练集进行预测。然后,该算法会增加分类错误的训练实例的相对权重。然后,它使用更新后的权重训练第二个分类器,并再次对训练集进行预测,更新实例权重,以此类推。一旦全部预测器训练完成,集成整体做出预测时就跟bagging或pasting方法一样了,除非预测器有不同的权重,因为它们总的准确率是基于加权后的训练集。
在这里插入图片描述
每次迭代后,增大前序模型分类错误样本的权重,减小正确分类样本的权重,迫使后续模型重点关注 “难分样本”。
同时不同的预测器有不同的权重,每个实例权重w(i)最初设置为1/m,对第一个预测器进行训练,并根据训练集计算其加权误差率r1。第j个预测器的加权误差率:
在这里插入图片描述
预测器的准确率越高,其权重就越高。如果它只是随机猜测,则其权重接近于零。但是,如果大部分情况下它都是错的(也就是准确率比随机猜测还低)​,那么它的权重为负。权重计算公式:
在这里插入图片描述
根据弱分类器的分类准确率分配权重,准确率越高的弱分类器在最终集成中的权重越大。
预测的时候,AdaBoost就是简单地计算所有预测器的预测结果,并使用预测器权重αj对它们进行加权。最后,得到大多数加权投票的类就是预测器给出的预测类
在这里插入图片描述
其中N是预测器的数量,接下来我们看一下代码实现:

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)

XGBoost

与AdaBoost类似,(XGBoost)梯度提升也是逐步在集成中添加预测器,每一个都对其前序做出改正。不同之处在于,它不是像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))

训练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,则需要更多的树来拟合训练集,但是预测的泛化效果通常更好,这是一种被称为收缩的正则化技术。
  • 要找到树的最佳数量,可以使用提前停止法。
    此外,流行的Python库XGBoost(该库代表Extreme Gradient Boosting)中提供了梯度提升的优化实现,该软件包最初是由Tianqi Chen作为分布式(深度)机器学习社区(DMLC)的一部分开发的,其开发目标是极快、可扩展和可移植。实际上,XGBoost通常是ML竞赛中获胜的重要组成部分。XGBoost的API与Scikit-Learn的非常相似:
import xgboost

xgb_reg = xgboost.XGBRegressor()
xgb_reg.fit(X_train, y_train)
y_pred = xgb_reg.predict(X_val)
#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)

最后一个集成方法叫作堆叠法(stacking)​,又称层叠泛化法。它基于一个简单的想法:与其使用一些简单的函数(比如硬投票)来聚合集成中所有预测器的预测,我们为什么不训练一个模型来执行这个聚合呢?如图底部的三个预测器分别预测了不同的值(3.1、2.7和2.9)​,然后最终的预测器(称为混合器或元学习器)将这些预测作为输入,进行最终预测。
在这里插入图片描述
训练混合器的常用方法是使用留存集。我们看看它是如何工作的。首先,将训练集分为两个子集,第一个子集用来训练第一层的预测器。然后,用第一层的预测器在第二个(留存)子集上进行预测​。因为预测器在训练时从未见过这些实例,所以可以确保预测是“干净的”​。那么现在对于留存集中的每个实例都有了三个预测值。我们可以使用这些预测值作为输入特征,创建一个新的训练集(新的训练集有三个维度)​,并保留目标值。在这个新的训练集上训练混合器,让它学习根据第一层的预测来预测目标值。
注意:如果直接用第一层模型在原始训练集上的预测值作为混合器的输入特征,会导致数据泄露—— 因为基模型在训练集上的预测可能过拟合(比如记住了训练数据的噪声),混合器会错误地依赖这些 “虚假准确” 的预测,导致泛化能力下降。所以我们用未被基模型训练过的留存集生成 “干净的预测值”,确保混合器学到的是基模型的泛化能力,而非训练集上的过拟合结果。
训练第一层
在这里插入图片描述
训练混合器
在这里插入图片描述

事实上,通过这种方法可以训练多种不同的混合器(例如,一个使用线性回归,另一个使用随机森林回归,等等)​。于是我们可以得到一个混合器层。诀窍在于将训练集分为三个子集:第一个用来训练第一层,第二个用来创造训练第二层的新训练集(使用第一层的预测)​,而第三个用来创造训练第三层的新训练集(使用第二层的预测)​。一旦训练完成,我们可以按照顺序遍历每层来对新实例进行预测。

在这里插入图片描述

总结

本期介绍了随机森林和集成学习及其代码实现。

写在文末

 有疑问的友友,欢迎在评论区交流,笔者看到会及时回复

请大家一定一定要关注!!!
请大家一定一定要关注!!!
请大家一定一定要关注!!!
友友们,你们的支持是我持续更新的动力~

创作不易,求关注,点赞,收藏,谢谢~

网站公告

今日签到

点亮在社区的每一天
去签到