机器学习笔记(四)决策树原理和实践

一、背景

在上一篇文章中,我们讲解了线性回归和逻辑回归的相关数学推导和Python实现,本篇文章我们将继续下一个机器学习中一个非常重要的模型——决策树的学习。决策树之所以叫决策树,是和它的原理紧密相关的,看下面这个场景,是不是很熟悉。首先看天气情况,然后根据天气情况做下一步决定,如果天气好,再看游泳馆是否开放,如果天气不好,再视父母是否在家来做决定,最终会有三种选择,或者可以认为是一个三分类问题。这就是一个典型的决策树场景,树的深度是3,从根节点(天气)开始,根据每一步的条件,决策下一步的计划(非叶子节点/内部节点),最终每条路都会走到一个叶子节点(游泳、看电视、去外婆家)。例如,今天天气好,父母在家,我们就知道今天是要去外婆家。

图1

上面我们已经引入了一个决策树的背景案例,不过这个决策树是已知的,我们只是根据这个决策树来决定(推理/预测)最终的行为,但是这个决策树是怎么来的呢?这和我们的机器学习又有什么关系呢?我们看下面一组数据,某化妆品店统计了十位顾客的属性信息以及是否会购买化妆品的数据,详细如下:

列名性别年龄是否购买
1青年
2中年
3中年
4青年
5青年
6青年
7中年
8青年
9青年
10中年

从上面这个表格中可以看出什么规律呢?统计可以发现,从性别看,所有的女性都购买了产品,所有的男性都没有购买,从年龄看,青年里面3人购买,3人未购买,中年里面3人购买,1人未购买,如果用决策树的形式表达出来,可以有下面两种:

图2

如果需要构建一个决策树模型,肯定会选择上图的(a),而不是(b),为什么呢?因为根据性别就可以直接得出结果了,没必要先判断年龄,再判断性别。换句话说,(a)树的深度更浅。这就是一个非常简单的构建决策树模型的过程,当然实际中的数据远比上面更复杂,比如会存在连续值数据,而接下来我们要详细介绍的决策树算法就是为了要解决在复杂数据场景下,如何构建一个更加合理的决策树模型的过程。一旦构造好了决策树,那么分类或者预测任务就很简单了,只需要走一遍决策树流程到叶子节点就ok了。

二、决策树模型

2.1 决策算法

建立决策树模型,非常重要的一步就是怎么构建决策树,换句话说就是哪个特征做父节点?哪个特征做子节点?关于特征决策的方法也有多种,主要有:ID3、C4.5、CART等。

2.1.1 信息熵

在第一章中我们提到了采用性别作为第一个判决条件比年龄更好,因为采用性别一次就可以划分所有结果,那么有没有一个量化值来衡量这个好坏呢?这里就引出了一个概念——信息熵。在介绍信息熵之前,先看一个例子。假如有人告诉你明天太阳从东边升起,你的感觉是什么?这TM不是废话吗?因为明天太阳从东边升起的概率可以认为是1,或者可认为是一个必然事件,所以这句话没什么意义。如果有人告诉你下期双色球的中奖号码,这还是废话吗?当然不是,因为这句话很有用,换句话这句话的价值很大,因为猜中双色球的概率是百万分之一,告诉你一个百万分之一的概率事件,当然这句话的信息量就很大。在信息论中,随机离散事件出现的概率存在着不确定性。为了衡量这种信息的不确定性,信息学之父香农引入了信息熵的概念,并给出了计算信息熵的数学公式:

其中,n为可能的所有类别数(例如抛硬币正反两种),p(xi)表示类别xi发生的概率。一个事件的不确定性越大,则信息熵越大,反之亦然。我们在决定哪个特征做根节点的时候,就可以通过信息熵来评判了,信息熵越小越好。计算上面案例的信息熵:

  • 性别的信息熵:\frac{6}{10}[-(1*log1+0*log0)] + \frac{4}{10}[-(0*log0 + 1*log1)] = 0     注:不考虑log0的不合理性
  • 年龄的信息熵:\frac{6}{10}[-(\frac{1}{2}*log\frac{1}{2}+\frac{1}{2}*log\frac{1}{2})] + \frac{4}{10}[-(\frac{3}{4}*log\frac{3}{4}+\frac{1}{4}*log\frac{1}{4})]

显然性别的信息熵更小,所以应该把性别作为根节点。

2.1.2 ID3

ID3的核心思想就是2.1节讲述的信息熵。看下面这个案例,现有14天不同情况下是否去打篮球的数据信息,现基于这些数据构造一个决策树:

图3

