决策树的搭建与绘制

理论知识大部分参考七月在线学习笔记(很好,推荐)https://www.zybuluo.com/frank-shaw/note/103575

部分理论和编程主要参考《机器学习实战》

 

一、作用

首先,要搞清楚决策树能做什么?

事实上,决策树学习是用于处理分类问题的机器学习方法,而这些类别事先是知道的,你只需要选择其中的某一个类作为你最终的决策即可,也就是说,决策树的学习是一个监督学习的过程。

具体例子网络上太多了,最经典的、我看的最多的一个也就是女儿相亲问题,有兴趣可以查看我给出的参考文献中的具体例子。

总之,决策树的作用就是帮助我们在一定的情况下,作出作好的决策或者说是决定!

 

二、决策树的一般流程

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

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

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

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

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

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

的内在含义。

 

三、决策树的生成算法

决策树算法中,最重要的一步,也就是根据已有数据构建决策树。那么如何来构建这个决策树呢?这里主要采用自顶向下的构建方式,采用的算法主要有以下三种:ID3、C4.5、CART,这里简单介绍一下三个算法表达的思想,具体数学推导这里不再赘述,网络上实在太多太多,参考资料中有较为消息的解释!

(1)ID 3

ID3算法的实质就是使用贪婪算法的思想,自顶向下地使寻找减小数据无序程度的分类方式!

而这个无序程度我们用信息熵来进行量化。公式如下:假如一个随机变量的取值为,每一种取到的概率分别是,那么

   的熵定义为

信息熵的值越高则表示当前数据无序程度越高,越低则表示数据无序程度越低。对于数据分类,我们当然希望得到无序程度尽可能低的分类方式!

那么,每选择一个分类特征,该数据的无序程度应该得到一定的减少,而这个减少的量我们称之为信息增益

上文中说明该算法使用到了贪婪算法的思想:实际上,每次之选择当前最好的,也就是能使当前数据无序程度减少最多的特征作为分类对象,而像这样的每次筛选,实际上是对局部最优解的求解而非全局,所以,该算法下决策数很可能过拟合

(2)C4.5

C4.5算法是基于ID3算法的,也是ID3的一种改进算法

之所以说ID3算法的改进,是因为ID3算法是有明显的缺点,举例说明:

若对于当前数据来说,有一个特征X下有10个(甚至更多)不同的、独立的取值,而每个取值恰好只有一个数据!

仔细想一下,该特征分类下,无序程度将刚好等于0,也就代表着此时的信息增益是最大的!那么算法选择该特征为分类特征,则会得到一个很宽但是很浅的决策数,这样的分类是很不理想的!

这是一个极端情况,实际上,ID3算法更倾向于选择特征取值更多的特征!

此时,引入了C4.5算法,该算法使用信息增益率来筛选分类特征!

上述情况下,信息增益率将引入一个很大的数(分母)来修正信息增益,于是就避免了ID3算法的不足之处!

(3)CART

也称为回归决策树

 CART算法是一种二分递归分割技术,把当前样本划分为两个子样本,使得生成的每个非叶子结点都有两个分支,

 因此CART算法生成的决策树是结构简洁的二叉树。由于CART算法构成的是一个二叉树,它在每一步的决策时只能

 是“是”或者“否”,即使一个feature有多个取值,也是把数据分为两部分。在CART算法中主要分为两个步骤

   (1)将样本递归划分进行建树过程

   (2)用验证数据进行剪枝

参考:http://blog.csdn.net/acdreamers/article/details/44664481

http://www.cnblogs.com/zhangchaoyang/articles/2709922.html

 

上述三个算法,都是基于贪新算法原理的,所以都存在局部最优解以及过拟合的问题。过拟合实际上就是对噪声数据过于敏感,是一种不利于得到正确结果的拟合方式。

决策树算法中,为了避免过拟合的出现,往往采用修剪树的办法,具体修剪理论,参考:http://blog.sina.com.cn/s/blog_4e4dec6c0101fdz6.html

 

四、基于Python的具体实现

代码见下,注释写的非常清楚了!

import matplotlib.pyplot as plt
from math import log
import operator

#定义节点的绘图类型以及箭头形式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")

