jasonfreak

一个懒惰的人,总是想设计更智能的程序来避免做重复性工作

使用sklearn进行集成学习——实践

系列

目录

1 Random Forest和Gradient Tree Boosting参数详解
2 如何调参?
  2.1 调参的目标:偏差和方差的协调
  2.2 参数对整体模型性能的影响
  2.3 一个朴实的方案:贪心的坐标下降法
    2.3.1 Random Forest调参案例:Digit Recognizer
      2.3.1.1 调整过程影响类参数
      2.3.1.2 调整子模型影响类参数
    2.3.2 Gradient Tree Boosting调参案例:Hackathon3.x
      2.3.2.1 调整过程影响类参数
      2.3.2.2 调整子模型影响类参数
      2.3.2.3 杀一记回马枪
  2.4 “局部最优解”(温馨提示:看到这里有彩蛋!)
  2.5 类别不均衡的陷阱
3 总结
4 参考资料


1 Random Forest和Gradient Tree Boosting参数详解

  在sklearn.ensemble库中,我们可以找到Random Forest分类和回归的实现:RandomForestClassifier和RandomForestRegression,Gradient Tree Boosting分类和回归的实现:GradientBoostingClassifier和GradientBoostingRegression。有了这些模型后,立马上手操练起来?少侠请留步!且听我说一说,使用这些模型时常遇到的问题:

  • 明明模型调教得很好了,可是效果离我的想象总有些偏差?——模型训练的第一步就是要定好目标,往错误的方向走太多也是后退。
  • 凭直觉调了某个参数,可是居然没有任何作用,有时甚至起到反作用?——定好目标后,接下来就是要确定哪些参数是影响目标的,其对目标是正影响还是负影响,影响的大小。
  • 感觉训练结束遥遥无期,sklearn只是个在小数据上的玩具?——虽然sklearn并不是基于分布式计算环境而设计的,但我们还是可以通过某些策略提高训练的效率。
  • 模型开始训练了,但是训练到哪一步了呢?——饱暖思淫欲啊,目标,性能和效率都得了满足后,我们有时还需要有别的追求,例如训练过程的输出,袋外得分计算等等。

  通过总结这些常见的问题,我们可以把模型的参数分为4类:目标类、性能类、效率类和附加类。下表详细地展示了4个模型参数的意义:

参数 类型 RandomForestClassifier RandomForestRegressor GradientBoostingClassifier GradientBoostingRegressor
loss 目标    

损失函数

● exponential:模型等同AdaBoost

★ deviance:和Logistic Regression的损失函数一致

损失函数

● exponential:模型等同AdaBoost

★ deviance:和Logistic Regression的损失函数一致

alpha 目标      损失函数为huber或quantile的时,alpha为损失函数中的参数   损失函数为huber或quantile的时,alpha为损失函数中的参数
class_weight 目标

类别的权值

     
n_estimators  性能

子模型的数量

● int:个数

★ 10:默认值

子模型的数量

● int:个数

★ 10:默认值

子模型的数量

● int:个数

★ 100:默认值

子模型的数量

● int:个数

★ 100:默认值

learning_rate 性能     学习率(缩减) 学习率(缩减)
criterion 性能

判断节点是否继续分裂采用的计算方法

● entropy

★ gini

判断节点是否继续分裂采用的计算方法

★ mse

   
max_features 性能

节点分裂时参与判断的最大特征数

● int:个数

● float:占所有特征的百分比

★ auto:所有特征数的开方

● sqrt:所有特征数的开方

● log2:所有特征数的log2值

● None:等于所有特征数

节点分裂时参与判断的最大特征数

● int:个数

● float:占所有特征的百分比

★ auto:所有特征数的开方

● sqrt:所有特征数的开方

● log2:所有特征数的log2值

● None:等于所有特征数

节点分裂时参与判断的最大特征数

● int:个数

● float:占所有特征的百分比

● auto:所有特征数的开方

● sqrt:所有特征数的开方

● log2:所有特征数的log2值

★ None:等于所有特征数

节点分裂时参与判断的最大特征数

● int:个数

● float:占所有特征的百分比

● auto:所有特征数的开方

● sqrt:所有特征数的开方

● log2:所有特征数的log2值

★ None:等于所有特征数

max_depth 性能

最大深度,如果max_leaf_nodes参数指定,则忽略

● int:深度

★ None:树会生长到所有叶子都分到一个类,或者某节点所代表的样本数已小于min_samples_split

最大深度,如果max_leaf_nodes参数指定,则忽略

● int:深度

★ None:树会生长到所有叶子都分到一个类,或者某节点所代表的样本数已小于min_samples_split

最大深度,如果max_leaf_nodes参数指定,则忽略

