机器学习——创建决策树

目录

一、决策树介绍

1.1什么是决策树

1.2决策树实例

1.3决策树的优缺点

二、划分选择

2.1信息增益

2.2增益率

2.3基尼指数

三、代码实现 

3.1以信息增益划分属性/ID3

数据加载

计算给定数据的香农熵 

根据某一特征划分数据集 

选择最佳属性划分数据集 

创建并递归遍历该树 

添加主函数

 运行结果

小结 

3.2以信息增益率划分属性/C4.5

运行结果

​编辑 

小结 

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

运行结果 

 


 

一、决策树介绍

1.1什么是决策树

        决策树(decision tree)是一种基本的分类与回归方法。决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是if-then规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。

        分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部结点和叶结点。内部结点表示一个特征或属性,叶结点表示一个类。

1.2决策树实例

        

根据以上对话我们可以画出一棵决策树:

决策过程中提出的每个判定问题都是对某个属性的“测试”

例如年龄=?、长相=?、收入=?、是否是公务员

每个测试的结果或是导出最终结论,或者导出进一步的判定问题,其考察范围是在上次决策结果的限定范围之内。

例如 年龄<=30,之后再判断长相,则仅考虑年龄小于30的对象

从根结点到每个叶结点的路径对应了一个判定测试序列

例如 年龄小于30,长相帅,收入高的路径,最终判定结果为

决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树。

1.3决策树的优缺点

优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。

缺点:可能会产生过度匹配问题。

适用数据类型:数值型和标称型。

二、划分选择

决策树学习的关键是如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“纯度”(puruty)越来越高

2.1信息增益

 “信息熵”是度量样本集合纯度最常用的一种指标,假定当前样本集合D中第k类样本所占的比例为p{_{k}}^{}(K=1,2,……,|y|),则D的信息熵定义为

Ent(D) = -\sum_{k=1}^{|y|}p_{k}^{}log{_{2}}^{}p_{k}^{}

 Ent(D)的值越小,则D的纯度越高

计算信息熵时约定:若p=0,则plog2p=0

Ent(D)的最小值为0,最大值为log2|y|

离散属性a有V个可能的取值{a^1 , a^2, ..., a^v },用a来进行划分,则会产生V个分支结点,其中第v个分支结点包含了D中所有在属性a上取值为 a^v的样本,记为D^v 。则可计算出用属性a对样本集D进行划分所获得的信息增益

Gain(D,a) = Ent(D) - \sum_{v=1}^{V}\frac{|D^{v}|}{|D|}Ent(D^{v})

再考虑到不同的分支结点所包含的样本数不同,给分支结点赋予权重,样本数越多的分支结点的影响越大

一般而言,信息增益越大,则意味着使用属性a来进行划分所获得的“纯度提升”越大 

说了这么多可能还是有点难理解信息增益是什么,怎么用,下面我们用个例子来说明一下 

这是西瓜书上的例子 ,我们根据一个西瓜的色泽、根蒂、敲声、纹理、触感等来判断它是不是一个好瓜。

该数据集包含17个训练样本,结果有两种,一是好瓜,二是坏瓜,所以|y|=2,其中正例(好瓜)占p_{1}=\frac{8}{17} ,反例(坏瓜)占p_{2}=\frac{9}{17},可以算出根结点的信息熵为

Ent(D) = - \sum_{k=1}^{2}p_{k}log_{2}p_{k} = -(\frac{8}{17}log_{2}\frac{8}{17}+\frac{9}{17}log_{2}\frac{9}{17}) = 0.998

以属性“色泽”为例其对应的3个数据子集分别为D^1(色泽= 青绿),D^2(色泽=乌黑),D^3 (色泽=浅白)

子集D^1包含编号为{1, 4, 6, 10, 13, 17} 的6个样例,其中正例占p_{1}=\frac{3}{6} ,反例占 p_{2}=\frac{3}{6}D^1的信息熵为:

 Ent(D^{1}) = - (\frac{3}{6}log_{2}\frac{3}{6}+\frac{3}{6}log_{2}\frac{3}{6}) = 1.000

 同理D^2D^3的信息熵为:

Ent(D^{^{2}}) = - (\frac{4}{6}log_{2}\frac{4}{6}+\frac{2}{6}log_{2}\frac{2}{6}) = 0.918

 Ent(D^{^{3}}) = - (\frac{1}{5}log_{2}\frac{1}{5}+\frac{4}{5}log_{2}\frac{4}{5}) = 0.722

