【Python机器学习实战】决策树和集成学习(一)——决策树原理
摘要:本部分对决策树几种算法的原理及算法过程进行简要介绍,然后编写程序实现决策树算法,再根据Python自带机器学习包实现决策树算法,最后从决策树引申至集成学习相关内容。
1.决策树
决策树作为一种常见的有监督学习算法,在机器学习领域通常有着不错的表现,决策树在生活中决策去做某件事时,会根据自己的经验考虑到多种因素,那么在程序逻辑中使用if~else的堆叠,决定最终结果的过程其实就算是决策树的一种体现,如下图(举个不太恰当的例子)。学术一点来说,决策树就是根据以往发生的事的概率,来评估风险,作出决策,通常根据以往事件发生的情况来构建的一种树形图。
构建决策树依据的是由概率所表示的数据的“混乱程度”,或者说是“确定性”,依据信息的不确定性将现有数据进行划分,即若某一种划分使得数据的不确定性变小,则这种划分就是有意义的,比如如下一组数据:
作业是否完成 | 天气 | 是否去打篮球 |
是 | 晴朗 | 是 |
否 |
晴朗 |
是 |
是 | 下雨 | 否 |
否 | 下雨 | 否 |
从数据来看,去不去打篮球的关键因素在于天气状况,作业完成情况对是否去打蓝球的影响不是很大,因此选用天气情况划分数据使最终数据的确定性更强(已经区分开了)。
而数据的不确定性概念属于信息论的知识,这里简要介绍一下:
通常度量数据的不确定性有两个指标:信息熵和基尼系数
(1)信息熵
熵原本是用来描述物质的混乱程度,借用到信息系统里则表示信息的不确定性,其可以用如下式表示:
信息熵越大表示信息的不确定性越强,想要把它搞清楚所需要的信息也就越多,H(y)越小意味着y就越规律,确定性越强。当log的底数为2时称之为比特,底数为e时称之为纳特。其函数图像如下:
(2)基尼系数
基尼系数是表示数据的纯度,其表达式为:
基尼系数表示样本集合中一个随机选中的样本被分错的概率。基尼指数越小表示集合中被选中的样本被分错的概率越小,也就说集合的纯度越高,反之,集合越不纯。即基尼系数越大表示数据的纯度越小,不确定性越强,基尼系数越小表示数据纯度越高,确定性越强。
(3)条件熵
有了信息熵之后,虽然能够反映出数据的确定性强弱,那么选取哪个特征能够减小样本数据的不确定性呢?这里就要用到条件熵的概念,条件熵是特征X关于y的信息量的大小,条件熵用H(y|X)表示,条件熵的计算公式为:
条件熵意味着在已知特征X的情况下Y的不确定性,其值越小,数据不确定性越小,意味着特征X对于数据的划分更加有用。
(4)信息增益
信息增益可以理解为特征X为信息的不确定性的减少所带来的价值,X越能为数据的不确定的减小价值贡献越大,则信息增益就越大,X就越能作为划分依据,因此,信息增益可以用下式计算:
X使得H(y|x)越小,表明不确定性越小,越能作为划分特征,因此选取特征时,信息增益越大越好。
关于信息熵、条件熵、信息增益、互信息可以用下图进行表示:
H(y|x)为条件熵,表示在变量x的条件下,y的不确定性;
H(x,y)是互信息,表示x,y的信息量之和;
G(y,x)=H(y)-H(y|x),表示信息增益,其值与互信息H(x,y)相同。
上面就是有关信息论的一些基本概念。有了上述一些基本概念后,就可以依据上述的信息增益和基尼系数进行决策树的构造了,决策树的构造类别也有很多种,常见的有ID3、C4.5和Cart决策树,下边分别介绍三种决策树的构造过程。
ID3决策树
ID3决策树就是就是利用信息增益进行特征选择和数据的划分的,其构造过程为从根节点开始每次选择信息增益最大的特征作为划分标准,直到满足条件即停止。信息增益的计算过程如下:
比较各个特征值下的信息增益,选取信息增益最大的那个特征作为划分依据进行树的分裂即可,下面结合具体数据简单描述上述过程。借用《Python机器学习实战》书中的数据,数据如下:
数据中共有4个特征,分别为{颜色,大小,人员属性,动作},类别Y为结果{爆炸,不爆炸},那么初次划分节点从4个特征中选取一个作为划分标准,计算每个特征的信息增益:
首先是Y的信息熵H(y):
然后计算四个特征的条件熵:
因此选取大小或者动作作为节点的划分标准,这里不妨选择气球大小作为划分数据标准,将数据切割为两部分:
去掉气球大小的属性,剩余数据集为:
小气球 大气球
然后再根据另外三个特征分别对两个节点NodeA和NodeB再次进行分割,以此类推,直到某个子节点属于同一类别即停止分裂,那么最终获得的树的结构如图所示:
以上便为ID3决策树的算法过程,然而ID3算法存在一定的缺陷,其在选取信息增益最大作为划分标准时往往偏向于选择取值较多的那个特征去作为划分依据,比如有一个样本集,样本数量为15,其中一个特征有15个不同的值,那么在计算条件熵时条件熵为0,信息增益最大,那在进行节点分裂时,会分出15个分支,形成一棵矮胖的决策树,这显示是不合理的,这也是正是ID3为什么不能用于划分连续型特征的原因,因此在ID3的基础上提出了改进的算法,此为C4.5算法。
C4.5决策树
C4.5是在ID3决策树的基础上,避免因特征取值较多而偏向于选取该特征作为分裂标准的不合理性,因此,C4.5将信息增益比作为树分裂的依据,信息增益比的计算公式如下:
在信息增益的基础上除上特征X的信息熵,当特征X的取值较多时,那么H(X)的值就会增大,从而使得信息增益比减小,具体计算过程就不再赘述。
同时C4.5可以用于处理连续型特征,其做法是将某个特征取值按照大小进行排序:a1,a2,...,am,然后将相邻两个数取平均值,将特征划分成m-1个值。对于这m-1个点,分别计算以该点作为二元分类点时的信息增益比,选择信息增益比最大的点作为该连续特征的二元离散分类点,注意,与离散特征不同的是,如果当前节点为连续属性,则该属性后面还可以参与分裂(有待考究,有见到过离散变量出现重用的情况)。然而,这种做法同样也会产生一些缺点,进行二叉分支时,可能会分出一个比较小的Node出来,而另一个Node则会非常庞大,在后续进一步分裂时这个特征还可以再分裂,那么会不断地向下生成一个较深的树,最终造成过拟合现象。
CART决策树
CART决策树的分裂规则是采用基尼系数的大小作为特征选择和树分裂标准的,采用该指标能够简化熵模型中的对数运算,同时又保留了熵的含义。前面提到基尼系数越大数据越不纯,相反,基尼系数越小数据就越纯,对于特征的选择就越好,基尼系数的计算公式如下:
对于特征X,假设X有2个不同的值,根据X将样本划分成两块D1和D2,那么有:
这里需要说明的是,CART决策树是强制二叉树,即无论特征X有多少个值,在进行分裂时只会分裂出两个节点,这与ID3和C4.5有所不同,在ID3和C4.5中,若特征X有3个取值,选择该特征进行划分时会划分出三个节点,而在CART中相当于ovr的思想。比如特征X取值{A1,A2,A3},那么在进行特征分裂时,会选择{A1}、{A2,A3}和{A2}、{A1,A3}和{A3}、{A1,A2}的组合分别计算基尼系数,若计算出{A2}、{A1,A3}组合的基尼系数最小,则将会分裂出两个节点,此时由于特征A并没有完全分开,在后面进行分裂时,该特征将会进一步重用。
同时CART算法不但能够用于分类,还可以对连续型数据做回归,称之为CART回归树。在分类树中采用的是基尼系数进行度量,而在回归树中,采用常用的和方差的度量方式,即对于特征X,若划分点为xp,xp将数据划分成两块D1和D2,那么要使两块数据的和方差最小,即:
其中cjp为两块样本数据输出的平均值。
回归树的预测最终根据叶子节点的平均值或者中位数作为预测结果。下面给出CART算法决策树的构建流程:
"""
输入是训练集D,基尼系数的阈值,样本个数阈值。
输出是决策树T
- 对于当前节点的数据集D,如果样本个数小于预设阈值或没有特征,则返回决策子树,停止递归;
- 计算数据集的基尼系数,若基尼系数小于预设值,则返回决策子树,停止递归;
- 计算当前节点现有的各个特征的每个特征值对数据集D的基尼系数,选取基尼系数最小的特征值x及其特征X,根据这个特征值和特征,将数据集D划分为两部分D1和D2,分裂出两个节点Node1和Node2,分别为左右节点;
- 对Node1和Node2分别递归上述过程,生成决策树
"""
决策树的剪枝
最近又看了一下决策树有关内容,之前对于剪枝部分的内容理解不是很深刻,这里重新看了一些资料,并在此对之前的内容进行从新整理,并对之前理解有失偏颇的地方进行更正。
前面在C4.5中提到对于连续型数据进行划分时会逐渐向下发展成很深的树,造成过拟合,同理,对于其他的树的构建,理论上只要树的深度足够深,就能够完全拟合训练数据,但这也造成了模型在测试数据上的泛化能力,造成过拟合现象。因此在构建决策树时或构建完成后需要进行剪枝操作,防止过拟合。剪枝可以分为预剪枝和后剪枝。
预剪枝实际上就是在树的分裂过程中预设的节点停止分裂的条件,在实际应用中通常不多见。
后剪枝是指在树生成后,剪掉树的的多余的节点,降低模型的复杂度。通常做法分为两种:(1)利用交叉验证,产生所有可能剪枝后的决策树,验证剪枝后的决策树的泛化能力,选出最好的一颗;(2)加入正则化,即综合考虑不确定性和模型的复杂度,加上一个损失,作为新的分别标准重新度量,再利用交叉验证,验证剪枝后的决策树的泛化能力,选出最好的一颗。
上面的表述本身是没有问题的,后来看了一下后剪枝的方法,严格来说,后剪枝的方法有三种:
(1)错误率降低剪枝法(Reduced-Error Pruning,REP)
这种方法就是通过一个新的验证集来解决过拟合的问题,原理也十分简单,就是对每个父节点,将其下的叶子节点剪掉,从而得到一个新的树,
如果新的树在新的测试集上正确率更高,那么就剪掉叶子节点,否则就保留。
REP是一种自下而上的方法,即从下向上,按照上面的方法,知道没有父节点可以被剪掉,算法即停止。
举个例子:
对上面这样一棵树进行剪枝,剪枝的过程如下:
1> 从节点4开始,首先将8,9合并到成为4(如果是分类树,4的类别属于8、9投票决定,如果是回归树,则4的取值为8,9的平均值)。合并后如果在新的测试集上准确率提高(误差减小),则进行合并,否则不合并;
2>然后是节点2和节点3,将(4)、5、8、9合并成为2,将6、7合并成为3,按照上述方法,查看合并后在新的测试集上的表现。
3>到达根节点,没有可以剪枝的了,停止。
对于回归树的剪枝的实现代码如下:
# 判断该节点是否是叶子节点 def is_Tree(obj): return type(obj).__name__ == 'dict' # 计算合并后的平均值 def getMean(tree): if is_Tree(tree['right']): tree['right'] = getMean(tree['right']) if is_Tree(tree['left']): tree['left'] = getMean(tree['left']) return (tree['left'] + tree['right'])/2 # 剪枝 def prune(tree, testData): if shape(testData)[0] == 0: return getMean(tree) if is_Tree(tree['left']) or is_Tree(tree['right']): lSet, rSet = bin_split_dataSet(testData, tree['spInt'], tree['spVal']) if is_Tree(tree['left']): tree['left'] = prune(tree['left'], lSet) if is_Tree(tree['right']): tree['right'] = prune(tree['right'], rSet) if not is_Tree(tree['left']) and not is_Tree(tree['right']): print(11111) lSet, rSet = bin_split_dataSet(testData, tree['spInt'], tree['spVal']) errorNoMerge = sum(power(lSet[:, -1] - tree['left'], 2)) + sum(power(rSet[:, -1] - tree['right'], 2)) treeMean = (tree['left'] + tree['right'])/2 errorMerge = sum(power(testData[:, -1] - treeMean, 2)) if errorMerge < errorNoMerge: print('merging') return treeMean else: return tree else: return tree
(2)悲观剪枝法(Pessimistic Error Pruning,PEP)
上面REP的方法必须要有新的测试集,PEP则不需要新的测试集就可以进行剪枝,PEP主要是根据剪之后的错误率来进行剪枝的,是一种自上而下的剪枝方法。
由于PEP不需要新的测试集来验证剪枝前后的错误率,那么利用训练集来进行验证剪枝的话,错误率一定会上升,因此需要加上一个惩罚项0.5。
假设某个叶节点被错分的样本数为E,该节点包含N个样本,那么错误率为:
假设整个子树的叶子节点为m个,那么总的错误率为p:
假设子树的每一个样本被误判服从二项分布B(N,p),在剪枝前,误判的期望为:
误判的标准差为:
然后进行剪枝,对子树进行剪枝之后,即把子树替换成1个叶节点,该叶子节点的误判次数是一个伯努利分布,误判率e=(E+0.5)/N,那么误判的均值为:
当子树的误判个数大于剪枝后的叶子结点误判个数的一个标准差之后,就决定剪枝,即:
下面是一个例子:
比如对于T4这颗子树,每个叶子结点的数字代表类别的样本数量,比如T8中,为3和2,说明该叶子节点有3个类别1的样本和2个类别2的样本,那么最终该叶子结点属于类别1。
对T4进行剪枝,先计算剪枝前,样本总数为16,其中误分类的包括T8中有2个误分类,T9中有0个误分类,T7中有3个误分类,那么:
剪枝后,即将T7、T8、T9合并到T4中,T4有9个类别1,7个类别2,那么误分类数目就为7,则:
由于7.5-1.96<6.5,因此进行剪枝。
(3)代价复杂度剪枝(Cost-Complexity Pruning,CCP)
首先先来看剪枝时的度量损失,对于任意在节点t的一颗子树T,损失函数表示为:
其中α表示正则化参数,与线性回归的一样,C(Tt)表示原度量标准,分类树中为基尼系数,回归树中为和方差,|Tt|为子树T的叶子结点个数。α=0表示未剪枝,α→∞时,表示仅保留根节点。
如果此时将其剪掉,仅保留根节点,那么损失变为:
当α=0或很小时,则有,当α增大到一定程度时会出现:
此时α满足下式:
此时表示Tt和T具有相同的损失函数,但是T的节点更少,因此可以对Tt进行剪枝操作,剪掉其他的叶节点,Tt成为了一个叶子结点。
C(T)为节点T的错误代价,
r(T)表示节点T的错分样本率;p(t)表示节点T内样本占总样本比例。
通过求取设置不同的α,从不剪枝到剪至只剩1个根节点,记为 {T0,T1,T2,...,Tn},通过交叉验证的方法选出最好的决策树作为最终的结果。
下面给出决策树剪枝的算法过程:
"""
输入建立好的完整CART决策树T0
输出最优的决策树Tα
- 初始化k=0,T=T0,最优子树合集为T={T0},α=∞;
- 从叶子节点开始从下向上计算各内部节点t的训练误差损失函数Cα(Tt),叶子节点的个数|Tt|,以及正则化的阈值α:
- αk=αmin;自上而下访问子树t的内部节点,若满足:
则进行剪枝,并决定叶节点t的,若是分类树,类别属于概率最高的类别,若是回归树,则是所有样本输出的均值,这样得到了αk对应的最优子树Tk,将放入子树Tk集合T中;
- k=k+1,T=Tk,如果T不是由根节点单独组成的树,则递归执行上述步骤,否则就得到了最优子树集合T;
- 利用交叉验证在T中选出最优的子树Tα。
"""
下面举个CCP的例子:
如图所示,图中每个节点包含了该节点的样本信息,比如T8节点,第一行1、0属于样本类别,那么T8中有4个类别1和2个类别0的样本,那么最终T8投票属于类别1。
假设总样本数量为100,那么从下到上,依次寻找非叶节点,从T5开始,那么:
计算得到α:
同理T6计算得到的α=0.01,由于0<0.01,所以我们剪掉T5,得到一颗新的树。
上面就是几种剪枝的方法。
剪枝部分参考资料:https://blog.csdn.net/weixin_43216017/article/details/87534496
至此,决策树的算法基本原理这里已经简单介绍完了,其中涉及较多原理部分可能需要进一步深挖,目前的层面就先到这里了,后面回头看的时候再深入理解,进一步补充,接下来将根据算法原理对决策树进行逻辑编写,然后借助一些数据集去建立模型和实现,最后一部分介绍一下决策树与集成学习的关系,并对集成学习的原理进行简要介绍。
参考:
《Python机器学习实战》