目录

机器学习局 | 决策树从理论到Python实现,看完就会决策树

本片文章的整体框架如下所示:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_decision_tree.png

1. 决策树是什么?

决策树是一种基本的分类和回归的方法,是基于树结构来进行决策。这种决策方式跟我们人类进行决策时有点类似,所以我们举一个相亲的例子,比如女方在相亲时会对男性程序员的年龄进行判断,假如年龄大于30,那么就不见了,因为30之后可能头发都没了,那么假如是小于等于30,则继续判断这个男性程序员的长相。如下图所示,某个女方在决定见不见男性程序员时,可能会有如下的决策逻辑。

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_decision_tree.png

那么上图中“年龄”、“长相”、“收入”、“公务员”这些绿色的结点,称为**“内部结点”**,对应的是对样本的某个属性的判断。测试或者判断的结果可能是对另一个属性的判断,也可能是一个“见”或”不见“的结果,即图中的橙色结点,称为**”叶结点“**,它表示的是”决策结果“,其中“年龄”这个绿色结点又称为**“根结点”**。决策就是从根结点开始走到叶结点的过程。每经过一个结点的判定,数据集就按照答案(属性值)划分为若干子集,**在子结点做判定时只需要考虑对应的数据子集就可以了**。其中根结点包含样本全集。

但是上述过程中,是已经给了一颗决策树,我们使用这个决策树的例子。然而机器学习中要做的是基于训练集得到一颗决策树,如上面的例子中,我们需要从大量的相亲数据中,训练得到如上的决策树。下面我们来讲一下如何构建决策树模型。

2. 构建决策树

2.1 构建流程

要创建类似上述的决策树结构,我们要依次选择出内部结点和叶结点。具体的构建流程如下,以伪代码的形式给出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# D 为数据集
# A 为属性集
函数 createTree(D,A){
    if D中样本属于同一类别 then
    	该结点为叶结点,return 类标签;
    end if 
    if A中的属性集为空 or D中样本在A上取值相同 then
    	该结点为叶结点,return D中相同类别标记数最多的类标签;
    end if
    从A中选择最优划分属性a;
    创建分支节点;
    for a的每一个值 av do
    	根据av划分出子集Dv,其中Dv表示D中在a属性上取值为av的样本子集;
    	createTree(Dv,A-av),A-av表示A中去掉av属性后的属性集合;
    end for
}

显然,决策树的创建过程是一个递归的过程。决策树基本算法中,有三种情形会导致递归返回:

  1. 当前结点包含的样本全属于同一类别,无须划分
  2. 当前结点属性集为空,或者所有样本在所有属性上取值相同,无法划分。

之后再从中选择出最优的划分属性a,根据属性a的不用取值,将齐划分成若干子集,并在子集上调用createTree()函数递归构建决策树。

2.2 选择划分属性

从上述的构建流程中我们可以看到,从A中选择最优划分属性a,是最重要的。那么下面来说一下这个划分属性的选择。决策树中常用的是使用“信息增益”、“增益率”、“基尼系数”这三种来选择出最佳的划分属性。

信息增益—ID3算法

ID3决策树学习算法,则是以信息增益为准则来选择划分属性。但是在讲解信息增益这个概念之前不得不讲一下“信息嫡(information entropy)”。信息嫡是度量样本集合纯度最常见的一种指标。假设当前样本集合D中第k类样本所占的比例为$p_k(k=1,2,…,|y|)$,则样本D的信息嫡定义为 $$ Ent(D) = -\sum_{k=1}^{|y|}p_klog_2p_k $$ 其中Ent(D)的值越小,则D的纯度越高。

我们拿极端例子进行考虑,比如样本集合中全都为同一类,那么Ent(D)为0;当样本集合中,每一个类别都只有一个样本,那么$log_2p_k$是最小的,且为负值,但是加和并取负号之后,就变为了一个正值,而且很大。

讲解完信息嫡之后,接下去我们讲解一下“信息增益”这个概念。假设离散属性a有V个可能的取值${a^1,a^2,…,,a^V}$,若使用a来对样本集合D进行划分,则会产生V个分支集合,其中第v个分支集合包含了D中在属性a上取值为$a^v$的所有样本,记为$D^v$。这个分支集合也会存在多个类别,那么根据信息嫡计算公式,可以计算出$D^v$的信息嫡,再考虑到不同的分支集合所包含的样本数不同,给分支集合赋予权重$|D^v|/|D|$。于是可计算出用属性a对样本集D进行划分所获得的“信息增益(information gain)” $$ Gain(D,a)=Ent(D)-\sum_{v=1}^V\frac{|D^v|}{|D|}Ent(D^v) $$

