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

来源:互联网 发布:大麦盒子直播软件 编辑:程序博客网 时间:2024/04/30 05:48

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

算法的缺点:可能会产生过度匹配问题

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

决策树算法,顾名思义,就是通过不断决策来对某一数据样本进行分类,比如在对一个西瓜好坏的判断,我们不可能不假思索的直接说这个西瓜的是好还是坏,我们需要从西瓜的不同特征中进行逐一判断,从而得出出西瓜的好坏这一结论。在判断的过程中,我们一定是进行了一系列决策过程,如我们首先判断“这个西瓜是什么颜色”,比如看到是“青绿色”,从而再进一步判断“西瓜的根蒂是什么样的”,如是“蜷缩的”,从而再进一步判断“这个西瓜敲上去声音如何?”,我们用手敲一敲它发出了“清浊的声音”,从而我们可以认为这个西瓜是个好瓜。而决策树就是完全模拟这一系列判断过程。我们用图来大致表示这一过程

                                                     

决策树的一个重要任务是为了理解数据中所蕴含的知识信息。因此决策树可以使用完全不熟悉的数据集合,并从中提取一系列规则,这些机器根据数据集创建规则的过程,就是机器学习的过程。专家系统中经常使用决策树,而且决策树得出的结果可以匹敌当前领域具有几十年工作经验的人类专家。 这听上去是一件很厉害的事情。

决策树最初是在1979年由罗斯.昆兰发表,就是所谓的ID3算法。引起了各界人士研究决策树的热潮,一步一步优化,进而产生了所谓的ID4 ID5算法,这些优化主要在特征选取规则上的优化,本文主要来讲解ID3算法。

下面我们用python逐步构建决策树,在构建过程中有很多有趣的细节。

首先导入我们可能用到的库。

import matplotlib.pyplot as pltfrom numpy import *from math import logimport operator
写一个简单的测试数据集
#用于测试数据matdata=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]labels=['no surfacing','flippers']

这个数据集样本类别就是要么‘yes’要么‘no’ 两个特征分别是no surfacing和flippers,这两个特征取值要么0要么1

决策树是基于不同特征一层一层进行判断的,所以,第一步我们需要用一定的方法来用特征划分数据集,我们需要有一个规则来逐步选取决策树每一层的划分特征。

这里我们首先引入一个香农熵(信息熵)的概念。它是度量样本集合纯度最常用的一种指标

香农熵的计算公式:


k代表一个样本数据集D中每个类别,pk是某一类别占总样本数据集的比例

假设类别标签集合为D,其中有yes和no 其中p1为yes类别所占总样本的比例,p2是no类别所占总样本的比例,香农熵即为

下面我们还需要介绍一个信息增益的概念。假设数据集D中某一属性a,a的取值集合为{a1,a2,a3........,ak}如果按a这一属性划分数据集,那么我们得到k个数据集合,在决策树中就是k个分支节点,我们分别计算出每个分支结点下的香农熵,再乘以权重,即该分支结点下样本数目除以总样本数目。最后再用D的香农熵减不同节点香农熵乘以权重的求和。听上去是不是有点混乱,它的公式是这样的

我们用构造的简单样本数据来说明这个计算公式,如我们要计算以'no surfacing'这一属性来作为决策树第一次判断的标签。那我们需要计算它的信息增益,在ID3算法中,就是以信息增益最大的属性作为决策树第一次判断的标签。我们来计算no surfacing这一属性划分数据集的信息增益,既然以它划分我们就只看他,它可以取0或1,它取0的有

[0,1,no]和[0,1,no]在这个之中标签均为no即Ent(D1)便为1.它取1的为[1,1,yes]和[1,1,yes]和[1,0,no]这里就是一个站2/3一个占1/3之后带入计算即可。其实还是比较好理解为什么这么算的。

如果要问信息增益和信息熵这种东西到底是什么原理,那么我也不知道,但也不需要知道,你只要明白它是衡量信息纯度的一个量,因为它们从诞生的那一天起,就注定会让人费解,克劳德.香农写完信息论后,约翰.冯.诺依曼建议使用“熵”这个术语,因为大家都不知道它是什么意思。

克劳德.香农是被公认是二十世纪最聪明的人之一,有人说将香农与爱因斯坦相提并论对香农是不公平。。。好像扯远了

现在我们就可以开始编写计算香农熵的函数了

#计算香农熵def calcShannonEnt(dataset):    numEntries=len(dataset)    labelCounts={}#用一个字典存放个标签出现的次数,以便于计算Pk    for featVec in dataset:        currentLabel=featVec[-1]#一般数据集都是每一行最后一列来代表分类标签        if(currentLabel not in labelCounts.keys()):            labelCounts[currentLabel]=0        labelCounts[currentLabel]+=1    shannonEnt=0.0    for key in labelCounts:        prob=float(labelCounts[key]/numEntries)        shannonEnt-=prob*log(prob,2)    return shannonEnt
上面这段代码读起来应该很好理解,就不多做解释。我们用我们构造的测试数据来测试一下


即样本数据的香农熵为0.97095........

下面我们编写划分数据集的函数,决策树在某一个层分类后,那个分类过后的标签属性就不存在了,即不用考虑了,因为已经作出了判断,有点类似条件概率的感觉,所以每经过一次决策,数据集的行维度就会减少一维,下面函数的返回值便是一个列表list,代表新的数据集

