欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《机器学习实战》3.决策树算法分析与源码实现

程序员文章站 2024-02-03 21:17:52
...

结合源码分析第三章中实现的Demo
运行环境:Anaconda——Jupyter Notebook
Python版本为:3.6.2(原书代码实现为2.x 所以在一些代码上略有改动)
阅读本博文你将获取:
1.决策树的基本思想
2.信息增益和熵的概念——本文中使用信息增益作为划分数据集的标准
3.全部的代码实现,且包含了大部分注释,便于初学者者理解
4.在最后的总结部分对决策树的优缺点做了总结
参考资料:Apachecn 专注于优秀项目维护的开源组织


前言

决策树(Decesion Tree)是一种基本的分类算法。它可以认为是if-then规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。其主要优点就是模型具有可读性,分类速度块。学习时,利用训练数据,根据损失函数最小化的原则建立决策树模型。旨在构建一个与训练数据拟合很好,并且复杂度小的决策树。因为从可能的决策树中直接选取最优决策树是NP问题,现实中采用启发方法学习次优的决策树。
决策树学习算法包括3部分:特征选择、树的生成和树的剪枝。常用算法有ID3(本文采用)、C4.5CART算法。
决策树的生成:通常使用信息增益最大信息增益比最大,或基尼指数最小作为特征选择的准则。决策树的生成往往通过计算信息增益或其他指标,从根节点开始,递归地产生决策树。这相当于用信息增益或其他准则不断地选取局部最优 的特征,或将训练集分割为能够基本正确分类的子集。
决策树的剪枝:由于生成的决策树存在过拟合的问题,需要对它进行剪枝(考虑全局最优)。决策树的剪枝,往往从已生成的树上剪掉一些叶结点或叶结点以上的树,并将其父结点或根结点作为新的叶结点。


3.1 决策树的构造

创建分支的伪代码如下:
《机器学习实战》3.决策树算法分析与源码实现
决策树的一般开发流程:
《机器学习实战》3.决策树算法分析与源码实现


3.1.1 信息增益(Information)和熵(Entropy)

严格来说这个概念是信息论里的概念。(笔者之前学过通信原理所以相对熟悉一点)。
首先,明白一点。我们日常生活中会接收到无数的消息,但是只有那些你关心在意(或对你有用)的才叫做信息。(张三告诉我李四今天一只袜子丢了,事实上我也不认识李四,对于我来说这仅仅是一则消息,因为这和我没有太多关系,我也不必去在意)。
另外,想一下如何度量这种信息的大小或多少呢,香农说了,可以使用信息量来度量。在日常生活中,极少发生 的事件一旦发生是容易引起人们关注的(新闻说发生空难了,那必然会引起人们很大的关注,但事实是发生空难的概率很小很小),而 司空见惯的事不会引起注意 ,也就是说,极少见的事件所带来的信息量多。如果用统计学的术语来描述,就是出现概率小的事件信息量多。因此,事件出现得概率越小,信息量愈大。即信息量的多少是与事件发生频繁(即概率大小)成反比


设X是一个取有限个值的离散随机变量,X=xi (i=1,2…,n)。则信息量定义为《机器学习实战》3.决策树算法分析与源码实现
为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值:《机器学习实战》3.决策树算法分析与源码实现<对数以2和e为底时,熵的单位分别叫做比特(bit)和奈特(nat)>

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

from math import log
def calcShanonEnt(dataSet):
    numEntries = len(dataSet) #计算实例总数
    labelCounts = {}
    for featVec in dataSet:
        currentLabel = featVec[-1] #-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) #base = 2 求对数
    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

输入:

myDat,labels = createDataSet()
myDat

输出:

[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

输入:

calcShanonEnt(myDat)

输出:

0.9709505944546686

按照书上内容,在数据集中增加更多的分类,观察熵的变化。
输入:

myDat[0][-1]='maybe'
myDat

输出:

[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

再次计算熵,输入:

calcShanonEnt(myDat)

输出:

1.3709505944546687

发现增加了maybe分类后,熵有了增加。


3.1.2 划分数据集

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

#输入:dataSet 数据集——待划分的数据集
#axis 表示每一行的axis列——划分数据集的特征
#value 表示axis列对应的value值——需要返回的特征的值
#输出:index列为value的数据集 <该数据集不包括axis列>
def splitDataSet(dataSet,axis,value):
    #splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:  #判断axis列的值是否为value
            reducedFeatVec = featVec[:axis]  #取featVec的前axis行
            # start 缺省默认从头开始。当操作对象是一维数据时,就是去前axis个元素
            reducedFeatVec.extend(featVec[axis+1:]) #[axis+1:]表示跳过第axis行,从第axis+1行开始,取接下来的数据
            #当操作对象是一维数据时,就是跳过第axis个元素,从第axis+1个元素开始,取接下来的数据
            retDataSet.append(reducedFeatVec)
    return retDataSet
myDat

输出:

[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

输入;

splitDataSet(myDat,0,1)

输出:

[[1, 'maybe'], [1, 'yes'], [0, 'no']]

输入:

splitDataSet(myDat,0,0)

输出:

[[1, 'no'], [1, 'no']]

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

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

#熵H(Y)与H(Y\X)之差称为互信息。决策树学习中的信息增益等价于训练数据集中类与特征的互信息
#输入:数据集
#输出:返回最优的特征列
def chooseBestFeatureToSplit(dataSet):
    #计算Feature的个数(列数)
    numFeatures = len(dataSet[0]) - 1  #dataSet[0]计算行元素的个数 最后一列是label
    baseEntropy = calcShanonEnt(dataSet)
    bestInfoGain = 0.0
    bestFeature = -1
    for i in range(numFeatures): #在所有Feature中循环
        # 获取每一个实例的第i+1个feature,组成list集合
        featList = [example[i] for example in dataSet] #example[i]代表样本集中,第i+1列所有feature
        uniqueVals = set(featList) #使用set对featList进行去重
        newEntropy = 0.0 #创建一个临时的信息熵
        # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
        for value in uniqueVals: # 遍历某一列的value集合,计算该列的信息熵
            subDataSet = splitDataSet(dataSet,i,value)
            prob = len(subDataSet)/float(len(dataSet)) #根据不同的特征值分类计算所占百分比
            newEntropy += prob * calcShanonEnt(subDataSet)
        infoGain = baseEntropy - newEntropy
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i 
    return bestFeature

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

键入命令:

chooseBestFeatureToSplit(myDat)

输出结果为:

0

代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。


3.1.3 递归构建决策树

在基于最好的属性值划分数据集后(第一次),数据将被向下传递到树分支的下一个节点,在这个节点上,我们再次划分数据。因此我们可以采用递归的原则处理数据集。
递归结束的条件是:
1.每个分支下的所有实例都具有相同的分类。
2.数据集已经处理了所有属性,但是类标签依然不唯一,此时我们需要使用投票的方式决定该叶子节点的分类。
程序清单 3-4:

#实现多数表决
import operator
def majorityCnt(classList):
    classCount = {}
    for vote in classCount:
        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] #这边返回的是yes和no
    #count()函数是统计括号中的值在list中出现的次数
    if classList.count(classList[0]) == len(classList):  
        return classList[0]
    # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    # 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
    # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    if len(dataSet[0]) == 1: #dataSet[0]为第一行所有元素
        return majorityCnt(classList)
    # 选择最优的列,得到最优列对应的label含义
    bestFeat = chooseBestFeatureToSplit(dataSet) #返回最优的特征列序号
    print('bestFeat:',bestFeat)
    bestFeatLabel = labels[bestFeat] # 获取对应的label名称
    print('bestFeatLabel:',bestFeatLabel)
    myTree = {bestFeatLabel:{}}
    del(labels[bestFeat]) #用完了删除该label名称
    featValues = [example[bestFeat] for example in dataSet]
    print('featValues:',featValues)
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:] #求出剩余的标签
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
        print('myTree:',myTree)
    return myTree

键入命令:

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

输出:

{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

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

3.2.1 Matplotlib注解

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

import matplotlib.pyplot as plt
#定义决策树决策结果的属性 sawtooth 波浪方框 fc表示字体颜色的深浅 0.1~0.9 依次变浅
decisionNode = dict(boxstyle = 'sawtooth',fc = '0.8')
#定义决策树的叶子节点的属性 round4 矩形方框 
leafNode = dict(boxstyle ="round4",fc="0.8")
#定义决策树箭头属性 
arrow_args = dict(arrowstyle ="<-")
#nodeTxt为要显示的文本,centerPt为文本的中心点,箭头所在的点,parentPt为指向文本的点
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为全局变量,绘制图像的句柄
    #frameon 是否绘制坐标轴矩形
    createPlot.ax1 = plt.subplot(111,frameon=False) 
    #绘制节点
    plotNode('decisionNode',(0.5,0.1),(0.1,0.5),decisionNode)
    plotNode('leafNode',(0.8,0.1),(0.3,0.8),leafNode)
    plt.show()

键入命令:

createPlot()

输出图形结果:
《机器学习实战》3.决策树算法分析与源码实现


3.2.2构造注解树

上面的结果只是一个绘制树节点的小例子。
如果想要构造完整的树还需要一些小技巧:
1.需要知道有多少个叶节点,以便确定x轴的长度
2.需要知道树的层数,以便确定y轴的高度
程序清单3-6 获取叶节点的数目和树的层数:

# 获取决策树的叶子节点
def getNumLeafs(myTree): #变量myTree包含了很多代表树结构信息的嵌套字典
    numLeafs = 0 # 初始化叶子节点数目
    #获取myTree的第一个key,即第一个特征,分割的标签
    firstStr = myTree.keys()[0]
    #根据key得到相应的value,即根据第一个特征分类的结果
    secondDict = myTree[firstStr]
    #遍历得到的secondDict
    for key in secondDict.keys(): #其实上面得到的value是(嵌套的)dict
        #注意type()和isinstance()的区别
        #如果secondDict[key]为一个字典
        if type(secondDict[key])._name_ == 'dict':
            #则递归计算secondDict中的叶子节点数
            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():
        #如果secondDict[key]为一个dict类型<非叶子节点>
        if type(secondDict[key])._name_ == 'dict':
            #则当前树的深度等于1加上secondDict的深度
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:
            #则将当前树的深度置为1
            thisDepth = 1
        #返回max{当前树的深度,最大树的深度}
        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]

输入如下代码:

myTree = retrieveTree(0)
print('NumLeafs:',getNumLeafs(myTree))
print('TreeDepth:',getTreeDepth(myTree))

输出结果应为:

NumLeafs: 3
TreeDepth: 2

现在我们将前面学习到的方法组合在一起,绘制一棵完整的树:

createPlot(myTree)

输出结果为:
《机器学习实战》3.决策树算法分析与源码实现
程序清单3-7: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,va='center',ha='center',rotation=30)
#绘制决策树
def plotTree(myTree,parentPt,nodeTxt):
    #获得决策树的叶子节点数与深度
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)
    #firstStr = myTree.keys()[0]
    firstSides = list(myTree.keys())
    firstStr = firstSides[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():
        #如果secondDict[key]是一颗子决策树,即字典
        if type(secondDict[key]) is 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
    plotTree(inTree,(0.5,1.0),'')
    plt.show()

树这里就不过多解释了。自己看以看一下书上的说明。上述程序是对的,可以绘出如下图形:
《机器学习实战》3.决策树算法分析与源码实现

3.3 测试和存储分类器

到目前为止,本章学习的主要内容是如何从原始数据中创建决策树,并使用Python函数库绘制树形图,下面我们将要学习如何利用决策树执行数据分类上

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

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。
程序清单 3-8 使用决策树的分类函数

#参数:inputTree--决策树模型 
#      featLabels--Feature标签对应的名称
#   testVec--测试输入的数据
#返回结果 classLabel分类的结果值(需要映射label才能知道名称)
def classify(inputTree,featLabels,testVec):
    firstsides = list(inputTree.keys())
    firStr = firstsides[0] #获取根节点的key
    secondDict = inputTree[firStr] #得到根节点对应的value(具体是嵌套着的一个Dict类型)
    #判断根节点名称,获取根节点在label中的向后顺序,这样就知道输入的testVec怎么开始对照树来做分类
    featIndex = featLabels.index(firStr)
    #测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    #判断valueOfFeat是不是字典类型,也就是判断是不是还有子集,有在进行递归判断
    if isinstance(valueOfFeat,dict):
        classLabel = classify(secondDict[key],featLabels,testVec)
    else:
        classLabel = valueOfFeat
    return classLabel

键入:

myTree = retrieveTree(0)
classify(myTree,labels,[1,0])

输出结果:

'no'

以前绘制的树形图和此处代表的数据结构完全相同。

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

由于构造决策树是一个十分耗时的任务。因此我们可以用创建好的决策树解决分类问题。此处,我们使用python中pickle序列化对象,保存相应的决策树模型。
程序清单3-9 使用pickle模块存储决策树

#使用pickle模块存储决策树
def storeTree(inputTree,filename):
    import pickle
    #创建一个可以'写'的文本文件
    #这里,如果按树中写的'w',将会报错write() argument must be str,not bytes
    #所以这里改为二进制写入'wb'
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw) #将inputTree保存到fw中
    fw.close()