一般而言,信息增益越大,则使用属性a来进行划分所获得的“纯度提升”越大。因此我们选择当前数据集中能产生最大信息增益的属性作为当前的划分属性。

信息增益的个人理解: 信息增益,重点是在“增”这个字。Ent(D)表示的是当前数据集D的信息嫡,即当前的集合纯度,那么$\sum_{v=1}^V\frac{|D^v|}{|D|}Ent(D^v)$表示的是按照a属性进行划分之后,所产生的信息嫡的总和(进行相加可以当做还是在数据集D上进行统一比较),所以信息增益也就表示成了前者减后者。

增益率—C4.5算法

C4.5决策树算法采用“增益率”来选择最优划分属性。信息增益对可取值数目较多的属性所有偏好(如一个属性每一个属性值都只有一个样本,那么会选择这个),为减少这种偏好可能带来的不利影响,C4.5在其基础之上,引入了“增益率“。增益率定义为 $$ Gain_ratio(D,a) = \frac{Gain(D,a)}{IV(a)} $$ 其中 $$ IV(a)=-\sum_{v=1}^V\frac{|D^v|}{|D|}log_2\frac{|D^v|}{|D|} $$ 称为属性a的“固有值(intrinsic value)”,属性a的可能取值数目越多,则IV(a)的值值通常会越大。然而信息增益率对可取值数目较少的属性有所偏好。因为**C4.5算法并不是直接选择增益率最大的属性作为划分属性**,而采用了一个启发式。**先从候选划分属性中找出信息增益高于平均水平的属性,之后再从中选择增益率最高的属性作为最终的划分属性**。

基尼系数—CART算法