4个特征,首先选择谁来做根节点呢?根据上图我们首先计算不同特征的信息熵:

  • 原始数据信息熵:-(\frac{9}{14}*\log \frac{9}{14} + \frac{5}{14}*\log \frac{5}{14}) = 0.940
  • outlook的信息熵:\frac{5}{14}*(-\frac{2}{5}*log\frac{2}{5} - \frac{3}{5}*log\frac{3}{5}) + \frac{4}{14}*0 + \frac{5}{14}*(-\frac{3}{5}*log\frac{3}{5} - \frac{2}{5}*log\frac{2}{5}) = 0.693,信息增益为:0.940 - 0.693 = 0.247

其他三个特征的信息增益计算方法和outlook一样,计算可以发现outlook的信息增益最大,因此选择outlook作为根节点,然后在outlook的每个分支中再分别计算其他三个特征的信息熵,选择信息增益最大的最为二级节点,依次进行即可。

2.1.3 C4.5

上面的ID3决策算法似乎很好的解决了特征决策问题,但是我们来看另外一个问题,在图3(a)中还有一列id项,假如id项也是一列特征,计算id列的信息熵会怎样呢?因为id是不重复的,每个id只对应一条数据,也就是play只有一个结果,显然信息熵为0,那是不是意味着我们应该选择id项作为根节点呢?显然也是不合理的,这里的id虽然是一个特例,但是实际中也不乏这种类别很多的特征。因此,针对这个问题,又提出了C4.5信息增益率决策算法。计算方法如下:

其中,Gain_ratio(D,a)表示特征a在集合D上的信息增益率,Gain(D,a)表示特征a在集合D上的信息增益,IV(a)表示特征a自身的信息熵。换句话说,信息增益率就是用信息增益再比上特征自身的信息熵。对于图3案例的id列:

IV(id) = -14*(\frac{1}{14} * log\frac{1}{14}), Gain_ratio(D,id) = \frac{0.940}{IV(id)} ,

因为IV(id)大,就导致信息增益率变小。从上面的计算公式可以发现信息增益率会偏向取值类别较少的特征,因此C4.5决策树先从候选划分属性中找出信息增益高于平均水平的属性,在从中选择信息增益率最高的。

2.1.4 CART

CART是另外一种决策算法,又名分类回归树,是在ID3的基础上进行优化的决策树,那么CART是如何评价节点的纯度呢?核心思想是利用GINI系数来作为衡量标准。如果是分类树,CART采用GINI值衡量节点纯度;如果是回归树,采用样本方差衡量节点纯度。本节我们主要讨论分类问题,回归问题后文再继续讨论。节点越不纯,节点分类或者预测的效果就越差。需要注意的是CART是一棵二叉树,即使某个特征存在两个以上的类别。

其中k是数据集的分类类别数,pk是按照特征划分以后的子数据集中每个类别的概率。CART在选择根节点特征的时候,会将一个特征的其中一个类别独立作为一个节点,其他的离散值生成另外一个节点即可。这种分裂方案有多少个离散值就有多少种划分的方法,举一个简单的例子:如果某离散属性一个有三个离散值X,Y,Z,则该属性的分裂方法有{X}、{Y,Z},{Y}、{X,Z},{Z}、{X,Y}三种情况,然后分别计算每种划分方法的基尼值或者样本方差确定最优的方法。CART构建决策树的过程如下:

  1. 对于数据集D中的任意一个特征A,对其所有类别中的任意一个类别a,根据此值将训练样本切分为D1和D2两部分,然后根据上式计算A=a基尼指数。
  2. 在所有可能的特征以及所有可能的值里面选择基尼指数最小的特征及其切分点作为最优的特征及切分点,将训练数据集分配到两个子结点中。
  3. 递归的调用1、2直到全部数据集分到叶子节点或者满足停止条件。计算停止的条件可能是:结点中的样本数小于阈值、样本集的GINI指数小于阈值或者特征已经用完等等。

单纯的文字总是难以理解的,我们仍然以图3的数据来讲解CART。首先分析outlook特征,有三种取值,分别计算三个类别的GINI系数:

Gini(outlook = sunny) = \frac{5}{14}[1-(\frac{2}{5})^{2}-(\frac{3}{5})^{2}] + \frac{9}{14}[1-(\frac{7}{9})^{2}-(\frac{2}{9})^{2}]

Gini(outlook = overcast) = \frac{4}{14}[1-1^{2}-0^{2}] + \frac{10}{14}[1-(\frac{5}{10})^{2}-(\frac{5}{10})^{2}]

同理,计算所有特征类别的GINI系数即可,然后选择GINI系数值最小的特征类别作为根节点。然后在两个子数据集中,再分别计算剩余的特征类别GINI系数即可。通过前面的讨论可以发现CART树相对于C4.5有如下优点:

  1. CART 算法的二分法可以简化决策树的规模,提高生成决策树的效率,不会像C4.5树的规模那么大
  2. CART 使用 Gini 系数作为变量的不纯度量,减少了大量的对数运算
  3. C4.5 只能分类,CART 既可以分类也可以回归,这个问题我们后面会讨论

