6、决策树(decision tree, DT)算法——监督分类

时间:2024-02-29 16:43:09

1、决策树(decision tree)算法

决策树(decision tree)算法是根据给定的训练数据集构建一个决策树模型,使它能够对实例进行正确地分类,本质是从训练集中归纳出一组分类规则。决策树生成方法包括ID3C4.5CART等。

优点:

  • 易于理解和解释,决策树可以可视化。
  • 几乎不需要数据预处理。其他方法经常需要数据标准化,创建虚拟变量和删除缺失值。决策树还不支持缺失值。
  • 使用树的花费(例如预测数据)是训练数据点(data points)数量的对数。 可以同时处理数值变量和分类变量。其他方法大都适用于分析一种变量的集合。
  • 可以处理多值输出变量问题。 使用白盒模型。如果一个情况被观察到,使用逻辑判断容易表示这种规则。相反,如果是黑盒模型(例如人工神经网络),结果会非常难解释。
  • 即使对真实模型来说,假设无效的情况下,也可以较好的适用。

缺点:

  • 决策树学习可能创建一个过于复杂的树,并不能很好的预测数据(过拟合)。
  • 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一棵完全不同的树。
  • 难以产生一棵全家最优的决策树。对样本和特征随机抽样可以降低整体效果偏差。
  • 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,在训练之前,需要先抽样使样本均衡。

适用数据类型:数值型和标称型。

2、决策树算法基本原理

决策树算法主要包括三个部分:特征选择、树的生成、树的剪枝。常用算法有 ID3、C4.5、CART。 

  • 特征选择:特征选择在于选取对训练数据具有分类能力的特征。通常特征选择的准则是:信息增益、信息增益比、基尼指数; 
  • 决策树的生成:通常是利用信息增益最大、信息增益比最大、Gini 指数最小作为特征选择的准则。从根节点开始,递归的生成决策树。相当于是不断选取局部最优特征,或将训练集分割为基本能够正确分类的子集; 
  • 决策树的剪枝:决策树的剪枝是为了防止树的过拟合,增强其泛化能力。包括预剪枝和后剪枝。

3、特征选择

特征选择在于选取对训练数据具有分类能力的特征。通常特征选择的准则是:信息增益信息增益比基尼指数。 

1) 熵(entropy)

熵表示随机变量不确定性的度量。

设$X$是一个取有限个值的离散随机变量,其概率分布为:
$P\left( {X = {x_i}} \right) = {p_i}{\rm{,}} \quad i = 1{\rm{,}}2{\rm{,}} \cdots {\rm{,}}n$

则随机变量$X$的熵定义为:

$H\left( X \right) =  - \sum\limits_{i = 1}^n {{p_i}{\rm{log }}{p_i}} $

其中,定义$0{\rm{log}}0 = 0$,对数$log$以2或e为底。

2)条件熵(conditional entropy)

条件熵$H\left( {Y|X} \right)$表示在已知随机变量$X$的条件下随机变量$Y$的不确定性。

设有随机变量$\left( {X{\rm{,}}Y} \right)$,其联合概率分布为:
$P\left( {X = {x_i}{\rm{,}}Y = {y_j}} \right) = {p_{ij}}{\rm{,}} \quad i = 1{\rm{,}}2{\rm{,}} \cdots {\rm{,}}n{\rm{;}} \quad j = 1{\rm{,}}2{\rm{,}} \cdots {\rm{,}}m$

则给定随机变量$X$的条件下随机变量$Y$的条件熵定义为:

$H\left( {Y|X} \right) = \sum\limits_{i = 1}^n {{p_i}H\left( {Y|X = {x_i}} \right)} $

其中,定义$0{\rm{log}}0 = 0$,${p_i} = P\left( {X = {x_i}} \right)$,$i = 1{\rm{,}}2{\rm{,}} \cdots {\rm{,}}n$。

3)信息增益(information gain)

信息增益表示得知特征$X$的信息而使得类$Y$的信息的不确定性减少的程度。

信息增益是相对于特征而言的。所以,特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即:

$g\left( {D{\rm{,}}A} \right) = H\left( D \right) - H\left( {D|A} \right)$

显然,对于数据集D而言,信息增益依赖于特征,不同的特征往往具有不同的信息增益。

信息增益大的特征,具有更强的分类能力(评判准则1)。

4)信息增益比

以信息增益作为划分训练数据集的特征,存在偏向于选择取值较多的特征的问题。使用信息增益比可以对这一问题进行校正(增加了一个惩罚项),这是特征选择的另一准则。

特征A对训练数据集D的信息增益比${g_R}\left( {D{\rm{,}}A} \right)$定义为其信息增益${g\left( {D{\rm{,}}A} \right)}$与训练数据集D关于特征A的经验熵${{H_A}\left( D \right)}$之比,即:

${g_R}\left( {D{\rm{,}}A} \right) = \frac{{g\left( {D{\rm{,}}A} \right)}}{{{H_A}\left( D \right)}}$

5)基尼指数

分类问题中,假设有K个类,样本点属于第k类的概率为${p_k}$,则概率分布的基尼指数定义为:

$Gini\left( p \right) = \sum\limits_{k = 1}^K {{p_k}\left( {1 - {p_k}} \right)}  = 1 - \sum\limits_{k = 1}^K {p_k^2} $

特别地,对于给定的二分类问题,若样本点属于第1类的概率为p,则概率分布的基尼指数为:

$Gini\left( p \right) = 2p\left( {1 - p} \right)$

在特征A的条件下,样本集合D的基尼指数定义为

$Gini\left( {D{\rm{,}}A = a} \right) = \frac{{\left| A \right|}}{{\left| D \right|}}Gini\left( A \right) + \frac{{\left| {\overline A } \right|}}{{\left| D \right|}}Gini\left( {\overline A } \right)$

其中,$a$为特征A的一个取值情况。

基尼指数值越大,样本集合的不确定性越大,与熵相似。

基尼指数与熵比较:

既然这两个都可以表示数据的不确定性,不纯度。那么这两个有什么区别?

Gini 指数的计算不需要对数运算,更加高效;

