Python《机器学习实战》读书笔记(三)——决策树

第三章 决策树

本章内容:

  • 决策树简介
  • 在数据集中度量一致性
  • 使用递归构造决策树
  • 使用Matplotlib绘制树形图

引言

在开始说决策树之前,先分享曾经我和舍友玩过的一个游戏:

游戏规则很简单:参与游戏的一方(提问方)在脑海里想某一个场景情节,然后提供游戏另一方(猜测方)一个简单的场景,猜测方需要根据提供的场景猜出提问方脑海里的情节过程。猜测过程允许猜测方对提问方提问,提问方对提的问题的答案只有“对”或“错”。猜测方通过向提问方问问题,不断推断分解,不断缩小猜测情节的范围,以最后得到提问方脑海里的场景情节。

当时舍友作为提问方给我一个简单的场景:五个人,在路上,突然下起大雨,其中四个人立刻跑去避雨,另一人没跑,但是跑的四人反而淋湿了,另一人没被淋湿。根据他给我的这个情景,我不断问他问题,他给我回答对或错。最后猜出结果是:四个人抬着棺材在路上,突然下雨,棺材里的人没有被被淋湿,抬棺材的被淋湿。

游戏内容是很简单的一个场景情节,但这个游戏中的某个过程和决策树的工作原理很相似:猜测方在猜测的过程向提问方提问题的过程,然后提问方给出相对应的答案。

如图所示的流程图就是一个决策树:
决策树流程图

该流程图中,正方形代表判断模块(decision block),椭圆形代表终止模块(terminating block),表示已得出结论,可以终止运行。从判断模块引出的左右箭头称作分支(branch),他可以达到另一判断模块或者终止模块。图中构造了一个假象的邮件分类系统,首先检测发送邮件域名地址。如果地址为myEmployer.com,则将其放在分类“无聊时需要阅读的邮件”中。如果邮件不是来自这个域名,则检查邮件内容里是否包含单词曲棍球,如果包含则将邮件归类到“需要及时处理的朋友邮件”,如果不包含则将邮件归类到“无需阅读的垃圾邮箱”。

相比于k-近邻算法无法给出数据的内在含义的缺点,决策树很多任务都是为了数据中所蕴含的知识信息,因此角次数可以使用不熟悉的数据集合,并从中提取出一系列规则,机器学习算法最终将使用这些机器从数据集中创造的规则。

通过引言,大致对决策树有了了解。下面正式开始学习。

3-1 决策树的构造

决策树
优点:计算浮渣都不高,输出结果易于理解,对中间值的缺失不敏感,可以处理相关特征数据。
缺点:可能会产成过度匹配问题
适用数据类型:数值型和标称型

在构造决策树时,需要解决的第一个问题是:当前数据集上哪个特征在划分数据分类时起决定性作用。

为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成测试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在每一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮箱已经正确地划分数据分类,无需进一步对数据集进行分割。如果数据自己内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。

创建分支的伪代码函数createBranch()如下所示

检测数据集中的每个子项是否属于同一分类:
      If so return 类标签;
      Else
         寻找划分数据集的最好特征
         划分数据集
         创建分支节点
               for 每个划分的子集
                   调用函数createBranch并增加返回结果到分支节点中
         return 分支节点

决策树的一般流程

1. 收集数据:可以使用任何方法。
2. 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
3. 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
4. 训练算法:构造树的数据结构。
5. 测试算法:使用经验树计算错误率。
6. 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。

一些决策树算法采用二分法划分数据,本书采用的是ID3算法划分数据集,该算法处理如何划分数据集,何时停止划分数据集。

不浮出水面是否可以生存是否有脚蹼属于鱼类
1
2
3
4
5

如上表的数据包含5个海洋动物,特征包括:不浮出水面是否可以生存和是否有脚蹼。我们可以将这些动物分为两类:鱼类和非鱼类。现在我们想要决定依据第一个特征还是第二个特征划分数据。解决这个问题之前,必须采用量化的方法判断如何划分数据,下面将讨论这个问题。

3-1-1 信息增益

划分数据集的大原则:将无序的数据变得更加有序。

在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择

在可以评测那种数据划分方式是最好的数据划分之前,必须学习如何计算信息增益。集合信息的度量方式成为香农熵或者简称熵。

