机器学习实践之决策树算法学习

Posted 蓝色之旅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了机器学习实践之决策树算法学习相关的知识,希望对你有一定的参考价值。

关于本文说明,本人原博客地址位于http://blog.csdn.net/qq_37608890,本文来自笔者于2017年12月06日 18:06:30所撰写内容(http://blog.csdn.net/qq_37608890/article/details/78731169)。

 

  本文根据最近学习机器学习书籍 网络文章的情况,特将一些学习思路做了归纳整理,详情如下.如有不当之处,请各位大拿多多指点,在此谢过.

一、决策树(decision tree)概述

1、决策树概念

       决策树(decision tree)是一个树结构(可以是二叉树或非二叉树)。其每个非叶节点表示一个特征属性上的测试,每个分支代表这个特征属性在某个值域上的输出,而每个叶节点存放一个类别。使用决策树进行决策的过程就是从根节点开始,测试待分类项中相应的特征属性,并按照其值选择输出分支,直到到达叶子节点,将叶子节点存放的类别作为决策结果。

2 工作原理

      在构造决策树时,需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起到来决定性的作用。为了找到决定性的特征,我们需要对每个特征都要进行评估.完成测试后,原始数据就被划分为几个数据子集.这些数据子集会分布在第一个决策点的所有分支上.若某一分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经被正确地划分数据分类,没必要再对数据集进行分类.否则,则需要重复划分数据子集的过程.这里划分子集的算法和划分原始数据集的方法相同,直至所有具有相同类型的数据都进入一个数据子集内.构造决策树伪代码函数createBranch()如下:

 

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

        一旦我们构造了一个决策树模型,以它为基础来进行分类将是非常容易的。具体做法是,从根节点开始,对实例的某一特征进行测试,根据测试结构将实例分配到其子节点(也就是选择适当的分支);沿着该分支可能达到叶子节点或者到达另一个内部节点时,那么就使用新的测试条件递归执行下去,直到抵达一个叶子节点。当到达叶子节点时,我们便得到了最终的分类结果。下面介绍一个小例子。

    通俗来说,决策树分类的思想类似于找对象。现想象一个女孩的母亲要给这个女孩介绍男朋友,于是有了下面的对话:

女儿:多大年纪了?
母亲:26。
女儿:长的帅不帅?
母亲:挺帅的。
女儿:收入高不?
母亲:不算很高,中等情况。
女儿:是公务员不?
母亲:是,在税务局上班呢。
女儿:那好,我去见见。


      这个女孩的决策过程就是典型的分类树决策。相当于通过年龄、长相、收入和是否公务员对将男人分为两个类别:见和不见。假设这个女孩对男人的要求是:30岁以下、长相中等以上并且是高收入者或中等以上收入的公务员,那么这个可以用下图表示女孩的决策逻辑:

 

技术分享图片

        上图完整表达了这个女孩决定是否见一个约会对象的策略,其中绿色节点表示判断条件,橙色节点表示决策结果,箭头表示在一个判断条件在不同情况下的决策路径,图中红色箭头表示了上面例子中女孩的决策过程。
这幅图基本可以算是一颗决策树,说它“基本可以算”是因为图中的判定条件没有量化,如收入高中低等等,还不能算是严格意义上的决策树,如果将所有条件量化,则就变成真正的决策树了。

3、决策树的相关特性

  •    优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。

  •    缺点:可能会产生过度匹配问题。

  •    使用数据类型: 数值型和标称型。

4、 一般流程

    (1) 收集数据: 可以使用任何方法.

    (2) 准备数据: 构造算法只适用于标称型数据,因此数值型数据必须离散化.

    (3) 分析数据: 可以使用任何方法,构造树完成之后,应该检查图形是否符合预期.

    (4) 训练算法: 构造树的数据结构.

    (5) 测试算法: 使用经验树计算错误率.

    (6) 使用算法: 此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义.

 

二 决策树场景

       假设,现在有一个叫做 "十五个问题" 的游戏,游戏的规则很简单:参与游戏的一方在脑海中想某个事物,其他参与者向他提问,只允许提 15个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围,最后得到游戏的答案。决策树的工作原理与15个问题类似,用户输入一系列数据后给出游戏答案。

      下图给出了一个假想的邮件分类系统,它首先检测发送邮件域名.如果地址为myEmployer.com,则将其放在"无聊时需要阅读的邮件"中。否则,则需要检查邮件内容中是否包含单词 曲棍球 ,若包含则将邮件归入"需要及时处理的朋友邮件",否则则归类到"无需阅读的垃圾邮件"。

 

    技术分享图片

 

   决策树一个很重要的任务就是为了理解数据中所蕴含的知识信息(这与K-近邻算法无法给出数据的内在含义有着显著不同),因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,这些机器根据数据集创建规则的过程,就是机器学习的过程。

