首页 > 技术文章 > 4.决策树的探赜索隐

xiaochi 2019-06-04 18:01 原文

  决策树是最早的机器学习算法之一。在1966年提出的CLS学习系统中有了决策树算法的概念,直到1979年才有了ID3算法的原型,1983-1986,ID3算法被进行了总结和简化,正式确立了决策树学习的理论,从机器学习的角度来看,这是决策树算法的起点,1986年,科学家在此基础上进行了改造,引入了节点缓冲区,提出ID4算法,1993年,ID3算法又得到了进一步发展,改进成C4.5算法,成为机器学习的十大算法之一。ID3的另外一根分支是分类回归决策树算法,与C4.5不同的是,分类回归决策树算法主要用于预测,这样决策树理论就完整地覆盖了机器学习中的分类和回归两个领域。本篇主要包括:

  • 决策树的算法思想
  • 信息熵和ID3
  • C4.5算法
  • Scikit-Learn与回归树

4.1 决策树的基本思想

  决策树的思想来源非常朴素,每个人的大脑都有类似if-then这样的逻辑判断,其中If表示条件,then就是选择或者是决策。最早的决策树就是利用这类结构分隔数据。下面从一个实例来讲解最简单的决策树的生成过程。

4.1.1 从一个实例开始 

  假设某家IT公司销售笔记本电脑产品, 为了提高销售收入,公司对各类客户建立了统一的调查表,统计了几个月的销售数据后得到如下的表格:

  老板为了提高销售的效率,希望你通过对表中的潜在客户进行分类,以利于销售人员的工作。这就出现了两个问题:

  1.如何对客户进行分类

  2.如何根据分类的依据,给出对销售人员的指导意见?

问题分析:

  从第一列来看这张表格,表格不大,一共15行,每行表示列去特征值不同值的统计人数。最后一列可以理解为分类标签,去两个值:麦,不买。

  那么对于任意给定特征值的一个客户,算法需要帮助公司将这位客户归类,也就是预测这位客户属于买计算机的那一类,还是输入不买计算机的那一类,并给出判断的依据。

  下面引入CLS(Concept Learning System)算法的思想。为了便于理解,我们先用手工实现上例的决策树。我们将决策树设计为三类节点:根节点,叶子节点和内部节点。如果从一棵空决策树开始,任意选择第一个特征就是根节点;我们按照某种条件进行划分,如果划分到某个子集为空,或子集中的所有样本都已经归为同一个类别标签,那么该子集就是叶节点,否则这些子集就对应于决策树的内部节点;如果是内部节点,就需要选择一个新的类别标签继续对该子集进行划分,直到所有的子集都为叶子节点,即为空或者属于同一类。

  接下来我们按照上述规则进行划分。我们选年龄作为根节点,这个特征值取三个值:老,中,青。我们将所有的样本分为老,中青三个集合,构成决策树的第一层。

  现在我们暂时忽略其它特征,仅关注年龄,将表变为如下形式

  (1)年龄=青,是否购买:不买,买

  (2)年龄=中,是否购买:买

  (3)年龄=老,是否购买:不买,买

  当年龄为中年时,是否购买标签都一致地变为买,此时的中年就称为决策树的叶子节点。当年龄为青年和老年时,是否购买有两个选择,可以继续分解。

  现在,将年龄特征等于青年的选项剪切处理,构成一张新的表格,选择第二个特征---收入,并根据收入排序:

 

  其中,高收入和低收入的特征值只有一个类别标签,将其作为叶子节点。然后继续划分中等收入的下一个特征---学生,就有了下表:

  学生特征只有两个取值,当取否是,对对应的标签为不买,当取是时,对应的标签为买。次数,学生特征就生成了决策树左侧分支的所有节点。

  如下图所示:(其中圆角矩阵为根节点或者内部节点,也就是可以继续划分的节点;椭圆节点是叶子节点,不能再划分,一般叶子节点都指向一个分类标签,即产生一种决策)。

  接下来,继续右侧分支的划分,这里划分我们做一个简单的变化,划分的顺序为信誉->收入->学生->计数,这样整个划分过程就变得简单了。当信誉为良时类别标签仅有一个选项,就是买,那么信誉为良的叶子节点:当信誉取值为优的时候,类别标签仅有一个选项,就是不买,如下图所示::

 

  最终的划分结果如下图:

 

 

   我们把所有买的节点都放在右侧,这样,对于任何用户,当出现从内部向左到叶子节点的路径时,就是不购买的用户。

  从定性的角度对潜在客户做出判断,下面给出定量的判断:

  我们知道,计数特征总数为1024,将途中的路径变除以1024,就得到了每个节点的购买概率。

