集体智慧:随机地向几千人问一个复杂问题,然后汇总他们的回答,这个汇总回答往往比专家 的回答要好。
同样,聚合一组预测器(可以是回归、分类等等)其结果也比最好的单个预测器要好,
这样的一组预测器,称为集成,这种技术也称为 集成学习
。聚合的方法也多种多样。
随机森林
是决策树算法的集成,它是迄今可用的最强大的机器学习方法之一。
事实上,在机器学习竞赛中获胜的方案通常都涉及多种集成方法。(如http://netflixprize.com/)
本章将学习以下几种最流行的集成方法: bagging
, boosting
, stacking
,
还会学习 随机森林
.
集成方法可以将弱学习器(仅比随机猜测好一点)变成一个强学习器(高准确率), 只要弱学习器的种类和数量足够多即可。 为什么呢?原因见P167,但我还是没看懂???
大数定理:随着不断地投掷硬币,正面朝上的概率会越来越接近于正面的概率。
如果每个分类器只有51%的正确率,当以大多数投票的类别作为预测结果时,可以得到75%的准确率。 但是前提是:所有分类器都是完全独立的,彼此的错误毫不相关。这是不可能的,因为它们在相同的 数据上训练得到,很可能犯相同的错误。
注意:当预测器尽可能互相独立时,集成方法的效果最优。使用不同的算法训练可以增加它们犯不同类型错误的机会,从而提升集成的准确率。
硬投票:聚合每个分类器的预测,将得票最多的类别作为预测结果。
sklearn实现 :参数 volting=soft
软投票:如果所有被集成的分类器都能给出类别概率(即有predict_proba()方法),那到么可以将不同类别 的概率在所有分类器上进行平均,以平均概率最高的类别作为预测结果。
通常来说,软投票比硬投票表现更好!因为它给予那些高度自信的投票更高的权重。
sklearn实现 :1.确保所有预测器都能给出概率。2.参数 volting=soft
注意:SVC默认不能给出概率,所以不能用以软投票;除非设probability为true,但这会开启交叉验证,减慢训练速度。
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
,使用方法也相同。
注意: 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是bootstrap aggregating的缩写,即自举汇聚法。bootstrap的意思是adj.依靠自已力量的, boot的意思是靴子,strap的意思是(皮)带子。aggregate是v. 合计、聚合的意思。
采样时如果将实例放回,就叫bagging;采样时不放回样本,就叫pasting。也就是说bagging和pasting都 允许同一实例被不同预测器中多次采样,而bagging的放回允许同一实例被同一预测器多次采用。
训练完成后,将所有预测器的预测结果用 聚合函数
简单地聚合起来,来对新实例做出预测。聚合函数通常是
统计法
(如大多数投票)用于分类或 平均法
用于回归。
注意: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会导致有些训练集实例未被采用(大约37%),这些未被采用的实例称为 外包(oob)
实例。 对所有预测器来说,这是不一样37%
。
正好可以用这些外包实例来评估模型。将每个预测器在其外包上的评估结果进行平均,即为对集成的 评估。
创建 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_features
和 max_features
控制,对实例抽样由参数 bootstrap
和 max_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
注意:特征随机抽样,对高维输入特别有用(如图像)。
方法一:在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实现为 ExtraTreesClassifier
类,它的API与RandomForestClassifier相同。
相应地 ExtraTreesRegressor
的API与 RandomForestRegressor
相同。
在训练好的决策树中,越重要的特征越可能出现在靠近根节点的位置,不重要的特征出现在靠近节点的位置,
甚至根本不出现。因此可以用特征在森林中的平均深度来评估其重要性。随机森林是一个非常便利的了解
什么特征真正重要的方法,可以用于 特征选择
。
sklearn在训练结束后自动计算特征重要性。可以通过 feature_importances_
来访问。
rdf_clf.feature_importances_
。
大多数提升法的总体思路是先循环训练预测器,每一次都对前序做出一些修正。有许多提升法, 比较流行的是自适应提升Adaboost和梯度提升Gradient Boosting.
对前序修正的方法之一,就是更多地关注前序拟合不足的训练实例,从而使新的预测器不断地越来越专注于难缠 的问题,这就是Adaboost使用的技术。
疑问:训练时就更加关注难预测的实例了?还是说,只为了得到聚合权重, 训练时前序预测器的预测结果不影响后序预测器的预测?
给每个实例一个权重,最初的权重相同,均为 w(i)=1/m ,每个预测器预测后更新权重 w(i) 。 更新规则如下:
权重更新函数用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
对上述Adaboost的权重更新规则我们可以发现以下几点:
1. 权重更新时,只对分类错误的实例进行提升,且提升的幅度相同。 2. 预测器解决难题的能力越强,其对自己分类错误的实例提升幅度越大。 3. 预测器分类错误的实例的权重和越小,其解决难题能力越强。 4. 预测器权重函数和更新规则函数都是单调递减的。
然后将所有权重更新后重新归一化,并重复这一过程。预测:计算所有预测器的预测结果,并使用 预测器权重αj对预测结果进行加权平均,得到大多数投票的类别就是预测类别。
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集成过拟合,可以减少估算器数量,或增加基础估算器的正则化程度。
梯度提升不是像自适应那样调整实例权重,而是让新的预测器针对前序预测器的残差进行拟合。 可用于分类也可用于回归。将所有这些预测器的预测相加即为新实例的预测结果。
使用决策树作为基础预测器的梯度提升回归被称为 梯度提升回归树(GBRT)
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()方法,它会在每训练完一棵树就返回一个 迭代器,
可以训练一个模型用来函数,而不是简单地用投票等方法。
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
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]