读书笔记:机器学习实战【第3章 决策树】

来源:互联网 发布:马良中国软件下载 编辑:程序博客网 时间:2024/06/06 03:08

读书笔记:机器学习实战【第3章 决策树】

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

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

适用数据类型:数值型,标称型。

3.1 决策树的构造

在构造决策树时,要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类的时候起决定作用。为了找到决定性特征,划分出最好的结果,我们必须苹果每个特征,而完成测试之后,原始数据集就被划分为几个数据子集,这些数据子集会分布在第一个决策点的所有分支上。

如果某个分支下的数据属于同一类型,则当前分支所有数据都被划分完成,无需进一步分割数据;否则需要重复划分数据子集的过程,直到所有具有相同类型的数据均在一个数据子集内。

创建分支的伪代码函数createBranch()如下所示:

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

上面的伪代码createBranch是一个递归函数,在倒数第二行直接调用了它自己,后面将要把上面的伪代码转换为Python代码,为此,需要进一步了解算法是如何划分数据集的。

决策树的一般流程:

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

一些决策树算法采用二分法划分数据,这里并不采用这种方法:如果依据某个属性划分数据会产生4个可能的值,我们就把数据划分成4块,并创建4个不同的分支。

本书采用ID3算法划分数据集。

3.1.1 信息增益

哪个属性最适合用于划分数据集呢?为了回答这个问题我们需要知道划分数据集的目的——将无序的数据变得更加有序,比如说,一组全部为1元的硬币和一堆多少面值都有的硬币,显然全为1元硬币的那堆更加有序,而后者则显得更加混乱无序。

为了衡量数据是否有序,这里我们引入信息论进行度量:我们把划分数据集前后信息发生的变化称为信息增益,只要知道如何计算信息增益,我们就可以计算根据每个特征划分数据所带来的信息增益,然后,从中选择带来最高信息增益的特征就是了。

这里我们引入熵的定义:熵被定义为信息的期望值,而信息的含义是这样的,比如对于待分类的事物,如果该事物它可能划分在多个类别中和,则其所包含的信息如下:

l(x i )=log 2 p(x i ) 

其中,p(x i ) 是事物属于x i  类的概率。

为了计算熵,我们需要计算所有类别的所有可能值包含的信息期望值,公式如下:

H= n i=1 p(x i )log 2 p(x i ) 

其中,n是可能分类的总数目,换言之,事物属于不同类别时所拥有的信息量是不同的,而根据不同类别下的信息量与对应概率,计算出来的期望值就是熵,熵衡量的是一组事物的信息量大小。熵越大,事物的信息量越大,也显得越混乱。

换句话说,如果在对数据集依照某个特征划分之后,能够使得数据集的熵显著下降,就说明这一特征相当有成效。

下述程序清单的功能是计算给定数据集的熵:

from math import log def calcShannonEnt(dataSet):  #计算    numEntries = len(dataSet) #数据集长度,即子样本总数    labelCounts = {} #建立分类标签初始空字典    ##以下五行:为所有可能分类创建字典    for featVec in dataSet: #逐行读取数据for循环        currentLabel = featVec[-1] #本行数据类别标签        if currentLabel not in labelCounts.keys():            labelCounts[currentLabel] = 0 ##如果字典中不包含该标签,则向字典引入该标签,标记出现次数为0。        labelCounts[currentLabel] +=1 #本标签的出现次数+1    ##字典创建完成,现在我们获得了一个字典LabelCounts    shannonEnt = 0.0 #初始化香农熵    for key in labelCounts: #字典内的for循环是可以的        prob = float(labelCounts[key])/numEntries ##把频数转化为浮点,然后除以numEntries即总样本数求出本分类的概率        shannonEnt -= prob * log(prob,2) ##加总每个分类xi下的分类pi*log(xi)    return shannonEnt