4.1.2 决策树的算法框架

  1.决策树主函数

  各种决策树的主函数都大同小异,本质上是一个递归函数。这个函数的主要功能是按照某种规则生长出决策树的各个分支节点,并根据终止条件结束算法。一般来说,主函数需要完成以下几个功能:

  2.计算最优特征子函数

  计算最优特征子函数是除了主函数外最重要的函数。每种决策树之所以不同,一般都是因为最优特征选择的标准上有所差异,不同的标准导致不同的决策树,例如ID3的最优特征值选择标准是信息增益,C4.5是信息增益率,CART是节点方差的大小等。后面所讲的理论部分,都是针对特征选择标准而言的。

  在逻辑算法上,一般选择最优特征需要遍历整个数据集,评估每个特征,找到最优的那一个特征返回。

  3.划分数据集函数

   划分数据集函数的主要功能是分割数据集。有的需要删除某个特征轴所在的数据列,返回剩余的数据集;有的干脆将数据集一分为二。虽然实现有所不同,但基本含义都是一致的。

  4.分类器

  所有的机器学习算法都要用于分类或回归预测。决策树的分类器就是通过遍历整个决策树,使测试集数据找到决策树中叶子节点对应的类别标签。这个标签就是返回的结果。

  以上四大部分构成了决策树的基本框架。

4.1.3 信息熵测度

  虽然之前手工实现了上述例子的决策过程,但是将这种实现方法使用编程形式自动计算还存在一些问题。首先,特征集中的数据常常表现为定性字符串数据,称为标称数据,使用这些数据的算法缺乏泛化能力,在实际计算中需要将这些数据量化为数字,也就是离散化。

  例如我们可以将年龄,收入,学生,信誉这些特征值转换为0,1,2,...,n的形式。这样:

  完成了特征离散化,回顾一下前面的手工计算过程,我们可以总结出这样一条规律:数据特征的划分过程是一个将数据集从无序变为有序的过程。这样我们就可以处理特征的划分依据问题,即对于一个由多维特征构成的数据集,如何优选出某个特征作为根节点?进一步扩展这个问题:如何每次都选出特征集中无序度最大的那列特征作为划分节点?

  为了衡量一个事物特征值取值的有(无)序程度,下面我们引入一个重要的概念:信息熵。为了便于理解,我们将这个词拆分为两部分:“信息”和“熵”。

  所谓“熵”就是表示 任何一种能量在空间中分布的均匀程度。分布越均匀,熵就越大。在定义熵之前,香农定义了信息的概念:信息就是对不确定的概念或者认识的消除。

 

 

  上式中的对数一般取2位底,就是平常所说的信息单位bit。

  信息熵是事物不确定性的度量标准,也称为信息的单位或“测度”。在决策树中个,它不仅能用来衡量类别的不确定性,也可以用来度量包含不同特征的数据样本和类别的不确定性。也就是说,如果某个特征列向量的信息熵越大,就说明该向量的不确定性程度越大,即混乱程度越大,这时候就应该优先考虑从该特征向量着手来进行划分。信息熵为决策树的划分提供了最重要的依据和标准。

 4.2 ID3决策树

4.2.1 ID3算法

  有了上面的概念,我们就可以手工实现以下ID3算法的决策树生成过程。

  (1)计算对给定样本所需要的信息熵。

如下图所示:

  类别标签S被分成两类:买或不买。其中S1(买)=640;S2(不买)=384。那么总S=S1+S2=1024。S1的概率P1=640/1024=0.625;S2的概率P2=384/1024=0.375。

根据公式:

(注:以上log以2为底 )

(2) 计算每个特征的信息熵。

  

根据公式:

根据公式:

(总的-部分的)

  按照这样的裸机价产生的决策树如下:

  从图中可以看出使用信息熵生成的决策树要比之前我们手动设计的决策树层数要少。如果数据集的特征很多,那么使用信息熵创建决策树在结构上要明显优于其他方法。

 4.2.2 ID3的实现

  前面我们使用手工计算实现了一棵决策树,接下来,我们将其转换为编码进行实现。先定义一个ID3DTree类来封装算法。

 训练集:dataset.dat 关注公众号d528848 回复dataset.data获取

python代码:

from numpy import *
import math
import copy
import pickle
import treePlotter as tp