Gini 指数更偏向于连续属性,熵更偏向于离散属性。

6)实例

下表为一个由15个样本组成的贷款申请训练数据集D,数据包括贷款申请人的4个特征(属性)和是否同意贷款的类别信息。

特征1:年龄A1={青年,中年,老年},

特征2:有工作A2={是,否},

特征3:有自己的房子A3={是,否},

特征4:贷款情况A4={一般,好,非常好}。

类别:贷款与否={是,否}。

实例1:由上表给出的训练集D,根据信息增益准则选择最优特征。

解:

1)首先计算经验熵$H\left( D \right)$。

$H\left( D \right) =  - \frac{9}{{15}}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{9}{{15}} - \frac{6}{{15}}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{6}{{15}}{\rm{ = 0}}{\rm{.971 }}$

2)然后计算各特征对数据集D的信息增益。

特征1中的年龄A1有三种取值,分别为青年,中年,老年。

$\begin{array}{l}
g\left( {D{\rm{,}}{A_1}} \right) = H\left( D \right) - H\left( {D|{A_1}} \right)\\
= {\rm{0}}{\rm{.971}} - \left\{ {\frac{5}{{15}}\left( { - \frac{2}{5}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{2}{5} - \frac{3}{5}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{3}{5}} \right) + \frac{5}{{15}}\left( { - \frac{3}{5}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{3}{5} - \frac{2}{5}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{2}{5}} \right) + \frac{5}{{15}}\left( { - \frac{4}{5}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{4}{5} - \frac{1}{5}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{1}{5}} \right)} \right\}\\
= 0.083
\end{array}$

特征2中的有工作A2有两种取值,分别为是,否。

$\begin{array}{l}
g\left( {D{\rm{,}}{A_2}} \right) = H\left( D \right) - H\left( {D|{A_2}} \right)\\
= {\rm{0}}{\rm{.971}} - \left\{ {\frac{5}{{15}} \times 0 + \frac{{10}}{{15}}\left( { - \frac{4}{{10}}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{4}{{10}} - \frac{6}{{10}}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{6}{{10}}} \right)} \right\} = 0.324
\end{array}$

特征3中的贷款情况A3有两种取值,分别为是,否。

$\begin{array}{l}
g\left( {D{\rm{,}}{A_3}} \right) = H\left( D \right) - H\left( {D|{A_3}} \right)\\
= {\rm{0}}{\rm{.971}} - \left\{ {\frac{6}{{15}} \times 0 + \frac{9}{{15}}\left( { - \frac{3}{9}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{3}{9} - \frac{6}{9}{\rm{lo}}{{\rm{g}}_{\rm{2}}}\frac{6}{9}} \right)} \right\}  = 0.420
\end{array}$

特征4中的贷款情况A4有三种取值,分别为一般,好,非常好。

$g\left( {D{\rm{,}}{A_4}} \right) = H\left( D \right) - H\left( {D|{A_4}} \right) = 0.363$

最后,比较各特征的信息增益值,由于特征$A_3$的信息增益值最大,所以选择该特征值作为最优特征。

实例2:利用代码计算信息增益。

from math import log