#计算最后一个特征分类的信息熵
def calcShannonEnt(dataSet):
    numEntries  = len(dataSet)#统计数据量的大小
    labelCounts = {}#对每一个例子特征计数的数组
    #遍历所有的例子
    for feature in dataSet:
        currentLabel = feature[-1]#这里计算每个例子的最后一个特征的信息熵
        if currentLabel not in labelCounts.keys():#如果该特征没有在之前的遍历中出现,则创建这样的空间存放
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1#计数,为计算概率打基础
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries#计算出现的概率
        shannonEnt -= prob * log(prob,2)#套用公式计算信息熵
    return shannonEnt
#创建数据
def createDataSet():
    dataset = [[0,1,1,1,'yes'],[0,0,1,1,'yes'],[1,1,0,0,'maybe'],[0,1,0,1,'no'],[1,0,0,0,'no']]
    labels = ['tail','head','no surfacing','flippers']
    #dataset = [[1,1,'mabey'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
    #labels = ['no surfacing','flippers']
    return dataset,labels
#dataset带划分数据集,axis划分数据集的特征,value特征的返回值
def splitDataSet(dataSet,axis,value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:#相当于挑出指定位置特征的特征值的其他特征
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet
#看选取哪一个特征作分类可以使得信息增益最大,也就是无序程度降得最低
def chooseBestFeatureToSplit(dataset):
    numFeature = len(dataset[0]) - 1#最后一个是决策层
    baseEntropy = calcShannonEnt(dataset)#计算整个数据的信息熵
    bestInfoGain = 0.0
    bestFeature = -1#表示最后一个特征哟
    for i in range(numFeature):
        featList = [example[i] for example in dataset]#第i个特征的所有特征分类值
        uniqueVals = set(featList)
        newEntropy = 0.0
        for value in uniqueVals:
            subDataSet = splitDataSet(dataset,i,value)
            prob = len(subDataSet)/float(len(dataset))#value的例子个数/整个例子的个数
            newEntropy += prob * calcShannonEnt(subDataSet)#这里实际上计算分类后的无序程度,作为增益
            infoGain = baseEntropy - newEntropy#信息增益是熵的减少或者是数据无序度的减少
        if (bestInfoGain < infoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature
#求某个特征中特征值最多的特征值
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(),
                              key = operator.itemgetter(1),reverse = True)
    return sortedClassCount[0][0]

#构造一个决策树
def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]#取所有例子中决策层特征
    if classList.count(classList[0]) == len(classList):#如果所有的特征值都和classList[0]相同的话,返回这个类别
        return classList[0]
    if len(dataSet[0]) == 1:#只有一个特征了,返回这个特征出现次数最多的特征值
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)#求当前最佳的划分特征
    #print(bestFeat)
    bestFeatLabel = labels[bestFeat]
    print(bestFeat)
    myTree = {bestFeatLabel:{}}
    #print(myTree)
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
    return myTree
 注意:1.数据要一一对应,

    data = {{data1,data2,....datan,decision1},{...},...,{...}}

    labels = {label1,label2,....,labeln}

    data中最后一个数据是决策层的数据!

     2.注意退出条件:(关注createTree函数)

    <1> 决策相同,无需分类(所以这可能出现有些类别用不到,后面会详细提到)

    <2> data只有决策层了,特征都已经分完了

              

 五、基于Python的决策树绘制

该部分参考《机器学习实战》一书,但是仔细做过的同学应该会发现,书上给的代码貌似不是很正确,或者说,不是很理想吧,这里对决策树的绘制进行了一些修改,得到还不错的效果。这里使用到Python的绘图工具Matplotlib,首先,我们需要安装Matplolib工具包。      

 (1)安装Matplotlib工具包

为了方便,可以安装Anaconda的Python科学集成环境,Anaconda中包含有很多Python的科学计算工具,包括numpy、matplotlib等。安装完成之后,在Pycharm中选择当前解释器为Anaconda/python.exe,即可使用Anaconda提供的科学工具包了

(2)先来看看代码和效果吧          

代码:      

