美文网首页
2. 决策树

2. 决策树

作者: ydlstartx | 来源:发表于2018-05-14 12:04 被阅读0次

    这一章分为三部分:

    • 决策树的构造方法
    • 测试和存储分类器
    • 使用matplotlib画出决策树结构

    1. 决策树的构造

    根据数据集构建树的过程就是决策树算法的学习过程。那么该如何构造树呢?根据数据的特征来划分数据集,以构造树。

    在构造决策树时,我们需要解决的第一个问题是,当前数据集上哪个特征在划分数据分类时起决定性作用(即根据该特征来划分效果最好,效果的评价在后面讲)。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。找到最佳特征后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点(就是选取的那个特征)的所有分支上。如果某个分支下的数据属于同一类型,则无需进一步对数据集进行分割。否则,需要在数据子集上重复进行划分数据集的过程。

    书中使用的是ID3算法来划分数据集,每次使用一个特征来划分数据,如果该属性有4个可能的值,就把数据划分为4份。所以,要求特征的取值为离散值。

    按照下面顺序讲解:

    • 数据集的熵
    • 按给定特征划分数据集
    • 使用信息增益来选择最好的划分方式
    • 递归构造决策树

    1.1 数据集的熵

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

    而信息熵可以用于评价信息的有序程度。

    这一节讲的是要通过信息增益来选择待划分的特征。关于熵和信息增益的概念可以看这里这里这里这里还有这里。如果根据某一特征划分数据集的信息增益最大,那么就使用该特征。

    首先需要计算原始数据集的熵,再计算根据某一特征划分后的子集的条件熵。熵-条件熵就的到了信息增益,即划分后数据稳定了多少,稳定的越多,划分的效果就越好。

    下面代码用于求给定数据集的熵:

    from math import log
    # 计算给定数据集的熵
    # dataSet为二维列表,[[feature1,feature2,...,label],...]形式
    def calcShannonEnt(dataSet):
        # 数据的个数
        numEntries = len(dataSet)
        labelCounts = {}
        # 统计各label出现的次数,也就是每一类有多少数据
        for featVec in dataSet:
            currentLabel = featVec[-1]
            labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1
            
        shannonEnt = 0.0
        # 计算整个数据集的熵
        for value in labelCounts.values():
            # 某一类在整个数据集中出现的概率
            prob = float(value) / numEntries
            # 求熵的公式
            shannonEnt -= prob * log(prob,2)
        return shannonEnt
    

    1.2 按给定特征划分数据集

    在求条件熵之前需要先根据某一特征划分完数据,下面代码完成这一操作:

    # 按照给定特征划分数据集
    # dataSet为二维列表,[[feature1,feature2,...,label],...]形式,待划分数据集
    # axis为划分特征索引
    # 如果特征取值为value则将该数据划分出来。
    def splitDataSet(dataSet, axis, value):
        retDataSet = []
        for featVec in dataSet:
            if featVec[axis] == value:
                # 注意这里将数据的特征featVec[axis]去掉后,
                # 再将数据加入新的集合
                reducedFeatVec = featVec[:axis]
                # 这里注意不能使用append
                reducedFeatVec.extend(featVec[axis+1:])
                # 此时reducedFeatVec为去掉特征后的数据
                # 将其加入子集
                retDataSet.append(reducedFeatVec)
        # 返回的子集中的数据都是本身具有值为value的特征featVec[axis]
        return retDataSet
    

    1.3 使用信息增益来选择最好的划分方式

    下面的代码重复使用所有的特征来划分数据,并计算每一次划分后的信息增益,找到使划分后信息增益最大的那个特征。

    # 选择最好的数据集划分方式(特征)
    # dataSet为二维列表,[[feature1,feature2,...,label],...]形式,待划分数据集
    # 最好的划分方式也就是使数据集更为有序,熵值也就越低
    def chooseBestFeatureToSplit(dataSet):
        # 得到feature的个数
        numFeatures = len(dataSet[0]) - 1
        # 计算初始数据集的熵
        baseEntropy = calcShannonEnt(dataSet)
        
        # 记录最大信息增益和对应的特征索引
        bestInfoGain = 0.0; bestFeature = -1
        # 按各个feature来迭代划分数据集,找到使信息增益最多的那个划分
        for i in range(numFeatures):
            # 将所有数据的第i个feature存入list中
            featureList = [example[i] for example in dataSet]
            # 仅保留唯一feature值,划分的数据子集数也就是len(uniqueVals)
            # 就是得到数据集中该特征的所有取值。
            uniqueVals = set(featureList)
            # 用于计算子集条件熵
            newEntropy = 0.0
            # 开始划分
            # 每次循环都根据该特征的一个取值划分出对应的子集
            for value in uniqueVals:
                subDataSet = splitDataSet(dataSet, i, value)
                prob = len(subDataSet) / float(len(dataSet))
                # 子集/全集*子集的熵,求和
                # 最后就为划分后集合的条件熵。
                newEntropy += prob * calcShannonEnt(subDataSet)
            
            # 熵-条件熵=信息增益
            # 因为划分后,数据就更有序了,所以熵值就降低了。
            infoGain = baseEntropy - newEntropy
            if infoGain > bestInfoGain:
                bestInfoGain = infoGain
                bestFeature = i
        return bestFeature
    

    1.4 递归构造决策树

    既然是使用递归,那么首先就需要确定停止条件,这里有两个停止条件对应两种情况:

    • 每个分支下的所有实例都具有相同的分类,即该子集中的所有实例都属于一个类别了,此时不需要再继续划分,直接返回该类别。
    • 如果已经对所有的特征进行划分了,这时仍然不能区分一些实例,即这些实例的特征值都相同,但却有着不同的分类。此时采用多数表决来决定该子集或该分支的类别。

    下面代码用于完成多数表决的功能:

    """
    输入的是集合中所有实例的类别,也就是之前dataSet的最后一列
    """
    def majorityCnt(classList):
        classCount = {}
        for vote in classList:
            classCount[vote] = classCount.get(vote, 0) + 1
        sortedClassCount = sorted(classCount.items(), 
                           key=operator.itemgetter(1), 
                           reverse=True)
        return sortedClassCount
    

    递归的过程是,在createTree()中先判断是否符合停止条件,符合则返回类别,否则,进行下面的迭代过程:对于某一集合,通过函数chooseBestFeatureToSplit(dataSet)找到使划分后信息增益最大的那个特征,对于该集合中该特征的n个取值,循环划分该集合(循环的次数即为n),在每次循环中使用splitDataSet(dataSet, axis, value)根据该特征的取值进行一次划分得到一个子集(共有n个子集),然后再调用createTree()函数对该子集进行迭代划分。这样就相当于最先找到一个叶节点,之后返回再找到相邻的叶节点。假设根节点的度为5,那么先完成第一个分支的构建,之后再完成下一分支。。。。。

    下面为创建树的代码:

    # dataSet为二维列表,[[feature1,feature2,...,label],...]形式,待划分数据集
    # labels为所有的特征名,也就是dataSet[i,:-1]各个列的名字
    def createTree(dataSet, labels):
        # 获取当前数据集的类列表
        classList = [example[-1] for example in dataSet]
        # 第一个递归停止条件,数据集中的实例全为同一类,返回类名
        if classList.count(classList[0]) == len(classList):
            return(classList[0])
        # 第二个停止条件,遍历完所有特征后,数据仍不是同一类
        # 则返回实例数量最多的那个类
        if len(dataSet[0]) == 1:
            return(majorityCnt(classList))
        
        # 得到对当前数据集的最优划分特征的索引
        bestFeat = chooseBestFeatureToSplit(dataSet)
        # 得到该特征的名字
        bestFeatLabel = labels[bestFeat]
        # 将该特征作为当前根节点来构建子树
        # 结构是多层嵌套的字典。
        # 如果key的value是类标签,则是叶节点
        # 若果是另一个字典,则是一个判断节点
        myTree = {bestFeatLabel:{}}
        # 下面就是根据该特征划分,该特征已经使用,将其从特征列表中删除,注意这里的删除是对原始labels中操作了
        del(labels[bestFeat])
        # 得到当前数据集中该特征的所有取值
        featValues = [example[bestFeat] for example in dataSet]
        uniqueVals = set(featValues)
        # 根据特征取值来划分数据集,由于在循环中递归调用了本函数
        # 这样就会不断进行划分的过程,直到完全分完该数据集
        for value in uniqueVals:
            subLabels = labels[:]
            newDataSet = splitDataSet(dataSet, bestFeat, value)
            myTree[bestFeatLabel][value] = createTree(newDataSet, subLabels)
        return(myTree)
    

    可见代码中使用深层嵌套的字典来代表树的结构。字典中有两种key,特征名称和该特征的取值。只有一种value,即类别。

    下面代码构建一个简单的树:

    # 一个简单的数据集
    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
    
    myDat,labels = createDataSet()
    # 构建树
    createTree(myDat, labels)
    """
    结果为(缩进一下方便看):
    {'no surfacing': {0: 'no', 
                      1: {'flippers': {0: 'no', 
                                       1: 'yes'}}}}
    """
    

    2. 测试和存储分类器

    使用构造好的树来对新实例进行分类。这里还是使用递归操作来查找类别:

    # 使用决策树进行分类,还是递归操作
    # inputTree为createTree()返回的树
    # featLabels为各特征按顺序组成的列表,用于得到某一特征的索引
    # testVec为待分类实例
    def classify(inputTree, featLabels, testVec):
        # 传入的inputTree一定是个树或子树
        # 所以firstStr一定为某一特征名
        firstStr = list(inputTree.keys())[0]
        # 得到该特征的分支,secondDict.keys()为该特征的各个取值
        secondDict = inputTree[firstStr]
        # 该特征在特征列表中的索引
        featIndex = featLabels.index(firstStr)
        # key为该特征的各个取值
        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
    

    上面代码有一个bug,如果输入实例的某一特征值在训练集中没有出现过,则代码会产生错误。修改如下:

    # 使用决策树进行分类,还是递归操作
    # inputTree为createTree()返回的树
    # featLabels为各特征按顺序组成的列表,用于得到某一特征的索引
    # testVec为待分类实例
    def classify(inputTree, featLabels, testVec):
        # 传入的inputTree一定是个树或子树
        # 所以firstStr一定为某一特征名
        firstStr = list(inputTree.keys())[0]
        # 得到该特征的分支,secondDict.keys()为该特征的各个取值
        secondDict = inputTree[firstStr]
        # 该特征在特征列表中的索引
        featIndex = featLabels.index(firstStr)
        
        # 如果输入实例的对应特征的值在secondDict.keys()中,说明其可能属于该子集下的某一类别
        if testVec[featIndex] in secondDict.keys():
            # 如果该分支下面还有分支,则进行迭代
            if type(secondDict[testVec[featIndex]]).__name__ == 'dict':
                classLabel = classify(secondDict[testVec[featIndex]], featLabels, testVec)
            # 如果下面已经没有分支了,说明找到了对应的类别
            else:
                classLabel = secondDict[testVec[featIndex]]
        # 如果输入实例的该特征的值不存在于树中,则返回没有该类
        else:
            classLabel = 'no class'
        # 返回所属类别
        return classLabel
    

    代码测试如下:

    >>> myDat,labels = createDataSet()
    >>> classify(myTree, labels, [1,2])
    'no class'
    >>> classify(myTree, labels, [1,0])
    'no'
    >>> classify(myTree, labels, [1,1])
    'yes'
    

    分类器的存储就是将树字典有pickle序列化存储,很简单。关于python中序列化操作的简介可看这里

    本章介绍的决策树算法有很多的缺点,如对输入数据的过拟合,ID3算法无法处理数值型数据。在后面的章节中会介绍决策树的裁剪和CART算法来改正这些问题。

    3. 使用matplotlib画出决策树结构

    待续。。。。。。

    4. 参考

    关于决策树比较详细的总结在这里

    相关文章

      网友评论

          本文标题:2. 决策树

          本文链接:https://www.haomeiwen.com/subject/vxufdftx.html