机器学习实战(2)——决策树

一、概述

说起决策树算法,其原理理解起来很容易,但是具体操作起来却有几个难点(比如如何选择决定属性,信息熵和信息增益是怎么回事)

1、生活类比

初入机器学习的小白可能觉得决策树是一个高大上的名词,但是其原理却非常简单,在我们生活中我们经常会用到他,比如我们在团体竞赛活动中经常玩的二十个问题游戏: 参与游戏的一方在脑海中幻象某个事物,其他参与者通过向他提问,只允许提20个问题,问题答案也只能用对或错来回答,问问题的人通过推断分解,逐步缩小待猜测事物的大小范围。

2、大致原理

决策树的原理和上面的二十个问题非常类似,用户给出一个物体的一系列属性,然后通过其属性值的判断来进行分类:


上图是西瓜书中的一个例子,列举了西瓜的多个特征,然后依次向下排列,所有特征都符合就会分类出好瓜(当然这个决策树是事先根据训练集训练好的,已经训练出来(色泽为青绿,且根蒂为蜷缩,且敲声为浊响)的西瓜就是好瓜)

一些卖西瓜的为什么只用敲一敲西瓜,就能分辨出是否是好瓜,就是因为他的脑子里面有一棵决策树,他第一眼看到这个瓜,根据其色泽就开始插入到决策树中,然后再看其根蒂,最后敲一敲,根据响声脑海中就浮现出这个瓜的种类(是不是好瓜),他的脑海中的决策树是根据他长期的经验来形成的,这棵决策树一旦形成,以后就用不到之前的经验了,只要有这棵决策树,他就可以轻易判断一个新的西瓜是不是好瓜

我一直觉得这个决策树很像是符号流派的,就好像是专家系统一样,有经验的专家根据自己的经验来创造一系列的逻辑规则,而决策树在我看来就是大量逻辑规则的集成体,每个关键分叉属性的选择其实就是一个权重大小的问题

3、决策树的构造

扫描二维码关注公众号,回复: 2183868 查看本文章

数据集:100个西瓜样本

特征数量:色泽、根蒂与敲声(共三个特征集)

决策树在构造过程中,一开始只有一个根节点,这个根节点包含所有数据样本(100个西瓜样本),接下来你要选择第一个分叉节点(也就是这一百个西瓜,你要根据三个属性特征中的一个来划分),但是这三个属性,你要选择哪一个呢,一旦选择错了,你的分类就可能会错误,所谓一步错,步步错


决策树算法主体是一个递归过程,因为它本来就是一棵树的创建过程嘛(树的创建一般都是采用递归的,因为步骤大都是重复的)

上图中,我用两个圈将整个算法分开了,上面那个圈是决定算法递归结束(标出了叶子节点):

  • 当前节点包含的样本全属于同一类别,无需划分
  • 当前属性集为空,或是所有样本在所有属性上取值相同,无法划分
  • 当前节点包含的样本集为空,不能划分

下面的圈是划分的流程,其中最重要的就是第8行,如何选择出最优的划分属性,这个搞定了,其他一切都OK了

二、划分选择

在属性划分的时候,我们希望随着划分过程不断进行,决策树分支节点包含的样本尽可能属于同一类别,即节点的“纯度(purity)”越来越高

1、信息增益(ID3算法)

(1)信息熵

通过计算信息熵的增加大小来选择划分属性,信息熵是描述信息纯度的一种度量,是有香农发明的,所以有时候又被称为香农熵(信息论领域常用度量)


上面这个公式很重要,尤其是机器学习对应的自然语言处理领域中,应用非常广,注意:当P = 0的时候,公式划线部分为0

(2)信息增益公式


上面介绍的就是信息增益的公式含义了,举个例子吧,当前有个样本集合D是西瓜样本集合,D有三个特征属性:色泽、敲声和根蒂,上面的a则是指着三个属性中的一个,比如a为色泽,那么属性a就有多个取值:{青绿,翠绿,青色},上面公式中:

  • 红线对应的信息熵是指所有样本D的信息熵
  • 蓝线部分是指对于属性a的可能取值划分样本(比如a为色泽),蓝线就是样本集合D中所有色泽为青绿色的西瓜D1的信息熵,加上色泽为翠绿色的西瓜D2信息熵,加上所有色泽为青色的西瓜D3的信息熵(注意:D=D1+D2+D3)
  • 黑线部分是权重了,不同颜色占所有西瓜样本的比值

