机器学习实战之决策树(三)


正方形代表判断模块(decision block) ,椭圆代表终止模块(terminating block),表示已经得到结论,可以终止运动。

决策树的优势在于数据形式容易理解。
决策树的很多任务都是为了数据中所蕴含的知识信息。
决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,机器学习算法最终将使用这些机器从数据集中创造的规则。

3.1决策树的构造

优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配的问题。

适用数据类型:数值型和标称型。
1.先讨论数学上如何使用信息论划分数据集;
2.编写代码将理论应用到具体的数据集上;
3.编写代码构建决策树;

创建分支的伪代码函数createBranch()
检测数据集中的每个子项是否属于同一分类:

    If so return 类标签 :
    Else
           寻找划分数据集的最好特征
           划分数据集
           创建分支节点
                   for 每个划分的子集
                           调用函数createBranch 并增加返回结果到分支节点中
            return   分子节点

上面的伪代码createBranch是一个递归函数,在倒数第二行直接调用它子集

决策树一般流程

收集数据:可以直接使用任何方法
准备数据:构造算法只适用于标称型数据,因此数值型数据必须离散化。
分析数据:可以使用任何方法,构造完树以后,我们应该检查图形是否符合预期。
训练算法:构造数的数据结构。
测试算法:使用经验树计算错误概率
使用算法:此步骤可以适应于任何监督学习算法,而使用决策树可以更好的理解数据的内在含义。

本次我们使用ID3算法来划分数据集。每次划分数据集的时我们只选取一个特征值。
决策树学习采用的是自顶向下的递归方法,其基本思想是以信息熵为度量构造一棵熵值下降最快的树,到叶子节点处的熵值为零,此时每个叶节点中的实例都属于同一类。

3.1.1 信息增益
划分数据集的大原则是 :将无序的数据变得有序。
组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。
在划分数据集之前之后的信息发生的变化称之为信息增益,知道如何计算信息增益,我们就可以计算每个特征的值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
集合信息的度量方式称之为 香农熵。

熵是信息论中的概念,用来表示集合的无序程度,熵越大表示集合越混乱,反之则表示集合越有序。熵的计算公式为:

E = -P * log2P

熵定义为信息的期望值。那什么是信息呢?如果待分类的事务可能划分在多个分类之中,负荷xi的信息定义为:


其中,p(xi)是选择分类的概率。
为了计算熵,我们需要计算所有类别的



其中n是分类的数目。

接下来我们将使用pythoon计算信息熵,去度量数据集的无序程度,创建名为trees.py的文件。

from math import log

def calcShannonEnt(dataSet):
    #计算数据集中实例的总数
    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.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        #以2为底求对数
        shannonEnt -= prob * log(prob,2)
    return shannonEnt

我们输入 一个数据来测试一下。

In [65]: import trees

In [67]: reload(trees)
Out[67]: <module 'trees' from 'trees.py'>

In [69]: def createDatSet():
    ...: dataSet = [[1,1,'yes'],
    ...: [1,1,'yes'],
    ...: [1,0,'no'],
    ...: [0,1,'no'],
    ...: [0,1,'no']]
    ...: labels = ['no surfacing','flippers']
    ...: return dataSet,labels
    ...:

In [70]: myDat,labels = trees.createDatSet()

In [71]: myDat
Out[71]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [72]: trees.calcShannonEnt(myDat)
Out[72]: 0.9709505944546686

熵越高,混合的数据就越多。
我们可以向数据集中添加更多的分类,以此来观测熵是如何变化的。

In [95]: myDat[0][-1]='maybe'

In [96]: myDat
Out[96]: [[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [97]: trees.calcShannonEnt(myDat)
Out[97]: 1.3709505944546687

3.1.2 划分数据集
分类算法除了要度量数据集的无序程度(信息熵),还需要划分数据集,度量划分数据集的熵。以便于判断当前是否正确划分了数据集。

我们队每个特征划分一次数据集的结果计算一次信息熵,然后去判断按照哪个特征划分数据集是最好的划分方式。

代码 : 按照给定的特征划分数据集

#dataSet:待划分的数据集
#axis划分数据集的特征
#特征的返回值
def splitDataSet(dataSet,axis,value):
    #创建新的list对象
    retDataSet=[]
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

上述代码append和extend方法,区别如下:

In [18]: a = [1,2,3]

In [19]: b = [4,5,6]

In [20]: a.append(b)

In [21]: a
Out[21]: [1, 2, 3, [4, 5, 6]]

In [22]: a = [1,2,3]

In [23]: b = [4,5,6]

In [24]: a.extend(b)

In [25]: a
Out[25]: [1, 2, 3, 4, 5, 6]

测试一下划分数据集的代码:

In [35]: reload(trees)
Out[35]: <module 'trees' from 'trees.py'>

In [36]: myDat,labels = trees.createDatSet()

In [37]: myDat
Out[37]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

In [38]: trees.splitDataSet(myDat,0,1)
Out[38]: [[1, 'yes'], [1, 'yes'], [0, 'no']]

In [39]: trees.splitDataSet(myDat,0,0)
Out[39]: [[1, 'no'], [1, 'no']]

记下来我们会遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。熵计算会得出如何划分数据集是最好的数据组织方式。

def choooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) -1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0
    beatFeature =-1
    for i in range(numFeatures):
        #创建唯一的分类标签列表
        #取dataSet的第i个数据的第i个数据,并写入列表
        featList = [example[i] for example in dataSet]
        #将列表的数据集合在一起,并去重
        uniqueVals = set(featList)
        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) 数据必须是一种由列表元素组成的列表,且所有的列表元素具有相同的数据长度。
