《机器学习实战》第三章决策树 学习笔记

1658883864528

决策树中正方形代表判断模块,椭圆代表终止模块,表示已经得出结论,可终止运行,从判断模块引出的左右箭头称为分支,可达到另一个判断模块或终止模块。

3.1决策树的构造

决策树

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

构造决策树时,首先需要通过评估每个特征,找到数据集上哪个特征在划分数据分类时起决定性作用。完成评估后,原始数据将被划分为几个数据子集,这些子集将分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数

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

1658884978806

决策树的一般流程

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

每次划分数据集只选择一个特征属性。


例子,海洋生物判断,标签为鱼类/非鱼类

1658885363160

3.1.1信息增益

划分数据集的大原则是:将无序的数据变得有效。其中使用的重要方式是信息论,在划分数据前使用信息论度量信息的内容。

划分数据集之前之后信息发生的变化成为信息增益,可以计算出每个特征值划分数据集获得的信息增益,获得信息增益最高的特征,该特征在划分数据分类时起决定性作用

集合信息的度量方式称为香农熵或者简称为。熵定义为信息期望值

如果待分类实物可能划分为多个分类,则特征符号xi的信息定义为:

1658886217711

p(xi)是选择该分类的概率(该特征值出现的概率),为了计算熵,需要计算所有类别所有可能值可能包含的信息期望值,其中n是分类的数目。

1658886415907

程序清单3-1 计算给定数据集的香农熵

#fuction1:计算给定数据集的香农熵
def clacShannonEnt(dataSet):#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#计算当前特征值出现的概率
        shannonEnt-=prob*log(prob,2)#以2为底求对数,从而计算香农熵
    return  shannonEnt#返回香农熵

#fuction1.1:创建数据集
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

输出结果如下,熵越高,则混合的数据就越多。得到熵之后,就可按照获取最大信息增益的方法划分数据集。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fbnA5VTt-1659067820736)(C:\Users\Tommy\AppData\Roaming\Typora\typora-user-images\1658888488552.png)]

3.1.2划分数据集

在完成数据集划分时,需要选择香农熵较大的特征。

程序清单3-2 按照给定特征划分数据集

def splitDataSet(dataSet,axis,value):#输入参数分别表示:待划分的数据集、划分数据集的特征(表示第几个特征)、特征的返回值
    retDataSet=[]#声明新list对象
    for featVec in dataSet:#遍历待划分数据集中的每个元素
        if featVec[axis]==value:#当发现元素符合要求
            reducedFeatVec=featVec[:axis]#将符合要求的元素抽取出来,添加到新列表中
            reducedFeatVec.extend(featVec[axis+1:])#将第axis个后面的元素添加到列表中
            retDataSet.append(reducedFeatVec)#extend\append结果不同
    return retDataSet

该函数将符合特征的数据提取出来,即将某些特征向量中符合要求的某个特征后去除后,将整个向量提取出来。如下代码表示如果数据集中每行特征向量的第二个元素符合预定特征(即等于1),将第二个元素去除后提取出符合要求的特征向量。

#划分数据集
myData=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
print(myData)
result=splitDataSet(myData,1,1)
print(result)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmLm5lNE-1659067820737)(C:\Users\Tommy\AppData\Roaming\Typora\typora-user-images\1658891999384.png)]

接下来将遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。

程序清单3-3 选择最好的数据集划分方式

函数能够实现选取特征、划分数据集,计算得出最好的划分数据集的特征。

def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0])-1#numFeature的大小等于列数据向量的长度减去一个标签,即所有特征的数目
    baseEntropy = calcShannonEnt(dataSet)#计算整个数据集的原始香农熵
    bestInfoGain = 0.0#保存最初的无度量值,用于与划分完之后的数据集计算的熵值就行比较
    bestFeature = -1
    for i in range(numFeature):#遍历数据集中的所有特征
        #创建唯一的分类标签列表
        featList = [example[i] for example in dataSet]#使用列表推导创建新的列表,将数据集中每一个特征向量的第i个特征值写入新列表
        uniqueVals = set(featList)#将list列表数据类型变为集合数据类型,集合类型中每个值互不相同,即去除相同元素,使每种不同元素的数目都为1个
        print(uniqueVals)
        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

函数调用的数据要满足一下要求:数据必须是一种由列表元素组成的列表,而且所有列表元素都要有相同的数据长度;数据的最后一列是类别标签。

3.1.3递归构建决策树

上一次划分之后,数据将被向下传递给树分支的下一个节点,在这个节点上,采用递归的原则再次划分数据。

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

1658904294351

程序清单3-4 创建树的函数代码

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]

#fuction4:创建分类树的函数代码
def createTree(dataSet,labels):#输入两个参数:数据即和标签列表,标签列表包蛤蟆数据集中所有特征的标签
    # print(dataSet)
    classList=[example[-1] for example in dataSet]#用新列表储存数据集中所有标签
    # print(classList)
    if classList.count(classList[0])==len(classList):# count()用于统计classList中第一个字符串出现的次数
        return classList[0]#如果所有标签完全相同,则直接返回该类标签
    if len(dataSet[0])==1:#如果数据集每个数据向量(特征+标签)的长度等于1,即使用完所有特征任然不能将数据集划分为只包含唯一类别的分组
        return majorityCnt(classList)#遍历完所有特征时返回出现次数最多的特征
    bestFeat= chooseBestFeatureToSplit(dataSet)#选择最好的数据集划分方式,返回最优选的特征所在的位置
    bestFeatLabel=labels[bestFeat]#选择出最优特征的名称
    myTree={
    
    bestFeatLabel:{
    
    }}#将最优特征存入字典变量中
    del(labels[bestFeat])# del用于list列表操作,删除最优特征的名称
    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

