2. 决策树

这一章分为三部分:

  • 决策树的构造方法
  • 测试和存储分类器
  • 使用matplotlib画出决策树结构

1. 决策树的构造

根据数据集构建树的过程就是决策树算法的学习过程。那么该如何构造树呢?根据数据的特征来划分数据集,以构造树。

在构造决策树时,我们需要解决的第一个问题是,当前数据集上哪个特征在划分数据分类时起决定性作用(即根据该特征来划分效果最好,效果的评价在后面讲)。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。找到最佳特征后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点(就是选取的那个特征)的所有分支上。如果某个分支下的数据属于同一类型,则无需进一步对数据集进行分割。否则,需要在数据子集上重复进行划分数据集的过程。

书中使用的是ID3算法来划分数据集,每次使用一个特征来划分数据,如果该属性有4个可能的值,就把数据划分为4份。所以,要求特征的取值为离散值。

按照下面顺序讲解:

  • 数据集的熵
  • 按给定特征划分数据集
  • 使用信息增益来选择最好的划分方式
  • 递归构造决策树

1.1 数据集的熵

划分数据集的大原则是:将无序的数据变得更加有序。

而信息熵可以用于评价信息的有序程度。

这一节讲的是要通过信息增益来选择待划分的特征。关于熵和信息增益的概念可以看这里这里这里这里还有这里。如果根据某一特征划分数据集的信息增益最大,那么就使用该特征。

首先需要计算原始数据集的熵,再计算根据某一特征划分后的子集的条件熵。熵-条件熵就的到了信息增益,即划分后数据稳定了多少,稳定的越多,划分的效果就越好。

下面代码用于求给定数据集的熵:

from math import log
# 计算给定数据集的熵
# dataSet为二维列表,[[feature1,feature2,...,label],...]形式
def calcShannonEnt(dataSet):
    # 数据的个数
    numEntries = len(dataSet)
    labelCounts = {}
    # 统计各label出现的次数,也就是每一类有多少数据
    for featVec in dataSet:
        currentLabel = featVec[-1]
        labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1
        
    shannonEnt = 0.0
    # 计算整个数据集的熵
    for value in labelCounts.values():
        # 某一类在整个数据集中出现的概率
        prob = float(value) / numEntries
        # 求熵的公式
        shannonEnt -= prob * log(prob,2)
    return shannonEnt

1.2 按给定特征划分数据集

在求条件熵之前需要先根据某一特征划分完数据,下面代码完成这一操作:

# 按照给定特征划分数据集
# dataSet为二维列表,[[feature1,feature2,...,label],...]形式,待划分数据集
# axis为划分特征索引
# 如果特征取值为value则将该数据划分出来。
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            # 注意这里将数据的特征featVec[axis]去掉后,
            # 再将数据加入新的集合
            reducedFeatVec = featVec[:axis]
            # 这里注意不能使用append
            reducedFeatVec.extend(featVec[axis+1:])
            # 此时reducedFeatVec为去掉特征后的数据
            # 将其加入子集
            retDataSet.append(reducedFeatVec)
    # 返回的子集中的数据都是本身具有值为value的特征featVec[axis]
    return retDataSet

1.3 使用信息增益来选择最好的划分方式

下面的代码重复使用所有的特征来划分数据,并计算每一次划分后的信息增益,找到使划分后信息增益最大的那个特征。

# 选择最好的数据集划分方式(特征)
# dataSet为二维列表,[[feature1,feature2,...,label],...]形式,待划分数据集
# 最好的划分方式也就是使数据集更为有序,熵值也就越低
def chooseBestFeatureToSplit(dataSet):
    # 得到feature的个数
    numFeatures = len(dataSet[0]) - 1
    # 计算初始数据集的熵
    baseEntropy = calcShannonEnt(dataSet)
    
    # 记录最大信息增益和对应的特征索引
    bestInfoGain = 0.0; bestFeature = -1
    # 按各个feature来迭代划分数据集,找到使信息增益最多的那个划分
    for i in range(numFeatures):
        # 将所有数据的第i个feature存入list中
        featureList = [example[i] for example in dataSet]
        # 仅保留唯一feature值,划分的数据子集数也就是len(uniqueVals)
        # 就是得到数据集中该特征的所有取值。
        uniqueVals = set(featureList)
        # 用于计算子集条件熵
        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

1.4 递归构造决策树

既然是使用递归,那么首先就需要确定停止条件,这里有两个停止条件对应两种情况:

  • 每个分支下的所有实例都具有相同的分类,即该子集中的所有实例都属于一个类别了,此时不需要再继续划分,直接返回该类别。
  • 如果已经对所有的特征进行划分了,这时仍然不能区分一些实例,即这些实例的特征值都相同,但却有着不同的分类。此时采用多数表决来决定该子集或该分支的类别。

下面代码用于完成多数表决的功能:

"""
输入的是集合中所有实例的类别,也就是之前dataSet的最后一列
"""
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        classCount[vote] = classCount.get(vote, 0) + 1
    sortedClassCount = sorted(classCount.items(), 
                       key=operator.itemgetter(1), 
                       reverse=True)
    return sortedClassCount

递归的过程是,在createTree()中先判断是否符合停止条件,符合则返回类别,否则,进行下面的迭代过程:对于某一集合,通过函数chooseBestFeatureToSplit(dataSet)找到使划分后信息增益最大的那个特征,对于该集合中该特征的n个取值,循环划分该集合(循环的次数即为n),在每次循环中使用splitDataSet(dataSet, axis, value)根据该特征的取值进行一次划分得到一个子集(共有n个子集),然后再调用createTree()函数对该子集进行迭代划分。这样就相当于最先找到一个叶节点,之后返回再找到相邻的叶节点。假设根节点的度为5,那么先完成第一个分支的构建,之后再完成下一分支。。。。。