CART(Classification And Regression Tree)决策树算法采用“基尼指数(Gini index)”来选择划分属性。数据集D的纯度可用基尼值来度量: $$ \begin{aligned} Gini(D)=&\sum_{k=1}^{|y|}\sum_{k'\not=k}p_kp_k'\
=&1-\sum_{k=1}^{|y|}p_k^2 \end{aligned} $$ 直观来说,Gini(D)反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。因此,Gini(D)越小,则数据集D的纯度越高。在得到基尼值之后,属性a的基尼指数定义为 $$ Gini_index(D,a)=\sum_{v=1}^{V}\frac{|D^v|}{|D|}Gini(D^v) $$ 我们**选择那个使得划分后基尼指数最小的属性作为最优划分属性**。

2.3 决策树优化—剪枝

剪枝(pruning)是决策树学习算法对付“过拟合”的主要手段。在决策树学习中,为了尽可能正确分类训练样本,结点划分过程将不断重复,有时会造成决策树分支过多,这时就可能因训练样本学得”太好“了,导致过拟合。因此,可通过主动去掉一些分支来降低过拟合的风险。决策树剪枝的基本策略有”预剪枝“和”后剪枝“。

预剪枝

预剪枝是指在决策树生成过程中,对每个结点在划分之前先进行估计,若当前结点的划分不能带来决策树泛化能力的提升,则停止划分并将当前结点标记记为叶结点。

优点 预剪枝不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间开销。

缺点 有些分支当前划分虽不能提升泛化性能、甚至可能导致泛化性能暂时下降,但在其基础上进行的后续划分却有可能导致性能显著提高,预剪枝基于“贪心”的本质可能给预剪枝决策树带来了欠拟合的风险。

后剪枝

后剪枝则是先从训练集生成一颗完整的决策树,然后自底向上地对非叶节点进行考察,若将该结点对应的子树替换为叶结点能带来决策树泛化性能提升,则将该子树替换为叶结点。

优点 相比预剪枝,后剪枝决策树的欠拟合风险很小,泛化性能往往优于预剪枝决策树。

缺点 后剪枝在时间上的开销比未剪枝决策树和预剪枝决策树都要大很多,因为后剪枝过程是在生成完全决策树之后,再自底向上地对树中的所有非叶结点进行逐一考察。

3. 决策树构建的示例

3.1 ID3算法初步构建决策树

这边的例子不是代码的实现,而是手动计算一些去更好的理解上述的理论知识。本节例子中采用西瓜书中的例子,使用的数据集如下:

编号 色泽 根蒂 敲声 纹理 脐部 触感 好瓜
1 青绿 蜷缩 浊响 清晰 凹陷 硬滑
2 乌黑 蜷缩 沉闷 清晰 凹陷 硬滑
3 乌黑 蜷缩 浊响 清晰 凹陷 硬滑
4 青绿 蜷缩 沉闷 清晰 凹陷 硬滑
5 浅白 蜷缩 浊响 清晰 凹陷 硬滑
6 青绿 稍蜷 浊响 清晰 稍凹 软粘
7 乌黑 稍蜷 浊响 稍糊 稍凹 软粘
8 乌黑 稍蜷 浊响 清晰 稍凹 硬滑
9 乌黑 稍蜷 沉闷 稍糊 稍凹 硬滑
10 青绿 硬挺 清脆 清晰 平坦 软粘
11 浅白 硬挺 清脆 模糊 平坦 硬滑
12 浅白 蜷缩 浊响 模糊 平坦 软粘
13 青绿 稍蜷 浊响 稍糊 凹陷 硬滑
14 浅白 稍蜷 沉闷 稍糊 凹陷 硬滑
15 乌黑 稍蜷 浊响 清晰 稍凹 软粘
16 浅白 蜷缩 浊响 模糊 平坦 硬滑
17 青绿 蜷缩 沉闷 稍糊 稍凹 硬滑

该数据集D中包含17个训练样例,用来学习一颗能预测没剖开的西瓜是不是好瓜的决策树,其中主要分为两类样本。在决策树学习开始时,根结点包含D中的所有样例,那么可以计算出数据集D的信息嫡为 $$ Ent(D) = -\sum_{k=1}^2p_klog_2k=-(\frac{8}{17}log_2\frac{8}{17}+\frac{9}{17}log_2\frac{9}{17})=0.998 $$

然后我们要计算出当前属性集合{色泽,根蒂,敲声,纹理,脐部,触感}中每个属性的信息增益。以属性“色泽”为例,它有3个可能取值:{青绿,乌黑,浅白},若使用该属性进行划分的,则可得到3个子集,分别记为D1(色泽=青绿),D2(色泽=乌黑),D3(色泽=浅白),那么D1包含编号为{1,4,6,10,13,17}的6个样例,D1中正例占3/6,反例占3/6;D2包含编号为{2,3,7,8,9,15}的6个样例,D2中正例占4/6,反例占2/6;D3包含编号为{5,11,12,14,16}的5个样例,D3中正例占1/5,反例占4/5.那么可以计算用“色泽”划分之后所获得的3个子集的信息嫡分别为 $$ \begin{aligned} &Ent(D1) = -(\frac{3}{6}log_2\frac{3}{6}+\frac{3}{6}log_2\frac{3}{6}) = 1.000\
&Ent(D2) = -(\frac{4}{6}log_2\frac{4}{6}+\frac{2}{6}log_2\frac{2}{6}) = 0.918\
&Ent(D3) = -(\frac{1}{5}log_2\frac{1}{5}+\frac{4}{5}log_2\frac{4}{5}) = 0.722 \end{aligned} $$ 那么属性“色泽”的信息增益为 $$ \begin{aligned} Gain(D,色泽)=&Ent(D) - \sum_{v=1}^3\frac{|D_v|}{|D|}Ent(D^v)\
=&0.998 - (\frac{6}{17}\times1.000+\frac{6}{17}\times 0.918+\frac{5}{17}\times 0.722)\
=&0.109 \end{aligned} $$ 类似的可以计算出,其他属性的信息增益为: $$ Gain(D,根蒂) = 0.143\
Gain(D,敲声) = 0.141\
Gain(D,纹理) = 0.381\
Gain(D,脐部) = 0.289\
Gain(D,触感) = 0.006 $$ 根据ID3算法,我们选择产生信息增益的最大那个属性作为划分属性,即“纹理”这个属性。之后根据纹理这个属性,对根结点的数据集D进行划分,划分的结果以及各分支结点所包含的样例子集如下所示:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_split.png

然后,对每个分支结点做进一步划分,以此类推最终得到的决策树如下图所示:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_tree.png

3.2 剪枝处理的过程

接下去我们借这个例子再讲一下剪枝的过程。本次我们采用“留出法”,即预留一部分数据用作“验证集”以进行性能评估,来判断性能是否提升。例如我们随机将训练数据集D划分为两部分,编号为{1,2,3,6,7,10,14,15,16,17}的样例为训练集,编号为{4,5,8,9,11,12,13}的样例组成验证集。采用信息增益准则来进行划分属性选择,我们基于上述的训练集可以得到如下所示的决策树:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_pruning_0.png

预剪枝

我们先讨论预剪枝的过程,根据信息增益准则,我们会选择“脐部”来对训练集进行划分,并产生3个分支,如下图所示:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_pruning_1.png

但是我们需要考虑是否有必要进行这个划分?我们对划分前后的泛化性能进行估计。划分之前,所有样本集中在根结点,那么该结点应该是被标记为叶结点,选择训练集中最多的类别为该叶结点的类别标记,假设我们将这个叶结点标记为“好瓜”,接下去使用验证集对这个单结点决策树进行评估,其中3个样例分类正确,验证集精度为42.9%。

那么假如进行划分,即用属性“脐部”进行划分,可将数据集划分为{1,2,3,14}、{6,7,15,17}、{10,16}三个子集,这3个子集对应的结点被标记为叶结点分别为“好瓜”、“好瓜”、“坏瓜”,那么划分之后使用验证集对其进行评估,其中{4,5,8,11,12}的样例被分类正确,验证集精度为71.4%,这个精度比划分之前要高,所以进行划分。然后我们对上述的结点2进行划分,基于信息增益准则挑选出的划分属性为“色泽”,然而使用划分之后,验证集精度下降为57.1%,于是不进行划分。对于结点3,最优划分属性为“根蒂”,划分之后的精度仍为71.4%,并没有提升验证集精度,于是不进行划分。对于结点4,因为所含训练样例为同一类,所以不再进行划分。最终,基于预剪枝策略所生成的决策树如图所示:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_pruning_1.png

这是仅有一层划分的决策树,亦称“决策树桩”。

对比不进行预剪枝和进行预剪枝生成树我们可以发现,预剪枝使得决策树的很多分支都没有展开,这不仅降低了过拟合的风险,也减少了决策树的训练时间开销。然而也正如理论中所提到的那样,有些分支的当前划分虽不能提升泛化性能、甚至可能导致泛化性能暂时下降,但是在其基础上进行的后续划分却有可能,导致性能显著提高,这种基于“贪心”的本质给预剪枝决策树带来了欠拟合的风险。

后剪枝

后剪枝先从训练集生成一颗完整决策树,即[该小节](#3.2 剪枝处理的过程)第一幅图所示。

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_pruning_0.png

这个决策树在验证集上的精度为42.9%。那么根据后剪枝策略,首先考虑编号为6的结点,假如不对这个结点进行划分,即将其替换成一个叶结点,那么该叶结点将会被标记为“好瓜”,此时的决策树在验证集上的精度提升至57.1%,于是后剪枝策略决定剪枝。 然后考虑结点5,不对这个结点进行划分的话,那么该结点会被标记为好瓜,此时决策树在验证集上的精度仍为57.1%,于是不进行剪枝。 之后考虑结点2,若不对这个结点进行划分,那么该结点将会被标记为“好瓜”的叶结点,此时验证集精度提高到71 .4%,于是进行剪枝操作。

最后依次考虑结点3和结点1,若将其领衔的子树替换为叶结点,验证集精度分别为71.4%和42.9%,均未得到提升,所以不剪枝。最终基于后剪枝策略所生成的决策树如下图所示,验证集精度为71.4%:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_demo_pruning_2.png

对比预剪枝决策树和后剪枝决策树,可以发现后剪枝决策树通常比预剪枝决策树保留了更多的分支。所以一般情形下,后剪枝决策树的欠拟合风险很小,泛化能力往往优于预剪枝决策树。然而后剪枝过程是在生成完全决策树之后进行的,并且自底向上地对树中的所有非叶结点进行逐一考察,所以它的训练时间开销比未剪枝决策树和预剪枝决策树都要大很多。

4.代码的实现

4.1 递归构建决策树

我们根据决策树构建流程的伪代码,先来搭建出整个框架,最主要的递归构建决策树的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'''
递归创建树
'''
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) or propertyIsSame(dataSet):
        return majorityCnt(classList)

    bestFeat = chooseBestFeature(dataSet)
    bestFeatLable = labels[bestFeat]
    decisionTree = {bestFeatLable:{}}
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    del (labels[bestFeat])
    for value in uniqueVals:
        subLabels = labels[:]
        decisionTree[bestFeatLable][value]  = createTree(splitDataSet(dataSet, bestFeat, value),
                                                         subLabels)
    return decisionTree

上述递归构建树的代码中,假如所有属性都已经遍历完或者样本在属性上取值都相同,那么则采用多数表决法。其中判断样本在属性上取值是否相同的函数如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
'''
判断样本在属性集合上取值是否相同
'''
def propertyIsSame(dataSet):
    numFeatures = len(dataSet[0])-1
    for i in range(numFeatures):
        temp = dataSet[0][i]
        for data in dataSet:
            if data[i] != temp:
                return 0
    return 1

多数表决法的代码如下所示,主要思想就是对类别名进行计数,然后排序选择出数量最多的那个类别,并返回类标记:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'''
获取数量最多的类别
'''
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)

    return sortedClassCount[0][0]

