机器学习笔记十七之集成学习、随机森林

146 阅读17分钟
原文链接: www.devtalking.com

到目前为止,我们已经学习了大概有八种机器学习的算法,其中有解决分类问题的,有解决回归问题的。这些算法其实没有谁是最好的,谁不好之说,反而应该将这些算法集合起来,发挥他们的最大价值。比如我们买东西或看电影之前,多少都会咨询身边的朋友,或去网上看看买家的评价,然后我们才会根据口碑好坏,或评价好坏决定买还是不买,看还是不看。在机器学习中,同样有这样的思路,这就是重要的集成学习。

集成学习

机器学习中的集成学习就是将选择若干算法,针对同一样本数据训练模型,然后看看结果,使用投票机制,少数服从多数,用多数算法给出的结果当作最终的决策依据,这就是集成学习的核心思路。下面我们先手动模拟一个使用集成学习解决回归问题的的示例:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

# 构建500个点的样本数据
X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)

# 绘制样本数据
plt.scatter(X[y==0, 0], X[y==0, 1])
plt.scatter(X[y==1, 0], X[y==1, 1])
plt.show()

分别使用逻辑回归、SVM、决策树针对上面的样本数据训练模型:

# 拆分样本数据
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)

# 使用逻辑回归
from sklearn.linear_model import LogisticRegression
log_clf = LogisticRegression()
log_clf.fit(X_train, y_train)
log_clf.score(X_test, y_test)

# 结果
0.872

# 使用SVM
from sklearn.svm import SVC
svc_clf = SVC()
svc_clf.fit(X_train, y_train)
svc_clf.score(X_test, y_test)

# 结果
0.89600000000000002

# 使用决策树
from sklearn.tree import DecisionTreeClassifier
dt_clf = DecisionTreeClassifier()
dt_clf.fit(X_train, y_train)
dt_clf.score(X_test, y_test)

# 结果
0.85599999999999998

可以看到,使用三种不同的分类算法训练出的模型,最后的 R 2 " role="presentation">R^2评分都不尽相同。下面我们使用投票的方式,选择出最终预测值,具体思路是先求出三种模型对测试数据的预测结果,将三个结果向量相加,得到新的结果向量,因为分类只有0和1,所以新的结果向量里的值最大为3,最小为0。然后通过Fancy Index的方式,求出三种模型预测中至少有2种预测为1的,才真正认为是1的分类,那么也就是新结果向量里大于等于2的结果,其余小于2的都认为是0的分类:

# 求出三种模型对测试数据的预测结果
y_predict1 = log_clf.predict(X_test)
y_predict2 = svc_clf.predict(X_test)
y_predict3 = dt_clf.predict(X_test)

y_predict = np.array((y_predict1 + y_predict2 + y_predict3) >= 2, dtype='int')

# 计算最终的评分
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_predict)

# 结果
0.88800000000000001

上面的示例是我们手动使用三种算法的结果通过投票方式求得了最终的决策依据。其实Scikit Learn中已经为我们封装了这种方式,名为VotingClassifier,既投票分类器:

# 引入投票分类器
from sklearn.ensemble import VotingClassifier

# VotingClassifier和Pipeline的用法非常类似,这里的voting="hard"可以先忽略
voting_clf = VotingClassifier(estimators=[
	("log_clf", LogisticRegression()),
	("svm_clf", SVC()),
	("dt_clf", DecisionTreeClassifier(random_state=666))
], voting="hard")

voting_clf.fit(X_train, y_train)
voting_clf.score(X_test, y_test)

# 结果
0.88800000000000001

可以看到,使用VotingClassifier最后的评分和我们手动模拟的是一致的。

Soft Voting

在上一节中,Scikit Learn提供的VotingClassifier有一个参数voting,我们传了hard这个值。其实这个参数就表示投票的方式,hard代表的就是少数服从多数的机制。