(3)信息增益的实际含义

信息增益实际含义是:当采用属性a进行划分后,所有样本的信息熵减少了多少,因为信息熵和纯度成反比,所以这里是增益。

一般而言,信息增益越大,则意味着用属性a来进行划分所获得的“纯度提升”越大,所以我们要计算西瓜当前剩余的特征属性的信息增益(这里要看已经划分好了几个分叉点,所以是剩余),取信息增益最大的属性来划分样本集合

注意:如果还是不太懂,可以看西瓜书,上面附有具体例子,下面附有西瓜书的电子版链接

也可以参考这个理解:【结合实例】信息增益的计算

2、增益率(gain ratio,C4.5算法)

(1)为什么会出现增益率

决策树中使用最广泛的还是信息增益,并且它也是最早开始出现的,但是信息增益有一个缺点:信息增益准则对于可取值数目较多的属性有所偏好

这个特点是由信息增益的公式决定的,也是它实际含义的一种体现,信息增益说白了就是:用哪个属性划分当前样本后,得到的样本纯度增加最多,那么哪个属性就是最好的,但是你想想,对当前样本进行划分,是不是划分组数越多,样本纯度相对来说增加越大呢,推到极限,每个小组只有一个样本,那么纯度就是最大的:100%(就好像让你分开混合在一起的十种液体,那么纯度最大的肯定就是,划分十组,每组一种液体,每种液体的纯度都是100%)

正是y由于信息增益的这个特点,有时候我们就不能采用信息增益来进行划分(就好像一个裁判很公正,但也很重私情,如果一场比赛中有他亲戚存在,虽然他很公正,但是却也会偏向他亲戚;如果没有他亲戚,他就会非常公正),在一个特征属性中,其属性值相对不多的时候,用信息增益非常有效的,但是如果一个属性有非常多的属性值,那么它就会非常偏向那个特征属性,这就不符合我们的预期了,这个时候就要结合信息增益率来决择了

(2)增益率公式


增益率和信息增益是相辅相成的,并不是说我采用增益率后,就和信息增益没有关系了,并不是这样,因为增益率会对可取值数目较少的属性有所偏好,就如同上图一样,增益率的计算公式中就有信息增益的公式

注意:我们采用增益率并不是直接就拿增益率最大的候选划分属性,而是使用一个启发式:先从候选划分属性中找出信息增益高于平均水平的属性,在从中选择增益率最高的。

3、基尼指数(Gini Index,CART决策树算法)

基尼值其实和信息熵一样,都是用来形容数据集纯度的,这里直接套用西瓜书的原话吧:


单从计算公式来看,信息熵和基尼值其实性质一样,都是和纯度成反比,他们只是两种不同算法的不同取值罢了,其实原理都一样

注意:一般来说,决策树中大都是采用信息增益的,后面的增益率和基尼值用的比较少(当然并不是前者就比后面两个一定好,主要是因为国内网上流行的大都是基于信息增益的源代码,国人大都比较懒,哈哈哈我也是,其他算法代码都比较少),如果你要深入了解决策树,建议还是通过想过论文,或者具体实现一下这三者,然后比较一下,再做出决定比较好。


三、实战代码

1、简单Python3实现

(1)决策树的生成

# -*- coding: UTF-8 -*-
import operator
from math import log  #计算信息熵要用到log函数

"""
函数0:创造数据集
好吧,这个函数又是最简单的一个数据集,下面还有一个数据集 
第二个数据集还好看一些
"""
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

def createDataSet1():    # 创造示例数据
    dataSet = [['长', '粗', '男'],
               ['短', '粗', '男'],
               ['短', '粗', '男'],
               ['长', '细', '女'],
               ['短', '细', '女'],
               ['短', '粗', '女'],
               ['长', '粗', '女'],
               ['长', '粗', '女']]
    labels = ['头发','声音']  #两个特征
    return dataSet,labels

