Skip to content

Latest commit

 

History

History
513 lines (455 loc) · 22.7 KB

File metadata and controls

513 lines (455 loc) · 22.7 KB

第七章 集成学习和随机森林

集体智慧:随机地向几千人问一个复杂问题,然后汇总他们的回答,这个汇总回答往往比专家 的回答要好。

同样,聚合一组预测器(可以是回归、分类等等)其结果也比最好的单个预测器要好, 这样的一组预测器,称为集成,这种技术也称为 集成学习 。聚合的方法也多种多样。

随机森林 是决策树算法的集成,它是迄今可用的最强大的机器学习方法之一。

事实上,在机器学习竞赛中获胜的方案通常都涉及多种集成方法。(如http://netflixprize.com/) 本章将学习以下几种最流行的集成方法: bagging, boosting, stacking, 还会学习 随机森林.

投票分类器(不同算法在相同训练集上的聚合)

集成方法可以将弱学习器(仅比随机猜测好一点)变成一个强学习器(高准确率), 只要弱学习器的种类和数量足够多即可。 为什么呢?原因见P167,但我还是没看懂???

大数定理:随着不断地投掷硬币,正面朝上的概率会越来越接近于正面的概率。

如果每个分类器只有51%的正确率,当以大多数投票的类别作为预测结果时,可以得到75%的准确率。 但是前提是:所有分类器都是完全独立的,彼此的错误毫不相关。这是不可能的,因为它们在相同的 数据上训练得到,很可能犯相同的错误。

注意:当预测器尽可能互相独立时,集成方法的效果最优。使用不同的算法训练可以增加它们犯不同类型错误的机会,从而提升集成的准确率。

硬投票分类器

硬投票:聚合每个分类器的预测,将得票最多的类别作为预测结果。

sklearn实现 :参数 volting=soft

软投票分类器

软投票:如果所有被集成的分类器都能给出类别概率(即有predict_proba()方法),那到么可以将不同类别 的概率在所有分类器上进行平均,以平均概率最高的类别作为预测结果。

通常来说,软投票比硬投票表现更好!因为它给予那些高度自信的投票更高的权重。

sklearn实现 :1.确保所有预测器都能给出概率。2.参数 volting=soft

注意:SVC默认不能给出概率,所以不能用以软投票;除非设probability为true,但这会开启交叉验证,减慢训练速度。

sklearn中投票分类器(回归器)的实现

from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import VotingClassifier

tree_clf = DecisionTreeClassifier() # 调用决策树
log_clf = LogisticRegression() # 调用逻辑回归
svm_clf = SVC() # 调用支持向量机分类

voting_clf = VotingClassifier( # 组装投票分类器
    estimators=[('lr', log_clf), ('dt', tree_clf), ('svc', svm_clf)],
    voting='hard' # 投票方法:硬投票
)
voting_clf.fit(X_train, y_train)

VotingClassifer 类中还有个有用的参数 weights ,用以加权计算在 硬投票 时的分类结果或 软投票 时平均之前的类别概率。比如 weights=[1, 2, 3]weights=[0.2, 0.2, 0.3] 元素个数要等于 n_classifiers

与分类相对应的回归实现为 VotingRegressor ,使用方法也相同。

三种不同的集成方法(bagging/pasting, boosting, stacking)

实际使用经验

注意:
    1.训练极端随机树集成要比随机森林快得多,因为找到最佳阀值是个耗时的任务。
    2.通常很难预先知道RandomForestClassifier与ExtraTreesClassifier哪个好,
      唯一的方法是两种都尝试一遍,再用交叉验证进行比较。
    3.使用随机森林和极端随机树时,要调整的参数主要是 n_estimators 和 max_features。
    4.根据经验,回归问题中使用 max_features = None (总是考虑所有的特征),
      分类问题使用 max_features = "sqrt" (随机考虑 sqrt(n_features) 特征,
      其中 n_features 是特征的个数)是比较好的默认值。
    5.max_depth = None 和 min_samples_split = 2 结合通常会有不错的效果(即生成完全的树)。
      请记住,这些(默认)值通常不是最佳的,同时还可能消耗大量的内存,最佳参数值应由交叉验证获得。
      另外,请注意,在随机森林中,默认使用自助采样法(bootstrap = True), 然而 extra-trees
      的默认策略是使用整个数据集(bootstrap = False)。 当使用自助采样法方法抽样时,
      泛化精度是可以通过剩余的或者袋外的样本来估算的,设置 oob_score = True 即可实现。
    6.对于拥有大量类别的数据集我们强烈推荐使用RandomForestClassifier来代替GradientBoostingClassifier
    7.max_leaf_nodes=k 可以给出与 max_depth=k-1 品质相当的结果,但是其训练速度明显更快,
      同时也会以多一点的训练误差作为代价。
    8.bagging方法可以减小过拟合,所以通常在强分类器和复杂模型上使用时表现的很好(例如,完全生长的决策树,
      fully developed decision trees);相比之下 boosting 方法则在弱模型上表现更好(例如,浅层决策树,
      shallow decision trees)。

bagging/pasting(相同算法在不同训练集随机子集上的聚合)

使用同一算法,在训练集的不同随机子集上进行训练,再聚合。

bagging是bootstrap aggregating的缩写,即自举汇聚法。bootstrap的意思是adj.依靠自已力量的, boot的意思是靴子,strap的意思是(皮)带子。aggregate是v. 合计、聚合的意思。

采样时如果将实例放回,就叫bagging;采样时不放回样本,就叫pasting。也就是说bagging和pasting都 允许同一实例被不同预测器中多次采样,而bagging的放回允许同一实例被同一预测器多次采用。

训练完成后,将所有预测器的预测结果用 聚合函数 简单地聚合起来,来对新实例做出预测。聚合函数通常是 统计法 (如大多数投票)用于分类或 平均法 用于回归。

注意:bagging和pasting流行的原因之一是,在训练和预测时可以并行处理。

sklearn中bagging/pasting的实现

sklearn提供了 BaggingClassifier 类用于bagging和pasting。相应的也有BaggingRegressor 用于回归。

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

tree_clf = DecisionTreeClassifier() # 调用决策树
bag_clf = BaggingClassifier( # 组装集成分类器
    tree_clf, # 基础预测器
    n_estimators=500, # 基础预测器的个数
    max_samples = 100, # 每次从训练集中随机采样的个数
    bootstrap=True, # 采样时是否放回
    n_jobs=-1 # 使用CPU核的个数,-1表示全用
)
bag_clf.fit(X_train, y_train) # 使用训练集训练模型参数
y_pred = bag_clf.predict(X_test) # 用训练得到的模型参数进行预测
注意:
1.如果基础分类器能给出预测概率,BaggingClassifier就默认使用软投票,而不是硬投票。
2.由于bagging在采样时引入了更多的多样性,所以通常表现比pasting要好。
3.如果时间和计算资源充足,也可以用交叉验证来对bagging和pasting进行比较,再做选择。

对bagging进行评估:外包评估

使用bagging会导致有些训练集实例未被采用(大约37%),这些未被采用的实例称为 外包(oob) 实例。 对所有预测器来说,这是不一样37%

正好可以用这些外包实例来评估模型。将每个预测器在其外包上的评估结果进行平均,即为对集成的 评估。

sklearn中bagging外包评估的实现

创建 BaggingClassifier 时设置 oob_score=True ,就可以在训练结束后自动进行 外包评估。通过 oob_score_ 可以取得集成的最终评估分数。

bag_clf = BaggingClassifier(
    tree_clf,
    n_estimators=500,
    max_samples=100,
    bootstrap=True, # 放回采样bagging
    oob_score=True, # 开启外包评估
    n_jobs=-1
)
bag_clf.fit(X_train, y_train) # 训练
bag_clf.oob_score_ # 取得对bagging集成的评估分数

决策函数P171(TODO)

BaggingClassifier 类中,对特征抽样由参数 bootstrap_featuresmax_features 控制,对实例抽样由参数 bootstrapmax_samples 控制。

Random Patches方法:对训练实例和特征都随机抽样; bootstrap=True, max_samples<1.0 , bootstrap_features=True, max_features<1.0

随机子空间法:保留所有实例,只对特征随机抽样; bootstrap=False, max_samples=1.0 , bootstrap_features=True, max_features<1.0

注意:特征随机抽样,对高维输入特别有用(如图像)。

在sklearn中实现随机森林(决策树的集成)

方法一:在BaggingClassifier中调用DecisionTreeClassifier.

方法二:使用RandomForestClassifier类,它对决策树更优化。相应地有也有RandomForestRegressor。 代码如下:

from sklearn.ensemble import RandomForestClassifier
rdf_clf = RandomForestClassifier(
    n_estimators=500,
    max_leaf_nodes=16,
    n_jobs=-1
    )
rdf_clf.fit(X_train, y_train)
y_pred = rdf_clf.predict(X_test)

RandomForestClassifier 有绝大多数 DecisionTreeClassifier 的超参数, 以及所有BaggingClassifier的超参数。前者控制树的生长,后者控制集成。

注意:
    1.RandomForestClassifier中没有max_samples超参(强制为1.0)。
    2.RandomForestClassifier中引入了更多随机性:分裂节点时,只在随机特征子空间中搜索最好特征,用以分裂节点。

极端随机树

对每个特征使用随机阀值而不是最佳阀值(如常规决策树),可以让决策树生长得更加随机。 这种树组成的森林称为 极端随机树集成

极端随机树的sklearn实现

sklearn实现为 ExtraTreesClassifier 类,它的API与RandomForestClassifier相同。 相应地 ExtraTreesRegressor 的API与 RandomForestRegressor 相同。

特征重要性

在训练好的决策树中,越重要的特征越可能出现在靠近根节点的位置,不重要的特征出现在靠近节点的位置, 甚至根本不出现。因此可以用特征在森林中的平均深度来评估其重要性。随机森林是一个非常便利的了解 什么特征真正重要的方法,可以用于 特征选择

sklearn在训练结束后自动计算特征重要性。可以通过 feature_importances_ 来访问。 rdf_clf.feature_importances_

boosting 提升法(将弱学习结合成强学习的任意集成方法)

大多数提升法的总体思路是先循环训练预测器,每一次都对前序做出一些修正。有许多提升法, 比较流行的是自适应提升Adaboost和梯度提升Gradient Boosting.

AdaBoost 自适应提升

对前序修正的方法之一,就是更多地关注前序拟合不足的训练实例,从而使新的预测器不断地越来越专注于难缠 的问题,这就是Adaboost使用的技术。

疑问:训练时就更加关注难预测的实例了?还是说,只为了得到聚合权重,
     训练时前序预测器的预测结果不影响后序预测器的预测?

给每个实例一个权重,最初的权重相同,均为 w(i)=1/m ,每个预测器预测后更新权重 w(i) 。 更新规则如下: ../images/updatew.png

../images/pred_w.png

../images/w_error.png

权重更新函数用gnuplot绘制如下所示:

set term qt font "Times New Roman,20" # 输出到屏幕
# set output "../images/update.png"
set title "Ada boost预测器权重{/symbol a}_j={/symbol \150}log((1-r_j)/r_j)与权重更新幅度exp({/symbol a}_j)"
eta = 0.8
f(x) = eta * log((1-x)/x)
g(x) = exp(f(x))
set xrange [0:1]
set yrange [-4:8]
# unset k
set xlabel "r_j"
set ylabel "{/symbol a}_j"
# set grid
plot f(x) w l lw 2 t '预测器权重', g(x) w lp lw 2 t '权重更新幅度', 1 t '{/symbol a}_j=1' w l dt 2 lw 1 lc 8
pause mouse close # 允许鼠标拖动
# set output

../images/update_weight.png

对上述Adaboost的权重更新规则我们可以发现以下几点:

1. 权重更新时,只对分类错误的实例进行提升,且提升的幅度相同。
2. 预测器解决难题的能力越强,其对自己分类错误的实例提升幅度越大。
3. 预测器分类错误的实例的权重和越小,其解决难题能力越强。
4. 预测器权重函数和更新规则函数都是单调递减的。

然后将所有权重更新后重新归一化,并重复这一过程。预测:计算所有预测器的预测结果,并使用 预测器权重αj对预测结果进行加权平均,得到大多数投票的类别就是预测类别。

sklearn中Adaboost的实现

sklearn实现的是AdaBoost的的多分类版本SAMME。当只有两个类别时,SAMME等同于AdaBoost. 如果基础预测器可以给出类别概率,sklearn会使用SAMME的变体SAMME.R,它使用概率而不是类别。

AdaboostClassifier 用于自适应提升分类, AdaBoostRegressor 用于自适应提升回归。

from sklearn.ensemble import AdaBoostClassifier
ada_clf = AdaBoostClassifier(
    DecisionTreeClassifier(max_depth=1), # 默认的基础预测器,深度为1
    n_estimators=200,
    algorithm='SAMME.R',
    learning_rate=0.5
)
ada_clf.fit(X_train, y_train)

如果AdaBoost集成过拟合,可以减少估算器数量,或增加基础估算器的正则化程度。

Gradient Boosting 梯度提升

梯度提升不是像自适应那样调整实例权重,而是让新的预测器针对前序预测器的残差进行拟合。 可用于分类也可用于回归。将所有这些预测器的预测相加即为新实例的预测结果。

使用决策树作为基础预测器的梯度提升回归被称为 梯度提升回归树(GBRT)

sklearn中Gradient boosting的实现

sklearn中可以用 GradientBoostingRegressor 类来实现GBRT,它有控制树成长的参数, 也有控制集成的参数,与 RandomForestRegressor 类似。下面的代码用以重现 三棵决策树的梯度提升集成。

from sklearn.ensemble import GradientBoostingRegressor
grd_reg = GradientBoostingRegressor(
    max_depth=2,
    n_estimators=3,
    learning_rate=1.0
)
grd_reg.fit(X_train, y_train)
注意:
    1. 超参learning_rate对每棵树的贡献进行缩放,可以缩小它用来正则化。
    2. 可以用早期停止法来,寻找最佳的树数量,再用这个数量的树进行训练。
    3. 也可以不用训练大量的树再找最佳数量,而真的提前停止训练。(利用warm_start=True)
    4. 超参数subsample=0.25,用于指定每训练棵树的实例数量比例为随机的25%。这称为 ~随机梯度提升~。
    5. 随机梯度提升引入了更多变化,也大量地加快了训练过程。
    6. 超参数loss来指定成本函数。

实现早期停止法可以用staged_predict()方法,它会在每训练完一棵树就返回一个 迭代器,

stacking 堆叠法(TODO)

可以训练一个模型用来函数,而不是简单地用投票等方法。

sklearn对stacking的实现(不直接支持!)(TODO)

练习

1.决策树超参调优

from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split, GridSearchCV, RandomizedSearchCV
from sklearn.ensemble import VotingClassifier, BaggingClassifier, RandomForestClassifier, ExtraTreesClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

X, y = make_moons( # 生成卫星数据集
    n_samples=10000, # 10000个实例,中等大小
    shuffle=True,
    noise=0.2,
    random_state=42
)
X_train, X_test, y_train, y_test = train_test_split( # 分离测试集
    X,
    y,
    test_size=0.2,
    random_state=42,
    #shuffle=True
)
stdscaler = StandardScaler() # 调用特征缩放器
#X_train = stdscaler.fit_transform(X_train) # 缩放特征

dt = DecisionTreeClassifier( # 创建决策树分类器
    criterion='gini' # 分裂标准为不纯度
)
grid_param = {
    'max_leaf_nodes': range(2, 100),
    #'max_depth': range(80),
    #'min_samples_leaf': range(50)
}
grid_search = GridSearchCV(
#grid_search = RandomizedSearchCV(
    dt,
    grid_param,
    cv=5,
    scoring='accuracy',
    verbose=1,
    n_jobs=-1,
)
grid_search.fit(X_train, y_train)
best_clf  = grid_search.best_estimator_
print(best_clf)
#best_clf.fit(X_train, y_train) # 不需要再训练,已经训练好了!
y_pred = best_clf.predict(X_test)
print(grid_search.best_params_, grid_search.best_score_, accuracy_score(y_pred, y_test))

训练时交叉验证的精度为85.52%, 测试的精度为79.92%,显然存在过拟合的情况,需要正则化。 可以设置参数的上下限或进行剪枝!

用随机网格搜索后得到交叉验证精度0.8566666666666667,测试集精度0.8104。以上两个模型中,对训练集先进行了 缩放和集中,导致在测试集上的预测准确率下降,如果不进行特征缩放,结果为:{‘max_leaf_nodes’: 23} 0.85925 0.8735

2.对比本章学到的分类器性能

from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
#import numpy as np
from sklearn.ensemble import VotingClassifier, BaggingClassifier, RandomForestClassifier, ExtraTreesClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier

X, y = make_moons( # 生成卫星数据集
    n_samples=10000, # 10000个实例,中等大小
    #shuffle=True,
    noise=0.2,
    random_state=42
)
X_train, X_test, y_train, y_test = train_test_split( # 分离测试集
    X,
    y,
    test_size=0.2,
    random_state=42,
    #shuffle=True
)
stdscaler = StandardScaler() # 调用特征缩放器
#X_train = stdscaler.fit_transform(X_train) # 缩放特征
#help(LogisticRegression);exit(0)
log_clf = LogisticRegression(solver='sag', n_jobs=-1) # 创建逻辑回归
svc = SVC(kernel='rbf') # 创建支持向量机分类器,高斯核
dt = DecisionTreeClassifier( # 创建决策树分类器
    #min_samples_leaf=5, # 节点最小实例数为5
    max_leaf_nodes=23, # 最大
    criterion='gini' # 分裂标准为不纯度
)
rdf_clf = RandomForestClassifier( # 创建随机森林分类器
    #min_samples_leaf=5, # 最小节点实例数为5
    max_leaf_nodes=23, # 最大叶节点数量
    criterion='gini', # 分裂标准为不纯度
    n_estimators=500, # 集成个数
    n_jobs=-1 # 全内核并行
    #max_samples=0.5, # 没有这一参数
)
vt_clf = VotingClassifier( # 创建投票分类器(集成方法)
    estimators=[('log', log_clf), ('dt', dt), ('rdf', rdf_clf)],
    voting='soft', # 软投票
    n_jobs=-1 # 使用全部内核
)
bg1_clf = BaggingClassifier( # 创建自助法分类器(集成方法)
    dt, # 基础预测器为决策树分类器
    bootstrap=True, # 抽样放回,bagging
    max_samples=0.5, # 每次抽样最大数比例
    n_estimators=500, # 集成树的个数
    oob_score=True, # 开启外包评估
    n_jobs=-1 # 使用所有内核
)
bg2_clf = BaggingClassifier(
    dt,
    bootstrap=False, # 抽样不放回,pasting
    max_samples=0.5,
    n_estimators=500,
    n_jobs=-1
)
#help(RandomForestClassifier)
ex_clf = ExtraTreesClassifier( # 创建极端随机树集成
    #min_samples_leaf=5,
    max_leaf_nodes=23, # 最大叶节点数量
    criterion='gini',
    n_estimators=500,
    n_jobs=-1
    #max_samples=0.5, # 没有这一参数
)
ada_clf = AdaBoostClassifier( # 创建自适应提升集成
    dt, # 被集成的基础预测器
    #DecisionTreeClassifier(max_depth=2),
    n_estimators=500, # 基础预测器的数量
    algorithm='SAMME.R', # 使用概率聚合而不使用类别
    learning_rate=0.2, # 学习率,用来对每个基础预测器的贡献进行缩放
    #n_jobs=-1 # 没有这一参数,依序学习,不能并行
)
gb_clf = GradientBoostingClassifier(
    n_estimators=500, # 基础预测器的数量
    #algorithm='SAMME.R', # 没有这一参数
    learning_rate=0.2, # 学习率,用来对每个基础预测器的贡献进行缩放
    subsample=0.5 # 随机梯度提升
)

clfs = (log_clf, svc, dt, vt_clf, bg1_clf, bg2_clf, rdf_clf, ex_clf, ada_clf, gb_clf)
#clfs = (gb_clf)
def validation(clfs, X, y): # 怎么评估不同模型的性能?
    scores = []
    for clf in clfs:
        clf.fit(X, y)
        #try:
        #    print(clf.__class__.__name__, clf.oob_score_)
        #except:
        #    pass
        #finally:
        #    try:
        #        print(clf.__class__.__name__, clf.feature_importances_)
        #    except:
        #        pass
        y_pred = clf.predict(X_test)
        acc_score = accuracy_score(y_pred, y_test)
        scores.append([clf.__class__.__name__, acc_score])
    return scores

scores = validation(clfs, X_train, y_train)
[print(x) for x in scores]

运行结果:

['LogisticRegression', 0.8415]
['SVC', 0.874]
['DecisionTreeClassifier', 0.8735]
['VotingClassifier', 0.8715]
['BaggingClassifier', 0.8715]
['BaggingClassifier', 0.8705]
['RandomForestClassifier', 0.8725]
['ExtraTreesClassifier', 0.869]
['AdaBoostClassifier', 0.8415]
['GradientBoostingClassifier', 0.8615]

8.MNIST数据集上的投票集成

chapt7_exercise.ipynb