但是,其实在很多时候少数服从多数得到的结果并不是正确的,这个在日常生活中其实很常见,所谓真理掌握在少数人手里就是这个意思。所以更合理的投票机制应该是对投票人加以权重值,投票人越专业,越权威,那么权重值就应该高一些。就好比歌唱比赛,评委有三类人,第一类是音乐制作人,特点是人少,但权重值高,第二类是职业歌手,人数次之,权重值也次之,第三类是普通观众,这类人人数最多,但是权重值也最低。那么决定选手去留还是掌握在少数的音乐制作人和职业歌手这些评委。这个思路其实就是Soft Voting。

再举一个示例,假设有5个模型,针对同一个二分类问题,将每种类别都计算出了概率:

  • 模型1 A-99%,B-1%
  • 模型2 A-49%,B-51%
  • 模型3 A-40%,B-60%
  • 模型4 A-90%,B-10%
  • 模型5 A-30%,B-70%

从上面的数据,明显可以得到,整体的分类应该B,因为模型2、模型3、模型5的结论都是B,所以按照Hard Voting方式,少数服从多数,那整体的类别会定为B。

但是我们可以换个角度去看问题,模型1和模型4对判定为类别A的概率都在90%以上,说明非常笃定。而模型2、模型3、模型5虽然结论为类别B,但是类别A和类别B的判定概率相差并不是很大。而我们将五种模型对类别A、类别B的概率加起来就可以明显的看到,判定为类别A的总概率为:

( 0.99 + 0.49 + 0.4 + 0.9 + 0.3 ) 5 = 0.616" role="presentation">(0.99+0.49+0.4+0.9+0.3)5=0.616 ( 0.99 + 0.49 + 0.4 + 0.9 + 0.3 ) 5 = 0.616

而判定为类别B的总概率为:

( 0.01 + 0.51 + 0.6 + 0.1 + 0.7 ) 5 = 0.384" role="presentation">(0.01+0.51+0.6+0.1+0.7)5=0.384 ( 0.01 + 0.51 + 0.6 + 0.1 + 0.7 ) 5 = 0.384

显然判定为类别A的总概率要远高于类别B,那么整体类别应该是A。

以上模型判定类别的概率其实就可以理解为权重值。所以Soft Voting要求集合里的每一个模型都能估计出类别的概率。那么我们来看看我们已经了解过的机器学习算法哪些是支持概率的:

  • 逻辑回归算法本身就是基于概率模型的,通过Sigmoid函数计算概率。
  • kNN算法也是支持估计概率的,如果和预测点相邻的3个点,有2个点是红色,1个是蓝色,那么可以很容计算出红色类别的概率是 2 3 " role="presentation">\frac 2 3,蓝色类别的概率是 1 3 " role="presentation">\frac 1 3
  • 决策树算法也是支持估计概率的,它的思路和kNN的很相近,每个叶子节点中如果信息熵或基尼系数不为0,那么就肯定至少包含2种以上的类别,那么用一个类别的数量除以所有类别的数据就能得到概率。
  • SVM算法本身是不能够天然支持估计概率的。不过Scikit Learn中提供的SVC通过其他方式实现了估计概率的能力,代价就是增加了算法的时间复杂度和训练时间。它有一个probability参数,默认为false既不支持估计概率,如果显示传入true,那么就会启用估计概率的能力。

下面来看看Soft Voting如何使用:

# 还是使用上一节的数据,同样构建VotingClassifier,voiting参数传入soft。这里注意SVC要显示传入probability=True
voting_clf1 = VotingClassifier(estimators=[
	("log_clf", LogisticRegression()),
	("svm_clf", SVC(probability=True)),
	("dt_clf", DecisionTreeClassifier(random_state=666))
], voting="soft")

voting_clf1.fit(X_train, y_train)
voting_clf1.score(X_test, y_test)

# 结果
0.89600000000000002

可以看到Soft Voting相比较Hard Voting,预测评分是有提高的。

Bagging

上两节主要介绍了集成学习的原理。那么就这个原理而言,它的好坏是有一个基本的先决条件的。对于投票机制而言,5个人投票和1000个人投票得到的结果,毫无疑问是后者更具有说服力。所以前两节我们只使用了三个机器学习算法训练的模型去投票是不具有很强的说服力的。那么问题来了,我们如何能有更多的模型,来作为投票者呢?这就是这一节要说的取样问题。