● int:深度

★ 3:默认值

最大深度,如果max_leaf_nodes参数指定,则忽略

● int:深度

★ 3:默认值

min_samples_split 性能

分裂所需的最小样本数

● int:样本数

★ 2:默认值

 

分裂所需的最小样本数

● int:样本数

★ 2:默认值

 

分裂所需的最小样本数

● int:样本数

★ 2:默认值

 

分裂所需的最小样本数

● int:样本数

★ 2:默认值

min_samples_leaf 性能

叶节点最小样本数

● int:样本数

★ 1:默认值

叶节点最小样本数

● int:样本数

★ 1:默认值

叶节点最小样本数

● int:样本数

★ 1:默认值

叶节点最小样本数

● int:样本数

★ 1:默认值

min_weight_fraction_leaf 性能

叶节点最小样本权重总值

● float:权重总值

★ 0:默认值

叶节点最小样本权重总值

● float:权重总值

★ 0:默认值

叶节点最小样本权重总值

● float:权重总值

★ 0:默认值

叶节点最小样本权重总值

● float:权重总值

★ 0:默认值

max_leaf_nodes 性能

最大叶节点数

● int:个数

★ None:不限制叶节点数

最大叶节点数

● int:个数

★ None:不限制叶节点数

最大叶节点数

● int:个数

★ None:不限制叶节点数

最大叶节点数

● int:个数

★ None:不限制叶节点数

bootstrap 性能

是否bootstrap对样本抽样

● False:子模型的样本一致,子模型间强相关

★ True:默认值

是否bootstrap对样本抽样

● False:子模型的样本一致,子模型间强相关

★ True:默认值

   
subsample 性能    

子采样率

● float:采样率

★ 1.0:默认值

子采样率

● float:采样率

★ 1.0:默认值

init 性能     初始子模型 初始子模型
n_jobs 效率

并行数

● int:个数

● -1:跟CPU核数一致

★ 1:默认值

并行数

● int:个数

● -1:跟CPU核数一致

★ 1:默认值

   
warm_start 效率

是否热启动,如果是,则下一次训练是以追加树的形式进行

● bool:热启动

★ False:默认值

是否热启动,如果是,则下一次训练是以追加树的形式进行

● bool:热启动

★ False:默认值

是否热启动,如果是,则下一次训练是以追加树的形式进行

● bool:热启动

★ False:默认值

是否热启动,如果是,则下一次训练是以追加树的形式进行

● bool:热启动

★ False:默认值

 presort 效率

 

   是否预排序,预排序可以加速查找最佳分裂点,对于稀疏数据不管用

● Bool

★ auto:非稀疏数据则预排序,若稀疏数据则不预排序

是否预排序,预排序可以加速查找最佳分裂点,对于稀疏数据不管用

● Bool

★ auto:非稀疏数据则预排序,若稀疏数据则不预排序

oob_score 附加

是否计算袋外得分

★ False:默认值

是否计算袋外得分

★ False:默认值

   
random_state 附加 随机器对象 随机器对象 随机器对象 随机器对象
verbose 附加

日志冗长度

● int:冗长度

★ 0:不输出训练过程

● 1:偶尔输出

● >1:对每个子模型都输出

日志冗长度

● int:冗长度

★ 0:不输出训练过程

● 1:偶尔输出

● >1:对每个子模型都输出

日志冗长度

● int:冗长度

★ 0:不输出训练过程

● 1:偶尔输出

● >1:对每个子模型都输出

日志冗长度

● int:冗长度

★ 0:不输出训练过程

● 1:偶尔输出

● >1:对每个子模型都输出

# ★:默认值

  不难发现,基于bagging的Random Forest模型和基于boosting的Gradient Tree Boosting模型有不少共同的参数,然而某些参数的默认值又相差甚远。在《使用sklearn进行集成学习——理论》一文中,我们对bagging和boosting两种集成学习技术有了初步的了解。Random Forest的子模型都拥有较低的偏差,整体模型的训练过程旨在降低方差,故其需要较少的子模型(n_estimators默认值为10)且子模型不为弱模型(max_depth的默认值为None),同时,降低子模型间的相关度可以起到减少整体模型的方差的效果(max_features的默认值为auto)。另一方面,Gradient Tree Boosting的子模型都拥有较低的方差,整体模型的训练过程旨在降低偏差,故其需要较多的子模型(n_estimators默认值为100)且子模型为弱模型(max_depth的默认值为3),但是降低子模型间的相关度不能显著减少整体模型的方差(max_features的默认值为None)。


 