上述程序清单的代码思路是这样的:首先,计算数据集中实例的总数;然后,创建一个数据字典,它的键值是数据集最后一列的数值(即实例分类标签),如果当前键值不存在,则扩展字典将当前键值加入字典。最终,字典内的每个键值都记录了当前类别出现的次数。根据这个字典计算每种分类出现的概率并计算信息期望值,就计算出了香农熵。

熵越高,混合的数据种类也就越多,相反的,熵值越低说明集合越有序,划分数据的过程就是让数据逐步有序的过程。

下面,建立一个简单的数据集来进行测试:

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

接下来,就可以对这个数据集计算香农熵:

In [4]: myDat,labels=createDataSet()In [5]: myDatOut[5]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]In [6]: calcShannonEnt(myDat)Out[6]: 0.9709505944546686

熵越高,则混合的数据越多,我们可以在数据集中增加更多的分类,观察熵的变化:

In [7]: myDat[0][-1] = 'maybe'In [8]: myDatOut[8]: [[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]In [9]: calcShannonEnt(myDat)Out[9]: 1.3709505944546687

可以看到熵变大了。

3.1.2 划分数据集

为了对数据集进行划分,除了测量信息熵之外,还需要选择合适的特征,为了判断最合适用来划分数据集的特征,我们需要对每个特征的划分结果计算信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。

以下代码是划分数据集的:

def splitDataSet(dataset,axis,value): #按照给定特征及取值划分数据集,dataset为待划分数据集,axis为用于划分数据集的特征的所在列,value为需要返回的特征的值    ##1.创建新的list对象:    retDataSet = [] #初始化空数据集    for featVec in dataSet:        if featVec[axis] == value:            reducedFeatVec = featVec[:axis]             reducedFeatVec.extend(featVec[axis+1:]) #这两行加在一起,目的是获得除了划分特征外的特征向量,之所以要除掉划分特征,是因为该特征后面不会再用到。另外,注意extend和append的区别。            retDataSet.append(reducedFeatVec) ##这样就获得了不含划分特征,且均属于value类的数据集    return retDataSet#这个函数获得的,是根据划分特征axis,从数据集中取出所有该特征的取值为value的块。

Python中列表的传递是不会新建表的,而只是获得视图,因此,对子列表的修改会反映到源列表上。这就是为什么这里要新建一个list的缘故。

关于append和extend的区别,在处理多个列表时,append(list)是将整个目标列表list添加入原列表,extend则是把list中的元素逐个填入列表。

仍然在前面生成的简单样本数据上测试函数splitDataSet():

In [15]: myDat,lables = createDataSet()In [16]: myDatOut[16]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]In [17]: splitDataSet(myDat,0,1) #以第0列作为划分特征,取出所有第0列为1的行:Out[17]: [[1, 'yes'], [1, 'yes'], [0, 'no']]

以上是根据特征及取值获得分割后的数据集,接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet()函数,目标是找到最好的特征划分方式:

def chooseBestFeatureToSplit(dataSet): #找出最优特征并根据该特征划分数据集    numFeatures = len(dataSet[0]) -1 ##单个行的长度,就是总列数,-1后就是可用特征数了,因为减去的是分类标签所在列。    baseEntropy = calcShannonEnt(dataSet) #基准香农熵,计算信息增益要用各特征下的香农熵减去这个    bestInfoGain = 0.0;bestFeature = -1 #信息增益初始化为0,最优特征初始化为-1    for i in range(numFeatrues):        #创建唯一的分类标签列表:        featList = [example[i] for example in dataSet] #注意这种写法,其含义是从数据集dataSet中,提取出所有行的I列标签。        uniqueVals = set(featList) #建立不含重复值的feat集合。        newEntropy = 0.0 ##初始化本特征的信息熵        ##计算每种划分方式的信息熵:        for value in uniqueVals:            subDataSet = splitDataSet(dataSet,i,value) #对每个value分割数据集            prob = len(subDataSet)/float(len(dataSet)) ##该部分数据集与总数据集样本量之比,是该value的权重            newEntropy += prob * calcShannonEnt(subDataSet) ##prob权重乘以本部分数据集,获得的期望值就是本value的熵。        infoGain = baseEntropy - newEntropy #计算信息增益        if (infoGain > bestinfoGain) : #根据特征i计算出来的信息增益是否高过当前最优信息增益,来判断是否最优            bestinfoGain = infoGain            bestFeature = i    return bestFeature