然后是决策树学习算法的核心,如何选择最优的划分属性,实现的代码如下所示,下面的代码主要是针对ID3算法实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'''
选择最好的特征,ID3算法
'''
def chooseBestFeature(dataSet):
    numEntries = len(dataSet)
    numFeatures = len(dataSet[0])-1 # 特征数
    baseEnt = calcShannonEnt(dataSet)   # 总的信息嫡
    baseInfoGain = 0.0          # 信息增益
    bestFeature = -1            # 最好的特征的Index
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet]  # 获取该特征的所有特征值
        uniqueVals = set(featList)     # 获取特征的不同特征值集合
        newEnt = 0.0 # 按照此特征进行分类的信息嫡之和
        for val in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, val)
            prob = len(subDataSet)/float(numEntries)
            newEnt += prob * calcShannonEnt(subDataSet)
        infoGain = baseEnt - newEnt
        if infoGain > baseInfoGain :
            baseInfoGain = infoGain
            bestFeature = i

    return bestFeature

在选择最好的特征这段代码中,最主要的部分是信息嫡的计算,信息嫡的计算函数如下所示。首先获取各个类别的数量,之后基于获取的数量计算比列并进行加和。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
'''
信息嫡的计算
'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)   # 数据集的数量
    labelCounts = {}            # label及其数量
    # 统计label及对应的数量
    for featVec in dataSet:
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    # 计算香农嫡
    shannonEnt = 0.0    # 香农嫡
    for key in labelCounts.keys():
        prob = float(labelCounts[key]) / numEntries
        shannonEnt -= prob * math.log(prob, 2)
    return shannonEnt

在完成总的信息嫡计算之后,我们要计算各个属性的信息增益,因为已经实现了信息嫡的计算公式,所以只要把根据属性划分出的子集当成参数传入进去即可。下面代码的实现主要是传入数据集、划分属性以及相应的属性值,就可以将数据集划分出来,并返回针对某一个属性同一属性值的样本集合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
'''
划分出分类后的数据集
'''
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)
    return retDataSet

通过函数chooseBestFeature()选择出最优的划分属性之后,我们就创建相应的分支结点,我们使用字典类型来存储这颗决策树。在这之后我们根据选出的最优的划分属性,对数据集进行划分。并删掉已经使用过的属性,同时将删掉后的属性集合同划分出来的子集,一起当做参数传入createTree()函数,从而实现递归创建决策树。为什么在代码中要复制一份属性集合呢?因为Python中函数参数是列表类型时,参数是按照引用方式传递的,为了保证每次调用函数createTree()时不改变原始列表类型时,所以使用新的变量代替原始变量。并且划分的每个子集,都使用一份新的变量,假如使用同一份变量,仍然会出现问题。

通过上述的代码,只要传入符合一定格式的数据集即可学习得到一颗决策树了。下面我们来进行一次小测试,我们可以模拟出一些训练集数据,进行测试。模拟的训练集数据如下所示:

编号 不浮出睡眠是否可以生存 是否有脚蹼 属于鱼类
1
2
3
4
5

下面对上面的数据集进行转化,转化成方便用于决策树学习的数据集,如下所示。dataSet就是上面数据转化后的形式,最后一列的’yes’或者’or’是样本所属的类别;labels是属性的名字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
'''
创建数据集
'''
def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['生存', '脚蹼']
    return dataSet, labels

下面我们编写如下代码进行测试,首先是调用createDataSet()函数,得到所需要的数据集,之后使用函数createTree()学习得到一颗决策树,并将这颗树的存储结构打印出来,同时将这颗决策树画出来。

画决策树的代码因为不是本节决策树算法的核心,所以这边不阐述这块代码的实现,所需代码可以在文末给出的地址中下载得到。

1
2
3
4
5
6
7
8
9
# 创建示例数据集
dataSet, labels = createDataSet()

# 学习构建决策树
tree = createTree(dataSet, labels)
print(tree)

# 画决策树
TreePlot.createPlot(tree)

最终得到的输出结果如下:

1
{'生存': {0: 'no', 1: {'脚蹼': {0: 'no', 1: 'yes'}}}}

画出的决策树如图所示

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_code_example.png

上述使用ID3算法构建决策树,那么使用C4.5算法构建决策树时,更改chooseBestFeature()函数即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
'''
选择最好的属性,C4.5
'''
def chooseBestFeature(dataSet):
    numEntries = len(dataSet)   # 数据集数量
    numFeatures = len(dataSet[0]) - 1 # 属性的数量
    baseEnt = calcShannonEnt(dataSet)   # 整个数据集的香农嫡
    bestFeature = -1    # 选出的最好属性的index
    infoGains = []  # 存储各属性的信息增益
    gainRatios = []  # 存储各属性的增益率
    maxGainRatio = 0.0 # 增益率最大
    for i in range(numFeatures):
        newEnt = 0.0    # 划分之后的信息嫡之和
        numIV = 0.0     # 属性的固有值
        featList = [data[i] for data in dataSet]    # 获取所有该属性的所有值
        featSet = set(featList) # 获取该属性不同的值
        for value in featSet:
            subDataSet = splitDataSet(dataSet, i, value)    # 获取属性值相同的数据集
            prob = float(len(subDataSet)) / numEntries
            numIV -= prob * math.log(prob, 2)
            newEnt += prob * calcShannonEnt(subDataSet)
        newGain = baseEnt - newEnt  # 划分之后的信息增益
        newGainRatio = newGain/numIV    # 划分之后的增益率
        infoGains.append(newGain)
        gainRatios.append(newGainRatio)

    infoGainAvg = sum(infoGains) / len(infoGains) # 平均信息增益率

    for i in range(len(infoGains)):
        if (infoGains[i] >= infoGainAvg) and (gainRatios[i] > maxGainRatio):
            maxGainRatio = gainRatios[i]
            bestFeature = i

    return bestFeature

那么如何使用基尼指数来构建决策树呢?这块内容我们留到讲解树回归的时候再提这种方式。

那关于剪枝的内容呢?这块内容我们同样留到讲解树回归的时候再统一讲解。

4.2 使用决策树执行分类

现在我们拥有一颗了通过学习得到的决策树,那么我们该如何应用这颗决策树来进行预测呢?其实道理同刚开始引入的例子一样,只是我们这边使用代码进行实现。预测的过程其实和学习构建的过程使用都是递归思想。下面是使用决策树进行预测的具体实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'''
测试决策树
'''
def classifyDecisionTree(tree, featLabels, testVec):
    firstStr = list(tree.keys())[0]
    secondDict = tree[firstStr]
    featIndex= featLabels.index(firstStr)
    if type(secondDict[testVec[featIndex]]).__name__ == 'dict':
        classLabel = classifyDecisionTree(secondDict[testVec[featIndex]], featLabels, testVec)
    else:
        classLabel = secondDict[testVec[featIndex]]
    return classLabel

我们使用如下代码进行对该函数进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 创建示例数据集
dataSet, labels = createDataSet()
lebelsCopy = labels[:]

# 学习构建决策树
tree = createTree(dataSet, labels)
print(tree)

# 画决策树
TreePlot.createPlot(tree)

print(classifyDecisionTree(tree, lebelsCopy, [1, 1]))

最终输出的结果为yes,与上述学习得到的决策树的判断结果相一致。

4.3 决策树的存储

本例子因为使用的数据集数量较少,所以训练时间很短。但是当数据集很大的时候,将会耗费很多计算时间,然而应用已经创建好的决策树解决分类问题是很快的。所以为了节省时间,最好能够在每次执行分类时调用已经构造好的决策树。为了解决这个问题,我们需要将构建好的决策树保存下来,需要用到的是Python模块pickle序列化对象。任何对象都可以执行序列化操作,字典对象也不例外。下面是使用pickle模块来存储决策树和读取决策树:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'''
存储序列化对象
'''
def storeTree(tree, filename):
    fw = open(filename, 'wb')
    pickle.dump(tree, fw)
    fw.close()


'''
读取序列化对象
'''
def grabTree(filename):
    fr = open(filename, "rb")
    return pickle.load(fr)

使用下面的代码对存储与读取效果进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 创建示例数据集
dataSet, labels = createDataSet()
lebelsCopy = labels[:]

# 学习构建决策树
tree = createTree(dataSet, labels)
print(tree)

# 序列化存储树结构
storeTree(tree, "object.txt")
# 文件中读取数结构
myTree = grabTree("object.txt")
print(myTree)
print(classifyDecisionTree(myTree, lebelsCopy, [1, 1]))

上述的输出结果为:

1
2
3
{'生存': {0: 'no', 1: {'脚蹼': {0: 'no', 1: 'yes'}}}}
{'生存': {0: 'no', 1: {'脚蹼': {0: 'no', 1: 'yes'}}}}
yes

可以看出,存储与读取都是没问题的,相当nice。

5. 实战:使用决策树预测隐形眼镜的类型

这边我们通过一个实战的例子来讲解决策树如何预测患者需要佩戴的隐形眼镜类型。隐形眼镜数据集是非常著名的数据集,它包含很多患者眼部状况的观察条件以及医生推荐的隐形眼镜类型(硬材质、软材质以及不适合佩戴隐形眼镜)。数据来源于UCI数据库,为了更容易显示数据,本书对数据做了简单的更改。部分数据集如下所示:

1
2
3
4
5
6
7
young	myope	no	reduced	no lenses
young	myope	no	normal	soft
young	myope	yes	reduced	no lenses
...
presbyopic	hyper	no	normal	soft
presbyopic	hyper	yes	reduced	no lenses
presbyopic	hyper	yes	normal	no lenses

我们首先读取文件加载需要的数据集,实现的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'''
从文件中读取数据集
'''
def loadDataSet(filePath):
    fr = open(filePath)
    lensesLable = ['age', 'prescript', 'astigmatic', 'tearRate']    # 属性名
    lensesData = [] # 隐形眼镜数据
    # 读取文件
    for line in fr.readlines():
        data = line.strip().split("\t")
        lensesData.append(data)
    return lensesData, lensesLable

接下来是对得到的数据集进行学习从而得到一颗决策树,这块使用的代码,与上面的讲述的一模一样,所以这边不再赘述。这边只贴出最终的集合版的代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import operator
import math
import TreePlot

'''
从文件中读取数据集
'''
def loadDataSet(filePath):
    fr = open(filePath)
    lensesLable = ['age', 'prescript', 'astigmatic', 'tearRate']    # 属性名
    lensesData = [] # 隐形眼镜数据
    # 读取文件
    for line in fr.readlines():
        data = line.strip().split("\t")
        lensesData.append(data)
    return lensesData, lensesLable


'''
选取数量最多的类别
'''
def majorityClass(classList):
    classCount = {} # 类别计数 类别:数量
    # 遍历进行计数
    for value in classList:
        if value not in classCount.keys():
            classCount[value] = 0
        classCount[value] += 1
    # 选择数量最多的类别
    classSort = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)

    return classSort[0][0]


'''
数据集进行划分
'''
def splitDataSet(dataSet, index, value):
    subDataSet = []
    for data in dataSet:
        if data[index] == value:
            subData = data[:index]
            subData.extend(data[index+1:])
            subDataSet.append(subData)
    return subDataSet


'''
计算信息嫡
'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)   # 数据集长度
    classList = [data[-1] for data in dataSet]  # 数据集的类别集合
    classCount = {} # 类别计数
    shannonEnt = 0.0   # 信息嫡
    # 遍历计数
    for value in classList:
        if value not in classCount.keys():
            classCount[value] = 0
        classCount[value] += 1

    # 计算信息嫡
    for key in classCount.keys():
        prob = float(classCount[key])/numEntries
        shannonEnt -= prob * math.log(prob, 2)

    return shannonEnt