class ID3DTree(object):
    def __init__(self):
        self.tree={ }#生成的树
        self.dataSet = []#数据集
        self.labels={}#标签集

    #导入数据
    def loadDataSet(self,path,labels):
        recordlist = []
        fp = open(path,"rb")#读取文件内容
        content = fp.read().decode()
        fp.close()
        rowlist = content.splitlines()#按行转换成一维表
        recordlist = [row.split("\t") for row in rowlist if row.strip()]
        print(recordlist)
        self.dataSet = recordlist
        self.labels = labels

    def train(self):
        labels = copy.deepcopy(self.labels)
        print(labels)
        self.tree = self.buildTree(self.dataSet,labels)

    #构建决策树,创建决策树主程序
    def buildTree(self,dataSet,labels):
        #抽取源数据集的决策标签列
        cateList = [data[-1] for data in dataSet]
        print(cateList)
        #程序终止条件1:如果cateList只有一种决策标签,停止划分,返回这个决决策标签
        if cateList.count(cateList[0]) == len(cateList):
            return cateList[0]
        #程序终止条件2:如果数据集的第一个决策标签只有一个,则
        #返回这个决策标签
        if len(dataSet[0]) == 1:
            return self.maxCate(cateList)
        #算法核心:
        bestFeat = self.getBestFeat(dataSet) #返回数据集的最优特征值
        print("bestFeat:",bestFeat)
        bestFeatLabel = labels[bestFeat]
        print("bestFeatLabel",bestFeatLabel)
        tree = {bestFeatLabel:{}}
        del(labels[bestFeat])
        #再次抽取最优特征轴的列向量
        uniqueVals = set([data[bestFeat] for data in dataSet])#去重
        print("uniqueVals:",uniqueVals)
        for value in uniqueVals:#决策树的递归增长
            subLabels = labels[:] #将删除后的特征类别集建立子类别集
            #按最优特征列和值分隔数据集,即筛选出第bestFeat列值为value的所有元素,返回的是去掉这一列后剩下的数据
            #本例中根据bestFeat划分为买或者是不买这两个类别,作为左右子树,再继续划分
            splitDataset = self.splitDataSet(dataSet,bestFeat,value)
            subTree = self.buildTree(splitDataset,subLabels)
            tree[bestFeatLabel][value] = subTree
        return tree

    #计算出现次数最多的类别标签
    def maxCate(self,catelist):
        items = dict([(catelist.count(i),i) for  i in catelist])
        return items([max(items.keys())])

    #计算最优特征
    def getBestFeat(self,dataSet):
        #计算特征向量维,其中最后一列用于类别标签,因此要减去
        print(dataSet[0])
        numFeatures = len(dataSet[0]) - 1#特征向量维数-1
        baseEntropy = self.computeEntropy(dataSet) #基础熵:源数据的香农熵,即买或者不买计算出来的熵值
        print("baseEntropy:",baseEntropy)
        bestInfoGain = 0.0 #初始化最优的信息增益
        bestFeature = -1 #初始化最优的特征轴
        #外循环:遍历数据集各列,计算最优特征轴
        #i为数据集列索引:取值范围:0-(numFeatures-1)
        #依次遍历每个特征
        for i in range(numFeatures):
            uniqueVals = set([data[i] for data in dataSet])#去重:该列的唯一值集,也就是求类别,比如青年,中年,老年
            print("uniqueVals:",uniqueVals)
            newEntropy = 0.0#初始化该列的香农熵
            #算不同类型中买与不买的比例以及熵
            for value in uniqueVals:#内循环按列和唯一值计算香农熵
                #按选定列i和唯一值分隔数据集,即筛选出第i列值为value的所有元素,返回的是去掉这一列后剩下的数据
                subDataSet = self.splitDataSet(dataSet,i,value)
                #各自在总体样本中所占的比例
                prob = len(subDataSet) / float(len(dataSet))
                print("prob:",prob)
                #概率*对应小类别中买与不买的熵值
                newEntropy += prob * self.computeEntropy(subDataSet)
            infoGain = baseEntropy - newEntropy #计算最大增益
            print("infoGain:",infoGain)
            if(infoGain > bestInfoGain):#如果信息增益>0
                bestInfoGain = infoGain #用当前信息增益代替之前的最优增益
                bestFeature = i #重置最优特征为当前列

        return bestFeature

    #计算信息熵
    def computeEntropy(self,dataSet):#计算香农熵
        datalen = float(len(dataSet))
        print("datalen",datalen)
        cateList = [data[-1] for data in dataSet]#从数据集中得到类别标签
        print("cateList",cateList)
        #得到类别为key,出现次数value的字典
        items = dict([(i,cateList.count(i)) for i in cateList])
        print("items:",items)
        infoEntropy = 0.0 #初始化香农熵
        for key in items:#香农熵 = -plog2(p)
            prob = float(items[key])/datalen
            infoEntropy -= prob*math.log(prob,2)
        return infoEntropy

    #划分数据集,分隔数据集,删除特征轴所在的数据列,返回剩余的数据集
    def splitDataSet(self,dataSet,axis,value):
        rtnList = []
        for featVec in dataSet:
            if featVec[axis] == value:
                rFeatVec = featVec[:axis] #取0-(axis-1)的元素
                rFeatVec.extend(featVec[axis+1:])#将特征轴(列)之后的元素加回
                rtnList.append(rFeatVec)
        return rtnList