(2) 数据最后一列或每个实例的最后一个元素是当前实例的类别标签。

测试代码:

In [179]: reload(trees)

In [179]: Out[179]: <module 'trees' from 'trees.py'>

In [180]: trees.choooseBestFeatureToSplit(myDat)
Out[180]: 0

In [181]: myDat
Out[181]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

根据上述的结果,第0个特征就是最好的用于划分数据集的特征。

3.1.3 递归构建决策树
目前我们已经给出了从数据集构造决策树算法所需要的子功能模块,工作原理如下:
得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被乡下传递到树分支的下一个节点,在这个节点上,可以再次划分数据。因此我们可以使用递归的原理来处理数据。

递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有的实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。

[图片上传中。。。(1)]

在代码最前面,输入

import operator

并输入以下代码

# 得出次数最多的分类名称
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            calssCount[vote] = 0
        classCount[vote] +=1
    sortedClassConnt = sorted(calssCount.iteritems(),key=operator.itemgetter(1),reverse=True)
    return sortedClassConnt[0][0]

函数使用分类名称的列表,然后创创建键值为classList中唯一值得数据字典,字典对象存储了classList中每个类标签出现的频率,利用operator操作键值排序字典,返回出现次数最多的分类名称。

下面给出创建树的代码:

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]
    myTree = {bestFeatLabel:{}}
    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

执行以下命令,测试代码:

In [185]: reload(trees)
Out[185]: <module 'trees' from 'trees.py'>

In [186]: myDat,labels=trees.createDataSet()

In [187]: myTree=trees.createTree(myDat,labels)

In [188]: myTree
Out[188]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

3.2 在python中使用matplotlib注解绘制树形图

给出的字典形式并不容易理解。决策树的优点就是直观易于理解。
于是我们自己绘制树形图。

3.2.1 Matplotlib 注解

由于字典的表示形式不好理解,所以我们使用matplotlib这个库来创建树形图。
Matplotlib 提供了一个工具annotations,它可以在数据图形上添加文本注解。注解同城用于解释数据的内容。


在计算机科学中,图是一种数据结构,用于表示数学上的概念。


接下来我们创建新的treePlotter.py文件

3-5 使用文本注解绘制树节点

#定义文本框和箭头格式
decisionNode = dict(boxstyle = "sawtooth", fc = "0.8")
leafNode =  dict(boxstyle = "round4", fc = "0.8")
arrow_args = dict(arrowstyle = "<-")

#绘制带箭头的注解,createPlot.ax1是一个全局变量
def plotNode(nodeTxt,centerPt,parentPt,nodeType):
    createPlot.ax1.annotate(nodeTxt,xy = parentPt,xycoords = "axes fraction",\
    xytext = centerPt,textcoords = "axes fraction",va = "center",\
    ha = "center",bbox = nodeType ,arrowprops = arrow_args)

#创建新图形并清空绘图区,在绘图区绘制决策节点和叶节点
def createPlot():
    fig = plt.figure(1,facecolor = 'white')
    fig.clf()
    createPlot.ax1 = plt.subplot(111,frameon = False)
    plotNode('decisionNodes',(0.5,0.1),(0.1,0.5),decisionNode)
    plotNode('leafNodes',(0.8,0.1),(0.3,0.8),leafNode)
    plt.show()

测试一下代码:

In [6]: import treePlotter

In [7]: reload(treePlotter)
Out[7]: <module 'treePlotter' from 'treePlotter.py'>

In [8]: treePlotter.createPlot()
plotNode_example.png

3.2.2 构造注解树

构造完整的一棵树,我们还需要知道,如何放置树节点,需要知道有多少个叶节点,便于确定x轴的长度,需要知道树多少层,便于正确确定y轴的高度。

获取叶节点的数目和树的层数

def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        #type()函数,测试节点的数据类型是否为字典
        if type(secondDict[key]).__name__ =='dict':
            numLeafs += getNumLeafs(secondDict[key])
        else:
            numLeafs += 1
    return numLeafs 

#计算遍历过程中的遇到判断节点的个数    
def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__ =='dict':
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:
            thisDepth = 1
        #如果达到子节点,则从递归调用中返回
        if thisDepth > maxDepth: 
            maxDepth = thisDepth
    return maxDepth
    


