zoukankan      html  css  js  c++  java
  • 决策树之ID3算法

    预备知识

    信息熵(information entropy)

    一句话概括:信息熵用来度量信息的不确定性,或者说用来度量样本集合的纯度,信息熵越小则信息的不确定性越低,样本集合纯度越高。生成决策树的过程就是不确定度降低的过程,或者说就是样本纯度提高的过程。
    公式:假设样本集合为D,样本集合中有k类样本( k = 1 , 2 , . . . ∣ y ∣ k = 1,2,...|y| k=1,2,...y), p k p_k pk代表第k类样本在样本集合D中的比例,则信息熵定义为:
    E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k ( 1 ) Ent(D) = -sum_{k=1}^{|y|}p_klog_2p_k qquad(1) Ent(D)=k=1ypklog2pk(1)


    条件熵

    一句话概括:已知某个属性a,其有V个可能取值{ a 1 , a 2 , . . . , a V a^1,a^2,...,a^V a1,a2,...,aV},每个取值 a ∗ a^* a将样本集合D划分为V个子集{ D 1 , D 2 , . . . , D V D^1,D^2,...,D^V D1,D2,...,DV},分别对这V个子集求信息熵并赋予权重 ∣ D v ∣ ∣ D ∣ frac {|D^v|}{|D|} DDv求和,权重越大即样本数越多,代表分支节点的影响越大。条件熵定义如下:
    E n t ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) ( 2 ) Ent(D,a)=sum_{v=1}^{V}frac {|D^v|}{|D|}Ent(D^v)qquad(2) Ent(D,a)=v=1VDDvEnt(Dv)(2)


    信息增益(information gain)

    一句话概括:信息增益越大,则按某属性a对样本集合D划分后,子集(子节点)较父集的样本纯度提升的越高,或者说不确定性降低的越多。其定义为:样本集合D的信息熵减去按某属性a划分后的条件熵。
    G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) ( 3 ) Gain(D,a) = Ent(D)-sum_{v=1}^{V}frac {|D^v|}{|D|}Ent(D^v)qquad(3) Gain(D,a)=Ent(D)v=1VDDvEnt(Dv)(3)


    递归返回条件

    • 节点中包含的样本全部属于同一个类别C,则无需划分

    • 当属性集为空,或 所有样本在所有属性上取值一样则无法划分

    • 当前节点的样本集合为空,则不能划分


    ID3算法

    ID3算法就是以信息增益来选择划分属性。举一个例子,下面是一个西瓜数据集,用来学习一棵是不是好瓜的决策树,显然 ∣ y ∣ = 2 |y| = 2 y=2 C 1 = 是 , C 2 = 否 C_1=是,C_2=否 C1=,C2=

    编号色泽根蒂敲声纹理脐部触感好瓜
    1青绿蜷缩浊响清晰凹陷硬滑
    2乌黑蜷缩沉闷清晰凹陷硬滑
    3乌黑蜷缩浊响清晰凹陷硬滑
    4青绿蜷缩沉闷清晰凹陷硬滑
    5浅白蜷缩浊响清晰凹陷硬滑
    6青绿稍蜷浊响清晰稍凹软粘
    7乌黑稍蜷浊响稍糊稍凹软粘
    8乌黑稍蜷浊响清晰稍凹硬滑
    9乌黑稍蜷沉闷稍糊稍凹硬滑
    10青绿硬挺清脆清晰平坦软粘
    11浅白硬挺清脆模糊平坦硬滑
    12浅白蜷缩浊响模糊平坦软粘
    13青绿稍蜷浊响稍糊凹陷硬滑
    14浅白稍蜷沉闷稍糊凹陷硬滑
    15乌黑稍蜷浊响清晰稍凹软粘
    16浅白蜷缩浊响模糊平坦硬滑
    17青绿蜷缩沉闷稍糊稍凹硬滑
    • 首先确定以哪个属性作为根节点
      step1:
      根节点包含样本集合D中的所有样例,其中正例 p 1 p_1 p1 8 17 frac {8}{17} 178,反例 p 2 p_2 p2 9 17 frac {9}{17} 179,代入公式(1)计算信息熵:
      E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k = − ( 8 17 l o g 2 8 17 + 9 17 l o g 2 9 17 ) = 0.998 Ent(D) = -sum_{k=1}^{|y|}p_klog_2p_k = -(frac {8}{17}log_2frac {8}{17}+ frac {9}{17}log_2frac {9}{17}) = 0.998 Ent(D)=k=1ypklog2pk=(178log2178+179log2179)=0.998
      step2:
      以属性“色泽”为例,它有三个可能的取值{青绿,乌黑,浅白},将样本集合D划分为了三个样本子集{ D 1 , D 2 , D 3 D^1,D^2,D^3 D1,D2,D3},如下表所示,分别计算其信息熵

    D 1 D^1 D1

    编号色泽根蒂敲声纹理脐部触感好瓜
    1青绿蜷缩浊响清晰凹陷硬滑
    4青绿蜷缩沉闷清晰凹陷硬滑
    6青绿稍蜷浊响清晰稍凹软粘
    10青绿硬挺清脆清晰平坦软粘
    13青绿稍蜷浊响稍糊凹陷硬滑
    17青绿蜷缩沉闷稍糊稍凹硬滑

    样本子集 D 1 D^1 D1样本数为6, ∣ y ∣ = 2 |y| = 2 y=2,正例 p 1 p_1 p1 3 6 frac {3}{6} 63,反例 p 2 p_2 p2 3 6 frac {3}{6} 63,代入公式(1):

    E ( D 1 ) = − ( 3 6 l o g 2 3 6 + 3 6 l o g 2 3 6 ) = 1.000 E(D^1) = -(frac{3}{6}log_2frac {3}{6} + frac{3}{6}log_2frac {3}{6}) = 1.000 E(D1)=(63log263+63log263)=1.000


    D 2 D^2 D2

    编号色泽根蒂敲声纹理脐部触感好瓜
    2乌黑蜷缩沉闷清晰凹陷硬滑
    3乌黑蜷缩浊响清晰凹陷硬滑
    7乌黑稍蜷浊响稍糊稍凹软粘
    8乌黑稍蜷浊响清晰稍凹硬滑
    9乌黑稍蜷沉闷稍糊稍凹硬滑
    15乌黑稍蜷浊响清晰稍凹软粘

    样本子集 D 2 D^2 D2的样本数为6, ∣ y ∣ = 2 |y| = 2 y=2,正例 p 1 p_1 p1 2 3 frac {2}{3} 32,反例 p 2 p_2 p2 1 3 frac {1}{3} 31,代入公式(1):
    E n t ( D 2 ) = − ( 2 3 l o g 2 2 3 + 1 3 l o g 2 1 3 ) = 0.918 Ent(D^2) = -(frac{2}{3}log_2frac{2}{3}+frac{1}{3}log_2frac{1}{3}) = 0.918 Ent(D2)=(32log232+31log231)=0.918


    D 3 D^3 D3

    编号色泽根蒂敲声纹理脐部触感好瓜
    5浅白蜷缩浊响清晰凹陷硬滑
    11浅白硬挺清脆模糊平坦硬滑
    12浅白蜷缩浊响模糊平坦软粘
    14浅白稍蜷沉闷稍糊凹陷硬滑
    16浅白蜷缩浊响模糊平坦硬滑

    样本子集 D 3 D^3 D3的样本数为5, ∣ y ∣ = 2 |y| = 2 y=2,正例 p 1 p_1 p1 1 5 frac {1}{5} 51,反例 p 2 p_2 p2 4 5 frac {4}{5} 54,代入公式(1):
    E n t ( D 3 ) = − ( 1 5 l o g 2 1 5 + 4 5 l o g 2 4 5 ) = 0.722 Ent(D^3) = -(frac{1}{5}log_2frac{1}{5}+frac{4}{5}log_2frac{4}{5}) = 0.722 Ent(D3)=(51log251+54log254)=0.722


    step3:
    E n t ( D 1 ) , E n t ( D 2 ) , E n t ( D 3 ) Ent(D^1),Ent(D^2),Ent(D^3) Ent(D1)Ent(D2)Ent(D3)分别赋予权重计算条件熵:
    子 集 D 1 和 D 2 占 样 本 集 合 D 的 6 17 , 子 集 D 3 占 5 17 子集D^1和D^2占样本集合D的frac{6}{17},子集D^3占frac{5}{17} D1D2D176,D3175
    E n t ( D , 色 泽 ) = 6 17 ∗ 1.000 + 6 17 ∗ 0.918 + 5 17 ∗ 0.722 = 0.889 Ent(D,色泽) = frac{6}{17}*1.000 + frac{6}{17} * 0.918 + frac{5}{17} * 0.722 = 0.889 Ent(D,)=1761.000+1760.918+1750.722=0.889
    step4:
    计算通过"色泽"属性对样本集合 D D D划分的信息增益:
    G a i n ( D , 色 泽 ) = E n t ( D ) − E n t ( D , 色 泽 ) = 0.998 − 0.889 = 0.109 Gain(D,色泽) = Ent(D) - Ent(D,色泽) = 0.998 - 0.889 = 0.109 Gain(D,)=Ent(D)Ent(D,)=0.9980.889=0.109
    step5:
    重复以上步骤计算按其余属性划分样本集合 D D D的信息增益:
    G a i n ( D , 根 蒂 ) = 0.143 Gain(D,根蒂) = 0.143 Gain(D,)=0.143 G a i n ( D , 敲 声 ) = 0.141 Gain(D,敲声) = 0.141 Gain(D,)=0.141 G a i n ( D , 纹 理 ) = 0.381 Gain(D,纹理) = 0.381 Gain(D,)=0.381 G a i n ( D , 脐 部 ) = 0.289 Gain(D,脐部) = 0.289 Gain(D,)=0.289 G a i n ( D , 触 感 ) = 0.006 Gain(D,触感) = 0.006 Gain(D,)=0.006
    step6:
    信息增益最大的属性作为划分属性,显然"纹理"属性信息增益最大,所以将"纹理"属性作为根节点。划分结果如图所示。
    在这里插入图片描述

    • 然后对每个分支节点做进一步的划分
      以第一个分支节点为例,样本集合 D 1 D^1 D1中有9个样例,可用属性集合为{色泽,根蒂,敲声,脐部,触感,好瓜},由 D 1 D^1 D1计算各属性增益:

    G a i n ( D 1 , 色 泽 ) = 0.043 Gain(D^1,色泽) = 0.043 Gain(D1,)=0.043 G a i n ( D 1 , 根 蒂 ) = 0.0485 Gain(D^1,根蒂) = 0.0485 Gain(D1,)=0.0485 G a i n ( D 1 , 敲 声 ) = 0.331 Gain(D^1,敲声) = 0.331 Gain(D1,)=0.331 G a i n ( D 1 , 脐 部 ) = 0.458 Gain(D^1,脐部) = 0.458 Gain(D1,)=0.458 G a i n ( D 1 , 触 感 ) = 0.458 Gain(D^1,触感) = 0.458 Gain(D1,)=0.458
    “根蒂”,“脐部”,“触感”三个属性信息增益均最大,可任选其一

    • 对每个分支节点做以上操作最后得到完整的决策树:

    在这里插入图片描述

    代码实现

    # _*_ coding: UTF-8 _*_
    
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    mpl.rcParams['font.sans-serif'] = ['KaiTi']
    mpl.rcParams['font.serif'] = ['KaiTi']
    
    """绘决策树的函数"""
    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':
             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 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):
       lens = len(txtString)
       xMid = (parentPt[0] + cntrPt[0]) / 2.0 - lens * 0.002
       yMid = (parentPt[1] + cntrPt[1]) / 2.0
       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)
       plotMidText(cntrPt, parentPt, nodeTxt)
       plotNode(firstStr, cntrPt, parentPt, decisionNode)
       secondDict = myTree[firstStr]
       plotTree.y0ff = plotTree.y0ff - 1.0 / plotTree.totalD
       for key in secondDict.keys():
          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)
             plotMidText((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()
    
    if __name__=='__main__':
        createPlot()
    
    from math import log
    import operator
    import treePlotter
    
    dataSet = [['青绿'	,'蜷缩'	,'浊响'	,'清晰'	,'凹陷'	,'硬滑'	,'好瓜'],
               ['乌黑'	,'蜷缩'	,'沉闷'	,'清晰'	,'凹陷'	,'硬滑'	,'好瓜'],
               ['乌黑'	,'蜷缩'	,'浊响'	,'清晰'	,'凹陷'	,'硬滑'	,'好瓜'],
               ['青绿'	,'蜷缩'	,'沉闷'	,'清晰'	,'凹陷'	,'硬滑'	,'好瓜'],
               ['浅白'	,'蜷缩'	,'浊响'	,'清晰'	,'凹陷'	,'硬滑'	,'好瓜'],
               ['青绿'	,'稍蜷'	,'浊响'	,'清晰'	,'稍凹'	,'软粘'	,'好瓜'],
               ['乌黑'	,'稍蜷'	,'浊响'	,'稍糊'	,'稍凹'	,'软粘'	,'好瓜'],
               ['乌黑'	,'稍蜷'	,'浊响'	,'清晰'	,'稍凹'	,'硬滑'	,'好瓜'],
               ['乌黑'	,'稍蜷'	,'沉闷'	,'稍糊'	,'稍凹'	,'硬滑'	,'坏瓜'],
               ['青绿'	,'硬挺'	,'清脆'	,'清晰'	,'平坦'	,'软粘'	,'坏瓜'],
               ['浅白'	,'硬挺'	,'清脆'	,'模糊'	,'平坦'	,'硬滑'	,'坏瓜'],
               ['浅白'	,'蜷缩'	,'浊响'	,'模糊'	,'平坦'	,'软粘'	,'坏瓜'],
               ['青绿'	,'稍蜷'	,'浊响'	,'稍糊'	,'凹陷'	,'硬滑'	,'坏瓜'],
               ['浅白'	,'稍蜷'	,'沉闷'	,'稍糊'	,'凹陷'	,'硬滑'	,'坏瓜'],
               ['乌黑'	,'稍蜷'	,'浊响'	,'清晰'	,'稍凹'	,'软粘'	,'坏瓜'],
               ['浅白'	,'蜷缩'	,'浊响'	,'模糊'	,'平坦'	,'硬滑'	,'坏瓜'],
               ['青绿'	,'蜷缩'	,'沉闷'	,'稍糊'	,'稍凹'	,'硬滑'	,'坏瓜']]
    
    A = ['色泽','根蒂','敲声','纹理','脐部','触感']
    
    dataSet1 = [[1, 1, 'yes'],
                   [1, 1, 'yes'],
                   [1, 0, 'no'],
                   [0, 1, 'no'],
                   [0, 1, 'no']]
    labels = ['no surfacing','flippers']
    
    def isEqual(D):# 判断所有样本是否在所有的属性上取值相同
        for i in range(len(D)):#遍历样例
            for j in range(i+1,len(D)):#遍历之后的样例
                for k in range(len(D[i])-1):#遍历属性
                    if D[i][k] != D[j][k]:
                        return False
                    else:
                        continue
        return True
    
    def mostClass(cList):
        classCount={}#计数器
        for className in cList:
            if className not in classCount.keys():
                classCount[className] = 0
            classCount[className] += 1
        sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1), reverse=True)
        print(sortedClassCount[0][0])
        return sortedClassCount[0][0]
    
    def Ent(dataSet):
        sampleNum = len(dataSet)#样例总数
        classCount = {}#类标签计数器
        for sample in dataSet:
            curLabel = sample[-1]#当前的类标签是样例的最后一列
            if curLabel not in classCount.keys():
                classCount[curLabel] = 0
            classCount[curLabel] += 1
        infoEnt = 0.0# 初始化信息熵
        for key in classCount.keys():
            prob = float(classCount[key])/sampleNum
            infoEnt -= prob * log(prob,2)
        return infoEnt
    
    def splitD(dataSet,value,index):
        retDataSet = []
        for sample in dataSet:  # 遍历数据集,并抽取按axis的当前value特征进划分的数据集(不包括axis列的值)
            if sample[index] == value:  #
                reducedFeatVec = sample[:index]
                reducedFeatVec.extend(sample[index + 1:])
                retDataSet.append(reducedFeatVec)
                # print axis,value,reducedFeatVec
        if retDataSet == []:#如果为空集返回当前集合
            return dataSet
        return retDataSet
    
    def chooseBestAttrToSplit(dataSet):
        #属性数量
        attrsNum = len(dataSet[0])-1
        #print(range(attrsNum))
        #计算信息熵
        entD = Ent(dataSet)
        #计算信息增益
        bestInfoGain = 0.0
        bestAttr = -1
        for i in range(attrsNum): #遍历每个样例的当前属性的属性值加入集合i
            attrList = [sample[i] for sample in dataSet]
            attrsValue = set(attrList)
            entDA = 0.0 # 初始化条件熵
            for value in attrsValue:#计算条件熵
                subDataSet = splitD(dataSet,value,i)#按属性值划分子集
                weight = len(subDataSet)/float(len(dataSet))#计算权重
                entDA += weight * Ent(subDataSet)
            infoGain = entD - entDA
            if(infoGain > bestInfoGain):
                bestInfoGain = infoGain
                bestAttr = i
        return bestAttr
            #选取信息增益最大的属性
    
    parent = []
    
    def treeGenerate(D,A):
        CnameList = [sample[-1] for sample in D]#遍历每一个样例,将每个样例的类标签组成一个集合
        if CnameList.count(CnameList[0]) == len(CnameList):#当结点包含的样本全属于同一类别,无需划分,直接返回类标签
            return CnameList[0]
        if len(A) == 0 or isEqual(D):#如果A为空集或者所有样本在所有属性上取值相同,则无法划分,返回所含样本最多的类别
            return mostClass(CnameList)
        #从A中选择最优的划分属性
        bestAttrIndex = chooseBestAttrToSplit(D) #获取最优属性下标
        bestAttrName = A[bestAttrIndex]#获取最优属性名字
        #使用字典存储树信息
        treeDict = {bestAttrName:{}}
        del(A[bestAttrIndex])# 删除已经选取的特征
        attrList = [sample[bestAttrIndex] for sample in D] #获取每个样例最佳划分属性的属性值列表
        attrsValue = set(attrList)
        for value in attrsValue:
            subA = A[:]
            if len(D) == 0:#如果子集D为空集则,返回父集中样本最多的类
                return mostClass(CnameList)
            else:
                treeDict[bestAttrName][value] = treeGenerate(splitD(D,value,bestAttrIndex),subA)
        return treeDict
    
    if __name__ == '__main__':
        tree = treeGenerate(dataSet,A)
        treePlotter.createPlot(tree)
    

    绘图结果

    在这里插入图片描述

    参考博客

    [1]决策树算法及Python实现
    [2]Pandas matplotlib 画图无法显示中文字体的问题
    [3]西瓜书

  • 相关阅读:
    Spring Boot简明教程之实现Web开发及常用参数获取方式分析
    SpringBoot 简明教程之项目属性配置(三):配置文件优先级及多配置切换
    史上最简单MySQL教程详解(进阶篇)之存储过程(二)
    史上最简单MySQL教程详解(进阶篇)之存储过程(一)
    文字从中间向两边延展
    字符串转化成驼峰命名
    统计字符串字符个数
    while求和(1到100)
    for循环实现乘阶
    递归遍历所有ul下的所有子节点
  • 原文地址:https://www.cnblogs.com/PythonFCG/p/13860120.html
Copyright © 2011-2022 走看看