dtree = ID3DTree()
#["age","revenue","student","credit"]
dtree.loadDataSet(r"C:\Users\Administrator\Desktop\dataset.dat",["age","revenue","student","credit"])
dtree.train()
print(dtree.tree)
tp.createPlot(dtree.tree)

所用到的画树的类:treePlotter.py:

'''
Created on Oct 14, 2010

@author: Peter Harrington
'''
import matplotlib.pyplot as plt

decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")


def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = list(myTree.keys())[0]
    secondDict = myTree[firstStr]
    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
            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':  # test to see if the nodes are dictonaires, if not they are leaf nodes
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:
            thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth


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 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 = list(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 dict

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
    # 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()


# def createPlot():
#    fig = plt.figure(1, facecolor='white')
#    fig.clf()
#    createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
#    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()

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]

    # createPlot(thisTree)

运行截图:

 4.2.5 持久化决策树

from numpy import *
import math
import copy
import pickle
import treePlotter as tp


class ID3DTree(object):
    def __init__(self):
        self.tree={ }#生成的树
        self.dataSet = []#数据集
        self.labels={}#标签集

    #导入数据
    def loadDataSet(self,path,labels):
        recordlist = []
        fp = open(path,"rb")#读取文件内容
        content = fp.read().decode()
        fp.close()
        rowlist = content.splitlines()#按行转换成一维表
        recordlist = [row.split("\t") for row in rowlist if row.strip()]
        print(recordlist)
        self.dataSet = recordlist
        self.labels = labels

    def train(self):
        labels = copy.deepcopy(self.labels)
        print(labels)
        self.tree = self.buildTree(self.dataSet,labels)

    #构建决策树,创建决策树主程序
    def buildTree(self,dataSet,labels):
        #抽取源数据集的决策标签列
        cateList = [data[-1] for data in dataSet]
        print(cateList)
        #程序终止条件1:如果cateList只有一种决策标签,停止划分,返回这个决决策标签
        if cateList.count(cateList[0]) == len(cateList):
            return cateList[0]
        #程序终止条件2:如果数据集的第一个决策标签只有一个,则
        #返回这个决策标签
        if len(dataSet[0]) == 1:
            return self.maxCate(cateList)
        #算法核心:
        bestFeat = self.getBestFeat(dataSet) #返回数据集的最优特征值
        print("bestFeat:",bestFeat)
        bestFeatLabel = labels[bestFeat]
        print("bestFeatLabel",bestFeatLabel)
        tree = {bestFeatLabel:{}}
        del(labels[bestFeat])
        #再次抽取最优特征轴的列向量
        uniqueVals = set([data[bestFeat] for data in dataSet])#去重
        print("uniqueVals:",uniqueVals)
        for value in uniqueVals:#决策树的递归增长
            subLabels = labels[:] #将删除后的特征类别集建立子类别集
            #按最优特征列和值分隔数据集,即筛选出第bestFeat列值为value的所有元素,返回的是去掉这一列后剩下的数据
            #本例中根据bestFeat划分为买或者是不买这两个类别,作为左右子树,再继续划分
            splitDataset = self.splitDataSet(dataSet,bestFeat,value)
            subTree = self.buildTree(splitDataset,subLabels)
            tree[bestFeatLabel][value] = subTree
        return tree

    #计算出现次数最多的类别标签
    def maxCate(self,catelist):
        items = dict([(catelist.count(i),i) for  i in catelist])
        return items([max(items.keys())])

    #计算最优特征
    def getBestFeat(self,dataSet):
        #计算特征向量维,其中最后一列用于类别标签,因此要减去
        print(dataSet[0])
        numFeatures = len(dataSet[0]) - 1#特征向量维数-1
        baseEntropy = self.computeEntropy(dataSet) #基础熵:源数据的香农熵,即买或者不买计算出来的熵值
        print("baseEntropy:",baseEntropy)
        bestInfoGain = 0.0 #初始化最优的信息增益
        bestFeature = -1 #初始化最优的特征轴
        #外循环:遍历数据集各列,计算最优特征轴
        #i为数据集列索引:取值范围:0-(numFeatures-1)
        #依次遍历每个特征
        for i in range(numFeatures):
            uniqueVals = set([data[i] for data in dataSet])#去重:该列的唯一值集,也就是求类别,比如青年,中年,老年
            print("uniqueVals:",uniqueVals)
            newEntropy = 0.0#初始化该列的香农熵
            #算不同类型中买与不买的比例以及熵
            for value in uniqueVals:#内循环按列和唯一值计算香农熵
                #按选定列i和唯一值分隔数据集,即筛选出第i列值为value的所有元素,返回的是去掉这一列后剩下的数据
                subDataSet = self.splitDataSet(dataSet,i,value)
                #各自在总体样本中所占的比例
                prob = len(subDataSet) / float(len(dataSet))
                print("prob:",prob)
                #概率*对应小类别中买与不买的熵值
                newEntropy += prob * self.computeEntropy(subDataSet)
            infoGain = baseEntropy - newEntropy #计算最大增益
            print("infoGain:",infoGain)
            if(infoGain > bestInfoGain):#如果信息增益>0
                bestInfoGain = infoGain #用当前信息增益代替之前的最优增益
                bestFeature = i #重置最优特征为当前列

        return bestFeature

    #计算信息熵
    def computeEntropy(self,dataSet):#计算香农熵
        datalen = float(len(dataSet))
        print("datalen",datalen)
        cateList = [data[-1] for data in dataSet]#从数据集中得到类别标签
        print("cateList",cateList)
        #得到类别为key,出现次数value的字典
        items = dict([(i,cateList.count(i)) for i in cateList])
        print("items:",items)
        infoEntropy = 0.0 #初始化香农熵
        for key in items:#香农熵 = -plog2(p)
            prob = float(items[key])/datalen
            infoEntropy -= prob*math.log(prob,2)
        return infoEntropy

    #划分数据集,分隔数据集,删除特征轴所在的数据列,返回剩余的数据集
    def splitDataSet(self,dataSet,axis,value):
        rtnList = []
        for featVec in dataSet:
            if featVec[axis] == value:
                rFeatVec = featVec[:axis] #取0-(axis-1)的元素
                rFeatVec.extend(featVec[axis+1:])#将特征轴(列)之后的元素加回
                rtnList.append(rFeatVec)
        return rtnList

    #持久化,存储树到文件
    def storeTree(self,inputTree,filename):
        fw = open(filename,'wb')
        pickle.dump(inputTree,fw)
        fw.close()


    #从文件中获取树
    def grabTree(self,filename):
        fr = open(filename,"rb")
        return pickle.load(fr)