"""
函数1:计算信息熵
一个样本信息熵越小表示样本纯度越高

"""
def calcShannonEnt(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   #标签数量加1


    #第三步:计算数据集的信息熵,主要是公式
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        shannonEnt -= prob * log(prob,2)
    return shannonEnt       #最后返回的是一个值



"""
函数3:分割数据集
就是将数据集dataSet按照axis这一特征来划分,如果样本的axis特征值为value
就将这个样本的属性除去axis,然后添加到返回样本中
dataSet相当于所有西瓜样本,axis相当于西瓜色泽、敲声、根蒂三个特征中的一个
比如axis是色泽,则value可以是青绿、黄绿、青色中的一个
splitDataSet(西瓜,色泽,青绿),返回的是当前样本中色泽是青绿色的所有西瓜
"""
def splitDataSet(dataSet,axis,value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:   #对所有样本进行选择,如果axis属性为value就选中
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            #上面去掉了axis属性,因为划分得到的样本都是该属性

            retDataSet.append(reducedFeatVec)
    return retDataSet  #返回的样本:去掉了axis属性,同时其axis全部为value


"""
函数4:选择最优划分属性
这也就是我们一开始说的决策树难点:如何选择划分属性
这里采用的是信息增益,信息增强越大则代表用这个属性划分越好
详细请参考信息增益公式,下面就是参照信息增益公式来写的
注意:最后返回的是一个数字,这个数字表示第几个数据特征
"""
def chooseBestFeatureToSplit(dataSet):

    #第一步:准备工作
    numFeatures = len(dataSet[0]) - 1  #数据特征数量,-1是除去标签那一列
    baseEntropy = calcShannonEnt(dataSet)  #计算数据集总的信息熵
    bestInfoGain = 0.0
    bestFeature = -1

    for i in range(numFeatures):   #针对每个数据特征都要进行操作

        # 第二步:计算信息增益=总信息熵-权重*每个属性信息熵
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)
        newEntropy = 0.0
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet,i,value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)
        infoGain = baseEntropy - newEntropy

        #第三步:找出所有信息增益中最大的,其对应特征即为所求
        if(infoGain>bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i

    return bestFeature  #返回最大信息增益的数据特征(这是一个数字)

"""
函数5:多票表决叶子节点的类别标签
当所有属性特征都是用完了以后,这个节点就是叶子节点了,但是并不是说这个节点是叶子节点
它包含的样本就都属于同一个类别,相反有时候会有很多类别,这时候我们要决定选择哪个类别作为标签
结论当然是类别数量最多的了
参数classList:叶子节点的所有样本的类别标签(注意有重复的)
"""
def majorityCnt(classList):
    classCount = {}    #用来存储类别和其对应的数量,格式为:{类别:数量}
    for vote in classList:
        if vote not in classCount.keys():  #如果字典中没有这个类,就增加,并且对应数量为0
            classCount[vote] = 0
        classCount[vote] +=1  #对应类别数量加1
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    #上面这个参数是字典按照其value值进行排序,是降序排列,所以第一个元素就是数量最多的类,也就是返回值

    return sortedClassCount[0][0]   #返回类数量最多的标签


"""
函数6:构建决策树
这个相当于是主函数,将上面所有函数都统筹起来,进行构建
当然,其中数据集和标签集可能要用到函数1(这里要另行调用了)
"""
def createTree(dataSet,labels,featLabels):

    #第一步:决定递归进程的结束和返回
    classList = [example[-1] for example in dataSet]
    #上面这句话要解释一下,for-in语句是将dataSet一行行赋值给example
    #然后又将example最后一项,也就是标签赋值给classList列表
    #说白了,就是将数据集所有标签制作成列表,然后赋值给classList

    #停止迭代1:classList中所有label相同,直接返回该label
    if classList.count(classList[0]) == len(classList):
        return classList[0]

    #停止迭代2:用完了所有特征仍然不能将数据集划分成仅包含唯一类别的分组
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)  #根据类数量最多的决定该节点的类标签


    #第二步:决定最优的分叉节点
    bestFeat = chooseBestFeatureToSplit(dataSet)  #这个就是算法第八行
    bestFeatLabel = labels[bestFeat]
    featLabels.append(bestFeatLabel)

    #第三步:根据上面的分叉节点,进行分组和分叉
    myTree = {bestFeatLabel:{}}
    del(labels[bestFeat])   #删除分叉节点的属性特征
    featValues = [example[bestFeat] for example in dataSet]  #将最优属性的特征值制成列表以此来分叉
    uniqueVals = set(featValues)   #列表转换为set集合
    for value in uniqueVals:
        subLabels = labels[:]       #copy all of labels, so trees don't mess up existing labels
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels,featLabels)
    return myTree

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