def grabTree(filename):
    import pickle
    #对应于二进制方式写入数据,'rb'采用二进制形式读出数据
    fr = open(filename,'rb')
    return pickle.load(fr) #读取

输入下列命令验证上述代码效果:

storeTree(myTree,'classifierStorage.txt')
grabTree('classifierStorage.txt')

输出结果为:
《机器学习实战》3.决策树算法分析与源码实现


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

依次输入:

fr=open(r'C:\Users\Rujin_shi\Apache_cn\input\3.DecisionTree\lenses.txt')
#将文本数据的每一个数据行按照tab键分割,并依次存入lenses
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lenseLabels = ['age','prescript','astigmatic','tearRate']
#创建并存入特征标签列表
lenseTree=createTree(lenses,lenseLabels)
lenseTree

可视化:

#可视化
createPlot(lenseTree)

《机器学习实战》3.决策树算法分析与源码实现

总结

本次实验所采取的算法称为ID3,虽然直观但是不是十分完美。ID3算法无法直接处理数值型数据,尽管我们可以通过量化的方法将数值型数据转化为标称型数据,但是如果存在太多的特征划分,ID3算法仍然会面临其他问题。
此外,我们还有的划分标准:信息增益比(C4.5)、基尼指数(CART)这些会在后续章节里用到。

对于决策树而言优点有
1.基于if-then的结构,理解简单;
2.计算复杂度不高,每一次预测的最大计算次数不会超过决策树的深度;
3.可以处理不相关特征数据
4.能够处理多输出问题
5.对中间缺失值不敏感
缺点:
1.容易生成较复杂的树,即过拟合问题(需要进行剪枝处理)
2.不适合处理高维数据,当属性数量过大的时候,部分决策树就不再适用
3.对异常值过于敏感。