dtree = ID3DTree()
#["age","revenue","student","credit"]
dtree.loadDataSet(r"C:\Users\Administrator\Desktop\dataset.dat",["age","revenue","student","credit"])
dtree.train()
print(dtree.tree)
tp.createPlot(dtree.tree)
#序列化
dtree.storeTree(dtree.tree,r"C:\Users\Administrator\Desktop\data.tree")
mytree = dtree.grabTree(r"C:\Users\Administrator\Desktop\data.tree")
print("****"*30)
print(mytree)

3.2.6 决策树分类

from numpy import *
import math
import copy
import pickle
import treePlotter as tp


class ID3DTree(object):
    def __init__(self):
        self.tree={ }#生成的树
        self.dataSet = []#数据集
        self.labels={}#标签集

    #导入数据
    def loadDataSet(self,path,labels):
        recordlist = []
        fp = open(path,"rb")#读取文件内容
        content = fp.read().decode()
        fp.close()
        rowlist = content.splitlines()#按行转换成一维表
        recordlist = [row.split("\t") for row in rowlist if row.strip()]
        print(recordlist)
        self.dataSet = recordlist
        self.labels = labels

    def train(self):
        labels = copy.deepcopy(self.labels)
        print(labels)
        self.tree = self.buildTree(self.dataSet,labels)

    #构建决策树,创建决策树主程序
    def buildTree(self,dataSet,labels):
        #抽取源数据集的决策标签列
        cateList = [data[-1] for data in dataSet]
        print(cateList)
        #程序终止条件1:如果cateList只有一种决策标签,停止划分,返回这个决决策标签
        if cateList.count(cateList[0]) == len(cateList):
            return cateList[0]
        #程序终止条件2:如果数据集的第一个决策标签只有一个,则
        #返回这个决策标签
        if len(dataSet[0]) == 1:
            return self.maxCate(cateList)
        #算法核心:
        bestFeat = self.getBestFeat(dataSet) #返回数据集的最优特征值
        print("bestFeat:",bestFeat)
        bestFeatLabel = labels[bestFeat]
        print("bestFeatLabel",bestFeatLabel)
        tree = {bestFeatLabel:{}}
        del(labels[bestFeat])
        #再次抽取最优特征轴的列向量
        uniqueVals = set([data[bestFeat] for data in dataSet])#去重
        print("uniqueVals:",uniqueVals)
        for value in uniqueVals:#决策树的递归增长
            subLabels = labels[:] #将删除后的特征类别集建立子类别集
            #按最优特征列和值分隔数据集,即筛选出第bestFeat列值为value的所有元素,返回的是去掉这一列后剩下的数据
            #本例中根据bestFeat划分为买或者是不买这两个类别,作为左右子树,再继续划分
            splitDataset = self.splitDataSet(dataSet,bestFeat,value)
            subTree = self.buildTree(splitDataset,subLabels)
            tree[bestFeatLabel][value] = subTree
        return tree

    #计算出现次数最多的类别标签
    def maxCate(self,catelist):
        items = dict([(catelist.count(i),i) for  i in catelist])
        return items([max(items.keys())])

    #计算最优特征
    def getBestFeat(self,dataSet):
        #计算特征向量维,其中最后一列用于类别标签,因此要减去
        print(dataSet[0])
        numFeatures = len(dataSet[0]) - 1#特征向量维数-1
        baseEntropy = self.computeEntropy(dataSet) #基础熵:源数据的香农熵,即买或者不买计算出来的熵值
        print("baseEntropy:",baseEntropy)
        bestInfoGain = 0.0 #初始化最优的信息增益
        bestFeature = -1 #初始化最优的特征轴
        #外循环:遍历数据集各列,计算最优特征轴
        #i为数据集列索引:取值范围:0-(numFeatures-1)
        #依次遍历每个特征
        for i in range(numFeatures):
            uniqueVals = set([data[i] for data in dataSet])#去重:该列的唯一值集,也就是求类别,比如青年,中年,老年
            print("uniqueVals:",uniqueVals)
            newEntropy = 0.0#初始化该列的香农熵
            #算不同类型中买与不买的比例以及熵
            for value in uniqueVals:#内循环按列和唯一值计算香农熵
                #按选定列i和唯一值分隔数据集,即筛选出第i列值为value的所有元素,返回的是去掉这一列后剩下的数据
                subDataSet = self.splitDataSet(dataSet,i,value)
                #各自在总体样本中所占的比例
                prob = len(subDataSet) / float(len(dataSet))
                print("prob:",prob)
                #概率*对应小类别中买与不买的熵值
                newEntropy += prob * self.computeEntropy(subDataSet)
            infoGain = baseEntropy - newEntropy #计算最大增益
            print("infoGain:",infoGain)
            if(infoGain > bestInfoGain):#如果信息增益>0
                bestInfoGain = infoGain #用当前信息增益代替之前的最优增益
                bestFeature = i #重置最优特征为当前列

        return bestFeature

    #计算信息熵
    def computeEntropy(self,dataSet):#计算香农熵
        datalen = float(len(dataSet))
        print("datalen",datalen)
        cateList = [data[-1] for data in dataSet]#从数据集中得到类别标签
        print("cateList",cateList)
        #得到类别为key,出现次数value的字典
        items = dict([(i,cateList.count(i)) for i in cateList])
        print("items:",items)
        infoEntropy = 0.0 #初始化香农熵
        for key in items:#香农熵 = -plog2(p)
            prob = float(items[key])/datalen
            infoEntropy -= prob*math.log(prob,2)
        return infoEntropy

    #划分数据集,分隔数据集,删除特征轴所在的数据列,返回剩余的数据集
    def splitDataSet(self,dataSet,axis,value):
        rtnList = []
        for featVec in dataSet:
            if featVec[axis] == value:
                rFeatVec = featVec[:axis] #取0-(axis-1)的元素
                rFeatVec.extend(featVec[axis+1:])#将特征轴(列)之后的元素加回
                rtnList.append(rFeatVec)
        return rtnList

    #持久化,存储树到文件
    def storeTree(self,inputTree,filename):
        fw = open(filename,'wb')
        pickle.dump(inputTree,fw)
        fw.close()


    #从文件中获取树
    def grabTree(self,filename):
        fr = open(filename,"rb")
        return pickle.load(fr)

    #决策树分类
    def predict(self,inputTree,featLabels,testVec):#分类器
        print("inputTree:",inputTree)
        root = list(inputTree.keys())[0] #树根节点
        print("root:",root)
        secondDict = inputTree[root] #value-子树结构或分类标签
        print("secondDict:",secondDict)
        featIndex = featLabels.index(root)#根节点在分类标签集中的位置
        print("featIndex:",featIndex)
        #取得对应要分类的特征对应的值
        key = testVec[featIndex]
        #得到对应的特征值的子树
        valueOfFeat = secondDict[key]
        print("valueOfFeat:",valueOfFeat)
        #如果还是树则继续分类
        if isinstance(valueOfFeat,dict):
            #递归分类
            classLabel = self.predict(valueOfFeat,featLabels,testVec)
        else:
            classLabel = valueOfFeat
        return classLabel

