决策树之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_2\frac {8}{17}+ \frac {9}{17}log_2\frac {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_2\frac {3}{6} + \frac{3}{6}log_2\frac {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_2\frac{2}{3}+\frac{1}{3}log_2\frac{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_2\frac{1}{5}+\frac{4}{5}log_2\frac{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]西瓜书

posted @ 2019-11-06 22:25  消灭猕猴桃  阅读(210)  评论(0编辑  收藏  举报