三 决策树项目案例一  对海洋生物进行鱼和非鱼判断

1 项目情况

      下表中的数据包含5个海洋生物,特征: 不浮出水面是否可以生存和是否有脚蹼.现将动物划分为两类: 鱼和非鱼.如果想依据给出的特征选出一个来划分数据,就涉及到要将划分数据的依据进行量化后才可以判断出来.

   技术分享图片 

  我们先构造进行数据输入的createDataSet()函数和计算给定数据集的香农熵函数calcShannonEnt()

   

    def createDataSet():  
        dataSet = [[1,1,‘yes‘],  
                  [1,1,‘yes‘],  
                  [1,0,‘no‘],  
                  [0,1,‘no‘],  
                  [0,1,‘no‘]]  
        labels=[‘no surfacing‘,‘flippers‘]  
        # change to discrete values  
        return dataSet,labels  
    #信息增益  
    #计算给定数据的香农熵  
      
    def calcShannonEnt(dataSet): #the the number of unique elements and their occurance  
        numEntries = len(dataSet)  
        labelCounts = {}  
        for featVec in dataSet:  
            currentLabel=featVec[-1]  
            if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0  
            labelCounts[currentLabel] +=1  
        shannonEnt = 0.00000  
        for key in labelCounts:  
            prob = float(labelCounts[key]) /numEntries  
            shannonEnt -= prob * log(prob,2)   #log base 2  
              
        return shannonEnt  

 

 执行

    myDat,labels=createDataSet()  
    myDat  

 得到 

[[1, 1, ‘yes‘], [1, 1, ‘yes‘], [1, 0, ‘no‘], [0, 1, ‘no‘], [0, 1, ‘no‘]]

 执行

calcShannonEnt(myDat)  

得到

0.9709505944546686

熵越高,则混合的数据越多,我们可用在数据集中添加更多的分类,观察熵是如何变化的.

按照给定特征划分数据集,将指定特征的特征值等于 value 的行剩下列作为子数据集。

 

    def splitDataSet(dataSet, index, value):  
        """splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行) 
            就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中 
        Args: 
            dataSet 数据集                 待划分的数据集 
            index 表示每一行的index列        划分数据集的特征 
            value 表示index列对应的value值   需要返回的特征的值。 
        Returns: 
            index列为value的数据集【该数据集需要排除index列】 
        """  
        retDataSet = []  
        for featVec in dataSet:   
            # index列为value的数据集【该数据集需要排除index列】  
            # 判断index列的值是否为value  
            if featVec[index] == value:  
                # chop out index used for splitting  
                # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行  
                reducedFeatVec = featVec[:index]  
                ‘‘‘‘‘ 
                请百度查询一下: extend和append的区别 
                list.append(object) 向列表中添加一个对象object 
                list.extend(sequence) 把一个序列seq的内容添加到列表中 
                1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。 
                2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。 
                result = [] 
                result.extend([1,2,3]) 
                print result 
                result.append([4,5,6]) 
                print result 
                result.extend([7,8,9]) 
                print result 
                结果: 
                [1, 2, 3] 
                [1, 2, 3, [4, 5, 6]] 
                [1, 2, 3, [4, 5, 6], 7, 8, 9] 
                ‘‘‘  
                reducedFeatVec.extend(featVec[index+1:])  
                # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据  
                # 收集结果值 index列为value的行【该行需要排除index列】  
                retDataSet.append(reducedFeatVec)  
        return retDataSet  

 选择最好的数据集划分方式:

 

    def chooseBestFeatureToSplit(dataSet):  
        """chooseBestFeatureToSplit(选择最好的特征) 
     
        Args: 
            dataSet 数据集 
        Returns: 
            bestFeature 最优的特征列 
        """  
        # 求第一行有多少列的 Feature, 最后一列是label列嘛  
        numFeatures = len(dataSet[0]) - 1  
        # 数据集的原始信息熵  
        baseEntropy = calcShannonEnt(dataSet)  
        # 最优的信息增益值, 和最优的Featurn编号  
        bestInfoGain, bestFeature = 0.0, -1  
        # iterate over all the features  
        for i in range(numFeatures):  
            # create a list of all the examples of this feature  
            # 获取对应的feature下的所有数据  
            featList = [example[i] for example in dataSet]  
            # get a set of unique values  
            # 获取剔重后的集合,使用set对list数据进行去重  
            uniqueVals = set(featList)  
            # 创建一个临时的信息熵  
            newEntropy = 0.0  
            # 遍历某一列的value集合,计算该列的信息熵   
            # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。  
            for value in uniqueVals:  
                subDataSet = splitDataSet(dataSet, i, value)  
                # 计算概率  
                prob = len(subDataSet)/float(len(dataSet))  
                # 计算信息熵  
                newEntropy += prob * calcShannonEnt(subDataSet)  
            # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值  
            # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。  
            infoGain = baseEntropy - newEntropy  
            print ‘infoGain=‘, infoGain, ‘bestFeature=‘, i, baseEntropy, newEntropy  
            if (infoGain > bestInfoGain):  
                bestInfoGain = infoGain  
                bestFeature = i  
        return bestFeature  

 