我们知道机器学习算法是有限的,有几十个就顶破天了,所以用不同的算法这条路是行不通的,那么我们就从同一种算法的不同模型这个思路入手。基本思路就是使用一种机器学习算法,创建更多的子模型,然后集成这些子模型的意见进行投票,有个前提是子模型之间不能一致,要有差异性。这一点大家应该很好理解,模型之间的差异性越大,投票才有意义。

那么如何创建子模型的差异性呢?通常的做法是训练每个子模型时只看样本数据的一部分,比如一共有1000个样本数据,每个子模型只看100个样本数据,因为样本数据有差异性,所以训练出的子模型之间就自然存在差异性了。这时问题又来了,训练子模型时只这么少的样本数据,那么每个子模型的准确率自然会比较低。此时就应征了,人多力量大,一把筷子折不断的道理。

假设有三个子模型,每个子模型的准确率只有51%,为什么要用51%作为示例呢,因为投硬币的概率都有50%,所以比它只高一点,算是很低的准确率了。那么整体的准确率为:

0.51 3 + C 3 2 ⋅ 0.51 2 ⋅ 0.49 = 0.515" role="presentation">0.513+C23⋅0.512⋅0.49=0.515 0.51 3 + C 3 2 ⋅ 0.51 2 ⋅ 0.49 = 0.515

三个准确率为51%的子模型,可以使整体的准确率提高至51.5%。那如果是500个子模型,整体的准确率会提升至:

∑ i = 251 500 C 500 i ⋅ 0.51 i ⋅ 0.49 500 − i = 0.656" role="presentation">500∑i=251Ci500⋅0.51i⋅0.49500−i=0.656 ∑ i = 251 500 C 500 i ⋅ 0.51 i ⋅ 0.49 500 − i = 0.656

可见当子模型的数量增加时,同时会增加整体的准确率。所以其实子模型并不需要太高的准确率。

子模型取样方式

子模型的取样方式有两种:

  • 放回取样:每次取完训练子模型的部分样本数据后,再放回样本数据池里,训练下一个子模型使用同样的方式。这样的方式,训练不同的子模型会可能会用到小部分相同的样本数据。
  • 不放回取样:每次取完训练子模型的部分样本数据后,这部分样本数据不再放回样本数据池里,训练下一个子模型使用同样的方式。这样的方式,训练不同的子模型的样本数据不会重复。

通常使用放回取样的方式更多,举个例子,假如有500个样本数据,训练子模型时使用50条数据,那么使用不放回取样只能训练出10个子模型。而如果使用放回取样的话,理论上可以训练出成千上万个子模型。在机器学习中,将取样称为Bagging。而在统计学中,放回取样称为Bootstrap。

下面我们用代码来看看如何使用取样的方式训练子模型:

# 还是使用之前构造的样本数据
# 我们使用决策树这个算法,然后导入BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier

# BaggingClassifier的第一个参数是给定一个算法类型
# n_estimators参数是创建多少个子模型
# max_samples参数是每个子模型使用多少样本数据训练
# bootstrap为True表示为放回取样方式,为False表示为不放回取样方式
bagging_clf = BaggingClassifier(
	DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)

bagging_clf.fit(X_train, y_train)
bagging_clf.score(X_test, y_test)

# 结果
0.88

OOB

使用放回取样方式虽然可以构建更多的子模型,但是它有一个问题,那就是在有限次的放回取样过程中,有一部分样本数据可能根本没有取到,按严格的数学计算,这个比例大概是37%。这个情况称为OOB(Out of Bag)换个思路思考,这37%根本没有被用到的样本数据恰好可以作为测试数据来用,所以在使用这种方式时,我们可以不用train_test_split对样本数据进行拆分,直接使用没有被用到的这37%的样本数据既可。来看看BaggingClassifier如何使用OOB:

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier

# 多加一个参数oob_score,True为使用OOB,既记住哪些样本取过,哪些没取过
bagging_clf = BaggingClassifier(
	DecisionTreeClassifier(), 
	n_estimators=500, 
	max_samples=100, 
	bootstrap=True,
	oob_score=True)

