决策树在MLib中的实现解析

决策树作为一种分类回归算法,在处理非线性、特征值缺少的数据方面有很多的优势,能够处理不相干的特征,并且对分类的结果通过树的方式有比较清晰的结构解释,但是容易过拟合,针对这个问题,可以采取对树进行剪枝的方式,还有一些融合集成的解决方案,比如随机森林RandomForest、GBDT (Gradient Boost Decision Tree)等,对于随机森林、GBDT在后面的章节进行介绍

模型的训练过程其实是决策树的构造过程,它采用自顶向下的递归方式,在决策树的内部结点进行属性值的比较,并根据不同的属性值判断从该结点向下分支,进行递归进行划分,直到满足一定的终止条件(可以进行自定义),其中叶结点是要学习划分的类。在当前节点用哪个属性特征作为判断进行切分(也叫分裂规则),取决于切分后节点数据集合中的类别(分区)的有序(纯)程度,划分后的分区数据越纯,那么当前分裂规则也合适。衡量节点数据集合的有序无序性,有熵、基尼Gini、方差,其中熵和Gini是针对分类的,方差是针对回归的。

这里介绍一下常用的熵以及信息增益。

熵代表集合的无序性的参数,熵越大,代表越无序、越不纯。熵的公式如下:

其中c表示类别,是样本集合中属于类别i的概率

在决策树分类中,一般是用信息增益infoGain来作为决策树节点特征属性划分的依据,采用使得信息增益最大的属性作为数据划分的度量依赖。信息增益infoGain定义如下:

其中V(A)代表属性A的分区,S代表样本集合,是S中属性A的值属于v分区的样本集合

决策树的算法实现在学术界有ID3,C4.5,CART等, ID3采用信息增益作为属性选择的度量,参见上面的Gain,这种方式的一个缺点是在计算信息增益时,倾向于选择具有大量值的属性,因此提出了C4.5的基于信息增益率的度量,而CART使用基尼Gini指数作为属性选择的度量,这些算法之间的差别主要包括在训练创建决策树过程中如何选择属性特征,以及剪枝的机制处理。这些算法相关的定义可以参考相关的材料,这里不做介绍了。

Spark MLlib对决策树提供了二元以及多label的分类以及回归的支持,支持连续型和离散型的特征变量。这里的决策树是一颗二叉树的,因此信息增益infoGain就为:

这里讲一下MLlib中的binsplit概念,split为切割点,对应二叉的决策树,bin为桶或者箱子数,一个split把数据集划分成2个桶,所以binsplit2倍。

在决策树的训练时,需要两个重要的参数,

           maxBins:每个特征分裂时,最大划分(桶)数量

            maxDepth:树的最大高度

MLlib中,基本的样本训练决策树的构建流程为:

寻找所有特征的可能的划分split以及桶信息bin,针对每次划分split,在spark executors上计算每个样本应该属于哪一个bin,后聚合每一个bin的统计信息,在Drivers上通过这些统计信息计算每次split的信息增益,并选择一个信息增益最大的分割split,按照该split对当前节点进行分割,直到满足终止条件。

为了防止过拟合,需要考虑剪枝,这里采用的是前向剪枝,当任一以下情况发生,MLlib的决策树节点就终止划分,形成叶子节点:

1、 树高度达到maxDepth

2、 minInfoGain,当前节点的所有属性分割带来的信息增益都比这个值要小

3、 minInstancesPerNode,需要保证节点分割出的左右子节点的最少的样本数量达到这个值

针对下面的一个样本集合:

age         revenue   stdudent     ….                       Label
 
youth        1         yes                                    1
middle       8         no                                     1
senior       2         no                                     0
senior       7         yes                                     0
middle       8         no            ….                        1
……


其中age,student是离散型的变量,revenue是连续变量

选择哪个特征进行划分,需要针对当前节点数据集合的每一个特征,求得信息增益,而根据公式3的信息增益的计算,需要寻找切分点split来计算分区;对当前特征的每一个划分进行对应的信息增益计算,然后取当前特征的最大的信息增益值作为该特征的信息增益;依次求候选的特征的信息增益,再选取具有最大的信息增益的那个候选特征作为当前节点的属性选择度量即可。

对于连续型的特征变量,从实现上来讲,需要对样本中的当前特征值排序,把每一个值都可能作为切分点split_poin,那么树的左边结合为A<=split_poin,右边集合为>split_poin;但是在实际的场景中样本量往往多到对特征值的排序影响系统的开销,做法是对样本进行抽样,对抽样的样本对于这一维度的特征值做排序,然后选择处于分位位置上的值作为分割值。

比如例子中的revenue特征,假设一共9个样本,采用3分位,那么分割点有以下两种候选的切分点。

                1,1,2,|4,4,6,|7,8,8,

对于离散的分类变量,如若特征有m个值,最多   种候选的划分split。在二元分类和回归的情况下,通过根据每个值对应的label的比例对m个特征值进行排序,可以把候选的划分split减少到m-1个。比如上面例子中的age,特征值为youth、middle、senior,假设youth对应的label为1的占比0.1,middle为0.7,senior为0.2,那么候选的划分为youth|senior,middle或者youth,senior|middle。 label 为多个值 ( 多分类 ) 的情况下,对于 fearture 当做无序的情况, split 划分为  ,否则为 m-1 split 划分;是否当做无序,这里有一个判断的标准,
//取样本数量和maxBin的最小值  
val maxPossibleBins = math.min(strategy.maxBins, numExamples).toInt
//根据maxPossibleBins,利用已下的公式(该公式是无序的公式2* 2^{M-1}-1的反向推导 ),来求m值
val maxCategoriesForUnorderedFeature =
        ((math.log(maxPossibleBins / 2 + 1) / math.log(2.0)) + 1).floor.toInt
//当前的特征数量小于m值,则认为无序
if (numCategories <= maxCategoriesForUnorderedFeature)

利用spark的分布式、高效迭代的计算框架实现对MLlib决策树的训练,有一些可以提及关键的优化点:

对于查找分割点split的并行操作,不是在一个node节点级别的并行,而是在树的level级别的并行,对于处于同一层次的节点,分割点的查找是同时并行操作的。也就是查找次数的复杂度依赖于层级L,而不是节点数2L次方-1,这样可以减少IO、计算、以及相关的通信开销。

前面讲到对于连续的特征,用排序后的唯一特征值作为计算最佳分割点的候选,对于大量的分布式的样本集,这个开销是很大的。为了提高这方面的性能,同时也不会严重降低准确度,MLlib决策树采用分位数作为分割点候选。

     查找每个节点最佳的分割点split,早期的版本是用map和reduce实现,而目前的实现是通过利用预先计算的分割点候选避免了map操作步骤,来提高计算和通信的开销。

      对于一些统计信息的计算都是在桶bins中进行的,由于预先对这些样本中bin的信息进行计算,勿需再每次迭代中计算,节省了计算开销。






你可能感兴趣的:(机器学习(广告,推荐,数据挖掘),spark)