下面为创建树的代码:

# dataSet为二维列表,[[feature1,feature2,...,label],...]形式,待划分数据集
# labels为所有的特征名,也就是dataSet[i,:-1]各个列的名字
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]
    # 将该特征作为当前根节点来构建子树
    # 结构是多层嵌套的字典。
    # 如果key的value是类标签,则是叶节点
    # 若果是另一个字典,则是一个判断节点
    myTree = {bestFeatLabel:{}}
    # 下面就是根据该特征划分,该特征已经使用,将其从特征列表中删除,注意这里的删除是对原始labels中操作了
    del(labels[bestFeat])
    # 得到当前数据集中该特征的所有取值
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    # 根据特征取值来划分数据集,由于在循环中递归调用了本函数
    # 这样就会不断进行划分的过程,直到完全分完该数据集
    for value in uniqueVals:
        subLabels = labels[:]
        newDataSet = splitDataSet(dataSet, bestFeat, value)
        myTree[bestFeatLabel][value] = createTree(newDataSet, subLabels)
    return(myTree)

可见代码中使用深层嵌套的字典来代表树的结构。字典中有两种key,特征名称和该特征的取值。只有一种value,即类别。

下面代码构建一个简单的树:

# 一个简单的数据集
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

myDat,labels = createDataSet()
# 构建树
createTree(myDat, labels)
"""
结果为(缩进一下方便看):
{'no surfacing': {0: 'no', 
                  1: {'flippers': {0: 'no', 
                                   1: 'yes'}}}}
"""

2. 测试和存储分类器

使用构造好的树来对新实例进行分类。这里还是使用递归操作来查找类别:

# 使用决策树进行分类,还是递归操作
# inputTree为createTree()返回的树
# featLabels为各特征按顺序组成的列表,用于得到某一特征的索引
# testVec为待分类实例
def classify(inputTree, featLabels, testVec):
    # 传入的inputTree一定是个树或子树
    # 所以firstStr一定为某一特征名
    firstStr = list(inputTree.keys())[0]
    # 得到该特征的分支,secondDict.keys()为该特征的各个取值
    secondDict = inputTree[firstStr]
    # 该特征在特征列表中的索引
    featIndex = featLabels.index(firstStr)
    # key为该特征的各个取值
    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

上面代码有一个bug,如果输入实例的某一特征值在训练集中没有出现过,则代码会产生错误。修改如下:

# 使用决策树进行分类,还是递归操作
# inputTree为createTree()返回的树
# featLabels为各特征按顺序组成的列表,用于得到某一特征的索引
# testVec为待分类实例
def classify(inputTree, featLabels, testVec):
    # 传入的inputTree一定是个树或子树
    # 所以firstStr一定为某一特征名
    firstStr = list(inputTree.keys())[0]
    # 得到该特征的分支,secondDict.keys()为该特征的各个取值
    secondDict = inputTree[firstStr]
    # 该特征在特征列表中的索引
    featIndex = featLabels.index(firstStr)
    
    # 如果输入实例的对应特征的值在secondDict.keys()中,说明其可能属于该子集下的某一类别
    if testVec[featIndex] in secondDict.keys():
        # 如果该分支下面还有分支,则进行迭代
        if type(secondDict[testVec[featIndex]]).__name__ == 'dict':
            classLabel = classify(secondDict[testVec[featIndex]], featLabels, testVec)
        # 如果下面已经没有分支了,说明找到了对应的类别
        else:
            classLabel = secondDict[testVec[featIndex]]
    # 如果输入实例的该特征的值不存在于树中,则返回没有该类
    else:
        classLabel = 'no class'
    # 返回所属类别
    return classLabel

代码测试如下:

>>> myDat,labels = createDataSet()
>>> classify(myTree, labels, [1,2])
'no class'
>>> classify(myTree, labels, [1,0])
'no'
>>> classify(myTree, labels, [1,1])
'yes'

分类器的存储就是将树字典有pickle序列化存储,很简单。关于python中序列化操作的简介可看这里

本章介绍的决策树算法有很多的缺点,如对输入数据的过拟合,ID3算法无法处理数值型数据。在后面的章节中会介绍决策树的裁剪和CART算法来改正这些问题。

3. 使用matplotlib画出决策树结构

待续。。。。。。

4. 参考

关于决策树比较详细的总结在这里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 看到标题的同学可能会有点疑问,在数据挖掘十大经典算法里,ID3的名字并不在列。其实,ID3与位列十大经典算法的C4...
    Frankie8713阅读 1,947评论 0 1
  • 上篇文章介绍了K-近邻的分类方法,这篇文章介绍另外一种分类的方法:决策树。和K-近邻不同,决策树的方法包括了一个训...
    Warren_Liu阅读 745评论 0 0
  • 决策树理论在决策树理论中,有这样一句话,“用较少的东西,照样可以做很好的事情。越是小的决策树,越优于大的决策树”。...
    制杖灶灶阅读 5,832评论 0 25
  • 决策树 原理 《机器学习》周志华 4.1 基本流程 决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强...
    hxiaom阅读 482评论 0 0
  • 前言: 通过第前面的学习介绍了机器学习回归模型创建的流程,并且知道了机器学习要做的事情是找到目标函数,优化它,通过...
    飘涯阅读 6,366评论 4 83