bagging_clf.fit(X, y)
# 使用没取过样本作为测试数据
bagging_clf.oob_score_

# 结果
0.91000000000000003

可以看到准确率是有所提高的。

并发取样

按照Bagging的思路,因为不需要保证每次取样的唯一性,所以每次取样是可以并行处理的。我们可以使用n_jobs指定运行的CPU核数:

# 先看看之前的训练时间
%%time
bagging_clf = BaggingClassifier(
	DecisionTreeClassifier(), 
	n_estimators=500, 
	max_samples=100, 
	bootstrap=True,
	oob_score=True)
bagging_clf.fit(X, y)

# 结果
CPU times: user 717 ms, sys: 6.41 ms, total: 724 ms
Wall time: 723 ms

# 再看看加了n_jobs参数后的训练时间,n_jobs=-1表示使用电脑的所有CPU核数
%%time
bagging_clf = BaggingClassifier(
	DecisionTreeClassifier(), 
	n_estimators=500, 
	max_samples=100, 
	bootstrap=True,
	oob_score=True,
	n_jobs=-1)
bagging_clf.fit(X, y)

# 结果
CPU times: user 264 ms, sys: 57.6 ms, total: 322 ms
Wall time: 571 ms

可以看到,训练时间缩短了150多毫秒。

机器学习笔记四之kNN算法、超参数、数据归一化 中的网格搜索超参数一节介绍过n_jobs参数。

特征取样

我们之前讲的都是对样本数据条数进行随机取样,BaggingClassifier还可以对特征进行随机取样,这样更能增加子模型的差异性,称为Random Subspaces。另外还有既对样本条数取样,又针对特征随机取样的方式,称为Random Patches。

# max_features 表示随机取几个特征
# bootstrap_features为True表示对特征取样是放回取样方式
random_subspaces_clf = BaggingClassifier(
	DecisionTreeClassifier(), 
	n_estimators=500, 
	max_samples=500, 
	bootstrap=True,
	oob_score=True,
	n_jobs=-1,
	max_features=1,
	bootstrap_features=True)

random_subspaces_clf.fit(X, y)
random_subspaces_clf.oob_score_

# 结果
0.82999999999999996

首先将max_samples设置为500,意在取消对样本数据条数随机取样,因为一共有500个样本数据,要创建500个子模型,如果每个子模型都使用500个样本数据,那相当于对样本条数取样是没有意义的。又因为我们的样本特征只有2个,所以max_features设置为1。

如果将max_samples设回100的话,那就是既对样本条数随机取样,又对特征随机取样:

random_patches_clf = BaggingClassifier(
	DecisionTreeClassifier(), 
	n_estimators=100, 
	max_samples=500, 
	bootstrap=True,
	oob_score=True,
	n_jobs=-1,
	max_features=1,
	bootstrap_features=True)

random_patches_clf.fit(X, y)
random_patches_clf.oob_score_

# 结果
0.79400000000000004

随机森林

前面几个章节介绍了集成学习的原理。在集成学习中,如果使用决策树,通过取样的方式创建子模型,这些子模型就是一个个随机的决策树。我们管这种方式形象的称为随机森林。在Scikit Learn中,也为我们封装好了随机森林的类,它的原理和上一小节示例中通过BaggingClassifierDecisionTreeClassifier构建的分类器基本是一样的。我们来看看如何使用:

from sklearn.ensemble import RandomForestClassifier

rf_clf = RandomForestClassifier(n_estimators=500, oob_score=True, random_state=666)

# 还是使用之前构建的样本数据
rf_clf.fit(X, y)

# 结果
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
			max_depth=None, max_features='auto', max_leaf_nodes=None,
			min_impurity_decrease=0.0, min_impurity_split=None,
			min_samples_leaf=1, min_samples_split=2,
			min_weight_fraction_leaf=0.0, n_estimators=500, n_jobs=1,
			oob_score=True, random_state=666, verbose=0, warm_start=False)

rf_clf.oob_score_

# 结果
0.89200000000000002