"""
函数说明:创建测试数据集
Parameters:无
Returns:
    dataSet:数据集
    labels:分类属性
"""
def creatDataSet():
    # 数据集
    dataSet=[[0, 0, 0, 0, \'no\'],
            [0, 0, 0, 1, \'no\'],
            [0, 1, 0, 1, \'yes\'],
            [0, 1, 1, 0, \'yes\'],
            [0, 0, 0, 0, \'no\'],
            [1, 0, 0, 0, \'no\'],
            [1, 0, 0, 1, \'no\'],
            [1, 1, 1, 1, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [2, 0, 1, 2, \'yes\'],
            [2, 0, 1, 1, \'yes\'],
            [2, 1, 0, 1, \'yes\'],
            [2, 1, 0, 2, \'yes\'],
            [2, 0, 0, 0, \'no\']]
    #分类属性
    labels=[\'年龄\',\'有工作\',\'有自己的房子\',\'信贷情况\']
    #返回数据集和分类属性
    return dataSet,labels


"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:经验熵
"""
def calcShannonEnt(dataSet):
    #返回数据集行数
    numEntries=len(dataSet)
    #保存每个标签(label)出现次数的字典
    labelCounts={}
    #对每组特征向量进行统计
    for featVec in dataSet:
        currentLabel=featVec[-1]                     #提取标签信息
        if currentLabel not in labelCounts.keys():   #如果标签没有放入统计次数的字典,添加进去
            labelCounts[currentLabel]=0
        labelCounts[currentLabel]+=1                 #label计数

    shannonEnt=0.0                                   #经验熵
    #计算经验熵
    for key in labelCounts:
        prob=float(labelCounts[key])/numEntries      #选择该标签的概率
        shannonEnt-=prob*log(prob,2)                 #利用公式计算
    return shannonEnt                                #返回经验熵


"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:信息增益最大特征的索引值

"""
def chooseBestFeatureToSplit(dataSet):
    #特征数量
    numFeatures = len(dataSet[0]) - 1
    #计数数据集的香农熵
    baseEntropy = calcShannonEnt(dataSet)
    #信息增益
    bestInfoGain = 0.0
    #最优特征的索引值
    bestFeature = -1
    #遍历所有特征
    for i in range(numFeatures):
        # 获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        #创建set集合{},元素不可重复
        uniqueVals = set(featList)
        #经验条件熵
        newEntropy = 0.0
        #计算信息增益
        for value in uniqueVals:
            #subDataSet划分后的子集
            subDataSet = splitDataSet(dataSet, i, value)
            #计算子集的概率
            prob = len(subDataSet) / float(len(dataSet))
            #根据公式计算经验条件熵
            newEntropy += prob * calcShannonEnt((subDataSet))
        #信息增益
        infoGain = baseEntropy - newEntropy
        #打印每个特征的信息增益
        print("第%d个特征的增益为%.3f" % (i, infoGain))
        #计算信息增益
        if (infoGain > bestInfoGain):
            #更新信息增益,找到最大的信息增益
            bestInfoGain = infoGain
            #记录信息增益最大的特征的索引值
            bestFeature = i
            #返回信息增益最大特征的索引值
    return bestFeature

"""
函数说明:按照给定特征划分数据集
Parameters:
    dataSet:待划分的数据集
    axis:划分数据集的特征
    value:需要返回的特征的值
Returns:
    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

#main函数
if __name__==\'__main__\':
    dataSet,features=creatDataSet()
    # print(dataSet)
    # print(calcShannonEnt(dataSet))
    print("最优索引值:"+str(chooseBestFeatureToSplit(dataSet)))
View Code

实验结果:

第0个特征的增益为0.083
第1个特征的增益为0.324
第2个特征的增益为0.420
第3个特征的增益为0.363
最优索引值:2

实例3:由上表给出的训练集D,求特征$A_1$的基尼指数。

解:特征1中的年龄A1有三种取值,分别为青年(1),中年(2),老年(3)。

$Gini\left( {D{\rm{,}}{A_1} = 1} \right) = \frac{5}{{15}}\left( {2 \times \frac{2}{5} \times \left( {1 - \frac{2}{5}} \right)} \right) + \frac{{10}}{{15}}\left( {2 \times \frac{7}{{10}} \times \left( {1 - \frac{7}{{10}}} \right)} \right)$

$Gini\left( {D{\rm{,}}{A_1} = 2} \right) = 0.48$

$Gini\left( {D{\rm{,}}{A_1} = 3} \right) = 0.44$

由于第一项和第三项相等,且最小,所以均可作为$A_1$的最优切分点。

4、决策树的生成

1)ID3算法

ID3算法的核心是在决策树各个结点上对应信息增益准则选择特征,递归地构建决策树。

具体方法是:

1)从根结点(root node)开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子节点。

2)再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止;

3)最后得到一个决策树。

ID3相当于用极大似然法进行概率模型的选择。

算法如下:

输入:训练数据集D,特征集A,阈值$\varepsilon$;

输出:决策树T

1) 若D中所有实例属于同一类${C_k}$,则T为单结点树,并将类${C_k}$作为该结点的类标记,返回T;

2)若$A = \emptyset $,则T为单结点树,并将D中实例数最大的类${C_k}$作为该结点的类标记,返回T;

3)否则,计算A中各特征对D的信息增益,选择信息增益最大的特征$A_g$;

4)如果$A_g$的信息增益小于阈值$\varepsilon$,则置T为单结点树,并将D中实例数最大的类${C_k}$作为该结点的类标记,返回T;

5)否则,对$A_g$的每一可能值${a_i}$,依${A_g} = {a_i}$将D分割为若干非空子集$D_i$,将$D_i$中实例数最大的类作为标记,构建子结点,由结点及其子结点构成数T,返回T;

6)对第$i$个子结点,以$D_i$为训练集,以$A - \left\{ {{A_g}} \right\}$为特征集,递归地调用步骤1)- 5),得到子树${T_i}$,返回${T_i}$。

实例1:用上表的训练集,利用ID3算法建立决策树。

解:上面已经求得,特征$A_3$(有自己的房子)的信息增益最大,所以选择A3为根结点的特征,它将训练集D划分为两个子集$D_1$($A_3$取值为“是”)和$D_2$($A_3$取值为“否”)。由于$D_1$只有同一类的样本点,所以它成为一个叶结点,结点的类标记为“是”。

对$D_2$则需要从特征$A_1$(年龄),$A_2$(有工作)和$A_4$(信贷情况)中选择新的特征,计算各个特征的信息增益:

$g\left( {{D_2}{\rm{,}}{A_1}} \right) = H\left( {{D_2}} \right) - H\left( {{D_2}|{A_1}} \right) = 0.918 - 0.667 = 0.251$

$g\left( {{D_2}{\rm{,}}{A_2}} \right) = H\left( {{D_2}} \right) - H\left( {{D_2}|{A_2}} \right) = 0.918$

$g\left( {{D_2}{\rm{,}}{A_4}} \right) = H\left( {{D_2}} \right) - H\left( {{D_2}|{A_4}} \right) = 0.474$

根据计算,选择信息增益最大的A2作为节点的特征,由于其有两个取值可能,所以引出两个子节点:

①对应“是”(有工作),包含三个样本,属于同一类,所以是一个叶子节点,类标记为“是”;

②对应“否”(无工作),包含六个样本,输入同一类,所以是一个叶子节点,类标记为“否”。

这样就生成一个决策树,该树只用了两个特征(有两个内部节点),生成的决策树如下图所示:

实例2:利用代码编写ID3算法

# -*- coding: utf-8 -*-
from math import log
import operator

"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:经验熵
Modify:
    2018-03-12

"""
def calcShannonEnt(dataSet):
    #返回数据集行数
    numEntries=len(dataSet)
    #保存每个标签(label)出现次数的字典
    labelCounts={}
    #对每组特征向量进行统计
    for featVec in dataSet:
        currentLabel=featVec[-1]                     #提取标签信息
        if currentLabel not in labelCounts.keys():   #如果标签没有放入统计次数的字典,添加进去
            labelCounts[currentLabel]=0
        labelCounts[currentLabel]+=1                 #label计数

    shannonEnt=0.0                                   #经验熵
    #计算经验熵
    for key in labelCounts:
        prob=float(labelCounts[key])/numEntries      #选择该标签的概率
        shannonEnt-=prob*log(prob,2)                 #利用公式计算
    return shannonEnt                                #返回经验熵

"""
函数说明:创建测试数据集
Parameters:无
Returns:
    dataSet:数据集
    labels:分类属性
Modify:
    2018-03-13

"""
def createDataSet():
    # 数据集
    dataSet=[[0, 0, 0, 0, \'no\'],
            [0, 0, 0, 1, \'no\'],
            [0, 1, 0, 1, \'yes\'],
            [0, 1, 1, 0, \'yes\'],
            [0, 0, 0, 0, \'no\'],
            [1, 0, 0, 0, \'no\'],
            [1, 0, 0, 1, \'no\'],
            [1, 1, 1, 1, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [2, 0, 1, 2, \'yes\'],
            [2, 0, 1, 1, \'yes\'],
            [2, 1, 0, 1, \'yes\'],
            [2, 1, 0, 2, \'yes\'],
            [2, 0, 0, 0, \'no\']]
    #分类属性
    labels=[\'年龄\',\'有工作\',\'有自己的房子\',\'信贷情况\']
    #返回数据集和分类属性
    return dataSet,labels

"""
函数说明:按照给定特征划分数据集

Parameters:
    dataSet:待划分的数据集
    axis:划分数据集的特征
    value:需要返回的特征值
Returns:
    无
Modify:
    2018-03-13

"""
def splitDataSet(dataSet,axis,value):
    #创建返回的数据集列表
    retDataSet=[]
    #遍历数据集
    for featVec in dataSet:
        if featVec[axis]==value:
            #去掉axis特征
            reduceFeatVec=featVec[:axis]
            #将符合条件的添加到返回的数据集
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)
    #返回划分后的数据集
    return retDataSet

"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:信息增益最大特征的索引值
Modify:
    2018-03-13

"""


def chooseBestFeatureToSplit(dataSet):
    #特征数量
    numFeatures = len(dataSet[0]) - 1
    #计数数据集的香农熵
    baseEntropy = calcShannonEnt(dataSet)
    #信息增益
    bestInfoGain = 0.0
    #最优特征的索引值
    bestFeature = -1
    #遍历所有特征
    for i in range(numFeatures):
        # 获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        #创建set集合{},元素不可重复
        uniqueVals = set(featList)
        #经验条件熵
        newEntropy = 0.0
        #计算信息增益
        for value in uniqueVals:
            #subDataSet划分后的子集
            subDataSet = splitDataSet(dataSet, i, value)
            #计算子集的概率
            prob = len(subDataSet) / float(len(dataSet))
            #根据公式计算经验条件熵
            newEntropy += prob * calcShannonEnt((subDataSet))
        #信息增益
        infoGain = baseEntropy - newEntropy
        #打印每个特征的信息增益
        print("第%d个特征的增益为%.3f" % (i, infoGain))
        #计算信息增益
        if (infoGain > bestInfoGain):
            #更新信息增益,找到最大的信息增益
            bestInfoGain = infoGain
            #记录信息增益最大的特征的索引值
            bestFeature = i
            #返回信息增益最大特征的索引值
    return bestFeature

"""
函数说明:统计classList中出现次数最多的元素(类标签)
Parameters:
    classList:类标签列表
Returns:
    sortedClassCount[0][0]:出现次数最多的元素(类标签)
Modify:
    2018-03-13

"""
def majorityCnt(classList):
    classCount={}
    #统计classList中每个元素出现的次数
    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]

"""
函数说明:创建决策树

Parameters:
    dataSet:训练数据集
    labels:分类属性标签
    featLabels:存储选择的最优特征标签
Returns:
    myTree:决策树
Modify:
    2018-03-13

"""
def createTree(dataSet,labels,featLabels):
    #取分类标签(是否放贷:yes or no)
    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]
    featLabels.append(bestFeatLabel)
    #根据最优特征的标签生成树
    myTree={bestFeatLabel:{}}
    #删除已经使用的特征标签
    del(labels[bestFeat])
    #得到训练集中所有最优特征的属性值
    featValues=[example[bestFeat] for example in dataSet]
    #去掉重复的属性值
    uniqueVls=set(featValues)
    #遍历特征,创建决策树
    for value in uniqueVls:
        myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),
                                               labels,featLabels)
    return myTree

if __name__==\'__main__\':
    dataSet,labels=createDataSet()
    featLabels=[]
    myTree=createTree(dataSet,labels,featLabels)
    print(myTree)
View Code

实验结果:

第0个特征的增益为0.083
第1个特征的增益为0.324
第2个特征的增益为0.420
第3个特征的增益为0.363
第0个特征的增益为0.252
第1个特征的增益为0.918
第2个特征的增益为0.474
{\'有自己的房子\': {0: {\'有工作\': {0: \'no\', 1: \'yes\'}}, 1: \'yes\'}}

实例3:用ID3算法进行分类预测

# -*- coding: utf-8 -*-
from math import log
import operator

"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:经验熵
Modify:
    2018-03-12

"""
def calcShannonEnt(dataSet):
    #返回数据集行数
    numEntries=len(dataSet)
    #保存每个标签(label)出现次数的字典
    labelCounts={}
    #对每组特征向量进行统计
    for featVec in dataSet:
        currentLabel=featVec[-1]                     #提取标签信息
        if currentLabel not in labelCounts.keys():   #如果标签没有放入统计次数的字典,添加进去
            labelCounts[currentLabel]=0
        labelCounts[currentLabel]+=1                 #label计数

    shannonEnt=0.0                                   #经验熵
    #计算经验熵
    for key in labelCounts:
        prob=float(labelCounts[key])/numEntries      #选择该标签的概率
        shannonEnt-=prob*log(prob,2)                 #利用公式计算
    return shannonEnt                                #返回经验熵

"""
函数说明:创建测试数据集
Parameters:无
Returns:
    dataSet:数据集
    labels:分类属性
Modify:
    2018-03-13

"""
def createDataSet():
    # 数据集
    dataSet=[[0, 0, 0, 0, \'no\'],
            [0, 0, 0, 1, \'no\'],
            [0, 1, 0, 1, \'yes\'],
            [0, 1, 1, 0, \'yes\'],
            [0, 0, 0, 0, \'no\'],
            [1, 0, 0, 0, \'no\'],
            [1, 0, 0, 1, \'no\'],
            [1, 1, 1, 1, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [2, 0, 1, 2, \'yes\'],
            [2, 0, 1, 1, \'yes\'],
            [2, 1, 0, 1, \'yes\'],
            [2, 1, 0, 2, \'yes\'],
            [2, 0, 0, 0, \'no\']]
    #分类属性
    labels=[\'年龄\',\'有工作\',\'有自己的房子\',\'信贷情况\']
    #返回数据集和分类属性
    return dataSet,labels

"""
函数说明:按照给定特征划分数据集

Parameters:
    dataSet:待划分的数据集
    axis:划分数据集的特征
    value:需要返回的特征值
Returns:
    无
Modify:
    2018-03-13

"""
def splitDataSet(dataSet,axis,value):
    #创建返回的数据集列表
    retDataSet=[]
    #遍历数据集
    for featVec in dataSet:
        if featVec[axis]==value:
            #去掉axis特征
            reduceFeatVec=featVec[:axis]
            #将符合条件的添加到返回的数据集
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)
    #返回划分后的数据集
    return retDataSet

"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:信息增益最大特征的索引值
Modify:
    2018-03-13

"""


def chooseBestFeatureToSplit(dataSet):
    #特征数量
    numFeatures = len(dataSet[0]) - 1
    #计数数据集的香农熵
    baseEntropy = calcShannonEnt(dataSet)
    #信息增益
    bestInfoGain = 0.0
    #最优特征的索引值
    bestFeature = -1
    #遍历所有特征
    for i in range(numFeatures):
        # 获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        #创建set集合{},元素不可重复
        uniqueVals = set(featList)
        #经验条件熵
        newEntropy = 0.0
        #计算信息增益
        for value in uniqueVals:
            #subDataSet划分后的子集
            subDataSet = splitDataSet(dataSet, i, value)
            #计算子集的概率
            prob = len(subDataSet) / float(len(dataSet))
            #根据公式计算经验条件熵
            newEntropy += prob * calcShannonEnt((subDataSet))
        #信息增益
        infoGain = baseEntropy - newEntropy
        #打印每个特征的信息增益
        print("第%d个特征的增益为%.3f" % (i, infoGain))
        #计算信息增益
        if (infoGain > bestInfoGain):
            #更新信息增益,找到最大的信息增益
            bestInfoGain = infoGain
            #记录信息增益最大的特征的索引值
            bestFeature = i
            #返回信息增益最大特征的索引值
    return bestFeature

"""
函数说明:统计classList中出现次数最多的元素(类标签)
Parameters:
    classList:类标签列表
Returns:
    sortedClassCount[0][0]:出现次数最多的元素(类标签)
Modify:
    2018-03-13

"""
def majorityCnt(classList):
    classCount={}
    #统计classList中每个元素出现的次数
    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]

"""
函数说明:创建决策树

Parameters:
    dataSet:训练数据集
    labels:分类属性标签
    featLabels:存储选择的最优特征标签
Returns:
    myTree:决策树
Modify:
    2018-03-13

"""
def createTree(dataSet,labels,featLabels):
    #取分类标签(是否放贷:yes or no)
    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]
    featLabels.append(bestFeatLabel)
    #根据最优特征的标签生成树
    myTree={bestFeatLabel:{}}
    #删除已经使用的特征标签
    del(labels[bestFeat])
    #得到训练集中所有最优特征的属性值
    featValues=[example[bestFeat] for example in dataSet]
    #去掉重复的属性值
    uniqueVls=set(featValues)
    #遍历特征,创建决策树
    for value in uniqueVls:
        myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),
                                               labels,featLabels)
    return myTree



"""
使用决策树进行分类
Parameters:
    inputTree;已经生成的决策树
    featLabels:存储选择的最优特征标签
    testVec:测试数据列表,顺序对应最优特征标签
Returns:
    classLabel:分类结果
Modify:2018-03-13

"""
def classify(inputTree,featLabels,testVec):
    #获取决策树节点
    firstStr=next(iter(inputTree))
    #下一个字典
    secondDict=inputTree[firstStr]
    featIndex=featLabels.index(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

if __name__==\'__main__\':
    dataSet,labels=createDataSet()
    featLabels=[]
    myTree=createTree(dataSet,labels,featLabels)
    #测试数据
    testVec=[0,1]
    result=classify(myTree,featLabels,testVec)

    if result==\'yes\':
        print(\'放贷\')
    if result==\'no\':
        print(\'不放贷\')
View Code

实验结果:
放贷

2)C4.5算法

与ID3算法相似,但是做了改进,将信息增益比作为选择特征的标准。 

算法如下:

输入:训练数据集D,特征集A,阈值$\varepsilon$;

输出:决策树T

1) 若D中所有实例属于同一类${C_k}$,则T为单结点树,并将类${C_k}$作为该结点的类标记,返回T;

2)若$A = \emptyset $,则T为单结点树,并将D中实例数最大的类${C_k}$作为该结点的类标记,返回T;

3)否则,计算A中各特征对D的信息增益比,选择信息增益比最大的特征$A_g$;

4)如果$A_g$的信息增益比小于阈值$\varepsilon$,则置T为单结点树,并将D中实例数最大的类${C_k}$作为该结点的类标记,返回T;

5)否则,对$A_g$的每一可能值${a_i}$,依${A_g} = {a_i}$将D分割为若干非空子集$D_i$,将$D_i$中实例数最大的类作为标记,构建子结点,由结点及其子结点构成数T,返回T;

6)对第$i$个子结点,以$D_i$为训练集,以$A - \left\{ {{A_g}} \right\}$为特征集,递归地调用步骤1)- 5),得到子树${T_i}$,返回${T_i}$。

3)CART算法

CART算法选用基尼指数选择最优特征,同时决定该特征的最优二值切分点。

算法如下:

输入:训练数据集D,停止计算的条件; 

输出:CART决策树。 

根据训练数据集,从根结点开始,递归地对每个结点进行以下操作,构建二叉决策树: 

1)设结点的训练数据集为D,计算现有特征对该数据集的基尼指数。此时,对每一个特征A,对其可能取的每个值a,根据样本点对A=a的测试为“是”或 “否”将D分割成D1和D2两部分,计算A=a时的基尼指数。 

2)在所有可能的特征A以及它们所有可能的切分点a中,选择基尼指数最小的特征及其对应的切分点作为最优特征与最优切分点。依最优特征与最优切分点,从现结点生成两个子结点,将训练数据集依特征分配到两个子结点中去。 

3)对两个子结点递归地调用步骤1)和2),直至满足停止条件。 

4)生成CART决策树。 

算法停止计算的条件是结点中的样本个数小于预定阈值,或样本集的Gini系数小于预定阈值(样本基本属于同一类),或者没有更多特征。

4)ID3、C4.5、CART算法的区别

  • 简单来说,ID3 使用信息增益作为选择特征的准则;C4.5 使用信息增益比作为选择特征的准则;CART 使用基尼指数作为选择特征的准则。
  • ID3 仅仅适用于二分类问题。ID3 仅仅能够处理离散属性。
  • C4.5 克服了ID3仅仅能够处理离散属性的问题,以及信息增益偏向选择取值较多特征的问题。
  • CART与ID3,C4.5不同之处在于CART生成的树必须是二叉树。既可以用于分类问题,也可以用于回归问题。

5、决策树的剪枝

决策树生成算法递归的产生决策树,直到不能继续下去为止,这样产生的树往往对训练数据的分类很准确,但对未知测试数据的分类缺没有那么精确,即会出现过拟合现象。过拟合产生的原因在于在学习时过多的考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树,解决方法是考虑决策树的复杂度,对已经生成的树进行简化。

剪枝(pruning):从已经生成的树上裁掉一些子树或叶节点,并将其根节点或父节点作为新的叶子节点,从而简化分类树模型。

实现方式:通过极小化决策树整体的损失函数或代价函数来实现。

设树T的叶结点个数为$\left| T \right|$,t是树T的叶结点,该叶结点有${N_t}$个样本点,其中k类的样本点有${N_{tk}}$个,$k = 1{\rm{,}}2{\rm{,}} \cdots {\rm{,}}K$,${H_t}\left( T \right)$为叶结点t上的经验熵,$\alpha  \ge 0$为参数,则决策树学习的损失函数可以定义为

${C_\alpha }\left( T \right) = \sum\limits_{t = 1}^{\left| T \right|} {{N_t}{H_t}\left( T \right) + } \alpha \left| T \right|$

其中经验熵为

${H_t}\left( T \right) =  - \sum\limits_{k = 1}^K {\frac{{{N_{tk}}}}{{{N_t}}}} {\rm{log}}\frac{{{N_{tk}}}}{{{N_t}}}$

在损失函数中,将其右端的第一项记作

$C\left( T \right) = \sum\limits_{t = 1}^{\left| T \right|} {{N_t}{H_t}\left( T \right)}  =  - \sum\limits_{t = 1}^{\left| T \right|} {\sum\limits_{k = 1}^K {{N_{tk}}} {\rm{log}}\frac{{{N_{tk}}}}{{{N_t}}}} $

这时有

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

其中,$C\left( T \right)$表示模型对训练数据的预测误差,即模型与训练数据的拟合程度。$\left| T \right|$表示模型复杂度,参数$\alpha  \ge 0$控制两者之间的影响,较大的$\alpha $促使选择较简单的模型(树),较小的$\alpha $促使选择较复杂的模型(树),$\alpha  = 0$意味着只考虑模型与训练数据的拟合程度,不考虑模型的复杂度。

剪枝就是当$\alpha $确定时,选择损失函数最小的模型,即损失函数最小的子树。

  • 当$\alpha $值确定时,子树越大,往往与训练数据的拟合越好,但是模型的复杂度越高;
  • 相反,子树越小,模型的复杂度就越低,但是往往与训练数据的拟合不好;
  • 损失函数正好表示了对两者的平衡。

可以看出,决策树的生成只是考虑通过提高信息增益对训练数据进行更好的拟合,而决策树剪枝通过优化损失函数还考虑了减小模型复杂度。

公式${C_\alpha }\left( T \right) = C\left( T \right) + \alpha \left| T \right|$定义的损失函数的极小化等价于正则化的极大似然估计,剪枝过程示意图:

树的剪枝算法:

输入:生成算法产生的整个树T,参数$\alpha $;

输出:修剪后的子树${T_\alpha }$。

1)计算每个结点的经验熵;

2)递归地从树的叶结点向上回缩;

设一组叶结点回缩到其父结点之前与之后的整体树分别为${T_B}$与${T_A}$,其对应的损失函数值分别为${C_\alpha }\left( {{T_B}} \right)$和${C_\alpha }\left( {{T_A}} \right)$,如果

${C_\alpha }\left( {{T_A}} \right) \le {C_\alpha }\left( {{T_B}} \right)$

则进行剪枝,即将父结点变为新的叶结点。

3)返回2),直至不能继续为止,得到损失函数最小的子树${T_\alpha }$。

CART树的剪枝算法:

输入:CART算法生成的决策树${T_0}$;

输出:最优决策树${T_\alpha }$。

1)设$k = 0$,$T = {T_0}$。

2)设$\alpha  =  + \infty $。

3)自下而上地对各内部结点$t$计算$C\left( {{T_t}} \right)$,$\left| {{T_t}} \right|$以及

$g\left( t \right) = \frac{{C\left( t \right) - C\left( {{T_t}} \right)}}{{\left| {{T_t}} \right| - 1}}$

$\alpha  = {\rm{min}}\left( {\alpha {\rm{,}}g\left( t \right)} \right)$

这里,$T_t$表示以$t$为根结点的子树,${C\left( {{T_t}} \right)}$是对训练数据的预测误差,$\left| {{T_t}} \right|$是$T_t$的叶结点个数。

4)对$g\left( t \right) = \alpha $的内部结点$t$进行剪枝,并对叶结点$t$以多数表决法决定其类,得到树T。

5)设$k = k+1$,${\alpha _k} = \alpha $,$T_k = T$。

6)如果${T_k}$不是由根结点及两个叶结点构成的树,则回到步骤3);否则令${T_k} = {T_n}$。

7)采用交叉验证法在子树序列${T_0}{\rm{,}}{T_1}{\rm{,}} \cdots {\rm{,}}{T_n}$中选取最优子树${T_\alpha }$。

7、决策树可视化

基于Python中的Matplotlib。可视化需要用到的函数:

getNumLeafs:获取决策树叶子结点的数目

getTreeDepth:获取决策树的层数

plotNode:绘制结点

plotMidText:标注有向边属性值

plotTree:绘制决策树

createPlot:创建绘制面板

实例1:利用代码绘制决策树

# -*- coding: utf-8 -*-
from math import log
import operator
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:经验熵
Modify:
    2018-03-12

"""
def calcShannonEnt(dataSet):
    #返回数据集行数
    numEntries=len(dataSet)
    #保存每个标签(label)出现次数的字典
    labelCounts={}
    #对每组特征向量进行统计
    for featVec in dataSet:
        currentLabel=featVec[-1]                     #提取标签信息
        if currentLabel not in labelCounts.keys():   #如果标签没有放入统计次数的字典,添加进去
            labelCounts[currentLabel]=0
        labelCounts[currentLabel]+=1                 #label计数

    shannonEnt=0.0                                   #经验熵
    #计算经验熵
    for key in labelCounts:
        prob=float(labelCounts[key])/numEntries      #选择该标签的概率
        shannonEnt-=prob*log(prob,2)                 #利用公式计算
    return shannonEnt                                #返回经验熵

"""
函数说明:创建测试数据集
Parameters:无
Returns:
    dataSet:数据集
    labels:分类属性
Modify:
    2018-03-13

"""
def createDataSet():
    # 数据集
    dataSet=[[0, 0, 0, 0, \'no\'],
            [0, 0, 0, 1, \'no\'],
            [0, 1, 0, 1, \'yes\'],
            [0, 1, 1, 0, \'yes\'],
            [0, 0, 0, 0, \'no\'],
            [1, 0, 0, 0, \'no\'],
            [1, 0, 0, 1, \'no\'],
            [1, 1, 1, 1, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [1, 0, 1, 2, \'yes\'],
            [2, 0, 1, 2, \'yes\'],
            [2, 0, 1, 1, \'yes\'],
            [2, 1, 0, 1, \'yes\'],
            [2, 1, 0, 2, \'yes\'],
            [2, 0, 0, 0, \'no\']]
    #分类属性
    labels=[\'年龄\',\'有工作\',\'有自己的房子\',\'信贷情况\']
    #返回数据集和分类属性
    return dataSet,labels

"""
函数说明:按照给定特征划分数据集

Parameters:
    dataSet:待划分的数据集
    axis:划分数据集的特征
    value:需要返回的特征值
Returns:
    无
Modify:
    2018-03-13

"""
def splitDataSet(dataSet,axis,value):
    #创建返回的数据集列表
    retDataSet=[]
    #遍历数据集
    for featVec in dataSet:
        if featVec[axis]==value:
            #去掉axis特征
            reduceFeatVec=featVec[:axis]
            #将符合条件的添加到返回的数据集
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)
    #返回划分后的数据集
    return retDataSet

"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
    dataSet:数据集
Returns:
    shannonEnt:信息增益最大特征的索引值
Modify:
    2018-03-13

"""


def chooseBestFeatureToSplit(dataSet):
    #特征数量
    numFeatures = len(dataSet[0]) - 1
    #计数数据集的香农熵
    baseEntropy = calcShannonEnt(dataSet)
    #信息增益
    bestInfoGain = 0.0
    #最优特征的索引值
    bestFeature = -1
    #遍历所有特征
    for i in range(numFeatures):
        # 获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        #创建set集合{},元素不可重复
        uniqueVals = set(featList)
        #经验条件熵
        newEntropy = 0.0
        #计算信息增益
        for value in uniqueVals:
            #subDataSet划分后的子集
            subDataSet = splitDataSet(dataSet, i, value)
            #计算子集的概率
            prob = len(subDataSet) / float(len(dataSet))
            #根据公式计算经验条件熵
            newEntropy += prob * calcShannonEnt((subDataSet))
        #信息增益
        infoGain = baseEntropy - newEntropy
        #打印每个特征的信息增益
        print("第%d个特征的增益为%.3f" % (i, infoGain))
        #计算信息增益
        if (infoGain > bestInfoGain):
            #更新信息增益,找到最大的信息增益
            bestInfoGain = infoGain
            #记录信息增益最大的特征的索引值
            bestFeature = i
            #返回信息增益最大特征的索引值
    return bestFeature

"""
函数说明:统计classList中出现次数最多的元素(类标签)
Parameters:
    classList:类标签列表
Returns:
    sortedClassCount[0][0]:出现次数最多的元素(类标签)
Modify:
    2018-03-13

"""
def majorityCnt(classList):
    classCount={}
    #统计classList中每个元素出现的次数
    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]

"""
函数说明:创建决策树

Parameters:
    dataSet:训练数据集
    labels:分类属性标签
    featLabels:存储选择的最优特征标签
Returns:
    myTree:决策树
Modify:
    2018-03-13

"""
def createTree(dataSet,labels,featLabels):
    #取分类标签(是否放贷:yes or no)
    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]
    featLabels.append(bestFeatLabel)
    #根据最优特征的标签生成树
    myTree={bestFeatLabel:{}}
    #删除已经使用的特征标签
    del(labels[bestFeat])
    #得到训练集中所有最优特征的属性值
    featValues=[example[bestFeat] for example in dataSet]
    #去掉重复的属性值
    uniqueVls=set(featValues)
    #遍历特征,创建决策树
    for value in uniqueVls:
        myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),
                                               labels,featLabels)
    return myTree

"""
函数说明:获取决策树叶子节点的数目

Parameters:
    myTree:决策树
Returns:
    numLeafs:决策树的叶子节点的数目
Modify:
    2018-03-13

"""

def getNumLeafs(myTree):
    numLeafs=0
    firstStr=next(iter(myTree))
    secondDict=myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__==\'dict\':
            numLeafs+=getNumLeafs(secondDict[key])
        else: numLeafs+=1
    return numLeafs

"""
函数说明:获取决策树的层数

Parameters:
    myTree:决策树
Returns:
    maxDepth:决策树的层数

Modify:
    2018-03-13
"""
def getTreeDepth(myTree):
    maxDepth = 0                                                #初始化决策树深度
    firstStr = next(iter(myTree))                                #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0]
    secondDict = myTree[firstStr]                                #获取下一个字典
    for key in secondDict.keys():
        if type(secondDict[key]).__name__==\'dict\':                #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth            #更新层数
    return maxDepth

"""
函数说明:绘制结点

Parameters:
    nodeTxt - 结点名
    centerPt - 文本位置
    parentPt - 标注的箭头位置
    nodeType - 结点格式
Returns:
    无
Modify:
    2018-03-13
"""
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    arrow_args = dict(arrowstyle="<-")                                            #定义箭头格式
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)        #设置中文字体
    createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords=\'axes fraction\',    #绘制结点
        xytext=centerPt, textcoords=\'axes fraction\',
        va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font)

"""
函数说明:标注有向边属性值

Parameters:
    cntrPt、parentPt - 用于计算标注位置
    txtString - 标注的内容
Returns:
    无
Modify:
    2018-03-13
"""
def plotMidText(cntrPt, parentPt, txtString):
    xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]                                            #计算标注位置
    yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
    createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)

"""
函数说明:绘制决策树

Parameters:
    myTree - 决策树(字典)
    parentPt - 标注的内容
    nodeTxt - 结点名
Returns:
    无
Modify:
    2018-03-13
"""
def plotTree(myTree, parentPt, nodeTxt):
    decisionNode = dict(boxstyle="sawtooth", fc="0.8")                                        #设置结点格式
    leafNode = dict(boxstyle="round4", fc="0.8")                                            #设置叶结点格式
    numLeafs = getNumLeafs(myTree)                                                          #获取决策树叶结点数目,决定了树的宽度
    depth = getTreeDepth(myTree)                                                            #获取决策树层数
    firstStr = next(iter(myTree))                                                            #下个字典
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)    #中心位置
    plotMidText(cntrPt, parentPt, nodeTxt)                                                    #标注有向边属性值
    plotNode(firstStr, cntrPt, parentPt, decisionNode)                                        #绘制结点
    secondDict = myTree[firstStr]                                                            #下一个字典,也就是继续绘制子结点
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD                                        #y偏移
    for key in secondDict.keys():
        if type(secondDict[key]).__name__==\'dict\':                                            #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
            plotTree(secondDict[key],cntrPt,str(key))                                        #不是叶结点,递归调用继续绘制
        else:                                                                                #如果是叶结点,绘制叶结点,并标注有向边属性值
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD

"""
函数说明:创建绘制面板

Parameters:
    inTree - 决策树(字典)
Returns:
    无
Modify:
    2018-03-13
"""
def createPlot(inTree):
    fig = plt.figure(1, facecolor=\'white\')#创建fig
    fig.clf()#清空fig
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)#去掉x、y轴
    plotTree.totalW = float(getNumLeafs(inTree))#获取决策树叶结点数目
    plotTree.totalD = float(getTreeDepth(inTree))#获取决策树层数
    plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0#x偏移
    plotTree(inTree, (0.5,1.0), \'\')#绘制决策树
    plt.show()#显示绘制结果

if __name__ == \'__main__\':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    print(myTree)
    createPlot(myTree)

if __name__==\'__main__\':
    dataSet,labels=createDataSet()
    featLabels=[]
    myTree=createTree(dataSet,labels,featLabels)
    print(myTree)
View Code

实验结果:

第0个特征的增益为0.083
第1个特征的增益为0.324
第2个特征的增益为0.420
第3个特征的增益为0.363
第0个特征的增益为0.252
第1个特征的增益为0.918
第2个特征的增益为0.474
{\'有自己的房子\': {0: {\'有工作\': {0: \'no\', 1: \'yes\'}}, 1: \'yes\'}}
第0个特征的增益为0.083
第1个特征的增益为0.324
第2个特征的增益为0.420
第3个特征的增益为0.363
第0个特征的增益为0.252
第1个特征的增益为0.918
第2个特征的增益为0.474
{\'有自己的房子\': {0: {\'有工作\': {0: \'no\', 1: \'yes\'}}, 1: \'yes\'}}

绘图如下:

8、决策树的存储与导出

使用Python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。

实例1:决策树的存储

假设我们已经得到决策树{\'有自己的房子\' : {0: {\'有工作\': {0: \'no\', 1: \'yes\'}}, 1: \'yes\'}},使用pickle.dump存储决策树。

# -*- coding: utf-8 -*-

import pickle
"""
函数说明:存储决策树
Parameters:
    inputTree:已经生成的决策树
    filename:决策树的存储文件名
Returns:
    无
Modify:
    2018-03-13

"""
def storeTree(inputTree,filename):
    with open(filename,\'wb\') as fw:
        pickle.dump(inputTree,fw)

if __name__==\'__main__\':
    myTree={\'有自己的房子\':{0:{\'有工作\':{0:\'no\',1:\'yes\'}},1:\'yes\'}}
    storeTree(myTree,\'classifierStorage.txt\')
View Code

运行代码,在该Python文件的相同目录下,会生成一个名为classifierStorage.txt的txt文件,这个文件二进制存储着我们的决策树。

实例2:决策树的导出

使用pickle.load进行载入即可,编写代码如下:

# -*- coding: utf-8 -*-

import pickle

"""
函数说明:读取决策树

Parameters:
    filename:决策树的存储文件名
Returns:
    pickle.load(fr):决策树字典
Modify:
    2018-03-13
"""
def grabTree(filename):
    fr = open(filename, \'rb\')
    return pickle.load(fr)

if __name__ == \'__main__\':
    myTree = grabTree(\'classifierStorage.txt\')
    print(myTree)
View Code

参考文献 

[1] 李航. 统计学习方法[M]. 北京:清华大学出版社,2012.

[2] Peter. 机器学习实战[M]. 北京:人民邮电出版社,2013.

[3] 赵志勇. Python机器学习算法[M]. 北京:电子工业出版社,2017.

[4] 周志华. 机器学习[M]. 北京:清华大学出版社,2016.