'''
选择最好的属性,ID3
'''
def chooseBestFeature(dataSet):
    numEntries = len(dataSet)   # 数据集数量
    numFeatures = len(dataSet[0]) - 1 # 属性的数量
    baseEnt = calcShannonEnt(dataSet)   # 整个数据集的香农嫡
    bestFeature = -1    # 选出的最好属性的index
    maxGain = 0.0    # 记录最大的信息增益
    for i in range(numFeatures):
        newEnt = 0.0    # 划分之后的信息嫡之和
        featList = [data[i] for data in dataSet]    # 获取所有该属性的所有值
        featSet = set(featList) # 获取该属性不同的值
        for value in featSet:
            subDataSet = splitDataSet(dataSet, i, value)    # 获取属性值相同的数据集
            prob = float(len(subDataSet)) / numEntries
            newEnt += prob * calcShannonEnt(subDataSet)
        newGain = baseEnt - newEnt
        if newGain > maxGain:
            maxGain = newGain
            bestFeature = i

    return bestFeature


'''
判断样本在属性集合上取值是否相同
'''
def propertyIsSame(dataSet):
    numFeatures = len(dataSet[0])-1
    for i in range(numFeatures):
        temp = dataSet[0][i]
        for data in dataSet:
            if data[i] != temp:
                return 0
    return 1