2.2 处理连续型数据

上面我们介绍了决策算法对于离散型变量(有限类别特征)的处理方法,但是在实际任务中往往存在许多连续型特征变量,例如年龄、高度、密度、距离等等,对于连续型特征,因为数目众多,甚至不能全部枚举,因此分别计算每个特征类别的信息增益方法就不再适用。鉴于上述情况,C4.5对于ID3的改进,并不仅仅只是信息增益率,还有对连续型数据的处理。主要处理手段就是离散化,常用的离散化策略是二分法。例如,有一组原始特征数据D = [3,7,5,9,2,7,1,4,6,1,8,3],处理步骤如下:

  1. 对原始特征去重后的n个值按照从小到大进行排序 A = [1,2,3,4,5,6,7,8,9],这里n=9
  2. 取任意两个相邻数据的中间点集合 T = [1.5,2.5,3.5,4.5,5.5,6.5,7.5,8.5],即把A中任意相邻区间中间点作为候选划分点 T_{i} = \frac{A_{i}+A_{i+1}}{2} | 0<i<n
  3. 依次遍历T中元素作为划分点,把数据集D划分为两个子数据集,分别是  <= T_{i} 和 > T_{i} 的数据,并计算划分后的信息增益,选择信息增益最大的划分点作为该类特征的最优候选划分点
  4. 把3中计算的信息增益与其他特征的信息增益比较取信息增益最大的即可。

具体的案例这里不再列举,可以参考这篇文章的案例详细讲解。另外,需要注意的是,连续型特征作为内部节点划分完毕后,在后续的子节点中,仍然是可以作为候选划分点处理的。例如,对于年龄采用30岁作为根节点划分比较合理,二级节点采用性别划分信息增益最大,在三级节点年龄仍然有可能再次成为最佳划分点,例如,可能20岁作为划分点的信息增益比其它特征更大,那么三级节点就是年龄特征(20岁)。

前面依次遍历划分点二分类方法有效解决了连续值特征问题,但是该方法也有一个比较明显的缺点,就是计算量大,需要计算所有划分点的信息增益,因此也有人提出了相关的改进方法。Fayyad等人证明了:无论用于学习的数据集有多少个类别,不管类别的分布如何,连续型属性的最佳分隔点总在边界点处。由此,在选择划分点的时候,就不需要计算所有划分点的信息增益,只要计算边界点的信息增益即可。例如:上文中的2、3、4、5都属于类别A,6、7都属于类别B,则只要计算5.5这个划分点的信息增益即可,详情可以参考《决策树C4.5连续属性分割阈值算法改进及其应用》。CART对连续值特征的处理和C4.5一样,也是离散划分不同的候选分隔点,只是评价标准是GINI系数,不再是信息增益率。

2.3 缺失值数据处理

数据缺失是机器学习建模过程中经常遇到的一个问题,在特征工程中针对缺失值也有专门的处理方法。主要包括:(1)如果缺失值数据比较少(占比低)可以考虑直接丢弃;(2)使用该特征值的众数、中位数、平均值等数据来填充;(3)把缺失值记录单独作为一种特征类别处理等。

缺失值记录对于决策树来说主要存在两个问题:(1)缺失的这条记录应该怎么进行特征选择,换句话说就是如何计算该特征的信息增益或者GINI系数;(2)在选择决策特征后,如何划分该条记录,换句话说就是应该放到哪个特征类别下面。如果在训练模型之前没有对缺失值进行前文所讲的处理工作,决策树也会有相应的处理方法:

  1. 对于第一个问题,如果某个特征中存在缺失值情况,在做特征决策的时候首先计算除缺失值以外数据的信息增益/信息增益率/GINI系数,再把结果值乘以一个非缺失值所占的权重即可。例如:已有100条数据,其中颜色特征有3类红色20条(8正12负)、黄色30条(10正20负)、黑色45条(5正40负),缺失值有5条(2正3负),在构建决策树的时候只计算95条数据的信息增益,然后再把结果乘以0.95即可,即信息增益为: Gain(D, color) = (-\frac{25}{100}log\frac{25}{100} - \frac{75}{100}log\frac{75}{100}) - 0.95 * [\frac{20}{95} * (-\frac{8}{20}log\frac{8}{20} - \frac{12}{20}log\frac{12}{20}) + \frac{30}{95} * (-\frac{10}{30}log\frac{10}{30} - \frac{20}{30}log\frac{20}{30}) + \frac{45}{95} * (-\frac{5}{45}log\frac{5}{45} - \frac{40}{45}log\frac{40}{45})]
  2. 对于第二个问题,在选定决策特征以后,就需要划分数据集了,决策树的处理方法是把缺失值数据分别划到每一个子数据集中,但是附加一个子树数量占比权重值。例如1中的例子,加入颜色的信息增益最大,接下来会划分为三个子树,分别是红(20条)、黄(30条)、黑(45条),剩余的5条数据会分别划入三个子树,但是划到红树的权重都变为\frac{20}{95}、划到黄树的权重都变为\frac{30}{95}、划到黑树的权重都变为\frac{45}{95}。这样红树的样本数为20+5=25,权重和为20*1 + 5*\frac{20}{95} ;黄树的样本数为30+5=35,权重和为30*1 + 5*\frac{30}{95} ;黑树的样本数为45+5=50,权重和为45*1 + 5*\frac{45}{95}

