机器学习实战之决策树

时间:2022-12-20 11:28:13

最近三天,看了机器学习实战这本书的决策树一张,打算根据自己的理解对一章的代码进行说明一下,顺便做一下笔记,方便以后的查找。

关于决策树,我有一篇是单独讲的(在这一篇,为了说明问题,可能会有一些重复的地方),只是没有代码的实现。在这里用代码进行实现(python来自机器学习实践这本书),并对代码进行讲解。

1、首先,我们要知道信息增益的概念(后面要用到),信息增益是经验熵和经验条件熵之差,这里有引出了经验熵和经验条件熵的概念,要想知道这些概念我们还要首先知道什么是熵。

btw:说一下熵这个东西,这个东西是香农提出来的,又叫香农熵,如果不知道香农,这里有一段话是说他的:贝尔实验室和MIT的很多人将香农和爱因斯坦相提并论,而其他人则认为这种对比是不公平的-------对香农是不公平的。有兴趣的可以查查这个人。

第一部分:

下面说正题,什么是熵??

熵定义为信息的期望值。符号xi信息的定义公式为: 

机器学习实战之决策树(公式1)

其中p(xi)是选择分类的概率

熵就是计算所有类别所有可能值包含的信息期望值,公式如下:

机器学习实战之决策树(公式2)

如果说不想看这个公式或者说这个公式很抽象,我们举个例子。

机器学习实战之决策树

我们上表中的信息用函数实现:

#定义简单鉴定的数据集函数
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
我们来计算这个表中的熵:

# -*- coding: cp936 -*-
#计算给定数据集的香农熵,熵定义为信息的期望值,它代表随即变量的不确定性
from math import log
import operator
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 #这样也可以,书上没有错行
        #else:
         #   labelCounts[currentLabel] += 1#书上错了,因为他没有这句话
    shannonEnt = 0.0
    for key in labelCounts:
        #print(key)
        #print(labelCounts[key])
        prob = float(labelCounts[key])/numEntries
        #print(prob)
        shannonEnt -= prob * log(prob,2)#首先计算熵,它的作用是要用它计算每个特征的信息增益
    return shannonEnt  #熵越高混合的数据也越多

将上面两个函数写到一个tree.py文件中。对代码进行执行。得到的结果如下:

>>> import tree
>>> myDat,labels=tree.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> tree.calcShannonEnt(myDat)
0.9709505944546686
>>> 
我们来验证一下他的计算结果对不对,我们根据公式2可知,计算公式为-2/5log2/5-3/5log3/5=0.971和上面结果一样,说明没错。

btw:我们这里用的是最后一列的数据,也就是类别。代码中也可以看出来。这里注意一下


下面我们用一个函数来划分数据集:

这个函数的作用:我们需要计算每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是做好的划分方式。(也就是信息增益大的那个数据集)

上面我们说了:信息增益是经验熵和经验条件熵之差,这个的划分数据集就是为了计算经验条件熵,如果还是不懂,就看代码,看完代码之后看我那篇讲决策树的例子,上面有例子。相结合就看明白了,我就是这么看明白的。

代码如下:

#得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集,
#那么如何划分数据集以及如何度量信息增益
'''
现在这说一个append和extend的区别
>>> a = [1,2,3]
>>> b = [4,5,6]
>>> a.append(b)
>>> a
[1, 2, 3, [4, 5, 6]]
>>> a = [1,2,3]
>>> a.extend(b)
>>> a
[1, 2, 3, 4, 5, 6]
'''
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)
    #print(retDataSet)
    return retDataSet

我们对上面的函数进行测试一下:

>>> tree.splitDataSet(myDat,0,1) #这里的意思是特征0中为1的数据集,也就是可以浮出水面可以生存的海洋生物(看上表)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> tree.splitDataSet(myDat,0,0)#这里的意思是特征0中为0的数据集,也就是不可以浮出水面可以生存的海洋生物(看上表)
[[1, 'no'], [1, 'no']]

下面我们的函数为选择最好的数据集划分方式的特征,也就是信息增益最大的特征。函数代码:

#选取特征,划分数据集,计算得出最好的划分数据集的特征
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1  #剩下的是特征的个数
    baseEntropy = calcShannonEnt(dataSet)#计算数据集的熵,放到baseEntropy中
    bestInfoGain = 0.0;bestFeature = -1
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)
        newEntropy = 0.0
        for value in uniqueVals:#下面是计算每种划分方式的信息熵,特征i个,每个特征value个值
            subDataSet = splitDataSet(dataSet, i ,value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy = prob * calcShannonEnt(subDataSet)
        infoGain = baseEntropy - newEntropy  #计算i个特征的信息熵
        #print(infoGain)
        if(infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

测试如上代码:

>>> myDat,labels=tree.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
<pre name="code" class="python">>>> labels
['no surfacing', 'flippers']
>>> tree.chooseBestFeatureToSplit(myDat)
0.419973094022
0.170950594455
0
>>> 
由上面可以看出,特征0的信息增益是 0.419973094022而特征1的信息增益是 0.170950594455。所以返回的是特征0.所以测试结果正确。


 
#返回出现次数最多的分类名称
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount  = sorted(classCount.iteritems(),\
                             key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]
经过测试,这里返回的是no
 
>>> reload(tree)
<module 'tree' from 'C:\Python27\tree.py'>
>>> tree.majorityCnt(classList)
'no'
它是先排序,将次数多的排到头一个,然后返回。
创建树的代码:
def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]#将最后一行的数据放到classList中
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    if len(dataSet[0]) == 1:#这里为什么是1呢?就是说特征数为1的时候
        return majorityCnt(classList)#就返回这个特征就行了,因为就这一个特征
    bestFeat = chooseBestFeatureToSplit(dataSet)
    print(bestFeat)
    bestFeatLabel = labels[bestFeat]#运行结果'no surfacing'
    myTree = {bestFeatLabel:{}}#运行结果{'no surfacing': {}}
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]#第0个特征值
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet\
                                                  (dataSet,bestFeat,value),subLabels)
        
    return myTree
对函数的测试代码
>>> reload(tree)
<module 'tree' from 'C:\Python27\tree.pyc'>
>>> myDat,labels = tree.createDataSet()
>>> myTree = tree.createTree(myDat,labels)
0.419973094022  #下面输出的是特征的信息增益,如果不想输出,把createTree里面的print(bestFeat)注释掉就OK了
0.170950594455
0
0.918295834054
0
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
这就是我们要的树。
第二部分:画树。到这里,我们就得到了要构造的树。下面我们要说的是:给定一个决策树,我们利用Matplotlib将其画出来。
 
#在这里我们新定义一个文件,叫treePlotter.py来实现这个功能。
#先定义两个函数,来实现获取叶结点的数目<strong>和</strong>树的层数的函数
#得到叶子节点的数目
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        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':#一层只有一个dict
            thisDepth = 1 + getTreeDepth(secondDict[key])
            #print(123)
        else:
            thisDepth = 1
            #print(456)
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth
 
#函数retrieveTree输出预先存储的树信息
#避免每次测试代码时都要从数据中创建树的麻烦
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]
 
下面要实现的就是画树的函数了。下面的代码没有看懂。
<pre name="code" class="python"># -*- coding: cp936 -*-
import matplotlib.pyplot as plt
decisionNode = dict(boxstyle = "sawtooth",fc="0.8")
leafNode = dict(boxstyle="round4",fc="0.8")
arrow_args = dict(arrowstyle="<-")

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.axl = plt.subplot(111,frameon=False)
    #在图中绘制两个代表不同类型的树节点
    plotNode('a decision node',(0.5,0.1),(0.1,0.5),decisionNode)
    plotNode('a leaf node',(0.8,0.1),(0.3,0.8),leafNode)
    plt.show()
'''
#画二叉树,实现和上面代码相似的功能
#1、计算父节点和子节点的中间位置
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 = 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]
    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

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;
    print(plotTree.xOff,plotTree.yOff)
    #plotTree.xOff = 1.0;plotTree.yOff = 1.0;
    plotTree(inTree, (0.5,1.0), '')
    plt.show()

对treePlotter.py中的函数进行测试
>>> import treePlotter

>>> 
>>> myTree=treePlotter.retrieveTree(0)
>>> treePlotter.createPlot(myTree)
(-0.16666666666666666, 1.0)
机器学习实战之决策树

第三部分:既然树有了(第一部分得到的,那么我们怎么用他来测试呢?)

定义一个使用决策树的分类函数

#下面代码我们将在真实的数据上使用决策树分类算法,验证它是否可以正确预测出患者应该使用的隐形眼镜类型
#使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)#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 

对上面代码的测试:
>>> import tree
>>> myDat,labels=tree.createDataSet()
>>> labels
['no surfacing', 'flippers']
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> tree.classify(myTree,labels,[1,0])#测试能浮出水面,但是没有脚蹼的生物是不是鱼类,结果是no
'no'
>>> tree.classify(myTree,labels,[1,1])#测试能浮出水面,但是有脚蹼的生物是不是鱼类,结果是yes
'yes'