最终结果:


上面的Python代码和一开始西瓜书上的伪代码是一致的,大家可以先看那个伪代码,然后再看详细代码,这样更好理解。

(2)测试函数

"""
函数7:通过我们构建的决策树来测试一个新的样本
可以说这个函数是功能函数了,调用上面的决策树开始预测新的样本
参数inputTree:就是上面函数6生成的决策树
参数featLabels:就是函数0生成的labels标签数据集(无重复)
参数testVec:这是一个新的样本特征数据,通过这个,可以预测其标签
"""
def classify(inputTree,featLabels,testVec):
    firstStr = next(iter(inputTree))  #得到当前树的第一个划分属性值
    secondDict = inputTree[firstStr]  #根据上面的划分属性值,得到其对应的划分子树
    featIndex = featLabels.index(firstStr)  #得到划分属性对应的索引
    classLabel = '无'
    for key in secondDict.keys():   #遍历所有划分属性值
        if testVec[featIndex] == key:  #查看测试数据集划分属性对应的值
            if type(secondDict[key]).__name__ == 'dict':  #
                classLabel = classify(secondDict[key],featLabels,testVec)  #递归
            else:
                classLabel = secondDict[key]
    return classLabel


myDat,labels = createDataSet1()
featLabels = []   #因为下面了labels会在调用createTree函数之后,发生改变,所以这里创建一个新的用来盛放
myTree = createTree(myDat,labels,featLabels)
print(myTree)
print(classify(myTree,featLabels,['粗','长']))  #注意这里的粗和长的顺序是一定的,这和决策树有关系

输出结果:


注意:如果想要将这个算法解决具体问题,只需要将函数0:创造数据集那个代码更改一下就可以了。

四、后续处理

上面列出的算法只是最简单的版本,当然这个简单版本就已经可以解决大部分简单问题了,如果你要解决的问题还需要更高的精确度,那么就需要深入了解决策树算法,决策树自从诞生(上世纪60年代)到现在发展了数十年,可以说已经非常成熟了,其第一代就是以信息增益为准则的ID3算法,后来有衍生出了C4.5算法,同时对于预测变量的缺值处理、剪枝技术、派生规则等方面作了较大改进,既适合于分类问题,又适合于回归问题。

1、剪枝处理(pruning)

剪枝是决策树处理“过拟合”的主要手段;在决策树学习中,为了尽可能正确分类训练样本,节点划分过程将不断重复,有时会造成决策树分支过多,这时就可能因训练样本学的太好了,以至于把训练集自身的一些特点当做所有数据具有的一般性质而导致过拟合,因此要通过主动去掉一些分支类降低过拟合的风险(如果这里的过拟合没有看懂,请自行百度或者查看西瓜书,上面有非常形象生动的解释)

这个策略就好像现实生活中,有些树长得太茂密了,就会被剪枝,减掉一些小的枝杈,将所有营养都供给那些大的枝杈,这样才会结出更好的果实,或者长成真正的参天大树(又直又粗)

(1)预剪枝(prepruning)

在决策树生成过程中,对每个节点在划分前进行估计,若当前节点的划分不能带来决策树泛化性能提升,则停止划分并将当前节点标记为叶节点(也就是我要对这个节点进行划分了,那么我会比较一下划分完了得到的信息熵和没有划分的时候的信息熵,比较一下,如果减小了,那么我就划分,说白了就是有效果我就划分,没效果我就不划分)

(2)后剪枝(postpruning)

后剪枝从含义上就可以略懂一二,就是先通过训练集生成一棵完整的决策树,然后自底向上地对非叶节点进行考察,若将该节点对应的子树替换为叶节点能带来决策树的泛化性提升,则将该子树替换为叶节点