#定义节点的绘图类型以及箭头形式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
#获得树的叶节点的数目
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = list(myTree.keys())[0]#获得当前第一个根节点
    secondDict = myTree[firstStr]#获取该根下的子树
    for key in list(secondDict.keys()):#获得所有子树的根节点,进行遍历
        if type(secondDict[key]).__name__ == 'dict':#如果子节点是dict类型,说明该子节点不是叶子节点
            numLeafs += getNumLeafs(secondDict[key])#不是子节点则继续往下寻找子节点,直到找到子节点为止
        else:
            numLeafs += 1#找到子节点,数+1
    return numLeafs

#获得树的层数
def getTreeDepth(myTree):
    treeDepth = 0
    temp = 0
    firstStr = list(myTree.keys())[0]#获得当前第一个根节点
    secondDict = myTree[firstStr]#获取该根下的子树
    for key in list(secondDict.keys()):#获得所有子树的根节点,进行遍历
        if type(secondDict[key]).__name__ == 'dict':#如果子节点是dict类型,说明该子节点不是叶子节点
            temp = 1 + getTreeDepth(secondDict[key])#该节点算一层,加上往下数的层数
        else:
            temp = 1#叶子节点算一层
        if temp > treeDepth:
            treeDepth = temp
    return treeDepth

#计算父节点到子节点的中点坐标,在该点上标注txt信息
def plotMidText(cntrPt,parentPt,txtString):
    xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0]
    yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]
    createPlot.ax1.text(xMid,yMid,txtString)


def plotTree(myTree,parentPt,nodeTxt):
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)
    firstStr = list(myTree.keys())[0]
    #cntrPt = (plotTree.xOff + (0.5 + float(numLeafs)/2.0/(plotTree.totalW*plotTree.totalW)),plotTree.yOff)
    #cntrPt = (0.5,1.0)
    cntrPt =((2 * plotTree.xOff + (float(numLeafs) + 1) * (1 / plotTree.totalW)) / 2 , plotTree.yOff)
    print(plotTree.xOff)
    plotMidText(cntrPt,parentPt,nodeTxt)
    plotNode(firstStr,cntrPt,parentPt,decisionNode)
    secondDict =  myTree[firstStr]
    #print(secondDict)
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
    for key in list(secondDict.keys()):
        if type(secondDict[key]).__name__ == 'dict':
            plotTree(secondDict[key],cntrPt,str(key))
        else:
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff,plotTree.yOff),cntrPt,leafNode)
            plotMidText((plotTree.xOff, plotTree.yOff),cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False,**axprops)
    plotTree.totalW = float(getNumLeafs(inTree))
    plotTree.totalD = float(getTreeDepth(inTree))
    plotTree.xOff = -0.5/plotTree.totalW
    plotTree.yOff = 1.0
    plotTree(inTree,(0.5,1.0),'')
    plt.show()

#给createPlot子节点绘图添加注释。具体解释:nodeTxt:节点显示的内容;xy:起点位置;xycoords/xytext:坐标说明?;xytext:显示字符的位置
#va/ha:显示的位置?;bbox:方框样式;arrow:箭头参数,字典类型数据
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
                            xytext=centerPt, textcoords='axes fraction',
                            va="bottom", ha="center", bbox=nodeType, arrowprops=arrow_args)                          
我们注意到程序中定义的数据和标签如下:
    dataset = [[0,1,1,1,'yes'],[0,0,1,1,'yes'],[1,1,0,0,'maybe'],[0,1,0,1,'no'],[1,0,0,0,'no']]
    labels = ['tail','head','no surfacing','flippers']
得到决策树结果:{'no surfacing': {0: {'tail': {0: 'no', 1: {'head': {0: 'no', 1: 'maybe'}}}}, 1: 'yes'}}

绘制决策树:

      这个决策树的绘制是怎么得到的呢?(再次强调一下,我自己运行《机器学习实战》一书的代码,得到的绘制结果是不对的,不知道是不是我自己弄错了,还望大家指正,这里我揣测文意,稍微修改了一下代码)

首先,我们要确定一下绘制决策树的一些原则:

1.自上到下的树形结构

2.树的根节点在图的中心位置

3.树的宽度尽量均匀覆盖整个图片(就是最左\右边的叶子离图像边框距离短且相等)