'''
递归创建决策树
'''
def createTree(dataSet, labels):
    classList = [data[-1] for data in dataSet]  # 获取数据集中所属类别的数据
    # 检测数据集是否符合同一个分类,相同则返回
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 假如属性用完了,那么选择当前数据集中数量最多的类别
    if (len(dataSet[0]) == 1) or propertyIsSame(dataSet):
        return majorityClass(classList)

    # 选取最好的属性,采取ID3
    bestFeature = chooseBestFeature(dataSet)    # 最好属性的index
    bestFeatLabel = labels[bestFeature] # 最好属性的名字
    decisionTree = {bestFeatLabel:{}}   # 存储决策树
    featValues = [data[bestFeature] for data in dataSet]
    featValuesSet = set(featValues)   # 不同类别的集合
    del(labels[bestFeature])
    for value in featValuesSet:
        subLabels = labels[:]   # 针对划分之后的数据集都要有一个新的labels
        decisionTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeature,
                                                                     value), subLabels)
    return decisionTree


'''
使用决策树进行预测
'''
def classifyDecisionTree(tree, labels, testData):
    firstLabel = list(tree.keys())[0]
    firstLabelIndex = labels.index(firstLabel)
    secondDict = tree[firstLabel]
    value = testData[firstLabelIndex]
    classLabel = None
    if type(secondDict[value]).__name__ == "dict":
        classLabel = classifyDecisionTree(secondDict[value], labels, testData)
    else:
        classLabel = secondDict[value]

    return classLabel