那么,属性“色泽”的信息增益为

Gain(D,色泽) = Ent(D) - \sum_{v=1}^{3}\frac{|D^{v}|}{|D|}Ent(D^{v}) = 0.998-(\frac{6}{17}\times 1.000+\frac{6}{17}\times 0.918+\frac{5}{17}\times 0.722) = 0.109

 同理,我们可以计算出其他属性的信息增益

Gain(D,根蒂) = 0.143

Gain(D,敲声) = 0.141

Gain(D,纹理) = 0.381

Gain(D,脐部) = 0.289

Gain(D,触感) = 0.006

根据计算结果,我们可以看出属性“纹理”的信息增益最大,其被选为划分属性

继续对每个分支进行划分,计算出每个结点下各属性的信息增益,选择最大信息增益作为下次划分属性,最终得到的决策树如图:

2.2增益率

采用信息增益来进行划分属性的决策存在问题,当某一个属性的取值种类非常多是,对应每一个属性取值的样本子集,其分类的信息熵可能会变得非常小。例如,将“编号”作为一个候选划分属性,则“编号”的信息熵:

Ent(D^{v}) = - (1log_{2}1+0log_{2}0) = 0

显然,这样的决策树不具有泛化能力,无法对新样本进行有效预测。

 为了减少这种偏好带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而使用增益率来选择最优划分属性。

信息增益率定义为:

Gain_ratio(D,a) = \frac{Gain(D,a)}{IV(a)}

其中

IV(a) = -\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}log_{2}\frac{|D^{v}|}{|D|}

称为属性a的“固有值”(intrinsic value) [Quinlan, 1993]. 属性a的可能取值数目越多(即V越大),则IV(a) 的值通常会越大。

计算上述数据的信息增益率:

a1表示触感,a2表示色泽,a3表示编号 

 IV(a1) = -\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}log_{2}\frac{|D^{v}|}{|D|} = -(\frac{12}{17}log_{2}\frac{12}{17}+\frac{5}{17}log_{2}\frac{5}{17}) = 0.874IV(a2) = -\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}log_{2}\frac{|D^{v}|}{|D|} = -(\frac{6}{17}log_{2}\frac{6}{17}+\frac{6}{17}log_{2}\frac{6}{17}+\frac{4}{17}log_{2}\frac{4}{17}) = 1.580

 IV(a3) = -\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}log_{2}\frac{|D^{v}|}{|D|} = -(17\times \frac{1}{17}log_{2}\frac{1}{17}) = 4.088

增益率准则对可取值数目较少的属性有所偏好

C4.5采用了一个启发式方法:先从候选划分属性中找出信息增益高于平均水平的属性,再从中选取增益率最高的  

2.3基尼指数

分类问题中,假设D有K个类,样本点属于第k类的概率为p_{k},则概率分布的基尼值定义为:

Gini(D) = \sum_{k=1}^{K}p_{k}(1-p_{k}) = 1-\sum_{k=1}^{K}p_{k}^{2}

Gini反映了随机抽取两个样本,其类别标记不一致的概率

Gini(D)越小,数据集D的纯度越高

给定数据集D,属性a的基尼指数定义为:

Gini_{index}(D,a) = \sum_{v=1}^{V}\frac{|D^{v}|}{|D|}Gini(D^{v})

同样使用上述数据集,计算属性色泽的基尼指数

色泽好瓜数量坏瓜数量
青绿33
乌黑42
浅白11

v1青绿,v2乌黑,v3浅白

Gini(D^{v1}) = \sum_{k=1}^{K}p_{k}(1-p_{k}) = 1-\sum_{k=1}^{K}p_{k}^{2} = 1-(\frac{3}{6})^{2}-(\frac{3}{6})^{2} = 0.5

Gini(D^{v2}) = \sum_{k=1}^{K}p_{k}(1-p_{k}) = 1-\sum_{k=1}^{K}p_{k}^{2} = 1-(\frac{4}{6})^{2}-(\frac{2}{6})^{2} = 0.44

 Gini(D^{v3}) = \sum_{k=1}^{K}p_{k}(1-p_{k}) = 1-\sum_{k=1}^{K}p_{k}^{2} = 1-(\frac{1}{5})^{2}-(\frac{4}{5})^{2} = 0.32 

