zoukankan      html  css  js  c++  java
  • 第三章:决策树

      自说原理:决策数是用训练集训练出一棵树,树怎么分叉是由属性(特征决定),测试集的属性按照这个树一直走下去,自然就分类了。一般树的深度越小越好,那如何选属性作为根节点,又选择哪一个属性作为第二个分叉点尼?

    这就用到了信息熵与信息增益的知识。ID3中选择信息增益大的,C4.5中选择增益率大的。以书中的数据集为例:

      属性1  no surfacing 属性2 flippers 标签
    1. 1 1 Y
    2. 1 1 Y
    3. 1 0 N
    4. 0 1 N
    5. 0 1 N

      决策数的算法有很多,ID3,C4.5,CART等等,按照ID3的步骤如下:

      1)算数据集的香农熵(也就是信息熵)。,这里其实计算只与标签这一列有关系,此处因为只有两类,所以n=2。H=-0.4log20.4-0.6log20.6=0.97(这个算下来相当于就是不用任何属性瞎猜的不确定度,越大越不好。)

      2)这里有两个属性,先按照哪个属性划分尼?这得算各自属性的信息增益。选择信息增益大的。比如算属性1的信息增益,得按照属性1的值先将数据集划分,属性1有两个取值{1,0}。属性11中有{【1,Y】,【1,Y】,【0,N】}。属性10中有{【1,N】,【1,N】}。

       然后计算属性1的信息增益,属性11返回的子数据集,算信息熵是Ent1=-2/3log22/3-1/3log21/3;属性10返回的子数据集,算信息熵是Ent2=-1log21;G=0.97-(3/5*Ent1+2/5*Ent2)

       故这里程序中分为两步:1)求属性划分的子数据集

                  2)算信息增益选择最好的数据集划分方式

      3)不断的递归,直到属性用完,或者每一个分支下都变成纯的数据。如果用完所有的属性,还不纯,该叶子节点得少数服从多数。

      4)将1)~3)构建的树进一步操作变成分类器,即输入无标签的测试集,可以进行预测。

      

      对于这些步骤,分别对应的代码(子函数)如下,首先为了调试方便,自营一个数据集:

    1 def createDataSet():
    2     dataSet = [[1, 1, 'yes'],
    3             [1, 1, 'yes'],
    4             [1, 0, 'no'],
    5             [0, 1, 'no'],
    6             [0, 1, 'no']]
    7     labels = ['no surfacing', 'flippers']
    8     return dataSet, labels

      步骤1:

       from math import log
    1
    def calcShannonEnt(dataSet): 2 ''' 3 算数据集的信息熵(也就是根据结果的类别占比,算不确定度) 4 :param dataSet:训练数据集(带标签) 5 :return:信息熵值H 6 ''' 7 # 求list的长度,表示计算参与训练的数据量 8 numEntries = len(dataSet) 9 # 计算分类标签label出现的次数 10 labelCounts = {} 11 for featVec in dataSet: 12 # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签 13 currentLabel = featVec[-1] 14 # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。 15 if currentLabel not in labelCounts.keys(): 16 labelCounts[currentLabel] = 0 17 labelCounts[currentLabel] += 1 18 19 # 对于 label 标签的占比,求出 label 标签的香农熵 20 shannonEnt = 0.0 21 for key in labelCounts: 22 # 使用所有类标签的发生频率计算类别出现的概率。 23 prob = float(labelCounts[key])/numEntries 24 # 计算香农熵,以 2 为底求对数 25 shannonEnt -= prob * log(prob, 2) 26 return shannonEnt

      步骤2)中的1):

     1 def splitDataSet(dataSet, index, value):
     2     '''
     3     求属性划分后的子数据集
     4     :param dataSet: 数据集(带标签)
     5     :param index: 列数(从0开始),比如按照第一个属性划分,那这个index=0
     6     :param value:该属性(作为划分标准的属性)下的某一个值
     7     :return:
     8     '''
     9     # -----------切分数据集的第一种方式 start------------------------------------
    10     retDataSet = []
    11     for featVec in dataSet:
    12         # index列为value的数据集【该数据集需要排除index列】
    13         # 判断index列的值是否为value
    14         if featVec[index] == value:
    15             # chop out index used for splitting
    16             # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行
    17             reducedFeatVec = featVec[:index]
    18             '''
    19             请百度查询一下: extend和append的区别
    20             list.append(object) 向列表中添加一个对象object
    21             list.extend(sequence) 把一个序列seq的内容添加到列表中
    22             1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。
    23             2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。
    24             result = []
    25             result.extend([1,2,3])
    26             print result
    27             result.append([4,5,6])
    28             print result
    29             result.extend([7,8,9])
    30             print result
    31             结果:
    32             [1, 2, 3]
    33             [1, 2, 3, [4, 5, 6]]
    34             [1, 2, 3, [4, 5, 6], 7, 8, 9]
    35             '''
    36             reducedFeatVec.extend(featVec[index+1:])
    37             # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据
    38             # 收集结果值 index列为value的行【该行需要排除index列】
    39             retDataSet.append(reducedFeatVec)
    40     # -----------切分数据集的第一种方式 end------------------------------------
    41 
    42     # # -----------切分数据集的第二种方式 start------------------------------------
    43     # retDataSet = [data for data in dataSet for i, v in enumerate(data) if i == axis and v == value]
    44     # # -----------切分数据集的第二种方式 end------------------------------------
    45     return retDataSet

    比如这里的测试的数据集传入,index=0,value=1,得到的结果是: [[1, 'y'], [1, 'y'], [0, 'n']]

      步骤2)中的2):

     1 def chooseBestFeatureToSplit(dataSet):
     2     """chooseBestFeatureToSplit(选择最好的特征)
     3     Args:
     4         dataSet 数据集
     5     Returns:
     6         bestFeature 最优的特征列的序号(int)
     7     """
     8     # 求有多少个属性
     9     numFeatures = len(dataSet[0]) - 1
    10     # label的信息熵
    11     baseEntropy = calcShannonEnt(dataSet)
    12     # 初始化最优的信息增益值(越大越好), 和最优的Featurn编号
    13     bestInfoGain, bestFeature = 0.0, -1
    14     # iterate over all the features
    15     for i in range(numFeatures):
    16         # 获取第i个属性下的所有值
    17         featList = [example[i] for example in dataSet]
    18         # 获取剔重后的集合,使用set对list数据进行去重
    19         uniqueVals = set(featList)
    20         # 创建一个临时的信息熵
    21         newEntropy = 0.0
    22         # 遍历某一列的value集合,计算该列的信息熵
    23         # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
    24         for value in uniqueVals:
    25             subDataSet = splitDataSet(dataSet, i, value)
    26             prob = len(subDataSet)/float(len(dataSet))
    27             newEntropy += prob * calcShannonEnt(subDataSet)
    28         # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
    29         # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
    30         infoGain = baseEntropy - newEntropy
    31         print('属性',i,'的信息增益infoGain=', infoGain, '       标签的信息熵=', baseEntropy,'        Ent和=', newEntropy)
    32         if (infoGain > bestInfoGain):
    33             bestInfoGain = infoGain
    34             bestFeature = i
    35     return bestFeature

    步骤3):

     1 import operator
     2 from math import log
     3 
     4 def majorityCnt(classList):
     5     """当属性用完,叶节点还是不纯时,得少数服从多数
     6     Args:
     7         classList label列的集合
     8     Returns:
     9         bestFeature 最优的特征列
    10     """
    11     classCount = {}
    12     for vote in classList:
    13         if vote not in classCount.keys():
    14             classCount[vote] = 0
    15         classCount[vote] += 1
    16     # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果
    17     sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    18     # print 'sortedClassCount:', sortedClassCount
    19     return sortedClassCount[0][0]
    20 
    21 def createTree(dataSet, labels):
    22     '''
    23     训练生成一棵数,只不过不是树状,是字典一层一层的
    24     :param dataSet:训练数据集
    25     :param labels:按顺序存放属性名字的列表
    26     :return:一棵用一层一层字典表示的数
    27     '''
    28     classList = [example[-1] for example in dataSet]
    29     # 如果数据集的最后一列(标签列)的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
    30     # 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。
    31     if classList.count(classList[0]) == len(classList):
    32         return classList[0]
    33     # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    34     # 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
    35     if len(dataSet[0]) == 1:
    36         return majorityCnt(classList)
    37 
    38     # 选择最优的列,得到最优列对应的label含义
    39     bestFeat = chooseBestFeatureToSplit(dataSet)
    40     # 获取label的名称
    41     bestFeatLabel = labels[bestFeat]
    42     # 初始化myTree
    43     myTree = {bestFeatLabel: {}}
    44     # 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
    45     # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    46     del(labels[bestFeat])
    47     # 取出最优列,然后它的branch做分类
    48     featValues = [example[bestFeat] for example in dataSet]
    49     uniqueVals = set(featValues)
    50     for value in uniqueVals:
    51         # 求出剩余的标签label
    52         subLabels = labels[:]
    53         # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
    54         myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    55         # print 'myTree', value, myTree
    56     return myTree

    测试:

    data,lab = createDataSet()
    mytree = createTree(data, lab)
    print(mytree)
    结果是:{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}这其实就是一棵树。

    对于这个返回的结果不像实际的树状那么直观,而决策树的一大优点就是直观,书中3.2节中用matplotlib进行画数型图直观化。这里省略了。

    步骤4):
    def classify(inputTree, featLabels, testVec):
        """利用也训练出的树进行预测分类
    
        Args:
            inputTree  决策树模型,也就是以有的树,这里是createTree函数返回的结果myTree
            featLabels Feature标签对应的名称列表
            testVec    测试输入的数据
        Returns:
            classLabel 分类的结果值,需要映射label才能知道名称
        """
        # 获取tree的根节点对于的key值
        firstStr = list(inputTree.keys())[0]
        # 通过key得到根节点对应的value
        secondDict = inputTree[firstStr]
        # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
        featIndex = featLabels.index(firstStr)
        # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
        key = testVec[featIndex]
        valueOfFeat = secondDict[key]
        # print('+++', firstStr, 'xxx', secondDict, '---', key, '>>>', valueOfFeat)
        # 判断分枝是否结束: 判断valueOfFeat是否是dict类型
        if isinstance(valueOfFeat, dict):
            classLabel = classify(valueOfFeat, featLabels, testVec)
        else:
            classLabel = valueOfFeat
        return classLabel
    注意步骤3)中的labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改。在调试时注意。
    data,lab = createDataSet()
    mytree = createTree(data, lab)
    x = classify(mytree,['no surfacing', 'flippers'],[1,1])
    print(x)
    结果是:yes

    此处生出另一个问题就是,每次预测都要训练,是很费事的,所以想办法将训练好的树存储起来。
    def storeTree(inputTree, filename):
        import pickle
        # -------------- 第一种方法 start --------------
        fw = open(filename, 'wb')
        pickle.dump(inputTree, fw)
        fw.close()
        # -------------- 第一种方法 end --------------
    
        # -------------- 第二种方法 start --------------
        with open(filename, 'wb') as fw:
            pickle.dump(inputTree, fw)
        # -------------- 第二种方法 start --------------
    
    
    def grabTree(filename):
        import pickle
        fr = open(filename,'rb')
        return pickle.load(fr)

    >>trees.storeTree(myTree,'1.txt')

    >>trees.grabTree('1.txt')

    到此基本ID3搞定了。完整的可以看这里(Python3运行略有错误,在上面的代码我已经改正)。但是好像有缺少剪枝什么的。

    以上是自写决策树模块,但是sklearn中也有决策数模块,具体使用方法看这里(别人的文章,哈哈)

    这里我也简单用sklearn中的决策树模块预测了上一章中的花蕊数据集。

    这之前与上一章基本一样,所以比较省略

    主要是这一张截图部分不同

     这里没有将数据归一化准确率就高达0.96666,上一章kNN没有归一化时准确率是0.9。这样看似乎是决策树好,但是用上一章的归一化方法处理后,决策树其它参数不变,其预测准确率又变成了0.9333333333333333






  • 相关阅读:
    windows服务器下安装APC
    (经验)在CSS中定义超链接样式a:link、a:visited、a:hover、a:active的顺序
    网站访问计数器
    网站计数器代码(jsp版本)
    .NET2.0隐形的翅膀,正则表达式搜魂者
    PHP中array数组教程
    [Python] py2exe先知其然
    [Python] Ubuntu12.04LTS
    [Python] 中文
    [linux] is not in the sudoers file
  • 原文地址:https://www.cnblogs.com/maxiaonong/p/9996289.html
Copyright © 2011-2022 走看看