在机器学习中有很多模型和算法是和树结构相关的,比如决策树(ID3、C4.5、CART)、随机森林(Random Forest)、Adaboost、GBDT、xgboost。在这些模型算法中决策树是最基础的,顾名思义,决策树是基于树结构来进行决策的,它具有可读性高、分类速度快等优点。决策树既可以用来做回归(CART,分类回归树)也可以用来做分类(ID3、C4.5、CART)。决策树学习算法主要包括三个部分:特征选择、树的生成以及树的剪枝。接下来就对决策树进行详细的说明,本篇文章主要是根据李航老师的《统计学习方法》、周志华老师的《机器学习》以及博客上一些前辈的描述外加自己的一些理解整理而成,有什么说的欠妥的地方,还请各位指正。
目录
2.3 信息增益比(information gain ratio)
3.2 CART(classification and regression tree)算法
1、引入
首先,说明决策树的概念,分类决策树是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部结点(internal node)和叶节点(leaf node)。内部结点表示属性或特征,叶结点表示决策结果,也就是类。
我们在对问题进行决策时,通常会进行一系列的判断或“子决策”,决策树进行分类从根结点开始,对实例的某一特征进行测试,根据测试结果将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至到达叶结点。最后将实例分配到叶结点的类中。根结点到每个叶结点的路径对应了一个判定测试序列,决策树学习的基本流程遵循“分而治之”的策略。
我们以高校硕士毕业生在北京找工作为例,这是一个二分类问题,最后的输出就是签约或者不签约。我们首先关注的是工资水平,如果工资高于15k,那么直接签约,如果工资在5-15k之间,再看看是否解决北京户口,如果解决,直接签约,如果不解决看每天工作时间是否超过8小时,如果超过8小时,再看看是否有年假,如果有年假,则签约,如果日工作时间超过8小时,且无年假拒绝签约,如果日工作时间不超过8小时,直接签约;如果工资低于5k直接拒绝签约。这个过程用决策树表示就如下图,其中圆表示内部结点方块表示叶结点:
在上面左边的框图是使用的工资水平这个特征作为根结点的,但是不同的根结点会产生不同的决策树,我们再以日工作时长这个特征作为根结点会得到如上图右边的决策树。可以看到以日工作时间为根结点得到的决策树的分支要比左边的以工资水平作为根结点得到的决策树多,这说明我们在选择工作时对工资水平的重视程度要高于对日工作时长的重视程度,换句话说,只要你钱给到位,加班我也愿意,有没有户口也无所谓。根据奥卡姆剃刀原理我们更倾向于选择左边的决策树。那么问题来了,一个实例可能会有很多的特征,那么我们应该选择哪个特征作为根结点呢,同样的,子结点又应该怎么选择才能使决策树在能正确分类的基础上变得尽可能简单呢。接下来,我们就说说特征选择的相关问题。
2、特征选择
特征选择在于选取对训练数据具有分类能力的特征,这样可以提高决策树学习的效率。特征选择是决定用哪个特征来划分空间。通常,我们在选择特征时会有个量化的参数,比如在线性分类器中,某个特征的权重越大则说明这个特征越重要,同样的在决策树学习中,我们也需要找出这么一个量化的东西,通常我们用信息增益或者信息增益比或者基尼指数来作为特征选择的准则。
2.1 熵(entropy)
在介绍信息增益前先介绍下另一个概念——熵。在物理学中,熵是物体在一定的宏观状态下所有微观状态的总和。听起来很抽象,但是这不是我们研究的范畴,有兴趣的同学可以参考「熵」是什么? 怎样以简单易懂的方式向其他人解释?。在信息论和概率统计中,熵是表示随机变量不确定性的度量。假设一个离散随机变量的概率分布为:,那么随机变量的熵定义为:
(2.1)
可以看到,熵与的取值无关,只依赖于的分布,所以的熵可记为,并定义。熵越大,说明随机变量的不确定性就越大。由定义式可以知道,当随机变量取值等概时,该随机变量的熵最大,最大值为,其中为取值的个数,这也很容易理解,当随机变量的取值等概时,那么这个随机变量的不确定性最大,从而其熵的值最大。
设有随机变量,其联合概率分布为:
条件熵(conditional entropy)表示在已知随机变量的条件下随机变量的不确定性,定义为:
(2.2)
当熵和条件熵的概率由数据估计得到时,所对应的熵与条件熵分别为经验熵(empirical entropy)和经验条件熵(empirical conditional entropy)。
2.2 信息增益(information gain)
有了熵和条件熵的铺垫之后,我们就可以对信息增益进行进一步的说明了。
信息增益:特征对训练数据集的信息增益,定义为集合的经验熵与特征给定条件下的经验条件熵之差,即: (2.3)
更一般的称熵与条件熵之差为互信息(mutual information)。
根据定义可以知道,经验熵表示对数据集进行分类的不确定性;而经验条件熵表示在特征给定条件下对数据集进行分类的不确定性,它们两个的差就表示得知特征的信息而使得数据集的分类的信息的不确定性减少的程度。显然,如果信息增益越大,那么不确定性减少的程度就越大,从而特征起的作用越大,也就是该特征具有更强的分类能力。
下面给出信息增益的算法:
输入:训练数据集和特征 | |
输出:信息增益 | |
1、计算数据集经验熵 (2.4) 2、计算特征对数据集的经验条件熵 (2.5) 3、计算信息增益 (2.6) |
上面的算法是基于以下假设:训练数据集为,表示样本个数。设有个类,为属于类的样本个数,那么有。设特征有个不同的取值,根据特征的取值将划分为个子集,其中为的样本个数,那么有。子集中属于类的样本集合为,其中为的样本个数。
为了对信息增益算法有更加深刻的理解,选用《统计学习方法》中的例子进行信息增益的计算。
例:下表是一个由15个样本组成的贷款申请训练数据。数据包括贷款申请人的4个特征(属性):第1个特征是年龄,有3个可能值(青年、中年、老年);第2个特征有工作,有2个可能取值(是、否);第3个特征是有自己的房子,有2个可能的取值(是、否);第4个特征是信贷情况,有3个可能的取值(非常好、好、一般)。最后一列表示类别,是否同意贷款,有2个可能取值(是、否)。
ID | 年龄 | 有工作 | 有自己的房子 | 信贷情况 | 同意贷款 |
1 | 青年 | 否 | 否 | 一般 | 否 |
2 | 青年 | 否 | 否 | 好 | 否 |
3 | 青年 | 是 | 否 | 好 | 是 |
4 | 青年 | 是 | 是 | 一般 | 是 |
5 | 青年 | 否 | 否 | 一般 | 否 |
6 | 中年 | 否 | 否 | 一般 | 否 |
7 | 中年 | 否 | 否 | 好 | 否 |
8 | 中年 | 是 | 是 | 好 | 是 |
9 | 中年 | 否 | 是 | 非常好 | 是 |
10 | 中年 | 否 | 是 | 非常好 | 是 |
11 | 老年 | 否 | 是 | 非常好 | 是 |
12 | 老年 | 否 | 是 | 好 | 是 |
13 | 老年 | 是 | 否 | 好 | 是 |
14 | 老年 | 是 | 否 | 非常好 | 是 |
15 | 老年 | 否 | 否 | 一般 | 否 |
先对上面的数据进行分析,数据集中一共有15个样本数据,那么,数据集可以分为2类,也就是,假设表示同意贷款,表示不同意贷款,那么。分别以表示年龄、有工作、有自己的房子、信贷情况这四个特征。以特征进行说明,其中特征有3个取值{青年、中年、老年}将数据集划分为,且。表示青年中同意贷款的数据集,那么,同样的表示青年中不同意贷款的数据集,那么,则,同理,可求出.
解:由信息增益的定义,我们需要计算经验熵和经验条件熵。我们先计算经验熵,由公式(2.4)可知:
接下来求特征对数据集的经验条件熵,由公式(2.5)得:
那么特征的信息增益为:
同样的方法分别计算特征的信息增益:
通过比较得知特征(有自己的房子)的信息增益最大,也就是有自己的房子的话,银行更有可能贷款给申请人。想想也是,毕竟如果申请人如果没有能力偿还贷款的话,可以用房子进行抵押。这样的话,我们就可以以特征作为根结点学习决策树了,在根结点的下一子结点,继续计算剩余三个特征的信息增益以选择第一子结点,依次类推,直到分类完毕。
2.3 信息增益比(information gain ratio)
在上面的例子中,我们并没有把第一列ID作为特征,设想一下,如果把ID列也作为特征并用来表示,那么根据特征会把数据集分成15类,那么特征对数据集的经验条件熵可由下式表示:
因为数据子集中都只有一个样本,那么,那么特征(ID)的信息增益为:
那么以ID作为根结点可以吗,显然是不可以的,因为这样学习得到的决策树不具有泛化能力。
实际上,以信息增益作为划分训练数据集的特征,存在偏向于选择取值较多特征的问题。比如刚刚说的ID。那么如何解决这个问题呢,使用信息增益比可以校正这一问题。
信息增益比:特征对训练数据集的信息增益比定义为其信息增益与训练数据集关于特征的值的熵之比,即:
,其中,是特征取值的个数。
那么我们以信息增益比为特征选择的准则分别计算包括特征在内的5个特征的
根据上面的公式以及之前计算的信息增益,可以计算出各个特征的信息增益比:
可以看到,以信息增益比作为划分准则,还是特征的信息增益比最大,所以还是选择特征作为最优特征,也就是以特征作为根结点,同样的,在第一子结点中分别计算剩余的4个特征信息增益比并取其中的最大值作为第一子结点,并以此类推。
2.4 基尼指数(Gini index)
基尼指数是CART决策树的特征选择准则,有关CART的内容在下文进行展开说明,本部分只对基尼指数进行说明。
基尼指数:在分类问题中,假设有K个类,样本点属于第k类的概率为,则概率分布的基尼指数定义为:
对于给定的样本集,基尼指数为:
其中是中属于第类的样本子集,是类的个数。
基尼指数反映了从数据集中随机抽取两个样本,其类别标记不一致的概率。基尼指数值越大,样本集合的不确定性就越大。
特征的基尼指数定义为:
表示经分割后集合的不确定程度。在选择特征的时候,选择使得划分后基尼指数最小的特征作为最优划分特征。
3、树的生成
ID3、C4.5以及CART都是决策树学习算法,下面将ID3和C4.5作为一个小节,而将CART单独作为一个小节。决策树的生成基于训练数据集生成决策树,生成的决策树要尽量大,这样才能更好对训练数据分类。(当然这样也会增高过拟合的风险,通常会在生成树之后通过最小化损失函数来对生成树进行剪枝,从而选择最优子树)。
3.1 ID3算法和C4.5算法
决策树算法常见的有两种:ID3算法和C4.5算法。两者不同的地方就在于ID3算法是应用信息增益准则来选择特征的,而C4.5算法是应用信息增益比来选择特征的。可以说C4.5算法是对ID3算法进行了改进。下面以ID3算法为例,说明决策树学习的基本算法。ID3算法的全称是Iterative Dichotomiser 3.
输入:训练数据集、特征集 |
输出:决策树 |
1、若数据集中样本全属于同一类别,将类作为该结点的类标记,返回树 2、若或中样本在上取值相同,将中实例数最大的类作为该结点的类标记,返回树 从中选择最优特征 3、按照信息增益的计算方法计算各特征的信息增益,选取信息增益最大的特征 4、特征将数据集划分成数据子集,若数据子集中的样本属于同一类别,那么将此类别作为该结点的类标记 5、若数据子集中样本属于不同的类别,那么以为新的数据集,以为特征集,重复上面的步骤直到中的样本属于同一类别。 |
还是以上一小节贷款申请为例子,对ID3算法进行说明。之前我们通过计算得到,特征(有自己的房子)的信息增益最大,所以以特征作为根结点,它有两个取值(是,否),将数据集划分为两个子集和。因为中的样本属于同一类别,所以它生成一个叶结点,类标记为“是”;中样本属于不同的类别,那么以为新的数据集,以为新的特征集,重复构建树的过程,首先就是进行新一轮的特征选择,也就是计算信息增益。经过计算得到
可以看到,在这一轮的特征选择中,特征(有工作)的信息增益最大,那么以它为第一子结点,它也有两个取值(是,否),将数据集分为两个子集和,这两个子集中的样本均属于同一类别,那么生成了两个叶结点,到此,决策树构建完毕。决策树的图形如下:
对于C4.5算法只需将特征选择部分的准则变为信息增益比即可,其他步骤不变。对于仅有一层划分的决策树,我们称其为“决策树桩”(decision stump)。
3.2 CART(classification and regression tree)算法
CART模型是分类回归树,既可以做回归也可以做分类,是一种使用广泛的决策树。与ID3、C4.5不同。CART假设决策树是二叉树,内部节点特征的取值为“是”和“否”,左分支是取值为“是”的分支,右分支是取值为“否”的分支;CART是递归的二分每个特征。CART的生成就是递归的构建二叉树的过程,对回归树用平方误差最小化准则进行特征选择,生成二叉树;对于分类树用基尼指数最小化准则,进行特征选择,生成二叉树。既然CART既可以进行分类,又可以进行回归,那么下面分别从分类树生成和回归树生成进行说明。
3.2.1 分类树的生成
在分类树中,用基尼指数来选择最优特征。
输入:训练数据集、特征集、停止计算的条件 |
输出:决策树 |
1、计算现有特征对该数据集的基尼指数。对每一个特征,对其可能取值的每个值,根据样本点对的测试为“是”或“否”将分割成和两部分,计算时的基尼指数 2、在所有可能的特征以及它们所有可能的切分点中,选择基尼指数最小的特征及其对应的切分点作为最优特征与最优切分点。依最优特征和最优切分点,从现节点生成两个子节点,将训练数据集依特征分配到两个子节点中去 3、对两个子节点递归调用步骤1、2,直到满足停止条件 4、生成决策树 |
算法停止的条件可以是:结点中样本个数小于预定阈值、样本集的基尼指数小于预定阈值(样本基本属于同一类)、没有更多的特征。
ID | 年龄 | 有工作 | 有自己的房子 | 信贷情况 | 同意贷款 |
1 | 青年 | 否 | 否 | 一般 | 否 |
2 | 青年 | 否 | 否 | 好 | 否 |
3 | 青年 | 是 | 否 | 好 | 是 |
4 | 青年 | 是 | 是 | 一般 | 是 |
5 | 青年 | 否 | 否 | 一般 | 否 |
6 | 中年 | 否 | 否 | 一般 | 否 |
7 | 中年 | 否 | 否 | 好 | 否 |
8 | 中年 | 是 | 是 | 好 | 是 |
9 | 中年 | 否 | 是 | 非常好 | 是 |
10 | 中年 | 否 | 是 | 非常好 | 是 |
11 | 老年 | 否 | 是 | 非常好 | 是 |
12 | 老年 | 否 | 是 | 好 | 是 |
13 | 老年 | 是 | 否 | 好 | 是 |
14 | 老年 | 是 | 否 | 非常好 | 是 |
15 | 老年 | 否 | 否 | 一般 | 否 |
还是以贷款申请为例子,来看一下的生成。分别以表示年龄、有工作、有自己的房子和信贷情况4个特征,并以1,2,3表示年龄的值为青年、中年、老年;以1,2表示有工作和有自己房子的值为是和否;以1,2,3表示信贷情况的值为非常好、好和一般。
先求解特征的基尼指数,
当时,
其中,,,,将其带入上式中得,同样的,可以得到,。可以看到当特征取值为1和3时得到的基尼指数是相等的都是最小值,所以都可以作为最优切分点。求特征和的基尼指数得:
特征的基尼指数为:
可得可作为特征的最优切分点。在 四个特征中, 最小,所以选择作为最优特征, 作为最优切分点,得到两个子节点,一个为叶子节点。对非叶子节点继续递归的寻找中的最优特征进和最优切分点。依次类推,最终得到下面的CART结构。
3.2.2 回归树的生成
回归树中的输出变量是连续变量,假设有数据集,假设将输入空间划分为个单元,并且在每个单元上有一个固定的输出值,于是回归树的模型可表示为:
在回归树中,通常用平方误差来表示回归树对于训练数据的预测误差。回归树生成算法如下:
输入:训练数据集 |
输出:回归树 |
1、选择最优切分变量和切分点,求解
遍历变量,对固定的切分变量扫描切分点,选择使上式达到最小值的对 2、用选定的对划分区域并决定相应的输出值:
3、继续对两个子区域调用步骤1、2,直到满足停止条件 4、将输入空间划分为个区域,生成决策树:
|
4、树的剪枝
当决策树的结构过于复杂时,容易出现过拟合现象。在决策树中,通常通过剪枝(pruning)对树进行简化,以此来防止过拟合。决策树的剪枝往往通过极小化决策树整体的损失函数或代价函数来实现。设树T的叶子节点个数为|T|,t是树T的叶子节点,该叶子节点有 个样本点,其中k类的样本点有个,k = 1,2,3,...K,为叶子节点t上的经验熵,为参数,则决策树的损失函数定义为:
(4.1)
其中叶子节点t的经验熵为:
(4.2)
将(4.2)式带入(4.1)式得:
(4.3)
其中第一项表示模型对训练数据的训练误差,第二项可看做是正则项,|T|表示树的复杂度,当较大时选择较简单的树模型;当较小时,选择较复杂的树模型,当意味着只考虑模型与训练数据的拟合程度,不考虑模型的复杂度。
剪枝,就是当确定时,选择损失函数最小的模型,即损失函数最小的子树。
5、决策树的实现
from math import log
import operator
def createDataSet():
'''创建数据集
第一列为age:'1':青年;'2':中年;'3':老年
第二列为work:'1':有工作;'2':无工作
第三列为house:'1':有房子;'2':无房子
第四列为credit:'1':一般;'2':好;'3':非常好
第五列为label:'yes':同意贷款;'no':不同意贷款
'''
dataSet = [[1,0,0,1,'no'],
[1,0,0,2,'no'],
[1,1,0,2,'yes'],
[1,1,1,1,'yes'],
[1,0,0,1,'no'],
[2,0,0,1,'no'],
[2,0,0,2,'no'],
[2,1,1,2,'yes'],
[2,0,1,3,'yes'],
[2,0,1,3,'yes'],
[3,0,1,3,'yes'],
[3,0,1,2,'yes'],
[3,1,0,2,'yes'],
[3,1,0,3,'yes'],
[3,0,0,1,'no']]
labels = ['age','work','house','credit']
return dataSet,labels
def calcShannonEnt(dataSet):
'''计算数据集的香浓熵'''
numEntries = len(dataSet) #实例总数
#为所有可能分类创建字典
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1] #获取特征向量的最后一列,也就是类标
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #记录当前类别出现的次数{'no':6,'yes':9}
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries #计算每个类别出现的概率
shannonEnt -= prob * log(prob,2) #计算香浓熵
return shannonEnt
def splitDataSet(dataSet,axis,value):
'''根据特征划分数据集'''
retDataSet = []
for featVec in dataSet:
# 抽取符合特征的数据集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reducedFeatVec)
return retDataSet
'''
dataSet需要满足两个条件:
1、数据必须是一种由列表元素组成的列表,而且所有的列表元素要具有相同的数据长度
2、数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签
'''
def chooseBestFeatureToSplit(dataSet):
'''通过计算各个特征的信息增益,选择最好的数据集划分方式'''
numFeatures = len(dataSet[0]) - 1 #计算数据集的特征数
baseEntropy = calcShannonEnt(dataSet) #计算数据集的原始香农熵H(D)
bestInfoGain = 0.0 #初始化信息增益
bestFeature = -1
for i in range(numFeatures):
'''循环遍历数据集中的所有特征'''
featList = [example[i] for example in dataSet] #将数据集中的所有特征值写入featList列表中
uniqueVals = set(featList) #将列表转换成集合,这样可以得到列表中的唯一元素值
newEntropy = 0.0
for value in uniqueVals:
'''循环遍历当前特征中的唯一特征值,并计算当前特征值下的香浓熵'''
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy #每个特征的信息增益
if (infoGain > bestInfoGain):
'''找到信息增益最大的特征,并返回最好特征的索引值'''
bestInfoGain = infoGain
bestFeature = i
return bestFeature
def majorityCnt(classList):
'''投票法定义叶子结点的分类'''
classCount = {}
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1 #字典对象classCount存储了classList中每个类标签出现的频率
sortedClassCount = sorted(classCount.iteritems(),key = operator.itemgetter(1),reverse = True) #排序
return sortedClassCount[0][0] #返回出现次数最多的分类名称
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet] #包含数据集所有类标签
if classList.count(classList[0]) == len(classList): #递归停止第一条件:所有类标签完全相同,则直接返回该类标签
return classList[0]
if len(dataSet[0]) == 1: #递归停止第二条件:遍历完所有特征时返回出现次数最多的类别
return majorityCnt(classList)
'''创建树'''
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}} #字典变量myTree存储树的信息
del(labels[bestFeat]) #删除列表中的最优特征
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues) #获取当前特征的特征值
for value in uniqueVals:
subLabels = labels[:] #复制类标签
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
return myTree
def classify(inputTree,featLabels,testVec):
'''使用决策树实现分类算法'''
firstStr = list(inputTree.keys())[0] #获取输入树的第一个键
secondDict = inputTree[firstStr] #获取第二个字典,也就是第一个键对应的值
featIndex = featLabels.index(firstStr) #查找当前列表中第一个匹配firstStr变量的元素,并返回索引值
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key],featLabels,testVec)
else:
classLabel = secondDict[key]
return classLabel
def storeTree(inputTree,filename):
'''使用pickle模块存储决策树'''
import pickle
fw = open(filename,'wb')
pickle.dump(inputTree,fw)
fw.close()
def loadTree(filename):
'''加载已存储的决策树算法'''
import pickle
fr = open(filename,'rb')
return pickle.load(fr)
if __name__ == '__main__':
myDat,labels = createDataSet()
myTree = createTree(myDat,labels) #创建决策树
myDat,labels = createDataSet()
print(classify(myTree,labels,[1,1,0,2])) #测试决策树分类器
storeTree(myTree,'decisionTree.txt')
print(loadTree('decisionTree.txt'))
if __name__ = 'main'中的第一个print的输出为:yes
表示如果输入新的特征向量[1,1,0,2],那么类标输出为yes,也就是同意贷款
第二个print的输出为:{'house': {0: {'work': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
这是根据上面例子生成的决策树
参考文献:
1、《统计学习方法 》 李航
2、《机器学习》 周志华
3、「熵」是什么? 怎样以简单易懂的方式向其他人解释? https://www.zhihu.com/question/19753084
4、机器学习实战