上述两个步骤就完成了一次数据划分,接下来在下一次计算信息增益节点决策的时候所有节点的权重就要按照新的值计算了,例如计算红色子树的正负样本所在比例为:

正样本比例:p_{1} = \frac{8+2*\frac{20}{95}}{20+5*\frac{20}{95}};黑样本比例:p_{2} = \frac{12+3*\frac{20}{95}}{20+5*\frac{20}{95}}

则信息熵为: -p_{1}*log p_{1} - p_{2}*log p_{2},后续其他特征的决策按照上述流程进行即可,更多的案例信息可以参考文献中的第三篇文章。

前面我们已经介绍了在构建决策树过程(训练)中对缺失值的处理,如果在测试阶段又该怎么处理呢?主要处理方法为:

  1. 手动处理缺失值,也就是特征工程部分的工作
  2. 如果有专门处理缺失值的分支,则走该分支,换句话说就是特征工程中对缺失值提炼出一个新类别处理
  3. 从该属性最常用(信息增益最大)的分支走
  4. 同时走一遍所有的分支,并组合他们的结果来得到类别对应的概率,取概率最大的类别

C4.5中采用的方法是:测试样本在该属性值上有缺失值,那么就同时探查(计算)所有分支,然后算每个类别的概率,取概率最大的类别赋值给该样本。如果有先验经验的话,个人建议还是在训练和测试阶段通过特征工程部分处理缺失值较好,因为用户才是对数据最了解的,并且靠算法去处理缺失值也往往会降低模型训练效率。。

2.4 剪枝

前文已经介绍了决策树的构建过程,如果特征足够多,且特征分布不同,理论上来说可以构建出一个把所有样本都分类正确的决策树模型,尤其是对于连续值特征。但是,这就带来了另一个问题,构建的决策树往往会很深,且有很大的概率会导致过拟合(噪声点的存在)。此时就需要对决策树进行剪枝,剪枝主要有两种方法:预剪枝和后剪枝。预剪枝的原理就是边构建树边剪枝,例如设置树的深度、叶子节点的样本数等,当达到剪枝条件后,就不再分裂;后剪枝的原理就是先完成整棵树的构造,然后从下往上剪枝,直到达到既定条件。如果盖楼一样,预剪枝边盖边看是否太高、是否不好看,后剪枝是先盖好,高了或者不好看的地方再拆掉,所以预剪枝效率高,但是可能会导致欠拟合,后剪枝效率低,但是更不容易欠拟合。

网上很多资料在讲解剪枝过程的时候,都提到了周志华老师在《机器学习》中提到的基于验证集的剪枝方法,简单的说就是预剪枝在分裂一个节点后,都要在验证集上测试一下,准确率是否提升了,如果提升了,就分裂,如果没有就剪枝,而后剪枝就是从底层开始测试如果把当前节点剪枝,是否可以使得验证集效果更好,如果更好就剪枝。熟悉sklearn库的同学可能会有一些疑惑,决策树的API里面有树的深度、叶子节点的样本数等参数,典型的预剪枝策略参数,需要用户有比较丰富的调参经验,并没有介绍基于验证集的剪枝方法和后剪枝方法,有兴趣的同学,包括下面所讲的剪枝方法需要自己实现。

下面我们主要介绍一下CART树基于极小化损失函数或代价函数的后剪枝方法。设树T的叶结点个数为|T|,t是树T的一个叶结点,该叶结点有N_{t}个样本点,其中k类的样本点有N_{tk}个,k = 1, 2, . . . K,K为样本空间中的所属分类数量。叶结点t上的经验熵H_{t}(T)为:

H_{t}(T) = -\sum_{k=1}^{K}\frac{N_{tk}}{N_{t}}\log \frac{N_{tk}}{N_{t}}

可以发现,上式就是信息熵公式,代表了该叶结点的分类混乱程度。考虑到所有的叶节点每个叶节点中的样本个数不同,我们采用下式来衡量模型对训练数据的整体测量误差。

C(T) =\sum_{t=1}^{|T|}N_{t} H_{t}(T) = -\sum_{t=1}^{|T|}\sum_{k=1}^{K}N_{tk}\log \frac{N_{tk}}{N_{t}}