fit之后,从返回结果里可以看到,RandomForestClassifier的参数综合了BaggingClassifierDecisionTreeClassifier的参数。我们可以对不同的参数进行调优,训练出更好的模型。

Boosting

我们之前介绍的集成学习中子模型之间是相互独立的,差异越大越好。那么集成学习中还有一种创建子模型的方式,就是每个子模型之间有关联,都在尝试增强整体的效果。这种方式称为Boosting方式。

Ada Boosting

在Boosting方式中,有一种方式称为Ada Boosting,我们用网络上的一幅解决回归问题的图来做以解释:

我们先来看第一个齿轮上下连接的图,下面的图是原始样本数据,每个样本点的权重值都是一样的,上面的图是第一个子模型预测出的结果,那势必会有没有被准确预测的样本点,将这些样本点的权重值加大。

第二列下图中展示的深色点就是权重值增大的样本点,浅色点是上一个子模型预测出的样本点。那么训练第二个子模型时会优先考虑权重大的样本点进行拟合,拟合出的结果如第二列上图所示。

然后再将第二个子模型没有预测出的样本点的权重值增大,如第三列下图所示,在训练第三个子模型时优先考虑第二个子模型没有预测出的样本点进行拟合。以此类推,这样就可以训练出很多子模型,不同于取样方式,Boosting方式的所有子模型使用全量的样本数据进行训练,不过样本数据有权重值的概念,而且后一个子模型是在完善上一个子模型的错误,从而所有子模型达到增强整体的作用。这就是Ada Boosting的原理。

下面来看看Scikit Learn为我们提供的AdaBoostClassifier如何使用:

from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

ada_clf = AdaBoostClassifier(DecisionTreeClassifier(max_depth=2), n_estimators=50)

# Boosting方式没有OOB的概念,所以还是需要使用拆分后的样本数据
ada_clf.fit(X_train, y_train)
ada_clf.score(X_test, y_test)

# 结果
0.86399999999999999

Gradient Boosting

还有一种Boosting的方式称为Gradient Boosting。它的原理是我们训练第一个子模型M1,它肯定会有没有准确预测到的样本,我们称为错误E1。然后我们将E1这些样本点作为训练第二个子模型的样本数据,训练出第二个子模型M2,然后它必然还会产生错误E2。那么再将E2作为训练第三个子模型的样本数据,产生错误E3,以此类推,训练出多个子模型。最终预测的结果是M1+M2+M3+…的结果。

下面来看看Scikit Learn为我们提供的GradientBoostingClassifier如何使用:

from sklearn.ensemble import GradientBoostingClassifier

# GradientBoostingClassifier本身就是使用决策树作为算法实现的,所以不再需要传入算法实例
gd_clf = GradientBoostingClassifier(max_depth=2, n_estimators=100)

# Boosting方式没有OOB的概念,所以还是需要使用拆分后的样本数据
gd_clf.fit(X_train, y_train)
gd_clf.score(X_test, y_test)

# 结果
0.90400000000000003

Stacking

这一小节我们再来认识一个集成学习创建子模型的思路,Stacking。

上图也是网络上的一幅图,我们先看中间那层,Subset1和Subset2是将原始样本数据分成两部分后的数据,我们先使用Subset1训练出三个子模型,这三个子模型会产生错误,既没有预测到的样本数据。然后将这三个子模型的三个错误结果和Subset2组成新的样本数据,训练出第四个子模型。整体的预测结果以第四个子模型的结果为准。这就是Stacking的基本原理,通过Stacking方式可以构建出比较复杂的子模型关系网:

上图有三层,一共7个子模型,就需要将原始样本数据分成三份,第一份作为训练第一层三个子模型的样本数据,第二份作为训练第二层子模型的样本数据其中一部分,以此类推。

不过在Scikit Learn中没有提供任何Stacking的类供我们使用,Stacking的原理已经有神经网络的雏形了,里面涉及到的调参环节非常多,大家有兴趣可以自己尝试实现Stacking算法。

申明:本文为慕课网liuyubobobo老师《Python3入门机器学习 经典算法与应用》课程的学习笔记,未经允许不得转载。