定义为信息的期望值,在明晰这个概念之前,我们必须知道信息的定义。如果待分类的事务可能划分在多个分类中,则符号符号的信息定义为:
信息定义
其中概率是选择该分类的概率。

为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到:
所有信息期望值
其中n是分类的数目。

使用Python计算信息熵,创建名为trees.py的文件,并在该文件里敲入下列代码,实现计算给定数据集的熵。

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    #返回数据集的行数
    labelCounts = {}
    #保存每个标签(Label)出现次数的字典    
    for featVec in dataSet:
    #对每组特征向量进行统计        
        currentLabel = featVec[-1]
        #提取标签(Label)信息
        if currentLabel not in labelCounts.keys():
        #如果标签(Label)没有放入统计次数的字典,添加进去        
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
        #Label计数
    shannonEnt = 0.0
    #经验熵(香农熵)
    for key in labelCounts:
    #计算香农熵    
        prob = float(labelCounts[key])/numEntries
        #选择该标签(Label)的概率
        shannonEnt -= prob * log(prob,2)
        #利用公式计算        
    return shannonEnt
    #返回经验熵(香农熵)

同时,在trees.py文件中输入createDataSet()得到前面表中所示的简单鱼鉴定数据集。代码如下:

def createDataSet():
    dataSet = [[1,1,'yes'],
              [1,1,'yes'],
              [1,0,'no'],
              [0,1,'no'],
              [0,1,'no']]
    #数据集
    labels = ['no surfacing','flippers']
    #分类属性
    return dataSet,labels
    #返回数据集和分类属性

在Python命令提示符下输入下列命令:

>>> reload(trees)
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> trees.calcShannonBnt(myDat)
>>> trees.calcShannonEnt(myDat)
0.9709505944546686

熵越高,则混合的数据也越多,例如在数据集中添加更多的分类,观察熵的变化情况。这里增加第三个名为maybe的分类,测试上的变化:

>>> myDat[0][-1] = 'maybe'
>>> myDat
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(myDat)
1.3709505944546687

得到熵之后,我们可以按周获取最大信息增量的方法划分数据集。

3-1-2 划分数据集

学习了如何度量数据集的无序程度,分类算除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。

在trees.py文件中输入下列代码:

"""
    Function:
        按给定特征划分数据集
    Parameters:
        dataSet——待划分的数据集
        axis——划分数据集的特征
        value——特征的返回值
    Return:
        retDataSet——划分好后的数据集列表
    Modify:
        2017-11-29    
"""
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    #创建返回数据列表对象
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            #去掉axis特征     
            reducedFeatVec.extend(featVec[axis+1:])
            #将符合条件的添加到返回的数据里表中
            retDataSet.append(reducedFeatVec)
    return retDataSet
    #返回划分后的数据集

代码中使用了Pyton语言列表类型自带的extend()和append()方法。两个方法功能类似,但是在处理多个列表时,两个方法的处理结果是完全不同的。

假设存在两个列表a和b:

>>> a = [1,2,3]
>>> b = [4,5,6]
>>> a.append(b)
>>> a
[1, 2, 3, [4, 5, 6]]
>>> 

使用extend:

>>> a = [1,2,3]
>>> a.extend(b)
>>> a
[1, 2, 3, 4, 5, 6]
>>> 

我们可以在前面的简单样本数据上测试函数splitDataSe():