#按照给定的特征划分数据集def splitDataSet(dataset,axis,value):    retDataSet=[]#这个列表用于存放新的数据集    for featVec in dataset:        if(featVec[axis]==value):            reducedFeatVec=featVec[:axis]            reducedFeatVec.extend(featVec[axis+1:])            retDataSet.append(reducedFeatVec)    return retDataSet
解释一下函数的三个参数,dataset为数据集,axis为用于划分的属性的列数,value为该属性下的返回值
下面这里我们对比一下[:axis]  [axis:]的区别 以及extend()与append()的区别,我们用运行结果来表示,就显而明了

现在很好理解这个函数中[axis+1:]和append与extend了

下面我们需要计算信息增益来来选择最好的数据集划分方式

#计算信息增益从而选取最好的特征作为数据划分方式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]#链表推导式,很好用的一个技巧,用于提取指定列的属性值并存放到列表中        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
上面程序注释的也很清楚,比较好理解,不在多做赘述。

下面我们需要来构建决策树了,在python中我们仍然用递归来构造决策树,用字典的嵌套来表示决策树,是十分方便的。

在构建决策树之前,我们需要想到一个细节,如果当所有属性都划分完了后,决策树的叶子仍然包含不同类别标签怎么办,这里我们可以采用k_邻近算法中的一些思想,就是选取出现频率最高的那个类别作为最终判定的类别。所以在递归的边界需要多一个if,这里根据出现频率来判定类别的程序如下:

#当所有属性均已划分完仍然无法完全归类时,采用取出现频率高的类别进行归类def majorityCnt(classlist):    classcount={}    for vote in classlist:        if(vote not in classcount.keys):            classcount[vote]=0;        classcount[vote]+=1    sortedClassCount=sorted(classcount.items(),key=operator.itemgetter(1),reverse=True)    return sortedClassCount[0][0]
这与k_邻近算法中取前k个中出现频率最高的类别的函数基本一样,不再赘述

下面我们可以构建决策树了

#创建决策树,用字典的嵌套来表示决策树def createTree(dataSet,Labels):    classlist=[example[-1] for example in dataSet]#得到类别列表    if(classlist.count(classlist[0])==len(classlist)):        return classlist[0]#当集合中类别均相同时便不可再划分,直接返回该类别标签    if(len(dataSet[0])==1):        return majorityCnt(classlist)#当属性划分完后,仍不是同一类别时,按出现次数最高的类别进行划分    BestFeat=chooseBestFeatureToSplit(dataSet)    bestFeatLabel=Labels[BestFeat]    mytree={bestFeatLabel:{}}#用于存放决策树的字典    del(Labels[BestFeat])#当按一属性划分后便将该属性去除    featValues=[example[BestFeat] for example in dataSet]    uniqueVals=set(featValues)    for value in uniqueVals:        subLabels=Labels[:]#用于调用函数的参数        mytree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,BestFeat,value),subLabels)#递归调用    return mytree
我们用样本测试一下运行结果


嗯,看上去没问题,第一个划分属性为no sufacing 如果为0即分类为no 如果为1则进一步以flippers这一属性进行划分,如果为0则为no如果为1则为yes

下面我们编写一个用决策树进行分类的函数

#通过决策树进行分类    def classify(inputTree,featLabels,testVec):    firstStr=list(inputTree.keys())[0]#第一次划分数据集的属性标签    secondDict=inputTree[firstStr]#剩下未进行划分的树    featIndex=featLabels.index(firstStr)#该属性在标签列表中的位置    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
解释一下函数的三个输入参数inputTree为训练好的决策树,featLabels为所有属性标签,testVec为待分类样本的各属性值

——————————————————————————————————————————————————————————————————————————

下面我们用一个实例来应用决策树进行分类,我们找到了一些关于隐形眼镜类型的样本,当我们知道相应人的一些眼部状况特征后,通过决策树来对他推荐合适的隐形眼镜,如英材质或软材质。

我们的训练数据样本如下:

young myopenoreducedno lenses
young myope no normal soft
young myope yes reduced no lenses
young myope yes normal hard
young hyper no reduced no lenses
young hyper no normal soft
young hyper yes reduced no lenses
young hyper yes normal hard
pre myope no reduced no lenses
pre myope no normal soft
pre myope yes reduced no lenses
pre myope yes normal hard
pre hyper no reduced no lenses
pre hyper no normal soft
pre hyper yes reduced no lenses
pre hyper yes normal no lenses
presbyopic myopenoreducedno lenses
presbyopic myopenonormalno lenses
presbyopic myopeyesreducedno lenses
presbyopic myopeyesnormalhard
前四列为咨询人员的特征,最后一列为向其推荐的隐形眼镜材质或不适合佩戴隐形眼镜

fr=open('lenses.txt')lenses=[inst.strip().split('\t') for inst in fr.readlines()]lensesLabels=['age','prescript','astigmatic','tearRats']lensesTree=createTree(lenses,lensesLabels)
读入数据并创建决策树,运行结果如下

Out[5]: 
{'tearRats': {'normal': {'astigmatic': {'no': {'age': {'pre': 'soft',
      'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}},
      'young': 'soft'}},
    'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses',
        'presbyopic': 'no lenses',
        'young': 'hard'}},
      'myope': 'hard'}}}},
  'reduced': 'no lenses'}}

由此我们输入相应前三个属性值便可以直接推荐出相应所需要的隐形眼镜类型