机器学习实战2(决策树篇)

目录

1、决策树

2、决策树的构造

3、决策树的可视化

4、测试和存储决策树


1、决策树

        你是否玩过二十个问题的游戏,游戏的规则很简单:参与游戏的一方在脑海里想某个事物,其他参与者向他提问题,只允许提20个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围。决策树的工作原理与20个问题类似,用户输人一系列数据,然后给出游戏的答案。

        我们经常使用决策树处理分类问题,近来的调查表明决策树也是最经常使用的数据挖掘算法R。它之所以如此流行,一个很重要的原因就是使用者基本上不用了解机器学习算法,也不用深究它是如何工作的。

        本文构造的决策树算法能够读取数据集合,构建类似于图下的决策树。

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

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

        ③适用数据类型:数值型和标称型。

        ④决策树的一般流程:

                (1)决策树的一般流程

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

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

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

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

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

2、决策树的构造

        在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成测试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类,无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。

        下表的数据包含5个海洋动物,特征包括:不浮出水面是否可以生存,以及是否有脚践。我们可以将这些动物分成两类:鱼类和非鱼类。现在我们想要决定依据第一个特征还是第二个特征划分数据。在回答这个问题之前,我们必须采用量化的方法判断如何划分数据。

         划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。我们可以在划分数据之前使用信息论量化度量信息的内容。

         在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择
       
        在可以评测哪种数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集合信息的度量方式称为香农嫡或者简称为嫡,这个名字来源于信息论之父克劳德·香农。

        为了计算嫡,我们需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到:

                                                H = - \sum_{i=1}^{n}p(x_{i})log(p(x_{i}))

 1、计算给定数据集的熵

        先利用createDataSet()函数得到简单鱼的鉴定数据集。

#创建数据
def createDataSet():
    dataSet = [[1,1,1],[1,1,1],[1,0,0],[0,1,0],[0,1,0]]#最后一列数据1代表为鱼类,0代表非鱼类
    labels = ['no surfacing','flippers']
    return dataSet,labels

         首先,计算数据集中实例的总数。我们也可以在需要时再计算这个值,但是由于代码中多次用到这个值,为了提高代码效率,我们显式地声明一个变量保存实例总数。然后,创建一个数据字典,它的键值是最后一列的数值。如果当前键值不存在,则扩展字典并将当前键值加人字典。每个键值都记录了当前类别出现的次数。最后,使用所有类标签的发生频率计算类别出现的概率。我们将用这个概率计算香农嫡,统计所有类标签发生的次数。

#计算给定数据的香农熵
def calShannonEnt(dataSet):
    numEntries = len(dataSet)#返回数据集的行数
    labelCounts = {}#保存每个标签(Label)出现次数的字典
    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#选择该标签(Label)的概率
        shannonEnt = shannonEnt - prob*log(prob,2)#香农熵
    return shannonEnt

2、划分数据集

        我们学习了如何度量数据集的无序程度,分类算法除了需要测量信息嫡,还需要划分数据集,度量花费数据集的嫡,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息嫡,然后判断按照哪个特征划分数据集是最好的划分方式。

#划分数据集
#我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照那个特征划分数据集是最好的划分方式
def splitDataSet(dataSet,axis,value):
    retDataSet = []#创建返回的数据集列表
    for featVec in dataSet:
        if featVec[axis] ==value:
            reduceFeatVec = featVec[:axis] #去掉axis特征
            reduceFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
            retDataSet.append(reduceFeatVec)
    return retDataSet#返回划分后的数据集

        我们输出检查一下:

dataSet,labels = createDataSet()#创建数据
ans1 = splitDataSet(dataSet,0,1)
print(ans1)

        可以得到输出的结果为:

         在开始划分数据集之前,我们需要计算整个数据集的原始香农嫡,我们保存最初的无序度量值,用于与划分完之后的数据集计算的嫡值进行比较。第1个for循环遍历数据集中的所有特征。使用列表推导(List Comprehension)来创建新的列表,将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中。然后使用Python语言原生的集合(set)数据类型。集合数据类型与列表类型相似,不同之处仅在于集合类型中的每个值互不相同。从列表中创建集合是Python语言得到列表中唯一元素值的最快方法。

        遍历当前特征中的所有唯一属性值,对每个特征划分一次数据集,然后计算数据集的新嫡值,并对所有唯一特征值得到的嫡求和。信息增益是嫡的减少或者是数据无序度的减少,大家肯定对于将嫡用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。

#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0])-1#特征数量,-1是因为最后一个是类别
    baseEntopy = calShannonEnt(dataSet)#计算数据集的香农熵
    bestInfoGain = 0.0#信息增益
    bestFeature = -1 #最优特征的索引值
    for i in range(numFeature):
         #获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList) #创建set集合{},元素不可重复
        newEntropy = 0.0
        #计算每种划分方式的信息熵
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet,i,value)#subDataSet划分后的子集
            prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
            newEntropy += prob*calShannonEnt(subDataSet)
        InfoGain = baseEntopy -newEntropy #信息增益
        if (bestInfoGain<InfoGain):
            bestInfoGain = InfoGain
            bestFeature = i
    return bestFeature

        输出检查一下:

dataSet,labels = createDataSet()#创建数据
# ans1 = splitDataSet(dataSet,0,1)
bestFeature = chooseBestFeatureToSplit(dataSet)
print(bestFeature)

        得到的输出结果为:

         代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。结果是否正确呢?这个结果又有什么实际意义呢?数据集中的数据来源于表3-1,如果我们按照第一个特征属性划分数据,也就是说第一个特征是1的放在一个组,第一个特征是0的放在另一个组,数据一致性如何?按照上述的方法划分数据集,第一个特征为1的海洋生物分组将有两个属于鱼类,一个属于非鱼类;另一个分组则全部属于非鱼类。如果按照第二个特征分组,结果又是怎么样呢?第一个海洋动物分组将有两个属于鱼类,两个属于非鱼类;另一个分组则只有一个非鱼类。