现在,测试上面的代码的实际输出结果:

In [27]: chooseBestFeatureToSplit(myDat)Out[27]: 0

说明第0组变量是最适合的。

3.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.iteritems(),key=operator.itemgetter(1),reverse=True) ##使用operator操作键值排列字典    ##operator.itemgetter(i):定义一个函数,该函数的含义:获取对象第i个维度的值。    ##sort:排序函数    ##iteritems:获得的是键值对,key:定义函数,因此key所定义的取维度1的函数,实际上取了iteritems键值对中的值(即排在1维的那个)    ##reverse:True为倒序    return sortedClassCount

该函数返回的是投票排序列表。

接下来,就要写创建决策树的函数代码:

def createTree(dataSet,labels):   #这个labels是特征名,和分类标签不是一个意思,不知道为什么要用同一个词    classList = [example[-1] for example in dataSet] #创建分类标签列表    if classList.count(classList[0]) == len(classList): ##IF条件;如果列表列表中第一类的频数等于类别列表的样本总数        return classList[0] ##那么不用继续划分了,直接返回分类标签    if len(dataSet[0]) == 1: ##IF条件:行长度为1,即已经遍历完毕所有特征,只剩下个分类标签列。        return majorityCnt(classList) ##那么不继续划分了,直接返回分类标签    bestFeat = chooseBestFeatureToSplit(dataSet) #最优特征编号    bestFeatLable = labels[bestFeat] #最优特征名    myTree = {bestFeatLable:{}} ##初始化决策树字典,把已经获得的最优特征列加入其中的key,子字典为空。实际上,子字典依然会是决策树字典的形式,大概是这样:                                ## {根节点:{节点1:{...},节点2:{...}}    del(labels[bestFeat]) #把已经用于划分的bestFeat特征从labels中删去,获得新的用于下一轮迭代的labels。    featValues = [example[bestFeat] for example in dataSet] ##bestFeat向量,也就是每一行的第bestFeat列的值组成的列表。    uniqueVals = set(featValues) #建立该bestFeat列取值的唯一值集合    for value in uniqueVals:        subLabels = labels[:]         myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)        ##字典的value其实就是下层决策树,下层决策树是新的字典。        ##根据bestFeat(最优特征列),value(特征的每个可能取值)划分出若干个子数据集,每个子数据集都是bestFeatLable下的第value个子树,递归直到返回分类标签。    return myTree

接下来,测试创建决策树的代码:

In [57]: myDat,labels = createDataSet()In [58]: myTree = createTree(myDat,labels)In [59]: myTreeOut[59]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

这样,就获得了一个嵌套字典形式的决策树。

3.2 在Python中使用Matplotlib注解绘制属性图

决策树的主要优点就是直观易于理解,所以如果不能将其直观地显示出来,就无法发挥其优势,本节学习如何编写代码绘制树形图。

3.2.1 Matplotlib注解

Matplotlib提供了一个注解工具annotations,非常有用,它可以再数据图形上添加文本注释。注解常用语解释数据的内容,由于数据上面直接存在文本描述非常丑陋,因此工具内嵌支持带箭头的画线工具。

输入下面的程序代码,使用文本注解绘制树形图:

import matplotlib.pyplot as pltdecisionNode = 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)##以上函数,绘制带箭头的注解##annotate为文本注释,create.Plot看下面、def createPlot():    fig = plt.figure(1,facecolor='white')    fig.clf()    createPlot.ax1 = plt.subplot(111,frameon=False)    plotNode(u'决策节点',(0.5,0.1),(0.1,0.5),decisionNode)    plotNode(u'叶节点',(0.8,0.1),(0.3,0.8),leafNode)    plt.show()