dtree = ID3DTree()
labels = ["age","revenue","student","credit"]
vector = ['0','1','0','0']
mytree = dtree.grabTree(r"C:\Users\Administrator\Desktop\data.tree")
print("真实输出:","no -> 决策树输出:",dtree.predict(mytree,labels,vector))

 ID3的缺点:

3.3 C4.5算法

  C4.5算法成功解决了ID3算法遇到的诸多问题,在业界得到了广泛的应用,并发展成为机器学习的十大算法之一。

3.3.1 信息增益率

  C4.5并没有改变ID3的算法逻辑,基本的程序结构仍与ID3相同,但在节点的划分标准上做了改进。C4.5使用的是信息增益率来代替信息增益进行特征的选择,克服了信息增益在选择特征时偏向于特征值个数较多的不足。信息增益率的定义如下:

 C4.5算法python实现:

from numpy import *
import math
import copy
import pickle
import treePlotter as tp


class C45DTree(object):
    def __init__(self):
        self.tree={ }#生成的树
        self.dataSet = []#数据集
        self.labels={}#标签集

    #导入数据
    def loadDataSet(self,path,labels):
        recordlist = []
        fp = open(path,"rb")#读取文件内容
        content = fp.read().decode()
        fp.close()
        rowlist = content.splitlines()#按行转换成一维表
        recordlist = [row.split("\t") for row in rowlist if row.strip()]
        print("recordList:",recordlist)
        self.dataSet = recordlist
        self.labels = labels

    def train(self):
        labels = copy.deepcopy(self.labels)
        print("labels:",labels)
        self.tree = self.buildTree(self.dataSet,labels)

    #构建决策树,创建决策树主程序
    def buildTree(self,dataSet,labels):
        #抽取源数据集的决策标签列
        cateList = [data[-1] for data in dataSet]
        print("cateList:",cateList)
        if(cateList.count(cateList[0]) == len(cateList)):
            return cateList[0]
        if(len(dataSet[0]) == 1):
            return self.maxCate(cateList)
        bestFeat,featValueList = self.getBestFeat(dataSet)
        bestFeatLabel = labels[bestFeat]
        print("bestFeatLabel:",bestFeatLabel)
        tree = {bestFeatLabel:{}}
        del(labels[bestFeat])
        for value in featValueList:
            subLabels = labels[:]
            splitDataset = self.splitDataSet(dataSet,bestFeat,value)
            subTree = self.buildTree(splitDataset,subLabels)
            tree[bestFeatLabel][value] = subTree
        return tree

    #计算出现次数最多的类别标签
    def maxCate(self,catelist):
        items = dict([(catelist.count(i),i) for  i in catelist])
        return items([max(items.keys())])

    #计算信息熵
    def computeEntropy(self,dataSet):#计算香农熵
        datalen = float(len(dataSet))
        print("datalen",datalen)
        cateList = [data[-1] for data in dataSet]#从数据集中得到类别标签
        print("cateList",cateList)
        #得到类别为key,出现次数value的字典
        items = dict([(i,cateList.count(i)) for i in cateList])
        print("items:",items)
        infoEntropy = 0.0 #初始化香农熵
        for key in items:#香农熵 = -plog2(p)
            prob = float(items[key])/datalen
            infoEntropy -= prob*math.log(prob,2)
        return infoEntropy

    #划分数据集,分隔数据集,删除特征轴所在的数据列,返回剩余的数据集
    def splitDataSet(self,dataSet,axis,value):
        rtnList = []
        for featVec in dataSet:
            if featVec[axis] == value:
                rFeatVec = featVec[:axis] #取0-(axis-1)的元素
                rFeatVec.extend(featVec[axis+1:])#将特征轴(列)之后的元素加回
                rtnList.append(rFeatVec)
        return rtnList

    #持久化,存储树到文件
    def storeTree(self,inputTree,filename):
        fw = open(filename,'wb')
        pickle.dump(inputTree,fw)
        fw.close()


    #从文件中获取树
    def grabTree(self,filename):
        fr = open(filename,"rb")
        return pickle.load(fr)

    #决策树分类
    def predict(self,inputTree,featLabels,testVec):#分类器
        print("inputTree:",inputTree)
        root = list(inputTree.keys())[0] #树根节点
        print("root:",root)
        secondDict = inputTree[root] #value-子树结构或分类标签
        print("secondDict:",secondDict)
        featIndex = featLabels.index(root)#根节点在分类标签集中的位置
        print("featIndex:",featIndex)
        #取得对应要分类的特征对应的值
        key = testVec[featIndex]
        #得到对应的特征值的子树
        valueOfFeat = secondDict[key]
        print("valueOfFeat:",valueOfFeat)
        #如果还是树则继续分类
        if isinstance(valueOfFeat,dict):
            #递归分类
            classLabel = self.predict(valueOfFeat,featLabels,testVec)
        else:
            classLabel = valueOfFeat
        return classLabel

    #使用信息增益率划分最优节点的方法
    def getBestFeat(self, dataSet):
        print("dataSet[0]:",dataSet[0])
        Num_Feats = len(dataSet[0][:-1])
        totality = len(dataSet)
        BaseEntropy = self.computeEntropy(dataSet)
        ConditionEntropy = []  # 初始化条件熵
        slpitInfo = []  # for C4.5, calculate gain ratio
        allFeatVList = []
        for f in range(Num_Feats):
            featList = [example[f] for example in dataSet]
            print("featList:",featList)
            [splitI, featureValueList] = self.computeSplitInfo(featList)
            allFeatVList.append(featureValueList)
            slpitInfo.append(splitI)
            resultGain = 0.0
            for value in featureValueList:
                #把第f列等于value的值取出
                subSet = self.splitDataSet(dataSet, f, value)
                appearNum = float(len(subSet))
                subEntropy = self.computeEntropy(subSet)#计算香农熵
                resultGain += (appearNum / totality) * subEntropy
            ConditionEntropy.append(resultGain)  # 总条件熵
        infoGainArray = BaseEntropy * ones(Num_Feats) - array(ConditionEntropy)
        print("array(slpitInfo):",array(slpitInfo))
        infoGainRatio = infoGainArray / array(slpitInfo)  # c4.5, info gain ratio
        bestFeatureIndex = argsort(-infoGainRatio)[0]
        return bestFeatureIndex, allFeatVList[bestFeatureIndex]

    #计算划分信息
    def computeSplitInfo(self, featureVList):
        numEntries = len(featureVList)
        featureVauleSetList = list(set(featureVList))
        print("featureVauleSetList:",featureVauleSetList)
        valueCounts = [featureVList.count(featVec) for featVec in featureVauleSetList]
        # caclulate shannonEnt
        pList = [float(item) / numEntries for item in valueCounts]
        lList = [item * math.log(item, 2) for item in pList]
        splitInfo = -sum(lList)
        return splitInfo, featureVauleSetList

labels = ["age","revenue","student","credit"]
dtree = C45DTree()
dtree.loadDataSet(r"D:\机器学习视频\机器学习算法原理与编程实战_cn\ml_cn_book_code-master\chapter03\dataset.dat",labels)
dtree.train()
print("dtree.tree:",dtree.tree)
tp.createPlot(dtree.tree)
vector = ['0','1','0','0']
print("真实输出:","no -> 决策树输出:",dtree.predict(dtree.tree,labels,vector))

运行截图:

 

  

 

推荐阅读