>>> reload(trees)
<module 'trees' from 'E:\Python_Files\CodeofMe\Chapter3\trees.py'>
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.splitDataSet(myDat,0,1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> trees.splitDataSet(myDat,0,0)
[[1, 'no'], [1, 'no']]

接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。

其函数如下:

"""
    Function:
        选择最优特征
    Parameters:
        dataSet——数据集
    Return:
        bestFeature——信息增益最大的(最优)特征的索引值
    Modify:
        2017-12-14    
"""
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      
    #特征数量
    baseEntropy = calcShannonEnt(dataSet)
    #计算数据集的香农熵
    bestInfoGain = 0.0;
    #信息增益 
    bestFeature = -1
    #最优特征的索引值    
    for i in range(numFeatures):        
    #iterate over all the features
        featList = [example[i] for example in dataSet]
        #获取dataSet的第i个所有特征 
        #create a list of all the examples of this feature
        uniqueVals = set(featList)       
        #创建set集合{},元素不可重复
        #get a set of unique values
        newEntropy = 0.0
        #经验条件熵
        for value in uniqueVals:
        #计算信息增益
            subDataSet = splitDataSet(dataSet, i, value)
            #subDataSet划分后的子集
            prob = len(subDataSet)/float(len(dataSet))
            #计算子集的概率
            newEntropy += prob * calcShannonEnt(subDataSet)
            #根据公式计算经验条件熵     
        infoGain = baseEntropy - newEntropy 
        #信息增益            
        #calculate the info gain; ie reduction in entropy
        if (infoGain > bestInfoGain):       
        #compare this to the best gain so far
        #计算信息增益        
            bestInfoGain = infoGain
            #更新信息增益,找到最大的信息增益         
            #if better than current best, set to best
            bestFeature = i
            #记录信息增益最大的特征的索引值            
    return bestFeature   
    #返回信息增益最大的特征的索引值

在Python命令提示符下输入下列命令:

>>> reload(trees)
<module 'trees' from 'E:\Python_Files\CodeofMe\Chapter3\trees.pyc'>
>>> myDat,labels = trees.createDataSet()
>>> trees.chooseBestFeatureToSplit(myDat)
0
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> 

代码运行结果显示,第0个特征为最好用于划分数据集的特征。为了检验这个结果的正确性和实际意义,我们对应前面列出的数据表和myDat中的数据。

如果我们按照第一个特征属性划分数据,也就是说第一个特征是1的放在一个组,第一个特征是0的放在一个组,按照这个方法划分数据集:第一个特征为1的海洋生物分组将有两哥属于鱼类,一个属于非鱼类;另一个分组则全部属于非鱼类。如果按照第二个特征分组:第一个海洋动物分组将有两个属于鱼类,两个属于非鱼类;另一个分组则只有一个非鱼类。不难看出,按照第一个特征分组的正确率高。

3-1-3 递归构建决策树

从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特增值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以在此划分数据。因此,可以采用递归的原则处理数据集。

递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如下图所示:
节点分类图

如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。

如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类。

在trees.py文件顶部增加一行代码

import operator

然后增加下面的代码到trees.py文件中:

"""
    Function:
        统计classList中出现此处最多的元素(类标签)
    Parameters:
        classList——类标签列表
    Return:
        sortedClassCount[0][0]——出现此处最多的元素(类标签)
    Modify:
        2017-12-15    
"""

def majorityCnt(classList):
    classCount={}
    for vote in classList:
    #统计classList中每个元素出现的次数
        if vote not in classCount.keys(): 
            classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]
    #返回classList中出现次数最多的元素

在trees.py里添加下列程序代码:

"""
    Function:
        创建决策树
    Parameters:
        dataSet——训练数据集
        labels——分类属性标签
    Return:
        myTree——决策树
    Modify:
        2017-12-15    
"""
def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]
    #取分类标签
    if classList.count(classList[0]) == len(classList): 
    #如果类别完全相同则停止继续划分    
        return classList[0]#stop splitting when all of the classes are equal
    if len(dataSet[0]) == 1: 
    #遍历完所有特征时返回出现次数最多的类标签
    #stop splitting when there are no more features in dataSet
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)
    #选择最优特征    
    bestFeatLabel = labels[bestFeat]
    #最优特征的标签    
    myTree = {bestFeatLabel:{}}
    #根据最优特征的标签生成树    
    del(labels[bestFeat])
    #删除已经使用特征标签
    featValues = [example[bestFeat] for example in dataSet]
    #得到训练集中所有最优特征的属性值    
    uniqueVals = set(featValues)
    #去掉重复的属性值
    for value in uniqueVals:
    #遍历特征,创建决策树。
        subLabels = labels[:]       
        #copy all of labels, so trees don't mess up existing labels
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree 

myTree存储了树的所有信息,这对于其后绘制树形图非常重要。

最后代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree(),得到的返回值将被插入到字典变量myTree中,因此函数终止执行时,字典中将会嵌套很多代表叶子节点信息的字典数据。在解释这个嵌套数据之前,我们先看一下循环的第一行:

subLabels = labels[:]   