def retrieveTree(i):
    listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
           {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head':{0: 'no', 1: 'yes'}}, 1: 'no'}}}}]  
    return listOfTrees[i]


测试代码:
In [39]: import treePlotter

In [40]: reload(treePlotter)
Out[40]: <module 'treePlotter' from 'treePlotter.py'>

In [41]: myTree = treePlotter.retrieveTree(1)

In [42]: treePlotter.getTreeDepth(myTree)
Out[42]: 2

In [43]: treePlotter.getNumLeafs(myTree)
Out[43]: 3


将下面的代码添加到treePlotter.py中。
# plotTree函数

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)

'''
全局变量plotTree.tatolW存储树的宽度
全局变量plotTree.tatolD存储树的高度
plotTree.xOff和plotTree.yOff追踪已经绘制的节点位置
'''   
def plotTree(myTree,parentPt,nodeTxt):
    #计算宽与高
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]
    cntrPt = (plotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,plotTree.yOff)
    #标记子节点属性值
    plotMidText(cntrPt,parentPt,nodeTxt)
    plotNode(firstStr,cntrPt,parentPt,decisionNode)
    secondDict = myTree[firstStr]
    #减少y偏移
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
    for key in 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

继续测试代码:

In [5]: import treePlotter

In [6]: reload(treePlotter)
Out[6]: <module 'treePlotter' from 'treePlotter.pyc'>

In [7]: myTree = treePlotter.retrieveTree(0)

In [44]: treePlotter.createPlot(myTree)

![决策树.png](http://upload-images.jianshu.io/upload_images/3668059-738fcb261a2b616c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

In [8]: myTree['no surfacing'][3] = 'maybe'

In [9]: myTree
Out[9]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}, 3: 'maybe'}}

In [10]: treePlotter.createPlot(myTree)
决策树2.png

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

在trees.py中,添加下面的代码

#使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    #将标签字符串转换为索引
    featIndex = featLabels.index(firstStr)
    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  

测试代码

In [11]: import trees

In [12]: reload(trees)
Out[12]: <module 'trees' from 'trees.pyc'>

In [14]: myDat,labels = trees.createDataSet()

In [15]: labels
Out[15]: ['no surfacing', 'flippers']

In [16]: myTree = treePlotter.retrieveTree(0)

In [17]: myTree
Out[17]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

In [18]: trees.classify(myTree,labels,[1,1])
Out[18]: 'yes'

In [19]: trees.classify(myTree,labels,[1,0])
Out[19]: 'no'

将此结果与之前的图做比较,不难发现,结果相符。

3.3.2 使用算法 :决策树的存储
构造决策树是一个很耗时的事情,如果数据集很大,将会非常耗时间。为此,我们调用python模块的pickle序列化对象。序列化对象可以在磁盘上保存文件,并在需要的时候读取出来。

#使用pickle模块存储决策树
def storeTree(inputTree,filename)        :
    import pickle
    fw = open(filename, 'w')
    pickle.dump(inputTree,fw)
    fw.close
    
def grabTree(filename):
    import pickle
    fr = open(filename)
    return pickle.load(fr)

测试代码:

In [22]: reload(trees)
Out[22]: <module 'trees' from 'trees.py'>

In [23]: trees.storeTree(myTree,r'E:\ML\ML_source_code\mlia\Ch03\classifierStorage.txt')

In [24]: trees.grabTree(r'E:\ML\ML_source_code\mlia\Ch03\classifierStorage.txt')
Out[24]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

通过上面的代码,我们可以对数据分类时重新学习一遍。

3.4 示例:使用决策树预测隐形眼镜类型
眼科医生是如何判断患者需要佩戴的镜片类型的。
由于前面已经写好了算法模块:

我们载入本地的数据集之后,可以直接测试代码:

In [5]: import trees

In [6]: reload(trees)
Out[6]: <module 'trees' from 'trees.pyc'>

In [8]: import treePlotters

In [9]: reload(treePlotters)
Out[9]: <module 'treePlotters' from 'treePlotters.pyc'>

In [11]: fr = open(r'E:\ML\ML_source_code\mlia\Ch03\lenses.txt')

In [12]: lenses = [inst.strip().split('\t') for inst in fr.readlines()]

In [13]: lensesLabels = ['age','prescript','astigmatic','tearRate']

In [14]: lensesTree = trees.createTree(lenses,lensesLabels)

In [15]: lensesTree
Out[15]:
{'tearRate': {'normal': {'astigmatic': {'no': {'age': {'pre': 'soft',
'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}},
'young': 'soft'}},
'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses',
'presbyopic': 'no lenses',
'young': 'hard'}},
'myope': 'hard'}}}},
'reduced': 'no lenses'}}

In [17]: treePlotters.createPlot(lensesTree)
lenses.png

最终得到上面这个图,可是这些匹配选项可能太多了,我们将这些问题称之为过度匹配。
为了减少这个问题,我们可以裁剪决策树,去掉一些不必要的子节点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容