但是如果仅仅用C(T)来作为优化目标函数,就会导致模型走向过拟合的结果。因为我们可以尽可能的对每一个分支划分到最细使得每一个叶结点的H_{t}(T) = 0,最终使得C(T) = 0。为了避免过拟合,我们需要给优化目标函数增加一个正则项,正则项应该包含模型的复杂度信息(树的规模大小)。对于决策树来说,其叶结点的数量 |T| 越多就越复杂,我们添加 |T| 的正则项来修正损失函数:

C_{\alpha}(T) = C(T) + \alpha|T|

参数α控制了两者之间的影响程度。较大的α促使选择较简单的模型(树),较小的α促使选择较复杂的模型(树)。当α = 0时,即没有正则项,原始生成的CART树即为最优子树。当α = ∞时,正则化强度最大,此时由原始的生成CART树的根节点组成的单节点树为最优子树(即不分裂)。当然,这是两种极端情况,一般来说,α越大,剪枝剪的越厉害,生成的最优子树相比原生决策树就越偏小。对于固定的α,一定存在使得损失函数C_{\alpha}(T)最小的唯一子树。在后剪枝过程中,就可以通过C_{\alpha}(T)来选择剪枝点。由上式可知,对于位于节点t的任意一颗子树T_{t},如果没有剪枝,损失函数是:

C_{\alpha}(T_{t}) = C(T_{t}) + \alpha|T_{t}|

如果将其剪掉,仅保留根节点作为叶子节点(此时对当前子树来说叶子节点数为1),损失函数是:

C_{\alpha}(t) = C(t) + \alpha

当α = 0或α很小,C_{\alpha }(T_{t}) 可以最大化分裂,则 C_{\alpha }(T_{t}) < C_{\alpha }(t),当α增大到一定程度时,可以找到一个点使得 C_{\alpha }(T_{t}) = C_{\alpha }(t)。此时,T_{t}和t有相同的损失函数,但t节点更少,树更简单,因此可以对子树T_{t}进行剪枝,也就是将它的子节点全部剪掉,变为一个叶子结点t。

\alpha = \frac{C(t) - C(T_{t})}{|T_{t}| - 1}

如果我们把所有节点是否剪枝的值α都计算出来,然后针对不同α对应的剪枝后的最优子树做交叉验证或者使用验证集验证剪枝的效果。这样可以选择最好的α,有了这个α,用对应的最优子树作为最终结果。可以发现只有在最后才使用了验证集,首先基于训练集多轮迭代计算每次最优的α,然后通过验证集挑选最合适的α值。具体的案例信息可以参考资料[5]。

2.5 决策树回归

前面我们的讨论都是基于分类问题,对于回归问题决策树会怎么处理呢?ID3和C4.5是无法处理回归问题的,接下来介绍的回归内容都是基于CART树讲解的。在2.1.4节中提到CART树做分类时是基于GINI系数来评价的,而在回归问题中CART树采用的评价指标是均方值误差:

min_{j,s}[min_{c_{1}}\sum_{x_{i}\in R_{1}(j,s)}(y_{i}-c_{1})^2+min_{c_{2}}\sum_{x_{i}\in R_{2}(j,s)}(y_{i}-c_{2})^2]

= min_{j,s}[\sum_{x_{i}\in R_{1}(j,s)}(y_{i}-\bar{c_{1}})^2+\sum_{x_{i}\in R_{2}(j,s)}(y_{i}-\bar{c_{2}})^2]

已知数据集为 (X, Y),其中X = [X_{A}, X_{B},X_{C},...]为自变量特征(A、B、C表示三个特征),Y为连续的标签值,假设已有n个训练集样本 \left \{ (x_{1},y_{1}), (x_{2},y_{2}),......,(x_{n},y_{n}) \right \}。我们以一个特征来理解上式(其他特征类似)。如果某个特征 j 是离散型特征,则取其中一个类别对应的子集R1(属于当前类别的样本),其他数据集为R2(不属于当前类别的样本),如果 j 是连续值类型,则同样按照上文提到的连续值离散方法(即对特征排序,依次取所有相邻特征的中间值作为候选分化点)依次取任意一个划分点 s 分隔后的数据集R1(不大于s的样本)、R2(大于s的样本),其中c1、c2分别是R1、R2子集上的输出固定值,现需要找出最合适的c1、c2使得上式中括号内的平方值误差最小,显然最合适的值分别是R1、R2对应输出y的平均值\bar{c_{1}}\bar{c_{2}}。只要按照上述方法依次遍历所有候选划分点即可计算出多个平方值误差,最小的平方值误差就是最合适的划分点,根据同样的流程计算其他所有特征的平方值误差,其中最小的(最优的j和s)就是当前的决策特征(决策树分裂特征)。这里搬运了参考文献[7]中的案例信息供参考:

  • 已知一批单特征样本数据如下:

  • 则9个候选划分点为\small \left \{ 1.5,2.5,3.5,4.5,5.5,6.5,7.5,8.5,9.5 \right \},依次计算每个划分点的左右子集平均值c1、c2:

  • 分别计算平方误差损失值,例如当s = 1.5时:L(s=1.5) = (5.56-5.56)^{2} + [(5.7-7.5)^{2}+...+(9.05-7.5)^{2}] = 15.72。同理,依次计算其他各切分点的损失函数值,结果如下:

  • 所以s=6.5时,损失函数值最小。因此,第一个划分点为(j=x,s=6.5)。划分后的子集为\small R_1=\left \{ 1,2,3,4,5,6 \right \}\small R_2=\left \{ 7,8,9,10 \right \}。对R1、R2重复上述步骤:

  • 假设两次划分后即停止(预剪枝树深度为2),则最终生成的回归树如下,最终的预测输出值也是对应阶段的c1、c2,所以决策树的回归输出往往是阶梯状的,因为特征在某个区间的输出值都是该区间训练集输出的平均值:

至此,我们已经介绍完了决策树的原理和构建过程,也可以发现决策树的特征重要性排序其实就是越处于上层节点的特征约重要。

三、实践

接下来我们将使用sklearn库中的决策树API和鸢尾花数据集为例来说明决策树的使用案例。

3.1 分类实例

3.1.1 环境配置

为了方便可视化生成的决策树,需要安装graphviz包,建议通过 conda install python-graphviz 的方式安装,如果使用 pip install graphviz 安装,可能会出现下面的问题,还需要再执行 yum install graphviz 或者 apt install graphviz:

failed to execute ['dot', '-Tpdf', '-O', 'iris'], make sure the Graphviz exe

sklearn中决策树分类的API类如下:

sklearn.tree.DecisionTreeClassifier(*, 
criterion='gini',    
##决策算法,可选gini和entropy,即CART树和C4.5
splitter='best',     
##节点分隔策略,可选best和random,表示选择选择最优的分裂策略或者最优的随机切分策略,一般都是best
max_depth=None,      
##树的深度,预剪枝参数,若不设置,则分裂直到所有叶子都是纯的、所有特征都用尽或者直到所有叶子包含的样本小于min_samples_split
min_samples_split=2, 
##如果某节点的样本数少于该值,则不会继续再尝试选择最优特征来进行划分。 默认是2.如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。若是整数,则不变,若是小数,则取ceil(min_samples_split * n_samples)
min_samples_leaf=1,  
##一个叶节点上所需的最小样本数,即当叶子节点样本数小于该值时会被剪枝,若是整数,则不变,若是小数,则取ceil(min_samples_leaf * n_samples)
min_weight_fraction_leaf=0.0, 
##这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会被剪枝。 默认是0,就是不考虑权重问题。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,就要注意这个值了。
max_features=None,   
##构建特征树时考虑的特征数量。整型n表示n个特征;小数表示全部特征数n_features的百分比int(max_features * n_features);"auto"表示max_features=sqrt(n_features);"sqrt"表示 max_features=sqrt(n_features);"log2"表示max_features=log2(n_features)
##默认是n_features全部特征。该参数通常不设置,通过其他参数剪枝。如果已经遍历max_features个特征,但还是没有找到一个有效的切分,那么还会继续寻找下一个特征,直到找到一个有效的切分为止
random_state=None,   
##控制splitter和max_features的随机数种子
max_leaf_nodes=None, 
##最大叶子节点数,可以防止过拟合,默认是"None”,即不限制最大的叶子节点数。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。
min_impurity_decrease=0.0, 
##如果节点的分裂导致不纯度的减少(基尼系数和均方差)大于或等于min_impurity_decrease,则分裂该节点,不纯度的减少值计算方法为:
#N_t / N * (impurity - N_t_R / N_t * right_impurity - N_t_L / N_t * left_impurity)。
#其中N为样本总数,N_t为当前节点的样本数,N_t_L为左子节点的样本数,N_t_R为右子节点的样本数,impurity指当前节点的基尼指数,right_impurity指分裂后右子节点的基尼指数。left_impurity指分裂后左子节点的基尼指数。
min_impurity_split=None,   
##最小分隔不纯度值,如果某节点的不纯度(基尼系数和均方差)小于这个阈值,则该节点不再生分割,即为叶子节点 。min_impurity_split在0.19中被丢弃,转而支持min_impurity_decrease。min_impurity_split的默认值在0.23中从1e-7更改为0,在0.25中将被删除。使用min_impurity_decrease代替。
class_weight=None, 
##类权重,形式为{class_label: weight}
##         (1).如果没有给出每个类别的权重,则每个类别的权重都为1。
##         (2).如果class_weight='balanced',则分类的权重与样本中每个类别出现的频率成反比。计算公式为:n_samples / (n_classes * np.bincount(y))
##         (3).如果sample_weight提供了样本权重(由fit方法提供),则这些权重都会乘以sample_weight。
presort='deprecated', 
##从0.22版本已被删除,是否需要提前排序数据从而加速训练中寻找最优切分的过程
ccp_alpha=0.0
##CCP剪枝算法的α值,α的计算方法可参考2.4节,当生成树的节点α大于ccp_alpha时将停止
)