代码运行效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f5IMP3nk-1659067820737)(C:\Users\Tommy\AppData\Roaming\Typora\typora-user-images\1658998865747.png)]

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

3.2.1Matplotlib注解

使用Matplotlib的注解功能绘制树形图。

程序清单3-5 使用文本注解绘制树节点

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.ax1=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()

代码运行效果如下:

1658998908232

3.2.2构建注解树

构建之前,需要获得树有多少层,从而可以确定y轴的高度。

**程序清单3-6 获取叶节点的数目和树的层数 **

def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = 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]

程序可返回预定义的树结构。

程序清单3-7 plotTree函数

def plotMinText(cntrPt,parentPt,txtString):#在父子节点间填充文本信息
    xMid= (parentPt[0]-cntrPt[0])/2.0+cntrPt[0]
    yMid=(parentPt[0]-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.x0ff+(1.0+float(numleafs))/2.0/plotTree.totalW,plotTree.y0ff)
    plotMinText(cntrPt,parentPt,nodeTxt)#标记子节点属性值
    plotNode(firstStr,cntrPt,parentPt,decisionNode)
    secondDict=myTree[firstStr]
    plotTree.y0ff=plotTree.y0ff-1.0/plotTree.totalD
    for key in secondDict.keys():#减少y偏移
        if type(secondDict[key]).__name__=='dict':
               plotTree(secondDict[key],cntrPt,str(key))
        else:
            plotTree.x0ff=plotTree.x0ff+1.0/plotTree.totalW
            plotNode(secondDict[key],(plotTree.x0ff,plotTree.y0ff),cntrPt,leafNode)
            plotMinText((plotTree.x0ff,plotTree.y0ff),cntrPt,str(key))
    plotTree.y0ff=plotTree.y0ff+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.x0ff=-0.5/plotTree.totalW;plotTree.y0ff=1.0;
    plotTree(inTree,(0.5,1.0),'')
    plt.show()

输出结果如下

1659062018796

3.3测试和储存分类器

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

执行数据分类时,需要决策树以及用于构建树的标签向量,通过程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点,最后将测试数据定义为叶子节点所属的类型。

程序清单3-8 使用决策树的分类函数

def classify(inputTree,featLabels,testVec):
    firstStr=inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    featIndex=featLabels.index(firstStr)#使用index方法查找当前列表第一个匹配firstStr变量的元素,将标签字符串转换为索引
    for key in secondDict.keys():#递归遍历整棵树
        if testVec[featIndex] == key:#比较testVec变量中的值与树节点的值
            if type(secondDict[key]).__name__=='dict':#如果到达叶子节点
                classLabel = classify(secondDict[key],featLabels,testVec)#返回当前节点的分类标签
            else:classLabel=secondDict[key]
    return classLabel

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

创建好序列化对象,并在磁盘中保存对象,从而在执行分类时读取出来。

程序清单3-9 使用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 )

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

示例:使用决策树预测隐形眼镜类型
(1)收集数据:提供的文本文件。
(2)准备数据:解析tab键分隔的数据行。
(3)分析数据:快速检查数据,确保正确地解析数据内容,使用createPlot ()函数绘制
最终的树形图。
(4)训练算法:使用3.1节的createTree ()函数。
(5)测试算法:编写测试函数验证决策树可以正确分类给定的数据实例。
(6)使用算法:存储树的数据结构,以便下次使用时无需重新构造树。
fr=open('lenses.txt')
    lenses=[inst.strip().split('\t') for inst in fr.readlines()]
    # print(lenses)
    lensesLabels=['age','prescript','astigmatic','tearRate']
    lensesTree=createTree(lenses,lensesLabels)#划分决策树
    # print(lensesTree)
    createPlot(lensesTree)#绘制决策树

输出结果:

1659065878058

该示例中还存在着过度匹配的问题,通过参见决策树可减少过度匹配问题,如果叶子节点只能增加少许信息,则可以删除该节点,将它并入其他叶子节点中,可通过决策树构造算法CART实现。

3.5 本章小结

开始处理数据集时,需要测量集合中数据的不一致性,也就是香农熵,根据每个特征香农熵的值,选择香农熵最小的特征作为当前一次划分的特征,并将上一次划分选择特征剔除,进行递归选择下一轮划分的最优特征,直到数据集中的所有数据属于同一分类

绘制决策树


输出结果:

<img src="C:\Users\Tommy\AppData\Roaming\Typora\typora-user-images\1659065878058.png" alt="1659065878058" style="zoom:80%;" />

该示例中还存在着过度匹配的问题,通过参见决策树可减少过度匹配问题,如果叶子节点只能增加少许信息,则可以删除该节点,将它并入其他叶子节点中,可通过决策树构造算法CART实现。

## 3.5 本章小结

开始处理数据集时,需要测量集合中数据的不一致性,也就是香农熵,根据每个特征香农熵的值,选择香农熵最小的特征作为当前一次划分的特征,并将上一次划分选择特征剔除,进行递归选择下一轮划分的最优特征,直到数据集中的所有数据属于同一分类





猜你喜欢

转载自blog.csdn.net/weixin_45182459/article/details/126053046