美文网首页
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. 参考

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

相关文章

  • 决策树

    问题: 1. 决策树节点是如何确定? 2. 决策树构建流程是怎么样? 3. 决策树都有哪些优缺点? 4. 决策树缺...

  • 2. 决策树

    这一章分为三部分: 决策树的构造方法 测试和存储分类器 使用matplotlib画出决策树结构 1. 决策树的构造...

  • 风险管理第五课

    # 决策树 1. 列表 2.列表

  • Parallel induction algorithms fo

    1. Abstract C4.5决策树的并行 2. Intro 决策树在大数据集上计算开销太大 找到最优的决策树是...

  • 数据挖掘之决策树(实现鸢尾花数据集分析)

    实验三、数据挖掘之决策树 一、实验目的 1. 熟悉掌握决策树的原理, 2. 熟练掌握决策树的生成方法与过程 二、实...

  • Decision Tree

    决策树非常之robust! 决策边界很有趣! 1.可线性分割吗? 2. 构建决策树(分割) 3. sklearn ...

  • 决策树

    1.基本概念 1.决策树是一种基本的分类与回归方法。这里主要讨论决策树用于分类。 2.决策树模型是描述对样本进行分...

  • 分类练习题1

    1.为四个布尔属性A,B,C,D的奇偶函数画一棵完全决策树。可以简化该决策树吗? 不能简化该决策树。 2.考虑下表...

  • 机器学习|决策树分类与python实现

    目录: 1.决策树简介 2.决策树生成 a) 选择标准——熵 b) 信息增益——ID3算法 c) 信息增益率——C...

  • 一周总结

    1. 决策树: 多重选择摆在眼前的时候,经过慎重思考,层层筛选,最终选择最佳的方案,这就是决策树。 2. 德尔菲...

网友评论

      本文标题:2. 决策树

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