这行代码复制了类标签,并将其存储在新列表变量subLables中。之所以这样做,是因为在Python语言中函数参数是列表类型时,参数是按照应用方式传递的。为了保证每次调用函数createTree()时不改变原始列表的内容,使用新变量subLabels代替原始列表。

在Python命令提示符下输入下列代码:

>>> reload(trees)
<module 'trees' from 'E:\Python_Files\CodeofMe\Chapter3\trees.pyc'>
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> myTree = trees.createTree(myDat,labels)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> 

变量myTree包含了很多代表树结构信息的嵌套字典,从左边第一个’no surfacing’是第一个划分数据集的特征名称,该关键字的值也是另一个数据字典。第二个关键字是’no surfacing’特征划分的数据集,这些关键字的值是’no surfacing’节点的子节点。这些值可能是类标签,也可能是另一个数据字典。如果是类标签,则该节点是叶子节点;如果值是另一个数据字典,则子节点是一个判断节点,这种格式结构不断重复就构成了整棵树。

3-2 在Python中使用Matplotlib注解绘制树形图

从数据集中创建树,并通过字典的形式表示出来,这个形式不易于理解,直接绘制图形也比较困难,所以使用Matplotlib库创建树形图。
如图所示是决策树的范例:
决策树的范例

3-2-1 Matplotlib注解

创建treePlotter.py文件,输入下列程序代码:

import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

"""
    Function:
        绘制节点
    Parameters:
        nodeTxt——节点名
        centerPt——文本位置
        parentPt——标注的箭头位置
        nodeType——节点格式
    Return:
        None
    Modify:
        2017-12-16    
"""
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    decisionNode = dict(boxstyle="sawtooth", fc="5.0")
    leafNode = dict(boxstyle="round4", fc="5.0")
    arrow_args = dict(arrowstyle="<-")
    #定义文本框箭头格式     
    font = FontProperties(fname=r"C:\\WINDOWS\\Fonts\\simsun.ttc",size=15) 
    #指定window自带的字体 \\表示转义字符,此处为表示‘\’此处字符串前的r表示此处字符串不转义    
    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 )
    #绘制节点