4.假设整张图片的大小为1*1

有了以上的基本原则,在使用决策树算法得到了myTree的决策树后,就能方便地绘制决策树了!在编程过程中,实际上是利用递归的方式(与myTree的构建方式类似),自上而下、自左而右的顺序来绘制图像,终止条件是遇到了叶子节点。

(1)对于高度,我们希望树能填充整个图像,于是,我们要得到树的深度depth,则可以设置高度的绘制步长为step_h = height/depth。由于绘制顺序是自上而下的(这里的自上而下是一个宏观上的看法,对于每个小的子树的搜寻顺序实际上是:根节点->左子树->根节点->右子树),故每一次递归都要y-step_h,当该次递归结束之后,需要y+step_h,回到递归的起始节点,继续判断递归(这个意思就是,该节点左子树递归,递归完毕回到该节点,开始递归右子树的过程)

(2)对于宽度,我们同样希望树能填充整个图像,于是,我们要得到树的叶子节点数目leafs,则可以设置宽度的绘制步长为step_w = width/leafs。根节点设置在中点位置,即(0.5,1)。接下来,每次递归的子树的根节点将位于其子树的中心位置(也就是该子树所有叶子节点构成宽度的中间)。由于绘制顺序是自左而右的,故我们设置xOff的初始值是-step_w*0.5,每次递归都令xOff = xOff + step_w,如此,最左和最右的叶子节点与图边框相差距离为step_w*0.5!

 绘图过程完毕!是不是很简单!

 

回过头来,再来看看决策树构建过程,createTree的终止条件问题。首先看这几组数据集得到的决策树结果:

(1)

 dataset = [[0,1,1,1,'yes'],[0,0,1,1,'yes'],[1,1,0,0,'maybe'],[0,1,0,1,'no'],[1,0,0,0,'no']]
 labels = ['tail','head','no surfacing','flippers']

 结果:{'no surfacing': {0: {'tail': {0: 'no', 1: {'head': {0: 'no', 1: 'maybe'}}}}, 1: 'yes'}}

 

(2)

dataset = [[0,1,1,1,'yes'],[0,0,1,1,'yes'],[1,1,0,0,'maybe'],[0,1,0,1,'no'],[1,0,0,1,'no']]
labels = ['tail','head','no surfacing','flippers']

    结果:{'no surfacing': {0: {'flippers': {0: 'maybe', 1: 'no'}}, 1: 'yes'}}              

                     
仔细看会发现,(1)和(2)的数据集相差仅一个数据的不同,但是(2)的分类方式却与(1)大相径庭,甚至少一个标签,乍一看,有种“我写的决策树是不是错的”感觉!其实,认真思考会发现,这是递归终止条件所导致的!回顾一下上文中提到的决策树递归终止条件,有两个:

     <1> 决策相同,无需分类

     <2> data只有决策层了,特征都已经分完了

不妨模拟一下(2)的过程。(2)的数据集如下表:

tail head no surfacing flippers fish
0 1 1 1 yes
0 0 1 1 yes
1 1 0 0 maybe
0 1 0 1 no
1 0 0 1 no

step1:发现,no surfacing是当前最好的分类特征,划分出来

于是根据no surfacing得到两组数据

1.no surfacing = 1

tail head flippers fish
0 1 1

yes

0 0 1 yes

2.no surfacing = 0

tail head flippers fish
1 1 0 maybe
0 1 1 no
1 0 1 no

step2:

对no surfacing = 1的数据,可以看到,决策层都为“yes”,故该子树递归结束!即,如图得到no surfacing的右子树。

对no surfacing = 0的数据,继续递归,得到最佳的分类特征为“flippers”,分类后得到数据如下表:

1.flippers = 0

tail head fish
1 1 maybe

2.flippers = 1

tail head fish
0 1 no
1 0 no

step3:

对flippers = 0的数据,很明显,只有一条数据集,故递归结束,该子树生成叶子节点。

flippers = 1的数据,决策层数据都为“no”,故递归也结束,生成叶子节点。

决策树生成完毕!!!

猜你喜欢

转载自lps-683.iteye.com/blog/2332424