• 决策树


    决策树简介

    很多人可能玩过或见过一个猜字游戏,游戏规则很简单,就是两个参与者,一个是提问者,一个是回答者,提问者可以不断的提问题,而回答者根据提问者的问题来回答是或不是,通过不断缩小猜测事务范围提问者最后确定了答案。

    图1-1构造了一个假设的三好生分类系统,黑框代表判断模块,蓝框代表终止模块,表示已得出结论,可以停止运行。分类系统中,它首先判断学生是否尊敬老师,如果判断不是则归类到不是三好生,如果判断是再继续判断是否友爱同学,如果不是则归类到不是三好生中,如果是再判断成绩是否优异,如果不是则归类到不是三好生中,如果是则归类到时三好生中。

    决策树的构造

    在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性特征,划分出最好的结果,需要评估每个特征。完成测试之后,原始数据集会被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上,如果某个分支下的数据属于同一类型,则当前无需对数据集进行分割,如果数据子集内的数据不属于同一类型,则需要重复划分数据子集。

    信息增益

    划分数据集的原则是:将无序的数据变得更加有序,阻止杂乱无章数据的一种方法就是使用信息论度量信息,我们可以在划分数据之前或之后使用信息论量化度量信息的内容。在划分数据集之前之后信息发生的彼岸花称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的增益信息,获得信息增益最高的特征就是最好的选择。集合信息的度量方式称为熵,熵的定义为信息的期望值。如果待分类的事务可能划分在多个分类中,则符号xi 的信息定义为:

    其中P(xi)为选择该分类的概率。

    为了计算熵需要计算所有类别所有可能值包含的期望值,通过下面的公式得到:

    其中n是分类的数目。

    代码1-2为创建数据集和根据数据集计算熵

    from numpy import *
    from math import log
    import operator
    
    
    # 生成数据集
    def createDataSet():
        dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
        lables = ['no surfacing', 'flippers']
        return dataSet, lables
    
    
    def calcShannonEnt(dataSet):
        numEntries = len(dataSet)
        lableCounts = {}
        for featVec in dataSet:
            curLable = featVec[-1]
            lableCounts[curLable] = lableCounts.get(curLable, 0) + 1
        shannonEnt = 0.0
        for key in lableCounts:
            prob = float(lableCounts[key]) / numEntries
            shannonEnt -= prob * log(prob, 2)
        return shannonEnt
    myData,lables = createDataSet()
    calcShannonEnt(myData)
    

      

    代码1-2创建数据集然后根据数据集计算熵的结果:

    >>> myData,lables = createDataSet()
    >>> myData
    [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
    >>> calcShannonEnt(myData)
    0.9709505944546686  

    熵越高,则混合的数据越多,我们可以在数据集中添加更多的分类,观察熵是如何变化的,这里我们增加第三个名为maybe的分类,测试熵的变化,如:

    >>> myData[0][-1] = 'maybe'
    >>> calcShannonEnt(myData)
    1.3709505944546687  

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

    划分数据集

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

    在划分数据集时,我们需要用到Python语言列表类型自带的extend()和append()方法。这两个方法功能类似,但处理多个列表时,结果不同。

    假定有a和b两个列表,如果执行a.append(b),则列表得到第四个元素,而且第四个元素也是一个列表。

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

    如果执行a.extend(b),则a列表包含b列表所有元素。

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

      

    代码1-3为按照给定特征划分数据集

    def splitDataSet(dataSet, axis, value):
        retDataSet = []
        for featVec in dataSet:
            if featVec[axis] != value: continue
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis + 1:])
            retDataSet.append(reducedFeatVec)
        return retDataSet
    

    通过代码1-3来划分数据集:

    >>> myData,lables = createDataSet()
    >>> myData
    [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
    >>> splitDataSet(myData, 0, 1)
    [[1, 'yes'], [1, 'yes'], [0, 'no']]
    >>> splitDataSet(myData, 0, 0)
    [[1, 'no'], [1, 'no']]
    

      

    代码1-4为选择最好的数据集划分方式

    def chooseBestFeatureToSplit(dataSet):
        numFeatures = len(dataSet[0]) - 1
        baseEntropy = calcShannonEnt(dataSet)
        bestInfoGain = 0.0
        bestFeature = -1
        for i in range(numFeatures):
            # 计算唯一的分类标签
            featList = [example[i] for example in dataSet]
            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): continue
            bestInfoGain = infoGain
            bestFeature = i
        return bestFeature
    

      

    代码1-4给出了函数chooseBestFeatureToSplit(),该函数实现了选取特征,划分数据集,计算得出最好的划分数据集特征,在划分数据之前,先计算整个数据集的原始熵,保证了最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。遍历当前特征中所有唯一的属性值,对每个特征划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。

    调用代码1-4得到的结果:

    >>> myData, lables = createDataSet()
    >>> chooseBestFeatureToSplit(myData)
    0
    

    代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。

    递归构建决策树

     构造决策树算法的原理:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大雨2两个分支的数据集划分。第一次划分之后,数据被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。

    递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或终止模块。任何达到叶子节点的数据必然属于叶子节点的分类。

    代码1-5为返回出现次数最好的分类名称

    def majorityCnt(classList):
        classCount = {}
        for vote in classList:
            classCount[vote] = classCount.get(vote, 0) + 1
        sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
        return sortedClassCount[0][0]
    

      

    代码1-6为创建决策树的代码

    def createTree(dataSet, lables):
        # 类别相同则停止继续划分
        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)
        bestFeatLable = lables[bestFeat]
        myTree = {bestFeatLable: {}}
        del (lables[bestFeat])
        # 得到列表包含的所有属性值
        featValues = [example[bestFeat] for example in dataSet]
        uniqueValues = set(featValues)
        for value in uniqueValues:
            subLables = lables[:]
            myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables)
        return myTree
    

    代码1-6的函数包含了两个输入参数:数据集和标签列表。标签列表包含了数据集中所有特征的标签。上述代码首先创建了一个名为classList的列表变量,用于存储数据集的所有类标签。递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类的类标签。递归函数的第二个停止条件是使用完使用的特征,仍然不能将数据集划分到仅包含唯一类别的分组,则将数据集进行投票,推选出票数最高的分类作为该类的类标签。

    下一步开始创建树,当前数据集选取最好的特征存储在变量bestFeat中,得到列表包含的所有属性值。最后,代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用createTree(),得到的返回值将插入到字典变量myTree中,因此函数终止执行时,字典中将会潜逃很多代表叶子节点信息的字典数据。

    调用代码1-6函数执行结果:

    >>> myData, lables = createDataSet()
    >>> myTree = createTree(myData, lables)
    >>> myTree
    {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
    

      

    代码1-7为获取叶子节点的树木和树的层数

    # 决策树数据
    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]
    
    
    # 获取叶子节点的数目
    def getNumLeafs(myTree):
        numLeafs = 0
        firstStr = myTree.keys()[0]
        secondDict = myTree[firstStr]
        for key in secondDict.keys():
            if type(secondDict[key]).__name__ == 'dict':
                numLeafs += getNumLeafs(secondDict[key])
            else:
                numLeafs += 1
        return numLeafs
    
    
    # 获取树的层数
    def getTreeDepth(myTree):
        maxDepth = 0
        firstStr = 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
    

        

    代码1-7中,两个函数都具有类似的结构。使用Python的type()函数可以判断子节点是否是字典类型,如果子节点是字典类型,则该节点需要递归调用getNumLeafs()函数,如果子节点是叶子节点,则对叶子节点数加1,并返回该值。getTreeDepth()同getNumLeafs()一样,会遍历所有的叶子节点,一旦到达叶子节点,则从递归调用中返回,并将树的层数加1,最后选取层数最大的值作为树的层数。

    调用代码1-7的计算树的叶子节点和层数:

    >>> myTree = retrieveTree(1)
    >>> myTree
    {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
    >>> getNumLeafs(myTree)
    4
    >>> getTreeDepth(myTree)
    3
    

      

     代码1-8使用决策树的分类函数

    def classify(inputTree, featLables, testVec):
        firstStr = inputTree.keys()[0]
        secondDict = inputTree[firstStr]
        # 将标签字符串转换为索引
        featIndex = featLables.index(firstStr)
        for key in secondDict.keys():
            if testVec[featIndex] != key: continue
            if type(secondDict[key]).__name__ == 'dict':
                classLable = classify(secondDict[key], featLables, testVec)
            else:
                classLable = secondDict[key]
        return classLable
    

      

    代码1-8定义的函数也是一个递归函数,使用index方法查找当前列表中第一个匹配firstStr变量的元素,然后代码递归遍历整棵树,比较testVec变量中的值与树节点的值,如果到达叶子节点,则返回当前节点的分类标签。

    调用代码1-8分类函数

    >>> myData, lables = createDataSet()
    >>> lables
    ['no surfacing', 'flippers']
    >>> myTree = retrieveTree(0)
    >>> myTree
    {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
    >>> classify(myTree, lables, [1, 0])
    'no'
    >>> classify(myTree, lables, [1, 1])
    'yes'
    

      

    代码1-9为本章代码整合

    # coding:utf-8
    
    from numpy import *
    from math import log
    import operator
    
    
    # 生成数据集
    def createDataSet():
        dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
        lables = ['no surfacing', 'flippers']
        return dataSet, lables
    
    
    def calcShannonEnt(dataSet):
        numEntries = len(dataSet)
        lableCounts = {}
        for featVec in dataSet:
            curLable = featVec[-1]
            lableCounts[curLable] = lableCounts.get(curLable, 0) + 1
        shannonEnt = 0.0
        for key in lableCounts:
            prob = float(lableCounts[key]) / numEntries
            shannonEnt -= prob * log(prob, 2)
        return shannonEnt
    
    
    def splitDataSet(dataSet, axis, value):
        retDataSet = []
        for featVec in dataSet:
            if featVec[axis] != value: continue
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis + 1:])
            retDataSet.append(reducedFeatVec)
        return retDataSet
    
    
    def chooseBestFeatureToSplit(dataSet):
        numFeatures = len(dataSet[0]) - 1
        baseEntropy = calcShannonEnt(dataSet)
        bestInfoGain = 0.0
        bestFeature = -1
        for i in range(numFeatures):
            # 计算唯一的分类标签
            featList = [example[i] for example in dataSet]
            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): continue
            bestInfoGain = infoGain
            bestFeature = i
        return bestFeature
    
    
    def majorityCnt(classList):
        classCount = {}
        for vote in classList:
            classCount[vote] = classCount.get(vote, 0) + 1
        sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
        return sortedClassCount[0][0]
    
    
    def createTree(dataSet, lables):
        # 类别相同则停止继续划分
        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)
        bestFeatLable = lables[bestFeat]
        myTree = {bestFeatLable: {}}
        del (lables[bestFeat])
        # 得到列表包含的所有属性值
        featValues = [example[bestFeat] for example in dataSet]
        uniqueValues = set(featValues)
        for value in uniqueValues:
            subLables = lables[:]
            myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables)
        return myTree
    
    
    # 决策树数据
    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]
    
    
    # 获取叶子节点的数目
    def getNumLeafs(myTree):
        numLeafs = 0
        firstStr = myTree.keys()[0]
        secondDict = myTree[firstStr]
        for key in secondDict.keys():
            if type(secondDict[key]).__name__ == 'dict':
                numLeafs += getNumLeafs(secondDict[key])
            else:
                numLeafs += 1
        return numLeafs
    
    
    # 获取树的层数
    def getTreeDepth(myTree):
        maxDepth = 0
        firstStr = 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
    
    
    def classify(inputTree, featLables, testVec):
        firstStr = inputTree.keys()[0]
        secondDict = inputTree[firstStr]
        # 将标签字符串转换为索引
        featIndex = featLables.index(firstStr)
        for key in secondDict.keys():
            if testVec[featIndex] != key: continue
            if type(secondDict[key]).__name__ == 'dict':
                classLable = classify(secondDict[key], featLables, testVec)
            else:
                classLable = secondDict[key]
        return classLable
    

      

    总结

    决策树分类器就像带有终止块的流程图,终止块代表分类结果。开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一类。构建决策树时,我们通常采用递归的方法将数据集转化为决策树,一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字段存储树节点的信息。  

  • 相关阅读:
    剑指offer:复杂链表的复制
    剑值offer:最小的k个数
    剑指offer:第一个只出现一次的字符
    剑指offer:树的子结构
    leetcode 240搜索二维矩阵
    leetcode 22括号生成
    leetcode 79 单词搜索
    leetcode 17电话号码的字母组合
    leetcode 78子集
    leetcode 105从前序与中序遍历序列构造二叉树
  • 原文地址:https://www.cnblogs.com/fuxinyue/p/7045432.html
Copyright © 2020-2023  润新知