Gini_{index}(D,a) = \sum_{v=1}^{V}\frac{|D^{v}|}{|D|}Gini(D^{v}) = \frac{6}{17}Gini(v1)+\frac{6}{17}Gini(v2)+\frac{5}{17}Gini(v3) = 0.425 

 

三、代码实现 

以福建省选调生报考条件为例,选择其中三项:担任过一年以上学生干部,品学兼优,党员优先 判断学生能否得到学院的推荐名额,数据集如下:

学习成绩分为优秀(outstanding)和良好(good),结果有推荐(recommend)、犹豫(vacillate)、拒绝(refuse) 

3.1以信息增益划分属性/ID3

数据加载

#数据加载
def loadData( ):
    data = pd.read_csv("STUdata3.csv")
    dataset = data.values.tolist()

    #四个属性
    labels=['担任班干一年以上','学习成绩','是否党员','result']
    return dataset,labels

计算给定数据的香农熵 

#计算给定数据的香农熵
def calShannonEnt(dataset):
    numEntries = len(dataset)   #获得数据集函数
    labelCounts={}  #用于保存每个标签出现的次数
    for data in dataset:    
        #提取标签信息
        classlabel = data[-1]
        if(classlabel not in labelCounts.keys()):   #如果标签未放入统计次数的字典,则添加进去
            labelCounts[classlabel]=0
        labelCounts[classlabel]+=1  #标签计数
    shannonEnt=0.0      #熵初始化
    for key in labelCounts:
        p = float(labelCounts[key])/numEntries      #选择该标签的概率
        shannonEnt-= p*np.log2(p)
    return shannonEnt   #返回经验熵

根据某一特征划分数据集 

#根据某一特征划分数据集
def splitDataset(dataset,axis,value):
# dataset 待划分的数据集 axis 划分数据集的特征 value 返回数据属性值为value
    retDataSet = [] #创建新的list对象
    for featVec in dataset: #遍历元素
        if featVec[axis]==value:    #符合条件的抽取出来
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

选择最佳属性划分数据集 

#选择最佳属性划分数据集
def chooseBestFeatureToSplit(dataset):
    numFeatures = len(dataset[0])-1 #计算特征数
    baseEntropy = calShannonEnt(dataset)#计算信息熵
    bestFeature = -1    #最优特征的索引值
    bestInfoGain = 0   #信息增益
    for i in range(numFeatures): #不断循环属性
        featList = [example[i] for example in dataset] #获取数据集的第i个特征
        uniqueVals = set(featList) #属性i的属性值有哪些
        newEntropy = 0.0
        for value in uniqueVals:
            subDataSet = splitDataset(dataset,i,value) #按照属性i和属性i的值value进行数据划分
            prob = len(subDataSet)/float(len(dataset))
            newEntropy +=prob*calShannonEnt(subDataSet) #计算划分过数据集的信息熵
        infoGain = baseEntropy-newEntropy #计算信息增益,也就是信息熵的变换量
        #print("第%d个特征的信息增益为:%.3f" % (i, infoGain))
        if(infoGain>bestInfoGain):
            bestInfoGain = infoGain 
            bestFeature=i
    #print("最优索引值:"+str(bestFeature))
    return bestFeature  #返回信息增益最大特征的索引值
# 统计出现次数最多的元素(类标签)
def majorityCnt(classList):
    classCount={}  #统计classList中每个类标签出现的次数
    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]  #返回出现次数最多的类标签

创建并递归遍历该树 

def createTree(dataset,labels):#数据集和标签列表
    classList =[example[-1] for example in dataset]#数据所属类得值
    if classList.count(classList[0])==len(classList):#条件1:classList只剩下一种值
        return classList[0]
    if len(dataset[0])==1:#条件2:数据dataset中属性已使用完毕,但没有分配完毕
        return majorityCnt(classList)#取数量多的作为分类
    bestFeat = chooseBestFeatureToSplit(dataset)#选择最好的分类点,即香农熵值最小的
    labels2 = labels.copy()#复制一分labels值,防止原数据被修改。
    bestFeatLabel = labels2[bestFeat]
    myTree = {bestFeatLabel:{}}#选取获取的最好的属性作为
    del(labels2[bestFeat])
    featValues = [example[bestFeat] for example in dataset]#获取该属性下的几类值
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels2[:]#剩余属性列表
        myTree[bestFeatLabel][value] = createTree(splitDataset(dataset,bestFeat,value),subLabels)
    return myTree

添加主函数

