决策树

决策树简介

很多人可能玩过或见过一个猜字游戏,游戏规则很简单,就是两个参与者,一个是提问者,一个是回答者,提问者可以不断的提问题,而回答者根据提问者的问题来回答是或不是,通过不断缩小猜测事务范围提问者最后确定了答案。

图1-1构造了一个假设的三好生分类系统,黑框代表判断模块,蓝框代表终止模块,表示已得出结论,可以停止运行。分类系统中,它首先判断学生是否尊敬老师,如果判断不是则归类到不是三好生,如果判断是再继续判断是否友爱同学,如果不是则归类到不是三好生中,如果是再判断成绩是否优异,如果不是则归类到不是三好生中,如果是则归类到时三好生中。

决策树的构造

在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性特征,划分出最好的结果,需要评估每个特征。完成测试之后,原始数据集会被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上,如果某个分支下的数据属于同一类型,则当前无需对数据集进行分割,如果数据子集内的数据不属于同一类型,则需要重复划分数据子集。

信息增益

划分数据集的原则是:将无序的数据变得更加有序,阻止杂乱无章数据的一种方法就是使用信息论度量信息,我们可以在划分数据之前或之后使用信息论量化度量信息的内容。在划分数据集之前之后信息发生的彼岸花称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的增益信息,获得信息增益最高的特征就是最好的选择。集合信息的度量方式称为熵,熵的定义为信息的期望值。如果待分类的事务可能划分在多个分类中,则符号xi 的信息定义为:

其中P(xi)为选择该分类的概率。

为了计算熵需要计算所有类别所有可能值包含的期望值,通过下面的公式得到:

其中n是分类的数目。

代码1-2为创建数据集和根据数据集计算熵

from numpy import *
from math import log
import operator


# 生成数据集
def createDataSet():
    dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
    lables = ['no surfacing', 'flippers']
    return dataSet, lables


def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    lableCounts = {}
    for featVec in dataSet:
        curLable = featVec[-1]
        lableCounts[curLable] = lableCounts.get(curLable, 0) + 1
    shannonEnt = 0.0
    for key in lableCounts:
        prob = float(lableCounts[key]) / numEntries
        shannonEnt -= prob * log(prob, 2)
    return shannonEnt
myData,lables = createDataSet()
calcShannonEnt(myData)

  

代码1-2创建数据集然后根据数据集计算熵的结果:

>>> myData,lables = createDataSet()
>>> myData
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> calcShannonEnt(myData)
0.9709505944546686  

熵越高,则混合的数据越多,我们可以在数据集中添加更多的分类,观察熵是如何变化的,这里我们增加第三个名为maybe的分类,测试熵的变化,如:

>>> myData[0][-1] = 'maybe'
>>> calcShannonEnt(myData)
1.3709505944546687  

 得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集。

 

划分数据集

分类算法除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。

在划分数据集时,我们需要用到Python语言列表类型自带的extend()和append()方法。这两个方法功能类似,但处理多个列表时,结果不同。

假定有a和b两个列表,如果执行a.append(b),则列表得到第四个元素,而且第四个元素也是一个列表。

>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> a.append(b)
>>> a
[1, 2, 3, [4, 5, 6]]

如果执行a.extend(b),则a列表包含b列表所有元素。

>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> a.extend(b)
>>> a
[1, 2, 3, 4, 5, 6]

  

代码1-3为按照给定特征划分数据集

def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] != value: continue
        reducedFeatVec = featVec[:axis]
        reducedFeatVec.extend(featVec[axis + 1:])
        retDataSet.append(reducedFeatVec)
    return retDataSet

 

通过代码1-3来划分数据集:

>>> myData,lables = createDataSet()
>>> myData
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> splitDataSet(myData, 0, 1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> splitDataSet(myData, 0, 0)
[[1, 'no'], [1, 'no']]

  

代码1-4为选择最好的数据集划分方式

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): continue
        bestInfoGain = infoGain
        bestFeature = i
    return bestFeature

  

代码1-4给出了函数chooseBestFeatureToSplit(),该函数实现了选取特征,划分数据集,计算得出最好的划分数据集特征,在划分数据之前,先计算整个数据集的原始熵,保证了最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。遍历当前特征中所有唯一的属性值,对每个特征划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。

调用代码1-4得到的结果:

>>> myData, lables = createDataSet()
>>> chooseBestFeatureToSplit(myData)
0

代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。

 

递归构建决策树

 构造决策树算法的原理:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大雨2两个分支的数据集划分。第一次划分之后,数据被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。

递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或终止模块。任何达到叶子节点的数据必然属于叶子节点的分类。

 

代码1-5为返回出现次数最好的分类名称

def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        classCount[vote] = classCount.get(vote, 0) + 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

  

代码1-6为创建决策树的代码

def createTree(dataSet, lables):
    # 类别相同则停止继续划分
    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)
    bestFeatLable = lables[bestFeat]
    myTree = {bestFeatLable: {}}
    del (lables[bestFeat])
    # 得到列表包含的所有属性值
    featValues = [example[bestFeat] for example in dataSet]
    uniqueValues = set(featValues)
    for value in uniqueValues:
        subLables = lables[:]
        myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables)
    return myTree