训练算法:构造树的数据结构

  创建树的函数

    def createTree(dataSet, labels):  
        classList = [example[-1] for example in dataSet]  
        # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行  
        # 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。  
        # count() 函数是统计括号中的值在list中出现的次数  
        if classList.count(classList[0]) == len(classList):  
            return classList[0]  
        # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果  
        # 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。  
        if len(dataSet[0]) == 1:  
            return majorityCnt(classList)  
      
        # 选择最优的列,得到最优列对应的label含义  
        bestFeat = chooseBestFeatureToSplit(dataSet)  
        # 获取label的名称  
        bestFeatLabel = labels[bestFeat]  
        # 初始化myTree  
        myTree = {bestFeatLabel: {}}  
        # 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改  
        # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示‘no surfacing‘ is not in list  
        del(labels[bestFeat])  
        # 取出最优列,然后它的branch做分类  
        featValues = [example[bestFeat] for example in dataSet]  
        uniqueVals = set(featValues)  
        for value in uniqueVals:  
            # 求出剩余的标签label  
            subLabels = labels[:]  
            # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()  
            myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)  
            # print ‘myTree‘, value, myTree  
        return myTree  

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

 

    def classify(inputTree, featLabels, testVec):  
        """classify(给输入的节点,进行分类) 
     
        Args: 
            inputTree  决策树模型 
            featLabels Feature标签对应的名称 
            testVec    测试输入的数据 
        Returns: 
            classLabel 分类的结果值,需要映射label才能知道名称 
        """  
        # 获取tree的根节点对于的key值  
        firstStr = 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  

 

 

三  项目案例2: 使用决策树预测隐形眼镜类型

项目概述

隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。
开发流程

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

收集数据:提供的文本文件

文本文件数据格式如下:

  

young   myope   no  reduced no lenses  
pre myope   no  reduced no lenses  
presbyopic  myope   no  reduced no lenses 

 

  解析数据:解析 tab 键分隔的数据行

    lecses = [inst.strip().split(‘\t‘) for inst in fr.readlines()]  
    lensesLabels = [‘age‘, ‘prescript‘, ‘astigmatic‘, ‘tearRate‘]  

 

分析数据:快速检查数据,确保正确地解析数据内容,使用 createPlot() 函数绘制最终的树形图。

treePlotter.createPlot(lensesTree)  

 
训练算法:使用 createTree() 函数

 

    >>> lensesTree = trees.createTree(lenses, lensesLabels)  
    >>> lensesTree  

 

 
得到

 

    {‘tearRate‘: {‘reduced‘: ‘no lenses‘, ‘normal‘: {‘astigmatic‘:{‘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‘}}}}}  

 

 

五 小结

其实决策树跟带终止块的流程图类似,所以这里的终止块就是分类结果.当我们进行数据处理时,首先要对集合中数据的不一致性进行测量评估,也就是计算香农熵,下一步才可以寻找最有方案划分数据,最终实现所有具有相同类型的数据都划分到同一个数据子集里面.在构建数据树时,我们一般采用递归方把数据集转化为决策树.多数情况下,我们不构造新的数据结构,而是采用Python语言内嵌的数据结构字典存储树节点信息.每一步选择信息增益最大的特征作为决策块,最终来完成决策树的生成.


Matplotlib的注解功能,可以让将存储的树结构转化为容易理解的图形.隐形眼镜的例子说明决策树可能会产生过多的数据集划分,结果导致过度匹配数据集的问题.当然可以通过裁剪决策树,合并相邻的不能产生信息增益的叶节点,来解决这个问题(过度匹配).


关于决策树的构造算法,这里本文只是用了ID3算法,当然还有C4.5和CART算法.对于决策树的完整工作过程而言,包括三部分:


1 特征选择;


2 生成决策树;


3 剪枝部分


而除去ID3算法,其他两个算法都有剪枝部分过程.所以这也迎合来隐形眼镜过拟合的问题.

  关于决策树部分,笔者先整理到这里,后续有机会会针对C4.5和CART算法做些归纳整理.有不足之处,请各位同仁多多指导.



















































以上是关于机器学习实践之决策树算法学习的主要内容,如果未能解决你的问题,请参考以下文章

机器学习:通俗易懂决策树与随机森林及代码实践

《全网最强》详解机器学习分类算法之决策树(附可视化和代码)

Python机器学习算法之决策树算法

常见机器学习算法原理+实践系列4(决策树)

机器学习算法之决策树(上)

机器学习算法 - 随机森林之决策树初探