上述参数中需要区分min_samples_split和min_samples_leaf的不同,可以认为min_samples_split设置了一个上限,min_samples_leaf设置了一个下限,且在优先级上max_depth > min_samples_leaf > min_samples_split。

3.1.2 模型训练

正式程序如下:

from sklearn.datasets import load_iris
from sklearn import tree
from sklearn.tree import export_text
import graphviz
from collections import Counter

iris = load_iris()
X = iris.data
y = iris.target

print(X.shape, y.shape)  ##((150, 4), (150,))
print(Counter(y))        ##Counter({0: 50, 1: 50, 2: 50})

clf = tree.DecisionTreeClassifier()
clf = clf.fit(X, y)
tree.plot_tree(clf)      ##打印生成树

dot_data = tree.export_graphviz(clf, out_file=None) 
graph = graphviz.Source(dot_data) 
graph.render("iris")     ##保存到文件

可以发现数据集大小是150 * 4,是一个三分类问题,类别标签是0、1、2,特征都是连续值类型,原始特征未做任何处理,如归一化等。生产的决策树模型如下:

根据特征名称和graphviz绘制更加可视化的决策树或者以文件形式输出:

dot_data = tree.export_graphviz(clf, out_file=None, 
                     feature_names=iris.feature_names,  
                     class_names=iris.target_names,  
                     filled=True, rounded=True,  
                     special_characters=True)  
graph = graphviz.Source(dot_data)  
graph

r = export_text(clf, feature_names=iris['feature_names'])
print(r)

设置树的最大深度为2:

clf = tree.DecisionTreeClassifier(random_state=0, max_depth=2)
clf = clf.fit(X, y)

3.1.3 CCP剪枝

在3.1.2节中,我们简单介绍了基于树深度的预剪枝方法,对于其他预剪枝参数操作类似,这里不再过多介绍,下面主要说明基于成本复杂度(CCP)的后剪枝操作,具体原理请参考2.4节。程序如下:

import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier
from collections import Counter

X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

print(Counter(y))
print(X.shape, y.shape)
print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
##Counter({1: 357, 0: 212})
##(569, 30) (569,)
##(426, 30) (426,)
##(143, 30) (143,)

clf = DecisionTreeClassifier(random_state=0)
path = clf.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas, impurities = path.ccp_alphas, path.impurities  ##α值和对应子树叶子的不纯度和

print(ccp_alphas)
'''
array([0.        , 0.00226647, 0.00464743, 0.0046598 , 0.0056338 ,
       0.00704225, 0.00784194, 0.00911402, 0.01144366, 0.018988  ,
       0.02314163, 0.03422475, 0.32729844])
'''

print(impurities)
'''
array([0.        , 0.00453294, 0.01847522, 0.02313502, 0.02876883,
       0.03581108, 0.04365302, 0.05276704, 0.0642107 , 0.0831987 ,
       0.10634033, 0.14056508, 0.46786352])
'''

fig, ax = plt.subplots()
ax.plot(ccp_alphas[:-1], impurities[:-1], marker='o', drawstyle="steps-post")
ax.set_xlabel("effective alpha")
ax.set_ylabel("total impurity of leaves")
ax.set_title("Total Impurity vs effective alpha for training set")

首先计算出每轮迭代的α,α越大,剪枝越厉害,不纯度(GINI系数)越大,上图没有画出最后一个α表示完全剪枝,只留一个叶节点。接下来依次基于所有的α训练模型:

clfs = []
for ccp_alpha in ccp_alphas:
    clf = DecisionTreeClassifier(random_state=0, ccp_alpha=ccp_alpha)
    clf.fit(X_train, y_train)
    clfs.append(clf)

print("Number of nodes in the last tree is: {} with ccp_alpha: {}".format(clfs[-1].tree_.node_count, ccp_alphas[-1]))

##Number of nodes in the last tree is: 1 with ccp_alpha: 0.3272984419327777

查看不同α对应的节点数和树深度:

clfs = clfs[:-1]
ccp_alphas = ccp_alphas[:-1]