if __name__ == '__main__':
    # 读取数据
    lensesData, lensesLable = loadDataSet("lenses.txt")

    # 复制一份属性标签
    # createTree()操作会影响传入的类别标签
    lensesLableCopy = lensesLable[:]

    # 创建树
    decisionTree = createTree(lensesData, lensesLableCopy)
    print(decisionTree)

    # 对树进行绘图
    TreePlot.createPlot(decisionTree)

    # 进行预测
    classLabel = classifyDecisionTree(decisionTree, lensesLable, ["young", "hyper", "yes", "normal"])
    print(classLabel)

控制台输出的结果如下所示:

1
2
{'tearRate': {'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses', 'young': 'hard', 'presbyopic': 'no lenses'}}, 'myope': 'hard'}}, 'no': {'age': {'pre': 'soft', 'young': 'soft', 'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}}}}}, 'reduced': 'no lenses'}}
hard

最终得到的决策树如图所示:

https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_code_exID3.png

上述采用的ID3算法构建决策树,那么采用C4.5算法构建决策树,主要将chooseBestFeature()函数进行替换即可。最终控制台输出的内容如下所示:

1
2
{'tearRate': {'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses', 'presbyopic': 'no lenses', 'young': 'hard'}}, 'myope': 'hard'}}, 'no': {'age': {'pre': 'soft', 'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft'}}}}, 'reduced': 'no lenses'}}
hard

得到的决策树如下图所示: https://img.dawnguo.cn/MachineLearning/BasicAlgorithm/6_code_exc45.png

附:

本文所有用到的数据集和代码均可在下面的链接中找到 https://github.com/DawnGuoDev/MachineLearningStudy/tree/master/DecisionTree

本文参考及推荐内容

  1. 周志华,机器学习
  2. Peter Harrington,机器学习实战
  3. CSDN_决策树算法及python实现