注意,以上的createPlot()尚不完善,后面会继续完善。

测试程序的输出结果:

In [79]: createPlot()

这里写图片描述

3.2.2 构造注解树

为了构造一颗完整的树,我们必须知道有多少个叶节点,以便正确确定x轴的长度;我们还需要知道树有多少层,以便正确确定y轴的高度,这里定义两个新函数getNumLeafs()和getTreeDepth(),来获取节点数目和树的层数:

def getNumLeafs(myTree):    numLeafs = 0 #初始化叶节点数为0    firstStr = myTree.keys()[0] #根节点    secondDict = myTree[firstStr] #根节点的子字典,其实就是根节点下的子树    for key in secondDict.keys():#对每个key,也就是每个子树for循环:        if type(secondDict[key]).__name__=='dict': #监测类别是否是字典            numLeafs += getNumLeafs(secondDict[key]) #因为类别是字典,说明还能继续向下遍历,则继续向本子树key下遍历计算叶节点数        else: numLeafs +=1 ##类别不是字典,说明到头,叶节点数+1    return numLeafsdef 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

进行测试,首先,保存先前生成的树:

myDat,labels = createDataSet()myTree = createTree(myDat,labels)

然后测试:

In [82]: getTreeDepth(myTree)Out[82]: 2In [83]: getNumLeafs(myTree)Out[83]: 3

接下来,把前面的方法组合在一切,绘制一棵完整的树:

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,va='center',ha='center',rotation=30)def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on    numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree    depth = getTreeDepth(myTree)    firstStr = myTree.keys()[0]     #the text label for this node should be this    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':#test to see if the nodes are dictonaires, if not they are leaf nodes               plotTree(secondDict[key],cntrPt,str(key))        #recursion        else:   #it's a leaf node print the leaf node            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#if you do get a dictonary you know it's a tree, and the first element will be another dictdef createPlot(inTree):    fig = plt.figure(1, facecolor='white')    fig.clf()    axprops = dict(xticks=[], yticks=[])    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks    #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses     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()

到此,树的绘制完成,尝试对现有的树来测试该代码:

In [103]: createPlot(myTree)

绘图如下:

这里写图片描述

3.3 测试和存储分类器

构造使用决策树的分类函数:

def classify(inputTree,featLabels,testVec): #构造使用决策树的分类函数    firstStr = inputTree.keys()[0]    secondDict = inputTree[firstStr]    #将标签字符串转化为索引:    featIndex = featLabels.index(firstStr) #.index函数,用于检索字符串中是否含有firstStr,并返回首次出现的起始位置,这里就是从featLabels中找到根结点特征序号    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 [131]: myDat,labels = createDataSet()In [132]: labelsOut[132]: ['no surfacing', 'flippers']In [133]: myTree = createTree(myDat,labels)In [134]: myTreeOut[134]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}In [135]: myDat,labels = createDataSet()In [136]: myTreeOut[136]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}In [137]: labelsOut[137]: ['no surfacing', 'flippers']In [138]: classify(myTree,labels,[1,0])Out[138]: 'no'In [139]: classify(myTree,labels,[1,1])Out[139]: 'yes'

3.3.2 使用算法:决策树的存储

构造决策树是很耗时的任务,而用创建好的决策树解决分类问题,却可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时,就调用已经构造好的决策树,为了解决这个问题,需要使用Python模块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 [145]: storeTree(myTree,'classifierStorage.txt')In [146]: grabTree('classifierStorage.txt')Out[146]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

3.4 示例:使用决策树预测隐形眼镜模型

In [152]: lenses = [inst.strip().split('\t') for inst in fr.readlines()]In [153]: lensesLabels = ['age','prescript','astigmatic','tearRate']In [154]: lensesTree = createTree(lenses,lensesLabels)In [155]: lensesTreeOut[155]: {'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'}}

可以看到,上述代码已经生成了决策树,而为了更为直观,使用createplot()画图如下:

这里写图片描述

阅读全文
0 0
原创粉丝点击