node_counts = [clf.tree_.node_count for clf in clfs]
depth = [clf.tree_.max_depth for clf in clfs]
fig, ax = plt.subplots(2, 1)
ax[0].plot(ccp_alphas, node_counts, marker='o', drawstyle="steps-post")
ax[0].set_xlabel("alpha")
ax[0].set_ylabel("number of nodes")
ax[0].set_title("Number of nodes vs alpha")
ax[1].plot(ccp_alphas, depth, marker='o', drawstyle="steps-post")
ax[1].set_xlabel("alpha")
ax[1].set_ylabel("depth of tree")
ax[1].set_title("Depth vs alpha")
fig.tight_layout()

对比不同的α在训练集和测试集上的精度:

train_scores = [clf.score(X_train, y_train) for clf in clfs]
test_scores = [clf.score(X_test, y_test) for clf in clfs]

fig, ax = plt.subplots()
ax.set_xlabel("alpha")
ax.set_ylabel("accuracy")
ax.set_title("Accuracy vs alpha for training and testing sets")
ax.plot(ccp_alphas, train_scores, marker='o', label="train", drawstyle="steps-post")
ax.plot(ccp_alphas, test_scores, marker='o', label="test", drawstyle="steps-post")
ax.legend()
plt.show()

可以发现在测试集上准确度最高的α是0.01144366,因此训练模型的参数可以设置为 ccp_alpha=0.015 得到一个最好的测试效果,得到的决策树模型为 clf = DecisionTreeClassifier(random_state=0, ccp_alpha=0.015):

通过前面的图可以发现,α是0.01144366的时候对应的树深度是3,可以再建立一个深度为3的树 clf = DecisionTreeClassifier(random_state=0, max_depth=3) 如下,会发现有很大不同。

3.2 回归实例

我们通过模拟一个正弦曲线来说明决策树回归API。决策树回归API类定义如下:

sklearn.tree.DecisionTreeRegressor(*, 
criterion='mse', 
splitter='best', 
max_depth=None, 
min_samples_split=2, 
min_samples_leaf=1, 
min_weight_fraction_leaf=0.0, 
max_features=None, 
random_state=None, 
max_leaf_nodes=None, 
min_impurity_decrease=0.0, 
min_impurity_split=None, 
presort='deprecated', 
ccp_alpha=0.0)

因为回归和分类的类参数相似,不再说明。第一个参数 criterion='mse' 表示使用均方误差作为评价指标,具体可以参考2.5节,通过使用叶子节点的均值来最小化L2损失;criterion='friedman_mse' 表示使用费尔德曼均方误差作为评价指标;criterion='mae' 表示使用绝对平均误差作为评价指标,该指标使用叶节点的中值来最小化L1损失。另外,需要注意的是,回归树的score方法返回的是决定系数R^{2},不是mse,其中:

R^{2} = 1 - \frac{u}{v} ,其中u u = \sum (y_{true} - y_{pred}) ^{2}v = \sum (y_{true} - y_{true}.mean()) ^{2}R^{2}可能为负,因为如果模型很差,u > v。

回归模型程序如下:

import numpy as np
from sklearn.tree import DecisionTreeRegressor
import matplotlib.pyplot as plt

# Create a random dataset
rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - rng.rand(16))

# Fit regression model
regr_1 = DecisionTreeRegressor(max_depth=2)
regr_2 = DecisionTreeRegressor(max_depth=5)
regr_1.fit(X, y)
regr_2.fit(X, y)

# Predict
X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
y_1 = regr_1.predict(X_test)
y_2 = regr_2.predict(X_test)

# Plot the results
plt.figure()
plt.scatter(X, y, s=20, edgecolor="black",
            c="darkorange", label="data")
plt.plot(X_test, y_1, color="cornflowerblue",
         label="max_depth=2", linewidth=2)
plt.plot(X_test, y_2, color="yellowgreen", label="max_depth=5", linewidth=2)
plt.xlabel("data")
plt.ylabel("target")
plt.title("Decision Tree Regression")
plt.legend()
plt.show()

可以发现,决策树的回归形状如前文所说的阶梯状,树越深越容易过拟合(拟合异常点)。另外,在调参的时候也可以通过交叉验证或者网格搜索等形式进行,本文没有列出。

参考文献

[1] https://www.cnblogs.com/yonghao/p/5135386.html

[2] https://blog.csdn.net/u012328159/article/details/79396893

[3] https://blog.csdn.net/u012328159/article/details/79413610

[4] https://zhuanlan.zhihu.com/p/128587211

[5] https://blog.csdn.net/am290333566/article/details/81187562

[6] https://www.cnblogs.com/keye/p/10564914.html

[7] https://blog.csdn.net/Albert201605/article/details/81865261

[8] https://www.cnblogs.com/pinard/p/6160412.html?utm_source=itdadao&utm_medium=referral

[9] https://scikit-learn.org/stable/modules/tree.html#classification

[10] https://scikit-learn.org/stable/auto_examples/tree/plot_cost_complexity_pruning.html#sphx-glr-auto-examples-tree-plot-cost-complexity-pruning-py

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值