----------------------集成学习----------------------
集成学习可以被分为三个主要研究领域:
-----------------------------------------------------模型融合-----------------------------------------------------
模型融合在最初的时候被称为“分类器结合”,这个领域主要关注强评估器,试图设计出强大的规则来融合强分类器的结果、以获取更好的融合结果。这个领域的手段主要包括了投票法Voting、堆叠法Stacking、混合法Blending等,且被融合的模型需要是强分类器。模型融合技巧是机器学习/深度学习竞赛中最为可靠的提分手段之一,常言道:当你做了一切尝试都无效,试试模型融合。
-----------------------------------------------------弱分类器集成-----------------------------------------------------
弱分类器集成主要专注于对传统机器学习算法的集成,这个领域覆盖了大部分我们熟悉的集成算法和集成手段,如装袋法bagging,提升法boosting。这个领域试图设计强大的集成算法、来将多个弱学习器提升成为强学习器。
-----------------------------------------------------混合专家模型(mixture of experts)--------------------------
混合专家模型常常出现在深度学习(神经网络)的领域。在其他集成领域当中,不同的学习器是针对同一任务、甚至在同一数据上进行训练,但在混合专家模型中,我们将一个复杂的任务拆解成几个相对简单且更小的子任务,然后针对不同的子任务训练个体学习器(专家),然后再结合这些个体学习器的结果得出最终的输出。
-----------------------------------------------------Bagging又称为“装袋法”--------------------------------------------
它是所有集成学习方法当中最为著名、最为简单、也最为有效的操作之一。
在Bagging集成当中,我们并行建立多个弱评估器(通常是决策树,也可以是其他非线性算法),并综合多个弱评估器的结果进行输出。当集成算法目标是回归任务时,集成算法的输出结果是弱评估器输出的结果的平均值,当集成算法的目标是分类任务时,集成算法的输出结果是弱评估器输出的结果少数服从多数。
-------------------------------------------下面这个代码在模型融合阶段很重要-------------------------
#分类的情况:输出7个弱评估器上的分类结果(0,1,2)
r_clf = np.array([0,2,1,1,2,1,0])
b_result_clf = np.argmax(np.bincount(r_clf))
#print --- 1
#bincount会先将array由小到大进行排序,然后对每个数值进行计数,并返回计数结果的函数。
#需要注意的是,bincount函数不能接受负数输入。
#argmax是找到array中最大值,并返回最大值索引的函数#分类的情况:输出7个弱评估器上的分类结果(-1,1)
b_result_clf = 1 if (r_clf == 1).sum() > (r_clf != 1).sum() else -1
#要自己手动判断他们的数量了#如果评估器的数量是偶数,而少数和多数刚好一致怎么办?
r_clf = np.array([1,1,1,0,0,0,2,2])
#从数量一致的类别中,返回编码数字更小的类别(如果使用argmax函数)#回归的情况:输出7个弱评估器上的回归结果
r_reg = np.array([-2.082, -0.601, -1.686, -1.001, -2.037, 0.1284, 0.8500])
b_result_reg = r_reg.mean()
-------------------------------------------随机森林------------------------------------
其算法构筑过程非常简单:从提供的数据中随机抽样出不同的子集,用于建立多棵不同的决策树,并按照Bagging的规则对单棵决策树的结果进行集成(回归则平均,分类则少数服从多数)
在sklearn中,随机森林可以实现回归也可以实现分类。随机森林回归器由类sklearn.ensemble.RandomForestRegressor实现,随机森林分类器则有类sklearn.ensemble.RandomForestClassifier实现。我们可以像调用逻辑回归、决策树等其他sklearn中的算法一样,使用“实例化、fit、predict/score”三部曲来使用随机森林,同时我们也可以使用sklearn中的交叉验证方法来实现随机森林。其中回归森林的默认评估指标为R2,分类森林的默认评估指标为准确率。
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.tree import DecisionTreeRegressor as DTR
from sklearn.model_selection import cross_validate, KFold
#这里我们不再使用cross_val_score,转而使用能够输出训练集分数的cross_validate
#决策树本身就是非常容易过拟合的算法,而集成模型的参数量/复杂度很难支持大规模网格搜索
#因此对于随机森林来说,一定要关注算法的过拟合情况
data = pd.read_csv("train_encode.csv",index_col=0)
X = data.iloc[:,:-1]
y = data.iloc[:,-1]
reg_f = RFR() #实例化随机森林
cv = KFold(n_splits=5,shuffle=True,random_state=1412) #实例化交叉验证方式
#与sklearn中其他回归算法一样,随机森林的默认评估指标是R2,
#但在机器学习竞赛、甚至实际使用时,我们很少使用损失以外的指标对回归类算法进行评估。对回归类算法而言,最常见的损失就是MSE。
result_f = cross_validate(reg_f #要进行交叉验证的评估器,X,y #数据,cv=cv #交叉验证模式,scoring="neg_mean_squared_error" #评估指标,return_train_score=True #是否返回训练分数,verbose=True #是否打印进程,n_jobs=-1 #线程数)
trainRMSE_f = abs(result_f["train_score"])**0.5
testRMSE_f = abs(result_f["test_score"])**0.5
#在集成学习中,我们衡量回归类算法的指标一般是RMSE(根均方误差),也就是MSE开根号后的结果。
#现实数据的标签往往数字巨大、数据量庞杂,MSE作为平方结果会放大现实数据上的误差(例如随机森林结果中得到的, 7∗108 等结果),因此我们会对平房结果开根号,
#让回归类算法的评估指标在数值上不要过于夸张。
#同样的,方差作为平方结果,在现实数据上也会太大,因此如果可以,我们使用标准差进行模型稳定性的衡量。
-------------------------------随机森林的参数-------------------------------------
类型 | 参数 |
---|---|
弱分类器数量 | n_estimators |
弱分类器的训练数据 | bootstrap, oob_score, max_samples, max_features, random_state |
弱分类器结构 | criterion, max_depth, min_samples_split, min_samples_leaf, min_weight_fraction_leaf, max_leaf_nodes, min_impurity_decrease |
其他 | n_jobs, verbose, ccp_alpha |
–弱分类器的结构
在集成算法当中,控制单个弱评估器的结构是一个重要的课题,因为单个弱评估器的复杂度/结果都会影响全局,其中单棵决策树的结构越复杂,集成算法的整体复杂度会更高,计算会更加缓慢、模型也会更加容易过拟合,因此集成算法中的弱评估器也需要被剪枝。随机森林回归器的弱评估器是回归树,因此集成评估器中有大量的参数都与弱评估器回归树中的参数重合
类型 | 参数 |
---|---|
弱分类器结构 | criterion:弱评估器分枝时的不纯度衡量指标 max_depth:弱评估器被允许的最大深度,默认None min_samples_split:弱评估器分枝时,父节点上最少要拥有的样本个数 min_samples_leaf:弱评估器的叶子节点上最少要拥有的样本个数 min_weight_fraction_leaf:当样本权重被调整时,叶子节点上最少要拥有的样本权重 max_leaf_nodes:弱评估器上最多可以有的叶子节点数量 min_impurity_decrease:弱评估器分枝时允许的最小不纯度下降量 |
1. 分枝标准与特征重要性
- criterion与feature_importances_
- 与分类树中的信息熵/基尼系数不同,回归树中的criterion可以选择"squared_error"(平方误差),“absolute_error”(绝对误差)以及"poisson"(泊松偏差)
- 其中平方误差与绝对误差是大家非常熟悉的概念,作为分枝标准,平方误差比绝对误差更敏感(类似于信息熵比基尼系数更敏感),并且在计算上平方误差比绝对误差快很多。泊松偏差则是适用于一个特殊场景的:当需要预测的标签全部为正整数时,标签的分布可以被认为是类似于泊松分布的。正整数预测在实际应用中非常常见,比如预测点击量、预测客户/离职人数、预测销售量等。我们现在正在使用的数据(房价预测),也可能比较适合于泊松偏差。
- 另外,当我们选择不同的criterion之后,决策树的feature_importances_也会随之变化,因为在sklearn当中,feature_importances_是特征对criterion下降量的总贡献量,因此不同的criterion可能得到不同的特征重要性。
- 对我们来说,选择criterion的唯一指标就是最终的交叉验证结果——无论理论是如何说明的,我们只取令随机森林的预测结果最好的criterion。
2. 调节树结构来控制过拟合
- max_depth
最粗犷的剪枝方式,从树结构层面来看,对随机森林抗过拟合能力影响最大的参数。max_depth的默认值为None,也就是不限深度。因此当随机森林表现为过拟合时,选择一个小的max_depth会很有效。
- max_leaf_nodes与min_sample_split
比max_depth更精细的减枝方式,但限制叶子数量和分枝,既可以实现微调,也可以实现大刀阔斧的剪枝。max_leaf_nodes的默认值为None,即不限叶子数量。min_sample_split的默认值为2,等同于不限制分枝。
- min_impurity_decrease
最精细的减枝方式,可以根据不纯度下降的程度减掉相应的叶子。默认值为0,因此是个相当有空间的参数。
–弱分类器数量
- n_estimators
n_estimators是森林中树木的数量,即弱评估器的数量,在sklearn中默认100,它是唯一一个对随机森林而言必填的参数。n_estimators对随机森林模型的精确程度、复杂度、学习能力、过拟合情况、需要的计算量和计算时间都有很大的影响,因此n_estimators往往是我们在调整随机森林时第一个需要确认的参数。
当模型复杂度上升时,模型的泛化能力会先增加再下降(相对的泛化误差会先下降再上升),我们需要找到模型泛化能力最佳的复杂度。在实际进行训练时,最佳复杂度往往是一个比较明显的转折点,当复杂度高于最佳复杂度时,模型的泛化误差要么开始上升,要么不再下降。
因此在调整n_estimators时,我们总是渴望在模型效果与训练难度之间取得平衡,同时我们还需要使用交叉验证来随时关注模型过拟合的情况。在sklearn现在的版本中,n_estimators的默认值为100,个人电脑能够容忍的n_estimators数量大约在200~1000左右。
–弱分类器训练的数据
还记得决策树是如何分枝的吗?对每个特征决策树都会找到不纯度下降程度最大的节点进行分枝,因此原则上来说,只要给出数据一致、并且不对决策树进行减枝的话,决策树的结构一定是完全相同的。对集成算法来说,平均多棵相同的决策树的结果并没有意义,因此集成算法中每棵树必然是不同的树,Bagging算法是依赖于随机抽样数据来实现这一点的。
随机森林会从提供的数据中随机抽样出不同的子集,用于建立多棵不同的决策树,最终再按照Bagging的规则对众多决策树的结果进行集成。因此在随机森林回归器的参数当中,有数个关于数据随机抽样的参数。
- 样本的随机抽样
- bootstrap,oob_score,max_samples
bootstrap参数的输入为布尔值,默认True,控制是否在每次建立决策树之前对数据进行随机抽样。如果设置为False,则表示每次都使用全部样本进行建树,如果为True,则随机抽样建树。从语言的意义上来看,bootstrap可以指代任意类型的随机抽样,但在随机森林中它特指有放回随机抽样技术。
如下图所示,在一个含有m个样本的原始训练集中,我们进行随机采样。每次采样一个样本,并在抽取下一个样本之前将该样本放回原始训练集,也就是说下次采样时这个样本依然可能被采集到,这样采集max_samples次,最终得到max_samples个样本组成的自助集。
然而有放回抽样也会有自己的问题。由于是有放回,一些样本可能在同一个自助集中出现多次,而其他一些却可能被忽略。当抽样次数足够多、且原始数据集足够大时,自助集大约平均会包含全数据的63%,因此,会有约37%的训练数据被浪费掉,没有参与建模,这些数据被称为袋外数据(out of bag data,简写为oob)。在实际使用随机森林时,袋外数据常常被我们当做验证集使用,所以我们或许可以不做交叉验证、不分割数据集,而只依赖于袋外数据来测试我们的模型即可。当然,这也不是绝对的,当树的数量n_estimators不足,或者max_samples太小时,很可能就没有数据掉落在袋外,自然也有无法使用oob数据来作为验证集了。
在随机森林回归器中,当boostrap=True时,我们可以使用参数oob_score和max_samples,其中:
- oob_score控制是否使用袋外数据进行验证,输入为布尔值,默认为False,如果希望使用袋外数据进行验证,修改为True即可。
- max_samples表示自助集的大小,可以输入整数、浮点数或None,默认为None。
- 输入整数m,则代表每次从全数据集中有放回抽样m个样本
- 输入浮点数f,则表示每次从全数据集中有放回抽样f*全数据量个样本
- 输入None,则表示每次抽样都抽取与全数据集一致的样本量(X.shape[0])
- 在使用袋外数据时,我们可以用随机森林的另一个重要属性:oob_score_来查看我们的在袋- - 外数据上测试的结果,遗憾的是我们无法调整oob_score_输出的评估指标,它默认是R2。
reg = RFR(n_estimators=20, bootstrap=True #进行随机抽样, oob_score=True #按袋外数据进行验证, max_samples=500).fit(X,y)
#重要属性oob_score_
reg.oob_score_ #在袋外数据上的R2为83%
特征的随机抽样
- max_features
数据抽样还有另一个维度:对特征的抽样。在学习决策树时,我们已经学习过对特征进行抽样的参数max_features,在随机森林中max_features的用法与决策树中完全一致,其输入也与决策树完全一致
输入整数,表示每次分枝时随机抽取max_features个特征
输入浮点数,表示每次分枝时抽取round(max_features * n_features)个特征
输入"auto"或者None,表示每次分枝时使用全部特征n_features
输入"sqrt",表示每次分枝时使用sqrt(n_features)
输入"log2",表示每次分枝时使用log2(n_features)
随机抽样的模式
-
random_state
-
在决策树当中,我们已经学习过控制随机模式的参数random_state,这个参数是“随机数种子”,它控制决策树当中多个具有随机性的流程。
-
当数据样本量足够大的时候(数万),变换随机数种子几乎不会对模型的泛化能力有影响,因此在数据量巨大的情况下,我们可以随意设置任意的数值。
-
当数据量较小的时候,我们可以把随机数种子当做参数进行调整,但前提是必须依赖于交叉验证的结果。选择交叉验证结果中均值最高、方差最低的随机数种子,以找到泛化能力最强大的随机模式。
集成算法的参数空间与网格优化
- 对随机森林来说,我们可以大致如下排列各个参数对算法的影响:
影响力 | 参数 |
---|---|
⭐⭐⭐⭐⭐ 几乎总是具有巨大影响力 | n_estimators(整体学习能力) max_depth(粗剪枝) max_features(随机性) |
⭐⭐⭐⭐ 大部分时候具有影响力 | max_samples(随机性) class_weight(样本均衡) |
⭐⭐ 可能有大影响力 大部分时候影响力不明显 | min_samples_split(精剪枝) min_impurity_decrease(精剪枝) max_leaf_nodes(精剪枝) criterion(分枝敏感度) |
⭐ 当数据量足够大时,几乎无影响 | random_state ccp_alpha(结构风险) |
虽然随机森林调参的空间较大,大部分人在调参过程中依然难以突破,因为树的集成模型的参数空间非常难以确定。当没有数据支撑时,人们很难通过感觉或经验来找到正确的参数范围。举例来说,我们也很难直接判断究竟多少棵树对于当前的模型最有效,同时,我们也很难判断不剪枝时一棵决策树究竟有多深、有多少叶子、或者一片叶子上究竟有多少个样本,更不要谈凭经验判断树模型整体的不纯度情况了。可以说,当森林建好之后,我们简直是对森林一无所知。对于网格搜索来说,新增一个潜在的参数可选值,计算量就会指数级增长,因此找到有效的参数空间非常重要。此时我们就要引入两个工具来帮助我们:
- 学习曲线
- 决策树对象Tree的属性
学习曲线是以参数的不同取值为横坐标,模型的结果为纵坐标的曲线。当模型的参数较少、且参数之间的相互作用较小时,我们可以直接使用学习曲线进行调参。但对于集成算法来说,学习曲线更多是我们探索参数与模型关系的关键手段。许多参数对模型的影响是确定且单调的,例如n_estimators,树越多模型的学习能力越强,再比如ccp_alpha,该参数值越大模型抗过拟合能力越强,因此我们可能通过学习曲线找到这些参数对模型影响的极限。我们会围绕这些极限点来构筑我们的参数空间。
当绘制学习曲线时,我们可以很容易找到泛化误差开始上升、或转变为平稳趋势的转折点。因此我们可以选择转折点或转折点附近的n_estimators取值,例如20,因此n_estimators的参数空间可以被确定为range(20,100,5),如果你比较保守,甚至可以确认为是range(15,25,5)。
- 决策树对象Tree
在sklearn中,树模型是单独的一类对象,每个树模型背后都有一套完整的属性供我们调用,包括树的结构、树的规模等众多细节。在之前的课程中,我们曾经使用过树模型的绘图功能plot_tree,除此之外树还有许多有用的属性。随机森林是树组成的算法,因此也可以调用这些属性。
reg_f = RFR(n_estimators=10,random_state=1412)
reg_f = reg_f.fit(X,y) #训练一个随机森林reg_f.estimators_ #一片随机森林中所有的树"""
[DecisionTreeRegressor(max_features='auto', random_state=1630984966),DecisionTreeRegressor(max_features='auto', random_state=472863509),DecisionTreeRegressor(max_features='auto', random_state=1082704530),DecisionTreeRegressor(max_features='auto', random_state=1930362544),DecisionTreeRegressor(max_features='auto', random_state=273973624),DecisionTreeRegressor(max_features='auto', random_state=21991934),DecisionTreeRegressor(max_features='auto', random_state=1886585710),DecisionTreeRegressor(max_features='auto', random_state=63725675),DecisionTreeRegressor(max_features='auto', random_state=1374343434),DecisionTreeRegressor(max_features='auto', random_state=1078007175)]
"""
#可以用索引单独提取一棵树
reg_f.estimators_[0]#调用这棵树的底层结构
reg_f.estimators_[0].tree_
#属性.max_depth,查看当前树的实际深度
reg_f.estimators_[0].tree_.max_depth #max_depth=None#如果树的数量较多,也可以查看平均或分布
reg_f = RFR(n_estimators=100,random_state=1412)
reg_f = reg_f.fit(X,y) #训练一个随机森林
d = pd.Series([],dtype="int64")
for idx,t in enumerate(reg_f.estimators_):d[idx] = t.tree_.max_depthd.describe()"""
count 100.000000
mean 22.250000
std 1.955954
min 19.000000
25% 21.000000
50% 22.000000
75% 23.000000
max 30.000000
dtype: float64
"""
相似的,我们也可以调用其他属性来辅助我们调参:
参数 | 参数含义 | 对应属性 | 属性含义 |
---|---|---|---|
n_estimators | 树的数量 | reg.estimators_ | 森林中所有树对象 |
max_depth | 允许的最大深度 | .tree_.max_depth | 0号树实际的深度 |
max_leaf_nodes | 允许的最大 叶子节点量 | .tree_.node_count | 0号树实际的总节点量 |
min_sample_split | 分枝所需最小 样本量 | .tree_.n_node_samples | 0号树每片叶子上实际的样本量 |
min_weight_fraction_leaf | 分枝所需最小 样本权重 | tree_.weighted_n_node_samples | 0号树每片叶子上实际的样本权重 |
min_impurity_decrease | 分枝所需最小 不纯度下降量 | .tree_.impurity .tree_.threshold | 0号树每片叶子上的实际不纯度 0号树每个节点分枝后不纯度下降量 |
#一棵树上的总叶子量
reg_f.estimators_[0].tree_.node_count#所有树上的总叶子量
for t in reg_f.estimators_:print(t.tree_.node_count)
"""
1807
1777
1763
1821
1777
1781
1811
1771
1753
1779
"""#每个节点上的不纯度下降量,为-2则表示该节点是叶子节点
reg_f.estimators_[0].tree_.threshold.tolist()[:20]"""
[6.5,5.5,327.0,214.0,0.5,1.0,104.0,0.5,-2.0,-2.0,-2.0,105.5,28.5,0.5,1.5,-2.0,-2.0,11.0,1212.5,2.5]"""#你怎么知道min_impurity_decrease的范围设置多少会剪掉多少叶子?
pd.Series(reg_f.estimators_[0].tree_.threshold).value_counts().sort_index()
"""
-2.0 9040.5 431.0 321.5 562.0 32... 1118.5 11162.5 11212.5 21254.5 11335.5 1
"""
pd.set_option("display.max_rows",None)
np.cumsum(pd.Series(reg_f.estimators_[0].tree_.threshold).value_counts().sort_index()[1:])
"""
1.0 32
1.5 88
2.0 120
2.5 167
3.0 189
3.5 208
4.0 224
4.5 249
5.0 258
5.5 271
6.0 276
6.5 287
7.0 302
7.5 307
8.0 313
8.5 321
9.0 326
9.5 334
10.0 335
10.5 343
11.0 346
11.5 348
.............
1118.5 855
1162.5 856
1212.5 858
1254.5 859
1335.5 860
"""
#从这棵树反馈的结果来看,min_impurity_decrease在现在的数据集上至少要设置到[2,10]的范围才可能对模型有较大的影响
import numpy as np
import pandas as pd
import sklearn
import matplotlib as mlp
import matplotlib.pyplot as plt
import time #计时模块time
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.model_selection import cross_validate, KFold, GridSearchCV
def RMSE(cvresult,key):return (abs(cvresult[key])**0.5).mean()
data = pd.read_csv(r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\House Price\train_encode.csv",index_col=0)
X = data.iloc[:,:-1]
y = data.iloc[:,-1]reg = RFR(random_state=1412)
cv = KFold(n_splits=5,shuffle=True,random_state=1412)
result_pre_adjusted = cross_validate(reg,X,y,cv=cv,scoring="neg_mean_squared_error",return_train_score=True,verbose=True,n_jobs=-1)
param_grid_simple = {"criterion": ["squared_error","poisson"], 'n_estimators': [*range(20,100,5)], 'max_depth': [*range(10,25,2)], "max_features": ["log2","sqrt",16,32,64,"auto"], "min_impurity_decrease": [*np.arange(0,5,10)]}
search = GridSearchCV(estimator=reg,param_grid=param_grid_simple,scoring = "neg_mean_squared_error",verbose = True,cv = cv,n_jobs=-1)
start = time.time()
search.fit(X,y)
print(time.time() - start)search.best_estimator_ #查看最佳参数
abs(search.best_score_)**0.5 #最佳评分ad_reg = RFR(n_estimators=85, max_depth=23, max_features=16, random_state=1412)
cv = KFold(n_splits=5,shuffle=True,random_state=1412)
result_post_adjusted = cross_validate(ad_reg,X,y,cv=cv,scoring="neg_mean_squared_error",return_train_score=True,verbose=True,n_jobs=-1)
RMSE(result_post_adjusted,"test_score")
"""
28572.070208366855
比默认参数的30571.26665524217要好很多了
"""
增量学习
通常来说,当一个模型经过一次训练之后,如果再使用新数据对模型进行训练,原始数据训练出的模型会被替代掉
model = RFR(n_estimators=3, warm_start=True) #支持增量学习
model2 = model.fit(X_fc,y_fc)
model2.estimators_"""
[DecisionTreeRegressor(max_features='auto', random_state=338470642),DecisionTreeRegressor(max_features='auto', random_state=1545812511),DecisionTreeRegressor(max_features='auto', random_state=740599321)]"""
model2.n_estimators += 2 #加两棵树给新数据集
model2 = model2.fit(X.iloc[:,:8],y)
model2.estimators_
"""
[DecisionTreeRegressor(max_features='auto', random_state=338470642),DecisionTreeRegressor(max_features='auto', random_state=1545812511),DecisionTreeRegressor(max_features='auto', random_state=740599321),DecisionTreeRegressor(max_features='auto', random_state=1633155700),DecisionTreeRegressor(max_features='auto', random_state=623929223)]"""