泛化性:这在机器学习中是一个非常重要的词,我们要训练一个算法模型,就是要得到一个泛化性能好的,什么叫泛化性能好的呢,说白了就是举一反三能力,相对现实生活中所有例子来说,我们得到的数据集总是有限的,但是我们要解决的问题总会出现在一些不在之前训练集中的例子,比如你通过题海战术学会了一套做题思路,那么给你出一道新题,让你通过这套思路去做,你能做出来吗?如果能做出来,说明你这套思路泛化性能很好,如果做不出来,这道思路只能解决之前已经做过的题,那就是泛化性能不好

注意:上面的两个策略在这里只是简要提了几句,让大家知道在决策树领域是有这个东西,如果想要深入了解,可以看西瓜书,当然了如果想要了解决策树领域最新的算法,就要看最新的论文了,已经著作成书的理论虽然经典,但是相比最新技术要晚上至少七八年

2、连续与缺失值

上面我们列举的实验代码中,那个数据集是最简单的,它有两个最主要的属性:离散和完整;所谓离散就是说他的特征属性的值都是离散的(比如第一个特征属性,不是0就是1,就这么两个值,而连续的则是这个属性可以去一段区间范围内的任意实数——无限的),而完整呢,是说它没有缺失任何一个属性

下面谈论一些如果解决决策树连续值和缺失值问题

(1)连续值处理

在现实生活中,我们遇到的值可以说不是连续的,就是离散的,当然连续的情况可能会非常多,那么我们如果解决这个问题呢,一般来说,最简单的思路就是:既然我只能处理离散的,那么我将连续的转化成离散的不就可以了嘛!

对,就是这样,将连续的值转换成离散的值,这样就可以了

举个例子吧:

给出一个人的身高、体重、肺活量三个特征属性,来判断这个人是否肥胖,那么问题来了,一个人的身高是连续的,从1米到2米之间有无数个取值可选,假设我们就选取身高为当前最优划分属性,那么我们怎么划分第一个分叉呢?

总不能第一个节点下面有无数个分叉吧,那样就太搞笑了,相信这个问题大家应该很容易想到答案,就是给1米-2米之间划分区间,比如划分四组:1-1.25;1.25-1.50;1.50-1.75;1,75-2.0,这样就可以分为四个叉,这样问题就解决了

具体思路:首先要将这段连续范围的值划分为几段,然后每一段取中间一个值来代替这一段即可(就好像全国代表大会是民主决策,但是全国人民太多了,无法全部参与,那么我们就将全国划分成一个个区域,然后每个区域选出一个代表来代替这个地区就可以了)




(2)缺失值处理

现实生活中,我们得到的数据集很多情况都是不完整的,因为没有一个人是全知全能的,总会有我们不知道的点,尤其是在属性数目很多的时候,往往会有大量的样本出现缺失值,如果简单地放弃不完整样本,那就太可惜了,因为这些缺失样本可能会占据整个数据集的很大一部分。

这一部分我就不搞了,看到一个博客写的还不错(虽然也是粘贴复制的):决策树(decision tree)(四)——缺失值处理

五、资源参考

1、西瓜书:链接:https://pan.baidu.com/s/1ozCZb-912fB2auyAGRcNmw 密码:3wg2

2、首先自然是《机器学习实战》这本电子书了,链接为:链接:https://pan.baidu.com/s/1nfJuwI2JQ6OAjM5Jbi7MOg 密码:l5xv(高清彩色版本)

3、这本书附带的源代码与数据集(这里的源代码是这本书附带的,不是我上面写的,有一部分是Python2格式):链接:https://pan.baidu.com/s/1mDqTlRVPAZBkok4E7ToVHQ  密码:162r

4、一些参考书:

用Python做科学计算:链接:https://pan.baidu.com/s/1hEwKT4k3jAqEDslla2L1eQ 密码:0k5l

笨方法学Python:链接:https://pan.baidu.com/s/1MKCKoRZjPV0Q4rQoa2LEpg 密码:enfk

流畅的Python:链接:https://pan.baidu.com/s/1Ln28HA3ITarp4sCPtT85VA 密码:elpc


猜你喜欢

转载自blog.csdn.net/yuangan1529/article/details/80872141