2 如何调参?

  聪明的读者应当要发问了:”博主,就算你列出来每个参数的意义,然并卵啊!我还是不知道无从下手啊!”

  参数分类的目的在于缩小调参的范围,首先我们要明确训练的目标,把目标类的参数定下来。接下来,我们需要根据数据集的大小,考虑是否采用一些提高训练效率的策略,否则一次训练就三天三夜,法国人孩子都生出来了。然后,我们终于进入到了重中之重的环节:调整那些影响整体模型性能的参数。

2.1 调参的目标:偏差和方差的协调

  同样在《使用sklearn进行集成学习——理论》中,我们已讨论过偏差和方差是怎样影响着模型的性能——准确度。调参的目标就是为了达到整体模型的偏差和方差的大和谐!进一步,这些参数又可分为两类:过程影响类及子模型影响类。在子模型不变的前提下,某些参数可以通过改变训练的过程,从而影响模型的性能,诸如:“子模型数”(n_estimators)、“学习率”(learning_rate)等。另外,我们还可以通过改变子模型性能来影响整体模型的性能,诸如:“最大树深度”(max_depth)、“分裂条件”(criterion)等。正由于bagging的训练过程旨在降低方差,而boosting的训练过程旨在降低偏差,过程影响类的参数能够引起整体模型性能的大幅度变化。一般来说,在此前提下,我们继续微调子模型影响类的参数,从而进一步提高模型的性能。

2.2 参数对整体模型性能的影响

  假设模型是一个多元函数F,其输出值为模型的准确度。我们可以固定其他参数,从而对某个参数对整体模型性能的影响进行分析:是正影响还是负影响,影响的单调性?

  对Random Forest来说,增加“子模型数”(n_estimators)可以明显降低整体模型的方差,且不会对子模型的偏差和方差有任何影响。模型的准确度会随着“子模型数”的增加而提高。由于减少的是整体模型方差公式的第二项,故准确度的提高有一个上限。在不同的场景下,“分裂条件”(criterion)对模型的准确度的影响也不一样,该参数需要在实际运用时灵活调整。调整“最大叶节点数”(max_leaf_nodes)以及“最大树深度”(max_depth)之一,可以粗粒度地调整树的结构:叶节点越多或者树越深,意味着子模型的偏差越低,方差越高;同时,调整“分裂所需最小样本数”(min_samples_split)、“叶节点最小样本数”(min_samples_leaf)及“叶节点最小权重总值”(min_weight_fraction_leaf),可以更细粒度地调整树的结构:分裂所需样本数越少或者叶节点所需样本越少,也意味着子模型越复杂。一般来说,我们总采用bootstrap对样本进行子采样来降低子模型之间的关联度,从而降低整体模型的方差。适当地减少“分裂时考虑的最大特征数”(max_features),给子模型注入了另外的随机性,同样也达到了降低子模型之间关联度的效果。但是一味地降低该参数也是不行的,因为分裂时可选特征变少,模型的偏差会越来越大。在下图中,我们可以看到这些参数对Random Forest整体模型性能的影响:

  对Gradient Tree Boosting来说,“子模型数”(n_estimators)和“学习率”(learning_rate)需要联合调整才能尽可能地提高模型的准确度:想象一下,A方案是走4步,每步走3米,B方案是走5步,每步走2米,哪个方案可以更接近10米远的终点?同理,子模型越复杂,对应整体模型偏差低,方差高,故“最大叶节点数”(max_leaf_nodes)、“最大树深度”(max_depth)等控制子模型结构的参数是与Random Forest一致的。类似“分裂时考虑的最大特征数”(max_features),降低“子采样率”(subsample),也会造成子模型间的关联度降低,整体模型的方差减小,但是当子采样率低到一定程度时,子模型的偏差增大,将引起整体模型的准确度降低。还记得“初始模型”(init)是什么吗?不同的损失函数有不一样的初始模型定义,通常,初始模型是一个更加弱的模型(以“平均”情况来预测),虽说支持自定义,大多数情况下保持默认即可。在下图中,我们可以看到这些参数对Gradient Tree Boosting整体模型性能的影响:

2.3 一个朴实的方案:贪心的坐标下降法

  到此为止,我们终于知道需要调整哪些参数,对于单个参数,我们也知道怎么调整才能提升性能。然而,表示模型的函数F并不是一元函数,这些参数需要共同调整才能得到全局最优解。也就是说,把这些参数丢给调参算法(诸如Grid Search)咯?对于小数据集,我们还能这么任性,但是参数组合爆炸,在大数据集上,或许我的子子孙孙能够看到训练结果吧。实际上网格搜索也不一定能得到全局最优解,而另一些研究者从解优化问题的角度尝试解决调参问题。

  坐标下降法是一类优化算法,其最大的优势在于不用计算待优化的目标函数的梯度。我们最容易想到一种特别朴实的类似于坐标下降法的方法,与坐标下降法不同的是,其不是循环使用各个参数进行调整,而是贪心地选取了对整体模型性能影响最大的参数。参数对整体模型性能的影响力是动态变化的,故每一轮坐标选取的过程中,这种方法在对每个坐标的下降方向进行一次直线搜索(line search)。首先,找到那些能够提升整体模型性能的参数,其次确保提升是单调或近似单调的。这意味着,我们筛选出来的参数是对整体模型性能有正影响的,且这种影响不是偶然性的,要知道,训练过程的随机性也会导致整体模型性能的细微区别,而这种区别是不具有单调性的。最后,在这些筛选出来的参数中,选取影响最大的参数进行调整即可。

  无法对整体模型性能进行量化,也就谈不上去比较参数影响整体模型性能的程度。是的,我们还没有一个准确的方法来量化整体模型性能,只能通过交叉验证来近似计算整体模型性能。然而交叉验证也存在随机性,假设我们以验证集上的平均准确度作为整体模型的准确度,我们还得关心在各个验证集上准确度的变异系数,如果变异系数过大,则平均值作为整体模型的准确度也是不合适的。在接下来的案例分析中,我们所谈及的整体模型性能均是指平均准确度,请各位留心。

2.3.1 Random Forest调参案例:Digit Recognizer

  在这里,我们选取Kaggle上101教学赛中的Digit Recognizer作为案例来演示对RandomForestClassifier调参的过程。当然,我们也不要傻乎乎地手工去设定不同的参数,然后训练模型。借助sklearn.grid_search库中的GridSearchCV类,不仅可以自动化调参,同时还可以对每一种参数组合进行交叉验证计算平均准确度。

2.3.1.1 调整过程影响类参数

  首先,我们需要对过程影响类参数进行调整,而Random Forest的过程影响类参数只有“子模型数”(n_estimators)。“子模型数”的默认值为10,在此基础上,我们以10为单位,考察取值范围在1至201的调参情况:

# 左图为模型在验证集上的平均准确度,右图为准确度的变异系数。横轴为参数的取值。

  通过上图我们可以看到,随着“子模型数”的增加,整体模型的方差减少,其防止过拟合的能力增强,故整体模型的准确度提高。当“子模型数”增加到40以上时,准确度的提升逐渐不明显。考虑到训练的效率,最终我们选择“子模型数”为200。此时,在Kaggle上提交结果,得分为:0.96500,很凑合。

2.3.1.2 调整子模型影响类参数

  在设定“子模型数”(n_estimators)为200的前提下,我们依次对子模型影响类的参数对整体模型性能的影响力进行分析。

  对“分裂条件”(criterion)分别取值gini和entropy,得到调参结果如下:

  显见,在此问题中,“分裂条件”保持默认值gini更加合适。

  对“分裂时参与判断的最大特征数”(max_feature)以1为单位,设定取值范围为28至47,得到调参结果如下:

  

  “分裂时参与判断的最大特征数”的默认值auto,即总特征数(sqrt(784)=28)的开方。通过提升该参数,整体模型的准确度得到了提升。可见,该参数的默认值过小,导致了子模型的偏差过大,从而整体模型的偏差过大。同时,我们还注意到,该参数对整体模型性能的影响是近似单调的:从28到38,模型的准确度逐步抖动提升。所以,我们可考虑将该参数纳入下一步的调参工作。

  对“最大深度”(max_depth)以10为单位,设定取值范围为10到100,得到调参结果如下:

  随着树的深度加深,子模型的偏差减少,整体模型的准确度得到提升。从理论上来说,子模型训练的后期,随着方差增大,子模型的准确度稍微降低,从而影响整体模型的准确度降低。看图中,似乎取值范围从40到60的情况可以印证这一观点。不妨以1为单位,设定取值范围为40到59,更加细致地分析:

  有点傻眼了,怎么跟预想的不太一样?为什么模型准确度的变化在40到59之间没有鲜明的“规律”了?要分析这个问题,我们得先思考一下,少一层子节点对子模型意味着什么?若少的那一层给原子模型带来的是方差增大,则新子模型会准确度提高;若少的那一层给原子模型带来的是偏差减小,则新子模型会准确度降低。所以,细粒度的层次变化既可能使整体模型的准确度提升,也可能使整体模型的准确度降低。从而也说明了,该参数更适合进行粗粒度的调整。在训练的现阶段,“抖动”现象的发生说明,此时对该参数的调整已不太合适了。

  对“分裂所需的最小样本数”(min_samples_split)以1为单位,设定取值范围为2到11,得到调参的结果:

  我们看到,随着分裂所需的最小样本数的增加,子模型的结构变得越来越简单,理论