3、递归构建决策树

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

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

        第一个结束条件使得算法可以终止,我们甚至可以设置算法可以划分的最大分组数目。

#出现次数最多的分类名称
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): #如果类别完全相同则停止继续划分
        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 valus in uniqueVals:   #遍历特征,创建决策树。
        subLabels = labels[:]   #复制类标签
        myTree[bestFeatLabel][valus] = createTree(splitDataSet(dataSet,bestFeat,valus),subLabels)
    return myTree
dataSet,labels = createDataSet()#创建数据
myTree = createTree(dataSet,labels)
print(myTree)

        构造的树为:

         至此,我们的决策树已经构造完成了,接下来讲的是怎么样将决策树进行可视化处理。

3、决策树的可视化

        本节我们将使用Matplotlib库创建树形图。决策树的主要优点就是直观易于理解,如果不能将其直观地显示出来,就无法发挥其优势。Python并没有提供绘制树的工具,因此我们必须自己绘制树形图。可视化这里我就不做过多的讲解了,大家可以直接看代码部分。

import matplotlib.pyplot as plt
import operator

#定义文本框和箭头格式
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(inTree):
    fig = plt.figure(1,facecolor='white')
    fig.clf()

    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks
    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()



#获取叶节点的数目
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = list(myTree.keys())[0]   #python3中myTree.keys()返回的是dict_keys,不是list,所以不能使用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 = list(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]


#在父子节点之间填充文本信息
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,nodeTree):
    #计算宽与高
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)
    firstStr = list(myTree.keys())[0]
    cntrPt = (plotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,plotTree.yOff)
    plotMidText(cntrPt,parentPt,nodeTree)
    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



Tree = retrieveTree(0)
createPlot(Tree)

        我们建造出来的树形图像为:

 

4、测试和存储决策树

        依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。

#分类函数
def classify(inputTree,featLabel,TestVec):
    firstStr = next(iter(inputTree))
    secondDict = inputTree[firstStr]
    featIndex = featLabel.index(firstStr)
    for key in secondDict.keys():
        if TestVec[featIndex] == key:
            if type(secondDict[key]).__name__=='dict':
                classLabel = classify(secondDict[key],featLabel,TestVec)

            else :
                classLabel = secondDict[key]

    return classLabel

测试代码:

dataSet,labels = createDataSet()#创建数据
T = labels[:] #复制labels
myTree = createTree(dataSet,labels)
ans = classify(myTree,T,[1,0])
print(ans)

输出为:

        构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。为了解决这个问题,需要使用Python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典对象也不例外。

#决策树的存储
def storeTree(inputTree,filename):
    import pickle
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()


def grabTree(filename):
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)

 

完整代码:

import numpy as np
from math import log
import operator


#创建数据
def createDataSet():
    dataSet = [[1,1,1],[1,1,1],[1,0,0],[0,1,0],[0,1,0]]#最后一列数据1代表为鱼类,0代表非鱼类
    labels = ['no surfacing','flippers']
    return dataSet,labels


#计算给定数据的香农熵
def calShannonEnt(dataSet):
    numEntries = len(dataSet)#返回数据集的行数
    labelCounts = {}#保存每个标签(Label)出现次数的字典
    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#选择该标签(Label)的概率
        shannonEnt = shannonEnt - prob*log(prob,2)#香农熵
    return shannonEnt


#划分数据集
#我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照那个特征划分数据集是最好的划分方式
def splitDataSet(dataSet,axis,value):
    retDataSet = []#创建返回的数据集列表
    for featVec in dataSet:
        if featVec[axis] ==value:
            reduceFeatVec = featVec[:axis] #去掉axis特征
            reduceFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
            retDataSet.append(reduceFeatVec)
    return retDataSet#返回划分后的数据集


#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0])-1#特征数量,-1是因为最后一个是类别
    baseEntopy = calShannonEnt(dataSet)#计算数据集的香农熵
    bestInfoGain = 0.0#信息增益
    bestFeature = -1 #最优特征的索引值
    for i in range(numFeature):
         #获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList) #创建set集合{},元素不可重复
        newEntropy = 0.0
        #计算每种划分方式的信息熵
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet,i,value)#subDataSet划分后的子集
            prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
            newEntropy += prob*calShannonEnt(subDataSet)
        InfoGain = baseEntopy -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): #如果类别完全相同则停止继续划分
        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 valus in uniqueVals:   #遍历特征,创建决策树。
        subLabels = labels[:]   #复制类标签
        myTree[bestFeatLabel][valus] = createTree(splitDataSet(dataSet,bestFeat,valus),subLabels)
    return myTree

#分类函数
def classify(inputTree,featLabel,TestVec):
    firstStr = next(iter(inputTree))
    secondDict = inputTree[firstStr]
    featIndex = featLabel.index(firstStr)
    for key in secondDict.keys():
        if TestVec[featIndex] == key:
            if type(secondDict[key]).__name__=='dict':
                classLabel = classify(secondDict[key],featLabel,TestVec)

            else :
                classLabel = secondDict[key]

    return classLabel


#决策树的存储
def storeTree(inputTree,filename):
    import pickle
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()


def grabTree(filename):
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)




dataSet,labels = createDataSet()#创建数据
T = labels[:] #复制labels
myTree = createTree(dataSet,labels)
ans = classify(myTree,T,[1,0])
print(ans)
storeTree(myTree,'store.txt')
AnsTree = grabTree('store.txt')

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值