代码1-6的函数包含了两个输入参数:数据集和标签列表。标签列表包含了数据集中所有特征的标签。上述代码首先创建了一个名为classList的列表变量,用于存储数据集的所有类标签。递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类的类标签。递归函数的第二个停止条件是使用完使用的特征,仍然不能将数据集划分到仅包含唯一类别的分组,则将数据集进行投票,推选出票数最高的分类作为该类的类标签。

下一步开始创建树,当前数据集选取最好的特征存储在变量bestFeat中,得到列表包含的所有属性值。最后,代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用createTree(),得到的返回值将插入到字典变量myTree中,因此函数终止执行时,字典中将会潜逃很多代表叶子节点信息的字典数据。

调用代码1-6函数执行结果:

>>> myData, lables = createDataSet()
>>> myTree = createTree(myData, lables)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

  

代码1-7为获取叶子节点的树木和树的层数

# 决策树数据
def retrieveTree(i):
    listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
                   {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
                   ]
    return listOfTrees[i]


# 获取叶子节点的数目
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = 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 = 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

    

代码1-7中,两个函数都具有类似的结构。使用Python的type()函数可以判断子节点是否是字典类型,如果子节点是字典类型,则该节点需要递归调用getNumLeafs()函数,如果子节点是叶子节点,则对叶子节点数加1,并返回该值。getTreeDepth()同getNumLeafs()一样,会遍历所有的叶子节点,一旦到达叶子节点,则从递归调用中返回,并将树的层数加1,最后选取层数最大的值作为树的层数。

调用代码1-7的计算树的叶子节点和层数:

>>> myTree = retrieveTree(1)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
>>> getNumLeafs(myTree)
4
>>> getTreeDepth(myTree)
3

  

 代码1-8使用决策树的分类函数

def classify(inputTree, featLables, testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    # 将标签字符串转换为索引
    featIndex = featLables.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] != key: continue
        if type(secondDict[key]).__name__ == 'dict':
            classLable = classify(secondDict[key], featLables, testVec)
        else:
            classLable = secondDict[key]
    return classLable

  

代码1-8定义的函数也是一个递归函数,使用index方法查找当前列表中第一个匹配firstStr变量的元素,然后代码递归遍历整棵树,比较testVec变量中的值与树节点的值,如果到达叶子节点,则返回当前节点的分类标签。

调用代码1-8分类函数

>>> myData, lables = createDataSet()
>>> lables
['no surfacing', 'flippers']
>>> myTree = retrieveTree(0)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> classify(myTree, lables, [1, 0])
'no'
>>> classify(myTree, lables, [1, 1])
'yes'

  

代码1-9为本章代码整合

# coding:utf-8

from numpy import *
from math import log
import operator


# 生成数据集
def createDataSet():
    dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
    lables = ['no surfacing', 'flippers']
    return dataSet, lables


def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    lableCounts = {}
    for featVec in dataSet:
        curLable = featVec[-1]
        lableCounts[curLable] = lableCounts.get(curLable, 0) + 1
    shannonEnt = 0.0
    for key in lableCounts:
        prob = float(lableCounts[key]) / numEntries
        shannonEnt -= prob * log(prob, 2)
    return shannonEnt


def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] != value: continue
        reducedFeatVec = featVec[:axis]
        reducedFeatVec.extend(featVec[axis + 1:])
        retDataSet.append(reducedFeatVec)
    return retDataSet


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): continue
        bestInfoGain = infoGain
        bestFeature = i
    return bestFeature


def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        classCount[vote] = classCount.get(vote, 0) + 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]


def createTree(dataSet, lables):
    # 类别相同则停止继续划分
    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)
    bestFeatLable = lables[bestFeat]
    myTree = {bestFeatLable: {}}
    del (lables[bestFeat])
    # 得到列表包含的所有属性值
    featValues = [example[bestFeat] for example in dataSet]
    uniqueValues = set(featValues)
    for value in uniqueValues:
        subLables = lables[:]
        myTree[bestFeatLable][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables)
    return myTree


# 决策树数据
def retrieveTree(i):
    listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
                   {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
                   ]
    return listOfTrees[i]


# 获取叶子节点的数目
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = 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 = 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 classify(inputTree, featLables, testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    # 将标签字符串转换为索引
    featIndex = featLables.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] != key: continue
        if type(secondDict[key]).__name__ == 'dict':
            classLable = classify(secondDict[key], featLables, testVec)
        else:
            classLable = secondDict[key]
    return classLable

  

总结

决策树分类器就像带有终止块的流程图,终止块代表分类结果。开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一类。构建决策树时,我们通常采用递归的方法将数据集转化为决策树,一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字段存储树节点的信息。  

 

posted @ 2017-06-20 22:24  付辛乐  阅读(166)  评论(0编辑  收藏  举报