if __name__ == '__main__':
    dataSet, labels = loadData( )
    print("数据集信息熵:"+str(calShannonEnt(dataSet)))
    mytree = createTree(dataSet,labels)
    print(mytree)

 运行结果

 

运行代码时发现树的分支不能过多,过多会报错list index out of range,这个问题暂时还没有解决

小结 

ID3优点:理论清晰,方法简单,学习能力较强

缺点:

  1. 信息增益的计算比较依赖于数目比较多的特征 
  2. ID3为非递增算法
  3. ID3为单变量决策树
  4. 抗糙性差

3.2以信息增益率划分属性/C4.5

只需要修改选择最佳属性划分数据集 的代码即可

# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1       #特征数量
    baseEntropy = calcShannonEnt(dataSet)   #计数数据集的香农熵
    bestInfoGain = 0.0                      #信息增益
    bestFeature = -1                        #最优特征的索引值
    for i in range(numFeatures):            #遍历数据集的所有特征
        featList = [example[i] for example in dataSet]          #获取dataSet的第i个所有特征
        uniqueVals = set(featList)                              #创建set集合{},元素不可重复
        newEntropy = 0.0
        splitInfo = 0.0                                        #信息熵
        for value in uniqueVals:                                #循环特征的值
            subDataSet = splitDataSet(dataSet, i, value)        #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet))        #计算子集的概率
            newEntropy += prob * calcShannonEnt((subDataSet))   #计算划分过数据集的信息熵 
            splitInfo -= prob * np.log2(prob)    
        infoGain = (baseEntropy - newEntropy)/splitInfo                   #求出第i列属性的信息增益率
        #print("第%d个特征的信息增益为%.3f" % (i, infoGain))      #打印每个特征的信息增益
        if (infoGain > bestInfoGain):                           #计算信息增益
            bestInfoGain = infoGain                             #更新信息增益,找到最大的信息增益
            bestFeature = i                                     #记录信息增益最大的特征的索引值
    return bestFeature                                          #返回信息增益最大特征的索引值

运行结果

 

小结 

C4.5算法与ID3算法的区别是主要有4点改进

  1. 采用信息增益率作为最优划分属性
  2. 能够处理连续值类型的属性
  3. 能够处理缺失值属性
  4. 增加了剪纸处理,从而避免过拟合 

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

import matplotlib
import matplotlib.pyplot as plt
 
# 定义文本框和箭头格式
decisionNode = dict(boxstyle="square", fc="0.8")  #boxstyle文本框样式、fc=”0.8” 是颜色深度
leafNode = dict(boxstyle="round4", fc="0.8")      #叶子节点
arrow_args = dict(arrowstyle="<-")                #定义箭头
 
# 绘制带箭头的注解
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    #createPlot.ax1是表示: ax1是函数createPlot的一个属性
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',xytext=centerPt,
                            textcoords='axes fraction',va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
 
# 获取叶节点的数目和树的层数
def getNumLeafs(myTree):
    numLeafs = 0                                      # 初始化
    firstStr = list(myTree.keys())[0]                 # 获得第一个key值(根节点)
    secondDict = myTree[firstStr]                     # 获得value值
    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]                      # 获得第一个key值(根节点)
    secondDict = myTree[firstStr]                          # 获得value值
    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': 'refuse', 'yes': {'学习成绩': {'outstanding': {'是否党员': {'no': 'vacillate', 'yes': 'recommend'}}, 'good': {'是否党员': {'no': 'refuse', 'yes': 'vacillate'}}}}}}]
    return listOfTrees[i]
 
# 在父子节点间填充文本信息
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 = list(myTree.keys())[0]     # 这个节点的文本标签
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff) #plotTree.totalW, plotTree.yOff全局变量,追踪已经绘制的节点,以及放置下一个节点的恰当位置
    plotMidText(cntrPt, parentPt, nodeTxt)                #标记子节点属性
    plotNode(firstStr, cntrPt, parentPt, decisionNode)
    secondDict = myTree[firstStr]
    plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD  #减少y偏移
    for key in secondDict.keys():
        if type(secondDict[key]).__name__ == '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()                                                       # 清空绘图区
    font = {'family': 'Songti SC'}
    matplotlib.rc("font", **font)
    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()

运行结果 

 

 

完整代码

链接: https://pan.baidu.com/s/1HvKRDVmlV0J7Q87DIPDrpA?pwd=qfyr 提取码: qfyr 

 

 

 

 

 

 

 

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值