"""
    Function:
        绘制节点
    Parameters:
        nodeTxt——节点名
        centerPt——文本位置
        parentPt——标注的箭头位置
        nodeType——节点格式
    Return:
        None
    Modify:
        2017-12-16    
"""
def createPlot():
    fig = plt.figure(1, facecolor='white')
    #创建fig
    fig.clf()
    #清空fig
    createPlot.ax1 = plt.subplot(111, frameon=False) 
    #ticks for demo puropses 
    plotNode(u'决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode)
    plotNode(u'叶节点', (0.8, 0.1), (0.3, 0.8), leafNode)
    plt.show()
    #显示绘制结果

在Python命令提示符下测试:

>>> import treePlotter
>>> treePlotter.createPlot()

测试结果如下:
函数plotNode的例子

3-2-2 构造注解树

在绘制一棵完整的树之前,我们需要定义两个新函数getNumLeafs()和getTreeDepth()来获取叶节点的数目和输的层数,以便可以正确确定x轴的长度和y轴的高度。

"""
    Function:
        获取决策树叶子结点的数目
    Parameters:
        myTree——决策树
    Return:
        numLeafs——决策树叶子结点数目
    Modify:
        2017-12-16    
"""
def getNumLeafs(myTree):
    numLeafs = 0
    #初始化叶子
    firstStr = myTree.keys()[0]
    #获取结点属性
    secondDict = myTree[firstStr]
    #获取下一组字典    
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':
        #test to see if the nodes are dictonaires, if not they are leaf nodes
        #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点        
            numLeafs += getNumLeafs(secondDict[key])
        else:   numLeafs +=1
    return numLeafs

"""
    Function:
        获取决策树的层数
    Parameters:
        myTree——决策树
    Return:
        maxDepth——决策树的层数
    Modify:
        2017-12-16    
"""
def getTreeDepth(myTree):
    maxDepth = 0
    #初始化决策树深度
    firstStr = myTree.keys()[0]
    #获取结点属性
    secondDict = myTree[firstStr]
    #获取下一组字典
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':
        #test to see if the nodes are dictonaires, if not they are leaf nodes
        #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
        #更新层数
    return maxDepth

为了节省时间,函数retrieveTree输出预先存储的数信息,避免了每次测试代码时都要从数据中创建树的麻烦。在
treePlotter.py中添加下列程序代码:

"""
    Function:
        输出存储的树信息
    Parameters:
        i——存储的第i个树结构
    Return:
        listOfTrees[i]——预定义树结构
    Modify:
        2017-12-16    
"""

def retrieveTree(i):
    listOfTrees =[{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
                  {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
                  ]
    return listOfTrees[i]

在Python命令提示符下输入下列命令:

>>> reload(treePlotter)
<module 'treePlotter' from 'E:\Python_Files\CodeofMe\Chapter3\treePlotter.py'>
>>> treePlotter.retrieveTree(1)
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
>>> treePlotter.retrieveTree(0)
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> myTree = treePlotter.retrieveTree(0)
>>> treePlotter.getNumLeafs(myTree)
3
>>> treePlotter.getTreeDepth(myTree)
2

这儿的retrieveTree()主要用来测试,返回预定义的树结构。

现在绘制一棵完整的树,程序代码如下:

"""
    Function:
        标注有向边属性值
    Parameters:
        cntrPt、parentPt——用于计算标注位置
        txtString——标注的内容
    Return:
        NONE 
    Modify:
        2017-12-16    
"""
def plotMidText(cntrPt, parentPt, txtString):
    font = FontProperties(fname=r"C:\\WINDOWS\\Fonts\\simsun.ttc",size=25) 
    #指定window自带的字体 \\表示转义字符,此处为表示‘\’此处字符串前的r表示此处字符串不转义  
    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,FontProperties = font)

"""
    Function:
        绘制决策树
    Parameters:
        myTree——决策树
        parentPt——标注的内容
        nodeTxt——结点名
    Return:
        NONE 
    Modify:
        2017-12-16    
"""
def plotTree(myTree, parentPt, nodeTxt):
#if the first key tells you what feat was split on
    decisionNode = dict(boxstyle="sawtooth", fc="5.0")
    #设置结点格式
    leafNode = dict(boxstyle="round4", fc="5.0")  
    #设置叶结点格式  
    numLeafs = getNumLeafs(myTree) 
    #获取决策树叶结点数目,决定了树的宽度 
    #this determines the x width of this tree
    depth = getTreeDepth(myTree)
    #获取决策树层数    
    firstStr = myTree.keys()[0]     
    #the text label for this node should be this
    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':
        #test to see if the nodes are dictonaires, if not they are leaf nodes 
        #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点          
            plotTree(secondDict[key],cntrPt,str(key))        
            #recursion
            #不是叶结点,递归调用继续绘制            
        else:   
        #it's a leaf node print the leaf node
        #如果是叶结点,绘制叶结点,并标注有向边属性值 
            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
#if you do get a dictonary you know it's a tree, and the first element will be another dict

"""
    Function:
        创建绘制面板
    Parameters:
        inTree——决策树(字典)
    Return:
        None
    Modify:
        2017-12-16    
"""

def createPlotTree(inTree):
    fig = plt.figure(1, facecolor='white')
    #创建fig
    fig.clf()
    #清空fig
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    
    #no ticks
    #去掉x、y轴    
    #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 
    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()
    #显示绘制结果

在Pythonm命令提示符下输入下列命令:

>>> reload(treePlotter)
<module 'treePlotter' from 'E:\Python_Files\CodeofMe\Chapter3\treePlotter.pyc'>
>>> myTree = treePlotter.retrieveTree(0)
>>> treePlotter.createPlotTree(myTree)

输出结果如图所示:
简单数据集绘制的树形图

按照如下变更字典,重新绘制树形图:

>>> myTree['no surfacing'][3] = 'maybe'
>>> treePlotter.createPlotTree(myTree)

如图所示,得到输出结果:
超过两个分支的树形图

3-3 测试和存储分类器

3-3-1 测试算法:使用决策树执行分类

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节;最后将测试数据定义为叶子节点所属的类型。

在tree.py中添加下列程序:

"""
    Function:
        决策树分类
    Parameters:
        inputTree——已经生成的决策树
        featLabels——存储选择的最优特征标签
        testVec——存储选择的最优特征标签
    Return:
        classLabel——分类结果
    Modify:
        2017-12-18    
"""
def classify(inputTree,featLabels,testVec):
    firstStr = inputTree.keys()[0]
    #获取已生成的决策树第一个键
    secondDict = inputTree[firstStr]    
    #获取决策树中第一个健值(字典类型)
    featIndex = featLabels.index(firstStr)
    #将标签字符串转换为索引
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

并在Python命令提示符下,输入下列命令:

>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter3')
>>> import trees
>>> import treePlotter
>>> myDat,labels = trees.createDataSet()
>>> labels
['no surfacing', 'flippers']
>>> myTree = treePlotter.retrieveTree(0)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> trees.classify(myTree,labels,[1,0])
'no'
>>> trees.classify(myTree,labels,[1,1])
'yes'
>>> 

输出结果和实际数据结果完全相同。

3-3-2 使用算法:决策树的存储

构造决策树是很耗时的任务即使处理很小的数据集,如前面的样本数据也要花费几秒的时间,如果数据及很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。

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

"""
    Function:
        保存决策树
    Parameters:
        inputTree——已经生成的决策树
        filename——文件名
    Return:
        None
    Modify:
        2017-12-19    
"""
def storeTree(inputTree,filename):
    import pickle
    fw = open(filename,'w')
    pickle.dump(inputTree,fw)
    fw.close()

"""
    Function:
        读取已保存决策树
    Parameters:
        filename——文件名
    Return:
        None
    Modify:
        2017-12-19    
"""
def grabTree(filename):
    import pickle
    fr = open(filename)
    return pickle.load(fr)

在Python命令提示符中输入下列命令验证上述代码:

>>> reload(trees)
<module 'trees' from 'E:\Python_Files\CodeofMe\Chapter3\trees.py'>
>>> filename = 'E:\Python_Files\CodeofMe\Chapter3\classifierStorage.txt'
>>> trees.storeTree(myTree,filename)
>>> trees.grabTree(filename)
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> 

通过上面代码,可以将分类器存储在硬盘上,而不用每次对数据分类是时重新学习一边,这也是决策树的优点之一。

3-4 示例:使用决策树预测隐形眼镜类型

通过决策树实现预测患者需要佩戴的隐形眼镜类型。

示例:使用决策树预测隐形眼镜类型

1. 收集数据:提供的文本文件。
2. 准备数据:解析tab健分隔的数据行。
3. 分析数据:快速检查数据,确保正确地解析数据内容,使用createPlot()函数绘制最终的树形图。
4. 训练算法:使用creatTree()函数。
5. 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例。
6. 实用算法:存储树的数据结构,以便下次使用时无需重新构造树。

在Python命令提示符中输入下列命令加载数据:

>>> filename =  'E:\Python_Files\CodeofMe\Chapter3\lenses.txt'
>>> fr = open(filename)
>>> lenses = [inst.strip().split('\t') for inst in fr.readlines()]
>>> lensesLabels = ['age', 'prescript', 'astgmatic', 'tearRate']
>>> lensesTree = trees.createTree(lenses,lensesLabels)
>>> lensesTree
{'tearRate': {'reduced': 'no lenses', 'normal': {'astgmatic': {'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'}}}}}}
>>> treePlotter.createPlotTree(lensesTree)

采用文本方式很难分辨出决策树的模样,最后一行命令调用createPlotTree()函数绘制了如图所示的树形图:

由ID3算法产生的决策树

沿着决策树的不同分支,我们可以得到不同患者需要佩戴的隐形眼镜类型。从图中我们可以发现,医生最多需要问四个问题就能确定患者需要佩戴哪种类型的隐形眼镜。

本章使用的算法称为ID3。ID3算法无法直接处理数值型数据,尽管我们可以通过量化的方法将数值型数据转化为标称型数值,但是如果存在太多的特征划分,ID3算法仍然面令其他问题。后面第九章会介绍另一个决策树构造算法CART。

3-5 本章小结

决策树分类器就像带有终止快的流程图,终止块表示分类结果。

开始处理数据集时,首先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一分类。

ID3算法可以用划分标称型数据集。构造决策树时,我们通常采用递归的的方法将数据集转化为决策树。

参考文献

《机器学习实战》

  • 8
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值